Compare commits

...

172 Commits

Author SHA1 Message Date
Mikayla Maki
fa77450a72 v0.70.x preview 2023-01-18 12:39:21 -08:00
Petros Amoiridis
6cc84a77c8 Merge pull request #2042 from zed-industries/fix-pasting-files
Allow pasting the same entry more than once in project panel
2023-01-18 18:37:31 +02:00
Petros Amoiridis
27a6951403 Allow pasting the same entry more than once in project panel
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-01-18 17:35:21 +02:00
Petros Amoiridis
9f3c8c1e3a Merge pull request #2041 from zed-industries/fix-renaming-file
Fix mouse interrupting file/dir editing in project panel
2023-01-18 15:53:08 +02:00
Petros Amoiridis
f8d092fdc6 Fix mouse interrupting file/dir editing in project panel
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-01-18 15:22:20 +02:00
Kay Simmons
2c3c8b4cb0 Merge pull request #2039 from zed-industries/vim-mode-single-line-editors
disable vim mode in non full editors
2023-01-17 18:43:16 -08:00
Kay Simmons
cf193154e1 fix broken test 2023-01-17 17:35:39 -08:00
Kay Simmons
c3518cefe8 disable vim mode in non full editors 2023-01-17 17:32:10 -08:00
Kay Simmons
4746fb5936 Merge pull request #2038 from zed-industries/fix-sidebar-width-with-dock
Fix issue with sidebars resizing themselves when dock is toggled
2023-01-17 17:22:24 -08:00
Kay Simmons
c9a306b4ac Change sidebars to use the window width as a max width rather than participating in the flex
co-authored-by: Mikayla <mikayla@zed.dev>
2023-01-17 16:58:55 -08:00
Joseph T. Lyons
c3b102f5a8 Add users to mailing list when using an invite link 2023-01-17 16:46:01 -05:00
Max Brunsfeld
f61b870db6 Merge pull request #2034 from zed-industries/tab-focus-search
Use tab instead of command-f to move focus from the search editor to the main editor
2023-01-17 10:25:04 -08:00
Max Brunsfeld
1a6a807db5 Merge pull request #2035 from zed-industries/always-auto-indent-block-on-paste
Always auto-indent in block-wise mode when pasting
2023-01-17 10:24:41 -08:00
Antonio Scandurra
01aac0de48 Merge pull request #2036 from zed-industries/spurious-modified-buffers
Fix buffers appearing as modified when guest joined after buffer had been saved
2023-01-17 18:21:21 +01:00
Antonio Scandurra
dc88a67f50 Fix assertions 2023-01-17 18:09:45 +01:00
Julia
5ce0472a75 Merge pull request #2037 from zed-industries/go-to-fit
Utilize fit autoscroll for various go-to actions
2023-01-17 10:49:22 -05:00
Antonio Scandurra
cc788dc5f7 Verify saved_version, saved_version_fingerprint and saved_mtime 2023-01-17 16:46:06 +01:00
Julia
7726a9ec3d Utilize fit autoscroll for various go-to actions 2023-01-17 10:42:53 -05:00
Antonio Scandurra
fcf97ab41e Bump protocol version 2023-01-17 16:32:54 +01:00
Antonio Scandurra
bb200aa082 Relay saved version metadata to ensure buffers modified state converges 2023-01-17 16:32:54 +01:00
Antonio Scandurra
2cd9db1cfe Ensure Buffer::{is_dirty,has_conflict} converge in randomized test 2023-01-17 16:32:51 +01:00
Antonio Scandurra
467e5691b9 Include saved mtime and fingerprint when serializing buffers
This still doesn't include:

- An assertion in the randomized test to ensure buffers are not spuriously
marked as modified
- Sending an update when synchronizing buffers after a reconnection
2023-01-17 10:46:19 +01:00
Max Brunsfeld
0bd6f9b6ce Add a test for block-wise auto-indent without original indent info 2023-01-16 18:06:58 -08:00
Max Brunsfeld
244f259331 Always auto-indent in block-wise mode when pasting
If the text was copied outside of Zed, so the original indent column is unknown,
then act as if the first line was copied in its entirety.
2023-01-16 17:42:06 -08:00
Max Brunsfeld
625151806a Merge pull request #2022 from zed-industries/restart-lsp-after-invalid-version-reported
Fix crash when restarting a language server after it reports an unknown buffer version
2023-01-16 16:26:50 -08:00
Max Brunsfeld
6810490bf4 Remove tree-sitter dependency from gpui 2023-01-16 16:11:13 -08:00
Max Brunsfeld
3312a06368 Move focus back from buffer search using tab, not cmd-f 2023-01-16 16:01:15 -08:00
Max Brunsfeld
373902d933 Add '>' child operator in keymap context predicates 2023-01-16 16:00:46 -08:00
Max Brunsfeld
f62d13de21 Use a hand-coded parser for keymap context predicates 2023-01-16 15:53:49 -08:00
Julia
df2e9625b3 Merge pull request #2033 from zed-industries/open-with-zed
Make Finder "Open With" work correctly
2023-01-16 16:39:02 -05:00
Julia
765773cfe6 Make Finder "Open With" work correctly 2023-01-16 16:34:10 -05:00
Max Brunsfeld
9e5612348c Merge pull request #2032 from zed-industries/drag-split-dock-panic
Fix panic when trying to create a split in the dock by dragging
2023-01-16 11:51:28 -08:00
Max Brunsfeld
aa9710f7c3 Avoid unwrapping pane split in SplitWithProjectEntry
Also, implement pane-splitting operations more consistently.
2023-01-16 11:46:47 -08:00
Max Brunsfeld
b90e1012bf Don't render split drag targets in the dock 2023-01-16 10:24:17 -08:00
Antonio Scandurra
96186a3dae Merge pull request #2030 from zed-industries/fix-typescript-lsp
Fix error when running TypeScript language server after version 3.0.2
2023-01-16 17:33:44 +01:00
Antonio Scandurra
2c1fd7b0bf Add a 5s timeout when running npm info and npm install
This prevents those two commands from getting stuck when there is
no internet connection.
2023-01-16 16:51:45 +01:00
Antonio Scandurra
9779663c6b Use cli.mjs when available in TypeScript language server
Otherwise, fall back to using `cli.js`.
2023-01-16 16:50:30 +01:00
Joseph T. Lyons
8e02266d07 Add Discourse release action 2023-01-14 02:30:21 -05:00
Mikayla Maki
24ef80f4b6 Merge pull request #2027 from zed-industries/fix-keybindings-in-command-palette
Fix bug where keybindings would not show in command palette
2023-01-11 16:40:04 -08:00
Mikayla Maki
febf992a43 Fix bug where keybindings would not show in command palette 2023-01-11 16:35:49 -08:00
Kay Simmons
e9fdb13cb5 Merge pull request #2025 from zed-industries/vim-r
Vim replace
2023-01-11 16:28:39 -08:00
Kay Simmons
216b1aec08 fix replace in normal and visual modes 2023-01-11 14:57:40 -08:00
Max Brunsfeld
02f6928328 collab 0.5.2 2023-01-11 14:00:44 -08:00
Max Brunsfeld
fe27f135c0 Bump protocol version after reconnect support 2023-01-11 14:00:16 -08:00
Max Brunsfeld
74f8b493b2 collab 0.5.1 2023-01-11 13:25:28 -08:00
Max Brunsfeld
49379924cb Avoid dropping is_complete column for backward compatibility
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2023-01-11 13:25:02 -08:00
Kay Simmons
14eec66e38 in progress 2023-01-11 12:10:55 -08:00
Mikayla Maki
048da9ddce collab 0.5.0 2023-01-11 10:50:16 -08:00
Mikayla Maki
9c627e82a0 v0.70.x dev 2023-01-11 10:34:11 -08:00
Mikayla Maki
14899d867e Merge pull request #2020 from zed-industries/telemtry-opt-out
Telemetry opt out
2023-01-10 17:43:30 -08:00
Max Brunsfeld
db831c3fbb Remove roadmap from readme 2023-01-10 17:38:34 -08:00
Mikayla Maki
bfb43c67f8 Silence spurious log error
co-authored-by: Kay <kay@zed.dev>
2023-01-10 16:50:54 -08:00
Mikayla Maki
a3da41bfad Fix test failures due to dependency on Settings global in client for telemetry
co-authored-by: kay <kay@zed.dev>
2023-01-10 16:39:03 -08:00
Max Brunsfeld
ef987cae6b Merge pull request #2019 from zed-industries/panic-activating-next-pane-in-dock
Fix crash when activating prev/next pane while dock is active
2023-01-10 16:27:39 -08:00
Max Brunsfeld
41ff42ddec Fix crash when restarting a language server after it reports an unknown buffer version
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2023-01-10 16:27:15 -08:00
Mikayla Maki
37a4de1a84 Add opt-out for metric reporting
co-authored-by: kay <kay@zed.dev>
2023-01-10 15:49:54 -08:00
Max Brunsfeld
551dc1f318 Fix crash when activating prev/next pane while dock is active
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2023-01-10 15:32:14 -08:00
Mikayla Maki
866f0e1344 Add the ability to opt-out of panic reporting
Co-authored-by: Kay <kay@zed.dev>
2023-01-10 15:07:01 -08:00
Kay Simmons
a222821dfa Merge pull request #2017 from zed-industries/dont-save-single-file-workspaces
Don't save single file worktrees
2023-01-09 17:31:34 -08:00
Mikayla Maki
d49a29d793 Merge pull request #2016 from zed-industries/serialization-updates
Serialization touch ups
2023-01-09 16:18:30 -08:00
Kay Simmons
176738d674 Address issue with workspaces where single file worktrees such as those from git commit messages would get restored
Co-authored-by: Mikayla <mikayla@zed.dev>
2023-01-09 16:18:04 -08:00
Mikayla Maki
ebbe6e7aa9 Add serializing and restoring editor scroll position
Co-authored-by: Kay <kay@zed.dev>
2023-01-09 14:06:40 -08:00
Mikayla Maki
d237bdaa9b Added support for ALTER TABLE syntax in the syntax error checker function
Co-authored-by: Kay <kay@zed.dev>
2023-01-09 12:41:37 -08:00
Mikayla Maki
828f406b4f Fixed issue where serialized terminal working directories would be lost in complex interactions
Co-authored-by: Kay <kay@zed.dev>
Co-authored-by: Julia <julia@zed.dev>
2023-01-09 10:54:13 -08:00
Mikayla Maki
e743f3b1d8 Merge pull request #2015 from zed-industries/screenshare-on-terminal
Added open screenshare when following into non-followable buffer
2023-01-09 10:28:46 -08:00
Mikayla Maki
69e28d04b0 Added open screenshare when following into non-followable buffer 2023-01-09 10:19:11 -08:00
Julia
2be4f41964 Merge pull request #2013 from zed-industries/autocomplete-require-word-start-match
Require first codepoint of autocomplete query to match the first codepoint of some completion's subword
2023-01-09 13:06:43 -05:00
Julia
97ed89a797 Test that completion word splitting does reasonable things 2023-01-09 13:02:44 -05:00
Antonio Scandurra
ad7eaca443 Make Buffer::diff_base available outside of tests 2023-01-08 09:36:58 -07:00
Antonio Scandurra
ddbf251b5f Merge pull request #2014 from zed-industries/git-diff-reconnect
Update git diff base when synchronizing a guest's buffers
2023-01-08 09:28:51 -07:00
Antonio Scandurra
95098e4f29 Update git diff base when synchronizing a guest's buffers 2023-01-08 09:10:57 -07:00
Antonio Scandurra
529ccbda3a Introduce git index mutations to randomized collaboration test
The test now fails at the following seed:

```bash
SEED=850 ITERATIONS=1 OPERATIONS=131 cargo test --package=collab random
```
2023-01-08 08:52:16 -07:00
Julia
a46ca32356 Completion word start filtering which is codepoint aware 2023-01-07 15:34:28 -05:00
Julia
12cd712b53 Require start autocomplete query byte to match a completion word start byte 2023-01-06 22:47:06 -05:00
Nathan Sobo
3cffee4065 Merge pull request #2011 from zed-industries/project-reconnection
Retain connection to remote projects when temporarily disconnected
2023-01-06 18:01:08 -07:00
Nathan Sobo
213658f1e9 Fix tests that failed due to defaulting the grouping interval to zero in tests 2023-01-06 17:56:21 -07:00
Kay Simmons
6b337914d7 Merge pull request #2010 from zed-industries/vim-f-t
Vim f and t bindings
2023-01-06 16:32:39 -08:00
Nathan Sobo
386f7ba16d Merge remote-tracking branch 'origin/main' into project-reconnection 2023-01-06 16:52:22 -07:00
Kay Simmons
73e7967a12 working f and t bindings 2023-01-06 14:24:20 -08:00
Antonio Scandurra
83c98ce049 Prevent making further requests after language server shut down
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-01-06 13:40:32 -07:00
Max Brunsfeld
6a57bd2794 Merge pull request #2008 from zed-industries/callback-leaks
Fix callback leaks when subscriptions are added and dropped in the same effect cycle
2023-01-06 12:01:27 -08:00
Antonio Scandurra
8487ae77e7 Share new worktrees when resharing project
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-01-06 12:58:19 -07:00
Max Brunsfeld
b762d70202 Remove unused CallbackCollection method 2023-01-06 11:51:36 -08:00
Max Brunsfeld
53cb3a4429 Remove GC step for callback collections, always drop callbacks asap 2023-01-06 11:33:50 -08:00
Max Brunsfeld
ef192a902a Remove dropped subscription eagerly when removing callbacks 2023-01-06 11:03:45 -08:00
Antonio Scandurra
585c23e9f6 Match guest's reported buffers on host when synchronizing after reconnect
If the host thinks a guest has a buffer that they don't have, the host won't
send it to them when they attempt to open it the next time. This can happen
if the guest disconnected before they received the host's response to an
initial open buffer request.

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-01-06 11:48:34 -07:00
Max Brunsfeld
4708f5d88f Add test for notifying and dropping subscriptions in an update cycle 2023-01-06 10:46:03 -08:00
Max Brunsfeld
a165cd596b Make event tests in gpui more consistent 2023-01-06 10:44:45 -08:00
Antonio Scandurra
0d31c8c1c8 Only share worktrees when UpdateProject succeeded 2023-01-06 10:41:11 -07:00
Antonio Scandurra
8c5a0ca3a4 Couple worktree sharing with project metadata updates 2023-01-06 10:31:36 -07:00
Antonio Scandurra
5c05b7d413 Ensure initial project metadata is sent when first sharing a project 2023-01-06 10:18:26 -07:00
Max Brunsfeld
3da69117ae Use a CallbackCollection for action dispatch observations 2023-01-06 09:15:53 -08:00
Nathan Sobo
4256a96051 Avoid holding project handle on a call that could hang
This fixes a leaked handle error.
2023-01-05 21:01:27 -07:00
Max Brunsfeld
82e9f736bd Use a CallbackCollection for release observations
Co-authored-by: Kay Simmons <kay@zed.dev>
2023-01-05 18:02:53 -08:00
Max Brunsfeld
fa620bf98f Fix logic error in dropping callback subscriptions
Co-authored-by: Kay Simmons <kay@zed.dev>
2023-01-05 17:30:39 -08:00
Max Brunsfeld
378f0c32fe Restructure callback subscriptions
Fix a callback leak that would occur when dropping a subscription
to a callback collection after triggering that callback, but before
processing the effect of *adding* the handler.

Co-authored-by: Kay Simmons <kay@zed.dev>
2023-01-05 16:41:23 -08:00
Nathan Sobo
77e322cb75 Wait for incomplete buffers when handling incoming buffer file updates 2023-01-05 13:50:25 -07:00
Julia
f669b8a029 Merge pull request #2007 from zed-industries/recent-projects-prefer-first-match
Prefer first max while fuzzy matching projects fixes unexpected behavior
2023-01-05 12:10:51 -05:00
Julia
09d57d1f26 Prefer first max while fuzzy matching projects fixes unexpected behavior 2023-01-05 11:27:50 -05:00
Nathan Sobo
7a629769b7 Re-request incomplete remote buffers when syncing buffers
Any buffers we requested but that haven't been fully sent will cause
outstainding open requests to hang. If we re-request them, any
waiting open requests will resume when the requested buffers finish
being created.

Co-authored-by: Max Brunsfeld <max@zed.dev>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2023-01-04 16:00:43 -07:00
Joseph T. Lyons
bd223f5a1f Merge pull request #2002 from zed-industries/appease-clippy
Appease clippy
2023-01-04 16:33:29 -05:00
Nathan Sobo
1006ada458 Update scan_id on worktree entries when there is a conflict
Forgetting to do this meant we were unable to sync changes with reconnecting
guests in some cases.
2023-01-04 13:59:16 -07:00
Mikayla Maki
79f8f08caf v0.69.x dev 2023-01-04 11:45:25 -08:00
Nathan Sobo
789bbf15b7 Update buffer files when synchronizing buffers
It's possible that the host was disconnected when attempting to notify
guests of a file save, so we need to transmit this in order to correctly
update the file's mtime.

Next failing seed OPERATIONS=200 SEED=6894
2023-01-04 12:33:48 -07:00
Nathan Sobo
1dd085fc92 Introduce completed_scan_id to worktree
We need to know the most recent scan id we have actually completed. This is to
handle the case where a guest disconnects when we're in the middle of streaming
worktree entries to them. When they reconnect, they need to report a scan_id
from before we started streaming the entries, because we have no record of when
the stream was interrupted.

Next failure:
SEED=5051 ITERATIONS=1 OPERATIONS=200 cargo test --release --package=collab random -- --nocapture
2023-01-03 18:26:57 -07:00
Julia
1e18480808 Merge pull request #2005 from zed-industries/tsserver-include-completion-detail
Include Typescript completion item `detail` field in completion label
2023-01-03 16:44:10 -05:00
Julia
93a634991b Include Typescript completion item detail field in completion label 2023-01-03 16:37:35 -05:00
Nathan Sobo
90fb9b53ad WIP 2023-01-03 13:30:14 -07:00
Julia
d0ce7b3516 Merge pull request #2003 from zed-industries/correct-ra-name-key-default-settings
Correct default settings' name key for RA in init options example
2023-01-03 13:51:03 -05:00
Julia
b94c265240 Correct default settings' name key for RA in init options example 2023-01-03 13:50:08 -05:00
Nathan Sobo
8d70a22fa3 Record failing seed 2023-01-02 21:12:39 -07:00
Nathan Sobo
a6ffcdd0cf Track open buffers when handling sync requests
When a host sends a buffer to a guest for the first time, they record that
they have done so in a set tied to that guest's peer id. When the guest
reconnects and syncs buffers, they do so under a different peer id, so we
need to be sure we track which buffers we have sent them to avoid sending
them the same buffer twice, which violates the guest's assumptions.
2023-01-02 20:27:59 -07:00
Max Brunsfeld
74843493f4 Assign fake fs entries' mtimes more consistently 2023-01-02 10:20:52 -08:00
Julia
6b62ce2aaa Merge pull request #2001 from zed-industries/dissmis-search-button
Add dismiss buffer search button & fix some faulty icon button styling
2023-01-02 11:21:16 -05:00
Julia
2b1118f597 Add dismiss buffer search button & fix some faulty icon button styling
Co-Authored-By: Nate Butler <nate@zed.dev>
2023-01-01 23:50:46 -05:00
Joseph Lyons
233b28a1b9 Appease clippy 2023-01-01 23:50:45 -05:00
Mikayla Maki
eeb21af841 Merge pull request #2000 from zed-industries/fix-line-seperator
Add other line seperators to regex normalization
2022-12-30 18:24:36 -08:00
Mikayla Maki
a5bccecd48 Add other line seperators to regex normalization 2022-12-30 18:18:02 -08:00
Joseph T. Lyons
0f818f2458 Merge pull request #1996 from zed-industries/add-close-clean-items-command
Add close clean items command
2022-12-29 14:12:04 -05:00
Joseph T. Lyons
7187cc8a4c Merge pull request #1994 from zed-industries/add-close-all-items-command
Add close all items command
2022-12-29 14:11:44 -05:00
Joseph Lyons
2bc36600d4 Rename variable 2022-12-29 13:43:56 -05:00
Joseph Lyons
60f29410ca Add close clean items command 2022-12-29 13:28:52 -05:00
Joseph Lyons
ca3c4566dd Add close all items command 2022-12-29 01:43:49 -05:00
Nathan Sobo
f3dee2d332 Remove printlns, found a failure
Failing seed:
SEED=416 MAX_PEERS=2 ITERATIONS=5000 OPERATIONS=159 cargo +beta test --package=collab random -- --nocapture
2022-12-27 17:01:31 -07:00
Nathan Sobo
273988b8d5 Set transaction group interval to ZERO by default in tests
We were seeing non-deterministic behavior in randomized tests when
generating backtraces took enough time to cause transactions to group
in some cases, but not group in others.

Tests will need to explicitly opt into grouping if they want it by
setting the interval explicitly. We have tests in the text module that
currently test the history grouping explicitly, but I'm not sure
it's needed elsewhere.
2022-12-27 16:47:28 -07:00
Joseph T. Lyons
b6337f59fd Merge pull request #1992 from zed-industries/add-home-and-end-key-support
Add home and end key support
2022-12-26 00:34:37 -05:00
Joseph Lyons
21a0df406f Add home and end key support 2022-12-26 00:24:26 -05:00
Max Brunsfeld
599acf0daa WIP - Panic immediately when detecting non-determinism via a change to the execution trace 2022-12-23 17:34:13 -08:00
Antonio Scandurra
6458a9144e WIP: failing randomized test
SEED=175 MAX_PEERS=2 ITERATIONS=1 OPERATIONS=159 cargo test --package=collab random -- --nocapture
2022-12-23 15:02:06 +01:00
Antonio Scandurra
344d05045d Avoid hanging waiting for operations when buffer has none 2022-12-23 12:26:48 +01:00
Antonio Scandurra
75803d8dbb Respond with an error when client hasn't got a registered handle 2022-12-23 11:53:13 +01:00
Joseph T. Lyons
04e053a216 Merge pull request #1991 from zed-industries/add-actions-for-requesting-features-and-filing-bug-reports
Add actions for requesting features and filing bug reports
2022-12-22 23:17:44 -05:00
Joseph Lyons
41bff3947c Add actions for requesting features and filing bug reports 2022-12-22 23:04:33 -05:00
Joseph T. Lyons
46152c6249 Merge pull request #1990 from zed-industries/add-memory-to-system-specs
Add memory to system specs
2022-12-22 18:16:50 -05:00
Joseph Lyons
f65fda2fa4 Add memory to system specs 2022-12-22 18:10:49 -05:00
Joseph T. Lyons
96ac650465 Merge pull request #1989 from zed-industries/add-command-to-copy-system-information-to-the-clipboard
add command to copy system information to the clipboard
2022-12-22 14:31:23 -05:00
Joseph Lyons
ea16082a42 Factored data into a SystemSpecs struct
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2022-12-22 14:27:32 -05:00
Max Brunsfeld
42e74e7eef Excluded deleted entries when initially sending worktrees to guests
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2022-12-22 11:18:10 -08:00
Antonio Scandurra
738e161bc6 WIP: failing test
SEED=882 RUST_LOG=collab::tests::randomized_integration_tests=info MAX_PEERS=2 ITERATIONS=1 OPERATIONS=49 cargo test --package=collab random -- --nocapture
2022-12-22 18:32:21 +01:00
Antonio Scandurra
559e14799c Restructure randomized test to be a bit clearer and test more stuff 2022-12-22 17:54:25 +01:00
Joseph Lyons
eeb5b03d63 add command to copy system information to the clipboard 2022-12-22 03:43:04 -05:00
Max Brunsfeld
d750b02a7c Handle file and diff updates to incomplete buffers
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2022-12-21 15:39:57 -08:00
Max Brunsfeld
c321f5d94a Assert that buffers' file state matches in randomized collab test
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2022-12-21 15:38:44 -08:00
Max Brunsfeld
89da738fae In randomized test, open remote projects via the room
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2022-12-21 14:13:43 -08:00
Max Brunsfeld
8cd94060bb 💄 Avoid referring to all clients as guests in random integration test 2022-12-21 11:37:18 -08:00
Max Brunsfeld
d8ccdff9fc Move randomized integration test into its own file 2022-12-21 11:26:24 -08:00
Antonio Scandurra
47348542ef Synchronize buffers when either the host or a guest reconnects 2022-12-21 14:20:56 +01:00
Antonio Scandurra
b5fb8e6b8b Remove unused JoinProjectError 2022-12-21 13:10:07 +01:00
Antonio Scandurra
b0336cd27e Add failing test for buffer synchronization after disconnecting 2022-12-21 11:56:15 +01:00
Antonio Scandurra
ecd80c553c Verify removing worktrees while host is offline 2022-12-21 11:47:01 +01:00
Antonio Scandurra
59d7f06c57 Handle proto::UpdateProjectCollaborator message in Project 2022-12-21 11:09:27 +01:00
Max Brunsfeld
15f666a50a Refresh project collaborator connection id for rejoined projects 2022-12-20 18:03:33 -08:00
Max Brunsfeld
ec6f2a3ad4 💄 Reorder private Project method 2022-12-20 17:32:42 -08:00
Max Brunsfeld
213be3d6bd Delete stale projects after cleanup interval, via server foreign key cascade 2022-12-20 17:27:42 -08:00
Max Brunsfeld
55800fc696 💄 Avoid repeated sql condition in rejoin_room 2022-12-20 17:23:52 -08:00
Max Brunsfeld
6a2066af6c 💄 Reduce indentation in Database::rejoin_room 2022-12-20 17:16:56 -08:00
Max Brunsfeld
cb8962691a Remove unnecessary UnshareProject message sent to clients leaving a project 2022-12-20 16:58:44 -08:00
Max Brunsfeld
bb00134f5f Clean up projects when leaving a room 2022-12-20 16:44:57 -08:00
Max Brunsfeld
21d6665c37 Merge branch 'main' into project-reconnection 2022-12-20 15:50:09 -08:00
Max Brunsfeld
6542b30d1f Implement rejoining projects as guest when rejoining a room
Co-authored-by: Julia Risley <julia@zed.dev>
2022-12-20 15:02:26 -08:00
Max Brunsfeld
55ebfe8321 Handle unshared projects when rejoining a room
Also, construct remote projects via the room, to guarantee
that the room can manage the projects' sharing lifecycle.

Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2022-12-20 11:10:46 -08:00
Antonio Scandurra
9d15b3d295 Remove unused import 2022-12-20 17:47:22 +01:00
Antonio Scandurra
d31fd9bbf2 Support adding worktrees to project while host is offline 2022-12-20 17:42:08 +01:00
Antonio Scandurra
52babc51a0 Make host reconnection test pass when mutating worktree while offline 2022-12-20 17:30:58 +01:00
Antonio Scandurra
1a3940a12e Fix project reconnection test to ensure rooms actually reconnects 2022-12-20 14:51:46 +01:00
Antonio Scandurra
1aec691b35 Sketch out project reconnection routine on the server 2022-12-20 12:03:43 +01:00
Max Brunsfeld
70dd586be9 Start work on rejoining rooms, supplying all project info at once
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2022-12-19 17:50:43 -08:00
Max Brunsfeld
af85db9ea5 WIP - Retain hosts' project state when they disconnect 2022-12-19 11:38:08 -08:00
Max Brunsfeld
67b265b3d5 Add failing integration test for resharing projects on reconnect 2022-12-19 11:37:28 -08:00
Antonio Scandurra
0ede89d82a WIP 2022-12-19 20:05:00 +01:00
Antonio Scandurra
d8219545c9 💄 2022-12-19 16:17:27 +01:00
Antonio Scandurra
06f6d02579 Stop counting extensions in worktree 2022-12-19 16:05:22 +01:00
147 changed files with 7212 additions and 5858 deletions

View File

