Compare commits

...

430 Commits

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

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-01-26 19:01:51 +02:00
Antonio Scandurra
eca6115e4b Ensure proto::UpdateWorktree::removed_entries doesn't exceed chunk size
This was causing the database to panic because we were trying to remove too
many entries at once.
2023-01-26 17:26:31 +01:00
Antonio Scandurra
74aeec360d Cancel pending call when participant leaves room after a reconnection
Previously, if a user temporarily disconnected while there was a pending
call, we would fail to cancel such pending call when the caller left the
room. This was due to the caller reconnecting and having a different connection
id than the one originally used to initiate the call.
2023-01-26 16:44:55 +01:00
Petros Amoiridis
2f26fcd889 Merge branch 'main' into 2064-remove-contacts 2023-01-26 16:34:17 +02:00
Joseph T. Lyons
a4d9d6c750 Merge pull request #2095 from zed-industries/fix-crash-when-opening-feedback-while-in-call
Fix crash when opening feedback while in call
2023-01-25 21:16:12 -05:00
Max Brunsfeld
a2a3ebc42f Merge pull request #2096 from zed-industries/lazy-load-languages
Load languages lazily in the background
2023-01-25 18:09:45 -08:00
Max Brunsfeld
ddf4e1a316 Load languages lazily in the background 2023-01-25 17:47:46 -08:00
Kay Simmons
a369fb8033 better but still broken 2023-01-25 17:05:57 -08:00
Joseph Lyons
9ff34bcb6a Remove no-longer-needed method 2023-01-25 20:03:44 -05:00
Julia
10f130ee30 Merge pull request #2094 from zed-industries/project-lost-window-close-action-shortcut-accessibility
Add "Close Window" global action which does not need a focused workspace
2023-01-25 18:58:22 -05:00
Julia
3819a67185 Add "Close Window" global action which does not need a focused workspace 2023-01-25 18:51:25 -05:00
Joseph Lyons
6e7101ca6b Fix crash when opening feedback while in call 2023-01-25 17:48:01 -05:00
Julia
2df2d09e3c Merge pull request #2091 from zed-industries/style
Style
2023-01-25 15:22:52 -05:00
Joseph Lyons
4c3244b982 v0.72.x dev 2023-01-25 15:20:41 -05:00
Julia
a79b4e312b Style
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-01-25 15:09:57 -05:00
Kay Simmons
5eac797a93 mostly working now 2023-01-25 11:36:38 -08:00
Kay Simmons
a581d0c5b8 wip 2023-01-25 11:32:19 -08:00
Kay Simmons
15799f7af6 wip 2023-01-25 11:32:19 -08:00
Joseph T. Lyons
81ed961659 Merge pull request #2088 from zed-industries/add-cursor-position-to-feedback-editor
Add cursor position to feedback editor
2023-01-25 14:29:24 -05:00
Max Brunsfeld
9db55b3029 Merge pull request #2087 from zed-industries/buffer-language-registry
Assign the language registry to all buffers in the project
2023-01-25 11:25:40 -08:00
Joseph Lyons
328b779185 Clean up construction of FeedbackEditor 2023-01-25 14:20:58 -05:00
Joseph Lyons
7f3d937938 Count chars 2023-01-25 14:20:40 -05:00
Joseph Lyons
f68f9f37ab Add cursor position to feedback editor
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
2023-01-25 14:20:23 -05:00
Julia
c22d13286d Merge pull request #2085 from zed-industries/cleanup-debug-printing
Clean up some debug printing
2023-01-25 14:18:43 -05:00
Joseph Lyons
44c7f162b6 Merge branch 'main' into add-cursor-position-to-feedback-editor 2023-01-25 14:01:45 -05:00
Max Brunsfeld
7003a475a7 Assign the language registry to all buffers in the project 2023-01-25 10:44:15 -08:00
Julia
3d8dbee76a Clean up some debug printing 2023-01-25 13:37:04 -05:00
Petros Amoiridis
160870c9de Improve user notification
The message is not really true. When one declines, the other person can notice that the contact request  is not pending any more. They will know. Switching to not alerted is closer to what is really happening.
2023-01-25 19:46:51 +02:00
Mikayla Maki
ba6ffd8256 Merge pull request #2081 from zed-industries/fix-failing-ci
Fixes a broken conditional that is only caught on darwin systems
2023-01-25 09:45:30 -08:00
Mikayla Maki
ecb7d1072f Fixes a broken conditional that is only caught on darwin systems 2023-01-25 09:33:07 -08:00
Mikayla Maki
38b83a70aa Merge pull request #2078 from zed-industries/fix-cursor-style
Fix cursor style thrashing on overlapping windows
2023-01-25 09:15:55 -08:00
Mikayla Maki
1fc6276eab Remove debug wiring 2023-01-25 09:10:51 -08:00
Mikayla Maki
45e4e3354e Changed the presenter to only send 'set_cursor_style' on the topmost window
co-authored-by: Antonio <antonio@zed.dev>
2023-01-25 09:10:35 -08:00
Mikayla Maki
27a80a1c94 WIP 2023-01-25 09:10:35 -08:00
Mikayla Maki
426aeb7c5e WIP - adds platform APIs for checking the top most window 2023-01-25 09:10:35 -08:00
Petros Amoiridis
35524db136 Add a confirmation prompt
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-01-25 18:55:08 +02:00
Petros Amoiridis
e928c1c61e Test removing a contact
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-01-25 17:31:42 +02:00
Petros Amoiridis
5d4eb2b7ae Push responder and requester to remove_contacts
When we ask the server to remove a contact we need to push the requester and responder ids to `remove_contacts` so that when the UI updates, the correct contacts will disappear from the list.

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

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-01-25 13:10:29 +02:00
Joseph Lyons
3329b2bbd6 Remove gpui:: prefix from parameters 2023-01-24 19:46:04 -05:00
Joseph T. Lyons
a66a0cfd70 Merge pull request #2075 from zed-industries/add-upper-character-count-limit
Add upper character count limit
2023-01-24 19:44:19 -05:00
Julia
27ee994e17 Merge pull request #2074 from zed-industries/decode-openurl-to-pathbuf
Decode URL from `openURLs` to handle percent encoded paths
2023-01-24 19:08:17 -05:00
Julia
0414723a54 Decode URL from openURLs to handle percent encoded paths
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-01-24 18:48:15 -05:00
Joseph Lyons
588419492a Add upper character count limit 2023-01-24 17:38:20 -05:00
Max Brunsfeld
52296836fe Merge pull request #2069 from zed-industries/markdown-fenced-blocks
Support syntax highlighting in Markdown fenced code blocks
2023-01-24 14:19:36 -08:00
Max Brunsfeld
678ee26c5e Merge branch 'main' into markdown-fenced-blocks 2023-01-24 14:13:50 -08:00
Julia
29d67452e0 Merge pull request #2072 from zed-industries/os-file-associations
Insert macOS file association metadata during bundle process
2023-01-24 17:09:41 -05:00
Max Brunsfeld
51984f0d39 Fix feedback editor compile error due to LanguageRegistry API change 2023-01-24 14:09:24 -08:00
Julia
4d73d4b1b9 Insert macOS file association metadata during bundle process 2023-01-24 17:07:02 -05:00
Nathan Sobo
e8cea130a4 Merge pull request #2068 from zed-industries/doc-reparse
Document Buffer::reparse
2023-01-24 09:09:38 -07:00
Antonio Scandurra
dff08d3cfe Merge branch 'main' into markdown-fenced-blocks 2023-01-24 15:43:35 +01:00
Antonio Scandurra
c48e3f3d05 Reparse unknown injection ranges in buffer when adding a new language 2023-01-24 15:29:59 +01:00
Antonio Scandurra
f3509824e8 WIP: Start on SyntaxMapSnapshot::unknown_injection_languages 2023-01-24 12:55:49 +01:00
Antonio Scandurra
14c72cac58 Store syntax layers even if a language for the injection can't be found 2023-01-24 12:25:12 +01:00
Joseph T. Lyons
f95bda64ba Merge pull request #2009 from zed-industries/in-app-feedback
In app feedback
2023-01-24 01:05:05 -05:00
Nathan Sobo
96ffe84edb Document Buffer::reparse 2023-01-23 21:51:10 -07:00
Joseph Lyons
2b3d09f70a Fix CI missing license check 2023-01-23 18:34:10 -05:00
Joseph Lyons
8e8f66a5e1 Merge branch 'main' into in-app-feedback 2023-01-23 18:24:12 -05:00
Joseph Lyons
c9299a49e1 Clean out unused code 2023-01-23 18:19:10 -05:00
Mikayla Maki
9f048a4b1c Merge pull request #2044 from zed-industries/licensing-scripts
Licensing scripts
2023-01-23 12:58:27 -08:00
Mikayla Maki
0f0d5d5726 Added cargo-about auto-install and CI steps 2023-01-23 12:51:32 -08:00
Mikayla Maki
d060114f00 Added complete scripts for generating third party license files 2023-01-23 12:47:12 -08:00
Mikayla Maki
9d58032064 Add action to open licenses file 2023-01-23 12:45:18 -08:00
Mikayla Maki
4609be20de WIP: Adding license compliance to CI 2023-01-23 12:43:42 -08:00
Mikayla Maki
4d05d61ed7 Merge pull request #2049 from zed-industries/425-create-file-for-cli
Create files passed as args to CLI
2023-01-23 10:44:55 -08:00
Antonio Scandurra
8dabdd1baa Ensure injection layer is recomputed when language changes
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-01-23 19:02:06 +01:00
Julia
4678f6e0a5 Merge pull request #2063 from zed-industries/active-tab-close-icon-pointing-hand
Avoid stomping on tab close icon's cursor style
2023-01-23 11:48:35 -05:00
Julia
95b259b841 Avoid stomping on tab close icon's cursor style 2023-01-23 11:43:50 -05:00
Antonio Scandurra
79cf6fb8b6 WIP: Add test for dynamic language injection 2023-01-23 09:45:36 +01:00
Antonio Scandurra
cb610f37f2 WIP: Search language injections also by file extension
There are still a few things left:

1. Add test to verify we can successfully locate a language by its extension
2. Add test to reproduce bug where changing the fenced code block language
   won't reparse the block with the new language
3. Reparse injections for which we couldn't find a language when the language
   registry changes.
4. Check why the markdown grammar considers the trailing triple backtick as
   `(code_block_content)`, as opposed to being part of the outer markdown.
2023-01-23 08:56:41 +01:00
Antonio Scandurra
36e4dcef16 Avoid allocating a string to compare language names 2023-01-23 08:56:41 +01:00
Antonio Scandurra
c49dc8d6e5 Rename LanguageRegistry::get_language to language_for_name 2023-01-23 08:56:41 +01:00
Antonio Scandurra
f086fa3f21 Add syntax injections for Markdown fenced code blocks 2023-01-23 08:56:41 +01:00
Joseph Lyons
c118f9aabd Fix new errors after merge 2023-01-23 01:31:02 -05:00
Joseph Lyons
f2a5a4d0fd Merge branch 'main' into in-app-feedback 2023-01-23 01:20:10 -05:00
Joseph Lyons
fb2278dc6d Complete first iteration of in-app feedback 2023-01-23 00:59:46 -05:00
Mikayla Maki
50d37e1ae7 Merge pull request #2060 from zed-industries/fix-ci-fail
Fix mismatched return types on CI
2023-01-20 18:28:59 -08:00
Mikayla Maki
8dcaa81aad switch return type of accepts_first_mouse 2023-01-20 18:19:24 -08:00
Max Brunsfeld
e1a58e9381 Merge pull request #2059 from zed-industries/no-indent-adjustment-on-error
Avoid adjusting indentation of lines inside of newly-created errors
2023-01-20 17:13:30 -08:00
Max Brunsfeld
56080771e6 Add test for avoiding indent adjustment inside newly-created errors 2023-01-20 17:02:38 -08:00
Mikayla Maki
bb24f1142f Removed dbg 2023-01-20 16:47:23 -08:00
Mikayla Maki
94b2f8e07f Merge pull request #2054 from zed-industries/notification-mouse-events
Notification mouse events
2023-01-20 16:41:27 -08:00
Mikayla Maki
310d867aab Switch PopUp windows to use the NSTrackingArea API and add support for the mouseExited event
Co-authored-by: Antonio <antonio@zed.dev>
2023-01-20 16:35:25 -08:00
Max Brunsfeld
9f74d6e4ac Highlight and auto-indent await expressions in rust 2023-01-20 15:56:56 -08:00
Max Brunsfeld
f7ceebfce3 Avoid adjusting indentation of lines inside newly-created errors 2023-01-20 15:56:45 -08:00
Joseph Lyons
083986dfae WIP
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-01-20 18:05:24 -05:00
Max Brunsfeld
df1e1295e3 Merge pull request #2056 from zed-industries/confirm-quit
Add confirm_quit setting
2023-01-20 14:11:02 -08:00
Joseph Lyons
c1934d6232 WIP 2023-01-20 16:56:56 -05:00
Kay Simmons
4bee273511 Merge pull request #2057 from zed-industries/multiple-definitions-multibuffer
Multiple Definitions Multibuffer
2023-01-20 13:49:07 -08:00
Kay Simmons
2e37c0ea4a Open multiple definitions in a multibuffer instead of opening the files directly 2023-01-20 13:28:13 -08:00
Max Brunsfeld
2f42af2ac3 Add confirm_quit setting 2023-01-20 13:02:38 -08:00
Max Brunsfeld
be2c601176 Merge pull request #2055 from zed-industries/language-config-overrides
Language config overrides
2023-01-20 11:15:26 -08:00
Max Brunsfeld
8dcef46842 Drop 'override.' prefix from capture names in override query
Co-authored-by: Julia Risley <julia@zed.dev>
2023-01-20 10:44:33 -08:00
Max Brunsfeld
2aa7a9e95b Add overrides for all languages
Co-authored-by: Julia Risley <julia@zed.dev>
2023-01-20 10:39:31 -08:00
Mikayla Maki
8af1294ba5 Changed platform mouse moved handling to only fire on active or popup windows
co-authored-by: Antonio <antonio@zed.dev>
2023-01-20 09:37:09 -08:00
Mikayla Maki
5a00729fad Merge pull request #2051 from zed-industries/show-following-to-followed
Show following to followed
2023-01-20 09:23:34 -08:00
Mikayla Maki
97203e1e02 Fix broken merge 2023-01-20 09:19:58 -08:00
Mikayla Maki
95e661a78c Switched from active hover to NSViews acceptsFirstMouse API
Co-authored-by: Nathan <nathan@zed.dev>
2023-01-20 09:14:38 -08:00
Julia
b54b77b9ec Merge pull request #2053 from zed-industries/on-move-out
Hide hovers when mouse leaves area & window focus is lost
2023-01-20 10:55:26 -05:00
Julia
467e3dc50a Hide editor hover on mouse move out & always notify when hiding hover
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-01-20 10:16:24 -05:00
Julia
131f3471fc Don't dispatch mousemove without focus & avoid swallowing external moves
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-01-20 10:11:28 -05:00
Mikayla Maki
88170df7f0 Switched from active hover to NSViews acceptsFirstMouse API 2023-01-19 15:21:26 -08:00
Max Brunsfeld
2967b46a17 Implement scope-specific bracket matching and comment toggling
Co-authored-by: Julia Risley <julia@zed.dev>
2023-01-19 15:04:27 -08:00
Mikayla Maki
4eeb1aec50 Adds UI for showing the followed-by status to collaboration 2023-01-19 14:22:12 -08:00
Max Brunsfeld
1851e2e77c Start work on language config overrides
Co-authored-by: Julia Risley <julia@zed.dev>
2023-01-19 12:32:08 -08:00
Mikayla Maki
4a46227909 Change incoming call notification to only require one click 2023-01-19 11:43:46 -08:00
Mikayla Maki
86371d9f5e Merge pull request #2050 from zed-industries/disable-soft-wrap-in-single-line-editors
Disable soft wrap in single line editors
2023-01-19 11:26:54 -08:00
Joseph Lyons
38476f5429 Disable soft wrap in single line editors
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-01-19 13:45:05 -05:00
Petros Amoiridis
6c9422808a Merge pull request #2048 from zed-industries/408-add-date-to-zedlog
Add date to the log format
2023-01-19 20:26:01 +02:00
Petros Amoiridis
d30e129d63 Create files passed as args to CLI
Co-Authored-by: Mikayla <mikayla@zed.dev>
2023-01-19 19:38:05 +02:00
Julia
ad1947fa50 Add in-window on-move-out mouse handler concept 2023-01-19 12:34:13 -05:00
Petros Amoiridis
f088de5947 Add date to the log format
Co-Authored-By: Mikayla <mikayla@zed.dev>
2023-01-19 19:05:17 +02:00
Antonio Scandurra
c85ad96b45 Merge pull request #2047 from zed-industries/optimize-large-multi-buffers
Avoid stalling the UI thread when running large searches
2023-01-19 17:31:14 +01:00
Antonio Scandurra
1f649e52de Document RopeFingerprint 2023-01-19 17:25:59 +01:00
Antonio Scandurra
0a7111d216 Fix tests 2023-01-19 16:26:27 +01:00
Antonio Scandurra
a58b39f884 Merge branch 'main' into optimize-large-multi-buffers 2023-01-19 16:18:21 +01:00
Antonio Scandurra
c124caeb0d Add test for stream_excerpts_with_context_lines 2023-01-19 15:54:32 +01:00
Antonio Scandurra
5ce065ac92 Introduce MultiBuffer::stream_excerpts_with_context_lines
This allows us to push excerpts in a streaming fashion without blocking
the main thread.
2023-01-19 15:42:14 +01:00
Max Brunsfeld
5189dea3d5 Merge pull request #2046 from zed-industries/line-breaks-in-outline-items
Prevent outline items from accidentally spanning multiple lines
2023-01-18 16:46:45 -08:00
Max Brunsfeld
d9948bf772 Prevent outline items from accidentally spanning multiple lines 2023-01-18 16:43:18 -08:00
Max Brunsfeld
062e7a03a9 Update comments in Pane::close_items 2023-01-18 15:17:44 -08:00
Max Brunsfeld
17b4bfdf98 Merge pull request #2045 from zed-industries/fewer-unsaved-prompts
Avoid prompting to save when closing an untitled buffer that is still open elsewhere
2023-01-18 15:10:19 -08:00
Max Brunsfeld
06c31a0daa Fix workspace tests after changing Item trait 2023-01-18 15:00:40 -08:00
Mikayla Maki
203f569f2e collab 0.5.3 2023-01-18 12:52:58 -08:00
Mikayla Maki
b0fb5913b6 v0.71.x dev 2023-01-18 12:39:38 -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
Antonio Scandurra
a8f466b422 Don't starve the main thread adding too many search excerpts at once 2023-01-18 14:22:23 +01: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
Antonio Scandurra
8ca0f9ac99 Fix compile errors 2023-01-18 13:58:01 +01:00
Antonio Scandurra
a653e87658 WIP: Avoid converting RopeFingerprint into a string
Co-Authored-By: Petros Amoiridis <petros@zed.dev>
2023-01-18 12:22:08 +01:00
Joseph Lyons
bec03dc882 WIP 2023-01-18 00:12:52 -05: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
Max Brunsfeld
a0a50cb412 Set up fake project paths correctly in tests 2023-01-17 17:40:34 -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
Max Brunsfeld
8651320c9f Make workspace items expose their underlying models, remove file-related methods 2023-01-17 17:21:06 -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
Max Brunsfeld
292708573f Replace MultiBuffer::files with ::for_each_buffer 2023-01-17 16:16:44 -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
Joseph Lyons
5517e743e1 Merge branch 'main' into in-app-feedback 2023-01-09 14:05:30 -05:00
Joseph Lyons
c1e61b479c Move feedback items into a feedback crate 2023-01-09 13:55:06 -05: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
Joseph Lyons
a73e264c3d Merge branch 'in-app-feedback' of https://github.com/zed-industries/zed into in-app-feedback 2023-01-07 18:53:11 -05:00
Joseph Lyons
0200fc5542 WIP
Don't rely on contacts popover or contacts list for theming
Add metrics id to request body
Clean up some code and comments

Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-01-07 18:53:00 -05:00
Joseph Lyons
9694771752 Move notes into PR 2023-01-07 18:53:00 -05:00
Joseph Lyons
9fc7f54631 Add to TODO 2023-01-07 18:53:00 -05:00
Joseph Lyons
1545b2ac61 Update TODO 2023-01-07 18:53:00 -05:00
Joseph Lyons
318a0b7ed0 In-app feedback WIP 2023-01-07 18:53:00 -05: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
Joseph Lyons
5387695ee0 WIP
Don't rely on contacts popover or contacts list for theming
Add metrics id to request body
Clean up some code and comments

Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-01-06 17:40:30 -05:00
Kay Simmons
73e7967a12 working f and t bindings 2023-01-06 14:24:20 -08:00
Joseph Lyons
9d4cf2ff62 Move notes into PR 2023-01-06 15:41:31 -05: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
Joseph Lyons
658541ec9f Add to TODO 2023-01-06 15:32:28 -05: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
Joseph Lyons
404f59090c Update TODO 2023-01-05 18:14:28 -05:00
Joseph Lyons
eb02834582 In-app feedback WIP 2023-01-05 17:58:52 -05: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
Max Brunsfeld
c8b209306e collab 0.4.2 2022-12-19 11:29:22 -08:00
Max Brunsfeld
61c6c825b5 Merge pull request #1980 from zed-industries/following-panics
Fix panics when following
2022-12-19 11:26:28 -08:00
Antonio Scandurra
0ede89d82a WIP 2022-12-19 20:05:00 +01:00
Julia
6f211292b2 Merge pull request #1984 from zed-industries/format-problematic-db-macros
Format problematic DB macros
2022-12-19 11:17:34 -05:00
Julia
c49573dc11 Format problematic DB macros 2022-12-19 11:11:10 -05:00
Julia
de9c58d216 Merge pull request #1983 from zed-industries/multi-buffer-git-gutter
Multi buffer git gutter
2022-12-19 10:53:42 -05:00
Antonio Scandurra
84a860e54d Merge pull request #1982 from zed-industries/fix-rust-analyzer
Update rust-analyzer's `disk_based_diagnostics_progress_token`
2022-12-19 16:33:01 +01:00
Antonio Scandurra
cb60eb8a57 Update rust-analyzer's disk_based_diagnostics_progress_token 2022-12-19 16:27:25 +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
Max Brunsfeld
1e02ebbd11 Replicate pending selections separately from other selections
This fixes a panic that would occur when a leader created
a pending selection that overlapped another selection,
because the follower would attempt to treat that pending
selection as non-pending, which would violate the invariant
that selections are sorted and disjoint.
2022-12-17 14:00:53 -08:00
Max Brunsfeld
8c64514570 Add ZED_STATELESS env var, for suppressing persistence
Use this env var in the start-local-collaboration script to make
the behavior more predictable.
2022-12-17 12:03:51 -08:00
Kay Simmons
6fcb3c9020 Merge pull request #1972 from zed-industries/recent-workspace
Recent Project Picker
2022-12-16 15:51:57 -08:00
Kay Simmons
2c47bd4a97 Clear stale projects if they no longer exist 2022-12-16 15:45:17 -08:00
Antonio Scandurra
a5f624203e collab 0.4.1 2022-12-16 12:02:03 +01:00
Antonio Scandurra
98d1b6ec5a Merge pull request #1975 from zed-industries/screen-share-after-reconnect
Prevent screen-sharing from being lost after a reconnection
2022-12-16 12:00:02 +01:00
Antonio Scandurra
457e1046c8 Bump protocol version 2022-12-16 11:48:14 +01:00
Antonio Scandurra
21ab1bb434 Remove unnecessary PeerId parsing code 2022-12-16 11:45:42 +01:00
Antonio Scandurra
aa44de3d16 Fix test ensuring room is left when disconnected from LiveKit 2022-12-16 10:52:32 +01:00
Max Brunsfeld
ad37034960 Identify LiveKit room participants by user id, not peer id
This way, their participant id can remain the same when they reconnect.
2022-12-15 17:19:32 -08:00
Julia
ebd0c5d000 Handle reversed=true for multi-buffer git-hunks-in-range iteration
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-12-15 18:17:32 -05:00
Julia
f88b413f6a Rewrite multi-buffer aware git hunks in range to be more correct
Less ad-hoc state tracking, rely more on values provided by the
underlying data

Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-12-15 17:09:09 -05:00
Antonio Scandurra
c2f5381e5a collab 0.4.0 2022-12-15 19:37:53 +01:00
Antonio Scandurra
ea1f6689b9 Merge pull request #1971 from zed-industries/update-app-icons
Update Zed & Zed Preview icons
2022-12-15 19:37:04 +01:00
Antonio Scandurra
b1affb13bb Merge pull request #1973 from zed-industries/fix-reconnects-after-deploy
Improve reconnections to server after it is redeployed
2022-12-15 19:35:42 +01:00
Antonio Scandurra
2679e245a5 Minor stylistic change 2022-12-15 16:40:16 +01:00
Antonio Scandurra
5a334622ea 💄 2022-12-15 16:34:59 +01:00
Antonio Scandurra
5720c43fe7 Merge branch 'main' into fix-reconnects-after-deploy 2022-12-15 15:32:05 +01:00
Joseph T. Lyons
af4d846428 Merge pull request #1954 from zed-industries/add-symlink-to-applications-directory-in-dmg 2022-12-15 08:06:17 -05:00
Antonio Scandurra
5fb522a9b1 collab 0.3.14 2022-12-15 11:31:51 +01:00
Antonio Scandurra
86e5ae1f2e Allow nulls in projects.host_connection_{id,server_id}
The server version on stable won't be able to fill values for those
columns when we deploy the migration to preview.

