Compare commits

...

478 Commits

Author SHA1 Message Date
Kirill Bulatov
eb12b0b9ed Merge pull request #2463 from zed-industries/kb/reapply-modal-accessibility
Reintroduce more accesible modal keybindings
2023-05-17 13:46:25 -04:00
Kirill Bulatov
5c3690c3f1 Merge pull request #2470 from zed-industries/kb/fix-project-search-esc
In project search on ESC, reduce multiple carets to one first
2023-05-17 13:45:18 -04:00
Joseph Lyons
baedb7d0fc v0.86.x stable 2023-05-17 12:38:43 -04:00
Joseph Lyons
fd72d24060 zed 0.86.1 2023-05-11 11:42:14 -04:00
Joseph Lyons
a17b5e8a80 Revert "More keybindings in macOs modals with buttons"
This reverts commit 1398a12062.
2023-05-11 11:38:30 -04:00
Joseph Lyons
0092df7c51 v0.86.x preview 2023-05-10 16:46:52 -04:00
Mikayla Maki
c7fcc031eb Merge pull request #2460 from zed-industries/show-dock-on-activate
Fix bug with terminal button
2023-05-10 09:08:45 -07:00
Mikayla Maki
0dce5ba7ae Fix bug with terminal button 2023-05-10 08:15:20 -07:00
Kirill Bulatov
eec60556ab Highlight include/exclude inputs when errors happen there 2023-05-10 11:11:31 +03:00
Kirill Bulatov
dfdf7e4866 Test the search inclusions/exclusions 2023-05-10 11:11:31 +03:00
Kirill Bulatov
80fc1bc276 Use placeholder in include/exclude editors 2023-05-10 11:11:31 +03:00
Kirill Bulatov
0e31d13a1e Rework tab and escape key handling in search panel
Fixes
https://linear.app/zed-industries/issue/Z-1238/focus-the-results-multibuffer-in-project-search-on-esc

and adds a tab shortcut for project search inputs

co-authored-by: Julia <julia@zed.dev>
2023-05-10 11:11:31 +03:00
Kirill Bulatov
3da55c14a6 Fix arrow layout 2023-05-10 11:11:31 +03:00
Kirill Bulatov
6fb8679184 Trim glob input 2023-05-10 11:11:31 +03:00
Kirill Bulatov
13296d502c Extra rows approach draft
co-authored-by: Max <max@zed.dev>
2023-05-10 11:11:31 +03:00
Kirill Bulatov
b5abac6af6 Draft search include/exclude logic 2023-05-10 11:11:31 +03:00
Kirill Bulatov
915154b047 Add initial include/exclude project search UI 2023-05-10 11:11:31 +03:00
Antonio Scandurra
3115c8381d Merge pull request #2458 from zed-industries/fix-context-menu-click
Always dismiss context menu on click
2023-05-10 09:24:23 +02:00
Antonio Scandurra
1b5e79251c Always dismiss context menu on click 2023-05-10 09:18:13 +02:00
Nathan Sobo
5e8b7bd06d Replace todo with unimplemented to reduce distractions 2023-05-09 16:31:53 -06:00
Mikayla Maki
26d80eef0a Merge pull request #2456 from zed-industries/fix-dock-pane-focus
Make dock not eagerly steal focus from sub items
2023-05-09 15:59:06 -04:00
Mikayla Maki
0214228689 Fix format 2023-05-09 12:54:53 -07:00
Mikayla Maki
6dfb48dbd5 Fix center items not being activated when deserialized 2023-05-09 12:27:07 -07:00
Mikayla Maki
8d561d6408 Make dock not eagerly steal focus from sub items 2023-05-09 12:00:09 -07:00
Mikayla Maki
2d7cfb8c7c Merge pull request #2454 from zed-industries/fix-mismatching-panes-when-deserializing-empty-panes
Fully remove panes and update internal data structures
2023-05-08 20:37:06 -04:00
Mikayla Maki
fa049bea6e Refactor and fix format 2023-05-08 17:32:40 -07:00
Mikayla Maki
49335d017a Add manual removal code to remove_panes 2023-05-08 17:25:28 -07:00
Mikayla Maki
9b2d3fcd48 Fully remove panes and update internal data structures 2023-05-08 17:09:29 -07:00
Max Brunsfeld
8fd0c9fb0e collab 0.12.0 2023-05-08 15:54:11 -07:00
Mikayla Maki
1d66f24f23 Merge pull request #2428 from zed-industries/add-branch-name
Add branch name and synchronize repository representations on the worktree.
2023-05-08 17:40:47 -04:00
Mikayla Maki
9366a0dbee Bump protocol version number 2023-05-08 14:34:14 -07:00
Mikayla Maki
f28419cfd1 Fix styling of titlebar highlights 2023-05-08 14:33:59 -07:00
Mikayla Maki
712fb5ad7f Add postgres migration 2023-05-08 14:33:59 -07:00
Mikayla Maki
1a9afd186b Restore randomized integration tests 2023-05-08 14:33:59 -07:00
Mikayla Maki
15d2f19b4a fix format 2023-05-08 14:33:59 -07:00
Mikayla Maki
d2279674a7 Fix panic in tests 2023-05-08 14:33:59 -07:00
Mikayla Maki
62e763d0d3 Removed test modifications, added special case to git initialization for when the repository is inside a .git folder 2023-05-08 14:33:59 -07:00
Mikayla Maki
f9e4464658 Refresh titlebar on project notifications 2023-05-08 14:33:59 -07:00
Mikayla Maki
2c2076bd77 Adjust tests to not create repositories inside repositories 2023-05-08 14:33:59 -07:00
Mikayla Maki
ab952f1b31 Fixed randomized test failures
co-authored-by: Max <max@zed.dev>
2023-05-08 14:33:59 -07:00
Mikayla Maki
d8dac07408 Removed scan ID from repository interfaces
co-authored-by: Max <max@zed.dev>
2023-05-08 14:33:59 -07:00
Mikayla Maki
270147d20c Finished RepositoryEntry refactoring, smoke tests passing
co-authored-by: Max <max@zed.dev>
2023-05-08 14:33:59 -07:00
Mikayla Maki
53569ece03 WIP: Change RepositoryEntry representation to be keyed off of the work directory
Removes branches button scaffolding
2023-05-08 14:33:59 -07:00
Mikayla Maki
b6d6f5c650 WIP: re-arranging the RepositoryEntry representation
Added branches to the randomized test to check the git branch
Added the remaining database integrations in collab

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Petros <petros@zed.dev>
2023-05-08 14:33:59 -07:00
Mikayla Maki
8bde496e74 Add branch name in title UI
co-authored-by: Petros <petros@zed.dev>
2023-05-08 14:33:59 -07:00
Petros Amoiridis
5302c256a4 Rebase main and fix error 2023-05-08 14:33:59 -07:00
Mikayla Maki
8301ee43d6 WIP: Add repository entries to collab and synchronize when rejoining the room
co-authored-by: Max <max@zed.dev>
2023-05-08 14:33:59 -07:00
Mikayla Maki
2fe5bf419b Add proto fields for repository entry maintenance 2023-05-08 14:33:59 -07:00
Mikayla Maki
c6d7ed33c2 Add smoke test for collaboration 2023-05-08 14:33:59 -07:00
Petros Amoiridis
ca4da52e39 Remove unused functions 2023-05-08 14:33:59 -07:00
Petros Amoiridis
e057b0193f Introduce BrancesButton in title bar
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-05-08 14:33:58 -07:00
Petros Amoiridis
797d47a08c Render title root names without branches 2023-05-08 14:33:58 -07:00
Petros Amoiridis
92a222aba8 Introduce a version control branch icon 2023-05-08 14:33:58 -07:00
Mikayla Maki
8f0aa3c6d9 Add branch name into title 2023-05-08 14:33:58 -07:00
Petros Amoiridis
d34ec462f8 Display branch information per worktree root
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-05-08 14:33:58 -07:00
Mikayla Maki
ffd9d4eb59 Fix bug in repo detection 2023-05-08 14:33:58 -07:00
Mikayla Maki
3570810516 Add API for accessing git branch 2023-05-08 14:33:58 -07:00
Mikayla Maki
26afd592c5 Wire in the branch name 2023-05-08 14:33:58 -07:00
Mikayla Maki
5b4e58d1de Fix repo_for and clean up repository_entries 2023-05-08 14:33:58 -07:00
Mikayla Maki
023d665fb3 Fix TreeMap retain 2023-05-08 14:33:58 -07:00
Mikayla Maki
ae890212e3 Restored a lost API and got everything compiling 2023-05-08 14:33:58 -07:00
Mikayla Maki
bcf608e9e9 WIP: Refactor existing git code to use new representation.
co-authored-by: petros <petros@zed.dev>
2023-05-08 14:33:58 -07:00
Mikayla Maki
563f13925f WIP: Convert old git repository vec to new treemap based approach.
co-authored-by: Nathan <nathan@zed.dev>
2023-05-08 14:33:58 -07:00
Mikayla Maki
a58d3d8128 Add a data driven representation of the current git repository state to the worktree snapshots
WIP: Switch git repositories to use SumTrees

Co-authored-by: Nathan <nathan@zed.dev>
2023-05-08 14:33:58 -07:00
Mikayla Maki
bb93447a0d Merge pull request #2453 from zed-industries/fix-click-fallthrough
Fixed clicks falling through the modal terminal
2023-05-08 13:59:57 -04:00
Mikayla Maki
2cf928c85a Fixed clicks falling through the modal terminal
co-authored-by: Antonio <antonio@zed.dev>
2023-05-08 10:54:12 -07:00
Kirill Bulatov
39bddfc7b7 Only allow invisbles in local and leader selections 2023-05-08 19:36:51 +03:00
Kirill Bulatov
98ff18c430 Code review fixes 2023-05-08 19:36:51 +03:00
Kirill Bulatov
e6489e999d Add invisibles wrapping test 2023-05-08 19:36:51 +03:00
Kirill Bulatov
d2b2dc39d9 Do not print invisibles in non-full mode editors 2023-05-08 19:36:51 +03:00
Nate Butler
ab6b3adb2b Add a theme entry for whitespace, use it to style whitespaces
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
Co-Authored-By: Kirill Bulatov <mail4score@gmail.com>
2023-05-08 19:36:51 +03:00
Kirill Bulatov
fb3ef4bcf6 Fix wrapped line detection 2023-05-08 19:36:51 +03:00
Kirill Bulatov
075bab2ea9 Use more convntional name for the settings 2023-05-08 19:36:51 +03:00
Kirill Bulatov
706f6f495a Add a test 2023-05-08 19:36:51 +03:00
Kirill Bulatov
ec725fe399 Do not print extra invisibles on line wraps 2023-05-08 19:36:51 +03:00
Kirill Bulatov
95bcd19020 Refactor line glyphs drawing methods 2023-05-08 19:36:51 +03:00
Kirill Bulatov
4aaf44df94 Support invisibles in the selection 2023-05-08 19:36:51 +03:00
Kirill Bulatov
1eeeec157e Use cached standard glyphs for invisible symbols
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
2023-05-08 19:36:51 +03:00
Kirill Bulatov
714734d279 Add whitespaces rendering 2023-05-08 19:36:51 +03:00
Kirill Bulatov
2d8c88ad73 Draw tabs with svg icons in editor code only 2023-05-08 19:36:51 +03:00
Kirill Bulatov
f0a88b3337 Make invisibles display configurable 2023-05-08 19:36:51 +03:00
Kirill Bulatov
ad731ea6d2 Draft invisibles' tabs display 2023-05-08 19:36:51 +03:00
Max Brunsfeld
4f8607039c Add is_tab field to chunks
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2023-05-08 19:36:51 +03:00
Julia
cf304a0edc Merge pull request #2452 from zed-industries/prevent-some-rounding-clipping
Prevent some cases of clipping icons due to pixel coord rounding
2023-05-08 11:48:33 -04:00
Julia
332b364a30 Prevent some cases of clipping icons due to pixel coord rounding 2023-05-08 11:42:00 -04:00
Antonio Scandurra
235470bbfd Merge pull request #2451 from zed-industries/kb/extra-deps
Remove unused dependencies
2023-05-08 14:46:27 +02:00
Kirill Bulatov
6cb0bc89d2 Remove unused dependencies 2023-05-07 21:07:55 +03:00
Antonio Scandurra
0296974ab1 Merge pull request #2441 from zed-industries/implicit-ancestry
Determine view ancestry during layout
2023-05-05 10:58:00 +02:00
Antonio Scandurra
5e16f70067 💄 2023-05-05 10:53:15 +02:00
Antonio Scandurra
080a1f00a3 Delay focus_in event for window activation till after layout 2023-05-05 10:47:42 +02:00
Antonio Scandurra
b9ed327b94 Replace usages of is_parent_view_focused with is_self_focused
Previously, this was used because we didn't have access to the current
view and `EventContext` was an element-only abstraction. Now that the
`EventContext` wraps the current view's `ViewContext` we can simply check
for the view's focus and avoid querying ancestors.
2023-05-05 10:08:22 +02:00
Antonio Scandurra
80ad59a620 Make focusing the parent an effect to avoid querying ancestors 2023-05-05 10:04:54 +02:00
Joseph T. Lyons
c55a4c0feb Merge pull request #2447 from zed-industries/fix-auto-update
Do not use post_json() to auto update
2023-05-04 17:15:09 -04:00
Mikayla Maki
3631b3a86c Merge pull request #2446 from zed-industries/fix-copilot-logged-out
Fix copilot stuck in logged out state
2023-05-04 16:52:04 -04:00
Mikayla Maki
89af803565 Rearrange the state machine 2023-05-04 13:45:31 -07:00
Julia
137cbaba34 Merge pull request #2445 from zed-industries/construct-text-buffer-in-background
Construct text buffer in background
2023-05-04 16:01:39 -04:00
Julia
eacea55aaf Fixup cases using buffer model handle id as buffer id 2023-05-04 12:32:31 -04:00
Julia
1883e260ce Offload text::Buffer construction to background worker
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-05-04 12:32:31 -04:00
Julia
7e06062bdb Store history base text as rope
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-05-04 12:32:31 -04:00
Joseph Lyons
8313414e1e Do not use post_json() to auto update 2023-05-04 12:12:36 -04:00
Antonio Scandurra
d6b0569bed Merge branch 'main' into implicit-ancestry 2023-05-04 17:19:06 +02:00
Antonio Scandurra
f51425d390 Merge pull request #2444 from zed-industries/fix-clicking-sidebar-buttons
Use `Workspace::toggle_sidebar_item` when clicking on sidebar button
2023-05-04 17:14:40 +02:00
Antonio Scandurra
64e0c16baa Use Workspace::toggle_sidebar_item when clicking on sidebar button
Previously, we were mistakenly using `Sidebar::toggle_item`, which only
performs part of the toggle operation.
2023-05-04 17:06:37 +02:00
Antonio Scandurra
cbae4e751b Merge pull request #2443 from zed-industries/fix-vim-mode-rename
Avoid calling `update_window` twice in `blurred` event handler
2023-05-04 16:24:51 +02:00
Antonio Scandurra
912a4cf549 Avoid calling update_window twice in blurred event handler
This was preventing us from unhooking vim when performing a rename,
which prevented typing in the rename editor.
2023-05-04 16:18:01 +02:00
Antonio Scandurra
0f93714d4f Merge pull request #2442 from zed-industries/filter-vim-commands
Filter out vim commands when vim mode is disabled
2023-05-04 15:00:19 +02:00
Antonio Scandurra
b1f5cfaa79 Merge pull request #2440 from zed-industries/fix-navigate-to-definitions-panic
Fix panic when opening multiple definitions in a multibuffer
2023-05-04 14:56:43 +02:00
Antonio Scandurra
b3baebde22 Filter out vim commands when vim mode is disabled 2023-05-04 14:52:34 +02:00
Antonio Scandurra
da19edc3e3 Merge branch 'main' into implicit-ancestry 2023-05-04 14:39:58 +02:00
Antonio Scandurra
121264d35a Fix panic when opening multiple definitions in a multibuffer
The editor is on the stack, so adding an item to the `Pane` containing
the editor will cause a double borrow and a consequent panic. This
commit fixes the issue by deferring the opening of the definitions.
2023-05-04 14:34:42 +02:00
Antonio Scandurra
7e2a461486 Merge pull request #2439 from zed-industries/fix-keystrokes-for-action
Cache view's type id and keymap context into a separate map
2023-05-04 14:22:42 +02:00
Antonio Scandurra
5cc6304fa6 Verify keystrokes can be queried while views are on the stack 2023-05-04 12:09:32 +02:00
Antonio Scandurra
3d679ddb26 Avoid re-allocating KeymapContext after every view notification 2023-05-04 12:04:30 +02:00
Antonio Scandurra
18e39ef2fa Cache view's type id and keymap context into a separate map
During `layout`, we now pass a mutable reference to the element's
parent view. This is a recursive process that causes the view to
be removed from `AppContext` and then re-inserted back into it once
the layout is complete.

As such, querying parent views during `layout` does not work as such
views will have been removed from `AppContext` and not yet re-inserted
into it. This caused a bug in `KeystrokeLabel`, which used the `keystrokes_for_action`
method to query its ancestors to determine their type id and keymap context.

Now, any time a view notifies, we will cache its keymap context so that
we don't need to query the parent view during `layout`.
2023-05-04 10:47:56 +02:00
Antonio Scandurra
7b7a495be3 Remove stray dbg! statement 2023-05-04 09:56:49 +02:00
Antonio Scandurra
f6f18be9c3 Remove WindowContext::is_child_focused 2023-05-04 09:53:35 +02:00
Antonio Scandurra
67a3891f15 Make dispatch_event related methods public to the crate only 2023-05-04 09:53:35 +02:00
Antonio Scandurra
92183e0d72 Ensure querying keystrokes or actions is safe
This is achieved by moving `available_actions` into `AsyncAppContext` (where
we know no view/window is on the stack) and `keystrokes_for_action` into `LayoutContext`
where we'll fetch the previous frame's ancestors and notify the current view if those
change after we perform a layout.
2023-05-04 09:53:31 +02:00
Joseph Lyons
053b34875b collab 0.11.0 2023-05-03 14:59:04 -04:00
Joseph Lyons
653ea3a85d v0.86.x dev 2023-05-03 14:38:41 -04:00
Max Brunsfeld
c2c29d3fb6 Merge pull request #2427 from zed-industries/copilot-disabled-globs
Add copilot.disabled_globs setting
2023-05-03 11:00:08 -07:00
Max Brunsfeld
9d41f83b1b Merge branch 'main' into copilot-disabled-globs 2023-05-03 10:53:28 -07:00
Antonio Scandurra
040cc4d4c3 Allow notifying views when the ancestry of another view is outdated 2023-05-03 19:25:00 +02:00
Max Brunsfeld
8eb1312deb Add copilot menu item for enabling paths by glob 2023-05-03 10:14:01 -07:00
Antonio Scandurra
7250754f8e Make dispatch_keystroke public to the crate only 2023-05-03 19:13:17 +02:00
Antonio Scandurra
9e8f852afb Remove ViewContext::is_child 2023-05-03 19:09:07 +02:00
Antonio Scandurra
5157442703 Fix integration test relying on deferred happening after focus
Focus is now one of the last things that happens during `flush_effects`,
and we shouldn't have relied on `defer` in the first place to verify
focus changes.
2023-05-03 19:00:32 +02:00
Antonio Scandurra
c65465b0b5 Ensure workspace gets rendered in collab integration tests 2023-05-03 18:31:07 +02:00
Antonio Scandurra
e9ed40da37 Remove the ability to retrieve the view's parent 2023-05-03 16:52:55 +02:00
Antonio Scandurra
7f137ed3dd Compute view ancestry at layout time 2023-05-03 16:36:14 +02:00
Antonio Scandurra
7f345f8bf5 Separate Window::build_scene into layout and paint 2023-05-03 12:18:16 +02:00
Antonio Scandurra
3f037e5128 Merge pull request #2437 from zed-industries/fix-joining-call
Fix "IncomingCallNotification was dropped" error when accepting a call
2023-05-03 10:49:04 +02:00
Antonio Scandurra
376aa1235f Fix "IncomingCallNotification was dropped" error when accepting a call
This was caused by accepting the call, which caused the notification to
be removed. When `active_call.accept_incoming()` finally completed, we
would try to get the app state from it in order to join the project, but
couldn't becuase the view would have already been dropped.

This commit fixes the bug by capturing a weak handle to the app state
when accepting the call as opposed to trying to read it from the view
when the accept completes.
2023-05-03 10:37:57 +02:00
Antonio Scandurra
c3cf9e3185 Merge pull request #2436 from zed-industries/close-window-end-call
Move methods querying window state into `AsyncAppContext`
2023-05-03 10:27:05 +02:00
Joseph T. Lyons
f576586cd7 Merge pull request #2435 from zed-industries/use-json-post-request-for-update-requests
Use json_post() request for update requests
2023-05-02 23:42:25 -04:00
Joseph Lyons
41d4454f45 Use post_json so that the Content-Type is set to application/json 2023-05-02 23:23:43 -04:00
Joseph Lyons
69a4fffae2 Update post_json to take in a bool for allowing for redirects 2023-05-02 23:22:55 -04:00
Joseph T. Lyons
7f5afeb9fa Merge pull request #2434 from zed-industries/add-download-and-uplodate-metadata-to-update-request
Add download and upload metadata to update request
2023-05-02 22:27:58 -04:00
Joseph Lyons
2b95aba99c Add download and upload metadata to update request 2023-05-02 17:16:42 -04:00
Kirill Bulatov
1398a12062 More keybindings in macOs modals with buttons
Closes https://github.com/zed-industries/community/issues/1095
by forcing the non-Cancel button to get a focus.
Due to the way macOs handles buttons on modals, the focus gain had to be
achieved via certain button addition order, rather than conventional
"setFocus"-ish API, see the related comment for details.

Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2023-05-02 21:10:20 +03:00
Antonio Scandurra
70f8cf4cf6 Move methods querying window state into AsyncAppContext 2023-05-02 19:38:48 +02:00
Julia
4f6939732e Merge pull request #2432 from zed-industries/more-diagnostic-source
Show diagnostic source in more places
2023-05-02 09:16:11 -04:00
Julia
185c1650df Show diagnostic source in inline diagnostic 2023-05-02 09:08:07 -04:00
Antonio Scandurra
9108e4151e Merge pull request #2431 from zed-industries/fix-broken-contact-finder
Fix broken styling in contact finder
2023-05-02 13:55:14 +02:00
Antonio Scandurra
94f1775533 Fix broken styling in contact finder
This regressed as part of #2372, where we forgot to theme the contact
finder picker differently from the rest of the app.
2023-05-02 13:46:49 +02:00
Antonio Scandurra
e8f2d985ff Merge pull request #2430 from zed-industries/fix-toggle-contacts-panic
Fix panic when showing contacts popover via keybinding
2023-05-02 11:53:24 +02:00
Antonio Scandurra
f985fac6f9 Fix panic when showing contacts popover via keybinding 2023-05-02 11:47:05 +02:00
Antonio Scandurra
484cda51cf Merge pull request #2429 from zed-industries/fix-debug-elements-panic
Move `debug_elements` to `AsyncAppContext`
2023-05-02 11:15:57 +02:00
Kirill Bulatov
f5278c49b0 Clarify GH Token scope requirements 2023-05-02 12:12:57 +03:00
Antonio Scandurra
794446bf8b Move debug_elements to AsyncAppContext
Previously, `debug_elements` was available on `WindowContext`. If that
method was called while having a borrow out to a view, it would panic because
the view would already have been borrowed.

By moving it to an `AsyncAppContext` we ensure the method can't be called while
a view is being used.
2023-05-02 11:09:40 +02:00
Antonio Scandurra
4c1cba6def Remove unnecessary Element impl for RootElement 2023-05-02 10:09:57 +02:00
Julia
f7de0ad8ae Show diagnostic source in diagnostic multibuffer headers 2023-05-01 16:48:27 -04:00
Max Brunsfeld
c485fc86a2 Add copilot.disabled_globs setting 2023-05-01 13:45:47 -07:00
Joseph T. Lyons
f62ba2eec7 use installation_id over device_id 2023-05-01 16:29:51 -04:00
Julia
5fb9d53dd0 Merge pull request #2425 from zed-industries/trim-eslint-workspace-configuration
Trim eslint workspace configuration & remove diagnostic source underline
2023-05-01 13:24:14 -04:00
Julia
40ab5c1fb9 Remove underline from diagnostic source 2023-05-01 13:15:41 -04:00
Julia
4966a4a681 Reduce hardcoded ESLint workspace configuration 2023-05-01 13:14:35 -04:00
Antonio Scandurra
a8084ad3f4 Merge pull request #2424 from zed-industries/constrain-programmatic-dispatch
Allow programmatic action dispatch only via `AsyncAppContext`
2023-05-01 17:10:57 +02:00
Antonio Scandurra
780ece551e Defer hiding the dock and going back/forward when Pane is on the stack 2023-05-01 17:06:05 +02:00
Antonio Scandurra
e3b2407ebf Run until parked now that the command palette spawns to dispatch action 2023-05-01 16:58:07 +02:00
Antonio Scandurra
6c931ab9da Inline test-only AppContext methods 2023-05-01 16:49:17 +02:00
Antonio Scandurra
eb2cce98a7 Move dispatch_action_any_action_at to AsyncAppContext 2023-05-01 16:40:57 +02:00
Antonio Scandurra
c4472b0786 Remove ViewContext::dispatch_action 2023-05-01 16:27:36 +02:00
Antonio Scandurra
d815fc88ae Remove ViewContext::dispatch_any_action 2023-05-01 14:24:00 +02:00
Antonio Scandurra
029538fe21 Make dispatch_global_action private 2023-05-01 11:45:35 +02:00
Antonio Scandurra
0f44648b38 Merge pull request #2423 from zed-industries/fix-panic-in-go-to-definition
Fix panic when clicking on a definition
2023-04-29 15:02:18 +02:00
Antonio Scandurra
e566929d9e Fix panic when clicking on a definition
This was introduced with #2420 and was caused by re-entrantly updating
the workspace. Instead of passing the workspace reference from the outside,
we now define the definition navigation as a method on the editor which solves
the issue.

Note that we also needed to introduce a `defer` call when navigating to a definition
to prevent the workspace from reading the editor during `open_project_item`.
2023-04-29 14:53:17 +02:00
Joseph T. Lyons
ae5794d911 Merge pull request #2421 from zed-industries/metrics-2
Rework telemetry code to support sending events to Clickhouse
2023-04-28 17:16:33 -04:00
Julia
32f26d1e9a Merge pull request #2422 from zed-industries/source-eslint-server-from-github
Source ESLint server from Github rather than 3rd party NPM package
2023-04-28 16:53:07 -04:00
Julia
1bf85214a4 Source ESLint server from Github rather than 3rd party NPM package 2023-04-28 16:42:36 -04:00
Joseph Lyons
6b0faa2d9c Rework telemetry code to support sending events to Clickhouse
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
2023-04-28 16:06:55 -04:00
Antonio Scandurra
dc999f719b Merge pull request #2420 from zed-industries/simplify-action-dispatch
Remove `impl_internal_actions!` macro
2023-04-28 17:39:56 +02:00
Antonio Scandurra
106ebeb386 Remove obsolete presenter modules 2023-04-28 17:37:25 +02:00
Antonio Scandurra
489b1f6a63 Merge remote-tracking branch 'origin/main' into simplify-action-dispatch 2023-04-28 17:31:12 +02:00
Antonio Scandurra
1c5376a560 Remove impl_internal_actions macro 2023-04-28 17:29:57 +02:00
Antonio Scandurra
1d41a703ad Remove internal actions from terminal_button 2023-04-28 17:29:17 +02:00
Antonio Scandurra
33da9e5690 Remove internal actions from project_panel 2023-04-28 17:21:10 +02:00
Antonio Scandurra
e1535735b8 Remove DeployContextMenu internal action 2023-04-28 16:51:01 +02:00
Antonio Scandurra
f65e64829e Remove Scroll internal action 2023-04-28 16:43:31 +02:00
Antonio Scandurra
3409ee1785 Remove DeployMouseContextMenu internal action
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-28 16:41:46 +02:00
Antonio Scandurra
c22342e271 💄
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-28 16:38:00 +02:00
Antonio Scandurra
1f35e1dbf9 Remove internal actions from copilot_button
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-28 16:36:57 +02:00
Antonio Scandurra
a91903c2ab Remove internal actions from link_go_to_definition 2023-04-28 16:24:46 +02:00
Antonio Scandurra
264a2c1835 Remove HideHover and HoverAt internal actions 2023-04-28 16:12:24 +02:00
Antonio Scandurra
e6f561ce46 Remove unused Jump internal action 2023-04-28 15:57:36 +02:00
Antonio Scandurra
c04cb0286a Remove Dismiss and RespondToContactRequest internal actions 2023-04-28 15:56:41 +02:00
Antonio Scandurra
0469e25de6 Remove LeaveCall internal action 2023-04-28 14:14:25 +02:00
Antonio Scandurra
83436213ad Remove Jump internal action 2023-04-28 14:13:07 +02:00
Antonio Scandurra
3763b985e3 Fix panic due to re-entrantly reading Workspace 2023-04-28 12:36:50 +02:00
Antonio Scandurra
a978f3fe4f Remove Select internal action 2023-04-28 12:33:22 +02:00
Antonio Scandurra
5215adbd3f Remove ToggleExpanded and Call internal actions 2023-04-28 12:26:47 +02:00
Antonio Scandurra
5d8fcceee3 Remove RespondToCall internal action 2023-04-28 12:24:07 +02:00
Antonio Scandurra
4bcba487c5 Remove SplitWithProjectEntry internal action 2023-04-28 12:19:38 +02:00
Antonio Scandurra
272039a858 Remove SplitWithItem internal action 2023-04-28 12:17:31 +02:00
Antonio Scandurra
6857426b78 Remove RemoveWorktreeFromProject internal action 2023-04-28 12:11:59 +02:00
Antonio Scandurra
71a4bc7905 Remove OpenSharedScreen internal action 2023-04-28 12:09:34 +02:00
Antonio Scandurra
d953729233 Remove JoinProject internal action 2023-04-28 11:22:04 +02:00
Antonio Scandurra
f881f9e3d8 Remove ToggleFollow internal action 2023-04-28 10:07:44 +02:00
Antonio Scandurra
06c01a5937 Eliminate OpenPaths global action for workspace and replace with methods
We no longer want to invoke this with dispatch_action.
2023-04-27 18:36:28 -06:00
Max Brunsfeld
db73ba5a1a Merge pull request #2419 from zed-industries/zombie-copilot-process
Shutdown copilot server when quitting zed
2023-04-27 14:47:30 -07:00
Max Brunsfeld
1533c17cd7 Shutdown copilot server when quitting zed 2023-04-27 14:39:00 -07:00
Max Brunsfeld
7258db7a4e Merge pull request #2417 from zed-industries/hover-markdown
Render markdown more correctly in the editor hover popover
2023-04-27 14:15:04 -07:00
Max Brunsfeld
6042df393b Give code spans in markdown a background highlight 2023-04-27 13:58:30 -07:00
Max Brunsfeld
8eb9c6563a Generalize Text element to let you add arbitrary scene primitives for runs of text 2023-04-27 13:58:06 -07:00
Julia
92c9de1f50 Merge pull request #2415 from zed-industries/diagnostic-hovers-source
Show source of diagnostic on hover
2023-04-27 15:12:22 -04:00
Julia
87539e7b82 Update test to not fail due to absence of diagnostic source 2023-04-27 15:04:48 -04:00
Max Brunsfeld
66d4cb8c14 Tweak rendering of multi-paragraph list items in markdown 2023-04-27 11:39:34 -07:00
Julia
a284fae515 Don't hardcode workspaceFolder for ESLint adapter 2023-04-27 14:35:34 -04:00
Julia
678c188de0 Re-allow diagnostics hovers to soft wrap
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-04-27 14:35:34 -04:00
Julia
3f7533a0b4 Show source of diagnostic hovers 2023-04-27 14:35:34 -04:00
Max Brunsfeld
45c7073934 Merge pull request #2418 from zed-industries/vim-inactive-window-crash
Fix vim mode crash when active editor changes in inactive window
2023-04-27 10:48:49 -07:00
Max Brunsfeld
30f20024c0 Fix vim mode crash when active editor changes in inactive window
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2023-04-27 10:43:35 -07:00
Antonio Scandurra
6cbc1dcd87 💄 2023-04-27 14:56:16 +02:00
Antonio Scandurra
20e38d2def Remove OpenProjectEntryInPane internal action 2023-04-27 14:51:02 +02:00
Antonio Scandurra
b6437d6d9e Remove Toast and DismissToast internal actions 2023-04-27 14:43:10 +02:00
Antonio Scandurra
2950344c25 Remove internal actions from Dock 2023-04-27 14:29:15 +02:00
Antonio Scandurra
15d83d40b0 Remove internal actions from menu crate 2023-04-27 14:29:11 +02:00
Antonio Scandurra
816eb06a7b Remove internal actions from CopilotCodeVerification 2023-04-27 14:25:34 +02:00
Antonio Scandurra
32f21771a6 Remove internal actions from ContextMenu 2023-04-27 14:25:14 +02:00
Antonio Scandurra
022368225e Remove internal actions from Pane 2023-04-27 14:23:26 +02:00
Antonio Scandurra
5521ff1b22 Allow passing a handler function to context menu items 2023-04-27 11:32:12 +02:00
Antonio Scandurra
d3b976d044 Remove ContextMenu::element_item 2023-04-27 11:11:21 +02:00
Antonio Scandurra
df2f471ddf Remove unused ContextMenu::ViewAction 2023-04-27 10:55:37 +02:00
Antonio Scandurra
06b12bbb68 Remove unnecessarily general keystroke_label_for function 2023-04-27 10:55:14 +02:00
Max Brunsfeld
c6abb0db3a Improve rendering of multi-paragraph list items in hover markdown 2023-04-26 17:09:20 -07:00
Max Brunsfeld
c75207c4e5 Merge pull request #2416 from zed-industries/outline-view-leaving-lines-highlighted
Remove highlighted rows when confirming outline view
2023-04-26 16:11:22 -07:00
Max Brunsfeld
c15dadbb8c Remove highlighted rows when confirming outline view 2023-04-26 15:46:29 -07:00
Max Brunsfeld
d298ce3fd3 Render more markdown features in hover popover 2023-04-26 15:33:10 -07:00
Max Brunsfeld
7960067cf9 Fix bug where Text element would wrap at the right glyph in the wrong run 2023-04-26 15:33:10 -07:00
Max Brunsfeld
54e7464163 collab 0.10.0 2023-04-26 13:24:08 -07:00
Max Brunsfeld
1bbcff543b Add API for adding mouse regions within Text 2023-04-26 12:00:26 -07:00
Joseph Lyons
99e82d829f v0.85.x dev 2023-04-26 14:19:52 -04:00
Nathan Sobo
a45282eb63 Merge pull request #2414 from zed-industries/spawn-weak-for-views
Pass a `WeakViewHandle` to `ViewContext::spawn`
2023-04-26 06:10:44 -06:00
Antonio Scandurra
6317e885c7 Don't allow strong view handles to be read/updated with an AsyncAppContext
This avoids an invitation to hold strong view handles across async await
points, which is a common source of leaks.

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-26 13:36:13 +02:00
Antonio Scandurra
689e878bd8 Use a WeakViewHandle in Client for view message handlers 2023-04-26 12:21:02 +02:00
Antonio Scandurra
57beec6071 Allow direct read/update of WeakViewHandle only in AsyncAppContext 2023-04-26 12:20:57 +02:00
Antonio Scandurra
2b6830c798 Remove unnecessary calls to WeakViewHandle::upgrade 2023-04-26 11:13:46 +02:00
Antonio Scandurra
94c2eaad23 Replace ViewContext::spawn with ViewContext::spawn_weak 2023-04-26 10:23:27 +02:00
Antonio Scandurra
09f7e41907 Merge pull request #2405 from zed-industries/fewer-context-traits
Simplify traits for accessing app state uniformly across different kinds of contexts
2023-04-26 10:02:56 +02:00
Antonio Scandurra
7ca412ade3 Merge remote-tracking branch 'origin/main' into fewer-context-traits 2023-04-26 09:54:58 +02:00
Max Brunsfeld
a7145021b6 Extract a named struct from text_layout::Line's style runs 2023-04-25 14:05:23 -07:00
Max Brunsfeld
3cb50ed6b7 Merge pull request #2413 from zed-industries/project-search-focus
Preserve previous focus when re-focusing project search
2023-04-25 09:33:09 -07:00
Max Brunsfeld
3db67a48b5 Preserve previous focus when re-focusing project search 2023-04-25 09:28:50 -07:00
Max Brunsfeld
c31a5063d0 Merge pull request #2411 from zed-industries/rust-snippet-variable-completions
Avoid treating snippet completions' details as their variable types
2023-04-25 08:59:35 -07:00
Max Brunsfeld
db276a422f Merge pull request #2412 from zed-industries/more-workspace-deps
Specify more dependencies at the cargo workspace level, to avoid repeating versions
2023-04-24 18:09:35 -07:00
Max Brunsfeld
ebbe52e6b0 🎨 Specify more dependencies at the workspace level 2023-04-24 17:41:55 -07:00
Max Brunsfeld
dd3f6ff4ca Avoid treating snippet completions' details as their variable types 2023-04-24 16:20:14 -07:00
Max Brunsfeld
b76194db97 Merge pull request #2409 from zed-industries/stale-excerpts
Fix stale project diagnostic excerpts for guests
2023-04-24 14:22:04 -07:00
Max Brunsfeld
7bd51851c2 🎨
Co-authored-by: Julia Risley <julia@zed.dev>
2023-04-24 13:54:47 -07:00
Max Brunsfeld
a8ddba55d8 Send language server updates via the same task that sends buffer operations
Co-authored-by: Julia Risley <julia@zed.dev>
2023-04-24 13:52:03 -07:00
Max Brunsfeld
ce34bf62fe Add failing test for diagnostic message ordering
Co-authored-by: Julia Risley <julia@zed.dev>
2023-04-24 13:18:37 -07:00
Max Brunsfeld
d2ba1ec275 Merge pull request #2407 from zed-industries/lsp-log-view
Add a simple language server log view
2023-04-24 10:15:29 -07:00
Max Brunsfeld
f3ada72785 Avoid accidentally dropping subscription when detaching it 2023-04-24 10:04:45 -07:00
Max Brunsfeld
1793c5ff6c Upgrade tree-sitter-json 2023-04-24 09:43:31 -07:00
Max Brunsfeld
e7cb996044 Improve the LSP log UI 2023-04-24 09:43:31 -07:00
Max Brunsfeld
6ed7f1281f Persist LSP logs after log view is closed 2023-04-24 09:43:31 -07:00
Max Brunsfeld
6ef6f03322 Update LspLog view to use new gpui APIs 2023-04-24 09:43:31 -07:00
Max Brunsfeld
a280a93cd8 Start work on a language server log view 2023-04-24 09:43:31 -07:00
Max Brunsfeld
2dd4920625 Add LanguageServer::on_io method, for observing JSON sent back and forth 2023-04-24 09:43:31 -07:00
Max Brunsfeld
abdccf7393 Use a workspace dependency for the futures crate 2023-04-24 09:43:31 -07:00
Mikayla Maki
d82cc49f79 Merge pull request #2408 from zed-industries/change-copilot-wording
Change copilot wording to be more explicit
2023-04-25 04:28:23 +12:00
Mikayla Maki
c12e2ac3fb fix formatting 2023-04-24 09:25:04 -07:00
Mikayla Maki
c7874cf169 Update copilot_button.rs 2023-04-24 09:03:54 -07:00
Antonio Scandurra
c165fb9be5 Remove ReadView and UpdateView traits
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-24 17:36:14 +02:00
Antonio Scandurra
a6115d9330 Remove UpdateModel trait
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-24 16:52:09 +02:00
Antonio Scandurra
a9417f3d2e Remove ReadModelWith trait
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-24 16:44:54 +02:00
Antonio Scandurra
5f500d34b2 Remove UpgradeModelHandle trait
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-24 16:40:30 +02:00
Antonio Scandurra
b8fab6fde9 Remove UpgradeViewHandle trait
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2023-04-24 16:33:27 +02:00
Mikayla Maki
455d383d08 Change copilot wording to be more explicit 2023-04-21 21:42:23 -07:00
Mikayla Maki
f10de10915 Merge pull request #2406 from zed-industries/fix-nondeterministic-terminal-test
Fix minor terminal bugs
2023-04-22 12:42:20 +12:00
Mikayla Maki
fa7f4974a0 Remove unused import 2023-04-21 17:26:45 -07:00
Mikayla Maki
733abc9ed2 Revert previous change 2023-04-21 17:24:20 -07:00
Mikayla Maki
616188c541 Fix a bug where the character under a cursor could not reliably be selected 2023-04-21 17:15:29 -07:00
Mikayla Maki
8e0d359c63 Merge branch 'main' into fix-nondeterministic-terminal-test 2023-04-22 11:17:06 +12:00
Mikayla Maki
d841c3729b Wire through the gutter, rather than implicitly adding it 2023-04-21 16:12:33 -07:00
Mikayla Maki
23932b7e6c Fixed non-deterministic test failure and made mouse to cell conversion work correctly 2023-04-21 16:06:07 -07:00
Max Brunsfeld
06cb388beb Merge pull request #2404 from zed-industries/fix-command-palette
Stopgap: Fix missing actions in the command palette
2023-04-21 15:16:45 -07:00
Nathan Sobo
e6604d1641 Fix missing actions in the command palette
Previously, the workspace view was on the stack when we were computing the
available actions, which excluded it. This is a stopgap. We should find a
better solution ASAP.
2023-04-21 15:59:29 -06:00
Nathan Sobo
83bf3d071d WIP 2023-04-21 15:36:52 -06:00
Nathan Sobo
55db28e074 Eliminate ReadModel trait 2023-04-21 15:05:19 -06:00
Nathan Sobo
5dac95c47c Eliminate ReadView trait 2023-04-21 15:03:57 -06:00
Nathan Sobo
bce51c521a Merge pull request #2403 from zed-industries/into-element
Rename Element to AnyElement, Drawable to Element, and make containers generic over any Element type
2023-04-21 14:44:22 -06:00
Nathan Sobo
993dbf86cb Merge remote-tracking branch 'origin/main' into into-element 2023-04-21 14:27:25 -06:00
Nathan Sobo
09111b65d8 Merge pull request #2401 from zed-industries/simplify-context-lifetimes
Remove unnecessary lifetime parameter from WindowContext
2023-04-21 14:26:26 -06:00
Nathan Sobo
caf3d5c163 Fix more formatting 2023-04-21 14:25:49 -06:00
Nathan Sobo
c1810e8ec9 Fix formatting 2023-04-21 13:09:36 -06:00
Nathan Sobo
fe492eacbf Refine naming of element-related types and traits
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-04-21 13:06:37 -06:00
Antonio Scandurra
03619dfa55 Rename Drawable::boxed to into_element and make containers generic
Multi-element are now generic over any drawable child, which can be converted
into an element.

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-04-21 12:43:19 -06:00
Mikayla Maki
69273648b3 Merge pull request #2402 from zed-industries/fix-panics
Fix panic in pane.rs
2023-04-22 05:53:48 +12:00
Mikayla Maki
b8fd6435d7 Fix test covering this case 2023-04-21 10:48:34 -07:00
Mikayla Maki
aa2af53f56 Align return of close_active_item to match original format 2023-04-21 10:44:27 -07:00
Mikayla Maki
39512655aa Remove unused cyclic dependency from copilot crate
Move notification handling earlier so that there are less copilot messages in the log
2023-04-21 10:38:36 -07:00
Mikayla Maki
6ee0d104d6 Fix panic in remove active item 2023-04-21 10:08:52 -07:00
Petros Amoiridis
c9048b54c1 Merge pull request #2399 from zed-industries/petros/z-827-copy-the-newline-shortcuts-to-zed
Add newline shortcuts for popular editors
2023-04-21 19:09:10 +03:00
Antonio Scandurra
4ac894ffbe Remove unnecessary lifetime parameter from WindowContext
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-21 18:06:53 +02:00
Antonio Scandurra
a4fbcbf160 Merge pull request #2400 from zed-industries/fix-breadcrumbs-signature
Fix `ProjectDiagnosticsEditor::breadcrumbs` signature
2023-04-21 18:06:22 +02:00
Antonio Scandurra
4d433663bd Fix ProjectDiagnosticsEditor::breadcrumbs signature
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-21 17:54:26 +02:00
Antonio Scandurra
238ebafa48 Merge pull request #2372 from zed-industries/window_context_2
Give elements access to their parent views and simplify contexts
2023-04-21 17:32:14 +02:00
Julia
88406045f5 Merge pull request #2398 from zed-industries/fix-diagnostics-breadcrumbs
Render breadcrumbs for diagnostics multi-buffer
2023-04-21 11:31:35 -04:00
Petros Amoiridis
3992e95109 Add newline shortcuts for popular editors 2023-04-21 18:29:35 +03:00
Antonio Scandurra
f54a289b6f Remove handle to view from TerminalElement
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-21 16:56:06 +02:00
Antonio Scandurra
a860a6cd62 Make App::notify_view private
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-21 16:50:57 +02:00
Antonio Scandurra
8c7f821d14 Introduce a new WindowContext::remove_window API
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-21 16:39:53 +02:00
Petros Amoiridis
c3231047ad Merge pull request #2396 from zed-industries/petros/z-77-the-start-local-collaboration-script-no
Fix script/start-local-collaboration
2023-04-21 17:36:22 +03:00
Antonio Scandurra
f12746c4b7 Avoid double borrow of view in ProjectPanel drag and drop
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-21 16:23:47 +02:00
Antonio Scandurra
31e906d068 Avoid double borrow of views on up and up_out in DragAndDrop
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-21 16:19:51 +02:00
Petros Amoiridis
65c5605e68 Look for the main display always 2023-04-21 17:18:01 +03:00
Petros Amoiridis
3c54b14c5b Remove debugging information 2023-04-21 17:17:04 +03:00
Antonio Scandurra
5b40641fde Take a target view when marking an element as draggable 2023-04-21 15:52:05 +02:00
Antonio Scandurra
b0cbd13e7a Remove stray push_layer from EditorElement::paint 2023-04-21 11:46:13 +02:00
Antonio Scandurra
e5192a4853 Use update_default_global to filter command palette items for copilot 2023-04-21 11:07:36 +02:00
Antonio Scandurra
c76b9794e4 Merge branch 'main' into window_context_2 2023-04-21 10:58:08 +02:00
Antonio Scandurra
d32a7218cd Remove stray dbg! statements 2023-04-21 10:49:47 +02:00
Julia
c7cc5bca02 Render breadcrumbs for diagnostics multi-buffer 2023-04-20 23:39:45 -04:00
Mikayla Maki
8e4cc549dc Merge pull request #2397 from zed-industries/finally-fix-terminal-line-height
Create a new setting to adjust the line height in the terminal
2023-04-21 12:39:49 +12:00
Mikayla Maki
1fa52adabd Fix warning 2023-04-20 17:34:47 -07:00
Mikayla Maki
c72b70d4ae Fixed silly custom settings parsing 2023-04-20 17:33:06 -07:00
Mikayla Maki
f54ab73b47 Merge branch 'main' into finally-fix-terminal-line-height 2023-04-21 12:26:32 +12:00
Mikayla Maki
dfdc826015 Create a new setting to adjust the line height in the terminal 2023-04-20 17:24:27 -07:00
Nathan Sobo
1b2e480e1e Remove unnecessary replace 2023-04-20 15:26:48 -06:00
Nathan Sobo
0bce80b6f8 Fix remaining vim failures 2023-04-20 15:25:11 -06:00
Antonio Scandurra
137d9384b5 Initialize the active editor when vim mode is enabled
Instead of waiting for a focus event. This makes more tests pass.
2023-04-20 14:02:40 -06:00
Petros Amoiridis
7b4b1d6312 Use regex to make extraction bulletproof 2023-04-20 20:06:33 +03:00
Max Brunsfeld
abdfb5a451 collab 0.9.0 2023-04-20 09:52:32 -07:00
Max Brunsfeld
3a855184bc v0.84.x dev 2023-04-20 09:52:07 -07:00
Joseph T. Lyons
65f7228fed Merge pull request #2389 from zed-industries/save-panics-as-structured-data
Save panics as structured data
2023-04-20 12:51:00 -04:00
Max Brunsfeld
b414d43ee3 Merge pull request #2392 from zed-industries/eslint-multiple-language-server-adapters-rebased
Allow buffers to use multiple language servers
2023-04-20 09:40:23 -07:00
Petros Amoiridis
dcc804783c Install jq if it's missing 2023-04-20 19:06:23 +03:00
Petros Amoiridis
460ea8e16c Increase reliability and support multiple monitors 2023-04-20 19:06:23 +03:00
Petros Amoiridis
b11e239779 Make the script more reliable 2023-04-20 19:06:22 +03:00
Mikayla Maki
ad71020990 Merge pull request #2395 from zed-industries/remove-stable-hiding-copilot
Remove stable guard for copilot
2023-04-21 04:04:04 +12:00
Max Brunsfeld
21bb13d309 Fix comment formatting errors for rust 1.69 2023-04-20 09:03:42 -07:00
Max Brunsfeld
32c57bcd22 Store buffer's diagnostic sets in a smallvec 2023-04-20 08:58:41 -07:00
Max Brunsfeld
960a2bc589 Don't use ESLint for now 2023-04-20 08:58:41 -07:00
Max Brunsfeld
0ebe44bfd5 Handle multiple language servers for a given path in project diagnostics view 2023-04-20 08:58:41 -07:00
Max Brunsfeld
4dd917c123 Introduce a LanguageServerId wrapper type
Clarify the meaning of all the usizes in use in all of these
struct fields an method signatures.
2023-04-20 08:58:41 -07:00
Julia
c5f86bc6af Avoid language servers fighting over diagnostics summaries
Previously each server would stomp all over the existing results

Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-04-20 08:58:41 -07:00
Julia
9e2949e7ba Refactor language server startup
Avoid parallel vecs

Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-04-20 08:58:41 -07:00
Julia
c59204c5e6 Cleanup
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-04-20 08:58:41 -07:00
Julia
26abc824a9 Bump protocol version 2023-04-20 08:58:41 -07:00
Julia
df94aee758 Fix failing tests
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-04-20 08:58:41 -07:00
Julia
6156dbced0 Finish getting multiple diagnostics sources building and running 2023-04-20 08:58:41 -07:00
Julia
bb4de47b15 Start getting diagnostics sets to work with multiple servers
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-20 08:58:41 -07:00
Julia
2a5c0fa5f8 Get ESLint to launch and provide diagnostics
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-04-20 08:58:41 -07:00
Julia
6e68ff5a50 Get it to build with multiple adapters per language! 🎉
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-04-20 08:58:41 -07:00
Julia
ba7233f265 Incomplete refactor to allow for multiple adapters per language 2023-04-20 08:58:39 -07:00
Mikayla Maki
c1daf0fc36 Fix format 2023-04-20 08:54:44 -07:00
Mikayla Maki
ad8162fc9c Make sign_in init conditional 2023-04-20 08:36:29 -07:00
Mikayla Maki
f5bbb41cc2 Remove import 2023-04-20 08:34:50 -07:00
Mikayla Maki
5c8b41dd54 Remove stable guard for copilot 2023-04-20 08:33:45 -07:00
Antonio Scandurra
0d5eea8169 Track active window id in test platform
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-20 17:22:11 +02:00
Antonio Scandurra
d9bb37c649 Add WindowContext::update_default_global
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-20 17:13:13 +02:00
Antonio Scandurra
1d487e19f9 Fix editor tests
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-20 16:15:14 +02:00
Antonio Scandurra
c52b6328b7 Merge branch 'main' into window_context_2 2023-04-20 16:01:47 +02:00
Antonio Scandurra
e282c7ad45 Finish converting all the pickers to the new API 2023-04-20 15:26:05 +02:00
Petros Amoiridis
21e39e7523 Merge pull request #2394 from zed-industries/petros/z-804-change-default-settings-for-json-to-2
Set default tab_size for JSON to 2 and apply new formatting
2023-04-20 14:45:52 +03:00
Antonio Scandurra
370875b1d4 Merge pull request #2393 from zed-industries/relay-change-events-to-copilot
Relay buffer change events to Copilot language server
2023-04-20 13:34:42 +02:00
Petros Amoiridis
eca93c124a Apply the tab_size change to keymaps and settings 2023-04-20 14:08:30 +03:00
Petros Amoiridis
bed76462e2 Define tab_size equal to 2 in default settings 2023-04-20 14:06:43 +03:00
Antonio Scandurra
df71a9cfae Move buffer change reporting to a background task 2023-04-20 11:59:05 +02:00
Antonio Scandurra
4151bd39da Add buffer management test to Copilot 2023-04-20 10:51:50 +02:00
Antonio Scandurra
4d207981ae Notify LSP when Copilot suggestions are accepted/rejected 2023-04-20 10:15:31 +02:00
Antonio Scandurra
5d57167302 Make it easier to access a running/authenticated copilot server 2023-04-20 10:12:10 +02:00
Antonio Scandurra
4c3d6c854a Send editor information to copilot 2023-04-20 09:34:20 +02:00
Antonio Scandurra
b9a7b70e52 Register unknown buffer on the fly if completions are requested for it 2023-04-20 09:34:20 +02:00
Antonio Scandurra
34bcf6f072 Reopen file in Copilot language server when language or URI changes 2023-04-20 09:34:20 +02:00
Antonio Scandurra
672cf6b8c7 Relay buffer change events to Copilot 2023-04-20 09:34:20 +02:00
Nathan Sobo
d70644618a WIP: Make PickerDelegate a fully owned object instead of a view
This avoids issues with the parent view being on the stack when we want to
interact with the delegate from the picker. Still have several picker usages
to convert.
2023-04-19 22:05:29 -06:00
Mikayla Maki
ce8442a3d8 Fix underflow potential 2023-04-19 17:42:19 -07:00
Mikayla Maki
dd73233973 Merge pull request #2386 from zed-industries/copilot-shipping
Get copilot ready to ship
2023-04-20 12:38:27 +12:00
Mikayla Maki
26ab774b7f Removed debounce on suggestion cycling code 2023-04-19 17:34:09 -07:00
Mikayla Maki
f16b96cafc add copilot menu 2023-04-19 17:27:44 -07:00
Mikayla Maki
9b8a3e4de5 Fixed panic in new cycling code
Made cycling fire without debounce
2023-04-19 16:50:31 -07:00
Mikayla Maki
2882e0fa5b Remove new CTA in copilot sign in UI
Add a trim_end to copilot suggestions
2023-04-19 16:39:55 -07:00
Mikayla Maki
745e5e3a09 Add italic styles to copilot suggestions 2023-04-19 15:23:19 -07:00
Mikayla Maki
70ff4ca48f WIP: lower our usage of the copilot API by seperating out the cycling completion
Restore copilot setting visibility

co-authored-by: antonio <antonio@zed.dev>
2023-04-19 15:23:17 -07:00
Mikayla Maki
ea1c3fa7a0 Only fire completion cycling requests if specifically asked for by the user 2023-04-19 15:21:30 -07:00
Antonio Scandurra
8610f3acf3 Introduce a button to disable copilot integration
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-19 20:25:59 +02:00
Joseph Lyons
0326a45a91 Give closure parameter a name 2023-04-19 14:21:53 -04:00
Antonio Scandurra
54a78d7024 Clarify Copilot context menu
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-19 20:18:06 +02:00
Antonio Scandurra
4a9989fe38 Clear all suggestions from Editor when disabling Copilot
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-19 20:10:57 +02:00
Antonio Scandurra
1fd07b6fcf Clarify copilot settings
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-19 20:07:05 +02:00
Petros Amoiridis
699b2060b3 Merge pull request #2390 from zed-industries/petros/z-66-command-to-add-a-new-line-on-the
Add newline above and improve newline below
2023-04-19 21:02:55 +03:00
Petros Amoiridis
b3b8f8532d Assert the editor and unmarked texts are the same
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
2023-04-19 20:52:01 +03:00
Petros Amoiridis
f9c60b98c0 Add newline above and improve newline below
Add a new action for inserting a new line above the current line. @ForLoveOfCats  also helped fix a bug among other things. When two collaborators had their cursors at the end of a line, and one collaborator performed a newline below action, the second collaborator's cursor would be dragged to the new line. This is also fixing that.

Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>
2023-04-19 19:57:23 +03:00
Joseph Lyons
27a6bacab8 Save panics as structured data 2023-04-19 08:31:47 -04:00
Nathan Sobo
5514349b6b Add Component elements 2023-04-18 22:49:06 -06:00
Max Brunsfeld
c5e56a5e45 Fail the randomized test build after reporting the error to zed.dev 2023-04-18 18:41:33 -07:00
Max Brunsfeld
5934e882b8 Merge pull request #2379 from zed-industries/shebang
Select language based on a file's first content line in addition to its path
2023-04-18 17:31:19 -07:00
Max Brunsfeld
ad9fe79cf2 Merge pull request #2388 from zed-industries/cmd-o-new-window
When opening projects, only reuse the current window if it is empty
2023-04-18 17:31:08 -07:00
Max Brunsfeld
7cc868bc8c When opening projects, only reuse the current window if it is empty 2023-04-18 17:05:26 -07:00
Max Brunsfeld
44d26b69ae Merge pull request #2387 from zed-industries/panic-in-set-selections-from-remote
Fix 'invalid insertion' panic when following
2023-04-18 16:43:47 -07:00
Max Brunsfeld
bd7d50f339 Fix 'invalid insertion' panic when following
Wait for the necessary buffer operations to arrive before attempting to
set selections and scroll top.
2023-04-18 16:13:18 -07:00
Nate Butler
a8b3826955 Merge pull request #2384 from zed-industries/update-copilot-styles
Update copilot styles
2023-04-18 15:17:10 -04:00
Julia
4c086a4836 Merge pull request #2385 from zed-industries/rerender-breadcrumbs-on-focus-change
Re-render toolbar items when updating their knowledge of pane focus
2023-04-18 15:13:53 -04:00
Julia
721baf5746 Re-render toolbar items when updating their knowledge of pane focus 2023-04-18 14:56:39 -04:00
Nate Butler
957ab65422 Mix neutral and blue to make a predictive color that is unique 2023-04-18 14:47:52 -04:00
Joseph T. Lyons
614a9c8977 Merge pull request #2377 from zed-industries/add-tab-tooltips
Add tab tooltips
2023-04-18 14:37:38 -04:00
Nate Butler
ae0647c3a9 Update predictive color
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-04-18 14:29:08 -04:00
Joseph Lyons
304eddbbe4 Remove unnecessary lifetimes from tab_description 2023-04-18 14:15:56 -04:00
Joseph Lyons
9afd804062 Remove unnecessary lifetimes from tab_tooltip_text 2023-04-18 14:03:02 -04:00
Antonio Scandurra
eee39b4c5c Don't capture Workspace handle in defer when rendering Dock
GPUI already provides a mutable reference to the `Workspace` in the
`defer` callback.
2023-04-18 17:41:40 +02:00
Antonio Scandurra
136e599051 Merge branch 'main' into window_context_2 2023-04-18 17:29:37 +02:00
Antonio Scandurra
bcba11ba82 Reflect new behavior in test now that we don't replace windows anymore 2023-04-18 17:27:05 +02:00
Antonio Scandurra
d03c431f9a Fix warnings/errors now that AsyncAppContext::update returns Result 2023-04-18 14:58:57 +02:00
Antonio Scandurra
31e6bb4fc1 Return Result from AsyncAppContext::update_view 2023-04-18 12:03:53 +02:00
Antonio Scandurra
1b477c9e38 Merge pull request #2383 from zed-industries/show-copilot-more-often
Clean up completion tasks, even if they fail or return no results
2023-04-18 11:16:47 +02:00
Antonio Scandurra
d26d0ac56f Clean up completion tasks, even if they fail or return no results
This fixes a bug where leaving the completion task in `completion_tasks`
could cause the Copilot suggestion to not be shown due to the LSP not
successfully return a completion.
2023-04-18 11:03:17 +02:00
Antonio Scandurra
75d6b6360f Add failing test to demonstrate Copilot not showing enough suggestions 2023-04-18 10:24:20 +02:00
Max Brunsfeld
8f25b98e6f Print the final minimized test plan before sending it to zed.dev 2023-04-17 16:35:54 -07:00
Max Brunsfeld
695973d117 Use Node 18 on CI, for fetch API 2023-04-17 16:22:30 -07:00
Max Brunsfeld
516964280b Merge pull request #2376 from zed-industries/randomized-tests-runner
Add an Actions workflow that repeatedly runs the randomized integration tests
2023-04-17 15:45:45 -07:00
Max Brunsfeld
485c56e3bd Don't run randomized tests on pushes to main 2023-04-17 15:43:12 -07:00
Max Brunsfeld
837866f962 Consolidate logic for running randomized tests in scripts 2023-04-17 15:37:11 -07:00
Julia
4adc92b8e5 Merge pull request #2382 from zed-industries/dont-have-contacts-popover-affect-share-unshare-button
Don't have contacts popover affect appearance of Share/Unshare button
2023-04-17 14:18:39 -04:00
Julia
14ef0edd7f Don't have contacts popover affect appearance of Share/Unshare button 2023-04-17 14:13:28 -04:00
Max Brunsfeld
233cd80f63 Merge pull request #2381 from zed-industries/fix-buffer-latency
Send buffer operations in batches to reduce latency
2023-04-17 10:59:51 -07:00
Max Brunsfeld
5d73e646d8 Delete pull_request_template.md 2023-04-17 10:23:57 -07:00
Antonio Scandurra
1f284408a9 Send buffer operations in batches to reduce latency
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-04-17 10:22:13 -07:00
Julia
f5a2534c1b Merge pull request #2380 from zed-industries/show-followers-full-color
Unconditionally display followers in full color
2023-04-17 13:16:19 -04:00
Julia
61f4f8aaeb Unconditionally display followers in full color 2023-04-17 13:10:40 -04:00
Antonio Scandurra
493a418c91 WIP 2023-04-17 18:52:48 +02:00
Antonio Scandurra
38ab6b123f Make production code compile again 2023-04-17 12:33:33 +02:00
Antonio Scandurra
bed94455b9 Avoid calling focus_in when creating window
This is redundant because when creating a window we will receive
an activation event if the window was effectively focused. And if
it wasn't, we shouldn't call `focus_in`.
2023-04-17 11:57:51 +02:00
Max Brunsfeld
1dcd4717b1 Select language based on a file's first content line in addition to its path 2023-04-16 12:28:27 -07:00
Joseph Lyons
ebe57254e0 Add tab tooltips 2023-04-15 06:46:14 -04:00
Max Brunsfeld
3569c61784 Minimize randomized test failures before reporting issues 2023-04-14 17:51:24 -07:00
Max Brunsfeld
5c3da91e15 Report randomized test failures to zed.dev, to create issues in linear 2023-04-14 15:45:01 -07:00
Max Brunsfeld
c329546570 Extract randomized test CI process into a script 2023-04-14 14:25:55 -07:00
Max Brunsfeld
253411bfd0 Start work randomized test runner GH action workflow 2023-04-14 13:54:57 -07:00
Max Brunsfeld
e655a6c767 Merge pull request #2375 from zed-industries/worktree-scan-id-fix
Always bump worktree's scan_id when refreshing an entry
2023-04-14 09:53:40 -07:00
Antonio Scandurra
f09e21aa93 WIP: force using (at least) a window context to update views 2023-04-14 17:08:13 +02:00
Antonio Scandurra
a820862165 Take WindowContext in workspace::ItemHandle methods 2023-04-14 16:36:23 +02:00
Antonio Scandurra
a8e75a9b55 Remove weak handle from EditorElement to Editor 2023-04-14 14:29:03 +02:00
Antonio Scandurra
060242a28a Take a WindowContext in StatusItemViewHandle::set_active_pane_item 2023-04-14 14:26:20 +02:00
Antonio Scandurra
2652f65bee Fix handle_window_activation_effect updating the wrong view 2023-04-14 12:33:13 +02:00
Antonio Scandurra
98dce89379 Pass WindowContext to ViewHandle::is_focused 2023-04-14 12:12:08 +02:00
Antonio Scandurra
74ca223114 Fix formatting for the entire workspace 2023-04-14 12:08:33 +02:00
Antonio Scandurra
33bc47dbe2 Merge branch 'main' into window_context_2 2023-04-14 12:07:09 +02:00
Antonio Scandurra
183b9ef809 Make full-screen and window bounds callbacks take a WindowContext 2023-04-14 11:56:31 +02:00
Antonio Scandurra
7394bf1cdc Fix most of the warnings 2023-04-14 10:51:53 +02:00
Antonio Scandurra
5666e8301e Log an error when a scene fails to build 2023-04-14 10:32:56 +02:00
Antonio Scandurra
9ef79735dc Move more window-specific methods from AppContext to WindowContext 2023-04-14 10:29:35 +02:00
Antonio Scandurra
c62357db02 Fix window activation 2023-04-14 08:24:01 +02:00
Max Brunsfeld
5ea49b3ae3 Fix inconsistent worktree state when renaming entries while scanning 2023-04-13 22:34:34 -07:00
Max Brunsfeld
bb1cfd51b8 Add randomized test for mutating worktree during initial scan 2023-04-13 22:34:03 -07:00
Max Brunsfeld
debb694d97 Always bump scan_id when refreshing an entry
The scan_id needs to be bumped even if a scan is already in progress,
so that worktree updates can detect that entries have changed. This
means that the worktree's completed_scan_id may increase by more than
one at the end of a scan.
2023-04-13 16:51:11 -07:00
Max Brunsfeld
c13914bda1 Merge pull request #2371 from zed-industries/refresh-entry-delay
Restructure background scanner to handle refresh requests even while scanning directories
2023-04-13 12:44:04 -07:00
Joseph T. Lyons
6a75e884c0 Merge pull request #2374 from zed-industries/add-vim-mode-metric
Add vim mode metric
2023-04-13 13:52:31 -04:00
Joseph Lyons
5f0bf5929f Add vim mode metric 2023-04-13 13:46:22 -04:00
Antonio Scandurra
84d2605ccf Avoid passing cx when emitting from CallbackCollection
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-04-13 16:53:44 +02:00
Antonio Scandurra
ff774786bf Reimplement AppContext::handle_focus_effect 2023-04-13 16:21:39 +02:00
Antonio Scandurra
4900e04ff3 Fix window refreshing 2023-04-13 12:19:11 +02:00
Antonio Scandurra
0269a8699b Start removing some warnings 2023-04-13 12:04:37 +02:00
Antonio Scandurra
702c4ce403 Uncomment List tests and fix related problems in WindowContext 2023-04-13 12:01:18 +02:00
Antonio Scandurra
f4daeb4778 Merge pull request #2373 from zed-industries/fix-copilot-panic
Avoid interpolating Copilot suggestion if cursor excerpt differs
2023-04-13 10:48:57 +02:00
Antonio Scandurra
495c7acadf Avoid interpolating Copilot suggestion if cursor excerpt differs 2023-04-13 10:44:08 +02:00
Max Brunsfeld
5ca603dbeb Don't process gitignore updates after the initial scan 2023-04-12 18:17:29 -07:00
Max Brunsfeld
3d14bfd90c Prioritize path refresh requests over gitignore status updates 2023-04-12 18:08:00 -07:00
Max Brunsfeld
2d97387f49 Restructure background scanner to handle refresh requests even while scanning 2023-04-12 16:29:51 -07:00
Nathan Sobo
b89c4e06be WIP: Compiling, many warnings, haven't tested 2023-04-12 12:54:24 -06:00
Nathan Sobo
25ad635577 WIP 2023-04-12 12:38:26 -06:00
Nathan Sobo
4cb13fb39c WIP 2023-04-12 12:22:26 -06:00
Nathan Sobo
a25f962185 WIP 2023-04-12 12:13:35 -06:00
Max Brunsfeld
a85c2d71ad collab 0.8.3 2023-04-12 11:11:32 -07:00
Joseph Lyons
afbd275f4f v0.83.x dev 2023-04-12 13:31:39 -04:00
Nathan Sobo
40896352ff wip 2023-04-12 10:28:53 -06:00
Nathan Sobo
868301bedb WIP 2023-04-12 10:07:17 -06:00
Antonio Scandurra
83070a19c4 WIP 2023-04-12 15:55:43 +02:00
Nathan Sobo
b54f08db77 WIP 2023-04-12 06:51:03 -06:00
Nathan Sobo
d9e4136b02 WIP 2023-04-11 21:56:37 -06:00
Nathan Sobo
e6cc132b19 WIP 2023-04-11 18:48:00 -06:00
Nathan Sobo
e115baa60c WIP 2023-04-11 18:21:56 -06:00
Nathan Sobo
3de8fe0f87 WIP 2023-04-10 17:27:47 -06:00
Nathan Sobo
6638407ff9 WIP: Everything shredded 2023-04-10 16:10:32 -06:00
Nathan Sobo
7536645eea WIP 2023-04-08 08:01:05 -06:00
Nathan Sobo
9d23a98157 WIP 2023-04-08 06:43:39 -06:00
Nathan Sobo
2186de38ab Merge AppContext impl blocks 2023-04-07 11:54:08 -06:00
284 changed files with 23578 additions and 17901 deletions

View File

@@ -1,9 +0,0 @@
## Description of feature or change
## Link to related issues from zed or community
## Before Merging
- [ ] Does this have tests or have existing tests been updated to cover this change?
- [ ] Have you added the necessary settings to configure this feature?
- [ ] Has documentation been created or updated (including above changes to settings)?

View File

@@ -54,7 +54,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v2
with:
node-version: '16'
node-version: '18'
- name: Checkout repo
uses: actions/checkout@v2
@@ -102,7 +102,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v2
with:
node-version: '16'
node-version: '18'
- name: Checkout repo
uses: actions/checkout@v2

43
.github/workflows/randomized_tests.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: Randomized Tests
concurrency: randomized-tests
on:
push:
branches:
- randomized-tests-runner
schedule:
- cron: '0 * * * *'
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
ZED_SERVER_URL: https://zed.dev
ZED_CLIENT_SECRET_TOKEN: ${{ secrets.ZED_CLIENT_SECRET_TOKEN }}
jobs:
tests:
name: Run randomized tests
runs-on:
- self-hosted
- randomized-tests
steps:
- name: Install Rust
run: |
rustup set profile minimal
rustup update stable
- name: Install Node
uses: actions/setup-node@v2
with:
node-version: '18'
- name: Checkout repo
uses: actions/checkout@v2
with:
clean: false
submodules: 'recursive'
- name: Run randomized tests
run: script/randomized-test-ci

68
Cargo.lock generated
View File

@@ -148,9 +148,6 @@ name = "anyhow"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
dependencies = [
"backtrace",
]
[[package]]
name = "arrayref"
@@ -1192,7 +1189,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.8.2"
version = "0.12.0"
dependencies = [
"anyhow",
"async-tungstenite",
@@ -1213,6 +1210,7 @@ dependencies = [
"git",
"gpui",
"hyper",
"indoc",
"language",
"lazy_static",
"lipsum",
@@ -1340,21 +1338,23 @@ dependencies = [
"anyhow",
"async-compression",
"async-tar",
"clock",
"collections",
"context_menu",
"fs",
"futures 0.3.25",
"gpui",
"language",
"log",
"lsp",
"node_runtime",
"rpc",
"serde",
"serde_derive",
"settings",
"smol",
"theme",
"util",
"workspace",
]
[[package]]
@@ -1362,6 +1362,7 @@ name = "copilot_button"
version = "0.1.0"
dependencies = [
"anyhow",
"assets",
"context_menu",
"copilot",
"editor",
@@ -1830,6 +1831,7 @@ dependencies = [
"editor",
"gpui",
"language",
"lsp",
"postage",
"project",
"serde_json",
@@ -1959,18 +1961,13 @@ version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2"
[[package]]
name = "easy-parallel"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946"
[[package]]
name = "editor"
version = "0.1.0"
dependencies = [
"aho-corasick",
"anyhow",
"client",
"clock",
"collections",
"context_menu",
@@ -1982,6 +1979,7 @@ dependencies = [
"futures 0.3.25",
"fuzzy",
"git",
"glob",
"gpui",
"indoc",
"itertools",
@@ -1993,6 +1991,7 @@ dependencies = [
"parking_lot 0.11.2",
"postage",
"project",
"pulldown-cmark",
"rand 0.8.5",
"rpc",
"serde",
@@ -2152,6 +2151,7 @@ dependencies = [
"serde",
"serde_derive",
"settings",
"smallvec",
"sysinfo",
"theme",
"tree-sitter-markdown",
@@ -3366,6 +3366,7 @@ dependencies = [
"project",
"settings",
"theme",
"util",
"workspace",
]
@@ -3607,6 +3608,26 @@ dependencies = [
"url",
]
[[package]]
name = "lsp_log"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"editor",
"futures 0.3.25",
"gpui",
"language",
"lsp",
"project",
"serde",
"settings",
"theme",
"unindent",
"util",
"workspace",
]
[[package]]
name = "mach"
version = "0.3.2"
@@ -4686,6 +4707,7 @@ dependencies = [
"client",
"clock",
"collections",
"copilot",
"ctor",
"db",
"env_logger",
@@ -4697,6 +4719,7 @@ dependencies = [
"glob",
"gpui",
"ignore",
"itertools",
"language",
"lazy_static",
"log",
@@ -4704,7 +4727,6 @@ dependencies = [
"parking_lot 0.11.2",
"postage",
"pretty_assertions",
"pulldown-cmark",
"rand 0.8.5",
"regex",
"rpc",
@@ -5750,6 +5772,7 @@ dependencies = [
"collections",
"editor",
"futures 0.3.25",
"glob",
"gpui",
"language",
"log",
@@ -5867,15 +5890,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_path_to_error"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "184c643044780f7ceb59104cef98a5a6f12cb2288a7bc701ab93a362b49fd47d"
dependencies = [
"serde",
]
[[package]]
name = "serde_repr"
version = "0.1.9"
@@ -5941,15 +5955,16 @@ dependencies = [
"collections",
"fs",
"futures 0.3.25",
"glob",
"gpui",
"json_comments",
"lazy_static",
"postage",
"pretty_assertions",
"schemars",
"serde",
"serde_derive",
"serde_json",
"serde_path_to_error",
"sqlez",
"staff_mode",
"theme",
@@ -6671,7 +6686,6 @@ dependencies = [
"serde",
"serde_derive",
"serde_json",
"serde_path_to_error",
"toml",
]
@@ -7233,7 +7247,7 @@ dependencies = [
[[package]]
name = "tree-sitter-json"
version = "0.20.0"
source = "git+https://github.com/tree-sitter/tree-sitter-json?rev=137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8#137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8"
source = "git+https://github.com/tree-sitter/tree-sitter-json?rev=40a81c01a40ac48744e0c8ccabbaba1920441199#40a81c01a40ac48744e0c8ccabbaba1920441199"
dependencies = [
"cc",
"tree-sitter",
@@ -8430,6 +8444,7 @@ name = "workspace"
version = "0.1.0"
dependencies = [
"anyhow",
"assets",
"async-recursion 1.0.0",
"bincode",
"call",
@@ -8516,7 +8531,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "zed"
version = "0.82.0"
version = "0.86.1"
dependencies = [
"activity_indicator",
"anyhow",
@@ -8542,7 +8557,6 @@ dependencies = [
"ctor",
"db",
"diagnostics",
"easy-parallel",
"editor",
"env_logger",
"feedback",
@@ -8565,6 +8579,7 @@ dependencies = [
"libc",
"log",
"lsp",
"lsp_log",
"node_runtime",
"num_cpus",
"outline",
@@ -8584,7 +8599,6 @@ dependencies = [
"serde",
"serde_derive",
"serde_json",
"serde_path_to_error",
"settings",
"simplelog",
"smallvec",

View File

@@ -35,6 +35,7 @@ members = [
"crates/live_kit_client",
"crates/live_kit_server",
"crates/lsp",
"crates/lsp_log",
"crates/media",
"crates/menu",
"crates/node_runtime",
@@ -71,11 +72,28 @@ default-members = ["crates/zed"]
resolver = "2"
[workspace.dependencies]
anyhow = { version = "1.0.57" }
async-trait = { version = "0.1" }
ctor = { version = "0.1" }
env_logger = { version = "0.9" }
futures = { version = "0.3" }
glob = { version = "0.3.1" }
lazy_static = { version = "1.4.0" }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
ordered-float = { version = "2.1.1" }
parking_lot = { version = "0.11.1" }
postage = { version = "0.5", features = ["futures-traits"] }
rand = { version = "0.8.5" }
regex = { version = "1.5" }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
rand = { version = "0.8" }
postage = { version = "0.5", features = ["futures-traits"] }
smallvec = { version = "1.6", features = ["union"] }
smol = { version = "1.2" }
tempdir = { version = "0.3.7" }
thiserror = { version = "1.0.29" }
time = { version = "0.3", features = ["serde", "serde-well-known"] }
unindent = { version = "0.1.7" }
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" }

View File

@@ -2,7 +2,7 @@
[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
Welcome to Zed, a lightning-fast, collaborative code editor that makes your dreams come true.
Welcome to Zed, a lightning-fast, collaborative code editor that makes your dreams come true.
## Development tips
@@ -31,7 +31,8 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
* 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.
Create a personal GitHub token to run `script/bootstrap` once successfully: the token needs to have an access to private repositories for the script to work (`repo` OAuth scope).
Then delete that token.
```
GITHUB_TOKEN=<$token> script/bootstrap

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="M4.75 1.875C4.75 2.71406 4.19922 3.42422 3.4375 3.66328V5.97891C3.9086 5.64609 4.4711 5.4375 5.125 5.4375H7.375C8.30782 5.4375 9.0625 4.68281 9.0625 3.75V3.66328C8.30078 3.42422 7.75 2.71406 7.75 1.875C7.75 0.839531 8.58907 0 9.625 0C10.6609 0 11.5 0.839531 11.5 1.875C11.5 2.71406 10.9492 3.42422 10.1875 3.66328V3.75C10.1875 5.30391 8.92891 6.5625 7.375 6.5625H5.125C4.19219 6.5625 3.4375 7.31719 3.4375 8.25V8.33672C4.19922 8.57578 4.75 9.28594 4.75 10.125C4.75 11.1609 3.91094 12 2.875 12C1.83953 12 1 11.1609 1 10.125C1 9.28594 1.55172 8.57578 2.3125 8.33672V3.66328C1.55172 3.42422 1 2.71406 1 1.875C1 0.839531 1.83953 0 2.875 0C3.91094 0 4.75 0.839531 4.75 1.875ZM2.875 2.625C3.28914 2.625 3.625 2.28914 3.625 1.875C3.625 1.46086 3.28914 1.125 2.875 1.125C2.46086 1.125 2.125 1.46086 2.125 1.875C2.125 2.28914 2.46086 2.625 2.875 2.625ZM9.625 1.125C9.21016 1.125 8.875 1.46086 8.875 1.875C8.875 2.28914 9.21016 2.625 9.625 2.625C10.0398 2.625 10.375 2.28914 10.375 1.875C10.375 1.46086 10.0398 1.125 9.625 1.125ZM2.875 10.875C3.28914 10.875 3.625 10.5398 3.625 10.125C3.625 9.71016 3.28914 9.375 2.875 9.375C2.46086 9.375 2.125 9.71016 2.125 10.125C2.125 10.5398 2.46086 10.875 2.875 10.875Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -18,7 +18,9 @@
],
"ctrl-shift-down": "editor::AddSelectionBelow",
"ctrl-shift-up": "editor::AddSelectionAbove",
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine"
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
"cmd-shift-enter": "editor::NewlineAbove",
"cmd-enter": "editor::NewlineBelow"
}
},
{

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@
"cmd-pagedown": "editor::MovePageDown",
"cmd-pageup": "editor::MovePageUp",
"ctrl-alt-shift-b": "editor::SelectToPreviousWordStart",
"cmd-alt-enter": "editor::NewlineAbove",
"shift-enter": "editor::NewlineBelow",
"cmd--": "editor::Fold",
"cmd-=": "editor::UnfoldLines",

View File

@@ -24,7 +24,9 @@
"ctrl-.": "editor::GoToHunk",
"ctrl-,": "editor::GoToPrevHunk",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd"
"ctrl-delete": "editor::DeleteToNextWordEnd",
"cmd-shift-enter": "editor::NewlineAbove",
"cmd-enter": "editor::NewlineBelow"
}
},
{

View File

@@ -12,6 +12,7 @@
"ctrl-shift-d": "editor::DuplicateLine",
"cmd-b": "editor::GoToDefinition",
"cmd-j": "editor::ScrollCursorCenter",
"cmd-alt-enter": "editor::NewlineAbove",
"cmd-enter": "editor::NewlineBelow",
"cmd-shift-l": "editor::SelectLine",
"cmd-shift-t": "outline::Toggle",

View File

@@ -1,325 +1,325 @@
[
{
"context": "Editor && VimControl && !VimWaiting",
"bindings": {
"g": [
"vim::PushOperator",
{
"Namespace": "G"
}
],
"i": [
"vim::PushOperator",
{
"Object": {
"around": false
}
}
],
"a": [
"vim::PushOperator",
{
"Object": {
"around": true
}
}
],
"h": "vim::Left",
"backspace": "vim::Backspace",
"j": "vim::Down",
"enter": "vim::NextLineStart",
"k": "vim::Up",
"l": "vim::Right",
"$": "vim::EndOfLine",
"shift-g": "vim::EndOfDocument",
"w": "vim::NextWordStart",
"shift-w": [
"vim::NextWordStart",
{
"ignorePunctuation": true
}
],
"e": "vim::NextWordEnd",
"shift-e": [
"vim::NextWordEnd",
{
"ignorePunctuation": true
}
],
"b": "vim::PreviousWordStart",
"shift-b": [
"vim::PreviousWordStart",
{
"ignorePunctuation": true
}
],
"%": "vim::Matching",
"ctrl-y": [
"vim::Scroll",
"LineUp"
],
"f": [
"vim::PushOperator",
{
"FindForward": {
"before": false
}
}
],
"t": [
"vim::PushOperator",
{
"FindForward": {
"before": true
}
}
],
"shift-f": [
"vim::PushOperator",
{
"FindBackward": {
"after": false
}
}
],
"shift-t": [
"vim::PushOperator",
{
"FindBackward": {
"after": true
}
}
],
"escape": "editor::Cancel",
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
"1": [
"vim::Number",
1
],
"2": [
"vim::Number",
2
],
"3": [
"vim::Number",
3
],
"4": [
"vim::Number",
4
],
"5": [
"vim::Number",
5
],
"6": [
"vim::Number",
6
],
"7": [
"vim::Number",
7
],
"8": [
"vim::Number",
8
],
"9": [
"vim::Number",
9
]
{
"context": "Editor && VimControl && !VimWaiting",
"bindings": {
"g": [
"vim::PushOperator",
{
"Namespace": "G"
}
},
{
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
"bindings": {
"c": [
"vim::PushOperator",
"Change"
],
"shift-c": "vim::ChangeToEndOfLine",
"d": [
"vim::PushOperator",
"Delete"
],
"shift-d": "vim::DeleteToEndOfLine",
"y": [
"vim::PushOperator",
"Yank"
],
"z": [
"vim::PushOperator",
{
"Namespace": "Z"
}
],
"i": [
"vim::SwitchMode",
"Insert"
],
"shift-i": "vim::InsertFirstNonWhitespace",
"a": "vim::InsertAfter",
"shift-a": "vim::InsertEndOfLine",
"x": "vim::DeleteRight",
"shift-x": "vim::DeleteLeft",
"^": "vim::FirstNonWhitespace",
"o": "vim::InsertLineBelow",
"shift-o": "vim::InsertLineAbove",
"v": [
"vim::SwitchMode",
{
"Visual": {
"line": false
}
}
],
"shift-v": [
"vim::SwitchMode",
{
"Visual": {
"line": true
}
}
],
"p": "vim::Paste",
"u": "editor::Undo",
"ctrl-r": "editor::Redo",
"ctrl-o": "pane::GoBack",
"/": [
"buffer_search::Deploy",
{
"focus": true
}
],
"ctrl-f": [
"vim::Scroll",
"PageDown"
],
"ctrl-b": [
"vim::Scroll",
"PageUp"
],
"ctrl-d": [
"vim::Scroll",
"HalfPageDown"
],
"ctrl-u": [
"vim::Scroll",
"HalfPageUp"
],
"ctrl-e": [
"vim::Scroll",
"LineDown"
],
"r": [
"vim::PushOperator",
"Replace"
]
],
"i": [
"vim::PushOperator",
{
"Object": {
"around": false
}
}
},
{
"context": "Editor && vim_operator == n",
"bindings": {
"0": [
"vim::Number",
0
]
],
"a": [
"vim::PushOperator",
{
"Object": {
"around": true
}
}
},
{
"context": "Editor && vim_operator == g",
"bindings": {
"g": "vim::StartOfDocument",
"h": "editor::Hover",
"escape": [
"vim::SwitchMode",
"Normal"
],
"d": "editor::GoToDefinition"
],
"h": "vim::Left",
"backspace": "vim::Backspace",
"j": "vim::Down",
"enter": "vim::NextLineStart",
"k": "vim::Up",
"l": "vim::Right",
"$": "vim::EndOfLine",
"shift-g": "vim::EndOfDocument",
"w": "vim::NextWordStart",
"shift-w": [
"vim::NextWordStart",
{
"ignorePunctuation": true
}
},
{
"context": "Editor && vim_operator == c",
"bindings": {
"c": "vim::CurrentLine"
],
"e": "vim::NextWordEnd",
"shift-e": [
"vim::NextWordEnd",
{
"ignorePunctuation": true
}
},
{
"context": "Editor && vim_operator == d",
"bindings": {
"d": "vim::CurrentLine"
],
"b": "vim::PreviousWordStart",
"shift-b": [
"vim::PreviousWordStart",
{
"ignorePunctuation": true
}
},
{
"context": "Editor && vim_operator == y",
"bindings": {
"y": "vim::CurrentLine"
],
"%": "vim::Matching",
"ctrl-y": [
"vim::Scroll",
"LineUp"
],
"f": [
"vim::PushOperator",
{
"FindForward": {
"before": false
}
}
},
{
"context": "Editor && vim_operator == z",
"bindings": {
"t": "editor::ScrollCursorTop",
"z": "editor::ScrollCursorCenter",
"b": "editor::ScrollCursorBottom",
"escape": [
"vim::SwitchMode",
"Normal"
]
],
"t": [
"vim::PushOperator",
{
"FindForward": {
"before": true
}
}
},
{
"context": "Editor && VimObject",
"bindings": {
"w": "vim::Word",
"shift-w": [
"vim::Word",
{
"ignorePunctuation": true
}
],
"s": "vim::Sentence",
"'": "vim::Quotes",
"`": "vim::BackQuotes",
"\"": "vim::DoubleQuotes",
"(": "vim::Parentheses",
")": "vim::Parentheses",
"[": "vim::SquareBrackets",
"]": "vim::SquareBrackets",
"{": "vim::CurlyBrackets",
"}": "vim::CurlyBrackets",
"<": "vim::AngleBrackets",
">": "vim::AngleBrackets"
],
"shift-f": [
"vim::PushOperator",
{
"FindBackward": {
"after": false
}
}
},
{
"context": "Editor && vim_mode == visual && !VimWaiting",
"bindings": {
"u": "editor::Undo",
"c": "vim::VisualChange",
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",
"y": "vim::VisualYank",
"p": "vim::VisualPaste",
"r": [
"vim::PushOperator",
"Replace"
]
}
},
{
"context": "Editor && vim_mode == insert",
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore"
}
},
{
"context": "Editor && VimWaiting",
"bindings": {
"tab": "vim::Tab",
"enter": "vim::Enter",
"escape": "editor::Cancel"
],
"shift-t": [
"vim::PushOperator",
{
"FindBackward": {
"after": true
}
}
],
"escape": "editor::Cancel",
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
"1": [
"vim::Number",
1
],
"2": [
"vim::Number",
2
],
"3": [
"vim::Number",
3
],
"4": [
"vim::Number",
4
],
"5": [
"vim::Number",
5
],
"6": [
"vim::Number",
6
],
"7": [
"vim::Number",
7
],
"8": [
"vim::Number",
8
],
"9": [
"vim::Number",
9
]
}
]
},
{
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
"bindings": {
"c": [
"vim::PushOperator",
"Change"
],
"shift-c": "vim::ChangeToEndOfLine",
"d": [
"vim::PushOperator",
"Delete"
],
"shift-d": "vim::DeleteToEndOfLine",
"y": [
"vim::PushOperator",
"Yank"
],
"z": [
"vim::PushOperator",
{
"Namespace": "Z"
}
],
"i": [
"vim::SwitchMode",
"Insert"
],
"shift-i": "vim::InsertFirstNonWhitespace",
"a": "vim::InsertAfter",
"shift-a": "vim::InsertEndOfLine",
"x": "vim::DeleteRight",
"shift-x": "vim::DeleteLeft",
"^": "vim::FirstNonWhitespace",
"o": "vim::InsertLineBelow",
"shift-o": "vim::InsertLineAbove",
"v": [
"vim::SwitchMode",
{
"Visual": {
"line": false
}
}
],
"shift-v": [
"vim::SwitchMode",
{
"Visual": {
"line": true
}
}
],
"p": "vim::Paste",
"u": "editor::Undo",
"ctrl-r": "editor::Redo",
"ctrl-o": "pane::GoBack",
"/": [
"buffer_search::Deploy",
{
"focus": true
}
],
"ctrl-f": [
"vim::Scroll",
"PageDown"
],
"ctrl-b": [
"vim::Scroll",
"PageUp"
],
"ctrl-d": [
"vim::Scroll",
"HalfPageDown"
],
"ctrl-u": [
"vim::Scroll",
"HalfPageUp"
],
"ctrl-e": [
"vim::Scroll",
"LineDown"
],
"r": [
"vim::PushOperator",
"Replace"
]
}
},
{
"context": "Editor && vim_operator == n",
"bindings": {
"0": [
"vim::Number",
0
]
}
},
{
"context": "Editor && vim_operator == g",
"bindings": {
"g": "vim::StartOfDocument",
"h": "editor::Hover",
"escape": [
"vim::SwitchMode",
"Normal"
],
"d": "editor::GoToDefinition"
}
},
{
"context": "Editor && vim_operator == c",
"bindings": {
"c": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == d",
"bindings": {
"d": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == y",
"bindings": {
"y": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == z",
"bindings": {
"t": "editor::ScrollCursorTop",
"z": "editor::ScrollCursorCenter",
"b": "editor::ScrollCursorBottom",
"escape": [
"vim::SwitchMode",
"Normal"
]
}
},
{
"context": "Editor && VimObject",
"bindings": {
"w": "vim::Word",
"shift-w": [
"vim::Word",
{
"ignorePunctuation": true
}
],
"s": "vim::Sentence",
"'": "vim::Quotes",
"`": "vim::BackQuotes",
"\"": "vim::DoubleQuotes",
"(": "vim::Parentheses",
")": "vim::Parentheses",
"[": "vim::SquareBrackets",
"]": "vim::SquareBrackets",
"{": "vim::CurlyBrackets",
"}": "vim::CurlyBrackets",
"<": "vim::AngleBrackets",
">": "vim::AngleBrackets"
}
},
{
"context": "Editor && vim_mode == visual && !VimWaiting",
"bindings": {
"u": "editor::Undo",
"c": "vim::VisualChange",
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",
"y": "vim::VisualYank",
"p": "vim::VisualPaste",
"r": [
"vim::PushOperator",
"Replace"
]
}
},
{
"context": "Editor && vim_mode == insert",
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore"
}
},
{
"context": "Editor && VimWaiting",
"bindings": {
"tab": "vim::Tab",
"enter": "vim::Enter",
"escape": "editor::Cancel"
}
}
]

View File

@@ -1,251 +1,287 @@
{
// The name of the Zed theme to use for the UI
"theme": "One Dark",
// The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Mono",
// The OpenType features to enable for text in the editor.
"buffer_font_features": {
// Disable ligatures:
// "calt": false
},
// The default font size for text in the editor
"buffer_font_size": 15,
// The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes.
"active_pane_magnification": 1.0,
// Enable / disable copilot integration.
"enable_copilot_integration": true,
// Controls whether copilot provides suggestion immediately
// or waits for a `copilot::Toggle`
"copilot": "on",
// Whether to enable vim modes and key bindings
"vim_mode": false,
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
"hover_popover_enabled": true,
// Whether to confirm before quitting Zed.
"confirm_quit": false,
// Whether the cursor blinks in the editor.
"cursor_blink": true,
// 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 shown in the os status bar.
"show_call_status_icon": true,
// Whether to use language servers to provide code intelligence.
"enable_language_server": true,
// When to automatically save edited buffers. This setting can
// take four values.
//
// 1. Never automatically save:
// "autosave": "off",
// 2. Save when changing focus away from the Zed window:
// "autosave": "on_window_change",
// 3. Save when changing focus away from a specific buffer:
// "autosave": "on_focus_change",
// 4. Save when idle for a certain amount of time:
// "autosave": { "after_delay": {"milliseconds": 500} },
"autosave": "off",
// Where to place the dock by default. This setting can take three
// values:
//
// 1. Position the dock attached to the bottom of the workspace
// "default_dock_anchor": "bottom"
// 2. Position the dock to the right of the workspace like a side panel
// "default_dock_anchor": "right"
// 3. Position the dock full screen over the entire workspace"
// "default_dock_anchor": "expanded"
"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:
//
// 1. Format code using the current language server:
// "format_on_save": "language_server"
// 2. Format code using an external command:
// "format_on_save": {
// "external": {
// "command": "prettier",
// "arguments": ["--stdin-filepath", "{buffer_path}"]
// }
// The name of the Zed theme to use for the UI
"theme": "One Dark",
// Features that can be globally enabled or disabled
"features": {
// Show Copilot icon in status bar
"copilot": true
},
// The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Mono",
// The OpenType features to enable for text in the editor.
"buffer_font_features": {
// Disable ligatures:
// "calt": false
},
// The default font size for text in the editor
"buffer_font_size": 15,
// The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes.
"active_pane_magnification": 1.0,
// Whether to enable vim modes and key bindings
"vim_mode": false,
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
"hover_popover_enabled": true,
// Whether to confirm before quitting Zed.
"confirm_quit": false,
// Whether the cursor blinks in the editor.
"cursor_blink": true,
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
// Controls whether copilot provides suggestion immediately
// or waits for a `copilot::Toggle`
"show_copilot_suggestions": true,
// Whether to show tabs and spaces in the editor.
// This setting can take two values:
//
// 1. Draw tabs and spaces only for the selected text (default):
// "selection"
// 2. Do not draw any tabs or spaces:
// "none"
// 3. Draw all invisible symbols:
// "all"
"show_whitespaces": "selection",
// Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true,
// Whether to use language servers to provide code intelligence.
"enable_language_server": true,
// When to automatically save edited buffers. This setting can
// take four values.
//
// 1. Never automatically save:
// "autosave": "off",
// 2. Save when changing focus away from the Zed window:
// "autosave": "on_window_change",
// 3. Save when changing focus away from a specific buffer:
// "autosave": "on_focus_change",
// 4. Save when idle for a certain amount of time:
// "autosave": { "after_delay": {"milliseconds": 500} },
"autosave": "off",
// Where to place the dock by default. This setting can take three
// values:
//
// 1. Position the dock attached to the bottom of the workspace
// "default_dock_anchor": "bottom"
// 2. Position the dock to the right of the workspace like a side panel
// "default_dock_anchor": "right"
// 3. Position the dock full screen over the entire workspace"
// "default_dock_anchor": "expanded"
"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:
//
// 1. Format code using the current language server:
// "format_on_save": "language_server"
// 2. Format code using an external command:
// "format_on_save": {
// "external": {
// "command": "prettier",
// "arguments": ["--stdin-filepath", "{buffer_path}"]
// }
// }
"formatter": "language_server",
// How to soft-wrap long lines of text. This setting can take
// three values:
//
// 1. Do not soft wrap.
// "soft_wrap": "none",
// 2. Soft wrap lines that overflow the editor:
// "soft_wrap": "editor_width",
// 3. Soft wrap lines at the preferred line length
// "soft_wrap": "preferred_line_length",
"soft_wrap": "none",
// The column at which to soft-wrap lines, for buffers where soft-wrap
// is enabled.
"preferred_line_length": 80,
// Whether to indent lines using tab characters, as opposed to multiple
// spaces.
"hard_tabs": false,
// How many columns a tab should occupy.
"tab_size": 4,
// 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:
// 1. Show the gutter
// "git_gutter": "tracked_files"
// 2. Hide the gutter
// "git_gutter": "hide"
"git_gutter": "tracked_files"
},
"copilot": {
// The set of glob patterns for which copilot should be disabled
// in any matching file.
"disabled_globs": [
".env"
]
},
// Settings specific to journaling
"journal": {
// The path of the directory where journal entries are stored
"path": "~",
// What format to display the hours in
// May take 2 values:
// 1. hour12
// 2. hour24
"hour_format": "hour12"
},
// Settings specific to the terminal
"terminal": {
// What shell to use when opening a terminal. May take 3 values:
// 1. Use the system's default terminal configuration in /etc/passwd
// "shell": "system"
// 2. A program:
// "shell": {
// "program": "sh"
// }
// 3. A program with arguments:
// "shell": {
// "with_arguments": {
// "program": "/bin/bash",
// "arguments": ["--login"]
// }
// }
"formatter": "language_server",
// How to soft-wrap long lines of text. This setting can take
// three values:
"shell": "system",
// What working directory to use when launching the terminal.
// May take 4 values:
// 1. Use the current file's project directory. Will Fallback to the
// first project directory strategy if unsuccessful
// "working_directory": "current_project_directory"
// 2. Use the first project in this workspace's directory
// "working_directory": "first_project_directory"
// 3. Always use this platform's home directory (if we can find it)
// "working_directory": "always_home"
// 4. Always use a specific directory. This value will be shell expanded.
// If this path is not a valid directory the terminal will default to
// this platform's home directory (if we can find it)
// "working_directory": {
// "always": {
// "directory": "~/zed/projects/"
// }
// }
//
// 1. Do not soft wrap.
// "soft_wrap": "none",
// 2. Soft wrap lines that overflow the editor:
// "soft_wrap": "editor_width",
// 3. Soft wrap lines at the preferred line length
// "soft_wrap": "preferred_line_length",
"soft_wrap": "none",
// The column at which to soft-wrap lines, for buffers where soft-wrap
// is enabled.
"preferred_line_length": 80,
// Whether to indent lines using tab characters, as opposed to multiple
// spaces.
"hard_tabs": false,
// How many columns a tab should occupy.
"tab_size": 4,
// 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
//
"working_directory": "current_project_directory",
// Set the cursor blinking behavior in the terminal.
// May take 4 values:
// 1. Never blink the cursor, ignoring the terminal mode
// "blinking": "off",
// 2. Default the cursor blink to off, but allow the terminal to
// set blinking
// "blinking": "terminal_controlled",
// 3. Always blink the cursor, ignoring the terminal mode
// "blinking": "on",
"blinking": "terminal_controlled",
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
// Alternate Scroll mode converts mouse scroll events into up / down key
// presses when in the alternate screen (e.g. when running applications
// like vim or less). The terminal can still set and unset this mode.
// May take 2 values:
// 1. Default alternate scroll mode to on
// "alternate_scroll": "on",
// 2. Default alternate scroll mode to off
// "alternate_scroll": "off",
"alternate_scroll": "off",
// Set whether the option key behaves as the meta key.
// May take 2 values:
// 1. Rely on default platform handling of option key, on macOS
// this means generating certain unicode characters
// "option_to_meta": false,
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
// "option_to_meta": true,
"option_as_meta": false,
// Whether or not selecting text in the terminal will automatically
// copy to the system clipboard.
"copy_on_select": false,
// Any key-value pairs added to this list will be added to the terminal's
// enviroment. Use `:` to seperate multiple values.
"env": {
// "KEY": "value1:value2"
},
// Automatically update Zed
"auto_update": true,
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
// 1. Show the gutter
// "git_gutter": "tracked_files"
// 2. Hide the gutter
// "git_gutter": "hide"
"git_gutter": "tracked_files"
// Set the terminal's line height.
// May take 3 values:
// 1. Use a line height that's comfortable for reading, 1.618
// "line_height": "comfortable"
// 2. Use a standard line height, 1.3. This option is useful for TUIs,
// particularly if they use box characters
// "line_height": "standard",
// 3. Use a custom line height.
// "line_height": {
// "custom": 2
// },
//
"line_height": "comfortable"
// Set the terminal's font size. If this option is not included,
// the terminal will default to matching the buffer's font size.
// "font_size": "15"
// Set the terminal's font family. If this option is not included,
// the terminal will default to matching the buffer's font family.
// "font_family": "Zed Mono"
},
// Different settings for specific languages.
"languages": {
"Plain Text": {
"soft_wrap": "preferred_line_length"
},
// Settings specific to journaling
"journal": {
// The path of the directory where journal entries are stored
"path": "~",
// What format to display the hours in
// May take 2 values:
// 1. hour12
// 2. hour24
"hour_format": "hour12"
"Elixir": {
"tab_size": 2
},
// Settings specific to the terminal
"terminal": {
// What shell to use when opening a terminal. May take 3 values:
// 1. Use the system's default terminal configuration in /etc/passwd
// "shell": "system"
// 2. A program:
// "shell": {
// "program": "sh"
// }
// 3. A program with arguments:
// "shell": {
// "with_arguments": {
// "program": "/bin/bash",
// "arguments": ["--login"]
// }
// }
"shell": "system",
// What working directory to use when launching the terminal.
// May take 4 values:
// 1. Use the current file's project directory. Will Fallback to the
// first project directory strategy if unsuccessful
// "working_directory": "current_project_directory"
// 2. Use the first project in this workspace's directory
// "working_directory": "first_project_directory"
// 3. Always use this platform's home directory (if we can find it)
// "working_directory": "always_home"
// 4. Always use a specific directory. This value will be shell expanded.
// If this path is not a valid directory the terminal will default to
// this platform's home directory (if we can find it)
// "working_directory": {
// "always": {
// "directory": "~/zed/projects/"
// }
// }
//
//
"working_directory": "current_project_directory",
// Set the cursor blinking behavior in the terminal.
// May take 4 values:
// 1. Never blink the cursor, ignoring the terminal mode
// "blinking": "off",
// 2. Default the cursor blink to off, but allow the terminal to
// set blinking
// "blinking": "terminal_controlled",
// 3. Always blink the cursor, ignoring the terminal mode
// "blinking": "on",
"blinking": "terminal_controlled",
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
// Alternate Scroll mode converts mouse scroll events into up / down key
// presses when in the alternate screen (e.g. when running applications
// like vim or less). The terminal can still set and unset this mode.
// May take 2 values:
// 1. Default alternate scroll mode to on
// "alternate_scroll": "on",
// 2. Default alternate scroll mode to off
// "alternate_scroll": "off",
"alternate_scroll": "off",
// Set whether the option key behaves as the meta key.
// May take 2 values:
// 1. Rely on default platform handling of option key, on macOS
// this means generating certain unicode characters
// "option_to_meta": false,
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
// "option_to_meta": true,
"option_as_meta": false,
// Whether or not selecting text in the terminal will automatically
// copy to the system clipboard.
"copy_on_select": false,
// Any key-value pairs added to this list will be added to the terminal's
// enviroment. Use `:` to seperate multiple values.
"env": {
// "KEY": "value1:value2"
}
// Set the terminal's font size. If this option is not included,
// the terminal will default to matching the buffer's font size.
// "font_size": "15"
// Set the terminal's font family. If this option is not included,
// the terminal will default to matching the buffer's font family.
// "font_family": "Zed Mono"
"Go": {
"tab_size": 4,
"hard_tabs": true
},
// Different settings for specific languages.
"languages": {
"Plain Text": {
"soft_wrap": "preferred_line_length"
},
"Elixir": {
"tab_size": 2
},
"Go": {
"tab_size": 4,
"hard_tabs": true
},
"Markdown": {
"soft_wrap": "preferred_line_length"
},
"JavaScript": {
"tab_size": 2
},
"TypeScript": {
"tab_size": 2
},
"TSX": {
"tab_size": 2
},
"YAML": {
"tab_size": 2
}
"Markdown": {
"soft_wrap": "preferred_line_length"
},
// LSP Specific settings.
"lsp": {
// Specify the LSP name as a key here.
// As of 8/10/22, supported LSPs are:
// pyright
// gopls
// rust-analyzer
// typescript-language-server
// vscode-json-languageserver
// "rust-analyzer": {
// //These initialization options are merged into Zed's defaults
// "initialization_options": {
// "checkOnSave": {
// "command": "clippy"
// }
// }
// }
"JavaScript": {
"tab_size": 2
},
"TypeScript": {
"tab_size": 2
},
"TSX": {
"tab_size": 2
},
"YAML": {
"tab_size": 2
},
"JSON": {
"tab_size": 2
}
},
// LSP Specific settings.
"lsp": {
// Specify the LSP name as a key here.
// As of 8/10/22, supported LSPs are:
// pyright
// gopls
// rust-analyzer
// typescript-language-server
// vscode-json-languageserver
// "rust-analyzer": {
// //These initialization options are merged into Zed's defaults
// "initialization_options": {
// "checkOnSave": {
// "command": "clippy"
// }
// }
// }
}
}

View File

@@ -7,5 +7,5 @@
// custom settings, run the `open default settings` command
// from the command palette or from `Zed` application menu.
{
"buffer_font_size": 15
"buffer_font_size": 15
}

View File

@@ -17,5 +17,5 @@ project = { path = "../project" }
settings = { path = "../settings" }
util = { path = "../util" }
workspace = { path = "../workspace" }
futures = "0.3"
smallvec = { version = "1.6", features = ["union"] }
futures.workspace = true
smallvec.workspace = true

View File

@@ -2,10 +2,10 @@ use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
use editor::Editor;
use futures::StreamExt;
use gpui::{
actions,
actions, anyhow,
elements::*,
platform::{CursorStyle, MouseButton},
Action, AppContext, Entity, ModelHandle, RenderContext, View, ViewContext, ViewHandle,
AppContext, Entity, ModelHandle, View, ViewContext, ViewHandle,
};
use language::{LanguageRegistry, LanguageServerBinaryStatus};
use project::{LanguageServerProgress, Project};
@@ -45,7 +45,7 @@ struct PendingWork<'a> {
struct Content {
icon: Option<&'static str>,
message: String,
action: Option<Box<dyn Action>>,
on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
}
pub fn init(cx: &mut AppContext) {
@@ -63,21 +63,18 @@ impl ActivityIndicator {
let auto_updater = AutoUpdater::get(cx);
let this = cx.add_view(|cx: &mut ViewContext<Self>| {
let mut status_events = languages.language_server_binary_statuses();
cx.spawn_weak(|this, mut cx| async move {
cx.spawn(|this, mut cx| async move {
while let Some((language, event)) = status_events.next().await {
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
this.statuses.retain(|s| s.name != language.name());
this.statuses.push(LspStatus {
name: language.name(),
status: event,
});
cx.notify();
this.update(&mut cx, |this, cx| {
this.statuses.retain(|s| s.name != language.name());
this.statuses.push(LspStatus {
name: language.name(),
status: event,
});
} else {
break;
}
cx.notify();
})?;
}
anyhow::Ok(())
})
.detach();
cx.observe(&project, |_, _, cx| cx.notify()).detach();
@@ -172,7 +169,7 @@ impl ActivityIndicator {
.flatten()
}
fn content_to_render(&mut self, cx: &mut RenderContext<Self>) -> Content {
fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Content {
// Show any language server has pending activity.
let mut pending_work = self.pending_language_server_work(cx);
if let Some(PendingWork {
@@ -202,7 +199,7 @@ impl ActivityIndicator {
return Content {
icon: None,
message,
action: None,
on_click: None,
};
}
@@ -233,7 +230,7 @@ impl ActivityIndicator {
downloading.join(", "),
if downloading.len() > 1 { "s" } else { "" }
),
action: None,
on_click: None,
};
} else if !checking_for_update.is_empty() {
return Content {
@@ -247,7 +244,7 @@ impl ActivityIndicator {
""
}
),
action: None,
on_click: None,
};
} else if !failed.is_empty() {
return Content {
@@ -257,7 +254,9 @@ impl ActivityIndicator {
failed.join(", "),
if failed.len() > 1 { "s" } else { "" }
),
action: Some(Box::new(ShowErrorMessage)),
on_click: Some(Arc::new(|this, cx| {
this.show_error_message(&Default::default(), cx)
})),
};
}
@@ -267,27 +266,31 @@ impl ActivityIndicator {
AutoUpdateStatus::Checking => Content {
icon: Some(DOWNLOAD_ICON),
message: "Checking for Zed updates…".to_string(),
action: None,
on_click: None,
},
AutoUpdateStatus::Downloading => Content {
icon: Some(DOWNLOAD_ICON),
message: "Downloading Zed update…".to_string(),
action: None,
on_click: None,
},
AutoUpdateStatus::Installing => Content {
icon: Some(DOWNLOAD_ICON),
message: "Installing Zed update…".to_string(),
action: None,
on_click: None,
},
AutoUpdateStatus::Updated => Content {
icon: None,
message: "Click to restart and update Zed".to_string(),
action: Some(Box::new(workspace::Restart)),
on_click: Some(Arc::new(|_, cx| {
workspace::restart(&Default::default(), cx)
})),
},
AutoUpdateStatus::Errored => Content {
icon: Some(WARNING_ICON),
message: "Auto update failed".to_string(),
action: Some(Box::new(DismissErrorMessage)),
on_click: Some(Arc::new(|this, cx| {
this.dismiss_error_message(&Default::default(), cx)
})),
},
AutoUpdateStatus::Idle => Default::default(),
};
@@ -297,7 +300,7 @@ impl ActivityIndicator {
return Content {
icon: None,
message: most_recent_active_task.to_string(),
action: None,
on_click: None,
};
}
@@ -314,21 +317,21 @@ impl View for ActivityIndicator {
"ActivityIndicator"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let Content {
icon,
message,
action,
on_click,
} = self.content_to_render(cx);
let mut element = MouseEventHandler::<Self>::new(0, cx, |state, cx| {
let mut element = MouseEventHandler::<Self, _>::new(0, cx, |state, cx| {
let theme = &cx
.global::<Settings>()
.theme
.workspace
.status_bar
.lsp_status;
let style = if state.hovered() && action.is_some() {
let style = if state.hovered() && on_click.is_some() {
theme.hover.as_ref().unwrap_or(&theme.default)
} else {
&theme.default
@@ -342,31 +345,27 @@ impl View for ActivityIndicator {
.contained()
.with_margin_right(style.icon_spacing)
.aligned()
.named("activity-icon")
.into_any_named("activity-icon")
}))
.with_child(
Text::new(message, style.message.clone())
.with_soft_wrap(false)
.aligned()
.boxed(),
.aligned(),
)
.constrained()
.with_height(style.height)
.contained()
.with_style(style.container)
.aligned()
.boxed()
});
if let Some(action) = action {
if let Some(on_click) = on_click.clone() {
element = element
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_any_action(action.boxed_clone())
});
.on_click(MouseButton::Left, move |_, this, cx| on_click(this, cx));
}
element.boxed()
element.into_any()
}
}

View File

@@ -10,6 +10,5 @@ doctest = false
[dependencies]
gpui = { path = "../gpui" }
anyhow = "1.0.38"
anyhow.workspace = true
rust-embed = { version = "6.3", features = ["include-exclude"] }

View File

@@ -18,12 +18,12 @@ settings = { path = "../settings" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }
util = { path = "../util" }
anyhow = "1.0.38"
anyhow.workspace = true
isahc = "1.7"
lazy_static = "1.4"
log = "0.4"
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
smol = "1.2.5"
tempdir = "0.3.7"
lazy_static.workspace = true
log.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
smol.workspace = true
tempdir.workspace = true

View File

@@ -1,13 +1,15 @@
mod update_notification;
use anyhow::{anyhow, Context, Result};
use client::{ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
use client::{Client, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
use db::kvp::KEY_VALUE_STORE;
use gpui::{
actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
Task, WeakViewHandle,
};
use isahc::AsyncBody;
use serde::Deserialize;
use serde_derive::Serialize;
use settings::Settings;
use smol::{fs::File, io::AsyncReadExt, process::Command};
use std::{ffi::OsString, sync::Arc, time::Duration};
@@ -21,6 +23,13 @@ const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
#[derive(Serialize)]
struct UpdateRequestBody {
installation_id: Option<Arc<str>>,
release_channel: Option<&'static str>,
telemetry: bool,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum AutoUpdateStatus {
Idle,
@@ -51,9 +60,8 @@ impl Entity for AutoUpdater {
pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) {
let server_url = server_url;
let auto_updater = cx.add_model(|cx| {
let updater = AutoUpdater::new(version, http_client, server_url.clone());
let updater = AutoUpdater::new(version, http_client, server_url);
let mut update_subscription = cx
.global::<Settings>()
@@ -74,25 +82,32 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
updater
});
cx.set_global(Some(auto_updater));
cx.add_global_action(|_: &Check, cx| {
if let Some(updater) = AutoUpdater::get(cx) {
updater.update(cx, |updater, cx| updater.poll(cx));
}
});
cx.add_global_action(move |_: &ViewReleaseNotes, cx| {
let latest_release_url = if cx.has_global::<ReleaseChannel>()
&& *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
{
format!("{server_url}/releases/preview/latest")
} else {
format!("{server_url}/releases/latest")
};
cx.platform().open_url(&latest_release_url);
});
cx.add_global_action(check);
cx.add_global_action(view_release_notes);
cx.add_action(UpdateNotification::dismiss);
}
}
pub fn check(_: &Check, cx: &mut AppContext) {
if let Some(updater) = AutoUpdater::get(cx) {
updater.update(cx, |updater, cx| updater.poll(cx));
}
}
fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
if let Some(auto_updater) = AutoUpdater::get(cx) {
let server_url = &auto_updater.read(cx).server_url;
let latest_release_url = if cx.has_global::<ReleaseChannel>()
&& *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
{
format!("{server_url}/releases/preview/latest")
} else {
format!("{server_url}/releases/latest")
};
cx.platform().open_url(&latest_release_url);
}
}
pub fn notify_of_any_new_update(
workspace: WeakViewHandle<Workspace>,
cx: &mut AppContext,
@@ -104,17 +119,15 @@ pub fn notify_of_any_new_update(
cx.spawn(|mut cx| async move {
let should_show_notification = should_show_notification.await?;
if should_show_notification {
if let Some(workspace) = workspace.upgrade(&cx) {
workspace.update(&mut cx, |workspace, cx| {
workspace.show_notification(0, cx, |cx| {
cx.add_view(|_| UpdateNotification::new(version))
});
updater
.read(cx)
.set_should_show_update_notification(false, cx)
.detach_and_log_err(cx);
workspace.update(&mut cx, |workspace, cx| {
workspace.show_notification(0, cx, |cx| {
cx.add_view(|_| UpdateNotification::new(version))
});
}
updater
.read(cx)
.set_should_show_update_notification(false, cx)
.detach_and_log_err(cx);
})?;
}
anyhow::Ok(())
})
@@ -243,7 +256,24 @@ impl AutoUpdater {
mounted_app_path.push("/");
let mut dmg_file = File::create(&dmg_path).await?;
let mut response = client.get(&release.url, Default::default(), true).await?;
let (installation_id, release_channel, telemetry) = cx.read(|cx| {
let installation_id = cx.global::<Arc<Client>>().telemetry().installation_id();
let release_channel = cx
.has_global::<ReleaseChannel>()
.then(|| cx.global::<ReleaseChannel>().display_name());
let telemetry = cx.global::<Settings>().telemetry().metrics();
(installation_id, release_channel, telemetry)
});
let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
installation_id,
release_channel,
telemetry,
})?);
let mut response = client.get(&release.url, request_body, true).await?;
smol::io::copy(response.body_mut(), &mut dmg_file).await?;
log::info!("downloaded update. path:{:?}", dmg_path);

View File

@@ -26,13 +26,13 @@ impl View for UpdateNotification {
"UpdateNotification"
}
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
let theme = cx.global::<Settings>().theme.clone();
let theme = &theme.update_notification;
let app_name = cx.global::<ReleaseChannel>().display_name();
MouseEventHandler::<ViewReleaseNotes>::new(0, cx, |state, cx| {
MouseEventHandler::<ViewReleaseNotes, _>::new(0, cx, |state, cx| {
Flex::column()
.with_child(
Flex::row()
@@ -46,11 +46,10 @@ impl View for UpdateNotification {
.aligned()
.top()
.left()
.flex(1., true)
.boxed(),
.flex(1., true),
)
.with_child(
MouseEventHandler::<Cancel>::new(0, cx, |state, _| {
MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false);
Svg::new("icons/x_mark_8.svg")
.with_color(style.color)
@@ -62,35 +61,32 @@ impl View for UpdateNotification {
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.boxed()
})
.with_padding(Padding::uniform(5.))
.on_click(MouseButton::Left, move |_, cx| cx.dispatch_action(Cancel))
.on_click(MouseButton::Left, move |_, this, cx| {
this.dismiss(&Default::default(), cx)
})
.aligned()
.constrained()
.with_height(cx.font_cache().line_height(theme.message.text.font_size))
.aligned()
.top()
.flex_float()
.boxed(),
)
.boxed(),
.flex_float(),
),
)
.with_child({
let style = theme.action_message.style_for(state, false);
Text::new("View the release notes", style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.contained()
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(ViewReleaseNotes)
.on_click(MouseButton::Left, |_, _, cx| {
crate::view_release_notes(&Default::default(), cx)
})
.boxed()
.into_any_named("update notification")
}
}

View File

@@ -1,13 +1,13 @@
use gpui::{
elements::*, platform::MouseButton, AppContext, Entity, RenderContext, Subscription, View,
ViewContext, ViewHandle,
elements::*, platform::MouseButton, AppContext, Entity, Subscription, View, ViewContext,
ViewHandle, WeakViewHandle,
};
use itertools::Itertools;
use search::ProjectSearchView;
use settings::Settings;
use workspace::{
item::{ItemEvent, ItemHandle},
ToolbarItemLocation, ToolbarItemView,
ToolbarItemLocation, ToolbarItemView, Workspace,
};
pub enum Event {
@@ -19,15 +19,17 @@ pub struct Breadcrumbs {
active_item: Option<Box<dyn ItemHandle>>,
project_search: Option<ViewHandle<ProjectSearchView>>,
subscription: Option<Subscription>,
workspace: WeakViewHandle<Workspace>,
}
impl Breadcrumbs {
pub fn new() -> Self {
pub fn new(workspace: &Workspace) -> Self {
Self {
pane_focused: false,
active_item: Default::default(),
subscription: Default::default(),
project_search: Default::default(),
workspace: workspace.weak_handle(),
}
}
}
@@ -41,10 +43,10 @@ impl View for Breadcrumbs {
"Breadcrumbs"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let active_item = match &self.active_item {
Some(active_item) => active_item,
None => return Empty::new().boxed(),
None => return Empty::new().into_any(),
};
let not_editor = active_item.downcast::<editor::Editor>().is_none();
@@ -53,12 +55,21 @@ impl View for Breadcrumbs {
let breadcrumbs = match active_item.breadcrumbs(&theme, cx) {
Some(breadcrumbs) => breadcrumbs,
None => return Empty::new().boxed(),
};
None => return Empty::new().into_any(),
}
.into_iter()
.map(|breadcrumb| {
Text::new(
breadcrumb.text,
theme.workspace.breadcrumbs.default.text.clone(),
)
.with_highlights(breadcrumb.highlights.unwrap_or_default())
.into_any()
});
let crumbs = Flex::row()
.with_children(Itertools::intersperse_with(breadcrumbs.into_iter(), || {
Label::new("", style.default.text.clone()).boxed()
.with_children(Itertools::intersperse_with(breadcrumbs, || {
Label::new("", style.default.text.clone()).into_any()
}))
.constrained()
.with_height(theme.workspace.breadcrumb_height)
@@ -69,17 +80,21 @@ impl View for Breadcrumbs {
.with_style(style.default.container)
.aligned()
.left()
.boxed();
.into_any();
}
MouseEventHandler::<Breadcrumbs>::new(0, cx, |state, _| {
MouseEventHandler::<Breadcrumbs, Breadcrumbs>::new(0, cx, |state, _| {
let style = style.style_for(state, false);
crumbs.with_style(style.container).boxed()
crumbs.with_style(style.container)
})
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(outline::Toggle);
.on_click(MouseButton::Left, |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
outline::toggle(workspace, &Default::default(), cx)
})
}
})
.with_tooltip::<Breadcrumbs, _>(
.with_tooltip::<Breadcrumbs>(
0,
"Show symbol outline".to_owned(),
Some(Box::new(outline::Toggle)),
@@ -88,7 +103,7 @@ impl View for Breadcrumbs {
)
.aligned()
.left()
.boxed()
.into_any()
}
}
@@ -136,7 +151,7 @@ impl ToolbarItemView for Breadcrumbs {
}
}
fn pane_focus_update(&mut self, pane_focused: bool, _: &mut gpui::AppContext) {
fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
self.pane_focused = pane_focused;
}
}

View File

@@ -22,7 +22,7 @@ test-support = [
client = { path = "../client" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }
log = "0.4"
log.workspace = true
live_kit_client = { path = "../live_kit_client" }
fs = { path = "../fs" }
language = { path = "../language" }
@@ -31,10 +31,10 @@ project = { path = "../project" }
settings = { path = "../settings" }
util = { path = "../util" }
anyhow = "1.0.38"
anyhow.workspace = true
async-broadcast = "0.4"
futures = "0.3"
postage = { workspace = true }
futures.workspace = true
postage.workspace = true
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }

View File

@@ -13,12 +13,12 @@ name = "cli"
path = "src/main.rs"
[dependencies]
anyhow = "1.0"
anyhow.workspace = true
clap = { version = "3.1", features = ["derive"] }
dirs = "3.0"
ipc-channel = "0.16"
serde = { workspace = true }
serde_derive = { workspace = true }
serde.workspace = true
serde_derive.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.9"

View File

@@ -17,27 +17,28 @@ db = { path = "../db" }
gpui = { path = "../gpui" }
util = { path = "../util" }
rpc = { path = "../rpc" }
settings = { path = "../settings" }
staff_mode = { path = "../staff_mode" }
sum_tree = { path = "../sum_tree" }
anyhow = "1.0.38"
anyhow.workspace = true
async-recursion = "0.3"
async-tungstenite = { version = "0.16", features = ["async-tls"] }
futures = "0.3"
futures.workspace = true
image = "0.23"
lazy_static = "1.4.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
postage = { workspace = true }
rand = "0.8.3"
smol = "1.2.5"
thiserror = "1.0.29"
time = { version = "0.3", features = ["serde", "serde-well-known"] }
lazy_static.workspace = true
log.workspace = true
parking_lot.workspace = true
postage.workspace = true
rand.workspace = true
smol.workspace = true
thiserror.workspace = true
time.workspace = true
tiny_http = "0.8"
uuid = { version = "1.1.2", features = ["v4"] }
url = "2.2"
serde = { workspace = true }
serde_derive = { workspace = true }
settings = { path = "../settings" }
serde.workspace = true
serde_derive.workspace = true
tempfile = "3"
[dev-dependencies]

View File

@@ -17,9 +17,9 @@ use futures::{
use gpui::{
actions,
platform::AppVersion,
serde_json::{self, Value},
AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext,
AsyncAppContext, Entity, ModelHandle, Task, View, ViewContext, ViewHandle,
serde_json::{self},
AnyModelHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
ModelHandle, Task, View, ViewContext, WeakViewHandle,
};
use lazy_static::lazy_static;
use parking_lot::RwLock;
@@ -27,7 +27,7 @@ use postage::watch;
use rand::prelude::*;
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
use serde::Deserialize;
use settings::{Settings, TelemetrySettings};
use settings::Settings;
use std::{
any::TypeId,
collections::HashMap,
@@ -47,6 +47,7 @@ use util::http::HttpClient;
use util::{ResultExt, TryFutureExt};
pub use rpc::*;
pub use telemetry::ClickhouseEvent;
pub use user::*;
lazy_static! {
@@ -221,7 +222,7 @@ enum WeakSubscriber {
enum Subscriber {
Model(AnyModelHandle),
View(AnyViewHandle),
View(AnyWeakViewHandle),
}
#[derive(Clone, Debug)]
@@ -567,7 +568,7 @@ impl Client {
H: 'static
+ Send
+ Sync
+ Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
+ Fn(WeakViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<()>>,
{
self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
@@ -666,7 +667,7 @@ impl Client {
H: 'static
+ Send
+ Sync
+ Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
+ Fn(WeakViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<M::Response>>,
{
self.add_view_message_handler(move |entity, envelope, client, cx| {
@@ -736,7 +737,7 @@ impl Client {
read_from_keychain = credentials.is_some();
if read_from_keychain {
cx.read(|cx| {
self.report_event(
self.telemetry().report_mixpanel_event(
"read credentials from keychain",
Default::default(),
cx.global::<Settings>().telemetry(),
@@ -1116,7 +1117,7 @@ impl Client {
.context("failed to decrypt access token")?;
platform.activate(true);
telemetry.report_event(
telemetry.report_mixpanel_event(
"authenticate with browser",
Default::default(),
metrics_enabled,
@@ -1273,7 +1274,15 @@ impl Client {
pending.push(message);
return;
}
Some(weak_subscriber @ _) => subscriber = weak_subscriber.upgrade(cx),
Some(weak_subscriber @ _) => match weak_subscriber {
WeakSubscriber::Model(handle) => {
subscriber = handle.upgrade(cx).map(Subscriber::Model);
}
WeakSubscriber::View(handle) => {
subscriber = Some(Subscriber::View(handle.clone()));
}
WeakSubscriber::Pending(_) => {}
},
_ => {}
}
}
@@ -1330,40 +1339,8 @@ impl Client {
}
}
pub fn start_telemetry(&self) {
self.telemetry.start();
}
pub fn report_event(
&self,
kind: &str,
properties: Value,
telemetry_settings: TelemetrySettings,
) {
self.telemetry
.report_event(kind, properties.clone(), telemetry_settings);
}
pub fn telemetry_log_file_path(&self) -> Option<PathBuf> {
self.telemetry.log_file_path()
}
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 {
fn upgrade(&self, cx: &AsyncAppContext) -> Option<Subscriber> {
match self {
WeakSubscriber::Model(handle) => handle.upgrade(cx).map(Subscriber::Model),
WeakSubscriber::View(handle) => handle.upgrade(cx).map(Subscriber::View),
WeakSubscriber::Pending(_) => None,
}
pub fn telemetry(&self) -> &Arc<Telemetry> {
&self.telemetry
}
}

View File

@@ -1,3 +1,4 @@
use crate::{ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use db::kvp::KEY_VALUE_STORE;
use gpui::{
executor::Background,
@@ -29,26 +30,62 @@ pub struct Telemetry {
#[derive(Default)]
struct TelemetryState {
metrics_id: Option<Arc<str>>,
device_id: Option<Arc<str>>,
metrics_id: Option<Arc<str>>, // Per logged-in user
installation_id: Option<Arc<str>>, // Per app installation
app_version: Option<Arc<str>>,
release_channel: Option<&'static str>,
os_version: Option<Arc<str>>,
os_name: &'static str,
queue: Vec<MixpanelEvent>,
next_event_id: usize,
flush_task: Option<Task<()>>,
mixpanel_events_queue: Vec<MixpanelEvent>,
clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
next_mixpanel_event_id: usize,
flush_mixpanel_events_task: Option<Task<()>>,
flush_clickhouse_events_task: Option<Task<()>>,
log_file: Option<NamedTempFile>,
is_staff: Option<bool>,
}
const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track";
const MIXPANEL_ENGAGE_URL: &'static str = "https://api.mixpanel.com/engage#profile-set";
const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
lazy_static! {
static ref MIXPANEL_TOKEN: Option<String> = std::env::var("ZED_MIXPANEL_TOKEN")
.ok()
.or_else(|| option_env!("ZED_MIXPANEL_TOKEN").map(|key| key.to_string()));
static ref CLICKHOUSE_EVENTS_URL: String =
format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH);
}
#[derive(Serialize, Debug)]
struct ClickhouseEventRequestBody {
token: &'static str,
installation_id: Option<Arc<str>>,
app_version: Option<Arc<str>>,
os_name: &'static str,
os_version: Option<Arc<str>>,
release_channel: Option<&'static str>,
events: Vec<ClickhouseEventWrapper>,
}
#[derive(Serialize, Debug)]
struct ClickhouseEventWrapper {
time: u128,
signed_in: bool,
#[serde(flatten)]
event: ClickhouseEvent,
}
#[derive(Serialize, Debug)]
#[serde(tag = "type")]
pub enum ClickhouseEvent {
Editor {
operation: &'static str,
file_extension: Option<String>,
vim_mode: bool,
copilot_enabled: bool,
copilot_enabled_for_language: bool,
},
}
#[derive(Serialize, Debug)]
@@ -63,7 +100,8 @@ struct MixpanelEventProperties {
#[serde(skip_serializing_if = "str::is_empty")]
token: &'static str,
time: u128,
distinct_id: Option<Arc<str>>,
#[serde(rename = "distinct_id")]
installation_id: Option<Arc<str>>,
#[serde(rename = "$insert_id")]
insert_id: usize,
// Custom fields
@@ -86,7 +124,7 @@ struct MixpanelEngageRequest {
#[serde(rename = "$token")]
token: &'static str,
#[serde(rename = "$distinct_id")]
distinct_id: Arc<str>,
installation_id: Arc<str>,
#[serde(rename = "$set")]
set: Value,
}
@@ -119,11 +157,13 @@ impl Telemetry {
os_name: platform.os_name().into(),
app_version: platform.app_version().ok().map(|v| v.to_string().into()),
release_channel,
device_id: None,
installation_id: None,
metrics_id: None,
queue: Default::default(),
flush_task: Default::default(),
next_event_id: 0,
mixpanel_events_queue: Default::default(),
clickhouse_events_queue: Default::default(),
flush_mixpanel_events_task: Default::default(),
flush_clickhouse_events_task: Default::default(),
next_mixpanel_event_id: 0,
log_file: None,
is_staff: None,
}),
@@ -154,29 +194,38 @@ impl Telemetry {
self.executor
.spawn(
async move {
let device_id =
if let Ok(Some(device_id)) = KEY_VALUE_STORE.read_kvp("device_id") {
device_id
let installation_id =
if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp("device_id") {
installation_id
} else {
let device_id = Uuid::new_v4().to_string();
let installation_id = Uuid::new_v4().to_string();
KEY_VALUE_STORE
.write_kvp("device_id".to_string(), device_id.clone())
.write_kvp("device_id".to_string(), installation_id.clone())
.await?;
device_id
installation_id
};
let device_id: Arc<str> = device_id.into();
let installation_id: Arc<str> = installation_id.into();
let mut state = this.state.lock();
state.device_id = Some(device_id.clone());
for event in &mut state.queue {
state.installation_id = Some(installation_id.clone());
for event in &mut state.mixpanel_events_queue {
event
.properties
.distinct_id
.get_or_insert_with(|| device_id.clone());
.installation_id
.get_or_insert_with(|| installation_id.clone());
}
if !state.queue.is_empty() {
drop(state);
this.flush();
let has_mixpanel_events = !state.mixpanel_events_queue.is_empty();
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
drop(state);
if has_mixpanel_events {
this.flush_mixpanel_events();
}
if has_clickhouse_events {
this.flush_clickhouse_events();
}
anyhow::Ok(())
@@ -200,19 +249,19 @@ impl Telemetry {
let this = self.clone();
let mut state = self.state.lock();
let device_id = state.device_id.clone();
let installation_id = state.installation_id.clone();
let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
state.metrics_id = metrics_id.clone();
state.is_staff = Some(is_staff);
drop(state);
if let Some((token, device_id)) = MIXPANEL_TOKEN.as_ref().zip(device_id) {
if let Some((token, installation_id)) = MIXPANEL_TOKEN.as_ref().zip(installation_id) {
self.executor
.spawn(
async move {
let json_bytes = serde_json::to_vec(&[MixpanelEngageRequest {
token,
distinct_id: device_id,
installation_id,
set: json!({
"Staff": is_staff,
"ID": metrics_id,
@@ -231,7 +280,42 @@ impl Telemetry {
}
}
pub fn report_event(
pub fn report_clickhouse_event(
self: &Arc<Self>,
event: ClickhouseEvent,
telemetry_settings: TelemetrySettings,
) {
if !telemetry_settings.metrics() {
return;
}
let mut state = self.state.lock();
let signed_in = state.metrics_id.is_some();
state.clickhouse_events_queue.push(ClickhouseEventWrapper {
time: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
signed_in,
event,
});
if state.installation_id.is_some() {
if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN {
drop(state);
self.flush_clickhouse_events();
} else {
let this = self.clone();
let executor = self.executor.clone();
state.flush_clickhouse_events_task = Some(self.executor.spawn(async move {
executor.timer(DEBOUNCE_INTERVAL).await;
this.flush_clickhouse_events();
}));
}
}
}
pub fn report_mixpanel_event(
self: &Arc<Self>,
kind: &str,
properties: Value,
@@ -243,15 +327,15 @@ impl Telemetry {
let mut state = self.state.lock();
let event = MixpanelEvent {
event: kind.to_string(),
event: kind.into(),
properties: MixpanelEventProperties {
token: "",
time: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
distinct_id: state.device_id.clone(),
insert_id: post_inc(&mut state.next_event_id),
installation_id: state.installation_id.clone(),
insert_id: post_inc(&mut state.next_mixpanel_event_id),
event_properties: if let Value::Object(properties) = properties {
Some(properties)
} else {
@@ -264,17 +348,17 @@ impl Telemetry {
signed_in: state.metrics_id.is_some(),
},
};
state.queue.push(event);
if state.device_id.is_some() {
if state.queue.len() >= MAX_QUEUE_LEN {
state.mixpanel_events_queue.push(event);
if state.installation_id.is_some() {
if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN {
drop(state);
self.flush();
self.flush_mixpanel_events();
} else {
let this = self.clone();
let executor = self.executor.clone();
state.flush_task = Some(self.executor.spawn(async move {
state.flush_mixpanel_events_task = Some(self.executor.spawn(async move {
executor.timer(DEBOUNCE_INTERVAL).await;
this.flush();
this.flush_mixpanel_events();
}));
}
}
@@ -284,14 +368,18 @@ impl Telemetry {
self.state.lock().metrics_id.clone()
}
pub fn installation_id(self: &Arc<Self>) -> Option<Arc<str>> {
self.state.lock().installation_id.clone()
}
pub fn is_staff(self: &Arc<Self>) -> Option<bool> {
self.state.lock().is_staff
}
fn flush(self: &Arc<Self>) {
fn flush_mixpanel_events(self: &Arc<Self>) {
let mut state = self.state.lock();
let mut events = mem::take(&mut state.queue);
state.flush_task.take();
let mut events = mem::take(&mut state.mixpanel_events_queue);
state.flush_mixpanel_events_task.take();
drop(state);
if let Some(token) = MIXPANEL_TOKEN.as_ref() {
@@ -325,4 +413,53 @@ impl Telemetry {
.detach();
}
}
fn flush_clickhouse_events(self: &Arc<Self>) {
let mut state = self.state.lock();
let mut events = mem::take(&mut state.clickhouse_events_queue);
state.flush_clickhouse_events_task.take();
drop(state);
let this = self.clone();
self.executor
.spawn(
async move {
let mut json_bytes = Vec::new();
if let Some(file) = &mut this.state.lock().log_file {
let file = file.as_file_mut();
for event in &mut events {
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, event)?;
file.write_all(&json_bytes)?;
file.write(b"\n")?;
}
}
{
let state = this.state.lock();
json_bytes.clear();
serde_json::to_writer(
&mut json_bytes,
&ClickhouseEventRequestBody {
token: ZED_SECRET_CLIENT_TOKEN,
installation_id: state.installation_id.clone(),
app_version: state.app_version.clone(),
os_name: state.os_name,
os_version: state.os_version.clone(),
release_channel: state.release_channel,
events,
},
)?;
}
this.http_client
.post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into())
.await?;
anyhow::Ok(())
}
.log_err(),
)
.detach();
}
}

View File

@@ -9,4 +9,4 @@ path = "src/clock.rs"
doctest = false
[dependencies]
smallvec = { version = "1.6", features = ["union"] }
smallvec.workspace = true

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
version = "0.8.2"
version = "0.12.0"
publish = false
[[bin]]
@@ -19,7 +19,7 @@ live_kit_server = { path = "../live_kit_server" }
rpc = { path = "../rpc" }
util = { path = "../util" }
anyhow = "1.0.40"
anyhow.workspace = true
async-tungstenite = "0.16"
axum = { version = "0.5", features = ["json", "headers", "ws"] }
axum-extra = { version = "0.3", features = ["erased-json"] }
@@ -27,26 +27,26 @@ base64 = "0.13"
clap = { version = "3.1", features = ["derive"], optional = true }
dashmap = "5.4"
envy = "0.4.2"
futures = "0.3"
futures.workspace = true
hyper = "0.14"
lazy_static = "1.4"
lazy_static.workspace = true
lipsum = { version = "0.8", optional = true }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
log.workspace = true
nanoid = "0.4"
parking_lot = "0.11.1"
parking_lot.workspace = true
prometheus = "0.13"
rand = "0.8"
rand.workspace = true
reqwest = { version = "0.11", features = ["json"], optional = true }
scrypt = "0.7"
# Remove fork dependency when a version with https://github.com/SeaQL/sea-orm/pull/1283 is released.
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls"] }
sea-query = "0.27"
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
sha-1 = "0.9"
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
time = { version = "0.3", features = ["serde", "serde-well-known"] }
time.workspace = true
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = "0.17"
tonic = "0.6"
@@ -74,14 +74,15 @@ settings = { path = "../settings", features = ["test-support"] }
theme = { path = "../theme" }
workspace = { path = "../workspace", features = ["test-support"] }
ctor = "0.1"
env_logger = "0.9"
ctor.workspace = true
env_logger.workspace = true
indoc = "1.0.4"
util = { path = "../util" }
lazy_static = "1.4"
lazy_static.workspace = true
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-sqlite"] }
serde_json = { workspace = true }
serde_json.workspace = true
sqlx = { version = "0.6", features = ["sqlite"] }
unindent = "0.1"
unindent.workspace = true
[features]
seed-support = ["clap", "lipsum", "reqwest"]

View File

@@ -82,6 +82,20 @@ CREATE TABLE "worktree_entries" (
CREATE INDEX "index_worktree_entries_on_project_id" ON "worktree_entries" ("project_id");
CREATE INDEX "index_worktree_entries_on_project_id_and_worktree_id" ON "worktree_entries" ("project_id", "worktree_id");
CREATE TABLE "worktree_repositories" (
"project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL,
"work_directory_id" INTEGER NOT NULL,
"scan_id" INTEGER NOT NULL,
"branch" VARCHAR,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, work_directory_id),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");
CREATE TABLE "worktree_diagnostic_summaries" (
"project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL,
@@ -153,7 +167,7 @@ CREATE TABLE "followers" (
"follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
"follower_connection_id" INTEGER NOT NULL
);
CREATE UNIQUE INDEX
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,13 @@
CREATE TABLE "worktree_repositories" (
"project_id" INTEGER NOT NULL,
"worktree_id" INT8 NOT NULL,
"work_directory_id" INT8 NOT NULL,
"scan_id" INT8 NOT NULL,
"branch" VARCHAR,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, work_directory_id),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");

View File

@@ -14,6 +14,7 @@ mod user;
mod worktree;
mod worktree_diagnostic_summary;
mod worktree_entry;
mod worktree_repository;
use crate::executor::Executor;
use crate::{Error, Result};
@@ -1489,6 +1490,8 @@ impl Database {
visible: db_worktree.visible,
updated_entries: Default::default(),
removed_entries: Default::default(),
updated_repositories: Default::default(),
removed_repositories: Default::default(),
diagnostic_summaries: Default::default(),
scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64,
@@ -1498,38 +1501,75 @@ impl Database {
.worktrees
.iter()
.find(|worktree| worktree.id == db_worktree.id as u64);
let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id)
} else {
worktree_entry::Column::IsDeleted.eq(false)
};
let mut db_entries = worktree_entry::Entity::find()
.filter(
Condition::all()
.add(worktree_entry::Column::WorktreeId.eq(worktree.id))
.add(entry_filter),
)
.stream(&*tx)
.await?;
while let Some(db_entry) = db_entries.next().await {
let db_entry = db_entry?;
if db_entry.is_deleted {
worktree.removed_entries.push(db_entry.id as u64);
// File entries
{
let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id)
} else {
worktree.updated_entries.push(proto::Entry {
id: db_entry.id as u64,
is_dir: db_entry.is_dir,
path: db_entry.path,
inode: db_entry.inode as u64,
mtime: Some(proto::Timestamp {
seconds: db_entry.mtime_seconds as u64,
nanos: db_entry.mtime_nanos as u32,
}),
is_symlink: db_entry.is_symlink,
is_ignored: db_entry.is_ignored,
});
worktree_entry::Column::IsDeleted.eq(false)
};
let mut db_entries = worktree_entry::Entity::find()
.filter(
Condition::all()
.add(worktree_entry::Column::WorktreeId.eq(worktree.id))
.add(entry_filter),
)
.stream(&*tx)
.await?;
while let Some(db_entry) = db_entries.next().await {
let db_entry = db_entry?;
if db_entry.is_deleted {
worktree.removed_entries.push(db_entry.id as u64);
} else {
worktree.updated_entries.push(proto::Entry {
id: db_entry.id as u64,
is_dir: db_entry.is_dir,
path: db_entry.path,
inode: db_entry.inode as u64,
mtime: Some(proto::Timestamp {
seconds: db_entry.mtime_seconds as u64,
nanos: db_entry.mtime_nanos as u32,
}),
is_symlink: db_entry.is_symlink,
is_ignored: db_entry.is_ignored,
});
}
}
}
// Repository Entries
{
let repository_entry_filter =
if let Some(rejoined_worktree) = rejoined_worktree {
worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id)
} else {
worktree_repository::Column::IsDeleted.eq(false)
};
let mut db_repositories = worktree_repository::Entity::find()
.filter(
Condition::all()
.add(worktree_repository::Column::WorktreeId.eq(worktree.id))
.add(repository_entry_filter),
)
.stream(&*tx)
.await?;
while let Some(db_repository) = db_repositories.next().await {
let db_repository = db_repository?;
if db_repository.is_deleted {
worktree
.removed_repositories
.push(db_repository.work_directory_id as u64);
} else {
worktree.updated_repositories.push(proto::RepositoryEntry {
work_directory_id: db_repository.work_directory_id as u64,
branch: db_repository.branch,
});
}
}
}
@@ -2330,6 +2370,53 @@ impl Database {
.await?;
}
if !update.updated_repositories.is_empty() {
worktree_repository::Entity::insert_many(update.updated_repositories.iter().map(
|repository| worktree_repository::ActiveModel {
project_id: ActiveValue::set(project_id),
worktree_id: ActiveValue::set(worktree_id),
work_directory_id: ActiveValue::set(repository.work_directory_id as i64),
scan_id: ActiveValue::set(update.scan_id as i64),
branch: ActiveValue::set(repository.branch.clone()),
is_deleted: ActiveValue::set(false),
},
))
.on_conflict(
OnConflict::columns([
worktree_repository::Column::ProjectId,
worktree_repository::Column::WorktreeId,
worktree_repository::Column::WorkDirectoryId,
])
.update_columns([
worktree_repository::Column::ScanId,
worktree_repository::Column::Branch,
])
.to_owned(),
)
.exec(&*tx)
.await?;
}
if !update.removed_repositories.is_empty() {
worktree_repository::Entity::update_many()
.filter(
worktree_repository::Column::ProjectId
.eq(project_id)
.and(worktree_repository::Column::WorktreeId.eq(worktree_id))
.and(
worktree_repository::Column::WorkDirectoryId
.is_in(update.removed_repositories.iter().map(|id| *id as i64)),
),
)
.set(worktree_repository::ActiveModel {
is_deleted: ActiveValue::Set(true),
scan_id: ActiveValue::Set(update.scan_id as i64),
..Default::default()
})
.exec(&*tx)
.await?;
}
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
Ok(connection_ids)
})
@@ -2505,6 +2592,7 @@ impl Database {
root_name: db_worktree.root_name,
visible: db_worktree.visible,
entries: Default::default(),
repository_entries: Default::default(),
diagnostic_summaries: Default::default(),
scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64,
@@ -2542,6 +2630,29 @@ impl Database {
}
}
// Populate repository entries.
{
let mut db_repository_entries = worktree_repository::Entity::find()
.filter(
Condition::all()
.add(worktree_repository::Column::ProjectId.eq(project_id))
.add(worktree_repository::Column::IsDeleted.eq(false)),
)
.stream(&*tx)
.await?;
while let Some(db_repository_entry) = db_repository_entries.next().await {
let db_repository_entry = db_repository_entry?;
if let Some(worktree) =
worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
{
worktree.repository_entries.push(proto::RepositoryEntry {
work_directory_id: db_repository_entry.work_directory_id as u64,
branch: db_repository_entry.branch,
});
}
}
}
// Populate worktree diagnostic summaries.
{
let mut db_summaries = worktree_diagnostic_summary::Entity::find()
@@ -3223,6 +3334,8 @@ pub struct RejoinedWorktree {
pub visible: bool,
pub updated_entries: Vec<proto::Entry>,
pub removed_entries: Vec<u64>,
pub updated_repositories: Vec<proto::RepositoryEntry>,
pub removed_repositories: Vec<u64>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub scan_id: u64,
pub completed_scan_id: u64,
@@ -3277,6 +3390,7 @@ pub struct Worktree {
pub root_name: String,
pub visible: bool,
pub entries: Vec<proto::Entry>,
pub repository_entries: Vec<proto::RepositoryEntry>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub scan_id: u64,
pub completed_scan_id: u64,

View File

@@ -0,0 +1,21 @@
use super::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worktree_repositories")]
pub struct Model {
#[sea_orm(primary_key)]
pub project_id: ProjectId,
#[sea_orm(primary_key)]
pub worktree_id: i64,
#[sea_orm(primary_key)]
pub work_directory_id: i64,
pub scan_id: i64,
pub branch: Option<String>,
pub is_deleted: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -1063,6 +1063,8 @@ async fn rejoin_room(
removed_entries: worktree.removed_entries,
scan_id: worktree.scan_id,
is_last_update: worktree.completed_scan_id == worktree.scan_id,
updated_repositories: worktree.updated_repositories,
removed_repositories: worktree.removed_repositories,
};
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
session.peer.send(session.connection_id, update.clone())?;
@@ -1383,6 +1385,8 @@ async fn join_project(
removed_entries: Default::default(),
scan_id: worktree.scan_id,
is_last_update: worktree.scan_id == worktree.completed_scan_id,
updated_repositories: worktree.repository_entries,
removed_repositories: Default::default(),
};
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
session.peer.send(session.connection_id, update.clone())?;

View File

@@ -12,7 +12,10 @@ use client::{
use collections::{HashMap, HashSet};
use fs::FakeFs;
use futures::{channel::oneshot, StreamExt as _};
use gpui::{executor::Deterministic, test::EmptyView, ModelHandle, TestAppContext, ViewHandle};
use gpui::{
elements::*, executor::Deterministic, AnyElement, Entity, ModelHandle, TestAppContext, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use language::LanguageRegistry;
use parking_lot::Mutex;
use project::{Project, WorktreeId};
@@ -462,8 +465,41 @@ impl TestClient {
project: &ModelHandle<Project>,
cx: &mut TestAppContext,
) -> ViewHandle<Workspace> {
let (_, root_view) = cx.add_window(|_| EmptyView);
cx.add_view(&root_view, |cx| Workspace::test_new(project.clone(), cx))
struct WorkspaceContainer {
workspace: Option<WeakViewHandle<Workspace>>,
}
impl Entity for WorkspaceContainer {
type Event = ();
}
impl View for WorkspaceContainer {
fn ui_name() -> &'static str {
"WorkspaceContainer"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if let Some(workspace) = self
.workspace
.as_ref()
.and_then(|workspace| workspace.upgrade(cx))
{
ChildView::new(&workspace, cx).into_any()
} else {
Empty::new().into_any()
}
}
}
// We use a workspace container so that we don't need to remove the window in order to
// drop the workspace and we can use a ViewHandle instead.
let (window_id, container) = cx.add_window(|_| WorkspaceContainer { workspace: None });
let workspace = cx.add_view(window_id, |cx| Workspace::test_new(project.clone(), cx));
container.update(cx, |container, cx| {
container.workspace = Some(workspace.downgrade());
cx.notify();
});
workspace
}
}

View File

@@ -6,21 +6,24 @@ use call::{room, ActiveCall, ParticipantLocation, Room};
use client::{User, RECEIVE_TIMEOUT};
use collections::HashSet;
use editor::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo,
Rename, ToOffset, ToggleCodeActions, Undo,
test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion,
ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions,
Undo,
};
use fs::{FakeFs, Fs as _, LineEnding, RemoveOptions};
use futures::StreamExt as _;
use gpui::{
executor::Deterministic, geometry::vector::vec2f, test::EmptyView, ModelHandle, TestAppContext,
ViewHandle,
executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle,
TestAppContext, ViewHandle,
};
use indoc::indoc;
use language::{
tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
LanguageConfig, OffsetRangeExt, Point, Rope,
};
use live_kit_client::MacOSDisplay;
use project::{search::SearchQuery, DiagnosticSummary, Project, ProjectPath};
use lsp::LanguageServerId;
use project::{search::SearchQuery, DiagnosticSummary, HoverBlockKind, Project, ProjectPath};
use rand::prelude::*;
use serde_json::json;
use settings::{Formatter, Settings};
@@ -29,12 +32,13 @@ use std::{
env, future, mem,
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
};
use unindent::Unindent as _;
use workspace::{
item::ItemHandle as _, shared_screen::SharedScreen, SplitDirection, ToggleFollow, Workspace,
};
use workspace::{item::ItemHandle as _, shared_screen::SharedScreen, SplitDirection, Workspace};
#[ctor::ctor]
fn init_logger() {
@@ -1198,7 +1202,7 @@ async fn test_share_project(
cx_c: &mut TestAppContext,
) {
deterministic.forbid_parking();
let (_, window_b) = cx_b.add_window(|_| EmptyView);
let (window_b, _) = cx_b.add_window(|_| EmptyView);
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;
@@ -1285,7 +1289,7 @@ async fn test_share_project(
.await
.unwrap();
let editor_b = cx_b.add_view(&window_b, |cx| Editor::for_buffer(buffer_b, None, cx));
let editor_b = cx_b.add_view(window_b, |cx| Editor::for_buffer(buffer_b, None, cx));
// Client A sees client B's selection
deterministic.run_until_parked();
@@ -1467,7 +1471,8 @@ 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::test_new(project_b.clone(), cx));
let (window_id_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, "b.txt"), None, true, cx)
@@ -1476,12 +1481,9 @@ async fn test_host_disconnect(
.unwrap()
.downcast::<Editor>()
.unwrap();
cx_b.read(|cx| {
assert_eq!(
cx.focused_view_id(workspace_b.window_id()),
Some(editor_b.id())
);
});
assert!(cx_b
.read_window(window_id_b, |cx| editor_b.is_focused(cx))
.unwrap());
editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
assert!(cx_b.is_window_edited(workspace_b.window_id()));
@@ -1495,8 +1497,8 @@ async fn test_host_disconnect(
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
// Ensure client B's edited state is reset and that the whole window is blurred.
cx_b.read(|cx| {
assert_eq!(cx.focused_view_id(workspace_b.window_id()), None);
cx_b.read_window(window_id_b, |cx| {
assert_eq!(cx.focused_view_id(), None);
});
assert!(!cx_b.is_window_edited(workspace_b.window_id()));
@@ -2602,6 +2604,92 @@ async fn test_git_diff_base_change(
});
}
#[gpui::test]
async fn test_git_branch_name(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_c: &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;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
client_a
.fs
.insert_tree(
"/dir",
json!({
".git": {},
}),
)
.await;
let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| {
call.share_project(project_local.clone(), cx)
})
.await
.unwrap();
let project_remote = client_b.build_remote_project(project_id, cx_b).await;
client_a
.fs
.as_fake()
.set_branch_name(Path::new("/dir/.git"), Some("branch-1"))
.await;
// Wait for it to catch up to the new branch
deterministic.run_until_parked();
#[track_caller]
fn assert_branch(branch_name: Option<impl Into<String>>, project: &Project, cx: &AppContext) {
let branch_name = branch_name.map(Into::into);
let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
let worktree = worktrees[0].clone();
let root_entry = worktree.read(cx).snapshot().root_git_entry().unwrap();
assert_eq!(root_entry.branch(), branch_name.map(Into::into));
}
// Smoke test branch reading
project_local.read_with(cx_a, |project, cx| {
assert_branch(Some("branch-1"), project, cx)
});
project_remote.read_with(cx_b, |project, cx| {
assert_branch(Some("branch-1"), project, cx)
});
client_a
.fs
.as_fake()
.set_branch_name(Path::new("/dir/.git"), Some("branch-2"))
.await;
// Wait for buffer_local_a to receive it
deterministic.run_until_parked();
// Smoke test branch reading
project_local.read_with(cx_a, |project, cx| {
assert_branch(Some("branch-2"), project, cx)
});
project_remote.read_with(cx_b, |project, cx| {
assert_branch(Some("branch-2"), project, cx)
});
let project_remote_c = client_c.build_remote_project(project_id, cx_c).await;
project_remote_c.read_with(cx_c, |project, cx| {
assert_branch(Some("branch-2"), project, cx)
});
}
#[gpui::test(iterations = 10)]
async fn test_fs_operations(
deterministic: Arc<Deterministic>,
@@ -3042,6 +3130,104 @@ async fn test_editing_while_guest_opens_buffer(
buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
}
#[gpui::test]
async fn test_newline_above_or_below_does_not_move_guest_cursor(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
client_a
.fs
.insert_tree("/dir", json!({ "a.txt": "Some text\n" }))
.await;
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
// Open a buffer as client A
let buffer_a = project_a
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
.await
.unwrap();
let (window_a, _) = cx_a.add_window(|_| EmptyView);
let editor_a = cx_a.add_view(window_a, |cx| {
Editor::for_buffer(buffer_a, Some(project_a), cx)
});
let mut editor_cx_a = EditorTestContext {
cx: cx_a,
window_id: window_a,
editor: editor_a,
};
// Open a buffer as client B
let buffer_b = project_b
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
.await
.unwrap();
let (window_b, _) = cx_b.add_window(|_| EmptyView);
let editor_b = cx_b.add_view(window_b, |cx| {
Editor::for_buffer(buffer_b, Some(project_b), cx)
});
let mut editor_cx_b = EditorTestContext {
cx: cx_b,
window_id: window_b,
editor: editor_b,
};
// Test newline above
editor_cx_a.set_selections_state(indoc! {"
Some textˇ
"});
editor_cx_b.set_selections_state(indoc! {"
Some textˇ
"});
editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx));
deterministic.run_until_parked();
editor_cx_a.assert_editor_state(indoc! {"
ˇ
Some text
"});
editor_cx_b.assert_editor_state(indoc! {"
Some textˇ
"});
// Test newline below
editor_cx_a.set_selections_state(indoc! {"
Some textˇ
"});
editor_cx_b.set_selections_state(indoc! {"
Some textˇ
"});
editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx));
deterministic.run_until_parked();
editor_cx_a.assert_editor_state(indoc! {"
Some text
ˇ
"});
editor_cx_b.assert_editor_state(indoc! {"
Some textˇ
"});
}
#[gpui::test(iterations = 10)]
async fn test_leaving_worktree_while_opening_buffer(
deterministic: Arc<Deterministic>,
@@ -3122,14 +3308,18 @@ async fn test_canceling_buffer_opening(
.unwrap();
// Open a buffer as client B but cancel after a random amount of time.
let buffer_b = project_b.update(cx_b, |p, cx| p.open_buffer_by_id(buffer_a.id() as u64, cx));
let buffer_b = project_b.update(cx_b, |p, cx| {
p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
});
deterministic.simulate_random_delay().await;
drop(buffer_b);
// Try opening the same buffer again as client B, and ensure we can
// still do it despite the cancellation above.
let buffer_b = project_b
.update(cx_b, |p, cx| p.open_buffer_by_id(buffer_a.id() as u64, cx))
.update(cx_b, |p, cx| {
p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
})
.await
.unwrap();
buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
@@ -3377,6 +3567,7 @@ async fn test_collaborating_with_diagnostics(
worktree_id,
path: Arc::from(Path::new("a.rs")),
},
LanguageServerId(0),
DiagnosticSummary {
error_count: 1,
warning_count: 0,
@@ -3412,6 +3603,7 @@ async fn test_collaborating_with_diagnostics(
worktree_id,
path: Arc::from(Path::new("a.rs")),
},
LanguageServerId(0),
DiagnosticSummary {
error_count: 1,
warning_count: 0,
@@ -3452,10 +3644,10 @@ async fn test_collaborating_with_diagnostics(
worktree_id,
path: Arc::from(Path::new("a.rs")),
},
LanguageServerId(0),
DiagnosticSummary {
error_count: 1,
warning_count: 1,
..Default::default()
},
)]
);
@@ -3468,10 +3660,10 @@ async fn test_collaborating_with_diagnostics(
worktree_id,
path: Arc::from(Path::new("a.rs")),
},
LanguageServerId(0),
DiagnosticSummary {
error_count: 1,
warning_count: 1,
..Default::default()
},
)]
);
@@ -3535,6 +3727,141 @@ async fn test_collaborating_with_diagnostics(
});
}
#[gpui::test(iterations = 10)]
async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
// Set up a fake language server.
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let mut fake_language_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
disk_based_diagnostics_progress_token: Some("the-disk-based-token".into()),
disk_based_diagnostics_sources: vec!["the-disk-based-diagnostics-source".into()],
..Default::default()
}))
.await;
client_a.language_registry.add(Arc::new(language));
let file_names = &["one.rs", "two.rs", "three.rs", "four.rs", "five.rs"];
client_a
.fs
.insert_tree(
"/test",
json!({
"one.rs": "const ONE: usize = 1;",
"two.rs": "const TWO: usize = 2;",
"three.rs": "const THREE: usize = 3;",
"four.rs": "const FOUR: usize = 3;",
"five.rs": "const FIVE: usize = 3;",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/test", cx_a).await;
// Share a project as client A
let active_call_a = cx_a.read(ActiveCall::global);
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
// Join the project as client B and open all three files.
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
}))
.await
.unwrap();
// Simulate a language server reporting errors for a file.
let fake_language_server = fake_language_servers.next().await.unwrap();
fake_language_server
.request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
})
.await
.unwrap();
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
lsp::WorkDoneProgressBegin {
title: "Progress Began".into(),
..Default::default()
},
)),
});
for file_name in file_names {
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(Path::new("/test").join(file_name)).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
severity: Some(lsp::DiagnosticSeverity::WARNING),
source: Some("the-disk-based-diagnostics-source".into()),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
message: "message one".to_string(),
..Default::default()
}],
},
);
}
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
lsp::WorkDoneProgressEnd { message: None },
)),
});
// When the "disk base diagnostics finished" message is received, the buffers'
// diagnostics are expected to be present.
let disk_based_diagnostics_finished = Arc::new(AtomicBool::new(false));
project_b.update(cx_b, {
let project_b = project_b.clone();
let disk_based_diagnostics_finished = disk_based_diagnostics_finished.clone();
move |_, cx| {
cx.subscribe(&project_b, move |_, _, event, cx| {
if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
disk_based_diagnostics_finished.store(true, SeqCst);
for buffer in &guest_buffers {
assert_eq!(
buffer
.read(cx)
.snapshot()
.diagnostics_in_range::<_, usize>(0..5, false)
.count(),
1,
"expected a diagnostic for buffer {:?}",
buffer.read(cx).file().unwrap().path(),
);
}
}
})
.detach();
}
});
deterministic.run_until_parked();
assert!(disk_based_diagnostics_finished.load(SeqCst));
}
#[gpui::test(iterations = 10)]
async fn test_collaborating_with_completion(
deterministic: Arc<Deterministic>,
@@ -3595,8 +3922,8 @@ async fn test_collaborating_with_completion(
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
.await
.unwrap();
let (_, window_b) = cx_b.add_window(|_| EmptyView);
let editor_b = cx_b.add_view(&window_b, |cx| {
let (window_b, _) = cx_b.add_window(|_| EmptyView);
let editor_b = cx_b.add_view(window_b, |cx| {
Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
});
@@ -4221,7 +4548,10 @@ async fn test_project_search(
// Perform a search as the guest.
let results = project_b
.update(cx_b, |project, cx| {
project.search(SearchQuery::text("world", false, false), cx)
project.search(
SearchQuery::text("world", false, false, Vec::new(), Vec::new()),
cx,
)
})
.await
.unwrap();
@@ -4454,11 +4784,13 @@ async fn test_lsp_hover(
vec![
project::HoverBlock {
text: "Test hover content.".to_string(),
language: None,
kind: HoverBlockKind::Markdown,
},
project::HoverBlock {
text: "let foo = 42;".to_string(),
language: Some("Rust".to_string()),
kind: HoverBlockKind::Code {
language: "Rust".to_string()
},
}
]
);
@@ -5862,22 +6194,48 @@ async fn test_basic_following(
// 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.handle_input("a", cx);
editor.handle_input("b", cx);
editor.handle_input("c", cx);
editor.select_left(&Default::default(), cx);
assert_eq!(editor.selections.ranges(cx), vec![3..2]);
});
editor_a2.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
editor.handle_input("d", cx);
editor.handle_input("e", cx);
editor.select_left(&Default::default(), cx);
assert_eq!(editor.selections.ranges(cx), vec![2..1]);
});
// 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(peer_id_a), cx)
.unwrap()
workspace.toggle_follow(peer_id_a, cx).unwrap()
})
.await
.unwrap();
cx_c.foreground().run_until_parked();
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
workspace
.active_item(cx)
.unwrap()
.downcast::<Editor>()
.unwrap()
});
assert_eq!(
cx_b.read(|cx| editor_b2.project_path(cx)),
Some((worktree_id, "2.txt").into())
);
assert_eq!(
editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
vec![2..1]
);
assert_eq!(
editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
vec![3..2]
);
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;
@@ -5891,9 +6249,7 @@ async fn test_basic_following(
// Client C also follows client A.
workspace_c
.update(cx_c, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(peer_id_a), cx)
.unwrap()
workspace.toggle_follow(peer_id_a, cx).unwrap()
})
.await
.unwrap();
@@ -5928,7 +6284,7 @@ async fn test_basic_following(
// Client C unfollows client A.
workspace_c.update(cx_c, |workspace, cx| {
workspace.toggle_follow(&ToggleFollow(peer_id_a), cx);
workspace.toggle_follow(peer_id_a, cx);
});
// All clients see that clients B is following client A.
@@ -5951,7 +6307,7 @@ async fn test_basic_following(
// Client C re-follows client A.
workspace_c.update(cx_c, |workspace, cx| {
workspace.toggle_follow(&ToggleFollow(peer_id_a), cx);
workspace.toggle_follow(peer_id_a, cx);
});
// All clients see that clients B and C are following client A.
@@ -5975,9 +6331,7 @@ async fn test_basic_following(
// Client D follows client C.
workspace_d
.update(cx_d, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(peer_id_c), cx)
.unwrap()
workspace.toggle_follow(peer_id_c, cx).unwrap()
})
.await
.unwrap();
@@ -6033,26 +6387,6 @@ async fn test_basic_following(
});
}
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
workspace
.active_item(cx)
.unwrap()
.downcast::<Editor>()
.unwrap()
});
assert_eq!(
cx_b.read(|cx| editor_b2.project_path(cx)),
Some((worktree_id, "2.txt").into())
);
assert_eq!(
editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
vec![2..3]
);
assert_eq!(
editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
vec![0..1]
);
// When client A activates a different editor, client B does so as well.
workspace_a.update(cx_a, |workspace, cx| {
workspace.activate_item(&editor_a1, cx)
@@ -6117,7 +6451,8 @@ async fn test_basic_following(
.update(cx_a, |workspace, cx| {
workspace::Pane::go_back(workspace, None, cx)
})
.await;
.await
.unwrap();
deterministic.run_until_parked();
workspace_b.read_with(cx_b, |workspace, cx| {
assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
@@ -6127,7 +6462,8 @@ async fn test_basic_following(
.update(cx_a, |workspace, cx| {
workspace::Pane::go_back(workspace, None, cx)
})
.await;
.await
.unwrap();
deterministic.run_until_parked();
workspace_b.read_with(cx_b, |workspace, cx| {
assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
@@ -6137,7 +6473,8 @@ async fn test_basic_following(
.update(cx_a, |workspace, cx| {
workspace::Pane::go_forward(workspace, None, cx)
})
.await;
.await
.unwrap();
deterministic.run_until_parked();
workspace_b.read_with(cx_b, |workspace, cx| {
assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
@@ -6184,9 +6521,7 @@ async fn test_basic_following(
// Client A starts following client B.
workspace_a
.update(cx_a, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(peer_id_b), cx)
.unwrap()
workspace.toggle_follow(peer_id_b, cx).unwrap()
})
.await
.unwrap();
@@ -6455,9 +6790,7 @@ async fn test_following_tab_order(
//Follow client B as client A
workspace_a
.update(cx_a, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(client_b_id), cx)
.unwrap()
workspace.toggle_follow(client_b_id, cx).unwrap()
})
.await
.unwrap();
@@ -6564,33 +6897,23 @@ async fn test_peers_following_each_other(
// Clients A and B follow each other in split panes
workspace_a.update(cx_a, |workspace, cx| {
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
let pane_a1 = pane_a1.clone();
cx.defer(move |workspace, _| {
assert_ne!(*workspace.active_pane(), pane_a1);
});
});
workspace_a
.update(cx_a, |workspace, cx| {
assert_ne!(*workspace.active_pane(), pane_a1);
let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
workspace
.toggle_follow(&workspace::ToggleFollow(leader_id), cx)
.unwrap()
workspace.toggle_follow(leader_id, cx).unwrap()
})
.await
.unwrap();
workspace_b.update(cx_b, |workspace, cx| {
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
let pane_b1 = pane_b1.clone();
cx.defer(move |workspace, _| {
assert_ne!(*workspace.active_pane(), pane_b1);
});
});
workspace_b
.update(cx_b, |workspace, cx| {
assert_ne!(*workspace.active_pane(), pane_b1);
let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
workspace
.toggle_follow(&workspace::ToggleFollow(leader_id), cx)
.unwrap()
workspace.toggle_follow(leader_id, cx).unwrap()
})
.await
.unwrap();
@@ -6736,9 +7059,7 @@ async fn test_auto_unfollowing(
});
workspace_b
.update(cx_b, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(leader_id), cx)
.unwrap()
workspace.toggle_follow(leader_id, cx).unwrap()
})
.await
.unwrap();
@@ -6763,9 +7084,7 @@ async fn test_auto_unfollowing(
workspace_b
.update(cx_b, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(leader_id), cx)
.unwrap()
workspace.toggle_follow(leader_id, cx).unwrap()
})
.await
.unwrap();
@@ -6783,9 +7102,7 @@ async fn test_auto_unfollowing(
workspace_b
.update(cx_b, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(leader_id), cx)
.unwrap()
workspace.toggle_follow(leader_id, cx).unwrap()
})
.await
.unwrap();
@@ -6805,9 +7122,7 @@ async fn test_auto_unfollowing(
workspace_b
.update(cx_b, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(leader_id), cx)
.unwrap()
workspace.toggle_follow(leader_id, cx).unwrap()
})
.await
.unwrap();
@@ -6882,14 +7197,10 @@ async fn test_peers_simultaneously_following_each_other(
});
let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(client_b_id), cx)
.unwrap()
workspace.toggle_follow(client_b_id, cx).unwrap()
});
let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(client_a_id), cx)
.unwrap()
workspace.toggle_follow(client_a_id, cx).unwrap()
});
futures::try_join!(a_follow_b, b_follow_a).unwrap();

View File

@@ -716,7 +716,10 @@ async fn apply_client_operation(
);
let search = project.update(cx, |project, cx| {
project.search(SearchQuery::text(query, false, false), cx)
project.search(
SearchQuery::text(query, false, false, Vec::new(), Vec::new()),
cx,
)
});
drop(project);
let search = cx.background().spawn(async move {
@@ -785,6 +788,28 @@ async fn apply_client_operation(
}
client.fs.set_index_for_repo(&dot_git_dir, &contents).await;
}
ClientOperation::WriteGitBranch {
repo_path,
new_branch,
} => {
if !client.fs.directories().contains(&repo_path) {
return Err(TestError::Inapplicable);
}
log::info!(
"{}: writing git branch for repo {:?}: {:?}",
client.username,
repo_path,
new_branch
);
let dot_git_dir = repo_path.join(".git");
if client.fs.metadata(&dot_git_dir).await?.is_none() {
client.fs.create_dir(&dot_git_dir).await?;
}
client.fs.set_branch_name(&dot_git_dir, new_branch).await;
}
}
Ok(())
}
@@ -859,6 +884,12 @@ fn check_consistency_between_clients(clients: &[(Rc<TestClient>, TestAppContext)
host_snapshot.abs_path(),
guest_project.remote_id(),
);
assert_eq!(guest_snapshot.repositories().collect::<Vec<_>>(), host_snapshot.repositories().collect::<Vec<_>>(),
"{} has different repositories than the host for worktree {:?} and project {:?}",
client.username,
host_snapshot.abs_path(),
guest_project.remote_id(),
);
assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(),
"{} has different scan id than the host for worktree {:?} and project {:?}",
client.username,
@@ -1151,6 +1182,10 @@ enum ClientOperation {
repo_path: PathBuf,
contents: Vec<(PathBuf, String)>,
},
WriteGitBranch {
repo_path: PathBuf,
new_branch: Option<String>,
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -1664,10 +1699,11 @@ impl TestPlan {
}
// Update a git index
91..=95 => {
91..=93 => {
let repo_path = client
.fs
.directories()
.into_iter()
.choose(&mut self.rng)
.unwrap()
.clone();
@@ -1698,6 +1734,24 @@ impl TestPlan {
};
}
// Update a git branch
94..=95 => {
let repo_path = client
.fs
.directories()
.choose(&mut self.rng)
.unwrap()
.clone();
let new_branch = (self.rng.gen_range(0..10) > 3)
.then(|| Alphanumeric.sample_string(&mut self.rng, 8));
break ClientOperation::WriteGitBranch {
repo_path,
new_branch,
};
}
// Create or update a file or directory
96.. => {
let is_dir = self.rng.gen::<bool>();

View File

@@ -39,12 +39,13 @@ settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow = "1.0"
futures = "0.3"
log = "0.4"
postage = { workspace = true }
serde = { workspace = true }
serde_derive = { workspace = true }
anyhow.workspace = true
futures.workspace = true
log.workspace = true
postage.workspace = true
serde.workspace = true
serde_derive.workspace = true
[dev-dependencies]
call = { path = "../call", features = ["test-support"] }

View File

@@ -1,10 +1,9 @@
use crate::{
collaborator_list_popover, collaborator_list_popover::CollaboratorListPopover,
contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
ToggleScreenSharing,
toggle_screen_sharing, ToggleScreenSharing,
};
use call::{ActiveCall, ParticipantLocation, Room};
use client::{proto::PeerId, ContactEventKind, SignIn, SignOut, User, UserStore};
use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
use clock::ReplicaId;
use contacts_popover::ContactsPopover;
use context_menu::{ContextMenu, ContextMenuItem};
@@ -13,22 +12,23 @@ use gpui::{
color::Color,
elements::*,
geometry::{rect::RectF, vector::vec2f, PathBuilder},
impl_internal_actions,
json::{self, ToJson},
platform::{CursorStyle, MouseButton},
AppContext, Entity, ImageData, ModelHandle, RenderContext, Subscription, View, ViewContext,
ViewHandle, WeakViewHandle,
AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use project::Project;
use settings::Settings;
use std::{ops::Range, sync::Arc};
use theme::{AvatarStyle, Theme};
use util::ResultExt;
use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace};
use workspace::{FollowNextCollaborator, Workspace};
const MAX_TITLE_LENGTH: usize = 75;
actions!(
collab,
[
ToggleCollaboratorList,
ToggleContactsMenu,
ToggleUserMenu,
ShareProject,
@@ -36,26 +36,20 @@ actions!(
]
);
impl_internal_actions!(collab, [LeaveCall]);
#[derive(Copy, Clone, PartialEq)]
pub(crate) struct LeaveCall;
pub fn init(cx: &mut AppContext) {
cx.add_action(CollabTitlebarItem::toggle_collaborator_list_popover);
cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
cx.add_action(CollabTitlebarItem::share_project);
cx.add_action(CollabTitlebarItem::unshare_project);
cx.add_action(CollabTitlebarItem::leave_call);
cx.add_action(CollabTitlebarItem::toggle_user_menu);
}
pub struct CollabTitlebarItem {
workspace: WeakViewHandle<Workspace>,
project: ModelHandle<Project>,
user_store: ModelHandle<UserStore>,
client: Arc<Client>,
workspace: WeakViewHandle<Workspace>,
contacts_popover: Option<ViewHandle<ContactsPopover>>,
user_menu: ViewHandle<ContextMenu>,
collaborator_list_popover: Option<ViewHandle<CollaboratorListPopover>>,
_subscriptions: Vec<Subscription>,
}
@@ -68,41 +62,22 @@ impl View for CollabTitlebarItem {
"CollabTitlebarItem"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let workspace = if let Some(workspace) = self.workspace.upgrade(cx) {
workspace
} else {
return Empty::new().boxed();
return Empty::new().into_any();
};
let project = workspace.read(cx).project().read(cx);
let mut project_title = String::new();
for (i, name) in project.worktree_root_names(cx).enumerate() {
if i > 0 {
project_title.push_str(", ");
}
project_title.push_str(name);
}
if project_title.is_empty() {
project_title = "empty project".to_owned();
}
let project = self.project.read(cx);
let theme = cx.global::<Settings>().theme.clone();
let mut left_container = Flex::row();
let mut right_container = Flex::row().align_children_center();
left_container.add_child(
Label::new(project_title, theme.workspace.titlebar.title.clone())
.contained()
.with_margin_right(theme.workspace.titlebar.item_spacing)
.aligned()
.left()
.boxed(),
);
left_container.add_child(self.collect_title_root_names(&project, theme.clone(), cx));
let user = workspace.read(cx).user_store().read(cx).current_user();
let peer_id = workspace.read(cx).client().peer_id();
let user = self.user_store.read(cx).current_user();
let peer_id = self.client.peer_id();
if let Some(((user, peer_id), room)) = user
.zip(peer_id)
.zip(ActiveCall::global(cx).read(cx).room().cloned())
@@ -128,28 +103,46 @@ impl View for CollabTitlebarItem {
}
Stack::new()
.with_child(left_container.boxed())
.with_child(right_container.aligned().right().boxed())
.boxed()
.with_child(left_container)
.with_child(
Flex::row()
.with_child(
right_container.contained().with_background_color(
theme
.workspace
.titlebar
.container
.background_color
.unwrap_or_else(|| Color::transparent_black()),
),
)
.aligned()
.right(),
)
.into_any()
}
}
impl CollabTitlebarItem {
pub fn new(
workspace: &ViewHandle<Workspace>,
user_store: &ModelHandle<UserStore>,
workspace: &Workspace,
workspace_handle: &ViewHandle<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let project = workspace.project().clone();
let user_store = workspace.app_state().user_store.clone();
let client = workspace.app_state().client.clone();
let active_call = ActiveCall::global(cx);
let mut subscriptions = Vec::new();
subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify()));
subscriptions.push(cx.observe(workspace_handle, |_, _, cx| cx.notify()));
subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify()));
subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
subscriptions.push(cx.observe_window_activation(|this, active, cx| {
this.window_activation_changed(active, cx)
}));
subscriptions.push(cx.observe(user_store, |_, _, cx| cx.notify()));
subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
subscriptions.push(
cx.subscribe(user_store, move |this, user_store, event, cx| {
cx.subscribe(&user_store, move |this, user_store, event, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
if let client::Event::Contact { user, kind } = event {
@@ -171,31 +164,88 @@ impl CollabTitlebarItem {
}),
);
let view_id = cx.view_id();
Self {
workspace: workspace.downgrade(),
user_store: user_store.clone(),
workspace: workspace.weak_handle(),
project,
user_store,
client,
contacts_popover: None,
user_menu: cx.add_view(|cx| {
let mut menu = ContextMenu::new(cx);
let mut menu = ContextMenu::new(view_id, cx);
menu.set_position_mode(OverlayPositionMode::Local);
menu
}),
collaborator_list_popover: None,
_subscriptions: subscriptions,
}
}
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
if let Some(workspace) = self.workspace.upgrade(cx) {
let project = if active {
Some(workspace.read(cx).project().clone())
} else {
None
};
ActiveCall::global(cx)
.update(cx, |call, cx| call.set_location(project.as_ref(), cx))
.detach_and_log_err(cx);
fn collect_title_root_names(
&self,
project: &Project,
theme: Arc<Theme>,
cx: &ViewContext<Self>,
) -> AnyElement<Self> {
let names_and_branches = project.visible_worktrees(cx).map(|worktree| {
let worktree = worktree.read(cx);
(worktree.root_name(), worktree.root_git_entry())
});
fn push_str(buffer: &mut String, index: &mut usize, str: &str) {
buffer.push_str(str);
*index += str.chars().count();
}
let mut indices = Vec::new();
let mut index = 0;
let mut title = String::new();
let mut names_and_branches = names_and_branches.peekable();
while let Some((name, entry)) = names_and_branches.next() {
let pre_index = index;
push_str(&mut title, &mut index, name);
indices.extend((pre_index..index).into_iter());
if let Some(branch) = entry.and_then(|entry| entry.branch()) {
push_str(&mut title, &mut index, "/");
push_str(&mut title, &mut index, &branch);
}
if names_and_branches.peek().is_some() {
push_str(&mut title, &mut index, ", ");
if index >= MAX_TITLE_LENGTH {
title.push_str("");
break;
}
}
}
let text_style = theme.workspace.titlebar.title.clone();
let item_spacing = theme.workspace.titlebar.item_spacing;
let mut highlight = text_style.clone();
highlight.color = theme.workspace.titlebar.highlight_color;
let style = LabelStyle {
text: text_style,
highlight_text: Some(highlight),
};
Label::new(title, style)
.with_highlights(indices)
.contained()
.with_margin_right(item_spacing)
.aligned()
.left()
.into_any_named("title-with-git-information")
}
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
let project = if active {
Some(self.project.clone())
} else {
None
};
ActiveCall::global(cx)
.update(cx, |call, cx| call.set_location(project.as_ref(), cx))
.detach_and_log_err(cx);
}
fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
@@ -206,73 +256,42 @@ impl CollabTitlebarItem {
}
fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
if let Some(workspace) = self.workspace.upgrade(cx) {
let active_call = ActiveCall::global(cx);
let project = workspace.read(cx).project().clone();
active_call
.update(cx, |call, cx| call.share_project(project, cx))
.detach_and_log_err(cx);
}
let active_call = ActiveCall::global(cx);
let project = self.project.clone();
active_call
.update(cx, |call, cx| call.share_project(project, cx))
.detach_and_log_err(cx);
}
fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
if let Some(workspace) = self.workspace.upgrade(cx) {
let active_call = ActiveCall::global(cx);
let project = workspace.read(cx).project().clone();
active_call
.update(cx, |call, cx| call.unshare_project(project, cx))
.log_err();
}
}
pub fn toggle_collaborator_list_popover(
&mut self,
_: &ToggleCollaboratorList,
cx: &mut ViewContext<Self>,
) {
match self.collaborator_list_popover.take() {
Some(_) => {}
None => {
if let Some(workspace) = self.workspace.upgrade(cx) {
let user_store = workspace.read(cx).user_store().clone();
let view = cx.add_view(|cx| CollaboratorListPopover::new(user_store, cx));
cx.subscribe(&view, |this, _, event, cx| {
match event {
collaborator_list_popover::Event::Dismissed => {
this.collaborator_list_popover = None;
}
}
cx.notify();
})
.detach();
self.collaborator_list_popover = Some(view);
}
}
}
cx.notify();
let active_call = ActiveCall::global(cx);
let project = self.project.clone();
active_call
.update(cx, |call, cx| call.unshare_project(project, cx))
.log_err();
}
pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext<Self>) {
if self.contacts_popover.take().is_none() {
if let Some(workspace) = self.workspace.upgrade(cx) {
let project = workspace.read(cx).project().clone();
let user_store = workspace.read(cx).user_store().clone();
let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx));
cx.subscribe(&view, |this, _, event, cx| {
match event {
contacts_popover::Event::Dismissed => {
this.contacts_popover = None;
}
let view = cx.add_view(|cx| {
ContactsPopover::new(
self.project.clone(),
self.user_store.clone(),
self.workspace.clone(),
cx,
)
});
cx.subscribe(&view, |this, _, event, cx| {
match event {
contacts_popover::Event::Dismissed => {
this.contacts_popover = None;
}
}
cx.notify();
})
.detach();
self.contacts_popover = Some(view);
}
cx.notify();
})
.detach();
self.contacts_popover = Some(view);
}
cx.notify();
@@ -294,21 +313,27 @@ impl CollabTitlebarItem {
Color::transparent_black(),
)
}))
.with_child(
Label::new(user.github_login.clone(), item_style.label.clone())
.boxed(),
)
.with_child(Label::new(
user.github_login.clone(),
item_style.label.clone(),
))
.contained()
.with_style(item_style.container)
.boxed()
.into_any()
})),
ContextMenuItem::item("Sign out", SignOut),
ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
ContextMenuItem::action("Sign out", SignOut),
ContextMenuItem::action(
"Send Feedback",
feedback::feedback_editor::GiveFeedback,
),
]
} else {
vec![
ContextMenuItem::item("Sign in", SignIn),
ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
ContextMenuItem::action("Sign in", SignIn),
ContextMenuItem::action(
"Send Feedback",
feedback::feedback_editor::GiveFeedback,
),
]
};
@@ -316,17 +341,11 @@ impl CollabTitlebarItem {
});
}
fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx))
.detach_and_log_err(cx);
}
fn render_toggle_contacts_button(
&self,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> ElementBox {
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let titlebar = &theme.workspace.titlebar;
let badge = if self
@@ -345,14 +364,13 @@ impl CollabTitlebarItem {
.contained()
.with_margin_left(titlebar.toggle_contacts_button.default.icon_width)
.with_margin_top(titlebar.toggle_contacts_button.default.icon_width)
.aligned()
.boxed(),
.aligned(),
)
};
Stack::new()
.with_child(
MouseEventHandler::<ToggleContactsMenu>::new(0, cx, |state, _| {
MouseEventHandler::<ToggleContactsMenu, Self>::new(0, cx, |state, _| {
let style = titlebar
.toggle_contacts_button
.style_for(state, self.contacts_popover.is_some());
@@ -366,32 +384,30 @@ impl CollabTitlebarItem {
.with_height(style.button_width)
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleContactsMenu);
.on_click(MouseButton::Left, move |_, this, cx| {
this.toggle_contacts_popover(&Default::default(), cx)
})
.with_tooltip::<ToggleContactsMenu, _>(
.with_tooltip::<ToggleContactsMenu>(
0,
"Show contacts menu".into(),
Some(Box::new(ToggleContactsMenu)),
theme.tooltip.clone(),
cx,
)
.boxed(),
),
)
.with_children(badge)
.with_children(self.render_contacts_popover_host(titlebar, cx))
.boxed()
.into_any()
}
fn render_toggle_screen_sharing_button(
&self,
theme: &Theme,
room: &ModelHandle<Room>,
cx: &mut RenderContext<Self>,
) -> ElementBox {
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let icon;
let tooltip;
if room.read(cx).is_screen_sharing() {
@@ -403,7 +419,7 @@ impl CollabTitlebarItem {
}
let titlebar = &theme.workspace.titlebar;
MouseEventHandler::<ToggleScreenSharing>::new(0, cx, |state, _| {
MouseEventHandler::<ToggleScreenSharing, Self>::new(0, cx, |state, _| {
let style = titlebar.call_control.style_for(state, false);
Svg::new(icon)
.with_color(style.color)
@@ -415,13 +431,12 @@ impl CollabTitlebarItem {
.with_height(style.button_width)
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleScreenSharing);
.on_click(MouseButton::Left, move |_, _, cx| {
toggle_screen_sharing(&Default::default(), cx)
})
.with_tooltip::<ToggleScreenSharing, _>(
.with_tooltip::<ToggleScreenSharing>(
0,
tooltip.into(),
Some(Box::new(ToggleScreenSharing)),
@@ -429,15 +444,15 @@ impl CollabTitlebarItem {
cx,
)
.aligned()
.boxed()
.into_any()
}
fn render_in_call_share_unshare_button(
&self,
workspace: &ViewHandle<Workspace>,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> Option<ElementBox> {
cx: &mut ViewContext<Self>,
) -> Option<AnyElement<Self>> {
let project = workspace.read(cx).project();
if project.read(cx).is_remote() {
return None;
@@ -457,46 +472,46 @@ impl CollabTitlebarItem {
Some(
Stack::new()
.with_child(
MouseEventHandler::<ShareUnshare>::new(0, cx, |state, _| {
MouseEventHandler::<ShareUnshare, Self>::new(0, cx, |state, _| {
//TODO: Ensure this button has consistant width for both text variations
let style = titlebar
.share_button
.style_for(state, self.contacts_popover.is_some());
let style = titlebar.share_button.style_for(state, false);
Label::new(label, style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
.on_click(MouseButton::Left, move |_, this, cx| {
if is_shared {
cx.dispatch_action(UnshareProject);
this.unshare_project(&Default::default(), cx);
} else {
cx.dispatch_action(ShareProject);
this.share_project(&Default::default(), cx);
}
})
.with_tooltip::<ShareUnshare, _>(
.with_tooltip::<ShareUnshare>(
0,
tooltip.to_owned(),
None,
theme.tooltip.clone(),
cx,
)
.boxed(),
),
)
.aligned()
.contained()
.with_margin_left(theme.workspace.titlebar.item_spacing)
.boxed(),
.into_any(),
)
}
fn render_user_menu_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
fn render_user_menu_button(
&self,
theme: &Theme,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let titlebar = &theme.workspace.titlebar;
Stack::new()
.with_child(
MouseEventHandler::<ToggleUserMenu>::new(0, cx, |state, _| {
MouseEventHandler::<ToggleUserMenu, Self>::new(0, cx, |state, _| {
let style = titlebar.call_control.style_for(state, false);
Svg::new("icons/ellipsis_14.svg")
.with_color(style.color)
@@ -508,13 +523,12 @@ impl CollabTitlebarItem {
.with_height(style.button_width)
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleUserMenu);
.on_click(MouseButton::Left, move |_, this, cx| {
this.toggle_user_menu(&Default::default(), cx)
})
.with_tooltip::<ToggleUserMenu, _>(
.with_tooltip::<ToggleUserMenu>(
0,
"Toggle user menu".to_owned(),
Some(Box::new(ToggleUserMenu)),
@@ -522,49 +536,49 @@ impl CollabTitlebarItem {
cx,
)
.contained()
.with_margin_left(theme.workspace.titlebar.item_spacing)
.boxed(),
.with_margin_left(theme.workspace.titlebar.item_spacing),
)
.with_child(
ChildView::new(&self.user_menu, cx)
.aligned()
.bottom()
.right()
.boxed(),
.right(),
)
.boxed()
.into_any()
}
fn render_sign_in_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let titlebar = &theme.workspace.titlebar;
MouseEventHandler::<SignIn>::new(0, cx, |state, _| {
MouseEventHandler::<SignIn, Self>::new(0, cx, |state, _| {
let style = titlebar.sign_in_prompt.style_for(state, false);
Label::new("Sign In", style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(SignIn);
.on_click(MouseButton::Left, move |_, this, cx| {
let client = this.client.clone();
cx.app_context()
.spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await })
.detach_and_log_err(cx);
})
.boxed()
.into_any()
}
fn render_contacts_popover_host<'a>(
&'a self,
_theme: &'a theme::Titlebar,
cx: &'a RenderContext<Self>,
) -> Option<ElementBox> {
cx: &'a ViewContext<Self>,
) -> Option<AnyElement<Self>> {
self.contacts_popover.as_ref().map(|popover| {
Overlay::new(ChildView::new(popover, cx).boxed())
Overlay::new(ChildView::new(popover, cx))
.with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_corner(AnchorCorner::TopRight)
.with_z_index(999)
.aligned()
.bottom()
.right()
.boxed()
.into_any()
})
}
@@ -573,8 +587,8 @@ impl CollabTitlebarItem {
workspace: &ViewHandle<Workspace>,
theme: &Theme,
room: &ModelHandle<Room>,
cx: &mut RenderContext<Self>,
) -> Vec<ElementBox> {
cx: &mut ViewContext<Self>,
) -> Vec<Container<Self>> {
let mut participants = room
.read(cx)
.remote_participants()
@@ -602,8 +616,7 @@ impl CollabTitlebarItem {
theme,
cx,
))
.with_margin_right(theme.workspace.titlebar.face_pile_spacing)
.boxed(),
.with_margin_right(theme.workspace.titlebar.face_pile_spacing),
)
})
.collect()
@@ -615,8 +628,8 @@ impl CollabTitlebarItem {
theme: &Theme,
user: &Arc<User>,
peer_id: PeerId,
cx: &mut RenderContext<Self>,
) -> ElementBox {
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let replica_id = workspace.read(cx).project().read(cx).replica_id();
Container::new(self.render_face_pile(
user,
@@ -628,7 +641,7 @@ impl CollabTitlebarItem {
cx,
))
.with_margin_right(theme.workspace.titlebar.item_spacing)
.boxed()
.into_any()
}
fn render_face_pile(
@@ -639,8 +652,8 @@ impl CollabTitlebarItem {
location: Option<ParticipantLocation>,
workspace: &ViewHandle<Workspace>,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> ElementBox {
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let project_id = workspace.read(cx).project().read(cx).remote_id();
let room = ActiveCall::global(cx).read(cx).room();
let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
@@ -710,11 +723,9 @@ impl CollabTitlebarItem {
}
})?;
let location = remote_participant.map(|p| p.location);
Some(Self::render_face(
avatar.clone(),
Self::location_style(workspace, location, follower_style, cx),
follower_style,
background_color,
))
}))
@@ -734,7 +745,7 @@ impl CollabTitlebarItem {
}
}
container.boxed()
container
}))
.with_children((|| {
let replica_id = replica_id?;
@@ -745,56 +756,67 @@ impl CollabTitlebarItem {
.with_width(theme.workspace.titlebar.avatar_ribbon.width)
.with_height(theme.workspace.titlebar.avatar_ribbon.height)
.aligned()
.bottom()
.boxed(),
.bottom(),
)
})())
.boxed();
.into_any();
if let Some(location) = location {
if let Some(replica_id) = replica_id {
content =
MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| {
content
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleFollow(peer_id))
})
.with_tooltip::<ToggleFollow, _>(
peer_id.as_u64() as usize,
if is_being_followed {
format!("Unfollow {}", user.github_login)
} else {
format!("Follow {}", user.github_login)
},
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.boxed();
enum ToggleFollow {}
content = MouseEventHandler::<ToggleFollow, Self>::new(
replica_id.into(),
cx,
move |_, _| content,
)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, item, cx| {
if let Some(workspace) = item.workspace.upgrade(cx) {
if let Some(task) = workspace
.update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx))
{
task.detach_and_log_err(cx);
}
}
})
.with_tooltip::<ToggleFollow>(
peer_id.as_u64() as usize,
if is_being_followed {
format!("Unfollow {}", user.github_login)
} else {
format!("Follow {}", user.github_login)
},
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.into_any();
} else if let ParticipantLocation::SharedProject { project_id } = location {
enum JoinProject {}
let user_id = user.id;
content = MouseEventHandler::<JoinProject>::new(
content = MouseEventHandler::<JoinProject, Self>::new(
peer_id.as_u64() as usize,
cx,
move |_, _| content,
)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(JoinProject {
project_id,
follow_user_id: user_id,
})
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
let app_state = workspace.read(cx).app_state().clone();
workspace::join_remote_project(project_id, user_id, app_state, cx)
.detach_and_log_err(cx);
}
})
.with_tooltip::<JoinProject, _>(
.with_tooltip::<JoinProject>(
peer_id.as_u64() as usize,
format!("Follow {} into external project", user.github_login),
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.boxed();
.into_any();
}
}
content
@@ -804,7 +826,7 @@ impl CollabTitlebarItem {
workspace: &ViewHandle<Workspace>,
location: Option<ParticipantLocation>,
mut style: AvatarStyle,
cx: &RenderContext<Self>,
cx: &ViewContext<Self>,
) -> AvatarStyle {
if let Some(location) = location {
if let ParticipantLocation::SharedProject { project_id } = location {
@@ -819,11 +841,11 @@ impl CollabTitlebarItem {
style
}
fn render_face(
fn render_face<V: View>(
avatar: Arc<ImageData>,
avatar_style: AvatarStyle,
background_color: Color,
) -> ElementBox {
) -> AnyElement<V> {
Image::from_data(avatar)
.with_style(avatar_style.image)
.aligned()
@@ -834,14 +856,14 @@ impl CollabTitlebarItem {
.with_width(avatar_style.outer_width)
.with_height(avatar_style.outer_width)
.aligned()
.boxed()
.into_any()
}
fn render_connection_status(
&self,
status: &client::Status,
cx: &mut RenderContext<Self>,
) -> Option<ElementBox> {
cx: &mut ViewContext<Self>,
) -> Option<AnyElement<Self>> {
enum ConnectionStatusButton {}
let theme = &cx.global::<Settings>().theme.clone();
@@ -851,23 +873,17 @@ impl CollabTitlebarItem {
| client::Status::Reauthenticating { .. }
| client::Status::Reconnecting { .. }
| client::Status::ReconnectionError { .. } => Some(
Container::new(
Align::new(
ConstrainedBox::new(
Svg::new("icons/cloud_slash_12.svg")
.with_color(theme.workspace.titlebar.offline_icon.color)
.boxed(),
)
.with_width(theme.workspace.titlebar.offline_icon.width)
.boxed(),
)
.boxed(),
)
.with_style(theme.workspace.titlebar.offline_icon.container)
.boxed(),
Svg::new("icons/cloud_slash_12.svg")
.with_color(theme.workspace.titlebar.offline_icon.color)
.constrained()
.with_width(theme.workspace.titlebar.offline_icon.width)
.aligned()
.contained()
.with_style(theme.workspace.titlebar.offline_icon.container)
.into_any(),
),
client::Status::UpgradeRequired => Some(
MouseEventHandler::<ConnectionStatusButton>::new(0, cx, |_, _| {
MouseEventHandler::<ConnectionStatusButton, Self>::new(0, cx, |_, _| {
Label::new(
"Please update Zed to collaborate",
theme.workspace.titlebar.outdated_warning.text.clone(),
@@ -875,13 +891,12 @@ impl CollabTitlebarItem {
.contained()
.with_style(theme.workspace.titlebar.outdated_warning.container)
.aligned()
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(auto_update::Check);
.on_click(MouseButton::Left, |_, _, cx| {
auto_update::check(&Default::default(), cx);
})
.boxed(),
.into_any(),
),
_ => None,
}
@@ -898,7 +913,7 @@ impl AvatarRibbon {
}
}
impl Element for AvatarRibbon {
impl Element<CollabTitlebarItem> for AvatarRibbon {
type LayoutState = ();
type PaintState = ();
@@ -906,17 +921,20 @@ impl Element for AvatarRibbon {
fn layout(
&mut self,
constraint: gpui::SizeConstraint,
_: &mut gpui::LayoutContext,
_: &mut CollabTitlebarItem,
_: &mut LayoutContext<CollabTitlebarItem>,
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
(constraint.max, ())
}
fn paint(
&mut self,
bounds: gpui::geometry::rect::RectF,
_: gpui::geometry::rect::RectF,
scene: &mut SceneBuilder,
bounds: RectF,
_: RectF,
_: &mut Self::LayoutState,
cx: &mut gpui::PaintContext,
_: &mut CollabTitlebarItem,
_: &mut ViewContext<CollabTitlebarItem>,
) -> Self::PaintState {
let mut path = PathBuilder::new();
path.reset(bounds.lower_left());
@@ -927,7 +945,7 @@ impl Element for AvatarRibbon {
path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
path.curve_to(bounds.lower_right(), bounds.upper_right());
path.line_to(bounds.lower_left());
cx.scene.push_path(path.build(self.color, None));
scene.push_path(path.build(self.color, None));
}
fn rect_for_text_range(
@@ -937,17 +955,19 @@ impl Element for AvatarRibbon {
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &gpui::MeasurementContext,
_: &CollabTitlebarItem,
_: &ViewContext<CollabTitlebarItem>,
) -> Option<RectF> {
None
}
fn debug(
&self,
bounds: gpui::geometry::rect::RectF,
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &gpui::DebugContext,
_: &CollabTitlebarItem,
_: &ViewContext<CollabTitlebarItem>,
) -> gpui::json::Value {
json::json!({
"type": "AvatarRibbon",

View File

@@ -1,5 +1,4 @@
mod collab_titlebar_item;
mod collaborator_list_popover;
mod contact_finder;
mod contact_list;
mod contact_notification;
@@ -10,29 +9,24 @@ mod notifications;
mod project_shared_notification;
mod sharing_status_indicator;
use anyhow::anyhow;
use call::ActiveCall;
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
use gpui::{actions, AppContext, Task};
use std::sync::Arc;
use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
use workspace::AppState;
actions!(collab, [ToggleScreenSharing]);
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
collab_titlebar_item::init(cx);
contact_notification::init(cx);
contact_list::init(cx);
contact_finder::init(cx);
contacts_popover::init(cx);
incoming_call_notification::init(cx);
project_shared_notification::init(cx);
incoming_call_notification::init(&app_state, cx);
project_shared_notification::init(&app_state, cx);
sharing_status_indicator::init(cx);
cx.add_global_action(toggle_screen_sharing);
cx.add_global_action(move |action: &JoinProject, cx| {
join_project(action, app_state.clone(), cx);
});
}
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
@@ -47,88 +41,3 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
toggle_screen_sharing.detach_and_log_err(cx);
}
}
fn join_project(action: &JoinProject, app_state: Arc<AppState>, cx: &mut AppContext) {
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(window_id)?.clone().downcast::<Workspace>())
.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

@@ -1,166 +0,0 @@
use call::ActiveCall;
use client::UserStore;
use gpui::Action;
use gpui::{
actions, elements::*, platform::MouseButton, Entity, ModelHandle, 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,49 +1,42 @@
use client::{ContactRequestStatus, User, UserStore};
use gpui::{
elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState, RenderContext, Task,
View, ViewContext, ViewHandle,
};
use picker::{Picker, PickerDelegate};
use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext};
use picker::{Picker, PickerDelegate, PickerEvent};
use settings::Settings;
use std::sync::Arc;
use util::TryFutureExt;
pub fn init(cx: &mut AppContext) {
Picker::<ContactFinder>::init(cx);
Picker::<ContactFinderDelegate>::init(cx);
}
pub struct ContactFinder {
picker: ViewHandle<Picker<Self>>,
pub type ContactFinder = Picker<ContactFinderDelegate>;
pub fn build_contact_finder(
user_store: ModelHandle<UserStore>,
cx: &mut ViewContext<ContactFinder>,
) -> ContactFinder {
Picker::new(
ContactFinderDelegate {
user_store,
potential_contacts: Arc::from([]),
selected_index: 0,
},
cx,
)
.with_theme(|theme| theme.contact_finder.picker.clone())
}
pub struct ContactFinderDelegate {
potential_contacts: Arc<[Arc<User>]>,
user_store: ModelHandle<UserStore>,
selected_index: usize,
}
pub enum Event {
Dismissed,
}
impl Entity for ContactFinder {
type Event = Event;
}
impl View for ContactFinder {
fn ui_name() -> &'static str {
"ContactFinder"
impl PickerDelegate for ContactFinderDelegate {
fn placeholder_text(&self) -> Arc<str> {
"Search collaborator by username...".into()
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
ChildView::new(&self.picker, cx).boxed()
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.picker);
}
}
}
impl PickerDelegate for ContactFinder {
fn match_count(&self) -> usize {
self.potential_contacts.len()
}
@@ -52,22 +45,22 @@ impl PickerDelegate for ContactFinder {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) {
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let search_users = self
.user_store
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
cx.spawn(|this, mut cx| async move {
cx.spawn(|picker, mut cx| async move {
async {
let potential_contacts = search_users.await?;
this.update(&mut cx, |this, cx| {
this.potential_contacts = potential_contacts.into();
picker.update(&mut cx, |picker, cx| {
picker.delegate_mut().potential_contacts = potential_contacts.into();
cx.notify();
});
})?;
anyhow::Ok(())
}
.log_err()
@@ -75,7 +68,7 @@ impl PickerDelegate for ContactFinder {
})
}
fn confirm(&mut self, cx: &mut ViewContext<Self>) {
fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
if let Some(user) = self.potential_contacts.get(self.selected_index) {
let user_store = self.user_store.read(cx);
match user_store.contact_request_status(user) {
@@ -94,8 +87,8 @@ impl PickerDelegate for ContactFinder {
}
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed);
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
cx.emit(PickerEvent::Dismiss);
}
fn render_match(
@@ -104,7 +97,7 @@ impl PickerDelegate for ContactFinder {
mouse_state: &mut MouseState,
selected: bool,
cx: &gpui::AppContext,
) -> ElementBox {
) -> AnyElement<Picker<Self>> {
let theme = &cx.global::<Settings>().theme;
let user = &self.potential_contacts[ix];
let request_status = self.user_store.read(cx).contact_request_status(user);
@@ -132,15 +125,13 @@ impl PickerDelegate for ContactFinder {
.with_style(theme.contact_finder.contact_avatar)
.aligned()
.left()
.boxed()
}))
.with_child(
Label::new(user.github_login.clone(), style.label.clone())
.contained()
.with_style(theme.contact_finder.contact_username)
.aligned()
.left()
.boxed(),
.left(),
)
.with_children(icon_path.map(|icon_path| {
Svg::new(icon_path)
@@ -155,37 +146,11 @@ impl PickerDelegate for ContactFinder {
.with_height(button_style.button_width)
.aligned()
.flex_float()
.boxed()
}))
.contained()
.with_style(style.container)
.constrained()
.with_height(theme.contact_finder.row_height)
.boxed()
}
}
impl ContactFinder {
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
let this = cx.weak_handle();
Self {
picker: cx.add_view(|cx| {
Picker::new("Search collaborator by username...", this, cx)
.with_theme(|theme| theme.contact_finder.picker.clone())
}),
potential_contacts: Arc::from([]),
user_store,
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
.into_any()
}
}

View File

@@ -1,5 +1,3 @@
use super::collab_titlebar_item::LeaveCall;
use crate::contacts_popover;
use call::ActiveCall;
use client::{proto::PeerId, Contact, User, UserStore};
use editor::{Cancel, Editor};
@@ -8,10 +6,10 @@ use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
elements::*,
geometry::{rect::RectF, vector::vec2f},
impl_actions, impl_internal_actions,
impl_actions,
keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton, PromptLevel},
AppContext, Entity, ModelHandle, RenderContext, Subscription, View, ViewContext, ViewHandle,
AppContext, Entity, ModelHandle, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
};
use menu::{Confirm, SelectNext, SelectPrev};
use project::Project;
@@ -19,10 +17,9 @@ use serde::Deserialize;
use settings::Settings;
use std::{mem, sync::Arc};
use theme::IconButton;
use workspace::{JoinProject, OpenSharedScreen};
use workspace::Workspace;
impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]);
impl_internal_actions!(contact_list, [ToggleExpanded, Call]);
pub fn init(cx: &mut AppContext) {
cx.add_action(ContactList::remove_contact);
@@ -31,17 +28,6 @@ pub fn init(cx: &mut AppContext) {
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);
}
#[derive(Clone, PartialEq)]
struct ToggleExpanded(Section);
#[derive(Clone, PartialEq)]
struct Call {
recipient_user_id: u64,
initial_project: Option<ModelHandle<Project>>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
@@ -153,14 +139,16 @@ pub struct RespondToContactRequest {
}
pub enum Event {
ToggleContactFinder,
Dismissed,
}
pub struct ContactList {
entries: Vec<ContactEntry>,
match_candidates: Vec<StringMatchCandidate>,
list_state: ListState,
list_state: ListState<Self>,
project: ModelHandle<Project>,
workspace: WeakViewHandle<Workspace>,
user_store: ModelHandle<UserStore>,
filter_editor: ViewHandle<Editor>,
collapsed_sections: Vec<Section>,
@@ -172,6 +160,7 @@ impl ContactList {
pub fn new(
project: ModelHandle<Project>,
user_store: ModelHandle<UserStore>,
workspace: WeakViewHandle<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let filter_editor = cx.add_view(|cx| {
@@ -202,7 +191,7 @@ impl ContactList {
})
.detach();
let list_state = ListState::new(0, Orientation::Top, 1000., cx, move |this, ix, cx| {
let list_state = ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
let theme = cx.global::<Settings>().theme.clone();
let is_selected = this.selection == Some(ix);
let current_project_id = this.project.read(cx).remote_id();
@@ -291,6 +280,7 @@ impl ContactList {
filter_editor,
_subscriptions: subscriptions,
project,
workspace,
user_store,
};
this.update_entries(cx);
@@ -403,18 +393,11 @@ impl ContactList {
if let Some(entry) = self.entries.get(selection) {
match entry {
ContactEntry::Header(section) => {
let section = *section;
self.toggle_expanded(&ToggleExpanded(section), cx);
self.toggle_expanded(*section, cx);
}
ContactEntry::Contact { contact, calling } => {
if contact.online && !contact.busy && !calling {
self.call(
&Call {
recipient_user_id: contact.user.id,
initial_project: Some(self.project.clone()),
},
cx,
);
self.call(contact.user.id, Some(self.project.clone()), cx);
}
}
ContactEntry::ParticipantProject {
@@ -422,13 +405,23 @@ impl ContactList {
host_user_id,
..
} => {
cx.dispatch_global_action(JoinProject {
project_id: *project_id,
follow_user_id: *host_user_id,
});
if let Some(workspace) = self.workspace.upgrade(cx) {
let app_state = workspace.read(cx).app_state().clone();
workspace::join_remote_project(
*project_id,
*host_user_id,
app_state,
cx,
)
.detach_and_log_err(cx);
}
}
ContactEntry::ParticipantScreen { peer_id, .. } => {
cx.dispatch_action(OpenSharedScreen { peer_id: *peer_id });
if let Some(workspace) = self.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
workspace.open_shared_screen(*peer_id, cx)
});
}
}
_ => {}
}
@@ -436,8 +429,7 @@ impl ContactList {
}
}
fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext<Self>) {
let section = action.0;
fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
self.collapsed_sections.remove(ix);
} else {
@@ -748,14 +740,13 @@ impl ContactList {
is_pending: bool,
is_selected: bool,
theme: &theme::ContactList,
) -> ElementBox {
) -> AnyElement<Self> {
Flex::row()
.with_children(user.avatar.clone().map(|avatar| {
Image::from_data(avatar)
.with_style(theme.contact_avatar)
.aligned()
.left()
.boxed()
}))
.with_child(
Label::new(
@@ -766,16 +757,14 @@ impl ContactList {
.with_style(theme.contact_username.container)
.aligned()
.left()
.flex(1., true)
.boxed(),
.flex(1., true),
)
.with_children(if is_pending {
Some(
Label::new("Calling", theme.calling_indicator.text.clone())
.contained()
.with_style(theme.calling_indicator.container)
.aligned()
.boxed(),
.aligned(),
)
} else {
None
@@ -788,7 +777,7 @@ impl ContactList {
.contact_row
.style_for(&mut Default::default(), is_selected),
)
.boxed()
.into_any()
}
fn render_participant_project(
@@ -799,8 +788,10 @@ impl ContactList {
is_last: bool,
is_selected: bool,
theme: &theme::ContactList,
cx: &mut RenderContext<Self>,
) -> ElementBox {
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
enum JoinProject {}
let font_cache = cx.font_cache();
let host_avatar_height = theme
.contact_avatar
@@ -819,48 +810,44 @@ impl ContactList {
worktree_root_names.join(", ")
};
MouseEventHandler::<JoinProject>::new(project_id as usize, cx, |mouse_state, _| {
MouseEventHandler::<JoinProject, Self>::new(project_id as usize, cx, |mouse_state, _| {
let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
let row = theme.project_row.style_for(mouse_state, is_selected);
Flex::row()
.with_child(
Stack::new()
.with_child(
Canvas::new(move |bounds, _, cx| {
let start_x = bounds.min_x() + (bounds.width() / 2.)
- (tree_branch.width / 2.);
let end_x = bounds.max_x();
let start_y = bounds.min_y();
let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
.with_child(Canvas::new(move |scene, bounds, _, _, _| {
let start_x =
bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.);
let end_x = bounds.max_x();
let start_y = bounds.min_y();
let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, start_y),
vec2f(
start_x + tree_branch.width,
if is_last { end_y } else { bounds.max_y() },
),
scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, start_y),
vec2f(
start_x + tree_branch.width,
if is_last { end_y } else { bounds.max_y() },
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, end_y),
vec2f(end_x, end_y + tree_branch.width),
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
})
.boxed(),
)
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, end_y),
vec2f(end_x, end_y + tree_branch.width),
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
}))
.constrained()
.with_width(host_avatar_height)
.boxed(),
.with_width(host_avatar_height),
)
.with_child(
Label::new(project_name, row.name.text.clone())
@@ -868,29 +855,28 @@ impl ContactList {
.left()
.contained()
.with_style(row.name.container)
.flex(1., false)
.boxed(),
.flex(1., false),
)
.constrained()
.with_height(theme.row_height)
.contained()
.with_style(row.container)
.boxed()
})
.with_cursor_style(if !is_current {
CursorStyle::PointingHand
} else {
CursorStyle::Arrow
})
.on_click(MouseButton::Left, move |_, cx| {
.on_click(MouseButton::Left, move |_, this, cx| {
if !is_current {
cx.dispatch_global_action(JoinProject {
project_id,
follow_user_id: host_user_id,
});
if let Some(workspace) = this.workspace.upgrade(cx) {
let app_state = workspace.read(cx).app_state().clone();
workspace::join_remote_project(project_id, host_user_id, app_state, cx)
.detach_and_log_err(cx);
}
}
})
.boxed()
.into_any()
}
fn render_participant_screen(
@@ -898,8 +884,10 @@ impl ContactList {
is_last: bool,
is_selected: bool,
theme: &theme::ContactList,
cx: &mut RenderContext<Self>,
) -> ElementBox {
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
enum OpenSharedScreen {}
let font_cache = cx.font_cache();
let host_avatar_height = theme
.contact_avatar
@@ -913,7 +901,7 @@ impl ContactList {
let baseline_offset =
row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
MouseEventHandler::<OpenSharedScreen>::new(
MouseEventHandler::<OpenSharedScreen, Self>::new(
peer_id.as_u64() as usize,
cx,
|mouse_state, _| {
@@ -923,42 +911,37 @@ impl ContactList {
Flex::row()
.with_child(
Stack::new()
.with_child(
Canvas::new(move |bounds, _, cx| {
let start_x = bounds.min_x() + (bounds.width() / 2.)
- (tree_branch.width / 2.);
let end_x = bounds.max_x();
let start_y = bounds.min_y();
let end_y =
bounds.min_y() + baseline_offset - (cap_height / 2.);
.with_child(Canvas::new(move |scene, bounds, _, _, _| {
let start_x = bounds.min_x() + (bounds.width() / 2.)
- (tree_branch.width / 2.);
let end_x = bounds.max_x();
let start_y = bounds.min_y();
let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, start_y),
vec2f(
start_x + tree_branch.width,
if is_last { end_y } else { bounds.max_y() },
),
scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, start_y),
vec2f(
start_x + tree_branch.width,
if is_last { end_y } else { bounds.max_y() },
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, end_y),
vec2f(end_x, end_y + tree_branch.width),
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
})
.boxed(),
)
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, end_y),
vec2f(end_x, end_y + tree_branch.width),
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
}))
.constrained()
.with_width(host_avatar_height)
.boxed(),
.with_width(host_avatar_height),
)
.with_child(
Svg::new("icons/disable_screen_sharing_12.svg")
@@ -968,8 +951,7 @@ impl ContactList {
.aligned()
.left()
.contained()
.with_style(row.icon.container)
.boxed(),
.with_style(row.icon.container),
)
.with_child(
Label::new("Screen", row.name.text.clone())
@@ -977,21 +959,23 @@ impl ContactList {
.left()
.contained()
.with_style(row.name.container)
.flex(1., false)
.boxed(),
.flex(1., false),
)
.constrained()
.with_height(theme.row_height)
.contained()
.with_style(row.container)
.boxed()
},
)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(OpenSharedScreen { peer_id });
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
workspace.open_shared_screen(peer_id, cx)
});
}
})
.boxed()
.into_any()
}
fn render_header(
@@ -999,8 +983,8 @@ impl ContactList {
theme: &theme::ContactList,
is_selected: bool,
is_collapsed: bool,
cx: &mut RenderContext<Self>,
) -> ElementBox {
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
enum Header {}
enum LeaveCallContactList {}
@@ -1015,23 +999,25 @@ impl ContactList {
};
let leave_call = if section == Section::ActiveCall {
Some(
MouseEventHandler::<LeaveCallContactList>::new(0, cx, |state, _| {
MouseEventHandler::<LeaveCallContactList, Self>::new(0, cx, |state, _| {
let style = theme.leave_call.style_for(state, false);
Label::new("Leave Call", style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(LeaveCall))
.aligned()
.boxed(),
.on_click(MouseButton::Left, |_, _, cx| {
ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx))
.detach_and_log_err(cx);
})
.aligned(),
)
} else {
None
};
let icon_size = theme.section_icon_size;
MouseEventHandler::<Header>::new(section as usize, cx, |_, _| {
MouseEventHandler::<Header, Self>::new(section as usize, cx, |_, _| {
Flex::row()
.with_child(
Svg::new(if is_collapsed {
@@ -1045,8 +1031,7 @@ impl ContactList {
.with_max_height(icon_size)
.aligned()
.constrained()
.with_width(icon_size)
.boxed(),
.with_width(icon_size),
)
.with_child(
Label::new(text, header_style.text.clone())
@@ -1054,21 +1039,19 @@ impl ContactList {
.left()
.contained()
.with_margin_left(theme.contact_username.container.margin.left)
.flex(1., true)
.boxed(),
.flex(1., true),
)
.with_children(leave_call)
.constrained()
.with_height(theme.row_height)
.contained()
.with_style(header_style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleExpanded(section))
.on_click(MouseButton::Left, move |_, this, cx| {
this.toggle_expanded(section, cx);
})
.boxed()
.into_any()
}
fn render_contact(
@@ -1077,15 +1060,15 @@ impl ContactList {
project: &ModelHandle<Project>,
theme: &theme::ContactList,
is_selected: bool,
cx: &mut RenderContext<Self>,
) -> ElementBox {
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
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| {
let mut event_handler =
MouseEventHandler::<Contact, Self>::new(contact.user.id as usize, cx, |_, cx| {
Flex::row()
.with_children(contact.user.avatar.clone().map(|avatar| {
let status_badge = if contact.online {
@@ -1098,8 +1081,7 @@ impl ContactList {
} else {
theme.contact_status_free
})
.aligned()
.boxed(),
.aligned(),
)
} else {
None
@@ -1109,11 +1091,9 @@ impl ContactList {
Image::from_data(avatar)
.with_style(theme.contact_avatar)
.aligned()
.left()
.boxed(),
.left(),
)
.with_children(status_badge)
.boxed()
}))
.with_child(
Label::new(
@@ -1124,11 +1104,10 @@ impl ContactList {
.with_style(theme.contact_username.container)
.aligned()
.left()
.flex(1., true)
.boxed(),
.flex(1., true),
)
.with_child(
MouseEventHandler::<Cancel>::new(
MouseEventHandler::<Cancel, Self>::new(
contact.user.id as usize,
cx,
|mouse_state, _| {
@@ -1137,27 +1116,27 @@ impl ContactList {
render_icon_button(button_style, "icons/x_mark_8.svg")
.aligned()
.flex_float()
.boxed()
},
)
.with_padding(Padding::uniform(2.))
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(RemoveContact {
user_id,
github_login: github_login.clone(),
})
.on_click(MouseButton::Left, move |_, this, cx| {
this.remove_contact(
&RemoveContact {
user_id,
github_login: github_login.clone(),
},
cx,
);
})
.flex_float()
.boxed(),
.flex_float(),
)
.with_children(if calling {
Some(
Label::new("Calling", theme.calling_indicator.text.clone())
.contained()
.with_style(theme.calling_indicator.container)
.aligned()
.boxed(),
.aligned(),
)
} else {
None
@@ -1170,22 +1149,18 @@ impl ContactList {
.contact_row
.style_for(&mut Default::default(), is_selected),
)
.boxed()
})
.on_click(MouseButton::Left, move |_, cx| {
.on_click(MouseButton::Left, move |_, this, cx| {
if online && !busy {
cx.dispatch_action(Call {
recipient_user_id: user_id,
initial_project: Some(initial_project.clone()),
});
this.call(user_id, Some(initial_project.clone()), cx);
}
});
if online {
element = element.with_cursor_style(CursorStyle::PointingHand);
event_handler = event_handler.with_cursor_style(CursorStyle::PointingHand);
}
element.boxed()
event_handler.into_any()
}
fn render_contact_request(
@@ -1194,8 +1169,8 @@ impl ContactList {
theme: &theme::ContactList,
is_incoming: bool,
is_selected: bool,
cx: &mut RenderContext<Self>,
) -> ElementBox {
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
enum Decline {}
enum Accept {}
enum Cancel {}
@@ -1206,7 +1181,6 @@ impl ContactList {
.with_style(theme.contact_avatar)
.aligned()
.left()
.boxed()
}))
.with_child(
Label::new(
@@ -1217,8 +1191,7 @@ impl ContactList {
.with_style(theme.contact_username.container)
.aligned()
.left()
.flex(1., true)
.boxed(),
.flex(1., true),
);
let user_id = user.id;
@@ -1227,28 +1200,31 @@ impl ContactList {
let button_spacing = theme.contact_button_spacing;
if is_incoming {
row.add_children([
MouseEventHandler::<Decline>::new(user.id as usize, cx, |mouse_state, _| {
row.add_child(
MouseEventHandler::<Decline, Self>::new(user.id as usize, cx, |mouse_state, _| {
let button_style = if is_contact_request_pending {
&theme.disabled_button
} else {
theme.contact_button.style_for(mouse_state, false)
};
render_icon_button(button_style, "icons/x_mark_8.svg")
.aligned()
.boxed()
render_icon_button(button_style, "icons/x_mark_8.svg").aligned()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(RespondToContactRequest {
user_id,
accept: false,
})
.on_click(MouseButton::Left, move |_, this, cx| {
this.respond_to_contact_request(
&RespondToContactRequest {
user_id,
accept: false,
},
cx,
);
})
.contained()
.with_margin_right(button_spacing)
.boxed(),
MouseEventHandler::<Accept>::new(user.id as usize, cx, |mouse_state, _| {
.with_margin_right(button_spacing),
);
row.add_child(
MouseEventHandler::<Accept, Self>::new(user.id as usize, cx, |mouse_state, _| {
let button_style = if is_contact_request_pending {
&theme.disabled_button
} else {
@@ -1257,20 +1233,21 @@ impl ContactList {
render_icon_button(button_style, "icons/check_8.svg")
.aligned()
.flex_float()
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(RespondToContactRequest {
user_id,
accept: true,
})
})
.boxed(),
]);
.on_click(MouseButton::Left, move |_, this, cx| {
this.respond_to_contact_request(
&RespondToContactRequest {
user_id,
accept: true,
},
cx,
);
}),
);
} else {
row.add_child(
MouseEventHandler::<Cancel>::new(user.id as usize, cx, |mouse_state, _| {
MouseEventHandler::<Cancel, Self>::new(user.id as usize, cx, |mouse_state, _| {
let button_style = if is_contact_request_pending {
&theme.disabled_button
} else {
@@ -1279,18 +1256,19 @@ impl ContactList {
render_icon_button(button_style, "icons/x_mark_8.svg")
.aligned()
.flex_float()
.boxed()
})
.with_padding(Padding::uniform(2.))
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(RemoveContact {
user_id,
github_login: github_login.clone(),
})
.on_click(MouseButton::Left, move |_, this, cx| {
this.remove_contact(
&RemoveContact {
user_id,
github_login: github_login.clone(),
},
cx,
);
})
.flex_float()
.boxed(),
.flex_float(),
);
}
@@ -1302,12 +1280,15 @@ impl ContactList {
.contact_row
.style_for(&mut Default::default(), is_selected),
)
.boxed()
.into_any()
}
fn call(&mut self, action: &Call, cx: &mut ViewContext<Self>) {
let recipient_user_id = action.recipient_user_id;
let initial_project = action.initial_project.clone();
fn call(
&mut self,
recipient_user_id: u64,
initial_project: Option<ModelHandle<Project>>,
cx: &mut ViewContext<Self>,
) {
ActiveCall::global(cx)
.update(cx, |call, cx| {
call.invite(recipient_user_id, initial_project, cx)
@@ -1325,13 +1306,12 @@ impl View for ContactList {
"ContactList"
}
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.add_identifier("menu");
cx
fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
Self::reset_to_default_keymap_context(keymap);
keymap.add_identifier("menu");
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
enum AddContact {}
let theme = cx.global::<Settings>().theme.clone();
@@ -1342,36 +1322,32 @@ impl View for ContactList {
ChildView::new(&self.filter_editor, cx)
.contained()
.with_style(theme.contact_list.user_query_editor.container)
.flex(1., true)
.boxed(),
.flex(1., true),
)
.with_child(
MouseEventHandler::<AddContact>::new(0, cx, |_, _| {
MouseEventHandler::<AddContact, Self>::new(0, cx, |_, _| {
render_icon_button(
&theme.contact_list.add_contact_button,
"icons/user_plus_16.svg",
)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(contacts_popover::ToggleContactFinder)
.on_click(MouseButton::Left, |_, _, cx| {
cx.emit(Event::ToggleContactFinder)
})
.with_tooltip::<AddContact, _>(
.with_tooltip::<AddContact>(
0,
"Search for new contact".into(),
None,
theme.tooltip.clone(),
cx,
)
.boxed(),
),
)
.constrained()
.with_height(theme.contact_list.user_query_editor_height)
.boxed(),
.with_height(theme.contact_list.user_query_editor_height),
)
.with_child(List::new(self.list_state.clone()).flex(1., false).boxed())
.boxed()
.with_child(List::new(self.list_state.clone()).flex(1., false))
.into_any()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
@@ -1387,7 +1363,7 @@ impl View for ContactList {
}
}
fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element {
fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<ContactList> {
Svg::new(svg_path)
.with_color(style.color)
.constrained()

View File

@@ -2,19 +2,9 @@ use std::sync::Arc;
use crate::notifications::render_user_notification;
use client::{ContactEventKind, User, UserStore};
use gpui::{
elements::*, impl_internal_actions, AppContext, Entity, ModelHandle, RenderContext, View,
ViewContext,
};
use gpui::{elements::*, Entity, ModelHandle, View, ViewContext};
use workspace::notifications::Notification;
impl_internal_actions!(contact_notifications, [Dismiss, RespondToContactRequest]);
pub fn init(cx: &mut AppContext) {
cx.add_action(ContactNotification::dismiss);
cx.add_action(ContactNotification::respond_to_contact_request);
}
pub struct ContactNotification {
user_store: ModelHandle<UserStore>,
user: Arc<User>,
@@ -43,26 +33,24 @@ impl View for ContactNotification {
"ContactNotification"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
match self.kind {
ContactEventKind::Requested => render_user_notification(
self.user.clone(),
"wants to add you as a contact",
Some("They won't be alerted if you decline."),
Dismiss(self.user.id),
|notification, cx| notification.dismiss(cx),
vec![
(
"Decline",
Box::new(RespondToContactRequest {
user_id: self.user.id,
accept: false,
Box::new(|notification, cx| {
notification.respond_to_contact_request(false, cx)
}),
),
(
"Accept",
Box::new(RespondToContactRequest {
user_id: self.user.id,
accept: true,
Box::new(|notification, cx| {
notification.respond_to_contact_request(true, cx)
}),
),
],
@@ -72,7 +60,7 @@ impl View for ContactNotification {
self.user.clone(),
"accepted your contact request",
None,
Dismiss(self.user.id),
|notification, cx| notification.dismiss(cx),
vec![],
cx,
),
@@ -114,7 +102,7 @@ impl ContactNotification {
}
}
fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
self.user_store.update(cx, |store, cx| {
store
.dismiss_contact_request(self.user.id, cx)
@@ -123,14 +111,10 @@ impl ContactNotification {
cx.emit(Event::Dismiss);
}
fn respond_to_contact_request(
&mut self,
action: &RespondToContactRequest,
cx: &mut ViewContext<Self>,
) {
fn respond_to_contact_request(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
self.user_store
.update(cx, |store, cx| {
store.respond_to_contact_request(action.user_id, action.accept, cx)
store.respond_to_contact_request(self.user.id, accept, cx)
})
.detach();
}

View File

@@ -1,11 +1,16 @@
use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleContactsMenu};
use crate::{
contact_finder::{build_contact_finder, ContactFinder},
contact_list::ContactList,
};
use client::UserStore;
use gpui::{
actions, elements::*, platform::MouseButton, AppContext, Entity, ModelHandle, RenderContext,
View, ViewContext, ViewHandle,
actions, elements::*, platform::MouseButton, AppContext, Entity, ModelHandle, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use picker::PickerEvent;
use project::Project;
use settings::Settings;
use workspace::Workspace;
actions!(contacts_popover, [ToggleContactFinder]);
@@ -26,6 +31,7 @@ pub struct ContactsPopover {
child: Child,
project: ModelHandle<Project>,
user_store: ModelHandle<UserStore>,
workspace: WeakViewHandle<Workspace>,
_subscription: Option<gpui::Subscription>,
}
@@ -33,14 +39,16 @@ impl ContactsPopover {
pub fn new(
project: ModelHandle<Project>,
user_store: ModelHandle<UserStore>,
workspace: WeakViewHandle<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let mut this = Self {
child: Child::ContactList(
cx.add_view(|cx| ContactList::new(project.clone(), user_store.clone(), cx)),
),
child: Child::ContactList(cx.add_view(|cx| {
ContactList::new(project.clone(), user_store.clone(), workspace.clone(), cx)
})),
project,
user_store,
workspace,
_subscription: None,
};
this.show_contact_list(String::new(), cx);
@@ -50,19 +58,19 @@ impl ContactsPopover {
fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext<Self>) {
match &self.child {
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)
}
Child::ContactFinder(finder) => self.show_contact_list(finder.read(cx).query(cx), 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)
let finder = build_contact_finder(self.user_store.clone(), cx);
finder.set_query(editor_text, cx);
finder
});
cx.focus(&child);
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
crate::contact_finder::Event::Dismissed => cx.emit(Event::Dismissed),
PickerEvent::Dismiss => cx.emit(Event::Dismissed),
}));
self.child = Child::ContactFinder(child);
cx.notify();
@@ -70,12 +78,20 @@ impl ContactsPopover {
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)
ContactList::new(
self.project.clone(),
self.user_store.clone(),
self.workspace.clone(),
cx,
)
.with_editor_text(editor_text, cx)
});
cx.focus(&child);
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
self._subscription = Some(cx.subscribe(&child, |this, _, event, cx| match event {
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
crate::contact_list::Event::ToggleContactFinder => {
this.toggle_contact_finder(&Default::default(), cx)
}
}));
self.child = Child::ContactList(child);
cx.notify();
@@ -91,27 +107,24 @@ impl View for ContactsPopover {
"ContactsPopover"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = cx.global::<Settings>().theme.clone();
let child = match &self.child {
Child::ContactList(child) => ChildView::new(child, cx),
Child::ContactFinder(child) => ChildView::new(child, cx),
};
MouseEventHandler::<ContactsPopover>::new(0, cx, |_, _| {
MouseEventHandler::<ContactsPopover, Self>::new(0, cx, |_, _| {
Flex::column()
.with_child(child.flex(1., true).boxed())
.with_child(child.flex(1., true))
.contained()
.with_style(theme.contacts_popover.container)
.constrained()
.with_width(theme.contacts_popover.width)
.with_height(theme.contacts_popover.height)
.boxed()
})
.on_down_out(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleContactsMenu);
})
.boxed()
.on_down_out(MouseButton::Left, move |_, _, cx| cx.emit(Event::Dismissed))
.into_any()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {

View File

@@ -7,12 +7,14 @@ use gpui::{
},
json::ToJson,
serde_json::{self, json},
Axis, DebugContext, Element, ElementBox, MeasurementContext, PaintContext,
AnyElement, Axis, Element, LayoutContext, SceneBuilder, ViewContext,
};
use crate::CollabTitlebarItem;
pub(crate) struct FacePile {
overlap: f32,
faces: Vec<ElementBox>,
faces: Vec<AnyElement<CollabTitlebarItem>>,
}
impl FacePile {
@@ -24,20 +26,21 @@ impl FacePile {
}
}
impl Element for FacePile {
impl Element<CollabTitlebarItem> for FacePile {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: gpui::SizeConstraint,
cx: &mut gpui::LayoutContext,
view: &mut CollabTitlebarItem,
cx: &mut LayoutContext<CollabTitlebarItem>,
) -> (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 += face.layout(constraint, view, cx).x();
}
width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
@@ -46,10 +49,12 @@ impl Element for FacePile {
fn paint(
&mut self,
scene: &mut SceneBuilder,
bounds: RectF,
visible_bounds: RectF,
_layout: &mut Self::LayoutState,
cx: &mut PaintContext,
view: &mut CollabTitlebarItem,
cx: &mut ViewContext<CollabTitlebarItem>,
) -> Self::PaintState {
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
@@ -59,8 +64,8 @@ impl Element for FacePile {
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);
scene.paint_layer(None, |scene| {
face.paint(scene, vec2f(origin_x, origin_y), visible_bounds, view, cx);
});
origin_x += self.overlap;
}
@@ -75,7 +80,8 @@ impl Element for FacePile {
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &MeasurementContext,
_: &CollabTitlebarItem,
_: &ViewContext<CollabTitlebarItem>,
) -> Option<RectF> {
None
}
@@ -85,7 +91,8 @@ impl Element for FacePile {
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &DebugContext,
_: &CollabTitlebarItem,
_: &ViewContext<CollabTitlebarItem>,
) -> serde_json::Value {
json!({
"type": "FacePile",
@@ -94,8 +101,8 @@ impl Element for FacePile {
}
}
impl Extend<ElementBox> for FacePile {
fn extend<T: IntoIterator<Item = ElementBox>>(&mut self, children: T) {
impl Extend<AnyElement<CollabTitlebarItem>> for FacePile {
fn extend<T: IntoIterator<Item = AnyElement<CollabTitlebarItem>>>(&mut self, children: T) {
self.faces.extend(children);
}
}

View File

@@ -1,22 +1,20 @@
use std::sync::{Arc, Weak};
use call::{ActiveCall, IncomingCall};
use client::proto;
use futures::StreamExt;
use gpui::{
elements::*,
geometry::{rect::RectF, vector::vec2f},
impl_internal_actions,
platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
AppContext, Entity, RenderContext, View, ViewContext,
AnyElement, AppContext, Entity, View, ViewContext,
};
use settings::Settings;
use util::ResultExt;
use workspace::JoinProject;
impl_internal_actions!(incoming_call_notification, [RespondToCall]);
pub fn init(cx: &mut AppContext) {
cx.add_action(IncomingCallNotification::respond_to_call);
use workspace::AppState;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
let app_state = Arc::downgrade(app_state);
let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
cx.spawn(|mut cx| async move {
let mut notification_windows = Vec::new();
@@ -48,7 +46,7 @@ pub fn init(cx: &mut AppContext) {
is_movable: false,
screen: Some(screen),
},
|_| IncomingCallNotification::new(incoming_call.clone()),
|_| IncomingCallNotification::new(incoming_call.clone(), app_state.clone()),
);
notification_windows.push(window_id);
@@ -66,32 +64,40 @@ struct RespondToCall {
pub struct IncomingCallNotification {
call: IncomingCall,
app_state: Weak<AppState>,
}
impl IncomingCallNotification {
pub fn new(call: IncomingCall) -> Self {
Self { call }
pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
Self { call, app_state }
}
fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext<Self>) {
fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
let active_call = ActiveCall::global(cx);
if action.accept {
if accept {
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
let caller_user_id = self.call.calling_user.id;
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
cx.spawn_weak(|_, mut cx| async move {
join.await?;
if let Some(project_id) = initial_project_id {
cx.update(|cx| {
cx.dispatch_global_action(JoinProject {
project_id,
follow_user_id: caller_user_id,
})
});
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
let app_state = self.app_state.clone();
cx.app_context()
.spawn(|mut cx| async move {
join.await?;
if let Some(project_id) = initial_project_id {
cx.update(|cx| {
if let Some(app_state) = app_state.upgrade() {
workspace::join_remote_project(
project_id,
caller_user_id,
app_state,
cx,
)
.detach_and_log_err(cx);
}
});
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
} else {
active_call.update(cx, |active_call, _| {
active_call.decline_incoming().log_err();
@@ -99,7 +105,7 @@ impl IncomingCallNotification {
}
}
fn render_caller(&self, cx: &mut RenderContext<Self>) -> ElementBox {
fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
let default_project = proto::ParticipantProject::default();
let initial_project = self
@@ -112,7 +118,6 @@ impl IncomingCallNotification {
Image::from_data(avatar)
.with_style(theme.caller_avatar)
.aligned()
.boxed()
}))
.with_child(
Flex::column()
@@ -122,8 +127,7 @@ impl IncomingCallNotification {
theme.caller_username.text.clone(),
)
.contained()
.with_style(theme.caller_username.container)
.boxed(),
.with_style(theme.caller_username.container),
)
.with_child(
Label::new(
@@ -138,8 +142,7 @@ impl IncomingCallNotification {
theme.caller_message.text.clone(),
)
.contained()
.with_style(theme.caller_message.container)
.boxed(),
.with_style(theme.caller_message.container),
)
.with_children(if initial_project.worktree_root_names.is_empty() {
None
@@ -150,57 +153,51 @@ impl IncomingCallNotification {
theme.worktree_roots.text.clone(),
)
.contained()
.with_style(theme.worktree_roots.container)
.boxed(),
.with_style(theme.worktree_roots.container),
)
})
.contained()
.with_style(theme.caller_metadata)
.aligned()
.boxed(),
.aligned(),
)
.contained()
.with_style(theme.caller_container)
.flex(1., true)
.boxed()
.into_any()
}
fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
enum Accept {}
enum Decline {}
Flex::column()
.with_child(
MouseEventHandler::<Accept>::new(0, cx, |_, cx| {
MouseEventHandler::<Accept, Self>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
Label::new("Accept", theme.accept_button.text.clone())
.aligned()
.contained()
.with_style(theme.accept_button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(RespondToCall { accept: true });
.on_click(MouseButton::Left, |_, this, cx| {
this.respond(true, cx);
})
.flex(1., true)
.boxed(),
.flex(1., true),
)
.with_child(
MouseEventHandler::<Decline>::new(0, cx, |_, cx| {
MouseEventHandler::<Decline, Self>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
Label::new("Decline", theme.decline_button.text.clone())
.aligned()
.contained()
.with_style(theme.decline_button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(RespondToCall { accept: false });
.on_click(MouseButton::Left, |_, this, cx| {
this.respond(false, cx);
})
.flex(1., true)
.boxed(),
.flex(1., true),
)
.constrained()
.with_width(
@@ -209,7 +206,7 @@ impl IncomingCallNotification {
.incoming_call_notification
.button_width,
)
.boxed()
.into_any()
}
}
@@ -222,7 +219,7 @@ impl View for IncomingCallNotification {
"IncomingCallNotification"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let background = cx
.global::<Settings>()
.theme
@@ -235,6 +232,6 @@ impl View for IncomingCallNotification {
.contained()
.with_background_color(background)
.expanded()
.boxed()
.into_any()
}
}

View File

@@ -2,7 +2,7 @@ use client::User;
use gpui::{
elements::*,
platform::{CursorStyle, MouseButton},
Action, Element, ElementBox, RenderContext, View,
AnyElement, Element, View, ViewContext,
};
use settings::Settings;
use std::sync::Arc;
@@ -10,14 +10,18 @@ use std::sync::Arc;
enum Dismiss {}
enum Button {}
pub fn render_user_notification<V: View, A: Action + Clone>(
pub fn render_user_notification<F, V>(
user: Arc<User>,
title: &'static str,
body: Option<&'static str>,
dismiss_action: A,
buttons: Vec<(&'static str, Box<dyn Action>)>,
cx: &mut RenderContext<V>,
) -> ElementBox {
on_dismiss: F,
buttons: Vec<(&'static str, Box<dyn Fn(&mut V, &mut ViewContext<V>)>)>,
cx: &mut ViewContext<V>,
) -> AnyElement<V>
where
F: 'static + Fn(&mut V, &mut ViewContext<V>),
V: View,
{
let theme = cx.global::<Settings>().theme.clone();
let theme = &theme.contact_notification;
@@ -35,7 +39,6 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
)
.aligned()
.top()
.boxed()
}))
.with_child(
Text::new(
@@ -47,11 +50,10 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
.aligned()
.top()
.left()
.flex(1., true)
.boxed(),
.flex(1., true),
)
.with_child(
MouseEventHandler::<Dismiss>::new(user.id as usize, cx, |state, _| {
MouseEventHandler::<Dismiss, V>::new(user.id as usize, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false);
Svg::new("icons/x_mark_8.svg")
.with_color(style.color)
@@ -63,13 +65,10 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.with_padding(Padding::uniform(5.))
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_any_action(dismiss_action.boxed_clone())
})
.on_click(MouseButton::Left, move |_, view, cx| on_dismiss(view, cx))
.aligned()
.constrained()
.with_height(
@@ -78,16 +77,14 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
)
.aligned()
.top()
.flex_float()
.boxed(),
.flex_float(),
)
.named("contact notification header"),
.into_any_named("contact notification header"),
)
.with_children(body.map(|body| {
Label::new(body, theme.body_message.text.clone())
.contained()
.with_style(theme.body_message.container)
.boxed()
}))
.with_children(if buttons.is_empty() {
None
@@ -95,26 +92,21 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
Some(
Flex::row()
.with_children(buttons.into_iter().enumerate().map(
|(ix, (message, action))| {
MouseEventHandler::<Button>::new(ix, cx, |state, _| {
|(ix, (message, handler))| {
MouseEventHandler::<Button, V>::new(ix, cx, |state, _| {
let button = theme.button.style_for(state, false);
Label::new(message, button.text.clone())
.contained()
.with_style(button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_any_action(action.boxed_clone())
})
.boxed()
.on_click(MouseButton::Left, move |_, view, cx| handler(view, cx))
},
))
.aligned()
.right()
.boxed(),
.right(),
)
})
.contained()
.boxed()
.into_any()
}

View File

@@ -2,22 +2,17 @@ use call::{room, ActiveCall};
use client::User;
use collections::HashMap;
use gpui::{
actions,
elements::*,
geometry::{rect::RectF, vector::vec2f},
platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
AppContext, Entity, RenderContext, View, ViewContext,
AppContext, Entity, View, ViewContext,
};
use settings::Settings;
use std::sync::Arc;
use workspace::JoinProject;
actions!(project_shared_notification, [DismissProject]);
pub fn init(cx: &mut AppContext) {
cx.add_action(ProjectSharedNotification::join);
cx.add_action(ProjectSharedNotification::dismiss);
use std::sync::{Arc, Weak};
use workspace::AppState;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
let app_state = Arc::downgrade(app_state);
let active_call = ActiveCall::global(cx);
let mut notification_windows = HashMap::default();
cx.subscribe(&active_call, move |_, event, cx| match event {
@@ -50,6 +45,7 @@ pub fn init(cx: &mut AppContext) {
owner.clone(),
*project_id,
worktree_root_names.clone(),
app_state.clone(),
)
},
);
@@ -62,14 +58,14 @@ pub fn init(cx: &mut AppContext) {
room::Event::RemoteProjectUnshared { project_id } => {
if let Some(window_ids) = notification_windows.remove(&project_id) {
for window_id in window_ids {
cx.remove_window(window_id);
cx.update_window(window_id, |cx| cx.remove_window());
}
}
}
room::Event::Left => {
for (_, window_ids) in notification_windows.drain() {
for window_id in window_ids {
cx.remove_window(window_id);
cx.update_window(window_id, |cx| cx.remove_window());
}
}
}
@@ -82,36 +78,43 @@ pub struct ProjectSharedNotification {
project_id: u64,
worktree_root_names: Vec<String>,
owner: Arc<User>,
app_state: Weak<AppState>,
}
impl ProjectSharedNotification {
fn new(owner: Arc<User>, project_id: u64, worktree_root_names: Vec<String>) -> Self {
fn new(
owner: Arc<User>,
project_id: u64,
worktree_root_names: Vec<String>,
app_state: Weak<AppState>,
) -> Self {
Self {
project_id,
worktree_root_names,
owner,
app_state,
}
}
fn join(&mut self, _: &JoinProject, cx: &mut ViewContext<Self>) {
let window_id = cx.window_id();
cx.remove_window(window_id);
cx.propagate_action();
fn join(&mut self, cx: &mut ViewContext<Self>) {
cx.remove_window();
if let Some(app_state) = self.app_state.upgrade() {
workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
.detach_and_log_err(cx);
}
}
fn dismiss(&mut self, _: &DismissProject, cx: &mut ViewContext<Self>) {
let window_id = cx.window_id();
cx.remove_window(window_id);
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
cx.remove_window();
}
fn render_owner(&self, cx: &mut RenderContext<Self>) -> ElementBox {
fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &cx.global::<Settings>().theme.project_shared_notification;
Flex::row()
.with_children(self.owner.avatar.clone().map(|avatar| {
Image::from_data(avatar)
.with_style(theme.owner_avatar)
.aligned()
.boxed()
}))
.with_child(
Flex::column()
@@ -121,8 +124,7 @@ impl ProjectSharedNotification {
theme.owner_username.text.clone(),
)
.contained()
.with_style(theme.owner_username.container)
.boxed(),
.with_style(theme.owner_username.container),
)
.with_child(
Label::new(
@@ -137,8 +139,7 @@ impl ProjectSharedNotification {
theme.message.text.clone(),
)
.contained()
.with_style(theme.message.container)
.boxed(),
.with_style(theme.message.container),
)
.with_children(if self.worktree_root_names.is_empty() {
None
@@ -149,63 +150,49 @@ impl ProjectSharedNotification {
theme.worktree_roots.text.clone(),
)
.contained()
.with_style(theme.worktree_roots.container)
.boxed(),
.with_style(theme.worktree_roots.container),
)
})
.contained()
.with_style(theme.owner_metadata)
.aligned()
.boxed(),
.aligned(),
)
.contained()
.with_style(theme.owner_container)
.flex(1., true)
.boxed()
.into_any()
}
fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
enum Open {}
enum Dismiss {}
let project_id = self.project_id;
let owner_user_id = self.owner.id;
Flex::column()
.with_child(
MouseEventHandler::<Open>::new(0, cx, |_, cx| {
MouseEventHandler::<Open, Self>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.project_shared_notification;
Label::new("Open", theme.open_button.text.clone())
.aligned()
.contained()
.with_style(theme.open_button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(JoinProject {
project_id,
follow_user_id: owner_user_id,
});
})
.flex(1., true)
.boxed(),
.on_click(MouseButton::Left, move |_, this, cx| this.join(cx))
.flex(1., true),
)
.with_child(
MouseEventHandler::<Dismiss>::new(0, cx, |_, cx| {
MouseEventHandler::<Dismiss, Self>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.project_shared_notification;
Label::new("Dismiss", theme.dismiss_button.text.clone())
.aligned()
.contained()
.with_style(theme.dismiss_button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(DismissProject);
.on_click(MouseButton::Left, |_, this, cx| {
this.dismiss(cx);
})
.flex(1., true)
.boxed(),
.flex(1., true),
)
.constrained()
.with_width(
@@ -214,7 +201,7 @@ impl ProjectSharedNotification {
.project_shared_notification
.button_width,
)
.boxed()
.into_any()
}
}
@@ -227,7 +214,7 @@ impl View for ProjectSharedNotification {
"ProjectSharedNotification"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
let background = cx
.global::<Settings>()
.theme
@@ -239,6 +226,6 @@ impl View for ProjectSharedNotification {
.contained()
.with_background_color(background)
.expanded()
.boxed()
.into_any()
}
}

View File

@@ -1,14 +1,13 @@
use crate::toggle_screen_sharing;
use call::ActiveCall;
use gpui::{
color::Color,
elements::{MouseEventHandler, Svg},
platform::{Appearance, MouseButton},
AppContext, Element, ElementBox, Entity, RenderContext, View,
AnyElement, AppContext, Element, Entity, View, ViewContext,
};
use settings::Settings;
use crate::ToggleScreenSharing;
pub fn init(cx: &mut AppContext) {
let active_call = ActiveCall::global(cx);
@@ -20,10 +19,10 @@ pub fn init(cx: &mut AppContext) {
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);
cx.update_window(window_id, |cx| cx.remove_window());
}
} else if let Some((window_id, _)) = status_indicator.take() {
cx.remove_status_bar_item(window_id);
cx.update_window(window_id, |cx| cx.remove_window());
}
})
.detach();
@@ -40,23 +39,22 @@ impl View for SharingStatusIndicator {
"SharingStatusIndicator"
}
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
let color = match cx.appearance {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let color = match cx.window_appearance() {
Appearance::Light | Appearance::VibrantLight => Color::black(),
Appearance::Dark | Appearance::VibrantDark => Color::white(),
};
MouseEventHandler::<Self>::new(0, cx, |_, _| {
MouseEventHandler::<Self, 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);
.on_click(MouseButton::Left, |_, _, cx| {
toggle_screen_sharing(&Default::default(), cx)
})
.boxed()
.into_any()
}
}

View File

@@ -24,7 +24,7 @@ workspace = { path = "../workspace" }
gpui = { path = "../gpui", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
serde_json = { workspace = true }
serde_json.workspace = true
workspace = { path = "../workspace", features = ["test-support"] }
ctor = "0.1"
env_logger = "0.9"
ctor.workspace = true
env_logger.workspace = true

View File

@@ -1,26 +1,25 @@
use collections::CommandPaletteFilter;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions,
elements::{ChildView, Flex, Label, ParentElement},
keymap_matcher::Keystroke,
Action, AnyViewHandle, AppContext, Element, Entity, MouseState, RenderContext, View,
ViewContext, ViewHandle,
actions, elements::*, keymap_matcher::Keystroke, Action, AppContext, Element, MouseState,
ViewContext,
};
use picker::{Picker, PickerDelegate};
use picker::{Picker, PickerDelegate, PickerEvent};
use settings::Settings;
use std::cmp;
use util::ResultExt;
use workspace::Workspace;
pub fn init(cx: &mut AppContext) {
cx.add_action(CommandPalette::toggle);
Picker::<CommandPalette>::init(cx);
cx.add_action(toggle_command_palette);
CommandPalette::init(cx);
}
actions!(command_palette, [Toggle]);
pub struct CommandPalette {
picker: ViewHandle<Picker<Self>>,
pub type CommandPalette = Picker<CommandPaletteDelegate>;
pub struct CommandPaletteDelegate {
actions: Vec<Command>,
matches: Vec<StringMatch>,
selected_ix: usize,
@@ -42,104 +41,29 @@ struct Command {
keystrokes: Vec<Keystroke>,
}
impl CommandPalette {
pub fn new(focused_view_id: usize, cx: &mut ViewContext<Self>) -> Self {
let this = cx.weak_handle();
let actions = cx
.available_actions(cx.window_id(), focused_view_id)
.filter_map(|(name, action, bindings)| {
if cx.has_global::<CommandPaletteFilter>() {
let filter = cx.global::<CommandPaletteFilter>();
if filter.filtered_namespaces.contains(action.namespace()) {
return None;
}
}
fn toggle_command_palette(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
let focused_view_id = cx.focused_view_id().unwrap_or_else(|| cx.view_id());
workspace.toggle_modal(cx, |_, cx| {
cx.add_view(|cx| Picker::new(CommandPaletteDelegate::new(focused_view_id), cx))
});
}
Some(Command {
name: humanize_action_name(name),
action,
keystrokes: bindings
.iter()
.map(|binding| binding.keystrokes())
.last()
.map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
})
})
.collect();
let picker = cx.add_view(|cx| Picker::new("Execute a command...", this, cx));
impl CommandPaletteDelegate {
pub fn new(focused_view_id: usize) -> Self {
Self {
picker,
actions,
actions: Default::default(),
matches: vec![],
selected_ix: 0,
focused_view_id,
}
}
fn toggle(_: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
let workspace = cx.handle();
let window_id = cx.window_id();
let focused_view_id = cx
.focused_view_id(window_id)
.unwrap_or_else(|| workspace.id());
cx.as_mut().defer(move |cx| {
let this = cx.add_view(&workspace, |cx| Self::new(focused_view_id, cx));
workspace.update(cx, |workspace, cx| {
workspace.toggle_modal(cx, |_, cx| {
cx.subscribe(&this, Self::on_event).detach();
this
});
});
});
}
fn on_event(
workspace: &mut Workspace,
_: ViewHandle<Self>,
event: &Event,
cx: &mut ViewContext<Workspace>,
) {
match event {
Event::Dismissed => workspace.dismiss_modal(cx),
Event::Confirmed {
window_id,
focused_view_id,
action,
} => {
let window_id = *window_id;
let focused_view_id = *focused_view_id;
let action = action.boxed_clone();
workspace.dismiss_modal(cx);
cx.as_mut()
.defer(move |cx| cx.dispatch_any_action_at(window_id, focused_view_id, action))
}
}
}
}
impl Entity for CommandPalette {
type Event = Event;
}
impl View for CommandPalette {
fn ui_name() -> &'static str {
"CommandPalette"
impl PickerDelegate for CommandPaletteDelegate {
fn placeholder_text(&self) -> std::sync::Arc<str> {
"Execute a command...".into()
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
ChildView::new(&self.picker, cx).boxed()
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.picker);
}
}
}
impl PickerDelegate for CommandPalette {
fn match_count(&self) -> usize {
self.matches.len()
}
@@ -148,26 +72,55 @@ impl PickerDelegate for CommandPalette {
self.selected_ix
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) {
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
self.selected_ix = ix;
}
fn update_matches(
&mut self,
query: String,
cx: &mut gpui::ViewContext<Self>,
cx: &mut ViewContext<Picker<Self>>,
) -> gpui::Task<()> {
let candidates = self
.actions
.iter()
.enumerate()
.map(|(ix, command)| StringMatchCandidate {
id: ix,
string: command.name.to_string(),
char_bag: command.name.chars().collect(),
})
.collect::<Vec<_>>();
cx.spawn(move |this, mut cx| async move {
let window_id = cx.window_id();
let view_id = self.focused_view_id;
cx.spawn(move |picker, mut cx| async move {
let actions = cx
.available_actions(window_id, view_id)
.into_iter()
.filter_map(|(name, action, bindings)| {
let filtered = cx.read(|cx| {
if cx.has_global::<CommandPaletteFilter>() {
let filter = cx.global::<CommandPaletteFilter>();
filter.filtered_namespaces.contains(action.namespace())
} else {
false
}
});
if filtered {
None
} else {
Some(Command {
name: humanize_action_name(name),
action,
keystrokes: bindings
.iter()
.map(|binding| binding.keystrokes())
.last()
.map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
})
}
})
.collect::<Vec<_>>();
let candidates = actions
.iter()
.enumerate()
.map(|(ix, command)| StringMatchCandidate {
id: ix,
string: command.name.to_string(),
char_bag: command.name.chars().collect(),
})
.collect::<Vec<_>>();
let matches = if query.is_empty() {
candidates
.into_iter()
@@ -190,32 +143,37 @@ impl PickerDelegate for CommandPalette {
)
.await
};
this.update(&mut cx, |this, _| {
this.matches = matches;
if this.matches.is_empty() {
this.selected_ix = 0;
} else {
this.selected_ix = cmp::min(this.selected_ix, this.matches.len() - 1);
}
});
picker
.update(&mut cx, |picker, _| {
let delegate = picker.delegate_mut();
delegate.actions = actions;
delegate.matches = matches;
if delegate.matches.is_empty() {
delegate.selected_ix = 0;
} else {
delegate.selected_ix =
cmp::min(delegate.selected_ix, delegate.matches.len() - 1);
}
})
.log_err();
})
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed);
}
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn confirm(&mut self, cx: &mut ViewContext<Self>) {
fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
if !self.matches.is_empty() {
let window_id = cx.window_id();
let focused_view_id = self.focused_view_id;
let action_ix = self.matches[self.selected_ix].candidate_id;
cx.emit(Event::Confirmed {
window_id: cx.window_id(),
focused_view_id: self.focused_view_id,
action: self.actions.remove(action_ix).action,
});
} else {
cx.emit(Event::Dismissed);
let action = self.actions.remove(action_ix).action;
cx.app_context()
.spawn(move |mut cx| async move {
cx.dispatch_action(window_id, focused_view_id, action.as_ref())
})
.detach_and_log_err(cx);
}
cx.emit(PickerEvent::Dismiss);
}
fn render_match(
@@ -224,7 +182,7 @@ impl PickerDelegate for CommandPalette {
mouse_state: &mut MouseState,
selected: bool,
cx: &gpui::AppContext,
) -> gpui::ElementBox {
) -> AnyElement<Picker<Self>> {
let mat = &self.matches[ix];
let command = &self.actions[mat.candidate_id];
let settings = cx.global::<Settings>();
@@ -236,8 +194,7 @@ impl PickerDelegate for CommandPalette {
Flex::row()
.with_child(
Label::new(mat.string.clone(), style.label.clone())
.with_highlights(mat.positions.clone())
.boxed(),
.with_highlights(mat.positions.clone()),
)
.with_children(command.keystrokes.iter().map(|keystroke| {
Flex::row()
@@ -254,8 +211,7 @@ impl PickerDelegate for CommandPalette {
Some(
Label::new(label, key_style.label.clone())
.contained()
.with_style(key_style.container)
.boxed(),
.with_style(key_style.container),
)
} else {
None
@@ -265,17 +221,15 @@ impl PickerDelegate for CommandPalette {
.with_child(
Label::new(keystroke.key.clone(), key_style.label.clone())
.contained()
.with_style(key_style.container)
.boxed(),
.with_style(key_style.container),
)
.contained()
.with_margin_left(keystroke_spacing)
.flex_float()
.boxed()
}))
.contained()
.with_style(style.container)
.boxed()
.into_any()
}
}
@@ -314,9 +268,11 @@ impl std::fmt::Debug for Command {
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use editor::Editor;
use gpui::TestAppContext;
use gpui::{executor::Deterministic, TestAppContext};
use project::Project;
use workspace::{AppState, Workspace};
@@ -337,7 +293,8 @@ mod tests {
}
#[gpui::test]
async fn test_command_palette(cx: &mut TestAppContext) {
async fn test_command_palette(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
deterministic.forbid_parking();
let app_state = cx.update(AppState::test);
cx.update(|cx| {
@@ -347,8 +304,8 @@ mod tests {
});
let project = Project::test(app_state.fs.clone(), [], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let editor = cx.add_view(&workspace, |cx| {
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let editor = cx.add_view(window_id, |cx| {
let mut editor = Editor::single_line(None, cx);
editor.set_text("abc", cx);
editor
@@ -360,7 +317,7 @@ mod tests {
});
workspace.update(cx, |workspace, cx| {
CommandPalette::toggle(workspace, &Toggle, cx)
toggle_command_palette(workspace, &Toggle, cx);
});
let palette = workspace.read_with(cx, |workspace, _| {
@@ -369,15 +326,17 @@ mod tests {
palette
.update(cx, |palette, cx| {
palette.update_matches("bcksp".to_string(), cx)
palette
.delegate_mut()
.update_matches("bcksp".to_string(), cx)
})
.await;
palette.update(cx, |palette, cx| {
assert_eq!(palette.matches[0].string, "editor: backspace");
palette.confirm(cx);
assert_eq!(palette.delegate().matches[0].string, "editor: backspace");
palette.confirm(&Default::default(), cx);
});
deterministic.run_until_parked();
editor.read_with(cx, |editor, cx| {
assert_eq!(editor.text(cx), "ab");
});
@@ -390,7 +349,7 @@ mod tests {
});
workspace.update(cx, |workspace, cx| {
CommandPalette::toggle(workspace, &Toggle, cx);
toggle_command_palette(workspace, &Toggle, cx);
});
// Assert editor command not present
@@ -400,10 +359,14 @@ mod tests {
palette
.update(cx, |palette, cx| {
palette.update_matches("bcksp".to_string(), cx)
palette
.delegate_mut()
.update_matches("bcksp".to_string(), cx)
})
.await;
palette.update(cx, |palette, _| assert!(palette.matches.is_empty()));
palette.update(cx, |palette, _| {
assert!(palette.delegate().matches.is_empty())
});
}
}

View File

@@ -13,4 +13,4 @@ gpui = { path = "../gpui" }
menu = { path = "../menu" }
settings = { path = "../settings" }
theme = { path = "../theme" }
smallvec = "1.6"
smallvec.workspace = true

View File

@@ -1,55 +1,72 @@
use gpui::{
anyhow,
elements::*,
geometry::vector::Vector2F,
impl_internal_actions,
keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton},
Action, AnyViewHandle, AppContext, Axis, Entity, MouseState, RenderContext, SizeConstraint,
Subscription, View, ViewContext,
Action, AnyViewHandle, AppContext, Axis, Entity, MouseState, SizeConstraint, Subscription,
View, ViewContext,
};
use menu::*;
use settings::Settings;
use std::{any::TypeId, borrow::Cow, time::Duration};
pub type StaticItem = Box<dyn Fn(&mut AppContext) -> ElementBox>;
#[derive(Copy, Clone, PartialEq)]
struct Clicked;
impl_internal_actions!(context_menu, [Clicked]);
use std::{any::TypeId, borrow::Cow, sync::Arc, time::Duration};
pub fn init(cx: &mut AppContext) {
cx.add_action(ContextMenu::select_first);
cx.add_action(ContextMenu::select_last);
cx.add_action(ContextMenu::select_next);
cx.add_action(ContextMenu::select_prev);
cx.add_action(ContextMenu::clicked);
cx.add_action(ContextMenu::confirm);
cx.add_action(ContextMenu::cancel);
}
type ContextMenuItemBuilder = Box<dyn Fn(&mut MouseState, &theme::ContextMenuItem) -> ElementBox>;
pub type StaticItem = Box<dyn Fn(&mut AppContext) -> AnyElement<ContextMenu>>;
type ContextMenuItemBuilder =
Box<dyn Fn(&mut MouseState, &theme::ContextMenuItem) -> AnyElement<ContextMenu>>;
pub enum ContextMenuItemLabel {
String(Cow<'static, str>),
Element(ContextMenuItemBuilder),
}
pub enum ContextMenuAction {
ParentAction {
action: Box<dyn Action>,
},
ViewAction {
action: Box<dyn Action>,
for_view: usize,
},
impl From<Cow<'static, str>> for ContextMenuItemLabel {
fn from(s: Cow<'static, str>) -> Self {
Self::String(s)
}
}
impl ContextMenuAction {
fn id(&self) -> TypeId {
impl From<&'static str> for ContextMenuItemLabel {
fn from(s: &'static str) -> Self {
Self::String(s.into())
}
}
impl From<String> for ContextMenuItemLabel {
fn from(s: String) -> Self {
Self::String(s.into())
}
}
impl<T> From<T> for ContextMenuItemLabel
where
T: 'static + Fn(&mut MouseState, &theme::ContextMenuItem) -> AnyElement<ContextMenu>,
{
fn from(f: T) -> Self {
Self::Element(Box::new(f))
}
}
pub enum ContextMenuItemAction {
Action(Box<dyn Action>),
Handler(Arc<dyn Fn(&mut ViewContext<ContextMenu>)>),
}
impl Clone for ContextMenuItemAction {
fn clone(&self) -> Self {
match self {
ContextMenuAction::ParentAction { action } => action.id(),
ContextMenuAction::ViewAction { action, .. } => action.id(),
Self::Action(action) => Self::Action(action.boxed_clone()),
Self::Handler(handler) => Self::Handler(handler.clone()),
}
}
}
@@ -57,42 +74,27 @@ impl ContextMenuAction {
pub enum ContextMenuItem {
Item {
label: ContextMenuItemLabel,
action: ContextMenuAction,
action: ContextMenuItemAction,
},
Static(StaticItem),
Separator,
}
impl ContextMenuItem {
pub fn element_item(label: ContextMenuItemBuilder, action: impl 'static + Action) -> Self {
pub fn action(label: impl Into<ContextMenuItemLabel>, action: impl 'static + Action) -> Self {
Self::Item {
label: ContextMenuItemLabel::Element(label),
action: ContextMenuAction::ParentAction {
action: Box::new(action),
},
label: label.into(),
action: ContextMenuItemAction::Action(Box::new(action)),
}
}
pub fn item(label: impl Into<Cow<'static, str>>, action: impl 'static + Action) -> Self {
Self::Item {
label: ContextMenuItemLabel::String(label.into()),
action: ContextMenuAction::ParentAction {
action: Box::new(action),
},
}
}
pub fn item_for_view(
label: impl Into<Cow<'static, str>>,
view_id: usize,
action: impl 'static + Action,
pub fn handler(
label: impl Into<ContextMenuItemLabel>,
handler: impl 'static + Fn(&mut ViewContext<ContextMenu>),
) -> Self {
Self::Item {
label: ContextMenuItemLabel::String(label.into()),
action: ContextMenuAction::ViewAction {
action: Box::new(action),
for_view: view_id,
},
label: label.into(),
action: ContextMenuItemAction::Handler(Arc::new(handler)),
}
}
@@ -106,7 +108,10 @@ impl ContextMenuItem {
fn action_id(&self) -> Option<TypeId> {
match self {
ContextMenuItem::Item { action, .. } => Some(action.id()),
ContextMenuItem::Item { action, .. } => match action {
ContextMenuItemAction::Action(action) => Some(action.id()),
ContextMenuItemAction::Handler(_) => None,
},
ContextMenuItem::Static(..) | ContextMenuItem::Separator => None,
}
}
@@ -121,7 +126,6 @@ pub struct ContextMenu {
selected_index: Option<usize>,
visible: bool,
previously_focused_view_id: Option<usize>,
clicked: bool,
parent_view_id: usize,
_actions_observation: Subscription,
}
@@ -135,29 +139,27 @@ impl View for ContextMenu {
"ContextMenu"
}
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.add_identifier("menu");
cx
fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
Self::reset_to_default_keymap_context(keymap);
keymap.add_identifier("menu");
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if !self.visible {
return Empty::new().boxed();
return Empty::new().into_any();
}
// Render the menu once at minimum width.
let mut collapsed_menu = self.render_menu_for_measurement(cx).boxed();
let expanded_menu = self
.render_menu(cx)
.constrained()
.dynamically(move |constraint, cx| {
SizeConstraint::strict_along(
Axis::Horizontal,
collapsed_menu.layout(constraint, cx).x(),
)
})
.boxed();
let mut collapsed_menu = self.render_menu_for_measurement(cx);
let expanded_menu =
self.render_menu(cx)
.constrained()
.dynamically(move |constraint, view, cx| {
SizeConstraint::strict_along(
Axis::Horizontal,
collapsed_menu.layout(constraint, view, cx).0.x(),
)
});
Overlay::new(expanded_menu)
.with_hoverable(true)
@@ -165,7 +167,7 @@ impl View for ContextMenu {
.with_anchor_position(self.anchor_position)
.with_anchor_corner(self.anchor_corner)
.with_position_mode(self.position_mode)
.boxed()
.into_any()
}
fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
@@ -174,9 +176,7 @@ impl View for ContextMenu {
}
impl ContextMenu {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
let parent_view_id = cx.parent().unwrap();
pub fn new(parent_view_id: usize, cx: &mut ViewContext<Self>) -> Self {
Self {
show_count: 0,
anchor_position: Default::default(),
@@ -186,7 +186,6 @@ impl ContextMenu {
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),
}
@@ -202,36 +201,33 @@ impl ContextMenu {
.iter()
.position(|item| item.action_id() == Some(action_id))
{
if self.clicked {
self.cancel(&Default::default(), cx);
} else {
self.selected_index = Some(ix);
cx.notify();
cx.spawn(|this, mut cx| async move {
cx.background().timer(Duration::from_millis(50)).await;
this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx));
})
.detach();
}
self.selected_index = Some(ix);
cx.notify();
cx.spawn(|this, mut cx| async move {
cx.background().timer(Duration::from_millis(50)).await;
this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx))?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
}
fn clicked(&mut self, _: &Clicked, _: &mut ViewContext<Self>) {
self.clicked = true;
}
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.selected_index {
if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
match action {
ContextMenuAction::ParentAction { action } => {
cx.dispatch_any_action(action.boxed_clone())
}
ContextMenuAction::ViewAction { action, for_view } => {
ContextMenuItemAction::Action(action) => {
let window_id = cx.window_id();
cx.dispatch_any_action_at(window_id, *for_view, action.boxed_clone())
let view_id = self.parent_view_id;
let action = action.boxed_clone();
cx.app_context()
.spawn(|mut cx| async move {
cx.dispatch_action(window_id, view_id, action.as_ref())
})
.detach_and_log_err(cx);
}
};
ContextMenuItemAction::Handler(handler) => handler(cx),
}
self.reset(cx);
}
}
@@ -252,7 +248,6 @@ impl ContextMenu {
self.items.clear();
self.visible = false;
self.selected_index.take();
self.clicked = false;
cx.notify();
}
@@ -314,7 +309,7 @@ impl ContextMenu {
self.visible = true;
self.show_count += 1;
if !cx.is_self_focused() {
self.previously_focused_view_id = cx.focused_view_id(cx.window_id());
self.previously_focused_view_id = cx.focused_view_id();
}
cx.focus_self();
} else {
@@ -327,45 +322,42 @@ impl ContextMenu {
self.position_mode = mode;
}
fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element {
let window_id = cx.window_id();
fn render_menu_for_measurement(&self, cx: &mut ViewContext<Self>) -> impl Element<ContextMenu> {
let style = cx.global::<Settings>().theme.context_menu.clone();
Flex::row()
.with_child(
Flex::column()
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item {
ContextMenuItem::Item { label, .. } => {
let style = style.item.style_for(
&mut Default::default(),
Some(ix) == self.selected_index,
);
Flex::column().with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item {
ContextMenuItem::Item { label, .. } => {
let style = style.item.style_for(
&mut Default::default(),
Some(ix) == self.selected_index,
);
match label {
ContextMenuItemLabel::String(label) => {
Label::new(label.to_string(), style.label.clone())
.contained()
.with_style(style.container)
.boxed()
}
ContextMenuItemLabel::Element(element) => {
element(&mut Default::default(), style)
}
match label {
ContextMenuItemLabel::String(label) => {
Label::new(label.to_string(), style.label.clone())
.contained()
.with_style(style.container)
.into_any()
}
ContextMenuItemLabel::Element(element) => {
element(&mut Default::default(), style)
}
}
ContextMenuItem::Static(f) => f(cx),
ContextMenuItem::Separator => Empty::new()
.collapsed()
.contained()
.with_style(style.separator)
.constrained()
.with_height(1.)
.boxed(),
}
}))
.boxed(),
ContextMenuItem::Static(f) => f(cx),
ContextMenuItem::Separator => Empty::new()
.collapsed()
.contained()
.with_style(style.separator)
.constrained()
.with_height(1.)
.into_any(),
}
})),
)
.with_child(
Flex::column()
@@ -376,26 +368,20 @@ impl ContextMenu {
&mut Default::default(),
Some(ix) == self.selected_index,
);
let (action, view_id) = match action {
ContextMenuAction::ParentAction { action } => {
(action.boxed_clone(), self.parent_view_id)
}
ContextMenuAction::ViewAction { action, for_view } => {
(action.boxed_clone(), *for_view)
}
};
KeystrokeLabel::new(
window_id,
view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),
)
.boxed()
match action {
ContextMenuItemAction::Action(action) => KeystrokeLabel::new(
self.parent_view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),
)
.into_any(),
ContextMenuItemAction::Handler(_) => Empty::new().into_any(),
}
}
ContextMenuItem::Static(_) => Empty::new().boxed(),
ContextMenuItem::Static(_) => Empty::new().into_any(),
ContextMenuItem::Separator => Empty::new()
.collapsed()
@@ -403,78 +389,84 @@ impl ContextMenu {
.with_height(1.)
.contained()
.with_style(style.separator)
.boxed(),
.into_any(),
}
}))
.contained()
.with_margin_left(style.keystroke_margin)
.boxed(),
.with_margin_left(style.keystroke_margin),
)
.contained()
.with_style(style.container)
}
fn render_menu(&self, cx: &mut RenderContext<Self>) -> impl Element {
fn render_menu(&self, cx: &mut ViewContext<Self>) -> impl Element<ContextMenu> {
enum Menu {}
enum MenuItem {}
let style = cx.global::<Settings>().theme.context_menu.clone();
let window_id = cx.window_id();
MouseEventHandler::<Menu>::new(0, cx, |_, cx| {
MouseEventHandler::<Menu, ContextMenu>::new(0, cx, |_, cx| {
Flex::column()
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item {
ContextMenuItem::Item { label, action } => {
let (action, view_id) = match action {
ContextMenuAction::ParentAction { action } => {
(action.boxed_clone(), self.parent_view_id)
}
ContextMenuAction::ViewAction { action, for_view } => {
(action.boxed_clone(), *for_view)
}
};
MouseEventHandler::<MenuItem>::new(ix, cx, |state, _| {
let action = action.clone();
let view_id = self.parent_view_id;
MouseEventHandler::<MenuItem, ContextMenu>::new(ix, cx, |state, _| {
let style =
style.item.style_for(state, Some(ix) == self.selected_index);
let keystroke = match &action {
ContextMenuItemAction::Action(action) => Some(
KeystrokeLabel::new(
view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),
)
.flex_float(),
),
ContextMenuItemAction::Handler(_) => None,
};
Flex::row()
.with_child(match label {
ContextMenuItemLabel::String(label) => {
Label::new(label.clone(), style.label.clone())
.contained()
.boxed()
.into_any()
}
ContextMenuItemLabel::Element(element) => {
element(state, style)
}
})
.with_child({
KeystrokeLabel::new(
window_id,
view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),
)
.flex_float()
.boxed()
})
.with_children(keystroke)
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_up(MouseButton::Left, |_, _| {}) // Capture these events
.on_down(MouseButton::Left, |_, _| {}) // Capture these events
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(Clicked);
.on_up(MouseButton::Left, |_, _, _| {}) // Capture these events
.on_down(MouseButton::Left, |_, _, _| {}) // Capture these events
.on_click(MouseButton::Left, move |_, menu, cx| {
menu.cancel(&Default::default(), cx);
let window_id = cx.window_id();
cx.dispatch_any_action_at(window_id, view_id, action.boxed_clone());
match &action {
ContextMenuItemAction::Action(action) => {
let action = action.boxed_clone();
cx.app_context()
.spawn(|mut cx| async move {
cx.dispatch_action(
window_id,
view_id,
action.as_ref(),
)
})
.detach_and_log_err(cx);
}
ContextMenuItemAction::Handler(handler) => handler(cx),
}
})
.on_drag(MouseButton::Left, |_, _| {})
.boxed()
.on_drag(MouseButton::Left, |_, _, _| {})
.into_any()
}
ContextMenuItem::Static(f) => f(cx),
@@ -484,14 +476,17 @@ impl ContextMenu {
.with_height(1.)
.contained()
.with_style(style.separator)
.boxed(),
.into_any(),
}
}))
.contained()
.with_style(style.container)
.boxed()
})
.on_down_out(MouseButton::Left, |_, cx| cx.dispatch_action(Cancel))
.on_down_out(MouseButton::Right, |_, cx| cx.dispatch_action(Cancel))
.on_down_out(MouseButton::Left, |_, this, cx| {
this.cancel(&Default::default(), cx);
})
.on_down_out(MouseButton::Right, |_, this, cx| {
this.cancel(&Default::default(), cx);
})
}
}

View File

@@ -30,18 +30,20 @@ node_runtime = { path = "../node_runtime"}
util = { path = "../util" }
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
async-tar = "0.4.2"
anyhow = "1.0"
log = "0.4"
serde = { workspace = true }
serde_derive = { workspace = true }
smol = "1.2.5"
futures = "0.3"
anyhow.workspace = true
log.workspace = true
serde.workspace = true
serde_derive.workspace = true
smol.workspace = true
futures.workspace = true
[dev-dependencies]
clock = { path = "../clock" }
collections = { path = "../collections", features = ["test-support"] }
fs = { path = "../fs", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

File diff suppressed because it is too large Load Diff

View File

@@ -99,14 +99,11 @@ pub struct GetCompletionsParams {
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetCompletionsDocument {
pub source: String,
pub tab_size: u32,
pub indent_size: u32,
pub insert_spaces: bool,
pub uri: lsp::Url,
pub path: String,
pub relative_path: String,
pub language_id: String,
pub position: lsp::Position,
pub version: usize,
}
@@ -146,8 +143,8 @@ pub enum LogMessage {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LogMessageParams {
pub message: String,
pub level: u8,
pub message: String,
pub metadata_str: String,
pub extra: Vec<String>,
}
@@ -169,3 +166,60 @@ impl lsp::notification::Notification for StatusNotification {
type Params = StatusNotificationParams;
const METHOD: &'static str = "statusNotification";
}
pub enum SetEditorInfo {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SetEditorInfoParams {
pub editor_info: EditorInfo,
pub editor_plugin_info: EditorPluginInfo,
}
impl lsp::request::Request for SetEditorInfo {
type Params = SetEditorInfoParams;
type Result = String;
const METHOD: &'static str = "setEditorInfo";
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EditorInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EditorPluginInfo {
pub name: String,
pub version: String,
}
pub enum NotifyAccepted {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NotifyAcceptedParams {
pub uuid: String,
}
impl lsp::request::Request for NotifyAccepted {
type Params = NotifyAcceptedParams;
type Result = String;
const METHOD: &'static str = "notifyAccepted";
}
pub enum NotifyRejected {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NotifyRejectedParams {
pub uuids: Vec<String>,
}
impl lsp::request::Request for NotifyRejected {
type Params = NotifyRejectedParams;
type Result = String;
const METHOD: &'static str = "notifyRejected";
}

View File

@@ -3,7 +3,8 @@ use gpui::{
elements::*,
geometry::rect::RectF,
platform::{WindowBounds, WindowKind, WindowOptions},
AppContext, ClipboardItem, Element, Entity, View, ViewContext, ViewHandle,
AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
ViewHandle,
};
use settings::Settings;
use theme::ui::modal;
@@ -17,52 +18,56 @@ struct OpenGithub;
const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
pub fn init(cx: &mut AppContext) {
let copilot = Copilot::global(cx).unwrap();
if let Some(copilot) = Copilot::global(cx) {
let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None;
cx.observe(&copilot, move |copilot, cx| {
let status = copilot.read(cx).status();
let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None;
cx.observe(&copilot, move |copilot, cx| {
let status = copilot.read(cx).status();
match &status {
crate::Status::SigningIn { prompt } => {
if let Some(code_verification_handle) = code_verification.as_mut() {
if cx.has_window(code_verification_handle.window_id()) {
code_verification_handle.update(cx, |code_verification_view, cx| {
code_verification_view.set_status(status, cx)
match &status {
crate::Status::SigningIn { prompt } => {
if let Some(code_verification_handle) = code_verification.as_mut() {
let window_id = code_verification_handle.window_id();
let updated = cx.update_window(window_id, |cx| {
code_verification_handle.update(cx, |code_verification, cx| {
code_verification.set_status(status.clone(), cx)
});
cx.activate_window();
});
cx.activate_window(code_verification_handle.window_id());
} else {
create_copilot_auth_window(cx, &status, &mut code_verification);
if updated.is_none() {
code_verification = Some(create_copilot_auth_window(cx, &status));
}
} else if let Some(_prompt) = prompt {
code_verification = Some(create_copilot_auth_window(cx, &status));
}
} else if let Some(_prompt) = prompt {
create_copilot_auth_window(cx, &status, &mut code_verification);
}
}
Status::Authorized | Status::Unauthorized => {
if let Some(code_verification) = code_verification.as_ref() {
code_verification.update(cx, |code_verification, cx| {
code_verification.set_status(status, cx)
});
Status::Authorized | Status::Unauthorized => {
if let Some(code_verification) = code_verification.as_ref() {
let window_id = code_verification.window_id();
cx.update_window(window_id, |cx| {
code_verification.update(cx, |code_verification, cx| {
code_verification.set_status(status, cx)
});
cx.platform().activate(true);
cx.activate_window(code_verification.window_id());
cx.platform().activate(true);
cx.activate_window();
});
}
}
_ => {
if let Some(code_verification) = code_verification.take() {
cx.update_window(code_verification.window_id(), |cx| cx.remove_window());
}
}
}
_ => {
if let Some(code_verification) = code_verification.take() {
cx.remove_window(code_verification.window_id());
}
}
}
})
.detach();
})
.detach();
}
}
fn create_copilot_auth_window(
cx: &mut AppContext,
status: &Status,
code_verification: &mut Option<ViewHandle<CopilotCodeVerification>>,
) {
) -> ViewHandle<CopilotCodeVerification> {
let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
let window_options = WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
@@ -76,16 +81,20 @@ fn create_copilot_auth_window(
let (_, view) = cx.add_window(window_options, |_cx| {
CopilotCodeVerification::new(status.clone())
});
*code_verification = Some(view);
view
}
pub struct CopilotCodeVerification {
status: Status,
connect_clicked: bool,
}
impl CopilotCodeVerification {
pub fn new(status: Status) -> Self {
Self { status }
Self {
status,
connect_clicked: false,
}
}
pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
@@ -96,8 +105,8 @@ impl CopilotCodeVerification {
fn render_device_code(
data: &PromptUserDeviceFlow,
style: &theme::Copilot,
cx: &mut gpui::RenderContext<Self>,
) -> ElementBox {
cx: &mut ViewContext<Self>,
) -> impl Element<Self> {
let copied = cx
.read_from_clipboard()
.map(|item| item.text() == &data.user_code)
@@ -105,16 +114,17 @@ impl CopilotCodeVerification {
let device_code_style = &style.auth.prompting.device_code;
MouseEventHandler::<Self>::new(0, cx, |state, _cx| {
MouseEventHandler::<Self, _>::new(0, cx, |state, _cx| {
Flex::row()
.with_children([
.with_child(
Label::new(data.user_code.clone(), device_code_style.text.clone())
.aligned()
.contained()
.with_style(device_code_style.left_container)
.constrained()
.with_width(device_code_style.left)
.boxed(),
.with_width(device_code_style.left),
)
.with_child(
Label::new(
if copied { "Copied!" } else { "Copy" },
device_code_style.cta.style_for(state, false).text.clone(),
@@ -123,196 +133,188 @@ impl CopilotCodeVerification {
.contained()
.with_style(*device_code_style.right_container.style_for(state, false))
.constrained()
.with_width(device_code_style.right)
.boxed(),
])
.with_width(device_code_style.right),
)
.contained()
.with_style(device_code_style.cta.style_for(state, false).container)
.boxed()
})
.on_click(gpui::platform::MouseButton::Left, {
let user_code = data.user_code.clone();
move |_, cx| {
move |_, _, cx| {
cx.platform()
.write_to_clipboard(ClipboardItem::new(user_code.clone()));
cx.notify();
}
})
.with_cursor_style(gpui::platform::CursorStyle::PointingHand)
.boxed()
}
fn render_prompting_modal(
connect_clicked: bool,
data: &PromptUserDeviceFlow,
style: &theme::Copilot,
cx: &mut gpui::RenderContext<Self>,
) -> ElementBox {
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
enum ConnectButton {}
Flex::column()
.with_children([
.with_child(
Flex::column()
.with_children([
Label::new(
"Enable Copilot by connecting",
style.auth.prompting.subheading.text.clone(),
)
.aligned()
.boxed(),
.aligned(),
Label::new(
"your existing license.",
style.auth.prompting.subheading.text.clone(),
)
.aligned()
.boxed(),
.aligned(),
])
.align_children_center()
.contained()
.with_style(style.auth.prompting.subheading.container)
.boxed(),
Self::render_device_code(data, &style, cx),
.with_style(style.auth.prompting.subheading.container),
)
.with_child(Self::render_device_code(data, &style, cx))
.with_child(
Flex::column()
.with_children([
Label::new(
"Paste this code into GitHub after",
style.auth.prompting.hint.text.clone(),
)
.aligned()
.boxed(),
.aligned(),
Label::new(
"clicking the button below.",
style.auth.prompting.hint.text.clone(),
)
.aligned()
.boxed(),
.aligned(),
])
.align_children_center()
.contained()
.with_style(style.auth.prompting.hint.container.clone())
.boxed(),
theme::ui::cta_button_with_click(
"Connect to GitHub",
style.auth.content_width,
&style.auth.cta_button,
cx,
{
let verification_uri = data.verification_uri.clone();
move |_, cx| cx.platform().open_url(&verification_uri)
},
)
.boxed(),
])
.with_style(style.auth.prompting.hint.container.clone()),
)
.with_child(theme::ui::cta_button::<ConnectButton, _, _, _>(
if connect_clicked {
"Waiting for connection..."
} else {
"Connect to GitHub"
},
style.auth.content_width,
&style.auth.cta_button,
cx,
{
let verification_uri = data.verification_uri.clone();
move |_, verification, cx| {
cx.platform().open_url(&verification_uri);
verification.connect_clicked = true;
}
},
))
.align_children_center()
.boxed()
.into_any()
}
fn render_enabled_modal(
style: &theme::Copilot,
cx: &mut gpui::RenderContext<Self>,
) -> ElementBox {
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
enum DoneButton {}
let enabled_style = &style.auth.authorized;
Flex::column()
.with_children([
.with_child(
Label::new("Copilot Enabled!", enabled_style.subheading.text.clone())
.contained()
.with_style(enabled_style.subheading.container)
.aligned()
.boxed(),
.aligned(),
)
.with_child(
Flex::column()
.with_children([
Label::new(
"You can update your settings or",
enabled_style.hint.text.clone(),
)
.aligned()
.boxed(),
.aligned(),
Label::new(
"sign out from the Copilot menu in",
enabled_style.hint.text.clone(),
)
.aligned()
.boxed(),
Label::new("the status bar.", enabled_style.hint.text.clone())
.aligned()
.boxed(),
.aligned(),
Label::new("the status bar.", enabled_style.hint.text.clone()).aligned(),
])
.align_children_center()
.contained()
.with_style(enabled_style.hint.container)
.boxed(),
theme::ui::cta_button_with_click(
"Done",
style.auth.content_width,
&style.auth.cta_button,
cx,
|_, cx| {
let window_id = cx.window_id();
cx.remove_window(window_id)
},
)
.boxed(),
])
.with_style(enabled_style.hint.container),
)
.with_child(theme::ui::cta_button::<DoneButton, _, _, _>(
"Done",
style.auth.content_width,
&style.auth.cta_button,
cx,
|_, _, cx| cx.remove_window(),
))
.align_children_center()
.boxed()
.into_any()
}
fn render_unauthorized_modal(
style: &theme::Copilot,
cx: &mut gpui::RenderContext<Self>,
) -> ElementBox {
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let unauthorized_style = &style.auth.not_authorized;
Flex::column()
.with_children([
.with_child(
Flex::column()
.with_children([
Label::new(
"Enable Copilot by connecting",
unauthorized_style.subheading.text.clone(),
)
.aligned()
.boxed(),
.aligned(),
Label::new(
"your existing license.",
unauthorized_style.subheading.text.clone(),
)
.aligned()
.boxed(),
.aligned(),
])
.align_children_center()
.contained()
.with_style(unauthorized_style.subheading.container)
.boxed(),
.with_style(unauthorized_style.subheading.container),
)
.with_child(
Flex::column()
.with_children([
Label::new(
"You must have an active copilot",
unauthorized_style.warning.text.clone(),
)
.aligned()
.boxed(),
.aligned(),
Label::new(
"license to use it in Zed.",
unauthorized_style.warning.text.clone(),
)
.aligned()
.boxed(),
.aligned(),
])
.align_children_center()
.contained()
.with_style(unauthorized_style.warning.container)
.boxed(),
theme::ui::cta_button_with_click(
"Subscribe on GitHub",
style.auth.content_width,
&style.auth.cta_button,
cx,
|_, cx| {
let window_id = cx.window_id();
cx.remove_window(window_id);
cx.platform().open_url(COPILOT_SIGN_UP_URL)
},
)
.boxed(),
])
.with_style(unauthorized_style.warning.container),
)
.with_child(theme::ui::cta_button::<Self, _, _, _>(
"Subscribe on GitHub",
style.auth.content_width,
&style.auth.cta_button,
cx,
|_, _, cx| {
cx.remove_window();
cx.platform().open_url(COPILOT_SIGN_UP_URL)
},
))
.align_children_center()
.boxed()
.into_any()
}
}
@@ -325,32 +327,50 @@ impl View for CopilotCodeVerification {
"CopilotCodeVerification"
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext<Self>) {
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
cx.notify()
}
fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext<Self>) {
fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
cx.notify()
}
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
enum ConnectModal {}
let style = cx.global::<Settings>().theme.clone();
modal("Connect Copilot to Zed", &style.copilot.modal, cx, |cx| {
Flex::column()
.with_children([
theme::ui::icon(&style.copilot.auth.header).boxed(),
match &self.status {
Status::SigningIn {
prompt: Some(prompt),
} => Self::render_prompting_modal(&prompt, &style.copilot, cx),
Status::Unauthorized => Self::render_unauthorized_modal(&style.copilot, cx),
Status::Authorized => Self::render_enabled_modal(&style.copilot, cx),
_ => Empty::new().boxed(),
},
])
.align_children_center()
.boxed()
})
modal::<ConnectModal, _, _, _, _>(
"Connect Copilot to Zed",
&style.copilot.modal,
cx,
|cx| {
Flex::column()
.with_children([
theme::ui::icon(&style.copilot.auth.header).into_any(),
match &self.status {
Status::SigningIn {
prompt: Some(prompt),
} => Self::render_prompting_modal(
self.connect_clicked,
&prompt,
&style.copilot,
cx,
),
Status::Unauthorized => {
self.connect_clicked = false;
Self::render_unauthorized_modal(&style.copilot, cx)
}
Status::Authorized => {
self.connect_clicked = false;
Self::render_enabled_modal(&style.copilot, cx)
}
_ => Empty::new().into_any(),
},
])
.align_children_center()
},
)
.into_any()
}
}

View File

@@ -9,6 +9,7 @@ path = "src/copilot_button.rs"
doctest = false
[dependencies]
assets = { path = "../assets" }
copilot = { path = "../copilot" }
editor = { path = "../editor" }
context_menu = { path = "../context_menu" }
@@ -17,6 +18,6 @@ settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow = "1.0"
smol = "1.2.5"
futures = "0.3"
anyhow.workspace = true
smol.workspace = true
futures.workspace = true

View File

@@ -1,85 +1,31 @@
use std::sync::Arc;
use anyhow::Result;
use context_menu::{ContextMenu, ContextMenuItem};
use editor::Editor;
use copilot::{Copilot, SignOut, Status};
use editor::{scroll::autoscroll::Autoscroll, Editor};
use gpui::{
elements::*,
impl_internal_actions,
platform::{CursorStyle, MouseButton},
AppContext, Element, ElementBox, Entity, MouseState, RenderContext, Subscription, View,
ViewContext, ViewHandle,
AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View,
ViewContext, ViewHandle, WeakViewHandle, WindowContext,
};
use settings::{settings_file::SettingsFile, Settings};
use std::{path::Path, sync::Arc};
use util::{paths, ResultExt};
use workspace::{
item::ItemHandle, notifications::simple_message_notification::OsOpen, DismissToast,
StatusItemView,
create_and_open_local_file, item::ItemHandle,
notifications::simple_message_notification::OsOpen, StatusItemView, Toast, Workspace,
};
use copilot::{Copilot, Reinstall, SignIn, SignOut, Status};
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
const COPILOT_STARTING_TOAST_ID: usize = 1337;
const COPILOT_ERROR_TOAST_ID: usize = 1338;
#[derive(Clone, PartialEq)]
pub struct DeployCopilotMenu;
#[derive(Clone, PartialEq)]
pub struct ToggleCopilotForLanguage {
language: Arc<str>,
}
#[derive(Clone, PartialEq)]
pub struct ToggleCopilotGlobally;
// TODO: Make the other code path use `get_or_insert` logic for this modal
#[derive(Clone, PartialEq)]
pub struct DeployCopilotModal;
impl_internal_actions!(
copilot,
[
DeployCopilotMenu,
DeployCopilotModal,
ToggleCopilotForLanguage,
ToggleCopilotGlobally,
]
);
pub fn init(cx: &mut AppContext) {
cx.add_action(CopilotButton::deploy_copilot_menu);
cx.add_action(
|_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| {
let language = action.language.to_owned();
let current_langauge = cx.global::<Settings>().copilot_on(Some(&language));
SettingsFile::update(cx, move |file_contents| {
file_contents.languages.insert(
language.to_owned(),
settings::EditorSettings {
copilot: Some((!current_langauge).into()),
..Default::default()
},
);
})
},
);
cx.add_action(|_: &mut CopilotButton, _: &ToggleCopilotGlobally, cx| {
let copilot_on = cx.global::<Settings>().copilot_on(None);
SettingsFile::update(cx, move |file_contents| {
file_contents.editor.copilot = Some((!copilot_on).into())
})
});
}
pub struct CopilotButton {
popup_menu: ViewHandle<ContextMenu>,
editor_subscription: Option<(Subscription, usize)>,
editor_enabled: Option<bool>,
language: Option<Arc<str>>,
path: Option<Arc<Path>>,
}
impl Entity for CopilotButton {
@@ -91,27 +37,27 @@ impl View for CopilotButton {
"CopilotButton"
}
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let settings = cx.global::<Settings>();
if !settings.enable_copilot_integration {
return Empty::new().boxed();
if !settings.features.copilot {
return Empty::new().into_any();
}
let theme = settings.theme.clone();
let active = self.popup_menu.read(cx).visible();
let Some(copilot) = Copilot::global(cx) else {
return Empty::new().boxed();
return Empty::new().into_any();
};
let status = copilot.read(cx).status();
let enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
let view_id = cx.view_id();
let enabled = self
.editor_enabled
.unwrap_or(settings.show_copilot_suggestions(None, None));
Stack::new()
.with_child(
MouseEventHandler::<Self>::new(0, cx, {
MouseEventHandler::<Self, _>::new(0, cx, {
let theme = theme.clone();
let status = status.clone();
move |state, _cx| {
@@ -141,88 +87,66 @@ impl View for CopilotButton {
.constrained()
.with_width(style.icon_size)
.aligned()
.named("copilot-icon"),
.into_any_named("copilot-icon"),
)
.constrained()
.with_height(style.icon_size)
.contained()
.with_style(style.container)
.boxed()
}
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, {
let status = status.clone();
move |_, cx| match status {
Status::Authorized => cx.dispatch_action(DeployCopilotMenu),
Status::Starting { ref task } => {
cx.dispatch_action(workspace::Toast::new(
COPILOT_STARTING_TOAST_ID,
"Copilot is starting...",
));
let window_id = cx.window_id();
let task = task.to_owned();
cx.spawn(|mut cx| async move {
task.await;
cx.update(|cx| {
if let Some(copilot) = Copilot::global(cx) {
let status = copilot.read(cx).status();
match status {
Status::Authorized => cx.dispatch_action_at(
window_id,
view_id,
workspace::Toast::new(
COPILOT_STARTING_TOAST_ID,
"Copilot has started!",
),
),
_ => {
cx.dispatch_action_at(
window_id,
view_id,
DismissToast::new(COPILOT_STARTING_TOAST_ID),
);
cx.dispatch_global_action(SignIn)
}
}
}
})
})
.detach();
move |_, this, cx| match status {
Status::Authorized => this.deploy_copilot_menu(cx),
Status::Error(ref e) => {
if let Some(workspace) = cx.root_view().clone().downcast::<Workspace>()
{
workspace.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(
COPILOT_ERROR_TOAST_ID,
format!("Copilot can't be started: {}", e),
)
.on_click(
"Reinstall Copilot",
|cx| {
if let Some(copilot) = Copilot::global(cx) {
copilot
.update(cx, |copilot, cx| {
copilot.reinstall(cx)
})
.detach();
}
},
),
cx,
);
});
}
}
Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action(
COPILOT_ERROR_TOAST_ID,
format!("Copilot can't be started: {}", e),
"Reinstall Copilot",
Reinstall,
)),
_ => cx.dispatch_action(SignIn),
_ => this.deploy_copilot_start_menu(cx),
}
})
.with_tooltip::<Self, _>(
.with_tooltip::<Self>(
0,
"GitHub Copilot".into(),
None,
theme.tooltip.clone(),
cx,
)
.boxed(),
),
)
.with_child(
ChildView::new(&self.popup_menu, cx)
.aligned()
.top()
.right()
.boxed(),
)
.boxed()
.with_child(ChildView::new(&self.popup_menu, cx).aligned().top().right())
.into_any()
}
}
impl CopilotButton {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
let button_view_id = cx.view_id();
let menu = cx.add_view(|cx| {
let mut menu = ContextMenu::new(cx);
let mut menu = ContextMenu::new(button_view_id, cx);
menu.set_position_mode(OverlayPositionMode::Local);
menu
});
@@ -239,62 +163,96 @@ impl CopilotButton {
editor_subscription: None,
editor_enabled: None,
language: None,
path: None,
}
}
pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext<Self>) {
pub fn deploy_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) {
let mut menu_options = Vec::with_capacity(2);
menu_options.push(ContextMenuItem::handler("Sign In", |cx| {
initiate_sign_in(cx)
}));
menu_options.push(ContextMenuItem::handler("Disable Copilot", |cx| {
hide_copilot(cx)
}));
self.popup_menu.update(cx, |menu, cx| {
menu.show(
Default::default(),
AnchorCorner::BottomRight,
menu_options,
cx,
);
});
}
pub fn deploy_copilot_menu(&mut self, cx: &mut ViewContext<Self>) {
let settings = cx.global::<Settings>();
let mut menu_options = Vec::with_capacity(6);
let mut menu_options = Vec::with_capacity(8);
if let Some(language) = &self.language {
let language_enabled = settings.copilot_on(Some(language.as_ref()));
menu_options.push(ContextMenuItem::item(
if let Some(language) = self.language.clone() {
let language_enabled = settings.copilot_enabled_for_language(Some(language.as_ref()));
menu_options.push(ContextMenuItem::handler(
format!(
"{} Copilot for {}",
if language_enabled {
"Disable"
} else {
"Enable"
},
"{} Suggestions for {}",
if language_enabled { "Hide" } else { "Show" },
language
),
ToggleCopilotForLanguage {
language: language.to_owned(),
move |cx| toggle_copilot_for_language(language.clone(), cx),
));
}
if let Some(path) = self.path.as_ref() {
let path_enabled = settings.copilot_enabled_for_path(path);
let path = path.clone();
menu_options.push(ContextMenuItem::handler(
format!(
"{} Suggestions for This Path",
if path_enabled { "Hide" } else { "Show" }
),
move |cx| {
if let Some(workspace) = cx.root_view().clone().downcast::<Workspace>() {
let workspace = workspace.downgrade();
cx.spawn(|_, cx| {
configure_disabled_globs(
workspace,
path_enabled.then_some(path.clone()),
cx,
)
})
.detach_and_log_err(cx);
}
},
));
}
let globally_enabled = cx.global::<Settings>().copilot_on(None);
menu_options.push(ContextMenuItem::item(
let globally_enabled = cx.global::<Settings>().features.copilot;
menu_options.push(ContextMenuItem::handler(
if globally_enabled {
"Disable Copilot Globally"
"Hide Suggestions for All Files"
} else {
"Enable Copilot Globally"
"Show Suggestions for All Files"
},
ToggleCopilotGlobally,
|cx| toggle_copilot_globally(cx),
));
menu_options.push(ContextMenuItem::Separator);
let icon_style = settings.theme.copilot.out_link_icon.clone();
menu_options.push(ContextMenuItem::element_item(
Box::new(
move |state: &mut MouseState, style: &theme::ContextMenuItem| {
Flex::row()
.with_children([
Label::new("Copilot Settings", style.label.clone()).boxed(),
theme::ui::icon(icon_style.style_for(state, false)).boxed(),
])
.align_children_center()
.boxed()
},
),
menu_options.push(ContextMenuItem::action(
move |state: &mut MouseState, style: &theme::ContextMenuItem| {
Flex::row()
.with_child(Label::new("Copilot Settings", style.label.clone()))
.with_child(theme::ui::icon(icon_style.style_for(state, false)))
.align_children_center()
.into_any()
},
OsOpen::new(COPILOT_SETTINGS_URL),
));
menu_options.push(ContextMenuItem::item("Sign Out", SignOut));
menu_options.push(ContextMenuItem::action("Sign Out", SignOut));
self.popup_menu.update(cx, |menu, cx| {
menu.show(
@@ -316,10 +274,14 @@ impl CopilotButton {
let language_name = snapshot
.language_at(suggestion_anchor)
.map(|language| language.name());
let path = snapshot
.file_at(suggestion_anchor)
.map(|file| file.path().clone());
self.language = language_name.clone();
self.editor_enabled = Some(settings.copilot_on(language_name.as_deref()));
self.editor_enabled =
Some(settings.show_copilot_suggestions(language_name.as_deref(), path.as_deref()));
self.language = language_name;
self.path = path;
cx.notify()
}
@@ -339,3 +301,134 @@ impl StatusItemView for CopilotButton {
cx.notify();
}
}
async fn configure_disabled_globs(
workspace: WeakViewHandle<Workspace>,
path_to_disable: Option<Arc<Path>>,
mut cx: AsyncAppContext,
) -> Result<()> {
let settings_editor = workspace
.update(&mut cx, |_, cx| {
create_and_open_local_file(&paths::SETTINGS, cx, || {
Settings::initial_user_settings_content(&assets::Assets)
.as_ref()
.into()
})
})?
.await?
.downcast::<Editor>()
.unwrap();
settings_editor.downgrade().update(&mut cx, |item, cx| {
let text = item.buffer().read(cx).snapshot(cx).text();
let edits = SettingsFile::update_unsaved(&text, cx, |file| {
let copilot = file.copilot.get_or_insert_with(Default::default);
let globs = copilot.disabled_globs.get_or_insert_with(|| {
cx.global::<Settings>()
.copilot
.disabled_globs
.clone()
.iter()
.map(|glob| glob.as_str().to_string())
.collect::<Vec<_>>()
});
if let Some(path_to_disable) = &path_to_disable {
globs.push(path_to_disable.to_string_lossy().into_owned());
} else {
globs.clear();
}
});
if !edits.is_empty() {
item.change_selections(Some(Autoscroll::newest()), cx, |selections| {
selections.select_ranges(edits.iter().map(|e| e.0.clone()));
});
// When *enabling* a path, don't actually perform an edit, just select the range.
if path_to_disable.is_some() {
item.edit(edits.iter().cloned(), cx);
}
}
})?;
anyhow::Ok(())
}
fn toggle_copilot_globally(cx: &mut AppContext) {
let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None, None);
SettingsFile::update(cx, move |file_contents| {
file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
});
}
fn toggle_copilot_for_language(language: Arc<str>, cx: &mut AppContext) {
let show_copilot_suggestions = cx
.global::<Settings>()
.show_copilot_suggestions(Some(&language), None);
SettingsFile::update(cx, move |file_contents| {
file_contents.languages.insert(
language,
settings::EditorSettings {
show_copilot_suggestions: Some((!show_copilot_suggestions).into()),
..Default::default()
},
);
});
}
fn hide_copilot(cx: &mut AppContext) {
SettingsFile::update(cx, move |file_contents| {
file_contents.features.copilot = Some(false)
});
}
fn initiate_sign_in(cx: &mut WindowContext) {
let Some(copilot) = Copilot::global(cx) else {
return;
};
let status = copilot.read(cx).status();
match status {
Status::Starting { task } => {
let Some(workspace) = cx.root_view().clone().downcast::<Workspace>() else {
return;
};
workspace.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."),
cx,
)
});
let workspace = workspace.downgrade();
cx.spawn(|mut cx| async move {
task.await;
if let Some(copilot) = cx.read(Copilot::global) {
workspace
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
Status::Authorized => workspace.show_toast(
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot has started!"),
cx,
),
_ => {
workspace.dismiss_toast(COPILOT_STARTING_TOAST_ID, cx);
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach_and_log_err(cx);
}
})
.log_err();
}
})
.detach();
}
_ => {
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach_and_log_err(cx);
}
}
}

View File

@@ -17,17 +17,17 @@ gpui = { path = "../gpui" }
sqlez = { path = "../sqlez" }
sqlez_macros = { path = "../sqlez_macros" }
util = { path = "../util" }
anyhow = "1.0.57"
anyhow.workspace = true
indoc = "1.0.4"
async-trait = "0.1"
lazy_static = "1.4.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
serde = { workspace = true }
serde_derive = { workspace = true }
smol = "1.2"
async-trait.workspace = true
lazy_static.workspace = true
log.workspace = true
parking_lot.workspace = true
serde.workspace = true
serde_derive.workspace = true
smol.workspace = true
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
env_logger = "0.9.1"
tempdir = { version = "0.3.7" }
env_logger.workspace = true
tempdir.workspace = true

View File

@@ -9,24 +9,28 @@ path = "src/diagnostics.rs"
doctest = false
[dependencies]
anyhow = "1.0"
smallvec = { version = "1.6", features = ["union"] }
collections = { path = "../collections" }
editor = { path = "../editor" }
language = { path = "../language" }
gpui = { path = "../gpui" }
language = { path = "../language" }
lsp = { path = "../lsp" }
project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
postage = { workspace = true }
anyhow.workspace = true
smallvec.workspace = true
postage.workspace = true
[dev-dependencies]
unindent = "0.1"
client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
serde_json = { workspace = true }
serde_json.workspace = true
unindent.workspace = true

View File

@@ -1,7 +1,7 @@
pub mod items;
use anyhow::Result;
use collections::{BTreeMap, HashSet};
use collections::{BTreeSet, HashSet};
use editor::{
diagnostic_block_renderer,
display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
@@ -10,20 +10,21 @@ use editor::{
Editor, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
};
use gpui::{
actions, elements::*, fonts::TextStyle, impl_internal_actions, serde_json, AnyViewHandle,
AppContext, Entity, ModelHandle, RenderContext, Task, View, ViewContext, ViewHandle,
WeakViewHandle,
actions, elements::*, fonts::TextStyle, serde_json, AnyViewHandle, AppContext, Entity,
ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use language::{
Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
SelectionGoal,
};
use lsp::LanguageServerId;
use project::{DiagnosticSummary, Project, ProjectPath};
use serde_json::json;
use settings::Settings;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
borrow::Cow,
cmp::Ordering,
ops::Range,
path::PathBuf,
@@ -31,14 +32,12 @@ use std::{
};
use util::TryFutureExt;
use workspace::{
item::{Item, ItemEvent, ItemHandle},
ItemNavHistory, Pane, Workspace,
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
};
actions!(diagnostics, [Deploy]);
impl_internal_actions!(diagnostics, [Jump]);
const CONTEXT_LINE_COUNT: u32 = 1;
pub fn init(cx: &mut AppContext) {
@@ -55,7 +54,7 @@ struct ProjectDiagnosticsEditor {
summary: DiagnosticSummary,
excerpts: ModelHandle<MultiBuffer>,
path_states: Vec<PathState>,
paths_to_update: BTreeMap<ProjectPath, usize>,
paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,
}
struct PathState {
@@ -71,6 +70,7 @@ struct Jump {
}
struct DiagnosticGroupState {
language_server_id: LanguageServerId,
primary_diagnostic: DiagnosticEntry<language::Anchor>,
primary_excerpt_ix: usize,
excerpts: Vec<ExcerptId>,
@@ -87,16 +87,16 @@ impl View for ProjectDiagnosticsEditor {
"ProjectDiagnosticsEditor"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if self.path_states.is_empty() {
let theme = &cx.global::<Settings>().theme.project_diagnostics;
Label::new("No problems in workspace", theme.empty_message.clone())
.aligned()
.contained()
.with_style(theme.container)
.boxed()
.into_any()
} else {
ChildView::new(&self.editor, cx).boxed()
ChildView::new(&self.editor, cx).into_any()
}
}
@@ -115,7 +115,7 @@ impl View for ProjectDiagnosticsEditor {
}),
"summary": self.summary,
"paths_to_update": self.paths_to_update.iter().map(|(path, server_id)|
(path.path.to_string_lossy(), server_id)
(path.path.to_string_lossy(), server_id.0)
).collect::<Vec<_>>(),
"paths_states": self.path_states.iter().map(|state|
json!({
@@ -148,7 +148,7 @@ impl ProjectDiagnosticsEditor {
path,
} => {
this.paths_to_update
.insert(path.clone(), *language_server_id);
.insert((path.clone(), *language_server_id));
}
_ => {}
})
@@ -167,7 +167,7 @@ impl ProjectDiagnosticsEditor {
let project = project_handle.read(cx);
let paths_to_update = project
.diagnostic_summaries(cx)
.map(|e| (e.0, e.1.language_server_id))
.map(|(path, server_id, _)| (path, server_id))
.collect();
let summary = project.diagnostic_summary(cx);
let mut this = Self {
@@ -195,9 +195,13 @@ impl ProjectDiagnosticsEditor {
}
}
fn update_excerpts(&mut self, language_server_id: Option<usize>, cx: &mut ViewContext<Self>) {
fn update_excerpts(
&mut self,
language_server_id: Option<LanguageServerId>,
cx: &mut ViewContext<Self>,
) {
let mut paths = Vec::new();
self.paths_to_update.retain(|path, server_id| {
self.paths_to_update.retain(|(path, server_id)| {
if language_server_id
.map_or(true, |language_server_id| language_server_id == *server_id)
{
@@ -214,7 +218,9 @@ impl ProjectDiagnosticsEditor {
let buffer = project
.update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
.await?;
this.update(&mut cx, |this, cx| this.populate_excerpts(path, buffer, cx))
this.update(&mut cx, |this, cx| {
this.populate_excerpts(path, language_server_id, buffer, cx)
})?;
}
Result::<_, anyhow::Error>::Ok(())
}
@@ -226,6 +232,7 @@ impl ProjectDiagnosticsEditor {
fn populate_excerpts(
&mut self,
path: ProjectPath,
language_server_id: Option<LanguageServerId>,
buffer: ModelHandle<Buffer>,
cx: &mut ViewContext<Self>,
) {
@@ -264,9 +271,9 @@ impl ProjectDiagnosticsEditor {
let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
let mut new_groups = snapshot
.diagnostic_groups()
.diagnostic_groups(language_server_id)
.into_iter()
.filter(|group| {
.filter(|(_, group)| {
group.entries[group.primary_ix].diagnostic.severity
<= DiagnosticSeverity::WARNING
})
@@ -278,12 +285,27 @@ impl ProjectDiagnosticsEditor {
match (old_groups.peek(), new_groups.peek()) {
(None, None) => break,
(None, Some(_)) => to_insert = new_groups.next(),
(Some(_), None) => to_remove = old_groups.next(),
(Some((_, old_group)), Some(new_group)) => {
(Some((_, old_group)), None) => {
if language_server_id.map_or(true, |id| id == old_group.language_server_id)
{
to_remove = old_groups.next();
} else {
to_keep = old_groups.next();
}
}
(Some((_, old_group)), Some((_, new_group))) => {
let old_primary = &old_group.primary_diagnostic;
let new_primary = &new_group.entries[new_group.primary_ix];
match compare_diagnostics(old_primary, new_primary, &snapshot) {
Ordering::Less => to_remove = old_groups.next(),
Ordering::Less => {
if language_server_id
.map_or(true, |id| id == old_group.language_server_id)
{
to_remove = old_groups.next();
} else {
to_keep = old_groups.next();
}
}
Ordering::Equal => {
to_keep = old_groups.next();
new_groups.next();
@@ -293,8 +315,9 @@ impl ProjectDiagnosticsEditor {
}
}
if let Some(group) = to_insert {
if let Some((language_server_id, group)) = to_insert {
let mut group_state = DiagnosticGroupState {
language_server_id,
primary_diagnostic: group.entries[group.primary_ix].clone(),
primary_excerpt_ix: 0,
excerpts: Default::default(),
@@ -505,12 +528,12 @@ impl ProjectDiagnosticsEditor {
}
impl Item for ProjectDiagnosticsEditor {
fn tab_content(
fn tab_content<T: View>(
&self,
_detail: Option<usize>,
style: &theme::Tab,
cx: &AppContext,
) -> ElementBox {
) -> AnyElement<T> {
render_summary(
&self.summary,
&style.label.text,
@@ -526,11 +549,20 @@ impl Item for ProjectDiagnosticsEditor {
false
}
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
self.editor
.update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
}
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
self.editor
.update(cx, |editor, cx| editor.navigate(data, cx))
}
fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
Some("Project Diagnostics".into())
}
fn is_dirty(&self, cx: &AppContext) -> bool {
self.excerpts.read(cx).is_dirty(cx)
}
@@ -625,6 +657,14 @@ impl Item for ProjectDiagnosticsEditor {
Some("diagnostics")
}
fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
self.editor.breadcrumbs(theme, cx)
}
fn breadcrumb_location(&self) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft { flex: None }
}
fn deserialize(
project: ModelHandle<Project>,
workspace: WeakViewHandle<Workspace>,
@@ -637,7 +677,7 @@ impl Item for ProjectDiagnosticsEditor {
}
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
Arc::new(move |cx| {
let settings = cx.global::<Settings>();
let theme = &settings.theme.editor;
@@ -658,8 +698,17 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
.with_width(icon_width)
.aligned()
.contained()
.boxed(),
.with_margin_right(cx.gutter_padding),
)
.with_children(diagnostic.source.as_ref().map(|source| {
Label::new(
format!("{source}: "),
style.source.label.clone().with_font_size(font_size),
)
.contained()
.with_style(style.message.container)
.aligned()
}))
.with_child(
Label::new(
message.clone(),
@@ -668,47 +717,45 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
.with_highlights(highlights.clone())
.contained()
.with_style(style.message.container)
.with_margin_left(cx.gutter_padding)
.aligned()
.boxed(),
.aligned(),
)
.with_children(diagnostic.code.clone().map(|code| {
Label::new(code, style.code.text.clone().with_font_size(font_size))
.contained()
.with_style(style.code.container)
.aligned()
.boxed()
}))
.contained()
.with_style(style.container)
.with_padding_left(cx.gutter_padding)
.with_padding_right(cx.gutter_padding)
.expanded()
.named("diagnostic header")
.into_any_named("diagnostic header")
})
}
pub(crate) fn render_summary(
pub(crate) fn render_summary<T: View>(
summary: &DiagnosticSummary,
text_style: &TextStyle,
theme: &theme::ProjectDiagnostics,
) -> ElementBox {
) -> AnyElement<T> {
if summary.error_count == 0 && summary.warning_count == 0 {
Label::new("No problems", text_style.clone()).boxed()
Label::new("No problems", text_style.clone()).into_any()
} else {
let icon_width = theme.tab_icon_width;
let icon_spacing = theme.tab_icon_spacing;
let summary_spacing = theme.tab_summary_spacing;
Flex::row()
.with_children([
.with_child(
Svg::new("icons/circle_x_mark_12.svg")
.with_color(text_style.color)
.constrained()
.with_width(icon_width)
.aligned()
.contained()
.with_margin_right(icon_spacing)
.named("no-icon"),
.with_margin_right(icon_spacing),
)
.with_child(
Label::new(
summary.error_count.to_string(),
LabelStyle {
@@ -716,8 +763,9 @@ pub(crate) fn render_summary(
highlight_text: None,
},
)
.aligned()
.boxed(),
.aligned(),
)
.with_child(
Svg::new("icons/triangle_exclamation_12.svg")
.with_color(text_style.color)
.constrained()
@@ -725,8 +773,9 @@ pub(crate) fn render_summary(
.aligned()
.contained()
.with_margin_left(summary_spacing)
.with_margin_right(icon_spacing)
.named("warn-icon"),
.with_margin_right(icon_spacing),
)
.with_child(
Label::new(
summary.warning_count.to_string(),
LabelStyle {
@@ -734,10 +783,9 @@ pub(crate) fn render_summary(
highlight_text: None,
},
)
.aligned()
.boxed(),
])
.boxed()
.aligned(),
)
.into_any()
}
}
@@ -766,28 +814,26 @@ mod tests {
display_map::{BlockContext, TransformBlock},
DisplayPoint,
};
use gpui::TestAppContext;
use gpui::{TestAppContext, WindowContext};
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
use project::FakeFs;
use serde_json::json;
use unindent::Unindent as _;
use workspace::AppState;
#[gpui::test]
async fn test_diagnostics(cx: &mut TestAppContext) {
let app_state = cx.update(AppState::test);
app_state
.fs
.as_fake()
.insert_tree(
"/test",
json!({
"consts.rs": "
Settings::test_async(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/test",
json!({
"consts.rs": "
const a: i32 = 'a';
const b: i32 = c;
"
.unindent(),
.unindent(),
"main.rs": "
"main.rs": "
fn main() {
let x = vec![];
let y = vec![];
@@ -799,19 +845,20 @@ mod tests {
d(x);
}
"
.unindent(),
}),
)
.await;
.unindent(),
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let language_server_id = LanguageServerId(0);
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
// Create some diagnostics
project.update(cx, |project, cx| {
project
.update_diagnostic_entries(
0,
language_server_id,
PathBuf::from("/test/main.rs"),
None,
vec![
@@ -892,7 +939,7 @@ mod tests {
});
// Open the project diagnostics view while there are already diagnostics.
let view = cx.add_view(&workspace, |cx| {
let view = cx.add_view(window_id, |cx| {
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
});
@@ -960,10 +1007,10 @@ mod tests {
// Diagnostics are added for another earlier path.
project.update(cx, |project, cx| {
project.disk_based_diagnostics_started(0, cx);
project.disk_based_diagnostics_started(language_server_id, cx);
project
.update_diagnostic_entries(
0,
language_server_id,
PathBuf::from("/test/consts.rs"),
None,
vec![DiagnosticEntry {
@@ -980,7 +1027,7 @@ mod tests {
cx,
)
.unwrap();
project.disk_based_diagnostics_finished(0, cx);
project.disk_based_diagnostics_finished(language_server_id, cx);
});
view.next_notification(cx).await;
@@ -1060,10 +1107,10 @@ mod tests {
// Diagnostics are added to the first path
project.update(cx, |project, cx| {
project.disk_based_diagnostics_started(0, cx);
project.disk_based_diagnostics_started(language_server_id, cx);
project
.update_diagnostic_entries(
0,
language_server_id,
PathBuf::from("/test/consts.rs"),
None,
vec![
@@ -1096,7 +1143,7 @@ mod tests {
cx,
)
.unwrap();
project.disk_based_diagnostics_finished(0, cx);
project.disk_based_diagnostics_finished(language_server_id, cx);
});
view.next_notification(cx).await;
@@ -1176,10 +1223,274 @@ mod tests {
});
}
fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut AppContext) -> Vec<(u32, String)> {
let mut presenter = cx.build_presenter(editor.id(), 0., Default::default());
let mut cx = presenter.build_layout_context(Default::default(), false, cx);
cx.render(editor, |editor, cx| {
#[gpui::test]
async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
Settings::test_async(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/test",
json!({
"main.js": "
a();
b();
c();
d();
e();
".unindent()
}),
)
.await;
let server_id_1 = LanguageServerId(100);
let server_id_2 = LanguageServerId(101);
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let view = cx.add_view(window_id, |cx| {
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
});
// Two language servers start updating diagnostics
project.update(cx, |project, cx| {
project.disk_based_diagnostics_started(server_id_1, cx);
project.disk_based_diagnostics_started(server_id_2, cx);
project
.update_diagnostic_entries(
server_id_1,
PathBuf::from("/test/main.js"),
None,
vec![DiagnosticEntry {
range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
diagnostic: Diagnostic {
message: "error 1".to_string(),
severity: DiagnosticSeverity::WARNING,
is_primary: true,
is_disk_based: true,
group_id: 1,
..Default::default()
},
}],
cx,
)
.unwrap();
project
.update_diagnostic_entries(
server_id_2,
PathBuf::from("/test/main.js"),
None,
vec![DiagnosticEntry {
range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
diagnostic: Diagnostic {
message: "warning 1".to_string(),
severity: DiagnosticSeverity::ERROR,
is_primary: true,
is_disk_based: true,
group_id: 2,
..Default::default()
},
}],
cx,
)
.unwrap();
});
// The first language server finishes
project.update(cx, |project, cx| {
project.disk_based_diagnostics_finished(server_id_1, cx);
});
// Only the first language server's diagnostics are shown.
cx.foreground().run_until_parked();
view.update(cx, |view, cx| {
assert_eq!(
editor_blocks(&view.editor, cx),
[
(0, "path header block".into()),
(2, "diagnostic header".into()),
]
);
assert_eq!(
view.editor.update(cx, |editor, cx| editor.display_text(cx)),
concat!(
"\n", // filename
"\n", // padding
// diagnostic group 1
"\n", // primary message
"\n", // padding
"a();\n", //
"b();",
)
);
});
// The second language server finishes
project.update(cx, |project, cx| {
project.disk_based_diagnostics_finished(server_id_2, cx);
});
// Both language server's diagnostics are shown.
cx.foreground().run_until_parked();
view.update(cx, |view, cx| {
assert_eq!(
editor_blocks(&view.editor, cx),
[
(0, "path header block".into()),
(2, "diagnostic header".into()),
(6, "collapsed context".into()),
(7, "diagnostic header".into()),
]
);
assert_eq!(
view.editor.update(cx, |editor, cx| editor.display_text(cx)),
concat!(
"\n", // filename
"\n", // padding
// diagnostic group 1
"\n", // primary message
"\n", // padding
"a();\n", // location
"b();\n", //
"\n", // collapsed context
// diagnostic group 2
"\n", // primary message
"\n", // padding
"a();\n", // context
"b();\n", //
"c();", // context
)
);
});
// Both language servers start updating diagnostics, and the first server finishes.
project.update(cx, |project, cx| {
project.disk_based_diagnostics_started(server_id_1, cx);
project.disk_based_diagnostics_started(server_id_2, cx);
project
.update_diagnostic_entries(
server_id_1,
PathBuf::from("/test/main.js"),
None,
vec![DiagnosticEntry {
range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
diagnostic: Diagnostic {
message: "warning 2".to_string(),
severity: DiagnosticSeverity::WARNING,
is_primary: true,
is_disk_based: true,
group_id: 1,
..Default::default()
},
}],
cx,
)
.unwrap();
project
.update_diagnostic_entries(
server_id_2,
PathBuf::from("/test/main.rs"),
None,
vec![],
cx,
)
.unwrap();
project.disk_based_diagnostics_finished(server_id_1, cx);
});
// Only the first language server's diagnostics are updated.
cx.foreground().run_until_parked();
view.update(cx, |view, cx| {
assert_eq!(
editor_blocks(&view.editor, cx),
[
(0, "path header block".into()),
(2, "diagnostic header".into()),
(7, "collapsed context".into()),
(8, "diagnostic header".into()),
]
);
assert_eq!(
view.editor.update(cx, |editor, cx| editor.display_text(cx)),
concat!(
"\n", // filename
"\n", // padding
// diagnostic group 1
"\n", // primary message
"\n", // padding
"a();\n", // location
"b();\n", //
"c();\n", // context
"\n", // collapsed context
// diagnostic group 2
"\n", // primary message
"\n", // padding
"b();\n", // context
"c();\n", //
"d();", // context
)
);
});
// The second language server finishes.
project.update(cx, |project, cx| {
project
.update_diagnostic_entries(
server_id_2,
PathBuf::from("/test/main.js"),
None,
vec![DiagnosticEntry {
range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
diagnostic: Diagnostic {
message: "warning 2".to_string(),
severity: DiagnosticSeverity::WARNING,
is_primary: true,
is_disk_based: true,
group_id: 1,
..Default::default()
},
}],
cx,
)
.unwrap();
project.disk_based_diagnostics_finished(server_id_2, cx);
});
// Both language servers' diagnostics are updated.
cx.foreground().run_until_parked();
view.update(cx, |view, cx| {
assert_eq!(
editor_blocks(&view.editor, cx),
[
(0, "path header block".into()),
(2, "diagnostic header".into()),
(7, "collapsed context".into()),
(8, "diagnostic header".into()),
]
);
assert_eq!(
view.editor.update(cx, |editor, cx| editor.display_text(cx)),
concat!(
"\n", // filename
"\n", // padding
// diagnostic group 1
"\n", // primary message
"\n", // padding
"b();\n", // location
"c();\n", //
"d();\n", // context
"\n", // collapsed context
// diagnostic group 2
"\n", // primary message
"\n", // padding
"c();\n", // context
"d();\n", //
"e();", // context
)
);
});
}
fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
snapshot
.blocks_in_range(0..snapshot.max_point().row())
@@ -1187,7 +1498,7 @@ mod tests {
let name = match block {
TransformBlock::Custom(block) => block
.render(&mut BlockContext {
cx,
view_context: cx,
anchor_x: 0.,
scroll_x: 0.,
gutter_padding: 0.,

View File

@@ -3,19 +3,21 @@ use editor::{Editor, GoToDiagnostic};
use gpui::{
elements::*,
platform::{CursorStyle, MouseButton},
serde_json, AppContext, Entity, ModelHandle, RenderContext, Subscription, View, ViewContext,
ViewHandle, WeakViewHandle,
serde_json, AppContext, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
};
use language::Diagnostic;
use project::Project;
use lsp::LanguageServerId;
use settings::Settings;
use workspace::{item::ItemHandle, StatusItemView};
use workspace::{item::ItemHandle, StatusItemView, Workspace};
use crate::ProjectDiagnosticsEditor;
pub struct DiagnosticIndicator {
summary: project::DiagnosticSummary,
active_editor: Option<WeakViewHandle<Editor>>,
workspace: WeakViewHandle<Workspace>,
current_diagnostic: Option<Diagnostic>,
in_progress_checks: HashSet<usize>,
in_progress_checks: HashSet<LanguageServerId>,
_observe_active_editor: Option<Subscription>,
}
@@ -24,7 +26,8 @@ pub fn init(cx: &mut AppContext) {
}
impl DiagnosticIndicator {
pub fn new(project: &ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
let project = workspace.project();
cx.subscribe(project, |this, project, event, cx| match event {
project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
this.in_progress_checks.insert(*language_server_id);
@@ -45,6 +48,7 @@ impl DiagnosticIndicator {
.language_servers_running_disk_based_diagnostics()
.collect(),
active_editor: None,
workspace: workspace.weak_handle(),
current_diagnostic: None,
_observe_active_editor: None,
}
@@ -84,14 +88,14 @@ impl View for DiagnosticIndicator {
"DiagnosticIndicator"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
enum Summary {}
enum Message {}
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
let in_progress = !self.in_progress_checks.is_empty();
let mut element = Flex::row().with_child(
MouseEventHandler::<Summary>::new(0, cx, |state, cx| {
MouseEventHandler::<Summary, _>::new(0, cx, |state, cx| {
let style = cx
.global::<Settings>()
.theme
@@ -102,23 +106,23 @@ impl View for DiagnosticIndicator {
let mut summary_row = Flex::row();
if self.summary.error_count > 0 {
summary_row.add_children([
summary_row.add_child(
Svg::new("icons/circle_x_mark_16.svg")
.with_color(style.icon_color_error)
.constrained()
.with_width(style.icon_width)
.aligned()
.contained()
.with_margin_right(style.icon_spacing)
.named("error-icon"),
.with_margin_right(style.icon_spacing),
);
summary_row.add_child(
Label::new(self.summary.error_count.to_string(), style.text.clone())
.aligned()
.boxed(),
]);
.aligned(),
);
}
if self.summary.warning_count > 0 {
summary_row.add_children([
summary_row.add_child(
Svg::new("icons/triangle_exclamation_16.svg")
.with_color(style.icon_color_warning)
.constrained()
@@ -130,12 +134,12 @@ impl View for DiagnosticIndicator {
style.summary_spacing
} else {
0.
})
.named("warning-icon"),
}),
);
summary_row.add_child(
Label::new(self.summary.warning_count.to_string(), style.text.clone())
.aligned()
.boxed(),
]);
.aligned(),
);
}
if self.summary.error_count == 0 && self.summary.warning_count == 0 {
@@ -145,7 +149,7 @@ impl View for DiagnosticIndicator {
.constrained()
.with_width(style.icon_width)
.aligned()
.named("ok-icon"),
.into_any_named("ok-icon"),
);
}
@@ -160,11 +164,16 @@ impl View for DiagnosticIndicator {
} else {
style.container_ok
})
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(crate::Deploy))
.with_tooltip::<Summary, _>(
.on_click(MouseButton::Left, |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
})
}
})
.with_tooltip::<Summary>(
0,
"Project Diagnostics".to_string(),
Some(Box::new(crate::Deploy)),
@@ -172,7 +181,7 @@ impl View for DiagnosticIndicator {
cx,
)
.aligned()
.boxed(),
.into_any(),
);
let style = &cx.global::<Settings>().theme.workspace.status_bar;
@@ -183,13 +192,12 @@ impl View for DiagnosticIndicator {
Label::new("Checking…", style.diagnostic_message.default.text.clone())
.aligned()
.contained()
.with_margin_left(item_spacing)
.boxed(),
.with_margin_left(item_spacing),
);
} else if let Some(diagnostic) = &self.current_diagnostic {
let message_style = style.diagnostic_message.clone();
element.add_child(
MouseEventHandler::<Message>::new(1, cx, |state, _| {
MouseEventHandler::<Message, _>::new(1, cx, |state, _| {
Label::new(
diagnostic.message.split('\n').next().unwrap().to_string(),
message_style.style_for(state, false).text.clone(),
@@ -197,17 +205,15 @@ impl View for DiagnosticIndicator {
.aligned()
.contained()
.with_margin_left(item_spacing)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(GoToDiagnostic)
})
.boxed(),
.on_click(MouseButton::Left, |_, this, cx| {
this.go_to_next_diagnostic(&Default::default(), cx)
}),
);
}
element.named("diagnostic indicator")
element.into_any_named("diagnostic indicator")
}
fn debug_json(&self, _: &gpui::AppContext) -> serde_json::Value {

View File

@@ -6,7 +6,7 @@ use gpui::{
geometry::{rect::RectF, vector::Vector2F},
platform::{CursorStyle, MouseButton},
scene::{MouseDown, MouseDrag},
AppContext, Element, ElementBox, EventContext, RenderContext, View, WeakViewHandle,
AnyElement, Element, View, ViewContext, WeakViewHandle, WindowContext,
};
const DEAD_ZONE: f32 = 4.;
@@ -26,7 +26,7 @@ enum State<V: View> {
region_offset: Vector2F,
region: RectF,
payload: Rc<dyn Any + 'static>,
render: Rc<dyn Fn(Rc<dyn Any>, &mut RenderContext<V>) -> ElementBox>,
render: Rc<dyn Fn(Rc<dyn Any>, &mut ViewContext<V>) -> AnyElement<V>>,
},
Canceled,
}
@@ -111,7 +111,7 @@ impl<V: View> DragAndDrop<V> {
})
}
pub fn drag_started(event: MouseDown, cx: &mut EventContext) {
pub fn drag_started(event: MouseDown, cx: &mut WindowContext) {
cx.update_global(|this: &mut Self, _| {
this.currently_dragged = Some(State::Down {
region_offset: event.position - event.region.origin(),
@@ -123,8 +123,8 @@ impl<V: View> DragAndDrop<V> {
pub fn dragging<T: Any>(
event: MouseDrag,
payload: Rc<T>,
cx: &mut EventContext,
render: Rc<impl 'static + Fn(&T, &mut RenderContext<V>) -> ElementBox>,
cx: &mut WindowContext,
render: Rc<impl 'static + Fn(&T, &mut ViewContext<V>) -> AnyElement<V>>,
) {
let window_id = cx.window_id();
cx.update_global(|this: &mut Self, cx| {
@@ -178,7 +178,7 @@ impl<V: View> DragAndDrop<V> {
});
}
pub fn render(cx: &mut RenderContext<V>) -> Option<ElementBox> {
pub fn render(cx: &mut ViewContext<V>) -> Option<AnyElement<V>> {
enum DraggedElementHandler {}
cx.global::<Self>()
.currently_dragged
@@ -199,23 +199,25 @@ impl<V: View> DragAndDrop<V> {
return None;
}
let position = position - region_offset;
let position = (position - region_offset).round();
Some(
Overlay::new(
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {
render(payload, cx)
})
MouseEventHandler::<DraggedElementHandler, V>::new(
0,
cx,
|_, cx| render(payload, cx),
)
.with_cursor_style(CursorStyle::Arrow)
.on_up(MouseButton::Left, |_, cx| {
cx.defer(|cx| {
.on_up(MouseButton::Left, |_, _, cx| {
cx.window_context().defer(|cx| {
cx.update_global::<Self, _, _>(|this, cx| {
this.finish_dragging(cx)
});
});
cx.propagate_event();
})
.on_up_out(MouseButton::Left, |_, cx| {
cx.defer(|cx| {
.on_up_out(MouseButton::Left, |_, _, cx| {
cx.window_context().defer(|cx| {
cx.update_global::<Self, _, _>(|this, cx| {
this.finish_dragging(cx)
});
@@ -225,43 +227,38 @@ impl<V: View> DragAndDrop<V> {
.with_hoverable(false)
.constrained()
.with_width(region.width())
.with_height(region.height())
.boxed(),
.with_height(region.height()),
)
.with_anchor_position(position)
.boxed(),
.into_any(),
)
}
State::Canceled => Some(
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, _| {
Empty::new()
.constrained()
.with_width(0.)
.with_height(0.)
.boxed()
MouseEventHandler::<DraggedElementHandler, V>::new(0, cx, |_, _| {
Empty::new().constrained().with_width(0.).with_height(0.)
})
.on_up(MouseButton::Left, |_, cx| {
cx.defer(|cx| {
.on_up(MouseButton::Left, |_, _, cx| {
cx.window_context().defer(|cx| {
cx.update_global::<Self, _, _>(|this, _| {
this.currently_dragged = None;
});
});
})
.on_up_out(MouseButton::Left, |_, cx| {
cx.defer(|cx| {
.on_up_out(MouseButton::Left, |_, _, cx| {
cx.window_context().defer(|cx| {
cx.update_global::<Self, _, _>(|this, _| {
this.currently_dragged = None;
});
});
})
.boxed(),
.into_any(),
),
}
})
}
pub fn cancel_dragging<P: Any>(&mut self, cx: &mut AppContext) {
pub fn cancel_dragging<P: Any>(&mut self, cx: &mut WindowContext) {
if let Some(State::Dragging {
payload, window_id, ..
}) = &self.currently_dragged
@@ -274,13 +271,13 @@ impl<V: View> DragAndDrop<V> {
}
}
fn finish_dragging(&mut self, cx: &mut AppContext) {
fn finish_dragging(&mut self, cx: &mut WindowContext) {
if let Some(State::Dragging { window_id, .. }) = self.currently_dragged.take() {
self.notify_containers_for_window(window_id, cx);
}
}
fn notify_containers_for_window(&mut self, window_id: usize, cx: &mut AppContext) {
fn notify_containers_for_window(&mut self, window_id: usize, cx: &mut WindowContext) {
self.containers.retain(|container| {
if let Some(container) = container.upgrade(cx) {
if container.window_id() == window_id {
@@ -294,35 +291,35 @@ impl<V: View> DragAndDrop<V> {
}
}
pub trait Draggable {
fn as_draggable<V: View, P: Any>(
pub trait Draggable<V: View> {
fn as_draggable<D: View, P: Any>(
self,
payload: P,
render: impl 'static + Fn(&P, &mut RenderContext<V>) -> ElementBox,
render: impl 'static + Fn(&P, &mut ViewContext<D>) -> AnyElement<D>,
) -> Self
where
Self: Sized;
}
impl<Tag> Draggable for MouseEventHandler<Tag> {
fn as_draggable<V: View, P: Any>(
impl<Tag, V: View> Draggable<V> for MouseEventHandler<Tag, V> {
fn as_draggable<D: View, P: Any>(
self,
payload: P,
render: impl 'static + Fn(&P, &mut RenderContext<V>) -> ElementBox,
render: impl 'static + Fn(&P, &mut ViewContext<D>) -> AnyElement<D>,
) -> Self
where
Self: Sized,
{
let payload = Rc::new(payload);
let render = Rc::new(render);
self.on_down(MouseButton::Left, move |e, cx| {
self.on_down(MouseButton::Left, move |e, _, cx| {
cx.propagate_event();
DragAndDrop::<V>::drag_started(e, cx);
DragAndDrop::<D>::drag_started(e, cx);
})
.on_drag(MouseButton::Left, move |e, cx| {
.on_drag(MouseButton::Left, move |e, _, cx| {
let payload = payload.clone();
let render = render.clone();
DragAndDrop::<V>::dragging(e, payload, cx, render)
DragAndDrop::<D>::dragging(e, payload, cx, render)
})
}
}

View File

@@ -23,6 +23,7 @@ test-support = [
]
[dependencies]
client = { path = "../client" }
clock = { path = "../clock" }
copilot = { path = "../copilot" }
db = { path = "../db" }
@@ -46,20 +47,21 @@ sqlez = { path = "../sqlez" }
workspace = { path = "../workspace" }
aho-corasick = "0.7"
anyhow = "1.0"
futures = "0.3"
anyhow.workspace = true
futures.workspace = true
indoc = "1.0.4"
itertools = "0.10"
lazy_static = "1.4"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
ordered-float = "2.1.1"
parking_lot = "0.11"
postage = { workspace = true }
rand = { version = "0.8.3", optional = true }
serde = { workspace = true }
serde_derive = { workspace = true }
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
lazy_static.workspace = true
log.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true
postage.workspace = true
pulldown-cmark = { version = "0.9.2", default-features = false }
rand = { workspace = true, optional = true }
serde.workspace = true
serde_derive.workspace = true
smallvec.workspace = true
smol.workspace = true
tree-sitter-rust = { version = "*", optional = true }
tree-sitter-html = { version = "*", optional = true }
tree-sitter-javascript = { version = "*", optional = true }
@@ -75,10 +77,12 @@ util = { path = "../util", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
ctor = "0.1"
env_logger = "0.9"
rand = "0.8"
unindent = "0.1.7"
ctor.workspace = true
env_logger.workspace = true
glob.workspace = true
rand.workspace = true
unindent.workspace = true
tree-sitter = "0.20"
tree-sitter-rust = "0.20"
tree-sitter-html = "0.19"

View File

@@ -973,7 +973,7 @@ pub mod tests {
position,
height,
disposition,
render: Arc::new(|_| Empty::new().boxed()),
render: Arc::new(|_| Empty::new().into_any()),
}
})
.collect::<Vec<_>>();

View File

@@ -2,9 +2,9 @@ use super::{
wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
TextHighlights,
};
use crate::{Anchor, ExcerptId, ExcerptRange, ToPoint as _};
use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _};
use collections::{Bound, HashMap, HashSet};
use gpui::{fonts::HighlightStyle, ElementBox, RenderContext};
use gpui::{fonts::HighlightStyle, AnyElement, ViewContext};
use language::{BufferSnapshot, Chunk, Patch, Point};
use parking_lot::Mutex;
use std::{
@@ -50,7 +50,7 @@ struct BlockRow(u32);
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
struct WrapRow(u32);
pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> ElementBox>;
pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>;
pub struct Block {
id: BlockId,
@@ -69,7 +69,7 @@ where
pub position: P,
pub height: u8,
pub style: BlockStyle,
pub render: Arc<dyn Fn(&mut BlockContext) -> ElementBox>,
pub render: Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>,
pub disposition: BlockDisposition,
}
@@ -80,8 +80,8 @@ pub enum BlockStyle {
Sticky,
}
pub struct BlockContext<'a, 'b> {
pub cx: &'b mut RenderContext<'a, crate::Editor>,
pub struct BlockContext<'a, 'b, 'c> {
pub view_context: &'c mut ViewContext<'a, 'b, Editor>,
pub anchor_x: f32,
pub scroll_x: f32,
pub gutter_width: f32,
@@ -833,10 +833,7 @@ impl<'a> Iterator for BlockChunks<'a> {
return Some(Chunk {
text: unsafe { std::str::from_utf8_unchecked(&NEWLINES[..line_count as usize]) },
syntax_highlight_id: None,
highlight_style: None,
diagnostic_severity: None,
is_unnecessary: false,
..Default::default()
});
}
@@ -932,22 +929,22 @@ impl BlockDisposition {
}
}
impl<'a, 'b> Deref for BlockContext<'a, 'b> {
type Target = RenderContext<'a, crate::Editor>;
impl<'a, 'b, 'c> Deref for BlockContext<'a, 'b, 'c> {
type Target = ViewContext<'a, 'b, Editor>;
fn deref(&self) -> &Self::Target {
self.cx
self.view_context
}
}
impl<'a, 'b> DerefMut for BlockContext<'a, 'b> {
impl DerefMut for BlockContext<'_, '_, '_> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.cx
self.view_context
}
}
impl Block {
pub fn render(&self, cx: &mut BlockContext) -> ElementBox {
pub fn render(&self, cx: &mut BlockContext) -> AnyElement<Editor> {
self.render.lock()(cx)
}
@@ -1045,21 +1042,21 @@ mod tests {
position: buffer_snapshot.anchor_after(Point::new(1, 0)),
height: 1,
disposition: BlockDisposition::Above,
render: Arc::new(|_| Empty::new().named("block 1")),
render: Arc::new(|_| Empty::new().into_any_named("block 1")),
},
BlockProperties {
style: BlockStyle::Fixed,
position: buffer_snapshot.anchor_after(Point::new(1, 2)),
height: 2,
disposition: BlockDisposition::Above,
render: Arc::new(|_| Empty::new().named("block 2")),
render: Arc::new(|_| Empty::new().into_any_named("block 2")),
},
BlockProperties {
style: BlockStyle::Fixed,
position: buffer_snapshot.anchor_after(Point::new(3, 3)),
height: 3,
disposition: BlockDisposition::Below,
render: Arc::new(|_| Empty::new().named("block 3")),
render: Arc::new(|_| Empty::new().into_any_named("block 3")),
},
]);
@@ -1219,14 +1216,14 @@ mod tests {
style: BlockStyle::Fixed,
position: buffer_snapshot.anchor_after(Point::new(1, 12)),
disposition: BlockDisposition::Above,
render: Arc::new(|_| Empty::new().named("block 1")),
render: Arc::new(|_| Empty::new().into_any_named("block 1")),
height: 1,
},
BlockProperties {
style: BlockStyle::Fixed,
position: buffer_snapshot.anchor_after(Point::new(1, 1)),
disposition: BlockDisposition::Below,
render: Arc::new(|_| Empty::new().named("block 2")),
render: Arc::new(|_| Empty::new().into_any_named("block 2")),
height: 1,
},
]);
@@ -1329,7 +1326,7 @@ mod tests {
position,
height,
disposition,
render: Arc::new(|_| Empty::new().boxed()),
render: Arc::new(|_| Empty::new().into_any()),
}
})
.collect::<Vec<_>>();

View File

@@ -1065,13 +1065,11 @@ impl<'a> Iterator for FoldChunks<'a> {
self.output_offset += output_text.len();
return Some(Chunk {
text: output_text,
syntax_highlight_id: None,
highlight_style: self.ellipses_color.map(|color| HighlightStyle {
color: Some(color),
..Default::default()
}),
diagnostic_severity: None,
is_unnecessary: false,
..Default::default()
});
}

View File

@@ -531,10 +531,8 @@ impl<'a> Iterator for SuggestionChunks<'a> {
if let Some(chunk) = chunks.next() {
return Some(Chunk {
text: chunk,
syntax_highlight_id: None,
highlight_style: self.highlight_style,
diagnostic_severity: None,
is_unnecessary: false,
..Default::default()
});
} else {
self.suggestion_chunks = None;

View File

@@ -268,6 +268,7 @@ impl TabSnapshot {
tab_size: self.tab_size,
chunk: Chunk {
text: &SPACES[0..(to_next_stop as usize)],
is_tab: true,
..Default::default()
},
inside_leading_tab: to_next_stop > 0,
@@ -545,6 +546,7 @@ impl<'a> Iterator for TabChunks<'a> {
self.output_position = next_output_position;
return Some(Chunk {
text: &SPACES[..len as usize],
is_tab: true,
..self.chunk
});
}
@@ -654,6 +656,56 @@ mod tests {
assert_eq!(tab_snapshot.text(), input);
}
#[gpui::test]
fn test_marking_tabs(cx: &mut gpui::AppContext) {
let input = "\t \thello";
let buffer = MultiBuffer::build_simple(&input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
assert_eq!(
chunks(&tab_snapshot, TabPoint::zero()),
vec![
(" ".to_string(), true),
(" ".to_string(), false),
(" ".to_string(), true),
("hello".to_string(), false),
]
);
assert_eq!(
chunks(&tab_snapshot, TabPoint::new(0, 2)),
vec![
(" ".to_string(), true),
(" ".to_string(), false),
(" ".to_string(), true),
("hello".to_string(), false),
]
);
fn chunks(snapshot: &TabSnapshot, start: TabPoint) -> Vec<(String, bool)> {
let mut chunks = Vec::new();
let mut was_tab = false;
let mut text = String::new();
for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None) {
if chunk.is_tab != was_tab {
if !text.is_empty() {
chunks.push((mem::take(&mut text), was_tab));
}
was_tab = chunk.is_tab;
}
text.push_str(chunk.text);
}
if !text.is_empty() {
chunks.push((text, was_tab));
}
chunks
}
}
#[gpui::test(iterations = 100)]
fn test_random_tabs(cx: &mut gpui::AppContext, mut rng: StdRng) {
let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,21 @@
use crate::{
display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot,
EditorStyle, RangeToAnchorExt,
};
use futures::FutureExt;
use gpui::{
actions,
elements::{Flex, MouseEventHandler, Padding, Text},
impl_internal_actions,
elements::{Flex, MouseEventHandler, Padding, ParentElement, Text},
fonts::{HighlightStyle, Underline, Weight},
platform::{CursorStyle, MouseButton},
AppContext, Axis, Element, ElementBox, ModelHandle, RenderContext, Task, ViewContext,
AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext,
};
use language::{Bias, DiagnosticEntry, DiagnosticSeverity};
use project::{HoverBlock, Project};
use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry};
use project::{HoverBlock, HoverBlockKind, Project};
use settings::Settings;
use std::{ops::Range, time::Duration};
use std::{ops::Range, sync::Arc, time::Duration};
use util::TryFutureExt;
use crate::{
display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot,
EditorStyle, GoToDiagnostic, RangeToAnchorExt,
};
pub const HOVER_DELAY_MILLIS: u64 = 350;
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
@@ -24,21 +23,10 @@ pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
pub const HOVER_POPOVER_GAP: f32 = 10.;
#[derive(Clone, PartialEq)]
pub struct HoverAt {
pub point: Option<DisplayPoint>,
}
#[derive(Copy, Clone, PartialEq)]
pub struct HideHover;
actions!(editor, [Hover]);
impl_internal_actions!(editor, [HoverAt, HideHover]);
pub fn init(cx: &mut AppContext) {
cx.add_action(hover);
cx.add_action(hover_at);
cx.add_action(hide_hover);
}
/// Bindable action which uses the most recent selection head to trigger a hover
@@ -49,12 +37,12 @@ pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
/// The internal hover action dispatches between `show_hover` or `hide_hover`
/// depending on whether a point to hover over is provided.
pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext<Editor>) {
pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) {
if cx.global::<Settings>().hover_popover_enabled {
if let Some(point) = action.point {
if let Some(point) = point {
show_hover(editor, point, false, cx);
} else {
hide_hover(editor, &HideHover, cx);
hide_hover(editor, cx);
}
}
}
@@ -62,7 +50,7 @@ pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext<Edit
/// Hides the type information popup.
/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
/// selections changed.
pub fn hide_hover(editor: &mut Editor, _: &HideHover, cx: &mut ViewContext<Editor>) -> bool {
pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
let did_hide = editor.hover_state.info_popover.take().is_some()
| editor.hover_state.diagnostic_popover.take().is_some();
@@ -129,7 +117,7 @@ fn show_hover(
// Hover triggered from same location as last time. Don't show again.
return;
} else {
hide_hover(editor, &HideHover, cx);
hide_hover(editor, cx);
}
}
}
@@ -149,7 +137,7 @@ fn show_hover(
}
}
let task = cx.spawn_weak(|this, mut cx| {
let task = cx.spawn(|this, mut cx| {
async move {
// If we need to delay, delay a set amount initially before making the lsp request
let delay = if !ignore_timeout {
@@ -201,15 +189,13 @@ fn show_hover(
})
});
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, _| {
this.hover_state.diagnostic_popover =
local_diagnostic.map(|local_diagnostic| DiagnosticPopover {
local_diagnostic,
primary_diagnostic,
});
});
}
this.update(&mut cx, |this, _| {
this.hover_state.diagnostic_popover =
local_diagnostic.map(|local_diagnostic| DiagnosticPopover {
local_diagnostic,
primary_diagnostic,
});
})?;
// Construct new hover popover from hover request
let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
@@ -235,27 +221,27 @@ fn show_hover(
Some(InfoPopover {
project: project.clone(),
symbol_range: range,
contents: hover_result.contents,
blocks: hover_result.contents,
rendered_content: None,
})
});
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
if let Some(hover_popover) = hover_popover.as_ref() {
// Highlight the selected symbol using a background highlight
this.highlight_background::<HoverState>(
vec![hover_popover.symbol_range.clone()],
|theme| theme.editor.hover_popover.highlight,
cx,
);
} else {
this.clear_background_highlights::<HoverState>(cx);
}
this.update(&mut cx, |this, cx| {
if let Some(hover_popover) = hover_popover.as_ref() {
// Highlight the selected symbol using a background highlight
this.highlight_background::<HoverState>(
vec![hover_popover.symbol_range.clone()],
|theme| theme.editor.hover_popover.highlight,
cx,
);
} else {
this.clear_background_highlights::<HoverState>(cx);
}
this.hover_state.info_popover = hover_popover;
cx.notify();
})?;
this.hover_state.info_popover = hover_popover;
cx.notify();
});
}
Ok::<_, anyhow::Error>(())
}
.log_err()
@@ -264,6 +250,225 @@ fn show_hover(
editor.hover_state.info_task = Some(task);
}
fn render_blocks(
theme_id: usize,
blocks: &[HoverBlock],
language_registry: &Arc<LanguageRegistry>,
style: &EditorStyle,
) -> RenderedInfo {
let mut text = String::new();
let mut highlights = Vec::new();
let mut region_ranges = Vec::new();
let mut regions = Vec::new();
for block in blocks {
match &block.kind {
HoverBlockKind::PlainText => {
new_paragraph(&mut text, &mut Vec::new());
text.push_str(&block.text);
}
HoverBlockKind::Markdown => {
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
let mut bold_depth = 0;
let mut italic_depth = 0;
let mut link_url = None;
let mut current_language = None;
let mut list_stack = Vec::new();
for event in Parser::new_ext(&block.text, Options::all()) {
let prev_len = text.len();
match event {
Event::Text(t) => {
if let Some(language) = &current_language {
render_code(
&mut text,
&mut highlights,
t.as_ref(),
language,
style,
);
} else {
text.push_str(t.as_ref());
let mut style = HighlightStyle::default();
if bold_depth > 0 {
style.weight = Some(Weight::BOLD);
}
if italic_depth > 0 {
style.italic = Some(true);
}
if let Some(link_url) = link_url.clone() {
region_ranges.push(prev_len..text.len());
regions.push(RenderedRegion {
link_url: Some(link_url),
code: false,
});
style.underline = Some(Underline {
thickness: 1.0.into(),
..Default::default()
});
}
if style != HighlightStyle::default() {
let mut new_highlight = true;
if let Some((last_range, last_style)) = highlights.last_mut() {
if last_range.end == prev_len && last_style == &style {
last_range.end = text.len();
new_highlight = false;
}
}
if new_highlight {
highlights.push((prev_len..text.len(), style));
}
}
}
}
Event::Code(t) => {
text.push_str(t.as_ref());
region_ranges.push(prev_len..text.len());
if link_url.is_some() {
highlights.push((
prev_len..text.len(),
HighlightStyle {
underline: Some(Underline {
thickness: 1.0.into(),
..Default::default()
}),
..Default::default()
},
));
}
regions.push(RenderedRegion {
code: true,
link_url: link_url.clone(),
});
}
Event::Start(tag) => match tag {
Tag::Paragraph => new_paragraph(&mut text, &mut list_stack),
Tag::Heading(_, _, _) => {
new_paragraph(&mut text, &mut list_stack);
bold_depth += 1;
}
Tag::CodeBlock(kind) => {
new_paragraph(&mut text, &mut list_stack);
if let CodeBlockKind::Fenced(language) = kind {
current_language = language_registry
.language_for_name(language.as_ref())
.now_or_never()
.and_then(Result::ok);
}
}
Tag::Emphasis => italic_depth += 1,
Tag::Strong => bold_depth += 1,
Tag::Link(_, url, _) => link_url = Some(url.to_string()),
Tag::List(number) => {
list_stack.push((number, false));
}
Tag::Item => {
let len = list_stack.len();
if let Some((list_number, has_content)) = list_stack.last_mut() {
*has_content = false;
if !text.is_empty() && !text.ends_with('\n') {
text.push('\n');
}
for _ in 0..len - 1 {
text.push_str(" ");
}
if let Some(number) = list_number {
text.push_str(&format!("{}. ", number));
*number += 1;
*has_content = false;
} else {
text.push_str("- ");
}
}
}
_ => {}
},
Event::End(tag) => match tag {
Tag::Heading(_, _, _) => bold_depth -= 1,
Tag::CodeBlock(_) => current_language = None,
Tag::Emphasis => italic_depth -= 1,
Tag::Strong => bold_depth -= 1,
Tag::Link(_, _, _) => link_url = None,
Tag::List(_) => drop(list_stack.pop()),
_ => {}
},
Event::HardBreak => text.push('\n'),
Event::SoftBreak => text.push(' '),
_ => {}
}
}
}
HoverBlockKind::Code { language } => {
if let Some(language) = language_registry
.language_for_name(language)
.now_or_never()
.and_then(Result::ok)
{
render_code(&mut text, &mut highlights, &block.text, &language, style);
} else {
text.push_str(&block.text);
}
}
}
}
if !text.is_empty() && !text.ends_with('\n') {
text.push('\n');
}
RenderedInfo {
theme_id,
text,
highlights,
region_ranges,
regions,
}
}
fn render_code(
text: &mut String,
highlights: &mut Vec<(Range<usize>, HighlightStyle)>,
content: &str,
language: &Arc<Language>,
style: &EditorStyle,
) {
let prev_len = text.len();
text.push_str(content);
for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
if let Some(style) = highlight_id.style(&style.syntax) {
highlights.push((prev_len + range.start..prev_len + range.end, style));
}
}
}
fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
let mut is_subsequent_paragraph_of_list = false;
if let Some((_, has_content)) = list_stack.last_mut() {
if *has_content {
is_subsequent_paragraph_of_list = true;
} else {
*has_content = true;
return;
}
}
if !text.is_empty() {
if !text.ends_with('\n') {
text.push('\n');
}
text.push('\n');
}
for _ in 0..list_stack.len().saturating_sub(1) {
text.push_str(" ");
}
if is_subsequent_paragraph_of_list {
text.push_str(" ");
}
}
#[derive(Default)]
pub struct HoverState {
pub info_popover: Option<InfoPopover>,
@@ -278,12 +483,12 @@ impl HoverState {
}
pub fn render(
&self,
&mut self,
snapshot: &EditorSnapshot,
style: &EditorStyle,
visible_rows: Range<u32>,
cx: &mut RenderContext<Editor>,
) -> Option<(DisplayPoint, Vec<ElementBox>)> {
cx: &mut ViewContext<Editor>,
) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> {
// If there is a diagnostic, position the popovers based on that.
// Otherwise use the start of the hover range
let anchor = self
@@ -307,7 +512,7 @@ impl HoverState {
if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
elements.push(diagnostic_popover.render(style, cx));
}
if let Some(info_popover) = self.info_popover.as_ref() {
if let Some(info_popover) = self.info_popover.as_mut() {
elements.push(info_popover.render(style, cx));
}
@@ -319,55 +524,101 @@ impl HoverState {
pub struct InfoPopover {
pub project: ModelHandle<Project>,
pub symbol_range: Range<Anchor>,
pub contents: Vec<HoverBlock>,
pub blocks: Vec<HoverBlock>,
rendered_content: Option<RenderedInfo>,
}
#[derive(Debug, Clone)]
struct RenderedInfo {
theme_id: usize,
text: String,
highlights: Vec<(Range<usize>, HighlightStyle)>,
region_ranges: Vec<Range<usize>>,
regions: Vec<RenderedRegion>,
}
#[derive(Debug, Clone)]
struct RenderedRegion {
code: bool,
link_url: Option<String>,
}
impl InfoPopover {
pub fn render(&self, style: &EditorStyle, cx: &mut RenderContext<Editor>) -> ElementBox {
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 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());
pub fn render(
&mut self,
style: &EditorStyle,
cx: &mut ViewContext<Editor>,
) -> AnyElement<Editor> {
if let Some(rendered) = &self.rendered_content {
if rendered.theme_id != style.theme_id {
self.rendered_content = None;
}
}
Text::new(content.text.clone(), style.text.clone())
.with_soft_wrap(true)
.with_highlights(
runs.iter()
.filter_map(|(range, id)| {
id.style(style.theme.syntax.as_ref())
.map(|style| (range.clone(), style))
})
.collect(),
let rendered_content = self.rendered_content.get_or_insert_with(|| {
render_blocks(
style.theme_id,
&self.blocks,
self.project.read(cx).languages(),
style,
)
});
MouseEventHandler::<InfoPopover, _>::new(0, cx, |_, cx| {
let mut region_id = 0;
let view_id = cx.view_id();
let code_span_background_color = style.document_highlight_read_background;
let regions = rendered_content.regions.clone();
Flex::column()
.scrollable::<HoverBlock>(1, None, cx)
.with_child(
Text::new(rendered_content.text.clone(), style.text.clone())
.with_highlights(rendered_content.highlights.clone())
.with_custom_runs(
rendered_content.region_ranges.clone(),
move |ix, bounds, scene, _| {
region_id += 1;
let region = regions[ix].clone();
if let Some(url) = region.link_url {
scene.push_cursor_region(CursorRegion {
bounds,
style: CursorStyle::PointingHand,
});
scene.push_mouse_region(
MouseRegion::new::<Self>(view_id, region_id, bounds)
.on_click::<Editor, _>(
MouseButton::Left,
move |_, _, cx| {
println!("clicked link {url}");
cx.platform().open_url(&url);
},
),
);
}
if region.code {
scene.push_quad(gpui::Quad {
bounds,
background: Some(code_span_background_color),
border: Default::default(),
corner_radius: 2.0,
});
}
},
)
.boxed()
} else {
let mut text_style = style.hover_popover.prose.clone();
text_style.font_size = style.text.font_size;
Text::new(content.text.clone(), text_style)
.with_soft_wrap(true)
.contained()
.with_style(style.hover_popover.block_style)
.boxed()
}
}));
flex.contained()
.with_soft_wrap(true),
)
.contained()
.with_style(style.hover_popover.container)
.boxed()
})
.on_move(|_, _| {}) // Consume move events so they don't reach regions underneath.
.on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath.
.with_cursor_style(CursorStyle::Arrow)
.with_padding(Padding {
bottom: HOVER_POPOVER_GAP,
top: HOVER_POPOVER_GAP,
..Default::default()
})
.boxed()
.into_any()
}
}
@@ -378,11 +629,22 @@ pub struct DiagnosticPopover {
}
impl DiagnosticPopover {
pub fn render(&self, style: &EditorStyle, cx: &mut RenderContext<Editor>) -> ElementBox {
pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> {
enum PrimaryDiagnostic {}
let mut text_style = style.hover_popover.prose.clone();
text_style.font_size = style.text.font_size;
let diagnostic_source_style = style.hover_popover.diagnostic_source_highlight.clone();
let text = match &self.local_diagnostic.diagnostic.source {
Some(source) => Text::new(
format!("{source}: {}", self.local_diagnostic.diagnostic.message),
text_style,
)
.with_highlights(vec![(0..source.len(), diagnostic_source_style)]),
None => Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style),
};
let container_style = match self.local_diagnostic.diagnostic.severity {
DiagnosticSeverity::HINT => style.hover_popover.info_container,
@@ -394,31 +656,29 @@ impl DiagnosticPopover {
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
MouseEventHandler::<DiagnosticPopover>::new(0, cx, |_, _| {
Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style)
.with_soft_wrap(true)
MouseEventHandler::<DiagnosticPopover, _>::new(0, cx, |_, _| {
text.with_soft_wrap(true)
.contained()
.with_style(container_style)
.boxed()
})
.with_padding(Padding {
top: HOVER_POPOVER_GAP,
bottom: HOVER_POPOVER_GAP,
..Default::default()
})
.on_move(|_, _| {}) // Consume move events so they don't reach regions underneath.
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(GoToDiagnostic)
.on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath.
.on_click(MouseButton::Left, |_, this, cx| {
this.go_to_diagnostic(&Default::default(), cx)
})
.with_cursor_style(CursorStyle::PointingHand)
.with_tooltip::<PrimaryDiagnostic, _>(
.with_tooltip::<PrimaryDiagnostic>(
0,
"Go To Diagnostic".to_string(),
Some(Box::new(crate::GoToDiagnostic)),
tooltip_style,
cx,
)
.boxed()
.into_any()
}
pub fn activation_info(&self) -> (usize, Anchor) {
@@ -433,15 +693,16 @@ impl DiagnosticPopover {
#[cfg(test)]
mod tests {
use indoc::indoc;
use language::{Diagnostic, DiagnosticSet};
use project::HoverBlock;
use smol::stream::StreamExt;
use crate::test::editor_lsp_test_context::EditorLspTestContext;
use super::*;
use crate::test::editor_lsp_test_context::EditorLspTestContext;
use gpui::fonts::Weight;
use indoc::indoc;
use language::{Diagnostic, DiagnosticSet};
use lsp::LanguageServerId;
use project::{HoverBlock, HoverBlockKind};
use smol::stream::StreamExt;
use unindent::Unindent;
use util::test::marked_text_ranges;
#[gpui::test]
async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
@@ -462,15 +723,7 @@ mod tests {
fn test() { printˇln!(); }
"});
cx.update_editor(|editor, cx| {
hover_at(
editor,
&HoverAt {
point: Some(hover_point),
},
cx,
)
});
cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
// After delay, hover should be visible.
@@ -482,10 +735,7 @@ mod tests {
Ok(Some(lsp::Hover {
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: indoc! {"
# Some basic docs
Some test documentation"}
.to_string(),
value: "some basic docs".to_string(),
}),
range: Some(symbol_range),
}))
@@ -497,17 +747,11 @@ mod tests {
cx.editor(|editor, _| {
assert!(editor.hover_state.visible());
assert_eq!(
editor.hover_state.info_popover.clone().unwrap().contents,
vec![
HoverBlock {
text: "Some basic docs".to_string(),
language: None
},
HoverBlock {
text: "Some test documentation".to_string(),
language: None
}
]
editor.hover_state.info_popover.clone().unwrap().blocks,
vec![HoverBlock {
text: "some basic docs".to_string(),
kind: HoverBlockKind::Markdown,
},]
)
});
@@ -518,15 +762,7 @@ mod tests {
let mut request = cx
.lsp
.handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
cx.update_editor(|editor, cx| {
hover_at(
editor,
&HoverAt {
point: Some(hover_point),
},
cx,
)
});
cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
cx.foreground()
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
request.next().await;
@@ -558,10 +794,7 @@ mod tests {
Ok(Some(lsp::Hover {
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: indoc! {"
# Some other basic docs
Some other test documentation"}
.to_string(),
value: "some other basic docs".to_string(),
}),
range: Some(symbol_range),
}))
@@ -572,17 +805,11 @@ mod tests {
cx.condition(|editor, _| editor.hover_state.visible()).await;
cx.editor(|editor, _| {
assert_eq!(
editor.hover_state.info_popover.clone().unwrap().contents,
vec![
HoverBlock {
text: "Some other basic docs".to_string(),
language: None
},
HoverBlock {
text: "Some other test documentation".to_string(),
language: None
}
]
editor.hover_state.info_popover.clone().unwrap().blocks,
vec![HoverBlock {
text: "some other basic docs".to_string(),
kind: HoverBlockKind::Markdown,
}]
)
});
}
@@ -620,7 +847,7 @@ mod tests {
}],
&snapshot,
);
buffer.update_diagnostics(set, cx);
buffer.update_diagnostics(LanguageServerId(0), set, cx);
});
// Hover pops diagnostic immediately
@@ -639,10 +866,7 @@ mod tests {
Ok(Some(lsp::Hover {
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: indoc! {"
# Some other basic docs
Some other test documentation"}
.to_string(),
value: "some new docs".to_string(),
}),
range: Some(range),
}))
@@ -655,4 +879,144 @@ mod tests {
hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
});
}
#[gpui::test]
fn test_render_blocks(cx: &mut gpui::TestAppContext) {
Settings::test_async(cx);
cx.add_window(|cx| {
let editor = Editor::single_line(None, cx);
let style = editor.style(cx);
struct Row {
blocks: Vec<HoverBlock>,
expected_marked_text: String,
expected_styles: Vec<HighlightStyle>,
}
let rows = &[
// Strong emphasis
Row {
blocks: vec![HoverBlock {
text: "one **two** three".to_string(),
kind: HoverBlockKind::Markdown,
}],
expected_marked_text: "one «two» three\n".to_string(),
expected_styles: vec![HighlightStyle {
weight: Some(Weight::BOLD),
..Default::default()
}],
},
// Links
Row {
blocks: vec![HoverBlock {
text: "one [two](the-url) three".to_string(),
kind: HoverBlockKind::Markdown,
}],
expected_marked_text: "one «two» three\n".to_string(),
expected_styles: vec![HighlightStyle {
underline: Some(Underline {
thickness: 1.0.into(),
..Default::default()
}),
..Default::default()
}],
},
// Lists
Row {
blocks: vec![HoverBlock {
text: "
lists:
* one
- a
- b
* two
- [c](the-url)
- d
"
.unindent(),
kind: HoverBlockKind::Markdown,
}],
expected_marked_text: "
lists:
- one
- a
- b
- two
- «c»
- d
"
.unindent(),
expected_styles: vec![HighlightStyle {
underline: Some(Underline {
thickness: 1.0.into(),
..Default::default()
}),
..Default::default()
}],
},
// Multi-paragraph list items
Row {
blocks: vec![HoverBlock {
text: "
* one two
three
* four five
* six seven
eight
nine
* ten
* six
"
.unindent(),
kind: HoverBlockKind::Markdown,
}],
expected_marked_text: "
- one two three
- four five
- six seven eight
nine
- ten
- six
"
.unindent(),
expected_styles: vec![HighlightStyle {
underline: Some(Underline {
thickness: 1.0.into(),
..Default::default()
}),
..Default::default()
}],
},
];
for Row {
blocks,
expected_marked_text,
expected_styles,
} in &rows[0..]
{
let rendered = render_blocks(0, &blocks, &Default::default(), &style);
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
let expected_highlights = ranges
.into_iter()
.zip(expected_styles.iter().cloned())
.collect::<Vec<_>>();
assert_eq!(
rendered.text,
dbg!(expected_text),
"wrong text for input {blocks:?}"
);
assert_eq!(
rendered.highlights, expected_highlights,
"wrong highlights for input {blocks:?}"
);
}
editor
});
}
}

View File

@@ -3,11 +3,11 @@ use crate::{
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
};
use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result};
use collections::HashSet;
use futures::future::try_join_all;
use gpui::{
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, RenderContext,
elements::*, geometry::vector::vec2f, AppContext, AsyncAppContext, Entity, ModelHandle,
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use language::{
@@ -28,7 +28,7 @@ use std::{
};
use text::Selection;
use util::{ResultExt, TryFutureExt};
use workspace::item::FollowableItemHandle;
use workspace::item::{BreadcrumbText, FollowableItemHandle};
use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
@@ -67,20 +67,23 @@ impl FollowableItem for Editor {
.collect::<Vec<_>>()
});
let pane = pane.downgrade();
Some(cx.spawn(|mut cx| async move {
let mut buffers = futures::future::try_join_all(buffers).await?;
let editor = pane.read_with(&cx, |pane, cx| {
let mut editors = pane.items_of_type::<Self>();
editors.find(|editor| {
editor.remote_id(&client, cx) == Some(remote_id)
|| state.singleton
&& buffers.len() == 1
&& editor.read(cx).buffer.read(cx).as_singleton().as_ref()
== Some(&buffers[0])
let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
let singleton_buffer_matches = state.singleton
&& buffers.first()
== editor.read(cx).buffer.read(cx).as_singleton().as_ref();
ids_match || singleton_buffer_matches
})
});
})?;
let editor = editor.unwrap_or_else(|| {
let editor = if let Some(editor) = editor {
editor
} else {
pane.update(&mut cx, |_, cx| {
let multibuffer = cx.add_model(|cx| {
let mut multibuffer;
@@ -115,46 +118,29 @@ impl FollowableItem for Editor {
multibuffer
});
cx.add_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx))
})
});
editor.update(&mut cx, |editor, cx| {
editor.remote_id = Some(remote_id);
let buffer = editor.buffer.read(cx).read(cx);
let selections = state
.selections
.into_iter()
.map(|selection| {
deserialize_selection(&buffer, selection)
.ok_or_else(|| anyhow!("invalid selection"))
cx.add_view(|cx| {
let mut editor =
Editor::for_multibuffer(multibuffer, Some(project.clone()), cx);
editor.remote_id = Some(remote_id);
editor
})
.collect::<Result<Vec<_>>>()?;
let pending_selection = state
.pending_selection
.map(|selection| deserialize_selection(&buffer, selection))
.flatten();
let scroll_top_anchor = state
.scroll_top_anchor
.and_then(|anchor| deserialize_anchor(&buffer, anchor));
drop(buffer);
})?
};
if !selections.is_empty() || pending_selection.is_some() {
editor.set_selections_from_remote(selections, pending_selection, cx);
}
if let Some(scroll_top_anchor) = scroll_top_anchor {
editor.set_scroll_anchor_remote(
ScrollAnchor {
top_anchor: scroll_top_anchor,
offset: vec2f(state.scroll_x, state.scroll_y),
},
cx,
);
}
anyhow::Ok(())
})?;
update_editor_from_message(
editor.downgrade(),
project,
proto::update_view::Editor {
selections: state.selections,
pending_selection: state.pending_selection,
scroll_top_anchor: state.scroll_top_anchor,
scroll_x: state.scroll_x,
scroll_y: state.scroll_y,
..Default::default()
},
&mut cx,
)
.await?;
Ok(editor)
}))
@@ -299,96 +285,9 @@ impl FollowableItem for Editor {
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
let update_view::Variant::Editor(message) = message;
let multibuffer = self.buffer.read(cx);
let multibuffer = multibuffer.read(cx);
let buffer_ids = message
.inserted_excerpts
.iter()
.filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
.collect::<HashSet<_>>();
let mut removals = message
.deleted_excerpts
.into_iter()
.map(ExcerptId::from_proto)
.collect::<Vec<_>>();
removals.sort_by(|a, b| a.cmp(&b, &multibuffer));
let selections = message
.selections
.into_iter()
.filter_map(|selection| deserialize_selection(&multibuffer, selection))
.collect::<Vec<_>>();
let pending_selection = message
.pending_selection
.and_then(|selection| deserialize_selection(&multibuffer, selection));
let scroll_top_anchor = message
.scroll_top_anchor
.and_then(|anchor| deserialize_anchor(&multibuffer, anchor));
drop(multibuffer);
let buffers = project.update(cx, |project, cx| {
buffer_ids
.into_iter()
.map(|id| project.open_buffer_by_id(id, cx))
.collect::<Vec<_>>()
});
let project = project.clone();
cx.spawn(|this, mut cx| async move {
let _buffers = try_join_all(buffers).await?;
this.update(&mut cx, |this, cx| {
this.buffer.update(cx, |multibuffer, cx| {
let mut insertions = message.inserted_excerpts.into_iter().peekable();
while let Some(insertion) = insertions.next() {
let Some(excerpt) = insertion.excerpt else { continue };
let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
let buffer_id = excerpt.buffer_id;
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
let adjacent_excerpts = iter::from_fn(|| {
let insertion = insertions.peek()?;
if insertion.previous_excerpt_id.is_none()
&& insertion.excerpt.as_ref()?.buffer_id == buffer_id
{
insertions.next()?.excerpt
} else {
None
}
});
multibuffer.insert_excerpts_with_ids_after(
ExcerptId::from_proto(previous_excerpt_id),
buffer,
[excerpt]
.into_iter()
.chain(adjacent_excerpts)
.filter_map(|excerpt| {
Some((
ExcerptId::from_proto(excerpt.id),
deserialize_excerpt_range(excerpt)?,
))
}),
cx,
);
}
multibuffer.remove_excerpts(removals, cx);
});
if !selections.is_empty() || pending_selection.is_some() {
this.set_selections_from_remote(selections, pending_selection, cx);
this.request_autoscroll_remotely(Autoscroll::newest(), cx);
} else if let Some(anchor) = scroll_top_anchor {
this.set_scroll_anchor_remote(ScrollAnchor {
top_anchor: anchor,
offset: vec2f(message.scroll_x, message.scroll_y)
}, cx);
}
});
Ok(())
update_editor_from_message(this, project, message, &mut cx).await
})
}
@@ -402,6 +301,128 @@ impl FollowableItem for Editor {
}
}
async fn update_editor_from_message(
this: WeakViewHandle<Editor>,
project: ModelHandle<Project>,
message: proto::update_view::Editor,
cx: &mut AsyncAppContext,
) -> Result<()> {
// Open all of the buffers of which excerpts were added to the editor.
let inserted_excerpt_buffer_ids = message
.inserted_excerpts
.iter()
.filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
.collect::<HashSet<_>>();
let inserted_excerpt_buffers = project.update(cx, |project, cx| {
inserted_excerpt_buffer_ids
.into_iter()
.map(|id| project.open_buffer_by_id(id, cx))
.collect::<Vec<_>>()
});
let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
// Update the editor's excerpts.
this.update(cx, |editor, cx| {
editor.buffer.update(cx, |multibuffer, cx| {
let mut removed_excerpt_ids = message
.deleted_excerpts
.into_iter()
.map(ExcerptId::from_proto)
.collect::<Vec<_>>();
removed_excerpt_ids.sort_by({
let multibuffer = multibuffer.read(cx);
move |a, b| a.cmp(&b, &multibuffer)
});
let mut insertions = message.inserted_excerpts.into_iter().peekable();
while let Some(insertion) = insertions.next() {
let Some(excerpt) = insertion.excerpt else { continue };
let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
let buffer_id = excerpt.buffer_id;
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
let adjacent_excerpts = iter::from_fn(|| {
let insertion = insertions.peek()?;
if insertion.previous_excerpt_id.is_none()
&& insertion.excerpt.as_ref()?.buffer_id == buffer_id
{
insertions.next()?.excerpt
} else {
None
}
});
multibuffer.insert_excerpts_with_ids_after(
ExcerptId::from_proto(previous_excerpt_id),
buffer,
[excerpt]
.into_iter()
.chain(adjacent_excerpts)
.filter_map(|excerpt| {
Some((
ExcerptId::from_proto(excerpt.id),
deserialize_excerpt_range(excerpt)?,
))
}),
cx,
);
}
multibuffer.remove_excerpts(removed_excerpt_ids, cx);
});
})?;
// Deserialize the editor state.
let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| {
let buffer = editor.buffer.read(cx).read(cx);
let selections = message
.selections
.into_iter()
.filter_map(|selection| deserialize_selection(&buffer, selection))
.collect::<Vec<_>>();
let pending_selection = message
.pending_selection
.and_then(|selection| deserialize_selection(&buffer, selection));
let scroll_top_anchor = message
.scroll_top_anchor
.and_then(|anchor| deserialize_anchor(&buffer, anchor));
anyhow::Ok((selections, pending_selection, scroll_top_anchor))
})??;
// Wait until the buffer has received all of the operations referenced by
// the editor's new state.
this.update(cx, |editor, cx| {
editor.buffer.update(cx, |buffer, cx| {
buffer.wait_for_anchors(
selections
.iter()
.chain(pending_selection.as_ref())
.flat_map(|selection| [selection.start, selection.end])
.chain(scroll_top_anchor),
cx,
)
})
})?
.await?;
// Update the editor's state.
this.update(cx, |editor, cx| {
if !selections.is_empty() || pending_selection.is_some() {
editor.set_selections_from_remote(selections, pending_selection, cx);
editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
} else if let Some(scroll_top_anchor) = scroll_top_anchor {
editor.set_scroll_anchor_remote(
ScrollAnchor {
top_anchor: scroll_top_anchor,
offset: vec2f(message.scroll_x, message.scroll_y),
},
cx,
);
}
})?;
Ok(())
}
fn serialize_excerpt(
buffer_id: u64,
id: &ExcerptId,
@@ -514,25 +535,38 @@ impl Item for Editor {
}
}
fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
let file_path = self
.buffer()
.read(cx)
.as_singleton()?
.read(cx)
.file()
.and_then(|f| f.as_local())?
.abs_path(cx);
let file_path = util::paths::compact(&file_path)
.to_string_lossy()
.to_string();
Some(file_path.into())
}
fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<str>> {
match path_for_buffer(&self.buffer, detail, true, cx)? {
Cow::Borrowed(path) => Some(path.to_string_lossy()),
Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()),
}
}
fn tab_content(
fn tab_content<T: View>(
&self,
detail: Option<usize>,
style: &theme::Tab,
cx: &AppContext,
) -> ElementBox {
) -> AnyElement<T> {
Flex::row()
.with_child(
Label::new(self.title(cx).to_string(), style.label.clone())
.aligned()
.boxed(),
)
.with_child(Label::new(self.title(cx).to_string(), style.label.clone()).aligned())
.with_children(detail.and_then(|detail| {
let path = path_for_buffer(&self.buffer, detail, false, cx)?;
let description = path.to_string_lossy();
@@ -543,11 +577,10 @@ impl Item for Editor {
)
.contained()
.with_style(style.description.container)
.aligned()
.boxed(),
.aligned(),
)
}))
.boxed()
.into_any()
}
fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
@@ -603,10 +636,10 @@ impl Item for Editor {
project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.report_event("save editor", cx);
self.report_editor_event("save", 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 {
cx.spawn(|_, mut cx| async move {
format.await?;
if buffers.len() == 1 {
@@ -670,11 +703,11 @@ impl Item for Editor {
let transaction = reload_buffers.log_err().await;
this.update(&mut cx, |editor, cx| {
editor.request_autoscroll(Autoscroll::fit(), cx)
});
buffer.update(&mut cx, |buffer, _| {
})?;
buffer.update(&mut cx, |buffer, cx| {
if let Some(transaction) = transaction {
if !buffer.is_singleton() {
buffer.push_transaction(&transaction.0);
buffer.push_transaction(&transaction.0, cx);
}
}
});
@@ -727,7 +760,7 @@ impl Item for Editor {
ToolbarItemLocation::PrimaryLeft { flex: None }
}
fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
let cursor = self.selections.newest_anchor().head();
let multibuffer = &self.buffer().read(cx);
let (buffer_id, symbols) =
@@ -747,15 +780,13 @@ impl Item for Editor {
.map(|path| path.to_string_lossy().to_string())
.unwrap_or_else(|| "untitled".to_string());
let filename_label = Label::new(filename, theme.workspace.breadcrumbs.default.text.clone());
let mut breadcrumbs = vec![filename_label.boxed()];
breadcrumbs.extend(symbols.into_iter().map(|symbol| {
Text::new(
symbol.text,
theme.workspace.breadcrumbs.default.text.clone(),
)
.with_highlights(symbol.highlight_ranges)
.boxed()
let mut breadcrumbs = vec![BreadcrumbText {
text: filename,
highlights: None,
}];
breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText {
text: symbol.text,
highlights: Some(symbol.highlight_ranges),
}));
Some(breadcrumbs)
}
@@ -763,7 +794,7 @@ impl Item for Editor {
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
let workspace_id = workspace.database_id();
let item_id = cx.view_id();
self.workspace_id = Some(workspace_id);
self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
fn serialize(
buffer: ModelHandle<Buffer>,
@@ -788,9 +819,9 @@ impl Item for Editor {
serialize(buffer.clone(), workspace_id, item_id, cx);
cx.subscribe(&buffer, |this, buffer, event, cx| {
if let Some(workspace_id) = this.workspace_id {
if let Some((_, workspace_id)) = this.workspace.as_ref() {
if let language::Event::FileHandleChanged = event {
serialize(buffer, workspace_id, cx.view_id(), cx);
serialize(buffer, *workspace_id, cx.view_id(), cx);
}
}
})
@@ -833,14 +864,13 @@ impl Item for Editor {
let buffer = project_item
.downcast::<Buffer>()
.context("Project item at stored path was not a buffer")?;
Ok(cx.update(|cx| {
cx.add_view(&pane, |cx| {
Ok(pane.update(&mut cx, |_, cx| {
cx.add_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
editor.read_scroll_position_from_db(item_id, workspace_id, cx);
editor
})
}))
})?)
})
})
.unwrap_or_else(|error| Task::ready(Err(error)))
@@ -1078,16 +1108,16 @@ impl View for CursorPosition {
"CursorPosition"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if let Some(position) = self.position {
let theme = &cx.global::<Settings>().theme.workspace.status_bar;
let mut text = format!("{},{}", position.row + 1, position.column + 1);
if self.selected_count > 0 {
write!(text, " ({} selected)", self.selected_count).unwrap();
}
Label::new(text, theme.cursor_position.clone()).boxed()
Label::new(text, theme.cursor_position.clone()).into_any()
} else {
Empty::new().boxed()
Empty::new().into_any()
}
}
}

View File

@@ -1,48 +1,11 @@
use std::ops::Range;
use gpui::{impl_internal_actions, AppContext, Task, ViewContext};
use crate::{Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase};
use gpui::{Task, ViewContext};
use language::{Bias, ToOffset};
use project::LocationLink;
use settings::Settings;
use util::TryFutureExt;
use workspace::Workspace;
use crate::{
Anchor, DisplayPoint, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, Select,
SelectPhase,
};
#[derive(Clone, PartialEq)]
pub struct UpdateGoToDefinitionLink {
pub point: Option<DisplayPoint>,
pub cmd_held: bool,
pub shift_held: bool,
}
#[derive(Clone, PartialEq)]
pub struct GoToFetchedDefinition {
pub point: DisplayPoint,
}
#[derive(Clone, PartialEq)]
pub struct GoToFetchedTypeDefinition {
pub point: DisplayPoint,
}
impl_internal_actions!(
editor,
[
UpdateGoToDefinitionLink,
GoToFetchedDefinition,
GoToFetchedTypeDefinition
]
);
pub fn init(cx: &mut AppContext) {
cx.add_action(update_go_to_definition_link);
cx.add_action(go_to_fetched_definition);
cx.add_action(go_to_fetched_type_definition);
}
#[derive(Debug, Default)]
pub struct LinkGoToDefinitionState {
@@ -55,11 +18,9 @@ pub struct LinkGoToDefinitionState {
pub fn update_go_to_definition_link(
editor: &mut Editor,
&UpdateGoToDefinitionLink {
point,
cmd_held,
shift_held,
}: &UpdateGoToDefinitionLink,
point: Option<DisplayPoint>,
cmd_held: bool,
shift_held: bool,
cx: &mut ViewContext<Editor>,
) {
let pending_nonempty_selection = editor.has_pending_nonempty_selection();
@@ -171,7 +132,7 @@ pub fn show_link_definition(
}
}
let task = cx.spawn_weak(|this, mut cx| {
let task = cx.spawn(|this, mut cx| {
async move {
// query the LSP for definition info
let definition_request = cx.update(|cx| {
@@ -202,67 +163,65 @@ pub fn show_link_definition(
)
});
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
// Clear any existing highlights
this.clear_text_highlights::<LinkGoToDefinitionState>(cx);
this.link_go_to_definition_state.kind = Some(definition_kind);
this.link_go_to_definition_state.symbol_range = result
.as_ref()
.and_then(|(symbol_range, _)| symbol_range.clone());
this.update(&mut cx, |this, cx| {
// Clear any existing highlights
this.clear_text_highlights::<LinkGoToDefinitionState>(cx);
this.link_go_to_definition_state.kind = Some(definition_kind);
this.link_go_to_definition_state.symbol_range = result
.as_ref()
.and_then(|(symbol_range, _)| symbol_range.clone());
if let Some((symbol_range, definitions)) = result {
this.link_go_to_definition_state.definitions = definitions.clone();
if let Some((symbol_range, definitions)) = result {
this.link_go_to_definition_state.definitions = definitions.clone();
let buffer_snapshot = buffer.read(cx).snapshot();
let buffer_snapshot = buffer.read(cx).snapshot();
// Only show highlight if there exists a definition to jump to that doesn't contain
// the current location.
let any_definition_does_not_contain_current_location =
definitions.iter().any(|definition| {
let target = &definition.target;
if target.buffer == buffer {
let range = &target.range;
// Expand range by one character as lsp definition ranges include positions adjacent
// but not contained by the symbol range
let start = buffer_snapshot.clip_offset(
range.start.to_offset(&buffer_snapshot).saturating_sub(1),
Bias::Left,
);
let end = buffer_snapshot.clip_offset(
range.end.to_offset(&buffer_snapshot) + 1,
Bias::Right,
);
let offset = buffer_position.to_offset(&buffer_snapshot);
!(start <= offset && end >= offset)
} else {
true
}
});
// Only show highlight if there exists a definition to jump to that doesn't contain
// the current location.
let any_definition_does_not_contain_current_location =
definitions.iter().any(|definition| {
let target = &definition.target;
if target.buffer == buffer {
let range = &target.range;
// Expand range by one character as lsp definition ranges include positions adjacent
// but not contained by the symbol range
let start = buffer_snapshot.clip_offset(
range.start.to_offset(&buffer_snapshot).saturating_sub(1),
Bias::Left,
);
let end = buffer_snapshot.clip_offset(
range.end.to_offset(&buffer_snapshot) + 1,
Bias::Right,
);
let offset = buffer_position.to_offset(&buffer_snapshot);
!(start <= offset && end >= offset)
} else {
true
}
});
if any_definition_does_not_contain_current_location {
// If no symbol range returned from language server, use the surrounding word.
let highlight_range = symbol_range.unwrap_or_else(|| {
let snapshot = &snapshot.buffer_snapshot;
let (offset_range, _) = snapshot.surrounding_word(trigger_point);
if any_definition_does_not_contain_current_location {
// If no symbol range returned from language server, use the surrounding word.
let highlight_range = symbol_range.unwrap_or_else(|| {
let snapshot = &snapshot.buffer_snapshot;
let (offset_range, _) = snapshot.surrounding_word(trigger_point);
snapshot.anchor_before(offset_range.start)
..snapshot.anchor_after(offset_range.end)
});
snapshot.anchor_before(offset_range.start)
..snapshot.anchor_after(offset_range.end)
});
// Highlight symbol using theme link definition highlight style
let style = cx.global::<Settings>().theme.editor.link_definition;
this.highlight_text::<LinkGoToDefinitionState>(
vec![highlight_range],
style,
cx,
);
} else {
hide_link_definition(this, cx);
}
// Highlight symbol using theme link definition highlight style
let style = cx.global::<Settings>().theme.editor.link_definition;
this.highlight_text::<LinkGoToDefinitionState>(
vec![highlight_range],
style,
cx,
);
} else {
hide_link_definition(this, cx);
}
})
}
}
})?;
Ok::<_, anyhow::Error>(())
}
@@ -287,70 +246,51 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
}
pub fn go_to_fetched_definition(
workspace: &mut Workspace,
&GoToFetchedDefinition { point }: &GoToFetchedDefinition,
cx: &mut ViewContext<Workspace>,
editor: &mut Editor,
point: DisplayPoint,
cx: &mut ViewContext<Editor>,
) {
go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, workspace, point, cx);
go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, editor, point, cx);
}
pub fn go_to_fetched_type_definition(
workspace: &mut Workspace,
&GoToFetchedTypeDefinition { point }: &GoToFetchedTypeDefinition,
cx: &mut ViewContext<Workspace>,
editor: &mut Editor,
point: DisplayPoint,
cx: &mut ViewContext<Editor>,
) {
go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, workspace, point, cx);
go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, editor, point, cx);
}
fn go_to_fetched_definition_of_kind(
kind: LinkDefinitionKind,
workspace: &mut Workspace,
editor: &mut Editor,
point: DisplayPoint,
cx: &mut ViewContext<Workspace>,
cx: &mut ViewContext<Editor>,
) {
let active_item = workspace.active_item(cx);
let editor_handle = if let Some(editor) = active_item
.as_ref()
.and_then(|item| item.act_as::<Editor>(cx))
{
editor
} else {
return;
};
let (cached_definitions, cached_definitions_kind) = editor_handle.update(cx, |editor, cx| {
let definitions = editor.link_go_to_definition_state.definitions.clone();
hide_link_definition(editor, cx);
(definitions, editor.link_go_to_definition_state.kind)
});
let cached_definitions = editor.link_go_to_definition_state.definitions.clone();
hide_link_definition(editor, cx);
let cached_definitions_kind = editor.link_go_to_definition_state.kind;
let is_correct_kind = cached_definitions_kind == Some(kind);
if !cached_definitions.is_empty() && is_correct_kind {
editor_handle.update(cx, |editor, cx| {
if !editor.focused {
cx.focus_self();
}
});
if !editor.focused {
cx.focus_self();
}
Editor::navigate_to_definitions(workspace, editor_handle, cached_definitions, cx);
editor.navigate_to_definitions(cached_definitions, cx);
} else {
editor_handle.update(cx, |editor, cx| {
editor.select(
&Select(SelectPhase::Begin {
position: point,
add: false,
click_count: 1,
}),
cx,
);
});
editor.select(
SelectPhase::Begin {
position: point,
add: false,
click_count: 1,
},
cx,
);
match kind {
LinkDefinitionKind::Symbol => Editor::go_to_definition(workspace, &GoToDefinition, cx),
LinkDefinitionKind::Type => {
Editor::go_to_type_definition(workspace, &GoToTypeDefinition, cx)
}
LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx),
LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx),
}
}
}
@@ -413,15 +353,7 @@ mod tests {
// Press cmd+shift to trigger highlight
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: true,
shift_held: true,
},
cx,
);
update_go_to_definition_link(editor, Some(hover_point), true, true, cx);
});
requests.next().await;
cx.foreground().run_until_parked();
@@ -471,12 +403,8 @@ mod tests {
])))
});
cx.update_workspace(|workspace, cx| {
go_to_fetched_type_definition(
workspace,
&GoToFetchedTypeDefinition { point: hover_point },
cx,
);
cx.update_editor(|editor, cx| {
go_to_fetched_type_definition(editor, hover_point, cx);
});
requests.next().await;
cx.foreground().run_until_parked();
@@ -529,15 +457,7 @@ mod tests {
});
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: true,
shift_held: false,
},
cx,
);
update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
});
requests.next().await;
cx.foreground().run_until_parked();
@@ -571,15 +491,7 @@ mod tests {
])))
});
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: true,
shift_held: false,
},
cx,
);
update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
});
requests.next().await;
cx.foreground().run_until_parked();
@@ -601,15 +513,7 @@ mod tests {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
});
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: true,
shift_held: false,
},
cx,
);
update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
});
requests.next().await;
cx.foreground().run_until_parked();
@@ -626,15 +530,7 @@ mod tests {
fn do_work() { teˇst(); }
"});
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: false,
shift_held: false,
},
cx,
);
update_go_to_definition_link(editor, Some(hover_point), false, false, cx);
});
cx.foreground().run_until_parked();
@@ -693,15 +589,7 @@ mod tests {
// Moving the mouse restores the highlights.
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: true,
shift_held: false,
},
cx,
);
update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
});
cx.foreground().run_until_parked();
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
@@ -715,15 +603,7 @@ mod tests {
fn do_work() { tesˇt(); }
"});
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: true,
shift_held: false,
},
cx,
);
update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
});
cx.foreground().run_until_parked();
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
@@ -732,8 +612,8 @@ mod tests {
"});
// Cmd click with existing definition doesn't re-request and dismisses highlight
cx.update_workspace(|workspace, cx| {
go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
cx.update_editor(|editor, cx| {
go_to_fetched_definition(editor, hover_point, cx);
});
// Assert selection moved to to definition
cx.lsp
@@ -773,8 +653,8 @@ mod tests {
},
])))
});
cx.update_workspace(|workspace, cx| {
go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
cx.update_editor(|editor, cx| {
go_to_fetched_definition(editor, hover_point, cx);
});
requests.next().await;
cx.foreground().run_until_parked();
@@ -819,15 +699,7 @@ mod tests {
});
});
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
&UpdateGoToDefinitionLink {
point: Some(hover_point),
cmd_held: true,
shift_held: false,
},
cx,
);
update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
});
cx.foreground().run_until_parked();
assert!(requests.try_next().is_err());

View File

@@ -1,29 +1,14 @@
use context_menu::ContextMenuItem;
use gpui::{
elements::AnchorCorner, geometry::vector::Vector2F, impl_internal_actions, AppContext,
ViewContext,
};
use crate::{
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
Rename, RevealInFinder, SelectMode, ToggleCodeActions,
};
#[derive(Clone, PartialEq)]
pub struct DeployMouseContextMenu {
pub position: Vector2F,
pub point: DisplayPoint,
}
impl_internal_actions!(editor, [DeployMouseContextMenu]);
pub fn init(cx: &mut AppContext) {
cx.add_action(deploy_context_menu);
}
use context_menu::ContextMenuItem;
use gpui::{elements::AnchorCorner, geometry::vector::Vector2F, ViewContext};
pub fn deploy_context_menu(
editor: &mut Editor,
&DeployMouseContextMenu { position, point }: &DeployMouseContextMenu,
position: Vector2F,
point: DisplayPoint,
cx: &mut ViewContext<Editor>,
) {
if !editor.focused {
@@ -51,18 +36,18 @@ pub fn deploy_context_menu(
position,
AnchorCorner::TopLeft,
vec![
ContextMenuItem::item("Rename Symbol", Rename),
ContextMenuItem::item("Go to Definition", GoToDefinition),
ContextMenuItem::item("Go to Type Definition", GoToTypeDefinition),
ContextMenuItem::item("Find All References", FindAllReferences),
ContextMenuItem::item(
ContextMenuItem::action("Rename Symbol", Rename),
ContextMenuItem::action("Go to Definition", GoToDefinition),
ContextMenuItem::action("Go to Type Definition", GoToTypeDefinition),
ContextMenuItem::action("Find All References", FindAllReferences),
ContextMenuItem::action(
"Code Actions",
ToggleCodeActions {
deployed_from_indicator: false,
},
),
ContextMenuItem::Separator,
ContextMenuItem::item("Reveal in Finder", RevealInFinder),
ContextMenuItem::action("Reveal in Finder", RevealInFinder),
],
cx,
);
@@ -98,16 +83,7 @@ mod tests {
do_wˇork();
}
"});
cx.update_editor(|editor, cx| {
deploy_context_menu(
editor,
&DeployMouseContextMenu {
position: Default::default(),
point,
},
cx,
)
});
cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx));
cx.assert_editor_state(indoc! {"
fn test() {

View File

@@ -1,6 +1,7 @@
mod anchor;
pub use anchor::{Anchor, AnchorRangeExt};
use anyhow::{anyhow, Result};
use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet};
use futures::{channel::mpsc, SinkExt};
@@ -9,14 +10,16 @@ use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
pub use language::Completion;
use language::{
char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
DiagnosticEntry, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline,
OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _,
ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16,
Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _,
ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
};
use std::{
borrow::Cow,
cell::{Ref, RefCell},
cmp, fmt, io,
cmp, fmt,
future::Future,
io,
iter::{self, FromIterator},
mem,
ops::{Range, RangeBounds, Sub},
@@ -40,7 +43,7 @@ pub struct ExcerptId(usize);
pub struct MultiBuffer {
snapshot: RefCell<MultiBufferSnapshot>,
buffers: RefCell<HashMap<usize, BufferState>>,
buffers: RefCell<HashMap<u64, BufferState>>,
next_excerpt_id: usize,
subscriptions: Topic,
singleton: bool,
@@ -61,6 +64,7 @@ pub enum Event {
},
Edited,
Reloaded,
LanguageChanged,
Reparsed,
Saved,
FileHandleChanged,
@@ -81,7 +85,7 @@ struct History {
#[derive(Clone)]
struct Transaction {
id: TransactionId,
buffer_transactions: HashMap<usize, text::TransactionId>,
buffer_transactions: HashMap<u64, text::TransactionId>,
first_edit_at: Instant,
last_edit_at: Instant,
suppress_grouping: bool,
@@ -141,7 +145,7 @@ pub struct ExcerptBoundary {
struct Excerpt {
id: ExcerptId,
locator: Locator,
buffer_id: usize,
buffer_id: u64,
buffer: BufferSnapshot,
range: ExcerptRange<text::Anchor>,
max_buffer_row: u32,
@@ -333,7 +337,7 @@ impl MultiBuffer {
offset: T,
theme: Option<&SyntaxTheme>,
cx: &AppContext,
) -> Option<(usize, Vec<OutlineItem<Anchor>>)> {
) -> Option<(u64, Vec<OutlineItem<Anchor>>)> {
self.read(cx).symbols_containing(offset, theme)
}
@@ -390,7 +394,7 @@ impl MultiBuffer {
is_insertion: bool,
original_indent_column: u32,
}
let mut buffer_edits: HashMap<usize, Vec<BufferEdit>> = Default::default();
let mut buffer_edits: HashMap<u64, 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();
@@ -589,7 +593,7 @@ impl MultiBuffer {
if let Some(transaction_id) =
buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx))
{
buffer_transactions.insert(buffer.id(), transaction_id);
buffer_transactions.insert(buffer.read(cx).remote_id(), transaction_id);
}
}
@@ -610,12 +614,12 @@ impl MultiBuffer {
}
}
pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T)
pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T, cx: &mut ModelContext<Self>)
where
T: IntoIterator<Item = (&'a ModelHandle<Buffer>, &'a language::Transaction)>,
{
self.history
.push_transaction(buffer_transactions, Instant::now());
.push_transaction(buffer_transactions, Instant::now(), cx);
self.history.finalize_last_transaction();
}
@@ -640,7 +644,7 @@ impl MultiBuffer {
cursor_shape: CursorShape,
cx: &mut ModelContext<Self>,
) {
let mut selections_by_buffer: HashMap<usize, Vec<Selection<text::Anchor>>> =
let mut selections_by_buffer: HashMap<u64, Vec<Selection<text::Anchor>>> =
Default::default();
let snapshot = self.read(cx);
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
@@ -781,8 +785,8 @@ impl MultiBuffer {
let (mut tx, rx) = mpsc::channel(256);
let task = cx.spawn(|this, mut cx| async move {
for (buffer, ranges) in excerpts {
let buffer_id = buffer.id();
let buffer_snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot());
let (buffer_id, buffer_snapshot) =
buffer.read_with(&cx, |buffer, _| (buffer.remote_id(), buffer.snapshot()));
let mut excerpt_ranges = Vec::new();
let mut range_counts = Vec::new();
@@ -851,7 +855,7 @@ impl MultiBuffer {
where
O: text::ToPoint + text::ToOffset,
{
let buffer_id = buffer.id();
let buffer_id = buffer.read(cx).remote_id();
let buffer_snapshot = buffer.read(cx).snapshot();
let (excerpt_ranges, range_counts) =
build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count);
@@ -920,7 +924,7 @@ impl MultiBuffer {
self.sync(cx);
let buffer_id = buffer.id();
let buffer_id = buffer.read(cx).remote_id();
let buffer_snapshot = buffer.read(cx).snapshot();
let mut buffers = self.buffers.borrow_mut();
@@ -1047,7 +1051,7 @@ impl MultiBuffer {
let buffers = self.buffers.borrow();
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
for locator in buffers
.get(&buffer.id())
.get(&buffer.read(cx).remote_id())
.map(|state| &state.excerpts)
.into_iter()
.flatten()
@@ -1238,6 +1242,39 @@ impl MultiBuffer {
cx.notify();
}
pub fn wait_for_anchors<'a>(
&self,
anchors: impl 'a + Iterator<Item = Anchor>,
cx: &mut ModelContext<Self>,
) -> impl 'static + Future<Output = Result<()>> {
let borrow = self.buffers.borrow();
let mut error = None;
let mut futures = Vec::new();
for anchor in anchors {
if let Some(buffer_id) = anchor.buffer_id {
if let Some(buffer) = borrow.get(&buffer_id) {
buffer.buffer.update(cx, |buffer, _| {
futures.push(buffer.wait_for_anchors([anchor.text_anchor]))
});
} else {
error = Some(anyhow!(
"buffer {buffer_id} is not part of this multi-buffer"
));
break;
}
}
}
async move {
if let Some(error) = error {
Err(error)?;
}
for future in futures {
future.await?;
}
Ok(())
}
}
pub fn text_anchor_for_position<T: ToOffset>(
&self,
position: T,
@@ -1266,6 +1303,7 @@ impl MultiBuffer {
language::Event::Saved => Event::Saved,
language::Event::FileHandleChanged => Event::FileHandleChanged,
language::Event::Reloaded => Event::Reloaded,
language::Event::LanguageChanged => Event::LanguageChanged,
language::Event::Reparsed => Event::Reparsed,
language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
language::Event::Closed => Event::Closed,
@@ -1283,7 +1321,7 @@ impl MultiBuffer {
.collect()
}
pub fn buffer(&self, buffer_id: usize) -> Option<ModelHandle<Buffer>> {
pub fn buffer(&self, buffer_id: u64) -> Option<ModelHandle<Buffer>> {
self.buffers
.borrow()
.get(&buffer_id)
@@ -1440,8 +1478,8 @@ impl MultiBuffer {
for (locator, buffer, buffer_edited) in excerpts_to_edit {
new_excerpts.push_tree(cursor.slice(&Some(locator), Bias::Left, &()), &());
let old_excerpt = cursor.item().unwrap();
let buffer_id = buffer.id();
let buffer = buffer.read(cx);
let buffer_id = buffer.remote_id();
let mut new_excerpt;
if buffer_edited {
@@ -1567,11 +1605,11 @@ impl MultiBuffer {
let buffer_handle = if rng.gen() || self.buffers.borrow().is_empty() {
let text = RandomCharIter::new(&mut *rng).take(10).collect::<String>();
buffers.push(cx.add_model(|cx| Buffer::new(0, text, cx)));
let buffer = buffers.last().unwrap();
let buffer = buffers.last().unwrap().read(cx);
log::info!(
"Creating new buffer {} with text: {:?}",
buffer.id(),
buffer.read(cx).text()
buffer.remote_id(),
buffer.text()
);
buffers.last().unwrap().clone()
} else {
@@ -1599,7 +1637,7 @@ impl MultiBuffer {
.collect::<Vec<_>>();
log::info!(
"Inserting excerpts from buffer {} and ranges {:?}: {:?}",
buffer_handle.id(),
buffer_handle.read(cx).remote_id(),
ranges.iter().map(|r| &r.context).collect::<Vec<_>>(),
ranges
.iter()
@@ -1792,7 +1830,7 @@ impl MultiBufferSnapshot {
(start..end, word_kind)
}
pub fn as_singleton(&self) -> Option<(&ExcerptId, usize, &BufferSnapshot)> {
pub fn as_singleton(&self) -> Option<(&ExcerptId, u64, &BufferSnapshot)> {
if self.singleton {
self.excerpts
.iter()
@@ -2716,6 +2754,11 @@ impl MultiBufferSnapshot {
self.trailing_excerpt_update_count
}
pub fn file_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc<dyn File>> {
self.point_to_buffer_offset(point)
.and_then(|(buffer, _)| buffer.file())
}
pub fn language_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc<Language>> {
self.point_to_buffer_offset(point)
.and_then(|(buffer, offset)| buffer.language_at(offset))
@@ -2726,6 +2769,15 @@ impl MultiBufferSnapshot {
.and_then(|(buffer, offset)| buffer.language_scope_at(offset))
}
pub fn language_indent_size_at<T: ToOffset>(
&self,
position: T,
cx: &AppContext,
) -> Option<IndentSize> {
let (buffer_snapshot, offset) = self.point_to_buffer_offset(position)?;
Some(buffer_snapshot.language_indent_size_at(offset, cx))
}
pub fn is_dirty(&self) -> bool {
self.is_dirty
}
@@ -2753,7 +2805,7 @@ impl MultiBufferSnapshot {
) -> impl Iterator<Item = DiagnosticEntry<O>> + 'a
where
T: 'a + ToOffset,
O: 'a + text::FromAnchor,
O: 'a + text::FromAnchor + Ord,
{
self.as_singleton()
.into_iter()
@@ -2886,7 +2938,7 @@ impl MultiBufferSnapshot {
&self,
offset: T,
theme: Option<&SyntaxTheme>,
) -> Option<(usize, Vec<OutlineItem<Anchor>>)> {
) -> Option<(u64, Vec<OutlineItem<Anchor>>)> {
let anchor = self.anchor_before(offset);
let excerpt_id = anchor.excerpt_id();
let excerpt = self.excerpt(excerpt_id)?;
@@ -2926,7 +2978,7 @@ impl MultiBufferSnapshot {
}
}
pub fn buffer_id_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<usize> {
pub fn buffer_id_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<u64> {
Some(self.excerpt(excerpt_id)?.buffer_id)
}
@@ -3064,7 +3116,7 @@ impl History {
fn end_transaction(
&mut self,
now: Instant,
buffer_transactions: HashMap<usize, TransactionId>,
buffer_transactions: HashMap<u64, TransactionId>,
) -> bool {
assert_ne!(self.transaction_depth, 0);
self.transaction_depth -= 1;
@@ -3089,8 +3141,12 @@ impl History {
}
}
fn push_transaction<'a, T>(&mut self, buffer_transactions: T, now: Instant)
where
fn push_transaction<'a, T>(
&mut self,
buffer_transactions: T,
now: Instant,
cx: &mut ModelContext<MultiBuffer>,
) where
T: IntoIterator<Item = (&'a ModelHandle<Buffer>, &'a language::Transaction)>,
{
assert_eq!(self.transaction_depth, 0);
@@ -3098,7 +3154,7 @@ impl History {
id: self.next_transaction_id.tick(),
buffer_transactions: buffer_transactions
.into_iter()
.map(|(buffer, transaction)| (buffer.id(), transaction.id))
.map(|(buffer, transaction)| (buffer.read(cx).remote_id(), transaction.id))
.collect(),
first_edit_at: now,
last_edit_at: now,
@@ -3195,7 +3251,7 @@ impl Excerpt {
fn new(
id: ExcerptId,
locator: Locator,
buffer_id: usize,
buffer_id: u64,
buffer: BufferSnapshot,
range: ExcerptRange<text::Anchor>,
has_trailing_newline: bool,
@@ -4663,7 +4719,7 @@ mod tests {
"Inserting excerpt at {} of {} for buffer {}: {:?}[{:?}] = {:?}",
excerpt_ix,
expected_excerpts.len(),
buffer_handle.id(),
buffer_handle.read(cx).remote_id(),
buffer.text(),
start_ix..end_ix,
&buffer.text()[start_ix..end_ix]
@@ -4749,8 +4805,8 @@ mod tests {
let mut excerpt_starts = excerpt_starts.into_iter();
for (buffer, range) in &expected_excerpts {
let buffer_id = buffer.id();
let buffer = buffer.read(cx);
let buffer_id = buffer.remote_id();
let buffer_range = range.to_offset(buffer);
let buffer_start_point = buffer.offset_to_point(buffer_range.start);
let buffer_start_point_utf16 =

View File

@@ -8,7 +8,7 @@ use sum_tree::Bias;
#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
pub struct Anchor {
pub(crate) buffer_id: Option<usize>,
pub(crate) buffer_id: Option<u64>,
pub(crate) excerpt_id: ExcerptId,
pub(crate) text_anchor: text::Anchor,
}

View File

@@ -17,7 +17,7 @@ use workspace::WorkspaceId;
use crate::{
display_map::{DisplaySnapshot, ToDisplayPoint},
hover_popover::{hide_hover, HideHover},
hover_popover::hide_hover,
persistence::DB,
Anchor, DisplayPoint, Editor, EditorMode, Event, MultiBufferSnapshot, ToPoint,
};
@@ -245,14 +245,14 @@ impl ScrollManager {
}
if cx.default_global::<ScrollbarAutoHide>().0 {
self.hide_scrollbar_task = Some(cx.spawn_weak(|editor, mut cx| async move {
self.hide_scrollbar_task = Some(cx.spawn(|editor, mut cx| async move {
cx.background().timer(SCROLLBAR_SHOW_INTERVAL).await;
if let Some(editor) = editor.upgrade(&cx) {
editor.update(&mut cx, |editor, cx| {
editor
.update(&mut cx, |editor, cx| {
editor.scroll_manager.show_scrollbars = false;
cx.notify();
});
}
})
.log_err();
}));
} else {
self.hide_scrollbar_task = None;
@@ -307,14 +307,10 @@ impl Editor {
) {
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
hide_hover(self, &HideHover, cx);
self.scroll_manager.set_scroll_position(
scroll_position,
&map,
local,
self.workspace_id,
cx,
);
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
self.scroll_manager
.set_scroll_position(scroll_position, &map, local, workspace_id, cx);
}
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
@@ -323,13 +319,14 @@ impl Editor {
}
pub fn set_scroll_anchor(&mut self, scroll_anchor: ScrollAnchor, cx: &mut ViewContext<Self>) {
hide_hover(self, &HideHover, cx);
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
let top_row = scroll_anchor
.top_anchor
.to_point(&self.buffer().read(cx).snapshot(cx))
.row;
self.scroll_manager
.set_anchor(scroll_anchor, top_row, true, self.workspace_id, cx);
.set_anchor(scroll_anchor, top_row, true, workspace_id, cx);
}
pub(crate) fn set_scroll_anchor_remote(
@@ -337,13 +334,14 @@ impl Editor {
scroll_anchor: ScrollAnchor,
cx: &mut ViewContext<Self>,
) {
hide_hover(self, &HideHover, cx);
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
let top_row = scroll_anchor
.top_anchor
.to_point(&self.buffer().read(cx).snapshot(cx))
.row;
self.scroll_manager
.set_anchor(scroll_anchor, top_row, false, self.workspace_id, cx);
.set_anchor(scroll_anchor, top_row, false, workspace_id, cx);
}
pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {

View File

@@ -1,6 +1,4 @@
use gpui::{
actions, geometry::vector::Vector2F, impl_internal_actions, AppContext, Axis, ViewContext,
};
use gpui::{actions, geometry::vector::Vector2F, AppContext, Axis, ViewContext};
use language::Bias;
use crate::{Editor, EditorMode};
@@ -23,17 +21,8 @@ actions!(
]
);
#[derive(Clone, PartialEq)]
pub struct Scroll {
pub scroll_position: Vector2F,
pub axis: Option<Axis>,
}
impl_internal_actions!(editor, [Scroll]);
pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::next_screen);
cx.add_action(Editor::scroll);
cx.add_action(Editor::scroll_cursor_top);
cx.add_action(Editor::scroll_cursor_center);
cx.add_action(Editor::scroll_cursor_bottom);
@@ -75,9 +64,14 @@ impl Editor {
Some(())
}
fn scroll(&mut self, action: &Scroll, cx: &mut ViewContext<Self>) {
self.scroll_manager.update_ongoing_scroll(action.axis);
self.set_scroll_position(action.scroll_position, cx);
pub fn scroll(
&mut self,
scroll_position: Vector2F,
axis: Option<Axis>,
cx: &mut ViewContext<Self>,
) {
self.scroll_manager.update_ongoing_scroll(axis);
self.set_scroll_position(scroll_position, cx);
}
fn scroll_cursor_top(editor: &mut Editor, _: &ScrollCursorTop, cx: &mut ViewContext<Editor>) {

View File

@@ -1,9 +1,9 @@
use std::cmp;
use gpui::{text_layout, ViewContext};
use gpui::ViewContext;
use language::Point;
use crate::{display_map::ToDisplayPoint, Editor, EditorMode};
use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles};
#[derive(PartialEq, Eq)]
pub enum Autoscroll {
@@ -172,7 +172,7 @@ impl Editor {
viewport_width: f32,
scroll_width: f32,
max_glyph_width: f32,
layouts: &[text_layout::Line],
layouts: &[LineWithInvisibles],
cx: &mut ViewContext<Self>,
) -> bool {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
@@ -194,10 +194,13 @@ impl Editor {
let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3);
target_left = target_left.min(
layouts[(head.row() - start_row) as usize]
.line
.x_for_index(start_column as usize),
);
target_right = target_right.max(
layouts[(head.row() - start_row) as usize].x_for_index(end_column as usize)
layouts[(head.row() - start_row) as usize]
.line
.x_for_index(end_column as usize)
+ max_glyph_width,
);
}

View File

@@ -34,11 +34,10 @@ impl<'a> EditorTestContext<'a> {
crate::init(cx);
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
cx.focus_self();
build_editor(MultiBuffer::build_simple("", cx), cx)
});
editor.update(cx, |_, cx| cx.focus_self());
(window_id, editor)
});
@@ -58,7 +57,7 @@ impl<'a> EditorTestContext<'a> {
pub fn editor<F, T>(&self, read: F) -> T
where
F: FnOnce(&Editor, &AppContext) -> T,
F: FnOnce(&Editor, &ViewContext<Editor>) -> T,
{
self.editor.read_with(self.cx, read)
}
@@ -167,7 +166,7 @@ impl<'a> EditorTestContext<'a> {
///
/// 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!(
let state_context = self.add_assertion_context(format!(
"Initial Editor State: \"{}\"",
marked_text.escape_debug().to_string()
));
@@ -178,7 +177,23 @@ impl<'a> EditorTestContext<'a> {
s.select_ranges(selection_ranges)
})
});
_state_context
state_context
}
/// Only change the editor's selections
pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
let state_context = self.add_assertion_context(format!(
"Initial Editor State: \"{}\"",
marked_text.escape_debug().to_string()
));
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
self.editor.update(self.cx, |editor, cx| {
assert_eq!(editor.text(cx), unmarked_text);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(selection_ranges)
})
});
state_context
}
/// Make an assertion about the editor's text and the ranges and directions
@@ -189,10 +204,11 @@ impl<'a> EditorTestContext<'a> {
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();
assert_eq!(
buffer_text, unmarked_text,
"Unmarked text doesn't match buffer text"
);
if buffer_text != unmarked_text {
panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}");
}
self.assert_selections(expected_selections, marked_text.to_string())
}
@@ -254,10 +270,10 @@ impl<'a> EditorTestContext<'a> {
panic!(
indoc! {"
{}Editor has unexpected selections.
Expected selections:
{}
Actual selections:
{}
"},

View File

@@ -11,25 +11,27 @@ path = "src/feedback.rs"
test-support = []
[dependencies]
anyhow = "1.0.38"
client = { path = "../client" }
editor = { path = "../editor" }
language = { path = "../language" }
log = "0.4"
futures = "0.3"
gpui = { path = "../gpui" }
human_bytes = "0.4.1"
isahc = "1.7"
lazy_static = "1.4.0"
postage = { workspace = true }
project = { path = "../project" }
search = { path = "../search" }
serde = { workspace = true }
serde_derive = { workspace = true }
settings = { path = "../settings" }
sysinfo = "0.27.1"
theme = { path = "../theme" }
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
urlencoding = "2.1.2"
util = { path = "../util" }
workspace = { path = "../workspace" }
log.workspace = true
futures.workspace = true
anyhow.workspace = true
smallvec.workspace = true
human_bytes = "0.4.1"
isahc = "1.7"
lazy_static.workspace = true
postage.workspace = true
serde.workspace = true
serde_derive.workspace = true
sysinfo = "0.27.1"
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
urlencoding = "2.1.2"

View File

@@ -1,15 +1,16 @@
use gpui::{
elements::*,
platform::{CursorStyle, MouseButton},
Entity, RenderContext, View, ViewContext,
Entity, View, ViewContext, WeakViewHandle,
};
use settings::Settings;
use workspace::{item::ItemHandle, StatusItemView};
use workspace::{item::ItemHandle, StatusItemView, Workspace};
use crate::feedback_editor::{FeedbackEditor, GiveFeedback};
pub struct DeployFeedbackButton {
active: bool,
workspace: WeakViewHandle<Workspace>,
}
impl Entity for DeployFeedbackButton {
@@ -17,8 +18,11 @@ impl Entity for DeployFeedbackButton {
}
impl DeployFeedbackButton {
pub fn new() -> Self {
DeployFeedbackButton { active: false }
pub fn new(workspace: &Workspace) -> Self {
DeployFeedbackButton {
active: false,
workspace: workspace.weak_handle(),
}
}
}
@@ -27,12 +31,12 @@ impl View for DeployFeedbackButton {
"DeployFeedbackButton"
}
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let active = self.active;
let theme = cx.global::<Settings>().theme.clone();
Stack::new()
.with_child(
MouseEventHandler::<Self>::new(0, cx, |state, _| {
MouseEventHandler::<Self, Self>::new(0, cx, |state, _| {
let style = &theme
.workspace
.status_bar
@@ -50,24 +54,25 @@ impl View for DeployFeedbackButton {
.with_height(style.icon_size)
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
.on_click(MouseButton::Left, move |_, this, cx| {
if !active {
cx.dispatch_action(GiveFeedback)
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace
.update(cx, |workspace, cx| FeedbackEditor::deploy(workspace, cx))
}
}
})
.with_tooltip::<Self, _>(
.with_tooltip::<Self>(
0,
"Send Feedback".into(),
Some(Box::new(GiveFeedback)),
theme.tooltip.clone(),
cx,
)
.boxed(),
),
)
.boxed()
.into_any()
}
}

View File

@@ -3,20 +3,10 @@ pub mod feedback_editor;
pub mod feedback_info_text;
pub mod submit_feedback_button;
use std::sync::Arc;
mod system_specs;
use gpui::{actions, impl_actions, platform::PromptLevel, AppContext, ClipboardItem, ViewContext};
use serde::Deserialize;
use gpui::{actions, platform::PromptLevel, AppContext, ClipboardItem, ViewContext};
use system_specs::SystemSpecs;
use workspace::{AppState, Workspace};
#[derive(Deserialize, Clone, PartialEq)]
pub struct OpenBrowser {
pub url: Arc<str>,
}
impl_actions!(zed, [OpenBrowser]);
use workspace::Workspace;
actions!(
zed,
@@ -28,29 +18,20 @@ actions!(
]
);
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
let system_specs = SystemSpecs::new(&cx);
let system_specs_text = system_specs.to_string();
feedback_editor::init(system_specs, app_state, cx);
cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
let url = format!(
"https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
urlencoding::encode(&system_specs_text)
);
pub fn init(cx: &mut AppContext) {
feedback_editor::init(cx);
cx.add_action(
move |_: &mut Workspace,
_: &CopySystemSpecsIntoClipboard,
cx: &mut ViewContext<Workspace>| {
let specs = SystemSpecs::new(&cx).to_string();
cx.prompt(
PromptLevel::Info,
&format!("Copied into clipboard:\n\n{system_specs_text}"),
&format!("Copied into clipboard:\n\n{specs}"),
&["OK"],
);
let item = ClipboardItem::new(system_specs_text.clone());
let item = ClipboardItem::new(specs.clone());
cx.write_to_clipboard(item);
},
);
@@ -58,24 +39,24 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
cx.add_action(
|_: &mut Workspace, _: &RequestFeature, cx: &mut ViewContext<Workspace>| {
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(),
});
cx.platform().open_url(url);
},
);
cx.add_action(
move |_: &mut Workspace, _: &FileBugReport, cx: &mut ViewContext<Workspace>| {
cx.dispatch_action(OpenBrowser {
url: url.clone().into(),
});
let url = format!(
"https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
urlencoding::encode(&SystemSpecs::new(&cx).to_string())
);
cx.platform().open_url(&url);
},
);
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() });
},
);
cx.add_global_action(open_zed_community_repo);
}
pub fn open_zed_community_repo(_: &OpenZedCommunityRepo, cx: &mut AppContext) {
let url = "https://github.com/zed-industries/community";
cx.platform().open_url(&url);
}

View File

@@ -1,9 +1,4 @@
use std::{
any::TypeId,
ops::{Range, RangeInclusive},
sync::Arc,
};
use crate::system_specs::SystemSpecs;
use anyhow::bail;
use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use editor::{Anchor, Editor};
@@ -12,52 +7,47 @@ use gpui::{
actions,
elements::{ChildView, Flex, Label, ParentElement, Svg},
platform::PromptLevel,
serde_json, AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, RenderContext,
Task, View, ViewContext, ViewHandle,
serde_json, AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Task, View,
ViewContext, ViewHandle,
};
use isahc::Request;
use language::Buffer;
use postage::prelude::Stream;
use project::Project;
use serde::Serialize;
use smallvec::SmallVec;
use std::{
any::TypeId,
borrow::Cow,
ops::{Range, RangeInclusive},
sync::Arc,
};
use util::ResultExt;
use workspace::{
item::{Item, ItemHandle},
item::{Item, ItemEvent, ItemHandle},
searchable::{SearchableItem, SearchableItemHandle},
AppState, Workspace,
Workspace,
};
use crate::{submit_feedback_button::SubmitFeedbackButton, system_specs::SystemSpecs};
const FEEDBACK_CHAR_LIMIT: RangeInclusive<usize> = 10..=5000;
const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
"Feedback failed to submit, see error log for details.";
actions!(feedback, [GiveFeedback, SubmitFeedback]);
pub fn init(system_specs: SystemSpecs, app_state: Arc<AppState>, cx: &mut AppContext) {
pub fn init(cx: &mut AppContext) {
cx.add_action({
move |workspace: &mut Workspace, _: &GiveFeedback, cx: &mut ViewContext<Workspace>| {
FeedbackEditor::deploy(system_specs.clone(), workspace, app_state.clone(), cx);
FeedbackEditor::deploy(workspace, cx);
}
});
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)]
struct FeedbackRequestBody<'a> {
feedback_text: &'a str,
metrics_id: Option<Arc<str>>,
installation_id: Option<Arc<str>>,
system_specs: SystemSpecs,
is_staff: bool,
token: &'a str,
@@ -93,7 +83,7 @@ impl FeedbackEditor {
}
}
fn handle_save(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
pub fn submit(&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();
@@ -123,34 +113,28 @@ impl FeedbackEditor {
&["Yes, Submit!", "No"],
);
let this = cx.handle();
let client = cx.global::<Arc<Client>>().clone();
let specs = self.system_specs.clone();
cx.spawn(|_, mut cx| async move {
cx.spawn(|this, mut cx| async move {
let answer = answer.recv().await;
if answer == Some(0) {
match FeedbackEditor::submit_feedback(&feedback_text, client, specs).await {
Ok(_) => {
cx.update(|cx| {
this.update(cx, |_, cx| {
cx.dispatch_action(workspace::CloseActiveItem);
})
});
this.update(&mut cx, |_, cx| cx.emit(editor::Event::Closed))
.log_err();
}
Err(error) => {
log::error!("{}", error);
cx.update(|cx| {
this.update(cx, |_, cx| {
cx.prompt(
PromptLevel::Critical,
FEEDBACK_SUBMISSION_ERROR_TEXT,
&["OK"],
);
})
});
this.update(&mut cx, |_, cx| {
cx.prompt(
PromptLevel::Critical,
FEEDBACK_SUBMISSION_ERROR_TEXT,
&["OK"],
);
})
.log_err();
}
}
}
@@ -167,13 +151,16 @@ impl FeedbackEditor {
) -> anyhow::Result<()> {
let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL);
let metrics_id = zed_client.metrics_id();
let is_staff = zed_client.is_staff();
let telemetry = zed_client.telemetry();
let metrics_id = telemetry.metrics_id();
let installation_id = telemetry.installation_id();
let is_staff = telemetry.is_staff();
let http_client = zed_client.http_client();
let request = FeedbackRequestBody {
feedback_text: &feedback_text,
metrics_id,
installation_id,
system_specs,
is_staff: is_staff.unwrap_or(false),
token: ZED_SECRET_CLIENT_TOKEN,
@@ -200,30 +187,29 @@ impl FeedbackEditor {
}
impl FeedbackEditor {
pub fn deploy(
system_specs: SystemSpecs,
_: &mut Workspace,
app_state: Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) {
let markdown = app_state.languages.language_for_name("Markdown");
pub fn deploy(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
let markdown = workspace
.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| {
workspace.with_local_workspace(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 system_specs = SystemSpecs::new(cx);
let feedback_editor = cx
.add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx));
workspace.add_item(Box::new(feedback_editor), cx);
})
})
.await;
})?
.await
})
.detach();
.detach_and_log_err(cx);
}
}
@@ -232,8 +218,8 @@ impl View for FeedbackEditor {
"FeedbackEditor"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
ChildView::new(&self.editor, cx).boxed()
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
ChildView::new(&self.editor, cx).into_any()
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
@@ -248,7 +234,16 @@ impl Entity for FeedbackEditor {
}
impl Item for FeedbackEditor {
fn tab_content(&self, _: Option<usize>, style: &theme::Tab, _: &AppContext) -> ElementBox {
fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
Some("Send Feedback".into())
}
fn tab_content<T: View>(
&self,
_: Option<usize>,
style: &theme::Tab,
_: &AppContext,
) -> AnyElement<T> {
Flex::row()
.with_child(
Svg::new("icons/feedback_16.svg")
@@ -257,16 +252,14 @@ impl Item for FeedbackEditor {
.with_width(style.type_icon_width)
.aligned()
.contained()
.with_margin_right(style.spacing)
.boxed(),
.with_margin_right(style.spacing),
)
.with_child(
Label::new("Send Feedback", style.label.clone())
.aligned()
.contained()
.boxed(),
.contained(),
)
.boxed()
.into_any()
}
fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
@@ -286,7 +279,7 @@ impl Item for FeedbackEditor {
_: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
self.handle_save(cx)
self.submit(cx)
}
fn save_as(
@@ -295,7 +288,7 @@ impl Item for FeedbackEditor {
_: std::path::PathBuf,
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
self.handle_save(cx)
self.submit(cx)
}
fn reload(
@@ -348,6 +341,10 @@ impl Item for FeedbackEditor {
None
}
}
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
Editor::to_item_events(event)
}
}
impl SearchableItem for FeedbackEditor {

View File

@@ -1,12 +1,12 @@
use gpui::{
elements::{Flex, Label, MouseEventHandler, ParentElement, Text},
platform::{CursorStyle, MouseButton},
Element, ElementBox, Entity, RenderContext, View, ViewContext, ViewHandle,
AnyElement, Element, Entity, View, ViewContext, ViewHandle,
};
use settings::Settings;
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
use crate::{feedback_editor::FeedbackEditor, OpenZedCommunityRepo};
use crate::{feedback_editor::FeedbackEditor, open_zed_community_repo, OpenZedCommunityRepo};
pub struct FeedbackInfoText {
active_item: Option<ViewHandle<FeedbackEditor>>,
@@ -29,7 +29,7 @@ impl View for FeedbackInfoText {
"FeedbackInfoText"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = cx.global::<Settings>().theme.clone();
Flex::row()
@@ -39,11 +39,10 @@ impl View for FeedbackInfoText {
theme.feedback.info_text_default.text.clone(),
)
.with_soft_wrap(false)
.aligned()
.boxed(),
.aligned(),
)
.with_child(
MouseEventHandler::<OpenZedCommunityRepo>::new(0, cx, |state, _| {
MouseEventHandler::<OpenZedCommunityRepo, Self>::new(0, cx, |state, _| {
let contained_text = if state.hovered() {
&theme.feedback.link_text_hover
} else {
@@ -55,24 +54,21 @@ impl View for FeedbackInfoText {
.aligned()
.left()
.clipped()
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(OpenZedCommunityRepo)
})
.boxed(),
.on_click(MouseButton::Left, |_, _, cx| {
open_zed_community_repo(&Default::default(), cx)
}),
)
.with_child(
Text::new(" on GitHub.", theme.feedback.info_text_default.text.clone())
.with_soft_wrap(false)
.aligned()
.boxed(),
.aligned(),
)
.aligned()
.left()
.clipped()
.boxed()
.into_any()
}
}

View File

@@ -1,12 +1,16 @@
use crate::feedback_editor::{FeedbackEditor, SubmitFeedback};
use anyhow::Result;
use gpui::{
elements::{Label, MouseEventHandler},
platform::{CursorStyle, MouseButton},
Element, ElementBox, Entity, RenderContext, View, ViewContext, ViewHandle,
AnyElement, AppContext, Element, Entity, Task, View, ViewContext, ViewHandle,
};
use settings::Settings;
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
use crate::feedback_editor::{FeedbackEditor, SubmitFeedback};
pub fn init(cx: &mut AppContext) {
cx.add_async_action(SubmitFeedbackButton::submit);
}
pub struct SubmitFeedbackButton {
pub(crate) active_item: Option<ViewHandle<FeedbackEditor>>,
@@ -18,6 +22,18 @@ impl SubmitFeedbackButton {
active_item: Default::default(),
}
}
pub fn submit(
&mut self,
_: &SubmitFeedback,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
if let Some(active_item) = self.active_item.as_ref() {
Some(active_item.update(cx, |feedback_editor, cx| feedback_editor.submit(cx)))
} else {
None
}
}
}
impl Entity for SubmitFeedbackButton {
@@ -29,31 +45,30 @@ impl View for SubmitFeedbackButton {
"SubmitFeedbackButton"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = cx.global::<Settings>().theme.clone();
enum SubmitFeedbackButton {}
MouseEventHandler::<SubmitFeedbackButton>::new(0, cx, |state, _| {
MouseEventHandler::<SubmitFeedbackButton, Self>::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)
.on_click(MouseButton::Left, |_, this, cx| {
this.submit(&Default::default(), cx);
})
.aligned()
.contained()
.with_margin_left(theme.feedback.button_margin)
.with_tooltip::<Self, _>(
.with_tooltip::<Self>(
0,
"cmd-s".into(),
Some(Box::new(SubmitFeedback)),
theme.tooltip.clone(),
cx,
)
.boxed()
.into_any()
}
}

View File

@@ -19,11 +19,11 @@ settings = { path = "../settings" }
util = { path = "../util" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }
postage = { workspace = true }
postage.workspace = true
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
serde_json = { workspace = true }
serde_json.workspace = true
workspace = { path = "../workspace", features = ["test-support"] }
ctor = "0.1"
env_logger = "0.9"
ctor.workspace = true
env_logger.workspace = true

View File

@@ -1,7 +1,6 @@
use fuzzy::PathMatch;
use gpui::{
actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState,
RenderContext, Task, View, ViewContext, ViewHandle,
actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle,
};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
@@ -13,12 +12,14 @@ use std::{
Arc,
},
};
use util::post_inc;
use util::{post_inc, ResultExt};
use workspace::Workspace;
pub struct FileFinder {
pub type FileFinder = Picker<FileFinderDelegate>;
pub struct FileFinderDelegate {
workspace: WeakViewHandle<Workspace>,
project: ModelHandle<Project>,
picker: ViewHandle<Picker<Self>>,
search_count: usize,
latest_search_id: usize,
latest_search_did_cancel: bool,
@@ -32,8 +33,26 @@ pub struct FileFinder {
actions!(file_finder, [Toggle]);
pub fn init(cx: &mut AppContext) {
cx.add_action(FileFinder::toggle);
Picker::<FileFinder>::init(cx);
cx.add_action(toggle_file_finder);
FileFinder::init(cx);
}
fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |workspace, cx| {
let relative_to = workspace
.active_item(cx)
.and_then(|item| item.project_path(cx))
.map(|project_path| project_path.path.clone());
let project = workspace.project().clone();
let workspace = cx.handle().downgrade();
let finder = cx.add_view(|cx| {
Picker::new(
FileFinderDelegate::new(workspace, project, relative_to, cx),
cx,
)
});
finder
});
}
pub enum Event {
@@ -41,27 +60,7 @@ pub enum Event {
Dismissed,
}
impl Entity for FileFinder {
type Event = Event;
}
impl View for FileFinder {
fn ui_name() -> &'static str {
"FileFinder"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
ChildView::new(&self.picker, cx).boxed()
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.picker);
}
}
}
impl FileFinder {
impl FileFinderDelegate {
fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) {
let path = &path_match.path;
let path_string = path.to_string_lossy();
@@ -88,48 +87,19 @@ impl FileFinder {
(file_name, file_name_positions, full_path, path_positions)
}
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |workspace, cx| {
let project = workspace.project().clone();
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
});
}
fn on_event(
workspace: &mut Workspace,
_: ViewHandle<FileFinder>,
event: &Event,
cx: &mut ViewContext<Workspace>,
) {
match event {
Event::Selected(project_path) => {
workspace
.open_path(project_path.clone(), None, true, cx)
.detach_and_log_err(cx);
workspace.dismiss_modal(cx);
}
Event::Dismissed => {
workspace.dismiss_modal(cx);
}
}
}
pub fn new(
workspace: WeakViewHandle<Workspace>,
project: ModelHandle<Project>,
relative_to: Option<Arc<Path>>,
cx: &mut ViewContext<Self>,
cx: &mut ViewContext<FileFinder>,
) -> Self {
let handle = cx.weak_handle();
cx.observe(&project, Self::project_updated).detach();
cx.observe(&project, |picker, _, cx| {
picker.update_matches(picker.query(cx), cx);
})
.detach();
Self {
workspace,
project,
picker: cx.add_view(|cx| Picker::new("Search project files...", handle, cx)),
search_count: 0,
latest_search_id: 0,
latest_search_did_cancel: false,
@@ -141,12 +111,7 @@ impl FileFinder {
}
}
fn project_updated(&mut self, _: ModelHandle<Project>, cx: &mut ViewContext<Self>) {
self.spawn_search(self.picker.read(cx).query(cx), cx)
.detach();
}
fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
fn spawn_search(&mut self, query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
let relative_to = self.relative_to.clone();
let worktrees = self
.project
@@ -172,7 +137,7 @@ impl FileFinder {
self.cancel_flag.store(true, atomic::Ordering::Relaxed);
self.cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag = self.cancel_flag.clone();
cx.spawn(|this, mut cx| async move {
cx.spawn(|picker, mut cx| async move {
let matches = fuzzy::match_path_sets(
candidate_sets.as_slice(),
&query,
@@ -184,9 +149,13 @@ impl FileFinder {
)
.await;
let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
this.update(&mut cx, |this, cx| {
this.set_matches(search_id, did_cancel, query, matches, cx)
});
picker
.update(&mut cx, |picker, cx| {
picker
.delegate_mut()
.set_matches(search_id, did_cancel, query, matches, cx)
})
.log_err();
})
}
@@ -196,7 +165,7 @@ impl FileFinder {
did_cancel: bool,
query: String,
matches: Vec<PathMatch>,
cx: &mut ViewContext<Self>,
cx: &mut ViewContext<FileFinder>,
) {
if search_id >= self.latest_search_id {
self.latest_search_id = search_id;
@@ -208,12 +177,15 @@ impl FileFinder {
self.latest_search_query = query;
self.latest_search_did_cancel = did_cancel;
cx.notify();
self.picker.update(cx, |_, cx| cx.notify());
}
}
}
impl PickerDelegate for FileFinder {
impl PickerDelegate for FileFinderDelegate {
fn placeholder_text(&self) -> Arc<str> {
"Search project files...".into()
}
fn match_count(&self) -> usize {
self.matches.len()
}
@@ -231,13 +203,13 @@ impl PickerDelegate for FileFinder {
0
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<FileFinder>) {
let mat = &self.matches[ix];
self.selected = Some((mat.worktree_id, mat.path.clone()));
cx.notify();
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
fn update_matches(&mut self, query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
if query.is_empty() {
self.latest_search_id = post_inc(&mut self.search_count);
self.matches.clear();
@@ -248,18 +220,25 @@ impl PickerDelegate for FileFinder {
}
}
fn confirm(&mut self, cx: &mut ViewContext<Self>) {
fn confirm(&mut self, cx: &mut ViewContext<FileFinder>) {
if let Some(m) = self.matches.get(self.selected_index()) {
cx.emit(Event::Selected(ProjectPath {
worktree_id: WorktreeId::from_usize(m.worktree_id),
path: m.path.clone(),
}));
if let Some(workspace) = self.workspace.upgrade(cx) {
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(m.worktree_id),
path: m.path.clone(),
};
workspace.update(cx, |workspace, cx| {
workspace
.open_path(project_path.clone(), None, true, cx)
.detach_and_log_err(cx);
workspace.dismiss_modal(cx);
})
}
}
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed);
}
fn dismissed(&mut self, _: &mut ViewContext<FileFinder>) {}
fn render_match(
&self,
@@ -267,7 +246,7 @@ impl PickerDelegate for FileFinder {
mouse_state: &mut MouseState,
selected: bool,
cx: &AppContext,
) -> ElementBox {
) -> AnyElement<Picker<Self>> {
let path_match = &self.matches[ix];
let settings = cx.global::<Settings>();
let style = settings.theme.picker.item.style_for(mouse_state, selected);
@@ -275,19 +254,15 @@ impl PickerDelegate for FileFinder {
self.labels_for_match(path_match);
Flex::column()
.with_child(
Label::new(file_name, style.label.clone())
.with_highlights(file_name_positions)
.boxed(),
Label::new(file_name, style.label.clone()).with_highlights(file_name_positions),
)
.with_child(
Label::new(full_path, style.label.clone())
.with_highlights(full_path_positions)
.boxed(),
Label::new(full_path, style.label.clone()).with_highlights(full_path_positions),
)
.flex(1., false)
.contained()
.with_style(style.container)
.named("match")
.into_any_named("match")
}
}
@@ -335,11 +310,11 @@ mod tests {
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
finder
.update(cx, |finder, cx| {
finder.update_matches("bna".to_string(), cx)
finder.delegate_mut().update_matches("bna".to_string(), cx)
})
.await;
finder.read_with(cx, |finder, _| {
assert_eq!(finder.matches.len(), 2);
assert_eq!(finder.delegate().matches.len(), 2);
});
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
@@ -384,23 +359,33 @@ mod tests {
let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
let (_, finder) = cx.add_window(|cx| {
Picker::new(
FileFinderDelegate::new(
workspace.downgrade(),
workspace.read(cx).project().clone(),
None,
cx,
),
cx,
)
});
let query = "hi".to_string();
finder
.update(cx, |f, cx| f.spawn_search(query.clone(), cx))
.update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx))
.await;
finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 5));
finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 5));
finder.update(cx, |finder, cx| {
let matches = finder.matches.clone();
let delegate = finder.delegate_mut();
let matches = delegate.matches.clone();
// Simulate a search being cancelled after the time limit,
// returning only a subset of the matches that would have been found.
drop(finder.spawn_search(query.clone(), cx));
finder.set_matches(
finder.latest_search_id,
drop(delegate.spawn_search(query.clone(), cx));
delegate.set_matches(
delegate.latest_search_id,
true, // did-cancel
query.clone(),
vec![matches[1].clone(), matches[3].clone()],
@@ -408,16 +393,16 @@ mod tests {
);
// Simulate another cancellation.
drop(finder.spawn_search(query.clone(), cx));
finder.set_matches(
finder.latest_search_id,
drop(delegate.spawn_search(query.clone(), cx));
delegate.set_matches(
delegate.latest_search_id,
true, // did-cancel
query.clone(),
vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
cx,
);
assert_eq!(finder.matches, matches[0..4])
assert_eq!(delegate.matches, matches[0..4])
});
}
@@ -458,12 +443,21 @@ mod tests {
)
.await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
let (_, finder) = cx.add_window(|cx| {
Picker::new(
FileFinderDelegate::new(
workspace.downgrade(),
workspace.read(cx).project().clone(),
None,
cx,
),
cx,
)
});
finder
.update(cx, |f, cx| f.spawn_search("hi".into(), cx))
.update(cx, |f, cx| f.delegate_mut().spawn_search("hi".into(), cx))
.await;
finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 7));
finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7));
}
#[gpui::test]
@@ -482,20 +476,30 @@ mod tests {
)
.await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
let (_, finder) = cx.add_window(|cx| {
Picker::new(
FileFinderDelegate::new(
workspace.downgrade(),
workspace.read(cx).project().clone(),
None,
cx,
),
cx,
)
});
// Even though there is only one worktree, that worktree's filename
// is included in the matching, because the worktree is a single file.
finder
.update(cx, |f, cx| f.spawn_search("thf".into(), cx))
.update(cx, |f, cx| f.delegate_mut().spawn_search("thf".into(), cx))
.await;
cx.read(|cx| {
let finder = finder.read(cx);
assert_eq!(finder.matches.len(), 1);
let delegate = finder.delegate();
assert_eq!(delegate.matches.len(), 1);
let (file_name, file_name_positions, full_path, full_path_positions) =
finder.labels_for_match(&finder.matches[0]);
delegate.labels_for_match(&delegate.matches[0]);
assert_eq!(file_name, "the-file");
assert_eq!(file_name_positions, &[0, 1, 4]);
assert_eq!(full_path, "the-file");
@@ -505,9 +509,9 @@ mod tests {
// Since the worktree root is a file, searching for its name followed by a slash does
// not match anything.
finder
.update(cx, |f, cx| f.spawn_search("thf/".into(), cx))
.update(cx, |f, cx| f.delegate_mut().spawn_search("thf/".into(), cx))
.await;
finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 0));
finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0));
}
#[gpui::test]
@@ -535,22 +539,32 @@ mod tests {
.await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
let (_, finder) = cx.add_window(|cx| {
Picker::new(
FileFinderDelegate::new(
workspace.downgrade(),
workspace.read(cx).project().clone(),
None,
cx,
),
cx,
)
});
// Run a search that matches two files with the same relative path.
finder
.update(cx, |f, cx| f.spawn_search("a.t".into(), cx))
.update(cx, |f, cx| f.delegate_mut().spawn_search("a.t".into(), cx))
.await;
// Can switch between different matches with the same relative path.
finder.update(cx, |f, cx| {
assert_eq!(f.matches.len(), 2);
assert_eq!(f.selected_index(), 0);
f.set_selected_index(1, cx);
assert_eq!(f.selected_index(), 1);
f.set_selected_index(0, cx);
assert_eq!(f.selected_index(), 0);
finder.update(cx, |finder, cx| {
let delegate = finder.delegate_mut();
assert_eq!(delegate.matches.len(), 2);
assert_eq!(delegate.selected_index(), 0);
delegate.set_selected_index(1, cx);
assert_eq!(delegate.selected_index(), 1);
delegate.set_selected_index(0, cx);
assert_eq!(delegate.selected_index(), 0);
});
}
@@ -581,16 +595,28 @@ mod tests {
// 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));
let (_, finder) = cx.add_window(|cx| {
Picker::new(
FileFinderDelegate::new(
workspace.downgrade(),
workspace.read(cx).project().clone(),
b_path,
cx,
),
cx,
)
});
finder
.update(cx, |f, cx| f.spawn_search("a.txt".into(), cx))
.update(cx, |f, cx| {
f.delegate_mut().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"));
let delegate = f.delegate();
assert_eq!(delegate.matches[0].path.as_ref(), Path::new("dir2/a.txt"));
assert_eq!(delegate.matches[1].path.as_ref(), Path::new("dir1/a.txt"));
});
}
@@ -613,14 +639,23 @@ mod tests {
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
let (_, finder) = cx.add_window(|cx| {
Picker::new(
FileFinderDelegate::new(
workspace.downgrade(),
workspace.read(cx).project().clone(),
None,
cx,
),
cx,
)
});
finder
.update(cx, |f, cx| f.spawn_search("dir".into(), cx))
.update(cx, |f, cx| f.delegate_mut().spawn_search("dir".into(), cx))
.await;
cx.read(|cx| {
let finder = finder.read(cx);
assert_eq!(finder.matches.len(), 0);
assert_eq!(finder.delegate().matches.len(), 0);
});
}
}

View File

@@ -13,20 +13,20 @@ gpui = { path = "../gpui" }
lsp = { path = "../lsp" }
rope = { path = "../rope" }
util = { path = "../util" }
anyhow = "1.0.57"
async-trait = "0.1"
futures = "0.3"
anyhow.workspace = true
async-trait.workspace = true
futures.workspace = true
tempfile = "3"
fsevent = { path = "../fsevent" }
lazy_static = "1.4.0"
parking_lot = "0.11.1"
smol = "1.2.5"
regex = "1.5"
lazy_static.workspace = true
parking_lot.workspace = true
smol.workspace = true
regex.workspace = true
git2 = { version = "0.15", default-features = false }
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
log.workspace = true
libc = "0.2"
[features]

View File

@@ -523,31 +523,7 @@ impl FakeFs {
}
pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
let mut state = self.state.lock();
let path = path.as_ref();
let inode = state.next_inode;
let mtime = state.next_mtime;
state.next_inode += 1;
state.next_mtime += Duration::from_nanos(1);
let file = Arc::new(Mutex::new(FakeFsEntry::File {
inode,
mtime,
content,
}));
state
.write_path(path, move |entry| {
match entry {
btree_map::Entry::Vacant(e) => {
e.insert(file);
}
btree_map::Entry::Occupied(mut e) => {
*e.get_mut() = file;
}
}
Ok(())
})
.unwrap();
state.emit_event(&[path]);
self.write_file_internal(path, content).unwrap()
}
pub async fn insert_symlink(&self, path: impl AsRef<Path>, target: PathBuf) {
@@ -569,6 +545,33 @@ impl FakeFs {
state.emit_event(&[path]);
}
fn write_file_internal(&self, path: impl AsRef<Path>, content: String) -> Result<()> {
let mut state = self.state.lock();
let path = path.as_ref();
let inode = state.next_inode;
let mtime = state.next_mtime;
state.next_inode += 1;
state.next_mtime += Duration::from_nanos(1);
let file = Arc::new(Mutex::new(FakeFsEntry::File {
inode,
mtime,
content,
}));
state.write_path(path, move |entry| {
match entry {
btree_map::Entry::Vacant(e) => {
e.insert(file);
}
btree_map::Entry::Occupied(mut e) => {
*e.get_mut() = file;
}
}
Ok(())
})?;
state.emit_event(&[path]);
Ok(())
}
pub async fn pause_events(&self) {
self.state.lock().events_paused = true;
}
@@ -616,7 +619,10 @@ impl FakeFs {
.boxed()
}
pub async fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
pub fn with_git_state<F>(&self, dot_git: &Path, f: F)
where
F: FnOnce(&mut FakeGitRepositoryState),
{
let mut state = self.state.lock();
let entry = state.read_path(dot_git).unwrap();
let mut entry = entry.lock();
@@ -625,12 +631,7 @@ impl FakeFs {
let repo_state = git_repo_state.get_or_insert_with(Default::default);
let mut repo_state = repo_state.lock();
repo_state.index_contents.clear();
repo_state.index_contents.extend(
head_state
.iter()
.map(|(path, content)| (path.to_path_buf(), content.clone())),
);
f(&mut repo_state);
state.emit_event([dot_git]);
} else {
@@ -638,6 +639,21 @@ impl FakeFs {
}
}
pub async fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
self.with_git_state(dot_git, |state| state.branch_name = branch.map(Into::into))
}
pub async fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
self.with_git_state(dot_git, |state| {
state.index_contents.clear();
state.index_contents.extend(
head_state
.iter()
.map(|(path, content)| (path.to_path_buf(), content.clone())),
);
});
}
pub fn paths(&self) -> Vec<PathBuf> {
let mut result = Vec::new();
let mut queue = collections::VecDeque::new();
@@ -952,7 +968,7 @@ impl Fs for FakeFs {
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
self.simulate_random_delay().await;
let path = normalize_path(path.as_path());
self.insert_file(path, data.to_string()).await;
self.write_file_internal(path, data.to_string())?;
Ok(())
}
@@ -961,7 +977,7 @@ impl Fs for FakeFs {
self.simulate_random_delay().await;
let path = normalize_path(path);
let content = chunks(text, line_ending).collect();
self.insert_file(path, content).await;
self.write_file_internal(path, content)?;
Ok(())
}

View File

@@ -5,6 +5,7 @@ use std::{
path::{Component, Path, PathBuf},
sync::Arc,
};
use util::ResultExt;
pub use git2::Repository as LibGitRepository;
@@ -13,6 +14,14 @@ pub trait GitRepository: Send {
fn reload_index(&self);
fn load_index_text(&self, relative_file_path: &Path) -> Option<String>;
fn branch_name(&self) -> Option<String>;
}
impl std::fmt::Debug for dyn GitRepository {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("dyn GitRepository<...>").finish()
}
}
#[async_trait::async_trait]
@@ -46,6 +55,12 @@ impl GitRepository for LibGitRepository {
}
None
}
fn branch_name(&self) -> Option<String> {
let head = self.head().log_err()?;
let branch = String::from_utf8_lossy(head.shorthand_bytes());
Some(branch.to_string())
}
}
#[derive(Debug, Clone, Default)]
@@ -56,6 +71,7 @@ pub struct FakeGitRepository {
#[derive(Debug, Clone, Default)]
pub struct FakeGitRepositoryState {
pub index_contents: HashMap<PathBuf, String>,
pub branch_name: Option<String>,
}
impl FakeGitRepository {
@@ -72,6 +88,11 @@ impl GitRepository for FakeGitRepository {
let state = self.state.lock();
state.index_contents.get(path).cloned()
}
fn branch_name(&self) -> Option<String> {
let state = self.state.lock();
state.branch_name.clone()
}
}
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {

View File

@@ -12,10 +12,10 @@ doctest = false
[dependencies]
bitflags = "1"
fsevent-sys = "3.0.2"
parking_lot = "0.11.1"
parking_lot.workspace = true
[dev-dependencies]
tempdir = "0.3.7"
tempdir.workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-apple-darwin"]

View File

@@ -8,22 +8,22 @@ publish = false
path = "src/git.rs"
[dependencies]
anyhow = "1.0.38"
anyhow.workspace = true
clock = { path = "../clock" }
lazy_static = "1.4.0"
lazy_static.workspace = true
sum_tree = { path = "../sum_tree" }
text = { path = "../text" }
collections = { path = "../collections" }
util = { path = "../util" }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
smol = "1.2"
parking_lot = "0.11.1"
async-trait = "0.1"
futures = "0.3"
log.workspace = true
smol.workspace = true
parking_lot.workspace = true
async-trait.workspace = true
futures.workspace = true
git2 = { version = "0.15", default-features = false }
[dev-dependencies]
unindent = "0.1.7"
unindent.workspace = true
[features]
test-support = []

View File

@@ -15,4 +15,4 @@ menu = { path = "../menu" }
settings = { path = "../settings" }
text = { path = "../text" }
workspace = { path = "../workspace" }
postage = { workspace = true }
postage.workspace = true

View File

@@ -3,12 +3,12 @@ use std::sync::Arc;
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
use gpui::{
actions, elements::*, geometry::vector::Vector2F, AnyViewHandle, AppContext, Axis, Entity,
RenderContext, View, ViewContext, ViewHandle,
View, ViewContext, ViewHandle,
};
use menu::{Cancel, Confirm};
use settings::Settings;
use text::{Bias, Point};
use workspace::Workspace;
use workspace::{Modal, Workspace};
actions!(go_to_line, [Toggle]);
@@ -65,11 +65,7 @@ impl GoToLine {
.active_item(cx)
.and_then(|active_item| active_item.downcast::<Editor>())
{
workspace.toggle_modal(cx, |_, cx| {
let view = cx.add_view(|cx| GoToLine::new(editor, cx));
cx.subscribe(&view, Self::on_event).detach();
view
});
workspace.toggle_modal(cx, |_, cx| cx.add_view(|cx| GoToLine::new(editor, cx)));
}
}
@@ -91,17 +87,6 @@ impl GoToLine {
cx.emit(Event::Dismissed);
}
fn on_event(
workspace: &mut Workspace,
_: ViewHandle<Self>,
event: &Event,
cx: &mut ViewContext<Workspace>,
) {
match event {
Event::Dismissed => workspace.dismiss_modal(cx),
}
}
fn on_line_editor_event(
&mut self,
_: ViewHandle<Editor>,
@@ -142,12 +127,14 @@ impl Entity for GoToLine {
fn release(&mut self, cx: &mut AppContext) {
let scroll_position = self.prev_scroll_position.take();
self.active_editor.update(cx, |editor, cx| {
editor.highlight_rows(None);
if let Some(scroll_position) = scroll_position {
editor.set_scroll_position(scroll_position, cx);
}
})
cx.update_window(self.active_editor.window_id(), |cx| {
self.active_editor.update(cx, |editor, cx| {
editor.highlight_rows(None);
if let Some(scroll_position) = scroll_position {
editor.set_scroll_position(scroll_position, cx);
}
})
});
}
}
@@ -156,7 +143,7 @@ impl View for GoToLine {
"GoToLine"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &cx.global::<Settings>().theme.picker;
let label = format!(
@@ -166,29 +153,31 @@ impl View for GoToLine {
self.max_point.row + 1
);
ConstrainedBox::new(
Container::new(
Flex::new(Axis::Vertical)
.with_child(
Container::new(ChildView::new(&self.line_editor, cx).boxed())
.with_style(theme.input_editor.container)
.boxed(),
)
.with_child(
Container::new(Label::new(label, theme.no_matches.label.clone()).boxed())
.with_style(theme.no_matches.container)
.boxed(),
)
.boxed(),
Flex::new(Axis::Vertical)
.with_child(
ChildView::new(&self.line_editor, cx)
.contained()
.with_style(theme.input_editor.container),
)
.with_child(
Label::new(label, theme.no_matches.label.clone())
.contained()
.with_style(theme.no_matches.container),
)
.contained()
.with_style(theme.container)
.boxed(),
)
.with_max_width(500.0)
.named("go to line")
.constrained()
.with_max_width(500.0)
.into_any_named("go to line")
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
cx.focus(&self.line_editor);
}
}
impl Modal for GoToLine {
fn dismiss_on_event(event: &Self::Event) -> bool {
matches!(event, Event::Dismissed)
}
}

View File

@@ -21,32 +21,32 @@ sum_tree = { path = "../sum_tree" }
sqlez = { path = "../sqlez" }
async-task = "4.0.3"
backtrace = { version = "0.3", optional = true }
ctor = "0.1"
ctor.workspace = true
dhat = { version = "0.3", optional = true }
env_logger = { version = "0.9", optional = true }
etagere = "0.2"
futures = "0.3"
futures.workspace = true
image = "0.23"
itertools = "0.10"
lazy_static = "1.4.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
lazy_static.workspace = true
log.workspace = true
num_cpus = "1.13"
ordered-float = "2.1.1"
ordered-float.workspace = true
parking = "2.0.0"
parking_lot = "0.11.1"
parking_lot.workspace = true
pathfinder_color = "0.5"
pathfinder_geometry = "0.5"
postage = { workspace = true }
rand = "0.8.3"
postage.workspace = true
rand.workspace = true
resvg = "0.14"
schemars = "0.8"
seahash = "4.1"
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
time = { version = "0.3", features = ["serde", "serde-well-known"] }
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
smallvec.workspace = true
smol.workspace = true
time.workspace = true
tiny-skia = "0.5"
usvg = "0.14"
uuid = { version = "1.1.2", features = ["v4"] }
@@ -60,13 +60,13 @@ cc = "1.0.67"
backtrace = "0.3"
collections = { path = "../collections", features = ["test-support"] }
dhat = "0.3"
env_logger = "0.9"
env_logger.workspace = true
png = "0.16"
simplelog = "0.9"
[target.'cfg(target_os = "macos")'.dependencies]
media = { path = "../media" }
anyhow = "1"
anyhow.workspace = true
block = "0.1"
cocoa = "0.24"
core-foundation = { version = "0.9.3", features = ["with-uuid"] }
@@ -74,6 +74,6 @@ core-graphics = "0.22.3"
core-text = "19.2"
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "8eaf7a918eafa28b0a37dc759e2e0e7683fa24f1" }
foreign-types = "0.3"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
log.workspace = true
metal = "0.21.0"
objc = "0.2"

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