Compare commits

...

340 Commits

Author SHA1 Message Date
Max Brunsfeld
e13012c48e Preview 0.62.x 2022-10-26 21:09:00 -07:00
Max Brunsfeld
df708465d1 Ensure only the just-built app bundle is included in the DMG 2022-10-26 21:06:06 -07:00
Kay Simmons
aa9ccf3411 Merge pull request #1823 from zed-industries/reduce-cursor-blink-load
Reduce Cursor Blink CPU Load
2022-10-26 17:58:56 -07:00
Max Brunsfeld
6410fdc474 Clear out bundle directory before creating a new app bundle 2022-10-26 17:49:03 -07:00
Kay Simmons
499d947e69 Merge pull request #1821 from zed-industries/better-pending-bindings
Better pending bindings
2022-10-26 17:42:56 -07:00
K Simmons
c093516351 fix minor warning 2022-10-26 17:42:03 -07:00
K Simmons
41699224ff fix typo in blink manager disable which didn't properly disable, and start editors with the blink manager disabled 2022-10-26 17:39:17 -07:00
Max Brunsfeld
8886cb5786 Fix environment variable reference in bundle app job 2022-10-26 17:34:53 -07:00
Max Brunsfeld
f56f0b7bbb Fix error in bundle app CI job 2022-10-26 17:21:31 -07:00
K Simmons
ae79b50101 Disallow new keybindings when there are any pending 2022-10-26 16:57:23 -07:00
Max Brunsfeld
fcfc4a4298 Dev 0.62.0 2022-10-26 16:38:38 -07:00
Max Brunsfeld
d355bd3372 Merge pull request #1813 from zed-industries/preview-channel
Create preview channel
2022-10-26 16:34:14 -07:00
Max Brunsfeld
2bfd46d48c Fix setting of preview param in RPC URL 2022-10-26 16:19:19 -07:00
Max Brunsfeld
f1b41389b3 Allow overriding release channel at runtime via env var 2022-10-26 16:19:19 -07:00
Max Brunsfeld
92a4998ddc Check invariants before changing git state in railcar script 2022-10-26 16:19:19 -07:00
Max Brunsfeld
23d7209f82 Handle different app names in bundle script 2022-10-26 16:19:19 -07:00
Max Brunsfeld
cf3c610eba Add railcar script 2022-10-26 16:19:19 -07:00
Max Brunsfeld
6a010f58be Account for current release channel in bump-app-version script 2022-10-26 16:19:19 -07:00
Max Brunsfeld
0f1b0a4a78 Use a separate icon for preview releases 2022-10-26 16:19:19 -07:00
Max Brunsfeld
a4a8596a29 Store current release channel name in a file in the zed crate 2022-10-26 16:19:19 -07:00
Max Brunsfeld
1cdd3c0e28 Differentiate preview channel in 'about zed' dialog 2022-10-26 16:19:19 -07:00
Max Brunsfeld
22db5bffe8 Update DO SSL certificate id in kube manifest 2022-10-26 16:19:19 -07:00
Max Brunsfeld
a61f3b715b Create preview channel 2022-10-26 16:19:19 -07:00
K Simmons
949a28d49c wip 2022-10-26 15:57:42 -07:00
Kay Simmons
88be4fe77e Merge pull request #1804 from zed-industries/vim-go-to-line
fix jump to line number in vim mode
2022-10-26 11:43:27 -07:00
Julia
625a62626e Merge pull request #1820 from zed-industries/allow-mouse-move-through-dragged-item-receiver
Propagate mouse move event through dragged-item-receiver if not dragging
2022-10-26 12:22:30 -04:00
Julia
ee440cf300 Propagate mouse move event through dragged-item-receiver if not dragging 2022-10-26 12:06:32 -04:00
Antonio Scandurra
cf2ec99a4d Merge pull request #1819 from zed-industries/remote-renames
Assign a new language when remote buffer is renamed
2022-10-26 16:58:45 +01:00
Antonio Scandurra
bb0f6e85a8 Assign a new language when remote buffer is renamed 2022-10-26 17:52:39 +02:00
Antonio Scandurra
4412217f51 Merge pull request #1817 from zed-industries/show-notifications-on-all-screens
Show call notifications on all screens
2022-10-26 13:45:31 +01:00
Antonio Scandurra
1e85361914 Log instead of panicking when we can't retrieve a drawable 2022-10-26 14:30:48 +02:00
Antonio Scandurra
f611b443c0 Convert window frame rect to screen coordinates 2022-10-26 14:27:53 +02:00
Antonio Scandurra
5984be3d84 Display call notifications on all screens 2022-10-26 12:05:56 +02:00
Antonio Scandurra
5a8061ac7b Add the ability to open a window on a given screen
This is done by supplying the screen in the `WindowOptions` struct.
Note that it's optional, and we will let the operating system choose
which screen to show the window on when `screen` is not provided, as
we did before this change.
2022-10-26 12:04:45 +02:00
Antonio Scandurra
509c327b3b Merge pull request #1816 from zed-industries/letterbox-background
Use the same background color as the editor for `SharedScreen`
2022-10-26 10:37:15 +01:00
Antonio Scandurra
56a66b348d Use the same background color as the editor for SharedScreen 2022-10-26 08:33:32 +02:00
Joseph T. Lyons
a7d86a164c Merge pull request #1812 from zed-industries/fix-500-error-on-user-join
Fix duplicate key error that occurs when a user joins that is already in the db
2022-10-25 16:55:25 -04:00
Joseph T Lyons
383334633f Fix duplicate key error that occurs when a user joins that is already in the db
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
2022-10-25 16:09:36 -04:00
Max Brunsfeld
6a2dc444c6 Merge pull request #1802 from zed-industries/autoclose-with-same-start-and-end
Fix autoclose skipping when start and end are the same character
2022-10-25 12:33:04 -07:00
Max Brunsfeld
e9073310c4 Add test for autoclosing w/ matching start and end char 2022-10-25 12:22:19 -07:00
Antonio Scandurra
3b67602b13 Merge pull request #1810 from zed-industries/contacts-scroll-position
Maintain scroll position in contacts list
2022-10-25 19:43:37 +01:00
Antonio Scandurra
04477e9f97 Explicitly list cargo workspace members
This prevents build failures when there are stale subfolders under
`crates/`.
2022-10-25 19:31:58 +02:00
Antonio Scandurra
990c83eabd Embed live_kit_client's .gitignore into top-level .gitignore
Co-authored-by: Max Brunsfeld <max@zed.dev>
2022-10-25 18:54:34 +02:00
Antonio Scandurra
ddc71653ad Maintain scroll position in contacts list
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-10-25 18:36:33 +02:00
Antonio Scandurra
e5e5cf1314 Merge pull request #1809 from zed-industries/contacts-popover-z-index
Prevent expanded dock from hiding contacts popover
2022-10-25 13:02:51 +01:00
Antonio Scandurra
f364a15d89 Prevent expanded dock from hiding contacts popover 2022-10-25 13:47:37 +02:00
Antonio Scandurra
2b4fd53202 Rename height to z-index 2022-10-25 13:47:12 +02:00
Antonio Scandurra
dfe2fd0386 Allow specifying a custom height for stacking contexts 2022-10-25 13:41:47 +02:00
Antonio Scandurra
2055f05b09 💄 2022-10-25 12:19:25 +02:00
Antonio Scandurra
33ebfc3f10 Rename depth to height when referring to stacking contexts 2022-10-25 12:18:23 +02:00
Antonio Scandurra
6a4f3aaa56 Create a SceneBuilder and sort stacking contexts when calling build 2022-10-25 12:16:09 +02:00
Antonio Scandurra
c1f7ac0d8c Merge pull request #1808 from zed-industries/fix-diagnostics-on-rust
Match progress token's prefix to detect disk-based diagnostic progress
2022-10-25 10:46:59 +01:00
Antonio Scandurra
19adfdf8bb Match progress token's prefix to detect disk-based diagnostic progress
The new version of rust-analyzer changed the disk-based diagnostic token
to `rust-analyzer/checkOnSave/0`. The trailing number could be different
from 0 when there are multiple Rust projects open using the same rust-analyzer
instance.

As such, with this commit we will perform a prefix match as opposed to a strict
equality check when detecting a disk-based diagnostics progress token.
2022-10-25 11:35:59 +02:00
Antonio Scandurra
af74d5409a Merge pull request #1806 from zed-industries/pending-state-when-calling
Show a `Calling` indicator right away when initiating a call
2022-10-25 10:10:44 +01:00
Antonio Scandurra
2a3773240d Show a Calling indicator right away when initiating a call 2022-10-25 11:05:57 +02:00
K Simmons
782676dc67 fix jump to line number in vim mode 2022-10-25 00:39:40 -07:00
Kay Simmons
68717d0fe8 Merge pull request #1792 from zed-industries/fn-modifier
Add fn modifier
2022-10-25 00:35:00 -07:00
Kay Simmons
8bd9577318 Merge pull request #1791 from zed-industries/drag-tabs-more-places
Drag tabs more places
2022-10-25 00:34:50 -07:00
K Simmons
2ac537393d fix failing test 2022-10-25 00:11:59 -07:00
K Simmons
82956b618a remove derive_more 2022-10-25 00:06:43 -07:00
K Simmons
a725ded95e Add fn modifier to modifier keys in gpui and refactor platform events to use a single modifiers struct 2022-10-24 23:50:39 -07:00
K Simmons
113b7f6f97 tweak drop target overlay color and make stack fully constraint children by the first child
's size
2022-10-24 23:47:43 -07:00
K Simmons
aed085b168 remove unnecessary Move branch in dispatch_events 2022-10-24 23:32:01 -07:00
K Simmons
345544646a remove more notify on moves 2022-10-24 23:32:01 -07:00
K Simmons
4520227e98 remove mouse position from render params 2022-10-24 23:32:01 -07:00
K Simmons
f5795ffc6f roll back mouse position in mouse_state struct in favor of using the dragged element position 2022-10-24 23:32:01 -07:00
K Simmons
8cde64d3f6 extract dragged item target 2022-10-24 23:32:00 -07:00
K Simmons
d7b8a189e4 fix issue where empty pane is created 2022-10-24 23:32:00 -07:00
K Simmons
cfde3e348c Add pane splitting by dragged item. Works, but the overlay doesn't clear quite right 2022-10-24 23:31:58 -07:00
K Simmons
70e2951e35 add mouse region handler bool for adding the handler above the child 2022-10-24 23:30:35 -07:00
Julia
ba35536664 Add action to move active item into the dock
Co-Authored-By: Kay Simmons <kay@zed.dev>
2022-10-24 23:30:35 -07:00
Julia
b9f9819637 Handle tab drag end on pane items to insert after active item
Co-Authored-By: Kay Simmons <kay@zed.dev>
2022-10-24 23:30:35 -07:00
Kay Simmons
076d353e84 Merge pull request #1803 from zed-industries/fix-vim-motion-panic
Add more explicit neovim testcase exemptions
2022-10-24 23:30:06 -07:00
K Simmons
64e9b9f893 remove mode after which is unused 2022-10-24 18:31:26 -07:00
K Simmons
21ad375b42 Fix panic in vim motion when not listed as exclusive and add features enum to capture why tests are ignored 2022-10-24 18:27:56 -07:00
Max Brunsfeld
cb9534eae0 Fix autoclose skipping when start and end are the same character 2022-10-24 17:46:06 -07:00
Max Brunsfeld
8b43368bf9 Checkout submodules on CI when publishing collab images 2022-10-24 17:13:20 -07:00
Max Brunsfeld
c96c8fd782 collab 0.2.0 2022-10-24 17:06:54 -07:00
Mikayla Maki
c295f943ba Merge pull request #1799 from zed-industries/fix-project-panel-notify
Fix project panel not showing files / folders
2022-10-24 13:28:26 -07:00
Mikayla Maki
e527474dd9 removed dev file 2022-10-24 13:20:45 -07:00
Mikayla Maki
73f267167f Delete generate-db.rs 2022-10-24 13:19:30 -07:00
Mikayla Maki
40290a9a42 Added notify call 2022-10-24 13:18:02 -07:00
Max Brunsfeld
bd35468d18 Merge pull request #1785 from zed-industries/auto-deploy-collab
Automatically build collab server docker images based on git tags
2022-10-24 12:07:35 -07:00
Max Brunsfeld
c80395fc18 Merge branch 'main' into auto-deploy-collab 2022-10-24 12:01:32 -07:00
Max Brunsfeld
95be2c6070 Add version bump scripts 2022-10-24 08:58:14 -07:00
Antonio Scandurra
fb7a92242b Merge pull request #1793 from zed-industries/screen-sharing
Introduce screen-sharing
2022-10-24 16:53:05 +01:00
Nathan Sobo
8c2ff69515 Render a tooltip on toggle screen sharing button
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
2022-10-24 09:44:05 -06:00
Antonio Scandurra
011085a93f Revert "Temporarily upload app bundle as CI artifact"
This reverts commit 2b5ac535b9.
2022-10-24 17:36:19 +02:00
Antonio Scandurra
dce21900a7 Bump protocol version
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-10-24 17:06:40 +02:00
Antonio Scandurra
2b5ac535b9 Temporarily upload app bundle as CI artifact
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-10-24 17:04:33 +02:00
Antonio Scandurra
484c8f7cbe Provide LiveKit environment variables on Kubernetes
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-10-24 17:03:18 +02:00
Antonio Scandurra
7e4d582d1e Replace Screen Sharing label with Screen
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-10-24 16:50:56 +02:00
Antonio Scandurra
50c4783333 Add test for screen-sharing 2022-10-24 15:17:25 +02:00
Antonio Scandurra
9860dbbbea Set location on ActiveCall even before there's a room
We will automatically call `Room::set_location` once a room has been
assigned.
2022-10-24 15:07:25 +02:00
Antonio Scandurra
874a3605f8 Init submodules on CI 2022-10-24 14:28:58 +02:00
Antonio Scandurra
088c5bac1f Remove stray log statement 2022-10-24 11:02:41 +02:00
Antonio Scandurra
e135b982c1 Focus shared screen item when clicking on it 2022-10-24 11:02:10 +02:00
Antonio Scandurra
a8bd234aa4 Simplify room events 2022-10-24 10:53:44 +02:00
Antonio Scandurra
f99d70500c Allow opening shared screen via the contacts popover 2022-10-24 10:47:47 +02:00
Antonio Scandurra
476020ae84 Show shared screen as a pane item 2022-10-24 10:04:08 +02:00
Max Brunsfeld
2f1ddc0d0f Improve deploy scripts 2022-10-21 15:50:14 -07:00
Nate Butler
ef5844bc79 Merge pull request #1783 from zed-industries/add-new-internal-themes
Add new internal themes
2022-10-21 18:31:02 -04:00
Max Brunsfeld
0c9ceb51e6 Add what-is-deployed-script 2022-10-21 14:28:55 -07:00
Max Brunsfeld
cedc0f64d5 Run migrations via a collab subcommand 2022-10-21 14:28:55 -07:00
Max Brunsfeld
9952f08cce Publish collab docker images on CI, deploy pre-built images 2022-10-21 14:24:43 -07:00
Max Brunsfeld
efa6745035 Add more paths to dockerignore 2022-10-21 14:24:43 -07:00
Joseph T. Lyons
4816a587c3 Merge pull request #1781 from zed-industries/switch-to-mixpanel
Switch to mixpanel
2022-10-21 15:26:08 -04:00
Nate Butler
6514eb5209 Make the assets/themes folder if it doesn't exist
- Also only run clearThemes if the folder exists

Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2022-10-21 13:19:44 -04:00
Nate Butler
2a38c4938d Update gitignore because of macOS case sensitive weirdness
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2022-10-21 13:05:59 -04:00
Nate Butler
b015761131 WIP Re-case internal and experiment theme folders
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2022-10-21 13:04:24 -04:00
Nate Butler
99e6ecc466 Update Zenburn license 2022-10-21 13:03:54 -04:00
Antonio Scandurra
7e411ae098 Merge branch 'main' into screen-sharing
# Conflicts:
#	crates/collab/src/integration_tests.rs
#	crates/collab/src/main.rs
#	styles/src/styleTree/workspace.ts
2022-10-21 14:29:45 +02:00
Antonio Scandurra
1bbb7dd126 Leave Zed room when LiveKit room disconnects 2022-10-21 14:21:45 +02:00
Antonio Scandurra
78969d0938 Switch back to using the legacy screen capturing API
The new API is buggy and inconsistent, so I think we should move on
for now.
2022-10-21 11:54:52 +02:00
Antonio Scandurra
bac3dc1ccd Re-build live_kit_client when MACOSX_DEPLOYMENT_TARGET changes 2022-10-21 10:18:03 +02:00
Antonio Scandurra
ae44a38285 Remove unused LKDisplays API 2022-10-21 10:12:24 +02:00
Nathan Sobo
77b13b1356 Merge pull request #1788 from zed-industries/style
Apply a slight stylistic tweak
2022-10-20 18:43:50 -06:00
Nathan Sobo
2e97e2dbfd Apply a slight stylistic tweak 2022-10-20 18:38:27 -06:00
Kay Simmons
75ec5c3b1b Merge pull request #1784 from zed-industries/fix-keymap-panic
Fix panic in keymap parsing
2022-10-20 16:39:38 -07:00
K Simmons
3a456b09cb catch keymap string only modifiers and no key 2022-10-20 16:30:07 -07:00
Joseph T Lyons
022f70b1de Temporarily restore integration with Amplitude
This will be reverted later, once we fully switch to Mixpanel
2022-10-20 19:27:55 -04:00
Nate Butler
c1e23fc6d9 Update tokyo night meta 2022-10-20 18:22:34 -04:00
Nate Butler
a6e9d0d061 Merge branch 'main' into add-new-internal-themes 2022-10-20 18:19:21 -04:00
Nate Butler
b700ea84a5 Add metadata to all themes and organize
Co-Authored-By: gibusu <95764254+gibusu@users.noreply.github.com>
2022-10-20 17:08:09 -04:00
Nathan Sobo
0ef62fc334 Preserve symlinks in WebRTC.framework to avoid bundle signing failure 2022-10-20 14:37:04 -06:00
Nate Butler
c3900565b9 Fix a few incorrectly named themes
Co-Authored-By: gibusu <95764254+gibusu@users.noreply.github.com>
2022-10-20 16:28:08 -04:00
Nate Butler
a86756ed20 Update gruvbox to use manual accent ramps
Also updated it's neutral to contain more values sourced from the gruvbox repo

Co-Authored-By: gibusu <95764254+gibusu@users.noreply.github.com>
2022-10-20 16:25:54 -04:00
Nate Butler
e3ef6d35ab Add a range of new themes as internal themes for testing 2022-10-20 15:32:56 -04:00
Nate Butler
038670cc6f Add brush trees as a experimental theme 2022-10-20 15:32:40 -04:00
Nate Butler
5d87a04dc3 Remove old theme template 2022-10-20 15:32:20 -04:00
Nate Butler
fbfe8a2311 WIP Update theme metadata and add license information 2022-10-20 15:32:13 -04:00
Nate Butler
bd8509990a Rename One theme and update Zed default theme 2022-10-20 15:31:17 -04:00
Nathan Sobo
6bdb08ab9c Fix crash loading Swift symbol (I think associated with concurrency)
I add /usr/lib/swift as an rpath, which seems to fix the issue even though
there doesn't seem to be a relevant library at that location on my machine.

Based on my research, wondering if `-Wl,-weak-lswiftCompatibilityConcurrency`
is also required for this to work on older OSes, but holding back for now.
2022-10-20 13:18:53 -06:00
Antonio Scandurra
db8b8ef66b WIP 2022-10-20 20:17:54 +02:00
Joseph T Lyons
ac5d5e2451 Merge branch 'main' into switch-to-mixpanel 2022-10-20 13:53:39 -04:00
Max Brunsfeld
fad6cfef05 Merge pull request #1782 from zed-industries/idempotent-redemption
Return an optional response when creating users via invites
2022-10-20 10:46:50 -07:00
Nate Butler
c83cae60f6 Add Ayu to iternal themes 2022-10-20 13:28:50 -04:00
Antonio Scandurra
9b8e6cce02 WIP: Try using the new ScreenCaptureKit API when possible 2022-10-20 19:28:21 +02:00
Nathan Sobo
9858906463 Return an optional response when creating users via invites
If the user already exists, we return none. This will allow the web frontend
to avoid reporting a "join alpha" user event but also not error.

Co-Authored-By: Max Brunsfeld <max@zed.dev>
Co-Authored-By: Joseph Lyons <joseph@zed.dev>
2022-10-20 10:52:34 -06:00
Antonio Scandurra
be1dc01d9e Add 5s timeout to LiveKit API requests
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-10-20 18:01:47 +02:00
Antonio Scandurra
de24b4b4e8 Bump minimum macOS version to 10.15.7
This solves an issue with loading Swift libraries when running the
x86_64 binary.

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-10-20 18:01:41 +02:00
Antonio Scandurra
629d3d473c Copy WebRTC into Zed.app/Contents/Frameworks when bundling the app 2022-10-20 15:38:54 +02:00
Antonio Scandurra
5dc82d3df8 Delete all live-kit rooms when server is shut down 2022-10-20 14:34:05 +02:00
Antonio Scandurra
76a1b81e45 Update live-kit to the latest version 2022-10-20 14:03:26 +02:00
Antonio Scandurra
99aa1219d2 Simplify renderer interface for live-kit-client 2022-10-20 09:51:55 +02:00
Nathan Sobo
69472f7823 Ensure we can send a second frame 2022-10-19 19:21:09 -06:00
Nathan Sobo
723fa83909 Use fake LiveKit server to test we can send frames when screen sharing 2022-10-19 19:14:55 -06:00
Joseph T Lyons
2f064d5ccc Remove debug prints 2022-10-19 17:30:00 -04:00
Nate Butler
ae9a0a99ea Add new internal themes 2022-10-19 17:02:23 -04:00
Kay Simmons
c2b9b08944 Merge pull request #1665 from zed-industries/elevations
Tracking PR: Elevations
2022-10-19 13:59:34 -07:00
K Simmons
2aa2e5af7a fix issue with text component and adjust layer selections some more 2022-10-19 13:45:00 -07:00
K Simmons
b7c439f4c4 Fixup some theme inconsistencies and incorrect layer selections 2022-10-19 13:39:46 -07:00
Max Brunsfeld
e6b29086a9 Merge pull request #1777 from zed-industries/impersonate-via-secret-token
Impersonate via secret token
2022-10-19 13:32:40 -07:00
Max Brunsfeld
83e4e26989 Allow setting ZED_SERVER_URL to URL of a collab server 2022-10-19 13:27:14 -07:00
K Simmons
caec9c1f45 fixed issue in testbench 2022-10-19 13:13:50 -07:00
K Simmons
e3809c267d flattened layers and elevations 2022-10-19 13:02:51 -07:00
Nate Butler
0d9eecd2ed WIP command palette changes 2022-10-19 14:55:22 -04:00
Joseph T Lyons
d7915840d0 Switch to Mixpanel analytics
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
2022-10-19 14:53:48 -04:00
Mikayla Maki
8098697847 Re-removed chat panel 2022-10-19 11:45:20 -07:00
Mikayla Maki
4c2f8406c7 Restored chat_panel, just in case 2022-10-19 11:42:29 -07:00
Nate Butler
e0a477265d Use lab color interpolation to improve the dark end of accent ramps 2022-10-19 14:35:09 -04:00
Nate Butler
364c3f2f00 Contrast rebalances 2022-10-19 13:03:58 -04:00
Nate Butler
75c79d60fe Improve contrast/scanability of constants 2022-10-19 13:03:34 -04:00
Nate Butler
5b2dd8e4d0 build-themes -> build to fix building themes on save 2022-10-19 13:03:09 -04:00
Nate Butler
9e8e227b46 Rebalance rose-pine-dawn 2022-10-19 13:02:34 -04:00
Julia
adf7578007 Merge pull request #1778 from zed-industries/trackpad-scroll-snap-lock
Lock trackpad scrolling in buffers to axis until broken free
2022-10-19 12:02:59 -04:00
Antonio Scandurra
b6e5aa3bb0 Use live_kit_client::TestServer in integration tests
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-10-19 16:35:34 +02:00
Antonio Scandurra
288c039929 Start on implementing a fake live-kit server 2022-10-19 14:58:50 +02:00
Antonio Scandurra
fb5c6493cf WIP: Start on a fake implementation of live-kit 2022-10-19 13:53:40 +02:00
Antonio Scandurra
3160d07b9c Model pending screen share in Room 2022-10-19 11:38:24 +02:00
Antonio Scandurra
e49fc9f4b1 Prevent Room from screen-sharing twice 2022-10-19 10:45:51 +02:00
Antonio Scandurra
ed6f482e68 Exercise unpublish_track in live_kit_client 2022-10-19 10:39:48 +02:00
Antonio Scandurra
773f569385 Add control to toggle screen-sharing 2022-10-19 10:19:20 +02:00
Antonio Scandurra
219793afcc Merge remote-tracking branch 'origin/main' into screen-sharing 2022-10-19 10:04:56 +02:00
Mikayla Maki
571636c526 Fixed cursor color being black 2022-10-18 22:26:14 -07:00
Julia
cbc15b6b58 Lock trackpad scrolling in buffers to axis until broken free 2022-10-19 01:00:13 -04:00
Max Brunsfeld
c410935c9c Allow impersonating users via the api token, bypassing oauth 2022-10-18 17:36:54 -07:00
K Simmons
79cf5dbd4b remove rocksdb 2022-10-18 17:21:15 -07:00
Kay Simmons
da5203011c Merge pull request #1773 from zed-industries/rusqlite
Swap to sqlite for client persistence
2022-10-18 16:11:54 -07:00
Mikayla Maki
84c7aa9cad Finished up initial sqlite implemention
Co-Authored-By: kay@zed.dev
2022-10-18 15:58:05 -07:00
Nathan Sobo
f8e5a08324 Merge pull request #1764 from zed-industries/gpui-events
Eliminate dispatch_event on Element trait
2022-10-18 15:24:13 -06:00
Max Brunsfeld
5e57a33df7 Store entire Config struct on collab AppState 2022-10-18 13:58:03 -07:00
Max Brunsfeld
38bdf7ad92 Remove unused env vars from collab k8s manifest 2022-10-18 13:58:03 -07:00
Max Brunsfeld
5447f63e9d Fix error in changes-since-last-release script on PRs with no body 2022-10-18 13:12:27 -07:00
Max Brunsfeld
50ba8bdc9b 0.61.0 2022-10-18 13:05:16 -07:00
Max Brunsfeld
6f279c0239 Merge pull request #1776 from zed-industries/tabbar-scroll
Scroll horizontal flex lists by whichever scroll delta dimension is g…
2022-10-18 13:04:28 -07:00
Max Brunsfeld
26ccd70e77 Scroll horizontal flex lists by whichever scroll delta dimension is greater 2022-10-18 12:59:04 -07:00
K Simmons
b0ddbeb0ad Merge branch 'main' into elevations 2022-10-18 12:47:15 -07:00
Julia
826eb113e7 Merge pull request #1775 from zed-industries/drag-on-context-menu-still-click
Don't allow drag event to fall through context menu
2022-10-18 15:24:38 -04:00
Julia
2661a9cc98 Don't allow drag event to fall through context menu 2022-10-18 15:00:49 -04:00
K Simmons
b06366ebb7 Get rusqlite more shippable 2022-10-18 11:43:18 -07:00
Antonio Scandurra
c7a629ba6b Merge pull request #1774 from zed-industries/stale-connections
Correctly handle disconnect when a different client for the same user is on a call
2022-10-18 18:37:43 +01:00
Antonio Scandurra
d155c11729 Fix client unit test by sending Hello in FakeServer 2022-10-18 19:33:38 +02:00
Antonio Scandurra
0c3c1e1f68 WIP 2022-10-18 19:30:45 +02:00
Antonio Scandurra
6c322dc835 Clear out incoming call when removing last connection for a user
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-10-18 19:15:14 +02:00
K Simmons
6019e4c37b remove items migration 2022-10-18 10:13:47 -07:00
K Simmons
9c8dd66b20 dont reference db items 2022-10-18 10:13:04 -07:00
Antonio Scandurra
0c0e8688ed Use PeerId in TestServer::disconnect_client
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-10-18 19:05:37 +02:00
Antonio Scandurra
6146923dbb WIP: Start on test to ensure incoming calls cancel upon recipient disconnection
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-10-18 18:45:50 +02:00
Antonio Scandurra
2c4f003897 Tell clients their peer id on connection in Hello message
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-10-18 18:42:55 +02:00
Antonio Scandurra
0491747eed Only leave room on connections that are associated with the active call
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-10-18 17:42:10 +02:00
Antonio Scandurra
29b9651ebd Use CFRelease instead of a custom LKRelease 2022-10-18 15:47:56 +02:00
Antonio Scandurra
48a1dd1588 Delete room when no participants are left 2022-10-18 14:59:12 +02:00
Antonio Scandurra
9cf39b1da6 Disconnect from live-kit Room on drop 2022-10-18 14:50:03 +02:00
Antonio Scandurra
47be340cac Fix invoking RemoveParticipant on live-kit server 2022-10-18 14:35:06 +02:00
Antonio Scandurra
bf98300547 Render remote participant's screen preserving aspect ratio 2022-10-18 14:16:19 +02:00
Antonio Scandurra
46635956f4 Emit Frame event when new frames are generated for a remote track 2022-10-18 12:18:49 +02:00
Antonio Scandurra
8c6de99159 Use participant identity and track sid everywhere 2022-10-18 12:05:59 +02:00
Nathan Sobo
a42a703b35 Pass tracks to Rust unretained
We always call CFRetain when constructing a track on the Rust side.
2022-10-17 23:56:41 -06:00
Nathan Sobo
59fab0bb2d WIP 2022-10-17 23:47:55 -06:00
Nathan Sobo
c73e2c2d0f Get test_app running without crashing 2022-10-17 23:38:43 -06:00
Nathan Sobo
8c1c98a0bf WIP 2022-10-17 23:25:04 -06:00
K Simmons
d99a074bc0 revert workspace changes 2022-10-17 17:05:08 -07:00
K Simmons
05b4b443d9 working items schema 2022-10-17 17:04:30 -07:00
Mikayla Maki
4b09f77950 WIP 2022-10-17 17:04:30 -07:00
Mikayla Maki
dbea3cf20c Converted to using rusqlite 2022-10-17 17:04:30 -07:00
K Simmons
aa8fa4a6d5 more wip 2022-10-17 17:04:29 -07:00
K Simmons
dbc03e2668 wip 2022-10-17 17:04:19 -07:00
Mikayla Maki
4ef69c8361 Merge pull request #1769 from zed-industries/breadcrumbs
Fix breadcrumbs
2022-10-17 17:02:56 -07:00
Mikayla Maki
895aeb033f Merge branch 'main' into breadcrumbs 2022-10-17 16:51:38 -07:00
Kay Simmons
e15cc376b0 Merge pull request #1763 from zed-industries/cursor-blink-setting
Adds the ability to disable cursor blinking and replicates cursor shape to collaborators
2022-10-17 16:51:20 -07:00
K Simmons
54428ca6f6 swap to using vercel to run the local zed.dev server 2022-10-17 16:49:34 -07:00
K Simmons
54cf6fa838 Pull blink functionality out of editor and into blink manager. Make blink manager subscribe to settings changes in order to start blinking properly when it is re-enabled.
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-10-17 16:20:51 -07:00
K Simmons
09a0b3eb55 increment protocol version 2022-10-17 16:20:51 -07:00
K Simmons
40c3e925ad Add cursor blink setting and replicate cursor shape to remote collaborators 2022-10-17 16:20:47 -07:00
Mikayla Maki
5ef5147780 Merge branch 'main' into gpui-events 2022-10-17 15:43:41 -07:00
Mikayla Maki
318b923bac Merge pull request #1765 from zed-industries/fix-terminal-hyperlinks
Open hyperlinks on up, not down, and disable them when dragging.
2022-10-17 15:41:40 -07:00
Mikayla Maki
93a30ea940 Removed breadcrumb scrollable 2022-10-17 15:29:51 -07:00
Mikayla Maki
5bb2edca8b Added absolute path info to remote worktrees (updated protocol version) 2022-10-17 15:27:46 -07:00
Mikayla Maki
1789dfb8b1 Fixed tests 2022-10-17 14:53:52 -07:00
Mikayla Maki
f473eadf2d Fixed failing test, now to make breadcrumbs scrollable... 2022-10-17 13:57:29 -07:00
Mikayla Maki
1f161b9aa1 Show full, absolute paths when displaying a local worktree 2022-10-17 13:35:45 -07:00
Mikayla Maki
354fefe61b Resovled behavioral inconsistency with how projects with multiple roots are handled 2022-10-17 13:08:05 -07:00
Mikayla Maki
19c98bb5ad fixed a bug where files outside of the project would show 'untitled' in the search bar 2022-10-17 12:58:48 -07:00
Julia
2149c17a0a Merge pull request #1768 from zed-industries/git-gutter-meets-code-folding
Git gutter meets code folding (and word wrap fixes)
2022-10-17 14:51:47 -04:00
Julia
1716aff969 Cleanup 2022-10-17 14:41:16 -04:00
Julia
2a5d7ea2de Inclusively check for hunk in fold range 2022-10-17 13:11:11 -04:00
Julia
be34c50c72 Deduplicate identical hunk layouts 2022-10-17 12:41:20 -04:00
Julia
50ae3e03f7 More concrete usage of display map to handle diff hunk gutter layout 2022-10-17 12:28:44 -04:00
Antonio Scandurra
499b8f5f55 WIP 2022-10-17 18:00:54 +02:00
Antonio Scandurra
81d83841ab WIP: Start integrating screen-sharing 2022-10-17 14:50:05 +02:00
Antonio Scandurra
cce00526b9 Remove participants from live-kit rooms when they leave zed rooms 2022-10-17 14:03:44 +02:00
Antonio Scandurra
c9225bb87c WIP: Start integrating with LiveKit when creating/joining rooms 2022-10-17 12:20:55 +02:00
Antonio Scandurra
75c339851f Add live_kit_server::api::Client::{create,delete}_room 2022-10-17 11:24:09 +02:00
Antonio Scandurra
e39c7c62e4 Update livekit_client 2022-10-17 10:48:09 +02:00
Antonio Scandurra
b6bb2985f5 Merge pull request #1767 from zed-industries/notify-on-auto-update
Notify `ActivityIndicator` when `AutoUpdater` changes
2022-10-17 09:10:57 +01:00
Antonio Scandurra
6bdbab2faf Notify ActivityIndicator when AutoUpdater changes
This fixes a bug that caused the status bar to not update when the
auto-update system changed its status.
2022-10-17 10:05:38 +02:00
Antonio Scandurra
f09d6b7b95 WIP 2022-10-17 09:59:16 +02:00
Nathan Sobo
19a2752674 WIP: Update token module to support server api 2022-10-17 09:59:16 +02:00
Antonio Scandurra
5d433b1666 WIP: start on live_kit_server 2022-10-17 09:59:16 +02:00
Antonio Scandurra
caeae38e3a Move live_kit to live_kit_client and add live_kit_server 2022-10-17 09:59:16 +02:00
Antonio Scandurra
c25acc155d Move ownership of MacOSDisplay to the rust side 2022-10-17 09:59:16 +02:00
Antonio Scandurra
4222f86537 Temporarily use legacy screen capture API 2022-10-17 09:59:16 +02:00
Nathan Sobo
9569323f93 WIP: Getting a big black window, then a crash 2022-10-17 09:59:16 +02:00
Nathan Sobo
0bbba90f30 Use ScreenCaptureKit-enabled LiveKit SDK and add display_sources function
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
2022-10-17 09:59:16 +02:00
Mikayla Maki
f1ff557a25 Rearranged mouse handling 2022-10-16 17:31:19 -07:00
Joseph T. Lyons
23d7143298 Merge pull request #1666 from zed-industries/settings-for-journal
Settings for journal
2022-10-16 19:55:27 -04:00
Nathan Sobo
12eab6551f Remove dispatch_event from Element trait 2022-10-16 13:08:25 -06:00
Nathan Sobo
d25c6b15a6 Move Terminal key down event handling from element to View::key_down method 2022-10-16 12:55:02 -06:00
Nathan Sobo
b9308ad80d Move handling of modifier changes to new View hook 2022-10-16 12:47:48 -06:00
Nathan Sobo
6e363e464c Start on view-level dispatch approach for keyboard events 2022-10-16 11:46:31 -06:00
Nathan Sobo
6e53deb1b2 Refine mouse event naming 2022-10-16 11:18:58 -06:00
Joseph T Lyons
0717c168d9 Derive Serialize on HourFormat 2022-10-16 12:51:48 -04:00
Joseph T Lyons
6d020a3ee9 Do not derive Default on JournalSettings 2022-10-16 12:51:34 -04:00
Joseph T Lyons
9a381c1803 Merge branch 'main' into settings-for-journal 2022-10-16 12:42:18 -04:00
Nathan Sobo
3e23d1f48d Merge pull request #1762 from zed-industries/less-click-and-hover-invalidation
Reduce unnecessary view invalidations related to mouse events
2022-10-16 10:23:54 -06:00
Nathan Sobo
1750fcf833 Merge pull request #1761 from zed-industries/mouse-region-view-invalidation
Remove unconditional invalidation when calling mouse region handlers
2022-10-14 18:31:23 -06:00
Nathan Sobo
646d344a11 Avoid re-rendering editor on mouse move
Only notify editor when clearing highlights if there were highlights to
begin with.

Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-10-14 18:27:55 -06:00
Nathan Sobo
bc03592912 Only invalidate parent view on click/hover if we read that state when rendering
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-10-14 18:09:15 -06:00
Max Brunsfeld
a4b518ec72 Merge pull request #1760 from zed-industries/invite-unknown-platform
Include waitlist entries w/ unknown platform when summarizing and sending invites
2022-10-14 16:24:48 -07:00
Max Brunsfeld
b541ac313c Revert unnecessary logic for fetching invites' platform_unknown flag 2022-10-14 16:13:38 -07:00
Nathan Sobo
934474f87e Remove unconditional invalidation when calling mouse region handlers
We want invalidation to opt-in as much as possible.
If you want a view to re-render, you need to call `cx.notify`.
2022-10-14 17:06:46 -06:00
Max Brunsfeld
3a4e802093 Include waitlist entries w/ unknown platform when summarizing and sending invites 2022-10-14 15:20:23 -07:00
Julia
b3eb5f7cdf WIP
Co-Authored-By: Kay Simmons <kay@zed.dev>
2022-10-14 17:14:33 -04:00
Mikayla Maki
c21e0e916c Merge pull request #1759 from zed-industries/move-page-up-down
Move page up / down
2022-10-14 14:02:27 -07:00
Mikayla Maki
d301a215f7 Finished implementing vscode, emacs, and mac style pageup/down. Added keybindings ctrl-v, alt-v for emacs up/down and shift-pageup, shift-pagedown for vscode style. Also improved incorporated pageup/down into context menus 2022-10-14 13:52:30 -07:00
Max Brunsfeld
8044beffc7 v0.60.4 2022-10-14 12:44:22 -07:00
Max Brunsfeld
8df84e0341 Add MovePageUp and MovePageDown editor commands
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2022-10-14 12:36:46 -07:00
Max Brunsfeld
137a9cefbd Enable auto-scroll when moving cursors in Editor::handle_input
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2022-10-14 11:32:22 -07:00
Max Brunsfeld
55576f879b Merge pull request #1758 from zed-industries/editor-paint-panic
Consolidate calculation of editor's visible row range
2022-10-14 10:47:16 -07:00
Max Brunsfeld
78aee53411 Merge pull request #1757 from zed-industries/detect-unshare
Clear project's shared state upon every disconnection
2022-10-14 10:44:35 -07:00
Max Brunsfeld
864020463f Consolidate calculation of editor's visible row range
We think this will fix a panic that was occuring in `paint_highlighted_range`
due to an out-of-bounds read into the line layouts. We think doing essentially the same
calculation in two different ways with floating point numbers might have
caused a different end row to be calculated in 2 different code paths.

Co-authored-by: Nathan Sobo <nathan@zed.dev>
2022-10-14 10:37:44 -07:00
Max Brunsfeld
2d3d07d4d7 Clear project's shared state upon every disconnection
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Antonio Scandurra <as-cii@zed.dev>
2022-10-14 10:17:59 -07:00
Max Brunsfeld
ad6f9b2499 0.60.3 2022-10-14 09:35:57 -07:00
Max Brunsfeld
330968434f Merge pull request #1756 from zed-industries/autoclose-wrong-closing-bracket
Avoid skipping over a different closing bracket in autoclose
2022-10-14 09:34:33 -07:00
Max Brunsfeld
4b12fb6b3b Avoid skipping over a different closing bracket in autoclose 2022-10-14 09:30:30 -07:00
Nathan Sobo
eef086f60f 0.60.2 2022-10-13 16:26:26 -06:00
Nathan Sobo
6ac0b81778 Merge pull request #1754 from zed-industries/fix-list-scroll
Pass the current view id when painting List's mouse region instead of 10
2022-10-13 16:24:55 -06:00
Nathan Sobo
8d82702da2 Pass the current view id value when painting List's mouse region
Previously, a dummy value was being passed. I think this slipped in accidentally.
2022-10-13 15:57:19 -06:00
Julia
dde3dfdbf6 Quick cut of using display point conversion to layout hunks
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-10-13 16:34:34 -04:00
Julia
8d609959f1 Clean 2022-10-13 15:23:41 -04:00
Julia
16f854b636 Expand diff gutter indicator to cover all of a wrapped line 2022-10-13 14:05:57 -04:00
Julia
9c47325c25 Use correct range to get diff hunks in the presence of wrapped lines 2022-10-13 13:52:44 -04:00
Julia
a6a7e85894 Misc fixes, still broken soft wrap 2022-10-13 02:02:29 -04:00
Julia
e75dcc853b Include deletion hunks in fold regardless of end 2022-10-13 00:42:53 -04:00
Julia
e744520d90 Correctly offset diff hunk layouts 2022-10-12 16:40:19 -04:00
Julia
a6910584b6 Something's happening, nothing correct, but something 2022-10-12 00:39:56 -04:00
Nate Butler
6dfa34fcf8 Remove a few Zed default themes
Co-Authored-By: Kay Simmons <3323631+Kethku@users.noreply.github.com>
2022-10-11 17:40:45 -04:00
Nate Butler
b626ec3bf9 Use different dark and light ramps for cave
Co-Authored-By: Kay Simmons <3323631+Kethku@users.noreply.github.com>
2022-10-11 17:39:38 -04:00
Nate Butler
5708879b5a Style elevations & update styleTrees
Also rename `info` -> `accent`

Co-Authored-By: Kay Simmons <3323631+Kethku@users.noreply.github.com>
2022-10-11 17:38:28 -04:00
Nate Butler
95bc18a995 Fix color ramps to use colored fg 2022-10-10 17:50:41 -04:00
Nate Butler
61dc703a58 Improve feedback button hover state 2022-10-10 17:42:23 -04:00
Nate Butler
a87d9d3578 Make code actions/autocomplete match contextMenu style 2022-10-10 17:35:12 -04:00
Nate Butler
fc770c6ea5 Merge pull request #1713 from zed-industries/elevations-dynamic-layers
(Elevations) Dynamic StyleSets
2022-10-10 16:59:22 -04:00
Nate Butler
0c68abbe17 Revert tab bar to pre-elevation style 2022-10-10 16:53:38 -04:00
Nate Butler
2d25e25ec3 WIP + Format 2022-10-09 19:43:06 -04:00
Nate Butler
c4028ef116 Calculate styles dynamically 2022-10-09 16:11:02 -04:00
Nate Butler
393d728769 wip 2022-10-09 15:27:39 -04:00
Nate Butler
431ac1267a Update contextMenu.ts 2022-10-06 21:08:53 -04:00
Nate Butler
5bc074005c WIP 2022-10-05 12:40:38 -04:00
Nate Butler
4a61b1011e Minor one dark improvements
Co-Authored-By: gibusu <95764254+gibusu@users.noreply.github.com>
2022-10-04 14:53:06 -04:00
Nate Butler
84847ff181 Remap theme ramp domains
Co-Authored-By: gibusu <95764254+gibusu@users.noreply.github.com>
2022-10-04 14:49:57 -04:00
Nate Butler
0bbc02e10d Add bottom padding and spacing between items to pickers
Co-Authored-By: gibusu <95764254+gibusu@users.noreply.github.com>
2022-10-04 13:55:01 -04:00
Nate Butler
0ed811b81b Update palettes 2022-10-04 13:16:11 -04:00
Nate Butler
ce2112df43 Update offline indicator 2022-10-04 12:36:31 -04:00
Nate Butler
7080dc9c23 WIP 2022-10-03 14:08:01 -04:00
Joseph T Lyons
3c62de34f7 Change journal location setting name to "path" and default to ~ 2022-09-29 17:12:57 -04:00
Nate Butler
a6cccf82f7 Fix illegible rename text 2022-09-29 13:28:31 -04:00
Joseph T Lyons
f8da5ab2e7 Remove "get" prefix from function names 2022-09-28 17:07:11 -04:00
Joseph T Lyons
fbe5f9225c Add descriptions to journal settings 2022-09-28 16:52:15 -04:00
Joseph T Lyons
773423fcf4 Initial work to add settings to journal feature 2022-09-28 16:25:37 -04:00
Nate Butler
a62e2a38d7 Update projectPanel.ts 2022-09-28 16:04:15 -04:00
Nate Butler
48dcc465f2 WIP 2022-09-28 16:03:00 -04:00
Nate Butler
d0c50b4fbf Style tab bar 2022-09-28 15:53:06 -04:00
Nate Butler
ab3a6f775e WIP Titlebar styling 2022-09-28 13:01:12 -04:00
Nate Butler
0674ca14d9 Update the neutral ramp for Andromeda 2022-09-27 12:49:51 -04:00
Nate Butler
d0b35b5e19 WIP Update style trees 2022-09-26 22:51:00 -04:00
Nate Butler
01570504ad WIP Allow applying domains to theme ramps
Co-Authored-By: Kay Simmons <3323631+Kethku@users.noreply.github.com>
2022-09-26 17:41:59 -04:00
Nate Butler
506c28d2b6 Fix incorrect import 2022-09-26 15:39:21 -04:00
Nate Butler
53f58f72f2 Add zed-pro as an internal theme 2022-09-26 15:15:35 -04:00
Nate Butler
c9786fe464 Create a baseline for ramps to start tweaking from 2022-09-26 15:07:24 -04:00
Nate Butler
c2ffc7086c Minor styletree fixes 2022-09-26 15:07:11 -04:00
K Simmons
96f9ee784d add more states to the theme testbench 2022-09-22 14:25:15 -07:00
K Simmons
962f087ac2 promote variant to its own styleset 2022-09-22 13:29:19 -07:00
Nate Butler
ebe8c952e4 WIP work on bottom and middle layer sets 2022-09-22 12:08:53 -04:00
K Simmons
eabd687cbc More tweaks and add variant to theme testbench 2022-09-21 16:59:33 -07:00
K Simmons
593c7a8cd1 fix rebase error 2022-09-21 16:35:24 -07:00
K Simmons
79b9420017 minor tweaks 2022-09-21 16:32:44 -07:00
K Simmons
db5c83eb36 add theme testbench command 2022-09-21 16:32:44 -07:00
K Simmons
56f9543a95 reworked style tree to use colorScheme instead of old theme. Very limited style for now 2022-09-21 16:32:42 -07:00
310 changed files with 12651 additions and 7095 deletions

View File

@@ -1,3 +1,11 @@
/target
/manifest.yml
/migrate.yml
**/target
zed.xcworkspace
.DS_Store
plugins/bin
script/node_modules
styles/node_modules
crates/collab/static/styles.css
vendor/bin
assets/themes/*.json
assets/themes/internal/*.json
assets/themes/experiments/*.json

View File

@@ -39,6 +39,7 @@ jobs:
uses: actions/checkout@v2
with:
clean: false
submodules: 'recursive'
- name: Run tests
run: cargo test --workspace --no-fail-fast
@@ -57,6 +58,7 @@ jobs:
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
ZED_AMPLITUDE_API_KEY: ${{ secrets.ZED_AMPLITUDE_API_KEY }}
ZED_MIXPANEL_TOKEN: ${{ secrets.ZED_MIXPANEL_TOKEN }}
steps:
- name: Install Rust
run: |
@@ -75,10 +77,32 @@ jobs:
uses: actions/checkout@v2
with:
clean: false
submodules: 'recursive'
- name: Validate version
- name: Determine version and release channel
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: script/validate-version
run: |
set -eu
version=$(script/get-crate-version zed)
channel=$(cat crates/zed/RELEASE_CHANNEL)
echo "Publishing version: ${version} on release channel ${channel}"
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
expected_tag_name=""
case ${channel} in
stable)
expected_tag_name="v${version}";;
preview)
expected_tag_name="v${version}-pre";;
*)
echo "can't publish a release on channel ${channel}"
exit 1;;
esac
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
exit 1
fi
- name: Create app bundle
run: script/bundle
@@ -91,12 +115,12 @@ jobs:
path: target/release/Zed.dmg
- uses: softprops/action-gh-release@v1
name: Upload app bundle to release if release tag
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
name: Upload app bundle to release
if: ${{ env.RELEASE_CHANNEL }}
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
files: target/release/Zed.dmg
overwrite: true
body: ""
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,46 @@
name: Publish Collab Server Image
on:
push:
tags:
- collab-v*
env:
DOCKER_BUILDKIT: 1
DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
jobs:
publish:
name: Publish collab server image
runs-on:
- self-hosted
- deploy
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Sign into DigitalOcean docker registry
run: doctl registry login
- name: Checkout repo
uses: actions/checkout@v3
with:
clean: false
submodules: 'recursive'
- name: Determine version
run: |
set -eu
version=$(script/get-crate-version collab)
if [[ $GITHUB_REF_NAME != "collab-v${version}" ]]; then
echo "release tag ${GITHUB_REF_NAME} does not match version ${version}"
exit 1
fi
echo "Publishing collab version: ${version}"
echo "COLLAB_VERSION=${version}" >> $GITHUB_ENV
- name: Build docker image
run: docker build . --tag registry.digitalocean.com/zed/collab:v${COLLAB_VERSION}
- name: Publish docker image
run: docker push registry.digitalocean.com/zed/collab:v${COLLAB_VERSION}

View File

@@ -30,4 +30,4 @@ jobs:
architecture: "x64"
cache: "pip"
- run: pip install -r script/amplitude_release/requirements.txt
- run: python script/amplitude_release/main.py ${{ github.event.release.tag_name }} ${{ secrets.ZED_AMPLITUDE_API_KEY }} ${{ secrets.ZED_AMPLITUDE_SECRET_KEY }}
- run: python script/amplitude_release/main.py ${{ github.event.release.tag_name }} ${{ secrets.ZED_AMPLITUDE_API_KEY }} ${{ secrets.ZED_AMPLITUDE_SECRET_KEY }}

14
.gitignore vendored
View File

@@ -7,6 +7,14 @@
/crates/collab/static/styles.css
/vendor/bin
/assets/themes/*.json
/assets/themes/internal/*.json
/assets/themes/experiments/*.json
**/venv
/assets/themes/Internal/*.json
/assets/themes/Experiments/*.json
**/venv
.build
Packages
*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "crates/live_kit_server/protocol"]
path = crates/live_kit_server/protocol
url = https://github.com/livekit/protocol

335
Cargo.lock generated
View File

@@ -172,13 +172,13 @@ dependencies = [
[[package]]
name = "async-broadcast"
version = "0.3.4"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b"
checksum = "6d26004fe83b2d1cd3a97609b21e39f9a31535822210fe83205d2ce48866ea61"
dependencies = [
"easy-parallel",
"event-listener",
"futures-core",
"parking_lot 0.12.1",
]
[[package]]
@@ -705,17 +705,6 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
[[package]]
name = "bzip2-sys"
version = "0.1.11+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "cache-padded"
version = "1.2.0"
@@ -727,10 +716,13 @@ name = "call"
version = "0.1.0"
dependencies = [
"anyhow",
"async-broadcast",
"client",
"collections",
"futures 0.3.24",
"gpui",
"live_kit_client",
"media",
"postage",
"project",
"util",
@@ -802,34 +794,6 @@ dependencies = [
"winx",
]
[[package]]
name = "capture"
version = "0.1.0"
dependencies = [
"anyhow",
"bindgen",
"block",
"byteorder",
"bytes 1.2.1",
"cocoa",
"core-foundation",
"core-graphics",
"foreign-types",
"futures 0.3.24",
"gpui",
"hmac 0.12.1",
"jwt",
"live_kit",
"log",
"media",
"objc",
"parking_lot 0.11.2",
"postage",
"serde",
"sha2 0.10.6",
"simplelog",
]
[[package]]
name = "castaway"
version = "0.1.2"
@@ -866,22 +830,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chat_panel"
version = "0.1.0"
dependencies = [
"client",
"editor",
"gpui",
"menu",
"postage",
"settings",
"theme",
"time 0.3.15",
"util",
"workspace",
]
[[package]]
name = "chrono"
version = "0.4.22"
@@ -1011,6 +959,7 @@ dependencies = [
"rand 0.8.5",
"rpc",
"serde",
"settings",
"smol",
"sum_tree",
"tempfile",
@@ -1079,7 +1028,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"anyhow",
"async-trait",
@@ -1103,6 +1052,8 @@ dependencies = [
"language",
"lazy_static",
"lipsum",
"live_kit_client",
"live_kit_server",
"log",
"lsp",
"nanoid",
@@ -1543,9 +1494,9 @@ dependencies = [
[[package]]
name = "cxx"
version = "1.0.78"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f39818dcfc97d45b03953c1292efc4e80954e1583c4aa770bac1383e2310a4"
checksum = "3f83d0ebf42c6eafb8d7c52f7e5f2d3003b89c7aa4fd2b79229209459a849af8"
dependencies = [
"cc",
"cxxbridge-flags",
@@ -1555,9 +1506,9 @@ dependencies = [
[[package]]
name = "cxx-build"
version = "1.0.78"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e580d70777c116df50c390d1211993f62d40302881e54d4b79727acb83d0199"
checksum = "07d050484b55975889284352b0ffc2ecbda25c0c55978017c132b29ba0818a86"
dependencies = [
"cc",
"codespan-reporting",
@@ -1570,15 +1521,15 @@ dependencies = [
[[package]]
name = "cxxbridge-flags"
version = "1.0.78"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56a46460b88d1cec95112c8c363f0e2c39afdb237f60583b0b36343bf627ea9c"
checksum = "99d2199b00553eda8012dfec8d3b1c75fce747cf27c169a270b3b99e3448ab78"
[[package]]
name = "cxxbridge-macro"
version = "1.0.78"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "747b608fecf06b0d72d440f27acc99288207324b793be2c17991839f3d4995ea"
checksum = "dcb67a6de1f602736dd7eaead0080cf3435df806c61b24b13328db128c58868f"
dependencies = [
"proc-macro2",
"quote",
@@ -1602,8 +1553,13 @@ dependencies = [
"async-trait",
"collections",
"gpui",
"lazy_static",
"log",
"parking_lot 0.11.2",
"rocksdb",
"rusqlite",
"rusqlite_migration",
"serde",
"serde_rusqlite",
"tempdir",
]
@@ -1735,12 +1691,9 @@ dependencies = [
[[package]]
name = "dotenvy"
version = "0.15.5"
version = "0.15.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9155c8f4dc55c7470ae9da3f63c6785245093b3f6aeb0f5bf2e968efbba314"
dependencies = [
"dirs 4.0.0",
]
checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0"
[[package]]
name = "drag_and_drop"
@@ -1784,6 +1737,7 @@ dependencies = [
"collections",
"context_menu",
"ctor",
"drag_and_drop",
"env_logger",
"futures 0.3.24",
"fuzzy",
@@ -1926,6 +1880,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fastrand"
version = "1.8.0"
@@ -2730,9 +2690,9 @@ dependencies = [
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.0"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde6edd6cef363e9359ed3c98ba64590ba9eecba2293eb5a723ab32aee8926aa"
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
dependencies = [
"cxx",
"cxx-build",
@@ -2946,6 +2906,8 @@ dependencies = [
"editor",
"gpui",
"log",
"settings",
"shellexpand",
"util",
"workspace",
]
@@ -3119,17 +3081,14 @@ dependencies = [
]
[[package]]
name = "librocksdb-sys"
version = "0.7.1+7.3.1"
source = "git+https://github.com/rust-rocksdb/rust-rocksdb?rev=39dc822dde743b2a26eb160b660e8fbdab079d49#39dc822dde743b2a26eb160b660e8fbdab079d49"
name = "libsqlite3-sys"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f0455f2c1bc9a7caa792907026e469c1d91761fb0ea37cbb16427c77280cf35"
dependencies = [
"bindgen",
"bzip2-sys",
"cc",
"glob",
"libc",
"libz-sys",
"zstd-sys",
"pkg-config",
"vcpkg",
]
[[package]]
@@ -3185,17 +3144,54 @@ dependencies = [
]
[[package]]
name = "live_kit"
name = "live_kit_client"
version = "0.1.0"
dependencies = [
"anyhow",
"async-broadcast",
"async-trait",
"block",
"byteorder",
"bytes 1.2.1",
"cocoa",
"collections",
"core-foundation",
"core-graphics",
"foreign-types",
"futures 0.3.24",
"gpui",
"hmac 0.12.1",
"jwt",
"lazy_static",
"live_kit_server",
"log",
"media",
"nanoid",
"objc",
"parking_lot 0.11.2",
"postage",
"serde",
"serde_json",
"sha2 0.10.6",
"simplelog",
]
[[package]]
name = "live_kit_server"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"futures 0.3.24",
"hmac 0.12.1",
"jwt",
"log",
"prost 0.8.0",
"prost-build",
"prost-types 0.8.0",
"reqwest",
"serde",
"sha2 0.10.6",
]
[[package]]
@@ -3450,7 +3446,7 @@ dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys",
"windows-sys 0.36.1",
]
[[package]]
@@ -3879,7 +3875,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core 0.9.3",
"parking_lot_core 0.9.4",
]
[[package]]
@@ -3898,15 +3894,15 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.9.3"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"smallvec",
"windows-sys",
"windows-sys 0.42.0",
]
[[package]]
@@ -4201,9 +4197,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.46"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
dependencies = [
"unicode-ident",
]
@@ -4246,7 +4242,6 @@ dependencies = [
"pulldown-cmark",
"rand 0.8.5",
"regex",
"rocksdb",
"rpc",
"serde",
"serde_json",
@@ -4353,7 +4348,7 @@ dependencies = [
"multimap",
"petgraph",
"prost 0.9.0",
"prost-types",
"prost-types 0.9.0",
"regex",
"tempfile",
"which",
@@ -4385,6 +4380,16 @@ dependencies = [
"syn",
]
[[package]]
name = "prost-types"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "603bbd6394701d13f3f25aada59c7de9d35a6a5887cfc156181234a44002771b"
dependencies = [
"bytes 1.2.1",
"prost 0.8.0",
]
[[package]]
name = "prost-types"
version = "0.9.0"
@@ -4745,15 +4750,6 @@ dependencies = [
"rmp",
]
[[package]]
name = "rocksdb"
version = "0.18.0"
source = "git+https://github.com/rust-rocksdb/rust-rocksdb?rev=39dc822dde743b2a26eb160b660e8fbdab079d49#39dc822dde743b2a26eb160b660e8fbdab079d49"
dependencies = [
"libc",
"librocksdb-sys",
]
[[package]]
name = "rope"
version = "0.1.0"
@@ -4825,6 +4821,31 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rusqlite"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a"
dependencies = [
"bitflags",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"serde_json",
"smallvec",
]
[[package]]
name = "rusqlite_migration"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eda44233be97aea786691f9f6f7ef230bcf905061f4012e90f4f39e6dcf31163"
dependencies = [
"log",
"rusqlite",
]
[[package]]
name = "rust-embed"
version = "6.4.1"
@@ -4912,9 +4933,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.20.6"
version = "0.20.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033"
checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c"
dependencies = [
"log",
"ring",
@@ -4993,7 +5014,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
dependencies = [
"lazy_static",
"windows-sys",
"windows-sys 0.36.1",
]
[[package]]
@@ -5220,6 +5241,16 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_rusqlite"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "538b51f10ee271375cbd9caa04fa6e3e50af431a21db97caae48da92a074244a"
dependencies = [
"rusqlite",
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@@ -5594,7 +5625,7 @@ dependencies = [
"paste",
"percent-encoding",
"rand 0.8.5",
"rustls 0.20.6",
"rustls 0.20.7",
"rustls-pemfile",
"serde",
"serde_json",
@@ -5826,6 +5857,7 @@ dependencies = [
"futures 0.3.24",
"gpui",
"itertools",
"language",
"lazy_static",
"libc",
"mio-extras",
@@ -5914,6 +5946,18 @@ dependencies = [
"workspace",
]
[[package]]
name = "theme_testbench"
version = "0.1.0"
dependencies = [
"gpui",
"project",
"settings",
"smallvec",
"theme",
"workspace",
]
[[package]]
name = "thiserror"
version = "1.0.37"
@@ -6100,7 +6144,7 @@ version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
dependencies = [
"rustls 0.20.6",
"rustls 0.20.7",
"tokio",
"webpki 0.22.0",
]
@@ -7384,43 +7428,100 @@ version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
dependencies = [
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_msvc",
"windows_aarch64_msvc 0.36.1",
"windows_i686_gnu 0.36.1",
"windows_i686_msvc 0.36.1",
"windows_x86_64_gnu 0.36.1",
"windows_x86_64_msvc 0.36.1",
]
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc 0.42.0",
"windows_i686_gnu 0.42.0",
"windows_i686_msvc 0.42.0",
"windows_x86_64_gnu 0.42.0",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc 0.42.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
[[package]]
name = "windows_aarch64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
[[package]]
name = "windows_i686_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
[[package]]
name = "windows_i686_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_i686_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
[[package]]
name = "windows_x86_64_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
[[package]]
name = "windows_x86_64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
[[package]]
name = "winreg"
version = "0.10.1"
@@ -7507,9 +7608,9 @@ checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
[[package]]
name = "xmlparser"
version = "0.13.3"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "114ba2b24d2167ef6d67d7d04c8cc86522b87f490025f39f0303b7db5bf5e3d8"
checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd"
[[package]]
name = "xmlwriter"
@@ -7528,7 +7629,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.60.1"
version = "0.62.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -7540,7 +7641,6 @@ dependencies = [
"backtrace",
"breadcrumbs",
"call",
"chat_panel",
"chrono",
"cli",
"client",
@@ -7599,6 +7699,7 @@ dependencies = [
"text",
"theme",
"theme_selector",
"theme_testbench",
"thiserror",
"tiny_http",
"toml",

View File

@@ -1,5 +1,61 @@
[workspace]
members = ["crates/*"]
members = [
"crates/activity_indicator",
"crates/assets",
"crates/auto_update",
"crates/breadcrumbs",
"crates/call",
"crates/cli",
"crates/client",
"crates/clock",
"crates/collab",
"crates/collab_ui",
"crates/collections",
"crates/command_palette",
"crates/context_menu",
"crates/db",
"crates/diagnostics",
"crates/drag_and_drop",
"crates/editor",
"crates/file_finder",
"crates/fs",
"crates/fsevent",
"crates/fuzzy",
"crates/git",
"crates/go_to_line",
"crates/gpui",
"crates/gpui_macros",
"crates/journal",
"crates/language",
"crates/live_kit_client",
"crates/live_kit_server",
"crates/lsp",
"crates/media",
"crates/menu",
"crates/outline",
"crates/picker",
"crates/plugin",
"crates/plugin_macros",
"crates/plugin_runtime",
"crates/project",
"crates/project_panel",
"crates/project_symbols",
"crates/rope",
"crates/rpc",
"crates/search",
"crates/settings",
"crates/snippet",
"crates/sum_tree",
"crates/terminal",
"crates/text",
"crates/theme",
"crates/theme_selector",
"crates/theme_testbench",
"crates/util",
"crates/vim",
"crates/workspace",
"crates/zed",
]
default-members = ["crates/zed"]
resolver = "2"
@@ -18,8 +74,6 @@ cocoa-foundation = { git = "https://github.com/servo/core-foundation-rs", rev =
core-foundation = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
core-foundation-sys = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
core-graphics = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
# TODO - Remove when a new version of RustRocksDB is released
rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "39dc822dde743b2a26eb160b660e8fbdab079d49" }
[profile.dev]
split-debuginfo = "unpacked"

View File

@@ -19,5 +19,7 @@ FROM debian:bullseye-slim as runtime
RUN apt-get update; \
apt-get install -y --no-install-recommends libcurl4-openssl-dev ca-certificates
WORKDIR app
COPY --from=builder /app/collab /app
COPY --from=builder /app/collab /app/collab
COPY --from=builder /app/crates/collab/migrations /app/migrations
ENV MIGRATIONS_PATH=/app/migrations
ENTRYPOINT ["/app/collab"]

View File

@@ -1,15 +0,0 @@
# syntax = docker/dockerfile:1.2
FROM rust:1.64-bullseye as builder
WORKDIR app
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=./target \
cargo install sqlx-cli --root=/app --target-dir=/app/target --version 0.5.7
FROM debian:bullseye-slim as runtime
RUN apt-get update; \
apt-get install -y --no-install-recommends libssl1.1
WORKDIR app
COPY --from=builder /app/bin/sqlx /app
COPY ./crates/collab/migrations /app/migrations
ENTRYPOINT ["/app/sqlx", "migrate", "run"]

View File

@@ -1,2 +1,2 @@
web: cd ../zed.dev && PORT=3000 npx next dev
collab: cd crates/collab && cargo run
web: cd ../zed.dev && PORT=3000 npx vercel dev
collab: cd crates/collab && cargo run serve

View File

@@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 0.666656H1C0.447917 0.666656 0 1.11457 0 1.66666V8.33332C0 8.88541 0.447917 9.33332 1 9.33332H5L4.66667 10.3333H3.16667C2.89167 10.3333 2.66667 10.5583 2.66667 10.8333C2.66667 11.1083 2.89167 11.3333 3.16667 11.3333H8.83333C9.10938 11.3333 9.33333 11.1094 9.33333 10.8333C9.33333 10.5573 9.10938 10.3333 8.83333 10.3333H7.33333L7 9.33332H11C11.5521 9.33332 12 8.88541 12 8.33332V1.66666C12 1.11457 11.5521 0.666656 11 0.666656ZM10.6667 7.99999H1.33333V1.99999H10.6667V7.99999Z" fill="#979DB4"/>
</svg>

After

Width:  |  Height:  |  Size: 611 B

View File

@@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.53324 9.90014H7.18324L6.88324 9.00014H7.63305L6.10211 7.80014H1.78324V4.41577L0.583236 3.47452V8.10014C0.583217 8.59702 0.986361 9.00014 1.46636 9.00014H5.04949L4.74949 9.90014H3.43324C3.1848 9.90014 2.98324 10.1017 2.98324 10.3501C2.98324 10.5986 3.1848 10.8001 3.43324 10.8001H8.51637C8.7648 10.8001 8.96637 10.5986 8.96637 10.3501C8.96637 10.1017 8.79762 9.90014 8.53324 9.90014ZM11.8276 9.99577L10.5507 8.99489C11.0234 8.96789 11.3999 8.57939 11.3999 8.09996V2.09995C11.3999 1.60308 10.9968 1.19995 10.4999 1.19995H1.5168C1.28617 1.19995 1.07786 1.28939 0.918674 1.43208L0.727799 1.29595C0.645299 1.23145 0.547423 1.19995 0.450673 1.19995C0.316986 1.19995 0.184611 1.2592 0.0961106 1.37226C-0.057452 1.56801 -0.023327 1.85095 0.172236 2.00414L11.2724 10.7041C11.4693 10.8579 11.7519 10.8226 11.9041 10.6276C12.0581 10.4321 12.0224 10.149 11.8274 9.99521L11.8276 9.99577ZM10.1832 7.80014H9.00968L2.11905 2.40014H10.1816L10.1832 7.80014Z" fill="#93A1A1"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -3,8 +3,12 @@
{
"bindings": {
"up": "menu::SelectPrev",
"pageup": "menu::SelectFirst",
"shift-pageup": "menu::SelectFirst",
"ctrl-p": "menu::SelectPrev",
"down": "menu::SelectNext",
"pagedown": "menu::SelectLast",
"shift-pagedown": "menu::SelectFirst",
"ctrl-n": "menu::SelectNext",
"cmd-up": "menu::SelectFirst",
"cmd-down": "menu::SelectLast",
@@ -60,13 +64,18 @@
"cmd-z": "editor::Undo",
"cmd-shift-z": "editor::Redo",
"up": "editor::MoveUp",
"pageup": "editor::PageUp",
"shift-pageup": "editor::MovePageUp",
"down": "editor::MoveDown",
"pagedown": "editor::PageDown",
"shift-pagedown": "editor::MovePageDown",
"left": "editor::MoveLeft",
"right": "editor::MoveRight",
"ctrl-p": "editor::MoveUp",
"ctrl-n": "editor::MoveDown",
"ctrl-b": "editor::MoveLeft",
"ctrl-f": "editor::MoveRight",
"ctrl-l": "editor::CenterScreen",
"alt-left": "editor::MoveToPreviousWordStart",
"alt-b": "editor::MoveToPreviousWordStart",
"alt-right": "editor::MoveToNextWordEnd",
@@ -118,8 +127,18 @@
"stop_at_soft_wraps": true
}
],
"pageup": "editor::PageUp",
"pagedown": "editor::PageDown",
"ctrl-v": [
"editor::MovePageDown",
{
"center_cursor": true
}
],
"alt-v": [
"editor::MovePageUp",
{
"center_cursor": true
}
],
"ctrl-cmd-space": "editor::ShowCharacterPalette"
}
},
@@ -412,6 +431,12 @@
"shift-escape": "dock::HideDock"
}
},
{
"context": "Pane",
"bindings": {
"cmd-escape": "dock::MoveActiveItemToDock"
}
},
{
"context": "ProjectPanel",
"bindings": {
@@ -451,10 +476,18 @@
"terminal::SendKeystroke",
"up"
],
"pageup": [
"terminal::SendKeystroke",
"pageup"
],
"down": [
"terminal::SendKeystroke",
"down"
],
"pagedown": [
"terminal::SendKeystroke",
"pagedown"
],
"escape": [
"terminal::SendKeystroke",
"escape"

View File

@@ -1,218 +1,230 @@
{
// The name of the Zed theme to use for the UI
"theme": "one-dark",
// The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Mono",
// The default font size for text in the editor
"buffer_font_size": 15,
// Whether to enable vim modes and key bindings
"vim_mode": false,
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
"hover_popover_enabled": true,
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
// Whether new projects should start out 'online'. Online projects
// appear in the contacts panel under your name, so that your contacts
// can see which projects you are working on. Regardless of this
// setting, projects keep their last online status when you reopen them.
"projects_online_by_default": true,
// Whether to use language servers to provide code intelligence.
"enable_language_server": true,
// When to automatically save edited buffers. This setting can
// take four values.
//
// 1. Never automatically save:
// "autosave": "off",
// 2. Save when changing focus away from the Zed window:
// "autosave": "on_window_change",
// 3. Save when changing focus away from a specific buffer:
// "autosave": "on_focus_change",
// 4. Save when idle for a certain amount of time:
// "autosave": { "after_delay": {"milliseconds": 500} },
"autosave": "off",
// Where to place the dock by default. This setting can take three
// values:
//
// 1. Position the dock attached to the bottom of the workspace
// "default_dock_anchor": "bottom"
// 2. Position the dock to the right of the workspace like a side panel
// "default_dock_anchor": "right"
// 3. Position the dock full screen over the entire workspace"
// "default_dock_anchor": "expanded"
"default_dock_anchor": "right",
// Whether or not to perform a buffer format before saving
"format_on_save": "on",
// How to perform a buffer format. This setting can take two values:
//
// 1. Format code using the current language server:
// "format_on_save": "language_server"
// 2. Format code using an external command:
// "format_on_save": {
// "external": {
// "command": "prettier",
// "arguments": ["--stdin-filepath", "{buffer_path}"]
// }
// The name of the Zed theme to use for the UI
"theme": "One Dark",
// The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Mono",
// The default font size for text in the editor
"buffer_font_size": 15,
// Whether to enable vim modes and key bindings
"vim_mode": false,
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
"hover_popover_enabled": true,
// Whether the cursor blinks in the editor.
"cursor_blink": true,
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
// Whether new projects should start out 'online'. Online projects
// appear in the contacts panel under your name, so that your contacts
// can see which projects you are working on. Regardless of this
// setting, projects keep their last online status when you reopen them.
"projects_online_by_default": true,
// Whether to use language servers to provide code intelligence.
"enable_language_server": true,
// When to automatically save edited buffers. This setting can
// take four values.
//
// 1. Never automatically save:
// "autosave": "off",
// 2. Save when changing focus away from the Zed window:
// "autosave": "on_window_change",
// 3. Save when changing focus away from a specific buffer:
// "autosave": "on_focus_change",
// 4. Save when idle for a certain amount of time:
// "autosave": { "after_delay": {"milliseconds": 500} },
"autosave": "off",
// Where to place the dock by default. This setting can take three
// values:
//
// 1. Position the dock attached to the bottom of the workspace
// "default_dock_anchor": "bottom"
// 2. Position the dock to the right of the workspace like a side panel
// "default_dock_anchor": "right"
// 3. Position the dock full screen over the entire workspace"
// "default_dock_anchor": "expanded"
"default_dock_anchor": "right",
// Whether or not to perform a buffer format before saving
"format_on_save": "on",
// How to perform a buffer format. This setting can take two values:
//
// 1. Format code using the current language server:
// "format_on_save": "language_server"
// 2. Format code using an external command:
// "format_on_save": {
// "external": {
// "command": "prettier",
// "arguments": ["--stdin-filepath", "{buffer_path}"]
// }
// }
"formatter": "language_server",
// How to soft-wrap long lines of text. This setting can take
// three values:
//
// 1. Do not soft wrap.
// "soft_wrap": "none",
// 2. Soft wrap lines that overflow the editor:
// "soft_wrap": "editor_width",
// 3. Soft wrap lines at the preferred line length
// "soft_wrap": "preferred_line_length",
"soft_wrap": "none",
// The column at which to soft-wrap lines, for buffers where soft-wrap
// is enabled.
"preferred_line_length": 80,
// Whether to indent lines using tab characters, as opposed to multiple
// spaces.
"hard_tabs": false,
// How many columns a tab should occupy.
"tab_size": 4,
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
// 1. Show the gutter
// "git_gutter": "tracked_files"
// 2. Hide the gutter
// "git_gutter": "hide"
"git_gutter": "tracked_files"
},
// Settings specific to journaling
"journal": {
// The path of the directory where journal entries are stored
"path": "~",
// What format to display the hours in
// May take 2 values:
// 1. hour12
// 2. hour24
"hour_format": "hour12"
},
// Settings specific to the terminal
"terminal": {
// What shell to use when opening a terminal. May take 3 values:
// 1. Use the system's default terminal configuration (e.g. $TERM).
// "shell": "system"
// 2. A program:
// "shell": {
// "program": "sh"
// }
// 3. A program with arguments:
// "shell": {
// "with_arguments": {
// "program": "/bin/bash",
// "arguments": ["--login"]
// }
// }
"formatter": "language_server",
// How to soft-wrap long lines of text. This setting can take
// three values:
"shell": "system",
// What working directory to use when launching the terminal.
// May take 4 values:
// 1. Use the current file's project directory. Will Fallback to the
// first project directory strategy if unsuccessful
// "working_directory": "current_project_directory"
// 2. Use the first project in this workspace's directory
// "working_directory": "first_project_directory"
// 3. Always use this platform's home directory (if we can find it)
// "working_directory": "always_home"
// 4. Always use a specific directory. This value will be shell expanded.
// If this path is not a valid directory the terminal will default to
// this platform's home directory (if we can find it)
// "working_directory": {
// "always": {
// "directory": "~/zed/projects/"
// }
// }
//
// 1. Do not soft wrap.
// "soft_wrap": "none",
// 2. Soft wrap lines that overflow the editor:
// "soft_wrap": "editor_width",
// 3. Soft wrap lines at the preferred line length
// "soft_wrap": "preferred_line_length",
"soft_wrap": "none",
// The column at which to soft-wrap lines, for buffers where soft-wrap
// is enabled.
"preferred_line_length": 80,
// Whether to indent lines using tab characters, as opposed to multiple
// spaces.
"hard_tabs": false,
// How many columns a tab should occupy.
"tab_size": 4,
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
// 1. Show the gutter
// "git_gutter": "tracked_files"
// 2. Hide the gutter
// "git_gutter": "hide"
"git_gutter": "tracked_files"
},
// Settings specific to the terminal
"terminal": {
// What shell to use when opening a terminal. May take 3 values:
// 1. Use the system's default terminal configuration (e.g. $TERM).
// "shell": "system"
// 2. A program:
// "shell": {
// "program": "sh"
// }
// 3. A program with arguments:
// "shell": {
// "with_arguments": {
// "program": "/bin/bash",
// "arguments": ["--login"]
// }
// }
"shell": "system",
// What working directory to use when launching the terminal.
// May take 4 values:
// 1. Use the current file's project directory. Will Fallback to the
// first project directory strategy if unsuccessful
// "working_directory": "current_project_directory"
// 2. Use the first project in this workspace's directory
// "working_directory": "first_project_directory"
// 3. Always use this platform's home directory (if we can find it)
// "working_directory": "always_home"
// 4. Always use a specific directory. This value will be shell expanded.
// If this path is not a valid directory the terminal will default to
// this platform's home directory (if we can find it)
// "working_directory": {
// "always": {
// "directory": "~/zed/projects/"
// }
// }
//
//
"working_directory": "current_project_directory",
// Set the cursor blinking behavior in the terminal.
// May take 4 values:
// 1. Never blink the cursor, ignoring the terminal mode
// "blinking": "off",
// 2. Default the cursor blink to off, but allow the terminal to
// set blinking
// "blinking": "terminal_controlled",
// 3. Always blink the cursor, ignoring the terminal mode
// "blinking": "on",
"blinking": "terminal_controlled",
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
// Alternate Scroll mode converts mouse scroll events into up / down key
// presses when in the alternate screen (e.g. when running applications
// like vim or less). The terminal can still set and unset this mode.
// May take 2 values:
// 1. Default alternate scroll mode to on
// "alternate_scroll": "on",
// 2. Default alternate scroll mode to off
// "alternate_scroll": "off",
"alternate_scroll": "off",
// Set whether the option key behaves as the meta key.
// May take 2 values:
// 1. Rely on default platform handling of option key, on macOS
// this means generating certain unicode characters
// "option_to_meta": false,
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
// "option_to_meta": true,
"option_as_meta": false,
// Whether or not selecting text in the terminal will automatically
// copy to the system clipboard.
"copy_on_select": false,
// Any key-value pairs added to this list will be added to the terminal's
// enviroment. Use `:` to seperate multiple values.
"env": {
// "KEY": "value1:value2"
}
// Set the terminal's font size. If this option is not included,
// the terminal will default to matching the buffer's font size.
// "font_size": "15"
// Set the terminal's font family. If this option is not included,
// the terminal will default to matching the buffer's font family.
// "font_family": "Zed Mono"
},
// Different settings for specific languages.
"languages": {
"Plain Text": {
"soft_wrap": "preferred_line_length"
},
"C": {
"tab_size": 2
},
"C++": {
"tab_size": 2
},
"Elixir": {
"tab_size": 2
},
"Go": {
"tab_size": 4,
"hard_tabs": true
},
"Markdown": {
"soft_wrap": "preferred_line_length"
},
"Rust": {
"tab_size": 4
},
"JavaScript": {
"tab_size": 2
},
"TypeScript": {
"tab_size": 2
},
"TSX": {
"tab_size": 2
}
},
// LSP Specific settings.
"lsp": {
// Specify the LSP name as a key here.
// As of 8/10/22, supported LSPs are:
// pyright
// gopls
// rust-analyzer
// typescript-language-server
// vscode-json-languageserver
// "rust_analyzer": {
// //These initialization options are merged into Zed's defaults
// "initialization_options": {
// "checkOnSave": {
// "command": "clippy"
// }
// }
// }
//
"working_directory": "current_project_directory",
// Set the cursor blinking behavior in the terminal.
// May take 4 values:
// 1. Never blink the cursor, ignoring the terminal mode
// "blinking": "off",
// 2. Default the cursor blink to off, but allow the terminal to
// set blinking
// "blinking": "terminal_controlled",
// 3. Always blink the cursor, ignoring the terminal mode
// "blinking": "on",
"blinking": "terminal_controlled",
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
// Alternate Scroll mode converts mouse scroll events into up / down key
// presses when in the alternate screen (e.g. when running applications
// like vim or less). The terminal can still set and unset this mode.
// May take 2 values:
// 1. Default alternate scroll mode to on
// "alternate_scroll": "on",
// 2. Default alternate scroll mode to off
// "alternate_scroll": "off",
"alternate_scroll": "off",
// Set whether the option key behaves as the meta key.
// May take 2 values:
// 1. Rely on default platform handling of option key, on macOS
// this means generating certain unicode characters
// "option_to_meta": false,
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
// "option_to_meta": true,
"option_as_meta": false,
// Whether or not selecting text in the terminal will automatically
// copy to the system clipboard.
"copy_on_select": false,
// Any key-value pairs added to this list will be added to the terminal's
// enviroment. Use `:` to seperate multiple values.
"env": {
// "KEY": "value1:value2"
}
// Set the terminal's font size. If this option is not included,
// the terminal will default to matching the buffer's font size.
// "font_size": "15"
// Set the terminal's font family. If this option is not included,
// the terminal will default to matching the buffer's font family.
// "font_family": "Zed Mono"
},
// Different settings for specific languages.
"languages": {
"Plain Text": {
"soft_wrap": "preferred_line_length"
},
"C": {
"tab_size": 2
},
"C++": {
"tab_size": 2
},
"Elixir": {
"tab_size": 2
},
"Go": {
"tab_size": 4,
"hard_tabs": true
},
"Markdown": {
"soft_wrap": "preferred_line_length"
},
"Rust": {
"tab_size": 4
},
"JavaScript": {
"tab_size": 2
},
"TypeScript": {
"tab_size": 2
},
"TSX": {
"tab_size": 2
}
},
// LSP Specific settings.
"lsp": {
// Specify the LSP name as a key here.
// As of 8/10/22, supported LSPs are:
// pyright
// gopls
// rust-analyzer
// typescript-language-server
// vscode-json-languageserver
// "rust_analyzer": {
// //These initialization options are merged into Zed's defaults
// "initialization_options": {
// "checkOnSave": {
// "command": "clippy"
// }
// }
// }
}
}

View File

@@ -46,6 +46,7 @@ impl ActivityIndicator {
cx: &mut ViewContext<Workspace>,
) -> ViewHandle<ActivityIndicator> {
let project = workspace.project().clone();
let auto_updater = AutoUpdater::get(cx);
let this = cx.add_view(|cx: &mut ViewContext<Self>| {
let mut status_events = languages.language_server_binary_statuses();
cx.spawn_weak(|this, mut cx| async move {
@@ -66,11 +67,14 @@ impl ActivityIndicator {
})
.detach();
cx.observe(&project, |_, _, cx| cx.notify()).detach();
if let Some(auto_updater) = auto_updater.as_ref() {
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
}
Self {
statuses: Default::default(),
project: project.clone(),
auto_updater: AutoUpdater::get(cx),
auto_updater,
}
});
cx.subscribe(&this, move |workspace, _, event, cx| match event {
@@ -285,7 +289,7 @@ impl View for ActivityIndicator {
.workspace
.status_bar
.lsp_status;
let style = if state.hovered && action.is_some() {
let style = if state.hovered() && action.is_some() {
theme.hover.as_ref().unwrap_or(&theme.default)
} else {
&theme.default

View File

@@ -1,13 +1,14 @@
mod update_notification;
use anyhow::{anyhow, Context, Result};
use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN};
use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use gpui::{
actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
MutableAppContext, Task, WeakViewHandle,
};
use lazy_static::lazy_static;
use serde::Deserialize;
use settings::ReleaseChannel;
use smol::{fs::File, io::AsyncReadExt, process::Command};
use std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration};
use update_notification::UpdateNotification;
@@ -40,7 +41,7 @@ pub struct AutoUpdater {
current_version: AppVersion,
http_client: Arc<dyn HttpClient>,
pending_poll: Option<Task<()>>,
db: Arc<project::Db>,
db: project::Db,
server_url: String,
}
@@ -54,13 +55,9 @@ impl Entity for AutoUpdater {
type Event = ();
}
pub fn init(
db: Arc<project::Db>,
http_client: Arc<dyn HttpClient>,
server_url: String,
cx: &mut MutableAppContext,
) {
pub fn init(db: project::Db, http_client: Arc<dyn HttpClient>, cx: &mut MutableAppContext) {
if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) {
let server_url = ZED_SERVER_URL.to_string();
let auto_updater = cx.add_model(|cx| {
let updater = AutoUpdater::new(version, db.clone(), http_client, server_url.clone());
updater.start_polling(cx).detach();
@@ -116,7 +113,7 @@ impl AutoUpdater {
fn new(
current_version: AppVersion,
db: Arc<project::Db>,
db: project::Db,
http_client: Arc<dyn HttpClient>,
server_url: String,
) -> Self {
@@ -177,9 +174,19 @@ impl AutoUpdater {
this.current_version,
)
});
let preview_param = cx.read(|cx| {
if cx.has_global::<ReleaseChannel>() {
if *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview {
return "&preview=1";
}
}
""
});
let mut response = client
.get(
&format!("{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg"),
&format!("{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg{preview_param}"),
Default::default(),
true,
)
@@ -283,9 +290,9 @@ impl AutoUpdater {
let db = self.db.clone();
cx.background().spawn(async move {
if should_show {
db.write([(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY, "")])?;
db.write_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY, "")?;
} else {
db.delete([(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)])?;
db.delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?;
}
Ok(())
})
@@ -293,8 +300,7 @@ impl AutoUpdater {
fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
let db = self.db.clone();
cx.background().spawn(async move {
Ok(db.read([(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)])?[0].is_some())
})
cx.background()
.spawn(async move { Ok(db.read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?.is_some()) })
}
}

View File

@@ -12,6 +12,7 @@ test-support = [
"client/test-support",
"collections/test-support",
"gpui/test-support",
"live_kit_client/test-support",
"project/test-support",
"util/test-support"
]
@@ -20,10 +21,13 @@ test-support = [
client = { path = "../client" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }
live_kit_client = { path = "../live_kit_client" }
media = { path = "../media" }
project = { path = "../project" }
util = { path = "../util" }
anyhow = "1.0.38"
async-broadcast = "0.4"
futures = "0.3"
postage = { version = "0.4.1", features = ["futures-traits"] }
@@ -31,5 +35,6 @@ postage = { version = "0.4.1", features = ["futures-traits"] }
client = { path = "../client", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }

View File

@@ -1,11 +1,12 @@
mod participant;
pub mod participant;
pub mod room;
use anyhow::{anyhow, Result};
use client::{proto, Client, TypedEnvelope, User, UserStore};
use collections::HashSet;
use gpui::{
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
Subscription, Task,
Subscription, Task, WeakModelHandle,
};
pub use participant::ParticipantLocation;
use postage::watch;
@@ -28,6 +29,8 @@ pub struct IncomingCall {
pub struct ActiveCall {
room: Option<(ModelHandle<Room>, Vec<Subscription>)>,
location: Option<WeakModelHandle<Project>>,
pending_invites: HashSet<u64>,
incoming_call: (
watch::Sender<Option<IncomingCall>>,
watch::Receiver<Option<IncomingCall>>,
@@ -49,6 +52,8 @@ impl ActiveCall {
) -> Self {
Self {
room: None,
location: None,
pending_invites: Default::default(),
incoming_call: watch::channel(),
_subscriptions: vec![
client.add_request_handler(cx.handle(), Self::handle_incoming_call),
@@ -111,31 +116,49 @@ impl ActiveCall {
) -> Task<Result<()>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
cx.spawn(|this, mut cx| async move {
if let Some(room) = this.read_with(&cx, |this, _| this.room().cloned()) {
let initial_project_id = if let Some(initial_project) = initial_project {
Some(
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))
.await?,
)
} else {
None
};
if !self.pending_invites.insert(recipient_user_id) {
return Task::ready(Err(anyhow!("user was already invited")));
}
room.update(&mut cx, |room, cx| {
room.call(recipient_user_id, initial_project_id, cx)
})
.await?;
} else {
let room = cx
.update(|cx| {
Room::create(recipient_user_id, initial_project, client, user_store, cx)
cx.notify();
cx.spawn(|this, mut cx| async move {
let invite = async {
if let Some(room) = this.read_with(&cx, |this, _| this.room().cloned()) {
let initial_project_id = if let Some(initial_project) = initial_project {
Some(
room.update(&mut cx, |room, cx| {
room.share_project(initial_project, cx)
})
.await?,
)
} else {
None
};
room.update(&mut cx, |room, cx| {
room.call(recipient_user_id, initial_project_id, cx)
})
.await?;
this.update(&mut cx, |this, cx| this.set_room(Some(room), cx));
} else {
let room = cx
.update(|cx| {
Room::create(recipient_user_id, initial_project, client, user_store, cx)
})
.await?;
this.update(&mut cx, |this, cx| this.set_room(Some(room), cx))
.await?;
};
Ok(())
};
Ok(())
let result = invite.await;
this.update(&mut cx, |this, cx| {
this.pending_invites.remove(&recipient_user_id);
cx.notify();
});
result
})
}
@@ -180,7 +203,8 @@ impl ActiveCall {
let join = Room::join(&call, self.client.clone(), self.user_store.clone(), cx);
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx));
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))
.await?;
Ok(())
})
}
@@ -223,39 +247,54 @@ impl ActiveCall {
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(Err(anyhow!("no active call")))
Task::ready(Ok(()))
}
}
fn set_room(&mut self, room: Option<ModelHandle<Room>>, cx: &mut ModelContext<Self>) {
fn set_room(
&mut self,
room: Option<ModelHandle<Room>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if room.as_ref() != self.room.as_ref().map(|room| &room.0) {
cx.notify();
if let Some(room) = room {
if room.read(cx).status().is_offline() {
self.room = None;
Task::ready(Ok(()))
} else {
let subscriptions = vec![
cx.observe(&room, |this, room, cx| {
if room.read(cx).status().is_offline() {
this.set_room(None, cx);
this.set_room(None, cx).detach_and_log_err(cx);
}
cx.notify();
}),
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
];
self.room = Some((room, subscriptions));
self.room = Some((room.clone(), subscriptions));
let location = self.location.and_then(|location| location.upgrade(cx));
room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
}
} else {
self.room = None;
Task::ready(Ok(()))
}
cx.notify();
} else {
Task::ready(Ok(()))
}
}
pub fn room(&self) -> Option<&ModelHandle<Room>> {
self.room.as_ref().map(|(room, _)| room)
}
pub fn pending_invites(&self) -> &HashSet<u64> {
&self.pending_invites
}
}

View File

@@ -1,6 +1,8 @@
use anyhow::{anyhow, Result};
use client::{proto, User};
use collections::HashMap;
use gpui::WeakModelHandle;
pub use live_kit_client::Frame;
use project::Project;
use std::sync::Arc;
@@ -34,9 +36,21 @@ pub struct LocalParticipant {
pub active_project: Option<WeakModelHandle<Project>>,
}
#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct RemoteParticipant {
pub user: Arc<User>,
pub projects: Vec<proto::ParticipantProject>,
pub location: ParticipantLocation,
pub tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
}
#[derive(Clone)]
pub struct RemoteVideoTrack {
pub(crate) live_kit_track: Arc<live_kit_client::RemoteVideoTrack>,
}
impl RemoteVideoTrack {
pub fn frames(&self) -> async_broadcast::Receiver<Frame> {
self.live_kit_track.frames()
}
}

View File

@@ -1,5 +1,5 @@
use crate::{
participant::{LocalParticipant, ParticipantLocation, RemoteParticipant},
participant::{LocalParticipant, ParticipantLocation, RemoteParticipant, RemoteVideoTrack},
IncomingCall,
};
use anyhow::{anyhow, Result};
@@ -7,12 +7,20 @@ use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
use collections::{BTreeMap, HashSet};
use futures::StreamExt;
use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate};
use postage::stream::Stream;
use project::Project;
use std::sync::Arc;
use util::ResultExt;
use std::{mem, os::unix::prelude::OsStrExt, sync::Arc};
use util::{post_inc, ResultExt};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
ParticipantLocationChanged {
participant_id: PeerId,
},
RemoteVideoTracksChanged {
participant_id: PeerId,
},
RemoteProjectShared {
owner: Arc<User>,
project_id: u64,
@@ -26,6 +34,7 @@ pub enum Event {
pub struct Room {
id: u64,
live_kit: Option<LiveKitRoom>,
status: RoomStatus,
local_participant: LocalParticipant,
remote_participants: BTreeMap<PeerId, RemoteParticipant>,
@@ -43,13 +52,16 @@ impl Entity for Room {
type Event = Event;
fn release(&mut self, _: &mut MutableAppContext) {
self.client.send(proto::LeaveRoom { id: self.id }).log_err();
if self.status.is_online() {
self.client.send(proto::LeaveRoom { id: self.id }).log_err();
}
}
}
impl Room {
fn new(
id: u64,
live_kit_connection_info: Option<proto::LiveKitConnectionInfo>,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut ModelContext<Self>,
@@ -69,8 +81,59 @@ impl Room {
})
.detach();
let live_kit_room = if let Some(connection_info) = live_kit_connection_info {
let room = live_kit_client::Room::new();
let mut status = room.status();
// Consume the initial status of the room.
let _ = status.try_recv();
let _maintain_room = cx.spawn_weak(|this, mut cx| async move {
while let Some(status) = status.next().await {
let this = if let Some(this) = this.upgrade(&cx) {
this
} else {
break;
};
if status == live_kit_client::ConnectionState::Disconnected {
this.update(&mut cx, |this, cx| this.leave(cx).log_err());
break;
}
}
});
let mut track_changes = room.remote_video_track_updates();
let _maintain_tracks = cx.spawn_weak(|this, mut cx| async move {
while let Some(track_change) = track_changes.next().await {
let this = if let Some(this) = this.upgrade(&cx) {
this
} else {
break;
};
this.update(&mut cx, |this, cx| {
this.remote_video_track_updated(track_change, cx).log_err()
});
}
});
cx.foreground()
.spawn(room.connect(&connection_info.server_url, &connection_info.token))
.detach_and_log_err(cx);
Some(LiveKitRoom {
room,
screen_track: ScreenTrack::None,
next_publish_id: 0,
_maintain_room,
_maintain_tracks,
})
} else {
None
};
Self {
id,
live_kit: live_kit_room,
status: RoomStatus::Online,
participant_user_ids: Default::default(),
local_participant: Default::default(),
@@ -94,7 +157,16 @@ impl Room {
) -> Task<Result<ModelHandle<Self>>> {
cx.spawn(|mut cx| async move {
let response = client.request(proto::CreateRoom {}).await?;
let room = cx.add_model(|cx| Self::new(response.id, client, user_store, cx));
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room = cx.add_model(|cx| {
Self::new(
room_proto.id,
response.live_kit_connection_info,
client,
user_store,
cx,
)
});
let initial_project_id = if let Some(initial_project) = initial_project {
let initial_project_id = room
@@ -130,7 +202,15 @@ impl Room {
cx.spawn(|mut cx| async move {
let response = client.request(proto::JoinRoom { id: room_id }).await?;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room = cx.add_model(|cx| Self::new(room_id, client, user_store, cx));
let room = cx.add_model(|cx| {
Self::new(
room_id,
response.live_kit_connection_info,
client,
user_store,
cx,
)
});
room.update(&mut cx, |room, cx| {
room.leave_when_empty = true;
room.apply_room_update(room_proto, cx)?;
@@ -160,6 +240,7 @@ impl Room {
self.pending_participants.clear();
self.participant_user_ids.clear();
self.subscriptions.clear();
self.live_kit.take();
self.client.send(proto::LeaveRoom { id: self.id })?;
Ok(())
}
@@ -272,15 +353,40 @@ impl Room {
});
}
this.remote_participants.insert(
peer_id,
RemoteParticipant {
user: user.clone(),
projects: participant.projects,
location: ParticipantLocation::from_proto(participant.location)
.unwrap_or(ParticipantLocation::External),
},
);
let location = ParticipantLocation::from_proto(participant.location)
.unwrap_or(ParticipantLocation::External);
if let Some(remote_participant) = this.remote_participants.get_mut(&peer_id)
{
remote_participant.projects = participant.projects;
if location != remote_participant.location {
remote_participant.location = location;
cx.emit(Event::ParticipantLocationChanged {
participant_id: peer_id,
});
}
} else {
this.remote_participants.insert(
peer_id,
RemoteParticipant {
user: user.clone(),
projects: participant.projects,
location,
tracks: Default::default(),
},
);
if let Some(live_kit) = this.live_kit.as_ref() {
let tracks =
live_kit.room.remote_video_tracks(&peer_id.0.to_string());
for track in tracks {
this.remote_video_track_updated(
RemoteVideoTrackUpdate::Subscribed(track),
cx,
)
.log_err();
}
}
}
}
this.remote_participants.retain(|_, participant| {
@@ -318,6 +424,49 @@ impl Room {
Ok(())
}
fn remote_video_track_updated(
&mut self,
change: RemoteVideoTrackUpdate,
cx: &mut ModelContext<Self>,
) -> Result<()> {
match change {
RemoteVideoTrackUpdate::Subscribed(track) => {
let peer_id = PeerId(track.publisher_id().parse()?);
let track_id = track.sid().to_string();
let participant = self
.remote_participants
.get_mut(&peer_id)
.ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
participant.tracks.insert(
track_id.clone(),
Arc::new(RemoteVideoTrack {
live_kit_track: track,
}),
);
cx.emit(Event::RemoteVideoTracksChanged {
participant_id: peer_id,
});
}
RemoteVideoTrackUpdate::Unsubscribed {
publisher_id,
track_id,
} => {
let peer_id = PeerId(publisher_id.parse()?);
let participant = self
.remote_participants
.get_mut(&peer_id)
.ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
participant.tracks.remove(&track_id);
cx.emit(Event::RemoteVideoTracksChanged {
participant_id: peer_id,
});
}
}
cx.notify();
Ok(())
}
fn check_invariants(&self) {
#[cfg(any(test, feature = "test-support"))]
{
@@ -389,6 +538,7 @@ impl Room {
id: worktree.id().to_proto(),
root_name: worktree.root_name().into(),
visible: worktree.is_visible(),
abs_path: worktree.abs_path().as_os_str().as_bytes().to_vec(),
}
})
.collect(),
@@ -417,7 +567,7 @@ impl Room {
})
}
pub fn set_location(
pub(crate) fn set_location(
&mut self,
project: Option<&ModelHandle<Project>>,
cx: &mut ModelContext<Self>,
@@ -457,6 +607,140 @@ impl Room {
Ok(())
})
}
pub fn is_screen_sharing(&self) -> bool {
self.live_kit.as_ref().map_or(false, |live_kit| {
!matches!(live_kit.screen_track, ScreenTrack::None)
})
}
pub fn share_screen(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
} else if self.is_screen_sharing() {
return Task::ready(Err(anyhow!("screen was already shared")));
}
let (displays, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
let publish_id = post_inc(&mut live_kit.next_publish_id);
live_kit.screen_track = ScreenTrack::Pending { publish_id };
cx.notify();
(live_kit.room.display_sources(), publish_id)
} else {
return Task::ready(Err(anyhow!("live-kit was not initialized")));
};
cx.spawn_weak(|this, mut cx| async move {
let publish_track = async {
let displays = displays.await?;
let display = displays
.first()
.ok_or_else(|| anyhow!("no display found"))?;
let track = LocalVideoTrack::screen_share_for_display(&display);
this.upgrade(&cx)
.ok_or_else(|| anyhow!("room was dropped"))?
.read_with(&cx, |this, _| {
this.live_kit
.as_ref()
.map(|live_kit| live_kit.room.publish_video_track(&track))
})
.ok_or_else(|| anyhow!("live-kit was not initialized"))?
.await
};
let publication = publish_track.await;
this.upgrade(&cx)
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, cx| {
let live_kit = this
.live_kit
.as_mut()
.ok_or_else(|| anyhow!("live-kit was not initialized"))?;
let canceled = if let ScreenTrack::Pending {
publish_id: cur_publish_id,
} = &live_kit.screen_track
{
*cur_publish_id != publish_id
} else {
true
};
match publication {
Ok(publication) => {
if canceled {
live_kit.room.unpublish_track(publication);
} else {
live_kit.screen_track = ScreenTrack::Published(publication);
cx.notify();
}
Ok(())
}
Err(error) => {
if canceled {
Ok(())
} else {
live_kit.screen_track = ScreenTrack::None;
cx.notify();
Err(error)
}
}
}
})
})
}
pub fn unshare_screen(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
if self.status.is_offline() {
return Err(anyhow!("room is offline"));
}
let live_kit = self
.live_kit
.as_mut()
.ok_or_else(|| anyhow!("live-kit was not initialized"))?;
match mem::take(&mut live_kit.screen_track) {
ScreenTrack::None => Err(anyhow!("screen was not shared")),
ScreenTrack::Pending { .. } => {
cx.notify();
Ok(())
}
ScreenTrack::Published(track) => {
live_kit.room.unpublish_track(track);
cx.notify();
Ok(())
}
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn set_display_sources(&self, sources: Vec<live_kit_client::MacOSDisplay>) {
self.live_kit
.as_ref()
.unwrap()
.room
.set_display_sources(sources);
}
}
struct LiveKitRoom {
room: Arc<live_kit_client::Room>,
screen_track: ScreenTrack,
next_publish_id: usize,
_maintain_room: Task<()>,
_maintain_tracks: Task<()>,
}
enum ScreenTrack {
None,
Pending { publish_id: usize },
Published(LocalTrackPublication),
}
impl Default for ScreenTrack {
fn default() -> Self {
Self::None
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
@@ -469,4 +753,8 @@ impl RoomStatus {
pub fn is_offline(&self) -> bool {
matches!(self, RoomStatus::Offline)
}
pub fn is_online(&self) -> bool {
matches!(self, RoomStatus::Online)
}
}

View File

@@ -1,32 +0,0 @@
[package]
name = "capture"
version = "0.1.0"
edition = "2021"
description = "An example of screen capture"
[dependencies]
gpui = { path = "../gpui" }
live_kit = { path = "../live_kit" }
media = { path = "../media" }
anyhow = "1.0.38"
block = "0.1"
bytes = "1.2"
byteorder = "1.4"
cocoa = "0.24"
core-foundation = "0.9.3"
core-graphics = "0.22.3"
foreign-types = "0.3"
futures = "0.3"
hmac = "0.12"
jwt = "0.16"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
objc = "0.2"
parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] }
serde = { version = "1.0", features = ["derive", "rc"] }
sha2 = "0.10"
simplelog = "0.9"
[build-dependencies]
bindgen = "0.59.2"

View File

@@ -1,7 +0,0 @@
fn main() {
// Find WebRTC.framework as a sibling of the executable when running outside of an application bundle
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
// Register exported Objective-C selectors, protocols, etc
println!("cargo:rustc-link-arg=-Wl,-ObjC");
}

View File

@@ -1,71 +0,0 @@
use anyhow::Result;
use hmac::{Hmac, Mac};
use jwt::SignWithKey;
use serde::Serialize;
use sha2::Sha256;
use std::{
ops::Add,
time::{Duration, SystemTime, UNIX_EPOCH},
};
static DEFAULT_TTL: Duration = Duration::from_secs(6 * 60 * 60); // 6 hours
#[derive(Default, Serialize)]
#[serde(rename_all = "camelCase")]
struct ClaimGrants<'a> {
iss: &'a str,
sub: &'a str,
iat: u64,
exp: u64,
nbf: u64,
jwtid: &'a str,
video: VideoGrant<'a>,
}
#[derive(Default, Serialize)]
#[serde(rename_all = "camelCase")]
struct VideoGrant<'a> {
room_create: Option<bool>,
room_join: Option<bool>,
room_list: Option<bool>,
room_record: Option<bool>,
room_admin: Option<bool>,
room: Option<&'a str>,
can_publish: Option<bool>,
can_subscribe: Option<bool>,
can_publish_data: Option<bool>,
hidden: Option<bool>,
recorder: Option<bool>,
}
pub fn create_token(
api_key: &str,
secret_key: &str,
room_name: &str,
participant_name: &str,
) -> Result<String> {
let secret_key: Hmac<Sha256> = Hmac::new_from_slice(secret_key.as_bytes())?;
let now = SystemTime::now();
let claims = ClaimGrants {
iss: api_key,
sub: participant_name,
iat: now.duration_since(UNIX_EPOCH).unwrap().as_secs(),
exp: now
.add(DEFAULT_TTL)
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
nbf: 0,
jwtid: participant_name,
video: VideoGrant {
room: Some(room_name),
room_join: Some(true),
can_publish: Some(true),
can_subscribe: Some(true),
..Default::default()
},
};
Ok(claims.sign_with_key(&secret_key)?)
}

View File

@@ -1,143 +0,0 @@
mod live_kit_token;
use futures::StreamExt;
use gpui::{
actions,
elements::{Canvas, *},
keymap::Binding,
platform::current::Surface,
Menu, MenuItem, ViewContext,
};
use live_kit::{LocalVideoTrack, Room};
use log::LevelFilter;
use media::core_video::CVImageBuffer;
use postage::watch;
use simplelog::SimpleLogger;
use std::sync::Arc;
actions!(capture, [Quit]);
fn main() {
SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
gpui::App::new(()).unwrap().run(|cx| {
cx.platform().activate(true);
cx.add_global_action(quit);
cx.add_bindings([Binding::new("cmd-q", Quit, None)]);
cx.set_menus(vec![Menu {
name: "Zed",
items: vec![MenuItem::Action {
name: "Quit",
action: Box::new(Quit),
}],
}]);
let live_kit_url = std::env::var("LIVE_KIT_URL").unwrap();
let live_kit_key = std::env::var("LIVE_KIT_KEY").unwrap();
let live_kit_secret = std::env::var("LIVE_KIT_SECRET").unwrap();
cx.spawn(|mut cx| async move {
let user1_token = live_kit_token::create_token(
&live_kit_key,
&live_kit_secret,
"test-room",
"test-participant-1",
)
.unwrap();
let room1 = Room::new();
room1.connect(&live_kit_url, &user1_token).await.unwrap();
let user2_token = live_kit_token::create_token(
&live_kit_key,
&live_kit_secret,
"test-room",
"test-participant-2",
)
.unwrap();
let room2 = Room::new();
room2.connect(&live_kit_url, &user2_token).await.unwrap();
cx.add_window(Default::default(), |cx| ScreenCaptureView::new(room2, cx));
let windows = live_kit::list_windows();
let window = windows
.iter()
.find(|w| w.owner_name.as_deref() == Some("Safari"))
.unwrap();
let track = LocalVideoTrack::screen_share_for_window(window.id);
room1.publish_video_track(&track).await.unwrap();
})
.detach();
});
}
struct ScreenCaptureView {
image_buffer: Option<CVImageBuffer>,
_room: Arc<Room>,
}
impl gpui::Entity for ScreenCaptureView {
type Event = ();
}
impl ScreenCaptureView {
pub fn new(room: Arc<Room>, cx: &mut ViewContext<Self>) -> Self {
let mut remote_video_tracks = room.remote_video_tracks();
cx.spawn_weak(|this, mut cx| async move {
if let Some(video_track) = remote_video_tracks.next().await {
let (mut frames_tx, mut frames_rx) = watch::channel_with(None);
video_track.add_renderer(move |frame| *frames_tx.borrow_mut() = Some(frame));
while let Some(frame) = frames_rx.next().await {
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
this.image_buffer = frame;
cx.notify();
});
} else {
break;
}
}
}
})
.detach();
Self {
image_buffer: None,
_room: room,
}
}
}
impl gpui::View for ScreenCaptureView {
fn ui_name() -> &'static str {
"View"
}
fn render(&mut self, _: &mut gpui::RenderContext<Self>) -> gpui::ElementBox {
let image_buffer = self.image_buffer.clone();
let canvas = Canvas::new(move |bounds, _, cx| {
if let Some(image_buffer) = image_buffer.clone() {
cx.scene.push_surface(Surface {
bounds,
image_buffer,
});
}
});
if let Some(image_buffer) = self.image_buffer.as_ref() {
canvas
.constrained()
.with_width(image_buffer.width() as f32)
.with_height(image_buffer.height() as f32)
.aligned()
.boxed()
} else {
canvas.boxed()
}
}
}
fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
cx.platform().quit();
}

View File

@@ -1,20 +0,0 @@
[package]
name = "chat_panel"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/chat_panel.rs"
doctest = false
[dependencies]
client = { path = "../client" }
editor = { path = "../editor" }
gpui = { path = "../gpui" }
menu = { path = "../menu" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
postage = { version = "0.4.1", features = ["futures-traits"] }
time = { version = "0.3", features = ["serde", "serde-well-known"] }

View File

@@ -1,433 +0,0 @@
use client::{
channel::{Channel, ChannelEvent, ChannelList, ChannelMessage},
Client,
};
use editor::Editor;
use gpui::{
actions,
elements::*,
platform::CursorStyle,
views::{ItemType, Select, SelectStyle},
AnyViewHandle, AppContext, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
Subscription, Task, View, ViewContext, ViewHandle,
};
use menu::Confirm;
use postage::prelude::Stream;
use settings::{Settings, SoftWrap};
use std::sync::Arc;
use time::{OffsetDateTime, UtcOffset};
use util::{ResultExt, TryFutureExt};
const MESSAGE_LOADING_THRESHOLD: usize = 50;
pub struct ChatPanel {
rpc: Arc<Client>,
channel_list: ModelHandle<ChannelList>,
active_channel: Option<(ModelHandle<Channel>, Subscription)>,
message_list: ListState,
input_editor: ViewHandle<Editor>,
channel_select: ViewHandle<Select>,
local_timezone: UtcOffset,
_observe_status: Task<()>,
}
pub enum Event {}
actions!(chat_panel, [LoadMoreMessages]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ChatPanel::send);
cx.add_action(ChatPanel::load_more_messages);
}
impl ChatPanel {
pub fn new(
rpc: Arc<Client>,
channel_list: ModelHandle<ChannelList>,
cx: &mut ViewContext<Self>,
) -> Self {
let input_editor = cx.add_view(|cx| {
let mut editor =
Editor::auto_height(4, Some(|theme| theme.chat_panel.input_editor.clone()), cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor
});
let channel_select = cx.add_view(|cx| {
let channel_list = channel_list.clone();
Select::new(0, cx, {
move |ix, item_type, is_hovered, cx| {
Self::render_channel_name(
&channel_list,
ix,
item_type,
is_hovered,
&cx.global::<Settings>().theme.chat_panel.channel_select,
cx,
)
}
})
.with_style(move |cx| {
let theme = &cx.global::<Settings>().theme.chat_panel.channel_select;
SelectStyle {
header: theme.header.container,
menu: theme.menu,
}
})
});
let mut message_list = ListState::new(0, Orientation::Bottom, 1000., cx, {
let this = cx.weak_handle();
move |_, ix, cx| {
let this = this.upgrade(cx).unwrap().read(cx);
let message = this.active_channel.as_ref().unwrap().0.read(cx).message(ix);
this.render_message(message, cx)
}
});
message_list.set_scroll_handler(|visible_range, cx| {
if visible_range.start < MESSAGE_LOADING_THRESHOLD {
cx.dispatch_action(LoadMoreMessages);
}
});
let _observe_status = cx.spawn_weak(|this, mut cx| {
let mut status = rpc.status();
async move {
while (status.recv().await).is_some() {
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |_, cx| cx.notify());
} else {
break;
}
}
}
});
let mut this = Self {
rpc,
channel_list,
active_channel: Default::default(),
message_list,
input_editor,
channel_select,
local_timezone: cx.platform().local_timezone(),
_observe_status,
};
this.init_active_channel(cx);
cx.observe(&this.channel_list, |this, _, cx| {
this.init_active_channel(cx);
})
.detach();
cx.observe(&this.channel_select, |this, channel_select, cx| {
let selected_ix = channel_select.read(cx).selected_index();
let selected_channel = this.channel_list.update(cx, |channel_list, cx| {
let available_channels = channel_list.available_channels()?;
let channel_id = available_channels.get(selected_ix)?.id;
channel_list.get_channel(channel_id, cx)
});
if let Some(selected_channel) = selected_channel {
this.set_active_channel(selected_channel, cx);
}
})
.detach();
this
}
fn init_active_channel(&mut self, cx: &mut ViewContext<Self>) {
let (active_channel, channel_count) = self.channel_list.update(cx, |list, cx| {
let channel_count;
let mut active_channel = None;
if let Some(available_channels) = list.available_channels() {
channel_count = available_channels.len();
if self.active_channel.is_none() {
if let Some(channel_id) = available_channels.first().map(|channel| channel.id) {
active_channel = list.get_channel(channel_id, cx);
}
}
} else {
channel_count = 0;
}
(active_channel, channel_count)
});
if let Some(active_channel) = active_channel {
self.set_active_channel(active_channel, cx);
} else {
self.message_list.reset(0);
self.active_channel = None;
}
self.channel_select.update(cx, |select, cx| {
select.set_item_count(channel_count, cx);
});
}
fn set_active_channel(&mut self, channel: ModelHandle<Channel>, cx: &mut ViewContext<Self>) {
if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) {
{
let channel = channel.read(cx);
self.message_list.reset(channel.message_count());
let placeholder = format!("Message #{}", channel.name());
self.input_editor.update(cx, move |editor, cx| {
editor.set_placeholder_text(placeholder, cx);
});
}
let subscription = cx.subscribe(&channel, Self::channel_did_change);
self.active_channel = Some((channel, subscription));
}
}
fn channel_did_change(
&mut self,
_: ModelHandle<Channel>,
event: &ChannelEvent,
cx: &mut ViewContext<Self>,
) {
match event {
ChannelEvent::MessagesUpdated {
old_range,
new_count,
} => {
self.message_list.splice(old_range.clone(), *new_count);
}
}
cx.notify();
}
fn render_channel(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.global::<Settings>().theme;
Flex::column()
.with_child(
Container::new(ChildView::new(&self.channel_select, cx).boxed())
.with_style(theme.chat_panel.channel_select.container)
.boxed(),
)
.with_child(self.render_active_channel_messages())
.with_child(self.render_input_box(cx))
.boxed()
}
fn render_active_channel_messages(&self) -> ElementBox {
let messages = if self.active_channel.is_some() {
List::new(self.message_list.clone()).boxed()
} else {
Empty::new().boxed()
};
FlexItem::new(messages).flex(1., true).boxed()
}
fn render_message(&self, message: &ChannelMessage, cx: &AppContext) -> ElementBox {
let now = OffsetDateTime::now_utc();
let settings = cx.global::<Settings>();
let theme = if message.is_pending() {
&settings.theme.chat_panel.pending_message
} else {
&settings.theme.chat_panel.message
};
Container::new(
Flex::column()
.with_child(
Flex::row()
.with_child(
Container::new(
Label::new(
message.sender.github_login.clone(),
theme.sender.text.clone(),
)
.boxed(),
)
.with_style(theme.sender.container)
.boxed(),
)
.with_child(
Container::new(
Label::new(
format_timestamp(message.timestamp, now, self.local_timezone),
theme.timestamp.text.clone(),
)
.boxed(),
)
.with_style(theme.timestamp.container)
.boxed(),
)
.boxed(),
)
.with_child(Text::new(message.body.clone(), theme.body.clone()).boxed())
.boxed(),
)
.with_style(theme.container)
.boxed()
}
fn render_input_box(&self, cx: &AppContext) -> ElementBox {
let theme = &cx.global::<Settings>().theme;
Container::new(ChildView::new(&self.input_editor, cx).boxed())
.with_style(theme.chat_panel.input_editor.container)
.boxed()
}
fn render_channel_name(
channel_list: &ModelHandle<ChannelList>,
ix: usize,
item_type: ItemType,
is_hovered: bool,
theme: &theme::ChannelSelect,
cx: &AppContext,
) -> ElementBox {
let channel = &channel_list.read(cx).available_channels().unwrap()[ix];
let theme = match (item_type, is_hovered) {
(ItemType::Header, _) => &theme.header,
(ItemType::Selected, false) => &theme.active_item,
(ItemType::Selected, true) => &theme.hovered_active_item,
(ItemType::Unselected, false) => &theme.item,
(ItemType::Unselected, true) => &theme.hovered_item,
};
Container::new(
Flex::row()
.with_child(
Container::new(Label::new("#".to_string(), theme.hash.text.clone()).boxed())
.with_style(theme.hash.container)
.boxed(),
)
.with_child(Label::new(channel.name.clone(), theme.name.clone()).boxed())
.boxed(),
)
.with_style(theme.container)
.boxed()
}
fn render_sign_in_prompt(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone();
let rpc = self.rpc.clone();
let this = cx.handle();
enum SignInPromptLabel {}
Align::new(
MouseEventHandler::<SignInPromptLabel>::new(0, cx, |mouse_state, _| {
Label::new(
"Sign in to use chat".to_string(),
if mouse_state.hovered {
theme.chat_panel.hovered_sign_in_prompt.clone()
} else {
theme.chat_panel.sign_in_prompt.clone()
},
)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
let rpc = rpc.clone();
let this = this.clone();
cx.spawn(|mut cx| async move {
if rpc
.authenticate_and_connect(true, &cx)
.log_err()
.await
.is_some()
{
cx.update(|cx| {
if let Some(this) = this.upgrade(cx) {
if this.is_focused(cx) {
this.update(cx, |this, cx| cx.focus(&this.input_editor));
}
}
})
}
})
.detach();
})
.boxed(),
)
.boxed()
}
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if let Some((channel, _)) = self.active_channel.as_ref() {
let body = self.input_editor.update(cx, |editor, cx| {
let body = editor.text(cx);
editor.clear(cx);
body
});
if let Some(task) = channel
.update(cx, |channel, cx| channel.send_message(body, cx))
.log_err()
{
task.detach();
}
}
}
fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
if let Some((channel, _)) = self.active_channel.as_ref() {
channel.update(cx, |channel, cx| {
channel.load_more_messages(cx);
})
}
}
}
impl Entity for ChatPanel {
type Event = Event;
}
impl View for ChatPanel {
fn ui_name() -> &'static str {
"ChatPanel"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let element = if self.rpc.user_id().is_some() {
self.render_channel(cx)
} else {
self.render_sign_in_prompt(cx)
};
let theme = &cx.global::<Settings>().theme;
ConstrainedBox::new(
Container::new(element)
.with_style(theme.chat_panel.container)
.boxed(),
)
.with_min_width(150.)
.boxed()
}
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if matches!(
*self.rpc.status().borrow(),
client::Status::Connected { .. }
) {
cx.focus(&self.input_editor);
}
}
}
fn format_timestamp(
mut timestamp: OffsetDateTime,
mut now: OffsetDateTime,
local_timezone: UtcOffset,
) -> String {
timestamp = timestamp.to_offset(local_timezone);
now = now.to_offset(local_timezone);
let today = now.date();
let date = timestamp.date();
let mut hour = timestamp.hour();
let mut part = "am";
if hour > 12 {
hour -= 12;
part = "pm";
}
if date == today {
format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
} else if date.next_day() == Some(today) {
format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
} else {
format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
}
}

View File

@@ -35,9 +35,11 @@ tiny_http = "0.8"
uuid = { version = "1.1.2", features = ["v4"] }
url = "2.2"
serde = { version = "*", features = ["derive"] }
settings = { path = "../settings" }
tempfile = "3"
[dev-dependencies]
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }

View File

@@ -0,0 +1,277 @@
use crate::http::HttpClient;
use db::Db;
use gpui::{
executor::Background,
serde_json::{self, value::Map, Value},
AppContext, Task,
};
use isahc::Request;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Serialize;
use serde_json::json;
use std::{
io::Write,
mem,
path::PathBuf,
sync::Arc,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tempfile::NamedTempFile;
use util::{post_inc, ResultExt, TryFutureExt};
use uuid::Uuid;
pub struct AmplitudeTelemetry {
http_client: Arc<dyn HttpClient>,
executor: Arc<Background>,
session_id: u128,
state: Mutex<AmplitudeTelemetryState>,
}
#[derive(Default)]
struct AmplitudeTelemetryState {
metrics_id: Option<Arc<str>>,
device_id: Option<Arc<str>>,
app_version: Option<Arc<str>>,
os_version: Option<Arc<str>>,
os_name: &'static str,
queue: Vec<AmplitudeEvent>,
next_event_id: usize,
flush_task: Option<Task<()>>,
log_file: Option<NamedTempFile>,
}
const AMPLITUDE_EVENTS_URL: &'static str = "https://api2.amplitude.com/batch";
lazy_static! {
static ref AMPLITUDE_API_KEY: Option<String> = std::env::var("ZED_AMPLITUDE_API_KEY")
.ok()
.or_else(|| option_env!("ZED_AMPLITUDE_API_KEY").map(|key| key.to_string()));
}
#[derive(Serialize)]
struct AmplitudeEventBatch {
api_key: &'static str,
events: Vec<AmplitudeEvent>,
}
#[derive(Serialize)]
struct AmplitudeEvent {
#[serde(skip_serializing_if = "Option::is_none")]
user_id: Option<Arc<str>>,
device_id: Option<Arc<str>>,
event_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
event_properties: Option<Map<String, Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
user_properties: Option<Map<String, Value>>,
os_name: &'static str,
os_version: Option<Arc<str>>,
app_version: Option<Arc<str>>,
platform: &'static str,
event_id: usize,
session_id: u128,
time: u128,
}
#[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 AmplitudeTelemetry {
pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
let platform = cx.platform();
let this = Arc::new(Self {
http_client: client,
executor: cx.background().clone(),
session_id: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
state: Mutex::new(AmplitudeTelemetryState {
os_version: platform
.os_version()
.log_err()
.map(|v| v.to_string().into()),
os_name: platform.os_name().into(),
app_version: platform
.app_version()
.log_err()
.map(|v| v.to_string().into()),
device_id: None,
queue: Default::default(),
flush_task: Default::default(),
next_event_id: 0,
log_file: None,
metrics_id: None,
}),
});
if AMPLITUDE_API_KEY.is_some() {
this.executor
.spawn({
let this = this.clone();
async move {
if let Some(tempfile) = NamedTempFile::new().log_err() {
this.state.lock().log_file = Some(tempfile);
}
}
})
.detach();
}
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>, db: Db) {
let this = self.clone();
self.executor
.spawn(
async move {
let device_id = if let Ok(Some(device_id)) = db.read_kvp("device_id") {
device_id
} else {
let device_id = Uuid::new_v4().to_string();
db.write_kvp("device_id", &device_id)?;
device_id
};
let device_id = Some(Arc::from(device_id));
let mut state = this.state.lock();
state.device_id = device_id.clone();
for event in &mut state.queue {
event.device_id = device_id.clone();
}
if !state.queue.is_empty() {
drop(state);
this.flush();
}
anyhow::Ok(())
}
.log_err(),
)
.detach();
}
pub fn set_authenticated_user_info(
self: &Arc<Self>,
metrics_id: Option<String>,
is_staff: bool,
) {
let is_signed_in = metrics_id.is_some();
self.state.lock().metrics_id = metrics_id.map(|s| s.into());
if is_signed_in {
self.report_event_with_user_properties(
"$identify",
Default::default(),
json!({ "$set": { "staff": is_staff } }),
)
}
}
pub fn report_event(self: &Arc<Self>, kind: &str, properties: Value) {
self.report_event_with_user_properties(kind, properties, Default::default());
}
fn report_event_with_user_properties(
self: &Arc<Self>,
kind: &str,
properties: Value,
user_properties: Value,
) {
if AMPLITUDE_API_KEY.is_none() {
return;
}
let mut state = self.state.lock();
let event = AmplitudeEvent {
event_type: kind.to_string(),
time: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
session_id: self.session_id,
event_properties: if let Value::Object(properties) = properties {
Some(properties)
} else {
None
},
user_properties: if let Value::Object(user_properties) = user_properties {
Some(user_properties)
} else {
None
},
user_id: state.metrics_id.clone(),
device_id: state.device_id.clone(),
os_name: state.os_name,
platform: "Zed",
os_version: state.os_version.clone(),
app_version: state.app_version.clone(),
event_id: post_inc(&mut state.next_event_id),
};
state.queue.push(event);
if state.device_id.is_some() {
if state.queue.len() >= MAX_QUEUE_LEN {
drop(state);
self.flush();
} else {
let this = self.clone();
let executor = self.executor.clone();
state.flush_task = Some(self.executor.spawn(async move {
executor.timer(DEBOUNCE_INTERVAL).await;
this.flush();
}));
}
}
}
fn flush(self: &Arc<Self>) {
let mut state = self.state.lock();
let events = mem::take(&mut state.queue);
state.flush_task.take();
drop(state);
if let Some(api_key) = AMPLITUDE_API_KEY.as_ref() {
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 &events {
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, event)?;
file.write_all(&json_bytes)?;
file.write(b"\n")?;
}
}
let batch = AmplitudeEventBatch { api_key, events };
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, &batch)?;
let request =
Request::post(AMPLITUDE_EVENTS_URL).body(json_bytes.into())?;
this.http_client.send(request).await?;
Ok(())
}
.log_err(),
)
.detach();
}
}
}

View File

@@ -1,11 +1,13 @@
#[cfg(any(test, feature = "test-support"))]
pub mod test;
pub mod amplitude_telemetry;
pub mod channel;
pub mod http;
pub mod telemetry;
pub mod user;
use amplitude_telemetry::AmplitudeTelemetry;
use anyhow::{anyhow, Context, Result};
use async_recursion::async_recursion;
use async_tungstenite::tungstenite::{
@@ -13,11 +15,13 @@ use async_tungstenite::tungstenite::{
http::{Request, StatusCode},
};
use db::Db;
use futures::{future::LocalBoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt};
use futures::{future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryStreamExt};
use gpui::{
actions, serde_json::Value, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle,
AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
MutableAppContext, Task, View, ViewContext, ViewHandle,
actions,
serde_json::{self, Value},
AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext,
AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext,
ViewHandle,
};
use http::HttpClient;
use lazy_static::lazy_static;
@@ -25,6 +29,8 @@ use parking_lot::RwLock;
use postage::watch;
use rand::prelude::*;
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage};
use serde::Deserialize;
use settings::ReleaseChannel;
use std::{
any::TypeId,
collections::HashMap,
@@ -50,6 +56,9 @@ lazy_static! {
pub static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
.ok()
.and_then(|s| if s.is_empty() { None } else { Some(s) });
pub static ref ADMIN_API_TOKEN: Option<String> = std::env::var("ZED_ADMIN_API_TOKEN")
.ok()
.and_then(|s| if s.is_empty() { None } else { Some(s) });
}
pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
@@ -76,6 +85,7 @@ pub struct Client {
peer: Arc<Peer>,
http: Arc<dyn HttpClient>,
telemetry: Arc<Telemetry>,
amplitude_telemetry: Arc<AmplitudeTelemetry>,
state: RwLock<ClientState>,
#[allow(clippy::type_complexity)]
@@ -143,11 +153,16 @@ pub enum Status {
Authenticating,
Connecting,
ConnectionError,
Connected { connection_id: ConnectionId },
Connected {
peer_id: PeerId,
connection_id: ConnectionId,
},
ConnectionLost,
Reauthenticating,
Reconnecting,
ReconnectionError { next_reconnection: Instant },
ReconnectionError {
next_reconnection: Instant,
},
}
impl Status {
@@ -250,6 +265,7 @@ impl Client {
id: 0,
peer: Peer::new(),
telemetry: Telemetry::new(http.clone(), cx),
amplitude_telemetry: AmplitudeTelemetry::new(http.clone(), cx),
http,
state: Default::default(),
@@ -314,6 +330,14 @@ impl Client {
.map(|credentials| credentials.user_id)
}
pub fn peer_id(&self) -> Option<PeerId> {
if let Status::Connected { peer_id, .. } = &*self.status().borrow() {
Some(*peer_id)
} else {
None
}
}
pub fn status(&self) -> watch::Receiver<Status> {
self.state.read().status.1.clone()
}
@@ -354,6 +378,8 @@ impl Client {
}
Status::SignedOut | Status::UpgradeRequired => {
self.telemetry.set_authenticated_user_info(None, false);
self.amplitude_telemetry
.set_authenticated_user_info(None, false);
state._reconnect_task.take();
}
_ => {}
@@ -663,6 +689,7 @@ impl Client {
self.set_status(Status::Reconnecting, cx);
}
let mut timeout = cx.background().timer(CONNECTION_TIMEOUT).fuse();
futures::select_biased! {
connection = self.establish_connection(&credentials, cx).fuse() => {
match connection {
@@ -671,8 +698,14 @@ impl Client {
if !read_from_keychain && IMPERSONATE_LOGIN.is_none() {
write_credentials_to_keychain(&credentials, cx).log_err();
}
self.set_connection(conn, cx);
Ok(())
futures::select_biased! {
result = self.set_connection(conn, cx).fuse() => result,
_ = timeout => {
self.set_status(Status::ConnectionError, cx);
Err(anyhow!("timed out waiting on hello message from server"))
}
}
}
Err(EstablishConnectionError::Unauthorized) => {
self.state.write().credentials.take();
@@ -695,21 +728,65 @@ impl Client {
}
}
}
_ = cx.background().timer(CONNECTION_TIMEOUT).fuse() => {
_ = &mut timeout => {
self.set_status(Status::ConnectionError, cx);
Err(anyhow!("timed out trying to establish connection"))
}
}
}
fn set_connection(self: &Arc<Self>, conn: Connection, cx: &AsyncAppContext) {
async fn set_connection(
self: &Arc<Self>,
conn: Connection,
cx: &AsyncAppContext,
) -> Result<()> {
let executor = cx.background();
log::info!("add connection to peer");
let (connection_id, handle_io, mut incoming) = self
.peer
.add_connection(conn, move |duration| executor.timer(duration));
log::info!("set status to connected {}", connection_id);
self.set_status(Status::Connected { connection_id }, cx);
let handle_io = cx.background().spawn(handle_io);
let peer_id = async {
log::info!("waiting for server hello");
let message = incoming
.next()
.await
.ok_or_else(|| anyhow!("no hello message received"))?;
log::info!("got server hello");
let hello_message_type_name = message.payload_type_name().to_string();
let hello = message
.into_any()
.downcast::<TypedEnvelope<proto::Hello>>()
.map_err(|_| {
anyhow!(
"invalid hello message received: {:?}",
hello_message_type_name
)
})?;
Ok(PeerId(hello.payload.peer_id))
};
let peer_id = match peer_id.await {
Ok(peer_id) => peer_id,
Err(error) => {
self.peer.disconnect(connection_id);
return Err(error);
}
};
log::info!(
"set status to connected (connection id: {}, peer id: {})",
connection_id,
peer_id
);
self.set_status(
Status::Connected {
peer_id,
connection_id,
},
cx,
);
cx.foreground()
.spawn({
let cx = cx.clone();
@@ -807,14 +884,18 @@ impl Client {
})
.detach();
let handle_io = cx.background().spawn(handle_io);
let this = self.clone();
let cx = cx.clone();
cx.foreground()
.spawn(async move {
match handle_io.await {
Ok(()) => {
if *this.status().borrow() == (Status::Connected { connection_id }) {
if *this.status().borrow()
== (Status::Connected {
connection_id,
peer_id,
})
{
this.set_status(Status::SignedOut, &cx);
}
}
@@ -825,6 +906,8 @@ impl Client {
}
})
.detach();
Ok(())
}
fn authenticate(self: &Arc<Self>, cx: &AsyncAppContext) -> Task<Result<Credentials>> {
@@ -849,11 +932,51 @@ impl Client {
self.establish_websocket_connection(credentials, cx)
}
async fn get_rpc_url(http: Arc<dyn HttpClient>, is_preview: bool) -> Result<Url> {
let preview_param = if is_preview { "?preview=1" } else { "" };
let url = format!("{}/rpc{preview_param}", *ZED_SERVER_URL);
let response = http.get(&url, Default::default(), false).await?;
// Normally, ZED_SERVER_URL is set to the URL of zed.dev website.
// The website's /rpc endpoint redirects to a collab server's /rpc endpoint,
// which requires authorization via an HTTP header.
//
// For testing purposes, ZED_SERVER_URL can also set to the direct URL of
// of a collab server. In that case, a request to the /rpc endpoint will
// return an 'unauthorized' response.
let collab_url = if response.status().is_redirection() {
response
.headers()
.get("Location")
.ok_or_else(|| anyhow!("missing location header in /rpc response"))?
.to_str()
.map_err(EstablishConnectionError::other)?
.to_string()
} else if response.status() == StatusCode::UNAUTHORIZED {
url
} else {
Err(anyhow!(
"unexpected /rpc response status {}",
response.status()
))?
};
Url::parse(&collab_url).context("invalid rpc url")
}
fn establish_websocket_connection(
self: &Arc<Self>,
credentials: &Credentials,
cx: &AsyncAppContext,
) -> Task<Result<Connection, EstablishConnectionError>> {
let is_preview = cx.read(|cx| {
if cx.has_global::<ReleaseChannel>() {
*cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
} else {
false
}
});
let request = Request::builder()
.header(
"Authorization",
@@ -863,28 +986,7 @@ impl Client {
let http = self.http.clone();
cx.background().spawn(async move {
let mut rpc_url = format!("{}/rpc", *ZED_SERVER_URL);
let rpc_response = http.get(&rpc_url, Default::default(), false).await?;
if rpc_response.status().is_redirection() {
rpc_url = rpc_response
.headers()
.get("Location")
.ok_or_else(|| anyhow!("missing location header in /rpc response"))?
.to_str()
.map_err(EstablishConnectionError::other)?
.to_string();
}
// Until we switch the zed.dev domain to point to the new Next.js app, there
// will be no redirect required, and the app will connect directly to
// wss://zed.dev/rpc.
else if rpc_response.status() != StatusCode::UPGRADE_REQUIRED {
Err(anyhow!(
"unexpected /rpc response status {}",
rpc_response.status()
))?
}
let mut rpc_url = Url::parse(&rpc_url).context("invalid rpc url")?;
let mut rpc_url = Self::get_rpc_url(http, is_preview).await?;
let rpc_host = rpc_url
.host_str()
.zip(rpc_url.port_or_known_default())
@@ -927,6 +1029,8 @@ impl Client {
let platform = cx.platform();
let executor = cx.background();
let telemetry = self.telemetry.clone();
let amplitude_telemetry = self.amplitude_telemetry.clone();
let http = self.http.clone();
executor.clone().spawn(async move {
// Generate a pair of asymmetric encryption keys. The public key will be used by the
// zed server to encrypt the user's access token, so that it can'be intercepted by
@@ -936,6 +1040,10 @@ impl Client {
let public_key_string =
String::try_from(public_key).expect("failed to serialize public key for auth");
if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) {
return Self::authenticate_as_admin(http, login.clone(), token.clone()).await;
}
// Start an HTTP server to receive the redirect from Zed's sign-in page.
let server = tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port");
let port = server.server_addr().port();
@@ -1006,6 +1114,7 @@ impl Client {
platform.activate(true);
telemetry.report_event("authenticate with browser", Default::default());
amplitude_telemetry.report_event("authenticate with browser", Default::default());
Ok(Credentials {
user_id: user_id.parse()?,
@@ -1014,6 +1123,50 @@ impl Client {
})
}
async fn authenticate_as_admin(
http: Arc<dyn HttpClient>,
login: String,
mut api_token: String,
) -> Result<Credentials> {
#[derive(Deserialize)]
struct AuthenticatedUserResponse {
user: User,
}
#[derive(Deserialize)]
struct User {
id: u64,
}
// Use the collab server's admin API to retrieve the id
// of the impersonated user.
let mut url = Self::get_rpc_url(http.clone(), false).await?;
url.set_path("/user");
url.set_query(Some(&format!("github_login={login}")));
let request = Request::get(url.as_str())
.header("Authorization", format!("token {api_token}"))
.body("".into())?;
let mut response = http.send(request).await?;
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
if !response.status().is_success() {
Err(anyhow!(
"admin user request failed {} - {}",
response.status().as_u16(),
body,
))?;
}
let response: AuthenticatedUserResponse = serde_json::from_str(&body)?;
// Use the admin API token to authenticate as the impersonated user.
api_token.insert_str(0, "ADMIN_TOKEN:");
Ok(Credentials {
user_id: response.user.id,
access_token: api_token,
})
}
pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
let conn_id = self.connection_id()?;
self.peer.disconnect(conn_id);
@@ -1072,15 +1225,18 @@ impl Client {
self.peer.respond_with_error(receipt, error)
}
pub fn start_telemetry(&self, db: Arc<Db>) {
self.telemetry.start(db);
pub fn start_telemetry(&self, db: Db) {
self.telemetry.start(db.clone());
self.amplitude_telemetry.start(db);
}
pub fn report_event(&self, kind: &str, properties: Value) {
self.telemetry.report_event(kind, properties)
self.telemetry.report_event(kind, properties.clone());
self.amplitude_telemetry.report_event(kind, properties);
}
pub fn telemetry_log_file_path(&self) -> Option<PathBuf> {
self.amplitude_telemetry.log_file_path();
self.telemetry.log_file_path()
}
}

View File

@@ -24,7 +24,6 @@ use uuid::Uuid;
pub struct Telemetry {
http_client: Arc<dyn HttpClient>,
executor: Arc<Background>,
session_id: u128,
state: Mutex<TelemetryState>,
}
@@ -35,43 +34,54 @@ struct TelemetryState {
app_version: Option<Arc<str>>,
os_version: Option<Arc<str>>,
os_name: &'static str,
queue: Vec<AmplitudeEvent>,
queue: Vec<MixpanelEvent>,
next_event_id: usize,
flush_task: Option<Task<()>>,
log_file: Option<NamedTempFile>,
}
const AMPLITUDE_EVENTS_URL: &'static str = "https://api2.amplitude.com/batch";
const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track";
const MIXPANEL_ENGAGE_URL: &'static str = "https://api.mixpanel.com/engage#profile-set";
lazy_static! {
static ref AMPLITUDE_API_KEY: Option<String> = std::env::var("ZED_AMPLITUDE_API_KEY")
static ref MIXPANEL_TOKEN: Option<String> = std::env::var("ZED_MIXPANEL_TOKEN")
.ok()
.or_else(|| option_env!("ZED_AMPLITUDE_API_KEY").map(|key| key.to_string()));
.or_else(|| option_env!("ZED_MIXPANEL_TOKEN").map(|key| key.to_string()));
}
#[derive(Serialize)]
struct AmplitudeEventBatch {
api_key: &'static str,
events: Vec<AmplitudeEvent>,
#[derive(Serialize, Debug)]
struct MixpanelEvent {
event: String,
properties: MixpanelEventProperties,
}
#[derive(Serialize)]
struct AmplitudeEvent {
#[serde(skip_serializing_if = "Option::is_none")]
user_id: Option<Arc<str>>,
device_id: Option<Arc<str>>,
event_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[derive(Serialize, Debug)]
struct MixpanelEventProperties {
// Mixpanel required fields
#[serde(skip_serializing_if = "str::is_empty")]
token: &'static str,
time: u128,
distinct_id: Option<Arc<str>>,
#[serde(rename = "$insert_id")]
insert_id: usize,
// Custom fields
#[serde(skip_serializing_if = "Option::is_none", flatten)]
event_properties: Option<Map<String, Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
user_properties: Option<Map<String, Value>>,
os_name: &'static str,
os_version: Option<Arc<str>>,
app_version: Option<Arc<str>>,
signed_in: bool,
platform: &'static str,
event_id: usize,
session_id: u128,
time: u128,
}
#[derive(Serialize)]
struct MixpanelEngageRequest {
#[serde(rename = "$token")]
token: &'static str,
#[serde(rename = "$distinct_id")]
distinct_id: Arc<str>,
#[serde(rename = "$set")]
set: Value,
}
#[cfg(debug_assertions)]
@@ -92,10 +102,6 @@ impl Telemetry {
let this = Arc::new(Self {
http_client: client,
executor: cx.background().clone(),
session_id: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
state: Mutex::new(TelemetryState {
os_version: platform
.os_version()
@@ -107,15 +113,15 @@ impl Telemetry {
.log_err()
.map(|v| v.to_string().into()),
device_id: None,
metrics_id: None,
queue: Default::default(),
flush_task: Default::default(),
next_event_id: 0,
log_file: None,
metrics_id: None,
}),
});
if AMPLITUDE_API_KEY.is_some() {
if MIXPANEL_TOKEN.is_some() {
this.executor
.spawn({
let this = this.clone();
@@ -135,30 +141,27 @@ impl Telemetry {
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
}
pub fn start(self: &Arc<Self>, db: Arc<Db>) {
pub fn start(self: &Arc<Self>, db: Db) {
let this = self.clone();
self.executor
.spawn(
async move {
let device_id = if let Some(device_id) = db
.read(["device_id"])?
.into_iter()
.flatten()
.next()
.and_then(|bytes| String::from_utf8(bytes).ok())
{
let device_id = if let Ok(Some(device_id)) = db.read_kvp("device_id") {
device_id
} else {
let device_id = Uuid::new_v4().to_string();
db.write([("device_id", device_id.as_bytes())])?;
db.write_kvp("device_id", &device_id)?;
device_id
};
let device_id = Some(Arc::from(device_id));
let device_id: Arc<str> = device_id.into();
let mut state = this.state.lock();
state.device_id = device_id.clone();
state.device_id = Some(device_id.clone());
for event in &mut state.queue {
event.device_id = device_id.clone();
event
.properties
.distinct_id
.get_or_insert_with(|| device_id.clone());
}
if !state.queue.is_empty() {
drop(state);
@@ -177,56 +180,57 @@ impl Telemetry {
metrics_id: Option<String>,
is_staff: bool,
) {
let is_signed_in = metrics_id.is_some();
self.state.lock().metrics_id = metrics_id.map(|s| s.into());
if is_signed_in {
self.report_event_with_user_properties(
"$identify",
Default::default(),
json!({ "$set": { "staff": is_staff } }),
)
let this = self.clone();
let mut state = self.state.lock();
let device_id = state.device_id.clone();
let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
state.metrics_id = metrics_id.clone();
drop(state);
if let Some((token, device_id)) = MIXPANEL_TOKEN.as_ref().zip(device_id) {
self.executor
.spawn(
async move {
let json_bytes = serde_json::to_vec(&[MixpanelEngageRequest {
token,
distinct_id: device_id,
set: json!({ "staff": is_staff, "id": metrics_id }),
}])?;
let request = Request::post(MIXPANEL_ENGAGE_URL)
.header("Content-Type", "application/json")
.body(json_bytes.into())?;
this.http_client.send(request).await?;
Ok(())
}
.log_err(),
)
.detach();
}
}
pub fn report_event(self: &Arc<Self>, kind: &str, properties: Value) {
self.report_event_with_user_properties(kind, properties, Default::default());
}
fn report_event_with_user_properties(
self: &Arc<Self>,
kind: &str,
properties: Value,
user_properties: Value,
) {
if AMPLITUDE_API_KEY.is_none() {
return;
}
let mut state = self.state.lock();
let event = AmplitudeEvent {
event_type: kind.to_string(),
time: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
session_id: self.session_id,
event_properties: if let Value::Object(properties) = properties {
Some(properties)
} else {
None
let event = MixpanelEvent {
event: kind.to_string(),
properties: MixpanelEventProperties {
token: "",
time: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
distinct_id: state.device_id.clone(),
insert_id: post_inc(&mut state.next_event_id),
event_properties: if let Value::Object(properties) = properties {
Some(properties)
} else {
None
},
os_name: state.os_name,
os_version: state.os_version.clone(),
app_version: state.app_version.clone(),
signed_in: state.metrics_id.is_some(),
platform: "Zed",
},
user_properties: if let Value::Object(user_properties) = user_properties {
Some(user_properties)
} else {
None
},
user_id: state.metrics_id.clone(),
device_id: state.device_id.clone(),
os_name: state.os_name,
platform: "Zed",
os_version: state.os_version.clone(),
app_version: state.app_version.clone(),
event_id: post_inc(&mut state.next_event_id),
};
state.queue.push(event);
if state.device_id.is_some() {
@@ -246,11 +250,11 @@ impl Telemetry {
fn flush(self: &Arc<Self>) {
let mut state = self.state.lock();
let events = mem::take(&mut state.queue);
let mut events = mem::take(&mut state.queue);
state.flush_task.take();
drop(state);
if let Some(api_key) = AMPLITUDE_API_KEY.as_ref() {
if let Some(token) = MIXPANEL_TOKEN.as_ref() {
let this = self.clone();
self.executor
.spawn(
@@ -259,19 +263,21 @@ impl Telemetry {
if let Some(file) = &mut this.state.lock().log_file {
let file = file.as_file_mut();
for event in &events {
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")?;
event.properties.token = token;
}
}
let batch = AmplitudeEventBatch { api_key, events };
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, &batch)?;
let request =
Request::post(AMPLITUDE_EVENTS_URL).body(json_bytes.into())?;
serde_json::to_writer(&mut json_bytes, &events)?;
let request = Request::post(MIXPANEL_EVENTS_URL)
.header("Content-Type", "application/json")
.body(json_bytes.into())?;
this.http_client.send(request).await?;
Ok(())
}

View File

@@ -84,9 +84,19 @@ impl FakeServer {
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);
{
let mut state = state.lock();
state.connection_id = Some(connection_id);
state.incoming = Some(incoming);
}
peer.send(
connection_id,
proto::Hello {
peer_id: connection_id.0,
},
)
.unwrap();
Ok(client_conn)
})
}

View File

@@ -143,13 +143,24 @@ impl UserStore {
let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
if let Some(info) = info {
client.telemetry.set_authenticated_user_info(
Some(info.metrics_id.clone()),
info.staff,
);
client.amplitude_telemetry.set_authenticated_user_info(
Some(info.metrics_id),
info.staff,
);
} else {
client.telemetry.set_authenticated_user_info(None, false);
client
.amplitude_telemetry
.set_authenticated_user_info(None, false);
}
client.telemetry.report_event("sign in", Default::default());
client
.amplitude_telemetry
.report_event("sign in", Default::default());
current_user_tx.send(user).await.ok();
}
}

View File

@@ -2,8 +2,9 @@ DATABASE_URL = "postgres://postgres@localhost/zed"
HTTP_PORT = 8080
API_TOKEN = "secret"
INVITE_LINK_PREFIX = "http://localhost:3000/invites/"
LIVE_KIT_SERVER = "http://localhost:7880"
LIVE_KIT_KEY = "devkey"
LIVE_KIT_SECRET = "secret"
# HONEYCOMB_API_KEY=
# HONEYCOMB_DATASET=
# RUST_LOG=info
# LOG_JSON=true

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
version = "0.1.0"
version = "0.2.0"
[[bin]]
name = "collab"
@@ -14,8 +14,10 @@ required-features = ["seed-support"]
[dependencies]
collections = { path = "../collections" }
live_kit_server = { path = "../live_kit_server" }
rpc = { path = "../rpc" }
util = { path = "../util" }
anyhow = "1.0.40"
async-trait = "0.1.50"
async-tungstenite = "0.16"
@@ -60,15 +62,17 @@ editor = { path = "../editor", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
fs = { path = "../fs", features = ["test-support"] }
git = { path = "../git", features = ["test-support"] }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
theme = { path = "../theme" }
workspace = { path = "../workspace", features = ["test-support"] }
ctor = "0.1"
env_logger = "0.9"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
util = { path = "../util" }
lazy_static = "1.4"
serde_json = { version = "1.0", features = ["preserve_order"] }

View File

@@ -1,2 +0,0 @@
collab: ./target/release/collab
release: ./target/release/sqlx migrate run

View File

@@ -0,0 +1,3 @@
ZED_ENVIRONMENT=preview
RUST_LOG=info
INVITE_LINK_PREFIX=https://zed.dev/invites/

View File

@@ -11,7 +11,7 @@ metadata:
name: collab
annotations:
service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443"
service.beta.kubernetes.io/do-loadbalancer-certificate-id: "40879815-9a6b-4bbb-8207-8f2c7c0218f9"
service.beta.kubernetes.io/do-loadbalancer-certificate-id: "08d9d8ce-761f-4ab3-bc78-4923ab5b0e33"
spec:
type: LoadBalancer
selector:
@@ -54,6 +54,8 @@ spec:
containers:
- name: collab
image: "${ZED_IMAGE_ID}"
args:
- serve
ports:
- containerPort: 8080
protocol: TCP
@@ -65,49 +67,32 @@ spec:
secretKeyRef:
name: database
key: url
- name: SESSION_SECRET
valueFrom:
secretKeyRef:
name: session
key: secret
- name: GITHUB_APP_ID
valueFrom:
secretKeyRef:
name: github
key: appId
- name: GITHUB_CLIENT_ID
valueFrom:
secretKeyRef:
name: github
key: clientId
- name: GITHUB_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: github
key: clientSecret
- name: GITHUB_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: github
key: privateKey
- name: API_TOKEN
valueFrom:
secretKeyRef:
name: api
key: token
- name: LIVE_KIT_SERVER
valueFrom:
secretKeyRef:
name: livekit
key: server
- name: LIVE_KIT_KEY
valueFrom:
secretKeyRef:
name: livekit
key: key
- name: LIVE_KIT_SECRET
valueFrom:
secretKeyRef:
name: livekit
key: secret
- name: INVITE_LINK_PREFIX
value: ${INVITE_LINK_PREFIX}
- name: RUST_LOG
value: ${RUST_LOG}
- name: LOG_JSON
value: "true"
- name: HONEYCOMB_DATASET
value: "collab"
- name: HONEYCOMB_API_KEY
valueFrom:
secretKeyRef:
name: honeycomb
key: apiKey
securityContext:
capabilities:
# FIXME - Switch to the more restrictive `PERFMON` capability.

View File

@@ -9,7 +9,10 @@ spec:
restartPolicy: Never
containers:
- name: migrator
imagePullPolicy: Always
image: ${ZED_IMAGE_ID}
args:
- migrate
env:
- name: DATABASE_URL
valueFrom:

View File

@@ -22,7 +22,7 @@ use time::OffsetDateTime;
use tower::ServiceBuilder;
use tracing::instrument;
pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
pub fn routes(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
Router::new()
.route("/user", get(get_authenticated_user))
.route("/users", get(get_users).post(create_user))
@@ -50,7 +50,7 @@ pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Bod
.layer(
ServiceBuilder::new()
.layer(Extension(state))
.layer(Extension(rpc_server.clone()))
.layer(Extension(rpc_server))
.layer(middleware::from_fn(validate_api_token)),
)
}
@@ -76,7 +76,7 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
let state = req.extensions().get::<Arc<AppState>>().unwrap();
if token != state.api_token {
if token != state.config.api_token {
Err(Error::Http(
StatusCode::UNAUTHORIZED,
"invalid authorization token".to_string(),
@@ -88,7 +88,7 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
#[derive(Debug, Deserialize)]
struct AuthenticatedUserParams {
github_user_id: i32,
github_user_id: Option<i32>,
github_login: String,
}
@@ -104,7 +104,7 @@ async fn get_authenticated_user(
) -> Result<Json<AuthenticatedUserResponse>> {
let user = app
.db
.get_user_by_github_account(&params.github_login, Some(params.github_user_id))
.get_user_by_github_account(&params.github_login, params.github_user_id)
.await?
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "user not found".into()))?;
let metrics_id = app.db.get_user_metrics_id(user.id).await?;
@@ -156,7 +156,7 @@ async fn create_user(
Json(params): Json<CreateUserParams>,
Extension(app): Extension<Arc<AppState>>,
Extension(rpc_server): Extension<Arc<rpc::Server>>,
) -> Result<Json<CreateUserResponse>> {
) -> Result<Json<Option<CreateUserResponse>>> {
let user = NewUserParams {
github_login: params.github_login,
github_user_id: params.github_user_id,
@@ -165,7 +165,8 @@ async fn create_user(
// Creating a user via the normal signup process
let result = if let Some(email_confirmation_code) = params.email_confirmation_code {
app.db
if let Some(result) = app
.db
.create_user_from_invite(
&Invite {
email_address: params.email_address,
@@ -174,6 +175,11 @@ async fn create_user(
user,
)
.await?
{
result
} else {
return Ok(Json(None));
}
}
// Creating a user as an admin
else if params.admin {
@@ -200,11 +206,11 @@ async fn create_user(
.await?
.ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
Ok(Json(CreateUserResponse {
Ok(Json(Some(CreateUserResponse {
user,
metrics_id: result.metrics_id,
signup_device_id: result.signup_device_id,
}))
})))
}
#[derive(Deserialize)]

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use super::db::{self, UserId};
use crate::{AppState, Error, Result};
use crate::{
db::{self, UserId},
AppState, Error, Result,
};
use anyhow::{anyhow, Context};
use axum::{
http::{self, Request, StatusCode},
@@ -13,6 +13,7 @@ use scrypt::{
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Scrypt,
};
use std::sync::Arc;
pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl IntoResponse {
let mut auth_header = req
@@ -21,7 +22,7 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
.and_then(|header| header.to_str().ok())
.ok_or_else(|| {
Error::Http(
StatusCode::BAD_REQUEST,
StatusCode::UNAUTHORIZED,
"missing authorization header".to_string(),
)
})?
@@ -41,12 +42,18 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
)
})?;
let state = req.extensions().get::<Arc<AppState>>().unwrap();
let mut credentials_valid = false;
for password_hash in state.db.get_access_token_hashes(user_id).await? {
if verify_access_token(access_token, &password_hash)? {
let state = req.extensions().get::<Arc<AppState>>().unwrap();
if let Some(admin_token) = access_token.strip_prefix("ADMIN_TOKEN:") {
if state.config.api_token == admin_token {
credentials_valid = true;
break;
}
} else {
for password_hash in state.db.get_access_token_hashes(user_id).await? {
if verify_access_token(access_token, &password_hash)? {
credentials_valid = true;
break;
}
}
}

View File

@@ -6,8 +6,12 @@ use collections::HashMap;
use futures::StreamExt;
use serde::{Deserialize, Serialize};
pub use sqlx::postgres::PgPoolOptions as DbOptions;
use sqlx::{types::Uuid, FromRow, QueryBuilder};
use std::{cmp, ops::Range, time::Duration};
use sqlx::{
migrate::{Migrate as _, Migration, MigrationSource},
types::Uuid,
FromRow, QueryBuilder,
};
use std::{cmp, ops::Range, path::Path, time::Duration};
use time::{OffsetDateTime, PrimitiveDateTime};
#[async_trait]
@@ -51,7 +55,7 @@ pub trait Db: Send + Sync {
&self,
invite: &Invite,
user: NewUserParams,
) -> Result<NewUserResult>;
) -> Result<Option<NewUserResult>>;
/// Registers a new project for the given user.
async fn register_project(&self, host_user_id: UserId) -> Result<ProjectId>;
@@ -173,6 +177,13 @@ pub trait Db: Send + Sync {
fn as_fake(&self) -> Option<&FakeDb>;
}
#[cfg(any(test, debug_assertions))]
pub const DEFAULT_MIGRATIONS_PATH: Option<&'static str> =
Some(concat!(env!("CARGO_MANIFEST_DIR"), "/migrations"));
#[cfg(not(any(test, debug_assertions)))]
pub const DEFAULT_MIGRATIONS_PATH: Option<&'static str> = None;
pub struct PostgresDb {
pool: sqlx::PgPool,
}
@@ -187,6 +198,47 @@ impl PostgresDb {
Ok(Self { pool })
}
pub async fn migrate(
&self,
migrations_path: &Path,
ignore_checksum_mismatch: bool,
) -> anyhow::Result<Vec<(Migration, Duration)>> {
let migrations = MigrationSource::resolve(migrations_path)
.await
.map_err(|err| anyhow!("failed to load migrations: {err:?}"))?;
let mut conn = self.pool.acquire().await?;
conn.ensure_migrations_table().await?;
let applied_migrations: HashMap<_, _> = conn
.list_applied_migrations()
.await?
.into_iter()
.map(|m| (m.version, m))
.collect();
let mut new_migrations = Vec::new();
for migration in migrations {
match applied_migrations.get(&migration.version) {
Some(applied_migration) => {
if migration.checksum != applied_migration.checksum && !ignore_checksum_mismatch
{
Err(anyhow!(
"checksum mismatch for applied migration {}",
migration.description
))?;
}
}
None => {
let elapsed = conn.apply(&migration).await?;
new_migrations.push((migration, elapsed));
}
}
}
Ok(new_migrations)
}
pub fn fuzzy_like_string(string: &str) -> String {
let mut result = String::with_capacity(string.len() * 2 + 1);
for c in string.chars() {
@@ -428,7 +480,8 @@ impl Db for PostgresDb {
COUNT(*) as count,
COALESCE(SUM(CASE WHEN platform_linux THEN 1 ELSE 0 END), 0) as linux_count,
COALESCE(SUM(CASE WHEN platform_mac THEN 1 ELSE 0 END), 0) as mac_count,
COALESCE(SUM(CASE WHEN platform_windows THEN 1 ELSE 0 END), 0) as windows_count
COALESCE(SUM(CASE WHEN platform_windows THEN 1 ELSE 0 END), 0) as windows_count,
COALESCE(SUM(CASE WHEN platform_unknown THEN 1 ELSE 0 END), 0) as unknown_count
FROM (
SELECT *
FROM signups
@@ -449,7 +502,7 @@ impl Db for PostgresDb {
FROM signups
WHERE
NOT email_confirmation_sent AND
platform_mac
(platform_mac OR platform_unknown)
LIMIT $1
",
)
@@ -481,7 +534,7 @@ impl Db for PostgresDb {
&self,
invite: &Invite,
user: NewUserParams,
) -> Result<NewUserResult> {
) -> Result<Option<NewUserResult>> {
let mut tx = self.pool.begin().await?;
let (signup_id, existing_user_id, inviting_user_id, signup_device_id): (
@@ -505,10 +558,7 @@ impl Db for PostgresDb {
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "no such invite".to_string()))?;
if existing_user_id.is_some() {
Err(Error::Http(
StatusCode::UNPROCESSABLE_ENTITY,
"invitation already redeemed".to_string(),
))?;
return Ok(None);
}
let (user_id, metrics_id): (UserId, String) = sqlx::query_as(
@@ -517,6 +567,10 @@ impl Db for PostgresDb {
(email_address, github_login, github_user_id, admin, invite_count, invite_code)
VALUES
($1, $2, $3, 'f', $4, $5)
ON CONFLICT (github_login) DO UPDATE SET
email_address = excluded.email_address,
github_user_id = excluded.github_user_id,
admin = excluded.admin
RETURNING id, metrics_id::text
",
)
@@ -566,6 +620,7 @@ impl Db for PostgresDb {
(user_id_a, user_id_b, a_to_b, should_notify, accepted)
VALUES
($1, $2, 't', 't', 't')
ON CONFLICT DO NOTHING
",
)
.bind(inviting_user_id)
@@ -575,12 +630,12 @@ impl Db for PostgresDb {
}
tx.commit().await?;
Ok(NewUserResult {
Ok(Some(NewUserResult {
user_id,
metrics_id,
inviting_user_id,
signup_device_id,
})
}))
}
// invite codes
@@ -1720,6 +1775,8 @@ pub struct WaitlistSummary {
pub mac_count: i64,
#[sqlx(default)]
pub windows_count: i64,
#[sqlx(default)]
pub unknown_count: i64,
}
#[derive(FromRow, PartialEq, Debug, Serialize, Deserialize)]
@@ -1763,11 +1820,8 @@ mod test {
use lazy_static::lazy_static;
use parking_lot::Mutex;
use rand::prelude::*;
use sqlx::{
migrate::{MigrateDatabase, Migrator},
Postgres,
};
use std::{path::Path, sync::Arc};
use sqlx::{migrate::MigrateDatabase, Postgres};
use std::sync::Arc;
use util::post_inc;
pub struct FakeDb {
@@ -1955,7 +2009,7 @@ mod test {
&self,
_invite: &Invite,
_user: NewUserParams,
) -> Result<NewUserResult> {
) -> Result<Option<NewUserResult>> {
unimplemented!()
}
@@ -2430,13 +2484,13 @@ mod test {
let mut rng = StdRng::from_entropy();
let name = format!("zed-test-{}", rng.gen::<u128>());
let url = format!("postgres://postgres@localhost/{}", name);
let migrations_path = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/migrations"));
Postgres::create_database(&url)
.await
.expect("failed to create test db");
let db = PostgresDb::new(&url, 5).await.unwrap();
let migrator = Migrator::new(migrations_path).await.unwrap();
migrator.run(&db.pool).await.unwrap();
db.migrate(Path::new(DEFAULT_MIGRATIONS_PATH.unwrap()), false)
.await
.unwrap();
Self {
db: Some(Arc::new(db)),
url,

View File

@@ -852,6 +852,7 @@ async fn test_invite_codes() {
},
)
.await
.unwrap()
.unwrap();
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
assert_eq!(invite_count, 1);
@@ -897,6 +898,7 @@ async fn test_invite_codes() {
},
)
.await
.unwrap()
.unwrap();
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
assert_eq!(invite_count, 0);
@@ -954,6 +956,7 @@ async fn test_invite_codes() {
)
.await
.unwrap()
.unwrap()
.user_id;
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
@@ -1022,6 +1025,7 @@ async fn test_signups() {
mac_count: 8,
linux_count: 4,
windows_count: 2,
unknown_count: 0,
}
);
@@ -1074,6 +1078,7 @@ async fn test_signups() {
mac_count: 5,
linux_count: 2,
windows_count: 1,
unknown_count: 0,
}
);
@@ -1097,6 +1102,7 @@ async fn test_signups() {
},
)
.await
.unwrap()
.unwrap();
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
assert!(inviting_user_id.is_none());
@@ -1106,19 +1112,21 @@ async fn test_signups() {
assert_eq!(signup_device_id.unwrap(), "device_id_0");
// cannot redeem the same signup again.
db.create_user_from_invite(
&Invite {
email_address: signups_batch1[0].email_address.clone(),
email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
},
NewUserParams {
github_login: "some-other-github_account".into(),
github_user_id: 1,
invite_count: 5,
},
)
.await
.unwrap_err();
assert!(db
.create_user_from_invite(
&Invite {
email_address: signups_batch1[0].email_address.clone(),
email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
},
NewUserParams {
github_login: "some-other-github_account".into(),
github_user_id: 1,
invite_count: 5,
},
)
.await
.unwrap()
.is_none());
// cannot redeem a signup with the wrong confirmation code.
db.create_user_from_invite(

View File

@@ -8,15 +8,18 @@ use anyhow::anyhow;
use call::{room, ActiveCall, ParticipantLocation, Room};
use client::{
self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection,
Credentials, EstablishConnectionError, User, UserStore, RECEIVE_TIMEOUT,
Credentials, EstablishConnectionError, PeerId, User, UserStore, RECEIVE_TIMEOUT,
};
use collections::{BTreeMap, HashMap, HashSet};
use editor::{
self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, ToOffset,
ToggleCodeActions, Undo,
};
use fs::{FakeFs, Fs as _, LineEnding};
use futures::{channel::mpsc, Future, StreamExt as _};
use fs::{FakeFs, Fs as _, HomeDir, LineEnding};
use futures::{
channel::{mpsc, oneshot},
Future, StreamExt as _,
};
use gpui::{
executor::{self, Deterministic},
geometry::vector::vec2f,
@@ -27,14 +30,13 @@ use language::{
range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
LanguageConfig, LanguageRegistry, OffsetRangeExt, Point, Rope,
};
use live_kit_client::MacOSDisplay;
use lsp::{self, FakeLanguageServer};
use parking_lot::Mutex;
use project::{
search::SearchQuery, worktree::WorktreeHandle, DiagnosticSummary, Project, ProjectPath,
ProjectStore, WorktreeId,
search::SearchQuery, DiagnosticSummary, Project, ProjectPath, ProjectStore, WorktreeId,
};
use rand::prelude::*;
use rpc::PeerId;
use serde_json::json;
use settings::{Formatter, Settings};
use sqlx::types::time::OffsetDateTime;
@@ -45,14 +47,14 @@ use std::{
path::{Path, PathBuf},
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
Arc,
},
time::Duration,
};
use theme::ThemeRegistry;
use unindent::Unindent as _;
use workspace::{Item, SplitDirection, ToggleFollow, Workspace};
use workspace::{shared_screen::SharedScreen, Item, SplitDirection, ToggleFollow, Workspace};
#[ctor::ctor]
fn init_logger() {
@@ -183,6 +185,37 @@ async fn test_basic_calls(
}
);
// User A shares their screen
let display = MacOSDisplay::new();
let events_b = active_call_events(cx_b);
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
room.set_display_sources(vec![display.clone()]);
room.share_screen(cx)
})
})
.await
.unwrap();
deterministic.run_until_parked();
assert_eq!(events_b.borrow().len(), 1);
let event = events_b.borrow().first().unwrap().clone();
if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event {
assert_eq!(participant_id, client_a.peer_id().unwrap());
room_b.read_with(cx_b, |room, _| {
assert_eq!(
room.remote_participants()[&client_a.peer_id().unwrap()]
.tracks
.len(),
1
);
});
} else {
panic!("unexpected event")
}
// User A leaves the room.
active_call_a.update(cx_a, |call, cx| {
call.hang_up(cx).unwrap();
@@ -204,12 +237,13 @@ async fn test_basic_calls(
}
);
// User B leaves the room.
active_call_b.update(cx_b, |call, cx| {
call.hang_up(cx).unwrap();
assert!(call.room().is_none());
});
deterministic.run_until_parked();
// User B gets disconnected from the LiveKit server, which causes them
// to automatically leave the room.
server
.test_live_kit_server
.disconnect_client(client_b.peer_id().unwrap().to_string())
.await;
active_call_b.update(cx_b, |call, _| assert!(call.room().is_none()));
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
@@ -385,7 +419,7 @@ async fn test_leaving_room_on_disconnection(
);
// When user A disconnects, both client A and B clear their room on the active call.
server.disconnect_client(client_a.current_user_id(cx_a));
server.disconnect_client(client_a.peer_id().unwrap());
cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
active_call_a.read_with(cx_a, |call, _| assert!(call.room().is_none()));
active_call_b.read_with(cx_b, |call, _| assert!(call.room().is_none()));
@@ -403,6 +437,63 @@ async fn test_leaving_room_on_disconnection(
pending: Default::default()
}
);
// Call user B again from client A.
active_call_a
.update(cx_a, |call, cx| {
call.invite(client_b.user_id().unwrap(), None, cx)
})
.await
.unwrap();
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
// User B receives the call and joins the room.
let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
incoming_call_b.next().await.unwrap().unwrap();
active_call_b
.update(cx_b, |call, cx| call.accept_incoming(cx))
.await
.unwrap();
let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
deterministic.run_until_parked();
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
remote: vec!["user_b".to_string()],
pending: Default::default()
}
);
assert_eq!(
room_participants(&room_b, cx_b),
RoomParticipants {
remote: vec!["user_a".to_string()],
pending: Default::default()
}
);
// User B gets disconnected from the LiveKit server, which causes it
// to automatically leave the room.
server
.test_live_kit_server
.disconnect_client(client_b.peer_id().unwrap().to_string())
.await;
deterministic.run_until_parked();
active_call_a.update(cx_a, |call, _| assert!(call.room().is_none()));
active_call_b.update(cx_b, |call, _| assert!(call.room().is_none()));
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
remote: Default::default(),
pending: Default::default()
}
);
assert_eq!(
room_participants(&room_b, cx_b),
RoomParticipants {
remote: Default::default(),
pending: Default::default()
}
);
}
#[gpui::test(iterations = 10)]
@@ -416,7 +507,7 @@ async fn test_calls_on_multiple_connections(
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b1 = server.create_client(cx_b1, "user_b").await;
let _client_b2 = server.create_client(cx_b2, "user_b").await;
let client_b2 = server.create_client(cx_b2, "user_b").await;
server
.make_contacts(&mut [(&client_a, cx_a), (&client_b1, cx_b1)])
.await;
@@ -468,6 +559,14 @@ async fn test_calls_on_multiple_connections(
assert!(incoming_call_b1.next().await.unwrap().is_none());
assert!(incoming_call_b2.next().await.unwrap().is_none());
// User B disconnects the client that is not on the call. Everything should be fine.
client_b1.disconnect(&cx_b1.to_async()).unwrap();
deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
client_b1
.authenticate_and_connect(false, &cx_b1.to_async())
.await
.unwrap();
// User B hangs up, and user A calls them again.
active_call_b2.update(cx_b2, |call, cx| call.hang_up(cx).unwrap());
deterministic.run_until_parked();
@@ -520,11 +619,29 @@ async fn test_calls_on_multiple_connections(
assert!(incoming_call_b1.next().await.unwrap().is_some());
assert!(incoming_call_b2.next().await.unwrap().is_some());
// User A disconnects up, causing both connections to stop ringing.
server.disconnect_client(client_a.current_user_id(cx_a));
cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
// User A disconnects, causing both connections to stop ringing.
server.disconnect_client(client_a.peer_id().unwrap());
deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
assert!(incoming_call_b1.next().await.unwrap().is_none());
assert!(incoming_call_b2.next().await.unwrap().is_none());
// User A reconnects automatically, then calls user B again.
active_call_a
.update(cx_a, |call, cx| {
call.invite(client_b1.user_id().unwrap(), None, cx)
})
.await
.unwrap();
deterministic.run_until_parked();
assert!(incoming_call_b1.next().await.unwrap().is_some());
assert!(incoming_call_b2.next().await.unwrap().is_some());
// User B disconnects all clients, causing user A to no longer see a pending call for them.
server.forbid_connections();
server.disconnect_client(client_b1.peer_id().unwrap());
server.disconnect_client(client_b2.peer_id().unwrap());
deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
active_call_a.read_with(cx_a, |call, _| assert!(call.room().is_none()));
}
#[gpui::test(iterations = 10)]
@@ -582,7 +699,7 @@ async fn test_share_project(
.update(cx_b, |call, cx| call.accept_incoming(cx))
.await
.unwrap();
let client_b_peer_id = client_b.peer_id;
let client_b_peer_id = client_b.peer_id().unwrap();
let project_b = client_b
.build_remote_project(initial_project.id, cx_b)
.await;
@@ -806,8 +923,8 @@ async fn test_host_disconnect(
assert!(cx_b.is_window_edited(workspace_b.window_id()));
// Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
server.disconnect_client(client_a.current_user_id(cx_a));
cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
server.disconnect_client(client_a.peer_id().unwrap());
deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
project_a
.condition(cx_a, |project, _| project.collaborators().is_empty())
.await;
@@ -829,6 +946,29 @@ async fn test_host_disconnect(
.await
.unwrap();
assert!(can_close);
let active_call_b = cx_b.read(ActiveCall::global);
active_call_b
.update(cx_b, |call, cx| {
call.invite(client_a.user_id().unwrap(), None, cx)
})
.await
.unwrap();
deterministic.run_until_parked();
active_call_a
.update(cx_a, |call, cx| call.accept_incoming(cx))
.await
.unwrap();
active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
// Drop client A's connection again. We should still unshare it successfully.
server.disconnect_client(client_a.peer_id().unwrap());
deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
}
#[gpui::test(iterations = 10)]
@@ -903,21 +1043,21 @@ async fn test_active_call_events(
deterministic.run_until_parked();
assert_eq!(mem::take(&mut *events_a.borrow_mut()), vec![]);
assert_eq!(mem::take(&mut *events_b.borrow_mut()), vec![]);
}
fn active_call_events(cx: &mut TestAppContext) -> Rc<RefCell<Vec<room::Event>>> {
let events = Rc::new(RefCell::new(Vec::new()));
let active_call = cx.read(ActiveCall::global);
cx.update({
let events = events.clone();
|cx| {
cx.subscribe(&active_call, move |_, event, _| {
events.borrow_mut().push(event.clone())
})
.detach()
}
});
events
}
fn active_call_events(cx: &mut TestAppContext) -> Rc<RefCell<Vec<room::Event>>> {
let events = Rc::new(RefCell::new(Vec::new()));
let active_call = cx.read(ActiveCall::global);
cx.update({
let events = events.clone();
|cx| {
cx.subscribe(&active_call, move |_, event, _| {
events.borrow_mut().push(event.clone())
})
.detach()
}
});
events
}
#[gpui::test(iterations = 10)]
@@ -933,15 +1073,9 @@ async fn test_room_location(
client_a.fs.insert_tree("/a", json!({})).await;
client_b.fs.insert_tree("/b", 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;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
let active_call_b = cx_b.read(ActiveCall::global);
let a_notified = Rc::new(Cell::new(false));
cx_a.update({
let notified = a_notified.clone();
@@ -951,8 +1085,6 @@ async fn test_room_location(
}
});
let active_call_b = cx_b.read(ActiveCall::global);
let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
let b_notified = Rc::new(Cell::new(false));
cx_b.update({
let b_notified = b_notified.clone();
@@ -962,10 +1094,18 @@ async fn test_room_location(
}
});
room_a
.update(cx_a, |room, cx| room.set_location(Some(&project_a), cx))
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
active_call_a
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
.await
.unwrap();
let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
deterministic.run_until_parked();
assert!(a_notified.take());
assert_eq!(
@@ -1020,8 +1160,8 @@ async fn test_room_location(
)]
);
room_b
.update(cx_b, |room, cx| room.set_location(Some(&project_b), cx))
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
.unwrap();
deterministic.run_until_parked();
@@ -1046,8 +1186,8 @@ async fn test_room_location(
)]
);
room_b
.update(cx_b, |room, cx| room.set_location(None, cx))
active_call_b
.update(cx_b, |call, cx| call.set_location(None, cx))
.await
.unwrap();
deterministic.run_until_parked();
@@ -1087,26 +1227,49 @@ async fn test_room_location(
#[gpui::test(iterations = 10)]
async fn test_propagate_saves_and_fs_changes(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
) {
cx_a.foreground().forbid_parking();
deterministic.forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let rust = Arc::new(Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
));
let javascript = Arc::new(Language::new(
LanguageConfig {
name: "JavaScript".into(),
path_suffixes: vec!["js".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
));
for client in [&client_a, &client_b, &client_c] {
client.language_registry.add(rust.clone());
client.language_registry.add(javascript.clone());
}
client_a
.fs
.insert_tree(
"/a",
json!({
"file1": "",
"file1.rs": "",
"file2": ""
}),
)
@@ -1126,19 +1289,25 @@ async fn test_propagate_saves_and_fs_changes(
// Open and edit a buffer as both guests B and C.
let buffer_b = project_b
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
.await
.unwrap();
let buffer_c = project_c
.update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
.update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
.await
.unwrap();
buffer_b.read_with(cx_b, |buffer, _| {
assert_eq!(&*buffer.language().unwrap().name(), "Rust");
});
buffer_c.read_with(cx_c, |buffer, _| {
assert_eq!(&*buffer.language().unwrap().name(), "Rust");
});
buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], None, cx));
buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], None, cx));
// Open and edit that buffer as the host.
let buffer_a = project_a
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1.rs"), cx))
.await
.unwrap();
@@ -1149,90 +1318,87 @@ async fn test_propagate_saves_and_fs_changes(
buf.edit([(buf.len()..buf.len(), "i-am-a")], None, cx)
});
// Wait for edits to propagate
buffer_a
.condition(cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
.await;
buffer_b
.condition(cx_b, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
.await;
buffer_c
.condition(cx_c, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
.await;
deterministic.run_until_parked();
buffer_a.read_with(cx_a, |buf, _| {
assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
});
buffer_b.read_with(cx_b, |buf, _| {
assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
});
buffer_c.read_with(cx_c, |buf, _| {
assert_eq!(buf.text(), "i-am-c, i-am-b, i-am-a");
});
// Edit the buffer as the host and concurrently save as guest B.
let save_b = buffer_b.update(cx_b, |buf, cx| buf.save(cx));
buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx));
save_b.await.unwrap();
assert_eq!(
client_a.fs.load("/a/file1".as_ref()).await.unwrap(),
client_a.fs.load("/a/file1.rs".as_ref()).await.unwrap(),
"hi-a, i-am-c, i-am-b, i-am-a"
);
deterministic.run_until_parked();
buffer_a.read_with(cx_a, |buf, _| assert!(!buf.is_dirty()));
buffer_b.read_with(cx_b, |buf, _| assert!(!buf.is_dirty()));
buffer_c.condition(cx_c, |buf, _| !buf.is_dirty()).await;
worktree_a.flush_fs_events(cx_a).await;
buffer_c.read_with(cx_c, |buf, _| assert!(!buf.is_dirty()));
// Make changes on host's file system, see those changes on guest worktrees.
client_a
.fs
.rename(
"/a/file1".as_ref(),
"/a/file1-renamed".as_ref(),
"/a/file1.rs".as_ref(),
"/a/file1.js".as_ref(),
Default::default(),
)
.await
.unwrap();
client_a
.fs
.rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default())
.await
.unwrap();
client_a.fs.insert_file("/a/file4", "4".into()).await;
deterministic.run_until_parked();
worktree_a
.condition(cx_a, |tree, _| {
worktree_a.read_with(cx_a, |tree, _| {
assert_eq!(
tree.paths()
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>()
== ["file1-renamed", "file3", "file4"]
})
.await;
worktree_b
.condition(cx_b, |tree, _| {
.collect::<Vec<_>>(),
["file1.js", "file3", "file4"]
)
});
worktree_b.read_with(cx_b, |tree, _| {
assert_eq!(
tree.paths()
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>()
== ["file1-renamed", "file3", "file4"]
})
.await;
worktree_c
.condition(cx_c, |tree, _| {
.collect::<Vec<_>>(),
["file1.js", "file3", "file4"]
)
});
worktree_c.read_with(cx_c, |tree, _| {
assert_eq!(
tree.paths()
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>()
== ["file1-renamed", "file3", "file4"]
})
.await;
.collect::<Vec<_>>(),
["file1.js", "file3", "file4"]
)
});
// Ensure buffer files are updated as well.
buffer_a
.condition(cx_a, |buf, _| {
buf.file().unwrap().path().to_str() == Some("file1-renamed")
})
.await;
buffer_b
.condition(cx_b, |buf, _| {
buf.file().unwrap().path().to_str() == Some("file1-renamed")
})
.await;
buffer_c
.condition(cx_c, |buf, _| {
buf.file().unwrap().path().to_str() == Some("file1-renamed")
})
.await;
buffer_a.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
});
buffer_b.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
});
buffer_c.read_with(cx_c, |buffer, _| {
assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
});
}
#[gpui::test(iterations = 10)]
@@ -2127,7 +2293,7 @@ async fn test_leaving_project(
// Simulate connection loss for client C and ensure client A observes client C leaving the project.
client_c.wait_for_current_user(cx_c).await;
server.disconnect_client(client_c.current_user_id(cx_c));
server.disconnect_client(client_c.peer_id().unwrap());
cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
deterministic.run_until_parked();
project_a.read_with(cx_a, |project, _| {
@@ -3015,7 +3181,7 @@ async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
assert_eq!(references[1].buffer, references[0].buffer);
assert_eq!(
three_buffer.file().unwrap().full_path(cx),
Path::new("three.rs")
Path::new("/root/dir-2/three.rs")
);
assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
@@ -4290,7 +4456,7 @@ async fn test_chat_reconnection(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
// Disconnect client B, ensuring we can still access its cached channel data.
server.forbid_connections();
server.disconnect_client(client_b.current_user_id(cx_b));
server.disconnect_client(client_b.peer_id().unwrap());
cx_b.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
while !matches!(
status_b.next().await,
@@ -4453,7 +4619,7 @@ async fn test_contacts(
]
);
server.disconnect_client(client_c.current_user_id(cx_c));
server.disconnect_client(client_c.peer_id().unwrap());
server.forbid_connections();
deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
assert_eq!(
@@ -4693,7 +4859,7 @@ async fn test_contacts(
);
server.forbid_connections();
server.disconnect_client(client_a.current_user_id(cx_a));
server.disconnect_client(client_a.peer_id().unwrap());
deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
assert_eq!(contacts(&client_a, cx_a), []);
assert_eq!(
@@ -4917,7 +5083,11 @@ async fn test_contact_requests(
}
#[gpui::test(iterations = 10)]
async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
async fn test_following(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
cx_a.foreground().forbid_parking();
cx_a.update(editor::init);
cx_b.update(editor::init);
@@ -4929,6 +5099,7 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
client_a
.fs
@@ -4942,11 +5113,20 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
active_call_a
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
.await
.unwrap();
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
.unwrap();
// Client A opens some editors.
let workspace_a = client_a.build_workspace(&project_a, cx_a);
@@ -5088,7 +5268,7 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
workspace_a.update(cx_a, |workspace, cx| {
workspace.activate_item(&editor_a2, cx)
});
cx_a.foreground().run_until_parked();
deterministic.run_until_parked();
assert_eq!(
workspace_b.read_with(cx_b, |workspace, cx| workspace
.active_item(cx)
@@ -5118,9 +5298,62 @@ async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
editor_a1.id()
);
// Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
let display = MacOSDisplay::new();
active_call_b
.update(cx_b, |call, cx| call.set_location(None, cx))
.await
.unwrap();
active_call_b
.update(cx_b, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
room.set_display_sources(vec![display.clone()]);
room.share_screen(cx)
})
})
.await
.unwrap();
deterministic.run_until_parked();
let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
workspace
.active_item(cx)
.unwrap()
.downcast::<SharedScreen>()
.unwrap()
});
// Client B activates Zed again, which causes the previous editor to become focused again.
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
.unwrap();
deterministic.run_until_parked();
assert_eq!(
workspace_a.read_with(cx_a, |workspace, cx| workspace
.active_item(cx)
.unwrap()
.id()),
editor_a1.id()
);
// Client B activates an external window again, and the previously-opened screen-sharing item
// gets activated.
active_call_b
.update(cx_b, |call, cx| call.set_location(None, cx))
.await
.unwrap();
deterministic.run_until_parked();
assert_eq!(
workspace_a.read_with(cx_a, |workspace, cx| workspace
.active_item(cx)
.unwrap()
.id()),
shared_screen.id()
);
// Following interrupts when client B disconnects.
client_b.disconnect(&cx_b.to_async()).unwrap();
cx_a.foreground().run_until_parked();
deterministic.run_until_parked();
assert_eq!(
workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
None
@@ -5140,6 +5373,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
// Client A shares a project.
client_a
@@ -5155,6 +5389,10 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
active_call_a
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
.await
.unwrap();
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
@@ -5162,6 +5400,10 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
// Client B joins the project.
let project_b = client_b.build_remote_project(project_id, cx_b).await;
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
.unwrap();
// Client A opens some editors.
let workspace_a = client_a.build_workspace(&project_a, cx_a);
@@ -5309,6 +5551,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
// Client A shares a project.
client_a
@@ -5323,11 +5566,20 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
active_call_a
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
.await
.unwrap();
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
active_call_b
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
.await
.unwrap();
// Client A opens some editors.
let workspace_a = client_a.build_workspace(&project_a, cx_a);
@@ -5603,6 +5855,7 @@ async fn test_random_collaboration(
let mut clients = Vec::new();
let mut user_ids = Vec::new();
let mut peer_ids = Vec::new();
let mut op_start_signals = Vec::new();
let mut next_entity_id = 100000;
@@ -5791,6 +6044,7 @@ async fn test_random_collaboration(
let op_start_signal = futures::channel::mpsc::unbounded();
user_ids.push(host_user_id);
peer_ids.push(host.peer_id().unwrap());
op_start_signals.push(op_start_signal.0);
clients.push(host_cx.foreground().spawn(host.simulate_host(
host_project,
@@ -5808,7 +6062,7 @@ async fn test_random_collaboration(
let mut operations = 0;
while operations < max_operations {
if operations == disconnect_host_at {
server.disconnect_client(user_ids[0]);
server.disconnect_client(peer_ids[0]);
deterministic.advance_clock(RECEIVE_TIMEOUT);
drop(op_start_signals);
@@ -5891,6 +6145,7 @@ async fn test_random_collaboration(
let op_start_signal = futures::channel::mpsc::unbounded();
user_ids.push(guest_user_id);
peer_ids.push(guest.peer_id().unwrap());
op_start_signals.push(op_start_signal.0);
clients.push(guest_cx.foreground().spawn(guest.simulate_guest(
guest_username.clone(),
@@ -5907,10 +6162,11 @@ async fn test_random_collaboration(
let guest_ix = rng.lock().gen_range(1..clients.len());
log::info!("Removing guest {}", user_ids[guest_ix]);
let removed_guest_id = user_ids.remove(guest_ix);
let removed_peer_id = peer_ids.remove(guest_ix);
let guest = clients.remove(guest_ix);
op_start_signals.remove(guest_ix);
server.forbid_connections();
server.disconnect_client(removed_guest_id);
server.disconnect_client(removed_peer_id);
deterministic.advance_clock(RECEIVE_TIMEOUT);
deterministic.start_waiting();
log::info!("Waiting for guest {} to exit...", removed_guest_id);
@@ -6034,8 +6290,10 @@ async fn test_random_collaboration(
let host_buffer = host_project.read_with(&host_cx, |project, cx| {
project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
panic!(
"host does not have buffer for guest:{}, peer:{}, id:{}",
guest_client.username, guest_client.peer_id, buffer_id
"host does not have buffer for guest:{}, peer:{:?}, id:{}",
guest_client.username,
guest_client.peer_id(),
buffer_id
)
})
});
@@ -6078,9 +6336,10 @@ struct TestServer {
server: Arc<Server>,
foreground: Rc<executor::Foreground>,
notifications: mpsc::UnboundedReceiver<()>,
connection_killers: Arc<Mutex<HashMap<UserId, Arc<AtomicBool>>>>,
connection_killers: Arc<Mutex<HashMap<PeerId, Arc<AtomicBool>>>>,
forbid_connections: Arc<AtomicBool>,
_test_db: TestDb,
test_live_kit_server: Arc<live_kit_client::TestServer>,
}
impl TestServer {
@@ -6088,8 +6347,18 @@ impl TestServer {
foreground: Rc<executor::Foreground>,
background: Arc<executor::Background>,
) -> Self {
static NEXT_LIVE_KIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0);
let test_db = TestDb::fake(background.clone());
let app_state = Self::build_app_state(&test_db).await;
let live_kit_server_id = NEXT_LIVE_KIT_SERVER_ID.fetch_add(1, SeqCst);
let live_kit_server = live_kit_client::TestServer::create(
format!("http://livekit.{}.test", live_kit_server_id),
format!("devkey-{}", live_kit_server_id),
format!("secret-{}", live_kit_server_id),
background.clone(),
)
.unwrap();
let app_state = Self::build_app_state(&test_db, &live_kit_server).await;
let peer = Peer::new();
let notifications = mpsc::unbounded();
let server = Server::new(app_state.clone(), Some(notifications.0));
@@ -6102,11 +6371,14 @@ impl TestServer {
connection_killers: Default::default(),
forbid_connections: Default::default(),
_test_db: test_db,
test_live_kit_server: live_kit_server,
}
}
async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
cx.update(|cx| {
cx.set_global(HomeDir(Path::new("/tmp/").to_path_buf()));
let mut settings = Settings::test(cx);
settings.projects_online_by_default = false;
cx.set_global(settings);
@@ -6142,7 +6414,6 @@ impl TestServer {
let db = self.app_state.db.clone();
let connection_killers = self.connection_killers.clone();
let forbid_connections = self.forbid_connections.clone();
let (connection_id_tx, mut connection_id_rx) = mpsc::channel(16);
Arc::get_mut(&mut client)
.unwrap()
@@ -6165,7 +6436,6 @@ impl TestServer {
let connection_killers = connection_killers.clone();
let forbid_connections = forbid_connections.clone();
let client_name = client_name.clone();
let connection_id_tx = connection_id_tx.clone();
cx.spawn(move |cx| async move {
if forbid_connections.load(SeqCst) {
Err(EstablishConnectionError::other(anyhow!(
@@ -6174,7 +6444,7 @@ impl TestServer {
} else {
let (client_conn, server_conn, killed) =
Connection::in_memory(cx.background());
connection_killers.lock().insert(user_id, killed);
let (connection_id_tx, connection_id_rx) = oneshot::channel();
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
cx.background()
.spawn(server.handle_connection(
@@ -6185,6 +6455,10 @@ impl TestServer {
cx.background(),
))
.detach();
let connection_id = connection_id_rx.await.unwrap();
connection_killers
.lock()
.insert(PeerId(connection_id.0), killed);
Ok(client_conn)
}
})
@@ -6216,11 +6490,9 @@ impl TestServer {
.authenticate_and_connect(false, &cx.to_async())
.await
.unwrap();
let peer_id = PeerId(connection_id_rx.next().await.unwrap().0);
let client = TestClient {
client,
peer_id,
username: name.to_string(),
user_store,
project_store,
@@ -6232,10 +6504,10 @@ impl TestServer {
client
}
fn disconnect_client(&self, user_id: UserId) {
fn disconnect_client(&self, peer_id: PeerId) {
self.connection_killers
.lock()
.remove(&user_id)
.remove(&peer_id)
.unwrap()
.store(true, SeqCst);
}
@@ -6295,11 +6567,14 @@ impl TestServer {
}
}
async fn build_app_state(test_db: &TestDb) -> Arc<AppState> {
async fn build_app_state(
test_db: &TestDb,
fake_server: &live_kit_client::TestServer,
) -> Arc<AppState> {
Arc::new(AppState {
db: test_db.db().clone(),
api_token: Default::default(),
invite_link_prefix: Default::default(),
live_kit_client: Some(Arc::new(fake_server.create_api_client())),
config: Default::default(),
})
}
@@ -6330,13 +6605,13 @@ impl Deref for TestServer {
impl Drop for TestServer {
fn drop(&mut self) {
self.peer.reset();
self.test_live_kit_server.teardown().unwrap();
}
}
struct TestClient {
client: Arc<Client>,
username: String,
pub peer_id: PeerId,
pub user_store: ModelHandle<UserStore>,
pub project_store: ModelHandle<ProjectStore>,
language_registry: Arc<LanguageRegistry>,

View File

@@ -9,44 +9,73 @@ mod db_tests;
#[cfg(test)]
mod integration_tests;
use axum::{body::Body, Router};
use crate::rpc::ResultExt as _;
use anyhow::anyhow;
use axum::{routing::get, Router};
use collab::{Error, Result};
use db::{Db, PostgresDb};
use serde::Deserialize;
use std::{
env::args,
net::{SocketAddr, TcpListener},
path::PathBuf,
sync::Arc,
time::Duration,
};
use tokio::signal;
use tracing_log::LogTracer;
use tracing_subscriber::{filter::EnvFilter, fmt::format::JsonFields, Layer};
use util::ResultExt;
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
#[derive(Default, Deserialize)]
pub struct Config {
pub http_port: u16,
pub database_url: String,
pub api_token: String,
pub invite_link_prefix: String,
pub honeycomb_api_key: Option<String>,
pub honeycomb_dataset: Option<String>,
pub live_kit_server: Option<String>,
pub live_kit_key: Option<String>,
pub live_kit_secret: Option<String>,
pub rust_log: Option<String>,
pub log_json: Option<bool>,
}
#[derive(Default, Deserialize)]
pub struct MigrateConfig {
pub database_url: String,
pub migrations_path: Option<PathBuf>,
}
pub struct AppState {
db: Arc<dyn Db>,
api_token: String,
invite_link_prefix: String,
live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
config: Config,
}
impl AppState {
async fn new(config: &Config) -> Result<Arc<Self>> {
async fn new(config: Config) -> Result<Arc<Self>> {
let db = PostgresDb::new(&config.database_url, 5).await?;
let live_kit_client = if let Some(((server, key), secret)) = config
.live_kit_server
.as_ref()
.zip(config.live_kit_key.as_ref())
.zip(config.live_kit_secret.as_ref())
{
Some(Arc::new(live_kit_server::api::LiveKitClient::new(
server.clone(),
key.clone(),
secret.clone(),
)) as Arc<dyn live_kit_server::api::Client>)
} else {
None
};
let this = Self {
db: Arc::new(db),
api_token: config.api_token.clone(),
invite_link_prefix: config.invite_link_prefix.clone(),
live_kit_client,
config,
};
Ok(Arc::new(this))
}
@@ -61,27 +90,62 @@ async fn main() -> Result<()> {
);
}
let config = envy::from_env::<Config>().expect("error loading config");
init_tracing(&config);
let state = AppState::new(&config).await?;
match args().skip(1).next().as_deref() {
Some("version") => {
println!("collab v{VERSION}");
}
Some("migrate") => {
let config = envy::from_env::<MigrateConfig>().expect("error loading config");
let db = PostgresDb::new(&config.database_url, 5).await?;
let listener = TcpListener::bind(&format!("0.0.0.0:{}", config.http_port))
.expect("failed to bind TCP listener");
let rpc_server = rpc::Server::new(state.clone(), None);
let migrations_path = config
.migrations_path
.as_deref()
.or(db::DEFAULT_MIGRATIONS_PATH.map(|s| s.as_ref()))
.ok_or_else(|| anyhow!("missing MIGRATIONS_PATH environment variable"))?;
rpc_server.start_recording_project_activity(Duration::from_secs(5 * 60), rpc::RealExecutor);
let migrations = db.migrate(&migrations_path, false).await?;
for (migration, duration) in migrations {
println!(
"Ran {} {} {:?}",
migration.version, migration.description, duration
);
}
let app = Router::<Body>::new()
.merge(api::routes(&rpc_server, state.clone()))
.merge(rpc::routes(rpc_server));
return Ok(());
}
Some("serve") => {
let config = envy::from_env::<Config>().expect("error loading config");
init_tracing(&config);
axum::Server::from_tcp(listener)?
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
.await?;
let state = AppState::new(config).await?;
let listener = TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port))
.expect("failed to bind TCP listener");
let rpc_server = rpc::Server::new(state.clone(), None);
rpc_server
.start_recording_project_activity(Duration::from_secs(5 * 60), rpc::RealExecutor);
let app = api::routes(rpc_server.clone(), state.clone())
.merge(rpc::routes(rpc_server.clone()))
.merge(Router::new().route("/", get(handle_root)));
axum::Server::from_tcp(listener)?
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
.with_graceful_shutdown(graceful_shutdown(rpc_server, state))
.await?;
}
_ => {
Err(anyhow!("usage: collab <version | migrate | serve>"))?;
}
}
Ok(())
}
async fn handle_root() -> String {
format!("collab v{VERSION}")
}
pub fn init_tracing(config: &Config) -> Option<()> {
use std::str::FromStr;
use tracing_subscriber::layer::SubscriberExt;
@@ -113,3 +177,52 @@ pub fn init_tracing(config: &Config) -> Option<()> {
None
}
async fn graceful_shutdown(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
if let Some(live_kit) = state.live_kit_client.as_ref() {
let deletions = rpc_server
.store()
.await
.rooms()
.values()
.map(|room| {
let name = room.live_kit_room.clone();
async {
live_kit.delete_room(name).await.trace_err();
}
})
.collect::<Vec<_>>();
tracing::info!("deleting all live-kit rooms");
if let Err(_) = tokio::time::timeout(
Duration::from_secs(10),
futures::future::join_all(deletions),
)
.await
{
tracing::error!("timed out waiting for live-kit room deletion");
}
}
}

View File

@@ -24,7 +24,7 @@ use axum::{
};
use collections::{HashMap, HashSet};
use futures::{
channel::mpsc,
channel::{mpsc, oneshot},
future::{self, BoxFuture},
stream::FuturesUnordered,
FutureExt, SinkExt, StreamExt, TryStreamExt,
@@ -42,6 +42,7 @@ use std::{
marker::PhantomData,
net::SocketAddr,
ops::{Deref, DerefMut},
os::unix::prelude::OsStrExt,
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
@@ -49,6 +50,7 @@ use std::{
},
time::Duration,
};
pub use store::{Store, Worktree};
use time::OffsetDateTime;
use tokio::{
sync::{Mutex, MutexGuard},
@@ -57,8 +59,6 @@ use tokio::{
use tower::ServiceBuilder;
use tracing::{info_span, instrument, Instrument};
pub use store::{Store, Worktree};
lazy_static! {
static ref METRIC_CONNECTIONS: IntGauge =
register_int_gauge!("connections", "number of connections").unwrap();
@@ -347,7 +347,7 @@ impl Server {
connection: Connection,
address: String,
user: User,
mut send_connection_id: Option<mpsc::Sender<ConnectionId>>,
mut send_connection_id: Option<oneshot::Sender<ConnectionId>>,
executor: E,
) -> impl Future<Output = Result<()>> {
let mut this = self.clone();
@@ -368,9 +368,11 @@ impl Server {
});
tracing::info!(%user_id, %login, %connection_id, %address, "connection opened");
this.peer.send(connection_id, proto::Hello { peer_id: connection_id.0 })?;
tracing::info!(%user_id, %login, %connection_id, %address, "sent hello message");
if let Some(send_connection_id) = send_connection_id.as_mut() {
let _ = send_connection_id.send(connection_id).await;
if let Some(send_connection_id) = send_connection_id.take() {
let _ = send_connection_id.send(connection_id);
}
if !user.connected_once {
@@ -394,7 +396,7 @@ impl Server {
if let Some((code, count)) = invite_code {
this.peer.send(connection_id, proto::UpdateInviteInfo {
url: format!("{}{}", this.app_state.invite_link_prefix, code),
url: format!("{}{}", this.app_state.config.invite_link_prefix, code),
count,
})?;
}
@@ -474,8 +476,13 @@ impl Server {
let mut projects_to_unshare = Vec::new();
let mut contacts_to_update = HashSet::default();
let mut room_left = None;
{
let mut store = self.store().await;
#[cfg(test)]
let removed_connection = store.remove_connection(connection_id).unwrap();
#[cfg(not(test))]
let removed_connection = store.remove_connection(connection_id)?;
for project in removed_connection.hosted_projects {
@@ -502,23 +509,24 @@ impl Server {
});
}
if let Some(room) = removed_connection.room {
self.room_updated(&room);
room_left = Some(self.room_left(&room, connection_id));
}
contacts_to_update.insert(removed_connection.user_id);
for connection_id in removed_connection.canceled_call_connection_ids {
self.peer
.send(connection_id, proto::CallCanceled {})
.trace_err();
contacts_to_update.extend(store.user_id_for_connection(connection_id).ok());
}
if let Some(room) = removed_connection
.room_id
.and_then(|room_id| store.room(room_id))
{
self.room_updated(room);
}
contacts_to_update.insert(removed_connection.user_id);
};
if let Some(room_left) = room_left {
room_left.await.trace_err();
}
for user_id in contacts_to_update {
self.update_user_contacts(user_id).await.trace_err();
}
@@ -554,7 +562,7 @@ impl Server {
self.peer.send(
connection_id,
proto::UpdateInviteInfo {
url: format!("{}{}", self.app_state.invite_link_prefix, &code),
url: format!("{}{}", self.app_state.config.invite_link_prefix, &code),
count: user.invite_count as u32,
},
)?;
@@ -572,7 +580,10 @@ impl Server {
self.peer.send(
connection_id,
proto::UpdateInviteInfo {
url: format!("{}{}", self.app_state.invite_link_prefix, invite_code),
url: format!(
"{}{}",
self.app_state.config.invite_link_prefix, invite_code
),
count: user.invite_count as u32,
},
)?;
@@ -597,13 +608,42 @@ impl Server {
response: Response<proto::CreateRoom>,
) -> Result<()> {
let user_id;
let room_id;
let room;
{
let mut store = self.store().await;
user_id = store.user_id_for_connection(request.sender_id)?;
room_id = store.create_room(request.sender_id)?;
room = store.create_room(request.sender_id)?.clone();
}
response.send(proto::CreateRoomResponse { id: room_id })?;
let live_kit_connection_info =
if let Some(live_kit) = self.app_state.live_kit_client.as_ref() {
if let Some(_) = live_kit
.create_room(room.live_kit_room.clone())
.await
.trace_err()
{
if let Some(token) = live_kit
.room_token(&room.live_kit_room, &request.sender_id.to_string())
.trace_err()
{
Some(proto::LiveKitConnectionInfo {
server_url: live_kit.url().into(),
token,
})
} else {
None
}
} else {
None
}
} else {
None
};
response.send(proto::CreateRoomResponse {
room: Some(room),
live_kit_connection_info,
})?;
self.update_user_contacts(user_id).await?;
Ok(())
}
@@ -624,8 +664,27 @@ impl Server {
.send(recipient_id, proto::CallCanceled {})
.trace_err();
}
let live_kit_connection_info =
if let Some(live_kit) = self.app_state.live_kit_client.as_ref() {
if let Some(token) = live_kit
.room_token(&room.live_kit_room, &request.sender_id.to_string())
.trace_err()
{
Some(proto::LiveKitConnectionInfo {
server_url: live_kit.url().into(),
token,
})
} else {
None
}
} else {
None
};
response.send(proto::JoinRoomResponse {
room: Some(room.clone()),
live_kit_connection_info,
})?;
self.room_updated(room);
}
@@ -635,6 +694,7 @@ impl Server {
async fn leave_room(self: Arc<Server>, message: TypedEnvelope<proto::LeaveRoom>) -> Result<()> {
let mut contacts_to_update = HashSet::default();
let room_left;
{
let mut store = self.store().await;
let user_id = store.user_id_for_connection(message.sender_id)?;
@@ -673,9 +733,8 @@ impl Server {
}
}
if let Some(room) = left_room.room {
self.room_updated(room);
}
self.room_updated(&left_room.room);
room_left = self.room_left(&left_room.room, message.sender_id);
for connection_id in left_room.canceled_call_connection_ids {
self.peer
@@ -685,6 +744,7 @@ impl Server {
}
}
room_left.await.trace_err();
for user_id in contacts_to_update {
self.update_user_contacts(user_id).await?;
}
@@ -833,6 +893,29 @@ impl Server {
}
}
fn room_left(
&self,
room: &proto::Room,
connection_id: ConnectionId,
) -> impl Future<Output = Result<()>> {
let client = self.app_state.live_kit_client.clone();
let room_name = room.live_kit_room.clone();
let participant_count = room.participants.len();
async move {
if let Some(client) = client {
client
.remove_participant(room_name.clone(), connection_id.to_string())
.await?;
if participant_count == 0 {
client.delete_room(room_name).await?;
}
}
Ok(())
}
}
async fn share_project(
self: Arc<Server>,
request: TypedEnvelope<proto::ShareProject>,
@@ -941,6 +1024,7 @@ impl Server {
id: *id,
root_name: worktree.root_name.clone(),
visible: worktree.visible,
abs_path: worktree.abs_path.as_os_str().as_bytes().to_vec(),
})
.collect::<Vec<_>>();
@@ -989,6 +1073,7 @@ impl Server {
let message = proto::UpdateWorktree {
project_id: project_id.to_proto(),
worktree_id: *worktree_id,
abs_path: worktree.abs_path.as_os_str().as_bytes().to_vec(),
root_name: worktree.root_name.clone(),
updated_entries: worktree.entries.values().cloned().collect(),
removed_entries: Default::default(),

View File

@@ -1,9 +1,10 @@
use crate::db::{self, ChannelId, ProjectId, UserId};
use anyhow::{anyhow, Result};
use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet};
use nanoid::nanoid;
use rpc::{proto, ConnectionId};
use serde::Serialize;
use std::{mem, path::PathBuf, str, time::Duration};
use std::{borrow::Cow, mem, path::PathBuf, str, time::Duration};
use time::OffsetDateTime;
use tracing::instrument;
use util::post_inc;
@@ -66,6 +67,7 @@ pub struct Collaborator {
#[derive(Default, Serialize)]
pub struct Worktree {
pub abs_path: PathBuf,
pub root_name: String,
pub visible: bool,
#[serde(skip)]
@@ -84,12 +86,12 @@ pub struct Channel {
pub type ReplicaId = u16;
#[derive(Default)]
pub struct RemovedConnectionState {
pub struct RemovedConnectionState<'a> {
pub user_id: UserId,
pub hosted_projects: Vec<Project>,
pub guest_projects: Vec<LeftProject>,
pub contact_ids: HashSet<UserId>,
pub room_id: Option<RoomId>,
pub room: Option<Cow<'a, proto::Room>>,
pub canceled_call_connection_ids: Vec<ConnectionId>,
}
@@ -102,7 +104,7 @@ pub struct LeftProject {
}
pub struct LeftRoom<'a> {
pub room: Option<&'a proto::Room>,
pub room: Cow<'a, proto::Room>,
pub unshared_projects: Vec<Project>,
pub left_projects: Vec<LeftProject>,
pub canceled_call_connection_ids: Vec<ConnectionId>,
@@ -214,11 +216,16 @@ impl Store {
let connected_user = self.connected_users.get(&user_id).unwrap();
if let Some(active_call) = connected_user.active_call.as_ref() {
let room_id = active_call.room_id;
let left_room = self.leave_room(room_id, connection_id)?;
result.hosted_projects = left_room.unshared_projects;
result.guest_projects = left_room.left_projects;
result.room_id = Some(room_id);
result.canceled_call_connection_ids = left_room.canceled_call_connection_ids;
if active_call.connection_id == Some(connection_id) {
let left_room = self.leave_room(room_id, connection_id)?;
result.hosted_projects = left_room.unshared_projects;
result.guest_projects = left_room.left_projects;
result.room = Some(Cow::Owned(left_room.room.into_owned()));
result.canceled_call_connection_ids = left_room.canceled_call_connection_ids;
} else if connected_user.connection_ids.len() == 1 {
let (room, _) = self.decline_call(room_id, connection_id)?;
result.room = Some(Cow::Owned(room.clone()));
}
}
let connected_user = self.connected_users.get_mut(&user_id).unwrap();
@@ -339,7 +346,7 @@ impl Store {
}
}
pub fn create_room(&mut self, creator_connection_id: ConnectionId) -> Result<RoomId> {
pub fn create_room(&mut self, creator_connection_id: ConnectionId) -> Result<&proto::Room> {
let connection = self
.connections
.get_mut(&creator_connection_id)
@@ -353,19 +360,23 @@ impl Store {
"can't create a room with an active call"
);
let mut room = proto::Room::default();
room.participants.push(proto::Participant {
user_id: connection.user_id.to_proto(),
peer_id: creator_connection_id.0,
projects: Default::default(),
location: Some(proto::ParticipantLocation {
variant: Some(proto::participant_location::Variant::External(
proto::participant_location::External {},
)),
}),
});
let room_id = post_inc(&mut self.next_room_id);
let room = proto::Room {
id: room_id,
participants: vec![proto::Participant {
user_id: connection.user_id.to_proto(),
peer_id: creator_connection_id.0,
projects: Default::default(),
location: Some(proto::ParticipantLocation {
variant: Some(proto::participant_location::Variant::External(
proto::participant_location::External {},
)),
}),
}],
pending_participant_user_ids: Default::default(),
live_kit_room: nanoid!(30),
};
self.rooms.insert(room_id, room);
connected_user.active_call = Some(Call {
caller_user_id: connection.user_id,
@@ -373,7 +384,7 @@ impl Store {
connection_id: Some(creator_connection_id),
initial_project_id: None,
});
Ok(room_id)
Ok(self.rooms.get(&room_id).unwrap())
}
pub fn join_room(
@@ -490,12 +501,14 @@ impl Store {
}
});
if room.participants.is_empty() && room.pending_participant_user_ids.is_empty() {
self.rooms.remove(&room_id);
}
let room = if room.participants.is_empty() {
Cow::Owned(self.rooms.remove(&room_id).unwrap())
} else {
Cow::Borrowed(self.rooms.get(&room_id).unwrap())
};
Ok(LeftRoom {
room: self.rooms.get(&room_id),
room,
unshared_projects,
left_projects,
canceled_call_connection_ids,
@@ -506,6 +519,10 @@ impl Store {
self.rooms.get(&room_id)
}
pub fn rooms(&self) -> &BTreeMap<RoomId, proto::Room> {
&self.rooms
}
pub fn call(
&mut self,
room_id: RoomId,

View File

@@ -10,17 +10,21 @@ use gpui::{
geometry::{rect::RectF, vector::vec2f, PathBuilder},
json::{self, ToJson},
Border, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use settings::Settings;
use std::ops::Range;
use theme::Theme;
use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace};
actions!(collab, [ToggleCollaborationMenu, ShareProject]);
actions!(
collab,
[ToggleCollaborationMenu, ToggleScreenSharing, ShareProject]
);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
cx.add_action(CollabTitlebarItem::toggle_screen_sharing);
cx.add_action(CollabTitlebarItem::share_project);
}
@@ -48,10 +52,12 @@ impl View for CollabTitlebarItem {
};
let theme = cx.global::<Settings>().theme.clone();
let project = workspace.read(cx).project().read(cx);
let mut container = Flex::row();
container.add_children(self.render_toggle_screen_sharing_button(&theme, cx));
if workspace.read(cx).client().status().borrow().is_connected() {
let project = workspace.read(cx).project().read(cx);
if project.is_shared()
|| project.is_remote()
|| ActiveCall::global(cx).read(cx).room().is_none()
@@ -114,19 +120,15 @@ impl CollabTitlebarItem {
}
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
let workspace = self.workspace.upgrade(cx);
let room = ActiveCall::global(cx).read(cx).room().cloned();
if let Some((workspace, room)) = workspace.zip(room) {
let workspace = workspace.read(cx);
if let Some(workspace) = self.workspace.upgrade(cx) {
let project = if active {
Some(workspace.project().clone())
Some(workspace.read(cx).project().clone())
} else {
None
};
room.update(cx, |room, cx| {
room.set_location(project.as_ref(), cx)
.detach_and_log_err(cx);
});
ActiveCall::global(cx)
.update(cx, |call, cx| call.set_location(project.as_ref(), cx))
.detach_and_log_err(cx);
}
}
@@ -169,6 +171,19 @@ impl CollabTitlebarItem {
cx.notify();
}
pub fn toggle_screen_sharing(&mut self, _: &ToggleScreenSharing, cx: &mut ViewContext<Self>) {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
let toggle_screen_sharing = room.update(cx, |room, cx| {
if room.is_screen_sharing() {
Task::ready(room.unshare_screen(cx))
} else {
room.share_screen(cx)
}
});
toggle_screen_sharing.detach_and_log_err(cx);
}
}
fn render_toggle_contacts_button(
&self,
theme: &Theme,
@@ -232,11 +247,62 @@ impl CollabTitlebarItem {
)
.with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_corner(AnchorCorner::BottomLeft)
.with_z_index(999)
.boxed()
}))
.boxed()
}
fn render_toggle_screen_sharing_button(
&self,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> Option<ElementBox> {
let active_call = ActiveCall::global(cx);
let room = active_call.read(cx).room().cloned()?;
let icon;
let tooltip;
if room.read(cx).is_screen_sharing() {
icon = "icons/disable_screen_sharing_12.svg";
tooltip = "Stop Sharing Screen"
} else {
icon = "icons/enable_screen_sharing_12.svg";
tooltip = "Share Screen";
}
let titlebar = &theme.workspace.titlebar;
Some(
MouseEventHandler::<ToggleScreenSharing>::new(0, cx, |state, _| {
let style = titlebar.call_control.style_for(state, false);
Svg::new(icon)
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleScreenSharing);
})
.with_tooltip::<ToggleScreenSharing, _>(
0,
tooltip.into(),
Some(Box::new(ToggleScreenSharing)),
theme.tooltip.clone(),
cx,
)
.aligned()
.boxed(),
)
}
fn render_share_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
enum Share {}
@@ -526,18 +592,6 @@ impl Element for AvatarRibbon {
cx.scene.push_path(path.build(self.color, None));
}
fn dispatch_event(
&mut self,
_: &gpui::Event,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut gpui::EventContext,
) -> bool {
false
}
fn rect_for_text_range(
&self,
_: Range<usize>,

View File

@@ -36,7 +36,7 @@ impl View for ContactFinder {
ChildView::new(self.picker.clone(), cx).boxed()
}
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.picker);
}
@@ -101,7 +101,7 @@ impl PickerDelegate for ContactFinder {
fn render_match(
&self,
ix: usize,
mouse_state: MouseState,
mouse_state: &mut MouseState,
selected: bool,
cx: &gpui::AppContext,
) -> ElementBox {

View File

@@ -1,4 +1,4 @@
use std::sync::Arc;
use std::{mem, sync::Arc};
use crate::contacts_popover;
use call::ActiveCall;
@@ -17,7 +17,7 @@ use serde::Deserialize;
use settings::Settings;
use theme::IconButton;
use util::ResultExt;
use workspace::JoinProject;
use workspace::{JoinProject, OpenSharedScreen};
impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]);
impl_internal_actions!(contact_list, [ToggleExpanded, Call, LeaveCall]);
@@ -67,9 +67,16 @@ enum ContactEntry {
host_user_id: u64,
is_last: bool,
},
ParticipantScreen {
peer_id: PeerId,
is_last: bool,
},
IncomingRequest(Arc<User>),
OutgoingRequest(Arc<User>),
Contact(Arc<Contact>),
Contact {
contact: Arc<Contact>,
calling: bool,
},
}
impl PartialEq for ContactEntry {
@@ -97,6 +104,16 @@ impl PartialEq for ContactEntry {
return project_id_1 == project_id_2;
}
}
ContactEntry::ParticipantScreen {
peer_id: peer_id_1, ..
} => {
if let ContactEntry::ParticipantScreen {
peer_id: peer_id_2, ..
} = other
{
return peer_id_1 == peer_id_2;
}
}
ContactEntry::IncomingRequest(user_1) => {
if let ContactEntry::IncomingRequest(user_2) = other {
return user_1.id == user_2.id;
@@ -107,8 +124,13 @@ impl PartialEq for ContactEntry {
return user_1.id == user_2.id;
}
}
ContactEntry::Contact(contact_1) => {
if let ContactEntry::Contact(contact_2) = other {
ContactEntry::Contact {
contact: contact_1, ..
} => {
if let ContactEntry::Contact {
contact: contact_2, ..
} = other
{
return contact_1.user.id == contact_2.user.id;
}
}
@@ -216,6 +238,15 @@ impl ContactList {
&theme.contact_list,
cx,
),
ContactEntry::ParticipantScreen { peer_id, is_last } => {
Self::render_participant_screen(
*peer_id,
*is_last,
is_selected,
&theme.contact_list,
cx,
)
}
ContactEntry::IncomingRequest(user) => Self::render_contact_request(
user.clone(),
this.user_store.clone(),
@@ -232,8 +263,9 @@ impl ContactList {
is_selected,
cx,
),
ContactEntry::Contact(contact) => Self::render_contact(
ContactEntry::Contact { contact, calling } => Self::render_contact(
contact,
*calling,
&this.project,
&theme.contact_list,
is_selected,
@@ -302,8 +334,14 @@ impl ContactList {
} else if !self.entries.is_empty() {
self.selection = Some(0);
}
cx.notify();
self.list_state.reset(self.entries.len());
if let Some(ix) = self.selection {
self.list_state.scroll_to(ListOffset {
item_ix: ix,
offset_in_item: 0.,
});
}
cx.notify();
}
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
@@ -314,8 +352,14 @@ impl ContactList {
self.selection = None;
}
}
cx.notify();
self.list_state.reset(self.entries.len());
if let Some(ix) = self.selection {
self.list_state.scroll_to(ListOffset {
item_ix: ix,
offset_in_item: 0.,
});
}
cx.notify();
}
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
@@ -326,8 +370,8 @@ impl ContactList {
let section = *section;
self.toggle_expanded(&ToggleExpanded(section), cx);
}
ContactEntry::Contact(contact) => {
if contact.online && !contact.busy {
ContactEntry::Contact { contact, calling } => {
if contact.online && !contact.busy && !calling {
self.call(
&Call {
recipient_user_id: contact.user.id,
@@ -347,6 +391,9 @@ impl ContactList {
follow_user_id: *host_user_id,
});
}
ContactEntry::ParticipantScreen { peer_id, .. } => {
cx.dispatch_action(OpenSharedScreen { peer_id: *peer_id });
}
_ => {}
}
}
@@ -369,7 +416,7 @@ impl ContactList {
let executor = cx.background().clone();
let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
self.entries.clear();
let old_entries = mem::take(&mut self.entries);
if let Some(room) = ActiveCall::global(cx).read(cx).room() {
let room = room.read(cx);
@@ -430,11 +477,10 @@ impl ContactList {
executor.clone(),
));
for mat in matches {
let participant = &room.remote_participants()[&PeerId(mat.candidate_id as u32)];
let peer_id = PeerId(mat.candidate_id as u32);
let participant = &room.remote_participants()[&peer_id];
participant_entries.push(ContactEntry::CallParticipant {
user: room.remote_participants()[&PeerId(mat.candidate_id as u32)]
.user
.clone(),
user: participant.user.clone(),
is_pending: false,
});
let mut projects = participant.projects.iter().peekable();
@@ -443,7 +489,13 @@ impl ContactList {
project_id: project.id,
worktree_root_names: project.worktree_root_names.clone(),
host_user_id: participant.user.id,
is_last: projects.peek().is_none(),
is_last: projects.peek().is_none() && participant.tracks.is_empty(),
});
}
if !participant.tracks.is_empty() {
participant_entries.push(ContactEntry::ParticipantScreen {
peer_id,
is_last: true,
});
}
}
@@ -590,9 +642,13 @@ impl ContactList {
if !matches.is_empty() {
self.entries.push(ContactEntry::Header(section));
if !self.collapsed_sections.contains(&section) {
let active_call = &ActiveCall::global(cx).read(cx);
for mat in matches {
let contact = &contacts[mat.candidate_id];
self.entries.push(ContactEntry::Contact(contact.clone()));
self.entries.push(ContactEntry::Contact {
contact: contact.clone(),
calling: active_call.pending_invites().contains(&contact.user.id),
});
}
}
}
@@ -609,7 +665,47 @@ impl ContactList {
}
}
let old_scroll_top = self.list_state.logical_scroll_top();
self.list_state.reset(self.entries.len());
// Attempt to maintain the same scroll position.
if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
let new_scroll_top = self
.entries
.iter()
.position(|entry| entry == old_top_entry)
.map(|item_ix| ListOffset {
item_ix,
offset_in_item: old_scroll_top.offset_in_item,
})
.or_else(|| {
let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
let item_ix = self
.entries
.iter()
.position(|entry| entry == entry_after_old_top)?;
Some(ListOffset {
item_ix,
offset_in_item: 0.,
})
})
.or_else(|| {
let entry_before_old_top =
old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
let item_ix = self
.entries
.iter()
.position(|entry| entry == entry_before_old_top)?;
Some(ListOffset {
item_ix,
offset_in_item: 0.,
})
});
self.list_state
.scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
}
cx.notify();
}
@@ -653,7 +749,11 @@ impl ContactList {
.constrained()
.with_height(theme.row_height)
.contained()
.with_style(*theme.contact_row.style_for(Default::default(), is_selected))
.with_style(
*theme
.contact_row
.style_for(&mut Default::default(), is_selected),
)
.boxed()
}
@@ -759,6 +859,102 @@ impl ContactList {
.boxed()
}
fn render_participant_screen(
peer_id: PeerId,
is_last: bool,
is_selected: bool,
theme: &theme::ContactList,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let font_cache = cx.font_cache();
let host_avatar_height = theme
.contact_avatar
.width
.or(theme.contact_avatar.height)
.unwrap_or(0.);
let row = &theme.project_row.default;
let tree_branch = theme.tree_branch;
let line_height = row.name.text.line_height(font_cache);
let cap_height = row.name.text.cap_height(font_cache);
let baseline_offset =
row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
MouseEventHandler::<OpenSharedScreen>::new(peer_id.0 as usize, cx, |mouse_state, _| {
let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
let row = theme.project_row.style_for(mouse_state, is_selected);
Flex::row()
.with_child(
Stack::new()
.with_child(
Canvas::new(move |bounds, _, cx| {
let start_x = bounds.min_x() + (bounds.width() / 2.)
- (tree_branch.width / 2.);
let end_x = bounds.max_x();
let start_y = bounds.min_y();
let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, start_y),
vec2f(
start_x + tree_branch.width,
if is_last { end_y } else { bounds.max_y() },
),
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
cx.scene.push_quad(gpui::Quad {
bounds: RectF::from_points(
vec2f(start_x, end_y),
vec2f(end_x, end_y + tree_branch.width),
),
background: Some(tree_branch.color),
border: gpui::Border::default(),
corner_radius: 0.,
});
})
.boxed(),
)
.constrained()
.with_width(host_avatar_height)
.boxed(),
)
.with_child(
Svg::new("icons/disable_screen_sharing_12.svg")
.with_color(row.icon.color)
.constrained()
.with_width(row.icon.width)
.aligned()
.left()
.contained()
.with_style(row.icon.container)
.boxed(),
)
.with_child(
Label::new("Screen".into(), row.name.text.clone())
.aligned()
.left()
.contained()
.with_style(row.name.container)
.flex(1., false)
.boxed(),
)
.constrained()
.with_height(theme.row_height)
.contained()
.with_style(row.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(OpenSharedScreen { peer_id });
})
.boxed()
}
fn render_header(
section: Section,
theme: &theme::ContactList,
@@ -768,7 +964,9 @@ impl ContactList {
) -> ElementBox {
enum Header {}
let header_style = theme.header_row.style_for(Default::default(), is_selected);
let header_style = theme
.header_row
.style_for(&mut Default::default(), is_selected);
let text = match section {
Section::ActiveCall => "Collaborators",
Section::Requests => "Contact Requests",
@@ -835,13 +1033,14 @@ impl ContactList {
fn render_contact(
contact: &Contact,
calling: bool,
project: &ModelHandle<Project>,
theme: &theme::ContactList,
is_selected: bool,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let online = contact.online;
let busy = contact.busy;
let busy = contact.busy || calling;
let user_id = contact.user.id;
let initial_project = project.clone();
let mut element =
@@ -853,7 +1052,7 @@ impl ContactList {
Empty::new()
.collapsed()
.contained()
.with_style(if contact.busy {
.with_style(if busy {
theme.contact_status_busy
} else {
theme.contact_status_free
@@ -887,10 +1086,25 @@ impl ContactList {
.flex(1., true)
.boxed(),
)
.with_children(if calling {
Some(
Label::new("Calling".to_string(), theme.calling_indicator.text.clone())
.contained()
.with_style(theme.calling_indicator.container)
.aligned()
.boxed(),
)
} else {
None
})
.constrained()
.with_height(theme.row_height)
.contained()
.with_style(*theme.contact_row.style_for(Default::default(), is_selected))
.with_style(
*theme
.contact_row
.style_for(&mut Default::default(), is_selected),
)
.boxed()
})
.on_click(MouseButton::Left, move |_, cx| {
@@ -1014,32 +1228,22 @@ impl ContactList {
row.constrained()
.with_height(theme.row_height)
.contained()
.with_style(*theme.contact_row.style_for(Default::default(), is_selected))
.with_style(
*theme
.contact_row
.style_for(&mut Default::default(), is_selected),
)
.boxed()
}
fn call(&mut self, action: &Call, cx: &mut ViewContext<Self>) {
let recipient_user_id = action.recipient_user_id;
let initial_project = action.initial_project.clone();
let window_id = cx.window_id();
let active_call = ActiveCall::global(cx);
cx.spawn_weak(|_, mut cx| async move {
active_call
.update(&mut cx, |active_call, cx| {
active_call.invite(recipient_user_id, initial_project.clone(), cx)
})
.await?;
if cx.update(|cx| cx.window_is_active(window_id)) {
active_call
.update(&mut cx, |call, cx| {
call.set_location(initial_project.as_ref(), cx)
})
.await?;
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
ActiveCall::global(cx)
.update(cx, |call, cx| {
call.invite(recipient_user_id, initial_project, cx)
})
.detach_and_log_err(cx);
}
fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
@@ -1107,13 +1311,13 @@ impl View for ContactList {
.boxed()
}
fn on_focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
if !self.filter_editor.is_focused(cx) {
cx.focus(&self.filter_editor);
}
}
fn on_focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
if !self.filter_editor.is_focused(cx) {
cx.emit(Event::Dismissed);
}

View File

@@ -160,7 +160,7 @@ impl View for ContactsPopover {
.boxed()
}
fn on_focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
match &self.child {
Child::ContactList(child) => cx.focus(child),

View File

@@ -18,34 +18,37 @@ pub fn init(cx: &mut MutableAppContext) {
let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
cx.spawn(|mut cx| async move {
let mut notification_window = None;
let mut notification_windows = Vec::new();
while let Some(incoming_call) = incoming_call.next().await {
if let Some(window_id) = notification_window.take() {
for window_id in notification_windows.drain(..) {
cx.remove_window(window_id);
}
if let Some(incoming_call) = incoming_call {
const PADDING: f32 = 16.;
let screen_size = cx.platform().screen_size();
let window_size = cx.read(|cx| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
vec2f(theme.window_width, theme.window_height)
});
let (window_id, _) = cx.add_window(
WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
window_size,
)),
titlebar: None,
center: false,
kind: WindowKind::PopUp,
is_movable: false,
},
|_| IncomingCallNotification::new(incoming_call),
);
notification_window = Some(window_id);
for screen in cx.platform().screens() {
let screen_size = screen.size();
let (window_id, _) = cx.add_window(
WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
window_size,
)),
titlebar: None,
center: false,
kind: WindowKind::PopUp,
is_movable: false,
screen: Some(screen),
},
|_| IncomingCallNotification::new(incoming_call.clone()),
);
notification_windows.push(window_id);
}
}
}
})

View File

@@ -27,41 +27,52 @@ pub fn init(cx: &mut MutableAppContext) {
worktree_root_names,
} => {
const PADDING: f32 = 16.;
let screen_size = cx.platform().screen_size();
let theme = &cx.global::<Settings>().theme.project_shared_notification;
let window_size = vec2f(theme.window_width, theme.window_height);
let (window_id, _) = cx.add_window(
WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
window_size,
)),
titlebar: None,
center: false,
kind: WindowKind::PopUp,
is_movable: false,
},
|_| {
ProjectSharedNotification::new(
owner.clone(),
*project_id,
worktree_root_names.clone(),
)
},
);
notification_windows.insert(*project_id, window_id);
for screen in cx.platform().screens() {
let screen_size = screen.size();
let (window_id, _) = cx.add_window(
WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
window_size,
)),
titlebar: None,
center: false,
kind: WindowKind::PopUp,
is_movable: false,
screen: Some(screen),
},
|_| {
ProjectSharedNotification::new(
owner.clone(),
*project_id,
worktree_root_names.clone(),
)
},
);
notification_windows
.entry(*project_id)
.or_insert(Vec::new())
.push(window_id);
}
}
room::Event::RemoteProjectUnshared { project_id } => {
if let Some(window_id) = notification_windows.remove(&project_id) {
cx.remove_window(window_id);
if let Some(window_ids) = notification_windows.remove(&project_id) {
for window_id in window_ids {
cx.remove_window(window_id);
}
}
}
room::Event::Left => {
for (_, window_id) in notification_windows.drain() {
cx.remove_window(window_id);
for (_, window_ids) in notification_windows.drain() {
for window_id in window_ids {
cx.remove_window(window_id);
}
}
}
_ => {}
})
.detach();
}

View File

@@ -135,7 +135,7 @@ impl View for CommandPalette {
ChildView::new(self.picker.clone(), cx).boxed()
}
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.picker);
}
@@ -224,7 +224,7 @@ impl PickerDelegate for CommandPalette {
fn render_match(
&self,
ix: usize,
mouse_state: MouseState,
mouse_state: &mut MouseState,
selected: bool,
cx: &gpui::AppContext,
) -> gpui::ElementBox {

View File

@@ -107,7 +107,7 @@ impl View for ContextMenu {
.boxed()
}
fn on_focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
self.reset(cx);
}
}
@@ -258,9 +258,10 @@ impl ContextMenu {
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item {
ContextMenuItem::Item { label, .. } => {
let style = style
.item
.style_for(Default::default(), Some(ix) == self.selected_index);
let style = style.item.style_for(
&mut Default::default(),
Some(ix) == self.selected_index,
);
Label::new(label.to_string(), style.label.clone())
.contained()
@@ -283,9 +284,10 @@ impl ContextMenu {
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item {
ContextMenuItem::Item { action, .. } => {
let style = style
.item
.style_for(Default::default(), Some(ix) == self.selected_index);
let style = style.item.style_for(
&mut Default::default(),
Some(ix) == self.selected_index,
);
KeystrokeLabel::new(
action.boxed_clone(),
style.keystroke.container,
@@ -313,13 +315,16 @@ impl ContextMenu {
fn render_menu(&self, cx: &mut RenderContext<Self>) -> impl Element {
enum Menu {}
enum MenuItem {}
let style = cx.global::<Settings>().theme.context_menu.clone();
MouseEventHandler::<Menu>::new(0, cx, |_, cx| {
Flex::column()
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item {
ContextMenuItem::Item { label, action } => {
let action = action.boxed_clone();
MouseEventHandler::<MenuItem>::new(ix, cx, |state, _| {
let style =
style.item.style_for(state, Some(ix) == self.selected_index);
@@ -348,6 +353,7 @@ impl ContextMenu {
cx.dispatch_action(Clicked);
cx.dispatch_any_action(action.boxed_clone());
})
.on_drag(MouseButton::Left, |_, _| {})
.boxed()
}
ContextMenuItem::Separator => Empty::new()

View File

@@ -14,8 +14,13 @@ test-support = []
collections = { path = "../collections" }
anyhow = "1.0.57"
async-trait = "0.1"
lazy_static = "1.4.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
rocksdb = "0.18"
rusqlite = { version = "0.28.0", features = ["bundled", "serde_json"] }
rusqlite_migration = "1.0.0"
serde = { workspace = true }
serde_rusqlite = "0.31.0"
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }

View File

@@ -1,161 +1,119 @@
use anyhow::Result;
use std::path::Path;
mod kvp;
mod migrations;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
pub struct Db(DbStore);
use anyhow::Result;
use log::error;
use parking_lot::Mutex;
use rusqlite::Connection;
enum DbStore {
use migrations::MIGRATIONS;
#[derive(Clone)]
pub enum Db {
Real(Arc<RealDb>),
Null,
Real(rocksdb::DB),
}
#[cfg(any(test, feature = "test-support"))]
Fake {
data: parking_lot::Mutex<collections::HashMap<Vec<u8>, Vec<u8>>>,
},
pub struct RealDb {
connection: Mutex<Connection>,
path: Option<PathBuf>,
}
impl Db {
/// Open or create a database at the given file path.
pub fn open(path: &Path) -> Result<Arc<Self>> {
let db = rocksdb::DB::open_default(path)?;
Ok(Arc::new(Self(DbStore::Real(db))))
/// Open or create a database at the given directory path.
pub fn open(db_dir: &Path) -> Self {
// Use 0 for now. Will implement incrementing and clearing of old db files soon TM
let current_db_dir = db_dir.join(Path::new("0"));
fs::create_dir_all(&current_db_dir)
.expect("Should be able to create the database directory");
let db_path = current_db_dir.join(Path::new("db.sqlite"));
Connection::open(db_path)
.map_err(Into::into)
.and_then(|connection| Self::initialize(connection))
.map(|connection| {
Db::Real(Arc::new(RealDb {
connection,
path: Some(db_dir.to_path_buf()),
}))
})
.unwrap_or_else(|e| {
error!(
"Connecting to file backed db failed. Reverting to null db. {}",
e
);
Self::Null
})
}
/// Open a null database that stores no data, for use as a fallback
/// when there is an error opening the real database.
pub fn null() -> Arc<Self> {
Arc::new(Self(DbStore::Null))
}
/// Open a fake database for testing.
/// Open a in memory database for testing and as a fallback.
#[cfg(any(test, feature = "test-support"))]
pub fn open_fake() -> Arc<Self> {
Arc::new(Self(DbStore::Fake {
data: Default::default(),
}))
pub fn open_in_memory() -> Self {
Connection::open_in_memory()
.map_err(Into::into)
.and_then(|connection| Self::initialize(connection))
.map(|connection| {
Db::Real(Arc::new(RealDb {
connection,
path: None,
}))
})
.unwrap_or_else(|e| {
error!(
"Connecting to in memory db failed. Reverting to null db. {}",
e
);
Self::Null
})
}
pub fn read<K, I>(&self, keys: I) -> Result<Vec<Option<Vec<u8>>>>
where
K: AsRef<[u8]>,
I: IntoIterator<Item = K>,
{
match &self.0 {
DbStore::Real(db) => db
.multi_get(keys)
.into_iter()
.map(|e| e.map_err(Into::into))
.collect(),
fn initialize(mut conn: Connection) -> Result<Mutex<Connection>> {
MIGRATIONS.to_latest(&mut conn)?;
DbStore::Null => Ok(keys.into_iter().map(|_| None).collect()),
conn.pragma_update(None, "journal_mode", "WAL")?;
conn.pragma_update(None, "synchronous", "NORMAL")?;
conn.pragma_update(None, "foreign_keys", true)?;
conn.pragma_update(None, "case_sensitive_like", true)?;
#[cfg(any(test, feature = "test-support"))]
DbStore::Fake { data: db } => {
let db = db.lock();
Ok(keys
.into_iter()
.map(|key| db.get(key.as_ref()).cloned())
.collect())
}
}
Ok(Mutex::new(conn))
}
pub fn delete<K, I>(&self, keys: I) -> Result<()>
where
K: AsRef<[u8]>,
I: IntoIterator<Item = K>,
{
match &self.0 {
DbStore::Real(db) => {
let mut batch = rocksdb::WriteBatch::default();
for key in keys {
batch.delete(key);
}
db.write(batch)?;
}
DbStore::Null => {}
#[cfg(any(test, feature = "test-support"))]
DbStore::Fake { data: db } => {
let mut db = db.lock();
for key in keys {
db.remove(key.as_ref());
}
}
}
Ok(())
pub fn persisting(&self) -> bool {
self.real().and_then(|db| db.path.as_ref()).is_some()
}
pub fn write<K, V, I>(&self, entries: I) -> Result<()>
where
K: AsRef<[u8]>,
V: AsRef<[u8]>,
I: IntoIterator<Item = (K, V)>,
{
match &self.0 {
DbStore::Real(db) => {
let mut batch = rocksdb::WriteBatch::default();
for (key, value) in entries {
batch.put(key, value);
}
db.write(batch)?;
}
DbStore::Null => {}
#[cfg(any(test, feature = "test-support"))]
DbStore::Fake { data: db } => {
let mut db = db.lock();
for (key, value) in entries {
db.insert(key.as_ref().into(), value.as_ref().into());
}
}
pub fn real(&self) -> Option<&RealDb> {
match self {
Db::Real(db) => Some(&db),
_ => None,
}
}
}
impl Drop for Db {
fn drop(&mut self) {
match self {
Db::Real(real_db) => {
let lock = real_db.connection.lock();
let _ = lock.pragma_update(None, "analysis_limit", "500");
let _ = lock.pragma_update(None, "optimize", "");
}
Db::Null => {}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempdir::TempDir;
use crate::migrations::MIGRATIONS;
#[gpui::test]
fn test_db() {
let dir = TempDir::new("db-test").unwrap();
let fake_db = Db::open_fake();
let real_db = Db::open(&dir.path().join("test.db")).unwrap();
for db in [&real_db, &fake_db] {
assert_eq!(
db.read(["key-1", "key-2", "key-3"]).unwrap(),
&[None, None, None]
);
db.write([("key-1", "one"), ("key-3", "three")]).unwrap();
assert_eq!(
db.read(["key-1", "key-2", "key-3"]).unwrap(),
&[
Some("one".as_bytes().to_vec()),
None,
Some("three".as_bytes().to_vec())
]
);
db.delete(["key-3", "key-4"]).unwrap();
assert_eq!(
db.read(["key-1", "key-2", "key-3"]).unwrap(),
&[Some("one".as_bytes().to_vec()), None, None,]
);
}
drop(real_db);
let real_db = Db::open(&dir.path().join("test.db")).unwrap();
assert_eq!(
real_db.read(["key-1", "key-2", "key-3"]).unwrap(),
&[Some("one".as_bytes().to_vec()), None, None,]
);
#[test]
fn test_migrations() {
assert!(MIGRATIONS.validate().is_ok());
}
}

311
crates/db/src/items.rs Normal file
View File

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

82
crates/db/src/kvp.rs Normal file
View File

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

View File

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

View File

@@ -99,7 +99,7 @@ impl View for ProjectDiagnosticsEditor {
}
}
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if !self.path_states.is_empty() {
cx.focus(&self.editor);
}

View File

@@ -4,7 +4,7 @@ use collections::HashSet;
use gpui::{
elements::{MouseEventHandler, Overlay},
geometry::vector::Vector2F,
scene::DragRegionEvent,
scene::MouseDrag,
CursorStyle, Element, ElementBox, EventContext, MouseButton, MutableAppContext, RenderContext,
View, WeakViewHandle,
};
@@ -70,7 +70,7 @@ impl<V: View> DragAndDrop<V> {
}
pub fn dragging<T: Any>(
event: DragRegionEvent,
event: MouseDrag,
payload: Rc<T>,
cx: &mut EventContext,
render: Rc<impl 'static + Fn(&T, &mut RenderContext<V>) -> ElementBox>,
@@ -125,7 +125,7 @@ impl<V: View> DragAndDrop<V> {
cx.defer(|cx| {
cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
});
cx.propogate_event();
cx.propagate_event();
})
.on_up_out(MouseButton::Left, |_, cx| {
cx.defer(|cx| {

View File

@@ -20,6 +20,7 @@ test-support = [
]
[dependencies]
drag_and_drop = { path = "../drag_and_drop" }
text = { path = "../text" }
clock = { path = "../clock" }
collections = { path = "../collections" }

View File

@@ -0,0 +1,111 @@
use std::time::Duration;
use gpui::{Entity, ModelContext};
use settings::Settings;
use smol::Timer;
pub struct BlinkManager {
blink_interval: Duration,
blink_epoch: usize,
blinking_paused: bool,
visible: bool,
enabled: bool,
}
impl BlinkManager {
pub fn new(blink_interval: Duration, cx: &mut ModelContext<Self>) -> Self {
let weak_handle = cx.weak_handle();
cx.observe_global::<Settings, _>(move |_, cx| {
if let Some(this) = weak_handle.upgrade(cx) {
// Make sure we blink the cursors if the setting is re-enabled
this.update(cx, |this, cx| this.blink_cursors(this.blink_epoch, cx));
}
})
.detach();
Self {
blink_interval,
blink_epoch: 0,
blinking_paused: false,
visible: true,
enabled: false,
}
}
fn next_blink_epoch(&mut self) -> usize {
self.blink_epoch += 1;
self.blink_epoch
}
pub fn pause_blinking(&mut self, cx: &mut ModelContext<Self>) {
if !self.visible {
self.visible = true;
cx.notify();
}
let epoch = self.next_blink_epoch();
let interval = self.blink_interval;
cx.spawn(|this, mut cx| {
let this = this.downgrade();
async move {
Timer::after(interval).await;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
}
}
})
.detach();
}
fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
if epoch == self.blink_epoch {
self.blinking_paused = false;
self.blink_cursors(epoch, cx);
}
}
fn blink_cursors(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
if cx.global::<Settings>().cursor_blink {
if epoch == self.blink_epoch && self.enabled && !self.blinking_paused {
self.visible = !self.visible;
cx.notify();
dbg!(cx.handle());
let epoch = self.next_blink_epoch();
let interval = self.blink_interval;
cx.spawn(|this, mut cx| {
let this = this.downgrade();
async move {
Timer::after(interval).await;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
}
}
})
.detach();
}
} else if !self.visible {
self.visible = true;
cx.notify();
}
}
pub fn enable(&mut self, cx: &mut ModelContext<Self>) {
self.enabled = true;
self.blink_cursors(self.blink_epoch, cx);
}
pub fn disable(&mut self, _cx: &mut ModelContext<Self>) {
self.enabled = false;
}
pub fn visible(&self) -> bool {
self.visible
}
}
impl Entity for BlinkManager {
type Event = ();
}

View File

@@ -157,6 +157,7 @@ pub struct BlockChunks<'a> {
max_output_row: u32,
}
#[derive(Clone)]
pub struct BlockBufferRows<'a> {
transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>,
input_buffer_rows: wrap_map::WrapBufferRows<'a>,

View File

@@ -987,6 +987,7 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize {
}
}
#[derive(Clone)]
pub struct FoldBufferRows<'a> {
cursor: Cursor<'a, Transform, (FoldPoint, Point)>,
input_buffer_rows: MultiBufferRows<'a>,

View File

@@ -62,6 +62,7 @@ pub struct WrapChunks<'a> {
transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
}
#[derive(Clone)]
pub struct WrapBufferRows<'a> {
input_buffer_rows: fold_map::FoldBufferRows<'a>,
input_buffer_row: Option<u32>,

View File

@@ -1,3 +1,4 @@
mod blink_manager;
pub mod display_map;
mod element;
mod highlight_matching_bracket;
@@ -16,6 +17,7 @@ pub mod test;
use aho_corasick::AhoCorasick;
use anyhow::Result;
use blink_manager::BlinkManager;
use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
pub use display_map::DisplayPoint;
@@ -33,20 +35,22 @@ use gpui::{
impl_actions, impl_internal_actions,
platform::CursorStyle,
serde_json::json,
text_layout, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox,
Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View,
ViewContext, ViewHandle, WeakViewHandle,
text_layout, AnyViewHandle, AppContext, AsyncAppContext, Axis, ClipboardItem, Element,
ElementBox, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription,
Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
pub use items::MAX_TAB_TITLE_LEN;
pub use language::{char_kind, CharKind};
use language::{
AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic,
DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point,
Selection, SelectionGoal, TransactionId,
AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
Diagnostic, DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16,
Point, Selection, SelectionGoal, TransactionId,
};
use link_go_to_definition::{
hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
};
use link_go_to_definition::{hide_link_definition, LinkGoToDefinitionState};
pub use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
ToPoint,
@@ -80,6 +84,7 @@ const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
const MAX_LINE_LEN: usize = 1024;
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
const MAX_SELECTION_HISTORY_LEN: usize = 1024;
pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
@@ -90,7 +95,10 @@ pub struct SelectNext {
}
#[derive(Clone, PartialEq)]
pub struct Scroll(pub Vector2F);
pub struct Scroll {
pub scroll_position: Vector2F,
pub axis: Option<Axis>,
}
#[derive(Clone, PartialEq)]
pub struct Select(pub SelectPhase);
@@ -108,6 +116,18 @@ pub struct SelectToBeginningOfLine {
stop_at_soft_wraps: bool,
}
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct MovePageUp {
#[serde(default)]
center_cursor: bool,
}
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct MovePageDown {
#[serde(default)]
center_cursor: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
pub struct SelectToEndOfLine {
#[serde(default)]
@@ -161,8 +181,11 @@ actions!(
Paste,
Undo,
Redo,
CenterScreen,
MoveUp,
PageUp,
MoveDown,
PageDown,
MoveLeft,
MoveRight,
MoveToPreviousWordStart,
@@ -202,8 +225,6 @@ actions!(
FindAllReferences,
Rename,
ConfirmRename,
PageUp,
PageDown,
Fold,
UnfoldLines,
FoldSelectedRanges,
@@ -222,6 +243,8 @@ impl_actions!(
SelectToBeginningOfLine,
SelectToEndOfLine,
ToggleCodeActions,
MovePageUp,
MovePageDown,
ConfirmCompletion,
ConfirmCodeAction,
]
@@ -244,7 +267,7 @@ struct ScrollbarAutoHide(bool);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(Editor::new_file);
cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx));
cx.add_action(Editor::scroll);
cx.add_action(Editor::select);
cx.add_action(Editor::cancel);
cx.add_action(Editor::newline);
@@ -273,7 +296,12 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(Editor::undo);
cx.add_action(Editor::redo);
cx.add_action(Editor::move_up);
cx.add_action(Editor::move_page_up);
cx.add_action(Editor::page_up);
cx.add_action(Editor::move_down);
cx.add_action(Editor::move_page_down);
cx.add_action(Editor::page_down);
cx.add_action(Editor::center_screen);
cx.add_action(Editor::move_left);
cx.add_action(Editor::move_right);
cx.add_action(Editor::move_to_previous_word_start);
@@ -312,8 +340,6 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(Editor::go_to_prev_diagnostic);
cx.add_action(Editor::go_to_definition);
cx.add_action(Editor::go_to_type_definition);
cx.add_action(Editor::page_up);
cx.add_action(Editor::page_down);
cx.add_action(Editor::fold);
cx.add_action(Editor::unfold_lines);
cx.add_action(Editor::fold_selected_ranges);
@@ -407,6 +433,69 @@ pub type GetFieldEditorTheme = fn(&theme::Theme) -> theme::FieldEditor;
type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
#[derive(Clone, Copy)]
pub struct OngoingScroll {
last_timestamp: Instant,
axis: Option<Axis>,
}
impl OngoingScroll {
fn initial() -> OngoingScroll {
OngoingScroll {
last_timestamp: Instant::now() - SCROLL_EVENT_SEPARATION,
axis: None,
}
}
fn update(&mut self, axis: Option<Axis>) {
self.last_timestamp = Instant::now();
self.axis = axis;
}
pub fn filter(&self, delta: &mut Vector2F) -> Option<Axis> {
const UNLOCK_PERCENT: f32 = 1.9;
const UNLOCK_LOWER_BOUND: f32 = 6.;
let mut axis = self.axis;
let x = delta.x().abs();
let y = delta.y().abs();
let duration = Instant::now().duration_since(self.last_timestamp);
if duration > SCROLL_EVENT_SEPARATION {
//New ongoing scroll will start, determine axis
axis = if x <= y {
Some(Axis::Vertical)
} else {
Some(Axis::Horizontal)
};
} else if x.max(y) >= UNLOCK_LOWER_BOUND {
//Check if the current ongoing will need to unlock
match axis {
Some(Axis::Vertical) => {
if x > y && x >= y * UNLOCK_PERCENT {
axis = None;
}
}
Some(Axis::Horizontal) => {
if y > x && y >= x * UNLOCK_PERCENT {
axis = None;
}
}
None => {}
}
}
match axis {
Some(Axis::Vertical) => *delta = vec2f(0., delta.y()),
Some(Axis::Horizontal) => *delta = vec2f(delta.x(), 0.),
None => {}
}
axis
}
}
pub struct Editor {
handle: WeakViewHandle<Self>,
buffer: ModelHandle<MultiBuffer>,
@@ -421,6 +510,7 @@ pub struct Editor {
select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
ime_transaction: Option<TransactionId>,
active_diagnostics: Option<ActiveDiagnosticGroup>,
ongoing_scroll: OngoingScroll,
scroll_position: Vector2F,
scroll_top_anchor: Anchor,
autoscroll_request: Option<(Autoscroll, bool)>,
@@ -429,12 +519,10 @@ pub struct Editor {
override_text_style: Option<Box<OverrideTextStyle>>,
project: Option<ModelHandle<Project>>,
focused: bool,
show_local_cursors: bool,
blink_manager: ModelHandle<BlinkManager>,
show_local_selections: bool,
show_scrollbars: bool,
hide_scrollbar_task: Option<Task<()>>,
blink_epoch: usize,
blinking_paused: bool,
mode: EditorMode,
vertical_scroll_margin: f32,
placeholder_text: Option<Arc<str>>,
@@ -466,6 +554,7 @@ pub struct EditorSnapshot {
pub display_snapshot: DisplaySnapshot,
pub placeholder_text: Option<Arc<str>>,
is_focused: bool,
ongoing_scroll: OngoingScroll,
scroll_position: Vector2F,
scroll_top_anchor: Anchor,
}
@@ -606,6 +695,18 @@ enum ContextMenu {
}
impl ContextMenu {
fn select_first(&mut self, cx: &mut ViewContext<Editor>) -> bool {
if self.visible() {
match self {
ContextMenu::Completions(menu) => menu.select_first(cx),
ContextMenu::CodeActions(menu) => menu.select_first(cx),
}
true
} else {
false
}
}
fn select_prev(&mut self, cx: &mut ViewContext<Editor>) -> bool {
if self.visible() {
match self {
@@ -630,6 +731,18 @@ impl ContextMenu {
}
}
fn select_last(&mut self, cx: &mut ViewContext<Editor>) -> bool {
if self.visible() {
match self {
ContextMenu::Completions(menu) => menu.select_last(cx),
ContextMenu::CodeActions(menu) => menu.select_last(cx),
}
true
} else {
false
}
}
fn visible(&self) -> bool {
match self {
ContextMenu::Completions(menu) => menu.visible(),
@@ -662,6 +775,12 @@ struct CompletionsMenu {
}
impl CompletionsMenu {
fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
self.selected_item = 0;
self.list.scroll_to(ScrollTarget::Show(self.selected_item));
cx.notify();
}
fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
if self.selected_item > 0 {
self.selected_item -= 1;
@@ -678,6 +797,12 @@ impl CompletionsMenu {
cx.notify();
}
fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
self.selected_item = self.matches.len() - 1;
self.list.scroll_to(ScrollTarget::Show(self.selected_item));
cx.notify();
}
fn visible(&self) -> bool {
!self.matches.is_empty()
}
@@ -705,7 +830,7 @@ impl CompletionsMenu {
|state, _| {
let item_style = if item_ix == selected_item {
style.autocomplete.selected_item
} else if state.hovered {
} else if state.hovered() {
style.autocomplete.hovered_item
} else {
style.autocomplete.item
@@ -809,6 +934,11 @@ struct CodeActionsMenu {
}
impl CodeActionsMenu {
fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
self.selected_item = 0;
cx.notify()
}
fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
if self.selected_item > 0 {
self.selected_item -= 1;
@@ -823,6 +953,11 @@ impl CodeActionsMenu {
}
}
fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
self.selected_item = self.actions.len() - 1;
cx.notify()
}
fn visible(&self) -> bool {
!self.actions.is_empty()
}
@@ -850,7 +985,7 @@ impl CodeActionsMenu {
MouseEventHandler::<ActionTag>::new(item_ix, cx, |state, _| {
let item_style = if item_ix == selected_item {
style.autocomplete.selected_item
} else if state.hovered {
} else if state.hovered() {
style.autocomplete.hovered_item
} else {
style.autocomplete.item
@@ -1012,6 +1147,8 @@ impl Editor {
let selections = SelectionsCollection::new(display_map.clone(), buffer.clone());
let blink_manager = cx.add_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
let mut this = Self {
handle: cx.weak_handle(),
buffer: buffer.clone(),
@@ -1029,16 +1166,15 @@ impl Editor {
soft_wrap_mode_override: None,
get_field_editor_theme,
project,
ongoing_scroll: OngoingScroll::initial(),
scroll_position: Vector2F::zero(),
scroll_top_anchor: Anchor::min(),
autoscroll_request: None,
focused: false,
show_local_cursors: false,
blink_manager: blink_manager.clone(),
show_local_selections: true,
show_scrollbars: true,
hide_scrollbar_task: None,
blink_epoch: 0,
blinking_paused: false,
mode,
vertical_scroll_margin: 3.0,
placeholder_text: None,
@@ -1066,6 +1202,7 @@ impl Editor {
cx.observe(&buffer, Self::on_buffer_changed),
cx.subscribe(&buffer, Self::on_buffer_event),
cx.observe(&display_map, Self::on_display_map_changed),
cx.observe(&blink_manager, |_, _, cx| cx.notify()),
],
};
this.end_selection(cx);
@@ -1122,6 +1259,7 @@ impl Editor {
EditorSnapshot {
mode: self.mode,
display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
ongoing_scroll: self.ongoing_scroll,
scroll_position: self.scroll_position,
scroll_top_anchor: self.scroll_top_anchor.clone(),
placeholder_text: self.placeholder_text.clone(),
@@ -1414,6 +1552,7 @@ impl Editor {
buffer.set_active_selections(
&self.selections.disjoint_anchors(),
self.selections.line_mode,
self.cursor_shape,
cx,
)
});
@@ -1477,7 +1616,7 @@ impl Editor {
refresh_matching_bracket_highlights(self, cx);
}
self.pause_cursor_blinking(cx);
self.blink_manager.update(cx, BlinkManager::pause_blinking);
cx.emit(Event::SelectionsChanged { local });
cx.notify();
}
@@ -1524,6 +1663,11 @@ impl Editor {
});
}
fn scroll(&mut self, action: &Scroll, cx: &mut ViewContext<Self>) {
self.ongoing_scroll.update(action.axis);
self.set_scroll_position(action.scroll_position, cx);
}
fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext<Self>) {
self.hide_context_menu(cx);
@@ -1936,11 +2080,14 @@ impl Editor {
));
continue;
}
} else if let Some(region) = autoclose_region {
}
if let Some(region) = autoclose_region {
// If the selection is followed by an auto-inserted closing bracket,
// then don't insert anything else; just move the selection past the
// closing bracket.
let should_skip = selection.end == region.range.end.to_point(&snapshot);
// then don't insert that closing bracket again; just move the selection
// past the closing bracket.
let should_skip = selection.end == region.range.end.to_point(&snapshot)
&& text.as_ref() == region.pair.end.as_str();
if should_skip {
let anchor = snapshot.anchor_after(selection.end);
new_selections.push((
@@ -2023,7 +2170,7 @@ impl Editor {
}
drop(snapshot);
this.change_selections(None, cx, |s| s.select(new_selections));
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(new_selections));
this.trigger_completion_on_input(&text, cx);
});
}
@@ -3848,6 +3995,23 @@ impl Editor {
})
}
pub fn center_screen(&mut self, _: &CenterScreen, cx: &mut ViewContext<Self>) {
if self.take_rename(true, cx).is_some() {
return;
}
if let Some(_) = self.context_menu.as_mut() {
return;
}
if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate_action();
return;
}
self.request_autoscroll(Autoscroll::Center, cx);
}
pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
if self.take_rename(true, cx).is_some() {
return;
@@ -3876,6 +4040,72 @@ impl Editor {
})
}
pub fn move_page_up(&mut self, action: &MovePageUp, cx: &mut ViewContext<Self>) {
if self.take_rename(true, cx).is_some() {
return;
}
if let Some(context_menu) = self.context_menu.as_mut() {
if context_menu.select_first(cx) {
return;
}
}
if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate_action();
return;
}
let row_count = match self.visible_line_count {
Some(row_count) => row_count as u32 - 1,
None => return,
};
let autoscroll = if action.center_cursor {
Autoscroll::Center
} else {
Autoscroll::Fit
};
self.change_selections(Some(autoscroll), cx, |s| {
let line_mode = s.line_mode;
s.move_with(|map, selection| {
if !selection.is_empty() && !line_mode {
selection.goal = SelectionGoal::None;
}
let (cursor, goal) =
movement::up_by_rows(map, selection.end, row_count, selection.goal, false);
selection.collapse_to(cursor, goal);
});
});
}
pub fn page_up(&mut self, _: &PageUp, cx: &mut ViewContext<Self>) {
if self.take_rename(true, cx).is_some() {
return;
}
if let Some(context_menu) = self.context_menu.as_mut() {
if context_menu.select_first(cx) {
return;
}
}
if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate_action();
return;
}
let lines = match self.visible_line_count {
Some(lines) => lines,
None => return,
};
let cur_position = self.scroll_position(cx);
let new_pos = cur_position - vec2f(0., lines + 1.);
self.set_scroll_position(new_pos, cx);
}
pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext<Self>) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_heads_with(|map, head, goal| movement::up(map, head, goal, false))
@@ -3908,6 +4138,72 @@ impl Editor {
});
}
pub fn move_page_down(&mut self, action: &MovePageDown, cx: &mut ViewContext<Self>) {
if self.take_rename(true, cx).is_some() {
return;
}
if let Some(context_menu) = self.context_menu.as_mut() {
if context_menu.select_last(cx) {
return;
}
}
if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate_action();
return;
}
let row_count = match self.visible_line_count {
Some(row_count) => row_count as u32 - 1,
None => return,
};
let autoscroll = if action.center_cursor {
Autoscroll::Center
} else {
Autoscroll::Fit
};
self.change_selections(Some(autoscroll), cx, |s| {
let line_mode = s.line_mode;
s.move_with(|map, selection| {
if !selection.is_empty() && !line_mode {
selection.goal = SelectionGoal::None;
}
let (cursor, goal) =
movement::down_by_rows(map, selection.end, row_count, selection.goal, false);
selection.collapse_to(cursor, goal);
});
});
}
pub fn page_down(&mut self, _: &PageDown, cx: &mut ViewContext<Self>) {
if self.take_rename(true, cx).is_some() {
return;
}
if let Some(context_menu) = self.context_menu.as_mut() {
if context_menu.select_last(cx) {
return;
}
}
if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate_action();
return;
}
let lines = match self.visible_line_count {
Some(lines) => lines,
None => return,
};
let cur_position = self.scroll_position(cx);
let new_pos = cur_position + vec2f(0., lines - 1.);
self.set_scroll_position(new_pos, cx);
}
pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext<Self>) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_heads_with(|map, head, goal| movement::down(map, head, goal, false))
@@ -5535,28 +5831,6 @@ impl Editor {
}
}
pub fn page_up(&mut self, _: &PageUp, cx: &mut ViewContext<Self>) {
let lines = match self.visible_line_count {
Some(lines) => lines,
None => return,
};
let cur_position = self.scroll_position(cx);
let new_pos = cur_position - vec2f(0., lines + 1.);
self.set_scroll_position(new_pos, cx);
}
pub fn page_down(&mut self, _: &PageDown, cx: &mut ViewContext<Self>) {
let lines = match self.visible_line_count {
Some(lines) => lines,
None => return,
};
let cur_position = self.scroll_position(cx);
let new_pos = cur_position + vec2f(0., lines - 1.);
self.set_scroll_position(new_pos, cx);
}
pub fn fold(&mut self, _: &Fold, cx: &mut ViewContext<Self>) {
let mut fold_ranges = Vec::new();
@@ -5791,8 +6065,11 @@ impl Editor {
&mut self,
cx: &mut ViewContext<Self>,
) -> Option<(fn(&Theme) -> Color, Vec<Range<Anchor>>)> {
cx.notify();
self.background_highlights.remove(&TypeId::of::<T>())
let highlights = self.background_highlights.remove(&TypeId::of::<T>());
if highlights.is_some() {
cx.notify();
}
highlights
}
#[cfg(feature = "test-support")]
@@ -5906,65 +6183,17 @@ impl Editor {
&mut self,
cx: &mut ViewContext<Self>,
) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
cx.notify();
self.display_map
.update(cx, |map, _| map.clear_text_highlights(TypeId::of::<T>()))
}
fn next_blink_epoch(&mut self) -> usize {
self.blink_epoch += 1;
self.blink_epoch
}
fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
if !self.focused {
return;
}
self.show_local_cursors = true;
cx.notify();
let epoch = self.next_blink_epoch();
cx.spawn(|this, mut cx| {
let this = this.downgrade();
async move {
Timer::after(CURSOR_BLINK_INTERVAL).await;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
}
}
})
.detach();
}
fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
if epoch == self.blink_epoch {
self.blinking_paused = false;
self.blink_cursors(epoch, cx);
}
}
fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
if epoch == self.blink_epoch && self.focused && !self.blinking_paused {
self.show_local_cursors = !self.show_local_cursors;
let highlights = self
.display_map
.update(cx, |map, _| map.clear_text_highlights(TypeId::of::<T>()));
if highlights.is_some() {
cx.notify();
let epoch = self.next_blink_epoch();
cx.spawn(|this, mut cx| {
let this = this.downgrade();
async move {
Timer::after(CURSOR_BLINK_INTERVAL).await;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
}
}
})
.detach();
}
highlights
}
pub fn show_local_cursors(&self) -> bool {
self.show_local_cursors && self.focused
pub fn show_local_cursors(&self, cx: &AppContext) -> bool {
self.blink_manager.read(cx).visible() && self.focused
}
pub fn show_scrollbars(&self) -> bool {
@@ -6267,9 +6496,7 @@ impl View for Editor {
}
Stack::new()
.with_child(
EditorElement::new(self.handle.clone(), style.clone(), self.cursor_shape).boxed(),
)
.with_child(EditorElement::new(self.handle.clone(), style.clone()).boxed())
.with_child(ChildView::new(&self.mouse_context_menu, cx).boxed())
.boxed()
}
@@ -6278,20 +6505,23 @@ impl View for Editor {
"Editor"
}
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
let focused_event = EditorFocused(cx.handle());
cx.emit_global(focused_event);
if let Some(rename) = self.pending_rename.as_ref() {
cx.focus(&rename.editor);
} else {
if !self.focused {
self.blink_manager.update(cx, BlinkManager::enable);
}
self.focused = true;
self.blink_cursors(self.blink_epoch, cx);
self.buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction(cx);
if self.leader_replica_id.is_none() {
buffer.set_active_selections(
&self.selections.disjoint_anchors(),
self.selections.line_mode,
self.cursor_shape,
cx,
);
}
@@ -6299,10 +6529,11 @@ impl View for Editor {
}
}
fn on_focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
let blurred_event = EditorBlurred(cx.handle());
cx.emit_global(blurred_event);
self.focused = false;
self.blink_manager.update(cx, BlinkManager::disable);
self.buffer
.update(cx, |buffer, cx| buffer.remove_active_selections(cx));
self.hide_context_menu(cx);
@@ -6311,6 +6542,44 @@ impl View for Editor {
cx.notify();
}
fn modifiers_changed(
&mut self,
event: &gpui::ModifiersChangedEvent,
cx: &mut ViewContext<Self>,
) -> bool {
let pending_selection = self.has_pending_selection();
if let Some(point) = self.link_go_to_definition_state.last_mouse_location.clone() {
if event.cmd && !pending_selection {
let snapshot = self.snapshot(cx);
let kind = if event.shift {
LinkDefinitionKind::Type
} else {
LinkDefinitionKind::Symbol
};
show_link_definition(kind, self, point, snapshot, cx);
return false;
}
}
{
if self.link_go_to_definition_state.symbol_range.is_some()
|| !self.link_go_to_definition_state.definitions.is_empty()
{
self.link_go_to_definition_state.symbol_range.take();
self.link_go_to_definition_state.definitions.clear();
cx.notify();
}
self.link_go_to_definition_state.task = None;
self.clear_text_highlights::<LinkGoToDefinitionState>(cx);
}
false
}
fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
let mut context = Self::default_keymap_context();
let mode = match self.mode {

View File

@@ -1,5 +1,6 @@
use std::{cell::RefCell, rc::Rc, time::Instant};
use drag_and_drop::DragAndDrop;
use futures::StreamExt;
use indoc::indoc;
use unindent::Unindent;
@@ -472,6 +473,7 @@ fn test_clone(cx: &mut gpui::MutableAppContext) {
#[gpui::test]
fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
cx.set_global(DragAndDrop::<Workspace>::default());
use workspace::Item;
let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(None, cx));
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
@@ -1194,6 +1196,120 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut gpui::MutableAppContext) {
});
}
#[gpui::test]
async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
let mut cx = EditorTestContext::new(cx);
let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
cx.simulate_window_resize(cx.window_id, vec2f(100., 4. * line_height));
cx.set_state(
&r#"
ˇone
two
threeˇ
four
five
six
seven
eight
nine
ten
"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
cx.assert_editor_state(
&r#"
one
two
three
ˇfour
five
sixˇ
seven
eight
nine
ten
"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
cx.assert_editor_state(
&r#"
one
two
three
four
five
six
ˇseven
eight
nineˇ
ten
"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
cx.assert_editor_state(
&r#"
one
two
three
ˇfour
five
sixˇ
seven
eight
nine
ten
"#
.unindent(),
);
cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
cx.assert_editor_state(
&r#"
ˇone
two
threeˇ
four
five
six
seven
eight
nine
ten
"#
.unindent(),
);
// Test select collapsing
cx.update_editor(|editor, cx| {
editor.move_page_down(&MovePageDown::default(), cx);
editor.move_page_down(&MovePageDown::default(), cx);
editor.move_page_down(&MovePageDown::default(), cx);
});
cx.assert_editor_state(
&r#"
one
two
three
four
five
six
seven
eight
nine
ˇten
ˇ"#
.unindent(),
);
}
#[gpui::test]
async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
let mut cx = EditorTestContext::new(cx);
@@ -2907,6 +3023,12 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
close: true,
newline: true,
},
BracketPair {
start: "(".to_string(),
end: ")".to_string(),
close: true,
newline: true,
},
BracketPair {
start: "/*".to_string(),
end: " */".to_string(),
@@ -2919,6 +3041,12 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
close: false,
newline: true,
},
BracketPair {
start: "\"".to_string(),
end: "\"".to_string(),
close: true,
newline: false,
},
],
autoclose_before: "})]".to_string(),
..Default::default()
@@ -2957,6 +3085,19 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
.unindent(),
);
// insert a different closing bracket
cx.update_editor(|view, cx| {
view.handle_input(")", cx);
});
cx.assert_editor_state(
&"
🏀{{{)ˇ}}}
ε{{{)ˇ}}}
❤️{{{)ˇ}}}
"
.unindent(),
);
// skip over the auto-closed brackets when typing a closing bracket
cx.update_editor(|view, cx| {
view.move_right(&MoveRight, cx);
@@ -2966,9 +3107,9 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
});
cx.assert_editor_state(
&"
🏀{{{}}}}ˇ
ε{{{}}}}ˇ
❤️{{{}}}}ˇ
🏀{{{)}}}}ˇ
ε{{{)}}}}ˇ
❤️{{{)}}}}ˇ
"
.unindent(),
);
@@ -3026,6 +3167,13 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
cx.set_state("«aˇ» b");
cx.update_editor(|view, cx| view.handle_input("{", cx));
cx.assert_editor_state("{«aˇ»} b");
// Autclose pair where the start and end characters are the same
cx.set_state("");
cx.update_editor(|view, cx| view.handle_input("\"", cx));
cx.assert_editor_state("a\"ˇ\"");
cx.update_editor(|view, cx| view.handle_input("\"", cx));
cx.assert_editor_state("a\"\"ˇ");
}
#[gpui::test]

View File

@@ -9,14 +9,14 @@ use crate::{
HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
},
link_go_to_definition::{
CmdShiftChanged, GoToFetchedDefinition, GoToFetchedTypeDefinition, UpdateGoToDefinitionLink,
GoToFetchedDefinition, GoToFetchedTypeDefinition, UpdateGoToDefinitionLink,
},
mouse_context_menu::DeployMouseContextMenu,
EditorStyle,
AnchorRangeExt, EditorStyle,
};
use clock::ReplicaId;
use collections::{BTreeMap, HashMap};
use git::diff::{DiffHunk, DiffHunkStatus};
use git::diff::DiffHunkStatus;
use gpui::{
color::Color,
elements::*,
@@ -29,13 +29,12 @@ use gpui::{
json::{self, ToJson},
platform::CursorStyle,
text_layout::{self, Line, RunStyle, TextLayoutCache},
AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext,
LayoutContext, ModifiersChangedEvent, MouseButton, MouseButtonEvent, MouseMovedEvent,
MouseRegion, MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext,
WeakViewHandle,
AppContext, Axis, Border, CursorRegion, Element, ElementBox, EventContext, LayoutContext,
Modifiers, MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion, MutableAppContext,
PaintContext, Quad, SceneBuilder, SizeConstraint, ViewContext, WeakViewHandle,
};
use json::json;
use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection};
use language::{Bias, CursorShape, DiagnosticSeverity, OffsetUtf16, Point, Selection};
use project::ProjectPath;
use settings::{GitGutter, Settings};
use smallvec::SmallVec;
@@ -46,10 +45,17 @@ use std::{
ops::{DerefMut, Range},
sync::Arc,
};
use theme::DiffStyle;
#[derive(Debug)]
struct DiffHunkLayout {
visual_range: Range<u32>,
status: DiffHunkStatus,
is_folded: bool,
}
struct SelectionLayout {
head: DisplayPoint,
cursor_shape: CursorShape,
range: Range<DisplayPoint>,
}
@@ -57,6 +63,7 @@ impl SelectionLayout {
fn new<T: ToPoint + ToDisplayPoint + Clone>(
selection: Selection<T>,
line_mode: bool,
cursor_shape: CursorShape,
map: &DisplaySnapshot,
) -> Self {
if line_mode {
@@ -64,6 +71,7 @@ impl SelectionLayout {
let point_range = map.expand_to_line(selection.range());
Self {
head: selection.head().to_display_point(map),
cursor_shape,
range: point_range.start.to_display_point(map)
..point_range.end.to_display_point(map),
}
@@ -71,6 +79,7 @@ impl SelectionLayout {
let selection = selection.map(|p| p.to_display_point(map));
Self {
head: selection.head(),
cursor_shape,
range: selection.range(),
}
}
@@ -81,19 +90,13 @@ impl SelectionLayout {
pub struct EditorElement {
view: WeakViewHandle<Editor>,
style: Arc<EditorStyle>,
cursor_shape: CursorShape,
}
impl EditorElement {
pub fn new(
view: WeakViewHandle<Editor>,
style: EditorStyle,
cursor_shape: CursorShape,
) -> Self {
pub fn new(view: WeakViewHandle<Editor>, style: EditorStyle) -> Self {
Self {
view,
style: Arc::new(style),
cursor_shape,
}
}
@@ -134,7 +137,7 @@ impl EditorElement {
gutter_bounds,
cx,
) {
cx.propogate_event();
cx.propagate_event();
}
}
})
@@ -147,7 +150,7 @@ impl EditorElement {
text_bounds,
cx,
) {
cx.propogate_event();
cx.propagate_event();
}
}
})
@@ -164,7 +167,7 @@ impl EditorElement {
text_bounds,
cx,
) {
cx.propogate_event()
cx.propagate_event()
}
}
})
@@ -179,7 +182,7 @@ impl EditorElement {
text_bounds,
cx,
) {
cx.propogate_event()
cx.propagate_event()
}
}
})
@@ -187,7 +190,7 @@ impl EditorElement {
let position_map = position_map.clone();
move |e, cx| {
if !Self::mouse_moved(e.platform_event, &position_map, text_bounds, cx) {
cx.propogate_event()
cx.propagate_event()
}
}
})
@@ -196,7 +199,7 @@ impl EditorElement {
move |e, cx| {
if !Self::scroll(e.position, e.delta, e.precise, &position_map, bounds, cx)
{
cx.propogate_event()
cx.propagate_event()
}
}
}),
@@ -206,10 +209,14 @@ impl EditorElement {
fn mouse_down(
MouseButtonEvent {
position,
ctrl,
alt,
shift,
cmd,
modifiers:
Modifiers {
shift,
ctrl,
alt,
cmd,
..
},
mut click_count,
..
}: MouseButtonEvent,
@@ -300,8 +307,7 @@ impl EditorElement {
fn mouse_dragged(
view: WeakViewHandle<Editor>,
MouseMovedEvent {
cmd,
shift,
modifiers: Modifiers { cmd, shift, .. },
position,
..
}: MouseMovedEvent,
@@ -376,8 +382,7 @@ impl EditorElement {
fn mouse_moved(
MouseMovedEvent {
cmd,
shift,
modifiers: Modifiers { shift, cmd, .. },
position,
..
}: MouseMovedEvent,
@@ -408,14 +413,6 @@ impl EditorElement {
true
}
fn modifiers_changed(&self, event: ModifiersChangedEvent, cx: &mut EventContext) -> bool {
cx.dispatch_action(CmdShiftChanged {
cmd_down: event.cmd,
shift_down: event.shift,
});
false
}
fn scroll(
position: Vector2F,
mut delta: Vector2F,
@@ -428,18 +425,27 @@ impl EditorElement {
return false;
}
let line_height = position_map.line_height;
let max_glyph_width = position_map.em_width;
if !precise {
delta *= vec2f(max_glyph_width, position_map.line_height);
}
let axis = if precise {
//Trackpad
position_map.snapshot.ongoing_scroll.filter(&mut delta)
} else {
//Not trackpad
delta *= vec2f(max_glyph_width, line_height);
None //Resets ongoing scroll
};
let scroll_position = position_map.snapshot.scroll_position();
let x = (scroll_position.x() * max_glyph_width - delta.x()) / max_glyph_width;
let y =
(scroll_position.y() * position_map.line_height - delta.y()) / position_map.line_height;
let y = (scroll_position.y() * line_height - delta.y()) / line_height;
let scroll_position = vec2f(x, y).clamp(Vector2F::zero(), position_map.scroll_max);
cx.dispatch_action(Scroll(scroll_position));
cx.dispatch_action(Scroll {
scroll_position,
axis,
});
true
}
@@ -525,85 +531,11 @@ impl EditorElement {
layout: &mut LayoutState,
cx: &mut PaintContext,
) {
struct GutterLayout {
line_height: f32,
// scroll_position: Vector2F,
scroll_top: f32,
bounds: RectF,
}
struct DiffLayout<'a> {
buffer_row: u32,
last_diff: Option<&'a DiffHunk<u32>>,
}
fn diff_quad(
hunk: &DiffHunk<u32>,
gutter_layout: &GutterLayout,
diff_style: &DiffStyle,
) -> Quad {
let color = match hunk.status() {
DiffHunkStatus::Added => diff_style.inserted,
DiffHunkStatus::Modified => diff_style.modified,
//TODO: This rendering is entirely a horrible hack
DiffHunkStatus::Removed => {
let row = hunk.buffer_range.start;
let offset = gutter_layout.line_height / 2.;
let start_y =
row as f32 * gutter_layout.line_height + offset - gutter_layout.scroll_top;
let end_y = start_y + gutter_layout.line_height;
let width = diff_style.removed_width_em * gutter_layout.line_height;
let highlight_origin = gutter_layout.bounds.origin() + vec2f(-width, start_y);
let highlight_size = vec2f(width * 2., end_y - start_y);
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
return Quad {
bounds: highlight_bounds,
background: Some(diff_style.deleted),
border: Border::new(0., Color::transparent_black()),
corner_radius: 1. * gutter_layout.line_height,
};
}
};
let start_row = hunk.buffer_range.start;
let end_row = hunk.buffer_range.end;
let start_y = start_row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
let end_y = end_row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
let width = diff_style.width_em * gutter_layout.line_height;
let highlight_origin = gutter_layout.bounds.origin() + vec2f(-width, start_y);
let highlight_size = vec2f(width * 2., end_y - start_y);
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
Quad {
bounds: highlight_bounds,
background: Some(color),
border: Border::new(0., Color::transparent_black()),
corner_radius: diff_style.corner_radius * gutter_layout.line_height,
}
}
let line_height = layout.position_map.line_height;
let scroll_position = layout.position_map.snapshot.scroll_position();
let gutter_layout = {
let line_height = layout.position_map.line_height;
GutterLayout {
scroll_top: scroll_position.y() * line_height,
line_height,
bounds,
}
};
let scroll_top = scroll_position.y() * line_height;
let mut diff_layout = DiffLayout {
buffer_row: scroll_position.y() as u32,
last_diff: None,
};
let diff_style = &cx.global::<Settings>().theme.editor.diff.clone();
let show_gutter = matches!(
&cx.global::<Settings>()
.git_overrides
@@ -612,58 +544,107 @@ impl EditorElement {
GitGutter::TrackedFiles
);
// line is `None` when there's a line wrap
if show_gutter {
Self::paint_diff_hunks(bounds, layout, cx);
}
for (ix, line) in layout.line_number_layouts.iter().enumerate() {
if let Some(line) = line {
let line_origin = bounds.origin()
+ vec2f(
bounds.width() - line.width() - layout.gutter_padding,
ix as f32 * gutter_layout.line_height
- (gutter_layout.scroll_top % gutter_layout.line_height),
ix as f32 * line_height - (scroll_top % line_height),
);
line.paint(line_origin, visible_bounds, gutter_layout.line_height, cx);
if show_gutter {
//This line starts a buffer line, so let's do the diff calculation
let new_hunk = get_hunk(diff_layout.buffer_row, &layout.diff_hunks);
let (is_ending, is_starting) = match (diff_layout.last_diff, new_hunk) {
(Some(old_hunk), Some(new_hunk)) if new_hunk == old_hunk => (false, false),
(a, b) => (a.is_some(), b.is_some()),
};
if is_ending {
let last_hunk = diff_layout.last_diff.take().unwrap();
cx.scene
.push_quad(diff_quad(last_hunk, &gutter_layout, diff_style));
}
if is_starting {
let new_hunk = new_hunk.unwrap();
diff_layout.last_diff = Some(new_hunk);
};
diff_layout.buffer_row += 1;
}
line.paint(line_origin, visible_bounds, line_height, cx);
}
}
// If we ran out with a diff hunk still being prepped, paint it now
if let Some(last_hunk) = diff_layout.last_diff {
cx.scene
.push_quad(diff_quad(last_hunk, &gutter_layout, diff_style))
}
if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
let mut x = bounds.width() - layout.gutter_padding;
let mut y = *row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
let mut y = *row as f32 * line_height - scroll_top;
x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
y += (gutter_layout.line_height - indicator.size().y()) / 2.;
y += (line_height - indicator.size().y()) / 2.;
indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
}
}
fn paint_diff_hunks(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
let diff_style = &cx.global::<Settings>().theme.editor.diff.clone();
let line_height = layout.position_map.line_height;
let scroll_position = layout.position_map.snapshot.scroll_position();
let scroll_top = scroll_position.y() * line_height;
for hunk in &layout.hunk_layouts {
let color = match (hunk.status, hunk.is_folded) {
(DiffHunkStatus::Added, false) => diff_style.inserted,
(DiffHunkStatus::Modified, false) => diff_style.modified,
//TODO: This rendering is entirely a horrible hack
(DiffHunkStatus::Removed, false) => {
let row = hunk.visual_range.start;
let offset = line_height / 2.;
let start_y = row as f32 * line_height - offset - scroll_top;
let end_y = start_y + line_height;
let width = diff_style.removed_width_em * line_height;
let highlight_origin = bounds.origin() + vec2f(-width, start_y);
let highlight_size = vec2f(width * 2., end_y - start_y);
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
cx.scene.push_quad(Quad {
bounds: highlight_bounds,
background: Some(diff_style.deleted),
border: Border::new(0., Color::transparent_black()),
corner_radius: 1. * line_height,
});
continue;
}
(_, true) => {
let row = hunk.visual_range.start;
let start_y = row as f32 * line_height - scroll_top;
let end_y = start_y + line_height;
let width = diff_style.removed_width_em * line_height;
let highlight_origin = bounds.origin() + vec2f(-width, start_y);
let highlight_size = vec2f(width * 2., end_y - start_y);
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
cx.scene.push_quad(Quad {
bounds: highlight_bounds,
background: Some(diff_style.modified),
border: Border::new(0., Color::transparent_black()),
corner_radius: 1. * line_height,
});
continue;
}
};
let start_row = hunk.visual_range.start;
let end_row = hunk.visual_range.end;
let start_y = start_row as f32 * line_height - scroll_top;
let end_y = end_row as f32 * line_height - scroll_top;
let width = diff_style.width_em * line_height;
let highlight_origin = bounds.origin() + vec2f(-width, start_y);
let highlight_size = vec2f(width * 2., end_y - start_y);
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
cx.scene.push_quad(Quad {
bounds: highlight_bounds,
background: Some(color),
border: Border::new(0., Color::transparent_black()),
corner_radius: diff_style.corner_radius * line_height,
});
}
}
fn paint_text(
&mut self,
bounds: RectF,
@@ -675,10 +656,8 @@ impl EditorElement {
let style = &self.style;
let local_replica_id = view.replica_id(cx);
let scroll_position = layout.position_map.snapshot.scroll_position();
let start_row = scroll_position.y() as u32;
let start_row = layout.visible_display_row_range.start;
let scroll_top = scroll_position.y() * layout.position_map.line_height;
let end_row =
((scroll_top + bounds.height()) / layout.position_map.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen
let max_glyph_width = layout.position_map.em_width;
let scroll_left = scroll_position.x() * max_glyph_width;
let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
@@ -697,8 +676,6 @@ impl EditorElement {
for (range, color) in &layout.highlighted_ranges {
self.paint_highlighted_range(
range.clone(),
start_row,
end_row,
*color,
0.,
0.15 * layout.position_map.line_height,
@@ -719,8 +696,6 @@ impl EditorElement {
for selection in selections {
self.paint_highlighted_range(
selection.range.clone(),
start_row,
end_row,
selection_style.selection,
corner_radius,
corner_radius * 2.,
@@ -732,9 +707,12 @@ impl EditorElement {
cx,
);
if view.show_local_cursors() || *replica_id != local_replica_id {
if view.show_local_cursors(cx) || *replica_id != local_replica_id {
let cursor_position = selection.head;
if (start_row..end_row).contains(&cursor_position.row()) {
if layout
.visible_display_row_range
.contains(&cursor_position.row())
{
let cursor_row_layout = &layout.position_map.line_layouts
[(cursor_position.row() - start_row) as usize];
let cursor_column = cursor_position.column() as usize;
@@ -745,7 +723,7 @@ impl EditorElement {
if block_width == 0.0 {
block_width = layout.position_map.em_width;
}
let block_text = if let CursorShape::Block = self.cursor_shape {
let block_text = if let CursorShape::Block = selection.cursor_shape {
layout
.position_map
.snapshot
@@ -781,7 +759,7 @@ impl EditorElement {
block_width,
origin: vec2f(x, y),
line_height: layout.position_map.line_height,
shape: self.cursor_shape,
shape: selection.cursor_shape,
block_text,
});
}
@@ -813,7 +791,7 @@ impl EditorElement {
cx.scene.pop_layer();
if let Some((position, context_menu)) = layout.context_menu.as_mut() {
cx.scene.push_stacking_context(None);
cx.scene.push_stacking_context(None, None);
let cursor_row_layout =
&layout.position_map.line_layouts[(position.row() - start_row) as usize];
let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
@@ -842,7 +820,7 @@ impl EditorElement {
}
if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() {
cx.scene.push_stacking_context(None);
cx.scene.push_stacking_context(None, None);
// This is safe because we check on layout whether the required row is available
let hovered_row_layout =
@@ -1025,8 +1003,6 @@ impl EditorElement {
fn paint_highlighted_range(
&self,
range: Range<DisplayPoint>,
start_row: u32,
end_row: u32,
color: Color,
corner_radius: f32,
line_end_overshoot: f32,
@@ -1037,6 +1013,8 @@ impl EditorElement {
bounds: RectF,
cx: &mut PaintContext,
) {
let start_row = layout.visible_display_row_range.start;
let end_row = layout.visible_display_row_range.end;
if range.start != range.end {
let row_range = if range.end.column() == 0 {
cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
@@ -1125,6 +1103,75 @@ impl EditorElement {
.width()
}
//Folds contained in a hunk are ignored apart from shrinking visual size
//If a fold contains any hunks then that fold line is marked as modified
fn layout_git_gutters(
&self,
rows: Range<u32>,
snapshot: &EditorSnapshot,
) -> Vec<DiffHunkLayout> {
let buffer_snapshot = &snapshot.buffer_snapshot;
let visual_start = DisplayPoint::new(rows.start, 0).to_point(snapshot).row;
let visual_end = DisplayPoint::new(rows.end, 0).to_point(snapshot).row;
let hunks = buffer_snapshot.git_diff_hunks_in_range(visual_start..visual_end);
let mut layouts = Vec::<DiffHunkLayout>::new();
for hunk in hunks {
let hunk_start_point = Point::new(hunk.buffer_range.start, 0);
let hunk_end_point = Point::new(hunk.buffer_range.end, 0);
let hunk_start_point_sub = Point::new(hunk.buffer_range.start.saturating_sub(1), 0);
let hunk_end_point_sub = Point::new(
hunk.buffer_range
.end
.saturating_sub(1)
.max(hunk.buffer_range.start),
0,
);
let is_removal = hunk.status() == DiffHunkStatus::Removed;
let folds_start = Point::new(hunk.buffer_range.start.saturating_sub(1), 0);
let folds_end = Point::new(hunk.buffer_range.end + 1, 0);
let folds_range = folds_start..folds_end;
let containing_fold = snapshot.folds_in_range(folds_range).find(|fold_range| {
let fold_point_range = fold_range.to_point(buffer_snapshot);
let fold_point_range = fold_point_range.start..=fold_point_range.end;
let folded_start = fold_point_range.contains(&hunk_start_point);
let folded_end = fold_point_range.contains(&hunk_end_point_sub);
let folded_start_sub = fold_point_range.contains(&hunk_start_point_sub);
(folded_start && folded_end) || (is_removal && folded_start_sub)
});
let visual_range = if let Some(fold) = containing_fold {
let row = fold.start.to_display_point(snapshot).row();
row..row
} else {
let start = hunk_start_point.to_display_point(snapshot).row();
let end = hunk_end_point.to_display_point(snapshot).row();
start..end
};
let has_existing_layout = match layouts.last() {
Some(e) => visual_range == e.visual_range && e.status == hunk.status(),
None => false,
};
if !has_existing_layout {
layouts.push(DiffHunkLayout {
visual_range,
status: hunk.status(),
is_folded: containing_fold.is_some(),
});
}
}
layouts
}
fn layout_line_numbers(
&self,
rows: Range<u32>,
@@ -1279,6 +1326,7 @@ impl EditorElement {
line_height: f32,
style: &EditorStyle,
line_layouts: &[text_layout::Line],
include_root: bool,
cx: &mut LayoutContext,
) -> (f32, Vec<BlockLayout>) {
let editor = if let Some(editor) = self.view.upgrade(cx) {
@@ -1382,10 +1430,11 @@ impl EditorElement {
let font_size =
(style.text_scale_factor * self.style.text.font_size).round();
let path = buffer.resolve_file_path(cx, include_root);
let mut filename = None;
let mut parent_path = None;
if let Some(file) = buffer.file() {
let path = file.path();
// Can't use .and_then() because `.file_name()` and `.parent()` return references :(
if let Some(path) = path {
filename = path.file_name().map(|f| f.to_string_lossy().to_string());
parent_path =
path.parent().map(|p| p.to_string_lossy().to_string() + "/");
@@ -1477,27 +1526,6 @@ impl EditorElement {
}
}
/// Get the hunk that contains buffer_line, starting from start_idx
/// Returns none if there is none found, and
fn get_hunk(buffer_line: u32, hunks: &[DiffHunk<u32>]) -> Option<&DiffHunk<u32>> {
for i in 0..hunks.len() {
// Safety: Index out of bounds is handled by the check above
let hunk = hunks.get(i).unwrap();
if hunk.buffer_range.contains(&(buffer_line as u32)) {
return Some(hunk);
} else if hunk.status() == DiffHunkStatus::Removed && buffer_line == hunk.buffer_range.start
{
return Some(hunk);
} else if hunk.buffer_range.start > buffer_line as u32 {
// If we've passed the buffer_line, just stop
return None;
}
}
// We reached the end of the array without finding a hunk, just return none.
return None;
}
impl Element for EditorElement {
type LayoutState = LayoutState;
type PaintState = ();
@@ -1610,6 +1638,7 @@ impl Element for EditorElement {
let mut highlighted_rows = None;
let mut highlighted_ranges = Vec::new();
let mut show_scrollbars = false;
let mut include_root = false;
self.update_view(cx.app, |view, cx| {
let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
@@ -1622,7 +1651,7 @@ impl Element for EditorElement {
);
let mut remote_selections = HashMap::default();
for (replica_id, line_mode, selection) in display_map
for (replica_id, line_mode, cursor_shape, selection) in display_map
.buffer_snapshot
.remote_selections_in_range(&(start_anchor.clone()..end_anchor.clone()))
{
@@ -1633,7 +1662,12 @@ impl Element for EditorElement {
remote_selections
.entry(replica_id)
.or_insert(Vec::new())
.push(SelectionLayout::new(selection, line_mode, &display_map));
.push(SelectionLayout::new(
selection,
line_mode,
cursor_shape,
&display_map,
));
}
selections.extend(remote_selections);
@@ -1665,22 +1699,29 @@ impl Element for EditorElement {
local_selections
.into_iter()
.map(|selection| {
SelectionLayout::new(selection, view.selections.line_mode, &display_map)
SelectionLayout::new(
selection,
view.selections.line_mode,
view.cursor_shape,
&display_map,
)
})
.collect(),
));
}
show_scrollbars = view.show_scrollbars();
include_root = view
.project
.as_ref()
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
.unwrap_or_default()
});
let line_number_layouts =
self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx);
let diff_hunks = snapshot
.buffer_snapshot
.git_diff_hunks_in_range(start_row..end_row)
.collect();
let hunk_layouts = self.layout_git_gutters(start_row..end_row, &snapshot);
let scrollbar_row_range = scroll_position.y()..(scroll_position.y() + height_in_lines);
@@ -1714,6 +1755,7 @@ impl Element for EditorElement {
line_height,
&style,
&line_layouts,
include_root,
cx,
);
@@ -1826,6 +1868,7 @@ impl Element for EditorElement {
em_advance,
snapshot,
}),
visible_display_row_range: start_row..end_row,
gutter_size,
gutter_padding,
text_size,
@@ -1837,7 +1880,7 @@ impl Element for EditorElement {
highlighted_rows,
highlighted_ranges,
line_number_layouts,
diff_hunks,
hunk_layouts,
blocks,
selections,
context_menu,
@@ -1889,22 +1932,6 @@ impl Element for EditorElement {
cx.scene.pop_layer();
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: RectF,
_: &mut LayoutState,
_: &mut (),
cx: &mut EventContext,
) -> bool {
if let Event::ModifiersChanged(event) = event {
self.modifiers_changed(*event, cx);
}
false
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
@@ -1971,9 +1998,11 @@ pub struct LayoutState {
gutter_margin: f32,
text_size: Vector2F,
mode: EditorMode,
visible_display_row_range: Range<u32>,
active_rows: BTreeMap<u32, bool>,
highlighted_rows: Option<Range<u32>>,
line_number_layouts: Vec<Option<text_layout::Line>>,
hunk_layouts: Vec<DiffHunkLayout>,
blocks: Vec<BlockLayout>,
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
@@ -1981,7 +2010,6 @@ pub struct LayoutState {
show_scrollbars: bool,
max_row: u32,
context_menu: Option<(DisplayPoint, ElementBox)>,
diff_hunks: Vec<DiffHunk<u32>>,
code_actions_indicator: Option<(u32, ElementBox)>,
hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
}
@@ -2069,20 +2097,6 @@ fn layout_line(
)
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum CursorShape {
Bar,
Block,
Underscore,
Hollow,
}
impl Default for CursorShape {
fn default() -> Self {
CursorShape::Bar
}
}
#[derive(Debug)]
pub struct Cursor {
origin: Vector2F,
@@ -2175,7 +2189,7 @@ pub struct HighlightedRangeLine {
}
impl HighlightedRange {
pub fn paint(&self, bounds: RectF, scene: &mut Scene) {
pub fn paint(&self, bounds: RectF, scene: &mut SceneBuilder) {
if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
self.paint_lines(self.start_y, &self.lines[0..1], bounds, scene);
self.paint_lines(
@@ -2194,7 +2208,7 @@ impl HighlightedRange {
start_y: f32,
lines: &[HighlightedRangeLine],
bounds: RectF,
scene: &mut Scene,
scene: &mut SceneBuilder,
) {
if lines.is_empty() {
return;
@@ -2323,11 +2337,7 @@ mod tests {
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
Editor::new(EditorMode::Full, buffer, None, None, cx)
});
let element = EditorElement::new(
editor.downgrade(),
editor.read(cx).style(cx),
CursorShape::Bar,
);
let element = EditorElement::new(editor.downgrade(), editor.read(cx).style(cx));
let layouts = editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
@@ -2363,13 +2373,9 @@ mod tests {
cx.blur();
});
let mut element = EditorElement::new(
editor.downgrade(),
editor.read(cx).style(cx),
CursorShape::Bar,
);
let mut element = EditorElement::new(editor.downgrade(), editor.read(cx).style(cx));
let mut scene = Scene::new(1.0);
let mut scene = SceneBuilder::new(1.0);
let mut presenter = cx.build_presenter(window_id, 30., Default::default());
let mut layout_cx = presenter.build_layout_context(Vector2F::zero(), false, cx);
let (size, mut state) = element.layout(

View File

@@ -354,7 +354,7 @@ impl InfoPopover {
.with_style(style.hover_popover.container)
.boxed()
})
.on_move(|_, _| {})
.on_move(|_, _| {}) // Consume move events so they don't reach regions underneath.
.with_cursor_style(CursorStyle::Arrow)
.with_padding(Padding {
bottom: HOVER_POPOVER_GAP,
@@ -400,7 +400,7 @@ impl DiagnosticPopover {
bottom: HOVER_POPOVER_GAP,
..Default::default()
})
.on_move(|_, _| {})
.on_move(|_, _| {}) // Consume move events so they don't reach regions underneath.
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(GoToDiagnostic)
})

View File

@@ -120,6 +120,7 @@ impl FollowableItem for Editor {
buffer.set_active_selections(
&self.selections.disjoint_anchors(),
self.selections.line_mode,
self.cursor_shape,
cx,
);
}
@@ -531,21 +532,17 @@ impl Item for Editor {
let buffer = multibuffer.buffer(buffer_id)?;
let buffer = buffer.read(cx);
let filename = if let Some(file) = buffer.file() {
if file.path().file_name().is_none()
|| self
.project
let filename = buffer
.snapshot()
.resolve_file_path(
cx,
self.project
.as_ref()
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
.unwrap_or_default()
{
file.full_path(cx).to_string_lossy().to_string()
} else {
file.path().to_string_lossy().to_string()
}
} else {
"untitled".to_string()
};
.unwrap_or_default(),
)
.map(|path| path.to_string_lossy().to_string())
.unwrap_or_else(|| "untitled".to_string());
let mut breadcrumbs = vec![Label::new(filename, theme.breadcrumbs.text.clone()).boxed()];
breadcrumbs.extend(symbols.into_iter().map(|symbol| {

View File

@@ -19,12 +19,6 @@ pub struct UpdateGoToDefinitionLink {
pub shift_held: bool,
}
#[derive(Clone, PartialEq)]
pub struct CmdShiftChanged {
pub cmd_down: bool,
pub shift_down: bool,
}
#[derive(Clone, PartialEq)]
pub struct GoToFetchedDefinition {
pub point: DisplayPoint,
@@ -39,7 +33,6 @@ impl_internal_actions!(
editor,
[
UpdateGoToDefinitionLink,
CmdShiftChanged,
GoToFetchedDefinition,
GoToFetchedTypeDefinition
]
@@ -47,7 +40,6 @@ impl_internal_actions!(
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(update_go_to_definition_link);
cx.add_action(cmd_shift_changed);
cx.add_action(go_to_fetched_definition);
cx.add_action(go_to_fetched_type_definition);
}
@@ -113,37 +105,6 @@ pub fn update_go_to_definition_link(
hide_link_definition(editor, cx);
}
pub fn cmd_shift_changed(
editor: &mut Editor,
&CmdShiftChanged {
cmd_down,
shift_down,
}: &CmdShiftChanged,
cx: &mut ViewContext<Editor>,
) {
let pending_selection = editor.has_pending_selection();
if let Some(point) = editor
.link_go_to_definition_state
.last_mouse_location
.clone()
{
if cmd_down && !pending_selection {
let snapshot = editor.snapshot(cx);
let kind = if shift_down {
LinkDefinitionKind::Type
} else {
LinkDefinitionKind::Symbol
};
show_link_definition(kind, editor, point, snapshot, cx);
return;
}
}
hide_link_definition(editor, cx)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LinkDefinitionKind {
Symbol,
@@ -397,6 +358,7 @@ fn go_to_fetched_definition_of_kind(
#[cfg(test)]
mod tests {
use futures::StreamExt;
use gpui::{Modifiers, ModifiersChangedEvent, View};
use indoc::indoc;
use lsp::request::{GotoDefinition, GotoTypeDefinition};
@@ -467,11 +429,13 @@ mod tests {
// Unpress shift causes highlight to go away (normal goto-definition is not valid here)
cx.update_editor(|editor, cx| {
cmd_shift_changed(
editor,
&CmdShiftChanged {
cmd_down: true,
shift_down: false,
editor.modifiers_changed(
&gpui::ModifiersChangedEvent {
modifiers: Modifiers {
cmd: true,
..Default::default()
},
..Default::default()
},
cx,
);
@@ -581,14 +545,7 @@ mod tests {
// Unpress cmd causes highlight to go away
cx.update_editor(|editor, cx| {
cmd_shift_changed(
editor,
&CmdShiftChanged {
cmd_down: false,
shift_down: false,
},
cx,
);
editor.modifiers_changed(&Default::default(), cx);
});
// Assert no link highlights
@@ -704,11 +661,12 @@ mod tests {
])))
});
cx.update_editor(|editor, cx| {
cmd_shift_changed(
editor,
&CmdShiftChanged {
cmd_down: true,
shift_down: false,
editor.modifiers_changed(
&ModifiersChangedEvent {
modifiers: Modifiers {
cmd: true,
..Default::default()
},
},
cx,
);

View File

@@ -29,6 +29,25 @@ pub fn up(
start: DisplayPoint,
goal: SelectionGoal,
preserve_column_at_start: bool,
) -> (DisplayPoint, SelectionGoal) {
up_by_rows(map, start, 1, goal, preserve_column_at_start)
}
pub fn down(
map: &DisplaySnapshot,
start: DisplayPoint,
goal: SelectionGoal,
preserve_column_at_end: bool,
) -> (DisplayPoint, SelectionGoal) {
down_by_rows(map, start, 1, goal, preserve_column_at_end)
}
pub fn up_by_rows(
map: &DisplaySnapshot,
start: DisplayPoint,
row_count: u32,
goal: SelectionGoal,
preserve_column_at_start: bool,
) -> (DisplayPoint, SelectionGoal) {
let mut goal_column = if let SelectionGoal::Column(column) = goal {
column
@@ -36,7 +55,7 @@ pub fn up(
map.column_to_chars(start.row(), start.column())
};
let prev_row = start.row().saturating_sub(1);
let prev_row = start.row().saturating_sub(row_count);
let mut point = map.clip_point(
DisplayPoint::new(prev_row, map.line_len(prev_row)),
Bias::Left,
@@ -62,9 +81,10 @@ pub fn up(
)
}
pub fn down(
pub fn down_by_rows(
map: &DisplaySnapshot,
start: DisplayPoint,
row_count: u32,
goal: SelectionGoal,
preserve_column_at_end: bool,
) -> (DisplayPoint, SelectionGoal) {
@@ -74,8 +94,8 @@ pub fn down(
map.column_to_chars(start.row(), start.column())
};
let next_row = start.row() + 1;
let mut point = map.clip_point(DisplayPoint::new(next_row, 0), Bias::Right);
let new_row = start.row() + row_count;
let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
if point.row() > start.row() {
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
} else if preserve_column_at_end {

View File

@@ -8,7 +8,7 @@ use git::diff::DiffHunk;
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
pub use language::Completion;
use language::{
char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk,
char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Outline,
OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _,
ToPoint as _, ToPointUtf16 as _, TransactionId,
@@ -143,6 +143,7 @@ struct ExcerptSummary {
text: TextSummary,
}
#[derive(Clone)]
pub struct MultiBufferRows<'a> {
buffer_row_range: Range<u32>,
excerpts: Cursor<'a, Excerpt, Point>,
@@ -603,6 +604,7 @@ impl MultiBuffer {
&mut self,
selections: &[Selection<Anchor>],
line_mode: bool,
cursor_shape: CursorShape,
cx: &mut ModelContext<Self>,
) {
let mut selections_by_buffer: HashMap<usize, Vec<Selection<text::Anchor>>> =
@@ -667,7 +669,7 @@ impl MultiBuffer {
}
Some(selection)
}));
buffer.set_active_selections(merged_selections, line_mode, cx);
buffer.set_active_selections(merged_selections, line_mode, cursor_shape, cx);
});
}
}
@@ -2697,7 +2699,7 @@ impl MultiBufferSnapshot {
pub fn remote_selections_in_range<'a>(
&'a self,
range: &'a Range<Anchor>,
) -> impl 'a + Iterator<Item = (ReplicaId, bool, Selection<Anchor>)> {
) -> impl 'a + Iterator<Item = (ReplicaId, bool, CursorShape, Selection<Anchor>)> {
let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
cursor.seek(&Some(&range.start.excerpt_id), Bias::Left, &());
cursor
@@ -2714,7 +2716,7 @@ impl MultiBufferSnapshot {
excerpt
.buffer
.remote_selections_in_range(query_range)
.flat_map(move |(replica_id, line_mode, selections)| {
.flat_map(move |(replica_id, line_mode, cursor_shape, selections)| {
selections.map(move |selection| {
let mut start = Anchor {
buffer_id: Some(excerpt.buffer_id),
@@ -2736,6 +2738,7 @@ impl MultiBufferSnapshot {
(
replica_id,
line_mode,
cursor_shape,
Selection {
id: selection.id,
start,

View File

@@ -53,7 +53,7 @@ impl View for FileFinder {
ChildView::new(self.picker.clone(), cx).boxed()
}
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.picker);
}
@@ -251,7 +251,7 @@ impl PickerDelegate for FileFinder {
fn render_match(
&self,
ix: usize,
mouse_state: MouseState,
mouse_state: &mut MouseState,
selected: bool,
cx: &AppContext,
) -> ElementBox {

View File

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

View File

@@ -190,7 +190,6 @@ impl BufferDiff {
}
if kind == GitDiffLineType::Deletion {
*buffer_row_divergence -= 1;
let end = content_offset + content_len;
match &mut head_byte_range {
@@ -203,6 +202,8 @@ impl BufferDiff {
let row = old_row as i64 + *buffer_row_divergence;
first_deletion_buffer_row = Some(row as u32);
}
*buffer_row_divergence -= 1;
}
}

View File

@@ -183,7 +183,7 @@ impl View for GoToLine {
.named("go to line")
}
fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
cx.focus(&self.line_editor);
}
}

View File

@@ -57,7 +57,7 @@ fn compile_metal_shaders() {
"macosx",
"metal",
"-gline-tables-only",
"-mmacosx-version-min=10.14",
"-mmacosx-version-min=10.15.7",
"-MO",
"-c",
shader_path,

View File

@@ -101,18 +101,6 @@ impl gpui::Element for TextElement {
line.paint(bounds.origin(), visible_bounds, bounds.height(), cx);
}
fn dispatch_event(
&mut self,
_: &gpui::Event,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut gpui::EventContext,
) -> bool {
false
}
fn rect_for_text_range(
&self,
_: Range<usize>,

View File

@@ -41,8 +41,8 @@ use crate::{
platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
presenter::Presenter,
util::post_inc,
Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseButton,
MouseRegionId, PathPromptOptions, TextLayoutCache,
Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, KeyUpEvent,
ModifiersChangedEvent, MouseButton, MouseRegionId, PathPromptOptions, TextLayoutCache,
};
pub trait Entity: 'static {
@@ -60,8 +60,18 @@ pub trait Entity: 'static {
pub trait View: Entity + Sized {
fn ui_name() -> &'static str;
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox;
fn on_focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {}
fn on_focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {}
fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {}
fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {}
fn key_down(&mut self, _: &KeyDownEvent, _: &mut ViewContext<Self>) -> bool {
false
}
fn key_up(&mut self, _: &KeyUpEvent, _: &mut ViewContext<Self>) -> bool {
false
}
fn modifiers_changed(&mut self, _: &ModifiersChangedEvent, _: &mut ViewContext<Self>) -> bool {
false
}
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
Self::default_keymap_context()
}
@@ -1297,7 +1307,7 @@ impl MutableAppContext {
) -> impl Iterator<Item = (&'static str, Box<dyn Action>, SmallVec<[&Binding; 1]>)> {
let mut action_types: HashSet<_> = self.global_actions.keys().copied().collect();
for view_id in self.parents(window_id, view_id) {
for view_id in self.ancestors(window_id, view_id) {
if let Some(view) = self.views.get(&(window_id, view_id)) {
let view_type = view.as_any().type_id();
if let Some(actions) = self.actions.get(&view_type) {
@@ -1327,7 +1337,7 @@ impl MutableAppContext {
let action_type = action.as_any().type_id();
if let Some(window_id) = self.cx.platform.key_window_id() {
if let Some(focused_view_id) = self.focused_view_id(window_id) {
for view_id in self.parents(window_id, focused_view_id) {
for view_id in self.ancestors(window_id, focused_view_id) {
if let Some(view) = self.views.get(&(window_id, view_id)) {
let view_type = view.as_any().type_id();
if let Some(actions) = self.actions.get(&view_type) {
@@ -1376,7 +1386,7 @@ impl MutableAppContext {
mut visit: impl FnMut(usize, bool, &mut MutableAppContext) -> bool,
) -> bool {
// List of view ids from the leaf to the root of the window
let path = self.parents(window_id, view_id).collect::<Vec<_>>();
let path = self.ancestors(window_id, view_id).collect::<Vec<_>>();
// Walk down from the root to the leaf calling visit with capture_phase = true
for view_id in path.iter().rev() {
@@ -1397,7 +1407,7 @@ impl MutableAppContext {
// Returns an iterator over all of the view ids from the passed view up to the root of the window
// Includes the passed view itself
fn parents(&self, window_id: usize, mut view_id: usize) -> impl Iterator<Item = usize> + '_ {
fn ancestors(&self, window_id: usize, mut view_id: usize) -> impl Iterator<Item = usize> + '_ {
std::iter::once(view_id)
.into_iter()
.chain(std::iter::from_fn(move || {
@@ -1445,40 +1455,111 @@ impl MutableAppContext {
self.keystroke_matcher.clear_bindings();
}
pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: &Keystroke) -> bool {
let mut pending = false;
pub fn dispatch_key_down(&mut self, window_id: usize, event: &KeyDownEvent) -> bool {
if let Some(focused_view_id) = self.focused_view_id(window_id) {
for view_id in self.parents(window_id, focused_view_id).collect::<Vec<_>>() {
let keymap_context = self
.cx
.views
.get(&(window_id, view_id))
.unwrap()
.keymap_context(self.as_ref());
match self.keystroke_matcher.push_keystroke(
keystroke.clone(),
view_id,
&keymap_context,
) {
MatchResult::None => {}
MatchResult::Pending => pending = true,
MatchResult::Action(action) => {
if self.handle_dispatch_action_from_effect(
window_id,
Some(view_id),
action.as_ref(),
) {
self.keystroke_matcher.clear_pending();
return true;
}
for view_id in self
.ancestors(window_id, focused_view_id)
.collect::<Vec<_>>()
{
if let Some(mut view) = self.cx.views.remove(&(window_id, view_id)) {
let handled = view.key_down(event, self, window_id, view_id);
self.cx.views.insert((window_id, view_id), view);
if handled {
return true;
}
} else {
log::error!("view {} does not exist", view_id)
}
}
}
pending
false
}
pub fn dispatch_key_up(&mut self, window_id: usize, event: &KeyUpEvent) -> bool {
if let Some(focused_view_id) = self.focused_view_id(window_id) {
for view_id in self
.ancestors(window_id, focused_view_id)
.collect::<Vec<_>>()
{
if let Some(mut view) = self.cx.views.remove(&(window_id, view_id)) {
let handled = view.key_up(event, self, window_id, view_id);
self.cx.views.insert((window_id, view_id), view);
if handled {
return true;
}
} else {
log::error!("view {} does not exist", view_id)
}
}
}
false
}
pub fn dispatch_modifiers_changed(
&mut self,
window_id: usize,
event: &ModifiersChangedEvent,
) -> bool {
if let Some(focused_view_id) = self.focused_view_id(window_id) {
for view_id in self
.ancestors(window_id, focused_view_id)
.collect::<Vec<_>>()
{
if let Some(mut view) = self.cx.views.remove(&(window_id, view_id)) {
let handled = view.modifiers_changed(event, self, window_id, view_id);
self.cx.views.insert((window_id, view_id), view);
if handled {
return true;
}
} else {
log::error!("view {} does not exist", view_id)
}
}
}
false
}
pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: &Keystroke) -> bool {
if let Some(focused_view_id) = self.focused_view_id(window_id) {
let dispatch_path = self
.ancestors(window_id, focused_view_id)
.map(|view_id| {
(
view_id,
self.cx
.views
.get(&(window_id, view_id))
.unwrap()
.keymap_context(self.as_ref()),
)
})
.collect();
match self
.keystroke_matcher
.push_keystroke(keystroke.clone(), dispatch_path)
{
MatchResult::None => false,
MatchResult::Pending => true,
MatchResult::Match { view_id, action } => {
if self.handle_dispatch_action_from_effect(
window_id,
Some(view_id),
action.as_ref(),
) {
self.keystroke_matcher.clear_pending();
true
} else {
false
}
}
}
} else {
false
}
}
pub fn default_global<T: 'static + Default>(&mut self) -> &T {
@@ -1580,7 +1661,7 @@ impl MutableAppContext {
is_fullscreen: false,
},
);
root_view.update(this, |view, cx| view.on_focus_in(cx.handle().into(), cx));
root_view.update(this, |view, cx| view.focus_in(cx.handle().into(), cx));
let window =
this.cx
@@ -1612,7 +1693,7 @@ impl MutableAppContext {
is_fullscreen: false,
},
);
root_view.update(this, |view, cx| view.on_focus_in(cx.handle().into(), cx));
root_view.update(this, |view, cx| view.focus_in(cx.handle().into(), cx));
let status_item = this.cx.platform.add_status_item();
this.register_platform_window(window_id, status_item);
@@ -2235,12 +2316,12 @@ impl MutableAppContext {
//Handle focus
let focused_id = window.focused_view_id?;
for view_id in this.parents(window_id, focused_id).collect::<Vec<_>>() {
for view_id in this.ancestors(window_id, focused_id).collect::<Vec<_>>() {
if let Some(mut view) = this.cx.views.remove(&(window_id, view_id)) {
if active {
view.on_focus_in(this, window_id, view_id, focused_id);
view.focus_in(this, window_id, view_id, focused_id);
} else {
view.on_focus_out(this, window_id, view_id, focused_id);
view.focus_out(this, window_id, view_id, focused_id);
}
this.cx.views.insert((window_id, view_id), view);
}
@@ -2272,16 +2353,16 @@ impl MutableAppContext {
});
let blurred_parents = blurred_id
.map(|blurred_id| this.parents(window_id, blurred_id).collect::<Vec<_>>())
.map(|blurred_id| this.ancestors(window_id, blurred_id).collect::<Vec<_>>())
.unwrap_or_default();
let focused_parents = focused_id
.map(|focused_id| this.parents(window_id, focused_id).collect::<Vec<_>>())
.map(|focused_id| this.ancestors(window_id, focused_id).collect::<Vec<_>>())
.unwrap_or_default();
if let Some(blurred_id) = blurred_id {
for view_id in blurred_parents.iter().copied() {
if let Some(mut view) = this.cx.views.remove(&(window_id, view_id)) {
view.on_focus_out(this, window_id, view_id, blurred_id);
view.focus_out(this, window_id, view_id, blurred_id);
this.cx.views.insert((window_id, view_id), view);
}
}
@@ -2294,7 +2375,7 @@ impl MutableAppContext {
if let Some(focused_id) = focused_id {
for view_id in focused_parents {
if let Some(mut view) = this.cx.views.remove(&(window_id, view_id)) {
view.on_focus_in(this, window_id, view_id, focused_id);
view.focus_in(this, window_id, view_id, focused_id);
this.cx.views.insert((window_id, view_id), view);
}
}
@@ -2961,20 +3042,41 @@ pub trait AnyView {
) -> Option<Pin<Box<dyn 'static + Future<Output = ()>>>>;
fn ui_name(&self) -> &'static str;
fn render(&mut self, params: RenderParams, cx: &mut MutableAppContext) -> ElementBox;
fn on_focus_in(
fn focus_in(
&mut self,
cx: &mut MutableAppContext,
window_id: usize,
view_id: usize,
focused_id: usize,
);
fn on_focus_out(
fn focus_out(
&mut self,
cx: &mut MutableAppContext,
window_id: usize,
view_id: usize,
focused_id: usize,
);
fn key_down(
&mut self,
event: &KeyDownEvent,
cx: &mut MutableAppContext,
window_id: usize,
view_id: usize,
) -> bool;
fn key_up(
&mut self,
event: &KeyUpEvent,
cx: &mut MutableAppContext,
window_id: usize,
view_id: usize,
) -> bool;
fn modifiers_changed(
&mut self,
event: &ModifiersChangedEvent,
cx: &mut MutableAppContext,
window_id: usize,
view_id: usize,
) -> bool;
fn keymap_context(&self, cx: &AppContext) -> keymap::Context;
fn debug_json(&self, cx: &AppContext) -> serde_json::Value;
@@ -3040,7 +3142,7 @@ where
View::render(self, &mut RenderContext::new(params, cx))
}
fn on_focus_in(
fn focus_in(
&mut self,
cx: &mut MutableAppContext,
window_id: usize,
@@ -3059,10 +3161,10 @@ where
.type_id();
AnyViewHandle::new(window_id, focused_id, focused_type, cx.ref_counts.clone())
};
View::on_focus_in(self, focused_view_handle, &mut cx);
View::focus_in(self, focused_view_handle, &mut cx);
}
fn on_focus_out(
fn focus_out(
&mut self,
cx: &mut MutableAppContext,
window_id: usize,
@@ -3081,7 +3183,40 @@ where
.type_id();
AnyViewHandle::new(window_id, blurred_id, blurred_type, cx.ref_counts.clone())
};
View::on_focus_out(self, blurred_view_handle, &mut cx);
View::focus_out(self, blurred_view_handle, &mut cx);
}
fn key_down(
&mut self,
event: &KeyDownEvent,
cx: &mut MutableAppContext,
window_id: usize,
view_id: usize,
) -> bool {
let mut cx = ViewContext::new(cx, window_id, view_id);
View::key_down(self, event, &mut cx)
}
fn key_up(
&mut self,
event: &KeyUpEvent,
cx: &mut MutableAppContext,
window_id: usize,
view_id: usize,
) -> bool {
let mut cx = ViewContext::new(cx, window_id, view_id);
View::key_up(self, event, &mut cx)
}
fn modifiers_changed(
&mut self,
event: &ModifiersChangedEvent,
cx: &mut MutableAppContext,
window_id: usize,
view_id: usize,
) -> bool {
let mut cx = ViewContext::new(cx, window_id, view_id);
View::modifiers_changed(self, event, &mut cx)
}
fn keymap_context(&self, cx: &AppContext) -> keymap::Context {
@@ -3463,7 +3598,7 @@ impl<'a, T: View> ViewContext<'a, T> {
if self.window_id != view.window_id {
return false;
}
self.parents(view.window_id, view.view_id)
self.ancestors(view.window_id, view.view_id)
.any(|parent| parent == self.view_id)
}
@@ -3701,6 +3836,11 @@ impl<'a, T: View> ViewContext<'a, T> {
self.app.notify_view(self.window_id, self.view_id);
}
pub fn dispatch_action(&mut self, action: impl Action) {
self.app
.dispatch_action_at(self.window_id, self.view_id, action)
}
pub fn dispatch_any_action(&mut self, action: Box<dyn Action>) {
self.app
.dispatch_any_action_at(self.window_id, self.view_id, action)
@@ -3774,10 +3914,32 @@ pub struct RenderContext<'a, T: View> {
pub refreshing: bool,
}
#[derive(Clone, Copy, Default)]
#[derive(Clone, Default)]
pub struct MouseState {
pub hovered: bool,
pub clicked: Option<MouseButton>,
hovered: bool,
clicked: Option<MouseButton>,
accessed_hovered: bool,
accessed_clicked: bool,
}
impl MouseState {
pub fn hovered(&mut self) -> bool {
self.accessed_hovered = true;
self.hovered
}
pub fn clicked(&mut self) -> Option<MouseButton> {
self.accessed_clicked = true;
self.clicked
}
pub fn accessed_hovered(&self) -> bool {
self.accessed_hovered
}
pub fn accessed_clicked(&self) -> bool {
self.accessed_clicked
}
}
impl<'a, V: View> RenderContext<'a, V> {
@@ -3818,6 +3980,8 @@ impl<'a, V: View> RenderContext<'a, V> {
None
}
}),
accessed_hovered: false,
accessed_clicked: false,
}
}
@@ -5614,10 +5778,7 @@ mod tests {
Event::MouseDown(MouseButtonEvent {
position: Default::default(),
button: MouseButton::Left,
ctrl: false,
alt: false,
shift: false,
cmd: false,
modifiers: Default::default(),
click_count: 1,
}),
false,
@@ -6330,13 +6491,13 @@ mod tests {
"View"
}
fn on_focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.handle().id() == focused.id() {
self.events.lock().push(format!("{} focused", &self.name));
}
}
fn on_focus_out(&mut self, blurred: AnyViewHandle, cx: &mut ViewContext<Self>) {
fn focus_out(&mut self, blurred: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.handle().id() == blurred.id() {
self.events.lock().push(format!("{} blurred", &self.name));
}

View File

@@ -17,10 +17,11 @@ use parking_lot::{Mutex, RwLock};
use smol::stream::StreamExt;
use crate::{
executor, keymap::Keystroke, platform, Action, AnyViewHandle, AppContext, Appearance, Entity,
Event, FontCache, InputHandler, KeyDownEvent, LeakDetector, ModelContext, ModelHandle,
MutableAppContext, Platform, ReadModelWith, ReadViewWith, RenderContext, Task, UpdateModel,
UpdateView, View, ViewContext, ViewHandle, WeakHandle, WindowInputHandler,
executor, geometry::vector::Vector2F, keymap::Keystroke, platform, Action, AnyViewHandle,
AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent, LeakDetector,
ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, ReadViewWith,
RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, WeakHandle,
WindowInputHandler,
};
use collections::BTreeMap;
@@ -275,6 +276,17 @@ impl TestAppContext {
}
}
pub fn simulate_window_resize(&self, window_id: usize, size: Vector2F) {
let mut window = self.window_mut(window_id);
window.size = size;
let mut handlers = mem::take(&mut window.resize_handlers);
drop(window);
for handler in &mut handlers {
handler();
}
self.window_mut(window_id).resize_handlers = handlers;
}
pub fn simulate_window_activation(&self, to_activate: Option<usize>) {
let mut handlers = BTreeMap::new();
{

View File

@@ -33,8 +33,8 @@ use crate::{
},
json,
presenter::MeasurementContext,
Action, DebugContext, Event, EventContext, LayoutContext, PaintContext, RenderContext,
SizeConstraint, View,
Action, DebugContext, EventContext, LayoutContext, PaintContext, RenderContext, SizeConstraint,
View,
};
use core::panic;
use json::ToJson;
@@ -50,7 +50,6 @@ use std::{
trait AnyElement {
fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F;
fn paint(&mut self, origin: Vector2F, visible_bounds: RectF, cx: &mut PaintContext);
fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool;
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
@@ -80,16 +79,6 @@ pub trait Element {
cx: &mut PaintContext,
) -> Self::PaintState;
fn dispatch_event(
&mut self,
event: &Event,
bounds: RectF,
visible_bounds: RectF,
layout: &mut Self::LayoutState,
paint: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool;
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
@@ -303,22 +292,6 @@ impl<T: Element> AnyElement for Lifecycle<T> {
}
}
fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool {
if let Lifecycle::PostPaint {
element,
bounds,
visible_bounds,
layout,
paint,
..
} = self
{
element.dispatch_event(event, *bounds, *visible_bounds, layout, paint, cx)
} else {
panic!("invalid element lifecycle state");
}
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
@@ -433,10 +406,6 @@ impl ElementRc {
self.element.borrow_mut().paint(origin, visible_bounds, cx);
}
pub fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool {
self.element.borrow_mut().dispatch_event(event, cx)
}
pub fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
@@ -495,7 +464,7 @@ pub trait ParentElement<'a>: Extend<ElementBox> + Sized {
impl<'a, T> ParentElement<'a> for T where T: Extend<ElementBox> {}
fn constrain_size_preserving_aspect_ratio(max_size: Vector2F, size: Vector2F) -> Vector2F {
pub fn constrain_size_preserving_aspect_ratio(max_size: Vector2F, size: Vector2F) -> Vector2F {
if max_size.x().is_infinite() && max_size.y().is_infinite() {
size
} else if max_size.x().is_infinite() || max_size.x() / max_size.y() > size.x() / size.y() {

View File

@@ -2,8 +2,7 @@ use crate::{
geometry::{rect::RectF, vector::Vector2F},
json,
presenter::MeasurementContext,
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
SizeConstraint,
DebugContext, Element, ElementBox, LayoutContext, PaintContext, SizeConstraint,
};
use json::ToJson;
@@ -84,18 +83,6 @@ impl Element for Align {
);
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn rect_for_text_range(
&self,
range_utf16: std::ops::Range<usize>,

View File

@@ -56,18 +56,6 @@ where
self.0(bounds, visible_bounds, cx)
}
fn dispatch_event(
&mut self,
_: &crate::Event,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut crate::EventContext,
) -> bool {
false
}
fn rect_for_text_range(
&self,
_: std::ops::Range<usize>,

View File

@@ -7,8 +7,7 @@ use crate::{
geometry::{rect::RectF, vector::Vector2F},
json,
presenter::MeasurementContext,
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
SizeConstraint,
DebugContext, Element, ElementBox, LayoutContext, PaintContext, SizeConstraint,
};
pub struct ConstrainedBox {
@@ -157,18 +156,6 @@ impl Element for ConstrainedBox {
self.child.paint(bounds.origin(), visible_bounds, cx);
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,

View File

@@ -11,7 +11,7 @@ use crate::{
platform::CursorStyle,
presenter::MeasurementContext,
scene::{self, Border, CursorRegion, Quad},
Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
Element, ElementBox, LayoutContext, PaintContext, SizeConstraint,
};
use serde::Deserialize;
use serde_json::json;
@@ -285,18 +285,6 @@ impl Element for Container {
}
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,

View File

@@ -9,7 +9,7 @@ use crate::{
presenter::MeasurementContext,
DebugContext,
};
use crate::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint};
use crate::{Element, LayoutContext, PaintContext, SizeConstraint};
#[derive(Default)]
pub struct Empty {
@@ -59,18 +59,6 @@ impl Element for Empty {
) -> Self::PaintState {
}
fn dispatch_event(
&mut self,
_: &Event,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut EventContext,
) -> bool {
false
}
fn rect_for_text_range(
&self,
_: Range<usize>,

View File

@@ -4,8 +4,7 @@ use crate::{
geometry::{rect::RectF, vector::Vector2F},
json,
presenter::MeasurementContext,
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
SizeConstraint,
DebugContext, Element, ElementBox, LayoutContext, PaintContext, SizeConstraint,
};
use serde_json::json;
@@ -66,18 +65,6 @@ impl Element for Expanded {
self.child.paint(bounds.origin(), visible_bounds, cx);
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,

View File

@@ -3,8 +3,8 @@ use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
use crate::{
json::{self, ToJson, Value},
presenter::MeasurementContext,
Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext,
LayoutContext, PaintContext, RenderContext, SizeConstraint, Vector2FExt, View,
Axis, DebugContext, Element, ElementBox, ElementStateHandle, LayoutContext, PaintContext,
RenderContext, SizeConstraint, Vector2FExt, View,
};
use pathfinder_geometry::{
rect::RectF,
@@ -259,7 +259,7 @@ impl Element for Flex {
if remaining_space < 0. {
let mut delta = match axis {
Axis::Horizontal => {
if e.delta.x() != 0. {
if e.delta.x().abs() >= e.delta.y().abs() {
e.delta.x()
} else {
e.delta.y()
@@ -277,7 +277,7 @@ impl Element for Flex {
cx.notify();
} else {
cx.propogate_event();
cx.propagate_event();
}
}
})
@@ -318,23 +318,6 @@ impl Element for Flex {
}
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
let mut handled = false;
for child in &mut self.children {
handled = child.dispatch_event(event, cx) || handled;
}
handled
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
@@ -420,18 +403,6 @@ impl Element for FlexItem {
self.child.paint(bounds.origin(), visible_bounds, cx)
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,

View File

@@ -4,8 +4,7 @@ use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::json,
presenter::MeasurementContext,
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
SizeConstraint,
DebugContext, Element, ElementBox, LayoutContext, PaintContext, SizeConstraint,
};
pub struct Hook {
@@ -56,18 +55,6 @@ impl Element for Hook {
self.child.paint(bounds.origin(), visible_bounds, cx);
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,

View File

@@ -6,8 +6,7 @@ use crate::{
},
json::{json, ToJson},
presenter::MeasurementContext,
scene, Border, DebugContext, Element, Event, EventContext, ImageData, LayoutContext,
PaintContext, SizeConstraint,
scene, Border, DebugContext, Element, ImageData, LayoutContext, PaintContext, SizeConstraint,
};
use serde::Deserialize;
use std::{ops::Range, sync::Arc};
@@ -81,18 +80,6 @@ impl Element for Image {
});
}
fn dispatch_event(
&mut self,
_: &Event,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut EventContext,
) -> bool {
false
}
fn rect_for_text_range(
&self,
_: Range<usize>,

View File

@@ -2,7 +2,7 @@ use crate::{
elements::*,
fonts::TextStyle,
geometry::{rect::RectF, vector::Vector2F},
Action, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
Action, ElementBox, LayoutContext, PaintContext, SizeConstraint,
};
use serde_json::json;
@@ -64,18 +64,6 @@ impl Element for KeystrokeLabel {
element.paint(bounds.origin(), visible_bounds, cx);
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: RectF,
element: &mut ElementBox,
_: &mut (),
cx: &mut EventContext,
) -> bool {
element.dispatch_event(event, cx)
}
fn rect_for_text_range(
&self,
_: Range<usize>,

View File

@@ -9,7 +9,7 @@ use crate::{
json::{ToJson, Value},
presenter::MeasurementContext,
text_layout::{Line, RunStyle},
DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
DebugContext, Element, LayoutContext, PaintContext, SizeConstraint,
};
use serde::Deserialize;
use serde_json::json;
@@ -165,18 +165,6 @@ impl Element for Label {
line.paint(bounds.origin(), visible_bounds, bounds.size().y(), cx)
}
fn dispatch_event(
&mut self,
_: &Event,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
_: &mut EventContext,
) -> bool {
false
}
fn rect_for_text_range(
&self,
_: Range<usize>,

View File

@@ -5,7 +5,7 @@ use crate::{
},
json::json,
presenter::MeasurementContext,
DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, MouseRegion,
DebugContext, Element, ElementBox, ElementRc, EventContext, LayoutContext, MouseRegion,
PaintContext, RenderContext, SizeConstraint, View, ViewContext,
};
use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc};
@@ -13,7 +13,6 @@ use sum_tree::{Bias, SumTree};
pub struct List {
state: ListState,
invalidated_elements: Vec<ElementRc>,
}
#[derive(Clone)]
@@ -39,8 +38,8 @@ struct StateInner {
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ListOffset {
item_ix: usize,
offset_in_item: f32,
pub item_ix: usize,
pub offset_in_item: f32,
}
#[derive(Clone)]
@@ -82,10 +81,7 @@ struct Height(f32);
impl List {
pub fn new(state: ListState) -> Self {
Self {
state,
invalidated_elements: Default::default(),
}
Self { state }
}
}
@@ -116,18 +112,7 @@ impl Element for List {
let mut new_items = SumTree::new();
let mut rendered_items = VecDeque::new();
let mut rendered_height = 0.;
let mut scroll_top = state
.logical_scroll_top
.unwrap_or_else(|| match state.orientation {
Orientation::Top => ListOffset {
item_ix: 0,
offset_in_item: 0.,
},
Orientation::Bottom => ListOffset {
item_ix: state.items.summary().count,
offset_in_item: 0.,
},
});
let mut scroll_top = state.logical_scroll_top();
// Render items after the scroll top, including those in the trailing overdraw.
let mut cursor = old_items.cursor::<Count>();
@@ -264,8 +249,8 @@ impl Element for List {
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
cx.scene.push_layer(Some(visible_bounds));
cx.scene
.push_mouse_region(MouseRegion::new::<Self>(10, 0, bounds).on_scroll({
cx.scene.push_mouse_region(
MouseRegion::new::<Self>(cx.current_view_id(), 0, bounds).on_scroll({
let state = self.state.clone();
let height = bounds.height();
let scroll_top = scroll_top.clone();
@@ -278,7 +263,8 @@ impl Element for List {
cx,
)
}
}));
}),
);
let state = &mut *self.state.0.borrow_mut();
for (mut element, origin) in state.visible_elements(bounds, scroll_top) {
@@ -288,50 +274,6 @@ impl Element for List {
cx.scene.pop_layer();
}
fn dispatch_event(
&mut self,
event: &Event,
bounds: RectF,
_: RectF,
scroll_top: &mut ListOffset,
_: &mut (),
cx: &mut EventContext,
) -> bool {
let mut handled = false;
let mut state = self.state.0.borrow_mut();
let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
let mut cursor = state.items.cursor::<Count>();
let mut new_items = cursor.slice(&Count(scroll_top.item_ix), Bias::Right, &());
while let Some(item) = cursor.item() {
if item_origin.y() > bounds.max_y() {
break;
}
if let ListItem::Rendered(element) = item {
let prev_notify_count = cx.notify_count();
let mut element = element.clone();
handled = element.dispatch_event(event, cx) || handled;
item_origin.set_y(item_origin.y() + element.size().y());
if cx.notify_count() > prev_notify_count {
new_items.push(ListItem::Unrendered, &());
self.invalidated_elements.push(element);
} else {
new_items.push(item.clone(), &());
}
cursor.next(&());
} else {
unreachable!();
}
}
new_items.push_tree(cursor.suffix(&()), &());
drop(cursor);
state.items = new_items;
handled
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
@@ -468,6 +410,20 @@ impl ListState {
) {
self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
}
pub fn logical_scroll_top(&self) -> ListOffset {
self.0.borrow().logical_scroll_top()
}
pub fn scroll_to(&self, mut scroll_top: ListOffset) {
let state = &mut *self.0.borrow_mut();
let item_count = state.items.summary().count;
if scroll_top.item_ix >= item_count {
scroll_top.item_ix = item_count;
scroll_top.offset_in_item = 0.;
}
state.logical_scroll_top = Some(scroll_top);
}
}
impl StateInner {
@@ -557,6 +513,22 @@ impl StateInner {
let visible_range = self.visible_range(height, scroll_top);
self.scroll_handler.as_mut().unwrap()(visible_range, cx);
}
cx.notify();
}
fn logical_scroll_top(&self) -> ListOffset {
self.logical_scroll_top
.unwrap_or_else(|| match self.orientation {
Orientation::Top => ListOffset {
item_ix: 0,
offset_in_item: 0.,
},
Orientation::Bottom => ListOffset {
item_ix: self.items.summary().count,
offset_in_item: 0.,
},
})
}
fn scroll_top(&self, logical_scroll_top: &ListOffset) -> f32 {
@@ -961,18 +933,6 @@ mod tests {
todo!()
}
fn dispatch_event(
&mut self,
_: &Event,
_: RectF,
_: RectF,
_: &mut (),
_: &mut (),
_: &mut EventContext,
) -> bool {
todo!()
}
fn rect_for_text_range(
&self,
_: Range<usize>,

View File

@@ -6,11 +6,10 @@ use crate::{
},
platform::CursorStyle,
scene::{
ClickRegionEvent, CursorRegion, DownOutRegionEvent, DownRegionEvent, DragRegionEvent,
HandlerSet, HoverRegionEvent, MoveRegionEvent, ScrollWheelRegionEvent, UpOutRegionEvent,
UpRegionEvent,
CursorRegion, HandlerSet, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseHover,
MouseMove, MouseScrollWheel, MouseUp, MouseUpOut,
},
DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MeasurementContext,
DebugContext, Element, ElementBox, EventContext, LayoutContext, MeasurementContext,
MouseButton, MouseRegion, MouseState, PaintContext, RenderContext, SizeConstraint, View,
};
use serde_json::json;
@@ -22,27 +21,52 @@ pub struct MouseEventHandler<Tag: 'static> {
cursor_style: Option<CursorStyle>,
handlers: HandlerSet,
hoverable: bool,
notify_on_hover: bool,
notify_on_click: bool,
above: bool,
padding: Padding,
_tag: PhantomData<Tag>,
}
/// Element which provides a render_child callback with a MouseState and paints a mouse
/// region under (or above) it for easy mouse event handling.
impl<Tag> MouseEventHandler<Tag> {
pub fn new<V, F>(region_id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
where
V: View,
F: FnOnce(MouseState, &mut RenderContext<V>) -> ElementBox,
F: FnOnce(&mut MouseState, &mut RenderContext<V>) -> ElementBox,
{
let mut mouse_state = cx.mouse_state::<Tag>(region_id);
let child = render_child(&mut mouse_state, cx);
let notify_on_hover = mouse_state.accessed_hovered();
let notify_on_click = mouse_state.accessed_clicked();
Self {
child: render_child(cx.mouse_state::<Tag>(region_id), cx),
child,
region_id,
cursor_style: None,
handlers: Default::default(),
notify_on_hover,
notify_on_click,
hoverable: true,
above: false,
padding: Default::default(),
_tag: PhantomData,
}
}
/// Modifies the MouseEventHandler to render the MouseRegion above the child element. Useful
/// for drag and drop handling and similar events which should be captured before the child
/// gets the opportunity
pub fn above<V, F>(region_id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
where
V: View,
F: FnOnce(&mut MouseState, &mut RenderContext<V>) -> ElementBox,
{
let mut handler = Self::new(region_id, cx, render_child);
handler.above = true;
handler
}
pub fn with_cursor_style(mut self, cursor: CursorStyle) -> Self {
self.cursor_style = Some(cursor);
self
@@ -53,10 +77,7 @@ impl<Tag> MouseEventHandler<Tag> {
self
}
pub fn on_move(
mut self,
handler: impl Fn(MoveRegionEvent, &mut EventContext) + 'static,
) -> Self {
pub fn on_move(mut self, handler: impl Fn(MouseMove, &mut EventContext) + 'static) -> Self {
self.handlers = self.handlers.on_move(handler);
self
}
@@ -64,7 +85,7 @@ impl<Tag> MouseEventHandler<Tag> {
pub fn on_down(
mut self,
button: MouseButton,
handler: impl Fn(DownRegionEvent, &mut EventContext) + 'static,
handler: impl Fn(MouseDown, &mut EventContext) + 'static,
) -> Self {
self.handlers = self.handlers.on_down(button, handler);
self
@@ -73,7 +94,7 @@ impl<Tag> MouseEventHandler<Tag> {
pub fn on_up(
mut self,
button: MouseButton,
handler: impl Fn(UpRegionEvent, &mut EventContext) + 'static,
handler: impl Fn(MouseUp, &mut EventContext) + 'static,
) -> Self {
self.handlers = self.handlers.on_up(button, handler);
self
@@ -82,7 +103,7 @@ impl<Tag> MouseEventHandler<Tag> {
pub fn on_click(
mut self,
button: MouseButton,
handler: impl Fn(ClickRegionEvent, &mut EventContext) + 'static,
handler: impl Fn(MouseClick, &mut EventContext) + 'static,
) -> Self {
self.handlers = self.handlers.on_click(button, handler);
self
@@ -91,7 +112,7 @@ impl<Tag> MouseEventHandler<Tag> {
pub fn on_down_out(
mut self,
button: MouseButton,
handler: impl Fn(DownOutRegionEvent, &mut EventContext) + 'static,
handler: impl Fn(MouseDownOut, &mut EventContext) + 'static,
) -> Self {
self.handlers = self.handlers.on_down_out(button, handler);
self
@@ -100,7 +121,7 @@ impl<Tag> MouseEventHandler<Tag> {
pub fn on_up_out(
mut self,
button: MouseButton,
handler: impl Fn(UpOutRegionEvent, &mut EventContext) + 'static,
handler: impl Fn(MouseUpOut, &mut EventContext) + 'static,
) -> Self {
self.handlers = self.handlers.on_up_out(button, handler);
self
@@ -109,23 +130,20 @@ impl<Tag> MouseEventHandler<Tag> {
pub fn on_drag(
mut self,
button: MouseButton,
handler: impl Fn(DragRegionEvent, &mut EventContext) + 'static,
handler: impl Fn(MouseDrag, &mut EventContext) + 'static,
) -> Self {
self.handlers = self.handlers.on_drag(button, handler);
self
}
pub fn on_hover(
mut self,
handler: impl Fn(HoverRegionEvent, &mut EventContext) + 'static,
) -> Self {
pub fn on_hover(mut self, handler: impl Fn(MouseHover, &mut EventContext) + 'static) -> Self {
self.handlers = self.handlers.on_hover(handler);
self
}
pub fn on_scroll(
mut self,
handler: impl Fn(ScrollWheelRegionEvent, &mut EventContext) + 'static,
handler: impl Fn(MouseScrollWheel, &mut EventContext) + 'static,
) -> Self {
self.handlers = self.handlers.on_scroll(handler);
self
@@ -148,6 +166,29 @@ impl<Tag> MouseEventHandler<Tag> {
)
.round_out()
}
fn paint_regions(&self, bounds: RectF, visible_bounds: RectF, cx: &mut PaintContext) {
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
let hit_bounds = self.hit_bounds(visible_bounds);
if let Some(style) = self.cursor_style {
cx.scene.push_cursor_region(CursorRegion {
bounds: hit_bounds,
style,
});
}
cx.scene.push_mouse_region(
MouseRegion::from_handlers::<Tag>(
cx.current_view_id(),
self.region_id,
hit_bounds,
self.handlers.clone(),
)
.with_hoverable(self.hoverable)
.with_notify_on_hover(self.notify_on_hover)
.with_notify_on_click(self.notify_on_click),
);
}
}
impl<Tag> Element for MouseEventHandler<Tag> {
@@ -169,38 +210,16 @@ impl<Tag> Element for MouseEventHandler<Tag> {
_: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
let hit_bounds = self.hit_bounds(visible_bounds);
if let Some(style) = self.cursor_style {
cx.scene.push_cursor_region(CursorRegion {
bounds: hit_bounds,
style,
if self.above {
self.child.paint(bounds.origin(), visible_bounds, cx);
cx.paint_layer(None, |cx| {
self.paint_regions(bounds, visible_bounds, cx);
});
} else {
self.paint_regions(bounds, visible_bounds, cx);
self.child.paint(bounds.origin(), visible_bounds, cx);
}
cx.scene.push_mouse_region(
MouseRegion::from_handlers::<Tag>(
cx.current_view_id(),
self.region_id,
hit_bounds,
self.handlers.clone(),
)
.with_hoverable(self.hoverable),
);
self.child.paint(bounds.origin(), visible_bounds, cx);
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
}
fn rect_for_text_range(

View File

@@ -4,8 +4,8 @@ use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::ToJson,
presenter::MeasurementContext,
Axis, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion,
PaintContext, SizeConstraint,
Axis, DebugContext, Element, ElementBox, LayoutContext, MouseRegion, PaintContext,
SizeConstraint,
};
use serde_json::json;
@@ -16,6 +16,7 @@ pub struct Overlay {
fit_mode: OverlayFitMode,
position_mode: OverlayPositionMode,
hoverable: bool,
z_index: Option<usize>,
}
#[derive(Copy, Clone)]
@@ -82,6 +83,7 @@ impl Overlay {
fit_mode: OverlayFitMode::None,
position_mode: OverlayPositionMode::Window,
hoverable: false,
z_index: None,
}
}
@@ -109,6 +111,11 @@ impl Overlay {
self.hoverable = hoverable;
self
}
pub fn with_z_index(mut self, z_index: usize) -> Self {
self.z_index = Some(z_index);
self
}
}
impl Element for Overlay {
@@ -204,37 +211,24 @@ impl Element for Overlay {
OverlayFitMode::None => {}
}
cx.scene.push_stacking_context(None);
cx.paint_stacking_context(None, self.z_index, |cx| {
if self.hoverable {
enum OverlayHoverCapture {}
// Block hovers in lower stacking contexts
cx.scene
.push_mouse_region(MouseRegion::new::<OverlayHoverCapture>(
cx.current_view_id(),
cx.current_view_id(),
bounds,
));
}
if self.hoverable {
enum OverlayHoverCapture {}
// Block hovers in lower stacking contexts
cx.scene
.push_mouse_region(MouseRegion::new::<OverlayHoverCapture>(
cx.current_view_id(),
cx.current_view_id(),
bounds,
));
}
self.child.paint(
bounds.origin(),
RectF::new(Vector2F::zero(), cx.window_size),
cx,
);
cx.scene.pop_stacking_context();
}
fn dispatch_event(
&mut self,
event: &Event,
_: RectF,
_: RectF,
_: &mut Self::LayoutState,
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
self.child.dispatch_event(event, cx)
self.child.paint(
bounds.origin(),
RectF::new(Vector2F::zero(), cx.window_size),
cx,
);
});
}
fn rect_for_text_range(

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