With this commit we're also dropping the unused `worktree_extensions`
and `project_activity_periods` tables. The last version of the server
on stable (0.2.6) doesn't contain any code that accesses those tables.
2022-12-15 11:30:51 +01:00
Antonio Scandurra
aadd7f2886 collab 0.3.13 2022-12-15 10:53:17 +01:00
Antonio Scandurra
067a19c971 Avoid logging an error when user who hasn't joined any room disconnects 2022-12-15 10:45:03 +01:00
Antonio Scandurra
688f179256 Use "id" nomenclature more consistently 2022-12-15 10:15:59 +01:00
Antonio Scandurra
af77f1188a Re-add server_id indices for room_participants/project_collaborators 2022-12-15 09:58:25 +01:00
Julia
0dedc1f3a4 Get tests building again 2022-12-15 00:17:28 -05:00
Max Brunsfeld
6c58a4f885 Fix stale server queries, use foreign keys from connectionsn to servers 2022-12-14 17:34:24 -08:00
Kay Simmons
81e3b48f37 Add keybinding 2022-12-14 16:14:16 -08:00
Kay Simmons
6da59311d1 Add open recent project to file menu 2022-12-14 16:02:48 -08:00
Kay Simmons
2bc685281c Add recent project picker 2022-12-14 15:59:50 -08:00
Max Brunsfeld
7e0b6ed1c6 Bump RPC version due to multibuffer following PR 2022-12-14 15:34:22 -08:00
Max Brunsfeld
e08d6cd6de Merge pull request #1921 from zed-industries/multibuffer-following
Allow following collaborators into editors with multi-excerpt buffers (refactors + find-all-refs)
2022-12-14 15:33:11 -08:00
Max Brunsfeld
954c9ac3fd Add integration test coverage for following into multibuffers 2022-12-14 15:28:58 -08:00
Max Brunsfeld
e4c5dfcf6c Use run_until_parked instead of 'condition' in all integration tests 2022-12-14 15:05:35 -08:00
Nate Butler
5f6313d336 Update Zed & Zed Preview icons 2022-12-14 17:41:18 -05:00
Max Brunsfeld
70efd2bebe Introduce a ViewId message, identifying views across calls 2022-12-14 14:40:07 -08:00
Max Brunsfeld
43b7e16c89 Handle retina screens in start-local-collaboration script 2022-12-14 11:50:15 -08:00
Max Brunsfeld
f99f581bfc Clean up state matching in from_state_proto using let/else statements 2022-12-14 11:09:33 -08:00
Max Brunsfeld
09d3fbf04f In editor following test, apply excerpt removals to both followers 2022-12-14 11:08:08 -08:00
Antonio Scandurra
363e3cae4b WIP 2022-12-14 19:25:07 +01:00
Antonio Scandurra
930be6706f WIP 2022-12-14 18:02:39 +01:00
Antonio Scandurra
05e99eb67e Introduce an epoch to ConnectionId and PeerId 2022-12-14 15:55:56 +01:00
Antonio Scandurra
9bd400cf16 collab 0.3.12 2022-12-14 11:43:33 +01:00
Antonio Scandurra
553585b9a1 Add more logging to Room 2022-12-14 11:43:12 +01:00
Antonio Scandurra
674fddac87 Instrument rpc::Server::start and reduce cleanup timeout again 2022-12-14 11:42:12 +01:00
Antonio Scandurra
63e7b9189d collab 0.3.11 2022-12-14 11:25:04 +01:00
Antonio Scandurra
9530976f61 Try using a longer timeout for cleaning up stale rooms 2022-12-14 11:24:36 +01:00
Antonio Scandurra
02c30b0091 collab 0.3.10 2022-12-14 09:35:52 +01:00
Antonio Scandurra
b9c7796547 Reduce readiness probe delay and period 2022-12-14 09:35:36 +01:00
Antonio Scandurra
e00cb6b074 collab 0.3.9 2022-12-14 09:05:19 +01:00
Antonio Scandurra
dc47552180 Fix kubernetes configuration for readiness probe 2022-12-14 08:58:19 +01:00
Antonio Scandurra
98a593b263 collab 0.3.8 2022-12-14 08:56:02 +01:00
Antonio Scandurra
897506c797 Define readiness probe to know when the new server can accept traffic 2022-12-14 08:54:46 +01:00
Antonio Scandurra
59c9a57570 collab 0.3.7 2022-12-14 08:43:18 +01:00
Antonio Scandurra
dde6cf596e Don't wait for stale project deletion before listening for connections 2022-12-14 08:42:34 +01:00
Julia
cf72173282 Clamp end of visual git hunk to requested range 2022-12-13 13:58:50 -05:00
Julia
ecd44e6914 Git diff recalc in project diagnostics 2022-12-13 12:35:58 -05:00
Julia
2cd9987b54 Git diff recalc in project search 2022-12-13 12:35:58 -05:00
Julia
7c3dc1e3dc Cleanup 2022-12-13 12:35:58 -05:00
Julia
00b7c78e33 Initial hacky displaying of git gutter in multi-buffers 2022-12-13 12:35:58 -05:00
Max Brunsfeld
11800a8a78 Merge branch 'main' into multibuffer-following 2022-12-13 09:25:18 -08:00
Max Brunsfeld
f797dfb88f Merge branch 'main' into multibuffer-following 2022-12-12 11:47:39 -08:00
Joseph Lyons
7608875625 Remove extraneous newline 2022-12-10 09:56:42 -05:00
Joseph Lyons
dcf11ac7e5 Add symlink to applications directory in dmg 2022-12-10 09:48:40 -05:00
Max Brunsfeld
82824f78b6 Make each Zed instance use half the screen in 'start-local-collaboration' script 2022-12-01 16:43:39 -08:00
Max Brunsfeld
e4507c1d74 Fetch missing buffers when adding excerpts to a multibuffer while following
Make FollowableItem::apply_update_proto asynchronous. Use a single
task per workspace to process all leader updates, to prevent updates
from being interleaved.

Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2022-12-01 15:17:51 -08:00
Max Brunsfeld
9314c0e313 Replicate multibuffer excerpt additions and removals to followers 2022-11-30 13:20:13 -08:00
Max Brunsfeld
a48cd9125b Start-local-collaboration script: put peers' windows at different positions 2022-11-30 09:29:49 -08:00
Max Brunsfeld
6120d6488b Start work on following in multi-buffers 2022-11-29 14:50:43 -08:00
Max Brunsfeld
82abf31ef1 Add start-local-collaboration script 2022-11-29 14:50:12 -08:00
Max Brunsfeld
6d9b55a654 Send full multibuffer anchors to following peers 2022-11-28 18:00:38 -08:00
Max Brunsfeld
3eac3e20d5 Emit events from a multibuffer when adding/removing excerpts 2022-11-28 17:57:55 -08:00
348 changed files with 16714 additions and 9884 deletions

View File

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

View File

@@ -4,7 +4,7 @@ on:
push:
branches:
- main
- "v*"
- "v[0-9]+.[0-9]+.x"
tags:
- "v*"
pull_request:
@@ -41,16 +41,19 @@ jobs:
with:
clean: false
submodules: 'recursive'
- name: Run tests
run: cargo test --workspace --no-fail-fast
- name: Build collab
run: cargo build -p collab
- name: Build other binaries
run: cargo build --workspace --bins --all-features
- name: Generate license file
run: script/generate-licenses
bundle:
name: Bundle app
runs-on:
@@ -109,6 +112,9 @@ jobs:
exit 1
fi
- name: Generate license file
run: script/generate-licenses
- name: Create app bundle
run: script/bundle

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:

4
.gitignore vendored
View File

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

115
Cargo.lock generated
View File