@@ -21,6 +21,15 @@ jobs:
${{ 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:

49
Cargo.lock generated
View File

@@ -820,8 +820,10 @@ dependencies = [
"async-broadcast",
"client",
"collections",
"fs",
"futures 0.3.25",
"gpui",
"language",
"live_kit_client",
"log",
"media",
@@ -1131,7 +1133,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.4.2"
version = "0.5.2"
dependencies = [
"anyhow",
"async-tungstenite",
@@ -2561,7 +2563,6 @@ dependencies = [
"sum_tree",
"time 0.3.17",
"tiny-skia",
"tree-sitter",
"usvg",
"util",
"waker-fn",
@@ -2757,6 +2758,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "human_bytes"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39b528196c838e8b3da8b665e08c30958a6f2ede91d79f2ffcd0d4664b9c64eb"
[[package]]
name = "humantime"
version = "2.1.0"
@@ -3755,6 +3762,15 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "ntapi"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc"
dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@@ -4424,7 +4440,7 @@ source = "git+https://github.com/zed-industries/wezterm?rev=5cd757e5f2eb039ed0c6
dependencies = [
"libc",
"log",
"ntapi",
"ntapi 0.3.7",
"winapi 0.3.9",
]
@@ -4435,6 +4451,7 @@ dependencies = [
"aho-corasick",
"anyhow",
"async-trait",
"backtrace",
"client",
"clock",
"collections",
@@ -6219,6 +6236,21 @@ dependencies = [
"libc",
]
[[package]]
name = "sysinfo"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccb297c0afb439440834b4bcf02c5c9da8ec2e808e70f36b0d8e815ff403bd24"
dependencies = [
"cfg-if 1.0.0",
"core-foundation-sys",
"libc",
"ntapi 0.4.0",
"once_cell",
"rayon",
"winapi 0.3.9",
]
[[package]]
name = "system-interface"
version = "0.20.0"
@@ -7201,6 +7233,12 @@ dependencies = [
"serde",
]
[[package]]
name = "urlencoding"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
[[package]]
name = "usvg"
version = "0.14.1"
@@ -8148,7 +8186,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "zed"
version = "0.68.0"
version = "0.70.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -8180,6 +8218,7 @@ dependencies = [
"fuzzy",
"go_to_line",
"gpui",
"human_bytes",
"ignore",
"image",
"indexmap",
@@ -8213,6 +8252,7 @@ dependencies = [
"smallvec",
"smol",
"sum_tree",
"sysinfo",
"tempdir",
"terminal_view",
"text",
@@ -8241,6 +8281,7 @@ dependencies = [
"tree-sitter-typescript",
"unindent",
"url",
"urlencoding",
"util",
"vim",
"workspace",

View File

@@ -83,56 +83,3 @@ rustup target add wasm32-wasi
```
Plugins can be found in the `plugins` folder in the root. For more information about how plugins work, check the [Plugin Guide](./crates/plugin_runtime/README.md) in `crates/plugin_runtime/README.md`.
## Roadmap
We will organize our efforts around the following major milestones. We'll create tracking issues for each of these milestones to detail the individual tasks that comprise them.
### Minimal text editor
[Tracking issue](https://github.com/zed-industries/zed/issues/2)
Ship a minimal text editor to investors and other insiders. It should be extremely fast and stable, but all it can do is open, edit, and save text files, making it potentially useful for basic editing but not for real coding.
Establish basic infrastructure for building the app bundle and uploading an artifact. Once this is released, we should regularly distribute updates as features land.
### Collaborative code editor for internal use
[Tracking issue](https://github.com/zed-industries/zed/issues/6)
Turn the minimal text editor into a collaborative _code_ editor. This will include the minimal features that the Zed team needs to collaborate in Zed to build Zed without net loss in developer productivity. This includes productivity-critical features such as:
- Syntax highlighting and syntax-aware editing and navigation
- The ability to see and edit non-local working copies of a repository
- Language server support for Rust code navigation, refactoring, diagnostics, etc.
- Project browsing and project-wide search and replace
We want to tackle collaboration fairly early so that the rest of the design of the product can flow around that assumption. We could probably produce a single-player code editor more quickly, but at the risk of having collaboration feel more "bolted on" when we eventually add it.
### Private alpha for Rust teams on macOS
The "minimal" milestones were about getting Zed to a point where the Zed team could use Zed productively to build Zed. What features are required for someone outside the company to use Zed to productively work on another project that is also written in Rust?
This includes infrastructure like auto-updates, error reporting, and metrics collection. It also includes some amount of polish to make the tool more discoverable for someone that didn't write it, such as a UI for updating settings and key bindings. We may also need to enhance the server to support user authentication and related concerns.
The initial target audience is like us. A small team working in Rust that's potentially interested in collaborating. As the alpha proceeds, we can work with teams of different sizes.
### Private beta for Rust teams on macOS
Once we're getting sufficiently positive feedback from our initial alpha users, we widen the audience by letting people share invites. Now may be a good time to get Zed running on the web, so that it's extremely easy for a Zed user to share a link and be collaborating in seconds. Once someone is using Zed on the Web, we'll let them register for the private beta and download the native binary if they're on macOS.
### Expand to other languages
Depending on how the Rust beta is going, focus hard on dominating another niche language such as Elixr or getting a foothold within a niche of a larger language, such as React/Typescript. Alternatively, go wide at this point and add decent support several widely-used languages such as Python, Ruby, Typescript, etc. This would entail taking 1-2 weeks per language and making sure we ship a solid experience based on a publicly-available language server. Each language has slightly different development practices, so we need to make sure Zed's UX meshes well with those practices.
### Future directions
Each of these sections could probably broken into multiple milestones, but this part of the roadmap is too far in the future to go into that level of detail at this point.
#### Expand to other platforms
Support Linux and Windows. We'll probably want to hire at least one person that prefers to work on each respective platform and have them spearhead the effort to port Zed to that platform. Once they've done so, they can join the general development effort while ensuring the user experience stays good on that platform.
#### Expand on collaboration
To start with, we'll focus on synchronous collaboration because that's where we're most differentiated, but there's no reason we have to limit ourselves to that. How can our tool facilitate collaboration generally, whether it's sync or async? What would it take for a team to go 100% Zed and collaborate fully within the tool? If we haven't added it already, basic Git support would be nice.

View File

@@ -20,8 +20,10 @@
"alt-cmd-left": "pane::ActivatePrevItem",
"alt-cmd-right": "pane::ActivateNextItem",
"cmd-w": "pane::CloseActiveItem",
"cmd-shift-w": "workspace::CloseWindow",
"alt-cmd-t": "pane::CloseInactiveItems",
"cmd-k u": "pane::CloseCleanItems",
"cmd-k cmd-w": "pane::CloseAllItems",
"cmd-shift-w": "workspace::CloseWindow",
"cmd-s": "workspace::Save",
"cmd-shift-s": "workspace::SaveAs",
"cmd-=": "zed::IncreaseBufferFontSize",
@@ -67,9 +69,11 @@
"up": "editor::MoveUp",
"pageup": "editor::PageUp",
"shift-pageup": "editor::MovePageUp",
"home": "editor::MoveToBeginningOfLine",
"down": "editor::MoveDown",
"pagedown": "editor::PageDown",
"shift-pagedown": "editor::MovePageDown",
"end": "editor::MoveToEndOfLine",
"left": "editor::MoveLeft",
"right": "editor::MoveRight",
"ctrl-p": "editor::MoveUp",
@@ -110,6 +114,12 @@
"stop_at_soft_wraps": true
}
],
"shift-home": [
"editor::SelectToBeginningOfLine",
{
"stop_at_soft_wraps": true
}
],
"ctrl-shift-a": [
"editor::SelectToBeginningOfLine",
{
@@ -122,6 +132,12 @@
"stop_at_soft_wraps": true
}
],
"shift-end": [
"editor::SelectToEndOfLine",
{
"stop_at_soft_wraps": true
}
],
"ctrl-shift-e": [
"editor::SelectToEndOfLine",
{
@@ -170,10 +186,10 @@
}
},
{
"context": "BufferSearchBar",
"context": "BufferSearchBar > Editor",
"bindings": {
"escape": "buffer_search::Dismiss",
"cmd-f": "buffer_search::FocusEditor",
"tab": "buffer_search::FocusEditor",
"enter": "search::SelectNextMatch",
"shift-enter": "search::SelectPrevMatch"
}

View File

@@ -1,6 +1,6 @@
[
{
"context": "Editor && VimControl",
"context": "Editor && VimControl && !VimWaiting",
"bindings": {
"g": [
"vim::PushOperator",
@@ -53,6 +53,42 @@
}
],
"%": "vim::Matching",
"ctrl-y": [
"vim::Scroll",
"LineUp"
],
"f": [
"vim::PushOperator",
{
"FindForward": {
"before": false
}
}
],
"t": [
"vim::PushOperator",
{
"FindForward": {
"before": true
}
}
],
"shift-f": [
"vim::PushOperator",
{
"FindBackward": {
"after": false
}
}
],
"shift-t": [
"vim::PushOperator",
{
"FindBackward": {
"after": true
}
}
],
"escape": "editor::Cancel",
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
"1": [
@@ -94,7 +130,7 @@
}
},
{
"context": "Editor && vim_mode == normal && vim_operator == none",
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
"bindings": {
"c": [
"vim::PushOperator",
@@ -174,9 +210,9 @@
"vim::Scroll",
"LineDown"
],
"ctrl-y": [
"vim::Scroll",
"LineUp"
"r": [
"vim::PushOperator",
"Replace"
]
}
},
@@ -255,14 +291,18 @@
}
},
{
"context": "Editor && vim_mode == visual",
"context": "Editor && vim_mode == visual && !VimWaiting",
"bindings": {
"u": "editor::Undo",
"c": "vim::VisualChange",
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",
"y": "vim::VisualYank",
"p": "vim::VisualPaste"
"p": "vim::VisualPaste",
"r": [
"vim::PushOperator",
"Replace"
]
}
},
{
@@ -271,5 +311,11 @@
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore"
}
},
{
"context": "Editor && VimWaiting",
"bindings": {
"*": "gpui::KeyPressed"
}
}
]

View File

@@ -79,6 +79,13 @@
"hard_tabs": false,
// How many columns a tab should occupy.
"tab_size": 4,
// Control what info Zed sends to our servers
"telemetry": {
// Send debug info like crash reports.
"diagnostics": true,
// Send anonymized usage data like what languages you're using Zed with.
"metrics": true
},
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
@@ -221,7 +228,7 @@
// rust-analyzer
// typescript-language-server
// vscode-json-languageserver
// "rust_analyzer": {
// "rust-analyzer": {
// //These initialization options are merged into Zed's defaults
// "initialization_options": {
// "checkOnSave": {

View File

@@ -23,6 +23,8 @@ collections = { path = "../collections" }
gpui = { path = "../gpui" }
log = "0.4"
live_kit_client = { path = "../live_kit_client" }
fs = { path = "../fs" }
language = { path = "../language" }
media = { path = "../media" }
project = { path = "../project" }
util = { path = "../util" }
@@ -34,6 +36,8 @@ postage = { version = "0.4.1", features = ["futures-traits"] }
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }
fs = { path = "../fs", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }

View File

@@ -7,11 +7,13 @@ use client::{
proto::{self, PeerId},
Client, TypedEnvelope, User, UserStore,
};
use collections::{BTreeMap, HashSet};
use collections::{BTreeMap, HashMap, HashSet};
use fs::Fs;
use futures::{FutureExt, StreamExt};
use gpui::{
AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakModelHandle,
};
use language::LanguageRegistry;
use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate};
use postage::stream::Stream;
use project::Project;
@@ -43,6 +45,8 @@ pub struct Room {
id: u64,
live_kit: Option<LiveKitRoom>,
status: RoomStatus,
shared_projects: HashSet<WeakModelHandle<Project>>,
joined_projects: HashSet<WeakModelHandle<Project>>,
local_participant: LocalParticipant,
remote_participants: BTreeMap<u64, RemoteParticipant>,
pending_participants: Vec<Arc<User>>,
@@ -62,7 +66,7 @@ impl Entity for Room {
fn release(&mut self, _: &mut MutableAppContext) {
if self.status.is_online() {
log::info!("room was released, sending leave message");
self.client.send(proto::LeaveRoom {}).log_err();
let _ = self.client.send(proto::LeaveRoom {});
}
}
}
@@ -132,6 +136,8 @@ impl Room {
id,
live_kit: live_kit_room,
status: RoomStatus::Online,
shared_projects: Default::default(),
joined_projects: Default::default(),
participant_user_ids: Default::default(),
local_participant: Default::default(),
remote_participants: Default::default(),
@@ -234,6 +240,22 @@ impl Room {
cx.notify();
cx.emit(Event::Left);
log::info!("leaving room");
for project in self.shared_projects.drain() {
if let Some(project) = project.upgrade(cx) {
project.update(cx, |project, cx| {
project.unshare(cx).log_err();
});
}
}
for project in self.joined_projects.drain() {
if let Some(project) = project.upgrade(cx) {
project.update(cx, |project, cx| {
project.disconnected_from_host(cx);
});
}
}
self.status = RoomStatus::Offline;
self.remote_participants.clear();
self.pending_participants.clear();
@@ -257,16 +279,15 @@ impl Room {
.next()
.await
.map_or(false, |s| s.is_connected());
// Even if we're initially connected, any future change of the status means we momentarily disconnected.
if !is_connected || client_status.next().await.is_some() {
log::info!("detected client disconnection");
let room_id = this
.upgrade(&cx)
this.upgrade(&cx)
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, cx| {
this.status = RoomStatus::Rejoining;
cx.notify();
this.id
});
// Wait for client to re-establish a connection to the server.
@@ -279,31 +300,21 @@ impl Room {
"waiting for client status change, remaining attempts {}",
remaining_attempts
);
if let Some(status) = client_status.next().await {
if status.is_connected() {
log::info!("client reconnected, attempting to rejoin room");
let rejoin_room = async {
let response =
client.request(proto::JoinRoom { id: room_id }).await?;
let room_proto =
response.room.ok_or_else(|| anyhow!("invalid room"))?;
this.upgrade(&cx)
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, cx| {
this.status = RoomStatus::Online;
this.apply_room_update(room_proto, cx)
})?;
anyhow::Ok(())
};
let Some(status) = client_status.next().await else { break };
if status.is_connected() {
log::info!("client reconnected, attempting to rejoin room");
if rejoin_room.await.log_err().is_some() {
return true;
} else {
remaining_attempts -= 1;
}
let Some(this) = this.upgrade(&cx) else { break };
if this
.update(&mut cx, |this, cx| this.rejoin(cx))
.await
.log_err()
.is_some()
{
return true;
} else {
remaining_attempts -= 1;
}
} else {
return false;
}
}
false
@@ -340,6 +351,82 @@ impl Room {
}
}
fn rejoin(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let mut projects = HashMap::default();
let mut reshared_projects = Vec::new();
let mut rejoined_projects = Vec::new();
self.shared_projects.retain(|project| {
if let Some(handle) = project.upgrade(cx) {
let project = handle.read(cx);
if let Some(project_id) = project.remote_id() {
projects.insert(project_id, handle.clone());
reshared_projects.push(proto::UpdateProject {
project_id,
worktrees: project.worktree_metadata_protos(cx),
});
return true;
}
}
false
});
self.joined_projects.retain(|project| {
if let Some(handle) = project.upgrade(cx) {
let project = handle.read(cx);
if let Some(project_id) = project.remote_id() {
projects.insert(project_id, handle.clone());
rejoined_projects.push(proto::RejoinProject {
id: project_id,
worktrees: project
.worktrees(cx)
.map(|worktree| {
let worktree = worktree.read(cx);
proto::RejoinWorktree {
id: worktree.id().to_proto(),
scan_id: worktree.completed_scan_id() as u64,
}
})
.collect(),
});
}
return true;
}
false
});
let response = self.client.request(proto::RejoinRoom {
id: self.id,
reshared_projects,
rejoined_projects,
});
cx.spawn(|this, mut cx| async move {
let response = response.await?;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
this.update(&mut cx, |this, cx| {
this.status = RoomStatus::Online;
this.apply_room_update(room_proto, cx)?;
for reshared_project in response.reshared_projects {
if let Some(project) = projects.get(&reshared_project.id) {
project.update(cx, |project, cx| {
project.reshared(reshared_project, cx).log_err();
});
}
}
for rejoined_project in response.rejoined_projects {
if let Some(project) = projects.get(&rejoined_project.id) {
project.update(cx, |project, cx| {
project.rejoined(rejoined_project, cx).log_err();
});
}
}
anyhow::Ok(())
})
})
}
pub fn id(&self) -> u64 {
self.id
}
@@ -454,6 +541,20 @@ impl Room {
}
for unshared_project_id in old_projects.difference(&new_projects) {
this.joined_projects.retain(|project| {
if let Some(project) = project.upgrade(cx) {
project.update(cx, |project, cx| {
if project.remote_id() == Some(*unshared_project_id) {
project.disconnected_from_host(cx);
false
} else {
true
}
})
} else {
false
}
});
cx.emit(Event::RemoteProjectUnshared {
project_id: *unshared_project_id,
});
@@ -630,6 +731,32 @@ impl Room {
})
}
pub fn join_project(
&mut self,
id: u64,
language_registry: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<Project>>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
cx.spawn(|this, mut cx| async move {
let project =
Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;
this.update(&mut cx, |this, cx| {
this.joined_projects.retain(|project| {
if let Some(project) = project.upgrade(cx) {
!project.read(cx).is_read_only()
} else {
false
}
});
this.joined_projects.insert(project.downgrade());
});
Ok(project)
})
}
pub(crate) fn share_project(
&mut self,
project: ModelHandle<Project>,
@@ -641,31 +768,18 @@ impl Room {
let request = self.client.request(proto::ShareProject {
room_id: self.id(),
worktrees: project
.read(cx)
.worktrees(cx)
.map(|worktree| {
let worktree = worktree.read(cx);
proto::WorktreeMetadata {
id: worktree.id().to_proto(),
root_name: worktree.root_name().into(),
visible: worktree.is_visible(),
abs_path: worktree.abs_path().to_string_lossy().into(),
}
})
.collect(),
worktrees: project.read(cx).worktree_metadata_protos(cx),
});
cx.spawn(|this, mut cx| async move {
let response = request.await?;
project.update(&mut cx, |project, cx| {
project
.shared(response.project_id, cx)
.detach_and_log_err(cx)
});
project.shared(response.project_id, cx)
})?;
// If the user's location is in this project, it changes from UnsharedProject to SharedProject.
this.update(&mut cx, |this, cx| {
this.shared_projects.insert(project.downgrade());
let active_project = this.local_participant.active_project.as_ref();
if active_project.map_or(false, |location| *location == project) {
this.set_location(Some(&project), cx)

View File

@@ -25,6 +25,7 @@ use postage::watch;
use rand::prelude::*;
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
use serde::Deserialize;
use settings::{Settings, TelemetrySettings};
use std::{
any::TypeId,
collections::HashMap,
@@ -423,7 +424,9 @@ impl Client {
}));
}
Status::SignedOut | Status::UpgradeRequired => {
self.telemetry.set_authenticated_user_info(None, false);
let telemetry_settings = cx.read(|cx| cx.global::<Settings>().telemetry());
self.telemetry
.set_authenticated_user_info(None, false, telemetry_settings);
state._reconnect_task.take();
}
_ => {}
@@ -706,7 +709,13 @@ impl Client {
credentials = read_credentials_from_keychain(cx);
read_from_keychain = credentials.is_some();
if read_from_keychain {
self.report_event("read credentials from keychain", Default::default());
cx.read(|cx| {
self.report_event(
"read credentials from keychain",
Default::default(),
cx.global::<Settings>().telemetry(),
);
});
}
}
if credentials.is_none() {
@@ -997,6 +1006,8 @@ impl Client {
let executor = cx.background();
let telemetry = self.telemetry.clone();
let http = self.http.clone();
let metrics_enabled = cx.read(|cx| cx.global::<Settings>().telemetry());
executor.clone().spawn(async move {
// Generate a pair of asymmetric encryption keys. The public key will be used by the
// zed server to encrypt the user's access token, so that it can'be intercepted by
@@ -1079,7 +1090,11 @@ impl Client {
.context("failed to decrypt access token")?;
platform.activate(true);
telemetry.report_event("authenticate with browser", Default::default());
telemetry.report_event(
"authenticate with browser",
Default::default(),
metrics_enabled,
);
Ok(Credentials {
user_id: user_id.parse()?,
@@ -1235,6 +1250,7 @@ impl Client {
subscriber
} else {
log::info!("unhandled message {}", type_name);
self.peer.respond_with_unhandled_message(message).log_err();
return;
};
@@ -1278,6 +1294,7 @@ impl Client {
.detach();
} else {
log::info!("unhandled message {}", type_name);
self.peer.respond_with_unhandled_message(message).log_err();
}
}
@@ -1285,8 +1302,14 @@ impl Client {
self.telemetry.start();
}
pub fn report_event(&self, kind: &str, properties: Value) {
self.telemetry.report_event(kind, properties.clone());
pub fn report_event(
&self,
kind: &str,
properties: Value,
telemetry_settings: TelemetrySettings,
) {
self.telemetry
.report_event(kind, properties.clone(), telemetry_settings);
}
pub fn telemetry_log_file_path(&self) -> Option<PathBuf> {

View File

@@ -10,6 +10,7 @@ use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Serialize;
use serde_json::json;
use settings::TelemetrySettings;
use std::{
io::Write,
mem,
@@ -184,11 +185,18 @@ impl Telemetry {
.detach();
}
/// This method takes the entire TelemetrySettings struct in order to force client code
/// to pull the struct out of the settings global. Do not remove!
pub fn set_authenticated_user_info(
self: &Arc<Self>,
metrics_id: Option<String>,
is_staff: bool,
telemetry_settings: TelemetrySettings,
) {
if !telemetry_settings.metrics() {
return;
}
let this = self.clone();
let mut state = self.state.lock();
let device_id = state.device_id.clone();
@@ -221,7 +229,16 @@ impl Telemetry {
}
}
pub fn report_event(self: &Arc<Self>, kind: &str, properties: Value) {
pub fn report_event(
self: &Arc<Self>,
kind: &str,
properties: Value,
telemetry_settings: TelemetrySettings,
) {
if !telemetry_settings.metrics() {
return;
}
let mut state = self.state.lock();
let event = MixpanelEvent {
event: kind.to_string(),

View File

@@ -5,6 +5,7 @@ use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse};
use settings::Settings;
use std::sync::{Arc, Weak};
use util::TryFutureExt as _;
@@ -141,14 +142,11 @@ impl UserStore {
let fetch_metrics_id =
client.request(proto::GetPrivateUserInfo {}).log_err();
let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
if let Some(info) = info {
client.telemetry.set_authenticated_user_info(
Some(info.metrics_id.clone()),
info.staff,
);
} else {
client.telemetry.set_authenticated_user_info(None, false);
}
client.telemetry.set_authenticated_user_info(
info.as_ref().map(|info| info.metrics_id.clone()),
info.as_ref().map(|info| info.staff).unwrap_or(false),
cx.read(|cx| cx.global::<Settings>().telemetry()),
);
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.4.2"
version = "0.5.2"
[[bin]]
name = "collab"

View File

@@ -57,7 +57,8 @@ CREATE TABLE "worktrees" (
"abs_path" VARCHAR NOT NULL,
"visible" BOOL NOT NULL,
"scan_id" INTEGER NOT NULL,
"is_complete" BOOL NOT NULL,
"is_complete" BOOL NOT NULL DEFAULT FALSE,
"completed_scan_id" INTEGER NOT NULL,
PRIMARY KEY(project_id, id)
);
CREATE INDEX "index_worktrees_on_project_id" ON "worktrees" ("project_id");
@@ -65,6 +66,7 @@ CREATE INDEX "index_worktrees_on_project_id" ON "worktrees" ("project_id");
CREATE TABLE "worktree_entries" (
"project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL,
"scan_id" INTEGER NOT NULL,
"id" INTEGER NOT NULL,
"is_dir" BOOL NOT NULL,
"path" VARCHAR NOT NULL,
@@ -73,6 +75,7 @@ CREATE TABLE "worktree_entries" (
"mtime_nanos" INTEGER NOT NULL,
"is_symlink" BOOL NOT NULL,
"is_ignored" BOOL NOT NULL,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, id),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
);

View File

@@ -0,0 +1,3 @@
ALTER TABLE "worktree_entries"
ADD COLUMN "scan_id" INT8,
ADD COLUMN "is_deleted" BOOL;

View File

@@ -0,0 +1,3 @@
ALTER TABLE worktrees
ALTER COLUMN is_complete SET DEFAULT FALSE,
ADD COLUMN completed_scan_id INT8;

View File

@@ -353,6 +353,8 @@ pub struct CreateInviteFromCodeParams {
invite_code: String,
email_address: String,
device_id: Option<String>,
#[serde(default)]
added_to_mailing_list: bool,
}
async fn create_invite_from_code(
@@ -365,6 +367,7 @@ async fn create_invite_from_code(
&params.invite_code,
&params.email_address,
params.device_id.as_deref(),
params.added_to_mailing_list,
)
.await?,
))

View File

@@ -123,34 +123,6 @@ impl Database {
.await
}
pub async fn delete_stale_projects(
&self,
environment: &str,
new_server_id: ServerId,
) -> Result<()> {
self.transaction(|tx| async move {
let stale_server_epochs = self
.stale_server_ids(environment, new_server_id, &tx)
.await?;
project_collaborator::Entity::delete_many()
.filter(
project_collaborator::Column::ConnectionServerId
.is_in(stale_server_epochs.iter().copied()),
)
.exec(&*tx)
.await?;
project::Entity::delete_many()
.filter(
project::Column::HostConnectionServerId
.is_in(stale_server_epochs.iter().copied()),
)
.exec(&*tx)
.await?;
Ok(())
})
.await
}
pub async fn stale_room_ids(
&self,
environment: &str,
@@ -235,8 +207,8 @@ impl Database {
pub async fn delete_stale_servers(
&self,
new_server_id: ServerId,
environment: &str,
new_server_id: ServerId,
) -> Result<()> {
self.transaction(|tx| async move {
server::Entity::delete_many()
@@ -910,6 +882,7 @@ impl Database {
code: &str,
email_address: &str,
device_id: Option<&str>,
added_to_mailing_list: bool,
) -> Result<Invite> {
self.transaction(|tx| async move {
let existing_user = user::Entity::find()
@@ -961,6 +934,7 @@ impl Database {
platform_windows: ActiveValue::set(false),
platform_unknown: ActiveValue::set(true),
device_id: ActiveValue::set(device_id.map(|device_id| device_id.into())),
added_to_mailing_list: ActiveValue::set(added_to_mailing_list),
..Default::default()
})
.on_conflict(
@@ -1319,15 +1293,7 @@ impl Database {
Condition::all()
.add(room_participant::Column::RoomId.eq(room_id))
.add(room_participant::Column::UserId.eq(user_id))
.add(
Condition::any()
.add(room_participant::Column::AnsweringConnectionId.is_null())
.add(room_participant::Column::AnsweringConnectionLost.eq(true))
.add(
room_participant::Column::AnsweringConnectionServerId
.ne(connection.owner_id as i32),
),
),
.add(room_participant::Column::AnsweringConnectionId.is_null()),
)
.set(room_participant::ActiveModel {
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
@@ -1349,6 +1315,245 @@ impl Database {
.await
}
pub async fn rejoin_room(
&self,
rejoin_room: proto::RejoinRoom,
user_id: UserId,
connection: ConnectionId,
) -> Result<RoomGuard<RejoinedRoom>> {
self.room_transaction(|tx| async {
let tx = tx;
let room_id = RoomId::from_proto(rejoin_room.id);
let participant_update = room_participant::Entity::update_many()
.filter(
Condition::all()
.add(room_participant::Column::RoomId.eq(room_id))
.add(room_participant::Column::UserId.eq(user_id))
.add(room_participant::Column::AnsweringConnectionId.is_not_null())
.add(
Condition::any()
.add(room_participant::Column::AnsweringConnectionLost.eq(true))
.add(
room_participant::Column::AnsweringConnectionServerId
.ne(connection.owner_id as i32),
),
),
)
.set(room_participant::ActiveModel {
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
answering_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
answering_connection_lost: ActiveValue::set(false),
..Default::default()
})
.exec(&*tx)
.await?;
if participant_update.rows_affected == 0 {
return Err(anyhow!("room does not exist or was already joined"))?;
}
let mut reshared_projects = Vec::new();
for reshared_project in &rejoin_room.reshared_projects {
let project_id = ProjectId::from_proto(reshared_project.project_id);
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("project does not exist"))?;
if project.host_user_id != user_id {
return Err(anyhow!("no such project"))?;
}
let mut collaborators = project
.find_related(project_collaborator::Entity)
.all(&*tx)
.await?;
let host_ix = collaborators
.iter()
.position(|collaborator| {
collaborator.user_id == user_id && collaborator.is_host
})
.ok_or_else(|| anyhow!("host not found among collaborators"))?;
let host = collaborators.swap_remove(host_ix);
let old_connection_id = host.connection();
project::Entity::update(project::ActiveModel {
host_connection_id: ActiveValue::set(Some(connection.id as i32)),
host_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
..project.into_active_model()
})
.exec(&*tx)
.await?;
project_collaborator::Entity::update(project_collaborator::ActiveModel {
connection_id: ActiveValue::set(connection.id as i32),
connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
..host.into_active_model()
})
.exec(&*tx)
.await?;
self.update_project_worktrees(project_id, &reshared_project.worktrees, &tx)
.await?;
reshared_projects.push(ResharedProject {
id: project_id,
old_connection_id,
collaborators: collaborators
.iter()
.map(|collaborator| ProjectCollaborator {
connection_id: collaborator.connection(),
user_id: collaborator.user_id,
replica_id: collaborator.replica_id,
is_host: collaborator.is_host,
})
.collect(),
worktrees: reshared_project.worktrees.clone(),
});
}
project::Entity::delete_many()
.filter(
Condition::all()
.add(project::Column::RoomId.eq(room_id))
.add(project::Column::HostUserId.eq(user_id))
.add(
project::Column::Id
.is_not_in(reshared_projects.iter().map(|project| project.id)),
),
)
.exec(&*tx)
.await?;
let mut rejoined_projects = Vec::new();
for rejoined_project in &rejoin_room.rejoined_projects {
let project_id = ProjectId::from_proto(rejoined_project.id);
let Some(project) = project::Entity::find_by_id(project_id)
.one(&*tx)
.await? else { continue };
let mut worktrees = Vec::new();
let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?;
for db_worktree in db_worktrees {
let mut worktree = RejoinedWorktree {
id: db_worktree.id as u64,
abs_path: db_worktree.abs_path,
root_name: db_worktree.root_name,
visible: db_worktree.visible,
updated_entries: Default::default(),
removed_entries: Default::default(),
diagnostic_summaries: Default::default(),
scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64,
};
let rejoined_worktree = rejoined_project
.worktrees
.iter()
.find(|worktree| worktree.id == db_worktree.id as u64);
let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id)
} else {
worktree_entry::Column::IsDeleted.eq(false)
};
let mut db_entries = worktree_entry::Entity::find()
.filter(
Condition::all()
.add(worktree_entry::Column::WorktreeId.eq(worktree.id))
.add(entry_filter),
)
.stream(&*tx)
.await?;
while let Some(db_entry) = db_entries.next().await {
let db_entry = db_entry?;
if db_entry.is_deleted {
worktree.removed_entries.push(db_entry.id as u64);
} else {
worktree.updated_entries.push(proto::Entry {
id: db_entry.id as u64,
is_dir: db_entry.is_dir,
path: db_entry.path,
inode: db_entry.inode as u64,
mtime: Some(proto::Timestamp {
seconds: db_entry.mtime_seconds as u64,
nanos: db_entry.mtime_nanos as u32,
}),
is_symlink: db_entry.is_symlink,
is_ignored: db_entry.is_ignored,
});
}
}
worktrees.push(worktree);
}
let language_servers = project
.find_related(language_server::Entity)
.all(&*tx)
.await?
.into_iter()
.map(|language_server| proto::LanguageServer {
id: language_server.id as u64,
name: language_server.name,
})
.collect::<Vec<_>>();
let mut collaborators = project
.find_related(project_collaborator::Entity)
.all(&*tx)
.await?;
let self_collaborator = if let Some(self_collaborator_ix) = collaborators
.iter()
.position(|collaborator| collaborator.user_id == user_id)
{
collaborators.swap_remove(self_collaborator_ix)
} else {
continue;
};
let old_connection_id = self_collaborator.connection();
project_collaborator::Entity::update(project_collaborator::ActiveModel {
connection_id: ActiveValue::set(connection.id as i32),
connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
..self_collaborator.into_active_model()
})
.exec(&*tx)
.await?;
let collaborators = collaborators
.into_iter()
.map(|collaborator| ProjectCollaborator {
connection_id: collaborator.connection(),
user_id: collaborator.user_id,
replica_id: collaborator.replica_id,
is_host: collaborator.is_host,
})
.collect::<Vec<_>>();
rejoined_projects.push(RejoinedProject {
id: project_id,
old_connection_id,
collaborators,
worktrees,
language_servers,
});
}
let room = self.get_room(room_id, &tx).await?;
Ok((
room_id,
RejoinedRoom {
room,
rejoined_projects,
reshared_projects,
},
))
})
.await
}
pub async fn leave_room(
&self,
connection: ConnectionId,
@@ -1445,10 +1650,7 @@ impl Database {
host_connection_id: Default::default(),
});
let collaborator_connection_id = ConnectionId {
owner_id: collaborator.connection_server_id.0 as u32,
id: collaborator.connection_id as u32,
};
let collaborator_connection_id = collaborator.connection();
if collaborator_connection_id != connection {
left_project.connection_ids.push(collaborator_connection_id);
}
@@ -1572,11 +1774,8 @@ impl Database {
.await
}
pub async fn connection_lost(
&self,
connection: ConnectionId,
) -> Result<RoomGuard<Vec<LeftProject>>> {
self.room_transaction(|tx| async move {
pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> {
self.transaction(|tx| async move {
let participant = room_participant::Entity::find()
.filter(
Condition::all()
@@ -1592,7 +1791,6 @@ impl Database {
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("not a participant in any room"))?;
let room_id = participant.room_id;
room_participant::Entity::update(room_participant::ActiveModel {
answering_connection_lost: ActiveValue::set(true),
@@ -1601,66 +1799,7 @@ impl Database {
.exec(&*tx)
.await?;
let collaborator_on_projects = project_collaborator::Entity::find()
.find_also_related(project::Entity)
.filter(
Condition::all()
.add(project_collaborator::Column::ConnectionId.eq(connection.id as i32))
.add(
project_collaborator::Column::ConnectionServerId
.eq(connection.owner_id as i32),
),
)
.all(&*tx)
.await?;
project_collaborator::Entity::delete_many()
.filter(
Condition::all()
.add(project_collaborator::Column::ConnectionId.eq(connection.id as i32))
.add(
project_collaborator::Column::ConnectionServerId
.eq(connection.owner_id as i32),
),
)
.exec(&*tx)
.await?;
let mut left_projects = Vec::new();
for (_, project) in collaborator_on_projects {
if let Some(project) = project {
let collaborators = project
.find_related(project_collaborator::Entity)
.all(&*tx)
.await?;
let connection_ids = collaborators
.into_iter()
.map(|collaborator| ConnectionId {
id: collaborator.connection_id as u32,
owner_id: collaborator.connection_server_id.0 as u32,
})
.collect();
left_projects.push(LeftProject {
id: project.id,
host_user_id: project.host_user_id,
host_connection_id: project.host_connection()?,
connection_ids,
});
}
}
project::Entity::delete_many()
.filter(
Condition::all()
.add(project::Column::HostConnectionId.eq(connection.id as i32))
.add(
project::Column::HostConnectionServerId.eq(connection.owner_id as i32),
),
)
.exec(&*tx)
.await?;
Ok((room_id, left_projects))
Ok(())
})
.await
}
@@ -1860,7 +1999,7 @@ impl Database {
root_name: ActiveValue::set(worktree.root_name.clone()),
visible: ActiveValue::set(worktree.visible),
scan_id: ActiveValue::set(0),
is_complete: ActiveValue::set(false),
completed_scan_id: ActiveValue::set(0),
}
}))
.exec(&*tx)
@@ -1930,35 +2069,7 @@ impl Database {
.await?
.ok_or_else(|| anyhow!("no such project"))?;
if !worktrees.is_empty() {
worktree::Entity::insert_many(worktrees.iter().map(|worktree| {
worktree::ActiveModel {
id: ActiveValue::set(worktree.id as i64),
project_id: ActiveValue::set(project.id),
abs_path: ActiveValue::set(worktree.abs_path.clone()),
root_name: ActiveValue::set(worktree.root_name.clone()),
visible: ActiveValue::set(worktree.visible),
scan_id: ActiveValue::set(0),
is_complete: ActiveValue::set(false),
}
}))
.on_conflict(
OnConflict::columns([worktree::Column::ProjectId, worktree::Column::Id])
.update_column(worktree::Column::RootName)
.to_owned(),
)
.exec(&*tx)
.await?;
}
worktree::Entity::delete_many()
.filter(
worktree::Column::ProjectId.eq(project.id).and(
worktree::Column::Id
.is_not_in(worktrees.iter().map(|worktree| worktree.id as i64)),
),
)
.exec(&*tx)
self.update_project_worktrees(project.id, worktrees, &tx)
.await?;
let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?;
@@ -1968,6 +2079,41 @@ impl Database {
.await
}
async fn update_project_worktrees(
&self,
project_id: ProjectId,
worktrees: &[proto::WorktreeMetadata],
tx: &DatabaseTransaction,
) -> Result<()> {
if !worktrees.is_empty() {
worktree::Entity::insert_many(worktrees.iter().map(|worktree| worktree::ActiveModel {
id: ActiveValue::set(worktree.id as i64),
project_id: ActiveValue::set(project_id),
abs_path: ActiveValue::set(worktree.abs_path.clone()),
root_name: ActiveValue::set(worktree.root_name.clone()),
visible: ActiveValue::set(worktree.visible),
scan_id: ActiveValue::set(0),
completed_scan_id: ActiveValue::set(0),
}))
.on_conflict(
OnConflict::columns([worktree::Column::ProjectId, worktree::Column::Id])
.update_column(worktree::Column::RootName)
.to_owned(),
)
.exec(&*tx)
.await?;
}
worktree::Entity::delete_many()
.filter(worktree::Column::ProjectId.eq(project_id).and(
worktree::Column::Id.is_not_in(worktrees.iter().map(|worktree| worktree.id as i64)),
))
.exec(&*tx)
.await?;
Ok(())
}
pub async fn update_worktree(
&self,
update: &proto::UpdateWorktree,
@@ -1997,7 +2143,11 @@ impl Database {
project_id: ActiveValue::set(project_id),
root_name: ActiveValue::set(update.root_name.clone()),
scan_id: ActiveValue::set(update.scan_id as i64),
is_complete: ActiveValue::set(update.is_last_update),
completed_scan_id: if update.is_last_update {
ActiveValue::set(update.scan_id as i64)
} else {
ActiveValue::default()
},
abs_path: ActiveValue::set(update.abs_path.clone()),
..Default::default()
})
@@ -2018,6 +2168,8 @@ impl Database {
mtime_nanos: ActiveValue::set(mtime.nanos as i32),
is_symlink: ActiveValue::set(entry.is_symlink),
is_ignored: ActiveValue::set(entry.is_ignored),
is_deleted: ActiveValue::set(false),
scan_id: ActiveValue::set(update.scan_id as i64),
}
}))
.on_conflict(
@@ -2034,6 +2186,7 @@ impl Database {
worktree_entry::Column::MtimeNanos,
worktree_entry::Column::IsSymlink,
worktree_entry::Column::IsIgnored,
worktree_entry::Column::ScanId,
])
.to_owned(),
)
@@ -2042,7 +2195,7 @@ impl Database {
}
if !update.removed_entries.is_empty() {
worktree_entry::Entity::delete_many()
worktree_entry::Entity::update_many()
.filter(
worktree_entry::Column::ProjectId
.eq(project_id)
@@ -2052,6 +2205,11 @@ impl Database {
.is_in(update.removed_entries.iter().map(|id| *id as i64)),
),
)
.set(worktree_entry::ActiveModel {
is_deleted: ActiveValue::Set(true),
scan_id: ActiveValue::Set(update.scan_id as i64),
..Default::default()
})
.exec(&*tx)
.await?;
}
@@ -2230,7 +2388,7 @@ impl Database {
entries: Default::default(),
diagnostic_summaries: Default::default(),
scan_id: db_worktree.scan_id as u64,
is_complete: db_worktree.is_complete,
completed_scan_id: db_worktree.completed_scan_id as u64,
},
)
})
@@ -2239,7 +2397,11 @@ impl Database {
// Populate worktree entries.
{
let mut db_entries = worktree_entry::Entity::find()
.filter(worktree_entry::Column::ProjectId.eq(project_id))
.filter(
Condition::all()
.add(worktree_entry::Column::ProjectId.eq(project_id))
.add(worktree_entry::Column::IsDeleted.eq(false)),
)
.stream(&*tx)
.await?;
while let Some(db_entry) = db_entries.next().await {
@@ -2290,7 +2452,15 @@ impl Database {
let room_id = project.room_id;
let project = Project {
collaborators,
collaborators: collaborators
.into_iter()
.map(|collaborator| ProjectCollaborator {
connection_id: collaborator.connection(),
user_id: collaborator.user_id,
replica_id: collaborator.replica_id,
is_host: collaborator.is_host,
})
.collect(),
worktrees,
language_servers: language_servers
.into_iter()
@@ -2337,10 +2507,7 @@ impl Database {
.await?;
let connection_ids = collaborators
.into_iter()
.map(|collaborator| ConnectionId {
owner_id: collaborator.connection_server_id.0 as u32,
id: collaborator.connection_id as u32,
})
.map(|collaborator| collaborator.connection())
.collect();
let left_project = LeftProject {
@@ -2357,8 +2524,8 @@ impl Database {
pub async fn project_collaborators(
&self,
project_id: ProjectId,
connection: ConnectionId,
) -> Result<RoomGuard<Vec<project_collaborator::Model>>> {
connection_id: ConnectionId,
) -> Result<RoomGuard<Vec<ProjectCollaborator>>> {
self.room_transaction(|tx| async move {
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
@@ -2367,15 +2534,20 @@ impl Database {
let collaborators = project_collaborator::Entity::find()
.filter(project_collaborator::Column::ProjectId.eq(project_id))
.all(&*tx)
.await?;
.await?
.into_iter()
.map(|collaborator| ProjectCollaborator {
connection_id: collaborator.connection(),
user_id: collaborator.user_id,
replica_id: collaborator.replica_id,
is_host: collaborator.is_host,
})
.collect::<Vec<_>>();
if collaborators.iter().any(|collaborator| {
let collaborator_connection = ConnectionId {
owner_id: collaborator.connection_server_id.0 as u32,
id: collaborator.connection_id as u32,
};
collaborator_connection == connection
}) {
if collaborators
.iter()
.any(|collaborator| collaborator.connection_id == connection_id)
{
Ok((project.room_id, collaborators))
} else {
Err(anyhow!("no such project"))?
@@ -2394,18 +2566,15 @@ impl Database {
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
let mut participants = project_collaborator::Entity::find()
let mut collaborators = project_collaborator::Entity::find()
.filter(project_collaborator::Column::ProjectId.eq(project_id))
.stream(&*tx)
.await?;
let mut connection_ids = HashSet::default();
while let Some(participant) = participants.next().await {
let participant = participant?;
connection_ids.insert(ConnectionId {
owner_id: participant.connection_server_id.0 as u32,
id: participant.connection_id as u32,
});
while let Some(collaborator) = collaborators.next().await {
let collaborator = collaborator?;
connection_ids.insert(collaborator.connection());
}
if connection_ids.contains(&connection_id) {
@@ -2422,7 +2591,7 @@ impl Database {
project_id: ProjectId,
tx: &DatabaseTransaction,
) -> Result<Vec<ConnectionId>> {
let mut participants = project_collaborator::Entity::find()
let mut collaborators = project_collaborator::Entity::find()
.filter(
project_collaborator::Column::ProjectId
.eq(project_id)
@@ -2432,12 +2601,9 @@ impl Database {
.await?;
let mut guest_connection_ids = Vec::new();
while let Some(participant) = participants.next().await {
let participant = participant?;
guest_connection_ids.push(ConnectionId {
owner_id: participant.connection_server_id.0 as u32,
id: participant.connection_id as u32,
});
while let Some(collaborator) = collaborators.next().await {
let collaborator = collaborator?;
guest_connection_ids.push(collaborator.connection());
}
Ok(guest_connection_ids)
}
@@ -2849,6 +3015,40 @@ id_type!(ServerId);
id_type!(SignupId);
id_type!(UserId);
pub struct RejoinedRoom {
pub room: proto::Room,
pub rejoined_projects: Vec<RejoinedProject>,
pub reshared_projects: Vec<ResharedProject>,
}
pub struct ResharedProject {
pub id: ProjectId,
pub old_connection_id: ConnectionId,
pub collaborators: Vec<ProjectCollaborator>,
pub worktrees: Vec<proto::WorktreeMetadata>,
}
pub struct RejoinedProject {
pub id: ProjectId,
pub old_connection_id: ConnectionId,
pub collaborators: Vec<ProjectCollaborator>,
pub worktrees: Vec<RejoinedWorktree>,
pub language_servers: Vec<proto::LanguageServer>,
}
#[derive(Debug)]
pub struct RejoinedWorktree {
pub id: u64,
pub abs_path: String,
pub root_name: String,
pub visible: bool,
pub updated_entries: Vec<proto::Entry>,
pub removed_entries: Vec<u64>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub scan_id: u64,
pub completed_scan_id: u64,
}
pub struct LeftRoom {
pub room: proto::Room,
pub left_projects: HashMap<ProjectId, LeftProject>,
@@ -2862,11 +3062,29 @@ pub struct RefreshedRoom {
}
pub struct Project {
pub collaborators: Vec<project_collaborator::Model>,
pub collaborators: Vec<ProjectCollaborator>,
pub worktrees: BTreeMap<u64, Worktree>,
pub language_servers: Vec<proto::LanguageServer>,
}
pub struct ProjectCollaborator {
pub connection_id: ConnectionId,
pub user_id: UserId,
pub replica_id: ReplicaId,
pub is_host: bool,
}
impl ProjectCollaborator {
pub fn to_proto(&self) -> proto::Collaborator {
proto::Collaborator {
peer_id: Some(self.connection_id.into()),
replica_id: self.replica_id.0 as u32,
user_id: self.user_id.to_proto(),
}
}
}
#[derive(Debug)]
pub struct LeftProject {
pub id: ProjectId,
pub host_user_id: UserId,
@@ -2882,7 +3100,7 @@ pub struct Worktree {
pub entries: Vec<proto::Entry>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub scan_id: u64,
pub is_complete: bool,
pub completed_scan_id: u64,
}
#[cfg(test)]

View File

@@ -1,4 +1,5 @@
use super::{ProjectCollaboratorId, ProjectId, ReplicaId, ServerId, UserId};
use rpc::ConnectionId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
@@ -14,6 +15,15 @@ pub struct Model {
pub is_host: bool,
}
impl Model {
pub fn connection(&self) -> ConnectionId {
ConnectionId {
owner_id: self.connection_server_id.0 as u32,
id: self.connection_id as u32,
}
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(

View File

@@ -567,7 +567,12 @@ async fn test_invite_codes() {
// User 2 redeems the invite code and becomes a contact of user 1.
let user2_invite = db
.create_invite_from_code(&invite_code, "user2@example.com", Some("user-2-device-id"))
.create_invite_from_code(
&invite_code,
"user2@example.com",
Some("user-2-device-id"),
true,
)
.await
.unwrap();
let NewUserResult {
@@ -617,7 +622,7 @@ async fn test_invite_codes() {
// User 3 redeems the invite code and becomes a contact of user 1.
let user3_invite = db
.create_invite_from_code(&invite_code, "user3@example.com", None)
.create_invite_from_code(&invite_code, "user3@example.com", None, true)
.await
.unwrap();
let NewUserResult {
@@ -672,9 +677,14 @@ async fn test_invite_codes() {
);
// Trying to reedem the code for the third time results in an error.
db.create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id"))
.await
.unwrap_err();
db.create_invite_from_code(
&invite_code,
"user4@example.com",
Some("user-4-device-id"),
true,
)
.await
.unwrap_err();
// Invite count can be updated after the code has been created.
db.set_invite_count_for_user(user1, 2).await.unwrap();
@@ -684,7 +694,12 @@ async fn test_invite_codes() {
// User 4 can now redeem the invite code and becomes a contact of user 1.
let user4_invite = db
.create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id"))
.create_invite_from_code(
&invite_code,
"user4@example.com",
Some("user-4-device-id"),
true,
)
.await
.unwrap();
let user4 = db
@@ -739,9 +754,14 @@ async fn test_invite_codes() {
);
// An existing user cannot redeem invite codes.
db.create_invite_from_code(&invite_code, "user2@example.com", Some("user-2-device-id"))
.await
.unwrap_err();
db.create_invite_from_code(
&invite_code,
"user2@example.com",
Some("user-2-device-id"),
true,
)
.await
.unwrap_err();
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
assert_eq!(invite_count, 1);
@@ -763,7 +783,7 @@ async fn test_invite_codes() {
db.set_invite_count_for_user(user5, 5).await.unwrap();
let (user5_invite_code, _) = db.get_invite_code_for_user(user5).await.unwrap().unwrap();
let user5_invite_to_user1 = db
.create_invite_from_code(&user5_invite_code, "user1@different.com", None)
.create_invite_from_code(&user5_invite_code, "user1@different.com", None, true)
.await
.unwrap();
let user1_2 = db

View File

@@ -11,8 +11,10 @@ pub struct Model {
pub abs_path: String,
pub root_name: String,
pub visible: bool,
/// The last scan for which we've observed entries. It may be in progress.
pub scan_id: i64,
pub is_complete: bool,
/// The last scan that fully completed.
pub completed_scan_id: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -17,6 +17,8 @@ pub struct Model {
pub mtime_nanos: i32,
pub is_symlink: bool,
pub is_ignored: bool,
pub is_deleted: bool,
pub scan_id: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -33,4 +33,12 @@ impl Executor {
}
}
}
pub fn record_backtrace(&self) {
match self {
Executor::Production => {}
#[cfg(test)]
Executor::Deterministic(background) => background.record_backtrace(),
}
}
}

View File

@@ -3,10 +3,11 @@ pub mod auth;
pub mod db;
pub mod env;
pub mod executor;
#[cfg(test)]
mod integration_tests;
pub mod rpc;
#[cfg(test)]
mod tests;
use axum::{http::StatusCode, response::IntoResponse};
use db::Database;
use serde::Deserialize;

View File

@@ -95,6 +95,7 @@ struct Session {
peer: Arc<Peer>,
connection_pool: Arc<parking_lot::Mutex<ConnectionPool>>,
live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
executor: Executor,
}
impl Session {
@@ -184,6 +185,7 @@ impl Server {
.add_request_handler(ping)
.add_request_handler(create_room)
.add_request_handler(join_room)
.add_request_handler(rejoin_room)
.add_message_handler(leave_room)
.add_request_handler(call)
.add_request_handler(cancel_call)
@@ -215,6 +217,7 @@ impl Server {
.add_request_handler(forward_project_request::<proto::PrepareRename>)
.add_request_handler(forward_project_request::<proto::PerformRename>)
.add_request_handler(forward_project_request::<proto::ReloadBuffers>)
.add_request_handler(forward_project_request::<proto::SynchronizeBuffers>)
.add_request_handler(forward_project_request::<proto::FormatBuffers>)
.add_request_handler(forward_project_request::<proto::CreateProjectEntry>)
.add_request_handler(forward_project_request::<proto::RenameProjectEntry>)
@@ -249,16 +252,6 @@ impl Server {
let live_kit_client = self.app_state.live_kit_client.clone();
let span = info_span!("start server");
let span_enter = span.enter();
tracing::info!("begin deleting stale projects");
app_state
.db
.delete_stale_projects(&app_state.config.zed_environment, server_id)
.await?;
tracing::info!("finish deleting stale projects");
drop(span_enter);
self.executor.spawn_detached(
async move {
tracing::info!("waiting for cleanup timeout");
@@ -354,7 +347,7 @@ impl Server {
app_state
.db
.delete_stale_servers(server_id, &app_state.config.zed_environment)
.delete_stale_servers(&app_state.config.zed_environment, server_id)
.await
.trace_err();
}
@@ -529,7 +522,8 @@ impl Server {
db: Arc::new(tokio::sync::Mutex::new(DbHandle(this.app_state.db.clone()))),
peer: this.peer.clone(),
connection_pool: this.connection_pool.clone(),
live_kit_client: this.app_state.live_kit_client.clone()
live_kit_client: this.app_state.live_kit_client.clone(),
executor: executor.clone(),
};
update_user_contacts(user_id, &session).await?;
@@ -586,7 +580,7 @@ impl Server {
drop(foreground_message_handlers);
tracing::info!(%user_id, %login, %connection_id, %address, "signing out");
if let Err(error) = sign_out(session, teardown, executor).await {
if let Err(error) = connection_lost(session, teardown, executor).await {
tracing::error!(%user_id, %login, %connection_id, %address, ?error, "error signing out");
}
@@ -678,15 +672,17 @@ impl<'a> Drop for ConnectionPoolGuard<'a> {
}
fn broadcast<F>(
sender_id: ConnectionId,
sender_id: Option<ConnectionId>,
receiver_ids: impl IntoIterator<Item = ConnectionId>,
mut f: F,
) where
F: FnMut(ConnectionId) -> anyhow::Result<()>,
{
for receiver_id in receiver_ids {
if receiver_id != sender_id {
f(receiver_id).trace_err();
if Some(receiver_id) != sender_id {
if let Err(error) = f(receiver_id) {
tracing::error!("failed to send to {:?} {}", receiver_id, error);
}
}
}
}
@@ -787,7 +783,7 @@ pub async fn handle_metrics(Extension(server): Extension<Arc<Server>>) -> Result
}
#[instrument(err, skip(executor))]
async fn sign_out(
async fn connection_lost(
session: Session,
mut teardown: watch::Receiver<()>,
executor: Executor,
@@ -798,17 +794,12 @@ async fn sign_out(
.await
.remove_connection(session.connection_id)?;
if let Some(mut left_projects) = session
session
.db()
.await
.connection_lost(session.connection_id)
.await
.trace_err()
{
for left_project in mem::take(&mut *left_projects) {
project_left(&left_project, &session);
}
}
.trace_err();
futures::select_biased! {
_ = executor.sleep(RECONNECT_TIMEOUT).fuse() => {
@@ -941,6 +932,164 @@ async fn join_room(
Ok(())
}
async fn rejoin_room(
request: proto::RejoinRoom,
response: Response<proto::RejoinRoom>,
session: Session,
) -> Result<()> {
{
let mut rejoined_room = session
.db()
.await
.rejoin_room(request, session.user_id, session.connection_id)
.await?;
response.send(proto::RejoinRoomResponse {
room: Some(rejoined_room.room.clone()),
reshared_projects: rejoined_room
.reshared_projects
.iter()
.map(|project| proto::ResharedProject {
id: project.id.to_proto(),
collaborators: project
.collaborators
.iter()
.map(|collaborator| collaborator.to_proto())
.collect(),
})
.collect(),
rejoined_projects: rejoined_room
.rejoined_projects
.iter()
.map(|rejoined_project| proto::RejoinedProject {
id: rejoined_project.id.to_proto(),
worktrees: rejoined_project
.worktrees
.iter()
.map(|worktree| proto::WorktreeMetadata {
id: worktree.id,
root_name: worktree.root_name.clone(),
visible: worktree.visible,
abs_path: worktree.abs_path.clone(),
})
.collect(),
collaborators: rejoined_project
.collaborators
.iter()
.map(|collaborator| collaborator.to_proto())
.collect(),
language_servers: rejoined_project.language_servers.clone(),
})
.collect(),
})?;
room_updated(&rejoined_room.room, &session.peer);
for project in &rejoined_room.reshared_projects {
for collaborator in &project.collaborators {
session
.peer
.send(
collaborator.connection_id,
proto::UpdateProjectCollaborator {
project_id: project.id.to_proto(),
old_peer_id: Some(project.old_connection_id.into()),
new_peer_id: Some(session.connection_id.into()),
},
)
.trace_err();
}
broadcast(
Some(session.connection_id),
project
.collaborators
.iter()
.map(|collaborator| collaborator.connection_id),
|connection_id| {
session.peer.forward_send(
session.connection_id,
connection_id,
proto::UpdateProject {
project_id: project.id.to_proto(),
worktrees: project.worktrees.clone(),
},
)
},
);
}
for project in &rejoined_room.rejoined_projects {
for collaborator in &project.collaborators {
session
.peer
.send(
collaborator.connection_id,
proto::UpdateProjectCollaborator {
project_id: project.id.to_proto(),
old_peer_id: Some(project.old_connection_id.into()),
new_peer_id: Some(session.connection_id.into()),
},
)
.trace_err();
}
}
for project in &mut rejoined_room.rejoined_projects {
for worktree in mem::take(&mut project.worktrees) {
#[cfg(any(test, feature = "test-support"))]
const MAX_CHUNK_SIZE: usize = 2;
#[cfg(not(any(test, feature = "test-support")))]
const MAX_CHUNK_SIZE: usize = 256;
// Stream this worktree's entries.
let message = proto::UpdateWorktree {
project_id: project.id.to_proto(),
worktree_id: worktree.id,
abs_path: worktree.abs_path.clone(),
root_name: worktree.root_name,
updated_entries: worktree.updated_entries,
removed_entries: worktree.removed_entries,
scan_id: worktree.scan_id,
is_last_update: worktree.completed_scan_id == worktree.scan_id,
};
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
session.peer.send(session.connection_id, update.clone())?;
}
// Stream this worktree's diagnostics.
for summary in worktree.diagnostic_summaries {
session.peer.send(
session.connection_id,
proto::UpdateDiagnosticSummary {
project_id: project.id.to_proto(),
worktree_id: worktree.id,
summary: Some(summary),
},
)?;
}
}
for language_server in &project.language_servers {
session.peer.send(
session.connection_id,
proto::UpdateLanguageServer {
project_id: project.id.to_proto(),
language_server_id: language_server.id,
variant: Some(
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
proto::LspDiskBasedDiagnosticsUpdated {},
),
),
},
)?;
}
}
}
update_user_contacts(session.user_id, &session).await?;
Ok(())
}
async fn leave_room(_message: proto::LeaveRoom, session: Session) -> Result<()> {
leave_room_for_session(&session).await
}
@@ -1132,7 +1281,7 @@ async fn unshare_project(message: proto::UnshareProject, session: Session) -> Re
.await?;
broadcast(
session.connection_id,
Some(session.connection_id),
guest_connection_ids.iter().copied(),
|conn_id| session.peer.send(conn_id, message.clone()),
);
@@ -1160,18 +1309,8 @@ async fn join_project(
let collaborators = project
.collaborators
.iter()
.map(|collaborator| {
let peer_id = proto::PeerId {
owner_id: collaborator.connection_server_id.0 as u32,
id: collaborator.connection_id as u32,
};
proto::Collaborator {
peer_id: Some(peer_id),
replica_id: collaborator.replica_id.0 as u32,
user_id: collaborator.user_id.to_proto(),
}
})
.filter(|collaborator| collaborator.peer_id != Some(session.connection_id.into()))
.filter(|collaborator| collaborator.connection_id != session.connection_id)
.map(|collaborator| collaborator.to_proto())
.collect::<Vec<_>>();
let worktrees = project
.worktrees
@@ -1224,7 +1363,7 @@ async fn join_project(
updated_entries: worktree.entries,
removed_entries: Default::default(),
scan_id: worktree.scan_id,
is_last_update: worktree.is_complete,
is_last_update: worktree.scan_id == worktree.completed_scan_id,
};
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
session.peer.send(session.connection_id, update.clone())?;
@@ -1293,7 +1432,7 @@ async fn update_project(
.update_project(project_id, session.connection_id, &request.worktrees)
.await?;
broadcast(
session.connection_id,
Some(session.connection_id),
guest_connection_ids.iter().copied(),
|connection_id| {
session
@@ -1319,7 +1458,7 @@ async fn update_worktree(
.await?;
broadcast(
session.connection_id,
Some(session.connection_id),
guest_connection_ids.iter().copied(),
|connection_id| {
session
@@ -1342,7 +1481,7 @@ async fn update_diagnostic_summary(
.await?;
broadcast(
session.connection_id,
Some(session.connection_id),
guest_connection_ids.iter().copied(),
|connection_id| {
session
@@ -1365,7 +1504,7 @@ async fn start_language_server(
.await?;
broadcast(
session.connection_id,
Some(session.connection_id),
guest_connection_ids.iter().copied(),
|connection_id| {
session
@@ -1380,6 +1519,7 @@ async fn update_language_server(
request: proto::UpdateLanguageServer,
session: Session,
) -> Result<()> {
session.executor.record_backtrace();
let project_id = ProjectId::from_proto(request.project_id);
let project_connection_ids = session
.db()
@@ -1387,7 +1527,7 @@ async fn update_language_server(
.project_connection_ids(project_id, session.connection_id)
.await?;
broadcast(
session.connection_id,
Some(session.connection_id),
project_connection_ids.iter().copied(),
|connection_id| {
session
@@ -1406,6 +1546,7 @@ async fn forward_project_request<T>(
where
T: EntityMessage + RequestMessage,
{
session.executor.record_backtrace();
let project_id = ProjectId::from_proto(request.remote_entity_id());
let host_connection_id = {
let collaborators = session
@@ -1413,14 +1554,11 @@ where
.await
.project_collaborators(project_id, session.connection_id)
.await?;
let host = collaborators
collaborators
.iter()
.find(|collaborator| collaborator.is_host)
.ok_or_else(|| anyhow!("host not found"))?;
ConnectionId {
owner_id: host.connection_server_id.0 as u32,
id: host.connection_id as u32,
}
.ok_or_else(|| anyhow!("host not found"))?
.connection_id
};
let payload = session
@@ -1444,14 +1582,11 @@ async fn save_buffer(
.await
.project_collaborators(project_id, session.connection_id)
.await?;
let host = collaborators
collaborators
.iter()
.find(|collaborator| collaborator.is_host)
.ok_or_else(|| anyhow!("host not found"))?;
ConnectionId {
owner_id: host.connection_server_id.0 as u32,
id: host.connection_id as u32,
}
.ok_or_else(|| anyhow!("host not found"))?
.connection_id
};
let response_payload = session
.peer
@@ -1463,22 +1598,19 @@ async fn save_buffer(
.await
.project_collaborators(project_id, session.connection_id)
.await?;
collaborators.retain(|collaborator| {
let collaborator_connection = ConnectionId {
owner_id: collaborator.connection_server_id.0 as u32,
id: collaborator.connection_id as u32,
};
collaborator_connection != session.connection_id
});
let project_connection_ids = collaborators.iter().map(|collaborator| ConnectionId {
owner_id: collaborator.connection_server_id.0 as u32,
id: collaborator.connection_id as u32,
});
broadcast(host_connection_id, project_connection_ids, |conn_id| {
session
.peer
.forward_send(host_connection_id, conn_id, response_payload.clone())
});
collaborators.retain(|collaborator| collaborator.connection_id != session.connection_id);
let project_connection_ids = collaborators
.iter()
.map(|collaborator| collaborator.connection_id);
broadcast(
Some(host_connection_id),
project_connection_ids,
|conn_id| {
session
.peer
.forward_send(host_connection_id, conn_id, response_payload.clone())
},
);
response.send(response_payload)?;
Ok(())
}
@@ -1487,6 +1619,7 @@ async fn create_buffer_for_peer(
request: proto::CreateBufferForPeer,
session: Session,
) -> Result<()> {
session.executor.record_backtrace();
let peer_id = request.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?;
session
.peer
@@ -1499,6 +1632,7 @@ async fn update_buffer(
response: Response<proto::UpdateBuffer>,
session: Session,
) -> Result<()> {
session.executor.record_backtrace();
let project_id = ProjectId::from_proto(request.project_id);
let project_connection_ids = session
.db()
@@ -1506,8 +1640,10 @@ async fn update_buffer(
.project_connection_ids(project_id, session.connection_id)
.await?;
session.executor.record_backtrace();
broadcast(
session.connection_id,
Some(session.connection_id),
project_connection_ids.iter().copied(),
|connection_id| {
session
@@ -1528,7 +1664,7 @@ async fn update_buffer_file(request: proto::UpdateBufferFile, session: Session)
.await?;
broadcast(
session.connection_id,
Some(session.connection_id),
project_connection_ids.iter().copied(),
|connection_id| {
session
@@ -1547,7 +1683,7 @@ async fn buffer_reloaded(request: proto::BufferReloaded, session: Session) -> Re
.project_connection_ids(project_id, session.connection_id)
.await?;
broadcast(
session.connection_id,
Some(session.connection_id),
project_connection_ids.iter().copied(),
|connection_id| {
session
@@ -1566,7 +1702,7 @@ async fn buffer_saved(request: proto::BufferSaved, session: Session) -> Result<(
.project_connection_ids(project_id, session.connection_id)
.await?;
broadcast(
session.connection_id,
Some(session.connection_id),
project_connection_ids.iter().copied(),
|connection_id| {
session
@@ -1858,7 +1994,7 @@ async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> R
.project_connection_ids(project_id, session.connection_id)
.await?;
broadcast(
session.connection_id,
Some(session.connection_id),
project_connection_ids.iter().copied(),
|connection_id| {
session
@@ -1968,21 +2104,20 @@ fn contact_for_user(
}
fn room_updated(room: &proto::Room, peer: &Peer) {
for participant in &room.participants {
if let Some(peer_id) = participant
.peer_id
.ok_or_else(|| anyhow!("invalid participant peer id"))
.trace_err()
{
broadcast(
None,
room.participants
.iter()
.filter_map(|participant| Some(participant.peer_id?.into())),
|peer_id| {
peer.send(
peer_id.into(),
proto::RoomUpdated {
room: Some(room.clone()),
},
)
.trace_err();
}
}
},
);
}
async fn update_user_contacts(user_id: UserId, session: &Session) -> Result<()> {
@@ -2103,16 +2238,6 @@ fn project_left(project: &db::LeftProject, session: &Session) {
.trace_err();
}
}
session
.peer
.send(
session.connection_id,
proto::UnshareProject {
project_id: project.id.to_proto(),
},
)
.trace_err();
}
pub trait ResultExt {

466
crates/collab/src/tests.rs Normal file
View File

@@ -0,0 +1,466 @@
use crate::{
db::{NewUserParams, TestDb, UserId},
executor::Executor,
rpc::{Server, CLEANUP_TIMEOUT},
AppState,
};
use anyhow::anyhow;
use call::ActiveCall;
use client::{
self, proto::PeerId, test::FakeHttpClient, Client, Connection, Credentials,
EstablishConnectionError, UserStore,
};
use collections::{HashMap, HashSet};
use fs::{FakeFs, HomeDir};
use futures::{channel::oneshot, StreamExt as _};
use gpui::{
executor::Deterministic, test::EmptyView, ModelHandle, Task, TestAppContext, ViewHandle,
};
use language::LanguageRegistry;
use parking_lot::Mutex;
use project::{Project, WorktreeId};
use settings::Settings;
use std::{
env,
ops::Deref,
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
Arc,
},
};
use theme::ThemeRegistry;
use workspace::Workspace;
mod integration_tests;
mod randomized_integration_tests;
struct TestServer {
app_state: Arc<AppState>,
server: Arc<Server>,
connection_killers: Arc<Mutex<HashMap<PeerId, Arc<AtomicBool>>>>,
forbid_connections: Arc<AtomicBool>,
_test_db: TestDb,
test_live_kit_server: Arc<live_kit_client::TestServer>,
}
impl TestServer {
async fn start(deterministic: &Arc<Deterministic>) -> Self {
static NEXT_LIVE_KIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0);
let use_postgres = env::var("USE_POSTGRES").ok();
let use_postgres = use_postgres.as_deref();
let test_db = if use_postgres == Some("true") || use_postgres == Some("1") {
TestDb::postgres(deterministic.build_background())
} else {
TestDb::sqlite(deterministic.build_background())
};
let live_kit_server_id = NEXT_LIVE_KIT_SERVER_ID.fetch_add(1, SeqCst);
let live_kit_server = live_kit_client::TestServer::create(
format!("http://livekit.{}.test", live_kit_server_id),
format!("devkey-{}", live_kit_server_id),
format!("secret-{}", live_kit_server_id),
deterministic.build_background(),
)
.unwrap();
let app_state = Self::build_app_state(&test_db, &live_kit_server).await;
let epoch = app_state
.db
.create_server(&app_state.config.zed_environment)
.await
.unwrap();
let server = Server::new(
epoch,
app_state.clone(),
Executor::Deterministic(deterministic.build_background()),
);
server.start().await.unwrap();
// Advance clock to ensure the server's cleanup task is finished.
deterministic.advance_clock(CLEANUP_TIMEOUT);
Self {
app_state,
server,
connection_killers: Default::default(),
forbid_connections: Default::default(),
_test_db: test_db,
test_live_kit_server: live_kit_server,
}
}
async fn reset(&self) {
self.app_state.db.reset();
let epoch = self
.app_state
.db
.create_server(&self.app_state.config.zed_environment)
.await
.unwrap();
self.server.reset(epoch);
}
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()));
let mut settings = Settings::test(cx);
settings.projects_online_by_default = false;
cx.set_global(settings);
});
let http = FakeHttpClient::with_404_response();
let user_id = if let Ok(Some(user)) = self
.app_state
.db
.get_user_by_github_account(name, None)
.await
{
user.id
} else {
self.app_state
.db
.create_user(
&format!("{name}@example.com"),
false,
NewUserParams {
github_login: name.into(),
github_user_id: 0,
invite_count: 0,
},
)
.await
.expect("creating user failed")
.user_id
};
let client_name = name.to_string();
let mut client = cx.read(|cx| Client::new(http.clone(), cx));
let server = self.server.clone();
let db = self.app_state.db.clone();
let connection_killers = self.connection_killers.clone();
let forbid_connections = self.forbid_connections.clone();
Arc::get_mut(&mut client)
.unwrap()
.set_id(user_id.0 as usize)
.override_authenticate(move |cx| {
cx.spawn(|_| async move {
let access_token = "the-token".to_string();
Ok(Credentials {
user_id: user_id.0 as u64,
access_token,
})
})
})
.override_establish_connection(move |credentials, cx| {
assert_eq!(credentials.user_id, user_id.0 as u64);
assert_eq!(credentials.access_token, "the-token");
let server = server.clone();
let db = db.clone();
let connection_killers = connection_killers.clone();
let forbid_connections = forbid_connections.clone();
let client_name = client_name.clone();
cx.spawn(move |cx| async move {
if forbid_connections.load(SeqCst) {
Err(EstablishConnectionError::other(anyhow!(
"server is forbidding connections"
)))
} else {
let (client_conn, server_conn, killed) =
Connection::in_memory(cx.background());
let (connection_id_tx, connection_id_rx) = oneshot::channel();
let user = db
.get_user_by_id(user_id)
.await
.expect("retrieving user failed")
.unwrap();
cx.background()
.spawn(server.handle_connection(
server_conn,
client_name,
user,
Some(connection_id_tx),
Executor::Deterministic(cx.background()),
))
.detach();
let connection_id = connection_id_rx.await.unwrap();
connection_killers
.lock()
.insert(connection_id.into(), killed);
Ok(client_conn)
}
})
});
let fs = FakeFs::new(cx.background());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
let app_state = Arc::new(workspace::AppState {
client: client.clone(),
user_store: user_store.clone(),
languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
themes: ThemeRegistry::new((), cx.font_cache()),
fs: fs.clone(),
build_window_options: Default::default,
initialize_workspace: |_, _, _| unimplemented!(),
dock_default_item_factory: |_, _| unimplemented!(),
});
Project::init(&client);
cx.update(|cx| {
workspace::init(app_state.clone(), cx);
call::init(client.clone(), user_store.clone(), cx);
});
client
.authenticate_and_connect(false, &cx.to_async())
.await
.unwrap();
let client = TestClient {
client,
username: name.to_string(),
local_projects: Default::default(),
remote_projects: Default::default(),
next_root_dir_id: 0,
user_store,
fs,
language_registry: Arc::new(LanguageRegistry::test()),
buffers: Default::default(),
};
client.wait_for_current_user(cx).await;
client
}
fn disconnect_client(&self, peer_id: PeerId) {
self.connection_killers
.lock()
.remove(&peer_id)
.unwrap()
.store(true, SeqCst);
}
fn forbid_connections(&self) {
self.forbid_connections.store(true, SeqCst);
}
fn allow_connections(&self) {
self.forbid_connections.store(false, SeqCst);
}
async fn make_contacts(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
for ix in 1..clients.len() {
let (left, right) = clients.split_at_mut(ix);
let (client_a, cx_a) = left.last_mut().unwrap();
for (client_b, cx_b) in right {
client_a
.user_store
.update(*cx_a, |store, cx| {
store.request_contact(client_b.user_id().unwrap(), cx)
})
.await
.unwrap();
cx_a.foreground().run_until_parked();
client_b
.user_store
.update(*cx_b, |store, cx| {
store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
})
.await
.unwrap();
}
}
}
async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
self.make_contacts(clients).await;
let (left, right) = clients.split_at_mut(1);
let (_client_a, cx_a) = &mut left[0];
let active_call_a = cx_a.read(ActiveCall::global);
for (client_b, cx_b) in right {
let user_id_b = client_b.current_user_id(*cx_b).to_proto();
active_call_a
.update(*cx_a, |call, cx| call.invite(user_id_b, None, cx))
.await
.unwrap();
cx_b.foreground().run_until_parked();
let active_call_b = cx_b.read(ActiveCall::global);
active_call_b
.update(*cx_b, |call, cx| call.accept_incoming(cx))
.await
.unwrap();
}
}
async fn build_app_state(
test_db: &TestDb,
fake_server: &live_kit_client::TestServer,
) -> Arc<AppState> {
Arc::new(AppState {
db: test_db.db().clone(),
live_kit_client: Some(Arc::new(fake_server.create_api_client())),
config: Default::default(),
})
}
}
impl Deref for TestServer {
type Target = Server;
fn deref(&self) -> &Self::Target {
&self.server
}
}
impl Drop for TestServer {
fn drop(&mut self) {
self.server.teardown();
self.test_live_kit_server.teardown().unwrap();
}
}
struct TestClient {
client: Arc<Client>,
username: String,
local_projects: Vec<ModelHandle<Project>>,
remote_projects: Vec<ModelHandle<Project>>,
next_root_dir_id: usize,
pub user_store: ModelHandle<UserStore>,
language_registry: Arc<LanguageRegistry>,
fs: Arc<FakeFs>,
buffers: HashMap<ModelHandle<Project>, HashSet<ModelHandle<language::Buffer>>>,
}
impl Deref for TestClient {
type Target = Arc<Client>;
fn deref(&self) -> &Self::Target {
&self.client
}
}
struct ContactsSummary {
pub current: Vec<String>,
pub outgoing_requests: Vec<String>,
pub incoming_requests: Vec<String>,
}
impl TestClient {
pub fn current_user_id(&self, cx: &TestAppContext) -> UserId {
UserId::from_proto(
self.user_store
.read_with(cx, |user_store, _| user_store.current_user().unwrap().id),
)
}
async fn wait_for_current_user(&self, cx: &TestAppContext) {
let mut authed_user = self
.user_store
.read_with(cx, |user_store, _| user_store.watch_current_user());
while authed_user.next().await.unwrap().is_none() {}
}
async fn clear_contacts(&self, cx: &mut TestAppContext) {
self.user_store
.update(cx, |store, _| store.clear_contacts())
.await;
}
fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary {
self.user_store.read_with(cx, |store, _| ContactsSummary {
current: store
.contacts()
.iter()
.map(|contact| contact.user.github_login.clone())
.collect(),
outgoing_requests: store
.outgoing_contact_requests()
.iter()
.map(|user| user.github_login.clone())
.collect(),
incoming_requests: store
.incoming_contact_requests()
.iter()
.map(|user| user.github_login.clone())
.collect(),
})
}
async fn build_local_project(
&self,
root_path: impl AsRef<Path>,
cx: &mut TestAppContext,
) -> (ModelHandle<Project>, WorktreeId) {
let project = cx.update(|cx| {
Project::local(
self.client.clone(),
self.user_store.clone(),
self.language_registry.clone(),
self.fs.clone(),
cx,
)
});
let (worktree, _) = project
.update(cx, |p, cx| {
p.find_or_create_local_worktree(root_path, true, cx)
})
.await
.unwrap();
worktree
.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
.await;
(project, worktree.read_with(cx, |tree, _| tree.id()))
}
async fn build_remote_project(
&self,
host_project_id: u64,
guest_cx: &mut TestAppContext,
) -> ModelHandle<Project> {
let active_call = guest_cx.read(ActiveCall::global);
let room = active_call.read_with(guest_cx, |call, _| call.room().unwrap().clone());
room.update(guest_cx, |room, cx| {
room.join_project(
host_project_id,
self.language_registry.clone(),
self.fs.clone(),
cx,
)
})
.await
.unwrap()
}
fn build_workspace(
&self,
project: &ModelHandle<Project>,
cx: &mut TestAppContext,
) -> ViewHandle<Workspace> {
let (_, root_view) = cx.add_window(|_| EmptyView);
cx.add_view(&root_view, |cx| {
Workspace::new(
Default::default(),
0,
project.clone(),
|_, _| unimplemented!(),
cx,
)
})
}
fn create_new_root_dir(&mut self) -> PathBuf {
format!(
"/{}-root-{}",
self.username,
util::post_inc(&mut self.next_root_dir_id)
)
.into()
}
}
impl Drop for TestClient {
fn drop(&mut self) {
self.client.teardown();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,10 +7,10 @@ mod incoming_call_notification;
mod notifications;
mod project_shared_notification;
use anyhow::anyhow;
use call::ActiveCall;
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleCollaborationMenu};
use gpui::MutableAppContext;
use project::Project;
use std::sync::Arc;
use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
@@ -39,15 +39,20 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
let workspace = if let Some(existing_workspace) = existing_workspace {
existing_workspace
} else {
let project = Project::remote(
project_id,
app_state.client.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
cx.clone(),
)
.await?;
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)(), |cx| {
let mut workspace = Workspace::new(

View File

@@ -8,8 +8,10 @@ use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
elements::*,
geometry::{rect::RectF, vector::vec2f},
impl_actions, impl_internal_actions, keymap, AppContext, CursorStyle, Entity, ModelHandle,
MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle,
impl_actions, impl_internal_actions,
keymap_matcher::KeymapContext,
AppContext, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
Subscription, View, ViewContext, ViewHandle,
};
use menu::{Confirm, SelectNext, SelectPrev};
use project::Project;
@@ -1267,7 +1269,7 @@ impl View for ContactList {
"ContactList"
}
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.set.insert("menu".into());
cx

View File

@@ -3,7 +3,7 @@ use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions,
elements::{ChildView, Flex, Label, ParentElement},
keymap::Keystroke,
keymap_matcher::Keystroke,
Action, AnyViewHandle, Element, Entity, MouseState, MutableAppContext, RenderContext, View,
ViewContext, ViewHandle,
};
@@ -64,8 +64,10 @@ impl CommandPalette {
name: humanize_action_name(name),
action,
keystrokes: bindings
.iter()
.filter_map(|binding| binding.keystrokes())
.last()
.map_or(Vec::new(), |binding| binding.keystrokes().to_vec()),
.map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
})
})
.collect();

View File

@@ -1,7 +1,7 @@
use gpui::{
elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap, platform::CursorStyle,
Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton, MutableAppContext, RenderContext,
SizeConstraint, Subscription, View, ViewContext,
elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap_matcher::KeymapContext,
platform::CursorStyle, Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton,
MutableAppContext, RenderContext, SizeConstraint, Subscription, View, ViewContext,
};
use menu::*;
use settings::Settings;
@@ -75,7 +75,7 @@ impl View for ContextMenu {
"ContextMenu"
}
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.set.insert("menu".into());
cx

View File

@@ -327,8 +327,6 @@ mod tests {
.path();
corrupted_backup_dir.push(DB_FILE_NAME);
dbg!(&corrupted_backup_dir);
let backup = Connection::open_file(&corrupted_backup_dir.to_string_lossy());
assert!(backup.select_row::<usize>("SELECT * FROM test").unwrap()()
.unwrap()

View File

@@ -36,6 +36,7 @@ use gpui::{
fonts::{self, HighlightStyle, TextStyle},
geometry::vector::Vector2F,
impl_actions, impl_internal_actions,
keymap_matcher::KeymapContext,
platform::CursorStyle,
serde_json::json,
AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
@@ -464,7 +465,7 @@ pub struct Editor {
searchable: bool,
cursor_shape: CursorShape,
workspace_id: Option<WorkspaceId>,
keymap_context_layers: BTreeMap<TypeId, gpui::keymap::Context>,
keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
input_enabled: bool,
leader_replica_id: Option<u16>,
remote_id: Option<ViewId>,
@@ -827,6 +828,23 @@ impl CompletionsMenu {
})
.collect()
};
//Remove all candidates where the query's start does not match the start of any word in the candidate
if let Some(query) = query {
if let Some(query_start) = query.chars().next() {
matches.retain(|string_match| {
split_words(&string_match.string).any(|word| {
//Check that the first codepoint of the word as lowercase matches the first
//codepoint of the query as lowercase
word.chars()
.flat_map(|codepoint| codepoint.to_lowercase())
.zip(query_start.to_lowercase())
.all(|(word_cp, query_cp)| word_cp == query_cp)
})
});
}
}
matches.sort_unstable_by_key(|mat| {
let completion = &self.completions[mat.candidate_id];
(
@@ -1225,7 +1243,7 @@ impl Editor {
}
}
pub fn set_keymap_context_layer<Tag: 'static>(&mut self, context: gpui::keymap::Context) {
pub fn set_keymap_context_layer<Tag: 'static>(&mut self, context: KeymapContext) {
self.keymap_context_layers
.insert(TypeId::of::<Tag>(), context);
}
@@ -2002,7 +2020,9 @@ impl Editor {
old_selections
.iter()
.map(|s| (s.start..s.end, text.clone())),
Some(AutoindentMode::EachLine),
Some(AutoindentMode::Block {
original_indent_columns: Vec::new(),
}),
cx,
);
anchors
@@ -4781,7 +4801,7 @@ impl Editor {
if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() {
let (group_id, jump_to) = popover.activation_info();
if self.activate_diagnostics(group_id, cx) {
self.change_selections(Some(Autoscroll::center()), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
let mut new_selection = s.newest_anchor().clone();
new_selection.collapse_to(jump_to, SelectionGoal::None);
s.select_anchors(vec![new_selection.clone()]);
@@ -4827,7 +4847,7 @@ impl Editor {
if let Some((primary_range, group_id)) = group {
if self.activate_diagnostics(group_id, cx) {
self.change_selections(Some(Autoscroll::center()), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(vec![Selection {
id: selection.id,
start: primary_range.start,
@@ -4902,7 +4922,7 @@ impl Editor {
.dedup();
if let Some(hunk) = hunks.next() {
this.change_selections(Some(Autoscroll::center()), cx, |s| {
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
let row = hunk.start_display_row();
let point = DisplayPoint::new(row, 0);
s.select_display_ranges([point..point]);
@@ -5000,7 +5020,7 @@ impl Editor {
if editor_handle != target_editor_handle {
pane.update(cx, |pane, _| pane.disable_history());
}
target_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([range]);
});
@@ -6069,10 +6089,11 @@ impl Editor {
let extension = Path::new(file.file_name(cx))
.extension()
.and_then(|e| e.to_str());
project
.read(cx)
.client()
.report_event(name, json!({ "File Extension": extension }));
project.read(cx).client().report_event(
name,
json!({ "File Extension": extension }),
cx.global::<Settings>().telemetry(),
);
}
}
}
@@ -6245,7 +6266,7 @@ impl View for Editor {
false
}
fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut context = Self::default_keymap_context();
let mode = match self.mode {
EditorMode::SingleLine => "single_line",
@@ -6799,6 +6820,34 @@ pub fn styled_runs_for_code_label<'a>(
})
}
pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator<Item = &'a str> + 'a {
let mut index = 0;
let mut codepoints = text.char_indices().peekable();
std::iter::from_fn(move || {
let start_index = index;
while let Some((new_index, codepoint)) = codepoints.next() {
index = new_index + codepoint.len_utf8();
let current_upper = codepoint.is_uppercase();
let next_upper = codepoints
.peek()
.map(|(_, c)| c.is_uppercase())
.unwrap_or(false);
if !current_upper && next_upper {
return Some(&text[start_index..index]);
}
}
index = text.len();
if start_index < text.len() {
return Some(&text[start_index..]);
}
None
})
.flat_map(|word| word.split_inclusive('_'))
}
trait RangeExt<T> {
fn sorted(&self) -> Range<T>;
fn to_inclusive(&self) -> RangeInclusive<T>;

View File

@@ -29,7 +29,11 @@ use workspace::{
#[gpui::test]
fn test_edit_events(cx: &mut MutableAppContext) {
cx.set_global(Settings::test(cx));
let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx));
let buffer = cx.add_model(|cx| {
let mut buffer = language::Buffer::new(0, "123456", cx);
buffer.set_group_interval(Duration::from_secs(1));
buffer
});
let events = Rc::new(RefCell::new(Vec::new()));
let (_, editor1) = cx.add_window(Default::default(), {
@@ -3502,6 +3506,8 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
]
);
view.undo(&Undo, cx);
view.undo(&Undo, cx);
view.undo(&Undo, cx);
assert_eq!(
view.text(cx),
@@ -5439,6 +5445,20 @@ async fn go_to_hunk(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppCon
);
}
#[test]
fn test_split_words() {
fn split<'a>(text: &'a str) -> Vec<&'a str> {
split_words(text).collect()
}
assert_eq!(split("HelloWorld"), &["Hello", "World"]);
assert_eq!(split("hello_world"), &["hello_", "world"]);
assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
assert_eq!(split("Hello_World"), &["Hello_", "World"]);
assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
assert_eq!(split("helloworld"), &["helloworld"]);
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(row as u32, column as u32);
point..point

View File

@@ -8,6 +8,7 @@ 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,
@@ -765,6 +766,7 @@ impl Item for Editor {
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
let workspace_id = workspace.database_id();
let item_id = cx.view_id();
self.workspace_id = Some(workspace_id);
fn serialize(
buffer: ModelHandle<Buffer>,
@@ -836,7 +838,11 @@ impl Item for Editor {
.context("Project item at stored path was not a buffer")?;
Ok(cx.update(|cx| {
cx.add_view(pane, |cx| Editor::for_buffer(buffer, Some(project), cx))
cx.add_view(pane, |cx| {
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
editor.read_scroll_position_from_db(item_id, workspace_id, cx);
editor
})
}))
})
})
@@ -1114,7 +1120,7 @@ fn path_for_buffer<'a>(
cx: &'a AppContext,
) -> Option<Cow<'a, Path>> {
let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
path_for_file(file, height, include_filename, cx)
path_for_file(file.as_ref(), height, include_filename, cx)
}
fn path_for_file<'a>(

View File

@@ -352,6 +352,29 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<
start..end
}
pub fn split_display_range_by_lines(
map: &DisplaySnapshot,
range: Range<DisplayPoint>,
) -> Vec<Range<DisplayPoint>> {
let mut result = Vec::new();
let mut start = range.start;
// Loop over all the covered rows until the one containing the range end
for row in range.start.row()..range.end.row() {
let row_end_column = map.line_len(row);
let end = map.clip_point(DisplayPoint::new(row, row_end_column), Bias::Left);
if start != end {
result.push(start..end);
}
start = map.clip_point(DisplayPoint::new(row + 1, 0), Bias::Left);
}
// Add the final range from the start of the last end to the original range end.
result.push(start..range.end);
result
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -1311,7 +1311,7 @@ impl MultiBuffer {
.and_then(|(buffer, offset)| buffer.read(cx).language_at(offset))
}
pub fn files<'a>(&'a self, cx: &'a AppContext) -> SmallVec<[&'a dyn File; 2]> {
pub fn files<'a>(&'a self, cx: &'a AppContext) -> SmallVec<[&'a Arc<dyn File>; 2]> {
let buffers = self.buffers.borrow();
buffers
.values()
@@ -3651,7 +3651,7 @@ mod tests {
let state = host_buffer.read(cx).to_proto();
let ops = cx
.background()
.block(host_buffer.read(cx).serialize_ops(cx));
.block(host_buffer.read(cx).serialize_ops(None, cx));
let mut buffer = Buffer::from_proto(1, state, None).unwrap();
buffer
.apply_ops(

View File

@@ -2,9 +2,19 @@ use std::path::PathBuf;
use db::sqlez_macros::sql;
use db::{define_connection, query};
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
define_connection!(
// Current table shape using pseudo-rust syntax:
// editors(
// item_id: usize,
// workspace_id: usize,
// path: PathBuf,
// scroll_top_row: usize,
// scroll_vertical_offset: f32,
// scroll_horizontal_offset: f32,
// )
pub static ref DB: EditorDb<WorkspaceDb> =
&[sql! (
CREATE TABLE editors(
@@ -15,8 +25,13 @@ define_connection!(
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
ON UPDATE CASCADE
) STRICT;
)];
) STRICT;
),
sql! (
ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0;
ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0;
ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0;
)];
);
impl EditorDb {
@@ -29,8 +44,40 @@ impl EditorDb {
query! {
pub async fn save_path(item_id: ItemId, workspace_id: WorkspaceId, path: PathBuf) -> Result<()> {
INSERT OR REPLACE INTO editors(item_id, workspace_id, path)
VALUES (?, ?, ?)
INSERT INTO editors
(item_id, workspace_id, path)
VALUES
(?1, ?2, ?3)
ON CONFLICT DO UPDATE SET
item_id = ?1,
workspace_id = ?2,
path = ?3
}
}
// Returns the scroll top row, and offset
query! {
pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f32, f32)>> {
SELECT scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
FROM editors
WHERE item_id = ? AND workspace_id = ?
}
}
query! {
pub async fn save_scroll_position(
item_id: ItemId,
workspace_id: WorkspaceId,
top_row: u32,
vertical_offset: f32,
horizontal_offset: f32
) -> Result<()> {
UPDATE OR IGNORE editors
SET
scroll_top_row = ?3,
scroll_horizontal_offset = ?4,
scroll_vertical_offset = ?5
WHERE item_id = ?1 AND workspace_id = ?2
}
}
}

View File

@@ -11,11 +11,14 @@ use gpui::{
geometry::vector::{vec2f, Vector2F},
Axis, MutableAppContext, Task, ViewContext,
};
use language::Bias;
use language::{Bias, Point};
use util::ResultExt;
use workspace::WorkspaceId;
use crate::{
display_map::{DisplaySnapshot, ToDisplayPoint},
hover_popover::hide_hover,
persistence::DB,
Anchor, DisplayPoint, Editor, EditorMode, Event, MultiBufferSnapshot, ToPoint,
};
@@ -170,37 +173,68 @@ impl ScrollManager {
scroll_position: Vector2F,
map: &DisplaySnapshot,
local: bool,
workspace_id: Option<i64>,
cx: &mut ViewContext<Editor>,
) {
let new_anchor = if scroll_position.y() <= 0. {
ScrollAnchor {
top_anchor: Anchor::min(),
offset: scroll_position.max(vec2f(0., 0.)),
}
let (new_anchor, top_row) = if scroll_position.y() <= 0. {
(
ScrollAnchor {
top_anchor: Anchor::min(),
offset: scroll_position.max(vec2f(0., 0.)),
},
0,
)
} else {
let scroll_top_buffer_offset =
DisplayPoint::new(scroll_position.y() as u32, 0).to_offset(&map, Bias::Right);
let scroll_top_buffer_point =
DisplayPoint::new(scroll_position.y() as u32, 0).to_point(&map);
let top_anchor = map
.buffer_snapshot
.anchor_at(scroll_top_buffer_offset, Bias::Right);
.anchor_at(scroll_top_buffer_point, Bias::Right);
ScrollAnchor {
top_anchor,
offset: vec2f(
scroll_position.x(),
scroll_position.y() - top_anchor.to_display_point(&map).row() as f32,
),
}
(
ScrollAnchor {
top_anchor,
offset: vec2f(
scroll_position.x(),
scroll_position.y() - top_anchor.to_display_point(&map).row() as f32,
),
},
scroll_top_buffer_point.row,
)
};
self.set_anchor(new_anchor, local, cx);
self.set_anchor(new_anchor, top_row, local, workspace_id, cx);
}
fn set_anchor(&mut self, anchor: ScrollAnchor, local: bool, cx: &mut ViewContext<Editor>) {
fn set_anchor(
&mut self,
anchor: ScrollAnchor,
top_row: u32,
local: bool,
workspace_id: Option<i64>,
cx: &mut ViewContext<Editor>,
) {
self.anchor = anchor;
cx.emit(Event::ScrollPositionChanged { local });
self.show_scrollbar(cx);
self.autoscroll_request.take();
if let Some(workspace_id) = workspace_id {
let item_id = cx.view_id();
cx.background()
.spawn(async move {
DB.save_scroll_position(
item_id,
workspace_id,
top_row,
anchor.offset.x(),
anchor.offset.y(),
)
.await
.log_err()
})
.detach()
}
cx.notify();
}
@@ -274,8 +308,13 @@ impl Editor {
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
hide_hover(self, cx);
self.scroll_manager
.set_scroll_position(scroll_position, &map, local, cx);
self.scroll_manager.set_scroll_position(
scroll_position,
&map,
local,
self.workspace_id,
cx,
);
}
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
@@ -285,7 +324,12 @@ impl Editor {
pub fn set_scroll_anchor(&mut self, scroll_anchor: ScrollAnchor, cx: &mut ViewContext<Self>) {
hide_hover(self, cx);
self.scroll_manager.set_anchor(scroll_anchor, true, cx);
let top_row = scroll_anchor
.top_anchor
.to_point(&self.buffer().read(cx).snapshot(cx))
.row;
self.scroll_manager
.set_anchor(scroll_anchor, top_row, true, self.workspace_id, cx);
}
pub(crate) fn set_scroll_anchor_remote(
@@ -294,7 +338,12 @@ impl Editor {
cx: &mut ViewContext<Self>,
) {
hide_hover(self, cx);
self.scroll_manager.set_anchor(scroll_anchor, false, cx);
let top_row = scroll_anchor
.top_anchor
.to_point(&self.buffer().read(cx).snapshot(cx))
.row;
self.scroll_manager
.set_anchor(scroll_anchor, top_row, false, self.workspace_id, cx);
}
pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {
@@ -345,4 +394,25 @@ impl Editor {
Ordering::Greater
}
pub fn read_scroll_position_from_db(
&mut self,
item_id: usize,
workspace_id: WorkspaceId,
cx: &mut ViewContext<Editor>,
) {
let scroll_position = DB.get_scroll_position(item_id, workspace_id);
if let Ok(Some((top_row, x, y))) = scroll_position {
let top_anchor = self
.buffer()
.read(cx)
.snapshot(cx)
.anchor_at(Point::new(top_row as u32, 0), Bias::Left);
let scroll_anchor = ScrollAnchor {
offset: Vector2F::new(x, y),
top_anchor,
};
self.set_scroll_anchor(scroll_anchor, cx);
}
}
}

View File

@@ -9,7 +9,9 @@ use indoc::indoc;
use crate::{
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
};
use gpui::{keymap::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle};
use gpui::{
keymap_matcher::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle,
};
use language::{Buffer, BufferSnapshot};
use settings::Settings;
use util::{

View File

@@ -35,7 +35,7 @@ use repository::FakeGitRepositoryState;
use std::sync::Weak;
lazy_static! {
static ref CARRIAGE_RETURNS_REGEX: Regex = Regex::new("\r\n|\r").unwrap();
static ref LINE_SEPERATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap();
}
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -80,13 +80,13 @@ impl LineEnding {
}
pub fn normalize(text: &mut String) {
if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(text, "\n") {
if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(text, "\n") {
*text = replaced;
}
}
pub fn normalize_arc(text: Arc<str>) -> Arc<str> {
if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(&text, "\n") {
if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(&text, "\n") {
replaced.into()
} else {
text
@@ -389,6 +389,7 @@ pub struct FakeFs {
struct FakeFsState {
root: Arc<Mutex<FakeFsEntry>>,
next_inode: u64,
next_mtime: SystemTime,
event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>,
}
@@ -517,10 +518,11 @@ impl FakeFs {
state: Mutex::new(FakeFsState {
root: Arc::new(Mutex::new(FakeFsEntry::Dir {
inode: 0,
mtime: SystemTime::now(),
mtime: SystemTime::UNIX_EPOCH,
entries: Default::default(),
git_repo_state: None,
})),
next_mtime: SystemTime::UNIX_EPOCH,
next_inode: 1,
event_txs: Default::default(),
}),
@@ -531,10 +533,12 @@ impl FakeFs {
let mut state = self.state.lock().await;
let path = path.as_ref();
let inode = state.next_inode;
let mtime = state.next_mtime;
state.next_inode += 1;
state.next_mtime += Duration::from_nanos(1);
let file = Arc::new(Mutex::new(FakeFsEntry::File {
inode,
mtime: SystemTime::now(),
mtime,
content,
}));
state
@@ -631,6 +635,21 @@ impl FakeFs {
}
}
pub async fn paths(&self) -> Vec<PathBuf> {
let mut result = Vec::new();
let mut queue = collections::VecDeque::new();
queue.push_back((PathBuf::from("/"), self.state.lock().await.root.clone()));
while let Some((path, entry)) = queue.pop_front() {
if let FakeFsEntry::Dir { entries, .. } = &*entry.lock().await {
for (name, entry) in entries {
queue.push_back((path.join(name), entry.clone()));
}
}
result.push(path);
}
result
}
pub async fn directories(&self) -> Vec<PathBuf> {
let mut result = Vec::new();
let mut queue = collections::VecDeque::new();
@@ -726,6 +745,8 @@ impl Fs for FakeFs {
}
let inode = state.next_inode;
let mtime = state.next_mtime;
state.next_mtime += Duration::from_nanos(1);
state.next_inode += 1;
state
.write_path(&cur_path, |entry| {
@@ -733,7 +754,7 @@ impl Fs for FakeFs {
created_dirs.push(cur_path.clone());
Arc::new(Mutex::new(FakeFsEntry::Dir {
inode,
mtime: SystemTime::now(),
mtime,
entries: Default::default(),
git_repo_state: None,
}))
@@ -751,10 +772,12 @@ impl Fs for FakeFs {
self.simulate_random_delay().await;
let mut state = self.state.lock().await;
let inode = state.next_inode;
let mtime = state.next_mtime;
state.next_mtime += Duration::from_nanos(1);
state.next_inode += 1;
let file = Arc::new(Mutex::new(FakeFsEntry::File {
inode,
mtime: SystemTime::now(),
mtime,
content: String::new(),
}));
state
@@ -816,6 +839,9 @@ impl Fs for FakeFs {
let source = normalize_path(source);
let target = normalize_path(target);
let mut state = self.state.lock().await;
let mtime = state.next_mtime;
let inode = util::post_inc(&mut state.next_inode);
state.next_mtime += Duration::from_nanos(1);
let source_entry = state.read_path(&source).await?;
let content = source_entry.lock().await.file_content(&source)?.clone();
let entry = state
@@ -831,8 +857,8 @@ impl Fs for FakeFs {
}
btree_map::Entry::Vacant(e) => Ok(Some(
e.insert(Arc::new(Mutex::new(FakeFsEntry::File {
inode: 0,
mtime: SystemTime::now(),
inode,
mtime,
content: String::new(),
})))
.clone(),

View File

@@ -45,7 +45,6 @@ smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
time = { version = "0.3", features = ["serde", "serde-well-known"] }
tiny-skia = "0.5"
tree-sitter = "0.20"
usvg = "0.14"
waker-fn = "1.1.0"

View File

@@ -6,7 +6,6 @@ use std::{
fn main() {
generate_dispatch_bindings();
compile_context_predicate_parser();
compile_metal_shaders();
generate_shader_bindings();
}
@@ -30,17 +29,6 @@ fn generate_dispatch_bindings() {
.expect("couldn't write dispatch bindings");
}
fn compile_context_predicate_parser() {
let dir = PathBuf::from("./grammars/context-predicate/src");
let parser_c = dir.join("parser.c");
println!("cargo:rerun-if-changed={}", &parser_c.to_str().unwrap());
cc::Build::new()
.include(&dir)
.file(parser_c)
.compile("tree_sitter_context_predicate");
}
const SHADER_HEADER_PATH: &str = "./src/platform/mac/shaders/shaders.h";
fn compile_metal_shaders() {
@@ -52,7 +40,7 @@ fn compile_metal_shaders() {
println!("cargo:rerun-if-changed={}", shader_path);
let output = Command::new("xcrun")
.args(&[
.args([
"-sdk",
"macosx",
"metal",
@@ -76,7 +64,7 @@ fn compile_metal_shaders() {
}
let output = Command::new("xcrun")
.args(&["-sdk", "macosx", "metallib"])
.args(["-sdk", "macosx", "metallib"])
.arg(air_output_path)
.arg("-o")
.arg(metallib_output_path)

View File

@@ -1,2 +0,0 @@
/node_modules
/build

View File

@@ -1,20 +0,0 @@
[package]
name = "tree-sitter-context-predicate"
description = "context-predicate grammar for the tree-sitter parsing library"
version = "0.0.1"
keywords = ["incremental", "parsing", "context-predicate"]
categories = ["parsing", "text-editors"]
repository = "https://github.com/tree-sitter/tree-sitter-javascript"
edition = "2021"
license = "MIT"
build = "bindings/rust/build.rs"
include = ["bindings/rust/*", "grammar.js", "queries/*", "src/*"]
[lib]
path = "bindings/rust/lib.rs"
[dependencies]
tree-sitter = "0.20"
[build-dependencies]
cc = "1.0"

View File

@@ -1,18 +0,0 @@
{
"targets": [
{
"target_name": "tree_sitter_context_predicate_binding",
"include_dirs": [
"<!(node -e \"require('nan')\")",
"src"
],
"sources": [
"src/parser.c",
"bindings/node/binding.cc"
],
"cflags_c": [
"-std=c99",
]
}
]
}

View File

@@ -1,30 +0,0 @@
#include "nan.h"
#include "tree_sitter/parser.h"
#include <node.h>
using namespace v8;
extern "C" TSLanguage *tree_sitter_context_predicate();
namespace {
NAN_METHOD(New) {}
void Init(Local<Object> exports, Local<Object> module) {
Local<FunctionTemplate> tpl = Nan::New<FunctionTemplate>(New);
tpl->SetClassName(Nan::New("Language").ToLocalChecked());
tpl->InstanceTemplate()->SetInternalFieldCount(1);
Local<Function> constructor = Nan::GetFunction(tpl).ToLocalChecked();
Local<Object> instance =
constructor->NewInstance(Nan::GetCurrentContext()).ToLocalChecked();
Nan::SetInternalFieldPointer(instance, 0, tree_sitter_context_predicate());
Nan::Set(instance, Nan::New("name").ToLocalChecked(),
Nan::New("context_predicate").ToLocalChecked());
Nan::Set(module, Nan::New("exports").ToLocalChecked(), instance);
}
NODE_MODULE(tree_sitter_context_predicate_binding, Init)
} // namespace

View File

@@ -1,19 +0,0 @@
try {
module.exports = require("../../build/Release/tree_sitter_context_predicate_binding");
} catch (error1) {
if (error1.code !== 'MODULE_NOT_FOUND') {
throw error1;
}
try {
module.exports = require("../../build/Debug/tree_sitter_context_predicate_binding");
} catch (error2) {
if (error2.code !== 'MODULE_NOT_FOUND') {
throw error2;
}
throw error1
}
}
try {
module.exports.nodeTypeInfo = require("../../src/node-types.json");
} catch (_) {}

View File

@@ -1,40 +0,0 @@
fn main() {
let src_dir = std::path::Path::new("src");
let mut c_config = cc::Build::new();
c_config.include(&src_dir);
c_config
.flag_if_supported("-Wno-unused-parameter")
.flag_if_supported("-Wno-unused-but-set-variable")
.flag_if_supported("-Wno-trigraphs");
let parser_path = src_dir.join("parser.c");
c_config.file(&parser_path);
// If your language uses an external scanner written in C,
// then include this block of code:
/*
let scanner_path = src_dir.join("scanner.c");
c_config.file(&scanner_path);
println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap());
*/
c_config.compile("parser");
println!("cargo:rerun-if-changed={}", parser_path.to_str().unwrap());
// If your language uses an external scanner written in C++,
// then include this block of code:
/*
let mut cpp_config = cc::Build::new();
cpp_config.cpp(true);
cpp_config.include(&src_dir);
cpp_config
.flag_if_supported("-Wno-unused-parameter")
.flag_if_supported("-Wno-unused-but-set-variable");
let scanner_path = src_dir.join("scanner.cc");
cpp_config.file(&scanner_path);
cpp_config.compile("scanner");
println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap());
*/
}

View File

@@ -1,52 +0,0 @@
//! This crate provides context_predicate language support for the [tree-sitter][] parsing library.
//!
//! Typically, you will use the [language][language func] function to add this language to a
//! tree-sitter [Parser][], and then use the parser to parse some code:
//!
//! ```
//! let code = "";
//! let mut parser = tree_sitter::Parser::new();
//! parser.set_language(tree_sitter_context_predicate::language()).expect("Error loading context_predicate grammar");
//! let tree = parser.parse(code, None).unwrap();
//! ```
//!
//! [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html
//! [language func]: fn.language.html
//! [Parser]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Parser.html
//! [tree-sitter]: https://tree-sitter.github.io/
use tree_sitter::Language;
extern "C" {
fn tree_sitter_context_predicate() -> Language;
}
/// Get the tree-sitter [Language][] for this grammar.
///
/// [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html
pub fn language() -> Language {
unsafe { tree_sitter_context_predicate() }
}
/// The content of the [`node-types.json`][] file for this grammar.
///
/// [`node-types.json`]: https://tree-sitter.github.io/tree-sitter/using-parsers#static-node-types
pub const NODE_TYPES: &'static str = include_str!("../../src/node-types.json");
// Uncomment these to include any queries that this grammar contains
// pub const HIGHLIGHTS_QUERY: &'static str = include_str!("../../queries/highlights.scm");
// pub const INJECTIONS_QUERY: &'static str = include_str!("../../queries/injections.scm");
// pub const LOCALS_QUERY: &'static str = include_str!("../../queries/locals.scm");
// pub const TAGS_QUERY: &'static str = include_str!("../../queries/tags.scm");
#[cfg(test)]
mod tests {
#[test]
fn test_can_load_grammar() {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(super::language())
.expect("Error loading context_predicate language");
}
}

View File

@@ -1,49 +0,0 @@
==================
Identifiers
==================
abc12
---
(source (identifier))
==================
Negation
==================
!abc
---
(source (not (identifier)))
==================
And/Or
==================
a || b && c && d
---
(source
(or
(identifier)
(and
(and (identifier) (identifier))
(identifier))))
==================
Expressions
==================
a && (b == c || d != e)
---
(source
(and
(identifier)
(parenthesized (or
(equal (identifier) (identifier))
(not_equal (identifier) (identifier))))))

View File

@@ -1,31 +0,0 @@
module.exports = grammar({
name: 'context_predicate',
rules: {
source: $ => $._expression,
_expression: $ => choice(
$.identifier,
$.not,
$.and,
$.or,
$.equal,
$.not_equal,
$.parenthesized,
),
identifier: $ => /[A-Za-z0-9_-]+/,
not: $ => prec(3, seq("!", field("expression", $._expression))),
and: $ => prec.left(2, seq(field("left", $._expression), "&&", field("right", $._expression))),
or: $ => prec.left(1, seq(field("left", $._expression), "||", field("right", $._expression))),
equal: $ => seq(field("left", $.identifier), "==", field("right", $.identifier)),
not_equal: $ => seq(field("left", $.identifier), "!=", field("right", $.identifier)),
parenthesized: $ => seq("(", field("expression", $._expression), ")"),
}
});

View File

@@ -1,44 +0,0 @@
{
"name": "tree-sitter-context-predicate",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "tree-sitter-context-predicate",
"dependencies": {
"nan": "^2.14.0"
},
"devDependencies": {
"tree-sitter-cli": "^0.19.5"
}
},
"node_modules/nan": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
},
"node_modules/tree-sitter-cli": {
"version": "0.19.5",
"resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.19.5.tgz",
"integrity": "sha512-kRzKrUAwpDN9AjA3b0tPBwT1hd8N2oQvvvHup2OEsX6mdsSMLmAvR+NSqK9fe05JrRbVvG8mbteNUQsxlMQohQ==",
"dev": true,
"hasInstallScript": true,
"bin": {
"tree-sitter": "cli.js"
}
}
},
"dependencies": {
"nan": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
},
"tree-sitter-cli": {
"version": "0.19.5",
"resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.19.5.tgz",
"integrity": "sha512-kRzKrUAwpDN9AjA3b0tPBwT1hd8N2oQvvvHup2OEsX6mdsSMLmAvR+NSqK9fe05JrRbVvG8mbteNUQsxlMQohQ==",
"dev": true
}
}
}

View File

@@ -1,10 +0,0 @@
{
"name": "tree-sitter-context-predicate",
"main": "bindings/node",
"devDependencies": {
"tree-sitter-cli": "^0.19.5"
},
"dependencies": {
"nan": "^2.14.0"
}
}

View File

@@ -1,208 +0,0 @@
{
"name": "context_predicate",
"rules": {
"source": {
"type": "SYMBOL",
"name": "_expression"
},
"_expression": {
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "identifier"
},
{
"type": "SYMBOL",
"name": "not"
},
{
"type": "SYMBOL",
"name": "and"
},
{
"type": "SYMBOL",
"name": "or"
},
{
"type": "SYMBOL",
"name": "equal"
},
{
"type": "SYMBOL",
"name": "not_equal"
},
{
"type": "SYMBOL",
"name": "parenthesized"
}
]
},
"identifier": {
"type": "PATTERN",
"value": "[A-Za-z0-9_-]+"
},
"not": {
"type": "PREC",
"value": 3,
"content": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "!"
},
{
"type": "FIELD",
"name": "expression",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
"and": {
"type": "PREC_LEFT",
"value": 2,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "STRING",
"value": "&&"
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
"or": {
"type": "PREC_LEFT",
"value": 1,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "STRING",
"value": "||"
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
"equal": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "identifier"
}
},
{
"type": "STRING",
"value": "=="
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "identifier"
}
}
]
},
"not_equal": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "identifier"
}
},
{
"type": "STRING",
"value": "!="
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "identifier"
}
}
]
},
"parenthesized": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "("
},
{
"type": "FIELD",
"name": "expression",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "STRING",
"value": ")"
}
]
}
},
"extras": [
{
"type": "PATTERN",
"value": "\\s"
}
],
"conflicts": [],
"precedences": [],
"externals": [],
"inline": [],
"supertypes": []
}

