Compare commits

..

294 Commits

Author SHA1 Message Date
Max Brunsfeld
c58605a70c Merge pull request #2203 from zed-industries/collab-ui-fixes
Fix minor issues with new collab UI
2023-02-22 14:22:47 -08:00
Max Brunsfeld
995dc195f6 Bump RPC protocol version number 2023-02-22 13:40:46 -08:00
Max Brunsfeld
d7f0c0f43a v0.75.x preview 2023-02-22 12:34:15 -08:00
Max Brunsfeld
b73423daaa Merge pull request #2114 from zed-industries/new-collaboration-ui
New collaboration UI part 1/N
2023-02-22 12:32:41 -08:00
Julia
0324ca3b08 Be more specific about clearing (leader, follower) row
Previously anyone unfollowing someone would clear all other rows for
other followers leading to an incorrect state, fix and test

Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-02-22 15:29:20 -05:00
Julia
36040cd0e1 Add top level leave call button
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-02-22 14:44:23 -05:00
Julia
a07867d628 Fiddle with titlebar item spacing
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-02-22 14:13:29 -05:00
Julia
812145f9ab Only show in-call share/unshare button if own project
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-02-22 13:40:43 -05:00
Julia
dbe5b0205c Add style leader selection container
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-02-22 13:36:06 -05:00
Mikayla Maki
7b559176f1 Merge pull request #2201 from zed-industries/save-shortcuts
Add OS UI Keybindings
2023-02-22 09:16:19 -08:00
Mikayla Maki
d7305077bf Merge pull request #2198 from zed-industries/more-item-defaults
Add more default impls to the item trait
2023-02-22 09:14:12 -08:00
Mikayla Maki
4798b72cb8 Fixed keyboard shortcuts in mac os native panels
co-authored-by: Antonio <antonio@zed.dev>
2023-02-22 09:10:16 -08:00
Mikayla Maki
71d8ead318 Introduce an OSAction that can be associated with menu items for mac platform compatibility.
Co-authored-by: Antonio <antonio@zed.dev>
2023-02-22 09:02:31 -08:00
Julia
9b92a8e3fe Add mockup accurate avatar background
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-02-22 11:34:55 -05:00
Julia
7f4da80386 Initial dedicated face pile element
Rather than overload Flex with yet another special case, add a dedicated
element to handle this overlap instead

Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-02-22 11:34:55 -05:00
Julia
6a731233c5 Remove Flex::with_reversed_pain_order 2023-02-22 11:34:55 -05:00
Max Brunsfeld
b7cf426908 Start work on styling of follower avatars in facepiles
* Make follower avatars smaller than top-level avatars
* Make avatars in facepiles overlap
* Render an opaque background behind avatars in facepiles.
2023-02-22 11:34:55 -05:00
Max Brunsfeld
0dc92bec5c Retrieve room id from the project when following/unfollowing
Previously, we were accidentally using the project id as the room id.
2023-02-22 11:34:55 -05:00
Julia
c75aca25b6 Remove left side collaborator list
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-02-22 11:34:55 -05:00
Julia
ae87961a77 Close contacts popover when call ends
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-02-22 11:34:55 -05:00
Julia
e9464815e0 Make tooltip lie less 2023-02-22 11:34:55 -05:00
Julia
1ed47663ef Avoid moving contacts popover during call start & add button style state
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-02-22 11:34:55 -05:00
Julia
dd02bc7748 Initial adding of contact menu to call-less share button 2023-02-22 11:22:37 -05:00
Julia
e403b868b7 Add followers table to sqlite scheme for tests
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-02-22 11:22:37 -05:00
Julia
3105ecd0bd Shuffle share/unshare button back to the right side 2023-02-22 11:22:37 -05:00
Julia
05e9615507 Highlight face pile which local user is following 2023-02-22 11:22:37 -05:00
Julia
1abb7794cb Handle case where follower is local user 2023-02-22 11:22:37 -05:00
Julia
50e681bbb1 Add username to right side 2023-02-22 11:22:37 -05:00
Julia
3fb8395085 Make things a bit more infallible 2023-02-22 11:22:37 -05:00
Julia
4513c40993 Following face piles finally take their first breath 2023-02-22 11:21:23 -05:00
Julia
4ffc8cd9fd Fix deadlock in db get_room
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-02-22 11:21:23 -05:00
Julia
33c265d3cf Abuse a closure instead of abusing options/iterators quite so much 2023-02-22 11:21:23 -05:00
Julia
58c41778e7 Absolute pain of the iterator kind (start laying out a user's followers) 2023-02-22 11:21:23 -05:00
Julia
2592ec7265 Initial tracking of unfollows on collab server 2023-02-22 11:21:23 -05:00
Julia
d6462c611c Begin tracking follow states on collab server
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-02-22 11:21:23 -05:00
Julia
28786a3c18 Add Flex with_reversed_paint_order & initially move face piles to left
Co-Authored-By: Petros Amoiridis <petros@zed.dev>
2023-02-22 11:21:23 -05:00
Julia
a5fd0250ab Start fleshing out layout of collaborator list entries
Co-Authored-By: Petros Amoiridis <petros@zed.dev>
2023-02-22 11:21:23 -05:00
Julia
f68eda97fb Tell project when it has been unshared
Co-Authored-By: Petros Amoiridis <petros@zed.dev>
2023-02-22 11:21:23 -05:00
Julia
99236f1875 Add collaborators to collaborator list, including self user 2023-02-22 11:21:23 -05:00
Julia
bf8658067f Add collaborator count to collaborator list button 2023-02-22 11:21:23 -05:00
Julia
c697c1a96a Switch collaborator list to using own style 2023-02-22 11:21:23 -05:00
Julia
2b6aa3f5d1 Begin adding collaborator list popover
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-02-22 11:21:23 -05:00
Julia
e96d52f35a Move share button and support unsharing 2023-02-22 11:21:23 -05:00
Julia
ed2f1ddd2d Move workspace title into collaboration titlebar item render 2023-02-22 11:21:11 -05:00
Kay Simmons
24fcad3fa2 Merge pull request #2189 from zed-industries/labeled-tasks
Labeled Tasks
2023-02-21 21:31:47 -08:00
Kay Simmons
46af9a90ce fix test warning 2023-02-21 21:13:03 -08:00
Kay Simmons
1c69e289b7 Fix formatting 2023-02-21 21:07:45 -08:00
Mikayla Maki
9d782be4c8 Remove now-default stubs 2023-02-21 17:51:49 -08:00
Mikayla Maki
cae9e733a1 Add more default impls to the item trait
Change pane to not split if the underlying item cannot be cloned
2023-02-21 17:29:39 -08:00
Mikayla Maki
77c396a0ab Merge pull request #2197 from zed-industries/label-text-cow
Changed label and text to be generic over static and owned strings
2023-02-21 17:05:57 -08:00
Mikayla Maki
b500ed3171 Changed label and text to be generic over static strings and owned strings 2023-02-21 16:47:29 -08:00
Kay Simmons
6b6e4e3bfe Add basic test for labeled tasks 2023-02-21 16:14:22 -08:00
Joseph T. Lyons
1683a54698 Merge pull request #2195 from zed-industries/add-reveal-in-finder-to-additional-locations 2023-02-21 18:48:16 -05:00
Mikayla Maki
14488619a3 Merge pull request #2196 from zed-industries/open_urls
Fix open URLs, restarts, and make bundling easier to use
2023-02-21 15:36:50 -08:00
Mikayla Maki
cf4e719484 Fixes a race condition in the restart implementation
Fixes open_urls racing workspace initialization and causing a double-open (community#927)
Adds a -d flag to the bundle script to compile in debug mode

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

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

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

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

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

With this commit, only one room can be created at any given time and further
invites have to wait until that room creation is done.
2023-02-14 12:03:30 +01:00
Antonio Scandurra
087d51634d Fix test that wasn't properly verifying disconnection from livekit 2023-02-14 10:46:29 +01:00
Max Brunsfeld
ea663f3017 Bump tree-sitter for tree-balancing bugfix 2023-02-13 23:46:44 -08:00
Kay Simmons
5041300b52 Merge pull request #2157 from zed-industries/vim-fixes
Vim fixes
2023-02-13 14:21:47 -08:00
Kay Simmons
2c9199fd32 fix build error 2023-02-13 14:12:43 -08:00
Kay Simmons
327932ba3b Remove catch all keymap and KeyPressed action 2023-02-13 13:50:37 -08:00
Kay Simmons
459060764a fix sqlez warning, introduce tab and enter bindings to vim for inputing tab and enter text when waiting for text 2023-02-13 13:50:37 -08:00
Kay Simmons
3d53336916 More vim fixes and move some more things out of app.rs 2023-02-13 13:50:37 -08:00
Kay Simmons
c1812ddc27 fix issue with single line editors in vim not properly unhooking vim mode bindings 2023-02-13 13:50:37 -08:00
Mikayla Maki
d80dba1fe3 Switch from vec to smallvec 2023-02-13 12:49:57 -08:00
Antonio Scandurra
6703264600 Limit BufferSnapshot::chunks to the outline item range
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-02-13 19:57:15 +01:00
Joseph T. Lyons
0a50d271b7 Merge pull request #2158 from zed-industries/split-feedback-editor
Split up feedback_editor.rs
2023-02-11 23:19:25 -05:00
Joseph T. Lyons
01a590a1fb Merge pull request #2160 from zed-industries/remove-toggle-right-sidebar-command
Remove toggle right sidebar command
2023-02-11 22:17:38 -05:00
Joseph Lyons
d42d495cb0 Remove toggle right sidebar command 2023-02-11 21:53:10 -05:00
Petros Amoiridis
9143790602 Include code only on macOS
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-02-11 11:12:46 +02:00
Kay Simmons
b31813fad3 Split concepts out into self contained files in feedback editor 2023-02-10 22:50:05 -08:00
Mikayla Maki
912c396b37 Merge pull request #2156 from zed-industries/fix-atelier-cave-license
Update the atelier cave license file
2023-02-10 12:04:36 -08:00
Mikayla Maki
436ab6e454 Fix other atelier license 2023-02-10 11:58:25 -08:00
Mikayla Maki
889b15683d Update the atelier cave license file 2023-02-10 11:52:31 -08:00
Mikayla Maki
135dcf19a2 Merge pull request #2154 from zed-industries/fix-tooltip-crash
Don't render tooltip keystroke label if there's no focused view
2023-02-10 11:41:03 -08:00
Petros Amoiridis
5d23aaacc8 Introduce an open function
And refactor some of the older code to simplify it

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

Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-02-10 21:11:05 +02:00
Nathan Sobo
da5a6a8b4f Don't render tooltip keystroke label if there's no focused view
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2023-02-10 07:57:01 -07:00
Joseph T. Lyons
7de04abdcb Merge pull request #2146 from zed-industries/feedback-editor-polish
Feedback editor polish
2023-02-09 10:01:27 -05:00
Antonio Scandurra
373e88e9fb Merge pull request #2147 from zed-industries/fix-pyright
Always respond to language server, even when its requests are malformed
2023-02-09 15:52:57 +01:00
Antonio Scandurra
c3a88857f9 Always respond to language server, even when its requests are malformed
This was causing Pyright to get stuck waiting for a response when sending
us the `workspace/configuration` request. For some reason, after upgrading
to Pyright 1.1.293, `scopeUri` is being sent as an empty string sometimes,
which causes serde to error when trying to deserialize that request.

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

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

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

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-02-02 20:15:40 +02:00
Joseph Lyons
888145ebed Trim leading and trailing whitespace in feedback 2023-02-02 12:43:55 -05:00
Kay Simmons
a50f0181fb Add setting to disable the call icon 2023-02-01 16:21:53 -08:00
Kay Simmons
62d32db66c Make display uuid optional if the display is disconnected 2023-02-01 14:59:43 -08:00
Nate Butler
d6962d957b Add note to base16.ts 2023-02-01 16:26:08 -05:00
Joseph Lyons
fd2a9b3df9 v0.73.x dev 2023-02-01 13:45:06 -05:00
Kay Simmons
460dc62888 start adding setting for the screen sharing status indicator 2023-01-31 15:17:16 -08:00
Kay Simmons
e35db69dbd Add call status indicator to the status bar 2023-01-31 15:00:49 -08:00
Mikayla Maki
a89cc22af4 Merge pull request #2113 from zed-industries/terminal-lost-cwd
Fix lost terminal working directories
2023-01-30 14:43:43 -08:00
Mikayla Maki
e682e2dd72 Changed SQLez migrations to be executed eagerly
Added fix for terminal working directory's sometimes getting lost
co-authored-by: Kay <kay@zed.dev>
2023-01-30 14:38:48 -08:00
Joseph T. Lyons
65641b1d3e Merge pull request #2112 from zed-industries/fix-version-for-feedback-related-commands
Fix version for feedback-related commands
2023-01-30 14:43:33 -05:00
Joseph Lyons
248161aa63 Fix version for feedback-related commands
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
2023-01-30 14:13:25 -05:00
Mikayla Maki
d9278f7416 Merge pull request #2066 from zed-industries/remove-staff-mode
Small patches
2023-01-27 15:50:00 -08:00
Mikayla Maki
57781fd7aa Move StaffMode declaration out of paths 2023-01-27 15:45:33 -08:00
Mikayla Maki
2d889f59bf Rewrite license documentation to be more clear 2023-01-27 15:44:19 -08:00
Mikayla Maki
2802e3a1c6 Fixed failling tests 2023-01-27 15:44:17 -08:00
Mikayla Maki
ea39983f78 Removed old experiments settings and staff mode flag, added new StaffMode global that is set based on the webserver's staff bit 2023-01-27 15:43:12 -08:00
Mikayla Maki
ca2e0256e1 Renamed open recent action to match menu 2023-01-27 15:38:48 -08:00
Mikayla Maki
070b89243f Merge pull request #2107 from zed-industries/fix-ci
Add an install step to the CI build script
2023-01-27 15:38:12 -08:00
Mikayla Maki
e530406d62 Add an install step to the CI build script 2023-01-27 15:24:21 -08:00
Kay Simmons
ea0dd8972f Merge pull request #2090 from zed-industries/workspace-window-position-persistence
Workspace window position persistence
2023-01-27 15:24:01 -08:00
Kay Simmons
a1308d20ce Merge pull request #2105 from zed-industries/fix-focus-stealing-when-collaborating
Limit focus grabbing in followed pane
2023-01-27 15:23:43 -08:00
Kay Simmons
486b3f64d1 Merge pull request #2106 from zed-industries/fix-local-integration-test-failure
fix local failing test
2023-01-27 15:23:23 -08:00
Kay Simmons
0f93386071 Add run until parked to test_fs_operations to ensure both update chunks are completed before asserting the changes 2023-01-27 15:07:51 -08:00
Kay Simmons
77a4f907a0 removed invalid focus assertion 2023-01-27 13:43:36 -08:00
Kay Simmons
d6acea525d add test for is_child_focused 2023-01-27 13:00:26 -08:00
Kay Simmons
89a5506f43 Add function which checks if a child of a view is focused and use that to only focus item updates from the leader when that the active item was focused 2023-01-27 12:39:32 -08:00
Antonio Scandurra
5431488a9a collab 0.5.4 2023-01-27 11:07:12 +01:00
Antonio Scandurra
ac7618da17 Merge pull request #2103 from zed-industries/connection-staleness
Fix connection staleness issues
2023-01-27 11:01:24 +01:00
Antonio Scandurra
647d9861b1 Abort collaboration process if any thread panics 2023-01-27 09:50:59 +01:00
Mikayla Maki
d7ac15fa71 Merge pull request #2101 from zed-industries/theme-licenses
Added build-licenses command to style tree
2023-01-26 18:29:20 -08:00
Mikayla Maki
3a1d533c01 Combine both license generations into one file 2023-01-26 18:25:28 -08:00
Mikayla Maki
c44acaefff Added build-licenses command to style tree 2023-01-26 17:33:54 -08:00
Kay Simmons
1593b1e13d window position restoration working 2023-01-26 16:35:00 -08:00
Max Brunsfeld
fabcdb909a Merge pull request #2100 from zed-industries/visible-worktrees-in-collab-ui
Omit hidden worktrees when showing projects in collaboration UI
2023-01-26 15:01:10 -08:00
Max Brunsfeld
f99e4043c4 Run CI for version branches but not all branches starting with 'v' 2023-01-26 14:57:24 -08:00
Max Brunsfeld
1b45911857 Omit hidden worktrees when showing projects in collaboration UI 2023-01-26 14:47:37 -08:00
Max Brunsfeld
4918ad5789 Merge pull request #2099 from zed-industries/empty-go-to-def-multibuffer
Avoid opening a definitions tab if there are no definitions found
2023-01-26 10:35:35 -08:00
Max Brunsfeld
9f86748aff Avoid opening a definitions tab if there are no definitions found 2023-01-26 10:30:01 -08:00
Petros Amoiridis
489be5e77b Merge pull request #2077 from zed-industries/2064-remove-contacts
Remove contact from contact list
2023-01-26 20:04:15 +02:00
Max Brunsfeld
b396e153d1 Merge pull request #2098 from zed-industries/help-menu-licenses
Add 'view dependency licenses' item to Help appication menu
2023-01-26 09:56:44 -08:00
Max Brunsfeld
1c572fd86e Add 'view dependency licenses' item to Help appication menu 2023-01-26 09:53:46 -08:00
Petros Amoiridis
73af155dd6 Refactor Database::remove_contact
Refactor it to avoid sending irrelevant messages to update the UI.

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-01-26 19:01:51 +02:00
Antonio Scandurra
eca6115e4b Ensure proto::UpdateWorktree::removed_entries doesn't exceed chunk size
This was causing the database to panic because we were trying to remove too
many entries at once.
2023-01-26 17:26:31 +01:00
Antonio Scandurra
74aeec360d Cancel pending call when participant leaves room after a reconnection
Previously, if a user temporarily disconnected while there was a pending
call, we would fail to cancel such pending call when the caller left the
room. This was due to the caller reconnecting and having a different connection
id than the one originally used to initiate the call.
2023-01-26 16:44:55 +01:00
Petros Amoiridis
2f26fcd889 Merge branch 'main' into 2064-remove-contacts 2023-01-26 16:34:17 +02:00
Joseph T. Lyons
a4d9d6c750 Merge pull request #2095 from zed-industries/fix-crash-when-opening-feedback-while-in-call
Fix crash when opening feedback while in call
2023-01-25 21:16:12 -05:00
Max Brunsfeld
a2a3ebc42f Merge pull request #2096 from zed-industries/lazy-load-languages
Load languages lazily in the background
2023-01-25 18:09:45 -08:00
Max Brunsfeld
ddf4e1a316 Load languages lazily in the background 2023-01-25 17:47:46 -08:00
Kay Simmons
a369fb8033 better but still broken 2023-01-25 17:05:57 -08:00
Joseph Lyons
9ff34bcb6a Remove no-longer-needed method 2023-01-25 20:03:44 -05:00
Julia
10f130ee30 Merge pull request #2094 from zed-industries/project-lost-window-close-action-shortcut-accessibility
Add "Close Window" global action which does not need a focused workspace
2023-01-25 18:58:22 -05:00
Julia
3819a67185 Add "Close Window" global action which does not need a focused workspace 2023-01-25 18:51:25 -05:00
Joseph Lyons
6e7101ca6b Fix crash when opening feedback while in call 2023-01-25 17:48:01 -05:00
Julia
2df2d09e3c Merge pull request #2091 from zed-industries/style
Style
2023-01-25 15:22:52 -05:00
Joseph Lyons
4c3244b982 v0.72.x dev 2023-01-25 15:20:41 -05:00
Julia
a79b4e312b Style
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-01-25 15:09:57 -05:00
Kay Simmons
5eac797a93 mostly working now 2023-01-25 11:36:38 -08:00
Kay Simmons
a581d0c5b8 wip 2023-01-25 11:32:19 -08:00
Kay Simmons
15799f7af6 wip 2023-01-25 11:32:19 -08:00
Petros Amoiridis
160870c9de Improve user notification
The message is not really true. When one declines, the other person can notice that the contact request  is not pending any more. They will know. Switching to not alerted is closer to what is really happening.
2023-01-25 19:46:51 +02:00
Petros Amoiridis
35524db136 Add a confirmation prompt
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-01-25 18:55:08 +02:00
Petros Amoiridis
e928c1c61e Test removing a contact
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-01-25 17:31:42 +02:00
Petros Amoiridis
5d4eb2b7ae Push responder and requester to remove_contacts
When we ask the server to remove a contact we need to push the requester and responder ids to `remove_contacts` so that when the UI updates, the correct contacts will disappear from the list.

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-01-25 13:10:29 +02:00
Petros Amoiridis
db978fcb6c Add an x mark icon to the list of contacts
We want to be able to remove contacts from our list. This was not possible. This change add an icon and dispatches the RemoveContact action.

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-01-25 13:10:29 +02:00
231 changed files with 8990 additions and 4577 deletions

View File

@@ -8,4 +8,4 @@ crates/collab/static/styles.css
vendor/bin
assets/themes/*.json
assets/themes/internal/*.json
assets/themes/experiments/*.json
assets/themes/staff/*.json

View File

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

View File

@@ -4,7 +4,7 @@ on:
push:
branches:
- main
- "v*"
- "v[0-9]+.[0-9]+.x"
tags:
- "v*"
pull_request:
@@ -17,6 +17,24 @@ env:
RUST_BACKTRACE: 1
jobs:
rustfmt:
name: Check formatting
runs-on: self-hosted
steps:
- name: Install Rust
run: |
rustup set profile minimal
rustup update stable
- name: Checkout repo
uses: actions/checkout@v2
with:
clean: false
submodules: 'recursive'
- name: cargo fmt
run: cargo fmt --all -- --check
tests:
name: Run tests
runs-on:
@@ -42,6 +60,9 @@ jobs:
clean: false
submodules: 'recursive'
- name: Run check
run: cargo check --workspace
- name: Run tests
run: cargo test --workspace --no-fail-fast

View File

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

5
.gitignore vendored
View File

@@ -7,9 +7,8 @@
/crates/collab/static/styles.css
/vendor/bin
/assets/themes/*.json
/assets/themes/Internal/*.json
/assets/themes/Experiments/*.json
/assets/licenses.md
/assets/*licenses.md
/assets/themes/staff/*.json
**/venv
.build
Packages

154
Cargo.lock generated
View File

@@ -259,6 +259,21 @@ dependencies = [
"futures-lite",
]
[[package]]
name = "async-global-executor"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776"
dependencies = [
"async-channel",
"async-executor",
"async-io",
"async-lock",
"blocking",
"futures-lite",
"once_cell",
]
[[package]]
name = "async-io"
version = "1.12.0"
@@ -350,6 +365,32 @@ dependencies = [
"syn",
]
[[package]]
name = "async-std"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d"
dependencies = [
"async-channel",
"async-global-executor",
"async-io",
"async-lock",
"crossbeam-utils 0.8.14",
"futures-channel",
"futures-core",
"futures-io",
"futures-lite",
"gloo-timers",
"kv-log-macro",
"log",
"memchr",
"once_cell",
"pin-project-lite 0.2.9",
"pin-utils",
"slab",
"wasm-bindgen-futures",
]
[[package]]
name = "async-stream"
version = "0.3.3"
@@ -371,6 +412,20 @@ dependencies = [
"syn",
]
[[package]]
name = "async-tar"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c49359998a76e32ef6e870dbc079ebad8f1e53e8441c5dd39d27b44493fe331"
dependencies = [
"async-std",
"filetime",
"libc",
"pin-project",
"redox_syscall",
"xattr",
]
[[package]]
name = "async-task"
version = "4.0.3"
@@ -828,6 +883,7 @@ dependencies = [
"media",
"postage",
"project",
"settings",
"util",
]
@@ -1132,7 +1188,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.5.3"
version = "0.5.4"
dependencies = [
"anyhow",
"async-tungstenite",
@@ -1196,6 +1252,7 @@ name = "collab_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"auto_update",
"call",
"client",
"clock",
@@ -1275,6 +1332,7 @@ source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2f
dependencies = [
"core-foundation-sys",
"libc",
"uuid 0.5.1",
]
[[package]]
@@ -1899,6 +1957,7 @@ dependencies = [
"tree-sitter-html",
"tree-sitter-javascript",
"tree-sitter-rust",
"tree-sitter-typescript 0.20.2",
"unindent",
"util",
"workspace",
@@ -2078,6 +2137,18 @@ dependencies = [
"workspace",
]
[[package]]
name = "filetime"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"windows-sys 0.42.0",
]
[[package]]
name = "fixedbitset"
version = "0.4.2"
@@ -2526,6 +2597,18 @@ dependencies = [
"regex",
]
[[package]]
name = "gloo-timers"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
dependencies = [
"futures-channel",
"futures-core",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "go_to_line"
version = "0.1.0"
@@ -2591,6 +2674,7 @@ dependencies = [
"tiny-skia",
"usvg",
"util",
"uuid 1.2.2",
"waker-fn",
]
@@ -3141,6 +3225,15 @@ dependencies = [
"arrayvec 0.7.2",
]
[[package]]
name = "kv-log-macro"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
dependencies = [
"log",
]
[[package]]
name = "language"
version = "0.1.0"
@@ -3158,6 +3251,7 @@ dependencies = [
"fuzzy",
"git",
"gpui",
"indoc",
"lazy_static",
"log",
"lsp",
@@ -3184,7 +3278,7 @@ dependencies = [
"tree-sitter-python",
"tree-sitter-ruby",
"tree-sitter-rust",
"tree-sitter-typescript",
"tree-sitter-typescript 0.20.1",
"unicase",
"unindent",
"util",
@@ -6014,6 +6108,7 @@ dependencies = [
"parking_lot 0.11.2",
"smol",
"thread_local",
"uuid 1.2.2",
]
[[package]]
@@ -6463,6 +6558,7 @@ dependencies = [
"settings",
"smol",
"theme",
"util",
"workspace",
]
@@ -6909,7 +7005,7 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.20.9"
source = "git+https://github.com/tree-sitter/tree-sitter?rev=36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da#36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da"
source = "git+https://github.com/tree-sitter/tree-sitter?rev=c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14#c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14"
dependencies = [
"cc",
"regex",
@@ -7011,6 +7107,16 @@ dependencies = [
"tree-sitter",
]
[[package]]
name = "tree-sitter-lua"
version = "0.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d489873fd1a2fa6d5f04930bfc5c081c96f0c038c1437104518b5b842c69b282"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-markdown"
version = "0.0.1"
@@ -7087,6 +7193,24 @@ dependencies = [
"tree-sitter",
]
[[package]]
name = "tree-sitter-typescript"
version = "0.20.2"
source = "git+https://github.com/tree-sitter/tree-sitter-typescript?rev=5d20856f34315b068c41edaee2ac8a100081d259#5d20856f34315b068c41edaee2ac8a100081d259"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-yaml"
version = "0.0.1"
source = "git+https://github.com/zed-industries/tree-sitter-yaml?rev=9050a4a4a847ed29e25485b1292a36eab8ae3492#9050a4a4a847ed29e25485b1292a36eab8ae3492"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "try-lock"
version = "0.2.3"
@@ -7324,6 +7448,12 @@ dependencies = [
"tempdir",
]
[[package]]
name = "uuid"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22"
[[package]]
name = "uuid"
version = "0.8.2"
@@ -8169,6 +8299,7 @@ dependencies = [
"smallvec",
"theme",
"util",
"uuid 1.2.2",
]
[[package]]
@@ -8181,6 +8312,15 @@ dependencies = [
"winapi-build",
]
[[package]]
name = "xattr"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
dependencies = [
"libc",
]
[[package]]
name = "xml-rs"
version = "0.8.4"
@@ -8216,13 +8356,14 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "zed"
version = "0.71.3"
version = "0.75.0"
dependencies = [
"activity_indicator",
"anyhow",
"assets",
"async-compression",
"async-recursion 0.3.2",
"async-tar",
"async-trait",
"auto_update",
"backtrace",
@@ -8300,6 +8441,7 @@ dependencies = [
"tree-sitter-go",
"tree-sitter-html",
"tree-sitter-json 0.20.0",
"tree-sitter-lua",
"tree-sitter-markdown",
"tree-sitter-python",
"tree-sitter-racket",
@@ -8307,11 +8449,13 @@ dependencies = [
"tree-sitter-rust",
"tree-sitter-scheme",
"tree-sitter-toml",
"tree-sitter-typescript",
"tree-sitter-typescript 0.20.2",
"tree-sitter-yaml",
"unindent",
"url",
"urlencoding",
"util",
"uuid 1.2.2",
"vim",
"workspace",
]

View File

@@ -69,7 +69,7 @@ serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
rand = { version = "0.8" }
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da" }
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" }
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457

View File

@@ -49,30 +49,14 @@ script/zed-with-local-servers --release
If you trigger `cmd-alt-i`, Zed will copy a JSON representation of the current window contents to the clipboard. You can paste this in a tool like [DJSON](https://chrome.google.com/webstore/detail/djson-json-viewer-formatt/chaeijjekipecdajnijdldjjipaegdjc?hl=en) to navigate the state of on-screen elements in a structured way.
### Staff Only Features
### Licensing
Many features (e.g. the terminal) take significant time and effort before they are polished enough to be released to even Alpha users. But Zed's team workflow relies on fast, daily PRs and there can be large merge conflicts for feature branchs that diverge for a few days. To bridge this gap, there is a `staff_mode` field in the Settings that staff can set to enable these unpolished or incomplete features. Note that this setting isn't leaked via autocompletion, but there is no mechanism to stop users from setting this anyway. As initilization of Zed components is only done once, on startup, setting `staff_mode` may require a restart to take effect. You can set staff only key bindings in the `assets/keymaps/internal.json` file, and add staff only themes in the `styles/src/themes/internal` directory
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
### Experimental Features
- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.
- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration).
A user facing feature flag can be added to Zed by:
* Adding a setting to the crates/settings/src/settings.rs FeatureFlags struct. Use a boolean for a simple on/off, or use a struct to experiment with different configuration options.
* If the feature needs keybindings, add a file to the `assets/keymaps/experiments/` folder, then update the `FeatureFlags::keymap_files()` method to check for your feature's flag and add it's keybindings's path to the method's list.
* If you want to add an experimental theme, add it to the `styles/src/themes/experiments` folder
The Settings global should be initialized with the user's feature flags by the time the feature's `init(cx)` equivalent is called.
To promote an experimental feature to a full feature:
* If this is an experimental theme, move the theme file from the `styles/src/themes/experiments` folder to the `styles/src/themes/` folder
* Take the features settings (if any) and add them under a new variable in the Settings struct. Don't forget to add a `merge()` call in `set_user_settings()`!
* Take the feature's keybindings and add them to the default.json (or equivalent) file
* Remove the file from the `FeatureFlags::keymap_files()` method
* Remove the conditional in the feature's `init(cx)` equivalent.
That's it 😸
### Wasm Plugins

View File

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

After

Width:  |  Height:  |  Size: 784 B

View File

@@ -38,7 +38,7 @@
"cmd-n": "workspace::NewFile",
"cmd-shift-n": "workspace::NewWindow",
"cmd-o": "workspace::Open",
"alt-cmd-o": "recent_projects::Toggle",
"alt-cmd-o": "projects::OpenRecent",
"ctrl-`": "workspace::NewTerminal"
}
},
@@ -164,6 +164,7 @@
"bindings": {
"enter": "editor::Newline",
"cmd-enter": "editor::NewlineBelow",
"alt-z": "editor::ToggleSoftWrap",
"cmd-f": [
"buffer_search::Deploy",
{
@@ -227,7 +228,12 @@
"replace_newest": true
}
],
"cmd-/": "editor::ToggleComments",
"cmd-/": [
"editor::ToggleComments",
{
"advance_downwards": false
}
],
"alt-up": "editor::SelectLargerSyntaxNode",
"alt-down": "editor::SelectSmallerSyntaxNode",
"cmd-u": "editor::UndoSelection",
@@ -412,7 +418,7 @@
{
"bindings": {
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
"cmd-shift-c": "collab::ToggleCollaborationMenu",
"cmd-shift-c": "collab::ToggleContactsMenu",
"cmd-alt-i": "zed::DebugElements"
}
},
@@ -433,8 +439,7 @@
{
"context": "Workspace",
"bindings": {
"shift-escape": "dock::FocusDock",
"cmd-shift-b": "workspace::ToggleRightSidebar"
"shift-escape": "dock::FocusDock"
}
},
{
@@ -445,15 +450,16 @@
}
},
{
"context": "Dock",
"context": "Pane",
"bindings": {
"shift-escape": "dock::HideDock"
"cmd-escape": "dock::AddTabToDock"
}
},
{
"context": "Pane",
"context": "Dock",
"bindings": {
"cmd-escape": "dock::MoveActiveItemToDock"
"shift-escape": "dock::HideDock",
"cmd-escape": "dock::RemoveTabFromDock"
}
},
{

View File

@@ -1 +0,0 @@
[]

View File

@@ -315,7 +315,9 @@
{
"context": "Editor && VimWaiting",
"bindings": {
"*": "gpui::KeyPressed"
"tab": "vim::Tab",
"enter": "vim::Enter",
"escape": "editor::Cancel"
}
}
]

View File

@@ -20,6 +20,8 @@
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
// Whether the screen sharing icon is showed in the os status bar.
"show_call_status_icon": true,
// Whether new projects should start out 'online'. Online projects
// appear in the contacts panel under your name, so that your contacts
// can see which projects you are working on. Regardless of this
@@ -88,6 +90,8 @@
// Send anonymized usage data like what languages you're using Zed with.
"metrics": true
},
// Automatically update Zed
"auto_update": true,
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
@@ -219,6 +223,9 @@
},
"TSX": {
"tab_size": 2
},
"YAML": {
"tab_size": 2
}
},
// LSP Specific settings.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -55,6 +55,7 @@ pub struct Room {
leave_when_empty: bool,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
follows_by_leader_id: HashMap<PeerId, Vec<PeerId>>,
subscriptions: Vec<client::Subscription>,
pending_room_update: Option<Task<()>>,
maintain_connection: Option<Task<Option<()>>>,
@@ -148,6 +149,7 @@ impl Room {
pending_room_update: None,
client,
user_store,
follows_by_leader_id: Default::default(),
maintain_connection: Some(maintain_connection),
}
}
@@ -457,6 +459,12 @@ impl Room {
self.participant_user_ids.contains(&user_id)
}
pub fn followers_for(&self, leader_id: PeerId) -> &[PeerId] {
self.follows_by_leader_id
.get(&leader_id)
.map_or(&[], |v| v.as_slice())
}
async fn handle_room_updated(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::RoomUpdated>,
@@ -487,11 +495,13 @@ impl Room {
.iter()
.map(|p| p.user_id)
.collect::<Vec<_>>();
let remote_participant_user_ids = room
.participants
.iter()
.map(|p| p.user_id)
.collect::<Vec<_>>();
let (remote_participants, pending_participants) =
self.user_store.update(cx, move |user_store, cx| {
(
@@ -499,6 +509,7 @@ impl Room {
user_store.get_users(pending_participant_user_ids, cx),
)
});
self.pending_room_update = Some(cx.spawn(|this, mut cx| async move {
let (remote_participants, pending_participants) =
futures::join!(remote_participants, pending_participants);
@@ -620,6 +631,26 @@ impl Room {
}
}
this.follows_by_leader_id.clear();
for follower in room.followers {
let (leader, follower) = match (follower.leader_id, follower.follower_id) {
(Some(leader), Some(follower)) => (leader, follower),
_ => {
log::error!("Follower message {follower:?} missing some state");
continue;
}
};
let list = this
.follows_by_leader_id
.entry(leader)
.or_insert(Vec::new());
if !list.contains(&follower) {
list.push(follower);
}
}
this.pending_room_update.take();
if this.should_leave() {
log::info!("room is empty, leaving");
@@ -793,6 +824,20 @@ impl Room {
})
}
pub(crate) fn unshare_project(
&mut self,
project: ModelHandle<Project>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
let project_id = match project.read(cx).remote_id() {
Some(project_id) => project_id,
None => return Ok(()),
};
self.client.send(proto::UnshareProject { project_id })?;
project.update(cx, |this, cx| this.unshare(cx))
}
pub(crate) fn set_location(
&mut self,
project: Option<&ModelHandle<Project>>,

View File

@@ -1324,6 +1324,10 @@ impl Client {
pub fn metrics_id(&self) -> Option<Arc<str>> {
self.telemetry.metrics_id()
}
pub fn is_staff(&self) -> Option<bool> {
self.telemetry.is_staff()
}
}
impl WeakSubscriber {

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse};
use settings::Settings;
use std::sync::{Arc, Weak};
use util::TryFutureExt as _;
use util::{StaffMode, TryFutureExt as _};
#[derive(Default, Debug)]
pub struct User {
@@ -148,6 +148,19 @@ impl UserStore {
cx.read(|cx| cx.global::<Settings>().telemetry()),
);
cx.update(|cx| {
cx.update_default_global(|staff_mode: &mut StaffMode, _| {
if !staff_mode.0 {
*staff_mode = StaffMode(
info.as_ref()
.map(|info| info.staff)
.unwrap_or_default(),
)
}
()
});
});
current_user_tx.send(user).await.ok();
}
}

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
mod access_token;
mod contact;
mod follower;
mod language_server;
mod project;
mod project_collaborator;
@@ -595,7 +596,16 @@ impl Database {
.await
}
pub async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()> {
/// Returns a bool indicating whether the removed contact had originally accepted or not
///
/// Deletes the contact identified by the requester and responder ids, and then returns
/// whether the deleted contact had originally accepted or was a pending contact request.
///
/// # Arguments
///
/// * `requester_id` - The user that initiates this request
/// * `responder_id` - The user that will be removed
pub async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<bool> {
self.transaction(|tx| async move {
let (id_a, id_b) = if responder_id < requester_id {
(responder_id, requester_id)
@@ -603,20 +613,18 @@ impl Database {
(requester_id, responder_id)
};
let result = contact::Entity::delete_many()
let contact = contact::Entity::find()
.filter(
contact::Column::UserIdA
.eq(id_a)
.and(contact::Column::UserIdB.eq(id_b)),
)
.exec(&*tx)
.await?;
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such contact"))?;
if result.rows_affected == 1 {
Ok(())
} else {
Err(anyhow!("no such contact"))?
}
contact::Entity::delete_by_id(contact.id).exec(&*tx).await?;
Ok(contact.accepted)
})
.await
}
@@ -1710,6 +1718,88 @@ impl Database {
.await
}
pub async fn follow(
&self,
project_id: ProjectId,
leader_connection: ConnectionId,
follower_connection: ConnectionId,
) -> Result<RoomGuard<proto::Room>> {
self.room_transaction(|tx| async move {
let room_id = self.room_id_for_project(project_id, &*tx).await?;
follower::ActiveModel {
room_id: ActiveValue::set(room_id),
project_id: ActiveValue::set(project_id),
leader_connection_server_id: ActiveValue::set(ServerId(
leader_connection.owner_id as i32,
)),
leader_connection_id: ActiveValue::set(leader_connection.id as i32),
follower_connection_server_id: ActiveValue::set(ServerId(
follower_connection.owner_id as i32,
)),
follower_connection_id: ActiveValue::set(follower_connection.id as i32),
..Default::default()
}
.insert(&*tx)
.await?;
Ok((room_id, self.get_room(room_id, &*tx).await?))
})
.await
}
pub async fn unfollow(
&self,
project_id: ProjectId,
leader_connection: ConnectionId,
follower_connection: ConnectionId,
) -> Result<RoomGuard<proto::Room>> {
self.room_transaction(|tx| async move {
let room_id = self.room_id_for_project(project_id, &*tx).await?;
follower::Entity::delete_many()
.filter(
Condition::all()
.add(follower::Column::ProjectId.eq(project_id))
.add(
follower::Column::LeaderConnectionServerId
.eq(leader_connection.owner_id)
.and(follower::Column::LeaderConnectionId.eq(leader_connection.id)),
)
.add(
follower::Column::FollowerConnectionServerId
.eq(follower_connection.owner_id)
.and(
follower::Column::FollowerConnectionId
.eq(follower_connection.id),
),
),
)
.exec(&*tx)
.await?;
Ok((room_id, self.get_room(room_id, &*tx).await?))
})
.await
}
async fn room_id_for_project(
&self,
project_id: ProjectId,
tx: &DatabaseTransaction,
) -> Result<RoomId> {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryAs {
RoomId,
}
Ok(project::Entity::find_by_id(project_id)
.select_only()
.column(project::Column::RoomId)
.into_values::<_, QueryAs>()
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?)
}
pub async fn update_room_participant_location(
&self,
room_id: RoomId,
@@ -1913,16 +2003,30 @@ impl Database {
};
if let Some(db_worktree) = db_worktree {
project.worktree_root_names.push(db_worktree.root_name);
if db_worktree.visible {
project.worktree_root_names.push(db_worktree.root_name);
}
}
}
}
drop(db_projects);
let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?;
let mut followers = Vec::new();
while let Some(db_follower) = db_followers.next().await {
let db_follower = db_follower?;
followers.push(proto::Follower {
leader_id: Some(db_follower.leader_connection().into()),
follower_id: Some(db_follower.follower_connection().into()),
});
}
Ok(proto::Room {
id: db_room.id.to_proto(),
live_kit_room: db_room.live_kit_room,
participants: participants.into_values().collect(),
pending_participants,
followers,
})
}
@@ -3002,6 +3106,7 @@ macro_rules! id_type {
id_type!(AccessTokenId);
id_type!(ContactId);
id_type!(FollowerId);
id_type!(RoomId);
id_type!(RoomParticipantId);
id_type!(ProjectId);

View File

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

View File

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

View File

@@ -1312,6 +1312,7 @@ async fn join_project(
.filter(|collaborator| collaborator.connection_id != session.connection_id)
.map(|collaborator| collaborator.to_proto())
.collect::<Vec<_>>();
let worktrees = project
.worktrees
.iter()
@@ -1724,6 +1725,7 @@ async fn follow(
.ok_or_else(|| anyhow!("invalid leader id"))?
.into();
let follower_id = session.connection_id;
{
let project_connection_ids = session
.db()
@@ -1744,6 +1746,14 @@ async fn follow(
.views
.retain(|view| view.leader_id != Some(follower_id.into()));
response.send(response_payload)?;
let room = session
.db()
.await
.follow(project_id, leader_id, follower_id)
.await?;
room_updated(&room, &session.peer);
Ok(())
}
@@ -1753,17 +1763,29 @@ async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
.leader_id
.ok_or_else(|| anyhow!("invalid leader id"))?
.into();
let project_connection_ids = session
let follower_id = session.connection_id;
if !session
.db()
.await
.project_connection_ids(project_id, session.connection_id)
.await?;
if !project_connection_ids.contains(&leader_id) {
.await?
.contains(&leader_id)
{
Err(anyhow!("no such peer"))?;
}
session
.peer
.forward_send(session.connection_id, leader_id, request)?;
let room = session
.db()
.await
.unfollow(project_id, leader_id, follower_id)
.await?;
room_updated(&room, &session.peer);
Ok(())
}
@@ -1961,23 +1983,31 @@ async fn remove_contact(
let requester_id = session.user_id;
let responder_id = UserId::from_proto(request.user_id);
let db = session.db().await;
db.remove_contact(requester_id, responder_id).await?;
let contact_accepted = db.remove_contact(requester_id, responder_id).await?;
let pool = session.connection_pool().await;
// Update outgoing contact requests of requester
let mut update = proto::UpdateContacts::default();
update
.remove_outgoing_requests
.push(responder_id.to_proto());
if contact_accepted {
update.remove_contacts.push(responder_id.to_proto());
} else {
update
.remove_outgoing_requests
.push(responder_id.to_proto());
}
for connection_id in pool.user_connection_ids(requester_id) {
session.peer.send(connection_id, update.clone())?;
}
// Update incoming contact requests of responder
let mut update = proto::UpdateContacts::default();
update
.remove_incoming_requests
.push(requester_id.to_proto());
if contact_accepted {
update.remove_contacts.push(requester_id.to_proto());
} else {
update
.remove_incoming_requests
.push(requester_id.to_proto());
}
for connection_id in pool.user_connection_ids(responder_id) {
session.peer.send(connection_id, update.clone())?;
}

View File

@@ -11,7 +11,7 @@ use client::{
EstablishConnectionError, UserStore,
};
use collections::{HashMap, HashSet};
use fs::{FakeFs, HomeDir};
use fs::FakeFs;
use futures::{channel::oneshot, StreamExt as _};
use gpui::{
executor::Deterministic, test::EmptyView, ModelHandle, Task, TestAppContext, ViewHandle,
@@ -100,7 +100,6 @@ impl TestServer {
async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
cx.update(|cx| {
cx.set_global(HomeDir(Path::new("/tmp/").to_path_buf()));
cx.set_global(Settings::test(cx));
});
@@ -196,7 +195,7 @@ impl TestServer {
languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
themes: ThemeRegistry::new((), cx.font_cache()),
fs: fs.clone(),
build_window_options: Default::default,
build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _| unimplemented!(),
dock_default_item_factory: |_, _| unimplemented!(),
});

View File

@@ -166,9 +166,67 @@ async fn test_basic_calls(
}
);
// Call user C again from user A.
active_call_a
.update(cx_a, |call, cx| {
call.invite(client_c.user_id().unwrap(), None, cx)
})
.await
.unwrap();
deterministic.run_until_parked();
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
remote: vec!["user_b".to_string()],
pending: vec!["user_c".to_string()]
}
);
assert_eq!(
room_participants(&room_b, cx_b),
RoomParticipants {
remote: vec!["user_a".to_string()],
pending: vec!["user_c".to_string()]
}
);
// User C accepts the call.
let call_c = incoming_call_c.next().await.unwrap().unwrap();
assert_eq!(call_c.calling_user.github_login, "user_a");
active_call_c
.update(cx_c, |call, cx| call.accept_incoming(cx))
.await
.unwrap();
assert!(incoming_call_c.next().await.unwrap().is_none());
let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone());
deterministic.run_until_parked();
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
remote: vec!["user_b".to_string(), "user_c".to_string()],
pending: Default::default()
}
);
assert_eq!(
room_participants(&room_b, cx_b),
RoomParticipants {
remote: vec!["user_a".to_string(), "user_c".to_string()],
pending: Default::default()
}
);
assert_eq!(
room_participants(&room_c, cx_c),
RoomParticipants {
remote: vec!["user_a".to_string(), "user_b".to_string()],
pending: Default::default()
}
);
// User A shares their screen
let display = MacOSDisplay::new();
let events_b = active_call_events(cx_b);
let events_c = active_call_events(cx_c);
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
@@ -181,9 +239,10 @@ async fn test_basic_calls(
deterministic.run_until_parked();
// User B observes the remote screen sharing track.
assert_eq!(events_b.borrow().len(), 1);
let event = events_b.borrow().first().unwrap().clone();
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event {
let event_b = events_b.borrow().first().unwrap().clone();
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_b {
assert_eq!(participant_id, client_a.peer_id().unwrap());
room_b.read_with(cx_b, |room, _| {
assert_eq!(
@@ -197,6 +256,23 @@ async fn test_basic_calls(
panic!("unexpected event")
}
// User C observes the remote screen sharing track.
assert_eq!(events_c.borrow().len(), 1);
let event_c = events_c.borrow().first().unwrap().clone();
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_c {
assert_eq!(participant_id, client_a.peer_id().unwrap());
room_c.read_with(cx_c, |room, _| {
assert_eq!(
room.remote_participants()[&client_a.user_id().unwrap()]
.tracks
.len(),
1
);
});
} else {
panic!("unexpected event")
}
// User A leaves the room.
active_call_a.update(cx_a, |call, cx| {
call.hang_up(cx).unwrap();
@@ -213,18 +289,28 @@ async fn test_basic_calls(
assert_eq!(
room_participants(&room_b, cx_b),
RoomParticipants {
remote: Default::default(),
remote: vec!["user_c".to_string()],
pending: Default::default()
}
);
assert_eq!(
room_participants(&room_c, cx_c),
RoomParticipants {
remote: vec!["user_b".to_string()],
pending: Default::default()
}
);
// User B gets disconnected from the LiveKit server, which causes them
// to automatically leave the room.
// to automatically leave the room. User C leaves the room as well because
// nobody else is in there.
server
.test_live_kit_server
.disconnect_client(client_b.peer_id().unwrap().to_string())
.disconnect_client(client_b.user_id().unwrap().to_string())
.await;
active_call_b.update(cx_b, |call, _| assert!(call.room().is_none()));
deterministic.run_until_parked();
active_call_b.read_with(cx_b, |call, _| assert!(call.room().is_none()));
active_call_c.read_with(cx_c, |call, _| assert!(call.room().is_none()));
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
@@ -239,6 +325,141 @@ async fn test_basic_calls(
pending: Default::default()
}
);
assert_eq!(
room_participants(&room_c, cx_c),
RoomParticipants {
remote: Default::default(),
pending: Default::default()
}
);
}
#[gpui::test(iterations = 10)]
async fn test_calling_multiple_users_simultaneously(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
cx_d: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
let client_d = server.create_client(cx_d, "user_d").await;
server
.make_contacts(&mut [
(&client_a, cx_a),
(&client_b, cx_b),
(&client_c, cx_c),
(&client_d, cx_d),
])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
let active_call_c = cx_c.read(ActiveCall::global);
let active_call_d = cx_d.read(ActiveCall::global);
// Simultaneously call user B and user C from client A.
let b_invite = active_call_a.update(cx_a, |call, cx| {
call.invite(client_b.user_id().unwrap(), None, cx)
});
let c_invite = active_call_a.update(cx_a, |call, cx| {
call.invite(client_c.user_id().unwrap(), None, cx)
});
b_invite.await.unwrap();
c_invite.await.unwrap();
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
deterministic.run_until_parked();
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
remote: Default::default(),
pending: vec!["user_b".to_string(), "user_c".to_string()]
}
);
// Call client D from client A.
active_call_a
.update(cx_a, |call, cx| {
call.invite(client_d.user_id().unwrap(), None, cx)
})
.await
.unwrap();
deterministic.run_until_parked();
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
remote: Default::default(),
pending: vec![
"user_b".to_string(),
"user_c".to_string(),
"user_d".to_string()
]
}
);
// Accept the call on all clients simultaneously.
let accept_b = active_call_b.update(cx_b, |call, cx| call.accept_incoming(cx));
let accept_c = active_call_c.update(cx_c, |call, cx| call.accept_incoming(cx));
let accept_d = active_call_d.update(cx_d, |call, cx| call.accept_incoming(cx));
accept_b.await.unwrap();
accept_c.await.unwrap();
accept_d.await.unwrap();
deterministic.run_until_parked();
let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
let room_c = active_call_c.read_with(cx_c, |call, _| call.room().unwrap().clone());
let room_d = active_call_d.read_with(cx_d, |call, _| call.room().unwrap().clone());
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
remote: vec![
"user_b".to_string(),
"user_c".to_string(),
"user_d".to_string(),
],
pending: Default::default()
}
);
assert_eq!(
room_participants(&room_b, cx_b),
RoomParticipants {
remote: vec![
"user_a".to_string(),
"user_c".to_string(),
"user_d".to_string(),
],
pending: Default::default()
}
);
assert_eq!(
room_participants(&room_c, cx_c),
RoomParticipants {
remote: vec![
"user_a".to_string(),
"user_b".to_string(),
"user_d".to_string(),
],
pending: Default::default()
}
);
assert_eq!(
room_participants(&room_d, cx_d),
RoomParticipants {
remote: vec![
"user_a".to_string(),
"user_b".to_string(),
"user_c".to_string(),
],
pending: Default::default()
}
);
}
#[gpui::test(iterations = 10)]
@@ -2023,7 +2244,9 @@ async fn test_propagate_saves_and_fs_changes(
});
// Edit the buffer as the host and concurrently save as guest B.
let save_b = buffer_b.update(cx_b, |buf, cx| buf.save(cx));
let save_b = project_b.update(cx_b, |project, cx| {
project.save_buffer(buffer_b.clone(), cx)
});
buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx));
save_b.await.unwrap();
assert_eq!(
@@ -2092,6 +2315,41 @@ async fn test_propagate_saves_and_fs_changes(
assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
});
let new_buffer_a = project_a
.update(cx_a, |p, cx| p.create_buffer("", None, cx))
.unwrap();
let new_buffer_id = new_buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id());
let new_buffer_b = project_b
.update(cx_b, |p, cx| p.open_buffer_by_id(new_buffer_id, cx))
.await
.unwrap();
new_buffer_b.read_with(cx_b, |buffer, _| {
assert!(buffer.file().is_none());
});
new_buffer_a.update(cx_a, |buffer, cx| {
buffer.edit([(0..0, "ok")], None, cx);
});
project_a
.update(cx_a, |project, cx| {
project.save_buffer_as(new_buffer_a.clone(), "/a/file3.rs".into(), cx)
})
.await
.unwrap();
deterministic.run_until_parked();
new_buffer_b.read_with(cx_b, |buffer_b, _| {
assert_eq!(
buffer_b.file().unwrap().path().as_ref(),
Path::new("file3.rs")
);
new_buffer_a.read_with(cx_a, |buffer_a, _| {
assert_eq!(buffer_b.saved_mtime(), buffer_a.saved_mtime());
assert_eq!(buffer_b.saved_version(), buffer_a.saved_version());
});
});
}
#[gpui::test(iterations = 10)]
@@ -2571,6 +2829,8 @@ async fn test_fs_operations(
})
.await
.unwrap();
deterministic.run_until_parked();
worktree_a.read_with(cx_a, |worktree, _| {
assert_eq!(
worktree
@@ -2659,7 +2919,12 @@ async fn test_buffer_conflict_after_save(
assert!(!buf.has_conflict());
});
buffer_b.update(cx_b, |buf, cx| buf.save(cx)).await.unwrap();
project_b
.update(cx_b, |project, cx| {
project.save_buffer(buffer_b.clone(), cx)
})
.await
.unwrap();
cx_a.foreground().forbid_parking();
buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
buffer_b.read_with(cx_b, |buf, _| {
@@ -5291,6 +5556,27 @@ async fn test_contacts(
[("user_b".to_string(), "online", "free")]
);
// Test removing a contact
client_b
.user_store
.update(cx_b, |store, cx| {
store.remove_contact(client_c.user_id().unwrap(), cx)
})
.await
.unwrap();
deterministic.run_until_parked();
assert_eq!(
contacts(&client_b, cx_b),
[
("user_a".to_string(), "offline", "free"),
("user_d".to_string(), "online", "free")
]
);
assert_eq!(
contacts(&client_c, cx_c),
[("user_a".to_string(), "offline", "free"),]
);
fn contacts(
client: &TestClient,
cx: &TestAppContext,
@@ -5500,6 +5786,7 @@ async fn test_following(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
) {
deterministic.forbid_parking();
cx_a.update(editor::init);
@@ -5508,9 +5795,13 @@ async fn test_following(
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
server
.make_contacts(&mut [(&client_a, cx_a), (&client_c, cx_c)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
@@ -5541,8 +5832,10 @@ async fn test_following(
.await
.unwrap();
// Client A opens some editors.
let workspace_a = client_a.build_workspace(&project_a, cx_a);
let workspace_b = client_b.build_workspace(&project_b, cx_b);
// Client A opens some editors.
let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
let editor_a1 = workspace_a
.update(cx_a, |workspace, cx| {
@@ -5562,7 +5855,6 @@ async fn test_following(
.unwrap();
// Client B opens an editor.
let workspace_b = client_b.build_workspace(&project_b, cx_b);
let editor_b1 = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
@@ -5572,29 +5864,97 @@ async fn test_following(
.downcast::<Editor>()
.unwrap();
let client_a_id = project_b.read_with(cx_b, |project, _| {
project.collaborators().values().next().unwrap().peer_id
});
let client_b_id = project_a.read_with(cx_a, |project, _| {
project.collaborators().values().next().unwrap().peer_id
});
let peer_id_a = client_a.peer_id().unwrap();
let peer_id_b = client_b.peer_id().unwrap();
let peer_id_c = client_c.peer_id().unwrap();
// When client B starts following client A, all visible view states are replicated to client B.
// Client A updates their selections in those editors
editor_a1.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
});
editor_a2.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
});
// When client B starts following client A, all visible view states are replicated to client B.
workspace_b
.update(cx_b, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(client_a_id), cx)
.toggle_follow(&ToggleFollow(peer_id_a), cx)
.unwrap()
})
.await
.unwrap();
// Client A invites client C to the call.
active_call_a
.update(cx_a, |call, cx| {
call.invite(client_c.current_user_id(cx_c).to_proto(), None, cx)
})
.await
.unwrap();
cx_c.foreground().run_until_parked();
let active_call_c = cx_c.read(ActiveCall::global);
active_call_c
.update(cx_c, |call, cx| call.accept_incoming(cx))
.await
.unwrap();
let project_c = client_c.build_remote_project(project_id, cx_c).await;
let workspace_c = client_c.build_workspace(&project_c, cx_c);
active_call_c
.update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
.await
.unwrap();
// Client C also follows client A.
workspace_c
.update(cx_c, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(peer_id_a), cx)
.unwrap()
})
.await
.unwrap();
// All clients see that clients B and C are following client A.
cx_c.foreground().run_until_parked();
for (name, active_call, cx) in [
("A", &active_call_a, &cx_a),
("B", &active_call_b, &cx_b),
("C", &active_call_c, &cx_c),
] {
active_call.read_with(*cx, |call, cx| {
let room = call.room().unwrap().read(cx);
assert_eq!(
room.followers_for(peer_id_a),
&[peer_id_b, peer_id_c],
"checking followers for A as {name}"
);
});
}
// Client C unfollows client A.
workspace_c.update(cx_c, |workspace, cx| {
workspace.toggle_follow(&ToggleFollow(peer_id_a), cx);
});
// All clients see that clients B is following client A.
cx_c.foreground().run_until_parked();
for (name, active_call, cx) in [
("A", &active_call_a, &cx_a),
("B", &active_call_b, &cx_b),
("C", &active_call_c, &cx_c),
] {
active_call.read_with(*cx, |call, cx| {
let room = call.room().unwrap().read(cx);
assert_eq!(
room.followers_for(peer_id_a),
&[peer_id_b],
"checking followers for A as {name}"
);
});
}
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
workspace
.active_item(cx)
@@ -5602,7 +5962,6 @@ async fn test_following(
.downcast::<Editor>()
.unwrap()
});
assert!(cx_b.read(|cx| editor_b2.is_focused(cx)));
assert_eq!(
cx_b.read(|cx| editor_b2.project_path(cx)),
Some((worktree_id, "2.txt").into())
@@ -5748,14 +6107,14 @@ async fn test_following(
workspace_a
.update(cx_a, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(client_b_id), cx)
.toggle_follow(&ToggleFollow(peer_id_b), cx)
.unwrap()
})
.await
.unwrap();
assert_eq!(
workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
Some(client_b_id)
Some(peer_id_b)
);
assert_eq!(
workspace_a.read_with(cx_a, |workspace, cx| workspace

View File

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

View File

@@ -22,6 +22,7 @@ test-support = [
]
[dependencies]
auto_update = { path = "../auto_update" }
call = { path = "../call" }
client = { path = "../client" }
clock = { path = "../clock" }

View File

@@ -1,5 +1,9 @@
use crate::{contact_notification::ContactNotification, contacts_popover};
use call::{ActiveCall, ParticipantLocation};
use crate::{
collaborator_list_popover, collaborator_list_popover::CollaboratorListPopover,
contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
ToggleScreenSharing,
};
use call::{ActiveCall, ParticipantLocation, Room};
use client::{proto::PeerId, Authenticate, ContactEventKind, User, UserStore};
use clock::ReplicaId;
use contacts_popover::ContactsPopover;
@@ -8,30 +12,52 @@ use gpui::{
color::Color,
elements::*,
geometry::{rect::RectF, vector::vec2f, PathBuilder},
impl_internal_actions,
json::{self, ToJson},
Border, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
CursorStyle, Entity, ImageData, ModelHandle, MouseButton, MutableAppContext, RenderContext,
Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
};
use settings::Settings;
use std::ops::Range;
use theme::Theme;
use std::{ops::Range, sync::Arc};
use theme::{AvatarStyle, Theme};
use util::ResultExt;
use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace};
actions!(
collab,
[ToggleCollaborationMenu, ToggleScreenSharing, ShareProject]
[
ToggleCollaboratorList,
ToggleContactsMenu,
ShareProject,
UnshareProject
]
);
impl_internal_actions!(collab, [LeaveCall]);
#[derive(Copy, Clone, PartialEq)]
pub(crate) struct LeaveCall;
#[derive(PartialEq, Eq)]
enum ContactsPopoverSide {
Left,
Right,
}
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(CollabTitlebarItem::toggle_collaborator_list_popover);
cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
cx.add_action(CollabTitlebarItem::toggle_screen_sharing);
cx.add_action(CollabTitlebarItem::share_project);
cx.add_action(CollabTitlebarItem::unshare_project);
cx.add_action(CollabTitlebarItem::leave_call);
}
pub struct CollabTitlebarItem {
workspace: WeakViewHandle<Workspace>,
user_store: ModelHandle<UserStore>,
contacts_popover: Option<ViewHandle<ContactsPopover>>,
contacts_popover_side: ContactsPopoverSide,
collaborator_list_popover: Option<ViewHandle<CollaboratorListPopover>>,
_subscriptions: Vec<Subscription>,
}
@@ -51,27 +77,71 @@ impl View for CollabTitlebarItem {
return Empty::new().boxed();
};
let theme = cx.global::<Settings>().theme.clone();
let mut container = Flex::row();
container.add_children(self.render_toggle_screen_sharing_button(&theme, cx));
if workspace.read(cx).client().status().borrow().is_connected() {
let project = workspace.read(cx).project().read(cx);
if project.is_shared()
|| project.is_remote()
|| ActiveCall::global(cx).read(cx).room().is_none()
{
container.add_child(self.render_toggle_contacts_button(&theme, cx));
} else {
container.add_child(self.render_share_button(&theme, cx));
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);
}
container.add_children(self.render_collaborators(&workspace, &theme, cx));
container.add_children(self.render_current_user(&workspace, &theme, cx));
container.add_children(self.render_connection_status(&workspace, cx));
container.boxed()
if project_title.is_empty() {
project_title = "empty project".to_owned();
}
let theme = cx.global::<Settings>().theme.clone();
let user = workspace.read(cx).user_store().read(cx).current_user();
let mut left_container = Flex::row();
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(),
);
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
left_container.add_child(self.render_current_user(&workspace, &theme, &user, cx));
left_container.add_children(self.render_collaborators(&workspace, &theme, room, cx));
left_container.add_child(self.render_toggle_contacts_button(&theme, cx));
}
let mut right_container = Flex::row();
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
right_container.add_child(self.render_leave_call_button(&theme, cx));
right_container
.add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
} else {
right_container.add_child(self.render_outside_call_share_button(&theme, cx));
}
right_container.add_children(self.render_connection_status(&workspace, cx));
if let Some(user) = user {
//TODO: Add style
right_container.add_child(
Label::new(
user.github_login.clone(),
theme.workspace.titlebar.title.clone(),
)
.aligned()
.contained()
.with_margin_left(theme.workspace.titlebar.item_spacing)
.boxed(),
);
} else {
right_container.add_child(Self::render_authenticate(&theme, cx));
}
Stack::new()
.with_child(left_container.boxed())
.with_child(right_container.aligned().right().boxed())
.boxed()
}
}
@@ -84,7 +154,7 @@ impl CollabTitlebarItem {
let active_call = ActiveCall::global(cx);
let mut subscriptions = Vec::new();
subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify()));
subscriptions.push(cx.observe(&active_call, |_, _, 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)
}));
@@ -116,6 +186,8 @@ impl CollabTitlebarItem {
workspace: workspace.downgrade(),
user_store: user_store.clone(),
contacts_popover: None,
contacts_popover_side: ContactsPopoverSide::Right,
collaborator_list_popover: None,
_subscriptions: subscriptions,
}
}
@@ -133,6 +205,13 @@ impl CollabTitlebarItem {
}
}
fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
if ActiveCall::global(cx).read(cx).room().is_none() {
self.contacts_popover = None;
}
cx.notify();
}
fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
if let Some(workspace) = self.workspace.upgrade(cx) {
let active_call = ActiveCall::global(cx);
@@ -143,46 +222,79 @@ impl CollabTitlebarItem {
}
}
pub fn toggle_contacts_popover(
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,
_: &ToggleCollaborationMenu,
_: &ToggleCollaboratorList,
cx: &mut ViewContext<Self>,
) {
match self.contacts_popover.take() {
match self.collaborator_list_popover.take() {
Some(_) => {}
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));
let view = cx.add_view(|cx| CollaboratorListPopover::new(user_store, cx));
cx.subscribe(&view, |this, _, event, cx| {
match event {
contacts_popover::Event::Dismissed => {
this.contacts_popover = None;
collaborator_list_popover::Event::Dismissed => {
this.collaborator_list_popover = None;
}
}
cx.notify();
})
.detach();
self.contacts_popover = Some(view);
self.collaborator_list_popover = Some(view);
}
}
}
cx.notify();
}
pub fn toggle_screen_sharing(&mut self, _: &ToggleScreenSharing, cx: &mut ViewContext<Self>) {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
let toggle_screen_sharing = room.update(cx, |room, cx| {
if room.is_screen_sharing() {
Task::ready(room.unshare_screen(cx))
} else {
room.share_screen(cx)
}
});
toggle_screen_sharing.detach_and_log_err(cx);
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;
}
}
cx.notify();
})
.detach();
self.contacts_popover_side = match ActiveCall::global(cx).read(cx).room() {
Some(_) => ContactsPopoverSide::Left,
None => ContactsPopoverSide::Right,
};
self.contacts_popover = Some(view);
}
}
cx.notify();
}
fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx))
.log_err();
}
fn render_toggle_contacts_button(
@@ -191,6 +303,7 @@ impl CollabTitlebarItem {
cx: &mut RenderContext<Self>,
) -> ElementBox {
let titlebar = &theme.workspace.titlebar;
let badge = if self
.user_store
.read(cx)
@@ -211,12 +324,15 @@ impl CollabTitlebarItem {
.boxed(),
)
};
Stack::new()
.with_child(
MouseEventHandler::<ToggleCollaborationMenu>::new(0, cx, |state, _| {
let style = titlebar
.toggle_contacts_button
.style_for(state, self.contacts_popover.is_some());
MouseEventHandler::<ToggleContactsMenu>::new(0, cx, |state, _| {
let style = titlebar.toggle_contacts_button.style_for(
state,
self.contacts_popover.is_some()
&& self.contacts_popover_side == ContactsPopoverSide::Left,
);
Svg::new("icons/plus_8.svg")
.with_color(style.color)
.constrained()
@@ -231,39 +347,35 @@ impl CollabTitlebarItem {
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleCollaborationMenu);
cx.dispatch_action(ToggleContactsMenu);
})
.with_tooltip::<ToggleContactsMenu, _>(
0,
"Show contacts menu".into(),
Some(Box::new(ToggleContactsMenu)),
theme.tooltip.clone(),
cx,
)
.aligned()
.boxed(),
)
.with_children(badge)
.with_children(self.contacts_popover.as_ref().map(|popover| {
Overlay::new(
ChildView::new(popover, cx)
.contained()
.with_margin_top(titlebar.height)
.with_margin_left(titlebar.toggle_contacts_button.default.button_width)
.with_margin_right(-titlebar.toggle_contacts_button.default.button_width)
.boxed(),
)
.with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_corner(AnchorCorner::BottomLeft)
.with_z_index(999)
.boxed()
}))
.with_children(self.render_contacts_popover_host(
ContactsPopoverSide::Left,
titlebar,
cx,
))
.boxed()
}
fn render_toggle_screen_sharing_button(
&self,
theme: &Theme,
room: &ModelHandle<Room>,
cx: &mut RenderContext<Self>,
) -> Option<ElementBox> {
let active_call = ActiveCall::global(cx);
let room = active_call.read(cx).room().cloned()?;
) -> ElementBox {
let icon;
let tooltip;
if room.read(cx).is_screen_sharing() {
icon = "icons/disable_screen_sharing_12.svg";
tooltip = "Stop Sharing Screen"
@@ -273,203 +385,409 @@ impl CollabTitlebarItem {
}
let titlebar = &theme.workspace.titlebar;
Some(
MouseEventHandler::<ToggleScreenSharing>::new(0, cx, |state, _| {
let style = titlebar.call_control.style_for(state, false);
Svg::new(icon)
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.constrained()
.with_width(style.button_width)
.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);
})
.with_tooltip::<ToggleScreenSharing, _>(
0,
tooltip.into(),
Some(Box::new(ToggleScreenSharing)),
theme.tooltip.clone(),
cx,
)
.aligned()
.boxed(),
)
}
fn render_share_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
enum Share {}
let titlebar = &theme.workspace.titlebar;
MouseEventHandler::<Share>::new(0, cx, |state, _| {
let style = titlebar.share_button.style_for(state, false);
Label::new("Share".into(), style.text.clone())
MouseEventHandler::<ToggleScreenSharing>::new(0, cx, |state, _| {
let style = titlebar.call_control.style_for(state, false);
Svg::new(icon)
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(ShareProject))
.with_tooltip::<Share, _>(
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleScreenSharing);
})
.with_tooltip::<ToggleScreenSharing, _>(
0,
"Share project with call participants".into(),
None,
tooltip.into(),
Some(Box::new(ToggleScreenSharing)),
theme.tooltip.clone(),
cx,
)
.aligned()
.contained()
.with_margin_left(theme.workspace.titlebar.avatar_margin)
.boxed()
}
fn render_leave_call_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
let titlebar = &theme.workspace.titlebar;
MouseEventHandler::<LeaveCall>::new(0, cx, |state, _| {
let style = titlebar.call_control.style_for(state, false);
Svg::new("icons/leave_12.svg")
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.constrained()
.with_width(style.button_width)
.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(LeaveCall);
})
.with_tooltip::<LeaveCall, _>(
0,
"Leave call".to_owned(),
Some(Box::new(LeaveCall)),
theme.tooltip.clone(),
cx,
)
.contained()
.with_margin_left(theme.workspace.titlebar.item_spacing)
.aligned()
.boxed()
}
fn render_in_call_share_unshare_button(
&self,
workspace: &ViewHandle<Workspace>,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> Option<ElementBox> {
let project = workspace.read(cx).project();
if project.read(cx).is_remote() {
return None;
}
let is_shared = project.read(cx).is_shared();
let label = if is_shared { "Unshare" } else { "Share" };
let tooltip = if is_shared {
"Unshare project from call participants"
} else {
"Share project with call participants"
};
let titlebar = &theme.workspace.titlebar;
enum ShareUnshare {}
Some(
Stack::new()
.with_child(
MouseEventHandler::<ShareUnshare>::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()
&& self.contacts_popover_side == ContactsPopoverSide::Right,
);
Label::new(label, style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
if is_shared {
cx.dispatch_action(UnshareProject);
} else {
cx.dispatch_action(ShareProject);
}
})
.with_tooltip::<ShareUnshare, _>(
0,
tooltip.to_owned(),
None,
theme.tooltip.clone(),
cx,
)
.boxed(),
)
.with_children(self.render_contacts_popover_host(
ContactsPopoverSide::Right,
titlebar,
cx,
))
.aligned()
.contained()
.with_margin_left(theme.workspace.titlebar.item_spacing)
.boxed(),
)
}
fn render_outside_call_share_button(
&self,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let tooltip = "Share project with new call";
let titlebar = &theme.workspace.titlebar;
enum OutsideCallShare {}
Stack::new()
.with_child(
MouseEventHandler::<OutsideCallShare>::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()
&& self.contacts_popover_side == ContactsPopoverSide::Right,
);
Label::new("Share".to_owned(), style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleContactsMenu);
})
.with_tooltip::<OutsideCallShare, _>(
0,
tooltip.to_owned(),
None,
theme.tooltip.clone(),
cx,
)
.boxed(),
)
.with_children(self.render_contacts_popover_host(
ContactsPopoverSide::Right,
titlebar,
cx,
))
.aligned()
.contained()
.with_margin_left(theme.workspace.titlebar.item_spacing)
.boxed()
}
fn render_contacts_popover_host<'a>(
&'a self,
side: ContactsPopoverSide,
theme: &'a theme::Titlebar,
cx: &'a RenderContext<Self>,
) -> impl Iterator<Item = ElementBox> + 'a {
self.contacts_popover
.iter()
.filter(move |_| self.contacts_popover_side == side)
.map(|popover| {
Overlay::new(
ChildView::new(popover, cx)
.contained()
.with_margin_top(theme.height)
.with_margin_left(theme.toggle_contacts_button.default.button_width)
.with_margin_right(-theme.toggle_contacts_button.default.button_width)
.boxed(),
)
.with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_corner(AnchorCorner::BottomLeft)
.with_z_index(999)
.boxed()
})
}
fn render_collaborators(
&self,
workspace: &ViewHandle<Workspace>,
theme: &Theme,
room: ModelHandle<Room>,
cx: &mut RenderContext<Self>,
) -> Vec<ElementBox> {
let active_call = ActiveCall::global(cx);
if let Some(room) = active_call.read(cx).room().cloned() {
let project = workspace.read(cx).project().read(cx);
let mut participants = room
.read(cx)
.remote_participants()
.values()
.cloned()
.collect::<Vec<_>>();
participants.sort_by_key(|p| Some(project.collaborators().get(&p.peer_id)?.replica_id));
participants
.into_iter()
.filter_map(|participant| {
let project = workspace.read(cx).project().read(cx);
let replica_id = project
.collaborators()
.get(&participant.peer_id)
.map(|collaborator| collaborator.replica_id);
let user = participant.user.clone();
Some(self.render_avatar(
let project = workspace.read(cx).project().read(cx);
let mut participants = room
.read(cx)
.remote_participants()
.values()
.cloned()
.collect::<Vec<_>>();
participants.sort_by_key(|p| Some(project.collaborators().get(&p.peer_id)?.replica_id));
participants
.into_iter()
.filter_map(|participant| {
let project = workspace.read(cx).project().read(cx);
let replica_id = project
.collaborators()
.get(&participant.peer_id)
.map(|collaborator| collaborator.replica_id);
let user = participant.user.clone();
Some(
Container::new(self.render_face_pile(
&user,
replica_id,
Some((
participant.peer_id,
&user.github_login,
participant.location,
)),
participant.peer_id,
Some(participant.location),
workspace,
theme,
cx,
))
})
.collect()
} else {
Default::default()
}
.with_margin_left(theme.workspace.titlebar.face_pile_spacing)
.boxed(),
)
})
.collect()
}
fn render_current_user(
&self,
workspace: &ViewHandle<Workspace>,
theme: &Theme,
user: &Option<Arc<User>>,
cx: &mut RenderContext<Self>,
) -> Option<ElementBox> {
let user = workspace.read(cx).user_store().read(cx).current_user();
) -> ElementBox {
let user = user.as_ref().expect("Active call without user");
let replica_id = workspace.read(cx).project().read(cx).replica_id();
let status = *workspace.read(cx).client().status().borrow();
if let Some(user) = user {
Some(self.render_avatar(&user, Some(replica_id), None, workspace, theme, cx))
} else if matches!(status, client::Status::UpgradeRequired) {
None
} else {
Some(
MouseEventHandler::<Authenticate>::new(0, cx, |state, _| {
let style = theme
.workspace
.titlebar
.sign_in_prompt
.style_for(state, false);
Label::new("Sign in".to_string(), style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate))
.with_cursor_style(CursorStyle::PointingHand)
.aligned()
.boxed(),
)
}
let peer_id = workspace
.read(cx)
.client()
.peer_id()
.expect("Active call without peer id");
self.render_face_pile(user, Some(replica_id), peer_id, None, workspace, theme, cx)
}
fn render_avatar(
fn render_authenticate(theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
MouseEventHandler::<Authenticate>::new(0, cx, |state, _| {
let style = theme
.workspace
.titlebar
.sign_in_prompt
.style_for(state, false);
Label::new("Sign in", style.text.clone())
.contained()
.with_style(style.container)
.with_margin_left(theme.workspace.titlebar.item_spacing)
.boxed()
})
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate))
.with_cursor_style(CursorStyle::PointingHand)
.aligned()
.boxed()
}
fn render_face_pile(
&self,
user: &User,
replica_id: Option<ReplicaId>,
peer: Option<(PeerId, &str, ParticipantLocation)>,
peer_id: PeerId,
location: Option<ParticipantLocation>,
workspace: &ViewHandle<Workspace>,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let is_followed = peer.map_or(false, |(peer_id, _, _)| {
workspace.read(cx).is_following(peer_id)
});
let room = ActiveCall::global(cx).read(cx).room();
let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
let followed_by_self = room
.map(|room| {
is_being_followed
&& room
.read(cx)
.followers_for(peer_id)
.iter()
.any(|&follower| Some(follower) == workspace.read(cx).client().peer_id())
})
.unwrap_or(false);
let mut avatar_style;
if let Some((_, _, location)) = peer.as_ref() {
if let ParticipantLocation::SharedProject { project_id } = *location {
let avatar_style;
if let Some(location) = location {
if let ParticipantLocation::SharedProject { project_id } = location {
if Some(project_id) == workspace.read(cx).project().read(cx).remote_id() {
avatar_style = theme.workspace.titlebar.avatar;
avatar_style = &theme.workspace.titlebar.avatar;
} else {
avatar_style = theme.workspace.titlebar.inactive_avatar;
avatar_style = &theme.workspace.titlebar.inactive_avatar;
}
} else {
avatar_style = theme.workspace.titlebar.inactive_avatar;
avatar_style = &theme.workspace.titlebar.inactive_avatar;
}
} else {
avatar_style = theme.workspace.titlebar.avatar;
avatar_style = &theme.workspace.titlebar.avatar;
}
let mut replica_color = None;
let mut background_color = theme
.workspace
.titlebar
.container
.background_color
.unwrap_or_default();
if let Some(replica_id) = replica_id {
let color = theme.editor.replica_selection_style(replica_id).cursor;
replica_color = Some(color);
if is_followed {
avatar_style.border = Border::all(1.0, color);
if followed_by_self {
let selection = theme.editor.replica_selection_style(replica_id).selection;
background_color = Color::blend(selection, background_color);
background_color.a = 255;
}
}
let content = Stack::new()
.with_children(user.avatar.as_ref().map(|avatar| {
Image::new(avatar.clone())
.with_style(avatar_style)
.constrained()
.with_width(theme.workspace.titlebar.avatar_width)
.aligned()
.boxed()
let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap)
.with_child(Self::render_face(
avatar.clone(),
avatar_style.clone(),
background_color,
))
.with_children(
(|| {
let room = room?.read(cx);
let followers = room.followers_for(peer_id);
Some(followers.into_iter().flat_map(|&follower| {
let avatar = room
.remote_participant_for_peer_id(follower)
.and_then(|participant| participant.user.avatar.clone())
.or_else(|| {
if follower == workspace.read(cx).client().peer_id()? {
workspace
.read(cx)
.user_store()
.read(cx)
.current_user()?
.avatar
.clone()
} else {
None
}
})?;
Some(Self::render_face(
avatar.clone(),
theme.workspace.titlebar.follower_avatar.clone(),
background_color,
))
}))
})()
.into_iter()
.flatten(),
);
let mut container = face_pile
.contained()
.with_style(theme.workspace.titlebar.leader_selection);
if let Some(replica_id) = replica_id {
if followed_by_self {
let color = theme.editor.replica_selection_style(replica_id).selection;
container = container.with_background_color(color);
}
}
container.boxed()
}))
.with_children(replica_color.map(|replica_color| {
AvatarRibbon::new(replica_color)
.constrained()
.with_width(theme.workspace.titlebar.avatar_ribbon.width)
.with_height(theme.workspace.titlebar.avatar_ribbon.height)
.aligned()
.bottom()
.boxed()
}))
.constrained()
.with_width(theme.workspace.titlebar.avatar_width)
.contained()
.with_margin_left(theme.workspace.titlebar.avatar_margin)
.with_children((|| {
let replica_id = replica_id?;
let color = theme.editor.replica_selection_style(replica_id).cursor;
Some(
AvatarRibbon::new(color)
.constrained()
.with_width(theme.workspace.titlebar.avatar_ribbon.width)
.with_height(theme.workspace.titlebar.avatar_ribbon.height)
.aligned()
.bottom()
.boxed(),
)
})())
.boxed();
if let Some((peer_id, peer_github_login, location)) = peer {
if let Some(location) = location {
if let Some(replica_id) = replica_id {
MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content)
.with_cursor_style(CursorStyle::PointingHand)
@@ -478,10 +796,10 @@ impl CollabTitlebarItem {
})
.with_tooltip::<ToggleFollow, _>(
peer_id.as_u64() as usize,
if is_followed {
format!("Unfollow {}", peer_github_login)
if is_being_followed {
format!("Unfollow {}", user.github_login)
} else {
format!("Follow {}", peer_github_login)
format!("Follow {}", user.github_login)
},
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
@@ -502,7 +820,7 @@ impl CollabTitlebarItem {
})
.with_tooltip::<JoinProject, _>(
peer_id.as_u64() as usize,
format!("Follow {} into external project", peer_github_login),
format!("Follow {} into external project", user.github_login),
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
@@ -516,12 +834,32 @@ impl CollabTitlebarItem {
}
}
fn render_face(
avatar: Arc<ImageData>,
avatar_style: AvatarStyle,
background_color: Color,
) -> ElementBox {
Image::new(avatar)
.with_style(avatar_style.image)
.aligned()
.contained()
.with_background_color(background_color)
.with_corner_radius(avatar_style.outer_corner_radius)
.constrained()
.with_width(avatar_style.outer_width)
.with_height(avatar_style.outer_width)
.aligned()
.boxed()
}
fn render_connection_status(
&self,
workspace: &ViewHandle<Workspace>,
cx: &mut RenderContext<Self>,
) -> Option<ElementBox> {
let theme = &cx.global::<Settings>().theme;
enum ConnectionStatusButton {}
let theme = &cx.global::<Settings>().theme.clone();
match &*workspace.read(cx).client().status().borrow() {
client::Status::ConnectionError
| client::Status::ConnectionLost
@@ -544,13 +882,20 @@ impl CollabTitlebarItem {
.boxed(),
),
client::Status::UpgradeRequired => Some(
Label::new(
"Please update Zed to collaborate".to_string(),
theme.workspace.titlebar.outdated_warning.text.clone(),
)
.contained()
.with_style(theme.workspace.titlebar.outdated_warning.container)
.aligned()
MouseEventHandler::<ConnectionStatusButton>::new(0, cx, |_, _| {
Label::new(
"Please update Zed to collaborate",
theme.workspace.titlebar.outdated_warning.text.clone(),
)
.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);
})
.boxed(),
),
_ => None,

View File

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

View File

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

View File

@@ -1,39 +1,38 @@
use std::{mem, sync::Arc};
use super::collab_titlebar_item::LeaveCall;
use crate::contacts_popover;
use call::ActiveCall;
use client::{proto::PeerId, Contact, User, UserStore};
use editor::{Cancel, Editor};
use futures::StreamExt;
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
elements::*,
geometry::{rect::RectF, vector::vec2f},
impl_actions, impl_internal_actions,
keymap_matcher::KeymapContext,
AppContext, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
Subscription, View, ViewContext, ViewHandle,
AppContext, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, PromptLevel,
RenderContext, Subscription, View, ViewContext, ViewHandle,
};
use menu::{Confirm, SelectNext, SelectPrev};
use project::Project;
use serde::Deserialize;
use settings::Settings;
use std::{mem, sync::Arc};
use theme::IconButton;
use util::ResultExt;
use workspace::{JoinProject, OpenSharedScreen};
impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]);
impl_internal_actions!(contact_list, [ToggleExpanded, Call, LeaveCall]);
impl_internal_actions!(contact_list, [ToggleExpanded, Call]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContactList::remove_contact);
cx.add_action(ContactList::respond_to_contact_request);
cx.add_action(ContactList::clear_filter);
cx.add_action(ContactList::cancel);
cx.add_action(ContactList::select_next);
cx.add_action(ContactList::select_prev);
cx.add_action(ContactList::confirm);
cx.add_action(ContactList::toggle_expanded);
cx.add_action(ContactList::call);
cx.add_action(ContactList::leave_call);
}
#[derive(Clone, PartialEq)]
@@ -45,9 +44,6 @@ struct Call {
initial_project: Option<ModelHandle<Project>>,
}
#[derive(Copy, Clone, PartialEq)]
struct LeaveCall;
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
enum Section {
ActiveCall,
@@ -299,9 +295,19 @@ impl ContactList {
}
fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext<Self>) {
self.user_store
.update(cx, |store, cx| store.remove_contact(request.0, cx))
.detach();
let user_id = request.0;
let user_store = self.user_store.clone();
let prompt_message = "Are you sure you want to remove this contact?";
let mut answer = cx.prompt(PromptLevel::Warning, prompt_message, &["Remove", "Cancel"]);
cx.spawn(|_, mut cx| async move {
if answer.next().await == Some(0) {
user_store
.update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
.await
.unwrap();
}
})
.detach();
}
fn respond_to_contact_request(
@@ -316,7 +322,7 @@ impl ContactList {
.detach();
}
fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
let did_clear = self.filter_editor.update(cx, |editor, cx| {
if editor.buffer().read(cx).len(cx) > 0 {
editor.set_text("", cx);
@@ -325,6 +331,7 @@ impl ContactList {
false
}
});
if !did_clear {
cx.emit(Event::Dismissed);
}
@@ -739,7 +746,7 @@ impl ContactList {
)
.with_children(if is_pending {
Some(
Label::new("Calling".to_string(), theme.calling_indicator.text.clone())
Label::new("Calling", theme.calling_indicator.text.clone())
.contained()
.with_style(theme.calling_indicator.container)
.aligned()
@@ -940,7 +947,7 @@ impl ContactList {
.boxed(),
)
.with_child(
Label::new("Screen".into(), row.name.text.clone())
Label::new("Screen", row.name.text.clone())
.aligned()
.left()
.contained()
@@ -970,6 +977,7 @@ impl ContactList {
cx: &mut RenderContext<Self>,
) -> ElementBox {
enum Header {}
enum LeaveCallContactList {}
let header_style = theme
.header_row
@@ -982,9 +990,9 @@ impl ContactList {
};
let leave_call = if section == Section::ActiveCall {
Some(
MouseEventHandler::<LeaveCall>::new(0, cx, |state, _| {
MouseEventHandler::<LeaveCallContactList>::new(0, cx, |state, _| {
let style = theme.leave_call.style_for(state, false);
Label::new("Leave Session".into(), style.text.clone())
Label::new("Leave Call", style.text.clone())
.contained()
.with_style(style.container)
.boxed()
@@ -1016,7 +1024,7 @@ impl ContactList {
.boxed(),
)
.with_child(
Label::new(text.to_string(), header_style.text.clone())
Label::new(text, header_style.text.clone())
.aligned()
.left()
.contained()
@@ -1051,7 +1059,7 @@ impl ContactList {
let user_id = contact.user.id;
let initial_project = project.clone();
let mut element =
MouseEventHandler::<Contact>::new(contact.user.id as usize, cx, |_, _| {
MouseEventHandler::<Contact>::new(contact.user.id as usize, cx, |_, cx| {
Flex::row()
.with_children(contact.user.avatar.clone().map(|avatar| {
let status_badge = if contact.online {
@@ -1093,9 +1101,30 @@ impl ContactList {
.flex(1., true)
.boxed(),
)
.with_child(
MouseEventHandler::<Cancel>::new(
contact.user.id as usize,
cx,
|mouse_state, _| {
let button_style =
theme.contact_button.style_for(mouse_state, false);
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))
})
.flex_float()
.boxed(),
)
.with_children(if calling {
Some(
Label::new("Calling".to_string(), theme.calling_indicator.text.clone())
Label::new("Calling", theme.calling_indicator.text.clone())
.contained()
.with_style(theme.calling_indicator.container)
.aligned()
@@ -1252,12 +1281,6 @@ impl ContactList {
})
.detach_and_log_err(cx);
}
fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx))
.log_err();
}
}
impl Entity for ContactList {
@@ -1303,7 +1326,7 @@ impl View for ContactList {
})
.with_tooltip::<AddContact, _>(
0,
"Add contact".into(),
"Search for new contact".into(),
None,
theme.tooltip.clone(),
cx,

View File

@@ -48,7 +48,7 @@ impl View for ContactNotification {
ContactEventKind::Requested => render_user_notification(
self.user.clone(),
"wants to add you as a contact",
Some("They won't know if you decline."),
Some("They won't be alerted if you decline."),
Dismiss(self.user.id),
vec![
(

View File

@@ -1,4 +1,4 @@
use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleCollaborationMenu};
use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleContactsMenu};
use client::UserStore;
use gpui::{
actions, elements::*, ClipboardItem, CursorStyle, Entity, ModelHandle, MouseButton,
@@ -155,7 +155,7 @@ impl View for ContactsPopover {
.boxed()
})
.on_down_out(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleCollaborationMenu);
cx.dispatch_action(ToggleContactsMenu);
})
.boxed()
}

View File

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

View File

@@ -32,11 +32,12 @@ pub fn init(cx: &mut MutableAppContext) {
});
for screen in cx.platform().screens() {
let screen_size = screen.size();
let screen_bounds = screen.bounds();
let (window_id, _) = cx.add_window(
WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
screen_bounds.upper_right()
- vec2f(PADDING + window_size.x(), PADDING),
window_size,
)),
titlebar: None,
@@ -171,7 +172,7 @@ impl IncomingCallNotification {
.with_child(
MouseEventHandler::<Accept>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
Label::new("Accept".to_string(), theme.accept_button.text.clone())
Label::new("Accept", theme.accept_button.text.clone())
.aligned()
.contained()
.with_style(theme.accept_button.container)
@@ -187,7 +188,7 @@ impl IncomingCallNotification {
.with_child(
MouseEventHandler::<Decline>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
Label::new("Decline".to_string(), theme.decline_button.text.clone())
Label::new("Decline", theme.decline_button.text.clone())
.aligned()
.contained()
.with_style(theme.decline_button.container)

View File

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

View File

@@ -31,11 +31,11 @@ pub fn init(cx: &mut MutableAppContext) {
let window_size = vec2f(theme.window_width, theme.window_height);
for screen in cx.platform().screens() {
let screen_size = screen.size();
let screen_bounds = screen.bounds();
let (window_id, _) = cx.add_window(
WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
screen_bounds.upper_right() - vec2f(PADDING + window_size.x(), PADDING),
window_size,
)),
titlebar: None,
@@ -175,7 +175,7 @@ impl ProjectSharedNotification {
.with_child(
MouseEventHandler::<Open>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.project_shared_notification;
Label::new("Open".to_string(), theme.open_button.text.clone())
Label::new("Open", theme.open_button.text.clone())
.aligned()
.contained()
.with_style(theme.open_button.container)
@@ -194,7 +194,7 @@ impl ProjectSharedNotification {
.with_child(
MouseEventHandler::<Dismiss>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.project_shared_notification;
Label::new("Dismiss".to_string(), theme.dismiss_button.text.clone())
Label::new("Dismiss", theme.dismiss_button.text.clone())
.aligned()
.contained()
.with_style(theme.dismiss_button.container)

View File

@@ -0,0 +1,59 @@
use call::ActiveCall;
use gpui::{
color::Color,
elements::{MouseEventHandler, Svg},
Appearance, Element, ElementBox, Entity, MouseButton, MutableAppContext, RenderContext, View,
};
use settings::Settings;
use crate::ToggleScreenSharing;
pub fn init(cx: &mut MutableAppContext) {
let active_call = ActiveCall::global(cx);
let mut status_indicator = None;
cx.observe(&active_call, move |call, cx| {
if let Some(room) = call.read(cx).room() {
if room.read(cx).is_screen_sharing() {
if status_indicator.is_none() && cx.global::<Settings>().show_call_status_icon {
status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator));
}
} else if let Some((window_id, _)) = status_indicator.take() {
cx.remove_status_bar_item(window_id);
}
}
})
.detach();
}
pub struct SharingStatusIndicator;
impl Entity for SharingStatusIndicator {
type Event = ();
}
impl View for SharingStatusIndicator {
fn ui_name() -> &'static str {
"SharingStatusIndicator"
}
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
let color = match cx.appearance {
Appearance::Light | Appearance::VibrantLight => Color::black(),
Appearance::Dark | Appearance::VibrantDark => Color::white(),
};
MouseEventHandler::<Self>::new(0, cx, |_, _| {
Svg::new("icons/disable_screen_sharing_12.svg")
.with_color(color)
.constrained()
.with_width(18.)
.aligned()
.boxed()
})
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(ToggleScreenSharing);
})
.boxed()
}
}

View File

@@ -65,7 +65,7 @@ impl CommandPalette {
action,
keystrokes: bindings
.iter()
.filter_map(|binding| binding.keystrokes())
.map(|binding| binding.keystrokes())
.last()
.map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
})
@@ -257,7 +257,7 @@ impl PickerDelegate for CommandPalette {
.filter_map(|(modifier, label)| {
if modifier {
Some(
Label::new(label.into(), key_style.label.clone())
Label::new(label, key_style.label.clone())
.contained()
.with_style(key_style.container)
.boxed(),

View File

@@ -63,6 +63,7 @@ pub struct ContextMenu {
visible: bool,
previously_focused_view_id: Option<usize>,
clicked: bool,
parent_view_id: usize,
_actions_observation: Subscription,
}
@@ -114,6 +115,8 @@ impl View for ContextMenu {
impl ContextMenu {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
let parent_view_id = cx.parent().unwrap();
Self {
show_count: 0,
anchor_position: Default::default(),
@@ -123,6 +126,7 @@ impl ContextMenu {
visible: Default::default(),
previously_focused_view_id: Default::default(),
clicked: false,
parent_view_id,
_actions_observation: cx.observe_actions(Self::action_dispatched),
}
}
@@ -251,6 +255,7 @@ impl ContextMenu {
}
fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element {
let window_id = cx.window_id();
let style = cx.global::<Settings>().theme.context_menu.clone();
Flex::row()
.with_child(
@@ -289,6 +294,8 @@ impl ContextMenu {
Some(ix) == self.selected_index,
);
KeystrokeLabel::new(
window_id,
self.parent_view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),
@@ -318,6 +325,7 @@ impl ContextMenu {
let style = cx.global::<Settings>().theme.context_menu.clone();
let window_id = cx.window_id();
MouseEventHandler::<Menu>::new(0, cx, |_, cx| {
Flex::column()
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
@@ -337,6 +345,8 @@ impl ContextMenu {
)
.with_child({
KeystrokeLabel::new(
window_id,
self.parent_view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),

View File

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

View File

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

View File

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

View File

@@ -337,7 +337,7 @@ impl DisplaySnapshot {
.map(|h| h.text)
}
// Returns text chunks starting at the end of the given display row in reverse until the start of the file
/// Returns text chunks starting at the end of the given display row in reverse until the start of the file
pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
(0..=display_row).into_iter().rev().flat_map(|row| {
self.blocks_snapshot
@@ -411,6 +411,67 @@ impl DisplaySnapshot {
})
}
/// Returns an iterator of the start positions of the occurances of `target` in the `self` after `from`
/// Stops if `condition` returns false for any of the character position pairs observed.
pub fn find_while<'a>(
&'a self,
from: DisplayPoint,
target: &str,
condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
) -> impl Iterator<Item = DisplayPoint> + 'a {
Self::find_internal(self.chars_at(from), target.chars().collect(), condition)
}
/// Returns an iterator of the end positions of the occurances of `target` in the `self` before `from`
/// Stops if `condition` returns false for any of the character position pairs observed.
pub fn reverse_find_while<'a>(
&'a self,
from: DisplayPoint,
target: &str,
condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
) -> impl Iterator<Item = DisplayPoint> + 'a {
Self::find_internal(
self.reverse_chars_at(from),
target.chars().rev().collect(),
condition,
)
}
fn find_internal<'a>(
iterator: impl Iterator<Item = (char, DisplayPoint)> + 'a,
target: Vec<char>,
mut condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
) -> impl Iterator<Item = DisplayPoint> + 'a {
// List of partial matches with the index of the last seen character in target and the starting point of the match
let mut partial_matches: Vec<(usize, DisplayPoint)> = Vec::new();
iterator
.take_while(move |(ch, point)| condition(*ch, *point))
.filter_map(move |(ch, point)| {
if Some(&ch) == target.get(0) {
partial_matches.push((0, point));
}
let mut found = None;
// Keep partial matches that have the correct next character
partial_matches.retain_mut(|(match_position, match_start)| {
if target.get(*match_position) == Some(&ch) {
*match_position += 1;
if *match_position == target.len() {
found = Some(match_start.clone());
// This match is completed. No need to keep tracking it
false
} else {
true
}
} else {
false
}
});
found
})
}
pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
let mut count = 0;
let mut column = 0;
@@ -627,7 +688,7 @@ pub mod tests {
use smol::stream::StreamExt;
use std::{env, sync::Arc};
use theme::SyntaxTheme;
use util::test::{marked_text_ranges, sample_text};
use util::test::{marked_text_offsets, marked_text_ranges, sample_text};
use Bias::*;
#[gpui::test(iterations = 100)]
@@ -1418,6 +1479,32 @@ pub mod tests {
)
}
#[test]
fn test_find_internal() {
assert("This is a ˇtest of find internal", "test");
assert("Some text ˇaˇaˇaa with repeated characters", "aa");
fn assert(marked_text: &str, target: &str) {
let (text, expected_offsets) = marked_text_offsets(marked_text);
let chars = text
.chars()
.enumerate()
.map(|(index, ch)| (ch, DisplayPoint::new(0, index as u32)));
let target = target.chars();
assert_eq!(
expected_offsets
.into_iter()
.map(|offset| offset as u32)
.collect::<Vec<_>>(),
DisplaySnapshot::find_internal(chars, target.collect(), |_, _| true)
.map(|point| point.column())
.collect::<Vec<_>>()
)
}
}
fn syntax_chunks<'a>(
rows: Range<u32>,
map: &ModelHandle<DisplayMap>,

View File

@@ -77,14 +77,14 @@ use std::{
cmp::{self, Ordering, Reverse},
mem,
num::NonZeroU32,
ops::{Deref, DerefMut, Range, RangeInclusive},
ops::{Deref, DerefMut, Range},
path::Path,
sync::Arc,
time::{Duration, Instant},
};
pub use sum_tree::Bias;
use theme::{DiagnosticStyle, Theme};
use util::{post_inc, ResultExt, TryFutureExt};
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::{ItemNavHistory, ViewId, Workspace, WorkspaceId};
use crate::git::diff_hunk_to_display;
@@ -154,6 +154,12 @@ pub struct ConfirmCodeAction {
pub item_ix: Option<usize>,
}
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct ToggleComments {
#[serde(default)]
pub advance_downwards: bool,
}
actions!(
editor,
[
@@ -216,7 +222,6 @@ actions!(
AddSelectionBelow,
Tab,
TabPrev,
ToggleComments,
ShowCharacterPalette,
SelectLargerSyntaxNode,
SelectSmallerSyntaxNode,
@@ -236,6 +241,8 @@ actions!(
RestartLanguageServer,
Hover,
Format,
ToggleSoftWrap,
RevealInFinder
]
);
@@ -250,6 +257,7 @@ impl_actions!(
MovePageDown,
ConfirmCompletion,
ConfirmCodeAction,
ToggleComments,
]
);
@@ -346,6 +354,8 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(Editor::toggle_code_actions);
cx.add_action(Editor::open_excerpts);
cx.add_action(Editor::jump);
cx.add_action(Editor::toggle_soft_wrap);
cx.add_action(Editor::reveal_in_finder);
cx.add_async_action(Editor::format);
cx.add_action(Editor::restart_language_server);
cx.add_action(Editor::show_character_palette);
@@ -400,7 +410,7 @@ pub enum SelectMode {
All,
}
#[derive(Copy, Clone, PartialEq, Eq)]
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum EditorMode {
SingleLine,
AutoHeight { max_lines: usize },
@@ -810,7 +820,7 @@ impl CompletionsMenu {
fuzzy::match_strings(
&self.match_candidates,
query,
false,
query.chars().any(|c| c.is_uppercase()),
100,
&Default::default(),
executor,
@@ -1732,11 +1742,13 @@ impl Editor {
}
pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
let text: Arc<str> = text.into();
if !self.input_enabled {
cx.emit(Event::InputIgnored { text });
return;
}
let text: Arc<str> = text.into();
let selections = self.selections.all_adjusted(cx);
let mut edits = Vec::new();
let mut new_selections = Vec::with_capacity(selections.len());
@@ -1751,8 +1763,8 @@ impl Editor {
// bracket of any of this language's bracket pairs.
let mut bracket_pair = None;
let mut is_bracket_pair_start = false;
for pair in language.brackets() {
if pair.close && pair.start.ends_with(text.as_ref()) {
for (pair, enabled) in language.brackets() {
if enabled && pair.close && pair.start.ends_with(text.as_ref()) {
bracket_pair = Some(pair.clone());
is_bracket_pair_start = true;
break;
@@ -1814,9 +1826,9 @@ impl Editor {
}
}
}
// If an opening bracket is typed while text is selected, then
// surround that text with the bracket pair.
else if is_bracket_pair_start {
// If an opening bracket is 1 character long and is typed while
// text is selected, then surround that text with the bracket pair.
else if is_bracket_pair_start && bracket_pair.start.chars().count() == 1 {
edits.push((selection.start..selection.start, text.clone()));
edits.push((
selection.end..selection.end,
@@ -1920,11 +1932,12 @@ impl Editor {
.map(|c| c.len_utf8())
.sum::<usize>();
insert_extra_newline = language.brackets().iter().any(|pair| {
insert_extra_newline = language.brackets().any(|(pair, enabled)| {
let pair_start = pair.start.trim_end();
let pair_end = pair.end.trim_start();
pair.newline
enabled
&& pair.newline
&& buffer
.contains_str_at(end + trailing_whitespace_len, pair_end)
&& buffer.contains_str_at(
@@ -3800,7 +3813,7 @@ impl Editor {
}
}
if matches!(self.mode, EditorMode::SingleLine) {
if self.mode == EditorMode::SingleLine {
cx.propagate_action();
return;
}
@@ -4462,7 +4475,7 @@ impl Editor {
}
}
pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext<Self>) {
pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext<Self>) {
self.transact(cx, |this, cx| {
let mut selections = this.selections.all::<Point>(cx);
let mut edits = Vec::new();
@@ -4681,6 +4694,34 @@ impl Editor {
drop(snapshot);
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
let selections = this.selections.all::<Point>(cx);
let selections_on_single_row = selections.windows(2).all(|selections| {
selections[0].start.row == selections[1].start.row
&& selections[0].end.row == selections[1].end.row
&& selections[0].start.row == selections[0].end.row
});
let selections_selecting = selections
.iter()
.any(|selection| selection.start != selection.end);
let advance_downwards = action.advance_downwards
&& selections_on_single_row
&& !selections_selecting
&& this.mode != EditorMode::SingleLine;
if advance_downwards {
let snapshot = this.buffer.read(cx).snapshot(cx);
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|display_snapshot, display_point, _| {
let mut point = display_point.to_point(display_snapshot);
point.row += 1;
point = snapshot.clip_point(point, Bias::Left);
let display_point = point.to_display_point(display_snapshot);
(display_point, SelectionGoal::Column(display_point.column()))
})
});
}
});
}
@@ -4750,27 +4791,54 @@ impl Editor {
_: &MoveToEnclosingBracket,
cx: &mut ViewContext<Self>,
) {
let buffer = self.buffer.read(cx).snapshot(cx);
let mut selections = self.selections.all::<usize>(cx);
for selection in &mut selections {
if let Some((open_range, close_range)) =
buffer.enclosing_bracket_ranges(selection.start..selection.end)
{
let close_range = close_range.to_inclusive();
let destination = if close_range.contains(&selection.start)
&& close_range.contains(&selection.end)
{
open_range.end
} else {
*close_range.start()
};
selection.start = destination;
selection.end = destination;
}
}
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(selections);
s.move_offsets_with(|snapshot, selection| {
let Some(enclosing_bracket_ranges) = snapshot.enclosing_bracket_ranges(selection.start..selection.end) else {
return;
};
let mut best_length = usize::MAX;
let mut best_inside = false;
let mut best_in_bracket_range = false;
let mut best_destination = None;
for (open, close) in enclosing_bracket_ranges {
let close = close.to_inclusive();
let length = close.end() - open.start;
let inside = selection.start >= open.end && selection.end <= *close.start();
let in_bracket_range = open.to_inclusive().contains(&selection.head()) || close.contains(&selection.head());
// If best is next to a bracket and current isn't, skip
if !in_bracket_range && best_in_bracket_range {
continue;
}
// Prefer smaller lengths unless best is inside and current isn't
if length > best_length && (best_inside || !inside) {
continue;
}
best_length = length;
best_inside = inside;
best_in_bracket_range = in_bracket_range;
best_destination = Some(if close.contains(&selection.start) && close.contains(&selection.end) {
if inside {
open.end
} else {
open.start
}
} else {
if inside {
*close.start()
} else {
*close.end()
}
});
}
if let Some(destination) = best_destination {
selection.collapse_to(destination, SelectionGoal::None);
}
})
});
}
@@ -5003,7 +5071,7 @@ impl Editor {
GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx),
});
cx.spawn(|workspace, mut cx| async move {
cx.spawn_labeled("Fetching Definition...", |workspace, mut cx| async move {
let definitions = definitions.await?;
workspace.update(&mut cx, |workspace, cx| {
Editor::navigate_to_definitions(workspace, editor_handle, definitions, cx);
@@ -5083,31 +5151,36 @@ impl Editor {
let project = workspace.project().clone();
let references = project.update(cx, |project, cx| project.references(&buffer, head, cx));
Some(cx.spawn(|workspace, mut cx| async move {
let locations = references.await?;
if locations.is_empty() {
return Ok(());
}
Some(cx.spawn_labeled(
"Finding All References...",
|workspace, mut cx| async move {
let locations = references.await?;
if locations.is_empty() {
return Ok(());
}
workspace.update(&mut cx, |workspace, cx| {
let title = locations
.first()
.as_ref()
.map(|location| {
let buffer = location.buffer.read(cx);
format!(
"References to `{}`",
buffer
.text_for_range(location.range.clone())
.collect::<String>()
)
})
.unwrap();
Self::open_locations_in_multibuffer(workspace, locations, replica_id, title, cx);
});
workspace.update(&mut cx, |workspace, cx| {
let title = locations
.first()
.as_ref()
.map(|location| {
let buffer = location.buffer.read(cx);
format!(
"References to `{}`",
buffer
.text_for_range(location.range.clone())
.collect::<String>()
)
})
.unwrap();
Self::open_locations_in_multibuffer(
workspace, locations, replica_id, title, cx,
);
});
Ok(())
}))
Ok(())
},
))
}
/// Opens a multibuffer with the given project locations in it
@@ -5810,6 +5883,27 @@ impl Editor {
.update(cx, |map, cx| map.set_wrap_width(width, cx))
}
pub fn toggle_soft_wrap(&mut self, _: &ToggleSoftWrap, cx: &mut ViewContext<Self>) {
if self.soft_wrap_mode_override.is_some() {
self.soft_wrap_mode_override.take();
} else {
let soft_wrap = match self.soft_wrap_mode(cx) {
SoftWrap::None => settings::SoftWrap::EditorWidth,
SoftWrap::EditorWidth | SoftWrap::Column(_) => settings::SoftWrap::None,
};
self.soft_wrap_mode_override = Some(soft_wrap);
}
cx.notify();
}
pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
cx.reveal_path(&file.abs_path(cx));
}
}
}
pub fn highlight_rows(&mut self, rows: Option<Range<u32>>) {
self.highlighted_rows = rows;
}
@@ -6187,6 +6281,9 @@ impl Deref for EditorSnapshot {
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
InputIgnored {
text: Arc<str>,
},
ExcerptsAdded {
buffer: ModelHandle<Buffer>,
predecessor: ExcerptId,
@@ -6253,8 +6350,10 @@ impl View for Editor {
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
let focused_event = EditorFocused(cx.handle());
cx.emit_global(focused_event);
if cx.is_self_focused() {
let focused_event = EditorFocused(cx.handle());
cx.emit_global(focused_event);
}
if let Some(rename) = self.pending_rename.as_ref() {
cx.focus(&rename.editor);
} else {
@@ -6393,26 +6492,29 @@ impl View for Editor {
text: &str,
cx: &mut ViewContext<Self>,
) {
self.transact(cx, |this, cx| {
if this.input_enabled {
let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
Some(this.selection_replacement_ranges(range_utf16, cx))
} else {
this.marked_text_ranges(cx)
};
if let Some(new_selected_ranges) = new_selected_ranges {
this.change_selections(None, cx, |selections| {
selections.select_ranges(new_selected_ranges)
});
}
}
this.handle_input(text, cx);
});
if !self.input_enabled {
return;
}
self.transact(cx, |this, cx| {
let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
Some(this.selection_replacement_ranges(range_utf16, cx))
} else {
this.marked_text_ranges(cx)
};
if let Some(new_selected_ranges) = new_selected_ranges {
this.change_selections(None, cx, |selections| {
selections.select_ranges(new_selected_ranges)
});
}
this.handle_input(text, cx);
});
if let Some(transaction) = self.ime_transaction {
self.buffer.update(cx, |buffer, cx| {
buffer.group_until_transaction(transaction, cx);
@@ -6909,21 +7011,6 @@ pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator<Item = &'a str
.flat_map(|word| word.split_inclusive('_'))
}
trait RangeExt<T> {
fn sorted(&self) -> Range<T>;
fn to_inclusive(&self) -> RangeInclusive<T>;
}
impl<T: Ord + Clone> RangeExt<T> for Range<T> {
fn sorted(&self) -> Self {
cmp::min(&self.start, &self.end).clone()..cmp::max(&self.start, &self.end).clone()
}
fn to_inclusive(&self) -> RangeInclusive<T> {
self.start.clone()..=self.end.clone()
}
}
trait RangeToAnchorExt {
fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor>;
}

View File

@@ -13,8 +13,9 @@ use gpui::{
executor::Deterministic,
geometry::{rect::RectF, vector::vec2f},
platform::{WindowBounds, WindowOptions},
serde_json,
};
use language::{FakeLspAdapter, LanguageConfig, LanguageRegistry, Point};
use language::{BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point};
use project::FakeFs;
use settings::EditorSettings;
use util::{
@@ -3002,20 +3003,23 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
let language = Arc::new(
Language::new(
LanguageConfig {
brackets: vec![
BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: false,
newline: true,
},
BracketPair {
start: "(".to_string(),
end: ")".to_string(),
close: false,
newline: true,
},
],
brackets: BracketPairConfig {
pairs: vec![
BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: false,
newline: true,
},
BracketPair {
start: "(".to_string(),
end: ")".to_string(),
close: false,
newline: true,
},
],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::language()),
@@ -3059,38 +3063,41 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
let language = Arc::new(Language::new(
LanguageConfig {
brackets: vec![
BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: true,
newline: true,
},
BracketPair {
start: "(".to_string(),
end: ")".to_string(),
close: true,
newline: true,
},
BracketPair {
start: "/*".to_string(),
end: " */".to_string(),
close: true,
newline: true,
},
BracketPair {
start: "[".to_string(),
end: "]".to_string(),
close: false,
newline: true,
},
BracketPair {
start: "\"".to_string(),
end: "\"".to_string(),
close: true,
newline: false,
},
],
brackets: BracketPairConfig {
pairs: vec![
BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: true,
newline: true,
},
BracketPair {
start: "(".to_string(),
end: ")".to_string(),
close: true,
newline: true,
},
BracketPair {
start: "/*".to_string(),
end: " */".to_string(),
close: true,
newline: true,
},
BracketPair {
start: "[".to_string(),
end: "]".to_string(),
close: false,
newline: true,
},
BracketPair {
start: "\"".to_string(),
end: "\"".to_string(),
close: true,
newline: false,
},
],
..Default::default()
},
autoclose_before: "})]".to_string(),
..Default::default()
},
@@ -3227,26 +3234,29 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
Language::new(
LanguageConfig {
name: "HTML".into(),
brackets: vec![
BracketPair {
start: "<".into(),
end: ">".into(),
close: true,
..Default::default()
},
BracketPair {
start: "{".into(),
end: "}".into(),
close: true,
..Default::default()
},
BracketPair {
start: "(".into(),
end: ")".into(),
close: true,
..Default::default()
},
],
brackets: BracketPairConfig {
pairs: vec![
BracketPair {
start: "<".into(),
end: ">".into(),
close: true,
..Default::default()
},
BracketPair {
start: "{".into(),
end: "}".into(),
close: true,
..Default::default()
},
BracketPair {
start: "(".into(),
end: ")".into(),
close: true,
..Default::default()
},
],
..Default::default()
},
autoclose_before: "})]>".into(),
..Default::default()
},
@@ -3265,26 +3275,29 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
let javascript_language = Arc::new(Language::new(
LanguageConfig {
name: "JavaScript".into(),
brackets: vec![
BracketPair {
start: "/*".into(),
end: " */".into(),
close: true,
..Default::default()
},
BracketPair {
start: "{".into(),
end: "}".into(),
close: true,
..Default::default()
},
BracketPair {
start: "(".into(),
end: ")".into(),
close: true,
..Default::default()
},
],
brackets: BracketPairConfig {
pairs: vec![
BracketPair {
start: "/*".into(),
end: " */".into(),
close: true,
..Default::default()
},
BracketPair {
start: "{".into(),
end: "}".into(),
close: true,
..Default::default()
},
BracketPair {
start: "(".into(),
end: ")".into(),
close: true,
..Default::default()
},
],
..Default::default()
},
autoclose_before: "})]>".into(),
..Default::default()
},
@@ -3447,17 +3460,125 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
);
}
#[gpui::test]
async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
let mut cx = EditorTestContext::new(cx);
let rust_language = Arc::new(
Language::new(
LanguageConfig {
name: "Rust".into(),
brackets: serde_json::from_value(json!([
{ "start": "{", "end": "}", "close": true, "newline": true },
{ "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
]))
.unwrap(),
autoclose_before: "})]>".into(),
..Default::default()
},
Some(tree_sitter_rust::language()),
)
.with_override_query("(string_literal) @string")
.unwrap(),
);
let registry = Arc::new(LanguageRegistry::test());
registry.add(rust_language.clone());
cx.update_buffer(|buffer, cx| {
buffer.set_language_registry(registry);
buffer.set_language(Some(rust_language), cx);
});
cx.set_state(
&r#"
let x = ˇ
"#
.unindent(),
);
// Inserting a quotation mark. A closing quotation mark is automatically inserted.
cx.update_editor(|editor, cx| {
editor.handle_input("\"", cx);
});
cx.assert_editor_state(
&r#"
let x = "ˇ"
"#
.unindent(),
);
// Inserting another quotation mark. The cursor moves across the existing
// automatically-inserted quotation mark.
cx.update_editor(|editor, cx| {
editor.handle_input("\"", cx);
});
cx.assert_editor_state(
&r#"
let x = ""ˇ
"#
.unindent(),
);
// Reset
cx.set_state(
&r#"
let x = ˇ
"#
.unindent(),
);
// Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
cx.update_editor(|editor, cx| {
editor.handle_input("\"", cx);
editor.handle_input(" ", cx);
editor.move_left(&Default::default(), cx);
editor.handle_input("\\", cx);
editor.handle_input("\"", cx);
});
cx.assert_editor_state(
&r#"
let x = "\"ˇ "
"#
.unindent(),
);
// Inserting a closing quotation mark at the position of an automatically-inserted quotation
// mark. Nothing is inserted.
cx.update_editor(|editor, cx| {
editor.move_right(&Default::default(), cx);
editor.handle_input("\"", cx);
});
cx.assert_editor_state(
&r#"
let x = "\" "ˇ
"#
.unindent(),
);
}
#[gpui::test]
async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx)));
let language = Arc::new(Language::new(
LanguageConfig {
brackets: vec![BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: true,
newline: true,
}],
brackets: BracketPairConfig {
pairs: vec![
BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: true,
newline: true,
},
BracketPair {
start: "/* ".to_string(),
end: "*/".to_string(),
close: true,
..Default::default()
},
],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::language()),
@@ -3526,6 +3647,67 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1)
]
);
// Ensure inserting the first character of a multi-byte bracket pair
// doesn't surround the selections with the bracket.
view.handle_input("/", cx);
assert_eq!(
view.text(cx),
"
/
/
/
"
.unindent()
);
assert_eq!(
view.selections.display_ranges(cx),
[
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
]
);
view.undo(&Undo, cx);
assert_eq!(
view.text(cx),
"
a
b
c
"
.unindent()
);
assert_eq!(
view.selections.display_ranges(cx),
[
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1)
]
);
// Ensure inserting the last character of a multi-byte bracket pair
// doesn't surround the selections with the bracket.
view.handle_input("*", cx);
assert_eq!(
view.text(cx),
"
*
*
*
"
.unindent()
);
assert_eq!(
view.selections.display_ranges(cx),
[
DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
]
);
});
}
@@ -3534,12 +3716,15 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx)));
let language = Arc::new(Language::new(
LanguageConfig {
brackets: vec![BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: true,
newline: true,
}],
brackets: BracketPairConfig {
pairs: vec![BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: true,
newline: true,
}],
..Default::default()
},
autoclose_before: "}".to_string(),
..Default::default()
},
@@ -4382,7 +4567,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
DisplayPoint::new(3, 5)..DisplayPoint::new(3, 6),
])
});
editor.toggle_comments(&ToggleComments, cx);
editor.toggle_comments(&ToggleComments::default(), cx);
assert_eq!(
editor.text(cx),
"
@@ -4400,7 +4585,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(1, 3)..DisplayPoint::new(3, 6)])
});
editor.toggle_comments(&ToggleComments, cx);
editor.toggle_comments(&ToggleComments::default(), cx);
assert_eq!(
editor.text(cx),
"
@@ -4417,7 +4602,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(3, 0)])
});
editor.toggle_comments(&ToggleComments, cx);
editor.toggle_comments(&ToggleComments::default(), cx);
assert_eq!(
editor.text(cx),
"
@@ -4432,6 +4617,139 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
});
}
#[gpui::test]
async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
let mut cx = EditorTestContext::new(cx);
cx.update(|cx| cx.set_global(Settings::test(cx)));
let language = Arc::new(Language::new(
LanguageConfig {
line_comment: Some("// ".into()),
..Default::default()
},
Some(tree_sitter_rust::language()),
));
let registry = Arc::new(LanguageRegistry::test());
registry.add(language.clone());
cx.update_buffer(|buffer, cx| {
buffer.set_language_registry(registry);
buffer.set_language(Some(language), cx);
});
let toggle_comments = &ToggleComments {
advance_downwards: true,
};
// Single cursor on one line -> advance
// Cursor moves horizontally 3 characters as well on non-blank line
cx.set_state(indoc!(
"fn a() {
ˇdog();
cat();
}"
));
cx.update_editor(|editor, cx| {
editor.toggle_comments(toggle_comments, cx);
});
cx.assert_editor_state(indoc!(
"fn a() {
// dog();
catˇ();
}"
));
// Single selection on one line -> don't advance
cx.set_state(indoc!(
"fn a() {
«dog()ˇ»;
cat();
}"
));
cx.update_editor(|editor, cx| {
editor.toggle_comments(toggle_comments, cx);
});
cx.assert_editor_state(indoc!(
"fn a() {
// «dog()ˇ»;
cat();
}"
));
// Multiple cursors on one line -> advance
cx.set_state(indoc!(
"fn a() {
ˇdˇog();
cat();
}"
));
cx.update_editor(|editor, cx| {
editor.toggle_comments(toggle_comments, cx);
});
cx.assert_editor_state(indoc!(
"fn a() {
// dog();
catˇ(ˇ);
}"
));
// Multiple cursors on one line, with selection -> don't advance
cx.set_state(indoc!(
"fn a() {
ˇdˇog«()ˇ»;
cat();
}"
));
cx.update_editor(|editor, cx| {
editor.toggle_comments(toggle_comments, cx);
});
cx.assert_editor_state(indoc!(
"fn a() {
// ˇdˇog«()ˇ»;
cat();
}"
));
// Single cursor on one line -> advance
// Cursor moves to column 0 on blank line
cx.set_state(indoc!(
"fn a() {
ˇdog();
cat();
}"
));
cx.update_editor(|editor, cx| {
editor.toggle_comments(toggle_comments, cx);
});
cx.assert_editor_state(indoc!(
"fn a() {
// dog();
ˇ
cat();
}"
));
// Single cursor on one line -> advance
// Cursor starts and ends at column 0
cx.set_state(indoc!(
"fn a() {
ˇ dog();
cat();
}"
));
cx.update_editor(|editor, cx| {
editor.toggle_comments(toggle_comments, cx);
});
cx.assert_editor_state(indoc!(
"fn a() {
// dog();
ˇ cat();
}"
));
}
#[gpui::test]
async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
let mut cx = EditorTestContext::new(cx);
@@ -4482,7 +4800,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
cx.assert_editor_state(
&r#"
<!-- <p>A</p>ˇ -->
@@ -4491,7 +4809,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
cx.assert_editor_state(
&r#"
<p>A</p>ˇ
@@ -4513,7 +4831,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
.unindent(),
);
cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
cx.assert_editor_state(
&r#"
<!-- <p>A«</p>
@@ -4523,7 +4841,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
cx.assert_editor_state(
&r#"
<p>A«</p>
@@ -4545,7 +4863,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
.unindent(),
);
cx.foreground().run_until_parked();
cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx));
cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
cx.assert_editor_state(
&r#"
<!-- ˇ<script> -->
@@ -4828,20 +5146,23 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
let language = Arc::new(
Language::new(
LanguageConfig {
brackets: vec![
BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: true,
newline: true,
},
BracketPair {
start: "/* ".to_string(),
end: " */".to_string(),
close: true,
newline: true,
},
],
brackets: BracketPairConfig {
pairs: vec![
BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: true,
newline: true,
},
BracketPair {
start: "/* ".to_string(),
end: " */".to_string(),
close: true,
newline: true,
},
],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::language()),
@@ -5459,6 +5780,54 @@ fn test_split_words() {
assert_eq!(split("helloworld"), &["helloworld"]);
}
#[gpui::test]
async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
let mut assert = |before, after| {
let _state_context = cx.set_state(before);
cx.update_editor(|editor, cx| {
editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx)
});
cx.assert_editor_state(after);
};
// Outside bracket jumps to outside of matching bracket
assert("console.logˇ(var);", "console.log(var)ˇ;");
assert("console.log(var)ˇ;", "console.logˇ(var);");
// Inside bracket jumps to inside of matching bracket
assert("console.log(ˇvar);", "console.log(varˇ);");
assert("console.log(varˇ);", "console.log(ˇvar);");
// When outside a bracket and inside, favor jumping to the inside bracket
assert(
"console.log('foo', [1, 2, 3]ˇ);",
"console.log(ˇ'foo', [1, 2, 3]);",
);
assert(
"console.log(ˇ'foo', [1, 2, 3]);",
"console.log('foo', [1, 2, 3]ˇ);",
);
// Bias forward if two options are equally likely
assert(
"let result = curried_fun()ˇ();",
"let result = curried_fun()()ˇ;",
);
// If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
assert(
indoc! {"
function test() {
console.log('test')ˇ
}"},
indoc! {"
function test() {
console.logˇ('test')
}"},
);
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(row as u32, column as u32);
point..point

View File

@@ -1438,7 +1438,7 @@ impl EditorElement {
} else {
let text_style = self.style.text.clone();
Flex::row()
.with_child(Label::new("".to_string(), text_style).boxed())
.with_child(Label::new("", text_style).boxed())
.with_children(jump_icon)
.contained()
.with_padding_left(gutter_padding)
@@ -1534,15 +1534,14 @@ impl Element for EditorElement {
let snapshot = self.update_view(cx.app, |view, cx| {
view.set_visible_line_count(size.y() / line_height);
let editor_width = text_width - gutter_margin - overscroll.x() - em_width;
let wrap_width = match view.soft_wrap_mode(cx) {
SoftWrap::None => Some((MAX_LINE_LEN / 2) as f32 * em_advance),
SoftWrap::EditorWidth => {
Some(text_width - gutter_margin - overscroll.x() - em_width)
}
SoftWrap::Column(column) => Some(column as f32 * em_advance),
SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance,
SoftWrap::EditorWidth => editor_width,
SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance),
};
if view.set_wrap_width(wrap_width, cx) {
if view.set_wrap_width(Some(wrap_width), cx) {
view.snapshot(cx)
} else {
snapshot

View File

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

View File

@@ -2,12 +2,10 @@ use crate::{
display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
FORMAT_TIMEOUT,
};
use anyhow::{anyhow, Context, Result};
use collections::HashSet;
use futures::future::try_join_all;
use futures::FutureExt;
use gpui::{
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
@@ -16,9 +14,10 @@ use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
SelectionGoal,
};
use project::{FormatTrigger, Item as _, Project, ProjectPath};
use project::{Item as _, Project, ProjectPath};
use rpc::proto::{self, update_view};
use settings::Settings;
use smallvec::SmallVec;
use std::{
borrow::Cow,
cmp::{self, Ordering},
@@ -530,7 +529,7 @@ impl Item for Editor {
) -> ElementBox {
Flex::row()
.with_child(
Label::new(self.title(cx).into(), style.label.clone())
Label::new(self.title(cx).to_string(), style.label.clone())
.aligned()
.boxed(),
)
@@ -609,32 +608,12 @@ impl Item for Editor {
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.report_event("save editor", cx);
let buffer = self.buffer().clone();
let buffers = buffer.read(cx).all_buffers();
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
let format = project.update(cx, |project, cx| {
project.format(buffers, true, FormatTrigger::Save, cx)
});
cx.spawn(|_, mut cx| async move {
let transaction = futures::select_biased! {
_ = timeout => {
log::warn!("timed out waiting for formatting");
None
}
transaction = format.log_err().fuse() => transaction,
};
buffer
.update(&mut cx, |buffer, cx| {
if let Some(transaction) = transaction {
if !buffer.is_singleton() {
buffer.push_transaction(&transaction.0);
}
}
buffer.save(cx)
})
let format = self.perform_format(project.clone(), cx);
let buffers = self.buffer().clone().read(cx).all_buffers();
cx.as_mut().spawn(|mut cx| async move {
format.await?;
project
.update(&mut cx, |project, cx| project.save_buffers(buffers, cx))
.await?;
Ok(())
})
@@ -693,8 +672,8 @@ impl Item for Editor {
Task::ready(Ok(()))
}
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
let mut result = Vec::new();
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
let mut result = SmallVec::new();
match event {
Event::Closed => result.push(ItemEvent::CloseItem),
Event::Saved | Event::TitleChanged => {
@@ -1158,7 +1137,6 @@ fn path_for_file<'a>(
mod tests {
use super::*;
use gpui::MutableAppContext;
use language::RopeFingerprint;
use std::{
path::{Path, PathBuf},
sync::Arc,
@@ -1204,17 +1182,6 @@ mod tests {
todo!()
}
fn save(
&self,
_: u64,
_: language::Rope,
_: clock::Global,
_: project::LineEnding,
_: &mut MutableAppContext,
) -> gpui::Task<anyhow::Result<(clock::Global, RopeFingerprint, SystemTime)>> {
todo!()
}
fn as_any(&self) -> &dyn std::any::Any {
todo!()
}

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ use db::{define_connection, query};
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
define_connection!(
// Current table shape using pseudo-rust syntax:
// Current schema shape using pseudo-rust syntax:
// editors(
// item_id: usize,
// workspace_id: usize,

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
use gpui::{
elements::{MouseEventHandler, ParentElement, Stack, Text},
CursorStyle, Element, ElementBox, Entity, MouseButton, RenderContext, View, ViewContext,
};
use settings::Settings;
use workspace::{item::ItemHandle, StatusItemView};
use crate::feedback_editor::GiveFeedback;
pub struct DeployFeedbackButton;
impl Entity for DeployFeedbackButton {
type Event = ();
}
impl View for DeployFeedbackButton {
fn ui_name() -> &'static str {
"DeployFeedbackButton"
}
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
Stack::new()
.with_child(
MouseEventHandler::<Self>::new(0, cx, |state, cx| {
let theme = &cx.global::<Settings>().theme;
let theme = &theme.workspace.status_bar.feedback;
Text::new("Give Feedback", theme.style_for(state, true).clone()).boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(GiveFeedback))
.boxed(),
)
.boxed()
}
}
impl StatusItemView for DeployFeedbackButton {
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,6 @@ use smol::io::{AsyncReadExt, AsyncWriteExt};
use std::borrow::Cow;
use std::cmp;
use std::io::Write;
use std::ops::Deref;
use std::sync::Arc;
use std::{
io,
@@ -94,16 +93,6 @@ impl LineEnding {
}
}
pub struct HomeDir(pub PathBuf);
impl Deref for HomeDir {
type Target = PathBuf;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[async_trait::async_trait]
pub trait Fs: Send + Sync {
async fn create_dir(&self, path: &Path) -> Result<()>;

View File

@@ -47,6 +47,7 @@ smol = "1.2"
time = { version = "0.3", features = ["serde", "serde-well-known"] }
tiny-skia = "0.5"
usvg = "0.14"
uuid = { version = "1.1.2", features = ["v4"] }
waker-fn = "1.1.0"
[build-dependencies]
@@ -66,7 +67,7 @@ media = { path = "../media" }
anyhow = "1"
block = "0.1"
cocoa = "0.24"
core-foundation = "0.9.3"
core-foundation = { version = "0.9.3", features = ["with-uuid"] }
core-graphics = "0.22.3"
core-text = "19.2"
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "8eaf7a918eafa28b0a37dc759e2e0e7683fa24f1" }

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -19,13 +19,14 @@ use smol::stream::StreamExt;
use crate::{
executor, geometry::vector::Vector2F, keymap_matcher::Keystroke, platform, Action,
AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent,
LeakDetector, ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith,
ReadViewWith, RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle,
WeakHandle, WindowInputHandler,
ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, ReadViewWith,
RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, WeakHandle,
};
use collections::BTreeMap;
use super::{AsyncAppContext, RefCounts};
use super::{
ref_counts::LeakDetector, window_input_handler::WindowInputHandler, AsyncAppContext, RefCounts,
};
pub struct TestAppContext {
cx: Rc<RefCell<MutableAppContext>>,
@@ -52,11 +53,7 @@ impl TestAppContext {
platform,
foreground_platform.clone(),
font_cache,
RefCounts {
#[cfg(any(test, feature = "test-support"))]
leak_detector,
..Default::default()
},
RefCounts::new(leak_detector),
(),
);
cx.next_entity_id = first_entity_id;
@@ -624,6 +621,8 @@ impl<T: View> ViewHandle<T> {
}
}
/// Tracks string context to be printed when assertions fail.
/// Often this is done by storing a context string in the manager and returning the handle.
#[derive(Clone)]
pub struct AssertionContextManager {
id: Arc<AtomicUsize>,
@@ -654,6 +653,9 @@ impl AssertionContextManager {
}
}
/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails.
/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails,
/// the state that was set initially for the failure can be printed in the error message
pub struct ContextHandle {
id: usize,
manager: AssertionContextManager,

View File

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

View File

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

View File

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

View File

@@ -308,7 +308,9 @@ impl Element for Flex {
}
}
}
child.paint(child_origin, visible_bounds, cx);
match self.axis {
Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,25 +5,16 @@ mod keystroke;
use std::{any::TypeId, fmt::Debug};
use collections::HashMap;
use serde::Deserialize;
use collections::{BTreeMap, HashMap};
use smallvec::SmallVec;
use crate::{impl_actions, Action};
use crate::Action;
pub use binding::{Binding, BindingMatchResult};
pub use keymap::Keymap;
pub use keymap_context::{KeymapContext, KeymapContextPredicate};
pub use keystroke::Keystroke;
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
pub struct KeyPressed {
#[serde(default)]
pub keystroke: Keystroke,
}
impl_actions!(gpui, [KeyPressed]);
pub struct KeymapMatcher {
pub contexts: Vec<KeymapContext>,
pending_views: HashMap<usize, KeymapContext>,
@@ -69,13 +60,28 @@ impl KeymapMatcher {
!self.pending_keystrokes.is_empty()
}
/// Pushes a keystroke onto the matcher.
/// The result of the new keystroke is returned:
/// MatchResult::None =>
/// No match is valid for this key given any pending keystrokes.
/// MatchResult::Pending =>
/// There exist bindings which are still waiting for more keys.
/// MatchResult::Complete(matches) =>
/// 1 or more bindings have recieved the necessary key presses.
/// The order of the matched actions is by order in the keymap file first and
/// position of the matching view second.
pub fn push_keystroke(
&mut self,
keystroke: Keystroke,
mut dispatch_path: Vec<(usize, KeymapContext)>,
) -> MatchResult {
let mut any_pending = false;
let mut matched_bindings: Vec<(usize, Box<dyn Action>)> = Vec::new();
// Collect matched bindings into an ordered list using the position in the matching binding first,
// and then the order the binding matched in the view tree second.
// The key is the reverse position of the binding in the bindings list so that later bindings
// match before earlier ones in the user's config
let mut matched_bindings: BTreeMap<usize, Vec<(usize, Box<dyn Action>)>> =
Default::default();
let first_keystroke = self.pending_keystrokes.is_empty();
self.pending_keystrokes.push(keystroke.clone());
@@ -84,35 +90,33 @@ impl KeymapMatcher {
self.contexts
.extend(dispatch_path.iter_mut().map(|e| std::mem::take(&mut e.1)));
for (i, (view_id, _)) in dispatch_path.into_iter().enumerate() {
// Find the bindings which map the pending keystrokes and current context
for (i, (view_id, _)) in dispatch_path.iter().enumerate() {
// Don't require pending view entry if there are no pending keystrokes
if !first_keystroke && !self.pending_views.contains_key(&view_id) {
if !first_keystroke && !self.pending_views.contains_key(view_id) {
continue;
}
// If there is a previous view context, invalidate that view if it
// has changed
if let Some(previous_view_context) = self.pending_views.remove(&view_id) {
if let Some(previous_view_context) = self.pending_views.remove(view_id) {
if previous_view_context != self.contexts[i] {
continue;
}
}
// Find the bindings which map the pending keystrokes and current context
for binding in self.keymap.bindings().iter().rev() {
for (order, binding) in self.keymap.bindings().iter().rev().enumerate() {
match binding.match_keys_and_context(&self.pending_keystrokes, &self.contexts[i..])
{
BindingMatchResult::Complete(mut action) => {
// Swap in keystroke for special KeyPressed action
if action.name() == "KeyPressed" && action.namespace() == "gpui" {
action = Box::new(KeyPressed {
keystroke: keystroke.clone(),
});
}
matched_bindings.push((view_id, action))
BindingMatchResult::Complete(action) => {
matched_bindings
.entry(order)
.or_default()
.push((*view_id, action));
}
BindingMatchResult::Partial => {
self.pending_views.insert(view_id, self.contexts[i].clone());
self.pending_views
.insert(*view_id, self.contexts[i].clone());
any_pending = true;
}
_ => {}
@@ -125,7 +129,9 @@ impl KeymapMatcher {
}
if !matched_bindings.is_empty() {
MatchResult::Matches(matched_bindings)
// Collect the sorted matched bindings into the final vec for ease of use
// Matched bindings are in order by precedence
MatchResult::Matches(matched_bindings.into_values().flatten().collect())
} else if any_pending {
MatchResult::Pending
} else {
@@ -221,7 +227,7 @@ mod tests {
#[test]
fn test_push_keystroke() -> Result<()> {
actions!(test, [B, AB, C, D, DA]);
actions!(test, [B, AB, C, D, DA, E, EF]);
let mut context1 = KeymapContext::default();
context1.set.insert("1".into());
@@ -280,6 +286,7 @@ mod tests {
matcher.push_keystroke(Keystroke::parse("d")?, dispatch_path.clone()),
MatchResult::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]),
);
// If none of the d action handlers consume the binding, a pending
// binding may then be used
assert_eq!(

View File

@@ -7,7 +7,7 @@ use super::{KeymapContext, KeymapContextPredicate, Keystroke};
pub struct Binding {
action: Box<dyn Action>,
keystrokes: Option<SmallVec<[Keystroke; 2]>>,
keystrokes: SmallVec<[Keystroke; 2]>,
context_predicate: Option<KeymapContextPredicate>,
}
@@ -23,16 +23,10 @@ impl Binding {
None
};
let keystrokes = if keystrokes == "*" {
None // Catch all context
} else {
Some(
keystrokes
.split_whitespace()
.map(Keystroke::parse)
.collect::<Result<_>>()?,
)
};
let keystrokes = keystrokes
.split_whitespace()
.map(Keystroke::parse)
.collect::<Result<_>>()?;
Ok(Self {
keystrokes,
@@ -41,7 +35,7 @@ impl Binding {
})
}
fn match_context(&self, contexts: &[KeymapContext]) -> bool {
pub fn match_context(&self, contexts: &[KeymapContext]) -> bool {
self.context_predicate
.as_ref()
.map(|predicate| predicate.eval(contexts))
@@ -53,20 +47,10 @@ impl Binding {
pending_keystrokes: &Vec<Keystroke>,
contexts: &[KeymapContext],
) -> BindingMatchResult {
if self
.keystrokes
.as_ref()
.map(|keystrokes| keystrokes.starts_with(&pending_keystrokes))
.unwrap_or(true)
&& self.match_context(contexts)
if self.keystrokes.as_ref().starts_with(&pending_keystrokes) && self.match_context(contexts)
{
// If the binding is completed, push it onto the matches list
if self
.keystrokes
.as_ref()
.map(|keystrokes| keystrokes.len() == pending_keystrokes.len())
.unwrap_or(true)
{
if self.keystrokes.as_ref().len() == pending_keystrokes.len() {
BindingMatchResult::Complete(self.action.boxed_clone())
} else {
BindingMatchResult::Partial
@@ -82,14 +66,14 @@ impl Binding {
contexts: &[KeymapContext],
) -> Option<SmallVec<[Keystroke; 2]>> {
if self.action.eq(action) && self.match_context(contexts) {
self.keystrokes.clone()
Some(self.keystrokes.clone())
} else {
None
}
}
pub fn keystrokes(&self) -> Option<&[Keystroke]> {
self.keystrokes.as_deref()
pub fn keystrokes(&self) -> &[Keystroke] {
self.keystrokes.as_slice()
}
pub fn action(&self) -> &dyn Action {

View File

@@ -43,7 +43,7 @@ impl KeymapContextPredicate {
pub fn eval(&self, contexts: &[KeymapContext]) -> bool {
let Some(context) = contexts.first() else { return false };
match self {
Self::Identifier(name) => context.set.contains(name.as_str()),
Self::Identifier(name) => (&context.set).contains(name.as_str()),
Self::Equal(left, right) => context
.map
.get(left)

View File

@@ -18,11 +18,15 @@ use crate::{
text_layout::{LineLayout, RunStyle},
Action, ClipboardItem, Menu, Scene,
};
use anyhow::{anyhow, Result};
use anyhow::{anyhow, bail, Result};
use async_task::Runnable;
pub use event::*;
use postage::oneshot;
use serde::Deserialize;
use sqlez::{
bindable::{Bind, Column, StaticColumnCount},
statement::Statement,
};
use std::{
any::Any,
fmt::{self, Debug, Display},
@@ -33,6 +37,7 @@ use std::{
sync::Arc,
};
use time::UtcOffset;
use uuid::Uuid;
pub trait Platform: Send + Sync {
fn dispatcher(&self) -> Arc<dyn Dispatcher>;
@@ -44,6 +49,7 @@ pub trait Platform: Send + Sync {
fn unhide_other_apps(&self);
fn quit(&self);
fn screen_by_id(&self, id: Uuid) -> Option<Rc<dyn Screen>>;
fn screens(&self) -> Vec<Rc<dyn Screen>>;
fn open_window(
@@ -74,6 +80,7 @@ pub trait Platform: Send + Sync {
fn app_version(&self) -> Result<AppVersion>;
fn os_name(&self) -> &'static str;
fn os_version(&self) -> Result<AppVersion>;
fn restart(&self);
}
pub(crate) trait ForegroundPlatform {
@@ -93,6 +100,7 @@ pub(crate) trait ForegroundPlatform {
options: PathPromptOptions,
) -> oneshot::Receiver<Option<Vec<PathBuf>>>;
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>;
fn reveal_path(&self, path: &Path);
}
pub trait Dispatcher: Send + Sync {
@@ -117,17 +125,19 @@ pub trait InputHandler {
pub trait Screen: Debug {
fn as_any(&self) -> &dyn Any;
fn size(&self) -> Vector2F;
fn bounds(&self) -> RectF;
fn display_uuid(&self) -> Option<Uuid>;
}
pub trait Window {
fn bounds(&self) -> WindowBounds;
fn content_size(&self) -> Vector2F;
fn scale_factor(&self) -> f32;
fn titlebar_height(&self) -> f32;
fn appearance(&self) -> Appearance;
fn screen(&self) -> Rc<dyn Screen>;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn on_event(&mut self, callback: Box<dyn FnMut(Event) -> bool>);
fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>);
fn on_resize(&mut self, callback: Box<dyn FnMut()>);
fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>);
fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>);
fn on_close(&mut self, callback: Box<dyn FnOnce()>);
fn set_input_handler(&mut self, input_handler: Box<dyn InputHandler>);
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
fn activate(&self);
@@ -136,14 +146,16 @@ pub trait Window {
fn show_character_palette(&self);
fn minimize(&self);
fn zoom(&self);
fn present_scene(&mut self, scene: Scene);
fn toggle_full_screen(&self);
fn bounds(&self) -> RectF;
fn content_size(&self) -> Vector2F;
fn scale_factor(&self) -> f32;
fn titlebar_height(&self) -> f32;
fn present_scene(&mut self, scene: Scene);
fn appearance(&self) -> Appearance;
fn on_event(&mut self, callback: Box<dyn FnMut(Event) -> bool>);
fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>);
fn on_resize(&mut self, callback: Box<dyn FnMut()>);
fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>);
fn on_moved(&mut self, callback: Box<dyn FnMut()>);
fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>);
fn on_close(&mut self, callback: Box<dyn FnOnce()>);
fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>);
fn is_topmost_for_position(&self, position: Vector2F) -> bool;
}
@@ -186,12 +198,70 @@ pub enum WindowKind {
PopUp,
}
#[derive(Debug)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum WindowBounds {
Fullscreen,
Maximized,
Fixed(RectF),
}
impl StaticColumnCount for WindowBounds {
fn column_count() -> usize {
5
}
}
impl Bind for WindowBounds {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
let (region, next_index) = match self {
WindowBounds::Fullscreen => {
let next_index = statement.bind("Fullscreen", start_index)?;
(None, next_index)
}
WindowBounds::Maximized => {
let next_index = statement.bind("Maximized", start_index)?;
(None, next_index)
}
WindowBounds::Fixed(region) => {
let next_index = statement.bind("Fixed", start_index)?;
(Some(*region), next_index)
}
};
statement.bind(
region.map(|region| {
(
region.min_x(),
region.min_y(),
region.width(),
region.height(),
)
}),
next_index,
)
}
}
impl Column for WindowBounds {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let (window_state, next_index) = String::column(statement, start_index)?;
let bounds = match window_state.as_str() {
"Fullscreen" => WindowBounds::Fullscreen,
"Maximized" => WindowBounds::Maximized,
"Fixed" => {
let ((x, y, width, height), _) = Column::column(statement, next_index)?;
WindowBounds::Fixed(RectF::new(
Vector2F::new(x, y),
Vector2F::new(width, height),
))
}
_ => bail!("Window State did not have a valid string"),
};
Ok((bounds, next_index + 4))
}
}
pub struct PathPromptOptions {
pub files: bool,
pub directories: bool,

View File

@@ -12,20 +12,27 @@ mod sprite_cache;
mod status_item;
mod window;
use cocoa::base::{BOOL, NO, YES};
use cocoa::{
base::{id, nil, BOOL, NO, YES},
foundation::{NSAutoreleasePool, NSNotFound, NSString, NSUInteger},
};
pub use dispatcher::Dispatcher;
pub use fonts::FontSystem;
use platform::{MacForegroundPlatform, MacPlatform};
pub use renderer::Surface;
use std::{rc::Rc, sync::Arc};
use std::{ops::Range, rc::Rc, sync::Arc};
use window::Window;
use crate::executor;
pub(crate) fn platform() -> Arc<dyn super::Platform> {
Arc::new(MacPlatform::new())
}
pub(crate) fn foreground_platform() -> Rc<dyn super::ForegroundPlatform> {
Rc::new(MacForegroundPlatform::default())
pub(crate) fn foreground_platform(
foreground: Rc<executor::Foreground>,
) -> Rc<dyn super::ForegroundPlatform> {
Rc::new(MacForegroundPlatform::new(foreground))
}
trait BoolExt {
@@ -41,3 +48,57 @@ impl BoolExt for bool {
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug)]
struct NSRange {
pub location: NSUInteger,
pub length: NSUInteger,
}
impl NSRange {
fn invalid() -> Self {
Self {
location: NSNotFound as NSUInteger,
length: 0,
}
}
fn is_valid(&self) -> bool {
self.location != NSNotFound as NSUInteger
}
fn to_range(self) -> Option<Range<usize>> {
if self.is_valid() {
let start = self.location as usize;
let end = start + self.length as usize;
Some(start..end)
} else {
None
}
}
}
impl From<Range<usize>> for NSRange {
fn from(range: Range<usize>) -> Self {
NSRange {
location: range.start as NSUInteger,
length: range.len() as NSUInteger,
}
}
}
unsafe impl objc::Encode for NSRange {
fn encode() -> objc::Encoding {
let encoding = format!(
"{{NSRange={}{}}}",
NSUInteger::encode().as_str(),
NSUInteger::encode().as_str()
);
unsafe { objc::Encoding::from_str(&encoding) }
}
}
unsafe fn ns_string(string: &str) -> id {
NSString::alloc(nil).init_str(string).autorelease()
}

View File

@@ -1,27 +1,97 @@
use cocoa::foundation::{NSPoint, NSRect, NSSize};
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
use cocoa::{
appkit::NSWindow,
base::id,
foundation::{NSPoint, NSRect, NSSize},
};
use objc::{msg_send, sel, sel_impl};
use pathfinder_geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
};
///! Macos screen have a y axis that goings up from the bottom of the screen and
///! an origin at the bottom left of the main display.
pub trait Vector2FExt {
fn to_ns_point(&self) -> NSPoint;
fn to_ns_size(&self) -> NSSize;
/// Converts self to an NSPoint with y axis pointing up.
fn to_screen_ns_point(&self, native_window: id, window_height: f64) -> NSPoint;
}
impl Vector2FExt for Vector2F {
fn to_screen_ns_point(&self, native_window: id, window_height: f64) -> NSPoint {
unsafe {
let point = NSPoint::new(self.x() as f64, window_height - self.y() as f64);
msg_send![native_window, convertPointToScreen: point]
}
}
}
pub trait RectFExt {
/// Converts self to an NSRect with y axis pointing up.
/// The resulting NSRect will have an origin at the bottom left of the rectangle.
/// Also takes care of converting from window scaled coordinates to screen coordinates
fn to_screen_ns_rect(&self, native_window: id) -> NSRect;
/// Converts self to an NSRect with y axis point up.
/// The resulting NSRect will have an origin at the bottom left of the rectangle.
/// Unlike to_screen_ns_rect, coordinates are not converted and are assumed to already be in screen scale
fn to_ns_rect(&self) -> NSRect;
}
impl Vector2FExt for Vector2F {
fn to_ns_point(&self) -> NSPoint {
NSPoint::new(self.x() as f64, self.y() as f64)
}
fn to_ns_size(&self) -> NSSize {
NSSize::new(self.x() as f64, self.y() as f64)
}
}
impl RectFExt for RectF {
fn to_screen_ns_rect(&self, native_window: id) -> NSRect {
unsafe { native_window.convertRectToScreen_(self.to_ns_rect()) }
}
fn to_ns_rect(&self) -> NSRect {
NSRect::new(self.origin().to_ns_point(), self.size().to_ns_size())
NSRect::new(
NSPoint::new(
self.origin_x() as f64,
-(self.origin_y() + self.height()) as f64,
),
NSSize::new(self.width() as f64, self.height() as f64),
)
}
}
pub trait NSRectExt {
/// Converts self to a RectF with y axis pointing down.
/// The resulting RectF will have an origin at the top left of the rectangle.
/// Also takes care of converting from screen scale coordinates to window coordinates
fn to_window_rectf(&self, native_window: id) -> RectF;
/// Converts self to a RectF with y axis pointing down.
/// The resulting RectF will have an origin at the top left of the rectangle.
/// Unlike to_screen_ns_rect, coordinates are not converted and are assumed to already be in screen scale
fn to_rectf(&self) -> RectF;
fn intersects(&self, other: Self) -> bool;
}
impl NSRectExt for NSRect {
fn to_window_rectf(&self, native_window: id) -> RectF {
unsafe {
self.origin.x;
let rect: NSRect = native_window.convertRectFromScreen_(*self);
rect.to_rectf()
}
}
fn to_rectf(&self) -> RectF {
RectF::new(
vec2f(
self.origin.x as f32,
-(self.origin.y + self.size.height) as f32,
),
vec2f(self.size.width as f32, self.size.height as f32),
)
}
fn intersects(&self, other: Self) -> bool {
self.size.width > 0.
&& self.size.height > 0.
&& other.size.width > 0.
&& other.size.height > 0.
&& self.origin.x <= other.origin.x + other.size.width
&& self.origin.x + self.size.width >= other.origin.x
&& self.origin.y <= other.origin.y + other.size.height
&& self.origin.y + self.size.height >= other.origin.y
}
}

View File

@@ -16,7 +16,7 @@ use cocoa::{
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
NSPasteboardTypeString, NSSavePanel, NSWindow,
},
base::{id, nil, selector, YES},
base::{id, nil, selector, BOOL, YES},
foundation::{
NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSString,
NSUInteger, NSURL,
@@ -45,6 +45,7 @@ use std::{
ffi::{c_void, CStr, OsStr},
os::{raw::c_char, unix::ffi::OsStrExt},
path::{Path, PathBuf},
process::Command,
ptr,
rc::Rc,
slice, str,
@@ -97,6 +98,31 @@ unsafe fn build_classes() {
sel!(handleGPUIMenuItem:),
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
);
// Add menu item handlers so that OS save panels have the correct key commands
decl.add_method(
sel!(cut:),
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(copy:),
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(paste:),
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(selectAll:),
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(undo:),
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(redo:),
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
);
decl.add_method(
sel!(validateMenuItem:),
validate_menu_item as extern "C" fn(&mut Object, Sel, id) -> bool,
@@ -113,10 +139,8 @@ unsafe fn build_classes() {
}
}
#[derive(Default)]
pub struct MacForegroundPlatform(RefCell<MacForegroundPlatformState>);
#[derive(Default)]
pub struct MacForegroundPlatformState {
become_active: Option<Box<dyn FnMut()>>,
resign_active: Option<Box<dyn FnMut()>>,
@@ -128,9 +152,26 @@ pub struct MacForegroundPlatformState {
open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
finish_launching: Option<Box<dyn FnOnce()>>,
menu_actions: Vec<Box<dyn Action>>,
foreground: Rc<executor::Foreground>,
}
impl MacForegroundPlatform {
pub fn new(foreground: Rc<executor::Foreground>) -> Self {
Self(RefCell::new(MacForegroundPlatformState {
become_active: Default::default(),
resign_active: Default::default(),
quit: Default::default(),
event: Default::default(),
menu_command: Default::default(),
validate_menu_command: Default::default(),
will_open_menu: Default::default(),
open_urls: Default::default(),
finish_launching: Default::default(),
menu_actions: Default::default(),
foreground,
}))
}
unsafe fn create_menu_bar(
&self,
menus: Vec<Menu>,
@@ -177,14 +218,28 @@ impl MacForegroundPlatform {
) -> id {
match item {
MenuItem::Separator => NSMenuItem::separatorItem(nil),
MenuItem::Action { name, action } => {
MenuItem::Action {
name,
action,
os_action,
} => {
// TODO
let keystrokes = keystroke_matcher
.bindings_for_action_type(action.as_any().type_id())
.find(|binding| binding.action().eq(action.as_ref()))
.map(|binding| binding.keystrokes());
let selector = match os_action {
Some(crate::OsAction::Cut) => selector("cut:"),
Some(crate::OsAction::Copy) => selector("copy:"),
Some(crate::OsAction::Paste) => selector("paste:"),
Some(crate::OsAction::SelectAll) => selector("selectAll:"),
Some(crate::OsAction::Undo) => selector("undo:"),
Some(crate::OsAction::Redo) => selector("redo:"),
None => selector("handleGPUIMenuItem:"),
};
let item;
if let Some(keystrokes) = keystrokes.flatten() {
if let Some(keystrokes) = keystrokes {
if keystrokes.len() == 1 {
let keystroke = &keystrokes[0];
let mut mask = NSEventModifierFlags::empty();
@@ -202,7 +257,7 @@ impl MacForegroundPlatform {
item = NSMenuItem::alloc(nil)
.initWithTitle_action_keyEquivalent_(
ns_string(name),
selector("handleGPUIMenuItem:"),
selector,
ns_string(key_to_native(&keystroke.key).as_ref()),
)
.autorelease();
@@ -224,7 +279,7 @@ impl MacForegroundPlatform {
item = NSMenuItem::alloc(nil)
.initWithTitle_action_keyEquivalent_(
ns_string(&name),
selector("handleGPUIMenuItem:"),
selector,
ns_string(""),
)
.autorelease();
@@ -233,7 +288,7 @@ impl MacForegroundPlatform {
item = NSMenuItem::alloc(nil)
.initWithTitle_action_keyEquivalent_(
ns_string(name),
selector("handleGPUIMenuItem:"),
selector,
ns_string(""),
)
.autorelease();
@@ -398,6 +453,26 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
done_rx
}
}
fn reveal_path(&self, path: &Path) {
unsafe {
let path = path.to_path_buf();
self.0
.borrow()
.foreground
.spawn(async move {
let full_path = ns_string(path.to_str().unwrap_or(""));
let root_full_path = ns_string("");
let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
let _: BOOL = msg_send![
workspace,
selectFile: full_path
inFileViewerRootedAtPath: root_full_path
];
})
.detach();
}
}
}
pub struct MacPlatform {
@@ -440,6 +515,10 @@ impl platform::Platform for MacPlatform {
self.dispatcher.clone()
}
fn fonts(&self) -> Arc<dyn platform::FontSystem> {
self.fonts.clone()
}
fn activate(&self, ignoring_other_apps: bool) {
unsafe {
let app = NSApplication::sharedApplication(nil);
@@ -488,6 +567,10 @@ impl platform::Platform for MacPlatform {
}
}
fn screen_by_id(&self, id: uuid::Uuid) -> Option<Rc<dyn crate::Screen>> {
Screen::find_by_id(id).map(|screen| Rc::new(screen) as Rc<_>)
}
fn screens(&self) -> Vec<Rc<dyn platform::Screen>> {
Screen::all()
.into_iter()
@@ -512,10 +595,6 @@ impl platform::Platform for MacPlatform {
Box::new(StatusItem::add(self.fonts()))
}
fn fonts(&self) -> Arc<dyn platform::FontSystem> {
self.fonts.clone()
}
fn write_to_clipboard(&self, item: ClipboardItem) {
unsafe {
self.pasteboard.clearContents();
@@ -786,6 +865,28 @@ impl platform::Platform for MacPlatform {
})
}
}
fn restart(&self) {
#[cfg(debug_assertions)]
let path = std::env::current_exe().unwrap();
#[cfg(not(debug_assertions))]
let path = self
.app_path()
.unwrap_or_else(|_| std::env::current_exe().unwrap());
let script = r#"lsof -p "$0" +r 1 &>/dev/null && open "$1""#;
Command::new("/bin/bash")
.arg("-c")
.arg(script)
.arg(std::process::id().to_string())
.arg(path)
.spawn()
.ok();
self.quit();
}
}
unsafe fn path_from_objc(path: id) -> PathBuf {

View File

@@ -1,14 +1,25 @@
use std::any::Any;
use std::{any::Any, ffi::c_void};
use crate::{
geometry::vector::{vec2f, Vector2F},
platform,
};
use crate::platform;
use cocoa::{
appkit::NSScreen,
base::{id, nil},
foundation::NSArray,
foundation::{NSArray, NSDictionary},
};
use core_foundation::{
number::{kCFNumberIntType, CFNumberGetValue, CFNumberRef},
uuid::{CFUUIDGetUUIDBytes, CFUUIDRef},
};
use core_graphics::display::CGDirectDisplayID;
use pathfinder_geometry::rect::RectF;
use uuid::Uuid;
use super::{geometry::NSRectExt, ns_string};
#[link(name = "ApplicationServices", kind = "framework")]
extern "C" {
pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef;
}
#[derive(Debug)]
pub struct Screen {
@@ -16,11 +27,23 @@ pub struct Screen {
}
impl Screen {
pub fn find_by_id(uuid: Uuid) -> Option<Self> {
unsafe {
let native_screens = NSScreen::screens(nil);
(0..NSArray::count(native_screens))
.into_iter()
.map(|ix| Screen {
native_screen: native_screens.objectAtIndex(ix),
})
.find(|screen| platform::Screen::display_uuid(screen) == Some(uuid))
}
}
pub fn all() -> Vec<Self> {
let mut screens = Vec::new();
unsafe {
let native_screens = NSScreen::screens(nil);
for ix in 0..native_screens.count() {
for ix in 0..NSArray::count(native_screens) {
screens.push(Screen {
native_screen: native_screens.objectAtIndex(ix),
});
@@ -35,10 +58,52 @@ impl platform::Screen for Screen {
self
}
fn size(&self) -> Vector2F {
fn display_uuid(&self) -> Option<uuid::Uuid> {
unsafe {
// Screen ids are not stable. Further, the default device id is also unstable across restarts.
// CGDisplayCreateUUIDFromDisplayID is stable but not exposed in the bindings we use.
// This approach is similar to that which winit takes
// https://github.com/rust-windowing/winit/blob/402cbd55f932e95dbfb4e8b5e8551c49e56ff9ac/src/platform_impl/macos/monitor.rs#L99
let device_description = self.native_screen.deviceDescription();
let key = ns_string("NSScreenNumber");
let device_id_obj = device_description.objectForKey_(key);
let mut device_id: u32 = 0;
CFNumberGetValue(
device_id_obj as CFNumberRef,
kCFNumberIntType,
(&mut device_id) as *mut _ as *mut c_void,
);
let cfuuid = CGDisplayCreateUUIDFromDisplayID(device_id as CGDirectDisplayID);
if cfuuid.is_null() {
return None;
}
let bytes = CFUUIDGetUUIDBytes(cfuuid);
Some(Uuid::from_bytes([
bytes.byte0,
bytes.byte1,
bytes.byte2,
bytes.byte3,
bytes.byte4,
bytes.byte5,
bytes.byte6,
bytes.byte7,
bytes.byte8,
bytes.byte9,
bytes.byte10,
bytes.byte11,
bytes.byte12,
bytes.byte13,
bytes.byte14,
bytes.byte15,
]))
}
}
fn bounds(&self) -> RectF {
unsafe {
let frame = self.native_screen.frame();
vec2f(frame.size.width as f32, frame.size.height as f32)
frame.to_rectf()
}
}
}

View File

@@ -7,7 +7,7 @@ use crate::{
self,
mac::{platform::NSViewLayerContentsRedrawDuringViewResize, renderer::Renderer},
},
Event, FontSystem, Scene,
Event, FontSystem, Scene, WindowBounds,
};
use cocoa::{
appkit::{NSScreen, NSSquareStatusItemLength, NSStatusBar, NSStatusItem, NSView, NSWindow},
@@ -32,6 +32,8 @@ use std::{
sync::Arc,
};
use super::screen::Screen;
static mut VIEW_CLASS: *const Class = ptr::null();
const STATE_IVAR: &str = "state";
@@ -167,28 +169,42 @@ impl StatusItem {
}
impl platform::Window for StatusItem {
fn bounds(&self) -> WindowBounds {
self.0.borrow().bounds()
}
fn content_size(&self) -> Vector2F {
self.0.borrow().content_size()
}
fn scale_factor(&self) -> f32 {
self.0.borrow().scale_factor()
}
fn titlebar_height(&self) -> f32 {
0.
}
fn appearance(&self) -> crate::Appearance {
unsafe {
let appearance: id =
msg_send![self.0.borrow().native_item.button(), effectiveAppearance];
crate::Appearance::from_native(appearance)
}
}
fn screen(&self) -> Rc<dyn crate::Screen> {
unsafe {
Rc::new(Screen {
native_screen: self.0.borrow().native_window().screen(),
})
}
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn on_event(&mut self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
self.0.borrow_mut().event_callback = Some(callback);
}
fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>) {
self.0.borrow_mut().appearance_changed_callback = Some(callback);
}
fn on_active_status_change(&mut self, _: Box<dyn FnMut(bool)>) {}
fn on_resize(&mut self, _: Box<dyn FnMut()>) {}
fn on_fullscreen(&mut self, _: Box<dyn FnMut(bool)>) {}
fn on_should_close(&mut self, _: Box<dyn FnMut() -> bool>) {}
fn on_close(&mut self, _: Box<dyn FnOnce()>) {}
fn set_input_handler(&mut self, _: Box<dyn crate::InputHandler>) {}
fn prompt(
@@ -224,26 +240,6 @@ impl platform::Window for StatusItem {
unimplemented!()
}
fn toggle_full_screen(&self) {
unimplemented!()
}
fn bounds(&self) -> RectF {
self.0.borrow().bounds()
}
fn content_size(&self) -> Vector2F {
self.0.borrow().content_size()
}
fn scale_factor(&self) -> f32 {
self.0.borrow().scale_factor()
}
fn titlebar_height(&self) -> f32 {
0.
}
fn present_scene(&mut self, scene: Scene) {
self.0.borrow_mut().scene = Some(scene);
unsafe {
@@ -251,12 +247,28 @@ impl platform::Window for StatusItem {
}
}
fn appearance(&self) -> crate::Appearance {
unsafe {
let appearance: id =
msg_send![self.0.borrow().native_item.button(), effectiveAppearance];
crate::Appearance::from_native(appearance)
}
fn toggle_full_screen(&self) {
unimplemented!()
}
fn on_event(&mut self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
self.0.borrow_mut().event_callback = Some(callback);
}
fn on_active_status_change(&mut self, _: Box<dyn FnMut(bool)>) {}
fn on_resize(&mut self, _: Box<dyn FnMut()>) {}
fn on_fullscreen(&mut self, _: Box<dyn FnMut(bool)>) {}
fn on_moved(&mut self, _: Box<dyn FnMut()>) {}
fn on_should_close(&mut self, _: Box<dyn FnMut() -> bool>) {}
fn on_close(&mut self, _: Box<dyn FnOnce()>) {}
fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>) {
self.0.borrow_mut().appearance_changed_callback = Some(callback);
}
fn is_topmost_for_position(&self, _: Vector2F) -> bool {
@@ -265,9 +277,9 @@ impl platform::Window for StatusItem {
}
impl StatusItemState {
fn bounds(&self) -> RectF {
fn bounds(&self) -> WindowBounds {
unsafe {
let window: id = msg_send![self.native_item.button(), window];
let window: id = self.native_window();
let screen_frame = window.screen().visibleFrame();
let window_frame = NSWindow::frame(window);
let origin = vec2f(
@@ -279,7 +291,7 @@ impl StatusItemState {
window_frame.size.width as f32,
window_frame.size.height as f32,
);
RectF::new(origin, size)
WindowBounds::Fixed(RectF::new(origin, size))
}
}
@@ -297,6 +309,10 @@ impl StatusItemState {
NSScreen::backingScaleFactor(window.screen()) as f32
}
}
pub fn native_window(&self) -> id {
unsafe { msg_send![self.native_item.button(), window] }
}
}
extern "C" fn dealloc_view(this: &Object, _: Sel) {

View File

@@ -17,14 +17,12 @@ use crate::{
use block::ConcreteBlock;
use cocoa::{
appkit::{
CGFloat, CGPoint, NSApplication, NSBackingStoreBuffered, NSScreen, NSView,
NSViewHeightSizable, NSViewWidthSizable, NSWindow, NSWindowButton,
NSWindowCollectionBehavior, NSWindowStyleMask,
CGPoint, NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable,
NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior,
NSWindowStyleMask, NSWindowTitleVisibility,
},
base::{id, nil},
foundation::{
NSAutoreleasePool, NSInteger, NSNotFound, NSPoint, NSRect, NSSize, NSString, NSUInteger,
},
foundation::{NSAutoreleasePool, NSInteger, NSPoint, NSRect, NSSize, NSString, NSUInteger},
};
use core_graphics::display::CGRect;
use ctor::ctor;
@@ -52,6 +50,11 @@ use std::{
time::Duration,
};
use super::{
geometry::{NSRectExt, Vector2FExt},
ns_string, NSRange,
};
const WINDOW_STATE_IVAR: &str = "windowState";
static mut WINDOW_CLASS: *const Class = ptr::null();
@@ -76,56 +79,6 @@ const NSTrackingInVisibleRect: NSUInteger = 0x200;
#[allow(non_upper_case_globals)]
const NSWindowAnimationBehaviorUtilityWindow: NSInteger = 4;
#[repr(C)]
#[derive(Copy, Clone, Debug)]
struct NSRange {
pub location: NSUInteger,
pub length: NSUInteger,
}
impl NSRange {
fn invalid() -> Self {
Self {
location: NSNotFound as NSUInteger,
length: 0,
}
}
fn is_valid(&self) -> bool {
self.location != NSNotFound as NSUInteger
}
fn to_range(self) -> Option<Range<usize>> {
if self.is_valid() {
let start = self.location as usize;
let end = start + self.length as usize;
Some(start..end)
} else {
None
}
}
}
impl From<Range<usize>> for NSRange {
fn from(range: Range<usize>) -> Self {
NSRange {
location: range.start as NSUInteger,
length: range.len() as NSUInteger,
}
}
}
unsafe impl objc::Encode for NSRange {
fn encode() -> objc::Encoding {
let encoding = format!(
"{{NSRange={}{}}}",
NSUInteger::encode().as_str(),
NSUInteger::encode().as_str()
);
unsafe { objc::Encoding::from_str(&encoding) }
}
}
#[ctor]
unsafe fn build_classes() {
WINDOW_CLASS = build_window_class("GPUIWindow", class!(NSWindow));
@@ -295,6 +248,10 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
sel!(windowWillExitFullScreen:),
window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidMove:),
window_did_move as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidBecomeKey:),
window_did_change_key_status as extern "C" fn(&Object, Sel, id),
@@ -311,8 +268,6 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
decl.register()
}
pub struct Window(Rc<RefCell<WindowState>>);
///Used to track what the IME does when we send it a keystroke.
///This is only used to handle the case where the IME mysteriously
///swallows certain keys.
@@ -325,6 +280,11 @@ enum ImeState {
None,
}
struct InsertText {
replacement_range: Option<Range<usize>>,
text: String,
}
struct WindowState {
id: usize,
native_window: id,
@@ -333,6 +293,7 @@ struct WindowState {
activate_callback: Option<Box<dyn FnMut(bool)>>,
resize_callback: Option<Box<dyn FnMut()>>,
fullscreen_callback: Option<Box<dyn FnMut(bool)>>,
moved_callback: Option<Box<dyn FnMut()>>,
should_close_callback: Option<Box<dyn FnMut() -> bool>>,
close_callback: Option<Box<dyn FnOnce()>>,
appearance_changed_callback: Option<Box<dyn FnMut()>>,
@@ -352,11 +313,109 @@ struct WindowState {
ime_text: Option<String>,
}
struct InsertText {
replacement_range: Option<Range<usize>>,
text: String,
impl WindowState {
fn move_traffic_light(&self) {
if let Some(traffic_light_position) = self.traffic_light_position {
let titlebar_height = self.titlebar_height();
unsafe {
let close_button: id = msg_send![
self.native_window,
standardWindowButton: NSWindowButton::NSWindowCloseButton
];
let min_button: id = msg_send![
self.native_window,
standardWindowButton: NSWindowButton::NSWindowMiniaturizeButton
];
let zoom_button: id = msg_send![
self.native_window,
standardWindowButton: NSWindowButton::NSWindowZoomButton
];
let mut close_button_frame: CGRect = msg_send![close_button, frame];
let mut min_button_frame: CGRect = msg_send![min_button, frame];
let mut zoom_button_frame: CGRect = msg_send![zoom_button, frame];
let mut origin = vec2f(
traffic_light_position.x(),
titlebar_height
- traffic_light_position.y()
- close_button_frame.size.height as f32,
);
let button_spacing =
(min_button_frame.origin.x - close_button_frame.origin.x) as f32;
close_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64);
let _: () = msg_send![close_button, setFrame: close_button_frame];
origin.set_x(origin.x() + button_spacing);
min_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64);
let _: () = msg_send![min_button, setFrame: min_button_frame];
origin.set_x(origin.x() + button_spacing);
zoom_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64);
let _: () = msg_send![zoom_button, setFrame: zoom_button_frame];
}
}
}
fn is_fullscreen(&self) -> bool {
unsafe {
let style_mask = self.native_window.styleMask();
style_mask.contains(NSWindowStyleMask::NSFullScreenWindowMask)
}
}
fn bounds(&self) -> WindowBounds {
unsafe {
if self.is_fullscreen() {
return WindowBounds::Fullscreen;
}
let window_frame = self.frame();
if window_frame == self.native_window.screen().visibleFrame().to_rectf() {
WindowBounds::Maximized
} else {
WindowBounds::Fixed(window_frame)
}
}
}
// Returns the window bounds in window coordinates
fn frame(&self) -> RectF {
unsafe {
let ns_frame = NSWindow::frame(self.native_window);
ns_frame.to_rectf()
}
}
fn content_size(&self) -> Vector2F {
let NSSize { width, height, .. } =
unsafe { NSView::frame(self.native_window.contentView()) }.size;
vec2f(width as f32, height as f32)
}
fn scale_factor(&self) -> f32 {
get_scale_factor(self.native_window)
}
fn titlebar_height(&self) -> f32 {
unsafe {
let frame = NSWindow::frame(self.native_window);
let content_layout_rect: CGRect = msg_send![self.native_window, contentLayoutRect];
(frame.size.height - content_layout_rect.size.height) as f32
}
}
fn present_scene(&mut self, scene: Scene) {
self.scene_to_render = Some(scene);
unsafe {
let _: () = msg_send![self.native_window.contentView(), setNeedsDisplay: YES];
}
}
}
pub struct Window(Rc<RefCell<WindowState>>);
impl Window {
pub fn open(
id: usize,
@@ -390,7 +449,7 @@ impl Window {
}
};
let native_window = native_window.initWithContentRect_styleMask_backing_defer_screen_(
RectF::new(Default::default(), vec2f(1024., 768.)).to_ns_rect(),
NSRect::new(NSPoint::new(0., 0.), NSSize::new(1024., 768.)),
style_mask,
NSBackingStoreBuffered,
NO,
@@ -405,30 +464,26 @@ impl Window {
let screen = native_window.screen();
match options.bounds {
WindowBounds::Fullscreen => {
native_window.toggleFullScreen_(nil);
}
WindowBounds::Maximized => {
native_window.setFrame_display_(screen.visibleFrame(), YES);
}
WindowBounds::Fixed(top_left_bounds) => {
let frame = screen.visibleFrame();
let bottom_left_bounds = RectF::new(
vec2f(
top_left_bounds.origin_x(),
frame.size.height as f32
- top_left_bounds.origin_y()
- top_left_bounds.height(),
),
top_left_bounds.size(),
)
.to_ns_rect();
native_window.setFrame_display_(
native_window.convertRectToScreen_(bottom_left_bounds),
YES,
);
WindowBounds::Fixed(rect) => {
let screen_frame = screen.visibleFrame();
let ns_rect = rect.to_ns_rect();
if ns_rect.intersects(screen_frame) {
native_window.setFrame_display_(ns_rect, YES);
} else {
native_window.setFrame_display_(screen_frame, YES);
}
}
}
let native_view: id = msg_send![VIEW_CLASS, alloc];
let native_view = NSView::init(native_view);
assert!(!native_view.is_null());
let window = Self(Rc::new(RefCell::new(WindowState {
@@ -441,6 +496,7 @@ impl Window {
close_callback: None,
activate_callback: None,
fullscreen_callback: None,
moved_callback: None,
appearance_changed_callback: None,
input_handler: None,
pending_key_down: None,
@@ -480,6 +536,7 @@ impl Window {
.map_or(true, |titlebar| titlebar.appears_transparent)
{
native_window.setTitlebarAppearsTransparent_(YES);
native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden);
}
native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable);
@@ -576,34 +633,41 @@ impl Drop for Window {
}
impl platform::Window for Window {
fn bounds(&self) -> WindowBounds {
self.0.as_ref().borrow().bounds()
}
fn content_size(&self) -> Vector2F {
self.0.as_ref().borrow().content_size()
}
fn scale_factor(&self) -> f32 {
self.0.as_ref().borrow().scale_factor()
}
fn titlebar_height(&self) -> f32 {
self.0.as_ref().borrow().titlebar_height()
}
fn appearance(&self) -> crate::Appearance {
unsafe {
let appearance: id = msg_send![self.0.borrow().native_window, effectiveAppearance];
crate::Appearance::from_native(appearance)
}
}
fn screen(&self) -> Rc<dyn crate::Screen> {
unsafe {
Rc::new(Screen {
native_screen: self.0.as_ref().borrow().native_window.screen(),
})
}
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn on_event(&mut self, callback: Box<dyn FnMut(Event) -> bool>) {
self.0.as_ref().borrow_mut().event_callback = Some(callback);
}
fn on_resize(&mut self, callback: Box<dyn FnMut()>) {
self.0.as_ref().borrow_mut().resize_callback = Some(callback);
}
fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>) {
self.0.as_ref().borrow_mut().fullscreen_callback = Some(callback);
}
fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>) {
self.0.as_ref().borrow_mut().should_close_callback = Some(callback);
}
fn on_close(&mut self, callback: Box<dyn FnOnce()>) {
self.0.as_ref().borrow_mut().close_callback = Some(callback);
}
fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>) {
self.0.as_ref().borrow_mut().activate_callback = Some(callback);
}
fn set_input_handler(&mut self, input_handler: Box<dyn InputHandler>) {
self.0.as_ref().borrow_mut().input_handler = Some(input_handler);
}
@@ -671,7 +735,8 @@ impl platform::Window for Window {
let app = NSApplication::sharedApplication(nil);
let window = self.0.borrow().native_window;
let title = ns_string(title);
msg_send![app, changeWindowsItem:window title:title filename:false]
let _: () = msg_send![app, changeWindowsItem:window title:title filename:false];
let _: () = msg_send![window, setTitle: title];
}
}
@@ -713,6 +778,10 @@ impl platform::Window for Window {
.detach();
}
fn present_scene(&mut self, scene: Scene) {
self.0.as_ref().borrow_mut().present_scene(scene);
}
fn toggle_full_screen(&self) {
let this = self.0.borrow();
let window = this.native_window;
@@ -725,31 +794,32 @@ impl platform::Window for Window {
.detach();
}
fn bounds(&self) -> RectF {
self.0.as_ref().borrow().bounds()
fn on_event(&mut self, callback: Box<dyn FnMut(Event) -> bool>) {
self.0.as_ref().borrow_mut().event_callback = Some(callback);
}
fn content_size(&self) -> Vector2F {
self.0.as_ref().borrow().content_size()
fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>) {
self.0.as_ref().borrow_mut().activate_callback = Some(callback);
}
fn scale_factor(&self) -> f32 {
self.0.as_ref().borrow().scale_factor()
fn on_resize(&mut self, callback: Box<dyn FnMut()>) {
self.0.as_ref().borrow_mut().resize_callback = Some(callback);
}
fn present_scene(&mut self, scene: Scene) {
self.0.as_ref().borrow_mut().present_scene(scene);
fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>) {
self.0.as_ref().borrow_mut().fullscreen_callback = Some(callback);
}
fn titlebar_height(&self) -> f32 {
self.0.as_ref().borrow().titlebar_height()
fn on_moved(&mut self, callback: Box<dyn FnMut()>) {
self.0.as_ref().borrow_mut().moved_callback = Some(callback);
}
fn appearance(&self) -> crate::Appearance {
unsafe {
let appearance: id = msg_send![self.0.borrow().native_window, effectiveAppearance];
crate::Appearance::from_native(appearance)
}
fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>) {
self.0.as_ref().borrow_mut().should_close_callback = Some(callback);
}
fn on_close(&mut self, callback: Box<dyn FnOnce()>) {
self.0.as_ref().borrow_mut().close_callback = Some(callback);
}
fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>) {
@@ -757,21 +827,18 @@ impl platform::Window for Window {
}
fn is_topmost_for_position(&self, position: Vector2F) -> bool {
let window_bounds = self.bounds();
let self_borrow = self.0.borrow();
let self_id = self_borrow.id;
unsafe {
let app = NSApplication::sharedApplication(nil);
// Convert back to bottom-left coordinates
let point = NSPoint::new(
position.x() as CGFloat,
(window_bounds.height() - position.y()) as CGFloat,
// Convert back to screen coordinates
let screen_point = position.to_screen_ns_point(
self_borrow.native_window,
self_borrow.content_size().y() as f64,
);
let screen_point: NSPoint =
msg_send![self_borrow.native_window, convertPointToScreen: point];
let window_number: NSInteger = msg_send![class!(NSWindow), windowNumberAtPoint:screen_point belowWindowWithWindowNumber:0];
let top_most_window: id = msg_send![app, windowWithWindowNumber: window_number];
@@ -788,94 +855,6 @@ impl platform::Window for Window {
}
}
impl WindowState {
fn move_traffic_light(&self) {
if let Some(traffic_light_position) = self.traffic_light_position {
let titlebar_height = self.titlebar_height();
unsafe {
let close_button: id = msg_send![
self.native_window,
standardWindowButton: NSWindowButton::NSWindowCloseButton
];
let min_button: id = msg_send![
self.native_window,
standardWindowButton: NSWindowButton::NSWindowMiniaturizeButton
];
let zoom_button: id = msg_send![
self.native_window,
standardWindowButton: NSWindowButton::NSWindowZoomButton
];
let mut close_button_frame: CGRect = msg_send![close_button, frame];
let mut min_button_frame: CGRect = msg_send![min_button, frame];
let mut zoom_button_frame: CGRect = msg_send![zoom_button, frame];
let mut origin = vec2f(
traffic_light_position.x(),
titlebar_height
- traffic_light_position.y()
- close_button_frame.size.height as f32,
);
let button_spacing =
(min_button_frame.origin.x - close_button_frame.origin.x) as f32;
close_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64);
let _: () = msg_send![close_button, setFrame: close_button_frame];
origin.set_x(origin.x() + button_spacing);
min_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64);
let _: () = msg_send![min_button, setFrame: min_button_frame];
origin.set_x(origin.x() + button_spacing);
zoom_button_frame.origin = CGPoint::new(origin.x() as f64, origin.y() as f64);
let _: () = msg_send![zoom_button, setFrame: zoom_button_frame];
}
}
}
fn bounds(&self) -> RectF {
unsafe {
let screen_frame = self.native_window.screen().visibleFrame();
let window_frame = NSWindow::frame(self.native_window);
let origin = vec2f(
window_frame.origin.x as f32,
(window_frame.origin.y - screen_frame.size.height - window_frame.size.height)
as f32,
);
let size = vec2f(
window_frame.size.width as f32,
window_frame.size.height as f32,
);
RectF::new(origin, size)
}
}
fn content_size(&self) -> Vector2F {
let NSSize { width, height, .. } =
unsafe { NSView::frame(self.native_window.contentView()) }.size;
vec2f(width as f32, height as f32)
}
fn scale_factor(&self) -> f32 {
get_scale_factor(self.native_window)
}
fn titlebar_height(&self) -> f32 {
unsafe {
let frame = NSWindow::frame(self.native_window);
let content_layout_rect: CGRect = msg_send![self.native_window, contentLayoutRect];
(frame.size.height - content_layout_rect.size.height) as f32
}
}
fn present_scene(&mut self, scene: Scene) {
self.scene_to_render = Some(scene);
unsafe {
let _: () = msg_send![self.native_window.contentView(), setNeedsDisplay: YES];
}
}
}
fn get_scale_factor(native_window: id) -> f32 {
unsafe {
let screen: id = msg_send![native_window, screen];
@@ -1137,6 +1116,16 @@ fn window_fullscreen_changed(this: &Object, is_fullscreen: bool) {
}
}
extern "C" fn window_did_move(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let mut window_state_borrow = window_state.as_ref().borrow_mut();
if let Some(mut callback) = window_state_borrow.moved_callback.take() {
drop(window_state_borrow);
callback();
window_state.borrow_mut().moved_callback = Some(callback);
}
}
extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let window_state_borrow = window_state.borrow();
@@ -1499,10 +1488,6 @@ async fn synthetic_drag(
}
}
unsafe fn ns_string(string: &str) -> id {
NSString::alloc(nil).init_str(string).autorelease()
}
fn with_input_handler<F, R>(window: &Object, f: F) -> Option<R>
where
F: FnOnce(&mut dyn InputHandler) -> R,

View File

@@ -5,7 +5,7 @@ use crate::{
vector::{vec2f, Vector2F},
},
keymap_matcher::KeymapMatcher,
Action, ClipboardItem,
Action, ClipboardItem, Menu,
};
use anyhow::{anyhow, Result};
use collections::VecDeque;
@@ -20,11 +20,20 @@ use std::{
};
use time::UtcOffset;
pub struct Platform {
dispatcher: Arc<dyn super::Dispatcher>,
fonts: Arc<dyn super::FontSystem>,
current_clipboard_item: Mutex<Option<ClipboardItem>>,
cursor: Mutex<CursorStyle>,
struct Dispatcher;
impl super::Dispatcher for Dispatcher {
fn is_main_thread(&self) -> bool {
true
}
fn run_on_main_thread(&self, task: async_task::Runnable) {
task.run();
}
}
pub fn foreground_platform() -> ForegroundPlatform {
ForegroundPlatform::default()
}
#[derive(Default)]
@@ -32,23 +41,6 @@ pub struct ForegroundPlatform {
last_prompt_for_new_path_args: RefCell<Option<(PathBuf, oneshot::Sender<Option<PathBuf>>)>>,
}
struct Dispatcher;
pub struct Window {
pub(crate) size: Vector2F,
scale_factor: f32,
current_scene: Option<crate::Scene>,
event_handlers: Vec<Box<dyn FnMut(super::Event) -> bool>>,
pub(crate) resize_handlers: Vec<Box<dyn FnMut()>>,
close_handlers: Vec<Box<dyn FnOnce()>>,
fullscreen_handlers: Vec<Box<dyn FnMut(bool)>>,
pub(crate) active_status_change_handlers: Vec<Box<dyn FnMut(bool)>>,
pub(crate) should_close_handler: Option<Box<dyn FnMut() -> bool>>,
pub(crate) title: Option<String>,
pub(crate) edited: bool,
pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>,
}
#[cfg(any(test, feature = "test-support"))]
impl ForegroundPlatform {
pub(crate) fn simulate_new_path_selection(
@@ -85,7 +77,7 @@ impl super::ForegroundPlatform for ForegroundPlatform {
fn on_menu_command(&self, _: Box<dyn FnMut(&dyn Action)>) {}
fn on_validate_menu_command(&self, _: Box<dyn FnMut(&dyn Action) -> bool>) {}
fn on_will_open_menu(&self, _: Box<dyn FnMut()>) {}
fn set_menus(&self, _: Vec<crate::Menu>, _: &KeymapMatcher) {}
fn set_menus(&self, _: Vec<Menu>, _: &KeymapMatcher) {}
fn prompt_for_paths(
&self,
@@ -100,6 +92,19 @@ impl super::ForegroundPlatform for ForegroundPlatform {
*self.last_prompt_for_new_path_args.borrow_mut() = Some((path.to_path_buf(), done_tx));
done_rx
}
fn reveal_path(&self, _: &Path) {}
}
pub fn platform() -> Platform {
Platform::new()
}
pub struct Platform {
dispatcher: Arc<dyn super::Dispatcher>,
fonts: Arc<dyn super::FontSystem>,
current_clipboard_item: Mutex<Option<ClipboardItem>>,
cursor: Mutex<CursorStyle>,
}
impl Platform {
@@ -132,6 +137,10 @@ impl super::Platform for Platform {
fn quit(&self) {}
fn screen_by_id(&self, _id: uuid::Uuid) -> Option<Rc<dyn crate::Screen>> {
None
}
fn screens(&self) -> Vec<Rc<dyn crate::Screen>> {
Default::default()
}
@@ -143,7 +152,7 @@ impl super::Platform for Platform {
_executor: Rc<super::executor::Foreground>,
) -> Box<dyn super::Window> {
Box::new(Window::new(match options.bounds {
WindowBounds::Maximized => vec2f(1024., 768.),
WindowBounds::Maximized | WindowBounds::Fullscreen => vec2f(1024., 768.),
WindowBounds::Fixed(rect) => rect.size(),
}))
}
@@ -217,6 +226,41 @@ impl super::Platform for Platform {
patch: 0,
})
}
fn restart(&self) {}
}
#[derive(Debug)]
pub struct Screen;
impl super::Screen for Screen {
fn as_any(&self) -> &dyn Any {
self
}
fn bounds(&self) -> RectF {
RectF::new(Vector2F::zero(), Vector2F::new(1920., 1080.))
}
fn display_uuid(&self) -> Option<uuid::Uuid> {
Some(uuid::Uuid::new_v4())
}
}
pub struct Window {
pub(crate) size: Vector2F,
scale_factor: f32,
current_scene: Option<crate::Scene>,
event_handlers: Vec<Box<dyn FnMut(super::Event) -> bool>>,
pub(crate) resize_handlers: Vec<Box<dyn FnMut()>>,
pub(crate) moved_handlers: Vec<Box<dyn FnMut()>>,
close_handlers: Vec<Box<dyn FnOnce()>>,
fullscreen_handlers: Vec<Box<dyn FnMut(bool)>>,
pub(crate) active_status_change_handlers: Vec<Box<dyn FnMut(bool)>>,
pub(crate) should_close_handler: Option<Box<dyn FnMut() -> bool>>,
pub(crate) title: Option<String>,
pub(crate) edited: bool,
pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>,
}
impl Window {
@@ -225,6 +269,7 @@ impl Window {
size,
event_handlers: Default::default(),
resize_handlers: Default::default(),
moved_handlers: Default::default(),
close_handlers: Default::default(),
should_close_handler: Default::default(),
active_status_change_handlers: Default::default(),
@@ -242,41 +287,35 @@ impl Window {
}
}
impl super::Dispatcher for Dispatcher {
fn is_main_thread(&self) -> bool {
true
}
fn run_on_main_thread(&self, task: async_task::Runnable) {
task.run();
}
}
impl super::Window for Window {
fn bounds(&self) -> WindowBounds {
WindowBounds::Fixed(RectF::new(Vector2F::zero(), self.size))
}
fn content_size(&self) -> Vector2F {
self.size
}
fn scale_factor(&self) -> f32 {
self.scale_factor
}
fn titlebar_height(&self) -> f32 {
24.
}
fn appearance(&self) -> crate::Appearance {
crate::Appearance::Light
}
fn screen(&self) -> Rc<dyn crate::Screen> {
Rc::new(Screen)
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn on_event(&mut self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
self.event_handlers.push(callback);
}
fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>) {
self.active_status_change_handlers.push(callback);
}
fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>) {
self.fullscreen_handlers.push(callback)
}
fn on_resize(&mut self, callback: Box<dyn FnMut()>) {
self.resize_handlers.push(callback);
}
fn on_close(&mut self, callback: Box<dyn FnOnce()>) {
self.close_handlers.push(callback);
}
fn set_input_handler(&mut self, _: Box<dyn crate::InputHandler>) {}
fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str]) -> oneshot::Receiver<usize> {
@@ -295,40 +334,44 @@ impl super::Window for Window {
self.edited = edited;
}
fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>) {
self.should_close_handler = Some(callback);
}
fn show_character_palette(&self) {}
fn minimize(&self) {}
fn zoom(&self) {}
fn toggle_full_screen(&self) {}
fn bounds(&self) -> RectF {
RectF::new(Default::default(), self.size)
}
fn content_size(&self) -> Vector2F {
self.size
}
fn scale_factor(&self) -> f32 {
self.scale_factor
}
fn titlebar_height(&self) -> f32 {
24.
}
fn present_scene(&mut self, scene: crate::Scene) {
self.current_scene = Some(scene);
}
fn appearance(&self) -> crate::Appearance {
crate::Appearance::Light
fn toggle_full_screen(&self) {}
fn on_event(&mut self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
self.event_handlers.push(callback);
}
fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>) {
self.active_status_change_handlers.push(callback);
}
fn on_resize(&mut self, callback: Box<dyn FnMut()>) {
self.resize_handlers.push(callback);
}
fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>) {
self.fullscreen_handlers.push(callback)
}
fn on_moved(&mut self, callback: Box<dyn FnMut()>) {
self.moved_handlers.push(callback);
}
fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>) {
self.should_close_handler = Some(callback);
}
fn on_close(&mut self, callback: Box<dyn FnOnce()>) {
self.close_handlers.push(callback);
}
fn on_appearance_changed(&mut self, _: Box<dyn FnMut()>) {}
@@ -337,11 +380,3 @@ impl super::Window for Window {
true
}
}
pub fn platform() -> Platform {
Platform::new()
}
pub fn foreground_platform() -> ForegroundPlatform {
ForegroundPlatform::default()
}

View File

@@ -4,7 +4,6 @@ use crate::{
font_cache::FontCache,
geometry::rect::RectF,
json::{self, ToJson},
keymap_matcher::Keystroke,
platform::{CursorStyle, Event},
scene::{
CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover,
@@ -23,7 +22,7 @@ use pathfinder_geometry::vector::{vec2f, Vector2F};
use serde_json::json;
use smallvec::SmallVec;
use sqlez::{
bindable::{Bind, Column},
bindable::{Bind, Column, StaticColumnCount},
statement::Statement,
};
use std::{
@@ -604,14 +603,6 @@ pub struct LayoutContext<'a> {
}
impl<'a> LayoutContext<'a> {
pub(crate) fn keystrokes_for_action(
&mut self,
action: &dyn Action,
) -> Option<SmallVec<[Keystroke; 2]>> {
self.app
.keystrokes_for_action(self.window_id, &self.view_stack, action)
}
fn layout(&mut self, view_id: usize, constraint: SizeConstraint) -> Vector2F {
let print_error = |view_id| {
format!(
@@ -932,6 +923,7 @@ impl ToJson for Axis {
}
}
impl StaticColumnCount for Axis {}
impl Bind for Axis {
fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
match self {

View File

@@ -1,14 +1,3 @@
use crate::{
elements::Empty,
executor::{self, ExecutorEvent},
platform,
util::CwdBacktrace,
Element, ElementBox, Entity, FontCache, Handle, LeakDetector, MutableAppContext, Platform,
RenderContext, Subscription, TestAppContext, View,
};
use futures::StreamExt;
use parking_lot::Mutex;
use smol::channel;
use std::{
fmt::Write,
panic::{self, RefUnwindSafe},
@@ -19,6 +8,20 @@ use std::{
},
};
use futures::StreamExt;
use parking_lot::Mutex;
use smol::channel;
use crate::{
app::ref_counts::LeakDetector,
elements::Empty,
executor::{self, ExecutorEvent},
platform,
util::CwdBacktrace,
Element, ElementBox, Entity, FontCache, Handle, MutableAppContext, Platform, RenderContext,
Subscription, TestAppContext, View,
};
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {

View File

@@ -66,6 +66,7 @@ settings = { path = "../settings", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
ctor = "0.1"
env_logger = "0.9"
indoc = "1.0.4"
rand = "0.8.3"
tree-sitter-embedded-template = "*"
tree-sitter-html = "*"

View File

@@ -41,7 +41,7 @@ pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, Opera
use theme::SyntaxTheme;
#[cfg(any(test, feature = "test-support"))]
use util::RandomCharIter;
use util::TryFutureExt as _;
use util::{RangeExt, TryFutureExt as _};
#[cfg(any(test, feature = "test-support"))]
pub use {tree_sitter_rust, tree_sitter_typescript};
@@ -214,15 +214,6 @@ pub trait File: Send + Sync {
fn is_deleted(&self) -> bool;
fn save(
&self,
buffer_id: u64,
text: Rope,
version: clock::Global,
line_ending: LineEnding,
cx: &mut MutableAppContext,
) -> Task<Result<(clock::Global, RopeFingerprint, SystemTime)>>;
fn as_any(&self) -> &dyn Any;
fn to_proto(&self) -> rpc::proto::File;
@@ -529,33 +520,6 @@ impl Buffer {
self.file.as_ref()
}
pub fn save(
&mut self,
cx: &mut ModelContext<Self>,
) -> Task<Result<(clock::Global, RopeFingerprint, SystemTime)>> {
let file = if let Some(file) = self.file.as_ref() {
file
} else {
return Task::ready(Err(anyhow!("buffer has no file")));
};
let text = self.as_rope().clone();
let version = self.version();
let save = file.save(
self.remote_id(),
text,
version,
self.line_ending(),
cx.as_mut(),
);
cx.spawn(|this, mut cx| async move {
let (version, fingerprint, mtime) = save.await?;
this.update(&mut cx, |this, cx| {
this.did_save(version.clone(), fingerprint, mtime, None, cx);
});
Ok((version, fingerprint, mtime))
})
}
pub fn saved_version(&self) -> &clock::Global {
&self.saved_version
}
@@ -585,16 +549,11 @@ impl Buffer {
version: clock::Global,
fingerprint: RopeFingerprint,
mtime: SystemTime,
new_file: Option<Arc<dyn File>>,
cx: &mut ModelContext<Self>,
) {
self.saved_version = version;
self.saved_version_fingerprint = fingerprint;
self.saved_mtime = mtime;
if let Some(new_file) = new_file {
self.file = Some(new_file);
self.file_update_count += 1;
}
cx.emit(Event::Saved);
cx.notify();
}
@@ -661,36 +620,35 @@ impl Buffer {
new_file: Arc<dyn File>,
cx: &mut ModelContext<Self>,
) -> Task<()> {
let old_file = if let Some(file) = self.file.as_ref() {
file
} else {
return Task::ready(());
};
let mut file_changed = false;
let mut task = Task::ready(());
if new_file.path() != old_file.path() {
file_changed = true;
}
if new_file.is_deleted() {
if !old_file.is_deleted() {
if let Some(old_file) = self.file.as_ref() {
if new_file.path() != old_file.path() {
file_changed = true;
if !self.is_dirty() {
cx.emit(Event::DirtyChanged);
}
if new_file.is_deleted() {
if !old_file.is_deleted() {
file_changed = true;
if !self.is_dirty() {
cx.emit(Event::DirtyChanged);
}
}
} else {
let new_mtime = new_file.mtime();
if new_mtime != old_file.mtime() {
file_changed = true;
if !self.is_dirty() {
let reload = self.reload(cx).log_err().map(drop);
task = cx.foreground().spawn(reload);
}
}
}
} else {
let new_mtime = new_file.mtime();
if new_mtime != old_file.mtime() {
file_changed = true;
if !self.is_dirty() {
let reload = self.reload(cx).log_err().map(drop);
task = cx.foreground().spawn(reload);
}
}
}
file_changed = true;
};
if file_changed {
self.file_update_count += 1;
@@ -1389,12 +1347,12 @@ impl Buffer {
.enumerate()
.zip(&edit_operation.as_edit().unwrap().new_text)
.map(|((ix, (range, _)), new_text)| {
let new_text_len = new_text.len();
let new_text_length = new_text.len();
let old_start = range.start.to_point(&before_edit);
let new_start = (delta + range.start as isize) as usize;
delta += new_text_len as isize - (range.end as isize - range.start as isize);
delta += new_text_length as isize - (range.end as isize - range.start as isize);
let mut range_of_insertion_to_indent = 0..new_text_len;
let mut range_of_insertion_to_indent = 0..new_text_length;
let mut first_line_is_new = false;
let mut original_indent_column = None;
@@ -2247,7 +2205,6 @@ impl BufferSnapshot {
.map(|g| g.outline_config.as_ref().unwrap())
.collect::<Vec<_>>();
let mut chunks = self.chunks(0..self.len(), true);
let mut stack = Vec::<Range<usize>>::new();
let mut items = Vec::new();
while let Some(mat) = matches.peek() {
@@ -2266,9 +2223,7 @@ impl BufferSnapshot {
continue;
}
let mut text = String::new();
let mut name_ranges = Vec::new();
let mut highlight_ranges = Vec::new();
let mut buffer_ranges = Vec::new();
for capture in mat.captures {
let node_is_name;
if capture.index == config.name_capture_ix {
@@ -2286,12 +2241,27 @@ impl BufferSnapshot {
range.start + self.line_len(start.row as u32) as usize - start.column;
}
buffer_ranges.push((range, node_is_name));
}
if buffer_ranges.is_empty() {
continue;
}
let mut text = String::new();
let mut highlight_ranges = Vec::new();
let mut name_ranges = Vec::new();
let mut chunks = self.chunks(
buffer_ranges.first().unwrap().0.start..buffer_ranges.last().unwrap().0.end,
true,
);
for (buffer_range, is_name) in buffer_ranges {
if !text.is_empty() {
text.push(' ');
}
if node_is_name {
if is_name {
let mut start = text.len();
let end = start + range.len();
let end = start + buffer_range.len();
// When multiple names are captured, then the matcheable text
// includes the whitespace in between the names.
@@ -2302,12 +2272,12 @@ impl BufferSnapshot {
name_ranges.push(start..end);
}
let mut offset = range.start;
let mut offset = buffer_range.start;
chunks.seek(offset);
for mut chunk in chunks.by_ref() {
if chunk.text.len() > range.end - offset {
chunk.text = &chunk.text[0..(range.end - offset)];
offset = range.end;
if chunk.text.len() > buffer_range.end - offset {
chunk.text = &chunk.text[0..(buffer_range.end - offset)];
offset = buffer_range.end;
} else {
offset += chunk.text.len();
}
@@ -2321,7 +2291,7 @@ impl BufferSnapshot {
highlight_ranges.push((start..end, style));
}
text.push_str(chunk.text);
if offset >= range.end {
if offset >= buffer_range.end {
break;
}
}
@@ -2346,56 +2316,50 @@ impl BufferSnapshot {
Some(items)
}
pub fn enclosing_bracket_ranges<T: ToOffset>(
&self,
/// Returns bracket range pairs overlapping or adjacent to `range`
pub fn bracket_ranges<'a, T: ToOffset>(
&'a self,
range: Range<T>,
) -> Option<(Range<usize>, Range<usize>)> {
) -> impl Iterator<Item = (Range<usize>, Range<usize>)> + 'a {
// Find bracket pairs that *inclusively* contain the given range.
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut matches = self.syntax.matches(
range.start.saturating_sub(1)..self.len().min(range.end + 1),
&self.text,
|grammar| grammar.brackets_config.as_ref().map(|c| &c.query),
);
let range = range.start.to_offset(self).saturating_sub(1)
..self.len().min(range.end.to_offset(self) + 1);
let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
grammar.brackets_config.as_ref().map(|c| &c.query)
});
let configs = matches
.grammars()
.iter()
.map(|grammar| grammar.brackets_config.as_ref().unwrap())
.collect::<Vec<_>>();
// Get the ranges of the innermost pair of brackets.
let mut result: Option<(Range<usize>, Range<usize>)> = None;
while let Some(mat) = matches.peek() {
let mut open = None;
let mut close = None;
let config = &configs[mat.grammar_index];
for capture in mat.captures {
if capture.index == config.open_capture_ix {
open = Some(capture.node.byte_range());
} else if capture.index == config.close_capture_ix {
close = Some(capture.node.byte_range());
iter::from_fn(move || {
while let Some(mat) = matches.peek() {
let mut open = None;
let mut close = None;
let config = &configs[mat.grammar_index];
for capture in mat.captures {
if capture.index == config.open_capture_ix {
open = Some(capture.node.byte_range());
} else if capture.index == config.close_capture_ix {
close = Some(capture.node.byte_range());
}
}
}
matches.advance();
matches.advance();
let Some((open, close)) = open.zip(close) else { continue };
if open.start > range.start || close.end < range.end {
continue;
}
let len = close.end - open.start;
let Some((open, close)) = open.zip(close) else { continue };
if let Some((existing_open, existing_close)) = &result {
let existing_len = existing_close.end - existing_open.start;
if len > existing_len {
let bracket_range = open.start..=close.end;
if !bracket_range.overlaps(&range) {
continue;
}
return Some((open, close));
}
result = Some((open, close));
}
result
None
})
}
#[allow(clippy::type_complexity)]

View File

@@ -3,6 +3,7 @@ use clock::ReplicaId;
use collections::BTreeMap;
use fs::LineEnding;
use gpui::{ModelHandle, MutableAppContext};
use indoc::indoc;
use proto::deserialize_operation;
use rand::prelude::*;
use settings::Settings;
@@ -15,7 +16,7 @@ use std::{
};
use text::network::Network;
use unindent::Unindent as _;
use util::{post_inc, test::marked_text_ranges, RandomCharIter};
use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter};
#[cfg(test)]
#[ctor::ctor]
@@ -576,53 +577,117 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
#[gpui::test]
fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
cx.set_global(Settings::test(cx));
let buffer = cx.add_model(|cx| {
let text = "
mod x {
mod y {
let mut assert = |selection_text, range_markers| {
assert_bracket_pairs(selection_text, range_markers, rust_lang(), cx)
};
assert(
indoc! {"
mod x {
moˇd y {
}
}
"
.unindent();
Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)
});
let buffer = buffer.read(cx);
assert_eq!(
buffer.enclosing_bracket_point_ranges(Point::new(1, 6)..Point::new(1, 6)),
Some((
Point::new(0, 6)..Point::new(0, 7),
Point::new(4, 0)..Point::new(4, 1)
))
);
assert_eq!(
buffer.enclosing_bracket_point_ranges(Point::new(1, 10)..Point::new(1, 10)),
Some((
Point::new(1, 10)..Point::new(1, 11),
Point::new(3, 4)..Point::new(3, 5)
))
);
assert_eq!(
buffer.enclosing_bracket_point_ranges(Point::new(3, 5)..Point::new(3, 5)),
Some((
Point::new(1, 10)..Point::new(1, 11),
Point::new(3, 4)..Point::new(3, 5)
))
let foo = 1;"},
vec![indoc! {"
mod x «{»
mod y {
}
«}»
let foo = 1;"}],
);
assert_eq!(
buffer.enclosing_bracket_point_ranges(Point::new(4, 1)..Point::new(4, 1)),
Some((
Point::new(0, 6)..Point::new(0, 7),
Point::new(4, 0)..Point::new(4, 1)
))
assert(
indoc! {"
mod x {
mod y ˇ{
}
}
let foo = 1;"},
vec![
indoc! {"
mod x «{»
mod y {
}
«}»
let foo = 1;"},
indoc! {"
mod x {
mod y «{»
«}»
}
let foo = 1;"},
],
);
assert(
indoc! {"
mod x {
mod y {
}
let foo = 1;"},
vec![
indoc! {"
mod x «{»
mod y {
}
«}»
let foo = 1;"},
indoc! {"
mod x {
mod y «{»
«}»
}
let foo = 1;"},
],
);
assert(
indoc! {"
mod x {
mod y {
}
ˇ}
let foo = 1;"},
vec![indoc! {"
mod x «{»
mod y {
}
«}»
let foo = 1;"}],
);
assert(
indoc! {"
mod x {
mod y {
}
}
let fˇoo = 1;"},
vec![],
);
// Regression test: avoid crash when querying at the end of the buffer.
assert_eq!(
buffer.enclosing_bracket_point_ranges(Point::new(4, 1)..Point::new(5, 0)),
None
assert(
indoc! {"
mod x {
mod y {
}
}
let foo = 1;ˇ"},
vec![],
);
}
@@ -630,52 +695,34 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(
cx: &mut MutableAppContext,
) {
let javascript_language = Arc::new(
Language::new(
LanguageConfig {
name: "JavaScript".into(),
..Default::default()
},
Some(tree_sitter_javascript::language()),
)
.with_brackets_query(
r#"
("{" @open "}" @close)
("(" @open ")" @close)
"#,
)
.unwrap(),
);
cx.set_global(Settings::test(cx));
let buffer = cx.add_model(|cx| {
let text = "
for (const a in b) {
// a comment that's longer than the for-loop header
}
"
.unindent();
Buffer::new(0, text, cx).with_language(javascript_language, cx)
});
let buffer = buffer.read(cx);
assert_eq!(
buffer.enclosing_bracket_point_ranges(Point::new(0, 18)..Point::new(0, 18)),
Some((
Point::new(0, 4)..Point::new(0, 5),
Point::new(0, 17)..Point::new(0, 18)
))
let mut assert = |selection_text, bracket_pair_texts| {
assert_bracket_pairs(selection_text, bracket_pair_texts, javascript_lang(), cx)
};
assert(
indoc! {"
for (const a in b)ˇ {
// a comment that's longer than the for-loop header
}"},
vec![indoc! {"
for «(»const a in b«)» {
// a comment that's longer than the for-loop header
}"}],
);
eprintln!("-----------------------");
// Regression test: even though the parent node of the parentheses (the for loop) does
// intersect the given range, the parentheses themselves do not contain the range, so
// they should not be returned. Only the curly braces contain the range.
assert_eq!(
buffer.enclosing_bracket_point_ranges(Point::new(0, 20)..Point::new(0, 20)),
Some((
Point::new(0, 19)..Point::new(0, 20),
Point::new(2, 0)..Point::new(2, 1)
))
assert(
indoc! {"
for (const a in b) {ˇ
// a comment that's longer than the for-loop header
}"},
vec![indoc! {"
for (const a in b) «{»
// a comment that's longer than the for-loop header
«}»"}],
);
}
@@ -1480,42 +1527,34 @@ fn test_language_config_at(cx: &mut MutableAppContext) {
LanguageConfig {
name: "JavaScript".into(),
line_comment: Some("// ".into()),
brackets: vec![
BracketPair {
start: "{".into(),
end: "}".into(),
close: true,
newline: false,
},
BracketPair {
start: "'".into(),
end: "'".into(),
close: true,
newline: false,
},
],
overrides: [
(
"element".into(),
LanguageConfigOverride {
line_comment: Override::Remove { remove: true },
block_comment: Override::Set(("{/*".into(), "*/}".into())),
..Default::default()
brackets: BracketPairConfig {
pairs: vec![
BracketPair {
start: "{".into(),
end: "}".into(),
close: true,
newline: false,
},
),
(
"string".into(),
LanguageConfigOverride {
brackets: Override::Set(vec![BracketPair {
start: "{".into(),
end: "}".into(),
close: true,
newline: false,
}]),
..Default::default()
BracketPair {
start: "'".into(),
end: "'".into(),
close: true,
newline: false,
},
),
]
],
disabled_scopes_by_bracket_ix: vec![
Vec::new(), //
vec!["string".into()],
],
},
overrides: [(
"element".into(),
LanguageConfigOverride {
line_comment: Override::Remove { remove: true },
block_comment: Override::Set(("{/*".into(), "*/}".into())),
..Default::default()
},
)]
.into_iter()
.collect(),
..Default::default()
@@ -1537,11 +1576,19 @@ fn test_language_config_at(cx: &mut MutableAppContext) {
let config = snapshot.language_scope_at(0).unwrap();
assert_eq!(config.line_comment_prefix().unwrap().as_ref(), "// ");
assert_eq!(config.brackets().len(), 2);
// Both bracket pairs are enabled
assert_eq!(
config.brackets().map(|e| e.1).collect::<Vec<_>>(),
&[true, true]
);
let string_config = snapshot.language_scope_at(3).unwrap();
assert_eq!(config.line_comment_prefix().unwrap().as_ref(), "// ");
assert_eq!(string_config.brackets().len(), 1);
assert_eq!(string_config.line_comment_prefix().unwrap().as_ref(), "// ");
// Second bracket pair is disabled
assert_eq!(
string_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
&[true, false]
);
let element_config = snapshot.language_scope_at(10).unwrap();
assert_eq!(element_config.line_comment_prefix(), None);
@@ -1549,7 +1596,11 @@ fn test_language_config_at(cx: &mut MutableAppContext) {
element_config.block_comment_delimiters(),
Some((&"{/*".into(), &"*/}".into()))
);
assert_eq!(element_config.brackets().len(), 2);
// Both bracket pairs are enabled
assert_eq!(
element_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
&[true, true]
);
buffer
});
@@ -1892,21 +1943,6 @@ fn test_contiguous_ranges() {
);
}
impl Buffer {
pub fn enclosing_bracket_point_ranges<T: ToOffset>(
&self,
range: Range<T>,
) -> Option<(Range<Point>, Range<Point>)> {
self.snapshot()
.enclosing_bracket_ranges(range)
.map(|(start, end)| {
let point_start = start.start.to_point(self)..start.end.to_point(self);
let point_end = end.start.to_point(self)..end.end.to_point(self);
(point_start, point_end)
})
}
}
fn ruby_lang() -> Language {
Language::new(
LanguageConfig {
@@ -1990,6 +2026,23 @@ fn json_lang() -> Language {
)
}
fn javascript_lang() -> Language {
Language::new(
LanguageConfig {
name: "JavaScript".into(),
..Default::default()
},
Some(tree_sitter_javascript::language()),
)
.with_brackets_query(
r#"
("{" @open "}" @close)
("(" @open ")" @close)
"#,
)
.unwrap()
}
fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> String {
buffer.read_with(cx, |buffer, _| {
let snapshot = buffer.snapshot();
@@ -1997,3 +2050,34 @@ fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> Str
layers[0].node.to_sexp()
})
}
// Assert that the enclosing bracket ranges around the selection match the pairs indicated by the marked text in `range_markers`
fn assert_bracket_pairs(
selection_text: &'static str,
bracket_pair_texts: Vec<&'static str>,
language: Language,
cx: &mut MutableAppContext,
) {
cx.set_global(Settings::test(cx));
let (expected_text, selection_ranges) = marked_text_ranges(selection_text, false);
let buffer = cx.add_model(|cx| {
Buffer::new(0, expected_text.clone(), cx).with_language(Arc::new(language), cx)
});
let buffer = buffer.update(cx, |buffer, _cx| buffer.snapshot());
let selection_range = selection_ranges[0].clone();
let bracket_pairs = bracket_pair_texts
.into_iter()
.map(|pair_text| {
let (bracket_text, ranges) = marked_text_ranges(pair_text, false);
assert_eq!(bracket_text, expected_text);
(ranges[0].clone(), ranges[1].clone())
})
.collect::<Vec<_>>();
assert_set_eq!(
buffer.bracket_ranges(selection_range).collect::<Vec<_>>(),
bracket_pairs
);
}

View File

@@ -231,7 +231,7 @@ pub struct CodeLabel {
pub struct LanguageConfig {
pub name: Arc<str>,
pub path_suffixes: Vec<String>,
pub brackets: Vec<BracketPair>,
pub brackets: BracketPairConfig,
#[serde(default = "auto_indent_using_last_non_empty_line_default")]
pub auto_indent_using_last_non_empty_line: bool,
#[serde(default, deserialize_with = "deserialize_regex")]
@@ -258,7 +258,7 @@ pub struct LanguageQueries {
pub overrides: Option<Cow<'static, str>>,
}
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct LanguageScope {
language: Arc<Language>,
override_id: Option<u32>,
@@ -270,8 +270,8 @@ pub struct LanguageConfigOverride {
pub line_comment: Override<Arc<str>>,
#[serde(default)]
pub block_comment: Override<(Arc<str>, Arc<str>)>,
#[serde(default)]
pub brackets: Override<Vec<BracketPair>>,
#[serde(skip_deserializing)]
pub disabled_bracket_ixs: Vec<u16>,
}
#[derive(Deserialize, Debug)]
@@ -336,7 +336,41 @@ pub struct FakeLspAdapter {
pub disk_based_diagnostics_sources: Vec<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[derive(Clone, Debug, Default)]
pub struct BracketPairConfig {
pub pairs: Vec<BracketPair>,
pub disabled_scopes_by_bracket_ix: Vec<Vec<String>>,
}
impl<'de> Deserialize<'de> for BracketPairConfig {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
pub struct Entry {
#[serde(flatten)]
pub bracket_pair: BracketPair,
#[serde(default)]
pub not_in: Vec<String>,
}
let result = Vec::<Entry>::deserialize(deserializer)?;
let mut brackets = Vec::with_capacity(result.len());
let mut disabled_scopes_by_bracket_ix = Vec::with_capacity(result.len());
for entry in result {
brackets.push(entry.bracket_pair);
disabled_scopes_by_bracket_ix.push(entry.not_in);
}
Ok(BracketPairConfig {
pairs: brackets,
disabled_scopes_by_bracket_ix,
})
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
pub struct BracketPair {
pub start: String,
pub end: String,
@@ -393,7 +427,7 @@ struct InjectionConfig {
struct OverrideConfig {
query: Query,
values: HashMap<u32, LanguageConfigOverride>,
values: HashMap<u32, (String, LanguageConfigOverride)>,
}
#[derive(Default, Clone)]
@@ -967,16 +1001,11 @@ impl Language {
pub fn with_override_query(mut self, source: &str) -> Result<Self> {
let query = Query::new(self.grammar_mut().ts_language, source)?;
let mut values = HashMap::default();
let mut override_configs_by_id = HashMap::default();
for (ix, name) in query.capture_names().iter().enumerate() {
if !name.starts_with('_') {
let value = self.config.overrides.remove(name).ok_or_else(|| {
anyhow!(
"language {:?} has override in query but not in config: {name:?}",
self.config.name
)
})?;
values.insert(ix as u32, value);
let value = self.config.overrides.remove(name).unwrap_or_default();
override_configs_by_id.insert(ix as u32, (name.clone(), value));
}
}
@@ -988,7 +1017,46 @@ impl Language {
))?;
}
self.grammar_mut().override_config = Some(OverrideConfig { query, values });
for disabled_scope_name in self
.config
.brackets
.disabled_scopes_by_bracket_ix
.iter()
.flatten()
{
if !override_configs_by_id
.values()
.any(|(scope_name, _)| scope_name == disabled_scope_name)
{
Err(anyhow!(
"language {:?} has overrides in config not in query: {disabled_scope_name:?}",
self.config.name
))?;
}
}
for (name, override_config) in override_configs_by_id.values_mut() {
override_config.disabled_bracket_ixs = self
.config
.brackets
.disabled_scopes_by_bracket_ix
.iter()
.enumerate()
.filter_map(|(ix, disabled_scope_names)| {
if disabled_scope_names.contains(name) {
Some(ix as u16)
} else {
None
}
})
.collect();
}
self.config.brackets.disabled_scopes_by_bracket_ix.clear();
self.grammar_mut().override_config = Some(OverrideConfig {
query,
values: override_configs_by_id,
});
Ok(self)
}
@@ -1132,12 +1200,26 @@ impl LanguageScope {
.map(|e| (&e.0, &e.1))
}
pub fn brackets(&self) -> &[BracketPair] {
Override::as_option(
self.config_override().map(|o| &o.brackets),
Some(&self.language.config.brackets),
)
.map_or(&[], Vec::as_slice)
pub fn brackets(&self) -> impl Iterator<Item = (&BracketPair, bool)> {
let mut disabled_ids = self
.config_override()
.map_or(&[] as _, |o| o.disabled_bracket_ixs.as_slice());
self.language
.config
.brackets
.pairs
.iter()
.enumerate()
.map(move |(ix, bracket)| {
let mut is_enabled = true;
if let Some(next_disabled_ix) = disabled_ids.first() {
if ix == *next_disabled_ix as usize {
disabled_ids = &disabled_ids[1..];
is_enabled = false;
}
}
(bracket, is_enabled)
})
}
pub fn should_autoclose_before(&self, c: char) -> bool {
@@ -1148,7 +1230,7 @@ impl LanguageScope {
let id = self.override_id?;
let grammar = self.language.grammar.as_ref()?;
let override_config = grammar.override_config.as_ref()?;
override_config.values.get(&id)
override_config.values.get(&id).map(|e| &e.1)
}
}

View File

@@ -608,6 +608,31 @@ impl SyntaxSnapshot {
self.layers = layers;
self.interpolated_version = text.version.clone();
self.parsed_version = text.version.clone();
#[cfg(debug_assertions)]
self.check_invariants(text);
}
#[cfg(debug_assertions)]
fn check_invariants(&self, text: &BufferSnapshot) {
let mut max_depth = 0;
let mut prev_range: Option<Range<Anchor>> = None;
for layer in self.layers.iter() {
if layer.depth == max_depth {
if let Some(prev_range) = prev_range {
match layer.range.start.cmp(&prev_range.start, text) {
Ordering::Less => panic!("layers out of order"),
Ordering::Equal => {
assert!(layer.range.end.cmp(&prev_range.end, text).is_ge())
}
Ordering::Greater => {}
}
}
} else if layer.depth < max_depth {
panic!("layers out of order")
}
max_depth = layer.depth;
prev_range = Some(layer.range.clone());
}
}
pub fn single_tree_captures<'a>(
@@ -1419,7 +1444,7 @@ impl sum_tree::Summary for SyntaxLayerSummary {
self.max_depth = other.max_depth;
self.range = other.range.clone();
} else {
if other.range.start.cmp(&self.range.start, buffer).is_lt() {
if self.range == (Anchor::MAX..Anchor::MAX) {
self.range.start = other.range.start;
}
if other.range.end.cmp(&self.range.end, buffer).is_gt() {

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