Compare commits

...

1007 Commits

Author SHA1 Message Date
Mikayla
e3da2e691c continue trampoline 2023-10-24 06:08:37 -07:00
Marshall Bowers
317acbf1d7 WIP: Trampoline 2023-10-24 14:55:23 +02:00
Nathan Sobo
b52db5c5e6 WIP 2023-10-24 10:51:54 +02:00
Nathan Sobo
30280ab897 WIP 2023-10-23 23:34:33 +02:00
Antonio Scandurra
4d621f355d WIP 2023-10-23 17:41:22 +02:00
Antonio Scandurra
05cbceec24 WIP 2023-10-23 17:36:49 +02:00
Nathan Sobo
192b3512fd Merge branch 'gpui2-drag-drop' into zed2 2023-10-23 17:18:07 +02:00
Nate Butler
3a326bfa7e 🤦 Remove references to system_color 2023-10-23 11:05:17 -04:00
Nate Butler
dd55ccef34 Merge branch 'n/d' into zed2 2023-10-23 11:01:36 -04:00
Nate Butler
1e13e273d2 Add ThemeColor interface for UI coloring and remove redundancy 2023-10-23 11:01:04 -04:00
Nate Butler
438cf529bb Remove duplicate ThemeColor defs 2023-10-23 11:00:45 -04:00
Nathan Sobo
ec0b2e5430 Add on_drop listeners 2023-10-23 16:59:16 +02:00
Antonio Scandurra
21d4546a86 WIP 2023-10-23 16:39:30 +02:00
Antonio Scandurra
7832120a4c WIP 2023-10-23 16:38:34 +02:00
Antonio Scandurra
efbf0c828d WIP 2023-10-23 16:38:34 +02:00
Nate Butler
c9d214e8ef Start crate doc 2023-10-23 10:21:30 -04:00
Nathan Sobo
38a7b39070 Don't start dragging until you move to 2px 2023-10-23 16:20:01 +02:00
Nathan Sobo
239b0c2f71 Clear active state when drag starts 2023-10-23 16:10:04 +02:00
Nate Butler
cc445f7cef Start scaffolding out the Copilot Modal UI
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-10-23 10:00:02 -04:00
Nathan Sobo
258fcaea94 Position drag handle relative to cursor 2023-10-23 15:20:33 +02:00
Nathan Sobo
fc927f7406 Checkpoint 2023-10-23 15:14:10 +02:00
Nathan Sobo
d1adce5890 Show red box when dragging 2023-10-23 15:09:22 +02:00
Antonio Scandurra
4e6fb9034d WIP 2023-10-23 14:59:57 +02:00
Nathan Sobo
96f2c4a9de Checkpoint 2023-10-23 14:15:12 +02:00
Antonio Scandurra
a72434f67b WIP 2023-10-23 12:16:33 +02:00
Antonio Scandurra
c0e8ae5dfa WIP 2023-10-23 11:53:24 +02:00
Antonio Scandurra
0de4a93ec7 WIP 2023-10-23 11:43:08 +02:00
Nathan Sobo
da8919002f Fix runtime errors 2023-10-23 11:34:35 +02:00
Antonio Scandurra
5247f217fd Checkpoint 2023-10-23 11:06:58 +02:00
Antonio Scandurra
56462ef793 Checkpoint 2023-10-23 10:59:29 +02:00
Antonio Scandurra
a0b667a2ca WIP 2023-10-22 19:56:25 +02:00
Antonio Scandurra
e7c04d4aca Checkpoint 2023-10-22 18:56:11 +02:00
Antonio Scandurra
72435af170 Checkpoint 2023-10-22 18:34:45 +02:00
Antonio Scandurra
ce75be91e1 Checkpoint 2023-10-22 18:25:24 +02:00
Antonio Scandurra
db6a3e1783 WIP 2023-10-22 18:01:00 +02:00
Antonio Scandurra
50bbdd5cab WIP 2023-10-22 16:53:59 +02:00
Antonio Scandurra
5d10dc7e58 WIP 2023-10-22 16:49:14 +02:00
Antonio Scandurra
7171818d24 WIP 2023-10-22 16:36:29 +02:00
Antonio Scandurra
48033463c8 WIP 2023-10-22 16:33:59 +02:00
Antonio Scandurra
6ffeb048b3 WIP 2023-10-22 16:27:23 +02:00
Antonio Scandurra
5423012368 WIP 2023-10-22 13:15:29 +02:00
Antonio Scandurra
f4135e6bcf WIP 2023-10-22 12:21:28 +02:00
Nathan Sobo
909fbb9538 Checkpoint 2023-10-21 19:07:59 +02:00
Nathan Sobo
89f4718ea1 Checkpoint 2023-10-21 18:48:30 +02:00
Antonio Scandurra
2e2825ae98 WIP 2023-10-21 18:41:09 +02:00
Antonio Scandurra
3740c9d852 WIP 2023-10-21 18:33:08 +02:00
Antonio Scandurra
7bb99c9b9c WIP 2023-10-21 18:30:44 +02:00
Antonio Scandurra
aa3fb28f81 WIP 2023-10-21 18:21:14 +02:00
Antonio Scandurra
b7d30fca2b WIP 2023-10-21 17:52:47 +02:00
Antonio Scandurra
e4fe9538d7 Checkpoint 2023-10-21 16:01:47 +02:00
Antonio Scandurra
f3979a9f28 Checkpoint 2023-10-21 15:59:52 +02:00
Antonio Scandurra
825c352b6a Checkpoint 2023-10-20 17:58:37 +02:00
Marshall Bowers
b0b7f27f3a Merge branch 'gpui2' of github.com:zed-industries/zed into gpui2 2023-10-20 11:32:11 -04:00
Marshall Bowers
c831c5749a Remove more unneeded Clone bounds 2023-10-20 11:32:10 -04:00
Antonio Scandurra
1409fc0da3 Checkpoint 2023-10-20 17:31:47 +02:00
Marshall Bowers
901af8de3f Remove Clone bound from Avatar 2023-10-20 11:13:13 -04:00
Marshall Bowers
d9a030157e Remove Clone bound from Keybinding 2023-10-20 11:12:37 -04:00
Marshall Bowers
a0996c1807 Remove Clone bound from ChatMessage 2023-10-20 11:10:13 -04:00
Antonio Scandurra
a3dcaf21cb Checkpoint 2023-10-20 16:31:03 +02:00
Antonio Scandurra
8ad7ebf02f Checkpoint 2023-10-20 16:31:00 +02:00
Antonio Scandurra
47aa387b91 Checkpoint 2023-10-20 15:13:53 +02:00
Antonio Scandurra
6150df71b2 Checkpoint 2023-10-20 15:08:54 +02:00
Antonio Scandurra
fd94f2a5b5 Checkpoint 2023-10-20 12:59:02 +02:00
Antonio Scandurra
847a1cb068 Checkpoint 2023-10-20 12:23:22 +02:00
Antonio Scandurra
c1f7c9bb87 Checkpoint 2023-10-20 12:12:06 +02:00
Antonio Scandurra
ac181183cc Checkpoint 2023-10-20 11:46:29 +02:00
Antonio Scandurra
8a11053f1f Checkpoint 2023-10-20 11:44:19 +02:00
Antonio Scandurra
b0acaed02f Checkpoint 2023-10-20 11:08:24 +02:00
Antonio Scandurra
c3a917f8b3 Checkpoint 2023-10-20 11:02:00 +02:00
Antonio Scandurra
766ee836b5 Checkpoint 2023-10-20 11:00:52 +02:00
Antonio Scandurra
68bc22f9cd Checkpoint 2023-10-20 10:55:06 +02:00
Nate Butler
0609628645 List actions for ListDetailsEntries 2023-10-19 20:17:41 -04:00
Nate Butler
32028fbbb1 Checkpoint – Notifications Panel 2023-10-19 20:04:21 -04:00
Antonio Scandurra
e3d948f60b Checkpoint 2023-10-19 23:56:43 +02:00
Antonio Scandurra
40c6f738b4 Checkpoint 2023-10-19 23:54:17 +02:00
Antonio Scandurra
296fc92721 Checkpoint 2023-10-19 23:47:32 +02:00
Antonio Scandurra
21b4ae3fdc Merge 2023-10-19 23:40:38 +02:00
Antonio Scandurra
d69105bb77 Checkpoint 2023-10-19 23:39:22 +02:00
Antonio Scandurra
4a6c8ff809 Checkpoint 2023-10-19 23:35:09 +02:00
Antonio Scandurra
1f6d9369d6 Checkpoint 2023-10-19 23:30:14 +02:00
Antonio Scandurra
3a70f02cbf Checkpoint 2023-10-19 23:21:26 +02:00
Antonio Scandurra
dd7e1c505c Checkpoint 2023-10-19 23:00:19 +02:00
Marshall Bowers
94f0140f62 Assign each IconButton an ID based on the icon 2023-10-19 16:50:33 -04:00
Marshall Bowers
28b29d0985 Give each Tab its own ID 2023-10-19 16:42:21 -04:00
Marshall Bowers
52f2521f6a Wire up active style for Tab 2023-10-19 16:37:54 -04:00
Marshall Bowers
e1e8b63eb5 Remove Clone bound from IconElement 2023-10-19 16:37:15 -04:00
Marshall Bowers
a1aba32209 Remove Clone bound from Button and Details 2023-10-19 16:35:44 -04:00
Marshall Bowers
fa3916d1bf Remove Clone bound for HighlightedLabel 2023-10-19 16:34:08 -04:00
Marshall Bowers
3ac7ef90ef Remove Clone bound for Input 2023-10-19 16:32:48 -04:00
Marshall Bowers
4050bf43c4 Remove Clone bound for Label 2023-10-19 16:31:24 -04:00
Antonio Scandurra
0fbf84e6bc Checkpoint 2023-10-19 22:28:42 +02:00
Marshall Bowers
d91b423a45 Remove unused Interactive impl for FakeSettings 2023-10-19 16:16:09 -04:00
Marshall Bowers
2bbce2f0fd Merge branch 'gpui2' of github.com:zed-industries/zed into gpui2 2023-10-19 16:14:39 -04:00
Marshall Bowers
1be1bffb29 Wire up active style for Breadcrumb 2023-10-19 16:13:25 -04:00
Marshall Bowers
92542e6b94 Identify IconButton 2023-10-19 16:12:21 -04:00
Marshall Bowers
3932c1064e Merge branch 'gpui2' into gpui2-theme-to-color 2023-10-19 16:10:44 -04:00
Antonio Scandurra
d446b91117 Checkpoint 2023-10-19 22:10:39 +02:00
Antonio Scandurra
673257bbbc Checkpoint 2023-10-19 22:05:01 +02:00
Antonio Scandurra
180ed7da81 Checkpoint 2023-10-19 22:05:01 +02:00
Antonio Scandurra
38d8ab2285 Checkpoint 2023-10-19 22:05:01 +02:00
Antonio Scandurra
c17a4d8453 Checkpoint 2023-10-19 22:05:01 +02:00
Antonio Scandurra
e74285f6d2 Checkpoint 2023-10-19 22:05:01 +02:00
Marshall Bowers
2189983323 Add missing Clone bounds 2023-10-19 15:02:00 -04:00
Nate Butler
58650b7d2d Checkpoint - Still Broken 2023-10-19 14:38:01 -04:00
Marshall Bowers
d5fc831321 Restore more active styles 2023-10-19 14:23:45 -04:00
Marshall Bowers
743949753a Fix mutual-exclusivity of right panels 2023-10-19 14:19:10 -04:00
Marshall Bowers
184f5f2397 Restore active styles for Inputs 2023-10-19 14:17:35 -04:00
Marshall Bowers
597aa0475e Remove unused import 2023-10-19 14:10:13 -04:00
Marshall Bowers
70984faee2 Merge branch 'gpui2' of github.com:zed-industries/zed into gpui2 2023-10-19 14:06:35 -04:00
Marshall Bowers
e657e4d1d1 Wire up livestream debug toggle 2023-10-19 14:06:31 -04:00
Antonio Scandurra
9e20ccc01a Checkpoint 2023-10-19 19:51:05 +02:00
Antonio Scandurra
1343ea66c9 Checkpoint 2023-10-19 19:51:05 +02:00
Antonio Scandurra
2b90b8d6b7 Checkpoint 2023-10-19 19:51:05 +02:00
Antonio Scandurra
90d34c1251 Checkpoint 2023-10-19 19:51:05 +02:00
Antonio Scandurra
93ff79febf Checkpoint 2023-10-19 19:51:05 +02:00
Antonio Scandurra
7fef03a7db Checkpoint 2023-10-19 19:51:05 +02:00
Antonio Scandurra
30269381e8 Checkpoint 2023-10-19 19:51:05 +02:00
Antonio Scandurra
9985f388ac Checkpoint 2023-10-19 19:51:05 +02:00
Marshall Bowers
a869de3b1f Add ability to toggle user settings 2023-10-19 13:38:19 -04:00
Marshall Bowers
4aac733238 Pass the settings to build_child 2023-10-19 13:23:08 -04:00
Marshall Bowers
7ed891e0c6 Merge branch 'gpui2' into gpui2ui-debug-panel 2023-10-19 13:04:37 -04:00
Marshall Bowers
a1f7a97ff5 Pull the settings from the global state 2023-10-19 13:02:32 -04:00
Nate Butler
bca97f7186 Checkpoint – Broken 2023-10-19 12:58:17 -04:00
Marshall Bowers
61e09ff532 Checkpoint: Thread WindowContext through to user_settings 2023-10-19 12:58:17 -04:00
Nate Butler
8e465b4393 Add basic debug panel 2023-10-19 12:09:39 -04:00
Marshall Bowers
b16d37953d Use line_height in z_index stories 2023-10-19 12:06:05 -04:00
Marshall Bowers
f5c76d93bc Add missing Arc for on_click handler 2023-10-19 10:25:54 -04:00
Antonio Scandurra
98c0e00a2c Checkpoint 2023-10-19 15:52:17 +02:00
Antonio Scandurra
3d8e9a593e Checkpoint 2023-10-19 15:44:02 +02:00
Antonio Scandurra
ffa3362e16 Checkpoint 2023-10-19 15:36:37 +02:00
Nate Butler
e34a488b55 WIP 2023-10-18 20:58:24 -04:00
Nate Butler
c22778bd92 Remove debugs 2023-10-18 19:15:17 -04:00
Nate Butler
65828c14fc Use ui_size to build icon button 2023-10-18 19:12:02 -04:00
Nate Butler
7cb00aeb34 Update line heights, buttons to respond to UI scale
Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>
2023-10-18 17:55:17 -04:00
Nate Butler
7b2782c0f6 Use ui_size to set relative font sizes
Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>
2023-10-18 17:02:57 -04:00
Nate Butler
3f076eeda6 Remove unused code from storybook 2
Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>
2023-10-18 16:27:34 -04:00
Nate Butler
a35d350cbd Update storybook2 to run the workspace by default
Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>
2023-10-18 16:21:04 -04:00
Nate Butler
a6a50113da Merge branch 'n/gpui2ui-settings' into gpui2 2023-10-18 16:17:57 -04:00
Nate Butler
8b637e194e Update approach to settings
Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>
2023-10-18 16:16:58 -04:00
Nate Butler
289255d67a Update UI elements and implement user settings for customization 2023-10-18 15:42:10 -04:00
Marshall Bowers
549e78d7b3 Use SharedString for Inputs 2023-10-18 14:28:28 -04:00
Marshall Bowers
5a42ca6772 Merge branch 'gpui2' of github.com:zed-industries/zed into gpui2 2023-10-18 14:19:29 -04:00
Marshall Bowers
8dad3ad8ea Use SharedString for Lists 2023-10-18 14:19:26 -04:00
Antonio Scandurra
159d798c34 Checkpoint 2023-10-18 20:13:19 +02:00
Marshall Bowers
856d23626f Use SharedString for Palette and PaletteItems 2023-10-18 14:03:13 -04:00
Antonio Scandurra
8890636a56 Checkpoint 2023-10-18 19:30:53 +02:00
Antonio Scandurra
a0634fa79e Checkpoint 2023-10-18 18:39:47 +02:00
Antonio Scandurra
03937a9f89 WIP 2023-10-18 18:39:47 +02:00
Marshall Bowers
24086191af Split Label and HighlightedLabel 2023-10-18 12:37:47 -04:00
Marshall Bowers
f0b9e9a89d Use SharedString for Keybindings 2023-10-18 12:26:51 -04:00
Marshall Bowers
6e3393c93f Use SharedString for Buttons 2023-10-18 10:59:42 -04:00
Marshall Bowers
aa41f97e38 Merge branch 'gpui2' of github.com:zed-industries/zed into gpui2 2023-10-18 10:58:54 -04:00
Marshall Bowers
2b53c67789 Use SharedString for Labels 2023-10-18 10:58:50 -04:00
Antonio Scandurra
731ce1721a Checkpoint 2023-10-18 16:55:55 +02:00
Marshall Bowers
8321b9430e Wire up hover styles on Buttons 2023-10-18 10:53:42 -04:00
Marshall Bowers
7f69350e4d Fix typo in doc comment 2023-10-18 10:45:20 -04:00
Marshall Bowers
1a156c1060 Merge branch 'gpui2' of github.com:zed-industries/zed into gpui2 2023-10-18 10:44:43 -04:00
Antonio Scandurra
7149f99f02 Checkpoint 2023-10-18 16:43:20 +02:00
Marshall Bowers
5491398a64 Merge branch 'gpui2' into gpui2-ui 2023-10-18 10:43:15 -04:00
Antonio Scandurra
0e4bd485e0 Checkpoint 2023-10-18 16:36:48 +02:00
Antonio Scandurra
fecb27232e Checkpoint 2023-10-18 16:30:48 +02:00
Antonio Scandurra
f58a9bad42 Checkpoint 2023-10-18 16:30:03 +02:00
Antonio Scandurra
f4d50c4dca Checkpoint 2023-10-18 16:27:58 +02:00
Antonio Scandurra
036e266bae Checkpoint 2023-10-18 16:21:52 +02:00
Antonio Scandurra
d98c347902 Checkpoint 2023-10-18 16:13:36 +02:00
Antonio Scandurra
1270bcc6ed Checkpoint 2023-10-18 16:10:58 +02:00
Antonio Scandurra
8914b94577 Checkpoint 2023-10-18 15:46:17 +02:00
Antonio Scandurra
acca8ea786 Checkpoint 2023-10-18 15:24:35 +02:00
Antonio Scandurra
eaef1c8b8e Checkpoint 2023-10-18 15:17:22 +02:00
Antonio Scandurra
0dfe70125b Checkpoint 2023-10-18 14:41:46 +02:00
Antonio Scandurra
5afd83c883 Checkpoint 2023-10-18 14:36:16 +02:00
Antonio Scandurra
a61b34cab5 Checkpoint 2023-10-18 14:12:50 +02:00
Antonio Scandurra
ad1b96720a Checkpoint 2023-10-18 13:36:26 +02:00
Antonio Scandurra
296a2b8e5d Rename fill to bg 2023-10-18 09:39:23 +02:00
Antonio Scandurra
597a9f9548 Use text color to paint SVGs 2023-10-18 09:39:20 +02:00
Antonio Scandurra
e031718747 Checkpoint 2023-10-18 09:22:49 +02:00
Antonio Scandurra
6452ff203e Checkpoint 2023-10-18 09:15:44 +02:00
Marshall Bowers
79e0509bf9 Begin documenting flex styles 2023-10-17 17:32:30 -04:00
Marshall Bowers
218922d9f8 Document gap styles 2023-10-17 17:21:52 -04:00
Marshall Bowers
7a2b04a5d1 Document border width styles 2023-10-17 16:47:37 -04:00
Marshall Bowers
dc32e56a9c Document rounded styles 2023-10-17 16:35:21 -04:00
Marshall Bowers
490cc7ded6 Add more placeholder doc strings 2023-10-17 16:24:36 -04:00
Antonio Scandurra
4db0350f06 Checkpoint 2023-10-17 22:16:48 +02:00
Marshall Bowers
edc52e5b28 Tweak grammar 2023-10-17 16:16:38 -04:00
Marshall Bowers
a1a1284696 Document top/right/bottom/left styles 2023-10-17 16:13:55 -04:00
Marshall Bowers
6f849e8f64 Document padding styles 2023-10-17 16:10:30 -04:00
Marshall Bowers
3e32504526 Document .size() 2023-10-17 16:07:52 -04:00
Marshall Bowers
6e84d3cce0 Document margin styles 2023-10-17 16:06:11 -04:00
Marshall Bowers
8c02de6c61 Document width and height styles 2023-10-17 16:06:07 -04:00
Marshall Bowers
f09df31480 Emit doc strings for custom value setters 2023-10-17 16:01:36 -04:00
Antonio Scandurra
a8697df9e3 Checkpoint 2023-10-17 21:54:28 +02:00
Marshall Bowers
6f30d6b4d0 Add placeholder doc strings for style prefixes 2023-10-17 15:53:54 -04:00
Antonio Scandurra
61490fbaa8 Checkpoint 2023-10-17 21:40:24 +02:00
Antonio Scandurra
4ce7f059c3 Checkpoint 2023-10-17 21:37:09 +02:00
Antonio Scandurra
deb0e57c49 Checkpoint 2023-10-17 21:11:52 +02:00
Antonio Scandurra
19c1a54fea WIP 2023-10-17 18:45:01 +02:00
Antonio Scandurra
850d43c1e8 WIP 2023-10-17 18:37:53 +02:00
Antonio Scandurra
ec368c8102 WIP 2023-10-17 18:28:58 +02:00
Antonio Scandurra
c04171abf6 WIP 2023-10-17 13:32:49 +02:00
Antonio Scandurra
488d08b43c WIP 2023-10-17 12:46:50 +02:00
Antonio Scandurra
18abb068b1 Checkpoint 2023-10-17 11:35:48 +02:00
Antonio Scandurra
cec5280013 Checkpoint 2023-10-17 11:31:13 +02:00
Antonio Scandurra
c126ff10a7 Checkpoint 2023-10-17 11:21:02 +02:00
Antonio Scandurra
bb348c1353 Checkpoint 2023-10-17 11:20:11 +02:00
Antonio Scandurra
fb1e7eef6b Checkpoint 2023-10-17 11:08:48 +02:00
Antonio Scandurra
ac5b32c491 Checkpoint 2023-10-17 10:14:22 +02:00
Antonio Scandurra
b526fc070d Merge branch 'gpui2-text-wrap' into gpui2 2023-10-17 09:03:16 +02:00
Antonio Scandurra
88ae4679d1 Checkpoint 2023-10-17 09:03:01 +02:00
Antonio Scandurra
9e7a579365 Checkpoint 2023-10-17 08:57:20 +02:00
Antonio Scandurra
b040ae8d4d Checkpoint 2023-10-17 08:52:26 +02:00
Antonio Scandurra
c6e20aed9b Checkpoint 2023-10-17 08:32:33 +02:00
Nathan Sobo
695a24d8a7 Checkpoint 2023-10-17 08:26:12 +02:00
Nathan Sobo
2472142532 Checkpoint 2023-10-17 06:09:03 +02:00
Nathan Sobo
bf49f55c95 Include length in run to not use tuples 2023-10-17 05:15:00 +02:00
Nathan Sobo
0df1eb71cb Use cloned 2023-10-17 05:08:56 +02:00
Marshall Bowers
c8b452d411 Merge branch 'gpui2-ui' into gpui2 2023-10-16 16:54:40 -04:00
Marshall Bowers
708034d1d3 Call is_toggled as a method 2023-10-16 15:55:43 -04:00
Nathan Sobo
3127c78bc7 Fix compile error 2023-10-16 20:19:04 +02:00
Nathan Sobo
938dd8b9ca Checkpoint 2023-10-16 20:16:35 +02:00
Nathan Sobo
847376cd8f WIP 2023-10-16 20:15:41 +02:00
Nathan Sobo
1a3650ef2a Get everything rendering again 2023-10-16 20:15:24 +02:00
Nate Butler
129273036a Add notifications panel to workspace UI structure 2023-10-16 13:11:52 -04:00
Nate Butler
97d77440e7 Simplify static panes for now 2023-10-16 13:07:15 -04:00
Marshall Bowers
5e43c332f1 Merge branch 'gpui2-ui' into gpui2 2023-10-13 17:44:28 -04:00
Marshall Bowers
6891e86621 Add state to BufferSearch 2023-10-13 17:44:21 -04:00
Marshall Bowers
3c1ec2e9ca Add rudimentary UI for BufferSearch 2023-10-13 17:36:27 -04:00
Marshall Bowers
49caeeafce Merge branch 'gpui2-ui' into gpui2 2023-10-13 17:20:54 -04:00
Marshall Bowers
349ad7858b Add placeholder BufferSearch 2023-10-13 17:20:44 -04:00
Marshall Bowers
c70f220db3 Wire up buffer search toggle for EditorPane 2023-10-13 17:14:09 -04:00
Nate Butler
603765732e Checkpoint 2023-10-13 14:50:37 -04:00
Nathan Sobo
297b6b282c Make all geometry types Default to support movement-based refinement 2023-10-13 12:41:00 -06:00
Antonio Scandurra
fedb787b4f WIP 2023-10-13 19:06:03 +02:00
Antonio Scandurra
90f226193c Checkpoint 2023-10-13 19:06:03 +02:00
Marshall Bowers
e477fa7a93 Wire up call controls in the TitleBar 2023-10-13 12:46:35 -04:00
Marshall Bowers
f3679b37a2 Change TitleBar to a view 2023-10-13 12:21:51 -04:00
Nate Butler
b30b1d145c Refactor button rendering to use ThemeColor instead of direct theme calls 2023-10-13 11:47:13 -04:00
Nate Butler
e902d5d917 Add example NotificationToast to workspace 2023-10-13 11:47:05 -04:00
Nate Butler
8bd4107423 Fix toast contents not filling container 2023-10-13 11:46:48 -04:00
Nate Butler
7ba305e033 Hook up buttons in NotificationToast
Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>
2023-10-13 11:03:59 -04:00
Nate Butler
caa0eb6e29 Add missing derive Element
Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>
2023-10-13 10:47:39 -04:00
Nate Butler
c6d831a564 Update NotificationToast implementation and use in Workspace component 2023-10-13 10:35:30 -04:00
Nate Butler
943c02bf79 Refactor NotificationToast structure to include primary and secondary actions 2023-10-13 10:20:26 -04:00
Nate Butler
c32b081029 Add notification toast component and remove ToastVariant from toast.rs 2023-10-13 10:05:50 -04:00
Marshall Bowers
44a30e269e Restore elevation docs 2023-10-12 18:18:44 -04:00
Marshall Bowers
ef18aaa66f Merge branch 'main' into gpui2 2023-10-12 17:43:05 -04:00
Marshall Bowers
45f3a98359 Remove old ui and storybook crates (#3125)
This PR deletes the old `ui` and `storybook` crates in favor of their
newer variants that we'll be landing to `main` in the near future.

### Motivation

These crates are based off the old version of GPUI 2 (the `gpui2`
crate).

At this point we have since transitioned to the new version of GPUI 2
(the `gpui3` crate, currently still on the `gpui2` branch).

Having both copies around is confusing, so the old ones are going the
way of the dinosaurs.

Release Notes:

- N/A
2023-10-12 17:40:20 -04:00
Marshall Bowers
36bca4f0d6 Restore click events on Buttons using on_mouse_down 2023-10-12 16:56:48 -04:00
Marshall Bowers
6e5ad75c5c Use Workspace::view to construct the view 2023-10-12 16:52:32 -04:00
Marshall Bowers
79a61c28d7 Remove global WorkspaceState 2023-10-12 16:23:10 -04:00
Max Brunsfeld
bac43ae38e Fix panic when following due to disconnected channel notes views (#3124)
In addition to fixing a panic, this makes it slightly more convenient to
re-open disconnected channel notes views. I didn't make it automatic,
but it will at least replace the previous, disconnected view.

Release Notes:

- Fixed a crash that sometimes occurred when following someone with a
disconnected channel notes view open.
2023-10-12 13:16:58 -07:00
Marshall Bowers
e900ea20b7 Fix toggling of left panel 2023-10-12 16:12:09 -04:00
Marshall Bowers
8496d02fe1 Hold the story view in the StoryWrapper 2023-10-12 16:11:59 -04:00
Marshall Bowers
fc94c4ea40 Render stories as Views 2023-10-12 16:06:54 -04:00
Marshall Bowers
c90d976d7a Remove debug logging in Element derive macro 2023-10-12 15:52:42 -04:00
Marshall Bowers
d320d3a8bf Remove hacky children 2023-10-12 15:50:09 -04:00
Marshall Bowers
24bab48043 Use new children approach for Toolbar 2023-10-12 15:47:26 -04:00
Max Brunsfeld
f5d6d7caca Mark channel notes as disconnected immediately upon explicitly signing out 2023-10-12 12:39:02 -07:00
Max Brunsfeld
85fe11ff11 Replace disconnected channel notes views when re-opening the notes 2023-10-12 12:38:23 -07:00
Marshall Bowers
30979caf25 Use new children approach for Panes and Toasts 2023-10-12 15:37:50 -04:00
Nathan Sobo
ce8533f83b Checkpoint 2023-10-12 13:27:46 -06:00
Max Brunsfeld
2e5461ee4d Exclude disconnected channel views from following messages 2023-10-12 11:55:39 -07:00
Antonio Scandurra
2044ccdc0b WIP 2023-10-12 19:40:13 +02:00
Antonio Scandurra
ca35573ad5 WIP 2023-10-12 19:30:00 +02:00
Marshall Bowers
6dbe983461 Checkpoint: Back to a compiling state 2023-10-12 12:22:23 -04:00
Marshall Bowers
262f5886a4 Checkpoint 2023-10-12 12:18:35 -04:00
Marshall Bowers
207d843aee Fix issues in storybook2 2023-10-12 10:44:18 -04:00
Marshall Bowers
a6b872bb0c Temporarily disable click handlers 2023-10-12 10:44:11 -04:00
Marshall Bowers
8cd112110e Reconcile with upstream changes 2023-10-12 10:40:47 -04:00
Marshall Bowers
9581279919 Fix some merge errors 2023-10-12 10:27:50 -04:00
Marshall Bowers
002458f4c8 Merge branch 'gpui2' into gpui2-ui 2023-10-12 10:27:43 -04:00
Kirill Bulatov
a50977e0fd Add prettier support (#3122) 2023-10-12 17:13:10 +03:00
Kirill Bulatov
ef73bf799c Fix license issue 2023-10-12 16:26:28 +03:00
Kirill Bulatov
7aea95704e Revert unnecessary style changes 2023-10-12 16:17:41 +03:00
Antonio Scandurra
564a8bdc19 Checkpoint 2023-10-12 14:58:31 +02:00
Kirill Bulatov
09ef3ccf67 Fix tailwind prettier plugin discovery 2023-10-12 15:58:00 +03:00
Antonio Scandurra
1f84cdb88c Checkpoint 2023-10-12 14:49:06 +02:00
Kirill Bulatov
12d7d8db0a Make all formatting to happen on the client's buffers, as needed 2023-10-12 15:29:57 +03:00
Kirill Bulatov
1bfde4bfa2 Add more tests 2023-10-12 15:14:51 +03:00
Antonio Scandurra
80c0a6ead3 Checkpoint 2023-10-12 13:25:49 +02:00
Kirill Bulatov
7f4ebf50d3 Make the first prettier test pass 2023-10-12 13:30:49 +03:00
Kirill Bulatov
a528c6c686 Prettier server style fixes 2023-10-12 12:31:30 +03:00
Antonio Scandurra
23f11fcd5e Merge branch 'main' into gpui2 2023-10-12 10:55:17 +02:00
Nathan Sobo
3dad0d9811 Add group_active 2023-10-11 21:48:21 -06:00
Nathan Sobo
d920f7edc1 Add group hovers 2023-10-11 21:34:08 -06:00
Nathan Sobo
f37b83a0ea WIP 2023-10-11 17:18:39 -06:00
Nate Butler
12573ed2e7 Refine project panel, list item 2023-10-11 19:15:27 -04:00
Conrad Irwin
be1800884e Make collaboration warning more useful (#3119)
Release Notes:

- Fixed the titlebar upgrade UI to restart zed when an update is
available
2023-10-11 15:35:41 -06:00
Nathan Sobo
93c233b1cf Checkpoint 2023-10-11 13:22:40 -06:00
Nathan Sobo
47b64a5074 Checkpoint 2023-10-11 12:51:56 -06:00
Joseph T. Lyons
d6fa06b3be collab 0.24.0 2023-10-11 13:51:01 -04:00
Nathan Sobo
e2da2b232e Checkpoint 2023-10-11 11:40:42 -06:00
Joseph T. Lyons
bdf1731db3 v0.109.x dev 2023-10-11 12:40:57 -04:00
Marshall Bowers
5477b87774 Hook up assistant panel 2023-10-11 12:38:06 -04:00
Marshall Bowers
7478e63ea0 Simplify state interactions 2023-10-11 12:32:05 -04:00
Marshall Bowers
922d1462a8 Merge branch 'gpui2-ui' of github.com:zed-industries/zed into gpui2-ui 2023-10-11 12:24:34 -04:00
Marshall Bowers
8f410d5e2e Add support for switching between the project and collab panels 2023-10-11 12:24:33 -04:00
Nate Butler
0d8c743dfe Refine project panel, list 2023-10-11 12:22:24 -04:00
Kirill Bulatov
e50f4c0ee5 Add prettier tests infrastructure 2023-10-11 19:13:28 +03:00
Marshall Bowers
b6a9c58994 Push language selector handler down into StatusBar 2023-10-11 12:11:22 -04:00
Marshall Bowers
382693a199 Adjust icon color based on whether the various components are open 2023-10-11 12:09:08 -04:00
Marshall Bowers
acf2c2c6a5 Add ability to toggle the terminal 2023-10-11 12:01:17 -04:00
Antonio Scandurra
006f840570 Checkpoint 2023-10-11 17:53:29 +02:00
Conrad Irwin
2d6725a41a Make collaboration warning more useful 2023-10-11 09:50:22 -06:00
Antonio Scandurra
457df8d3f3 Start and stop display link on the main thread 2023-10-11 16:38:54 +02:00
Antonio Scandurra
b6e4208ea8 Checkpoint 2023-10-11 15:08:28 +02:00
Antonio Scandurra
56fba5541a Checkpoint 2023-10-11 14:19:04 +02:00
Kirill Bulatov
4a88a9e253 Initialize prettier right after the buffer gets it language 2023-10-11 14:48:32 +03:00
Antonio Scandurra
a69dbafe3c Checkpoint 2023-10-11 12:47:19 +02:00
Antonio Scandurra
a9c69bf774 Checkpoint 2023-10-11 12:45:09 +02:00
Kirill Bulatov
986a516bf1 Small style fixes 2023-10-11 12:56:29 +03:00
Kirill Bulatov
9bf22c56cd Rebase fixes 2023-10-11 12:56:29 +03:00
Kirill Bulatov
b5705e079f Draft remote prettier formatting 2023-10-11 12:56:29 +03:00
Kirill Bulatov
2ec2036c2f Invoke remote Prettier commands 2023-10-11 12:56:29 +03:00
Kirill Bulatov
faf1d38a6d Draft local and remote prettier separation 2023-10-11 12:56:29 +03:00
Kirill Bulatov
6c1c7eaf75 Better detect Svelte plugins 2023-10-11 12:56:29 +03:00
Kirill Bulatov
2d5741aef8 Better prettier format logging 2023-10-11 12:56:29 +03:00
Kirill Bulatov
a9f80a603c Resolve prettier config before every formatting 2023-10-11 12:56:29 +03:00
Kirill Bulatov
658b58378e Properly use WorktreeId 2023-10-11 12:56:29 +03:00
Kirill Bulatov
8a807102a6 Properly support prettier plugins 2023-10-11 12:56:29 +03:00
Kirill Bulatov
afee29ad3f Do not clear cache for default prettiers 2023-10-11 12:56:29 +03:00
Kirill Bulatov
6ec3927dd3 Allow to configure default prettier 2023-10-11 12:56:29 +03:00
Kirill Bulatov
b109075bf2 Watch for prettier file changes 2023-10-11 12:56:29 +03:00
Kirill Bulatov
f4667cbc33 Resolve prettier config on server init 2023-10-11 12:56:29 +03:00
Kirill Bulatov
d021842fa1 Properly log pre-lsp prettier_server events 2023-10-11 12:56:29 +03:00
Kirill Bulatov
f42cb109a0 Improve prettier_server LSP names in the log panel 2023-10-11 12:56:29 +03:00
Kirill Bulatov
1b70e7d0df Before server startup, log to stderr 2023-10-11 12:56:29 +03:00
Kirill Bulatov
b687270207 Implement missing prettier_server clear method 2023-10-11 12:56:29 +03:00
Kirill Bulatov
06cac18d78 Return message id in prettier_server error responses 2023-10-11 12:56:29 +03:00
Kirill Bulatov
6cac58b34c Add prettier language servers to LSP logs panel 2023-10-11 12:56:29 +03:00
Kirill Bulatov
4b15a2bd63 Rebase fixes 2023-10-11 12:56:29 +03:00
Kirill Bulatov
e8409a0108 Even more generic header printing in prettier_server 2023-10-11 12:56:29 +03:00
Kirill Bulatov
39ad3a625c Generify prettier properties, add tabWidth 2023-10-11 12:56:29 +03:00
Kirill Bulatov
2a5b9b635b Better pass prettier options 2023-10-11 12:56:29 +03:00
Kirill Bulatov
e2056756ef Calculate the diff 2023-10-11 12:56:29 +03:00
Kirill Bulatov
6a8e3fd02d Add more parameters into prettier invocations 2023-10-11 12:56:29 +03:00
Kirill Bulatov
2a68f01402 Draft prettier_server formatting 2023-10-11 12:56:29 +03:00
Kirill Bulatov
dca93fb177 Initialize prettier_server.js wrapper along with default prettier 2023-10-11 12:56:29 +03:00
Kirill Bulatov
010bb73ac2 Use LSP-like protocol for prettier wrapper commands 2023-10-11 12:56:29 +03:00
Kirill Bulatov
bb2cc2d157 Async-ify prettier wrapper 2023-10-11 12:56:29 +03:00
Kirill Bulatov
86618a64c6 Require prettier argument and library in the wrapper 2023-10-11 12:56:29 +03:00
Kirill Bulatov
1ff17bd15d Install default prettier and plugins on startup 2023-10-11 12:56:29 +03:00
Kirill Bulatov
12ea12e4e7 Make language adapters able to require certain bundled formatters 2023-10-11 12:56:29 +03:00
Kirill Bulatov
4f956d71e2 Slightly better prettier settings and discovery 2023-10-11 12:56:29 +03:00
Kirill Bulatov
ce6b31d938 Make NodeRuntime non-static for prettier runner 2023-10-11 12:56:29 +03:00
Kirill Bulatov
a8387b8b19 Use proper NodeRuntime in the formatter interface 2023-10-11 12:56:28 +03:00
Kirill Bulatov
a420d9cdc7 Add prettier search 2023-10-11 12:56:28 +03:00
Kirill Bulatov
a8dfa01362 Prepare prettier file lookup code infra 2023-10-11 12:56:28 +03:00
Kirill Bulatov
92f23e626e Properly connect prettier lookup/creation methods 2023-10-11 12:56:28 +03:00
Kirill Bulatov
553abd01be Draft a project part of the prettier 2023-10-11 12:56:28 +03:00
Julia
eced842dfc Get started with a prettier server package
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
2023-10-11 12:56:28 +03:00
Antonio Scandurra
b6a3d9ce59 Checkpoint 2023-10-11 11:03:08 +02:00
Antonio Scandurra
eebbc807e5 Checkpoint 2023-10-11 10:45:57 +02:00
Antonio Scandurra
0fb7364235 Checkpoint 2023-10-11 09:17:25 +02:00
Joseph T. Lyons
76191fe47d Fix Discord text truncation 2023-10-11 01:54:32 -04:00
Nathan Sobo
f1cc62c21f WIP 2023-10-10 22:49:47 -06:00
Nathan Sobo
f53b63eaf6 Checkpoint 2023-10-10 22:14:47 -06:00
Conrad Irwin
821997d372 Revert accidental build change 2023-10-10 19:59:57 -06:00
Conrad Irwin
85b76b1143 Don't wrap on paragraphs (#3094)
Release Notes:

- vim: `{` and `}` will no longer wrap around end of file
([#2116](https://github.com/zed-industries/community/issues/2116)).
2023-10-10 19:25:40 -06:00
Conrad Irwin
9004254fbf vim: Add shift-y (#3117)
Release Notes:

- vim: Add `Y` to copy line-wise (this copies vim's behaviour, which
differs from nvim's)
2023-10-10 19:25:32 -06:00
Conrad Irwin
1de9add304 vim: Add shift-y 2023-10-10 18:46:49 -06:00
Max Brunsfeld
7a39455af9 Fix inclusion of spurious views from other projects in FollowResponse (#3116)
A logic error in https://github.com/zed-industries/zed/pull/2993 caused
follow responses to sometimes contain extra views for other unshared
projects 😱 . These views would generally fail to deserialize on the
other end. This would create a broken intermediate state, where the
following relationship was registered on the server (and on the leader's
client), but the follower didn't have the state necessary for following
into certain views.

Release Notes:

- Fixed a bug where following would sometimes fail if the leader had
another unshared project open.
2023-10-10 15:53:11 -07:00
Max Brunsfeld
96d60eff23 Fix inclusion of spurious views from other projects in FollowResponse 2023-10-10 15:40:40 -07:00
Marshall Bowers
a69f93d214 Wire up toggling of project and chat panels 2023-10-10 18:35:20 -04:00
Marshall Bowers
8e1638b773 Add working toggle for LanguageSelector 2023-10-10 18:21:44 -04:00
Marshall Bowers
95ef61bc45 Thread click handler through from workspace to language selector 2023-10-10 18:02:08 -04:00
Marshall Bowers
c142676b20 Add click handlers to some of the buttons in the StatusBar 2023-10-10 17:26:53 -04:00
Marshall Bowers
be843227a1 Add on_click to IconButton 2023-10-10 17:26:33 -04:00
Marshall Bowers
48d9b49ada Wire up click handlers on Buttons 2023-10-10 17:19:18 -04:00
Mikayla Maki
19f774a4a4 Update channel rooms to be ephemeral (#3115)
This fixes a bug that was introduced by
https://github.com/zed-industries/zed/pull/3093, which assumed that
rooms for channels where ephemeral, by making rooms for channels
ephemeral.

Release Notes:

- N/A
2023-10-10 13:28:42 -07:00
Mikayla
d7d027bcf1 Rename release channel to enviroment 2023-10-10 13:23:03 -07:00
Joseph T. Lyons
e6228ca682 Slim down pull request template 2023-10-10 16:04:31 -04:00
Marshall Bowers
f2ee61553f Colocate element stories with their elements 2023-10-10 16:00:04 -04:00
Marshall Bowers
30088afa89 Colocate component stories with their components 2023-10-10 15:52:58 -04:00
Mikayla
40430cf01b Update channel rooms to be ephemeral
Remove redundant live kit initialization code
Fix bug in recent channel links changes where channel rooms would have the incorrect release set

co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
co-authored-by: Max <max@zed.dev>
2023-10-10 12:39:16 -07:00
Marshall Bowers
b1d88ced61 Add an example of colocating a story for a UI component with its definition 2023-10-10 15:30:16 -04:00
Marshall Bowers
5b7ca6435c Comment out overridden widths 2023-10-10 15:17:44 -04:00
Marshall Bowers
a6ae6b0752 Merge branch 'gpui2' into gpui2-ui 2023-10-10 15:12:59 -04:00
Nathan Sobo
61b8ad38bd Remove state erasure for now 2023-10-10 12:44:40 -06:00
Nathan Sobo
e714653478 Checkpoint 2023-10-10 12:42:44 -06:00
Nathan Sobo
d70b4f04f6 Checkpoint 2023-10-10 12:41:28 -06:00
Antonio Scandurra
9eff99de49 --amend 2023-10-10 20:02:34 +02:00
Antonio Scandurra
4855b8f3de WIP 2023-10-10 20:02:23 +02:00
Antonio Scandurra
84ad2cb827 Checkpoint 2023-10-10 19:48:32 +02:00
Antonio Scandurra
0e537cced4 Revert outline summarization (#3114)
This pull request essentially reverts #3067: we noticed that only using
the function signatures produces far worse results in codegen, and so
that feels like a regression compared to before. We should re-enable
this once we have a smarter approach to fetching context during codegen,
possibly when #3097 lands.

As a drive-by, we also fixed a longstanding bug that caused codegen to
include the final line of a selection even if the selection ended at the
start of the line.

Ideally, I'd like to hot fix this to preview so that it goes to stable
during the weekly release.

/cc: @KCaverly @nathansobo 

Release Notes:

- N/A
2023-10-10 19:20:54 +02:00
Antonio Scandurra
b366592878 Don't include start of a line when selection ends at start of line 2023-10-10 19:11:13 +02:00
Antonio Scandurra
5cf92980f0 Revert summarizing file content until we can be more intelligent about what we send
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-10-10 17:51:17 +02:00
Marshall Bowers
8f7f38536d Re-enable hover styles 2023-10-10 11:48:25 -04:00
Antonio Scandurra
97edec6e72 WIP 2023-10-10 17:31:42 +02:00
Marshall Bowers
40d58c9bc3 Use Self::State in children_any and child_any 2023-10-10 11:26:18 -04:00
Marshall Bowers
f76c9041bb Merge branch 'gpui2' into marshall/gpui2-playground 2023-10-10 11:24:45 -04:00
Conrad Irwin
66af1707a1 Add channel links (#3093)
Release notes:

- `mute_on_join` setting now defaults to false.
- Right click on a channel to "Copy Channel Link", these links work to
open Zed and auto-join the channel

Blocked on: https://github.com/zed-industries/zed.dev/pull/388
2023-10-10 08:53:50 -06:00
Antonio Scandurra
96fbf9fd06 Checkpoint 2023-10-10 16:47:09 +02:00
Antonio Scandurra
48a12be538 WIP 2023-10-10 15:03:47 +02:00
Antonio Scandurra
012a7743ad Checkpoint 2023-10-10 14:43:55 +02:00
Antonio Scandurra
678235023f Checkpoint 2023-10-10 13:07:53 +02:00
Antonio Scandurra
a4afb72535 Checkpoint: beziers 2023-10-10 13:01:35 +02:00
Kirill Bulatov
1db24e5f2a Omit history files with path that does not exist on disk anymore (#3113) 2023-10-10 11:55:06 +02:00
Kirill Bulatov
639ae671ae Omit history files with path that does not exist on disk anymore 2023-10-10 12:26:48 +03:00
Joseph T. Lyons
1a4e9ecfef Truncate Discord release note text (#3112)
Hopefully this works the first time 😅

Release Notes:

- N/A
2023-10-10 00:07:48 -04:00
Joseph T. Lyons
dcdd74dff4 Truncate Discord release note text 2023-10-10 00:00:57 -04:00
Nathan Sobo
fe60f264c4 Checkpoint 2023-10-09 21:46:49 -06:00
Nathan Sobo
dfdb691f73 Checkpoint 2023-10-09 21:30:14 -06:00
Nathan Sobo
9fe5836240 Move events module up 2023-10-09 21:19:56 -06:00
Nathan Sobo
8074e6b46a Add basic mouse event handling 2023-10-09 21:17:56 -06:00
Conrad Irwin
d4ef764305 Merge branch 'main' into links 2023-10-09 20:08:48 -06:00
Nathan Sobo
f763ed9a7e Checkpoint 2023-10-09 19:54:29 -06:00
Conrad Irwin
8922437fcd code review 2023-10-09 19:06:55 -06:00
Max Brunsfeld
6e98cd5aad More small following-related fixes (#3110) 2023-10-09 15:25:22 -07:00
Marshall Bowers
08f4576aa6 Rename helper style 2023-10-09 18:17:01 -04:00
Max Brunsfeld
1d29709c32 Avoid possible panic in Room::most_active_project
Participants' locations might momentarily reference projects that have already been unshared.
2023-10-09 15:04:01 -07:00
Marshall Bowers
7610028a89 Add a story showcasing z-index 2023-10-09 18:00:49 -04:00
Max Brunsfeld
bdcbf9b92e Add a Reconnect action, for simulating connection blips 2023-10-09 14:46:33 -07:00
Max Brunsfeld
b807b3c785 Handle participants' participant index changing
This normally doesn't happen, but it can happen if a participant
loses connection ungracefully, restarts their app, and then
explicitly joins again.
2023-10-09 14:45:19 -07:00
Max Brunsfeld
90b54a45e8 Log a warning when leader activates an unknown view 2023-10-09 14:29:45 -07:00
Kirill Bulatov
bb85d6f63e Detect file paths that end with : (#3109)
New rustc messages look like

```
thread 'tests::test_history_items_vs_very_good_external_match' panicked at crates/file_finder/src/file_finder.rs:1902:13:
assertion `left == right` failed: Only one history item contains collab_ui, it should be present and others should be filtered out
  left: 0
 right: 1
```

now and we fail to parse that `13:` bit properly, fix that.

One caveat is that we highlight the entire word including the trailing
`:`:
<img width="914" alt="image"
src="https://github.com/zed-industries/zed/assets/2690773/d653a8ff-3e6e-4e3d-b6ea-dad0c8db0f06">

this is unfortunate, but better than nothing (as now).
This is due to the fact, that we detect words with regex inside the
`terminal.rs` and send events to other place that's able to check paths
for existence (and whether that's a path at all), currently there's no
way to detect a path and sanitize it in `terminal.rs`

Release Notes:

- N/A
2023-10-09 23:16:03 +02:00
Marshall Bowers
0d903f4d0d Clean up theme loading 2023-10-09 17:00:10 -04:00
Kirill Bulatov
ba4f4e0a3e Detect file paths that end with :
New rustc messages look like

```
thread 'tests::test_history_items_vs_very_good_external_match' panicked at crates/file_finder/src/file_finder.rs:1902:13:
assertion `left == right` failed: Only one history item contains collab_ui, it should be present and others should be filtered out
  left: 0
 right: 1
```

now and we fail to parse that `13:` bit properly, fix that.
2023-10-09 23:55:58 +03:00
Marshall Bowers
312f3d2ab9 Change how the default theme gets determined 2023-10-09 16:53:28 -04:00
Max Brunsfeld
6b710dc146 Fix bug that allowed following multiple people in one pane (#3108)
I've also simplified the representation of a workspace's leaders, so
that it encodes in the type that there can only be one leader per pane.

Release Notes:

- Fixed a bug where you could accidentally follow multiple collaborators
in one pane at the same time.
2023-10-09 13:50:51 -07:00
Marshall Bowers
def67295e5 Add theme loading 2023-10-09 16:37:20 -04:00
Kirill Bulatov
0823a18cff Ignore history items' paths when matching search queries (#3107)
Follow-up of https://github.com/zed-industries/zed/pull/3059 

Before: 

![image](https://github.com/zed-industries/zed/assets/2690773/4eb2d2d1-1aa3-40b8-b782-bf2bc5f17b43)

After:

![image](https://github.com/zed-industries/zed/assets/2690773/5587d46b-9198-45fe-9372-114a95d4b7d6)

Release Notes:

- N/A
2023-10-09 22:35:11 +02:00
Max Brunsfeld
ca735ad70f Ensure there's only one leader per pane 2023-10-09 13:32:38 -07:00
Max Brunsfeld
af90077a6a Add failing test for switching leaders in a pane 2023-10-09 13:30:14 -07:00
Kirill Bulatov
9cba45910e Ignore history items' paths when matching search queries 2023-10-09 23:14:32 +03:00
Marshall Bowers
613973d2b1 Add support for switching between the two hardcoded themes 2023-10-09 15:52:57 -04:00
Max Brunsfeld
29ccdb3cd9 Unify the two local zed scripts, take a flag for an instance count (#3106)
This PR introduces a new script for running Zed against a local collab
server, called `script/zed-local`. This script replaces the two existing
scripts that we had for this purpose: `script/zed-with-local-servers`
and `script/start-local-collaboration`.

By default, the script starts one single instance of Zed, but you can
pass a numeric flag to start 1, 2, 3 or 4 instances. So to start up two
instances side by side, (like `start-local-collaboration` script), you'd
do this:

```
script/zed-local -2
```

But you can also start *three* (or even four) instances, each taking up
a quarter of the screen, like this:

```
script/zed-local -3
```

Like before, you can pass other arguments to the script, and they will
be passed through to the first zed instance.

Also, unlike the `start-local-collaboration` script, this script now
requires a call to GitHub to determine your GitHub username. It just
logs you in as Nathan by default, unless you set `ZED_IMPERSONATE`
explicitly.
2023-10-09 12:52:20 -07:00
Max Brunsfeld
1e4f5145cf Update docs to refer to new zed-local script 2023-10-09 12:49:12 -07:00
Max Brunsfeld
a0ab9fe56b Unify the 2 local zed scripts, take a flag for instance count 2023-10-09 12:40:36 -07:00
Conrad Irwin
fb57299a1d re-trigger build with new profile? 2023-10-09 13:40:22 -06:00
Conrad Irwin
162cb19cff Only allow one release channel in a call 2023-10-09 12:59:18 -06:00
Conrad Irwin
abfb4490d5 Focus the currently active project if there is one
(also consider your own projects in "most_active_projects")
2023-10-09 12:05:26 -06:00
Antonio Scandurra
7b610f8dd8 WIP 2023-10-09 19:50:48 +02:00
Marshall Bowers
8b3a357949 Add note about CSS hack 2023-10-09 13:39:08 -04:00
Marshall Bowers
f73708d725 Add a Toast in the bottom-right corner 2023-10-09 13:32:19 -04:00
Marshall Bowers
d3c79c7078 Add div.z_index 2023-10-09 13:19:32 -04:00
Antonio Scandurra
d889cdecde Checkpoint 2023-10-09 19:13:57 +02:00
Marshall Bowers
2654942b3c Use command modifier for example keybindings in CommandPalette 2023-10-09 12:51:57 -04:00
Marshall Bowers
ed2c8cdc25 Add strikethrough support back to Label 2023-10-09 12:33:52 -04:00
Marshall Bowers
19434afe0a Add back negative margins, now that they're supported again 2023-10-09 12:33:30 -04:00
Marshall Bowers
a7c4ae530d Update gpui3_macros::style_helpers! based on its gpui2 equivalent 2023-10-09 12:31:22 -04:00
Max Brunsfeld
b2d735e573 Always log panics (#2896)
I just panicked and wanted to see the cause, but forgot that panic files
get deleted when Zed uploads them.

Release Notes:

- Panics are now written to `~/Library/Logs/Zed/Zed.log`
2023-10-09 09:21:08 -07:00
Max Brunsfeld
044701e907 Add a crate-dep-graph script, remove a few unnecessary dependencies (#3103)
This was motivated by me trying to decide which crate I should put a
`NotificationStore` in.

Run `script/crate-dep-graph` to generate an SVG showing the dependency
graph of our `crates` folder, and open it in a web browser.

After running this command, I noticed a couple of dependencies that
didn't make sense and were easy to remove.

Current dependency graph:

![Screen Shot 2023-10-06 at 1 15 42
PM](https://github.com/zed-industries/zed/assets/326587/b5008235-498a-4562-a826-cc923898c052)
2023-10-09 09:20:06 -07:00
Marshall Bowers
42e9800bde Add Details component 2023-10-09 11:55:27 -04:00
Marshall Bowers
d956bd3743 Add RecentProjects component 2023-10-09 11:47:22 -04:00
Conrad Irwin
6084486dcd Code quality 2023-10-09 09:44:09 -06:00
Marshall Bowers
100a4731e2 Add ThemeSelector component 2023-10-09 11:44:08 -04:00
Marshall Bowers
000ae27aff Add LanguageSelector component 2023-10-09 11:39:42 -04:00
Marshall Bowers
06b0707aa9 Add MultiBuffer component 2023-10-09 11:36:09 -04:00
Marshall Bowers
ac93449788 Remove unused Arc import 2023-10-09 11:32:10 -04:00
Marshall Bowers
02d32de044 Add Toast component 2023-10-09 11:31:56 -04:00
Conrad Irwin
8f4d81903c Add "Copy Link" to channel right click menu 2023-10-09 09:30:00 -06:00
Marshall Bowers
333e3e4f01 Add ContextMenu component 2023-10-09 11:25:33 -04:00
Marshall Bowers
f7721d0523 Add CommandPalette component 2023-10-09 11:20:10 -04:00
Marshall Bowers
e5473fc51a Add Palette component 2023-10-09 11:15:50 -04:00
Marshall Bowers
a08ceadd1a Rename view_type to state_type 2023-10-09 11:11:03 -04:00
Marshall Bowers
dc2ddfb42c Add Keybinding component 2023-10-09 11:09:44 -04:00
Marshall Bowers
4eeed14d34 Add CollabPanel component 2023-10-09 11:04:53 -04:00
Conrad Irwin
5dbda70235 Fix ./script/bundle to allow passing key 2023-10-09 08:59:25 -06:00
Kirill Bulatov
38d53a6fe2 Bump curl-sys to fix Sonoma issues with it
See https://github.com/alexcrichton/curl-rust/issues/524
2023-10-09 17:09:58 +03:00
Antonio Scandurra
6a4c2a0d40 WIP 2023-10-09 16:02:55 +02:00
Joseph T. Lyons
77a932fe3b Add enable vim mode checkbox to welcome screen (#3105)
Had a user state that they didn't know how to enable vim mode and that
it was "almost a non-starter" for them. IMO, it is a big enough feature
to warrant being on the welcome screen.

<img width="968" alt="SCR-20231008-rnhj"
src="https://github.com/zed-industries/zed/assets/19867440/a189c646-1fa7-497c-b6d9-37cb1caa0492">

Release Notes:

- Added an `Enable vim mode` checkbox to the welcome screen
2023-10-08 21:27:31 -04:00
Joseph T. Lyons
4b2c24dd8c Add enable vim mode checkbox to welcome screen 2023-10-08 20:07:59 -04:00
Marshall Bowers
8814ea8241 Time compute_layout 2023-10-07 12:23:25 -04:00
Marshall Bowers
8f6649e29e Merge branch 'gpui2' into marshall/gpui2-playground 2023-10-07 12:16:48 -04:00
Marshall Bowers
73360d37f7 Merge branch 'main' into gpui2 2023-10-07 12:15:23 -04:00
Marshall Bowers
eb642551ac Add TitleBar component and wire up to the workspace 2023-10-07 12:10:39 -04:00
Marshall Bowers
f33d41af63 Add Facepile and PlayerStack components 2023-10-07 12:02:42 -04:00
Marshall Bowers
5e7954f152 Add TrafficLights component 2023-10-07 11:55:10 -04:00
Marshall Bowers
9e79ad5a62 Add ChatPanel component 2023-10-07 11:50:41 -04:00
Marshall Bowers
0dcbc47e15 Remove duplicate module declaration 2023-10-07 11:42:50 -04:00
Marshall Bowers
b8b8fe6120 Add Button component 2023-10-07 11:41:48 -04:00
Marshall Bowers
ff066ef177 Add EditorPane component and wire up in the workspace 2023-10-07 11:33:11 -04:00
Marshall Bowers
63e834ce73 Add Toolbar component 2023-10-07 11:21:09 -04:00
Marshall Bowers
b118e60160 Add Breadcrumb component 2023-10-07 11:18:06 -04:00
Marshall Bowers
00e8531898 Add TabBar component 2023-10-07 11:13:54 -04:00
Marshall Bowers
7c8d662315 Increase storybook window size 2023-10-07 11:09:21 -04:00
Marshall Bowers
2f6d67cad6 Update Cargo.lock 2023-10-07 10:56:21 -04:00
Marshall Bowers
f5e5b44bc1 Merge branch 'gpui2' into marshall/gpui2-playground 2023-10-07 10:54:17 -04:00
Marshall Bowers
f795177ab6 Fix icon paths 2023-10-07 10:51:19 -04:00
Marshall Bowers
a4bde421db Revert changes to gpui2 crate 2023-10-07 10:50:50 -04:00
Marshall Bowers
f6a4151f60 Merge branch 'main' into gpui2 2023-10-07 10:50:05 -04:00
Conrad Irwin
34b7537948 Add universal links support to mac platform 2023-10-06 23:15:37 -06:00
Conrad Irwin
66120fb97a Try universal link entitlement too 2023-10-06 22:25:00 -06:00
Mikayla
6de69de868 Remove change to linker args 2023-10-06 16:04:45 -07:00
Marshall Bowers
82577b4acc Add Terminal component 2023-10-06 18:50:49 -04:00
Conrad Irwin
f6bc229d1d More progress and some debug logs to remove 2023-10-06 16:48:29 -06:00
Marshall Bowers
8db7f7ed37 Add Tab component 2023-10-06 18:43:25 -04:00
Marshall Bowers
d5ffd4a1fb Add Pane and PaneGroup components 2023-10-06 18:37:28 -04:00
Marshall Bowers
b53579858a Add StatusBar component 2023-10-06 18:25:55 -04:00
Marshall Bowers
28d504d7d3 Add WorkspaceElement component 2023-10-06 18:19:12 -04:00
Conrad Irwin
63a230f92e Make joining on boot work 2023-10-06 16:11:45 -06:00
Marshall Bowers
56c2ac048d Add ProjectPanel component 2023-10-06 17:58:23 -04:00
Marshall Bowers
208d5df106 Add Buffer component 2023-10-06 17:47:10 -04:00
Marshall Bowers
d09f53c380 Add AssistantPanel component 2023-10-06 17:24:52 -04:00
Max Brunsfeld
f8ca86c6a7 Remove workspace -> channel dependency 2023-10-06 14:19:25 -07:00
Conrad Irwin
4128e2ffcb Fix panic if the host is not there. 2023-10-06 15:18:25 -06:00
Marshall Bowers
696aee3891 Add IconButton component 2023-10-06 17:16:00 -04:00
Marshall Bowers
bcad2f4e9e Move UI out of storybook2 and into ui2 2023-10-06 17:07:59 -04:00
Marshall Bowers
1cf5cdbeca Add ui2 crate 2023-10-06 16:52:05 -04:00
Marshall Bowers
8e94f3902b Merge branch 'marshall/merge-main-into-gpui2' into marshall/gpui2-playground 2023-10-06 16:47:40 -04:00
Max Brunsfeld
3412bb75be Remove call -> channel dependency 2023-10-06 13:39:10 -07:00
Max Brunsfeld
17925ed563 Remove unnecessary dependencies on client and rpc 2023-10-06 13:14:53 -07:00
Max Brunsfeld
43da36948b Add a crate-dep-graph script for showing the crate dependency graph 2023-10-06 13:14:39 -07:00
Marshall Bowers
88a6a41c7c Revert changes to gpui2 crate 2023-10-06 15:49:53 -04:00
Conrad Irwin
b58c42cd53 TEMP 2023-10-06 13:47:35 -06:00
Marshall Bowers
d37785c214 Fix icon paths 2023-10-06 15:46:02 -04:00
Marshall Bowers
b369a6dc2a Merge branch 'main' into marshall/merge-main-into-gpui2 2023-10-06 15:41:18 -04:00
Max Brunsfeld
9f32a6e209 collab 0.23.3 2023-10-06 11:25:46 -07:00
Max Brunsfeld
3f66caedfc Fix error in query for last N channel messages (#3100) 2023-10-06 11:24:34 -07:00
Joseph T. Lyons
1dd82df59e Use display name for release channel in panic events (#3101)
This was a mistake from long ago - something I've been meaning to fix
for a long time. All other events use `display_name()`, but panic
events, which leads to mistakes when filtering out `Zed Dev`, which
isn't the format that `dev_name()` returns. I'm adding a fix to zed.dev
as well:

- https://github.com/zed-industries/zed.dev/pull/393

so that the values are adjusted for all clients, not just ones with this
fix. I will correct the data in clickhouse, and adjust the queries in
metabase.

Release Notes:

- N/A
2023-10-06 14:20:06 -04:00
Joseph T. Lyons
81bc86be07 Use display name for release channel in panic events 2023-10-06 14:04:38 -04:00
Max Brunsfeld
663649a100 Fix error in query for last N channel messages 2023-10-06 10:58:34 -07:00
Marshall Bowers
5ee6814947 Fix compile errors 2023-10-06 13:54:37 -04:00
Marshall Bowers
65cd4f5838 Restore Sized bound on StyleHelpers 2023-10-06 13:45:56 -04:00
Marshall Bowers
7fd35d68bb Merge branch 'gpui2' into marshall/gpui2-playground 2023-10-06 13:45:11 -04:00
Marshall Bowers
ad8187b151 Merge branch 'main' into marshall/gpui2-playground 2023-10-06 13:33:04 -04:00
Joseph T. Lyons
1e557dddcc Add session id to panic events (#3098)
Release Notes:

- N/A
2023-10-06 13:32:45 -04:00
Marshall Bowers
456baaa112 Mainline GPUI2 UI work (#3099)
This PR mainlines the current state of new GPUI2-based UI from the
`gpui2-ui` branch.

Included in this is a performance improvement to make use of the
`TextLayoutCache` when calling `layout` for `Text` elements.

Release Notes:

- N/A

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2023-10-06 13:18:56 -04:00
Joseph T. Lyons
2c7e37e9ff Add session id to panic events 2023-10-06 12:32:20 -04:00
Conrad Irwin
2d99b327fc Don't wrap on paragraphs
For zed-industries/community#2116
2023-10-06 10:32:15 -06:00
Marshall Bowers
79ad5c08e4 Add profiling information for compute_layout 2023-10-06 10:48:25 -04:00
Antonio Scandurra
ca6eb5511c Checkpoint: underlines 2023-10-06 15:34:37 +02:00
Piotr Osiewicz
c46137e40d chore: Upgrade to Rust 1.73 (#3096)
Release Notes:
- N/A
2023-10-06 14:50:29 +02:00
Piotr Osiewicz
b391f5615b rust: Highlight async functions in completions (#3095)
Before (code in screenshot is from this branch,
`crates/zed/languages/rust.rs:179`):

![image](https://github.com/zed-industries/zed/assets/24362066/6b709f8c-1b80-4aaa-8ddc-8db9dbca5a5e)
Notice how the last 2 entries (that are async functions) are not
highlighted properly.
After:

![image](https://github.com/zed-industries/zed/assets/24362066/88337f43-b97f-4257-9c31-54c9023e8dbb)

This is slightly suboptimal, as it's hard to tell that this is an async
function - I guess adding an `async` prefix is not really an option, as
then we should have a prefix for non-async functions too. Still, at
least you can tell that something is a function in the first place. :)

Release Notes:
- Fixed Rust async functions not being highlighted in completions.
2023-10-06 14:43:03 +02:00
Nathan Sobo
65c7765c07 Checkpoint 2023-10-05 21:02:26 -06:00
Nathan Sobo
e99f6c03c1 Synchronize access when starting and stopping display links
Hoping this prevents panics we were observing when starting.
2023-10-05 20:46:26 -06:00
Mikayla
31062d424f make bundle script incremental when using debug or local builds 2023-10-05 16:56:44 -07:00
Max Brunsfeld
559433bed0 Fix panic when immediately closing a window while opening paths (#3092)
Fixes this panic that I've been seeing in Slack:


[example](https://zed-industries.slack.com/archives/C04S6T1T7TQ/p1696530575535779)


```
thread 'main' panicked at 'assertion failed: opened_items.len() == project_paths_to_open.len()'
crates/workspace/src/workspace.rs:3628
<backtrace::capture::Backtrace>::create
<backtrace::capture::Backtrace>::new
Zed::init_panic_hook::{closure#0}
std::panicking::rust_panic_with_hook
std::panicking::begin_panic_handler::{{closure}}
std::sys_common::backtrace::__rust_end_short_backtrace
_rust_begin_unwind
core::panicking::panic_fmt
core::panicking::panic
<workspace::Workspace>::new_local::{closure#0}::{closure#0}
```

I believe it was caused by a window being closed immediately, while it
was still loading some paths. There was a mismatch in expectation
between the `workspace::open_items` function (which contains this
assertion), and the `Workspace::load_workspace` method. That later
method can return an empty vector if the workspace handle is dropped
while it is executing.

Release Notes:

- Fixed a crash when closing a Zed window immediately after opening it
2023-10-05 16:28:23 -07:00
Max Brunsfeld
8fafae2cfa Fix panic when immediately closing a window while opening paths 2023-10-05 16:21:14 -07:00
Max Brunsfeld
b3c9473bc8 collab 0.23.2 2023-10-05 16:06:28 -07:00
Max Brunsfeld
b77c815bcd Fix bugs in handling mutual following (#3091)
This fixes some bugs in our following logic, due to our attempts to
prevent infinite loops when two people follow each other.

* Propagate all of leader's views to a new follower, even if those views
were originally created by that follower.
* Propagate active view changes to followers, even if the active view is
following that follower.
* Avoid redundant active view updates on the client.

Release Notes:

- Fixed bugs where it was impossible to follow someone into a view that
they previously following you into.
2023-10-05 15:16:58 -07:00
Conrad Irwin
13192fa03c Code to allow opening zed:/channel/1234
Refactored a bit how url arguments are handled to avoid adding too much
extra complexity to main.
2023-10-05 14:57:45 -07:00
Conrad Irwin
b258ee5f77 Fix ./script/bundle -l 2023-10-05 14:55:39 -07:00
Conrad Irwin
a63eccf188 Add url schemes to Zed 2023-10-05 14:55:39 -07:00
Mikayla Maki
37de4a9990 Add markdown parsing to channel chat (#3088)
TODO:
- [x] Add markdown rendering to channel chat
- [x] Unify (?) rendering logic between hover popover and chat
- [x] ~~Determine how to deal with document-oriented markdown like `#`~~
Unimportant until we want to do something special with `#channel`
- [x] Tidy up spacing and styles in chat panel

Release Notes:

- Added markdown rendering to channel chat
- Improved channel chat message style
- Fixed a bug where long chat messages would not soft wrap
2023-10-05 14:30:12 -07:00
Mikayla
c4870e1b6b re-unify markdown parsing between hover_popover and chat 2023-10-05 14:22:41 -07:00
Nathan Sobo
6f7c305308 Checkpoint 2023-10-05 14:42:29 -06:00
Max Brunsfeld
438dd42f7d Fix bugs in handling mutual following
* Propagate all of leader's views to a new follower, even if those views
  were originally created by that follower.
* Propagate active view changes to followers, even if the active view is
  following that follower.
* Avoid redundant active view updates on the client.
2023-10-05 13:28:46 -07:00
Mikayla
f57d563578 Improve chat rendering 2023-10-05 11:58:41 -07:00
Joseph T. Lyons
c8535440d3 Add session id (#3090)
Release Notes:

- N/A
2023-10-05 14:57:08 -04:00
Joseph T. Lyons
84ea34f918 Add session id 2023-10-05 14:50:48 -04:00
Mikayla
44ada52185 Fix bug where chat text wouldn't wrap to width 2023-10-05 11:06:29 -07:00
Conrad Irwin
78b1231386 Clear SelectionGoal on input (#3089)
Release Notes:

- `up` and `down` now go to the correct place after inserting
2023-10-05 09:57:36 -06:00
Antonio Scandurra
fe3ef08f39 Checkpoint! 2023-10-05 09:34:54 -06:00
Conrad Irwin
f1c743286d Clear SelectionGoal on input 2023-10-05 09:02:52 -06:00
Antonio Scandurra
657a25178d Checkpoint 2023-10-05 17:00:37 +02:00
Antonio Scandurra
f3560caf93 Checkpoint 2023-10-05 15:34:57 +02:00
Antonio Scandurra
2e056e9b0b WIP 2023-10-05 15:30:47 +02:00
Antonio Scandurra
92bda1231e Use content mask for quad as well 2023-10-05 12:11:28 +02:00
Antonio Scandurra
7643bd61fd Checkpoint 2023-10-05 10:59:50 +02:00
Antonio Scandurra
bf73b40529 Draw only once on next frame callbacks 2023-10-05 10:57:16 +02:00
Nathan Sobo
ed20397a2b Checkpoint 2023-10-05 00:13:17 -06:00
Nathan Sobo
1c70ca2214 Checkpoint 2023-10-05 00:08:45 -06:00
Nathan Sobo
77b9a7aa5a Checkpoint 2023-10-04 23:59:21 -06:00
Nathan Sobo
0d0c760d94 Checkpoint 2023-10-04 23:03:00 -06:00
Nathan Sobo
177e385bb9 Checkpoint: Fix a crash 2023-10-04 22:59:01 -06:00
Nathan Sobo
699a5d2944 Checkpoint 2023-10-04 20:35:24 -06:00
Mikayla
d298afba01 Create markdown text element and add to channel chat 2023-10-04 17:47:30 -07:00
Marshall Bowers
45d08c70f0 Add .when to Elements 2023-10-04 18:33:28 -04:00
Marshall Bowers
77feecc623 Add List component 2023-10-04 18:25:43 -04:00
Mikayla Maki
acffc7e7f0 Remove old code from notes icon click handler (#3085)
Release Notes:

- Fix clicking the notes icon when people are in the channel (preview
only)
2023-10-04 15:15:25 -07:00
Mikayla Maki
b0e56b7c54 107 channel touch ups (#3087)
Release Notes:

- Add user avatars to channel chat messages
- Group messages by sender
- Fix visual bugs in new chat and note buttons
2023-10-04 15:14:39 -07:00
Max Brunsfeld
df2fa87e6b collab 0.23.1 2023-10-04 15:12:17 -07:00
Max Brunsfeld
a27be35325 Ensure chat messages are retrieved in order of id (#3086)
Also, remove logic for implicitly marking chat messages as observed when
they are fetched. I think this is unnecessary, because the client always
explicitly acknowledges messages when they are shown.

Release Notes:

- Fixed a bug where chat messages were shown out of order (preview only)
2023-10-04 15:10:49 -07:00
Mikayla
2f3c3d510f Fix hit boxes and hover styles for new buttons
co-authored-by: conrad <conrad.irwin@gmail.com>
2023-10-04 14:44:50 -07:00
Max Brunsfeld
d09767a90b Ensure chat messages are retrieved in order of id 2023-10-04 14:43:53 -07:00
Conrad Irwin
427a857e9a Fix panic in increment (#3084)
Release Notes:

- Fixes a panic in vim when incrementing a non-number.
2023-10-04 15:39:24 -06:00
Conrad Irwin
e9842091e4 save tweaks (#3031)
- use SaveAll instead of Save
- TODO: fix where closing a multi-buffer gives a confusing save prompt
2023-10-04 15:38:07 -06:00
Marshall Bowers
332f3f5617 Merge branch 'gpui2' into marshall/gpui2-playground 2023-10-04 17:32:21 -04:00
Mikayla
73e78a2257 Adjust channel rendering to group related messages 2023-10-04 14:29:08 -07:00
Conrad Irwin
f7cd0e84f9 Remove old code from notes icon click handler 2023-10-04 15:18:26 -06:00
Conrad Irwin
a4e77af571 Fix panic in increment 2023-10-04 15:13:01 -06:00
Nathan Sobo
c8bc68c267 Checkpoint 2023-10-04 15:08:04 -06:00
Nathan Sobo
02d6b91b73 Checkpoint 2023-10-04 15:05:04 -06:00
Mikayla
5074bccae4 Add image avatars to channel messages 2023-10-04 14:04:02 -07:00
Conrad Irwin
7d94b0325f Fix renaming (#3083)
Release Notes:

- Fix bugs arising from saving an untitled buffer
2023-10-04 14:50:31 -06:00
Conrad Irwin
ff1722d307 Fix tracking newly saved buffers
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2023-10-04 14:44:21 -06:00
Nathan Sobo
e68b24f839 Checkpoint 2023-10-04 13:43:21 -06:00
Marshall Bowers
339ba7986f Add Avatar element 2023-10-04 15:34:14 -04:00
Joseph T. Lyons
6cb674a0aa collab 0.23.0 2023-10-04 15:01:38 -04:00
Joseph T. Lyons
6db47478cf v0.108.x dev 2023-10-04 15:00:53 -04:00
Mikayla Maki
01b45f4f23 Show when a channel resource changes (#3074)
This PR adds a mechanism for notifying the client when a channel note
has been edited or a message has been changed.

TODO: 
- [x] Fix infinite loop when opening the chat panel
- [x] Switch to client-side ack model of observation detection
- [x] Add client-side-only change detection (e.g. for when a channel
note is open locally but not focused)
- [x] Review implementation / query performance.
- [x] Fix lack of ACK on restart for channel buffers
- [x] remove channel note opening on click
- [x] Fix channel messages sent while chat channel is in the background
not showing the channel as changed.

Release Notes:

- N/A
2023-10-04 11:57:33 -07:00
Mikayla
4d61d01943 Add an RPC handler for channel buffer acks
co-authored-by: max <max@zed.dev>
2023-10-04 11:47:13 -07:00
Mikayla
dd0edcd203 Changed the on-click behavior of joining a channel to not open the chat, and only open 1 project instead of all projects
Co-authored-by: conrad <conrad.irwin@gmail.com>
Co-authored-by: max <max@zed.dev>
2023-10-04 11:46:08 -07:00
Antonio Scandurra
ebc80597d5 WIP 2023-10-04 20:09:55 +02:00
Antonio Scandurra
d28c81571c Checkpoint 2023-10-04 19:59:24 +02:00
Antonio Scandurra
dc9a260425 Checkpoint 2023-10-04 19:53:29 +02:00
Marshall Bowers
249e6fe637 Add Icon element 2023-10-04 13:46:45 -04:00
Marshall Bowers
e84b8747a1 Add storybook CLI 2023-10-04 13:33:28 -04:00
Mikayla
e548572f12 Fix channel messages test 2023-10-04 10:13:02 -07:00
Marshall Bowers
0323a60d85 Remove unneeded theme function
This also fixes the panic when trying to declare the `GPUIApplication` class.
2023-10-04 13:11:02 -04:00
Marshall Bowers
a05cbf8169 Begin setting up stories 2023-10-04 12:49:06 -04:00
Antonio Scandurra
5aa45607eb Checkpoint 2023-10-04 18:38:08 +02:00
Antonio Scandurra
133c3a330c Checkpoint 2023-10-04 17:59:29 +02:00
Antonio Scandurra
f9646208e9 Checkpoint 2023-10-04 17:48:28 +02:00
Marshall Bowers
4b793f44ef Wire up hacky children for Panel 2023-10-04 11:22:33 -04:00
Marshall Bowers
aae4f00a4b Render Panel 2023-10-04 10:58:23 -04:00
Marshall Bowers
366a4918c3 Fix icon paths 2023-10-04 10:52:39 -04:00
Antonio Scandurra
bc1801fb03 Checkpoint 2023-10-04 16:42:28 +02:00
Marshall Bowers
25cd12cf33 Fix icon name 2023-10-04 10:41:21 -04:00
Marshall Bowers
90e22da930 Render workspace 2023-10-04 10:37:29 -04:00
Marshall Bowers
e6c7e57711 Merge branch 'gpui2' into marshall/gpui2-playground 2023-10-04 10:33:40 -04:00
Antonio Scandurra
d385bc9cce Allow tinting images grayscale 2023-10-04 15:27:51 +02:00
Antonio Scandurra
1816ab95a0 Checkpoint: start rendering images 2023-10-04 15:03:21 +02:00
Antonio Scandurra
5c750b6880 Checkpoint: emojis rendering 2023-10-04 12:41:21 +02:00
Antonio Scandurra
cd1c137542 WIP 2023-10-04 11:53:20 +02:00
Antonio Scandurra
4cf2ba20c2 Checkpoint: render SVGs 2023-10-04 10:51:47 +02:00
Antonio Scandurra
a1ee2db6d1 Use Courier for now, to avoid panicking 2023-10-04 08:48:05 +02:00
Mikayla
db8096ccdc Fix most tests for new chat changes 2023-10-03 20:50:17 -07:00
Nathan Sobo
25a2554bdd Checkpoint 2023-10-03 21:23:32 -06:00
Mikayla
3bc7024f8b Fix unit test
co-authored-by: Conrad <conrad.irwin@gmail.com>
2023-10-03 20:03:57 -07:00
Mikayla
4ff80a7074 Fix a few mouse event id bugs and move facepile to the left
co-authored-by: conrad <conrad.irwin@gmail.com>
2023-10-03 19:45:33 -07:00
Mikayla
23ee8211c7 Lower frequency of popup warning when leaving a call
co-authored-by: conrad <conrad.irwin@gmail.com>
2023-10-03 19:30:05 -07:00
Nathan Sobo
1e0ff65337 Checkpoint 2023-10-03 20:19:59 -06:00
Nathan Sobo
da211bef96 Checkpoint 2023-10-03 20:04:17 -06:00
Max Brunsfeld
95342c8c33 Merge branch 'main' into channel-changes 2023-10-03 17:52:28 -07:00
Max Brunsfeld
61e0289014 Acknowledge channel notes and chat changes when views are active
Co-authored-by: Mikayla <mikayla@zed.dev>
2023-10-03 17:40:10 -07:00
Max Brunsfeld
af09861f5c Specify uuid crate in the root Cargo.toml
Co-authored-by: Mikayla <mikayla@zed.dev>
2023-10-03 17:39:24 -07:00
Nathan Sobo
7f9e3bc787 Checkpoint 2023-10-03 17:58:11 -06:00
Nathan Sobo
d995192dde Checkpoint: Get basic workspace rendering 2023-10-03 17:39:03 -06:00
Nathan Sobo
c57e19c8fa Checkpoint: Glyphs rendering how I'd like 2023-10-03 17:29:36 -06:00
Nathan Sobo
550d9a9f71 Checkpoint 2023-10-03 16:17:25 -06:00
Nathan Sobo
4208ac2958 WIP 2023-10-03 15:17:45 -06:00
Nathan Sobo
45429b5400 WIP 2023-10-03 14:25:29 -06:00
Nathan Sobo
d3916b84c9 Checkpoint 2023-10-03 13:55:53 -06:00
Conrad Irwin
55d2b9b3c9 join channels (#3082)
Release Notes:

- Clicking on a channel in the sidebar will now join the channel and
open the notes
- If you join a channel that already shared projects, you will join the
projects automatically and follow the host.
- Clicking on the current channel in the sidebar will re-open the notes.
- Chat can now be accessed from the right click menu of channels.


- (probably not worth mentioning) Various improvements to hover states
and tooltips in the collab ui; and if you click on a channel while in
another call, confirm before switching.
2023-10-03 13:54:11 -06:00
Nathan Sobo
3b27d41c72 Checkpoint 2023-10-03 13:52:10 -06:00
Conrad Irwin
044fb9e2f5 Confirm on switching channels 2023-10-03 13:45:48 -06:00
Mikayla
6007c8705c Upgrade SeaORM to latest version, also upgrade sqlite bindings, rustqlite, and remove SeaQuery
co-authored-by: Max <max@zed.dev>
2023-10-03 12:16:53 -07:00
Nathan Sobo
a8c1958c75 Checkpoint 2023-10-03 13:03:29 -06:00
Conrad Irwin
d696b394c4 Tooltips for contacts 2023-10-03 12:54:39 -06:00
Mikayla
32c4138758 Added db message and edit operation observation
Co-authored-by: Max <max@zed.dev>
2023-10-03 11:39:59 -07:00
Conrad Irwin
d8bfe77a3b Scroll so that collab panel is in good state for calls 2023-10-03 12:00:02 -06:00
Joseph T. Lyons
8b0969b698 Update cpu and memory event code (#3081)
Release Notes:

- N/A
2023-10-03 13:36:35 -04:00
Conrad Irwin
66dfa47c66 Update collab ui to join channels again 2023-10-03 11:36:01 -06:00
Joseph T. Lyons
b10255a6dd Update cpu and memory event code
Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>
2023-10-03 13:29:08 -04:00
Nathan Sobo
3698e89b88 Checkpoint 2023-10-03 11:16:42 -06:00
Antonio Scandurra
bfa211fb02 Checkpoint 2023-10-03 18:04:45 +02:00
Antonio Scandurra
dc40ac854a WIP 2023-10-03 17:53:44 +02:00
Antonio Scandurra
2b6d041cb6 Checkpoint 2023-10-03 17:36:12 +02:00
Antonio Scandurra
8a58733d91 Checkpoint 2023-10-03 16:53:49 +02:00
Antonio Scandurra
e49b411205 Checkpoint 2023-10-03 16:30:41 +02:00
Antonio Scandurra
08464ee26e Checkpoint 2023-10-03 15:23:49 +02:00
Antonio Scandurra
12ba10bc2c Checkpoint 2023-10-03 14:48:08 +02:00
Nathan Sobo
dcaf4c905f Checkpoint 2023-10-03 05:57:15 -06:00
Nathan Sobo
6046ed4f5c Checkpoint 2023-10-03 05:51:59 -06:00
Kyle Caverly
cf5d89d13c Leverage embeddings query to collapse syntax nodes if not selected (#3067)
Reverts zed-industries/zed#3049
2023-10-03 12:02:47 +03:00
KCaverly
9f160537ef move collapsed only matches outside item parent in embedding.scm 2023-10-03 11:56:45 +03:00
Conrad Irwin
18e7305b6d Change channel join behavior
- Clicking on a channel name now joins the channel if you are not in it
- (or opens the notes if you are already there).
- When joining a channel, previously shared projects are opened
  automatically.
- If there are no previously shared projects, the notes are opened.
2023-10-02 23:20:06 -06:00
Conrad Irwin
d9813a5bec show host in titlebar (#3072)
Release Notes:

- show host in the titlebar of shared projects
- clicking on faces in the titlebar will now always follow the person
(it used to toggle)
- clicking on someone in the channel panel will follow that person
- highlight the currently open project in the channel panel

- fixes a bug where sometimes following between workspaces would not
work
2023-10-02 21:02:02 -06:00
Conrad Irwin
d7867cd1e2 Add/fix mouse interactions in current call sidebar 2023-10-02 19:38:45 -06:00
Marshall Bowers
30afc8b1d2 WIP: Panel 2023-10-02 20:16:55 -04:00
Mikayla
32b4b4d24d Add message and operation ACK messages to protos 2023-10-02 17:10:03 -07:00
Joseph T. Lyons
7d32a717af Add memory and cpu events (#3080)
Release Notes:

- N/A
2023-10-02 19:42:49 -04:00
Joseph T. Lyons
892350fa2d Add memory and cpu events
Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>
2023-10-02 19:35:31 -04:00
Max Brunsfeld
0db4b29452 Avoid N+1 query for channels with new messages
Co-authored-by: Mikayla <mikayla@zed.dev>
2023-10-02 16:22:28 -07:00
Marshall Bowers
74ac6eb8a3 Begin building out new ui crate in storybook2 2023-10-02 18:59:44 -04:00
Max Brunsfeld
d9d997b218 Avoid N+1 query for channels with notes changes
Also, start work on new timing for recording observed notes edits.

Co-authored-by: Mikayla <mikayla@zed.dev>
2023-10-02 15:58:34 -07:00
Max Brunsfeld
84c4db13fb Avoid spurious notifies in chat channel select
Co-authored-by: Mikayla <mikayla@zed.dev>
2023-10-02 15:57:59 -07:00
Conrad Irwin
528fa5c57b Refactor to remove toggle_follow 2023-10-02 16:51:02 -06:00
Marshall Bowers
9a9a35bf40 Move Sized bound up onto StyleHelpers 2023-10-02 18:46:25 -04:00
Marshall Bowers
d14dc35efe Merge branch 'gpui2' into marshall/gpui2-playground 2023-10-02 18:36:22 -04:00
Conrad Irwin
27d784b23e Fix bug in following
Prior to this change you could only follow across workspaces when you
were heading to the first window.
2023-10-02 16:29:42 -06:00
Marshall Bowers
9e1f7c4c18 Mainline GPUI2 UI work (#3079)
This PR mainlines the current state of new GPUI2-based UI from the
`gpui2-ui` branch.

Release Notes:

- N/A

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Nate <nate@zed.dev>
2023-10-02 18:20:47 -04:00
Marshall Bowers
77e67c19fe Fix deadlock when obtaining the font ID 2023-10-02 16:10:41 -04:00
Nathan Sobo
91582257fb WIP 2023-10-02 14:02:28 -06:00
Nathan Sobo
66ef5549e9 Checkpoint 2023-10-02 13:34:07 -06:00
Nathan Sobo
79e1e1a747 Checkpoint 2023-10-02 13:16:10 -06:00
Nathan Sobo
0b13c0a437 Checkpoint 2023-10-02 12:47:45 -06:00
Julia
08361eb84e Detach completion confirmation task when selecting with mouse (#3078)
Otherwise the spawn to resolve the additional edits never runs causing
autocomplete to never add imports automatically when clicking with the
mouse

Release Notes:

- Fixed auto-complete additional edits, such as auto-import, not
applying when selecting a completion with a mouse click.
2023-10-02 13:32:06 -04:00
Julia
3d68fcad0b Detach completion confirmation task when selecting with mouse
Otherwise the spawn to resolve the additional edits never runs causing
autocomplete to never add imports automatically when clicking with the
mouse
2023-10-02 13:18:49 -04:00
Conrad Irwin
7f44083a96 Remove unused function 2023-10-02 11:03:55 -06:00
Conrad Irwin
39af2bb0a4 Ensure notifications are dismissed
Before this change if you joined a project without clicking on the
notification it would never disappear.

Fix a related bug where if you have more than one monitor, the
notification was only dismissed from one of them.
2023-10-02 11:01:21 -06:00
Conrad Irwin
9dc292772a Add a screen for gpui tests
Allows me to test notifications
2023-10-02 09:53:30 -06:00
Antonio Scandurra
bf5d9e3224 Sort matches before processing them 2023-10-02 17:50:52 +02:00
Antonio Scandurra
d70014cfd0 Summarize file in the background 2023-10-02 15:36:10 +02:00
Piotr Osiewicz
a785eb9141 auto-update: Link to the current release's changelog, not the latest one (#3076)
An user complained in zed-industries/community#2093 that we always link
to the latest release changelog, not the one that they've just updated
to.


Release Notes:
- Fixed changelog link in update notification always leading to the
latest release changelog, not the one that was updated to. Fixes
zed-industries/community#2093.
2023-10-02 15:24:09 +02:00
Antonio Scandurra
f52200a340 Prevent deploying the inline assistant when selection spans multiple excerpts 2023-10-02 15:21:58 +02:00
Antonio Scandurra
df7ac9b815 💄 2023-10-02 14:36:16 +02:00
Antonio Scandurra
64a55681e6 Summarize the contents of a file using the embedding query 2023-10-02 14:32:13 +02:00
Mikayla
1d5b665f13 Implement channel changes for messages 2023-10-01 22:32:11 -07:00
Mikayla
51cf6a5ff3 Add database implementation of channel message change tracking 2023-10-01 22:32:11 -07:00
Mikayla
e0ff7ba180 Add channel note indicator and clear changed status 2023-10-01 22:32:10 -07:00
Mikayla
9ba975d6ad Channel notifications from the server works 2023-10-01 22:30:21 -07:00
Mikayla
1469c02998 Add observed_channel_notes table and implement note diffing 2023-10-01 22:26:27 -07:00
Joseph T. Lyons
95e09dd2e9 Add Nushell support to venv activation (#3073)
This PR adds an option to run `activate.nu` in the automatic venv
activation code (relevant comment
[here](https://github.com/zed-industries/community/issues/2103#issuecomment-1742355651))

Release Notes:

- Added a `nushell` option to the
`terminal.detect_venv.on.activate_script` setting
([2103](https://github.com/zed-industries/community/issues/2103)).
2023-10-01 23:55:57 -04:00
Joseph T. Lyons
e5e63ed201 Add Nushell support to venv activation 2023-10-01 23:38:30 -04:00
Nathan Sobo
4212a45767 WIP 2023-09-30 10:01:59 -06:00
Nathan Sobo
ef01a64826 Fix infinite loop 2023-09-30 09:43:52 -06:00
Marshall Bowers
46b4118b9e Checkpoint: Things are running, but with a stack overflow 2023-09-29 22:57:17 -04:00
Marshall Bowers
c7fc5f3ab7 Checkpoint: Fix downcasting 2023-09-29 22:53:24 -04:00
Marshall Bowers
f50a23accd Adjust window dimensions
Since resizing freezes the window.
2023-09-29 21:55:34 -04:00
Marshall Bowers
43a1296150 Checkpoint: Storybook window showing 2023-09-29 21:51:27 -04:00
Marshall Bowers
3b38641f98 Fix stack overflow by removing Deref and DerefMut impls 2023-09-29 21:21:08 -04:00
Marshall Bowers
8cac89d17c Checkpoint: Compiling 2023-09-29 20:51:52 -04:00
Conrad Irwin
92bb9a5fdc Make following more good
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-09-29 17:59:19 -06:00
Marshall Bowers
963f179d7f Checkpoint: Cast through std::mem::transmute 2023-09-29 19:41:31 -04:00
Marshall Bowers
103183f494 WIP: Parameterize over thread 2023-09-29 19:20:18 -04:00
Conrad Irwin
1cfc2f0c07 Show host in titlebar
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-09-29 16:37:00 -06:00
Conrad Irwin
219715449d More logging on collab by default 2023-09-29 16:37:00 -06:00
Max Brunsfeld
f011a3df52 Allow following participants who aren't in the same project (#2993)
The goal of this PR is to make Following more intuitive.

### Old Behavior

Previously, following was scoped to a project. In order to follow
someone in a given window, the window needed to contain a shared
project, and the leader needed to be present in the project. Otherwise,
following failed.

### New Behavior

* You can always follow **any** participant in the current call, in any
pane of any window.
* When following someone in a project that you're both collaborating in,
it works the same as before.
* When following someone in an unshared project, or a project that they
don't have open, you'll only get updates about the leader's views that
don't belong to a project, such as channel notes views. When the leader
focuses a file in a different project, you'll get the "follow $LEADER to
their active project" indicator

### Todo

* [x] Change db schema and RPC protocol so a project id isn't required
for following
* [x] Change client to allow following into non-project items regardless
of the leader's project
* [x] Assign colors to users in a way that doesn't require users to be
in a shared project.
2023-09-29 15:18:05 -07:00
Max Brunsfeld
7adaa2046d Show current user as follower when following in unshared projects 2023-09-29 15:08:25 -07:00
Max Brunsfeld
948871969f Fix active view update when center pane is not focused 2023-09-29 14:37:28 -07:00
Mikayla Maki
57707a80e6 Refactor elixir LSP settings (#3071)
This PR is a bit of a last minute change, but I realized there was
actually a third player in the Elixir LSP space who wants support as
well,
[lexical](https://github.com/zed-industries/community/issues/1567). I
realized that the settings arrangement I shipped in this preview
precludes adding a third kind of LSP. I don't have the time to learn how
to fully support this LSP, but I thought I'd at least refactor how the
settings are represented before this hits stable.

Release Notes:

- Changed the new `"elixir": { "next": "on" }` setting to `"elixir": {
"lsp": "next_ls" }`. The `elixir.lsp` setting requires a full restart to
take effect. (Preview only)
2023-09-29 14:25:05 -07:00
Max Brunsfeld
55da5bc25d Switch .leader_replica_id -> .leader_peer_id 2023-09-29 14:16:38 -07:00
Max Brunsfeld
c718b810f6 Merge branch 'main' into allow-following-outside-of-projects 2023-09-29 14:15:33 -07:00
Max Brunsfeld
afd293ee87 Update active view when activating a window 2023-09-29 14:12:51 -07:00
Mikayla
752bc5dcdd Refactor elixir LSP settings 2023-09-29 14:12:50 -07:00
Max Brunsfeld
973f03e73e Fix bug in follower updates for non-project items 2023-09-29 14:09:14 -07:00
Max Brunsfeld
555c9847d4 Add ZED_ALWAYS_ACTIVE env var, use it in local collaboration script
This makes zed always behave as if the app is active, even if no window is focused.
It prevents the 'viewing a window outside of zed' state during collaboration.
2023-09-29 13:43:43 -07:00
Nathan Sobo
c1a35a29a8 WIP 2023-09-29 14:34:40 -06:00
Nathan Sobo
7a6c27cf24 WIP 2023-09-29 14:04:58 -06:00
Conrad Irwin
d9c1cf9874 vim: Fix accidental visual selection when following (#3068)
Release Notes:

- vim: Fix a bug where following could put you in visual mode
2023-09-29 13:59:59 -06:00
Mikayla Maki
1155f1b0e1 Add support for the TextDocumentSyncKind LSP option (#3070)
fixes https://github.com/zed-industries/community/issues/2098

Release Notes:

- Fixed a bug in Zed's LSP implementation when using Next LS.
2023-09-29 12:25:37 -07:00
Nathan Sobo
dcc314f088 Checkpoint 2023-09-29 13:22:53 -06:00
Mikayla
31ff5bffd6 Fix tests relying on off-spec behavior 2023-09-29 12:19:58 -07:00
Mikayla
4887ea3563 Add support for the TextDocumentSyncKind LSP options 2023-09-29 12:05:21 -07:00
Kyle Caverly
dbaaf4216d add scheme for full parseable files in semantic index (#3069)
add scheme as a parseable file type in semantic index.
Each file will operate as a single embedding, in which no real scheme
syntax or tree-sitter level data is stored.

Release Notes:

- Added scheme to Semantic Index
2023-09-29 14:42:15 -04:00
Antonio Scandurra
53c25690f9 WIP: Use a different approach to codegen outline 2023-09-29 20:37:07 +02:00
KCaverly
3c12e711a4 add scheme for full parseable files in semantic index 2023-09-29 14:35:02 -04:00
Conrad Irwin
9b7bd4e9ae vim: Fix accidental visual selection when following 2023-09-29 12:08:25 -06:00
Max Brunsfeld
026b3a1d0f Remove uneeded Workspace::project_remote_id_changed method 2023-09-29 08:54:23 -07:00
Antonio Scandurra
d9c08de58a Revert "Revert "leverage file outline and selection as opposed to entire file"" 2023-09-29 17:15:26 +02:00
Marshall Bowers
c379a6f2fb ui: Fix glyph used for option key in Keybinding (#3066)
This PR fixes the glyph used for the option key in the new `Keybinding`
component.

Same fix as in #3065, but applied to the new `Keybinding` component so
that we don't regress when switching to GPUI2.

<img width="750" alt="Screenshot 2023-09-29 at 10 50 15 AM"
src="https://github.com/zed-industries/zed/assets/1486634/8c6147e9-fa05-4804-954c-b8e3b98cbdf0">

Release Notes:

- N/A
2023-09-29 11:02:35 -04:00
Piotr Osiewicz
488a3eeace ui: Mirror option key in keybindings (#3065)
![image](https://github.com/zed-industries/zed/assets/24362066/94731737-a21a-4cef-a445-eb855f1a4d3e)

![image](https://github.com/zed-industries/zed/assets/24362066/e879ec9a-70aa-4989-923f-4cca18d01587)

Release Notes:

- Fixed option key's appearance in keybindings
2023-09-29 16:45:49 +02:00
Antonio Scandurra
4dd9c9e2b9 Introduce the ability to include or exclude warnings from project diagnostics (#3056)
![CleanShot 2023-09-27 at 18 09
37](https://github.com/zed-industries/zed/assets/482957/317d31e4-81f8-44d8-b94f-8ca7150d3fd2)

Release Notes:

- Added the ability to exclude warnings from project diagnostics. By
default, they will be on but they can be disabled temporarily by
clicking on the warnings icon. The default behavior can be changed by
changing the new `diagnostics.include_warnings` setting.
2023-09-29 13:13:04 +01:00
Max Brunsfeld
ca0a4bdf8e Introduce a WorkspaceStore for handling following 2023-09-28 18:58:52 -07:00
Marshall Bowers
247c7eff14 storybook: Fix kitchen sink story (#3064)
This PR fixes the kitchen sink story in the storybook.

Included are some additional changes that make it so the kitchen sink is
automatically populated by all of the defined stories.

Release Notes:

- N/A
2023-09-28 21:22:50 -04:00
Max Brunsfeld
837ec5a27c Remove stray file 2023-09-28 17:14:53 -07:00
Max Brunsfeld
5a15692589 🎨 Workspace::leader_updated 2023-09-28 17:13:10 -07:00
Max Brunsfeld
0058702749 Remove unused db query method 2023-09-28 17:13:10 -07:00
Max Brunsfeld
e34ebbc665 Remove unused dependencies on theme 2023-09-28 17:13:10 -07:00
Max Brunsfeld
38a9e6fde1 Fix removal of followers on Unfollow 2023-09-28 16:46:43 -07:00
Marshall Bowers
f26ca0866c Mainline GPUI2 UI work (#3062)
This PR mainlines the current state of new GPUI2-based UI from the
`gpui2-ui` branch.

Release Notes:

- N/A

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Nate <nate@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
2023-09-28 19:36:21 -04:00
Conrad Irwin
e7ee8a95f6 vim: Fix some dw edge cases (#3058)
Release Notes:

- vim: Fix `dw` on the last word of a line, and on empty lines.
2023-09-28 15:32:29 -06:00
Conrad Irwin
91adefedfa vim keybinding updates (#3057)
Release Notes:

- vim: Add ctrl-i to go forward
([#1732](https://github.com/zed-industries/community/issues/1732)).
ctrl-o was already supported.
- vim: Add `g <space>` to open the current snippet in its own file.
- vim: Escape will now return to normal mode even if completion menus
are open (use `ctrl-x ctrl-z` to hide menus, as in vim).
- vim: Add key bindings for Zed's various completion mechanisms:
- - `ctrl-x ctrl-o` to open the completion menu,
- -  `ctrl-x ctrl-l` to open the LSP action menu,
- - `ctrl-x ctrl-c` to trigger Copilot (requires configuring copilot),
- - `ctrl-x ctrl-a` to trigger the inline Assistant (requires
configuring openAI),

NOTE: we should add these to the docs before shipping 0.107 to stable.
2023-09-28 15:32:21 -06:00
Conrad Irwin
2f5eaa8475 vim increment (#3054)
- vim: add ctrl-a/ctrl-x for increment/decrement
2023-09-28 15:32:11 -06:00
Joseph T. Lyons
da964fae93 Enable semantic_index by default (#3061)
Release Notes:

- Enabled the `semantic_index` setting by default.
2023-09-28 17:24:00 -04:00
Max Brunsfeld
e9c1ad6acd Undo making project optional on stored follower states
Following works without a project, but following in unshared projects does
not need to be replicated to other participants.
2023-09-28 14:21:44 -07:00
Joseph T. Lyons
f965ee9b1b Enable semantic_index by default 2023-09-28 17:17:26 -04:00
Max Brunsfeld
ce940da8e9 Fix errors from assuming all room_participant rows had a non-null participant_index
Rows representing pending participants have a null participant_index.

Co-authored-by: Conrad <conrad@zed.dev>
2023-09-28 12:03:53 -07:00
Max Brunsfeld
a8b35eb8f5 Merge branch 'main' into allow-following-outside-of-projects 2023-09-28 11:58:28 -07:00
Max Brunsfeld
0c95e5a6ca Fix coloring of local selections when following
Co-authored-by: Conrad <conrad@zed.dev>
2023-09-28 11:37:47 -07:00
Max Brunsfeld
0f39b63801 Rename color_index to participant_index
Co-authored-by: Conrad <conrad@zed.dev>
2023-09-28 11:37:22 -07:00
Max Brunsfeld
545b5e0161 Assign unique color indices to room participants, use those instead of replica_ids
Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Antonio <antonio@zed.dev>
2023-09-28 11:06:09 -07:00
Joseph T. Lyons
3cf7164a54 Fix text transformation commands for multiple line, single selection cases (#3060)
If you highlight the following block of text (with a single selection):

```
The quick brown
fox jumps over
the lazy dog
```

and run `editor: convert to upper camel case`, you'll get:

```
TheQuickBrown
foxJumpsOver
theLazyDog
```

instead of:

```
TheQuickBrown
FoxJumpsOver
TheLazyDog
```

The same thing happens for `editor: convert to title case`. This happens
because [`to_case` crate](https://crates.io/crates/convert_case) doesn't
allow the user to define '\n' as a boundary. I wanted to fix this at the
lib level, so I filled [an
issue](https://github.com/rutrum/convert-case/issues/16) but I never
heard back. What's strange is VS Code and Sublime I think both exhibit
the same output as we do currently, but I don't personally think this
feels right (happy to hear opposing opinions). I'm just doing the naive
thing to hack around this limitation of the `to_case` crate.

I did some testing and it seems I only need to adjust `editor: convert
to title case` and `editor: convert to upper camel case`. The way the
other transformations are implemented in `to_case` don't seem to have
this issue.

Release Notes:

- Fixed a bug where running certain text transfomration commands on a
single selection covering multiple lines would not transform all
selected lines as expected.
2023-09-28 14:04:17 -04:00
Nathan Sobo
3f50779a17 Checkpoint 2023-09-28 12:02:52 -06:00
Kirill Bulatov
a8188a2f33 Improve file finder ergonomics (#3059)
Deals with https://github.com/zed-industries/community/issues/2086
Part of https://github.com/zed-industries/community/issues/351

Initial:
<img width="585" alt="Screenshot 2023-09-28 at 09 50 05"
src="https://github.com/zed-industries/zed/assets/2690773/e0149312-dfe3-4b7c-948c-0f593d6f540c">
First query letter input (only two history items match that, both are
preserved on top, with their order preserved also)
<img width="603" alt="Screenshot 2023-09-28 at 09 50 08"
src="https://github.com/zed-industries/zed/assets/2690773/85ab2f4c-bb9c-4811-b8b0-b5c14a370ae2">
Second query letter input, no matching history items:
<img width="614" alt="Screenshot 2023-09-28 at 09 50 11"
src="https://github.com/zed-industries/zed/assets/2690773/6d380403-a43c-4f00-a05b-88f43f91fefb">
Remove second query letter, history items match again and pop to the
top:
<img width="574" alt="Screenshot 2023-09-28 at 09 50 15"
src="https://github.com/zed-industries/zed/assets/2690773/5981ca53-6bc8-4305-ae36-27144080e1a2">


* allows `file_finder::Toggle` (cmd-p by default) to cycle through file
finder items (ESC closes the modal still)
* on query typing, preserve history items that match the query and keep
them on top, with their ordering preserved
* show history items' matched letters

Release Notes:

- Improve file finder ergonomics: allow cycle through items with the
toggle action, preserve matching history items on query input
2023-09-28 19:53:09 +03:00
Kirill Bulatov
d30385f07c Show path matches inside history items matching the query 2023-09-28 09:49:25 -07:00
Kirill Bulatov
1b5ff68c43 Show matching search history whenever possible 2023-09-28 09:34:20 -07:00
Kirill Bulatov
97eabe6f81 Add tests 2023-09-28 09:00:25 -07:00
Kirill Bulatov
57a95d1799 Preserve matching history items and their order 2023-09-28 06:55:49 -07:00
Kirill Bulatov
541dd994a9 Cycle file finder entries on cmd-p 2023-09-28 06:55:49 -07:00
Piotr Osiewicz
81a107f503 assets: Add keybinds to replace (#3055)
Release Notes:

- N/A
2023-09-28 13:04:14 +02:00
Nathan Sobo
5ab1034698 Checkpoint 2023-09-28 01:16:47 -06:00
Nathan Sobo
13ba450c4c Checkpoint 2023-09-28 00:46:15 -06:00
Nathan Sobo
c5470d4050 Checkpoint 2023-09-27 23:24:07 -06:00
Nathan Sobo
7e49c7d782 Checkpoint 2023-09-27 23:19:32 -06:00
Nathan Sobo
769a04517f Checkpoint - No warnings 2023-09-27 23:10:22 -06:00
Conrad Irwin
768c991909 vim: Fix some dw edge cases 2023-09-27 23:09:09 -06:00
Nathan Sobo
1ee70a0146 Checkpoint 2023-09-27 23:05:39 -06:00
Nathan Sobo
8be8047b8d Checkpoint 2023-09-27 22:02:48 -06:00
Conrad Irwin
51b24bbaf3 Add vim-style completion bindings: 2023-09-27 21:29:18 -06:00
Conrad Irwin
2cb320e246 Escape returns to normal mode even if completion is open
For zed-industries/community#1746
2023-09-27 21:28:30 -06:00
Nathan Sobo
7524f7fbe8 Checkpoint 2023-09-27 21:25:06 -06:00
Nathan Sobo
9fefb1d898 Checkpoint 2023-09-27 21:14:09 -06:00
Conrad Irwin
73fc1c1c56 Add g space for option-enter
vimify all the things
2023-09-27 21:05:58 -06:00
Conrad Irwin
d1baff1743 Add ctrl-i to go forward
For zed-industries/community#1732
2023-09-27 21:04:13 -06:00
Conrad Irwin
dd1cf5c3cf vim: add ctrl-a/ctrl-x
For zed-industries/community#1411
For zed-industries/community#619
2023-09-27 19:49:31 -06:00
Conrad Irwin
9246c11c35 Don't prompt to save unchanged files (#3053)
- don't prompt to save a set of unchanged files when closing
(preview-only)
2023-09-27 19:20:08 -06:00
Antonio Scandurra
0e6002dca2 Fix tests 2023-09-27 18:19:35 -06:00
Antonio Scandurra
78908bc5cb Introduce a new include_warnings setting under diagnostics 2023-09-27 18:08:08 -06:00
Nathan Sobo
49672bfc5f Checkpoint 2023-09-27 17:51:12 -06:00
Antonio Scandurra
f603d682cd Add an include/exclude warnings toggle in project diagnostics 2023-09-27 17:47:19 -06:00
Nathan Sobo
b364d404a9 Checkpoint 2023-09-27 17:25:04 -06:00
Nathan Sobo
96f9c67e77 Checkpoint 2023-09-27 17:17:30 -06:00
Conrad Irwin
6cebcac805 fix tests 2023-09-27 17:02:47 -06:00
Conrad Irwin
3573896fe0 Don't prompt to save unchanged files 2023-09-27 16:07:35 -06:00
Nathan Sobo
e9a84a21e4 Checkpoint 2023-09-27 15:35:51 -06:00
Conrad Irwin
25429f760c ctrl-a/x for vim 2023-09-27 12:32:01 -06:00
Joseph T. Lyons
ece4875973 v0.107.x dev 2023-09-27 12:26:48 -04:00
Kyle Caverly
2c0547079a Revert "leverage file outline and selection as opposed to entire file" (#3049)
Reverts zed-industries/zed#3040
2023-09-27 12:21:11 -04:00
Kyle Caverly
b3b3a56164 Revert "leverage file outline and selection as opposed to entire file" 2023-09-27 12:21:03 -04:00
Kyle Caverly
4242b45646 Revert "removed stale dbg in assistant from main" (#3048)
Reverts zed-industries/zed#3046
2023-09-27 12:19:54 -04:00
Kyle Caverly
cab80cbe9d Revert "removed stale dbg in assistant from main" 2023-09-27 12:19:44 -04:00
Julia
d671a8a21d Bump update notification size back up (#3047)
Regressed:
<img width="422" alt="CleanShot 2023-09-27 at 11 07 37@2x"
src="https://github.com/zed-industries/zed/assets/30666851/636d7bec-4518-45e6-87bd-84b45dda28e1">

Fixed:
<img width="424" alt="CleanShot 2023-09-27 at 11 04 13@2x"
src="https://github.com/zed-industries/zed/assets/30666851/186a1d49-4daf-4211-891a-dacfd1144311">

Release Notes:

- N/A
2023-09-27 11:25:16 -04:00
Julia
6b88ac9c32 Bump update notification size back up 2023-09-27 11:04:25 -04:00
Piotr Osiewicz
6ccaf55e54 search: Reorder items in search bar (#3039)
Release Notes:

- Reordered items in project and buffer search bar
2023-09-27 16:51:20 +02:00
Kyle Caverly
edf29aa67d implement new search strategy (#3029)
Augment current search strategy in semantic search, reducing search
times by ~60%

Release Notes:

- Implemented minimum batch sizes for concurrent database reads.
- Batch embedding matrix multiplication.
- Calculate matmul with ndarray
2023-09-27 10:37:48 -04:00
KCaverly
0e6fd645fd leverage embeddings len returned in construction matrix multiplication 2023-09-27 10:33:04 -04:00
Conrad Irwin
c63cc78ffd vim: Fix ctrl-u/ctrl-d (#3044)
- vim: Fix ctrl-d/ctrl-u to match vim (when :set scrolloff=3)
2023-09-27 07:48:50 -06:00
KCaverly
3682751455 Merge branch 'main' of github.com:zed-industries/zed into faster_semantic_search 2023-09-27 09:43:39 -04:00
KCaverly
abefa2738b removed blas and increase batch size for vector search 2023-09-27 09:43:23 -04:00
Kyle Caverly
4ccd69350b removed stale dbg in assistant from main (#3046)
remove small dbg! statement in main
2023-09-27 09:13:41 -04:00
KCaverly
0d6880adb3 removed stale dbg in assistant from main 2023-09-27 09:13:00 -04:00
Kyle Caverly
2f368de397 leverage file outline and selection as opposed to entire file (#3040)
Transition generate prompt for inline assist to leverage outline as
opposed to full file.
This enables, us to leverage the inline assist for large files.

Release Notes:

- Change inline assist to use tree-sitter based outlines for code
generation instead of full files
2023-09-27 09:10:18 -04:00
KCaverly
650a160f04 update test outline for prompt tests for new cursor span 2023-09-27 09:06:53 -04:00
Piotr Osiewicz
ecb037fc0e language: Add block_comment to CSS (#3045)
Fixes zed-industries/community#2081

Release Notes:
- Fixed "toggle comment" action not working in CSS buffers.
2023-09-27 11:56:26 +02:00
Conrad Irwin
8e1bbf32be vim: Fix ctrl-u/ctrl-d
They should work by exactly half a screen, and also move the cursor.
2023-09-26 22:28:04 -06:00
Conrad Irwin
30bb3a109e Add SwapPaneInDirection (#3043)
- Add cmd-k shift-{left,right,up,down} to swap panes in that direction
- vim: Add ctrl-w shift-{h,j,k,l} to swap panes in that direction
([#278](https://github.com/zed-industries/community/issues/278))
2023-09-26 22:18:02 -06:00
Conrad Irwin
37b6e1cbb7 Add SwapPaneInDirection
Add keybindings for vim (and non-vim)
2023-09-26 22:00:51 -06:00
Kirill Bulatov
cb83b49432 Hide inlay hints toggle if they are not supported by the current editor (#3041)
Release Notes:

- N/A
2023-09-27 01:16:02 +03:00
Marshall Bowers
568fec0f54 Add Sized bound to StyleHelpers (#3042)
This PR adds a `Sized` bound to the `StyleHelpers` trait.

All of the individual methods on this trait already had a `Self: Sized`
bound, so moving it up to the trait level will make it so we don't have
to repeat ourselves so much.

There's an open question of whether we can hoist the `Sized` bound to
`Styleable`, but it's possible there are cases where we'd want to have a
`Styleable` trait object.

Release Notes:

- N/A
2023-09-26 18:15:41 -04:00
Kirill Bulatov
7e2cef98a7 Hide inlay hints toggle if they are not supported by the current editor 2023-09-26 23:52:11 +02:00
KCaverly
90f17d4a28 updated codegen match to leverage unused values 2023-09-26 17:11:20 -04:00
KCaverly
e8dd412ac1 update inline generate prompt to leverage more explicit <|START| and |END|> spans 2023-09-26 17:10:31 -04:00
KCaverly
54c63063e4 changed inline assist generate prompt to leverage outline as opposed to entire prior file
Co-Authored-by: Antonio <antonio@zed.dev>
2023-09-26 16:23:48 -04:00
Antonio Scandurra
58dadad8ec WIP 2023-09-26 13:42:58 -06:00
Joseph T. Lyons
e9e558d8c8 Rework call events api (#3038)
There were times when events with bad data were being emitted. What we
found was that places where certain collaboration-related code could
fail, like sending an invite, would still send events; those events
would be in a bad state, as certain elements, such as a room, weren't
constructed as expected, causing the event to have missing data. The new
API guarantees that we have data in the correct configuration. In the
future, we will add events for certain types of failures within Zed, to
cover things like invites failing.

Release Notes:

- N/A
2023-09-26 14:29:25 -04:00
Joseph T. Lyons
0897ed561f Rework call events api
There were time when events with bad data were being emitted. What we found was that places where certain collaboration-related code could fail, like sending an, would still send events, and those events be in a bad state, as certain elements weren't constructed as expected, thus missing in the event. The new API guarantees that we have data in the correct configuration. In the future, we will add events for certain types of failures within Zed.

Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>
2023-09-26 14:18:32 -04:00
Piotr Osiewicz
e263805847 workspace: change save prompt for unnamed buffers (#3037)
Release Notes:
- N/A
2023-09-26 19:35:10 +02:00
Antonio Scandurra
bfe2205ecb Checkpoint 2023-09-26 11:34:41 -06:00
Antonio Scandurra
04d3ea9563 Checkpoint 2023-09-26 11:29:44 -06:00
Piotr Osiewicz
8c47f117db editor: Start transaction in replace impl (#3036)
This fixes the undo with replace in project
/cc @maxbrunsfeld 

Release Notes:

- N/A
2023-09-26 19:21:15 +02:00
Piotr Osiewicz
36f022bb58 project_replace: Fix up key bindings (#3034)
Release Notes:
- N/A
2023-09-26 18:40:41 +02:00
KCaverly
e75f56a0f2 move to system blas 2023-09-26 12:39:22 -04:00
Marshall Bowers
342a00b89e Remove dbg! from styleable_helpers! (#3035)
This PR removes a leftover `dbg!` from `styleable_helpers!`.

We already removed this in the `gpui2-ui` branch, but getting this on
`main` since @KCaverly pointed it out.

Release Notes:

- N/A
2023-09-26 10:49:55 -04:00
KCaverly
330a71d28b fixed bug limiting number of results returned 2023-09-26 10:29:55 -04:00
KCaverly
ea278b5b12 ensure desc sort and cleanup unused imports 2023-09-26 09:53:49 -04:00
Kirill Bulatov
5e7f0c65fe Fix another place where Copilot may panic (#3033) 2023-09-26 11:12:36 +03:00
Kirill Bulatov
b131a2cb98 Fix another place where Copilot may panic 2023-09-26 10:51:13 +03:00
Joseph T. Lyons
b5a39de3e2 Add reset_db script 2023-09-25 21:45:28 -04:00
Conrad Irwin
42df5ef45e vim: Add multicursor shortcuts (#3032)
Adding a few bindings to bring first class feeling multiselect to zed's
vim emulation.

gn and gN are similar to similar vim bindings, ga is similar to gA (and
I doubt we need vim's real ga), g> and g< are just made up.

Release Notes:

- vim: `g n` / `g N` to select next/previous
- vim: `g >` / `g <` to skip current selection and select next/previous
- vim: `g a` to select all
2023-09-25 17:18:12 -05:00
Conrad Irwin
b29e295e1b vim: Add multicursor shortcuts
- g n / g N to select next/previous
- g > / g < to select next/previous replacing current
- g a to select all matches
2023-09-25 15:32:03 -06:00
Mikayla Maki
8c90157990 Fix space and copy/paste when editing a channel (#3030)
This fixes several bugs with how spaces and keyboard commands interact
with channel creating / renaming.

fixes
https://github.com/zed-industries/community/discussions/2076#discussioncomment-7096959

Release Notes:

- N/A
2023-09-25 15:20:00 -05:00
Conrad Irwin
b454f43b6c Add cmd-+ as an alias for cmd-= (#3028)
Release Notes:

- Allow cmd-+ in addition to cmd-= for zoom in
([#1021](https://github.com/zed-industries/community/issues/1021)).

Although I had initially thought this was something more to do with
option key handling, it turns out to be a straightforward and reasonable
feature request.
2023-09-25 14:45:46 -05:00
Conrad Irwin
53194ede5e Use SaveAll instead of Save
If we're closing items we should not be writing files that have not
changed (e.g. empty untitled buffers)
2023-09-25 13:14:30 -06:00
Conrad Irwin
d17d38fe70 vim: Command (#2951)
Release Notes:

- vim: Add v1 of command mode
([#279](https://github.com/zed-industries/community/issues/279)). The
goal was to cover 90% of what most people actually do, but it is very
incomplete. Known omissions are that ranges cannot be specified (except
that `:%s//` must always specify the % range), commands cannot take
arguments (you can `:w` but not `:w [file]`), and there is no history.
Please file feature requests on
https://github.com/zed-industries/community as you notice things that
could be better.
- `:` triggers zed's command palette. If you type a known vim command it
will run it, otherwise you get zed's normal fuzzy search. For this
release supported commands are limited to:
- - `:w[rite][!]`, `:wq[!]`, `:q[uit][!]`, `:wa[ll][!]`, `:wqa[ll][!]`,
`:qa[ll][!]`, `:[e]x[it][!]`, `:up[date]` to save/close tab(s) and
pane(s).
- - `:cq` to quit completely.
- - `:vs[plit]`, `:sp[lit]` to split vertically/horizontally
- - `:new`, `:vne[w]` to create a new file in a new pane above or to the
left
- - `:tabedit`, `:tabnew` to create a new file in a new tab.
- - `:tabn[ext]`, `:tabp[rev]` to go to previous/next tabs
- - `:tabc[lose]` to close tabs
- - `:cn[ext]`, `:cp[rev]`, `:ln[ext]`, `:lp[rev]` to go to the
next/prev diagnostics.
- - `:cc`, `:ll` to open the errors page
- - `:<number>` to jump to a line number.
- - `:$` to jump to end of file
- - `:%s/foo/bar/` (note that /g is always implied, the range must
always be %, and zed uses different regex syntax to vim)
- - `:/foo` and `:?foo` to jump to next/prev line matching foo
- - `:j[oin]`, to join the current line (no range is yet supported)
- - `:d[elete][l][p]`, to delete the current line (no range is yet
supported)
- - `:s[ort] [i]` to sort the current selection (case-insensitively)
- vim: Add `ctrl-w o` (closes everything except the current item) and
`ctrl-w n` (creates a new file in the pane above).
([#1884](https://github.com/zed-industries/community/issues/1884))
- all: Add a "Discard" option to prompt when saving a file with
conflicts (previously this only appeared on close, not on save).

Internal changes:
- The Picker will now wait for pending queries before confirming (to
handle people typing `: w enter` rapidly.
- workspace::save_item and Pane::save_item are now merged together, and
the behavior controlled by `workspace::SaveIntent`.
- Many actions related to closing/saving items now take an optional
`SaveIntent`.
-
2023-09-25 14:07:22 -05:00
Mikayla
667fc25766 Fix space and copy/paste when editing a channel 2023-09-25 11:31:02 -07:00
Conrad Irwin
359847d047 Revert "Revert "workspace: Improve save prompt. (#3025)""
This reverts commit 5c75450a77.
2023-09-25 12:18:03 -06:00
Antonio Scandurra
15567493ba WIP 2023-09-25 11:55:05 -06:00
Mikayla Maki
591ec02cea Add support for the experimental Next LS for Elixir (#3024)
This is a PR I built for a friend of a friend at StrangeLoop, who is
making a much better LSP for elixir that elixir folks want to experiment
with. This PR also improves the our debug log viewer to handle LSP
restarts.

TODO:
- [ ] Make sure NextLS binary loading works.

Release Notes:

- Added support for the experimental Next LS for Elxir, to enable it add
the following field to your settings to enable:

```json
"elixir": {
    "next": "on"
}
```
2023-09-25 12:52:56 -05:00
Antonio Scandurra
a1e080d495 Checkpoint 2023-09-25 11:48:51 -06:00
Mikayla
c2fca054ae Fix compile and test errors 2023-09-25 10:46:09 -07:00
Julia
bf6c2f0dfd Activate correct item when clicking on a code action with the mouse (#3027)
Release Notes:

- Fixed clicking a code action only ever performing the first action in
the list rather than the one clicked on.
2023-09-25 13:45:20 -04:00
KCaverly
86ec0b1d9f implement new search strategy 2023-09-25 13:44:19 -04:00
Conrad Irwin
769c330b3d Merge branch 'vim-command' 2023-09-25 11:41:13 -06:00
Conrad Irwin
5c75450a77 Revert "workspace: Improve save prompt. (#3025)"
This reverts commit 0a491e773b.
2023-09-25 11:41:09 -06:00
Mikayla
ad7c1f3c81 Download next-ls automatically from github 2023-09-25 10:40:20 -07:00
Conrad Irwin
23767f734f Add cmd-+ as an alias for cmd-=
For github.com/zed-industries/community#1021
2023-09-25 11:31:34 -06:00
Julia
80eaabd360 Activate correct item when clicking on a code action with the mouse 2023-09-25 13:31:00 -04:00
Julia
ff5d0f2aeb Trigger scroll_to on code action list when moving selection (#3026)
Release Notes:
- Fixed the code action popup menu not scrolling as selection moves.
2023-09-25 11:21:24 -04:00
Julia
a278428bd5 Trigger scroll_to on code action list when moving selection 2023-09-25 11:13:50 -04:00
Piotr Osiewicz
0a491e773b workspace: Improve save prompt. (#3025)
Add buffer path to the prompt.

Z-2903

Release Notes:
- Added a "Save all/Discard all" prompt when closing a pane with
multiple edited buffers.
2023-09-25 16:15:29 +02:00
Antonio Scandurra
45540a00ee Checkpoint 2023-09-24 17:12:59 -06:00
Antonio Scandurra
55f4aa3b34 Checkpoint 2023-09-24 16:52:33 -06:00
Mikayla
8b63e45f0b Implement LSP adapter methods for syntax highlighting 2023-09-24 05:08:05 -07:00
Mikayla
052cb459a6 Improve lsp log viewer's behavior in the presence of LSP restarts
Improve settings interface to local LSP
2023-09-24 04:59:55 -07:00
Antonio Scandurra
a7803570dc Checkpoint 2023-09-23 17:26:22 -06:00
Antonio Scandurra
b516ea2fe2 Checkpoint 2023-09-23 15:56:40 -06:00
Antonio Scandurra
1fa45c69d6 Checkpoint 2023-09-23 15:52:16 -06:00
Antonio Scandurra
c4abd93b9b WIP 2023-09-23 15:24:01 -06:00
Antonio Scandurra
91c1768939 Checkpoint 2023-09-23 15:03:05 -06:00
Antonio Scandurra
1a5d6aa498 Checkpoint 2023-09-23 14:20:07 -06:00
Antonio Scandurra
fb69f3d45f Checkpoint
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-09-23 12:21:52 -06:00
Antonio Scandurra
3fbe93f029 Checkpoint
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-09-23 11:06:10 -06:00
Antonio Scandurra
df388d9f33 Checkpoint 2023-09-23 09:10:33 -06:00
Marshall Bowers
0697d08e54 Restructure ui into just elements and components (#3023)
This PR restructures the `ui` crate into just `elements` and
`components`.

This was already done on the `gpui2-ui` branch, just getting it onto
`main`.

Release Notes:

- N/A

---------

Co-authored-by: Nate Butler <nate@zed.dev>
2023-09-22 21:27:47 -04:00
Marshall Bowers
895386cfaf Mainline Icon and IconButton changes (#3022)
This PR mainlines the `Icon` and `IconButton` changes from the
`gpui2-ui` branch.

Release Notes:

- N/A

Co-authored-by: Nate Butler <nate@zed.dev>
2023-09-22 19:14:12 -04:00
Antonio Scandurra
6a95f9e349 WIP 2023-09-22 16:31:26 -06:00
Marshall Bowers
ad62a966a6 Display available stories in storybook CLI (#3021)
This PR updates the storybook CLI to support displaying all of the
available stories.

The `--help` flag will now show a list of all the available stories:

<img width="1435" alt="Screenshot 2023-09-22 at 6 11 00 PM"
src="https://github.com/zed-industries/zed/assets/1486634/284e1a24-46ec-462e-9709-0f9b6e94931f">

Inputting an invalid story name will also show the list of available
stories:

<img width="1435" alt="Screenshot 2023-09-22 at 6 10 43 PM"
src="https://github.com/zed-industries/zed/assets/1486634/1ce3ae3f-ab03-4976-a06a-5a2b5f61eae3">

Release Notes:

- N/A
2023-09-22 18:16:16 -04:00
Marshall Bowers
fe4248cf34 Scaffold Toolbar and Breadcrumb components (#3020)
This PR scaffolds the `Toolbar` and `Breadcrumb` components.

Right now they both just consist of hardcoded data.

<img width="846" alt="Screenshot 2023-09-22 at 4 54 00 PM"
src="https://github.com/zed-industries/zed/assets/1486634/70578df2-7216-42d2-97ef-d38b83fb4a25">

<img width="799" alt="Screenshot 2023-09-22 at 4 46 04 PM"
src="https://github.com/zed-industries/zed/assets/1486634/73ca3d8a-baf9-4ed4-b4c4-279c674672a3">

Release Notes:

- N/A
2023-09-22 16:57:33 -04:00
Antonio Scandurra
a237aa8164 Checkpoint
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-09-22 14:53:25 -06:00
Antonio Scandurra
3dc1e917bf Checkpoint
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-09-22 14:40:39 -06:00
Marshall Bowers
27e3e09bb9 Label component states in stories (#3019)
This PR updates the UI component stories to label the various states
that they are in.

Release Notes:

- N/A
2023-09-22 15:48:32 -04:00
Antonio Scandurra
d1791a999d Checkpoint
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-09-22 13:30:51 -06:00
Marshall Bowers
d0b15ed940 Report which requested font families are not present on the system (#3006)
This PR improves the error message when `FontCache.load_family` attempts
to load a font that is not present on the system.

I ran into this while trying to run the `storybook` for the first time.
The error message indicated that a font family was not found, but did
not provide any information as to which font family was being loaded.

### Before

```
   Compiling storybook v0.1.0 (/Users/maxdeviant/projects/zed/crates/storybook)
    Finished dev [unoptimized + debuginfo] target(s) in 8.52s
     Running `/Users/maxdeviant/projects/zed/target/debug/storybook`
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: could not find a non-empty font family matching one of the given names', crates/theme/src/theme_settings.rs:132:18
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
libc++abi: terminating due to uncaught foreign exception
fish: Job 1, 'cargo run' terminated by signal SIGABRT (Abort)
```

### After

```
   Compiling storybook v0.1.0 (/Users/maxdeviant/projects/zed/crates/storybook)
    Finished dev [unoptimized + debuginfo] target(s) in 7.90s
     Running `/Users/maxdeviant/projects/zed/target/debug/storybook`
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: could not find a non-empty font family matching one of the given names: `Zed Mono`', crates/theme/src/theme_settings.rs:132:18
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
libc++abi: terminating due to uncaught foreign exception
fish: Job 1, 'cargo run' terminated by signal SIGABRT (Abort)
```

Release Notes:

- N/A
2023-09-22 15:27:42 -04:00
Antonio Scandurra
e4e9da7673 Checkpoint
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-09-22 12:44:37 -06:00
Marshall Bowers
8b6e982495 Remove manual mapping in FromStr implementation for StorySelector (#3018)
This PR removes the need for writing manual mappings in the `FromStr`
implementation for the `StorySelector` enum used in the storybook CLI.

We are now using the
[`EnumString`](https://docs.rs/strum/0.25.0/strum/derive.EnumString.html)
trait from `strum` to automatically derive snake_cased names for the
enums.

This will cut down on some of the manual work needed to wire up more
stories to the storybook.

Release Notes:

- N/A
2023-09-22 14:06:09 -04:00
Marshall Bowers
71c1e36d1e Put Theme behind an Arc (#3017)
This PR puts the `Theme` returned from the `theme` function behind an
`Arc`.

### Motivation

While working on wiring up window focus events for the `TitleBar`
component we ran into issues where `theme` was holding an immutable
borrow to the `ViewContext` for the entirety of the `render` scope,
which prevented having mutable borrows in the same scope.

### Explanation

To avoid this, we can make `theme` return an `Arc<Theme>` to allow for
cheap clones and avoiding the issues with the borrow checker.

Release Notes:

- N/A

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2023-09-22 13:35:30 -04:00
Antonio Scandurra
343c426307 Checkpoint 2023-09-22 10:02:11 -06:00
Marshall Bowers
d8c6adf338 Factor story boilerplate out into separate components (#3016)
This PR factors out the bulk of the boilerplate required to setup a
story in the storybook out into separate components.

The pattern we're using here is adapted from the "[associated
component](https://maxdeviant.com/posts/2021/react-associated-components/)"
pattern in React.

Release Notes:

- N/A
2023-09-22 11:38:01 -04:00
Nathan Sobo
e979d75cb8 WIP 2023-09-22 08:34:43 -06:00
Kirill Bulatov
afa7045847 Tone down inlay hint update logs 2023-09-22 17:04:11 +03:00
Kyle Caverly
e84339ef4a reorganize AI crates to structure future development (#3015)
Reorganized assistant/semantic_index crates and introduced AI crate to
include shared functionality.

Release Notes:

- Moved most of the Assistant functionality from ai crate to assistant
crate
- Moved interaction with embedding providers from semantic_index to ai
crate
2023-09-22 09:54:46 -04:00
KCaverly
fbd6b5b434 cargo fmt 2023-09-22 09:46:06 -04:00
KCaverly
dc49dec4f0 catchup with main 2023-09-22 09:43:39 -04:00
KCaverly
68c37ca2a4 move embedding provider to ai crate 2023-09-22 09:33:59 -04:00
Kirill Bulatov
1f1c669673 Do not resubscribe for Copilot logs events (#3014)
Last follow-up of https://github.com/zed-industries/zed/pull/3002
Fixes
https://zed-industries.slack.com/archives/C04S6T1T7TQ/p1695281196667609

Copilot sends multiple events about its LSP server readiness, not
necessarily recreating the server from scratch (e.g. due to re-sign in
action). Avoid re-adding same log subscriptions on the same LSP server,
which causes panics.

Release Notes:

- N/A
2023-09-22 13:48:13 +03:00
Kirill Bulatov
d61565d227 Do not resubscribe for Copilot logs events
Copilot sends multiple events about its LSP server readiness, not necessarily recreating the server from scratch (e.g. due to re-sign in action).
Avoid re-adding same log subscriptions on the same LSP server, which
causes panics.
2023-09-22 13:40:20 +03:00
Nate Butler
a5e055f8a5 Bring UI crate up to date (#3013)
Merges various in-progress gpui2 component branches with the new `ui`
crate.
2023-09-21 23:54:11 -04:00
Nate Butler
30b105afd5 Remove leftover state doc 2023-09-21 23:51:03 -04:00
Nate Butler
d14e4d41ea Merge branch 'main' into nate/bring-ui-crate-up-to-date 2023-09-21 23:47:17 -04:00
Nate Butler
f54634aeb2 Bring UI crate up to date 2023-09-21 23:46:06 -04:00
Marshall Bowers
5083ab7694 Add TrafficLights component (#3011)
This PR adds a `TrafficLights` component for GPUI2.

<img width="861" alt="Screenshot 2023-09-21 at 11 32 10 PM"
src="https://github.com/zed-industries/zed/assets/1486634/0fe0e847-49b3-44dc-bd4c-64f12f0051c1">

Release Notes:

- N/A
2023-09-21 23:42:18 -04:00
KCaverly
48e151495f introduce ai crate with completion providers 2023-09-21 22:44:56 -04:00
Marshall Bowers
66358f2900 Update storybook to support stories for individual components (#3010)
This PR updates the `storybook` with support for adding stories for
individual components.

### Motivation

Right now we just have one story in the storybook that renders an entire
`WorkspaceElement`.

While iterating on the various UI components, it will be helpful to be
able to create stories of those components just by themselves.

This is especially true for components that have a number of different
states, as we can render the components in all of the various states in
a single layout.

### Explanation

We achieve this by adding a simple CLI to the storybook.

The `storybook` binary now accepts an optional `[STORY]` parameter that
can be used to indicate which story should be loaded. If this parameter
is not provided, it will load the workspace story as it currently does.

Passing a story name will load the corresponding story, if it exists.

For example:

```
cargo run -- elements/avatar
```

<img width="723" alt="Screenshot 2023-09-21 at 10 29 52 PM"
src="https://github.com/zed-industries/zed/assets/1486634/5df489ed-8607-4024-9c19-c5f4541f97c9">

```
cargo run -- components/facepile
```

<img width="785" alt="Screenshot 2023-09-21 at 10 30 07 PM"
src="https://github.com/zed-industries/zed/assets/1486634/e04a4577-7403-405d-b23c-e765b7a06229">



Release Notes:

- N/A
2023-09-21 22:41:53 -04:00
KCaverly
5f6334696a rename ai crate to assistant crate 2023-09-21 21:54:59 -04:00
Mikayla
02a85b1252 Add local next LSP adapter 2023-09-21 18:09:02 -07:00
Nate Butler
4628639ac6 Update ambiguous theme import (#3009)
Fixes an ambiguous reference to `theme` causing storybook not to build.
2023-09-21 20:32:41 -04:00
Nate Butler
8440ac3a54 Fix fmt complaining about order 2023-09-21 20:25:25 -04:00
Nate Butler
1e6ac8caf2 theme::* -> crate::theme::*; 2023-09-21 20:21:56 -04:00
Max Brunsfeld
7711530704 Simplify titlebar facepile click rendering / mouse handling 2023-09-21 17:12:59 -07:00
Max Brunsfeld
4ffa167256 Allow following into channel notes regardless of project 2023-09-21 17:12:59 -07:00
Marshall Bowers
baa07e935e Extract UI elements from storybook into new ui crate (#3008)
This PR extracts the various UI elements from the `storybook` crate into
a new `ui` library crate.

Release Notes:

- N/A
2023-09-21 19:25:35 -04:00
Marshall Bowers
c252eae32e Reorganize ui module exports (#3007)
This PR reorganizes the exports for the `ui` module in the `storybook`
crate.

### Motivation

Currently we expose each of the various elements/components/modules in
two places:

- Through the module itself (e.g., `ui::element::Avatar`)
- Through the `ui` module's re-exports (e.g., `ui::Avatar`)

This means it's possible to import any given item from two spots, which
can lead to inconsistencies in the consumers. Additionally, it also
means we're shipping the exact module structure underneath `ui` as part
of the public API.

### Explanation

To avoid this, we can avoid exposing each of the individual modules
underneath `ui::{element, component, module}` and instead export just
the module contents themselves.

This makes the `ui` module namespace flat.

Release Notes:

- N/A
2023-09-21 17:46:37 -04:00
Marshall Bowers
92d3115f3d Fix some typos in tools.md 2023-09-21 17:21:40 -04:00
Marshall Bowers
6bbf614a37 Fix some typos in README.md 2023-09-21 16:56:04 -04:00
Max Brunsfeld
ed8b022b51 Add initial failing test for following to channel notes in an unshared project 2023-09-21 13:14:15 -07:00
Max Brunsfeld
f34c6bd1ce Start work on allowing following without a shared project 2023-09-21 13:14:15 -07:00
Max Brunsfeld
c71566e7f5 Make project id optional when following - server only 2023-09-21 13:14:15 -07:00
Max Brunsfeld
83455028b0 Procfile: run zed.dev via 'next dev', not 'vercel dev' 2023-09-21 13:14:15 -07:00
Nathan Sobo
d120d0cf2e Checkpoint 2023-09-21 14:10:53 -06:00
Nathan Sobo
a0416e9c6d WIP 2023-09-21 13:46:31 -06:00
Nathan Sobo
a53c0b9472 WIP 2023-09-21 13:39:54 -06:00
Kyle Caverly
3c2b05be90 add semantic index status, for non authenticated users (#3005)
Update project search semantic ui to accommodate for users who have not
set the OPENAI_API_KEY in their environment variables.

Release Notes:

- Expand Semantic Index status to include status for non authenticated
users
- Update Search UI to illustrate this status.
2023-09-21 14:18:58 -04:00
Nathan Sobo
8573c6e8c6 WIP 2023-09-21 12:18:09 -06:00
KCaverly
7b63369df2 move api authentication to embedding provider 2023-09-21 14:00:00 -04:00
KCaverly
997f362cc2 add semantic index status, for non authenticated users 2023-09-21 13:40:01 -04:00
Max Brunsfeld
59e561dcf9 Bump rust from 1.72.0 to 1.72.1 2023-09-21 10:17:55 -07:00
Nate Butler
056353f8a8 Correct icon_margin_scale for fold indicator (#3003)
Fixes a design regression on Preview where the fold icon became small
due to the icon standardization PR.

Release Notes:

- [Preview] Fixed an issue with the size of the fold line icon.
2023-09-21 12:52:54 -04:00
Max Brunsfeld
19a9753663 Fix channel move cancel (#3004)
Release Notes:

- Fixes a bug where channels could no longer be rearranged with drag and
drop.
2023-09-21 09:11:09 -07:00
Mikayla
66dd0e9ec0 Switch drag end event to be fired after mouse up 2023-09-21 08:58:36 -07:00
Nate Butler
d74b8ec4e3 Correct icon_margin_scale 2023-09-21 11:57:35 -04:00
Piotr Osiewicz
dbfa1d7263 [WIP] Replace in project (#2984)
Targeting Preview of 09.27.
This is still pending several touchups/clearups:
- We should watch multibuffer for changes and rescan the excerpts. This
should also update match count.
- Closing editor while multibuffer with 100's of changed files is open
leads to us prompting for save once per each file in the multibuffer.
One could in theory save in multibuffer before closing it (thus avoiding
unnecessary prompts), but it'd be cool to be able to "Save all"/"Discard
All".

Release Notes:

- Added "Replace in project" functionality
2023-09-21 16:27:58 +02:00
Kirill Bulatov
d090fd25e4 Supplementary LSP server log improvements (#3002)
Follow-up of https://github.com/zed-industries/zed/pull/2991 improving
rough edges around supplementary LSP servers:

* Fixes
https://zed-industries.slack.com/archives/C04S6T1T7TQ/p1695281196667609
Copilot init panic
* Makes LSP server list scrollable in the panel
* Shows supplementary servers' RPC logs in the panel

Release Notes:

- N/A
2023-09-21 11:22:56 +03:00
Kirill Bulatov
1c53b0a1c0 Properly re-add Copilot LSP server 2023-09-21 11:02:03 +03:00
Kirill Bulatov
a2ac5ae478 Fix RPC logs not being displayed for supplementary servers 2023-09-21 11:00:05 +03:00
Kirill Bulatov
ead7155b0f Make LSP panel scrollable
co-authored-by: Max <max@zed.dev>
2023-09-21 10:59:19 +03:00
Nathan Sobo
dfeb702544 WIP - Next: implement Element derive macro 2023-09-20 22:26:46 -06:00
Conrad Irwin
32f8733313 Code review changes 2023-09-20 21:29:45 -06:00
Conrad Irwin
4bf4c780be Revert accidental Cargo change 2023-09-20 20:50:22 -06:00
Conrad Irwin
7a7ff4bb96 Fix save related tests, and refactor saves again 2023-09-20 20:44:42 -06:00
Conrad Irwin
a59da3634b Fix backward search from command 2023-09-20 20:44:42 -06:00
Conrad Irwin
a25fcfdfa7 Iron out some edge-cases 2023-09-20 20:44:42 -06:00
Conrad Irwin
2d9db0fed1 Flesh out v1.0 of vim : 2023-09-20 20:44:41 -06:00
Conrad Irwin
6ad1f19a21 Add NewFileInDirection 2023-09-20 20:44:26 -06:00
Conrad Irwin
88a32ae48d Merge Workspace::save_item into Pane::save_item
These methods were slightly different which caused (for example) there
to be no "Discard" option in the conflict case at the workspace level.

To make this work, a new SaveBehavior (::PromptForNewPath) was added to
support SaveAs.
2023-09-20 20:44:26 -06:00
Conrad Irwin
a4f96e6452 tests: wait deterministically after simulating_keystrokes 2023-09-20 20:44:26 -06:00
Conrad Irwin
e27b7d7812 Ensure the picker waits for pending updates
Particularly in development builds (and in tests), when typing in the
command palette, I tend to hit enter before the suggestions have
settled.
2023-09-20 20:44:26 -06:00
Conrad Irwin
ba5d84f7e8 Fix vim tests on my machine
In a rare case of "it broke on my machine" I haven't been able to run
the vim tests locally for a few days; turns out I ran out of swap file
names...
2023-09-20 20:44:26 -06:00
Conrad Irwin
ea3a1745f5 Add vim-specific interactions to command
This mostly adds the commonly requested set (:wq and friends) and
a few that I use frequently
:<line> to go to a line number
:vsp / :sp to create a split
:cn / :cp to go to diagnostics
2023-09-20 20:44:26 -06:00
Nathan Sobo
6d2b27689d WIP 2023-09-20 20:40:30 -06:00
Nathan Sobo
a8bb3dd9a3 WIP 2023-09-20 20:28:32 -06:00
Max Brunsfeld
d42093e069 collab 0.22.1 2023-09-20 17:39:21 -07:00
Max Brunsfeld
98482f0150 Fix select all bugs (#3001)
Release Notes:

- Restore `cmd-shift-d` as 'editor::DuplicateLine' and move
`editor::SelectAllMatches` to `cmd-shift-L`, like in VS Code. The
previous action for `cmd-shift-l`, `editor::SplitSelectionIntoLines`,
has been moved to the sublime base keymap.
- Fixes a panic when using 'editor::SelectAllMatches'  on an empty line.
2023-09-20 17:21:10 -07:00
Mikayla
58f4efb579 fix default keybindings for select all matches 2023-09-20 17:14:19 -07:00
Mikayla
fe10875285 Fix panic on select all when query is empty 2023-09-20 17:10:23 -07:00
Mikayla Maki
e0fe97401d Fix bugs from channel moving (#3000)
This PR fixes several bugs related to channel moving and it's
unintuitive behavior when attempting to re-order channels

Release Notes:

- N/A
2023-09-20 17:01:14 -07:00
Mikayla
f2f507e619 Fix bug in channel rendering
Fix drag and drop stale state bug revealed by the channel panel

co-authored-by: Max <max@zed.dev>
2023-09-20 16:40:29 -07:00
Conrad Irwin
f4d4a2f41b vim fixes for find&replace (#2995)
* allow replacing with the empty string to delete
* fix <enter> for ReplaceNext (in vim mode)

Release Notes:

- allow replacement to be empty
2023-09-20 16:42:39 -06:00
Mikayla
4ff44dfa3b Fix bugs in moving channels that could cause channels to be stranded or moved unexpectedly
Made channel linking not query in a loop

co-authored-by: Max <max@zed.dev>
2023-09-20 15:32:06 -07:00
Max Brunsfeld
ee16b2051e Fix opening channel notes from collab panel context menu (#2998)
Release Notes:

- Fixed a bug where the 'Open Notes' action in the collaboration panel
context menu didn't work (preview only).
2023-09-20 13:55:23 -07:00
Max Brunsfeld
3633f091c5 Fix opening channel notes from context menu 2023-09-20 13:45:35 -07:00
Nathan Sobo
44608517c1 WIP 2023-09-20 14:32:55 -06:00
Conrad Irwin
841b4d648c Fix vim panic when over-shooting with j (#2997)
Release Notes:

- vim: fix a panic when using `j` to go beyond end of file
2023-09-20 12:17:07 -06:00
Nathan Sobo
5b0e333967 Checkpoint 2023-09-20 12:16:26 -06:00
Conrad Irwin
01b2db4845 Fix vim test recording 2023-09-20 12:01:04 -06:00
Joseph T. Lyons
e7d73b833b collab 0.22.0 2023-09-20 13:59:36 -04:00
Nate Butler
f7696114bb Add an initial set of GPUI2 components to the storybook (#2990)
This PR adds an initial set of components to `crates/storybook/src/ui`.

All changes still are contained to inside storybook. Merging to keep up
to date with main.
2023-09-20 13:52:47 -04:00
Conrad Irwin
8de67fd9d9 Fix vim panic when over-shooting with j 2023-09-20 11:20:35 -06:00
Nate Butler
be6690bf0b Update tracker.md 2023-09-20 13:08:20 -04:00
Joseph T. Lyons
a86dc942d6 v0.106.x dev 2023-09-20 13:02:13 -04:00
Nate Butler
6dcb0bafb0 WIP Project Tracker 2023-09-20 12:53:08 -04:00
Nathan Sobo
83dae46ec6 Checkpoint 2023-09-20 10:17:29 -06:00
Mikayla
0cceb3fdf1 Get nextLS running 2023-09-20 06:55:24 -07:00
Nathan Sobo
7885eaf974 Checkpoint 2023-09-19 21:55:49 -06:00
Nathan Sobo
37d0f06e07 Checkpoint 2023-09-19 20:55:13 -06:00
Conrad Irwin
2da664ed17 vim fixes for find&replace
* allow replacing with the empty string to delete
* fix <enter> for ReplaceNext
2023-09-19 20:48:01 -06:00
Nate Butler
2699f170ca Checkpoint - Details 2023-09-19 19:18:23 -04:00
Nathan Sobo
8406c0d9a3 Checkpoint 2023-09-19 13:27:57 -06:00
Nathan Sobo
e762f7f3c0 Checkpoint 2023-09-19 13:09:00 -06:00
Nate Butler
65aa4d5642 Draw indent guides using indent_level
Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>
2023-09-19 12:38:46 -04:00
Nate Butler
3a9f5d6ddc use u32 as indent_level
Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>
2023-09-19 11:59:55 -04:00
Nate Butler
748ad5f05a Make list_item toggleable, improve optional left_icon on list item
Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>
2023-09-19 11:52:35 -04:00
Nathan Sobo
89519b1521 WIP 2023-09-19 08:46:02 -06:00
Nate Butler
7e300079ce WIP 2023-09-19 08:31:22 -04:00
Nate Butler
26f442a675 Merge branch 'main' into nate/gpui2-ui-components 2023-09-19 00:26:41 -04:00
Nate Butler
8aa4fbea83 Add icon, list_item, work on project panel 2023-09-19 00:25:46 -04:00
Nathan Sobo
2701be91e3 Add negative style helpers 2023-09-18 20:47:40 -06:00
Nathan Sobo
f2e87a3429 Add Element::when method 2023-09-18 20:25:12 -06:00
Nathan Sobo
c7a3186d08 Checkpoint 2023-09-18 20:17:27 -06:00
Nathan Sobo
a5e4ceb735 Checkpoint: Add methods for setting arbitrary lengths 2023-09-18 19:48:22 -06:00
Nate Butler
b725cadf48 Checkpoint 2023-09-18 19:59:01 -04:00
Nathan Sobo
df9a05ba92 Checkpoint 2023-09-18 17:40:47 -06:00
Nate Butler
db1dacde5d Add facepile, indicator, follow_group 2023-09-15 22:10:51 -04:00
Nate Butler
9f2a9d43b1 Organize design system under ui 2023-09-15 16:34:56 -04:00
Nate Butler
d6f24feb4a WIP 2023-09-15 14:14:28 -04:00
Nate Butler
40e785fdff Merge branch 'main' into nate/gpui2-ui-components 2023-09-15 13:57:15 -04:00
Nate Butler
70a91c5426 Checkpoint 2023-09-15 11:41:21 -04:00
Nathan Sobo
0a2a5be71c Checkpoint 2023-09-14 21:42:56 -06:00
Nathan Sobo
4fd4f44bb7 Checkpoint 2023-09-14 18:51:03 -06:00
Nathan Sobo
378b2fbd9e WIP 2023-09-14 16:52:04 -06:00
Nathan Sobo
1f9b52d5e1 Checkpoint 2023-09-14 16:19:48 -06:00
Nathan Sobo
b30bb56483 Checkpoint 2023-09-14 16:14:42 -06:00
Nathan Sobo
ba22da00bf Checkpoint 2023-09-14 15:49:54 -06:00
Nathan Sobo
cee5ddee53 Checkpoint 2023-09-14 15:34:39 -06:00
Nathan Sobo
c74c60a629 Slotmaps 2023-09-14 15:21:49 -06:00
Nathan Sobo
e4f8fe941e Checkpoint 2023-09-14 14:47:02 -06:00
Nathan Sobo
b9e1ca1385 Checkpoint 2023-09-14 14:42:04 -06:00
Nate Butler
fd10b49742 Fix .active() interaction state
Co-Authored-By: Nathan Sobo <1789+nathansobo@users.noreply.github.com>
2023-09-13 13:56:13 -04:00
Nathan Sobo
1c20a8cd31 Checkpoint 2023-09-13 11:07:24 -06:00
Nate Butler
a316e25034 Checkpoint 2023-09-13 12:50:01 -04:00
Nate Butler
f54f2c52e9 Checkpoint 2023-09-13 12:40:28 -04:00
Nathan Sobo
1adb7fa58c Checkpoint 2023-09-13 09:28:04 -06:00
Nathan Sobo
dcf6059a15 Checkpoint 2023-09-13 08:55:14 -06:00
Nathan Sobo
8a6e9f90be Checkpoint 2023-09-13 08:13:45 -06:00
Nathan Sobo
77af33ba5d WIP 2023-09-13 00:03:56 -06:00
Nathan Sobo
faabed1df0 Checkpoint 2023-09-12 21:34:03 -06:00
Nathan Sobo
cae5c76bed Checkpoint 2023-09-12 21:07:35 -06:00
Nathan Sobo
53b698adb6 Checkpoint 2023-09-12 20:40:05 -06:00
Nate Butler
bbc4673f17 Checkpoint 2023-09-12 15:18:13 -04:00
Nathan Sobo
dc2733998e WIP 2023-09-12 12:43:08 -06:00
Nate Butler
0d161519e4 Checkpoint 2023-09-12 11:34:27 -04:00
Nathan Sobo
5dad97779a WIP 2023-09-12 07:34:42 -06:00
Nathan Sobo
3ba8857491 Checkpoint 2023-09-11 12:58:55 -06:00
Nathan Sobo
7cd416c63e Always log panics 2023-08-25 21:42:18 -06:00
458 changed files with 97657 additions and 8678 deletions

View File

@@ -2,11 +2,4 @@
Release Notes:
- N/A
or
- (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
If the release notes are only intended for a specific release channel only, add `(<release_channel>-only)` to the end of the release note line.
These will be removed by the person making the release.

View File

@@ -6,8 +6,8 @@ jobs:
discord_release:
runs-on: ubuntu-latest
steps:
- name: Get appropriate URL
id: get-appropriate-url
- name: Get release URL
id: get-release-url
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
URL="https://zed.dev/releases/preview/latest"
@@ -15,14 +15,19 @@ jobs:
URL="https://zed.dev/releases/stable/latest"
fi
echo "::set-output name=URL::$URL"
- name: Get content
uses: 2428392/gh-truncate-string-action@v1.2.0
id: get-content
with:
stringToTruncate: |
📣 Zed ${{ github.event.release.tag_name }} was just released!
Restart your Zed or head to ${{ steps.get-release-url.outputs.URL }} to grab it.
${{ github.event.release.body }}
maxLength: 2000
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: |
📣 Zed ${{ github.event.release.tag_name }} was just released!
Restart your Zed or head to ${{ steps.get-appropriate-url.outputs.URL }} to grab it.
${{ github.event.release.body }}
content: ${{ steps.get-content.outputs.string }}

1426
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
members = [
"crates/activity_indicator",
"crates/ai",
"crates/assistant",
"crates/audio",
"crates/auto_update",
"crates/breadcrumbs",
@@ -9,6 +10,7 @@ members = [
"crates/channel",
"crates/cli",
"crates/client",
"crates/client2",
"crates/clock",
"crates/collab",
"crates/collab_ui",
@@ -17,18 +19,23 @@ members = [
"crates/component_test",
"crates/context_menu",
"crates/copilot",
"crates/copilot2",
"crates/copilot_button",
"crates/db",
"crates/db2",
"crates/refineable",
"crates/refineable/derive_refineable",
"crates/diagnostics",
"crates/drag_and_drop",
"crates/editor",
"crates/feature_flags",
"crates/feature_flags2",
"crates/feedback",
"crates/file_finder",
"crates/fs",
"crates/fsevent",
"crates/fuzzy",
"crates/fuzzy2",
"crates/git",
"crates/go_to_line",
"crates/gpui",
@@ -38,11 +45,13 @@ members = [
"crates/install_cli",
"crates/journal",
"crates/language",
"crates/language2",
"crates/language_selector",
"crates/language_tools",
"crates/live_kit_client",
"crates/live_kit_server",
"crates/lsp",
"crates/lsp2",
"crates/media",
"crates/menu",
"crates/node_runtime",
@@ -51,7 +60,9 @@ members = [
"crates/plugin",
"crates/plugin_macros",
"crates/plugin_runtime",
"crates/prettier",
"crates/project",
# "crates/project2",
"crates/project_panel",
"crates/project_symbols",
"crates/recent_projects",
@@ -59,16 +70,20 @@ members = [
"crates/rpc",
"crates/search",
"crates/settings",
"crates/settings2",
"crates/snippet",
"crates/sqlez",
"crates/sqlez_macros",
"crates/feature_flags",
"crates/storybook",
"crates/rich_text",
"crates/storybook2",
"crates/sum_tree",
"crates/terminal",
#"crates/terminal2",
"crates/text",
"crates/theme",
"crates/theme2",
"crates/theme_selector",
"crates/ui2",
"crates/util",
"crates/semantic_index",
"crates/vim",
@@ -77,6 +92,7 @@ members = [
"crates/welcome",
"crates/xtask",
"crates/zed",
"crates/zed2",
"crates/zed-actions"
]
default-members = ["crates/zed"]
@@ -103,12 +119,14 @@ rand = { version = "0.8.5" }
refineable = { path = "./crates/refineable" }
regex = { version = "1.5" }
rust-embed = { version = "8.0", features = ["include-exclude"] }
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
schemars = { version = "0.8" }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
smallvec = { version = "1.6", features = ["union"] }
smol = { version = "1.2" }
sysinfo = "0.29.10"
tempdir = { version = "0.3.7" }
thiserror = { version = "1.0.29" }
time = { version = "0.3", features = ["serde", "serde-well-known"] }
@@ -117,6 +135,7 @@ tree-sitter = "0.20"
unindent = { version = "0.1.7" }
pretty_assertions = "1.3.0"
git2 = { version = "0.15", default-features = false}
uuid = { version = "1.1.2", features = ["v4"] }
tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "1b0321ee85701d5036c334a6f04761cdc672e64c" }
tree-sitter-c = "0.20.1"

View File

@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2
FROM rust:1.72-bullseye as builder
FROM rust:1.73-bullseye as builder
WORKDIR app
COPY . .

View File

@@ -1,4 +1,4 @@
web: cd ../zed.dev && PORT=3000 npx vercel dev
collab: cd crates/collab && cargo run serve
web: cd ../zed.dev && PORT=3000 npm run dev
collab: cd crates/collab && RUST_LOG=${RUST_LOG:-collab=info} cargo run serve
livekit: livekit-server --dev
postgrest: postgrest crates/collab/admin_api.conf

View File

@@ -13,7 +13,7 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
sudo xcodebuild -license
```
* Install homebrew, node and rustup-init (rutup, rust, cargo, etc.)
* Install homebrew, node and rustup-init (rustup, rust, cargo, etc.)
```
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install node rustup-init
@@ -36,7 +36,7 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
brew install foreman
```
* Ensure the Zed.dev website is checked out in a sibling directory and install it's dependencies:
* Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies:
```
cd ..
@@ -83,9 +83,7 @@ foreman start
If you want to run Zed pointed at the local servers, you can run:
```
script/zed-with-local-servers
# or...
script/zed-with-local-servers --release
script/zed-local
```
### Dump element JSON

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -30,6 +30,7 @@
"cmd-s": "workspace::Save",
"cmd-shift-s": "workspace::SaveAs",
"cmd-=": "zed::IncreaseBufferFontSize",
"cmd-+": "zed::IncreaseBufferFontSize",
"cmd--": "zed::DecreaseBufferFontSize",
"cmd-0": "zed::ResetBufferFontSize",
"cmd-,": "zed::OpenSettings",
@@ -249,6 +250,7 @@
"bindings": {
"escape": "project_search::ToggleFocus",
"alt-tab": "search::CycleMode",
"cmd-shift-h": "search::ToggleReplace",
"alt-cmd-g": "search::ActivateRegexMode",
"alt-cmd-s": "search::ActivateSemanticMode",
"alt-cmd-x": "search::ActivateTextMode"
@@ -261,11 +263,19 @@
"down": "search::NextHistoryQuery"
}
},
{
"context": "ProjectSearchBar && in_replace",
"bindings": {
"enter": "search::ReplaceNext",
"cmd-enter": "search::ReplaceAll"
}
},
{
"context": "ProjectSearchView",
"bindings": {
"escape": "project_search::ToggleFocus",
"alt-tab": "search::CycleMode",
"cmd-shift-h": "search::ToggleReplace",
"alt-cmd-g": "search::ActivateRegexMode",
"alt-cmd-s": "search::ActivateSemanticMode",
"alt-cmd-x": "search::ActivateTextMode"
@@ -277,6 +287,7 @@
"cmd-f": "project_search::ToggleFocus",
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPrevMatch",
"cmd-shift-h": "search::ToggleReplace",
"alt-enter": "search::SelectAllMatches",
"alt-cmd-c": "search::ToggleCaseSensitive",
"alt-cmd-w": "search::ToggleWholeWord",
@@ -303,7 +314,7 @@
"replace_newest": false
}
],
"cmd-shift-d": "editor::SelectAllMatches",
"cmd-shift-l": "editor::SelectAllMatches",
"ctrl-cmd-d": [
"editor::SelectPrevious",
{
@@ -463,7 +474,7 @@
"context": "Editor",
"bindings": {
"ctrl-shift-k": "editor::DeleteLine",
"cmd-shift-l": "editor::SplitSelectionIntoLines",
"cmd-shift-d": "editor::DuplicateLine",
"ctrl-j": "editor::JoinLines",
"ctrl-cmd-up": "editor::MoveLineUp",
"ctrl-cmd-down": "editor::MoveLineDown",
@@ -498,6 +509,22 @@
"cmd-k cmd-down": [
"workspace::ActivatePaneInDirection",
"Down"
],
"cmd-k shift-left": [
"workspace::SwapPaneInDirection",
"Left"
],
"cmd-k shift-right": [
"workspace::SwapPaneInDirection",
"Right"
],
"cmd-k shift-up": [
"workspace::SwapPaneInDirection",
"Up"
],
"cmd-k shift-down": [
"workspace::SwapPaneInDirection",
"Down"
]
}
},
@@ -562,7 +589,7 @@
}
},
{
"context": "ProjectSearchBar",
"context": "ProjectSearchBar && !in_replace",
"bindings": {
"cmd-enter": "project_search::SearchInNew"
}
@@ -588,14 +615,20 @@
}
},
{
"context": "CollabPanel",
"context": "CollabPanel && not_editing",
"bindings": {
"ctrl-backspace": "collab_panel::Remove",
"space": "menu::Confirm"
}
},
{
"context": "CollabPanel > Editor",
"context": "(CollabPanel && editing) > Editor",
"bindings": {
"space": "collab_panel::InsertSpace"
}
},
{
"context": "(CollabPanel && not_editing) > Editor",
"bindings": {
"cmd-c": "collab_panel::StartLinkChannel",
"cmd-x": "collab_panel::StartMoveChannel",

View File

@@ -17,6 +17,7 @@
"ctrl-shift-down": "editor::AddSelectionBelow",
"cmd-shift-space": "editor::SelectAll",
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
"cmd-shift-l": "editor::SplitSelectionIntoLines",
"cmd-shift-a": "editor::SelectLargerSyntaxNode",
"shift-f12": "editor::FindAllReferences",
"alt-cmd-down": "editor::GoToDefinition",

View File

@@ -18,6 +18,7 @@
}
}
],
":": "command_palette::Toggle",
"h": "vim::Left",
"left": "vim::Left",
"backspace": "vim::Backspace",
@@ -94,6 +95,7 @@
}
],
"ctrl-o": "pane::GoBack",
"ctrl-i": "pane::GoForward",
"ctrl-]": "editor::GoToDefinition",
"escape": [
"vim::SwitchMode",
@@ -125,10 +127,26 @@
"g shift-t": "pane::ActivatePrevItem",
"g d": "editor::GoToDefinition",
"g shift-d": "editor::GoToTypeDefinition",
"g n": "vim::SelectNext",
"g shift-n": "vim::SelectPrevious",
"g >": [
"editor::SelectNext",
{
"replace_newest": true
}
],
"g <": [
"editor::SelectPrevious",
{
"replace_newest": true
}
],
"g a": "editor::SelectAllMatches",
"g s": "outline::Toggle",
"g shift-s": "project_symbols::Toggle",
"g .": "editor::ToggleCodeActions", // zed specific
"g shift-a": "editor::FindAllReferences", // zed specific
"g space": "editor::OpenExcerpts", // zed specific
"g *": [
"vim::MoveToNext",
{
@@ -205,13 +223,13 @@
"shift-z shift-q": [
"pane::CloseActiveItem",
{
"saveBehavior": "dontSave"
"saveIntent": "skip"
}
],
"shift-z shift-z": [
"pane::CloseActiveItem",
{
"saveBehavior": "promptOnConflict"
"saveIntent": "saveAll"
}
],
// Count support
@@ -300,6 +318,38 @@
"workspace::ActivatePaneInDirection",
"Down"
],
"ctrl-w shift-left": [
"workspace::SwapPaneInDirection",
"Left"
],
"ctrl-w shift-right": [
"workspace::SwapPaneInDirection",
"Right"
],
"ctrl-w shift-up": [
"workspace::SwapPaneInDirection",
"Up"
],
"ctrl-w shift-down": [
"workspace::SwapPaneInDirection",
"Down"
],
"ctrl-w shift-h": [
"workspace::SwapPaneInDirection",
"Left"
],
"ctrl-w shift-l": [
"workspace::SwapPaneInDirection",
"Right"
],
"ctrl-w shift-k": [
"workspace::SwapPaneInDirection",
"Up"
],
"ctrl-w shift-j": [
"workspace::SwapPaneInDirection",
"Down"
],
"ctrl-w g t": "pane::ActivateNextItem",
"ctrl-w ctrl-g t": "pane::ActivateNextItem",
"ctrl-w g shift-t": "pane::ActivatePrevItem",
@@ -318,7 +368,17 @@
"ctrl-w c": "pane::CloseAllItems",
"ctrl-w ctrl-c": "pane::CloseAllItems",
"ctrl-w q": "pane::CloseAllItems",
"ctrl-w ctrl-q": "pane::CloseAllItems"
"ctrl-w ctrl-q": "pane::CloseAllItems",
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
"ctrl-w n": [
"workspace::NewFileInDirection",
"Up"
],
"ctrl-w ctrl-n": [
"workspace::NewFileInDirection",
"Up"
]
}
},
{
@@ -348,6 +408,7 @@
"vim::PushOperator",
"Yank"
],
"shift-y": "vim::YankLine",
"i": "vim::InsertBefore",
"shift-i": "vim::InsertFirstNonWhitespace",
"a": "vim::InsertAfter",
@@ -357,6 +418,8 @@
"o": "vim::InsertLineBelow",
"shift-o": "vim::InsertLineAbove",
"~": "vim::ChangeCase",
"ctrl-a": "vim::Increment",
"ctrl-x": "vim::Decrement",
"p": "vim::Paste",
"shift-p": [
"vim::Paste",
@@ -468,6 +531,20 @@
"shift-r": "vim::SubstituteLine",
"c": "vim::Substitute",
"~": "vim::ChangeCase",
"ctrl-a": "vim::Increment",
"ctrl-x": "vim::Decrement",
"g ctrl-a": [
"vim::Increment",
{
"step": true
}
],
"g ctrl-x": [
"vim::Decrement",
{
"step": true
}
],
"shift-i": "vim::InsertBefore",
"shift-a": "vim::InsertAfter",
"shift-j": "vim::JoinLines",
@@ -508,11 +585,16 @@
}
},
{
"context": "Editor && vim_mode == insert && !menu",
"context": "Editor && vim_mode == insert",
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore",
"ctrl-[": "vim::NormalBefore"
"ctrl-[": "vim::NormalBefore",
"ctrl-x ctrl-o": "editor::ShowCompletions",
"ctrl-x ctrl-a": "assistant::InlineAssist", // zed specific
"ctrl-x ctrl-c": "copilot::Suggest", // zed specific
"ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific
"ctrl-x ctrl-z": "editor::Cancel"
}
},
{
@@ -531,7 +613,7 @@
}
},
{
"context": "BufferSearchBar > VimEnabled",
"context": "BufferSearchBar && !in_replace > VimEnabled",
"bindings": {
"enter": "vim::SearchSubmit",
"escape": "buffer_search::Dismiss"

View File

@@ -76,7 +76,7 @@
// Settings related to calls in Zed
"calls": {
// Join calls with the microphone muted by default
"mute_on_join": true
"mute_on_join": false
},
// Scrollbar related settings
"scrollbar": {
@@ -199,7 +199,12 @@
// "arguments": ["--stdin-filepath", "{buffer_path}"]
// }
// }
"formatter": "language_server",
// 3. Format code using Zed's Prettier integration:
// "formatter": "prettier"
// 4. Default. Format files using Zed's Prettier integration (if applicable),
// or falling back to formatting via language server:
// "formatter": "auto"
"formatter": "auto",
// How to soft-wrap long lines of text. This setting can take
// three values:
//
@@ -227,6 +232,11 @@
},
// Automatically update Zed
"auto_update": true,
// Diagnostics configuration.
"diagnostics": {
// Whether to show warnings or not by default.
"include_warnings": true
},
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
@@ -356,7 +366,7 @@
".venv",
"venv"
],
// Can also be 'csh' and 'fish'
// Can also be 'csh', 'fish', and `nushell`
"activate_script": "default"
}
}
@@ -370,7 +380,28 @@
},
// Difference settings for semantic_index
"semantic_index": {
"enabled": false
"enabled": true
},
// Settings specific to our elixir integration
"elixir": {
// Change the LSP zed uses for elixir.
// Note that changing this setting requires a restart of Zed
// to take effect.
//
// May take 3 values:
// 1. Use the standard ElixirLS, this is the default
// "lsp": "elixir_ls"
// 2. Use the experimental NextLs
// "lsp": "next_ls",
// 3. Use a language server installed locally on your machine:
// "lsp": {
// "local": {
// "path": "~/next-ls/bin/start",
// "arguments": ["--stdio"]
// }
// },
//
"lsp": "elixir_ls"
},
// Different settings for specific languages.
"languages": {
@@ -403,6 +434,16 @@
"tab_size": 2
}
},
// Zed's Prettier integration settings.
// If Prettier is enabled, Zed will use this its Prettier instance for any applicable file, if
// project has no other Prettier installed.
"prettier": {
// Use regular Prettier json configuration:
// "trailingComma": "es5",
// "tabWidth": 4,
// "semi": false,
// "singleQuote": true
},
// LSP Specific settings.
"lsp": {
// Specify the LSP name as a key here.

View File

@@ -9,39 +9,26 @@ path = "src/ai.rs"
doctest = false
[dependencies]
client = { path = "../client" }
collections = { path = "../collections"}
editor = { path = "../editor" }
fs = { path = "../fs" }
gpui = { path = "../gpui" }
language = { path = "../language" }
menu = { path = "../menu" }
search = { path = "../search" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
uuid = { version = "1.1.2", features = ["v4"] }
workspace = { path = "../workspace" }
async-trait.workspace = true
anyhow.workspace = true
chrono = { version = "0.4", features = ["serde"] }
futures.workspace = true
indoc.workspace = true
isahc.workspace = true
lazy_static.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true
isahc.workspace = true
regex.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
tiktoken-rs = "0.4"
postage.workspace = true
rand.workspace = true
log.workspace = true
parse_duration = "2.1.1"
tiktoken-rs = "0.5.0"
matrixmultiply = "0.3.7"
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
bincode = "1.3.3"
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
ctor.workspace = true
env_logger.workspace = true
log.workspace = true
rand.workspace = true
gpui = { path = "../gpui", features = ["test-support"] }

View File

@@ -1,294 +1,2 @@
pub mod assistant;
mod assistant_settings;
mod codegen;
mod streaming_diff;
use anyhow::{anyhow, Result};
pub use assistant::AssistantPanel;
use assistant_settings::OpenAIModel;
use chrono::{DateTime, Local};
use collections::HashMap;
use fs::Fs;
use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
use gpui::{executor::Background, AppContext};
use isahc::{http::StatusCode, Request, RequestExt};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{
cmp::Reverse,
ffi::OsStr,
fmt::{self, Display},
io,
path::PathBuf,
sync::Arc,
};
use util::paths::CONVERSATIONS_DIR;
const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
// Data types for chat completion requests
#[derive(Debug, Default, Serialize)]
pub struct OpenAIRequest {
model: String,
messages: Vec<RequestMessage>,
stream: bool,
}
#[derive(
Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
struct MessageId(usize);
#[derive(Clone, Debug, Serialize, Deserialize)]
struct MessageMetadata {
role: Role,
sent_at: DateTime<Local>,
status: MessageStatus,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
enum MessageStatus {
Pending,
Done,
Error(Arc<str>),
}
#[derive(Serialize, Deserialize)]
struct SavedMessage {
id: MessageId,
start: usize,
}
#[derive(Serialize, Deserialize)]
struct SavedConversation {
id: Option<String>,
zed: String,
version: String,
text: String,
messages: Vec<SavedMessage>,
message_metadata: HashMap<MessageId, MessageMetadata>,
summary: String,
model: OpenAIModel,
}
impl SavedConversation {
const VERSION: &'static str = "0.1.0";
}
struct SavedConversationMetadata {
title: String,
path: PathBuf,
mtime: chrono::DateTime<chrono::Local>,
}
impl SavedConversationMetadata {
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
fs.create_dir(&CONVERSATIONS_DIR).await?;
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
let mut conversations = Vec::<SavedConversationMetadata>::new();
while let Some(path) = paths.next().await {
let path = path?;
if path.extension() != Some(OsStr::new("json")) {
continue;
}
let pattern = r" - \d+.zed.json$";
let re = Regex::new(pattern).unwrap();
let metadata = fs.metadata(&path).await?;
if let Some((file_name, metadata)) = path
.file_name()
.and_then(|name| name.to_str())
.zip(metadata)
{
let title = re.replace(file_name, "");
conversations.push(Self {
title: title.into_owned(),
path,
mtime: metadata.mtime.into(),
});
}
}
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
Ok(conversations)
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct RequestMessage {
role: Role,
content: String,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct ResponseMessage {
role: Option<Role>,
content: Option<String>,
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
enum Role {
User,
Assistant,
System,
}
impl Role {
pub fn cycle(&mut self) {
*self = match self {
Role::User => Role::Assistant,
Role::Assistant => Role::System,
Role::System => Role::User,
}
}
}
impl Display for Role {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Role::User => write!(f, "User"),
Role::Assistant => write!(f, "Assistant"),
Role::System => write!(f, "System"),
}
}
}
#[derive(Deserialize, Debug)]
pub struct OpenAIResponseStreamEvent {
pub id: Option<String>,
pub object: String,
pub created: u32,
pub model: String,
pub choices: Vec<ChatChoiceDelta>,
pub usage: Option<Usage>,
}
#[derive(Deserialize, Debug)]
pub struct Usage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
#[derive(Deserialize, Debug)]
pub struct ChatChoiceDelta {
pub index: u32,
pub delta: ResponseMessage,
pub finish_reason: Option<String>,
}
#[derive(Deserialize, Debug)]
struct OpenAIUsage {
prompt_tokens: u64,
completion_tokens: u64,
total_tokens: u64,
}
#[derive(Deserialize, Debug)]
struct OpenAIChoice {
text: String,
index: u32,
logprobs: Option<serde_json::Value>,
finish_reason: Option<String>,
}
pub fn init(cx: &mut AppContext) {
assistant::init(cx);
}
pub async fn stream_completion(
api_key: String,
executor: Arc<Background>,
mut request: OpenAIRequest,
) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
request.stream = true;
let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
let json_data = serde_json::to_string(&request)?;
let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions"))
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", api_key))
.body(json_data)?
.send_async()
.await?;
let status = response.status();
if status == StatusCode::OK {
executor
.spawn(async move {
let mut lines = BufReader::new(response.body_mut()).lines();
fn parse_line(
line: Result<String, io::Error>,
) -> Result<Option<OpenAIResponseStreamEvent>> {
if let Some(data) = line?.strip_prefix("data: ") {
let event = serde_json::from_str(&data)?;
Ok(Some(event))
} else {
Ok(None)
}
}
while let Some(line) = lines.next().await {
if let Some(event) = parse_line(line).transpose() {
let done = event.as_ref().map_or(false, |event| {
event
.choices
.last()
.map_or(false, |choice| choice.finish_reason.is_some())
});
if tx.unbounded_send(event).is_err() {
break;
}
if done {
break;
}
}
}
anyhow::Ok(())
})
.detach();
Ok(rx)
} else {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
#[derive(Deserialize)]
struct OpenAIResponse {
error: OpenAIError,
}
#[derive(Deserialize)]
struct OpenAIError {
message: String,
}
match serde_json::from_str::<OpenAIResponse>(&body) {
Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
"Failed to connect to OpenAI API: {}",
response.error.message,
)),
_ => Err(anyhow!(
"Failed to connect to OpenAI API: {} {}",
response.status(),
body,
)),
}
}
}
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {
if std::env::var("RUST_LOG").is_ok() {
env_logger::init();
}
}
pub mod completion;
pub mod embedding;

212
crates/ai/src/completion.rs Normal file
View File

@@ -0,0 +1,212 @@
use anyhow::{anyhow, Result};
use futures::{
future::BoxFuture, io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, FutureExt,
Stream, StreamExt,
};
use gpui::executor::Background;
use isahc::{http::StatusCode, Request, RequestExt};
use serde::{Deserialize, Serialize};
use std::{
fmt::{self, Display},
io,
sync::Arc,
};
pub const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Assistant,
System,
}
impl Role {
pub fn cycle(&mut self) {
*self = match self {
Role::User => Role::Assistant,
Role::Assistant => Role::System,
Role::System => Role::User,
}
}
}
impl Display for Role {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Role::User => write!(f, "User"),
Role::Assistant => write!(f, "Assistant"),
Role::System => write!(f, "System"),
}
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct RequestMessage {
pub role: Role,
pub content: String,
}
#[derive(Debug, Default, Serialize)]
pub struct OpenAIRequest {
pub model: String,
pub messages: Vec<RequestMessage>,
pub stream: bool,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct ResponseMessage {
pub role: Option<Role>,
pub content: Option<String>,
}
#[derive(Deserialize, Debug)]
pub struct OpenAIUsage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
#[derive(Deserialize, Debug)]
pub struct ChatChoiceDelta {
pub index: u32,
pub delta: ResponseMessage,
pub finish_reason: Option<String>,
}
#[derive(Deserialize, Debug)]
pub struct OpenAIResponseStreamEvent {
pub id: Option<String>,
pub object: String,
pub created: u32,
pub model: String,
pub choices: Vec<ChatChoiceDelta>,
pub usage: Option<OpenAIUsage>,
}
pub async fn stream_completion(
api_key: String,
executor: Arc<Background>,
mut request: OpenAIRequest,
) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
request.stream = true;
let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
let json_data = serde_json::to_string(&request)?;
let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions"))
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", api_key))
.body(json_data)?
.send_async()
.await?;
let status = response.status();
if status == StatusCode::OK {
executor
.spawn(async move {
let mut lines = BufReader::new(response.body_mut()).lines();
fn parse_line(
line: Result<String, io::Error>,
) -> Result<Option<OpenAIResponseStreamEvent>> {
if let Some(data) = line?.strip_prefix("data: ") {
let event = serde_json::from_str(&data)?;
Ok(Some(event))
} else {
Ok(None)
}
}
while let Some(line) = lines.next().await {
if let Some(event) = parse_line(line).transpose() {
let done = event.as_ref().map_or(false, |event| {
event
.choices
.last()
.map_or(false, |choice| choice.finish_reason.is_some())
});
if tx.unbounded_send(event).is_err() {
break;
}
if done {
break;
}
}
}
anyhow::Ok(())
})
.detach();
Ok(rx)
} else {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
#[derive(Deserialize)]
struct OpenAIResponse {
error: OpenAIError,
}
#[derive(Deserialize)]
struct OpenAIError {
message: String,
}
match serde_json::from_str::<OpenAIResponse>(&body) {
Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
"Failed to connect to OpenAI API: {}",
response.error.message,
)),
_ => Err(anyhow!(
"Failed to connect to OpenAI API: {} {}",
response.status(),
body,
)),
}
}
}
pub trait CompletionProvider {
fn complete(
&self,
prompt: OpenAIRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>;
}
pub struct OpenAICompletionProvider {
api_key: String,
executor: Arc<Background>,
}
impl OpenAICompletionProvider {
pub fn new(api_key: String, executor: Arc<Background>) -> Self {
Self { api_key, executor }
}
}
impl CompletionProvider for OpenAICompletionProvider {
fn complete(
&self,
prompt: OpenAIRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
let request = stream_completion(self.api_key.clone(), self.executor.clone(), prompt);
async move {
let response = request.await?;
let stream = response
.filter_map(|response| async move {
match response {
Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)),
Err(error) => Some(Err(error)),
}
})
.boxed();
Ok(stream)
}
.boxed()
}
}

View File

@@ -27,8 +27,30 @@ lazy_static! {
}
#[derive(Debug, PartialEq, Clone)]
pub struct Embedding(Vec<f32>);
pub struct Embedding(pub Vec<f32>);
// This is needed for semantic index functionality
// Unfortunately it has to live wherever the "Embedding" struct is created.
// Keeping this in here though, introduces a 'rusqlite' dependency into AI
// which is less than ideal
impl FromSql for Embedding {
fn column_result(value: ValueRef) -> FromSqlResult<Self> {
let bytes = value.as_blob()?;
let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
if embedding.is_err() {
return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
}
Ok(Embedding(embedding.unwrap()))
}
}
impl ToSql for Embedding {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
let bytes = bincode::serialize(&self.0)
.map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
}
}
impl From<Vec<f32>> for Embedding {
fn from(value: Vec<f32>) -> Self {
Embedding(value)
@@ -63,24 +85,24 @@ impl Embedding {
}
}
impl FromSql for Embedding {
fn column_result(value: ValueRef) -> FromSqlResult<Self> {
let bytes = value.as_blob()?;
let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
if embedding.is_err() {
return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
}
Ok(Embedding(embedding.unwrap()))
}
}
// impl FromSql for Embedding {
// fn column_result(value: ValueRef) -> FromSqlResult<Self> {
// let bytes = value.as_blob()?;
// let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
// if embedding.is_err() {
// return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
// }
// Ok(Embedding(embedding.unwrap()))
// }
// }
impl ToSql for Embedding {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
let bytes = bincode::serialize(&self.0)
.map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
}
}
// impl ToSql for Embedding {
// fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
// let bytes = bincode::serialize(&self.0)
// .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
// Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
// }
// }
#[derive(Clone)]
pub struct OpenAIEmbeddings {
@@ -117,6 +139,7 @@ struct OpenAIEmbeddingUsage {
#[async_trait]
pub trait EmbeddingProvider: Sync + Send {
fn is_authenticated(&self) -> bool;
async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>>;
fn max_tokens_per_batch(&self) -> usize;
fn truncate(&self, span: &str) -> (String, usize);
@@ -127,6 +150,9 @@ pub struct DummyEmbeddings {}
#[async_trait]
impl EmbeddingProvider for DummyEmbeddings {
fn is_authenticated(&self) -> bool {
true
}
fn rate_limit_expiration(&self) -> Option<Instant> {
None
}
@@ -229,6 +255,9 @@ impl OpenAIEmbeddings {
#[async_trait]
impl EmbeddingProvider for OpenAIEmbeddings {
fn is_authenticated(&self) -> bool {
OPENAI_API_KEY.as_ref().is_some()
}
fn max_tokens_per_batch(&self) -> usize {
50000
}

View File

@@ -0,0 +1,48 @@
[package]
name = "assistant"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/assistant.rs"
doctest = false
[dependencies]
ai = { path = "../ai" }
client = { path = "../client" }
collections = { path = "../collections"}
editor = { path = "../editor" }
fs = { path = "../fs" }
gpui = { path = "../gpui" }
language = { path = "../language" }
menu = { path = "../menu" }
search = { path = "../search" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
uuid.workspace = true
anyhow.workspace = true
chrono = { version = "0.4", features = ["serde"] }
futures.workspace = true
indoc.workspace = true
isahc.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true
regex.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
tiktoken-rs = "0.4"
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
ctor.workspace = true
env_logger.workspace = true
log.workspace = true
rand.workspace = true

View File

@@ -0,0 +1,113 @@
pub mod assistant_panel;
mod assistant_settings;
mod codegen;
mod prompts;
mod streaming_diff;
use ai::completion::Role;
use anyhow::Result;
pub use assistant_panel::AssistantPanel;
use assistant_settings::OpenAIModel;
use chrono::{DateTime, Local};
use collections::HashMap;
use fs::Fs;
use futures::StreamExt;
use gpui::AppContext;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc};
use util::paths::CONVERSATIONS_DIR;
#[derive(
Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
struct MessageId(usize);
#[derive(Clone, Debug, Serialize, Deserialize)]
struct MessageMetadata {
role: Role,
sent_at: DateTime<Local>,
status: MessageStatus,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
enum MessageStatus {
Pending,
Done,
Error(Arc<str>),
}
#[derive(Serialize, Deserialize)]
struct SavedMessage {
id: MessageId,
start: usize,
}
#[derive(Serialize, Deserialize)]
struct SavedConversation {
id: Option<String>,
zed: String,
version: String,
text: String,
messages: Vec<SavedMessage>,
message_metadata: HashMap<MessageId, MessageMetadata>,
summary: String,
model: OpenAIModel,
}
impl SavedConversation {
const VERSION: &'static str = "0.1.0";
}
struct SavedConversationMetadata {
title: String,
path: PathBuf,
mtime: chrono::DateTime<chrono::Local>,
}
impl SavedConversationMetadata {
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
fs.create_dir(&CONVERSATIONS_DIR).await?;
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
let mut conversations = Vec::<SavedConversationMetadata>::new();
while let Some(path) = paths.next().await {
let path = path?;
if path.extension() != Some(OsStr::new("json")) {
continue;
}
let pattern = r" - \d+.zed.json$";
let re = Regex::new(pattern).unwrap();
let metadata = fs.metadata(&path).await?;
if let Some((file_name, metadata)) = path
.file_name()
.and_then(|name| name.to_str())
.zip(metadata)
{
let title = re.replace(file_name, "");
conversations.push(Self {
title: title.into_owned(),
path,
mtime: metadata.mtime.into(),
});
}
}
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
Ok(conversations)
}
}
pub fn init(cx: &mut AppContext) {
assistant_panel::init(cx);
}
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {
if std::env::var("RUST_LOG").is_ok() {
env_logger::init();
}
}

View File

@@ -1,8 +1,12 @@
use crate::{
assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel},
codegen::{self, Codegen, CodegenKind, OpenAICompletionProvider},
stream_completion, MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage,
Role, SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL,
codegen::{self, Codegen, CodegenKind},
prompts::generate_content_prompt,
MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata,
SavedMessage,
};
use ai::completion::{
stream_completion, OpenAICompletionProvider, OpenAIRequest, RequestMessage, OPENAI_API_URL,
};
use anyhow::{anyhow, Result};
use chrono::{DateTime, Local};
@@ -13,7 +17,7 @@ use editor::{
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint,
},
scroll::autoscroll::{Autoscroll, AutoscrollStrategy},
Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset,
Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint,
};
use fs::Fs;
use futures::StreamExt;
@@ -270,22 +274,40 @@ impl AssistantPanel {
return;
};
let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
let selection = editor.read(cx).selections.newest_anchor().clone();
if selection.start.excerpt_id() != selection.end.excerpt_id() {
return;
}
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
// Extend the selection to the start and the end of the line.
let mut point_selection = selection.map(|selection| selection.to_point(&snapshot));
if point_selection.end > point_selection.start {
point_selection.start.column = 0;
// If the selection ends at the start of the line, we don't want to include it.
if point_selection.end.column == 0 {
point_selection.end.row -= 1;
}
point_selection.end.column = snapshot.line_len(point_selection.end.row);
}
let codegen_kind = if point_selection.start == point_selection.end {
CodegenKind::Generate {
position: snapshot.anchor_after(point_selection.start),
}
} else {
CodegenKind::Transform {
range: snapshot.anchor_before(point_selection.start)
..snapshot.anchor_after(point_selection.end),
}
};
let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
let provider = Arc::new(OpenAICompletionProvider::new(
api_key,
cx.background().clone(),
));
let selection = editor.read(cx).selections.newest_anchor().clone();
let codegen_kind = if editor.read(cx).selections.newest::<usize>(cx).is_empty() {
CodegenKind::Generate {
position: selection.start,
}
} else {
CodegenKind::Transform {
range: selection.start..selection.end,
}
};
let codegen = cx.add_model(|cx| {
Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
});
@@ -311,7 +333,7 @@ impl AssistantPanel {
editor.insert_blocks(
[BlockProperties {
style: BlockStyle::Flex,
position: selection.head().bias_left(&snapshot),
position: snapshot.anchor_before(point_selection.head()),
height: 2,
render: Arc::new({
let inline_assistant = inline_assistant.clone();
@@ -538,11 +560,26 @@ impl AssistantPanel {
self.inline_prompt_history.pop_front();
}
let codegen = pending_assist.codegen.clone();
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
let range = pending_assist.codegen.read(cx).range();
let selected_text = snapshot.text_for_range(range.clone()).collect::<String>();
let range = codegen.read(cx).range();
let start = snapshot.point_to_buffer_offset(range.start);
let end = snapshot.point_to_buffer_offset(range.end);
let (buffer, range) = if let Some((start, end)) = start.zip(end) {
let (start_buffer, start_buffer_offset) = start;
let (end_buffer, end_buffer_offset) = end;
if start_buffer.remote_id() == end_buffer.remote_id() {
(start_buffer.clone(), start_buffer_offset..end_buffer_offset)
} else {
self.finish_inline_assist(inline_assist_id, false, cx);
return;
}
} else {
self.finish_inline_assist(inline_assist_id, false, cx);
return;
};
let language = snapshot.language_at(range.start);
let language = buffer.language_at(range.start);
let language_name = if let Some(language) = language.as_ref() {
if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
None
@@ -552,95 +589,9 @@ impl AssistantPanel {
} else {
None
};
let language_name = language_name.as_deref();
let mut prompt = String::new();
if let Some(language_name) = language_name {
writeln!(prompt, "You're an expert {language_name} engineer.").unwrap();
}
match pending_assist.codegen.read(cx).kind() {
CodegenKind::Transform { .. } => {
writeln!(
prompt,
"You're currently working inside an editor on this file:"
)
.unwrap();
if let Some(language_name) = language_name {
writeln!(prompt, "```{language_name}").unwrap();
} else {
writeln!(prompt, "```").unwrap();
}
for chunk in snapshot.text_for_range(Anchor::min()..Anchor::max()) {
write!(prompt, "{chunk}").unwrap();
}
writeln!(prompt, "```").unwrap();
writeln!(
prompt,
"In particular, the user has selected the following text:"
)
.unwrap();
if let Some(language_name) = language_name {
writeln!(prompt, "```{language_name}").unwrap();
} else {
writeln!(prompt, "```").unwrap();
}
writeln!(prompt, "{selected_text}").unwrap();
writeln!(prompt, "```").unwrap();
writeln!(prompt).unwrap();
writeln!(
prompt,
"Modify the selected text given the user prompt: {user_prompt}"
)
.unwrap();
writeln!(
prompt,
"You MUST reply only with the edited selected text, not the entire file."
)
.unwrap();
}
CodegenKind::Generate { .. } => {
writeln!(
prompt,
"You're currently working inside an editor on this file:"
)
.unwrap();
if let Some(language_name) = language_name {
writeln!(prompt, "```{language_name}").unwrap();
} else {
writeln!(prompt, "```").unwrap();
}
for chunk in snapshot.text_for_range(Anchor::min()..range.start) {
write!(prompt, "{chunk}").unwrap();
}
write!(prompt, "<|>").unwrap();
for chunk in snapshot.text_for_range(range.start..Anchor::max()) {
write!(prompt, "{chunk}").unwrap();
}
writeln!(prompt).unwrap();
writeln!(prompt, "```").unwrap();
writeln!(
prompt,
"Assume the cursor is located where the `<|>` marker is."
)
.unwrap();
writeln!(
prompt,
"Text can't be replaced, so assume your answer will be inserted at the cursor."
)
.unwrap();
writeln!(
prompt,
"Complete the text given the user prompt: {user_prompt}"
)
.unwrap();
}
}
if let Some(language_name) = language_name {
writeln!(prompt, "Your answer MUST always be valid {language_name}.").unwrap();
}
writeln!(prompt, "Always wrap your response in a Markdown codeblock.").unwrap();
writeln!(prompt, "Never make remarks about the output.").unwrap();
let codegen_kind = codegen.read(cx).kind().clone();
let user_prompt = user_prompt.to_string();
let mut messages = Vec::new();
let mut model = settings::get::<AssistantSettings>(cx)
@@ -657,18 +608,26 @@ impl AssistantPanel {
model = conversation.model.clone();
}
messages.push(RequestMessage {
role: Role::User,
content: prompt,
let prompt = cx.background().spawn(async move {
let language_name = language_name.as_deref();
generate_content_prompt(user_prompt, language_name, &buffer, range, codegen_kind)
});
let request = OpenAIRequest {
model: model.full_name().into(),
messages,
stream: true,
};
pending_assist
.codegen
.update(cx, |codegen, cx| codegen.start(request, cx));
cx.spawn(|_, mut cx| async move {
let prompt = prompt.await;
messages.push(RequestMessage {
role: Role::User,
content: prompt,
});
let request = OpenAIRequest {
model: model.full_name().into(),
messages,
stream: true,
};
codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx));
})
.detach();
}
fn update_highlights_for_editor(

View File

@@ -1,59 +1,12 @@
use crate::{
stream_completion,
streaming_diff::{Hunk, StreamingDiff},
OpenAIRequest,
};
use crate::streaming_diff::{Hunk, StreamingDiff};
use ai::completion::{CompletionProvider, OpenAIRequest};
use anyhow::Result;
use editor::{
multi_buffer, Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
};
use futures::{
channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, SinkExt, Stream, StreamExt,
};
use gpui::{executor::Background, Entity, ModelContext, ModelHandle, Task};
use editor::{multi_buffer, Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
use gpui::{Entity, ModelContext, ModelHandle, Task};
use language::{Rope, TransactionId};
use std::{cmp, future, ops::Range, sync::Arc};
pub trait CompletionProvider {
fn complete(
&self,
prompt: OpenAIRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>;
}
pub struct OpenAICompletionProvider {
api_key: String,
executor: Arc<Background>,
}
impl OpenAICompletionProvider {
pub fn new(api_key: String, executor: Arc<Background>) -> Self {
Self { api_key, executor }
}
}
impl CompletionProvider for OpenAICompletionProvider {
fn complete(
&self,
prompt: OpenAIRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
let request = stream_completion(self.api_key.clone(), self.executor.clone(), prompt);
async move {
let response = request.await?;
let stream = response
.filter_map(|response| async move {
match response {
Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)),
Err(error) => Some(Err(error)),
}
})
.boxed();
Ok(stream)
}
.boxed()
}
}
pub enum Event {
Finished,
Undone,
@@ -85,26 +38,11 @@ impl Entity for Codegen {
impl Codegen {
pub fn new(
buffer: ModelHandle<MultiBuffer>,
mut kind: CodegenKind,
kind: CodegenKind,
provider: Arc<dyn CompletionProvider>,
cx: &mut ModelContext<Self>,
) -> Self {
let snapshot = buffer.read(cx).snapshot(cx);
match &mut kind {
CodegenKind::Transform { range } => {
let mut point_range = range.to_point(&snapshot);
point_range.start.column = 0;
if point_range.end.column > 0 || point_range.start.row == point_range.end.row {
point_range.end.column = snapshot.line_len(point_range.end.row);
}
range.start = snapshot.anchor_before(point_range.start);
range.end = snapshot.anchor_after(point_range.end);
}
CodegenKind::Generate { position } => {
*position = position.bias_right(&snapshot);
}
}
Self {
provider,
buffer: buffer.clone(),
@@ -397,13 +335,17 @@ fn strip_markdown_codeblock(
#[cfg(test)]
mod tests {
use super::*;
use futures::stream;
use futures::{
future::BoxFuture,
stream::{self, BoxStream},
};
use gpui::{executor::Deterministic, TestAppContext};
use indoc::indoc;
use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point};
use parking_lot::Mutex;
use rand::prelude::*;
use settings::SettingsStore;
use smol::future::FutureExt;
#[gpui::test(iterations = 10)]
async fn test_transform_autoindent(
@@ -427,7 +369,7 @@ mod tests {
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let range = buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(1, 4))..snapshot.anchor_after(Point::new(4, 4))
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
});
let provider = Arc::new(TestCompletionProvider::new());
let codegen = cx.add_model(|cx| {

View File

@@ -0,0 +1,418 @@
use crate::codegen::CodegenKind;
use language::{BufferSnapshot, OffsetRangeExt, ToOffset};
use std::cmp::{self, Reverse};
use std::fmt::Write;
use std::ops::Range;
#[allow(dead_code)]
fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String {
#[derive(Debug)]
struct Match {
collapse: Range<usize>,
keep: Vec<Range<usize>>,
}
let selected_range = selected_range.to_offset(buffer);
let mut ts_matches = buffer.matches(0..buffer.len(), |grammar| {
Some(&grammar.embedding_config.as_ref()?.query)
});
let configs = ts_matches
.grammars()
.iter()
.map(|g| g.embedding_config.as_ref().unwrap())
.collect::<Vec<_>>();
let mut matches = Vec::new();
while let Some(mat) = ts_matches.peek() {
let config = &configs[mat.grammar_index];
if let Some(collapse) = mat.captures.iter().find_map(|cap| {
if Some(cap.index) == config.collapse_capture_ix {
Some(cap.node.byte_range())
} else {
None
}
}) {
let mut keep = Vec::new();
for capture in mat.captures.iter() {
if Some(capture.index) == config.keep_capture_ix {
keep.push(capture.node.byte_range());
} else {
continue;
}
}
ts_matches.advance();
matches.push(Match { collapse, keep });
} else {
ts_matches.advance();
}
}
matches.sort_unstable_by_key(|mat| (mat.collapse.start, Reverse(mat.collapse.end)));
let mut matches = matches.into_iter().peekable();
let mut summary = String::new();
let mut offset = 0;
let mut flushed_selection = false;
while let Some(mat) = matches.next() {
// Keep extending the collapsed range if the next match surrounds
// the current one.
while let Some(next_mat) = matches.peek() {
if mat.collapse.start <= next_mat.collapse.start
&& mat.collapse.end >= next_mat.collapse.end
{
matches.next().unwrap();
} else {
break;
}
}
if offset > mat.collapse.start {
// Skip collapsed nodes that have already been summarized.
offset = cmp::max(offset, mat.collapse.end);
continue;
}
if offset <= selected_range.start && selected_range.start <= mat.collapse.end {
if !flushed_selection {
// The collapsed node ends after the selection starts, so we'll flush the selection first.
summary.extend(buffer.text_for_range(offset..selected_range.start));
summary.push_str("<|START|");
if selected_range.end == selected_range.start {
summary.push_str(">");
} else {
summary.extend(buffer.text_for_range(selected_range.clone()));
summary.push_str("|END|>");
}
offset = selected_range.end;
flushed_selection = true;
}
// If the selection intersects the collapsed node, we won't collapse it.
if selected_range.end >= mat.collapse.start {
continue;
}
}
summary.extend(buffer.text_for_range(offset..mat.collapse.start));
for keep in mat.keep {
summary.extend(buffer.text_for_range(keep));
}
offset = mat.collapse.end;
}
// Flush selection if we haven't already done so.
if !flushed_selection && offset <= selected_range.start {
summary.extend(buffer.text_for_range(offset..selected_range.start));
summary.push_str("<|START|");
if selected_range.end == selected_range.start {
summary.push_str(">");
} else {
summary.extend(buffer.text_for_range(selected_range.clone()));
summary.push_str("|END|>");
}
offset = selected_range.end;
}
summary.extend(buffer.text_for_range(offset..buffer.len()));
summary
}
pub fn generate_content_prompt(
user_prompt: String,
language_name: Option<&str>,
buffer: &BufferSnapshot,
range: Range<impl ToOffset>,
kind: CodegenKind,
) -> String {
let range = range.to_offset(buffer);
let mut prompt = String::new();
// General Preamble
if let Some(language_name) = language_name {
writeln!(prompt, "You're an expert {language_name} engineer.\n").unwrap();
} else {
writeln!(prompt, "You're an expert engineer.\n").unwrap();
}
let mut content = String::new();
content.extend(buffer.text_for_range(0..range.start));
if range.start == range.end {
content.push_str("<|START|>");
} else {
content.push_str("<|START|");
}
content.extend(buffer.text_for_range(range.clone()));
if range.start != range.end {
content.push_str("|END|>");
}
content.extend(buffer.text_for_range(range.end..buffer.len()));
writeln!(
prompt,
"The file you are currently working on has the following content:"
)
.unwrap();
if let Some(language_name) = language_name {
let language_name = language_name.to_lowercase();
writeln!(prompt, "```{language_name}\n{content}\n```").unwrap();
} else {
writeln!(prompt, "```\n{content}\n```").unwrap();
}
match kind {
CodegenKind::Generate { position: _ } => {
writeln!(prompt, "In particular, the user's cursor is current on the '<|START|>' span in the above outline, with no text selected.").unwrap();
writeln!(
prompt,
"Assume the cursor is located where the `<|START|` marker is."
)
.unwrap();
writeln!(
prompt,
"Text can't be replaced, so assume your answer will be inserted at the cursor."
)
.unwrap();
writeln!(
prompt,
"Generate text based on the users prompt: {user_prompt}"
)
.unwrap();
}
CodegenKind::Transform { range: _ } => {
writeln!(prompt, "In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.").unwrap();
writeln!(
prompt,
"Modify the users code selected text based upon the users prompt: {user_prompt}"
)
.unwrap();
writeln!(
prompt,
"You MUST reply with only the adjusted code (within the '<|START|' and '|END|>' spans), not the entire file."
)
.unwrap();
}
}
if let Some(language_name) = language_name {
writeln!(prompt, "Your answer MUST always be valid {language_name}").unwrap();
}
writeln!(prompt, "Always wrap your response in a Markdown codeblock").unwrap();
writeln!(prompt, "Never make remarks about the output.").unwrap();
prompt
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use std::sync::Arc;
use gpui::AppContext;
use indoc::indoc;
use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point};
use settings::SettingsStore;
pub(crate) fn rust_lang() -> Language {
Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
)
.with_embedding_query(
r#"
(
[(line_comment) (attribute_item)]* @context
.
[
(struct_item
name: (_) @name)
(enum_item
name: (_) @name)
(impl_item
trait: (_)? @name
"for"? @name
type: (_) @name)
(trait_item
name: (_) @name)
(function_item
name: (_) @name
body: (block
"{" @keep
"}" @keep) @collapse)
(macro_definition
name: (_) @name)
] @item
)
"#,
)
.unwrap()
}
#[gpui::test]
fn test_outline_for_prompt(cx: &mut AppContext) {
cx.set_global(SettingsStore::test(cx));
language_settings::init(cx);
let text = indoc! {"
struct X {
a: usize,
b: usize,
}
impl X {
fn new() -> Self {
let a = 1;
let b = 2;
Self { a, b }
}
pub fn a(&self, param: bool) -> usize {
self.a
}
pub fn b(&self) -> usize {
self.b
}
}
"};
let buffer =
cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
let snapshot = buffer.read(cx).snapshot();
assert_eq!(
summarize(&snapshot, Point::new(1, 4)..Point::new(1, 4)),
indoc! {"
struct X {
<|START|>a: usize,
b: usize,
}
impl X {
fn new() -> Self {}
pub fn a(&self, param: bool) -> usize {}
pub fn b(&self) -> usize {}
}
"}
);
assert_eq!(
summarize(&snapshot, Point::new(8, 12)..Point::new(8, 14)),
indoc! {"
struct X {
a: usize,
b: usize,
}
impl X {
fn new() -> Self {
let <|START|a |END|>= 1;
let b = 2;
Self { a, b }
}
pub fn a(&self, param: bool) -> usize {}
pub fn b(&self) -> usize {}
}
"}
);
assert_eq!(
summarize(&snapshot, Point::new(6, 0)..Point::new(6, 0)),
indoc! {"
struct X {
a: usize,
b: usize,
}
impl X {
<|START|>
fn new() -> Self {}
pub fn a(&self, param: bool) -> usize {}
pub fn b(&self) -> usize {}
}
"}
);
assert_eq!(
summarize(&snapshot, Point::new(21, 0)..Point::new(21, 0)),
indoc! {"
struct X {
a: usize,
b: usize,
}
impl X {
fn new() -> Self {}
pub fn a(&self, param: bool) -> usize {}
pub fn b(&self) -> usize {}
}
<|START|>"}
);
// Ensure nested functions get collapsed properly.
let text = indoc! {"
struct X {
a: usize,
b: usize,
}
impl X {
fn new() -> Self {
let a = 1;
let b = 2;
Self { a, b }
}
pub fn a(&self, param: bool) -> usize {
let a = 30;
fn nested() -> usize {
3
}
self.a + nested()
}
pub fn b(&self) -> usize {
self.b
}
}
"};
buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
let snapshot = buffer.read(cx).snapshot();
assert_eq!(
summarize(&snapshot, Point::new(0, 0)..Point::new(0, 0)),
indoc! {"
<|START|>struct X {
a: usize,
b: usize,
}
impl X {
fn new() -> Self {}
pub fn a(&self, param: bool) -> usize {}
pub fn b(&self) -> usize {}
}
"}
);
}
}

View File

@@ -115,13 +115,15 @@ pub fn check(_: &Check, cx: &mut AppContext) {
fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
if let Some(auto_updater) = AutoUpdater::get(cx) {
let server_url = &auto_updater.read(cx).server_url;
let auto_updater = auto_updater.read(cx);
let server_url = &auto_updater.server_url;
let current_version = auto_updater.current_version;
let latest_release_url = if cx.has_global::<ReleaseChannel>()
&& *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
{
format!("{server_url}/releases/preview/latest")
format!("{server_url}/releases/preview/{current_version}")
} else {
format!("{server_url}/releases/stable/latest")
format!("{server_url}/releases/stable/{current_version}")
};
cx.platform().open_url(&latest_release_url);
}

View File

@@ -20,7 +20,6 @@ test-support = [
[dependencies]
audio = { path = "../audio" }
channel = { path = "../channel" }
client = { path = "../client" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }

View File

@@ -2,22 +2,22 @@ pub mod call_settings;
pub mod participant;
pub mod room;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use audio::Audio;
use call_settings::CallSettings;
use channel::ChannelId;
use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
use client::{
proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
ZED_ALWAYS_ACTIVE,
};
use collections::HashSet;
use futures::{future::Shared, FutureExt};
use postage::watch;
use gpui::{
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task,
WeakModelHandle,
};
use postage::watch;
use project::Project;
use std::sync::Arc;
pub use participant::ParticipantLocation;
pub use room::Room;
@@ -68,6 +68,7 @@ impl ActiveCall {
location: None,
pending_invites: Default::default(),
incoming_call: watch::channel(),
_subscriptions: vec![
client.add_request_handler(cx.handle(), Self::handle_incoming_call),
client.add_message_handler(cx.handle(), Self::handle_call_canceled),
@@ -77,7 +78,7 @@ impl ActiveCall {
}
}
pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
pub fn channel_id(&self, cx: &AppContext) -> Option<u64> {
self.room()?.read(cx).channel_id()
}
@@ -206,9 +207,14 @@ impl ActiveCall {
cx.spawn(|this, mut cx| async move {
let result = invite.await;
if result.is_ok() {
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx));
} else {
// TODO: Resport collaboration error
}
this.update(&mut cx, |this, cx| {
this.pending_invites.remove(&called_user_id);
this.report_call_event("invite", cx);
cx.notify();
});
result
@@ -273,13 +279,7 @@ impl ActiveCall {
.borrow_mut()
.take()
.ok_or_else(|| anyhow!("no incoming call"))?;
Self::report_call_event_for_room(
"decline incoming",
Some(call.room_id),
None,
&self.client,
cx,
);
report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx);
self.client.send(proto::DeclineCall {
room_id: call.room_id,
})?;
@@ -290,10 +290,10 @@ impl ActiveCall {
&mut self,
channel_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
) -> Task<Result<ModelHandle<Room>>> {
if let Some(room) = self.room().cloned() {
if room.read(cx).channel_id() == Some(channel_id) {
return Task::ready(Ok(()));
return Task::ready(Ok(room));
} else {
room.update(cx, |room, cx| room.clear_state(cx));
}
@@ -308,7 +308,7 @@ impl ActiveCall {
this.update(&mut cx, |this, cx| {
this.report_call_event("join channel", cx)
});
Ok(())
Ok(room)
})
}
@@ -349,17 +349,22 @@ impl ActiveCall {
}
}
pub fn location(&self) -> Option<&WeakModelHandle<Project>> {
self.location.as_ref()
}
pub fn set_location(
&mut self,
project: Option<&ModelHandle<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
self.location = project.map(|project| project.downgrade());
if let Some((room, _)) = self.room.as_ref() {
room.update(cx, |room, cx| room.set_location(project, cx))
} else {
Task::ready(Ok(()))
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
self.location = project.map(|project| project.downgrade());
if let Some((room, _)) = self.room.as_ref() {
return room.update(cx, |room, cx| room.set_location(project, cx));
}
}
Task::ready(Ok(()))
}
fn set_room(
@@ -409,31 +414,46 @@ impl ActiveCall {
&self.pending_invites
}
fn report_call_event(&self, operation: &'static str, cx: &AppContext) {
let (room_id, channel_id) = match self.room() {
Some(room) => {
let room = room.read(cx);
(Some(room.id()), room.channel_id())
}
None => (None, None),
};
Self::report_call_event_for_room(operation, room_id, channel_id, &self.client, cx)
}
pub fn report_call_event_for_room(
operation: &'static str,
room_id: Option<u64>,
channel_id: Option<u64>,
client: &Arc<Client>,
cx: &AppContext,
) {
let telemetry = client.telemetry();
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
let event = ClickhouseEvent::Call {
operation,
room_id,
channel_id,
};
telemetry.report_clickhouse_event(event, telemetry_settings);
pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) {
if let Some(room) = self.room() {
let room = room.read(cx);
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx);
}
}
}
pub fn report_call_event_for_room(
operation: &'static str,
room_id: u64,
channel_id: Option<u64>,
client: &Arc<Client>,
cx: &AppContext,
) {
let telemetry = client.telemetry();
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
let event = ClickhouseEvent::Call {
operation,
room_id: Some(room_id),
channel_id,
};
telemetry.report_clickhouse_event(event, telemetry_settings);
}
pub fn report_call_event_for_channel(
operation: &'static str,
channel_id: u64,
client: &Arc<Client>,
cx: &AppContext,
) {
let room = ActiveCall::global(cx).read(cx).room();
let telemetry = client.telemetry();
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
let event = ClickhouseEvent::Call {
operation,
room_id: room.map(|r| r.read(cx).id()),
channel_id: Some(channel_id),
};
telemetry.report_clickhouse_event(event, telemetry_settings);
}

View File

@@ -1,4 +1,5 @@
use anyhow::{anyhow, Result};
use client::ParticipantIndex;
use client::{proto, User};
use collections::HashMap;
use gpui::WeakModelHandle;
@@ -43,6 +44,7 @@ pub struct RemoteParticipant {
pub peer_id: proto::PeerId,
pub projects: Vec<proto::ParticipantProject>,
pub location: ParticipantLocation,
pub participant_index: ParticipantIndex,
pub muted: bool,
pub speaking: bool,
pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,

View File

@@ -7,7 +7,7 @@ use anyhow::{anyhow, Result};
use audio::{Audio, Sound};
use client::{
proto::{self, PeerId},
Client, TypedEnvelope, User, UserStore,
Client, ParticipantIndex, TypedEnvelope, User, UserStore,
};
use collections::{BTreeMap, HashMap, HashSet};
use fs::Fs;
@@ -18,7 +18,7 @@ use live_kit_client::{
LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RemoteAudioTrackUpdate,
RemoteVideoTrackUpdate,
};
use postage::stream::Stream;
use postage::{sink::Sink, stream::Stream, watch};
use project::Project;
use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration};
use util::{post_inc, ResultExt, TryFutureExt};
@@ -44,6 +44,12 @@ pub enum Event {
RemoteProjectUnshared {
project_id: u64,
},
RemoteProjectJoined {
project_id: u64,
},
RemoteProjectInvitationDiscarded {
project_id: u64,
},
Left,
}
@@ -64,6 +70,8 @@ pub struct Room {
user_store: ModelHandle<UserStore>,
follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec<PeerId>>,
subscriptions: Vec<client::Subscription>,
room_update_completed_tx: watch::Sender<Option<()>>,
room_update_completed_rx: watch::Receiver<Option<()>>,
pending_room_update: Option<Task<()>>,
maintain_connection: Option<Task<Option<()>>>,
}
@@ -98,6 +106,10 @@ impl Room {
self.channel_id
}
pub fn is_sharing_project(&self) -> bool {
!self.shared_projects.is_empty()
}
#[cfg(any(test, feature = "test-support"))]
pub fn is_connected(&self) -> bool {
if let Some(live_kit) = self.live_kit.as_ref() {
@@ -201,6 +213,8 @@ impl Room {
Audio::play_sound(Sound::Joined, cx);
let (room_update_completed_tx, room_update_completed_rx) = watch::channel();
Self {
id,
channel_id,
@@ -220,6 +234,8 @@ impl Room {
user_store,
follows_by_leader_id_project_id: Default::default(),
maintain_connection: Some(maintain_connection),
room_update_completed_tx,
room_update_completed_rx,
}
}
@@ -588,6 +604,43 @@ impl Room {
.map_or(&[], |v| v.as_slice())
}
/// Returns the most 'active' projects, defined as most people in the project
pub fn most_active_project(&self, cx: &AppContext) -> Option<(u64, u64)> {
let mut project_hosts_and_guest_counts = HashMap::<u64, (Option<u64>, u32)>::default();
for participant in self.remote_participants.values() {
match participant.location {
ParticipantLocation::SharedProject { project_id } => {
project_hosts_and_guest_counts
.entry(project_id)
.or_default()
.1 += 1;
}
ParticipantLocation::External | ParticipantLocation::UnsharedProject => {}
}
for project in &participant.projects {
project_hosts_and_guest_counts
.entry(project.id)
.or_default()
.0 = Some(participant.user.id);
}
}
if let Some(user) = self.user_store.read(cx).current_user() {
for project in &self.local_participant.projects {
project_hosts_and_guest_counts
.entry(project.id)
.or_default()
.0 = Some(user.id);
}
}
project_hosts_and_guest_counts
.into_iter()
.filter_map(|(id, (host, guest_count))| Some((id, host?, guest_count)))
.max_by_key(|(_, _, guest_count)| *guest_count)
.map(|(id, host, _)| (id, host))
}
async fn handle_room_updated(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::RoomUpdated>,
@@ -651,6 +704,7 @@ impl Room {
let Some(peer_id) = participant.peer_id else {
continue;
};
let participant_index = ParticipantIndex(participant.participant_index);
this.participant_user_ids.insert(participant.user_id);
let old_projects = this
@@ -701,8 +755,9 @@ impl Room {
if let Some(remote_participant) =
this.remote_participants.get_mut(&participant.user_id)
{
remote_participant.projects = participant.projects;
remote_participant.peer_id = peer_id;
remote_participant.projects = participant.projects;
remote_participant.participant_index = participant_index;
if location != remote_participant.location {
remote_participant.location = location;
cx.emit(Event::ParticipantLocationChanged {
@@ -714,6 +769,7 @@ impl Room {
participant.user_id,
RemoteParticipant {
user: user.clone(),
participant_index,
peer_id,
projects: participant.projects,
location,
@@ -807,7 +863,17 @@ impl Room {
let _ = this.leave(cx);
}
this.user_store.update(cx, |user_store, cx| {
let participant_indices_by_user_id = this
.remote_participants
.iter()
.map(|(user_id, participant)| (*user_id, participant.participant_index))
.collect();
user_store.set_participant_indices(participant_indices_by_user_id, cx);
});
this.check_invariants();
this.room_update_completed_tx.try_send(Some(())).ok();
cx.notify();
});
}));
@@ -816,6 +882,17 @@ impl Room {
Ok(())
}
pub fn room_update_completed(&mut self) -> impl Future<Output = ()> {
let mut done_rx = self.room_update_completed_rx.clone();
async move {
while let Some(result) = done_rx.next().await {
if result.is_some() {
break;
}
}
}
}
fn remote_video_track_updated(
&mut self,
change: RemoteVideoTrackUpdate,
@@ -1003,6 +1080,7 @@ impl Room {
) -> Task<Result<ModelHandle<Project>>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
cx.emit(Event::RemoteProjectJoined { project_id: id });
cx.spawn(|this, mut cx| async move {
let project =
Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;

View File

@@ -23,6 +23,7 @@ language = { path = "../language" }
settings = { path = "../settings" }
feature_flags = { path = "../feature_flags" }
sum_tree = { path = "../sum_tree" }
clock = { path = "../clock" }
anyhow.workspace = true
futures.workspace = true
@@ -38,7 +39,7 @@ smol.workspace = true
thiserror.workspace = true
time.workspace = true
tiny_http = "0.8"
uuid = { version = "1.1.2", features = ["v4"] }
uuid.workspace = true
url = "2.2"
serde.workspace = true
serde_derive.workspace = true

View File

@@ -2,19 +2,21 @@ mod channel_buffer;
mod channel_chat;
mod channel_store;
pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent};
use client::{Client, UserStore};
use gpui::{AppContext, ModelHandle};
use std::sync::Arc;
pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL};
pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId};
pub use channel_store::{
Channel, ChannelData, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore,
};
use client::Client;
use std::sync::Arc;
#[cfg(test)]
mod channel_store_tests;
pub fn init(client: &Arc<Client>) {
pub fn init(client: &Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
channel_store::init(client, user_store, cx);
channel_buffer::init(client);
channel_chat::init(client);
}

View File

@@ -1,31 +1,39 @@
use crate::Channel;
use anyhow::Result;
use client::Client;
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle};
use rpc::{proto, TypedEnvelope};
use std::sync::Arc;
use client::{Client, Collaborator, UserStore};
use collections::HashMap;
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
use language::proto::serialize_version;
use rpc::{
proto::{self, PeerId},
TypedEnvelope,
};
use std::{sync::Arc, time::Duration};
use util::ResultExt;
pub const ACKNOWLEDGE_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(250);
pub(crate) fn init(client: &Arc<Client>) {
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer);
client.add_model_message_handler(ChannelBuffer::handle_add_channel_buffer_collaborator);
client.add_model_message_handler(ChannelBuffer::handle_remove_channel_buffer_collaborator);
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborator);
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborators);
}
pub struct ChannelBuffer {
pub(crate) channel: Arc<Channel>,
connected: bool,
collaborators: Vec<proto::Collaborator>,
collaborators: HashMap<PeerId, Collaborator>,
user_store: ModelHandle<UserStore>,
buffer: ModelHandle<language::Buffer>,
buffer_epoch: u64,
client: Arc<Client>,
subscription: Option<client::Subscription>,
acknowledge_task: Option<Task<Result<()>>>,
}
pub enum ChannelBufferEvent {
CollaboratorsChanged,
Disconnected,
BufferEdited,
}
impl Entity for ChannelBuffer {
@@ -33,6 +41,9 @@ impl Entity for ChannelBuffer {
fn release(&mut self, _: &mut AppContext) {
if self.connected {
if let Some(task) = self.acknowledge_task.take() {
task.detach();
}
self.client
.send(proto::LeaveChannelBuffer {
channel_id: self.channel.id,
@@ -46,6 +57,7 @@ impl ChannelBuffer {
pub(crate) async fn new(
channel: Arc<Channel>,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
mut cx: AsyncAppContext,
) -> Result<ModelHandle<Self>> {
let response = client
@@ -61,8 +73,6 @@ impl ChannelBuffer {
.map(language::proto::deserialize_operation)
.collect::<Result<Vec<_>, _>>()?;
let collaborators = response.collaborators;
let buffer = cx.add_model(|_| {
language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text)
});
@@ -73,34 +83,50 @@ impl ChannelBuffer {
anyhow::Ok(cx.add_model(|cx| {
cx.subscribe(&buffer, Self::on_buffer_update).detach();
Self {
let mut this = Self {
buffer,
buffer_epoch: response.epoch,
client,
connected: true,
collaborators,
collaborators: Default::default(),
acknowledge_task: None,
channel,
subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())),
}
user_store,
};
this.replace_collaborators(response.collaborators, cx);
this
}))
}
pub fn remote_id(&self, cx: &AppContext) -> u64 {
self.buffer.read(cx).remote_id()
}
pub fn user_store(&self) -> &ModelHandle<UserStore> {
&self.user_store
}
pub(crate) fn replace_collaborators(
&mut self,
collaborators: Vec<proto::Collaborator>,
cx: &mut ModelContext<Self>,
) {
for old_collaborator in &self.collaborators {
if collaborators
.iter()
.any(|c| c.replica_id == old_collaborator.replica_id)
{
let mut new_collaborators = HashMap::default();
for collaborator in collaborators {
if let Ok(collaborator) = Collaborator::from_proto(collaborator) {
new_collaborators.insert(collaborator.peer_id, collaborator);
}
}
for (_, old_collaborator) in &self.collaborators {
if !new_collaborators.contains_key(&old_collaborator.peer_id) {
self.buffer.update(cx, |buffer, cx| {
buffer.remove_peer(old_collaborator.replica_id as u16, cx)
});
}
}
self.collaborators = collaborators;
self.collaborators = new_collaborators;
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
cx.notify();
}
@@ -127,64 +153,14 @@ impl ChannelBuffer {
Ok(())
}
async fn handle_add_channel_buffer_collaborator(
async fn handle_update_channel_buffer_collaborators(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::AddChannelBufferCollaborator>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
let collaborator = envelope.payload.collaborator.ok_or_else(|| {
anyhow::anyhow!(
"Should have gotten a collaborator in the AddChannelBufferCollaborator message"
)
})?;
this.update(&mut cx, |this, cx| {
this.collaborators.push(collaborator);
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
cx.notify();
});
Ok(())
}
async fn handle_remove_channel_buffer_collaborator(
this: ModelHandle<Self>,
message: TypedEnvelope<proto::RemoveChannelBufferCollaborator>,
message: TypedEnvelope<proto::UpdateChannelBufferCollaborators>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.collaborators.retain(|collaborator| {
if collaborator.peer_id == message.payload.peer_id {
this.buffer.update(cx, |buffer, cx| {
buffer.remove_peer(collaborator.replica_id as u16, cx)
});
false
} else {
true
}
});
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
cx.notify();
});
Ok(())
}
async fn handle_update_channel_buffer_collaborator(
this: ModelHandle<Self>,
message: TypedEnvelope<proto::UpdateChannelBufferCollaborator>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
for collaborator in &mut this.collaborators {
if collaborator.peer_id == message.payload.old_peer_id {
collaborator.peer_id = message.payload.new_peer_id;
break;
}
}
this.replace_collaborators(message.payload.collaborators, cx);
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
cx.notify();
});
@@ -196,19 +172,45 @@ impl ChannelBuffer {
&mut self,
_: ModelHandle<language::Buffer>,
event: &language::Event,
_: &mut ModelContext<Self>,
cx: &mut ModelContext<Self>,
) {
if let language::Event::Operation(operation) = event {
let operation = language::proto::serialize_operation(operation);
self.client
.send(proto::UpdateChannelBuffer {
channel_id: self.channel.id,
operations: vec![operation],
})
.log_err();
match event {
language::Event::Operation(operation) => {
let operation = language::proto::serialize_operation(operation);
self.client
.send(proto::UpdateChannelBuffer {
channel_id: self.channel.id,
operations: vec![operation],
})
.log_err();
}
language::Event::Edited => {
cx.emit(ChannelBufferEvent::BufferEdited);
}
_ => {}
}
}
pub fn acknowledge_buffer_version(&mut self, cx: &mut ModelContext<'_, ChannelBuffer>) {
let buffer = self.buffer.read(cx);
let version = buffer.version();
let buffer_id = buffer.remote_id();
let client = self.client.clone();
let epoch = self.epoch();
self.acknowledge_task = Some(cx.spawn_weak(|_, cx| async move {
cx.background().timer(ACKNOWLEDGE_DEBOUNCE_INTERVAL).await;
client
.send(proto::AckBufferOperation {
buffer_id,
epoch,
version: serialize_version(&version),
})
.ok();
Ok(())
}));
}
pub fn epoch(&self) -> u64 {
self.buffer_epoch
}
@@ -217,7 +219,7 @@ impl ChannelBuffer {
self.buffer.clone()
}
pub fn collaborators(&self) -> &[proto::Collaborator] {
pub fn collaborators(&self) -> &HashMap<PeerId, Collaborator> {
&self.collaborators
}

View File

@@ -1,4 +1,4 @@
use crate::Channel;
use crate::{Channel, ChannelId, ChannelStore};
use anyhow::{anyhow, Result};
use client::{
proto,
@@ -16,7 +16,9 @@ use util::{post_inc, ResultExt as _, TryFutureExt};
pub struct ChannelChat {
channel: Arc<Channel>,
messages: SumTree<ChannelMessage>,
channel_store: ModelHandle<ChannelStore>,
loaded_all_messages: bool,
last_acknowledged_id: Option<u64>,
next_pending_message_id: usize,
user_store: ModelHandle<UserStore>,
rpc: Arc<Client>,
@@ -34,7 +36,7 @@ pub struct ChannelMessage {
pub nonce: u128,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ChannelMessageId {
Saved(u64),
Pending(usize),
@@ -55,6 +57,10 @@ pub enum ChannelChatEvent {
old_range: Range<usize>,
new_count: usize,
},
NewMessage {
channel_id: ChannelId,
message_id: u64,
},
}
pub fn init(client: &Arc<Client>) {
@@ -77,6 +83,7 @@ impl Entity for ChannelChat {
impl ChannelChat {
pub async fn new(
channel: Arc<Channel>,
channel_store: ModelHandle<ChannelStore>,
user_store: ModelHandle<UserStore>,
client: Arc<Client>,
mut cx: AsyncAppContext,
@@ -94,11 +101,13 @@ impl ChannelChat {
let mut this = Self {
channel,
user_store,
channel_store,
rpc: client,
outgoing_messages_lock: Default::default(),
messages: Default::default(),
loaded_all_messages,
next_pending_message_id: 0,
last_acknowledged_id: None,
rng: StdRng::from_entropy(),
_subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()),
};
@@ -219,6 +228,26 @@ impl ChannelChat {
false
}
pub fn acknowledge_last_message(&mut self, cx: &mut ModelContext<Self>) {
if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id {
if self
.last_acknowledged_id
.map_or(true, |acknowledged_id| acknowledged_id < latest_message_id)
{
self.rpc
.send(proto::AckChannelMessage {
channel_id: self.channel.id,
message_id: latest_message_id,
})
.ok();
self.last_acknowledged_id = Some(latest_message_id);
self.channel_store.update(cx, |store, cx| {
store.acknowledge_message_id(self.channel.id, latest_message_id, cx);
});
}
}
}
pub fn rejoin(&mut self, cx: &mut ModelContext<Self>) {
let user_store = self.user_store.clone();
let rpc = self.rpc.clone();
@@ -313,10 +342,15 @@ impl ChannelChat {
.payload
.message
.ok_or_else(|| anyhow!("empty message"))?;
let message_id = message.id;
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
this.update(&mut cx, |this, cx| {
this.insert_messages(SumTree::from_item(message, &()), cx)
this.insert_messages(SumTree::from_item(message, &()), cx);
cx.emit(ChannelChatEvent::NewMessage {
channel_id: this.channel.id,
message_id,
})
});
Ok(())
@@ -388,6 +422,7 @@ impl ChannelChat {
old_range: start_ix..end_ix,
new_count,
});
cx.notify();
}
}

View File

@@ -2,11 +2,10 @@ mod channel_index;
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat};
use anyhow::{anyhow, Result};
use channel_index::ChannelIndex;
use client::{Client, Subscription, User, UserId, UserStore};
use collections::{
hash_map::{self, DefaultHasher},
HashMap, HashSet,
};
use collections::{hash_map, HashMap, HashSet};
use db::RELEASE_CHANNEL;
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
use rpc::{
@@ -14,17 +13,14 @@ use rpc::{
TypedEnvelope,
};
use serde_derive::{Deserialize, Serialize};
use std::{
borrow::Cow,
hash::{Hash, Hasher},
mem,
ops::Deref,
sync::Arc,
time::Duration,
};
use std::{borrow::Cow, hash::Hash, mem, ops::Deref, sync::Arc, time::Duration};
use util::ResultExt;
use self::channel_index::ChannelIndex;
pub fn init(client: &Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
let channel_store =
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
cx.set_global(channel_store);
}
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
@@ -53,6 +49,28 @@ pub type ChannelData = (Channel, ChannelPath);
pub struct Channel {
pub id: ChannelId,
pub name: String,
pub unseen_note_version: Option<(u64, clock::Global)>,
pub unseen_message_id: Option<u64>,
}
impl Channel {
pub fn link(&self) -> String {
RELEASE_CHANNEL.link_prefix().to_owned()
+ "channel/"
+ &self.slug()
+ "-"
+ &self.id.to_string()
}
pub fn slug(&self) -> String {
let slug: String = self
.name
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '-' })
.collect();
slug.trim_matches(|c| c == '-').to_string()
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
@@ -79,6 +97,10 @@ enum OpenedModelHandle<E: Entity> {
}
impl ChannelStore {
pub fn global(cx: &AppContext) -> ModelHandle<Self> {
cx.global::<ModelHandle<Self>>().clone()
}
pub fn new(
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
@@ -92,12 +114,21 @@ impl ChannelStore {
let watch_connection_status = cx.spawn_weak(|this, mut cx| async move {
while let Some(status) = connection_status.next().await {
let this = this.upgrade(&cx)?;
match status {
client::Status::Connected { .. } => {
this.update(&mut cx, |this, cx| this.handle_connect(cx))
.await
.log_err()?;
}
client::Status::SignedOut | client::Status::UpgradeRequired => {
this.update(&mut cx, |this, cx| this.handle_disconnect(false, cx));
}
_ => {
this.update(&mut cx, |this, cx| this.handle_disconnect(true, cx));
}
}
if status.is_connected() {
this.update(&mut cx, |this, cx| this.handle_connect(cx))
.await
.log_err()?;
} else {
this.update(&mut cx, |this, cx| this.handle_disconnect(cx));
}
}
Some(())
@@ -208,14 +239,73 @@ impl ChannelStore {
cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<ChannelBuffer>>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
self.open_channel_resource(
channel_id,
|this| &mut this.opened_buffers,
|channel, cx| ChannelBuffer::new(channel, client, cx),
|channel, cx| ChannelBuffer::new(channel, client, user_store, cx),
cx,
)
}
pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> Option<bool> {
self.channel_index
.by_id()
.get(&channel_id)
.map(|channel| channel.unseen_note_version.is_some())
}
pub fn has_new_messages(&self, channel_id: ChannelId) -> Option<bool> {
self.channel_index
.by_id()
.get(&channel_id)
.map(|channel| channel.unseen_message_id.is_some())
}
pub fn notes_changed(
&mut self,
channel_id: ChannelId,
epoch: u64,
version: &clock::Global,
cx: &mut ModelContext<Self>,
) {
self.channel_index.note_changed(channel_id, epoch, version);
cx.notify();
}
pub fn new_message(
&mut self,
channel_id: ChannelId,
message_id: u64,
cx: &mut ModelContext<Self>,
) {
self.channel_index.new_message(channel_id, message_id);
cx.notify();
}
pub fn acknowledge_message_id(
&mut self,
channel_id: ChannelId,
message_id: u64,
cx: &mut ModelContext<Self>,
) {
self.channel_index
.acknowledge_message_id(channel_id, message_id);
cx.notify();
}
pub fn acknowledge_notes_version(
&mut self,
channel_id: ChannelId,
epoch: u64,
version: &clock::Global,
cx: &mut ModelContext<Self>,
) {
self.channel_index
.acknowledge_note_version(channel_id, epoch, version);
cx.notify();
}
pub fn open_channel_chat(
&mut self,
channel_id: ChannelId,
@@ -223,10 +313,11 @@ impl ChannelStore {
) -> Task<Result<ModelHandle<ChannelChat>>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
let this = cx.handle();
self.open_channel_resource(
channel_id,
|this| &mut this.opened_chats,
|channel, cx| ChannelChat::new(channel, user_store, client, cx),
|channel, cx| ChannelChat::new(channel, this, user_store, client, cx),
cx,
)
}
@@ -741,7 +832,7 @@ impl ChannelStore {
})
}
fn handle_disconnect(&mut self, cx: &mut ModelContext<Self>) {
fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext<Self>) {
self.channel_index.clear();
self.channel_invitations.clear();
self.channel_participants.clear();
@@ -752,7 +843,10 @@ impl ChannelStore {
self.disconnect_channel_buffers_task.get_or_insert_with(|| {
cx.spawn_weak(|this, mut cx| async move {
cx.background().timer(RECONNECT_TIMEOUT).await;
if wait_for_reconnect {
cx.background().timer(RECONNECT_TIMEOUT).await;
}
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
for (_, buffer) in this.opened_buffers.drain() {
@@ -788,6 +882,8 @@ impl ChannelStore {
Arc::new(Channel {
id: channel.id,
name: channel.name,
unseen_note_version: None,
unseen_message_id: None,
}),
),
}
@@ -796,7 +892,9 @@ impl ChannelStore {
let channels_changed = !payload.channels.is_empty()
|| !payload.delete_channels.is_empty()
|| !payload.insert_edge.is_empty()
|| !payload.delete_edge.is_empty();
|| !payload.delete_edge.is_empty()
|| !payload.unseen_channel_messages.is_empty()
|| !payload.unseen_channel_buffer_changes.is_empty();
if channels_changed {
if !payload.delete_channels.is_empty() {
@@ -823,6 +921,22 @@ impl ChannelStore {
index.insert(channel)
}
for unseen_buffer_change in payload.unseen_channel_buffer_changes {
let version = language::proto::deserialize_version(&unseen_buffer_change.version);
index.note_changed(
unseen_buffer_change.channel_id,
unseen_buffer_change.epoch,
&version,
);
}
for unseen_channel_message in payload.unseen_channel_messages {
index.new_messages(
unseen_channel_message.channel_id,
unseen_channel_message.message_id,
);
}
for edge in payload.insert_edge {
index.insert_edge(edge.channel_id, edge.parent_id);
}
@@ -910,12 +1024,6 @@ impl ChannelPath {
pub fn channel_id(&self) -> ChannelId {
self.0[self.0.len() - 1]
}
pub fn unique_id(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.0.deref().hash(&mut hasher);
hasher.finish()
}
}
impl From<ChannelPath> for Cow<'static, ChannelPath> {

View File

@@ -38,6 +38,43 @@ impl ChannelIndex {
channels_by_id: &mut self.channels_by_id,
}
}
pub fn acknowledge_note_version(
&mut self,
channel_id: ChannelId,
epoch: u64,
version: &clock::Global,
) {
if let Some(channel) = self.channels_by_id.get_mut(&channel_id) {
let channel = Arc::make_mut(channel);
if let Some((unseen_epoch, unseen_version)) = &channel.unseen_note_version {
if epoch > *unseen_epoch
|| epoch == *unseen_epoch && version.observed_all(unseen_version)
{
channel.unseen_note_version = None;
}
}
}
}
pub fn acknowledge_message_id(&mut self, channel_id: ChannelId, message_id: u64) {
if let Some(channel) = self.channels_by_id.get_mut(&channel_id) {
let channel = Arc::make_mut(channel);
if let Some(unseen_message_id) = channel.unseen_message_id {
if message_id >= unseen_message_id {
channel.unseen_message_id = None;
}
}
}
}
pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) {
insert_note_changed(&mut self.channels_by_id, channel_id, epoch, version);
}
pub fn new_message(&mut self, channel_id: ChannelId, message_id: u64) {
insert_new_message(&mut self.channels_by_id, channel_id, message_id)
}
}
impl Deref for ChannelIndex {
@@ -76,6 +113,14 @@ impl<'a> ChannelPathsInsertGuard<'a> {
}
}
pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) {
insert_note_changed(&mut self.channels_by_id, channel_id, epoch, &version);
}
pub fn new_messages(&mut self, channel_id: ChannelId, message_id: u64) {
insert_new_message(&mut self.channels_by_id, channel_id, message_id)
}
pub fn insert(&mut self, channel_proto: proto::Channel) {
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
Arc::make_mut(existing_channel).name = channel_proto.name;
@@ -85,6 +130,8 @@ impl<'a> ChannelPathsInsertGuard<'a> {
Arc::new(Channel {
id: channel_proto.id,
name: channel_proto.name,
unseen_note_version: None,
unseen_message_id: None,
}),
);
self.insert_root(channel_proto.id);
@@ -160,3 +207,32 @@ fn channel_path_sorting_key<'a>(
path.iter()
.map(|id| Some(channels_by_id.get(id)?.name.as_str()))
}
fn insert_note_changed(
channels_by_id: &mut BTreeMap<ChannelId, Arc<Channel>>,
channel_id: u64,
epoch: u64,
version: &clock::Global,
) {
if let Some(channel) = channels_by_id.get_mut(&channel_id) {
let unseen_version = Arc::make_mut(channel)
.unseen_note_version
.get_or_insert((0, clock::Global::new()));
if epoch > unseen_version.0 {
*unseen_version = (epoch, version.clone());
} else {
unseen_version.1.join(&version);
}
}
}
fn insert_new_message(
channels_by_id: &mut BTreeMap<ChannelId, Arc<Channel>>,
channel_id: u64,
message_id: u64,
) {
if let Some(channel) = channels_by_id.get_mut(&channel_id) {
let unseen_message_id = Arc::make_mut(channel).unseen_message_id.get_or_insert(0);
*unseen_message_id = message_id.max(*unseen_message_id);
}
}

View File

@@ -340,10 +340,10 @@ fn init_test(cx: &mut AppContext) -> ModelHandle<ChannelStore> {
cx.foreground().forbid_parking();
cx.set_global(SettingsStore::test(cx));
crate::init(&client);
client::init(&client, cx);
crate::init(&client, user_store, cx);
cx.add_model(|cx| ChannelStore::new(client, user_store, cx))
ChannelStore::global(cx)
}
fn update_channels(

View File

@@ -182,6 +182,7 @@ impl Bundle {
kCFStringEncodingUTF8,
ptr::null(),
));
// equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
LSOpenFromURLSpec(
&LSLaunchURLSpec {

View File

@@ -33,15 +33,16 @@ parking_lot.workspace = true
postage.workspace = true
rand.workspace = true
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
smol.workspace = true
sysinfo.workspace = true
tempfile = "3"
thiserror.workspace = true
time.workspace = true
tiny_http = "0.8"
uuid = { version = "1.1.2", features = ["v4"] }
uuid.workspace = true
url = "2.2"
serde.workspace = true
serde_derive.workspace = true
tempfile = "3"
[dev-dependencies]
collections = { path = "../collections", features = ["test-support"] }

View File

@@ -34,7 +34,7 @@ use std::{
future::Future,
marker::PhantomData,
path::PathBuf,
sync::{Arc, Weak},
sync::{atomic::AtomicU64, Arc, Weak},
time::{Duration, Instant},
};
use telemetry::Telemetry;
@@ -62,13 +62,15 @@ lazy_static! {
.and_then(|v| v.parse().ok());
pub static ref ZED_APP_PATH: Option<PathBuf> =
std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
pub static ref ZED_ALWAYS_ACTIVE: bool =
std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| e.len() > 0);
}
pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
actions!(client, [SignIn, SignOut]);
actions!(client, [SignIn, SignOut, Reconnect]);
pub fn init_settings(cx: &mut AppContext) {
settings::register::<TelemetrySettings>(cx);
@@ -100,10 +102,21 @@ pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
}
}
});
cx.add_global_action({
let client = client.clone();
move |_: &Reconnect, cx| {
if let Some(client) = client.upgrade() {
cx.spawn(|cx| async move {
client.reconnect(&cx);
})
.detach();
}
}
});
}
pub struct Client {
id: usize,
id: AtomicU64,
peer: Arc<Peer>,
http: Arc<dyn HttpClient>,
telemetry: Arc<Telemetry>,
@@ -372,7 +385,7 @@ impl settings::Setting for TelemetrySettings {
impl Client {
pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
Arc::new(Self {
id: 0,
id: AtomicU64::new(0),
peer: Peer::new(0),
telemetry: Telemetry::new(http.clone(), cx),
http,
@@ -385,17 +398,16 @@ impl Client {
})
}
pub fn id(&self) -> usize {
self.id
pub fn id(&self) -> u64 {
self.id.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn http_client(&self) -> Arc<dyn HttpClient> {
self.http.clone()
}
#[cfg(any(test, feature = "test-support"))]
pub fn set_id(&mut self, id: usize) -> &Self {
self.id = id;
pub fn set_id(&self, id: u64) -> &Self {
self.id.store(id, std::sync::atomic::Ordering::SeqCst);
self
}
@@ -452,7 +464,7 @@ impl Client {
}
fn set_status(self: &Arc<Self>, status: Status, cx: &AsyncAppContext) {
log::info!("set status on client {}: {:?}", self.id, status);
log::info!("set status on client {}: {:?}", self.id(), status);
let mut state = self.state.write();
*state.status.0.borrow_mut() = status;
@@ -803,6 +815,7 @@ impl Client {
}
}
let credentials = credentials.unwrap();
self.set_id(credentials.user_id);
if was_disconnected {
self.set_status(Status::Connecting, cx);
@@ -1210,6 +1223,11 @@ impl Client {
self.set_status(Status::SignedOut, cx);
}
pub fn reconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
self.peer.teardown();
self.set_status(Status::ConnectionLost, cx);
}
fn connection_id(&self) -> Result<ConnectionId> {
if let Status::Connected { connection_id, .. } = *self.status().borrow() {
Ok(connection_id)
@@ -1219,7 +1237,7 @@ impl Client {
}
pub fn send<T: EnvelopedMessage>(&self, message: T) -> Result<()> {
log::debug!("rpc send. client_id:{}, name:{}", self.id, T::NAME);
log::debug!("rpc send. client_id:{}, name:{}", self.id(), T::NAME);
self.peer.send(self.connection_id()?, message)
}
@@ -1235,7 +1253,7 @@ impl Client {
&self,
request: T,
) -> impl Future<Output = Result<TypedEnvelope<T::Response>>> {
let client_id = self.id;
let client_id = self.id();
log::debug!(
"rpc request start. client_id:{}. name:{}",
client_id,
@@ -1256,7 +1274,7 @@ impl Client {
}
fn respond<T: RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) -> Result<()> {
log::debug!("rpc respond. client_id:{}. name:{}", self.id, T::NAME);
log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME);
self.peer.respond(receipt, response)
}
@@ -1265,7 +1283,7 @@ impl Client {
receipt: Receipt<T>,
error: proto::Error,
) -> Result<()> {
log::debug!("rpc respond. client_id:{}. name:{}", self.id, T::NAME);
log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME);
self.peer.respond_with_error(receipt, error)
}
@@ -1334,7 +1352,7 @@ impl Client {
if let Some(handler) = handler {
let future = handler(subscriber, message, &self, cx.clone());
let client_id = self.id;
let client_id = self.id();
log::debug!(
"rpc message received. client_id:{}, sender_id:{:?}, type:{}",
client_id,

View File

@@ -4,6 +4,7 @@ use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Serialize;
use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
use tempfile::NamedTempFile;
use util::http::HttpClient;
use util::{channel::ReleaseChannel, TryFutureExt};
@@ -17,7 +18,8 @@ pub struct Telemetry {
#[derive(Default)]
struct TelemetryState {
metrics_id: Option<Arc<str>>, // Per logged-in user
installation_id: Option<Arc<str>>, // Per app installation
installation_id: Option<Arc<str>>, // Per app installation (different for dev, preview, and stable)
session_id: Option<Arc<str>>, // Per app launch
app_version: Option<Arc<str>>,
release_channel: Option<&'static str>,
os_name: &'static str,
@@ -40,6 +42,7 @@ lazy_static! {
struct ClickhouseEventRequestBody {
token: &'static str,
installation_id: Option<Arc<str>>,
session_id: Option<Arc<str>>,
is_staff: Option<bool>,
app_version: Option<Arc<str>>,
os_name: &'static str,
@@ -88,6 +91,14 @@ pub enum ClickhouseEvent {
kind: AssistantKind,
model: &'static str,
},
Cpu {
usage_as_percentage: f32,
core_count: u32,
},
Memory {
memory_in_bytes: u64,
virtual_memory_in_bytes: u64,
},
}
#[cfg(debug_assertions)]
@@ -122,6 +133,7 @@ impl Telemetry {
release_channel,
installation_id: None,
metrics_id: None,
session_id: None,
clickhouse_events_queue: Default::default(),
flush_clickhouse_events_task: Default::default(),
log_file: None,
@@ -136,15 +148,61 @@ impl Telemetry {
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
}
pub fn start(self: &Arc<Self>, installation_id: Option<String>) {
pub fn start(
self: &Arc<Self>,
installation_id: Option<String>,
session_id: String,
cx: &mut AppContext,
) {
let mut state = self.state.lock();
state.installation_id = installation_id.map(|id| id.into());
state.session_id = Some(session_id.into());
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
drop(state);
if has_clickhouse_events {
self.flush_clickhouse_events();
}
let this = self.clone();
cx.spawn(|mut cx| async move {
let mut system = System::new_all();
system.refresh_all();
loop {
// Waiting some amount of time before the first query is important to get a reasonable value
// https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage
const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60);
smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
system.refresh_memory();
system.refresh_processes();
let current_process = Pid::from_u32(std::process::id());
let Some(process) = system.processes().get(&current_process) else {
let process = current_process;
log::error!("Failed to find own process {process:?} in system process table");
// TODO: Fire an error telemetry event
return;
};
let memory_event = ClickhouseEvent::Memory {
memory_in_bytes: process.memory(),
virtual_memory_in_bytes: process.virtual_memory(),
};
let cpu_event = ClickhouseEvent::Cpu {
usage_as_percentage: process.cpu_usage(),
core_count: system.cpus().len() as u32,
};
let telemetry_settings = cx.update(|cx| *settings::get::<TelemetrySettings>(cx));
this.report_clickhouse_event(memory_event, telemetry_settings);
this.report_clickhouse_event(cpu_event, telemetry_settings);
}
})
.detach();
}
pub fn set_authenticated_user_info(
@@ -230,22 +288,21 @@ impl Telemetry {
{
let state = this.state.lock();
json_bytes.clear();
serde_json::to_writer(
&mut json_bytes,
&ClickhouseEventRequestBody {
token: ZED_SECRET_CLIENT_TOKEN,
installation_id: state.installation_id.clone(),
is_staff: state.is_staff.clone(),
app_version: state.app_version.clone(),
os_name: state.os_name,
os_version: state.os_version.clone(),
architecture: state.architecture,
let request_body = ClickhouseEventRequestBody {
token: ZED_SECRET_CLIENT_TOKEN,
installation_id: state.installation_id.clone(),
session_id: state.session_id.clone(),
is_staff: state.is_staff.clone(),
app_version: state.app_version.clone(),
os_name: state.os_name,
os_version: state.os_version.clone(),
architecture: state.architecture,
release_channel: state.release_channel,
events,
},
)?;
release_channel: state.release_channel,
events,
};
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, &request_body)?;
}
this.http_client

View File

@@ -7,11 +7,15 @@ use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse};
use std::sync::{Arc, Weak};
use text::ReplicaId;
use util::http::HttpClient;
use util::TryFutureExt as _;
pub type UserId = u64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParticipantIndex(pub u32);
#[derive(Default, Debug)]
pub struct User {
pub id: UserId,
@@ -19,6 +23,13 @@ pub struct User {
pub avatar: Option<Arc<ImageData>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Collaborator {
pub peer_id: proto::PeerId,
pub replica_id: ReplicaId,
pub user_id: UserId,
}
impl PartialOrd for User {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
@@ -56,6 +67,7 @@ pub enum ContactRequestStatus {
pub struct UserStore {
users: HashMap<u64, Arc<User>>,
participant_indices: HashMap<u64, ParticipantIndex>,
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
current_user: watch::Receiver<Option<Arc<User>>>,
contacts: Vec<Arc<Contact>>,
@@ -81,6 +93,7 @@ pub enum Event {
kind: ContactEventKind,
},
ShowContacts,
ParticipantIndicesChanged,
}
#[derive(Clone, Copy)]
@@ -118,6 +131,7 @@ impl UserStore {
current_user: current_user_rx,
contacts: Default::default(),
incoming_contact_requests: Default::default(),
participant_indices: Default::default(),
outgoing_contact_requests: Default::default(),
invite_info: None,
client: Arc::downgrade(&client),
@@ -581,6 +595,10 @@ impl UserStore {
self.load_users(proto::FuzzySearchUsers { query }, cx)
}
pub fn get_cached_user(&self, user_id: u64) -> Option<Arc<User>> {
self.users.get(&user_id).cloned()
}
pub fn get_user(
&mut self,
user_id: u64,
@@ -641,6 +659,21 @@ impl UserStore {
}
})
}
pub fn set_participant_indices(
&mut self,
participant_indices: HashMap<u64, ParticipantIndex>,
cx: &mut ModelContext<Self>,
) {
if participant_indices != self.participant_indices {
self.participant_indices = participant_indices;
cx.emit(Event::ParticipantIndicesChanged);
}
}
pub fn participant_indices(&self) -> &HashMap<u64, ParticipantIndex> {
&self.participant_indices
}
}
impl User {
@@ -672,6 +705,16 @@ impl Contact {
}
}
impl Collaborator {
pub fn from_proto(message: proto::Collaborator) -> Result<Self> {
Ok(Self {
peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
replica_id: message.replica_id as ReplicaId,
user_id: message.user_id as UserId,
})
}
}
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
let mut response = http
.get(url, Default::default(), true)

52
crates/client2/Cargo.toml Normal file
View File

@@ -0,0 +1,52 @@
[package]
name = "client2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/client2.rs"
doctest = false
[features]
test-support = ["collections/test-support", "gpui2/test-support", "rpc/test-support"]
[dependencies]
collections = { path = "../collections" }
db = { path = "../db" }
gpui2 = { path = "../gpui2" }
util = { path = "../util" }
rpc = { path = "../rpc" }
text = { path = "../text" }
settings2 = { path = "../settings2" }
feature_flags2 = { path = "../feature_flags2" }
sum_tree = { path = "../sum_tree" }
anyhow.workspace = true
async-recursion = "0.3"
async-tungstenite = { version = "0.16", features = ["async-tls"] }
futures.workspace = true
image = "0.23"
lazy_static.workspace = true
log.workspace = true
parking_lot.workspace = true
postage.workspace = true
rand.workspace = true
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
smol.workspace = true
sysinfo.workspace = true
tempfile = "3"
thiserror.workspace = true
time.workspace = true
tiny_http = "0.8"
uuid.workspace = true
url = "2.2"
[dev-dependencies]
collections = { path = "../collections", features = ["test-support"] }
gpui2 = { path = "../gpui2", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,323 @@
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use gpui2::{serde_json, AppContext, AppMetadata, Executor, Task};
use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Serialize;
use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
use tempfile::NamedTempFile;
use util::http::HttpClient;
use util::{channel::ReleaseChannel, TryFutureExt};
pub struct Telemetry {
http_client: Arc<dyn HttpClient>,
executor: Executor,
state: Mutex<TelemetryState>,
}
struct TelemetryState {
metrics_id: Option<Arc<str>>, // Per logged-in user
installation_id: Option<Arc<str>>, // Per app installation (different for dev, preview, and stable)
session_id: Option<Arc<str>>, // Per app launch
release_channel: Option<&'static str>,
app_metadata: AppMetadata,
architecture: &'static str,
clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
flush_clickhouse_events_task: Option<Task<()>>,
log_file: Option<NamedTempFile>,
is_staff: Option<bool>,
}
const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
lazy_static! {
static ref CLICKHOUSE_EVENTS_URL: String =
format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH);
}
#[derive(Serialize, Debug)]
struct ClickhouseEventRequestBody {
token: &'static str,
installation_id: Option<Arc<str>>,
session_id: Option<Arc<str>>,
is_staff: Option<bool>,
app_version: Option<String>,
os_name: &'static str,
os_version: Option<String>,
architecture: &'static str,
release_channel: Option<&'static str>,
events: Vec<ClickhouseEventWrapper>,
}
#[derive(Serialize, Debug)]
struct ClickhouseEventWrapper {
signed_in: bool,
#[serde(flatten)]
event: ClickhouseEvent,
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum AssistantKind {
Panel,
Inline,
}
#[derive(Serialize, Debug)]
#[serde(tag = "type")]
pub enum ClickhouseEvent {
Editor {
operation: &'static str,
file_extension: Option<String>,
vim_mode: bool,
copilot_enabled: bool,
copilot_enabled_for_language: bool,
},
Copilot {
suggestion_id: Option<String>,
suggestion_accepted: bool,
file_extension: Option<String>,
},
Call {
operation: &'static str,
room_id: Option<u64>,
channel_id: Option<u64>,
},
Assistant {
conversation_id: Option<String>,
kind: AssistantKind,
model: &'static str,
},
Cpu {
usage_as_percentage: f32,
core_count: u32,
},
Memory {
memory_in_bytes: u64,
virtual_memory_in_bytes: u64,
},
}
#[cfg(debug_assertions)]
const MAX_QUEUE_LEN: usize = 1;
#[cfg(not(debug_assertions))]
const MAX_QUEUE_LEN: usize = 10;
#[cfg(debug_assertions)]
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
#[cfg(not(debug_assertions))]
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30);
impl Telemetry {
pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
let release_channel = if cx.has_global::<ReleaseChannel>() {
Some(cx.global::<ReleaseChannel>().display_name())
} else {
None
};
// TODO: Replace all hardware stuff with nested SystemSpecs json
let this = Arc::new(Self {
http_client: client,
executor: cx.executor().clone(),
state: Mutex::new(TelemetryState {
app_metadata: cx.app_metadata(),
architecture: env::consts::ARCH,
release_channel,
installation_id: None,
metrics_id: None,
session_id: None,
clickhouse_events_queue: Default::default(),
flush_clickhouse_events_task: Default::default(),
log_file: None,
is_staff: None,
}),
});
this
}
pub fn log_file_path(&self) -> Option<PathBuf> {
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
}
pub fn start(
self: &Arc<Self>,
installation_id: Option<String>,
session_id: String,
cx: &mut AppContext,
) {
let mut state = self.state.lock();
state.installation_id = installation_id.map(|id| id.into());
state.session_id = Some(session_id.into());
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
drop(state);
if has_clickhouse_events {
self.flush_clickhouse_events();
}
let this = self.clone();
cx.spawn(|cx| async move {
let mut system = System::new_all();
system.refresh_all();
loop {
// Waiting some amount of time before the first query is important to get a reasonable value
// https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage
const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60);
smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
system.refresh_memory();
system.refresh_processes();
let current_process = Pid::from_u32(std::process::id());
let Some(process) = system.processes().get(&current_process) else {
let process = current_process;
log::error!("Failed to find own process {process:?} in system process table");
// TODO: Fire an error telemetry event
return;
};
let memory_event = ClickhouseEvent::Memory {
memory_in_bytes: process.memory(),
virtual_memory_in_bytes: process.virtual_memory(),
};
let cpu_event = ClickhouseEvent::Cpu {
usage_as_percentage: process.cpu_usage(),
core_count: system.cpus().len() as u32,
};
let telemetry_settings = if let Ok(telemetry_settings) =
cx.update(|cx| *settings2::get::<TelemetrySettings>(cx))
{
telemetry_settings
} else {
break;
};
this.report_clickhouse_event(memory_event, telemetry_settings);
this.report_clickhouse_event(cpu_event, telemetry_settings);
}
})
.detach();
}
pub fn set_authenticated_user_info(
self: &Arc<Self>,
metrics_id: Option<String>,
is_staff: bool,
cx: &AppContext,
) {
if !settings2::get::<TelemetrySettings>(cx).metrics {
return;
}
let mut state = self.state.lock();
let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
state.metrics_id = metrics_id.clone();
state.is_staff = Some(is_staff);
drop(state);
}
pub fn report_clickhouse_event(
self: &Arc<Self>,
event: ClickhouseEvent,
telemetry_settings: TelemetrySettings,
) {
if !telemetry_settings.metrics {
return;
}
let mut state = self.state.lock();
let signed_in = state.metrics_id.is_some();
state
.clickhouse_events_queue
.push(ClickhouseEventWrapper { signed_in, event });
if state.installation_id.is_some() {
if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
drop(state);
self.flush_clickhouse_events();
} else {
let this = self.clone();
let executor = self.executor.clone();
state.flush_clickhouse_events_task = Some(self.executor.spawn(async move {
executor.timer(DEBOUNCE_INTERVAL).await;
this.flush_clickhouse_events();
}));
}
}
}
pub fn metrics_id(self: &Arc<Self>) -> Option<Arc<str>> {
self.state.lock().metrics_id.clone()
}
pub fn installation_id(self: &Arc<Self>) -> Option<Arc<str>> {
self.state.lock().installation_id.clone()
}
pub fn is_staff(self: &Arc<Self>) -> Option<bool> {
self.state.lock().is_staff
}
fn flush_clickhouse_events(self: &Arc<Self>) {
let mut state = self.state.lock();
let mut events = mem::take(&mut state.clickhouse_events_queue);
state.flush_clickhouse_events_task.take();
drop(state);
let this = self.clone();
self.executor
.spawn(
async move {
let mut json_bytes = Vec::new();
if let Some(file) = &mut this.state.lock().log_file {
let file = file.as_file_mut();
for event in &mut events {
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, event)?;
file.write_all(&json_bytes)?;
file.write(b"\n")?;
}
}
{
let state = this.state.lock();
let request_body = ClickhouseEventRequestBody {
token: ZED_SECRET_CLIENT_TOKEN,
installation_id: state.installation_id.clone(),
session_id: state.session_id.clone(),
is_staff: state.is_staff.clone(),
app_version: state
.app_metadata
.app_version
.map(|version| version.to_string()),
os_name: state.app_metadata.os_name,
os_version: state
.app_metadata
.os_version
.map(|version| version.to_string()),
architecture: state.architecture,
release_channel: state.release_channel,
events,
};
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, &request_body)?;
}
this.http_client
.post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into())
.await?;
anyhow::Ok(())
}
.log_err(),
)
.detach();
}
}

215
crates/client2/src/test.rs Normal file
View File

@@ -0,0 +1,215 @@
// use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
// use anyhow::{anyhow, Result};
// use futures::{stream::BoxStream, StreamExt};
// use gpui2::{Executor, Handle, TestAppContext};
// use parking_lot::Mutex;
// use rpc::{
// proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse},
// ConnectionId, Peer, Receipt, TypedEnvelope,
// };
// use std::{rc::Rc, sync::Arc};
// use util::http::FakeHttpClient;
// pub struct FakeServer {
// peer: Arc<Peer>,
// state: Arc<Mutex<FakeServerState>>,
// user_id: u64,
// executor: Executor,
// }
// #[derive(Default)]
// struct FakeServerState {
// incoming: Option<BoxStream<'static, Box<dyn proto::AnyTypedEnvelope>>>,
// connection_id: Option<ConnectionId>,
// forbid_connections: bool,
// auth_count: usize,
// access_token: usize,
// }
// impl FakeServer {
// pub async fn for_client(
// client_user_id: u64,
// client: &Arc<Client>,
// cx: &TestAppContext,
// ) -> Self {
// let server = Self {
// peer: Peer::new(0),
// state: Default::default(),
// user_id: client_user_id,
// executor: cx.foreground(),
// };
// client
// .override_authenticate({
// let state = Arc::downgrade(&server.state);
// move |cx| {
// let state = state.clone();
// cx.spawn(move |_| async move {
// let state = state.upgrade().ok_or_else(|| anyhow!("server dropped"))?;
// let mut state = state.lock();
// state.auth_count += 1;
// let access_token = state.access_token.to_string();
// Ok(Credentials {
// user_id: client_user_id,
// access_token,
// })
// })
// }
// })
// .override_establish_connection({
// let peer = Arc::downgrade(&server.peer);
// let state = Arc::downgrade(&server.state);
// move |credentials, cx| {
// let peer = peer.clone();
// let state = state.clone();
// let credentials = credentials.clone();
// cx.spawn(move |cx| async move {
// let state = state.upgrade().ok_or_else(|| anyhow!("server dropped"))?;
// let peer = peer.upgrade().ok_or_else(|| anyhow!("server dropped"))?;
// if state.lock().forbid_connections {
// Err(EstablishConnectionError::Other(anyhow!(
// "server is forbidding connections"
// )))?
// }
// assert_eq!(credentials.user_id, client_user_id);
// if credentials.access_token != state.lock().access_token.to_string() {
// Err(EstablishConnectionError::Unauthorized)?
// }
// let (client_conn, server_conn, _) = Connection::in_memory(cx.background());
// let (connection_id, io, incoming) =
// peer.add_test_connection(server_conn, cx.background());
// cx.background().spawn(io).detach();
// {
// let mut state = state.lock();
// state.connection_id = Some(connection_id);
// state.incoming = Some(incoming);
// }
// peer.send(
// connection_id,
// proto::Hello {
// peer_id: Some(connection_id.into()),
// },
// )
// .unwrap();
// Ok(client_conn)
// })
// }
// });
// client
// .authenticate_and_connect(false, &cx.to_async())
// .await
// .unwrap();
// server
// }
// pub fn disconnect(&self) {
// if self.state.lock().connection_id.is_some() {
// self.peer.disconnect(self.connection_id());
// let mut state = self.state.lock();
// state.connection_id.take();
// state.incoming.take();
// }
// }
// pub fn auth_count(&self) -> usize {
// self.state.lock().auth_count
// }
// pub fn roll_access_token(&self) {
// self.state.lock().access_token += 1;
// }
// pub fn forbid_connections(&self) {
// self.state.lock().forbid_connections = true;
// }
// pub fn allow_connections(&self) {
// self.state.lock().forbid_connections = false;
// }
// pub fn send<T: proto::EnvelopedMessage>(&self, message: T) {
// self.peer.send(self.connection_id(), message).unwrap();
// }
// #[allow(clippy::await_holding_lock)]
// pub async fn receive<M: proto::EnvelopedMessage>(&self) -> Result<TypedEnvelope<M>> {
// self.executor.start_waiting();
// loop {
// let message = self
// .state
// .lock()
// .incoming
// .as_mut()
// .expect("not connected")
// .next()
// .await
// .ok_or_else(|| anyhow!("other half hung up"))?;
// self.executor.finish_waiting();
// let type_name = message.payload_type_name();
// let message = message.into_any();
// if message.is::<TypedEnvelope<M>>() {
// return Ok(*message.downcast().unwrap());
// }
// if message.is::<TypedEnvelope<GetPrivateUserInfo>>() {
// self.respond(
// message
// .downcast::<TypedEnvelope<GetPrivateUserInfo>>()
// .unwrap()
// .receipt(),
// GetPrivateUserInfoResponse {
// metrics_id: "the-metrics-id".into(),
// staff: false,
// flags: Default::default(),
// },
// );
// continue;
// }
// panic!(
// "fake server received unexpected message type: {:?}",
// type_name
// );
// }
// }
// pub fn respond<T: proto::RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) {
// self.peer.respond(receipt, response).unwrap()
// }
// fn connection_id(&self) -> ConnectionId {
// self.state.lock().connection_id.expect("not connected")
// }
// pub async fn build_user_store(
// &self,
// client: Arc<Client>,
// cx: &mut TestAppContext,
// ) -> ModelHandle<UserStore> {
// let http_client = FakeHttpClient::with_404_response();
// let user_store = cx.add_model(|cx| UserStore::new(client, http_client, cx));
// assert_eq!(
// self.receive::<proto::GetUsers>()
// .await
// .unwrap()
// .payload
// .user_ids,
// &[self.user_id]
// );
// user_store
// }
// }
// impl Drop for FakeServer {
// fn drop(&mut self) {
// self.disconnect();
// }
// }

739
crates/client2/src/user.rs Normal file
View File

@@ -0,0 +1,739 @@
use super::{proto, Client, Status, TypedEnvelope};
use anyhow::{anyhow, Context, Result};
use collections::{hash_map::Entry, HashMap, HashSet};
use feature_flags2::FeatureFlagAppExt;
use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
use gpui2::{AsyncAppContext, EventEmitter, Handle, ImageData, ModelContext, Task};
use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse};
use std::sync::{Arc, Weak};
use text::ReplicaId;
use util::http::HttpClient;
use util::TryFutureExt as _;
pub type UserId = u64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParticipantIndex(pub u32);
#[derive(Default, Debug)]
pub struct User {
pub id: UserId,
pub github_login: String,
pub avatar: Option<Arc<ImageData>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Collaborator {
pub peer_id: proto::PeerId,
pub replica_id: ReplicaId,
pub user_id: UserId,
}
impl PartialOrd for User {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for User {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.github_login.cmp(&other.github_login)
}
}
impl PartialEq for User {
fn eq(&self, other: &Self) -> bool {
self.id == other.id && self.github_login == other.github_login
}
}
impl Eq for User {}
#[derive(Debug, PartialEq)]
pub struct Contact {
pub user: Arc<User>,
pub online: bool,
pub busy: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContactRequestStatus {
None,
RequestSent,
RequestReceived,
RequestAccepted,
}
pub struct UserStore {
users: HashMap<u64, Arc<User>>,
participant_indices: HashMap<u64, ParticipantIndex>,
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
current_user: watch::Receiver<Option<Arc<User>>>,
contacts: Vec<Arc<Contact>>,
incoming_contact_requests: Vec<Arc<User>>,
outgoing_contact_requests: Vec<Arc<User>>,
pending_contact_requests: HashMap<u64, usize>,
invite_info: Option<InviteInfo>,
client: Weak<Client>,
http: Arc<dyn HttpClient>,
_maintain_contacts: Task<()>,
_maintain_current_user: Task<Result<()>>,
}
#[derive(Clone)]
pub struct InviteInfo {
pub count: u32,
pub url: Arc<str>,
}
pub enum Event {
Contact {
user: Arc<User>,
kind: ContactEventKind,
},
ShowContacts,
ParticipantIndicesChanged,
}
#[derive(Clone, Copy)]
pub enum ContactEventKind {
Requested,
Accepted,
Cancelled,
}
impl EventEmitter for UserStore {
type Event = Event;
}
enum UpdateContacts {
Update(proto::UpdateContacts),
Wait(postage::barrier::Sender),
Clear(postage::barrier::Sender),
}
impl UserStore {
pub fn new(
client: Arc<Client>,
http: Arc<dyn HttpClient>,
cx: &mut ModelContext<Self>,
) -> Self {
let (mut current_user_tx, current_user_rx) = watch::channel();
let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded();
let rpc_subscriptions = vec![
client.add_message_handler(cx.weak_handle(), Self::handle_update_contacts),
client.add_message_handler(cx.weak_handle(), Self::handle_update_invite_info),
client.add_message_handler(cx.weak_handle(), Self::handle_show_contacts),
];
Self {
users: Default::default(),
current_user: current_user_rx,
contacts: Default::default(),
incoming_contact_requests: Default::default(),
participant_indices: Default::default(),
outgoing_contact_requests: Default::default(),
invite_info: None,
client: Arc::downgrade(&client),
update_contacts_tx,
http,
_maintain_contacts: cx.spawn(|this, mut cx| async move {
let _subscriptions = rpc_subscriptions;
while let Some(message) = update_contacts_rx.next().await {
if let Ok(task) =
this.update(&mut cx, |this, cx| this.update_contacts(message, cx))
{
task.log_err().await;
} else {
break;
}
}
}),
_maintain_current_user: cx.spawn(|this, mut cx| async move {
let mut status = client.status();
while let Some(status) = status.next().await {
match status {
Status::Connected { .. } => {
if let Some(user_id) = client.user_id() {
let fetch_user = if let Ok(fetch_user) = this
.update(&mut cx, |this, cx| {
this.get_user(user_id, cx).log_err()
}) {
fetch_user
} else {
break;
};
let fetch_metrics_id =
client.request(proto::GetPrivateUserInfo {}).log_err();
let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
cx.update(|cx| {
if let Some(info) = info {
cx.update_flags(info.staff, info.flags);
client.telemetry.set_authenticated_user_info(
Some(info.metrics_id.clone()),
info.staff,
cx,
)
}
})?;
current_user_tx.send(user).await.ok();
this.update(&mut cx, |_, cx| cx.notify())?;
}
}
Status::SignedOut => {
current_user_tx.send(None).await.ok();
this.update(&mut cx, |this, cx| {
cx.notify();
this.clear_contacts()
})?
.await;
}
Status::ConnectionLost => {
this.update(&mut cx, |this, cx| {
cx.notify();
this.clear_contacts()
})?
.await;
}
_ => {}
}
}
Ok(())
}),
pending_contact_requests: Default::default(),
}
}
#[cfg(feature = "test-support")]
pub fn clear_cache(&mut self) {
self.users.clear();
}
async fn handle_update_invite_info(
this: Handle<Self>,
message: TypedEnvelope<proto::UpdateInviteInfo>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.invite_info = Some(InviteInfo {
url: Arc::from(message.payload.url),
count: message.payload.count,
});
cx.notify();
})?;
Ok(())
}
async fn handle_show_contacts(
this: Handle<Self>,
_: TypedEnvelope<proto::ShowContacts>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |_, cx| cx.emit(Event::ShowContacts))?;
Ok(())
}
pub fn invite_info(&self) -> Option<&InviteInfo> {
self.invite_info.as_ref()
}
async fn handle_update_contacts(
this: Handle<Self>,
message: TypedEnvelope<proto::UpdateContacts>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _| {
this.update_contacts_tx
.unbounded_send(UpdateContacts::Update(message.payload))
.unwrap();
})?;
Ok(())
}
fn update_contacts(
&mut self,
message: UpdateContacts,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
match message {
UpdateContacts::Wait(barrier) => {
drop(barrier);
Task::ready(Ok(()))
}
UpdateContacts::Clear(barrier) => {
self.contacts.clear();
self.incoming_contact_requests.clear();
self.outgoing_contact_requests.clear();
drop(barrier);
Task::ready(Ok(()))
}
UpdateContacts::Update(message) => {
let mut user_ids = HashSet::default();
for contact in &message.contacts {
user_ids.insert(contact.user_id);
}
user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id));
user_ids.extend(message.outgoing_requests.iter());
let load_users = self.get_users(user_ids.into_iter().collect(), cx);
cx.spawn(|this, mut cx| async move {
load_users.await?;
// Users are fetched in parallel above and cached in call to get_users
// No need to paralellize here
let mut updated_contacts = Vec::new();
let this = this
.upgrade()
.ok_or_else(|| anyhow!("can't upgrade user store handle"))?;
for contact in message.contacts {
let should_notify = contact.should_notify;
updated_contacts.push((
Arc::new(Contact::from_proto(contact, &this, &mut cx).await?),
should_notify,
));
}
let mut incoming_requests = Vec::new();
for request in message.incoming_requests {
incoming_requests.push({
let user = this
.update(&mut cx, |this, cx| {
this.get_user(request.requester_id, cx)
})?
.await?;
(user, request.should_notify)
});
}
let mut outgoing_requests = Vec::new();
for requested_user_id in message.outgoing_requests {
outgoing_requests.push(
this.update(&mut cx, |this, cx| this.get_user(requested_user_id, cx))?
.await?,
);
}
let removed_contacts =
HashSet::<u64>::from_iter(message.remove_contacts.iter().copied());
let removed_incoming_requests =
HashSet::<u64>::from_iter(message.remove_incoming_requests.iter().copied());
let removed_outgoing_requests =
HashSet::<u64>::from_iter(message.remove_outgoing_requests.iter().copied());
this.update(&mut cx, |this, cx| {
// Remove contacts
this.contacts
.retain(|contact| !removed_contacts.contains(&contact.user.id));
// Update existing contacts and insert new ones
for (updated_contact, should_notify) in updated_contacts {
if should_notify {
cx.emit(Event::Contact {
user: updated_contact.user.clone(),
kind: ContactEventKind::Accepted,
});
}
match this.contacts.binary_search_by_key(
&&updated_contact.user.github_login,
|contact| &contact.user.github_login,
) {
Ok(ix) => this.contacts[ix] = updated_contact,
Err(ix) => this.contacts.insert(ix, updated_contact),
}
}
// Remove incoming contact requests
this.incoming_contact_requests.retain(|user| {
if removed_incoming_requests.contains(&user.id) {
cx.emit(Event::Contact {
user: user.clone(),
kind: ContactEventKind::Cancelled,
});
false
} else {
true
}
});
// Update existing incoming requests and insert new ones
for (user, should_notify) in incoming_requests {
if should_notify {
cx.emit(Event::Contact {
user: user.clone(),
kind: ContactEventKind::Requested,
});
}
match this
.incoming_contact_requests
.binary_search_by_key(&&user.github_login, |contact| {
&contact.github_login
}) {
Ok(ix) => this.incoming_contact_requests[ix] = user,
Err(ix) => this.incoming_contact_requests.insert(ix, user),
}
}
// Remove outgoing contact requests
this.outgoing_contact_requests
.retain(|user| !removed_outgoing_requests.contains(&user.id));
// Update existing incoming requests and insert new ones
for request in outgoing_requests {
match this
.outgoing_contact_requests
.binary_search_by_key(&&request.github_login, |contact| {
&contact.github_login
}) {
Ok(ix) => this.outgoing_contact_requests[ix] = request,
Err(ix) => this.outgoing_contact_requests.insert(ix, request),
}
}
cx.notify();
})?;
Ok(())
})
}
}
}
pub fn contacts(&self) -> &[Arc<Contact>] {
&self.contacts
}
pub fn has_contact(&self, user: &Arc<User>) -> bool {
self.contacts
.binary_search_by_key(&&user.github_login, |contact| &contact.user.github_login)
.is_ok()
}
pub fn incoming_contact_requests(&self) -> &[Arc<User>] {
&self.incoming_contact_requests
}
pub fn outgoing_contact_requests(&self) -> &[Arc<User>] {
&self.outgoing_contact_requests
}
pub fn is_contact_request_pending(&self, user: &User) -> bool {
self.pending_contact_requests.contains_key(&user.id)
}
pub fn contact_request_status(&self, user: &User) -> ContactRequestStatus {
if self
.contacts
.binary_search_by_key(&&user.github_login, |contact| &contact.user.github_login)
.is_ok()
{
ContactRequestStatus::RequestAccepted
} else if self
.outgoing_contact_requests
.binary_search_by_key(&&user.github_login, |user| &user.github_login)
.is_ok()
{
ContactRequestStatus::RequestSent
} else if self
.incoming_contact_requests
.binary_search_by_key(&&user.github_login, |user| &user.github_login)
.is_ok()
{
ContactRequestStatus::RequestReceived
} else {
ContactRequestStatus::None
}
}
pub fn request_contact(
&mut self,
responder_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
self.perform_contact_request(responder_id, proto::RequestContact { responder_id }, cx)
}
pub fn remove_contact(
&mut self,
user_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
self.perform_contact_request(user_id, proto::RemoveContact { user_id }, cx)
}
pub fn respond_to_contact_request(
&mut self,
requester_id: u64,
accept: bool,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
self.perform_contact_request(
requester_id,
proto::RespondToContactRequest {
requester_id,
response: if accept {
proto::ContactRequestResponse::Accept
} else {
proto::ContactRequestResponse::Decline
} as i32,
},
cx,
)
}
pub fn dismiss_contact_request(
&mut self,
requester_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.upgrade();
cx.spawn(move |_, _| async move {
client
.ok_or_else(|| anyhow!("can't upgrade client reference"))?
.request(proto::RespondToContactRequest {
requester_id,
response: proto::ContactRequestResponse::Dismiss as i32,
})
.await?;
Ok(())
})
}
fn perform_contact_request<T: RequestMessage>(
&mut self,
user_id: u64,
request: T,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.upgrade();
*self.pending_contact_requests.entry(user_id).or_insert(0) += 1;
cx.notify();
cx.spawn(move |this, mut cx| async move {
let response = client
.ok_or_else(|| anyhow!("can't upgrade client reference"))?
.request(request)
.await;
this.update(&mut cx, |this, cx| {
if let Entry::Occupied(mut request_count) =
this.pending_contact_requests.entry(user_id)
{
*request_count.get_mut() -= 1;
if *request_count.get() == 0 {
request_count.remove();
}
}
cx.notify();
})?;
response?;
Ok(())
})
}
pub fn clear_contacts(&mut self) -> impl Future<Output = ()> {
let (tx, mut rx) = postage::barrier::channel();
self.update_contacts_tx
.unbounded_send(UpdateContacts::Clear(tx))
.unwrap();
async move {
rx.next().await;
}
}
pub fn contact_updates_done(&mut self) -> impl Future<Output = ()> {
let (tx, mut rx) = postage::barrier::channel();
self.update_contacts_tx
.unbounded_send(UpdateContacts::Wait(tx))
.unwrap();
async move {
rx.next().await;
}
}
pub fn get_users(
&mut self,
user_ids: Vec<u64>,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Arc<User>>>> {
let mut user_ids_to_fetch = user_ids.clone();
user_ids_to_fetch.retain(|id| !self.users.contains_key(id));
cx.spawn(|this, mut cx| async move {
if !user_ids_to_fetch.is_empty() {
this.update(&mut cx, |this, cx| {
this.load_users(
proto::GetUsers {
user_ids: user_ids_to_fetch,
},
cx,
)
})?
.await?;
}
this.update(&mut cx, |this, _| {
user_ids
.iter()
.map(|user_id| {
this.users
.get(user_id)
.cloned()
.ok_or_else(|| anyhow!("user {} not found", user_id))
})
.collect()
})?
})
}
pub fn fuzzy_search_users(
&mut self,
query: String,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Arc<User>>>> {
self.load_users(proto::FuzzySearchUsers { query }, cx)
}
pub fn get_cached_user(&self, user_id: u64) -> Option<Arc<User>> {
self.users.get(&user_id).cloned()
}
pub fn get_user(
&mut self,
user_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<Arc<User>>> {
if let Some(user) = self.users.get(&user_id).cloned() {
return Task::ready(Ok(user));
}
let load_users = self.get_users(vec![user_id], cx);
cx.spawn(move |this, mut cx| async move {
load_users.await?;
this.update(&mut cx, |this, _| {
this.users
.get(&user_id)
.cloned()
.ok_or_else(|| anyhow!("server responded with no users"))
})?
})
}
pub fn current_user(&self) -> Option<Arc<User>> {
self.current_user.borrow().clone()
}
pub fn watch_current_user(&self) -> watch::Receiver<Option<Arc<User>>> {
self.current_user.clone()
}
fn load_users(
&mut self,
request: impl RequestMessage<Response = UsersResponse>,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Arc<User>>>> {
let client = self.client.clone();
let http = self.http.clone();
cx.spawn(|this, mut cx| async move {
if let Some(rpc) = client.upgrade() {
let response = rpc.request(request).await.context("error loading users")?;
let users = future::join_all(
response
.users
.into_iter()
.map(|user| User::new(user, http.as_ref())),
)
.await;
this.update(&mut cx, |this, _| {
for user in &users {
this.users.insert(user.id, user.clone());
}
})
.ok();
Ok(users)
} else {
Ok(Vec::new())
}
})
}
pub fn set_participant_indices(
&mut self,
participant_indices: HashMap<u64, ParticipantIndex>,
cx: &mut ModelContext<Self>,
) {
if participant_indices != self.participant_indices {
self.participant_indices = participant_indices;
cx.emit(Event::ParticipantIndicesChanged);
}
}
pub fn participant_indices(&self) -> &HashMap<u64, ParticipantIndex> {
&self.participant_indices
}
}
impl User {
async fn new(message: proto::User, http: &dyn HttpClient) -> Arc<Self> {
Arc::new(User {
id: message.id,
github_login: message.github_login,
avatar: fetch_avatar(http, &message.avatar_url).warn_on_err().await,
})
}
}
impl Contact {
async fn from_proto(
contact: proto::Contact,
user_store: &Handle<UserStore>,
cx: &mut AsyncAppContext,
) -> Result<Self> {
let user = user_store
.update(cx, |user_store, cx| {
user_store.get_user(contact.user_id, cx)
})?
.await?;
Ok(Self {
user,
online: contact.online,
busy: contact.busy,
})
}
}
impl Collaborator {
pub fn from_proto(message: proto::Collaborator) -> Result<Self> {
Ok(Self {
peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
replica_id: message.replica_id as ReplicaId,
user_id: message.user_id as UserId,
})
}
}
// todo!("we probably don't need this now that we fetch")
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
let mut response = http
.get(url, Default::default(), true)
.await
.map_err(|e| anyhow!("failed to send user avatar request: {}", e))?;
if !response.status().is_success() {
return Err(anyhow!("avatar request failed {:?}", response.status()));
}
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.map_err(|e| anyhow!("failed to read user avatar response body: {}", e))?;
let format = image::guess_format(&body)?;
let image = image::load_from_memory_with_format(&body, format)?.into_bgra8();
Ok(Arc::new(ImageData::new(image)))
}

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
version = "0.21.0"
version = "0.24.0"
publish = false
[[bin]]
@@ -42,14 +42,12 @@ rand.workspace = true
reqwest = { version = "0.11", features = ["json"], optional = true }
scrypt = "0.7"
smallvec.workspace = true
# 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"
sea-orm = { version = "0.12.x", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
sha-1 = "0.9"
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
time.workspace = true
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = "0.17"
@@ -59,6 +57,7 @@ toml.workspace = true
tracing = "0.1.34"
tracing-log = "0.1.3"
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
uuid.workspace = true
[dev-dependencies]
audio = { path = "../audio" }
@@ -73,6 +72,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"] }
node_runtime = { path = "../node_runtime" }
project = { path = "../project", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
@@ -87,9 +87,9 @@ env_logger.workspace = true
indoc.workspace = true
util = { path = "../util" }
lazy_static.workspace = true
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-sqlite"] }
sea-orm = { version = "0.12.x", features = ["sqlx-sqlite"] }
serde_json.workspace = true
sqlx = { version = "0.6", features = ["sqlite"] }
sqlx = { version = "0.7", features = ["sqlite"] }
unindent.workspace = true
[features]

View File

@@ -37,8 +37,10 @@ CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b");
CREATE TABLE "rooms" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"live_kit_room" VARCHAR NOT NULL,
"enviroment" VARCHAR,
"channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
CREATE TABLE "projects" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -158,7 +160,8 @@ CREATE TABLE "room_participants" (
"initial_project_id" INTEGER,
"calling_user_id" INTEGER NOT NULL REFERENCES users (id),
"calling_connection_id" INTEGER NOT NULL,
"calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL
"calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL,
"participant_index" INTEGER
);
CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id");
CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id");
@@ -288,3 +291,24 @@ CREATE TABLE "user_features" (
CREATE UNIQUE INDEX "index_user_features_user_id_and_feature_id" ON "user_features" ("user_id", "feature_id");
CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id");
CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id");
CREATE TABLE "observed_buffer_edits" (
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
"buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
"epoch" INTEGER NOT NULL,
"lamport_timestamp" INTEGER NOT NULL,
"replica_id" INTEGER NOT NULL,
PRIMARY KEY (user_id, buffer_id)
);
CREATE UNIQUE INDEX "index_observed_buffers_user_and_buffer_id" ON "observed_buffer_edits" ("user_id", "buffer_id");
CREATE TABLE IF NOT EXISTS "observed_channel_messages" (
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
"channel_message_id" INTEGER NOT NULL,
PRIMARY KEY (user_id, channel_id)
);
CREATE UNIQUE INDEX "index_observed_channel_messages_user_and_channel_id" ON "observed_channel_messages" ("user_id", "channel_id");

View File

@@ -0,0 +1,19 @@
CREATE TABLE IF NOT EXISTS "observed_buffer_edits" (
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
"buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
"epoch" INTEGER NOT NULL,
"lamport_timestamp" INTEGER NOT NULL,
"replica_id" INTEGER NOT NULL,
PRIMARY KEY (user_id, buffer_id)
);
CREATE UNIQUE INDEX "index_observed_buffer_user_and_buffer_id" ON "observed_buffer_edits" ("user_id", "buffer_id");
CREATE TABLE IF NOT EXISTS "observed_channel_messages" (
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
"channel_message_id" INTEGER NOT NULL,
PRIMARY KEY (user_id, channel_id)
);
CREATE UNIQUE INDEX "index_observed_channel_messages_user_and_channel_id" ON "observed_channel_messages" ("user_id", "channel_id");

View File

@@ -0,0 +1 @@
ALTER TABLE room_participants ADD COLUMN participant_index INTEGER;

View File

@@ -0,0 +1 @@
ALTER TABLE rooms ADD COLUMN enviroment TEXT;

View File

@@ -0,0 +1 @@
CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");

View File

@@ -19,11 +19,12 @@ use rpc::{
ConnectionId,
};
use sea_orm::{
entity::prelude::*, ActiveValue, Condition, ConnectionTrait, DatabaseConnection,
DatabaseTransaction, DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType,
QueryOrder, QuerySelect, Statement, TransactionTrait,
entity::prelude::*,
sea_query::{Alias, Expr, OnConflict, Query},
ActiveValue, Condition, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbErr,
FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, QueryOrder, QuerySelect, Statement,
TransactionTrait,
};
use sea_query::{Alias, Expr, OnConflict, Query};
use serde::{Deserialize, Serialize};
use sqlx::{
migrate::{Migrate, Migration, MigrationSource},
@@ -62,6 +63,7 @@ pub struct Database {
// separate files in the `queries` folder.
impl Database {
pub async fn new(options: ConnectOptions, executor: Executor) -> Result<Self> {
sqlx::any::install_default_drivers();
Ok(Self {
options: options.clone(),
pool: sea_orm::Database::connect(options).await?,
@@ -119,7 +121,7 @@ impl Database {
Ok(new_migrations)
}
async fn transaction<F, Fut, T>(&self, f: F) -> Result<T>
pub async fn transaction<F, Fut, T>(&self, f: F) -> Result<T>
where
F: Send + Fn(TransactionHandle) -> Fut,
Fut: Send + Future<Output = Result<T>>,
@@ -321,7 +323,7 @@ fn is_serialization_error(error: &Error) -> bool {
}
}
struct TransactionHandle(Arc<Option<DatabaseTransaction>>);
pub struct TransactionHandle(Arc<Option<DatabaseTransaction>>);
impl Deref for TransactionHandle {
type Target = DatabaseTransaction;
@@ -437,6 +439,8 @@ pub struct ChannelsForUser {
pub channels: ChannelGraph,
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
pub channels_with_admin_privileges: HashSet<ChannelId>,
pub unseen_buffer_changes: Vec<proto::UnseenChannelBufferChange>,
pub channel_messages: Vec<proto::UnseenChannelMessage>,
}
#[derive(Debug)]
@@ -510,7 +514,7 @@ pub struct RefreshedRoom {
pub struct RefreshedChannelBuffer {
pub connection_ids: Vec<ConnectionId>,
pub removed_collaborators: Vec<proto::RemoveChannelBufferCollaborator>,
pub collaborators: Vec<proto::Collaborator>,
}
pub struct Project {

View File

@@ -1,6 +1,5 @@
use crate::Result;
use sea_orm::DbErr;
use sea_query::{Value, ValueTypeErr};
use sea_orm::{entity::prelude::*, DbErr};
use serde::{Deserialize, Serialize};
macro_rules! id_type {
@@ -17,6 +16,7 @@ macro_rules! id_type {
Hash,
Serialize,
Deserialize,
DeriveValueType,
)]
#[serde(transparent)]
pub struct $name(pub i32);
@@ -42,40 +42,6 @@ macro_rules! id_type {
}
}
impl From<$name> for sea_query::Value {
fn from(value: $name) -> Self {
sea_query::Value::Int(Some(value.0))
}
}
impl sea_orm::TryGetable for $name {
fn try_get(
res: &sea_orm::QueryResult,
pre: &str,
col: &str,
) -> Result<Self, sea_orm::TryGetError> {
Ok(Self(i32::try_get(res, pre, col)?))
}
}
impl sea_query::ValueType for $name {
fn try_from(v: Value) -> Result<Self, sea_query::ValueTypeErr> {
Ok(Self(value_to_integer(v)?))
}
fn type_name() -> String {
stringify!($name).into()
}
fn array_type() -> sea_query::ArrayType {
sea_query::ArrayType::Int
}
fn column_type() -> sea_query::ColumnType {
sea_query::ColumnType::Integer(None)
}
}
impl sea_orm::TryFromU64 for $name {
fn try_from_u64(n: u64) -> Result<Self, DbErr> {
Ok(Self(n.try_into().map_err(|_| {
@@ -88,7 +54,7 @@ macro_rules! id_type {
}
}
impl sea_query::Nullable for $name {
impl sea_orm::sea_query::Nullable for $name {
fn null() -> Value {
Value::Int(None)
}
@@ -96,20 +62,6 @@ macro_rules! id_type {
};
}
fn value_to_integer(v: Value) -> Result<i32, ValueTypeErr> {
match v {
Value::TinyInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
Value::SmallInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
Value::Int(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
Value::BigInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
Value::TinyUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
Value::SmallUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
Value::Unsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
Value::BigUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
_ => Err(ValueTypeErr),
}
}
id_type!(BufferId);
id_type!(AccessTokenId);
id_type!(ChannelChatParticipantId);

View File

@@ -2,6 +2,12 @@ use super::*;
use prost::Message;
use text::{EditOperation, UndoOperation};
pub struct LeftChannelBuffer {
pub channel_id: ChannelId,
pub collaborators: Vec<proto::Collaborator>,
pub connections: Vec<ConnectionId>,
}
impl Database {
pub async fn join_channel_buffer(
&self,
@@ -68,7 +74,32 @@ impl Database {
.await?;
collaborators.push(collaborator);
let (base_text, operations) = self.get_buffer_state(&buffer, &tx).await?;
let (base_text, operations, max_operation) =
self.get_buffer_state(&buffer, &tx).await?;
// Save the last observed operation
if let Some(op) = max_operation {
observed_buffer_edits::Entity::insert(observed_buffer_edits::ActiveModel {
user_id: ActiveValue::Set(user_id),
buffer_id: ActiveValue::Set(buffer.id),
epoch: ActiveValue::Set(op.epoch),
lamport_timestamp: ActiveValue::Set(op.lamport_timestamp),
replica_id: ActiveValue::Set(op.replica_id),
})
.on_conflict(
OnConflict::columns([
observed_buffer_edits::Column::UserId,
observed_buffer_edits::Column::BufferId,
])
.update_columns([
observed_buffer_edits::Column::Epoch,
observed_buffer_edits::Column::LamportTimestamp,
])
.to_owned(),
)
.exec(&*tx)
.await?;
}
Ok(proto::JoinChannelBufferResponse {
buffer_id: buffer.id.to_proto(),
@@ -204,23 +235,26 @@ impl Database {
server_id: ServerId,
) -> Result<RefreshedChannelBuffer> {
self.transaction(|tx| async move {
let collaborators = channel_buffer_collaborator::Entity::find()
let db_collaborators = channel_buffer_collaborator::Entity::find()
.filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id))
.all(&*tx)
.await?;
let mut connection_ids = Vec::new();
let mut removed_collaborators = Vec::new();
let mut collaborators = Vec::new();
let mut collaborator_ids_to_remove = Vec::new();
for collaborator in &collaborators {
if !collaborator.connection_lost && collaborator.connection_server_id == server_id {
connection_ids.push(collaborator.connection());
for db_collaborator in &db_collaborators {
if !db_collaborator.connection_lost
&& db_collaborator.connection_server_id == server_id
{
connection_ids.push(db_collaborator.connection());
collaborators.push(proto::Collaborator {
peer_id: Some(db_collaborator.connection().into()),
replica_id: db_collaborator.replica_id.0 as u32,
user_id: db_collaborator.user_id.to_proto(),
})
} else {
removed_collaborators.push(proto::RemoveChannelBufferCollaborator {
channel_id: channel_id.to_proto(),
peer_id: Some(collaborator.connection().into()),
});
collaborator_ids_to_remove.push(collaborator.id);
collaborator_ids_to_remove.push(db_collaborator.id);
}
}
@@ -231,7 +265,7 @@ impl Database {
Ok(RefreshedChannelBuffer {
connection_ids,
removed_collaborators,
collaborators,
})
})
.await
@@ -241,7 +275,7 @@ impl Database {
&self,
channel_id: ChannelId,
connection: ConnectionId,
) -> Result<Vec<ConnectionId>> {
) -> Result<LeftChannelBuffer> {
self.transaction(|tx| async move {
self.leave_channel_buffer_internal(channel_id, connection, &*tx)
.await
@@ -275,7 +309,7 @@ impl Database {
pub async fn leave_channel_buffers(
&self,
connection: ConnectionId,
) -> Result<Vec<(ChannelId, Vec<ConnectionId>)>> {
) -> Result<Vec<LeftChannelBuffer>> {
self.transaction(|tx| async move {
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
enum QueryChannelIds {
@@ -294,10 +328,10 @@ impl Database {
let mut result = Vec::new();
for channel_id in channel_ids {
let collaborators = self
let left_channel_buffer = self
.leave_channel_buffer_internal(channel_id, connection, &*tx)
.await?;
result.push((channel_id, collaborators));
result.push(left_channel_buffer);
}
Ok(result)
@@ -310,7 +344,7 @@ impl Database {
channel_id: ChannelId,
connection: ConnectionId,
tx: &DatabaseTransaction,
) -> Result<Vec<ConnectionId>> {
) -> Result<LeftChannelBuffer> {
let result = channel_buffer_collaborator::Entity::delete_many()
.filter(
Condition::all()
@@ -327,6 +361,7 @@ impl Database {
Err(anyhow!("not a collaborator on this project"))?;
}
let mut collaborators = Vec::new();
let mut connections = Vec::new();
let mut rows = channel_buffer_collaborator::Entity::find()
.filter(
@@ -336,19 +371,26 @@ impl Database {
.await?;
while let Some(row) = rows.next().await {
let row = row?;
connections.push(ConnectionId {
id: row.connection_id as u32,
owner_id: row.connection_server_id.0 as u32,
let connection = row.connection();
connections.push(connection);
collaborators.push(proto::Collaborator {
peer_id: Some(connection.into()),
replica_id: row.replica_id.0 as u32,
user_id: row.user_id.to_proto(),
});
}
drop(rows);
if connections.is_empty() {
if collaborators.is_empty() {
self.snapshot_channel_buffer(channel_id, &tx).await?;
}
Ok(connections)
Ok(LeftChannelBuffer {
channel_id,
collaborators,
connections,
})
}
pub async fn get_channel_buffer_collaborators(
@@ -356,33 +398,46 @@ impl Database {
channel_id: ChannelId,
) -> Result<Vec<UserId>> {
self.transaction(|tx| async move {
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
enum QueryUserIds {
UserId,
}
let users: Vec<UserId> = channel_buffer_collaborator::Entity::find()
.select_only()
.column(channel_buffer_collaborator::Column::UserId)
.filter(
Condition::all()
.add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)),
)
.into_values::<_, QueryUserIds>()
.all(&*tx)
.await?;
Ok(users)
self.get_channel_buffer_collaborators_internal(channel_id, &*tx)
.await
})
.await
}
async fn get_channel_buffer_collaborators_internal(
&self,
channel_id: ChannelId,
tx: &DatabaseTransaction,
) -> Result<Vec<UserId>> {
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
enum QueryUserIds {
UserId,
}
let users: Vec<UserId> = channel_buffer_collaborator::Entity::find()
.select_only()
.column(channel_buffer_collaborator::Column::UserId)
.filter(
Condition::all().add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)),
)
.into_values::<_, QueryUserIds>()
.all(&*tx)
.await?;
Ok(users)
}
pub async fn update_channel_buffer(
&self,
channel_id: ChannelId,
user: UserId,
operations: &[proto::Operation],
) -> Result<Vec<ConnectionId>> {
) -> Result<(
Vec<ConnectionId>,
Vec<UserId>,
i32,
Vec<proto::VectorClockEntry>,
)> {
self.transaction(move |tx| async move {
self.check_user_is_channel_member(channel_id, user, &*tx)
.await?;
@@ -401,7 +456,38 @@ impl Database {
.iter()
.filter_map(|op| operation_to_storage(op, &buffer, serialization_version))
.collect::<Vec<_>>();
let mut channel_members;
let max_version;
if !operations.is_empty() {
let max_operation = operations
.iter()
.max_by_key(|op| (op.lamport_timestamp.as_ref(), op.replica_id.as_ref()))
.unwrap();
max_version = vec![proto::VectorClockEntry {
replica_id: *max_operation.replica_id.as_ref() as u32,
timestamp: *max_operation.lamport_timestamp.as_ref() as u32,
}];
// get current channel participants and save the max operation above
self.save_max_operation(
user,
buffer.id,
buffer.epoch,
*max_operation.replica_id.as_ref(),
*max_operation.lamport_timestamp.as_ref(),
&*tx,
)
.await?;
channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
let collaborators = self
.get_channel_buffer_collaborators_internal(channel_id, &*tx)
.await?;
channel_members.retain(|member| !collaborators.contains(member));
buffer_operation::Entity::insert_many(operations)
.on_conflict(
OnConflict::columns([
@@ -415,6 +501,9 @@ impl Database {
)
.exec(&*tx)
.await?;
} else {
channel_members = Vec::new();
max_version = Vec::new();
}
let mut connections = Vec::new();
@@ -433,11 +522,53 @@ impl Database {
});
}
Ok(connections)
Ok((connections, channel_members, buffer.epoch, max_version))
})
.await
}
async fn save_max_operation(
&self,
user_id: UserId,
buffer_id: BufferId,
epoch: i32,
replica_id: i32,
lamport_timestamp: i32,
tx: &DatabaseTransaction,
) -> Result<()> {
use observed_buffer_edits::Column;
observed_buffer_edits::Entity::insert(observed_buffer_edits::ActiveModel {
user_id: ActiveValue::Set(user_id),
buffer_id: ActiveValue::Set(buffer_id),
epoch: ActiveValue::Set(epoch),
replica_id: ActiveValue::Set(replica_id),
lamport_timestamp: ActiveValue::Set(lamport_timestamp),
})
.on_conflict(
OnConflict::columns([Column::UserId, Column::BufferId])
.update_columns([Column::Epoch, Column::LamportTimestamp, Column::ReplicaId])
.action_cond_where(
Condition::any().add(Column::Epoch.lt(epoch)).add(
Condition::all().add(Column::Epoch.eq(epoch)).add(
Condition::any()
.add(Column::LamportTimestamp.lt(lamport_timestamp))
.add(
Column::LamportTimestamp
.eq(lamport_timestamp)
.and(Column::ReplicaId.lt(replica_id)),
),
),
),
)
.to_owned(),
)
.exec_without_returning(tx)
.await?;
Ok(())
}
async fn get_buffer_operation_serialization_version(
&self,
buffer_id: BufferId,
@@ -455,7 +586,7 @@ impl Database {
.ok_or_else(|| anyhow!("missing buffer snapshot"))?)
}
async fn get_channel_buffer(
pub async fn get_channel_buffer(
&self,
channel_id: ChannelId,
tx: &DatabaseTransaction,
@@ -474,7 +605,11 @@ impl Database {
&self,
buffer: &buffer::Model,
tx: &DatabaseTransaction,
) -> Result<(String, Vec<proto::Operation>)> {
) -> Result<(
String,
Vec<proto::Operation>,
Option<buffer_operation::Model>,
)> {
let id = buffer.id;
let (base_text, version) = if buffer.epoch > 0 {
let snapshot = buffer_snapshot::Entity::find()
@@ -499,16 +634,28 @@ impl Database {
.eq(id)
.and(buffer_operation::Column::Epoch.eq(buffer.epoch)),
)
.order_by_asc(buffer_operation::Column::LamportTimestamp)
.order_by_asc(buffer_operation::Column::ReplicaId)
.stream(&*tx)
.await?;
let mut operations = Vec::new();
let mut last_row = None;
while let Some(row) = rows.next().await {
let row = row?;
last_row = Some(buffer_operation::Model {
buffer_id: row.buffer_id,
epoch: row.epoch,
lamport_timestamp: row.lamport_timestamp,
replica_id: row.lamport_timestamp,
value: Default::default(),
});
operations.push(proto::Operation {
variant: Some(operation_from_storage(row?, version)?),
})
variant: Some(operation_from_storage(row, version)?),
});
}
Ok((base_text, operations))
Ok((base_text, operations, last_row))
}
async fn snapshot_channel_buffer(
@@ -517,7 +664,7 @@ impl Database {
tx: &DatabaseTransaction,
) -> Result<()> {
let buffer = self.get_channel_buffer(channel_id, tx).await?;
let (base_text, operations) = self.get_buffer_state(&buffer, tx).await?;
let (base_text, operations, _) = self.get_buffer_state(&buffer, tx).await?;
if operations.is_empty() {
return Ok(());
}
@@ -550,6 +697,150 @@ impl Database {
Ok(())
}
pub async fn observe_buffer_version(
&self,
buffer_id: BufferId,
user_id: UserId,
epoch: i32,
version: &[proto::VectorClockEntry],
) -> Result<()> {
self.transaction(|tx| async move {
// For now, combine concurrent operations.
let Some(component) = version.iter().max_by_key(|version| version.timestamp) else {
return Ok(());
};
self.save_max_operation(
user_id,
buffer_id,
epoch,
component.replica_id as i32,
component.timestamp as i32,
&*tx,
)
.await?;
Ok(())
})
.await
}
pub async fn unseen_channel_buffer_changes(
&self,
user_id: UserId,
channel_ids: &[ChannelId],
tx: &DatabaseTransaction,
) -> Result<Vec<proto::UnseenChannelBufferChange>> {
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
enum QueryIds {
ChannelId,
Id,
}
let mut channel_ids_by_buffer_id = HashMap::default();
let mut rows = buffer::Entity::find()
.filter(buffer::Column::ChannelId.is_in(channel_ids.iter().copied()))
.stream(&*tx)
.await?;
while let Some(row) = rows.next().await {
let row = row?;
channel_ids_by_buffer_id.insert(row.id, row.channel_id);
}
drop(rows);
let mut observed_edits_by_buffer_id = HashMap::default();
let mut rows = observed_buffer_edits::Entity::find()
.filter(observed_buffer_edits::Column::UserId.eq(user_id))
.filter(
observed_buffer_edits::Column::BufferId
.is_in(channel_ids_by_buffer_id.keys().copied()),
)
.stream(&*tx)
.await?;
while let Some(row) = rows.next().await {
let row = row?;
observed_edits_by_buffer_id.insert(row.buffer_id, row);
}
drop(rows);
let latest_operations = self
.get_latest_operations_for_buffers(channel_ids_by_buffer_id.keys().copied(), &*tx)
.await?;
let mut changes = Vec::default();
for latest in latest_operations {
if let Some(observed) = observed_edits_by_buffer_id.get(&latest.buffer_id) {
if (
observed.epoch,
observed.lamport_timestamp,
observed.replica_id,
) >= (latest.epoch, latest.lamport_timestamp, latest.replica_id)
{
continue;
}
}
if let Some(channel_id) = channel_ids_by_buffer_id.get(&latest.buffer_id) {
changes.push(proto::UnseenChannelBufferChange {
channel_id: channel_id.to_proto(),
epoch: latest.epoch as u64,
version: vec![proto::VectorClockEntry {
replica_id: latest.replica_id as u32,
timestamp: latest.lamport_timestamp as u32,
}],
});
}
}
Ok(changes)
}
pub async fn get_latest_operations_for_buffers(
&self,
buffer_ids: impl IntoIterator<Item = BufferId>,
tx: &DatabaseTransaction,
) -> Result<Vec<buffer_operation::Model>> {
let mut values = String::new();
for id in buffer_ids {
if !values.is_empty() {
values.push_str(", ");
}
write!(&mut values, "({})", id).unwrap();
}
if values.is_empty() {
return Ok(Vec::default());
}
let sql = format!(
r#"
SELECT
*
FROM
(
SELECT
*,
row_number() OVER (
PARTITION BY buffer_id
ORDER BY
epoch DESC,
lamport_timestamp DESC,
replica_id DESC
) as row_number
FROM buffer_operations
WHERE
buffer_id in ({values})
) AS last_operations
WHERE
row_number = 1
"#,
);
let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
Ok(buffer_operation::Entity::find()
.from_raw_sql(stmt)
.all(&*tx)
.await?)
}
}
fn operation_to_storage(

View File

@@ -1,8 +1,7 @@
use super::*;
use rpc::proto::ChannelEdge;
use smallvec::SmallVec;
use super::*;
type ChannelDescendants = HashMap<ChannelId, SmallSet<ChannelId>>;
impl Database {
@@ -20,21 +19,14 @@ impl Database {
.await
}
pub async fn create_root_channel(
&self,
name: &str,
live_kit_room: &str,
creator_id: UserId,
) -> Result<ChannelId> {
self.create_channel(name, None, live_kit_room, creator_id)
.await
pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result<ChannelId> {
self.create_channel(name, None, creator_id).await
}
pub async fn create_channel(
&self,
name: &str,
parent: Option<ChannelId>,
live_kit_room: &str,
creator_id: UserId,
) -> Result<ChannelId> {
let name = Self::sanitize_channel_name(name)?;
@@ -91,14 +83,6 @@ impl Database {
.insert(&*tx)
.await?;
room::ActiveModel {
channel_id: ActiveValue::Set(Some(channel.id)),
live_kit_room: ActiveValue::Set(live_kit_room.to_string()),
..Default::default()
}
.insert(&*tx)
.await?;
Ok(channel.id)
})
.await
@@ -391,7 +375,8 @@ impl Database {
.all(&*tx)
.await?;
self.get_user_channels(channel_memberships, &tx).await
self.get_user_channels(user_id, channel_memberships, &tx)
.await
})
.await
}
@@ -414,13 +399,15 @@ impl Database {
.all(&*tx)
.await?;
self.get_user_channels(channel_membership, &tx).await
self.get_user_channels(user_id, channel_membership, &tx)
.await
})
.await
}
pub async fn get_user_channels(
&self,
user_id: UserId,
channel_memberships: Vec<channel_member::Model>,
tx: &DatabaseTransaction,
) -> Result<ChannelsForUser> {
@@ -460,10 +447,21 @@ impl Database {
}
}
let channel_ids = graph.channels.iter().map(|c| c.id).collect::<Vec<_>>();
let channel_buffer_changes = self
.unseen_channel_buffer_changes(user_id, &channel_ids, &*tx)
.await?;
let unseen_messages = self
.unseen_channel_messages(user_id, &channel_ids, &*tx)
.await?;
Ok(ChannelsForUser {
channels: graph,
channel_participants,
channels_with_admin_privileges,
unseen_buffer_changes: channel_buffer_changes,
channel_messages: unseen_messages,
})
}
@@ -645,7 +643,7 @@ impl Database {
) -> Result<Vec<ChannelId>> {
let paths = channel_path::Entity::find()
.filter(channel_path::Column::ChannelId.eq(channel_id))
.order_by(channel_path::Column::IdPath, sea_query::Order::Desc)
.order_by(channel_path::Column::IdPath, sea_orm::Order::Desc)
.all(tx)
.await?;
let mut channel_ids = Vec::new();
@@ -784,18 +782,36 @@ impl Database {
.await
}
pub async fn room_id_for_channel(&self, channel_id: ChannelId) -> Result<RoomId> {
pub async fn get_or_create_channel_room(
&self,
channel_id: ChannelId,
live_kit_room: &str,
enviroment: &str,
) -> Result<RoomId> {
self.transaction(|tx| async move {
let tx = tx;
let room = channel::Model {
id: channel_id,
..Default::default()
}
.find_related(room::Entity)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("invalid channel"))?;
Ok(room.id)
let room = room::Entity::find()
.filter(room::Column::ChannelId.eq(channel_id))
.one(&*tx)
.await?;
let room_id = if let Some(room) = room {
room.id
} else {
let result = room::Entity::insert(room::ActiveModel {
channel_id: ActiveValue::Set(Some(channel_id)),
live_kit_room: ActiveValue::Set(live_kit_room.to_string()),
enviroment: ActiveValue::Set(Some(enviroment.to_string())),
..Default::default()
})
.exec(&*tx)
.await?;
result.last_insert_id
};
Ok(room_id)
})
.await
}
@@ -828,68 +844,53 @@ impl Database {
) -> Result<ChannelGraph> {
self.check_user_is_channel_admin(to, user, &*tx).await?;
let to_ancestors = self.get_channel_ancestors(to, &*tx).await?;
let paths = channel_path::Entity::find()
.filter(channel_path::Column::IdPath.like(&format!("%/{}/%", channel)))
.all(tx)
.await?;
let mut new_path_suffixes = HashSet::default();
for path in paths {
if let Some(start_offset) = path.id_path.find(&format!("/{}/", channel)) {
new_path_suffixes.insert((
path.channel_id,
path.id_path[(start_offset + 1)..].to_string(),
));
}
}
let paths_to_new_parent = channel_path::Entity::find()
.filter(channel_path::Column::ChannelId.eq(to))
.all(tx)
.await?;
let mut new_paths = Vec::new();
for path in paths_to_new_parent {
if path.id_path.contains(&format!("/{}/", channel)) {
Err(anyhow!("cycle"))?;
}
new_paths.extend(new_path_suffixes.iter().map(|(channel_id, path_suffix)| {
channel_path::ActiveModel {
channel_id: ActiveValue::Set(*channel_id),
id_path: ActiveValue::Set(format!("{}{}", &path.id_path, path_suffix)),
}
}));
}
channel_path::Entity::insert_many(new_paths)
.exec(&*tx)
.await?;
// remove any root edges for the channel we just linked
{
channel_path::Entity::delete_many()
.filter(channel_path::Column::IdPath.like(&format!("/{}/%", channel)))
.exec(&*tx)
.await?;
}
let mut channel_descendants = self.get_channel_descendants([channel], &*tx).await?;
for ancestor in to_ancestors {
if channel_descendants.contains_key(&ancestor) {
return Err(anyhow!("Cannot create a channel cycle").into());
}
}
// Now insert all of the new paths
let sql = r#"
INSERT INTO channel_paths
(id_path, channel_id)
SELECT
id_path || $1 || '/', $2
FROM
channel_paths
WHERE
channel_id = $3
ON CONFLICT (id_path) DO NOTHING;
"#;
let channel_paths_stmt = Statement::from_sql_and_values(
self.pool.get_database_backend(),
sql,
[
channel.to_proto().into(),
channel.to_proto().into(),
to.to_proto().into(),
],
);
tx.execute(channel_paths_stmt).await?;
for (descdenant_id, descendant_parent_ids) in
channel_descendants.iter().filter(|(id, _)| id != &&channel)
{
for descendant_parent_id in descendant_parent_ids.iter() {
let channel_paths_stmt = Statement::from_sql_and_values(
self.pool.get_database_backend(),
sql,
[
descdenant_id.to_proto().into(),
descdenant_id.to_proto().into(),
descendant_parent_id.to_proto().into(),
],
);
tx.execute(channel_paths_stmt).await?;
}
}
// If we're linking a channel, remove any root edges for the channel
{
let sql = r#"
DELETE FROM channel_paths
WHERE
id_path = '/' || $1 || '/'
"#;
let channel_paths_stmt = Statement::from_sql_and_values(
self.pool.get_database_backend(),
sql,
[channel.to_proto().into()],
);
tx.execute(channel_paths_stmt).await?;
}
if let Some(channel) = channel_descendants.get_mut(&channel) {
// Remove the other parents
channel.clear();
@@ -936,35 +937,43 @@ impl Database {
self.check_user_is_channel_admin(from, user, &*tx).await?;
let sql = r#"
DELETE FROM channel_paths
WHERE
id_path LIKE '%' || $1 || '/' || $2 || '%'
"#;
let channel_paths_stmt = Statement::from_sql_and_values(
self.pool.get_database_backend(),
sql,
[from.to_proto().into(), channel.to_proto().into()],
);
tx.execute(channel_paths_stmt).await?;
DELETE FROM channel_paths
WHERE
id_path LIKE '%/' || $1 || '/' || $2 || '/%'
RETURNING id_path, channel_id
"#;
let paths = channel_path::Entity::find()
.from_raw_sql(Statement::from_sql_and_values(
self.pool.get_database_backend(),
sql,
[from.to_proto().into(), channel.to_proto().into()],
))
.all(&*tx)
.await?;
let is_stranded = channel_path::Entity::find()
.filter(channel_path::Column::ChannelId.eq(channel))
.count(&*tx)
.await?
== 0;
// Make sure that there is always at least one path to the channel
let sql = r#"
INSERT INTO channel_paths
(id_path, channel_id)
SELECT
'/' || $1 || '/', $2
WHERE NOT EXISTS
(SELECT *
FROM channel_paths
WHERE channel_id = $2)
"#;
let channel_paths_stmt = Statement::from_sql_and_values(
self.pool.get_database_backend(),
sql,
[channel.to_proto().into(), channel.to_proto().into()],
);
tx.execute(channel_paths_stmt).await?;
if is_stranded {
let root_paths: Vec<_> = paths
.iter()
.map(|path| {
let start_offset = path.id_path.find(&format!("/{}/", channel)).unwrap();
channel_path::ActiveModel {
channel_id: ActiveValue::Set(path.channel_id),
id_path: ActiveValue::Set(path.id_path[start_offset..].to_string()),
}
})
.collect();
channel_path::Entity::insert_many(root_paths)
.exec(&*tx)
.await?;
}
Ok(())
}
@@ -978,6 +987,13 @@ impl Database {
from: ChannelId,
to: ChannelId,
) -> Result<ChannelGraph> {
if from == to {
return Ok(ChannelGraph {
channels: vec![],
edges: vec![],
});
}
self.transaction(|tx| async move {
self.check_user_is_channel_admin(channel, user, &*tx)
.await?;

View File

@@ -18,12 +18,12 @@ impl Database {
let user_b_participant = Alias::new("user_b_participant");
let mut db_contacts = contact::Entity::find()
.column_as(
Expr::tbl(user_a_participant.clone(), room_participant::Column::Id)
Expr::col((user_a_participant.clone(), room_participant::Column::Id))
.is_not_null(),
"user_a_busy",
)
.column_as(
Expr::tbl(user_b_participant.clone(), room_participant::Column::Id)
Expr::col((user_b_participant.clone(), room_participant::Column::Id))
.is_not_null(),
"user_b_busy",
)

View File

@@ -89,6 +89,7 @@ impl Database {
let mut rows = channel_message::Entity::find()
.filter(condition)
.order_by_desc(channel_message::Column::Id)
.limit(count as u64)
.stream(&*tx)
.await?;
@@ -108,7 +109,8 @@ impl Database {
}),
});
}
drop(rows);
messages.reverse();
Ok(messages)
})
.await
@@ -121,7 +123,7 @@ impl Database {
body: &str,
timestamp: OffsetDateTime,
nonce: u128,
) -> Result<(MessageId, Vec<ConnectionId>)> {
) -> Result<(MessageId, Vec<ConnectionId>, Vec<UserId>)> {
self.transaction(|tx| async move {
let mut rows = channel_chat_participant::Entity::find()
.filter(channel_chat_participant::Column::ChannelId.eq(channel_id))
@@ -130,11 +132,13 @@ impl Database {
let mut is_participant = false;
let mut participant_connection_ids = Vec::new();
let mut participant_user_ids = Vec::new();
while let Some(row) = rows.next().await {
let row = row?;
if row.user_id == user_id {
is_participant = true;
}
participant_user_ids.push(row.user_id);
participant_connection_ids.push(row.connection());
}
drop(rows);
@@ -167,11 +171,141 @@ impl Database {
ConnectionId,
}
Ok((message.last_insert_id, participant_connection_ids))
// Observe this message for the sender
self.observe_channel_message_internal(
channel_id,
user_id,
message.last_insert_id,
&*tx,
)
.await?;
let mut channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
channel_members.retain(|member| !participant_user_ids.contains(member));
Ok((
message.last_insert_id,
participant_connection_ids,
channel_members,
))
})
.await
}
pub async fn observe_channel_message(
&self,
channel_id: ChannelId,
user_id: UserId,
message_id: MessageId,
) -> Result<()> {
self.transaction(|tx| async move {
self.observe_channel_message_internal(channel_id, user_id, message_id, &*tx)
.await?;
Ok(())
})
.await
}
async fn observe_channel_message_internal(
&self,
channel_id: ChannelId,
user_id: UserId,
message_id: MessageId,
tx: &DatabaseTransaction,
) -> Result<()> {
observed_channel_messages::Entity::insert(observed_channel_messages::ActiveModel {
user_id: ActiveValue::Set(user_id),
channel_id: ActiveValue::Set(channel_id),
channel_message_id: ActiveValue::Set(message_id),
})
.on_conflict(
OnConflict::columns([
observed_channel_messages::Column::ChannelId,
observed_channel_messages::Column::UserId,
])
.update_column(observed_channel_messages::Column::ChannelMessageId)
.action_cond_where(observed_channel_messages::Column::ChannelMessageId.lt(message_id))
.to_owned(),
)
// TODO: Try to upgrade SeaORM so we don't have to do this hack around their bug
.exec_without_returning(&*tx)
.await?;
Ok(())
}
pub async fn unseen_channel_messages(
&self,
user_id: UserId,
channel_ids: &[ChannelId],
tx: &DatabaseTransaction,
) -> Result<Vec<proto::UnseenChannelMessage>> {
let mut observed_messages_by_channel_id = HashMap::default();
let mut rows = observed_channel_messages::Entity::find()
.filter(observed_channel_messages::Column::UserId.eq(user_id))
.filter(observed_channel_messages::Column::ChannelId.is_in(channel_ids.iter().copied()))
.stream(&*tx)
.await?;
while let Some(row) = rows.next().await {
let row = row?;
observed_messages_by_channel_id.insert(row.channel_id, row);
}
drop(rows);
let mut values = String::new();
for id in channel_ids {
if !values.is_empty() {
values.push_str(", ");
}
write!(&mut values, "({})", id).unwrap();
}
if values.is_empty() {
return Ok(Default::default());
}
let sql = format!(
r#"
SELECT
*
FROM (
SELECT
*,
row_number() OVER (
PARTITION BY channel_id
ORDER BY id DESC
) as row_number
FROM channel_messages
WHERE
channel_id in ({values})
) AS messages
WHERE
row_number = 1
"#,
);
let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
let last_messages = channel_message::Model::find_by_statement(stmt)
.all(&*tx)
.await?;
let mut changes = Vec::new();
for last_message in last_messages {
if let Some(observed_message) =
observed_messages_by_channel_id.get(&last_message.channel_id)
{
if observed_message.channel_message_id == last_message.id {
continue;
}
}
changes.push(proto::UnseenChannelMessage {
channel_id: last_message.channel_id.to_proto(),
message_id: last_message.id.to_proto(),
});
}
Ok(changes)
}
pub async fn remove_channel_message(
&self,
channel_id: ChannelId,

View File

@@ -738,7 +738,7 @@ impl Database {
Condition::any()
.add(
Condition::all()
.add(follower::Column::ProjectId.eq(project_id))
.add(follower::Column::ProjectId.eq(Some(project_id)))
.add(
follower::Column::LeaderConnectionServerId
.eq(connection.owner_id),
@@ -747,7 +747,7 @@ impl Database {
)
.add(
Condition::all()
.add(follower::Column::ProjectId.eq(project_id))
.add(follower::Column::ProjectId.eq(Some(project_id)))
.add(
follower::Column::FollowerConnectionServerId
.eq(connection.owner_id),
@@ -862,13 +862,46 @@ impl Database {
.await
}
pub async fn check_room_participants(
&self,
room_id: RoomId,
leader_id: ConnectionId,
follower_id: ConnectionId,
) -> Result<()> {
self.transaction(|tx| async move {
use room_participant::Column;
let count = room_participant::Entity::find()
.filter(
Condition::all().add(Column::RoomId.eq(room_id)).add(
Condition::any()
.add(Column::AnsweringConnectionId.eq(leader_id.id as i32).and(
Column::AnsweringConnectionServerId.eq(leader_id.owner_id as i32),
))
.add(Column::AnsweringConnectionId.eq(follower_id.id as i32).and(
Column::AnsweringConnectionServerId.eq(follower_id.owner_id as i32),
)),
),
)
.count(&*tx)
.await?;
if count < 2 {
Err(anyhow!("not room participants"))?;
}
Ok(())
})
.await
}
pub async fn follow(
&self,
room_id: RoomId,
project_id: ProjectId,
leader_connection: ConnectionId,
follower_connection: ConnectionId,
) -> Result<RoomGuard<proto::Room>> {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
follower::ActiveModel {
room_id: ActiveValue::set(room_id),
@@ -894,15 +927,16 @@ impl Database {
pub async fn unfollow(
&self,
room_id: RoomId,
project_id: ProjectId,
leader_connection: ConnectionId,
follower_connection: ConnectionId,
) -> Result<RoomGuard<proto::Room>> {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
follower::Entity::delete_many()
.filter(
Condition::all()
.add(follower::Column::RoomId.eq(room_id))
.add(follower::Column::ProjectId.eq(project_id))
.add(
follower::Column::LeaderConnectionServerId

View File

@@ -107,10 +107,12 @@ impl Database {
user_id: UserId,
connection: ConnectionId,
live_kit_room: &str,
release_channel: &str,
) -> Result<proto::Room> {
self.transaction(|tx| async move {
let room = room::ActiveModel {
live_kit_room: ActiveValue::set(live_kit_room.into()),
enviroment: ActiveValue::set(Some(release_channel.to_string())),
..Default::default()
}
.insert(&*tx)
@@ -128,6 +130,7 @@ impl Database {
calling_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
participant_index: ActiveValue::set(Some(0)),
..Default::default()
}
.insert(&*tx)
@@ -152,6 +155,7 @@ impl Database {
room_id: ActiveValue::set(room_id),
user_id: ActiveValue::set(called_user_id),
answering_connection_lost: ActiveValue::set(false),
participant_index: ActiveValue::NotSet,
calling_user_id: ActiveValue::set(calling_user_id),
calling_connection_id: ActiveValue::set(calling_connection.id as i32),
calling_connection_server_id: ActiveValue::set(Some(ServerId(
@@ -268,20 +272,52 @@ impl Database {
room_id: RoomId,
user_id: UserId,
connection: ConnectionId,
enviroment: &str,
) -> Result<RoomGuard<JoinRoom>> {
self.room_transaction(room_id, |tx| async move {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryChannelId {
enum QueryChannelIdAndEnviroment {
ChannelId,
Enviroment,
}
let channel_id: Option<ChannelId> = room::Entity::find()
let (channel_id, release_channel): (Option<ChannelId>, Option<String>) =
room::Entity::find()
.select_only()
.column(room::Column::ChannelId)
.column(room::Column::Enviroment)
.filter(room::Column::Id.eq(room_id))
.into_values::<_, QueryChannelIdAndEnviroment>()
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such room"))?;
if let Some(release_channel) = release_channel {
if &release_channel != enviroment {
Err(anyhow!("must join using the {} release", release_channel))?;
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryParticipantIndices {
ParticipantIndex,
}
let existing_participant_indices: Vec<i32> = room_participant::Entity::find()
.filter(
room_participant::Column::RoomId
.eq(room_id)
.and(room_participant::Column::ParticipantIndex.is_not_null()),
)
.select_only()
.column(room::Column::ChannelId)
.filter(room::Column::Id.eq(room_id))
.into_values::<_, QueryChannelId>()
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such room"))?;
.column(room_participant::Column::ParticipantIndex)
.into_values::<_, QueryParticipantIndices>()
.all(&*tx)
.await?;
let mut participant_index = 0;
while existing_participant_indices.contains(&participant_index) {
participant_index += 1;
}
if let Some(channel_id) = channel_id {
self.check_user_is_channel_member(channel_id, user_id, &*tx)
@@ -300,6 +336,7 @@ impl Database {
calling_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
participant_index: ActiveValue::Set(Some(participant_index)),
..Default::default()
}])
.on_conflict(
@@ -308,6 +345,7 @@ impl Database {
room_participant::Column::AnsweringConnectionId,
room_participant::Column::AnsweringConnectionServerId,
room_participant::Column::AnsweringConnectionLost,
room_participant::Column::ParticipantIndex,
])
.to_owned(),
)
@@ -322,6 +360,7 @@ impl Database {
.add(room_participant::Column::AnsweringConnectionId.is_null()),
)
.set(room_participant::ActiveModel {
participant_index: ActiveValue::Set(Some(participant_index)),
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
answering_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
@@ -793,10 +832,7 @@ impl Database {
let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
let deleted = if room.participants.is_empty() {
let result = room::Entity::delete_by_id(room_id)
.filter(room::Column::ChannelId.is_null())
.exec(&*tx)
.await?;
let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?;
result.rows_affected > 0
} else {
false
@@ -960,6 +996,39 @@ impl Database {
Ok(room)
}
pub async fn room_connection_ids(
&self,
room_id: RoomId,
connection_id: ConnectionId,
) -> Result<RoomGuard<HashSet<ConnectionId>>> {
self.room_transaction(room_id, |tx| async move {
let mut participants = room_participant::Entity::find()
.filter(room_participant::Column::RoomId.eq(room_id))
.stream(&*tx)
.await?;
let mut is_participant = false;
let mut connection_ids = HashSet::default();
while let Some(participant) = participants.next().await {
let participant = participant?;
if let Some(answering_connection) = participant.answering_connection() {
if answering_connection == connection_id {
is_participant = true;
} else {
connection_ids.insert(answering_connection);
}
}
}
if !is_participant {
Err(anyhow!("not a room participant"))?;
}
Ok(connection_ids)
})
.await
}
async fn get_channel_room(
&self,
room_id: RoomId,
@@ -978,10 +1047,15 @@ impl Database {
let mut pending_participants = Vec::new();
while let Some(db_participant) = db_participants.next().await {
let db_participant = db_participant?;
if let Some((answering_connection_id, answering_connection_server_id)) = db_participant
.answering_connection_id
.zip(db_participant.answering_connection_server_id)
{
if let (
Some(answering_connection_id),
Some(answering_connection_server_id),
Some(participant_index),
) = (
db_participant.answering_connection_id,
db_participant.answering_connection_server_id,
db_participant.participant_index,
) {
let location = match (
db_participant.location_kind,
db_participant.location_project_id,
@@ -1012,6 +1086,7 @@ impl Database {
peer_id: Some(answering_connection.into()),
projects: Default::default(),
location: Some(proto::ParticipantLocation { variant: location }),
participant_index: participant_index as u32,
},
);
} else {

View File

@@ -184,7 +184,7 @@ impl Database {
Ok(user::Entity::find()
.from_raw_sql(Statement::from_sql_and_values(
self.pool.get_database_backend(),
query.into(),
query,
vec![like_string.into(), name_query.into(), limit.into()],
))
.all(&*tx)

View File

@@ -12,6 +12,8 @@ pub mod contact;
pub mod feature_flag;
pub mod follower;
pub mod language_server;
pub mod observed_buffer_edits;
pub mod observed_channel_messages;
pub mod project;
pub mod project_collaborator;
pub mod room;

View File

@@ -0,0 +1,43 @@
use crate::db::{BufferId, UserId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "observed_buffer_edits")]
pub struct Model {
#[sea_orm(primary_key)]
pub user_id: UserId,
pub buffer_id: BufferId,
pub epoch: i32,
pub lamport_timestamp: i32,
pub replica_id: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::buffer::Entity",
from = "Column::BufferId",
to = "super::buffer::Column::Id"
)]
Buffer,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
}
impl Related<super::buffer::Entity> for Entity {
fn to() -> RelationDef {
Relation::Buffer.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,41 @@
use crate::db::{ChannelId, MessageId, UserId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "observed_channel_messages")]
pub struct Model {
#[sea_orm(primary_key)]
pub user_id: UserId,
pub channel_id: ChannelId,
pub channel_message_id: MessageId,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::channel::Entity",
from = "Column::ChannelId",
to = "super::channel::Column::Id"
)]
Channel,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
}
impl Related<super::channel::Entity> for Entity {
fn to() -> RelationDef {
Relation::Channel.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -8,6 +8,7 @@ pub struct Model {
pub id: RoomId,
pub live_kit_room: String,
pub channel_id: Option<ChannelId>,
pub enviroment: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -1,4 +1,5 @@
use crate::db::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId};
use rpc::ConnectionId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
@@ -17,6 +18,16 @@ pub struct Model {
pub calling_user_id: UserId,
pub calling_connection_id: i32,
pub calling_connection_server_id: Option<ServerId>,
pub participant_index: Option<i32>,
}
impl Model {
pub fn answering_connection(&self) -> Option<ConnectionId> {
Some(ConnectionId {
owner_id: self.answering_connection_server_id?.0 as u32,
id: self.answering_connection_id? as u32,
})
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -12,6 +12,8 @@ use sea_orm::ConnectionTrait;
use sqlx::migrate::MigrateDatabase;
use std::sync::Arc;
const TEST_RELEASE_CHANNEL: &'static str = "test";
pub struct TestDb {
pub db: Option<Arc<Database>>,
pub connection: Option<sqlx::AnyConnection>,
@@ -39,7 +41,7 @@ impl TestDb {
db.pool
.execute(sea_orm::Statement::from_string(
db.pool.get_database_backend(),
sql.into(),
sql,
))
.await
.unwrap();
@@ -134,7 +136,7 @@ impl Drop for TestDb {
db.pool
.execute(sea_orm::Statement::from_string(
db.pool.get_database_backend(),
query.into(),
query,
))
.await
.log_err();

View File

@@ -1,6 +1,6 @@
use super::*;
use crate::test_both_dbs;
use language::proto;
use language::proto::{self, serialize_version};
use text::Buffer;
test_both_dbs!(
@@ -54,7 +54,7 @@ async fn test_channel_buffers(db: &Arc<Database>) {
let owner_id = db.create_server("production").await.unwrap().0 as u32;
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
db.invite_channel_member(zed_id, b_id, a_id, false)
.await
@@ -134,14 +134,14 @@ async fn test_channel_buffers(db: &Arc<Database>) {
let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap();
assert_eq!(zed_collaborats, &[a_id, b_id]);
let collaborators = db
let left_buffer = db
.leave_channel_buffer(zed_id, connection_id_b)
.await
.unwrap();
assert_eq!(collaborators, &[connection_id_a],);
assert_eq!(left_buffer.connections, &[connection_id_a],);
let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap();
let cargo_id = db.create_root_channel("cargo", a_id).await.unwrap();
let _ = db
.join_channel_buffer(cargo_id, a_id, connection_id_a)
.await
@@ -163,3 +163,349 @@ async fn test_channel_buffers(db: &Arc<Database>) {
assert_eq!(buffer_response_b.base_text, "hello, cruel world");
assert_eq!(buffer_response_b.operations, &[]);
}
test_both_dbs!(
test_channel_buffers_last_operations,
test_channel_buffers_last_operations_postgres,
test_channel_buffers_last_operations_sqlite
);
async fn test_channel_buffers_last_operations(db: &Database) {
let user_id = db
.create_user(
"user_a@example.com",
false,
NewUserParams {
github_login: "user_a".into(),
github_user_id: 101,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let observer_id = db
.create_user(
"user_b@example.com",
false,
NewUserParams {
github_login: "user_b".into(),
github_user_id: 102,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let owner_id = db.create_server("production").await.unwrap().0 as u32;
let connection_id = ConnectionId {
owner_id,
id: user_id.0 as u32,
};
let mut buffers = Vec::new();
let mut text_buffers = Vec::new();
for i in 0..3 {
let channel = db
.create_root_channel(&format!("channel-{i}"), user_id)
.await
.unwrap();
db.invite_channel_member(channel, observer_id, user_id, false)
.await
.unwrap();
db.respond_to_channel_invite(channel, observer_id, true)
.await
.unwrap();
db.join_channel_buffer(channel, user_id, connection_id)
.await
.unwrap();
buffers.push(
db.transaction(|tx| async move { db.get_channel_buffer(channel, &*tx).await })
.await
.unwrap(),
);
text_buffers.push(Buffer::new(0, 0, "".to_string()));
}
let operations = db
.transaction(|tx| {
let buffers = &buffers;
async move {
db.get_latest_operations_for_buffers([buffers[0].id, buffers[2].id], &*tx)
.await
}
})
.await
.unwrap();
assert!(operations.is_empty());
update_buffer(
buffers[0].channel_id,
user_id,
db,
vec![
text_buffers[0].edit([(0..0, "a")]),
text_buffers[0].edit([(0..0, "b")]),
text_buffers[0].edit([(0..0, "c")]),
],
)
.await;
update_buffer(
buffers[1].channel_id,
user_id,
db,
vec![
text_buffers[1].edit([(0..0, "d")]),
text_buffers[1].edit([(1..1, "e")]),
text_buffers[1].edit([(2..2, "f")]),
],
)
.await;
// cause buffer 1's epoch to increment.
db.leave_channel_buffer(buffers[1].channel_id, connection_id)
.await
.unwrap();
db.join_channel_buffer(buffers[1].channel_id, user_id, connection_id)
.await
.unwrap();
text_buffers[1] = Buffer::new(1, 0, "def".to_string());
update_buffer(
buffers[1].channel_id,
user_id,
db,
vec![
text_buffers[1].edit([(0..0, "g")]),
text_buffers[1].edit([(0..0, "h")]),
],
)
.await;
update_buffer(
buffers[2].channel_id,
user_id,
db,
vec![text_buffers[2].edit([(0..0, "i")])],
)
.await;
let operations = db
.transaction(|tx| {
let buffers = &buffers;
async move {
db.get_latest_operations_for_buffers([buffers[1].id, buffers[2].id], &*tx)
.await
}
})
.await
.unwrap();
assert_operations(
&operations,
&[
(buffers[1].id, 1, &text_buffers[1]),
(buffers[2].id, 0, &text_buffers[2]),
],
);
let operations = db
.transaction(|tx| {
let buffers = &buffers;
async move {
db.get_latest_operations_for_buffers([buffers[0].id, buffers[1].id], &*tx)
.await
}
})
.await
.unwrap();
assert_operations(
&operations,
&[
(buffers[0].id, 0, &text_buffers[0]),
(buffers[1].id, 1, &text_buffers[1]),
],
);
let buffer_changes = db
.transaction(|tx| {
let buffers = &buffers;
async move {
db.unseen_channel_buffer_changes(
observer_id,
&[
buffers[0].channel_id,
buffers[1].channel_id,
buffers[2].channel_id,
],
&*tx,
)
.await
}
})
.await
.unwrap();
pretty_assertions::assert_eq!(
buffer_changes,
[
rpc::proto::UnseenChannelBufferChange {
channel_id: buffers[0].channel_id.to_proto(),
epoch: 0,
version: serialize_version(&text_buffers[0].version()),
},
rpc::proto::UnseenChannelBufferChange {
channel_id: buffers[1].channel_id.to_proto(),
epoch: 1,
version: serialize_version(&text_buffers[1].version())
.into_iter()
.filter(|vector| vector.replica_id
== buffer_changes[1].version.first().unwrap().replica_id)
.collect::<Vec<_>>(),
},
rpc::proto::UnseenChannelBufferChange {
channel_id: buffers[2].channel_id.to_proto(),
epoch: 0,
version: serialize_version(&text_buffers[2].version()),
},
]
);
db.observe_buffer_version(
buffers[1].id,
observer_id,
1,
serialize_version(&text_buffers[1].version()).as_slice(),
)
.await
.unwrap();
let buffer_changes = db
.transaction(|tx| {
let buffers = &buffers;
async move {
db.unseen_channel_buffer_changes(
observer_id,
&[
buffers[0].channel_id,
buffers[1].channel_id,
buffers[2].channel_id,
],
&*tx,
)
.await
}
})
.await
.unwrap();
assert_eq!(
buffer_changes,
[
rpc::proto::UnseenChannelBufferChange {
channel_id: buffers[0].channel_id.to_proto(),
epoch: 0,
version: serialize_version(&text_buffers[0].version()),
},
rpc::proto::UnseenChannelBufferChange {
channel_id: buffers[2].channel_id.to_proto(),
epoch: 0,
version: serialize_version(&text_buffers[2].version()),
},
]
);
// Observe an earlier version of the buffer.
db.observe_buffer_version(
buffers[1].id,
observer_id,
1,
&[rpc::proto::VectorClockEntry {
replica_id: 0,
timestamp: 0,
}],
)
.await
.unwrap();
let buffer_changes = db
.transaction(|tx| {
let buffers = &buffers;
async move {
db.unseen_channel_buffer_changes(
observer_id,
&[
buffers[0].channel_id,
buffers[1].channel_id,
buffers[2].channel_id,
],
&*tx,
)
.await
}
})
.await
.unwrap();
assert_eq!(
buffer_changes,
[
rpc::proto::UnseenChannelBufferChange {
channel_id: buffers[0].channel_id.to_proto(),
epoch: 0,
version: serialize_version(&text_buffers[0].version()),
},
rpc::proto::UnseenChannelBufferChange {
channel_id: buffers[2].channel_id.to_proto(),
epoch: 0,
version: serialize_version(&text_buffers[2].version()),
},
]
);
}
async fn update_buffer(
channel_id: ChannelId,
user_id: UserId,
db: &Database,
operations: Vec<text::Operation>,
) {
let operations = operations
.into_iter()
.map(|op| proto::serialize_operation(&language::Operation::Buffer(op)))
.collect::<Vec<_>>();
db.update_channel_buffer(channel_id, user_id, &operations)
.await
.unwrap();
}
fn assert_operations(
operations: &[buffer_operation::Model],
expected: &[(BufferId, i32, &text::Buffer)],
) {
let actual = operations
.iter()
.map(|op| buffer_operation::Model {
buffer_id: op.buffer_id,
epoch: op.epoch,
lamport_timestamp: op.lamport_timestamp,
replica_id: op.replica_id,
value: vec![],
})
.collect::<Vec<_>>();
let expected = expected
.iter()
.map(|(buffer_id, epoch, buffer)| buffer_operation::Model {
buffer_id: *buffer_id,
epoch: *epoch,
lamport_timestamp: buffer.lamport_clock.value as i32 - 1,
replica_id: buffer.replica_id() as i32,
value: vec![],
})
.collect::<Vec<_>>();
assert_eq!(actual, expected, "unexpected operations")
}

View File

@@ -5,7 +5,11 @@ use rpc::{
};
use crate::{
db::{queries::channels::ChannelGraph, tests::graph, ChannelId, Database, NewUserParams},
db::{
queries::channels::ChannelGraph,
tests::{graph, TEST_RELEASE_CHANNEL},
ChannelId, Database, NewUserParams,
},
test_both_dbs,
};
use std::sync::Arc;
@@ -41,7 +45,7 @@ async fn test_channels(db: &Arc<Database>) {
.unwrap()
.user_id;
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
// Make sure that people cannot read channels they haven't been invited to
assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none());
@@ -54,16 +58,13 @@ async fn test_channels(db: &Arc<Database>) {
.await
.unwrap();
let crdb_id = db
.create_channel("crdb", Some(zed_id), "2", a_id)
.await
.unwrap();
let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap();
let livestreaming_id = db
.create_channel("livestreaming", Some(zed_id), "3", a_id)
.create_channel("livestreaming", Some(zed_id), a_id)
.await
.unwrap();
let replace_id = db
.create_channel("replace", Some(zed_id), "4", a_id)
.create_channel("replace", Some(zed_id), a_id)
.await
.unwrap();
@@ -71,14 +72,14 @@ async fn test_channels(db: &Arc<Database>) {
members.sort();
assert_eq!(members, &[a_id, b_id]);
let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap();
let rust_id = db.create_root_channel("rust", a_id).await.unwrap();
let cargo_id = db
.create_channel("cargo", Some(rust_id), "6", a_id)
.create_channel("cargo", Some(rust_id), a_id)
.await
.unwrap();
let cargo_ra_id = db
.create_channel("cargo-ra", Some(cargo_id), "7", a_id)
.create_channel("cargo-ra", Some(cargo_id), a_id)
.await
.unwrap();
@@ -198,15 +199,20 @@ async fn test_joining_channels(db: &Arc<Database>) {
.unwrap()
.user_id;
let channel_1 = db
.create_root_channel("channel_1", "1", user_1)
let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
let room_1 = db
.get_or_create_channel_room(channel_1, "1", TEST_RELEASE_CHANNEL)
.await
.unwrap();
let room_1 = db.room_id_for_channel(channel_1).await.unwrap();
// can join a room with membership to its channel
let joined_room = db
.join_room(room_1, user_1, ConnectionId { owner_id, id: 1 })
.join_room(
room_1,
user_1,
ConnectionId { owner_id, id: 1 },
TEST_RELEASE_CHANNEL,
)
.await
.unwrap();
assert_eq!(joined_room.room.participants.len(), 1);
@@ -214,7 +220,12 @@ async fn test_joining_channels(db: &Arc<Database>) {
drop(joined_room);
// cannot join a room without membership to its channel
assert!(db
.join_room(room_1, user_2, ConnectionId { owner_id, id: 1 })
.join_room(
room_1,
user_2,
ConnectionId { owner_id, id: 1 },
TEST_RELEASE_CHANNEL
)
.await
.is_err());
}
@@ -269,15 +280,9 @@ async fn test_channel_invites(db: &Arc<Database>) {
.unwrap()
.user_id;
let channel_1_1 = db
.create_root_channel("channel_1", "1", user_1)
.await
.unwrap();
let channel_1_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
let channel_1_2 = db
.create_root_channel("channel_2", "2", user_1)
.await
.unwrap();
let channel_1_2 = db.create_root_channel("channel_2", user_1).await.unwrap();
db.invite_channel_member(channel_1_1, user_2, user_1, false)
.await
@@ -339,7 +344,7 @@ async fn test_channel_invites(db: &Arc<Database>) {
.unwrap();
let channel_1_3 = db
.create_channel("channel_3", Some(channel_1_1), "1", user_1)
.create_channel("channel_3", Some(channel_1_1), user_1)
.await
.unwrap();
@@ -401,7 +406,7 @@ async fn test_channel_renames(db: &Arc<Database>) {
.unwrap()
.user_id;
let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap();
let zed_id = db.create_root_channel("zed", user_1).await.unwrap();
db.rename_channel(zed_id, user_1, "#zed-archive")
.await
@@ -446,25 +451,22 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
.unwrap()
.user_id;
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
let crdb_id = db
.create_channel("crdb", Some(zed_id), "2", a_id)
.await
.unwrap();
let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap();
let gpui2_id = db
.create_channel("gpui2", Some(zed_id), "3", a_id)
.create_channel("gpui2", Some(zed_id), a_id)
.await
.unwrap();
let livestreaming_id = db
.create_channel("livestreaming", Some(crdb_id), "4", a_id)
.create_channel("livestreaming", Some(crdb_id), a_id)
.await
.unwrap();
let livestreaming_dag_id = db
.create_channel("livestreaming_dag", Some(livestreaming_id), "5", a_id)
.create_channel("livestreaming_dag", Some(livestreaming_id), a_id)
.await
.unwrap();
@@ -517,12 +519,7 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
// ========================================================================
// Create a new channel below a channel with multiple parents
let livestreaming_dag_sub_id = db
.create_channel(
"livestreaming_dag_sub",
Some(livestreaming_dag_id),
"6",
a_id,
)
.create_channel("livestreaming_dag_sub", Some(livestreaming_dag_id), a_id)
.await
.unwrap();
@@ -791,6 +788,64 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
assert!(result.channels.is_empty())
}
test_both_dbs!(
test_db_channel_moving_bugs,
test_db_channel_moving_bugs_postgres,
test_db_channel_moving_bugs_sqlite
);
async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
let user_id = db
.create_user(
"user1@example.com",
false,
NewUserParams {
github_login: "user1".into(),
github_user_id: 5,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let zed_id = db.create_root_channel("zed", user_id).await.unwrap();
let projects_id = db
.create_channel("projects", Some(zed_id), user_id)
.await
.unwrap();
let livestreaming_id = db
.create_channel("livestreaming", Some(projects_id), user_id)
.await
.unwrap();
// Dag is: zed - projects - livestreaming
// Move to same parent should be a no-op
assert!(db
.move_channel(user_id, projects_id, zed_id, zed_id)
.await
.unwrap()
.is_empty());
// Stranding a channel should retain it's sub channels
db.unlink_channel(user_id, projects_id, zed_id)
.await
.unwrap();
let result = db.get_channels_for_user(user_id).await.unwrap();
assert_dag(
result.channels,
&[
(zed_id, None),
(projects_id, None),
(livestreaming_id, Some(projects_id)),
],
);
}
#[track_caller]
fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)]) {
let mut actual_map: HashMap<ChannelId, HashSet<ChannelId>> = HashMap::default();

View File

@@ -479,7 +479,7 @@ async fn test_project_count(db: &Arc<Database>) {
.unwrap();
let room_id = RoomId::from_proto(
db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "")
db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "", "dev")
.await
.unwrap()
.id,
@@ -493,9 +493,14 @@ async fn test_project_count(db: &Arc<Database>) {
)
.await
.unwrap();
db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 })
.await
.unwrap();
db.join_room(
room_id,
user2.user_id,
ConnectionId { owner_id, id: 1 },
"dev",
)
.await
.unwrap();
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
@@ -575,6 +580,85 @@ async fn test_fuzzy_search_users() {
}
}
test_both_dbs!(
test_non_matching_release_channels,
test_non_matching_release_channels_postgres,
test_non_matching_release_channels_sqlite
);
async fn test_non_matching_release_channels(db: &Arc<Database>) {
let owner_id = db.create_server("test").await.unwrap().0 as u32;
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 = db
.create_room(
user1.user_id,
ConnectionId { owner_id, id: 0 },
"",
"stable",
)
.await
.unwrap();
db.call(
RoomId::from_proto(room.id),
user1.user_id,
ConnectionId { owner_id, id: 0 },
user2.user_id,
None,
)
.await
.unwrap();
// User attempts to join from preview
let result = db
.join_room(
RoomId::from_proto(room.id),
user2.user_id,
ConnectionId { owner_id, id: 1 },
"preview",
)
.await;
assert!(result.is_err());
// User switches to stable
let result = db
.join_room(
RoomId::from_proto(room.id),
user2.user_id,
ConnectionId { owner_id, id: 1 },
"stable",
)
.await;
assert!(result.is_ok())
}
fn build_background_executor() -> Arc<Background> {
Deterministic::new(0).build_background()
}

View File

@@ -1,10 +1,72 @@
use crate::{
db::{Database, NewUserParams},
db::{Database, MessageId, NewUserParams},
test_both_dbs,
};
use std::sync::Arc;
use time::OffsetDateTime;
test_both_dbs!(
test_channel_message_retrieval,
test_channel_message_retrieval_postgres,
test_channel_message_retrieval_sqlite
);
async fn test_channel_message_retrieval(db: &Arc<Database>) {
let user = db
.create_user(
"user@example.com",
false,
NewUserParams {
github_login: "user".into(),
github_user_id: 1,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let channel = db.create_channel("channel", None, user).await.unwrap();
let owner_id = db.create_server("test").await.unwrap().0 as u32;
db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user)
.await
.unwrap();
let mut all_messages = Vec::new();
for i in 0..10 {
all_messages.push(
db.create_channel_message(channel, user, &i.to_string(), OffsetDateTime::now_utc(), i)
.await
.unwrap()
.0
.to_proto(),
);
}
let messages = db
.get_channel_messages(channel, user, 3, None)
.await
.unwrap()
.into_iter()
.map(|message| message.id)
.collect::<Vec<_>>();
assert_eq!(messages, &all_messages[7..10]);
let messages = db
.get_channel_messages(
channel,
user,
4,
Some(MessageId::from_proto(all_messages[6])),
)
.await
.unwrap()
.into_iter()
.map(|message| message.id)
.collect::<Vec<_>>();
assert_eq!(messages, &all_messages[2..6]);
}
test_both_dbs!(
test_channel_message_nonces,
test_channel_message_nonces_postgres,
@@ -25,10 +87,7 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
.await
.unwrap()
.user_id;
let channel = db
.create_channel("channel", None, "room", user)
.await
.unwrap();
let channel = db.create_channel("channel", None, user).await.unwrap();
let owner_id = db.create_server("test").await.unwrap().0 as u32;
@@ -57,3 +116,182 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
assert_eq!(msg1_id, msg3_id);
assert_eq!(msg2_id, msg4_id);
}
test_both_dbs!(
test_channel_message_new_notification,
test_channel_message_new_notification_postgres,
test_channel_message_new_notification_sqlite
);
async fn test_channel_message_new_notification(db: &Arc<Database>) {
let user = db
.create_user(
"user_a@example.com",
false,
NewUserParams {
github_login: "user_a".into(),
github_user_id: 1,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let observer = db
.create_user(
"user_b@example.com",
false,
NewUserParams {
github_login: "user_b".into(),
github_user_id: 1,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let channel_1 = db.create_channel("channel", None, user).await.unwrap();
let channel_2 = db.create_channel("channel-2", None, user).await.unwrap();
db.invite_channel_member(channel_1, observer, user, false)
.await
.unwrap();
db.respond_to_channel_invite(channel_1, observer, true)
.await
.unwrap();
db.invite_channel_member(channel_2, observer, user, false)
.await
.unwrap();
db.respond_to_channel_invite(channel_2, observer, true)
.await
.unwrap();
let owner_id = db.create_server("test").await.unwrap().0 as u32;
let user_connection_id = rpc::ConnectionId { owner_id, id: 0 };
db.join_channel_chat(channel_1, user_connection_id, user)
.await
.unwrap();
let _ = db
.create_channel_message(channel_1, user, "1_1", OffsetDateTime::now_utc(), 1)
.await
.unwrap();
let (second_message, _, _) = db
.create_channel_message(channel_1, user, "1_2", OffsetDateTime::now_utc(), 2)
.await
.unwrap();
let (third_message, _, _) = db
.create_channel_message(channel_1, user, "1_3", OffsetDateTime::now_utc(), 3)
.await
.unwrap();
db.join_channel_chat(channel_2, user_connection_id, user)
.await
.unwrap();
let (fourth_message, _, _) = db
.create_channel_message(channel_2, user, "2_1", OffsetDateTime::now_utc(), 4)
.await
.unwrap();
// Check that observer has new messages
let unseen_messages = db
.transaction(|tx| async move {
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
.await
})
.await
.unwrap();
assert_eq!(
unseen_messages,
[
rpc::proto::UnseenChannelMessage {
channel_id: channel_1.to_proto(),
message_id: third_message.to_proto(),
},
rpc::proto::UnseenChannelMessage {
channel_id: channel_2.to_proto(),
message_id: fourth_message.to_proto(),
},
]
);
// Observe the second message
db.observe_channel_message(channel_1, observer, second_message)
.await
.unwrap();
// Make sure the observer still has a new message
let unseen_messages = db
.transaction(|tx| async move {
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
.await
})
.await
.unwrap();
assert_eq!(
unseen_messages,
[
rpc::proto::UnseenChannelMessage {
channel_id: channel_1.to_proto(),
message_id: third_message.to_proto(),
},
rpc::proto::UnseenChannelMessage {
channel_id: channel_2.to_proto(),
message_id: fourth_message.to_proto(),
},
]
);
// Observe the third message,
db.observe_channel_message(channel_1, observer, third_message)
.await
.unwrap();
// Make sure the observer does not have a new method
let unseen_messages = db
.transaction(|tx| async move {
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
.await
})
.await
.unwrap();
assert_eq!(
unseen_messages,
[rpc::proto::UnseenChannelMessage {
channel_id: channel_2.to_proto(),
message_id: fourth_message.to_proto(),
}]
);
// Observe the second message again, should not regress our observed state
db.observe_channel_message(channel_1, observer, second_message)
.await
.unwrap();
// Make sure the observer does not have a new message
let unseen_messages = db
.transaction(|tx| async move {
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
.await
})
.await
.unwrap();
assert_eq!(
unseen_messages,
[rpc::proto::UnseenChannelMessage {
channel_id: channel_2.to_proto(),
message_id: fourth_message.to_proto(),
}]
);
}

View File

@@ -3,8 +3,8 @@ mod connection_pool;
use crate::{
auth,
db::{
self, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, ServerId, User,
UserId,
self, BufferId, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId,
ServerId, User, UserId,
},
executor::Executor,
AppState, Result,
@@ -38,8 +38,8 @@ use lazy_static::lazy_static;
use prometheus::{register_int_gauge, IntGauge};
use rpc::{
proto::{
self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, ChannelEdge, EntityMessage,
EnvelopedMessage, LiveKitConnectionInfo, RequestMessage,
self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage,
LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators,
},
Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
};
@@ -63,6 +63,7 @@ use time::OffsetDateTime;
use tokio::sync::{watch, Semaphore};
use tower::ServiceBuilder;
use tracing::{info_span, instrument, Instrument};
use util::channel::RELEASE_CHANNEL_NAME;
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
@@ -274,7 +275,9 @@ impl Server {
.add_message_handler(unfollow)
.add_message_handler(update_followers)
.add_message_handler(update_diff_base)
.add_request_handler(get_private_user_info);
.add_request_handler(get_private_user_info)
.add_message_handler(acknowledge_channel_message)
.add_message_handler(acknowledge_buffer_version);
Arc::new(server)
}
@@ -313,9 +316,16 @@ impl Server {
.trace_err()
{
for connection_id in refreshed_channel_buffer.connection_ids {
for message in &refreshed_channel_buffer.removed_collaborators {
peer.send(connection_id, message.clone()).trace_err();
}
peer.send(
connection_id,
proto::UpdateChannelBufferCollaborators {
channel_id: channel_id.to_proto(),
collaborators: refreshed_channel_buffer
.collaborators
.clone(),
},
)
.trace_err();
}
}
}
@@ -928,11 +938,6 @@ async fn create_room(
util::async_iife!({
let live_kit = live_kit?;
live_kit
.create_room(live_kit_room.clone())
.await
.trace_err()?;
let token = live_kit
.room_token(&live_kit_room, &session.user_id.to_string())
.trace_err()?;
@@ -948,7 +953,12 @@ async fn create_room(
let room = session
.db()
.await
.create_room(session.user_id, session.connection_id, &live_kit_room)
.create_room(
session.user_id,
session.connection_id,
&live_kit_room,
RELEASE_CHANNEL_NAME.as_str(),
)
.await?;
response.send(proto::CreateRoomResponse {
@@ -970,7 +980,12 @@ async fn join_room(
let room = session
.db()
.await
.join_room(room_id, session.user_id, session.connection_id)
.join_room(
room_id,
session.user_id,
session.connection_id,
RELEASE_CHANNEL_NAME.as_str(),
)
.await?;
room_updated(&room.room, &session.peer);
room.into_inner()
@@ -1883,94 +1898,94 @@ async fn follow(
response: Response<proto::Follow>,
session: Session,
) -> Result<()> {
let project_id = ProjectId::from_proto(request.project_id);
let room_id = RoomId::from_proto(request.room_id);
let project_id = request.project_id.map(ProjectId::from_proto);
let leader_id = request
.leader_id
.ok_or_else(|| anyhow!("invalid leader id"))?
.into();
let follower_id = session.connection_id;
{
let project_connection_ids = session
.db()
.await
.project_connection_ids(project_id, session.connection_id)
.await?;
session
.db()
.await
.check_room_participants(room_id, leader_id, session.connection_id)
.await?;
if !project_connection_ids.contains(&leader_id) {
Err(anyhow!("no such peer"))?;
}
}
let mut response_payload = session
let response_payload = session
.peer
.forward_request(session.connection_id, leader_id, request)
.await?;
response_payload
.views
.retain(|view| view.leader_id != Some(follower_id.into()));
response.send(response_payload)?;
let room = session
.db()
.await
.follow(project_id, leader_id, follower_id)
.await?;
room_updated(&room, &session.peer);
if let Some(project_id) = project_id {
let room = session
.db()
.await
.follow(room_id, project_id, leader_id, follower_id)
.await?;
room_updated(&room, &session.peer);
}
Ok(())
}
async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
let project_id = ProjectId::from_proto(request.project_id);
let room_id = RoomId::from_proto(request.room_id);
let project_id = request.project_id.map(ProjectId::from_proto);
let leader_id = request
.leader_id
.ok_or_else(|| anyhow!("invalid leader id"))?
.into();
let follower_id = session.connection_id;
if !session
session
.db()
.await
.project_connection_ids(project_id, session.connection_id)
.await?
.contains(&leader_id)
{
Err(anyhow!("no such peer"))?;
}
.check_room_participants(room_id, leader_id, session.connection_id)
.await?;
session
.peer
.forward_send(session.connection_id, leader_id, request)?;
let room = session
.db()
.await
.unfollow(project_id, leader_id, follower_id)
.await?;
room_updated(&room, &session.peer);
if let Some(project_id) = project_id {
let room = session
.db()
.await
.unfollow(room_id, project_id, leader_id, follower_id)
.await?;
room_updated(&room, &session.peer);
}
Ok(())
}
async fn update_followers(request: proto::UpdateFollowers, session: Session) -> Result<()> {
let project_id = ProjectId::from_proto(request.project_id);
let project_connection_ids = session
.db
.lock()
.await
.project_connection_ids(project_id, session.connection_id)
.await?;
let room_id = RoomId::from_proto(request.room_id);
let database = session.db.lock().await;
let leader_id = request.variant.as_ref().and_then(|variant| match variant {
proto::update_followers::Variant::CreateView(payload) => payload.leader_id,
let connection_ids = if let Some(project_id) = request.project_id {
let project_id = ProjectId::from_proto(project_id);
database
.project_connection_ids(project_id, session.connection_id)
.await?
} else {
database
.room_connection_ids(room_id, session.connection_id)
.await?
};
// For now, don't send view update messages back to that view's current leader.
let connection_id_to_omit = request.variant.as_ref().and_then(|variant| match variant {
proto::update_followers::Variant::UpdateView(payload) => payload.leader_id,
proto::update_followers::Variant::UpdateActiveView(payload) => payload.leader_id,
_ => None,
});
for follower_peer_id in request.follower_ids.iter().copied() {
let follower_connection_id = follower_peer_id.into();
if project_connection_ids.contains(&follower_connection_id)
&& Some(follower_peer_id) != leader_id
if Some(follower_peer_id) != connection_id_to_omit
&& connection_ids.contains(&follower_connection_id)
{
session.peer.forward_send(
session.connection_id,
@@ -2186,15 +2201,10 @@ async fn create_channel(
session: Session,
) -> Result<()> {
let db = session.db().await;
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
if let Some(live_kit) = session.live_kit_client.as_ref() {
live_kit.create_room(live_kit_room.clone()).await?;
}
let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id));
let id = db
.create_channel(&request.name, parent_id, &live_kit_room, session.user_id)
.create_channel(&request.name, parent_id, session.user_id)
.await?;
let channel = proto::Channel {
@@ -2474,6 +2484,11 @@ async fn move_channel(
.move_channel(session.user_id, channel_id, from_parent, to)
.await?;
if channels_to_send.is_empty() {
response.send(Ack {})?;
return Ok(());
}
let members_from = db.get_channel_members(from_parent).await?;
let members_to = db.get_channel_members(to).await?;
@@ -2556,6 +2571,8 @@ async fn respond_to_channel_invite(
name: channel.name,
}),
);
update.unseen_channel_messages = result.channel_messages;
update.unseen_channel_buffer_changes = result.unseen_buffer_changes;
update.insert_edge = result.channels.edges;
update
.channel_participants
@@ -2592,15 +2609,23 @@ async fn join_channel(
session: Session,
) -> Result<()> {
let channel_id = ChannelId::from_proto(request.channel_id);
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
let joined_room = {
leave_room_for_session(&session).await?;
let db = session.db().await;
let room_id = db.room_id_for_channel(channel_id).await?;
let room_id = db
.get_or_create_channel_room(channel_id, &live_kit_room, &*RELEASE_CHANNEL_NAME)
.await?;
let joined_room = db
.join_room(room_id, session.user_id, session.connection_id)
.join_room(
room_id,
session.user_id,
session.connection_id,
RELEASE_CHANNEL_NAME.as_str(),
)
.await?;
let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| {
@@ -2653,18 +2678,12 @@ async fn join_channel_buffer(
.join_channel_buffer(channel_id, session.user_id, session.connection_id)
.await?;
let replica_id = open_response.replica_id;
let collaborators = open_response.collaborators.clone();
response.send(open_response)?;
let update = AddChannelBufferCollaborator {
let update = UpdateChannelBufferCollaborators {
channel_id: channel_id.to_proto(),
collaborator: Some(proto::Collaborator {
user_id: session.user_id.to_proto(),
peer_id: Some(session.connection_id.into()),
replica_id,
}),
collaborators: collaborators.clone(),
};
channel_buffer_updated(
session.connection_id,
@@ -2685,7 +2704,7 @@ async fn update_channel_buffer(
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
let collaborators = db
let (collaborators, non_collaborators, epoch, version) = db
.update_channel_buffer(channel_id, session.user_id, &request.operations)
.await?;
@@ -2698,6 +2717,29 @@ async fn update_channel_buffer(
},
&session.peer,
);
let pool = &*session.connection_pool().await;
broadcast(
None,
non_collaborators
.iter()
.flat_map(|user_id| pool.user_connection_ids(*user_id)),
|peer_id| {
session.peer.send(
peer_id.into(),
proto::UpdateChannels {
unseen_channel_buffer_changes: vec![proto::UnseenChannelBufferChange {
channel_id: channel_id.to_proto(),
epoch: epoch as u64,
version: version.clone(),
}],
..Default::default()
},
)
},
);
Ok(())
}
@@ -2711,8 +2753,8 @@ async fn rejoin_channel_buffers(
.rejoin_channel_buffers(&request.buffers, session.user_id, session.connection_id)
.await?;
for buffer in &buffers {
let collaborators_to_notify = buffer
for rejoined_buffer in &buffers {
let collaborators_to_notify = rejoined_buffer
.buffer
.collaborators
.iter()
@@ -2720,10 +2762,9 @@ async fn rejoin_channel_buffers(
channel_buffer_updated(
session.connection_id,
collaborators_to_notify,
&proto::UpdateChannelBufferCollaborator {
channel_id: buffer.buffer.channel_id,
old_peer_id: Some(buffer.old_connection_id.into()),
new_peer_id: Some(session.connection_id.into()),
&proto::UpdateChannelBufferCollaborators {
channel_id: rejoined_buffer.buffer.channel_id,
collaborators: rejoined_buffer.buffer.collaborators.clone(),
},
&session.peer,
);
@@ -2744,7 +2785,7 @@ async fn leave_channel_buffer(
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
let collaborators_to_notify = db
let left_buffer = db
.leave_channel_buffer(channel_id, session.connection_id)
.await?;
@@ -2752,10 +2793,10 @@ async fn leave_channel_buffer(
channel_buffer_updated(
session.connection_id,
collaborators_to_notify,
&proto::RemoveChannelBufferCollaborator {
left_buffer.connections,
&proto::UpdateChannelBufferCollaborators {
channel_id: channel_id.to_proto(),
peer_id: Some(session.connection_id.into()),
collaborators: left_buffer.collaborators,
},
&session.peer,
);
@@ -2794,7 +2835,7 @@ async fn send_channel_message(
.ok_or_else(|| anyhow!("nonce can't be blank"))?;
let channel_id = ChannelId::from_proto(request.channel_id);
let (message_id, connection_ids) = session
let (message_id, connection_ids, non_participants) = session
.db()
.await
.create_channel_message(
@@ -2824,6 +2865,27 @@ async fn send_channel_message(
response.send(proto::SendChannelMessageResponse {
message: Some(message),
})?;
let pool = &*session.connection_pool().await;
broadcast(
None,
non_participants
.iter()
.flat_map(|user_id| pool.user_connection_ids(*user_id)),
|peer_id| {
session.peer.send(
peer_id.into(),
proto::UpdateChannels {
unseen_channel_messages: vec![proto::UnseenChannelMessage {
channel_id: channel_id.to_proto(),
message_id: message_id.to_proto(),
}],
..Default::default()
},
)
},
);
Ok(())
}
@@ -2846,6 +2908,38 @@ async fn remove_channel_message(
Ok(())
}
async fn acknowledge_channel_message(
request: proto::AckChannelMessage,
session: Session,
) -> Result<()> {
let channel_id = ChannelId::from_proto(request.channel_id);
let message_id = MessageId::from_proto(request.message_id);
session
.db()
.await
.observe_channel_message(channel_id, session.user_id, message_id)
.await?;
Ok(())
}
async fn acknowledge_buffer_version(
request: proto::AckBufferOperation,
session: Session,
) -> Result<()> {
let buffer_id = BufferId::from_proto(request.buffer_id);
session
.db()
.await
.observe_buffer_version(
buffer_id,
session.user_id,
request.epoch as i32,
&request.version,
)
.await?;
Ok(())
}
async fn join_channel_chat(
request: proto::JoinChannelChat,
response: Response<proto::JoinChannelChat>,
@@ -2981,6 +3075,8 @@ fn build_initial_channels_update(
});
}
update.unseen_channel_buffer_changes = channels.unseen_buffer_changes;
update.unseen_channel_messages = channels.channel_messages;
update.insert_edge = channels.channels.edges;
for (channel_id, participants) in channels.channel_participants {
@@ -3230,13 +3326,13 @@ async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> {
.leave_channel_buffers(session.connection_id)
.await?;
for (channel_id, connections) in left_channel_buffers {
for left_buffer in left_channel_buffers {
channel_buffer_updated(
session.connection_id,
connections,
&proto::RemoveChannelBufferCollaborator {
channel_id: channel_id.to_proto(),
peer_id: Some(session.connection_id.into()),
left_buffer.connections,
&proto::UpdateChannelBufferCollaborators {
channel_id: left_buffer.channel_id.to_proto(),
collaborators: left_buffer.collaborators,
},
&session.peer,
);

View File

@@ -4,6 +4,7 @@ use gpui::{ModelHandle, TestAppContext};
mod channel_buffer_tests;
mod channel_message_tests;
mod channel_tests;
mod following_tests;
mod integration_tests;
mod random_channel_buffer_tests;
mod random_project_collaboration_tests;

View File

@@ -3,15 +3,17 @@ use crate::{
tests::TestServer,
};
use call::ActiveCall;
use channel::Channel;
use client::UserId;
use channel::{Channel, ACKNOWLEDGE_DEBOUNCE_INTERVAL};
use client::ParticipantIndex;
use client::{Collaborator, UserId};
use collab_ui::channel_view::ChannelView;
use collections::HashMap;
use editor::{Anchor, Editor, ToOffset};
use futures::future;
use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
use rpc::{proto, RECEIVE_TIMEOUT};
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
use rpc::{proto::PeerId, RECEIVE_TIMEOUT};
use serde_json::json;
use std::sync::Arc;
use std::{ops::Range, sync::Arc};
#[gpui::test]
async fn test_core_channel_buffers(
@@ -100,7 +102,7 @@ async fn test_core_channel_buffers(
channel_buffer_b.read_with(cx_b, |buffer, _| {
assert_collaborators(
&buffer.collaborators(),
&[client_b.user_id(), client_a.user_id()],
&[client_a.user_id(), client_b.user_id()],
);
});
@@ -120,10 +122,10 @@ async fn test_core_channel_buffers(
}
#[gpui::test]
async fn test_channel_buffer_replica_ids(
async fn test_channel_notes_participant_indices(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
mut cx_a: &mut TestAppContext,
mut cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
) {
deterministic.forbid_parking();
@@ -132,6 +134,13 @@ async fn test_channel_buffer_replica_ids(
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
cx_a.update(editor::init);
cx_b.update(editor::init);
cx_c.update(editor::init);
let channel_id = server
.make_channel(
"the-channel",
@@ -141,140 +150,173 @@ async fn test_channel_buffer_replica_ids(
)
.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);
// Clients A and B join a channel.
active_call_a
.update(cx_a, |call, cx| call.join_channel(channel_id, cx))
.await
.unwrap();
active_call_b
.update(cx_b, |call, cx| call.join_channel(channel_id, cx))
.await
.unwrap();
// Clients A, B, and C join a channel buffer
// C first so that the replica IDs in the project and the channel buffer are different
let channel_buffer_c = client_c
.channel_store()
.update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx))
.await
.unwrap();
let channel_buffer_b = client_b
.channel_store()
.update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
.await
.unwrap();
let channel_buffer_a = client_a
.channel_store()
.update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
.await
.unwrap();
// Client B shares a project
client_b
client_a
.fs()
.insert_tree("/dir", json!({ "file.txt": "contents" }))
.insert_tree("/root", json!({"file.txt": "123"}))
.await;
let (project_b, _) = client_b.build_local_project("/dir", cx_b).await;
let shared_project_id = active_call_b
.update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await;
let project_b = client_b.build_empty_local_project(cx_b);
let project_c = client_c.build_empty_local_project(cx_c);
let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c);
// Clients A, B, and C open the channel notes
let channel_view_a = cx_a
.update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx))
.await
.unwrap();
let channel_view_b = cx_b
.update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx))
.await
.unwrap();
let channel_view_c = cx_c
.update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx))
.await
.unwrap();
// Client A joins the project
let project_a = client_a.build_remote_project(shared_project_id, cx_a).await;
// Clients A, B, and C all insert and select some text
channel_view_a.update(cx_a, |notes, cx| {
notes.editor.update(cx, |editor, cx| {
editor.insert("a", cx);
editor.change_selections(None, cx, |selections| {
selections.select_ranges(vec![0..1]);
});
});
});
deterministic.run_until_parked();
// Client C is in a separate project.
client_c.fs().insert_tree("/dir", json!({})).await;
let (separate_project_c, _) = client_c.build_local_project("/dir", cx_c).await;
// Note that each user has a different replica id in the projects vs the
// channel buffer.
channel_buffer_a.read_with(cx_a, |channel_buffer, cx| {
assert_eq!(project_a.read(cx).replica_id(), 1);
assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 2);
channel_view_b.update(cx_b, |notes, cx| {
notes.editor.update(cx, |editor, cx| {
editor.move_down(&Default::default(), cx);
editor.insert("b", cx);
editor.change_selections(None, cx, |selections| {
selections.select_ranges(vec![1..2]);
});
});
});
channel_buffer_b.read_with(cx_b, |channel_buffer, cx| {
assert_eq!(project_b.read(cx).replica_id(), 0);
assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 1);
});
channel_buffer_c.read_with(cx_c, |channel_buffer, cx| {
// C is not in the project
assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 0);
deterministic.run_until_parked();
channel_view_c.update(cx_c, |notes, cx| {
notes.editor.update(cx, |editor, cx| {
editor.move_down(&Default::default(), cx);
editor.insert("c", cx);
editor.change_selections(None, cx, |selections| {
selections.select_ranges(vec![2..3]);
});
});
});
let channel_window_a =
cx_a.add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), cx));
let channel_window_b =
cx_b.add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), cx));
let channel_window_c = cx_c.add_window(|cx| {
ChannelView::new(separate_project_c.clone(), channel_buffer_c.clone(), cx)
// Client A sees clients B and C without assigned colors, because they aren't
// in a call together.
deterministic.run_until_parked();
channel_view_a.update(cx_a, |notes, cx| {
notes.editor.update(cx, |editor, cx| {
assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx);
});
});
let channel_view_a = channel_window_a.root(cx_a);
let channel_view_b = channel_window_b.root(cx_b);
let channel_view_c = channel_window_c.root(cx_c);
// Clients A and B join the same call.
for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] {
call.update(*cx, |call, cx| call.join_channel(channel_id, cx))
.await
.unwrap();
}
// For clients A and B, the replica ids in the channel buffer are mapped
// so that they match the same users' replica ids in their shared project.
channel_view_a.read_with(cx_a, |view, cx| {
assert_eq!(
view.editor.read(cx).replica_id_map().unwrap(),
&[(1, 0), (2, 1)].into_iter().collect::<HashMap<_, _>>()
);
// Clients A and B see each other with two different assigned colors. Client C
// still doesn't have a color.
deterministic.run_until_parked();
channel_view_a.update(cx_a, |notes, cx| {
notes.editor.update(cx, |editor, cx| {
assert_remote_selections(
editor,
&[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)],
cx,
);
});
});
channel_view_b.read_with(cx_b, |view, cx| {
assert_eq!(
view.editor.read(cx).replica_id_map().unwrap(),
&[(1, 0), (2, 1)].into_iter().collect::<HashMap<u16, u16>>(),
)
channel_view_b.update(cx_b, |notes, cx| {
notes.editor.update(cx, |editor, cx| {
assert_remote_selections(
editor,
&[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)],
cx,
);
});
});
// Client C only sees themself, as they're not part of any shared project
channel_view_c.read_with(cx_c, |view, cx| {
assert_eq!(
view.editor.read(cx).replica_id_map().unwrap(),
&[(0, 0)].into_iter().collect::<HashMap<u16, u16>>(),
);
});
// Client C joins the project that clients A and B are in.
active_call_c
.update(cx_c, |call, cx| call.join_channel(channel_id, cx))
// Client A shares a project, and client B joins.
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_c = client_c.build_remote_project(shared_project_id, cx_c).await;
deterministic.run_until_parked();
project_c.read_with(cx_c, |project, _| {
assert_eq!(project.replica_id(), 2);
});
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
// For clients A and B, client C's replica id in the channel buffer is
// now mapped to their replica id in the shared project.
channel_view_a.read_with(cx_a, |view, cx| {
assert_eq!(
view.editor.read(cx).replica_id_map().unwrap(),
&[(1, 0), (2, 1), (0, 2)]
.into_iter()
.collect::<HashMap<_, _>>()
);
// Clients A and B open the same file.
let editor_a = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
editor_a.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |selections| {
selections.select_ranges(vec![0..1]);
});
});
channel_view_b.read_with(cx_b, |view, cx| {
assert_eq!(
view.editor.read(cx).replica_id_map().unwrap(),
&[(1, 0), (2, 1), (0, 2)]
.into_iter()
.collect::<HashMap<_, _>>(),
)
editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |selections| {
selections.select_ranges(vec![2..3]);
});
});
deterministic.run_until_parked();
// Clients A and B see each other with the same colors as in the channel notes.
editor_a.update(cx_a, |editor, cx| {
assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx);
});
editor_b.update(cx_b, |editor, cx| {
assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx);
});
}
#[track_caller]
fn assert_remote_selections(
editor: &mut Editor,
expected_selections: &[(Option<ParticipantIndex>, Range<usize>)],
cx: &mut ViewContext<Editor>,
) {
let snapshot = editor.snapshot(cx);
let range = Anchor::min()..Anchor::max();
let remote_selections = snapshot
.remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx)
.map(|s| {
let start = s.selection.start.to_offset(&snapshot.buffer_snapshot);
let end = s.selection.end.to_offset(&snapshot.buffer_snapshot);
(s.participant_index, start..end)
})
.collect::<Vec<_>>();
assert_eq!(
remote_selections, expected_selections,
"incorrect remote selections"
);
}
#[gpui::test]
async fn test_reopen_channel_buffer(deterministic: Arc<Deterministic>, cx_a: &mut TestAppContext) {
async fn test_multiple_handles_to_channel_buffer(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
@@ -368,10 +410,7 @@ async fn test_channel_buffer_disconnect(
channel_buffer_a.update(cx_a, |buffer, _| {
assert_eq!(
buffer.channel().as_ref(),
&Channel {
id: channel_id,
name: "the-channel".to_string()
}
&channel(channel_id, "the-channel")
);
assert!(!buffer.is_connected());
});
@@ -396,15 +435,21 @@ async fn test_channel_buffer_disconnect(
channel_buffer_b.update(cx_b, |buffer, _| {
assert_eq!(
buffer.channel().as_ref(),
&Channel {
id: channel_id,
name: "the-channel".to_string()
}
&channel(channel_id, "the-channel")
);
assert!(!buffer.is_connected());
});
}
fn channel(id: u64, name: &'static str) -> Channel {
Channel {
id,
name: name.to_string(),
unseen_note_version: None,
unseen_message_id: None,
}
}
#[gpui::test]
async fn test_rejoin_channel_buffer(
deterministic: Arc<Deterministic>,
@@ -565,26 +610,284 @@ async fn test_channel_buffers_and_server_restarts(
channel_buffer_a.read_with(cx_a, |buffer_a, _| {
channel_buffer_b.read_with(cx_b, |buffer_b, _| {
assert_eq!(
buffer_a
.collaborators()
.iter()
.map(|c| c.user_id)
.collect::<Vec<_>>(),
vec![client_a.user_id().unwrap(), client_b.user_id().unwrap()]
assert_collaborators(
buffer_a.collaborators(),
&[client_a.user_id(), client_b.user_id()],
);
assert_eq!(buffer_a.collaborators(), buffer_b.collaborators());
});
});
}
#[gpui::test(iterations = 10)]
async fn test_following_to_channel_notes_without_a_shared_project(
deterministic: Arc<Deterministic>,
mut cx_a: &mut TestAppContext,
mut cx_b: &mut TestAppContext,
mut cx_c: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
cx_a.update(editor::init);
cx_b.update(editor::init);
cx_c.update(editor::init);
cx_a.update(collab_ui::channel_view::init);
cx_b.update(collab_ui::channel_view::init);
cx_c.update(collab_ui::channel_view::init);
let channel_1_id = server
.make_channel(
"channel-1",
None,
(&client_a, cx_a),
&mut [(&client_b, cx_b), (&client_c, cx_c)],
)
.await;
let channel_2_id = server
.make_channel(
"channel-2",
None,
(&client_a, cx_a),
&mut [(&client_b, cx_b), (&client_c, cx_c)],
)
.await;
// Clients A, B, and C join a channel.
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);
for (call, cx) in [
(&active_call_a, &mut cx_a),
(&active_call_b, &mut cx_b),
(&active_call_c, &mut cx_c),
] {
call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx))
.await
.unwrap();
}
deterministic.run_until_parked();
// Clients A, B, and C all open their own unshared projects.
client_a.fs().insert_tree("/a", json!({})).await;
client_b.fs().insert_tree("/b", json!({})).await;
client_c.fs().insert_tree("/c", json!({})).await;
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
let (project_c, _) = client_b.build_local_project("/c", cx_c).await;
let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
let _workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c);
active_call_a
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
.await
.unwrap();
// Client A opens the notes for channel 1.
let channel_view_1_a = cx_a
.update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx))
.await
.unwrap();
channel_view_1_a.update(cx_a, |notes, cx| {
assert_eq!(notes.channel(cx).name, "channel-1");
notes.editor.update(cx, |editor, cx| {
editor.insert("Hello from A.", cx);
editor.change_selections(None, cx, |selections| {
selections.select_ranges(vec![3..4]);
});
});
});
// Client B follows client A.
workspace_b
.update(cx_b, |workspace, cx| {
workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
})
.await
.unwrap();
// Client B is taken to the notes for channel 1, with the same
// text selected as client A.
deterministic.run_until_parked();
let channel_view_1_b = workspace_b.read_with(cx_b, |workspace, cx| {
assert_eq!(
workspace.leader_for_pane(workspace.active_pane()),
Some(client_a.peer_id().unwrap())
);
workspace
.active_item(cx)
.expect("no active item")
.downcast::<ChannelView>()
.expect("active item is not a channel view")
});
channel_view_1_b.read_with(cx_b, |notes, cx| {
assert_eq!(notes.channel(cx).name, "channel-1");
let editor = notes.editor.read(cx);
assert_eq!(editor.text(cx), "Hello from A.");
assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
});
// Client A opens the notes for channel 2.
let channel_view_2_a = cx_a
.update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx))
.await
.unwrap();
channel_view_2_a.read_with(cx_a, |notes, cx| {
assert_eq!(notes.channel(cx).name, "channel-2");
});
// Client B is taken to the notes for channel 2.
deterministic.run_until_parked();
let channel_view_2_b = workspace_b.read_with(cx_b, |workspace, cx| {
assert_eq!(
workspace.leader_for_pane(workspace.active_pane()),
Some(client_a.peer_id().unwrap())
);
workspace
.active_item(cx)
.expect("no active item")
.downcast::<ChannelView>()
.expect("active item is not a channel view")
});
channel_view_2_b.read_with(cx_b, |notes, cx| {
assert_eq!(notes.channel(cx).name, "channel-2");
});
}
#[gpui::test]
async fn test_channel_buffer_changes(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let channel_id = server
.make_channel(
"the-channel",
None,
(&client_a, cx_a),
&mut [(&client_b, cx_b)],
)
.await;
let channel_buffer_a = client_a
.channel_store()
.update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
.await
.unwrap();
// Client A makes an edit, and client B should see that the note has changed.
channel_buffer_a.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.edit([(0..0, "1")], None, cx);
})
});
deterministic.run_until_parked();
let has_buffer_changed = cx_b.read(|cx| {
client_b
.channel_store()
.read(cx)
.has_channel_buffer_changed(channel_id)
.unwrap()
});
assert!(has_buffer_changed);
// Opening the buffer should clear the changed flag.
let project_b = client_b.build_empty_local_project(cx_b);
let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
let channel_view_b = cx_b
.update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx))
.await
.unwrap();
deterministic.run_until_parked();
let has_buffer_changed = cx_b.read(|cx| {
client_b
.channel_store()
.read(cx)
.has_channel_buffer_changed(channel_id)
.unwrap()
});
assert!(!has_buffer_changed);
// Editing the channel while the buffer is open should not show that the buffer has changed.
channel_buffer_a.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.edit([(0..0, "2")], None, cx);
})
});
deterministic.run_until_parked();
let has_buffer_changed = cx_b.read(|cx| {
client_b
.channel_store()
.read(cx)
.has_channel_buffer_changed(channel_id)
.unwrap()
});
assert!(!has_buffer_changed);
deterministic.advance_clock(ACKNOWLEDGE_DEBOUNCE_INTERVAL);
// Test that the server is tracking things correctly, and we retain our 'not changed'
// state across a disconnect
server.simulate_long_connection_interruption(client_b.peer_id().unwrap(), &deterministic);
let has_buffer_changed = cx_b.read(|cx| {
client_b
.channel_store()
.read(cx)
.has_channel_buffer_changed(channel_id)
.unwrap()
});
assert!(!has_buffer_changed);
// Closing the buffer should re-enable change tracking
cx_b.update(|cx| {
workspace_b.update(cx, |workspace, cx| {
workspace.close_all_items_and_panes(&Default::default(), cx)
});
drop(channel_view_b)
});
deterministic.run_until_parked();
channel_buffer_a.update(cx_a, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.edit([(0..0, "3")], None, cx);
})
});
deterministic.run_until_parked();
let has_buffer_changed = cx_b.read(|cx| {
client_b
.channel_store()
.read(cx)
.has_channel_buffer_changed(channel_id)
.unwrap()
});
assert!(has_buffer_changed);
}
#[track_caller]
fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option<UserId>]) {
fn assert_collaborators(collaborators: &HashMap<PeerId, Collaborator>, ids: &[Option<UserId>]) {
let mut user_ids = collaborators
.values()
.map(|collaborator| collaborator.user_id)
.collect::<Vec<_>>();
user_ids.sort();
assert_eq!(
collaborators
.into_iter()
.map(|collaborator| collaborator.user_id)
.collect::<Vec<_>>(),
user_ids,
ids.into_iter().map(|id| id.unwrap()).collect::<Vec<_>>()
);
}

View File

@@ -1,7 +1,9 @@
use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
use channel::{ChannelChat, ChannelMessageId};
use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
use collab_ui::chat_panel::ChatPanel;
use gpui::{executor::Deterministic, BorrowAppContext, ModelHandle, TestAppContext};
use std::sync::Arc;
use workspace::dock::Panel;
#[gpui::test]
async fn test_basic_channel_messages(
@@ -223,3 +225,136 @@ fn assert_messages(chat: &ModelHandle<ChannelChat>, messages: &[&str], cx: &mut
messages
);
}
#[gpui::test]
async fn test_channel_message_changes(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let channel_id = server
.make_channel(
"the-channel",
None,
(&client_a, cx_a),
&mut [(&client_b, cx_b)],
)
.await;
// Client A sends a message, client B should see that there is a new message.
let channel_chat_a = client_a
.channel_store()
.update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
.await
.unwrap();
channel_chat_a
.update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
.await
.unwrap();
deterministic.run_until_parked();
let b_has_messages = cx_b.read_with(|cx| {
client_b
.channel_store()
.read(cx)
.has_new_messages(channel_id)
.unwrap()
});
assert!(b_has_messages);
// Opening the chat should clear the changed flag.
cx_b.update(|cx| {
collab_ui::init(&client_b.app_state, cx);
});
let project_b = client_b.build_empty_local_project(cx_b);
let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
let chat_panel_b = workspace_b.update(cx_b, |workspace, cx| ChatPanel::new(workspace, cx));
chat_panel_b
.update(cx_b, |chat_panel, cx| {
chat_panel.set_active(true, cx);
chat_panel.select_channel(channel_id, cx)
})
.await
.unwrap();
deterministic.run_until_parked();
let b_has_messages = cx_b.read_with(|cx| {
client_b
.channel_store()
.read(cx)
.has_new_messages(channel_id)
.unwrap()
});
assert!(!b_has_messages);
// Sending a message while the chat is open should not change the flag.
channel_chat_a
.update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap())
.await
.unwrap();
deterministic.run_until_parked();
let b_has_messages = cx_b.read_with(|cx| {
client_b
.channel_store()
.read(cx)
.has_new_messages(channel_id)
.unwrap()
});
assert!(!b_has_messages);
// Sending a message while the chat is closed should change the flag.
chat_panel_b.update(cx_b, |chat_panel, cx| {
chat_panel.set_active(false, cx);
});
// Sending a message while the chat is open should not change the flag.
channel_chat_a
.update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap())
.await
.unwrap();
deterministic.run_until_parked();
let b_has_messages = cx_b.read_with(|cx| {
client_b
.channel_store()
.read(cx)
.has_new_messages(channel_id)
.unwrap()
});
assert!(b_has_messages);
// Closing the chat should re-enable change tracking
cx_b.update(|_| drop(chat_panel_b));
channel_chat_a
.update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap())
.await
.unwrap();
deterministic.run_until_parked();
let b_has_messages = cx_b.read_with(|cx| {
client_b
.channel_store()
.read(cx)
.has_new_messages(channel_id)
.unwrap()
});
assert!(b_has_messages);
}

View File

@@ -145,8 +145,6 @@ async fn test_core_channels(
],
);
println!("STARTING CREATE CHANNEL C");
let channel_c_id = client_a
.channel_store()
.update(cx_a, |channel_store, cx| {
@@ -382,6 +380,8 @@ async fn test_channel_room(
// Give everyone a chance to observe user A joining
deterministic.run_until_parked();
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
room_a.read_with(cx_a, |room, _| assert!(room.is_connected()));
client_a.channel_store().read_with(cx_a, |channels, _| {
assert_participants_eq(
@@ -1028,10 +1028,6 @@ async fn test_channel_moving(
// - ep
assert_channels_list_shape(client_c.channel_store(), cx_c, &[(channel_ep_id, 0)]);
println!("*******************************************");
println!("********** STARTING LINK CHANNEL **********");
println!("*******************************************");
dbg!(client_b.user_id());
client_b
.channel_store()
.update(cx_b, |channel_store, cx| {
@@ -1199,5 +1195,5 @@ fn assert_channels_list_shape(
.map(|(depth, channel)| (channel.id, depth))
.collect::<Vec<_>>()
});
pretty_assertions::assert_eq!(dbg!(actual), expected_channels);
pretty_assertions::assert_eq!(actual, expected_channels);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -46,12 +46,7 @@ impl RandomizedTest for RandomChannelBufferTest {
let db = &server.app_state.db;
for ix in 0..CHANNEL_COUNT {
let id = db
.create_channel(
&format!("channel-{ix}"),
None,
&format!("livekit-room-{ix}"),
users[0].user_id,
)
.create_channel(&format!("channel-{ix}"), None, users[0].user_id)
.await
.unwrap();
for user in &users[1..] {
@@ -273,7 +268,7 @@ impl RandomizedTest for RandomChannelBufferTest {
// channel buffer.
let collaborators = channel_buffer.collaborators();
let mut user_ids =
collaborators.iter().map(|c| c.user_id).collect::<Vec<_>>();
collaborators.values().map(|c| c.user_id).collect::<Vec<_>>();
user_ids.sort();
assert_eq!(
user_ids,

View File

@@ -1,7 +1,7 @@
use crate::{
db::{tests::TestDb, NewUserParams, UserId},
executor::Executor,
rpc::{Server, CLEANUP_TIMEOUT},
rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
AppState,
};
use anyhow::anyhow;
@@ -15,8 +15,10 @@ use fs::FakeFs;
use futures::{channel::oneshot, StreamExt as _};
use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle};
use language::LanguageRegistry;
use node_runtime::FakeNodeRuntime;
use parking_lot::Mutex;
use project::{Project, WorktreeId};
use rpc::RECEIVE_TIMEOUT;
use settings::SettingsStore;
use std::{
cell::{Ref, RefCell, RefMut},
@@ -29,7 +31,7 @@ use std::{
},
};
use util::http::FakeHttpClient;
use workspace::Workspace;
use workspace::{Workspace, WorkspaceStore};
pub struct TestServer {
pub app_state: Arc<AppState>,
@@ -43,6 +45,7 @@ pub struct TestServer {
pub struct TestClient {
pub username: String,
pub app_state: Arc<workspace::AppState>,
channel_store: ModelHandle<ChannelStore>,
state: RefCell<TestClientState>,
}
@@ -151,12 +154,12 @@ impl TestServer {
Arc::get_mut(&mut client)
.unwrap()
.set_id(user_id.0 as usize)
.set_id(user_id.to_proto())
.override_authenticate(move |cx| {
cx.spawn(|_| async move {
let access_token = "the-token".to_string();
Ok(Credentials {
user_id: user_id.0 as u64,
user_id: user_id.to_proto(),
access_token,
})
})
@@ -204,17 +207,19 @@ impl TestServer {
let fs = FakeFs::new(cx.background());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
let channel_store =
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
let mut language_registry = LanguageRegistry::test();
language_registry.set_executor(cx.background());
let app_state = Arc::new(workspace::AppState {
client: client.clone(),
user_store: user_store.clone(),
channel_store: channel_store.clone(),
languages: Arc::new(LanguageRegistry::test()),
workspace_store,
languages: Arc::new(language_registry),
fs: fs.clone(),
build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
background_actions: || &[],
node_runtime: FakeNodeRuntime::new(),
});
cx.update(|cx| {
@@ -226,7 +231,7 @@ impl TestServer {
workspace::init(app_state.clone(), cx);
audio::init((), cx);
call::init(client.clone(), user_store.clone(), cx);
channel::init(&client);
channel::init(&client, user_store, cx);
});
client
@@ -237,6 +242,7 @@ impl TestServer {
let client = TestClient {
app_state,
username: name.to_string(),
channel_store: cx.read(ChannelStore::global).clone(),
state: Default::default(),
};
client.wait_for_current_user(cx).await;
@@ -251,6 +257,19 @@ impl TestServer {
.store(true, SeqCst);
}
pub fn simulate_long_connection_interruption(
&self,
peer_id: PeerId,
deterministic: &Arc<Deterministic>,
) {
self.forbid_connections();
self.disconnect_client(peer_id);
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
self.allow_connections();
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
deterministic.run_until_parked();
}
pub fn forbid_connections(&self) {
self.forbid_connections.store(true, SeqCst);
}
@@ -292,10 +311,9 @@ impl TestServer {
admin: (&TestClient, &mut TestAppContext),
members: &mut [(&TestClient, &mut TestAppContext)],
) -> u64 {
let (admin_client, admin_cx) = admin;
let channel_id = admin_client
.app_state
.channel_store
let (_, admin_cx) = admin;
let channel_id = admin_cx
.read(ChannelStore::global)
.update(admin_cx, |channel_store, cx| {
channel_store.create_channel(channel, parent, cx)
})
@@ -303,9 +321,8 @@ impl TestServer {
.unwrap();
for (member_client, member_cx) in members {
admin_client
.app_state
.channel_store
admin_cx
.read(ChannelStore::global)
.update(admin_cx, |channel_store, cx| {
channel_store.invite_member(
channel_id,
@@ -319,9 +336,8 @@ impl TestServer {
admin_cx.foreground().run_until_parked();
member_client
.app_state
.channel_store
member_cx
.read(ChannelStore::global)
.update(*member_cx, |channels, _| {
channels.respond_to_channel_invite(channel_id, true)
})
@@ -429,7 +445,7 @@ impl TestClient {
}
pub fn channel_store(&self) -> &ModelHandle<ChannelStore> {
&self.app_state.channel_store
&self.channel_store
}
pub fn user_store(&self) -> &ModelHandle<UserStore> {
@@ -536,15 +552,7 @@ impl TestClient {
root_path: impl AsRef<Path>,
cx: &mut TestAppContext,
) -> (ModelHandle<Project>, WorktreeId) {
let project = cx.update(|cx| {
Project::local(
self.client().clone(),
self.app_state.user_store.clone(),
self.app_state.languages.clone(),
self.app_state.fs.clone(),
cx,
)
});
let project = self.build_empty_local_project(cx);
let (worktree, _) = project
.update(cx, |p, cx| {
p.find_or_create_local_worktree(root_path, true, cx)
@@ -557,6 +565,19 @@ impl TestClient {
(project, worktree.read_with(cx, |tree, _| tree.id()))
}
pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> ModelHandle<Project> {
cx.update(|cx| {
Project::local(
self.client().clone(),
self.app_state.node_runtime.clone(),
self.app_state.user_store.clone(),
self.app_state.languages.clone(),
self.app_state.fs.clone(),
cx,
)
})
}
pub async fn build_remote_project(
&self,
host_project_id: u64,
@@ -592,8 +613,8 @@ impl TestClient {
) {
let (other_client, other_cx) = user;
self.app_state
.channel_store
cx_self
.read(ChannelStore::global)
.update(cx_self, |channel_store, cx| {
channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx)
})
@@ -602,11 +623,10 @@ impl TestClient {
cx_self.foreground().run_until_parked();
other_client
.app_state
.channel_store
.update(other_cx, |channels, _| {
channels.respond_to_channel_invite(channel, true)
other_cx
.read(ChannelStore::global)
.update(other_cx, |channel_store, _| {
channel_store.respond_to_channel_invite(channel, true)
})
.await
.unwrap();

View File

@@ -37,6 +37,7 @@ fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
language = { path = "../language" }
menu = { path = "../menu" }
rich_text = { path = "../rich_text" }
picker = { path = "../picker" }
project = { path = "../project" }
recent_projects = {path = "../recent_projects"}

View File

@@ -1,10 +1,12 @@
use anyhow::{anyhow, Result};
use call::ActiveCall;
use channel::{ChannelBuffer, ChannelBufferEvent, ChannelId};
use client::proto;
use clock::ReplicaId;
use call::report_call_event_for_channel;
use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
use client::{
proto::{self, PeerId},
Collaborator, ParticipantIndex,
};
use collections::HashMap;
use editor::Editor;
use editor::{CollaborationHub, Editor};
use gpui::{
actions,
elements::{ChildView, Label},
@@ -13,54 +15,57 @@ use gpui::{
ViewContext, ViewHandle,
};
use project::Project;
use std::any::{Any, TypeId};
use std::{
any::{Any, TypeId},
sync::Arc,
};
use util::ResultExt;
use workspace::{
item::{FollowableItem, Item, ItemHandle},
register_followable_item,
searchable::SearchableItemHandle,
ItemNavHistory, Pane, ViewId, Workspace, WorkspaceId,
ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
};
actions!(channel_view, [Deploy]);
pub(crate) fn init(cx: &mut AppContext) {
pub fn init(cx: &mut AppContext) {
register_followable_item::<ChannelView>(cx)
}
pub struct ChannelView {
pub editor: ViewHandle<Editor>,
project: ModelHandle<Project>,
channel_store: ModelHandle<ChannelStore>,
channel_buffer: ModelHandle<ChannelBuffer>,
remote_id: Option<ViewId>,
_editor_event_subscription: Subscription,
}
impl ChannelView {
pub fn deploy(channel_id: ChannelId, workspace: ViewHandle<Workspace>, cx: &mut AppContext) {
pub fn open(
channel_id: ChannelId,
workspace: ViewHandle<Workspace>,
cx: &mut AppContext,
) -> Task<Result<ViewHandle<Self>>> {
let pane = workspace.read(cx).active_pane().clone();
let channel_view = Self::open(channel_id, pane.clone(), workspace.clone(), cx);
let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
cx.spawn(|mut cx| async move {
let channel_view = channel_view.await?;
pane.update(&mut cx, |pane, cx| {
let room_id = ActiveCall::global(cx)
.read(cx)
.room()
.map(|room| room.read(cx).id());
ActiveCall::report_call_event_for_room(
report_call_event_for_channel(
"open channel notes",
room_id,
Some(channel_id),
channel_id,
&workspace.read(cx).app_state().client,
cx,
);
pane.add_item(Box::new(channel_view), true, true, None, cx);
pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
});
anyhow::Ok(())
anyhow::Ok(channel_view)
})
.detach();
}
pub fn open(
pub fn open_in_pane(
channel_id: ChannelId,
pane: ViewHandle<Pane>,
workspace: ViewHandle<Workspace>,
@@ -68,7 +73,7 @@ impl ChannelView {
) -> Task<Result<ViewHandle<Self>>> {
let workspace = workspace.read(cx);
let project = workspace.project().to_owned();
let channel_store = workspace.app_state().channel_store.clone();
let channel_store = ChannelStore::global(cx);
let markdown = workspace
.app_state()
.languages
@@ -79,17 +84,45 @@ impl ChannelView {
cx.spawn(|mut cx| async move {
let channel_buffer = channel_buffer.await?;
let markdown = markdown.await?;
channel_buffer.update(&mut cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.set_language(Some(markdown), cx);
})
});
if let Some(markdown) = markdown.await.log_err() {
channel_buffer.update(&mut cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
buffer.set_language(Some(markdown), cx);
})
});
}
pane.update(&mut cx, |pane, cx| {
pane.items_of_type::<Self>()
.find(|channel_view| channel_view.read(cx).channel_buffer == channel_buffer)
.unwrap_or_else(|| cx.add_view(|cx| Self::new(project, channel_buffer, cx)))
let buffer_id = channel_buffer.read(cx).remote_id(cx);
let existing_view = pane
.items_of_type::<Self>()
.find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
// If this channel buffer is already open in this pane, just return it.
if let Some(existing_view) = existing_view.clone() {
if existing_view.read(cx).channel_buffer == channel_buffer {
return existing_view;
}
}
let view = cx.add_view(|cx| {
let mut this = Self::new(project, channel_store, channel_buffer, cx);
this.acknowledge_buffer_version(cx);
this
});
// If the pane contained a disconnected view for this channel buffer,
// replace that.
if let Some(existing_item) = existing_view {
if let Some(ix) = pane.index_for_item(&existing_item) {
pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
.detach();
pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
}
}
view
})
.ok_or_else(|| anyhow!("pane was dropped"))
})
@@ -97,44 +130,35 @@ impl ChannelView {
pub fn new(
project: ModelHandle<Project>,
channel_store: ModelHandle<ChannelStore>,
channel_buffer: ModelHandle<ChannelBuffer>,
cx: &mut ViewContext<Self>,
) -> Self {
let buffer = channel_buffer.read(cx).buffer();
let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx));
let editor = cx.add_view(|cx| {
let mut editor = Editor::for_buffer(buffer, None, cx);
editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
channel_buffer.clone(),
)));
editor
});
let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
cx.subscribe(&project, Self::handle_project_event).detach();
cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
.detach();
let this = Self {
Self {
editor,
project,
channel_store,
channel_buffer,
remote_id: None,
_editor_event_subscription,
};
this.refresh_replica_id_map(cx);
this
}
}
fn handle_project_event(
&mut self,
_: ModelHandle<Project>,
event: &project::Event,
cx: &mut ViewContext<Self>,
) {
match event {
project::Event::RemoteIdChanged(_) => {}
project::Event::DisconnectedFromHost => {}
project::Event::Closed => {}
project::Event::CollaboratorUpdated { .. } => {}
project::Event::CollaboratorLeft(_) => {}
project::Event::CollaboratorJoined(_) => {}
_ => return,
}
self.refresh_replica_id_map(cx);
pub fn channel(&self, cx: &AppContext) -> Arc<Channel> {
self.channel_buffer.read(cx).channel()
}
fn handle_channel_buffer_event(
@@ -144,48 +168,41 @@ impl ChannelView {
cx: &mut ViewContext<Self>,
) {
match event {
ChannelBufferEvent::CollaboratorsChanged => {
self.refresh_replica_id_map(cx);
}
ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
editor.set_read_only(true);
cx.notify();
}),
ChannelBufferEvent::BufferEdited => {
if cx.is_self_focused() || self.editor.is_focused(cx) {
self.acknowledge_buffer_version(cx);
} else {
self.channel_store.update(cx, |store, cx| {
let channel_buffer = self.channel_buffer.read(cx);
store.notes_changed(
channel_buffer.channel().id,
channel_buffer.epoch(),
&channel_buffer.buffer().read(cx).version(),
cx,
)
});
}
}
_ => {}
}
}
/// Build a mapping of channel buffer replica ids to the corresponding
/// replica ids in the current project.
///
/// Using this mapping, a given user can be displayed with the same color
/// in the channel buffer as in other files in the project. Users who are
/// in the channel buffer but not the project will not have a color.
fn refresh_replica_id_map(&self, cx: &mut ViewContext<Self>) {
let mut project_replica_ids_by_channel_buffer_replica_id = HashMap::default();
let project = self.project.read(cx);
let channel_buffer = self.channel_buffer.read(cx);
project_replica_ids_by_channel_buffer_replica_id
.insert(channel_buffer.replica_id(cx), project.replica_id());
project_replica_ids_by_channel_buffer_replica_id.extend(
channel_buffer
.collaborators()
.iter()
.filter_map(|channel_buffer_collaborator| {
project
.collaborators()
.values()
.find_map(|project_collaborator| {
(project_collaborator.user_id == channel_buffer_collaborator.user_id)
.then_some((
channel_buffer_collaborator.replica_id as ReplicaId,
project_collaborator.replica_id,
))
})
}),
);
self.editor.update(cx, |editor, cx| {
editor.set_replica_id_map(Some(project_replica_ids_by_channel_buffer_replica_id), cx)
fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) {
self.channel_store.update(cx, |store, cx| {
let channel_buffer = self.channel_buffer.read(cx);
store.acknowledge_notes_version(
channel_buffer.channel().id,
channel_buffer.epoch(),
&channel_buffer.buffer().read(cx).version(),
cx,
)
});
self.channel_buffer.update(cx, |buffer, cx| {
buffer.acknowledge_buffer_version(cx);
});
}
}
@@ -205,6 +222,7 @@ impl View for ChannelView {
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
self.acknowledge_buffer_version(cx);
cx.focus(self.editor.as_any())
}
}
@@ -244,6 +262,7 @@ impl Item for ChannelView {
fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
Some(Self::new(
self.project.clone(),
self.channel_store.clone(),
self.channel_buffer.clone(),
cx,
))
@@ -287,10 +306,14 @@ impl FollowableItem for ChannelView {
}
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
let channel = self.channel_buffer.read(cx).channel();
let channel_buffer = self.channel_buffer.read(cx);
if !channel_buffer.is_connected() {
return None;
}
Some(proto::view::Variant::ChannelView(
proto::view::ChannelView {
channel_id: channel.id,
channel_id: channel_buffer.channel().id,
editor: if let Some(proto::view::Variant::Editor(proto)) =
self.editor.read(cx).to_state_proto(cx)
{
@@ -316,7 +339,7 @@ impl FollowableItem for ChannelView {
unreachable!()
};
let open = ChannelView::open(state.channel_id, pane, workspace, cx);
let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
Some(cx.spawn(|mut cx| async move {
let this = open.await?;
@@ -376,17 +399,32 @@ impl FollowableItem for ChannelView {
})
}
fn set_leader_replica_id(
&mut self,
leader_replica_id: Option<u16>,
cx: &mut ViewContext<Self>,
) {
fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |editor, cx| {
editor.set_leader_replica_id(leader_replica_id, cx)
editor.set_leader_peer_id(leader_peer_id, cx)
})
}
fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
Editor::should_unfollow_on_event(event, cx)
}
fn is_project_item(&self, _cx: &AppContext) -> bool {
false
}
}
struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
impl CollaborationHub for ChannelBufferCollaborationHub {
fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
self.0.read(cx).collaborators()
}
fn user_participant_indices<'a>(
&self,
cx: &'a AppContext,
) -> &'a HashMap<u64, ParticipantIndex> {
self.0.read(cx).user_store().read(cx).participant_indices()
}
}

View File

@@ -3,6 +3,7 @@ use anyhow::Result;
use call::ActiveCall;
use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
use client::Client;
use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
use editor::Editor;
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
@@ -12,12 +13,13 @@ use gpui::{
platform::{CursorStyle, MouseButton},
serde_json,
views::{ItemType, Select, SelectStyle},
AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
ViewContext, ViewHandle, WeakViewHandle,
AnyViewHandle, AppContext, AsyncAppContext, Entity, ImageData, ModelHandle, Subscription, Task,
View, ViewContext, ViewHandle, WeakViewHandle,
};
use language::language_settings::SoftWrap;
use language::{language_settings::SoftWrap, LanguageRegistry};
use menu::Confirm;
use project::Fs;
use rich_text::RichText;
use serde::{Deserialize, Serialize};
use settings::SettingsStore;
use std::sync::Arc;
@@ -35,6 +37,7 @@ const CHAT_PANEL_KEY: &'static str = "ChatPanel";
pub struct ChatPanel {
client: Arc<Client>,
channel_store: ModelHandle<ChannelStore>,
languages: Arc<LanguageRegistry>,
active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
message_list: ListState<ChatPanel>,
input_editor: ViewHandle<Editor>,
@@ -42,10 +45,12 @@ pub struct ChatPanel {
local_timezone: UtcOffset,
fs: Arc<dyn Fs>,
width: Option<f32>,
active: bool,
pending_serialization: Task<Option<()>>,
subscriptions: Vec<gpui::Subscription>,
workspace: WeakViewHandle<Workspace>,
has_focus: bool,
markdown_data: HashMap<ChannelMessageId, RichText>,
}
#[derive(Serialize, Deserialize)]
@@ -76,7 +81,8 @@ impl ChatPanel {
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
let fs = workspace.app_state().fs.clone();
let client = workspace.app_state().client.clone();
let channel_store = workspace.app_state().channel_store.clone();
let channel_store = ChannelStore::global(cx);
let languages = workspace.app_state().languages.clone();
let input_editor = cx.add_view(|cx| {
let mut editor = Editor::auto_height(
@@ -129,6 +135,8 @@ impl ChatPanel {
fs,
client,
channel_store,
languages,
active_chat: Default::default(),
pending_serialization: Task::ready(None),
message_list,
@@ -138,7 +146,9 @@ impl ChatPanel {
has_focus: false,
subscriptions: Vec::new(),
workspace: workspace_handle,
active: false,
width: None,
markdown_data: Default::default(),
};
let mut old_dock_position = this.position(cx);
@@ -154,9 +164,9 @@ impl ChatPanel {
}),
);
this.init_active_channel(cx);
this.update_channel_count(cx);
cx.observe(&this.channel_store, |this, _, cx| {
this.init_active_channel(cx);
this.update_channel_count(cx)
})
.detach();
@@ -175,10 +185,33 @@ impl ChatPanel {
})
.detach();
let markdown = this.languages.language_for_name("Markdown");
cx.spawn(|this, mut cx| async move {
let markdown = markdown.await?;
this.update(&mut cx, |this, cx| {
this.input_editor.update(cx, |editor, cx| {
editor.buffer().update(cx, |multi_buffer, cx| {
multi_buffer
.as_singleton()
.unwrap()
.update(cx, |buffer, cx| buffer.set_language(Some(markdown), cx))
})
})
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
this
})
}
pub fn active_chat(&self) -> Option<ModelHandle<ChannelChat>> {
self.active_chat.as_ref().map(|(chat, _)| chat.clone())
}
pub fn load(
workspace: WeakViewHandle<Workspace>,
cx: AsyncAppContext,
@@ -225,10 +258,8 @@ impl ChatPanel {
);
}
fn init_active_channel(&mut self, cx: &mut ViewContext<Self>) {
fn update_channel_count(&mut self, cx: &mut ViewContext<Self>) {
let channel_count = self.channel_store.read(cx).channel_count();
self.message_list.reset(0);
self.active_chat = None;
self.channel_select.update(cx, |select, cx| {
select.set_item_count(channel_count, cx);
});
@@ -247,6 +278,7 @@ impl ChatPanel {
}
let subscription = cx.subscribe(&chat, Self::channel_did_change);
self.active_chat = Some((chat, subscription));
self.acknowledge_last_message(cx);
self.channel_select.update(cx, |select, cx| {
if let Some(ix) = self.channel_store.read(cx).index_of_channel(id) {
select.set_selected_index(ix, cx);
@@ -268,11 +300,34 @@ impl ChatPanel {
new_count,
} => {
self.message_list.splice(old_range.clone(), *new_count);
if self.active {
self.acknowledge_last_message(cx);
}
}
ChannelChatEvent::NewMessage {
channel_id,
message_id,
} => {
if !self.active {
self.channel_store.update(cx, |store, cx| {
store.new_message(*channel_id, *message_id, cx)
})
}
}
}
cx.notify();
}
fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) {
if self.active {
if let Some((chat, _)) = &self.active_chat {
chat.update(cx, |chat, cx| {
chat.acknowledge_last_message(cx);
});
}
}
}
fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = theme::current(cx);
Flex::column()
@@ -299,13 +354,33 @@ impl ChatPanel {
messages.flex(1., true).into_any()
}
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let message = self.active_chat.as_ref().unwrap().0.read(cx).message(ix);
fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let (message, is_continuation, is_last) = {
let active_chat = self.active_chat.as_ref().unwrap().0.read(cx);
let last_message = active_chat.message(ix.saturating_sub(1));
let this_message = active_chat.message(ix);
let is_continuation = last_message.id != this_message.id
&& this_message.sender.id == last_message.sender.id;
(
active_chat.message(ix).clone(),
is_continuation,
active_chat.message_count() == ix + 1,
)
};
let is_pending = message.is_pending();
let text = self
.markdown_data
.entry(message.id)
.or_insert_with(|| rich_text::render_markdown(message.body, &self.languages, None));
let now = OffsetDateTime::now_utc();
let theme = theme::current(cx);
let style = if message.is_pending() {
let style = if is_pending {
&theme.chat_panel.pending_message
} else if is_continuation {
&theme.chat_panel.continuation_message
} else {
&theme.chat_panel.message
};
@@ -318,52 +393,90 @@ impl ChatPanel {
None
};
enum DeleteMessage {}
let body = message.body.clone();
Flex::column()
.with_child(
enum MessageBackgroundHighlight {}
MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
let container = style.container.style_for(state);
if is_continuation {
Flex::row()
.with_child(
Label::new(
message.sender.github_login.clone(),
style.sender.text.clone(),
text.element(
theme.editor.syntax.clone(),
style.body.clone(),
theme.editor.document_highlight_read_background,
cx,
)
.contained()
.with_style(style.sender.container),
.flex(1., true),
)
.with_child(render_remove(message_id_to_remove, cx, &theme))
.contained()
.with_style(*container)
.with_margin_bottom(if is_last {
theme.chat_panel.last_message_bottom_spacing
} else {
0.
})
.into_any()
} else {
Flex::column()
.with_child(
Flex::row()
.with_child(
Flex::row()
.with_child(render_avatar(
message.sender.avatar.clone(),
&theme,
))
.with_child(
Label::new(
message.sender.github_login.clone(),
style.sender.text.clone(),
)
.contained()
.with_style(style.sender.container),
)
.with_child(
Label::new(
format_timestamp(
message.timestamp,
now,
self.local_timezone,
),
style.timestamp.text.clone(),
)
.contained()
.with_style(style.timestamp.container),
)
.align_children_center()
.flex(1., true),
)
.with_child(render_remove(message_id_to_remove, cx, &theme))
.align_children_center(),
)
.with_child(
Label::new(
format_timestamp(message.timestamp, now, self.local_timezone),
style.timestamp.text.clone(),
)
.contained()
.with_style(style.timestamp.container),
Flex::row()
.with_child(
text.element(
theme.editor.syntax.clone(),
style.body.clone(),
theme.editor.document_highlight_read_background,
cx,
)
.flex(1., true),
)
// Add a spacer to make everything line up
.with_child(render_remove(None, cx, &theme)),
)
.with_children(message_id_to_remove.map(|id| {
MouseEventHandler::new::<DeleteMessage, _>(
id as usize,
cx,
|mouse_state, _| {
let button_style =
theme.chat_panel.icon_button.style_for(mouse_state);
render_icon_button(button_style, "icons/x.svg")
.aligned()
.into_any()
},
)
.with_padding(Padding::uniform(2.))
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
this.remove_message(id, cx);
})
.flex_float()
})),
)
.with_child(Text::new(body, style.body.clone()))
.contained()
.with_style(style.container)
.into_any()
.contained()
.with_style(*container)
.with_margin_bottom(if is_last {
theme.chat_panel.last_message_bottom_spacing
} else {
0.
})
.into_any()
}
})
.into_any()
}
fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
@@ -409,7 +522,7 @@ impl ChatPanel {
})
.on_click(MouseButton::Left, move |_, _, cx| {
if let Some(workspace) = workspace.upgrade(cx) {
ChannelView::deploy(channel_id, workspace, cx);
ChannelView::open(channel_id, workspace, cx).detach();
}
})
.with_tooltip::<OpenChannelNotes>(
@@ -537,6 +650,7 @@ impl ChatPanel {
cx.spawn(|this, mut cx| async move {
let chat = open_chat.await?;
this.update(&mut cx, |this, cx| {
this.markdown_data = Default::default();
this.set_active_chat(chat, cx);
})
})
@@ -546,7 +660,7 @@ impl ChatPanel {
if let Some((chat, _)) = &self.active_chat {
let channel_id = chat.read(cx).channel().id;
if let Some(workspace) = self.workspace.upgrade(cx) {
ChannelView::deploy(channel_id, workspace, cx);
ChannelView::open(channel_id, workspace, cx).detach();
}
}
}
@@ -561,6 +675,72 @@ impl ChatPanel {
}
}
fn render_avatar(avatar: Option<Arc<ImageData>>, theme: &Arc<Theme>) -> AnyElement<ChatPanel> {
let avatar_style = theme.chat_panel.avatar;
avatar
.map(|avatar| {
Image::from_data(avatar)
.with_style(avatar_style.image)
.aligned()
.contained()
.with_corner_radius(avatar_style.outer_corner_radius)
.constrained()
.with_width(avatar_style.outer_width)
.with_height(avatar_style.outer_width)
.into_any()
})
.unwrap_or_else(|| {
Empty::new()
.constrained()
.with_width(avatar_style.outer_width)
.into_any()
})
.contained()
.with_style(theme.chat_panel.avatar_container)
.into_any()
}
fn render_remove(
message_id_to_remove: Option<u64>,
cx: &mut ViewContext<'_, '_, ChatPanel>,
theme: &Arc<Theme>,
) -> AnyElement<ChatPanel> {
enum DeleteMessage {}
message_id_to_remove
.map(|id| {
MouseEventHandler::new::<DeleteMessage, _>(id as usize, cx, |mouse_state, _| {
let button_style = theme.chat_panel.icon_button.style_for(mouse_state);
render_icon_button(button_style, "icons/x.svg")
.aligned()
.into_any()
})
.with_padding(Padding::uniform(2.))
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
this.remove_message(id, cx);
})
.flex_float()
.into_any()
})
.unwrap_or_else(|| {
let style = theme.chat_panel.icon_button.default;
Empty::new()
.constrained()
.with_width(style.icon_width)
.aligned()
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.contained()
.with_uniform_padding(2.)
.flex_float()
.into_any()
})
}
impl Entity for ChatPanel {
type Event = Event;
}
@@ -627,8 +807,12 @@ impl Panel for ChatPanel {
}
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
if active && !is_chat_feature_enabled(cx) {
cx.emit(Event::Dismissed);
self.active = active;
if active {
self.acknowledge_last_message(cx);
if !is_chat_feature_enabled(cx) {
cx.emit(Event::Dismissed);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@ use crate::{
contact_notification::ContactNotification, face_pile::FacePile, toggle_deafen, toggle_mute,
toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing,
};
use auto_update::AutoUpdateStatus;
use call::{ActiveCall, ParticipantLocation, Room};
use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
use clock::ReplicaId;
@@ -215,7 +216,13 @@ impl CollabTitlebarItem {
let git_style = theme.titlebar.git_menu_button.clone();
let item_spacing = theme.titlebar.item_spacing;
let mut ret = Flex::row().with_child(
let mut ret = Flex::row();
if let Some(project_host) = self.collect_project_host(theme.clone(), cx) {
ret = ret.with_child(project_host)
}
ret = ret.with_child(
Stack::new()
.with_child(
MouseEventHandler::new::<ToggleProjectMenu, _>(0, cx, |mouse_state, cx| {
@@ -283,6 +290,71 @@ impl CollabTitlebarItem {
ret.into_any()
}
fn collect_project_host(
&self,
theme: Arc<Theme>,
cx: &mut ViewContext<Self>,
) -> Option<AnyElement<Self>> {
if ActiveCall::global(cx).read(cx).room().is_none() {
return None;
}
let project = self.project.read(cx);
let user_store = self.user_store.read(cx);
if project.is_local() {
return None;
}
let Some(host) = project.host() else {
return None;
};
let (Some(host_user), Some(participant_index)) = (
user_store.get_cached_user(host.user_id),
user_store.participant_indices().get(&host.user_id),
) else {
return None;
};
enum ProjectHost {}
enum ProjectHostTooltip {}
let host_style = theme.titlebar.project_host.clone();
let selection_style = theme
.editor
.selection_style_for_room_participant(participant_index.0);
let peer_id = host.peer_id.clone();
Some(
MouseEventHandler::new::<ProjectHost, _>(0, cx, |mouse_state, _| {
let mut host_style = host_style.style_for(mouse_state).clone();
host_style.text.color = selection_style.cursor;
Label::new(host_user.github_login.clone(), host_style.text)
.contained()
.with_style(host_style.container)
.aligned()
.left()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
if let Some(task) =
workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
{
task.detach_and_log_err(cx);
}
}
})
.with_tooltip::<ProjectHostTooltip>(
0,
host_user.github_login.clone() + " is sharing this project. Click to follow.",
None,
theme.tooltip.clone(),
cx,
)
.into_any_named("project-host"),
)
}
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
let project = if active {
Some(self.project.clone())
@@ -877,7 +949,7 @@ impl CollabTitlebarItem {
fn render_face_pile(
&self,
user: &User,
replica_id: Option<ReplicaId>,
_replica_id: Option<ReplicaId>,
peer_id: PeerId,
location: Option<ParticipantLocation>,
muted: bool,
@@ -886,23 +958,20 @@ impl CollabTitlebarItem {
theme: &Theme,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let user_id = user.id;
let project_id = workspace.read(cx).project().read(cx).remote_id();
let room = ActiveCall::global(cx).read(cx).room();
let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
let followed_by_self = room
.and_then(|room| {
Some(
is_being_followed
&& room
.read(cx)
.followers_for(peer_id, project_id?)
.iter()
.any(|&follower| {
Some(follower) == workspace.read(cx).client().peer_id()
}),
)
})
.unwrap_or(false);
let room = ActiveCall::global(cx).read(cx).room().cloned();
let self_peer_id = workspace.read(cx).client().peer_id();
let self_following = workspace.read(cx).is_being_followed(peer_id);
let self_following_initialized = self_following
&& room.as_ref().map_or(false, |room| match project_id {
None => true,
Some(project_id) => room
.read(cx)
.followers_for(peer_id, project_id)
.iter()
.any(|&follower| Some(follower) == self_peer_id),
});
let leader_style = theme.titlebar.leader_avatar;
let follower_style = theme.titlebar.follower_avatar;
@@ -921,147 +990,131 @@ impl CollabTitlebarItem {
.background_color
.unwrap_or_default();
if let Some(replica_id) = replica_id {
if followed_by_self {
let selection = theme.editor.replica_selection_style(replica_id).selection;
let participant_index = self
.user_store
.read(cx)
.participant_indices()
.get(&user_id)
.copied();
if let Some(participant_index) = participant_index {
if self_following_initialized {
let selection = theme
.editor
.selection_style_for_room_participant(participant_index.0)
.selection;
background_color = Color::blend(selection, background_color);
background_color.a = 255;
}
}
let mut content = Stack::new()
.with_children(user.avatar.as_ref().map(|avatar| {
let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
.with_child(Self::render_face(
avatar.clone(),
Self::location_style(workspace, location, leader_style, cx),
background_color,
microphone_state,
))
.with_children(
(|| {
let project_id = project_id?;
let room = room?.read(cx);
let followers = room.followers_for(peer_id, project_id);
enum TitlebarParticipant {}
Some(followers.into_iter().flat_map(|&follower| {
let remote_participant =
room.remote_participant_for_peer_id(follower);
let avatar = remote_participant
.and_then(|p| p.user.avatar.clone())
.or_else(|| {
if follower == workspace.read(cx).client().peer_id()? {
workspace
.read(cx)
.user_store()
.read(cx)
.current_user()?
.avatar
.clone()
} else {
None
let content = MouseEventHandler::new::<TitlebarParticipant, _>(
peer_id.as_u64() as usize,
cx,
move |_, cx| {
Stack::new()
.with_children(user.avatar.as_ref().map(|avatar| {
let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
.with_child(Self::render_face(
avatar.clone(),
Self::location_style(workspace, location, leader_style, cx),
background_color,
microphone_state,
))
.with_children(
(|| {
let project_id = project_id?;
let room = room?.read(cx);
let followers = room.followers_for(peer_id, project_id);
Some(followers.into_iter().filter_map(|&follower| {
if Some(follower) == self_peer_id {
return None;
}
})?;
let participant =
room.remote_participant_for_peer_id(follower)?;
Some(Self::render_face(
participant.user.avatar.clone()?,
follower_style,
background_color,
None,
))
}))
})()
.into_iter()
.flatten(),
)
.with_children(
self_following_initialized
.then(|| self.user_store.read(cx).current_user())
.and_then(|user| {
Some(Self::render_face(
user?.avatar.clone()?,
follower_style,
background_color,
None,
))
}),
);
Some(Self::render_face(
avatar.clone(),
follower_style,
background_color,
None,
))
}))
})()
.into_iter()
.flatten(),
);
let mut container = face_pile
.contained()
.with_style(theme.titlebar.leader_selection);
let mut container = face_pile
.contained()
.with_style(theme.titlebar.leader_selection);
if let Some(replica_id) = replica_id {
if followed_by_self {
let color = theme.editor.replica_selection_style(replica_id).selection;
container = container.with_background_color(color);
}
}
container
}))
.with_children((|| {
let replica_id = replica_id?;
let color = theme.editor.replica_selection_style(replica_id).cursor;
Some(
AvatarRibbon::new(color)
.constrained()
.with_width(theme.titlebar.avatar_ribbon.width)
.with_height(theme.titlebar.avatar_ribbon.height)
.aligned()
.bottom(),
)
})())
.into_any();
if let Some(location) = location {
if let Some(replica_id) = replica_id {
enum ToggleFollow {}
content = MouseEventHandler::new::<ToggleFollow, _>(
replica_id.into(),
cx,
move |_, _| content,
)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, item, cx| {
if let Some(workspace) = item.workspace.upgrade(cx) {
if let Some(task) = workspace
.update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx))
{
task.detach_and_log_err(cx);
if let Some(participant_index) = participant_index {
if self_following_initialized {
let color = theme
.editor
.selection_style_for_room_participant(participant_index.0)
.selection;
container = container.with_background_color(color);
}
}
}
})
.with_tooltip::<ToggleFollow>(
peer_id.as_u64() as usize,
if is_being_followed {
format!("Unfollow {}", user.github_login)
} else {
format!("Follow {}", user.github_login)
},
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.into_any();
} else if let ParticipantLocation::SharedProject { project_id } = location {
enum JoinProject {}
let user_id = user.id;
content = MouseEventHandler::new::<JoinProject, _>(
peer_id.as_u64() as usize,
cx,
move |_, _| content,
)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
let app_state = workspace.read(cx).app_state().clone();
workspace::join_remote_project(project_id, user_id, app_state, cx)
.detach_and_log_err(cx);
}
})
.with_tooltip::<JoinProject>(
peer_id.as_u64() as usize,
format!("Follow {} into external project", user.github_login),
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.into_any();
}
container
}))
.with_children((|| {
let participant_index = participant_index?;
let color = theme
.editor
.selection_style_for_room_participant(participant_index.0)
.cursor;
Some(
AvatarRibbon::new(color)
.constrained()
.with_width(theme.titlebar.avatar_ribbon.width)
.with_height(theme.titlebar.avatar_ribbon.height)
.aligned()
.bottom(),
)
})())
},
);
if Some(peer_id) == self_peer_id {
return content.into_any();
}
content
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| {
let Some(workspace) = this.workspace.upgrade(cx) else {
return;
};
if let Some(task) =
workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
{
task.detach_and_log_err(cx);
}
})
.with_tooltip::<TitlebarParticipant>(
peer_id.as_u64() as usize,
format!("Follow {}", user.github_login),
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.into_any()
}
fn location_style(
@@ -1125,22 +1178,38 @@ impl CollabTitlebarItem {
.with_style(theme.titlebar.offline_icon.container)
.into_any(),
),
client::Status::UpgradeRequired => Some(
MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
Label::new(
"Please update Zed to collaborate",
theme.titlebar.outdated_warning.text.clone(),
)
.contained()
.with_style(theme.titlebar.outdated_warning.container)
.aligned()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| {
auto_update::check(&Default::default(), cx);
})
.into_any(),
),
client::Status::UpgradeRequired => {
let auto_updater = auto_update::AutoUpdater::get(cx);
let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
Some(AutoUpdateStatus::Installing)
| Some(AutoUpdateStatus::Downloading)
| Some(AutoUpdateStatus::Checking) => "Updating...",
Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
"Please update Zed to Collaborate"
}
};
Some(
MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
Label::new(label, theme.titlebar.outdated_warning.text.clone())
.contained()
.with_style(theme.titlebar.outdated_warning.container)
.aligned()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| {
if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
workspace::restart(&Default::default(), cx);
return;
}
}
auto_update::check(&Default::default(), cx);
})
.into_any(),
)
}
_ => None,
}
}

View File

@@ -7,10 +7,10 @@ mod face_pile;
mod incoming_call_notification;
mod notifications;
mod panel_settings;
mod project_shared_notification;
pub mod project_shared_notification;
mod sharing_status_indicator;
use call::{ActiveCall, Room};
use call::{report_call_event_for_room, ActiveCall, Room};
use gpui::{
actions,
geometry::{
@@ -55,18 +55,18 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
let client = call.client();
let toggle_screen_sharing = room.update(cx, |room, cx| {
if room.is_screen_sharing() {
ActiveCall::report_call_event_for_room(
report_call_event_for_room(
"disable screen share",
Some(room.id()),
room.id(),
room.channel_id(),
&client,
cx,
);
Task::ready(room.unshare_screen(cx))
} else {
ActiveCall::report_call_event_for_room(
report_call_event_for_room(
"enable screen share",
Some(room.id()),
room.id(),
room.channel_id(),
&client,
cx,
@@ -83,23 +83,13 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
if let Some(room) = call.room().cloned() {
let client = call.client();
room.update(cx, |room, cx| {
if room.is_muted(cx) {
ActiveCall::report_call_event_for_room(
"enable microphone",
Some(room.id()),
room.channel_id(),
&client,
cx,
);
let operation = if room.is_muted(cx) {
"enable microphone"
} else {
ActiveCall::report_call_event_for_room(
"disable microphone",
Some(room.id()),
room.channel_id(),
&client,
cx,
);
}
"disable microphone"
};
report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
room.toggle_mute(cx)
})
.map(|task| task.detach_and_log_err(cx))

View File

@@ -40,7 +40,9 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
.push(window);
}
}
room::Event::RemoteProjectUnshared { project_id } => {
room::Event::RemoteProjectUnshared { project_id }
| room::Event::RemoteProjectJoined { project_id }
| room::Event::RemoteProjectInvitationDiscarded { project_id } => {
if let Some(windows) = notification_windows.remove(&project_id) {
for window in windows {
window.remove(cx);
@@ -82,7 +84,6 @@ impl ProjectSharedNotification {
}
fn join(&mut self, cx: &mut ViewContext<Self>) {
cx.remove_window();
if let Some(app_state) = self.app_state.upgrade() {
workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
.detach_and_log_err(cx);
@@ -90,7 +91,15 @@ impl ProjectSharedNotification {
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
cx.remove_window();
if let Some(active_room) =
ActiveCall::global(cx).read_with(cx, |call, _| call.room().cloned())
{
active_room.update(cx, |_, cx| {
cx.emit(room::Event::RemoteProjectInvitationDiscarded {
project_id: self.project_id,
});
});
}
}
fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {

View File

@@ -18,6 +18,15 @@ actions!(command_palette, [Toggle]);
pub type CommandPalette = Picker<CommandPaletteDelegate>;
pub type CommandPaletteInterceptor =
Box<dyn Fn(&str, &AppContext) -> Option<CommandInterceptResult>>;
pub struct CommandInterceptResult {
pub action: Box<dyn Action>,
pub string: String,
pub positions: Vec<usize>,
}
pub struct CommandPaletteDelegate {
actions: Vec<Command>,
matches: Vec<StringMatch>,
@@ -117,7 +126,7 @@ impl PickerDelegate for CommandPaletteDelegate {
}
})
.collect::<Vec<_>>();
let actions = cx.read(move |cx| {
let mut actions = cx.read(move |cx| {
let hit_counts = cx.optional_global::<HitCounts>();
actions.sort_by_key(|action| {
(
@@ -136,7 +145,7 @@ impl PickerDelegate for CommandPaletteDelegate {
char_bag: command.name.chars().collect(),
})
.collect::<Vec<_>>();
let matches = if query.is_empty() {
let mut matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
@@ -158,6 +167,40 @@ impl PickerDelegate for CommandPaletteDelegate {
)
.await
};
let intercept_result = cx.read(|cx| {
if cx.has_global::<CommandPaletteInterceptor>() {
cx.global::<CommandPaletteInterceptor>()(&query, cx)
} else {
None
}
});
if let Some(CommandInterceptResult {
action,
string,
positions,
}) = intercept_result
{
if let Some(idx) = matches
.iter()
.position(|m| actions[m.candidate_id].action.id() == action.id())
{
matches.remove(idx);
}
actions.push(Command {
name: string.clone(),
action,
keystrokes: vec![],
});
matches.insert(
0,
StringMatch {
candidate_id: actions.len() - 1,
string,
positions,
score: 0.0,
},
)
}
picker
.update(&mut cx, |picker, _| {
let delegate = picker.delegate_mut();
@@ -222,7 +265,7 @@ impl PickerDelegate for CommandPaletteDelegate {
.with_children(
[
(keystroke.ctrl, "^"),
(keystroke.alt, ""),
(keystroke.alt, ""),
(keystroke.cmd, ""),
(keystroke.shift, ""),
]

View File

@@ -0,0 +1,49 @@
[package]
name = "copilot2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/copilot2.rs"
doctest = false
[features]
test-support = [
"collections/test-support",
"gpui2/test-support",
"language2/test-support",
"lsp2/test-support",
"settings2/test-support",
"util/test-support",
]
[dependencies]
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
gpui2 = { path = "../gpui2" }
language2 = { path = "../language2" }
settings2 = { path = "../settings2" }
theme = { path = "../theme" }
lsp2 = { path = "../lsp2" }
node_runtime = { path = "../node_runtime"}
util = { path = "../util" }
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
async-tar = "0.4.2"
anyhow.workspace = true
log.workspace = true
serde.workspace = true
serde_derive.workspace = true
smol.workspace = true
futures.workspace = true
[dev-dependencies]
clock = { path = "../clock" }
collections = { path = "../collections", features = ["test-support"] }
fs = { path = "../fs", features = ["test-support"] }
gpui2 = { path = "../gpui2", features = ["test-support"] }
language2 = { path = "../language2", features = ["test-support"] }
lsp2 = { path = "../lsp2", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
settings2 = { path = "../settings2", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,225 @@
use serde::{Deserialize, Serialize};
pub enum CheckStatus {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CheckStatusParams {
pub local_checks_only: bool,
}
impl lsp2::request::Request for CheckStatus {
type Params = CheckStatusParams;
type Result = SignInStatus;
const METHOD: &'static str = "checkStatus";
}
pub enum SignInInitiate {}
#[derive(Debug, Serialize, Deserialize)]
pub struct SignInInitiateParams {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "status")]
pub enum SignInInitiateResult {
AlreadySignedIn { user: String },
PromptUserDeviceFlow(PromptUserDeviceFlow),
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptUserDeviceFlow {
pub user_code: String,
pub verification_uri: String,
}
impl lsp2::request::Request for SignInInitiate {
type Params = SignInInitiateParams;
type Result = SignInInitiateResult;
const METHOD: &'static str = "signInInitiate";
}
pub enum SignInConfirm {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignInConfirmParams {
pub user_code: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "status")]
pub enum SignInStatus {
#[serde(rename = "OK")]
Ok {
user: String,
},
MaybeOk {
user: String,
},
AlreadySignedIn {
user: String,
},
NotAuthorized {
user: String,
},
NotSignedIn,
}
impl lsp2::request::Request for SignInConfirm {
type Params = SignInConfirmParams;
type Result = SignInStatus;
const METHOD: &'static str = "signInConfirm";
}
pub enum SignOut {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignOutParams {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignOutResult {}
impl lsp2::request::Request for SignOut {
type Params = SignOutParams;
type Result = SignOutResult;
const METHOD: &'static str = "signOut";
}
pub enum GetCompletions {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetCompletionsParams {
pub doc: GetCompletionsDocument,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetCompletionsDocument {
pub tab_size: u32,
pub indent_size: u32,
pub insert_spaces: bool,
pub uri: lsp2::Url,
pub relative_path: String,
pub position: lsp2::Position,
pub version: usize,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetCompletionsResult {
pub completions: Vec<Completion>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Completion {
pub text: String,
pub position: lsp2::Position,
pub uuid: String,
pub range: lsp2::Range,
pub display_text: String,
}
impl lsp2::request::Request for GetCompletions {
type Params = GetCompletionsParams;
type Result = GetCompletionsResult;
const METHOD: &'static str = "getCompletions";
}
pub enum GetCompletionsCycling {}
impl lsp2::request::Request for GetCompletionsCycling {
type Params = GetCompletionsParams;
type Result = GetCompletionsResult;
const METHOD: &'static str = "getCompletionsCycling";
}
pub enum LogMessage {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LogMessageParams {
pub level: u8,
pub message: String,
pub metadata_str: String,
pub extra: Vec<String>,
}
impl lsp2::notification::Notification for LogMessage {
type Params = LogMessageParams;
const METHOD: &'static str = "LogMessage";
}
pub enum StatusNotification {}
#[derive(Debug, Serialize, Deserialize)]
pub struct StatusNotificationParams {
pub message: String,
pub status: String, // One of Normal/InProgress
}
impl lsp2::notification::Notification for StatusNotification {
type Params = StatusNotificationParams;
const METHOD: &'static str = "statusNotification";
}
pub enum SetEditorInfo {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SetEditorInfoParams {
pub editor_info: EditorInfo,
pub editor_plugin_info: EditorPluginInfo,
}
impl lsp2::request::Request for SetEditorInfo {
type Params = SetEditorInfoParams;
type Result = String;
const METHOD: &'static str = "setEditorInfo";
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EditorInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EditorPluginInfo {
pub name: String,
pub version: String,
}
pub enum NotifyAccepted {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NotifyAcceptedParams {
pub uuid: String,
}
impl lsp2::request::Request for NotifyAccepted {
type Params = NotifyAcceptedParams;
type Result = String;
const METHOD: &'static str = "notifyAccepted";
}
pub enum NotifyRejected {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NotifyRejectedParams {
pub uuids: Vec<String>,
}
impl lsp2::request::Request for NotifyRejected {
type Params = NotifyRejectedParams;
type Result = String;
const METHOD: &'static str = "notifyRejected";
}

View File

@@ -0,0 +1,376 @@
// TODO add logging in
// use crate::{request::PromptUserDeviceFlow, Copilot, Status};
// use gpui::{
// elements::*,
// geometry::rect::RectF,
// platform::{WindowBounds, WindowKind, WindowOptions},
// AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
// WindowHandle,
// };
// use theme::ui::modal;
// #[derive(PartialEq, Eq, Debug, Clone)]
// struct CopyUserCode;
// #[derive(PartialEq, Eq, Debug, Clone)]
// struct OpenGithub;
// const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
// pub fn init(cx: &mut AppContext) {
// if let Some(copilot) = Copilot::global(cx) {
// let mut verification_window: Option<WindowHandle<CopilotCodeVerification>> = None;
// cx.observe(&copilot, move |copilot, cx| {
// let status = copilot.read(cx).status();
// match &status {
// crate::Status::SigningIn { prompt } => {
// if let Some(window) = verification_window.as_mut() {
// let updated = window
// .root(cx)
// .map(|root| {
// root.update(cx, |verification, cx| {
// verification.set_status(status.clone(), cx);
// cx.activate_window();
// })
// })
// .is_some();
// if !updated {
// verification_window = Some(create_copilot_auth_window(cx, &status));
// }
// } else if let Some(_prompt) = prompt {
// verification_window = Some(create_copilot_auth_window(cx, &status));
// }
// }
// Status::Authorized | Status::Unauthorized => {
// if let Some(window) = verification_window.as_ref() {
// if let Some(verification) = window.root(cx) {
// verification.update(cx, |verification, cx| {
// verification.set_status(status, cx);
// cx.platform().activate(true);
// cx.activate_window();
// });
// }
// }
// }
// _ => {
// if let Some(code_verification) = verification_window.take() {
// code_verification.update(cx, |cx| cx.remove_window());
// }
// }
// }
// })
// .detach();
// }
// }
// fn create_copilot_auth_window(
// cx: &mut AppContext,
// status: &Status,
// ) -> WindowHandle<CopilotCodeVerification> {
// let window_size = theme::current(cx).copilot.modal.dimensions();
// let window_options = WindowOptions {
// bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
// titlebar: None,
// center: true,
// focus: true,
// show: true,
// kind: WindowKind::Normal,
// is_movable: true,
// screen: None,
// };
// cx.add_window(window_options, |_cx| {
// CopilotCodeVerification::new(status.clone())
// })
// }
// pub struct CopilotCodeVerification {
// status: Status,
// connect_clicked: bool,
// }
// impl CopilotCodeVerification {
// pub fn new(status: Status) -> Self {
// Self {
// status,
// connect_clicked: false,
// }
// }
// pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
// self.status = status;
// cx.notify();
// }
// fn render_device_code(
// data: &PromptUserDeviceFlow,
// style: &theme::Copilot,
// cx: &mut ViewContext<Self>,
// ) -> impl Element<Self> {
// let copied = cx
// .read_from_clipboard()
// .map(|item| item.text() == &data.user_code)
// .unwrap_or(false);
// let device_code_style = &style.auth.prompting.device_code;
// MouseEventHandler::new::<Self, _>(0, cx, |state, _cx| {
// Flex::row()
// .with_child(
// Label::new(data.user_code.clone(), device_code_style.text.clone())
// .aligned()
// .contained()
// .with_style(device_code_style.left_container)
// .constrained()
// .with_width(device_code_style.left),
// )
// .with_child(
// Label::new(
// if copied { "Copied!" } else { "Copy" },
// device_code_style.cta.style_for(state).text.clone(),
// )
// .aligned()
// .contained()
// .with_style(*device_code_style.right_container.style_for(state))
// .constrained()
// .with_width(device_code_style.right),
// )
// .contained()
// .with_style(device_code_style.cta.style_for(state).container)
// })
// .on_click(gpui::platform::MouseButton::Left, {
// let user_code = data.user_code.clone();
// move |_, _, cx| {
// cx.platform()
// .write_to_clipboard(ClipboardItem::new(user_code.clone()));
// cx.notify();
// }
// })
// .with_cursor_style(gpui::platform::CursorStyle::PointingHand)
// }
// fn render_prompting_modal(
// connect_clicked: bool,
// data: &PromptUserDeviceFlow,
// style: &theme::Copilot,
// cx: &mut ViewContext<Self>,
// ) -> AnyElement<Self> {
// enum ConnectButton {}
// Flex::column()
// .with_child(
// Flex::column()
// .with_children([
// Label::new(
// "Enable Copilot by connecting",
// style.auth.prompting.subheading.text.clone(),
// )
// .aligned(),
// Label::new(
// "your existing license.",
// style.auth.prompting.subheading.text.clone(),
// )
// .aligned(),
// ])
// .align_children_center()
// .contained()
// .with_style(style.auth.prompting.subheading.container),
// )
// .with_child(Self::render_device_code(data, &style, cx))
// .with_child(
// Flex::column()
// .with_children([
// Label::new(
// "Paste this code into GitHub after",
// style.auth.prompting.hint.text.clone(),
// )
// .aligned(),
// Label::new(
// "clicking the button below.",
// style.auth.prompting.hint.text.clone(),
// )
// .aligned(),
// ])
// .align_children_center()
// .contained()
// .with_style(style.auth.prompting.hint.container.clone()),
// )
// .with_child(theme::ui::cta_button::<ConnectButton, _, _, _>(
// if connect_clicked {
// "Waiting for connection..."
// } else {
// "Connect to GitHub"
// },
// style.auth.content_width,
// &style.auth.cta_button,
// cx,
// {
// let verification_uri = data.verification_uri.clone();
// move |_, verification, cx| {
// cx.platform().open_url(&verification_uri);
// verification.connect_clicked = true;
// }
// },
// ))
// .align_children_center()
// .into_any()
// }
// fn render_enabled_modal(
// style: &theme::Copilot,
// cx: &mut ViewContext<Self>,
// ) -> AnyElement<Self> {
// enum DoneButton {}
// let enabled_style = &style.auth.authorized;
// Flex::column()
// .with_child(
// Label::new("Copilot Enabled!", enabled_style.subheading.text.clone())
// .contained()
// .with_style(enabled_style.subheading.container)
// .aligned(),
// )
// .with_child(
// Flex::column()
// .with_children([
// Label::new(
// "You can update your settings or",
// enabled_style.hint.text.clone(),
// )
// .aligned(),
// Label::new(
// "sign out from the Copilot menu in",
// enabled_style.hint.text.clone(),
// )
// .aligned(),
// Label::new("the status bar.", enabled_style.hint.text.clone()).aligned(),
// ])
// .align_children_center()
// .contained()
// .with_style(enabled_style.hint.container),
// )
// .with_child(theme::ui::cta_button::<DoneButton, _, _, _>(
// "Done",
// style.auth.content_width,
// &style.auth.cta_button,
// cx,
// |_, _, cx| cx.remove_window(),
// ))
// .align_children_center()
// .into_any()
// }
// fn render_unauthorized_modal(
// style: &theme::Copilot,
// cx: &mut ViewContext<Self>,
// ) -> AnyElement<Self> {
// let unauthorized_style = &style.auth.not_authorized;
// Flex::column()
// .with_child(
// Flex::column()
// .with_children([
// Label::new(
// "Enable Copilot by connecting",
// unauthorized_style.subheading.text.clone(),
// )
// .aligned(),
// Label::new(
// "your existing license.",
// unauthorized_style.subheading.text.clone(),
// )
// .aligned(),
// ])
// .align_children_center()
// .contained()
// .with_style(unauthorized_style.subheading.container),
// )
// .with_child(
// Flex::column()
// .with_children([
// Label::new(
// "You must have an active copilot",
// unauthorized_style.warning.text.clone(),
// )
// .aligned(),
// Label::new(
// "license to use it in Zed.",
// unauthorized_style.warning.text.clone(),
// )
// .aligned(),
// ])
// .align_children_center()
// .contained()
// .with_style(unauthorized_style.warning.container),
// )
// .with_child(theme::ui::cta_button::<Self, _, _, _>(
// "Subscribe on GitHub",
// style.auth.content_width,
// &style.auth.cta_button,
// cx,
// |_, _, cx| {
// cx.remove_window();
// cx.platform().open_url(COPILOT_SIGN_UP_URL)
// },
// ))
// .align_children_center()
// .into_any()
// }
// }
// impl Entity for CopilotCodeVerification {
// type Event = ();
// }
// impl View for CopilotCodeVerification {
// fn ui_name() -> &'static str {
// "CopilotCodeVerification"
// }
// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
// cx.notify()
// }
// fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
// cx.notify()
// }
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// enum ConnectModal {}
// let style = theme::current(cx).clone();
// modal::<ConnectModal, _, _, _, _>(
// "Connect Copilot to Zed",
// &style.copilot.modal,
// cx,
// |cx| {
// Flex::column()
// .with_children([
// theme::ui::icon(&style.copilot.auth.header).into_any(),
// match &self.status {
// Status::SigningIn {
// prompt: Some(prompt),
// } => Self::render_prompting_modal(
// self.connect_clicked,
// &prompt,
// &style.copilot,
// cx,
// ),
// Status::Unauthorized => {
// self.connect_clicked = false;
// Self::render_unauthorized_modal(&style.copilot, cx)
// }
// Status::Authorized => {
// self.connect_clicked = false;
// Self::render_enabled_modal(&style.copilot, cx)
// }
// _ => Empty::new().into_any(),
// },
// ])
// .align_children_center()
// },
// )
// .into_any()
// }
// }

33
crates/db2/Cargo.toml Normal file
View File

@@ -0,0 +1,33 @@
[package]
name = "db2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/db2.rs"
doctest = false
[features]
test-support = []
[dependencies]
collections = { path = "../collections" }
gpui2 = { path = "../gpui2" }
sqlez = { path = "../sqlez" }
sqlez_macros = { path = "../sqlez_macros" }
util = { path = "../util" }
anyhow.workspace = true
indoc.workspace = true
async-trait.workspace = true
lazy_static.workspace = true
log.workspace = true
parking_lot.workspace = true
serde.workspace = true
serde_derive.workspace = true
smol.workspace = true
[dev-dependencies]
gpui2 = { path = "../gpui2", features = ["test-support"] }
env_logger.workspace = true
tempdir.workspace = true

5
crates/db2/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/

327
crates/db2/src/db2.rs Normal file
View File

@@ -0,0 +1,327 @@
pub mod kvp;
pub mod query;
// Re-export
pub use anyhow;
use anyhow::Context;
use gpui2::AppContext;
pub use indoc::indoc;
pub use lazy_static;
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::future::Future;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use util::channel::ReleaseChannel;
use util::{async_iife, ResultExt};
const CONNECTION_INITIALIZE_QUERY: &'static str = sql!(
PRAGMA foreign_keys=TRUE;
);
const DB_INITIALIZE_QUERY: &'static str = sql!(
PRAGMA journal_mode=WAL;
PRAGMA busy_timeout=1;
PRAGMA case_sensitive_like=TRUE;
PRAGMA synchronous=NORMAL;
);
const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
const DB_FILE_NAME: &'static str = "db.sqlite";
lazy_static::lazy_static! {
pub static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
}
/// Open or create a database at the given directory path.
/// This will retry a couple times if there are failures. If opening fails once, the db directory
/// is moved to a backup folder and a new one is created. If that fails, a shared in memory db is created.
/// In either case, static variables are set so that the user can be notified.
pub async fn open_db<M: Migrator + 'static>(
db_dir: &Path,
release_channel: &ReleaseChannel,
) -> ThreadSafeConnection<M> {
if *ZED_STATELESS {
return open_fallback_db().await;
}
let release_channel_name = release_channel.dev_name();
let main_db_dir = db_dir.join(Path::new(&format!("0-{}", release_channel_name)));
let connection = async_iife!({
smol::fs::create_dir_all(&main_db_dir)
.await
.context("Could not create db directory")
.log_err()?;
let db_path = main_db_dir.join(Path::new(DB_FILE_NAME));
open_main_db(&db_path).await
})
.await;
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
}
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()
}
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
}
}
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)));
}
};
}
pub fn write_and_log<F>(cx: &mut AppContext, db_write: impl FnOnce() -> F + Send + 'static)
where
F: Future<Output = anyhow::Result<()>> + Send,
{
cx.executor()
.spawn(async move { db_write().await.log_err() })
.detach()
}
// #[cfg(test)]
// mod tests {
// use std::thread;
// use sqlez::domain::Domain;
// use sqlez_macros::sql;
// use tempdir::TempDir;
// use crate::open_db;
// // 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()
// );
// }
// /// Test that DB exists but corrupted (causing recreate)
// #[gpui::test(iterations = 30)]
// 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());
// }
// }
// }

62
crates/db2/src/kvp.rs Normal file
View File

@@ -0,0 +1,62 @@
use sqlez_macros::sql;
use crate::{define_connection, query};
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 KeyValueStore {
query! {
pub fn read_kvp(key: &str) -> Result<Option<String>> {
SELECT value FROM kv_store WHERE key = (?)
}
}
query! {
pub async fn write_kvp(key: String, value: String) -> Result<()> {
INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?))
}
}
query! {
pub async fn delete_kvp(key: String) -> Result<()> {
DELETE FROM kv_store WHERE key = (?)
}
}
}
// #[cfg(test)]
// mod tests {
// use crate::kvp::KeyValueStore;
// #[gpui::test]
// async fn test_kvp() {
// let db = KeyValueStore(crate::open_test_db("test_kvp").await);
// assert_eq!(db.read_kvp("key-1").unwrap(), 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".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-2".to_string(), "two".to_string())
// .await
// .unwrap();
// assert_eq!(db.read_kvp("key-2").unwrap(), Some("two".to_string()));
// db.delete_kvp("key-1".to_string()).await.unwrap();
// assert_eq!(db.read_kvp("key-1").unwrap(), None);
// }
// }

314
crates/db2/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(move |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
))
}).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

@@ -21,6 +21,9 @@ util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow.workspace = true
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
smallvec.workspace = true
postage.workspace = true

View File

@@ -1,4 +1,6 @@
pub mod items;
mod project_diagnostics_settings;
mod toolbar_controls;
use anyhow::Result;
use collections::{BTreeSet, HashSet};
@@ -19,6 +21,7 @@ use language::{
};
use lsp::LanguageServerId;
use project::{DiagnosticSummary, Project, ProjectPath};
use project_diagnostics_settings::ProjectDiagnosticsSettings;
use serde_json::json;
use smallvec::SmallVec;
use std::{
@@ -30,18 +33,21 @@ use std::{
sync::Arc,
};
use theme::ThemeSettings;
pub use toolbar_controls::ToolbarControls;
use util::TryFutureExt;
use workspace::{
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
ItemNavHistory, Pane, PaneBackdrop, ToolbarItemLocation, Workspace,
};
actions!(diagnostics, [Deploy]);
actions!(diagnostics, [Deploy, ToggleWarnings]);
const CONTEXT_LINE_COUNT: u32 = 1;
pub fn init(cx: &mut AppContext) {
settings::register::<ProjectDiagnosticsSettings>(cx);
cx.add_action(ProjectDiagnosticsEditor::deploy);
cx.add_action(ProjectDiagnosticsEditor::toggle_warnings);
items::init(cx);
}
@@ -55,6 +61,7 @@ struct ProjectDiagnosticsEditor {
excerpts: ModelHandle<MultiBuffer>,
path_states: Vec<PathState>,
paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,
include_warnings: bool,
}
struct PathState {
@@ -187,6 +194,7 @@ impl ProjectDiagnosticsEditor {
editor,
path_states: Default::default(),
paths_to_update,
include_warnings: settings::get::<ProjectDiagnosticsSettings>(cx).include_warnings,
};
this.update_excerpts(None, cx);
this
@@ -204,6 +212,18 @@ impl ProjectDiagnosticsEditor {
}
}
fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {
self.include_warnings = !self.include_warnings;
self.paths_to_update = self
.project
.read(cx)
.diagnostic_summaries(cx)
.map(|(path, server_id, _)| (path, server_id))
.collect();
self.update_excerpts(None, cx);
cx.notify();
}
fn update_excerpts(
&mut self,
language_server_id: Option<LanguageServerId>,
@@ -277,14 +297,18 @@ impl ProjectDiagnosticsEditor {
let mut blocks_to_add = Vec::new();
let mut blocks_to_remove = HashSet::default();
let mut first_excerpt_id = None;
let max_severity = if self.include_warnings {
DiagnosticSeverity::WARNING
} else {
DiagnosticSeverity::ERROR
};
let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
let mut new_groups = snapshot
.diagnostic_groups(language_server_id)
.into_iter()
.filter(|(_, group)| {
group.entries[group.primary_ix].diagnostic.severity
<= DiagnosticSeverity::WARNING
group.entries[group.primary_ix].diagnostic.severity <= max_severity
})
.peekable();
loop {
@@ -1501,6 +1525,7 @@ mod tests {
client::init_settings(cx);
workspace::init_settings(cx);
Project::init_settings(cx);
crate::init(cx);
});
}

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