Compare commits

...

687 Commits

Author SHA1 Message Date
Joseph T. Lyons
bf4331e8cf Merge pull request #1829 from zed-industries/add-release-channel-information-to-telemetry-events
Add release channel information to telemetry events
2022-10-31 14:35:34 -07:00
Max Brunsfeld
8e7f711371 Merge pull request #1840 from zed-industries/build-themes-in-assets-crate
Generate themes before compiling the 'assets' crate
2022-10-31 14:19:31 -07:00
Max Brunsfeld
df837283e8 Merge pull request #1839 from zed-industries/contact-list-project-color
Don't use 'on' background color for projects in the contact list
2022-10-31 12:39:58 -07:00
Mikayla Maki
4e788b1818 zed 0.62.3 2022-10-31 11:15:29 -07:00
Mikayla Maki
b8733feeff Merge pull request #1833 from zed-industries/add-channel-to-db
Added channel info to database directories
2022-10-31 11:10:40 -07:00
Mikayla Maki
e8b917e3f0 Merge pull request #1832 from zed-industries/upgrade-migration-lib
Upgraded migration library to remove panic
2022-10-31 11:10:23 -07:00
Max Brunsfeld
5c0083d4dd zed 0.62.2 2022-10-27 16:49:05 -07:00
Max Brunsfeld
85cb68c3f5 Merge pull request #1830 from zed-industries/auto-update-filename
Make auto-update handle an app bundle name other than 'Zed.app'
2022-10-27 16:48:42 -07:00
Max Brunsfeld
7aec6b5531 zed 0.62.1 2022-10-27 15:45:30 -07:00
Max Brunsfeld
4dab0f89f4 Tweak version-bumping scripts 2022-10-27 15:45:07 -07:00
Max Brunsfeld
d2f6b315a3 Avoid posting in Discord about preview releases (for now)
Co-authored-by: Joseph Lyons <joseph@zed.dev>
2022-10-27 15:44:32 -07:00
Kay Simmons
62d4473c3f Merge pull request #1827 from zed-industries/fix-keymap-resolution-fallback
Keymap resolution fallbacks
2022-10-27 15:44:05 -07:00
Max Brunsfeld
6e2d3aae68 Merge pull request #1828 from zed-industries/following-scrollbar
Show scrollbar when scrolling while following
2022-10-27 15:43:53 -07:00
Max Brunsfeld
e5959483ed Merge pull request #1825 from zed-industries/update-notification-release-channel
Indicate release channel in auto-update notification
2022-10-27 11:01:41 -07:00
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
Max Brunsfeld
cf499abf31 v0.60.1 2022-10-13 10:00:07 -07:00
Antonio Scandurra
86ddbc6d26 Merge pull request #1752 from zed-industries/allow-inviting-users-to-another-guest-project
Allow inviting users to a project that was shared by someone else
2022-10-13 17:58:22 +01:00
Antonio Scandurra
b8bc5a282e Allow inviting users to a project that was shared by someone else
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-10-13 18:48:14 +02:00
Antonio Scandurra
f5db02a605 Merge pull request #1749 from zed-industries/child-view-panic
Prevent `ChildView` from retaining an otherwise dropped view
2022-10-13 15:45:53 +01:00
Antonio Scandurra
9ebd586350 Improve error message when rendering a child view for a dropped view
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-10-13 16:40:52 +02:00
Antonio Scandurra
1bec8087ee Add unit test for ChildView 2022-10-13 15:59:52 +02:00
Antonio Scandurra
a5a60eb854 Log view name alongside error in ChildView 2022-10-13 15:44:01 +02:00
Antonio Scandurra
edb61a9c8f Avoid panicking if child view points to a view that was not rendered 2022-10-13 15:11:57 +02:00
Antonio Scandurra
06dfb74663 Prevent ChildView from retaining an otherwise dropped view 2022-10-13 15:04:57 +02:00
Antonio Scandurra
26b03afa60 Merge pull request #1747 from zed-industries/fix-clangd-error
Fix error on clangd when `compile-commands.json` is present
2022-10-13 10:16:10 +01:00
Antonio Scandurra
c4680e66ff Fix error on clangd when compile-commands.json is present
The language server was failing because we were forgetting to provide
a `jsonrpc` field for responses to requests coming from the lsp.
2022-10-13 11:10:23 +02:00
Antonio Scandurra
06e9b8276f Merge pull request #1745 from zed-industries/contact-popover-focus
Fix some issues with contact popover focus
2022-10-13 08:37:14 +01:00
Antonio Scandurra
ad975da8bd Merge pull request #1746 from zed-industries/maintain-buffer-identity-across-renames
Preserve buffer identity when underlying entry temporarily disappears
2022-10-13 08:36:21 +01:00
Antonio Scandurra
37a0fd33c5 Use fake file system for buffer identity test 2022-10-13 09:33:55 +02:00
Antonio Scandurra
f28cc5ca0c Preserve buffer identity when underlying entry temporarily disappears 2022-10-13 09:10:10 +02:00
Antonio Scandurra
0a1aea6cb8 Add test to ensure buffer identity is kept across Project::rename 2022-10-13 08:17:35 +02: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
Max Brunsfeld
b5786cbf30 Dismiss contacts popover when clicking outside, even w/o focus change
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2022-10-12 17:55:11 -07:00
Max Brunsfeld
513c02e67f Remove spurious focus of contact popover when opening it
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2022-10-12 17:39:44 -07:00
Mikayla Maki
51c0a140c6 Merge pull request #1743 from zed-industries/new-settings-writing
Improved settings writing to be strongly typed and based on settings file content diffs
2022-10-12 17:18:48 -07:00
Mikayla Maki
e73270085b Fixed settings 2022-10-12 17:11:47 -07:00
Mikayla Maki
dd1320e6d1 Improved settings writing to be strongly typed and based on settings file content diffs
Co-Authored-By: kay@zed.dev
2022-10-12 17:05:23 -07:00
Kay Simmons
d42bf8eebe Merge pull request #1740 from zed-industries/fix-dock-focus-issues
Fix Dock infinite loop
2022-10-12 16:19:09 -07:00
K Simmons
2a1dbd6fb5 Update gpui focus test to match the new removal of intermediate focus filtering 2022-10-12 15:57:29 -07:00
Max Brunsfeld
9760eb0081 Merge pull request #1742 from zed-industries/deps-fixes
Re-export basic text types from text and language crates, remove unused deps from collab
2022-10-12 15:56:55 -07:00
Max Brunsfeld
6cdf4e98fc Re-export basic text types from text and language crates
Also avoid production dependencies on fs and rope in collab
2022-10-12 15:48:19 -07:00
K Simmons
2ff6ffff58 fix lock merge error 2022-10-12 15:39:04 -07:00
Kay Simmons
27a87c3d9e Merge branch 'main' into fix-dock-focus-issues 2022-10-12 15:18:28 -07:00
K Simmons
1d8717f4de Remove focus filtering from gpui so all focus events result in focus-in and focus-out calls
Remove pane focused event in favor of focus_in at the workspace level
Added is_child to ViewContext to determine if a given view is a child of the current view
Fix issue where dock would get in a infinite loop when activated after dragging an item out of it
Fix issue where the last focused view in an item was not correctly refocused when a pane is focused after switching active tabs

Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-10-12 15:10:00 -07:00
Max Brunsfeld
fedec68d39 Update Dockerfiles to use Rust 1.64 2022-10-12 14:25:40 -07:00
Max Brunsfeld
490a608663 v0.60.0 2022-10-12 14:18:40 -07:00
Max Brunsfeld
94a5bbc0ab Merge pull request #1739 from zed-industries/collab-menu-key-binding
Allow toggling collaboration menu from the keyboard
2022-10-12 14:17:51 -07:00
Max Brunsfeld
89f05ada0b Allow toggling collaboration menu from the keyboard 2022-10-12 14:14:03 -07:00
Max Brunsfeld
3bb1f0097f Merge pull request #1738 from zed-industries/out-of-sync-diagnostics
Fix bugs that caused guests to see different diagnostics than host
2022-10-12 14:13:16 -07:00
Max Brunsfeld
69dcfbb423 Send guests DiskBasedDiagnosticsFinished messages when they join a project
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2022-10-12 13:49:09 -07:00
Julia
e744520d90 Correctly offset diff hunk layouts 2022-10-12 16:40:19 -04:00
Max Brunsfeld
3c3671a193 Avoid sending stale diagnostics after sharing a worktree
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2022-10-12 11:33:19 -07:00
Max Brunsfeld
cbf31e6d27 Merge pull request #1734 from zed-industries/fix-layout-crash
Fix rounding error in computing editor's row range during layout
2022-10-12 09:38:47 -07:00
Mikayla Maki
b3567a7240 Merge pull request #1736 from zed-industries/fix-terminal-bold
Fix a bug in how I parse alacritty's styles
2022-10-12 09:38:27 -07:00
Mikayla Maki
296656570e Merge pull request #1735 from zed-industries/rollback-dock-anchor-setting
Stops the dock anchor from being written to settings
2022-10-12 09:34:27 -07:00
Mikayla Maki
aac24938f5 Fix a bug in how I parse alacritty's styles 2022-10-12 09:34:17 -07:00
Mikayla Maki
47332f97c7 Stops the dock anchor from being written to settings 2022-10-12 09:28:55 -07:00
Max Brunsfeld
1179f8f7be Fix rounding error in computing editor's row range during layout
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2022-10-12 09:19:29 -07:00
Antonio Scandurra
bd146306c6 Merge pull request #1729 from zed-industries/connection-timeout
Introduce client-side timeout when trying to connect
2022-10-12 14:43:35 +01:00
Antonio Scandurra
c4dde0f4e2 💄 2022-10-12 15:35:28 +02:00
Antonio Scandurra
ec19f0f8e9 Remove unnecessary async from Peer::add_connection 2022-10-12 15:32:39 +02:00
Antonio Scandurra
cc56fa9ea6 Introduce client-side timeout when trying to connect 2022-10-12 15:32:30 +02:00
Antonio Scandurra
a19783919c Merge pull request #1728 from zed-industries/plain-text-leaks
Clear auto-indent requests if they couldn't be computed
2022-10-12 10:38:44 +01:00
Antonio Scandurra
83d3fad80d Clear auto-indent requests if they couldn't be computed 2022-10-12 10:53:44 +02:00
Antonio Scandurra
202950aa98 Merge pull request #1726 from zed-industries/rejoining-projects
Fix opening a buffer after leaving and joining the same project
2022-10-12 09:37:50 +01:00
Antonio Scandurra
9adbab5d99 Fix opening a buffer after leaving and joining the same project
This bug existed prior to #1700 and was caused by not clearing the
buffers that were already shared with a peer that left and opened
a project using the same connection. When such peer would re-join
the project and open a buffer that it had opened previously, the
host assumed the peer had already seen that buffer and wouldn't bother
sending it again.
2022-10-12 10:31:06 +02:00
Julia
a6910584b6 Something's happening, nothing correct, but something 2022-10-12 00:39:56 -04:00
Mikayla Maki
e24a69b838 Merge pull request #1723 from zed-industries/test-branch
Writing settings
2022-10-11 20:33:03 -07:00
Mikayla Maki
b1f64d9550 Updated new vim tests with new rope crate 2022-10-11 20:25:39 -07:00
Mikayla Maki
41590ef64b Merge branch 'main' into test-branch 2022-10-11 19:55:32 -07:00
Mikayla Maki
e7b6d1befe Added theme and dock anchor saving :D 2022-10-11 19:18:29 -07:00
Max Brunsfeld
76a86b7e5e Merge pull request #1721 from zed-industries/scrollbar-fix
Fix scrollbar's range of motion in large files
2022-10-11 19:14:49 -07:00
Max Brunsfeld
7eceff1d7b Impose min scrollbar height in a way that doesn't impede scrollbar's movement
Also, fix the editor's scroll max so that you can scroll to the last
display row.
2022-10-11 18:50:04 -07:00
Kay Simmons
81a3a22379 Merge pull request #1685 from zed-industries/vim-text-objects
Vim Text Objects and Numeric Repitions
2022-10-11 16:36:19 -07:00
K Simmons
d1f1eb9a29 Add count argument to motion functions and add ability to jump to a given line 2022-10-11 16:27:54 -07:00
Mikayla Maki
5487f99ac7 Moved settings_file.rs into settings crate. Should be ready to start now :D 2022-10-11 16:03:38 -07:00
Joseph T Lyons
bc2a6e429c Use tag_name for Discord release message 2022-10-11 18:31:17 -04:00
Mikayla Maki
0beb97547e Finished refactoring out fs and rope 2022-10-11 15:25:54 -07:00
Joseph T. Lyons
941f4097fe Add amplitude release (#1720)
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2022-10-11 18:25:36 -04:00
K Simmons
673041d1f5 working quote and bracket text objects 2022-10-11 15:17:29 -07: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
Max Brunsfeld
638e9f9477 Merge pull request #1715 from zed-industries/scrollbars
Add scrollbars
2022-10-11 13:34:15 -07:00
Max Brunsfeld
acc85ad03c Impose a minimum height on the scrollbar 2022-10-11 13:18:33 -07:00
Mikayla Maki
0a8e2f6bb0 Moved fs to it's own crate, build failing due to cyclic dependency on rope 2022-10-11 13:03:36 -07:00
Max Brunsfeld
9bdcd37f60 Merge pull request #1718 from zed-industries/dont-wait-for-project-upload
Proceed with share while project state uploads
2022-10-11 12:48:33 -07:00
Mikayla Maki
a833652077 Undid change to paths variables and cleaned up leftovers 2022-10-11 12:48:30 -07:00
Mikayla Maki
7ce758b343 Added notes from working with Nathan 2022-10-11 12:48:30 -07:00
Mikayla Maki
cc8ae45012 Added theme writing code, really bad race condition 2022-10-11 12:48:30 -07:00
Max Brunsfeld
65b8c512fe Allow opening other local projects via contacts list 2022-10-11 12:37:00 -07:00
Max Brunsfeld
0e695eaae8 Wait for project sharing to complete in LSP status integration test 2022-10-11 12:15:54 -07:00
Max Brunsfeld
1f0a9ce418 Proceed with share while project state uploads
Co-authored-by: Antonio Scandurra <as-cii@zed.dev>
2022-10-11 10:17:20 -07:00
Antonio Scandurra
a656047c15 Merge pull request #1700 from zed-industries/room
Introduce call-based collaboration
2022-10-11 17:40:44 +01:00
Antonio Scandurra
f26695ea8c 💄 2022-10-11 18:34:04 +02:00
Max Brunsfeld
f4306d977f Refresh scrollbar auto-hide setting when opening a new editor 2022-10-11 09:28:17 -07:00
Max Brunsfeld
d93e75bf5f Make scrollbars a little bit narrower 2022-10-11 09:26:31 -07:00
Max Brunsfeld
67a32de7d4 Hide the scrollbar track, not just the thumb 2022-10-11 09:26:19 -07:00
Antonio Scandurra
ba6c5441c0 Always show invite link in contacts popover 2022-10-11 18:22:00 +02:00
Max Brunsfeld
e2700ff8c6 Enable/disable scrollbar auto-hide based on OS setting 2022-10-11 09:13:34 -07:00
Antonio Scandurra
f83de0a91c Respect contacts popover size 2022-10-11 17:30:17 +02:00
Antonio Scandurra
4c07a0782b Allow active call to be optional on workspace
This prepares us for a future where the workspace is unaware of the
active call and doesn't require all tests to invoke `call::init`.
2022-10-11 17:27:37 +02:00
Antonio Scandurra
ee2587d3e5 Fix integration tests 2022-10-11 17:09:54 +02:00
Antonio Scandurra
45d118f96f Decide whether to clip to visible bounds on a per-element basis
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-10-11 17:05:13 +02:00
Antonio Scandurra
eb711cde53 Polish styling of contacts popover
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-10-11 16:52:20 +02:00
Antonio Scandurra
4504b36c8f Show a different message when participant is active on unshared project
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-10-11 15:24:31 +02:00
Antonio Scandurra
29c3b81a0a Show prompt when closing last window while there's an active call
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-10-11 14:52:47 +02:00
Antonio Scandurra
feb17c29ec Show participant projects in contacts popover 2022-10-11 12:23:15 +02:00
Antonio Scandurra
8e7f96cebc Update contacts when automatically canceling calls 2022-10-11 11:55:15 +02:00
Antonio Scandurra
0a306808da Dismiss project shared notifications when a project was unshared 2022-10-11 11:44:31 +02:00
Antonio Scandurra
1d4bdfc4a1 Cancel calls automatically when caller hangs up or disconnects 2022-10-11 11:28:27 +02:00
Antonio Scandurra
9ec62d4c1f Foreground app when accepting calls and project shares 2022-10-11 11:03:49 +02:00
Antonio Scandurra
bf0a04ab50 Dismiss popover when contact finder is unfocused 2022-10-11 11:01:38 +02:00
Antonio Scandurra
bf488f2027 Show project root names when displaying incoming call notification 2022-10-11 10:59:36 +02:00
Max Brunsfeld
b229bc69b9 Tweak scrollbar styling 2022-10-10 17:54:40 -07:00
Max Brunsfeld
7b084199be Auto-hide scrollbars 2022-10-10 17:54:29 -07:00
Nathan Sobo
e0b6b0df2a Rename Join button to Open, rework message slightly 2022-10-10 18:12:00 -06:00
Max Brunsfeld
6dcf638322 Represent scrollbar range with f32s 2022-10-10 17:06:48 -07:00
Nathan Sobo
b8c2acf0f2 Show worktree root names when sharing additional projects on a call
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
2022-10-10 17:56:03 -06:00
Max Brunsfeld
eedcc585af Add scrollbars to editors 2022-10-10 16:20:47 -07:00
Kay Simmons
7528bf8f32 Merge pull request #1714 from zed-industries/fix-autoclose
Don't autoclose brackets when `close` is false
2022-10-10 15:35:47 -07:00
K Simmons
0d31ea7cf2 fix minor issue where undo is not available in visual mode 2022-10-10 15:34:40 -07:00
K Simmons
6a237deb21 Add some tests for portions of visual text objects. Note: they are slightly broken currently as described in the tests 2022-10-10 15:32:12 -07:00
Nate Butler
95bc18a995 Fix color ramps to use colored fg 2022-10-10 17:50:41 -04:00
K Simmons
d2494822b0 Add assertion context manager to TestAppContext and convert existing vim tests to use neovim backed test context 2022-10-10 14:46:07 -07: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
Nathan Sobo
425e540c9a Fix tests by providing close: true 2022-10-10 15:29:24 -06:00
Nathan Sobo
3ae96f2c6e Don't autoclose brackets when is false 2022-10-10 15:15:43 -06: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
Mikayla Maki
576581c20d Merge pull request #1699 from zed-industries/page-up
Implemented page up and page down for the editor
2022-10-10 11:43:35 -07:00
Mikayla Maki
1d2495d57b Re-arrange how lines are set 2022-10-10 11:38:28 -07:00
Julia
7d6690335f Merge pull request #1712 from zed-industries/dont-select-on-copy-by-default-terminal
Don't select on copy by default in the terminal
2022-10-10 14:15:20 -04:00
Julia
2f96a09c46 Don't select on copy by default in the terminal
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-10-10 13:48:56 -04:00
Antonio Scandurra
94c68d246e 📝
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-10-10 19:18:05 +02:00
Antonio Scandurra
8dc99d42ff Remove menu bar extra 2022-10-10 18:21:11 +02:00
Antonio Scandurra
04fcd18c75 Show contacts popover when clicking on menu bar extra 2022-10-10 16:30:02 +02:00
Antonio Scandurra
d9d99e5e04 Fix seed script 2022-10-10 16:05:22 +02:00
Antonio Scandurra
5f9cedad23 Add margin to picker in contacts popover 2022-10-10 16:05:09 +02:00
Antonio Scandurra
afaacba41f Merge remote-tracking branch 'origin/main' into room 2022-10-10 15:43:38 +02:00
Antonio Scandurra
3396a98978 💄 2022-10-10 14:41:18 +02:00
Antonio Scandurra
7cfe435e62 Style project shared notifications 2022-10-10 14:37:51 +02:00
Antonio Scandurra
9d990ae329 Show all room participants in titlebar
...and allow following them into external projects.
2022-10-10 14:20:45 +02:00
Antonio Scandurra
25ff5959fb Superimpose external location message on active view 2022-10-10 12:23:50 +02:00
Antonio Scandurra
d7bac3cea6 Style incoming call notification 2022-10-10 11:36:39 +02:00
Antonio Scandurra
79748803a9 Add leave button on active call header 2022-10-10 10:30:51 +02:00
Antonio Scandurra
6f4edf6df5 Move contact finder into contacts popover 2022-10-10 09:56:21 +02:00
Mikayla Maki
1af4b263b2 Implemented page up and page down for the editor 2022-10-09 19:19:40 -07: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
K Simmons
5fec8c8bfd Enable verifying of visual mode selections in neovim backed tests 2022-10-09 01:19:22 -07:00
K Simmons
f90b693ed5 fix some warnings and merge errors 2022-10-08 23:49:04 -07:00
K Simmons
515c1ea123 Fixed some neovim test context issues, added repeated commands in vim mode, and ported some tests to use the neovim testing strategy 2022-10-08 21:52:07 -07:00
K Simmons
b82db3a254 Adds word and sentence text objects along with a new vim testing system which uses cached neovim data to verify our test accuracy 2022-10-08 21:51:49 -07:00
Antonio Scandurra
34cb742db1 Set current location after calling another user 2022-10-08 14:47:40 +02:00
Antonio Scandurra
59aaf4ce1b Call contact on enter 2022-10-08 14:43:41 +02:00
Antonio Scandurra
d14744d02f Show current user in active call 2022-10-08 14:38:17 +02:00
Max Brunsfeld
e96abf1429 0.59.0 2022-10-07 14:51:18 -07:00
Mikayla Maki
2758234e03 Merge pull request #1693 from zed-industries/terminal-tidying
Terminal Tidying
2022-10-07 13:11:43 -07:00
Max Brunsfeld
00188511cb Merge pull request #1697 from zed-industries/css-highlighting-fixes
Highlighting fixes
2022-10-07 12:56:31 -07:00
Julia
4456e81163 Merge pull request #1696 from zed-industries/reset-diff-on-set-none-diff-base
Reset buffer git diff when setting diff base to None
2022-10-07 15:48:18 -04:00
Max Brunsfeld
6ecf870c66 Tweak SCREAMING_SNAKE_CASE regexes in highlight queries 2022-10-07 12:46:49 -07:00
Max Brunsfeld
95cb9ceac9 Collapse variant and type into the same color 2022-10-07 12:44:55 -07:00
Max Brunsfeld
fcf13b44fb CSS: color '#' the same as the rest of the color 2022-10-07 12:44:39 -07:00
Max Brunsfeld
070c4bc503 Add color for 'variable.special' and use it in highlight queries 2022-10-07 12:44:20 -07:00
Julia
e15f27106d Reset buffer git diff when setting diff base to None
Co-Authored-By: Joseph Lyons <joseph@zed.dev>
2022-10-07 15:37:37 -04:00
Mikayla Maki
15595a67fa Added a horrible hacky way of doing cmd-k correctly. 2022-10-07 12:04:26 -07:00
Mikayla Maki
bf50a8ad8e Implemented a simplistic version of correct cmd-k behavior 2022-10-07 11:37:39 -07:00
Mikayla Maki
188b775fa6 Fixed non-block terminal cursors being displayed incorrectly 2022-10-07 10:03:09 -07:00
Max Brunsfeld
ec76146a23 Merge pull request #1692 from zed-industries/avoid-duplicate-autoformat-edits
Avoid duplicate autoformat edits
2022-10-07 09:35:10 -07:00
Antonio Scandurra
f9fb3f78b2 WIP: Render active call in contacts popover
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-10-07 17:01:48 +02:00
Antonio Scandurra
96c5bb8c39 Fix flicker due to adding and removing menu bar extra unnecessarily 2022-10-07 15:07:09 +02:00
Antonio Scandurra
560d8a8004 Don't leave the room if there's a pending room update 2022-10-07 14:52:39 +02:00
Antonio Scandurra
251e06c50f 💄 2022-10-07 14:51:04 +02:00
Antonio Scandurra
6fb5901d69 Ensure sharing the same project twice is idempotent 2022-10-07 14:47:06 +02:00
Antonio Scandurra
d3cddfdced Fix styling for busy contacts 2022-10-07 14:42:18 +02:00
Antonio Scandurra
386de03f46 Fix room disconnection problems when creating room and sharing project 2022-10-07 14:39:11 +02:00
Antonio Scandurra
4aaf3df8c7 Show contact status 2022-10-07 13:56:28 +02:00
Antonio Scandurra
d7cea646fc Include a busy field in proto::Contact 2022-10-07 12:21:56 +02:00
Antonio Scandurra
e82320cde8 Never set a room on active call if it is offline 2022-10-07 12:00:23 +02:00
Antonio Scandurra
669406d5af Leave room when client is the only participant 2022-10-07 11:58:49 +02:00
Antonio Scandurra
b479c8c8ba Move project sharing into Room 2022-10-07 10:14:17 +02:00
Antonio Scandurra
3d467a9491 Unset room on active call when disconnecting 2022-10-07 09:23:25 +02:00
Julia
8fb8fff61b Merge pull request #1682 from zed-industries/load-diff-base-from-correct-relative-path
Fix some git gutter bugs
2022-10-06 22:28:52 -04:00
Julia
d67fad8dca Extend a test to cover repos not at worktree root 2022-10-06 22:20:10 -04:00
Nate Butler
431ac1267a Update contextMenu.ts 2022-10-06 21:08:53 -04:00
Max Brunsfeld
47a8e4222a Don't allow multiple concurrent formatting requests for the same buffer
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2022-10-06 17:03:38 -07:00
Max Brunsfeld
4508d94a3e In deterministic executor, ensure fake timers are ordered by wake time
Previously, advancing the clock would fail to wake a timer that was
set *after* another time whose wake time had not yet arrived.
2022-10-06 17:03:23 -07:00
Max Brunsfeld
8411d886ac Fix multi-line string formatting in editor_test.rs 2022-10-06 15:13:29 -07:00
Max Brunsfeld
17ed80f74d Merge pull request #1691 from zed-industries/move-tests
Organize buffer and editor tests
2022-10-06 13:39:54 -07:00
Max Brunsfeld
63e1c839fe Rename language::tests -> language::buffer_tests 2022-10-06 13:32:49 -07:00
Max Brunsfeld
b6525e9164 Extract editor tests to their own file 2022-10-06 13:32:34 -07:00
Max Brunsfeld
c0ee8dc007 Merge pull request #1689 from zed-industries/optimize-buffer-diff
Apply buffer diff edits as a single batch
2022-10-06 12:28:42 -07:00
Max Brunsfeld
fe7a39ba5c Apply buffer diff edits as a single batch 2022-10-06 11:54:28 -07:00
Max Brunsfeld
51fa06cc8d Merge pull request #1404 from zed-industries/html-support
Basic html support
2022-10-06 10:32:44 -07:00
Julia
771215d254 Reload git index on file events to catch new contents 2022-10-06 12:01:21 -04:00
Antonio Scandurra
9f81699e01 WIP: start on menu bar extra 2022-10-06 16:10:45 +02:00
Antonio Scandurra
95e08edbb8 Always include room id in protos
This is redundant, but it futures-proof the ability to talk about
multiple rooms at any given time and feels safer in terms of race
conditions.
2022-10-06 15:20:49 +02:00
Antonio Scandurra
baf6097b49 Remove stale contacts panel reference 2022-10-06 15:17:02 +02:00
Antonio Scandurra
4cb306fbf3 Implement call cancellation 2022-10-06 15:12:27 +02:00
Antonio Scandurra
2e84fc6737 Delete rooms without pending users or participants 2022-10-06 14:20:40 +02:00
Antonio Scandurra
c43956d70a Move contacts panel styles into contacts popover 2022-10-06 14:07:21 +02:00
Antonio Scandurra
40163da679 Move contacts panel features into collab_ui 2022-10-06 14:00:14 +02:00
Antonio Scandurra
7763acbdd5 Move IncomingCall into call crate 2022-10-06 09:52:03 +02:00
Antonio Scandurra
55cc142319 Move incoming calls into ActiveCall 2022-10-06 09:50:26 +02:00
Joseph T. Lyons
edf4c3ec00 Add Discord webhook for published releases (#1684) 2022-10-05 21:22:53 -04:00
Max Brunsfeld
b7e115a6a1 Add a test for multi-language auto-indent 2022-10-05 17:59:31 -07:00
Max Brunsfeld
7fb5fe036a Derive indent size from the language at the cursor when auto-indenting 2022-10-05 17:07:35 -07:00
Max Brunsfeld
8b86781ad1 Remove last usages of MultiBufferSnapshot::language 2022-10-05 14:44:34 -07:00
Julia
3f4be5521c Load diff base from correct relative path 2022-10-05 16:04:55 -04:00
Max Brunsfeld
aa86806408 Finish generalizing ToggleComments to support block comments 2022-10-05 12:25:32 -07:00
Nate Butler
5bc074005c WIP 2022-10-05 12:40:38 -04:00
Antonio Scandurra
fa31c9659b Check room invariants in Store::check_invariants 2022-10-05 16:29:22 +02:00
Antonio Scandurra
5ef342f8c4 Enhance integration test to verify creating rooms while busy 2022-10-05 16:20:01 +02:00
Antonio Scandurra
5b811e4304 Add integration test verifying calls to busy users 2022-10-05 16:14:40 +02:00
Antonio Scandurra
183ca5da6f Allow following users into external projects 2022-10-05 15:32:55 +02:00
Antonio Scandurra
8f8843711f Move logic for joining project into a global action in collab_ui 2022-10-05 15:04:03 +02:00
Antonio Scandurra
383c21046f Set room location when active workspace changes 2022-10-05 15:03:40 +02:00
Antonio Scandurra
78e3370c1e Set room only after project has been shared to avoid flicker 2022-10-05 11:19:44 +02:00
Antonio Scandurra
84eebbe24a Always open project when added to a call via the + button 2022-10-05 11:01:28 +02:00
Antonio Scandurra
087760dba0 Use AppContext instead of MutableAppContext for ActiveCall::global 2022-10-05 10:51:51 +02:00
Max Brunsfeld
d9fb8c90d8 Start work on toggling block comments for HTML 2022-10-04 17:27:03 -07:00
Julia
836b536a90 Merge pull request #1632 from zed-industries/git-gutter
Tracking PR: Git gutter
2022-10-04 15:12:28 -04:00
Julia
2bd947d4d0 Use correct start row for hunk retrieval & correct paint offset
Co-Authored-By: Joseph Lyons <joseph@zed.dev>
2022-10-04 15:04:42 -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
Max Brunsfeld
b5d941b10c 0.58.0 2022-10-04 11:43:52 -07: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
Antonio Scandurra
fceba6814f Automatically share project when creating the room
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-10-04 19:25:48 +02: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
Antonio Scandurra
678b013da6 Don't show share button for remote projects
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-10-04 18:35:54 +02:00
Antonio Scandurra
ebee2168fc Re-emit notifications and events from ActiveCall
This lets us only observe and subscribe to the active call without
needing to track the underlying `Room` if it changes, which implies
writing the same boilerplate over and over.
2022-10-04 18:15:56 +02:00
Antonio Scandurra
41240351d3 Simplify Collaborator to stop including the user
It can be retrieved from the `Room` and we're guaranteed to have
a room in order to have collaborators in a project.
2022-10-04 18:00:54 +02:00
Antonio Scandurra
debedaf004 Show notification when a new project is shared and allow joining it 2022-10-04 16:55:41 +02:00
Antonio Scandurra
57930cb88a Show Share button for unshared projects when inside of a room 2022-10-04 15:56:20 +02:00
Antonio Scandurra
de917c4678 Use a different style for inactive participants 2022-10-04 15:06:20 +02:00
Antonio Scandurra
456dde200c Implement Room::set_location 2022-10-04 11:46:01 +02:00
Max Brunsfeld
218ba81013 Fix autoclose error when cursor was at column 0 2022-10-03 17:44:18 -07:00
Mikayla Maki
499e95d16a Removed debugs, simplified settings 2022-10-03 17:43:05 -07:00
Mikayla Maki
6f7547d28f Fixed a couple bugs in tests and worktree path handling 2022-10-03 17:18:38 -07:00
Max Brunsfeld
c354b9b959 Add assertions to test for autoclose with embedded languages 2022-10-03 13:24:37 -07:00
Max Brunsfeld
841ba405f0 Merge pull request #1680 from zed-industries/telemetry-tweaks
Telemetry tweaks
2022-10-03 13:08:05 -07:00
Julia
6f6d72890a Once again respect user settings for git gutter
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-10-03 15:42:30 -04:00
Max Brunsfeld
f3d83631ef Remove unnecessary min_id_length option from amplitude requests 2022-10-03 12:13:27 -07:00
Julia
e6487de069 Rename head text to indicate that it's not always going to be from head
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-10-03 15:11:06 -04:00
Julia
a5c2f22bf7 Move git gutter settings out of editor settings
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-10-03 14:53:33 -04:00
Nate Butler
7080dc9c23 WIP 2022-10-03 14:08:01 -04:00
Max Brunsfeld
06813be5c8 Mark platform as "Zed" for telemetry events from the app
Co-authored-by: Joseph Lyons <joseph@zed.dev>
2022-10-03 11:05:45 -07:00
Julia
8f4b3c3493 Store repo content path as absolute
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-10-03 14:00:58 -04:00
Max Brunsfeld
4477f95ee6 Set staff user property in telemetry
Co-authored-by: Joseph Lyons <joseph@zed.dev>
2022-10-03 10:52:57 -07:00
Julia
9427bb7553 Be clearer about using GitFilesIncluded setting 2022-10-03 11:58:48 -04:00
Antonio Scandurra
1e45198b9f Emit event on Room when a user shares a new project 2022-10-03 17:12:07 +02:00
Antonio Scandurra
ad323d6e3b Automatically fetch remote participant users in Room 2022-10-03 16:09:49 +02:00
Antonio Scandurra
da6106db8e Prevent calls from users who aren't contacts 2022-10-03 15:54:20 +02:00
Antonio Scandurra
bec6b41448 Fix randomized integration test failure 2022-10-03 15:50:47 +02:00
Antonio Scandurra
6426037653 Adapt integration tests to always pass a room id to Project::share
Randomized test is failing, so we'll look into that next.
2022-10-03 15:44:11 +02:00
Mikayla Maki
01176e04b7 Added clarification for git gutter settings 2022-10-02 18:42:03 -07:00
Mikayla Maki
c237075102 Touched up settings text 2022-10-02 18:35:19 -07:00
Mikayla Maki
0f1d71c38f Merge branch 'main' into git-gutter 2022-10-02 18:03:50 -07:00
Mikayla Maki
56b4162023 Fix stray merge failure 2022-10-02 18:02:25 -07:00
Antonio Scandurra
fd42811ef1 Cache CGEventSource and avoid leaking CGEvent when handling events 2022-10-02 18:01:49 -07:00
Max Brunsfeld
34926abe83 0.57.0 2022-10-02 18:01:49 -07:00
Max Brunsfeld
1aa554f4c9 Fix FakeServer to expect new GetPrivateUserInfo request 2022-10-02 18:01:49 -07:00
Mikayla Maki
52dbf2f9b8 add proto stuff 2022-10-02 18:01:37 -07:00
Mikayla Maki
5769cdc354 made git diff rendering respect line wrap 2022-10-02 18:00:13 -07:00
Julia
7f84abaf13 Increment protocol version again for previous commit 2022-10-02 14:11:35 -04:00
Mikayla Maki
512f817e2f Added proto messages for updating the head text 2022-10-01 18:18:35 -07:00
Mikayla Maki
8c24c858c9 Touched up comments 2022-09-30 17:36:22 -07:00
Mikayla Maki
a1299d9b68 Fixed 1 test 2022-09-30 17:34:14 -07:00
Mikayla Maki
af0974264c Refactored git repository code to seperate out repository entry tracking data and git2 mocking code.
Co-authored-by: Max <max@zed.dev>
Co-authored-by: Julia <julia@zed.dev>
2022-09-30 17:33:34 -07:00
Julia
c95646a298 WIP Start refactoring separation of concerns for repo metadata
Co-Authored-By: Max Brunsfeld <max@zed.dev>
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-09-30 18:25:25 -04:00
Julia
42b7820dbb Perform git diff on remote buffer open 2022-09-30 18:05:09 -04:00
Julia
ce7f6dd082 Start a test for remote git data updating
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-09-30 15:51:32 -04:00
Julia
6540936970 Fix some panics in tests 2022-09-30 13:51:54 -04:00
Julia
1c5d15b85e Use sumtree instead of iterator linear search for diff hunks in range
Co-Authored-By: Max Brunsfeld <max@zed.dev>
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-09-30 13:32:54 -04:00
Antonio Scandurra
964a5d2db7 WIP: require sharing projects on a given Room 2022-09-30 18:21:47 +02:00
Julia
bce25918a0 Fix test build 2022-09-30 11:13:22 -04:00
Antonio Scandurra
074b8f18d1 Rip out project registration and use sharing/unsharing instead 2022-09-30 12:23:57 +02:00
Antonio Scandurra
be8990ea78 Remove project join requests 2022-09-30 11:35:50 +02:00
Antonio Scandurra
761ae3ae6f Merge pull request #1673 from zed-industries/fix-cgevent-memory-leak
Cache `CGEventSource` and avoid leaking `CGEvent` when handling events
2022-09-30 09:04:25 +01:00
Antonio Scandurra
25bba396ef Cache CGEventSource and avoid leaking CGEvent when handling events 2022-09-30 09:51:03 +02:00
Joseph T Lyons
3c62de34f7 Change journal location setting name to "path" and default to ~ 2022-09-29 17:12:57 -04:00
Antonio Scandurra
b35e8f0164 Remove projects from contact updates
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-09-29 19:40:36 +02:00
Nate Butler
a6cccf82f7 Fix illegible rename text 2022-09-29 13:28:31 -04:00
Julia
fcf11b1181 Bump protocol version to be ahead of main 2022-09-29 13:16:02 -04:00
Julia
e865b85d9c Track index instead of head for diffs 2022-09-29 13:16:02 -04:00
Mikayla Maki
9fe6a5e83e made git stuff slightly more themable 2022-09-29 13:16:02 -04:00
Nate Butler
b395fbb3f2 wip 2022-09-29 13:16:02 -04:00
Nate Butler
8a2430090b WIP Git gutter styling 2022-09-29 13:16:02 -04:00
Mikayla Maki
113d3b88d0 Added test, and fix, for changed_repos method on LocalWorktree 2022-09-29 13:16:02 -04:00
Julia
f7714a25d1 Don't pretend this is async 2022-09-29 13:16:02 -04:00
Mikayla Maki
71b2126eca WIP, re-doing fs and fake git repos 2022-09-29 13:16:02 -04:00
Julia
d5fd531743 Move git related things into specialized git crate
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-09-29 13:16:02 -04:00
Julia
bf3b3da6ed Build again 2022-09-29 13:16:02 -04:00
Julia
7e5d49487b WIP Notifying buffers of head text change
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-09-29 13:16:02 -04:00
Julia
759b7f1e07 Update repo scan id when files under dot git dir events
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-09-29 13:16:02 -04:00
Julia
d2b18790a0 Remove git repos from worktree when deleted on storage
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-09-29 13:16:02 -04:00
Julia
4251e0f5f1 Find repos under worktree & return correct results for repo queries
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-09-29 13:16:02 -04:00
Mikayla Maki
c8e63d76a4 Get the test to failing,,, correctly 2022-09-29 13:16:02 -04:00
Mikayla Maki
6ac9308a03 Added git repository type infrastructure and moved git file system stuff into fs abstraction so we can test without touching the file system. Co-Authored-By: kay@zed.dev 2022-09-29 13:16:02 -04:00
Mikayla Maki
0d1b2a7e46 WIP - max & mikayla working on tests 2022-09-29 13:16:02 -04:00
Julia
bb8798a844 WIP pls amend me
Co-Authored-By: Max Brunsfeld <max@zed.dev>
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-09-29 13:16:02 -04:00
Julia
8d2de1074b Pull git indicator colors out of theme
Co-Authored-By: Kay Simmons <kay@zed.dev>
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-09-29 13:16:02 -04:00
Julia
632f47930f Utilize initial file contents as head text by default
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-09-29 13:16:02 -04:00
Julia
a679557e40 Avoid racing git diffs & allow for "as fast as possible" diff updating
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-09-29 13:16:02 -04:00
Julia
b18dd8fcff Fully qualify outside git-related code when a diff is a git diff 2022-09-29 13:16:02 -04:00
Julia
8edee9b2a8 Async-ify head text loading 2022-09-29 13:16:02 -04:00
Julia
6633c0b328 Perform initial file load git diff async 2022-09-29 13:16:02 -04:00
Julia
6825b6077a Properly invalidate when async git diff completes 2022-09-29 13:16:02 -04:00
Julia
9c82954877 Changed diffs to be async and dropped git delay 2022-09-29 13:16:02 -04:00
Julia
c4da8c46f7 Disable unnecessary libgit2 cargo features
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-09-29 13:16:02 -04:00
Julia
b9d84df127 Track buffer row divergence while iterating through diff lines
This allows for offsetting head row index of deleted lines to normalize
into buffer row space
2022-09-29 13:16:02 -04:00
Julia
446bf88655 Use row range while building buffer range during diff line iteration 2022-09-29 13:16:02 -04:00
Julia
03b6f3e0bf Reorganize for for purely file level invalidation 2022-09-29 13:16:02 -04:00
Julia
e72e132ce2 Clear out commented code & once again perform full file diff on update 2022-09-29 13:16:02 -04:00
Julia
c1249a3d84 Handle deletions more robustly and correctly 2022-09-29 13:16:02 -04:00
Julia
96917a8007 Small clean 2022-09-29 13:16:02 -04:00
Julia
2f7283fd13 buffer_divergence doesn't seem to be a concept that needs to be tracked 2022-09-29 13:16:02 -04:00
Julia
e0ea932fa7 Checkpoint preparing for a more organized approach to incremental diff 2022-09-29 13:16:02 -04:00
Julia
4b2040a7ca Move diff logic back into BufferDiff::update 2022-09-29 13:16:02 -04:00
Julia
a2e8fc79d9 Switch head range from row range to byte offset range
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-09-29 13:16:02 -04:00
Julia
61ff24edc8 Move cloneable diff state into new snapshot type
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-09-29 13:16:02 -04:00
Julia
a86e93d46f Checkpoint on incremental diff sumtree shenanigans 2022-09-29 13:16:02 -04:00
Julia
883d5b7a08 Update git gutter status after debounced delay
Co-authored-by: Max Brunsfeld <max@zed.com>
2022-09-29 13:16:02 -04:00
Julia
5157c71fa9 Render deletion gutter markers 2022-09-29 13:16:02 -04:00
Julia
fdda2abb78 Correct start/end of git diff hunks 2022-09-29 13:16:02 -04:00
Julia
641daf0a6e Correct git gutter indicator scroll position & add rounded corner 2022-09-29 13:16:02 -04:00
Julia
55ca02351c Start painting some sort of hunk info, it's wrong but it's close
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-09-29 13:16:01 -04:00
Julia
6fa2e62fa4 Start asking Editors to update git after a debounced delay 2022-09-29 13:16:01 -04:00
ForLoveOfCats
2a14af4cde Load a file's head text on file load just to get started 2022-09-29 13:16:01 -04:00
Antonio Scandurra
1898e813f5 Encapsulate Room interaction within ActiveCall
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-09-29 17:39:53 +02:00
Antonio Scandurra
e0db62173a Rename room crate to call
Also, rename `client::Call` to `client::IncomingCall`.

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-09-29 17:24:31 +02:00
Antonio Scandurra
1158911560 Wire up accepting/declining a call 2022-09-29 15:33:33 +02:00
Antonio Scandurra
634f9de7e6 Avoid using global for Room and extract that logic into ActiveCall 2022-09-29 10:48:51 +02: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
Max Brunsfeld
4f44375abd Make Buffer::language_at fall back to Buffer::language
For languages with no grammar (plain text), there
will be no layers.
2022-09-28 13:38:54 -07: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
Max Brunsfeld
2da32af340 Update EditorTestContext usage to reflect new synchronous constructor 2022-09-28 12:36:55 -07:00
Max Brunsfeld
2b0794f5ae Restructure autoclosing to account for multi-language documents 2022-09-28 12:32:04 -07:00
Max Brunsfeld
67e188a015 Add Buffer::language_at, update MultiBuffer to use it
Co-authored-by: Julia Risley <floc@unpromptedtirade.com>
2022-09-28 12:32:04 -07:00
Max Brunsfeld
a2e57e8d71 Add basic syntax highlighting for CSS 2022-09-28 12:32:04 -07:00
Max Brunsfeld
21fb2b9bf1 Tweak HTML indents and highlights 2022-09-28 12:32:04 -07:00
Max Brunsfeld
e4f5e85c3c Add JavaScript language injection in HTML 2022-09-28 12:32:04 -07:00
Isaac Clayton
a48995c782 Basic html highlighting + lsp support 2022-09-28 12:32:04 -07:00
Antonio Scandurra
04d194924e WIP: Start on ActiveCall
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-09-28 19:50:13 +02:00
Antonio Scandurra
46b61feb9a Open popup window when receiving a call
We still need to style and allow people to accept the call but this
is a good starting point.

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-09-28 19:35:24 +02:00
Antonio Scandurra
aa3cb8e35e Rename collab_titlebar_item crate to collab_ui
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-09-28 19:14:31 +02:00
Nathan Sobo
8ff4f044b7 Start a call when clicking on a contact in the contacts popover
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
2022-09-28 11:02:26 -06:00
Nate Butler
ab3a6f775e WIP Titlebar styling 2022-09-28 13:01:12 -04:00
Nathan Sobo
815cf44647 Rename AddParticipantPopover to ContactsPopover
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
2022-09-28 09:10:01 -06:00
Nathan Sobo
f5b2d56efd Remove contacts menu bar extra
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
2022-09-28 09:06:28 -06:00
Antonio Scandurra
1d1bd3975a Remove current user from contacts
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
Co-Authored-By: Mikayla Maki <mikayla@zed.dev>
2022-09-28 11:33:38 +02:00
Antonio Scandurra
4b73239972 WIP: Start moving contacts panel into "add participants" popover 2022-09-28 11:33:38 +02:00
Antonio Scandurra
0a29e13d4a Add active style when participant popover is open 2022-09-28 11:33:38 +02:00
Antonio Scandurra
0db6eb2fb8 Show add participant popover on click 2022-09-28 11:33:38 +02:00
Antonio Scandurra
782309f369 Rename contacts_titlebar_item to collab_titlebar_item 2022-09-28 11:33:38 +02:00
Antonio Scandurra
5a3a85b2c8 Introduce a + button in the titlebar 2022-09-28 11:33:38 +02:00
Antonio Scandurra
c8a48e8990 Extract contacts titlebar item into a separate crate
This allows us to implement a new contacts popover that uses the
`editor` crate.
2022-09-28 11:33:38 +02:00
Antonio Scandurra
80ab144bf3 Ring users upon connection if somebody was calling them before connecting
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-09-28 11:33:38 +02:00
Antonio Scandurra
6aa0f0b200 Leave room automatically on disconnection
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-09-28 11:33:38 +02:00
Antonio Scandurra
f0c45cbceb Remove projects from basic calls test for now 2022-09-28 11:33:38 +02:00
Antonio Scandurra
e55e7e4844 Leave room when Room entity is dropped 2022-09-28 11:33:38 +02:00
Antonio Scandurra
573086eed2 Always rely on the server to cancel the incoming call 2022-09-28 11:33:38 +02:00
Antonio Scandurra
df285def59 💄 2022-09-28 11:33:38 +02:00
Antonio Scandurra
bb9ce86a29 Introduce the ability of declining calls 2022-09-28 11:33:38 +02:00
Antonio Scandurra
f4697ff4d1 Prevent the same user from being called more than once 2022-09-28 11:33:38 +02:00
Antonio Scandurra
55b095cbd3 Implement joining a room and sending updates after people join/leave 2022-09-28 11:33:38 +02:00
Antonio Scandurra
4a9bf8f4fe Introduce call infrastructure
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-09-28 11:33:38 +02:00
Antonio Scandurra
ebb5ffcedc Introduce the ability of creating rooms on the server 2022-09-28 11:33:38 +02:00
Antonio Scandurra
0b1e372d11 Start sketching out an integration test for calls
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2022-09-28 11:33:38 +02:00
Antonio Scandurra
8fec7da799 WIP 2022-09-28 11:33:38 +02:00
Antonio Scandurra
46019f8537 WIP 2022-09-28 11:33:38 +02: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
446 changed files with 33514 additions and 20340 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}

34
.github/workflows/release_actions.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
on:
release:
types: [published]
jobs:
discord_release:
runs-on: ubuntu-latest
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v5.3.0
if: ${{ ! github.event.release.prerelease }}
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: |
📣 Zed ${{ github.event.release.tag_name }} was just released!
Restart your Zed or head to https://zed.dev/releases to grab it.
```md
### Changelog
${{ github.event.release.body }}
```
amplitude_release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.10.5"
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 }}