View File

@@ -1,353 +0,0 @@
[
{
"type": "and",
"named": true,
"fields": {
"left": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
},
"right": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
}
}
},
{
"type": "equal",
"named": true,
"fields": {
"left": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
}
]
},
"right": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
}
]
}
}
},
{
"type": "not",
"named": true,
"fields": {
"expression": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
}
}
},
{
"type": "not_equal",
"named": true,
"fields": {
"left": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
}
]
},
"right": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
}
]
}
}
},
{
"type": "or",
"named": true,
"fields": {
"left": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
},
"right": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
}
}
},
{
"type": "parenthesized",
"named": true,
"fields": {
"expression": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
}
}
},
{
"type": "source",
"named": true,
"fields": {},
"children": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
}
},
{
"type": "!",
"named": false
},
{
"type": "!=",
"named": false
},
{
"type": "&&",
"named": false
},
{
"type": "(",
"named": false
},
{
"type": ")",
"named": false
},
{
"type": "==",
"named": false
},
{
"type": "identifier",
"named": true
},
{
"type": "||",
"named": false
}
]

View File

@@ -1,584 +0,0 @@
#include <tree_sitter/parser.h>
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
#endif
#define LANGUAGE_VERSION 13
#define STATE_COUNT 18
#define LARGE_STATE_COUNT 6
#define SYMBOL_COUNT 17
#define ALIAS_COUNT 0
#define TOKEN_COUNT 9
#define EXTERNAL_TOKEN_COUNT 0
#define FIELD_COUNT 3
#define MAX_ALIAS_SEQUENCE_LENGTH 3
#define PRODUCTION_ID_COUNT 3
enum {
sym_identifier = 1,
anon_sym_BANG = 2,
anon_sym_AMP_AMP = 3,
anon_sym_PIPE_PIPE = 4,
anon_sym_EQ_EQ = 5,
anon_sym_BANG_EQ = 6,
anon_sym_LPAREN = 7,
anon_sym_RPAREN = 8,
sym_source = 9,
sym__expression = 10,
sym_not = 11,
sym_and = 12,
sym_or = 13,
sym_equal = 14,
sym_not_equal = 15,
sym_parenthesized = 16,
};
static const char *const ts_symbol_names[] = {
[ts_builtin_sym_end] = "end",
[sym_identifier] = "identifier",
[anon_sym_BANG] = "!",
[anon_sym_AMP_AMP] = "&&",
[anon_sym_PIPE_PIPE] = "||",
[anon_sym_EQ_EQ] = "==",
[anon_sym_BANG_EQ] = "!=",
[anon_sym_LPAREN] = "(",
[anon_sym_RPAREN] = ")",
[sym_source] = "source",
[sym__expression] = "_expression",
[sym_not] = "not",
[sym_and] = "and",
[sym_or] = "or",
[sym_equal] = "equal",
[sym_not_equal] = "not_equal",
[sym_parenthesized] = "parenthesized",
};
static const TSSymbol ts_symbol_map[] = {
[ts_builtin_sym_end] = ts_builtin_sym_end,
[sym_identifier] = sym_identifier,
[anon_sym_BANG] = anon_sym_BANG,
[anon_sym_AMP_AMP] = anon_sym_AMP_AMP,
[anon_sym_PIPE_PIPE] = anon_sym_PIPE_PIPE,
[anon_sym_EQ_EQ] = anon_sym_EQ_EQ,
[anon_sym_BANG_EQ] = anon_sym_BANG_EQ,
[anon_sym_LPAREN] = anon_sym_LPAREN,
[anon_sym_RPAREN] = anon_sym_RPAREN,
[sym_source] = sym_source,
[sym__expression] = sym__expression,
[sym_not] = sym_not,
[sym_and] = sym_and,
[sym_or] = sym_or,
[sym_equal] = sym_equal,
[sym_not_equal] = sym_not_equal,
[sym_parenthesized] = sym_parenthesized,
};
static const TSSymbolMetadata ts_symbol_metadata[] = {
[ts_builtin_sym_end] =
{
.visible = false,
.named = true,
},
[sym_identifier] =
{
.visible = true,
.named = true,
},
[anon_sym_BANG] =
{
.visible = true,
.named = false,
},
[anon_sym_AMP_AMP] =
{
.visible = true,
.named = false,
},
[anon_sym_PIPE_PIPE] =
{
.visible = true,
.named = false,
},
[anon_sym_EQ_EQ] =
{
.visible = true,
.named = false,
},
[anon_sym_BANG_EQ] =
{
.visible = true,
.named = false,
},
[anon_sym_LPAREN] =
{
.visible = true,
.named = false,
},
[anon_sym_RPAREN] =
{
.visible = true,
.named = false,
},
[sym_source] =
{
.visible = true,
.named = true,
},
[sym__expression] =
{
.visible = false,
.named = true,
},
[sym_not] =
{
.visible = true,
.named = true,
},
[sym_and] =
{
.visible = true,
.named = true,
},
[sym_or] =
{
.visible = true,
.named = true,
},
[sym_equal] =
{
.visible = true,
.named = true,
},
[sym_not_equal] =
{
.visible = true,
.named = true,
},
[sym_parenthesized] =
{
.visible = true,
.named = true,
},
};
enum {
field_expression = 1,
field_left = 2,
field_right = 3,
};
static const char *const ts_field_names[] = {
[0] = NULL,
[field_expression] = "expression",
[field_left] = "left",
[field_right] = "right",
};
static const TSFieldMapSlice ts_field_map_slices[PRODUCTION_ID_COUNT] = {
[1] = {.index = 0, .length = 1},
[2] = {.index = 1, .length = 2},
};
static const TSFieldMapEntry ts_field_map_entries[] = {
[0] = {field_expression, 1},
[1] = {field_left, 0},
{field_right, 2},
};
static const TSSymbol ts_alias_sequences[PRODUCTION_ID_COUNT]
[MAX_ALIAS_SEQUENCE_LENGTH] = {
[0] = {0},
};
static const uint16_t ts_non_terminal_alias_map[] = {
0,
};
static bool ts_lex(TSLexer *lexer, TSStateId state) {
START_LEXER();
eof = lexer->eof(lexer);
switch (state) {
case 0:
if (eof)
ADVANCE(7);
if (lookahead == '!')
ADVANCE(10);
if (lookahead == '&')
ADVANCE(2);
if (lookahead == '(')
ADVANCE(15);
if (lookahead == ')')
ADVANCE(16);
if (lookahead == '=')
ADVANCE(4);
if (lookahead == '|')
ADVANCE(5);
if (lookahead == '\t' || lookahead == '\n' || lookahead == '\r' ||
lookahead == ' ')
SKIP(0)
if (lookahead == '-' || ('0' <= lookahead && lookahead <= '9') ||
('A' <= lookahead && lookahead <= 'Z') || lookahead == '_' ||
('a' <= lookahead && lookahead <= 'z'))
ADVANCE(8);
END_STATE();
case 1:
if (lookahead == '!')
ADVANCE(9);
if (lookahead == '(')
ADVANCE(15);
if (lookahead == '\t' || lookahead == '\n' || lookahead == '\r' ||
lookahead == ' ')
SKIP(1)
if (lookahead == '-' || ('0' <= lookahead && lookahead <= '9') ||
('A' <= lookahead && lookahead <= 'Z') || lookahead == '_' ||
('a' <= lookahead && lookahead <= 'z'))
ADVANCE(8);
END_STATE();
case 2:
if (lookahead == '&')
ADVANCE(11);
END_STATE();
case 3:
if (lookahead == '=')
ADVANCE(14);
END_STATE();
case 4:
if (lookahead == '=')
ADVANCE(13);
END_STATE();
case 5:
if (lookahead == '|')
ADVANCE(12);
END_STATE();
case 6:
if (eof)
ADVANCE(7);
if (lookahead == '!')
ADVANCE(3);
if (lookahead == '&')
ADVANCE(2);
if (lookahead == ')')
ADVANCE(16);
if (lookahead == '=')
ADVANCE(4);
if (lookahead == '|')
ADVANCE(5);
if (lookahead == '\t' || lookahead == '\n' || lookahead == '\r' ||
lookahead == ' ')
SKIP(6)
END_STATE();
case 7:
ACCEPT_TOKEN(ts_builtin_sym_end);
END_STATE();
case 8:
ACCEPT_TOKEN(sym_identifier);
if (lookahead == '-' || ('0' <= lookahead && lookahead <= '9') ||
('A' <= lookahead && lookahead <= 'Z') || lookahead == '_' ||
('a' <= lookahead && lookahead <= 'z'))
ADVANCE(8);
END_STATE();
case 9:
ACCEPT_TOKEN(anon_sym_BANG);
END_STATE();
case 10:
ACCEPT_TOKEN(anon_sym_BANG);
if (lookahead == '=')
ADVANCE(14);
END_STATE();
case 11:
ACCEPT_TOKEN(anon_sym_AMP_AMP);
END_STATE();
case 12:
ACCEPT_TOKEN(anon_sym_PIPE_PIPE);
END_STATE();
case 13:
ACCEPT_TOKEN(anon_sym_EQ_EQ);
END_STATE();
case 14:
ACCEPT_TOKEN(anon_sym_BANG_EQ);
END_STATE();
case 15:
ACCEPT_TOKEN(anon_sym_LPAREN);
END_STATE();
case 16:
ACCEPT_TOKEN(anon_sym_RPAREN);
END_STATE();
default:
return false;
}
}
static const TSLexMode ts_lex_modes[STATE_COUNT] = {
[0] = {.lex_state = 0}, [1] = {.lex_state = 1}, [2] = {.lex_state = 1},
[3] = {.lex_state = 1}, [4] = {.lex_state = 1}, [5] = {.lex_state = 1},
[6] = {.lex_state = 6}, [7] = {.lex_state = 0}, [8] = {.lex_state = 0},
[9] = {.lex_state = 0}, [10] = {.lex_state = 0}, [11] = {.lex_state = 0},
[12] = {.lex_state = 0}, [13] = {.lex_state = 0}, [14] = {.lex_state = 0},
[15] = {.lex_state = 0}, [16] = {.lex_state = 0}, [17] = {.lex_state = 0},
};
static const uint16_t ts_parse_table[LARGE_STATE_COUNT][SYMBOL_COUNT] = {
[0] =
{
[ts_builtin_sym_end] = ACTIONS(1),
[sym_identifier] = ACTIONS(1),
[anon_sym_BANG] = ACTIONS(1),
[anon_sym_AMP_AMP] = ACTIONS(1),
[anon_sym_PIPE_PIPE] = ACTIONS(1),
[anon_sym_EQ_EQ] = ACTIONS(1),
[anon_sym_BANG_EQ] = ACTIONS(1),
[anon_sym_LPAREN] = ACTIONS(1),
[anon_sym_RPAREN] = ACTIONS(1),
},
[1] =
{
[sym_source] = STATE(15),
[sym__expression] = STATE(13),
[sym_not] = STATE(13),
[sym_and] = STATE(13),
[sym_or] = STATE(13),
[sym_equal] = STATE(13),
[sym_not_equal] = STATE(13),
[sym_parenthesized] = STATE(13),
[sym_identifier] = ACTIONS(3),
[anon_sym_BANG] = ACTIONS(5),
[anon_sym_LPAREN] = ACTIONS(7),
},
[2] =
{
[sym__expression] = STATE(7),
[sym_not] = STATE(7),
[sym_and] = STATE(7),
[sym_or] = STATE(7),
[sym_equal] = STATE(7),
[sym_not_equal] = STATE(7),
[sym_parenthesized] = STATE(7),
[sym_identifier] = ACTIONS(3),
[anon_sym_BANG] = ACTIONS(5),
[anon_sym_LPAREN] = ACTIONS(7),
},
[3] =
{
[sym__expression] = STATE(14),
[sym_not] = STATE(14),
[sym_and] = STATE(14),
[sym_or] = STATE(14),
[sym_equal] = STATE(14),
[sym_not_equal] = STATE(14),
[sym_parenthesized] = STATE(14),
[sym_identifier] = ACTIONS(3),
[anon_sym_BANG] = ACTIONS(5),
[anon_sym_LPAREN] = ACTIONS(7),
},
[4] =
{
[sym__expression] = STATE(11),
[sym_not] = STATE(11),
[sym_and] = STATE(11),
[sym_or] = STATE(11),
[sym_equal] = STATE(11),
[sym_not_equal] = STATE(11),
[sym_parenthesized] = STATE(11),
[sym_identifier] = ACTIONS(3),
[anon_sym_BANG] = ACTIONS(5),
[anon_sym_LPAREN] = ACTIONS(7),
},
[5] =
{
[sym__expression] = STATE(12),
[sym_not] = STATE(12),
[sym_and] = STATE(12),
[sym_or] = STATE(12),
[sym_equal] = STATE(12),
[sym_not_equal] = STATE(12),
[sym_parenthesized] = STATE(12),
[sym_identifier] = ACTIONS(3),
[anon_sym_BANG] = ACTIONS(5),
[anon_sym_LPAREN] = ACTIONS(7),
},
};
static const uint16_t ts_small_parse_table[] = {
[0] = 3,
ACTIONS(11),
1,
anon_sym_EQ_EQ,
ACTIONS(13),
1,
anon_sym_BANG_EQ,
ACTIONS(9),
4,
ts_builtin_sym_end,
anon_sym_AMP_AMP,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[13] = 1,
ACTIONS(15),
4,
ts_builtin_sym_end,
anon_sym_AMP_AMP,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[20] = 1,
ACTIONS(17),
4,
ts_builtin_sym_end,
anon_sym_AMP_AMP,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[27] = 1,
ACTIONS(19),
4,
ts_builtin_sym_end,
anon_sym_AMP_AMP,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[34] = 1,
ACTIONS(21),
4,
ts_builtin_sym_end,
anon_sym_AMP_AMP,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[41] = 1,
ACTIONS(23),
4,
ts_builtin_sym_end,
anon_sym_AMP_AMP,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[48] = 2,
ACTIONS(27),
1,
anon_sym_AMP_AMP,
ACTIONS(25),
3,
ts_builtin_sym_end,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[57] = 3,
ACTIONS(27),
1,
anon_sym_AMP_AMP,
ACTIONS(29),
1,
ts_builtin_sym_end,
ACTIONS(31),
1,
anon_sym_PIPE_PIPE,
[67] = 3,
ACTIONS(27),
1,
anon_sym_AMP_AMP,
ACTIONS(31),
1,
anon_sym_PIPE_PIPE,
ACTIONS(33),
1,
anon_sym_RPAREN,
[77] = 1,
ACTIONS(35),
1,
ts_builtin_sym_end,
[81] = 1,
ACTIONS(37),
1,
sym_identifier,
[85] = 1,
ACTIONS(39),
1,
sym_identifier,
};
static const uint32_t ts_small_parse_table_map[] = {
[SMALL_STATE(6)] = 0, [SMALL_STATE(7)] = 13, [SMALL_STATE(8)] = 20,
[SMALL_STATE(9)] = 27, [SMALL_STATE(10)] = 34, [SMALL_STATE(11)] = 41,
[SMALL_STATE(12)] = 48, [SMALL_STATE(13)] = 57, [SMALL_STATE(14)] = 67,
[SMALL_STATE(15)] = 77, [SMALL_STATE(16)] = 81, [SMALL_STATE(17)] = 85,
};
static const TSParseActionEntry ts_parse_actions[] = {
[0] = {.entry = {.count = 0, .reusable = false}},
[1] = {.entry = {.count = 1, .reusable = false}},
RECOVER(),
[3] = {.entry = {.count = 1, .reusable = true}},
SHIFT(6),
[5] = {.entry = {.count = 1, .reusable = true}},
SHIFT(2),
[7] = {.entry = {.count = 1, .reusable = true}},
SHIFT(3),
[9] = {.entry = {.count = 1, .reusable = true}},
REDUCE(sym__expression, 1),
[11] = {.entry = {.count = 1, .reusable = true}},
SHIFT(16),
[13] = {.entry = {.count = 1, .reusable = true}},
SHIFT(17),
[15] = {.entry = {.count = 1, .reusable = true}},
REDUCE(sym_not, 2, .production_id = 1),
[17] = {.entry = {.count = 1, .reusable = true}},
REDUCE(sym_equal, 3, .production_id = 2),
[19] = {.entry = {.count = 1, .reusable = true}},
REDUCE(sym_not_equal, 3, .production_id = 2),
[21] = {.entry = {.count = 1, .reusable = true}},
REDUCE(sym_parenthesized, 3, .production_id = 1),
[23] = {.entry = {.count = 1, .reusable = true}},
REDUCE(sym_and, 3, .production_id = 2),
[25] = {.entry = {.count = 1, .reusable = true}},
REDUCE(sym_or, 3, .production_id = 2),
[27] = {.entry = {.count = 1, .reusable = true}},
SHIFT(4),
[29] = {.entry = {.count = 1, .reusable = true}},
REDUCE(sym_source, 1),
[31] = {.entry = {.count = 1, .reusable = true}},
SHIFT(5),
[33] = {.entry = {.count = 1, .reusable = true}},
SHIFT(10),
[35] = {.entry = {.count = 1, .reusable = true}},
ACCEPT_INPUT(),
[37] = {.entry = {.count = 1, .reusable = true}},
SHIFT(8),
[39] = {.entry = {.count = 1, .reusable = true}},
SHIFT(9),
};
#ifdef __cplusplus
extern "C" {
#endif
#ifdef _WIN32
#define extern __declspec(dllexport)
#endif
extern const TSLanguage *tree_sitter_context_predicate(void) {
static const TSLanguage language = {
.version = LANGUAGE_VERSION,
.symbol_count = SYMBOL_COUNT,
.alias_count = ALIAS_COUNT,
.token_count = TOKEN_COUNT,
.external_token_count = EXTERNAL_TOKEN_COUNT,
.state_count = STATE_COUNT,
.large_state_count = LARGE_STATE_COUNT,
.production_id_count = PRODUCTION_ID_COUNT,
.field_count = FIELD_COUNT,
.max_alias_sequence_length = MAX_ALIAS_SEQUENCE_LENGTH,
.parse_table = &ts_parse_table[0][0],
.small_parse_table = ts_small_parse_table,
.small_parse_table_map = ts_small_parse_table_map,
.parse_actions = ts_parse_actions,
.symbol_names = ts_symbol_names,
.field_names = ts_field_names,
.field_map_slices = ts_field_map_slices,
.field_map_entries = ts_field_map_entries,
.symbol_metadata = ts_symbol_metadata,
.public_symbol_map = ts_symbol_map,
.alias_map = ts_non_terminal_alias_map,
.alias_sequences = &ts_alias_sequences[0][0],
.lex_modes = ts_lex_modes,
.lex_fn = ts_lex,
};
return &language;
}
#ifdef __cplusplus
}
#endif

