Compare commits

..

257 Commits

Author SHA1 Message Date
Joseph Lyons
ab978ff1a3 collab 0.3.0 2022-12-08 16:35:13 -05:00
Joseph T. Lyons
dcd4b8f7db Merge pull request #1941 from zed-industries/Allow-overwriting-signup-data
Allow overwriting signup data if a user signs up more than once with the same email address
2022-12-08 16:11:28 -05:00
Kay Simmons
2eb335158b Merge pull request #1946 from zed-industries/fix-zombie-tooltips
notify views when hover finishes in tooltip wrapper
2022-12-08 11:37:12 -08:00
Kay Simmons
10aecc310e notify views when hover finishes in tooltip wrapper 2022-12-08 11:26:46 -08:00
Kay Simmons
750e7eb833 Merge pull request #1945 from zed-industries/drag-and-drop-deadzones
Add deadzones to drag and drop
2022-12-08 11:15:42 -08:00
Kay Simmons
36bc90b2b8 Add deadzones to drag and drop 2022-12-07 17:46:00 -08:00
Antonio Scandurra
3313387b28 Merge pull request #1943 from zed-industries/fix-inviting-existing-users-via-different-mail
Fix inviting existing users via a different email address
2022-12-07 14:19:17 +01:00
Joseph Lyons
d71d543337 Ensure that subsequent signup happens after initial
We can't rely on the fact that the test won't run fast enough such that both `created_at`s are the same time.  This ensures the subsequent signup happens after the initial one and that the database doesn't overwrite the initial one.
2022-12-07 08:15:01 -05:00
Antonio Scandurra
665219fb00 Fix inviting user that had already signed up via a different email 2022-12-07 14:07:01 +01:00
Antonio Scandurra
1b8f23eeed Add failing test showcasing inviting existing user via different email 2022-12-07 14:06:59 +01:00
Joseph Lyons
5f31907127 Clean up test 2022-12-07 07:12:27 -05:00
Joseph Lyons
97989b04a0 Remove comment 2022-12-06 17:18:54 -05:00
Joseph Lyons
694840cdd6 Allow overwriting signup data if a user signs up more than once with the same email address 2022-12-06 17:12:12 -05:00
Antonio Scandurra
1920de81d9 Merge pull request #1938 from zed-industries/fix-metrics
Query project count as `i64` instead of `i32` when gathering metrics
2022-12-06 15:04:27 +01:00
Antonio Scandurra
3b5b48c043 Query project count as i64 instead of i32 when gathering metrics
Using the latter will cause a type mismatch when performing the query.
2022-12-06 15:00:32 +01:00
Antonio Scandurra
2080d3efff Merge pull request #1937 from zed-industries/fix-accepted-contact-busy-status
Fix busy status when accepting a contact request
2022-12-06 10:29:58 +01:00
Antonio Scandurra
fc7b01b74e Fix busy status when accepting a contact request
Previously, we would send an contact update when accepting a request
using the same `busy` status for both the requester and the responder.
This was obviously wrong and caused the requester to see their own
busy status as the newly-added responder contact's status.
2022-12-06 10:19:34 +01:00
Antonio Scandurra
f1b35981c2 Merge pull request #1935 from zed-industries/reconnections-2
Move in-memory server state to the database
2022-12-06 09:22:59 +01:00
Antonio Scandurra
744714b478 Remove unused UserId import from seed script 2022-12-06 09:07:25 +01:00
Max Brunsfeld
35549ffabe Merge pull request #1936 from zed-industries/c-outline-pointers
Include outline items for c/c++ functions returning pointers
2022-12-05 14:03:49 -08:00
Max Brunsfeld
855f17c378 Include outline items for c/c++ functions returning pointers-to-pointers, references
Co-authored-by: Julia Risley <julia@zed.dev>
2022-12-05 13:56:21 -08:00
Mikayla Maki
f23f294b86 Merge pull request #1858 from zed-industries/add-lisp
Added tree sitter support for scheme and racket
2022-12-05 11:40:52 -08:00
Mikayla Maki
0921178b42 Got tree sitter integration to a shippable place 2022-12-05 11:31:52 -08:00
Mikayla Maki
30872d3992 Added experimental support for scheme, racket, and commonlisp 2022-12-05 11:31:49 -08:00
Antonio Scandurra
cd08d289aa Fix warnings 2022-12-05 19:45:56 +01:00
Antonio Scandurra
9a62150dce Merge branch 'main' into reconnections-2 2022-12-05 19:18:40 +01:00
Antonio Scandurra
7bbd97cfb9 Send diagnostic summaries synchronously 2022-12-05 19:07:06 +01:00
Antonio Scandurra
5443d9cffe Return project collaborators and connection IDs in a RoomGuard 2022-12-05 18:37:01 +01:00
Antonio Scandurra
be3fb1e985 Update sea-orm to fix bug on failure to commit transactions
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-12-05 18:36:25 +01:00
Antonio Scandurra
b97c35a468 Remove project_id foreign key from room_participants 2022-12-05 15:16:06 +01:00
Antonio Scandurra
eec3df09be Upgrade sea-orm 2022-12-05 14:56:01 +01:00
Antonio Scandurra
d3c411677a Remove random pauses to prevent the database from deadlocking 2022-12-05 12:03:45 +01:00
Antonio Scandurra
d97a8364ad Retry transactions if there's a serialization failure during commit 2022-12-05 10:49:53 +01:00
Antonio Scandurra
0ed731780a Remove duplication between transaction and room_transaction 2022-12-05 09:46:03 +01:00
Julia
11c1254e71 Merge pull request #1924 from zed-industries/simon-says-dont-move
Do not reorder tab opened by follower to end of item list
2022-12-04 13:00:07 -05:00
Mikayla Maki
6ba225f3a5 Merge pull request #1798 from zed-industries/serializing-workspaces
Serializing workspaces
2022-12-03 16:56:02 -08:00
Mikayla Maki
55eb0a3742 Fixed and error message and properly initialized the DB 2022-12-03 16:46:35 -08:00
Mikayla Maki
1ce0863158 Removed old code 2022-12-03 16:27:45 -08:00
Mikayla Maki
d609237c32 Found db parallelism problem :( 2022-12-03 16:26:37 -08:00
Mikayla Maki
4288f10873 And library change 2022-12-03 16:13:02 -08:00
Mikayla Maki
80e035cc2c Fixed bad rebase 2022-12-03 16:12:07 -08:00
Mikayla Maki
a1f273278b Added user notifications 2022-12-03 16:06:02 -08:00
Mikayla Maki
ffcad4e4e2 WIP fixing dock problems 2022-12-03 16:06:02 -08:00
Mikayla Maki
5262e8c77e CHANGE LOCK TO NOT BE DROPPED INSTANTLY. DANG U RUST
co-authored-by: kay@zed.dev
2022-12-03 16:06:02 -08:00
Mikayla Maki
5e240f98f0 Reworked thread safe connection be threadsafer,,,, again
Co-Authored-By: kay@zed.dev
2022-12-03 16:06:02 -08:00
Mikayla Maki
189a820113 First draft of graceful corruption restoration 2022-12-03 16:06:02 -08:00
Mikayla Maki
b8d423555b Added side bar restoration 2022-12-03 16:06:02 -08:00
Kay Simmons
8a48567857 Reactivate the correct item in each pane when deserializing 2022-12-03 16:06:01 -08:00
Kay Simmons
f68e8d4664 Address some issues with the sqlez_macros 2022-12-03 16:06:01 -08:00
Kay Simmons
1b225fa37c fix test failures 2022-12-03 16:06:01 -08:00
Kay Simmons
a29ccb4ff8 make thread safe connection more thread safe
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-12-03 16:06:01 -08:00
Mikayla Maki
9cd6894dc5 Added multi-threading problem test 2022-12-03 16:06:01 -08:00
Kay Simmons
dd9d20be25 Added sql! proc macro which checks syntax errors on sql code and displays them with reasonable underline locations
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-12-03 16:06:01 -08:00
Mikayla Maki
260164a711 Added basic syntax checker to sqlez 2022-12-03 16:06:01 -08:00
Kay Simmons
359b8aaf47 rename sql_method to query and adjust the syntax to more closely match function definitions 2022-12-03 16:06:01 -08:00
Kay Simmons
1cc3e4820a working serialized writes with panics on failure. Everything seems to be working 2022-12-03 16:06:01 -08:00
Mikayla Maki
b01243109e Removed database test files 2022-12-03 16:06:01 -08:00
Mikayla Maki
3e0f9d27a7 Made dev tools not break everything about the db
Also improved multi statements to allow out of order parameter binding in statements
Ensured that all statements are run for maybe_row and single, and that of all statements only 1 of them returns only 1 row
Made bind and column calls add useful context to errors

Co-authored-by: kay@zed.dev
2022-12-03 16:06:01 -08:00
Mikayla Maki
2dc1130902 Added extra sql methods 2022-12-03 16:06:01 -08:00
Mikayla Maki
37174f45f0 Touched up sql macro 2022-12-03 16:06:01 -08:00
Mikayla Maki
76c42af62a Finished terminal working directory restoration 2022-12-03 16:06:01 -08:00
Mikayla Maki
cf4c103660 Fixed workspace tests 2022-12-03 16:06:01 -08:00
Mikayla Maki
e1eff3f4cd WIP: Some bugs switching to database provided IDs, terminal titles don't reload when restored from serialized, workspace tests are no longer passing but should be easy to fix when it isn't 11:44 2022-12-03 16:06:01 -08:00
Mikayla Maki
a47f2ca445 Added UUID based, stable workspace ID for caching on item startup. Completed first sketch of terminal persistence. Still need to debug it though.... 2022-12-03 16:06:01 -08:00
Mikayla Maki
e659823e6c WIP termial implementation. need some way of getting the currently valid workspace ID 2022-12-03 16:06:01 -08:00
Mikayla Maki
a8ed95e1dc Implementing persistence for the terminal working directory, found an issue with my current data model. :( 2022-12-03 16:06:01 -08:00
Kay Simmons
cb1d2cd1f2 WIP serializing and deserializing editors 2022-12-03 16:06:01 -08:00
Mikayla Maki
9077b058a2 removed test file 2022-12-03 16:06:01 -08:00
Mikayla Maki
7ceb5e815e workspace level integration of serialization complete! Time for item level integration....
Co-Authored-By: kay@zed.dev
2022-12-03 16:06:01 -08:00
Mikayla Maki
992b94eef3 Rebased to main 2022-12-03 16:06:01 -08:00
Mikayla Maki
a0cb6542ba Polishing workspace data structures
Co-authored-by: kay@zed.dev
2022-12-03 16:06:01 -08:00
Mikayla Maki
6530658c3e Added center group deserialization 2022-12-03 16:06:01 -08:00
Kay Simmons
75d3d46b1b wip serialize editor 2022-12-03 16:06:01 -08:00
Kay Simmons
d20d21c6a2 Dock persistence working!
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-12-03 16:06:01 -08:00
Kay Simmons
c1f7902309 wip 2022-12-03 16:06:01 -08:00
Mikayla Maki
4798161118 Distributed database pattern built.
Co-Authored-By: kay@zed.dev
2022-12-03 16:06:01 -08:00
Mikayla Maki
2a5565ca93 WIP 2022-12-03 16:06:00 -08:00
Mikayla Maki
a5edac312e Moved to workspaces crate... don't feel great about it 2022-12-03 16:05:26 -08:00
Mikayla Maki
e578f2530e WIP commit, migrating workspace serialization code into the workspace 2022-12-03 16:05:25 -08:00
Mikayla Maki
c84201fc9f Done first draft of strongly typed migrations 2022-12-03 16:05:25 -08:00
Kay Simmons
4a00f0b062 Add typed statements 2022-12-03 16:05:25 -08:00
Mikayla Maki
64ac84fdf4 Re-use big union statement for get_center_pane 2022-12-03 16:05:25 -08:00
Mikayla Maki
f27a9d77d1 Finished the bulk of workspace serialization. Just items and wiring it all through.
Co-Authored-By: kay@zed.dev
2022-12-03 16:05:25 -08:00
Mikayla Maki
0186289420 Refined sqlez, implemented 60% of workspace serialization sql 2022-12-03 16:05:25 -08:00
Mikayla Maki
6b214acbc4 Got Zed compiling again 🥰 2022-12-03 16:05:25 -08:00
Kay Simmons
d419f27d75 replace worktree roots table with serialized worktree roots list 2022-12-03 16:05:25 -08:00
Kay Simmons
eb0598dac2 more refactoring and slightly better api 2022-12-03 16:05:25 -08:00
Mikayla Maki
aa7b909b7b WIP3 2022-12-03 16:05:25 -08:00
Mikayla Maki
b552f1788c WIP2 2022-12-03 16:05:25 -08:00
Mikayla Maki
d492cbced9 WIP 2022-12-03 16:05:25 -08:00
Mikayla Maki
19aac6a57f Moved docks to a better position 2022-12-03 16:05:25 -08:00
Kay Simmons
685bc9fed3 impl bind and column and adjust pane tables 2022-12-03 16:05:25 -08:00
Mikayla Maki
406663c75e Converted to sqlez, so much nicer 2022-12-03 16:05:25 -08:00
Mikayla Maki
c8face33fa WIP, incorporating type parsing using new sqlez patterns 2022-12-03 16:05:25 -08:00
Mikayla Maki
3c1b747f64 WIP almost compiling with sqlez 2022-12-03 16:05:25 -08:00
Mikayla Maki
777f05eb76 Finished implementing the workspace stuff 2022-12-03 16:05:25 -08:00
Mikayla Maki
395070cb92 remove submodule 2022-12-03 16:05:25 -08:00
Mikayla Maki
a4a1859dfc Added sqlez api 2022-12-03 16:05:25 -08:00
Kay Simmons
e3fdfe02e5 WIP switching to sqlez 2022-12-03 16:05:24 -08:00
Mikayla Maki
7744c9ba45 Abandoning rusqlite, the API is miserable 2022-12-03 16:04:10 -08:00
Mikayla Maki
e6ca0adbcb Fixed failing serialization issues 2022-12-03 16:04:10 -08:00
Mikayla Maki
c105f41487 Started working on dock panes
co-authored-by: kay@zed.dev
2022-12-03 16:04:10 -08:00
Mikayla Maki
ddecba143f Refactored workspaces API and corrected method headers + fixed bug caused by migration failures
co-authored-by: kay@zed.dev
2022-12-03 16:04:10 -08:00
Mikayla Maki
3451a3c7fe Rebase - Got Zed compiling and fixed a build error due to conflicting dependencies that cargo didn't catch :(
Co-Authored-By: kay@zed.dev
2022-12-03 16:04:10 -08:00
Mikayla Maki
b9cbd4084e WIP: fixing up behavior of workspace initialization 2022-12-03 16:04:10 -08:00
Mikayla Maki
5505a776e6 Figured out a good schema for the pane serialization stuff 2022-12-03 16:04:10 -08:00
Mikayla Maki
46ff0885f0 WIP: Writing tests 2022-12-03 16:04:10 -08:00
Mikayla Maki
a9dc46c950 added stubs for more tests 2022-12-03 16:04:10 -08:00
Mikayla Maki
7d33520b2c Tidied up code, managed errors, etc. 2022-12-03 16:04:10 -08:00
Mikayla Maki
e9ea751f3d All workspace tests passing :D 2022-12-03 16:04:10 -08:00
Mikayla Maki
d7bbfb82a3 Rebase - Successfully detecting workplace IDs :D 2022-12-03 16:04:10 -08:00
Mikayla Maki
500ecbf915 Rebase fix + Started writing the real SQL we're going to need 2022-12-03 16:04:10 -08:00
K Simmons
e5c6393f85 rebase fix - almost have serialize_workspace piped to the workspace constructor. Just a few compile errors left 2022-12-03 16:04:10 -08:00
K Simmons
73f0459a0f wip 2022-12-03 16:04:10 -08:00
K Simmons
0c466f806c WIP 2022-12-03 16:04:10 -08:00
Mikayla Maki
b48e28b555 Built first draft of workspace serialization schemas, started writing DB tests
Co-Authored-By: kay@zed.dev
2022-12-03 16:04:10 -08:00
Mikayla Maki
60ebe33518 Rebase fix - Reworking approach to sql for take 2022-12-03 16:04:10 -08:00
Mikayla Maki
72c1ee904b Fix rebase - Broken tab 2022-12-03 16:04:10 -08:00
Julia
57e10b7dd5 Cleanup dbg 2022-12-02 16:42:49 -05:00
Julia
4bc1d77535 Fix tab following order test to wait for file open to propagate
Now it can actually repro the original bug

Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-12-02 16:09:37 -05:00
Antonio Scandurra
d96f524fb6 WIP: Manually rollback transactions to avoid spurious savepoint failure
TODO:
- Avoid unwrapping transaction after f(tx)
- Remove duplication between `transaction` and `room_transaction`
- Introduce random delay before and after committing a transaction
- Run lots of randomized tests
- Investigate diverging diagnostic summaries

Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-12-02 20:36:50 +01:00
Antonio Scandurra
1c30767592 Remove stale Error variant
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-12-02 19:20:51 +01:00
Antonio Scandurra
969c314315 Merge branch 'main' into reconnections-2 2022-12-02 19:09:33 +01:00
Antonio Scandurra
568de814aa Delete empty rooms 2022-12-02 16:56:41 +01:00
Antonio Scandurra
27f6ae945d Clear stale data on startup
This is a stopgap measure until we introduce reconnection support.
2022-12-02 16:30:00 +01:00
Antonio Scandurra
1b46b7a7d6 Move modules into collab library as opposed to using the binary
This ensures that we can use collab's modules from the seed script
as well.
2022-12-02 14:37:52 +01:00
Antonio Scandurra
7502558631 Make all tests pass again after migration to sea-orm 2022-12-02 14:22:36 +01:00
Antonio Scandurra
48b6ee313f Use i32 to represent Postgres INTEGER types in Rust 2022-12-02 13:58:54 +01:00
Antonio Scandurra
dec5f37e4e Finish porting remaining db methods to sea-orm 2022-12-02 13:58:23 +01:00
Julia
239a04ea5b Add test that should have exercised tab reordering while following
Except it doesn't, it passes both with and without the prior commit.
Investigate further
2022-12-02 00:31:16 -05:00
Joseph T. Lyons
ea03b48243 Merge pull request #1876 from zed-industries/update-release-urls-to-match-new-zed.dev-url-format
Update release urls to match new zed.dev url format
2022-12-01 20:32:14 -05:00
Antonio Scandurra
585ac3e1be WIP 2022-12-01 18:39:24 +01:00
Antonio Scandurra
29a4baf346 Replace i32 with u32 for database columns
We never expect to return signed integers and so we shouldn't use
a signed type. I think this was a limitation of sqlx.
2022-12-01 17:47:51 +01:00
Antonio Scandurra
cfdf0a57b8 Implement Database::update_project 2022-12-01 17:36:36 +01:00
Antonio Scandurra
944d6554de Implement Database::unshare_project 2022-12-01 16:26:13 +01:00
Antonio Scandurra
e3ac67784a Implement Database::project_guest_connection_ids 2022-12-01 16:23:29 +01:00
Antonio Scandurra
62624b81d8 Avoid using col_expr whenever possible
...and use the more type-safe `::set`.
2022-12-01 16:17:27 +01:00
Antonio Scandurra
256e3e8e0f Get basic calls working again with sea-orm 2022-12-01 16:17:24 +01:00
Antonio Scandurra
aebc6326a9 Implement Database::create_room 2022-12-01 15:22:20 +01:00
Antonio Scandurra
db1d93576f Go back to a compiling state, panicking on unimplemented db methods 2022-12-01 15:13:57 +01:00
Antonio Scandurra
d2385bd6a0 Start using the new sea-orm backed database 2022-12-01 14:41:59 +01:00
Antonio Scandurra
19d14737bf Implement signups using sea-orm 2022-12-01 11:58:07 +01:00
Antonio Scandurra
4f864a20a7 Implement invite codes using sea-orm 2022-12-01 11:10:51 +01:00
Antonio Scandurra
2375741bdf Implement db2::Database::fuzzy_search_users 2022-12-01 10:09:53 +01:00
Julia
46f1d5f5c2 Avoid moving tab when leader item updates 2022-12-01 00:29:58 -05:00
Max Brunsfeld
d70996bb99 collab 0.2.5 2022-11-30 14:10:10 -08:00
Julia
5a0c39cbed Merge pull request #1922 from zed-industries/dont-panic-clip-instead
Dont panic in point conversion, clip instead
2022-11-30 13:28:10 -05:00
Julia
41b2fde10d Style
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-11-30 13:11:08 -05:00
Julia
023ecd595b Change verify macro to debug panic
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-11-30 13:03:15 -05:00
Julia
2b979d3b88 Don't panic rope point conversions 2022-11-30 12:43:43 -05:00
Julia
5965113fc8 Add verify macros & use in one location for point conversion 2022-11-30 12:43:43 -05:00
Antonio Scandurra
4c04d512db Implement db2::Database::remove_contact 2022-11-30 17:39:17 +01:00
Antonio Scandurra
d1a44b889e Implement contacts using sea-orm
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-11-30 17:36:25 +01:00
Antonio Scandurra
04d553d4d3 Implement db2::Database::get_user_metrics_id 2022-11-30 15:06:04 +01:00
Antonio Scandurra
2e24d128db Implement access tokens using sea-orm 2022-11-30 14:47:03 +01:00
Antonio Scandurra
9e59056e7f Implement db2::Database::get_user_by_github_account 2022-11-30 14:18:46 +01:00
Antonio Scandurra
d9a892a423 Make some db tests pass against the new sea-orm implementation 2022-11-30 12:13:16 +01:00
Joseph T. Lyons
3a1cd6ed3a Merge pull request #1913 from zed-industries/Add-column-to-signups-for-added-to-mailing-list
Add "added_to_mailing_list" column on signups table
2022-11-29 19:30:11 -05:00
Joseph T. Lyons
9f9398476d Merge pull request #1920 from zed-industries/order-invites-by-creation-time
Order invites by creation time
2022-11-29 14:28:53 -05:00
Antonio Scandurra
b7294887c7 WIP: move to a non-generic test database struct
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
Co-Authored-By: Julia Risley <julia@zed.dev>
2022-11-29 19:20:11 +01:00
Joseph Lyons
049c0f8ba4 Order invites by creation time 2022-11-29 12:57:51 -05:00
Antonio Scandurra
11a39226e8 Start on a new db2 module that uses SeaORM 2022-11-29 16:49:04 +01:00
Antonio Scandurra
ac24600a40 Start moving towards using sea-query to construct queries 2022-11-29 13:55:08 +01:00
Antonio Scandurra
d525cfd697 Increase probability of creating new files in randomized test 2022-11-29 11:02:14 +01:00
Joseph Lyons
4436ec48eb Add "added_to_mailing_list" column on signups table 2022-11-29 02:13:13 -05:00
Joseph T. Lyons
5a9a0f9fa5 Merge pull request #1918 from zed-industries/remove-sign-in-telemetry-event
Remove sign in telemetry event
2022-11-29 01:59:33 -05:00
Joseph Lyons
d2cd9c94f7 Remove sign in telemetry event 2022-11-28 18:56:27 -05:00
Max Brunsfeld
3adc0b947f Merge pull request #1917 from zed-industries/integer-excerpt-ids
Use integers for excerpt ids, map them to locators internally
2022-11-28 14:27:35 -08:00
Max Brunsfeld
718f802157 Implement Copy for multibuffer anchors 2022-11-28 14:18:49 -08:00
Max Brunsfeld
f71145bb32 Add a layer of indirection between excerpt ids and locators 2022-11-28 14:18:49 -08:00
Antonio Scandurra
cd2a8579b9 Capture runnable backtraces only when detecting nondeterminism 2022-11-28 19:35:33 +01:00
Antonio Scandurra
d0709e7bfa Error if project is disconnected after getting completions response 2022-11-28 19:19:24 +01:00
Antonio Scandurra
fa3f100eff Introduce a new detect_nondeterminism = true attribute to gpui::test 2022-11-28 19:01:28 +01:00
Antonio Scandurra
f0a721032d Remove non-determinism caused by random entropy when reconnecting 2022-11-28 18:56:11 +01:00
Antonio Scandurra
0a565c6bae 💄 2022-11-28 17:44:18 +01:00
Antonio Scandurra
af2a2d2494 Return error when waiting on a worktree snapshot after disconnecting 2022-11-28 17:43:40 +01:00
Antonio Scandurra
cd0b663f62 Introduce per-room lock acquired before committing a transaction 2022-11-28 17:00:47 +01:00
Antonio Scandurra
2a0ddd99d2 Error if project is disconnected after getting code actions response 2022-11-28 15:05:34 +01:00
Antonio Scandurra
5581674f8f After completing LSP request, return an error if guest is disconnected 2022-11-28 14:39:27 +01:00
Antonio Scandurra
b0e1d6bc7f Fix integration test incorrectly assuming a certain ordering 2022-11-28 13:57:15 +01:00
Antonio Scandurra
ae11e4f798 Check the correct serialization failure code when retrying transaction 2022-11-28 13:56:03 +01:00
Max Brunsfeld
0b0fe91545 Merge pull request #1912 from zed-industries/matching-brackets-must-contain-range
Fix enclosing-bracket bug that appeared in JS for loops
2022-11-23 13:44:48 -08:00
Max Brunsfeld
aeea47323a Fix enclosing-bracket bug that appeared in JS for loops
Previously, we were relying on the tree-sitter query's range restriction to
avoid returning brackets that did not contain the given range. But the
query's range restriction only guarantees that we don't descend into parent
nodes unless they intersect the range.
2022-11-23 13:37:22 -08:00
Julia
e4185f38cf Merge pull request #1910 from zed-industries/lsp-coordinate-clamp
Don't trust LSP coordinates to be within document bounds
2022-11-23 14:07:37 -05:00
Julia
09e6d44873 Move Unclipped into separate file 2022-11-23 14:02:11 -05:00
Julia
525d84e5bf Remove spurious lifetimes
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-11-23 13:52:39 -05:00
Julia
55ca085d7d Consistency in prefix/suffix/signature of UTF16 point to point conversion
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-11-23 13:52:18 -05:00
Julia
03cfd23ac5 Bump protocol version back down as proto changes are non-breaking 2022-11-23 13:40:49 -05:00
Julia
a666ca3e40 Collapse proto Point into the one kind of use case, utf-16 coords
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-11-23 13:28:44 -05:00
Julia
b58ae8bdd7 Clip diagnostic range before and during empty range expansion
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-11-23 13:21:05 -05:00
Max Brunsfeld
5e7652698d v0.67.x dev 2022-11-23 09:56:06 -08:00
Julia
e51cbf67ab Fixup compile errors 2022-11-22 02:49:47 -05:00
Julia
8c75df30cb Wrap a bunch of traits for Unclipped<T> 2022-11-21 15:58:44 -05:00
Julia
1c84e77c37 Start adding concept of Unclipped text coordinates
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-11-21 15:48:25 -05:00
Julia
436c89650a Rename clamped -> clipped 2022-11-21 15:23:00 -05:00
Julia
4ead1ecbbf Simply logic of this method
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-11-21 14:25:01 -05:00
Julia
074e3cfbd6 Clamp UTF-16 to point conversions 2022-11-21 14:25:01 -05:00
Julia
bb32599ded Clamp for all UTF-16 to offset conversions which used to use ToOffset 2022-11-21 14:25:01 -05:00
Julia
f9cbed5a1f Clamp UTF-16 coordinate while performing LSP edits rather than panicing 2022-11-21 11:48:13 -05:00
Antonio Scandurra
4c1b4953c1 Remove version from Room
We won't need it once we add the per-room lock.
2022-11-18 20:18:48 +01:00
Antonio Scandurra
c3d556d9bd Don't take an Arc<Server> in message handlers 2022-11-18 11:45:42 +01:00
Antonio Scandurra
44bb2ce024 Rename Store to ConnectionPool 2022-11-17 19:03:59 +01:00
Antonio Scandurra
6c83be3f89 Remove obsolete code from Store 2022-11-17 18:46:39 +01:00
Antonio Scandurra
0a4517f97e WIP: Introduce a db field to Session
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-11-17 17:30:26 +01:00
Antonio Scandurra
c34a5f3177 Introduce a new Session struct to server message handlers
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-11-17 17:11:06 +01:00
Antonio Scandurra
4f39181c4c Revert "Don't replace newer diagnostics with older ones"
This reverts commit 71eeeedc05.
2022-11-17 16:57:40 +01:00
Antonio Scandurra
e7e45be6e1 Revert "Wait for previous UpdateFollowers message ack before sending new ones"
This reverts commit fe93263ad4.
2022-11-17 16:57:32 +01:00
Antonio Scandurra
8621c88a3c Use int8 for scan_id and inode in Postgres 2022-11-17 16:56:43 +01:00
Antonio Scandurra
7dae21cb36 🎨 2022-11-17 15:35:18 +01:00
Antonio Scandurra
0f4598a243 Fix seed script 2022-11-17 15:34:35 +01:00
Antonio Scandurra
6415809b61 Fix errors in Postgres schema 2022-11-17 15:34:12 +01:00
Antonio Scandurra
fe93263ad4 Wait for previous UpdateFollowers message ack before sending new ones 2022-11-17 14:12:00 +01:00
Antonio Scandurra
3b34d858b5 Remove unwrap from Server::share_project 2022-11-17 13:33:26 +01:00
Antonio Scandurra
71eeeedc05 Don't replace newer diagnostics with older ones 2022-11-17 12:21:51 +01:00
Antonio Scandurra
532a599239 Use Db::get_guest_connection_ids in other db methods 2022-11-17 11:38:00 +01:00
Nathan Sobo
9eee22ff0a Fix column name in query 2022-11-16 19:40:53 -07:00
Nathan Sobo
94fe93c6ee Move unshare_project to db module 2022-11-16 18:28:45 -07:00
Nathan Sobo
e5f05c9f3b Move leave_project from Store to db module 2022-11-16 17:45:47 -07:00
Nathan Sobo
bdb521cb6b Fix typo in query 2022-11-16 16:52:05 -07:00
Antonio Scandurra
c1291a093b WIP: Allow subscribing to remote entity before creating a model
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-11-16 19:51:24 +01:00
Antonio Scandurra
adf43c87dd Batch some of the new queries in Db
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-11-16 17:19:06 +01:00
Antonio Scandurra
faf265328e Wait for acknowledgment before sending the next diagnostic summary 2022-11-16 16:03:01 +01:00
Antonio Scandurra
9bc57c0c61 Move Store::start_language_server to Db 2022-11-16 15:48:26 +01:00
Antonio Scandurra
95369f92eb Move Store::update_diagnostic_summary to Db 2022-11-16 15:41:33 +01:00
Antonio Scandurra
117458f4f6 Send worktree updates after project metadata has been sent 2022-11-16 14:58:11 +01:00
Antonio Scandurra
eeb32fa888 Improve queries for composite primary keys 2022-11-16 11:07:39 +01:00
Antonio Scandurra
f9567ae116 Cascade deletes when project is deleted 2022-11-16 10:41:36 +01:00
Antonio Scandurra
c151c87e12 Correctly leave projects when leaving room 2022-11-16 10:36:48 +01:00
Antonio Scandurra
3190236396 Update worktree entry instead of erroring when it already exists 2022-11-16 08:57:19 +01:00
Antonio Scandurra
0817f905a2 Fix syntax error in schema 2022-11-15 18:02:07 +01:00
Antonio Scandurra
ad67f5e4de Always use the database to retrieve collaborators for a project 2022-11-15 17:49:37 +01:00
Antonio Scandurra
e9eadcaa6a Move Store::update_worktree to Db::update_worktree 2022-11-15 17:18:28 +01:00
Antonio Scandurra
4b1dcf2d55 Always use strings to represent paths over the wire
Previously, the protocol used a mix of strings and bytes without any consistency.

When we go to multiple platforms, we won't be able to mix encodings of paths anyway.
We don't know this is the right approach, but it at least makes things consistent
and easy to read in the database, on the wire, etc. Really, we should be using entry
ids etc to refer to entries on the wire anyway, but there's a chance this is the
wrong decision.

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-11-15 16:46:17 +01:00
Antonio Scandurra
974ef967a3 Move Store::join_project to Db::join_project
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-11-15 16:37:51 +01:00
Antonio Scandurra
be523617c9 Start reworking join_project to use the database 2022-11-15 11:44:26 +01:00
Antonio Scandurra
6cbf197226 Determine whether a contact is busy via the database 2022-11-15 10:41:21 +01:00
Antonio Scandurra
3e8fcb04f7 Finish implementing Db::update_project 2022-11-15 09:01:51 +01:00
Antonio Scandurra
42bb5f0e9f Add random delay after returning results from the database 2022-11-15 08:48:16 +01:00
Antonio Scandurra
b9af2ae66e Switch to serializable isolation
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-11-14 19:39:12 +01:00
Antonio Scandurra
d7369ace6a Skip applying room updates if they're older than the local room state 2022-11-14 15:35:39 +01:00
Antonio Scandurra
40073f6100 Wait for acknowledgment before sending the next project update 2022-11-14 15:32:49 +01:00
Antonio Scandurra
65c5adff05 Automatically decline call when user drops their last connection 2022-11-14 11:32:26 +01:00
Antonio Scandurra
59e8600e4c Implement Db::cancel_call 2022-11-14 11:12:23 +01:00
Antonio Scandurra
0310e27347 Fix query errors in Db::share_project 2022-11-14 10:53:11 +01:00
Antonio Scandurra
9902211af1 Leave room when connection is dropped 2022-11-14 10:13:36 +01:00
Joseph Lyons
1da5be6e8f Update release urls to match new zed.dev url format 2022-11-12 21:39:08 -05:00
Antonio Scandurra
2145965749 WIP 2022-11-11 19:36:20 +01:00
Antonio Scandurra
11caba4a4c Remove stray log statement 2022-11-11 18:54:08 +01:00
Antonio Scandurra
9f39dcf7cf Get basic calls test passing again 2022-11-11 18:53:23 +01:00
Antonio Scandurra
1135aeecb8 WIP: Move Store::leave_room to Db::leave_room 2022-11-11 16:59:54 +01:00
Antonio Scandurra
0d1d267213 Move Store::decline_call to Db::decline_call 2022-11-11 15:41:56 +01:00
Antonio Scandurra
c213c98ea4 Remove calls table and use just room_participants 2022-11-11 15:22:04 +01:00
Antonio Scandurra
cc58607c3b Move Store::join_room into Db::join_room 2022-11-11 14:43:40 +01:00
Antonio Scandurra
58947c5c72 Move incoming calls into Db 2022-11-11 14:28:26 +01:00
Antonio Scandurra
6871bbbc71 Start moving Store state into the database 2022-11-11 12:06:43 +01:00
Antonio Scandurra
28aa1567ce Include sender_user_id when handling a server message/request 2022-11-11 11:45:58 +01:00
Antonio Scandurra
f639c4c3d1 Add schema for reconnection support 2022-11-11 10:41:44 +01:00
158 changed files with 14450 additions and 6937 deletions

1
.gitignore vendored
View File

@@ -18,3 +18,4 @@ DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
**/*.db

1161
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -45,6 +45,8 @@ members = [
"crates/search",
"crates/settings",
"crates/snippet",
"crates/sqlez",
"crates/sqlez_macros",
"crates/sum_tree",
"crates/terminal",
"crates/text",
@@ -81,3 +83,4 @@ split-debuginfo = "unpacked"
[profile.release]
debug = true

View File

@@ -11,7 +11,7 @@ use settings::Settings;
use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc};
use util::ResultExt;
use workspace::{ItemHandle, StatusItemView, Workspace};
use workspace::{item::ItemHandle, StatusItemView, Workspace};
actions!(lsp_status, [ShowErrorMessage]);

View File

@@ -8,6 +8,7 @@ path = "src/auto_update.rs"
doctest = false
[dependencies]
db = { path = "../db" }
client = { path = "../client" }
gpui = { path = "../gpui" }
menu = { path = "../menu" }

View File

@@ -1,17 +1,18 @@
mod update_notification;
use anyhow::{anyhow, Context, Result};
use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN};
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 settings::ReleaseChannel;
use smol::{fs::File, io::AsyncReadExt, process::Command};
use std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration};
use update_notification::UpdateNotification;
use util::channel::ReleaseChannel;
use workspace::Workspace;
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
@@ -41,7 +42,6 @@ pub struct AutoUpdater {
current_version: AppVersion,
http_client: Arc<dyn HttpClient>,
pending_poll: Option<Task<()>>,
db: project::Db,
server_url: String,
}
@@ -55,11 +55,11 @@ impl Entity for AutoUpdater {
type Event = ();
}
pub fn init(db: project::Db, http_client: Arc<dyn HttpClient>, cx: &mut MutableAppContext) {
pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut MutableAppContext) {
if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) {
let server_url = ZED_SERVER_URL.to_string();
let server_url = server_url;
let auto_updater = cx.add_model(|cx| {
let updater = AutoUpdater::new(version, db.clone(), http_client, server_url.clone());
let updater = AutoUpdater::new(version, http_client, server_url.clone());
updater.start_polling(cx).detach();
updater
});
@@ -120,14 +120,12 @@ impl AutoUpdater {
fn new(
current_version: AppVersion,
db: project::Db,
http_client: Arc<dyn HttpClient>,
server_url: String,
) -> Self {
Self {
status: AutoUpdateStatus::Idle,
current_version,
db,
http_client,
server_url,
pending_poll: None,
@@ -297,20 +295,28 @@ impl AutoUpdater {
should_show: bool,
cx: &AppContext,
) -> Task<Result<()>> {
let db = self.db.clone();
cx.background().spawn(async move {
if should_show {
db.write_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY, "")?;
KEY_VALUE_STORE
.write_kvp(
SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(),
"".to_string(),
)
.await?;
} else {
db.delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?;
KEY_VALUE_STORE
.delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string())
.await?;
}
Ok(())
})
}
fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
let db = self.db.clone();
cx.background()
.spawn(async move { Ok(db.read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?.is_some()) })
cx.background().spawn(async move {
Ok(KEY_VALUE_STORE
.read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
.is_some())
})
}
}

View File

@@ -5,8 +5,9 @@ use gpui::{
Element, Entity, MouseButton, View, ViewContext,
};
use menu::Cancel;
use settings::{ReleaseChannel, Settings};
use workspace::Notification;
use settings::Settings;
use util::channel::ReleaseChannel;
use workspace::notifications::Notification;
pub struct UpdateNotification {
version: AppVersion,
@@ -27,9 +28,9 @@ impl View for UpdateNotification {
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
let theme = cx.global::<Settings>().theme.clone();
let theme = &theme.update_notification;
let theme = &theme.simple_message_notification;
let app_name = cx.global::<ReleaseChannel>().name();
let app_name = cx.global::<ReleaseChannel>().display_name();
MouseEventHandler::<ViewReleaseNotes>::new(0, cx, |state, cx| {
Flex::column()

View File

@@ -4,7 +4,10 @@ use gpui::{
use itertools::Itertools;
use search::ProjectSearchView;
use settings::Settings;
use workspace::{ItemEvent, ItemHandle, ToolbarItemLocation, ToolbarItemView};
use workspace::{
item::{ItemEvent, ItemHandle},
ToolbarItemLocation, ToolbarItemView,
};
pub enum Event {
UpdateLocation,

View File

@@ -22,7 +22,7 @@ pub fn init(client: Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut Mu
#[derive(Clone)]
pub struct IncomingCall {
pub room_id: u64,
pub caller: Arc<User>,
pub calling_user: Arc<User>,
pub participants: Vec<Arc<User>>,
pub initial_project: Option<proto::ParticipantProject>,
}
@@ -78,9 +78,9 @@ impl ActiveCall {
user_store.get_users(envelope.payload.participant_user_ids, cx)
})
.await?,
caller: user_store
calling_user: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_user(envelope.payload.caller_user_id, cx)
user_store.get_user(envelope.payload.calling_user_id, cx)
})
.await?,
initial_project: envelope.payload.initial_project,
@@ -110,13 +110,13 @@ impl ActiveCall {
pub fn invite(
&mut self,
recipient_user_id: u64,
called_user_id: u64,
initial_project: Option<ModelHandle<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
if !self.pending_invites.insert(recipient_user_id) {
if !self.pending_invites.insert(called_user_id) {
return Task::ready(Err(anyhow!("user was already invited")));
}
@@ -136,13 +136,13 @@ impl ActiveCall {
};
room.update(&mut cx, |room, cx| {
room.call(recipient_user_id, initial_project_id, cx)
room.call(called_user_id, initial_project_id, cx)
})
.await?;
} else {
let room = cx
.update(|cx| {
Room::create(recipient_user_id, initial_project, client, user_store, cx)
Room::create(called_user_id, initial_project, client, user_store, cx)
})
.await?;
@@ -155,7 +155,7 @@ impl ActiveCall {
let result = invite.await;
this.update(&mut cx, |this, cx| {
this.pending_invites.remove(&recipient_user_id);
this.pending_invites.remove(&called_user_id);
cx.notify();
});
result
@@ -164,7 +164,7 @@ impl ActiveCall {
pub fn cancel_invite(
&mut self,
recipient_user_id: u64,
called_user_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let room_id = if let Some(room) = self.room() {
@@ -178,7 +178,7 @@ impl ActiveCall {
client
.request(proto::CancelCall {
room_id,
recipient_user_id,
called_user_id,
})
.await?;
anyhow::Ok(())

View File

@@ -10,7 +10,7 @@ use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext
use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate};
use postage::stream::Stream;
use project::Project;
use std::{mem, os::unix::prelude::OsStrExt, sync::Arc};
use std::{mem, sync::Arc};
use util::{post_inc, ResultExt};
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -53,7 +53,7 @@ impl Entity for Room {
fn release(&mut self, _: &mut MutableAppContext) {
if self.status.is_online() {
self.client.send(proto::LeaveRoom { id: self.id }).log_err();
self.client.send(proto::LeaveRoom {}).log_err();
}
}
}
@@ -149,7 +149,7 @@ impl Room {
}
pub(crate) fn create(
recipient_user_id: u64,
called_user_id: u64,
initial_project: Option<ModelHandle<Project>>,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
@@ -182,7 +182,7 @@ impl Room {
match room
.update(&mut cx, |room, cx| {
room.leave_when_empty = true;
room.call(recipient_user_id, initial_project_id, cx)
room.call(called_user_id, initial_project_id, cx)
})
.await
{
@@ -241,7 +241,7 @@ impl Room {
self.participant_user_ids.clear();
self.subscriptions.clear();
self.live_kit.take();
self.client.send(proto::LeaveRoom { id: self.id })?;
self.client.send(proto::LeaveRoom {})?;
Ok(())
}
@@ -294,6 +294,11 @@ impl Room {
.position(|participant| Some(participant.user_id) == self.client.user_id());
let local_participant = local_participant_ix.map(|ix| room.participants.swap_remove(ix));
let pending_participant_user_ids = room
.pending_participants
.iter()
.map(|p| p.user_id)
.collect::<Vec<_>>();
let remote_participant_user_ids = room
.participants
.iter()
@@ -303,7 +308,7 @@ impl Room {
self.user_store.update(cx, move |user_store, cx| {
(
user_store.get_users(remote_participant_user_ids, cx),
user_store.get_users(room.pending_participant_user_ids, cx),
user_store.get_users(pending_participant_user_ids, cx),
)
});
self.pending_room_update = Some(cx.spawn(|this, mut cx| async move {
@@ -487,7 +492,7 @@ impl Room {
pub(crate) fn call(
&mut self,
recipient_user_id: u64,
called_user_id: u64,
initial_project_id: Option<u64>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
@@ -503,7 +508,7 @@ impl Room {
let result = client
.request(proto::Call {
room_id,
recipient_user_id,
called_user_id,
initial_project_id,
})
.await;
@@ -538,7 +543,7 @@ impl Room {
id: worktree.id().to_proto(),
root_name: worktree.root_name().into(),
visible: worktree.is_visible(),
abs_path: worktree.abs_path().as_os_str().as_bytes().to_vec(),
abs_path: worktree.abs_path().to_string_lossy().into(),
}
})
.collect(),

View File

@@ -11,14 +11,12 @@ use async_tungstenite::tungstenite::{
error::Error as WebsocketError,
http::{Request, StatusCode},
};
use db::Db;
use futures::{future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryStreamExt};
use gpui::{
actions,
serde_json::{self, Value},
AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext,
AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext,
ViewHandle,
AsyncAppContext, Entity, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
};
use http::HttpClient;
use lazy_static::lazy_static;
@@ -27,13 +25,13 @@ use postage::watch;
use rand::prelude::*;
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage};
use serde::Deserialize;
use settings::ReleaseChannel;
use std::{
any::TypeId,
collections::HashMap,
convert::TryFrom,
fmt::Write as _,
future::Future,
marker::PhantomData,
path::PathBuf,
sync::{Arc, Weak},
time::{Duration, Instant},
@@ -41,6 +39,7 @@ use std::{
use telemetry::Telemetry;
use thiserror::Error;
use url::Url;
use util::channel::ReleaseChannel;
use util::{ResultExt, TryFutureExt};
pub use rpc::*;
@@ -172,7 +171,7 @@ struct ClientState {
entity_id_extractors: HashMap<TypeId, fn(&dyn AnyTypedEnvelope) -> u64>,
_reconnect_task: Option<Task<()>>,
reconnect_interval: Duration,
entities_by_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakEntityHandle>,
entities_by_type_and_remote_id: HashMap<(TypeId, u64), WeakSubscriber>,
models_by_message_type: HashMap<TypeId, AnyWeakModelHandle>,
entity_types_by_message_type: HashMap<TypeId, TypeId>,
#[allow(clippy::type_complexity)]
@@ -182,7 +181,7 @@ struct ClientState {
dyn Send
+ Sync
+ Fn(
AnyEntityHandle,
Subscriber,
Box<dyn AnyTypedEnvelope>,
&Arc<Client>,
AsyncAppContext,
@@ -191,12 +190,13 @@ struct ClientState {
>,
}
enum AnyWeakEntityHandle {
enum WeakSubscriber {
Model(AnyWeakModelHandle),
View(AnyWeakViewHandle),
Pending(Vec<Box<dyn AnyTypedEnvelope>>),
}
enum AnyEntityHandle {
enum Subscriber {
Model(AnyModelHandle),
View(AnyViewHandle),
}
@@ -254,6 +254,54 @@ impl Drop for Subscription {
}
}
pub struct PendingEntitySubscription<T: Entity> {
client: Arc<Client>,
remote_id: u64,
_entity_type: PhantomData<T>,
consumed: bool,
}
impl<T: Entity> PendingEntitySubscription<T> {
pub fn set_model(mut self, model: &ModelHandle<T>, cx: &mut AsyncAppContext) -> Subscription {
self.consumed = true;
let mut state = self.client.state.write();
let id = (TypeId::of::<T>(), self.remote_id);
let Some(WeakSubscriber::Pending(messages)) =
state.entities_by_type_and_remote_id.remove(&id)
else {
unreachable!()
};
state
.entities_by_type_and_remote_id
.insert(id, WeakSubscriber::Model(model.downgrade().into()));
drop(state);
for message in messages {
self.client.handle_message(message, cx);
}
Subscription::Entity {
client: Arc::downgrade(&self.client),
id,
}
}
}
impl<T: Entity> Drop for PendingEntitySubscription<T> {
fn drop(&mut self) {
if !self.consumed {
let mut state = self.client.state.write();
if let Some(WeakSubscriber::Pending(messages)) = state
.entities_by_type_and_remote_id
.remove(&(TypeId::of::<T>(), self.remote_id))
{
for message in messages {
log::info!("unhandled message {}", message.payload_type_name());
}
}
}
}
}
impl Client {
pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
Arc::new(Self {
@@ -349,7 +397,11 @@ impl Client {
let this = self.clone();
let reconnect_interval = state.reconnect_interval;
state._reconnect_task = Some(cx.spawn(|cx| async move {
#[cfg(any(test, feature = "test-support"))]
let mut rng = StdRng::seed_from_u64(0);
#[cfg(not(any(test, feature = "test-support")))]
let mut rng = StdRng::from_entropy();
let mut delay = INITIAL_RECONNECTION_DELAY;
while let Err(error) = this.authenticate_and_connect(true, &cx).await {
log::error!("failed to connect {}", error);
@@ -387,26 +439,28 @@ impl Client {
self.state
.write()
.entities_by_type_and_remote_id
.insert(id, AnyWeakEntityHandle::View(cx.weak_handle().into()));
.insert(id, WeakSubscriber::View(cx.weak_handle().into()));
Subscription::Entity {
client: Arc::downgrade(self),
id,
}
}
pub fn add_model_for_remote_entity<T: Entity>(
pub fn subscribe_to_entity<T: Entity>(
self: &Arc<Self>,
remote_id: u64,
cx: &mut ModelContext<T>,
) -> Subscription {
) -> PendingEntitySubscription<T> {
let id = (TypeId::of::<T>(), remote_id);
self.state
.write()
.entities_by_type_and_remote_id
.insert(id, AnyWeakEntityHandle::Model(cx.weak_handle().into()));
Subscription::Entity {
client: Arc::downgrade(self),
id,
.insert(id, WeakSubscriber::Pending(Default::default()));
PendingEntitySubscription {
client: self.clone(),
remote_id,
consumed: false,
_entity_type: PhantomData,
}
}
@@ -434,7 +488,7 @@ impl Client {
let prev_handler = state.message_handlers.insert(
message_type_id,
Arc::new(move |handle, envelope, client, cx| {
let handle = if let AnyEntityHandle::Model(handle) = handle {
let handle = if let Subscriber::Model(handle) = handle {
handle
} else {
unreachable!();
@@ -488,7 +542,7 @@ impl Client {
F: 'static + Future<Output = Result<()>>,
{
self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
if let AnyEntityHandle::View(handle) = handle {
if let Subscriber::View(handle) = handle {
handler(handle.downcast::<E>().unwrap(), message, client, cx)
} else {
unreachable!();
@@ -507,7 +561,7 @@ impl Client {
F: 'static + Future<Output = Result<()>>,
{
self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
if let AnyEntityHandle::Model(handle) = handle {
if let Subscriber::Model(handle) = handle {
handler(handle.downcast::<E>().unwrap(), message, client, cx)
} else {
unreachable!();
@@ -522,7 +576,7 @@ impl Client {
H: 'static
+ Send
+ Sync
+ Fn(AnyEntityHandle, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
+ Fn(Subscriber, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<()>>,
{
let model_type_id = TypeId::of::<E>();
@@ -784,94 +838,8 @@ impl Client {
let cx = cx.clone();
let this = self.clone();
async move {
let mut message_id = 0_usize;
while let Some(message) = incoming.next().await {
let mut state = this.state.write();
message_id += 1;
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 model = state
.models_by_message_type
.get(&payload_type_id)
.and_then(|model| model.upgrade(&cx))
.map(AnyEntityHandle::Model)
.or_else(|| {
let entity_type_id =
*state.entity_types_by_message_type.get(&payload_type_id)?;
let entity_id = state
.entity_id_extractors
.get(&message.payload_type_id())
.map(|extract_entity_id| {
(extract_entity_id)(message.as_ref())
})?;
let entity = state
.entities_by_type_and_remote_id
.get(&(entity_type_id, entity_id))?;
if let Some(entity) = entity.upgrade(&cx) {
Some(entity)
} else {
state
.entities_by_type_and_remote_id
.remove(&(entity_type_id, entity_id));
None
}
});
let model = if let Some(model) = model {
model
} else {
log::info!("unhandled message {}", type_name);
continue;
};
let handler = state.message_handlers.get(&payload_type_id).cloned();
// Dropping the state prevents deadlocks if the handler interacts with rpc::Client.
// It also ensures we don't hold the lock while yielding back to the executor, as
// that might cause the executor thread driving this future to block indefinitely.
drop(state);
if let Some(handler) = handler {
let future = handler(model, message, &this, cx.clone());
let client_id = this.id;
log::debug!(
"rpc message received. client_id:{}, message_id:{}, sender_id:{:?}, type:{}",
client_id,
message_id,
sender_id,
type_name
);
cx.foreground()
.spawn(async move {
match future.await {
Ok(()) => {
log::debug!(
"rpc message handled. client_id:{}, message_id:{}, sender_id:{:?}, type:{}",
client_id,
message_id,
sender_id,
type_name
);
}
Err(error) => {
log::error!(
"error handling message. client_id:{}, message_id:{}, sender_id:{:?}, type:{}, error:{:?}",
client_id,
message_id,
sender_id,
type_name,
error
);
}
}
})
.detach();
} else {
log::info!("unhandled message {}", type_name);
}
this.handle_message(message, &cx);
// Don't starve the main thread when receiving lots of messages at once.
smol::future::yield_now().await;
}
@@ -1218,8 +1186,99 @@ impl Client {
self.peer.respond_with_error(receipt, error)
}
pub fn start_telemetry(&self, db: Db) {
self.telemetry.start(db.clone());
fn handle_message(
self: &Arc<Client>,
message: Box<dyn AnyTypedEnvelope>,
cx: &AsyncAppContext,
) {
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 mut subscriber = None;
if let Some(message_model) = state
.models_by_message_type
.get(&payload_type_id)
.and_then(|model| model.upgrade(cx))
{
subscriber = Some(Subscriber::Model(message_model));
} else if let Some((extract_entity_id, entity_type_id)) =
state.entity_id_extractors.get(&payload_type_id).zip(
state
.entity_types_by_message_type
.get(&payload_type_id)
.copied(),
)
{
let entity_id = (extract_entity_id)(message.as_ref());
match state
.entities_by_type_and_remote_id
.get_mut(&(entity_type_id, entity_id))
{
Some(WeakSubscriber::Pending(pending)) => {
pending.push(message);
return;
}
Some(weak_subscriber @ _) => subscriber = weak_subscriber.upgrade(cx),
_ => {}
}
}
let subscriber = if let Some(subscriber) = subscriber {
subscriber
} else {
log::info!("unhandled message {}", type_name);
return;
};
let handler = state.message_handlers.get(&payload_type_id).cloned();
// Dropping the state prevents deadlocks if the handler interacts with rpc::Client.
// It also ensures we don't hold the lock while yielding back to the executor, as
// that might cause the executor thread driving this future to block indefinitely.
drop(state);
if let Some(handler) = handler {
let future = handler(subscriber, message, &self, cx.clone());
let client_id = self.id;
log::debug!(
"rpc message received. client_id:{}, sender_id:{:?}, type:{}",
client_id,
sender_id,
type_name
);
cx.foreground()
.spawn(async move {
match future.await {
Ok(()) => {
log::debug!(
"rpc message handled. client_id:{}, sender_id:{:?}, type:{}",
client_id,
sender_id,
type_name
);
}
Err(error) => {
log::error!(
"error handling message. client_id:{}, sender_id:{:?}, type:{}, error:{:?}",
client_id,
sender_id,
type_name,
error
);
}
}
})
.detach();
} else {
log::info!("unhandled message {}", type_name);
}
}
pub fn start_telemetry(&self) {
self.telemetry.start();
}
pub fn report_event(&self, kind: &str, properties: Value) {
@@ -1231,11 +1290,12 @@ impl Client {
}
}
impl AnyWeakEntityHandle {
fn upgrade(&self, cx: &AsyncAppContext) -> Option<AnyEntityHandle> {
impl WeakSubscriber {
fn upgrade(&self, cx: &AsyncAppContext) -> Option<Subscriber> {
match self {
AnyWeakEntityHandle::Model(handle) => handle.upgrade(cx).map(AnyEntityHandle::Model),
AnyWeakEntityHandle::View(handle) => handle.upgrade(cx).map(AnyEntityHandle::View),
WeakSubscriber::Model(handle) => handle.upgrade(cx).map(Subscriber::Model),
WeakSubscriber::View(handle) => handle.upgrade(cx).map(Subscriber::View),
WeakSubscriber::Pending(_) => None,
}
}
}
@@ -1480,11 +1540,17 @@ mod tests {
subscription: None,
});
let _subscription1 = model1.update(cx, |_, cx| client.add_model_for_remote_entity(1, cx));
let _subscription2 = model2.update(cx, |_, cx| client.add_model_for_remote_entity(2, cx));
let _subscription1 = client
.subscribe_to_entity(1)
.set_model(&model1, &mut cx.to_async());
let _subscription2 = client
.subscribe_to_entity(2)
.set_model(&model2, &mut cx.to_async());
// Ensure dropping a subscription for the same entity type still allows receiving of
// messages for other entity IDs of the same type.
let subscription3 = model3.update(cx, |_, cx| client.add_model_for_remote_entity(3, cx));
let subscription3 = client
.subscribe_to_entity(3)
.set_model(&model3, &mut cx.to_async());
drop(subscription3);
server.send(proto::JoinProject { project_id: 1 });

View File

@@ -1,5 +1,5 @@
use crate::http::HttpClient;
use db::Db;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
executor::Background,
serde_json::{self, value::Map, Value},
@@ -10,7 +10,6 @@ use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Serialize;
use serde_json::json;
use settings::ReleaseChannel;
use std::{
io::Write,
mem,
@@ -19,7 +18,7 @@ use std::{
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tempfile::NamedTempFile;
use util::{post_inc, ResultExt, TryFutureExt};
use util::{channel::ReleaseChannel, post_inc, ResultExt, TryFutureExt};
use uuid::Uuid;
pub struct Telemetry {
@@ -107,7 +106,7 @@ impl Telemetry {
pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
let platform = cx.platform();
let release_channel = if cx.has_global::<ReleaseChannel>() {
Some(cx.global::<ReleaseChannel>().name())
Some(cx.global::<ReleaseChannel>().display_name())
} else {
None
};
@@ -148,18 +147,21 @@ impl Telemetry {
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
}
pub fn start(self: &Arc<Self>, db: Db) {
pub fn start(self: &Arc<Self>) {
let this = self.clone();
self.executor
.spawn(
async move {
let device_id = if let Ok(Some(device_id)) = db.read_kvp("device_id") {
device_id
} else {
let device_id = Uuid::new_v4().to_string();
db.write_kvp("device_id", &device_id)?;
device_id
};
let device_id =
if let Ok(Some(device_id)) = KEY_VALUE_STORE.read_kvp("device_id") {
device_id
} else {
let device_id = Uuid::new_v4().to_string();
KEY_VALUE_STORE
.write_kvp("device_id".to_string(), device_id.clone())
.await?;
device_id
};
let device_id: Arc<str> = device_id.into();
let mut state = this.state.lock();

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
version = "0.2.4"
version = "0.3.0"
[[bin]]
name = "collab"
@@ -19,12 +19,12 @@ rpc = { path = "../rpc" }
util = { path = "../util" }
anyhow = "1.0.40"
async-trait = "0.1.50"
async-tungstenite = "0.16"
axum = { version = "0.5", features = ["json", "headers", "ws"] }
axum-extra = { version = "0.3", features = ["erased-json"] }
base64 = "0.13"
clap = { version = "3.1", features = ["derive"], optional = true }
dashmap = "5.4"
envy = "0.4.2"
futures = "0.3"
hyper = "0.14"
@@ -36,9 +36,13 @@ prometheus = "0.13"
rand = "0.8"
reqwest = { version = "0.11", features = ["json"], optional = true }
scrypt = "0.7"
# Remove fork dependency when a version with https://github.com/SeaQL/sea-orm/pull/1283 is released.
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls"] }
sea-query = "0.27"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
sha-1 = "0.9"
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
time = { version = "0.3", features = ["serde", "serde-well-known"] }
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = "0.17"
@@ -49,11 +53,6 @@ tracing = "0.1.34"
tracing-log = "0.1.3"
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
[dependencies.sqlx]
git = "https://github.com/launchbadge/sqlx"
rev = "4b7053807c705df312bcb9b6281e184bf7534eb3"
features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid"]
[dev-dependencies]
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
@@ -65,6 +64,7 @@ fs = { path = "../fs", features = ["test-support"] }
git = { path = "../git", features = ["test-support"] }
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
pretty_assertions = "1.3.0"
project = { path = "../project", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
@@ -76,13 +76,10 @@ env_logger = "0.9"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
util = { path = "../util" }
lazy_static = "1.4"
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-sqlite"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
sqlx = { version = "0.6", features = ["sqlite"] }
unindent = "0.1"
[dev-dependencies.sqlx]
git = "https://github.com/launchbadge/sqlx"
rev = "4b7053807c705df312bcb9b6281e184bf7534eb3"
features = ["sqlite"]
[features]
seed-support = ["clap", "lipsum", "reqwest"]

View File

@@ -1,4 +1,4 @@
CREATE TABLE IF NOT EXISTS "users" (
CREATE TABLE "users" (
"id" INTEGER PRIMARY KEY,
"github_login" VARCHAR,
"admin" BOOLEAN,
@@ -8,7 +8,7 @@ CREATE TABLE IF NOT EXISTS "users" (
"inviter_id" INTEGER REFERENCES users (id),
"connected_once" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP NOT NULL DEFAULT now,
"metrics_id" VARCHAR(255),
"metrics_id" TEXT,
"github_user_id" INTEGER
);
CREATE UNIQUE INDEX "index_users_github_login" ON "users" ("github_login");
@@ -16,14 +16,14 @@ CREATE UNIQUE INDEX "index_invite_code_users" ON "users" ("invite_code");
CREATE INDEX "index_users_on_email_address" ON "users" ("email_address");
CREATE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id");
CREATE TABLE IF NOT EXISTS "access_tokens" (
CREATE TABLE "access_tokens" (
"id" INTEGER PRIMARY KEY,
"user_id" INTEGER REFERENCES users (id),
"hash" VARCHAR(128)
);
CREATE INDEX "index_access_tokens_user_id" ON "access_tokens" ("user_id");
CREATE TABLE IF NOT EXISTS "contacts" (
CREATE TABLE "contacts" (
"id" INTEGER PRIMARY KEY,
"user_id_a" INTEGER REFERENCES users (id) NOT NULL,
"user_id_b" INTEGER REFERENCES users (id) NOT NULL,
@@ -34,8 +34,96 @@ CREATE TABLE IF NOT EXISTS "contacts" (
CREATE UNIQUE INDEX "index_contacts_user_ids" ON "contacts" ("user_id_a", "user_id_b");
CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b");
CREATE TABLE IF NOT EXISTS "projects" (
CREATE TABLE "rooms" (
"id" INTEGER PRIMARY KEY,
"host_user_id" INTEGER REFERENCES users (id) NOT NULL,
"unregistered" BOOLEAN NOT NULL DEFAULT false
"live_kit_room" VARCHAR NOT NULL
);
CREATE TABLE "projects" (
"id" INTEGER PRIMARY KEY,
"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
);
CREATE INDEX "index_projects_on_host_connection_epoch" ON "projects" ("host_connection_epoch");
CREATE TABLE "worktrees" (
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
"id" INTEGER NOT NULL,
"root_name" VARCHAR NOT NULL,
"abs_path" VARCHAR NOT NULL,
"visible" BOOL NOT NULL,
"scan_id" INTEGER NOT NULL,
"is_complete" BOOL NOT NULL,
PRIMARY KEY(project_id, id)
);
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,
"id" INTEGER NOT NULL,
"is_dir" BOOL NOT NULL,
"path" VARCHAR NOT NULL,
"inode" INTEGER NOT NULL,
"mtime_seconds" INTEGER NOT NULL,
"mtime_nanos" INTEGER NOT NULL,
"is_symlink" BOOL NOT NULL,
"is_ignored" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, id),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_entries_on_project_id" ON "worktree_entries" ("project_id");
CREATE INDEX "index_worktree_entries_on_project_id_and_worktree_id" ON "worktree_entries" ("project_id", "worktree_id");
CREATE TABLE "worktree_diagnostic_summaries" (
"project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL,
"path" VARCHAR NOT NULL,
"language_server_id" INTEGER NOT NULL,
"error_count" INTEGER NOT NULL,
"warning_count" INTEGER NOT NULL,
PRIMARY KEY(project_id, worktree_id, path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_diagnostic_summaries_on_project_id" ON "worktree_diagnostic_summaries" ("project_id");
CREATE INDEX "index_worktree_diagnostic_summaries_on_project_id_and_worktree_id" ON "worktree_diagnostic_summaries" ("project_id", "worktree_id");
CREATE TABLE "language_servers" (
"id" INTEGER NOT NULL,
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
"name" VARCHAR NOT NULL,
PRIMARY KEY(project_id, id)
);
CREATE INDEX "index_language_servers_on_project_id" ON "language_servers" ("project_id");
CREATE TABLE "project_collaborators" (
"id" INTEGER PRIMARY KEY,
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
"connection_id" INTEGER NOT NULL,
"connection_epoch" TEXT NOT NULL,
"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 TABLE "room_participants" (
"id" INTEGER PRIMARY KEY,
"room_id" INTEGER NOT NULL REFERENCES rooms (id),
"user_id" INTEGER NOT NULL REFERENCES users (id),
"answering_connection_id" INTEGER,
"answering_connection_epoch" TEXT,
"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
);
CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_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");

View File

@@ -0,0 +1,91 @@
CREATE TABLE IF NOT EXISTS "rooms" (
"id" SERIAL PRIMARY KEY,
"live_kit_room" VARCHAR NOT NULL
);
ALTER TABLE "projects"
ADD "room_id" INTEGER REFERENCES rooms (id),
ADD "host_connection_id" INTEGER,
ADD "host_connection_epoch" UUID,
DROP COLUMN "unregistered";
CREATE INDEX "index_projects_on_host_connection_epoch" ON "projects" ("host_connection_epoch");
CREATE TABLE "worktrees" (
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
"id" INT8 NOT NULL,
"root_name" VARCHAR NOT NULL,
"abs_path" VARCHAR NOT NULL,
"visible" BOOL NOT NULL,
"scan_id" INT8 NOT NULL,
"is_complete" BOOL NOT NULL,
PRIMARY KEY(project_id, id)
);
CREATE INDEX "index_worktrees_on_project_id" ON "worktrees" ("project_id");
CREATE TABLE "worktree_entries" (
"project_id" INTEGER NOT NULL,
"worktree_id" INT8 NOT NULL,
"id" INT8 NOT NULL,
"is_dir" BOOL NOT NULL,
"path" VARCHAR NOT NULL,
"inode" INT8 NOT NULL,
"mtime_seconds" INT8 NOT NULL,
"mtime_nanos" INTEGER NOT NULL,
"is_symlink" BOOL NOT NULL,
"is_ignored" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, id),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_entries_on_project_id" ON "worktree_entries" ("project_id");
CREATE INDEX "index_worktree_entries_on_project_id_and_worktree_id" ON "worktree_entries" ("project_id", "worktree_id");
CREATE TABLE "worktree_diagnostic_summaries" (
"project_id" INTEGER NOT NULL,
"worktree_id" INT8 NOT NULL,
"path" VARCHAR NOT NULL,
"language_server_id" INT8 NOT NULL,
"error_count" INTEGER NOT NULL,
"warning_count" INTEGER NOT NULL,
PRIMARY KEY(project_id, worktree_id, path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_diagnostic_summaries_on_project_id" ON "worktree_diagnostic_summaries" ("project_id");
CREATE INDEX "index_worktree_diagnostic_summaries_on_project_id_and_worktree_id" ON "worktree_diagnostic_summaries" ("project_id", "worktree_id");
CREATE TABLE "language_servers" (
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
"id" INT8 NOT NULL,
"name" VARCHAR NOT NULL,
PRIMARY KEY(project_id, id)
);
CREATE INDEX "index_language_servers_on_project_id" ON "language_servers" ("project_id");
CREATE TABLE "project_collaborators" (
"id" SERIAL PRIMARY KEY,
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
"connection_id" INTEGER NOT NULL,
"connection_epoch" UUID NOT NULL,
"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 TABLE "room_participants" (
"id" SERIAL PRIMARY KEY,
"room_id" INTEGER NOT NULL REFERENCES rooms (id),
"user_id" INTEGER NOT NULL REFERENCES users (id),
"answering_connection_id" INTEGER,
"answering_connection_epoch" UUID,
"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" UUID NOT NULL
);
CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_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");

View File

@@ -0,0 +1,2 @@
ALTER TABLE "signups"
ADD "added_to_mailing_list" BOOLEAN NOT NULL DEFAULT FALSE;

View File

@@ -1,6 +1,6 @@
use crate::{
auth,
db::{Invite, NewUserParams, Signup, User, UserId, WaitlistSummary},
db::{Invite, NewSignup, NewUserParams, User, UserId, WaitlistSummary},
rpc::{self, ResultExt},
AppState, Error, Result,
};
@@ -204,7 +204,7 @@ async fn create_user(
#[derive(Deserialize)]
struct UpdateUserParams {
admin: Option<bool>,
invite_count: Option<u32>,
invite_count: Option<i32>,
}
async fn update_user(
@@ -335,7 +335,7 @@ async fn get_user_for_invite_code(
}
async fn create_signup(
Json(params): Json<Signup>,
Json(params): Json<NewSignup>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<()> {
app.db.create_signup(&params).await?;

View File

@@ -75,7 +75,7 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
const MAX_ACCESS_TOKENS_TO_STORE: usize = 8;
pub async fn create_access_token(db: &db::DefaultDb, user_id: UserId) -> Result<String> {
pub async fn create_access_token(db: &db::Database, user_id: UserId) -> Result<String> {
let access_token = rpc::auth::random_token();
let access_token_hash =
hash_access_token(&access_token).context("failed to hash access token")?;

View File

@@ -1,12 +1,8 @@
use collab::{Error, Result};
use db::DefaultDb;
use collab::db;
use db::{ConnectOptions, Database};
use serde::{de::DeserializeOwned, Deserialize};
use std::fmt::Write;
#[allow(unused)]
#[path = "../db.rs"]
mod db;
#[derive(Debug, Deserialize)]
struct GitHubUser {
id: i32,
@@ -17,7 +13,7 @@ struct GitHubUser {
#[tokio::main]
async fn main() {
let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var");
let db = DefaultDb::new(&database_url, 5)
let db = Database::new(ConnectOptions::new(database_url))
.await
.expect("failed to connect to postgres database");
let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
use super::{AccessTokenId, UserId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "access_tokens")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: AccessTokenId,
pub user_id: UserId,
pub hash: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,58 @@
use super::{ContactId, UserId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "contacts")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: ContactId,
pub user_id_a: UserId,
pub user_id_b: UserId,
pub a_to_b: bool,
pub should_notify: bool,
pub accepted: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::room_participant::Entity",
from = "Column::UserIdA",
to = "super::room_participant::Column::UserId"
)]
UserARoomParticipant,
#[sea_orm(
belongs_to = "super::room_participant::Entity",
from = "Column::UserIdB",
to = "super::room_participant::Column::UserId"
)]
UserBRoomParticipant,
}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Contact {
Accepted {
user_id: UserId,
should_notify: bool,
busy: bool,
},
Outgoing {
user_id: UserId,
},
Incoming {
user_id: UserId,
should_notify: bool,
},
}
impl Contact {
pub fn user_id(&self) -> UserId {
match self {
Contact::Accepted { user_id, .. } => *user_id,
Contact::Outgoing { user_id } => *user_id,
Contact::Incoming { user_id, .. } => *user_id,
}
}
}

View File

@@ -0,0 +1,30 @@
use super::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "language_servers")]
pub struct Model {
#[sea_orm(primary_key)]
pub project_id: ProjectId,
#[sea_orm(primary_key)]
pub id: i64,
pub name: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::project::Entity",
from = "Column::ProjectId",
to = "super::project::Column::Id"
)]
Project,
}
impl Related<super::project::Entity> for Entity {
fn to() -> RelationDef {
Relation::Project.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,67 @@
use super::{ProjectId, RoomId, UserId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "projects")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: ProjectId,
pub room_id: RoomId,
pub host_user_id: UserId,
pub host_connection_id: i32,
pub host_connection_epoch: Uuid,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::HostUserId",
to = "super::user::Column::Id"
)]
HostUser,
#[sea_orm(
belongs_to = "super::room::Entity",
from = "Column::RoomId",
to = "super::room::Column::Id"
)]
Room,
#[sea_orm(has_many = "super::worktree::Entity")]
Worktrees,
#[sea_orm(has_many = "super::project_collaborator::Entity")]
Collaborators,
#[sea_orm(has_many = "super::language_server::Entity")]
LanguageServers,
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::HostUser.def()
}
}
impl Related<super::room::Entity> for Entity {
fn to() -> RelationDef {
Relation::Room.def()
}
}
impl Related<super::worktree::Entity> for Entity {
fn to() -> RelationDef {
Relation::Worktrees.def()
}
}
impl Related<super::project_collaborator::Entity> for Entity {
fn to() -> RelationDef {
Relation::Collaborators.def()
}
}
impl Related<super::language_server::Entity> for Entity {
fn to() -> RelationDef {
Relation::LanguageServers.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,33 @@
use super::{ProjectCollaboratorId, ProjectId, ReplicaId, UserId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "project_collaborators")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: ProjectCollaboratorId,
pub project_id: ProjectId,
pub connection_id: i32,
pub connection_epoch: Uuid,
pub user_id: UserId,
pub replica_id: ReplicaId,
pub is_host: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::project::Entity",
from = "Column::ProjectId",
to = "super::project::Column::Id"
)]
Project,
}
impl Related<super::project::Entity> for Entity {
fn to() -> RelationDef {
Relation::Project.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,32 @@
use super::RoomId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "rooms")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: RoomId,
pub live_kit_room: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::room_participant::Entity")]
RoomParticipant,
#[sea_orm(has_many = "super::project::Entity")]
Project,
}
impl Related<super::room_participant::Entity> for Entity {
fn to() -> RelationDef {
Relation::RoomParticipant.def()
}
}
impl Related<super::project::Entity> for Entity {
fn to() -> RelationDef {
Relation::Project.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,49 @@
use super::{ProjectId, RoomId, RoomParticipantId, UserId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "room_participants")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: RoomParticipantId,
pub room_id: RoomId,
pub user_id: UserId,
pub answering_connection_id: Option<i32>,
pub answering_connection_epoch: Option<Uuid>,
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,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
#[sea_orm(
belongs_to = "super::room::Entity",
from = "Column::RoomId",
to = "super::room::Column::Id"
)]
Room,
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl Related<super::room::Entity> for Entity {
fn to() -> RelationDef {
Relation::Room.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,57 @@
use super::{SignupId, UserId};
use sea_orm::{entity::prelude::*, FromQueryResult};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "signups")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: SignupId,
pub email_address: String,
pub email_confirmation_code: String,
pub email_confirmation_sent: bool,
pub created_at: DateTime,
pub device_id: Option<String>,
pub user_id: Option<UserId>,
pub inviting_user_id: Option<UserId>,
pub platform_mac: bool,
pub platform_linux: bool,
pub platform_windows: bool,
pub platform_unknown: bool,
pub editor_features: Option<Vec<String>>,
pub programming_languages: Option<Vec<String>>,
pub added_to_mailing_list: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Clone, Debug, PartialEq, Eq, FromQueryResult, Serialize, Deserialize)]
pub struct Invite {
pub email_address: String,
pub email_confirmation_code: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct NewSignup {
pub email_address: String,
pub platform_mac: bool,
pub platform_windows: bool,
pub platform_linux: bool,
pub editor_features: Vec<String>,
pub programming_languages: Vec<String>,
pub device_id: Option<String>,
pub added_to_mailing_list: bool,
pub created_at: Option<DateTime>,
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, FromQueryResult)]
pub struct WaitlistSummary {
pub count: i64,
pub linux_count: i64,
pub mac_count: i64,
pub windows_count: i64,
pub unknown_count: i64,
}

View File

@@ -1,19 +1,22 @@
use super::db::*;
use super::*;
use gpui::executor::{Background, Deterministic};
use std::sync::Arc;
#[cfg(test)]
use pretty_assertions::{assert_eq, assert_ne};
macro_rules! test_both_dbs {
($postgres_test_name:ident, $sqlite_test_name:ident, $db:ident, $body:block) => {
#[gpui::test]
async fn $postgres_test_name() {
let test_db = PostgresTestDb::new(Deterministic::new(0).build_background());
let test_db = TestDb::postgres(Deterministic::new(0).build_background());
let $db = test_db.db();
$body
}
#[gpui::test]
async fn $sqlite_test_name() {
let test_db = SqliteTestDb::new(Deterministic::new(0).build_background());
let test_db = TestDb::sqlite(Deterministic::new(0).build_background());
let $db = test_db.db();
$body
}
@@ -26,9 +29,10 @@ test_both_dbs!(
db,
{
let mut user_ids = Vec::new();
let mut user_metric_ids = Vec::new();
for i in 1..=4 {
user_ids.push(
db.create_user(
let user = db
.create_user(
&format!("user{i}@example.com"),
false,
NewUserParams {
@@ -38,9 +42,9 @@ test_both_dbs!(
},
)
.await
.unwrap()
.user_id,
);
.unwrap();
user_ids.push(user.user_id);
user_metric_ids.push(user.metrics_id);
}
assert_eq!(
@@ -52,6 +56,7 @@ test_both_dbs!(
github_user_id: Some(1),
email_address: Some("user1@example.com".to_string()),
admin: false,
metrics_id: user_metric_ids[0].parse().unwrap(),
..Default::default()
},
User {
@@ -60,6 +65,7 @@ test_both_dbs!(
github_user_id: Some(2),
email_address: Some("user2@example.com".to_string()),
admin: false,
metrics_id: user_metric_ids[1].parse().unwrap(),
..Default::default()
},
User {
@@ -68,6 +74,7 @@ test_both_dbs!(
github_user_id: Some(3),
email_address: Some("user3@example.com".to_string()),
admin: false,
metrics_id: user_metric_ids[2].parse().unwrap(),
..Default::default()
},
User {
@@ -76,6 +83,7 @@ test_both_dbs!(
github_user_id: Some(4),
email_address: Some("user4@example.com".to_string()),
admin: false,
metrics_id: user_metric_ids[3].parse().unwrap(),
..Default::default()
}
]
@@ -258,7 +266,8 @@ test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, {
db.get_contacts(user_1).await.unwrap(),
&[Contact::Accepted {
user_id: user_2,
should_notify: true
should_notify: true,
busy: false,
}],
);
assert!(db.has_contact(user_1, user_2).await.unwrap());
@@ -268,6 +277,7 @@ test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, {
&[Contact::Accepted {
user_id: user_1,
should_notify: false,
busy: false,
}]
);
@@ -284,6 +294,7 @@ test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, {
&[Contact::Accepted {
user_id: user_2,
should_notify: true,
busy: false,
}]
);
@@ -296,6 +307,7 @@ test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, {
&[Contact::Accepted {
user_id: user_2,
should_notify: false,
busy: false,
}]
);
@@ -309,10 +321,12 @@ test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, {
Contact::Accepted {
user_id: user_2,
should_notify: false,
busy: false,
},
Contact::Accepted {
user_id: user_3,
should_notify: false
should_notify: false,
busy: false,
}
]
);
@@ -320,7 +334,8 @@ test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, {
db.get_contacts(user_3).await.unwrap(),
&[Contact::Accepted {
user_id: user_1,
should_notify: false
should_notify: false,
busy: false,
}],
);
@@ -335,14 +350,16 @@ test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, {
db.get_contacts(user_2).await.unwrap(),
&[Contact::Accepted {
user_id: user_1,
should_notify: false
should_notify: false,
busy: false,
}]
);
assert_eq!(
db.get_contacts(user_3).await.unwrap(),
&[Contact::Accepted {
user_id: user_1,
should_notify: false
should_notify: false,
busy: false,
}],
);
});
@@ -388,16 +405,81 @@ test_both_dbs!(test_metrics_id_postgres, test_metrics_id_sqlite, db, {
assert_ne!(metrics_id1, metrics_id2);
});
test_both_dbs!(
test_project_count_postgres,
test_project_count_sqlite,
db,
{
let user1 = db
.create_user(
&format!("admin@example.com"),
true,
NewUserParams {
github_login: "admin".into(),
github_user_id: 0,
invite_count: 0,
},
)
.await
.unwrap();
let user2 = db
.create_user(
&format!("user@example.com"),
false,
NewUserParams {
github_login: "user".into(),
github_user_id: 1,
invite_count: 0,
},
)
.await
.unwrap();
let room_id = RoomId::from_proto(
db.create_room(user1.user_id, ConnectionId(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))
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
db.share_project(room_id, ConnectionId(1), &[])
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1);
db.share_project(room_id, ConnectionId(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), &[])
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2);
db.leave_room(ConnectionId(1)).await.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
}
);
#[test]
fn test_fuzzy_like_string() {
assert_eq!(DefaultDb::fuzzy_like_string("abcd"), "%a%b%c%d%");
assert_eq!(DefaultDb::fuzzy_like_string("x y"), "%x%y%");
assert_eq!(DefaultDb::fuzzy_like_string(" z "), "%z%");
assert_eq!(Database::fuzzy_like_string("abcd"), "%a%b%c%d%");
assert_eq!(Database::fuzzy_like_string("x y"), "%x%y%");
assert_eq!(Database::fuzzy_like_string(" z "), "%z%");
}
#[gpui::test]
async fn test_fuzzy_search_users() {
let test_db = PostgresTestDb::new(build_background_executor());
let test_db = TestDb::postgres(build_background_executor());
let db = test_db.db();
for (i, github_login) in [
"California",
@@ -433,7 +515,7 @@ async fn test_fuzzy_search_users() {
&["rhode-island", "colorado", "oregon"],
);
async fn fuzzy_search_user_names(db: &Db<sqlx::Postgres>, query: &str) -> Vec<String> {
async fn fuzzy_search_user_names(db: &Database, query: &str) -> Vec<String> {
db.fuzzy_search_users(query, 10)
.await
.unwrap()
@@ -445,7 +527,7 @@ async fn test_fuzzy_search_users() {
#[gpui::test]
async fn test_invite_codes() {
let test_db = PostgresTestDb::new(build_background_executor());
let test_db = TestDb::postgres(build_background_executor());
let db = test_db.db();
let NewUserResult { user_id: user1, .. } = db
@@ -504,16 +586,20 @@ async fn test_invite_codes() {
db.get_contacts(user1).await.unwrap(),
[Contact::Accepted {
user_id: user2,
should_notify: true
should_notify: true,
busy: false,
}]
);
assert_eq!(
db.get_contacts(user2).await.unwrap(),
[Contact::Accepted {
user_id: user1,
should_notify: false
should_notify: false,
busy: false,
}]
);
assert!(db.has_contact(user1, user2).await.unwrap());
assert!(db.has_contact(user2, user1).await.unwrap());
assert_eq!(
db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
7
@@ -550,11 +636,13 @@ async fn test_invite_codes() {
[
Contact::Accepted {
user_id: user2,
should_notify: true
should_notify: true,
busy: false,
},
Contact::Accepted {
user_id: user3,
should_notify: true
should_notify: true,
busy: false,
}
]
);
@@ -562,9 +650,12 @@ async fn test_invite_codes() {
db.get_contacts(user3).await.unwrap(),
[Contact::Accepted {
user_id: user1,
should_notify: false
should_notify: false,
busy: false,
}]
);
assert!(db.has_contact(user1, user3).await.unwrap());
assert!(db.has_contact(user3, user1).await.unwrap());
assert_eq!(
db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
3
@@ -607,15 +698,18 @@ async fn test_invite_codes() {
[
Contact::Accepted {
user_id: user2,
should_notify: true
should_notify: true,
busy: false,
},
Contact::Accepted {
user_id: user3,
should_notify: true
should_notify: true,
busy: false,
},
Contact::Accepted {
user_id: user4,
should_notify: true
should_notify: true,
busy: false,
}
]
);
@@ -623,9 +717,12 @@ async fn test_invite_codes() {
db.get_contacts(user4).await.unwrap(),
[Contact::Accepted {
user_id: user1,
should_notify: false
should_notify: false,
busy: false,
}]
);
assert!(db.has_contact(user1, user4).await.unwrap());
assert!(db.has_contact(user4, user1).await.unwrap());
assert_eq!(
db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,
5
@@ -637,11 +734,162 @@ async fn test_invite_codes() {
.unwrap_err();
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
assert_eq!(invite_count, 1);
// A newer user can invite an existing one via a different email address
// than the one they used to sign up.
let user5 = db
.create_user(
"user5@example.com",
false,
NewUserParams {
github_login: "user5".into(),
github_user_id: 5,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
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)
.await
.unwrap();
let user1_2 = db
.create_user_from_invite(
&user5_invite_to_user1,
NewUserParams {
github_login: "user1".into(),
github_user_id: 1,
invite_count: 5,
},
)
.await
.unwrap()
.unwrap()
.user_id;
assert_eq!(user1_2, user1);
assert_eq!(
db.get_contacts(user1).await.unwrap(),
[
Contact::Accepted {
user_id: user2,
should_notify: true,
busy: false,
},
Contact::Accepted {
user_id: user3,
should_notify: true,
busy: false,
},
Contact::Accepted {
user_id: user4,
should_notify: true,
busy: false,
},
Contact::Accepted {
user_id: user5,
should_notify: false,
busy: false,
}
]
);
assert_eq!(
db.get_contacts(user5).await.unwrap(),
[Contact::Accepted {
user_id: user1,
should_notify: true,
busy: false,
}]
);
assert!(db.has_contact(user1, user5).await.unwrap());
assert!(db.has_contact(user5, user1).await.unwrap());
}
#[gpui::test]
async fn test_multiple_signup_overwrite() {
let test_db = TestDb::postgres(build_background_executor());
let db = test_db.db();
let email_address = "user_1@example.com".to_string();
let initial_signup_created_at_milliseconds = 0;
let initial_signup = NewSignup {
email_address: email_address.clone(),
platform_mac: false,
platform_linux: true,
platform_windows: false,
editor_features: vec!["speed".into()],
programming_languages: vec!["rust".into(), "c".into()],
device_id: Some(format!("device_id")),
added_to_mailing_list: false,
created_at: Some(
DateTime::from_timestamp_millis(initial_signup_created_at_milliseconds).unwrap(),
),
};
db.create_signup(&initial_signup).await.unwrap();
let initial_signup_from_db = db.get_signup(&email_address).await.unwrap();
assert_eq!(
initial_signup_from_db.clone(),
signup::Model {
email_address: initial_signup.email_address,
platform_mac: initial_signup.platform_mac,
platform_linux: initial_signup.platform_linux,
platform_windows: initial_signup.platform_windows,
editor_features: Some(initial_signup.editor_features),
programming_languages: Some(initial_signup.programming_languages),
added_to_mailing_list: initial_signup.added_to_mailing_list,
..initial_signup_from_db
}
);
let subsequent_signup = NewSignup {
email_address: email_address.clone(),
platform_mac: true,
platform_linux: false,
platform_windows: true,
editor_features: vec!["git integration".into(), "clean design".into()],
programming_languages: vec!["d".into(), "elm".into()],
device_id: Some(format!("different_device_id")),
added_to_mailing_list: true,
// subsequent signup happens next day
created_at: Some(
DateTime::from_timestamp_millis(
initial_signup_created_at_milliseconds + (1000 * 60 * 60 * 24),
)
.unwrap(),
),
};
db.create_signup(&subsequent_signup).await.unwrap();
let subsequent_signup_from_db = db.get_signup(&email_address).await.unwrap();
assert_eq!(
subsequent_signup_from_db.clone(),
signup::Model {
platform_mac: subsequent_signup.platform_mac,
platform_linux: subsequent_signup.platform_linux,
platform_windows: subsequent_signup.platform_windows,
editor_features: Some(subsequent_signup.editor_features),
programming_languages: Some(subsequent_signup.programming_languages),
device_id: subsequent_signup.device_id,
added_to_mailing_list: subsequent_signup.added_to_mailing_list,
// shouldn't overwrite their creation Datetime - user shouldn't lose their spot in line
created_at: initial_signup_from_db.created_at,
..subsequent_signup_from_db
}
);
}
#[gpui::test]
async fn test_signups() {
let test_db = PostgresTestDb::new(build_background_executor());
let test_db = TestDb::postgres(build_background_executor());
let db = test_db.db();
let usernames = (0..8).map(|i| format!("person-{i}")).collect::<Vec<_>>();
@@ -649,7 +897,7 @@ async fn test_signups() {
let all_signups = usernames
.iter()
.enumerate()
.map(|(i, username)| Signup {
.map(|(i, username)| NewSignup {
email_address: format!("{username}@example.com"),
platform_mac: true,
platform_linux: i % 2 == 0,
@@ -657,8 +905,10 @@ async fn test_signups() {
editor_features: vec!["speed".into()],
programming_languages: vec!["rust".into(), "c".into()],
device_id: Some(format!("device_id_{i}")),
added_to_mailing_list: i != 0, // One user failed to subscribe
created_at: Some(DateTime::from_timestamp_millis(i as i64).unwrap()), // Signups are consecutive
})
.collect::<Vec<Signup>>();
.collect::<Vec<NewSignup>>();
// people sign up on the waitlist
for signup in &all_signups {

View File

@@ -0,0 +1,49 @@
use super::UserId;
use sea_orm::entity::prelude::*;
use serde::Serialize;
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)]
#[sea_orm(table_name = "users")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: UserId,
pub github_login: String,
pub github_user_id: Option<i32>,
pub email_address: Option<String>,
pub admin: bool,
pub invite_code: Option<String>,
pub invite_count: i32,
pub inviter_id: Option<UserId>,
pub connected_once: bool,
pub metrics_id: Uuid,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::access_token::Entity")]
AccessToken,
#[sea_orm(has_one = "super::room_participant::Entity")]
RoomParticipant,
#[sea_orm(has_many = "super::project::Entity")]
HostedProjects,
}
impl Related<super::access_token::Entity> for Entity {
fn to() -> RelationDef {
Relation::AccessToken.def()
}
}
impl Related<super::room_participant::Entity> for Entity {
fn to() -> RelationDef {
Relation::RoomParticipant.def()
}
}
impl Related<super::project::Entity> for Entity {
fn to() -> RelationDef {
Relation::HostedProjects.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,34 @@
use super::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worktrees")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
#[sea_orm(primary_key)]
pub project_id: ProjectId,
pub abs_path: String,
pub root_name: String,
pub visible: bool,
pub scan_id: i64,
pub is_complete: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::project::Entity",
from = "Column::ProjectId",
to = "super::project::Column::Id"
)]
Project,
}
impl Related<super::project::Entity> for Entity {
fn to() -> RelationDef {
Relation::Project.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,21 @@
use super::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worktree_diagnostic_summaries")]
pub struct Model {
#[sea_orm(primary_key)]
pub project_id: ProjectId,
#[sea_orm(primary_key)]
pub worktree_id: i64,
#[sea_orm(primary_key)]
pub path: String,
pub language_server_id: i64,
pub error_count: i32,
pub warning_count: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,25 @@
use super::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worktree_entries")]
pub struct Model {
#[sea_orm(primary_key)]
pub project_id: ProjectId,
#[sea_orm(primary_key)]
pub worktree_id: i64,
#[sea_orm(primary_key)]
pub id: i64,
pub is_dir: bool,
pub path: String,
pub inode: i64,
pub mtime_seconds: i64,
pub mtime_nanos: i32,
pub is_symlink: bool,
pub is_ignored: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -1,8 +1,9 @@
use crate::{
db::{NewUserParams, ProjectId, SqliteTestDb as TestDb, UserId},
db::{self, NewUserParams, TestDb, UserId},
rpc::{Executor, Server},
AppState,
};
use ::rpc::Peer;
use anyhow::anyhow;
use call::{room, ActiveCall, ParticipantLocation, Room};
@@ -30,9 +31,7 @@ use language::{
use live_kit_client::MacOSDisplay;
use lsp::{self, FakeLanguageServer};
use parking_lot::Mutex;
use project::{
search::SearchQuery, DiagnosticSummary, Project, ProjectPath, ProjectStore, WorktreeId,
};
use project::{search::SearchQuery, DiagnosticSummary, Project, ProjectPath, WorktreeId};
use rand::prelude::*;
use serde_json::json;
use settings::{Formatter, Settings};
@@ -51,7 +50,7 @@ use std::{
use theme::ThemeRegistry;
use unindent::Unindent as _;
use util::post_inc;
use workspace::{shared_screen::SharedScreen, Item, SplitDirection, ToggleFollow, Workspace};
use workspace::{item::Item, shared_screen::SharedScreen, SplitDirection, ToggleFollow, Workspace};
#[ctor::ctor]
fn init_logger() {
@@ -71,8 +70,6 @@ async fn test_basic_calls(
deterministic.forbid_parking();
let mut server = TestServer::start(cx_a.background()).await;
let start = std::time::Instant::now();
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
@@ -104,7 +101,7 @@ async fn test_basic_calls(
// User B receives the call.
let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
let call_b = incoming_call_b.next().await.unwrap().unwrap();
assert_eq!(call_b.caller.github_login, "user_a");
assert_eq!(call_b.calling_user.github_login, "user_a");
// User B connects via another client and also receives a ring on the newly-connected client.
let _client_b2 = server.create_client(cx_b2, "user_b").await;
@@ -112,7 +109,7 @@ async fn test_basic_calls(
let mut incoming_call_b2 = active_call_b2.read_with(cx_b2, |call, _| call.incoming());
deterministic.run_until_parked();
let call_b2 = incoming_call_b2.next().await.unwrap().unwrap();
assert_eq!(call_b2.caller.github_login, "user_a");
assert_eq!(call_b2.calling_user.github_login, "user_a");
// User B joins the room using the first client.
active_call_b
@@ -165,7 +162,7 @@ async fn test_basic_calls(
// User C receives the call, but declines it.
let call_c = incoming_call_c.next().await.unwrap().unwrap();
assert_eq!(call_c.caller.github_login, "user_b");
assert_eq!(call_c.calling_user.github_login, "user_b");
active_call_c.update(cx_c, |call, _| call.decline_incoming().unwrap());
assert!(incoming_call_c.next().await.unwrap().is_none());
@@ -258,8 +255,6 @@ async fn test_basic_calls(
pending: Default::default()
}
);
eprintln!("finished test {:?}", start.elapsed());
}
#[gpui::test(iterations = 10)]
@@ -308,7 +303,7 @@ async fn test_room_uniqueness(
// User B receives the call from user A.
let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
let call_b1 = incoming_call_b.next().await.unwrap().unwrap();
assert_eq!(call_b1.caller.github_login, "user_a");
assert_eq!(call_b1.calling_user.github_login, "user_a");
// Ensure calling users A and B from client C fails.
active_call_c
@@ -367,7 +362,7 @@ async fn test_room_uniqueness(
.unwrap();
deterministic.run_until_parked();
let call_b2 = incoming_call_b.next().await.unwrap().unwrap();
assert_eq!(call_b2.caller.github_login, "user_c");
assert_eq!(call_b2.calling_user.github_login, "user_c");
}
#[gpui::test(iterations = 10)]
@@ -695,7 +690,7 @@ async fn test_share_project(
let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
deterministic.run_until_parked();
let call = incoming_call_b.borrow().clone().unwrap();
assert_eq!(call.caller.github_login, "user_a");
assert_eq!(call.calling_user.github_login, "user_a");
let initial_project = call.initial_project.unwrap();
active_call_b
.update(cx_b, |call, cx| call.accept_incoming(cx))
@@ -766,7 +761,7 @@ async fn test_share_project(
let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
deterministic.run_until_parked();
let call = incoming_call_c.borrow().clone().unwrap();
assert_eq!(call.caller.github_login, "user_b");
assert_eq!(call.calling_user.github_login, "user_b");
let initial_project = call.initial_project.unwrap();
active_call_c
.update(cx_c, |call, cx| call.accept_incoming(cx))
@@ -905,8 +900,15 @@ async fn test_host_disconnect(
let project_b = client_b.build_remote_project(project_id, cx_b).await;
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
let (_, workspace_b) =
cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
let (_, workspace_b) = cx_b.add_window(|cx| {
Workspace::new(
Default::default(),
0,
project_b.clone(),
|_, _| unimplemented!(),
cx,
)
});
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "b.txt"), None, true, cx)
@@ -2284,7 +2286,6 @@ async fn test_leaving_project(
project_id,
client_b.client.clone(),
client_b.user_store.clone(),
client_b.project_store.clone(),
client_b.language_registry.clone(),
FakeFs::new(cx.background()),
cx,
@@ -2408,12 +2409,6 @@ async fn test_collaborating_with_diagnostics(
// Wait for server to see the diagnostics update.
deterministic.run_until_parked();
{
let store = server.store.lock().await;
let project = store.project(ProjectId::from_proto(project_id)).unwrap();
let worktree = project.worktrees.get(&worktree_id.to_proto()).unwrap();
assert!(!worktree.diagnostic_summaries.is_empty());
}
// Ensure client B observes the new diagnostics.
project_b.read_with(cx_b, |project, cx| {
@@ -2435,7 +2430,10 @@ async fn test_collaborating_with_diagnostics(
// Join project as client C and observe the diagnostics.
let project_c = client_c.build_remote_project(project_id, cx_c).await;
let project_c_diagnostic_summaries = Rc::new(RefCell::new(Vec::new()));
let project_c_diagnostic_summaries =
Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
project.diagnostic_summaries(cx).collect::<Vec<_>>()
})));
project_c.update(cx_c, |_, cx| {
let summaries = project_c_diagnostic_summaries.clone();
cx.subscribe(&project_c, {
@@ -3701,8 +3699,15 @@ async fn test_collaborating_with_code_actions(
// Join the project as client B.
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let (_window_b, workspace_b) =
cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
let (_window_b, workspace_b) = cx_b.add_window(|cx| {
Workspace::new(
Default::default(),
0,
project_b.clone(),
|_, _| unimplemented!(),
cx,
)
});
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
@@ -3922,8 +3927,15 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let (_window_b, workspace_b) =
cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
let (_window_b, workspace_b) = cx_b.add_window(|cx| {
Workspace::new(
Default::default(),
0,
project_b.clone(),
|_, _| unimplemented!(),
cx,
)
});
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "one.rs"), None, true, cx)
@@ -4176,18 +4188,21 @@ async fn test_contacts(
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
cx_d: &mut TestAppContext,
) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
let client_d = server.create_client(cx_d, "user_d").await;
server
.make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
let active_call_c = cx_c.read(ActiveCall::global);
let _active_call_d = cx_d.read(ActiveCall::global);
deterministic.run_until_parked();
assert_eq!(
@@ -4211,6 +4226,7 @@ async fn test_contacts(
("user_b".to_string(), "online", "free")
]
);
assert_eq!(contacts(&client_d, cx_d), []);
server.disconnect_client(client_c.peer_id().unwrap());
server.forbid_connections();
@@ -4230,6 +4246,7 @@ async fn test_contacts(
]
);
assert_eq!(contacts(&client_c, cx_c), []);
assert_eq!(contacts(&client_d, cx_d), []);
server.allow_connections();
client_c
@@ -4259,6 +4276,7 @@ async fn test_contacts(
("user_b".to_string(), "online", "free")
]
);
assert_eq!(contacts(&client_d, cx_d), []);
active_call_a
.update(cx_a, |call, cx| {
@@ -4288,6 +4306,39 @@ async fn test_contacts(
("user_b".to_string(), "online", "busy")
]
);
assert_eq!(contacts(&client_d, cx_d), []);
// Client B and client D become contacts while client B is being called.
server
.make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
.await;
deterministic.run_until_parked();
assert_eq!(
contacts(&client_a, cx_a),
[
("user_b".to_string(), "online", "busy"),
("user_c".to_string(), "online", "free")
]
);
assert_eq!(
contacts(&client_b, cx_b),
[
("user_a".to_string(), "online", "busy"),
("user_c".to_string(), "online", "free"),
("user_d".to_string(), "online", "free"),
]
);
assert_eq!(
contacts(&client_c, cx_c),
[
("user_a".to_string(), "online", "busy"),
("user_b".to_string(), "online", "busy")
]
);
assert_eq!(
contacts(&client_d, cx_d),
[("user_b".to_string(), "online", "busy")]
);
active_call_b.update(cx_b, |call, _| call.decline_incoming().unwrap());
deterministic.run_until_parked();
@@ -4302,7 +4353,8 @@ async fn test_contacts(
contacts(&client_b, cx_b),
[
("user_a".to_string(), "online", "free"),
("user_c".to_string(), "online", "free")
("user_c".to_string(), "online", "free"),
("user_d".to_string(), "online", "free")
]
);
assert_eq!(
@@ -4312,6 +4364,10 @@ async fn test_contacts(
("user_b".to_string(), "online", "free")
]
);
assert_eq!(
contacts(&client_d, cx_d),
[("user_b".to_string(), "online", "free")]
);
active_call_c
.update(cx_c, |call, cx| {
@@ -4331,7 +4387,8 @@ async fn test_contacts(
contacts(&client_b, cx_b),
[
("user_a".to_string(), "online", "busy"),
("user_c".to_string(), "online", "busy")
("user_c".to_string(), "online", "busy"),
("user_d".to_string(), "online", "free")
]
);
assert_eq!(
@@ -4341,6 +4398,10 @@ async fn test_contacts(
("user_b".to_string(), "online", "free")
]
);
assert_eq!(
contacts(&client_d, cx_d),
[("user_b".to_string(), "online", "free")]
);
active_call_a
.update(cx_a, |call, cx| call.accept_incoming(cx))
@@ -4358,7 +4419,8 @@ async fn test_contacts(
contacts(&client_b, cx_b),
[
("user_a".to_string(), "online", "busy"),
("user_c".to_string(), "online", "busy")
("user_c".to_string(), "online", "busy"),
("user_d".to_string(), "online", "free")
]
);
assert_eq!(
@@ -4368,6 +4430,10 @@ async fn test_contacts(
("user_b".to_string(), "online", "free")
]
);
assert_eq!(
contacts(&client_d, cx_d),
[("user_b".to_string(), "online", "free")]
);
active_call_a
.update(cx_a, |call, cx| {
@@ -4387,7 +4453,8 @@ async fn test_contacts(
contacts(&client_b, cx_b),
[
("user_a".to_string(), "online", "busy"),
("user_c".to_string(), "online", "busy")
("user_c".to_string(), "online", "busy"),
("user_d".to_string(), "online", "free")
]
);
assert_eq!(
@@ -4397,6 +4464,10 @@ async fn test_contacts(
("user_b".to_string(), "online", "busy")
]
);
assert_eq!(
contacts(&client_d, cx_d),
[("user_b".to_string(), "online", "busy")]
);
active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap());
deterministic.run_until_parked();
@@ -4411,7 +4482,8 @@ async fn test_contacts(
contacts(&client_b, cx_b),
[
("user_a".to_string(), "online", "free"),
("user_c".to_string(), "online", "free")
("user_c".to_string(), "online", "free"),
("user_d".to_string(), "online", "free")
]
);
assert_eq!(
@@ -4421,6 +4493,10 @@ async fn test_contacts(
("user_b".to_string(), "online", "free")
]
);
assert_eq!(
contacts(&client_d, cx_d),
[("user_b".to_string(), "online", "free")]
);
active_call_a
.update(cx_a, |call, cx| {
@@ -4440,7 +4516,8 @@ async fn test_contacts(
contacts(&client_b, cx_b),
[
("user_a".to_string(), "online", "busy"),
("user_c".to_string(), "online", "free")
("user_c".to_string(), "online", "free"),
("user_d".to_string(), "online", "free")
]
);
assert_eq!(
@@ -4450,6 +4527,10 @@ async fn test_contacts(
("user_b".to_string(), "online", "busy")
]
);
assert_eq!(
contacts(&client_d, cx_d),
[("user_b".to_string(), "online", "busy")]
);
server.forbid_connections();
server.disconnect_client(client_a.peer_id().unwrap());
@@ -4459,7 +4540,8 @@ async fn test_contacts(
contacts(&client_b, cx_b),
[
("user_a".to_string(), "offline", "free"),
("user_c".to_string(), "online", "free")
("user_c".to_string(), "online", "free"),
("user_d".to_string(), "online", "free")
]
);
assert_eq!(
@@ -4469,8 +4551,11 @@ async fn test_contacts(
("user_b".to_string(), "online", "free")
]
);
assert_eq!(
contacts(&client_d, cx_d),
[("user_b".to_string(), "online", "free")]
);
#[allow(clippy::type_complexity)]
fn contacts(
client: &TestClient,
cx: &TestAppContext,
@@ -4953,6 +5038,129 @@ async fn test_following(
);
}
#[gpui::test]
async fn test_following_tab_order(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
cx_a.update(editor::init);
cx_b.update(editor::init);
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
client_a
.fs
.insert_tree(
"/a",
json!({
"1.txt": "one",
"2.txt": "two",
"3.txt": "three",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
active_call_a
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
.await
.unwrap();
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
.unwrap();
let workspace_a = client_a.build_workspace(&project_a, cx_a);
let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
let workspace_b = client_b.build_workspace(&project_b, cx_b);
let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
let client_b_id = project_a.read_with(cx_a, |project, _| {
project.collaborators().values().next().unwrap().peer_id
});
//Open 1, 3 in that order on client A
workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
})
.await
.unwrap();
workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "3.txt"), None, true, cx)
})
.await
.unwrap();
let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
pane.update(cx, |pane, cx| {
pane.items()
.map(|item| {
item.project_path(cx)
.unwrap()
.path
.to_str()
.unwrap()
.to_owned()
})
.collect::<Vec<_>>()
})
};
//Verify that the tabs opened in the order we expect
assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
//Follow client B as client A
workspace_a
.update(cx_a, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(client_b_id), cx)
.unwrap()
})
.await
.unwrap();
//Open just 2 on client B
workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
})
.await
.unwrap();
deterministic.run_until_parked();
// Verify that newly opened followed file is at the end
assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
//Open just 1 on client B
workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
})
.await
.unwrap();
assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
deterministic.run_until_parked();
// Verify that following into 1 did not reorder
assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
}
#[gpui::test(iterations = 10)]
async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
@@ -5482,18 +5690,15 @@ async fn test_random_collaboration(
}
for user_id in &user_ids {
let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap();
let contacts = server
.store
.lock()
.await
.build_initial_contacts_update(contacts)
.contacts;
let pool = server.connection_pool.lock().await;
for contact in contacts {
if contact.online {
assert_ne!(
contact.user_id, removed_guest_id.0 as u64,
"removed guest is still a contact of another peer"
);
if let db::Contact::Accepted { user_id, .. } = contact {
if pool.is_user_online(user_id) {
assert_ne!(
user_id, removed_guest_id,
"removed guest is still a contact of another peer"
);
}
}
}
}
@@ -5685,7 +5890,13 @@ impl TestServer {
async fn start(background: Arc<executor::Background>) -> Self {
static NEXT_LIVE_KIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0);
let test_db = TestDb::new(background.clone());
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(background.clone())
} else {
TestDb::sqlite(background.clone())
};
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),
@@ -5803,11 +6014,9 @@ impl TestServer {
let fs = FakeFs::new(cx.background());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
let project_store = cx.add_model(|_| ProjectStore::new());
let app_state = Arc::new(workspace::AppState {
client: client.clone(),
user_store: user_store.clone(),
project_store: project_store.clone(),
languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
themes: ThemeRegistry::new((), cx.font_cache()),
fs: fs.clone(),
@@ -5834,7 +6043,6 @@ impl TestServer {
remote_projects: Default::default(),
next_root_dir_id: 0,
user_store,
project_store,
fs,
language_registry: Arc::new(LanguageRegistry::test()),
buffers: Default::default(),
@@ -5940,7 +6148,6 @@ struct TestClient {
remote_projects: Vec<ModelHandle<Project>>,
next_root_dir_id: usize,
pub user_store: ModelHandle<UserStore>,
pub project_store: ModelHandle<ProjectStore>,
language_registry: Arc<LanguageRegistry>,
fs: Arc<FakeFs>,
buffers: HashMap<ModelHandle<Project>, HashSet<ModelHandle<language::Buffer>>>,
@@ -6010,7 +6217,6 @@ impl TestClient {
Project::local(
self.client.clone(),
self.user_store.clone(),
self.project_store.clone(),
self.language_registry.clone(),
self.fs.clone(),
cx,
@@ -6038,7 +6244,6 @@ impl TestClient {
host_project_id,
self.client.clone(),
self.user_store.clone(),
self.project_store.clone(),
self.language_registry.clone(),
FakeFs::new(cx.background()),
cx,
@@ -6054,7 +6259,13 @@ impl TestClient {
) -> ViewHandle<Workspace> {
let (_, root_view) = cx.add_window(|_| EmptyView);
cx.add_view(&root_view, |cx| {
Workspace::new(project.clone(), |_, _| unimplemented!(), cx)
Workspace::new(
Default::default(),
0,
project.clone(),
|_, _| unimplemented!(),
cx,
)
})
}
@@ -6168,7 +6379,6 @@ impl TestClient {
remote_project_id,
client.client.clone(),
client.user_store.clone(),
client.project_store.clone(),
client.language_registry.clone(),
FakeFs::new(cx.background()),
cx.to_async(),
@@ -6418,7 +6628,7 @@ impl TestClient {
buffers.extend(search.await?.into_keys());
}
}
60..=69 => {
60..=79 => {
let worktree = project
.read_with(cx, |project, cx| {
project

View File

@@ -1,9 +1,21 @@
pub mod api;
pub mod auth;
pub mod db;
pub mod env;
#[cfg(test)]
mod integration_tests;
pub mod rpc;
use axum::{http::StatusCode, response::IntoResponse};
use db::Database;
use serde::Deserialize;
use std::{path::PathBuf, sync::Arc};
pub type Result<T, E = Error> = std::result::Result<T, E>;
pub enum Error {
Http(StatusCode, String),
Database(sea_orm::error::DbErr),
Internal(anyhow::Error),
}
@@ -13,9 +25,9 @@ impl From<anyhow::Error> for Error {
}
}
impl From<sqlx::Error> for Error {
fn from(error: sqlx::Error) -> Self {
Self::Internal(error.into())
impl From<sea_orm::error::DbErr> for Error {
fn from(error: sea_orm::error::DbErr) -> Self {
Self::Database(error)
}
}
@@ -41,6 +53,9 @@ impl IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
match self {
Error::Http(code, message) => (code, message).into_response(),
Error::Database(error) => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
}
Error::Internal(error) => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
}
@@ -52,6 +67,7 @@ impl std::fmt::Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Http(code, message) => (code, message).fmt(f),
Error::Database(error) => error.fmt(f),
Error::Internal(error) => error.fmt(f),
}
}
@@ -61,9 +77,64 @@ impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Http(code, message) => write!(f, "{code}: {message}"),
Error::Database(error) => error.fmt(f),
Error::Internal(error) => error.fmt(f),
}
}
}
impl std::error::Error for Error {}
#[derive(Default, Deserialize)]
pub struct Config {
pub http_port: u16,
pub database_url: String,
pub api_token: String,
pub invite_link_prefix: String,
pub live_kit_server: Option<String>,
pub live_kit_key: Option<String>,
pub live_kit_secret: Option<String>,
pub rust_log: Option<String>,
pub log_json: Option<bool>,
}
#[derive(Default, Deserialize)]
pub struct MigrateConfig {
pub database_url: String,
pub migrations_path: Option<PathBuf>,
}
pub struct AppState {
pub db: Arc<Database>,
pub live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
pub config: Config,
}
impl AppState {
pub async fn new(config: Config) -> Result<Arc<Self>> {
let mut db_options = db::ConnectOptions::new(config.database_url.clone());
db_options.max_connections(5);
let db = Database::new(db_options).await?;
let live_kit_client = if let Some(((server, key), secret)) = config
.live_kit_server
.as_ref()
.zip(config.live_kit_key.as_ref())
.zip(config.live_kit_secret.as_ref())
{
Some(Arc::new(live_kit_server::api::LiveKitClient::new(
server.clone(),
key.clone(),
secret.clone(),
)) as Arc<dyn live_kit_server::api::Client>)
} else {
None
};
let this = Self {
db: Arc::new(db),
live_kit_client,
config,
};
Ok(Arc::new(this))
}
}

View File

@@ -1,86 +1,18 @@
mod api;
mod auth;
mod db;
mod env;
mod rpc;
#[cfg(test)]
mod db_tests;
#[cfg(test)]
mod integration_tests;
use crate::rpc::ResultExt as _;
use anyhow::anyhow;
use axum::{routing::get, Router};
use collab::{Error, Result};
use db::DefaultDb as Db;
use serde::Deserialize;
use collab::{db, env, AppState, Config, MigrateConfig, Result};
use db::Database;
use std::{
env::args,
net::{SocketAddr, TcpListener},
path::{Path, PathBuf},
sync::Arc,
time::Duration,
path::Path,
};
use tokio::signal;
use tracing_log::LogTracer;
use tracing_subscriber::{filter::EnvFilter, fmt::format::JsonFields, Layer};
use util::ResultExt;
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
#[derive(Default, Deserialize)]
pub struct Config {
pub http_port: u16,
pub database_url: String,
pub api_token: String,
pub invite_link_prefix: String,
pub live_kit_server: Option<String>,
pub live_kit_key: Option<String>,
pub live_kit_secret: Option<String>,
pub rust_log: Option<String>,
pub log_json: Option<bool>,
}
#[derive(Default, Deserialize)]
pub struct MigrateConfig {
pub database_url: String,
pub migrations_path: Option<PathBuf>,
}
pub struct AppState {
db: Arc<Db>,
live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
config: Config,
}
impl AppState {
async fn new(config: Config) -> Result<Arc<Self>> {
let db = Db::new(&config.database_url, 5).await?;
let live_kit_client = if let Some(((server, key), secret)) = config
.live_kit_server
.as_ref()
.zip(config.live_kit_key.as_ref())
.zip(config.live_kit_secret.as_ref())
{
Some(Arc::new(live_kit_server::api::LiveKitClient::new(
server.clone(),
key.clone(),
secret.clone(),
)) as Arc<dyn live_kit_server::api::Client>)
} else {
None
};
let this = Self {
db: Arc::new(db),
live_kit_client,
config,
};
Ok(Arc::new(this))
}
}
#[tokio::main]
async fn main() -> Result<()> {
if let Err(error) = env::load_dotenv() {
@@ -96,7 +28,9 @@ async fn main() -> Result<()> {
}
Some("migrate") => {
let config = envy::from_env::<MigrateConfig>().expect("error loading config");
let db = Db::new(&config.database_url, 5).await?;
let mut db_options = db::ConnectOptions::new(config.database_url.clone());
db_options.max_connections(5);
let db = Database::new(db_options).await?;
let migrations_path = config
.migrations_path
@@ -118,18 +52,19 @@ async fn main() -> Result<()> {
init_tracing(&config);
let state = AppState::new(config).await?;
state.db.clear_stale_data().await?;
let listener = TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port))
.expect("failed to bind TCP listener");
let rpc_server = rpc::Server::new(state.clone());
let rpc_server = collab::rpc::Server::new(state.clone());
let app = api::routes(rpc_server.clone(), state.clone())
.merge(rpc::routes(rpc_server.clone()))
let app = collab::api::routes(rpc_server.clone(), state.clone())
.merge(collab::rpc::routes(rpc_server.clone()))
.merge(Router::new().route("/", get(handle_root)));
axum::Server::from_tcp(listener)?
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
.with_graceful_shutdown(graceful_shutdown(rpc_server, state))
.await?;
}
_ => {
@@ -174,52 +109,3 @@ pub fn init_tracing(config: &Config) -> Option<()> {
None
}
async fn graceful_shutdown(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
if let Some(live_kit) = state.live_kit_client.as_ref() {
let deletions = rpc_server
.store()
.await
.rooms()
.values()
.map(|room| {
let name = room.live_kit_room.clone();
async {
live_kit.delete_room(name).await.trace_err();
}
})
.collect::<Vec<_>>();
tracing::info!("deleting all live-kit rooms");
if let Err(_) = tokio::time::timeout(
Duration::from_secs(10),
futures::future::join_all(deletions),
)
.await
{
tracing::error!("timed out waiting for live-kit room deletion");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,93 @@
use crate::db::UserId;
use anyhow::{anyhow, Result};
use collections::{BTreeMap, HashSet};
use rpc::ConnectionId;
use serde::Serialize;
use tracing::instrument;
#[derive(Default, Serialize)]
pub struct ConnectionPool {
connections: BTreeMap<ConnectionId, Connection>,
connected_users: BTreeMap<UserId, ConnectedUser>,
}
#[derive(Default, Serialize)]
struct ConnectedUser {
connection_ids: HashSet<ConnectionId>,
}
#[derive(Serialize)]
pub struct Connection {
pub user_id: UserId,
pub admin: bool,
}
impl ConnectionPool {
#[instrument(skip(self))]
pub fn add_connection(&mut self, connection_id: ConnectionId, user_id: UserId, admin: bool) {
self.connections
.insert(connection_id, Connection { user_id, admin });
let connected_user = self.connected_users.entry(user_id).or_default();
connected_user.connection_ids.insert(connection_id);
}
#[instrument(skip(self))]
pub fn remove_connection(&mut self, connection_id: ConnectionId) -> Result<()> {
let connection = self
.connections
.get_mut(&connection_id)
.ok_or_else(|| anyhow!("no such connection"))?;
let user_id = connection.user_id;
let connected_user = self.connected_users.get_mut(&user_id).unwrap();
connected_user.connection_ids.remove(&connection_id);
if connected_user.connection_ids.is_empty() {
self.connected_users.remove(&user_id);
}
self.connections.remove(&connection_id).unwrap();
Ok(())
}
pub fn connections(&self) -> impl Iterator<Item = &Connection> {
self.connections.values()
}
pub fn user_connection_ids(&self, user_id: UserId) -> impl Iterator<Item = ConnectionId> + '_ {
self.connected_users
.get(&user_id)
.into_iter()
.map(|state| &state.connection_ids)
.flatten()
.copied()
}
pub fn is_user_online(&self, user_id: UserId) -> bool {
!self
.connected_users
.get(&user_id)
.unwrap_or(&Default::default())
.connection_ids
.is_empty()
}
#[cfg(test)]
pub fn check_invariants(&self) {
for (connection_id, connection) in &self.connections {
assert!(self
.connected_users
.get(&connection.user_id)
.unwrap()
.connection_ids
.contains(connection_id));
}
for (user_id, state) in &self.connected_users {
for connection_id in &state.connection_ids {
assert_eq!(
self.connections.get(connection_id).unwrap().user_id,
*user_id
);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -43,7 +43,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
project_id,
app_state.client.clone(),
app_state.user_store.clone(),
app_state.project_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
cx.clone(),
@@ -51,7 +50,13 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
.await?;
let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
let mut workspace = Workspace::new(project, app_state.default_item_factory, cx);
let mut workspace = Workspace::new(
Default::default(),
0,
project,
app_state.default_item_factory,
cx,
);
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
workspace
});

View File

@@ -6,7 +6,7 @@ use gpui::{
elements::*, impl_internal_actions, Entity, ModelHandle, MutableAppContext, RenderContext,
View, ViewContext,
};
use workspace::Notification;
use workspace::notifications::Notification;
impl_internal_actions!(contact_notifications, [Dismiss, RespondToContactRequest]);

View File

@@ -74,7 +74,7 @@ impl IncomingCallNotification {
let active_call = ActiveCall::global(cx);
if action.accept {
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
let caller_user_id = self.call.caller.id;
let caller_user_id = self.call.calling_user.id;
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
cx.spawn_weak(|_, mut cx| async move {
join.await?;
@@ -105,7 +105,7 @@ impl IncomingCallNotification {
.as_ref()
.unwrap_or(&default_project);
Flex::row()
.with_children(self.call.caller.avatar.clone().map(|avatar| {
.with_children(self.call.calling_user.avatar.clone().map(|avatar| {
Image::new(avatar)
.with_style(theme.caller_avatar)
.aligned()
@@ -115,7 +115,7 @@ impl IncomingCallNotification {
Flex::column()
.with_child(
Label::new(
self.call.caller.github_login.clone(),
self.call.calling_user.github_login.clone(),
theme.caller_username.text.clone(),
)
.contained()

View File

@@ -350,8 +350,9 @@ mod tests {
});
let project = Project::test(app_state.fs.clone(), [], cx).await;
let (_, workspace) =
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let (_, workspace) = cx.add_window(|cx| {
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
});
let editor = cx.add_view(&workspace, |cx| {
let mut editor = Editor::single_line(None, cx);
editor.set_text("abc", cx);

View File

@@ -12,16 +12,20 @@ test-support = []
[dependencies]
collections = { path = "../collections" }
gpui = { path = "../gpui" }
sqlez = { path = "../sqlez" }
sqlez_macros = { path = "../sqlez_macros" }
util = { path = "../util" }
anyhow = "1.0.57"
indoc = "1.0.4"
async-trait = "0.1"
lazy_static = "1.4.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
rusqlite = { version = "0.28.0", features = ["bundled", "serde_json"] }
rusqlite_migration = { git = "https://github.com/cljoly/rusqlite_migration", rev = "c433555d7c1b41b103426e35756eb3144d0ebbc6" }
serde = { workspace = true }
serde_rusqlite = "0.31.0"
serde = { version = "1.0", features = ["derive"] }
smol = "1.2"
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
env_logger = "0.9.1"
tempdir = { version = "0.3.7" }

5
crates/db/README.md Normal file
View File

@@ -0,0 +1,5 @@
# Building Queries
First, craft your test data. The examples folder shows a template for building a test-db, and can be ran with `cargo run --example [your-example]`.
To actually use and test your queries, import the generated DB file into https://sqliteonline.com/

View File

@@ -1,119 +1,365 @@
mod kvp;
mod migrations;
pub mod kvp;
pub mod query;
use std::fs;
// Re-export
pub use anyhow;
use anyhow::Context;
pub use indoc::indoc;
pub use lazy_static;
use parking_lot::{Mutex, RwLock};
pub use smol;
pub use sqlez;
pub use sqlez_macros;
pub use util::channel::{RELEASE_CHANNEL, RELEASE_CHANNEL_NAME};
pub use util::paths::DB_DIR;
use sqlez::domain::Migrator;
use sqlez::thread_safe_connection::ThreadSafeConnection;
use sqlez_macros::sql;
use std::fs::create_dir_all;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use util::{async_iife, ResultExt};
use util::channel::ReleaseChannel;
use anyhow::Result;
use log::error;
use parking_lot::Mutex;
use rusqlite::Connection;
const CONNECTION_INITIALIZE_QUERY: &'static str = sql!(
PRAGMA foreign_keys=TRUE;
);
use migrations::MIGRATIONS;
const DB_INITIALIZE_QUERY: &'static str = sql!(
PRAGMA journal_mode=WAL;
PRAGMA busy_timeout=1;
PRAGMA case_sensitive_like=TRUE;
PRAGMA synchronous=NORMAL;
);
#[derive(Clone)]
pub enum Db {
Real(Arc<RealDb>),
Null,
const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
const DB_FILE_NAME: &'static str = "db.sqlite";
lazy_static::lazy_static! {
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 struct RealDb {
connection: Mutex<Connection>,
path: Option<PathBuf>,
}
/// 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> {
let release_channel_name = release_channel.dev_name();
let main_db_dir = db_dir.join(Path::new(&format!("0-{}", release_channel_name)));
impl Db {
/// Open or create a database at the given directory path.
pub fn open(db_dir: &Path, channel: &'static str) -> Self {
// Use 0 for now. Will implement incrementing and clearing of old db files soon TM
let current_db_dir = db_dir.join(Path::new(&format!("0-{}", channel)));
fs::create_dir_all(&current_db_dir)
.expect("Should be able to create the database directory");
let db_path = current_db_dir.join(Path::new("db.sqlite"));
Connection::open(db_path)
.map_err(Into::into)
.and_then(|connection| Self::initialize(connection))
.map(|connection| {
Db::Real(Arc::new(RealDb {
connection,
path: Some(db_dir.to_path_buf()),
}))
})
.unwrap_or_else(|e| {
error!(
"Connecting to file backed db failed. Reverting to null db. {}",
e
);
Self::Null
})
}
/// Open a in memory database for testing and as a fallback.
#[cfg(any(test, feature = "test-support"))]
pub fn open_in_memory() -> Self {
Connection::open_in_memory()
.map_err(Into::into)
.and_then(|connection| Self::initialize(connection))
.map(|connection| {
Db::Real(Arc::new(RealDb {
connection,
path: None,
}))
})
.unwrap_or_else(|e| {
error!(
"Connecting to in memory db failed. Reverting to null db. {}",
e
);
Self::Null
})
}
fn initialize(mut conn: Connection) -> Result<Mutex<Connection>> {
MIGRATIONS.to_latest(&mut conn)?;
conn.pragma_update(None, "journal_mode", "WAL")?;
conn.pragma_update(None, "synchronous", "NORMAL")?;
conn.pragma_update(None, "foreign_keys", true)?;
conn.pragma_update(None, "case_sensitive_like", true)?;
Ok(Mutex::new(conn))
}
pub fn persisting(&self) -> bool {
self.real().and_then(|db| db.path.as_ref()).is_some()
}
pub fn real(&self) -> Option<&RealDb> {
match self {
Db::Real(db) => Some(&db),
_ => None,
let connection = async_iife!({
// Note: This still has a race condition where 1 set of migrations succeeds
// (e.g. (Workspace, Editor)) and another fails (e.g. (Workspace, Terminal))
// This will cause the first connection to have the database taken out
// from under it. This *should* be fine though. The second dabatase failure will
// cause errors in the log and so should be observed by developers while writing
// soon-to-be good migrations. If user databases are corrupted, we toss them out
// and try again from a blank. As long as running all migrations from start to end
// on a blank database is ok, this race condition will never be triggered.
//
// Basically: Don't ever push invalid migrations to stable or everyone will have
// 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
if let Some(connection) = open_main_db(&db_path).await {
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
let _lock = DB_FILE_OPERATIONS.lock();
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!(
"{}-{}",
backup_timestamp,
release_channel_name,
)));
std::fs::rename(&main_db_dir, &backup_db_dir)
.context("Failed clean up corrupted database, panicking.")?;
// Set a static ref with the failed timestamp and error so we can notify the user
{
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));
// Try again
open_main_db(&db_path).await.context("Could not newly created db")
}).await.log_err();
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
}
impl Drop for Db {
fn drop(&mut self) {
match self {
Db::Real(real_db) => {
let lock = real_db.connection.lock();
async fn open_main_db<M: Migrator>(db_path: &PathBuf) -> Option<ThreadSafeConnection<M>> {
log::info!("Opening main db");
ThreadSafeConnection::<M>::builder(db_path.to_string_lossy().as_ref(), true)
.with_db_initialization_query(DB_INITIALIZE_QUERY)
.with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
.build()
.await
.log_err()
}
let _ = lock.pragma_update(None, "analysis_limit", "500");
let _ = lock.pragma_update(None, "optimize", "");
async fn open_fallback_db<M: Migrator>() -> ThreadSafeConnection<M> {
log::info!("Opening fallback db");
ThreadSafeConnection::<M>::builder(FALLBACK_DB_NAME, false)
.with_db_initialization_query(DB_INITIALIZE_QUERY)
.with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
.build()
.await
.expect(
"Fallback in memory database failed. Likely initialization queries or migrations have fundamental errors",
)
}
#[cfg(any(test, feature = "test-support"))]
pub async fn open_test_db<M: Migrator>(db_name: &str) -> ThreadSafeConnection<M> {
use sqlez::thread_safe_connection::locking_queue;
ThreadSafeConnection::<M>::builder(db_name, false)
.with_db_initialization_query(DB_INITIALIZE_QUERY)
.with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
// Serialize queued writes via a mutex and run them synchronously
.with_write_queue_constructor(locking_queue())
.build()
.await
.unwrap()
}
/// Implements a basic DB wrapper for a given domain
#[macro_export]
macro_rules! define_connection {
(pub static ref $id:ident: $t:ident<()> = $migrations:expr;) => {
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection<$t>);
impl ::std::ops::Deref for $t {
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection<$t>;
fn deref(&self) -> &Self::Target {
&self.0
}
Db::Null => {}
}
}
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"))]
$crate::lazy_static::lazy_static! {
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_test_db(stringify!($id))));
}
#[cfg(not(any(test, feature = "test-support")))]
$crate::lazy_static::lazy_static! {
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db(&$crate::DB_DIR, &$crate::RELEASE_CHANNEL)));
}
};
(pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr;) => {
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection<( $($d),+, $t )>);
impl ::std::ops::Deref for $t {
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection<($($d),+, $t)>;
fn deref(&self) -> &Self::Target {
&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"))]
$crate::lazy_static::lazy_static! {
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_test_db(stringify!($id))));
}
#[cfg(not(any(test, feature = "test-support")))]
$crate::lazy_static::lazy_static! {
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db(&$crate::DB_DIR, &$crate::RELEASE_CHANNEL)));
}
};
}
#[cfg(test)]
mod tests {
use crate::migrations::MIGRATIONS;
use std::{fs, thread};
#[test]
fn test_migrations() {
assert!(MIGRATIONS.validate().is_ok());
use sqlez::{domain::Domain, connection::Connection};
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);),
// failure because test already exists
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;
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();
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());
}
/// 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;
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());
});
guards.push(guard);
}
for guard in guards.into_iter() {
assert!(guard.join().is_ok());
}
}
}

View File

@@ -1,311 +0,0 @@
use std::{ffi::OsStr, fmt::Display, hash::Hash, os::unix::prelude::OsStrExt, path::PathBuf};
use anyhow::Result;
use collections::HashSet;
use rusqlite::{named_params, params};
use super::Db;
pub(crate) const ITEMS_M_1: &str = "
CREATE TABLE items(
id INTEGER PRIMARY KEY,
kind TEXT
) STRICT;
CREATE TABLE item_path(
item_id INTEGER PRIMARY KEY,
path BLOB
) STRICT;
CREATE TABLE item_query(
item_id INTEGER PRIMARY KEY,
query TEXT
) STRICT;
";
#[derive(PartialEq, Eq, Hash, Debug)]
pub enum SerializedItemKind {
Editor,
Terminal,
ProjectSearch,
Diagnostics,
}
impl Display for SerializedItemKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&format!("{:?}", self))
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum SerializedItem {
Editor(usize, PathBuf),
Terminal(usize),
ProjectSearch(usize, String),
Diagnostics(usize),
}
impl SerializedItem {
fn kind(&self) -> SerializedItemKind {
match self {
SerializedItem::Editor(_, _) => SerializedItemKind::Editor,
SerializedItem::Terminal(_) => SerializedItemKind::Terminal,
SerializedItem::ProjectSearch(_, _) => SerializedItemKind::ProjectSearch,
SerializedItem::Diagnostics(_) => SerializedItemKind::Diagnostics,
}
}
fn id(&self) -> usize {
match self {
SerializedItem::Editor(id, _)
| SerializedItem::Terminal(id)
| SerializedItem::ProjectSearch(id, _)
| SerializedItem::Diagnostics(id) => *id,
}
}
}
impl Db {
fn write_item(&self, serialized_item: SerializedItem) -> Result<()> {
self.real()
.map(|db| {
let mut lock = db.connection.lock();
let tx = lock.transaction()?;
// Serialize the item
let id = serialized_item.id();
{
let mut stmt = tx.prepare_cached(
"INSERT OR REPLACE INTO items(id, kind) VALUES ((?), (?))",
)?;
dbg!("inserting item");
stmt.execute(params![id, serialized_item.kind().to_string()])?;
}
// Serialize item data
match &serialized_item {
SerializedItem::Editor(_, path) => {
dbg!("inserting path");
let mut stmt = tx.prepare_cached(
"INSERT OR REPLACE INTO item_path(item_id, path) VALUES ((?), (?))",
)?;
let path_bytes = path.as_os_str().as_bytes();
stmt.execute(params![id, path_bytes])?;
}
SerializedItem::ProjectSearch(_, query) => {
dbg!("inserting query");
let mut stmt = tx.prepare_cached(
"INSERT OR REPLACE INTO item_query(item_id, query) VALUES ((?), (?))",
)?;
stmt.execute(params![id, query])?;
}
_ => {}
}
tx.commit()?;
let mut stmt = lock.prepare_cached("SELECT id, kind FROM items")?;
let _ = stmt
.query_map([], |row| {
let zero: usize = row.get(0)?;
let one: String = row.get(1)?;
dbg!(zero, one);
Ok(())
})?
.collect::<Vec<Result<(), _>>>();
Ok(())
})
.unwrap_or(Ok(()))
}
fn delete_item(&self, item_id: usize) -> Result<()> {
self.real()
.map(|db| {
let lock = db.connection.lock();
let mut stmt = lock.prepare_cached(
r#"
DELETE FROM items WHERE id = (:id);
DELETE FROM item_path WHERE id = (:id);
DELETE FROM item_query WHERE id = (:id);
"#,
)?;
stmt.execute(named_params! {":id": item_id})?;
Ok(())
})
.unwrap_or(Ok(()))
}
fn take_items(&self) -> Result<HashSet<SerializedItem>> {
self.real()
.map(|db| {
let mut lock = db.connection.lock();
let tx = lock.transaction()?;
// When working with transactions in rusqlite, need to make this kind of scope
// To make the borrow stuff work correctly. Don't know why, rust is wild.
let result = {
let mut editors_stmt = tx.prepare_cached(
r#"
SELECT items.id, item_path.path
FROM items
LEFT JOIN item_path
ON items.id = item_path.item_id
WHERE items.kind = ?;
"#,
)?;
let editors_iter = editors_stmt.query_map(
[SerializedItemKind::Editor.to_string()],
|row| {
let id: usize = row.get(0)?;
let buf: Vec<u8> = row.get(1)?;
let path: PathBuf = OsStr::from_bytes(&buf).into();
Ok(SerializedItem::Editor(id, path))
},
)?;
let mut terminals_stmt = tx.prepare_cached(
r#"
SELECT items.id
FROM items
WHERE items.kind = ?;
"#,
)?;
let terminals_iter = terminals_stmt.query_map(
[SerializedItemKind::Terminal.to_string()],
|row| {
let id: usize = row.get(0)?;
Ok(SerializedItem::Terminal(id))
},
)?;
let mut search_stmt = tx.prepare_cached(
r#"
SELECT items.id, item_query.query
FROM items
LEFT JOIN item_query
ON items.id = item_query.item_id
WHERE items.kind = ?;
"#,
)?;
let searches_iter = search_stmt.query_map(
[SerializedItemKind::ProjectSearch.to_string()],
|row| {
let id: usize = row.get(0)?;
let query = row.get(1)?;
Ok(SerializedItem::ProjectSearch(id, query))
},
)?;
#[cfg(debug_assertions)]
let tmp =
searches_iter.collect::<Vec<Result<SerializedItem, rusqlite::Error>>>();
#[cfg(debug_assertions)]
debug_assert!(tmp.len() == 0 || tmp.len() == 1);
#[cfg(debug_assertions)]
let searches_iter = tmp.into_iter();
let mut diagnostic_stmt = tx.prepare_cached(
r#"
SELECT items.id
FROM items
WHERE items.kind = ?;
"#,
)?;
let diagnostics_iter = diagnostic_stmt.query_map(
[SerializedItemKind::Diagnostics.to_string()],
|row| {
let id: usize = row.get(0)?;
Ok(SerializedItem::Diagnostics(id))
},
)?;
#[cfg(debug_assertions)]
let tmp =
diagnostics_iter.collect::<Vec<Result<SerializedItem, rusqlite::Error>>>();
#[cfg(debug_assertions)]
debug_assert!(tmp.len() == 0 || tmp.len() == 1);
#[cfg(debug_assertions)]
let diagnostics_iter = tmp.into_iter();
let res = editors_iter
.chain(terminals_iter)
.chain(diagnostics_iter)
.chain(searches_iter)
.collect::<Result<HashSet<SerializedItem>, rusqlite::Error>>()?;
let mut delete_stmt = tx.prepare_cached(
r#"
DELETE FROM items;
DELETE FROM item_path;
DELETE FROM item_query;
"#,
)?;
delete_stmt.execute([])?;
res
};
tx.commit()?;
Ok(result)
})
.unwrap_or(Ok(HashSet::default()))
}
}
#[cfg(test)]
mod test {
use anyhow::Result;
use super::*;
#[test]
fn test_items_round_trip() -> Result<()> {
let db = Db::open_in_memory();
let mut items = vec![
SerializedItem::Editor(0, PathBuf::from("/tmp/test.txt")),
SerializedItem::Terminal(1),
SerializedItem::ProjectSearch(2, "Test query!".to_string()),
SerializedItem::Diagnostics(3),
]
.into_iter()
.collect::<HashSet<_>>();
for item in items.iter() {
dbg!("Inserting... ");
db.write_item(item.clone())?;
}
assert_eq!(items, db.take_items()?);
// Check that it's empty, as expected
assert_eq!(HashSet::default(), db.take_items()?);
for item in items.iter() {
db.write_item(item.clone())?;
}
items.remove(&SerializedItem::ProjectSearch(2, "Test query!".to_string()));
db.delete_item(2)?;
assert_eq!(items, db.take_items()?);
Ok(())
}
}

View File

@@ -1,82 +1,62 @@
use anyhow::Result;
use rusqlite::OptionalExtension;
use sqlez_macros::sql;
use super::Db;
use crate::{define_connection, query};
pub(crate) const KVP_M_1_UP: &str = "
CREATE TABLE kv_store(
key TEXT PRIMARY KEY,
value TEXT NOT NULL
) STRICT;
";
define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
&[sql!(
CREATE TABLE IF NOT EXISTS kv_store(
key TEXT PRIMARY KEY,
value TEXT NOT NULL
) STRICT;
)];
);
impl Db {
pub fn read_kvp(&self, key: &str) -> Result<Option<String>> {
self.real()
.map(|db| {
let lock = db.connection.lock();
let mut stmt = lock.prepare_cached("SELECT value FROM kv_store WHERE key = (?)")?;
Ok(stmt.query_row([key], |row| row.get(0)).optional()?)
})
.unwrap_or(Ok(None))
impl KeyValueStore {
query! {
pub fn read_kvp(key: &str) -> Result<Option<String>> {
SELECT value FROM kv_store WHERE key = (?)
}
}
pub fn write_kvp(&self, key: &str, value: &str) -> Result<()> {
self.real()
.map(|db| {
let lock = db.connection.lock();
let mut stmt = lock.prepare_cached(
"INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?))",
)?;
stmt.execute([key, value])?;
Ok(())
})
.unwrap_or(Ok(()))
query! {
pub async fn write_kvp(key: String, value: String) -> Result<()> {
INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?))
}
}
pub fn delete_kvp(&self, key: &str) -> Result<()> {
self.real()
.map(|db| {
let lock = db.connection.lock();
let mut stmt = lock.prepare_cached("DELETE FROM kv_store WHERE key = (?)")?;
stmt.execute([key])?;
Ok(())
})
.unwrap_or(Ok(()))
query! {
pub async fn delete_kvp(key: String) -> Result<()> {
DELETE FROM kv_store WHERE key = (?)
}
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use crate::kvp::KeyValueStore;
use super::*;
#[gpui::test]
async fn test_kvp() {
let db = KeyValueStore(crate::open_test_db("test_kvp").await);
#[test]
fn test_kvp() -> Result<()> {
let db = Db::open_in_memory();
assert_eq!(db.read_kvp("key-1").unwrap(), None);
assert_eq!(db.read_kvp("key-1")?, None);
db.write_kvp("key-1".to_string(), "one".to_string())
.await
.unwrap();
assert_eq!(db.read_kvp("key-1").unwrap(), Some("one".to_string()));
db.write_kvp("key-1", "one")?;
assert_eq!(db.read_kvp("key-1")?, Some("one".to_string()));
db.write_kvp("key-1".to_string(), "one-2".to_string())
.await
.unwrap();
assert_eq!(db.read_kvp("key-1").unwrap(), Some("one-2".to_string()));
db.write_kvp("key-1", "one-2")?;
assert_eq!(db.read_kvp("key-1")?, Some("one-2".to_string()));
db.write_kvp("key-2".to_string(), "two".to_string())
.await
.unwrap();
assert_eq!(db.read_kvp("key-2").unwrap(), Some("two".to_string()));
db.write_kvp("key-2", "two")?;
assert_eq!(db.read_kvp("key-2")?, Some("two".to_string()));
db.delete_kvp("key-1")?;
assert_eq!(db.read_kvp("key-1")?, None);
Ok(())
db.delete_kvp("key-1".to_string()).await.unwrap();
assert_eq!(db.read_kvp("key-1").unwrap(), None);
}
}

View File

@@ -1,15 +0,0 @@
use rusqlite_migration::{Migrations, M};
// use crate::items::ITEMS_M_1;
use crate::kvp::KVP_M_1_UP;
// This must be ordered by development time! Only ever add new migrations to the end!!
// Bad things will probably happen if you don't monotonically edit this vec!!!!
// And no re-ordering ever!!!!!!!!!! The results of these migrations are on the user's
// file system and so everything we do here is locked in _f_o_r_e_v_e_r_.
lazy_static::lazy_static! {
pub static ref MIGRATIONS: Migrations<'static> = Migrations::new(vec![
M::up(KVP_M_1_UP),
// M::up(ITEMS_M_1),
]);
}

314
crates/db/src/query.rs Normal file
View File

@@ -0,0 +1,314 @@
#[macro_export]
macro_rules! query {
($vis:vis fn $id:ident() -> Result<()> { $($sql:tt)+ }) => {
$vis fn $id(&self) -> $crate::anyhow::Result<()> {
use $crate::anyhow::Context;
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
self.exec(sql_stmt)?().context(::std::format!(
"Error in {}, exec failed to execute or parse for: {}",
::std::stringify!($id),
sql_stmt,
))
}
};
($vis:vis async fn $id:ident() -> Result<()> { $($sql:tt)+ }) => {
$vis async fn $id(&self) -> $crate::anyhow::Result<()> {
use $crate::anyhow::Context;
self.write(|connection| {
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
connection.exec(sql_stmt)?().context(::std::format!(
"Error in {}, exec failed to execute or parse for: {}",
::std::stringify!($id),
sql_stmt
))
}).await
}
};
($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<()> { $($sql:tt)+ }) => {
$vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<()> {
use $crate::anyhow::Context;
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
self.exec_bound::<($($arg_type),+)>(sql_stmt)?(($($arg),+))
.context(::std::format!(
"Error in {}, exec_bound failed to execute or parse for: {}",
::std::stringify!($id),
sql_stmt
))
}
};
($vis:vis async fn $id:ident($arg:ident: $arg_type:ty) -> Result<()> { $($sql:tt)+ }) => {
$vis async fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<()> {
use $crate::anyhow::Context;
self.write(move |connection| {
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
connection.exec_bound::<$arg_type>(sql_stmt)?($arg)
.context(::std::format!(
"Error in {}, exec_bound failed to execute or parse for: {}",
::std::stringify!($id),
sql_stmt
))
}).await
}
};
($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<()> { $($sql:tt)+ }) => {
$vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<()> {
use $crate::anyhow::Context;
self.write(move |connection| {
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
connection.exec_bound::<($($arg_type),+)>(sql_stmt)?(($($arg),+))
.context(::std::format!(
"Error in {}, exec_bound failed to execute or parse for: {}",
::std::stringify!($id),
sql_stmt
))
}).await
}
};
($vis:vis fn $id:ident() -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
$vis fn $id(&self) -> $crate::anyhow::Result<Vec<$return_type>> {
use $crate::anyhow::Context;
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
self.select::<$return_type>(sql_stmt)?(())
.context(::std::format!(
"Error in {}, select_row failed to execute or parse for: {}",
::std::stringify!($id),
sql_stmt
))
}
};
($vis:vis async fn $id:ident() -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
pub async fn $id(&self) -> $crate::anyhow::Result<Vec<$return_type>> {
use $crate::anyhow::Context;
self.write(|connection| {
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
connection.select::<$return_type>(sql_stmt)?(())
.context(::std::format!(
"Error in {}, select_row failed to execute or parse for: {}",
::std::stringify!($id),
sql_stmt
))
}).await
}
};
($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
$vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Vec<$return_type>> {
use $crate::anyhow::Context;
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
self.select_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
.context(::std::format!(
"Error in {}, exec_bound failed to execute or parse for: {}",
::std::stringify!($id),
sql_stmt
))
}
};
($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
$vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Vec<$return_type>> {
use $crate::anyhow::Context;
self.write(|connection| {
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
connection.select_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
.context(::std::format!(
"Error in {}, exec_bound failed to execute or parse for: {}",
::std::stringify!($id),
sql_stmt
))
}).await
}
};
($vis:vis fn $id:ident() -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
$vis fn $id(&self) -> $crate::anyhow::Result<Option<$return_type>> {
use $crate::anyhow::Context;
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
self.select_row::<$return_type>(sql_stmt)?()
.context(::std::format!(
"Error in {}, select_row failed to execute or parse for: {}",
::std::stringify!($id),
sql_stmt
))
}
};
($vis:vis async fn $id:ident() -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
$vis async fn $id(&self) -> $crate::anyhow::Result<Option<$return_type>> {
use $crate::anyhow::Context;
self.write(|connection| {
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
connection.select_row::<$return_type>(sql_stmt)?()
.context(::std::format!(
"Error in {}, select_row failed to execute or parse for: {}",
::std::stringify!($id),
sql_stmt
))
}).await
}
};
($vis:vis fn $id:ident($arg:ident: $arg_type:ty) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
$vis fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<Option<$return_type>> {
use $crate::anyhow::Context;
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
self.select_row_bound::<$arg_type, $return_type>(sql_stmt)?($arg)
.context(::std::format!(
"Error in {}, select_row_bound failed to execute or parse for: {}",
::std::stringify!($id),
sql_stmt
))
}
};
($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
$vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Option<$return_type>> {
use $crate::anyhow::Context;
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
self.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
.context(::std::format!(
"Error in {}, select_row_bound failed to execute or parse for: {}",
::std::stringify!($id),
sql_stmt
))
}
};
($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
$vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Option<$return_type>> {
use $crate::anyhow::Context;
self.write(|connection| {
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
connection.select_row_bound::<($($arg_type),+), $return_type>(indoc! { $sql })?(($($arg),+))
.context(::std::format!(
"Error in {}, select_row_bound failed to execute or parse for: {}",
::std::stringify!($id),
sql_stmt
))
}).await
}
};
($vis:vis fn $id:ident() -> Result<$return_type:ty> { $($sql:tt)+ }) => {
$vis fn $id(&self) -> $crate::anyhow::Result<$return_type> {
use $crate::anyhow::Context;
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
self.select_row::<$return_type>(indoc! { $sql })?()
.context(::std::format!(
"Error in {}, select_row_bound failed to execute or parse for: {}",
::std::stringify!($id),
sql_stmt
))?
.context(::std::format!(
"Error in {}, select_row_bound expected single row result but found none for: {}",
::std::stringify!($id),
sql_stmt
))
}
};
($vis:vis async fn $id:ident() -> Result<$return_type:ty> { $($sql:tt)+ }) => {
$vis async fn $id(&self) -> $crate::anyhow::Result<$return_type> {
use $crate::anyhow::Context;
self.write(|connection| {
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
connection.select_row::<$return_type>(sql_stmt)?()
.context(::std::format!(
"Error in {}, select_row_bound failed to execute or parse for: {}",
::std::stringify!($id),
sql_stmt
))?
.context(::std::format!(
"Error in {}, select_row_bound expected single row result but found none for: {}",
::std::stringify!($id),
sql_stmt
))
}).await
}
};
($vis:vis fn $id:ident($arg:ident: $arg_type:ty) -> Result<$return_type:ty> { $($sql:tt)+ }) => {
pub fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<$return_type> {
use $crate::anyhow::Context;
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
self.select_row_bound::<$arg_type, $return_type>(sql_stmt)?($arg)
.context(::std::format!(
"Error in {}, select_row_bound failed to execute or parse for: {}",
::std::stringify!($id),
sql_stmt
))?
.context(::std::format!(
"Error in {}, select_row_bound expected single row result but found none for: {}",
::std::stringify!($id),
sql_stmt
))
}
};
($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<$return_type:ty> { $($sql:tt)+ }) => {
$vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<$return_type> {
use $crate::anyhow::Context;
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
self.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
.context(::std::format!(
"Error in {}, select_row_bound failed to execute or parse for: {}",
::std::stringify!($id),
sql_stmt
))?
.context(::std::format!(
"Error in {}, select_row_bound expected single row result but found none for: {}",
::std::stringify!($id),
sql_stmt
))
}
};
($vis:vis fn async $id:ident($($arg:ident: $arg_type:ty),+) -> Result<$return_type:ty> { $($sql:tt)+ }) => {
$vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<$return_type> {
use $crate::anyhow::Context;
self.write(|connection| {
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
connection.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
.context(::std::format!(
"Error in {}, select_row_bound failed to execute or parse for: {}",
::std::stringify!($id),
sql_stmt
))?
.context(::std::format!(
"Error in {}, select_row_bound expected single row result but found none for: {}",
::std::stringify!($id),
sql_stmt
))
}).await
}
};
}

View File

@@ -29,7 +29,10 @@ use std::{
sync::Arc,
};
use util::TryFutureExt;
use workspace::{ItemHandle as _, ItemNavHistory, Workspace};
use workspace::{
item::{Item, ItemEvent, ItemHandle},
ItemNavHistory, Pane, Workspace,
};
actions!(diagnostics, [Deploy]);
@@ -322,7 +325,7 @@ impl ProjectDiagnosticsEditor {
);
let excerpt_id = excerpts
.insert_excerpts_after(
&prev_excerpt_id,
prev_excerpt_id,
buffer.clone(),
[ExcerptRange {
context: excerpt_start..excerpt_end,
@@ -384,7 +387,7 @@ impl ProjectDiagnosticsEditor {
groups_to_add.push(group_state);
} else if let Some((group_ix, group_state)) = to_remove {
excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
excerpts.remove_excerpts(group_state.excerpts.iter().copied(), excerpts_cx);
group_ixs_to_remove.push(group_ix);
blocks_to_remove.extend(group_state.blocks.iter().copied());
} else if let Some((_, group)) = to_keep {
@@ -457,10 +460,15 @@ impl ProjectDiagnosticsEditor {
}
// If any selection has lost its position, move it to start of the next primary diagnostic.
let snapshot = editor.snapshot(cx);
for selection in &mut selections {
if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
let group_ix = match groups.binary_search_by(|probe| {
probe.excerpts.last().unwrap().cmp(new_excerpt_id)
probe
.excerpts
.last()
.unwrap()
.cmp(new_excerpt_id, &snapshot.buffer_snapshot)
}) {
Ok(ix) | Err(ix) => ix,
};
@@ -498,7 +506,7 @@ impl ProjectDiagnosticsEditor {
}
}
impl workspace::Item for ProjectDiagnosticsEditor {
impl Item for ProjectDiagnosticsEditor {
fn tab_content(
&self,
_detail: Option<usize>,
@@ -566,7 +574,7 @@ impl workspace::Item for ProjectDiagnosticsEditor {
unreachable!()
}
fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
Editor::to_item_events(event)
}
@@ -576,7 +584,11 @@ impl workspace::Item for ProjectDiagnosticsEditor {
});
}
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
fn clone_on_split(
&self,
_workspace_id: workspace::WorkspaceId,
cx: &mut ViewContext<Self>,
) -> Option<Self>
where
Self: Sized,
{
@@ -605,6 +617,20 @@ impl workspace::Item for ProjectDiagnosticsEditor {
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |editor, cx| editor.deactivated(cx));
}
fn serialized_item_kind() -> Option<&'static str> {
Some("diagnostics")
}
fn deserialize(
project: ModelHandle<Project>,
workspace: WeakViewHandle<Workspace>,
_workspace_id: workspace::WorkspaceId,
_item_id: workspace::ItemId,
cx: &mut ViewContext<Pane>,
) -> Task<Result<ViewHandle<Self>>> {
Task::ready(Ok(cx.add_view(|cx| Self::new(project, workspace, cx))))
}
}
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
@@ -738,7 +764,7 @@ mod tests {
DisplayPoint,
};
use gpui::TestAppContext;
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
use serde_json::json;
use unindent::Unindent as _;
use workspace::AppState;
@@ -776,8 +802,15 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
let (_, workspace) =
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
let (_, workspace) = cx.add_window(|cx| {
Workspace::new(
Default::default(),
0,
project.clone(),
|_, _| unimplemented!(),
cx,
)
});
// Create some diagnostics
project.update(cx, |project, cx| {
@@ -788,7 +821,7 @@ mod tests {
None,
vec![
DiagnosticEntry {
range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
diagnostic: Diagnostic {
message:
"move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
@@ -801,7 +834,7 @@ mod tests {
},
},
DiagnosticEntry {
range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
diagnostic: Diagnostic {
message:
"move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
@@ -814,7 +847,7 @@ mod tests {
},
},
DiagnosticEntry {
range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
diagnostic: Diagnostic {
message: "value moved here".to_string(),
severity: DiagnosticSeverity::INFORMATION,
@@ -825,7 +858,7 @@ mod tests {
},
},
DiagnosticEntry {
range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
diagnostic: Diagnostic {
message: "value moved here".to_string(),
severity: DiagnosticSeverity::INFORMATION,
@@ -836,7 +869,7 @@ mod tests {
},
},
DiagnosticEntry {
range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
diagnostic: Diagnostic {
message: "use of moved value\nvalue used here after move".to_string(),
severity: DiagnosticSeverity::ERROR,
@@ -847,7 +880,7 @@ mod tests {
},
},
DiagnosticEntry {
range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
diagnostic: Diagnostic {
message: "use of moved value\nvalue used here after move".to_string(),
severity: DiagnosticSeverity::ERROR,
@@ -939,7 +972,7 @@ mod tests {
PathBuf::from("/test/consts.rs"),
None,
vec![DiagnosticEntry {
range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
diagnostic: Diagnostic {
message: "mismatched types\nexpected `usize`, found `char`".to_string(),
severity: DiagnosticSeverity::ERROR,
@@ -1040,7 +1073,8 @@ mod tests {
None,
vec![
DiagnosticEntry {
range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
range: Unclipped(PointUtf16::new(0, 15))
..Unclipped(PointUtf16::new(0, 15)),
diagnostic: Diagnostic {
message: "mismatched types\nexpected `usize`, found `char`"
.to_string(),
@@ -1052,7 +1086,8 @@ mod tests {
},
},
DiagnosticEntry {
range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
range: Unclipped(PointUtf16::new(1, 15))
..Unclipped(PointUtf16::new(1, 15)),
diagnostic: Diagnostic {
message: "unresolved name `c`".to_string(),
severity: DiagnosticSeverity::ERROR,

View File

@@ -7,7 +7,7 @@ use gpui::{
use language::Diagnostic;
use project::Project;
use settings::Settings;
use workspace::StatusItemView;
use workspace::{item::ItemHandle, StatusItemView};
pub struct DiagnosticIndicator {
summary: project::DiagnosticSummary,
@@ -219,7 +219,7 @@ impl View for DiagnosticIndicator {
impl StatusItemView for DiagnosticIndicator {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn workspace::ItemHandle>,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) {
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {

View File

@@ -9,11 +9,17 @@ use gpui::{
View, WeakViewHandle,
};
const DEAD_ZONE: f32 = 4.;
enum State<V: View> {
Down {
region_offset: Vector2F,
region: RectF,
},
DeadZone {
region_offset: Vector2F,
region: RectF,
},
Dragging {
window_id: usize,
position: Vector2F,
@@ -35,6 +41,13 @@ impl<V: View> Clone for State<V> {
region_offset,
region,
},
&State::DeadZone {
region_offset,
region,
} => State::DeadZone {
region_offset,
region,
},
State::Dragging {
window_id,
position,
@@ -101,7 +114,7 @@ impl<V: View> DragAndDrop<V> {
pub fn drag_started(event: MouseDown, cx: &mut EventContext) {
cx.update_global(|this: &mut Self, _| {
this.currently_dragged = Some(State::Down {
region_offset: event.region.origin() - event.position,
region_offset: event.position - event.region.origin(),
region: event.region,
});
})
@@ -122,7 +135,31 @@ impl<V: View> DragAndDrop<V> {
region_offset,
region,
})
| Some(&State::Dragging {
| Some(&State::DeadZone {
region_offset,
region,
}) => {
if (dbg!(event.position) - (dbg!(region.origin() + region_offset))).length()
> DEAD_ZONE
{
this.currently_dragged = Some(State::Dragging {
window_id,
region_offset,
region,
position: event.position,
payload,
render: Rc::new(move |payload, cx| {
render(payload.downcast_ref::<T>().unwrap(), cx)
}),
});
} else {
this.currently_dragged = Some(State::DeadZone {
region_offset,
region,
})
}
}
Some(&State::Dragging {
region_offset,
region,
..
@@ -151,6 +188,7 @@ impl<V: View> DragAndDrop<V> {
.and_then(|state| {
match state {
State::Down { .. } => None,
State::DeadZone { .. } => None,
State::Dragging {
window_id,
region_offset,
@@ -163,7 +201,7 @@ impl<V: View> DragAndDrop<V> {
return None;
}
let position = position + region_offset;
let position = position - region_offset;
Some(
Overlay::new(
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {

View File

@@ -23,6 +23,7 @@ test-support = [
drag_and_drop = { path = "../drag_and_drop" }
text = { path = "../text" }
clock = { path = "../clock" }
db = { path = "../db" }
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
fuzzy = { path = "../fuzzy" }
@@ -37,6 +38,7 @@ snippet = { path = "../snippet" }
sum_tree = { path = "../sum_tree" }
theme = { path = "../theme" }
util = { path = "../util" }
sqlez = { path = "../sqlez" }
workspace = { path = "../workspace" }
aho-corasick = "0.7"
anyhow = "1.0"

View File

@@ -2,7 +2,7 @@ use super::{
wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
TextHighlights,
};
use crate::{Anchor, ExcerptRange, ToPoint as _};
use crate::{Anchor, ExcerptId, ExcerptRange, ToPoint as _};
use collections::{Bound, HashMap, HashSet};
use gpui::{ElementBox, RenderContext};
use language::{BufferSnapshot, Chunk, Patch, Point};
@@ -107,7 +107,7 @@ struct Transform {
pub enum TransformBlock {
Custom(Arc<Block>),
ExcerptHeader {
key: usize,
id: ExcerptId,
buffer: BufferSnapshot,
range: ExcerptRange<text::Anchor>,
height: u8,
@@ -371,7 +371,7 @@ impl BlockMap {
.make_wrap_point(Point::new(excerpt_boundary.row, 0), Bias::Left)
.row(),
TransformBlock::ExcerptHeader {
key: excerpt_boundary.key,
id: excerpt_boundary.id,
buffer: excerpt_boundary.buffer,
range: excerpt_boundary.range,
height: if excerpt_boundary.starts_new_buffer {

View File

@@ -9,6 +9,7 @@ mod link_go_to_definition;
mod mouse_context_menu;
pub mod movement;
mod multi_buffer;
mod persistence;
pub mod selections_collection;
#[cfg(test)]
@@ -80,7 +81,7 @@ use std::{
pub use sum_tree::Bias;
use theme::{DiagnosticStyle, Theme};
use util::{post_inc, ResultExt, TryFutureExt};
use workspace::{ItemNavHistory, Workspace};
use workspace::{ItemNavHistory, Workspace, WorkspaceId};
use crate::git::diff_hunk_to_display;
@@ -372,6 +373,7 @@ pub fn init(cx: &mut MutableAppContext) {
workspace::register_project_item::<Editor>(cx);
workspace::register_followable_item::<Editor>(cx);
workspace::register_deserializable_item::<Editor>(cx);
}
trait InvalidationRegion {
@@ -582,6 +584,7 @@ pub struct Editor {
pending_rename: Option<RenameState>,
searchable: bool,
cursor_shape: CursorShape,
workspace_id: Option<WorkspaceId>,
keymap_context_layers: BTreeMap<TypeId, gpui::keymap::Context>,
input_enabled: bool,
leader_replica_id: Option<u16>,
@@ -1162,7 +1165,7 @@ impl Editor {
});
clone.selections.set_state(&self.selections);
clone.scroll_position = self.scroll_position;
clone.scroll_top_anchor = self.scroll_top_anchor.clone();
clone.scroll_top_anchor = self.scroll_top_anchor;
clone.searchable = self.searchable;
clone
}
@@ -1235,6 +1238,7 @@ impl Editor {
searchable: true,
override_text_style: None,
cursor_shape: Default::default(),
workspace_id: None,
keymap_context_layers: Default::default(),
input_enabled: true,
leader_replica_id: None,
@@ -1305,7 +1309,7 @@ impl Editor {
display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
ongoing_scroll: self.ongoing_scroll,
scroll_position: self.scroll_position,
scroll_top_anchor: self.scroll_top_anchor.clone(),
scroll_top_anchor: self.scroll_top_anchor,
placeholder_text: self.placeholder_text.clone(),
is_focused: self
.handle
@@ -1791,17 +1795,15 @@ impl Editor {
.pending_anchor()
.expect("extend_selection not called with pending selection");
if position >= tail {
pending_selection.start = tail_anchor.clone();
pending_selection.start = tail_anchor;
} else {
pending_selection.end = tail_anchor.clone();
pending_selection.end = tail_anchor;
pending_selection.reversed = true;
}
let mut pending_mode = self.selections.pending_mode().unwrap();
match &mut pending_mode {
SelectMode::Word(range) | SelectMode::Line(range) => {
*range = tail_anchor.clone()..tail_anchor
}
SelectMode::Word(range) | SelectMode::Line(range) => *range = tail_anchor..tail_anchor,
_ => {}
}
@@ -2145,10 +2147,9 @@ impl Editor {
));
if following_text_allows_autoclose && preceding_text_matches_prefix {
let anchor = snapshot.anchor_before(selection.end);
new_selections
.push((selection.map(|_| anchor.clone()), text.len()));
new_selections.push((selection.map(|_| anchor), text.len()));
new_autoclose_regions.push((
anchor.clone(),
anchor,
text.len(),
selection.id,
bracket_pair.clone(),
@@ -2169,10 +2170,8 @@ impl Editor {
&& text.as_ref() == region.pair.end.as_str();
if should_skip {
let anchor = snapshot.anchor_after(selection.end);
new_selections.push((
selection.map(|_| anchor.clone()),
region.pair.end.len(),
));
new_selections
.push((selection.map(|_| anchor), region.pair.end.len()));
continue;
}
}
@@ -2204,7 +2203,7 @@ impl Editor {
// text with the given input and move the selection to the end of the
// newly inserted text.
let anchor = snapshot.anchor_after(selection.end);
new_selections.push((selection.map(|_| anchor.clone()), 0));
new_selections.push((selection.map(|_| anchor), 0));
edits.push((selection.start..selection.end, text.clone()));
}
@@ -2306,7 +2305,7 @@ impl Editor {
}
let anchor = buffer.anchor_after(end);
let new_selection = selection.map(|_| anchor.clone());
let new_selection = selection.map(|_| anchor);
(
(start..end, new_text),
(insert_extra_newline, new_selection),
@@ -2386,7 +2385,7 @@ impl Editor {
.iter()
.map(|s| {
let anchor = snapshot.anchor_after(s.end);
s.map(|_| anchor.clone())
s.map(|_| anchor)
})
.collect::<Vec<_>>()
};
@@ -3650,7 +3649,7 @@ impl Editor {
String::new(),
));
let insertion_anchor = buffer.anchor_after(insertion_point);
edits.push((insertion_anchor.clone()..insertion_anchor, text));
edits.push((insertion_anchor..insertion_anchor, text));
let row_delta = range_to_move.start.row - insertion_point.row + 1;
@@ -3755,7 +3754,7 @@ impl Editor {
String::new(),
));
let insertion_anchor = buffer.anchor_after(insertion_point);
edits.push((insertion_anchor.clone()..insertion_anchor, text));
edits.push((insertion_anchor..insertion_anchor, text));
let row_delta = insertion_point.row - range_to_move.end.row + 1;
@@ -4625,7 +4624,7 @@ impl Editor {
cursor_anchor: position,
cursor_position: point,
scroll_position: self.scroll_position,
scroll_top_anchor: self.scroll_top_anchor.clone(),
scroll_top_anchor: self.scroll_top_anchor,
scroll_top_row,
}),
cx,

View File

@@ -22,7 +22,10 @@ use util::{
assert_set_eq,
test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
};
use workspace::{FollowableItem, ItemHandle, NavigationEntry, Pane};
use workspace::{
item::{FollowableItem, ItemHandle},
NavigationEntry, Pane,
};
#[gpui::test]
fn test_edit_events(cx: &mut MutableAppContext) {
@@ -475,7 +478,7 @@ fn test_clone(cx: &mut gpui::MutableAppContext) {
fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
cx.set_global(DragAndDrop::<Workspace>::default());
use workspace::Item;
use workspace::item::Item;
let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(None, cx));
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
@@ -542,7 +545,7 @@ fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
// Set scroll position to check later
editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx);
let original_scroll_position = editor.scroll_position;
let original_scroll_top_anchor = editor.scroll_top_anchor.clone();
let original_scroll_top_anchor = editor.scroll_top_anchor;
// Jump to the end of the document and adjust scroll
editor.move_to_end(&MoveToEnd, cx);
@@ -556,12 +559,12 @@ fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
assert_eq!(editor.scroll_top_anchor, original_scroll_top_anchor);
// Ensure we don't panic when navigation data contains invalid anchors *and* points.
let mut invalid_anchor = editor.scroll_top_anchor.clone();
let mut invalid_anchor = editor.scroll_top_anchor;
invalid_anchor.text_anchor.buffer_id = Some(999);
let invalid_point = Point::new(9999, 0);
editor.navigate(
Box::new(NavigationData {
cursor_anchor: invalid_anchor.clone(),
cursor_anchor: invalid_anchor,
cursor_position: invalid_point,
scroll_top_anchor: invalid_anchor,
scroll_top_row: invalid_point.row,
@@ -4718,9 +4721,7 @@ fn test_refresh_selections(cx: &mut gpui::MutableAppContext) {
// Refreshing selections is a no-op when excerpts haven't changed.
editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| {
s.refresh();
});
editor.change_selections(None, cx, |s| s.refresh());
assert_eq!(
editor.selections.ranges(cx),
[
@@ -4731,7 +4732,7 @@ fn test_refresh_selections(cx: &mut gpui::MutableAppContext) {
});
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.remove_excerpts([&excerpt1_id.unwrap()], cx);
multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
});
editor.update(cx, |editor, cx| {
// Removing an excerpt causes the first selection to become degenerate.
@@ -4745,9 +4746,7 @@ fn test_refresh_selections(cx: &mut gpui::MutableAppContext) {
// Refreshing selections will relocate the first selection to the original buffer
// location.
editor.change_selections(None, cx, |s| {
s.refresh();
});
editor.change_selections(None, cx, |s| s.refresh());
assert_eq!(
editor.selections.ranges(cx),
[
@@ -4801,7 +4800,7 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut gpui::MutableAppC
});
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.remove_excerpts([&excerpt1_id.unwrap()], cx);
multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
});
editor.update(cx, |editor, cx| {
assert_eq!(
@@ -4810,9 +4809,7 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut gpui::MutableAppC
);
// Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
editor.change_selections(None, cx, |s| {
s.refresh();
});
editor.change_selections(None, cx, |s| s.refresh());
assert_eq!(
editor.selections.ranges(cx),
[Point::new(0, 3)..Point::new(0, 3)]

View File

@@ -1334,12 +1334,13 @@ impl EditorElement {
})
}
TransformBlock::ExcerptHeader {
key,
id,
buffer,
range,
starts_new_buffer,
..
} => {
let id = *id;
let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
let jump_position = range
.primary
@@ -1356,7 +1357,7 @@ impl EditorElement {
enum JumpIcon {}
cx.render(&editor, |_, cx| {
MouseEventHandler::<JumpIcon>::new(*key, cx, |state, _| {
MouseEventHandler::<JumpIcon>::new(id.into(), cx, |state, _| {
let style = style.jump_icon.style_for(state, false);
Svg::new("icons/arrow_up_right_8.svg")
.with_color(style.color)
@@ -1375,7 +1376,7 @@ impl EditorElement {
cx.dispatch_action(jump_action.clone())
})
.with_tooltip::<JumpIcon, _>(
*key,
id.into(),
"Jump to Buffer".to_string(),
Some(Box::new(crate::OpenExcerpts)),
tooltip_style.clone(),
@@ -1606,16 +1607,13 @@ impl Element for EditorElement {
highlighted_rows = view.highlighted_rows();
let theme = cx.global::<Settings>().theme.as_ref();
highlighted_ranges = view.background_highlights_in_range(
start_anchor.clone()..end_anchor.clone(),
&display_map,
theme,
);
highlighted_ranges =
view.background_highlights_in_range(start_anchor..end_anchor, &display_map, theme);
let mut remote_selections = HashMap::default();
for (replica_id, line_mode, cursor_shape, selection) in display_map
.buffer_snapshot
.remote_selections_in_range(&(start_anchor.clone()..end_anchor.clone()))
.remote_selections_in_range(&(start_anchor..end_anchor))
{
// The local selections match the leader's selections.
if Some(replica_id) == view.leader_replica_id {

View File

@@ -221,7 +221,7 @@ fn show_hover(
start..end
} else {
anchor.clone()..anchor.clone()
anchor..anchor
};
Some(InfoPopover {

View File

@@ -1,13 +1,8 @@
use crate::{
display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
movement::surrounding_word, Anchor, Autoscroll, Editor, Event, ExcerptId, MultiBuffer,
MultiBufferSnapshot, NavigationData, ToPoint as _, FORMAT_TIMEOUT,
};
use anyhow::{anyhow, Result};
use anyhow::{anyhow, Context, Result};
use futures::FutureExt;
use gpui::{
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use language::{Bias, Buffer, File as _, OffsetRangeExt, Point, SelectionGoal};
use project::{File, FormatTrigger, Project, ProjectEntryId, ProjectPath};
@@ -22,11 +17,17 @@ use std::{
path::{Path, PathBuf},
};
use text::Selection;
use util::TryFutureExt;
use util::{ResultExt, TryFutureExt};
use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
FollowableItem, Item, ItemEvent, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView,
ToolbarItemLocation,
ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, Workspace, WorkspaceId,
};
use crate::{
display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
movement::surrounding_word, persistence::DB, Anchor, Autoscroll, Editor, Event, ExcerptId,
MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, FORMAT_TIMEOUT,
};
pub const MAX_TAB_TITLE_LEN: usize = 24;
@@ -367,7 +368,7 @@ impl Item for Editor {
self.buffer.read(cx).is_singleton()
}
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
fn clone_on_split(&self, _workspace_id: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self>
where
Self: Sized,
{
@@ -490,7 +491,7 @@ impl Item for Editor {
Task::ready(Ok(()))
}
fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
let mut result = Vec::new();
match event {
Event::Closed => result.push(ItemEvent::CloseItem),
@@ -552,6 +553,87 @@ impl Item for Editor {
}));
Some(breadcrumbs)
}
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();
fn serialize(
buffer: ModelHandle<Buffer>,
workspace_id: WorkspaceId,
item_id: ItemId,
cx: &mut MutableAppContext,
) {
if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
let path = file.abs_path(cx);
cx.background()
.spawn(async move {
DB.save_path(item_id, workspace_id, path.clone())
.await
.log_err()
})
.detach();
}
}
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
serialize(buffer.clone(), workspace_id, item_id, cx);
cx.subscribe(&buffer, |this, buffer, event, cx| {
if let Some(workspace_id) = this.workspace_id {
if let language::Event::FileHandleChanged = event {
serialize(buffer, workspace_id, cx.view_id(), cx);
}
}
})
.detach();
}
}
fn serialized_item_kind() -> Option<&'static str> {
Some("Editor")
}
fn deserialize(
project: ModelHandle<Project>,
_workspace: WeakViewHandle<Workspace>,
workspace_id: workspace::WorkspaceId,
item_id: ItemId,
cx: &mut ViewContext<Pane>,
) -> Task<Result<ViewHandle<Self>>> {
let project_item: Result<_> = project.update(cx, |project, cx| {
// Look up the path with this key associated, create a self with that path
let path = DB
.get_path(item_id, workspace_id)?
.context("No path stored for this editor")?;
let (worktree, path) = project
.find_local_worktree(&path, cx)
.with_context(|| format!("No worktree for path: {path:?}"))?;
let project_path = ProjectPath {
worktree_id: worktree.read(cx).id(),
path: path.into(),
};
Ok(project.open_path(project_path, cx))
});
project_item
.map(|project_item| {
cx.spawn(|pane, mut cx| async move {
let (_, project_item) = project_item.await?;
let buffer = project_item
.downcast::<Buffer>()
.context("Project item at stored path was not a buffer")?;
Ok(cx.update(|cx| {
cx.add_view(pane, |cx| Editor::for_buffer(buffer, Some(project), cx))
}))
})
})
.unwrap_or_else(|error| Task::ready(Err(error)))
}
}
impl ProjectItem for Editor {

View File

@@ -11,7 +11,7 @@ use language::{
char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Outline,
OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _,
ToPoint as _, ToPointUtf16 as _, TransactionId,
ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
};
use smallvec::SmallVec;
use std::{
@@ -36,13 +36,13 @@ use util::post_inc;
const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
pub type ExcerptId = Locator;
#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct ExcerptId(usize);
pub struct MultiBuffer {
snapshot: RefCell<MultiBufferSnapshot>,
buffers: RefCell<HashMap<usize, BufferState>>,
used_excerpt_ids: SumTree<ExcerptId>,
next_excerpt_key: usize,
next_excerpt_id: usize,
subscriptions: Topic,
singleton: bool,
replica_id: ReplicaId,
@@ -92,7 +92,7 @@ struct BufferState {
last_diagnostics_update_count: usize,
last_file_update_count: usize,
last_git_diff_update_count: usize,
excerpts: Vec<ExcerptId>,
excerpts: Vec<Locator>,
_subscriptions: [gpui::Subscription; 2],
}
@@ -100,6 +100,7 @@ struct BufferState {
pub struct MultiBufferSnapshot {
singleton: bool,
excerpts: SumTree<Excerpt>,
excerpt_ids: SumTree<ExcerptIdMapping>,
parse_count: usize,
diagnostics_update_count: usize,
trailing_excerpt_update_count: usize,
@@ -111,7 +112,6 @@ pub struct MultiBufferSnapshot {
pub struct ExcerptBoundary {
pub id: ExcerptId,
pub key: usize,
pub row: u32,
pub buffer: BufferSnapshot,
pub range: ExcerptRange<text::Anchor>,
@@ -121,7 +121,7 @@ pub struct ExcerptBoundary {
#[derive(Clone)]
struct Excerpt {
id: ExcerptId,
key: usize,
locator: Locator,
buffer_id: usize,
buffer: BufferSnapshot,
range: ExcerptRange<text::Anchor>,
@@ -130,6 +130,12 @@ struct Excerpt {
has_trailing_newline: bool,
}
#[derive(Clone, Debug)]
struct ExcerptIdMapping {
id: ExcerptId,
locator: Locator,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ExcerptRange<T> {
pub context: Range<T>,
@@ -139,6 +145,7 @@ pub struct ExcerptRange<T> {
#[derive(Clone, Debug, Default)]
struct ExcerptSummary {
excerpt_id: ExcerptId,
excerpt_locator: Locator,
max_buffer_row: u32,
text: TextSummary,
}
@@ -178,8 +185,7 @@ impl MultiBuffer {
Self {
snapshot: Default::default(),
buffers: Default::default(),
used_excerpt_ids: Default::default(),
next_excerpt_key: Default::default(),
next_excerpt_id: 1,
subscriptions: Default::default(),
singleton: false,
replica_id,
@@ -218,8 +224,7 @@ impl MultiBuffer {
Self {
snapshot: RefCell::new(self.snapshot.borrow().clone()),
buffers: RefCell::new(buffers),
used_excerpt_ids: self.used_excerpt_ids.clone(),
next_excerpt_key: self.next_excerpt_key,
next_excerpt_id: 1,
subscriptions: Default::default(),
singleton: self.singleton,
replica_id: self.replica_id,
@@ -610,11 +615,14 @@ impl MultiBuffer {
let mut selections_by_buffer: HashMap<usize, Vec<Selection<text::Anchor>>> =
Default::default();
let snapshot = self.read(cx);
let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptId>>();
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
for selection in selections {
cursor.seek(&Some(&selection.start.excerpt_id), Bias::Left, &());
let start_locator = snapshot.excerpt_locator_for_id(selection.start.excerpt_id);
let end_locator = snapshot.excerpt_locator_for_id(selection.end.excerpt_id);
cursor.seek(&Some(start_locator), Bias::Left, &());
while let Some(excerpt) = cursor.item() {
if excerpt.id > selection.end.excerpt_id {
if excerpt.locator > *end_locator {
break;
}
@@ -745,7 +753,7 @@ impl MultiBuffer {
where
O: text::ToOffset,
{
self.insert_excerpts_after(&ExcerptId::max(), buffer, ranges, cx)
self.insert_excerpts_after(ExcerptId::max(), buffer, ranges, cx)
}
pub fn push_excerpts_with_context_lines<O>(
@@ -818,7 +826,7 @@ impl MultiBuffer {
pub fn insert_excerpts_after<O>(
&mut self,
prev_excerpt_id: &ExcerptId,
prev_excerpt_id: ExcerptId,
buffer: ModelHandle<Buffer>,
ranges: impl IntoIterator<Item = ExcerptRange<O>>,
cx: &mut ModelContext<Self>,
@@ -854,8 +862,12 @@ impl MultiBuffer {
});
let mut snapshot = self.snapshot.borrow_mut();
let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptId>>();
let mut new_excerpts = cursor.slice(&Some(prev_excerpt_id), Bias::Right, &());
let mut prev_locator = snapshot.excerpt_locator_for_id(prev_excerpt_id).clone();
let mut new_excerpt_ids = mem::take(&mut snapshot.excerpt_ids);
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
let mut new_excerpts = cursor.slice(&prev_locator, Bias::Right, &());
prev_locator = cursor.start().unwrap_or(Locator::min_ref()).clone();
let edit_start = new_excerpts.summary().text.len;
new_excerpts.update_last(
@@ -865,25 +877,17 @@ impl MultiBuffer {
&(),
);
let mut used_cursor = self.used_excerpt_ids.cursor::<Locator>();
used_cursor.seek(prev_excerpt_id, Bias::Right, &());
let mut prev_id = if let Some(excerpt_id) = used_cursor.prev_item() {
excerpt_id.clone()
let next_locator = if let Some(excerpt) = cursor.item() {
excerpt.locator.clone()
} else {
ExcerptId::min()
Locator::max()
};
let next_id = if let Some(excerpt_id) = used_cursor.item() {
excerpt_id.clone()
} else {
ExcerptId::max()
};
drop(used_cursor);
let mut ids = Vec::new();
while let Some(range) = ranges.next() {
let id = ExcerptId::between(&prev_id, &next_id);
if let Err(ix) = buffer_state.excerpts.binary_search(&id) {
buffer_state.excerpts.insert(ix, id.clone());
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());
}
let range = ExcerptRange {
context: buffer_snapshot.anchor_before(&range.context.start)
@@ -893,22 +897,20 @@ impl MultiBuffer {
..buffer_snapshot.anchor_after(&primary.end)
}),
};
let id = ExcerptId(post_inc(&mut self.next_excerpt_id));
let excerpt = Excerpt::new(
id.clone(),
post_inc(&mut self.next_excerpt_key),
id,
locator.clone(),
buffer_id,
buffer_snapshot.clone(),
range,
ranges.peek().is_some() || cursor.item().is_some(),
);
new_excerpts.push(excerpt, &());
prev_id = id.clone();
prev_locator = locator.clone();
new_excerpt_ids.push(ExcerptIdMapping { id, locator }, &());
ids.push(id);
}
self.used_excerpt_ids.edit(
ids.iter().cloned().map(sum_tree::Edit::Insert).collect(),
&(),
);
let edit_end = new_excerpts.summary().text.len;
@@ -917,6 +919,7 @@ impl MultiBuffer {
new_excerpts.push_tree(suffix, &());
drop(cursor);
snapshot.excerpts = new_excerpts;
snapshot.excerpt_ids = new_excerpt_ids;
if changed_trailing_excerpt {
snapshot.trailing_excerpt_update_count += 1;
}
@@ -956,16 +959,16 @@ impl MultiBuffer {
let mut excerpts = Vec::new();
let snapshot = self.read(cx);
let buffers = self.buffers.borrow();
let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptId>>();
for excerpt_id in buffers
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
for locator in buffers
.get(&buffer.id())
.map(|state| &state.excerpts)
.into_iter()
.flatten()
{
cursor.seek_forward(&Some(excerpt_id), Bias::Left, &());
cursor.seek_forward(&Some(locator), Bias::Left, &());
if let Some(excerpt) = cursor.item() {
if excerpt.id == *excerpt_id {
if excerpt.locator == *locator {
excerpts.push((excerpt.id.clone(), excerpt.range.clone()));
}
}
@@ -975,10 +978,11 @@ impl MultiBuffer {
}
pub fn excerpt_ids(&self) -> Vec<ExcerptId> {
self.buffers
self.snapshot
.borrow()
.values()
.flat_map(|state| state.excerpts.iter().cloned())
.excerpts
.iter()
.map(|entry| entry.id)
.collect()
}
@@ -1061,32 +1065,34 @@ impl MultiBuffer {
result
}
pub fn remove_excerpts<'a>(
pub fn remove_excerpts(
&mut self,
excerpt_ids: impl IntoIterator<Item = &'a ExcerptId>,
excerpt_ids: impl IntoIterator<Item = ExcerptId>,
cx: &mut ModelContext<Self>,
) {
self.sync(cx);
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<&ExcerptId>, usize)>();
let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>();
let mut edits = Vec::new();
let mut excerpt_ids = excerpt_ids.into_iter().peekable();
while let Some(mut excerpt_id) = excerpt_ids.next() {
while let Some(excerpt_id) = excerpt_ids.next() {
// Seek to the next excerpt to remove, preserving any preceding excerpts.
new_excerpts.push_tree(cursor.slice(&Some(excerpt_id), Bias::Left, &()), &());
let locator = snapshot.excerpt_locator_for_id(excerpt_id);
new_excerpts.push_tree(cursor.slice(&Some(locator), Bias::Left, &()), &());
if let Some(mut excerpt) = cursor.item() {
if excerpt.id != *excerpt_id {
if excerpt.id != excerpt_id {
continue;
}
let mut old_start = cursor.start().1;
// Skip over the removed excerpt.
loop {
'remove_excerpts: loop {
if let Some(buffer_state) = buffers.get_mut(&excerpt.buffer_id) {
buffer_state.excerpts.retain(|id| id != excerpt_id);
buffer_state.excerpts.retain(|l| l != &excerpt.locator);
if buffer_state.excerpts.is_empty() {
buffers.remove(&excerpt.buffer_id);
}
@@ -1094,14 +1100,16 @@ impl MultiBuffer {
cursor.next(&());
// Skip over any subsequent excerpts that are also removed.
if let Some(&next_excerpt_id) = excerpt_ids.peek() {
while let Some(&next_excerpt_id) = excerpt_ids.peek() {
let next_locator = snapshot.excerpt_locator_for_id(next_excerpt_id);
if let Some(next_excerpt) = cursor.item() {
if next_excerpt.id == *next_excerpt_id {
if next_excerpt.locator == *next_locator {
excerpt_ids.next();
excerpt = next_excerpt;
excerpt_id = excerpt_ids.next().unwrap();
continue;
continue 'remove_excerpts;
}
}
break;
}
break;
@@ -1128,6 +1136,7 @@ impl MultiBuffer {
new_excerpts.push_tree(suffix, &());
drop(cursor);
snapshot.excerpts = new_excerpts;
if changed_trailing_excerpt {
snapshot.trailing_excerpt_update_count += 1;
}
@@ -1307,7 +1316,7 @@ impl MultiBuffer {
buffer_state
.excerpts
.iter()
.map(|excerpt_id| (excerpt_id, buffer_state.buffer.clone(), buffer_edited)),
.map(|locator| (locator, buffer_state.buffer.clone(), buffer_edited)),
);
}
@@ -1333,14 +1342,14 @@ impl MultiBuffer {
snapshot.is_dirty = is_dirty;
snapshot.has_conflict = has_conflict;
excerpts_to_edit.sort_unstable_by_key(|(excerpt_id, _, _)| *excerpt_id);
excerpts_to_edit.sort_unstable_by_key(|(locator, _, _)| *locator);
let mut edits = Vec::new();
let mut new_excerpts = SumTree::new();
let mut cursor = snapshot.excerpts.cursor::<(Option<&ExcerptId>, usize)>();
let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>();
for (id, buffer, buffer_edited) in excerpts_to_edit {
new_excerpts.push_tree(cursor.slice(&Some(id), Bias::Left, &()), &());
for (locator, buffer, buffer_edited) in excerpts_to_edit {
new_excerpts.push_tree(cursor.slice(&Some(locator), Bias::Left, &()), &());
let old_excerpt = cursor.item().unwrap();
let buffer_id = buffer.id();
let buffer = buffer.read(cx);
@@ -1365,8 +1374,8 @@ impl MultiBuffer {
);
new_excerpt = Excerpt::new(
id.clone(),
old_excerpt.key,
old_excerpt.id,
locator.clone(),
buffer_id,
buffer.snapshot(),
old_excerpt.range.clone(),
@@ -1467,13 +1476,7 @@ impl MultiBuffer {
continue;
}
let excerpt_ids = self
.buffers
.borrow()
.values()
.flat_map(|b| &b.excerpts)
.cloned()
.collect::<Vec<_>>();
let excerpt_ids = self.excerpt_ids();
if excerpt_ids.is_empty() || (rng.gen() && excerpt_ids.len() < max_excerpts) {
let buffer_handle = if rng.gen() || self.buffers.borrow().is_empty() {
let text = RandomCharIter::new(&mut *rng).take(10).collect::<String>();
@@ -1511,24 +1514,26 @@ impl MultiBuffer {
log::info!(
"Inserting excerpts from buffer {} and ranges {:?}: {:?}",
buffer_handle.id(),
ranges,
ranges.iter().map(|r| &r.context).collect::<Vec<_>>(),
ranges
.iter()
.map(|range| &buffer_text[range.context.clone()])
.map(|r| &buffer_text[r.context.clone()])
.collect::<Vec<_>>()
);
let excerpt_id = self.push_excerpts(buffer_handle.clone(), ranges, cx);
log::info!("Inserted with id: {:?}", excerpt_id);
log::info!("Inserted with ids: {:?}", excerpt_id);
} else {
let remove_count = rng.gen_range(1..=excerpt_ids.len());
let mut excerpts_to_remove = excerpt_ids
.choose_multiple(rng, remove_count)
.cloned()
.collect::<Vec<_>>();
excerpts_to_remove.sort();
let snapshot = self.snapshot.borrow();
excerpts_to_remove.sort_unstable_by(|a, b| a.cmp(b, &*snapshot));
drop(snapshot);
log::info!("Removing excerpts {:?}", excerpts_to_remove);
self.remove_excerpts(&excerpts_to_remove, cx);
self.remove_excerpts(excerpts_to_remove, cx);
}
}
}
@@ -1563,6 +1568,38 @@ impl MultiBuffer {
} else {
self.randomly_edit_excerpts(rng, mutation_count, cx);
}
self.check_invariants(cx);
}
fn check_invariants(&self, cx: &mut ModelContext<Self>) {
let snapshot = self.read(cx);
let excerpts = snapshot.excerpts.items(&());
let excerpt_ids = snapshot.excerpt_ids.items(&());
for (ix, excerpt) in excerpts.iter().enumerate() {
if ix == 0 {
if excerpt.locator <= Locator::min() {
panic!("invalid first excerpt locator {:?}", excerpt.locator);
}
} else {
if excerpt.locator <= excerpts[ix - 1].locator {
panic!("excerpts are out-of-order: {:?}", excerpts);
}
}
}
for (ix, entry) in excerpt_ids.iter().enumerate() {
if ix == 0 {
if entry.id.cmp(&ExcerptId::min(), &*snapshot).is_le() {
panic!("invalid first excerpt id {:?}", entry.id);
}
} else {
if entry.id <= excerpt_ids[ix - 1].id {
panic!("excerpt ids are out-of-order: {:?}", excerpt_ids);
}
}
}
}
}
@@ -1749,20 +1786,20 @@ impl MultiBufferSnapshot {
*cursor.start() + overshoot
}
pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 {
pub fn clip_point_utf16(&self, point: Unclipped<PointUtf16>, bias: Bias) -> PointUtf16 {
if let Some((_, _, buffer)) = self.as_singleton() {
return buffer.clip_point_utf16(point, bias);
}
let mut cursor = self.excerpts.cursor::<PointUtf16>();
cursor.seek(&point, Bias::Right, &());
cursor.seek(&point.0, Bias::Right, &());
let overshoot = if let Some(excerpt) = cursor.item() {
let excerpt_start = excerpt
.buffer
.offset_to_point_utf16(excerpt.range.context.start.to_offset(&excerpt.buffer));
let buffer_point = excerpt
.buffer
.clip_point_utf16(excerpt_start + (point - cursor.start()), bias);
.clip_point_utf16(Unclipped(excerpt_start + (point.0 - cursor.start())), bias);
buffer_point.saturating_sub(excerpt_start)
} else {
PointUtf16::zero()
@@ -2151,7 +2188,9 @@ impl MultiBufferSnapshot {
D: TextDimension + Ord + Sub<D, Output = D>,
{
let mut cursor = self.excerpts.cursor::<ExcerptSummary>();
cursor.seek(&Some(&anchor.excerpt_id), Bias::Left, &());
let locator = self.excerpt_locator_for_id(anchor.excerpt_id);
cursor.seek(locator, Bias::Left, &());
if cursor.item().is_none() {
cursor.next(&());
}
@@ -2189,24 +2228,25 @@ impl MultiBufferSnapshot {
let mut cursor = self.excerpts.cursor::<ExcerptSummary>();
let mut summaries = Vec::new();
while let Some(anchor) = anchors.peek() {
let excerpt_id = &anchor.excerpt_id;
let excerpt_id = anchor.excerpt_id;
let excerpt_anchors = iter::from_fn(|| {
let anchor = anchors.peek()?;
if anchor.excerpt_id == *excerpt_id {
if anchor.excerpt_id == excerpt_id {
Some(&anchors.next().unwrap().text_anchor)
} else {
None
}
});
cursor.seek_forward(&Some(excerpt_id), Bias::Left, &());
let locator = self.excerpt_locator_for_id(excerpt_id);
cursor.seek_forward(locator, Bias::Left, &());
if cursor.item().is_none() {
cursor.next(&());
}
let position = D::from_text_summary(&cursor.start().text);
if let Some(excerpt) = cursor.item() {
if excerpt.id == *excerpt_id {
if excerpt.id == excerpt_id {
let excerpt_buffer_start =
excerpt.range.context.start.summary::<D>(&excerpt.buffer);
let excerpt_buffer_end =
@@ -2240,13 +2280,18 @@ impl MultiBufferSnapshot {
I: 'a + IntoIterator<Item = &'a Anchor>,
{
let mut anchors = anchors.into_iter().enumerate().peekable();
let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
let mut cursor = self.excerpts.cursor::<Option<&Locator>>();
cursor.next(&());
let mut result = Vec::new();
while let Some((_, anchor)) = anchors.peek() {
let old_excerpt_id = &anchor.excerpt_id;
let old_excerpt_id = anchor.excerpt_id;
// Find the location where this anchor's excerpt should be.
cursor.seek_forward(&Some(old_excerpt_id), Bias::Left, &());
let old_locator = self.excerpt_locator_for_id(old_excerpt_id);
cursor.seek_forward(&Some(old_locator), Bias::Left, &());
if cursor.item().is_none() {
cursor.next(&());
}
@@ -2256,27 +2301,22 @@ impl MultiBufferSnapshot {
// Process all of the anchors for this excerpt.
while let Some((_, anchor)) = anchors.peek() {
if anchor.excerpt_id != *old_excerpt_id {
if anchor.excerpt_id != old_excerpt_id {
break;
}
let mut kept_position = false;
let (anchor_ix, anchor) = anchors.next().unwrap();
let mut anchor = anchor.clone();
let id_invalid =
*old_excerpt_id == ExcerptId::max() || *old_excerpt_id == ExcerptId::min();
let still_exists = next_excerpt.map_or(false, |excerpt| {
excerpt.id == *old_excerpt_id && excerpt.contains(&anchor)
});
let mut anchor = *anchor;
// Leave min and max anchors unchanged if invalid or
// if the old excerpt still exists at this location
if id_invalid || still_exists {
kept_position = true;
}
let mut kept_position = next_excerpt
.map_or(false, |e| e.id == old_excerpt_id && e.contains(&anchor))
|| old_excerpt_id == ExcerptId::max()
|| old_excerpt_id == ExcerptId::min();
// If the old excerpt no longer exists at this location, then attempt to
// find an equivalent position for this anchor in an adjacent excerpt.
else {
if !kept_position {
for excerpt in [next_excerpt, prev_excerpt].iter().filter_map(|e| *e) {
if excerpt.contains(&anchor) {
anchor.excerpt_id = excerpt.id.clone();
@@ -2285,6 +2325,7 @@ impl MultiBufferSnapshot {
}
}
}
// If there's no adjacent excerpt that contains the anchor's position,
// then report that the anchor has lost its position.
if !kept_position {
@@ -2354,7 +2395,7 @@ impl MultiBufferSnapshot {
};
}
let mut cursor = self.excerpts.cursor::<(usize, Option<&ExcerptId>)>();
let mut cursor = self.excerpts.cursor::<(usize, Option<ExcerptId>)>();
cursor.seek(&offset, Bias::Right, &());
if cursor.item().is_none() && offset == cursor.start().0 && bias == Bias::Left {
cursor.prev(&());
@@ -2382,8 +2423,9 @@ impl MultiBufferSnapshot {
}
pub fn anchor_in_excerpt(&self, excerpt_id: ExcerptId, text_anchor: text::Anchor) -> Anchor {
let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
cursor.seek(&Some(&excerpt_id), Bias::Left, &());
let locator = self.excerpt_locator_for_id(excerpt_id);
let mut cursor = self.excerpts.cursor::<Option<&Locator>>();
cursor.seek(locator, Bias::Left, &());
if let Some(excerpt) = cursor.item() {
if excerpt.id == excerpt_id {
let text_anchor = excerpt.clip_anchor(text_anchor);
@@ -2401,7 +2443,7 @@ impl MultiBufferSnapshot {
pub fn can_resolve(&self, anchor: &Anchor) -> bool {
if anchor.excerpt_id == ExcerptId::min() || anchor.excerpt_id == ExcerptId::max() {
true
} else if let Some(excerpt) = self.excerpt(&anchor.excerpt_id) {
} else if let Some(excerpt) = self.excerpt(anchor.excerpt_id) {
excerpt.buffer.can_resolve(&anchor.text_anchor)
} else {
false
@@ -2456,7 +2498,6 @@ impl MultiBufferSnapshot {
let starts_new_buffer = Some(excerpt.buffer_id) != prev_buffer_id;
let boundary = ExcerptBoundary {
id: excerpt.id.clone(),
key: excerpt.key,
row: cursor.start().1.row,
buffer: excerpt.buffer.clone(),
range: excerpt.range.clone(),
@@ -2678,8 +2719,8 @@ impl MultiBufferSnapshot {
.flatten()
.map(|item| OutlineItem {
depth: item.depth,
range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start)
..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end),
range: self.anchor_in_excerpt(excerpt_id, item.range.start)
..self.anchor_in_excerpt(excerpt_id, item.range.end),
text: item.text,
highlight_ranges: item.highlight_ranges,
name_ranges: item.name_ranges,
@@ -2688,11 +2729,29 @@ impl MultiBufferSnapshot {
))
}
fn excerpt<'a>(&'a self, excerpt_id: &'a ExcerptId) -> Option<&'a Excerpt> {
let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
cursor.seek(&Some(excerpt_id), Bias::Left, &());
fn excerpt_locator_for_id<'a>(&'a self, id: ExcerptId) -> &'a Locator {
if id == ExcerptId::min() {
Locator::min_ref()
} else if id == ExcerptId::max() {
Locator::max_ref()
} else {
let mut cursor = self.excerpt_ids.cursor::<ExcerptId>();
cursor.seek(&id, Bias::Left, &());
if let Some(entry) = cursor.item() {
if entry.id == id {
return &entry.locator;
}
}
panic!("invalid excerpt id {:?}", 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);
cursor.seek(&Some(locator), Bias::Left, &());
if let Some(excerpt) = cursor.item() {
if excerpt.id == *excerpt_id {
if excerpt.id == excerpt_id {
return Some(excerpt);
}
}
@@ -2703,10 +2762,12 @@ impl MultiBufferSnapshot {
&'a self,
range: &'a Range<Anchor>,
) -> impl 'a + Iterator<Item = (ReplicaId, bool, CursorShape, Selection<Anchor>)> {
let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
cursor.seek(&Some(&range.start.excerpt_id), Bias::Left, &());
let mut cursor = self.excerpts.cursor::<ExcerptSummary>();
let start_locator = self.excerpt_locator_for_id(range.start.excerpt_id);
let end_locator = self.excerpt_locator_for_id(range.end.excerpt_id);
cursor.seek(start_locator, Bias::Left, &());
cursor
.take_while(move |excerpt| excerpt.id <= range.end.excerpt_id)
.take_while(move |excerpt| excerpt.locator <= *end_locator)
.flat_map(move |excerpt| {
let mut query_range = excerpt.range.context.start..excerpt.range.context.end;
if excerpt.id == range.start.excerpt_id {
@@ -2916,7 +2977,7 @@ impl History {
impl Excerpt {
fn new(
id: ExcerptId,
key: usize,
locator: Locator,
buffer_id: usize,
buffer: BufferSnapshot,
range: ExcerptRange<text::Anchor>,
@@ -2924,7 +2985,7 @@ impl Excerpt {
) -> Self {
Excerpt {
id,
key,
locator,
max_buffer_row: range.context.end.to_point(&buffer).row,
text_summary: buffer
.text_summary_for_range::<TextSummary, _>(range.context.to_offset(&buffer)),
@@ -3010,10 +3071,33 @@ impl Excerpt {
}
}
impl ExcerptId {
pub fn min() -> Self {
Self(0)
}
pub fn max() -> Self {
Self(usize::MAX)
}
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);
a.cmp(&b).then_with(|| self.0.cmp(&other.0))
}
}
impl Into<usize> for ExcerptId {
fn into(self) -> usize {
self.0
}
}
impl fmt::Debug for Excerpt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Excerpt")
.field("id", &self.id)
.field("locator", &self.locator)
.field("buffer_id", &self.buffer_id)
.field("range", &self.range)
.field("text_summary", &self.text_summary)
@@ -3031,19 +3115,44 @@ impl sum_tree::Item for Excerpt {
text += TextSummary::from("\n");
}
ExcerptSummary {
excerpt_id: self.id.clone(),
excerpt_id: self.id,
excerpt_locator: self.locator.clone(),
max_buffer_row: self.max_buffer_row,
text,
}
}
}
impl sum_tree::Item for ExcerptIdMapping {
type Summary = ExcerptId;
fn summary(&self) -> Self::Summary {
self.id
}
}
impl sum_tree::KeyedItem for ExcerptIdMapping {
type Key = ExcerptId;
fn key(&self) -> Self::Key {
self.id
}
}
impl sum_tree::Summary for ExcerptId {
type Context = ();
fn add_summary(&mut self, other: &Self, _: &()) {
*self = *other;
}
}
impl sum_tree::Summary for ExcerptSummary {
type Context = ();
fn add_summary(&mut self, summary: &Self, _: &()) {
debug_assert!(summary.excerpt_id > self.excerpt_id);
self.excerpt_id = summary.excerpt_id.clone();
debug_assert!(summary.excerpt_locator > self.excerpt_locator);
self.excerpt_locator = summary.excerpt_locator.clone();
self.text.add_summary(&summary.text, &());
self.max_buffer_row = cmp::max(self.max_buffer_row, summary.max_buffer_row);
}
@@ -3067,9 +3176,15 @@ impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for usize {
}
}
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for Option<&'a ExcerptId> {
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, Option<&'a Locator>> for Locator {
fn cmp(&self, cursor_location: &Option<&'a Locator>, _: &()) -> cmp::Ordering {
Ord::cmp(&Some(self), cursor_location)
}
}
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for Locator {
fn cmp(&self, cursor_location: &ExcerptSummary, _: &()) -> cmp::Ordering {
Ord::cmp(self, &Some(&cursor_location.excerpt_id))
Ord::cmp(self, &cursor_location.excerpt_locator)
}
}
@@ -3091,9 +3206,15 @@ impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for PointUtf16 {
}
}
impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option<&'a ExcerptId> {
impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option<&'a Locator> {
fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) {
*self = Some(&summary.excerpt_id);
*self = Some(&summary.excerpt_locator);
}
}
impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option<ExcerptId> {
fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) {
*self = Some(summary.excerpt_id);
}
}
@@ -3274,12 +3395,6 @@ impl ToOffset for Point {
}
}
impl ToOffset for PointUtf16 {
fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize {
snapshot.point_utf16_to_offset(*self)
}
}
impl ToOffset for usize {
fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize {
assert!(*self <= snapshot.len(), "offset is out of range");
@@ -3293,6 +3408,12 @@ impl ToOffset for OffsetUtf16 {
}
}
impl ToOffset for PointUtf16 {
fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize {
snapshot.point_utf16_to_offset(*self)
}
}
impl ToOffsetUtf16 for OffsetUtf16 {
fn to_offset_utf16(&self, _snapshot: &MultiBufferSnapshot) -> OffsetUtf16 {
*self
@@ -3591,7 +3712,7 @@ mod tests {
let snapshot = multibuffer.update(cx, |multibuffer, cx| {
let (buffer_2_excerpt_id, _) =
multibuffer.excerpts_for_buffer(&buffer_2, cx)[0].clone();
multibuffer.remove_excerpts(&[buffer_2_excerpt_id], cx);
multibuffer.remove_excerpts([buffer_2_excerpt_id], cx);
multibuffer.snapshot(cx)
});
@@ -3780,7 +3901,7 @@ mod tests {
// Replace the buffer 1 excerpt with new excerpts from buffer 2.
let (excerpt_id_2, excerpt_id_3) = multibuffer.update(cx, |multibuffer, cx| {
multibuffer.remove_excerpts([&excerpt_id_1], cx);
multibuffer.remove_excerpts([excerpt_id_1], cx);
let mut ids = multibuffer
.push_excerpts(
buffer_2.clone(),
@@ -3810,9 +3931,8 @@ mod tests {
assert_ne!(excerpt_id_2, excerpt_id_1);
// Resolve some anchors from the previous snapshot in the new snapshot.
// Although there is still an excerpt with the same id, it is for
// a different buffer, so we don't attempt to resolve the old text
// anchor in the new buffer.
// The current excerpts are from a different buffer, so we don't attempt to
// resolve the old text anchor in the new buffer.
assert_eq!(
snapshot_2.summary_for_anchor::<usize>(&snapshot_1.anchor_before(2)),
0
@@ -3824,6 +3944,9 @@ mod tests {
]),
vec![0, 0]
);
// Refresh anchors from the old snapshot. The return value indicates that both
// anchors lost their original excerpt.
let refresh =
snapshot_2.refresh_anchors(&[snapshot_1.anchor_before(2), snapshot_1.anchor_after(3)]);
assert_eq!(
@@ -3837,10 +3960,10 @@ mod tests {
// Replace the middle excerpt with a smaller excerpt in buffer 2,
// that intersects the old excerpt.
let excerpt_id_5 = multibuffer.update(cx, |multibuffer, cx| {
multibuffer.remove_excerpts([&excerpt_id_3], cx);
multibuffer.remove_excerpts([excerpt_id_3], cx);
multibuffer
.insert_excerpts_after(
&excerpt_id_3,
excerpt_id_2,
buffer_2.clone(),
[ExcerptRange {
context: 5..8,
@@ -3857,8 +3980,8 @@ mod tests {
assert_ne!(excerpt_id_5, excerpt_id_3);
// Resolve some anchors from the previous snapshot in the new snapshot.
// The anchor in the middle excerpt snaps to the beginning of the
// excerpt, since it is not
// The third anchor can't be resolved, since its excerpt has been removed,
// so it resolves to the same position as its predecessor.
let anchors = [
snapshot_2.anchor_before(0),
snapshot_2.anchor_after(2),
@@ -3867,7 +3990,7 @@ mod tests {
];
assert_eq!(
snapshot_3.summaries_for_anchors::<usize, _>(&anchors),
&[0, 2, 5, 13]
&[0, 2, 9, 13]
);
let new_anchors = snapshot_3.refresh_anchors(&anchors);
@@ -3889,7 +4012,7 @@ mod tests {
let mut buffers: Vec<ModelHandle<Buffer>> = Vec::new();
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
let mut excerpt_ids = Vec::new();
let mut excerpt_ids = Vec::<ExcerptId>::new();
let mut expected_excerpts = Vec::<(ModelHandle<Buffer>, Range<text::Anchor>)>::new();
let mut anchors = Vec::new();
let mut old_versions = Vec::new();
@@ -3919,9 +4042,11 @@ mod tests {
.collect::<String>(),
);
}
ids_to_remove.sort_unstable();
let snapshot = multibuffer.read(cx).read(cx);
ids_to_remove.sort_unstable_by(|a, b| a.cmp(&b, &snapshot));
drop(snapshot);
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.remove_excerpts(&ids_to_remove, cx)
multibuffer.remove_excerpts(ids_to_remove, cx)
});
}
30..=39 if !expected_excerpts.is_empty() => {
@@ -3945,7 +4070,6 @@ mod tests {
// Ensure the newly-refreshed anchors point to a valid excerpt and don't
// overshoot its boundaries.
assert_eq!(anchors.len(), prev_len);
let mut cursor = multibuffer.excerpts.cursor::<Option<&ExcerptId>>();
for anchor in &anchors {
if anchor.excerpt_id == ExcerptId::min()
|| anchor.excerpt_id == ExcerptId::max()
@@ -3953,8 +4077,7 @@ mod tests {
continue;
}
cursor.seek_forward(&Some(&anchor.excerpt_id), Bias::Left, &());
let excerpt = cursor.item().unwrap();
let excerpt = multibuffer.excerpt(anchor.excerpt_id).unwrap();
assert_eq!(excerpt.id, anchor.excerpt_id);
assert!(excerpt.contains(anchor));
}
@@ -3994,7 +4117,7 @@ mod tests {
let excerpt_id = multibuffer.update(cx, |multibuffer, cx| {
multibuffer
.insert_excerpts_after(
&prev_excerpt_id,
prev_excerpt_id,
buffer_handle.clone(),
[ExcerptRange {
context: start_ix..end_ix,
@@ -4158,12 +4281,14 @@ mod tests {
}
for _ in 0..ch.len_utf16() {
let left_point_utf16 = snapshot.clip_point_utf16(point_utf16, Bias::Left);
let right_point_utf16 = snapshot.clip_point_utf16(point_utf16, Bias::Right);
let left_point_utf16 =
snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Left);
let right_point_utf16 =
snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Right);
let buffer_left_point_utf16 =
buffer.clip_point_utf16(buffer_point_utf16, Bias::Left);
buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Left);
let buffer_right_point_utf16 =
buffer.clip_point_utf16(buffer_point_utf16, Bias::Right);
buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Right);
assert_eq!(
left_point_utf16,
excerpt_start.lines_utf16()

View File

@@ -6,7 +6,7 @@ use std::{
};
use sum_tree::Bias;
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
pub struct Anchor {
pub(crate) buffer_id: Option<usize>,
pub(crate) excerpt_id: ExcerptId,
@@ -30,16 +30,16 @@ impl Anchor {
}
}
pub fn excerpt_id(&self) -> &ExcerptId {
&self.excerpt_id
pub fn excerpt_id(&self) -> ExcerptId {
self.excerpt_id
}
pub fn cmp(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering {
let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id);
let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id, snapshot);
if excerpt_id_cmp.is_eq() {
if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() {
Ordering::Equal
} else if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) {
} else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer)
} else {
Ordering::Equal
@@ -51,7 +51,7 @@ impl Anchor {
pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
if self.text_anchor.bias != Bias::Left {
if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) {
if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
return Self {
buffer_id: self.buffer_id,
excerpt_id: self.excerpt_id.clone(),
@@ -64,7 +64,7 @@ impl Anchor {
pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
if self.text_anchor.bias != Bias::Right {
if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) {
if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
return Self {
buffer_id: self.buffer_id,
excerpt_id: self.excerpt_id.clone(),

View File

@@ -0,0 +1,36 @@
use std::path::PathBuf;
use db::sqlez_macros::sql;
use db::{define_connection, query};
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
define_connection!(
pub static ref DB: EditorDb<WorkspaceDb> =
&[sql! (
CREATE TABLE editors(
item_id INTEGER NOT NULL,
workspace_id INTEGER NOT NULL,
path BLOB NOT NULL,
PRIMARY KEY(item_id, workspace_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
ON UPDATE CASCADE
) STRICT;
)];
);
impl EditorDb {
query! {
pub fn get_path(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
SELECT path FROM editors
WHERE item_id = ? AND workspace_id = ?
}
}
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 (?, ?, ?)
}
}
}

View File

@@ -544,11 +544,21 @@ impl<'a> MutableSelectionsCollection<'a> {
T: ToOffset,
{
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
let ranges = ranges
.into_iter()
.map(|range| range.start.to_offset(&buffer)..range.end.to_offset(&buffer));
self.select_offset_ranges(ranges);
}
fn select_offset_ranges<I>(&mut self, ranges: I)
where
I: IntoIterator<Item = Range<usize>>,
{
let selections = ranges
.into_iter()
.map(|range| {
let mut start = range.start.to_offset(&buffer);
let mut end = range.end.to_offset(&buffer);
let mut start = range.start;
let mut end = range.end;
let reversed = if start > end {
mem::swap(&mut start, &mut end);
true

View File

@@ -63,8 +63,15 @@ impl<'a> EditorLspTestContext<'a> {
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
.await;
let (window_id, workspace) =
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
let (window_id, workspace) = cx.add_window(|cx| {
Workspace::new(
Default::default(),
0,
project.clone(),
|_, _| unimplemented!(),
cx,
)
});
project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx)

View File

@@ -316,8 +316,9 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (window_id, workspace) =
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let (window_id, workspace) = cx.add_window(|cx| {
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
});
cx.dispatch_action(window_id, Toggle);
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
@@ -371,8 +372,9 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
let (_, workspace) =
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let (_, workspace) = cx.add_window(|cx| {
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
});
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
@@ -446,8 +448,9 @@ mod tests {
cx,
)
.await;
let (_, workspace) =
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let (_, workspace) = cx.add_window(|cx| {
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
});
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
finder
@@ -471,8 +474,9 @@ mod tests {
cx,
)
.await;
let (_, workspace) =
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let (_, workspace) = cx.add_window(|cx| {
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
});
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
@@ -524,8 +528,9 @@ mod tests {
cx,
)
.await;
let (_, workspace) =
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let (_, workspace) = cx.add_window(|cx| {
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
});
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
@@ -563,8 +568,9 @@ mod tests {
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (_, workspace) =
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
let (_, workspace) = cx.add_window(|cx| {
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
});
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
finder

View File

@@ -17,6 +17,7 @@ collections = { path = "../collections" }
gpui_macros = { path = "../gpui_macros" }
util = { path = "../util" }
sum_tree = { path = "../sum_tree" }
sqlez = { path = "../sqlez" }
async-task = "4.0.3"
backtrace = { version = "0.3", optional = true }
ctor = "0.1"

View File

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

View File

@@ -115,6 +115,7 @@ impl Tooltip {
} else {
state.visible.set(false);
state.debounce.take();
cx.notify();
}
}
})

View File

@@ -66,21 +66,32 @@ struct DeterministicState {
rng: rand::prelude::StdRng,
seed: u64,
scheduled_from_foreground: collections::HashMap<usize, Vec<ForegroundRunnable>>,
scheduled_from_background: Vec<Runnable>,
scheduled_from_background: Vec<BackgroundRunnable>,
forbid_parking: bool,
block_on_ticks: std::ops::RangeInclusive<usize>,
now: std::time::Instant,
next_timer_id: usize,
pending_timers: Vec<(usize, std::time::Instant, postage::barrier::Sender)>,
waiting_backtrace: Option<backtrace::Backtrace>,
next_runnable_id: usize,
poll_history: Vec<usize>,
enable_runnable_backtraces: bool,
runnable_backtraces: collections::HashMap<usize, backtrace::Backtrace>,
}
#[cfg(any(test, feature = "test-support"))]
struct ForegroundRunnable {
id: usize,
runnable: Runnable,
main: bool,
}
#[cfg(any(test, feature = "test-support"))]
struct BackgroundRunnable {
id: usize,
runnable: Runnable,
}
#[cfg(any(test, feature = "test-support"))]
pub struct Deterministic {
state: Arc<parking_lot::Mutex<DeterministicState>>,
@@ -117,11 +128,29 @@ impl Deterministic {
next_timer_id: Default::default(),
pending_timers: Default::default(),
waiting_backtrace: None,
next_runnable_id: 0,
poll_history: Default::default(),
enable_runnable_backtraces: false,
runnable_backtraces: Default::default(),
})),
parker: Default::default(),
})
}
pub fn runnable_history(&self) -> Vec<usize> {
self.state.lock().poll_history.clone()
}
pub fn enable_runnable_backtrace(&self) {
self.state.lock().enable_runnable_backtraces = true;
}
pub fn runnable_backtrace(&self, runnable_id: usize) -> backtrace::Backtrace {
let mut backtrace = self.state.lock().runnable_backtraces[&runnable_id].clone();
backtrace.resolve();
backtrace
}
pub fn build_background(self: &Arc<Self>) -> Arc<Background> {
Arc::new(Background::Deterministic {
executor: self.clone(),
@@ -142,6 +171,17 @@ impl Deterministic {
main: bool,
) -> AnyLocalTask {
let state = self.state.clone();
let id;
{
let mut state = state.lock();
id = util::post_inc(&mut state.next_runnable_id);
if state.enable_runnable_backtraces {
state
.runnable_backtraces
.insert(id, backtrace::Backtrace::new_unresolved());
}
}
let unparker = self.parker.lock().unparker();
let (runnable, task) = async_task::spawn_local(future, move |runnable| {
let mut state = state.lock();
@@ -149,7 +189,7 @@ impl Deterministic {
.scheduled_from_foreground
.entry(cx_id)
.or_default()
.push(ForegroundRunnable { runnable, main });
.push(ForegroundRunnable { id, runnable, main });
unparker.unpark();
});
runnable.schedule();
@@ -158,10 +198,23 @@ impl Deterministic {
fn spawn(&self, future: AnyFuture) -> AnyTask {
let state = self.state.clone();
let id;
{
let mut state = state.lock();
id = util::post_inc(&mut state.next_runnable_id);
if state.enable_runnable_backtraces {
state
.runnable_backtraces
.insert(id, backtrace::Backtrace::new_unresolved());
}
}
let unparker = self.parker.lock().unparker();
let (runnable, task) = async_task::spawn(future, move |runnable| {
let mut state = state.lock();
state.scheduled_from_background.push(runnable);
state
.scheduled_from_background
.push(BackgroundRunnable { id, runnable });
unparker.unpark();
});
runnable.schedule();
@@ -178,15 +231,27 @@ impl Deterministic {
let woken = Arc::new(AtomicBool::new(false));
let state = self.state.clone();
let id;
{
let mut state = state.lock();
id = util::post_inc(&mut state.next_runnable_id);
if state.enable_runnable_backtraces {
state
.runnable_backtraces
.insert(id, backtrace::Backtrace::new_unresolved());
}
}
let unparker = self.parker.lock().unparker();
let (runnable, mut main_task) = unsafe {
async_task::spawn_unchecked(main_future, move |runnable| {
let mut state = state.lock();
let state = &mut *state.lock();
state
.scheduled_from_foreground
.entry(cx_id)
.or_default()
.push(ForegroundRunnable {
id: util::post_inc(&mut state.next_runnable_id),
runnable,
main: true,
});
@@ -248,9 +313,10 @@ impl Deterministic {
if !state.scheduled_from_background.is_empty() && state.rng.gen() {
let background_len = state.scheduled_from_background.len();
let ix = state.rng.gen_range(0..background_len);
let runnable = state.scheduled_from_background.remove(ix);
let background_runnable = state.scheduled_from_background.remove(ix);
state.poll_history.push(background_runnable.id);
drop(state);
runnable.run();
background_runnable.runnable.run();
} else if !state.scheduled_from_foreground.is_empty() {
let available_cx_ids = state
.scheduled_from_foreground
@@ -266,6 +332,7 @@ impl Deterministic {
if scheduled_from_cx.is_empty() {
state.scheduled_from_foreground.remove(&cx_id_to_run);
}
state.poll_history.push(foreground_runnable.id);
drop(state);
@@ -298,9 +365,10 @@ impl Deterministic {
let runnable_count = state.scheduled_from_background.len();
let ix = state.rng.gen_range(0..=runnable_count);
if ix < state.scheduled_from_background.len() {
let runnable = state.scheduled_from_background.remove(ix);
let background_runnable = state.scheduled_from_background.remove(ix);
state.poll_history.push(background_runnable.id);
drop(state);
runnable.run();
background_runnable.runnable.run();
} else {
drop(state);
if let Poll::Ready(result) = future.poll(&mut cx) {

View File

@@ -17,10 +17,15 @@ use crate::{
SceneBuilder, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle,
WeakViewHandle,
};
use anyhow::bail;
use collections::{HashMap, HashSet};
use pathfinder_geometry::vector::{vec2f, Vector2F};
use serde_json::json;
use smallvec::SmallVec;
use sqlez::{
bindable::{Bind, Column},
statement::Statement,
};
use std::{
marker::PhantomData,
ops::{Deref, DerefMut, Range},
@@ -863,8 +868,9 @@ pub struct DebugContext<'a> {
pub app: &'a AppContext,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Axis {
#[default]
Horizontal,
Vertical,
}
@@ -894,6 +900,31 @@ impl ToJson for Axis {
}
}
impl Bind for Axis {
fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
match self {
Axis::Horizontal => "Horizontal",
Axis::Vertical => "Vertical",
}
.bind(statement, start_index)
}
}
impl Column for Axis {
fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
String::column(statement, start_index).and_then(|(axis_text, next_index)| {
Ok((
match axis_text.as_str() {
"Horizontal" => Axis::Horizontal,
"Vertical" => Axis::Vertical,
_ => bail!("Stored serialized item kind is incorrect"),
},
next_index,
))
})
}
}
pub trait Vector2FExt {
fn along(self, axis: Axis) -> f32;
}

View File

@@ -1,11 +1,13 @@
use crate::{
elements::Empty, executor, platform, Element, ElementBox, Entity, FontCache, Handle,
LeakDetector, MutableAppContext, Platform, RenderContext, Subscription, TestAppContext, View,
elements::Empty, executor, platform, util::CwdBacktrace, Element, ElementBox, Entity,
FontCache, Handle, LeakDetector, MutableAppContext, Platform, RenderContext, Subscription,
TestAppContext, View,
};
use futures::StreamExt;
use parking_lot::Mutex;
use smol::channel;
use std::{
fmt::Write,
panic::{self, RefUnwindSafe},
rc::Rc,
sync::{
@@ -29,13 +31,13 @@ pub fn run_test(
mut num_iterations: u64,
mut starting_seed: u64,
max_retries: usize,
detect_nondeterminism: bool,
test_fn: &mut (dyn RefUnwindSafe
+ Fn(
&mut MutableAppContext,
Rc<platform::test::ForegroundPlatform>,
Arc<executor::Deterministic>,
u64,
bool,
)),
fn_name: String,
) {
@@ -60,16 +62,20 @@ pub fn run_test(
let platform = Arc::new(platform::test::platform());
let font_system = platform.fonts();
let font_cache = Arc::new(FontCache::new(font_system));
let mut prev_runnable_history: Option<Vec<usize>> = None;
loop {
let seed = atomic_seed.fetch_add(1, SeqCst);
let is_last_iteration = seed + 1 >= starting_seed + num_iterations;
for _ in 0..num_iterations {
let seed = atomic_seed.load(SeqCst);
if is_randomized {
dbg!(seed);
}
let deterministic = executor::Deterministic::new(seed);
if detect_nondeterminism {
deterministic.enable_runnable_backtrace();
}
let leak_detector = Arc::new(Mutex::new(LeakDetector::default()));
let mut cx = TestAppContext::new(
foreground_platform.clone(),
@@ -82,13 +88,7 @@ pub fn run_test(
fn_name.clone(),
);
cx.update(|cx| {
test_fn(
cx,
foreground_platform.clone(),
deterministic.clone(),
seed,
is_last_iteration,
);
test_fn(cx, foreground_platform.clone(), deterministic.clone(), seed);
});
cx.update(|cx| cx.remove_all_windows());
@@ -96,8 +96,64 @@ pub fn run_test(
cx.update(|cx| cx.clear_globals());
leak_detector.lock().detect();
if is_last_iteration {
break;
if detect_nondeterminism {
let curr_runnable_history = deterministic.runnable_history();
if let Some(prev_runnable_history) = prev_runnable_history {
let mut prev_entries = prev_runnable_history.iter().fuse();
let mut curr_entries = curr_runnable_history.iter().fuse();
let mut nondeterministic = false;
let mut common_history_prefix = Vec::new();
let mut prev_history_suffix = Vec::new();
let mut curr_history_suffix = Vec::new();
loop {
match (prev_entries.next(), curr_entries.next()) {
(None, None) => break,
(None, Some(curr_id)) => curr_history_suffix.push(*curr_id),
(Some(prev_id), None) => prev_history_suffix.push(*prev_id),
(Some(prev_id), Some(curr_id)) => {
if nondeterministic {
prev_history_suffix.push(*prev_id);
curr_history_suffix.push(*curr_id);
} else if prev_id == curr_id {
common_history_prefix.push(*curr_id);
} else {
nondeterministic = true;
prev_history_suffix.push(*prev_id);
curr_history_suffix.push(*curr_id);
}
}
}
}
if nondeterministic {
let mut error = String::new();
writeln!(&mut error, "Common prefix: {:?}", common_history_prefix)
.unwrap();
writeln!(&mut error, "Previous suffix: {:?}", prev_history_suffix)
.unwrap();
writeln!(&mut error, "Current suffix: {:?}", curr_history_suffix)
.unwrap();
let last_common_backtrace = common_history_prefix
.last()
.map(|runnable_id| deterministic.runnable_backtrace(*runnable_id));
writeln!(
&mut error,
"Last future that ran on both executions: {:?}",
last_common_backtrace.as_ref().map(CwdBacktrace)
)
.unwrap();
panic!("Detected non-determinism.\n{}", error);
}
}
prev_runnable_history = Some(curr_runnable_history);
}
if !detect_nondeterminism {
atomic_seed.fetch_add(1, SeqCst);
}
}
});
@@ -112,7 +168,7 @@ pub fn run_test(
println!("retrying: attempt {}", retries);
} else {
if is_randomized {
eprintln!("failing seed: {}", atomic_seed.load(SeqCst) - 1);
eprintln!("failing seed: {}", atomic_seed.load(SeqCst));
}
panic::resume_unwind(error);
}

View File

@@ -12,3 +12,4 @@ doctest = false
syn = "1.0"
quote = "1.0"
proc-macro2 = "1.0"

View File

@@ -14,6 +14,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
let mut max_retries = 0;
let mut num_iterations = 1;
let mut starting_seed = 0;
let mut detect_nondeterminism = false;
for arg in args {
match arg {
@@ -26,6 +27,9 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
let key_name = meta.path.get_ident().map(|i| i.to_string());
let result = (|| {
match key_name.as_deref() {
Some("detect_nondeterminism") => {
detect_nondeterminism = parse_bool(&meta.lit)?
}
Some("retries") => max_retries = parse_int(&meta.lit)?,
Some("iterations") => num_iterations = parse_int(&meta.lit)?,
Some("seed") => starting_seed = parse_int(&meta.lit)?,
@@ -77,10 +81,6 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed),));
continue;
}
Some("bool") => {
inner_fn_args.extend(quote!(is_last_iteration,));
continue;
}
Some("Arc") => {
if let syn::PathArguments::AngleBracketed(args) =
&last_segment.unwrap().arguments
@@ -146,7 +146,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
#num_iterations as u64,
#starting_seed as u64,
#max_retries,
&mut |cx, foreground_platform, deterministic, seed, is_last_iteration| {
#detect_nondeterminism,
&mut |cx, foreground_platform, deterministic, seed| {
#cx_vars
cx.foreground().run(#inner_fn_name(#inner_fn_args));
#cx_teardowns
@@ -165,9 +166,6 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
Some("StdRng") => {
inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed),));
}
Some("bool") => {
inner_fn_args.extend(quote!(is_last_iteration,));
}
_ => {}
}
} else {
@@ -189,7 +187,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
#num_iterations as u64,
#starting_seed as u64,
#max_retries,
&mut |cx, _, _, seed, is_last_iteration| #inner_fn_name(#inner_fn_args),
#detect_nondeterminism,
&mut |cx, _, _, seed| #inner_fn_name(#inner_fn_args),
stringify!(#outer_fn_name).to_string(),
);
}
@@ -209,3 +208,13 @@ fn parse_int(literal: &Lit) -> Result<usize, TokenStream> {
result.map_err(|err| TokenStream::from(err.into_compile_error()))
}
fn parse_bool(literal: &Lit) -> Result<bool, TokenStream> {
let result = if let Lit::Bool(result) = &literal {
Ok(result.value)
} else {
Err(syn::Error::new(literal.span(), "must be a boolean"))
};
result.map_err(|err| TokenStream::from(err.into_compile_error()))
}

View File

@@ -115,7 +115,7 @@ mod tests {
#[test]
fn test_heading_entry_defaults_to_hour_12() {
let naive_time = NaiveTime::from_hms_milli(15, 0, 0, 0);
let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap();
let actual_heading_entry = heading_entry(naive_time, &None);
let expected_heading_entry = "# 3:00 PM";
@@ -124,7 +124,7 @@ mod tests {
#[test]
fn test_heading_entry_is_hour_12() {
let naive_time = NaiveTime::from_hms_milli(15, 0, 0, 0);
let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap();
let actual_heading_entry = heading_entry(naive_time, &Some(HourFormat::Hour12));
let expected_heading_entry = "# 3:00 PM";
@@ -133,7 +133,7 @@ mod tests {
#[test]
fn test_heading_entry_is_hour_24() {
let naive_time = NaiveTime::from_hms_milli(15, 0, 0, 0);
let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap();
let actual_heading_entry = heading_entry(naive_time, &Some(HourFormat::Hour24));
let expected_heading_entry = "# 15:00";

View File

@@ -2225,11 +2225,12 @@ impl BufferSnapshot {
range: Range<T>,
) -> Option<(Range<usize>, Range<usize>)> {
// Find bracket pairs that *inclusively* contain the given range.
let range = range.start.to_offset(self).saturating_sub(1)
..self.len().min(range.end.to_offset(self) + 1);
let mut matches = self.syntax.matches(range, &self.text, |grammar| {
grammar.brackets_config.as_ref().map(|c| &c.query)
});
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut matches = self.syntax.matches(
range.start.saturating_sub(1)..self.len().min(range.end + 1),
&self.text,
|grammar| grammar.brackets_config.as_ref().map(|c| &c.query),
);
let configs = matches
.grammars()
.iter()
@@ -2252,18 +2253,20 @@ impl BufferSnapshot {
matches.advance();
if let Some((open, close)) = open.zip(close) {
let len = close.end - open.start;
if let Some((existing_open, existing_close)) = &result {
let existing_len = existing_close.end - existing_open.start;
if len > existing_len {
continue;
}
}
result = Some((open, close));
let Some((open, close)) = open.zip(close) else { continue };
if open.start > range.start || close.end < range.end {
continue;
}
let len = close.end - open.start;
if let Some((existing_open, existing_close)) = &result {
let existing_len = existing_close.end - existing_open.start;
if len > existing_len {
continue;
}
}
result = Some((open, close));
}
result

View File

@@ -573,14 +573,72 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
))
);
// Regression test: avoid crash when querying at the end of the buffer.
assert_eq!(
buffer.enclosing_bracket_point_ranges(buffer.len() - 1..buffer.len()),
buffer.enclosing_bracket_point_ranges(Point::new(4, 1)..Point::new(4, 1)),
Some((
Point::new(0, 6)..Point::new(0, 7),
Point::new(4, 0)..Point::new(4, 1)
))
);
// Regression test: avoid crash when querying at the end of the buffer.
assert_eq!(
buffer.enclosing_bracket_point_ranges(Point::new(4, 1)..Point::new(5, 0)),
None
);
}
#[gpui::test]
fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(
cx: &mut MutableAppContext,
) {
let javascript_language = Arc::new(
Language::new(
LanguageConfig {
name: "JavaScript".into(),
..Default::default()
},
Some(tree_sitter_javascript::language()),
)
.with_brackets_query(
r#"
("{" @open "}" @close)
("(" @open ")" @close)
"#,
)
.unwrap(),
);
cx.set_global(Settings::test(cx));
let buffer = cx.add_model(|cx| {
let text = "
for (const a in b) {
// a comment that's longer than the for-loop header
}
"
.unindent();
Buffer::new(0, text, cx).with_language(javascript_language, cx)
});
let buffer = buffer.read(cx);
assert_eq!(
buffer.enclosing_bracket_point_ranges(Point::new(0, 18)..Point::new(0, 18)),
Some((
Point::new(0, 4)..Point::new(0, 5),
Point::new(0, 17)..Point::new(0, 18)
))
);
// Regression test: even though the parent node of the parentheses (the for loop) does
// intersect the given range, the parentheses themselves do not contain the range, so
// they should not be returned. Only the curly braces contain the range.
assert_eq!(
buffer.enclosing_bracket_point_ranges(Point::new(0, 20)..Point::new(0, 20)),
Some((
Point::new(0, 19)..Point::new(0, 20),
Point::new(2, 0)..Point::new(2, 1)
))
);
}
#[gpui::test]
@@ -1337,6 +1395,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) {
(0..entry_count).map(|_| {
let range = buffer.random_byte_range(0, &mut rng);
let range = range.to_point_utf16(buffer);
let range = range.start..range.end;
DiagnosticEntry {
range,
diagnostic: Diagnostic {

View File

@@ -71,7 +71,7 @@ impl DiagnosticSet {
diagnostics: SumTree::from_iter(
entries.into_iter().map(|entry| DiagnosticEntry {
range: buffer.anchor_before(entry.range.start)
..buffer.anchor_after(entry.range.end),
..buffer.anchor_before(entry.range.end),
diagnostic: entry.diagnostic,
}),
buffer,

View File

@@ -1053,8 +1053,8 @@ pub fn point_to_lsp(point: PointUtf16) -> lsp::Position {
lsp::Position::new(point.row, point.column)
}
pub fn point_from_lsp(point: lsp::Position) -> PointUtf16 {
PointUtf16::new(point.line, point.character)
pub fn point_from_lsp(point: lsp::Position) -> Unclipped<PointUtf16> {
Unclipped(PointUtf16::new(point.line, point.character))
}
pub fn range_to_lsp(range: Range<PointUtf16>) -> lsp::Range {
@@ -1064,7 +1064,7 @@ pub fn range_to_lsp(range: Range<PointUtf16>) -> lsp::Range {
}
}
pub fn range_from_lsp(range: lsp::Range) -> Range<PointUtf16> {
pub fn range_from_lsp(range: lsp::Range) -> Range<Unclipped<PointUtf16>> {
let mut start = point_from_lsp(range.start);
let mut end = point_from_lsp(range.end);
if start > end {

View File

@@ -128,8 +128,8 @@ impl LspCommand for PrepareRename {
) = message
{
let Range { start, end } = range_from_lsp(range);
if buffer.clip_point_utf16(start, Bias::Left) == start
&& buffer.clip_point_utf16(end, Bias::Left) == end
if buffer.clip_point_utf16(start, Bias::Left) == start.0
&& buffer.clip_point_utf16(end, Bias::Left) == end.0
{
return Ok(Some(buffer.anchor_after(start)..buffer.anchor_before(end)));
}

View File

@@ -10,7 +10,11 @@ use anyhow::{anyhow, Context, Result};
use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
use clock::ReplicaId;
use collections::{hash_map, BTreeMap, HashMap, HashSet};
use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
use futures::{
channel::{mpsc, oneshot},
future::Shared,
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
};
use gpui::{
AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
@@ -26,6 +30,7 @@ use language::{
CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent,
File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, OffsetRangeExt,
Operation, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction,
Unclipped,
};
use lsp::{
DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer, LanguageString,
@@ -44,12 +49,10 @@ use std::{
cell::RefCell,
cmp::{self, Ordering},
convert::TryInto,
ffi::OsString,
hash::Hash,
mem,
num::NonZeroU32,
ops::Range,
os::unix::{ffi::OsStrExt, prelude::OsStringExt},
path::{Component, Path, PathBuf},
rc::Rc,
str,
@@ -62,7 +65,6 @@ use std::{
use thiserror::Error;
use util::{defer, post_inc, ResultExt, TryFutureExt as _};
pub use db::Db;
pub use fs::*;
pub use worktree::*;
@@ -70,10 +72,6 @@ pub trait Item: Entity {
fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId>;
}
pub struct ProjectStore {
projects: Vec<WeakModelHandle<Project>>,
}
// Language server state is stored across 3 collections:
// language_servers =>
// a mapping from unique server id to LanguageServerState which can either be a task for a
@@ -102,7 +100,6 @@ pub struct Project {
next_entry_id: Arc<AtomicUsize>,
next_diagnostic_group_id: usize,
user_store: ModelHandle<UserStore>,
project_store: ModelHandle<ProjectStore>,
fs: Arc<dyn Fs>,
client_state: Option<ProjectClientState>,
collaborators: HashMap<PeerId, Collaborator>,
@@ -152,6 +149,8 @@ enum WorktreeHandle {
enum ProjectClientState {
Local {
remote_id: u64,
metadata_changed: mpsc::UnboundedSender<oneshot::Sender<()>>,
_maintain_metadata: Task<()>,
_detect_unshare: Task<Option<()>>,
},
Remote {
@@ -252,7 +251,7 @@ pub struct Symbol {
pub label: CodeLabel,
pub name: String,
pub kind: lsp::SymbolKind,
pub range: Range<PointUtf16>,
pub range: Range<Unclipped<PointUtf16>>,
pub signature: [u8; 32],
}
@@ -412,46 +411,39 @@ impl Project {
pub fn local(
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
project_store: ModelHandle<ProjectStore>,
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
cx: &mut MutableAppContext,
) -> ModelHandle<Self> {
cx.add_model(|cx: &mut ModelContext<Self>| {
let handle = cx.weak_handle();
project_store.update(cx, |store, cx| store.add_project(handle, cx));
Self {
worktrees: Default::default(),
collaborators: Default::default(),
opened_buffers: Default::default(),
shared_buffers: Default::default(),
incomplete_buffers: Default::default(),
loading_buffers: Default::default(),
loading_local_worktrees: Default::default(),
buffer_snapshots: Default::default(),
client_state: None,
opened_buffer: watch::channel(),
client_subscriptions: Vec::new(),
_subscriptions: vec![cx.observe_global::<Settings, _>(Self::on_settings_changed)],
_maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx),
active_entry: None,
languages,
client,
user_store,
project_store,
fs,
next_entry_id: Default::default(),
next_diagnostic_group_id: Default::default(),
language_servers: Default::default(),
language_server_ids: Default::default(),
language_server_statuses: Default::default(),
last_workspace_edits_by_language_server: Default::default(),
language_server_settings: Default::default(),
buffers_being_formatted: Default::default(),
next_language_server_id: 0,
nonce: StdRng::from_entropy().gen(),
}
cx.add_model(|cx: &mut ModelContext<Self>| Self {
worktrees: Default::default(),
collaborators: Default::default(),
opened_buffers: Default::default(),
shared_buffers: Default::default(),
incomplete_buffers: Default::default(),
loading_buffers: Default::default(),
loading_local_worktrees: Default::default(),
buffer_snapshots: Default::default(),
client_state: None,
opened_buffer: watch::channel(),
client_subscriptions: Vec::new(),
_subscriptions: vec![cx.observe_global::<Settings, _>(Self::on_settings_changed)],
_maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx),
active_entry: None,
languages,
client,
user_store,
fs,
next_entry_id: Default::default(),
next_diagnostic_group_id: Default::default(),
language_servers: Default::default(),
language_server_ids: Default::default(),
language_server_statuses: Default::default(),
last_workspace_edits_by_language_server: Default::default(),
language_server_settings: Default::default(),
buffers_being_formatted: Default::default(),
next_language_server_id: 0,
nonce: StdRng::from_entropy().gen(),
})
}
@@ -459,31 +451,28 @@ impl Project {
remote_id: u64,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
project_store: ModelHandle<ProjectStore>,
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
mut cx: AsyncAppContext,
) -> Result<ModelHandle<Self>, JoinProjectError> {
client.authenticate_and_connect(true, &cx).await?;
let subscription = client.subscribe_to_entity(remote_id);
let response = client
.request(proto::JoinProject {
project_id: remote_id,
})
.await?;
let this = cx.add_model(|cx| {
let replica_id = response.replica_id as ReplicaId;
let replica_id = response.replica_id as ReplicaId;
let mut worktrees = Vec::new();
for worktree in response.worktrees {
let worktree = cx
.update(|cx| Worktree::remote(remote_id, replica_id, worktree, client.clone(), cx));
worktrees.push(worktree);
}
let this = cx.add_model(|cx: &mut ModelContext<Self>| {
let handle = cx.weak_handle();
project_store.update(cx, |store, cx| store.add_project(handle, cx));
let mut worktrees = Vec::new();
for worktree in response.worktrees {
let worktree = cx.update(|cx| {
Worktree::remote(remote_id, replica_id, worktree, client.clone(), cx)
});
worktrees.push(worktree);
}
let mut this = Self {
worktrees: Vec::new(),
@@ -497,11 +486,10 @@ impl Project {
_maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx),
languages,
user_store: user_store.clone(),
project_store,
fs,
next_entry_id: Default::default(),
next_diagnostic_group_id: Default::default(),
client_subscriptions: vec![client.add_model_for_remote_entity(remote_id, cx)],
client_subscriptions: Default::default(),
_subscriptions: Default::default(),
client: client.clone(),
client_state: Some(ProjectClientState::Remote {
@@ -550,10 +538,11 @@ impl Project {
nonce: StdRng::from_entropy().gen(),
};
for worktree in worktrees {
this.add_worktree(&worktree, cx);
let _ = this.add_worktree(&worktree, cx);
}
this
});
let subscription = subscription.set_model(&this, &mut cx);
let user_ids = response
.collaborators
@@ -571,6 +560,7 @@ impl Project {
this.update(&mut cx, |this, _| {
this.collaborators = collaborators;
this.client_subscriptions.push(subscription);
});
Ok(this)
@@ -593,9 +583,7 @@ impl Project {
let http_client = client::test::FakeHttpClient::with_404_response();
let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
let project_store = cx.add_model(|_| ProjectStore::new());
let project =
cx.update(|cx| Project::local(client, user_store, project_store, languages, fs, cx));
let project = cx.update(|cx| Project::local(client, user_store, languages, fs, cx));
for path in root_paths {
let (tree, _) = project
.update(cx, |project, cx| {
@@ -676,10 +664,6 @@ impl Project {
self.user_store.clone()
}
pub fn project_store(&self) -> ModelHandle<ProjectStore> {
self.project_store.clone()
}
#[cfg(any(test, feature = "test-support"))]
pub fn check_invariants(&self, cx: &AppContext) {
if self.is_local() {
@@ -751,59 +735,29 @@ impl Project {
}
}
fn metadata_changed(&mut self, cx: &mut ModelContext<Self>) {
if let Some(ProjectClientState::Local { remote_id, .. }) = &self.client_state {
let project_id = *remote_id;
// Broadcast worktrees only if the project is online.
let worktrees = self
.worktrees
.iter()
.filter_map(|worktree| {
worktree
.upgrade(cx)
.map(|worktree| worktree.read(cx).as_local().unwrap().metadata_proto())
})
.collect();
self.client
.send(proto::UpdateProject {
project_id,
worktrees,
})
.log_err();
let worktrees = self.visible_worktrees(cx).collect::<Vec<_>>();
let scans_complete = futures::future::join_all(
worktrees
.iter()
.filter_map(|worktree| Some(worktree.read(cx).as_local()?.scan_complete())),
);
let worktrees = worktrees.into_iter().map(|handle| handle.downgrade());
cx.spawn_weak(move |_, cx| async move {
scans_complete.await;
cx.read(|cx| {
for worktree in worktrees {
if let Some(worktree) = worktree
.upgrade(cx)
.and_then(|worktree| worktree.read(cx).as_local())
{
worktree.send_extension_counts(project_id);
}
}
})
})
.detach();
fn metadata_changed(&mut self, cx: &mut ModelContext<Self>) -> impl Future<Output = ()> {
let (tx, rx) = oneshot::channel();
if let Some(ProjectClientState::Local {
metadata_changed, ..
}) = &mut self.client_state
{
let _ = metadata_changed.unbounded_send(tx);
}
self.project_store.update(cx, |_, cx| cx.notify());
cx.notify();
async move {
// If the project is shared, this will resolve when the `_maintain_metadata` task has
// a chance to update the metadata. Otherwise, it will resolve right away because `tx`
// will get dropped.
let _ = rx.await;
}
}
pub fn collaborators(&self) -> &HashMap<PeerId, Collaborator> {
&self.collaborators
}
/// Collect all worktrees, including ones that don't appear in the project panel
pub fn worktrees<'a>(
&'a self,
cx: &'a AppContext,
@@ -813,6 +767,7 @@ impl Project {
.filter_map(move |worktree| worktree.upgrade(cx))
}
/// Collect all user-visible worktrees, the ones that appear in the project panel
pub fn visible_worktrees<'a>(
&'a self,
cx: &'a AppContext,
@@ -897,7 +852,7 @@ impl Project {
.request(proto::CreateProjectEntry {
worktree_id: project_path.worktree_id.to_proto(),
project_id,
path: project_path.path.as_os_str().as_bytes().to_vec(),
path: project_path.path.to_string_lossy().into(),
is_directory,
})
.await?;
@@ -941,7 +896,7 @@ impl Project {
.request(proto::CopyProjectEntry {
project_id,
entry_id: entry_id.to_proto(),
new_path: new_path.as_os_str().as_bytes().to_vec(),
new_path: new_path.to_string_lossy().into(),
})
.await?;
let entry = response
@@ -984,7 +939,7 @@ impl Project {
.request(proto::RenameProjectEntry {
project_id,
entry_id: entry_id.to_proto(),
new_path: new_path.as_os_str().as_bytes().to_vec(),
new_path: new_path.to_string_lossy().into(),
})
.await?;
let entry = response
@@ -1085,15 +1040,51 @@ impl Project {
});
}
self.client_subscriptions
.push(self.client.add_model_for_remote_entity(project_id, cx));
self.metadata_changed(cx);
self.client_subscriptions.push(
self.client
.subscribe_to_entity(project_id)
.set_model(&cx.handle(), &mut cx.to_async()),
);
let _ = self.metadata_changed(cx);
cx.emit(Event::RemoteIdChanged(Some(project_id)));
cx.notify();
let mut status = self.client.status();
let (metadata_changed_tx, mut metadata_changed_rx) = mpsc::unbounded();
self.client_state = Some(ProjectClientState::Local {
remote_id: project_id,
metadata_changed: metadata_changed_tx,
_maintain_metadata: cx.spawn_weak(move |this, cx| async move {
while let Some(tx) = metadata_changed_rx.next().await {
let mut txs = vec![tx];
while let Ok(Some(next_tx)) = metadata_changed_rx.try_next() {
txs.push(next_tx);
}
let Some(this) = this.upgrade(&cx) else { break };
this.read_with(&cx, |this, cx| {
let worktrees = this
.worktrees
.iter()
.filter_map(|worktree| {
worktree.upgrade(cx).map(|worktree| {
worktree.read(cx).as_local().unwrap().metadata_proto()
})
})
.collect();
this.client.request(proto::UpdateProject {
project_id,
worktrees,
})
})
.await
.log_err();
for tx in txs {
let _ = tx.send(());
}
}
}),
_detect_unshare: cx.spawn_weak(move |this, mut cx| {
async move {
let is_connected = status.next().await.map_or(false, |s| s.is_connected());
@@ -1143,7 +1134,7 @@ impl Project {
}
}
self.metadata_changed(cx);
let _ = self.metadata_changed(cx);
cx.notify();
self.client.send(proto::UnshareProject {
project_id: remote_id,
@@ -1632,10 +1623,6 @@ impl Project {
operations: vec![language::proto::serialize_operation(operation)],
});
cx.background().spawn(request).detach_and_log_err(cx);
} else if let Some(project_id) = self.remote_id() {
let _ = self
.client
.send(proto::RegisterProjectActivity { project_id });
}
}
BufferEvent::Edited { .. } => {
@@ -2597,7 +2584,7 @@ impl Project {
language_server_id: usize,
abs_path: PathBuf,
version: Option<i32>,
diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
cx: &mut ModelContext<Project>,
) -> Result<(), anyhow::Error> {
let (worktree, relative_path) = self
@@ -2635,7 +2622,7 @@ impl Project {
fn update_buffer_diagnostics(
&mut self,
buffer: &ModelHandle<Buffer>,
mut diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
mut diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
version: Option<i32>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
@@ -2659,7 +2646,7 @@ impl Project {
let mut sanitized_diagnostics = Vec::new();
let edits_since_save = Patch::new(
snapshot
.edits_since::<PointUtf16>(buffer.read(cx).saved_version())
.edits_since::<Unclipped<PointUtf16>>(buffer.read(cx).saved_version())
.collect(),
);
for entry in diagnostics {
@@ -2679,13 +2666,14 @@ impl Project {
let mut range = snapshot.clip_point_utf16(start, Bias::Left)
..snapshot.clip_point_utf16(end, Bias::Right);
// Expand empty ranges by one character
// Expand empty ranges by one codepoint
if range.start == range.end {
// This will be go to the next boundary when being clipped
range.end.column += 1;
range.end = snapshot.clip_point_utf16(range.end, Bias::Right);
range.end = snapshot.clip_point_utf16(Unclipped(range.end), Bias::Right);
if range.start == range.end && range.end.column > 0 {
range.start.column -= 1;
range.start = snapshot.clip_point_utf16(range.start, Bias::Left);
range.end = snapshot.clip_point_utf16(Unclipped(range.end), Bias::Left);
}
}
@@ -3288,7 +3276,7 @@ impl Project {
return Task::ready(Ok(Default::default()));
};
let position = position.to_point_utf16(source_buffer);
let position = Unclipped(position.to_point_utf16(source_buffer));
let anchor = source_buffer.anchor_after(position);
if worktree.read(cx).as_local().is_some() {
@@ -3307,7 +3295,7 @@ impl Project {
lsp::TextDocumentIdentifier::new(
lsp::Url::from_file_path(buffer_abs_path).unwrap(),
),
point_to_lsp(position),
point_to_lsp(position.0),
),
context: Default::default(),
work_done_progress_params: Default::default(),
@@ -3350,7 +3338,7 @@ impl Project {
let range = range_from_lsp(edit.range);
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
if start != range.start || end != range.end {
if start != range.start.0 || end != range.end.0 {
log::info!("completion out of expected range");
return None;
}
@@ -3362,7 +3350,7 @@ impl Project {
// If the language server does not provide a range, then infer
// the range based on the syntax tree.
None => {
if position != clipped_position {
if position.0 != clipped_position {
log::info!("completion out of expected range");
return None;
}
@@ -3426,19 +3414,29 @@ impl Project {
position: Some(language::proto::serialize_anchor(&anchor)),
version: serialize_version(&source_buffer.version()),
};
cx.spawn_weak(|_, mut cx| async move {
cx.spawn_weak(|this, mut cx| async move {
let response = rpc.request(message).await?;
source_buffer_handle
.update(&mut cx, |buffer, _| {
buffer.wait_for_version(deserialize_version(response.version))
})
.await;
if this
.upgrade(&cx)
.ok_or_else(|| anyhow!("project was dropped"))?
.read_with(&cx, |this, _| this.is_read_only())
{
return Err(anyhow!(
"failed to get completions: project was disconnected"
));
} else {
source_buffer_handle
.update(&mut cx, |buffer, _| {
buffer.wait_for_version(deserialize_version(response.version))
})
.await;
let completions = response.completions.into_iter().map(|completion| {
language::proto::deserialize_completion(completion, language.clone())
});
futures::future::try_join_all(completions).await
let completions = response.completions.into_iter().map(|completion| {
language::proto::deserialize_completion(completion, language.clone())
});
futures::future::try_join_all(completions).await
}
})
} else {
Task::ready(Ok(Default::default()))
@@ -3615,7 +3613,7 @@ impl Project {
} else if let Some(project_id) = self.remote_id() {
let rpc = self.client.clone();
let version = buffer.version();
cx.spawn_weak(|_, mut cx| async move {
cx.spawn_weak(|this, mut cx| async move {
let response = rpc
.request(proto::GetCodeActions {
project_id,
@@ -3626,17 +3624,27 @@ impl Project {
})
.await?;
buffer_handle
.update(&mut cx, |buffer, _| {
buffer.wait_for_version(deserialize_version(response.version))
})
.await;
if this
.upgrade(&cx)
.ok_or_else(|| anyhow!("project was dropped"))?
.read_with(&cx, |this, _| this.is_read_only())
{
return Err(anyhow!(
"failed to get code actions: project was disconnected"
));
} else {
buffer_handle
.update(&mut cx, |buffer, _| {
buffer.wait_for_version(deserialize_version(response.version))
})
.await;
response
.actions
.into_iter()
.map(language::proto::deserialize_code_action)
.collect()
response
.actions
.into_iter()
.map(language::proto::deserialize_code_action)
.collect()
}
})
} else {
Task::ready(Ok(Default::default()))
@@ -4145,9 +4153,13 @@ impl Project {
let message = request.to_proto(project_id, buffer);
return cx.spawn(|this, cx| async move {
let response = rpc.request(message).await?;
request
.response_from_proto(response, this, buffer_handle, cx)
.await
if this.read_with(&cx, |this, _| this.is_read_only()) {
Err(anyhow!("disconnected before completing request"))
} else {
request
.response_from_proto(response, this, buffer_handle, cx)
.await
}
});
}
Task::ready(Ok(Default::default()))
@@ -4225,12 +4237,13 @@ impl Project {
});
let worktree = worktree?;
let project_id = project.update(&mut cx, |project, cx| {
project.add_worktree(&worktree, cx);
project.remote_id()
});
project
.update(&mut cx, |project, cx| project.add_worktree(&worktree, cx))
.await;
if let Some(project_id) = project_id {
if let Some(project_id) =
project.read_with(&cx, |project, _| project.remote_id())
{
worktree
.update(&mut cx, |worktree, cx| {
worktree.as_local_mut().unwrap().share(project_id, cx)
@@ -4254,7 +4267,11 @@ impl Project {
})
}
pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
pub fn remove_worktree(
&mut self,
id_to_remove: WorktreeId,
cx: &mut ModelContext<Self>,
) -> impl Future<Output = ()> {
self.worktrees.retain(|worktree| {
if let Some(worktree) = worktree.upgrade(cx) {
let id = worktree.read(cx).id();
@@ -4268,11 +4285,14 @@ impl Project {
false
}
});
self.metadata_changed(cx);
cx.notify();
self.metadata_changed(cx)
}
fn add_worktree(&mut self, worktree: &ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
fn add_worktree(
&mut self,
worktree: &ModelHandle<Worktree>,
cx: &mut ModelContext<Self>,
) -> impl Future<Output = ()> {
cx.observe(worktree, |_, _, cx| cx.notify()).detach();
if worktree.read(cx).is_local() {
cx.subscribe(worktree, |this, worktree, event, cx| match event {
@@ -4296,15 +4316,13 @@ impl Project {
.push(WorktreeHandle::Weak(worktree.downgrade()));
}
self.metadata_changed(cx);
cx.observe_release(worktree, |this, worktree, cx| {
this.remove_worktree(worktree.id(), cx);
cx.notify();
let _ = this.remove_worktree(worktree.id(), cx);
})
.detach();
cx.emit(Event::WorktreeAdded);
cx.notify();
self.metadata_changed(cx)
}
fn update_local_worktree_buffers(
@@ -4621,11 +4639,11 @@ impl Project {
} else {
let worktree =
Worktree::remote(remote_id, replica_id, worktree, client.clone(), cx);
this.add_worktree(&worktree, cx);
let _ = this.add_worktree(&worktree, cx);
}
}
this.metadata_changed(cx);
let _ = this.metadata_changed(cx);
for (id, _) in old_worktrees_by_id {
cx.emit(Event::WorktreeRemoved(id));
}
@@ -4667,7 +4685,7 @@ impl Project {
let entry = worktree
.update(&mut cx, |worktree, cx| {
let worktree = worktree.as_local_mut().unwrap();
let path = PathBuf::from(OsString::from_vec(envelope.payload.path));
let path = PathBuf::from(envelope.payload.path);
worktree.create_entry(path, envelope.payload.is_directory, cx)
})
.await?;
@@ -4691,7 +4709,7 @@ impl Project {
let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id());
let entry = worktree
.update(&mut cx, |worktree, cx| {
let new_path = PathBuf::from(OsString::from_vec(envelope.payload.new_path));
let new_path = PathBuf::from(envelope.payload.new_path);
worktree
.as_local_mut()
.unwrap()
@@ -4719,7 +4737,7 @@ impl Project {
let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id());
let entry = worktree
.update(&mut cx, |worktree, cx| {
let new_path = PathBuf::from(OsString::from_vec(envelope.payload.new_path));
let new_path = PathBuf::from(envelope.payload.new_path);
worktree
.as_local_mut()
.unwrap()
@@ -5117,22 +5135,30 @@ impl Project {
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<proto::GetCompletionsResponse> {
let position = envelope
.payload
.position
.and_then(language::proto::deserialize_anchor)
.ok_or_else(|| anyhow!("invalid position"))?;
let version = deserialize_version(envelope.payload.version);
let buffer = this.read_with(&cx, |this, cx| {
this.opened_buffers
.get(&envelope.payload.buffer_id)
.and_then(|buffer| buffer.upgrade(cx))
.ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))
})?;
let position = envelope
.payload
.position
.and_then(language::proto::deserialize_anchor)
.map(|p| {
buffer.read_with(&cx, |buffer, _| {
buffer.clip_point_utf16(Unclipped(p.to_point_utf16(buffer)), Bias::Left)
})
})
.ok_or_else(|| anyhow!("invalid position"))?;
let version = deserialize_version(envelope.payload.version);
buffer
.update(&mut cx, |buffer, _| buffer.wait_for_version(version))
.await;
let version = buffer.read_with(&cx, |buffer, _| buffer.version());
let completions = this
.update(&mut cx, |this, cx| this.completions(&buffer, position, cx))
.await?;
@@ -5619,8 +5645,8 @@ impl Project {
},
name: serialized_symbol.name,
range: PointUtf16::new(start.row, start.column)
..PointUtf16::new(end.row, end.column),
range: Unclipped(PointUtf16::new(start.row, start.column))
..Unclipped(PointUtf16::new(end.row, end.column)),
kind,
signature: serialized_symbol
.signature
@@ -5706,10 +5732,10 @@ impl Project {
let mut lsp_edits = lsp_edits.into_iter().peekable();
let mut edits = Vec::new();
while let Some((mut range, mut new_text)) = lsp_edits.next() {
while let Some((range, mut new_text)) = lsp_edits.next() {
// Clip invalid ranges provided by the language server.
range.start = snapshot.clip_point_utf16(range.start, Bias::Left);
range.end = snapshot.clip_point_utf16(range.end, Bias::Left);
let mut range = snapshot.clip_point_utf16(range.start, Bias::Left)
..snapshot.clip_point_utf16(range.end, Bias::Left);
// Combine any LSP edits that are adjacent.
//
@@ -5721,11 +5747,11 @@ impl Project {
// In order for the diffing logic below to work properly, any edits that
// cancel each other out must be combined into one.
while let Some((next_range, next_text)) = lsp_edits.peek() {
if next_range.start > range.end {
if next_range.start.row > range.end.row + 1
|| next_range.start.column > 0
if next_range.start.0 > range.end {
if next_range.start.0.row > range.end.row + 1
|| next_range.start.0.column > 0
|| snapshot.clip_point_utf16(
PointUtf16::new(range.end.row, u32::MAX),
Unclipped(PointUtf16::new(range.end.row, u32::MAX)),
Bias::Left,
) > range.end
{
@@ -5733,7 +5759,7 @@ impl Project {
}
new_text.push('\n');
}
range.end = next_range.end;
range.end = snapshot.clip_point_utf16(next_range.end, Bias::Left);
new_text.push_str(next_text);
lsp_edits.next();
}
@@ -5853,48 +5879,6 @@ impl Project {
}
}
impl ProjectStore {
pub fn new() -> Self {
Self {
projects: Default::default(),
}
}
pub fn projects<'a>(
&'a self,
cx: &'a AppContext,
) -> impl 'a + Iterator<Item = ModelHandle<Project>> {
self.projects
.iter()
.filter_map(|project| project.upgrade(cx))
}
fn add_project(&mut self, project: WeakModelHandle<Project>, cx: &mut ModelContext<Self>) {
if let Err(ix) = self
.projects
.binary_search_by_key(&project.id(), WeakModelHandle::id)
{
self.projects.insert(ix, project);
}
cx.notify();
}
fn prune_projects(&mut self, cx: &mut ModelContext<Self>) {
let mut did_change = false;
self.projects.retain(|project| {
if project.is_upgradable(cx) {
true
} else {
did_change = true;
false
}
});
if did_change {
cx.notify();
}
}
}
impl WorktreeHandle {
pub fn upgrade(&self, cx: &AppContext) -> Option<ModelHandle<Worktree>> {
match self {
@@ -5973,16 +5957,10 @@ impl<'a> Iterator for PathMatchCandidateSetIter<'a> {
}
}
impl Entity for ProjectStore {
type Event = ();
}
impl Entity for Project {
type Event = Event;
fn release(&mut self, cx: &mut gpui::MutableAppContext) {
self.project_store.update(cx, ProjectStore::prune_projects);
fn release(&mut self, _: &mut gpui::MutableAppContext) {
match &self.client_state {
Some(ProjectClientState::Local { remote_id, .. }) => {
self.client
@@ -6054,13 +6032,13 @@ fn serialize_symbol(symbol: &Symbol) -> proto::Symbol {
path: symbol.path.path.to_string_lossy().to_string(),
name: symbol.name.clone(),
kind: unsafe { mem::transmute(symbol.kind) },
start: Some(proto::Point {
row: symbol.range.start.row,
column: symbol.range.start.column,
start: Some(proto::PointUtf16 {
row: symbol.range.start.0.row,
column: symbol.range.start.0.column,
}),
end: Some(proto::Point {
row: symbol.range.end.row,
column: symbol.range.end.column,
end: Some(proto::PointUtf16 {
row: symbol.range.end.0.row,
column: symbol.range.end.0.column,
}),
signature: symbol.signature.to_vec(),
}

View File

@@ -1239,7 +1239,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
&buffer,
vec![
DiagnosticEntry {
range: PointUtf16::new(0, 10)..PointUtf16::new(0, 10),
range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 10)),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "syntax error 1".to_string(),
@@ -1247,7 +1247,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
},
},
DiagnosticEntry {
range: PointUtf16::new(1, 10)..PointUtf16::new(1, 10),
range: Unclipped(PointUtf16::new(1, 10))..Unclipped(PointUtf16::new(1, 10)),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "syntax error 2".to_string(),
@@ -2166,7 +2166,11 @@ async fn test_rescan_and_remote_updates(
proto::WorktreeMetadata {
id: initial_snapshot.id().to_proto(),
root_name: initial_snapshot.root_name().into(),
abs_path: initial_snapshot.abs_path().as_os_str().as_bytes().to_vec(),
abs_path: initial_snapshot
.abs_path()
.as_os_str()
.to_string_lossy()
.into(),
visible: true,
},
rpc.clone(),

View File

@@ -20,6 +20,7 @@ use gpui::{
executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
Task,
};
use language::Unclipped;
use language::{
proto::{deserialize_version, serialize_line_ending, serialize_version},
Buffer, DiagnosticEntry, PointUtf16, Rope,
@@ -40,7 +41,6 @@ use std::{
future::Future,
mem,
ops::{Deref, DerefMut},
os::unix::prelude::{OsStrExt, OsStringExt},
path::{Path, PathBuf},
sync::{atomic::AtomicUsize, Arc},
task::Poll,
@@ -65,7 +65,7 @@ pub struct LocalWorktree {
_background_scanner_task: Option<Task<()>>,
poll_task: Option<Task<()>>,
share: Option<ShareState>,
diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<PointUtf16>>>,
diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<Unclipped<PointUtf16>>>>,
diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
client: Arc<Client>,
fs: Arc<dyn Fs>,
@@ -82,6 +82,7 @@ pub struct RemoteWorktree {
replica_id: ReplicaId,
diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
visible: bool,
disconnected: bool,
}
#[derive(Clone)]
@@ -167,7 +168,7 @@ enum ScanState {
struct ShareState {
project_id: u64,
snapshots_tx: watch::Sender<LocalSnapshot>,
_maintain_remote_snapshot: Option<Task<Option<()>>>,
_maintain_remote_snapshot: Task<Option<()>>,
}
pub enum Event {
@@ -221,7 +222,7 @@ impl Worktree {
let root_name = worktree.root_name.clone();
let visible = worktree.visible;
let abs_path = PathBuf::from(OsString::from_vec(worktree.abs_path));
let abs_path = PathBuf::from(worktree.abs_path);
let snapshot = Snapshot {
id: WorktreeId(remote_id as usize),
abs_path: Arc::from(abs_path.deref()),
@@ -247,6 +248,7 @@ impl Worktree {
client: client.clone(),
diagnostic_summaries: Default::default(),
visible,
disconnected: false,
})
});
@@ -499,7 +501,10 @@ impl LocalWorktree {
})
}
pub fn diagnostics_for_path(&self, path: &Path) -> Option<Vec<DiagnosticEntry<PointUtf16>>> {
pub fn diagnostics_for_path(
&self,
path: &Path,
) -> Option<Vec<DiagnosticEntry<Unclipped<PointUtf16>>>> {
self.diagnostics.get(path).cloned()
}
@@ -507,7 +512,7 @@ impl LocalWorktree {
&mut self,
language_server_id: usize,
worktree_path: Arc<Path>,
diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
_: &mut ModelContext<Worktree>,
) -> Result<bool> {
self.diagnostics.remove(&worktree_path);
@@ -656,7 +661,7 @@ impl LocalWorktree {
id: self.id().to_proto(),
root_name: self.root_name().to_string(),
visible: self.visible,
abs_path: self.abs_path().as_os_str().as_bytes().to_vec(),
abs_path: self.abs_path().as_os_str().to_string_lossy().into(),
}
}
@@ -968,11 +973,10 @@ impl LocalWorktree {
let _ = share_tx.send(Ok(()));
} else {
let (snapshots_tx, mut snapshots_rx) = watch::channel_with(self.snapshot());
let rpc = self.client.clone();
let worktree_id = cx.model_id() as u64;
for (path, summary) in self.diagnostic_summaries.iter() {
if let Err(e) = rpc.send(proto::UpdateDiagnosticSummary {
if let Err(e) = self.client.send(proto::UpdateDiagnosticSummary {
project_id,
worktree_id,
summary: Some(summary.to_proto(&path.0)),
@@ -982,15 +986,14 @@ impl LocalWorktree {
}
let maintain_remote_snapshot = cx.background().spawn({
let rpc = rpc;
let rpc = self.client.clone();
async move {
let mut prev_snapshot = match snapshots_rx.recv().await {
Some(snapshot) => {
let update = proto::UpdateWorktree {
project_id,
worktree_id,
abs_path: snapshot.abs_path().as_os_str().as_bytes().to_vec(),
abs_path: snapshot.abs_path().to_string_lossy().into(),
root_name: snapshot.root_name().to_string(),
updated_entries: snapshot
.entries_by_path
@@ -1030,10 +1033,11 @@ impl LocalWorktree {
}
.log_err()
});
self.share = Some(ShareState {
project_id,
snapshots_tx,
_maintain_remote_snapshot: Some(maintain_remote_snapshot),
_maintain_remote_snapshot: maintain_remote_snapshot,
});
}
@@ -1051,25 +1055,6 @@ impl LocalWorktree {
pub fn is_shared(&self) -> bool {
self.share.is_some()
}
pub fn send_extension_counts(&self, project_id: u64) {
let mut extensions = Vec::new();
let mut counts = Vec::new();
for (extension, count) in self.extension_counts() {
extensions.push(extension.to_string_lossy().to_string());
counts.push(*count as u32);
}
self.client
.send(proto::UpdateWorktreeExtensions {
project_id,
worktree_id: self.id().to_proto(),
extensions,
counts,
})
.log_err();
}
}
impl RemoteWorktree {
@@ -1086,6 +1071,7 @@ impl RemoteWorktree {
pub fn disconnected_from_host(&mut self) {
self.updates_tx.take();
self.snapshot_subscriptions.clear();
self.disconnected = true;
}
pub fn update_from_remote(&mut self, update: proto::UpdateWorktree) {
@@ -1100,10 +1086,12 @@ impl RemoteWorktree {
self.scan_id > scan_id || (self.scan_id == scan_id && self.is_complete)
}
fn wait_for_snapshot(&mut self, scan_id: usize) -> impl Future<Output = ()> {
fn wait_for_snapshot(&mut self, scan_id: usize) -> impl Future<Output = Result<()>> {
let (tx, rx) = oneshot::channel();
if self.observed_snapshot(scan_id) {
let _ = tx.send(());
} else if self.disconnected {
drop(tx);
} else {
match self
.snapshot_subscriptions
@@ -1114,7 +1102,8 @@ impl RemoteWorktree {
}
async move {
let _ = rx.await;
rx.await?;
Ok(())
}
}
@@ -1143,7 +1132,7 @@ impl RemoteWorktree {
) -> Task<Result<Entry>> {
let wait_for_snapshot = self.wait_for_snapshot(scan_id);
cx.spawn(|this, mut cx| async move {
wait_for_snapshot.await;
wait_for_snapshot.await?;
this.update(&mut cx, |worktree, _| {
let worktree = worktree.as_remote_mut().unwrap();
let mut snapshot = worktree.background_snapshot.lock();
@@ -1162,7 +1151,7 @@ impl RemoteWorktree {
) -> Task<Result<()>> {
let wait_for_snapshot = self.wait_for_snapshot(scan_id);
cx.spawn(|this, mut cx| async move {
wait_for_snapshot.await;
wait_for_snapshot.await?;
this.update(&mut cx, |worktree, _| {
let worktree = worktree.as_remote_mut().unwrap();
let mut snapshot = worktree.background_snapshot.lock();
@@ -1400,7 +1389,7 @@ impl LocalSnapshot {
proto::UpdateWorktree {
project_id,
worktree_id: self.id().to_proto(),
abs_path: self.abs_path().as_os_str().as_bytes().to_vec(),
abs_path: self.abs_path().to_string_lossy().into(),
root_name,
updated_entries: self.entries_by_path.iter().map(Into::into).collect(),
removed_entries: Default::default(),
@@ -1468,7 +1457,7 @@ impl LocalSnapshot {
proto::UpdateWorktree {
project_id,
worktree_id,
abs_path: self.abs_path().as_os_str().as_bytes().to_vec(),
abs_path: self.abs_path().to_string_lossy().into(),
root_name: self.root_name().to_string(),
updated_entries,
removed_entries,
@@ -2947,7 +2936,7 @@ impl<'a> From<&'a Entry> for proto::Entry {
Self {
id: entry.id.to_proto(),
is_dir: entry.is_dir(),
path: entry.path.as_os_str().as_bytes().to_vec(),
path: entry.path.to_string_lossy().into(),
inode: entry.inode,
mtime: Some(entry.mtime.into()),
is_symlink: entry.is_symlink,
@@ -2965,14 +2954,10 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
EntryKind::Dir
} else {
let mut char_bag = *root_char_bag;
char_bag.extend(
String::from_utf8_lossy(&entry.path)
.chars()
.map(|c| c.to_ascii_lowercase()),
);
char_bag.extend(entry.path.chars().map(|c| c.to_ascii_lowercase()));
EntryKind::File(char_bag)
};
let path: Arc<Path> = PathBuf::from(OsString::from_vec(entry.path)).into();
let path: Arc<Path> = PathBuf::from(entry.path).into();
Ok(Entry {
id: ProjectEntryId::from_proto(entry.id),
kind,

View File

@@ -1393,8 +1393,15 @@ mod tests {
.await;
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
let (_, workspace) =
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
let (_, workspace) = cx.add_window(|cx| {
Workspace::new(
Default::default(),
0,
project.clone(),
|_, _| unimplemented!(),
cx,
)
});
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
assert_eq!(
visible_entries_as_strings(&panel, 0..50, cx),
@@ -1486,8 +1493,15 @@ mod tests {
.await;
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
let (_, workspace) =
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
let (_, workspace) = cx.add_window(|cx| {
Workspace::new(
Default::default(),
0,
project.clone(),
|_, _| unimplemented!(),
cx,
)
});
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
select_path(&panel, "root1", cx);

View File

@@ -12,7 +12,7 @@ smallvec = { version = "1.6", features = ["union"] }
sum_tree = { path = "../sum_tree" }
arrayvec = "0.7.1"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
util = { path = "../util" }
[dev-dependencies]
rand = "0.8.3"

View File

@@ -1,16 +1,23 @@
mod offset_utf16;
mod point;
mod point_utf16;
mod unclipped;
use arrayvec::ArrayString;
use bromberg_sl2::{DigestString, HashMatrix};
use smallvec::SmallVec;
use std::{cmp, fmt, io, mem, ops::Range, str};
use std::{
cmp, fmt, io, mem,
ops::{AddAssign, Range},
str,
};
use sum_tree::{Bias, Dimension, SumTree};
use util::debug_panic;
pub use offset_utf16::OffsetUtf16;
pub use point::Point;
pub use point_utf16::PointUtf16;
pub use unclipped::Unclipped;
#[cfg(test)]
const CHUNK_BASE: usize = 6;
@@ -260,6 +267,14 @@ impl Rope {
}
pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize {
self.point_utf16_to_offset_impl(point, false)
}
pub fn unclipped_point_utf16_to_offset(&self, point: Unclipped<PointUtf16>) -> usize {
self.point_utf16_to_offset_impl(point.0, true)
}
fn point_utf16_to_offset_impl(&self, point: PointUtf16, clip: bool) -> usize {
if point >= self.summary().lines_utf16() {
return self.summary().len;
}
@@ -269,20 +284,20 @@ impl Rope {
cursor.start().1
+ cursor
.item()
.map_or(0, |chunk| chunk.point_utf16_to_offset(overshoot))
.map_or(0, |chunk| chunk.point_utf16_to_offset(overshoot, clip))
}
pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point {
if point >= self.summary().lines_utf16() {
pub fn unclipped_point_utf16_to_point(&self, point: Unclipped<PointUtf16>) -> Point {
if point.0 >= self.summary().lines_utf16() {
return self.summary().lines;
}
let mut cursor = self.chunks.cursor::<(PointUtf16, Point)>();
cursor.seek(&point, Bias::Left, &());
let overshoot = point - cursor.start().0;
cursor.seek(&point.0, Bias::Left, &());
let overshoot = Unclipped(point.0 - cursor.start().0);
cursor.start().1
+ cursor
.item()
.map_or(Point::zero(), |chunk| chunk.point_utf16_to_point(overshoot))
+ cursor.item().map_or(Point::zero(), |chunk| {
chunk.unclipped_point_utf16_to_point(overshoot)
})
}
pub fn clip_offset(&self, mut offset: usize, bias: Bias) -> usize {
@@ -330,11 +345,11 @@ impl Rope {
}
}
pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 {
pub fn clip_point_utf16(&self, point: Unclipped<PointUtf16>, bias: Bias) -> PointUtf16 {
let mut cursor = self.chunks.cursor::<PointUtf16>();
cursor.seek(&point, Bias::Right, &());
cursor.seek(&point.0, Bias::Right, &());
if let Some(chunk) = cursor.item() {
let overshoot = point - cursor.start();
let overshoot = Unclipped(point.0 - cursor.start());
*cursor.start() + chunk.clip_point_utf16(overshoot, bias)
} else {
self.summary().lines_utf16()
@@ -665,28 +680,33 @@ impl Chunk {
fn point_to_offset(&self, target: Point) -> usize {
let mut offset = 0;
let mut point = Point::new(0, 0);
for ch in self.0.chars() {
if point >= target {
if point > target {
panic!("point {:?} is inside of character {:?}", target, ch);
debug_panic!("point {target:?} is inside of character {ch:?}");
}
break;
}
if ch == '\n' {
point.row += 1;
if point.row > target.row {
panic!(
"point {:?} is beyond the end of a line with length {}",
target, point.column
);
}
point.column = 0;
if point.row > target.row {
debug_panic!(
"point {target:?} is beyond the end of a line with length {}",
point.column
);
break;
}
} else {
point.column += ch.len_utf8() as u32;
}
offset += ch.len_utf8();
}
offset
}
@@ -711,45 +731,62 @@ impl Chunk {
point_utf16
}
fn point_utf16_to_offset(&self, target: PointUtf16) -> usize {
fn point_utf16_to_offset(&self, target: PointUtf16, clip: bool) -> usize {
let mut offset = 0;
let mut point = PointUtf16::new(0, 0);
for ch in self.0.chars() {
if point >= target {
if point > target {
panic!("point {:?} is inside of character {:?}", target, ch);
}
if point == target {
break;
}
if ch == '\n' {
point.row += 1;
if point.row > target.row {
panic!(
"point {:?} is beyond the end of a line with length {}",
target, point.column
);
}
point.column = 0;
if point.row > target.row {
if !clip {
debug_panic!(
"point {target:?} is beyond the end of a line with length {}",
point.column
);
}
// Return the offset of the newline
return offset;
}
} else {
point.column += ch.len_utf16() as u32;
}
if point > target {
if !clip {
debug_panic!("point {target:?} is inside of codepoint {ch:?}");
}
// Return the offset of the codepoint which we have landed within, bias left
return offset;
}
offset += ch.len_utf8();
}
offset
}
fn point_utf16_to_point(&self, target: PointUtf16) -> Point {
fn unclipped_point_utf16_to_point(&self, target: Unclipped<PointUtf16>) -> Point {
let mut point = Point::zero();
let mut point_utf16 = PointUtf16::zero();
for ch in self.0.chars() {
if point_utf16 >= target {
if point_utf16 > target {
panic!("point {:?} is inside of character {:?}", target, ch);
}
if point_utf16 == target.0 {
break;
}
if point_utf16 > target.0 {
// If the point is past the end of a line or inside of a code point,
// return the last valid point before the target.
return point;
}
if ch == '\n' {
point_utf16 += PointUtf16::new(1, 0);
point += Point::new(1, 0);
@@ -758,6 +795,7 @@ impl Chunk {
point += Point::new(0, ch.len_utf8() as u32);
}
}
point
}
@@ -777,11 +815,11 @@ impl Chunk {
unreachable!()
}
fn clip_point_utf16(&self, target: PointUtf16, bias: Bias) -> PointUtf16 {
fn clip_point_utf16(&self, target: Unclipped<PointUtf16>, bias: Bias) -> PointUtf16 {
for (row, line) in self.0.split('\n').enumerate() {
if row == target.row as usize {
if row == target.0.row as usize {
let mut code_units = line.encode_utf16();
let mut column = code_units.by_ref().take(target.column as usize).count();
let mut column = code_units.by_ref().take(target.0.column as usize).count();
if char::decode_utf16(code_units).next().transpose().is_err() {
match bias {
Bias::Left => column -= 1,
@@ -917,7 +955,7 @@ impl std::ops::Add<Self> for TextSummary {
type Output = Self;
fn add(mut self, rhs: Self) -> Self::Output {
self.add_assign(&rhs);
AddAssign::add_assign(&mut self, &rhs);
self
}
}
@@ -1114,15 +1152,15 @@ mod tests {
);
assert_eq!(
rope.clip_point_utf16(PointUtf16::new(0, 1), Bias::Left),
rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 1)), Bias::Left),
PointUtf16::new(0, 0)
);
assert_eq!(
rope.clip_point_utf16(PointUtf16::new(0, 1), Bias::Right),
rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 1)), Bias::Right),
PointUtf16::new(0, 2)
);
assert_eq!(
rope.clip_point_utf16(PointUtf16::new(0, 3), Bias::Right),
rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 3)), Bias::Right),
PointUtf16::new(0, 2)
);
@@ -1238,7 +1276,7 @@ mod tests {
}
let mut offset_utf16 = OffsetUtf16(0);
let mut point_utf16 = PointUtf16::zero();
let mut point_utf16 = Unclipped(PointUtf16::zero());
for unit in expected.encode_utf16() {
let left_offset = actual.clip_offset_utf16(offset_utf16, Bias::Left);
let right_offset = actual.clip_offset_utf16(offset_utf16, Bias::Right);
@@ -1250,15 +1288,15 @@ mod tests {
let left_point = actual.clip_point_utf16(point_utf16, Bias::Left);
let right_point = actual.clip_point_utf16(point_utf16, Bias::Right);
assert!(right_point >= left_point);
// Ensure translating UTF-16 points to offsets doesn't panic.
// Ensure translating valid UTF-16 points to offsets doesn't panic.
actual.point_utf16_to_offset(left_point);
actual.point_utf16_to_offset(right_point);
offset_utf16.0 += 1;
if unit == b'\n' as u16 {
point_utf16 += PointUtf16::new(1, 0);
point_utf16.0 += PointUtf16::new(1, 0);
} else {
point_utf16 += PointUtf16::new(0, 1);
point_utf16.0 += PointUtf16::new(0, 1);
}
}

View File

@@ -0,0 +1,57 @@
use crate::{ChunkSummary, TextDimension, TextSummary};
use std::ops::{Add, AddAssign, Sub, SubAssign};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Unclipped<T>(pub T);
impl<T> From<T> for Unclipped<T> {
fn from(value: T) -> Self {
Unclipped(value)
}
}
impl<'a, T: sum_tree::Dimension<'a, ChunkSummary>> sum_tree::Dimension<'a, ChunkSummary>
for Unclipped<T>
{
fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) {
self.0.add_summary(summary, &());
}
}
impl<T: TextDimension> TextDimension for Unclipped<T> {
fn from_text_summary(summary: &TextSummary) -> Self {
Unclipped(T::from_text_summary(summary))
}
fn add_assign(&mut self, other: &Self) {
TextDimension::add_assign(&mut self.0, &other.0);
}
}
impl<T: Add<T, Output = T>> Add<Unclipped<T>> for Unclipped<T> {
type Output = Unclipped<T>;
fn add(self, rhs: Unclipped<T>) -> Self::Output {
Unclipped(self.0 + rhs.0)
}
}
impl<T: Sub<T, Output = T>> Sub<Unclipped<T>> for Unclipped<T> {
type Output = Unclipped<T>;
fn sub(self, rhs: Unclipped<T>) -> Self::Output {
Unclipped(self.0 - rhs.0)
}
}
impl<T: AddAssign<T>> AddAssign<Unclipped<T>> for Unclipped<T> {
fn add_assign(&mut self, rhs: Unclipped<T>) {
self.0 += rhs.0;
}
}
impl<T: SubAssign<T>> SubAssign<Unclipped<T>> for Unclipped<T> {
fn sub_assign(&mut self, rhs: Unclipped<T>) {
self.0 -= rhs.0;
}
}

View File

@@ -48,9 +48,7 @@ message Envelope {
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 40;
UpdateProject update_project = 41;
RegisterProjectActivity register_project_activity = 42;
UpdateWorktree update_worktree = 43;
UpdateWorktreeExtensions update_worktree_extensions = 44;
CreateProjectEntry create_project_entry = 45;
RenameProjectEntry rename_project_entry = 46;
@@ -158,14 +156,12 @@ message JoinRoomResponse {
optional LiveKitConnectionInfo live_kit_connection_info = 2;
}
message LeaveRoom {
uint64 id = 1;
}
message LeaveRoom {}
message Room {
uint64 id = 1;
repeated Participant participants = 2;
repeated uint64 pending_participant_user_ids = 3;
repeated PendingParticipant pending_participants = 3;
string live_kit_room = 4;
}
@@ -176,6 +172,12 @@ message Participant {
ParticipantLocation location = 4;
}
message PendingParticipant {
uint64 user_id = 1;
uint64 calling_user_id = 2;
optional uint64 initial_project_id = 3;
}
message ParticipantProject {
uint64 id = 1;
repeated string worktree_root_names = 2;
@@ -199,13 +201,13 @@ message ParticipantLocation {
message Call {
uint64 room_id = 1;
uint64 recipient_user_id = 2;
uint64 called_user_id = 2;
optional uint64 initial_project_id = 3;
}
message IncomingCall {
uint64 room_id = 1;
uint64 caller_user_id = 2;
uint64 calling_user_id = 2;
repeated uint64 participant_user_ids = 3;
optional ParticipantProject initial_project = 4;
}
@@ -214,7 +216,7 @@ message CallCanceled {}
message CancelCall {
uint64 room_id = 1;
uint64 recipient_user_id = 2;
uint64 called_user_id = 2;
}
message DeclineCall {
@@ -253,10 +255,6 @@ message UpdateProject {
repeated WorktreeMetadata worktrees = 2;
}
message RegisterProjectActivity {
uint64 project_id = 1;
}
message JoinProject {
uint64 project_id = 1;
}
@@ -280,33 +278,26 @@ message UpdateWorktree {
repeated uint64 removed_entries = 5;
uint64 scan_id = 6;
bool is_last_update = 7;
bytes abs_path = 8;
}
message UpdateWorktreeExtensions {
uint64 project_id = 1;
uint64 worktree_id = 2;
repeated string extensions = 3;
repeated uint32 counts = 4;
string abs_path = 8;
}
message CreateProjectEntry {
uint64 project_id = 1;
uint64 worktree_id = 2;
bytes path = 3;
string path = 3;
bool is_directory = 4;
}
message RenameProjectEntry {
uint64 project_id = 1;
uint64 entry_id = 2;
bytes new_path = 3;
string new_path = 3;
}
message CopyProjectEntry {
uint64 project_id = 1;
uint64 entry_id = 2;
bytes new_path = 3;
string new_path = 3;
}
message DeleteProjectEntry {
@@ -412,8 +403,10 @@ message Symbol {
string name = 4;
int32 kind = 5;
string path = 6;
Point start = 7;
Point end = 8;
// Cannot use generate anchors for unopend files,
// so we are forced to use point coords instead
PointUtf16 start = 7;
PointUtf16 end = 8;
bytes signature = 9;
}
@@ -892,7 +885,7 @@ message File {
message Entry {
uint64 id = 1;
bool is_dir = 2;
bytes path = 3;
string path = 3;
uint64 inode = 4;
Timestamp mtime = 5;
bool is_symlink = 6;
@@ -1042,7 +1035,7 @@ message Range {
uint64 end = 2;
}
message Point {
message PointUtf16 {
uint32 row = 1;
uint32 column = 2;
}
@@ -1076,7 +1069,7 @@ message WorktreeMetadata {
uint64 id = 1;
string root_name = 2;
bool visible = 3;
bytes abs_path = 4;
string abs_path = 4;
}
message UpdateDiffBase {

View File

@@ -24,7 +24,7 @@ use std::{
};
use tracing::instrument;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize)]
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize)]
pub struct ConnectionId(pub u32);
impl fmt::Display for ConnectionId {

View File

@@ -140,12 +140,11 @@ messages!(
(OpenBufferResponse, Background),
(PerformRename, Background),
(PerformRenameResponse, Background),
(Ping, Foreground),
(PrepareRename, Background),
(PrepareRenameResponse, Background),
(ProjectEntryResponse, Foreground),
(RemoveContact, Foreground),
(Ping, Foreground),
(RegisterProjectActivity, Foreground),
(ReloadBuffers, Foreground),
(ReloadBuffersResponse, Foreground),
(RemoveProjectCollaborator, Foreground),
@@ -175,7 +174,6 @@ messages!(
(UpdateParticipantLocation, Foreground),
(UpdateProject, Foreground),
(UpdateWorktree, Foreground),
(UpdateWorktreeExtensions, Background),
(UpdateDiffBase, Background),
(GetPrivateUserInfo, Foreground),
(GetPrivateUserInfoResponse, Foreground),
@@ -231,6 +229,7 @@ request_messages!(
(Test, Test),
(UpdateBuffer, Ack),
(UpdateParticipantLocation, Ack),
(UpdateProject, Ack),
(UpdateWorktree, Ack),
);
@@ -262,7 +261,6 @@ entity_messages!(
OpenBufferForSymbol,
PerformRename,
PrepareRename,
RegisterProjectActivity,
ReloadBuffers,
RemoveProjectCollaborator,
RenameProjectEntry,
@@ -278,7 +276,6 @@ entity_messages!(
UpdateLanguageServer,
UpdateProject,
UpdateWorktree,
UpdateWorktreeExtensions,
UpdateDiffBase
);

View File

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

View File

@@ -14,8 +14,9 @@ use serde::Deserialize;
use settings::Settings;
use std::{any::Any, sync::Arc};
use workspace::{
item::ItemHandle,
searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
ItemHandle, Pane, ToolbarItemLocation, ToolbarItemView,
Pane, ToolbarItemLocation, ToolbarItemView,
};
#[derive(Clone, Deserialize, PartialEq)]

View File

@@ -24,9 +24,9 @@ use std::{
};
use util::ResultExt as _;
use workspace::{
item::{Item, ItemEvent, ItemHandle},
searchable::{Direction, SearchableItem, SearchableItemHandle},
Item, ItemEvent, ItemHandle, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView,
Workspace,
ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
};
actions!(project_search, [SearchInNew, ToggleFocus]);
@@ -315,7 +315,7 @@ impl Item for ProjectSearchView {
.update(cx, |editor, cx| editor.reload(project, cx))
}
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
fn clone_on_split(&self, _workspace_id: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self>
where
Self: Sized,
{
@@ -353,6 +353,20 @@ impl Item for ProjectSearchView {
fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
self.results_editor.breadcrumbs(theme, cx)
}
fn serialized_item_kind() -> Option<&'static str> {
None
}
fn deserialize(
_project: ModelHandle<Project>,
_workspace: WeakViewHandle<Workspace>,
_workspace_id: workspace::WorkspaceId,
_item_id: workspace::ItemId,
_cx: &mut ViewContext<Pane>,
) -> Task<anyhow::Result<ViewHandle<Self>>> {
unimplemented!()
}
}
impl ProjectSearchView {
@@ -893,7 +907,7 @@ impl View for ProjectSearchBar {
impl ToolbarItemView for ProjectSearchBar {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn workspace::ItemHandle>,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) -> ToolbarItemLocation {
cx.notify();

View File

@@ -14,6 +14,7 @@ test-support = []
assets = { path = "../assets" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }
sqlez = { path = "../sqlez" }
fs = { path = "../fs" }
anyhow = "1.0.38"
futures = "0.3"

View File

@@ -2,7 +2,7 @@ mod keymap_file;
pub mod settings_file;
pub mod watched_json;
use anyhow::Result;
use anyhow::{bail, Result};
use gpui::{
font_cache::{FamilyId, FontCache},
AssetSource,
@@ -14,6 +14,10 @@ use schemars::{
};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value;
use sqlez::{
bindable::{Bind, Column},
statement::Statement,
};
use std::{collections::HashMap, fmt::Write as _, num::NonZeroU32, str, sync::Arc};
use theme::{Theme, ThemeRegistry};
use tree_sitter::Query;
@@ -55,24 +59,6 @@ pub struct FeatureFlags {
pub experimental_themes: bool,
}
#[derive(Copy, Clone, PartialEq, Eq, Default)]
pub enum ReleaseChannel {
#[default]
Dev,
Preview,
Stable,
}
impl ReleaseChannel {
pub fn name(&self) -> &'static str {
match self {
ReleaseChannel::Dev => "Zed Dev",
ReleaseChannel::Preview => "Zed Preview",
ReleaseChannel::Stable => "Zed",
}
}
}
impl FeatureFlags {
pub fn keymap_files(&self) -> Vec<&'static str> {
vec![]
@@ -244,6 +230,33 @@ pub enum DockAnchor {
Expanded,
}
impl Bind for DockAnchor {
fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
match self {
DockAnchor::Bottom => "Bottom",
DockAnchor::Right => "Right",
DockAnchor::Expanded => "Expanded",
}
.bind(statement, start_index)
}
}
impl Column for DockAnchor {
fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
String::column(statement, start_index).and_then(|(anchor_text, next_index)| {
Ok((
match anchor_text.as_ref() {
"Bottom" => DockAnchor::Bottom,
"Right" => DockAnchor::Right,
"Expanded" => DockAnchor::Expanded,
_ => bail!("Stored dock anchor is incorrect"),
},
next_index,
))
})
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct SettingsFileContent {
pub experiments: Option<FeatureFlags>,

2
crates/sqlez/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
debug/
target/

150
crates/sqlez/Cargo.lock generated Normal file
View File

@@ -0,0 +1,150 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "anyhow"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
dependencies = [
"backtrace",
]
[[package]]
name = "backtrace"
version = "0.3.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]]
name = "cc"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "gimli"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
[[package]]
name = "indoc"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3"
[[package]]
name = "libc"
version = "0.2.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
[[package]]
name = "libsqlite3-sys"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "memchr"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "miniz_oxide"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34"
dependencies = [
"adler",
]
[[package]]
name = "object"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]]
name = "pkg-config"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]]
name = "rustc-demangle"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
[[package]]
name = "sqlez"
version = "0.1.0"
dependencies = [
"anyhow",
"indoc",
"libsqlite3-sys",
"thread_local",
]
[[package]]
name = "thread_local"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
dependencies = [
"once_cell",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"

16
crates/sqlez/Cargo.toml Normal file
View File

@@ -0,0 +1,16 @@
[package]
name = "sqlez"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = { version = "1.0.38", features = ["backtrace"] }
indoc = "1.0.7"
libsqlite3-sys = { version = "0.24", features = ["bundled"] }
smol = "1.2"
thread_local = "1.1.4"
lazy_static = "1.4"
parking_lot = "0.11.1"
futures = "0.3"

View File

@@ -0,0 +1,352 @@
use std::{
ffi::OsStr,
os::unix::prelude::OsStrExt,
path::{Path, PathBuf},
sync::Arc,
};
use anyhow::{Context, Result};
use crate::statement::{SqlType, Statement};
pub trait Bind {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32>;
}
pub trait Column: Sized {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)>;
}
impl Bind for bool {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement
.bind(self.then_some(1).unwrap_or(0), start_index)
.with_context(|| format!("Failed to bind bool at index {start_index}"))
}
}
impl Column for bool {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
i32::column(statement, start_index)
.map(|(i, next_index)| (i != 0, next_index))
.with_context(|| format!("Failed to read bool at index {start_index}"))
}
}
impl Bind for &[u8] {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement
.bind_blob(start_index, self)
.with_context(|| format!("Failed to bind &[u8] at index {start_index}"))?;
Ok(start_index + 1)
}
}
impl<const C: usize> Bind for &[u8; C] {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement
.bind_blob(start_index, self.as_slice())
.with_context(|| format!("Failed to bind &[u8; C] at index {start_index}"))?;
Ok(start_index + 1)
}
}
impl Bind for Vec<u8> {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement
.bind_blob(start_index, self)
.with_context(|| format!("Failed to bind Vec<u8> at index {start_index}"))?;
Ok(start_index + 1)
}
}
impl Column for Vec<u8> {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let result = statement
.column_blob(start_index)
.with_context(|| format!("Failed to read Vec<u8> at index {start_index}"))?;
Ok((Vec::from(result), start_index + 1))
}
}
impl Bind for f64 {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement
.bind_double(start_index, *self)
.with_context(|| format!("Failed to bind f64 at index {start_index}"))?;
Ok(start_index + 1)
}
}
impl Column for f64 {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let result = statement
.column_double(start_index)
.with_context(|| format!("Failed to parse f64 at index {start_index}"))?;
Ok((result, start_index + 1))
}
}
impl Bind for i32 {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement
.bind_int(start_index, *self)
.with_context(|| format!("Failed to bind i32 at index {start_index}"))?;
Ok(start_index + 1)
}
}
impl Column for i32 {
fn column<'a>(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let result = statement.column_int(start_index)?;
Ok((result, start_index + 1))
}
}
impl Bind for i64 {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement
.bind_int64(start_index, *self)
.with_context(|| format!("Failed to bind i64 at index {start_index}"))?;
Ok(start_index + 1)
}
}
impl Column for i64 {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let result = statement.column_int64(start_index)?;
Ok((result, start_index + 1))
}
}
impl Bind for usize {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
(*self as i64)
.bind(statement, start_index)
.with_context(|| format!("Failed to bind usize at index {start_index}"))
}
}
impl Column for usize {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let result = statement.column_int64(start_index)?;
Ok((result as usize, start_index + 1))
}
}
impl Bind for &str {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement.bind_text(start_index, self)?;
Ok(start_index + 1)
}
}
impl Bind for Arc<str> {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement.bind_text(start_index, self.as_ref())?;
Ok(start_index + 1)
}
}
impl Bind for String {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement.bind_text(start_index, self)?;
Ok(start_index + 1)
}
}
impl Column for Arc<str> {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let result = statement.column_text(start_index)?;
Ok((Arc::from(result), start_index + 1))
}
}
impl Column for String {
fn column<'a>(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let result = statement.column_text(start_index)?;
Ok((result.to_owned(), start_index + 1))
}
}
impl<T: Bind> Bind for Option<T> {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
if let Some(this) = self {
this.bind(statement, start_index)
} else {
statement.bind_null(start_index)?;
Ok(start_index + 1)
}
}
}
impl<T: Column> Column for Option<T> {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
if let SqlType::Null = statement.column_type(start_index)? {
Ok((None, start_index + 1))
} else {
T::column(statement, start_index).map(|(result, next_index)| (Some(result), next_index))
}
}
}
impl<T: Bind, const COUNT: usize> Bind for [T; COUNT] {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
let mut current_index = start_index;
for binding in self {
current_index = binding.bind(statement, current_index)?
}
Ok(current_index)
}
}
impl<T: Column + Default + Copy, const COUNT: usize> Column for [T; COUNT] {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let mut array = [Default::default(); COUNT];
let mut current_index = start_index;
for i in 0..COUNT {
(array[i], current_index) = T::column(statement, current_index)?;
}
Ok((array, current_index))
}
}
impl<T: Bind> Bind for Vec<T> {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
let mut current_index = start_index;
for binding in self.iter() {
current_index = binding.bind(statement, current_index)?
}
Ok(current_index)
}
}
impl<T: Bind> Bind for &[T] {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
let mut current_index = start_index;
for binding in *self {
current_index = binding.bind(statement, current_index)?
}
Ok(current_index)
}
}
impl Bind for &Path {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
self.as_os_str().as_bytes().bind(statement, start_index)
}
}
impl Bind for Arc<Path> {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
self.as_ref().bind(statement, start_index)
}
}
impl Bind for PathBuf {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
(self.as_ref() as &Path).bind(statement, start_index)
}
}
impl Column for PathBuf {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let blob = statement.column_blob(start_index)?;
Ok((
PathBuf::from(OsStr::from_bytes(blob).to_owned()),
start_index + 1,
))
}
}
/// Unit impls do nothing. This simplifies query macros
impl Bind for () {
fn bind(&self, _statement: &Statement, start_index: i32) -> Result<i32> {
Ok(start_index)
}
}
impl Column for () {
fn column(_statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
Ok(((), start_index))
}
}
impl<T1: Bind, T2: Bind> Bind for (T1, T2) {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
let next_index = self.0.bind(statement, start_index)?;
self.1.bind(statement, next_index)
}
}
impl<T1: Column, T2: Column> Column for (T1, T2) {
fn column<'a>(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let (first, next_index) = T1::column(statement, start_index)?;
let (second, next_index) = T2::column(statement, next_index)?;
Ok(((first, second), next_index))
}
}
impl<T1: Bind, T2: Bind, T3: Bind> Bind for (T1, T2, T3) {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
let next_index = self.0.bind(statement, start_index)?;
let next_index = self.1.bind(statement, next_index)?;
self.2.bind(statement, next_index)
}
}
impl<T1: Column, T2: Column, T3: Column> Column for (T1, T2, T3) {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let (first, next_index) = T1::column(statement, start_index)?;
let (second, next_index) = T2::column(statement, next_index)?;
let (third, next_index) = T3::column(statement, next_index)?;
Ok(((first, second, third), next_index))
}
}
impl<T1: Bind, T2: Bind, T3: Bind, T4: Bind> Bind for (T1, T2, T3, T4) {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
let next_index = self.0.bind(statement, start_index)?;
let next_index = self.1.bind(statement, next_index)?;
let next_index = self.2.bind(statement, next_index)?;
self.3.bind(statement, next_index)
}
}
impl<T1: Column, T2: Column, T3: Column, T4: Column> Column for (T1, T2, T3, T4) {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let (first, next_index) = T1::column(statement, start_index)?;
let (second, next_index) = T2::column(statement, next_index)?;
let (third, next_index) = T3::column(statement, next_index)?;
let (fourth, next_index) = T4::column(statement, next_index)?;
Ok(((first, second, third, fourth), next_index))
}
}
impl<T1: Bind, T2: Bind, T3: Bind, T4: Bind, T5: Bind> Bind for (T1, T2, T3, T4, T5) {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
let next_index = self.0.bind(statement, start_index)?;
let next_index = self.1.bind(statement, next_index)?;
let next_index = self.2.bind(statement, next_index)?;
let next_index = self.3.bind(statement, next_index)?;
self.4.bind(statement, next_index)
}
}
impl<T1: Column, T2: Column, T3: Column, T4: Column, T5: Column> Column for (T1, T2, T3, T4, T5) {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let (first, next_index) = T1::column(statement, start_index)?;
let (second, next_index) = T2::column(statement, next_index)?;
let (third, next_index) = T3::column(statement, next_index)?;
let (fourth, next_index) = T4::column(statement, next_index)?;
let (fifth, next_index) = T5::column(statement, next_index)?;
Ok(((first, second, third, fourth, fifth), next_index))
}
}

View File

@@ -0,0 +1,334 @@
use std::{
cell::RefCell,
ffi::{CStr, CString},
marker::PhantomData,
path::Path,
ptr,
};
use anyhow::{anyhow, Result};
use libsqlite3_sys::*;
pub struct Connection {
pub(crate) sqlite3: *mut sqlite3,
persistent: bool,
pub(crate) write: RefCell<bool>,
_sqlite: PhantomData<sqlite3>,
}
unsafe impl Send for Connection {}
impl Connection {
pub(crate) fn open(uri: &str, persistent: bool) -> Result<Self> {
let mut connection = Self {
sqlite3: 0 as *mut _,
persistent,
write: RefCell::new(true),
_sqlite: PhantomData,
};
let flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX | SQLITE_OPEN_READWRITE;
unsafe {
sqlite3_open_v2(
CString::new(uri)?.as_ptr(),
&mut connection.sqlite3,
flags,
0 as *const _,
);
// Turn on extended error codes
sqlite3_extended_result_codes(connection.sqlite3, 1);
connection.last_error()?;
}
Ok(connection)
}
/// Attempts to open the database at uri. If it fails, a shared memory db will be opened
/// instead.
pub fn open_file(uri: &str) -> Self {
Self::open(uri, true).unwrap_or_else(|_| Self::open_memory(Some(uri)))
}
pub fn open_memory(uri: Option<&str>) -> Self {
let in_memory_path = if let Some(uri) = uri {
format!("file:{}?mode=memory&cache=shared", uri)
} else {
":memory:".to_string()
};
Self::open(&in_memory_path, false).expect("Could not create fallback in memory db")
}
pub fn persistent(&self) -> bool {
self.persistent
}
pub fn can_write(&self) -> bool {
*self.write.borrow()
}
pub fn backup_main(&self, destination: &Connection) -> Result<()> {
unsafe {
let backup = sqlite3_backup_init(
destination.sqlite3,
CString::new("main")?.as_ptr(),
self.sqlite3,
CString::new("main")?.as_ptr(),
);
sqlite3_backup_step(backup, -1);
sqlite3_backup_finish(backup);
destination.last_error()
}
}
pub fn backup_main_to(&self, destination: impl AsRef<Path>) -> Result<()> {
let destination = Self::open_file(destination.as_ref().to_string_lossy().as_ref());
self.backup_main(&destination)
}
pub fn sql_has_syntax_error(&self, sql: &str) -> Option<(String, usize)> {
let sql = CString::new(sql).unwrap();
let mut remaining_sql = sql.as_c_str();
let sql_start = remaining_sql.as_ptr();
unsafe {
while {
let remaining_sql_str = remaining_sql.to_str().unwrap().trim();
remaining_sql_str != ";" && !remaining_sql_str.is_empty()
} {
let mut raw_statement = 0 as *mut sqlite3_stmt;
let mut remaining_sql_ptr = ptr::null();
sqlite3_prepare_v2(
self.sqlite3,
remaining_sql.as_ptr(),
-1,
&mut raw_statement,
&mut remaining_sql_ptr,
);
let res = sqlite3_errcode(self.sqlite3);
let offset = sqlite3_error_offset(self.sqlite3);
let message = sqlite3_errmsg(self.sqlite3);
sqlite3_finalize(raw_statement);
if res == 1 && offset >= 0 {
let err_msg =
String::from_utf8_lossy(CStr::from_ptr(message as *const _).to_bytes())
.into_owned();
let sub_statement_correction =
remaining_sql.as_ptr() as usize - sql_start as usize;
return Some((err_msg, offset as usize + sub_statement_correction));
}
remaining_sql = CStr::from_ptr(remaining_sql_ptr);
}
}
None
}
pub(crate) fn last_error(&self) -> Result<()> {
unsafe {
let code = sqlite3_errcode(self.sqlite3);
const NON_ERROR_CODES: &[i32] = &[SQLITE_OK, SQLITE_ROW];
if NON_ERROR_CODES.contains(&code) {
return Ok(());
}
let message = sqlite3_errmsg(self.sqlite3);
let message = if message.is_null() {
None
} else {
Some(
String::from_utf8_lossy(CStr::from_ptr(message as *const _).to_bytes())
.into_owned(),
)
};
Err(anyhow!(
"Sqlite call failed with code {} and message: {:?}",
code as isize,
message
))
}
}
pub(crate) fn with_write<T>(&self, callback: impl FnOnce(&Connection) -> T) -> T {
*self.write.borrow_mut() = true;
let result = callback(self);
*self.write.borrow_mut() = false;
result
}
}
impl Drop for Connection {
fn drop(&mut self) {
unsafe { sqlite3_close(self.sqlite3) };
}
}
#[cfg(test)]
mod test {
use anyhow::Result;
use indoc::indoc;
use crate::connection::Connection;
#[test]
fn string_round_trips() -> Result<()> {
let connection = Connection::open_memory(Some("string_round_trips"));
connection
.exec(indoc! {"
CREATE TABLE text (
text TEXT
);"})
.unwrap()()
.unwrap();
let text = "Some test text";
connection
.exec_bound("INSERT INTO text (text) VALUES (?);")
.unwrap()(text)
.unwrap();
assert_eq!(
connection.select_row("SELECT text FROM text;").unwrap()().unwrap(),
Some(text.to_string())
);
Ok(())
}
#[test]
fn tuple_round_trips() {
let connection = Connection::open_memory(Some("tuple_round_trips"));
connection
.exec(indoc! {"
CREATE TABLE test (
text TEXT,
integer INTEGER,
blob BLOB
);"})
.unwrap()()
.unwrap();
let tuple1 = ("test".to_string(), 64, vec![0, 1, 2, 4, 8, 16, 32, 64]);
let tuple2 = ("test2".to_string(), 32, vec![64, 32, 16, 8, 4, 2, 1, 0]);
let mut insert = connection
.exec_bound::<(String, usize, Vec<u8>)>(
"INSERT INTO test (text, integer, blob) VALUES (?, ?, ?)",
)
.unwrap();
insert(tuple1.clone()).unwrap();
insert(tuple2.clone()).unwrap();
assert_eq!(
connection
.select::<(String, usize, Vec<u8>)>("SELECT * FROM test")
.unwrap()()
.unwrap(),
vec![tuple1, tuple2]
);
}
#[test]
fn bool_round_trips() {
let connection = Connection::open_memory(Some("bool_round_trips"));
connection
.exec(indoc! {"
CREATE TABLE bools (
t INTEGER,
f INTEGER
);"})
.unwrap()()
.unwrap();
connection
.exec_bound("INSERT INTO bools(t, f) VALUES (?, ?)")
.unwrap()((true, false))
.unwrap();
assert_eq!(
connection
.select_row::<(bool, bool)>("SELECT * FROM bools;")
.unwrap()()
.unwrap(),
Some((true, false))
);
}
#[test]
fn backup_works() {
let connection1 = Connection::open_memory(Some("backup_works"));
connection1
.exec(indoc! {"
CREATE TABLE blobs (
data BLOB
);"})
.unwrap()()
.unwrap();
let blob = vec![0, 1, 2, 4, 8, 16, 32, 64];
connection1
.exec_bound::<Vec<u8>>("INSERT INTO blobs (data) VALUES (?);")
.unwrap()(blob.clone())
.unwrap();
// Backup connection1 to connection2
let connection2 = Connection::open_memory(Some("backup_works_other"));
connection1.backup_main(&connection2).unwrap();
// Delete the added blob and verify its deleted on the other side
let read_blobs = connection1
.select::<Vec<u8>>("SELECT * FROM blobs;")
.unwrap()()
.unwrap();
assert_eq!(read_blobs, vec![blob]);
}
#[test]
fn multi_step_statement_works() {
let connection = Connection::open_memory(Some("multi_step_statement_works"));
connection
.exec(indoc! {"
CREATE TABLE test (
col INTEGER
)"})
.unwrap()()
.unwrap();
connection
.exec(indoc! {"
INSERT INTO test(col) VALUES (2)"})
.unwrap()()
.unwrap();
assert_eq!(
connection
.select_row::<usize>("SELECT * FROM test")
.unwrap()()
.unwrap(),
Some(2)
);
}
#[test]
fn test_sql_has_syntax_errors() {
let connection = Connection::open_memory(Some("test_sql_has_syntax_errors"));
let first_stmt =
"CREATE TABLE kv_store(key TEXT PRIMARY KEY, value TEXT NOT NULL) STRICT ;";
let second_stmt = "SELECT FROM";
let second_offset = connection.sql_has_syntax_error(second_stmt).unwrap().1;
let res = connection
.sql_has_syntax_error(&format!("{}\n{}", first_stmt, second_stmt))
.map(|(_, offset)| offset);
assert_eq!(res, Some(first_stmt.len() + second_offset + 1));
}
}

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