Compare commits

...

582 Commits

Author SHA1 Message Date
Max Brunsfeld
e60dea7049 collab 0.7.2 2023-03-13 10:11:44 -07:00
Antonio Scandurra
f6f09e8661 Merge pull request #2274 from zed-industries/leave-on-quit
Leave room on quit
2023-03-13 18:08:01 +01:00
Max Brunsfeld
ef7d8f46df Merge pull request #2273 from zed-industries/fix-dock-opening-in-collaboration
Fix dock opening on collaboration
2023-03-13 10:04:43 -07:00
Antonio Scandurra
bca1acf6d3 Leave room on quit
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-03-13 17:52:10 +01:00
Mikayla Maki
097a768725 Remove panic in recent projects picker 2023-03-13 09:44:42 -07:00
Mikayla Maki
404dd43c30 Fix dock opening on collaboration 2023-03-13 09:36:45 -07:00
Petros Amoiridis
c6f27903cc Merge pull request #2271 from zed-industries/petros/z-278-lock-pop-up-to-the-terminal-button
A new approach for positioning pop-up menus tied to an icon/button
2023-03-13 08:46:44 -07:00
Antonio Scandurra
f6b0c56a47 Merge pull request #2268 from zed-industries/mention-username-of-user-in-prompt-when-removing-a-contact
Mention username of user in prompt when removing a contact
2023-03-13 14:28:11 +01:00
Antonio Scandurra
e45d680126 Merge pull request #2272 from zed-industries/add-textmate-keymap
Add TextMate keymap
2023-03-13 14:26:32 +01:00
Antonio Scandurra
e993d32900 Merge pull request #2270 from zed-industries/add-default-keybindings-for-reveal-in-finder
Add default keybindings for reveal in finder
2023-03-13 14:25:26 +01:00
Antonio Scandurra
09911d43bc Merge pull request #2269 from zed-industries/unify-action-names-with-keybinding-names
Unify action names with keybinding name
2023-03-13 14:24:42 +01:00
Joseph Lyons
c8696149b8 Add TextMate keymap 2023-03-13 08:27:04 -04:00
Petros Amoiridis
db73d831c7 Use local overlay position mode and alignment
When we show the context menu, we don't specify an explicit position other than the default one which is equal to the origin in the context of a local overlay position mode. We then rely on `AnchorCorner` and aligning the context menu child view in the stack with `top().right()` for pop-ups that need to appear at the top of the icon/button.

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-03-13 12:50:12 +02:00
Petros Amoiridis
d2411a6c86 Merge pull request #2253 from zed-industries/joseph/z-226-add-terminal-popup-menu
Add terminal pop up menu
2023-03-13 02:47:44 -07:00
Petros Amoiridis
726c8eb43f Use type_id to determine what has the focus
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-03-13 11:42:40 +02:00
Joseph Lyons
c59dafab7e Update method names to match action names 2023-03-12 06:08:12 -04:00
Joseph Lyons
e272a1a18f Switch github_login to String
One of our macros, `impl_actions!`, didn't like that `RemoveContact` had a lifetime.
2023-03-12 05:03:31 -04:00
Joseph Lyons
90bca1b94a Add default keybindings for reveal in finder 2023-03-12 04:58:17 -04:00
Joseph Lyons
06ad3a7f7b Unify action names with keybinding name 2023-03-12 04:37:39 -04:00
Joseph Lyons
c18f1b6246 Mention username of user in prompt when removing a contact 2023-03-11 19:28:35 -05:00
Joseph Lyons
46efb844af Remove hardcoding of "Terminal" string 2023-03-11 16:27:00 -05:00
Petros Amoiridis
29f0078084 Show tooltip for Give Feedback icon 2023-03-11 17:40:47 +02:00
Petros Amoiridis
432aeeac56 Remove comment
I know @JosephTLyons you had added that, but I am not entirely sure this is how the view should be called. Let's discuss this further if you fill strong about it.
2023-03-11 17:22:21 +02:00
Petros Amoiridis
8440a98850 Activate terminal button when a terminal is focus
This is code I brought over from https://github.com/zed-industries/zed/pull/2267 by @mikayla-maki after fixing the conflicts.

Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-03-11 16:45:28 +02:00
Petros Amoiridis
bccc34c61a Merge branch 'main' into joseph/z-226-add-terminal-popup-menu 2023-03-11 16:42:42 +02:00
Max Brunsfeld
e8b3d4e0fa Encode db-max-connections env var as a string in k8s manifest 2023-03-10 17:19:16 -08:00
Max Brunsfeld
ff1c7db38f collab 0.7.1 2023-03-10 16:36:03 -08:00
Mikayla Maki
30a08467b0 Merge pull request #2263 from zed-industries/language-status-bar
Language status bar
2023-03-10 16:26:42 -08:00
Mikayla Maki
c8de738972 Align feedback button styles with other sidebar buttons
Make feedback button reflect whether you're in a feedback buffer
2023-03-10 16:19:33 -08:00
Mikayla Maki
87ac409e51 Merge pull request #2265 from zed-industries/fix-menu-bar-greying-out
Fix too-agressive menu bar greying
2023-03-10 16:05:07 -08:00
Mikayla Maki
badfe70a93 Merge pull request #2267 from zed-industries/fix-terminal-icon-active-state
Activate and deactivate the terminal icon based on focused view
2023-03-10 16:02:34 -08:00
Mikayla Maki
11d8394af2 Fix the terminal icon button to only be active when a terminal is focused 2023-03-10 15:54:42 -08:00
Mikayla Maki
c24194156e Merge pull request #2257 from zed-industries/check-all-contexts
Fix keyboard shortcuts not showing
2023-03-10 15:52:56 -08:00
Mikayla Maki
ece2af1e22 Fix a corner case in available action resolution
Add tests for new keybinding resolution behavior

co-authored-by: max <max@zed.dev>
2023-03-10 15:36:20 -08:00
Mikayla Maki
adf94a1681 Switch from using the key window to the main window mac platform API
When the help menu is open, the help menu's search field is the key window, and this was causing menu item action resolution to fail

co-authored-by: Max <max@zed.dev>
2023-03-10 14:43:28 -08:00
Julia
09d306df85 Merge pull request #2266 from zed-industries/avoid-panic
Avoid panic scanning recursive symlink before gitignore is encountered
2023-03-10 17:19:55 -05:00
Julia
0a5cf4b831 Avoid panic scanning recursive symlink before gitignore is encountered 2023-03-10 16:54:57 -05:00
Mikayla Maki
9398de6a57 Add feedback for telemetry item when there's no data 2023-03-10 13:15:36 -08:00
Mikayla Maki
e45104a1c0 Move feedback to overflow menu and help menu 2023-03-10 12:48:11 -08:00
Mikayla Maki
74b10e4ba5 Merge pull request #2264 from zed-industries/make-terminal-default-better
Make terminal fallback correctly when unable to deserialize a cwd
2023-03-10 12:27:54 -08:00
Mikayla Maki
ddbffd2c41 Make terminal fallback correctly when unable to deserialize your cwd 2023-03-10 12:23:29 -08:00
Mikayla Maki
00a38e4c3b Bound the search range for the keybindings by the highest handler path 2023-03-10 12:12:32 -08:00
Mikayla Maki
37d01c7fb3 Merge pull request #2199 from zed-industries/welcome-experience
Welcome experience
2023-03-10 10:48:30 -08:00
Mikayla Maki
281ff92236 Stub out blank pane experience 2023-03-10 09:58:11 -08:00
Antonio Scandurra
bb721a08f5 💄 2023-03-10 17:43:48 +01:00
Antonio Scandurra
f50b51bdad Adjust styling 2023-03-10 17:37:45 +01:00
Antonio Scandurra
693172854c Show active buffer's language on the right in the status bar 2023-03-10 17:37:34 +01:00
Antonio Scandurra
b3c7526fb5 Return last excerpt in MultiBuffer::excerpt_containing if overshooting 2023-03-10 17:28:08 +01:00
Antonio Scandurra
6e37ff880f Replace "Give Feedback" with an icon and move it to the left
This is so we can show the current language in the status bar on
the right, and having two pieces of text sitting next to each other
felt too busy.

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-03-10 17:02:52 +01:00
Antonio Scandurra
f08685f65f Merge pull request #2262 from zed-industries/manual-language-selection
Introduce language selector
2023-03-10 16:38:17 +01:00
Antonio Scandurra
ce828d55d5 Bind language_selector::Toggle to cmd-k m
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-03-10 16:32:18 +01:00
Antonio Scandurra
f28806d09b Emphasize currently-selected language 2023-03-10 15:48:39 +01:00
Antonio Scandurra
686f5439ad Set buffer language when confirming selection in language selector
Co-Authored-By: Julia Risley <julia@zed.dev>
2023-03-10 15:34:26 +01:00
Antonio Scandurra
b402f27d50 Introduce a new language picker that displays available languages
Right now this panics when trying to select a language, so that's
what we're going to implement next.

Co-Authored-By: Julia Risley <julia@zed.dev>
2023-03-10 15:28:10 +01:00
Antonio Scandurra
d39c761de5 Merge pull request #2261 from zed-industries/async-language-loading
Allow waiting for language to be loaded in LanguageRegistry APIs
2023-03-10 15:27:08 +01:00
Antonio Scandurra
7a600e7a65 Allow waiting for language to be loaded in LanguageRegistry APIs 2023-03-10 12:17:47 +01:00
Antonio Scandurra
221bb54e48 Introduce a new TryFutureExt::unwrap method 2023-03-10 11:41:13 +01:00
Antonio Scandurra
431e11a033 Merge pull request #2260 from zed-industries/increase-max-connections
Increase the amount of max connections to the database
2023-03-10 10:27:26 +01:00
Antonio Scandurra
8b7273e46e Increase the amount of max connections to the database 2023-03-10 10:10:59 +01:00
Mikayla Maki
9dc608dc4b Switch from changing the meaning of the predicate to adding an additional match_dispatch_path_context API for UI elements 2023-03-09 19:32:09 -08:00
Mikayla Maki
648f0e5b7b Remove new logo from style tree 2023-03-09 19:18:17 -08:00
Julia
b40ea4df14 Merge pull request #2259 from zed-industries/preserve-contacts-editor-contents
Preserve contacts popover editor contents when switching to search mode
2023-03-09 22:10:46 -05:00
Julia
01e3173ed0 Preserve contacts popover editor contents when switching to search mode 2023-03-09 22:05:32 -05:00
Nate Butler
8ee25be7b9 Update empty pane styling
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-03-09 20:18:29 -05:00
Mikayla Maki
0384456e7d Change context matcher to search the entire stack 2023-03-09 16:18:37 -08:00
Mikayla Maki
20064b5629 Add welcome to menu
remove debug
2023-03-09 15:38:28 -08:00
Mikayla Maki
daed75096e Fix editor test to clearly show trailing whitespace
Adjsut default dock size to be a multiple of 16
2023-03-09 13:18:03 -08:00
Mikayla Maki
718052bb72 Undo accidental indent change of hoverpopover.ts 2023-03-09 13:02:25 -08:00
Mikayla Maki
4eb75f058f Fix bug with wrong view ids being passed 2023-03-09 12:00:58 -08:00
Petros Amoiridis
6c68a3e709 Remove unneeded code 2023-03-09 20:02:40 +02:00
Nate Butler
e7af3f223a Merge pull request #2254 from zed-industries/nate/update-themes
WIP: More theme updates
2023-03-09 11:00:20 -05:00
Nate Butler
baff428de5 Re-add Ayu 2023-03-09 10:51:28 -05:00
Nate Butler
3952e98320 Remove Zed Pro theme 2023-03-09 10:50:11 -05:00
Nate Butler
51be0efa1f Format 2023-03-09 10:47:58 -05:00
Nate Butler
5bfd5e35b3 Add light themes 2023-03-09 10:47:01 -05:00
Petros Amoiridis
d53c18cc57 Open menu relative to the mouse cursor 2023-03-09 16:57:38 +02:00
Petros Amoiridis
5b7d0ee6fe Show button in a normal state 2023-03-09 16:54:47 +02:00
Petros Amoiridis
e2bdd261a1 Remove debugging statement 2023-03-09 16:53:34 +02:00
Mikayla Maki
9187863d0e re-add spaces removed by new setting 2023-03-09 00:45:05 -08:00
Antonio Scandurra
3daeabc1d6 collab 0.7.0 2023-03-09 09:30:04 +01:00
Antonio Scandurra
bebfe53e89 Merge pull request #2256 from zed-industries/liveness-probe
Introduce Kubernetes liveness probe to ensure database works
2023-03-09 09:26:21 +01:00
Antonio Scandurra
9328bb0153 Introduce Kubernetes liveness probe to ensure database works 2023-03-09 09:17:55 +01:00
Antonio Scandurra
89c283ecf0 Merge pull request #2252 from zed-industries/limit-messages-processed-in-parallel
Prevent collab server from being overwhelmed with messages
2023-03-09 08:51:48 +01:00
Antonio Scandurra
a00ce3f286 Add randomized test to remove active selections from buffer 2023-03-09 07:47:05 +01:00
Antonio Scandurra
4ce51c8138 Limit dirty buffer save optimization to multi-buffers 2023-03-09 07:26:22 +01:00
Mikayla Maki
f626920af1 Remove permanent Zed stateless 2023-03-08 19:03:50 -08:00
Mikayla Maki
325827699e Adjust styling for blank page experience 2023-03-08 19:02:13 -08:00
Julia
a3b1980a5e Merge pull request #2255 from zed-industries/sort-collaborators-alphabetical
Sort collaborators in titlebar alphabetically instead of by replica id
2023-03-08 21:54:29 -05:00
Mikayla Maki
709c101834 Adjust styles on usage note 2023-03-08 18:50:17 -08:00
Mikayla Maki
943ea61452 Add a note on how to check the telemetry 2023-03-08 18:47:52 -08:00
Julia
981b3a459f Sort collaborators in titlebar alphabetically instead of by replica id 2023-03-08 21:38:30 -05:00
Mikayla Maki
a65dd0fd98 Restore correct checkbox text 2023-03-08 18:15:29 -08:00
Mikayla Maki
cf6ea6d698 Fix bug with action keybindings not being resolved 2023-03-08 18:11:11 -08:00
Mikayla Maki
152755b043 Add blank pane experience 2023-03-08 17:56:39 -08:00
Mikayla Maki
dad66eb3fb Make the workspace always open the dock 2023-03-08 14:49:52 -08:00
Mikayla Maki
f62e0b502a Remove welcome experience action
Make logo switch between light and dark

co-authored-by: Nathan <nathan@zed.dev>
2023-03-08 14:49:48 -08:00
Nate Butler
344f59adf7 Tweak welcome design
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-03-08 17:14:15 -05:00
Nate Butler
cc33f83e4e Add Zed logo icon
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-03-08 16:45:35 -05:00
Nate Butler
9842b7ad1a WIP
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-03-08 16:34:27 -05:00
Max Brunsfeld
14497027d4 collab 0.6.2 2023-03-08 12:22:16 -08:00
Joseph Lyons
ae510c80db v0.77.x dev 2023-03-08 13:25:32 -05:00
Petros Amoiridis
ad7e49ed06 Give focus to the selected terminal
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
2023-03-08 19:47:57 +02:00
Antonio Scandurra
b687aec9d9 Avoid saving buffer if it's neither dirty nor in conflict
However, keep emitting `Saved` events so that the language server
is notified and can perform tasks related to saving (e.g., running
`cargo check` in the case of rust-analyzer).
2023-03-08 17:04:01 +01:00
Antonio Scandurra
a435dc1339 Clear selections on buffer only if they hadn't been cleared already 2023-03-08 17:04:01 +01:00
Antonio Scandurra
b4561b848d Limit the number of parallel messages handled for any given connection 2023-03-08 17:04:01 +01:00
Petros Amoiridis
baa9e271d5 Make pop up open on the side of the button 2023-03-08 11:00:30 +02:00
Mikayla Maki
350ddf2025 Add keymap picker UI
Co-authored-by: Max <max@zed.dev>
2023-03-07 17:29:08 -08:00
Mikayla Maki
3594243644 Fix bug where open would offer to hang up a remote call
Co-authored-by: max <max@zed.dev>
2023-03-07 17:29:08 -08:00
Mikayla Maki
904993dfc9 Change open paths to replace the existing window if dispatched from a window
co-authored-by: Max <max@zed.dev>
2023-03-07 17:28:52 -08:00
Julia
4179ed66a6 Merge pull request #2251 from zed-industries/clear-follow-state-on-project-close
Update db followers table when user leaves a project
2023-03-07 19:09:09 -05:00
Julia
d173b1d412 Update db followers table when user leaves a project
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-03-07 18:56:03 -05:00
Mikayla Maki
ab4b3293d1 Fix project panel button and style it
Co-authored-by: max <max@zed.dev>
2023-03-07 14:49:13 -08:00
Mikayla Maki
5892f16602 Add test for base keymap setting 2023-03-07 14:02:42 -08:00
Nate Butler
84aefb9dcb Add the rest of the Atelier dark themes 2023-03-07 13:47:18 -08:00
Nate Butler
4e81513af1 Add more Atelier themes 2023-03-07 13:35:32 -08:00
Nate Butler
90296667b0 Remove current staff themes 2023-03-07 13:35:23 -08:00
Nate Butler
e0f9b2b40f Update Atelier Dune 2023-03-07 13:27:01 -08:00
Nate Butler
477453c396 Update existing Atelier themes 2023-03-07 13:22:07 -08:00
Mikayla Maki
19fc143209 Add base keymap setting
Format all files

Co-Authored-by: Nathan <nathan@zed.dev>
2023-03-07 12:19:51 -08:00
Joseph Lyons
ca03d871a6 Add a separator between the New Terminal button and existing terminal buttons 2023-03-07 15:09:06 -05:00
Joseph Lyons
0a3f0c5252 Use terminal titles for buttons 2023-03-07 15:04:12 -05:00
Joseph Lyons
c80942ea00 Begin work to dynamically add terminal names to menu 2023-03-07 14:41:18 -05:00
Joseph Lyons
caa6a75238 Show a pop up menu for terminals
Co-Authored-By: Joseph T. Lyons <19867440+JosephTLyons@users.noreply.github.com>
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-03-07 20:16:08 +02:00
Julia
4f4af55329 Merge pull request #2248 from zed-industries/increase-reconnect-timeout
Increase room reconnect timeout
2023-03-07 12:30:20 -05:00
Julia
1e5aff9e51 Update collab integration test to new reconnect timeout 2023-03-07 12:23:18 -05:00
Nate Butler
ee154feda4 Merge pull request #2231 from zed-industries/nate/add-gruvbox
Add Gruvbox Theme
2023-03-06 18:49:26 -08:00
Mikayla Maki
3b31f10c6f Made the theme picker sort from dark to light
Added a layer into 'ConstrainedBox' to clip it
's children

Made the welcome experience responsive to small and large sizes
2023-03-06 18:36:18 -08:00
Mikayla Maki
8db7e17ac5 Move install_cli function to a seperate crate
Add install cli button to welcome experience
Add toast pop ups for CLI installation status
2023-03-06 17:55:58 -08:00
Mikayla Maki
1f6bd0ea77 Fix edge case where the welcome page might open in the dock if the user's actions race the welcome experience action 2023-03-06 16:35:15 -08:00
Mikayla Maki
ba652fc033 Finish basic welcome experience 2023-03-06 16:28:23 -08:00
Max Brunsfeld
7163ba429b Merge pull request #2250 from zed-industries/syntax-highlighting-tweaks
Syntax highlighting tweaks
2023-03-06 16:05:10 -08:00
Max Brunsfeld
c832e4406e Remove more colors from default syntax theme
These default colors weren't consistent with the rest of the
syntax theme.
2023-03-06 15:57:04 -08:00
Max Brunsfeld
515724821e Make racket highlight query more consistent with other langs 2023-03-06 15:56:20 -08:00
Max Brunsfeld
0867162c87 Fix lua highlight query
* Fix regex predicate on constants.
* Remove 'function.call' highlight name. In other languages, we
differentiate calls from definitions with the 'function.definition'
highlight name. We actually don't use this in any themes though.
2023-03-06 15:54:17 -08:00
Max Brunsfeld
aba2914a31 Fix constant highlighting in JS, TypeScript
Previously, SCREAMING_SNAKE_CASE identifiers were highlighted as
types due to a mistake in the order of patterns in the highlight
queries.
2023-03-06 15:38:12 -08:00
Max Brunsfeld
246a6adab7 Merge pull request #2239 from zed-industries/add-constructor-to-syntax-overrides
Add constructor to syntax overrides
2023-03-06 15:27:09 -08:00
Mikayla Maki
020a0965b0 WIP 2023-03-06 14:26:32 -08:00
Mikayla Maki
b74553455f Add an element to pane to take care of wiring initial mouse handlers 2023-03-06 14:26:32 -08:00
Mikayla Maki
4a8527478d Add child item alignment to flex implementation
Fix checkbox styling

co-authored-by: Nathan <nathan@zed.dev>
2023-03-06 14:26:32 -08:00
Mikayla Maki
4c179875ab Add png image loading to gpui
add zed logo into welcome experience

Co-authored-by: Nathan <nathan@zed.dev>
2023-03-06 14:26:32 -08:00
Mikayla Maki
f89f33347d Added CTA buttons to welcome experience
Co-authored-by: Nathan <nathan@zed.dev>
2023-03-06 14:26:32 -08:00
Mikayla Maki
9dee2ca2be WIP 2023-03-06 14:26:32 -08:00
Mikayla Maki
62aeb6b8b3 Added background to welcome page 2023-03-06 14:26:32 -08:00
Mikayla Maki
5210be95fe Added welcome experience sketch
Made toolbar hideable
2023-03-06 14:26:32 -08:00
Mikayla Maki
7d7053b990 Move to using stateless 2023-03-06 14:26:31 -08:00
Mikayla Maki
118435a348 Added basic styling for checkboxes, yay
Co-authored-by: Max <max@zed.dev>
2023-03-06 14:25:10 -08:00
Mikayla Maki
86e2101592 Added the ability to nested values to the settings file, while preserving user formatting
co-authored-by: max <max@zed.dev>
2023-03-06 14:25:10 -08:00
Mikayla Maki
50586812ec Make generate licenses quieter 2023-03-06 14:25:10 -08:00
Mikayla Maki
416c793076 Start on welcome experience settings 2023-03-06 14:25:10 -08:00
Mikayla Maki
a0637a769c WIP 2023-03-06 14:25:10 -08:00
Mikayla Maki
9401ef223d Add welcome crate and associated types 2023-03-06 14:25:10 -08:00
Julia
620890c411 Merge pull request #2247 from zed-industries/tidy-up-sprite-cache
Tidy up `SpriteCache::render_glyph`
2023-03-06 11:29:51 -05:00
Antonio Scandurra
0ec984f924 Tidy up SpriteCache::render_glyph 2023-03-06 17:08:35 +01:00
Max Brunsfeld
01bbf20962 Merge pull request #2235 from zed-industries/no-panic-uploads-in-debug
Don't upload panic files when running in a PTY
2023-03-04 09:42:46 -08:00
Mikayla Maki
996294ba67 Merge pull request #2246 from zed-industries/fix-lsp-derive-error
Make diagnostic processing order independent
2023-03-04 02:25:47 -08:00
Mikayla Maki
ddf2f2cb0a Update test to use new group ids and new ordering of diagnostics. 2023-03-04 02:21:55 -08:00
Mikayla Maki
bd4d7551a5 Make diagnostic processing order independent
Co-authored-by: max <max@zed.dev>
2023-03-03 16:35:12 -08:00
Julia
5097cf5cb7 Merge pull request #2245 from zed-industries/confirm-restart-unsaved
Confirm restart if prompt-quit is enabled or there are unsaved changes
2023-03-03 16:10:40 -08:00
Julia
13212d274e Confirm restart if prompt-quit is enabled or there are unsaved changes 2023-03-03 16:06:03 -08:00
Julia
b9110c9268 Increase reconnect timeout
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-03-03 13:10:08 -08:00
Joseph T. Lyons
b9573872e1 Merge pull request #2243 from zed-industries/joseph/z-225-track-open-terminals
Keep track of open terminals
2023-03-03 12:55:28 -08:00
Joseph Lyons
3ec71a742d Keep track of open terminals
Co-Authored-By: Petros Amoiridis <petros@hey.com>
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-03-03 12:50:08 -08:00
Mikayla Maki
50682dc685 Merge pull request #2233 from zed-industries/fix-code-folding-initialization
Initialize code fold indicators on buffer startup
2023-03-03 12:30:53 -08:00
Julia
2bca64f13b Merge pull request #2242 from zed-industries/mouse-event-handlers-yes
Fix surprising mouse propagation & avoid focusing tab while closing
2023-03-03 12:29:50 -08:00
Mikayla Maki
606d683f29 Add interactable fold markers
Change fold handlers to be driven by the fold map
Switch to a mouse region based implementation for click regions

Co-authored-by: Max <max@zed.dev>
2023-03-03 12:26:36 -08:00
Julia
ff2e6bc3bd Fix surprising mouse propagation & avoid focusing tab while closing
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-03-03 12:22:58 -08:00
Joseph T. Lyons
218f2fd0fe Merge pull request #2241 from zed-industries/joseph/z-223-add-terminal-button-to-status-bar
Add a terminal button to status bar
2023-03-03 12:15:59 -08:00
Max Brunsfeld
bb0257bbac Merge branch 'main' into no-panic-uploads-in-debug 2023-03-03 12:02:44 -08:00
Joseph Lyons
929ebd7175 Add a terminal button to status bar
Co-Authored-By: Petros Amoiridis <petros@hey.com>
2023-03-03 12:00:41 -08:00
Max Brunsfeld
124aa74b03 Merge pull request #2234 from zed-industries/error-on-combined-injections-in-injections
Fix range relativization when combined injections occur inside of other injections
2023-03-03 11:58:13 -08:00
Petros Amoiridis
a2f75eb031 Merge pull request #2240 from zed-industries/petros/update-setup-instructions
Add missing steps to the setup instructions
2023-03-03 11:47:38 -08:00
Petros Amoiridis
6194c5df16 Add missing steps to the setup instructions 2023-03-03 11:36:26 -08:00
Max Brunsfeld
d14b684237 Merge pull request #2236 from zed-industries/avoid-clobbering-panic-file
Avoid clobbering panic files when they happen at the same time
2023-03-03 11:15:48 -08:00
Max Brunsfeld
7a8cba0544 Merge pull request #2227 from zed-industries/strip-trailing-whitespace
Add settings to normalize whitespace on save
2023-03-03 11:15:32 -08:00
Nate Butler
f1b5bf051a Fornat 2023-03-03 10:58:43 -08:00
Nate Butler
ad4201f768 Hack to fix syntax.constructor causing TS error 2023-03-03 10:58:25 -08:00
Nate Butler
75a9cfdabe Remove leftover log 2023-03-03 10:22:50 -08:00
Julia
3b6f66791f Merge pull request #2238 from zed-industries/subpixel-variant-incorrectly-wrapping
Avoid wrapping to the 0th glyph variant when the 4th should be used
2023-03-03 13:13:34 -05:00
Julia
9311e01271 Avoid wrapping to the 0th glyph variant when the 4th should be used
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-03-03 10:04:44 -08:00
Nate Butler
6d068e926b Merge pull request #2237 from zed-industries/revert-2232-tab-bar-background-focus-drag
Revert "Avoid tab bar background activating an item at the end of a tab drag"
2023-03-03 09:49:55 -08:00
Nate Butler
6854063d0b Revert "Avoid tab bar background activating an item at the end of a tab drag" 2023-03-03 09:47:58 -08:00
Mikayla Maki
7ca0b38048 Made fold inline styles be driven by the fold map
co-authored-by: Max <max@zed.dev>
2023-03-03 09:32:58 -08:00
Max Brunsfeld
a598f0b13c Avoid clobbering panic files when they happen at the same time
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2023-03-02 20:51:29 -08:00
Max Brunsfeld
eb6088701e Don't upload panic files when running in a PTY 2023-03-02 20:32:49 -08:00
Mikayla Maki
24ba47e75d Merge pull request #2230 from zed-industries/fix-tab-x
Fix tab bar x
2023-03-02 19:48:08 -08:00
Mikayla Maki
3dd5b3f426 Attempted to initialize code-fold indicators, does not work 2023-03-02 19:42:59 -08:00
Mikayla Maki
9f86ca8574 Update tabBar.ts 2023-03-02 15:48:31 -08:00
Julia
bc2ea58c6a Merge pull request #2232 from zed-industries/tab-bar-background-focus-drag
Avoid tab bar background activating an item at the end of a tab drag
2023-03-02 16:18:04 -05:00
Julia
b343e8056a Avoid tab bar background activating an item at the end of a tab drag 2023-03-02 13:06:58 -08:00
Mikayla Maki
6a2a1303c4 Fix failing license
co-authored-by: nate <nate@zed.dev>
2023-03-02 12:32:47 -08:00
Max Brunsfeld
a366ba19af Fix range relativization when combined injections occur inside of other injections
For example, ERB template inside of a markdown code block

Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2023-03-01 14:38:35 -08:00
Max Brunsfeld
70cb2fa8d7 Apply external command formatting if buffer has changed while computing it 2023-03-01 10:17:04 -08:00
Nate Butler
f67c3f1f1d Add Gruvbox syntax overrides 2023-03-01 11:49:13 -05:00
Nate Butler
bde0456111 Init gruvbox theme 2023-03-01 10:16:45 -05:00
Mikayla Maki
8734bd8435 Seperate out x-mark width 2023-03-01 00:18:45 -08:00
Mikayla Maki
34fbffb4cc Fix tab bar x 2023-02-28 22:48:31 -08:00
Max Brunsfeld
368d2a73ea Perform whitespace formatting regardless of whether buffer has a language server or path 2023-02-28 21:52:00 -08:00
Max Brunsfeld
e7b56f6342 adjust buffer-formatting assertion to reflect final newline addition 2023-02-28 21:52:00 -08:00
Max Brunsfeld
1deff43639 Avoid calling edits_since in apply_diff 2023-02-28 21:52:00 -08:00
Max Brunsfeld
a890b8f3b7 Add a setting for ensuring a single final newline on save 2023-02-28 21:52:00 -08:00
Max Brunsfeld
7faa0da5c7 Avoid finalizing transactions inside Buffer::apply_diff 2023-02-28 21:52:00 -08:00
Max Brunsfeld
ff85bc6d42 Add setting for removing trailing whitespace on save 2023-02-28 21:52:00 -08:00
Max Brunsfeld
b00e467ede Add APIs for stripping trailing whitespace from a buffer 2023-02-28 21:52:00 -08:00
Mikayla Maki
2e1adb0724 Merge pull request #2229 from zed-industries/fix-click-range-bug
Fix off by one error in click ranges
2023-02-28 20:33:00 -08:00
Mikayla Maki
269df10a16 Fix off by one error in click ranges 2023-02-28 20:27:34 -08:00
Mikayla Maki
8358efbd6c Merge pull request #2228 from zed-industries/make-folds-show-on-active-lines
Made code fold indicators show up on active line indicators
2023-02-28 20:01:09 -08:00
Mikayla Maki
dc11d2726e Made code fold indicators show up on active line indicators 2023-02-28 19:53:53 -08:00
Mikayla Maki
41d3c5287b Merge pull request #2220 from zed-industries/add-fold-indicators
Add code folding indicators into the gutter.
2023-02-28 17:30:17 -08:00
Mikayla Maki
2198c295b3 Fix comments 2023-02-28 17:25:35 -08:00
Mikayla Maki
6cf62a5b02 Update tests to use new fold indicator 2023-02-28 17:17:29 -08:00
Mikayla Maki
f8401394f5 Removed DisplayRow abstraction 2023-02-28 17:12:48 -08:00
Mikayla Maki
b53d1eef71 Added background styling of the ... 2023-02-28 16:35:44 -08:00
Mikayla Maki
c397fd9a71 Added click regions and cursor styles 2023-02-28 16:34:28 -08:00
Mikayla Maki
9b8adecf05 Adjusted code-folding behavior 2023-02-28 16:34:28 -08:00
Mikayla Maki
e0f553c0f5 WIp 2023-02-28 16:34:28 -08:00
Mikayla Maki
37a2ef9d41 Make chevrons and lightning bolt interactive 2023-02-28 16:34:28 -08:00
Mikayla Maki
89b93d4f6f Added fold changes on gutter hover 2023-02-28 16:34:28 -08:00
Mikayla Maki
2036fc48b5 moved code action indicator to the left 2023-02-28 16:34:28 -08:00
Mikayla Maki
cb3e873a67 Fixed autoscroll timing 2023-02-28 16:34:28 -08:00
Mikayla Maki
da78abd99f Added DisplayRow abstraction to make folding code more readable 2023-02-28 16:34:28 -08:00
Mikayla Maki
637e8ada42 Fix bugs in code folding 2023-02-28 16:34:28 -08:00
Mikayla Maki
e3061066c9 Add code folding indicators into the gutter. 2023-02-28 16:34:28 -08:00
Nate Butler
514da604d7 Merge pull request #2223 from zed-industries/nate/extend-syntax
Extend syntax styles available in the theme
2023-02-28 11:26:47 -05:00
Nate Butler
b9811e48e4 One family tune up + tidy 2023-02-28 11:20:16 -05:00
Nate Butler
fb69611568 Fix order of template_substitution in highlights.scm 2023-02-28 11:19:55 -05:00
Nate Butler
a8a045e8bf Merge branch 'main' into nate/extend-syntax 2023-02-28 11:07:35 -05:00
Nate Butler
59bd503696 Tidy theme names 2023-02-28 10:55:08 -05:00
Nate Butler
fb7818f93c Revert changes to Andromeda 2023-02-28 10:50:03 -05:00
Joseph T. Lyons
3fb426e8b2 Merge pull request #2209 from zed-industries/add-link-to-community-repo-in-feedback-editor
Add link to community repo in feedback editor text
2023-02-28 09:03:21 -05:00
Kay Simmons
f0a31f86c7 remove commented line 2023-02-27 12:06:10 -08:00
Kay Simmons
dc7fe72f18 Merge pull request #2226 from zed-industries/fix-infinite-loop-in-path-distance
fix infinite loop in path distance for fuzzy finder
2023-02-27 12:03:40 -08:00
Kay Simmons
b3dffeaf2a fix infinite loop in path distance for fuzzy finder 2023-02-27 11:57:21 -08:00
Kay Simmons
81cbefec22 Merge pull request #2225 from zed-industries/vim-go-to-definition
add go to definition binding to vim normal mode
2023-02-27 11:10:23 -08:00
Joseph Lyons
4f9a07cffc Merge branch 'main' into add-link-to-community-repo-in-feedback-editor 2023-02-27 13:56:01 -05:00
Kay Simmons
184f37015a add go to definition binding to vim normal mode 2023-02-27 10:23:30 -08:00
Nate Butler
c9997a81a3 Move syntax types to syntax.ts 2023-02-27 11:01:32 -05:00
Nate Butler
df798c1a7f Remove punctuation.special until bug is fixed 2023-02-27 10:52:53 -05:00
Nate Butler
465fcec36d Format 2023-02-27 10:48:55 -05:00
Nate Butler
40c2409b80 Add missing variable property to Syntax 2023-02-27 10:47:52 -05:00
Nate Butler
46dc347a1a Bring andromeda in line with it's correct colors 2023-02-27 10:47:30 -05:00
Nate Butler
f84046b74f use @boolean in all flavors of js 2023-02-27 10:40:01 -05:00
Nate Butler
8c51a62a8d Unify regex highlight style under @string.regex 2023-02-27 10:08:24 -05:00
Nate Butler
794e6e22a6 Format 2023-02-26 17:12:48 -05:00
Nate Butler
504d88d56c Remove unused code 2023-02-26 17:12:11 -05:00
Nate Butler
94c76c45e6 Style One Dark with new properties 2023-02-26 17:11:02 -05:00
Nate Butler
f2d6a03dff Finish adding default properties 2023-02-26 17:10:52 -05:00
Nate Butler
3b19a409f8 Add some comments 2023-02-26 15:01:18 -05:00
Nate Butler
7854f4a1ef WIP need to finish default styles 2023-02-26 14:57:43 -05:00
Nate Butler
6cb35536b3 Start on syntax organization 2023-02-26 14:15:06 -05:00
Nate Butler
161373710c WIP 2023-02-26 13:24:41 -05:00
Joseph T. Lyons
11e2caff15 Merge pull request #2222 from zed-industries/update-app-icons
Update app icons
2023-02-26 11:07:07 -05:00
Joseph Lyons
36ada13966 Update app-icon@2x.png 2023-02-26 11:03:43 -05:00
Joseph Lyons
2c61eeb56d Update app-icon.png 2023-02-26 11:03:31 -05:00
Joseph Lyons
ccae9448d4 Update app-icon-preview@2x.png 2023-02-26 11:03:22 -05:00
Joseph Lyons
bb46b26494 Update app-icon-preview.png 2023-02-26 11:03:03 -05:00
Nate Butler
098e6969f7 Merge pull request #2221 from zed-industries/theme-syntax-overrides
Theme syntax overrides 🎉
2023-02-26 10:56:45 -05:00
Nate Butler
d910eed1f1 Format 2023-02-26 10:31:19 -05:00
Nate Butler
64b07dbfeb Add syntax overrides for One Light 2023-02-26 10:09:16 -05:00
Nate Butler
4f307c7601 Use syntax as merged name instead of mergedSyntax
This lets us retain the existing uses of syntax, and fixes colors that were being assigned incorrectly.
2023-02-26 01:20:44 -05:00
Nate Butler
23c967418a Add a syntax override style for One Dark 2023-02-26 01:07:45 -05:00
Nate Butler
77ed437cda Add the ability to override the system syntax config 2023-02-26 01:07:21 -05:00
Kay Simmons
0b1334b8c5 Merge pull request #2218 from zed-industries/file-finder-distance-sorting
Sort matches in file finder by distance to active item after score
2023-02-25 14:26:05 -08:00
Kay Simmons
cdc6566d87 fixup poor utility naming 2023-02-25 14:12:25 -08:00
Kay Simmons
36f3d3d738 Add test for new sorting behavior 2023-02-25 14:06:54 -08:00
Nate Butler
27712c25ef Merge pull request #2219 from zed-industries/nate/theme-tidying
Tidy `styles` app
2023-02-25 12:07:02 -05:00
Nate Butler
68af726ee4 Update packages
Tested post update.
2023-02-25 11:53:01 -05:00
Nate Butler
0ea7959ba4 Remove unused/old base16 theme code
Neither of these files have anything in them that is used anywhere else in the styles app.

Tested both `build` and `build-licenses` after removing these.
2023-02-25 11:50:22 -05:00
Nate Butler
bcb7b80517 Don't format package or package-lock 2023-02-25 11:47:27 -05:00
Nate Butler
10a30cf330 Format styles with updated prettier config
In the system color PR I updated the prettier config to match what we use on zed.dev. I didn't want to format all of styles as it would add a lot of unrelated line changes to that PR.

Doing that format now.
2023-02-25 11:46:33 -05:00
Nate Butler
06a86162bb Merge pull request #2150 from zed-industries/nate/system-colors
Add system color palette
2023-02-25 11:43:02 -05:00
Nate Butler
b986c38a31 Format using new prettier config 2023-02-25 11:33:57 -05:00
Nate Butler
69fd273367 Add the same prettier config as zed.dev 2023-02-25 11:33:16 -05:00
Nate Butler
8e828947fb Add missing dep 2023-02-25 11:32:04 -05:00
Nate Butler
2d8adf4c56 Remove theme tool for now
This is likely the biggest thing I'm unsure about for this PR, so I'm going to pull it into a seperate branch so I can merge the system color library in.

I think a better approach for this will be one of two things:
- Have a single next app that covers everything to do with themes (previewing ramps, generated themes, components etc.)
- Create a mono or turborepo that splits things up into packages (system, theme, themes, theme-tool, etc)
2023-02-25 11:27:18 -05:00
Kay Simmons
0b48e238f2 Sort file finder matches by distance to the active item after match score 2023-02-24 18:13:26 -08:00
Max Brunsfeld
04495aa8cd Merge pull request #2217 from zed-industries/format-on-save-trigger
Pass the 'Save' format trigger when formatting on save
2023-02-24 17:29:52 -08:00
Max Brunsfeld
5fea49e639 Pass the 'Save' format trigger when formatting on save
In an earlier refactor, I accidentally caused the 'Manual'
trigger to *always* be passed.

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2023-02-24 17:11:35 -08:00
Mikayla Maki
0704d9dcdb Merge pull request #2216 from zed-industries/update-bromberg
Update bromberg dependency to have alloc
2023-02-24 14:41:32 -08:00
Mikayla Maki
a57fcf5afc Update bromberg dependency to have alloc 2023-02-24 14:34:24 -08:00
Max Brunsfeld
e910fd8493 collab 0.6.1 2023-02-24 09:44:23 -08:00
Mikayla Maki
d5123bc832 Merge pull request #2215 from zed-industries/revert-2214-fix-reveal-path-panic
Revert "Remove borrow from reveal_path()"
2023-02-24 09:40:03 -08:00
Mikayla Maki
8656708de4 Revert "Remove borrow from reveal_path()" 2023-02-24 09:39:52 -08:00
Joseph Lyons
72197802a2 Tweak code to remove duplication 2023-02-24 08:53:58 -05:00
Joseph Lyons
f8f1a3f86e Unify text style names 2023-02-24 08:46:28 -05:00
Mikayla Maki
2ec25bef84 Merge pull request #2214 from zed-industries/fix-reveal-path-panic
Remove borrow from reveal_path()
2023-02-23 21:48:09 -08:00
Mikayla Maki
89ddf14b0e Remove borrow from reveal_path() 2023-02-23 21:36:17 -08:00
Julia
be86cb35ba Merge pull request #2213 from zed-industries/per-project-follow-status
Differentiate between follow state on a per-project basis
2023-02-24 00:26:41 -05:00
Julia
465d8cc2ff Differentiate between follow state on a per-project basis 2023-02-24 00:07:17 -05:00
Max Brunsfeld
93b9e762ec Merge pull request #2212 from zed-industries/initial-traffic-light-position
Adjust window's traffic light position when setting its title
2023-02-23 15:02:05 -08:00
Max Brunsfeld
fbc934b884 Adjust window's traffic light position when setting its title 2023-02-23 14:44:45 -08:00
Mikayla Maki
350b7b82f7 Merge pull request #2207 from zed-industries/project-panel-actions
Fix project panel actions
2023-02-23 14:31:29 -08:00
Mikayla Maki
b179fc2b99 Remove comment 2023-02-23 14:24:53 -08:00
Mikayla Maki
8860346324 Fix broken dock command 2023-02-23 14:15:29 -08:00
Mikayla Maki
9004640586 Convert keymap context to use generics and Cow<'static> so we don't have to add .to_string() and .into() for each usage 2023-02-23 14:10:55 -08:00
Kay Simmons
03498314fa Merge pull request #2211 from zed-industries/hover-binding
add hover binding from vscode
2023-02-23 14:05:28 -08:00
Kay Simmons
ce4b672a14 add hover binding from vscode 2023-02-23 13:50:43 -08:00
Kay Simmons
3f9405f8f1 Merge pull request #2210 from zed-industries/fix-enter-vim-normal-mode
Fix enter in normal mode acting incorrectly
2023-02-23 13:46:15 -08:00
Kay Simmons
2276d25bdf Fix enter in normal mode acting incorrectly 2023-02-23 13:40:31 -08:00
Mikayla Maki
ffe53bed87 Reverts keymap precedence order change 2023-02-23 13:32:45 -08:00
Joseph Lyons
37f910949d Add link to community repo in feedback editor 2023-02-23 16:30:00 -05:00
Julia
1e3b4f0387 Merge pull request #2208 from zed-industries/new-collab-ui-3
Add same grayscale logic to followers which leaders have; new call UI [3/N]
2023-02-23 16:05:42 -05:00
Julia
e1df85e86d Add same grayscale logic to followers which leaders have 2023-02-23 15:59:37 -05:00
Mikayla Maki
f6601f64e5 Added editor-in-project-panel overrides to the default keymap 2023-02-23 09:36:30 -08:00
Antonio Scandurra
6ccc90327c Merge pull request #2206 from zed-industries/fix-refresh-room
Fix error when deleting rooms containing projects on refresh
2023-02-23 16:03:14 +01:00
Antonio Scandurra
bbeb33bc7e Fix error when deleting rooms containing projects on refresh
A foreign key violation was causing the server to never delete stale
rooms during `Database::refresh_room` due to having one or more project
records referencing the room.
2023-02-23 15:54:35 +01:00
Antonio Scandurra
e74db2d180 Merge pull request #2205 from zed-industries/call-ui-follow-up
Refine new call UI
2023-02-23 15:39:59 +01:00
Antonio Scandurra
74e0bed38f Fix compilation errors after restructuring room_transaction 2023-02-23 15:17:22 +01:00
Antonio Scandurra
832549f1a3 Merge branch 'main' into call-ui-follow-up 2023-02-23 15:15:46 +01:00
Antonio Scandurra
b965333325 Show avatar in user menu 2023-02-23 15:09:32 +01:00
Joseph T. Lyons
2be0283bf2 Merge pull request #2204 from zed-industries/fix-description-of-telemetry-setting
Fix description of telemetry setting
2023-02-23 08:59:30 -05:00
Antonio Scandurra
59a66190e5 Avoid trying to reconnect to a room if client is signed out 2023-02-23 14:53:10 +01:00
Antonio Scandurra
9334267bd0 Tear down peer when signing out 2023-02-23 14:47:02 +01:00
Antonio Scandurra
a0daf47134 Don't panic when rendering collab titlebar item while signed out 2023-02-23 14:46:02 +01:00
Petros Amoiridis
9a729a2e64 Merge pull request #2202 from zed-industries/petros/z-86-replace-terminal-tab-title-with-an-icon
Replace terminal tab title with an icon
2023-02-23 11:04:43 +02:00
Antonio Scandurra
1c636500de Merge pull request #2200 from zed-industries/fix-slow-project-join
Hold room lock through the entirety of a `room_transaction`
2023-02-23 09:11:58 +01:00
Max Brunsfeld
65a9ac449f Remove leave button from the title bar 2023-02-22 23:30:32 -08:00
Max Brunsfeld
bf5c3d963a Rearrange collab titlebar items to avoid movement of the toggle contacts button
* Replace username in titelbar with a `...` user menu that shows
  the current user name and contains a sign-in/sign-out button.
* Move the '+' (toggle contacts) button back to the right side.
* Move the collaborators back to the right side.
* Move the share/unshare button to the left side, beside the project title
* Only show the share/unshare button when in a call.
2023-02-22 22:56:11 -08:00
Joseph Lyons
c33d0f940a Fix description of telemetry setting 2023-02-22 23:50:20 -05:00
Max Brunsfeld
24e0a027ee Run check-formatting CI job on a mac mini 2023-02-22 15:35:05 -08:00
Max Brunsfeld
d49e35f947 Merge pull request #2203 from zed-industries/collab-ui-fixes
Fix minor issues with new collab UI
2023-02-22 14:22:05 -08:00
Max Brunsfeld
40aee8d7bc Add missing tooltip for contacts menu button
Co-authored-by: Joseph Lyons <joseph@zed.dev>
2023-02-22 14:18:17 -08:00
Max Brunsfeld
d33d27faa4 Fix ToggleContactsMenu action name in keymap
Co-authored-by: Joseph Lyons <joseph@zed.dev>
2023-02-22 14:17:59 -08:00
Max Brunsfeld
46ead28971 Bump RPC protocol version number 2023-02-22 13:40:14 -08:00
Max Brunsfeld
111aff29cc collab 0.6.0 2023-02-22 12:35:15 -08:00
Max Brunsfeld
e2a2e40599 v0.76.x dev 2023-02-22 12:34:29 -08:00
Max Brunsfeld
b73423daaa Merge pull request #2114 from zed-industries/new-collaboration-ui
New collaboration UI part 1/N
2023-02-22 12:32:41 -08:00
Julia
0324ca3b08 Be more specific about clearing (leader, follower) row
Previously anyone unfollowing someone would clear all other rows for
other followers leading to an incorrect state, fix and test

Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-02-22 15:29:20 -05:00
Julia
36040cd0e1 Add top level leave call button
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-02-22 14:44:23 -05:00
Julia
a07867d628 Fiddle with titlebar item spacing
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-02-22 14:13:29 -05:00
Julia
812145f9ab Only show in-call share/unshare button if own project
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-02-22 13:40:43 -05:00
Julia
dbe5b0205c Add style leader selection container
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-02-22 13:36:06 -05:00
Petros Amoiridis
3d6c81584f Add an icon to the terminal view tab
The terminal icon already existed in `assets/icons`

Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>
2023-02-22 20:30:44 +02:00
Petros Amoiridis
81ece4fd44 Deduplicate tab theme related code
We've noticed that the search theme struct had two fields for a tab icon width and spacing. But we already have those in the tab theme struct. We decided to remove the duplicate and reuse the tab fields.

We also wanted to move where the spacing is being used. Instead of doing it at the left of the label, we do it at the right of the icon to match how it is done in other areas of the UI.

Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>
2023-02-22 20:28:57 +02:00
Petros Amoiridis
2ec5c88f98 Make icon width match other areas
This was 8 but we've seen areas where this was 14, like the project search tab icon. We want to match this.

Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>
2023-02-22 20:25:39 +02:00
Mikayla Maki
7b559176f1 Merge pull request #2201 from zed-industries/save-shortcuts
Add OS UI Keybindings
2023-02-22 09:16:19 -08:00
Mikayla Maki
d7305077bf Merge pull request #2198 from zed-industries/more-item-defaults
Add more default impls to the item trait
2023-02-22 09:14:12 -08:00
Mikayla Maki
4798b72cb8 Fixed keyboard shortcuts in mac os native panels
co-authored-by: Antonio <antonio@zed.dev>
2023-02-22 09:10:16 -08:00
Mikayla Maki
71d8ead318 Introduce an OSAction that can be associated with menu items for mac platform compatibility.
Co-authored-by: Antonio <antonio@zed.dev>
2023-02-22 09:02:31 -08:00
Julia
9b92a8e3fe Add mockup accurate avatar background
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-02-22 11:34:55 -05:00
Julia
7f4da80386 Initial dedicated face pile element
Rather than overload Flex with yet another special case, add a dedicated
element to handle this overlap instead

Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-02-22 11:34:55 -05:00
Julia
6a731233c5 Remove Flex::with_reversed_pain_order 2023-02-22 11:34:55 -05:00
Max Brunsfeld
b7cf426908 Start work on styling of follower avatars in facepiles
* Make follower avatars smaller than top-level avatars
* Make avatars in facepiles overlap
* Render an opaque background behind avatars in facepiles.
2023-02-22 11:34:55 -05:00
Max Brunsfeld
0dc92bec5c Retrieve room id from the project when following/unfollowing
Previously, we were accidentally using the project id as the room id.
2023-02-22 11:34:55 -05:00
Julia
c75aca25b6 Remove left side collaborator list
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-02-22 11:34:55 -05:00
Julia
ae87961a77 Close contacts popover when call ends
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-02-22 11:34:55 -05:00
Julia
e9464815e0 Make tooltip lie less 2023-02-22 11:34:55 -05:00
Julia
1ed47663ef Avoid moving contacts popover during call start & add button style state
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-02-22 11:34:55 -05:00
Julia
dd02bc7748 Initial adding of contact menu to call-less share button 2023-02-22 11:22:37 -05:00
Julia
e403b868b7 Add followers table to sqlite scheme for tests
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-02-22 11:22:37 -05:00
Julia
3105ecd0bd Shuffle share/unshare button back to the right side 2023-02-22 11:22:37 -05:00
Julia
05e9615507 Highlight face pile which local user is following 2023-02-22 11:22:37 -05:00
Julia
1abb7794cb Handle case where follower is local user 2023-02-22 11:22:37 -05:00
Julia
50e681bbb1 Add username to right side 2023-02-22 11:22:37 -05:00
Julia
3fb8395085 Make things a bit more infallible 2023-02-22 11:22:37 -05:00
Julia
4513c40993 Following face piles finally take their first breath 2023-02-22 11:21:23 -05:00
Julia
4ffc8cd9fd Fix deadlock in db get_room
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-02-22 11:21:23 -05:00
Julia
33c265d3cf Abuse a closure instead of abusing options/iterators quite so much 2023-02-22 11:21:23 -05:00
Julia
58c41778e7 Absolute pain of the iterator kind (start laying out a user's followers) 2023-02-22 11:21:23 -05:00
Julia
2592ec7265 Initial tracking of unfollows on collab server 2023-02-22 11:21:23 -05:00
Julia
d6462c611c Begin tracking follow states on collab server
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-02-22 11:21:23 -05:00
Julia
28786a3c18 Add Flex with_reversed_paint_order & initially move face piles to left
Co-Authored-By: Petros Amoiridis <petros@zed.dev>
2023-02-22 11:21:23 -05:00
Julia
a5fd0250ab Start fleshing out layout of collaborator list entries
Co-Authored-By: Petros Amoiridis <petros@zed.dev>
2023-02-22 11:21:23 -05:00
Julia
f68eda97fb Tell project when it has been unshared
Co-Authored-By: Petros Amoiridis <petros@zed.dev>
2023-02-22 11:21:23 -05:00
Julia
99236f1875 Add collaborators to collaborator list, including self user 2023-02-22 11:21:23 -05:00
Julia
bf8658067f Add collaborator count to collaborator list button 2023-02-22 11:21:23 -05:00
Julia
c697c1a96a Switch collaborator list to using own style 2023-02-22 11:21:23 -05:00
Julia
2b6aa3f5d1 Begin adding collaborator list popover
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-02-22 11:21:23 -05:00
Julia
e96d52f35a Move share button and support unsharing 2023-02-22 11:21:23 -05:00
Julia
ed2f1ddd2d Move workspace title into collaboration titlebar item render 2023-02-22 11:21:11 -05:00
Antonio Scandurra
8dd249a7cd Hold room lock through the entirety of a room_transaction
Previously, when the host repeatedly sent `UpdateWorktree` messages,
new guests attempting to join a project would observe a severe slowdown
caused by a database serialization error (e.g., the coherence of the data
would get violated midway through `Database::join_project` due to worktree
entries being mutated as the user joined). Writing entries is pretty fast,
whereas reading all of them for a project can take more than 100ms.
Transactions that failed due to a serialization error are retried, but the guest
would keep retrying until the host finished writing because the guest's read
was slow.

This commit changes the semantics of `room_transaction` to acquire a room
lock before even starting the transaction and holding it all the way after
commit (storing it, as before, in the `RoomGuard`). This ensures that a fast
writer (the host) can't starve a slow reader (the guest), allowing the latter
to make progress by temporarily pausing writes by the former.
2023-02-22 16:04:29 +01:00
Kay Simmons
24fcad3fa2 Merge pull request #2189 from zed-industries/labeled-tasks
Labeled Tasks
2023-02-21 21:31:47 -08:00
Kay Simmons
46af9a90ce fix test warning 2023-02-21 21:13:03 -08:00
Kay Simmons
1c69e289b7 Fix formatting 2023-02-21 21:07:45 -08:00
Mikayla Maki
9d782be4c8 Remove now-default stubs 2023-02-21 17:51:49 -08:00
Mikayla Maki
cae9e733a1 Add more default impls to the item trait
Change pane to not split if the underlying item cannot be cloned
2023-02-21 17:29:39 -08:00
Mikayla Maki
77c396a0ab Merge pull request #2197 from zed-industries/label-text-cow
Changed label and text to be generic over static and owned strings
2023-02-21 17:05:57 -08:00
Mikayla Maki
b500ed3171 Changed label and text to be generic over static strings and owned strings 2023-02-21 16:47:29 -08:00
Kay Simmons
6b6e4e3bfe Add basic test for labeled tasks 2023-02-21 16:14:22 -08:00
Joseph T. Lyons
1683a54698 Merge pull request #2195 from zed-industries/add-reveal-in-finder-to-additional-locations 2023-02-21 18:48:16 -05:00
Mikayla Maki
14488619a3 Merge pull request #2196 from zed-industries/open_urls
Fix open URLs, restarts, and make bundling easier to use
2023-02-21 15:36:50 -08:00
Mikayla Maki
cf4e719484 Fixes a race condition in the restart implementation
Fixes open_urls racing workspace initialization and causing a double-open (community#927)
Adds a -d flag to the bundle script to compile in debug mode

Co-Authored-by: Max <max@zed.dev>
2023-02-21 15:28:16 -08:00
Joseph Lyons
8c3232bb9b Add reveal in finder to additional locations
Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>
2023-02-21 16:09:11 -05:00
Joseph Lyons
ebf1da1de8 Remove print macros 2023-02-21 10:46:12 -05:00
Kay Simmons
3564e95f27 Add labeled tasks to gpui and observe them in the activity status to give feedback when we are still waiting for the language server 2023-02-20 20:28:48 -08:00
Max Brunsfeld
ecf77a510a Merge pull request #2194 from zed-industries/window-position-env-vars
Fix handling of ZED_WINDOW_{SIZE,POSITION} env vars
2023-02-20 15:29:51 -08:00
Max Brunsfeld
927f7b3363 Fix handling of ZED_WINDOW_{SIZE,POSITION} env vars
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2023-02-20 15:11:20 -08:00
Max Brunsfeld
07bb42898f Merge pull request #2193 from zed-industries/autoclose-fixes
Restructure scope-specific auto-close pairs, fix regression in skipping over auto-closed brackets
2023-02-20 13:51:29 -08:00
Max Brunsfeld
a11165ad0a Add unit test for auto-closing quotation marks with overrides 2023-02-20 13:46:17 -08:00
Max Brunsfeld
ab82e13167 Run cargo fmt 2023-02-20 11:10:30 -08:00
Julia
0e0170712e Merge pull request #2192 from zed-industries/format-ci-enforcement
Enforce rustfmt on CI & clean up some let-else format errors
2023-02-20 13:55:34 -05:00
Max Brunsfeld
8be844a13f Add test that loads all bundled languages 2023-02-20 10:53:37 -08:00
Max Brunsfeld
7c98395e77 Add missing comment pattern to TSX overrides query 2023-02-20 10:53:37 -08:00
Max Brunsfeld
8922156923 Restructure how bracket pairs are overridden to unify lists
This way, a bracket pair that is disabled in a given scope can still be skipped, if
it was auto-closed before that scope existed.
2023-02-20 10:53:37 -08:00
Julia
bda37ffb9c Enforce rustfmt on CI & clean up some let-else format errors 2023-02-20 13:27:35 -05:00
Max Brunsfeld
2982a98d1c Merge pull request #2187 from zed-industries/save-untitled-buffer-bugs
Fix newly-discovered bugs in saving untitled buffers
2023-02-20 10:05:58 -08:00
Max Brunsfeld
010eba509c Make Project::save_buffer and ::save_buffers into methods 2023-02-20 09:42:44 -08:00
Max Brunsfeld
56b7eb6b6f Only send UpdateBufferFile messages for buffers whose files have changed
Send that message when saving a buffer as a new path.
2023-02-20 09:41:59 -08:00
Petros Amoiridis
6551742c58 Merge pull request #2191 from zed-industries/petros/z-53-reveal-in-finder-crashes-zed
Move reveal_path to ForegroundPlatform
2023-02-20 19:09:54 +02:00
Petros Amoiridis
4bb986b3be Move reveal_path to ForegroundPlatform
So that we can use spawn to use the OS API call.

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-02-20 18:57:37 +02:00
Kay Simmons
efafd1d8d3 Merge pull request #2188 from zed-industries/dont-open-project-items-in-dock
Dont open project items in dock
2023-02-18 13:53:29 -08:00
Kay Simmons
0981244797 further tweak comment 2023-02-18 13:53:13 -08:00
Kay Simmons
159d3ab00c Add comment explaining push_keystroke 2023-02-18 13:49:08 -08:00
Kay Simmons
3fb6e31b92 revert for loop change and store matched actions in a sorted set instead 2023-02-18 13:42:28 -08:00
Kay Simmons
04df00b221 Iterate over keymap then dispatch path when matching keybindings to make precedence more intuitive
Rename action which adds the active tab to the dock to be more intuitive
Add action which moves the active tab out of the dock and bind it to the same keybinding
2023-02-18 13:10:01 -08:00
Kay Simmons
dc6f7fd577 pull toggle button into its own file 2023-02-18 12:32:19 -08:00
Kay Simmons
ac3e8f61ef Merge pull request #2186 from zed-industries/better-vim-matching-motion
Better vim matching motion
2023-02-17 22:10:28 -08:00
Kay Simmons
fc811d14b1 Fix failing test 2023-02-17 22:00:39 -08:00
Max Brunsfeld
cdf64b6cad Unify save and save_as for local worktrees
This fixes state propagation bugs due to missing RPC calls in save_as.
2023-02-17 17:21:48 -08:00
Max Brunsfeld
3a7cfc3901 Move the save and save_as code paths close together 2023-02-17 17:21:48 -08:00
Kay Simmons
5e4d113308 fix bracket ranges failing test 2023-02-17 17:19:23 -08:00
Max Brunsfeld
de6eb00e2b Start work on making save and save_as code paths more similar 2023-02-17 15:52:13 -08:00
Max Brunsfeld
76975c29a9 Refactor: split Project::format logic into local and remote cases 2023-02-17 15:29:54 -08:00
Kay Simmons
57a7ff9a6f fix vim percent motion to better match the docs and observed behavior 2023-02-17 14:55:19 -08:00
Max Brunsfeld
eebce28b32 Respect UpdateBufferFile messages on guest buffers without file
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2023-02-17 12:38:04 -08:00
Max Brunsfeld
31dac39e34 Fix assignment of language to formerly-untitled buffers
When lazy-loading a language, check if it matches plain text buffers.

Co-authored-by: Nathan Sobo <nathan@zed.dev>
2023-02-17 11:12:57 -08:00
Mikayla Maki
5cfe206433 Merge pull request #2185 from zed-industries/refactor-restart
Used the pre-existing app_path call in the GPUI platform
2023-02-17 10:39:45 -08:00
Mikayla Maki
ff2fb06b2c Used the pre-existing app_path call in the GPUI platform 2023-02-17 10:30:28 -08:00
Mikayla Maki
a5ad2f544e Update README.md 2023-02-16 16:51:57 -08:00
Mikayla Maki
7b291df21f Merge pull request #2184 from zed-industries/relaunch
Restart
2023-02-16 16:50:20 -08:00
Mikayla Maki
6e33f33da1 Switch to open based restarting 2023-02-16 16:47:43 -08:00
Mikayla Maki
4ea7a24b93 Made the 'update zed to collaborate' button clickable 2023-02-16 14:53:50 -08:00
Julia
48b76f96fc Merge pull request #2183 from zed-industries/default-settings-yaml-casing
Use correct case for YAML in default settings
2023-02-16 16:37:37 -05:00
Julia
c72a50e203 Use correct case for YAML in default settings 2023-02-16 16:37:07 -05:00
Mikayla Maki
43f61ab413 Added action to autoupdate 2023-02-16 13:35:32 -08:00
Mikayla Maki
cf83ecccbb Added workspace restart command 2023-02-16 13:28:56 -08:00
Kay Simmons
848c6b78d5 Merge pull request #2181 from zed-industries/update-typesript-tree-sitter
update tree-sitter-typescript to add support for new satisfies operator
2023-02-16 12:56:40 -08:00
Max Brunsfeld
b90fc046ca Merge pull request #2180 from zed-industries/turbofish-highlight
Highlight functions called with a turbofish in Rust
2023-02-16 12:52:37 -08:00
Max Brunsfeld
98b51634c4 Merge pull request #2182 from zed-industries/faster-injections
Fix syntax-related performance problems on gigantic files
2023-02-16 12:52:13 -08:00
Max Brunsfeld
28eb69e74e Bump tree-sitter for optimization of querying in range 2023-02-16 12:35:35 -08:00
Kay Simmons
b03eebeb6c update tree-sitter-typescript to add support for new satisfies operator 2023-02-16 12:24:35 -08:00
Kay Simmons
eac33d732e wip 2023-02-16 12:23:45 -08:00
Max Brunsfeld
2d39358323 rust: Highlight functions called with a turbofish 2023-02-16 12:11:57 -08:00
Joseph T. Lyons
a4a179763a Merge pull request #2171 from zed-industries/add-option-to-advance-cursor-downward-when-toggling-comment
Add option to advance cursor downward when toggling comment
2023-02-16 15:03:38 -05:00
Julia
19b686ad65 Merge pull request #2177 from zed-industries/check-before-test
Cargo check before test to catch warnings/errors
2023-02-16 14:43:33 -05:00
Antonio Scandurra
ac882c7db5 Merge pull request #2179 from zed-industries/debounce-diagnostics
Simulate disk-based diagnostics finishing 1s after saving buffer
2023-02-16 16:34:19 +01:00
Antonio Scandurra
baee6d0342 Simulate disk-based diagnostics finishing 1s after saving buffer
Previously, we would simulate disk-based diagnostics finishing after
saving a buffer. However, the language server may produce diagnostics
right after emitting the event, causing the diagnostics status bar item
to not reflect the latest state of the buffer.

With this change, we will instead simulate disk-based diagnostics finishing
after 1s after saving the buffer (only for language servers that
don't have the concept of disk-based diagnostics, such as TypeScript). This
ensures we always reflect the latest state and doesn't cause the UI to flicker
as a result of the LSP sending us diagnostics after every input.
2023-02-16 16:04:08 +01:00
Julia
50ccf16de1 Cargo check before test to catch warnings/errors
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-02-15 17:53:28 -05:00
Kay Simmons
bef2013c7f wip 2023-02-15 14:40:14 -08:00
Kay Simmons
2c904cb0bf Merge pull request #2176 from zed-industries/better-move-to-brackets
Make jump to matching bracket action more predictable
2023-02-15 14:32:51 -08:00
Kay Simmons
33306846a6 add tree-sitter-typescript to editor crate test support features 2023-02-15 14:28:50 -08:00
Kay Simmons
30caeeaeb5 fix comment typo 2023-02-15 14:11:00 -08:00
Kay Simmons
0ba051a754 use more predictable rules for selecting which bracket to jump to and where 2023-02-15 14:04:16 -08:00
Max Brunsfeld
32191e318e Merge pull request #2175 from zed-industries/restore-toggle-dock-binding
Put back shift-escape binding for FocusDock action
2023-02-15 14:04:01 -08:00
Max Brunsfeld
7037842bef Put back shift-escape binding for FocusDock action 2023-02-15 13:57:07 -08:00
Julia
8bd20d8c3a Merge pull request #2173 from zed-industries/tab-bar-background-focus-pane
Focus pane when clicking on tab bar background
2023-02-15 16:07:10 -05:00
Julia
df1775326c Merge pull request #2172 from zed-industries/window-title-shenanigans
Window title shenanigans
2023-02-15 16:03:30 -05:00
Julia
df0715e7c9 Indicate in native window title if project is shared or remote 2023-02-15 15:56:16 -05:00
Julia
e56dfd9177 Tell OS about window title 2023-02-15 15:55:55 -05:00
Joseph Lyons
afb375f909 v0.75.x dev 2023-02-15 14:57:51 -05:00
Joseph T. Lyons
bcf7a32284 Update pull_request_template.md 2023-02-15 14:10:23 -05:00
Joseph Lyons
5fbc9736e5 Add option to advance cursor downward when toggling comment
Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>
2023-02-15 14:00:49 -05:00
Petros Amoiridis
fbd23986e3 Merge pull request #2161 from zed-industries/community/3-reveal-in-finder
Introduce Reveal in Finder
2023-02-15 16:11:04 +02:00
Antonio Scandurra
114eef8592 Merge pull request #2170 from zed-industries/fix-persistent-lsp-messages
Fix LSP status messages persisting
2023-02-15 15:09:01 +01:00
Antonio Scandurra
5df50e2fc9 Add timeouts to HTTP client 2023-02-15 15:00:44 +01:00
Petros Amoiridis
7a667f390b Use open_url from the platform module
And remove the open function from the `util` crate.
2023-02-15 15:58:57 +02:00
Antonio Scandurra
2482a1a9ce Add timeout to HTTP requests during npm info and npm fetch 2023-02-15 14:48:36 +01:00
Antonio Scandurra
7641965326 Merge pull request #2169 from zed-industries/improve-autocomplete
Score matches case-sensitively when query contains an uppercase char
2023-02-15 13:30:05 +01:00
Antonio Scandurra
8db131a3a1 Score matches case-sensitively when query contains an uppercase char 2023-02-15 13:12:45 +01:00
Mikayla Maki
4f1e8c953e Merge pull request #2168 from zed-industries/auto-update-setting
Add auto update setting
2023-02-14 18:10:56 -08:00
Mikayla Maki
c2de0f6b5e Add auto update setting 2023-02-14 18:05:42 -08:00
Mikayla Maki
17e8172dc3 Merge pull request #2167 from zed-industries/clear-terminal-highlights
Reset search matches when the terminal is cleared
2023-02-14 10:02:06 -08:00
Mikayla Maki
8e9d95fefc Fix error where terminal search matches wouldn't be updated when clearing 2023-02-14 09:54:31 -08:00
Julia
3a7ac9c0ff Focus pane when clicking on tab bar background 2023-02-14 12:39:29 -05:00
Antonio Scandurra
88c6b890bc Merge pull request #2165 from zed-industries/toggle-soft-wrap
Introduce `editor: toggle soft wrap` bound to `alt-z`
2023-02-14 15:27:31 +01:00
Antonio Scandurra
1012cea4af Soft wrap at editor width if it's narrower than preferred line length 2023-02-14 15:22:00 +01:00
Antonio Scandurra
4a2b7e4820 Use alt-z to toggle soft wrap in active editor
When there isn't a default soft-wrapping for the active editor, we
will soft wrap at the editor width. This is consistent with Visual
Studio Code.
2023-02-14 15:16:06 +01:00
Antonio Scandurra
6c0b35acb0 Merge pull request #2164 from zed-industries/fix-surround-rust-star
Surround with bracket only when opening brace is 1 character long
2023-02-14 14:42:13 +01:00
Antonio Scandurra
888fcb5b1b Surround with bracket only when opening brace is 1 character long 2023-02-14 14:36:18 +01:00
Petros Amoiridis
015b8db1c3 Introduce reveal_path in Platform
And implement it for MacPlatform, and instead of using an external process to run `open`, use the NSWorkspace selectFile instance method.
2023-02-14 15:14:15 +02:00
Antonio Scandurra
ebe1fa7a96 Merge pull request #2163 from zed-industries/quick-invite-bug
Avoid creating more than one room when inviting multiple people at once
2023-02-14 12:55:21 +01:00
Antonio Scandurra
7be868e372 Avoid creating more than one room when inviting multiple people at once
Previously, when initiating a call by calling multiple people, only
the first person would get the call while all the others would briefly
show a "pending" status but never get the call.

This would happen because `ActiveCall` was trying to a create a different
room for each person called, because the original room creation hadn't finished
and so a `ModelHandle<Room>` wasn't being store in the active call.

With this commit, only one room can be created at any given time and further
invites have to wait until that room creation is done.
2023-02-14 12:03:30 +01:00
Antonio Scandurra
087d51634d Fix test that wasn't properly verifying disconnection from livekit 2023-02-14 10:46:29 +01:00
Max Brunsfeld
ea663f3017 Bump tree-sitter for tree-balancing bugfix 2023-02-13 23:46:44 -08:00
Kay Simmons
5041300b52 Merge pull request #2157 from zed-industries/vim-fixes
Vim fixes
2023-02-13 14:21:47 -08:00
Kay Simmons
2c9199fd32 fix build error 2023-02-13 14:12:43 -08:00
Kay Simmons
327932ba3b Remove catch all keymap and KeyPressed action 2023-02-13 13:50:37 -08:00
Kay Simmons
459060764a fix sqlez warning, introduce tab and enter bindings to vim for inputing tab and enter text when waiting for text 2023-02-13 13:50:37 -08:00
Kay Simmons
3d53336916 More vim fixes and move some more things out of app.rs 2023-02-13 13:50:37 -08:00
Kay Simmons
c1812ddc27 fix issue with single line editors in vim not properly unhooking vim mode bindings 2023-02-13 13:50:37 -08:00
Mikayla Maki
d80dba1fe3 Switch from vec to smallvec 2023-02-13 12:49:57 -08:00
Antonio Scandurra
6703264600 Limit BufferSnapshot::chunks to the outline item range
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-02-13 19:57:15 +01:00
Nate Butler
5ce147a2ad Remove unneeded API from theme-tool 2023-02-12 21:04:54 -05:00
Nate Butler
a32c0d1c9b Update colors. 2023-02-12 21:04:31 -05:00
Nate Butler
e65c0810ba Add gray color families 2023-02-12 20:02:51 -05:00
Nate Butler
1fcfa5d272 Generate color meta, document functions and tidy. 2023-02-12 19:45:16 -05:00
Nate Butler
addfcdc1f4 Remove bezier-easing from styles
This was installed in the wrong app
2023-02-12 11:59:36 -05:00
Nate Butler
4501a5a7ee Add initial system palette reference colors 2023-02-12 00:15:41 -05:00
Nate Butler
a120996f0d Test using HSL + curves to build a scale 2023-02-11 23:34:46 -05:00
Joseph T. Lyons
0a50d271b7 Merge pull request #2158 from zed-industries/split-feedback-editor
Split up feedback_editor.rs
2023-02-11 23:19:25 -05:00
Joseph T. Lyons
01a590a1fb Merge pull request #2160 from zed-industries/remove-toggle-right-sidebar-command
Remove toggle right sidebar command
2023-02-11 22:17:38 -05:00
Joseph Lyons
d42d495cb0 Remove toggle right sidebar command 2023-02-11 21:53:10 -05:00
Nate Butler
187fac1579 Allow passing a chroma color as a start/mid/end color 2023-02-11 21:10:47 -05:00
Nate Butler
0acb820f04 Document ref/color further 2023-02-11 08:14:13 -05:00
Nate Butler
dda0febf39 Organize 2023-02-11 07:56:41 -05:00
Petros Amoiridis
9143790602 Include code only on macOS
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-02-11 11:12:46 +02:00
Kay Simmons
b31813fad3 Split concepts out into self contained files in feedback editor 2023-02-10 22:50:05 -08:00
Nate Butler
0e238210bb Update chip style 2023-02-10 22:06:03 -05:00
Mikayla Maki
912c396b37 Merge pull request #2156 from zed-industries/fix-atelier-cave-license
Update the atelier cave license file
2023-02-10 12:04:36 -08:00
Mikayla Maki
436ab6e454 Fix other atelier license 2023-02-10 11:58:25 -08:00
Mikayla Maki
889b15683d Update the atelier cave license file 2023-02-10 11:52:31 -08:00
Mikayla Maki
135dcf19a2 Merge pull request #2154 from zed-industries/fix-tooltip-crash
Don't render tooltip keystroke label if there's no focused view
2023-02-10 11:41:03 -08:00
Petros Amoiridis
5d23aaacc8 Introduce an open function
And refactor some of the older code to simplify it

Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-02-10 21:11:54 +02:00
Petros Amoiridis
a789476c95 Introduce reveal_in_finder function
And use this function in a new Reveal in Finder option of the project panel context menu.

Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-02-10 21:11:05 +02:00
Nathan Sobo
da5a6a8b4f Don't render tooltip keystroke label if there's no focused view
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2023-02-10 07:57:01 -07:00
Nate Butler
76685406ed Add remaining colors 2023-02-09 23:26:44 -05:00
Nate Butler
70eedbb48e Group light and dark scales 2023-02-09 13:41:37 -05:00
Nate Butler
42b5fa1fa3 WIP: Use algorithm to generate reference color palette
Adapted from @k-vyn/coloralgorithm
Generate colors for our reference palette.
2023-02-09 12:37:05 -05:00
Joseph T. Lyons
7de04abdcb Merge pull request #2146 from zed-industries/feedback-editor-polish
Feedback editor polish
2023-02-09 10:01:27 -05:00
Antonio Scandurra
373e88e9fb Merge pull request #2147 from zed-industries/fix-pyright
Always respond to language server, even when its requests are malformed
2023-02-09 15:52:57 +01:00
Antonio Scandurra
c3a88857f9 Always respond to language server, even when its requests are malformed
This was causing Pyright to get stuck waiting for a response when sending
us the `workspace/configuration` request. For some reason, after upgrading
to Pyright 1.1.293, `scopeUri` is being sent as an empty string sometimes,
which causes serde to error when trying to deserialize that request.

Co-Authored-By: Petros Amoiridis <petros@zed.dev>
2023-02-09 15:30:10 +01:00
Nate Butler
f787f6054b List colors for reference palette in theme tool 2023-02-09 00:39:57 -05:00
Nate Butler
6f342bb2c6 Remove leftovers from create-next-app 2023-02-09 00:10:32 -05:00
Nate Butler
0ba44c6dc4 Start on system colors 2023-02-09 00:09:52 -05:00
Nate Butler
2ff82732b9 Init theme tool 2023-02-09 00:09:44 -05:00
Nate Butler
cbfdfa8124 Remove comment 2023-02-08 22:52:27 -05:00
Joseph Lyons
57e10ce7c6 Style info text 2023-02-08 21:26:27 -05:00
Joseph Lyons
a9c2f42f70 Move string to variable 2023-02-08 21:26:27 -05:00
Joseph Lyons
83f9d51dee Fix layout of elements in the feedback editor's toolbar
Co-Authored-By: Kay Simmons <3323631+Kethku@users.noreply.github.com>
Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>
2023-02-08 21:26:27 -05:00
Mikayla Maki
767d2f9766 Merge pull request #2134 from zed-industries/fix-action-keystroke-bugs
Fix several action dispatching bugs
2023-02-08 15:56:50 -08:00
Joseph Lyons
3fb14d7caf v0.74.x dev 2023-02-08 14:55:21 -05:00
Antonio Scandurra
952cdb4e98 Merge pull request #2145 from zed-industries/fix-focus-in-project-search
Focus results editor only when starting a new project search
2023-02-08 18:25:02 +01:00
Joseph Lyons
bbe8297297 Add feedback information text 2023-02-08 11:23:44 -05:00
Antonio Scandurra
76c066baee Focus results editor only when starting a new project search
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2023-02-08 17:22:14 +01:00
Joseph Lyons
654ee48feb Add tooltip to submit feedback button 2023-02-08 10:37:04 -05:00
Joseph Lyons
ef16963772 Remove placeholder text 2023-02-08 10:22:36 -05:00
Joseph T. Lyons
37c052f53d Include is_staff boolean in in-app feedback 2023-02-08 10:14:18 -05:00
Joseph T. Lyons
582f5d0114 Merge pull request #2130 from zed-industries/inform-user-that-telemetry-can-be-disabled
Inform user that telemetry can be disabled
2023-02-07 21:15:16 -05:00
Joseph T. Lyons
fd016b9bcd Merge pull request #2133 from zed-industries/feedback-submit-button
Implement a button for submitting feedback
2023-02-07 21:13:42 -05:00
Joseph Lyons
317eb7535c Fix variable names 2023-02-07 21:08:07 -05:00
Kay Simmons
55589533e2 Update yaml-tree-sitter git hash
Update yaml-tree-sitter git hash
2023-02-07 15:51:45 -08:00
Kay Simmons
9a8585ce0c Merge pull request #2144 from zed-industries/yaml-lsp
Add yaml language server
2023-02-07 15:44:10 -08:00
Kay Simmons
aa0a18968a removed unused import 2023-02-07 15:40:29 -08:00
Kay Simmons
0777f459ba Add yaml language server 2023-02-07 15:34:27 -08:00
Joseph T. Lyons
2732cc2cbe Merge pull request #2143 from zed-industries/remove-release-action-for-Discourse
Remove release action for Discourse
2023-02-07 18:34:21 -05:00
Joseph Lyons
e8dad56af9 Remove release action for Discourse 2023-02-07 18:26:55 -05:00
Mikayla Maki
87cf8ac60e Fixed strange y results from faulty conversion to screen coordinates
co-authored-by: Nathan <nathan@zed.dev>
2023-02-07 15:26:03 -08:00
Joseph T. Lyons
f44658ad2a Merge pull request #2142 from zed-industries/update-links-to-community-repository
Update links to community repository
2023-02-07 18:23:50 -05:00
Joseph Lyons
20377ea4e9 Update links to community repository 2023-02-07 18:19:27 -05:00
Mikayla Maki
db2aaa4367 Fixed bug in setting cursor style 2023-02-07 14:35:46 -08:00
Kay Simmons
099b79910f Merge pull request #2137 from zed-industries/yaml
yaml highlighting
2023-02-07 14:32:02 -08:00
Kay Simmons
fe25994fb3 fix highlights, indents, and tab size for yaml 2023-02-07 14:20:23 -08:00
Joseph Lyons
7cef4a5d40 Allocate theme struct directly into the heap
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-02-07 13:39:48 -05:00
Max Brunsfeld
0c49030ade Merge pull request #2140 from zed-industries/feedback/929-project-search-crashes
Feedback/929 project search crashes
2023-02-07 10:12:00 -08:00
Petros Amoiridis
e15ffc8560 Make truncate_and_trailoff a bit more clear
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
2023-02-07 20:06:20 +02:00
Joseph Lyons
58987275fc Merge branch 'main' into feedback-submit-button 2023-02-07 12:55:53 -05:00
Petros Amoiridis
9bff82f161 Use truncate_and_trailoff function
A function that already works with unicode characters.
2023-02-07 19:25:57 +02:00
Petros Amoiridis
be0241bab1 Add test for string with unicode characters 2023-02-07 19:25:07 +02:00
Kay Simmons
de0b136be2 wip yaml highlighting 2023-02-07 01:00:50 -08:00
Kay Simmons
4e80ae13ec Merge pull request #2136 from zed-industries/fix-recent-projects-panic
Fix Recent Project Panic
2023-02-07 00:38:18 -08:00
Kay Simmons
b020955ac4 show notification if no recent projects 2023-02-07 00:10:11 -08:00
Max Brunsfeld
a606058537 Merge pull request #2135 from zed-industries/handle-window-moved-crash
Fix crash when unplugging display containing a zed window
2023-02-06 17:05:32 -08:00
Max Brunsfeld
f065399799 Fix crash when unplugging display containing a zed window
Co-authored-by: Kay Simmons <kay@zed.dev>
2023-02-06 16:44:06 -08:00
Mikayla Maki
926b59b15d Fixed a bug where the command palette wouldn't check the keymap context when showing available actions
Fixed a bug where context menus wouldn't show action keystrokes
WIP Fixing a bug where tooltips won't show action keystrokes

Co-Authored-By: Max <max@zed.dev>
2023-02-06 15:42:14 -08:00
Kay Simmons
2d6219ebe2 Merge pull request #2131 from zed-industries/lua
Add lua syntax highlighting and lsp support
2023-02-06 15:40:03 -08:00
Joseph Lyons
8228618b9e Correct theme function name 2023-02-06 18:19:15 -05:00
Joseph Lyons
d4d9a142fc Implement a button for submitting feedback
Co-Authored-By: Kay Simmons <3323631+Kethku@users.noreply.github.com>
2023-02-06 17:41:36 -05:00
Kay Simmons
035901127a remove unused version regex 2023-02-05 23:25:20 -08:00
Kay Simmons
37bfeed2e6 Merge pull request #2129 from zed-industries/move-sharing-status-indicator
Move sharing status indicator to collab ui
2023-02-05 23:24:23 -08:00
Kay Simmons
4642817e72 Add lua syntax highlighting and lsp support 2023-02-05 23:21:29 -08:00
Joseph Lyons
83e21387af Inform user that telemetry can be disabled 2023-02-04 22:18:07 -05:00
Kay Simmons
3e92e4d110 fix unsaved change 2023-02-03 12:47:20 -08:00
Kay Simmons
303216291b Move sharing status indicator out of the call crate and into collab_ui in order so that the model doesn't depend on the view 2023-02-03 11:17:50 -08:00
Kay Simmons
8be9d21340 Merge pull request #2128 from zed-industries/feedback/922-unify-find-all-references-label
Make app menu and context menu labels consistent
2023-02-03 10:29:43 -08:00
Joseph Lyons
9742bd7fd4 Reduce length of feedback placeholder text 2023-02-03 08:14:14 -05:00
Petros Amoiridis
3014cc5299 Do not capitalize prepositions in title case
This also match the app menu
2023-02-03 12:16:09 +02:00
Petros Amoiridis
d6b728409f Be consistent in the app & context menus 2023-02-03 12:14:13 +02:00
Petros Amoiridis
433f284571 Merge pull request #2126 from zed-industries/731-make-cursor-visible-immediately
Focus the editor when activating previous pane
2023-02-03 10:12:34 +02:00
Kay Simmons
7270f950b8 Merge pull request #2115 from zed-industries/call-status-indicator
Add call status indicator to the status bar
2023-02-02 17:18:22 -08:00
Kay Simmons
ae15673dfd Merge pull request #2124 from zed-industries/fix-display-uuid-panic
Make display uuid optional if the display is disconnected
2023-02-02 17:17:01 -08:00
Joseph T. Lyons
8697f81a37 Merge pull request #2127 from zed-industries/fix-discourse-release
Fix discourse release
2023-02-02 14:29:50 -05:00
Joseph T. Lyons
21ded7639a Merge pull request #2125 from zed-industries/trim-leading-and-trailing-whitespace-in-feedback
Trim leading and trailing whitespace in feedback
2023-02-02 13:43:35 -05:00
Joseph Lyons
3f95788d45 Clean up whitespace 2023-02-02 13:38:41 -05:00
Joseph Lyons
1afd6f859d Fix discourse release action
Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>
2023-02-02 13:38:25 -05:00
Petros Amoiridis
2b0592da21 Guard against tab_bar_context_menu
We don't want to have the tab_bar_context_menu as the active item of the pane where the split started from

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-02-02 20:15:43 +02:00
Petros Amoiridis
8f61134e7e Allow comparing ViewHandle to AnyViewHandle
Since they both have a window_id and a view_id.

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-02-02 20:15:40 +02:00
Joseph Lyons
888145ebed Trim leading and trailing whitespace in feedback 2023-02-02 12:43:55 -05:00
Kay Simmons
a50f0181fb Add setting to disable the call icon 2023-02-01 16:21:53 -08:00
Kay Simmons
62d32db66c Make display uuid optional if the display is disconnected 2023-02-01 14:59:43 -08:00
Nate Butler
d6962d957b Add note to base16.ts 2023-02-01 16:26:08 -05:00
Joseph Lyons
fd2a9b3df9 v0.73.x dev 2023-02-01 13:45:06 -05:00
Kay Simmons
460dc62888 start adding setting for the screen sharing status indicator 2023-01-31 15:17:16 -08:00
Kay Simmons
e35db69dbd Add call status indicator to the status bar 2023-01-31 15:00:49 -08:00
332 changed files with 18889 additions and 9016 deletions

View File

@@ -1,6 +1,6 @@
## Description of feature or change
## Link to related issues from zed or insiders
## Link to related issues from zed or community
## Before Merging

View File

@@ -17,6 +17,26 @@ env:
RUST_BACKTRACE: 1
jobs:
rustfmt:
name: Check formatting
runs-on:
- self-hosted
- test
steps:
- name: Install Rust
run: |
rustup set profile minimal
rustup update stable
- name: Checkout repo
uses: actions/checkout@v2
with:
clean: false
submodules: 'recursive'
- name: cargo fmt
run: cargo fmt --all -- --check
tests:
name: Run tests
runs-on:
@@ -42,6 +62,9 @@ jobs:
clean: false
submodules: 'recursive'
- name: Run check
run: cargo check --workspace
- name: Run tests
run: cargo test --workspace --no-fail-fast

View File

@@ -13,23 +13,14 @@ jobs:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: |
📣 Zed ${{ github.event.release.tag_name }} was just released!
Restart your Zed or head to https://zed.dev/releases/latest to grab it.
```md
# Changelog
${{ github.event.release.body }}
```
discourse_release:
runs-on: ubuntu-latest
steps:
- name: Install Node
uses: actions/setup-node@v2
if: ${{ ! github.event.release.prerelease }}
with:
node-version: '16'
- run: script/discourse_release ${{ secrets.DISCOURSE_RELEASES_API_KEY }} ${{ github.event.release.tag_name }} ${{ github.event.release.body }}
mixpanel_release:
runs-on: ubuntu-latest
steps:
@@ -40,7 +31,7 @@ jobs:
architecture: "x64"
cache: "pip"
- run: pip install -r script/mixpanel_release/requirements.txt
- run: >
- run: >
python script/mixpanel_release/main.py
${{ github.event.release.tag_name }}
${{ secrets.MIXPANEL_PROJECT_ID }}

202
Cargo.lock generated
View File

@@ -259,6 +259,21 @@ dependencies = [
"futures-lite",
]
[[package]]
name = "async-global-executor"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776"
dependencies = [
"async-channel",
"async-executor",
"async-io",
"async-lock",
"blocking",
"futures-lite",
"once_cell",
]
[[package]]
name = "async-io"
version = "1.12.0"
@@ -350,6 +365,32 @@ dependencies = [
"syn",
]
[[package]]
name = "async-std"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d"
dependencies = [
"async-channel",
"async-global-executor",
"async-io",
"async-lock",
"crossbeam-utils 0.8.14",
"futures-channel",
"futures-core",
"futures-io",
"futures-lite",
"gloo-timers",
"kv-log-macro",
"log",
"memchr",
"once_cell",
"pin-project-lite 0.2.9",
"pin-utils",
"slab",
"wasm-bindgen-futures",
]
[[package]]
name = "async-stream"
version = "0.3.3"
@@ -371,6 +412,20 @@ dependencies = [
"syn",
]
[[package]]
name = "async-tar"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c49359998a76e32ef6e870dbc079ebad8f1e53e8441c5dd39d27b44493fe331"
dependencies = [
"async-std",
"filetime",
"libc",
"pin-project",
"redox_syscall",
"xattr",
]
[[package]]
name = "async-task"
version = "4.0.3"
@@ -739,7 +794,7 @@ dependencies = [
[[package]]
name = "bromberg_sl2"
version = "0.6.0"
source = "git+https://github.com/zed-industries/bromberg_sl2?rev=dac565a90e8f9245f48ff46225c915dc50f76920#dac565a90e8f9245f48ff46225c915dc50f76920"
source = "git+https://github.com/zed-industries/bromberg_sl2?rev=950bc5482c216c395049ae33ae4501e08975f17f#950bc5482c216c395049ae33ae4501e08975f17f"
dependencies = [
"digest 0.9.0",
"lazy_static",
@@ -828,6 +883,7 @@ dependencies = [
"media",
"postage",
"project",
"settings",
"util",
]
@@ -1132,7 +1188,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.5.4"
version = "0.7.2"
dependencies = [
"anyhow",
"async-tungstenite",
@@ -1196,11 +1252,14 @@ name = "collab_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"auto_update",
"call",
"client",
"clock",
"collections",
"context_menu",
"editor",
"feedback",
"futures 0.3.25",
"fuzzy",
"gpui",
@@ -1900,6 +1959,7 @@ dependencies = [
"tree-sitter-html",
"tree-sitter-javascript",
"tree-sitter-rust",
"tree-sitter-typescript 0.20.2",
"unindent",
"util",
"workspace",
@@ -2079,6 +2139,18 @@ dependencies = [
"workspace",
]
[[package]]
name = "filetime"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"windows-sys 0.42.0",
]
[[package]]
name = "fixedbitset"
version = "0.4.2"
@@ -2527,6 +2599,18 @@ dependencies = [
"regex",
]
[[package]]
name = "gloo-timers"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
dependencies = [
"futures-channel",
"futures-core",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "go_to_line"
version = "0.1.0"
@@ -2935,6 +3019,17 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3"
[[package]]
name = "install_cli"
version = "0.1.0"
dependencies = [
"anyhow",
"gpui",
"log",
"smol",
"util",
]
[[package]]
name = "instant"
version = "0.1.12"
@@ -3074,6 +3169,7 @@ dependencies = [
name = "journal"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"dirs 4.0.0",
"editor",
@@ -3143,6 +3239,15 @@ dependencies = [
"arrayvec 0.7.2",
]
[[package]]
name = "kv-log-macro"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
dependencies = [
"log",
]
[[package]]
name = "language"
version = "0.1.0"
@@ -3160,6 +3265,7 @@ dependencies = [
"fuzzy",
"git",
"gpui",
"indoc",
"lazy_static",
"log",
"lsp",
@@ -3186,12 +3292,28 @@ dependencies = [
"tree-sitter-python",
"tree-sitter-ruby",
"tree-sitter-rust",
"tree-sitter-typescript",
"tree-sitter-typescript 0.20.1",
"unicase",
"unindent",
"util",
]
[[package]]
name = "language_selector"
version = "0.1.0"
dependencies = [
"anyhow",
"editor",
"fuzzy",
"gpui",
"language",
"picker",
"project",
"settings",
"theme",
"workspace",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@@ -4499,6 +4621,7 @@ dependencies = [
"lsp",
"parking_lot 0.11.2",
"postage",
"pretty_assertions",
"pulldown-cmark",
"rand 0.8.5",
"regex",
@@ -6015,7 +6138,6 @@ dependencies = [
"libsqlite3-sys",
"parking_lot 0.11.2",
"smol",
"sqlez_macros",
"thread_local",
"uuid 1.2.2",
]
@@ -6914,7 +7036,7 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.20.9"
source = "git+https://github.com/tree-sitter/tree-sitter?rev=36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da#36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da"
source = "git+https://github.com/tree-sitter/tree-sitter?rev=c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14#c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14"
dependencies = [
"cc",
"regex",
@@ -7016,6 +7138,16 @@ dependencies = [
"tree-sitter",
]
[[package]]
name = "tree-sitter-lua"
version = "0.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d489873fd1a2fa6d5f04930bfc5c081c96f0c038c1437104518b5b842c69b282"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-markdown"
version = "0.0.1"
@@ -7092,6 +7224,24 @@ dependencies = [
"tree-sitter",
]
[[package]]
name = "tree-sitter-typescript"
version = "0.20.2"
source = "git+https://github.com/tree-sitter/tree-sitter-typescript?rev=5d20856f34315b068c41edaee2ac8a100081d259#5d20856f34315b068c41edaee2ac8a100081d259"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-yaml"
version = "0.0.1"
source = "git+https://github.com/zed-industries/tree-sitter-yaml?rev=9050a4a4a847ed29e25485b1292a36eab8ae3492#9050a4a4a847ed29e25485b1292a36eab8ae3492"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "try-lock"
version = "0.2.3"
@@ -7892,6 +8042,26 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
[[package]]
name = "welcome"
version = "0.1.0"
dependencies = [
"anyhow",
"db",
"editor",
"fuzzy",
"gpui",
"install_cli",
"log",
"picker",
"project",
"settings",
"theme",
"theme_selector",
"util",
"workspace",
]
[[package]]
name = "wepoll-ffi"
version = "0.1.2"
@@ -8167,6 +8337,7 @@ dependencies = [
"futures 0.3.25",
"gpui",
"indoc",
"install_cli",
"language",
"lazy_static",
"log",
@@ -8178,6 +8349,7 @@ dependencies = [
"serde_json",
"settings",
"smallvec",
"terminal",
"theme",
"util",
"uuid 1.2.2",
@@ -8193,6 +8365,15 @@ dependencies = [
"winapi-build",
]
[[package]]
name = "xattr"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
dependencies = [
"libc",
]
[[package]]
name = "xml-rs"
version = "0.8.4"
@@ -8228,13 +8409,14 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "zed"
version = "0.72.0"
version = "0.77.0"
dependencies = [
"activity_indicator",
"anyhow",
"assets",
"async-compression",
"async-recursion 0.3.2",
"async-tar",
"async-trait",
"auto_update",
"backtrace",
@@ -8249,6 +8431,7 @@ dependencies = [
"command_palette",
"context_menu",
"ctor",
"db",
"diagnostics",
"easy-parallel",
"editor",
@@ -8264,9 +8447,11 @@ dependencies = [
"ignore",
"image",
"indexmap",
"install_cli",
"isahc",
"journal",
"language",
"language_selector",
"lazy_static",
"libc",
"log",
@@ -8312,6 +8497,7 @@ dependencies = [
"tree-sitter-go",
"tree-sitter-html",
"tree-sitter-json 0.20.0",
"tree-sitter-lua",
"tree-sitter-markdown",
"tree-sitter-python",
"tree-sitter-racket",
@@ -8319,13 +8505,15 @@ dependencies = [
"tree-sitter-rust",
"tree-sitter-scheme",
"tree-sitter-toml",
"tree-sitter-typescript",
"tree-sitter-typescript 0.20.2",
"tree-sitter-yaml",
"unindent",
"url",
"urlencoding",
"util",
"uuid 1.2.2",
"vim",
"welcome",
"workspace",
]

View File

@@ -26,8 +26,10 @@ members = [
"crates/go_to_line",
"crates/gpui",
"crates/gpui_macros",
"crates/install_cli",
"crates/journal",
"crates/language",
"crates/language_selector",
"crates/live_kit_client",
"crates/live_kit_server",
"crates/lsp",
@@ -58,6 +60,7 @@ members = [
"crates/util",
"crates/vim",
"crates/workspace",
"crates/welcome",
"crates/zed",
]
default-members = ["crates/zed"]
@@ -69,7 +72,7 @@ serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
rand = { version = "0.8" }
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da" }
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" }
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457

View File

@@ -23,10 +23,18 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
git clone https://github.com/zed-industries/zed.dev
```
* Set up a local `zed` database and seed it with some initial users:
* Initialize submodules
```
script/bootstrap
git submodule update --init --recursive
```
* Set up a local `zed` database and seed it with some initial users:
Create a personal GitHub token to run `script/bootstrap` once successfully. Then delete that token.
```
GITHUB_TOKEN=<$token> script/bootstrap
```
### Testing against locally-running servers
@@ -51,7 +59,7 @@ If you trigger `cmd-alt-i`, Zed will copy a JSON representation of the current w
### Licensing
We use `[cargo-about](https://github.com/EmbarkStudios/cargo-about)` to automatically comply with open source licenses. If CI is failing, check the following:
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.

View File

@@ -0,0 +1,3 @@
<svg width="14" height="4" viewBox="0 0 14 4" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.125 2C3.125 2.62132 2.62132 3.125 2 3.125C1.37868 3.125 0.875 2.62132 0.875 2C0.875 1.37868 1.37868 0.875 2 0.875C2.62132 0.875 3.125 1.37868 3.125 2ZM8.125 2C8.125 2.62132 7.62132 3.125 7 3.125C6.37868 3.125 5.875 2.62132 5.875 2C5.875 1.37868 6.37868 0.875 7 0.875C7.62132 0.875 8.125 1.37868 8.125 2ZM12 3.125C12.6213 3.125 13.125 2.62132 13.125 2C13.125 1.37868 12.6213 0.875 12 0.875C11.3787 0.875 10.875 1.37868 10.875 2C10.875 2.62132 11.3787 3.125 12 3.125Z" fill="#ABB2BF"/>
</svg>

After

Width:  |  Height:  |  Size: 637 B

View File

@@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 1C0 0.585786 0.335786 0.25 0.75 0.25H7.25C7.66421 0.25 8 0.585786 8 1C8 1.41421 7.66421 1.75 7.25 1.75H1.5V10.25H7.25C7.66421 10.25 8 10.5858 8 11C8 11.4142 7.66421 11.75 7.25 11.75H0.75C0.335786 11.75 0 11.4142 0 11V1ZM8.78148 2.91435C9.10493 2.65559 9.57689 2.70803 9.83565 3.03148L11.8357 5.53148C12.0548 5.80539 12.0548 6.19461 11.8357 6.46852L9.83565 8.96852C9.57689 9.29197 9.10493 9.34441 8.78148 9.08565C8.45803 8.82689 8.40559 8.35493 8.66435 8.03148L9.68953 6.75H3.75C3.33579 6.75 3 6.41421 3 6C3 5.58579 3.33579 5.25 3.75 5.25H9.68953L8.66435 3.96852C8.40559 3.64507 8.45803 3.17311 8.78148 2.91435Z" fill="#ABB2BF"/>
</svg>

After

Width:  |  Height:  |  Size: 784 B

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

@@ -0,0 +1,3 @@
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 6C7.34315 6 6 7.34315 6 9V75H0V9C0 4.02944 4.02944 0 9 0H89.3787C93.3878 0 95.3955 4.84715 92.5607 7.68198L43.0551 57.1875H57V51H63V58.6875C63 61.1728 60.9853 63.1875 58.5 63.1875H37.0551L26.7426 73.5H73.5V36H79.5V73.5C79.5 76.8137 76.8137 79.5 73.5 79.5H20.7426L10.2426 90H87C88.6569 90 90 88.6569 90 87V21H96V87C96 91.9706 91.9706 96 87 96H6.62132C2.61224 96 0.604504 91.1529 3.43934 88.318L52.7574 39H39V45H33V37.5C33 35.0147 35.0147 33 37.5 33H58.7574L69.2574 22.5H22.5V60H16.5V22.5C16.5 19.1863 19.1863 16.5 22.5 16.5H75.2574L85.7574 6H9Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 715 B

View File

@@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.6667 0.400196H1.33346C0.819658 0.400196 0.399658 0.820196 0.399658 1.3326V10.6658C0.399658 11.181 0.816998 11.5982 1.33206 11.5982C1.58966 11.5982 1.82206 11.4932 1.99146 11.3238L4.51706 8.79684H10.6639C11.1763 8.79684 11.5963 8.37544 11.5963 7.86304V1.3298C11.5963 0.815996 11.1749 0.395996 10.6625 0.395996L10.6667 0.400196ZM2.2667 2.2664H6.00008V3.1988H2.26628V2.265L2.2667 2.2664ZM7.8667 6.93316H2.2667V5.99936H7.8667V6.93176V6.93316ZM9.7329 5.06556H2.26488V4.13176H9.73164V5.06416L9.7329 5.06556Z" fill="#282C34"/>
</svg>

After

Width:  |  Height:  |  Size: 636 B

68
assets/keymaps/atom.json Normal file
View File

@@ -0,0 +1,68 @@
[
{
"bindings": {
"cmd-k cmd-p": "workspace::ActivatePreviousPane",
"cmd-k cmd-n": "workspace::ActivateNextPane"
}
},
{
"context": "Editor",
"bindings": {
"cmd-b": "editor::GoToDefinition",
"cmd-<": "editor::ScrollCursorCenter",
"cmd-g": [
"editor::SelectNext",
{
"replace_newest": true
}
],
"ctrl-shift-down": "editor::AddSelectionBelow",
"ctrl-shift-up": "editor::AddSelectionAbove",
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine"
}
},
{
"context": "Editor && mode == full",
"bindings": {
"cmd-r": "outline::Toggle"
}
},
{
"context": "BufferSearchBar",
"bindings": {
"cmd-f3": "search::SelectNextMatch",
"cmd-shift-f3": "search::SelectPrevMatch"
}
},
{
"context": "Workspace",
"bindings": {
"cmd-\\": "workspace::ToggleLeftSidebar",
"cmd-k cmd-b": "workspace::ToggleLeftSidebar",
"cmd-t": "file_finder::Toggle",
"cmd-shift-r": "project_symbols::Toggle"
}
},
{
"context": "Pane",
"bindings": {
"alt-cmd-/": "search::ToggleRegex",
"ctrl-0": "project_panel::ToggleFocus"
}
},
{
"context": "ProjectPanel",
"bindings": {
"ctrl-[": "project_panel::CollapseSelectedEntry",
"ctrl-b": "project_panel::CollapseSelectedEntry",
"h": "project_panel::CollapseSelectedEntry",
"ctrl-]": "project_panel::ExpandSelectedEntry",
"ctrl-f": "project_panel::ExpandSelectedEntry",
"ctrl-shift-c": "project_panel::CopyPath"
}
},
{
"context": "Dock",
"bindings": {}
}
]

View File

@@ -164,6 +164,7 @@
"bindings": {
"enter": "editor::Newline",
"cmd-enter": "editor::NewlineBelow",
"alt-z": "editor::ToggleSoftWrap",
"cmd-f": [
"buffer_search::Deploy",
{
@@ -227,7 +228,13 @@
"replace_newest": true
}
],
"cmd-/": "editor::ToggleComments",
"cmd-k cmd-i": "editor::Hover",
"cmd-/": [
"editor::ToggleComments",
{
"advance_downwards": false
}
],
"alt-up": "editor::SelectLargerSyntaxNode",
"alt-down": "editor::SelectSmallerSyntaxNode",
"cmd-u": "editor::UndoSelection",
@@ -242,7 +249,8 @@
"alt-cmd-[": "editor::Fold",
"alt-cmd-]": "editor::UnfoldLines",
"ctrl-space": "editor::ShowCompletions",
"cmd-.": "editor::ToggleCodeActions"
"cmd-.": "editor::ToggleCodeActions",
"alt-cmd-r": "editor::RevealInFinder"
}
},
{
@@ -346,7 +354,8 @@
"cmd-shift-p": "command_palette::Toggle",
"cmd-shift-m": "diagnostics::Deploy",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll"
"cmd-alt-s": "workspace::SaveAll",
"cmd-k m": "language_selector::Toggle"
}
},
// Bindings from Sublime Text
@@ -412,7 +421,7 @@
{
"bindings": {
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
"cmd-shift-c": "collab::ToggleCollaborationMenu",
"cmd-shift-c": "collab::ToggleContactsMenu",
"cmd-alt-i": "zed::DebugElements"
}
},
@@ -433,8 +442,7 @@
{
"context": "Workspace",
"bindings": {
"shift-escape": "dock::FocusDock",
"cmd-shift-b": "workspace::ToggleRightSidebar"
"shift-escape": "dock::FocusDock"
}
},
{
@@ -445,15 +453,16 @@
}
},
{
"context": "Dock",
"context": "Pane",
"bindings": {
"shift-escape": "dock::HideDock"
"cmd-escape": "dock::AddTabToDock"
}
},
{
"context": "Pane",
"context": "Pane && docked",
"bindings": {
"cmd-escape": "dock::MoveActiveItemToDock"
"shift-escape": "dock::HideDock",
"cmd-escape": "dock::RemoveTabFromDock"
}
},
{
@@ -466,7 +475,8 @@
"cmd-v": "project_panel::Paste",
"cmd-alt-c": "project_panel::CopyPath",
"f2": "project_panel::Rename",
"backspace": "project_panel::Delete"
"backspace": "project_panel::Delete",
"alt-cmd-r": "project_panel::RevealInFinder"
}
},
{
@@ -530,4 +540,4 @@
]
}
}
]
]

View File

@@ -0,0 +1,78 @@
[
{
"bindings": {
"cmd-shift-[": "pane::ActivatePrevItem",
"cmd-shift-]": "pane::ActivateNextItem"
}
},
{
"context": "Editor",
"bindings": {
"ctrl->": "zed::IncreaseBufferFontSize",
"ctrl-<": "zed::DecreaseBufferFontSize",
"cmd-d": "editor::DuplicateLine",
"cmd-pagedown": "editor::MovePageDown",
"cmd-pageup": "editor::MovePageUp",
"ctrl-alt-shift-b": "editor::SelectToPreviousWordStart",
"shift-enter": "editor::NewlineBelow",
"cmd--": "editor::Fold",
"cmd-=": "editor::UnfoldLines",
"alt-shift-g": "editor::SplitSelectionIntoLines",
"ctrl-g": [
"editor::SelectNext",
{
"replace_newest": false
}
],
"cmd-/": [
"editor::ToggleComments",
{
"advance_downwards": true
}
],
"shift-alt-up": "editor::MoveLineUp",
"shift-alt-down": "editor::MoveLineDown",
"cmd-[": "pane::GoBack",
"cmd-]": "pane::GoForward",
"alt-f7": "editor::FindAllReferences",
"cmd-alt-f7": "editor::FindAllReferences",
"cmd-b": "editor::GoToDefinition",
"cmd-alt-b": "editor::GoToDefinition",
"cmd-shift-b": "editor::GoToTypeDefinition",
"alt-enter": "editor::ToggleCodeActions",
"f2": "editor::GoToDiagnostic",
"cmd-f2": "editor::GoToPrevDiagnostic",
"ctrl-alt-shift-down": "editor::GoToHunk",
"ctrl-alt-shift-up": "editor::GoToPrevHunk",
"cmd-home": "editor::MoveToBeginning",
"cmd-end": "editor::MoveToEnd",
"cmd-shift-home": "editor::SelectToBeginning",
"cmd-shift-end": "editor::SelectToEnd"
}
},
{
"context": "Editor && mode == full",
"bindings": {
"cmd-f12": "outline::Toggle",
"cmd-7": "outline::Toggle",
"cmd-shift-o": "file_finder::Toggle",
"cmd-l": "go_to_line::Toggle"
}
},
{
"context": "Workspace",
"bindings": {
"cmd-shift-a": "command_palette::Toggle",
"cmd-alt-o": "project_symbols::Toggle",
"cmd-1": "workspace::ToggleLeftSidebar",
"cmd-6": "diagnostics::Deploy",
"alt-f12": "dock::FocusDock"
}
},
{
"context": "Dock",
"bindings": {
"alt-f12": "dock::HideDock"
}
}
]

View File

@@ -0,0 +1,60 @@
[
{
"bindings": {
"cmd-shift-[": "pane::ActivatePrevItem",
"cmd-shift-]": "pane::ActivateNextItem",
"ctrl-pagedown": "pane::ActivatePrevItem",
"ctrl-pageup": "pane::ActivateNextItem",
"ctrl-shift-tab": "pane::ActivateNextItem",
"ctrl-tab": "pane::ActivatePrevItem",
"cmd-+": "zed::IncreaseBufferFontSize"
}
},
{
"context": "Editor",
"bindings": {
"ctrl-shift-up": "editor::AddSelectionAbove",
"ctrl-shift-down": "editor::AddSelectionBelow",
"cmd-shift-space": "editor::SelectAll",
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
"cmd-shift-a": "editor::SelectLargerSyntaxNode",
"shift-f12": "editor::FindAllReferences",
"alt-cmd-down": "editor::GoToDefinition",
"alt-shift-cmd-down": "editor::FindAllReferences",
"ctrl-.": "editor::GoToHunk",
"ctrl-,": "editor::GoToPrevHunk",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd"
}
},
{
"context": "Editor && mode == full",
"bindings": {
"cmd-r": "outline::Toggle"
}
},
{
"context": "Pane",
"bindings": {
"f4": "search::SelectNextMatch",
"shift-f4": "search::SelectPrevMatch"
}
},
{
"context": "Workspace",
"bindings": {
"ctrl-`": "dock::FocusDock",
"cmd-k cmd-b": "workspace::ToggleLeftSidebar",
"cmd-t": "file_finder::Toggle",
"shift-cmd-r": "project_symbols::Toggle",
// Currently busted: https://github.com/zed-industries/feedback/issues/898
"ctrl-0": "project_panel::ToggleFocus"
}
},
{
"context": "Dock",
"bindings": {
"ctrl-`": "dock::HideDock"
}
}
]

View File

@@ -0,0 +1,90 @@
[
{
"bindings": {
"cmd-shift-o": "projects::OpenRecent",
"cmd-alt-tab": "project_panel::ToggleFocus"
}
},
{
"context": "Editor",
"bindings": {
"cmd-l": "go_to_line::Toggle",
"ctrl-shift-d": "editor::DuplicateLine",
"cmd-b": "editor::GoToDefinition",
"cmd-j": "editor::ScrollCursorCenter",
"cmd-enter": "editor::NewlineBelow",
"cmd-shift-l": "editor::SelectLine",
"cmd-shift-t": "outline::Toggle",
"alt-backspace": "editor::DeleteToPreviousWordStart",
"alt-shift-backspace": "editor::DeleteToNextWordEnd",
"alt-delete": "editor::DeleteToNextWordEnd",
"alt-shift-delete": "editor::DeleteToNextWordEnd",
"ctrl-backspace": "editor::DeleteToPreviousSubwordStart",
"ctrl-delete": "editor::DeleteToNextSubwordEnd",
"alt-left": [
"editor::MoveToPreviousWordStart",
{
"stop_at_soft_wraps": true
}
],
"alt-right": [
"editor::MoveToNextWordEnd",
{
"stop_at_soft_wraps": true
}
],
"ctrl-left": "editor::MoveToPreviousSubwordStart",
"ctrl-right": "editor::MoveToNextSubwordEnd",
"cmd-shift-left": "editor::SelectToBeginningOfLine",
"cmd-shift-right": "editor::SelectToEndOfLine",
"alt-shift-left": [
"editor::SelectToBeginningOfLine",
{
"stop_at_soft_wraps": true
}
],
"alt-shift-right": [
"editor::SelectToEndOfLine",
{
"stop_at_soft_wraps": true
}
],
"ctrl-shift-left": "editor::SelectToPreviousSubwordStart",
"ctrl-shift-right": "editor::SelectToNextSubwordEnd"
}
},
{
"context": "Editor && mode == full",
"bindings": {}
},
{
"context": "BufferSearchBar",
"bindings": {
"ctrl-s": "search::SelectNextMatch",
"ctrl-shift-s": "search::SelectPrevMatch"
}
},
{
"context": "Workspace",
"bindings": {
"cmd-alt-ctrl-d": "workspace::ToggleLeftSidebar",
"cmd-t": "file_finder::Toggle",
"cmd-shift-t": "project_symbols::Toggle"
}
},
{
"context": "Pane",
"bindings": {
"alt-cmd-r": "search::ToggleRegex",
"ctrl-tab": "project_panel::ToggleFocus"
}
},
{
"context": "ProjectPanel",
"bindings": {}
},
{
"context": "Dock",
"bindings": {}
}
]

View File

@@ -27,6 +27,7 @@
"h": "vim::Left",
"backspace": "vim::Backspace",
"j": "vim::Down",
"enter": "vim::NextLineStart",
"k": "vim::Up",
"l": "vim::Right",
"$": "vim::EndOfLine",
@@ -233,7 +234,8 @@
"escape": [
"vim::SwitchMode",
"Normal"
]
],
"d": "editor::GoToDefinition"
}
},
{
@@ -315,7 +317,9 @@
{
"context": "Editor && VimWaiting",
"bindings": {
"*": "gpui::KeyPressed"
"tab": "vim::Tab",
"enter": "vim::Enter",
"escape": "editor::Cancel"
}
}
]

View File

@@ -20,6 +20,8 @@
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
// Whether the screen sharing icon is showed in the os status bar.
"show_call_status_icon": true,
// Whether new projects should start out 'online'. Online projects
// appear in the contacts panel under your name, so that your contacts
// can see which projects you are working on. Regardless of this
@@ -48,7 +50,13 @@
// "default_dock_anchor": "right"
// 3. Position the dock full screen over the entire workspace"
// "default_dock_anchor": "expanded"
"default_dock_anchor": "right",
"default_dock_anchor": "bottom",
// Whether or not to remove any trailing whitespace from lines of a buffer
// before saving it.
"remove_trailing_whitespace_on_save": true,
// Whether or not to ensure there's a single newline at the end of a buffer
// when saving it.
"ensure_final_newline_on_save": true,
// Whether or not to perform a buffer format before saving
"format_on_save": "on",
// How to perform a buffer format. This setting can take two values:
@@ -81,13 +89,15 @@
"hard_tabs": false,
// How many columns a tab should occupy.
"tab_size": 4,
// Control what info Zed sends to our servers
// Control what info is collected by Zed.
"telemetry": {
// Send debug info like crash reports.
"diagnostics": true,
// Send anonymized usage data like what languages you're using Zed with.
"metrics": true
},
// Automatically update Zed
"auto_update": true,
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
@@ -219,6 +229,9 @@
},
"TSX": {
"tab_size": 2
},
"YAML": {
"tab_size": 2
}
},
// LSP Specific settings.

View File

@@ -33,6 +33,19 @@ struct LspStatus {
status: LanguageServerBinaryStatus,
}
struct PendingWork<'a> {
language_server_name: &'a str,
progress_token: &'a str,
progress: &'a LanguageServerProgress,
}
#[derive(Default)]
struct Content {
icon: Option<&'static str>,
message: String,
action: Option<Box<dyn Action>>,
}
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ActivityIndicator::show_error_message);
cx.add_action(ActivityIndicator::dismiss_error_message);
@@ -69,6 +82,8 @@ impl ActivityIndicator {
if let Some(auto_updater) = auto_updater.as_ref() {
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
}
cx.observe_active_labeled_tasks(|_, cx| cx.notify())
.detach();
Self {
statuses: Default::default(),
@@ -130,7 +145,7 @@ impl ActivityIndicator {
fn pending_language_server_work<'a>(
&self,
cx: &'a AppContext,
) -> impl Iterator<Item = (&'a str, &'a str, &'a LanguageServerProgress)> {
) -> impl Iterator<Item = PendingWork<'a>> {
self.project
.read(cx)
.language_server_statuses()
@@ -142,23 +157,29 @@ impl ActivityIndicator {
let mut pending_work = status
.pending_work
.iter()
.map(|(token, progress)| (status.name.as_str(), token.as_str(), progress))
.map(|(token, progress)| PendingWork {
language_server_name: status.name.as_str(),
progress_token: token.as_str(),
progress,
})
.collect::<SmallVec<[_; 4]>>();
pending_work.sort_by_key(|(_, _, progress)| Reverse(progress.last_update_at));
pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
Some(pending_work)
}
})
.flatten()
}
fn content_to_render(
&mut self,
cx: &mut RenderContext<Self>,
) -> (Option<&'static str>, String, Option<Box<dyn Action>>) {
fn content_to_render(&mut self, cx: &mut RenderContext<Self>) -> Content {
// Show any language server has pending activity.
let mut pending_work = self.pending_language_server_work(cx);
if let Some((lang_server_name, progress_token, progress)) = pending_work.next() {
let mut message = lang_server_name.to_string();
if let Some(PendingWork {
language_server_name,
progress_token,
progress,
}) = pending_work.next()
{
let mut message = language_server_name.to_string();
message.push_str(": ");
if let Some(progress_message) = progress.message.as_ref() {
@@ -176,7 +197,11 @@ impl ActivityIndicator {
write!(&mut message, " + {} more", additional_work_count).unwrap();
}
return (None, message, None);
return Content {
icon: None,
message,
action: None,
};
}
// Show any language server installation info.
@@ -199,19 +224,19 @@ impl ActivityIndicator {
}
if !downloading.is_empty() {
return (
Some(DOWNLOAD_ICON),
format!(
return Content {
icon: Some(DOWNLOAD_ICON),
message: format!(
"Downloading {} language server{}...",
downloading.join(", "),
if downloading.len() > 1 { "s" } else { "" }
),
None,
);
action: None,
};
} else if !checking_for_update.is_empty() {
return (
Some(DOWNLOAD_ICON),
format!(
return Content {
icon: Some(DOWNLOAD_ICON),
message: format!(
"Checking for updates to {} language server{}...",
checking_for_update.join(", "),
if checking_for_update.len() > 1 {
@@ -220,49 +245,61 @@ impl ActivityIndicator {
""
}
),
None,
);
action: None,
};
} else if !failed.is_empty() {
return (
Some(WARNING_ICON),
format!(
return Content {
icon: Some(WARNING_ICON),
message: format!(
"Failed to download {} language server{}. Click to show error.",
failed.join(", "),
if failed.len() > 1 { "s" } else { "" }
),
Some(Box::new(ShowErrorMessage)),
);
action: Some(Box::new(ShowErrorMessage)),
};
}
// Show any application auto-update info.
if let Some(updater) = &self.auto_updater {
match &updater.read(cx).status() {
AutoUpdateStatus::Checking => (
Some(DOWNLOAD_ICON),
"Checking for Zed updates…".to_string(),
None,
),
AutoUpdateStatus::Downloading => (
Some(DOWNLOAD_ICON),
"Downloading Zed update…".to_string(),
None,
),
AutoUpdateStatus::Installing => (
Some(DOWNLOAD_ICON),
"Installing Zed update…".to_string(),
None,
),
AutoUpdateStatus::Updated => (None, "Restart to update Zed".to_string(), None),
AutoUpdateStatus::Errored => (
Some(WARNING_ICON),
"Auto update failed".to_string(),
Some(Box::new(DismissErrorMessage)),
),
return match &updater.read(cx).status() {
AutoUpdateStatus::Checking => Content {
icon: Some(DOWNLOAD_ICON),
message: "Checking for Zed updates…".to_string(),
action: None,
},
AutoUpdateStatus::Downloading => Content {
icon: Some(DOWNLOAD_ICON),
message: "Downloading Zed update…".to_string(),
action: None,
},
AutoUpdateStatus::Installing => Content {
icon: Some(DOWNLOAD_ICON),
message: "Installing Zed update…".to_string(),
action: None,
},
AutoUpdateStatus::Updated => Content {
icon: None,
message: "Click to restart and update Zed".to_string(),
action: Some(Box::new(workspace::Restart)),
},
AutoUpdateStatus::Errored => Content {
icon: Some(WARNING_ICON),
message: "Auto update failed".to_string(),
action: Some(Box::new(DismissErrorMessage)),
},
AutoUpdateStatus::Idle => Default::default(),
}
} else {
Default::default()
};
}
if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
return Content {
icon: None,
message: most_recent_active_task.to_string(),
action: None,
};
}
Default::default()
}
}
@@ -276,7 +313,11 @@ impl View for ActivityIndicator {
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let (icon, message, action) = self.content_to_render(cx);
let Content {
icon,
message,
action,
} = self.content_to_render(cx);
let mut element = MouseEventHandler::<Self>::new(0, cx, |state, cx| {
let theme = &cx

View File

@@ -9,6 +9,7 @@ use gpui::{
MutableAppContext, Task, WeakViewHandle,
};
use serde::Deserialize;
use settings::Settings;
use smol::{fs::File, io::AsyncReadExt, process::Command};
use std::{ffi::OsString, sync::Arc, time::Duration};
use update_notification::UpdateNotification;
@@ -53,7 +54,23 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut Mutab
let server_url = server_url;
let auto_updater = cx.add_model(|cx| {
let updater = AutoUpdater::new(version, http_client, server_url.clone());
updater.start_polling(cx).detach();
let mut update_subscription = cx
.global::<Settings>()
.auto_update
.then(|| updater.start_polling(cx));
cx.observe_global::<Settings, _>(move |updater, cx| {
if cx.global::<Settings>().auto_update {
if update_subscription.is_none() {
*(&mut update_subscription) = Some(updater.start_polling(cx))
}
} else {
(&mut update_subscription).take();
}
})
.detach();
updater
});
cx.set_global(Some(auto_updater));

View File

@@ -78,7 +78,7 @@ impl View for UpdateNotification {
)
.with_child({
let style = theme.action_message.style_for(state, false);
Text::new("View the release notes".to_string(), style.text.clone())
Text::new("View the release notes", style.text.clone())
.contained()
.with_style(style.container)
.boxed()

View File

@@ -47,7 +47,7 @@ impl View for Breadcrumbs {
{
Flex::row()
.with_children(Itertools::intersperse_with(breadcrumbs.into_iter(), || {
Label::new("".to_string(), theme.breadcrumbs.text.clone()).boxed()
Label::new("", theme.breadcrumbs.text.clone()).boxed()
}))
.contained()
.with_style(theme.breadcrumbs.container)

View File

@@ -28,6 +28,7 @@ fs = { path = "../fs" }
language = { path = "../language" }
media = { path = "../media" }
project = { path = "../project" }
settings = { path = "../settings" }
util = { path = "../util" }
anyhow = "1.0.38"

View File

@@ -1,18 +1,22 @@
pub mod participant;
pub mod room;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use client::{proto, Client, TypedEnvelope, User, UserStore};
use collections::HashSet;
use futures::{future::Shared, FutureExt};
use postage::watch;
use gpui::{
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
Subscription, Task, WeakModelHandle,
};
pub use participant::ParticipantLocation;
use postage::watch;
use project::Project;
pub use participant::ParticipantLocation;
pub use room::Room;
use std::sync::Arc;
pub fn init(client: Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut MutableAppContext) {
let active_call = cx.add_model(|cx| ActiveCall::new(client, user_store, cx));
@@ -27,8 +31,10 @@ pub struct IncomingCall {
pub initial_project: Option<proto::ParticipantProject>,
}
/// Singleton global maintaining the user's participation in a room across workspaces.
pub struct ActiveCall {
room: Option<(ModelHandle<Room>, Vec<Subscription>)>,
pending_room_creation: Option<Shared<Task<Result<ModelHandle<Room>, Arc<anyhow::Error>>>>>,
location: Option<WeakModelHandle<Project>>,
pending_invites: HashSet<u64>,
incoming_call: (
@@ -52,6 +58,7 @@ impl ActiveCall {
) -> Self {
Self {
room: None,
pending_room_creation: None,
location: None,
pending_invites: Default::default(),
incoming_call: watch::channel(),
@@ -120,45 +127,74 @@ impl ActiveCall {
initial_project: Option<ModelHandle<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
if !self.pending_invites.insert(called_user_id) {
return Task::ready(Err(anyhow!("user was already invited")));
}
cx.notify();
cx.spawn(|this, mut cx| async move {
let invite = async {
if let Some(room) = this.read_with(&cx, |this, _| this.room().cloned()) {
let initial_project_id = if let Some(initial_project) = initial_project {
Some(
room.update(&mut cx, |room, cx| {
room.share_project(initial_project, cx)
})
let room = if let Some(room) = self.room().cloned() {
Some(Task::ready(Ok(room)).shared())
} else {
self.pending_room_creation.clone()
};
let invite = if let Some(room) = room {
cx.spawn_weak(|_, mut cx| async move {
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
let initial_project_id = if let Some(initial_project) = initial_project {
Some(
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))
.await?,
)
} else {
None
};
room.update(&mut cx, |room, cx| {
room.call(called_user_id, initial_project_id, cx)
})
.await?;
)
} else {
let room = cx
.update(|cx| {
Room::create(called_user_id, initial_project, client, user_store, cx)
})
.await?;
this.update(&mut cx, |this, cx| this.set_room(Some(room), cx))
.await?;
None
};
Ok(())
};
room.update(&mut cx, |room, cx| {
room.call(called_user_id, initial_project_id, cx)
})
.await?;
anyhow::Ok(())
})
} else {
let client = self.client.clone();
let user_store = self.user_store.clone();
let room = cx
.spawn(|this, mut cx| async move {
let create_room = async {
let room = cx
.update(|cx| {
Room::create(
called_user_id,
initial_project,
client,
user_store,
cx,
)
})
.await?;
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))
.await?;
anyhow::Ok(room)
};
let room = create_room.await;
this.update(&mut cx, |this, _| this.pending_room_creation = None);
room.map_err(Arc::new)
})
.shared();
self.pending_room_creation = Some(room.clone());
cx.foreground().spawn(async move {
room.await.map_err(|err| anyhow!("{:?}", err))?;
anyhow::Ok(())
})
};
cx.spawn(|this, mut cx| async move {
let result = invite.await;
this.update(&mut cx, |this, cx| {
this.pending_invites.remove(&called_user_id);
@@ -228,12 +264,13 @@ impl ActiveCall {
Ok(())
}
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
if let Some((room, _)) = self.room.take() {
room.update(cx, |room, cx| room.leave(cx))?;
cx.notify();
room.update(cx, |room, cx| room.leave(cx))
} else {
Task::ready(Ok(()))
}
Ok(())
}
pub fn share_project(
@@ -248,6 +285,18 @@ impl ActiveCall {
}
}
pub fn unshare_project(
&mut self,
project: ModelHandle<Project>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
room.update(cx, |room, cx| room.unshare_project(project, cx))
} else {
Err(anyhow!("no active call"))
}
}
pub fn set_location(
&mut self,
project: Option<&ModelHandle<Project>>,

View File

@@ -17,10 +17,10 @@ use language::LanguageRegistry;
use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate};
use postage::stream::Stream;
use project::Project;
use std::{mem, sync::Arc, time::Duration};
use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration};
use util::{post_inc, ResultExt, TryFutureExt};
pub const RECONNECT_TIMEOUT: Duration = client::RECEIVE_TIMEOUT;
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
@@ -55,6 +55,7 @@ pub struct Room {
leave_when_empty: bool,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec<PeerId>>,
subscriptions: Vec<client::Subscription>,
pending_room_update: Option<Task<()>>,
maintain_connection: Option<Task<Option<()>>>,
@@ -63,10 +64,27 @@ pub struct Room {
impl Entity for Room {
type Event = Event;
fn release(&mut self, _: &mut MutableAppContext) {
fn release(&mut self, cx: &mut MutableAppContext) {
if self.status.is_online() {
log::info!("room was released, sending leave message");
let _ = self.client.send(proto::LeaveRoom {});
self.leave_internal(cx).detach_and_log_err(cx);
}
}
fn app_will_quit(
&mut self,
cx: &mut MutableAppContext,
) -> Option<Pin<Box<dyn Future<Output = ()>>>> {
if self.status.is_online() {
let leave = self.leave_internal(cx);
Some(
cx.background()
.spawn(async move {
leave.await.log_err();
})
.boxed(),
)
} else {
None
}
}
}
@@ -148,6 +166,7 @@ impl Room {
pending_room_update: None,
client,
user_store,
follows_by_leader_id_project_id: Default::default(),
maintain_connection: Some(maintain_connection),
}
}
@@ -232,13 +251,17 @@ impl Room {
&& self.pending_call_count == 0
}
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
if self.status.is_offline() {
return Err(anyhow!("room is offline"));
}
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
cx.emit(Event::Left);
self.leave_internal(cx)
}
fn leave_internal(&mut self, cx: &mut MutableAppContext) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
}
log::info!("leaving room");
for project in self.shared_projects.drain() {
@@ -264,8 +287,12 @@ impl Room {
self.live_kit.take();
self.pending_room_update.take();
self.maintain_connection.take();
self.client.send(proto::LeaveRoom {})?;
Ok(())
let leave_room = self.client.request(proto::LeaveRoom {});
cx.background().spawn(async move {
leave_room.await?;
anyhow::Ok(())
})
}
async fn maintain_connection(
@@ -275,14 +302,12 @@ impl Room {
) -> Result<()> {
let mut client_status = client.status();
loop {
let is_connected = client_status
.next()
.await
.map_or(false, |s| s.is_connected());
let _ = client_status.try_recv();
let is_connected = client_status.borrow().is_connected();
// Even if we're initially connected, any future change of the status means we momentarily disconnected.
if !is_connected || client_status.next().await.is_some() {
log::info!("detected client disconnection");
this.upgrade(&cx)
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, cx| {
@@ -296,12 +321,7 @@ impl Room {
let client_reconnection = async {
let mut remaining_attempts = 3;
while remaining_attempts > 0 {
log::info!(
"waiting for client status change, remaining attempts {}",
remaining_attempts
);
let Some(status) = client_status.next().await else { break };
if status.is_connected() {
if client_status.borrow().is_connected() {
log::info!("client reconnected, attempting to rejoin room");
let Some(this) = this.upgrade(&cx) else { break };
@@ -315,7 +335,15 @@ impl Room {
} else {
remaining_attempts -= 1;
}
} else if client_status.borrow().is_signed_out() {
return false;
}
log::info!(
"waiting for client status change, remaining attempts {}",
remaining_attempts
);
client_status.next().await;
}
false
}
@@ -337,18 +365,20 @@ impl Room {
}
}
// The client failed to re-establish a connection to the server
// or an error occurred while trying to re-join the room. Either way
// we leave the room and return an error.
if let Some(this) = this.upgrade(&cx) {
log::info!("reconnection failed, leaving room");
let _ = this.update(&mut cx, |this, cx| this.leave(cx));
}
return Err(anyhow!(
"can't reconnect to room: client failed to re-establish connection"
));
break;
}
}
// The client failed to re-establish a connection to the server
// or an error occurred while trying to re-join the room. Either way
// we leave the room and return an error.
if let Some(this) = this.upgrade(&cx) {
log::info!("reconnection failed, leaving room");
let _ = this.update(&mut cx, |this, cx| this.leave(cx));
}
Err(anyhow!(
"can't reconnect to room: client failed to re-establish connection"
))
}
fn rejoin(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
@@ -457,6 +487,12 @@ impl Room {
self.participant_user_ids.contains(&user_id)
}
pub fn followers_for(&self, leader_id: PeerId, project_id: u64) -> &[PeerId] {
self.follows_by_leader_id_project_id
.get(&(leader_id, project_id))
.map_or(&[], |v| v.as_slice())
}
async fn handle_room_updated(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::RoomUpdated>,
@@ -487,11 +523,13 @@ impl Room {
.iter()
.map(|p| p.user_id)
.collect::<Vec<_>>();
let remote_participant_user_ids = room
.participants
.iter()
.map(|p| p.user_id)
.collect::<Vec<_>>();
let (remote_participants, pending_participants) =
self.user_store.update(cx, move |user_store, cx| {
(
@@ -499,6 +537,7 @@ impl Room {
user_store.get_users(pending_participant_user_ids, cx),
)
});
self.pending_room_update = Some(cx.spawn(|this, mut cx| async move {
let (remote_participants, pending_participants) =
futures::join!(remote_participants, pending_participants);
@@ -620,6 +659,27 @@ impl Room {
}
}
this.follows_by_leader_id_project_id.clear();
for follower in room.followers {
let project_id = follower.project_id;
let (leader, follower) = match (follower.leader_id, follower.follower_id) {
(Some(leader), Some(follower)) => (leader, follower),
_ => {
log::error!("Follower message {follower:?} missing some state");
continue;
}
};
let list = this
.follows_by_leader_id_project_id
.entry((leader, project_id))
.or_insert(Vec::new());
if !list.contains(&follower) {
list.push(follower);
}
}
this.pending_room_update.take();
if this.should_leave() {
log::info!("room is empty, leaving");
@@ -723,10 +783,10 @@ impl Room {
this.update(&mut cx, |this, cx| {
this.pending_call_count -= 1;
if this.should_leave() {
this.leave(cx)?;
this.leave(cx).detach_and_log_err(cx);
}
result
})?;
});
result?;
Ok(())
})
}
@@ -793,6 +853,20 @@ impl Room {
})
}
pub(crate) fn unshare_project(
&mut self,
project: ModelHandle<Project>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
let project_id = match project.read(cx).remote_id() {
Some(project_id) => project_id,
None => return Ok(()),
};
self.client.send(proto::UnshareProject { project_id })?;
project.update(cx, |this, cx| this.unshare(cx))
}
pub(crate) fn set_location(
&mut self,
project: Option<&ModelHandle<Project>>,

View File

@@ -66,7 +66,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, [Authenticate]);
actions!(client, [Authenticate, SignOut]);
pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
cx.add_global_action({
@@ -79,6 +79,16 @@ pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
.detach();
}
});
cx.add_global_action({
let client = client.clone();
move |_: &SignOut, cx| {
let client = client.clone();
cx.spawn(|cx| async move {
client.disconnect(&cx);
})
.detach();
}
});
}
pub struct Client {
@@ -169,6 +179,10 @@ impl Status {
pub fn is_connected(&self) -> bool {
matches!(self, Self::Connected { .. })
}
pub fn is_signed_out(&self) -> bool {
matches!(self, Self::SignedOut | Self::UpgradeRequired)
}
}
struct ClientState {
@@ -1152,11 +1166,9 @@ impl Client {
})
}
pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
let conn_id = self.connection_id()?;
self.peer.disconnect(conn_id);
pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
self.peer.teardown();
self.set_status(Status::SignedOut, cx);
Ok(())
}
fn connection_id(&self) -> Result<ConnectionId> {
@@ -1324,6 +1336,10 @@ impl Client {
pub fn metrics_id(&self) -> Option<Arc<str>> {
self.telemetry.metrics_id()
}
pub fn is_staff(&self) -> Option<bool> {
self.telemetry.is_staff()
}
}
impl WeakSubscriber {

View File

@@ -9,7 +9,7 @@ pub use isahc::{
Error,
};
use smol::future::FutureExt;
use std::sync::Arc;
use std::{sync::Arc, time::Duration};
pub use url::Url;
pub type Request = isahc::Request<AsyncBody>;
@@ -41,7 +41,13 @@ pub trait HttpClient: Send + Sync {
}
pub fn client() -> Arc<dyn HttpClient> {
Arc::new(isahc::HttpClient::builder().build().unwrap())
Arc::new(
isahc::HttpClient::builder()
.connect_timeout(Duration::from_secs(5))
.low_speed_timeout(100, Duration::from_secs(5))
.build()
.unwrap(),
)
}
impl HttpClient for isahc::HttpClient {

View File

@@ -40,6 +40,7 @@ struct TelemetryState {
next_event_id: usize,
flush_task: Option<Task<()>>,
log_file: Option<NamedTempFile>,
is_staff: Option<bool>,
}
const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track";
@@ -125,6 +126,7 @@ impl Telemetry {
flush_task: Default::default(),
next_event_id: 0,
log_file: None,
is_staff: None,
}),
});
@@ -202,6 +204,7 @@ impl Telemetry {
let device_id = state.device_id.clone();
let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
state.metrics_id = metrics_id.clone();
state.is_staff = Some(is_staff);
drop(state);
if let Some((token, device_id)) = MIXPANEL_TOKEN.as_ref().zip(device_id) {
@@ -221,7 +224,7 @@ impl Telemetry {
.header("Content-Type", "application/json")
.body(json_bytes.into())?;
this.http_client.send(request).await?;
Ok(())
anyhow::Ok(())
}
.log_err(),
)
@@ -282,6 +285,10 @@ impl Telemetry {
self.state.lock().metrics_id.clone()
}
pub fn is_staff(self: &Arc<Self>) -> Option<bool> {
self.state.lock().is_staff
}
fn flush(self: &Arc<Self>) {
let mut state = self.state.lock();
let mut events = mem::take(&mut state.queue);
@@ -313,7 +320,7 @@ impl Telemetry {
.header("Content-Type", "application/json")
.body(json_bytes.into())?;
this.http_client.send(request).await?;
Ok(())
anyhow::Ok(())
}
.log_err(),
)

View File

@@ -1,4 +1,5 @@
DATABASE_URL = "postgres://postgres@localhost/zed"
DATABASE_MAX_CONNECTIONS = 5
HTTP_PORT = 8080
API_TOKEN = "secret"
INVITE_LINK_PREFIX = "http://localhost:3000/invites/"

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
version = "0.5.4"
version = "0.7.2"
publish = false
[[bin]]

View File

@@ -1,3 +1,4 @@
ZED_ENVIRONMENT=preview
RUST_LOG=info
INVITE_LINK_PREFIX=https://zed.dev/invites/
DATABASE_MAX_CONNECTIONS=10

View File

@@ -1,3 +1,4 @@
ZED_ENVIRONMENT=production
RUST_LOG=info
INVITE_LINK_PREFIX=https://zed.dev/invites/
DATABASE_MAX_CONNECTIONS=85

View File

@@ -1,3 +1,4 @@
ZED_ENVIRONMENT=staging
RUST_LOG=info
INVITE_LINK_PREFIX=https://staging.zed.dev/invites/
DATABASE_MAX_CONNECTIONS=5

View File

@@ -59,6 +59,13 @@ spec:
ports:
- containerPort: 8080
protocol: TCP
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /
@@ -73,6 +80,8 @@ spec:
secretKeyRef:
name: database
key: url
- name: DATABASE_MAX_CONNECTIONS
value: "${DATABASE_MAX_CONNECTIONS}"
- name: API_TOKEN
valueFrom:
secretKeyRef:

View File

@@ -143,3 +143,17 @@ CREATE TABLE "servers" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"environment" VARCHAR NOT NULL
);
CREATE TABLE "followers" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"room_id" INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE,
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
"leader_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
"leader_connection_id" INTEGER NOT NULL,
"follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
"follower_connection_id" INTEGER NOT NULL
);
CREATE UNIQUE INDEX
"index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");

View File

@@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS "followers" (
"id" SERIAL PRIMARY KEY,
"room_id" INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE,
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
"leader_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
"leader_connection_id" INTEGER NOT NULL,
"follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
"follower_connection_id" INTEGER NOT NULL
);
CREATE UNIQUE INDEX
"index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");

View File

@@ -1,5 +1,6 @@
mod access_token;
mod contact;
mod follower;
mod language_server;
mod project;
mod project_collaborator;
@@ -157,7 +158,7 @@ impl Database {
room_id: RoomId,
new_server_id: ServerId,
) -> Result<RoomGuard<RefreshedRoom>> {
self.room_transaction(|tx| async move {
self.room_transaction(room_id, |tx| async move {
let stale_participant_filter = Condition::all()
.add(room_participant::Column::RoomId.eq(room_id))
.add(room_participant::Column::AnsweringConnectionId.is_not_null())
@@ -190,17 +191,18 @@ impl Database {
.filter(room_participant::Column::RoomId.eq(room_id))
.exec(&*tx)
.await?;
project::Entity::delete_many()
.filter(project::Column::RoomId.eq(room_id))
.exec(&*tx)
.await?;
room::Entity::delete_by_id(room_id).exec(&*tx).await?;
}
Ok((
room_id,
RefreshedRoom {
room,
stale_participant_user_ids,
canceled_calls_to_user_ids,
},
))
Ok(RefreshedRoom {
room,
stale_participant_user_ids,
canceled_calls_to_user_ids,
})
})
.await
}
@@ -1129,18 +1131,16 @@ impl Database {
user_id: UserId,
connection: ConnectionId,
live_kit_room: &str,
) -> Result<RoomGuard<proto::Room>> {
self.room_transaction(|tx| async move {
) -> Result<proto::Room> {
self.transaction(|tx| async move {
let room = room::ActiveModel {
live_kit_room: ActiveValue::set(live_kit_room.into()),
..Default::default()
}
.insert(&*tx)
.await?;
let room_id = room.id;
room_participant::ActiveModel {
room_id: ActiveValue::set(room_id),
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(
@@ -1157,8 +1157,8 @@ impl Database {
.insert(&*tx)
.await?;
let room = self.get_room(room_id, &tx).await?;
Ok((room_id, room))
let room = self.get_room(room.id, &tx).await?;
Ok(room)
})
.await
}
@@ -1171,7 +1171,7 @@ impl Database {
called_user_id: UserId,
initial_project_id: Option<ProjectId>,
) -> Result<RoomGuard<(proto::Room, proto::IncomingCall)>> {
self.room_transaction(|tx| async move {
self.room_transaction(room_id, |tx| async move {
room_participant::ActiveModel {
room_id: ActiveValue::set(room_id),
user_id: ActiveValue::set(called_user_id),
@@ -1190,7 +1190,7 @@ impl Database {
let room = self.get_room(room_id, &tx).await?;
let incoming_call = Self::build_incoming_call(&room, called_user_id)
.ok_or_else(|| anyhow!("failed to build incoming call"))?;
Ok((room_id, (room, incoming_call)))
Ok((room, incoming_call))
})
.await
}
@@ -1200,7 +1200,7 @@ impl Database {
room_id: RoomId,
called_user_id: UserId,
) -> Result<RoomGuard<proto::Room>> {
self.room_transaction(|tx| async move {
self.room_transaction(room_id, |tx| async move {
room_participant::Entity::delete_many()
.filter(
room_participant::Column::RoomId
@@ -1210,7 +1210,7 @@ impl Database {
.exec(&*tx)
.await?;
let room = self.get_room(room_id, &tx).await?;
Ok((room_id, room))
Ok(room)
})
.await
}
@@ -1257,7 +1257,7 @@ impl Database {
calling_connection: ConnectionId,
called_user_id: UserId,
) -> Result<RoomGuard<proto::Room>> {
self.room_transaction(|tx| async move {
self.room_transaction(room_id, |tx| async move {
let participant = room_participant::Entity::find()
.filter(
Condition::all()
@@ -1276,14 +1276,13 @@ impl Database {
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no call to cancel"))?;
let room_id = participant.room_id;
room_participant::Entity::delete(participant.into_active_model())
.exec(&*tx)
.await?;
let room = self.get_room(room_id, &tx).await?;
Ok((room_id, room))
Ok(room)
})
.await
}
@@ -1294,7 +1293,7 @@ impl Database {
user_id: UserId,
connection: ConnectionId,
) -> Result<RoomGuard<proto::Room>> {
self.room_transaction(|tx| async move {
self.room_transaction(room_id, |tx| async move {
let result = room_participant::Entity::update_many()
.filter(
Condition::all()
@@ -1316,7 +1315,7 @@ impl Database {
Err(anyhow!("room does not exist or was already joined"))?
} else {
let room = self.get_room(room_id, &tx).await?;
Ok((room_id, room))
Ok(room)
}
})
.await
@@ -1328,9 +1327,9 @@ impl Database {
user_id: UserId,
connection: ConnectionId,
) -> Result<RoomGuard<RejoinedRoom>> {
self.room_transaction(|tx| async {
let room_id = RoomId::from_proto(rejoin_room.id);
self.room_transaction(room_id, |tx| async {
let tx = tx;
let room_id = RoomId::from_proto(rejoin_room.id);
let participant_update = room_participant::Entity::update_many()
.filter(
Condition::all()
@@ -1549,14 +1548,11 @@ impl Database {
}
let room = self.get_room(room_id, &tx).await?;
Ok((
room_id,
RejoinedRoom {
room,
rejoined_projects,
reshared_projects,
},
))
Ok(RejoinedRoom {
room,
rejoined_projects,
reshared_projects,
})
})
.await
}
@@ -1717,13 +1713,75 @@ impl Database {
.await
}
pub async fn follow(
&self,
project_id: ProjectId,
leader_connection: ConnectionId,
follower_connection: ConnectionId,
) -> Result<RoomGuard<proto::Room>> {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
follower::ActiveModel {
room_id: ActiveValue::set(room_id),
project_id: ActiveValue::set(project_id),
leader_connection_server_id: ActiveValue::set(ServerId(
leader_connection.owner_id as i32,
)),
leader_connection_id: ActiveValue::set(leader_connection.id as i32),
follower_connection_server_id: ActiveValue::set(ServerId(
follower_connection.owner_id as i32,
)),
follower_connection_id: ActiveValue::set(follower_connection.id as i32),
..Default::default()
}
.insert(&*tx)
.await?;
let room = self.get_room(room_id, &*tx).await?;
Ok(room)
})
.await
}
pub async fn unfollow(
&self,
project_id: ProjectId,
leader_connection: ConnectionId,
follower_connection: ConnectionId,
) -> Result<RoomGuard<proto::Room>> {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
follower::Entity::delete_many()
.filter(
Condition::all()
.add(follower::Column::ProjectId.eq(project_id))
.add(
follower::Column::LeaderConnectionServerId
.eq(leader_connection.owner_id),
)
.add(follower::Column::LeaderConnectionId.eq(leader_connection.id))
.add(
follower::Column::FollowerConnectionServerId
.eq(follower_connection.owner_id),
)
.add(follower::Column::FollowerConnectionId.eq(follower_connection.id)),
)
.exec(&*tx)
.await?;
let room = self.get_room(room_id, &*tx).await?;
Ok(room)
})
.await
}
pub async fn update_room_participant_location(
&self,
room_id: RoomId,
connection: ConnectionId,
location: proto::ParticipantLocation,
) -> Result<RoomGuard<proto::Room>> {
self.room_transaction(|tx| async {
self.room_transaction(room_id, |tx| async {
let tx = tx;
let location_kind;
let location_project_id;
@@ -1769,7 +1827,7 @@ impl Database {
if result.rows_affected == 1 {
let room = self.get_room(room_id, &tx).await?;
Ok((room_id, room))
Ok(room)
} else {
Err(anyhow!("could not update room participant location"))?
}
@@ -1926,12 +1984,25 @@ impl Database {
}
}
}
drop(db_projects);
let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?;
let mut followers = Vec::new();
while let Some(db_follower) = db_followers.next().await {
let db_follower = db_follower?;
followers.push(proto::Follower {
leader_id: Some(db_follower.leader_connection().into()),
follower_id: Some(db_follower.follower_connection().into()),
project_id: db_follower.project_id.to_proto(),
});
}
Ok(proto::Room {
id: db_room.id.to_proto(),
live_kit_room: db_room.live_kit_room,
participants: participants.into_values().collect(),
pending_participants,
followers,
})
}
@@ -1963,7 +2034,7 @@ impl Database {
connection: ConnectionId,
worktrees: &[proto::WorktreeMetadata],
) -> Result<RoomGuard<(ProjectId, proto::Room)>> {
self.room_transaction(|tx| async move {
self.room_transaction(room_id, |tx| async move {
let participant = room_participant::Entity::find()
.filter(
Condition::all()
@@ -2024,7 +2095,7 @@ impl Database {
.await?;
let room = self.get_room(room_id, &tx).await?;
Ok((room_id, (project.id, room)))
Ok((project.id, room))
})
.await
}
@@ -2034,7 +2105,8 @@ impl Database {
project_id: ProjectId,
connection: ConnectionId,
) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
self.room_transaction(|tx| async move {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
let project = project::Entity::find_by_id(project_id)
@@ -2042,12 +2114,11 @@ impl Database {
.await?
.ok_or_else(|| anyhow!("project not found"))?;
if project.host_connection()? == connection {
let room_id = project.room_id;
project::Entity::delete(project.into_active_model())
.exec(&*tx)
.await?;
let room = self.get_room(room_id, &tx).await?;
Ok((room_id, (room, guest_connection_ids)))
Ok((room, guest_connection_ids))
} else {
Err(anyhow!("cannot unshare a project hosted by another user"))?
}
@@ -2061,7 +2132,8 @@ impl Database {
connection: ConnectionId,
worktrees: &[proto::WorktreeMetadata],
) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
self.room_transaction(|tx| async move {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let project = project::Entity::find_by_id(project_id)
.filter(
Condition::all()
@@ -2079,7 +2151,7 @@ impl Database {
let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?;
let room = self.get_room(project.room_id, &tx).await?;
Ok((project.room_id, (room, guest_connection_ids)))
Ok((room, guest_connection_ids))
})
.await
}
@@ -2124,12 +2196,12 @@ impl Database {
update: &proto::UpdateWorktree,
connection: ConnectionId,
) -> Result<RoomGuard<Vec<ConnectionId>>> {
self.room_transaction(|tx| async move {
let project_id = ProjectId::from_proto(update.project_id);
let worktree_id = update.worktree_id as i64;
let project_id = ProjectId::from_proto(update.project_id);
let worktree_id = update.worktree_id as i64;
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
// Ensure the update comes from the host.
let project = project::Entity::find_by_id(project_id)
let _project = project::Entity::find_by_id(project_id)
.filter(
Condition::all()
.add(project::Column::HostConnectionId.eq(connection.id as i32))
@@ -2140,7 +2212,6 @@ impl Database {
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
let room_id = project.room_id;
// Update metadata.
worktree::Entity::update(worktree::ActiveModel {
@@ -2220,7 +2291,7 @@ impl Database {
}
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
Ok((room_id, connection_ids))
Ok(connection_ids)
})
.await
}
@@ -2230,9 +2301,10 @@ impl Database {
update: &proto::UpdateDiagnosticSummary,
connection: ConnectionId,
) -> Result<RoomGuard<Vec<ConnectionId>>> {
self.room_transaction(|tx| async move {
let project_id = ProjectId::from_proto(update.project_id);
let worktree_id = update.worktree_id as i64;
let project_id = ProjectId::from_proto(update.project_id);
let worktree_id = update.worktree_id as i64;
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let summary = update
.summary
.as_ref()
@@ -2274,7 +2346,7 @@ impl Database {
.await?;
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
Ok((project.room_id, connection_ids))
Ok(connection_ids)
})
.await
}
@@ -2284,8 +2356,9 @@ impl Database {
update: &proto::StartLanguageServer,
connection: ConnectionId,
) -> Result<RoomGuard<Vec<ConnectionId>>> {
self.room_transaction(|tx| async move {
let project_id = ProjectId::from_proto(update.project_id);
let project_id = ProjectId::from_proto(update.project_id);
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let server = update
.server
.as_ref()
@@ -2319,7 +2392,7 @@ impl Database {
.await?;
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
Ok((project.room_id, connection_ids))
Ok(connection_ids)
})
.await
}
@@ -2329,7 +2402,8 @@ impl Database {
project_id: ProjectId,
connection: ConnectionId,
) -> Result<RoomGuard<(Project, ReplicaId)>> {
self.room_transaction(|tx| async move {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let participant = room_participant::Entity::find()
.filter(
Condition::all()
@@ -2455,7 +2529,6 @@ impl Database {
.all(&*tx)
.await?;
let room_id = project.room_id;
let project = Project {
collaborators: collaborators
.into_iter()
@@ -2475,7 +2548,7 @@ impl Database {
})
.collect(),
};
Ok((room_id, (project, replica_id as ReplicaId)))
Ok((project, replica_id as ReplicaId))
})
.await
}
@@ -2484,8 +2557,9 @@ impl Database {
&self,
project_id: ProjectId,
connection: ConnectionId,
) -> Result<RoomGuard<LeftProject>> {
self.room_transaction(|tx| async move {
) -> Result<RoomGuard<(proto::Room, LeftProject)>> {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let result = project_collaborator::Entity::delete_many()
.filter(
Condition::all()
@@ -2515,13 +2589,39 @@ impl Database {
.map(|collaborator| collaborator.connection())
.collect();
follower::Entity::delete_many()
.filter(
Condition::any()
.add(
Condition::all()
.add(follower::Column::ProjectId.eq(project_id))
.add(
follower::Column::LeaderConnectionServerId
.eq(connection.owner_id),
)
.add(follower::Column::LeaderConnectionId.eq(connection.id)),
)
.add(
Condition::all()
.add(follower::Column::ProjectId.eq(project_id))
.add(
follower::Column::FollowerConnectionServerId
.eq(connection.owner_id),
)
.add(follower::Column::FollowerConnectionId.eq(connection.id)),
),
)
.exec(&*tx)
.await?;
let room = self.get_room(project.room_id, &tx).await?;
let left_project = LeftProject {
id: project_id,
host_user_id: project.host_user_id,
host_connection_id: project.host_connection()?,
connection_ids,
};
Ok((project.room_id, left_project))
Ok((room, left_project))
})
.await
}
@@ -2531,11 +2631,8 @@ impl Database {
project_id: ProjectId,
connection_id: ConnectionId,
) -> Result<RoomGuard<Vec<ProjectCollaborator>>> {
self.room_transaction(|tx| async move {
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let collaborators = project_collaborator::Entity::find()
.filter(project_collaborator::Column::ProjectId.eq(project_id))
.all(&*tx)
@@ -2553,7 +2650,7 @@ impl Database {
.iter()
.any(|collaborator| collaborator.connection_id == connection_id)
{
Ok((project.room_id, collaborators))
Ok(collaborators)
} else {
Err(anyhow!("no such project"))?
}
@@ -2566,11 +2663,8 @@ impl Database {
project_id: ProjectId,
connection_id: ConnectionId,
) -> Result<RoomGuard<HashSet<ConnectionId>>> {
self.room_transaction(|tx| async move {
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let mut collaborators = project_collaborator::Entity::find()
.filter(project_collaborator::Column::ProjectId.eq(project_id))
.stream(&*tx)
@@ -2583,7 +2677,7 @@ impl Database {
}
if connection_ids.contains(&connection_id) {
Ok((project.room_id, connection_ids))
Ok(connection_ids)
} else {
Err(anyhow!("no such project"))?
}
@@ -2613,6 +2707,17 @@ impl Database {
Ok(guest_connection_ids)
}
async fn room_id_for_project(&self, project_id: ProjectId) -> Result<RoomId> {
self.transaction(|tx| async move {
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("project {} not found", project_id))?;
Ok(project.room_id)
})
.await
}
// access tokens
pub async fn create_access_token_hash(
@@ -2763,21 +2868,48 @@ impl Database {
self.run(body).await
}
async fn room_transaction<F, Fut, T>(&self, f: F) -> Result<RoomGuard<T>>
async fn room_transaction<F, Fut, T>(&self, room_id: RoomId, f: F) -> Result<RoomGuard<T>>
where
F: Send + Fn(TransactionHandle) -> Fut,
Fut: Send + Future<Output = Result<(RoomId, T)>>,
Fut: Send + Future<Output = Result<T>>,
{
let data = self
.optional_room_transaction(move |tx| {
let future = f(tx);
async {
let data = future.await?;
Ok(Some(data))
let body = async {
loop {
let lock = self.rooms.entry(room_id).or_default().clone();
let _guard = lock.lock_owned().await;
let (tx, result) = self.with_transaction(&f).await?;
match result {
Ok(data) => {
match tx.commit().await.map_err(Into::into) {
Ok(()) => {
return Ok(RoomGuard {
data,
_guard,
_not_send: PhantomData,
});
}
Err(error) => {
if is_serialization_error(&error) {
// Retry (don't break the loop)
} else {
return Err(error);
}
}
}
}
Err(error) => {
tx.rollback().await?;
if is_serialization_error(&error) {
// Retry (don't break the loop)
} else {
return Err(error);
}
}
}
})
.await?;
Ok(data.unwrap())
}
};
self.run(body).await
}
async fn with_transaction<F, Fut, T>(&self, f: &F) -> Result<(DatabaseTransaction, Result<T>)>
@@ -3011,6 +3143,7 @@ macro_rules! id_type {
id_type!(AccessTokenId);
id_type!(ContactId);
id_type!(FollowerId);
id_type!(RoomId);
id_type!(RoomParticipantId);
id_type!(ProjectId);

View File

@@ -0,0 +1,51 @@
use super::{FollowerId, ProjectId, RoomId, ServerId};
use rpc::ConnectionId;
use sea_orm::entity::prelude::*;
use serde::Serialize;
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)]
#[sea_orm(table_name = "followers")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: FollowerId,
pub room_id: RoomId,
pub project_id: ProjectId,
pub leader_connection_server_id: ServerId,
pub leader_connection_id: i32,
pub follower_connection_server_id: ServerId,
pub follower_connection_id: i32,
}
impl Model {
pub fn leader_connection(&self) -> ConnectionId {
ConnectionId {
owner_id: self.leader_connection_server_id.0 as u32,
id: self.leader_connection_id as u32,
}
}
pub fn follower_connection(&self) -> ConnectionId {
ConnectionId {
owner_id: self.follower_connection_server_id.0 as u32,
id: self.follower_connection_id as u32,
}
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::room::Entity",
from = "Column::RoomId",
to = "super::room::Column::Id"
)]
Room,
}
impl Related<super::room::Entity> for Entity {
fn to() -> RelationDef {
Relation::Room.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -15,6 +15,8 @@ pub enum Relation {
RoomParticipant,
#[sea_orm(has_many = "super::project::Entity")]
Project,
#[sea_orm(has_many = "super::follower::Entity")]
Follower,
}
impl Related<super::room_participant::Entity> for Entity {
@@ -29,4 +31,10 @@ impl Related<super::project::Entity> for Entity {
}
}
impl Related<super::follower::Entity> for Entity {
fn to() -> RelationDef {
Relation::Follower.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -91,6 +91,7 @@ impl std::error::Error for Error {}
pub struct Config {
pub http_port: u16,
pub database_url: String,
pub database_max_connections: u32,
pub api_token: String,
pub invite_link_prefix: String,
pub live_kit_server: Option<String>,
@@ -116,7 +117,7 @@ pub struct AppState {
impl AppState {
pub async fn new(config: Config) -> Result<Arc<Self>> {
let mut db_options = db::ConnectOptions::new(config.database_url.clone());
db_options.max_connections(5);
db_options.max_connections(config.database_max_connections);
let db = Database::new(db_options).await?;
let live_kit_client = if let Some(((server, key), secret)) = config
.live_kit_server

View File

@@ -1,11 +1,12 @@
use anyhow::anyhow;
use axum::{routing::get, Router};
use axum::{routing::get, Extension, Router};
use collab::{db, env, executor::Executor, AppState, Config, MigrateConfig, Result};
use db::Database;
use std::{
env::args,
net::{SocketAddr, TcpListener},
path::Path,
sync::Arc,
};
use tokio::signal::unix::SignalKind;
use tracing_log::LogTracer;
@@ -66,7 +67,12 @@ async fn main() -> Result<()> {
let app = collab::api::routes(rpc_server.clone(), state.clone())
.merge(collab::rpc::routes(rpc_server.clone()))
.merge(Router::new().route("/", get(handle_root)));
.merge(
Router::new()
.route("/", get(handle_root))
.route("/healthz", get(handle_liveness_probe))
.layer(Extension(state.clone())),
);
axum::Server::from_tcp(listener)?
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
@@ -95,6 +101,11 @@ async fn handle_root() -> String {
format!("collab v{VERSION}")
}
async fn handle_liveness_probe(Extension(state): Extension<Arc<AppState>>) -> Result<String> {
state.db.get_all_users(0, 1).await?;
Ok("ok".to_string())
}
pub fn init_tracing(config: &Config) -> Option<()> {
use std::str::FromStr;
use tracing_subscriber::layer::SubscriberExt;

View File

@@ -53,11 +53,11 @@ use std::{
},
time::Duration,
};
use tokio::sync::watch;
use tokio::sync::{watch, Semaphore};
use tower::ServiceBuilder;
use tracing::{info_span, instrument, Instrument};
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(5);
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
lazy_static! {
@@ -186,7 +186,7 @@ impl Server {
.add_request_handler(create_room)
.add_request_handler(join_room)
.add_request_handler(rejoin_room)
.add_message_handler(leave_room)
.add_request_handler(leave_room)
.add_request_handler(call)
.add_request_handler(cancel_call)
.add_message_handler(decline_call)
@@ -270,8 +270,11 @@ impl Server {
let mut live_kit_room = String::new();
let mut delete_live_kit_room = false;
if let Ok(mut refreshed_room) =
app_state.db.refresh_room(room_id, server_id).await
if let Some(mut refreshed_room) = app_state
.db
.refresh_room(room_id, server_id)
.await
.trace_err()
{
tracing::info!(
room_id = room_id.0,
@@ -539,8 +542,13 @@ impl Server {
// This arrangement ensures we will attempt to process earlier messages first, but fall
// back to processing messages arrived later in the spirit of making progress.
let mut foreground_message_handlers = FuturesUnordered::new();
let concurrent_handlers = Arc::new(Semaphore::new(256));
loop {
let next_message = incoming_rx.next().fuse();
let next_message = async {
let permit = concurrent_handlers.clone().acquire_owned().await.unwrap();
let message = incoming_rx.next().await;
(permit, message)
}.fuse();
futures::pin_mut!(next_message);
futures::select_biased! {
_ = teardown.changed().fuse() => return Ok(()),
@@ -551,7 +559,8 @@ impl Server {
break;
}
_ = foreground_message_handlers.next() => {}
message = next_message => {
next_message = next_message => {
let (permit, message) = next_message;
if let Some(message) = message {
let type_name = message.payload_type_name();
let span = tracing::info_span!("receive message", %user_id, %login, %connection_id, %address, type_name);
@@ -561,7 +570,10 @@ impl Server {
let handle_message = (handler)(message, session.clone());
drop(span_enter);
let handle_message = handle_message.instrument(span);
let handle_message = async move {
handle_message.await;
drop(permit);
}.instrument(span);
if is_background {
executor.spawn_detached(handle_message);
} else {
@@ -1090,8 +1102,14 @@ async fn rejoin_room(
Ok(())
}
async fn leave_room(_message: proto::LeaveRoom, session: Session) -> Result<()> {
leave_room_for_session(&session).await
async fn leave_room(
_: proto::LeaveRoom,
response: Response<proto::LeaveRoom>,
session: Session,
) -> Result<()> {
leave_room_for_session(&session).await?;
response.send(proto::Ack {})?;
Ok(())
}
async fn call(
@@ -1312,6 +1330,7 @@ async fn join_project(
.filter(|collaborator| collaborator.connection_id != session.connection_id)
.map(|collaborator| collaborator.to_proto())
.collect::<Vec<_>>();
let worktrees = project
.worktrees
.iter()
@@ -1404,7 +1423,7 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
let sender_id = session.connection_id;
let project_id = ProjectId::from_proto(request.project_id);
let project = session
let (room, project) = &*session
.db()
.await
.leave_project(project_id, sender_id)
@@ -1415,7 +1434,9 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
host_connection_id = %project.host_connection_id,
"leave project"
);
project_left(&project, &session);
room_updated(&room, &session.peer);
Ok(())
}
@@ -1724,6 +1745,7 @@ async fn follow(
.ok_or_else(|| anyhow!("invalid leader id"))?
.into();
let follower_id = session.connection_id;
{
let project_connection_ids = session
.db()
@@ -1744,6 +1766,14 @@ async fn follow(
.views
.retain(|view| view.leader_id != Some(follower_id.into()));
response.send(response_payload)?;
let room = session
.db()
.await
.follow(project_id, leader_id, follower_id)
.await?;
room_updated(&room, &session.peer);
Ok(())
}
@@ -1753,17 +1783,29 @@ async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
.leader_id
.ok_or_else(|| anyhow!("invalid leader id"))?
.into();
let project_connection_ids = session
let follower_id = session.connection_id;
if !session
.db()
.await
.project_connection_ids(project_id, session.connection_id)
.await?;
if !project_connection_ids.contains(&leader_id) {
.await?
.contains(&leader_id)
{
Err(anyhow!("no such peer"))?;
}
session
.peer
.forward_send(session.connection_id, leader_id, request)?;
let room = session
.db()
.await
.unfollow(project_id, leader_id, follower_id)
.await?;
room_updated(&room, &session.peer);
Ok(())
}

View File

@@ -197,7 +197,8 @@ impl TestServer {
fs: fs.clone(),
build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _| unimplemented!(),
dock_default_item_factory: |_, _| unimplemented!(),
dock_default_item_factory: |_, _| None,
background_actions: || &[],
});
Project::init(&client);
@@ -434,15 +435,7 @@ impl TestClient {
cx: &mut TestAppContext,
) -> ViewHandle<Workspace> {
let (_, root_view) = cx.add_window(|_| EmptyView);
cx.add_view(&root_view, |cx| {
Workspace::new(
Default::default(),
0,
project.clone(),
|_, _| unimplemented!(),
cx,
)
})
cx.add_view(&root_view, |cx| Workspace::test_new(project.clone(), cx))
}
fn create_new_root_dir(&mut self) -> PathBuf {

View File

@@ -166,9 +166,67 @@ async fn test_basic_calls(
}
);
// Call user C again from user A.
active_call_a
.update(cx_a, |call, cx| {
call.invite(client_c.user_id().unwrap(), None, cx)
})
.await
.unwrap();
deterministic.run_until_parked();
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
remote: vec!["user_b".to_string()],
pending: vec!["user_c".to_string()]
}
);
assert_eq!(
room_participants(&room_b, cx_b),
RoomParticipants {
remote: vec!["user_a".to_string()],
pending: vec!["user_c".to_string()]
}
);
// User C accepts the call.
let call_c = incoming_call_c.next().await.unwrap().unwrap();
assert_eq!(call_c.calling_user.github_login, "user_a");
active_call_c
.update(cx_c, |call, cx| call.accept_incoming(cx))
.await
.unwrap();
assert!(incoming_call_c.next().await.unwrap().is_none());
let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone());
deterministic.run_until_parked();
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
remote: vec!["user_b".to_string(), "user_c".to_string()],
pending: Default::default()
}
);
assert_eq!(
room_participants(&room_b, cx_b),
RoomParticipants {
remote: vec!["user_a".to_string(), "user_c".to_string()],
pending: Default::default()
}
);
assert_eq!(
room_participants(&room_c, cx_c),
RoomParticipants {
remote: vec!["user_a".to_string(), "user_b".to_string()],
pending: Default::default()
}
);
// User A shares their screen
let display = MacOSDisplay::new();
let events_b = active_call_events(cx_b);
let events_c = active_call_events(cx_c);
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
@@ -181,9 +239,10 @@ async fn test_basic_calls(
deterministic.run_until_parked();
// User B observes the remote screen sharing track.
assert_eq!(events_b.borrow().len(), 1);
let event = events_b.borrow().first().unwrap().clone();
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event {
let event_b = events_b.borrow().first().unwrap().clone();
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_b {
assert_eq!(participant_id, client_a.peer_id().unwrap());
room_b.read_with(cx_b, |room, _| {
assert_eq!(
@@ -197,11 +256,32 @@ async fn test_basic_calls(
panic!("unexpected event")
}
// User C observes the remote screen sharing track.
assert_eq!(events_c.borrow().len(), 1);
let event_c = events_c.borrow().first().unwrap().clone();
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_c {
assert_eq!(participant_id, client_a.peer_id().unwrap());
room_c.read_with(cx_c, |room, _| {
assert_eq!(
room.remote_participants()[&client_a.user_id().unwrap()]
.tracks
.len(),
1
);
});
} else {
panic!("unexpected event")
}
// User A leaves the room.
active_call_a.update(cx_a, |call, cx| {
call.hang_up(cx).unwrap();
assert!(call.room().is_none());
});
active_call_a
.update(cx_a, |call, cx| {
let hang_up = call.hang_up(cx);
assert!(call.room().is_none());
hang_up
})
.await
.unwrap();
deterministic.run_until_parked();
assert_eq!(
room_participants(&room_a, cx_a),
@@ -213,18 +293,28 @@ async fn test_basic_calls(
assert_eq!(
room_participants(&room_b, cx_b),
RoomParticipants {
remote: Default::default(),
remote: vec!["user_c".to_string()],
pending: Default::default()
}
);
assert_eq!(
room_participants(&room_c, cx_c),
RoomParticipants {
remote: vec!["user_b".to_string()],
pending: Default::default()
}
);
// User B gets disconnected from the LiveKit server, which causes them
// to automatically leave the room.
// to automatically leave the room. User C leaves the room as well because
// nobody else is in there.
server
.test_live_kit_server
.disconnect_client(client_b.peer_id().unwrap().to_string())
.disconnect_client(client_b.user_id().unwrap().to_string())
.await;
active_call_b.update(cx_b, |call, _| assert!(call.room().is_none()));
deterministic.run_until_parked();
active_call_b.read_with(cx_b, |call, _| assert!(call.room().is_none()));
active_call_c.read_with(cx_c, |call, _| assert!(call.room().is_none()));
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
@@ -239,6 +329,141 @@ async fn test_basic_calls(
pending: Default::default()
}
);
assert_eq!(
room_participants(&room_c, cx_c),
RoomParticipants {
remote: Default::default(),
pending: Default::default()
}
);
}
#[gpui::test(iterations = 10)]
async fn test_calling_multiple_users_simultaneously(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
cx_d: &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 client_c = server.create_client(cx_c, "user_c").await;
let client_d = server.create_client(cx_d, "user_d").await;
server
.make_contacts(&mut [
(&client_a, cx_a),
(&client_b, cx_b),
(&client_c, cx_c),
(&client_d, cx_d),
])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
let active_call_c = cx_c.read(ActiveCall::global);
let active_call_d = cx_d.read(ActiveCall::global);
// Simultaneously call user B and user C from client A.
let b_invite = active_call_a.update(cx_a, |call, cx| {
call.invite(client_b.user_id().unwrap(), None, cx)
});
let c_invite = active_call_a.update(cx_a, |call, cx| {
call.invite(client_c.user_id().unwrap(), None, cx)
});
b_invite.await.unwrap();
c_invite.await.unwrap();
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
deterministic.run_until_parked();
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
remote: Default::default(),
pending: vec!["user_b".to_string(), "user_c".to_string()]
}
);
// Call client D from client A.
active_call_a
.update(cx_a, |call, cx| {
call.invite(client_d.user_id().unwrap(), None, cx)
})
.await
.unwrap();
deterministic.run_until_parked();
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
remote: Default::default(),
pending: vec![
"user_b".to_string(),
"user_c".to_string(),
"user_d".to_string()
]
}
);
// Accept the call on all clients simultaneously.
let accept_b = active_call_b.update(cx_b, |call, cx| call.accept_incoming(cx));
let accept_c = active_call_c.update(cx_c, |call, cx| call.accept_incoming(cx));
let accept_d = active_call_d.update(cx_d, |call, cx| call.accept_incoming(cx));
accept_b.await.unwrap();
accept_c.await.unwrap();
accept_d.await.unwrap();
deterministic.run_until_parked();
let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone());
let room_d = active_call_d.read_with(cx_d, |call, _| call.room().unwrap().clone());
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
remote: vec![
"user_b".to_string(),
"user_c".to_string(),
"user_d".to_string(),
],
pending: Default::default()
}
);
assert_eq!(
room_participants(&room_b, cx_b),
RoomParticipants {
remote: vec![
"user_a".to_string(),
"user_c".to_string(),
"user_d".to_string(),
],
pending: Default::default()
}
);
assert_eq!(
room_participants(&room_c, cx_c),
RoomParticipants {
remote: vec![
"user_a".to_string(),
"user_b".to_string(),
"user_d".to_string(),
],
pending: Default::default()
}
);
assert_eq!(
room_participants(&room_d, cx_d),
RoomParticipants {
remote: vec![
"user_a".to_string(),
"user_b".to_string(),
"user_c".to_string(),
],
pending: Default::default()
}
);
}
#[gpui::test(iterations = 10)]
@@ -336,6 +561,7 @@ async fn test_room_uniqueness(
// Client C can successfully call client B after client B leaves the room.
active_call_b
.update(cx_b, |call, cx| call.hang_up(cx))
.await
.unwrap();
deterministic.run_until_parked();
active_call_c
@@ -512,6 +738,14 @@ async fn test_server_restarts(
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
client_a
.fs
.insert_tree("/a", json!({ "a.txt": "a-contents" }))
.await;
// Invite client B to collaborate on a project
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
let client_d = server.create_client(cx_d, "user_d").await;
@@ -532,19 +766,19 @@ async fn test_server_restarts(
// User A calls users B, C, and D.
active_call_a
.update(cx_a, |call, cx| {
call.invite(client_b.user_id().unwrap(), None, cx)
call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
})
.await
.unwrap();
active_call_a
.update(cx_a, |call, cx| {
call.invite(client_c.user_id().unwrap(), None, cx)
call.invite(client_c.user_id().unwrap(), Some(project_a.clone()), cx)
})
.await
.unwrap();
active_call_a
.update(cx_a, |call, cx| {
call.invite(client_d.user_id().unwrap(), None, cx)
call.invite(client_d.user_id().unwrap(), Some(project_a.clone()), cx)
})
.await
.unwrap();
@@ -600,7 +834,7 @@ async fn test_server_restarts(
// Users A and B reconnect to the call. User C has troubles reconnecting, so it leaves the room.
client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
deterministic.advance_clock(RECEIVE_TIMEOUT);
deterministic.advance_clock(RECONNECT_TIMEOUT);
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
@@ -707,6 +941,7 @@ async fn test_server_restarts(
// User D hangs up.
active_call_d
.update(cx_d, |call, cx| call.hang_up(cx))
.await
.unwrap();
deterministic.run_until_parked();
assert_eq!(
@@ -772,7 +1007,7 @@ async fn test_server_restarts(
client_a.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
client_b.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
deterministic.advance_clock(RECEIVE_TIMEOUT);
deterministic.advance_clock(RECONNECT_TIMEOUT);
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
@@ -862,7 +1097,7 @@ async fn test_calls_on_multiple_connections(
assert!(incoming_call_b2.next().await.unwrap().is_none());
// User B disconnects the client that is not on the call. Everything should be fine.
client_b1.disconnect(&cx_b1.to_async()).unwrap();
client_b1.disconnect(&cx_b1.to_async());
deterministic.advance_clock(RECEIVE_TIMEOUT);
client_b1
.authenticate_and_connect(false, &cx_b1.to_async())
@@ -870,7 +1105,10 @@ async fn test_calls_on_multiple_connections(
.unwrap();
// User B hangs up, and user A calls them again.
active_call_b2.update(cx_b2, |call, cx| call.hang_up(cx).unwrap());
active_call_b2
.update(cx_b2, |call, cx| call.hang_up(cx))
.await
.unwrap();
deterministic.run_until_parked();
active_call_a
.update(cx_a, |call, cx| {
@@ -905,7 +1143,10 @@ async fn test_calls_on_multiple_connections(
assert!(incoming_call_b2.next().await.unwrap().is_some());
// User A hangs up, causing both connections to stop ringing.
active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap());
active_call_a
.update(cx_a, |call, cx| call.hang_up(cx))
.await
.unwrap();
deterministic.run_until_parked();
assert!(incoming_call_b1.next().await.unwrap().is_none());
assert!(incoming_call_b2.next().await.unwrap().is_none());
@@ -1142,7 +1383,10 @@ async fn test_unshare_project(
.unwrap();
// When client B leaves the room, the project becomes read-only.
active_call_b.update(cx_b, |call, cx| call.hang_up(cx).unwrap());
active_call_b
.update(cx_b, |call, cx| call.hang_up(cx))
.await
.unwrap();
deterministic.run_until_parked();
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
@@ -1171,7 +1415,10 @@ async fn test_unshare_project(
.unwrap();
// When client A (the host) leaves the room, the project gets unshared and guests are notified.
active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap());
active_call_a
.update(cx_a, |call, cx| call.hang_up(cx))
.await
.unwrap();
deterministic.run_until_parked();
project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
project_c2.read_with(cx_c, |project, _| {
@@ -1220,15 +1467,7 @@ async fn test_host_disconnect(
deterministic.run_until_parked();
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
let (_, workspace_b) = cx_b.add_window(|cx| {
Workspace::new(
Default::default(),
0,
project_b.clone(),
|_, _| unimplemented!(),
cx,
)
});
let (_, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "b.txt"), None, true, cx)
@@ -2023,7 +2262,9 @@ async fn test_propagate_saves_and_fs_changes(
});
// Edit the buffer as the host and concurrently save as guest B.
let save_b = buffer_b.update(cx_b, |buf, cx| buf.save(cx));
let save_b = project_b.update(cx_b, |project, cx| {
project.save_buffer(buffer_b.clone(), cx)
});
buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx));
save_b.await.unwrap();
assert_eq!(
@@ -2092,6 +2333,41 @@ async fn test_propagate_saves_and_fs_changes(
assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
});
let new_buffer_a = project_a
.update(cx_a, |p, cx| p.create_buffer("", None, cx))
.unwrap();
let new_buffer_id = new_buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id());
let new_buffer_b = project_b
.update(cx_b, |p, cx| p.open_buffer_by_id(new_buffer_id, cx))
.await
.unwrap();
new_buffer_b.read_with(cx_b, |buffer, _| {
assert!(buffer.file().is_none());
});
new_buffer_a.update(cx_a, |buffer, cx| {
buffer.edit([(0..0, "ok")], None, cx);
});
project_a
.update(cx_a, |project, cx| {
project.save_buffer_as(new_buffer_a.clone(), "/a/file3.rs".into(), cx)
})
.await
.unwrap();
deterministic.run_until_parked();
new_buffer_b.read_with(cx_b, |buffer_b, _| {
assert_eq!(
buffer_b.file().unwrap().path().as_ref(),
Path::new("file3.rs")
);
new_buffer_a.read_with(cx_a, |buffer_a, _| {
assert_eq!(buffer_b.saved_mtime(), buffer_a.saved_mtime());
assert_eq!(buffer_b.saved_version(), buffer_a.saved_version());
});
});
}
#[gpui::test(iterations = 10)]
@@ -2572,7 +2848,7 @@ async fn test_fs_operations(
.await
.unwrap();
deterministic.run_until_parked();
worktree_a.read_with(cx_a, |worktree, _| {
assert_eq!(
worktree
@@ -2661,7 +2937,12 @@ async fn test_buffer_conflict_after_save(
assert!(!buf.has_conflict());
});
buffer_b.update(cx_b, |buf, cx| buf.save(cx)).await.unwrap();
project_b
.update(cx_b, |project, cx| {
project.save_buffer(buffer_b.clone(), cx)
})
.await
.unwrap();
cx_a.foreground().forbid_parking();
buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
buffer_b.read_with(cx_b, |buf, _| {
@@ -2964,7 +3245,7 @@ async fn test_leaving_project(
buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
// Drop client B's connection and ensure client A and client C observe client B leaving.
client_b.disconnect(&cx_b.to_async()).unwrap();
client_b.disconnect(&cx_b.to_async());
deterministic.advance_clock(RECONNECT_TIMEOUT);
project_a.read_with(cx_a, |project, _| {
assert_eq!(project.collaborators().len(), 1);
@@ -3621,9 +3902,11 @@ async fn test_formatting_buffer(
})
.await
.unwrap();
// The edits from the LSP are applied, and a final newline is added.
assert_eq!(
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
"let honey = \"two\""
"let honey = \"two\"\n"
);
// Ensure buffer can be formatted using an external command. Notice how the
@@ -4433,15 +4716,7 @@ async fn test_collaborating_with_code_actions(
// Join the project as client B.
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let (_window_b, workspace_b) = cx_b.add_window(|cx| {
Workspace::new(
Default::default(),
0,
project_b.clone(),
|_, _| unimplemented!(),
cx,
)
});
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
@@ -4664,15 +4939,7 @@ async fn test_collaborating_with_renames(
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let (_window_b, workspace_b) = cx_b.add_window(|cx| {
Workspace::new(
Default::default(),
0,
project_b.clone(),
|_, _| unimplemented!(),
cx,
)
});
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "one.rs"), None, true, cx)
@@ -5206,7 +5473,10 @@ async fn test_contacts(
[("user_b".to_string(), "online", "busy")]
);
active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap());
active_call_a
.update(cx_a, |call, cx| call.hang_up(cx))
.await
.unwrap();
deterministic.run_until_parked();
assert_eq!(
contacts(&client_a, cx_a),
@@ -5509,7 +5779,7 @@ async fn test_contact_requests(
.is_empty());
async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
client.disconnect(&cx.to_async()).unwrap();
client.disconnect(&cx.to_async());
client.clear_contacts(cx).await;
client
.authenticate_and_connect(false, &cx.to_async())
@@ -5519,10 +5789,12 @@ async fn test_contact_requests(
}
#[gpui::test(iterations = 10)]
async fn test_following(
async fn test_basic_following(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
cx_d: &mut TestAppContext,
) {
deterministic.forbid_parking();
cx_a.update(editor::init);
@@ -5531,8 +5803,15 @@ async fn test_following(
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 client_c = server.create_client(cx_c, "user_c").await;
let client_d = server.create_client(cx_d, "user_d").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.create_room(&mut [
(&client_a, cx_a),
(&client_b, cx_b),
(&client_c, cx_c),
(&client_d, cx_d),
])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
@@ -5564,8 +5843,10 @@ async fn test_following(
.await
.unwrap();
// Client A opens some editors.
let workspace_a = client_a.build_workspace(&project_a, cx_a);
let workspace_b = client_b.build_workspace(&project_b, cx_b);
// Client A opens some editors.
let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
let editor_a1 = workspace_a
.update(cx_a, |workspace, cx| {
@@ -5585,7 +5866,6 @@ async fn test_following(
.unwrap();
// Client B opens an editor.
let workspace_b = client_b.build_workspace(&project_b, cx_b);
let editor_b1 = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
@@ -5595,29 +5875,184 @@ async fn test_following(
.downcast::<Editor>()
.unwrap();
let client_a_id = project_b.read_with(cx_b, |project, _| {
project.collaborators().values().next().unwrap().peer_id
});
let client_b_id = project_a.read_with(cx_a, |project, _| {
project.collaborators().values().next().unwrap().peer_id
});
let peer_id_a = client_a.peer_id().unwrap();
let peer_id_b = client_b.peer_id().unwrap();
let peer_id_c = client_c.peer_id().unwrap();
let peer_id_d = client_d.peer_id().unwrap();
// When client B starts following client A, all visible view states are replicated to client B.
// Client A updates their selections in those editors
editor_a1.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
});
editor_a2.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
});
// When client B starts following client A, all visible view states are replicated to client B.
workspace_b
.update(cx_b, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(client_a_id), cx)
.toggle_follow(&ToggleFollow(peer_id_a), cx)
.unwrap()
})
.await
.unwrap();
cx_c.foreground().run_until_parked();
let active_call_c = cx_c.read(ActiveCall::global);
let project_c = client_c.build_remote_project(project_id, cx_c).await;
let workspace_c = client_c.build_workspace(&project_c, cx_c);
active_call_c
.update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
.await
.unwrap();
drop(project_c);
// Client C also follows client A.
workspace_c
.update(cx_c, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(peer_id_a), cx)
.unwrap()
})
.await
.unwrap();
cx_d.foreground().run_until_parked();
let active_call_d = cx_d.read(ActiveCall::global);
let project_d = client_d.build_remote_project(project_id, cx_d).await;
let workspace_d = client_d.build_workspace(&project_d, cx_d);
active_call_d
.update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
.await
.unwrap();
drop(project_d);
// 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}"
);
});
}
// Client C unfollows client A.
workspace_c.update(cx_c, |workspace, cx| {
workspace.toggle_follow(&ToggleFollow(peer_id_a), cx);
});
// 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}"
);
});
}
// Client C re-follows client A.
workspace_c.update(cx_c, |workspace, cx| {
workspace.toggle_follow(&ToggleFollow(peer_id_a), cx);
});
// 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}"
);
});
}
// Client D follows client C.
workspace_d
.update(cx_d, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(peer_id_c), cx)
.unwrap()
})
.await
.unwrap();
// 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}"
);
});
}
// Client C closes the project.
cx_c.drop_last(workspace_c);
// 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}"
);
});
}
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
workspace
.active_item(cx)
@@ -5770,14 +6205,14 @@ async fn test_following(
workspace_a
.update(cx_a, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(client_b_id), cx)
.toggle_follow(&ToggleFollow(peer_id_b), cx)
.unwrap()
})
.await
.unwrap();
assert_eq!(
workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
Some(client_b_id)
Some(peer_id_b)
);
assert_eq!(
workspace_a.read_with(cx_a, |workspace, cx| workspace
@@ -5849,7 +6284,7 @@ async fn test_following(
);
// Following interrupts when client B disconnects.
client_b.disconnect(&cx_b.to_async()).unwrap();
client_b.disconnect(&cx_b.to_async());
deterministic.advance_clock(RECONNECT_TIMEOUT);
assert_eq!(
workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),

View File

@@ -641,7 +641,7 @@ async fn randomly_mutate_active_call(
if can_hang_up && active_call.read_with(cx, |call, _| call.room().is_some()) =>
{
log::info!("{}: hanging up", client.username);
active_call.update(cx, |call, cx| call.hang_up(cx))?;
active_call.update(cx, |call, cx| call.hang_up(cx)).await?;
}
_ => {}
}
@@ -1064,15 +1064,16 @@ async fn randomly_query_and_mutate_buffers(
}
}
30..=39 if buffer.read_with(cx, |buffer, _| buffer.is_dirty()) => {
let (requested_version, save) = buffer.update(cx, |buffer, cx| {
let requested_version = buffer.update(cx, |buffer, cx| {
log::info!(
"{}: saving buffer {} ({:?})",
client.username,
buffer.remote_id(),
buffer.file().unwrap().full_path(cx)
);
(buffer.version(), buffer.save(cx))
buffer.version()
});
let save = project.update(cx, |project, cx| project.save_buffer(buffer, cx));
let save = cx.background().spawn(async move {
let (saved_version, _, _) = save
.await

View File

@@ -22,11 +22,14 @@ test-support = [
]
[dependencies]
auto_update = { path = "../auto_update" }
call = { path = "../call" }
client = { path = "../client" }
clock = { path = "../clock" }
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
editor = { path = "../editor" }
feedback = { path = "../feedback" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
menu = { path = "../menu" }

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,24 @@
mod collab_titlebar_item;
mod collaborator_list_popover;
mod contact_finder;
mod contact_list;
mod contact_notification;
mod contacts_popover;
mod face_pile;
mod incoming_call_notification;
mod notifications;
mod project_shared_notification;
mod sharing_status_indicator;
use anyhow::anyhow;
use call::ActiveCall;
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleCollaborationMenu};
use gpui::MutableAppContext;
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
use gpui::{actions, MutableAppContext, Task};
use std::sync::Arc;
use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
actions!(collab, [ToggleScreenSharing]);
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
collab_titlebar_item::init(cx);
contact_notification::init(cx);
@@ -22,89 +27,108 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
contacts_popover::init(cx);
incoming_call_notification::init(cx);
project_shared_notification::init(cx);
sharing_status_indicator::init(cx);
cx.add_global_action(toggle_screen_sharing);
cx.add_global_action(move |action: &JoinProject, cx| {
let project_id = action.project_id;
let follow_user_id = action.follow_user_id;
let app_state = app_state.clone();
cx.spawn(|mut cx| async move {
let existing_workspace = cx.update(|cx| {
cx.window_ids()
.filter_map(|window_id| cx.root_view::<Workspace>(window_id))
.find(|workspace| {
workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
})
});
let workspace = if let Some(existing_workspace) = existing_workspace {
existing_workspace
} else {
let active_call = cx.read(ActiveCall::global);
let room = active_call
.read_with(&cx, |call, _| call.room().cloned())
.ok_or_else(|| anyhow!("not in a call"))?;
let project = room
.update(&mut cx, |room, cx| {
room.join_project(
project_id,
app_state.languages.clone(),
app_state.fs.clone(),
cx,
)
})
.await?;
let (_, workspace) = cx.add_window(
(app_state.build_window_options)(None, None, cx.platform().as_ref()),
|cx| {
let mut workspace = Workspace::new(
Default::default(),
0,
project,
app_state.dock_default_item_factory,
cx,
);
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
workspace
},
);
workspace
};
cx.activate_window(workspace.window_id());
cx.platform().activate(true);
workspace.update(&mut cx, |workspace, cx| {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
let follow_peer_id = room
.read(cx)
.remote_participants()
.iter()
.find(|(_, participant)| participant.user.id == follow_user_id)
.map(|(_, p)| p.peer_id)
.or_else(|| {
// If we couldn't follow the given user, follow the host instead.
let collaborator = workspace
.project()
.read(cx)
.collaborators()
.values()
.find(|collaborator| collaborator.replica_id == 0)?;
Some(collaborator.peer_id)
});
if let Some(follow_peer_id) = follow_peer_id {
if !workspace.is_following(follow_peer_id) {
workspace
.toggle_follow(&ToggleFollow(follow_peer_id), cx)
.map(|follow| follow.detach_and_log_err(cx));
}
}
}
});
anyhow::Ok(())
})
.detach_and_log_err(cx);
join_project(action, app_state.clone(), cx);
});
}
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut MutableAppContext) {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
let toggle_screen_sharing = room.update(cx, |room, cx| {
if room.is_screen_sharing() {
Task::ready(room.unshare_screen(cx))
} else {
room.share_screen(cx)
}
});
toggle_screen_sharing.detach_and_log_err(cx);
}
}
fn join_project(action: &JoinProject, app_state: Arc<AppState>, cx: &mut MutableAppContext) {
let project_id = action.project_id;
let follow_user_id = action.follow_user_id;
cx.spawn(|mut cx| async move {
let existing_workspace = cx.update(|cx| {
cx.window_ids()
.filter_map(|window_id| cx.root_view::<Workspace>(window_id))
.find(|workspace| {
workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
})
});
let workspace = if let Some(existing_workspace) = existing_workspace {
existing_workspace
} else {
let active_call = cx.read(ActiveCall::global);
let room = active_call
.read_with(&cx, |call, _| call.room().cloned())
.ok_or_else(|| anyhow!("not in a call"))?;
let project = room
.update(&mut cx, |room, cx| {
room.join_project(
project_id,
app_state.languages.clone(),
app_state.fs.clone(),
cx,
)
})
.await?;
let (_, workspace) = cx.add_window(
(app_state.build_window_options)(None, None, cx.platform().as_ref()),
|cx| {
let mut workspace = Workspace::new(
Default::default(),
0,
project,
app_state.dock_default_item_factory,
app_state.background_actions,
cx,
);
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
workspace
},
);
workspace
};
cx.activate_window(workspace.window_id());
cx.platform().activate(true);
workspace.update(&mut cx, |workspace, cx| {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
let follow_peer_id = room
.read(cx)
.remote_participants()
.iter()
.find(|(_, participant)| participant.user.id == follow_user_id)
.map(|(_, p)| p.peer_id)
.or_else(|| {
// If we couldn't follow the given user, follow the host instead.
let collaborator = workspace
.project()
.read(cx)
.collaborators()
.values()
.find(|collaborator| collaborator.replica_id == 0)?;
Some(collaborator.peer_id)
});
if let Some(follow_peer_id) = follow_peer_id {
if !workspace.is_being_followed(follow_peer_id) {
workspace
.toggle_follow(&ToggleFollow(follow_peer_id), cx)
.map(|follow| follow.detach_and_log_err(cx));
}
}
}
});
anyhow::Ok(())
})
.detach_and_log_err(cx);
}

View File

@@ -0,0 +1,165 @@
use call::ActiveCall;
use client::UserStore;
use gpui::Action;
use gpui::{
actions, elements::*, Entity, ModelHandle, MouseButton, RenderContext, View, ViewContext,
};
use settings::Settings;
use crate::collab_titlebar_item::ToggleCollaboratorList;
pub(crate) enum Event {
Dismissed,
}
enum Collaborator {
SelfUser { username: String },
RemoteUser { username: String },
}
actions!(collaborator_list_popover, [NoOp]);
pub(crate) struct CollaboratorListPopover {
list_state: ListState,
}
impl Entity for CollaboratorListPopover {
type Event = Event;
}
impl View for CollaboratorListPopover {
fn ui_name() -> &'static str {
"CollaboratorListPopover"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone();
MouseEventHandler::<Self>::new(0, cx, |_, _| {
List::new(self.list_state.clone())
.contained()
.with_style(theme.contacts_popover.container) //TODO: Change the name of this theme key
.constrained()
.with_width(theme.contacts_popover.width)
.with_height(theme.contacts_popover.height)
.boxed()
})
.on_down_out(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleCollaboratorList);
})
.boxed()
}
fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed);
}
}
impl CollaboratorListPopover {
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
let active_call = ActiveCall::global(cx);
let mut collaborators = user_store
.read(cx)
.current_user()
.map(|u| Collaborator::SelfUser {
username: u.github_login.clone(),
})
.into_iter()
.collect::<Vec<_>>();
//TODO: What should the canonical sort here look like, consult contacts list implementation
if let Some(room) = active_call.read(cx).room() {
for participant in room.read(cx).remote_participants() {
collaborators.push(Collaborator::RemoteUser {
username: participant.1.user.github_login.clone(),
});
}
}
Self {
list_state: ListState::new(
collaborators.len(),
Orientation::Top,
0.,
cx,
move |_, index, cx| match &collaborators[index] {
Collaborator::SelfUser { username } => render_collaborator_list_entry(
index,
username,
None::<NoOp>,
None,
Svg::new("icons/chevron_right_12.svg"),
NoOp,
"Leave call".to_owned(),
cx,
),
Collaborator::RemoteUser { username } => render_collaborator_list_entry(
index,
username,
Some(NoOp),
Some(format!("Follow {username}")),
Svg::new("icons/x_mark_12.svg"),
NoOp,
format!("Remove {username} from call"),
cx,
),
},
),
}
}
}
fn render_collaborator_list_entry<UA: Action + Clone, IA: Action + Clone>(
index: usize,
username: &str,
username_action: Option<UA>,
username_tooltip: Option<String>,
icon: Svg,
icon_action: IA,
icon_tooltip: String,
cx: &mut RenderContext<CollaboratorListPopover>,
) -> ElementBox {
enum Username {}
enum UsernameTooltip {}
enum Icon {}
enum IconTooltip {}
let theme = &cx.global::<Settings>().theme;
let username_theme = theme.contact_list.contact_username.text.clone();
let tooltip_theme = theme.tooltip.clone();
let username = MouseEventHandler::<Username>::new(index, cx, |_, _| {
Label::new(username.to_owned(), username_theme.clone()).boxed()
})
.on_click(MouseButton::Left, move |_, cx| {
if let Some(username_action) = username_action.clone() {
cx.dispatch_action(username_action);
}
});
Flex::row()
.with_child(if let Some(username_tooltip) = username_tooltip {
username
.with_tooltip::<UsernameTooltip, _>(
index,
username_tooltip,
None,
tooltip_theme.clone(),
cx,
)
.boxed()
} else {
username.boxed()
})
.with_child(
MouseEventHandler::<Icon>::new(index, cx, |_, _| icon.boxed())
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(icon_action.clone())
})
.with_tooltip::<IconTooltip, _>(index, icon_tooltip, None, tooltip_theme, cx)
.boxed(),
)
.boxed()
}

View File

@@ -1,7 +1,7 @@
use client::{ContactRequestStatus, User, UserStore};
use gpui::{
elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext, RenderContext,
Task, View, ViewContext, ViewHandle,
elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState, MutableAppContext,
RenderContext, Task, View, ViewContext, ViewHandle,
};
use picker::{Picker, PickerDelegate};
use settings::Settings;
@@ -68,7 +68,7 @@ impl PickerDelegate for ContactFinder {
this.potential_contacts = potential_contacts.into();
cx.notify();
});
Ok(())
anyhow::Ok(())
}
.log_err()
.await;
@@ -128,7 +128,7 @@ impl PickerDelegate for ContactFinder {
.style_for(mouse_state, selected);
Flex::row()
.with_children(user.avatar.clone().map(|avatar| {
Image::new(avatar)
Image::from_data(avatar)
.with_style(theme.contact_finder.contact_avatar)
.aligned()
.left()
@@ -178,4 +178,14 @@ impl ContactFinder {
selected_index: 0,
}
}
pub fn editor_text(&self, cx: &AppContext) -> String {
self.picker.read(cx).query(cx)
}
pub fn with_editor_text(self, editor_text: String, cx: &mut ViewContext<Self>) -> Self {
self.picker
.update(cx, |picker, cx| picker.set_query(editor_text, cx));
self
}
}

View File

@@ -1,3 +1,4 @@
use super::collab_titlebar_item::LeaveCall;
use crate::contacts_popover;
use call::ActiveCall;
use client::{proto::PeerId, Contact, User, UserStore};
@@ -18,22 +19,20 @@ use serde::Deserialize;
use settings::Settings;
use std::{mem, sync::Arc};
use theme::IconButton;
use util::ResultExt;
use workspace::{JoinProject, OpenSharedScreen};
impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]);
impl_internal_actions!(contact_list, [ToggleExpanded, Call, LeaveCall]);
impl_internal_actions!(contact_list, [ToggleExpanded, Call]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContactList::remove_contact);
cx.add_action(ContactList::respond_to_contact_request);
cx.add_action(ContactList::clear_filter);
cx.add_action(ContactList::cancel);
cx.add_action(ContactList::select_next);
cx.add_action(ContactList::select_prev);
cx.add_action(ContactList::confirm);
cx.add_action(ContactList::toggle_expanded);
cx.add_action(ContactList::call);
cx.add_action(ContactList::leave_call);
}
#[derive(Clone, PartialEq)]
@@ -45,9 +44,6 @@ struct Call {
initial_project: Option<ModelHandle<Project>>,
}
#[derive(Copy, Clone, PartialEq)]
struct LeaveCall;
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
enum Section {
ActiveCall,
@@ -145,7 +141,10 @@ impl PartialEq for ContactEntry {
pub struct RequestContact(pub u64);
#[derive(Clone, Deserialize, PartialEq)]
pub struct RemoveContact(pub u64);
pub struct RemoveContact {
user_id: u64,
github_login: String,
}
#[derive(Clone, Deserialize, PartialEq)]
pub struct RespondToContactRequest {
@@ -298,11 +297,25 @@ impl ContactList {
this
}
pub fn editor_text(&self, cx: &AppContext) -> String {
self.filter_editor.read(cx).text(cx)
}
pub fn with_editor_text(self, editor_text: String, cx: &mut ViewContext<Self>) -> Self {
self.filter_editor
.update(cx, |picker, cx| picker.set_text(editor_text, cx));
self
}
fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext<Self>) {
let user_id = request.0;
let user_id = request.user_id;
let github_login = &request.github_login;
let user_store = self.user_store.clone();
let prompt_message = "Are you sure you want to remove this contact?";
let mut answer = cx.prompt(PromptLevel::Warning, prompt_message, &["Remove", "Cancel"]);
let prompt_message = format!(
"Are you sure you want to remove \"{}\" from your contacts?",
github_login
);
let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
cx.spawn(|_, mut cx| async move {
if answer.next().await == Some(0) {
user_store
@@ -326,7 +339,7 @@ impl ContactList {
.detach();
}
fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
let did_clear = self.filter_editor.update(cx, |editor, cx| {
if editor.buffer().read(cx).len(cx) > 0 {
editor.set_text("", cx);
@@ -335,6 +348,7 @@ impl ContactList {
false
}
});
if !did_clear {
cx.emit(Event::Dismissed);
}
@@ -729,7 +743,7 @@ impl ContactList {
) -> ElementBox {
Flex::row()
.with_children(user.avatar.clone().map(|avatar| {
Image::new(avatar)
Image::from_data(avatar)
.with_style(theme.contact_avatar)
.aligned()
.left()
@@ -749,7 +763,7 @@ impl ContactList {
)
.with_children(if is_pending {
Some(
Label::new("Calling".to_string(), theme.calling_indicator.text.clone())
Label::new("Calling", theme.calling_indicator.text.clone())
.contained()
.with_style(theme.calling_indicator.container)
.aligned()
@@ -950,7 +964,7 @@ impl ContactList {
.boxed(),
)
.with_child(
Label::new("Screen".into(), row.name.text.clone())
Label::new("Screen", row.name.text.clone())
.aligned()
.left()
.contained()
@@ -980,6 +994,7 @@ impl ContactList {
cx: &mut RenderContext<Self>,
) -> ElementBox {
enum Header {}
enum LeaveCallContactList {}
let header_style = theme
.header_row
@@ -992,9 +1007,9 @@ impl ContactList {
};
let leave_call = if section == Section::ActiveCall {
Some(
MouseEventHandler::<LeaveCall>::new(0, cx, |state, _| {
MouseEventHandler::<LeaveCallContactList>::new(0, cx, |state, _| {
let style = theme.leave_call.style_for(state, false);
Label::new("Leave Session".into(), style.text.clone())
Label::new("Leave Call", style.text.clone())
.contained()
.with_style(style.container)
.boxed()
@@ -1026,7 +1041,7 @@ impl ContactList {
.boxed(),
)
.with_child(
Label::new(text.to_string(), header_style.text.clone())
Label::new(text, header_style.text.clone())
.aligned()
.left()
.contained()
@@ -1059,6 +1074,7 @@ impl ContactList {
let online = contact.online;
let busy = contact.busy || calling;
let user_id = contact.user.id;
let github_login = contact.user.github_login.clone();
let initial_project = project.clone();
let mut element =
MouseEventHandler::<Contact>::new(contact.user.id as usize, cx, |_, cx| {
@@ -1082,7 +1098,7 @@ impl ContactList {
};
Stack::new()
.with_child(
Image::new(avatar)
Image::from_data(avatar)
.with_style(theme.contact_avatar)
.aligned()
.left()
@@ -1119,14 +1135,17 @@ impl ContactList {
.with_padding(Padding::uniform(2.))
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(RemoveContact(user_id))
cx.dispatch_action(RemoveContact {
user_id,
github_login: github_login.clone(),
})
})
.flex_float()
.boxed(),
)
.with_children(if calling {
Some(
Label::new("Calling".to_string(), theme.calling_indicator.text.clone())
Label::new("Calling", theme.calling_indicator.text.clone())
.contained()
.with_style(theme.calling_indicator.container)
.aligned()
@@ -1175,7 +1194,7 @@ impl ContactList {
let mut row = Flex::row()
.with_children(user.avatar.clone().map(|avatar| {
Image::new(avatar)
Image::from_data(avatar)
.with_style(theme.contact_avatar)
.aligned()
.left()
@@ -1195,6 +1214,7 @@ impl ContactList {
);
let user_id = user.id;
let github_login = user.github_login.clone();
let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
let button_spacing = theme.contact_button_spacing;
@@ -1256,7 +1276,10 @@ impl ContactList {
.with_padding(Padding::uniform(2.))
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(RemoveContact(user_id))
cx.dispatch_action(RemoveContact {
user_id,
github_login: github_login.clone(),
})
})
.flex_float()
.boxed(),
@@ -1283,12 +1306,6 @@ impl ContactList {
})
.detach_and_log_err(cx);
}
fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx))
.log_err();
}
}
impl Entity for ContactList {
@@ -1302,7 +1319,7 @@ impl View for ContactList {
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.set.insert("menu".into());
cx.add_identifier("menu");
cx
}
@@ -1334,7 +1351,7 @@ impl View for ContactList {
})
.with_tooltip::<AddContact, _>(
0,
"Add contact".into(),
"Search for new contact".into(),
None,
theme.tooltip.clone(),
cx,

View File

@@ -1,4 +1,4 @@
use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleCollaborationMenu};
use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleContactsMenu};
use client::UserStore;
use gpui::{
actions, elements::*, ClipboardItem, CursorStyle, Entity, ModelHandle, MouseButton,
@@ -43,19 +43,23 @@ impl ContactsPopover {
user_store,
_subscription: None,
};
this.show_contact_list(cx);
this.show_contact_list(String::new(), cx);
this
}
fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext<Self>) {
match &self.child {
Child::ContactList(_) => self.show_contact_finder(cx),
Child::ContactFinder(_) => self.show_contact_list(cx),
Child::ContactList(list) => self.show_contact_finder(list.read(cx).editor_text(cx), cx),
Child::ContactFinder(finder) => {
self.show_contact_list(finder.read(cx).editor_text(cx), cx)
}
}
}
fn show_contact_finder(&mut self, cx: &mut ViewContext<ContactsPopover>) {
let child = cx.add_view(|cx| ContactFinder::new(self.user_store.clone(), cx));
fn show_contact_finder(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
let child = cx.add_view(|cx| {
ContactFinder::new(self.user_store.clone(), cx).with_editor_text(editor_text, cx)
});
cx.focus(&child);
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
crate::contact_finder::Event::Dismissed => cx.emit(Event::Dismissed),
@@ -64,9 +68,11 @@ impl ContactsPopover {
cx.notify();
}
fn show_contact_list(&mut self, cx: &mut ViewContext<ContactsPopover>) {
let child =
cx.add_view(|cx| ContactList::new(self.project.clone(), self.user_store.clone(), cx));
fn show_contact_list(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
let child = cx.add_view(|cx| {
ContactList::new(self.project.clone(), self.user_store.clone(), cx)
.with_editor_text(editor_text, cx)
});
cx.focus(&child);
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
@@ -155,7 +161,7 @@ impl View for ContactsPopover {
.boxed()
})
.on_down_out(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleCollaborationMenu);
cx.dispatch_action(ToggleContactsMenu);
})
.boxed()
}

View File

@@ -0,0 +1,101 @@
use std::ops::Range;
use gpui::{
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json::ToJson,
serde_json::{self, json},
Axis, DebugContext, Element, ElementBox, MeasurementContext, PaintContext,
};
pub(crate) struct FacePile {
overlap: f32,
faces: Vec<ElementBox>,
}
impl FacePile {
pub fn new(overlap: f32) -> FacePile {
FacePile {
overlap,
faces: Vec::new(),
}
}
}
impl Element for FacePile {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: gpui::SizeConstraint,
cx: &mut gpui::LayoutContext,
) -> (Vector2F, Self::LayoutState) {
debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
let mut width = 0.;
for face in &mut self.faces {
width += face.layout(constraint, cx).x();
}
width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
(Vector2F::new(width, constraint.max.y()), ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_layout: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
let origin_y = bounds.upper_right().y();
let mut origin_x = bounds.upper_right().x();
for face in self.faces.iter_mut().rev() {
let size = face.size();
origin_x -= size.x();
cx.paint_layer(None, |cx| {
face.paint(vec2f(origin_x, origin_y), visible_bounds, cx);
});
origin_x += self.overlap;
}
()
}
fn rect_for_text_range(
&self,
_: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &MeasurementContext,
) -> Option<RectF> {
None
}
fn debug(
&self,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &DebugContext,
) -> serde_json::Value {
json!({
"type": "FacePile",
"bounds": bounds.to_json()
})
}
}
impl Extend<ElementBox> for FacePile {
fn extend<T: IntoIterator<Item = ElementBox>>(&mut self, children: T) {
self.faces.extend(children);
}
}

View File

@@ -108,7 +108,7 @@ impl IncomingCallNotification {
.unwrap_or(&default_project);
Flex::row()
.with_children(self.call.calling_user.avatar.clone().map(|avatar| {
Image::new(avatar)
Image::from_data(avatar)
.with_style(theme.caller_avatar)
.aligned()
.boxed()
@@ -172,7 +172,7 @@ impl IncomingCallNotification {
.with_child(
MouseEventHandler::<Accept>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
Label::new("Accept".to_string(), theme.accept_button.text.clone())
Label::new("Accept", theme.accept_button.text.clone())
.aligned()
.contained()
.with_style(theme.accept_button.container)
@@ -188,7 +188,7 @@ impl IncomingCallNotification {
.with_child(
MouseEventHandler::<Decline>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
Label::new("Decline".to_string(), theme.decline_button.text.clone())
Label::new("Decline", theme.decline_button.text.clone())
.aligned()
.contained()
.with_style(theme.decline_button.container)

View File

@@ -11,8 +11,8 @@ enum Button {}
pub fn render_user_notification<V: View, A: Action + Clone>(
user: Arc<User>,
title: &str,
body: Option<&str>,
title: &'static str,
body: Option<&'static str>,
dismiss_action: A,
buttons: Vec<(&'static str, Box<dyn Action>)>,
cx: &mut RenderContext<V>,
@@ -24,7 +24,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
.with_child(
Flex::row()
.with_children(user.avatar.clone().map(|avatar| {
Image::new(avatar)
Image::from_data(avatar)
.with_style(theme.header_avatar)
.aligned()
.constrained()
@@ -83,7 +83,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
.named("contact notification header"),
)
.with_children(body.map(|body| {
Label::new(body.to_string(), theme.body_message.text.clone())
Label::new(body, theme.body_message.text.clone())
.contained()
.with_style(theme.body_message.container)
.boxed()
@@ -97,7 +97,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|(ix, (message, action))| {
MouseEventHandler::<Button>::new(ix, cx, |state, _| {
let button = theme.button.style_for(state, false);
Label::new(message.to_string(), button.text.clone())
Label::new(message, button.text.clone())
.contained()
.with_style(button.container)
.boxed()

View File

@@ -108,7 +108,7 @@ impl ProjectSharedNotification {
let theme = &cx.global::<Settings>().theme.project_shared_notification;
Flex::row()
.with_children(self.owner.avatar.clone().map(|avatar| {
Image::new(avatar)
Image::from_data(avatar)
.with_style(theme.owner_avatar)
.aligned()
.boxed()
@@ -175,7 +175,7 @@ impl ProjectSharedNotification {
.with_child(
MouseEventHandler::<Open>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.project_shared_notification;
Label::new("Open".to_string(), theme.open_button.text.clone())
Label::new("Open", theme.open_button.text.clone())
.aligned()
.contained()
.with_style(theme.open_button.container)
@@ -194,7 +194,7 @@ impl ProjectSharedNotification {
.with_child(
MouseEventHandler::<Dismiss>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.project_shared_notification;
Label::new("Dismiss".to_string(), theme.dismiss_button.text.clone())
Label::new("Dismiss", theme.dismiss_button.text.clone())
.aligned()
.contained()
.with_style(theme.dismiss_button.container)

View File

@@ -0,0 +1,59 @@
use call::ActiveCall;
use gpui::{
color::Color,
elements::{MouseEventHandler, Svg},
Appearance, Element, ElementBox, Entity, MouseButton, MutableAppContext, RenderContext, View,
};
use settings::Settings;
use crate::ToggleScreenSharing;
pub fn init(cx: &mut MutableAppContext) {
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() && cx.global::<Settings>().show_call_status_icon {
status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator));
}
} else if let Some((window_id, _)) = status_indicator.take() {
cx.remove_status_bar_item(window_id);
}
}
})
.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 RenderContext<'_, Self>) -> ElementBox {
let color = match cx.appearance {
Appearance::Light | Appearance::VibrantLight => Color::black(),
Appearance::Dark | Appearance::VibrantDark => Color::white(),
};
MouseEventHandler::<Self>::new(0, cx, |_, _| {
Svg::new("icons/disable_screen_sharing_12.svg")
.with_color(color)
.constrained()
.with_width(18.)
.aligned()
.boxed()
})
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(ToggleScreenSharing);
})
.boxed()
}
}

View File

@@ -65,7 +65,7 @@ impl CommandPalette {
action,
keystrokes: bindings
.iter()
.filter_map(|binding| binding.keystrokes())
.map(|binding| binding.keystrokes())
.last()
.map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
})
@@ -257,7 +257,7 @@ impl PickerDelegate for CommandPalette {
.filter_map(|(modifier, label)| {
if modifier {
Some(
Label::new(label.into(), key_style.label.clone())
Label::new(label, key_style.label.clone())
.contained()
.with_style(key_style.container)
.boxed(),
@@ -352,9 +352,7 @@ mod tests {
});
let project = Project::test(app_state.fs.clone(), [], cx).await;
let (_, workspace) = cx.add_window(|cx| {
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
});
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let editor = cx.add_view(&workspace, |cx| {
let mut editor = Editor::single_line(None, cx);
editor.set_text("abc", cx);

View File

@@ -5,7 +5,9 @@ use gpui::{
};
use menu::*;
use settings::Settings;
use std::{any::TypeId, time::Duration};
use std::{any::TypeId, borrow::Cow, time::Duration};
pub type StaticItem = Box<dyn Fn(&mut MutableAppContext) -> ElementBox>;
#[derive(Copy, Clone, PartialEq)]
struct Clicked;
@@ -24,16 +26,17 @@ pub fn init(cx: &mut MutableAppContext) {
pub enum ContextMenuItem {
Item {
label: String,
label: Cow<'static, str>,
action: Box<dyn Action>,
},
Static(StaticItem),
Separator,
}
impl ContextMenuItem {
pub fn item(label: impl ToString, action: impl 'static + Action) -> Self {
pub fn item(label: impl Into<Cow<'static, str>>, action: impl 'static + Action) -> Self {
Self::Item {
label: label.to_string(),
label: label.into(),
action: Box::new(action),
}
}
@@ -42,14 +45,14 @@ impl ContextMenuItem {
Self::Separator
}
fn is_separator(&self) -> bool {
matches!(self, Self::Separator)
fn is_action(&self) -> bool {
matches!(self, Self::Item { .. })
}
fn action_id(&self) -> Option<TypeId> {
match self {
ContextMenuItem::Item { action, .. } => Some(action.id()),
ContextMenuItem::Separator => None,
ContextMenuItem::Static(..) | ContextMenuItem::Separator => None,
}
}
}
@@ -58,11 +61,13 @@ pub struct ContextMenu {
show_count: usize,
anchor_position: Vector2F,
anchor_corner: AnchorCorner,
position_mode: OverlayPositionMode,
items: Vec<ContextMenuItem>,
selected_index: Option<usize>,
visible: bool,
previously_focused_view_id: Option<usize>,
clicked: bool,
parent_view_id: usize,
_actions_observation: Subscription,
}
@@ -77,7 +82,7 @@ impl View for ContextMenu {
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.set.insert("menu".into());
cx.add_identifier("menu");
cx
}
@@ -104,6 +109,7 @@ impl View for ContextMenu {
.with_fit_mode(OverlayFitMode::SnapToWindow)
.with_anchor_position(self.anchor_position)
.with_anchor_corner(self.anchor_corner)
.with_position_mode(self.position_mode)
.boxed()
}
@@ -114,15 +120,19 @@ impl View for ContextMenu {
impl ContextMenu {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
let parent_view_id = cx.parent().unwrap();
Self {
show_count: 0,
anchor_position: Default::default(),
anchor_corner: AnchorCorner::TopLeft,
position_mode: OverlayPositionMode::Window,
items: Default::default(),
selected_index: Default::default(),
visible: Default::default(),
previously_focused_view_id: Default::default(),
clicked: false,
parent_view_id,
_actions_observation: cx.observe_actions(Self::action_dispatched),
}
}
@@ -184,13 +194,13 @@ impl ContextMenu {
}
fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
self.selected_index = self.items.iter().position(|item| !item.is_separator());
self.selected_index = self.items.iter().position(|item| item.is_action());
cx.notify();
}
fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
for (ix, item) in self.items.iter().enumerate().rev() {
if !item.is_separator() {
if item.is_action() {
self.selected_index = Some(ix);
cx.notify();
break;
@@ -201,7 +211,7 @@ impl ContextMenu {
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.selected_index {
for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
if !item.is_separator() {
if item.is_action() {
self.selected_index = Some(ix);
cx.notify();
break;
@@ -215,7 +225,7 @@ impl ContextMenu {
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.selected_index {
for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
if !item.is_separator() {
if item.is_action() {
self.selected_index = Some(ix);
cx.notify();
break;
@@ -230,7 +240,7 @@ impl ContextMenu {
&mut self,
anchor_position: Vector2F,
anchor_corner: AnchorCorner,
items: impl IntoIterator<Item = ContextMenuItem>,
items: Vec<ContextMenuItem>,
cx: &mut ViewContext<Self>,
) {
let mut items = items.into_iter().peekable();
@@ -250,7 +260,12 @@ impl ContextMenu {
cx.notify();
}
pub fn set_position_mode(&mut self, mode: OverlayPositionMode) {
self.position_mode = mode;
}
fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element {
let window_id = cx.window_id();
let style = cx.global::<Settings>().theme.context_menu.clone();
Flex::row()
.with_child(
@@ -268,6 +283,9 @@ impl ContextMenu {
.with_style(style.container)
.boxed()
}
ContextMenuItem::Static(f) => f(cx),
ContextMenuItem::Separator => Empty::new()
.collapsed()
.contained()
@@ -289,12 +307,17 @@ impl ContextMenu {
Some(ix) == self.selected_index,
);
KeystrokeLabel::new(
window_id,
self.parent_view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),
)
.boxed()
}
ContextMenuItem::Static(_) => Empty::new().boxed(),
ContextMenuItem::Separator => Empty::new()
.collapsed()
.constrained()
@@ -318,6 +341,7 @@ impl ContextMenu {
let style = cx.global::<Settings>().theme.context_menu.clone();
let window_id = cx.window_id();
MouseEventHandler::<Menu>::new(0, cx, |_, cx| {
Flex::column()
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
@@ -331,12 +355,14 @@ impl ContextMenu {
Flex::row()
.with_child(
Label::new(label.to_string(), style.label.clone())
Label::new(label.clone(), style.label.clone())
.contained()
.boxed(),
)
.with_child({
KeystrokeLabel::new(
window_id,
self.parent_view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),
@@ -356,6 +382,9 @@ impl ContextMenu {
.on_drag(MouseButton::Left, |_, _| {})
.boxed()
}
ContextMenuItem::Static(f) => f(cx),
ContextMenuItem::Separator => Empty::new()
.constrained()
.with_height(1.)

View File

@@ -4,6 +4,7 @@ pub mod query;
// Re-export
pub use anyhow;
use anyhow::Context;
use gpui::MutableAppContext;
pub use indoc::indoc;
pub use lazy_static;
use parking_lot::{Mutex, RwLock};
@@ -17,6 +18,7 @@ use sqlez::domain::Migrator;
use sqlez::thread_safe_connection::ThreadSafeConnection;
use sqlez_macros::sql;
use std::fs::create_dir_all;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
@@ -39,6 +41,7 @@ const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
const DB_FILE_NAME: &'static str = "db.sqlite";
lazy_static::lazy_static! {
// !!!!!!! CHANGE BACK TO DEFAULT FALSE BEFORE SHIPPING
static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
@@ -63,11 +66,11 @@ pub async fn open_db<M: Migrator + 'static>(
let connection = async_iife!({
// Note: This still has a race condition where 1 set of migrations succeeds
// (e.g. (Workspace, Editor)) and another fails (e.g. (Workspace, Terminal))
// This will cause the first connection to have the database taken out
// This will cause the first connection to have the database taken out
// from under it. This *should* be fine though. The second dabatase failure will
// cause errors in the log and so should be observed by developers while writing
// soon-to-be good migrations. If user databases are corrupted, we toss them out
// and try again from a blank. As long as running all migrations from start to end
// and try again from a blank. As long as running all migrations from start to end
// on a blank database is ok, this race condition will never be triggered.
//
// Basically: Don't ever push invalid migrations to stable or everyone will have
@@ -85,7 +88,7 @@ pub async fn open_db<M: Migrator + 'static>(
};
}
// Take a lock in the failure case so that we move the db once per process instead
// Take a lock in the failure case so that we move the db once per process instead
// of potentially multiple times from different threads. This shouldn't happen in the
// normal path
let _lock = DB_FILE_OPERATIONS.lock();
@@ -236,6 +239,15 @@ macro_rules! define_connection {
};
}
pub fn write_and_log<F>(cx: &mut MutableAppContext, db_write: impl FnOnce() -> F + Send + 'static)
where
F: Future<Output = anyhow::Result<()>> + Send,
{
cx.background()
.spawn(async move { db_write().await.log_err() })
.detach()
}
#[cfg(test)]
mod tests {
use std::{fs, thread};

View File

@@ -21,6 +21,7 @@ use language::{
use project::{DiagnosticSummary, Project, ProjectPath};
use serde_json::json;
use settings::Settings;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
cmp::Ordering,
@@ -89,14 +90,11 @@ impl View for ProjectDiagnosticsEditor {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
if self.path_states.is_empty() {
let theme = &cx.global::<Settings>().theme.project_diagnostics;
Label::new(
"No problems in workspace".to_string(),
theme.empty_message.clone(),
)
.aligned()
.contained()
.with_style(theme.container)
.boxed()
Label::new("No problems in workspace", theme.empty_message.clone())
.aligned()
.contained()
.with_style(theme.container)
.boxed()
} else {
ChildView::new(&self.editor, cx).boxed()
}
@@ -579,7 +577,7 @@ impl Item for ProjectDiagnosticsEditor {
.update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
}
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
Editor::to_item_events(event)
}
@@ -696,7 +694,7 @@ pub(crate) fn render_summary(
theme: &theme::ProjectDiagnostics,
) -> ElementBox {
if summary.error_count == 0 && summary.warning_count == 0 {
Label::new("No problems".to_string(), text_style.clone()).boxed()
Label::new("No problems", text_style.clone()).boxed()
} else {
let icon_width = theme.tab_icon_width;
let icon_spacing = theme.tab_icon_spacing;
@@ -807,15 +805,7 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| {
Workspace::new(
Default::default(),
0,
project.clone(),
|_, _| unimplemented!(),
cx,
)
});
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
// Create some diagnostics
project.update(cx, |project, cx| {

View File

@@ -178,14 +178,11 @@ impl View for DiagnosticIndicator {
if in_progress {
element.add_child(
Label::new(
"Checking…".into(),
style.diagnostic_message.default.text.clone(),
)
.aligned()
.contained()
.with_margin_left(item_spacing)
.boxed(),
Label::new("Checking…", style.diagnostic_message.default.text.clone())
.aligned()
.contained()
.with_margin_left(item_spacing)
.boxed(),
);
} else if let Some(diagnostic) = &self.current_diagnostic {
let message_style = style.diagnostic_message.clone();

View File

@@ -17,7 +17,8 @@ test-support = [
"project/test-support",
"util/test-support",
"workspace/test-support",
"tree-sitter-rust"
"tree-sitter-rust",
"tree-sitter-typescript"
]
[dependencies]
@@ -58,6 +59,7 @@ smol = "1.2"
tree-sitter-rust = { version = "*", optional = true }
tree-sitter-html = { version = "*", optional = true }
tree-sitter-javascript = { version = "*", optional = true }
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259", optional = true }
[dev-dependencies]
text = { path = "../text", features = ["test-support"] }
@@ -75,4 +77,5 @@ unindent = "0.1.7"
tree-sitter = "0.20"
tree-sitter-rust = "0.20"
tree-sitter-html = "0.19"
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }
tree-sitter-javascript = "0.20"

View File

@@ -8,6 +8,7 @@ use block_map::{BlockMap, BlockPoint};
use collections::{HashMap, HashSet};
use fold_map::FoldMap;
use gpui::{
color::Color,
fonts::{FontId, HighlightStyle},
Entity, ModelContext, ModelHandle,
};
@@ -23,6 +24,12 @@ pub use block_map::{
BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum FoldStatus {
Folded,
Foldable,
}
pub trait ToDisplayPoint {
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
}
@@ -212,6 +219,10 @@ impl DisplayMap {
.update(cx, |map, cx| map.set_font(font_id, font_size, cx))
}
pub fn set_fold_ellipses_color(&mut self, color: Color) -> bool {
self.fold_map.set_ellipses_color(color)
}
pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
self.wrap_map
.update(cx, |map, cx| map.set_wrap_width(width, cx))
@@ -337,7 +348,7 @@ impl DisplaySnapshot {
.map(|h| h.text)
}
// Returns text chunks starting at the end of the given display row in reverse until the start of the file
/// Returns text chunks starting at the end of the given display row in reverse until the start of the file
pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
(0..=display_row).into_iter().rev().flat_map(|row| {
self.blocks_snapshot
@@ -411,6 +422,67 @@ impl DisplaySnapshot {
})
}
/// Returns an iterator of the start positions of the occurances of `target` in the `self` after `from`
/// Stops if `condition` returns false for any of the character position pairs observed.
pub fn find_while<'a>(
&'a self,
from: DisplayPoint,
target: &str,
condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
) -> impl Iterator<Item = DisplayPoint> + 'a {
Self::find_internal(self.chars_at(from), target.chars().collect(), condition)
}
/// Returns an iterator of the end positions of the occurances of `target` in the `self` before `from`
/// Stops if `condition` returns false for any of the character position pairs observed.
pub fn reverse_find_while<'a>(
&'a self,
from: DisplayPoint,
target: &str,
condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
) -> impl Iterator<Item = DisplayPoint> + 'a {
Self::find_internal(
self.reverse_chars_at(from),
target.chars().rev().collect(),
condition,
)
}
fn find_internal<'a>(
iterator: impl Iterator<Item = (char, DisplayPoint)> + 'a,
target: Vec<char>,
mut condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
) -> impl Iterator<Item = DisplayPoint> + 'a {
// List of partial matches with the index of the last seen character in target and the starting point of the match
let mut partial_matches: Vec<(usize, DisplayPoint)> = Vec::new();
iterator
.take_while(move |(ch, point)| condition(*ch, *point))
.filter_map(move |(ch, point)| {
if Some(&ch) == target.get(0) {
partial_matches.push((0, point));
}
let mut found = None;
// Keep partial matches that have the correct next character
partial_matches.retain_mut(|(match_position, match_start)| {
if target.get(*match_position) == Some(&ch) {
*match_position += 1;
if *match_position == target.len() {
found = Some(match_start.clone());
// This match is completed. No need to keep tracking it
false
} else {
true
}
} else {
false
}
});
found
})
}
pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
let mut count = 0;
let mut column = 0;
@@ -530,6 +602,59 @@ impl DisplaySnapshot {
self.blocks_snapshot.longest_row()
}
pub fn fold_for_line(self: &Self, display_row: u32) -> Option<FoldStatus> {
if self.is_foldable(display_row) {
Some(FoldStatus::Foldable)
} else if self.is_line_folded(display_row) {
Some(FoldStatus::Folded)
} else {
None
}
}
pub fn is_foldable(self: &Self, row: u32) -> bool {
let max_point = self.max_point();
if row >= max_point.row() {
return false;
}
let (start_indent, is_blank) = self.line_indent(row);
if is_blank {
return false;
}
for display_row in next_rows(row, self) {
let (indent, is_blank) = self.line_indent(display_row);
if !is_blank {
return indent > start_indent;
}
}
return false;
}
pub fn foldable_range(self: &Self, row: u32) -> Option<Range<DisplayPoint>> {
let start = DisplayPoint::new(row, self.line_len(row));
if self.is_foldable(row) && !self.is_line_folded(start.row()) {
let (start_indent, _) = self.line_indent(row);
let max_point = self.max_point();
let mut end = None;
for row in next_rows(row, self) {
let (indent, is_blank) = self.line_indent(row);
if !is_blank && indent <= start_indent {
end = Some(DisplayPoint::new(row - 1, self.line_len(row - 1)));
break;
}
}
let end = end.unwrap_or(max_point);
Some(start..end)
} else {
None
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn highlight_ranges<Tag: ?Sized + 'static>(
&self,
@@ -617,6 +742,24 @@ impl ToDisplayPoint for Anchor {
}
}
pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterator<Item = u32> {
let max_row = display_map.max_point().row();
let start_row = display_row + 1;
let mut current = None;
std::iter::from_fn(move || {
if current == None {
current = Some(start_row);
} else {
current = Some(current.unwrap() + 1)
}
if current.unwrap() > max_row {
None
} else {
current
}
})
}
#[cfg(test)]
pub mod tests {
use super::*;
@@ -627,7 +770,7 @@ pub mod tests {
use smol::stream::StreamExt;
use std::{env, sync::Arc};
use theme::SyntaxTheme;
use util::test::{marked_text_ranges, sample_text};
use util::test::{marked_text_offsets, marked_text_ranges, sample_text};
use Bias::*;
#[gpui::test(iterations = 100)]
@@ -1106,7 +1249,7 @@ pub mod tests {
vec![
("fn ".to_string(), None),
("out".to_string(), Some(Color::blue())),
("".to_string(), None),
("".to_string(), None),
(" fn ".to_string(), Some(Color::red())),
("inner".to_string(), Some(Color::blue())),
("() {}\n}".to_string(), Some(Color::red())),
@@ -1187,7 +1330,7 @@ pub mod tests {
cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)),
[
("out".to_string(), Some(Color::blue())),
("\n".to_string(), None),
("\n".to_string(), None),
(" \nfn ".to_string(), Some(Color::red())),
("i\n".to_string(), Some(Color::blue()))
]
@@ -1418,6 +1561,32 @@ pub mod tests {
)
}
#[test]
fn test_find_internal() {
assert("This is a ˇtest of find internal", "test");
assert("Some text ˇaˇaˇaa with repeated characters", "aa");
fn assert(marked_text: &str, target: &str) {
let (text, expected_offsets) = marked_text_offsets(marked_text);
let chars = text
.chars()
.enumerate()
.map(|(index, ch)| (ch, DisplayPoint::new(0, index as u32)));
let target = target.chars();
assert_eq!(
expected_offsets
.into_iter()
.map(|offset| offset as u32)
.collect::<Vec<_>>(),
DisplaySnapshot::find_internal(chars, target.collect(), |_, _| true)
.map(|point| point.column())
.collect::<Vec<_>>()
)
}
}
fn syntax_chunks<'a>(
rows: Range<u32>,
map: &ModelHandle<DisplayMap>,

View File

@@ -4,7 +4,7 @@ use crate::{
ToOffset,
};
use collections::BTreeMap;
use gpui::fonts::HighlightStyle;
use gpui::{color::Color, fonts::HighlightStyle};
use language::{Chunk, Edit, Point, TextSummary};
use parking_lot::Mutex;
use std::{
@@ -133,6 +133,7 @@ impl<'a> FoldMapWriter<'a> {
folds: self.0.folds.clone(),
buffer_snapshot: buffer,
version: self.0.version.load(SeqCst),
ellipses_color: self.0.ellipses_color,
};
(snapshot, edits)
}
@@ -182,6 +183,7 @@ impl<'a> FoldMapWriter<'a> {
folds: self.0.folds.clone(),
buffer_snapshot: buffer,
version: self.0.version.load(SeqCst),
ellipses_color: self.0.ellipses_color,
};
(snapshot, edits)
}
@@ -192,6 +194,7 @@ pub struct FoldMap {
transforms: Mutex<SumTree<Transform>>,
folds: SumTree<Fold>,
version: AtomicUsize,
ellipses_color: Option<Color>,
}
impl FoldMap {
@@ -209,6 +212,7 @@ impl FoldMap {
},
&(),
)),
ellipses_color: None,
version: Default::default(),
};
@@ -217,6 +221,7 @@ impl FoldMap {
folds: this.folds.clone(),
buffer_snapshot: this.buffer.lock().clone(),
version: this.version.load(SeqCst),
ellipses_color: None,
};
(this, snapshot)
}
@@ -233,6 +238,7 @@ impl FoldMap {
folds: self.folds.clone(),
buffer_snapshot: self.buffer.lock().clone(),
version: self.version.load(SeqCst),
ellipses_color: self.ellipses_color,
};
(snapshot, edits)
}
@@ -246,6 +252,15 @@ impl FoldMap {
(FoldMapWriter(self), snapshot, edits)
}
pub fn set_ellipses_color(&mut self, color: Color) -> bool {
if self.ellipses_color != Some(color) {
self.ellipses_color = Some(color);
true
} else {
false
}
}
fn check_invariants(&self) {
if cfg!(test) {
assert_eq!(
@@ -370,7 +385,7 @@ impl FoldMap {
}
if fold.end > fold.start {
let output_text = "";
let output_text = "";
new_transforms.push(
Transform {
summary: TransformSummary {
@@ -477,6 +492,7 @@ pub struct FoldSnapshot {
folds: SumTree<Fold>,
buffer_snapshot: MultiBufferSnapshot,
pub version: usize,
pub ellipses_color: Option<Color>,
}
impl FoldSnapshot {
@@ -739,6 +755,7 @@ impl FoldSnapshot {
max_output_offset: range.end.0,
highlight_endpoints: highlight_endpoints.into_iter().peekable(),
active_highlights: Default::default(),
ellipses_color: self.ellipses_color,
}
}
@@ -1029,6 +1046,7 @@ pub struct FoldChunks<'a> {
max_output_offset: usize,
highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
ellipses_color: Option<Color>,
}
impl<'a> Iterator for FoldChunks<'a> {
@@ -1058,7 +1076,10 @@ impl<'a> Iterator for FoldChunks<'a> {
return Some(Chunk {
text: output_text,
syntax_highlight_id: None,
highlight_style: None,
highlight_style: self.ellipses_color.map(|color| HighlightStyle {
color: Some(color),
..Default::default()
}),
diagnostic_severity: None,
is_unnecessary: false,
});
@@ -1214,7 +1235,7 @@ mod tests {
Point::new(0, 2)..Point::new(2, 2),
Point::new(2, 4)..Point::new(4, 1),
]);
assert_eq!(snapshot2.text(), "aacceeeee");
assert_eq!(snapshot2.text(), "aacceeeee");
assert_eq!(
edits,
&[
@@ -1241,7 +1262,7 @@ mod tests {
buffer.snapshot(cx)
});
let (snapshot3, edits) = map.read(buffer_snapshot, subscription.consume().into_inner());
assert_eq!(snapshot3.text(), "123ac123ceeeee");
assert_eq!(snapshot3.text(), "123ac123ceeeee");
assert_eq!(
edits,
&[
@@ -1261,12 +1282,12 @@ mod tests {
buffer.snapshot(cx)
});
let (snapshot4, _) = map.read(buffer_snapshot.clone(), subscription.consume().into_inner());
assert_eq!(snapshot4.text(), "123ac123456eee");
assert_eq!(snapshot4.text(), "123ac123456eee");
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), false);
let (snapshot5, _) = map.read(buffer_snapshot.clone(), vec![]);
assert_eq!(snapshot5.text(), "123ac123456eee");
assert_eq!(snapshot5.text(), "123ac123456eee");
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), true);
@@ -1287,19 +1308,19 @@ mod tests {
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
writer.fold(vec![5..8]);
let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "abcdeijkl");
assert_eq!(snapshot.text(), "abcdeijkl");
// Create an fold adjacent to the start of the first fold.
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
writer.fold(vec![0..1, 2..5]);
let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "…b…ijkl");
assert_eq!(snapshot.text(), "⋯b⋯ijkl");
// Create an fold adjacent to the end of the first fold.
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
writer.fold(vec![11..11, 8..10]);
let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "…b…kl");
assert_eq!(snapshot.text(), "⋯b⋯kl");
}
{
@@ -1309,7 +1330,7 @@ mod tests {
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
writer.fold(vec![0..2, 2..5]);
let (snapshot, _) = map.read(buffer_snapshot, vec![]);
assert_eq!(snapshot.text(), "fghijkl");
assert_eq!(snapshot.text(), "fghijkl");
// Edit within one of the folds.
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
@@ -1317,7 +1338,7 @@ mod tests {
buffer.snapshot(cx)
});
let (snapshot, _) = map.read(buffer_snapshot, subscription.consume().into_inner());
assert_eq!(snapshot.text(), "12345fghijkl");
assert_eq!(snapshot.text(), "12345fghijkl");
}
}
@@ -1334,7 +1355,7 @@ mod tests {
Point::new(3, 1)..Point::new(4, 1),
]);
let (snapshot, _) = map.read(buffer_snapshot, vec![]);
assert_eq!(snapshot.text(), "aaeeeee");
assert_eq!(snapshot.text(), "aaeeeee");
}
#[gpui::test]
@@ -1351,14 +1372,14 @@ mod tests {
Point::new(3, 1)..Point::new(4, 1),
]);
let (snapshot, _) = map.read(buffer_snapshot, vec![]);
assert_eq!(snapshot.text(), "aacccc\ndeeeee");
assert_eq!(snapshot.text(), "aacccc\ndeeeee");
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], None, cx);
buffer.snapshot(cx)
});
let (snapshot, _) = map.read(buffer_snapshot, subscription.consume().into_inner());
assert_eq!(snapshot.text(), "aaeeeee");
assert_eq!(snapshot.text(), "aaeeeee");
}
#[gpui::test]
@@ -1450,7 +1471,7 @@ mod tests {
let mut expected_text: String = buffer_snapshot.text().to_string();
for fold_range in map.merged_fold_ranges().into_iter().rev() {
expected_text.replace_range(fold_range.start..fold_range.end, "");
expected_text.replace_range(fold_range.start..fold_range.end, "");
}
assert_eq!(snapshot.text(), expected_text);
@@ -1655,7 +1676,7 @@ mod tests {
]);
let (snapshot, _) = map.read(buffer_snapshot, vec![]);
assert_eq!(snapshot.text(), "aacccc\ndeeeee\nffffff\n");
assert_eq!(snapshot.text(), "aacccc\ndeeeee\nffffff\n");
assert_eq!(
snapshot.buffer_rows(0).collect::<Vec<_>>(),
[Some(0), Some(3), Some(5), Some(6)]

View File

@@ -1,6 +1,7 @@
mod blink_manager;
pub mod display_map;
mod element;
mod git;
mod highlight_matching_bracket;
mod hover_popover;
@@ -77,14 +78,14 @@ use std::{
cmp::{self, Ordering, Reverse},
mem,
num::NonZeroU32,
ops::{Deref, DerefMut, Range, RangeInclusive},
ops::{Deref, DerefMut, Range},
path::Path,
sync::Arc,
time::{Duration, Instant},
};
pub use sum_tree::Bias;
use theme::{DiagnosticStyle, Theme};
use util::{post_inc, ResultExt, TryFutureExt};
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::{ItemNavHistory, ViewId, Workspace, WorkspaceId};
use crate::git::diff_hunk_to_display;
@@ -154,6 +155,27 @@ pub struct ConfirmCodeAction {
pub item_ix: Option<usize>,
}
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct ToggleComments {
#[serde(default)]
pub advance_downwards: bool,
}
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct FoldAt {
pub display_row: u32,
}
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct UnfoldAt {
pub display_row: u32,
}
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct GutterHover {
pub hovered: bool,
}
actions!(
editor,
[
@@ -216,7 +238,6 @@ actions!(
AddSelectionBelow,
Tab,
TabPrev,
ToggleComments,
ShowCharacterPalette,
SelectLargerSyntaxNode,
SelectSmallerSyntaxNode,
@@ -236,6 +257,8 @@ actions!(
RestartLanguageServer,
Hover,
Format,
ToggleSoftWrap,
RevealInFinder
]
);
@@ -250,6 +273,10 @@ impl_actions!(
MovePageDown,
ConfirmCompletion,
ConfirmCodeAction,
ToggleComments,
FoldAt,
UnfoldAt,
GutterHover
]
);
@@ -340,12 +367,17 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(Editor::go_to_definition);
cx.add_action(Editor::go_to_type_definition);
cx.add_action(Editor::fold);
cx.add_action(Editor::fold_at);
cx.add_action(Editor::unfold_lines);
cx.add_action(Editor::unfold_at);
cx.add_action(Editor::gutter_hover);
cx.add_action(Editor::fold_selected_ranges);
cx.add_action(Editor::show_completions);
cx.add_action(Editor::toggle_code_actions);
cx.add_action(Editor::open_excerpts);
cx.add_action(Editor::jump);
cx.add_action(Editor::toggle_soft_wrap);
cx.add_action(Editor::reveal_in_finder);
cx.add_async_action(Editor::format);
cx.add_action(Editor::restart_language_server);
cx.add_action(Editor::show_character_palette);
@@ -400,7 +432,7 @@ pub enum SelectMode {
All,
}
#[derive(Copy, Clone, PartialEq, Eq)]
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum EditorMode {
SingleLine,
AutoHeight { max_lines: usize },
@@ -470,6 +502,7 @@ pub struct Editor {
leader_replica_id: Option<u16>,
remote_id: Option<ViewId>,
hover_state: HoverState,
gutter_hovered: bool,
link_go_to_definition_state: LinkGoToDefinitionState,
_subscriptions: Vec<Subscription>,
}
@@ -810,7 +843,7 @@ impl CompletionsMenu {
fuzzy::match_strings(
&self.match_candidates,
query,
false,
query.chars().any(|c| c.is_uppercase()),
100,
&Default::default(),
executor,
@@ -1141,6 +1174,7 @@ impl Editor {
remote_id: None,
hover_state: Default::default(),
link_go_to_definition_state: Default::default(),
gutter_hovered: false,
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
cx.subscribe(&buffer, Self::on_buffer_event),
@@ -1220,6 +1254,15 @@ impl Editor {
self.buffer.read(cx).language_at(point, cx)
}
pub fn active_excerpt(
&self,
cx: &AppContext,
) -> Option<(ExcerptId, ModelHandle<Buffer>, Range<text::Anchor>)> {
self.buffer
.read(cx)
.excerpt_containing(self.selections.newest_anchor().head(), cx)
}
fn style(&self, cx: &AppContext) -> EditorStyle {
build_style(
cx.global::<Settings>(),
@@ -1732,11 +1775,13 @@ impl Editor {
}
pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
let text: Arc<str> = text.into();
if !self.input_enabled {
cx.emit(Event::InputIgnored { text });
return;
}
let text: Arc<str> = text.into();
let selections = self.selections.all_adjusted(cx);
let mut edits = Vec::new();
let mut new_selections = Vec::with_capacity(selections.len());
@@ -1751,8 +1796,8 @@ impl Editor {
// bracket of any of this language's bracket pairs.
let mut bracket_pair = None;
let mut is_bracket_pair_start = false;
for pair in language.brackets() {
if pair.close && pair.start.ends_with(text.as_ref()) {
for (pair, enabled) in language.brackets() {
if enabled && pair.close && pair.start.ends_with(text.as_ref()) {
bracket_pair = Some(pair.clone());
is_bracket_pair_start = true;
break;
@@ -1814,9 +1859,9 @@ impl Editor {
}
}
}
// If an opening bracket is typed while text is selected, then
// surround that text with the bracket pair.
else if is_bracket_pair_start {
// If an opening bracket is 1 character long and is typed while
// text is selected, then surround that text with the bracket pair.
else if is_bracket_pair_start && bracket_pair.start.chars().count() == 1 {
edits.push((selection.start..selection.start, text.clone()));
edits.push((
selection.end..selection.end,
@@ -1920,11 +1965,12 @@ impl Editor {
.map(|c| c.len_utf8())
.sum::<usize>();
insert_extra_newline = language.brackets().iter().any(|pair| {
insert_extra_newline = language.brackets().any(|(pair, enabled)| {
let pair_start = pair.start.trim_end();
let pair_end = pair.end.trim_start();
pair.newline
enabled
&& pair.newline
&& buffer
.contains_str_at(end + trailing_whitespace_len, pair_end)
&& buffer.contains_str_at(
@@ -2632,14 +2678,15 @@ impl Editor {
pub fn render_code_actions_indicator(
&self,
style: &EditorStyle,
active: bool,
cx: &mut RenderContext<Self>,
) -> Option<ElementBox> {
if self.available_code_actions.is_some() {
enum Tag {}
enum CodeActions {}
Some(
MouseEventHandler::<Tag>::new(0, cx, |_, _| {
MouseEventHandler::<CodeActions>::new(0, cx, |state, _| {
Svg::new("icons/bolt_8.svg")
.with_color(style.code_actions.indicator)
.with_color(style.code_actions.indicator.style_for(state, active).color)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
@@ -2656,6 +2703,80 @@ impl Editor {
}
}
pub fn render_fold_indicators(
&self,
fold_data: Option<Vec<(u32, FoldStatus)>>,
active_rows: &BTreeMap<u32, bool>,
style: &EditorStyle,
gutter_hovered: bool,
line_height: f32,
gutter_margin: f32,
cx: &mut RenderContext<Self>,
) -> Option<Vec<(u32, ElementBox)>> {
enum FoldIndicators {}
let style = style.folds.clone();
fold_data.map(|fold_data| {
fold_data
.iter()
.copied()
.filter_map(|(fold_location, fold_status)| {
(gutter_hovered
|| fold_status == FoldStatus::Folded
|| !*active_rows.get(&fold_location).unwrap_or(&true))
.then(|| {
(
fold_location,
MouseEventHandler::<FoldIndicators>::new(
fold_location as usize,
cx,
|mouse_state, _| -> ElementBox {
Svg::new(match fold_status {
FoldStatus::Folded => style.folded_icon.clone(),
FoldStatus::Foldable => style.foldable_icon.clone(),
})
.with_color(
style
.indicator
.style_for(
mouse_state,
fold_status == FoldStatus::Folded,
)
.color,
)
.constrained()
.with_width(style.icon_width)
.aligned()
.constrained()
.with_height(line_height)
.with_width(gutter_margin)
.aligned()
.boxed()
},
)
.with_cursor_style(CursorStyle::PointingHand)
.with_padding(Padding::uniform(3.))
.on_click(MouseButton::Left, {
move |_, cx| {
cx.dispatch_any_action(match fold_status {
FoldStatus::Folded => Box::new(UnfoldAt {
display_row: fold_location,
}),
FoldStatus::Foldable => Box::new(FoldAt {
display_row: fold_location,
}),
});
}
})
.boxed(),
)
})
})
.collect()
})
}
pub fn context_menu_visible(&self) -> bool {
self.context_menu
.as_ref()
@@ -3238,26 +3359,12 @@ impl Editor {
while let Some(selection) = selections.next() {
// Find all the selections that span a contiguous row range
contiguous_row_selections.push(selection.clone());
let start_row = selection.start.row;
let mut end_row = if selection.end.column > 0 || selection.is_empty() {
display_map.next_line_boundary(selection.end).0.row + 1
} else {
selection.end.row
};
while let Some(next_selection) = selections.peek() {
if next_selection.start.row <= end_row {
end_row = if next_selection.end.column > 0 || next_selection.is_empty() {
display_map.next_line_boundary(next_selection.end).0.row + 1
} else {
next_selection.end.row
};
contiguous_row_selections.push(selections.next().unwrap().clone());
} else {
break;
}
}
let (start_row, end_row) = consume_contiguous_rows(
&mut contiguous_row_selections,
selection,
&display_map,
&mut selections,
);
// Move the text spanned by the row range to be before the line preceding the row range
if start_row > 0 {
@@ -3322,13 +3429,13 @@ impl Editor {
}
self.transact(cx, |this, cx| {
this.unfold_ranges(unfold_ranges, true, cx);
this.unfold_ranges(unfold_ranges, true, true, cx);
this.buffer.update(cx, |buffer, cx| {
for (range, text) in edits {
buffer.edit([(range, text)], None, cx);
}
});
this.fold_ranges(refold_ranges, cx);
this.fold_ranges(refold_ranges, true, cx);
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(new_selections);
})
@@ -3350,26 +3457,12 @@ impl Editor {
while let Some(selection) = selections.next() {
// Find all the selections that span a contiguous row range
contiguous_row_selections.push(selection.clone());
let start_row = selection.start.row;
let mut end_row = if selection.end.column > 0 || selection.is_empty() {
display_map.next_line_boundary(selection.end).0.row + 1
} else {
selection.end.row
};
while let Some(next_selection) = selections.peek() {
if next_selection.start.row <= end_row {
end_row = if next_selection.end.column > 0 || next_selection.is_empty() {
display_map.next_line_boundary(next_selection.end).0.row + 1
} else {
next_selection.end.row
};
contiguous_row_selections.push(selections.next().unwrap().clone());
} else {
break;
}
}
let (start_row, end_row) = consume_contiguous_rows(
&mut contiguous_row_selections,
selection,
&display_map,
&mut selections,
);
// Move the text spanned by the row range to be after the last line of the row range
if end_row <= buffer.max_point().row {
@@ -3427,13 +3520,13 @@ impl Editor {
}
self.transact(cx, |this, cx| {
this.unfold_ranges(unfold_ranges, true, cx);
this.unfold_ranges(unfold_ranges, true, true, cx);
this.buffer.update(cx, |buffer, cx| {
for (range, text) in edits {
buffer.edit([(range, text)], None, cx);
}
});
this.fold_ranges(refold_ranges, cx);
this.fold_ranges(refold_ranges, true, cx);
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
});
}
@@ -3800,7 +3893,7 @@ impl Editor {
}
}
if matches!(self.mode, EditorMode::SingleLine) {
if self.mode == EditorMode::SingleLine {
cx.propagate_action();
return;
}
@@ -4261,7 +4354,7 @@ impl Editor {
to_unfold.push(selection.start..selection.end);
}
}
self.unfold_ranges(to_unfold, true, cx);
self.unfold_ranges(to_unfold, true, true, cx);
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(new_selection_ranges);
});
@@ -4410,7 +4503,7 @@ impl Editor {
}
if let Some(next_selected_range) = next_selected_range {
self.unfold_ranges([next_selected_range.clone()], false, cx);
self.unfold_ranges([next_selected_range.clone()], false, true, cx);
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
if action.replace_newest {
s.delete(s.newest_anchor().id);
@@ -4443,7 +4536,7 @@ impl Editor {
wordwise: true,
done: false,
};
self.unfold_ranges([selection.start..selection.end], false, cx);
self.unfold_ranges([selection.start..selection.end], false, true, cx);
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
s.select(selections);
});
@@ -4462,7 +4555,7 @@ impl Editor {
}
}
pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext<Self>) {
pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext<Self>) {
self.transact(cx, |this, cx| {
let mut selections = this.selections.all::<Point>(cx);
let mut edits = Vec::new();
@@ -4681,6 +4774,34 @@ impl Editor {
drop(snapshot);
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
let selections = this.selections.all::<Point>(cx);
let selections_on_single_row = selections.windows(2).all(|selections| {
selections[0].start.row == selections[1].start.row
&& selections[0].end.row == selections[1].end.row
&& selections[0].start.row == selections[0].end.row
});
let selections_selecting = selections
.iter()
.any(|selection| selection.start != selection.end);
let advance_downwards = action.advance_downwards
&& selections_on_single_row
&& !selections_selecting
&& this.mode != EditorMode::SingleLine;
if advance_downwards {
let snapshot = this.buffer.read(cx).snapshot(cx);
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|display_snapshot, display_point, _| {
let mut point = display_point.to_point(display_snapshot);
point.row += 1;
point = snapshot.clip_point(point, Bias::Left);
let display_point = point.to_display_point(display_snapshot);
(display_point, SelectionGoal::Column(display_point.column()))
})
});
}
});
}
@@ -4750,27 +4871,54 @@ impl Editor {
_: &MoveToEnclosingBracket,
cx: &mut ViewContext<Self>,
) {
let buffer = self.buffer.read(cx).snapshot(cx);
let mut selections = self.selections.all::<usize>(cx);
for selection in &mut selections {
if let Some((open_range, close_range)) =
buffer.enclosing_bracket_ranges(selection.start..selection.end)
{
let close_range = close_range.to_inclusive();
let destination = if close_range.contains(&selection.start)
&& close_range.contains(&selection.end)
{
open_range.end
} else {
*close_range.start()
};
selection.start = destination;
selection.end = destination;
}
}
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(selections);
s.move_offsets_with(|snapshot, selection| {
let Some(enclosing_bracket_ranges) = snapshot.enclosing_bracket_ranges(selection.start..selection.end) else {
return;
};
let mut best_length = usize::MAX;
let mut best_inside = false;
let mut best_in_bracket_range = false;
let mut best_destination = None;
for (open, close) in enclosing_bracket_ranges {
let close = close.to_inclusive();
let length = close.end() - open.start;
let inside = selection.start >= open.end && selection.end <= *close.start();
let in_bracket_range = open.to_inclusive().contains(&selection.head()) || close.contains(&selection.head());
// If best is next to a bracket and current isn't, skip
if !in_bracket_range && best_in_bracket_range {
continue;
}
// Prefer smaller lengths unless best is inside and current isn't
if length > best_length && (best_inside || !inside) {
continue;
}
best_length = length;
best_inside = inside;
best_in_bracket_range = in_bracket_range;
best_destination = Some(if close.contains(&selection.start) && close.contains(&selection.end) {
if inside {
open.end
} else {
open.start
}
} else {
if inside {
*close.start()
} else {
*close.end()
}
});
}
if let Some(destination) = best_destination {
selection.collapse_to(destination, SelectionGoal::None);
}
})
});
}
@@ -5003,7 +5151,7 @@ impl Editor {
GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx),
});
cx.spawn(|workspace, mut cx| async move {
cx.spawn_labeled("Fetching Definition...", |workspace, mut cx| async move {
let definitions = definitions.await?;
workspace.update(&mut cx, |workspace, cx| {
Editor::navigate_to_definitions(workspace, editor_handle, definitions, cx);
@@ -5083,31 +5231,36 @@ impl Editor {
let project = workspace.project().clone();
let references = project.update(cx, |project, cx| project.references(&buffer, head, cx));
Some(cx.spawn(|workspace, mut cx| async move {
let locations = references.await?;
if locations.is_empty() {
return Ok(());
}
Some(cx.spawn_labeled(
"Finding All References...",
|workspace, mut cx| async move {
let locations = references.await?;
if locations.is_empty() {
return Ok(());
}
workspace.update(&mut cx, |workspace, cx| {
let title = locations
.first()
.as_ref()
.map(|location| {
let buffer = location.buffer.read(cx);
format!(
"References to `{}`",
buffer
.text_for_range(location.range.clone())
.collect::<String>()
)
})
.unwrap();
Self::open_locations_in_multibuffer(workspace, locations, replica_id, title, cx);
});
workspace.update(&mut cx, |workspace, cx| {
let title = locations
.first()
.as_ref()
.map(|location| {
let buffer = location.buffer.read(cx);
format!(
"References to `{}`",
buffer
.text_for_range(location.range.clone())
.collect::<String>()
)
})
.unwrap();
Self::open_locations_in_multibuffer(
workspace, locations, replica_id, title, cx,
);
});
Ok(())
}))
Ok(())
},
))
}
/// Opens a multibuffer with the given project locations in it
@@ -5386,21 +5539,20 @@ impl Editor {
None => return None,
};
Some(self.perform_format(project, cx))
Some(self.perform_format(project, FormatTrigger::Manual, cx))
}
fn perform_format(
&mut self,
project: ModelHandle<Project>,
trigger: FormatTrigger,
cx: &mut ViewContext<'_, Self>,
) -> Task<Result<()>> {
let buffer = self.buffer().clone();
let buffers = buffer.read(cx).all_buffers();
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
let format = project.update(cx, |project, cx| {
project.format(buffers, true, FormatTrigger::Manual, cx)
});
let format = project.update(cx, |project, cx| project.format(buffers, true, trigger, cx));
cx.spawn(|_, mut cx| async move {
let transaction = futures::select_biased! {
@@ -5604,14 +5756,18 @@ impl Editor {
let mut fold_ranges = Vec::new();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all::<Point>(cx);
for selection in selections {
let range = selection.display_range(&display_map).sorted();
let buffer_start_row = range.start.to_point(&display_map).row;
for row in (0..=range.end.row()).rev() {
if self.is_line_foldable(&display_map, row) && !display_map.is_line_folded(row) {
let fold_range = self.foldable_range_for_line(&display_map, row);
let fold_range = display_map.foldable_range(row).map(|range| {
range.start.to_point(&display_map)..range.end.to_point(&display_map)
});
if let Some(fold_range) = fold_range {
if fold_range.end.row >= buffer_start_row {
fold_ranges.push(fold_range);
if row <= range.start.row() {
@@ -5622,7 +5778,26 @@ impl Editor {
}
}
self.fold_ranges(fold_ranges, cx);
self.fold_ranges(fold_ranges, true, cx);
}
pub fn fold_at(&mut self, fold_at: &FoldAt, cx: &mut ViewContext<Self>) {
let display_row = fold_at.display_row;
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
if let Some(fold_range) = display_map.foldable_range(display_row) {
let autoscroll = self
.selections
.all::<Point>(cx)
.iter()
.any(|selection| fold_range.overlaps(&selection.display_range(&display_map)));
let fold_range =
fold_range.start.to_point(&display_map)..fold_range.end.to_point(&display_map);
self.fold_ranges(std::iter::once(fold_range), autoscroll, cx);
}
}
pub fn unfold_lines(&mut self, _: &UnfoldLines, cx: &mut ViewContext<Self>) {
@@ -5640,85 +5815,86 @@ impl Editor {
start..end
})
.collect::<Vec<_>>();
self.unfold_ranges(ranges, true, cx);
self.unfold_ranges(ranges, true, true, cx);
}
fn is_line_foldable(&self, display_map: &DisplaySnapshot, display_row: u32) -> bool {
let max_point = display_map.max_point();
if display_row >= max_point.row() {
false
} else {
let (start_indent, is_blank) = display_map.line_indent(display_row);
if is_blank {
false
} else {
for display_row in display_row + 1..=max_point.row() {
let (indent, is_blank) = display_map.line_indent(display_row);
if !is_blank {
return indent > start_indent;
}
}
false
}
}
}
pub fn unfold_at(&mut self, unfold_at: &UnfoldAt, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
fn foldable_range_for_line(
&self,
display_map: &DisplaySnapshot,
start_row: u32,
) -> Range<Point> {
let max_point = display_map.max_point();
let intersection_range = DisplayPoint::new(unfold_at.display_row, 0)
..DisplayPoint::new(
unfold_at.display_row,
display_map.line_len(unfold_at.display_row),
);
let (start_indent, _) = display_map.line_indent(start_row);
let start = DisplayPoint::new(start_row, display_map.line_len(start_row));
let mut end = None;
for row in start_row + 1..=max_point.row() {
let (indent, is_blank) = display_map.line_indent(row);
if !is_blank && indent <= start_indent {
end = Some(DisplayPoint::new(row - 1, display_map.line_len(row - 1)));
break;
}
}
let autoscroll =
self.selections.all::<Point>(cx).iter().any(|selection| {
intersection_range.overlaps(&selection.display_range(&display_map))
});
let end = end.unwrap_or(max_point);
start.to_point(display_map)..end.to_point(display_map)
let display_point = DisplayPoint::new(unfold_at.display_row, 0).to_point(&display_map);
let mut point_range = display_point..display_point;
point_range.start.column = 0;
point_range.end.column = display_map.buffer_snapshot.line_len(point_range.end.row);
self.unfold_ranges(std::iter::once(point_range), true, autoscroll, cx)
}
pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext<Self>) {
let selections = self.selections.all::<Point>(cx);
let ranges = selections.into_iter().map(|s| s.start..s.end);
self.fold_ranges(ranges, cx);
self.fold_ranges(ranges, true, cx);
}
pub fn fold_ranges<T: ToOffset>(
pub fn fold_ranges<T: ToOffset + Clone>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
auto_scroll: bool,
cx: &mut ViewContext<Self>,
) {
let mut ranges = ranges.into_iter().peekable();
if ranges.peek().is_some() {
self.display_map.update(cx, |map, cx| map.fold(ranges, cx));
self.request_autoscroll(Autoscroll::fit(), cx);
if auto_scroll {
self.request_autoscroll(Autoscroll::fit(), cx);
}
cx.notify();
}
}
pub fn unfold_ranges<T: ToOffset>(
pub fn unfold_ranges<T: ToOffset + Clone>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
inclusive: bool,
auto_scroll: bool,
cx: &mut ViewContext<Self>,
) {
let mut ranges = ranges.into_iter().peekable();
if ranges.peek().is_some() {
self.display_map
.update(cx, |map, cx| map.unfold(ranges, inclusive, cx));
self.request_autoscroll(Autoscroll::fit(), cx);
if auto_scroll {
self.request_autoscroll(Autoscroll::fit(), cx);
}
cx.notify();
}
}
pub fn gutter_hover(
&mut self,
GutterHover { hovered }: &GutterHover,
cx: &mut ViewContext<Self>,
) {
self.gutter_hovered = *hovered;
cx.notify();
}
pub fn insert_blocks(
&mut self,
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
@@ -5810,6 +5986,27 @@ impl Editor {
.update(cx, |map, cx| map.set_wrap_width(width, cx))
}
pub fn toggle_soft_wrap(&mut self, _: &ToggleSoftWrap, cx: &mut ViewContext<Self>) {
if self.soft_wrap_mode_override.is_some() {
self.soft_wrap_mode_override.take();
} else {
let soft_wrap = match self.soft_wrap_mode(cx) {
SoftWrap::None => settings::SoftWrap::EditorWidth,
SoftWrap::EditorWidth | SoftWrap::Column(_) => settings::SoftWrap::None,
};
self.soft_wrap_mode_override = Some(soft_wrap);
}
cx.notify();
}
pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
cx.reveal_path(&file.abs_path(cx));
}
}
}
pub fn highlight_rows(&mut self, rows: Option<Range<u32>>) {
self.highlighted_rows = rows;
}
@@ -6159,6 +6356,35 @@ impl Editor {
}
}
fn consume_contiguous_rows(
contiguous_row_selections: &mut Vec<Selection<Point>>,
selection: &Selection<Point>,
display_map: &DisplaySnapshot,
selections: &mut std::iter::Peekable<std::slice::Iter<Selection<Point>>>,
) -> (u32, u32) {
contiguous_row_selections.push(selection.clone());
let start_row = selection.start.row;
let mut end_row = ending_row(selection, display_map);
while let Some(next_selection) = selections.peek() {
if next_selection.start.row <= end_row {
end_row = ending_row(next_selection, display_map);
contiguous_row_selections.push(selections.next().unwrap().clone());
} else {
break;
}
}
(start_row, end_row)
}
fn ending_row(next_selection: &Selection<Point>, display_map: &DisplaySnapshot) -> u32 {
if next_selection.end.column > 0 || next_selection.is_empty() {
display_map.next_line_boundary(next_selection.end).0.row + 1
} else {
next_selection.end.row
}
}
impl EditorSnapshot {
pub fn language_at<T: ToOffset>(&self, position: T) -> Option<&Arc<Language>> {
self.display_snapshot.buffer_snapshot.language_at(position)
@@ -6187,6 +6413,9 @@ impl Deref for EditorSnapshot {
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
InputIgnored {
text: Arc<str>,
},
ExcerptsAdded {
buffer: ModelHandle<Buffer>,
predecessor: ExcerptId,
@@ -6227,6 +6456,7 @@ impl View for Editor {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let style = self.style(cx);
let font_changed = self.display_map.update(cx, |map, cx| {
map.set_fold_ellipses_color(style.folds.ellipses.text_color);
map.set_font(style.text.font_id, style.text.font_size, cx)
});
@@ -6253,8 +6483,10 @@ impl View for Editor {
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
let focused_event = EditorFocused(cx.handle());
cx.emit_global(focused_event);
if cx.is_self_focused() {
let focused_event = EditorFocused(cx.handle());
cx.emit_global(focused_event);
}
if let Some(rename) = self.pending_rename.as_ref() {
cx.focus(&rename.editor);
} else {
@@ -6334,17 +6566,13 @@ impl View for Editor {
EditorMode::AutoHeight { .. } => "auto_height",
EditorMode::Full => "full",
};
context.map.insert("mode".into(), mode.into());
context.add_key("mode", mode);
if self.pending_rename.is_some() {
context.set.insert("renaming".into());
context.add_identifier("renaming");
}
match self.context_menu.as_ref() {
Some(ContextMenu::Completions(_)) => {
context.set.insert("showing_completions".into());
}
Some(ContextMenu::CodeActions(_)) => {
context.set.insert("showing_code_actions".into());
}
Some(ContextMenu::Completions(_)) => context.add_identifier("showing_completions"),
Some(ContextMenu::CodeActions(_)) => context.add_identifier("showing_code_actions"),
None => {}
}
@@ -6393,26 +6621,29 @@ impl View for Editor {
text: &str,
cx: &mut ViewContext<Self>,
) {
self.transact(cx, |this, cx| {
if this.input_enabled {
let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
Some(this.selection_replacement_ranges(range_utf16, cx))
} else {
this.marked_text_ranges(cx)
};
if let Some(new_selected_ranges) = new_selected_ranges {
this.change_selections(None, cx, |selections| {
selections.select_ranges(new_selected_ranges)
});
}
}
this.handle_input(text, cx);
});
if !self.input_enabled {
return;
}
self.transact(cx, |this, cx| {
let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
Some(this.selection_replacement_ranges(range_utf16, cx))
} else {
this.marked_text_ranges(cx)
};
if let Some(new_selected_ranges) = new_selected_ranges {
this.change_selections(None, cx, |selections| {
selections.select_ranges(new_selected_ranges)
});
}
this.handle_input(text, cx);
});
if let Some(transaction) = self.ime_transaction {
self.buffer.update(cx, |buffer, cx| {
buffer.group_until_transaction(transaction, cx);
@@ -6909,21 +7140,6 @@ pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator<Item = &'a str
.flat_map(|word| word.split_inclusive('_'))
}
trait RangeExt<T> {
fn sorted(&self) -> Range<T>;
fn to_inclusive(&self) -> RangeInclusive<T>;
}
impl<T: Ord + Clone> RangeExt<T> for Range<T> {
fn sorted(&self) -> Self {
cmp::min(&self.start, &self.end).clone()..cmp::max(&self.start, &self.end).clone()
}
fn to_inclusive(&self) -> RangeInclusive<T> {
self.start.clone()..=self.end.clone()
}
}
trait RangeToAnchorExt {
fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor>;
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ use super::{
ToPoint, MAX_LINE_LEN,
};
use crate::{
display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
git::{diff_hunk_to_display, DisplayDiffHunk},
hover_popover::{
HideHover, HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
@@ -14,7 +14,7 @@ use crate::{
},
mouse_context_menu::DeployMouseContextMenu,
scroll::actions::Scroll,
EditorStyle,
EditorStyle, GutterHover, UnfoldAt,
};
use clock::ReplicaId;
use collections::{BTreeMap, HashMap};
@@ -48,6 +48,9 @@ use std::{
ops::{DerefMut, Range},
sync::Arc,
};
use workspace::item::Item;
enum FoldMarkers {}
struct SelectionLayout {
head: DisplayPoint,
@@ -212,6 +215,17 @@ impl EditorElement {
}
}),
);
enum GutterHandlers {}
cx.scene.push_mouse_region(
MouseRegion::new::<GutterHandlers>(view.id(), view.id() + 1, gutter_bounds).on_hover(
|hover, cx| {
cx.dispatch_action(GutterHover {
hovered: hover.started,
})
},
),
)
}
fn mouse_down(
@@ -400,16 +414,7 @@ impl EditorElement {
) -> bool {
// This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
// Don't trigger hover popover if mouse is hovering over context menu
let point = if text_bounds.contains_point(position) {
let (point, target_point) = position_map.point_for_position(text_bounds, position);
if point == target_point {
Some(point)
} else {
None
}
} else {
None
};
let point = position_to_display_point(position, text_bounds, position_map);
cx.dispatch_action(UpdateGoToDefinitionLink {
point,
@@ -418,6 +423,7 @@ impl EditorElement {
});
cx.dispatch_action(HoverAt { point });
true
}
@@ -569,12 +575,25 @@ impl EditorElement {
}
if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
let mut x = bounds.width() - layout.gutter_padding;
let mut x = 0.;
let mut y = *row as f32 * line_height - scroll_top;
x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
y += (line_height - indicator.size().y()) / 2.;
indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
}
layout.fold_indicators.as_mut().map(|fold_indicators| {
for (line, fold_indicator) in fold_indicators.iter_mut() {
let mut x = bounds.width() - layout.gutter_padding;
let mut y = *line as f32 * line_height - scroll_top;
x += ((layout.gutter_padding + layout.gutter_margin) - fold_indicator.size().x())
/ 2.;
y += (line_height - fold_indicator.size().y()) / 2.;
fold_indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
}
});
}
fn paint_diff_hunks(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
@@ -676,6 +695,7 @@ impl EditorElement {
let max_glyph_width = layout.position_map.em_width;
let scroll_left = scroll_position.x() * max_glyph_width;
let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
let line_end_overshoot = 0.15 * layout.position_map.line_height;
cx.scene.push_layer(Some(bounds));
@@ -688,12 +708,54 @@ impl EditorElement {
},
});
let fold_corner_radius =
self.style.folds.ellipses.corner_radius_factor * layout.position_map.line_height;
for (id, range, color) in layout.fold_ranges.iter() {
self.paint_highlighted_range(
range.clone(),
*color,
fold_corner_radius,
fold_corner_radius * 2.,
layout,
content_origin,
scroll_top,
scroll_left,
bounds,
cx,
);
for bound in range_to_bounds(
&range,
content_origin,
scroll_left,
scroll_top,
&layout.visible_display_row_range,
line_end_overshoot,
&layout.position_map,
) {
cx.scene.push_cursor_region(CursorRegion {
bounds: bound,
style: CursorStyle::PointingHand,
});
let display_row = range.start.row();
cx.scene.push_mouse_region(
MouseRegion::new::<FoldMarkers>(self.view.id(), *id as usize, bound)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(UnfoldAt { display_row })
})
.with_notify_on_hover(true)
.with_notify_on_click(true),
)
}
}
for (range, color) in &layout.highlighted_ranges {
self.paint_highlighted_range(
range.clone(),
*color,
0.,
0.15 * layout.position_map.line_height,
line_end_overshoot,
layout,
content_origin,
scroll_top,
@@ -704,9 +766,10 @@ impl EditorElement {
}
let mut cursors = SmallVec::<[Cursor; 32]>::new();
let corner_radius = 0.15 * layout.position_map.line_height;
for (replica_id, selections) in &layout.selections {
let selection_style = style.replica_selection_style(*replica_id);
let corner_radius = 0.15 * layout.position_map.line_height;
for selection in selections {
self.paint_highlighted_range(
@@ -1118,6 +1181,24 @@ impl EditorElement {
.width()
}
fn get_fold_indicators(
&self,
is_singleton: bool,
display_rows: Range<u32>,
snapshot: &EditorSnapshot,
) -> Option<Vec<(u32, FoldStatus)>> {
is_singleton.then(|| {
display_rows
.into_iter()
.filter_map(|display_row| {
snapshot
.fold_for_line(display_row)
.map(|fold_status| (display_row, fold_status))
})
.collect()
})
}
//Folds contained in a hunk are ignored apart from shrinking visual size
//If a fold contains any hunks then that fold line is marked as modified
fn layout_git_gutters(
@@ -1438,7 +1519,7 @@ impl EditorElement {
} else {
let text_style = self.style.text.clone();
Flex::row()
.with_child(Label::new("".to_string(), text_style).boxed())
.with_child(Label::new("", text_style).boxed())
.with_children(jump_icon)
.contained()
.with_padding_left(gutter_padding)
@@ -1534,15 +1615,14 @@ impl Element for EditorElement {
let snapshot = self.update_view(cx.app, |view, cx| {
view.set_visible_line_count(size.y() / line_height);
let editor_width = text_width - gutter_margin - overscroll.x() - em_width;
let wrap_width = match view.soft_wrap_mode(cx) {
SoftWrap::None => Some((MAX_LINE_LEN / 2) as f32 * em_advance),
SoftWrap::EditorWidth => {
Some(text_width - gutter_margin - overscroll.x() - em_width)
}
SoftWrap::Column(column) => Some(column as f32 * em_advance),
SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance,
SoftWrap::EditorWidth => editor_width,
SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance),
};
if view.set_wrap_width(wrap_width, cx) {
if view.set_wrap_width(Some(wrap_width), cx) {
view.snapshot(cx)
} else {
snapshot
@@ -1607,9 +1687,13 @@ impl Element for EditorElement {
let mut active_rows = BTreeMap::new();
let mut highlighted_rows = None;
let mut highlighted_ranges = Vec::new();
let mut fold_ranges = Vec::new();
let mut show_scrollbars = false;
let mut include_root = false;
let mut is_singleton = false;
self.update_view(cx.app, |view, cx| {
is_singleton = view.is_singleton(cx);
let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
highlighted_rows = view.highlighted_rows();
@@ -1617,6 +1701,19 @@ impl Element for EditorElement {
highlighted_ranges =
view.background_highlights_in_range(start_anchor..end_anchor, &display_map, theme);
fold_ranges.extend(
snapshot
.folds_in_range(start_anchor..end_anchor)
.map(|anchor| {
let start = anchor.start.to_point(&snapshot.buffer_snapshot);
(
start.row,
start.to_display_point(&snapshot.display_snapshot)
..anchor.end.to_display_point(&snapshot),
)
}),
);
let mut remote_selections = HashMap::default();
for (replica_id, line_mode, cursor_shape, selection) in display_map
.buffer_snapshot
@@ -1685,11 +1782,28 @@ impl Element for EditorElement {
.unwrap_or_default()
});
let fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)> = fold_ranges
.into_iter()
.map(|(id, fold)| {
let color = self
.style
.folds
.ellipses
.background
.style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize), false)
.color;
(id, fold, color)
})
.collect();
let line_number_layouts =
self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx);
let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot);
let folds = self.get_fold_indicators(is_singleton, start_row..end_row, &snapshot);
let scrollbar_row_range = scroll_position.y()..(scroll_position.y() + height_in_lines);
let mut max_visible_line_width = 0.0;
@@ -1756,7 +1870,7 @@ impl Element for EditorElement {
let mut code_actions_indicator = None;
let mut hover = None;
let mut mode = EditorMode::Full;
cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
let mut fold_indicators = cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
let newest_selection_head = view
.selections
.newest::<usize>(cx)
@@ -1770,14 +1884,26 @@ impl Element for EditorElement {
view.render_context_menu(newest_selection_head, style.clone(), cx);
}
let active = matches!(view.context_menu, Some(crate::ContextMenu::CodeActions(_)));
code_actions_indicator = view
.render_code_actions_indicator(&style, cx)
.render_code_actions_indicator(&style, active, cx)
.map(|indicator| (newest_selection_head.row(), indicator));
}
let visible_rows = start_row..start_row + line_layouts.len() as u32;
hover = view.hover_state.render(&snapshot, &style, visible_rows, cx);
mode = view.mode;
view.render_fold_indicators(
folds,
&active_rows,
&style,
view.gutter_hovered,
line_height,
gutter_margin,
cx,
)
});
if let Some((_, context_menu)) = context_menu.as_mut() {
@@ -1803,6 +1929,18 @@ impl Element for EditorElement {
);
}
fold_indicators.as_mut().map(|fold_indicators| {
for (_, indicator) in fold_indicators.iter_mut() {
indicator.layout(
SizeConstraint::strict_along(
Axis::Vertical,
line_height * style.code_actions.vertical_scale,
),
cx,
);
}
});
if let Some((_, hover_popovers)) = hover.as_mut() {
for hover_popover in hover_popovers.iter_mut() {
hover_popover.layout(
@@ -1846,12 +1984,14 @@ impl Element for EditorElement {
active_rows,
highlighted_rows,
highlighted_ranges,
fold_ranges,
line_number_layouts,
display_hunks,
blocks,
selections,
context_menu,
code_actions_indicator,
fold_indicators,
hover_popovers: hover,
},
)
@@ -1959,6 +2099,8 @@ impl Element for EditorElement {
}
}
type BufferRow = u32;
pub struct LayoutState {
position_map: Arc<PositionMap>,
gutter_size: Vector2F,
@@ -1973,6 +2115,7 @@ pub struct LayoutState {
display_hunks: Vec<DisplayDiffHunk>,
blocks: Vec<BlockLayout>,
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)>,
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
scrollbar_row_range: Range<f32>,
show_scrollbars: bool,
@@ -1980,6 +2123,7 @@ pub struct LayoutState {
context_menu: Option<(DisplayPoint, ElementBox)>,
code_actions_indicator: Option<(u32, ElementBox)>,
hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
fold_indicators: Option<Vec<(u32, ElementBox)>>,
}
pub struct PositionMap {
@@ -2278,6 +2422,75 @@ impl HighlightedRange {
}
}
pub fn position_to_display_point(
position: Vector2F,
text_bounds: RectF,
position_map: &PositionMap,
) -> Option<DisplayPoint> {
if text_bounds.contains_point(position) {
let (point, target_point) = position_map.point_for_position(text_bounds, position);
if point == target_point {
Some(point)
} else {
None
}
} else {
None
}
}
pub fn range_to_bounds(
range: &Range<DisplayPoint>,
content_origin: Vector2F,
scroll_left: f32,
scroll_top: f32,
visible_row_range: &Range<u32>,
line_end_overshoot: f32,
position_map: &PositionMap,
) -> impl Iterator<Item = RectF> {
let mut bounds: SmallVec<[RectF; 1]> = SmallVec::new();
if range.start == range.end {
return bounds.into_iter();
}
let start_row = visible_row_range.start;
let end_row = visible_row_range.end;
let row_range = if range.end.column() == 0 {
cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
} else {
cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row)
};
let first_y =
content_origin.y() + row_range.start as f32 * position_map.line_height - scroll_top;
for (idx, row) in row_range.enumerate() {
let line_layout = &position_map.line_layouts[(row - start_row) as usize];
let start_x = if row == range.start.row() {
content_origin.x() + line_layout.x_for_index(range.start.column() as usize)
- scroll_left
} else {
content_origin.x() - scroll_left
};
let end_x = if row == range.end.row() {
content_origin.x() + line_layout.x_for_index(range.end.column() as usize) - scroll_left
} else {
content_origin.x() + line_layout.width() + line_end_overshoot - scroll_left
};
bounds.push(RectF::from_points(
vec2f(start_x, first_y + position_map.line_height * idx as f32),
vec2f(end_x, first_y + position_map.line_height * (idx + 1) as f32),
))
}
bounds.into_iter()
}
pub fn scale_vertical_mouse_autoscroll_delta(delta: f32) -> f32 {
delta.powf(1.5) / 100.0
}

View File

@@ -17,7 +17,7 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
let snapshot = editor.snapshot(cx);
if let Some((opening_range, closing_range)) = snapshot
.buffer_snapshot
.enclosing_bracket_ranges(head..head)
.innermost_enclosing_bracket_ranges(head..head)
{
editor.highlight_background::<MatchingBracketHighlight>(
vec![
@@ -32,11 +32,10 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
#[cfg(test)]
mod tests {
use crate::test::editor_lsp_test_context::EditorLspTestContext;
use super::*;
use crate::test::editor_lsp_test_context::EditorLspTestContext;
use indoc::indoc;
use language::{BracketPair, Language, LanguageConfig};
use language::{BracketPair, BracketPairConfig, Language, LanguageConfig};
#[gpui::test]
async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
@@ -45,20 +44,23 @@ mod tests {
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
brackets: vec![
BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: false,
newline: true,
},
BracketPair {
start: "(".to_string(),
end: ")".to_string(),
close: false,
newline: true,
},
],
brackets: BracketPairConfig {
pairs: vec![
BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: false,
newline: true,
},
BracketPair {
start: "(".to_string(),
end: ")".to_string(),
close: false,
newline: true,
},
],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::language()),

View File

@@ -1,3 +1,4 @@
use futures::FutureExt;
use gpui::{
actions,
elements::{Flex, MouseEventHandler, Padding, Text},
@@ -327,12 +328,10 @@ impl InfoPopover {
MouseEventHandler::<InfoPopover>::new(0, cx, |_, cx| {
let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
flex.extend(self.contents.iter().map(|content| {
let project = self.project.read(cx);
if let Some(language) = content
.language
.clone()
.and_then(|language| project.languages().language_for_name(&language))
{
let languages = self.project.read(cx).languages();
if let Some(language) = content.language.clone().and_then(|language| {
languages.language_for_name(&language).now_or_never()?.ok()
}) {
let runs = language
.highlight_text(&content.text.as_str().into(), 0..content.text.len());

View File

@@ -2,12 +2,10 @@ use crate::{
display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
FORMAT_TIMEOUT,
};
use anyhow::{anyhow, Context, Result};
use collections::HashSet;
use futures::future::try_join_all;
use futures::FutureExt;
use gpui::{
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
@@ -19,6 +17,7 @@ use language::{
use project::{FormatTrigger, Item as _, Project, ProjectPath};
use rpc::proto::{self, update_view};
use settings::Settings;
use smallvec::SmallVec;
use std::{
borrow::Cow,
cmp::{self, Ordering},
@@ -530,7 +529,7 @@ impl Item for Editor {
) -> ElementBox {
Flex::row()
.with_child(
Label::new(self.title(cx).into(), style.label.clone())
Label::new(self.title(cx).to_string(), style.label.clone())
.aligned()
.boxed(),
)
@@ -609,33 +608,38 @@ impl Item for Editor {
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.report_event("save editor", cx);
let format = self.perform_format(project.clone(), FormatTrigger::Save, cx);
let buffers = self.buffer().clone().read(cx).all_buffers();
cx.as_mut().spawn(|mut cx| async move {
format.await?;
let buffer = self.buffer().clone();
let buffers = buffer.read(cx).all_buffers();
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
let format = project.update(cx, |project, cx| {
project.format(buffers, true, FormatTrigger::Save, cx)
});
cx.spawn(|_, mut cx| async move {
let transaction = futures::select_biased! {
_ = timeout => {
log::warn!("timed out waiting for formatting");
None
if buffers.len() == 1 {
project
.update(&mut cx, |project, cx| project.save_buffers(buffers, cx))
.await?;
} else {
// For multi-buffers, only save those ones that contain changes. For clean buffers
// we simulate saving by calling `Buffer::did_save`, so that language servers or
// other downstream listeners of save events get notified.
let (dirty_buffers, clean_buffers) = buffers.into_iter().partition(|buffer| {
buffer.read_with(&cx, |buffer, _| buffer.is_dirty() || buffer.has_conflict())
});
project
.update(&mut cx, |project, cx| {
project.save_buffers(dirty_buffers, cx)
})
.await?;
for buffer in clean_buffers {
buffer.update(&mut cx, |buffer, cx| {
let version = buffer.saved_version().clone();
let fingerprint = buffer.saved_version_fingerprint();
let mtime = buffer.saved_mtime();
buffer.did_save(version, fingerprint, mtime, cx);
});
}
transaction = format.log_err().fuse() => transaction,
};
}
buffer
.update(&mut cx, |buffer, cx| {
if let Some(transaction) = transaction {
if !buffer.is_singleton() {
buffer.push_transaction(&transaction.0);
}
}
buffer.save(cx)
})
.await?;
Ok(())
})
}
@@ -693,8 +697,8 @@ impl Item for Editor {
Task::ready(Ok(()))
}
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
let mut result = Vec::new();
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
let mut result = SmallVec::new();
match event {
Event::Closed => result.push(ItemEvent::CloseItem),
Event::Saved | Event::TitleChanged => {
@@ -907,7 +911,7 @@ impl SearchableItem for Editor {
matches: Vec<Range<Anchor>>,
cx: &mut ViewContext<Self>,
) {
self.unfold_ranges([matches[index].clone()], false, cx);
self.unfold_ranges([matches[index].clone()], false, true, cx);
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([matches[index].clone()])
});
@@ -1158,7 +1162,6 @@ fn path_for_file<'a>(
mod tests {
use super::*;
use gpui::MutableAppContext;
use language::RopeFingerprint;
use std::{
path::{Path, PathBuf},
sync::Arc,
@@ -1204,17 +1207,6 @@ mod tests {
todo!()
}
fn save(
&self,
_: u64,
_: language::Rope,
_: clock::Global,
_: project::LineEnding,
_: &mut MutableAppContext,
) -> gpui::Task<anyhow::Result<(clock::Global, RopeFingerprint, SystemTime)>> {
todo!()
}
fn as_any(&self) -> &dyn std::any::Any {
todo!()
}

View File

@@ -6,7 +6,7 @@ use gpui::{
use crate::{
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
Rename, SelectMode, ToggleCodeActions,
Rename, RevealInFinder, SelectMode, ToggleCodeActions,
};
#[derive(Clone, PartialEq)]
@@ -52,8 +52,8 @@ pub fn deploy_context_menu(
AnchorCorner::TopLeft,
vec![
ContextMenuItem::item("Rename Symbol", Rename),
ContextMenuItem::item("Go To Definition", GoToDefinition),
ContextMenuItem::item("Go To Type Definition", GoToTypeDefinition),
ContextMenuItem::item("Go to Definition", GoToDefinition),
ContextMenuItem::item("Go to Type Definition", GoToTypeDefinition),
ContextMenuItem::item("Find All References", FindAllReferences),
ContextMenuItem::item(
"Code Actions",
@@ -61,6 +61,8 @@ pub fn deploy_context_menu(
deployed_from_indicator: false,
},
),
ContextMenuItem::Separator,
ContextMenuItem::item("Reveal in Finder", RevealInFinder),
],
cx,
);

View File

@@ -1,7 +1,6 @@
mod anchor;
pub use anchor::{Anchor, AnchorRangeExt};
use anyhow::Result;
use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet};
use futures::{channel::mpsc, SinkExt};
@@ -385,9 +384,13 @@ impl MultiBuffer {
_ => Default::default(),
};
#[allow(clippy::type_complexity)]
let mut buffer_edits: HashMap<usize, Vec<(Range<usize>, Arc<str>, bool, u32)>> =
Default::default();
struct BufferEdit {
range: Range<usize>,
new_text: Arc<str>,
is_insertion: bool,
original_indent_column: u32,
}
let mut buffer_edits: HashMap<usize, Vec<BufferEdit>> = Default::default();
let mut cursor = snapshot.excerpts.cursor::<usize>();
for (ix, (range, new_text)) in edits.enumerate() {
let new_text: Arc<str> = new_text.into();
@@ -422,12 +425,12 @@ impl MultiBuffer {
buffer_edits
.entry(start_excerpt.buffer_id)
.or_insert(Vec::new())
.push((
buffer_start..buffer_end,
.push(BufferEdit {
range: buffer_start..buffer_end,
new_text,
true,
is_insertion: true,
original_indent_column,
));
});
} else {
let start_excerpt_range = buffer_start
..start_excerpt
@@ -444,21 +447,21 @@ impl MultiBuffer {
buffer_edits
.entry(start_excerpt.buffer_id)
.or_insert(Vec::new())
.push((
start_excerpt_range,
new_text.clone(),
true,
.push(BufferEdit {
range: start_excerpt_range,
new_text: new_text.clone(),
is_insertion: true,
original_indent_column,
));
});
buffer_edits
.entry(end_excerpt.buffer_id)
.or_insert(Vec::new())
.push((
end_excerpt_range,
new_text.clone(),
false,
.push(BufferEdit {
range: end_excerpt_range,
new_text: new_text.clone(),
is_insertion: false,
original_indent_column,
));
});
cursor.seek(&range.start, Bias::Right, &());
cursor.next(&());
@@ -469,19 +472,19 @@ impl MultiBuffer {
buffer_edits
.entry(excerpt.buffer_id)
.or_insert(Vec::new())
.push((
excerpt.range.context.to_offset(&excerpt.buffer),
new_text.clone(),
false,
.push(BufferEdit {
range: excerpt.range.context.to_offset(&excerpt.buffer),
new_text: new_text.clone(),
is_insertion: false,
original_indent_column,
));
});
cursor.next(&());
}
}
}
for (buffer_id, mut edits) in buffer_edits {
edits.sort_unstable_by_key(|(range, _, _, _)| range.start);
edits.sort_unstable_by_key(|edit| edit.range.start);
self.buffers.borrow()[&buffer_id]
.buffer
.update(cx, |buffer, cx| {
@@ -490,14 +493,19 @@ impl MultiBuffer {
let mut original_indent_columns = Vec::new();
let mut deletions = Vec::new();
let empty_str: Arc<str> = "".into();
while let Some((
while let Some(BufferEdit {
mut range,
new_text,
mut is_insertion,
original_indent_column,
)) = edits.next()
}) = edits.next()
{
while let Some((next_range, _, next_is_insertion, _)) = edits.peek() {
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;
@@ -1074,18 +1082,21 @@ impl MultiBuffer {
let mut cursor = snapshot.excerpts.cursor::<usize>();
cursor.seek(&position, Bias::Right, &());
cursor.item().map(|excerpt| {
(
excerpt.id.clone(),
self.buffers
.borrow()
.get(&excerpt.buffer_id)
.unwrap()
.buffer
.clone(),
excerpt.range.context.clone(),
)
})
cursor
.item()
.or_else(|| snapshot.excerpts.last())
.map(|excerpt| {
(
excerpt.id.clone(),
self.buffers
.borrow()
.get(&excerpt.buffer_id)
.unwrap()
.buffer
.clone(),
excerpt.range.context.clone(),
)
})
}
// If point is at the end of the buffer, the last excerpt is returned
@@ -1279,20 +1290,6 @@ impl MultiBuffer {
.map(|state| state.buffer.clone())
}
pub fn save(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let mut save_tasks = Vec::new();
for BufferState { buffer, .. } in self.buffers.borrow().values() {
save_tasks.push(buffer.update(cx, |buffer, cx| buffer.save(cx)));
}
cx.spawn(|_, _| async move {
for save in save_tasks {
save.await?;
}
Ok(())
})
}
pub fn is_completion_trigger<T>(&self, position: T, text: &str, cx: &AppContext) -> bool
where
T: ToOffset,
@@ -2621,57 +2618,89 @@ impl MultiBufferSnapshot {
self.parse_count
}
pub fn enclosing_bracket_ranges<T: ToOffset>(
/// Returns the smallest enclosing bracket ranges containing the given range or
/// None if no brackets contain range or the range is not contained in a single
/// excerpt
pub fn innermost_enclosing_bracket_ranges<T: ToOffset>(
&self,
range: Range<T>,
) -> Option<(Range<usize>, Range<usize>)> {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut cursor = self.excerpts.cursor::<usize>();
cursor.seek(&range.start, Bias::Right, &());
let start_excerpt = cursor.item();
// Get the ranges of the innermost pair of brackets.
let mut result: Option<(Range<usize>, Range<usize>)> = None;
cursor.seek(&range.end, Bias::Right, &());
let end_excerpt = cursor.item();
let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else { return None; };
start_excerpt
.zip(end_excerpt)
.and_then(|(start_excerpt, end_excerpt)| {
if start_excerpt.id != end_excerpt.id {
return None;
for (open, close) in enclosing_bracket_ranges {
let len = close.end - open.start;
if let Some((existing_open, existing_close)) = &result {
let existing_len = existing_close.end - existing_open.start;
if len > existing_len {
continue;
}
}
let excerpt_buffer_start = start_excerpt
.range
.context
.start
.to_offset(&start_excerpt.buffer);
let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.len;
result = Some((open, close));
}
let start_in_buffer =
excerpt_buffer_start + range.start.saturating_sub(*cursor.start());
let end_in_buffer =
excerpt_buffer_start + range.end.saturating_sub(*cursor.start());
let (mut start_bracket_range, mut end_bracket_range) = start_excerpt
.buffer
.enclosing_bracket_ranges(start_in_buffer..end_in_buffer)?;
result
}
if start_bracket_range.start >= excerpt_buffer_start
&& end_bracket_range.end <= excerpt_buffer_end
{
/// Returns enclosing bracket ranges containing the given range or returns None if the range is
/// not contained in a single excerpt
pub fn enclosing_bracket_ranges<'a, T: ToOffset>(
&'a self,
range: Range<T>,
) -> Option<impl Iterator<Item = (Range<usize>, Range<usize>)> + 'a> {
let range = range.start.to_offset(self)..range.end.to_offset(self);
self.bracket_ranges(range.clone()).map(|range_pairs| {
range_pairs
.filter(move |(open, close)| open.start <= range.start && close.end >= range.end)
})
}
/// Returns bracket range pairs overlapping the given `range` or returns None if the `range` is
/// not contained in a single excerpt
pub fn bracket_ranges<'a, T: ToOffset>(
&'a self,
range: Range<T>,
) -> Option<impl Iterator<Item = (Range<usize>, Range<usize>)> + 'a> {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let excerpt = self.excerpt_containing(range.clone());
excerpt.map(|(excerpt, excerpt_offset)| {
let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len;
let start_in_buffer = excerpt_buffer_start + range.start.saturating_sub(excerpt_offset);
let end_in_buffer = excerpt_buffer_start + range.end.saturating_sub(excerpt_offset);
excerpt
.buffer
.bracket_ranges(start_in_buffer..end_in_buffer)
.filter_map(move |(start_bracket_range, end_bracket_range)| {
if start_bracket_range.start < excerpt_buffer_start
|| end_bracket_range.end > excerpt_buffer_end
{
return None;
}
let mut start_bracket_range = start_bracket_range.clone();
start_bracket_range.start =
cursor.start() + (start_bracket_range.start - excerpt_buffer_start);
excerpt_offset + (start_bracket_range.start - excerpt_buffer_start);
start_bracket_range.end =
cursor.start() + (start_bracket_range.end - excerpt_buffer_start);
excerpt_offset + (start_bracket_range.end - excerpt_buffer_start);
let mut end_bracket_range = end_bracket_range.clone();
end_bracket_range.start =
cursor.start() + (end_bracket_range.start - excerpt_buffer_start);
excerpt_offset + (end_bracket_range.start - excerpt_buffer_start);
end_bracket_range.end =
cursor.start() + (end_bracket_range.end - excerpt_buffer_start);
excerpt_offset + (end_bracket_range.end - excerpt_buffer_start);
Some((start_bracket_range, end_bracket_range))
} else {
None
}
})
})
})
}
pub fn diagnostics_update_count(&self) -> usize {
@@ -2812,40 +2841,23 @@ impl MultiBufferSnapshot {
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut cursor = self.excerpts.cursor::<usize>();
cursor.seek(&range.start, Bias::Right, &());
let start_excerpt = cursor.item();
cursor.seek(&range.end, Bias::Right, &());
let end_excerpt = cursor.item();
start_excerpt
.zip(end_excerpt)
.and_then(|(start_excerpt, end_excerpt)| {
if start_excerpt.id != end_excerpt.id {
return None;
}
let excerpt_buffer_start = start_excerpt
.range
.context
.start
.to_offset(&start_excerpt.buffer);
let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.len;
self.excerpt_containing(range.clone())
.and_then(|(excerpt, excerpt_offset)| {
let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len;
let start_in_buffer =
excerpt_buffer_start + range.start.saturating_sub(*cursor.start());
let end_in_buffer =
excerpt_buffer_start + range.end.saturating_sub(*cursor.start());
let mut ancestor_buffer_range = start_excerpt
excerpt_buffer_start + range.start.saturating_sub(excerpt_offset);
let end_in_buffer = excerpt_buffer_start + range.end.saturating_sub(excerpt_offset);
let mut ancestor_buffer_range = excerpt
.buffer
.range_for_syntax_ancestor(start_in_buffer..end_in_buffer)?;
ancestor_buffer_range.start =
cmp::max(ancestor_buffer_range.start, excerpt_buffer_start);
ancestor_buffer_range.end = cmp::min(ancestor_buffer_range.end, excerpt_buffer_end);
let start = cursor.start() + (ancestor_buffer_range.start - excerpt_buffer_start);
let end = cursor.start() + (ancestor_buffer_range.end - excerpt_buffer_start);
let start = excerpt_offset + (ancestor_buffer_range.start - excerpt_buffer_start);
let end = excerpt_offset + (ancestor_buffer_range.end - excerpt_buffer_start);
Some(start..end)
})
}
@@ -2929,6 +2941,35 @@ impl MultiBufferSnapshot {
None
}
/// Returns the excerpt containing range and its offset start within the multibuffer or none if `range` spans multiple excerpts
fn excerpt_containing<'a, T: ToOffset>(
&'a self,
range: Range<T>,
) -> Option<(&'a Excerpt, usize)> {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut cursor = self.excerpts.cursor::<usize>();
cursor.seek(&range.start, Bias::Right, &());
let start_excerpt = cursor.item();
if range.start == range.end {
return start_excerpt.map(|excerpt| (excerpt, *cursor.start()));
}
cursor.seek(&range.end, Bias::Right, &());
let end_excerpt = cursor.item();
start_excerpt
.zip(end_excerpt)
.and_then(|(start_excerpt, end_excerpt)| {
if start_excerpt.id != end_excerpt.id {
return None;
}
Some((start_excerpt, *cursor.start()))
})
}
pub fn remote_selections_in_range<'a>(
&'a self,
range: &'a Range<Anchor>,

View File

@@ -659,6 +659,31 @@ impl<'a> MutableSelectionsCollection<'a> {
}
}
pub fn move_offsets_with(
&mut self,
mut move_selection: impl FnMut(&MultiBufferSnapshot, &mut Selection<usize>),
) {
let mut changed = false;
let snapshot = self.buffer().clone();
let selections = self
.all::<usize>(self.cx)
.into_iter()
.map(|selection| {
let mut moved_selection = selection.clone();
move_selection(&snapshot, &mut moved_selection);
if selection != moved_selection {
changed = true;
}
moved_selection
})
.collect();
drop(snapshot);
if changed {
self.select(selections)
}
}
pub fn move_heads_with(
&mut self,
mut update_head: impl FnMut(

View File

@@ -1,4 +1,5 @@
use std::{
borrow::Cow,
ops::{Deref, DerefMut, Range},
sync::Arc,
};
@@ -7,7 +8,8 @@ use anyhow::Result;
use futures::Future;
use gpui::{json, ViewContext, ViewHandle};
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig};
use indoc::indoc;
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries};
use lsp::{notification, request};
use project::Project;
use smol::stream::StreamExt;
@@ -37,7 +39,7 @@ impl<'a> EditorLspTestContext<'a> {
pane::init(cx);
});
let params = cx.update(AppState::test);
let app_state = cx.update(AppState::test);
let file_name = format!(
"file.{}",
@@ -54,24 +56,16 @@ impl<'a> EditorLspTestContext<'a> {
}))
.await;
let project = Project::test(params.fs.clone(), [], cx).await;
let project = Project::test(app_state.fs.clone(), [], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
params
app_state
.fs
.as_fake()
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
.insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
.await;
let (window_id, workspace) = cx.add_window(|cx| {
Workspace::new(
Default::default(),
0,
project.clone(),
|_, _| unimplemented!(),
cx,
)
});
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx)
@@ -105,7 +99,7 @@ impl<'a> EditorLspTestContext<'a> {
},
lsp,
workspace,
buffer_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
buffer_lsp_url: lsp::Url::from_file_path(format!("/root/dir/{file_name}")).unwrap(),
}
}
@@ -120,7 +114,59 @@ impl<'a> EditorLspTestContext<'a> {
..Default::default()
},
Some(tree_sitter_rust::language()),
);
)
.with_queries(LanguageQueries {
indents: Some(Cow::from(indoc! {r#"
[
((where_clause) _ @end)
(field_expression)
(call_expression)
(assignment_expression)
(let_declaration)
(let_chain)
(await_expression)
] @indent
(_ "[" "]" @end) @indent
(_ "<" ">" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent"#})),
brackets: Some(Cow::from(indoc! {r#"
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
("\"" @open "\"" @close)
(closure_parameters "|" @open "|" @close)"#})),
..Default::default()
})
.expect("Could not parse queries");
Self::new(language, capabilities, cx).await
}
pub async fn new_typescript(
capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> {
let language = Language::new(
LanguageConfig {
name: "Typescript".into(),
path_suffixes: vec!["ts".to_string()],
..Default::default()
},
Some(tree_sitter_typescript::language_typescript()),
)
.with_queries(LanguageQueries {
brackets: Some(Cow::from(indoc! {r#"
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
("\"" @open "\"" @close)"#})),
..Default::default()
})
.expect("Could not parse queries");
Self::new(language, capabilities, cx).await
}

View File

@@ -162,10 +162,13 @@ impl<'a> EditorTestContext<'a> {
/// embedded range markers that represent the ranges and directions of
/// each selection.
///
/// Returns a context handle so that assertion failures can print what
/// editor state was needed to cause the failure.
///
/// See the `util::test::marked_text_ranges` function for more information.
pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
let _state_context = self.add_assertion_context(format!(
"Editor State: \"{}\"",
"Initial Editor State: \"{}\"",
marked_text.escape_debug().to_string()
));
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
@@ -182,6 +185,7 @@ impl<'a> EditorTestContext<'a> {
/// of its selections using a string containing embedded range markers.
///
/// See the `util::test::marked_text_ranges` function for more information.
#[track_caller]
pub fn assert_editor_state(&mut self, marked_text: &str) {
let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
let buffer_text = self.buffer_text();

View File

@@ -0,0 +1,82 @@
use gpui::{elements::*, CursorStyle, Entity, MouseButton, RenderContext, View, ViewContext};
use settings::Settings;
use workspace::{item::ItemHandle, StatusItemView};
use crate::feedback_editor::{FeedbackEditor, GiveFeedback};
pub struct DeployFeedbackButton {
active: bool,
}
impl Entity for DeployFeedbackButton {
type Event = ();
}
impl DeployFeedbackButton {
pub fn new() -> Self {
DeployFeedbackButton { active: false }
}
}
impl View for DeployFeedbackButton {
fn ui_name() -> &'static str {
"DeployFeedbackButton"
}
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
let active = self.active;
let theme = cx.global::<Settings>().theme.clone();
Stack::new()
.with_child(
MouseEventHandler::<Self>::new(0, cx, |state, _| {
let style = &theme
.workspace
.status_bar
.sidebar_buttons
.item
.style_for(state, active);
Svg::new("icons/speech_bubble_12.svg")
.with_color(style.icon_color)
.constrained()
.with_width(style.icon_size)
.aligned()
.constrained()
.with_width(style.icon_size)
.with_height(style.icon_size)
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
if !active {
cx.dispatch_action(GiveFeedback)
}
})
.with_tooltip::<Self, _>(
0,
"Give Feedback".into(),
Some(Box::new(GiveFeedback)),
theme.tooltip.clone(),
cx,
)
.boxed(),
)
.boxed()
}
}
impl StatusItemView for DeployFeedbackButton {
fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
if let Some(item) = item {
if let Some(_) = item.downcast::<FeedbackEditor>() {
self.active = true;
cx.notify();
return;
}
}
self.active = false;
cx.notify();
}
}

View File

@@ -1,6 +1,10 @@
pub mod deploy_feedback_button;
pub mod feedback_editor;
pub mod feedback_info_text;
pub mod submit_feedback_button;
use std::sync::Arc;
pub mod feedback_editor;
mod system_specs;
use gpui::{actions, impl_actions, ClipboardItem, MutableAppContext, PromptLevel, ViewContext};
use serde::Deserialize;
@@ -16,7 +20,12 @@ impl_actions!(zed, [OpenBrowser]);
actions!(
zed,
[CopySystemSpecsIntoClipboard, FileBugReport, RequestFeature]
[
CopySystemSpecsIntoClipboard,
FileBugReport,
RequestFeature,
OpenZedCommunityRepo
]
);
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
@@ -28,7 +37,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
let url = format!(
"https://github.com/zed-industries/feedback/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
"https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
urlencoding::encode(&system_specs_text)
);
@@ -48,7 +57,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
cx.add_action(
|_: &mut Workspace, _: &RequestFeature, cx: &mut ViewContext<Workspace>| {
let url = "https://github.com/zed-industries/feedback/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml";
let url = "https://github.com/zed-industries/community/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml";
cx.dispatch_action(OpenBrowser {
url: url.into(),
});
@@ -62,4 +71,11 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
});
},
);
cx.add_action(
|_: &mut Workspace, _: &OpenZedCommunityRepo, cx: &mut ViewContext<Workspace>| {
let url = "https://github.com/zed-industries/community";
cx.dispatch_action(OpenBrowser { url: url.into() });
},
);
}

View File

@@ -10,10 +10,9 @@ use editor::{Anchor, Editor};
use futures::AsyncReadExt;
use gpui::{
actions,
elements::{ChildView, Flex, Label, MouseEventHandler, ParentElement, Stack, Text},
serde_json, AnyViewHandle, AppContext, CursorStyle, Element, ElementBox, Entity, ModelHandle,
MouseButton, MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext,
ViewHandle, WeakViewHandle,
elements::{ChildView, Flex, Label, ParentElement},
serde_json, AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle,
MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
};
use isahc::Request;
use language::Buffer;
@@ -21,21 +20,20 @@ use postage::prelude::Stream;
use project::Project;
use serde::Serialize;
use settings::Settings;
use util::ResultExt;
use workspace::{
item::{Item, ItemHandle},
searchable::{SearchableItem, SearchableItemHandle},
AppState, StatusItemView, Workspace,
AppState, Workspace,
};
use crate::system_specs::SystemSpecs;
use crate::{submit_feedback_button::SubmitFeedbackButton, system_specs::SystemSpecs};
const FEEDBACK_CHAR_LIMIT: RangeInclusive<usize> = 10..=5000;
const FEEDBACK_PLACEHOLDER_TEXT: &str = "Thanks for spending time with Zed. Enter your feedback here as Markdown. Save the tab to submit your feedback.";
const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
"Feedback failed to submit, see error log for details.";
actions!(feedback, [SubmitFeedback, GiveFeedback, DeployFeedback]);
actions!(feedback, [GiveFeedback, SubmitFeedback]);
pub fn init(system_specs: SystemSpecs, app_state: Arc<AppState>, cx: &mut MutableAppContext) {
cx.add_action({
@@ -43,42 +41,16 @@ pub fn init(system_specs: SystemSpecs, app_state: Arc<AppState>, cx: &mut Mutabl
FeedbackEditor::deploy(system_specs.clone(), workspace, app_state.clone(), cx);
}
});
}
pub struct FeedbackButton;
impl Entity for FeedbackButton {
type Event = ();
}
impl View for FeedbackButton {
fn ui_name() -> &'static str {
"FeedbackButton"
}
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
Stack::new()
.with_child(
MouseEventHandler::<Self>::new(0, cx, |state, cx| {
let theme = &cx.global::<Settings>().theme;
let theme = &theme.workspace.status_bar.feedback;
Text::new(
"Give Feedback".to_string(),
theme.style_for(state, true).clone(),
)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(GiveFeedback))
.boxed(),
)
.boxed()
}
}
impl StatusItemView for FeedbackButton {
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
cx.add_async_action(
|submit_feedback_button: &mut SubmitFeedbackButton, _: &SubmitFeedback, cx| {
if let Some(active_item) = submit_feedback_button.active_item.as_ref() {
Some(active_item.update(cx, |feedback_editor, cx| feedback_editor.handle_save(cx)))
} else {
None
}
},
);
}
#[derive(Serialize)]
@@ -86,11 +58,12 @@ struct FeedbackRequestBody<'a> {
feedback_text: &'a str,
metrics_id: Option<Arc<str>>,
system_specs: SystemSpecs,
is_staff: bool,
token: &'a str,
}
#[derive(Clone)]
struct FeedbackEditor {
pub(crate) struct FeedbackEditor {
system_specs: SystemSpecs,
editor: ViewHandle<Editor>,
project: ModelHandle<Project>,
@@ -106,7 +79,6 @@ impl FeedbackEditor {
let editor = cx.add_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
editor.set_vertical_scroll_margin(5, cx);
editor.set_placeholder_text(FEEDBACK_PLACEHOLDER_TEXT, cx);
editor
});
@@ -120,12 +92,10 @@ impl FeedbackEditor {
}
}
fn handle_save(
&mut self,
_: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
let feedback_char_count = self.editor.read(cx).text(cx).chars().count();
fn handle_save(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
let feedback_text = self.editor.read(cx).text(cx);
let feedback_char_count = feedback_text.chars().count();
let feedback_text = feedback_text.trim().to_string();
let error = if feedback_char_count < *FEEDBACK_CHAR_LIMIT.start() {
Some(format!(
@@ -154,7 +124,6 @@ impl FeedbackEditor {
let this = cx.handle();
let client = cx.global::<Arc<Client>>().clone();
let feedback_text = self.editor.read(cx).text(cx);
let specs = self.system_specs.clone();
cx.spawn(|_, mut cx| async move {
@@ -198,12 +167,14 @@ impl FeedbackEditor {
let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL);
let metrics_id = zed_client.metrics_id();
let is_staff = zed_client.is_staff();
let http_client = zed_client.http_client();
let request = FeedbackRequestBody {
feedback_text: &feedback_text,
metrics_id,
system_specs,
is_staff: is_staff.unwrap_or(false),
token: ZED_SECRET_CLIENT_TOKEN,
};
@@ -230,24 +201,28 @@ impl FeedbackEditor {
impl FeedbackEditor {
pub fn deploy(
system_specs: SystemSpecs,
workspace: &mut Workspace,
_: &mut Workspace,
app_state: Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) {
workspace
.with_local_workspace(&app_state, cx, |workspace, cx| {
let project = workspace.project().clone();
let markdown_language = project.read(cx).languages().language_for_name("Markdown");
let buffer = project
.update(cx, |project, cx| {
project.create_buffer("", markdown_language, cx)
let markdown = app_state.languages.language_for_name("Markdown");
cx.spawn(|workspace, mut cx| async move {
let markdown = markdown.await.log_err();
workspace
.update(&mut cx, |workspace, cx| {
workspace.with_local_workspace(&app_state, cx, |workspace, cx| {
let project = workspace.project().clone();
let buffer = project
.update(cx, |project, cx| project.create_buffer("", markdown, cx))
.expect("creating buffers on a local workspace always succeeds");
let feedback_editor = cx
.add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx));
workspace.add_item(Box::new(feedback_editor), cx);
})
.expect("creating buffers on a local workspace always succeeds");
let feedback_editor =
cx.add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx));
workspace.add_item(Box::new(feedback_editor), cx);
})
.detach();
})
.await;
})
.detach();
}
}
@@ -275,7 +250,7 @@ impl Item for FeedbackEditor {
fn tab_content(&self, _: Option<usize>, style: &theme::Tab, _: &AppContext) -> ElementBox {
Flex::row()
.with_child(
Label::new("Feedback".to_string(), style.label.clone())
Label::new("Feedback", style.label.clone())
.aligned()
.contained()
.boxed(),
@@ -287,35 +262,29 @@ impl Item for FeedbackEditor {
self.editor.for_each_project_item(cx, f)
}
fn to_item_events(_: &Self::Event) -> Vec<workspace::item::ItemEvent> {
Vec::new()
}
fn is_singleton(&self, _: &AppContext) -> bool {
true
}
fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
fn can_save(&self, _: &AppContext) -> bool {
true
}
fn save(
&mut self,
project: ModelHandle<Project>,
_: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
self.handle_save(project, cx)
self.handle_save(cx)
}
fn save_as(
&mut self,
project: ModelHandle<Project>,
_: ModelHandle<Project>,
_: std::path::PathBuf,
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
self.handle_save(project, cx)
self.handle_save(cx)
}
fn reload(
@@ -323,7 +292,7 @@ impl Item for FeedbackEditor {
_: ModelHandle<Project>,
_: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
unreachable!("reload should not have been called")
Task::Ready(Some(Ok(())))
}
fn clone_on_split(
@@ -350,20 +319,6 @@ impl Item for FeedbackEditor {
))
}
fn serialized_item_kind() -> Option<&'static str> {
None
}
fn deserialize(
_: ModelHandle<Project>,
_: WeakViewHandle<Workspace>,
_: workspace::WorkspaceId,
_: workspace::ItemId,
_: &mut ViewContext<workspace::Pane>,
) -> Task<anyhow::Result<ViewHandle<Self>>> {
unreachable!()
}
fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(handle.clone()))
}

View File

@@ -0,0 +1,97 @@
use gpui::{
elements::{Flex, Label, MouseEventHandler, ParentElement, Text},
CursorStyle, Element, ElementBox, Entity, MouseButton, RenderContext, View, ViewContext,
ViewHandle,
};
use settings::Settings;
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
use crate::{feedback_editor::FeedbackEditor, OpenZedCommunityRepo};
pub struct FeedbackInfoText {
active_item: Option<ViewHandle<FeedbackEditor>>,
}
impl FeedbackInfoText {
pub fn new() -> Self {
Self {
active_item: Default::default(),
}
}
}
impl Entity for FeedbackInfoText {
type Event = ();
}
impl View for FeedbackInfoText {
fn ui_name() -> &'static str {
"FeedbackInfoText"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone();
Flex::row()
.with_child(
Text::new(
"We read whatever you submit here. For issues and discussions, visit the ",
theme.feedback.info_text_default.text.clone(),
)
.with_soft_wrap(false)
.aligned()
.boxed(),
)
.with_child(
MouseEventHandler::<OpenZedCommunityRepo>::new(0, cx, |state, _| {
let contained_text = if state.hovered() {
&theme.feedback.link_text_hover
} else {
&theme.feedback.link_text_default
};
Label::new("community repo", contained_text.text.clone())
.contained()
.aligned()
.left()
.clipped()
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(OpenZedCommunityRepo)
})
.boxed(),
)
.with_child(
Text::new(" on GitHub.", theme.feedback.info_text_default.text.clone())
.with_soft_wrap(false)
.aligned()
.boxed(),
)
.aligned()
.left()
.clipped()
.boxed()
}
}
impl ToolbarItemView for FeedbackInfoText {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) -> workspace::ToolbarItemLocation {
cx.notify();
if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::<FeedbackEditor>())
{
self.active_item = Some(feedback_editor);
ToolbarItemLocation::PrimaryLeft {
flex: Some((1., false)),
}
} else {
self.active_item = None;
ToolbarItemLocation::Hidden
}
}
}

View File

@@ -0,0 +1,76 @@
use gpui::{
elements::{Label, MouseEventHandler},
CursorStyle, Element, ElementBox, Entity, MouseButton, RenderContext, View, ViewContext,
ViewHandle,
};
use settings::Settings;
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
use crate::feedback_editor::{FeedbackEditor, SubmitFeedback};
pub struct SubmitFeedbackButton {
pub(crate) active_item: Option<ViewHandle<FeedbackEditor>>,
}
impl SubmitFeedbackButton {
pub fn new() -> Self {
Self {
active_item: Default::default(),
}
}
}
impl Entity for SubmitFeedbackButton {
type Event = ();
}
impl View for SubmitFeedbackButton {
fn ui_name() -> &'static str {
"SubmitFeedbackButton"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone();
enum SubmitFeedbackButton {}
MouseEventHandler::<SubmitFeedbackButton>::new(0, cx, |state, _| {
let style = theme.feedback.submit_button.style_for(state, false);
Label::new("Submit as Markdown", style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(SubmitFeedback)
})
.aligned()
.contained()
.with_margin_left(theme.feedback.button_margin)
.with_tooltip::<Self, _>(
0,
"cmd-s".into(),
Some(Box::new(SubmitFeedback)),
theme.tooltip.clone(),
cx,
)
.boxed()
}
}
impl ToolbarItemView for SubmitFeedbackButton {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) -> workspace::ToolbarItemLocation {
cx.notify();
if let Some(feedback_editor) = active_pane_item.and_then(|i| i.downcast::<FeedbackEditor>())
{
self.active_item = Some(feedback_editor);
ToolbarItemLocation::PrimaryRight { flex: None }
} else {
self.active_item = None;
ToolbarItemLocation::Hidden
}
}
}

View File

@@ -23,6 +23,7 @@ pub struct FileFinder {
latest_search_id: usize,
latest_search_did_cancel: bool,
latest_search_query: String,
relative_to: Option<Arc<Path>>,
matches: Vec<PathMatch>,
selected: Option<(usize, Arc<Path>)>,
cancel_flag: Arc<AtomicBool>,
@@ -90,7 +91,11 @@ impl FileFinder {
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |workspace, cx| {
let project = workspace.project().clone();
let finder = cx.add_view(|cx| Self::new(project, cx));
let relative_to = workspace
.active_item(cx)
.and_then(|item| item.project_path(cx))
.map(|project_path| project_path.path.clone());
let finder = cx.add_view(|cx| Self::new(project, relative_to, cx));
cx.subscribe(&finder, Self::on_event).detach();
finder
});
@@ -115,7 +120,11 @@ impl FileFinder {
}
}
pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
pub fn new(
project: ModelHandle<Project>,
relative_to: Option<Arc<Path>>,
cx: &mut ViewContext<Self>,
) -> Self {
let handle = cx.weak_handle();
cx.observe(&project, Self::project_updated).detach();
Self {
@@ -125,6 +134,7 @@ impl FileFinder {
latest_search_id: 0,
latest_search_did_cancel: false,
latest_search_query: String::new(),
relative_to,
matches: Vec::new(),
selected: None,
cancel_flag: Arc::new(AtomicBool::new(false)),
@@ -137,6 +147,7 @@ impl FileFinder {
}
fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
let relative_to = self.relative_to.clone();
let worktrees = self
.project
.read(cx)
@@ -165,6 +176,7 @@ impl FileFinder {
let matches = fuzzy::match_path_sets(
candidate_sets.as_slice(),
&query,
relative_to,
false,
100,
&cancel_flag,
@@ -317,9 +329,7 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| {
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
});
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
cx.dispatch_action(window_id, Toggle);
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
@@ -373,11 +383,9 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| {
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
});
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
let query = "hi".to_string();
finder
@@ -449,11 +457,9 @@ mod tests {
cx,
)
.await;
let (_, workspace) = cx.add_window(|cx| {
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
});
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
finder
.update(cx, |f, cx| f.spawn_search("hi".into(), cx))
.await;
@@ -475,11 +481,9 @@ mod tests {
cx,
)
.await;
let (_, workspace) = cx.add_window(|cx| {
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
});
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
// Even though there is only one worktree, that worktree's filename
// is included in the matching, because the worktree is a single file.
@@ -529,11 +533,10 @@ mod tests {
cx,
)
.await;
let (_, workspace) = cx.add_window(|cx| {
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
});
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
// Run a search that matches two files with the same relative path.
finder
@@ -551,6 +554,46 @@ mod tests {
});
}
#[gpui::test]
async fn test_path_distance_ordering(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking();
let app_state = cx.update(AppState::test);
app_state
.fs
.as_fake()
.insert_tree(
"/root",
json!({
"dir1": { "a.txt": "" },
"dir2": {
"a.txt": "",
"b.txt": ""
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
// When workspace has an active item, sort items which are closer to that item
// first when they have the same name. In this case, b.txt is closer to dir2's a.txt
// so that one should be sorted earlier
let b_path = Some(Arc::from(Path::new("/root/dir2/b.txt")));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), b_path, cx));
finder
.update(cx, |f, cx| f.spawn_search("a.txt".into(), cx))
.await;
finder.read_with(cx, |f, _| {
assert_eq!(f.matches[0].path.as_ref(), Path::new("dir2/a.txt"));
assert_eq!(f.matches[1].path.as_ref(), Path::new("dir1/a.txt"));
});
}
#[gpui::test]
async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) {
let app_state = cx.update(AppState::test);
@@ -569,11 +612,9 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| {
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
});
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
finder
.update(cx, |f, cx| f.spawn_search("dir".into(), cx))
.await;

View File

@@ -443,6 +443,7 @@ mod tests {
positions: Vec::new(),
path: candidate.path.clone(),
path_prefix: "".into(),
distance_to_relative_ancestor: usize::MAX,
},
);

View File

@@ -25,6 +25,9 @@ pub struct PathMatch {
pub worktree_id: usize,
pub path: Arc<Path>,
pub path_prefix: Arc<str>,
/// Number of steps removed from a shared parent with the relative path
/// Used to order closer paths first in the search list
pub distance_to_relative_ancestor: usize,
}
pub trait PathMatchCandidateSet<'a>: Send + Sync {
@@ -78,6 +81,11 @@ impl Ord for PathMatch {
.partial_cmp(&other.score)
.unwrap_or(Ordering::Equal)
.then_with(|| self.worktree_id.cmp(&other.worktree_id))
.then_with(|| {
other
.distance_to_relative_ancestor
.cmp(&self.distance_to_relative_ancestor)
})
.then_with(|| self.path.cmp(&other.path))
}
}
@@ -85,6 +93,7 @@ impl Ord for PathMatch {
pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
candidate_sets: &'a [Set],
query: &str,
relative_to: Option<Arc<Path>>,
smart_case: bool,
max_results: usize,
cancel_flag: &AtomicBool,
@@ -111,6 +120,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
background
.scoped(|scope| {
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
let relative_to = relative_to.clone();
scope.spawn(async move {
let segment_start = segment_idx * segment_size;
let segment_end = segment_start + segment_size;
@@ -149,6 +159,15 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
positions: Vec::new(),
path: candidate.path.clone(),
path_prefix: candidate_set.prefix(),
distance_to_relative_ancestor: relative_to.as_ref().map_or(
usize::MAX,
|relative_to| {
distance_between_paths(
candidate.path.as_ref(),
relative_to.as_ref(),
)
},
),
},
);
}
@@ -172,3 +191,30 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
}
results
}
/// Compute the distance from a given path to some other path
/// If there is no shared path, returns usize::MAX
fn distance_between_paths(path: &Path, relative_to: &Path) -> usize {
let mut path_components = path.components();
let mut relative_components = relative_to.components();
while path_components
.next()
.zip(relative_components.next())
.map(|(path_component, relative_component)| path_component == relative_component)
.unwrap_or_default()
{}
path_components.count() + relative_components.count() + 1
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::distance_between_paths;
#[test]
fn test_distance_between_paths_empty() {
distance_between_paths(Path::new(""), Path::new(""));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,14 @@ pub trait Action: 'static {
Self: Sized;
}
impl std::fmt::Debug for dyn Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("dyn Action")
.field("namespace", &self.namespace())
.field("name", &self.name())
.finish()
}
}
/// Define a set of unit struct types that all implement the `Action` trait.
///
/// The first argument is a namespace that will be associated with each of

View File

@@ -0,0 +1,89 @@
use crate::{Action, App, ForegroundPlatform, MutableAppContext};
pub struct Menu<'a> {
pub name: &'a str,
pub items: Vec<MenuItem<'a>>,
}
pub enum MenuItem<'a> {
Separator,
Submenu(Menu<'a>),
Action {
name: &'a str,
action: Box<dyn Action>,
os_action: Option<OsAction>,
},
}
impl<'a> MenuItem<'a> {
pub fn separator() -> Self {
Self::Separator
}
pub fn submenu(menu: Menu<'a>) -> Self {
Self::Submenu(menu)
}
pub fn action(name: &'a str, action: impl Action) -> Self {
Self::Action {
name,
action: Box::new(action),
os_action: None,
}
}
pub fn os_action(name: &'a str, action: impl Action, os_action: OsAction) -> Self {
Self::Action {
name,
action: Box::new(action),
os_action: Some(os_action),
}
}
}
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum OsAction {
Cut,
Copy,
Paste,
SelectAll,
Undo,
Redo,
}
impl MutableAppContext {
pub fn set_menus(&mut self, menus: Vec<Menu>) {
self.foreground_platform
.set_menus(menus, &self.keystroke_matcher);
}
}
pub(crate) fn setup_menu_handlers(foreground_platform: &dyn ForegroundPlatform, app: &App) {
foreground_platform.on_will_open_menu(Box::new({
let cx = app.0.clone();
move || {
let mut cx = cx.borrow_mut();
cx.keystroke_matcher.clear_pending();
}
}));
foreground_platform.on_validate_menu_command(Box::new({
let cx = app.0.clone();
move |action| {
let cx = cx.borrow_mut();
!cx.keystroke_matcher.has_pending_keystrokes() && cx.is_action_available(action)
}
}));
foreground_platform.on_menu_command(Box::new({
let cx = app.0.clone();
move |action| {
let mut cx = cx.borrow_mut();
if let Some(main_window_id) = cx.cx.platform.main_window_id() {
if let Some(view_id) = cx.focused_view_id(main_window_id) {
cx.handle_dispatch_action_from_effect(main_window_id, Some(view_id), action);
return;
}
}
cx.dispatch_global_action_any(action);
}
}));
}

View File

@@ -0,0 +1,220 @@
#[cfg(any(test, feature = "test-support"))]
use std::sync::Arc;
use lazy_static::lazy_static;
#[cfg(any(test, feature = "test-support"))]
use parking_lot::Mutex;
use collections::{hash_map::Entry, HashMap, HashSet};
#[cfg(any(test, feature = "test-support"))]
use crate::util::post_inc;
use crate::ElementStateId;
lazy_static! {
static ref LEAK_BACKTRACE: bool =
std::env::var("LEAK_BACKTRACE").map_or(false, |b| !b.is_empty());
}
struct ElementStateRefCount {
ref_count: usize,
frame_id: usize,
}
#[derive(Default)]
pub struct RefCounts {
entity_counts: HashMap<usize, usize>,
element_state_counts: HashMap<ElementStateId, ElementStateRefCount>,
dropped_models: HashSet<usize>,
dropped_views: HashSet<(usize, usize)>,
dropped_element_states: HashSet<ElementStateId>,
#[cfg(any(test, feature = "test-support"))]
pub leak_detector: Arc<Mutex<LeakDetector>>,
}
impl RefCounts {
#[cfg(any(test, feature = "test-support"))]
pub fn new(leak_detector: Arc<Mutex<LeakDetector>>) -> Self {
Self {
#[cfg(any(test, feature = "test-support"))]
leak_detector,
..Default::default()
}
}
pub fn inc_model(&mut self, model_id: usize) {
match self.entity_counts.entry(model_id) {
Entry::Occupied(mut entry) => {
*entry.get_mut() += 1;
}
Entry::Vacant(entry) => {
entry.insert(1);
self.dropped_models.remove(&model_id);
}
}
}
pub fn inc_view(&mut self, window_id: usize, view_id: usize) {
match self.entity_counts.entry(view_id) {
Entry::Occupied(mut entry) => *entry.get_mut() += 1,
Entry::Vacant(entry) => {
entry.insert(1);
self.dropped_views.remove(&(window_id, view_id));
}
}
}
pub fn inc_element_state(&mut self, id: ElementStateId, frame_id: usize) {
match self.element_state_counts.entry(id) {
Entry::Occupied(mut entry) => {
let entry = entry.get_mut();
if entry.frame_id == frame_id || entry.ref_count >= 2 {
panic!("used the same element state more than once in the same frame");
}
entry.ref_count += 1;
entry.frame_id = frame_id;
}
Entry::Vacant(entry) => {
entry.insert(ElementStateRefCount {
ref_count: 1,
frame_id,
});
self.dropped_element_states.remove(&id);
}
}
}
pub fn dec_model(&mut self, model_id: usize) {
let count = self.entity_counts.get_mut(&model_id).unwrap();
*count -= 1;
if *count == 0 {
self.entity_counts.remove(&model_id);
self.dropped_models.insert(model_id);
}
}
pub fn dec_view(&mut self, window_id: usize, view_id: usize) {
let count = self.entity_counts.get_mut(&view_id).unwrap();
*count -= 1;
if *count == 0 {
self.entity_counts.remove(&view_id);
self.dropped_views.insert((window_id, view_id));
}
}
pub fn dec_element_state(&mut self, id: ElementStateId) {
let entry = self.element_state_counts.get_mut(&id).unwrap();
entry.ref_count -= 1;
if entry.ref_count == 0 {
self.element_state_counts.remove(&id);
self.dropped_element_states.insert(id);
}
}
pub fn is_entity_alive(&self, entity_id: usize) -> bool {
self.entity_counts.contains_key(&entity_id)
}
pub fn take_dropped(
&mut self,
) -> (
HashSet<usize>,
HashSet<(usize, usize)>,
HashSet<ElementStateId>,
) {
(
std::mem::take(&mut self.dropped_models),
std::mem::take(&mut self.dropped_views),
std::mem::take(&mut self.dropped_element_states),
)
}
}
#[cfg(any(test, feature = "test-support"))]
#[derive(Default)]
pub struct LeakDetector {
next_handle_id: usize,
#[allow(clippy::type_complexity)]
handle_backtraces: HashMap<
usize,
(
Option<&'static str>,
HashMap<usize, Option<backtrace::Backtrace>>,
),
>,
}
#[cfg(any(test, feature = "test-support"))]
impl LeakDetector {
pub fn handle_created(&mut self, type_name: Option<&'static str>, entity_id: usize) -> usize {
let handle_id = post_inc(&mut self.next_handle_id);
let entry = self.handle_backtraces.entry(entity_id).or_default();
let backtrace = if *LEAK_BACKTRACE {
Some(backtrace::Backtrace::new_unresolved())
} else {
None
};
if let Some(type_name) = type_name {
entry.0.get_or_insert(type_name);
}
entry.1.insert(handle_id, backtrace);
handle_id
}
pub fn handle_dropped(&mut self, entity_id: usize, handle_id: usize) {
if let Some((_, backtraces)) = self.handle_backtraces.get_mut(&entity_id) {
assert!(backtraces.remove(&handle_id).is_some());
if backtraces.is_empty() {
self.handle_backtraces.remove(&entity_id);
}
}
}
pub fn assert_dropped(&mut self, entity_id: usize) {
if let Some((type_name, backtraces)) = self.handle_backtraces.get_mut(&entity_id) {
for trace in backtraces.values_mut().flatten() {
trace.resolve();
eprintln!("{:?}", crate::util::CwdBacktrace(trace));
}
let hint = if *LEAK_BACKTRACE {
""
} else {
" set LEAK_BACKTRACE=1 for more information"
};
panic!(
"{} handles to {} {} still exist{}",
backtraces.len(),
type_name.unwrap_or("entity"),
entity_id,
hint
);
}
}
pub fn detect(&mut self) {
let mut found_leaks = false;
for (id, (type_name, backtraces)) in self.handle_backtraces.iter_mut() {
eprintln!(
"leaked {} handles to {} {}",
backtraces.len(),
type_name.unwrap_or("entity"),
id
);
for trace in backtraces.values_mut().flatten() {
trace.resolve();
eprintln!("{:?}", crate::util::CwdBacktrace(trace));
}
found_leaks = true;
}
let hint = if *LEAK_BACKTRACE {
""
} else {
" set LEAK_BACKTRACE=1 for more information"
};
assert!(!found_leaks, "detected leaked handles{}", hint);
}
}

View File

@@ -18,14 +18,16 @@ use smol::stream::StreamExt;
use crate::{
executor, geometry::vector::Vector2F, keymap_matcher::Keystroke, platform, Action,
AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent,
LeakDetector, ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith,
AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, Handle, InputHandler,
KeyDownEvent, ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith,
ReadViewWith, RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle,
WeakHandle, WindowInputHandler,
WeakHandle,
};
use collections::BTreeMap;
use super::{AsyncAppContext, RefCounts};
use super::{
ref_counts::LeakDetector, window_input_handler::WindowInputHandler, AsyncAppContext, RefCounts,
};
pub struct TestAppContext {
cx: Rc<RefCell<MutableAppContext>>,
@@ -52,11 +54,7 @@ impl TestAppContext {
platform,
foreground_platform.clone(),
font_cache,
RefCounts {
#[cfg(any(test, feature = "test-support"))]
leak_detector,
..Default::default()
},
RefCounts::new(leak_detector),
(),
);
cx.next_entity_id = first_entity_id;
@@ -332,6 +330,14 @@ impl TestAppContext {
.assert_dropped(handle.id())
}
/// Drop a handle, assuming it is the last. If it is not the last, panic with debug information about
/// where the stray handles were created.
pub fn drop_last<T, W: WeakHandle, H: Handle<T, Weak = W>>(&mut self, handle: H) {
let weak = handle.downgrade();
self.update(|_| drop(handle));
self.assert_dropped(weak);
}
fn window_mut(&self, window_id: usize) -> std::cell::RefMut<platform::test::Window> {
std::cell::RefMut::map(self.cx.borrow_mut(), |state| {
let (_, window) = state
@@ -624,6 +630,8 @@ impl<T: View> ViewHandle<T> {
}
}
/// Tracks string context to be printed when assertions fail.
/// Often this is done by storing a context string in the manager and returning the handle.
#[derive(Clone)]
pub struct AssertionContextManager {
id: Arc<AtomicUsize>,
@@ -654,6 +662,9 @@ impl AssertionContextManager {
}
}
/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails.
/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails,
/// the state that was set initially for the failure can be printed in the error message
pub struct ContextHandle {
id: usize,
manager: AssertionContextManager,

View File

@@ -0,0 +1,98 @@
use std::{cell::RefCell, ops::Range, rc::Rc};
use pathfinder_geometry::rect::RectF;
use crate::{AnyView, AppContext, InputHandler, MutableAppContext};
pub struct WindowInputHandler {
pub app: Rc<RefCell<MutableAppContext>>,
pub window_id: usize,
}
impl WindowInputHandler {
fn read_focused_view<T, F>(&self, f: F) -> Option<T>
where
F: FnOnce(&dyn AnyView, &AppContext) -> T,
{
// Input-related application hooks are sometimes called by the OS during
// a call to a window-manipulation API, like prompting the user for file
// paths. In that case, the AppContext will already be borrowed, so any
// InputHandler methods need to fail gracefully.
//
// See https://github.com/zed-industries/community/issues/444
let app = self.app.try_borrow().ok()?;
let view_id = app.focused_view_id(self.window_id)?;
let view = app.cx.views.get(&(self.window_id, view_id))?;
let result = f(view.as_ref(), &app);
Some(result)
}
fn update_focused_view<T, F>(&mut self, f: F) -> Option<T>
where
F: FnOnce(usize, usize, &mut dyn AnyView, &mut MutableAppContext) -> T,
{
let mut app = self.app.try_borrow_mut().ok()?;
app.update(|app| {
let view_id = app.focused_view_id(self.window_id)?;
let mut view = app.cx.views.remove(&(self.window_id, view_id))?;
let result = f(self.window_id, view_id, view.as_mut(), &mut *app);
app.cx.views.insert((self.window_id, view_id), view);
Some(result)
})
}
}
impl InputHandler for WindowInputHandler {
fn text_for_range(&self, range: Range<usize>) -> Option<String> {
self.read_focused_view(|view, cx| view.text_for_range(range.clone(), cx))
.flatten()
}
fn selected_text_range(&self) -> Option<Range<usize>> {
self.read_focused_view(|view, cx| view.selected_text_range(cx))
.flatten()
}
fn replace_text_in_range(&mut self, range: Option<Range<usize>>, text: &str) {
self.update_focused_view(|window_id, view_id, view, cx| {
view.replace_text_in_range(range, text, cx, window_id, view_id);
});
}
fn marked_text_range(&self) -> Option<Range<usize>> {
self.read_focused_view(|view, cx| view.marked_text_range(cx))
.flatten()
}
fn unmark_text(&mut self) {
self.update_focused_view(|window_id, view_id, view, cx| {
view.unmark_text(cx, window_id, view_id);
});
}
fn replace_and_mark_text_in_range(
&mut self,
range: Option<Range<usize>>,
new_text: &str,
new_selected_range: Option<Range<usize>>,
) {
self.update_focused_view(|window_id, view_id, view, cx| {
view.replace_and_mark_text_in_range(
range,
new_text,
new_selected_range,
cx,
window_id,
view_id,
);
});
}
fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF> {
let app = self.app.borrow();
let (presenter, _) = app.presenters_and_platform_windows.get(&self.window_id)?;
let presenter = presenter.borrow();
presenter.rect_for_text_range(range_utf16, &app)
}
}

View File

@@ -1,5 +1,8 @@
use anyhow::{anyhow, Result};
use std::{borrow::Cow, cell::RefCell, collections::HashMap};
use image::ImageFormat;
use std::{borrow::Cow, cell::RefCell, collections::HashMap, sync::Arc};
use crate::ImageData;
pub trait AssetSource: 'static + Send + Sync {
fn load(&self, path: &str) -> Result<Cow<[u8]>>;
@@ -22,6 +25,7 @@ impl AssetSource for () {
pub struct AssetCache {
source: Box<dyn AssetSource>,
svgs: RefCell<HashMap<String, usvg::Tree>>,
pngs: RefCell<HashMap<String, Arc<ImageData>>>,
}
impl AssetCache {
@@ -29,6 +33,7 @@ impl AssetCache {
Self {
source: Box::new(source),
svgs: RefCell::new(HashMap::new()),
pngs: RefCell::new(HashMap::new()),
}
}
@@ -43,4 +48,18 @@ impl AssetCache {
Ok(svg)
}
}
pub fn png(&self, path: &str) -> Result<Arc<ImageData>> {
let mut pngs = self.pngs.borrow_mut();
if let Some(png) = pngs.get(path) {
Ok(png.clone())
} else {
let bytes = self.source.load(path)?;
let image = ImageData::new(
image::load_from_memory_with_format(&bytes, ImageFormat::Png)?.into_bgra8(),
);
pngs.insert(path.to_string(), image.clone());
Ok(image)
}
}
}

View File

@@ -1,5 +1,6 @@
mod align;
mod canvas;
mod clipped;
mod constrained_box;
mod container;
mod empty;
@@ -19,12 +20,12 @@ mod text;
mod tooltip;
mod uniform_list;
use self::expanded::Expanded;
pub use self::{
align::*, canvas::*, constrained_box::*, container::*, empty::*, flex::*, hook::*, image::*,
keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*, resizable::*,
stack::*, svg::*, text::*, tooltip::*, uniform_list::*,
};
use self::{clipped::Clipped, expanded::Expanded};
pub use crate::presenter::ChildView;
use crate::{
geometry::{
@@ -135,6 +136,13 @@ pub trait Element {
Align::new(self.boxed())
}
fn clipped(self) -> Clipped
where
Self: 'static + Sized,
{
Clipped::new(self.boxed())
}
fn contained(self) -> Container
where
Self: 'static + Sized,
@@ -288,7 +296,10 @@ impl<T: Element> AnyElement for Lifecycle<T> {
paint,
}
}
_ => panic!("invalid element lifecycle state"),
Lifecycle::Empty => panic!("invalid element lifecycle state"),
Lifecycle::Init { .. } => {
panic!("invalid element lifecycle state, paint called before layout")
}
}
}
@@ -355,6 +366,7 @@ impl<T: Element> AnyElement for Lifecycle<T> {
value
}
}
_ => panic!("invalid element lifecycle state"),
}
}

View File

@@ -0,0 +1,69 @@
use std::ops::Range;
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
use serde_json::json;
use crate::{
json, DebugContext, Element, ElementBox, LayoutContext, MeasurementContext, PaintContext,
SizeConstraint,
};
pub struct Clipped {
child: ElementBox,
}
impl Clipped {
pub fn new(child: ElementBox) -> Self {
Self { child }
}
}
impl Element for Clipped {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
(self.child.layout(constraint, cx), ())
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
cx.scene.push_layer(Some(bounds));
self.child.paint(bounds.origin(), visible_bounds, cx);
cx.scene.pop_layer();
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &MeasurementContext,
) -> Option<RectF> {
self.child.rect_for_text_range(range_utf16, cx)
}
fn debug(
&self,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
) -> json::Value {
json!({
"type": "Clipped",
"child": self.child.debug(cx)
})
}
}

View File

@@ -153,7 +153,9 @@ impl Element for ConstrainedBox {
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
self.child.paint(bounds.origin(), visible_bounds, cx);
cx.paint_layer(Some(visible_bounds), |cx| {
self.child.paint(bounds.origin(), visible_bounds, cx);
})
}
fn rect_for_text_range(

View File

@@ -22,6 +22,7 @@ pub struct Flex {
axis: Axis,
children: Vec<ElementBox>,
scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
child_alignment: f32,
}
impl Flex {
@@ -30,6 +31,7 @@ impl Flex {
axis,
children: Default::default(),
scroll_state: None,
child_alignment: -1.,
}
}
@@ -41,6 +43,15 @@ impl Flex {
Self::new(Axis::Vertical)
}
/// Render children centered relative to the cross-axis of the parent flex.
///
/// If this is a flex row, children will be centered vertically. If this is a
/// flex column, children will be centered horizontally.
pub fn align_children_center(mut self) -> Self {
self.child_alignment = 0.;
self
}
pub fn scrollable<Tag, V>(
mut self,
element_id: usize,
@@ -308,7 +319,32 @@ impl Element for Flex {
}
}
}
child.paint(child_origin, visible_bounds, cx);
// We use the child_alignment f32 to determine a point along the cross axis of the
// overall flex element and each child. We then align these points. So 0 would center
// each child relative to the overall height/width of the flex. -1 puts children at
// the start. 1 puts children at the end.
let aligned_child_origin = {
let cross_axis = self.axis.invert();
let my_center = bounds.size().along(cross_axis) / 2.;
let my_target = my_center + my_center * self.child_alignment;
let child_center = child.size().along(cross_axis) / 2.;
let child_target = child_center + child_center * self.child_alignment;
let mut aligned_child_origin = child_origin;
match self.axis {
Axis::Horizontal => aligned_child_origin
.set_y(aligned_child_origin.y() - (child_target - my_target)),
Axis::Vertical => aligned_child_origin
.set_x(aligned_child_origin.x() - (child_target - my_target)),
}
aligned_child_origin
};
child.paint(aligned_child_origin, visible_bounds, cx);
match self.axis {
Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),

View File

@@ -11,8 +11,13 @@ use crate::{
use serde::Deserialize;
use std::{ops::Range, sync::Arc};
enum ImageSource {
Path(&'static str),
Data(Arc<ImageData>),
}
pub struct Image {
data: Arc<ImageData>,
source: ImageSource,
style: ImageStyle,
}
@@ -31,9 +36,16 @@ pub struct ImageStyle {
}
impl Image {
pub fn new(data: Arc<ImageData>) -> Self {
pub fn new(asset_path: &'static str) -> Self {
Self {
data,
source: ImageSource::Path(asset_path),
style: Default::default(),
}
}
pub fn from_data(data: Arc<ImageData>) -> Self {
Self {
source: ImageSource::Data(data),
style: Default::default(),
}
}
@@ -45,39 +57,53 @@ impl Image {
}
impl Element for Image {
type LayoutState = ();
type LayoutState = Option<Arc<ImageData>>;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
_: &mut LayoutContext,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let data = match &self.source {
ImageSource::Path(path) => match cx.asset_cache.png(path) {
Ok(data) => data,
Err(error) => {
log::error!("could not load image: {}", error);
return (Vector2F::zero(), None);
}
},
ImageSource::Data(data) => data.clone(),
};
let desired_size = vec2f(
self.style.width.unwrap_or_else(|| constraint.max.x()),
self.style.height.unwrap_or_else(|| constraint.max.y()),
);
let size = constrain_size_preserving_aspect_ratio(
constraint.constrain(desired_size),
self.data.size().to_f32(),
data.size().to_f32(),
);
(size, ())
(size, Some(data))
}
fn paint(
&mut self,
bounds: RectF,
_: RectF,
_: &mut Self::LayoutState,
layout: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
cx.scene.push_image(scene::Image {
bounds,
border: self.style.border,
corner_radius: self.style.corner_radius,
grayscale: self.style.grayscale,
data: self.data.clone(),
});
if let Some(data) = layout {
cx.scene.push_image(scene::Image {
bounds,
border: self.style.border,
corner_radius: self.style.corner_radius,
grayscale: self.style.grayscale,
data: data.clone(),
});
}
}
fn rect_for_text_range(

View File

@@ -12,15 +12,21 @@ pub struct KeystrokeLabel {
action: Box<dyn Action>,
container_style: ContainerStyle,
text_style: TextStyle,
window_id: usize,
view_id: usize,
}
impl KeystrokeLabel {
pub fn new(
window_id: usize,
view_id: usize,
action: Box<dyn Action>,
container_style: ContainerStyle,
text_style: TextStyle,
) -> Self {
Self {
window_id,
view_id,
action,
container_style,
text_style,
@@ -37,7 +43,10 @@ impl Element for KeystrokeLabel {
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, ElementBox) {
let mut element = if let Some(keystrokes) = cx.keystrokes_for_action(self.action.as_ref()) {
let mut element = if let Some(keystrokes) =
cx.app
.keystrokes_for_action(self.window_id, self.view_id, self.action.as_ref())
{
Flex::row()
.with_children(keystrokes.iter().map(|keystroke| {
Label::new(keystroke.to_string(), self.text_style.clone())

View File

@@ -1,4 +1,4 @@
use std::ops::Range;
use std::{borrow::Cow, ops::Range};
use crate::{
fonts::TextStyle,
@@ -16,7 +16,7 @@ use serde_json::json;
use smallvec::{smallvec, SmallVec};
pub struct Label {
text: String,
text: Cow<'static, str>,
style: LabelStyle,
highlight_indices: Vec<usize>,
}
@@ -44,9 +44,9 @@ impl LabelStyle {
}
impl Label {
pub fn new(text: String, style: impl Into<LabelStyle>) -> Self {
pub fn new<I: Into<Cow<'static, str>>>(text: I, style: impl Into<LabelStyle>) -> Self {
Self {
text,
text: text.into(),
highlight_indices: Default::default(),
style: style.into(),
}
@@ -138,11 +138,9 @@ impl Element for Label {
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let runs = self.compute_runs();
let line = cx.text_layout_cache.layout_str(
self.text.as_str(),
self.style.text.font_size,
runs.as_slice(),
);
let line =
cx.text_layout_cache
.layout_str(&self.text, self.style.text.font_size, runs.as_slice());
let size = vec2f(
line.width()

View File

@@ -15,7 +15,7 @@ use serde_json::json;
use std::{borrow::Cow, ops::Range, sync::Arc};
pub struct Text {
text: String,
text: Cow<'static, str>,
style: TextStyle,
soft_wrap: bool,
highlights: Vec<(Range<usize>, HighlightStyle)>,
@@ -28,9 +28,9 @@ pub struct LayoutState {
}
impl Text {
pub fn new(text: String, style: TextStyle) -> Self {
pub fn new<I: Into<Cow<'static, str>>>(text: I, style: TextStyle) -> Self {
Self {
text,
text: text.into(),
style,
soft_wrap: true,
highlights: Vec::new(),
@@ -280,7 +280,7 @@ mod tests {
let (window_id, _) = cx.add_window(Default::default(), |_| TestView);
let mut presenter = cx.build_presenter(window_id, Default::default(), Default::default());
fonts::with_font_cache(cx.font_cache().clone(), || {
let mut text = Text::new("Hello\r\n".into(), Default::default()).with_soft_wrap(true);
let mut text = Text::new("Hello\r\n", Default::default()).with_soft_wrap(true);
let (_, state) = text.layout(
SizeConstraint::new(Default::default(), vec2f(f32::INFINITY, f32::INFINITY)),
&mut presenter.build_layout_context(Default::default(), false, cx),

View File

@@ -61,11 +61,14 @@ impl Tooltip {
) -> Self {
struct ElementState<Tag>(Tag);
struct MouseEventHandlerState<Tag>(Tag);
let focused_view_id = cx.focused_view_id(cx.window_id);
let state_handle = cx.default_element_state::<ElementState<Tag>, Rc<TooltipState>>(id);
let state = state_handle.read(cx).clone();
let tooltip = if state.visible.get() {
let mut collapsed_tooltip = Self::render_tooltip(
cx.window_id,
focused_view_id,
text.clone(),
style.clone(),
action.as_ref().map(|a| a.boxed_clone()),
@@ -74,7 +77,7 @@ impl Tooltip {
.boxed();
Some(
Overlay::new(
Self::render_tooltip(text, style, action, false)
Self::render_tooltip(cx.window_id, focused_view_id, text, style, action, false)
.constrained()
.dynamically(move |constraint, cx| {
SizeConstraint::strict_along(
@@ -128,6 +131,8 @@ impl Tooltip {
}
pub fn render_tooltip(
window_id: usize,
focused_view_id: Option<usize>,
text: String,
style: TooltipStyle,
action: Option<Box<dyn Action>>,
@@ -144,13 +149,18 @@ impl Tooltip {
text.flex(1., false).aligned().boxed()
}
})
.with_children(action.map(|action| {
let keystroke_label =
KeystrokeLabel::new(action, style.keystroke.container, style.keystroke.text);
.with_children(action.and_then(|action| {
let keystroke_label = KeystrokeLabel::new(
window_id,
focused_view_id?,
action,
style.keystroke.container,
style.keystroke.text,
);
if measure {
keystroke_label.boxed()
Some(keystroke_label.boxed())
} else {
keystroke_label.aligned().boxed()
Some(keystroke_label.aligned().boxed())
}
}))
.contained()

View File

@@ -6,24 +6,15 @@ mod keystroke;
use std::{any::TypeId, fmt::Debug};
use collections::HashMap;
use serde::Deserialize;
use smallvec::SmallVec;
use crate::{impl_actions, Action};
use crate::Action;
pub use binding::{Binding, BindingMatchResult};
pub use keymap::Keymap;
pub use keymap_context::{KeymapContext, KeymapContextPredicate};
pub use keystroke::Keystroke;
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
pub struct KeyPressed {
#[serde(default)]
pub keystroke: Keystroke,
}
impl_actions!(gpui, [KeyPressed]);
pub struct KeymapMatcher {
pub contexts: Vec<KeymapContext>,
pending_views: HashMap<usize, KeymapContext>,
@@ -69,13 +60,27 @@ impl KeymapMatcher {
!self.pending_keystrokes.is_empty()
}
/// Pushes a keystroke onto the matcher.
/// The result of the new keystroke is returned:
/// MatchResult::None =>
/// No match is valid for this key given any pending keystrokes.
/// MatchResult::Pending =>
/// There exist bindings which are still waiting for more keys.
/// MatchResult::Complete(matches) =>
/// 1 or more bindings have recieved the necessary key presses.
/// The order of the matched actions is by position of the matching first,
// and order in the keymap second.
pub fn push_keystroke(
&mut self,
keystroke: Keystroke,
mut dispatch_path: Vec<(usize, KeymapContext)>,
) -> MatchResult {
let mut any_pending = false;
let mut matched_bindings: Vec<(usize, Box<dyn Action>)> = Vec::new();
// Collect matched bindings into an ordered list using the position in the matching binding first,
// and then the order the binding matched in the view tree second.
// The key is the reverse position of the binding in the bindings list so that later bindings
// match before earlier ones in the user's config
let mut matched_bindings: Vec<(usize, Box<dyn Action>)> = Default::default();
let first_keystroke = self.pending_keystrokes.is_empty();
self.pending_keystrokes.push(keystroke.clone());
@@ -84,35 +89,30 @@ impl KeymapMatcher {
self.contexts
.extend(dispatch_path.iter_mut().map(|e| std::mem::take(&mut e.1)));
for (i, (view_id, _)) in dispatch_path.into_iter().enumerate() {
// Find the bindings which map the pending keystrokes and current context
for (i, (view_id, _)) in dispatch_path.iter().enumerate() {
// Don't require pending view entry if there are no pending keystrokes
if !first_keystroke && !self.pending_views.contains_key(&view_id) {
if !first_keystroke && !self.pending_views.contains_key(view_id) {
continue;
}
// If there is a previous view context, invalidate that view if it
// has changed
if let Some(previous_view_context) = self.pending_views.remove(&view_id) {
if let Some(previous_view_context) = self.pending_views.remove(view_id) {
if previous_view_context != self.contexts[i] {
continue;
}
}
// Find the bindings which map the pending keystrokes and current context
for binding in self.keymap.bindings().iter().rev() {
match binding.match_keys_and_context(&self.pending_keystrokes, &self.contexts[i..])
{
BindingMatchResult::Complete(mut action) => {
// Swap in keystroke for special KeyPressed action
if action.name() == "KeyPressed" && action.namespace() == "gpui" {
action = Box::new(KeyPressed {
keystroke: keystroke.clone(),
});
}
matched_bindings.push((view_id, action))
BindingMatchResult::Complete(action) => {
matched_bindings.push((*view_id, action));
}
BindingMatchResult::Partial => {
self.pending_views.insert(view_id, self.contexts[i].clone());
self.pending_views
.insert(*view_id, self.contexts[i].clone());
any_pending = true;
}
_ => {}
@@ -125,6 +125,8 @@ impl KeymapMatcher {
}
if !matched_bindings.is_empty() {
// Collect the sorted matched bindings into the final vec for ease of use
// Matched bindings are in order by precedence
MatchResult::Matches(matched_bindings)
} else if any_pending {
MatchResult::Pending
@@ -219,15 +221,47 @@ mod tests {
use super::*;
#[test]
fn test_keymap_and_view_ordering() -> Result<()> {
actions!(test, [EditorAction, ProjectPanelAction]);
let mut editor = KeymapContext::default();
editor.add_identifier("Editor");
let mut project_panel = KeymapContext::default();
project_panel.add_identifier("ProjectPanel");
// Editor 'deeper' in than project panel
let dispatch_path = vec![(2, editor), (1, project_panel)];
// But editor actions 'higher' up in keymap
let keymap = Keymap::new(vec![
Binding::new("left", EditorAction, Some("Editor")),
Binding::new("left", ProjectPanelAction, Some("ProjectPanel")),
]);
let mut matcher = KeymapMatcher::new(keymap);
assert_eq!(
matcher.push_keystroke(Keystroke::parse("left")?, dispatch_path.clone()),
MatchResult::Matches(vec![
(2, Box::new(EditorAction)),
(1, Box::new(ProjectPanelAction)),
]),
);
Ok(())
}
#[test]
fn test_push_keystroke() -> Result<()> {
actions!(test, [B, AB, C, D, DA]);
actions!(test, [B, AB, C, D, DA, E, EF]);
let mut context1 = KeymapContext::default();
context1.set.insert("1".into());
context1.add_identifier("1");
let mut context2 = KeymapContext::default();
context2.set.insert("2".into());
context2.add_identifier("2");
let dispatch_path = vec![(2, context2), (1, context1)];
@@ -280,6 +314,7 @@ mod tests {
matcher.push_keystroke(Keystroke::parse("d")?, dispatch_path.clone()),
MatchResult::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]),
);
// If none of the d action handlers consume the binding, a pending
// binding may then be used
assert_eq!(
@@ -360,22 +395,22 @@ mod tests {
let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap();
let mut context = KeymapContext::default();
context.set.insert("a".into());
context.add_identifier("a");
assert!(!predicate.eval(&[context]));
let mut context = KeymapContext::default();
context.set.insert("a".into());
context.set.insert("b".into());
context.add_identifier("a");
context.add_identifier("b");
assert!(predicate.eval(&[context]));
let mut context = KeymapContext::default();
context.set.insert("a".into());
context.map.insert("c".into(), "x".into());
context.add_identifier("a");
context.add_key("c", "x");
assert!(!predicate.eval(&[context]));
let mut context = KeymapContext::default();
context.set.insert("a".into());
context.map.insert("c".into(), "d".into());
context.add_identifier("a");
context.add_key("c", "d");
assert!(predicate.eval(&[context]));
let predicate = KeymapContextPredicate::parse("!a").unwrap();
@@ -415,10 +450,11 @@ mod tests {
assert!(!predicate.eval(&contexts[6..]));
fn context_set(names: &[&str]) -> KeymapContext {
KeymapContext {
set: names.iter().copied().map(str::to_string).collect(),
..Default::default()
}
let mut keymap = KeymapContext::new();
names
.iter()
.for_each(|name| keymap.add_identifier(name.to_string()));
keymap
}
}
@@ -441,10 +477,10 @@ mod tests {
]);
let mut context_a = KeymapContext::default();
context_a.set.insert("a".into());
context_a.add_identifier("a");
let mut context_b = KeymapContext::default();
context_b.set.insert("b".into());
context_b.add_identifier("b");
let mut matcher = KeymapMatcher::new(keymap);
@@ -489,7 +525,7 @@ mod tests {
matcher.clear_pending();
let mut context_c = KeymapContext::default();
context_c.set.insert("c".into());
context_c.add_identifier("c");
// Pending keystrokes are maintained per-view
assert_eq!(

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