View File

@@ -1,223 +0,0 @@
#ifndef TREE_SITTER_PARSER_H_
#define TREE_SITTER_PARSER_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#define ts_builtin_sym_error ((TSSymbol)-1)
#define ts_builtin_sym_end 0
#define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024
typedef uint16_t TSStateId;
#ifndef TREE_SITTER_API_H_
typedef uint16_t TSSymbol;
typedef uint16_t TSFieldId;
typedef struct TSLanguage TSLanguage;
#endif
typedef struct {
TSFieldId field_id;
uint8_t child_index;
bool inherited;
} TSFieldMapEntry;
typedef struct {
uint16_t index;
uint16_t length;
} TSFieldMapSlice;
typedef struct {
bool visible;
bool named;
bool supertype;
} TSSymbolMetadata;
typedef struct TSLexer TSLexer;
struct TSLexer {
int32_t lookahead;
TSSymbol result_symbol;
void (*advance)(TSLexer *, bool);
void (*mark_end)(TSLexer *);
uint32_t (*get_column)(TSLexer *);
bool (*is_at_included_range_start)(const TSLexer *);
bool (*eof)(const TSLexer *);
};
typedef enum {
TSParseActionTypeShift,
TSParseActionTypeReduce,
TSParseActionTypeAccept,
TSParseActionTypeRecover,
} TSParseActionType;
typedef union {
struct {
uint8_t type;
TSStateId state;
bool extra;
bool repetition;
} shift;
struct {
uint8_t type;
uint8_t child_count;
TSSymbol symbol;
int16_t dynamic_precedence;
uint16_t production_id;
} reduce;
uint8_t type;
} TSParseAction;
typedef struct {
uint16_t lex_state;
uint16_t external_lex_state;
} TSLexMode;
typedef union {
TSParseAction action;
struct {
uint8_t count;
bool reusable;
} entry;
} TSParseActionEntry;
struct TSLanguage {
uint32_t version;
uint32_t symbol_count;
uint32_t alias_count;
uint32_t token_count;
uint32_t external_token_count;
uint32_t state_count;
uint32_t large_state_count;
uint32_t production_id_count;
uint32_t field_count;
uint16_t max_alias_sequence_length;
const uint16_t *parse_table;
const uint16_t *small_parse_table;
const uint32_t *small_parse_table_map;
const TSParseActionEntry *parse_actions;
const char * const *symbol_names;
const char * const *field_names;
const TSFieldMapSlice *field_map_slices;
const TSFieldMapEntry *field_map_entries;
const TSSymbolMetadata *symbol_metadata;
const TSSymbol *public_symbol_map;
const uint16_t *alias_map;
const TSSymbol *alias_sequences;
const TSLexMode *lex_modes;
bool (*lex_fn)(TSLexer *, TSStateId);
bool (*keyword_lex_fn)(TSLexer *, TSStateId);
TSSymbol keyword_capture_token;
struct {
const bool *states;
const TSSymbol *symbol_map;
void *(*create)(void);
void (*destroy)(void *);
bool (*scan)(void *, TSLexer *, const bool *symbol_whitelist);
unsigned (*serialize)(void *, char *);
void (*deserialize)(void *, const char *, unsigned);
} external_scanner;
};
/*
* Lexer Macros
*/
#define START_LEXER() \
bool result = false; \
bool skip = false; \
bool eof = false; \
int32_t lookahead; \
goto start; \
next_state: \
lexer->advance(lexer, skip); \
start: \
skip = false; \
lookahead = lexer->lookahead;
#define ADVANCE(state_value) \
{ \
state = state_value; \
goto next_state; \
}
#define SKIP(state_value) \
{ \
skip = true; \
state = state_value; \
goto next_state; \
}
#define ACCEPT_TOKEN(symbol_value) \
result = true; \
lexer->result_symbol = symbol_value; \
lexer->mark_end(lexer);
#define END_STATE() return result;
/*
* Parse Table Macros
*/
#define SMALL_STATE(id) id - LARGE_STATE_COUNT
#define STATE(id) id
#define ACTIONS(id) id
#define SHIFT(state_value) \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.state = state_value \
} \
}}
#define SHIFT_REPEAT(state_value) \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.state = state_value, \
.repetition = true \
} \
}}
#define SHIFT_EXTRA() \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.extra = true \
} \
}}
#define REDUCE(symbol_val, child_count_val, ...) \
{{ \
.reduce = { \
.type = TSParseActionTypeReduce, \
.symbol = symbol_val, \
.child_count = child_count_val, \
__VA_ARGS__ \
}, \
}}
#define RECOVER() \
{{ \
.type = TSParseActionTypeRecover \
}}
#define ACCEPT_INPUT() \
{{ \
.type = TSParseActionTypeAccept \
}}
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_PARSER_H_

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,44 @@
use crate::MutableAppContext;
use collections::{BTreeMap, HashMap, HashSet};
use parking_lot::Mutex;
use std::sync::Arc;
use std::{hash::Hash, sync::Weak};
use parking_lot::Mutex;
use collections::{btree_map, BTreeMap, HashMap};
use crate::MutableAppContext;
pub type Mapping<K, F> = Mutex<HashMap<K, BTreeMap<usize, Option<F>>>>;
pub struct CallbackCollection<K: Hash + Eq, F> {
internal: Arc<Mapping<K, F>>,
pub struct CallbackCollection<K: Clone + Hash + Eq, F> {
internal: Arc<Mutex<Mapping<K, F>>>,
}
impl<K: Hash + Eq, F> Clone for CallbackCollection<K, F> {
pub struct Subscription<K: Clone + Hash + Eq, F> {
key: K,
id: usize,
mapping: Option<Weak<Mutex<Mapping<K, F>>>>,
}
struct Mapping<K, F> {
callbacks: HashMap<K, BTreeMap<usize, F>>,
dropped_subscriptions: HashMap<K, HashSet<usize>>,
}
impl<K: Hash + Eq, F> Mapping<K, F> {
fn clear_dropped_state(&mut self, key: &K, subscription_id: usize) -> bool {
if let Some(subscriptions) = self.dropped_subscriptions.get_mut(&key) {
subscriptions.remove(&subscription_id)
} else {
false
}
}
}
impl<K, F> Default for Mapping<K, F> {
fn default() -> Self {
Self {
callbacks: Default::default(),
dropped_subscriptions: Default::default(),
}
}
}
impl<K: Clone + Hash + Eq, F> Clone for CallbackCollection<K, F> {
fn clone(&self) -> Self {
Self {
internal: self.internal.clone(),
@@ -21,7 +46,7 @@ impl<K: Hash + Eq, F> Clone for CallbackCollection<K, F> {
}
}
impl<K: Hash + Eq + Copy, F> Default for CallbackCollection<K, F> {
impl<K: Clone + Hash + Eq + Copy, F> Default for CallbackCollection<K, F> {
fn default() -> Self {
CallbackCollection {
internal: Arc::new(Mutex::new(Default::default())),
@@ -29,78 +54,114 @@ impl<K: Hash + Eq + Copy, F> Default for CallbackCollection<K, F> {
}
}
impl<K: Hash + Eq + Copy, F> CallbackCollection<K, F> {
pub fn downgrade(&self) -> Weak<Mapping<K, F>> {
Arc::downgrade(&self.internal)
}
impl<K: Clone + Hash + Eq + Copy, F> CallbackCollection<K, F> {
#[cfg(test)]
pub fn is_empty(&self) -> bool {
self.internal.lock().is_empty()
self.internal.lock().callbacks.is_empty()
}
pub fn add_callback(&mut self, id: K, subscription_id: usize, callback: F) {
self.internal
.lock()
.entry(id)
.or_default()
.insert(subscription_id, Some(callback));
}
pub fn remove(&mut self, id: K) {
self.internal.lock().remove(&id);
}
pub fn add_or_remove_callback(&mut self, id: K, subscription_id: usize, callback: F) {
match self
.internal
.lock()
.entry(id)
.or_default()
.entry(subscription_id)
{
btree_map::Entry::Vacant(entry) => {
entry.insert(Some(callback));
}
btree_map::Entry::Occupied(entry) => {
// TODO: This seems like it should never be called because no code
// should ever attempt to remove an existing callback
debug_assert!(entry.get().is_none());
entry.remove();
}
pub fn subscribe(&mut self, key: K, subscription_id: usize) -> Subscription<K, F> {
Subscription {
key,
id: subscription_id,
mapping: Some(Arc::downgrade(&self.internal)),
}
}
pub fn emit_and_cleanup<C: FnMut(&mut F, &mut MutableAppContext) -> bool>(
pub fn add_callback(&mut self, key: K, subscription_id: usize, callback: F) {
let mut this = self.internal.lock();
// If this callback's subscription was dropped before the callback was
// added, then just drop the callback.
if this.clear_dropped_state(&key, subscription_id) {
return;
}
this.callbacks
.entry(key)
.or_default()
.insert(subscription_id, callback);
}
pub fn remove(&mut self, key: K) {
// Drop these callbacks after releasing the lock, in case one of them
// owns a subscription to this callback collection.
let mut this = self.internal.lock();
let callbacks = this.callbacks.remove(&key);
this.dropped_subscriptions.remove(&key);
drop(this);
drop(callbacks);
}
pub fn emit<C: FnMut(&mut F, &mut MutableAppContext) -> bool>(
&mut self,
id: K,
key: K,
cx: &mut MutableAppContext,
mut call_callback: C,
) {
let callbacks = self.internal.lock().remove(&id);
let callbacks = self.internal.lock().callbacks.remove(&key);
if let Some(callbacks) = callbacks {
for (subscription_id, callback) in callbacks {
if let Some(mut callback) = callback {
let alive = call_callback(&mut callback, cx);
if alive {
match self
.internal
.lock()
.entry(id)
.or_default()
.entry(subscription_id)
{
btree_map::Entry::Vacant(entry) => {
entry.insert(Some(callback));
}
btree_map::Entry::Occupied(entry) => {
entry.remove();
}
}
}
for (subscription_id, mut callback) in callbacks {
// If this callback's subscription was dropped while invoking an
// earlier callback, then just drop the callback.
let mut this = self.internal.lock();
if this.clear_dropped_state(&key, subscription_id) {
continue;
}
drop(this);
let alive = call_callback(&mut callback, cx);
// If this callback's subscription was dropped while invoking the callback
// itself, or if the callback returns false, then just drop the callback.
let mut this = self.internal.lock();
if this.clear_dropped_state(&key, subscription_id) || !alive {
continue;
}
this.callbacks
.entry(key)
.or_default()
.insert(subscription_id, callback);
}
}
}
}
impl<K: Clone + Hash + Eq, F> Subscription<K, F> {
pub fn id(&self) -> usize {
self.id
}
pub fn detach(&mut self) {
self.mapping.take();
}
}
impl<K: Clone + Hash + Eq, F> Drop for Subscription<K, F> {
fn drop(&mut self) {
if let Some(mapping) = self.mapping.as_ref().and_then(|mapping| mapping.upgrade()) {
let mut mapping = mapping.lock();
// If the callback is present in the mapping, then just remove it.
if let Some(callbacks) = mapping.callbacks.get_mut(&self.key) {
let callback = callbacks.remove(&self.id);
if callback.is_some() {
drop(mapping);
drop(callback);
return;
}
}
// If this subscription's callback is not present, then either it has been
// temporarily removed during emit, or it has not yet been added. Record
// that this subscription has been dropped so that the callback can be
// removed later.
mapping
.dropped_subscriptions
.entry(self.key.clone())
.or_default()
.insert(self.id);
}
}
}

View File

@@ -17,11 +17,11 @@ use parking_lot::{Mutex, RwLock};
use smol::stream::StreamExt;
use crate::{
executor, geometry::vector::Vector2F, keymap::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,
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,
};
use collections::BTreeMap;

View File

@@ -74,11 +74,18 @@ struct DeterministicState {
pending_timers: Vec<(usize, std::time::Instant, postage::barrier::Sender)>,
waiting_backtrace: Option<backtrace::Backtrace>,
next_runnable_id: usize,
poll_history: Vec<usize>,
poll_history: Vec<ExecutorEvent>,
previous_poll_history: Option<Vec<ExecutorEvent>>,
enable_runnable_backtraces: bool,
runnable_backtraces: collections::HashMap<usize, backtrace::Backtrace>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ExecutorEvent {
PollRunnable { id: usize },
EnqueuRunnable { id: usize },
}
#[cfg(any(test, feature = "test-support"))]
struct ForegroundRunnable {
id: usize,
@@ -130,6 +137,7 @@ impl Deterministic {
waiting_backtrace: None,
next_runnable_id: 0,
poll_history: Default::default(),
previous_poll_history: Default::default(),
enable_runnable_backtraces: false,
runnable_backtraces: Default::default(),
})),
@@ -137,10 +145,14 @@ impl Deterministic {
})
}
pub fn runnable_history(&self) -> Vec<usize> {
pub fn execution_history(&self) -> Vec<ExecutorEvent> {
self.state.lock().poll_history.clone()
}
pub fn set_previous_execution_history(&self, history: Option<Vec<ExecutorEvent>>) {
self.state.lock().previous_poll_history = history;
}
pub fn enable_runnable_backtrace(&self) {
self.state.lock().enable_runnable_backtraces = true;
}
@@ -185,6 +197,7 @@ impl Deterministic {
let unparker = self.parker.lock().unparker();
let (runnable, task) = async_task::spawn_local(future, move |runnable| {
let mut state = state.lock();
state.push_to_history(ExecutorEvent::EnqueuRunnable { id });
state
.scheduled_from_foreground
.entry(cx_id)
@@ -212,6 +225,9 @@ impl Deterministic {
let unparker = self.parker.lock().unparker();
let (runnable, task) = async_task::spawn(future, move |runnable| {
let mut state = state.lock();
state
.poll_history
.push(ExecutorEvent::EnqueuRunnable { id });
state
.scheduled_from_background
.push(BackgroundRunnable { id, runnable });
@@ -314,7 +330,9 @@ impl Deterministic {
let background_len = state.scheduled_from_background.len();
let ix = state.rng.gen_range(0..background_len);
let background_runnable = state.scheduled_from_background.remove(ix);
state.poll_history.push(background_runnable.id);
state.push_to_history(ExecutorEvent::PollRunnable {
id: background_runnable.id,
});
drop(state);
background_runnable.runnable.run();
} else if !state.scheduled_from_foreground.is_empty() {
@@ -332,7 +350,9 @@ impl Deterministic {
if scheduled_from_cx.is_empty() {
state.scheduled_from_foreground.remove(&cx_id_to_run);
}
state.poll_history.push(foreground_runnable.id);
state.push_to_history(ExecutorEvent::PollRunnable {
id: foreground_runnable.id,
});
drop(state);
@@ -366,7 +386,9 @@ impl Deterministic {
let ix = state.rng.gen_range(0..=runnable_count);
if ix < state.scheduled_from_background.len() {
let background_runnable = state.scheduled_from_background.remove(ix);
state.poll_history.push(background_runnable.id);
state.push_to_history(ExecutorEvent::PollRunnable {
id: background_runnable.id,
});
drop(state);
background_runnable.runnable.run();
} else {
@@ -465,6 +487,25 @@ impl Deterministic {
}
}
}
pub fn record_backtrace(&self) {
let mut state = self.state.lock();
if state.enable_runnable_backtraces {
let current_id = state
.poll_history
.iter()
.rev()
.find_map(|event| match event {
ExecutorEvent::PollRunnable { id } => Some(*id),
_ => None,
});
if let Some(id) = current_id {
state
.runnable_backtraces
.insert(id, backtrace::Backtrace::new_unresolved());
}
}
}
}
impl Drop for Timer {
@@ -506,6 +547,40 @@ impl Future for Timer {
#[cfg(any(test, feature = "test-support"))]
impl DeterministicState {
fn push_to_history(&mut self, event: ExecutorEvent) {
use std::fmt::Write as _;
self.poll_history.push(event);
if let Some(prev_history) = &self.previous_poll_history {
let ix = self.poll_history.len() - 1;
let prev_event = prev_history[ix];
if event != prev_event {
let mut message = String::new();
writeln!(
&mut message,
"current runnable backtrace:\n{:?}",
self.runnable_backtraces.get_mut(&event.id()).map(|trace| {
trace.resolve();
util::CwdBacktrace(trace)
})
)
.unwrap();
writeln!(
&mut message,
"previous runnable backtrace:\n{:?}",
self.runnable_backtraces
.get_mut(&prev_event.id())
.map(|trace| {
trace.resolve();
util::CwdBacktrace(trace)
})
)
.unwrap();
panic!("detected non-determinism after {ix}. {message}");
}
}
}
fn will_park(&mut self) {
if self.forbid_parking {
let mut backtrace_message = String::new();
@@ -526,6 +601,16 @@ impl DeterministicState {
}
}
#[cfg(any(test, feature = "test-support"))]
impl ExecutorEvent {
pub fn id(&self) -> usize {
match self {
ExecutorEvent::PollRunnable { id } => *id,
ExecutorEvent::EnqueuRunnable { id } => *id,
}
}
}
impl Foreground {
pub fn platform(dispatcher: Arc<dyn platform::Dispatcher>) -> Result<Self> {
if dispatcher.is_main_thread() {
@@ -755,6 +840,16 @@ impl Background {
}
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn record_backtrace(&self) {
match self {
Self::Deterministic { executor, .. } => executor.record_backtrace(),
_ => {
panic!("this method can only be called on a deterministic executor")
}
}
}
}
impl Default for Background {

View File

@@ -25,7 +25,7 @@ pub mod executor;
pub use executor::Task;
pub mod color;
pub mod json;
pub mod keymap;
pub mod keymap_matcher;
pub mod platform;
pub use gpui_macros::test;
pub use platform::*;

View File

@@ -1,757 +0,0 @@
use crate::Action;
use anyhow::{anyhow, Result};
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
collections::{HashMap, HashSet},
fmt::{Debug, Write},
};
use tree_sitter::{Language, Node, Parser};
extern "C" {
fn tree_sitter_context_predicate() -> Language;
}
pub struct Matcher {
pending_views: HashMap<usize, Context>,
pending_keystrokes: Vec<Keystroke>,
keymap: Keymap,
}
#[derive(Default)]
pub struct Keymap {
bindings: Vec<Binding>,
binding_indices_by_action_type: HashMap<TypeId, SmallVec<[usize; 3]>>,
}
pub struct Binding {
keystrokes: SmallVec<[Keystroke; 2]>,
action: Box<dyn Action>,
context_predicate: Option<ContextPredicate>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Keystroke {
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
pub cmd: bool,
pub function: bool,
pub key: String,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Context {
pub set: HashSet<String>,
pub map: HashMap<String, String>,
}
#[derive(Debug, Eq, PartialEq)]
enum ContextPredicate {
Identifier(String),
Equal(String, String),
NotEqual(String, String),
Not(Box<ContextPredicate>),
And(Box<ContextPredicate>, Box<ContextPredicate>),
Or(Box<ContextPredicate>, Box<ContextPredicate>),
}
trait ActionArg {
fn boxed_clone(&self) -> Box<dyn Any>;
}
impl<T> ActionArg for T
where
T: 'static + Any + Clone,
{
fn boxed_clone(&self) -> Box<dyn Any> {
Box::new(self.clone())
}
}
pub enum MatchResult {
None,
Pending,
Matches(Vec<(usize, Box<dyn Action>)>),
}
impl Debug for MatchResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MatchResult::None => f.debug_struct("MatchResult::None").finish(),
MatchResult::Pending => f.debug_struct("MatchResult::Pending").finish(),
MatchResult::Matches(matches) => f
.debug_list()
.entries(
matches
.iter()
.map(|(view_id, action)| format!("{view_id}, {}", action.name())),
)
.finish(),
}
}
}
impl PartialEq for MatchResult {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(MatchResult::None, MatchResult::None) => true,
(MatchResult::Pending, MatchResult::Pending) => true,
(MatchResult::Matches(matches), MatchResult::Matches(other_matches)) => {
matches.len() == other_matches.len()
&& matches.iter().zip(other_matches.iter()).all(
|((view_id, action), (other_view_id, other_action))| {
view_id == other_view_id && action.eq(other_action.as_ref())
},
)
}
_ => false,
}
}
}
impl Eq for MatchResult {}
impl Clone for MatchResult {
fn clone(&self) -> Self {
match self {
MatchResult::None => MatchResult::None,
MatchResult::Pending => MatchResult::Pending,
MatchResult::Matches(matches) => MatchResult::Matches(
matches
.iter()
.map(|(view_id, action)| (*view_id, Action::boxed_clone(action.as_ref())))
.collect(),
),
}
}
}
impl Matcher {
pub fn new(keymap: Keymap) -> Self {
Self {
pending_views: HashMap::new(),
pending_keystrokes: Vec::new(),
keymap,
}
}
pub fn set_keymap(&mut self, keymap: Keymap) {
self.clear_pending();
self.keymap = keymap;
}
pub fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
self.clear_pending();
self.keymap.add_bindings(bindings);
}
pub fn clear_bindings(&mut self) {
self.clear_pending();
self.keymap.clear();
}
pub fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator<Item = &Binding> {
self.keymap.bindings_for_action_type(action_type)
}
pub fn clear_pending(&mut self) {
self.pending_keystrokes.clear();
self.pending_views.clear();
}
pub fn has_pending_keystrokes(&self) -> bool {
!self.pending_keystrokes.is_empty()
}
pub fn push_keystroke(
&mut self,
keystroke: Keystroke,
dispatch_path: Vec<(usize, Context)>,
) -> MatchResult {
let mut any_pending = false;
let mut matched_bindings = Vec::new();
let first_keystroke = self.pending_keystrokes.is_empty();
self.pending_keystrokes.push(keystroke);
for (view_id, context) in dispatch_path {
// Don't require pending view entry if there are no pending keystrokes
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 previous_view_context != context {
continue;
}
}
// Find the bindings which map the pending keystrokes and current context
for binding in self.keymap.bindings.iter().rev() {
if binding.keystrokes.starts_with(&self.pending_keystrokes)
&& binding
.context_predicate
.as_ref()
.map(|c| c.eval(&context))
.unwrap_or(true)
{
// If the binding is completed, push it onto the matches list
if binding.keystrokes.len() == self.pending_keystrokes.len() {
matched_bindings.push((view_id, binding.action.boxed_clone()));
} else {
// Otherwise, the binding is still pending
self.pending_views.insert(view_id, context.clone());
any_pending = true;
}
}
}
}
if !any_pending {
self.clear_pending();
}
if !matched_bindings.is_empty() {
MatchResult::Matches(matched_bindings)
} else if any_pending {
MatchResult::Pending
} else {
MatchResult::None
}
}
pub fn keystrokes_for_action(
&self,
action: &dyn Action,
cx: &Context,
) -> Option<SmallVec<[Keystroke; 2]>> {
for binding in self.keymap.bindings.iter().rev() {
if binding.action.eq(action)
&& binding
.context_predicate
.as_ref()
.map_or(true, |predicate| predicate.eval(cx))
{
return Some(binding.keystrokes.clone());
}
}
None
}
}
impl Default for Matcher {
fn default() -> Self {
Self::new(Keymap::default())
}
}
impl Keymap {
pub fn new(bindings: Vec<Binding>) -> Self {
let mut binding_indices_by_action_type = HashMap::new();
for (ix, binding) in bindings.iter().enumerate() {
binding_indices_by_action_type
.entry(binding.action.as_any().type_id())
.or_insert_with(SmallVec::new)
.push(ix);
}
Self {
binding_indices_by_action_type,
bindings,
}
}
fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator<Item = &'_ Binding> {
self.binding_indices_by_action_type
.get(&action_type)
.map(SmallVec::as_slice)
.unwrap_or(&[])
.iter()
.map(|ix| &self.bindings[*ix])
}
fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
for binding in bindings {
self.binding_indices_by_action_type
.entry(binding.action.as_any().type_id())
.or_default()
.push(self.bindings.len());
self.bindings.push(binding);
}
}
fn clear(&mut self) {
self.bindings.clear();
self.binding_indices_by_action_type.clear();
}
}
impl Binding {
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
Self::load(keystrokes, Box::new(action), context).unwrap()
}
pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
let context = if let Some(context) = context {
Some(ContextPredicate::parse(context)?)
} else {
None
};
let keystrokes = keystrokes
.split_whitespace()
.map(Keystroke::parse)
.collect::<Result<_>>()?;
Ok(Self {
keystrokes,
action,
context_predicate: context,
})
}
pub fn keystrokes(&self) -> &[Keystroke] {
&self.keystrokes
}
pub fn action(&self) -> &dyn Action {
self.action.as_ref()
}
}
impl Keystroke {
pub fn parse(source: &str) -> anyhow::Result<Self> {
let mut ctrl = false;
let mut alt = false;
let mut shift = false;
let mut cmd = false;
let mut function = false;
let mut key = None;
let mut components = source.split('-').peekable();
while let Some(component) = components.next() {
match component {
"ctrl" => ctrl = true,
"alt" => alt = true,
"shift" => shift = true,
"cmd" => cmd = true,
"fn" => function = true,
_ => {
if let Some(component) = components.peek() {
if component.is_empty() && source.ends_with('-') {
key = Some(String::from("-"));
break;
} else {
return Err(anyhow!("Invalid keystroke `{}`", source));
}
} else {
key = Some(String::from(component));
}
}
}
}
let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
Ok(Keystroke {
ctrl,
alt,
shift,
cmd,
function,
key,
})
}
pub fn modified(&self) -> bool {
self.ctrl || self.alt || self.shift || self.cmd
}
}
impl std::fmt::Display for Keystroke {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.ctrl {
f.write_char('^')?;
}
if self.alt {
f.write_char('⎇')?;
}
if self.cmd {
f.write_char('⌘')?;
}
if self.shift {
f.write_char('⇧')?;
}
let key = match self.key.as_str() {
"backspace" => '⌫',
"up" => '↑',
"down" => '↓',
"left" => '←',
"right" => '→',
"tab" => '⇥',
"escape" => '⎋',
key => {
if key.len() == 1 {
key.chars().next().unwrap().to_ascii_uppercase()
} else {
return f.write_str(key);
}
}
};
f.write_char(key)
}
}
impl Context {
pub fn extend(&mut self, other: &Context) {
for v in &other.set {
self.set.insert(v.clone());
}
for (k, v) in &other.map {
self.map.insert(k.clone(), v.clone());
}
}
}
impl ContextPredicate {
fn parse(source: &str) -> anyhow::Result<Self> {
let mut parser = Parser::new();
let language = unsafe { tree_sitter_context_predicate() };
parser.set_language(language).unwrap();
let source = source.as_bytes();
let tree = parser.parse(source, None).unwrap();
Self::from_node(tree.root_node(), source)
}
fn from_node(node: Node, source: &[u8]) -> anyhow::Result<Self> {
let parse_error = "error parsing context predicate";
let kind = node.kind();
match kind {
"source" => Self::from_node(node.child(0).ok_or_else(|| anyhow!(parse_error))?, source),
"identifier" => Ok(Self::Identifier(node.utf8_text(source)?.into())),
"not" => {
let child = Self::from_node(
node.child_by_field_name("expression")
.ok_or_else(|| anyhow!(parse_error))?,
source,
)?;
Ok(Self::Not(Box::new(child)))
}
"and" | "or" => {
let left = Box::new(Self::from_node(
node.child_by_field_name("left")
.ok_or_else(|| anyhow!(parse_error))?,
source,
)?);
let right = Box::new(Self::from_node(
node.child_by_field_name("right")
.ok_or_else(|| anyhow!(parse_error))?,
source,
)?);
if kind == "and" {
Ok(Self::And(left, right))
} else {
Ok(Self::Or(left, right))
}
}
"equal" | "not_equal" => {
let left = node
.child_by_field_name("left")
.ok_or_else(|| anyhow!(parse_error))?
.utf8_text(source)?
.into();
let right = node
.child_by_field_name("right")
.ok_or_else(|| anyhow!(parse_error))?
.utf8_text(source)?
.into();
if kind == "equal" {
Ok(Self::Equal(left, right))
} else {
Ok(Self::NotEqual(left, right))
}
}
"parenthesized" => Self::from_node(
node.child_by_field_name("expression")
.ok_or_else(|| anyhow!(parse_error))?,
source,
),
_ => Err(anyhow!(parse_error)),
}
}
fn eval(&self, cx: &Context) -> bool {
match self {
Self::Identifier(name) => cx.set.contains(name.as_str()),
Self::Equal(left, right) => cx
.map
.get(left)
.map(|value| value == right)
.unwrap_or(false),
Self::NotEqual(left, right) => {
cx.map.get(left).map(|value| value != right).unwrap_or(true)
}
Self::Not(pred) => !pred.eval(cx),
Self::And(left, right) => left.eval(cx) && right.eval(cx),
Self::Or(left, right) => left.eval(cx) || right.eval(cx),
}
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use serde::Deserialize;
use crate::{actions, impl_actions};
use super::*;
#[test]
fn test_push_keystroke() -> Result<()> {
actions!(test, [B, AB, C, D, DA]);
let mut ctx1 = Context::default();
ctx1.set.insert("1".into());
let mut ctx2 = Context::default();
ctx2.set.insert("2".into());
let dispatch_path = vec![(2, ctx2), (1, ctx1)];
let keymap = Keymap::new(vec![
Binding::new("a b", AB, Some("1")),
Binding::new("b", B, Some("2")),
Binding::new("c", C, Some("2")),
Binding::new("d", D, Some("1")),
Binding::new("d", D, Some("2")),
Binding::new("d a", DA, Some("2")),
]);
let mut matcher = Matcher::new(keymap);
// Binding with pending prefix always takes precedence
assert_eq!(
matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
MatchResult::Pending,
);
// B alone doesn't match because a was pending, so AB is returned instead
assert_eq!(
matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
MatchResult::Matches(vec![(1, Box::new(AB))]),
);
assert!(!matcher.has_pending_keystrokes());
// Without an a prefix, B is dispatched like expected
assert_eq!(
matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
MatchResult::Matches(vec![(2, Box::new(B))]),
);
assert!(!matcher.has_pending_keystrokes());
// If a is prefixed, C will not be dispatched because there
// was a pending binding for it
assert_eq!(
matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
MatchResult::Pending,
);
assert_eq!(
matcher.push_keystroke(Keystroke::parse("c")?, dispatch_path.clone()),
MatchResult::None,
);
assert!(!matcher.has_pending_keystrokes());
// If a single keystroke matches multiple bindings in the tree
// all of them are returned so that we can fallback if the action
// handler decides to propagate the action
assert_eq!(
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!(
matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
MatchResult::Matches(vec![(2, Box::new(DA))]),
);
assert!(!matcher.has_pending_keystrokes());
Ok(())
}
#[test]
fn test_keystroke_parsing() -> Result<()> {
assert_eq!(
Keystroke::parse("ctrl-p")?,
Keystroke {
key: "p".into(),
ctrl: true,
alt: false,
shift: false,
cmd: false,
function: false,
}
);
assert_eq!(
Keystroke::parse("alt-shift-down")?,
Keystroke {
key: "down".into(),
ctrl: false,
alt: true,
shift: true,
cmd: false,
function: false,
}
);
assert_eq!(
Keystroke::parse("shift-cmd--")?,
Keystroke {
key: "-".into(),
ctrl: false,
alt: false,
shift: true,
cmd: true,
function: false,
}
);
Ok(())
}
#[test]
fn test_context_predicate_parsing() -> Result<()> {
use ContextPredicate::*;
assert_eq!(
ContextPredicate::parse("a && (b == c || d != e)")?,
And(
Box::new(Identifier("a".into())),
Box::new(Or(
Box::new(Equal("b".into(), "c".into())),
Box::new(NotEqual("d".into(), "e".into())),
))
)
);
assert_eq!(
ContextPredicate::parse("!a")?,
Not(Box::new(Identifier("a".into())),)
);
Ok(())
}
#[test]
fn test_context_predicate_eval() -> Result<()> {
let predicate = ContextPredicate::parse("a && b || c == d")?;
let mut context = Context::default();
context.set.insert("a".into());
assert!(!predicate.eval(&context));
context.set.insert("b".into());
assert!(predicate.eval(&context));
context.set.remove("b");
context.map.insert("c".into(), "x".into());
assert!(!predicate.eval(&context));
context.map.insert("c".into(), "d".into());
assert!(predicate.eval(&context));
let predicate = ContextPredicate::parse("!a")?;
assert!(predicate.eval(&Context::default()));
Ok(())
}
#[test]
fn test_matcher() -> Result<()> {
#[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
pub struct A(pub String);
impl_actions!(test, [A]);
actions!(test, [B, Ab]);
#[derive(Clone, Debug, Eq, PartialEq)]
struct ActionArg {
a: &'static str,
}
let keymap = Keymap::new(vec![
Binding::new("a", A("x".to_string()), Some("a")),
Binding::new("b", B, Some("a")),
Binding::new("a b", Ab, Some("a || b")),
]);
let mut ctx_a = Context::default();
ctx_a.set.insert("a".into());
let mut ctx_b = Context::default();
ctx_b.set.insert("b".into());
let mut matcher = Matcher::new(keymap);
// Basic match
assert_eq!(
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, ctx_a.clone())]),
MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))])
);
matcher.clear_pending();
// Multi-keystroke match
assert_eq!(
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, ctx_b.clone())]),
MatchResult::Pending
);
assert_eq!(
matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, ctx_b.clone())]),
MatchResult::Matches(vec![(1, Box::new(Ab))])
);
matcher.clear_pending();
// Failed matches don't interfere with matching subsequent keys
assert_eq!(
matcher.push_keystroke(Keystroke::parse("x")?, vec![(1, ctx_a.clone())]),
MatchResult::None
);
assert_eq!(
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, ctx_a.clone())]),
MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))])
);
matcher.clear_pending();
// Pending keystrokes are cleared when the context changes
assert_eq!(
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, ctx_b.clone())]),
MatchResult::Pending
);
assert_eq!(
matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, ctx_a.clone())]),
MatchResult::None
);
matcher.clear_pending();
let mut ctx_c = Context::default();
ctx_c.set.insert("c".into());
// Pending keystrokes are maintained per-view
assert_eq!(
matcher.push_keystroke(
Keystroke::parse("a")?,
vec![(1, ctx_b.clone()), (2, ctx_c.clone())]
),
MatchResult::Pending
);
assert_eq!(
matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, ctx_b.clone())]),
MatchResult::Matches(vec![(1, Box::new(Ab))])
);
Ok(())
}
}