@@ -739,8 +739,7 @@ dependencies = [
[[package]]
name = "bromberg_sl2"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ed88064f69518b7e3ea50ecfc1b61d43f19248618a377b95ae5c8b611134d4d"
source = "git+https://github.com/zed-industries/bromberg_sl2?rev=dac565a90e8f9245f48ff46225c915dc50f76920#dac565a90e8f9245f48ff46225c915dc50f76920"
dependencies = [
"digest 0.9.0",
"lazy_static",
@@ -820,9 +819,12 @@ dependencies = [
"async-broadcast",
"client",
"collections",
"fs",
"futures 0.3.25",
"gpui",
"language",
"live_kit_client",
"log",
"media",
"postage",
"project",
@@ -1130,7 +1132,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.3.6"
version = "0.5.4"
dependencies = [
"anyhow",
"async-tungstenite",
@@ -1273,6 +1275,7 @@ source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2f
dependencies = [
"core-foundation-sys",
"libc",
"uuid 0.5.1",
]
[[package]]
@@ -2019,6 +2022,33 @@ dependencies = [
"instant",
]
[[package]]
name = "feedback"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"editor",
"futures 0.3.25",
"gpui",
"human_bytes",
"isahc",
"language",
"lazy_static",
"log",
"postage",
"project",
"search",
"serde",
"settings",
"sysinfo",
"theme",
"tree-sitter-markdown",
"urlencoding",
"util",
"workspace",
]
[[package]]
name = "file-per-thread-logger"
version = "0.1.5"
@@ -2560,9 +2590,9 @@ dependencies = [
"sum_tree",
"time 0.3.17",
"tiny-skia",
"tree-sitter",
"usvg",
"util",
"uuid 1.2.2",
"waker-fn",
]
@@ -2756,6 +2786,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"
@@ -3146,10 +3182,12 @@ dependencies = [
"tree-sitter-html",
"tree-sitter-javascript",
"tree-sitter-json 0.19.0",
"tree-sitter-markdown",
"tree-sitter-python",
"tree-sitter-ruby",
"tree-sitter-rust",
"tree-sitter-typescript",
"unicase",
"unindent",
"util",
]
@@ -3754,6 +3792,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"
@@ -4423,7 +4470,7 @@ source = "git+https://github.com/zed-industries/wezterm?rev=5cd757e5f2eb039ed0c6
dependencies = [
"libc",
"log",
"ntapi",
"ntapi 0.3.7",
"winapi 0.3.9",
]
@@ -4434,6 +4481,7 @@ dependencies = [
"aho-corasick",
"anyhow",
"async-trait",
"backtrace",
"client",
"clock",
"collections",
@@ -4806,6 +4854,24 @@ dependencies = [
"rand_core 0.3.1",
]
[[package]]
name = "recent_projects"
version = "0.1.0"
dependencies = [
"db",
"editor",
"fuzzy",
"gpui",
"language",
"ordered-float",
"picker",
"postage",
"settings",
"smol",
"text",
"workspace",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
@@ -5475,6 +5541,7 @@ dependencies = [
"anyhow",
"collections",
"editor",
"futures 0.3.25",
"gpui",
"language",
"log",
@@ -5485,6 +5552,7 @@ dependencies = [
"serde_json",
"settings",
"smallvec",
"smol",
"theme",
"unindent",
"util",
@@ -5947,7 +6015,9 @@ dependencies = [
"libsqlite3-sys",
"parking_lot 0.11.2",
"smol",
"sqlez_macros",
"thread_local",
"uuid 1.2.2",
]
[[package]]
@@ -6200,6 +6270,21 @@ dependencies = [
"libc",
]
[[package]]
name = "sysinfo"
version = "0.27.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1620f9573034c573376acc550f3b9a2be96daeb08abb3c12c8523e1cee06e80f"
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"
@@ -6382,6 +6467,7 @@ dependencies = [
"settings",
"smol",
"theme",
"util",
"workspace",
]
@@ -7182,6 +7268,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"
@@ -7237,6 +7329,12 @@ dependencies = [
"tempdir",
]
[[package]]
name = "uuid"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22"
[[package]]
name = "uuid"
version = "0.8.2"
@@ -8082,6 +8180,7 @@ dependencies = [
"smallvec",
"theme",
"util",
"uuid 1.2.2",
]
[[package]]
@@ -8129,7 +8228,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "zed"
version = "0.68.0"
version = "0.72.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -8154,6 +8253,7 @@ dependencies = [
"easy-parallel",
"editor",
"env_logger",
"feedback",
"file_finder",
"fs",
"fsevent",
@@ -8180,6 +8280,7 @@ dependencies = [
"project_panel",
"project_symbols",
"rand 0.8.5",
"recent_projects",
"regex",
"rpc",
"rsa",
@@ -8221,7 +8322,9 @@ dependencies = [
"tree-sitter-typescript",
"unindent",
"url",
"urlencoding",
"util",
"uuid 1.2.2",
"vim",
"workspace",
]

View File

@@ -17,6 +17,7 @@ members = [
"crates/diagnostics",
"crates/drag_and_drop",
"crates/editor",
"crates/feedback",
"crates/file_finder",
"crates/fs",
"crates/fsevent",
@@ -40,6 +41,7 @@ members = [
"crates/project",
"crates/project_panel",
"crates/project_symbols",
"crates/recent_projects",
"crates/rope",
"crates/rpc",
"crates/search",
@@ -82,5 +84,3 @@ split-debuginfo = "unpacked"
[profile.release]
debug = true

View File

@@ -5,6 +5,7 @@ WORKDIR app
COPY . .
# Compile collab server
ARG CARGO_PROFILE_RELEASE_PANIC=abort
RUN --mount=type=cache,target=./script/node_modules \
--mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=./target \

View File

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

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

@@ -13,6 +13,8 @@
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
"hover_popover_enabled": true,
// Whether to confirm before quitting Zed.
"confirm_quit": false,
// Whether the cursor blinks in the editor.
"cursor_blink": true,
// Whether to pop the completions menu while typing in an editor without
@@ -79,6 +81,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 +230,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

@@ -2,6 +2,7 @@
name = "activity_indicator"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/activity_indicator.rs"

View File

@@ -2,6 +2,7 @@
name = "assets"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/assets.rs"

View File

@@ -2,6 +2,7 @@
name = "auto_update"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/auto_update.rs"

View File

@@ -2,15 +2,15 @@ mod update_notification;
use anyhow::{anyhow, Context, Result};
use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN};
use client::{ZED_APP_PATH, ZED_APP_VERSION};
use db::kvp::KEY_VALUE_STORE;
use gpui::{
actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
MutableAppContext, Task, WeakViewHandle,
};
use lazy_static::lazy_static;
use serde::Deserialize;
use smol::{fs::File, io::AsyncReadExt, process::Command};
use std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration};
use std::{ffi::OsString, sync::Arc, time::Duration};
use update_notification::UpdateNotification;
use util::channel::ReleaseChannel;
use workspace::Workspace;
@@ -18,13 +18,6 @@ use workspace::Workspace;
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
lazy_static! {
pub static ref ZED_APP_VERSION: Option<AppVersion> = env::var("ZED_APP_VERSION")
.ok()
.and_then(|v| v.parse().ok());
pub static ref ZED_APP_PATH: Option<PathBuf> = env::var("ZED_APP_PATH").ok().map(PathBuf::from);
}
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
#[derive(Clone, Copy, PartialEq, Eq)]

View File

@@ -2,6 +2,7 @@
name = "breadcrumbs"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/breadcrumbs.rs"

View File

@@ -2,6 +2,7 @@
name = "call"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/call.rs"
@@ -21,7 +22,10 @@ test-support = [
client = { path = "../client" }
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" }
@@ -33,6 +37,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

@@ -39,6 +39,7 @@ pub struct LocalParticipant {
#[derive(Clone, Debug)]
pub struct RemoteParticipant {
pub user: Arc<User>,
pub peer_id: proto::PeerId,
pub projects: Vec<proto::ParticipantProject>,
pub location: ParticipantLocation,
pub tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,

View File

@@ -3,27 +3,32 @@ use crate::{
IncomingCall,
};
use anyhow::{anyhow, Result};
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
use collections::{BTreeMap, HashSet};
use client::{
proto::{self, PeerId},
Client, TypedEnvelope, User, UserStore,
};
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;
use std::{mem, sync::Arc, time::Duration};
use util::{post_inc, ResultExt};
use util::{post_inc, ResultExt, TryFutureExt};
pub const RECONNECT_TIMEOUT: Duration = client::RECEIVE_TIMEOUT;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
ParticipantLocationChanged {
participant_id: PeerId,
participant_id: proto::PeerId,
},
RemoteVideoTracksChanged {
participant_id: PeerId,
participant_id: proto::PeerId,
},
RemoteProjectShared {
owner: Arc<User>,
@@ -40,8 +45,10 @@ 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<PeerId, RemoteParticipant>,
remote_participants: BTreeMap<u64, RemoteParticipant>,
pending_participants: Vec<Arc<User>>,
participant_user_ids: HashSet<u64>,
pending_call_count: usize,
@@ -50,7 +57,7 @@ pub struct Room {
user_store: ModelHandle<UserStore>,
subscriptions: Vec<client::Subscription>,
pending_room_update: Option<Task<()>>,
maintain_connection: Option<Task<Result<()>>>,
maintain_connection: Option<Task<Option<()>>>,
}
impl Entity for Room {
@@ -58,7 +65,8 @@ impl Entity for Room {
fn release(&mut self, _: &mut MutableAppContext) {
if self.status.is_online() {
self.client.send(proto::LeaveRoom {}).log_err();
log::info!("room was released, sending leave message");
let _ = self.client.send(proto::LeaveRoom {});
}
}
}
@@ -122,12 +130,14 @@ impl Room {
};
let maintain_connection =
cx.spawn_weak(|this, cx| Self::maintain_connection(this, client.clone(), cx));
cx.spawn_weak(|this, cx| Self::maintain_connection(this, client.clone(), cx).log_err());
Self {
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(),
@@ -229,6 +239,23 @@ 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();
@@ -252,15 +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() {
let room_id = this
.upgrade(&cx)
log::info!("detected client disconnection");
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.
@@ -269,30 +296,25 @@ impl Room {
let client_reconnection = async {
let mut remaining_attempts = 3;
while remaining_attempts > 0 {
if let Some(status) = client_status.next().await {
if status.is_connected() {
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(())
};
log::info!(
"waiting for client status change, remaining attempts {}",
remaining_attempts
);
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.is_ok() {
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
@@ -303,12 +325,15 @@ impl Room {
futures::select_biased! {
reconnected = client_reconnection => {
if reconnected {
log::info!("successfully reconnected to room");
// If we successfully joined the room, go back around the loop
// waiting for future connection status changes.
continue;
}
}
_ = reconnection_timeout => {}
_ = reconnection_timeout => {
log::info!("room reconnection timeout expired");
}
}
}
@@ -316,6 +341,7 @@ impl Room {
// or an error occurred while trying to re-join the room. Either way
// we leave the room and return an error.
if let Some(this) = this.upgrade(&cx) {
log::info!("reconnection failed, leaving room");
let _ = this.update(&mut cx, |this, cx| this.leave(cx));
}
return Err(anyhow!(
@@ -325,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
}
@@ -337,10 +439,16 @@ impl Room {
&self.local_participant
}
pub fn remote_participants(&self) -> &BTreeMap<PeerId, RemoteParticipant> {
pub fn remote_participants(&self) -> &BTreeMap<u64, RemoteParticipant> {
&self.remote_participants
}
pub fn remote_participant_for_peer_id(&self, peer_id: PeerId) -> Option<&RemoteParticipant> {
self.remote_participants
.values()
.find(|p| p.peer_id == peer_id)
}
pub fn pending_participants(&self) -> &[Arc<User>] {
&self.pending_participants
}
@@ -405,15 +513,13 @@ impl Room {
}
if let Some(participants) = remote_participants.log_err() {
let mut participant_peer_ids = HashSet::default();
for (participant, user) in room.participants.into_iter().zip(participants) {
let peer_id = PeerId(participant.peer_id);
let Some(peer_id) = participant.peer_id else { continue };
this.participant_user_ids.insert(participant.user_id);
participant_peer_ids.insert(peer_id);
let old_projects = this
.remote_participants
.get(&peer_id)
.get(&participant.user_id)
.into_iter()
.flat_map(|existing| &existing.projects)
.map(|project| project.id)
@@ -435,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,
});
@@ -442,9 +562,11 @@ impl Room {
let location = ParticipantLocation::from_proto(participant.location)
.unwrap_or(ParticipantLocation::External);
if let Some(remote_participant) = this.remote_participants.get_mut(&peer_id)
if let Some(remote_participant) =
this.remote_participants.get_mut(&participant.user_id)
{
remote_participant.projects = participant.projects;
remote_participant.peer_id = peer_id;
if location != remote_participant.location {
remote_participant.location = location;
cx.emit(Event::ParticipantLocationChanged {
@@ -453,9 +575,10 @@ impl Room {
}
} else {
this.remote_participants.insert(
peer_id,
participant.user_id,
RemoteParticipant {
user: user.clone(),
peer_id,
projects: participant.projects,
location,
tracks: Default::default(),
@@ -464,7 +587,7 @@ impl Room {
if let Some(live_kit) = this.live_kit.as_ref() {
let tracks =
live_kit.room.remote_video_tracks(&peer_id.0.to_string());
live_kit.room.remote_video_tracks(&peer_id.to_string());
for track in tracks {
this.remote_video_track_updated(
RemoteVideoTrackUpdate::Subscribed(track),
@@ -476,8 +599,8 @@ impl Room {
}
}
this.remote_participants.retain(|peer_id, participant| {
if participant_peer_ids.contains(peer_id) {
this.remote_participants.retain(|user_id, participant| {
if this.participant_user_ids.contains(user_id) {
true
} else {
for project in &participant.projects {
@@ -499,6 +622,7 @@ impl Room {
this.pending_room_update.take();
if this.should_leave() {
log::info!("room is empty, leaving");
let _ = this.leave(cx);
}
@@ -518,11 +642,11 @@ impl Room {
) -> Result<()> {
match change {
RemoteVideoTrackUpdate::Subscribed(track) => {
let peer_id = PeerId(track.publisher_id().parse()?);
let user_id = track.publisher_id().parse()?;
let track_id = track.sid().to_string();
let participant = self
.remote_participants
.get_mut(&peer_id)
.get_mut(&user_id)
.ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
participant.tracks.insert(
track_id.clone(),
@@ -531,21 +655,21 @@ impl Room {
}),
);
cx.emit(Event::RemoteVideoTracksChanged {
participant_id: peer_id,
participant_id: participant.peer_id,
});
}
RemoteVideoTrackUpdate::Unsubscribed {
publisher_id,
track_id,
} => {
let peer_id = PeerId(publisher_id.parse()?);
let user_id = publisher_id.parse()?;
let participant = self
.remote_participants
.get_mut(&peer_id)
.get_mut(&user_id)
.ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
participant.tracks.remove(&track_id);
cx.emit(Event::RemoteVideoTracksChanged {
participant_id: peer_id,
participant_id: participant.peer_id,
});
}
}
@@ -607,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>,
@@ -618,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

@@ -2,6 +2,7 @@
name = "cli"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/cli.rs"

View File

@@ -9,7 +9,13 @@ use core_foundation::{
use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
use serde::Deserialize;
use std::{ffi::OsStr, fs, path::PathBuf, ptr};
use std::{
ffi::OsStr,
fs::{self, OpenOptions},
io,
path::{Path, PathBuf},
ptr,
};
#[derive(Parser)]
#[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))]
@@ -54,6 +60,12 @@ fn main() -> Result<()> {
return Ok(());
}
for path in args.paths.iter() {
if !path.exists() {
touch(path.as_path())?;
}
}
let (tx, rx) = launch_app(bundle_path)?;
tx.send(CliRequest::Open {
@@ -77,6 +89,13 @@ fn main() -> Result<()> {
Ok(())
}
fn touch(path: &Path) -> io::Result<()> {
match OpenOptions::new().create(true).write(true).open(path) {
Ok(_) => Ok(()),
Err(e) => Err(e),
}
}
fn locate_bundle() -> Result<PathBuf> {
let cli_path = std::env::current_exe()?.canonicalize()?;
let mut app_path = cli_path.clone();

View File

@@ -2,6 +2,7 @@
name = "client"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/client.rs"

View File

@@ -15,7 +15,7 @@ use futures::{future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamEx
use gpui::{
actions,
serde_json::{self, Value},
AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext,
AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext, AppVersion,
AsyncAppContext, Entity, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
};
use http::HttpClient;
@@ -23,8 +23,9 @@ use lazy_static::lazy_static;
use parking_lot::RwLock;
use postage::watch;
use rand::prelude::*;
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage};
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
use serde::Deserialize;
use settings::{Settings, TelemetrySettings};
use std::{
any::TypeId,
collections::HashMap,
@@ -54,6 +55,11 @@ lazy_static! {
pub static ref ADMIN_API_TOKEN: Option<String> = std::env::var("ZED_ADMIN_API_TOKEN")
.ok()
.and_then(|s| if s.is_empty() { None } else { Some(s) });
pub static ref ZED_APP_VERSION: Option<AppVersion> = std::env::var("ZED_APP_VERSION")
.ok()
.and_then(|v| v.parse().ok());
pub static ref ZED_APP_PATH: Option<PathBuf> =
std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
}
pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
@@ -140,7 +146,7 @@ impl EstablishConnectionError {
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Status {
SignedOut,
UpgradeRequired,
@@ -306,7 +312,7 @@ impl Client {
pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
Arc::new(Self {
id: 0,
peer: Peer::new(),
peer: Peer::new(0),
telemetry: Telemetry::new(http.clone(), cx),
http,
state: Default::default(),
@@ -333,14 +339,14 @@ impl Client {
}
#[cfg(any(test, feature = "test-support"))]
pub fn tear_down(&self) {
pub fn teardown(&self) {
let mut state = self.state.write();
state._reconnect_task.take();
state.message_handlers.clear();
state.models_by_message_type.clear();
state.entities_by_type_and_remote_id.clear();
state.entity_id_extractors.clear();
self.peer.reset();
self.peer.teardown();
}
#[cfg(any(test, feature = "test-support"))]
@@ -423,7 +429,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 +714,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() {
@@ -810,7 +824,11 @@ impl Client {
hello_message_type_name
)
})?;
Ok(PeerId(hello.payload.peer_id))
let peer_id = hello
.payload
.peer_id
.ok_or_else(|| anyhow!("invalid peer id"))?;
Ok(peer_id)
};
let peer_id = match peer_id.await {
@@ -822,7 +840,7 @@ impl Client {
};
log::info!(
"set status to connected (connection id: {}, peer id: {})",
"set status to connected (connection id: {:?}, peer id: {:?})",
connection_id,
peer_id
);
@@ -853,7 +871,7 @@ impl Client {
.spawn(async move {
match handle_io.await {
Ok(()) => {
if *this.status().borrow()
if this.status().borrow().clone()
== (Status::Connected {
connection_id,
peer_id,
@@ -993,6 +1011,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
@@ -1075,7 +1095,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()?,
@@ -1194,7 +1218,7 @@ impl Client {
let mut state = self.state.write();
let type_name = message.payload_type_name();
let payload_type_id = message.payload_type_id();
let sender_id = message.original_sender_id().map(|id| id.0);
let sender_id = message.original_sender_id();
let mut subscriber = None;
@@ -1231,6 +1255,7 @@ impl Client {
subscriber
} else {
log::info!("unhandled message {}", type_name);
self.peer.respond_with_unhandled_message(message).log_err();
return;
};
@@ -1274,6 +1299,7 @@ impl Client {
.detach();
} else {
log::info!("unhandled message {}", type_name);
self.peer.respond_with_unhandled_message(message).log_err();
}
}
@@ -1281,13 +1307,23 @@ 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> {
self.telemetry.log_file_path()
}
pub fn metrics_id(&self) -> Option<Arc<str>> {
self.telemetry.metrics_id()
}
}
impl WeakSubscriber {

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(),
@@ -261,6 +278,10 @@ impl Telemetry {
}
}
pub fn metrics_id(self: &Arc<Self>) -> Option<Arc<str>> {
self.state.lock().metrics_id.clone()
}
fn flush(self: &Arc<Self>) {
let mut state = self.state.lock();
let mut events = mem::take(&mut state.queue);

View File

@@ -35,7 +35,7 @@ impl FakeServer {
cx: &TestAppContext,
) -> Self {
let server = Self {
peer: Peer::new(),
peer: Peer::new(0),
state: Default::default(),
user_id: client_user_id,
executor: cx.foreground(),
@@ -92,7 +92,7 @@ impl FakeServer {
peer.send(
connection_id,
proto::Hello {
peer_id: connection_id.0,
peer_id: Some(connection_id.into()),
},
)
.unwrap();

View File

@@ -5,8 +5,9 @@ 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 _;
use util::{StaffMode, TryFutureExt as _};
#[derive(Default, Debug)]
pub struct User {
@@ -141,14 +142,24 @@ 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()),
);
cx.update(|cx| {
cx.update_default_global(|staff_mode: &mut StaffMode, _| {
if !staff_mode.0 {
*staff_mode = StaffMode(
info.as_ref()
.map(|info| info.staff)
.unwrap_or_default(),
)
}
()
});
});
current_user_tx.send(user).await.ok();
}

View File

@@ -2,6 +2,7 @@
name = "clock"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/clock.rs"

View File

@@ -2,6 +2,7 @@ DATABASE_URL = "postgres://postgres@localhost/zed"
HTTP_PORT = 8080
API_TOKEN = "secret"
INVITE_LINK_PREFIX = "http://localhost:3000/invites/"
ZED_ENVIRONMENT = "development"
LIVE_KIT_SERVER = "http://localhost:7880"
LIVE_KIT_KEY = "devkey"
LIVE_KIT_SECRET = "secret"

View File

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

View File

@@ -59,6 +59,12 @@ spec:
ports:
- containerPort: 8080
protocol: TCP
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 1
periodSeconds: 1
env:
- name: HTTP_PORT
value: "8080"
@@ -93,6 +99,8 @@ spec:
value: ${RUST_LOG}
- name: LOG_JSON
value: "true"
- name: ZED_ENVIRONMENT
value: ${ZED_ENVIRONMENT}
securityContext:
capabilities:
# FIXME - Switch to the more restrictive `PERFMON` capability.

View File

@@ -43,11 +43,12 @@ CREATE TABLE "projects" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"room_id" INTEGER REFERENCES rooms (id) NOT NULL,
"host_user_id" INTEGER REFERENCES users (id) NOT NULL,
"host_connection_id" INTEGER NOT NULL,
"host_connection_epoch" TEXT NOT NULL,
"host_connection_id" INTEGER,
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX "index_projects_on_host_connection_epoch" ON "projects" ("host_connection_epoch");
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");
CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id");
CREATE TABLE "worktrees" (
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
@@ -56,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");
@@ -64,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,
@@ -72,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
);
@@ -103,34 +107,39 @@ CREATE TABLE "project_collaborators" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
"connection_id" INTEGER NOT NULL,
"connection_epoch" TEXT NOT NULL,
"connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
"user_id" INTEGER NOT NULL,
"replica_id" INTEGER NOT NULL,
"is_host" BOOLEAN NOT NULL
);
CREATE INDEX "index_project_collaborators_on_project_id" ON "project_collaborators" ("project_id");
CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_and_replica_id" ON "project_collaborators" ("project_id", "replica_id");
CREATE INDEX "index_project_collaborators_on_connection_epoch" ON "project_collaborators" ("connection_epoch");
CREATE INDEX "index_project_collaborators_on_connection_server_id" ON "project_collaborators" ("connection_server_id");
CREATE INDEX "index_project_collaborators_on_connection_id" ON "project_collaborators" ("connection_id");
CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_connection_id_and_epoch" ON "project_collaborators" ("project_id", "connection_id", "connection_epoch");
CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_connection_id_and_server_id" ON "project_collaborators" ("project_id", "connection_id", "connection_server_id");
CREATE TABLE "room_participants" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"room_id" INTEGER NOT NULL REFERENCES rooms (id),
"user_id" INTEGER NOT NULL REFERENCES users (id),
"answering_connection_id" INTEGER,
"answering_connection_epoch" TEXT,
"answering_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
"answering_connection_lost" BOOLEAN NOT NULL,
"location_kind" INTEGER,
"location_project_id" INTEGER,
"initial_project_id" INTEGER,
"calling_user_id" INTEGER NOT NULL REFERENCES users (id),
"calling_connection_id" INTEGER NOT NULL,
"calling_connection_epoch" TEXT NOT NULL
"calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL
);
CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id");
CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id");
CREATE INDEX "index_room_participants_on_answering_connection_epoch" ON "room_participants" ("answering_connection_epoch");
CREATE INDEX "index_room_participants_on_calling_connection_epoch" ON "room_participants" ("calling_connection_epoch");
CREATE INDEX "index_room_participants_on_answering_connection_server_id" ON "room_participants" ("answering_connection_server_id");
CREATE INDEX "index_room_participants_on_calling_connection_server_id" ON "room_participants" ("calling_connection_server_id");
CREATE INDEX "index_room_participants_on_answering_connection_id" ON "room_participants" ("answering_connection_id");
CREATE UNIQUE INDEX "index_room_participants_on_answering_connection_id_and_answering_connection_epoch" ON "room_participants" ("answering_connection_id", "answering_connection_epoch");
CREATE UNIQUE INDEX "index_room_participants_on_answering_connection_id_and_answering_connection_server_id" ON "room_participants" ("answering_connection_id", "answering_connection_server_id");
CREATE TABLE "servers" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"environment" VARCHAR NOT NULL
);

View File

@@ -0,0 +1,30 @@
CREATE TABLE servers (
id SERIAL PRIMARY KEY,
environment VARCHAR NOT NULL
);
DROP TABLE worktree_extensions;
DROP TABLE project_activity_periods;
DELETE from projects;
ALTER TABLE projects
DROP COLUMN host_connection_epoch,
ADD COLUMN host_connection_server_id INTEGER REFERENCES servers (id) ON DELETE CASCADE;
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");
CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id");
DELETE FROM project_collaborators;
ALTER TABLE project_collaborators
DROP COLUMN connection_epoch,
ADD COLUMN connection_server_id INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE;
CREATE INDEX "index_project_collaborators_on_connection_server_id" ON "project_collaborators" ("connection_server_id");
CREATE UNIQUE INDEX "index_project_collaborators_on_project_id_connection_id_and_server_id" ON "project_collaborators" ("project_id", "connection_id", "connection_server_id");
DELETE FROM room_participants;
ALTER TABLE room_participants
DROP COLUMN answering_connection_epoch,
DROP COLUMN calling_connection_epoch,
ADD COLUMN answering_connection_server_id INTEGER REFERENCES servers (id) ON DELETE CASCADE,
ADD COLUMN calling_connection_server_id INTEGER REFERENCES servers (id) ON DELETE SET NULL;
CREATE INDEX "index_room_participants_on_answering_connection_server_id" ON "room_participants" ("answering_connection_server_id");
CREATE INDEX "index_room_participants_on_calling_connection_server_id" ON "room_participants" ("calling_connection_server_id");
CREATE UNIQUE INDEX "index_room_participants_on_answering_connection_id_and_answering_connection_server_id" ON "room_participants" ("answering_connection_id", "answering_connection_server_id");

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?,
))

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,6 @@
use super::{ProjectId, RoomId, UserId};
use super::{ProjectId, Result, RoomId, ServerId, UserId};
use anyhow::anyhow;
use rpc::ConnectionId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
@@ -8,8 +10,23 @@ pub struct Model {
pub id: ProjectId,
pub room_id: RoomId,
pub host_user_id: UserId,
pub host_connection_id: i32,
pub host_connection_epoch: Uuid,
pub host_connection_id: Option<i32>,
pub host_connection_server_id: Option<ServerId>,
}
impl Model {
pub fn host_connection(&self) -> Result<ConnectionId> {
let host_connection_server_id = self
.host_connection_server_id
.ok_or_else(|| anyhow!("empty host_connection_server_id"))?;
let host_connection_id = self
.host_connection_id
.ok_or_else(|| anyhow!("empty host_connection_id"))?;
Ok(ConnectionId {
owner_id: host_connection_server_id.0 as u32,
id: host_connection_id as u32,
})
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -1,4 +1,5 @@
use super::{ProjectCollaboratorId, ProjectId, ReplicaId, UserId};
use super::{ProjectCollaboratorId, ProjectId, ReplicaId, ServerId, UserId};
use rpc::ConnectionId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
@@ -8,12 +9,21 @@ pub struct Model {
pub id: ProjectCollaboratorId,
pub project_id: ProjectId,
pub connection_id: i32,
pub connection_epoch: Uuid,
pub connection_server_id: ServerId,
pub user_id: UserId,
pub replica_id: ReplicaId,
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

@@ -1,4 +1,4 @@
use super::{ProjectId, RoomId, RoomParticipantId, UserId};
use super::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
@@ -9,14 +9,14 @@ pub struct Model {
pub room_id: RoomId,
pub user_id: UserId,
pub answering_connection_id: Option<i32>,
pub answering_connection_epoch: Option<Uuid>,
pub answering_connection_server_id: Option<ServerId>,
pub answering_connection_lost: bool,
pub location_kind: Option<i32>,
pub location_project_id: Option<ProjectId>,
pub initial_project_id: Option<ProjectId>,
pub calling_user_id: UserId,
pub calling_connection_id: i32,
pub calling_connection_epoch: Uuid,
pub calling_connection_server_id: Option<ServerId>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -0,0 +1,15 @@
use super::ServerId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "servers")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: ServerId,
pub environment: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -410,6 +410,8 @@ test_both_dbs!(
test_project_count_sqlite,
db,
{
let owner_id = db.create_server("test").await.unwrap().0 as u32;
let user1 = db
.create_user(
&format!("admin@example.com"),
@@ -436,36 +438,44 @@ test_both_dbs!(
.unwrap();
let room_id = RoomId::from_proto(
db.create_room(user1.user_id, ConnectionId(0), "")
db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "")
.await
.unwrap()
.id,
);
db.call(room_id, user1.user_id, ConnectionId(0), user2.user_id, None)
.await
.unwrap();
db.join_room(room_id, user2.user_id, ConnectionId(1))
db.call(
room_id,
user1.user_id,
ConnectionId { owner_id, id: 0 },
user2.user_id,
None,
)
.await
.unwrap();
db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 })
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
db.share_project(room_id, ConnectionId(1), &[])
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1);
db.share_project(room_id, ConnectionId(1), &[])
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
// Projects shared by admins aren't counted.
db.share_project(room_id, ConnectionId(0), &[])
db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[])
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
db.leave_room(ConnectionId(1)).await.unwrap();
db.leave_room(ConnectionId { owner_id, id: 1 })
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
}
);
@@ -557,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 {
@@ -607,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 {
@@ -662,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();
@@ -674,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
@@ -729,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);
@@ -753,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;
@@ -97,6 +98,7 @@ pub struct Config {
pub live_kit_secret: Option<String>,
pub rust_log: Option<String>,
pub log_json: Option<bool>,
pub zed_environment: String,
}
#[derive(Default, Deserialize)]

View File

@@ -57,7 +57,11 @@ async fn main() -> Result<()> {
let listener = TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port))
.expect("failed to bind TCP listener");
let rpc_server = collab::rpc::Server::new(state.clone(), Executor::Production);
let epoch = state
.db
.create_server(&state.config.zed_environment)
.await?;
let rpc_server = collab::rpc::Server::new(epoch, state.clone(), Executor::Production);
rpc_server.start().await?;
let app = collab::api::routes(rpc_server.clone(), state.clone())
@@ -73,8 +77,7 @@ async fn main() -> Result<()> {
.expect("failed to listen for interrupt signal");
let sigterm = sigterm.recv();
let sigint = sigint.recv();
futures::pin_mut!(sigterm);
futures::pin_mut!(sigint);
futures::pin_mut!(sigterm, sigint);
futures::future::select(sigterm, sigint).await;
tracing::info!("Received interrupt signal");
rpc_server.teardown();

View File

@@ -2,7 +2,7 @@ mod connection_pool;
use crate::{
auth,
db::{self, Database, ProjectId, RoomId, User, UserId},
db::{self, Database, ProjectId, RoomId, ServerId, User, UserId},
executor::Executor,
AppState, Result,
};
@@ -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 {
@@ -138,6 +139,7 @@ impl Deref for DbHandle {
}
pub struct Server {
id: parking_lot::Mutex<ServerId>,
peer: Arc<Peer>,
pub(crate) connection_pool: Arc<parking_lot::Mutex<ConnectionPool>>,
app_state: Arc<AppState>,
@@ -168,9 +170,10 @@ where
}
impl Server {
pub fn new(app_state: Arc<AppState>, executor: Executor) -> Arc<Self> {
pub fn new(id: ServerId, app_state: Arc<AppState>, executor: Executor) -> Arc<Self> {
let mut server = Self {
peer: Peer::new(),
id: parking_lot::Mutex::new(id),
peer: Peer::new(id.0 as u32),
app_state,
executor,
connection_pool: Default::default(),
@@ -182,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)
@@ -213,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>)
@@ -239,97 +244,136 @@ impl Server {
}
pub async fn start(&self) -> Result<()> {
self.app_state.db.delete_stale_projects().await?;
let db = self.app_state.db.clone();
let server_id = *self.id.lock();
let app_state = self.app_state.clone();
let peer = self.peer.clone();
let timeout = self.executor.sleep(CLEANUP_TIMEOUT);
let pool = self.connection_pool.clone();
let live_kit_client = self.app_state.live_kit_client.clone();
self.executor.spawn_detached(async move {
timeout.await;
if let Some(room_ids) = db.stale_room_ids().await.trace_err() {
for room_id in room_ids {
let mut contacts_to_update = HashSet::default();
let mut canceled_calls_to_user_ids = Vec::new();
let mut live_kit_room = String::new();
let mut delete_live_kit_room = false;
if let Ok(mut refreshed_room) = db.refresh_room(room_id).await {
room_updated(&refreshed_room.room, &peer);
contacts_to_update
.extend(refreshed_room.stale_participant_user_ids.iter().copied());
contacts_to_update
.extend(refreshed_room.canceled_calls_to_user_ids.iter().copied());
canceled_calls_to_user_ids =
mem::take(&mut refreshed_room.canceled_calls_to_user_ids);
live_kit_room = mem::take(&mut refreshed_room.room.live_kit_room);
delete_live_kit_room = refreshed_room.room.participants.is_empty();
}
let span = info_span!("start server");
self.executor.spawn_detached(
async move {
tracing::info!("waiting for cleanup timeout");
timeout.await;
tracing::info!("cleanup timeout expired, retrieving stale rooms");
if let Some(room_ids) = app_state
.db
.stale_room_ids(&app_state.config.zed_environment, server_id)
.await
.trace_err()
{
tracing::info!(stale_room_count = room_ids.len(), "retrieved stale rooms");
for room_id in room_ids {
let mut contacts_to_update = HashSet::default();
let mut canceled_calls_to_user_ids = Vec::new();
let mut live_kit_room = String::new();
let mut delete_live_kit_room = false;
{
let pool = pool.lock();
for canceled_user_id in canceled_calls_to_user_ids {
for connection_id in pool.user_connection_ids(canceled_user_id) {
peer.send(
connection_id,
proto::CallCanceled {
room_id: room_id.to_proto(),
},
)
.trace_err();
if let Ok(mut refreshed_room) =
app_state.db.refresh_room(room_id, server_id).await
{
tracing::info!(
room_id = room_id.0,
new_participant_count = refreshed_room.room.participants.len(),
"refreshed room"
);
room_updated(&refreshed_room.room, &peer);
contacts_to_update
.extend(refreshed_room.stale_participant_user_ids.iter().copied());
contacts_to_update
.extend(refreshed_room.canceled_calls_to_user_ids.iter().copied());
canceled_calls_to_user_ids =
mem::take(&mut refreshed_room.canceled_calls_to_user_ids);
live_kit_room = mem::take(&mut refreshed_room.room.live_kit_room);
delete_live_kit_room = refreshed_room.room.participants.is_empty();
}
{
let pool = pool.lock();
for canceled_user_id in canceled_calls_to_user_ids {
for connection_id in pool.user_connection_ids(canceled_user_id) {
peer.send(
connection_id,
proto::CallCanceled {
room_id: room_id.to_proto(),
},
)
.trace_err();
}
}
}
}
for user_id in contacts_to_update {
let busy = db.is_user_busy(user_id).await.trace_err();
let contacts = db.get_contacts(user_id).await.trace_err();
if let Some((busy, contacts)) = busy.zip(contacts) {
let pool = pool.lock();
let updated_contact = contact_for_user(user_id, false, busy, &pool);
for contact in contacts {
if let db::Contact::Accepted {
user_id: contact_user_id,
..
} = contact
{
for contact_conn_id in pool.user_connection_ids(contact_user_id)
for user_id in contacts_to_update {
let busy = app_state.db.is_user_busy(user_id).await.trace_err();
let contacts = app_state.db.get_contacts(user_id).await.trace_err();
if let Some((busy, contacts)) = busy.zip(contacts) {
let pool = pool.lock();
let updated_contact = contact_for_user(user_id, false, busy, &pool);
for contact in contacts {
if let db::Contact::Accepted {
user_id: contact_user_id,
..
} = contact
{
peer.send(
contact_conn_id,
proto::UpdateContacts {
contacts: vec![updated_contact.clone()],
remove_contacts: Default::default(),
incoming_requests: Default::default(),
remove_incoming_requests: Default::default(),
outgoing_requests: Default::default(),
remove_outgoing_requests: Default::default(),
},
)
.trace_err();
for contact_conn_id in
pool.user_connection_ids(contact_user_id)
{
peer.send(
contact_conn_id,
proto::UpdateContacts {
contacts: vec![updated_contact.clone()],
remove_contacts: Default::default(),
incoming_requests: Default::default(),
remove_incoming_requests: Default::default(),
outgoing_requests: Default::default(),
remove_outgoing_requests: Default::default(),
},
)
.trace_err();
}
}
}
}
}
}
if let Some(live_kit) = live_kit_client.as_ref() {
if delete_live_kit_room {
live_kit.delete_room(live_kit_room).await.trace_err();
if let Some(live_kit) = live_kit_client.as_ref() {
if delete_live_kit_room {
live_kit.delete_room(live_kit_room).await.trace_err();
}
}
}
}
app_state
.db
.delete_stale_servers(&app_state.config.zed_environment, server_id)
.await
.trace_err();
}
});
.instrument(span),
);
Ok(())
}
pub fn teardown(&self) {
self.peer.reset();
self.peer.teardown();
self.connection_pool.lock().reset();
let _ = self.teardown.send(());
}
#[cfg(test)]
pub fn reset(&self, id: ServerId) {
self.teardown();
*self.id.lock() = id;
self.peer.reset(id.0 as u32);
}
#[cfg(test)]
pub fn id(&self) -> ServerId {
*self.id.lock()
}
fn add_handler<F, Fut, M>(&mut self, handler: F) -> &mut Self
where
F: 'static + Send + Sync + Fn(TypedEnvelope<M>, Session) -> Fut,
@@ -438,7 +482,7 @@ impl Server {
});
tracing::info!(%user_id, %login, %connection_id, %address, "connection opened");
this.peer.send(connection_id, proto::Hello { peer_id: connection_id.0 })?;
this.peer.send(connection_id, proto::Hello { peer_id: Some(connection_id.into()) })?;
tracing::info!(%user_id, %login, %connection_id, %address, "sent hello message");
if let Some(send_connection_id) = send_connection_id.take() {
@@ -478,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?;
@@ -535,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");
}
@@ -627,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);
}
}
}
}
@@ -736,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,
@@ -747,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() => {
@@ -769,7 +811,7 @@ async fn sign_out(
.is_user_online(session.user_id)
{
let db = session.db().await;
if let Some(room) = db.decline_call(None, session.user_id).await.trace_err() {
if let Some(room) = db.decline_call(None, session.user_id).await.trace_err().flatten() {
room_updated(&room, &session.peer);
}
}
@@ -799,7 +841,7 @@ async fn create_room(
.trace_err()
{
if let Some(token) = live_kit
.room_token(&live_kit_room, &session.connection_id.to_string())
.room_token(&live_kit_room, &session.user_id.to_string())
.trace_err()
{
Some(proto::LiveKitConnectionInfo {
@@ -867,7 +909,7 @@ async fn join_room(
let live_kit_connection_info = if let Some(live_kit) = session.live_kit_client.as_ref() {
if let Some(token) = live_kit
.room_token(&room.live_kit_room, &session.connection_id.to_string())
.room_token(&room.live_kit_room, &session.user_id.to_string())
.trace_err()
{
Some(proto::LiveKitConnectionInfo {
@@ -890,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
}
@@ -973,7 +1173,7 @@ async fn cancel_call(
let room = session
.db()
.await
.cancel_call(Some(room_id), session.connection_id, called_user_id)
.cancel_call(room_id, session.connection_id, called_user_id)
.await?;
room_updated(&room, &session.peer);
}
@@ -1006,7 +1206,8 @@ async fn decline_call(message: proto::DeclineCall, session: Session) -> Result<(
.db()
.await
.decline_call(Some(room_id), session.user_id)
.await?;
.await?
.ok_or_else(|| anyhow!("failed to decline call"))?;
room_updated(&room, &session.peer);
}
@@ -1080,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()),
);
@@ -1108,12 +1309,8 @@ async fn join_project(
let collaborators = project
.collaborators
.iter()
.filter(|collaborator| collaborator.connection_id != session.connection_id.0 as i32)
.map(|collaborator| proto::Collaborator {
peer_id: collaborator.connection_id as u32,
replica_id: collaborator.replica_id.0 as u32,
user_id: collaborator.user_id.to_proto(),
})
.filter(|collaborator| collaborator.connection_id != session.connection_id)
.map(|collaborator| collaborator.to_proto())
.collect::<Vec<_>>();
let worktrees = project
.worktrees
@@ -1130,11 +1327,11 @@ async fn join_project(
session
.peer
.send(
ConnectionId(collaborator.peer_id),
collaborator.peer_id.unwrap().into(),
proto::AddProjectCollaborator {
project_id: project_id.to_proto(),
collaborator: Some(proto::Collaborator {
peer_id: session.connection_id.0,
peer_id: Some(session.connection_id.into()),
replica_id: replica_id.0 as u32,
user_id: guest_user_id.to_proto(),
}),
@@ -1166,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())?;
@@ -1235,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
@@ -1261,7 +1458,7 @@ async fn update_worktree(
.await?;
broadcast(
session.connection_id,
Some(session.connection_id),
guest_connection_ids.iter().copied(),
|connection_id| {
session
@@ -1284,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
@@ -1307,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
@@ -1322,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()
@@ -1329,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
@@ -1348,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
@@ -1355,13 +1554,11 @@ where
.await
.project_collaborators(project_id, session.connection_id)
.await?;
ConnectionId(
collaborators
.iter()
.find(|collaborator| collaborator.is_host)
.ok_or_else(|| anyhow!("host not found"))?
.connection_id as u32,
)
collaborators
.iter()
.find(|collaborator| collaborator.is_host)
.ok_or_else(|| anyhow!("host not found"))?
.connection_id
};
let payload = session
@@ -1385,11 +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(host.connection_id as u32)
.ok_or_else(|| anyhow!("host not found"))?
.connection_id
};
let response_payload = session
.peer
@@ -1401,16 +1598,19 @@ async fn save_buffer(
.await
.project_collaborators(project_id, session.connection_id)
.await?;
collaborators
.retain(|collaborator| collaborator.connection_id != session.connection_id.0 as i32);
collaborators.retain(|collaborator| collaborator.connection_id != session.connection_id);
let project_connection_ids = collaborators
.iter()
.map(|collaborator| ConnectionId(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())
});
.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(())
}
@@ -1419,11 +1619,11 @@ async fn create_buffer_for_peer(
request: proto::CreateBufferForPeer,
session: Session,
) -> Result<()> {
session.peer.forward_send(
session.connection_id,
ConnectionId(request.peer_id),
request,
)?;
session.executor.record_backtrace();
let peer_id = request.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?;
session
.peer
.forward_send(session.connection_id, peer_id.into(), request)?;
Ok(())
}
@@ -1432,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()
@@ -1439,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
@@ -1461,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
@@ -1480,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
@@ -1499,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
@@ -1516,7 +1719,10 @@ async fn follow(
session: Session,
) -> Result<()> {
let project_id = ProjectId::from_proto(request.project_id);
let leader_id = ConnectionId(request.leader_id);
let leader_id = request
.leader_id
.ok_or_else(|| anyhow!("invalid leader id"))?
.into();
let follower_id = session.connection_id;
{
let project_connection_ids = session
@@ -1536,14 +1742,17 @@ async fn follow(
.await?;
response_payload
.views
.retain(|view| view.leader_id != Some(follower_id.0));
.retain(|view| view.leader_id != Some(follower_id.into()));
response.send(response_payload)?;
Ok(())
}
async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
let project_id = ProjectId::from_proto(request.project_id);
let leader_id = ConnectionId(request.leader_id);
let leader_id = request
.leader_id
.ok_or_else(|| anyhow!("invalid leader id"))?
.into();
let project_connection_ids = session
.db()
.await
@@ -1572,12 +1781,16 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) ->
proto::update_followers::Variant::UpdateView(payload) => payload.leader_id,
proto::update_followers::Variant::UpdateActiveView(payload) => payload.leader_id,
});
for follower_id in &request.follower_ids {
let follower_id = ConnectionId(*follower_id);
if project_connection_ids.contains(&follower_id) && Some(follower_id.0) != leader_id {
session
.peer
.forward_send(session.connection_id, follower_id, request.clone())?;
for follower_peer_id in request.follower_ids.iter().copied() {
let follower_connection_id = follower_peer_id.into();
if project_connection_ids.contains(&follower_connection_id)
&& Some(follower_peer_id) != leader_id
{
session.peer.forward_send(
session.connection_id,
follower_connection_id,
request.clone(),
)?;
}
}
Ok(())
@@ -1748,23 +1961,31 @@ async fn remove_contact(
let requester_id = session.user_id;
let responder_id = UserId::from_proto(request.user_id);
let db = session.db().await;
db.remove_contact(requester_id, responder_id).await?;
let contact_accepted = db.remove_contact(requester_id, responder_id).await?;
let pool = session.connection_pool().await;
// Update outgoing contact requests of requester
let mut update = proto::UpdateContacts::default();
update
.remove_outgoing_requests
.push(responder_id.to_proto());
if contact_accepted {
update.remove_contacts.push(responder_id.to_proto());
} else {
update
.remove_outgoing_requests
.push(responder_id.to_proto());
}
for connection_id in pool.user_connection_ids(requester_id) {
session.peer.send(connection_id, update.clone())?;
}
// Update incoming contact requests of responder
let mut update = proto::UpdateContacts::default();
update
.remove_incoming_requests
.push(requester_id.to_proto());
if contact_accepted {
update.remove_contacts.push(requester_id.to_proto());
} else {
update
.remove_incoming_requests
.push(requester_id.to_proto());
}
for connection_id in pool.user_connection_ids(responder_id) {
session.peer.send(connection_id, update.clone())?;
}
@@ -1781,7 +2002,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
@@ -1891,15 +2112,20 @@ fn contact_for_user(
}
fn room_updated(room: &proto::Room, peer: &Peer) {
for participant in &room.participants {
peer.send(
ConnectionId(participant.peer_id),
proto::RoomUpdated {
room: Some(room.clone()),
},
)
.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()),
},
)
},
);
}
async fn update_user_contacts(user_id: UserId, session: &Session) -> Result<()> {
@@ -1943,8 +2169,7 @@ async fn leave_room_for_session(session: &Session) -> Result<()> {
let canceled_calls_to_user_ids;
let live_kit_room;
let delete_live_kit_room;
{
let mut left_room = session.db().await.leave_room(session.connection_id).await?;
if let Some(mut left_room) = session.db().await.leave_room(session.connection_id).await? {
contacts_to_update.insert(session.user_id);
for project in left_room.left_projects.values() {
@@ -1956,6 +2181,8 @@ async fn leave_room_for_session(session: &Session) -> Result<()> {
canceled_calls_to_user_ids = mem::take(&mut left_room.canceled_calls_to_user_ids);
live_kit_room = mem::take(&mut left_room.room.live_kit_room);
delete_live_kit_room = left_room.room.participants.is_empty();
} else {
return Ok(());
}
{
@@ -1982,7 +2209,7 @@ async fn leave_room_for_session(session: &Session) -> Result<()> {
if let Some(live_kit) = session.live_kit_client.as_ref() {
live_kit
.remove_participant(live_kit_room.clone(), session.connection_id.to_string())
.remove_participant(live_kit_room.clone(), session.user_id.to_string())
.await
.trace_err();
@@ -2013,22 +2240,12 @@ fn project_left(project: &db::LeftProject, session: &Session) {
*connection_id,
proto::RemoveProjectCollaborator {
project_id: project.id.to_proto(),
peer_id: session.connection_id.0,
peer_id: Some(session.connection_id.into()),
},
)
.trace_err();
}
}
session
.peer
.send(
session.connection_id,
proto::UnshareProject {
project_id: project.id.to_proto(),
},
)
.trace_err();
}
pub trait ResultExt {

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

@@ -0,0 +1,462 @@
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;
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(Settings::test(cx));
});
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

@@ -2,6 +2,7 @@
name = "collab_ui"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/collab_ui.rs"

View File

@@ -1,6 +1,6 @@
use crate::{contact_notification::ContactNotification, contacts_popover};
use call::{ActiveCall, ParticipantLocation};
use client::{Authenticate, ContactEventKind, PeerId, User, UserStore};
use client::{proto::PeerId, Authenticate, ContactEventKind, User, UserStore};
use clock::ReplicaId;
use contacts_popover::ContactsPopover;
use gpui::{
@@ -342,24 +342,27 @@ impl CollabTitlebarItem {
let mut participants = room
.read(cx)
.remote_participants()
.iter()
.map(|(peer_id, collaborator)| (*peer_id, collaborator.clone()))
.values()
.cloned()
.collect::<Vec<_>>();
participants
.sort_by_key(|(peer_id, _)| Some(project.collaborators().get(peer_id)?.replica_id));
participants.sort_by_key(|p| Some(project.collaborators().get(&p.peer_id)?.replica_id));
participants
.into_iter()
.filter_map(|(peer_id, participant)| {
.filter_map(|participant| {
let project = workspace.read(cx).project().read(cx);
let replica_id = project
.collaborators()
.get(&peer_id)
.get(&participant.peer_id)
.map(|collaborator| collaborator.replica_id);
let user = participant.user.clone();
Some(self.render_avatar(
&user,
replica_id,
Some((peer_id, &user.github_login, participant.location)),
Some((
participant.peer_id,
&user.github_login,
participant.location,
)),
workspace,
theme,
cx,
@@ -474,7 +477,7 @@ impl CollabTitlebarItem {
cx.dispatch_action(ToggleFollow(peer_id))
})
.with_tooltip::<ToggleFollow, _>(
peer_id.0 as usize,
peer_id.as_u64() as usize,
if is_followed {
format!("Unfollow {}", peer_github_login)
} else {
@@ -487,22 +490,24 @@ impl CollabTitlebarItem {
.boxed()
} else if let ParticipantLocation::SharedProject { project_id } = location {
let user_id = user.id;
MouseEventHandler::<JoinProject>::new(peer_id.0 as usize, cx, move |_, _| content)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(JoinProject {
project_id,
follow_user_id: user_id,
})
MouseEventHandler::<JoinProject>::new(peer_id.as_u64() as usize, cx, move |_, _| {
content
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(JoinProject {
project_id,
follow_user_id: user_id,
})
.with_tooltip::<JoinProject, _>(
peer_id.0 as usize,
format!("Follow {} into external project", peer_github_login),
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.boxed()
})
.with_tooltip::<JoinProject, _>(
peer_id.as_u64() as usize,
format!("Follow {} into external project", peer_github_login),
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.boxed()
} else {
content
}

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,27 +39,35 @@ 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(
Default::default(),
0,
project,
app_state.dock_default_item_factory,
cx,
);
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
workspace
});
let (_, workspace) = cx.add_window(
(app_state.build_window_options)(None, None, cx.platform().as_ref()),
|cx| {
let mut workspace = Workspace::new(
Default::default(),
0,
project,
app_state.dock_default_item_factory,
cx,
);
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
workspace
},
);
workspace
};
@@ -73,7 +81,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
.remote_participants()
.iter()
.find(|(_, participant)| participant.user.id == follow_user_id)
.map(|(peer_id, _)| *peer_id)
.map(|(_, p)| p.peer_id)
.or_else(|| {
// If we couldn't follow the given user, follow the host instead.
let collaborator = workspace

View File

@@ -1,20 +1,22 @@
use std::{mem, sync::Arc};
use crate::contacts_popover;
use call::ActiveCall;
use client::{Contact, PeerId, User, UserStore};
use client::{proto::PeerId, Contact, User, UserStore};
use editor::{Cancel, Editor};
use futures::StreamExt;
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
elements::*,
geometry::{rect::RectF, vector::vec2f},
impl_actions, impl_internal_actions, keymap, 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, PromptLevel,
RenderContext, Subscription, View, ViewContext, ViewHandle,
};
use menu::{Confirm, SelectNext, SelectPrev};
use project::Project;
use serde::Deserialize;
use settings::Settings;
use std::{mem, sync::Arc};
use theme::IconButton;
use util::ResultExt;
use workspace::{JoinProject, OpenSharedScreen};
@@ -297,9 +299,19 @@ impl ContactList {
}
fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext<Self>) {
self.user_store
.update(cx, |store, cx| store.remove_contact(request.0, cx))
.detach();
let user_id = request.0;
let user_store = self.user_store.clone();
let prompt_message = "Are you sure you want to remove this contact?";
let mut answer = cx.prompt(PromptLevel::Warning, prompt_message, &["Remove", "Cancel"]);
cx.spawn(|_, mut cx| async move {
if answer.next().await == Some(0) {
user_store
.update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
.await
.unwrap();
}
})
.detach();
}
fn respond_to_contact_request(
@@ -461,15 +473,13 @@ impl ContactList {
// Populate remote participants.
self.match_candidates.clear();
self.match_candidates
.extend(
room.remote_participants()
.iter()
.map(|(peer_id, participant)| StringMatchCandidate {
id: peer_id.0 as usize,
string: participant.user.github_login.clone(),
char_bag: participant.user.github_login.chars().collect(),
}),
);
.extend(room.remote_participants().iter().map(|(_, participant)| {
StringMatchCandidate {
id: participant.user.id as usize,
string: participant.user.github_login.clone(),
char_bag: participant.user.github_login.chars().collect(),
}
}));
let matches = executor.block(match_strings(
&self.match_candidates,
&query,
@@ -479,8 +489,8 @@ impl ContactList {
executor.clone(),
));
for mat in matches {
let peer_id = PeerId(mat.candidate_id as u32);
let participant = &room.remote_participants()[&peer_id];
let user_id = mat.candidate_id as u64;
let participant = &room.remote_participants()[&user_id];
participant_entries.push(ContactEntry::CallParticipant {
user: participant.user.clone(),
is_pending: false,
@@ -496,7 +506,7 @@ impl ContactList {
}
if !participant.tracks.is_empty() {
participant_entries.push(ContactEntry::ParticipantScreen {
peer_id,
peer_id: participant.peer_id,
is_last: true,
});
}
@@ -881,75 +891,80 @@ impl ContactList {
let baseline_offset =
row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
MouseEventHandler::<OpenSharedScreen>::new(peer_id.0 as usize, cx, |mouse_state, _| {
let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
let row = theme.project_row.style_for(mouse_state, is_selected);
MouseEventHandler::<OpenSharedScreen>::new(
peer_id.as_u64() as usize,
cx,
|mouse_state, _| {
let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
let row = theme.project_row.style_for(mouse_state, is_selected);
Flex::row()
.with_child(
Stack::new()
.with_child(
Canvas::new(move |bounds, _, cx| {
let start_x = bounds.min_x() + (bounds.width() / 2.)
- (tree_branch.width / 2.);
let end_x = bounds.max_x();
let start_y = bounds.min_y();
let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
Flex::row()
.with_child(
Stack::new()
.with_child(
Canvas::new(move |bounds, _, cx| {
let start_x = bounds.min_x() + (bounds.width() / 2.)
- (tree_branch.width / 2.);
let end_x = bounds.max_x();
let start_y = bounds.min_y();
let end_y =
bounds.min_y() + baseline_offset - (cap_height / 2.);
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, start_y),
vec2f(
start_x + tree_branch.width,
if is_last { end_y } else { bounds.max_y() },
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, start_y),
vec2f(
start_x + tree_branch.width,
if is_last { end_y } else { bounds.max_y() },
),
),
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, end_y),
vec2f(end_x, end_y + tree_branch.width),
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
})
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, end_y),
vec2f(end_x, end_y + tree_branch.width),
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
})
.boxed(),
)
.constrained()
.with_width(host_avatar_height)
.boxed(),
)
.constrained()
.with_width(host_avatar_height)
.boxed(),
)
.with_child(
Svg::new("icons/disable_screen_sharing_12.svg")
.with_color(row.icon.color)
.constrained()
.with_width(row.icon.width)
.aligned()
.left()
.contained()
.with_style(row.icon.container)
.boxed(),
)
.with_child(
Label::new("Screen".into(), row.name.text.clone())
.aligned()
.left()
.contained()
.with_style(row.name.container)
.flex(1., false)
.boxed(),
)
.constrained()
.with_height(theme.row_height)
.contained()
.with_style(row.container)
.boxed()
})
)
.with_child(
Svg::new("icons/disable_screen_sharing_12.svg")
.with_color(row.icon.color)
.constrained()
.with_width(row.icon.width)
.aligned()
.left()
.contained()
.with_style(row.icon.container)
.boxed(),
)
.with_child(
Label::new("Screen".into(), row.name.text.clone())
.aligned()
.left()
.contained()
.with_style(row.name.container)
.flex(1., false)
.boxed(),
)
.constrained()
.with_height(theme.row_height)
.contained()
.with_style(row.container)
.boxed()
},
)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(OpenSharedScreen { peer_id });
@@ -1046,7 +1061,7 @@ impl ContactList {
let user_id = contact.user.id;
let initial_project = project.clone();
let mut element =
MouseEventHandler::<Contact>::new(contact.user.id as usize, cx, |_, _| {
MouseEventHandler::<Contact>::new(contact.user.id as usize, cx, |_, cx| {
Flex::row()
.with_children(contact.user.avatar.clone().map(|avatar| {
let status_badge = if contact.online {
@@ -1088,6 +1103,27 @@ impl ContactList {
.flex(1., true)
.boxed(),
)
.with_child(
MouseEventHandler::<Cancel>::new(
contact.user.id as usize,
cx,
|mouse_state, _| {
let button_style =
theme.contact_button.style_for(mouse_state, false);
render_icon_button(button_style, "icons/x_mark_8.svg")
.aligned()
.flex_float()
.boxed()
},
)
.with_padding(Padding::uniform(2.))
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(RemoveContact(user_id))
})
.flex_float()
.boxed(),
)
.with_children(if calling {
Some(
Label::new("Calling".to_string(), theme.calling_indicator.text.clone())
@@ -1264,7 +1300,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

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

View File

@@ -32,11 +32,12 @@ pub fn init(cx: &mut MutableAppContext) {
});
for screen in cx.platform().screens() {
let screen_size = screen.size();
let screen_bounds = screen.bounds();
let (window_id, _) = cx.add_window(
WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
screen_bounds.upper_right()
- vec2f(PADDING + window_size.x(), PADDING),
window_size,
)),
titlebar: None,
@@ -48,6 +49,7 @@ pub fn init(cx: &mut MutableAppContext) {
},
|_| IncomingCallNotification::new(incoming_call.clone()),
);
notification_windows.push(window_id);
}
}
@@ -225,6 +227,7 @@ impl View for IncomingCallNotification {
.theme
.incoming_call_notification
.background;
Flex::row()
.with_child(self.render_caller(cx))
.with_child(self.render_buttons(cx))

View File

@@ -31,11 +31,11 @@ pub fn init(cx: &mut MutableAppContext) {
let window_size = vec2f(theme.window_width, theme.window_height);
for screen in cx.platform().screens() {
let screen_size = screen.size();
let screen_bounds = screen.bounds();
let (window_id, _) = cx.add_window(
WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
screen_bounds.upper_right() - vec2f(PADDING + window_size.x(), PADDING),
window_size,
)),
titlebar: None,

View File

@@ -2,6 +2,7 @@
name = "collections"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/collections.rs"

View File

@@ -2,6 +2,7 @@
name = "command_palette"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/command_palette.rs"

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

@@ -2,6 +2,7 @@
name = "context_menu"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/context_menu.rs"

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

@@ -2,6 +2,7 @@
name = "db"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/db.rs"

View File

@@ -20,8 +20,8 @@ use std::fs::create_dir_all;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use util::{async_iife, ResultExt};
use util::channel::ReleaseChannel;
use util::{async_iife, ResultExt};
const CONNECTION_INITIALIZE_QUERY: &'static str = sql!(
PRAGMA foreign_keys=TRUE;
@@ -39,16 +39,24 @@ const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
const DB_FILE_NAME: &'static str = "db.sqlite";
lazy_static::lazy_static! {
static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
}
/// Open or create a database at the given directory path.
/// This will retry a couple times if there are failures. If opening fails once, the db directory
/// is moved to a backup folder and a new one is created. If that fails, a shared in memory db is created.
/// In either case, static variables are set so that the user can be notified.
pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, release_channel: &ReleaseChannel) -> ThreadSafeConnection<M> {
pub async fn open_db<M: Migrator + 'static>(
db_dir: &Path,
release_channel: &ReleaseChannel,
) -> ThreadSafeConnection<M> {
if *ZED_STATELESS {
return open_fallback_db().await;
}
let release_channel_name = release_channel.dev_name();
let main_db_dir = db_dir.join(Path::new(&format!("0-{}", release_channel_name)));
@@ -64,11 +72,11 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, release_channel: &Rel
//
// Basically: Don't ever push invalid migrations to stable or everyone will have
// a bad time.
// If no db folder, create one at 0-{channel}
create_dir_all(&main_db_dir).context("Could not create db directory")?;
let db_path = main_db_dir.join(Path::new(DB_FILE_NAME));
// Optimistically open databases in parallel
if !DB_FILE_OPERATIONS.is_locked() {
// Try building a connection
@@ -76,7 +84,7 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, release_channel: &Rel
return Ok(connection)
};
}
// Take a lock in the failure case so that we move the db once per process instead
// of potentially multiple times from different threads. This shouldn't happen in the
// normal path
@@ -84,12 +92,12 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, release_channel: &Rel
if let Some(connection) = open_main_db(&db_path).await {
return Ok(connection)
};
let backup_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("System clock is set before the unix timestamp, Zed does not support this region of spacetime")
.as_millis();
// If failed, move 0-{channel} to {current unix timestamp}-{channel}
let backup_db_dir = db_dir.join(Path::new(&format!(
"{}-{}",
@@ -105,7 +113,7 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, release_channel: &Rel
let mut guard = BACKUP_DB_PATH.write();
*guard = Some(backup_db_dir);
}
// Create a new 0-{channel}
create_dir_all(&main_db_dir).context("Should be able to create the database directory")?;
let db_path = main_db_dir.join(Path::new(DB_FILE_NAME));
@@ -117,10 +125,10 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, release_channel: &Rel
if let Some(connection) = connection {
return connection;
}
// Set another static ref so that we can escalate the notification
ALL_FILE_DB_FAILED.store(true, Ordering::Release);
// If still failed, create an in memory db with a known name
open_fallback_db().await
}
@@ -174,15 +182,15 @@ macro_rules! define_connection {
&self.0
}
}
impl $crate::sqlez::domain::Domain for $t {
fn name() -> &'static str {
stringify!($t)
}
fn migrations() -> &'static [&'static str] {
$migrations
}
}
}
#[cfg(any(test, feature = "test-support"))]
@@ -205,15 +213,15 @@ macro_rules! define_connection {
&self.0
}
}
impl $crate::sqlez::domain::Domain for $t {
fn name() -> &'static str {
stringify!($t)
}
fn migrations() -> &'static [&'static str] {
$migrations
}
}
}
#[cfg(any(test, feature = "test-support"))]
@@ -232,134 +240,155 @@ macro_rules! define_connection {
mod tests {
use std::{fs, thread};
use sqlez::{domain::Domain, connection::Connection};
use sqlez::{connection::Connection, domain::Domain};
use sqlez_macros::sql;
use tempdir::TempDir;
use crate::{open_db, DB_FILE_NAME};
// Test bad migration panics
#[gpui::test]
#[should_panic]
async fn test_bad_migration_panics() {
enum BadDB {}
impl Domain for BadDB {
fn name() -> &'static str {
"db_tests"
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test(value);),
&[
sql!(CREATE TABLE test(value);),
// failure because test already exists
sql!(CREATE TABLE test(value);)]
sql!(CREATE TABLE test(value);),
]
}
}
let tempdir = TempDir::new("DbTests").unwrap();
let _bad_db = open_db::<BadDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
}
/// Test that DB exists but corrupted (causing recreate)
#[gpui::test]
async fn test_db_corruption() {
enum CorruptedDB {}
impl Domain for CorruptedDB {
fn name() -> &'static str {
"db_tests"
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test(value);)]
}
}
enum GoodDB {}
impl Domain for GoodDB {
fn name() -> &'static str {
"db_tests" //Notice same name
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test2(value);)] //But different migration
}
}
let tempdir = TempDir::new("DbTests").unwrap();
{
let corrupt_db = open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
let corrupt_db =
open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
assert!(corrupt_db.persistent());
}
let good_db = open_db::<GoodDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
assert!(good_db.select_row::<usize>("SELECT * FROM test2").unwrap()().unwrap().is_none());
let mut corrupted_backup_dir = fs::read_dir(
tempdir.path()
).unwrap().find(|entry| {
!entry.as_ref().unwrap().file_name().to_str().unwrap().starts_with("0")
}
).unwrap().unwrap().path();
assert!(
good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
.unwrap()
.is_none()
);
let mut corrupted_backup_dir = fs::read_dir(tempdir.path())
.unwrap()
.find(|entry| {
!entry
.as_ref()
.unwrap()
.file_name()
.to_str()
.unwrap()
.starts_with("0")
})
.unwrap()
.unwrap()
.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().is_none());
assert!(backup.select_row::<usize>("SELECT * FROM test").unwrap()()
.unwrap()
.is_none());
}
/// Test that DB exists but corrupted (causing recreate)
#[gpui::test]
async fn test_simultaneous_db_corruption() {
enum CorruptedDB {}
impl Domain for CorruptedDB {
fn name() -> &'static str {
"db_tests"
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test(value);)]
}
}
enum GoodDB {}
impl Domain for GoodDB {
fn name() -> &'static str {
"db_tests" //Notice same name
}
fn migrations() -> &'static [&'static str] {
&[sql!(CREATE TABLE test2(value);)] //But different migration
}
}
let tempdir = TempDir::new("DbTests").unwrap();
{
// Setup the bad database
let corrupt_db = open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
let corrupt_db =
open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
assert!(corrupt_db.persistent());
}
// Try to connect to it a bunch of times at once
let mut guards = vec![];
for _ in 0..10 {
let tmp_path = tempdir.path().to_path_buf();
let guard = thread::spawn(move || {
let good_db = smol::block_on(open_db::<GoodDB>(tmp_path.as_path(), &util::channel::ReleaseChannel::Dev));
assert!(good_db.select_row::<usize>("SELECT * FROM test2").unwrap()().unwrap().is_none());
let good_db = smol::block_on(open_db::<GoodDB>(
tmp_path.as_path(),
&util::channel::ReleaseChannel::Dev,
));
assert!(
good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
.unwrap()
.is_none()
);
});
guards.push(guard);
}
for guard in guards.into_iter() {
assert!(guard.join().is_ok());
}
for guard in guards.into_iter() {
assert!(guard.join().is_ok());
}
}
}

View File

@@ -80,7 +80,7 @@ macro_rules! query {
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
self.select::<$return_type>(sql_stmt)?(())
self.select::<$return_type>(sql_stmt)?()
.context(::std::format!(
"Error in {}, select_row failed to execute or parse for: {}",
::std::stringify!($id),
@@ -95,7 +95,7 @@ macro_rules! query {
self.write(|connection| {
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
connection.select::<$return_type>(sql_stmt)?(())
connection.select::<$return_type>(sql_stmt)?()
.context(::std::format!(
"Error in {}, select_row failed to execute or parse for: {}",
::std::stringify!($id),

View File

@@ -2,6 +2,7 @@
name = "diagnostics"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/diagnostics.rs"

View File

@@ -21,7 +21,6 @@ use language::{
use project::{DiagnosticSummary, Project, ProjectPath};
use serde_json::json;
use settings::Settings;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
cmp::Ordering,
@@ -164,7 +163,7 @@ impl ProjectDiagnosticsEditor {
editor.set_vertical_scroll_margin(5, cx);
editor
});
cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()))
.detach();
let project = project_handle.read(cx);
@@ -521,12 +520,8 @@ impl Item for ProjectDiagnosticsEditor {
)
}
fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
None
}
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[project::ProjectEntryId; 3]> {
self.editor.project_entry_ids(cx)
fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
self.editor.for_each_project_item(cx, f)
}
fn is_singleton(&self, _: &AppContext) -> bool {
@@ -575,6 +570,15 @@ impl Item for ProjectDiagnosticsEditor {
unreachable!()
}
fn git_diff_recalc(
&mut self,
project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.editor
.update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
}
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
Editor::to_item_events(event)
}

View File

@@ -2,6 +2,7 @@
name = "drag_and_drop"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/drag_and_drop.rs"

View File

@@ -2,6 +2,7 @@
name = "editor"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/editor.rs"

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,
@@ -43,7 +44,7 @@ use gpui::{
ViewContext, ViewHandle, WeakViewHandle,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
use hover_popover::{hide_hover, HideHover, HoverState};
pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools;
pub use language::{char_kind, CharKind};
@@ -61,7 +62,7 @@ pub use multi_buffer::{
};
use multi_buffer::{MultiBufferChunks, ToOffsetUtf16};
use ordered_float::OrderedFloat;
use project::{FormatTrigger, LocationLink, Project, ProjectPath, ProjectTransaction};
use project::{FormatTrigger, Location, LocationLink, Project, ProjectPath, ProjectTransaction};
use scroll::{
autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide,
};
@@ -84,7 +85,7 @@ use std::{
pub use sum_tree::Bias;
use theme::{DiagnosticStyle, Theme};
use util::{post_inc, ResultExt, TryFutureExt};
use workspace::{ItemNavHistory, Workspace, WorkspaceId};
use workspace::{ItemNavHistory, ViewId, Workspace, WorkspaceId};
use crate::git::diff_hunk_to_display;
@@ -464,9 +465,10 @@ 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>,
hover_state: HoverState,
link_go_to_definition_state: LinkGoToDefinitionState,
_subscriptions: Vec<Subscription>,
@@ -826,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];
(
@@ -989,6 +1008,15 @@ impl Editor {
Self::new(EditorMode::SingleLine, buffer, None, field_editor_style, cx)
}
pub fn multi_line(
field_editor_style: Option<Arc<GetFieldEditorTheme>>,
cx: &mut ViewContext<Self>,
) -> Self {
let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
Self::new(EditorMode::Full, buffer, None, field_editor_style, cx)
}
pub fn auto_height(
max_lines: usize,
field_editor_style: Option<Arc<GetFieldEditorTheme>>,
@@ -1067,6 +1095,8 @@ impl Editor {
let blink_manager = cx.add_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
let soft_wrap_mode_override =
(mode == EditorMode::SingleLine).then(|| settings::SoftWrap::None);
let mut this = Self {
handle: cx.weak_handle(),
buffer: buffer.clone(),
@@ -1082,7 +1112,7 @@ impl Editor {
select_larger_syntax_node_stack: Vec::new(),
ime_transaction: Default::default(),
active_diagnostics: None,
soft_wrap_mode_override: None,
soft_wrap_mode_override,
get_field_editor_theme,
project,
focused: false,
@@ -1108,6 +1138,7 @@ impl Editor {
keymap_context_layers: Default::default(),
input_enabled: true,
leader_replica_id: None,
remote_id: None,
hover_state: Default::default(),
link_go_to_definition_state: Default::default(),
_subscriptions: vec![
@@ -1223,7 +1254,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);
}
@@ -1299,7 +1330,7 @@ impl Editor {
}
}
hide_hover(self, cx);
hide_hover(self, &HideHover, cx);
if old_cursor_position.to_display_point(&display_map).row()
!= new_cursor_position.to_display_point(&display_map).row()
@@ -1674,7 +1705,7 @@ impl Editor {
return;
}
if hide_hover(self, cx) {
if hide_hover(self, &HideHover, cx) {
return;
}
@@ -1715,7 +1746,7 @@ impl Editor {
for (selection, autoclose_region) in
self.selections_with_autoclose_regions(selections, &snapshot)
{
if let Some(language) = snapshot.language_at(selection.head()) {
if let Some(language) = snapshot.language_scope_at(selection.head()) {
// Determine if the inserted text matches the opening or closing
// bracket of any of this language's bracket pairs.
let mut bracket_pair = None;
@@ -1876,7 +1907,7 @@ impl Editor {
let end = selection.end;
let mut insert_extra_newline = false;
if let Some(language) = buffer.language_at(start) {
if let Some(language) = buffer.language_scope_at(start) {
let leading_whitespace_len = buffer
.reversed_chars_at(start)
.take_while(|c| c.is_whitespace() && *c != '\n')
@@ -2000,7 +2031,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
@@ -4509,7 +4542,10 @@ impl Editor {
// TODO: Handle selections that cross excerpts
for selection in &mut selections {
let language = if let Some(language) = snapshot.language_at(selection.start) {
let start_column = snapshot.indent_size_for_line(selection.start.row).len;
let language = if let Some(language) =
snapshot.language_scope_at(Point::new(selection.start.row, start_column))
{
language
} else {
continue;
@@ -4779,7 +4815,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()]);
@@ -4825,7 +4861,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,
@@ -4900,7 +4936,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]);
@@ -4985,25 +5021,49 @@ impl Editor {
cx: &mut ViewContext<Workspace>,
) {
let pane = workspace.active_pane().clone();
for definition in definitions {
// If there is one definition, just open it directly
if let [definition] = definitions.as_slice() {
let range = definition
.target
.range
.to_offset(definition.target.buffer.read(cx));
let target_editor_handle = workspace.open_project_item(definition.target.buffer, cx);
let target_editor_handle =
workspace.open_project_item(definition.target.buffer.clone(), cx);
target_editor_handle.update(cx, |target_editor, cx| {
// When selecting a definition in a different buffer, disable the nav history
// to avoid creating a history entry at the previous cursor location.
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]);
});
pane.update(cx, |pane, _| pane.enable_history());
});
} else if !definitions.is_empty() {
let replica_id = editor_handle.read(cx).replica_id(cx);
let title = definitions
.iter()
.find(|definition| definition.origin.is_some())
.and_then(|definition| {
definition.origin.as_ref().map(|origin| {
let buffer = origin.buffer.read(cx);
format!(
"Definitions for {}",
buffer
.text_for_range(origin.range.clone())
.collect::<String>()
)
})
})
.unwrap_or("Definitions".to_owned());
let locations = definitions
.into_iter()
.map(|definition| definition.target)
.collect();
Self::open_locations_in_multibuffer(workspace, locations, replica_id, title, cx)
}
}
@@ -5024,64 +5084,87 @@ impl Editor {
let project = workspace.project().clone();
let references = project.update(cx, |project, cx| project.references(&buffer, head, cx));
Some(cx.spawn(|workspace, mut cx| async move {
let mut locations = references.await?;
let locations = references.await?;
if locations.is_empty() {
return Ok(());
}
locations.sort_by_key(|location| location.buffer.id());
let mut locations = locations.into_iter().peekable();
let mut ranges_to_highlight = Vec::new();
let excerpt_buffer = cx.add_model(|cx| {
let mut symbol_name = None;
let mut multibuffer = MultiBuffer::new(replica_id);
while let Some(location) = locations.next() {
let buffer = location.buffer.read(cx);
let mut ranges_for_buffer = Vec::new();
let range = location.range.to_offset(buffer);
ranges_for_buffer.push(range.clone());
if symbol_name.is_none() {
symbol_name = Some(buffer.text_for_range(range).collect::<String>());
}
while let Some(next_location) = locations.peek() {
if next_location.buffer == location.buffer {
ranges_for_buffer.push(next_location.range.to_offset(buffer));
locations.next();
} else {
break;
}
}
ranges_for_buffer.sort_by_key(|range| (range.start, Reverse(range.end)));
ranges_to_highlight.extend(multibuffer.push_excerpts_with_context_lines(
location.buffer.clone(),
ranges_for_buffer,
1,
cx,
));
}
multibuffer.with_title(format!("References to `{}`", symbol_name.unwrap()))
});
workspace.update(&mut cx, |workspace, cx| {
let editor =
cx.add_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx));
editor.update(cx, |editor, cx| {
editor.highlight_background::<Self>(
ranges_to_highlight,
|theme| theme.editor.highlighted_line_background,
cx,
);
});
workspace.add_item(Box::new(editor), cx);
let title = locations
.first()
.as_ref()
.map(|location| {
let buffer = location.buffer.read(cx);
format!(
"References to `{}`",
buffer
.text_for_range(location.range.clone())
.collect::<String>()
)
})
.unwrap();
Self::open_locations_in_multibuffer(workspace, locations, replica_id, title, cx);
});
Ok(())
}))
}
/// Opens a multibuffer with the given project locations in it
pub fn open_locations_in_multibuffer(
workspace: &mut Workspace,
mut locations: Vec<Location>,
replica_id: ReplicaId,
title: String,
cx: &mut ViewContext<Workspace>,
) {
// If there are multiple definitions, open them in a multibuffer
locations.sort_by_key(|location| location.buffer.id());
let mut locations = locations.into_iter().peekable();
let mut ranges_to_highlight = Vec::new();
let excerpt_buffer = cx.add_model(|cx| {
let mut multibuffer = MultiBuffer::new(replica_id);
while let Some(location) = locations.next() {
let buffer = location.buffer.read(cx);
let mut ranges_for_buffer = Vec::new();
let range = location.range.to_offset(buffer);
ranges_for_buffer.push(range.clone());
while let Some(next_location) = locations.peek() {
if next_location.buffer == location.buffer {
ranges_for_buffer.push(next_location.range.to_offset(buffer));
locations.next();
} else {
break;
}
}
ranges_for_buffer.sort_by_key(|range| (range.start, Reverse(range.end)));
ranges_to_highlight.extend(multibuffer.push_excerpts_with_context_lines(
location.buffer.clone(),
ranges_for_buffer,
1,
cx,
))
}
multibuffer.with_title(title)
});
let editor = cx.add_view(|cx| {
Editor::for_multibuffer(excerpt_buffer, Some(workspace.project().clone()), cx)
});
editor.update(cx, |editor, cx| {
editor.highlight_background::<Self>(
ranges_to_highlight,
|theme| theme.editor.highlighted_line_background,
cx,
);
});
workspace.add_item(Box::new(editor), cx);
}
pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
use language::ToOffset as _;
@@ -5451,11 +5534,17 @@ impl Editor {
pub fn set_selections_from_remote(
&mut self,
selections: Vec<Selection<Anchor>>,
pending_selection: Option<Selection<Anchor>>,
cx: &mut ViewContext<Self>,
) {
let old_cursor_position = self.selections.newest_anchor().head();
self.selections.change_with(cx, |s| {
s.select_anchors(selections);
if let Some(pending_selection) = pending_selection {
s.set_pending(pending_selection, SelectMode::Character);
} else {
s.clear_pending();
}
});
self.selections_did_change(false, &old_cursor_position, cx);
}
@@ -5883,25 +5972,36 @@ impl Editor {
fn on_buffer_event(
&mut self,
_: ModelHandle<MultiBuffer>,
event: &language::Event,
event: &multi_buffer::Event,
cx: &mut ViewContext<Self>,
) {
match event {
language::Event::Edited => {
multi_buffer::Event::Edited => {
self.refresh_active_diagnostics(cx);
self.refresh_code_actions(cx);
cx.emit(Event::BufferEdited);
}
language::Event::Reparsed => cx.emit(Event::Reparsed),
language::Event::DirtyChanged => cx.emit(Event::DirtyChanged),
language::Event::Saved => cx.emit(Event::Saved),
language::Event::FileHandleChanged => cx.emit(Event::TitleChanged),
language::Event::Reloaded => cx.emit(Event::TitleChanged),
language::Event::Closed => cx.emit(Event::Closed),
language::Event::DiagnosticsUpdated => {
multi_buffer::Event::ExcerptsAdded {
buffer,
predecessor,
excerpts,
} => cx.emit(Event::ExcerptsAdded {
buffer: buffer.clone(),
predecessor: *predecessor,
excerpts: excerpts.clone(),
}),
multi_buffer::Event::ExcerptsRemoved { ids } => {
cx.emit(Event::ExcerptsRemoved { ids: ids.clone() })
}
multi_buffer::Event::Reparsed => cx.emit(Event::Reparsed),
multi_buffer::Event::DirtyChanged => cx.emit(Event::DirtyChanged),
multi_buffer::Event::Saved => cx.emit(Event::Saved),
multi_buffer::Event::FileHandleChanged => cx.emit(Event::TitleChanged),
multi_buffer::Event::Reloaded => cx.emit(Event::TitleChanged),
multi_buffer::Event::Closed => cx.emit(Event::Closed),
multi_buffer::Event::DiagnosticsUpdated => {
self.refresh_active_diagnostics(cx);
}
_ => {}
}
}
@@ -6050,10 +6150,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(),
);
}
}
}
@@ -6084,8 +6185,16 @@ impl Deref for EditorSnapshot {
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
ExcerptsAdded {
buffer: ModelHandle<Buffer>,
predecessor: ExcerptId,
excerpts: Vec<(ExcerptId, ExcerptRange<language::Anchor>)>,
},
ExcerptsRemoved {
ids: Vec<ExcerptId>,
},
BufferEdited,
Edited,
Reparsed,
@@ -6093,8 +6202,12 @@ pub enum Event {
DirtyChanged,
Saved,
TitleChanged,
SelectionsChanged { local: bool },
ScrollPositionChanged { local: bool },
SelectionsChanged {
local: bool,
},
ScrollPositionChanged {
local: bool,
},
Closed,
}
@@ -6122,7 +6235,7 @@ impl View for Editor {
cx.defer(move |cx| {
if let Some(editor) = handle.upgrade(cx) {
editor.update(cx, |editor, cx| {
hide_hover(editor, cx);
hide_hover(editor, &HideHover, cx);
hide_link_definition(editor, cx);
})
}
@@ -6171,7 +6284,7 @@ impl View for Editor {
self.buffer
.update(cx, |buffer, cx| buffer.remove_active_selections(cx));
self.hide_context_menu(cx);
hide_hover(self, cx);
hide_hover(self, &HideHover, cx);
cx.emit(Event::Blurred);
cx.notify();
}
@@ -6214,7 +6327,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",
@@ -6768,6 +6881,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

@@ -1,8 +1,7 @@
use std::{cell::RefCell, rc::Rc, time::Instant};
use drag_and_drop::DragAndDrop;
use futures::StreamExt;
use indoc::indoc;
use std::{cell::RefCell, rc::Rc, time::Instant};
use unindent::Unindent;
use super::*;
@@ -24,13 +23,17 @@ use util::{
};
use workspace::{
item::{FollowableItem, ItemHandle},
NavigationEntry, Pane,
NavigationEntry, Pane, ViewId,
};
#[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(), {
@@ -41,7 +44,7 @@ fn test_edit_events(cx: &mut MutableAppContext) {
event,
Event::Edited | Event::BufferEdited | Event::DirtyChanged
) {
events.borrow_mut().push(("editor1", *event));
events.borrow_mut().push(("editor1", event.clone()));
}
})
.detach();
@@ -56,7 +59,7 @@ fn test_edit_events(cx: &mut MutableAppContext) {
event,
Event::Edited | Event::BufferEdited | Event::DirtyChanged
) {
events.borrow_mut().push(("editor2", *event));
events.borrow_mut().push(("editor2", event.clone()));
}
})
.detach();
@@ -3503,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),
@@ -4969,19 +4974,27 @@ fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) {
}
#[gpui::test]
fn test_following(cx: &mut gpui::MutableAppContext) {
let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
async fn test_following(cx: &mut gpui::TestAppContext) {
Settings::test_async(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
cx.set_global(Settings::test(cx));
let (_, leader) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
let (_, follower) = cx.add_window(
WindowOptions {
bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))),
..Default::default()
},
|cx| build_editor(buffer.clone(), cx),
);
let buffer = project.update(cx, |project, cx| {
let buffer = project
.create_buffer(&sample_text(16, 8, 'a'), None, cx)
.unwrap();
cx.add_model(|cx| MultiBuffer::singleton(buffer, cx))
});
let (_, leader) = cx.add_window(|cx| build_editor(buffer.clone(), cx));
let (_, follower) = cx.update(|cx| {
cx.add_window(
WindowOptions {
bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))),
..Default::default()
},
|cx| build_editor(buffer.clone(), cx),
)
});
let is_still_following = Rc::new(RefCell::new(true));
let pending_update = Rc::new(RefCell::new(None));
@@ -5009,44 +5022,50 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
leader.update(cx, |leader, cx| {
leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
});
follower.update(cx, |follower, cx| {
follower
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
.unwrap();
follower
.update(cx, |follower, cx| {
follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
})
.await
.unwrap();
follower.read_with(cx, |follower, cx| {
assert_eq!(follower.selections.ranges(cx), vec![1..1]);
});
assert_eq!(follower.read(cx).selections.ranges(cx), vec![1..1]);
assert_eq!(*is_still_following.borrow(), true);
// Update the scroll position only
leader.update(cx, |leader, cx| {
leader.set_scroll_position(vec2f(1.5, 3.5), cx);
});
follower.update(cx, |follower, cx| {
follower
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
.unwrap();
});
follower
.update(cx, |follower, cx| {
follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
})
.await
.unwrap();
assert_eq!(
follower.update(cx, |follower, cx| follower.scroll_position(cx)),
vec2f(1.5, 3.5)
);
assert_eq!(*is_still_following.borrow(), true);
// Update the selections and scroll position
// Update the selections and scroll position. The follower's scroll position is updated
// via autoscroll, not via the leader's exact scroll position.
leader.update(cx, |leader, cx| {
leader.change_selections(None, cx, |s| s.select_ranges([0..0]));
leader.request_autoscroll(Autoscroll::newest(), cx);
leader.set_scroll_position(vec2f(1.5, 3.5), cx);
});
follower
.update(cx, |follower, cx| {
follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
})
.await
.unwrap();
follower.update(cx, |follower, cx| {
let initial_scroll_position = follower.scroll_position(cx);
follower
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
.unwrap();
assert_eq!(follower.scroll_position(cx), initial_scroll_position);
assert!(follower.scroll_manager.has_autoscroll_request());
assert_eq!(follower.scroll_position(cx), vec2f(1.5, 0.0));
assert_eq!(follower.selections.ranges(cx), vec![0..0]);
});
assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..0]);
assert_eq!(*is_still_following.borrow(), true);
// Creating a pending selection that precedes another selection
@@ -5054,24 +5073,30 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
leader.begin_selection(DisplayPoint::new(0, 0), true, 1, cx);
});
follower.update(cx, |follower, cx| {
follower
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
.unwrap();
follower
.update(cx, |follower, cx| {
follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
})
.await
.unwrap();
follower.read_with(cx, |follower, cx| {
assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
});
assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..0, 1..1]);
assert_eq!(*is_still_following.borrow(), true);
// Extend the pending selection so that it surrounds another selection
leader.update(cx, |leader, cx| {
leader.extend_selection(DisplayPoint::new(0, 2), 1, cx);
});
follower.update(cx, |follower, cx| {
follower
.apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
.unwrap();
follower
.update(cx, |follower, cx| {
follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
})
.await
.unwrap();
follower.read_with(cx, |follower, cx| {
assert_eq!(follower.selections.ranges(cx), vec![0..2]);
});
assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..2]);
// Scrolling locally breaks the follow
follower.update(cx, |follower, cx| {
@@ -5087,6 +5112,165 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
assert_eq!(*is_still_following.borrow(), false);
}
#[gpui::test]
async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
Settings::test_async(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
let (_, pane) = cx.add_window(|cx| Pane::new(None, cx));
let leader = pane.update(cx, |_, cx| {
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
cx.add_view(|cx| build_editor(multibuffer.clone(), cx))
});
// Start following the editor when it has no excerpts.
let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
let follower_1 = cx
.update(|cx| {
Editor::from_state_proto(
pane.clone(),
project.clone(),
ViewId {
creator: Default::default(),
id: 0,
},
&mut state_message,
cx,
)
})
.unwrap()
.await
.unwrap();
let update_message = Rc::new(RefCell::new(None));
follower_1.update(cx, {
let update = update_message.clone();
|_, cx| {
cx.subscribe(&leader, move |_, leader, event, cx| {
leader
.read(cx)
.add_event_to_update_proto(event, &mut *update.borrow_mut(), cx);
})
.detach();
}
});
let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
(
project
.create_buffer("abc\ndef\nghi\njkl\n", None, cx)
.unwrap(),
project
.create_buffer("mno\npqr\nstu\nvwx\n", None, cx)
.unwrap(),
)
});
// Insert some excerpts.
leader.update(cx, |leader, cx| {
leader.buffer.update(cx, |multibuffer, cx| {
let excerpt_ids = multibuffer.push_excerpts(
buffer_1.clone(),
[
ExcerptRange {
context: 1..6,
primary: None,
},
ExcerptRange {
context: 12..15,
primary: None,
},
ExcerptRange {
context: 0..3,
primary: None,
},
],
cx,
);
multibuffer.insert_excerpts_after(
excerpt_ids[0],
buffer_2.clone(),
[
ExcerptRange {
context: 8..12,
primary: None,
},
ExcerptRange {
context: 0..6,
primary: None,
},
],
cx,
);
});
});
// Apply the update of adding the excerpts.
follower_1
.update(cx, |follower, cx| {
follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
})
.await
.unwrap();
assert_eq!(
follower_1.read_with(cx, Editor::text),
leader.read_with(cx, Editor::text)
);
update_message.borrow_mut().take();
// Start following separately after it already has excerpts.
let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
let follower_2 = cx
.update(|cx| {
Editor::from_state_proto(
pane.clone(),
project.clone(),
ViewId {
creator: Default::default(),
id: 0,
},
&mut state_message,
cx,
)
})
.unwrap()
.await
.unwrap();
assert_eq!(
follower_2.read_with(cx, Editor::text),
leader.read_with(cx, Editor::text)
);
// Remove some excerpts.
leader.update(cx, |leader, cx| {
leader.buffer.update(cx, |multibuffer, cx| {
let excerpt_ids = multibuffer.excerpt_ids();
multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
multibuffer.remove_excerpts([excerpt_ids[0]], cx);
});
});
// Apply the update of removing the excerpts.
follower_1
.update(cx, |follower, cx| {
follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
})
.await
.unwrap();
follower_2
.update(cx, |follower, cx| {
follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
})
.await
.unwrap();
update_message.borrow_mut().take();
assert_eq!(
follower_1.read_with(cx, Editor::text),
leader.read_with(cx, Editor::text)
);
}
#[test]
fn test_combine_syntax_and_fuzzy_match_highlights() {
let string = "abcdefghijklmnop";
@@ -5261,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

@@ -7,7 +7,7 @@ use crate::{
display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
git::{diff_hunk_to_display, DisplayDiffHunk},
hover_popover::{
HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
HideHover, HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
},
link_go_to_definition::{
GoToFetchedDefinition, GoToFetchedTypeDefinition, UpdateGoToDefinitionLink,
@@ -114,6 +114,7 @@ impl EditorElement {
fn attach_mouse_handlers(
view: &WeakViewHandle<Editor>,
position_map: &Arc<PositionMap>,
has_popovers: bool,
visible_bounds: RectF,
text_bounds: RectF,
gutter_bounds: RectF,
@@ -190,6 +191,11 @@ impl EditorElement {
}
}
})
.on_move_out(move |_, cx| {
if has_popovers {
cx.dispatch_action(HideHover);
}
})
.on_scroll({
let position_map = position_map.clone();
move |e, cx| {
@@ -1870,6 +1876,7 @@ impl Element for EditorElement {
Self::attach_mouse_handlers(
&self.view,
&layout.position_map,
layout.hover_popovers.is_some(),
visible_bounds,
text_bounds,
gutter_bounds,

View File

@@ -29,12 +29,16 @@ pub struct HoverAt {
pub point: Option<DisplayPoint>,
}
#[derive(Copy, Clone, PartialEq)]
pub struct HideHover;
actions!(editor, [Hover]);
impl_internal_actions!(editor, [HoverAt]);
impl_internal_actions!(editor, [HoverAt, HideHover]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(hover);
cx.add_action(hover_at);
cx.add_action(hide_hover);
}
/// Bindable action which uses the most recent selection head to trigger a hover
@@ -50,7 +54,7 @@ pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext<Edit
if let Some(point) = action.point {
show_hover(editor, point, false, cx);
} else {
hide_hover(editor, cx);
hide_hover(editor, &HideHover, cx);
}
}
}
@@ -58,7 +62,7 @@ pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext<Edit
/// Hides the type information popup.
/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
/// selections changed.
pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
pub fn hide_hover(editor: &mut Editor, _: &HideHover, cx: &mut ViewContext<Editor>) -> bool {
let did_hide = editor.hover_state.info_popover.take().is_some()
| editor.hover_state.diagnostic_popover.take().is_some();
@@ -67,6 +71,10 @@ pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
editor.clear_background_highlights::<HoverState>(cx);
if did_hide {
cx.notify();
}
did_hide
}
@@ -121,7 +129,7 @@ fn show_hover(
// Hover triggered from same location as last time. Don't show again.
return;
} else {
hide_hover(editor, cx);
hide_hover(editor, &HideHover, cx);
}
}
}
@@ -323,7 +331,7 @@ impl InfoPopover {
if let Some(language) = content
.language
.clone()
.and_then(|language| project.languages().get_language(&language))
.and_then(|language| project.languages().language_for_name(&language))
{
let runs = language
.highlight_text(&content.text.as_str().into(), 0..content.text.len());

View File

@@ -1,109 +1,162 @@
use crate::{
display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
FORMAT_TIMEOUT,
};
use anyhow::{anyhow, Context, Result};
use collections::HashSet;
use futures::future::try_join_all;
use futures::FutureExt;
use gpui::{
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use language::{Bias, Buffer, File as _, OffsetRangeExt, Point, SelectionGoal};
use project::{File, FormatTrigger, Project, ProjectEntryId, ProjectPath};
use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
SelectionGoal,
};
use project::{FormatTrigger, Item as _, Project, ProjectPath};
use rpc::proto::{self, update_view};
use settings::Settings;
use smallvec::SmallVec;
use std::{
borrow::Cow,
cmp::{self, Ordering},
fmt::Write,
iter,
ops::Range,
path::{Path, PathBuf},
};
use text::Selection;
use util::{ResultExt, TryFutureExt};
use workspace::item::FollowableItemHandle;
use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, Workspace, WorkspaceId,
};
use crate::{
display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
Event, ExcerptId, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
FORMAT_TIMEOUT,
ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, ViewId, Workspace,
WorkspaceId,
};
pub const MAX_TAB_TITLE_LEN: usize = 24;
impl FollowableItem for Editor {
fn remote_id(&self) -> Option<ViewId> {
self.remote_id
}
fn from_state_proto(
pane: ViewHandle<workspace::Pane>,
project: ModelHandle<Project>,
remote_id: ViewId,
state: &mut Option<proto::view::Variant>,
cx: &mut MutableAppContext,
) -> Option<Task<Result<ViewHandle<Self>>>> {
let state = if matches!(state, Some(proto::view::Variant::Editor(_))) {
if let Some(proto::view::Variant::Editor(state)) = state.take() {
state
} else {
unreachable!()
}
} else {
return None;
};
let Some(proto::view::Variant::Editor(_)) = state else { return None };
let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() };
let buffer = project.update(cx, |project, cx| {
project.open_buffer_by_id(state.buffer_id, cx)
let client = project.read(cx).client();
let replica_id = project.read(cx).replica_id();
let buffer_ids = state
.excerpts
.iter()
.map(|excerpt| excerpt.buffer_id)
.collect::<HashSet<_>>();
let buffers = project.update(cx, |project, cx| {
buffer_ids
.iter()
.map(|id| project.open_buffer_by_id(*id, cx))
.collect::<Vec<_>>()
});
Some(cx.spawn(|mut cx| async move {
let buffer = buffer.await?;
let editor = pane
.read_with(&cx, |pane, cx| {
pane.items_of_type::<Self>().find(|editor| {
editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffer)
})
let mut buffers = futures::future::try_join_all(buffers).await?;
let editor = pane.read_with(&cx, |pane, cx| {
let mut editors = pane.items_of_type::<Self>();
editors.find(|editor| {
editor.remote_id(&client, cx) == Some(remote_id)
|| state.singleton
&& buffers.len() == 1
&& editor.read(cx).buffer.read(cx).as_singleton().as_ref()
== Some(&buffers[0])
})
.unwrap_or_else(|| {
pane.update(&mut cx, |_, cx| {
cx.add_view(|cx| Editor::for_buffer(buffer, Some(project), cx))
})
});
});
let editor = editor.unwrap_or_else(|| {
pane.update(&mut cx, |_, cx| {
let multibuffer = cx.add_model(|cx| {
let mut multibuffer;
if state.singleton && buffers.len() == 1 {
multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
} else {
multibuffer = MultiBuffer::new(replica_id);
let mut excerpts = state.excerpts.into_iter().peekable();
while let Some(excerpt) = excerpts.peek() {
let buffer_id = excerpt.buffer_id;
let buffer_excerpts = iter::from_fn(|| {
let excerpt = excerpts.peek()?;
(excerpt.buffer_id == buffer_id)
.then(|| excerpts.next().unwrap())
});
let buffer =
buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
if let Some(buffer) = buffer {
multibuffer.push_excerpts(
buffer.clone(),
buffer_excerpts.filter_map(deserialize_excerpt_range),
cx,
);
}
}
};
if let Some(title) = &state.title {
multibuffer = multibuffer.with_title(title.clone())
}
multibuffer
});
cx.add_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx))
})
});
editor.update(&mut cx, |editor, cx| {
let excerpt_id;
let buffer_id;
{
let buffer = editor.buffer.read(cx).read(cx);
let singleton = buffer.as_singleton().unwrap();
excerpt_id = singleton.0.clone();
buffer_id = singleton.1;
}
editor.remote_id = Some(remote_id);
let buffer = editor.buffer.read(cx).read(cx);
let selections = state
.selections
.into_iter()
.map(|selection| {
deserialize_selection(&excerpt_id, buffer_id, selection)
deserialize_selection(&buffer, selection)
.ok_or_else(|| anyhow!("invalid selection"))
})
.collect::<Result<Vec<_>>>()?;
if !selections.is_empty() {
editor.set_selections_from_remote(selections, cx);
let pending_selection = state
.pending_selection
.map(|selection| deserialize_selection(&buffer, selection))
.flatten();
let scroll_top_anchor = state
.scroll_top_anchor
.and_then(|anchor| deserialize_anchor(&buffer, anchor));
drop(buffer);
if !selections.is_empty() || pending_selection.is_some() {
editor.set_selections_from_remote(selections, pending_selection, cx);
}
if let Some(anchor) = state.scroll_top_anchor {
if let Some(scroll_top_anchor) = scroll_top_anchor {
editor.set_scroll_anchor_remote(
ScrollAnchor {
top_anchor: Anchor {
buffer_id: Some(state.buffer_id as usize),
excerpt_id,
text_anchor: language::proto::deserialize_anchor(anchor)
.ok_or_else(|| anyhow!("invalid scroll top"))?,
},
top_anchor: scroll_top_anchor,
offset: vec2f(state.scroll_x, state.scroll_y),
},
cx,
);
}
Ok::<_, anyhow::Error>(())
anyhow::Ok(())
})?;
Ok(editor)
}))
}
@@ -134,13 +187,32 @@ impl FollowableItem for Editor {
}
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id();
let buffer = self.buffer.read(cx);
let scroll_anchor = self.scroll_manager.anchor();
let excerpts = buffer
.read(cx)
.excerpts()
.map(|(id, buffer, range)| proto::Excerpt {
id: id.to_proto(),
buffer_id: buffer.remote_id(),
context_start: Some(serialize_text_anchor(&range.context.start)),
context_end: Some(serialize_text_anchor(&range.context.end)),
primary_start: range
.primary
.as_ref()
.map(|range| serialize_text_anchor(&range.start)),
primary_end: range
.primary
.as_ref()
.map(|range| serialize_text_anchor(&range.end)),
})
.collect();
Some(proto::view::Variant::Editor(proto::view::Editor {
buffer_id,
scroll_top_anchor: Some(language::proto::serialize_anchor(
&scroll_anchor.top_anchor.text_anchor,
)),
singleton: buffer.is_singleton(),
title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()),
excerpts,
scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.top_anchor)),
scroll_x: scroll_anchor.offset.x(),
scroll_y: scroll_anchor.offset.y(),
selections: self
@@ -149,6 +221,11 @@ impl FollowableItem for Editor {
.iter()
.map(serialize_selection)
.collect(),
pending_selection: self
.selections
.pending_anchor()
.as_ref()
.map(serialize_selection),
}))
}
@@ -156,18 +233,43 @@ impl FollowableItem for Editor {
&self,
event: &Self::Event,
update: &mut Option<proto::update_view::Variant>,
_: &AppContext,
cx: &AppContext,
) -> bool {
let update =
update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
match update {
proto::update_view::Variant::Editor(update) => match event {
Event::ExcerptsAdded {
buffer,
predecessor,
excerpts,
} => {
let buffer_id = buffer.read(cx).remote_id();
let mut excerpts = excerpts.iter();
if let Some((id, range)) = excerpts.next() {
update.inserted_excerpts.push(proto::ExcerptInsertion {
previous_excerpt_id: Some(predecessor.to_proto()),
excerpt: serialize_excerpt(buffer_id, id, range),
});
update.inserted_excerpts.extend(excerpts.map(|(id, range)| {
proto::ExcerptInsertion {
previous_excerpt_id: None,
excerpt: serialize_excerpt(buffer_id, id, range),
}
}))
}
true
}
Event::ExcerptsRemoved { ids } => {
update
.deleted_excerpts
.extend(ids.iter().map(ExcerptId::to_proto));
true
}
Event::ScrollPositionChanged { .. } => {
let scroll_anchor = self.scroll_manager.anchor();
update.scroll_top_anchor = Some(language::proto::serialize_anchor(
&scroll_anchor.top_anchor.text_anchor,
));
update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.top_anchor));
update.scroll_x = scroll_anchor.offset.x();
update.scroll_y = scroll_anchor.offset.y();
true
@@ -177,9 +279,13 @@ impl FollowableItem for Editor {
.selections
.disjoint_anchors()
.iter()
.chain(self.selections.pending_anchor().as_ref())
.map(serialize_selection)
.collect();
update.pending_selection = self
.selections
.pending_anchor()
.as_ref()
.map(serialize_selection);
true
}
_ => false,
@@ -189,45 +295,102 @@ impl FollowableItem for Editor {
fn apply_update_proto(
&mut self,
project: &ModelHandle<Project>,
message: update_view::Variant,
cx: &mut ViewContext<Self>,
) -> Result<()> {
match message {
update_view::Variant::Editor(message) => {
let buffer = self.buffer.read(cx);
let buffer = buffer.read(cx);
let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
let excerpt_id = excerpt_id.clone();
drop(buffer);
) -> Task<Result<()>> {
let update_view::Variant::Editor(message) = message;
let multibuffer = self.buffer.read(cx);
let multibuffer = multibuffer.read(cx);
let selections = message
.selections
.into_iter()
.filter_map(|selection| {
deserialize_selection(&excerpt_id, buffer_id, selection)
})
.collect::<Vec<_>>();
let buffer_ids = message
.inserted_excerpts
.iter()
.filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
.collect::<HashSet<_>>();
if !selections.is_empty() {
self.set_selections_from_remote(selections, cx);
self.request_autoscroll_remotely(Autoscroll::newest(), cx);
} else if let Some(anchor) = message.scroll_top_anchor {
self.set_scroll_anchor_remote(
ScrollAnchor {
top_anchor: Anchor {
buffer_id: Some(buffer_id),
excerpt_id,
text_anchor: language::proto::deserialize_anchor(anchor)
.ok_or_else(|| anyhow!("invalid scroll top"))?,
},
offset: vec2f(message.scroll_x, message.scroll_y),
},
cx,
);
let mut removals = message
.deleted_excerpts
.into_iter()
.map(ExcerptId::from_proto)
.collect::<Vec<_>>();
removals.sort_by(|a, b| a.cmp(&b, &multibuffer));
let selections = message
.selections
.into_iter()
.filter_map(|selection| deserialize_selection(&multibuffer, selection))
.collect::<Vec<_>>();
let pending_selection = message
.pending_selection
.and_then(|selection| deserialize_selection(&multibuffer, selection));
let scroll_top_anchor = message
.scroll_top_anchor
.and_then(|anchor| deserialize_anchor(&multibuffer, anchor));
drop(multibuffer);
let buffers = project.update(cx, |project, cx| {
buffer_ids
.into_iter()
.map(|id| project.open_buffer_by_id(id, cx))
.collect::<Vec<_>>()
});
let project = project.clone();
cx.spawn(|this, mut cx| async move {
let _buffers = try_join_all(buffers).await?;
this.update(&mut cx, |this, cx| {
this.buffer.update(cx, |multibuffer, cx| {
let mut insertions = message.inserted_excerpts.into_iter().peekable();
while let Some(insertion) = insertions.next() {
let Some(excerpt) = insertion.excerpt else { continue };
let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
let buffer_id = excerpt.buffer_id;
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
let adjacent_excerpts = iter::from_fn(|| {
let insertion = insertions.peek()?;
if insertion.previous_excerpt_id.is_none()
&& insertion.excerpt.as_ref()?.buffer_id == buffer_id
{
insertions.next()?.excerpt
} else {
None
}
});
multibuffer.insert_excerpts_with_ids_after(
ExcerptId::from_proto(previous_excerpt_id),
buffer,
[excerpt]
.into_iter()
.chain(adjacent_excerpts)
.filter_map(|excerpt| {
Some((
ExcerptId::from_proto(excerpt.id),
deserialize_excerpt_range(excerpt)?,
))
}),
cx,
);
}
multibuffer.remove_excerpts(removals, cx);
});
if !selections.is_empty() || pending_selection.is_some() {
this.set_selections_from_remote(selections, pending_selection, cx);
this.request_autoscroll_remotely(Autoscroll::newest(), cx);
} else if let Some(anchor) = scroll_top_anchor {
this.set_scroll_anchor_remote(ScrollAnchor {
top_anchor: anchor,
offset: vec2f(message.scroll_x, message.scroll_y)
}, cx);
}
}
}
Ok(())
});
Ok(())
})
}
fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool {
@@ -240,41 +403,82 @@ impl FollowableItem for Editor {
}
}
fn serialize_excerpt(
buffer_id: u64,
id: &ExcerptId,
range: &ExcerptRange<language::Anchor>,
) -> Option<proto::Excerpt> {
Some(proto::Excerpt {
id: id.to_proto(),
buffer_id,
context_start: Some(serialize_text_anchor(&range.context.start)),
context_end: Some(serialize_text_anchor(&range.context.end)),
primary_start: range
.primary
.as_ref()
.map(|r| serialize_text_anchor(&r.start)),
primary_end: range
.primary
.as_ref()
.map(|r| serialize_text_anchor(&r.end)),
})
}
fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
proto::Selection {
id: selection.id as u64,
start: Some(language::proto::serialize_anchor(
&selection.start.text_anchor,
)),
end: Some(language::proto::serialize_anchor(
&selection.end.text_anchor,
)),
start: Some(serialize_anchor(&selection.start)),
end: Some(serialize_anchor(&selection.end)),
reversed: selection.reversed,
}
}
fn serialize_anchor(anchor: &Anchor) -> proto::EditorAnchor {
proto::EditorAnchor {
excerpt_id: anchor.excerpt_id.to_proto(),
anchor: Some(serialize_text_anchor(&anchor.text_anchor)),
}
}
fn deserialize_excerpt_range(excerpt: proto::Excerpt) -> Option<ExcerptRange<language::Anchor>> {
let context = {
let start = language::proto::deserialize_anchor(excerpt.context_start?)?;
let end = language::proto::deserialize_anchor(excerpt.context_end?)?;
start..end
};
let primary = excerpt
.primary_start
.zip(excerpt.primary_end)
.and_then(|(start, end)| {
let start = language::proto::deserialize_anchor(start)?;
let end = language::proto::deserialize_anchor(end)?;
Some(start..end)
});
Some(ExcerptRange { context, primary })
}
fn deserialize_selection(
excerpt_id: &ExcerptId,
buffer_id: usize,
buffer: &MultiBufferSnapshot,
selection: proto::Selection,
) -> Option<Selection<Anchor>> {
Some(Selection {
id: selection.id as usize,
start: Anchor {
buffer_id: Some(buffer_id),
excerpt_id: excerpt_id.clone(),
text_anchor: language::proto::deserialize_anchor(selection.start?)?,
},
end: Anchor {
buffer_id: Some(buffer_id),
excerpt_id: excerpt_id.clone(),
text_anchor: language::proto::deserialize_anchor(selection.end?)?,
},
start: deserialize_anchor(buffer, selection.start?)?,
end: deserialize_anchor(buffer, selection.end?)?,
reversed: selection.reversed,
goal: SelectionGoal::None,
})
}
fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) -> Option<Anchor> {
let excerpt_id = ExcerptId::from_proto(anchor.excerpt_id);
Some(Anchor {
excerpt_id,
text_anchor: language::proto::deserialize_anchor(anchor.anchor?)?,
buffer_id: buffer.buffer_id_for_excerpt(excerpt_id),
})
}
impl Item for Editor {
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
if let Ok(data) = data.downcast::<NavigationData>() {
@@ -351,22 +555,10 @@ impl Item for Editor {
.boxed()
}
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
let buffer = self.buffer.read(cx).as_singleton()?;
let file = buffer.read(cx).file();
File::from_dyn(file).map(|file| ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path().clone(),
})
}
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
self.buffer
.read(cx)
.files(cx)
.into_iter()
.filter_map(|file| File::from_dyn(Some(file))?.project_entry_id(cx))
.collect()
.for_each_buffer(|buffer| f(buffer.id(), buffer.read(cx)));
}
fn is_singleton(&self, cx: &AppContext) -> bool {
@@ -403,7 +595,12 @@ impl Item for Editor {
}
fn can_save(&self, cx: &AppContext) -> bool {
!self.buffer().read(cx).is_singleton() || self.project_path(cx).is_some()
let buffer = &self.buffer().read(cx);
if let Some(buffer) = buffer.as_singleton() {
buffer.read(cx).project_path(cx).is_some()
} else {
true
}
}
fn save(
@@ -562,6 +759,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>,
@@ -633,7 +831,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
})
}))
})
})
@@ -892,7 +1094,7 @@ impl StatusItemView for CursorPosition {
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) {
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
self.update_position(editor, cx);
} else {
@@ -911,7 +1113,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>(
@@ -956,9 +1158,11 @@ fn path_for_file<'a>(
mod tests {
use super::*;
use gpui::MutableAppContext;
use language::RopeFingerprint;
use std::{
path::{Path, PathBuf},
sync::Arc,
time::SystemTime,
};
#[gpui::test]
@@ -988,7 +1192,7 @@ mod tests {
todo!()
}
fn mtime(&self) -> std::time::SystemTime {
fn mtime(&self) -> SystemTime {
todo!()
}
@@ -1007,7 +1211,7 @@ mod tests {
_: clock::Global,
_: project::LineEnding,
_: &mut MutableAppContext,
) -> gpui::Task<anyhow::Result<(clock::Global, String, std::time::SystemTime)>> {
) -> gpui::Task<anyhow::Result<(clock::Global, RopeFingerprint, SystemTime)>> {
todo!()
}

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

@@ -4,16 +4,16 @@ pub use anchor::{Anchor, AnchorRangeExt};
use anyhow::Result;
use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet};
use futures::{channel::mpsc, SinkExt};
use git::diff::DiffHunk;
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
pub use language::Completion;
use language::{
char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Outline,
DiagnosticEntry, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline,
OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _,
ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
};
use smallvec::SmallVec;
use std::{
borrow::Cow,
cell::{Ref, RefCell},
@@ -50,6 +50,26 @@ pub struct MultiBuffer {
title: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
ExcerptsAdded {
buffer: ModelHandle<Buffer>,
predecessor: ExcerptId,
excerpts: Vec<(ExcerptId, ExcerptRange<language::Anchor>)>,
},
ExcerptsRemoved {
ids: Vec<ExcerptId>,
},
Edited,
Reloaded,
Reparsed,
Saved,
FileHandleChanged,
Closed,
DirtyChanged,
DiagnosticsUpdated,
}
#[derive(Clone)]
struct History {
next_transaction_id: TransactionId,
@@ -744,6 +764,63 @@ impl MultiBuffer {
None
}
pub fn stream_excerpts_with_context_lines(
&mut self,
excerpts: Vec<(ModelHandle<Buffer>, Vec<Range<text::Anchor>>)>,
context_line_count: u32,
cx: &mut ModelContext<Self>,
) -> (Task<()>, mpsc::Receiver<Range<Anchor>>) {
let (mut tx, rx) = mpsc::channel(256);
let task = cx.spawn(|this, mut cx| async move {
for (buffer, ranges) in excerpts {
let buffer_id = buffer.id();
let buffer_snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot());
let mut excerpt_ranges = Vec::new();
let mut range_counts = Vec::new();
cx.background()
.scoped(|scope| {
scope.spawn(async {
let (ranges, counts) =
build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count);
excerpt_ranges = ranges;
range_counts = counts;
});
})
.await;
let mut ranges = ranges.into_iter();
let mut range_counts = range_counts.into_iter();
for excerpt_ranges in excerpt_ranges.chunks(100) {
let excerpt_ids = this.update(&mut cx, |this, cx| {
this.push_excerpts(buffer.clone(), excerpt_ranges.iter().cloned(), cx)
});
for (excerpt_id, range_count) in
excerpt_ids.into_iter().zip(range_counts.by_ref())
{
for range in ranges.by_ref().take(range_count) {
let start = Anchor {
buffer_id: Some(buffer_id),
excerpt_id: excerpt_id.clone(),
text_anchor: range.start,
};
let end = Anchor {
buffer_id: Some(buffer_id),
excerpt_id: excerpt_id.clone(),
text_anchor: range.end,
};
if tx.send(start..end).await.is_err() {
break;
}
}
}
}
}
});
(task, rx)
}
pub fn push_excerpts<O>(
&mut self,
buffer: ModelHandle<Buffer>,
@@ -768,39 +845,8 @@ impl MultiBuffer {
{
let buffer_id = buffer.id();
let buffer_snapshot = buffer.read(cx).snapshot();
let max_point = buffer_snapshot.max_point();
let mut range_counts = Vec::new();
let mut excerpt_ranges = Vec::new();
let mut range_iter = ranges
.iter()
.map(|range| {
range.start.to_point(&buffer_snapshot)..range.end.to_point(&buffer_snapshot)
})
.peekable();
while let Some(range) = range_iter.next() {
let excerpt_start = Point::new(range.start.row.saturating_sub(context_line_count), 0);
let mut excerpt_end =
Point::new(range.end.row + 1 + context_line_count, 0).min(max_point);
let mut ranges_in_excerpt = 1;
while let Some(next_range) = range_iter.peek() {
if next_range.start.row <= excerpt_end.row + context_line_count {
excerpt_end =
Point::new(next_range.end.row + 1 + context_line_count, 0).min(max_point);
ranges_in_excerpt += 1;
range_iter.next();
} else {
break;
}
}
excerpt_ranges.push(ExcerptRange {
context: excerpt_start..excerpt_end,
primary: Some(range),
});
range_counts.push(ranges_in_excerpt);
}
let (excerpt_ranges, range_counts) =
build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count);
let excerpt_ids = self.push_excerpts(buffer, excerpt_ranges, cx);
@@ -833,6 +879,30 @@ impl MultiBuffer {
) -> Vec<ExcerptId>
where
O: text::ToOffset,
{
let mut ids = Vec::new();
let mut next_excerpt_id = self.next_excerpt_id;
self.insert_excerpts_with_ids_after(
prev_excerpt_id,
buffer,
ranges.into_iter().map(|range| {
let id = ExcerptId(post_inc(&mut next_excerpt_id));
ids.push(id);
(id, range)
}),
cx,
);
ids
}
pub fn insert_excerpts_with_ids_after<O>(
&mut self,
prev_excerpt_id: ExcerptId,
buffer: ModelHandle<Buffer>,
ranges: impl IntoIterator<Item = (ExcerptId, ExcerptRange<O>)>,
cx: &mut ModelContext<Self>,
) where
O: text::ToOffset,
{
assert_eq!(self.history.transaction_depth, 0);
let mut ranges = ranges.into_iter().peekable();
@@ -858,7 +928,7 @@ impl MultiBuffer {
cx.observe(&buffer, |_, _, cx| cx.notify()),
cx.subscribe(&buffer, Self::on_buffer_event),
],
buffer,
buffer: buffer.clone(),
});
let mut snapshot = self.snapshot.borrow_mut();
@@ -883,8 +953,8 @@ impl MultiBuffer {
Locator::max()
};
let mut ids = Vec::new();
while let Some(range) = ranges.next() {
let mut excerpts = Vec::new();
while let Some((id, range)) = ranges.next() {
let locator = Locator::between(&prev_locator, &next_locator);
if let Err(ix) = buffer_state.excerpts.binary_search(&locator) {
buffer_state.excerpts.insert(ix, locator.clone());
@@ -897,7 +967,10 @@ impl MultiBuffer {
..buffer_snapshot.anchor_after(&primary.end)
}),
};
let id = ExcerptId(post_inc(&mut self.next_excerpt_id));
if id.0 >= self.next_excerpt_id {
self.next_excerpt_id = id.0 + 1;
}
excerpts.push((id, range.clone()));
let excerpt = Excerpt::new(
id,
locator.clone(),
@@ -909,7 +982,6 @@ impl MultiBuffer {
new_excerpts.push(excerpt, &());
prev_locator = locator.clone();
new_excerpt_ids.push(ExcerptIdMapping { id, locator }, &());
ids.push(id);
}
let edit_end = new_excerpts.summary().text.len;
@@ -929,12 +1001,17 @@ impl MultiBuffer {
new: edit_start..edit_end,
}]);
cx.emit(Event::Edited);
cx.emit(Event::ExcerptsAdded {
buffer,
predecessor: prev_excerpt_id,
excerpts,
});
cx.notify();
ids
}
pub fn clear(&mut self, cx: &mut ModelContext<Self>) {
self.sync(cx);
let ids = self.excerpt_ids();
self.buffers.borrow_mut().clear();
let mut snapshot = self.snapshot.borrow_mut();
let prev_len = snapshot.len();
@@ -948,6 +1025,7 @@ impl MultiBuffer {
new: 0..0,
}]);
cx.emit(Event::Edited);
cx.emit(Event::ExcerptsRemoved { ids });
cx.notify();
}
@@ -1071,12 +1149,14 @@ impl MultiBuffer {
cx: &mut ModelContext<Self>,
) {
self.sync(cx);
let ids = excerpt_ids.into_iter().collect::<Vec<_>>();
let mut buffers = self.buffers.borrow_mut();
let mut snapshot = self.snapshot.borrow_mut();
let mut new_excerpts = SumTree::new();
let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>();
let mut edits = Vec::new();
let mut excerpt_ids = excerpt_ids.into_iter().peekable();
let mut excerpt_ids = ids.iter().copied().peekable();
while let Some(excerpt_id) = excerpt_ids.next() {
// Seek to the next excerpt to remove, preserving any preceding excerpts.
@@ -1143,6 +1223,7 @@ impl MultiBuffer {
self.subscriptions.publish_mut(edits);
cx.emit(Event::Edited);
cx.emit(Event::ExcerptsRemoved { ids });
cx.notify();
}
@@ -1165,10 +1246,22 @@ impl MultiBuffer {
fn on_buffer_event(
&mut self,
_: ModelHandle<Buffer>,
event: &Event,
event: &language::Event,
cx: &mut ModelContext<Self>,
) {
cx.emit(event.clone());
cx.emit(match event {
language::Event::Edited => Event::Edited,
language::Event::DirtyChanged => Event::DirtyChanged,
language::Event::Saved => Event::Saved,
language::Event::FileHandleChanged => Event::FileHandleChanged,
language::Event::Reloaded => Event::Reloaded,
language::Event::Reparsed => Event::Reparsed,
language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
language::Event::Closed => Event::Closed,
//
language::Event::Operation(_) => return,
});
}
pub fn all_buffers(&self) -> HashSet<ModelHandle<Buffer>> {
@@ -1244,12 +1337,11 @@ 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]> {
let buffers = self.buffers.borrow();
buffers
pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle<Buffer>)) {
self.buffers
.borrow()
.values()
.filter_map(|buffer| buffer.buffer.read(cx).file())
.collect()
.for_each(|state| f(&state.buffer))
}
pub fn title<'a>(&'a self, cx: &'a AppContext) -> Cow<'a, str> {
@@ -1604,7 +1696,7 @@ impl MultiBuffer {
}
impl Entity for MultiBuffer {
type Event = language::Event;
type Event = Event;
}
impl MultiBufferSnapshot {
@@ -2450,6 +2542,14 @@ impl MultiBufferSnapshot {
}
}
pub fn excerpts(
&self,
) -> impl Iterator<Item = (ExcerptId, &BufferSnapshot, ExcerptRange<text::Anchor>)> {
self.excerpts
.iter()
.map(|excerpt| (excerpt.id, &excerpt.buffer, excerpt.range.clone()))
}
pub fn excerpt_boundaries_in_range<R, T>(
&self,
range: R,
@@ -2591,6 +2691,11 @@ impl MultiBufferSnapshot {
.and_then(|(buffer, offset)| buffer.language_at(offset))
}
pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option<LanguageScope> {
self.point_to_buffer_offset(point)
.and_then(|(buffer, offset)| buffer.language_scope_at(offset))
}
pub fn is_dirty(&self) -> bool {
self.is_dirty
}
@@ -2635,11 +2740,73 @@ impl MultiBufferSnapshot {
row_range: Range<u32>,
reversed: bool,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
self.as_singleton()
.into_iter()
.flat_map(move |(_, _, buffer)| {
buffer.git_diff_hunks_in_range(row_range.clone(), reversed)
})
let mut cursor = self.excerpts.cursor::<Point>();
if reversed {
cursor.seek(&Point::new(row_range.end, 0), Bias::Left, &());
if cursor.item().is_none() {
cursor.prev(&());
}
} else {
cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &());
}
std::iter::from_fn(move || {
let excerpt = cursor.item()?;
let multibuffer_start = *cursor.start();
let multibuffer_end = multibuffer_start + excerpt.text_summary.lines;
if multibuffer_start.row >= row_range.end {
return None;
}
let mut buffer_start = excerpt.range.context.start;
let mut buffer_end = excerpt.range.context.end;
let excerpt_start_point = buffer_start.to_point(&excerpt.buffer);
let excerpt_end_point = excerpt_start_point + excerpt.text_summary.lines;
if row_range.start > multibuffer_start.row {
let buffer_start_point =
excerpt_start_point + Point::new(row_range.start - multibuffer_start.row, 0);
buffer_start = excerpt.buffer.anchor_before(buffer_start_point);
}
if row_range.end < multibuffer_end.row {
let buffer_end_point =
excerpt_start_point + Point::new(row_range.end - multibuffer_start.row, 0);
buffer_end = excerpt.buffer.anchor_before(buffer_end_point);
}
let buffer_hunks = excerpt
.buffer
.git_diff_hunks_intersecting_range(buffer_start..buffer_end, reversed)
.filter_map(move |hunk| {
let start = multibuffer_start.row
+ hunk
.buffer_range
.start
.saturating_sub(excerpt_start_point.row);
let end = multibuffer_start.row
+ hunk
.buffer_range
.end
.min(excerpt_end_point.row + 1)
.saturating_sub(excerpt_start_point.row);
Some(DiffHunk {
buffer_range: start..end,
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
})
});
if reversed {
cursor.prev(&());
} else {
cursor.next(&());
}
Some(buffer_hunks)
})
.flatten()
}
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
@@ -2746,6 +2913,10 @@ impl MultiBufferSnapshot {
}
}
pub fn buffer_id_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<usize> {
Some(self.excerpt(excerpt_id)?.buffer_id)
}
fn excerpt<'a>(&'a self, excerpt_id: ExcerptId) -> Option<&'a Excerpt> {
let mut cursor = self.excerpts.cursor::<Option<&Locator>>();
let locator = self.excerpt_locator_for_id(excerpt_id);
@@ -3080,6 +3251,14 @@ impl ExcerptId {
Self(usize::MAX)
}
pub fn to_proto(&self) -> u64 {
self.0 as _
}
pub fn from_proto(proto: u64) -> Self {
Self(proto as _)
}
pub fn cmp(&self, other: &Self, snapshot: &MultiBufferSnapshot) -> cmp::Ordering {
let a = snapshot.excerpt_locator_for_id(*self);
let b = snapshot.excerpt_locator_for_id(*other);
@@ -3456,19 +3635,62 @@ impl ToPointUtf16 for PointUtf16 {
}
}
fn build_excerpt_ranges<T>(
buffer: &BufferSnapshot,
ranges: &[Range<T>],
context_line_count: u32,
) -> (Vec<ExcerptRange<Point>>, Vec<usize>)
where
T: text::ToPoint,
{
let max_point = buffer.max_point();
let mut range_counts = Vec::new();
let mut excerpt_ranges = Vec::new();
let mut range_iter = ranges
.iter()
.map(|range| range.start.to_point(buffer)..range.end.to_point(buffer))
.peekable();
while let Some(range) = range_iter.next() {
let excerpt_start = Point::new(range.start.row.saturating_sub(context_line_count), 0);
let mut excerpt_end = Point::new(range.end.row + 1 + context_line_count, 0).min(max_point);
let mut ranges_in_excerpt = 1;
while let Some(next_range) = range_iter.peek() {
if next_range.start.row <= excerpt_end.row + context_line_count {
excerpt_end =
Point::new(next_range.end.row + 1 + context_line_count, 0).min(max_point);
ranges_in_excerpt += 1;
range_iter.next();
} else {
break;
}
}
excerpt_ranges.push(ExcerptRange {
context: excerpt_start..excerpt_end,
primary: Some(range),
});
range_counts.push(ranges_in_excerpt);
}
(excerpt_ranges, range_counts)
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::MutableAppContext;
use futures::StreamExt;
use gpui::{MutableAppContext, TestAppContext};
use language::{Buffer, Rope};
use rand::prelude::*;
use settings::Settings;
use std::{env, rc::Rc};
use unindent::Unindent;
use util::test::sample_text;
#[gpui::test]
fn test_singleton_multibuffer(cx: &mut MutableAppContext) {
fn test_singleton(cx: &mut MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx));
let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx));
@@ -3495,13 +3717,13 @@ mod tests {
}
#[gpui::test]
fn test_remote_multibuffer(cx: &mut MutableAppContext) {
fn test_remote(cx: &mut MutableAppContext) {
let host_buffer = cx.add_model(|cx| Buffer::new(0, "a", cx));
let guest_buffer = cx.add_model(|cx| {
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(
@@ -3526,7 +3748,7 @@ mod tests {
}
#[gpui::test]
fn test_excerpt_buffer(cx: &mut MutableAppContext) {
fn test_excerpt_boundaries_and_clipping(cx: &mut MutableAppContext) {
let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx));
let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'g'), cx));
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
@@ -3535,7 +3757,9 @@ mod tests {
multibuffer.update(cx, |_, cx| {
let events = events.clone();
cx.subscribe(&multibuffer, move |_, _, event, _| {
events.borrow_mut().push(event.clone())
if let Event::Edited = event {
events.borrow_mut().push(event.clone())
}
})
.detach();
});
@@ -3748,7 +3972,84 @@ mod tests {
}
#[gpui::test]
fn test_excerpts_with_context_lines(cx: &mut MutableAppContext) {
fn test_excerpt_events(cx: &mut MutableAppContext) {
let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'a'), cx));
let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'm'), cx));
let leader_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
let follower_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
follower_multibuffer.update(cx, |_, cx| {
cx.subscribe(&leader_multibuffer, |follower, _, event, cx| {
match event.clone() {
Event::ExcerptsAdded {
buffer,
predecessor,
excerpts,
} => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx),
Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx),
_ => {}
}
})
.detach();
});
leader_multibuffer.update(cx, |leader, cx| {
leader.push_excerpts(
buffer_1.clone(),
[
ExcerptRange {
context: 0..8,
primary: None,
},
ExcerptRange {
context: 12..16,
primary: None,
},
],
cx,
);
leader.insert_excerpts_after(
leader.excerpt_ids()[0],
buffer_2.clone(),
[
ExcerptRange {
context: 0..5,
primary: None,
},
ExcerptRange {
context: 10..15,
primary: None,
},
],
cx,
)
});
assert_eq!(
leader_multibuffer.read(cx).snapshot(cx).text(),
follower_multibuffer.read(cx).snapshot(cx).text(),
);
leader_multibuffer.update(cx, |leader, cx| {
let excerpt_ids = leader.excerpt_ids();
leader.remove_excerpts([excerpt_ids[1], excerpt_ids[3]], cx);
});
assert_eq!(
leader_multibuffer.read(cx).snapshot(cx).text(),
follower_multibuffer.read(cx).snapshot(cx).text(),
);
leader_multibuffer.update(cx, |leader, cx| {
leader.clear(cx);
});
assert_eq!(
leader_multibuffer.read(cx).snapshot(cx).text(),
follower_multibuffer.read(cx).snapshot(cx).text(),
);
}
#[gpui::test]
fn test_push_excerpts_with_context_lines(cx: &mut MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx));
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| {
@@ -3784,7 +4085,45 @@ mod tests {
}
#[gpui::test]
fn test_empty_excerpt_buffer(cx: &mut MutableAppContext) {
async fn test_stream_excerpts_with_context_lines(cx: &mut TestAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx));
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
let (task, anchor_ranges) = multibuffer.update(cx, |multibuffer, cx| {
let snapshot = buffer.read(cx);
let ranges = vec![
snapshot.anchor_before(Point::new(3, 2))..snapshot.anchor_before(Point::new(4, 2)),
snapshot.anchor_before(Point::new(7, 1))..snapshot.anchor_before(Point::new(7, 3)),
snapshot.anchor_before(Point::new(15, 0))
..snapshot.anchor_before(Point::new(15, 0)),
];
multibuffer.stream_excerpts_with_context_lines(vec![(buffer.clone(), ranges)], 2, cx)
});
let anchor_ranges = anchor_ranges.collect::<Vec<_>>().await;
// Ensure task is finished when stream completes.
task.await;
let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
assert_eq!(
snapshot.text(),
"bbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\n\nnnn\nooo\nppp\nqqq\nrrr\n"
);
assert_eq!(
anchor_ranges
.iter()
.map(|range| range.to_point(&snapshot))
.collect::<Vec<_>>(),
vec![
Point::new(2, 2)..Point::new(3, 2),
Point::new(6, 1)..Point::new(6, 3),
Point::new(12, 0)..Point::new(12, 0)
]
);
}
#[gpui::test]
fn test_empty_multibuffer(cx: &mut MutableAppContext) {
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
let snapshot = multibuffer.read(cx).snapshot(cx);
@@ -3872,9 +4211,7 @@ mod tests {
}
#[gpui::test]
fn test_multibuffer_resolving_anchors_after_replacing_their_excerpts(
cx: &mut MutableAppContext,
) {
fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut MutableAppContext) {
let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx));
let buffer_2 = cx.add_model(|cx| Buffer::new(0, "ABCDEFGHIJKLMNOP", cx));
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
@@ -4004,6 +4341,178 @@ mod tests {
);
}
#[gpui::test]
async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
use git::diff::DiffHunkStatus;
// buffer has two modified hunks with two rows each
let buffer_1 = cx.add_model(|cx| {
let mut buffer = Buffer::new(
0,
"
1.zero
1.ONE
1.TWO
1.three
1.FOUR
1.FIVE
1.six
"
.unindent(),
cx,
);
buffer.set_diff_base(
Some(
"
1.zero
1.one
1.two
1.three
1.four
1.five
1.six
"
.unindent(),
),
cx,
);
buffer
});
// buffer has a deletion hunk and an insertion hunk
let buffer_2 = cx.add_model(|cx| {
let mut buffer = Buffer::new(
0,
"
2.zero
2.one
2.two
2.three
2.four
2.five
2.six
"
.unindent(),
cx,
);
buffer.set_diff_base(
Some(
"
2.zero
2.one
2.one-and-a-half
2.two
2.three
2.four
2.six
"
.unindent(),
),
cx,
);
buffer
});
cx.foreground().run_until_parked();
let multibuffer = cx.add_model(|cx| {
let mut multibuffer = MultiBuffer::new(0);
multibuffer.push_excerpts(
buffer_1.clone(),
[
// excerpt ends in the middle of a modified hunk
ExcerptRange {
context: Point::new(0, 0)..Point::new(1, 5),
primary: Default::default(),
},
// excerpt begins in the middle of a modified hunk
ExcerptRange {
context: Point::new(5, 0)..Point::new(6, 5),
primary: Default::default(),
},
],
cx,
);
multibuffer.push_excerpts(
buffer_2.clone(),
[
// excerpt ends at a deletion
ExcerptRange {
context: Point::new(0, 0)..Point::new(1, 5),
primary: Default::default(),
},
// excerpt starts at a deletion
ExcerptRange {
context: Point::new(2, 0)..Point::new(2, 5),
primary: Default::default(),
},
// excerpt fully contains a deletion hunk
ExcerptRange {
context: Point::new(1, 0)..Point::new(2, 5),
primary: Default::default(),
},
// excerpt fully contains an insertion hunk
ExcerptRange {
context: Point::new(4, 0)..Point::new(6, 5),
primary: Default::default(),
},
],
cx,
);
multibuffer
});
let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
assert_eq!(
snapshot.text(),
"
1.zero
1.ONE
1.FIVE
1.six
2.zero
2.one
2.two
2.one
2.two
2.four
2.five
2.six"
.unindent()
);
let expected = [
(DiffHunkStatus::Modified, 1..2),
(DiffHunkStatus::Modified, 2..3),
//TODO: Define better when and where removed hunks show up at range extremities
(DiffHunkStatus::Removed, 6..6),
(DiffHunkStatus::Removed, 8..8),
(DiffHunkStatus::Added, 10..11),
];
assert_eq!(
snapshot
.git_diff_hunks_in_range(0..12, false)
.map(|hunk| (hunk.status(), hunk.buffer_range))
.collect::<Vec<_>>(),
&expected,
);
assert_eq!(
snapshot
.git_diff_hunks_in_range(0..12, true)
.map(|hunk| (hunk.status(), hunk.buffer_range))
.collect::<Vec<_>>(),
expected
.iter()
.rev()
.cloned()
.collect::<Vec<_>>()
.as_slice(),
);
}
#[gpui::test(iterations = 100)]
fn test_random_multibuffer(cx: &mut MutableAppContext, mut rng: StdRng) {
let operations = env::var("OPERATIONS")

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 schema 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,
hover_popover::{hide_hover, HideHover},
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();
}
@@ -273,9 +307,14 @@ 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);
hide_hover(self, &HideHover, 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 {
@@ -284,8 +323,13 @@ 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);
hide_hover(self, &HideHover, 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(
@@ -293,8 +337,13 @@ impl Editor {
scroll_anchor: ScrollAnchor,
cx: &mut ViewContext<Self>,
) {
hide_hover(self, cx);
self.scroll_manager.set_anchor(scroll_anchor, false, cx);
hide_hover(self, &HideHover, 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::{
@@ -254,7 +256,7 @@ impl<'a> EditorTestContext<'a> {
Actual selections:
{}
"},
"},
self.assertion_context(),
expected_marked_text,
actual_marked_text,

View File

@@ -0,0 +1,34 @@
[package]
name = "feedback"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/feedback.rs"
[features]
test-support = []
[dependencies]
anyhow = "1.0.38"
client = { path = "../client" }
editor = { path = "../editor" }
language = { path = "../language" }
log = "0.4"
futures = "0.3"
gpui = { path = "../gpui" }
human_bytes = "0.4.1"
isahc = "1.7"
lazy_static = "1.4.0"
postage = { version = "0.4", features = ["futures-traits"] }
project = { path = "../project" }
search = { path = "../search" }
serde = { version = "1.0", features = ["derive", "rc"] }
settings = { path = "../settings" }
sysinfo = "0.27.1"
theme = { path = "../theme" }
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
urlencoding = "2.1.2"
util = { path = "../util" }
workspace = { path = "../workspace" }

View File

@@ -0,0 +1,65 @@
use std::sync::Arc;
pub mod feedback_editor;
mod system_specs;
use gpui::{actions, impl_actions, ClipboardItem, MutableAppContext, PromptLevel, ViewContext};
use serde::Deserialize;
use system_specs::SystemSpecs;
use workspace::{AppState, Workspace};
#[derive(Deserialize, Clone, PartialEq)]
pub struct OpenBrowser {
pub url: Arc<str>,
}
impl_actions!(zed, [OpenBrowser]);
actions!(
zed,
[CopySystemSpecsIntoClipboard, FileBugReport, RequestFeature]
);
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
let system_specs = SystemSpecs::new(&cx);
let system_specs_text = system_specs.to_string();
feedback_editor::init(system_specs, app_state, cx);
cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
let url = format!(
"https://github.com/zed-industries/feedback/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
urlencoding::encode(&system_specs_text)
);
cx.add_action(
move |_: &mut Workspace,
_: &CopySystemSpecsIntoClipboard,
cx: &mut ViewContext<Workspace>| {
cx.prompt(
PromptLevel::Info,
&format!("Copied into clipboard:\n\n{system_specs_text}"),
&["OK"],
);
let item = ClipboardItem::new(system_specs_text.clone());
cx.write_to_clipboard(item);
},
);
cx.add_action(
|_: &mut Workspace, _: &RequestFeature, cx: &mut ViewContext<Workspace>| {
let url = "https://github.com/zed-industries/feedback/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml";
cx.dispatch_action(OpenBrowser {
url: url.into(),
});
},
);
cx.add_action(
move |_: &mut Workspace, _: &FileBugReport, cx: &mut ViewContext<Workspace>| {
cx.dispatch_action(OpenBrowser {
url: url.clone().into(),
});
},
);
}

View File

@@ -0,0 +1,436 @@
use std::{
any::TypeId,
ops::{Range, RangeInclusive},
sync::Arc,
};
use anyhow::bail;
use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use editor::{Anchor, Editor};
use futures::AsyncReadExt;
use gpui::{
actions,
elements::{ChildView, Flex, Label, MouseEventHandler, ParentElement, Stack, Text},
serde_json, AnyViewHandle, AppContext, CursorStyle, Element, ElementBox, Entity, ModelHandle,
MouseButton, MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext,
ViewHandle, WeakViewHandle,
};
use isahc::Request;
use language::Buffer;
use postage::prelude::Stream;
use project::Project;
use serde::Serialize;
use settings::Settings;
use workspace::{
item::{Item, ItemHandle},
searchable::{SearchableItem, SearchableItemHandle},
AppState, StatusItemView, Workspace,
};
use crate::system_specs::SystemSpecs;
const FEEDBACK_CHAR_LIMIT: RangeInclusive<usize> = 10..=5000;
const FEEDBACK_PLACEHOLDER_TEXT: &str = "Thanks for spending time with Zed. Enter your feedback here as Markdown. Save the tab to submit your feedback.";
const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
"Feedback failed to submit, see error log for details.";
actions!(feedback, [SubmitFeedback, GiveFeedback, DeployFeedback]);
pub fn init(system_specs: SystemSpecs, app_state: Arc<AppState>, cx: &mut MutableAppContext) {
cx.add_action({
move |workspace: &mut Workspace, _: &GiveFeedback, cx: &mut ViewContext<Workspace>| {
FeedbackEditor::deploy(system_specs.clone(), workspace, app_state.clone(), cx);
}
});
}
pub struct FeedbackButton;
impl Entity for FeedbackButton {
type Event = ();
}
impl View for FeedbackButton {
fn ui_name() -> &'static str {
"FeedbackButton"
}
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
Stack::new()
.with_child(
MouseEventHandler::<Self>::new(0, cx, |state, cx| {
let theme = &cx.global::<Settings>().theme;
let theme = &theme.workspace.status_bar.feedback;
Text::new(
"Give Feedback".to_string(),
theme.style_for(state, true).clone(),
)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(GiveFeedback))
.boxed(),
)
.boxed()
}
}
impl StatusItemView for FeedbackButton {
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
}
#[derive(Serialize)]
struct FeedbackRequestBody<'a> {
feedback_text: &'a str,
metrics_id: Option<Arc<str>>,
system_specs: SystemSpecs,
token: &'a str,
}
#[derive(Clone)]
struct FeedbackEditor {
system_specs: SystemSpecs,
editor: ViewHandle<Editor>,
project: ModelHandle<Project>,
}
impl FeedbackEditor {
fn new(
system_specs: SystemSpecs,
project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
cx: &mut ViewContext<Self>,
) -> Self {
let editor = cx.add_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
editor.set_vertical_scroll_margin(5, cx);
editor.set_placeholder_text(FEEDBACK_PLACEHOLDER_TEXT, cx);
editor
});
cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()))
.detach();
Self {
system_specs: system_specs.clone(),
editor,
project,
}
}
fn handle_save(
&mut self,
_: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
let feedback_char_count = self.editor.read(cx).text(cx).chars().count();
let error = if feedback_char_count < *FEEDBACK_CHAR_LIMIT.start() {
Some(format!(
"Feedback can't be shorter than {} characters.",
FEEDBACK_CHAR_LIMIT.start()
))
} else if feedback_char_count > *FEEDBACK_CHAR_LIMIT.end() {
Some(format!(
"Feedback can't be longer than {} characters.",
FEEDBACK_CHAR_LIMIT.end()
))
} else {
None
};
if let Some(error) = error {
cx.prompt(PromptLevel::Critical, &error, &["OK"]);
return Task::ready(Ok(()));
}
let mut answer = cx.prompt(
PromptLevel::Info,
"Ready to submit your feedback?",
&["Yes, Submit!", "No"],
);
let this = cx.handle();
let client = cx.global::<Arc<Client>>().clone();
let feedback_text = self.editor.read(cx).text(cx);
let specs = self.system_specs.clone();
cx.spawn(|_, mut cx| async move {
let answer = answer.recv().await;
if answer == Some(0) {
match FeedbackEditor::submit_feedback(&feedback_text, client, specs).await {
Ok(_) => {
cx.update(|cx| {
this.update(cx, |_, cx| {
cx.dispatch_action(workspace::CloseActiveItem);
})
});
}
Err(error) => {
log::error!("{}", error);
cx.update(|cx| {
this.update(cx, |_, cx| {
cx.prompt(
PromptLevel::Critical,
FEEDBACK_SUBMISSION_ERROR_TEXT,
&["OK"],
);
})
});
}
}
}
})
.detach();
Task::ready(Ok(()))
}
async fn submit_feedback(
feedback_text: &str,
zed_client: Arc<Client>,
system_specs: SystemSpecs,
) -> anyhow::Result<()> {
let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL);
let metrics_id = zed_client.metrics_id();
let http_client = zed_client.http_client();
let request = FeedbackRequestBody {
feedback_text: &feedback_text,
metrics_id,
system_specs,
token: ZED_SECRET_CLIENT_TOKEN,
};
let json_bytes = serde_json::to_vec(&request)?;
let request = Request::post(feedback_endpoint)
.header("content-type", "application/json")
.body(json_bytes.into())?;
let mut response = http_client.send(request).await?;
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
let response_status = response.status();
if !response_status.is_success() {
bail!("Feedback API failed with error: {}", response_status)
}
Ok(())
}
}
impl FeedbackEditor {
pub fn deploy(
system_specs: SystemSpecs,
workspace: &mut Workspace,
app_state: Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) {
workspace
.with_local_workspace(&app_state, cx, |workspace, cx| {
let project = workspace.project().clone();
let markdown_language = project.read(cx).languages().language_for_name("Markdown");
let buffer = project
.update(cx, |project, cx| {
project.create_buffer("", markdown_language, cx)
})
.expect("creating buffers on a local workspace always succeeds");
let feedback_editor =
cx.add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx));
workspace.add_item(Box::new(feedback_editor), cx);
})
.detach();
}
}
impl View for FeedbackEditor {
fn ui_name() -> &'static str {
"FeedbackEditor"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
ChildView::new(&self.editor, cx).boxed()
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.editor);
}
}
}
impl Entity for FeedbackEditor {
type Event = editor::Event;
}
impl Item for FeedbackEditor {
fn tab_content(&self, _: Option<usize>, style: &theme::Tab, _: &AppContext) -> ElementBox {
Flex::row()
.with_child(
Label::new("Feedback".to_string(), style.label.clone())
.aligned()
.contained()
.boxed(),
)
.boxed()
}
fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
self.editor.for_each_project_item(cx, f)
}
fn to_item_events(_: &Self::Event) -> Vec<workspace::item::ItemEvent> {
Vec::new()
}
fn is_singleton(&self, _: &AppContext) -> bool {
true
}
fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
fn can_save(&self, _: &AppContext) -> bool {
true
}
fn save(
&mut self,
project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
self.handle_save(project, cx)
}
fn save_as(
&mut self,
project: ModelHandle<Project>,
_: std::path::PathBuf,
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
self.handle_save(project, cx)
}
fn reload(
&mut self,
_: ModelHandle<Project>,
_: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
unreachable!("reload should not have been called")
}
fn clone_on_split(
&self,
_workspace_id: workspace::WorkspaceId,
cx: &mut ViewContext<Self>,
) -> Option<Self>
where
Self: Sized,
{
let buffer = self
.editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.expect("Feedback buffer is only ever singleton");
Some(Self::new(
self.system_specs.clone(),
self.project.clone(),
buffer.clone(),
cx,
))
}
fn serialized_item_kind() -> Option<&'static str> {
None
}
fn deserialize(
_: ModelHandle<Project>,
_: WeakViewHandle<Workspace>,
_: workspace::WorkspaceId,
_: workspace::ItemId,
_: &mut ViewContext<workspace::Pane>,
) -> Task<anyhow::Result<ViewHandle<Self>>> {
unreachable!()
}
fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(handle.clone()))
}
fn act_as_type(
&self,
type_id: TypeId,
self_handle: &ViewHandle<Self>,
_: &AppContext,
) -> Option<AnyViewHandle> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.into())
} else if type_id == TypeId::of::<Editor>() {
Some((&self.editor).into())
} else {
None
}
}
}
impl SearchableItem for FeedbackEditor {
type Match = Range<Anchor>;
fn to_search_event(event: &Self::Event) -> Option<workspace::searchable::SearchEvent> {
Editor::to_search_event(event)
}
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
self.editor
.update(cx, |editor, cx| editor.clear_matches(cx))
}
fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
self.editor
.update(cx, |editor, cx| editor.update_matches(matches, cx))
}
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
self.editor
.update(cx, |editor, cx| editor.query_suggestion(cx))
}
fn activate_match(
&mut self,
index: usize,
matches: Vec<Self::Match>,
cx: &mut ViewContext<Self>,
) {
self.editor
.update(cx, |editor, cx| editor.activate_match(index, matches, cx))
}
fn find_matches(
&mut self,
query: project::search::SearchQuery,
cx: &mut ViewContext<Self>,
) -> Task<Vec<Self::Match>> {
self.editor
.update(cx, |editor, cx| editor.find_matches(query, cx))
}
fn active_match_index(
&mut self,
matches: Vec<Self::Match>,
cx: &mut ViewContext<Self>,
) -> Option<usize> {
self.editor
.update(cx, |editor, cx| editor.active_match_index(matches, cx))
}
}

View File

@@ -0,0 +1,75 @@
use client::ZED_APP_VERSION;
use gpui::{AppContext, AppVersion};
use human_bytes::human_bytes;
use serde::Serialize;
use std::{env, fmt::Display};
use sysinfo::{System, SystemExt};
use util::channel::ReleaseChannel;
#[derive(Clone, Debug, Serialize)]
pub struct SystemSpecs {
#[serde(serialize_with = "serialize_app_version")]
app_version: Option<AppVersion>,
release_channel: &'static str,
os_name: &'static str,
os_version: Option<String>,
memory: u64,
architecture: &'static str,
}
impl SystemSpecs {
pub fn new(cx: &AppContext) -> Self {
let platform = cx.platform();
let app_version = ZED_APP_VERSION.or_else(|| platform.app_version().ok());
let release_channel = cx.global::<ReleaseChannel>().dev_name();
let os_name = platform.os_name();
let system = System::new_all();
let memory = system.total_memory();
let architecture = env::consts::ARCH;
let os_version = platform
.os_version()
.ok()
.map(|os_version| os_version.to_string());
SystemSpecs {
app_version,
release_channel,
os_name,
os_version,
memory,
architecture,
}
}
}
impl Display for SystemSpecs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let os_information = match &self.os_version {
Some(os_version) => format!("OS: {} {}", self.os_name, os_version),
None => format!("OS: {}", self.os_name),
};
let app_version_information = self
.app_version
.as_ref()
.map(|app_version| format!("Zed: v{} ({})", app_version, self.release_channel));
let system_specs = [
app_version_information,
Some(os_information),
Some(format!("Memory: {}", human_bytes(self.memory as f64))),
Some(format!("Architecture: {}", self.architecture)),
]
.into_iter()
.flatten()
.collect::<Vec<String>>()
.join("\n");
write!(f, "{system_specs}")
}
}
fn serialize_app_version<S>(version: &Option<AppVersion>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
version.map(|v| v.to_string()).serialize(serializer)
}

View File

@@ -2,6 +2,7 @@
name = "file_finder"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/file_finder.rs"

View File

@@ -62,11 +62,12 @@ impl View for FileFinder {
impl FileFinder {
fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) {
let path_string = path_match.path.to_string_lossy();
let path = &path_match.path;
let path_string = path.to_string_lossy();
let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
let path_positions = path_match.positions.clone();
let file_name = path_match.path.file_name().map_or_else(
let file_name = path.file_name().map_or_else(
|| path_match.path_prefix.to_string(),
|file_name| file_name.to_string_lossy().to_string(),
);
@@ -161,7 +162,7 @@ impl FileFinder {
self.cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag = self.cancel_flag.clone();
cx.spawn(|this, mut cx| async move {
let matches = fuzzy::match_paths(
let matches = fuzzy::match_path_sets(
candidate_sets.as_slice(),
&query,
false,

View File

@@ -2,6 +2,7 @@
name = "fs"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/fs.rs"

View File

@@ -13,7 +13,6 @@ use smol::io::{AsyncReadExt, AsyncWriteExt};
use std::borrow::Cow;
use std::cmp;
use std::io::Write;
use std::ops::Deref;
use std::sync::Arc;
use std::{
io,
@@ -35,7 +34,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 +79,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
@@ -94,16 +93,6 @@ impl LineEnding {
}
}
pub struct HomeDir(pub PathBuf);
impl Deref for HomeDir {
type Target = PathBuf;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[async_trait::async_trait]
pub trait Fs: Send + Sync {
async fn create_dir(&self, path: &Path) -> Result<()>;
@@ -389,6 +378,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 +507,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 +522,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 +624,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 +734,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 +743,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 +761,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 +828,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 +846,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

@@ -3,6 +3,7 @@ name = "fsevent"
version = "2.0.2"
license = "MIT"
edition = "2021"
publish = false
[lib]
path = "src/fsevent.rs"

View File

@@ -2,6 +2,7 @@
name = "fuzzy"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/fuzzy.rs"

View File

@@ -1,794 +1,8 @@
mod char_bag;
use gpui::executor;
use std::{
borrow::Cow,
cmp::{self, Ordering},
path::Path,
sync::atomic::{self, AtomicBool},
sync::Arc,
};
mod matcher;
mod paths;
mod strings;
pub use char_bag::CharBag;
const BASE_DISTANCE_PENALTY: f64 = 0.6;
const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05;
const MIN_DISTANCE_PENALTY: f64 = 0.2;
pub struct Matcher<'a> {
query: &'a [char],
lowercase_query: &'a [char],
query_char_bag: CharBag,
smart_case: bool,
max_results: usize,
min_score: f64,
match_positions: Vec<usize>,
last_positions: Vec<usize>,
score_matrix: Vec<Option<f64>>,
best_position_matrix: Vec<usize>,
}
trait Match: Ord {
fn score(&self) -> f64;
fn set_positions(&mut self, positions: Vec<usize>);
}
trait MatchCandidate {
fn has_chars(&self, bag: CharBag) -> bool;
fn to_string(&self) -> Cow<'_, str>;
}
#[derive(Clone, Debug)]
pub struct PathMatchCandidate<'a> {
pub path: &'a Arc<Path>,
pub char_bag: CharBag,
}
#[derive(Clone, Debug)]
pub struct PathMatch {
pub score: f64,
pub positions: Vec<usize>,
pub worktree_id: usize,
pub path: Arc<Path>,
pub path_prefix: Arc<str>,
}
#[derive(Clone, Debug)]
pub struct StringMatchCandidate {
pub id: usize,
pub string: String,
pub char_bag: CharBag,
}
pub trait PathMatchCandidateSet<'a>: Send + Sync {
type Candidates: Iterator<Item = PathMatchCandidate<'a>>;
fn id(&self) -> usize;
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn prefix(&self) -> Arc<str>;
fn candidates(&'a self, start: usize) -> Self::Candidates;
}
impl Match for PathMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl Match for StringMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl<'a> MatchCandidate for PathMatchCandidate<'a> {
fn has_chars(&self, bag: CharBag) -> bool {
self.char_bag.is_superset(bag)
}
fn to_string(&self) -> Cow<'a, str> {
self.path.to_string_lossy()
}
}
impl StringMatchCandidate {
pub fn new(id: usize, string: String) -> Self {
Self {
id,
char_bag: CharBag::from(string.as_str()),
string,
}
}
}
impl<'a> MatchCandidate for &'a StringMatchCandidate {
fn has_chars(&self, bag: CharBag) -> bool {
self.char_bag.is_superset(bag)
}
fn to_string(&self) -> Cow<'a, str> {
self.string.as_str().into()
}
}
#[derive(Clone, Debug)]
pub struct StringMatch {
pub candidate_id: usize,
pub score: f64,
pub positions: Vec<usize>,
pub string: String,
}
impl PartialEq for StringMatch {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
impl Eq for StringMatch {}
impl PartialOrd for StringMatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for StringMatch {
fn cmp(&self, other: &Self) -> Ordering {
self.score
.partial_cmp(&other.score)
.unwrap_or(Ordering::Equal)
.then_with(|| self.candidate_id.cmp(&other.candidate_id))
}
}
impl PartialEq for PathMatch {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
impl Eq for PathMatch {}
impl PartialOrd for PathMatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PathMatch {
fn cmp(&self, other: &Self) -> Ordering {
self.score
.partial_cmp(&other.score)
.unwrap_or(Ordering::Equal)
.then_with(|| self.worktree_id.cmp(&other.worktree_id))
.then_with(|| Arc::as_ptr(&self.path).cmp(&Arc::as_ptr(&other.path)))
}
}
pub async fn match_strings(
candidates: &[StringMatchCandidate],
query: &str,
smart_case: bool,
max_results: usize,
cancel_flag: &AtomicBool,
background: Arc<executor::Background>,
) -> Vec<StringMatch> {
if candidates.is_empty() || max_results == 0 {
return Default::default();
}
if query.is_empty() {
return candidates
.iter()
.map(|candidate| StringMatch {
candidate_id: candidate.id,
score: 0.,
positions: Default::default(),
string: candidate.string.clone(),
})
.collect();
}
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let lowercase_query = &lowercase_query;
let query = &query;
let query_char_bag = CharBag::from(&lowercase_query[..]);
let num_cpus = background.num_cpus().min(candidates.len());
let segment_size = (candidates.len() + num_cpus - 1) / num_cpus;
let mut segment_results = (0..num_cpus)
.map(|_| Vec::with_capacity(max_results.min(candidates.len())))
.collect::<Vec<_>>();
background
.scoped(|scope| {
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
let cancel_flag = &cancel_flag;
scope.spawn(async move {
let segment_start = cmp::min(segment_idx * segment_size, candidates.len());
let segment_end = cmp::min(segment_start + segment_size, candidates.len());
let mut matcher = Matcher::new(
query,
lowercase_query,
query_char_bag,
smart_case,
max_results,
);
matcher.match_strings(
&candidates[segment_start..segment_end],
results,
cancel_flag,
);
});
}
})
.await;
let mut results = Vec::new();
for segment_result in segment_results {
if results.is_empty() {
results = segment_result;
} else {
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
}
}
results
}
pub async fn match_paths<'a, Set: PathMatchCandidateSet<'a>>(
candidate_sets: &'a [Set],
query: &str,
smart_case: bool,
max_results: usize,
cancel_flag: &AtomicBool,
background: Arc<executor::Background>,
) -> Vec<PathMatch> {
let path_count: usize = candidate_sets.iter().map(|s| s.len()).sum();
if path_count == 0 {
return Vec::new();
}
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let lowercase_query = &lowercase_query;
let query = &query;
let query_char_bag = CharBag::from(&lowercase_query[..]);
let num_cpus = background.num_cpus().min(path_count);
let segment_size = (path_count + num_cpus - 1) / num_cpus;
let mut segment_results = (0..num_cpus)
.map(|_| Vec::with_capacity(max_results))
.collect::<Vec<_>>();
background
.scoped(|scope| {
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
scope.spawn(async move {
let segment_start = segment_idx * segment_size;
let segment_end = segment_start + segment_size;
let mut matcher = Matcher::new(
query,
lowercase_query,
query_char_bag,
smart_case,
max_results,
);
let mut tree_start = 0;
for candidate_set in candidate_sets {
let tree_end = tree_start + candidate_set.len();
if tree_start < segment_end && segment_start < tree_end {
let start = cmp::max(tree_start, segment_start) - tree_start;
let end = cmp::min(tree_end, segment_end) - tree_start;
let candidates = candidate_set.candidates(start).take(end - start);
matcher.match_paths(
candidate_set.id(),
candidate_set.prefix(),
candidates,
results,
cancel_flag,
);
}
if tree_end >= segment_end {
break;
}
tree_start = tree_end;
}
})
}
})
.await;
let mut results = Vec::new();
for segment_result in segment_results {
if results.is_empty() {
results = segment_result;
} else {
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
}
}
results
}
impl<'a> Matcher<'a> {
pub fn new(
query: &'a [char],
lowercase_query: &'a [char],
query_char_bag: CharBag,
smart_case: bool,
max_results: usize,
) -> Self {
Self {
query,
lowercase_query,
query_char_bag,
min_score: 0.0,
last_positions: vec![0; query.len()],
match_positions: vec![0; query.len()],
score_matrix: Vec::new(),
best_position_matrix: Vec::new(),
smart_case,
max_results,
}
}
pub fn match_strings(
&mut self,
candidates: &[StringMatchCandidate],
results: &mut Vec<StringMatch>,
cancel_flag: &AtomicBool,
) {
self.match_internal(
&[],
&[],
candidates.iter(),
results,
cancel_flag,
|candidate, score| StringMatch {
candidate_id: candidate.id,
score,
positions: Vec::new(),
string: candidate.string.to_string(),
},
)
}
pub fn match_paths<'c: 'a>(
&mut self,
tree_id: usize,
path_prefix: Arc<str>,
path_entries: impl Iterator<Item = PathMatchCandidate<'c>>,
results: &mut Vec<PathMatch>,
cancel_flag: &AtomicBool,
) {
let prefix = path_prefix.chars().collect::<Vec<_>>();
let lowercase_prefix = prefix
.iter()
.map(|c| c.to_ascii_lowercase())
.collect::<Vec<_>>();
self.match_internal(
&prefix,
&lowercase_prefix,
path_entries,
results,
cancel_flag,
|candidate, score| PathMatch {
score,
worktree_id: tree_id,
positions: Vec::new(),
path: candidate.path.clone(),
path_prefix: path_prefix.clone(),
},
)
}
fn match_internal<C: MatchCandidate, R, F>(
&mut self,
prefix: &[char],
lowercase_prefix: &[char],
candidates: impl Iterator<Item = C>,
results: &mut Vec<R>,
cancel_flag: &AtomicBool,
build_match: F,
) where
R: Match,
F: Fn(&C, f64) -> R,
{
let mut candidate_chars = Vec::new();
let mut lowercase_candidate_chars = Vec::new();
for candidate in candidates {
if !candidate.has_chars(self.query_char_bag) {
continue;
}
if cancel_flag.load(atomic::Ordering::Relaxed) {
break;
}
candidate_chars.clear();
lowercase_candidate_chars.clear();
for c in candidate.to_string().chars() {
candidate_chars.push(c);
lowercase_candidate_chars.push(c.to_ascii_lowercase());
}
if !self.find_last_positions(lowercase_prefix, &lowercase_candidate_chars) {
continue;
}
let matrix_len = self.query.len() * (prefix.len() + candidate_chars.len());
self.score_matrix.clear();
self.score_matrix.resize(matrix_len, None);
self.best_position_matrix.clear();
self.best_position_matrix.resize(matrix_len, 0);
let score = self.score_match(
&candidate_chars,
&lowercase_candidate_chars,
prefix,
lowercase_prefix,
);
if score > 0.0 {
let mut mat = build_match(&candidate, score);
if let Err(i) = results.binary_search_by(|m| mat.cmp(m)) {
if results.len() < self.max_results {
mat.set_positions(self.match_positions.clone());
results.insert(i, mat);
} else if i < results.len() {
results.pop();
mat.set_positions(self.match_positions.clone());
results.insert(i, mat);
}
if results.len() == self.max_results {
self.min_score = results.last().unwrap().score();
}
}
}
}
}
fn find_last_positions(
&mut self,
lowercase_prefix: &[char],
lowercase_candidate: &[char],
) -> bool {
let mut lowercase_prefix = lowercase_prefix.iter();
let mut lowercase_candidate = lowercase_candidate.iter();
for (i, char) in self.lowercase_query.iter().enumerate().rev() {
if let Some(j) = lowercase_candidate.rposition(|c| c == char) {
self.last_positions[i] = j + lowercase_prefix.len();
} else if let Some(j) = lowercase_prefix.rposition(|c| c == char) {
self.last_positions[i] = j;
} else {
return false;
}
}
true
}
fn score_match(
&mut self,
path: &[char],
path_cased: &[char],
prefix: &[char],
lowercase_prefix: &[char],
) -> f64 {
let score = self.recursive_score_match(
path,
path_cased,
prefix,
lowercase_prefix,
0,
0,
self.query.len() as f64,
) * self.query.len() as f64;
if score <= 0.0 {
return 0.0;
}
let path_len = prefix.len() + path.len();
let mut cur_start = 0;
let mut byte_ix = 0;
let mut char_ix = 0;
for i in 0..self.query.len() {
let match_char_ix = self.best_position_matrix[i * path_len + cur_start];
while char_ix < match_char_ix {
let ch = prefix
.get(char_ix)
.or_else(|| path.get(char_ix - prefix.len()))
.unwrap();
byte_ix += ch.len_utf8();
char_ix += 1;
}
cur_start = match_char_ix + 1;
self.match_positions[i] = byte_ix;
}
score
}
#[allow(clippy::too_many_arguments)]
fn recursive_score_match(
&mut self,
path: &[char],
path_cased: &[char],
prefix: &[char],
lowercase_prefix: &[char],
query_idx: usize,
path_idx: usize,
cur_score: f64,
) -> f64 {
if query_idx == self.query.len() {
return 1.0;
}
let path_len = prefix.len() + path.len();
if let Some(memoized) = self.score_matrix[query_idx * path_len + path_idx] {
return memoized;
}
let mut score = 0.0;
let mut best_position = 0;
let query_char = self.lowercase_query[query_idx];
let limit = self.last_positions[query_idx];
let mut last_slash = 0;
for j in path_idx..=limit {
let path_char = if j < prefix.len() {
lowercase_prefix[j]
} else {
path_cased[j - prefix.len()]
};
let is_path_sep = path_char == '/' || path_char == '\\';
if query_idx == 0 && is_path_sep {
last_slash = j;
}
if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') {
let curr = if j < prefix.len() {
prefix[j]
} else {
path[j - prefix.len()]
};
let mut char_score = 1.0;
if j > path_idx {
let last = if j - 1 < prefix.len() {
prefix[j - 1]
} else {
path[j - 1 - prefix.len()]
};
if last == '/' {
char_score = 0.9;
} else if (last == '-' || last == '_' || last == ' ' || last.is_numeric())
|| (last.is_lowercase() && curr.is_uppercase())
{
char_score = 0.8;
} else if last == '.' {
char_score = 0.7;
} else if query_idx == 0 {
char_score = BASE_DISTANCE_PENALTY;
} else {
char_score = MIN_DISTANCE_PENALTY.max(
BASE_DISTANCE_PENALTY
- (j - path_idx - 1) as f64 * ADDITIONAL_DISTANCE_PENALTY,
);
}
}
// Apply a severe penalty if the case doesn't match.
// This will make the exact matches have higher score than the case-insensitive and the
// path insensitive matches.
if (self.smart_case || curr == '/') && self.query[query_idx] != curr {
char_score *= 0.001;
}
let mut multiplier = char_score;
// Scale the score based on how deep within the path we found the match.
if query_idx == 0 {
multiplier /= ((prefix.len() + path.len()) - last_slash) as f64;
}
let mut next_score = 1.0;
if self.min_score > 0.0 {
next_score = cur_score * multiplier;
// Scores only decrease. If we can't pass the previous best, bail
if next_score < self.min_score {
// Ensure that score is non-zero so we use it in the memo table.
if score == 0.0 {
score = 1e-18;
}
continue;
}
}
let new_score = self.recursive_score_match(
path,
path_cased,
prefix,
lowercase_prefix,
query_idx + 1,
j + 1,
next_score,
) * multiplier;
if new_score > score {
score = new_score;
best_position = j;
// Optimization: can't score better than 1.
if new_score == 1.0 {
break;
}
}
}
}
if best_position != 0 {
self.best_position_matrix[query_idx * path_len + path_idx] = best_position;
}
self.score_matrix[query_idx * path_len + path_idx] = Some(score);
score
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_get_last_positions() {
let mut query: &[char] = &['d', 'c'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
assert!(!result);
query = &['c', 'd'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
assert!(result);
assert_eq!(matcher.last_positions, vec![2, 4]);
query = &['z', '/', 'z', 'f'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['z', 'e', 'd', '/'], &['z', 'e', 'd', '/', 'f']);
assert!(result);
assert_eq!(matcher.last_positions, vec![0, 3, 4, 8]);
}
#[test]
fn test_match_path_entries() {
let paths = vec![
"",
"a",
"ab",
"abC",
"abcd",
"alphabravocharlie",
"AlphaBravoCharlie",
"thisisatestdir",
"/////ThisIsATestDir",
"/this/is/a/test/dir",
"/test/tiatd",
];
assert_eq!(
match_query("abc", false, &paths),
vec![
("abC", vec![0, 1, 2]),
("abcd", vec![0, 1, 2]),
("AlphaBravoCharlie", vec![0, 5, 10]),
("alphabravocharlie", vec![4, 5, 10]),
]
);
assert_eq!(
match_query("t/i/a/t/d", false, &paths),
vec![("/this/is/a/test/dir", vec![1, 5, 6, 8, 9, 10, 11, 15, 16]),]
);
assert_eq!(
match_query("tiatd", false, &paths),
vec![
("/test/tiatd", vec![6, 7, 8, 9, 10]),
("/this/is/a/test/dir", vec![1, 6, 9, 11, 16]),
("/////ThisIsATestDir", vec![5, 9, 11, 12, 16]),
("thisisatestdir", vec![0, 2, 6, 7, 11]),
]
);
}
#[test]
fn test_match_multibyte_path_entries() {
let paths = vec!["aαbβ/cγ", "αβγδ/bcde", "c1⃣2⃣3⃣/d4⃣5⃣6⃣/e7⃣8⃣9⃣/f", "/d/🆒/h"];
assert_eq!("1".len(), 7);
assert_eq!(
match_query("bcd", false, &paths),
vec![
("αβγδ/bcde", vec![9, 10, 11]),
("aαbβ/cγ", vec![3, 7, 10]),
]
);
assert_eq!(
match_query("cde", false, &paths),
vec![
("αβγδ/bcde", vec![10, 11, 12]),
("c1⃣2⃣3⃣/d4⃣5⃣6⃣/e7⃣8⃣9⃣/f", vec![0, 23, 46]),
]
);
}
fn match_query<'a>(
query: &str,
smart_case: bool,
paths: &[&'a str],
) -> Vec<(&'a str, Vec<usize>)> {
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let query_chars = CharBag::from(&lowercase_query[..]);
let path_arcs = paths
.iter()
.map(|path| Arc::from(PathBuf::from(path)))
.collect::<Vec<_>>();
let mut path_entries = Vec::new();
for (i, path) in paths.iter().enumerate() {
let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>();
let char_bag = CharBag::from(lowercase_path.as_slice());
path_entries.push(PathMatchCandidate {
char_bag,
path: path_arcs.get(i).unwrap(),
});
}
let mut matcher = Matcher::new(&query, &lowercase_query, query_chars, smart_case, 100);
let cancel_flag = AtomicBool::new(false);
let mut results = Vec::new();
matcher.match_paths(
0,
"".into(),
path_entries.into_iter(),
&mut results,
&cancel_flag,
);
results
.into_iter()
.map(|result| {
(
paths
.iter()
.copied()
.find(|p| result.path.as_ref() == Path::new(p))
.unwrap(),
result.positions,
)
})
.collect()
}
}
pub use paths::{match_path_sets, PathMatch, PathMatchCandidate, PathMatchCandidateSet};
pub use strings::{match_strings, StringMatch, StringMatchCandidate};

463
crates/fuzzy/src/matcher.rs Normal file
View File

@@ -0,0 +1,463 @@
use std::{
borrow::Cow,
sync::atomic::{self, AtomicBool},
};
use crate::CharBag;
const BASE_DISTANCE_PENALTY: f64 = 0.6;
const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05;
const MIN_DISTANCE_PENALTY: f64 = 0.2;
pub struct Matcher<'a> {
query: &'a [char],
lowercase_query: &'a [char],
query_char_bag: CharBag,
smart_case: bool,
max_results: usize,
min_score: f64,
match_positions: Vec<usize>,
last_positions: Vec<usize>,
score_matrix: Vec<Option<f64>>,
best_position_matrix: Vec<usize>,
}
pub trait Match: Ord {
fn score(&self) -> f64;
fn set_positions(&mut self, positions: Vec<usize>);
}
pub trait MatchCandidate {
fn has_chars(&self, bag: CharBag) -> bool;
fn to_string(&self) -> Cow<'_, str>;
}
impl<'a> Matcher<'a> {
pub fn new(
query: &'a [char],
lowercase_query: &'a [char],
query_char_bag: CharBag,
smart_case: bool,
max_results: usize,
) -> Self {
Self {
query,
lowercase_query,
query_char_bag,
min_score: 0.0,
last_positions: vec![0; query.len()],
match_positions: vec![0; query.len()],
score_matrix: Vec::new(),
best_position_matrix: Vec::new(),
smart_case,
max_results,
}
}
pub fn match_candidates<C: MatchCandidate, R, F>(
&mut self,
prefix: &[char],
lowercase_prefix: &[char],
candidates: impl Iterator<Item = C>,
results: &mut Vec<R>,
cancel_flag: &AtomicBool,
build_match: F,
) where
R: Match,
F: Fn(&C, f64) -> R,
{
let mut candidate_chars = Vec::new();
let mut lowercase_candidate_chars = Vec::new();
for candidate in candidates {
if !candidate.has_chars(self.query_char_bag) {
continue;
}
if cancel_flag.load(atomic::Ordering::Relaxed) {
break;
}
candidate_chars.clear();
lowercase_candidate_chars.clear();
for c in candidate.to_string().chars() {
candidate_chars.push(c);
lowercase_candidate_chars.push(c.to_ascii_lowercase());
}
if !self.find_last_positions(lowercase_prefix, &lowercase_candidate_chars) {
continue;
}
let matrix_len = self.query.len() * (prefix.len() + candidate_chars.len());
self.score_matrix.clear();
self.score_matrix.resize(matrix_len, None);
self.best_position_matrix.clear();
self.best_position_matrix.resize(matrix_len, 0);
let score = self.score_match(
&candidate_chars,
&lowercase_candidate_chars,
prefix,
lowercase_prefix,
);
if score > 0.0 {
let mut mat = build_match(&candidate, score);
if let Err(i) = results.binary_search_by(|m| mat.cmp(m)) {
if results.len() < self.max_results {
mat.set_positions(self.match_positions.clone());
results.insert(i, mat);
} else if i < results.len() {
results.pop();
mat.set_positions(self.match_positions.clone());
results.insert(i, mat);
}
if results.len() == self.max_results {
self.min_score = results.last().unwrap().score();
}
}
}
}
}
fn find_last_positions(
&mut self,
lowercase_prefix: &[char],
lowercase_candidate: &[char],
) -> bool {
let mut lowercase_prefix = lowercase_prefix.iter();
let mut lowercase_candidate = lowercase_candidate.iter();
for (i, char) in self.lowercase_query.iter().enumerate().rev() {
if let Some(j) = lowercase_candidate.rposition(|c| c == char) {
self.last_positions[i] = j + lowercase_prefix.len();
} else if let Some(j) = lowercase_prefix.rposition(|c| c == char) {
self.last_positions[i] = j;
} else {
return false;
}
}
true
}
fn score_match(
&mut self,
path: &[char],
path_cased: &[char],
prefix: &[char],
lowercase_prefix: &[char],
) -> f64 {
let score = self.recursive_score_match(
path,
path_cased,
prefix,
lowercase_prefix,
0,
0,
self.query.len() as f64,
) * self.query.len() as f64;
if score <= 0.0 {
return 0.0;
}
let path_len = prefix.len() + path.len();
let mut cur_start = 0;
let mut byte_ix = 0;
let mut char_ix = 0;
for i in 0..self.query.len() {
let match_char_ix = self.best_position_matrix[i * path_len + cur_start];
while char_ix < match_char_ix {
let ch = prefix
.get(char_ix)
.or_else(|| path.get(char_ix - prefix.len()))
.unwrap();
byte_ix += ch.len_utf8();
char_ix += 1;
}
cur_start = match_char_ix + 1;
self.match_positions[i] = byte_ix;
}
score
}
#[allow(clippy::too_many_arguments)]
fn recursive_score_match(
&mut self,
path: &[char],
path_cased: &[char],
prefix: &[char],
lowercase_prefix: &[char],
query_idx: usize,
path_idx: usize,
cur_score: f64,
) -> f64 {
if query_idx == self.query.len() {
return 1.0;
}
let path_len = prefix.len() + path.len();
if let Some(memoized) = self.score_matrix[query_idx * path_len + path_idx] {
return memoized;
}
let mut score = 0.0;
let mut best_position = 0;
let query_char = self.lowercase_query[query_idx];
let limit = self.last_positions[query_idx];
let mut last_slash = 0;
for j in path_idx..=limit {
let path_char = if j < prefix.len() {
lowercase_prefix[j]
} else {
path_cased[j - prefix.len()]
};
let is_path_sep = path_char == '/' || path_char == '\\';
if query_idx == 0 && is_path_sep {
last_slash = j;
}
if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') {
let curr = if j < prefix.len() {
prefix[j]
} else {
path[j - prefix.len()]
};
let mut char_score = 1.0;
if j > path_idx {
let last = if j - 1 < prefix.len() {
prefix[j - 1]
} else {
path[j - 1 - prefix.len()]
};
if last == '/' {
char_score = 0.9;
} else if (last == '-' || last == '_' || last == ' ' || last.is_numeric())
|| (last.is_lowercase() && curr.is_uppercase())
{
char_score = 0.8;
} else if last == '.' {
char_score = 0.7;
} else if query_idx == 0 {
char_score = BASE_DISTANCE_PENALTY;
} else {
char_score = MIN_DISTANCE_PENALTY.max(
BASE_DISTANCE_PENALTY
- (j - path_idx - 1) as f64 * ADDITIONAL_DISTANCE_PENALTY,
);
}
}
// Apply a severe penalty if the case doesn't match.
// This will make the exact matches have higher score than the case-insensitive and the
// path insensitive matches.
if (self.smart_case || curr == '/') && self.query[query_idx] != curr {
char_score *= 0.001;
}
let mut multiplier = char_score;
// Scale the score based on how deep within the path we found the match.
if query_idx == 0 {
multiplier /= ((prefix.len() + path.len()) - last_slash) as f64;
}
let mut next_score = 1.0;
if self.min_score > 0.0 {
next_score = cur_score * multiplier;
// Scores only decrease. If we can't pass the previous best, bail
if next_score < self.min_score {
// Ensure that score is non-zero so we use it in the memo table.
if score == 0.0 {
score = 1e-18;
}
continue;
}
}
let new_score = self.recursive_score_match(
path,
path_cased,
prefix,
lowercase_prefix,
query_idx + 1,
j + 1,
next_score,
) * multiplier;
if new_score > score {
score = new_score;
best_position = j;
// Optimization: can't score better than 1.
if new_score == 1.0 {
break;
}
}
}
}
if best_position != 0 {
self.best_position_matrix[query_idx * path_len + path_idx] = best_position;
}
self.score_matrix[query_idx * path_len + path_idx] = Some(score);
score
}
}
#[cfg(test)]
mod tests {
use crate::{PathMatch, PathMatchCandidate};
use super::*;
use std::{
path::{Path, PathBuf},
sync::Arc,
};
#[test]
fn test_get_last_positions() {
let mut query: &[char] = &['d', 'c'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
assert!(!result);
query = &['c', 'd'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
assert!(result);
assert_eq!(matcher.last_positions, vec![2, 4]);
query = &['z', '/', 'z', 'f'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let result = matcher.find_last_positions(&['z', 'e', 'd', '/'], &['z', 'e', 'd', '/', 'f']);
assert!(result);
assert_eq!(matcher.last_positions, vec![0, 3, 4, 8]);
}
#[test]
fn test_match_path_entries() {
let paths = vec![
"",
"a",
"ab",
"abC",
"abcd",
"alphabravocharlie",
"AlphaBravoCharlie",
"thisisatestdir",
"/////ThisIsATestDir",
"/this/is/a/test/dir",
"/test/tiatd",
];
assert_eq!(
match_single_path_query("abc", false, &paths),
vec![
("abC", vec![0, 1, 2]),
("abcd", vec![0, 1, 2]),
("AlphaBravoCharlie", vec![0, 5, 10]),
("alphabravocharlie", vec![4, 5, 10]),
]
);
assert_eq!(
match_single_path_query("t/i/a/t/d", false, &paths),
vec![("/this/is/a/test/dir", vec![1, 5, 6, 8, 9, 10, 11, 15, 16]),]
);
assert_eq!(
match_single_path_query("tiatd", false, &paths),
vec![
("/test/tiatd", vec![6, 7, 8, 9, 10]),
("/this/is/a/test/dir", vec![1, 6, 9, 11, 16]),
("/////ThisIsATestDir", vec![5, 9, 11, 12, 16]),
("thisisatestdir", vec![0, 2, 6, 7, 11]),
]
);
}
#[test]
fn test_match_multibyte_path_entries() {
let paths = vec!["aαbβ/cγ", "αβγδ/bcde", "c1⃣2⃣3⃣/d4⃣5⃣6⃣/e7⃣8⃣9⃣/f", "/d/🆒/h"];
assert_eq!("1".len(), 7);
assert_eq!(
match_single_path_query("bcd", false, &paths),
vec![
("αβγδ/bcde", vec![9, 10, 11]),
("aαbβ/cγ", vec![3, 7, 10]),
]
);
assert_eq!(
match_single_path_query("cde", false, &paths),
vec![
("αβγδ/bcde", vec![10, 11, 12]),
("c1⃣2⃣3⃣/d4⃣5⃣6⃣/e7⃣8⃣9⃣/f", vec![0, 23, 46]),
]
);
}
fn match_single_path_query<'a>(
query: &str,
smart_case: bool,
paths: &[&'a str],
) -> Vec<(&'a str, Vec<usize>)> {
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let query_chars = CharBag::from(&lowercase_query[..]);
let path_arcs: Vec<Arc<Path>> = paths
.iter()
.map(|path| Arc::from(PathBuf::from(path)))
.collect::<Vec<_>>();
let mut path_entries = Vec::new();
for (i, path) in paths.iter().enumerate() {
let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>();
let char_bag = CharBag::from(lowercase_path.as_slice());
path_entries.push(PathMatchCandidate {
char_bag,
path: &path_arcs[i],
});
}
let mut matcher = Matcher::new(&query, &lowercase_query, query_chars, smart_case, 100);
let cancel_flag = AtomicBool::new(false);
let mut results = Vec::new();
matcher.match_candidates(
&[],
&[],
path_entries.into_iter(),
&mut results,
&cancel_flag,
|candidate, score| PathMatch {
score,
worktree_id: 0,
positions: Vec::new(),
path: candidate.path.clone(),
path_prefix: "".into(),
},
);
results
.into_iter()
.map(|result| {
(
paths
.iter()
.copied()
.find(|p| result.path.as_ref() == Path::new(p))
.unwrap(),
result.positions,
)
})
.collect()
}
}

174
crates/fuzzy/src/paths.rs Normal file
View File

@@ -0,0 +1,174 @@
use std::{
borrow::Cow,
cmp::{self, Ordering},
path::Path,
sync::{atomic::AtomicBool, Arc},
};
use gpui::executor;
use crate::{
matcher::{Match, MatchCandidate, Matcher},
CharBag,
};
#[derive(Clone, Debug)]
pub struct PathMatchCandidate<'a> {
pub path: &'a Arc<Path>,
pub char_bag: CharBag,
}
#[derive(Clone, Debug)]
pub struct PathMatch {
pub score: f64,
pub positions: Vec<usize>,
pub worktree_id: usize,
pub path: Arc<Path>,
pub path_prefix: Arc<str>,
}
pub trait PathMatchCandidateSet<'a>: Send + Sync {
type Candidates: Iterator<Item = PathMatchCandidate<'a>>;
fn id(&self) -> usize;
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn prefix(&self) -> Arc<str>;
fn candidates(&'a self, start: usize) -> Self::Candidates;
}
impl Match for PathMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl<'a> MatchCandidate for PathMatchCandidate<'a> {
fn has_chars(&self, bag: CharBag) -> bool {
self.char_bag.is_superset(bag)
}
fn to_string(&self) -> Cow<'a, str> {
self.path.to_string_lossy()
}
}
impl PartialEq for PathMatch {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
impl Eq for PathMatch {}
impl PartialOrd for PathMatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PathMatch {
fn cmp(&self, other: &Self) -> Ordering {
self.score
.partial_cmp(&other.score)
.unwrap_or(Ordering::Equal)
.then_with(|| self.worktree_id.cmp(&other.worktree_id))
.then_with(|| self.path.cmp(&other.path))
}
}
pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
candidate_sets: &'a [Set],
query: &str,
smart_case: bool,
max_results: usize,
cancel_flag: &AtomicBool,
background: Arc<executor::Background>,
) -> Vec<PathMatch> {
let path_count: usize = candidate_sets.iter().map(|s| s.len()).sum();
if path_count == 0 {
return Vec::new();
}
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let lowercase_query = &lowercase_query;
let query = &query;
let query_char_bag = CharBag::from(&lowercase_query[..]);
let num_cpus = background.num_cpus().min(path_count);
let segment_size = (path_count + num_cpus - 1) / num_cpus;
let mut segment_results = (0..num_cpus)
.map(|_| Vec::with_capacity(max_results))
.collect::<Vec<_>>();
background
.scoped(|scope| {
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
scope.spawn(async move {
let segment_start = segment_idx * segment_size;
let segment_end = segment_start + segment_size;
let mut matcher = Matcher::new(
query,
lowercase_query,
query_char_bag,
smart_case,
max_results,
);
let mut tree_start = 0;
for candidate_set in candidate_sets {
let tree_end = tree_start + candidate_set.len();
if tree_start < segment_end && segment_start < tree_end {
let start = cmp::max(tree_start, segment_start) - tree_start;
let end = cmp::min(tree_end, segment_end) - tree_start;
let candidates = candidate_set.candidates(start).take(end - start);
let worktree_id = candidate_set.id();
let prefix = candidate_set.prefix().chars().collect::<Vec<_>>();
let lowercase_prefix = prefix
.iter()
.map(|c| c.to_ascii_lowercase())
.collect::<Vec<_>>();
matcher.match_candidates(
&prefix,
&lowercase_prefix,
candidates,
results,
cancel_flag,
|candidate, score| PathMatch {
score,
worktree_id,
positions: Vec::new(),
path: candidate.path.clone(),
path_prefix: candidate_set.prefix(),
},
);
}
if tree_end >= segment_end {
break;
}
tree_start = tree_end;
}
})
}
})
.await;
let mut results = Vec::new();
for segment_result in segment_results {
if results.is_empty() {
results = segment_result;
} else {
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
}
}
results
}

161
crates/fuzzy/src/strings.rs Normal file
View File

@@ -0,0 +1,161 @@
use std::{
borrow::Cow,
cmp::{self, Ordering},
sync::{atomic::AtomicBool, Arc},
};
use gpui::executor;
use crate::{
matcher::{Match, MatchCandidate, Matcher},
CharBag,
};
#[derive(Clone, Debug)]
pub struct StringMatchCandidate {
pub id: usize,
pub string: String,
pub char_bag: CharBag,
}
impl Match for StringMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl StringMatchCandidate {
pub fn new(id: usize, string: String) -> Self {
Self {
id,
char_bag: CharBag::from(string.as_str()),
string,
}
}
}
impl<'a> MatchCandidate for &'a StringMatchCandidate {
fn has_chars(&self, bag: CharBag) -> bool {
self.char_bag.is_superset(bag)
}
fn to_string(&self) -> Cow<'a, str> {
self.string.as_str().into()
}
}
#[derive(Clone, Debug)]
pub struct StringMatch {
pub candidate_id: usize,
pub score: f64,
pub positions: Vec<usize>,
pub string: String,
}
impl PartialEq for StringMatch {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
impl Eq for StringMatch {}
impl PartialOrd for StringMatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for StringMatch {
fn cmp(&self, other: &Self) -> Ordering {
self.score
.partial_cmp(&other.score)
.unwrap_or(Ordering::Equal)
.then_with(|| self.candidate_id.cmp(&other.candidate_id))
}
}
pub async fn match_strings(
candidates: &[StringMatchCandidate],
query: &str,
smart_case: bool,
max_results: usize,
cancel_flag: &AtomicBool,
background: Arc<executor::Background>,
) -> Vec<StringMatch> {
if candidates.is_empty() || max_results == 0 {
return Default::default();
}
if query.is_empty() {
return candidates
.iter()
.map(|candidate| StringMatch {
candidate_id: candidate.id,
score: 0.,
positions: Default::default(),
string: candidate.string.clone(),
})
.collect();
}
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let lowercase_query = &lowercase_query;
let query = &query;
let query_char_bag = CharBag::from(&lowercase_query[..]);
let num_cpus = background.num_cpus().min(candidates.len());
let segment_size = (candidates.len() + num_cpus - 1) / num_cpus;
let mut segment_results = (0..num_cpus)
.map(|_| Vec::with_capacity(max_results.min(candidates.len())))
.collect::<Vec<_>>();
background
.scoped(|scope| {
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
let cancel_flag = &cancel_flag;
scope.spawn(async move {
let segment_start = cmp::min(segment_idx * segment_size, candidates.len());
let segment_end = cmp::min(segment_start + segment_size, candidates.len());
let mut matcher = Matcher::new(
query,
lowercase_query,
query_char_bag,
smart_case,
max_results,
);
matcher.match_candidates(
&[],
&[],
candidates[segment_start..segment_end].iter(),
results,
cancel_flag,
|candidate, score| StringMatch {
candidate_id: candidate.id,
score,
positions: Vec::new(),
string: candidate.string.to_string(),
},
);
});
}
})
.await;
let mut results = Vec::new();
for segment_result in segment_results {
if results.is_empty() {
results = segment_result;
} else {
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
}
}
results
}

View File

@@ -2,6 +2,7 @@
name = "git"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/git.rs"

View File

@@ -71,18 +71,26 @@ impl BufferDiff {
}
}
pub fn hunks_in_range<'a>(
pub fn hunks_in_row_range<'a>(
&'a self,
query_row_range: Range<u32>,
range: Range<u32>,
buffer: &'a BufferSnapshot,
reversed: bool,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let start = buffer.anchor_before(Point::new(query_row_range.start, 0));
let end = buffer.anchor_after(Point::new(query_row_range.end, 0));
let start = buffer.anchor_before(Point::new(range.start, 0));
let end = buffer.anchor_after(Point::new(range.end, 0));
self.hunks_intersecting_range(start..end, buffer, reversed)
}
pub fn hunks_intersecting_range<'a>(
&'a self,
range: Range<Anchor>,
buffer: &'a BufferSnapshot,
reversed: bool,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| {
let before_start = summary.buffer_range.end.cmp(&start, buffer).is_lt();
let after_end = summary.buffer_range.start.cmp(&end, buffer).is_gt();
let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt();
!before_start && !after_end
});
@@ -141,7 +149,9 @@ impl BufferDiff {
#[cfg(test)]
fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
self.hunks_in_range(0..u32::MAX, text, false)
let start = text.anchor_before(Point::new(0, 0));
let end = text.anchor_after(Point::new(u32::MAX, u32::MAX));
self.hunks_intersecting_range(start..end, text, false)
}
fn diff<'a>(head: &'a str, current: &'a str) -> Option<GitPatch<'a>> {
@@ -355,7 +365,7 @@ mod tests {
assert_eq!(diff.hunks(&buffer).count(), 8);
assert_hunks(
diff.hunks_in_range(7..12, &buffer, false),
diff.hunks_in_row_range(7..12, &buffer, false),
&buffer,
&diff_base,
&[

View File

@@ -2,6 +2,7 @@
name = "go_to_line"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/go_to_line.rs"

View File

@@ -4,6 +4,7 @@ edition = "2021"
name = "gpui"
version = "0.1.0"
description = "A GPU-accelerated UI framework"
publish = false
[lib]
path = "src/gpui.rs"
@@ -45,8 +46,8 @@ 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"
uuid = { version = "1.1.2", features = ["v4"] }
waker-fn = "1.1.0"
[build-dependencies]
@@ -66,7 +67,7 @@ media = { path = "../media" }
anyhow = "1"
block = "0.1"
cocoa = "0.24"
core-foundation = "0.9.3"
core-foundation = { version = "0.9.3", features = ["with-uuid"] }
core-graphics = "0.22.3"
core-text = "19.2"
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "8eaf7a918eafa28b0a37dc759e2e0e7683fa24f1" }

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

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