13
.gitignore vendored
View File

@@ -7,5 +7,14 @@
/crates/collab/static/styles.css
/vendor/bin
/assets/themes/*.json
/assets/themes/internal/*.json
/assets/themes/experiments/*.json
/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

1722
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,69 @@
[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"
[workspace.dependencies]
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
rand = { version = "0.8" }
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "366210ae925d7ea0891bc7a0c738f60c77c04d7b" }
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
@@ -13,11 +74,10 @@ 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"
[profile.release]
debug = true

View File

@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2
FROM rust:1.62-bullseye as builder
FROM rust:1.64-bullseye as builder
WORKDIR app
COPY . .
@@ -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.62-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

@@ -1,4 +0,0 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 11C5 14.3137 7.68629 17 11 17C14.3137 17 17 14.3137 17 11C17 7.68629 14.3137 5 11 5C7.68629 5 5 7.68629 5 11ZM11 3C6.58172 3 3 6.58172 3 11C3 15.4183 6.58172 19 11 19C15.4183 19 19 15.4183 19 11C19 6.58172 15.4183 3 11 3Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.09092 8.09088H14.6364L10.5511 12.4545H12.4546L13.9091 13.9091H7.36365L11.7273 9.54543H9.54547L8.09092 8.09088Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 571 B

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"
}
},
@@ -376,6 +395,7 @@
{
"bindings": {
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
"cmd-shift-c": "collab::ToggleCollaborationMenu",
"cmd-alt-i": "zed::DebugElements"
}
},
@@ -395,7 +415,6 @@
"context": "Workspace",
"bindings": {
"shift-escape": "dock::FocusDock",
"cmd-shift-c": "contacts_panel::ToggleFocus",
"cmd-shift-b": "workspace::ToggleRightSidebar"
}
},
@@ -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

@@ -9,11 +9,10 @@
}
],
"h": "vim::Left",
"backspace": "vim::Left",
"backspace": "vim::Backspace",
"j": "vim::Down",
"k": "vim::Up",
"l": "vim::Right",
"0": "vim::StartOfLine",
"$": "vim::EndOfLine",
"shift-g": "vim::EndOfDocument",
"w": "vim::NextWordStart",
@@ -38,7 +37,60 @@
}
],
"%": "vim::Matching",
"escape": "editor::Cancel"
"escape": "editor::Cancel",
"i": [
"vim::PushOperator",
{
"Object": {
"around": false
}
}
],
"a": [
"vim::PushOperator",
{
"Object": {
"around": true
}
}
],
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
"1": [
"vim::Number",
1
],
"2": [
"vim::Number",
2
],
"3": [
"vim::Number",
3
],
"4": [
"vim::Number",
4
],
"5": [
"vim::Number",
5
],
"6": [
"vim::Number",
6
],
"7": [
"vim::Number",
7
],
"8": [
"vim::Number",
8
],
"9": [
"vim::Number",
9
]
}
},
{
@@ -98,6 +150,15 @@
]
}
},
{
"context": "Editor && vim_operator == n",
"bindings": {
"0": [
"vim::Number",
0
]
}
},
{
"context": "Editor && vim_operator == g",
"bindings": {
@@ -112,13 +173,6 @@
{
"context": "Editor && vim_operator == c",
"bindings": {
"w": "vim::ChangeWord",
"shift-w": [
"vim::ChangeWord",
{
"ignorePunctuation": true
}
],
"c": "vim::CurrentLine"
}
},
@@ -134,9 +188,34 @@
"y": "vim::CurrentLine"
}
},
{
"context": "Editor && VimObject",
"bindings": {
"w": "vim::Word",
"shift-w": [
"vim::Word",
{
"ignorePunctuation": true
}
],
"s": "vim::Sentence",
"'": "vim::Quotes",
"`": "vim::BackQuotes",
"\"": "vim::DoubleQuotes",
"(": "vim::Parentheses",
")": "vim::Parentheses",
"[": "vim::SquareBrackets",
"]": "vim::SquareBrackets",
"{": "vim::CurlyBrackets",
"}": "vim::CurlyBrackets",
"<": "vim::AngleBrackets",
">": "vim::AngleBrackets"
}
},
{
"context": "Editor && vim_mode == visual",
"bindings": {
"u": "editor::Undo",
"c": "vim::VisualChange",
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",

View File

@@ -1,206 +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,
// 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,
// 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

29
crates/assets/build.rs Normal file
View File

@@ -0,0 +1,29 @@
use std::process::Command;
fn main() {
let output = Command::new("npm")
.current_dir("../../styles")
.args(["install", "--no-save"])
.output()
.expect("failed to run npm");
if !output.status.success() {
panic!(
"failed to install theme dependencies {}",
String::from_utf8_lossy(&output.stderr)
);
}
let output = Command::new("npm")
.current_dir("../../styles")
.args(["run", "build"])
.output()
.expect("failed to run npm");
if !output.status.success() {
panic!(
"build script failed {}",
String::from_utf8_lossy(&output.stderr)
);
}
println!("cargo:rerun-if-changed=../../styles/src");
}

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,
)
@@ -211,11 +218,14 @@ impl AutoUpdater {
let temp_dir = tempdir::TempDir::new("zed-auto-update")?;
let dmg_path = temp_dir.path().join("Zed.dmg");
let mount_path = temp_dir.path().join("Zed");
let mut mounted_app_path: OsString = mount_path.join("Zed.app").into();
mounted_app_path.push("/");
let running_app_path = ZED_APP_PATH
.clone()
.map_or_else(|| cx.platform().app_path(), Ok)?;
let running_app_filename = running_app_path
.file_name()
.ok_or_else(|| anyhow!("invalid running app path"))?;
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
mounted_app_path.push("/");
let mut dmg_file = File::create(&dmg_path).await?;
let mut response = client.get(&release.url, Default::default(), true).await?;
@@ -283,9 +293,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 +303,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

@@ -5,7 +5,7 @@ use gpui::{
Element, Entity, MouseButton, View, ViewContext,
};
use menu::Cancel;
use settings::Settings;
use settings::{ReleaseChannel, Settings};
use workspace::Notification;
pub struct UpdateNotification {
@@ -29,13 +29,15 @@ impl View for UpdateNotification {
let theme = cx.global::<Settings>().theme.clone();
let theme = &theme.update_notification;
let app_name = cx.global::<ReleaseChannel>().name();
MouseEventHandler::<ViewReleaseNotes>::new(0, cx, |state, cx| {
Flex::column()
.with_child(
Flex::row()
.with_child(
Text::new(
format!("Updated to Zed {}", self.version),
format!("Updated to {app_name} {}", self.version),
theme.message.text.clone(),
)
.contained()

40
crates/call/Cargo.toml Normal file
View File

@@ -0,0 +1,40 @@
[package]
name = "call"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/call.rs"
doctest = false
[features]
test-support = [
"client/test-support",
"collections/test-support",
"gpui/test-support",
"live_kit_client/test-support",
"project/test-support",
"util/test-support"
]
[dependencies]
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"] }
[dev-dependencies]
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"] }

300
crates/call/src/call.rs Normal file
View File

@@ -0,0 +1,300 @@
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, WeakModelHandle,
};
pub use participant::ParticipantLocation;
use postage::watch;
use project::Project;
pub use room::Room;
use std::sync::Arc;
pub fn init(client: Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut MutableAppContext) {
let active_call = cx.add_model(|cx| ActiveCall::new(client, user_store, cx));
cx.set_global(active_call);
}
#[derive(Clone)]
pub struct IncomingCall {
pub room_id: u64,
pub caller: Arc<User>,
pub participants: Vec<Arc<User>>,
pub initial_project: Option<proto::ParticipantProject>,
}
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>>,
),
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
_subscriptions: Vec<client::Subscription>,
}
impl Entity for ActiveCall {
type Event = room::Event;
}
impl ActiveCall {
fn new(
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut ModelContext<Self>,
) -> 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),
client.add_message_handler(cx.handle(), Self::handle_call_canceled),
],
client,
user_store,
}
}
async fn handle_incoming_call(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::IncomingCall>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<proto::Ack> {
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
let call = IncomingCall {
room_id: envelope.payload.room_id,
participants: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_users(envelope.payload.participant_user_ids, cx)
})
.await?,
caller: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_user(envelope.payload.caller_user_id, cx)
})
.await?,
initial_project: envelope.payload.initial_project,
};
this.update(&mut cx, |this, _| {
*this.incoming_call.0.borrow_mut() = Some(call);
});
Ok(proto::Ack {})
}
async fn handle_call_canceled(
this: ModelHandle<Self>,
_: TypedEnvelope<proto::CallCanceled>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _| {
*this.incoming_call.0.borrow_mut() = None;
});
Ok(())
}
pub fn global(cx: &AppContext) -> ModelHandle<Self> {
cx.global::<ModelHandle<Self>>().clone()
}
pub fn invite(
&mut self,
recipient_user_id: u64,
initial_project: Option<ModelHandle<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.clone();
let user_store = self.user_store.clone();
if !self.pending_invites.insert(recipient_user_id) {
return Task::ready(Err(anyhow!("user was already invited")));
}
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?;
} 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(())
};
let result = invite.await;
this.update(&mut cx, |this, cx| {
this.pending_invites.remove(&recipient_user_id);
cx.notify();
});
result
})
}
pub fn cancel_invite(
&mut self,
recipient_user_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let room_id = if let Some(room) = self.room() {
room.read(cx).id()
} else {
return Task::ready(Err(anyhow!("no active call")));
};
let client = self.client.clone();
cx.foreground().spawn(async move {
client
.request(proto::CancelCall {
room_id,
recipient_user_id,
})
.await?;
anyhow::Ok(())
})
}
pub fn incoming(&self) -> watch::Receiver<Option<IncomingCall>> {
self.incoming_call.1.clone()
}
pub fn accept_incoming(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.room.is_some() {
return Task::ready(Err(anyhow!("cannot join while on another call")));
}
let call = if let Some(call) = self.incoming_call.1.borrow().clone() {
call
} else {
return Task::ready(Err(anyhow!("no incoming call")));
};
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))
.await?;
Ok(())
})
}
pub fn decline_incoming(&mut self) -> Result<()> {
let call = self
.incoming_call
.0
.borrow_mut()
.take()
.ok_or_else(|| anyhow!("no incoming call"))?;
self.client.send(proto::DeclineCall {
room_id: call.room_id,
})?;
Ok(())
}
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
if let Some((room, _)) = self.room.take() {
room.update(cx, |room, cx| room.leave(cx))?;
cx.notify();
}
Ok(())
}
pub fn share_project(
&mut self,
project: ModelHandle<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
room.update(cx, |room, cx| room.share_project(project, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
}
}
pub fn set_location(
&mut self,
project: Option<&ModelHandle<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
self.location = project.map(|project| project.downgrade());
if let Some((room, _)) = self.room.as_ref() {
room.update(cx, |room, cx| room.set_location(project, cx))
} else {
Task::ready(Ok(()))
}
}
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).detach_and_log_err(cx);
}
cx.notify();
}),
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
];
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(()))
}
} 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

@@ -0,0 +1,56 @@
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;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ParticipantLocation {
SharedProject { project_id: u64 },
UnsharedProject,
External,
}
impl ParticipantLocation {
pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
match location.and_then(|l| l.variant) {
Some(proto::participant_location::Variant::SharedProject(project)) => {
Ok(Self::SharedProject {
project_id: project.id,
})
}
Some(proto::participant_location::Variant::UnsharedProject(_)) => {
Ok(Self::UnsharedProject)
}
Some(proto::participant_location::Variant::External(_)) => Ok(Self::External),
None => Err(anyhow!("participant location was not provided")),
}
}
}
#[derive(Clone, Default)]
pub struct LocalParticipant {
pub projects: Vec<proto::ParticipantProject>,
pub active_project: Option<WeakModelHandle<Project>>,
}
#[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()
}
}

760
crates/call/src/room.rs Normal file
View File

@@ -0,0 +1,760 @@
use crate::{
participant::{LocalParticipant, ParticipantLocation, RemoteParticipant, RemoteVideoTrack},
IncomingCall,
};
use anyhow::{anyhow, Result};
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::{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,
worktree_root_names: Vec<String>,
},
RemoteProjectUnshared {
project_id: u64,
},
Left,
}
pub struct Room {
id: u64,
live_kit: Option<LiveKitRoom>,
status: RoomStatus,
local_participant: LocalParticipant,
remote_participants: BTreeMap<PeerId, RemoteParticipant>,
pending_participants: Vec<Arc<User>>,
participant_user_ids: HashSet<u64>,
pending_call_count: usize,
leave_when_empty: bool,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
subscriptions: Vec<client::Subscription>,
pending_room_update: Option<Task<()>>,
}
impl Entity for Room {
type Event = Event;
fn release(&mut self, _: &mut MutableAppContext) {
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>,
) -> Self {
let mut client_status = client.status();
cx.spawn_weak(|this, mut cx| async move {
let is_connected = client_status
.next()
.await
.map_or(false, |s| s.is_connected());
// Even if we're initially connected, any future change of the status means we momentarily disconnected.
if !is_connected || client_status.next().await.is_some() {
if let Some(this) = this.upgrade(&cx) {
let _ = this.update(&mut cx, |this, cx| this.leave(cx));
}
}
})
.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(),
remote_participants: Default::default(),
pending_participants: Default::default(),
pending_call_count: 0,
subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)],
leave_when_empty: false,
pending_room_update: None,
client,
user_store,
}
}
pub(crate) fn create(
recipient_user_id: u64,
initial_project: Option<ModelHandle<Project>>,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut MutableAppContext,
) -> Task<Result<ModelHandle<Self>>> {
cx.spawn(|mut cx| async move {
let response = client.request(proto::CreateRoom {}).await?;
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
.update(&mut cx, |room, cx| {
room.share_project(initial_project.clone(), cx)
})
.await?;
Some(initial_project_id)
} else {
None
};
match room
.update(&mut cx, |room, cx| {
room.leave_when_empty = true;
room.call(recipient_user_id, initial_project_id, cx)
})
.await
{
Ok(()) => Ok(room),
Err(error) => Err(anyhow!("room creation failed: {:?}", error)),
}
})
}
pub(crate) fn join(
call: &IncomingCall,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
cx: &mut MutableAppContext,
) -> Task<Result<ModelHandle<Self>>> {
let room_id = call.room_id;
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,
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)?;
anyhow::Ok(())
})?;
Ok(room)
})
}
fn should_leave(&self) -> bool {
self.leave_when_empty
&& self.pending_room_update.is_none()
&& self.pending_participants.is_empty()
&& self.remote_participants.is_empty()
&& self.pending_call_count == 0
}
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
if self.status.is_offline() {
return Err(anyhow!("room is offline"));
}
cx.notify();
cx.emit(Event::Left);
self.status = RoomStatus::Offline;
self.remote_participants.clear();
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(())
}
pub fn id(&self) -> u64 {
self.id
}
pub fn status(&self) -> RoomStatus {
self.status
}
pub fn local_participant(&self) -> &LocalParticipant {
&self.local_participant
}
pub fn remote_participants(&self) -> &BTreeMap<PeerId, RemoteParticipant> {
&self.remote_participants
}
pub fn pending_participants(&self) -> &[Arc<User>] {
&self.pending_participants
}
pub fn contains_participant(&self, user_id: u64) -> bool {
self.participant_user_ids.contains(&user_id)
}
async fn handle_room_updated(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::RoomUpdated>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
let room = envelope
.payload
.room
.ok_or_else(|| anyhow!("invalid room"))?;
this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))
}
fn apply_room_update(
&mut self,
mut room: proto::Room,
cx: &mut ModelContext<Self>,
) -> Result<()> {
// Filter ourselves out from the room's participants.
let local_participant_ix = room
.participants
.iter()
.position(|participant| Some(participant.user_id) == self.client.user_id());
let local_participant = local_participant_ix.map(|ix| room.participants.swap_remove(ix));
let remote_participant_user_ids = room
.participants
.iter()
.map(|p| p.user_id)
.collect::<Vec<_>>();
let (remote_participants, pending_participants) =
self.user_store.update(cx, move |user_store, cx| {
(
user_store.get_users(remote_participant_user_ids, cx),
user_store.get_users(room.pending_participant_user_ids, cx),
)
});
self.pending_room_update = Some(cx.spawn(|this, mut cx| async move {
let (remote_participants, pending_participants) =
futures::join!(remote_participants, pending_participants);
this.update(&mut cx, |this, cx| {
this.participant_user_ids.clear();
if let Some(participant) = local_participant {
this.local_participant.projects = participant.projects;
} else {
this.local_participant.projects.clear();
}
if let Some(participants) = remote_participants.log_err() {
for (participant, user) in room.participants.into_iter().zip(participants) {
let peer_id = PeerId(participant.peer_id);
this.participant_user_ids.insert(participant.user_id);
let old_projects = this
.remote_participants
.get(&peer_id)
.into_iter()
.flat_map(|existing| &existing.projects)
.map(|project| project.id)
.collect::<HashSet<_>>();
let new_projects = participant
.projects
.iter()
.map(|project| project.id)
.collect::<HashSet<_>>();
for project in &participant.projects {
if !old_projects.contains(&project.id) {
cx.emit(Event::RemoteProjectShared {
owner: user.clone(),
project_id: project.id,
worktree_root_names: project.worktree_root_names.clone(),
});
}
}
for unshared_project_id in old_projects.difference(&new_projects) {
cx.emit(Event::RemoteProjectUnshared {
project_id: *unshared_project_id,
});
}
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| {
if this.participant_user_ids.contains(&participant.user.id) {
true
} else {
for project in &participant.projects {
cx.emit(Event::RemoteProjectUnshared {
project_id: project.id,
});
}
false
}
});
}
if let Some(pending_participants) = pending_participants.log_err() {
this.pending_participants = pending_participants;
for participant in &this.pending_participants {
this.participant_user_ids.insert(participant.id);
}
}
this.pending_room_update.take();
if this.should_leave() {
let _ = this.leave(cx);
}
this.check_invariants();
cx.notify();
});
}));
cx.notify();
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"))]
{
for participant in self.remote_participants.values() {
assert!(self.participant_user_ids.contains(&participant.user.id));
}
for participant in &self.pending_participants {
assert!(self.participant_user_ids.contains(&participant.id));
}
assert_eq!(
self.participant_user_ids.len(),
self.remote_participants.len() + self.pending_participants.len()
);
}
}
pub(crate) fn call(
&mut self,
recipient_user_id: u64,
initial_project_id: Option<u64>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
}
cx.notify();
let client = self.client.clone();
let room_id = self.id;
self.pending_call_count += 1;
cx.spawn(|this, mut cx| async move {
let result = client
.request(proto::Call {
room_id,
recipient_user_id,
initial_project_id,
})
.await;
this.update(&mut cx, |this, cx| {
this.pending_call_count -= 1;
if this.should_leave() {
this.leave(cx)?;
}
result
})?;
Ok(())
})
}
pub(crate) fn share_project(
&mut self,
project: ModelHandle<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some(project_id) = project.read(cx).remote_id() {
return Task::ready(Ok(project_id));
}
let request = self.client.request(proto::ShareProject {
room_id: self.id(),
worktrees: project
.read(cx)
.worktrees(cx)
.map(|worktree| {
let worktree = worktree.read(cx);
proto::WorktreeMetadata {
id: worktree.id().to_proto(),
root_name: worktree.root_name().into(),
visible: worktree.is_visible(),
abs_path: worktree.abs_path().as_os_str().as_bytes().to_vec(),
}
})
.collect(),
});
cx.spawn(|this, mut cx| async move {
let response = request.await?;
project.update(&mut cx, |project, cx| {
project
.shared(response.project_id, cx)
.detach_and_log_err(cx)
});
// If the user's location is in this project, it changes from UnsharedProject to SharedProject.
this.update(&mut cx, |this, cx| {
let active_project = this.local_participant.active_project.as_ref();
if active_project.map_or(false, |location| *location == project) {
this.set_location(Some(&project), cx)
} else {
Task::ready(Ok(()))
}
})
.await?;
Ok(response.project_id)
})
}
pub(crate) fn set_location(
&mut self,
project: Option<&ModelHandle<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
}
let client = self.client.clone();
let room_id = self.id;
let location = if let Some(project) = project {
self.local_participant.active_project = Some(project.downgrade());
if let Some(project_id) = project.read(cx).remote_id() {
proto::participant_location::Variant::SharedProject(
proto::participant_location::SharedProject { id: project_id },
)
} else {
proto::participant_location::Variant::UnsharedProject(
proto::participant_location::UnsharedProject {},
)
}
} else {
self.local_participant.active_project = None;
proto::participant_location::Variant::External(proto::participant_location::External {})
};
cx.notify();
cx.foreground().spawn(async move {
client
.request(proto::UpdateParticipantLocation {
room_id,
location: Some(proto::ParticipantLocation {
variant: Some(location),
}),
})
.await?;
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)]
pub enum RoomStatus {
Online,
Offline,
}
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).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).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,271 @@
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().ok().map(|v| v.to_string().into()),
os_name: platform.os_name().into(),
app_version: platform.app_version().ok().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

@@ -530,7 +530,7 @@ impl ChannelMessage {
) -> Result<Self> {
let sender = user_store
.update(cx, |user_store, cx| {
user_store.fetch_user(message.sender_id, cx)
user_store.get_user(message.sender_id, cx)
})
.await?;
Ok(ChannelMessage {

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,9 +56,14 @@ 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";
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
actions!(client, [Authenticate]);
@@ -74,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)]
@@ -141,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 {
@@ -248,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(),
@@ -312,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()
}
@@ -330,7 +356,7 @@ impl Client {
let reconnect_interval = state.reconnect_interval;
state._reconnect_task = Some(cx.spawn(|cx| async move {
let mut rng = StdRng::from_entropy();
let mut delay = Duration::from_millis(100);
let mut delay = INITIAL_RECONNECTION_DELAY;
while let Err(error) = this.authenticate_and_connect(true, &cx).await {
log::error!("failed to connect {}", error);
if matches!(*this.status().borrow(), Status::ConnectionError) {
@@ -351,7 +377,9 @@ impl Client {
}));
}
Status::SignedOut | Status::UpgradeRequired => {
self.telemetry.set_metrics_id(None);
self.telemetry.set_authenticated_user_info(None, false);
self.amplitude_telemetry
.set_authenticated_user_info(None, false);
state._reconnect_task.take();
}
_ => {}
@@ -434,6 +462,29 @@ impl Client {
}
}
pub fn add_request_handler<M, E, H, F>(
self: &Arc<Self>,
model: ModelHandle<E>,
handler: H,
) -> Subscription
where
M: RequestMessage,
E: Entity,
H: 'static
+ Send
+ Sync
+ Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
F: 'static + Future<Output = Result<M::Response>>,
{
self.add_message_handler(model, move |handle, envelope, this, cx| {
Self::respond_to_request(
envelope.receipt(),
handler(handle, envelope, this.clone(), cx),
this,
)
})
}
pub fn add_view_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
where
M: EntityMessage,
@@ -638,46 +689,104 @@ impl Client {
self.set_status(Status::Reconnecting, cx);
}
match self.establish_connection(&credentials, cx).await {
Ok(conn) => {
self.state.write().credentials = Some(credentials.clone());
if !read_from_keychain && IMPERSONATE_LOGIN.is_none() {
write_credentials_to_keychain(&credentials, cx).log_err();
}
self.set_connection(conn, cx).await;
Ok(())
}
Err(EstablishConnectionError::Unauthorized) => {
self.state.write().credentials.take();
if read_from_keychain {
cx.platform().delete_credentials(&ZED_SERVER_URL).log_err();
self.set_status(Status::SignedOut, cx);
self.authenticate_and_connect(false, cx).await
} else {
self.set_status(Status::ConnectionError, cx);
Err(EstablishConnectionError::Unauthorized)?
let mut timeout = cx.background().timer(CONNECTION_TIMEOUT).fuse();
futures::select_biased! {
connection = self.establish_connection(&credentials, cx).fuse() => {
match connection {
Ok(conn) => {
self.state.write().credentials = Some(credentials.clone());
if !read_from_keychain && IMPERSONATE_LOGIN.is_none() {
write_credentials_to_keychain(&credentials, cx).log_err();
}
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();
if read_from_keychain {
cx.platform().delete_credentials(&ZED_SERVER_URL).log_err();
self.set_status(Status::SignedOut, cx);
self.authenticate_and_connect(false, cx).await
} else {
self.set_status(Status::ConnectionError, cx);
Err(EstablishConnectionError::Unauthorized)?
}
}
Err(EstablishConnectionError::UpgradeRequired) => {
self.set_status(Status::UpgradeRequired, cx);
Err(EstablishConnectionError::UpgradeRequired)?
}
Err(error) => {
self.set_status(Status::ConnectionError, cx);
Err(error)?
}
}
}
Err(EstablishConnectionError::UpgradeRequired) => {
self.set_status(Status::UpgradeRequired, cx);
Err(EstablishConnectionError::UpgradeRequired)?
}
Err(error) => {
_ = &mut timeout => {
self.set_status(Status::ConnectionError, cx);
Err(error)?
Err(anyhow!("timed out trying to establish connection"))
}
}
}
async 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))
.await;
log::info!("set status to connected {}", connection_id);
self.set_status(Status::Connected { connection_id }, cx);
.add_connection(conn, move |duration| executor.timer(duration));
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();
@@ -775,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);
}
}
@@ -793,6 +906,8 @@ impl Client {
}
})
.detach();
Ok(())
}
fn authenticate(self: &Arc<Self>, cx: &AsyncAppContext) -> Task<Result<Credentials>> {
@@ -817,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",
@@ -831,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())
@@ -895,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
@@ -904,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();
@@ -974,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()?,
@@ -982,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);
@@ -1040,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()
}
}
@@ -1146,6 +1334,76 @@ mod tests {
assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token
}
#[gpui::test(iterations = 10)]
async fn test_connection_timeout(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
deterministic.forbid_parking();
let user_id = 5;
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let mut status = client.status();
// Time out when client tries to connect.
client.override_authenticate(move |cx| {
cx.foreground().spawn(async move {
Ok(Credentials {
user_id,
access_token: "token".into(),
})
})
});
client.override_establish_connection(|_, cx| {
cx.foreground().spawn(async move {
future::pending::<()>().await;
unreachable!()
})
});
let auth_and_connect = cx.spawn({
let client = client.clone();
|cx| async move { client.authenticate_and_connect(false, &cx).await }
});
deterministic.run_until_parked();
assert!(matches!(status.next().await, Some(Status::Connecting)));
deterministic.advance_clock(CONNECTION_TIMEOUT);
assert!(matches!(
status.next().await,
Some(Status::ConnectionError { .. })
));
auth_and_connect.await.unwrap_err();
// Allow the connection to be established.
let server = FakeServer::for_client(user_id, &client, cx).await;
assert!(matches!(
status.next().await,
Some(Status::Connected { .. })
));
// Disconnect client.
server.forbid_connections();
server.disconnect();
while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {}
// Time out when re-establishing the connection.
server.allow_connections();
client.override_establish_connection(|_, cx| {
cx.foreground().spawn(async move {
future::pending::<()>().await;
unreachable!()
})
});
deterministic.advance_clock(2 * INITIAL_RECONNECTION_DELAY);
assert!(matches!(
status.next().await,
Some(Status::Reconnecting { .. })
));
deterministic.advance_clock(CONNECTION_TIMEOUT);
assert!(matches!(
status.next().await,
Some(Status::ReconnectionError { .. })
));
}
#[gpui::test(iterations = 10)]
async fn test_authenticating_more_than_once(
cx: &mut TestAppContext,

View File

@@ -9,6 +9,8 @@ use isahc::Request;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Serialize;
use serde_json::json;
use settings::ReleaseChannel;
use std::{
io::Write,
mem,
@@ -23,7 +25,6 @@ use uuid::Uuid;
pub struct Telemetry {
http_client: Arc<dyn HttpClient>,
executor: Arc<Background>,
session_id: u128,
state: Mutex<TelemetryState>,
}
@@ -32,50 +33,63 @@ struct TelemetryState {
metrics_id: Option<Arc<str>>,
device_id: Option<Arc<str>>,
app_version: Option<Arc<str>>,
release_channel: Option<&'static 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>,
options: AmplitudeEventBatchOptions,
#[derive(Serialize, Debug)]
struct MixpanelEvent {
event: String,
properties: MixpanelEventProperties,
}
#[derive(Serialize)]
struct AmplitudeEventBatchOptions {
min_id_length: usize,
}
#[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>>,
event_id: usize,
session_id: u128,
#[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(rename = "OS Name")]
os_name: &'static str,
#[serde(rename = "OS Version")]
os_version: Option<Arc<str>>,
#[serde(rename = "Release Channel")]
release_channel: Option<&'static str>,
#[serde(rename = "App Version")]
app_version: Option<Arc<str>>,
#[serde(rename = "Signed In")]
signed_in: bool,
platform: &'static str,
}
#[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)]
@@ -93,33 +107,29 @@ const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30);
impl Telemetry {
pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
let platform = cx.platform();
let release_channel = if cx.has_global::<ReleaseChannel>() {
Some(cx.global::<ReleaseChannel>().name())
} else {
None
};
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()
.log_err()
.map(|v| v.to_string().into()),
os_version: platform.os_version().ok().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()),
app_version: platform.app_version().ok().map(|v| v.to_string().into()),
release_channel,
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();
@@ -139,30 +149,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);
@@ -176,35 +183,63 @@ impl Telemetry {
.detach();
}
pub fn set_metrics_id(&self, metrics_id: Option<String>) {
self.state.lock().metrics_id = metrics_id.map(|s| s.into());
pub fn set_authenticated_user_info(
self: &Arc<Self>,
metrics_id: Option<String>,
is_staff: bool,
) {
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) {
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(),
release_channel: state.release_channel,
app_version: state.app_version.clone(),
signed_in: state.metrics_id.is_some(),
platform: "Zed",
},
user_properties: None,
user_id: state.metrics_id.clone(),
device_id: state.device_id.clone(),
os_name: state.os_name,
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() {
@@ -224,11 +259,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(
@@ -237,23 +272,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,
options: AmplitudeEventBatchOptions { min_id_length: 1 },
};
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

@@ -82,11 +82,21 @@ impl FakeServer {
let (client_conn, server_conn, _) = Connection::in_memory(cx.background());
let (connection_id, io, incoming) =
peer.add_test_connection(server_conn, cx.background()).await;
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)
})
}
@@ -101,10 +111,12 @@ impl FakeServer {
}
pub fn disconnect(&self) {
self.peer.disconnect(self.connection_id());
let mut state = self.state.lock();
state.connection_id.take();
state.incoming.take();
if self.state.lock().connection_id.is_some() {
self.peer.disconnect(self.connection_id());
let mut state = self.state.lock();
state.connection_id.take();
state.incoming.take();
}
}
pub fn auth_count(&self) -> usize {
@@ -157,6 +169,7 @@ impl FakeServer {
.receipt(),
GetPrivateUserInfoResponse {
metrics_id: "the-metrics-id".into(),
staff: false,
},
)
.await;

View File

@@ -1,14 +1,14 @@
use super::{http::HttpClient, proto, Client, Status, TypedEnvelope};
use anyhow::{anyhow, Context, Result};
use collections::{hash_map::Entry, BTreeSet, HashMap, HashSet};
use collections::{hash_map::Entry, HashMap, HashSet};
use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
use postage::{prelude::Stream, sink::Sink, watch};
use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse};
use std::sync::{Arc, Weak};
use util::TryFutureExt as _;
#[derive(Debug)]
#[derive(Default, Debug)]
pub struct User {
pub id: u64,
pub github_login: String,
@@ -39,14 +39,7 @@ impl Eq for User {}
pub struct Contact {
pub user: Arc<User>,
pub online: bool,
pub projects: Vec<ProjectMetadata>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ProjectMetadata {
pub id: u64,
pub visible_worktree_root_names: Vec<String>,
pub guests: BTreeSet<Arc<User>>,
pub busy: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -138,18 +131,36 @@ impl UserStore {
}),
_maintain_current_user: cx.spawn_weak(|this, mut cx| async move {
let mut status = client.status();
while let Some(status) = status.recv().await {
while let Some(status) = status.next().await {
match status {
Status::Connected { .. } => {
if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) {
let fetch_user = this
.update(&mut cx, |this, cx| this.fetch_user(user_id, cx))
.update(&mut cx, |this, cx| this.get_user(user_id, cx))
.log_err();
let fetch_metrics_id =
client.request(proto::GetPrivateUserInfo {}).log_err();
let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
client.telemetry.set_metrics_id(info.map(|i| i.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();
}
}
@@ -237,7 +248,6 @@ impl UserStore {
let mut user_ids = HashSet::default();
for contact in &message.contacts {
user_ids.insert(contact.user_id);
user_ids.extend(contact.projects.iter().flat_map(|w| &w.guests).copied());
}
user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id));
user_ids.extend(message.outgoing_requests.iter());
@@ -261,9 +271,7 @@ impl UserStore {
for request in message.incoming_requests {
incoming_requests.push({
let user = this
.update(&mut cx, |this, cx| {
this.fetch_user(request.requester_id, cx)
})
.update(&mut cx, |this, cx| this.get_user(request.requester_id, cx))
.await?;
(user, request.should_notify)
});
@@ -272,7 +280,7 @@ impl UserStore {
let mut outgoing_requests = Vec::new();
for requested_user_id in message.outgoing_requests {
outgoing_requests.push(
this.update(&mut cx, |this, cx| this.fetch_user(requested_user_id, cx))
this.update(&mut cx, |this, cx| this.get_user(requested_user_id, cx))
.await?,
);
}
@@ -497,7 +505,7 @@ impl UserStore {
.unbounded_send(UpdateContacts::Clear(tx))
.unwrap();
async move {
rx.recv().await;
rx.next().await;
}
}
@@ -507,25 +515,43 @@ impl UserStore {
.unbounded_send(UpdateContacts::Wait(tx))
.unwrap();
async move {
rx.recv().await;
rx.next().await;
}
}
pub fn get_users(
&mut self,
mut user_ids: Vec<u64>,
user_ids: Vec<u64>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
user_ids.retain(|id| !self.users.contains_key(id));
if user_ids.is_empty() {
Task::ready(Ok(()))
} else {
let load = self.load_users(proto::GetUsers { user_ids }, cx);
cx.foreground().spawn(async move {
load.await?;
Ok(())
) -> Task<Result<Vec<Arc<User>>>> {
let mut user_ids_to_fetch = user_ids.clone();
user_ids_to_fetch.retain(|id| !self.users.contains_key(id));
cx.spawn(|this, mut cx| async move {
if !user_ids_to_fetch.is_empty() {
this.update(&mut cx, |this, cx| {
this.load_users(
proto::GetUsers {
user_ids: user_ids_to_fetch,
},
cx,
)
})
.await?;
}
this.read_with(&cx, |this, _| {
user_ids
.iter()
.map(|user_id| {
this.users
.get(user_id)
.cloned()
.ok_or_else(|| anyhow!("user {} not found", user_id))
})
.collect()
})
}
})
}
pub fn fuzzy_search_users(
@@ -536,7 +562,7 @@ impl UserStore {
self.load_users(proto::FuzzySearchUsers { query }, cx)
}
pub fn fetch_user(
pub fn get_user(
&mut self,
user_id: u64,
cx: &mut ModelContext<Self>,
@@ -616,39 +642,15 @@ impl Contact {
) -> Result<Self> {
let user = user_store
.update(cx, |user_store, cx| {
user_store.fetch_user(contact.user_id, cx)
user_store.get_user(contact.user_id, cx)
})
.await?;
let mut projects = Vec::new();
for project in contact.projects {
let mut guests = BTreeSet::new();
for participant_id in project.guests {
guests.insert(
user_store
.update(cx, |user_store, cx| {
user_store.fetch_user(participant_id, cx)
})
.await?,
);
}
projects.push(ProjectMetadata {
id: project.id,
visible_worktree_root_names: project.visible_worktree_root_names.clone(),
guests,
});
}
Ok(Self {
user,
online: contact.online,
projects,
busy: contact.busy,
})
}
pub fn non_empty_projects(&self) -> impl Iterator<Item = &ProjectMetadata> {
self.projects
.iter()
.filter(|project| !project.visible_worktree_root_names.is_empty())
}
}
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {

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

@@ -1,9 +1,9 @@
[package]
authors = ["Nathan Sobo <nathan@warp.dev>"]
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,6 +14,7 @@ required-features = ["seed-support"]
[dependencies]
collections = { path = "../collections" }
live_kit_server = { path = "../live_kit_server" }
rpc = { path = "../rpc" }
util = { path = "../util" }
@@ -55,21 +56,27 @@ features = ["runtime-tokio-rustls", "postgres", "time", "uuid"]
[dev-dependencies]
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
call = { path = "../call", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
fs = { path = "../fs", features = ["test-support"] }
git = { path = "../git", features = ["test-support"] }
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
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"] }
unindent = "0.1"
[features]
seed-support = ["clap", "lipsum", "reqwest"]

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

@@ -84,7 +84,23 @@ async fn main() {
},
)
.await
.expect("failed to insert user"),
.expect("failed to insert user")
.user_id,
);
} else if admin {
zed_user_ids.push(
db.create_user(
&format!("{}@zed.dev", github_user.login),
admin,
db::NewUserParams {
github_login: github_user.login,
github_user_id: github_user.id,
invite_count: 5,
},
)
.await
.expect("failed to insert user")
.user_id,
);
}
}

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
@@ -1098,10 +1153,7 @@ impl Db for PostgresDb {
.bind(user_id)
.fetch(&self.pool);
let mut contacts = vec![Contact::Accepted {
user_id,
should_notify: false,
}];
let mut contacts = Vec::new();
while let Some(row) = rows.next().await {
let (user_id_a, user_id_b, a_to_b, accepted, should_notify) = row?;
@@ -1723,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)]
@@ -1766,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 {
@@ -1958,7 +2009,7 @@ mod test {
&self,
_invite: &Invite,
_user: NewUserParams,
) -> Result<NewUserResult> {
) -> Result<Option<NewUserResult>> {
unimplemented!()
}
@@ -2080,10 +2131,7 @@ mod test {
async fn get_contacts(&self, id: UserId) -> Result<Vec<Contact>> {
self.background.simulate_random_delay().await;
let mut contacts = vec![Contact::Accepted {
user_id: id,
should_notify: false,
}];
let mut contacts = Vec::new();
for contact in self.contacts.lock().iter() {
if contact.requester_id == id {
@@ -2436,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

@@ -666,13 +666,7 @@ async fn test_add_contacts() {
let user_3 = user_ids[2];
// User starts with no contacts
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
vec![Contact::Accepted {
user_id: user_1,
should_notify: false
}],
);
assert_eq!(db.get_contacts(user_1).await.unwrap(), &[]);
// User requests a contact. Both users see the pending request.
db.send_contact_request(user_1, user_2).await.unwrap();
@@ -680,26 +674,14 @@ async fn test_add_contacts() {
assert!(!db.has_contact(user_2, user_1).await.unwrap());
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Outgoing { user_id: user_2 }
],
&[Contact::Outgoing { user_id: user_2 }],
);
assert_eq!(
db.get_contacts(user_2).await.unwrap(),
&[
Contact::Incoming {
user_id: user_1,
should_notify: true
},
Contact::Accepted {
user_id: user_2,
should_notify: false
},
]
&[Contact::Incoming {
user_id: user_1,
should_notify: true
}]
);
// User 2 dismisses the contact request notification without accepting or rejecting.
@@ -712,16 +694,10 @@ async fn test_add_contacts() {
.unwrap();
assert_eq!(
db.get_contacts(user_2).await.unwrap(),
&[
Contact::Incoming {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_2,
should_notify: false
},
]
&[Contact::Incoming {
user_id: user_1,
should_notify: false
}]
);
// User can't accept their own contact request
@@ -735,31 +711,19 @@ async fn test_add_contacts() {
.unwrap();
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_2,
should_notify: true
}
],
&[Contact::Accepted {
user_id: user_2,
should_notify: true
}],
);
assert!(db.has_contact(user_1, user_2).await.unwrap());
assert!(db.has_contact(user_2, user_1).await.unwrap());
assert_eq!(
db.get_contacts(user_2).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false,
},
Contact::Accepted {
user_id: user_2,
should_notify: false,
},
]
&[Contact::Accepted {
user_id: user_1,
should_notify: false,
}]
);
// Users cannot re-request existing contacts.
@@ -772,16 +736,10 @@ async fn test_add_contacts() {
.unwrap_err();
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_2,
should_notify: true,
},
]
&[Contact::Accepted {
user_id: user_2,
should_notify: true,
}]
);
// Users can dismiss notifications of other users accepting their requests.
@@ -790,16 +748,10 @@ async fn test_add_contacts() {
.unwrap();
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_2,
should_notify: false,
},
]
&[Contact::Accepted {
user_id: user_2,
should_notify: false,
}]
);
// Users send each other concurrent contact requests and
@@ -809,10 +761,6 @@ async fn test_add_contacts() {
assert_eq!(
db.get_contacts(user_1).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_2,
should_notify: false,
@@ -820,21 +768,15 @@ async fn test_add_contacts() {
Contact::Accepted {
user_id: user_3,
should_notify: false
},
}
]
);
assert_eq!(
db.get_contacts(user_3).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_3,
should_notify: false
}
],
&[Contact::Accepted {
user_id: user_1,
should_notify: false
}],
);
// User declines a contact request. Both users see that it is gone.
@@ -846,29 +788,17 @@ async fn test_add_contacts() {
assert!(!db.has_contact(user_3, user_2).await.unwrap());
assert_eq!(
db.get_contacts(user_2).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_2,
should_notify: false
}
]
&[Contact::Accepted {
user_id: user_1,
should_notify: false
}]
);
assert_eq!(
db.get_contacts(user_3).await.unwrap(),
&[
Contact::Accepted {
user_id: user_1,
should_notify: false
},
Contact::Accepted {
user_id: user_3,
should_notify: false
}
],
&[Contact::Accepted {
user_id: user_1,
should_notify: false
}],
);
}
}
@@ -922,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);
@@ -930,29 +861,17 @@ async fn test_invite_codes() {
assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id);
assert_eq!(
db.get_contacts(user1).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user2,
should_notify: true
}
]
[Contact::Accepted {
user_id: user2,
should_notify: true
}]
);
assert_eq!(
db.get_contacts(user2).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user2,
should_notify: false
}
]
[Contact::Accepted {
user_id: user1,
should_notify: false
}]
);
assert_eq!(
db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
@@ -979,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);
@@ -987,10 +907,6 @@ async fn test_invite_codes() {
assert_eq!(
db.get_contacts(user1).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user2,
should_notify: true
@@ -1003,16 +919,10 @@ async fn test_invite_codes() {
);
assert_eq!(
db.get_contacts(user3).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user3,
should_notify: false
},
]
[Contact::Accepted {
user_id: user1,
should_notify: false
}]
);
assert_eq!(
db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
@@ -1046,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();
@@ -1053,10 +964,6 @@ async fn test_invite_codes() {
assert_eq!(
db.get_contacts(user1).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user2,
should_notify: true
@@ -1073,16 +980,10 @@ async fn test_invite_codes() {
);
assert_eq!(
db.get_contacts(user4).await.unwrap(),
[
Contact::Accepted {
user_id: user1,
should_notify: false
},
Contact::Accepted {
user_id: user4,
should_notify: false
},
]
[Contact::Accepted {
user_id: user1,
should_notify: false
}]
);
assert_eq!(
db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,
@@ -1124,6 +1025,7 @@ async fn test_signups() {
mac_count: 8,
linux_count: 4,
windows_count: 2,
unknown_count: 0,
}
);
@@ -1176,6 +1078,7 @@ async fn test_signups() {
mac_count: 5,
linux_count: 2,
windows_count: 1,
unknown_count: 0,
}
);
@@ -1199,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());
@@ -1208,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(

File diff suppressed because it is too large Load Diff

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");
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
[package]
name = "collab_ui"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/collab_ui.rs"
doctest = false
[features]
test-support = [
"call/test-support",
"client/test-support",
"collections/test-support",
"editor/test-support",
"gpui/test-support",
"project/test-support",
"settings/test-support",
"util/test-support",
"workspace/test-support",
]
[dependencies]
call = { path = "../call" }
client = { path = "../client" }
clock = { path = "../clock" }
collections = { path = "../collections" }
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
menu = { path = "../menu" }
picker = { path = "../picker" }
project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow = "1.0"
futures = "0.3"
log = "0.4"
postage = { version = "0.4.1", features = ["futures-traits"] }
serde = { version = "1.0", features = ["derive", "rc"] }
[dev-dependencies]
call = { path = "../call", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

View File

@@ -0,0 +1,620 @@
use crate::{contact_notification::ContactNotification, contacts_popover};
use call::{ActiveCall, ParticipantLocation};
use client::{Authenticate, ContactEventKind, PeerId, User, UserStore};
use clock::ReplicaId;
use contacts_popover::ContactsPopover;
use gpui::{
actions,
color::Color,
elements::*,
geometry::{rect::RectF, vector::vec2f, PathBuilder},
json::{self, ToJson},
Border, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
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, 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);
}
pub struct CollabTitlebarItem {
workspace: WeakViewHandle<Workspace>,
user_store: ModelHandle<UserStore>,
contacts_popover: Option<ViewHandle<ContactsPopover>>,
_subscriptions: Vec<Subscription>,
}
impl Entity for CollabTitlebarItem {
type Event = ();
}
impl View for CollabTitlebarItem {
fn ui_name() -> &'static str {
"CollabTitlebarItem"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let workspace = if let Some(workspace) = self.workspace.upgrade(cx) {
workspace
} else {
return Empty::new().boxed();
};
let theme = cx.global::<Settings>().theme.clone();
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()
{
container.add_child(self.render_toggle_contacts_button(&theme, cx));
} else {
container.add_child(self.render_share_button(&theme, cx));
}
}
container.add_children(self.render_collaborators(&workspace, &theme, cx));
container.add_children(self.render_current_user(&workspace, &theme, cx));
container.add_children(self.render_connection_status(&workspace, cx));
container.boxed()
}
}
impl CollabTitlebarItem {
pub fn new(
workspace: &ViewHandle<Workspace>,
user_store: &ModelHandle<UserStore>,
cx: &mut ViewContext<Self>,
) -> Self {
let active_call = ActiveCall::global(cx);
let mut subscriptions = Vec::new();
subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify()));
subscriptions.push(cx.observe(&active_call, |_, _, cx| cx.notify()));
subscriptions.push(cx.observe_window_activation(|this, active, cx| {
this.window_activation_changed(active, cx)
}));
subscriptions.push(cx.observe(user_store, |_, _, cx| cx.notify()));
subscriptions.push(
cx.subscribe(user_store, move |this, user_store, event, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
if let client::Event::Contact { user, kind } = event {
if let ContactEventKind::Requested | ContactEventKind::Accepted = kind {
workspace.show_notification(user.id as usize, cx, |cx| {
cx.add_view(|cx| {
ContactNotification::new(
user.clone(),
*kind,
user_store,
cx,
)
})
})
}
}
});
}
}),
);
Self {
workspace: workspace.downgrade(),
user_store: user_store.clone(),
contacts_popover: None,
_subscriptions: subscriptions,
}
}
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
if let Some(workspace) = self.workspace.upgrade(cx) {
let project = if active {
Some(workspace.read(cx).project().clone())
} else {
None
};
ActiveCall::global(cx)
.update(cx, |call, cx| call.set_location(project.as_ref(), cx))
.detach_and_log_err(cx);
}
}
fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
if let Some(workspace) = self.workspace.upgrade(cx) {
let active_call = ActiveCall::global(cx);
let project = workspace.read(cx).project().clone();
active_call
.update(cx, |call, cx| call.share_project(project, cx))
.detach_and_log_err(cx);
}
}
pub fn toggle_contacts_popover(
&mut self,
_: &ToggleCollaborationMenu,
cx: &mut ViewContext<Self>,
) {
match self.contacts_popover.take() {
Some(_) => {}
None => {
if let Some(workspace) = self.workspace.upgrade(cx) {
let project = workspace.read(cx).project().clone();
let user_store = workspace.read(cx).user_store().clone();
let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx));
cx.subscribe(&view, |this, _, event, cx| {
match event {
contacts_popover::Event::Dismissed => {
this.contacts_popover = None;
}
}
cx.notify();
})
.detach();
self.contacts_popover = Some(view);
}
}
}
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,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let titlebar = &theme.workspace.titlebar;
let badge = if self
.user_store
.read(cx)
.incoming_contact_requests()
.is_empty()
{
None
} else {
Some(
Empty::new()
.collapsed()
.contained()
.with_style(titlebar.toggle_contacts_badge)
.contained()
.with_margin_left(titlebar.toggle_contacts_button.default.icon_width)
.with_margin_top(titlebar.toggle_contacts_button.default.icon_width)
.aligned()
.boxed(),
)
};
Stack::new()
.with_child(
MouseEventHandler::<ToggleCollaborationMenu>::new(0, cx, |state, _| {
let style = titlebar
.toggle_contacts_button
.style_for(state, self.contacts_popover.is_some());
Svg::new("icons/plus_8.svg")
.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(ToggleCollaborationMenu);
})
.aligned()
.boxed(),
)
.with_children(badge)
.with_children(self.contacts_popover.as_ref().map(|popover| {
Overlay::new(
ChildView::new(popover, cx)
.contained()
.with_margin_top(titlebar.height)
.with_margin_left(titlebar.toggle_contacts_button.default.button_width)
.with_margin_right(-titlebar.toggle_contacts_button.default.button_width)
.boxed(),
)
.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 {}
let titlebar = &theme.workspace.titlebar;
MouseEventHandler::<Share>::new(0, cx, |state, _| {
let style = titlebar.share_button.style_for(state, false);
Label::new("Share".into(), style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(ShareProject))
.with_tooltip::<Share, _>(
0,
"Share project with call participants".into(),
None,
theme.tooltip.clone(),
cx,
)
.aligned()
.boxed()
}
fn render_collaborators(
&self,
workspace: &ViewHandle<Workspace>,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> Vec<ElementBox> {
let active_call = ActiveCall::global(cx);
if let Some(room) = active_call.read(cx).room().cloned() {
let project = workspace.read(cx).project().read(cx);
let mut participants = room
.read(cx)
.remote_participants()
.iter()
.map(|(peer_id, collaborator)| (*peer_id, collaborator.clone()))
.collect::<Vec<_>>();
participants
.sort_by_key(|(peer_id, _)| Some(project.collaborators().get(peer_id)?.replica_id));
participants
.into_iter()
.filter_map(|(peer_id, participant)| {
let project = workspace.read(cx).project().read(cx);
let replica_id = project
.collaborators()
.get(&peer_id)
.map(|collaborator| collaborator.replica_id);
let user = participant.user.clone();
Some(self.render_avatar(
&user,
replica_id,
Some((peer_id, &user.github_login, participant.location)),
workspace,
theme,
cx,
))
})
.collect()
} else {
Default::default()
}
}
fn render_current_user(
&self,
workspace: &ViewHandle<Workspace>,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> Option<ElementBox> {
let user = workspace.read(cx).user_store().read(cx).current_user();
let replica_id = workspace.read(cx).project().read(cx).replica_id();
let status = *workspace.read(cx).client().status().borrow();
if let Some(user) = user {
Some(self.render_avatar(&user, Some(replica_id), None, workspace, theme, cx))
} else if matches!(status, client::Status::UpgradeRequired) {
None
} else {
Some(
MouseEventHandler::<Authenticate>::new(0, cx, |state, _| {
let style = theme
.workspace
.titlebar
.sign_in_prompt
.style_for(state, false);
Label::new("Sign in".to_string(), style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate))
.with_cursor_style(CursorStyle::PointingHand)
.aligned()
.boxed(),
)
}
}
fn render_avatar(
&self,
user: &User,
replica_id: Option<ReplicaId>,
peer: Option<(PeerId, &str, ParticipantLocation)>,
workspace: &ViewHandle<Workspace>,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let is_followed = peer.map_or(false, |(peer_id, _, _)| {
workspace.read(cx).is_following(peer_id)
});
let mut avatar_style;
if let Some((_, _, location)) = peer.as_ref() {
if let ParticipantLocation::SharedProject { project_id } = *location {
if Some(project_id) == workspace.read(cx).project().read(cx).remote_id() {
avatar_style = theme.workspace.titlebar.avatar;
} else {
avatar_style = theme.workspace.titlebar.inactive_avatar;
}
} else {
avatar_style = theme.workspace.titlebar.inactive_avatar;
}
} else {
avatar_style = theme.workspace.titlebar.avatar;
}
let mut replica_color = None;
if let Some(replica_id) = replica_id {
let color = theme.editor.replica_selection_style(replica_id).cursor;
replica_color = Some(color);
if is_followed {
avatar_style.border = Border::all(1.0, color);
}
}
let content = Stack::new()
.with_children(user.avatar.as_ref().map(|avatar| {
Image::new(avatar.clone())
.with_style(avatar_style)
.constrained()
.with_width(theme.workspace.titlebar.avatar_width)
.aligned()
.boxed()
}))
.with_children(replica_color.map(|replica_color| {
AvatarRibbon::new(replica_color)
.constrained()
.with_width(theme.workspace.titlebar.avatar_ribbon.width)
.with_height(theme.workspace.titlebar.avatar_ribbon.height)
.aligned()
.bottom()
.boxed()
}))
.constrained()
.with_width(theme.workspace.titlebar.avatar_width)
.contained()
.with_margin_left(theme.workspace.titlebar.avatar_margin)
.boxed();
if let Some((peer_id, peer_github_login, location)) = peer {
if let Some(replica_id) = replica_id {
MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleFollow(peer_id))
})
.with_tooltip::<ToggleFollow, _>(
peer_id.0 as usize,
if is_followed {
format!("Unfollow {}", peer_github_login)
} else {
format!("Follow {}", peer_github_login)
},
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.boxed()
} else if let ParticipantLocation::SharedProject { project_id } = location {
let user_id = user.id;
MouseEventHandler::<JoinProject>::new(peer_id.0 as usize, cx, move |_, _| content)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(JoinProject {
project_id,
follow_user_id: user_id,
})
})
.with_tooltip::<JoinProject, _>(
peer_id.0 as usize,
format!("Follow {} into external project", peer_github_login),
Some(Box::new(FollowNextCollaborator)),
theme.tooltip.clone(),
cx,
)
.boxed()
} else {
content
}
} else {
content
}
}
fn render_connection_status(
&self,
workspace: &ViewHandle<Workspace>,
cx: &mut RenderContext<Self>,
) -> Option<ElementBox> {
let theme = &cx.global::<Settings>().theme;
match &*workspace.read(cx).client().status().borrow() {
client::Status::ConnectionError
| client::Status::ConnectionLost
| client::Status::Reauthenticating { .. }
| client::Status::Reconnecting { .. }
| client::Status::ReconnectionError { .. } => Some(
Container::new(
Align::new(
ConstrainedBox::new(
Svg::new("icons/cloud_slash_12.svg")
.with_color(theme.workspace.titlebar.offline_icon.color)
.boxed(),
)
.with_width(theme.workspace.titlebar.offline_icon.width)
.boxed(),
)
.boxed(),
)
.with_style(theme.workspace.titlebar.offline_icon.container)
.boxed(),
),
client::Status::UpgradeRequired => Some(
Label::new(
"Please update Zed to collaborate".to_string(),
theme.workspace.titlebar.outdated_warning.text.clone(),
)
.contained()
.with_style(theme.workspace.titlebar.outdated_warning.container)
.aligned()
.boxed(),
),
_ => None,
}
}
}
pub struct AvatarRibbon {
color: Color,
}
impl AvatarRibbon {
pub fn new(color: Color) -> AvatarRibbon {
AvatarRibbon { color }
}
}
impl Element for AvatarRibbon {
type LayoutState = ();
type PaintState = ();
fn layout(
&mut self,
constraint: gpui::SizeConstraint,
_: &mut gpui::LayoutContext,
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
(constraint.max, ())
}
fn paint(
&mut self,
bounds: gpui::geometry::rect::RectF,
_: gpui::geometry::rect::RectF,
_: &mut Self::LayoutState,
cx: &mut gpui::PaintContext,
) -> Self::PaintState {
let mut path = PathBuilder::new();
path.reset(bounds.lower_left());
path.curve_to(
bounds.origin() + vec2f(bounds.height(), 0.),
bounds.origin(),
);
path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
path.curve_to(bounds.lower_right(), bounds.upper_right());
path.line_to(bounds.lower_left());
cx.scene.push_path(path.build(self.color, None));
}
fn rect_for_text_range(
&self,
_: Range<usize>,
_: RectF,
_: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &gpui::MeasurementContext,
) -> Option<RectF> {
None
}
fn debug(
&self,
bounds: gpui::geometry::rect::RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
_: &gpui::DebugContext,
) -> gpui::json::Value {
json::json!({
"type": "AvatarRibbon",
"bounds": bounds.to_json(),
"color": self.color.to_json(),
})
}
}

View File

@@ -0,0 +1,97 @@
mod collab_titlebar_item;
mod contact_finder;
mod contact_list;
mod contact_notification;
mod contacts_popover;
mod incoming_call_notification;
mod notifications;
mod project_shared_notification;
use call::ActiveCall;
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleCollaborationMenu};
use gpui::MutableAppContext;
use project::Project;
use std::sync::Arc;
use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
collab_titlebar_item::init(cx);
contact_notification::init(cx);
contact_list::init(cx);
contact_finder::init(cx);
contacts_popover::init(cx);
incoming_call_notification::init(cx);
project_shared_notification::init(cx);
cx.add_global_action(move |action: &JoinProject, cx| {
let project_id = action.project_id;
let follow_user_id = action.follow_user_id;
let app_state = app_state.clone();
cx.spawn(|mut cx| async move {
let existing_workspace = cx.update(|cx| {
cx.window_ids()
.filter_map(|window_id| cx.root_view::<Workspace>(window_id))
.find(|workspace| {
workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
})
});
let workspace = if let Some(existing_workspace) = existing_workspace {
existing_workspace
} else {
let project = Project::remote(
project_id,
app_state.client.clone(),
app_state.user_store.clone(),
app_state.project_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
cx.clone(),
)
.await?;
let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
let mut workspace = Workspace::new(project, app_state.default_item_factory, cx);
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
workspace
});
workspace
};
cx.activate_window(workspace.window_id());
cx.platform().activate(true);
workspace.update(&mut cx, |workspace, cx| {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
let follow_peer_id = room
.read(cx)
.remote_participants()
.iter()
.find(|(_, participant)| participant.user.id == follow_user_id)
.map(|(peer_id, _)| *peer_id)
.or_else(|| {
// If we couldn't follow the given user, follow the host instead.
let collaborator = workspace
.project()
.read(cx)
.collaborators()
.values()
.find(|collaborator| collaborator.replica_id == 0)?;
Some(collaborator.peer_id)
});
if let Some(follow_peer_id) = follow_peer_id {
if !workspace.is_following(follow_peer_id) {
workspace
.toggle_follow(&ToggleFollow(follow_peer_id), cx)
.map(|follow| follow.detach_and_log_err(cx));
}
}
}
});
anyhow::Ok(())
})
.detach_and_log_err(cx);
});
}

View File

@@ -1,21 +1,15 @@
use client::{ContactRequestStatus, User, UserStore};
use gpui::{
actions, elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext,
RenderContext, Task, View, ViewContext, ViewHandle,
elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext, RenderContext,
Task, View, ViewContext, ViewHandle,
};
use picker::{Picker, PickerDelegate};
use settings::Settings;
use std::sync::Arc;
use util::TryFutureExt;
use workspace::Workspace;
use crate::render_icon_button;
actions!(contact_finder, [Toggle]);
pub fn init(cx: &mut MutableAppContext) {
Picker::<ContactFinder>::init(cx);
cx.add_action(ContactFinder::toggle);
}
pub struct ContactFinder {
@@ -38,11 +32,11 @@ impl View for ContactFinder {
"ContactFinder"
}
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
ChildView::new(self.picker.clone()).boxed()
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
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);
}
@@ -107,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 {
@@ -117,18 +111,21 @@ impl PickerDelegate for ContactFinder {
let icon_path = match request_status {
ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
"icons/check_8.svg"
}
ContactRequestStatus::RequestSent | ContactRequestStatus::RequestAccepted => {
"icons/x_mark_8.svg"
Some("icons/check_8.svg")
}
ContactRequestStatus::RequestSent => Some("icons/x_mark_8.svg"),
ContactRequestStatus::RequestAccepted => None,
};
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
&theme.contact_finder.disabled_contact_button
} else {
&theme.contact_finder.contact_button
};
let style = theme.picker.item.style_for(mouse_state, selected);
let style = theme
.contact_finder
.picker
.item
.style_for(mouse_state, selected);
Flex::row()
.with_children(user.avatar.clone().map(|avatar| {
Image::new(avatar)
@@ -145,12 +142,21 @@ impl PickerDelegate for ContactFinder {
.left()
.boxed(),
)
.with_child(
render_icon_button(button_style, icon_path)
.with_children(icon_path.map(|icon_path| {
Svg::new(icon_path)
.with_color(button_style.color)
.constrained()
.with_width(button_style.icon_width)
.aligned()
.contained()
.with_style(button_style.container)
.constrained()
.with_width(button_style.button_width)
.with_height(button_style.button_width)
.aligned()
.flex_float()
.boxed(),
)
.boxed()
}))
.contained()
.with_style(style.container)
.constrained()
@@ -160,34 +166,16 @@ impl PickerDelegate for ContactFinder {
}
impl ContactFinder {
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |workspace, cx| {
let finder = cx.add_view(|cx| Self::new(workspace.user_store().clone(), cx));
cx.subscribe(&finder, Self::on_event).detach();
finder
});
}
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
let this = cx.weak_handle();
Self {
picker: cx.add_view(|cx| Picker::new(this, cx)),
picker: cx.add_view(|cx| {
Picker::new(this, cx)
.with_theme(|cx| &cx.global::<Settings>().theme.contact_finder.picker)
}),
potential_contacts: Arc::from([]),
user_store,
selected_index: 0,
}
}
fn on_event(
workspace: &mut Workspace,
_: ViewHandle<Self>,
event: &Event,
cx: &mut ViewContext<Workspace>,
) {
match event {
Event::Dismissed => {
workspace.dismiss_modal(cx);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -49,10 +49,7 @@ impl View for ContactNotification {
self.user.clone(),
"wants to add you as a contact",
Some("They won't know if you decline."),
RespondToContactRequest {
user_id: self.user.id,
accept: false,
},
Dismiss(self.user.id),
vec![
(
"Decline",

View File

@@ -0,0 +1,171 @@
use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleCollaborationMenu};
use client::UserStore;
use gpui::{
actions, elements::*, ClipboardItem, CursorStyle, Entity, ModelHandle, MouseButton,
MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
};
use project::Project;
use settings::Settings;
actions!(contacts_popover, [ToggleContactFinder]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContactsPopover::toggle_contact_finder);
}
pub enum Event {
Dismissed,
}
enum Child {
ContactList(ViewHandle<ContactList>),
ContactFinder(ViewHandle<ContactFinder>),
}
pub struct ContactsPopover {
child: Child,
project: ModelHandle<Project>,
user_store: ModelHandle<UserStore>,
_subscription: Option<gpui::Subscription>,
}
impl ContactsPopover {
pub fn new(
project: ModelHandle<Project>,
user_store: ModelHandle<UserStore>,
cx: &mut ViewContext<Self>,
) -> Self {
let mut this = Self {
child: Child::ContactList(
cx.add_view(|cx| ContactList::new(project.clone(), user_store.clone(), cx)),
),
project,
user_store,
_subscription: None,
};
this.show_contact_list(cx);
this
}
fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext<Self>) {
match &self.child {
Child::ContactList(_) => self.show_contact_finder(cx),
Child::ContactFinder(_) => self.show_contact_list(cx),
}
}
fn show_contact_finder(&mut self, cx: &mut ViewContext<ContactsPopover>) {
let child = cx.add_view(|cx| ContactFinder::new(self.user_store.clone(), cx));
cx.focus(&child);
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
crate::contact_finder::Event::Dismissed => cx.emit(Event::Dismissed),
}));
self.child = Child::ContactFinder(child);
cx.notify();
}
fn show_contact_list(&mut self, cx: &mut ViewContext<ContactsPopover>) {
let child =
cx.add_view(|cx| ContactList::new(self.project.clone(), self.user_store.clone(), cx));
cx.focus(&child);
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
}));
self.child = Child::ContactList(child);
cx.notify();
}
}
impl Entity for ContactsPopover {
type Event = Event;
}
impl View for ContactsPopover {
fn ui_name() -> &'static str {
"ContactsPopover"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone();
let child = match &self.child {
Child::ContactList(child) => ChildView::new(child, cx),
Child::ContactFinder(child) => ChildView::new(child, cx),
};
MouseEventHandler::<ContactsPopover>::new(0, cx, |_, cx| {
Flex::column()
.with_child(child.flex(1., true).boxed())
.with_children(
self.user_store
.read(cx)
.invite_info()
.cloned()
.and_then(|info| {
enum InviteLink {}
if info.count > 0 {
Some(
MouseEventHandler::<InviteLink>::new(0, cx, |state, cx| {
let style = theme
.contacts_popover
.invite_row
.style_for(state, false)
.clone();
let copied =
cx.read_from_clipboard().map_or(false, |item| {
item.text().as_str() == info.url.as_ref()
});
Label::new(
format!(
"{} invite link ({} left)",
if copied { "Copied" } else { "Copy" },
info.count
),
style.label.clone(),
)
.aligned()
.left()
.constrained()
.with_height(theme.contacts_popover.invite_row_height)
.contained()
.with_style(style.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.write_to_clipboard(ClipboardItem::new(
info.url.to_string(),
));
cx.notify();
})
.boxed(),
)
} else {
None
}
}),
)
.contained()
.with_style(theme.contacts_popover.container)
.constrained()
.with_width(theme.contacts_popover.width)
.with_height(theme.contacts_popover.height)
.boxed()
})
.on_down_out(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleCollaborationMenu);
})
.boxed()
}
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),
Child::ContactFinder(child) => cx.focus(child),
}
}
}
}

View File

@@ -0,0 +1,235 @@
use call::{ActiveCall, IncomingCall};
use client::proto;
use futures::StreamExt;
use gpui::{
elements::*,
geometry::{rect::RectF, vector::vec2f},
impl_internal_actions, CursorStyle, Entity, MouseButton, MutableAppContext, RenderContext,
View, ViewContext, WindowBounds, WindowKind, WindowOptions,
};
use settings::Settings;
use util::ResultExt;
use workspace::JoinProject;
impl_internal_actions!(incoming_call_notification, [RespondToCall]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(IncomingCallNotification::respond_to_call);
let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
cx.spawn(|mut cx| async move {
let mut notification_windows = Vec::new();
while let Some(incoming_call) = incoming_call.next().await {
for window_id in notification_windows.drain(..) {
cx.remove_window(window_id);
}
if let Some(incoming_call) = incoming_call {
const PADDING: f32 = 16.;
let window_size = cx.read(|cx| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
vec2f(theme.window_width, theme.window_height)
});
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);
}
}
}
})
.detach();
}
#[derive(Clone, PartialEq)]
struct RespondToCall {
accept: bool,
}
pub struct IncomingCallNotification {
call: IncomingCall,
}
impl IncomingCallNotification {
pub fn new(call: IncomingCall) -> Self {
Self { call }
}
fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext<Self>) {
let active_call = ActiveCall::global(cx);
if action.accept {
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
let caller_user_id = self.call.caller.id;
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
cx.spawn_weak(|_, mut cx| async move {
join.await?;
if let Some(project_id) = initial_project_id {
cx.update(|cx| {
cx.dispatch_global_action(JoinProject {
project_id,
follow_user_id: caller_user_id,
})
});
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
} else {
active_call.update(cx, |active_call, _| {
active_call.decline_incoming().log_err();
});
}
}
fn render_caller(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
let default_project = proto::ParticipantProject::default();
let initial_project = self
.call
.initial_project
.as_ref()
.unwrap_or(&default_project);
Flex::row()
.with_children(self.call.caller.avatar.clone().map(|avatar| {
Image::new(avatar)
.with_style(theme.caller_avatar)
.aligned()
.boxed()
}))
.with_child(
Flex::column()
.with_child(
Label::new(
self.call.caller.github_login.clone(),
theme.caller_username.text.clone(),
)
.contained()
.with_style(theme.caller_username.container)
.boxed(),
)
.with_child(
Label::new(
format!(
"is sharing a project in Zed{}",
if initial_project.worktree_root_names.is_empty() {
""
} else {
":"
}
),
theme.caller_message.text.clone(),
)
.contained()
.with_style(theme.caller_message.container)
.boxed(),
)
.with_children(if initial_project.worktree_root_names.is_empty() {
None
} else {
Some(
Label::new(
initial_project.worktree_root_names.join(", "),
theme.worktree_roots.text.clone(),
)
.contained()
.with_style(theme.worktree_roots.container)
.boxed(),
)
})
.contained()
.with_style(theme.caller_metadata)
.aligned()
.boxed(),
)
.contained()
.with_style(theme.caller_container)
.flex(1., true)
.boxed()
}
fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
enum Accept {}
enum Decline {}
Flex::column()
.with_child(
MouseEventHandler::<Accept>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
Label::new("Accept".to_string(), theme.accept_button.text.clone())
.aligned()
.contained()
.with_style(theme.accept_button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(RespondToCall { accept: true });
})
.flex(1., true)
.boxed(),
)
.with_child(
MouseEventHandler::<Decline>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
Label::new("Decline".to_string(), theme.decline_button.text.clone())
.aligned()
.contained()
.with_style(theme.decline_button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(RespondToCall { accept: false });
})
.flex(1., true)
.boxed(),
)
.constrained()
.with_width(
cx.global::<Settings>()
.theme
.incoming_call_notification
.button_width,
)
.boxed()
}
}
impl Entity for IncomingCallNotification {
type Event = ();
}
impl View for IncomingCallNotification {
fn ui_name() -> &'static str {
"IncomingCallNotification"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
let background = cx
.global::<Settings>()
.theme
.incoming_call_notification
.background;
Flex::row()
.with_child(self.render_caller(cx))
.with_child(self.render_buttons(cx))
.contained()
.with_background_color(background)
.expanded()
.boxed()
}
}

View File

@@ -1,9 +1,7 @@
use crate::render_icon_button;
use client::User;
use gpui::{
elements::{Flex, Image, Label, MouseEventHandler, Padding, ParentElement, Text},
platform::CursorStyle,
Action, Element, ElementBox, MouseButton, RenderContext, View,
elements::*, platform::CursorStyle, Action, Element, ElementBox, MouseButton, RenderContext,
View,
};
use settings::Settings;
use std::sync::Arc;
@@ -53,11 +51,18 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
)
.with_child(
MouseEventHandler::<Dismiss>::new(user.id as usize, cx, |state, _| {
render_icon_button(
theme.dismiss_button.style_for(state, false),
"icons/x_mark_thin_8.svg",
)
.boxed()
let style = theme.dismiss_button.style_for(state, false);
Svg::new("icons/x_mark_thin_8.svg")
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.contained()
.with_style(style.container)
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.with_padding(Padding::uniform(5.))

View File

@@ -0,0 +1,243 @@
use call::{room, ActiveCall};
use client::User;
use collections::HashMap;
use gpui::{
actions,
elements::*,
geometry::{rect::RectF, vector::vec2f},
CursorStyle, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext,
WindowBounds, WindowKind, WindowOptions,
};
use settings::Settings;
use std::sync::Arc;
use workspace::JoinProject;
actions!(project_shared_notification, [DismissProject]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ProjectSharedNotification::join);
cx.add_action(ProjectSharedNotification::dismiss);
let active_call = ActiveCall::global(cx);
let mut notification_windows = HashMap::default();
cx.subscribe(&active_call, move |_, event, cx| match event {
room::Event::RemoteProjectShared {
owner,
project_id,
worktree_root_names,
} => {
const PADDING: f32 = 16.;
let theme = &cx.global::<Settings>().theme.project_shared_notification;
let window_size = vec2f(theme.window_width, theme.window_height);
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_ids) = notification_windows.remove(&project_id) {
for window_id in window_ids {
cx.remove_window(window_id);
}
}
}
room::Event::Left => {
for (_, window_ids) in notification_windows.drain() {
for window_id in window_ids {
cx.remove_window(window_id);
}
}
}
_ => {}
})
.detach();
}
pub struct ProjectSharedNotification {
project_id: u64,
worktree_root_names: Vec<String>,
owner: Arc<User>,
}
impl ProjectSharedNotification {
fn new(owner: Arc<User>, project_id: u64, worktree_root_names: Vec<String>) -> Self {
Self {
project_id,
worktree_root_names,
owner,
}
}
fn join(&mut self, _: &JoinProject, cx: &mut ViewContext<Self>) {
let window_id = cx.window_id();
cx.remove_window(window_id);
cx.propagate_action();
}
fn dismiss(&mut self, _: &DismissProject, cx: &mut ViewContext<Self>) {
let window_id = cx.window_id();
cx.remove_window(window_id);
}
fn render_owner(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.global::<Settings>().theme.project_shared_notification;
Flex::row()
.with_children(self.owner.avatar.clone().map(|avatar| {
Image::new(avatar)
.with_style(theme.owner_avatar)
.aligned()
.boxed()
}))
.with_child(
Flex::column()
.with_child(
Label::new(
self.owner.github_login.clone(),
theme.owner_username.text.clone(),
)
.contained()
.with_style(theme.owner_username.container)
.boxed(),
)
.with_child(
Label::new(
format!(
"is sharing a project in Zed{}",
if self.worktree_root_names.is_empty() {
""
} else {
":"
}
),
theme.message.text.clone(),
)
.contained()
.with_style(theme.message.container)
.boxed(),
)
.with_children(if self.worktree_root_names.is_empty() {
None
} else {
Some(
Label::new(
self.worktree_root_names.join(", "),
theme.worktree_roots.text.clone(),
)
.contained()
.with_style(theme.worktree_roots.container)
.boxed(),
)
})
.contained()
.with_style(theme.owner_metadata)
.aligned()
.boxed(),
)
.contained()
.with_style(theme.owner_container)
.flex(1., true)
.boxed()
}
fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
enum Open {}
enum Dismiss {}
let project_id = self.project_id;
let owner_user_id = self.owner.id;
Flex::column()
.with_child(
MouseEventHandler::<Open>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.project_shared_notification;
Label::new("Open".to_string(), theme.open_button.text.clone())
.aligned()
.contained()
.with_style(theme.open_button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(JoinProject {
project_id,
follow_user_id: owner_user_id,
});
})
.flex(1., true)
.boxed(),
)
.with_child(
MouseEventHandler::<Dismiss>::new(0, cx, |_, cx| {
let theme = &cx.global::<Settings>().theme.project_shared_notification;
Label::new("Dismiss".to_string(), theme.dismiss_button.text.clone())
.aligned()
.contained()
.with_style(theme.dismiss_button.container)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(DismissProject);
})
.flex(1., true)
.boxed(),
)
.constrained()
.with_width(
cx.global::<Settings>()
.theme
.project_shared_notification
.button_width,
)
.boxed()
}
}
impl Entity for ProjectSharedNotification {
type Event = ();
}
impl View for ProjectSharedNotification {
fn ui_name() -> &'static str {
"ProjectSharedNotification"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
let background = cx
.global::<Settings>()
.theme
.project_shared_notification
.background;
Flex::row()
.with_child(self.render_owner(cx))
.with_child(self.render_buttons(cx))
.contained()
.with_background_color(background)
.expanded()
.boxed()
}
}

View File

@@ -4,8 +4,8 @@ use gpui::{
actions,
elements::{ChildView, Flex, Label, ParentElement},
keymap::Keystroke,
Action, AnyViewHandle, Element, Entity, MouseState, MutableAppContext, View, ViewContext,
ViewHandle,
Action, AnyViewHandle, Element, Entity, MouseState, MutableAppContext, RenderContext, View,
ViewContext, ViewHandle,
};
use picker::{Picker, PickerDelegate};
use settings::Settings;
@@ -131,11 +131,11 @@ impl View for CommandPalette {
"CommandPalette"
}
fn render(&mut self, _: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
ChildView::new(self.picker.clone()).boxed()
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
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

@@ -1,32 +0,0 @@
[package]
name = "contacts_panel"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/contacts_panel.rs"
doctest = false
[dependencies]
client = { path = "../client" }
collections = { path = "../collections" }
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
menu = { path = "../menu" }
picker = { path = "../picker" }
project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow = "1.0"
futures = "0.3"
log = "0.4"
postage = { version = "0.4.1", features = ["futures-traits"] }
serde = { version = "1.0", features = ["derive", "rc"] }
[dev-dependencies]
language = { path = "../language", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

File diff suppressed because it is too large Load Diff

View File

@@ -1,80 +0,0 @@
use client::User;
use gpui::{
actions, ElementBox, Entity, ModelHandle, MutableAppContext, RenderContext, View, ViewContext,
};
use project::Project;
use std::sync::Arc;
use workspace::Notification;
use crate::notifications::render_user_notification;
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(JoinProjectNotification::decline);
cx.add_action(JoinProjectNotification::accept);
}
pub enum Event {
Dismiss,
}
actions!(contacts_panel, [Accept, Decline]);
pub struct JoinProjectNotification {
project: ModelHandle<Project>,
user: Arc<User>,
}
impl JoinProjectNotification {
pub fn new(project: ModelHandle<Project>, user: Arc<User>, cx: &mut ViewContext<Self>) -> Self {
cx.subscribe(&project, |this, _, event, cx| {
if let project::Event::ContactCancelledJoinRequest(user) = event {
if *user == this.user {
cx.emit(Event::Dismiss);
}
}
})
.detach();
Self { project, user }
}
fn decline(&mut self, _: &Decline, cx: &mut ViewContext<Self>) {
self.project.update(cx, |project, cx| {
project.respond_to_join_request(self.user.id, false, cx)
});
cx.emit(Event::Dismiss)
}
fn accept(&mut self, _: &Accept, cx: &mut ViewContext<Self>) {
self.project.update(cx, |project, cx| {
project.respond_to_join_request(self.user.id, true, cx)
});
cx.emit(Event::Dismiss)
}
}
impl Entity for JoinProjectNotification {
type Event = Event;
}
impl View for JoinProjectNotification {
fn ui_name() -> &'static str {
"JoinProjectNotification"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
render_user_notification(
self.user.clone(),
"wants to join your project",
None,
Decline,
vec![("Decline", Box::new(Decline)), ("Accept", Box::new(Accept))],
cx,
)
}
}
impl Notification for JoinProjectNotification {
fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
matches!(event, Event::Dismiss)
}
}

View File

@@ -1,32 +0,0 @@
[package]
name = "contacts_status_item"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/contacts_status_item.rs"
doctest = false
[dependencies]
client = { path = "../client" }
collections = { path = "../collections" }
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
menu = { path = "../menu" }
picker = { path = "../picker" }
project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow = "1.0"
futures = "0.3"
log = "0.4"
postage = { version = "0.4.1", features = ["futures-traits"] }
serde = { version = "1.0", features = ["derive", "rc"] }
[dev-dependencies]
language = { path = "../language", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

View File

@@ -1,94 +0,0 @@
use editor::Editor;
use gpui::{elements::*, Entity, RenderContext, View, ViewContext, ViewHandle};
use settings::Settings;
pub enum Event {
Deactivated,
}
pub struct ContactsPopover {
filter_editor: ViewHandle<Editor>,
}
impl Entity for ContactsPopover {
type Event = Event;
}
impl View for ContactsPopover {
fn ui_name() -> &'static str {
"ContactsPopover"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.global::<Settings>().theme.contacts_popover;
Flex::row()
.with_child(
ChildView::new(self.filter_editor.clone())
.contained()
.with_style(
cx.global::<Settings>()
.theme
.contacts_panel
.user_query_editor
.container,
)
.flex(1., true)
.boxed(),
)
// .with_child(
// MouseEventHandler::<AddContact>::new(0, cx, |_, _| {
// Svg::new("icons/user_plus_16.svg")
// .with_color(theme.add_contact_button.color)
// .constrained()
// .with_height(16.)
// .contained()
// .with_style(theme.add_contact_button.container)
// .aligned()
// .boxed()
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, |_, cx| {
// cx.dispatch_action(contact_finder::Toggle)
// })
// .boxed(),
// )
.constrained()
.with_height(
cx.global::<Settings>()
.theme
.contacts_panel
.user_query_editor_height,
)
.aligned()
.top()
.contained()
.with_background_color(theme.background)
.with_uniform_padding(4.)
.boxed()
}
}
impl ContactsPopover {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
cx.observe_window_activation(Self::window_activation_changed)
.detach();
let filter_editor = cx.add_view(|cx| {
let mut editor = Editor::single_line(
Some(|theme| theme.contacts_panel.user_query_editor.clone()),
cx,
);
editor.set_placeholder_text("Filter contacts", cx);
editor
});
Self { filter_editor }
}
fn window_activation_changed(&mut self, is_active: bool, cx: &mut ViewContext<Self>) {
if !is_active {
cx.emit(Event::Deactivated);
}
}
}

View File

@@ -1,94 +0,0 @@
mod contacts_popover;
use contacts_popover::ContactsPopover;
use gpui::{
actions,
color::Color,
elements::*,
geometry::{rect::RectF, vector::vec2f},
Appearance, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext,
ViewHandle, WindowKind,
};
actions!(contacts_status_item, [ToggleContactsPopover]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContactsStatusItem::toggle_contacts_popover);
}
pub struct ContactsStatusItem {
popover: Option<ViewHandle<ContactsPopover>>,
}
impl Entity for ContactsStatusItem {
type Event = ();
}
impl View for ContactsStatusItem {
fn ui_name() -> &'static str {
"ContactsStatusItem"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let color = match cx.appearance {
Appearance::Light | Appearance::VibrantLight => Color::black(),
Appearance::Dark | Appearance::VibrantDark => Color::white(),
};
MouseEventHandler::<Self>::new(0, cx, |_, _| {
Svg::new("icons/zed_22.svg")
.with_color(color)
.aligned()
.boxed()
})
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(ToggleContactsPopover);
})
.boxed()
}
}
impl ContactsStatusItem {
pub fn new() -> Self {
Self { popover: None }
}
fn toggle_contacts_popover(&mut self, _: &ToggleContactsPopover, cx: &mut ViewContext<Self>) {
match self.popover.take() {
Some(popover) => {
cx.remove_window(popover.window_id());
}
None => {
let window_bounds = cx.window_bounds();
let size = vec2f(360., 460.);
let origin = window_bounds.lower_left()
+ vec2f(window_bounds.width() / 2. - size.x() / 2., 0.);
let (_, popover) = cx.add_window(
gpui::WindowOptions {
bounds: gpui::WindowBounds::Fixed(RectF::new(origin, size)),
titlebar: None,
center: false,
kind: WindowKind::PopUp,
is_movable: false,
},
|cx| ContactsPopover::new(cx),
);
cx.subscribe(&popover, Self::on_popover_event).detach();
self.popover = Some(popover);
}
}
}
fn on_popover_event(
&mut self,
popover: ViewHandle<ContactsPopover>,
event: &contacts_popover::Event,
cx: &mut ViewContext<Self>,
) {
match event {
contacts_popover::Event::Deactivated => {
self.popover.take();
cx.remove_window(popover.window_id());
}
}
}
}

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 = { git = "https://github.com/cljoly/rusqlite_migration", rev = "c433555d7c1b41b103426e35756eb3144d0ebbc6" }
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, channel: &'static str) -> Self {
// Use 0 for now. Will implement incrementing and clearing of old db files soon TM
let current_db_dir = db_dir.join(Path::new(&format!("0-{}", channel)));
fs::create_dir_all(&current_db_dir)
.expect("Should be able to create the database directory");
let db_path = current_db_dir.join(Path::new("db.sqlite"));
Connection::open(db_path)
.map_err(Into::into)
.and_then(|connection| Self::initialize(connection))
.map(|connection| {
Db::Real(Arc::new(RealDb {
connection,
path: Some(db_dir.to_path_buf()),
}))
})
.unwrap_or_else(|e| {
error!(
"Connecting to file backed db failed. Reverting to null db. {}",
e
);
Self::Null
})
}
/// Open a 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

@@ -95,11 +95,11 @@ impl View for ProjectDiagnosticsEditor {
.with_style(theme.container)
.boxed()
} else {
ChildView::new(&self.editor).boxed()
ChildView::new(&self.editor, cx).boxed()
}
}
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,11 +20,13 @@ test-support = [
]
[dependencies]
drag_and_drop = { path = "../drag_and_drop" }
text = { path = "../text" }
clock = { path = "../clock" }
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
fuzzy = { path = "../fuzzy" }
git = { path = "../git" }
gpui = { path = "../gpui" }
language = { path = "../language" }
lsp = { path = "../lsp" }
@@ -47,10 +49,12 @@ ordered-float = "2.1.1"
parking_lot = "0.11"
postage = { version = "0.4", features = ["futures-traits"] }
rand = { version = "0.8.3", optional = true }
serde = { version = "1.0", features = ["derive", "rc"] }
serde = { workspace = true }
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
tree-sitter-rust = { version = "*", optional = true }
tree-sitter-html = { version = "*", optional = true }
tree-sitter-javascript = { version = "*", optional = true }
[dev-dependencies]
text = { path = "../text", features = ["test-support"] }
@@ -67,3 +71,5 @@ rand = "0.8"
unindent = "0.1.7"
tree-sitter = "0.20"
tree-sitter-rust = "0.20"
tree-sitter-html = "0.19"
tree-sitter-javascript = "0.20"

View File

@@ -0,0 +1,110 @@
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();
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

@@ -330,34 +330,91 @@ impl DisplaySnapshot {
DisplayPoint(self.blocks_snapshot.max_point())
}
/// Returns text chunks starting at the given display row until the end of the file
pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
self.blocks_snapshot
.chunks(display_row..self.max_point().row() + 1, false, None)
.map(|h| h.text)
}
// Returns text chunks starting at the end of the given display row in reverse until the start of the file
pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
(0..=display_row).into_iter().rev().flat_map(|row| {
self.blocks_snapshot
.chunks(row..row + 1, false, None)
.map(|h| h.text)
.collect::<Vec<_>>()
.into_iter()
.rev()
})
}
pub fn chunks(&self, display_rows: Range<u32>, language_aware: bool) -> DisplayChunks<'_> {
self.blocks_snapshot
.chunks(display_rows, language_aware, Some(&self.text_highlights))
}
pub fn chars_at(&self, point: DisplayPoint) -> impl Iterator<Item = char> + '_ {
let mut column = 0;
let mut chars = self.text_chunks(point.row()).flat_map(str::chars);
while column < point.column() {
if let Some(c) = chars.next() {
column += c.len_utf8() as u32;
} else {
break;
}
}
chars
pub fn chars_at(
&self,
mut point: DisplayPoint,
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
self.text_chunks(point.row())
.flat_map(str::chars)
.skip_while({
let mut column = 0;
move |char| {
let at_point = column >= point.column();
column += char.len_utf8() as u32;
!at_point
}
})
.map(move |ch| {
let result = (ch, point);
if ch == '\n' {
*point.row_mut() += 1;
*point.column_mut() = 0;
} else {
*point.column_mut() += ch.len_utf8() as u32;
}
result
})
}
pub fn reverse_chars_at(
&self,
mut point: DisplayPoint,
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
self.reverse_text_chunks(point.row())
.flat_map(|chunk| chunk.chars().rev())
.skip_while({
let mut column = self.line_len(point.row());
if self.max_point().row() > point.row() {
column += 1;
}
move |char| {
let at_point = column <= point.column();
column = column.saturating_sub(char.len_utf8() as u32);
!at_point
}
})
.map(move |ch| {
if ch == '\n' {
*point.row_mut() -= 1;
*point.column_mut() = self.line_len(point.row());
} else {
*point.column_mut() = point.column().saturating_sub(ch.len_utf8() as u32);
}
(ch, point)
})
}
pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
let mut count = 0;
let mut column = 0;
for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
if column >= target {
break;
}
@@ -370,7 +427,7 @@ impl DisplaySnapshot {
pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 {
let mut column = 0;
for (count, c) in self.chars_at(DisplayPoint::new(display_row, 0)).enumerate() {
for (count, (c, _)) in self.chars_at(DisplayPoint::new(display_row, 0)).enumerate() {
if c == '\n' || count >= char_count as usize {
break;
}
@@ -454,7 +511,7 @@ impl DisplaySnapshot {
pub fn line_indent(&self, display_row: u32) -> (u32, bool) {
let mut indent = 0;
let mut is_blank = true;
for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
if c == ' ' {
indent += 1;
} else {
@@ -565,7 +622,7 @@ pub mod tests {
use super::*;
use crate::{movement, test::marked_display_snapshot};
use gpui::{color::Color, elements::*, test::observe, MutableAppContext};
use language::{Buffer, Language, LanguageConfig, RandomCharIter, SelectionGoal};
use language::{Buffer, Language, LanguageConfig, SelectionGoal};
use rand::{prelude::*, Rng};
use smol::stream::StreamExt;
use std::{env, sync::Arc};
@@ -609,7 +666,9 @@ pub mod tests {
let buffer = cx.update(|cx| {
if rng.gen() {
let len = rng.gen_range(0..10);
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
let text = util::RandomCharIter::new(&mut rng)
.take(len)
.collect::<String>();
MultiBuffer::build_simple(&text, cx)
} else {
MultiBuffer::build_random(&mut rng, cx)

View File

@@ -5,7 +5,7 @@ use super::{
use crate::{Anchor, ExcerptRange, ToPoint as _};
use collections::{Bound, HashMap, HashSet};
use gpui::{ElementBox, RenderContext};
use language::{BufferSnapshot, Chunk, Patch};
use language::{BufferSnapshot, Chunk, Patch, Point};
use parking_lot::Mutex;
use std::{
cell::RefCell,
@@ -18,7 +18,7 @@ use std::{
},
};
use sum_tree::{Bias, SumTree};
use text::{Edit, Point};
use text::Edit;
const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
@@ -42,7 +42,7 @@ pub struct BlockSnapshot {
pub struct BlockId(usize);
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct BlockPoint(pub super::Point);
pub struct BlockPoint(pub Point);
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
struct BlockRow(u32);
@@ -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>,
@@ -994,7 +995,7 @@ mod tests {
use rand::prelude::*;
use settings::Settings;
use std::env;
use text::RandomCharIter;
use util::RandomCharIter;
#[gpui::test]
fn test_offset_for_row() {

View File

@@ -18,11 +18,11 @@ use std::{
use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct FoldPoint(pub super::Point);
pub struct FoldPoint(pub Point);
impl FoldPoint {
pub fn new(row: u32, column: u32) -> Self {
Self(super::Point::new(row, column))
Self(Point::new(row, column))
}
pub fn row(self) -> u32 {
@@ -274,6 +274,7 @@ impl FoldMap {
if buffer.edit_count() != new_buffer.edit_count()
|| buffer.parse_count() != new_buffer.parse_count()
|| buffer.diagnostics_update_count() != new_buffer.diagnostics_update_count()
|| buffer.git_diff_update_count() != new_buffer.git_diff_update_count()
|| buffer.trailing_excerpt_update_count()
!= new_buffer.trailing_excerpt_update_count()
{
@@ -986,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>,
@@ -1195,8 +1197,8 @@ mod tests {
use settings::Settings;
use std::{cmp::Reverse, env, mem, sync::Arc};
use sum_tree::TreeMap;
use text::RandomCharIter;
use util::test::sample_text;
use util::RandomCharIter;
use Bias::{Left, Right};
#[gpui::test]

View File

@@ -3,11 +3,10 @@ use super::{
TextHighlights,
};
use crate::MultiBufferSnapshot;
use language::{rope, Chunk};
use language::{Chunk, Point};
use parking_lot::Mutex;
use std::{cmp, mem, num::NonZeroU32, ops::Range};
use sum_tree::Bias;
use text::Point;
pub struct TabMap(Mutex<TabSnapshot>);
@@ -332,11 +331,11 @@ impl TabSnapshot {
}
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct TabPoint(pub super::Point);
pub struct TabPoint(pub Point);
impl TabPoint {
pub fn new(row: u32, column: u32) -> Self {
Self(super::Point::new(row, column))
Self(Point::new(row, column))
}
pub fn zero() -> Self {
@@ -352,8 +351,8 @@ impl TabPoint {
}
}
impl From<super::Point> for TabPoint {
fn from(point: super::Point) -> Self {
impl From<Point> for TabPoint {
fn from(point: Point) -> Self {
Self(point)
}
}
@@ -362,7 +361,7 @@ pub type TabEdit = text::Edit<TabPoint>;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct TextSummary {
pub lines: super::Point,
pub lines: Point,
pub first_line_chars: u32,
pub last_line_chars: u32,
pub longest_row: u32,
@@ -371,7 +370,7 @@ pub struct TextSummary {
impl<'a> From<&'a str> for TextSummary {
fn from(text: &'a str) -> Self {
let sum = rope::TextSummary::from(text);
let sum = text::TextSummary::from(text);
TextSummary {
lines: sum.lines,
@@ -485,7 +484,6 @@ mod tests {
use super::*;
use crate::{display_map::fold_map::FoldMap, MultiBuffer};
use rand::{prelude::StdRng, Rng};
use text::{RandomCharIter, Rope};
#[test]
fn test_expand_tabs() {
@@ -508,7 +506,9 @@ mod tests {
let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
let len = rng.gen_range(0..30);
let buffer = if rng.gen() {
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
let text = util::RandomCharIter::new(&mut rng)
.take(len)
.collect::<String>();
MultiBuffer::build_simple(&text, cx)
} else {
MultiBuffer::build_random(&mut rng, cx)
@@ -522,7 +522,7 @@ mod tests {
log::info!("FoldMap text: {:?}", folds_snapshot.text());
let (_, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
let text = Rope::from(tabs_snapshot.text().as_str());
let text = text::Rope::from(tabs_snapshot.text().as_str());
log::info!(
"TabMap text (tab size: {}): {:?}",
tab_size,

View File

@@ -3,12 +3,12 @@ use super::{
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
TextHighlights,
};
use crate::{MultiBufferSnapshot, Point};
use crate::MultiBufferSnapshot;
use gpui::{
fonts::FontId, text_layout::LineWrapper, Entity, ModelContext, ModelHandle, MutableAppContext,
Task,
};
use language::Chunk;
use language::{Chunk, Point};
use lazy_static::lazy_static;
use smol::future::yield_now;
use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration};
@@ -52,7 +52,7 @@ struct TransformSummary {
}
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct WrapPoint(pub super::Point);
pub struct WrapPoint(pub Point);
pub struct WrapChunks<'a> {
input_chunks: tab_map::TabChunks<'a>,
@@ -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>,
@@ -959,7 +960,7 @@ impl SumTreeExt for SumTree<Transform> {
impl WrapPoint {
pub fn new(row: u32, column: u32) -> Self {
Self(super::Point::new(row, column))
Self(Point::new(row, column))
}
pub fn row(self) -> u32 {
@@ -1029,7 +1030,6 @@ mod tests {
MultiBuffer,
};
use gpui::test::observe;
use language::RandomCharIter;
use rand::prelude::*;
use settings::Settings;
use smol::stream::StreamExt;
@@ -1067,7 +1067,9 @@ mod tests {
MultiBuffer::build_random(&mut rng, cx)
} else {
let len = rng.gen_range(0..10);
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
let text = util::RandomCharIter::new(&mut rng)
.take(len)
.collect::<String>();
MultiBuffer::build_simple(&text, cx)
}
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -9,13 +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::DiffHunkStatus;
use gpui::{
color::Color,
elements::*,
@@ -28,26 +29,33 @@ 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::Settings;
use settings::{GitGutter, Settings};
use smallvec::SmallVec;
use std::{
cmp::{self, Ordering},
fmt::Write,
iter,
ops::Range,
ops::{DerefMut, Range},
sync::Arc,
};
#[derive(Debug)]
struct DiffHunkLayout {
visual_range: Range<u32>,
status: DiffHunkStatus,
is_folded: bool,
}
struct SelectionLayout {
head: DisplayPoint,
cursor_shape: CursorShape,
range: Range<DisplayPoint>,
}
@@ -55,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 {
@@ -62,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),
}
@@ -69,6 +79,7 @@ impl SelectionLayout {
let selection = selection.map(|p| p.to_display_point(map));
Self {
head: selection.head(),
cursor_shape,
range: selection.range(),
}
}
@@ -79,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,
}
}
@@ -132,7 +137,7 @@ impl EditorElement {
gutter_bounds,
cx,
) {
cx.propogate_event();
cx.propagate_event();
}
}
})
@@ -145,7 +150,7 @@ impl EditorElement {
text_bounds,
cx,
) {
cx.propogate_event();
cx.propagate_event();
}
}
})
@@ -162,7 +167,7 @@ impl EditorElement {
text_bounds,
cx,
) {
cx.propogate_event()
cx.propagate_event()
}
}
})
@@ -177,7 +182,7 @@ impl EditorElement {
text_bounds,
cx,
) {
cx.propogate_event()
cx.propagate_event()
}
}
})
@@ -185,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()
}
}
})
@@ -194,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()
}
}
}),
@@ -204,10 +209,14 @@ impl EditorElement {
fn mouse_down(
MouseButtonEvent {
position,
ctrl,
alt,
shift,
cmd,
modifiers:
Modifiers {
shift,
ctrl,
alt,
cmd,
..
},
mut click_count,
..
}: MouseButtonEvent,
@@ -298,8 +307,7 @@ impl EditorElement {
fn mouse_dragged(
view: WeakViewHandle<Editor>,
MouseMovedEvent {
cmd,
shift,
modifiers: Modifiers { cmd, shift, .. },
position,
..
}: MouseMovedEvent,
@@ -374,8 +382,7 @@ impl EditorElement {
fn mouse_moved(
MouseMovedEvent {
cmd,
shift,
modifiers: Modifiers { shift, cmd, .. },
position,
..
}: MouseMovedEvent,
@@ -406,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,
@@ -426,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
}
@@ -452,7 +460,6 @@ impl EditorElement {
let bounds = gutter_bounds.union_rect(text_bounds);
let scroll_top =
layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
let editor = self.view(cx.app);
cx.scene.push_quad(Quad {
bounds: gutter_bounds,
background: Some(self.style.gutter_background),
@@ -466,7 +473,7 @@ impl EditorElement {
corner_radius: 0.,
});
if let EditorMode::Full = editor.mode {
if let EditorMode::Full = layout.mode {
let mut active_rows = layout.active_rows.iter().peekable();
while let Some((start_row, contains_non_empty_selection)) = active_rows.next() {
let mut end_row = *start_row;
@@ -524,34 +531,120 @@ impl EditorElement {
layout: &mut LayoutState,
cx: &mut PaintContext,
) {
let scroll_top =
layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
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;
let show_gutter = matches!(
&cx.global::<Settings>()
.git_overrides
.git_gutter
.unwrap_or_default(),
GitGutter::TrackedFiles
);
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 * layout.position_map.line_height
- (scroll_top % layout.position_map.line_height),
ix as f32 * line_height - (scroll_top % line_height),
);
line.paint(
line_origin,
visible_bounds,
layout.position_map.line_height,
cx,
);
line.paint(line_origin, visible_bounds, line_height, cx);
}
}
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 * layout.position_map.line_height - scroll_top;
let mut y = *row as f32 * line_height - scroll_top;
x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
y += (layout.position_map.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,
@@ -563,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.);
@@ -585,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,
@@ -607,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.,
@@ -620,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;
@@ -633,13 +723,13 @@ 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
.chars_at(cursor_position)
.next()
.and_then(|character| {
.and_then(|(character, _)| {
let font_id =
cursor_row_layout.font_for_index(cursor_column)?;
let text = character.to_string();
@@ -669,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,
});
}
@@ -701,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;
@@ -730,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 =
@@ -796,12 +886,123 @@ impl EditorElement {
cx.scene.pop_layer();
}
fn paint_scrollbar(&mut self, bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
enum ScrollbarMouseHandlers {}
if layout.mode != EditorMode::Full {
return;
}
let view = self.view.clone();
let style = &self.style.theme.scrollbar;
let top = bounds.min_y();
let bottom = bounds.max_y();
let right = bounds.max_x();
let left = right - style.width;
let row_range = &layout.scrollbar_row_range;
let max_row = layout.max_row as f32 + (row_range.end - row_range.start);
let mut height = bounds.height();
let mut first_row_y_offset = 0.0;
// Impose a minimum height on the scrollbar thumb
let min_thumb_height =
style.min_height_factor * cx.font_cache.line_height(self.style.text.font_size);
let thumb_height = (row_range.end - row_range.start) * height / max_row;
if thumb_height < min_thumb_height {
first_row_y_offset = (min_thumb_height - thumb_height) / 2.0;
height -= min_thumb_height - thumb_height;
}
let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * height / max_row };
let thumb_top = y_for_row(row_range.start) - first_row_y_offset;
let thumb_bottom = y_for_row(row_range.end) + first_row_y_offset;
let track_bounds = RectF::from_points(vec2f(left, top), vec2f(right, bottom));
let thumb_bounds = RectF::from_points(vec2f(left, thumb_top), vec2f(right, thumb_bottom));
if layout.show_scrollbars {
cx.scene.push_quad(Quad {
bounds: track_bounds,
border: style.track.border,
background: style.track.background_color,
..Default::default()
});
cx.scene.push_quad(Quad {
bounds: thumb_bounds,
border: style.thumb.border,
background: style.thumb.background_color,
corner_radius: style.thumb.corner_radius,
});
}
cx.scene.push_cursor_region(CursorRegion {
bounds: track_bounds,
style: CursorStyle::Arrow,
});
cx.scene.push_mouse_region(
MouseRegion::new::<ScrollbarMouseHandlers>(view.id(), view.id(), track_bounds)
.on_move({
let view = view.clone();
move |_, cx| {
if let Some(view) = view.upgrade(cx.deref_mut()) {
view.update(cx.deref_mut(), |view, cx| {
view.make_scrollbar_visible(cx);
});
}
}
})
.on_down(MouseButton::Left, {
let view = view.clone();
let row_range = row_range.clone();
move |e, cx| {
let y = e.position.y();
if let Some(view) = view.upgrade(cx.deref_mut()) {
view.update(cx.deref_mut(), |view, cx| {
if y < thumb_top || thumb_bottom < y {
let center_row =
((y - top) * max_row as f32 / height).round() as u32;
let top_row = center_row.saturating_sub(
(row_range.end - row_range.start) as u32 / 2,
);
let mut position = view.scroll_position(cx);
position.set_y(top_row as f32);
view.set_scroll_position(position, cx);
} else {
view.make_scrollbar_visible(cx);
}
});
}
}
})
.on_drag(MouseButton::Left, {
let view = view.clone();
move |e, cx| {
let y = e.prev_mouse_position.y();
let new_y = e.position.y();
if thumb_top < y && y < thumb_bottom {
if let Some(view) = view.upgrade(cx.deref_mut()) {
view.update(cx.deref_mut(), |view, cx| {
let mut position = view.scroll_position(cx);
position.set_y(
position.y() + (new_y - y) * (max_row as f32) / height,
);
if position.y() < 0.0 {
position.set_y(0.);
}
view.set_scroll_position(position, cx);
});
}
}
}
}),
);
}
#[allow(clippy::too_many_arguments)]
fn paint_highlighted_range(
&self,
range: Range<DisplayPoint>,
start_row: u32,
end_row: u32,
color: Color,
corner_radius: f32,
line_end_overshoot: f32,
@@ -812,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)
@@ -900,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>,
@@ -1054,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) {
@@ -1157,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() + "/");
@@ -1288,6 +1562,8 @@ impl Element for EditorElement {
let em_advance = style.text.em_advance(cx.font_cache);
let overscroll = vec2f(em_width, 0.);
let snapshot = self.update_view(cx.app, |view, cx| {
view.set_visible_line_count(size.y() / line_height);
let wrap_width = match view.soft_wrap_mode(cx) {
SoftWrap::None => Some((MAX_LINE_LEN / 2) as f32 * em_advance),
SoftWrap::EditorWidth => {
@@ -1333,12 +1609,13 @@ impl Element for EditorElement {
// The scroll position is a fractional point, the whole number of which represents
// the top of the window in terms of display rows.
let start_row = scroll_position.y() as u32;
let scroll_top = scroll_position.y() * line_height;
let height_in_lines = size.y() / line_height;
let max_row = snapshot.max_point().row();
// Add 1 to ensure selections bleed off screen
let end_row = 1 + cmp::min(
((scroll_top + size.y()) / line_height).ceil() as u32,
snapshot.max_point().row(),
(scroll_position.y() + height_in_lines).ceil() as u32,
max_row,
);
let start_anchor = if start_row == 0 {
@@ -1348,7 +1625,7 @@ impl Element for EditorElement {
.buffer_snapshot
.anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left))
};
let end_anchor = if end_row > snapshot.max_point().row() {
let end_anchor = if end_row > max_row {
Anchor::max()
} else {
snapshot
@@ -1360,6 +1637,8 @@ impl Element for EditorElement {
let mut active_rows = BTreeMap::new();
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));
@@ -1372,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()))
{
@@ -1383,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);
@@ -1415,16 +1699,32 @@ 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 hunk_layouts = self.layout_git_gutters(start_row..end_row, &snapshot);
let scrollbar_row_range = scroll_position.y()..(scroll_position.y() + height_in_lines);
let mut max_visible_line_width = 0.0;
let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx);
for line in &line_layouts {
@@ -1455,13 +1755,13 @@ impl Element for EditorElement {
line_height,
&style,
&line_layouts,
include_root,
cx,
);
let max_row = snapshot.max_point().row();
let scroll_max = vec2f(
((scroll_width - text_size.x()) / em_width).max(0.0),
max_row.saturating_sub(1) as f32,
max_row as f32,
);
self.update_view(cx.app, |view, cx| {
@@ -1488,6 +1788,7 @@ impl Element for EditorElement {
let mut context_menu = None;
let mut code_actions_indicator = None;
let mut hover = None;
let mut mode = EditorMode::Full;
cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
let newest_selection_head = view
.selections
@@ -1509,6 +1810,7 @@ impl Element for EditorElement {
let visible_rows = start_row..start_row + line_layouts.len() as u32;
hover = view.hover_state.render(&snapshot, &style, visible_rows, cx);
mode = view.mode;
});
if let Some((_, context_menu)) = context_menu.as_mut() {
@@ -1556,6 +1858,7 @@ impl Element for EditorElement {
(
size,
LayoutState {
mode,
position_map: Arc::new(PositionMap {
size,
scroll_max,
@@ -1565,14 +1868,19 @@ impl Element for EditorElement {
em_advance,
snapshot,
}),
visible_display_row_range: start_row..end_row,
gutter_size,
gutter_padding,
text_size,
scrollbar_row_range,
show_scrollbars,
max_row,
gutter_margin,
active_rows,
highlighted_rows,
highlighted_ranges,
line_number_layouts,
hunk_layouts,
blocks,
selections,
context_menu,
@@ -1589,7 +1897,8 @@ impl Element for EditorElement {
layout: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
cx.scene.push_layer(Some(bounds));
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
cx.scene.push_layer(Some(visible_bounds));
let gutter_bounds = RectF::new(bounds.origin(), layout.gutter_size);
let text_bounds = RectF::new(
@@ -1613,31 +1922,16 @@ impl Element for EditorElement {
}
self.paint_text(text_bounds, visible_bounds, layout, cx);
cx.scene.push_layer(Some(bounds));
if !layout.blocks.is_empty() {
cx.scene.push_layer(Some(bounds));
self.paint_blocks(bounds, visible_bounds, layout, cx);
cx.scene.pop_layer();
}
self.paint_scrollbar(bounds, layout, cx);
cx.scene.pop_layer();
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>,
@@ -1703,12 +1997,18 @@ pub struct LayoutState {
gutter_padding: f32,
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>)>,
scrollbar_row_range: Range<f32>,
show_scrollbars: bool,
max_row: u32,
context_menu: Option<(DisplayPoint, ElementBox)>,
code_actions_indicator: Option<(u32, ElementBox)>,
hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
@@ -1797,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,
@@ -1903,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(
@@ -1922,7 +2208,7 @@ impl HighlightedRange {
start_y: f32,
lines: &[HighlightedRangeLine],
bounds: RectF,
scene: &mut Scene,
scene: &mut SceneBuilder,
) {
if lines.is_empty() {
return;
@@ -2051,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);
@@ -2091,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

@@ -32,8 +32,9 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
#[cfg(test)]
mod tests {
use crate::test::editor_lsp_test_context::EditorLspTestContext;
use super::*;
use crate::test::EditorLspTestContext;
use indoc::indoc;
use language::{BracketPair, Language, LanguageConfig};

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)
})
@@ -427,13 +427,13 @@ impl DiagnosticPopover {
#[cfg(test)]
mod tests {
use futures::StreamExt;
use indoc::indoc;
use language::{Diagnostic, DiagnosticSet};
use project::HoverBlock;
use smol::stream::StreamExt;
use crate::test::EditorLspTestContext;
use crate::test::editor_lsp_test_context::EditorLspTestContext;
use super::*;

View File

@@ -9,7 +9,7 @@ use gpui::{
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
};
use language::{Bias, Buffer, File as _, OffsetRangeExt, SelectionGoal};
use language::{Bias, Buffer, File as _, OffsetRangeExt, Point, SelectionGoal};
use project::{File, FormatTrigger, Project, ProjectEntryId, ProjectPath};
use rpc::proto::{self, update_view};
use settings::Settings;
@@ -21,7 +21,7 @@ use std::{
ops::Range,
path::{Path, PathBuf},
};
use text::{Point, Selection};
use text::Selection;
use util::TryFutureExt;
use workspace::{
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
@@ -120,6 +120,7 @@ impl FollowableItem for Editor {
buffer.set_active_selections(
&self.selections.disjoint_anchors(),
self.selections.line_mode,
self.cursor_shape,
cx,
);
}
@@ -478,6 +479,17 @@ impl Item for Editor {
})
}
fn git_diff_recalc(
&mut self,
_project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.buffer().update(cx, |multibuffer, cx| {
multibuffer.git_diff_recalc(cx);
});
Task::ready(Ok(()))
}
fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
let mut result = Vec::new();
match event {
@@ -520,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,10 +358,11 @@ 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};
use crate::test::EditorLspTestContext;
use crate::test::editor_lsp_test_context::EditorLspTestContext;
use super::*;
@@ -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

@@ -70,8 +70,9 @@ pub fn deploy_context_menu(
#[cfg(test)]
mod tests {
use crate::test::editor_lsp_test_context::EditorLspTestContext;
use super::*;
use crate::test::EditorLspTestContext;
use indoc::indoc;
#[gpui::test]

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 {
@@ -101,6 +121,22 @@ pub fn line_beginning(
map: &DisplaySnapshot,
display_point: DisplayPoint,
stop_at_soft_boundaries: bool,
) -> DisplayPoint {
let point = display_point.to_point(map);
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
let line_start = map.prev_line_boundary(point).1;
if stop_at_soft_boundaries && display_point != soft_line_start {
soft_line_start
} else {
line_start
}
}
pub fn indented_line_beginning(
map: &DisplaySnapshot,
display_point: DisplayPoint,
stop_at_soft_boundaries: bool,
) -> DisplayPoint {
let point = display_point.to_point(map);
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
@@ -167,54 +203,79 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
})
}
/// Scans for a boundary from the start of each line preceding the given end point until a boundary
/// is found, indicated by the given predicate returning true. The predicate is called with the
/// character to the left and right of the candidate boundary location, and will be called with `\n`
/// characters indicating the start or end of a line. If the predicate returns true multiple times
/// on a line, the *rightmost* boundary is returned.
/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
/// given predicate returning true. The predicate is called with the character to the left and right
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
/// or end of a line.
pub fn find_preceding_boundary(
map: &DisplaySnapshot,
end: DisplayPoint,
from: DisplayPoint,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> DisplayPoint {
let mut point = end;
loop {
*point.column_mut() = 0;
if point.row() > 0 {
if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
*point.column_mut() = indent;
let mut start_column = 0;
let mut soft_wrap_row = from.row() + 1;
let mut prev = None;
for (ch, point) in map.reverse_chars_at(from) {
// Recompute soft_wrap_indent if the row has changed
if point.row() != soft_wrap_row {
soft_wrap_row = point.row();
if point.row() == 0 {
start_column = 0;
} else if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
start_column = indent;
}
}
let mut boundary = None;
let mut prev_ch = if point.is_zero() { None } else { Some('\n') };
for ch in map.chars_at(point) {
if point >= end {
break;
}
if let Some(prev_ch) = prev_ch {
if is_boundary(prev_ch, ch) {
boundary = Some(point);
}
}
if ch == '\n' {
break;
}
prev_ch = Some(ch);
*point.column_mut() += ch.len_utf8() as u32;
// If the current point is in the soft_wrap, skip comparing it
if point.column() < start_column {
continue;
}
if let Some(boundary) = boundary {
return boundary;
} else if point.row() == 0 {
return DisplayPoint::zero();
} else {
*point.row_mut() -= 1;
if let Some((prev_ch, prev_point)) = prev {
if is_boundary(ch, prev_ch) {
return prev_point;
}
}
prev = Some((ch, point));
}
DisplayPoint::zero()
}
/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
/// given predicate returning true. The predicate is called with the character to the left and right
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
/// or end of a line. If no boundary is found, the start of the line is returned.
pub fn find_preceding_boundary_in_line(
map: &DisplaySnapshot,
from: DisplayPoint,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> DisplayPoint {
let mut start_column = 0;
if from.row() > 0 {
if let Some(indent) = map.soft_wrap_indent(from.row() - 1) {
start_column = indent;
}
}
let mut prev = None;
for (ch, point) in map.reverse_chars_at(from) {
if let Some((prev_ch, prev_point)) = prev {
if is_boundary(ch, prev_ch) {
return prev_point;
}
}
if ch == '\n' || point.column() < start_column {
break;
}
prev = Some((ch, point));
}
prev.map(|(_, point)| point).unwrap_or(from)
}
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
@@ -223,26 +284,48 @@ pub fn find_preceding_boundary(
/// or end of a line.
pub fn find_boundary(
map: &DisplaySnapshot,
mut point: DisplayPoint,
from: DisplayPoint,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> DisplayPoint {
let mut prev_ch = None;
for ch in map.chars_at(point) {
for (ch, point) in map.chars_at(from) {
if let Some(prev_ch) = prev_ch {
if is_boundary(prev_ch, ch) {
break;
return map.clip_point(point, Bias::Right);
}
}
if ch == '\n' {
*point.row_mut() += 1;
*point.column_mut() = 0;
} else {
*point.column_mut() += ch.len_utf8() as u32;
}
prev_ch = Some(ch);
}
map.clip_point(point, Bias::Right)
map.clip_point(map.max_point(), Bias::Right)
}
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
/// given predicate returning true. The predicate is called with the character to the left and right
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
/// or end of a line. If no boundary is found, the end of the line is returned
pub fn find_boundary_in_line(
map: &DisplaySnapshot,
from: DisplayPoint,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> DisplayPoint {
let mut prev = None;
for (ch, point) in map.chars_at(from) {
if let Some((prev_ch, _)) = prev {
if is_boundary(prev_ch, ch) {
return map.clip_point(point, Bias::Right);
}
}
prev = Some((ch, point));
if ch == '\n' {
break;
}
}
// Return the last position checked so that we give a point right before the newline or eof.
map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Right)
}
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
@@ -273,7 +356,6 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<
mod tests {
use super::*;
use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer};
use language::Point;
use settings::Settings;
#[gpui::test]

View File

@@ -4,12 +4,14 @@ pub use anchor::{Anchor, AnchorRangeExt};
use anyhow::Result;
use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet};
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,
DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, Outline, OutlineItem,
Selection, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId,
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,
};
use smallvec::SmallVec;
use std::{
@@ -26,9 +28,8 @@ use std::{
use sum_tree::{Bias, Cursor, SumTree};
use text::{
locator::Locator,
rope::TextDimension,
subscription::{Subscription, Topic},
Edit, OffsetUtf16, Point, PointUtf16, TextSummary,
Edit, TextSummary,
};
use theme::SyntaxTheme;
use util::post_inc;
@@ -90,6 +91,7 @@ struct BufferState {
last_selections_update_count: usize,
last_diagnostics_update_count: usize,
last_file_update_count: usize,
last_git_diff_update_count: usize,
excerpts: Vec<ExcerptId>,
_subscriptions: [gpui::Subscription; 2],
}
@@ -101,6 +103,7 @@ pub struct MultiBufferSnapshot {
parse_count: usize,
diagnostics_update_count: usize,
trailing_excerpt_update_count: usize,
git_diff_update_count: usize,
edit_count: usize,
is_dirty: bool,
has_conflict: bool,
@@ -140,6 +143,7 @@ struct ExcerptSummary {
text: TextSummary,
}
#[derive(Clone)]
pub struct MultiBufferRows<'a> {
buffer_row_range: Range<u32>,
excerpts: Cursor<'a, Excerpt, Point>,
@@ -165,7 +169,7 @@ struct ExcerptChunks<'a> {
}
struct ExcerptBytes<'a> {
content_bytes: language::rope::Bytes<'a>,
content_bytes: text::Bytes<'a>,
footer_height: usize,
}
@@ -202,6 +206,7 @@ impl MultiBuffer {
last_selections_update_count: buffer_state.last_selections_update_count,
last_diagnostics_update_count: buffer_state.last_diagnostics_update_count,
last_file_update_count: buffer_state.last_file_update_count,
last_git_diff_update_count: buffer_state.last_git_diff_update_count,
excerpts: buffer_state.excerpts.clone(),
_subscriptions: [
new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()),
@@ -308,6 +313,17 @@ impl MultiBuffer {
self.read(cx).symbols_containing(offset, theme)
}
pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) {
let buffers = self.buffers.borrow();
for buffer_state in buffers.values() {
if buffer_state.buffer.read(cx).needs_git_diff_recalc() {
buffer_state
.buffer
.update(cx, |buffer, cx| buffer.git_diff_recalc(cx))
}
}
}
pub fn edit<I, S, T>(
&mut self,
edits: I,
@@ -588,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>>> =
@@ -652,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);
});
}
}
@@ -827,6 +844,7 @@ impl MultiBuffer {
last_selections_update_count: buffer_snapshot.selections_update_count(),
last_diagnostics_update_count: buffer_snapshot.diagnostics_update_count(),
last_file_update_count: buffer_snapshot.file_update_count(),
last_git_diff_update_count: buffer_snapshot.git_diff_update_count(),
excerpts: Default::default(),
_subscriptions: [
cx.observe(&buffer, |_, _, cx| cx.notify()),
@@ -1212,9 +1230,9 @@ impl MultiBuffer {
&self,
point: T,
cx: &'a AppContext,
) -> Option<&'a Arc<Language>> {
) -> Option<Arc<Language>> {
self.point_to_buffer_offset(point, cx)
.and_then(|(buffer, _)| buffer.read(cx).language())
.and_then(|(buffer, offset)| buffer.read(cx).language_at(offset))
}
pub fn files<'a>(&'a self, cx: &'a AppContext) -> SmallVec<[&'a dyn File; 2]> {
@@ -1249,6 +1267,7 @@ impl MultiBuffer {
let mut excerpts_to_edit = Vec::new();
let mut reparsed = false;
let mut diagnostics_updated = false;
let mut git_diff_updated = false;
let mut is_dirty = false;
let mut has_conflict = false;
let mut edited = false;
@@ -1260,6 +1279,7 @@ impl MultiBuffer {
let selections_update_count = buffer.selections_update_count();
let diagnostics_update_count = buffer.diagnostics_update_count();
let file_update_count = buffer.file_update_count();
let git_diff_update_count = buffer.git_diff_update_count();
let buffer_edited = version.changed_since(&buffer_state.last_version);
let buffer_reparsed = parse_count > buffer_state.last_parse_count;
@@ -1268,17 +1288,21 @@ impl MultiBuffer {
let buffer_diagnostics_updated =
diagnostics_update_count > buffer_state.last_diagnostics_update_count;
let buffer_file_updated = file_update_count > buffer_state.last_file_update_count;
let buffer_git_diff_updated =
git_diff_update_count > buffer_state.last_git_diff_update_count;
if buffer_edited
|| buffer_reparsed
|| buffer_selections_updated
|| buffer_diagnostics_updated
|| buffer_file_updated
|| buffer_git_diff_updated
{
buffer_state.last_version = version;
buffer_state.last_parse_count = parse_count;
buffer_state.last_selections_update_count = selections_update_count;
buffer_state.last_diagnostics_update_count = diagnostics_update_count;
buffer_state.last_file_update_count = file_update_count;
buffer_state.last_git_diff_update_count = git_diff_update_count;
excerpts_to_edit.extend(
buffer_state
.excerpts
@@ -1290,6 +1314,7 @@ impl MultiBuffer {
edited |= buffer_edited;
reparsed |= buffer_reparsed;
diagnostics_updated |= buffer_diagnostics_updated;
git_diff_updated |= buffer_git_diff_updated;
is_dirty |= buffer.is_dirty();
has_conflict |= buffer.has_conflict();
}
@@ -1302,6 +1327,9 @@ impl MultiBuffer {
if diagnostics_updated {
snapshot.diagnostics_update_count += 1;
}
if git_diff_updated {
snapshot.git_diff_update_count += 1;
}
snapshot.is_dirty = is_dirty;
snapshot.has_conflict = has_conflict;
@@ -1386,7 +1414,7 @@ impl MultiBuffer {
edit_count: usize,
cx: &mut ModelContext<Self>,
) {
use text::RandomCharIter;
use util::RandomCharIter;
let snapshot = self.read(cx);
let mut edits: Vec<(Range<usize>, Arc<str>)> = Vec::new();
@@ -1425,7 +1453,7 @@ impl MultiBuffer {
) {
use rand::prelude::*;
use std::env;
use text::RandomCharIter;
use util::RandomCharIter;
let max_excerpts = env::var("MAX_EXCERPTS")
.map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable"))
@@ -1940,6 +1968,24 @@ impl MultiBufferSnapshot {
}
}
pub fn point_to_buffer_offset<T: ToOffset>(
&self,
point: T,
) -> Option<(&BufferSnapshot, usize)> {
let offset = point.to_offset(&self);
let mut cursor = self.excerpts.cursor::<usize>();
cursor.seek(&offset, Bias::Right, &());
if cursor.item().is_none() {
cursor.prev(&());
}
cursor.item().map(|excerpt| {
let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
let buffer_point = excerpt_start + offset - *cursor.start();
(&excerpt.buffer, buffer_point)
})
}
pub fn suggested_indents(
&self,
rows: impl IntoIterator<Item = u32>,
@@ -1949,8 +1995,10 @@ impl MultiBufferSnapshot {
let mut rows_for_excerpt = Vec::new();
let mut cursor = self.excerpts.cursor::<Point>();
let mut rows = rows.into_iter().peekable();
let mut prev_row = u32::MAX;
let mut prev_language_indent_size = IndentSize::default();
while let Some(row) = rows.next() {
cursor.seek(&Point::new(row, 0), Bias::Right, &());
let excerpt = match cursor.item() {
@@ -1958,7 +2006,17 @@ impl MultiBufferSnapshot {
_ => continue,
};
let single_indent_size = excerpt.buffer.single_indent_size(cx);
// Retrieve the language and indent size once for each disjoint region being indented.
let single_indent_size = if row.saturating_sub(1) == prev_row {
prev_language_indent_size
} else {
excerpt
.buffer
.language_indent_size_at(Point::new(row, 0), cx)
};
prev_language_indent_size = single_indent_size;
prev_row = row;
let start_buffer_row = excerpt.range.context.start.to_point(&excerpt.buffer).row;
let start_multibuffer_row = cursor.start().row;
@@ -2479,15 +2537,17 @@ impl MultiBufferSnapshot {
self.diagnostics_update_count
}
pub fn git_diff_update_count(&self) -> usize {
self.git_diff_update_count
}
pub fn trailing_excerpt_update_count(&self) -> usize {
self.trailing_excerpt_update_count
}
pub fn language(&self) -> Option<&Arc<Language>> {
self.excerpts
.iter()
.next()
.and_then(|excerpt| excerpt.buffer.language())
pub fn language_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc<Language>> {
self.point_to_buffer_offset(point)
.and_then(|(buffer, offset)| buffer.language_at(offset))
}
pub fn is_dirty(&self) -> bool {
@@ -2529,6 +2589,15 @@ impl MultiBufferSnapshot {
})
}
pub fn git_diff_hunks_in_range<'a>(
&'a self,
row_range: Range<u32>,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
self.as_singleton()
.into_iter()
.flat_map(move |(_, _, buffer)| buffer.git_diff_hunks_in_range(row_range.clone()))
}
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
let range = range.start.to_offset(self)..range.end.to_offset(self);
@@ -2630,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
@@ -2647,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),
@@ -2669,6 +2738,7 @@ impl MultiBufferSnapshot {
(
replica_id,
line_mode,
cursor_shape,
Selection {
id: selection.id,
start,
@@ -3270,7 +3340,7 @@ mod tests {
use rand::prelude::*;
use settings::Settings;
use std::{env, rc::Rc};
use text::{Point, RandomCharIter};
use util::test::sample_text;
#[gpui::test]
@@ -3888,7 +3958,9 @@ mod tests {
}
_ => {
let buffer_handle = if buffers.is_empty() || rng.gen_bool(0.4) {
let base_text = RandomCharIter::new(&mut rng).take(10).collect::<String>();
let base_text = util::RandomCharIter::new(&mut rng)
.take(10)
.collect::<String>();
buffers.push(cx.add_model(|cx| Buffer::new(0, base_text, cx)));
buffers.last().unwrap()
} else {

View File

@@ -1,10 +1,10 @@
use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToOffsetUtf16, ToPoint};
use language::{OffsetUtf16, Point, TextDimension};
use std::{
cmp::Ordering,
ops::{Range, Sub},
};
use sum_tree::Bias;
use text::{rope::TextDimension, OffsetUtf16, Point};
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
pub struct Anchor {

View File

@@ -8,7 +8,7 @@ use std::{
use collections::HashMap;
use gpui::{AppContext, ModelHandle, MutableAppContext};
use itertools::Itertools;
use language::{rope::TextDimension, Bias, Point, Selection, SelectionGoal, ToPoint};
use language::{Bias, Point, Selection, SelectionGoal, TextDimension, ToPoint};
use util::post_inc;
use crate::{

View File

@@ -1,28 +1,14 @@
pub mod editor_lsp_test_context;
pub mod editor_test_context;
use crate::{
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
multi_buffer::ToPointUtf16,
AnchorRangeExt, Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer, ToPoint,
DisplayPoint, Editor, EditorMode, MultiBuffer,
};
use anyhow::Result;
use futures::{Future, StreamExt};
use gpui::{
json, keymap::Keystroke, AppContext, ModelContext, ModelHandle, ViewContext, ViewHandle,
};
use indoc::indoc;
use language::{point_to_lsp, Buffer, BufferSnapshot, FakeLspAdapter, Language, LanguageConfig};
use lsp::{notification, request};
use project::Project;
use settings::Settings;
use std::{
any::TypeId,
ops::{Deref, DerefMut, Range},
sync::Arc,
};
use util::{
assert_set_eq, set_eq,
test::{generate_marked_text, marked_text_offsets, marked_text_ranges},
};
use workspace::{pane, AppState, Workspace, WorkspaceHandle};
use gpui::{ModelHandle, ViewContext};
use util::test::{marked_text_offsets, marked_text_ranges};
#[cfg(test)]
#[ctor::ctor]
@@ -80,430 +66,3 @@ pub(crate) fn build_editor(
) -> Editor {
Editor::new(EditorMode::Full, buffer, None, None, cx)
}
pub struct EditorTestContext<'a> {
pub cx: &'a mut gpui::TestAppContext,
pub window_id: usize,
pub editor: ViewHandle<Editor>,
}
impl<'a> EditorTestContext<'a> {
pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
let (window_id, editor) = cx.update(|cx| {
cx.set_global(Settings::test(cx));
crate::init(cx);
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
build_editor(MultiBuffer::build_simple("", cx), cx)
});
editor.update(cx, |_, cx| cx.focus_self());
(window_id, editor)
});
Self {
cx,
window_id,
editor,
}
}
pub fn condition(
&self,
predicate: impl FnMut(&Editor, &AppContext) -> bool,
) -> impl Future<Output = ()> {
self.editor.condition(self.cx, predicate)
}
pub fn editor<F, T>(&self, read: F) -> T
where
F: FnOnce(&Editor, &AppContext) -> T,
{
self.editor.read_with(self.cx, read)
}
pub fn update_editor<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
{
self.editor.update(self.cx, update)
}
pub fn multibuffer<F, T>(&self, read: F) -> T
where
F: FnOnce(&MultiBuffer, &AppContext) -> T,
{
self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
}
pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
{
self.update_editor(|editor, cx| editor.buffer().update(cx, update))
}
pub fn buffer_text(&self) -> String {
self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
}
pub fn buffer<F, T>(&self, read: F) -> T
where
F: FnOnce(&Buffer, &AppContext) -> T,
{
self.multibuffer(|multibuffer, cx| {
let buffer = multibuffer.as_singleton().unwrap().read(cx);
read(buffer, cx)
})
}
pub fn update_buffer<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
{
self.update_multibuffer(|multibuffer, cx| {
let buffer = multibuffer.as_singleton().unwrap();
buffer.update(cx, update)
})
}
pub fn buffer_snapshot(&self) -> BufferSnapshot {
self.buffer(|buffer, _| buffer.snapshot())
}
pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
let keystroke = Keystroke::parse(keystroke_text).unwrap();
self.cx.dispatch_keystroke(self.window_id, keystroke, false);
}
pub fn simulate_keystrokes<const COUNT: usize>(&mut self, keystroke_texts: [&str; COUNT]) {
for keystroke_text in keystroke_texts.into_iter() {
self.simulate_keystroke(keystroke_text);
}
}
pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
assert_eq!(self.buffer_text(), unmarked_text);
ranges
}
pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
let ranges = self.ranges(marked_text);
let snapshot = self
.editor
.update(self.cx, |editor, cx| editor.snapshot(cx));
ranges[0].start.to_display_point(&snapshot)
}
// Returns anchors for the current buffer using `«` and `»`
pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> {
let ranges = self.ranges(marked_text);
let snapshot = self.buffer_snapshot();
snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
}
/// Change the editor's text and selections using a string containing
/// embedded range markers that represent the ranges and directions of
/// each selection.
///
/// See the `util::test::marked_text_ranges` function for more information.
pub fn set_state(&mut self, marked_text: &str) {
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
self.editor.update(self.cx, |editor, cx| {
editor.set_text(unmarked_text, cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.select_ranges(selection_ranges)
})
})
}
/// Make an assertion about the editor's text and the ranges and directions
/// of its selections using a string containing embedded range markers.
///
/// See the `util::test::marked_text_ranges` function for more information.
pub fn assert_editor_state(&mut self, marked_text: &str) {
let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
let buffer_text = self.buffer_text();
assert_eq!(
buffer_text, unmarked_text,
"Unmarked text doesn't match buffer text"
);
self.assert_selections(expected_selections, marked_text.to_string())
}
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text);
let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
editor
.background_highlights
.get(&TypeId::of::<Tag>())
.map(|h| h.1.clone())
.unwrap_or_default()
.into_iter()
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
.collect()
});
assert_set_eq!(actual_ranges, expected_ranges);
}
pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text);
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let actual_ranges: Vec<Range<usize>> = snapshot
.highlight_ranges::<Tag>()
.map(|ranges| ranges.as_ref().clone().1)
.unwrap_or_default()
.into_iter()
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
.collect();
assert_set_eq!(actual_ranges, expected_ranges);
}
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
let expected_marked_text =
generate_marked_text(&self.buffer_text(), &expected_selections, true);
self.assert_selections(expected_selections, expected_marked_text)
}
fn assert_selections(
&mut self,
expected_selections: Vec<Range<usize>>,
expected_marked_text: String,
) {
let actual_selections = self
.editor
.read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
.into_iter()
.map(|s| {
if s.reversed {
s.end..s.start
} else {
s.start..s.end
}
})
.collect::<Vec<_>>();
let actual_marked_text =
generate_marked_text(&self.buffer_text(), &actual_selections, true);
if expected_selections != actual_selections {
panic!(
indoc! {"
Editor has unexpected selections.
Expected selections:
{}
Actual selections:
{}
"},
expected_marked_text, actual_marked_text,
);
}
}
}
impl<'a> Deref for EditorTestContext<'a> {
type Target = gpui::TestAppContext;
fn deref(&self) -> &Self::Target {
self.cx
}
}
impl<'a> DerefMut for EditorTestContext<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
}
pub struct EditorLspTestContext<'a> {
pub cx: EditorTestContext<'a>,
pub lsp: lsp::FakeLanguageServer,
pub workspace: ViewHandle<Workspace>,
pub buffer_lsp_url: lsp::Url,
}
impl<'a> EditorLspTestContext<'a> {
pub async fn new(
mut language: Language,
capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> {
use json::json;
cx.update(|cx| {
crate::init(cx);
pane::init(cx);
});
let params = cx.update(AppState::test);
let file_name = format!(
"file.{}",
language
.path_suffixes()
.first()
.unwrap_or(&"txt".to_string())
);
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities,
..Default::default()
}))
.await;
let project = Project::test(params.fs.clone(), [], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
params
.fs
.as_fake()
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
.await;
let (window_id, workspace) =
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx)
})
.await
.unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
let item = workspace
.update(cx, |workspace, cx| workspace.open_path(file, true, cx))
.await
.expect("Could not open test file");
let editor = cx.update(|cx| {
item.act_as::<Editor>(cx)
.expect("Opened test file wasn't an editor")
});
editor.update(cx, |_, cx| cx.focus_self());
let lsp = fake_servers.next().await.unwrap();
Self {
cx: EditorTestContext {
cx,
window_id,
editor,
},
lsp,
workspace,
buffer_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
}
}
pub async fn new_rust(
capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> {
let language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
);
Self::new(language, capabilities, cx).await
}
// Constructs lsp range using a marked string with '[', ']' range delimiters
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
let ranges = self.ranges(marked_text);
self.to_lsp_range(ranges[0].clone())
}
pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let start_point = range.start.to_point(&snapshot.buffer_snapshot);
let end_point = range.end.to_point(&snapshot.buffer_snapshot);
self.editor(|editor, cx| {
let buffer = editor.buffer().read(cx);
let start = point_to_lsp(
buffer
.point_to_buffer_offset(start_point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
);
let end = point_to_lsp(
buffer
.point_to_buffer_offset(end_point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
);
lsp::Range { start, end }
})
}
pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let point = offset.to_point(&snapshot.buffer_snapshot);
self.editor(|editor, cx| {
let buffer = editor.buffer().read(cx);
point_to_lsp(
buffer
.point_to_buffer_offset(point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
)
})
}
pub fn update_workspace<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
{
self.workspace.update(self.cx.cx, update)
}
pub fn handle_request<T, F, Fut>(
&self,
mut handler: F,
) -> futures::channel::mpsc::UnboundedReceiver<()>
where
T: 'static + request::Request,
T::Params: 'static + Send,
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
Fut: 'static + Send + Future<Output = Result<T::Result>>,
{
let url = self.buffer_lsp_url.clone();
self.lsp.handle_request::<T, _, _>(move |params, cx| {
let url = url.clone();
handler(url, params, cx)
})
}
pub fn notify<T: notification::Notification>(&self, params: T::Params) {
self.lsp.notify::<T>(params);
}
}
impl<'a> Deref for EditorLspTestContext<'a> {
type Target = EditorTestContext<'a>;
fn deref(&self) -> &Self::Target {
&self.cx
}
}
impl<'a> DerefMut for EditorLspTestContext<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
}

View File

@@ -0,0 +1,208 @@
use std::{
ops::{Deref, DerefMut, Range},
sync::Arc,
};
use anyhow::Result;
use futures::Future;
use gpui::{json, ViewContext, ViewHandle};
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig};
use lsp::{notification, request};
use project::Project;
use smol::stream::StreamExt;
use workspace::{pane, AppState, Workspace, WorkspaceHandle};
use crate::{multi_buffer::ToPointUtf16, Editor, ToPoint};
use super::editor_test_context::EditorTestContext;
pub struct EditorLspTestContext<'a> {
pub cx: EditorTestContext<'a>,
pub lsp: lsp::FakeLanguageServer,
pub workspace: ViewHandle<Workspace>,
pub buffer_lsp_url: lsp::Url,
}
impl<'a> EditorLspTestContext<'a> {
pub async fn new(
mut language: Language,
capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> {
use json::json;
cx.update(|cx| {
crate::init(cx);
pane::init(cx);
});
let params = cx.update(AppState::test);
let file_name = format!(
"file.{}",
language
.path_suffixes()
.first()
.unwrap_or(&"txt".to_string())
);
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities,
..Default::default()
}))
.await;
let project = Project::test(params.fs.clone(), [], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
params
.fs
.as_fake()
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
.await;
let (window_id, workspace) =
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx)
})
.await
.unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
let item = workspace
.update(cx, |workspace, cx| workspace.open_path(file, true, cx))
.await
.expect("Could not open test file");
let editor = cx.update(|cx| {
item.act_as::<Editor>(cx)
.expect("Opened test file wasn't an editor")
});
editor.update(cx, |_, cx| cx.focus_self());
let lsp = fake_servers.next().await.unwrap();
Self {
cx: EditorTestContext {
cx,
window_id,
editor,
},
lsp,
workspace,
buffer_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
}
}
pub async fn new_rust(
capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> {
let language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
);
Self::new(language, capabilities, cx).await
}
// Constructs lsp range using a marked string with '[', ']' range delimiters
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
let ranges = self.ranges(marked_text);
self.to_lsp_range(ranges[0].clone())
}
pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let start_point = range.start.to_point(&snapshot.buffer_snapshot);
let end_point = range.end.to_point(&snapshot.buffer_snapshot);
self.editor(|editor, cx| {
let buffer = editor.buffer().read(cx);
let start = point_to_lsp(
buffer
.point_to_buffer_offset(start_point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
);
let end = point_to_lsp(
buffer
.point_to_buffer_offset(end_point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
);
lsp::Range { start, end }
})
}
pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let point = offset.to_point(&snapshot.buffer_snapshot);
self.editor(|editor, cx| {
let buffer = editor.buffer().read(cx);
point_to_lsp(
buffer
.point_to_buffer_offset(point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
)
})
}
pub fn update_workspace<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
{
self.workspace.update(self.cx.cx, update)
}
pub fn handle_request<T, F, Fut>(
&self,
mut handler: F,
) -> futures::channel::mpsc::UnboundedReceiver<()>
where
T: 'static + request::Request,
T::Params: 'static + Send,
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
Fut: 'static + Send + Future<Output = Result<T::Result>>,
{
let url = self.buffer_lsp_url.clone();
self.lsp.handle_request::<T, _, _>(move |params, cx| {
let url = url.clone();
handler(url, params, cx)
})
}
pub fn notify<T: notification::Notification>(&self, params: T::Params) {
self.lsp.notify::<T>(params);
}
}
impl<'a> Deref for EditorLspTestContext<'a> {
type Target = EditorTestContext<'a>;
fn deref(&self) -> &Self::Target {
&self.cx
}
}
impl<'a> DerefMut for EditorLspTestContext<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
}

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