View File

@@ -0,0 +1,509 @@
mod binding;
mod keymap;
mod keymap_context;
mod keystroke;
use std::{any::TypeId, fmt::Debug};
use collections::HashMap;
use serde::Deserialize;
use smallvec::SmallVec;
use crate::{impl_actions, 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>,
pending_keystrokes: Vec<Keystroke>,
keymap: Keymap,
}
impl KeymapMatcher {
pub fn new(keymap: Keymap) -> Self {
Self {
contexts: Vec::new(),
pending_views: Default::default(),
pending_keystrokes: Vec::new(),
keymap,
}
}
pub fn set_keymap(&mut self, keymap: Keymap) {
self.clear_pending();
self.keymap = keymap;
}
pub fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
self.clear_pending();
self.keymap.add_bindings(bindings);
}
pub fn clear_bindings(&mut self) {
self.clear_pending();
self.keymap.clear();
}
pub fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator<Item = &Binding> {
self.keymap.bindings_for_action_type(action_type)
}
pub fn clear_pending(&mut self) {
self.pending_keystrokes.clear();
self.pending_views.clear();
}
pub fn has_pending_keystrokes(&self) -> bool {
!self.pending_keystrokes.is_empty()
}
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();
let first_keystroke = self.pending_keystrokes.is_empty();
self.pending_keystrokes.push(keystroke.clone());
self.contexts.clear();
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() {
// Don't require pending view entry if there are no pending keystrokes
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 previous_view_context != self.contexts[i] {
continue;
}
}
// Find the bindings which map the pending keystrokes and current context
for binding in self.keymap.bindings().iter().rev() {
match binding.match_keys_and_context(&self.pending_keystrokes, &self.contexts[i..])
{
BindingMatchResult::Complete(mut action) => {
// Swap in keystroke for special KeyPressed action
if action.name() == "KeyPressed" && action.namespace() == "gpui" {
action = Box::new(KeyPressed {
keystroke: keystroke.clone(),
});
}
matched_bindings.push((view_id, action))
}
BindingMatchResult::Partial => {
self.pending_views.insert(view_id, self.contexts[i].clone());
any_pending = true;
}
_ => {}
}
}
}
if !any_pending {
self.clear_pending();
}
if !matched_bindings.is_empty() {
MatchResult::Matches(matched_bindings)
} else if any_pending {
MatchResult::Pending
} else {
MatchResult::None
}
}
pub fn keystrokes_for_action(
&self,
action: &dyn Action,
contexts: &[KeymapContext],
) -> Option<SmallVec<[Keystroke; 2]>> {
self.keymap
.bindings()
.iter()
.rev()
.find_map(|binding| binding.keystrokes_for_action(action, contexts))
}
}
impl Default for KeymapMatcher {
fn default() -> Self {
Self::new(Keymap::default())
}
}
pub enum MatchResult {
None,
Pending,
Matches(Vec<(usize, Box<dyn Action>)>),
}
impl Debug for MatchResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MatchResult::None => f.debug_struct("MatchResult::None").finish(),
MatchResult::Pending => f.debug_struct("MatchResult::Pending").finish(),
MatchResult::Matches(matches) => f
.debug_list()
.entries(
matches
.iter()
.map(|(view_id, action)| format!("{view_id}, {}", action.name())),
)
.finish(),
}
}
}
impl PartialEq for MatchResult {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(MatchResult::None, MatchResult::None) => true,
(MatchResult::Pending, MatchResult::Pending) => true,
(MatchResult::Matches(matches), MatchResult::Matches(other_matches)) => {
matches.len() == other_matches.len()
&& matches.iter().zip(other_matches.iter()).all(
|((view_id, action), (other_view_id, other_action))| {
view_id == other_view_id && action.eq(other_action.as_ref())
},
)
}
_ => false,
}
}
}
impl Eq for MatchResult {}
impl Clone for MatchResult {
fn clone(&self) -> Self {
match self {
MatchResult::None => MatchResult::None,
MatchResult::Pending => MatchResult::Pending,
MatchResult::Matches(matches) => MatchResult::Matches(
matches
.iter()
.map(|(view_id, action)| (*view_id, Action::boxed_clone(action.as_ref())))
.collect(),
),
}
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use serde::Deserialize;
use crate::{actions, impl_actions, keymap_matcher::KeymapContext};
use super::*;
#[test]
fn test_push_keystroke() -> Result<()> {
actions!(test, [B, AB, C, D, DA]);
let mut context1 = KeymapContext::default();
context1.set.insert("1".into());
let mut context2 = KeymapContext::default();
context2.set.insert("2".into());
let dispatch_path = vec![(2, context2), (1, context1)];
let keymap = Keymap::new(vec![
Binding::new("a b", AB, Some("1")),
Binding::new("b", B, Some("2")),
Binding::new("c", C, Some("2")),
Binding::new("d", D, Some("1")),
Binding::new("d", D, Some("2")),
Binding::new("d a", DA, Some("2")),
]);
let mut matcher = KeymapMatcher::new(keymap);
// Binding with pending prefix always takes precedence
assert_eq!(
matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
MatchResult::Pending,
);
// B alone doesn't match because a was pending, so AB is returned instead
assert_eq!(
matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
MatchResult::Matches(vec![(1, Box::new(AB))]),
);
assert!(!matcher.has_pending_keystrokes());
// Without an a prefix, B is dispatched like expected
assert_eq!(
matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
MatchResult::Matches(vec![(2, Box::new(B))]),
);
assert!(!matcher.has_pending_keystrokes());
// If a is prefixed, C will not be dispatched because there
// was a pending binding for it
assert_eq!(
matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
MatchResult::Pending,
);
assert_eq!(
matcher.push_keystroke(Keystroke::parse("c")?, dispatch_path.clone()),
MatchResult::None,
);
assert!(!matcher.has_pending_keystrokes());
// If a single keystroke matches multiple bindings in the tree
// all of them are returned so that we can fallback if the action
// handler decides to propagate the action
assert_eq!(
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!(
matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
MatchResult::Matches(vec![(2, Box::new(DA))]),
);
assert!(!matcher.has_pending_keystrokes());
Ok(())
}
#[test]
fn test_keystroke_parsing() -> Result<()> {
assert_eq!(
Keystroke::parse("ctrl-p")?,
Keystroke {
key: "p".into(),
ctrl: true,
alt: false,
shift: false,
cmd: false,
function: false,
}
);
assert_eq!(
Keystroke::parse("alt-shift-down")?,
Keystroke {
key: "down".into(),
ctrl: false,
alt: true,
shift: true,
cmd: false,
function: false,
}
);
assert_eq!(
Keystroke::parse("shift-cmd--")?,
Keystroke {
key: "-".into(),
ctrl: false,
alt: false,
shift: true,
cmd: true,
function: false,
}
);
Ok(())
}
#[test]
fn test_context_predicate_parsing() -> Result<()> {
use KeymapContextPredicate::*;
assert_eq!(
KeymapContextPredicate::parse("a && (b == c || d != e)")?,
And(
Box::new(Identifier("a".into())),
Box::new(Or(
Box::new(Equal("b".into(), "c".into())),
Box::new(NotEqual("d".into(), "e".into())),
))
)
);
assert_eq!(
KeymapContextPredicate::parse("!a")?,
Not(Box::new(Identifier("a".into())),)
);
Ok(())
}
#[test]
fn test_context_predicate_eval() {
let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap();
let mut context = KeymapContext::default();
context.set.insert("a".into());
assert!(!predicate.eval(&[context]));
let mut context = KeymapContext::default();
context.set.insert("a".into());
context.set.insert("b".into());
assert!(predicate.eval(&[context]));
let mut context = KeymapContext::default();
context.set.insert("a".into());
context.map.insert("c".into(), "x".into());
assert!(!predicate.eval(&[context]));
let mut context = KeymapContext::default();
context.set.insert("a".into());
context.map.insert("c".into(), "d".into());
assert!(predicate.eval(&[context]));
let predicate = KeymapContextPredicate::parse("!a").unwrap();
assert!(predicate.eval(&[KeymapContext::default()]));
}
#[test]
fn test_context_child_predicate_eval() {
let predicate = KeymapContextPredicate::parse("a && b > c").unwrap();
let contexts = [
context_set(&["e", "f"]),
context_set(&["c", "d"]), // match this context
context_set(&["a", "b"]),
];
assert!(!predicate.eval(&contexts[0..]));
assert!(predicate.eval(&contexts[1..]));
assert!(!predicate.eval(&contexts[2..]));
let predicate = KeymapContextPredicate::parse("a && b > c && !d > e").unwrap();
let contexts = [
context_set(&["f"]),
context_set(&["e"]), // only match this context
context_set(&["c"]),
context_set(&["a", "b"]),
context_set(&["e"]),
context_set(&["c", "d"]),
context_set(&["a", "b"]),
];
assert!(!predicate.eval(&contexts[0..]));
assert!(predicate.eval(&contexts[1..]));
assert!(!predicate.eval(&contexts[2..]));
assert!(!predicate.eval(&contexts[3..]));
assert!(!predicate.eval(&contexts[4..]));
assert!(!predicate.eval(&contexts[5..]));
assert!(!predicate.eval(&contexts[6..]));
fn context_set(names: &[&str]) -> KeymapContext {
KeymapContext {
set: names.iter().copied().map(str::to_string).collect(),
..Default::default()
}
}
}
#[test]
fn test_matcher() -> Result<()> {
#[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
pub struct A(pub String);
impl_actions!(test, [A]);
actions!(test, [B, Ab]);
#[derive(Clone, Debug, Eq, PartialEq)]
struct ActionArg {
a: &'static str,
}
let keymap = Keymap::new(vec![
Binding::new("a", A("x".to_string()), Some("a")),
Binding::new("b", B, Some("a")),
Binding::new("a b", Ab, Some("a || b")),
]);
let mut context_a = KeymapContext::default();
context_a.set.insert("a".into());
let mut context_b = KeymapContext::default();
context_b.set.insert("b".into());
let mut matcher = KeymapMatcher::new(keymap);
// Basic match
assert_eq!(
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))])
);
matcher.clear_pending();
// Multi-keystroke match
assert_eq!(
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
MatchResult::Pending
);
assert_eq!(
matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
MatchResult::Matches(vec![(1, Box::new(Ab))])
);
matcher.clear_pending();
// Failed matches don't interfere with matching subsequent keys
assert_eq!(
matcher.push_keystroke(Keystroke::parse("x")?, vec![(1, context_a.clone())]),
MatchResult::None
);
assert_eq!(
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))])
);
matcher.clear_pending();
// Pending keystrokes are cleared when the context changes
assert_eq!(
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
MatchResult::Pending
);
assert_eq!(
matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_a.clone())]),
MatchResult::None
);
matcher.clear_pending();
let mut context_c = KeymapContext::default();
context_c.set.insert("c".into());
// Pending keystrokes are maintained per-view
assert_eq!(
matcher.push_keystroke(
Keystroke::parse("a")?,
vec![(1, context_b.clone()), (2, context_c.clone())]
),
MatchResult::Pending
);
assert_eq!(
matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
MatchResult::Matches(vec![(1, Box::new(Ab))])
);
Ok(())
}
}

View File

@@ -0,0 +1,104 @@
use anyhow::Result;
use smallvec::SmallVec;
use crate::Action;
use super::{KeymapContext, KeymapContextPredicate, Keystroke};
pub struct Binding {
action: Box<dyn Action>,
keystrokes: Option<SmallVec<[Keystroke; 2]>>,
context_predicate: Option<KeymapContextPredicate>,
}
impl Binding {
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
Self::load(keystrokes, Box::new(action), context).unwrap()
}
pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
let context = if let Some(context) = context {
Some(KeymapContextPredicate::parse(context)?)
} else {
None
};
let keystrokes = if keystrokes == "*" {
None // Catch all context
} else {
Some(
keystrokes
.split_whitespace()
.map(Keystroke::parse)
.collect::<Result<_>>()?,
)
};
Ok(Self {
keystrokes,
action,
context_predicate: context,
})
}
fn match_context(&self, contexts: &[KeymapContext]) -> bool {
self.context_predicate
.as_ref()
.map(|predicate| predicate.eval(contexts))
.unwrap_or(true)
}
pub fn match_keys_and_context(
&self,
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 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)
{
BindingMatchResult::Complete(self.action.boxed_clone())
} else {
BindingMatchResult::Partial
}
} else {
BindingMatchResult::Fail
}
}
pub fn keystrokes_for_action(
&self,
action: &dyn Action,
contexts: &[KeymapContext],
) -> Option<SmallVec<[Keystroke; 2]>> {
if self.action.eq(action) && self.match_context(contexts) {
self.keystrokes.clone()
} else {
None
}
}
pub fn keystrokes(&self) -> Option<&[Keystroke]> {
self.keystrokes.as_deref()
}
pub fn action(&self) -> &dyn Action {
self.action.as_ref()
}
}
pub enum BindingMatchResult {
Complete(Box<dyn Action>),
Partial,
Fail,
}

View File

@@ -0,0 +1,61 @@
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
collections::HashMap,
};
use super::Binding;
#[derive(Default)]
pub struct Keymap {
bindings: Vec<Binding>,
binding_indices_by_action_type: HashMap<TypeId, SmallVec<[usize; 3]>>,
}
impl Keymap {
pub fn new(bindings: Vec<Binding>) -> Self {
let mut binding_indices_by_action_type = HashMap::new();
for (ix, binding) in bindings.iter().enumerate() {
binding_indices_by_action_type
.entry(binding.action().type_id())
.or_insert_with(SmallVec::new)
.push(ix);
}
Self {
binding_indices_by_action_type,
bindings,
}
}
pub(crate) fn bindings_for_action_type(
&self,
action_type: TypeId,
) -> impl Iterator<Item = &'_ Binding> {
self.binding_indices_by_action_type
.get(&action_type)
.map(SmallVec::as_slice)
.unwrap_or(&[])
.iter()
.map(|ix| &self.bindings[*ix])
}
pub(crate) fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
for binding in bindings {
self.binding_indices_by_action_type
.entry(binding.action().as_any().type_id())
.or_default()
.push(self.bindings.len());
self.bindings.push(binding);
}
}
pub(crate) fn clear(&mut self) {
self.bindings.clear();
self.binding_indices_by_action_type.clear();
}
pub fn bindings(&self) -> &Vec<Binding> {
&self.bindings
}
}

View File

@@ -0,0 +1,298 @@
use anyhow::{anyhow, Result};
use collections::{HashMap, HashSet};
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct KeymapContext {
pub set: HashSet<String>,
pub map: HashMap<String, String>,
}
impl KeymapContext {
pub fn extend(&mut self, other: &Self) {
for v in &other.set {
self.set.insert(v.clone());
}
for (k, v) in &other.map {
self.map.insert(k.clone(), v.clone());
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub enum KeymapContextPredicate {
Identifier(String),
Equal(String, String),
NotEqual(String, String),
Child(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
Not(Box<KeymapContextPredicate>),
And(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
Or(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
}
impl KeymapContextPredicate {
pub fn parse(source: &str) -> Result<Self> {
let source = Self::skip_whitespace(source);
let (predicate, rest) = Self::parse_expr(source, 0)?;
if let Some(next) = rest.chars().next() {
Err(anyhow!("unexpected character {next:?}"))
} else {
Ok(predicate)
}
}
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::Equal(left, right) => context
.map
.get(left)
.map(|value| value == right)
.unwrap_or(false),
Self::NotEqual(left, right) => context
.map
.get(left)
.map(|value| value != right)
.unwrap_or(true),
Self::Not(pred) => !pred.eval(contexts),
Self::Child(parent, child) => parent.eval(&contexts[1..]) && child.eval(contexts),
Self::And(left, right) => left.eval(contexts) && right.eval(contexts),
Self::Or(left, right) => left.eval(contexts) || right.eval(contexts),
}
}
fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
type Op =
fn(KeymapContextPredicate, KeymapContextPredicate) -> Result<KeymapContextPredicate>;
let (mut predicate, rest) = Self::parse_primary(source)?;
source = rest;
'parse: loop {
for (operator, precedence, constructor) in [
(">", PRECEDENCE_CHILD, Self::new_child as Op),
("&&", PRECEDENCE_AND, Self::new_and as Op),
("||", PRECEDENCE_OR, Self::new_or as Op),
("==", PRECEDENCE_EQ, Self::new_eq as Op),
("!=", PRECEDENCE_EQ, Self::new_neq as Op),
] {
if source.starts_with(operator) && precedence >= min_precedence {
source = Self::skip_whitespace(&source[operator.len()..]);
let (right, rest) = Self::parse_expr(source, precedence + 1)?;
predicate = constructor(predicate, right)?;
source = rest;
continue 'parse;
}
}
break;
}
Ok((predicate, source))
}
fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> {
let next = source
.chars()
.next()
.ok_or_else(|| anyhow!("unexpected eof"))?;
match next {
'(' => {
source = Self::skip_whitespace(&source[1..]);
let (predicate, rest) = Self::parse_expr(source, 0)?;
if rest.starts_with(')') {
source = Self::skip_whitespace(&rest[1..]);
Ok((predicate, source))
} else {
Err(anyhow!("expected a ')'"))
}
}
'!' => {
let source = Self::skip_whitespace(&source[1..]);
let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?;
Ok((KeymapContextPredicate::Not(Box::new(predicate)), source))
}
_ if next.is_alphanumeric() || next == '_' => {
let len = source
.find(|c: char| !(c.is_alphanumeric() || c == '_'))
.unwrap_or(source.len());
let (identifier, rest) = source.split_at(len);
source = Self::skip_whitespace(rest);
Ok((
KeymapContextPredicate::Identifier(identifier.into()),
source,
))
}
_ => Err(anyhow!("unexpected character {next:?}")),
}
}
fn skip_whitespace(source: &str) -> &str {
let len = source
.find(|c: char| !c.is_whitespace())
.unwrap_or(source.len());
&source[len..]
}
fn new_or(self, other: Self) -> Result<Self> {
Ok(Self::Or(Box::new(self), Box::new(other)))
}
fn new_and(self, other: Self) -> Result<Self> {
Ok(Self::And(Box::new(self), Box::new(other)))
}
fn new_child(self, other: Self) -> Result<Self> {
Ok(Self::Child(Box::new(self), Box::new(other)))
}
fn new_eq(self, other: Self) -> Result<Self> {
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
Ok(Self::Equal(left, right))
} else {
Err(anyhow!("operands must be identifiers"))
}
}
fn new_neq(self, other: Self) -> Result<Self> {
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
Ok(Self::NotEqual(left, right))
} else {
Err(anyhow!("operands must be identifiers"))
}
}
}
const PRECEDENCE_CHILD: u32 = 1;
const PRECEDENCE_OR: u32 = 2;
const PRECEDENCE_AND: u32 = 3;
const PRECEDENCE_EQ: u32 = 4;
const PRECEDENCE_NOT: u32 = 5;
#[cfg(test)]
mod tests {
use super::KeymapContextPredicate::{self, *};
#[test]
fn test_parse_identifiers() {
// Identifiers
assert_eq!(
KeymapContextPredicate::parse("abc12").unwrap(),
Identifier("abc12".into())
);
assert_eq!(
KeymapContextPredicate::parse("_1a").unwrap(),
Identifier("_1a".into())
);
}
#[test]
fn test_parse_negations() {
assert_eq!(
KeymapContextPredicate::parse("!abc").unwrap(),
Not(Box::new(Identifier("abc".into())))
);
assert_eq!(
KeymapContextPredicate::parse(" ! ! abc").unwrap(),
Not(Box::new(Not(Box::new(Identifier("abc".into())))))
);
}
#[test]
fn test_parse_equality_operators() {
assert_eq!(
KeymapContextPredicate::parse("a == b").unwrap(),
Equal("a".into(), "b".into())
);
assert_eq!(
KeymapContextPredicate::parse("c!=d").unwrap(),
NotEqual("c".into(), "d".into())
);
assert_eq!(
KeymapContextPredicate::parse("c == !d")
.unwrap_err()
.to_string(),
"operands must be identifiers"
);
}
#[test]
fn test_parse_boolean_operators() {
assert_eq!(
KeymapContextPredicate::parse("a || b").unwrap(),
Or(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into()))
)
);
assert_eq!(
KeymapContextPredicate::parse("a || !b && c").unwrap(),
Or(
Box::new(Identifier("a".into())),
Box::new(And(
Box::new(Not(Box::new(Identifier("b".into())))),
Box::new(Identifier("c".into()))
))
)
);
assert_eq!(
KeymapContextPredicate::parse("a && b || c&&d").unwrap(),
Or(
Box::new(And(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into()))
)),
Box::new(And(
Box::new(Identifier("c".into())),
Box::new(Identifier("d".into()))
))
)
);
assert_eq!(
KeymapContextPredicate::parse("a == b && c || d == e && f").unwrap(),
Or(
Box::new(And(
Box::new(Equal("a".into(), "b".into())),
Box::new(Identifier("c".into()))
)),
Box::new(And(
Box::new(Equal("d".into(), "e".into())),
Box::new(Identifier("f".into()))
))
)
);
assert_eq!(
KeymapContextPredicate::parse("a && b && c && d").unwrap(),
And(
Box::new(And(
Box::new(And(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into()))
)),
Box::new(Identifier("c".into())),
)),
Box::new(Identifier("d".into()))
),
);
}
#[test]
fn test_parse_parenthesized_expressions() {
assert_eq!(
KeymapContextPredicate::parse("a && (b == c || d != e)").unwrap(),
And(
Box::new(Identifier("a".into())),
Box::new(Or(
Box::new(Equal("b".into(), "c".into())),
Box::new(NotEqual("d".into(), "e".into())),
)),
),
);
assert_eq!(
KeymapContextPredicate::parse(" ( a || b ) ").unwrap(),
Or(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into())),
)
);
}
}

View File

@@ -0,0 +1,97 @@
use std::fmt::Write;
use anyhow::anyhow;
use serde::Deserialize;
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize)]
pub struct Keystroke {
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
pub cmd: bool,
pub function: bool,
pub key: String,
}
impl Keystroke {
pub fn parse(source: &str) -> anyhow::Result<Self> {
let mut ctrl = false;
let mut alt = false;
let mut shift = false;
let mut cmd = false;
let mut function = false;
let mut key = None;
let mut components = source.split('-').peekable();
while let Some(component) = components.next() {
match component {
"ctrl" => ctrl = true,
"alt" => alt = true,
"shift" => shift = true,
"cmd" => cmd = true,
"fn" => function = true,
_ => {
if let Some(component) = components.peek() {
if component.is_empty() && source.ends_with('-') {
key = Some(String::from("-"));
break;
} else {
return Err(anyhow!("Invalid keystroke `{}`", source));
}
} else {
key = Some(String::from(component));
}
}
}
}
let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
Ok(Keystroke {
ctrl,
alt,
shift,
cmd,
function,
key,
})
}
pub fn modified(&self) -> bool {
self.ctrl || self.alt || self.shift || self.cmd
}
}
impl std::fmt::Display for Keystroke {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.ctrl {
f.write_char('^')?;
}
if self.alt {
f.write_char('⎇')?;
}
if self.cmd {
f.write_char('⌘')?;
}
if self.shift {
f.write_char('⇧')?;
}
let key = match self.key.as_str() {
"backspace" => '⌫',
"up" => '↑',
"down" => '↓',
"left" => '←',
"right" => '→',
"tab" => '⇥',
"escape" => '⎋',
key => {
if key.len() == 1 {
key.chars().next().unwrap().to_ascii_uppercase()
} else {
return f.write_str(key);
}
}
};
f.write_char(key)
}
}

View File

@@ -14,7 +14,7 @@ use crate::{
rect::{RectF, RectI},
vector::Vector2F,
},
keymap,
keymap_matcher::KeymapMatcher,
text_layout::{LineLayout, RunStyle},
Action, ClipboardItem, Menu, Scene,
};
@@ -87,7 +87,7 @@ pub(crate) trait ForegroundPlatform {
fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>);
fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
fn on_will_open_menu(&self, callback: Box<dyn FnMut()>);
fn set_menus(&self, menus: Vec<Menu>, matcher: &keymap::Matcher);
fn set_menus(&self, menus: Vec<Menu>, matcher: &KeymapMatcher);
fn prompt_for_paths(
&self,
options: PathPromptOptions,

View File

@@ -2,7 +2,7 @@ use std::ops::Deref;
use pathfinder_geometry::vector::vec2f;
use crate::{geometry::vector::Vector2F, keymap::Keystroke};
use crate::{geometry::vector::Vector2F, keymap_matcher::Keystroke};
#[derive(Clone, Debug)]
pub struct KeyDownEvent {

View File

@@ -1,6 +1,6 @@
use crate::{
geometry::vector::vec2f,
keymap::Keystroke,
keymap_matcher::Keystroke,
platform::{Event, NavigationDirection},
KeyDownEvent, KeyUpEvent, Modifiers, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
MouseMovedEvent, ScrollDelta, ScrollWheelEvent, TouchPhase,
@@ -47,6 +47,8 @@ pub fn key_to_native(key: &str) -> Cow<str> {
"right" => NSRightArrowFunctionKey,
"pageup" => NSPageUpFunctionKey,
"pagedown" => NSPageDownFunctionKey,
"home" => NSHomeFunctionKey,
"end" => NSEndFunctionKey,
"delete" => NSDeleteFunctionKey,
"f1" => NSF1FunctionKey,
"f2" => NSF2FunctionKey,
@@ -258,6 +260,8 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
Some(NSRightArrowFunctionKey) => "right".to_string(),
Some(NSPageUpFunctionKey) => "pageup".to_string(),
Some(NSPageDownFunctionKey) => "pagedown".to_string(),
Some(NSHomeFunctionKey) => "home".to_string(),
Some(NSEndFunctionKey) => "end".to_string(),
Some(NSDeleteFunctionKey) => "delete".to_string(),
Some(NSF1FunctionKey) => "f1".to_string(),
Some(NSF2FunctionKey) => "f2".to_string(),

View File

@@ -3,7 +3,8 @@ use super::{
FontSystem, Window,
};
use crate::{
executor, keymap,
executor,
keymap_matcher::KeymapMatcher,
platform::{self, CursorStyle},
Action, AppVersion, ClipboardItem, Event, Menu, MenuItem,
};
@@ -135,7 +136,7 @@ impl MacForegroundPlatform {
menus: Vec<Menu>,
delegate: id,
actions: &mut Vec<Box<dyn Action>>,
keystroke_matcher: &keymap::Matcher,
keystroke_matcher: &KeymapMatcher,
) -> id {
let application_menu = NSMenu::new(nil).autorelease();
application_menu.setDelegate_(delegate);
@@ -172,7 +173,7 @@ impl MacForegroundPlatform {
item: MenuItem,
delegate: id,
actions: &mut Vec<Box<dyn Action>>,
keystroke_matcher: &keymap::Matcher,
keystroke_matcher: &KeymapMatcher,
) -> id {
match item {
MenuItem::Separator => NSMenuItem::separatorItem(nil),
@@ -183,7 +184,7 @@ impl MacForegroundPlatform {
.map(|binding| binding.keystrokes());
let item;
if let Some(keystrokes) = keystrokes {
if let Some(keystrokes) = keystrokes.flatten() {
if keystrokes.len() == 1 {
let keystroke = &keystrokes[0];
let mut mask = NSEventModifierFlags::empty();
@@ -317,7 +318,7 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
self.0.borrow_mut().validate_menu_command = Some(callback);
}
fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &keymap::Matcher) {
fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &KeymapMatcher) {
unsafe {
let app: id = msg_send![APP_CLASS, sharedApplication];
let mut state = self.0.borrow_mut();
@@ -647,7 +648,7 @@ impl platform::Platform for MacPlatform {
attrs.set(kSecReturnAttributes as *const _, cf_true);
attrs.set(kSecReturnData as *const _, cf_true);
let mut result = CFTypeRef::from(ptr::null_mut());
let mut result = CFTypeRef::from(ptr::null());
let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result);
match status {
security::errSecSuccess => {}

View File

@@ -4,7 +4,7 @@ use crate::{
rect::RectF,
vector::{vec2f, Vector2F},
},
keymap::Keystroke,
keymap_matcher::Keystroke,
mac::platform::NSViewLayerContentsRedrawDuringViewResize,
platform::{
self,

View File

@@ -4,7 +4,8 @@ use crate::{
rect::RectF,
vector::{vec2f, Vector2F},
},
keymap, Action, ClipboardItem,
keymap_matcher::KeymapMatcher,
Action, ClipboardItem,
};
use anyhow::{anyhow, Result};
use collections::VecDeque;
@@ -84,7 +85,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>, _: &keymap::Matcher) {}
fn set_menus(&self, _: Vec<crate::Menu>, _: &KeymapMatcher) {}
fn prompt_for_paths(
&self,

View File

@@ -4,7 +4,7 @@ use crate::{
font_cache::FontCache,
geometry::rect::RectF,
json::{self, ToJson},
keymap::Keystroke,
keymap_matcher::Keystroke,
platform::{CursorStyle, Event},
scene::{
CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover,
@@ -573,7 +573,7 @@ pub struct LayoutContext<'a> {
impl<'a> LayoutContext<'a> {
pub(crate) fn keystrokes_for_action(
&self,
&mut self,
action: &dyn Action,
) -> Option<SmallVec<[Keystroke; 2]>> {
self.app

View File

@@ -1,7 +1,10 @@
use crate::{
elements::Empty, executor, platform, util::CwdBacktrace, Element, ElementBox, Entity,
FontCache, Handle, LeakDetector, MutableAppContext, Platform, RenderContext, Subscription,
TestAppContext, View,
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;
@@ -62,7 +65,7 @@ pub fn run_test(
let platform = Arc::new(platform::test::platform());
let font_system = platform.fonts();
let font_cache = Arc::new(FontCache::new(font_system));
let mut prev_runnable_history: Option<Vec<usize>> = None;
let mut prev_runnable_history: Option<Vec<ExecutorEvent>> = None;
for _ in 0..num_iterations {
let seed = atomic_seed.load(SeqCst);
@@ -73,6 +76,7 @@ pub fn run_test(
let deterministic = executor::Deterministic::new(seed);
if detect_nondeterminism {
deterministic.set_previous_execution_history(prev_runnable_history.clone());
deterministic.enable_runnable_backtrace();
}
@@ -98,7 +102,7 @@ pub fn run_test(
leak_detector.lock().detect();
if detect_nondeterminism {
let curr_runnable_history = deterministic.runnable_history();
let curr_runnable_history = deterministic.execution_history();
if let Some(prev_runnable_history) = prev_runnable_history {
let mut prev_entries = prev_runnable_history.iter().fuse();
let mut curr_entries = curr_runnable_history.iter().fuse();
@@ -138,7 +142,7 @@ pub fn run_test(
let last_common_backtrace = common_history_prefix
.last()
.map(|runnable_id| deterministic.runnable_backtrace(*runnable_id));
.map(|event| deterministic.runnable_backtrace(event.id()));
writeln!(
&mut error,

View File

@@ -162,11 +162,9 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
if let FnArg::Typed(arg) = arg {
if let Type::Path(ty) = &*arg.ty {
let last_segment = ty.path.segments.last();
match last_segment.map(|s| s.ident.to_string()).as_deref() {
Some("StdRng") => {
inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed),));
}
_ => {}
if let Some("StdRng") = last_segment.map(|s| s.ident.to_string()).as_deref() {
inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed),));
}
} else {
inner_fn_args.extend(quote!(cx,));

View File

@@ -385,6 +385,12 @@ impl Buffer {
rpc::proto::LineEnding::from_i32(message.line_ending)
.ok_or_else(|| anyhow!("missing line_ending"))?,
));
this.saved_version = proto::deserialize_version(message.saved_version);
this.saved_version_fingerprint = message.saved_version_fingerprint;
this.saved_mtime = message
.saved_mtime
.ok_or_else(|| anyhow!("invalid saved_mtime"))?
.into();
Ok(this)
}
@@ -395,10 +401,17 @@ impl Buffer {
base_text: self.base_text().to_string(),
diff_base: self.diff_base.as_ref().map(|h| h.to_string()),
line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
saved_version: proto::serialize_version(&self.saved_version),
saved_version_fingerprint: self.saved_version_fingerprint.clone(),
saved_mtime: Some(self.saved_mtime.into()),
}
}
pub fn serialize_ops(&self, cx: &AppContext) -> Task<Vec<proto::Operation>> {
pub fn serialize_ops(
&self,
since: Option<clock::Global>,
cx: &AppContext,
) -> Task<Vec<proto::Operation>> {
let mut operations = Vec::new();
operations.extend(self.deferred_ops.iter().map(proto::serialize_operation));
operations.extend(self.remote_selections.iter().map(|(_, set)| {
@@ -422,9 +435,11 @@ impl Buffer {
let text_operations = self.text.operations().clone();
cx.background().spawn(async move {
let since = since.unwrap_or_default();
operations.extend(
text_operations
.iter()
.filter(|(_, op)| !since.observed(op.local_timestamp()))
.map(|(_, op)| proto::serialize_operation(&Operation::Buffer(op.clone()))),
);
operations.sort_unstable_by_key(proto::lamport_timestamp_for_operation);
@@ -508,8 +523,8 @@ impl Buffer {
self.text.snapshot()
}
pub fn file(&self) -> Option<&dyn File> {
self.file.as_deref()
pub fn file(&self) -> Option<&Arc<dyn File>> {
self.file.as_ref()
}
pub fn save(
@@ -543,6 +558,14 @@ impl Buffer {
&self.saved_version
}
pub fn saved_version_fingerprint(&self) -> &str {
&self.saved_version_fingerprint
}
pub fn saved_mtime(&self) -> SystemTime {
self.saved_mtime
}
pub fn set_language(&mut self, language: Option<Arc<Language>>, cx: &mut ModelContext<Self>) {
self.syntax_map.lock().clear();
self.language = language;
@@ -676,7 +699,6 @@ impl Buffer {
task
}
#[cfg(any(test, feature = "test-support"))]
pub fn diff_base(&self) -> Option<&str> {
self.diff_base.as_deref()
}
@@ -1327,13 +1349,6 @@ impl Buffer {
let edit_id = edit_operation.local_timestamp();
if let Some((before_edit, mode)) = autoindent_request {
let (start_columns, is_block_mode) = match mode {
AutoindentMode::Block {
original_indent_columns: start_columns,
} => (start_columns, true),
AutoindentMode::EachLine => (Default::default(), false),
};
let mut delta = 0isize;
let entries = edits
.into_iter()
@@ -1347,7 +1362,7 @@ impl Buffer {
let mut range_of_insertion_to_indent = 0..new_text_len;
let mut first_line_is_new = false;
let mut start_column = None;
let mut original_indent_column = None;
// When inserting an entire line at the beginning of an existing line,
// treat the insertion as new.
@@ -1359,14 +1374,23 @@ impl Buffer {
// When inserting text starting with a newline, avoid auto-indenting the
// previous line.
if new_text[range_of_insertion_to_indent.clone()].starts_with('\n') {
if new_text.starts_with('\n') {
range_of_insertion_to_indent.start += 1;
first_line_is_new = true;
}
// Avoid auto-indenting after the insertion.
if is_block_mode {
start_column = start_columns.get(ix).copied();
if let AutoindentMode::Block {
original_indent_columns,
} = &mode
{
original_indent_column =
Some(original_indent_columns.get(ix).copied().unwrap_or_else(|| {
indent_size_for_text(
new_text[range_of_insertion_to_indent.clone()].chars(),
)
.len
}));
if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') {
range_of_insertion_to_indent.end -= 1;
}
@@ -1374,7 +1398,7 @@ impl Buffer {
AutoindentRequestEntry {
first_line_is_new,
original_indent_column: start_column,
original_indent_column,
indent_size: before_edit.language_indent_size_at(range.start, cx),
range: self.anchor_before(new_start + range_of_insertion_to_indent.start)
..self.anchor_after(new_start + range_of_insertion_to_indent.end),
@@ -1385,7 +1409,7 @@ impl Buffer {
self.autoindent_requests.push(Arc::new(AutoindentRequest {
before_edit,
entries,
is_block_mode,
is_block_mode: matches!(mode, AutoindentMode::Block { .. }),
}));
}
@@ -2367,8 +2391,8 @@ impl BufferSnapshot {
self.selections_update_count
}
pub fn file(&self) -> Option<&dyn File> {
self.file.as_deref()
pub fn file(&self) -> Option<&Arc<dyn File>> {
self.file.as_ref()
}
pub fn resolve_file_path(&self, cx: &AppContext, include_root: bool) -> Option<PathBuf> {
@@ -2392,7 +2416,7 @@ impl BufferSnapshot {
}
}
pub fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize {
fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize {
indent_size_for_text(text.chars_at(Point::new(row, 0)))
}

View File

@@ -289,6 +289,9 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) {
);
buffer.update(cx, |buf, cx| {
buf.undo(cx);
buf.undo(cx);
buf.undo(cx);
buf.undo(cx);
assert_eq!(buf.text(), "fn a() {}");
assert!(buf.is_parsing());
@@ -304,6 +307,9 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) {
);
buffer.update(cx, |buf, cx| {
buf.redo(cx);
buf.redo(cx);
buf.redo(cx);
buf.redo(cx);
assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
assert!(buf.is_parsing());
@@ -989,12 +995,17 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) {
.unindent();
let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
// When this text was copied, both of the quotation marks were at the same
// indent level, but the indentation of the first line was not included in
// the copied text. This information is retained in the
// 'original_indent_columns' vector.
let original_indent_columns = vec![4];
let inserted_text = r#"
"
c
d
e
"
c
d
e
"
"#
.unindent();
@@ -1003,7 +1014,7 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) {
buffer.edit(
[(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())],
Some(AutoindentMode::Block {
original_indent_columns: vec![0],
original_indent_columns: original_indent_columns.clone(),
}),
cx,
);
@@ -1022,13 +1033,16 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) {
.unindent()
);
// Grouping is disabled in tests, so we need 2 undos
buffer.undo(cx); // Undo the auto-indent
buffer.undo(cx); // Undo the original edit
// Insert the block at a deeper indent level. The entire block is outdented.
buffer.undo(cx);
buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx);
buffer.edit(
[(Point::new(2, 8)..Point::new(2, 8), inserted_text)],
Some(AutoindentMode::Block {
original_indent_columns: vec![0],
original_indent_columns: original_indent_columns.clone(),
}),
cx,
);
@@ -1051,6 +1065,84 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) {
});
}
#[gpui::test]
fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut MutableAppContext) {
cx.set_global(Settings::test(cx));
cx.add_model(|cx| {
let text = r#"
fn a() {
if b() {
}
}
"#
.unindent();
let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
// The original indent columns are not known, so this text is
// auto-indented in a block as if the first line was copied in
// its entirety.
let original_indent_columns = Vec::new();
let inserted_text = " c\n .d()\n .e();";
// Insert the block at column zero. The entire block is indented
// so that the first line matches the previous line's indentation.
buffer.edit(
[(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())],
Some(AutoindentMode::Block {
original_indent_columns: original_indent_columns.clone(),
}),
cx,
);
assert_eq!(
buffer.text(),
r#"
fn a() {
if b() {
c
.d()
.e();
}
}
"#
.unindent()
);
// Grouping is disabled in tests, so we need 2 undos
buffer.undo(cx); // Undo the auto-indent
buffer.undo(cx); // Undo the original edit
// Insert the block at a deeper indent level. The entire block is outdented.
buffer.edit(
[(Point::new(2, 0)..Point::new(2, 0), " ".repeat(12))],
None,
cx,
);
buffer.edit(
[(Point::new(2, 12)..Point::new(2, 12), inserted_text)],
Some(AutoindentMode::Block {
original_indent_columns: Vec::new(),
}),
cx,
);
assert_eq!(
buffer.text(),
r#"
fn a() {
if b() {
c
.d()
.e();
}
}
"#
.unindent()
);
buffer
});
}
#[gpui::test]
fn test_autoindent_language_without_indents_query(cx: &mut MutableAppContext) {
cx.set_global(Settings::test(cx));
@@ -1275,7 +1367,9 @@ fn test_serialization(cx: &mut gpui::MutableAppContext) {
assert_eq!(buffer1.read(cx).text(), "abcDF");
let state = buffer1.read(cx).to_proto();
let ops = cx.background().block(buffer1.read(cx).serialize_ops(cx));
let ops = cx
.background()
.block(buffer1.read(cx).serialize_ops(None, cx));
let buffer2 = cx.add_model(|cx| {
let mut buffer = Buffer::from_proto(1, state, None).unwrap();
buffer
@@ -1316,7 +1410,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
let state = base_buffer.read(cx).to_proto();
let ops = cx
.background()
.block(base_buffer.read(cx).serialize_ops(cx));
.block(base_buffer.read(cx).serialize_ops(None, cx));
let mut buffer = Buffer::from_proto(i as ReplicaId, state, None).unwrap();
buffer
.apply_ops(
@@ -1413,7 +1507,9 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
}
50..=59 if replica_ids.len() < max_peers => {
let old_buffer_state = buffer.read(cx).to_proto();
let old_buffer_ops = cx.background().block(buffer.read(cx).serialize_ops(cx));
let old_buffer_ops = cx
.background()
.block(buffer.read(cx).serialize_ops(None, cx));
let new_replica_id = (0..=replica_ids.len() as ReplicaId)
.filter(|replica_id| *replica_id != buffer.read(cx).replica_id())
.choose(&mut rng)

View File

@@ -5,7 +5,7 @@ use std::{
process::Command,
};
const SWIFT_PACKAGE_NAME: &'static str = "LiveKitBridge";
const SWIFT_PACKAGE_NAME: &str = "LiveKitBridge";
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -61,8 +61,8 @@ fn build_bridge(swift_target: &SwiftTarget) {
let swift_package_root = swift_package_root();
if !Command::new("swift")
.arg("build")
.args(&["--configuration", &env::var("PROFILE").unwrap()])
.args(&["--triple", &swift_target.target.triple])
.args(["--configuration", &env::var("PROFILE").unwrap()])
.args(["--triple", &swift_target.target.triple])
.current_dir(&swift_package_root)
.status()
.unwrap()
@@ -116,7 +116,7 @@ fn get_swift_target() -> SwiftTarget {
let target = format!("{}-apple-macosx{}", arch, MACOS_TARGET_VERSION);
let swift_target_info_str = Command::new("swift")
.args(&["-target", &target, "-print-target-info"])
.args(["-target", &target, "-print-target-info"])
.output()
.unwrap()
.stdout;
@@ -143,7 +143,7 @@ fn copy_dir(source: &Path, destination: &Path) {
assert!(
Command::new("cp")
.arg("-R")
.args(&[source, destination])
.args([source, destination])
.status()
.unwrap()
.success(),

View File

@@ -1,5 +1,5 @@
use futures::StreamExt;
use gpui::{actions, keymap::Binding, Menu, MenuItem};
use gpui::{actions, keymap_matcher::Binding, Menu, MenuItem};
use live_kit_client::{LocalVideoTrack, RemoteVideoTrackUpdate, Room};
use live_kit_server::token::{self, VideoGrant};
use log::LevelFilter;

View File

@@ -40,7 +40,7 @@ pub struct LanguageServer {
name: String,
capabilities: ServerCapabilities,
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
response_handlers: Arc<Mutex<HashMap<usize, ResponseHandler>>>,
response_handlers: Arc<Mutex<Option<HashMap<usize, ResponseHandler>>>>,
executor: Arc<executor::Background>,
#[allow(clippy::type_complexity)]
io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
@@ -170,12 +170,18 @@ impl LanguageServer {
let (outbound_tx, outbound_rx) = channel::unbounded::<Vec<u8>>();
let notification_handlers =
Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default()));
let response_handlers = Arc::new(Mutex::new(HashMap::<_, ResponseHandler>::default()));
let response_handlers =
Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default())));
let input_task = cx.spawn(|cx| {
let notification_handlers = notification_handlers.clone();
let response_handlers = response_handlers.clone();
async move {
let _clear_response_handlers = ClearResponseHandlers(response_handlers.clone());
let _clear_response_handlers = util::defer({
let response_handlers = response_handlers.clone();
move || {
response_handlers.lock().take();
}
});
let mut buffer = Vec::new();
loop {
buffer.clear();
@@ -200,7 +206,11 @@ impl LanguageServer {
} else if let Ok(AnyResponse { id, error, result }) =
serde_json::from_slice(&buffer)
{
if let Some(handler) = response_handlers.lock().remove(&id) {
if let Some(handler) = response_handlers
.lock()
.as_mut()
.and_then(|handlers| handlers.remove(&id))
{
if let Some(error) = error {
handler(Err(error));
} else if let Some(result) = result {
@@ -226,7 +236,12 @@ impl LanguageServer {
let output_task = cx.background().spawn({
let response_handlers = response_handlers.clone();
async move {
let _clear_response_handlers = ClearResponseHandlers(response_handlers);
let _clear_response_handlers = util::defer({
let response_handlers = response_handlers.clone();
move || {
response_handlers.lock().take();
}
});
let mut content_len_buffer = Vec::new();
while let Ok(message) = outbound_rx.recv().await {
log::trace!("outgoing message:{}", String::from_utf8_lossy(&message));
@@ -366,7 +381,7 @@ impl LanguageServer {
async move {
log::debug!("language server shutdown started");
shutdown_request.await?;
response_handlers.lock().clear();
response_handlers.lock().take();
exit?;
output_done.recv().await;
log::debug!("language server shutdown finished");
@@ -521,7 +536,7 @@ impl LanguageServer {
fn request_internal<T: request::Request>(
next_id: &AtomicUsize,
response_handlers: &Mutex<HashMap<usize, ResponseHandler>>,
response_handlers: &Mutex<Option<HashMap<usize, ResponseHandler>>>,
outbound_tx: &channel::Sender<Vec<u8>>,
params: T::Params,
) -> impl 'static + Future<Output = Result<T::Result>>
@@ -537,25 +552,31 @@ impl LanguageServer {
})
.unwrap();
let (tx, rx) = oneshot::channel();
let handle_response = response_handlers
.lock()
.as_mut()
.ok_or_else(|| anyhow!("server shut down"))
.map(|handlers| {
handlers.insert(
id,
Box::new(move |result| {
let response = match result {
Ok(response) => serde_json::from_str(response)
.context("failed to deserialize response"),
Err(error) => Err(anyhow!("{}", error.message)),
};
let _ = tx.send(response);
}),
);
});
let send = outbound_tx
.try_send(message)
.context("failed to write to language server's stdin");
let (tx, rx) = oneshot::channel();
response_handlers.lock().insert(
id,
Box::new(move |result| {
let response = match result {
Ok(response) => {
serde_json::from_str(response).context("failed to deserialize response")
}
Err(error) => Err(anyhow!("{}", error.message)),
};
let _ = tx.send(response);
}),
);
async move {
handle_response?;
send?;
rx.await?
}
@@ -762,14 +783,6 @@ impl FakeLanguageServer {
}
}
struct ClearResponseHandlers(Arc<Mutex<HashMap<usize, ResponseHandler>>>);
impl Drop for ClearResponseHandlers {
fn drop(&mut self) {
self.0.lock().clear();
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -3,7 +3,7 @@ use std::{env, path::PathBuf, process::Command};
fn main() {
let sdk_path = String::from_utf8(
Command::new("xcrun")
.args(&["--sdk", "macosx", "--show-sdk-path"])
.args(["--sdk", "macosx", "--show-sdk-path"])
.output()
.unwrap()
.stdout,

View File

@@ -113,9 +113,9 @@ pub mod core_video {
let mut this = ptr::null();
let result = CVMetalTextureCacheCreate(
kCFAllocatorDefault,
ptr::null_mut(),
ptr::null(),
metal_device,
ptr::null_mut(),
ptr::null(),
&mut this,
);
if result == kCVReturnSuccess {
@@ -192,7 +192,7 @@ pub mod core_video {
pub fn as_texture_ref(&self) -> &metal::TextureRef {
unsafe {
let texture = CVMetalTextureGetTexture(self.as_concrete_TypeRef());
&metal::TextureRef::from_ptr(texture as *mut _)
metal::TextureRef::from_ptr(texture as *mut _)
}
}
}

View File

@@ -2,7 +2,7 @@ use editor::Editor;
use gpui::{
elements::*,
geometry::vector::{vec2f, Vector2F},
keymap,
keymap_matcher::KeymapContext,
platform::CursorStyle,
AnyViewHandle, AppContext, Axis, Entity, MouseButton, MouseState, MutableAppContext,
RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
@@ -124,7 +124,7 @@ impl<D: PickerDelegate> View for Picker<D> {
.named("picker")
}
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.set.insert("menu".into());
cx

View File

@@ -37,6 +37,7 @@ util = { path = "../util" }
aho-corasick = "0.7"
anyhow = "1.0.57"
async-trait = "0.1"
backtrace = "0.3"
futures = "0.3"
ignore = "0.4"
lazy_static = "1.4.0"

View File

@@ -524,7 +524,7 @@ async fn location_links_from_proto(
Some(origin) => {
let buffer = project
.update(&mut cx, |this, cx| {
this.wait_for_buffer(origin.buffer_id, cx)
this.wait_for_remote_buffer(origin.buffer_id, cx)
})
.await?;
let start = origin
@@ -549,7 +549,7 @@ async fn location_links_from_proto(
let target = link.target.ok_or_else(|| anyhow!("missing target"))?;
let buffer = project
.update(&mut cx, |this, cx| {
this.wait_for_buffer(target.buffer_id, cx)
this.wait_for_remote_buffer(target.buffer_id, cx)
})
.await?;
let start = target
@@ -814,7 +814,7 @@ impl LspCommand for GetReferences {
for location in message.locations {
let target_buffer = project
.update(&mut cx, |this, cx| {
this.wait_for_buffer(location.buffer_id, cx)
this.wait_for_remote_buffer(location.buffer_id, cx)
})
.await?;
let start = location

File diff suppressed because it is too large Load Diff

View File

@@ -806,6 +806,55 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
});
}
#[gpui::test]
async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking();
let mut language = Language::new(
LanguageConfig {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
None,
);
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
name: "the-lsp",
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.background());
fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
.await
.unwrap();
// Before restarting the server, report diagnostics with an unknown buffer version.
let fake_server = fake_servers.next().await.unwrap();
fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(),
version: Some(10000),
diagnostics: Vec::new(),
});
cx.foreground().run_until_parked();
project.update(cx, |project, cx| {
project.restart_language_servers_for_buffers([buffer.clone()], cx);
});
let mut fake_server = fake_servers.next().await.unwrap();
let notification = fake_server
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await
.text_document;
assert_eq!(notification.version, 0);
}
#[gpui::test]
async fn test_toggling_enable_language_server(
deterministic: Arc<Deterministic>,

View File

@@ -36,7 +36,7 @@ use std::{
any::Any,
cmp::{self, Ordering},
convert::TryFrom,
ffi::{OsStr, OsString},
ffi::OsStr,
fmt,
future::Future,
mem,
@@ -94,7 +94,7 @@ pub struct Snapshot {
entries_by_path: SumTree<Entry>,
entries_by_id: SumTree<PathEntry>,
scan_id: usize,
is_complete: bool,
completed_scan_id: usize,
}
#[derive(Clone)]
@@ -125,7 +125,6 @@ pub struct LocalSnapshot {
removed_entry_ids: HashMap<u64, ProjectEntryId>,
next_entry_id: Arc<AtomicUsize>,
snapshot: Snapshot,
extension_counts: HashMap<OsString, usize>,
}
impl Clone for LocalSnapshot {
@@ -136,7 +135,6 @@ impl Clone for LocalSnapshot {
removed_entry_ids: self.removed_entry_ids.clone(),
next_entry_id: self.next_entry_id.clone(),
snapshot: self.snapshot.clone(),
extension_counts: self.extension_counts.clone(),
}
}
}
@@ -168,6 +166,7 @@ enum ScanState {
struct ShareState {
project_id: u64,
snapshots_tx: watch::Sender<LocalSnapshot>,
resume_updates: watch::Sender<()>,
_maintain_remote_snapshot: Task<Option<()>>,
}
@@ -231,7 +230,7 @@ impl Worktree {
entries_by_path: Default::default(),
entries_by_id: Default::default(),
scan_id: 0,
is_complete: false,
completed_scan_id: 0,
};
let (updates_tx, mut updates_rx) = mpsc::unbounded();
@@ -345,6 +344,13 @@ impl Worktree {
}
}
pub fn completed_scan_id(&self) -> usize {
match self {
Worktree::Local(worktree) => worktree.snapshot.completed_scan_id,
Worktree::Remote(worktree) => worktree.snapshot.completed_scan_id,
}
}
pub fn is_visible(&self) -> bool {
match self {
Worktree::Local(worktree) => worktree.visible,
@@ -425,9 +431,8 @@ impl LocalWorktree {
entries_by_path: Default::default(),
entries_by_id: Default::default(),
scan_id: 0,
is_complete: true,
completed_scan_id: 0,
},
extension_counts: Default::default(),
};
if let Some(metadata) = metadata {
let entry = Entry::new(
@@ -957,8 +962,9 @@ impl LocalWorktree {
if let Some(old_path) = old_path {
snapshot.remove_path(&old_path);
}
snapshot.scan_started();
inserted_entry = snapshot.insert_entry(entry, fs.as_ref());
snapshot.scan_id += 1;
snapshot.scan_completed();
}
this.poll_snapshot(true, cx);
Ok(inserted_entry)
@@ -969,10 +975,12 @@ impl LocalWorktree {
pub fn share(&mut self, project_id: u64, cx: &mut ModelContext<Worktree>) -> Task<Result<()>> {
let (share_tx, share_rx) = oneshot::channel();
if self.share.is_some() {
let _ = share_tx.send(Ok(()));
if let Some(share) = self.share.as_mut() {
let _ = share_tx.send(());
*share.resume_updates.borrow_mut() = ();
} else {
let (snapshots_tx, mut snapshots_rx) = watch::channel_with(self.snapshot());
let (resume_updates_tx, mut resume_updates_rx) = watch::channel();
let worktree_id = cx.model_id() as u64;
for (path, summary) in self.diagnostic_summaries.iter() {
@@ -985,47 +993,49 @@ impl LocalWorktree {
}
}
let maintain_remote_snapshot = cx.background().spawn({
let rpc = self.client.clone();
let _maintain_remote_snapshot = cx.background().spawn({
let client = self.client.clone();
async move {
let mut prev_snapshot = match snapshots_rx.recv().await {
Some(snapshot) => {
let update = proto::UpdateWorktree {
project_id,
worktree_id,
abs_path: snapshot.abs_path().to_string_lossy().into(),
root_name: snapshot.root_name().to_string(),
updated_entries: snapshot
.entries_by_path
.iter()
.map(Into::into)
.collect(),
removed_entries: Default::default(),
scan_id: snapshot.scan_id as u64,
is_last_update: true,
};
if let Err(error) = send_worktree_update(&rpc, update).await {
let _ = share_tx.send(Err(error));
return Err(anyhow!("failed to send initial update worktree"));
} else {
let _ = share_tx.send(Ok(()));
snapshot
let mut share_tx = Some(share_tx);
let mut prev_snapshot = LocalSnapshot {
ignores_by_parent_abs_path: Default::default(),
git_repositories: Default::default(),
removed_entry_ids: Default::default(),
next_entry_id: Default::default(),
snapshot: Snapshot {
id: WorktreeId(worktree_id as usize),
abs_path: Path::new("").into(),
root_name: Default::default(),
root_char_bag: Default::default(),
entries_by_path: Default::default(),
entries_by_id: Default::default(),
scan_id: 0,
completed_scan_id: 0,
},
};
while let Some(snapshot) = snapshots_rx.recv().await {
#[cfg(any(test, feature = "test-support"))]
const MAX_CHUNK_SIZE: usize = 2;
#[cfg(not(any(test, feature = "test-support")))]
const MAX_CHUNK_SIZE: usize = 256;
let update =
snapshot.build_update(&prev_snapshot, project_id, worktree_id, true);
for update in proto::split_worktree_update(update, MAX_CHUNK_SIZE) {
let _ = resume_updates_rx.try_recv();
while let Err(error) = client.request(update.clone()).await {
log::error!("failed to send worktree update: {}", error);
log::info!("waiting to resume updates");
if resume_updates_rx.next().await.is_none() {
return Ok(());
}
}
}
None => {
share_tx
.send(Err(anyhow!("worktree dropped before share completed")))
.ok();
return Err(anyhow!("failed to send initial update worktree"));
}
};
while let Some(snapshot) = snapshots_rx.recv().await {
send_worktree_update(
&rpc,
snapshot.build_update(&prev_snapshot, project_id, worktree_id, true),
)
.await?;
if let Some(share_tx) = share_tx.take() {
let _ = share_tx.send(());
}
prev_snapshot = snapshot;
}
@@ -1037,15 +1047,13 @@ impl LocalWorktree {
self.share = Some(ShareState {
project_id,
snapshots_tx,
_maintain_remote_snapshot: maintain_remote_snapshot,
resume_updates: resume_updates_tx,
_maintain_remote_snapshot,
});
}
cx.foreground().spawn(async move {
share_rx
.await
.unwrap_or_else(|_| Err(anyhow!("share ended")))
})
cx.foreground()
.spawn(async move { share_rx.await.map_err(|_| anyhow!("share ended")) })
}
pub fn unshare(&mut self) {
@@ -1083,7 +1091,7 @@ impl RemoteWorktree {
}
fn observed_snapshot(&self, scan_id: usize) -> bool {
self.scan_id > scan_id || (self.scan_id == scan_id && self.is_complete)
self.completed_scan_id >= scan_id
}
fn wait_for_snapshot(&mut self, scan_id: usize) -> impl Future<Output = Result<()>> {
@@ -1246,7 +1254,9 @@ impl Snapshot {
self.entries_by_path.edit(entries_by_path_edits, &());
self.entries_by_id.edit(entries_by_id_edits, &());
self.scan_id = update.scan_id as usize;
self.is_complete = update.is_last_update;
if update.is_last_update {
self.completed_scan_id = update.scan_id as usize;
}
Ok(())
}
@@ -1335,6 +1345,14 @@ impl Snapshot {
&self.root_name
}
pub fn scan_started(&mut self) {
self.scan_id += 1;
}
pub fn scan_completed(&mut self) {
self.completed_scan_id = self.scan_id;
}
pub fn scan_id(&self) -> usize {
self.scan_id
}
@@ -1363,10 +1381,6 @@ impl Snapshot {
}
impl LocalSnapshot {
pub fn extension_counts(&self) -> &HashMap<OsString, usize> {
&self.extension_counts
}
// Gives the most specific git repository for a given path
pub(crate) fn repo_for(&self, path: &Path) -> Option<GitRepositoryEntry> {
self.git_repositories
@@ -1462,7 +1476,7 @@ impl LocalSnapshot {
updated_entries,
removed_entries,
scan_id: self.scan_id as u64,
is_last_update: true,
is_last_update: self.completed_scan_id == self.scan_id,
}
}
@@ -1496,9 +1510,9 @@ impl LocalSnapshot {
}
}
self.entries_by_path.insert_or_replace(entry.clone(), &());
let scan_id = self.scan_id;
let removed_entry = self.entries_by_id.insert_or_replace(
self.entries_by_path.insert_or_replace(entry.clone(), &());
self.entries_by_id.insert_or_replace(
PathEntry {
id: entry.id,
path: entry.path.clone(),
@@ -1508,11 +1522,6 @@ impl LocalSnapshot {
&(),
);
if let Some(removed_entry) = removed_entry {
self.dec_extension_count(&removed_entry.path, removed_entry.is_ignored);
}
self.inc_extension_count(&entry.path, entry.is_ignored);
entry
}
@@ -1573,7 +1582,6 @@ impl LocalSnapshot {
for mut entry in entries {
self.reuse_entry_id(&mut entry);
self.inc_extension_count(&entry.path, entry.is_ignored);
entries_by_id_edits.push(Edit::Insert(PathEntry {
id: entry.id,
path: entry.path.clone(),
@@ -1584,33 +1592,7 @@ impl LocalSnapshot {
}
self.entries_by_path.edit(entries_by_path_edits, &());
let removed_entries = self.entries_by_id.edit(entries_by_id_edits, &());
for removed_entry in removed_entries {
self.dec_extension_count(&removed_entry.path, removed_entry.is_ignored);
}
}
fn inc_extension_count(&mut self, path: &Path, ignored: bool) {
if !ignored {
if let Some(extension) = path.extension() {
if let Some(count) = self.extension_counts.get_mut(extension) {
*count += 1;
} else {
self.extension_counts.insert(extension.into(), 1);
}
}
}
}
fn dec_extension_count(&mut self, path: &Path, ignored: bool) {
if !ignored {
if let Some(extension) = path.extension() {
if let Some(count) = self.extension_counts.get_mut(extension) {
*count -= 1;
}
}
}
self.entries_by_id.edit(entries_by_id_edits, &());
}
fn reuse_entry_id(&mut self, entry: &mut Entry) {
@@ -1640,7 +1622,6 @@ impl LocalSnapshot {
.or_insert(entry.id);
*removed_entry_id = cmp::max(*removed_entry_id, entry.id);
entries_by_id_edits.push(Edit::Remove(entry.id));
self.dec_extension_count(&entry.path, entry.is_ignored);
}
self.entries_by_id.edit(entries_by_id_edits, &());
@@ -2010,7 +1991,7 @@ impl File {
})
}
pub fn from_dyn(file: Option<&dyn language::File>) -> Option<&Self> {
pub fn from_dyn(file: Option<&Arc<dyn language::File>>) -> Option<&Self> {
file.and_then(|f| f.as_any().downcast_ref())
}
@@ -2277,7 +2258,8 @@ impl BackgroundScanner {
let is_dir;
let next_entry_id;
{
let snapshot = self.snapshot.lock();
let mut snapshot = self.snapshot.lock();
snapshot.scan_started();
root_char_bag = snapshot.root_char_bag;
root_abs_path = snapshot.abs_path.clone();
root_inode = snapshot.root_entry().map(|e| e.inode);
@@ -2343,6 +2325,8 @@ impl BackgroundScanner {
}
})
.await;
self.snapshot.lock().scan_completed();
}
Ok(())
@@ -2470,7 +2454,8 @@ impl BackgroundScanner {
let root_abs_path;
let next_entry_id;
{
let snapshot = self.snapshot.lock();
let mut snapshot = self.snapshot.lock();
snapshot.scan_started();
root_char_bag = snapshot.root_char_bag;
root_abs_path = snapshot.abs_path.clone();
next_entry_id = snapshot.next_entry_id.clone();
@@ -2495,7 +2480,6 @@ impl BackgroundScanner {
let (scan_queue_tx, scan_queue_rx) = channel::unbounded();
{
let mut snapshot = self.snapshot.lock();
snapshot.scan_id += 1;
for event in &events {
if let Ok(path) = event.path.strip_prefix(&root_canonical_path) {
snapshot.remove_path(path);
@@ -2582,6 +2566,7 @@ impl BackgroundScanner {
self.update_ignore_statuses().await;
self.update_git_repositories();
self.snapshot.lock().scan_completed();
true
}
@@ -2976,19 +2961,6 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
}
}
async fn send_worktree_update(client: &Arc<Client>, update: proto::UpdateWorktree) -> Result<()> {
#[cfg(any(test, feature = "test-support"))]
const MAX_CHUNK_SIZE: usize = 2;
#[cfg(not(any(test, feature = "test-support")))]
const MAX_CHUNK_SIZE: usize = 256;
for update in proto::split_worktree_update(update, MAX_CHUNK_SIZE) {
client.request(update).await?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -3479,9 +3451,8 @@ mod tests {
root_name: Default::default(),
root_char_bag: Default::default(),
scan_id: 0,
is_complete: true,
completed_scan_id: 0,
},
extension_counts: Default::default(),
};
initial_snapshot.insert_entry(
Entry::new(
@@ -3763,15 +3734,6 @@ mod tests {
.entry_for_path(ignore_parent_path.join(&*GITIGNORE))
.is_some());
}
// Ensure extension counts are correct.
let mut expected_extension_counts = HashMap::default();
for extension in self.entries(false).filter_map(|e| e.path.extension()) {
*expected_extension_counts
.entry(extension.into())
.or_insert(0) += 1;
}
assert_eq!(self.extension_counts, expected_extension_counts);
}
fn to_vec(&self, include_ignored: bool) -> Vec<(&Path, u64, bool)> {

View File

@@ -10,7 +10,8 @@ use gpui::{
MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
},
geometry::vector::Vector2F,
impl_internal_actions, keymap,
impl_internal_actions,
keymap_matcher::KeymapContext,
platform::CursorStyle,
AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MouseButton,
MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
@@ -762,7 +763,6 @@ impl ProjectPanel {
ix += 1;
}
self.clipboard_entry.take();
if clipboard_entry.is_cut() {
if let Some(task) = self.project.update(cx, |project, cx| {
project.rename_entry(clipboard_entry.entry_id(), new_path, cx)
@@ -1175,13 +1175,15 @@ impl ProjectPanel {
)
})
.on_click(MouseButton::Left, move |e, cx| {
if kind == EntryKind::Dir {
cx.dispatch_action(ToggleExpanded(entry_id))
} else {
cx.dispatch_action(Open {
entry_id,
change_focus: e.click_count > 1,
})
if !show_editor {
if kind == EntryKind::Dir {
cx.dispatch_action(ToggleExpanded(entry_id))
} else {
cx.dispatch_action(Open {
entry_id,
change_focus: e.click_count > 1,
})
}
}
})
.on_down(MouseButton::Right, move |e, cx| {
@@ -1301,7 +1303,7 @@ impl View for ProjectPanel {
.boxed()
}
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.set.insert("menu".into());
cx

View File

@@ -146,6 +146,7 @@ impl PickerDelegate for RecentProjectsView {
.matches
.iter()
.enumerate()
.rev()
.max_by_key(|(_, m)| OrderedFloat(m.score))
.map(|(ix, _)| ix)
.unwrap_or(0);

View File

@@ -21,6 +21,8 @@ message Envelope {
CreateRoomResponse create_room_response = 10;
JoinRoom join_room = 11;
JoinRoomResponse join_room_response = 12;
RejoinRoom rejoin_room = 108;
RejoinRoomResponse rejoin_room_response = 109;
LeaveRoom leave_room = 13;
Call call = 14;
IncomingCall incoming_call = 15;
@@ -37,6 +39,7 @@ message Envelope {
JoinProjectResponse join_project_response = 25;
LeaveProject leave_project = 26;
AddProjectCollaborator add_project_collaborator = 27;
UpdateProjectCollaborator update_project_collaborator = 110;
RemoveProjectCollaborator remove_project_collaborator = 28;
GetDefinition get_definition = 29;
@@ -76,6 +79,8 @@ message Envelope {
BufferReloaded buffer_reloaded = 61;
ReloadBuffers reload_buffers = 62;
ReloadBuffersResponse reload_buffers_response = 63;
SynchronizeBuffers synchronize_buffers = 200;
SynchronizeBuffersResponse synchronize_buffers_response = 201;
FormatBuffers format_buffers = 64;
FormatBuffersResponse format_buffers_response = 65;
GetCompletions get_completions = 66;
@@ -161,6 +166,40 @@ message JoinRoomResponse {
optional LiveKitConnectionInfo live_kit_connection_info = 2;
}
message RejoinRoom {
uint64 id = 1;
repeated UpdateProject reshared_projects = 2;
repeated RejoinProject rejoined_projects = 3;
}
message RejoinProject {
uint64 id = 1;
repeated RejoinWorktree worktrees = 2;
}
message RejoinWorktree {
uint64 id = 1;
uint64 scan_id = 2;
}
message RejoinRoomResponse {
Room room = 1;
repeated ResharedProject reshared_projects = 2;
repeated RejoinedProject rejoined_projects = 3;
}
message ResharedProject {
uint64 id = 1;
repeated Collaborator collaborators = 2;
}
message RejoinedProject {
uint64 id = 1;
repeated WorktreeMetadata worktrees = 2;
repeated Collaborator collaborators = 3;
repeated LanguageServer language_servers = 4;
}
message LeaveRoom {}
message Room {
@@ -322,6 +361,12 @@ message AddProjectCollaborator {
Collaborator collaborator = 2;
}
message UpdateProjectCollaborator {
uint64 project_id = 1;
PeerId old_peer_id = 2;
PeerId new_peer_id = 3;
}
message RemoveProjectCollaborator {
uint64 project_id = 1;
PeerId peer_id = 2;
@@ -494,6 +539,20 @@ message ReloadBuffersResponse {
ProjectTransaction transaction = 1;
}
message SynchronizeBuffers {
uint64 project_id = 1;
repeated BufferVersion buffers = 2;
}
message SynchronizeBuffersResponse {
repeated BufferVersion buffers = 1;
}
message BufferVersion {
uint64 id = 1;
repeated VectorClockEntry version = 2;
}
enum FormatTrigger {
Save = 0;
Manual = 1;
@@ -916,6 +975,9 @@ message BufferState {
string base_text = 3;
optional string diff_base = 4;
LineEnding line_ending = 5;
repeated VectorClockEntry saved_version = 6;
string saved_version_fingerprint = 7;
Timestamp saved_mtime = 8;
}
message BufferChunk {

View File

@@ -23,7 +23,7 @@ pub fn random_token() -> String {
for byte in token_bytes.iter_mut() {
*byte = rng.gen();
}
base64::encode_config(&token_bytes, base64::URL_SAFE)
base64::encode_config(token_bytes, base64::URL_SAFE)
}
impl PublicKey {

View File

@@ -494,6 +494,27 @@ impl Peer {
Ok(())
}
pub fn respond_with_unhandled_message(
&self,
envelope: Box<dyn AnyTypedEnvelope>,
) -> Result<()> {
let connection = self.connection_state(envelope.sender_id())?;
let response = proto::Error {
message: format!("message {} was not handled", envelope.payload_type_name()),
};
let message_id = connection
.next_message_id
.fetch_add(1, atomic::Ordering::SeqCst);
connection
.outgoing_tx
.unbounded_send(proto::Message::Envelope(response.into_envelope(
message_id,
Some(envelope.message_id()),
None,
)))?;
Ok(())
}
fn connection_state(&self, connection_id: ConnectionId) -> Result<ConnectionState> {
let connections = self.connections.read();
let connection = connections

View File

@@ -42,6 +42,8 @@ pub trait AnyTypedEnvelope: 'static + Send + Sync {
fn into_any(self: Box<Self>) -> Box<dyn Any + Send + Sync>;
fn is_background(&self) -> bool;
fn original_sender_id(&self) -> Option<PeerId>;
fn sender_id(&self) -> ConnectionId;
fn message_id(&self) -> u32;
}
pub enum MessagePriority {
@@ -73,6 +75,14 @@ impl<T: EnvelopedMessage> AnyTypedEnvelope for TypedEnvelope<T> {
fn original_sender_id(&self) -> Option<PeerId> {
self.original_sender_id
}
fn sender_id(&self) -> ConnectionId {
self.sender_id
}
fn message_id(&self) -> u32 {
self.message_id
}
}
impl PeerId {
@@ -188,6 +198,8 @@ messages!(
(PrepareRename, Background),
(PrepareRenameResponse, Background),
(ProjectEntryResponse, Foreground),
(RejoinRoom, Foreground),
(RejoinRoomResponse, Foreground),
(RemoveContact, Foreground),
(ReloadBuffers, Foreground),
(ReloadBuffersResponse, Foreground),
@@ -205,6 +217,8 @@ messages!(
(ShareProjectResponse, Foreground),
(ShowContacts, Foreground),
(StartLanguageServer, Foreground),
(SynchronizeBuffers, Foreground),
(SynchronizeBuffersResponse, Foreground),
(Test, Foreground),
(Unfollow, Foreground),
(UnshareProject, Foreground),
@@ -217,6 +231,7 @@ messages!(
(UpdateLanguageServer, Foreground),
(UpdateParticipantLocation, Foreground),
(UpdateProject, Foreground),
(UpdateProjectCollaborator, Foreground),
(UpdateWorktree, Foreground),
(UpdateDiffBase, Background),
(GetPrivateUserInfo, Foreground),
@@ -254,6 +269,7 @@ request_messages!(
(JoinChannel, JoinChannelResponse),
(JoinProject, JoinProjectResponse),
(JoinRoom, JoinRoomResponse),
(RejoinRoom, RejoinRoomResponse),
(IncomingCall, Ack),
(OpenBufferById, OpenBufferResponse),
(OpenBufferByPath, OpenBufferResponse),
@@ -270,6 +286,7 @@ request_messages!(
(SearchProject, SearchProjectResponse),
(SendChannelMessage, SendChannelMessageResponse),
(ShareProject, ShareProjectResponse),
(SynchronizeBuffers, SynchronizeBuffersResponse),
(Test, Test),
(UpdateBuffer, Ack),
(UpdateParticipantLocation, Ack),
@@ -311,6 +328,7 @@ entity_messages!(
SaveBuffer,
SearchProject,
StartLanguageServer,
SynchronizeBuffers,
Unfollow,
UnshareProject,
UpdateBuffer,
@@ -319,6 +337,7 @@ entity_messages!(
UpdateFollowers,
UpdateLanguageServer,
UpdateProject,
UpdateProjectCollaborator,
UpdateWorktree,
UpdateDiffBase
);

View File

@@ -6,4 +6,4 @@ pub use conn::Connection;
pub use peer::*;
mod macros;
pub const PROTOCOL_VERSION: u32 = 44;
pub const PROTOCOL_VERSION: u32 = 46;

View File

@@ -106,73 +106,79 @@ impl View for BufferSearchBar {
.with_child(
Flex::row()
.with_child(
ChildView::new(&self.query_editor, cx)
.aligned()
.left()
.flex(1., true)
.boxed(),
)
.with_children(self.active_searchable_item.as_ref().and_then(
|searchable_item| {
let matches = self
.seachable_items_with_matches
.get(&searchable_item.downgrade())?;
let message = if let Some(match_ix) = self.active_match_index {
format!("{}/{}", match_ix + 1, matches.len())
} else {
"No matches".to_string()
};
Some(
Label::new(message, theme.search.match_index.text.clone())
.contained()
.with_style(theme.search.match_index.container)
Flex::row()
.with_child(
ChildView::new(&self.query_editor, cx)
.aligned()
.left()
.flex(1., true)
.boxed(),
)
},
))
.contained()
.with_style(editor_container)
.aligned()
.constrained()
.with_min_width(theme.search.editor.min_width)
.with_max_width(theme.search.editor.max_width)
.flex(1., false)
.boxed(),
)
.with_child(
Flex::row()
.with_child(self.render_nav_button("<", Direction::Prev, cx))
.with_child(self.render_nav_button(">", Direction::Next, cx))
.aligned()
.boxed(),
)
.with_child(
Flex::row()
.with_children(self.render_search_option(
supported_options.case,
"Case",
SearchOption::CaseSensitive,
cx,
))
.with_children(self.render_search_option(
supported_options.word,
"Word",
SearchOption::WholeWord,
cx,
))
.with_children(self.render_search_option(
supported_options.regex,
"Regex",
SearchOption::Regex,
cx,
))
.contained()
.with_style(theme.search.option_button_group)
.aligned()
.with_children(self.active_searchable_item.as_ref().and_then(
|searchable_item| {
let matches = self
.seachable_items_with_matches
.get(&searchable_item.downgrade())?;
let message = if let Some(match_ix) = self.active_match_index {
format!("{}/{}", match_ix + 1, matches.len())
} else {
"No matches".to_string()
};
Some(
Label::new(message, theme.search.match_index.text.clone())
.contained()
.with_style(theme.search.match_index.container)
.aligned()
.boxed(),
)
},
))
.contained()
.with_style(editor_container)
.aligned()
.constrained()
.with_min_width(theme.search.editor.min_width)
.with_max_width(theme.search.editor.max_width)
.flex(1., false)
.boxed(),
)
.with_child(
Flex::row()
.with_child(self.render_nav_button("<", Direction::Prev, cx))
.with_child(self.render_nav_button(">", Direction::Next, cx))
.aligned()
.boxed(),
)
.with_child(
Flex::row()
.with_children(self.render_search_option(
supported_options.case,
"Case",
SearchOption::CaseSensitive,
cx,
))
.with_children(self.render_search_option(
supported_options.word,
"Word",
SearchOption::WholeWord,
cx,
))
.with_children(self.render_search_option(
supported_options.regex,
"Regex",
SearchOption::Regex,
cx,
))
.contained()
.with_style(theme.search.option_button_group)
.aligned()
.boxed(),
)
.flex(1., true)
.boxed(),
)
.with_child(self.render_close_button(&theme.search, cx))
.contained()
.with_style(theme.search.container)
.named("search bar")
@@ -325,7 +331,7 @@ impl BufferSearchBar {
let is_active = self.is_search_option_enabled(option);
Some(
MouseEventHandler::<Self>::new(option as usize, cx, |state, cx| {
let style = &cx
let style = cx
.global::<Settings>()
.theme
.search
@@ -373,7 +379,7 @@ impl BufferSearchBar {
enum NavButton {}
MouseEventHandler::<NavButton>::new(direction as usize, cx, |state, cx| {
let style = &cx
let style = cx
.global::<Settings>()
.theme
.search
@@ -399,6 +405,38 @@ impl BufferSearchBar {
.boxed()
}
fn render_close_button(
&self,
theme: &theme::Search,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let action = Box::new(Dismiss);
let tooltip = "Dismiss Buffer Search";
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
enum CloseButton {}
MouseEventHandler::<CloseButton>::new(0, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false);
Svg::new("icons/x_mark_8.svg")
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.constrained()
.with_width(style.button_width)
.contained()
.with_style(style.container)
.boxed()
})
.on_click(MouseButton::Left, {
let action = action.boxed_clone();
move |_, cx| cx.dispatch_any_action(action.boxed_clone())
})
.with_cursor_style(CursorStyle::PointingHand)
.with_tooltip::<CloseButton, _>(0, tooltip.to_string(), Some(action), tooltip_style, cx)
.boxed()
}
fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {

View File

@@ -2,7 +2,7 @@ use crate::{parse_json_with_comments, Settings};
use anyhow::{Context, Result};
use assets::Assets;
use collections::BTreeMap;
use gpui::{keymap::Binding, MutableAppContext};
use gpui::{keymap_matcher::Binding, MutableAppContext};
use schemars::{
gen::{SchemaGenerator, SchemaSettings},
schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},

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