Compare commits

...

209 Commits

Author SHA1 Message Date
Joseph Lyons
67c046239e v0.76.x preview 2023-03-08 13:25:18 -05:00
Julia
4179ed66a6 Merge pull request #2251 from zed-industries/clear-follow-state-on-project-close
Update db followers table when user leaves a project
2023-03-07 19:09:09 -05:00
Julia
d173b1d412 Update db followers table when user leaves a project
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-03-07 18:56:03 -05:00
Julia
4f4af55329 Merge pull request #2248 from zed-industries/increase-reconnect-timeout
Increase room reconnect timeout
2023-03-07 12:30:20 -05:00
Julia
1e5aff9e51 Update collab integration test to new reconnect timeout 2023-03-07 12:23:18 -05:00
Nate Butler
ee154feda4 Merge pull request #2231 from zed-industries/nate/add-gruvbox
Add Gruvbox Theme
2023-03-06 18:49:26 -08:00
Max Brunsfeld
7163ba429b Merge pull request #2250 from zed-industries/syntax-highlighting-tweaks
Syntax highlighting tweaks
2023-03-06 16:05:10 -08:00
Max Brunsfeld
c832e4406e Remove more colors from default syntax theme
These default colors weren't consistent with the rest of the
syntax theme.
2023-03-06 15:57:04 -08:00
Max Brunsfeld
515724821e Make racket highlight query more consistent with other langs 2023-03-06 15:56:20 -08:00
Max Brunsfeld
0867162c87 Fix lua highlight query
* Fix regex predicate on constants.
* Remove 'function.call' highlight name. In other languages, we
differentiate calls from definitions with the 'function.definition'
highlight name. We actually don't use this in any themes though.
2023-03-06 15:54:17 -08:00
Max Brunsfeld
aba2914a31 Fix constant highlighting in JS, TypeScript
Previously, SCREAMING_SNAKE_CASE identifiers were highlighted as
types due to a mistake in the order of patterns in the highlight
queries.
2023-03-06 15:38:12 -08:00
Max Brunsfeld
246a6adab7 Merge pull request #2239 from zed-industries/add-constructor-to-syntax-overrides
Add constructor to syntax overrides
2023-03-06 15:27:09 -08:00
Julia
620890c411 Merge pull request #2247 from zed-industries/tidy-up-sprite-cache
Tidy up `SpriteCache::render_glyph`
2023-03-06 11:29:51 -05:00
Antonio Scandurra
0ec984f924 Tidy up SpriteCache::render_glyph 2023-03-06 17:08:35 +01:00
Max Brunsfeld
01bbf20962 Merge pull request #2235 from zed-industries/no-panic-uploads-in-debug
Don't upload panic files when running in a PTY
2023-03-04 09:42:46 -08:00
Mikayla Maki
996294ba67 Merge pull request #2246 from zed-industries/fix-lsp-derive-error
Make diagnostic processing order independent
2023-03-04 02:25:47 -08:00
Mikayla Maki
ddf2f2cb0a Update test to use new group ids and new ordering of diagnostics. 2023-03-04 02:21:55 -08:00
Mikayla Maki
bd4d7551a5 Make diagnostic processing order independent
Co-authored-by: max <max@zed.dev>
2023-03-03 16:35:12 -08:00
Julia
5097cf5cb7 Merge pull request #2245 from zed-industries/confirm-restart-unsaved
Confirm restart if prompt-quit is enabled or there are unsaved changes
2023-03-03 16:10:40 -08:00
Julia
13212d274e Confirm restart if prompt-quit is enabled or there are unsaved changes 2023-03-03 16:06:03 -08:00
Julia
b9110c9268 Increase reconnect timeout
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-03-03 13:10:08 -08:00
Joseph T. Lyons
b9573872e1 Merge pull request #2243 from zed-industries/joseph/z-225-track-open-terminals
Keep track of open terminals
2023-03-03 12:55:28 -08:00
Joseph Lyons
3ec71a742d Keep track of open terminals
Co-Authored-By: Petros Amoiridis <petros@hey.com>
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-03-03 12:50:08 -08:00
Mikayla Maki
50682dc685 Merge pull request #2233 from zed-industries/fix-code-folding-initialization
Initialize code fold indicators on buffer startup
2023-03-03 12:30:53 -08:00
Julia
2bca64f13b Merge pull request #2242 from zed-industries/mouse-event-handlers-yes
Fix surprising mouse propagation & avoid focusing tab while closing
2023-03-03 12:29:50 -08:00
Mikayla Maki
606d683f29 Add interactable fold markers
Change fold handlers to be driven by the fold map
Switch to a mouse region based implementation for click regions

Co-authored-by: Max <max@zed.dev>
2023-03-03 12:26:36 -08:00
Julia
ff2e6bc3bd Fix surprising mouse propagation & avoid focusing tab while closing
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-03-03 12:22:58 -08:00
Joseph T. Lyons
218f2fd0fe Merge pull request #2241 from zed-industries/joseph/z-223-add-terminal-button-to-status-bar
Add a terminal button to status bar
2023-03-03 12:15:59 -08:00
Max Brunsfeld
bb0257bbac Merge branch 'main' into no-panic-uploads-in-debug 2023-03-03 12:02:44 -08:00
Joseph Lyons
929ebd7175 Add a terminal button to status bar
Co-Authored-By: Petros Amoiridis <petros@hey.com>
2023-03-03 12:00:41 -08:00
Max Brunsfeld
124aa74b03 Merge pull request #2234 from zed-industries/error-on-combined-injections-in-injections
Fix range relativization when combined injections occur inside of other injections
2023-03-03 11:58:13 -08:00
Petros Amoiridis
a2f75eb031 Merge pull request #2240 from zed-industries/petros/update-setup-instructions
Add missing steps to the setup instructions
2023-03-03 11:47:38 -08:00
Petros Amoiridis
6194c5df16 Add missing steps to the setup instructions 2023-03-03 11:36:26 -08:00
Max Brunsfeld
d14b684237 Merge pull request #2236 from zed-industries/avoid-clobbering-panic-file
Avoid clobbering panic files when they happen at the same time
2023-03-03 11:15:48 -08:00
Max Brunsfeld
7a8cba0544 Merge pull request #2227 from zed-industries/strip-trailing-whitespace
Add settings to normalize whitespace on save
2023-03-03 11:15:32 -08:00
Nate Butler
f1b5bf051a Fornat 2023-03-03 10:58:43 -08:00
Nate Butler
ad4201f768 Hack to fix syntax.constructor causing TS error 2023-03-03 10:58:25 -08:00
Nate Butler
75a9cfdabe Remove leftover log 2023-03-03 10:22:50 -08:00
Julia
3b6f66791f Merge pull request #2238 from zed-industries/subpixel-variant-incorrectly-wrapping
Avoid wrapping to the 0th glyph variant when the 4th should be used
2023-03-03 13:13:34 -05:00
Julia
9311e01271 Avoid wrapping to the 0th glyph variant when the 4th should be used
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-03-03 10:04:44 -08:00
Nate Butler
6d068e926b Merge pull request #2237 from zed-industries/revert-2232-tab-bar-background-focus-drag
Revert "Avoid tab bar background activating an item at the end of a tab drag"
2023-03-03 09:49:55 -08:00
Nate Butler
6854063d0b Revert "Avoid tab bar background activating an item at the end of a tab drag" 2023-03-03 09:47:58 -08:00
Mikayla Maki
7ca0b38048 Made fold inline styles be driven by the fold map
co-authored-by: Max <max@zed.dev>
2023-03-03 09:32:58 -08:00
Max Brunsfeld
a598f0b13c Avoid clobbering panic files when they happen at the same time
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2023-03-02 20:51:29 -08:00
Max Brunsfeld
eb6088701e Don't upload panic files when running in a PTY 2023-03-02 20:32:49 -08:00
Mikayla Maki
24ba47e75d Merge pull request #2230 from zed-industries/fix-tab-x
Fix tab bar x
2023-03-02 19:48:08 -08:00
Mikayla Maki
3dd5b3f426 Attempted to initialize code-fold indicators, does not work 2023-03-02 19:42:59 -08:00
Mikayla Maki
9f86ca8574 Update tabBar.ts 2023-03-02 15:48:31 -08:00
Julia
bc2ea58c6a Merge pull request #2232 from zed-industries/tab-bar-background-focus-drag
Avoid tab bar background activating an item at the end of a tab drag
2023-03-02 16:18:04 -05:00
Julia
b343e8056a Avoid tab bar background activating an item at the end of a tab drag 2023-03-02 13:06:58 -08:00
Mikayla Maki
6a2a1303c4 Fix failing license
co-authored-by: nate <nate@zed.dev>
2023-03-02 12:32:47 -08:00
Max Brunsfeld
a366ba19af Fix range relativization when combined injections occur inside of other injections
For example, ERB template inside of a markdown code block

Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2023-03-01 14:38:35 -08:00
Max Brunsfeld
70cb2fa8d7 Apply external command formatting if buffer has changed while computing it 2023-03-01 10:17:04 -08:00
Nate Butler
f67c3f1f1d Add Gruvbox syntax overrides 2023-03-01 11:49:13 -05:00
Nate Butler
bde0456111 Init gruvbox theme 2023-03-01 10:16:45 -05:00
Mikayla Maki
8734bd8435 Seperate out x-mark width 2023-03-01 00:18:45 -08:00
Mikayla Maki
34fbffb4cc Fix tab bar x 2023-02-28 22:48:31 -08:00
Max Brunsfeld
368d2a73ea Perform whitespace formatting regardless of whether buffer has a language server or path 2023-02-28 21:52:00 -08:00
Max Brunsfeld
e7b56f6342 adjust buffer-formatting assertion to reflect final newline addition 2023-02-28 21:52:00 -08:00
Max Brunsfeld
1deff43639 Avoid calling edits_since in apply_diff 2023-02-28 21:52:00 -08:00
Max Brunsfeld
a890b8f3b7 Add a setting for ensuring a single final newline on save 2023-02-28 21:52:00 -08:00
Max Brunsfeld
7faa0da5c7 Avoid finalizing transactions inside Buffer::apply_diff 2023-02-28 21:52:00 -08:00
Max Brunsfeld
ff85bc6d42 Add setting for removing trailing whitespace on save 2023-02-28 21:52:00 -08:00
Max Brunsfeld
b00e467ede Add APIs for stripping trailing whitespace from a buffer 2023-02-28 21:52:00 -08:00
Mikayla Maki
2e1adb0724 Merge pull request #2229 from zed-industries/fix-click-range-bug
Fix off by one error in click ranges
2023-02-28 20:33:00 -08:00
Mikayla Maki
269df10a16 Fix off by one error in click ranges 2023-02-28 20:27:34 -08:00
Mikayla Maki
8358efbd6c Merge pull request #2228 from zed-industries/make-folds-show-on-active-lines
Made code fold indicators show up on active line indicators
2023-02-28 20:01:09 -08:00
Mikayla Maki
dc11d2726e Made code fold indicators show up on active line indicators 2023-02-28 19:53:53 -08:00
Mikayla Maki
41d3c5287b Merge pull request #2220 from zed-industries/add-fold-indicators
Add code folding indicators into the gutter.
2023-02-28 17:30:17 -08:00
Mikayla Maki
2198c295b3 Fix comments 2023-02-28 17:25:35 -08:00
Mikayla Maki
6cf62a5b02 Update tests to use new fold indicator 2023-02-28 17:17:29 -08:00
Mikayla Maki
f8401394f5 Removed DisplayRow abstraction 2023-02-28 17:12:48 -08:00
Mikayla Maki
b53d1eef71 Added background styling of the ... 2023-02-28 16:35:44 -08:00
Mikayla Maki
c397fd9a71 Added click regions and cursor styles 2023-02-28 16:34:28 -08:00
Mikayla Maki
9b8adecf05 Adjusted code-folding behavior 2023-02-28 16:34:28 -08:00
Mikayla Maki
e0f553c0f5 WIp 2023-02-28 16:34:28 -08:00
Mikayla Maki
37a2ef9d41 Make chevrons and lightning bolt interactive 2023-02-28 16:34:28 -08:00
Mikayla Maki
89b93d4f6f Added fold changes on gutter hover 2023-02-28 16:34:28 -08:00
Mikayla Maki
2036fc48b5 moved code action indicator to the left 2023-02-28 16:34:28 -08:00
Mikayla Maki
cb3e873a67 Fixed autoscroll timing 2023-02-28 16:34:28 -08:00
Mikayla Maki
da78abd99f Added DisplayRow abstraction to make folding code more readable 2023-02-28 16:34:28 -08:00
Mikayla Maki
637e8ada42 Fix bugs in code folding 2023-02-28 16:34:28 -08:00
Mikayla Maki
e3061066c9 Add code folding indicators into the gutter. 2023-02-28 16:34:28 -08:00
Nate Butler
514da604d7 Merge pull request #2223 from zed-industries/nate/extend-syntax
Extend syntax styles available in the theme
2023-02-28 11:26:47 -05:00
Nate Butler
b9811e48e4 One family tune up + tidy 2023-02-28 11:20:16 -05:00
Nate Butler
fb69611568 Fix order of template_substitution in highlights.scm 2023-02-28 11:19:55 -05:00
Nate Butler
a8a045e8bf Merge branch 'main' into nate/extend-syntax 2023-02-28 11:07:35 -05:00
Nate Butler
59bd503696 Tidy theme names 2023-02-28 10:55:08 -05:00
Nate Butler
fb7818f93c Revert changes to Andromeda 2023-02-28 10:50:03 -05:00
Joseph T. Lyons
3fb426e8b2 Merge pull request #2209 from zed-industries/add-link-to-community-repo-in-feedback-editor
Add link to community repo in feedback editor text
2023-02-28 09:03:21 -05:00
Kay Simmons
f0a31f86c7 remove commented line 2023-02-27 12:06:10 -08:00
Kay Simmons
dc7fe72f18 Merge pull request #2226 from zed-industries/fix-infinite-loop-in-path-distance
fix infinite loop in path distance for fuzzy finder
2023-02-27 12:03:40 -08:00
Kay Simmons
b3dffeaf2a fix infinite loop in path distance for fuzzy finder 2023-02-27 11:57:21 -08:00
Kay Simmons
81cbefec22 Merge pull request #2225 from zed-industries/vim-go-to-definition
add go to definition binding to vim normal mode
2023-02-27 11:10:23 -08:00
Joseph Lyons
4f9a07cffc Merge branch 'main' into add-link-to-community-repo-in-feedback-editor 2023-02-27 13:56:01 -05:00
Kay Simmons
184f37015a add go to definition binding to vim normal mode 2023-02-27 10:23:30 -08:00
Nate Butler
c9997a81a3 Move syntax types to syntax.ts 2023-02-27 11:01:32 -05:00
Nate Butler
df798c1a7f Remove punctuation.special until bug is fixed 2023-02-27 10:52:53 -05:00
Nate Butler
465fcec36d Format 2023-02-27 10:48:55 -05:00
Nate Butler
40c2409b80 Add missing variable property to Syntax 2023-02-27 10:47:52 -05:00
Nate Butler
46dc347a1a Bring andromeda in line with it's correct colors 2023-02-27 10:47:30 -05:00
Nate Butler
f84046b74f use @boolean in all flavors of js 2023-02-27 10:40:01 -05:00
Nate Butler
8c51a62a8d Unify regex highlight style under @string.regex 2023-02-27 10:08:24 -05:00
Nate Butler
794e6e22a6 Format 2023-02-26 17:12:48 -05:00
Nate Butler
504d88d56c Remove unused code 2023-02-26 17:12:11 -05:00
Nate Butler
94c76c45e6 Style One Dark with new properties 2023-02-26 17:11:02 -05:00
Nate Butler
f2d6a03dff Finish adding default properties 2023-02-26 17:10:52 -05:00
Nate Butler
3b19a409f8 Add some comments 2023-02-26 15:01:18 -05:00
Nate Butler
7854f4a1ef WIP need to finish default styles 2023-02-26 14:57:43 -05:00
Nate Butler
6cb35536b3 Start on syntax organization 2023-02-26 14:15:06 -05:00
Nate Butler
161373710c WIP 2023-02-26 13:24:41 -05:00
Joseph T. Lyons
11e2caff15 Merge pull request #2222 from zed-industries/update-app-icons
Update app icons
2023-02-26 11:07:07 -05:00
Joseph Lyons
36ada13966 Update app-icon@2x.png 2023-02-26 11:03:43 -05:00
Joseph Lyons
2c61eeb56d Update app-icon.png 2023-02-26 11:03:31 -05:00
Joseph Lyons
ccae9448d4 Update app-icon-preview@2x.png 2023-02-26 11:03:22 -05:00
Joseph Lyons
bb46b26494 Update app-icon-preview.png 2023-02-26 11:03:03 -05:00
Nate Butler
098e6969f7 Merge pull request #2221 from zed-industries/theme-syntax-overrides
Theme syntax overrides 🎉
2023-02-26 10:56:45 -05:00
Nate Butler
d910eed1f1 Format 2023-02-26 10:31:19 -05:00
Nate Butler
64b07dbfeb Add syntax overrides for One Light 2023-02-26 10:09:16 -05:00
Nate Butler
4f307c7601 Use syntax as merged name instead of mergedSyntax
This lets us retain the existing uses of syntax, and fixes colors that were being assigned incorrectly.
2023-02-26 01:20:44 -05:00
Nate Butler
23c967418a Add a syntax override style for One Dark 2023-02-26 01:07:45 -05:00
Nate Butler
77ed437cda Add the ability to override the system syntax config 2023-02-26 01:07:21 -05:00
Kay Simmons
0b1334b8c5 Merge pull request #2218 from zed-industries/file-finder-distance-sorting
Sort matches in file finder by distance to active item after score
2023-02-25 14:26:05 -08:00
Kay Simmons
cdc6566d87 fixup poor utility naming 2023-02-25 14:12:25 -08:00
Kay Simmons
36f3d3d738 Add test for new sorting behavior 2023-02-25 14:06:54 -08:00
Nate Butler
27712c25ef Merge pull request #2219 from zed-industries/nate/theme-tidying
Tidy `styles` app
2023-02-25 12:07:02 -05:00
Nate Butler
68af726ee4 Update packages
Tested post update.
2023-02-25 11:53:01 -05:00
Nate Butler
0ea7959ba4 Remove unused/old base16 theme code
Neither of these files have anything in them that is used anywhere else in the styles app.

Tested both `build` and `build-licenses` after removing these.
2023-02-25 11:50:22 -05:00
Nate Butler
bcb7b80517 Don't format package or package-lock 2023-02-25 11:47:27 -05:00
Nate Butler
10a30cf330 Format styles with updated prettier config
In the system color PR I updated the prettier config to match what we use on zed.dev. I didn't want to format all of styles as it would add a lot of unrelated line changes to that PR.

Doing that format now.
2023-02-25 11:46:33 -05:00
Nate Butler
06a86162bb Merge pull request #2150 from zed-industries/nate/system-colors
Add system color palette
2023-02-25 11:43:02 -05:00
Nate Butler
b986c38a31 Format using new prettier config 2023-02-25 11:33:57 -05:00
Nate Butler
69fd273367 Add the same prettier config as zed.dev 2023-02-25 11:33:16 -05:00
Nate Butler
8e828947fb Add missing dep 2023-02-25 11:32:04 -05:00
Nate Butler
2d8adf4c56 Remove theme tool for now
This is likely the biggest thing I'm unsure about for this PR, so I'm going to pull it into a seperate branch so I can merge the system color library in.

I think a better approach for this will be one of two things:
- Have a single next app that covers everything to do with themes (previewing ramps, generated themes, components etc.)
- Create a mono or turborepo that splits things up into packages (system, theme, themes, theme-tool, etc)
2023-02-25 11:27:18 -05:00
Kay Simmons
0b48e238f2 Sort file finder matches by distance to the active item after match score 2023-02-24 18:13:26 -08:00
Max Brunsfeld
04495aa8cd Merge pull request #2217 from zed-industries/format-on-save-trigger
Pass the 'Save' format trigger when formatting on save
2023-02-24 17:29:52 -08:00
Max Brunsfeld
5fea49e639 Pass the 'Save' format trigger when formatting on save
In an earlier refactor, I accidentally caused the 'Manual'
trigger to *always* be passed.

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2023-02-24 17:11:35 -08:00
Mikayla Maki
0704d9dcdb Merge pull request #2216 from zed-industries/update-bromberg
Update bromberg dependency to have alloc
2023-02-24 14:41:32 -08:00
Mikayla Maki
a57fcf5afc Update bromberg dependency to have alloc 2023-02-24 14:34:24 -08:00
Max Brunsfeld
e910fd8493 collab 0.6.1 2023-02-24 09:44:23 -08:00
Mikayla Maki
d5123bc832 Merge pull request #2215 from zed-industries/revert-2214-fix-reveal-path-panic
Revert "Remove borrow from reveal_path()"
2023-02-24 09:40:03 -08:00
Mikayla Maki
8656708de4 Revert "Remove borrow from reveal_path()" 2023-02-24 09:39:52 -08:00
Joseph Lyons
72197802a2 Tweak code to remove duplication 2023-02-24 08:53:58 -05:00
Joseph Lyons
f8f1a3f86e Unify text style names 2023-02-24 08:46:28 -05:00
Mikayla Maki
2ec25bef84 Merge pull request #2214 from zed-industries/fix-reveal-path-panic
Remove borrow from reveal_path()
2023-02-23 21:48:09 -08:00
Mikayla Maki
89ddf14b0e Remove borrow from reveal_path() 2023-02-23 21:36:17 -08:00
Julia
be86cb35ba Merge pull request #2213 from zed-industries/per-project-follow-status
Differentiate between follow state on a per-project basis
2023-02-24 00:26:41 -05:00
Julia
465d8cc2ff Differentiate between follow state on a per-project basis 2023-02-24 00:07:17 -05:00
Max Brunsfeld
93b9e762ec Merge pull request #2212 from zed-industries/initial-traffic-light-position
Adjust window's traffic light position when setting its title
2023-02-23 15:02:05 -08:00
Max Brunsfeld
fbc934b884 Adjust window's traffic light position when setting its title 2023-02-23 14:44:45 -08:00
Mikayla Maki
350b7b82f7 Merge pull request #2207 from zed-industries/project-panel-actions
Fix project panel actions
2023-02-23 14:31:29 -08:00
Mikayla Maki
b179fc2b99 Remove comment 2023-02-23 14:24:53 -08:00
Mikayla Maki
8860346324 Fix broken dock command 2023-02-23 14:15:29 -08:00
Mikayla Maki
9004640586 Convert keymap context to use generics and Cow<'static> so we don't have to add .to_string() and .into() for each usage 2023-02-23 14:10:55 -08:00
Kay Simmons
03498314fa Merge pull request #2211 from zed-industries/hover-binding
add hover binding from vscode
2023-02-23 14:05:28 -08:00
Kay Simmons
ce4b672a14 add hover binding from vscode 2023-02-23 13:50:43 -08:00
Kay Simmons
3f9405f8f1 Merge pull request #2210 from zed-industries/fix-enter-vim-normal-mode
Fix enter in normal mode acting incorrectly
2023-02-23 13:46:15 -08:00
Kay Simmons
2276d25bdf Fix enter in normal mode acting incorrectly 2023-02-23 13:40:31 -08:00
Mikayla Maki
ffe53bed87 Reverts keymap precedence order change 2023-02-23 13:32:45 -08:00
Joseph Lyons
37f910949d Add link to community repo in feedback editor 2023-02-23 16:30:00 -05:00
Julia
1e3b4f0387 Merge pull request #2208 from zed-industries/new-collab-ui-3
Add same grayscale logic to followers which leaders have; new call UI [3/N]
2023-02-23 16:05:42 -05:00
Julia
e1df85e86d Add same grayscale logic to followers which leaders have 2023-02-23 15:59:37 -05:00
Mikayla Maki
f6601f64e5 Added editor-in-project-panel overrides to the default keymap 2023-02-23 09:36:30 -08:00
Antonio Scandurra
6ccc90327c Merge pull request #2206 from zed-industries/fix-refresh-room
Fix error when deleting rooms containing projects on refresh
2023-02-23 16:03:14 +01:00
Antonio Scandurra
bbeb33bc7e Fix error when deleting rooms containing projects on refresh
A foreign key violation was causing the server to never delete stale
rooms during `Database::refresh_room` due to having one or more project
records referencing the room.
2023-02-23 15:54:35 +01:00
Antonio Scandurra
e74db2d180 Merge pull request #2205 from zed-industries/call-ui-follow-up
Refine new call UI
2023-02-23 15:39:59 +01:00
Antonio Scandurra
74e0bed38f Fix compilation errors after restructuring room_transaction 2023-02-23 15:17:22 +01:00
Antonio Scandurra
832549f1a3 Merge branch 'main' into call-ui-follow-up 2023-02-23 15:15:46 +01:00
Antonio Scandurra
b965333325 Show avatar in user menu 2023-02-23 15:09:32 +01:00
Joseph T. Lyons
2be0283bf2 Merge pull request #2204 from zed-industries/fix-description-of-telemetry-setting
Fix description of telemetry setting
2023-02-23 08:59:30 -05:00
Antonio Scandurra
59a66190e5 Avoid trying to reconnect to a room if client is signed out 2023-02-23 14:53:10 +01:00
Antonio Scandurra
9334267bd0 Tear down peer when signing out 2023-02-23 14:47:02 +01:00
Antonio Scandurra
a0daf47134 Don't panic when rendering collab titlebar item while signed out 2023-02-23 14:46:02 +01:00
Petros Amoiridis
9a729a2e64 Merge pull request #2202 from zed-industries/petros/z-86-replace-terminal-tab-title-with-an-icon
Replace terminal tab title with an icon
2023-02-23 11:04:43 +02:00
Antonio Scandurra
1c636500de Merge pull request #2200 from zed-industries/fix-slow-project-join
Hold room lock through the entirety of a `room_transaction`
2023-02-23 09:11:58 +01:00
Max Brunsfeld
65a9ac449f Remove leave button from the title bar 2023-02-22 23:30:32 -08:00
Max Brunsfeld
bf5c3d963a Rearrange collab titlebar items to avoid movement of the toggle contacts button
* Replace username in titelbar with a `...` user menu that shows
  the current user name and contains a sign-in/sign-out button.
* Move the '+' (toggle contacts) button back to the right side.
* Move the collaborators back to the right side.
* Move the share/unshare button to the left side, beside the project title
* Only show the share/unshare button when in a call.
2023-02-22 22:56:11 -08:00
Joseph Lyons
c33d0f940a Fix description of telemetry setting 2023-02-22 23:50:20 -05:00
Max Brunsfeld
24e0a027ee Run check-formatting CI job on a mac mini 2023-02-22 15:35:05 -08:00
Max Brunsfeld
d49e35f947 Merge pull request #2203 from zed-industries/collab-ui-fixes
Fix minor issues with new collab UI
2023-02-22 14:22:05 -08:00
Max Brunsfeld
40aee8d7bc Add missing tooltip for contacts menu button
Co-authored-by: Joseph Lyons <joseph@zed.dev>
2023-02-22 14:18:17 -08:00
Max Brunsfeld
d33d27faa4 Fix ToggleContactsMenu action name in keymap
Co-authored-by: Joseph Lyons <joseph@zed.dev>
2023-02-22 14:17:59 -08:00
Max Brunsfeld
46ead28971 Bump RPC protocol version number 2023-02-22 13:40:14 -08:00
Max Brunsfeld
111aff29cc collab 0.6.0 2023-02-22 12:35:15 -08:00
Max Brunsfeld
e2a2e40599 v0.76.x dev 2023-02-22 12:34:29 -08:00
Petros Amoiridis
3d6c81584f Add an icon to the terminal view tab
The terminal icon already existed in `assets/icons`

Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>
2023-02-22 20:30:44 +02:00
Petros Amoiridis
81ece4fd44 Deduplicate tab theme related code
We've noticed that the search theme struct had two fields for a tab icon width and spacing. But we already have those in the tab theme struct. We decided to remove the duplicate and reuse the tab fields.

We also wanted to move where the spacing is being used. Instead of doing it at the left of the label, we do it at the right of the icon to match how it is done in other areas of the UI.

Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>
2023-02-22 20:28:57 +02:00
Petros Amoiridis
2ec5c88f98 Make icon width match other areas
This was 8 but we've seen areas where this was 14, like the project search tab icon. We want to match this.

Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>
2023-02-22 20:25:39 +02:00
Antonio Scandurra
8dd249a7cd Hold room lock through the entirety of a room_transaction
Previously, when the host repeatedly sent `UpdateWorktree` messages,
new guests attempting to join a project would observe a severe slowdown
caused by a database serialization error (e.g., the coherence of the data
would get violated midway through `Database::join_project` due to worktree
entries being mutated as the user joined). Writing entries is pretty fast,
whereas reading all of them for a project can take more than 100ms.
Transactions that failed due to a serialization error are retried, but the guest
would keep retrying until the host finished writing because the guest's read
was slow.

This commit changes the semantics of `room_transaction` to acquire a room
lock before even starting the transaction and holding it all the way after
commit (storing it, as before, in the `RoomGuard`). This ensures that a fast
writer (the host) can't starve a slow reader (the guest), allowing the latter
to make progress by temporarily pausing writes by the former.
2023-02-22 16:04:29 +01:00
Nate Butler
5ce147a2ad Remove unneeded API from theme-tool 2023-02-12 21:04:54 -05:00
Nate Butler
a32c0d1c9b Update colors. 2023-02-12 21:04:31 -05:00
Nate Butler
e65c0810ba Add gray color families 2023-02-12 20:02:51 -05:00
Nate Butler
1fcfa5d272 Generate color meta, document functions and tidy. 2023-02-12 19:45:16 -05:00
Nate Butler
addfcdc1f4 Remove bezier-easing from styles
This was installed in the wrong app
2023-02-12 11:59:36 -05:00
Nate Butler
4501a5a7ee Add initial system palette reference colors 2023-02-12 00:15:41 -05:00
Nate Butler
a120996f0d Test using HSL + curves to build a scale 2023-02-11 23:34:46 -05:00
Nate Butler
187fac1579 Allow passing a chroma color as a start/mid/end color 2023-02-11 21:10:47 -05:00
Nate Butler
0acb820f04 Document ref/color further 2023-02-11 08:14:13 -05:00
Nate Butler
dda0febf39 Organize 2023-02-11 07:56:41 -05:00
Nate Butler
0e238210bb Update chip style 2023-02-10 22:06:03 -05:00
Nate Butler
76685406ed Add remaining colors 2023-02-09 23:26:44 -05:00
Nate Butler
70eedbb48e Group light and dark scales 2023-02-09 13:41:37 -05:00
Nate Butler
42b5fa1fa3 WIP: Use algorithm to generate reference color palette
Adapted from @k-vyn/coloralgorithm
Generate colors for our reference palette.
2023-02-09 12:37:05 -05:00
Nate Butler
f787f6054b List colors for reference palette in theme tool 2023-02-09 00:39:57 -05:00
Nate Butler
6f342bb2c6 Remove leftovers from create-next-app 2023-02-09 00:10:32 -05:00
Nate Butler
0ba44c6dc4 Start on system colors 2023-02-09 00:09:52 -05:00
Nate Butler
2ff82732b9 Init theme tool 2023-02-09 00:09:44 -05:00
Nate Butler
cbfdfa8124 Remove comment 2023-02-08 22:52:27 -05:00
161 changed files with 7492 additions and 4913 deletions

View File

@@ -19,7 +19,9 @@ env:
jobs:
rustfmt:
name: Check formatting
runs-on: self-hosted
runs-on:
- self-hosted
- test
steps:
- name: Install Rust
run: |

8
Cargo.lock generated
View File

@@ -794,7 +794,7 @@ dependencies = [
[[package]]
name = "bromberg_sl2"
version = "0.6.0"
source = "git+https://github.com/zed-industries/bromberg_sl2?rev=dac565a90e8f9245f48ff46225c915dc50f76920#dac565a90e8f9245f48ff46225c915dc50f76920"
source = "git+https://github.com/zed-industries/bromberg_sl2?rev=950bc5482c216c395049ae33ae4501e08975f17f#950bc5482c216c395049ae33ae4501e08975f17f"
dependencies = [
"digest 0.9.0",
"lazy_static",
@@ -1188,7 +1188,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.5.4"
version = "0.6.1"
dependencies = [
"anyhow",
"async-tungstenite",
@@ -1257,6 +1257,7 @@ dependencies = [
"client",
"clock",
"collections",
"context_menu",
"editor",
"futures 0.3.25",
"fuzzy",
@@ -4591,6 +4592,7 @@ dependencies = [
"lsp",
"parking_lot 0.11.2",
"postage",
"pretty_assertions",
"pulldown-cmark",
"rand 0.8.5",
"regex",
@@ -8356,7 +8358,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "zed"
version = "0.75.0"
version = "0.76.0"
dependencies = [
"activity_indicator",
"anyhow",

View File

@@ -23,10 +23,18 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
git clone https://github.com/zed-industries/zed.dev
```
* Set up a local `zed` database and seed it with some initial users:
* Initialize submodules
```
script/bootstrap
git submodule update --init --recursive
```
* Set up a local `zed` database and seed it with some initial users:
Create a personal GitHub token to run `script/bootstrap` once successfully. Then delete that token.
```
GITHUB_TOKEN=<$token> script/bootstrap
```
### Testing against locally-running servers

View File

@@ -0,0 +1,3 @@
<svg width="14" height="4" viewBox="0 0 14 4" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.125 2C3.125 2.62132 2.62132 3.125 2 3.125C1.37868 3.125 0.875 2.62132 0.875 2C0.875 1.37868 1.37868 0.875 2 0.875C2.62132 0.875 3.125 1.37868 3.125 2ZM8.125 2C8.125 2.62132 7.62132 3.125 7 3.125C6.37868 3.125 5.875 2.62132 5.875 2C5.875 1.37868 6.37868 0.875 7 0.875C7.62132 0.875 8.125 1.37868 8.125 2ZM12 3.125C12.6213 3.125 13.125 2.62132 13.125 2C13.125 1.37868 12.6213 0.875 12 0.875C11.3787 0.875 10.875 1.37868 10.875 2C10.875 2.62132 11.3787 3.125 12 3.125Z" fill="#ABB2BF"/>
</svg>

After

Width:  |  Height:  |  Size: 637 B

View File

@@ -228,6 +228,7 @@
"replace_newest": true
}
],
"cmd-k cmd-i": "editor::Hover",
"cmd-/": [
"editor::ToggleComments",
{
@@ -418,7 +419,7 @@
{
"bindings": {
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
"cmd-shift-c": "collab::ToggleCollaborationMenu",
"cmd-shift-c": "collab::ToggleContactsMenu",
"cmd-alt-i": "zed::DebugElements"
}
},
@@ -456,7 +457,7 @@
}
},
{
"context": "Dock",
"context": "Pane && docked",
"bindings": {
"shift-escape": "dock::HideDock",
"cmd-escape": "dock::RemoveTabFromDock"

View File

@@ -27,6 +27,7 @@
"h": "vim::Left",
"backspace": "vim::Backspace",
"j": "vim::Down",
"enter": "vim::NextLineStart",
"k": "vim::Up",
"l": "vim::Right",
"$": "vim::EndOfLine",
@@ -233,7 +234,8 @@
"escape": [
"vim::SwitchMode",
"Normal"
]
],
"d": "editor::GoToDefinition"
}
},
{

View File

@@ -51,6 +51,12 @@
// 3. Position the dock full screen over the entire workspace"
// "default_dock_anchor": "expanded"
"default_dock_anchor": "right",
// Whether or not to remove any trailing whitespace from lines of a buffer
// before saving it.
"remove_trailing_whitespace_on_save": true,
// Whether or not to ensure there's a single newline at the end of a buffer
// when saving it.
"ensure_final_newline_on_save": true,
// 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:
@@ -83,7 +89,7 @@
"hard_tabs": false,
// How many columns a tab should occupy.
"tab_size": 4,
// Control what info Zed sends to our servers
// Control what info is collected by Zed.
"telemetry": {
// Send debug info like crash reports.
"diagnostics": true,

View File

@@ -20,7 +20,7 @@ use project::Project;
use std::{mem, sync::Arc, time::Duration};
use util::{post_inc, ResultExt, TryFutureExt};
pub const RECONNECT_TIMEOUT: Duration = client::RECEIVE_TIMEOUT;
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
@@ -55,7 +55,7 @@ pub struct Room {
leave_when_empty: bool,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
follows_by_leader_id: HashMap<PeerId, Vec<PeerId>>,
follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec<PeerId>>,
subscriptions: Vec<client::Subscription>,
pending_room_update: Option<Task<()>>,
maintain_connection: Option<Task<Option<()>>>,
@@ -149,7 +149,7 @@ impl Room {
pending_room_update: None,
client,
user_store,
follows_by_leader_id: Default::default(),
follows_by_leader_id_project_id: Default::default(),
maintain_connection: Some(maintain_connection),
}
}
@@ -277,14 +277,12 @@ impl Room {
) -> Result<()> {
let mut client_status = client.status();
loop {
let is_connected = client_status
.next()
.await
.map_or(false, |s| s.is_connected());
let _ = client_status.try_recv();
let is_connected = client_status.borrow().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() {
log::info!("detected client disconnection");
this.upgrade(&cx)
.ok_or_else(|| anyhow!("room was dropped"))?
.update(&mut cx, |this, cx| {
@@ -298,12 +296,7 @@ impl Room {
let client_reconnection = async {
let mut remaining_attempts = 3;
while remaining_attempts > 0 {
log::info!(
"waiting for client status change, remaining attempts {}",
remaining_attempts
);
let Some(status) = client_status.next().await else { break };
if status.is_connected() {
if client_status.borrow().is_connected() {
log::info!("client reconnected, attempting to rejoin room");
let Some(this) = this.upgrade(&cx) else { break };
@@ -317,7 +310,15 @@ impl Room {
} else {
remaining_attempts -= 1;
}
} else if client_status.borrow().is_signed_out() {
return false;
}
log::info!(
"waiting for client status change, remaining attempts {}",
remaining_attempts
);
client_status.next().await;
}
false
}
@@ -339,18 +340,20 @@ impl Room {
}
}
// The client failed to re-establish a connection to the server
// or an error occurred while trying to re-join the room. Either way
// we leave the room and return an error.
if let Some(this) = this.upgrade(&cx) {
log::info!("reconnection failed, leaving room");
let _ = this.update(&mut cx, |this, cx| this.leave(cx));
}
return Err(anyhow!(
"can't reconnect to room: client failed to re-establish connection"
));
break;
}
}
// The client failed to re-establish a connection to the server
// or an error occurred while trying to re-join the room. Either way
// we leave the room and return an error.
if let Some(this) = this.upgrade(&cx) {
log::info!("reconnection failed, leaving room");
let _ = this.update(&mut cx, |this, cx| this.leave(cx));
}
Err(anyhow!(
"can't reconnect to room: client failed to re-establish connection"
))
}
fn rejoin(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
@@ -459,9 +462,9 @@ impl Room {
self.participant_user_ids.contains(&user_id)
}
pub fn followers_for(&self, leader_id: PeerId) -> &[PeerId] {
self.follows_by_leader_id
.get(&leader_id)
pub fn followers_for(&self, leader_id: PeerId, project_id: u64) -> &[PeerId] {
self.follows_by_leader_id_project_id
.get(&(leader_id, project_id))
.map_or(&[], |v| v.as_slice())
}
@@ -631,8 +634,9 @@ impl Room {
}
}
this.follows_by_leader_id.clear();
this.follows_by_leader_id_project_id.clear();
for follower in room.followers {
let project_id = follower.project_id;
let (leader, follower) = match (follower.leader_id, follower.follower_id) {
(Some(leader), Some(follower)) => (leader, follower),
@@ -643,8 +647,8 @@ impl Room {
};
let list = this
.follows_by_leader_id
.entry(leader)
.follows_by_leader_id_project_id
.entry((leader, project_id))
.or_insert(Vec::new());
if !list.contains(&follower) {
list.push(follower);

View File

@@ -66,7 +66,7 @@ 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]);
actions!(client, [Authenticate, SignOut]);
pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
cx.add_global_action({
@@ -79,6 +79,16 @@ pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
.detach();
}
});
cx.add_global_action({
let client = client.clone();
move |_: &SignOut, cx| {
let client = client.clone();
cx.spawn(|cx| async move {
client.disconnect(&cx);
})
.detach();
}
});
}
pub struct Client {
@@ -169,6 +179,10 @@ impl Status {
pub fn is_connected(&self) -> bool {
matches!(self, Self::Connected { .. })
}
pub fn is_signed_out(&self) -> bool {
matches!(self, Self::SignedOut | Self::UpgradeRequired)
}
}
struct ClientState {
@@ -1152,11 +1166,9 @@ impl Client {
})
}
pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
let conn_id = self.connection_id()?;
self.peer.disconnect(conn_id);
pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
self.peer.teardown();
self.set_status(Status::SignedOut, cx);
Ok(())
}
fn connection_id(&self) -> Result<ConnectionId> {

View File

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

View File

@@ -158,7 +158,7 @@ impl Database {
room_id: RoomId,
new_server_id: ServerId,
) -> Result<RoomGuard<RefreshedRoom>> {
self.room_transaction(|tx| async move {
self.room_transaction(room_id, |tx| async move {
let stale_participant_filter = Condition::all()
.add(room_participant::Column::RoomId.eq(room_id))
.add(room_participant::Column::AnsweringConnectionId.is_not_null())
@@ -191,17 +191,18 @@ impl Database {
.filter(room_participant::Column::RoomId.eq(room_id))
.exec(&*tx)
.await?;
project::Entity::delete_many()
.filter(project::Column::RoomId.eq(room_id))
.exec(&*tx)
.await?;
room::Entity::delete_by_id(room_id).exec(&*tx).await?;
}
Ok((
room_id,
RefreshedRoom {
room,
stale_participant_user_ids,
canceled_calls_to_user_ids,
},
))
Ok(RefreshedRoom {
room,
stale_participant_user_ids,
canceled_calls_to_user_ids,
})
})
.await
}
@@ -1130,18 +1131,16 @@ impl Database {
user_id: UserId,
connection: ConnectionId,
live_kit_room: &str,
) -> Result<RoomGuard<proto::Room>> {
self.room_transaction(|tx| async move {
) -> Result<proto::Room> {
self.transaction(|tx| async move {
let room = room::ActiveModel {
live_kit_room: ActiveValue::set(live_kit_room.into()),
..Default::default()
}
.insert(&*tx)
.await?;
let room_id = room.id;
room_participant::ActiveModel {
room_id: ActiveValue::set(room_id),
room_id: ActiveValue::set(room.id),
user_id: ActiveValue::set(user_id),
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
answering_connection_server_id: ActiveValue::set(Some(ServerId(
@@ -1158,8 +1157,8 @@ impl Database {
.insert(&*tx)
.await?;
let room = self.get_room(room_id, &tx).await?;
Ok((room_id, room))
let room = self.get_room(room.id, &tx).await?;
Ok(room)
})
.await
}
@@ -1172,7 +1171,7 @@ impl Database {
called_user_id: UserId,
initial_project_id: Option<ProjectId>,
) -> Result<RoomGuard<(proto::Room, proto::IncomingCall)>> {
self.room_transaction(|tx| async move {
self.room_transaction(room_id, |tx| async move {
room_participant::ActiveModel {
room_id: ActiveValue::set(room_id),
user_id: ActiveValue::set(called_user_id),
@@ -1191,7 +1190,7 @@ impl Database {
let room = self.get_room(room_id, &tx).await?;
let incoming_call = Self::build_incoming_call(&room, called_user_id)
.ok_or_else(|| anyhow!("failed to build incoming call"))?;
Ok((room_id, (room, incoming_call)))
Ok((room, incoming_call))
})
.await
}
@@ -1201,7 +1200,7 @@ impl Database {
room_id: RoomId,
called_user_id: UserId,
) -> Result<RoomGuard<proto::Room>> {
self.room_transaction(|tx| async move {
self.room_transaction(room_id, |tx| async move {
room_participant::Entity::delete_many()
.filter(
room_participant::Column::RoomId
@@ -1211,7 +1210,7 @@ impl Database {
.exec(&*tx)
.await?;
let room = self.get_room(room_id, &tx).await?;
Ok((room_id, room))
Ok(room)
})
.await
}
@@ -1258,7 +1257,7 @@ impl Database {
calling_connection: ConnectionId,
called_user_id: UserId,
) -> Result<RoomGuard<proto::Room>> {
self.room_transaction(|tx| async move {
self.room_transaction(room_id, |tx| async move {
let participant = room_participant::Entity::find()
.filter(
Condition::all()
@@ -1277,14 +1276,13 @@ impl Database {
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no call to cancel"))?;
let room_id = participant.room_id;
room_participant::Entity::delete(participant.into_active_model())
.exec(&*tx)
.await?;
let room = self.get_room(room_id, &tx).await?;
Ok((room_id, room))
Ok(room)
})
.await
}
@@ -1295,7 +1293,7 @@ impl Database {
user_id: UserId,
connection: ConnectionId,
) -> Result<RoomGuard<proto::Room>> {
self.room_transaction(|tx| async move {
self.room_transaction(room_id, |tx| async move {
let result = room_participant::Entity::update_many()
.filter(
Condition::all()
@@ -1317,7 +1315,7 @@ impl Database {
Err(anyhow!("room does not exist or was already joined"))?
} else {
let room = self.get_room(room_id, &tx).await?;
Ok((room_id, room))
Ok(room)
}
})
.await
@@ -1329,9 +1327,9 @@ impl Database {
user_id: UserId,
connection: ConnectionId,
) -> Result<RoomGuard<RejoinedRoom>> {
self.room_transaction(|tx| async {
let room_id = RoomId::from_proto(rejoin_room.id);
self.room_transaction(room_id, |tx| async {
let tx = tx;
let room_id = RoomId::from_proto(rejoin_room.id);
let participant_update = room_participant::Entity::update_many()
.filter(
Condition::all()
@@ -1550,14 +1548,11 @@ impl Database {
}
let room = self.get_room(room_id, &tx).await?;
Ok((
room_id,
RejoinedRoom {
room,
rejoined_projects,
reshared_projects,
},
))
Ok(RejoinedRoom {
room,
rejoined_projects,
reshared_projects,
})
})
.await
}
@@ -1724,8 +1719,8 @@ impl Database {
leader_connection: ConnectionId,
follower_connection: ConnectionId,
) -> Result<RoomGuard<proto::Room>> {
self.room_transaction(|tx| async move {
let room_id = self.room_id_for_project(project_id, &*tx).await?;
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
follower::ActiveModel {
room_id: ActiveValue::set(room_id),
project_id: ActiveValue::set(project_id),
@@ -1742,7 +1737,8 @@ impl Database {
.insert(&*tx)
.await?;
Ok((room_id, self.get_room(room_id, &*tx).await?))
let room = self.get_room(room_id, &*tx).await?;
Ok(room)
})
.await
}
@@ -1753,60 +1749,39 @@ impl Database {
leader_connection: ConnectionId,
follower_connection: ConnectionId,
) -> Result<RoomGuard<proto::Room>> {
self.room_transaction(|tx| async move {
let room_id = self.room_id_for_project(project_id, &*tx).await?;
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
follower::Entity::delete_many()
.filter(
Condition::all()
.add(follower::Column::ProjectId.eq(project_id))
.add(
follower::Column::LeaderConnectionServerId
.eq(leader_connection.owner_id)
.and(follower::Column::LeaderConnectionId.eq(leader_connection.id)),
.eq(leader_connection.owner_id),
)
.add(follower::Column::LeaderConnectionId.eq(leader_connection.id))
.add(
follower::Column::FollowerConnectionServerId
.eq(follower_connection.owner_id)
.and(
follower::Column::FollowerConnectionId
.eq(follower_connection.id),
),
),
.eq(follower_connection.owner_id),
)
.add(follower::Column::FollowerConnectionId.eq(follower_connection.id)),
)
.exec(&*tx)
.await?;
Ok((room_id, self.get_room(room_id, &*tx).await?))
let room = self.get_room(room_id, &*tx).await?;
Ok(room)
})
.await
}
async fn room_id_for_project(
&self,
project_id: ProjectId,
tx: &DatabaseTransaction,
) -> Result<RoomId> {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryAs {
RoomId,
}
Ok(project::Entity::find_by_id(project_id)
.select_only()
.column(project::Column::RoomId)
.into_values::<_, QueryAs>()
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?)
}
pub async fn update_room_participant_location(
&self,
room_id: RoomId,
connection: ConnectionId,
location: proto::ParticipantLocation,
) -> Result<RoomGuard<proto::Room>> {
self.room_transaction(|tx| async {
self.room_transaction(room_id, |tx| async {
let tx = tx;
let location_kind;
let location_project_id;
@@ -1852,7 +1827,7 @@ impl Database {
if result.rows_affected == 1 {
let room = self.get_room(room_id, &tx).await?;
Ok((room_id, room))
Ok(room)
} else {
Err(anyhow!("could not update room participant location"))?
}
@@ -2018,6 +1993,7 @@ impl Database {
followers.push(proto::Follower {
leader_id: Some(db_follower.leader_connection().into()),
follower_id: Some(db_follower.follower_connection().into()),
project_id: db_follower.project_id.to_proto(),
});
}
@@ -2058,7 +2034,7 @@ impl Database {
connection: ConnectionId,
worktrees: &[proto::WorktreeMetadata],
) -> Result<RoomGuard<(ProjectId, proto::Room)>> {
self.room_transaction(|tx| async move {
self.room_transaction(room_id, |tx| async move {
let participant = room_participant::Entity::find()
.filter(
Condition::all()
@@ -2119,7 +2095,7 @@ impl Database {
.await?;
let room = self.get_room(room_id, &tx).await?;
Ok((room_id, (project.id, room)))
Ok((project.id, room))
})
.await
}
@@ -2129,7 +2105,8 @@ impl Database {
project_id: ProjectId,
connection: ConnectionId,
) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
self.room_transaction(|tx| async move {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
let project = project::Entity::find_by_id(project_id)
@@ -2137,12 +2114,11 @@ impl Database {
.await?
.ok_or_else(|| anyhow!("project not found"))?;
if project.host_connection()? == connection {
let room_id = project.room_id;
project::Entity::delete(project.into_active_model())
.exec(&*tx)
.await?;
let room = self.get_room(room_id, &tx).await?;
Ok((room_id, (room, guest_connection_ids)))
Ok((room, guest_connection_ids))
} else {
Err(anyhow!("cannot unshare a project hosted by another user"))?
}
@@ -2156,7 +2132,8 @@ impl Database {
connection: ConnectionId,
worktrees: &[proto::WorktreeMetadata],
) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
self.room_transaction(|tx| async move {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let project = project::Entity::find_by_id(project_id)
.filter(
Condition::all()
@@ -2174,7 +2151,7 @@ impl Database {
let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?;
let room = self.get_room(project.room_id, &tx).await?;
Ok((project.room_id, (room, guest_connection_ids)))
Ok((room, guest_connection_ids))
})
.await
}
@@ -2219,12 +2196,12 @@ impl Database {
update: &proto::UpdateWorktree,
connection: ConnectionId,
) -> Result<RoomGuard<Vec<ConnectionId>>> {
self.room_transaction(|tx| async move {
let project_id = ProjectId::from_proto(update.project_id);
let worktree_id = update.worktree_id as i64;
let project_id = ProjectId::from_proto(update.project_id);
let worktree_id = update.worktree_id as i64;
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
// Ensure the update comes from the host.
let project = project::Entity::find_by_id(project_id)
let _project = project::Entity::find_by_id(project_id)
.filter(
Condition::all()
.add(project::Column::HostConnectionId.eq(connection.id as i32))
@@ -2235,7 +2212,6 @@ impl Database {
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
let room_id = project.room_id;
// Update metadata.
worktree::Entity::update(worktree::ActiveModel {
@@ -2315,7 +2291,7 @@ impl Database {
}
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
Ok((room_id, connection_ids))
Ok(connection_ids)
})
.await
}
@@ -2325,9 +2301,10 @@ impl Database {
update: &proto::UpdateDiagnosticSummary,
connection: ConnectionId,
) -> Result<RoomGuard<Vec<ConnectionId>>> {
self.room_transaction(|tx| async move {
let project_id = ProjectId::from_proto(update.project_id);
let worktree_id = update.worktree_id as i64;
let project_id = ProjectId::from_proto(update.project_id);
let worktree_id = update.worktree_id as i64;
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let summary = update
.summary
.as_ref()
@@ -2369,7 +2346,7 @@ impl Database {
.await?;
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
Ok((project.room_id, connection_ids))
Ok(connection_ids)
})
.await
}
@@ -2379,8 +2356,9 @@ impl Database {
update: &proto::StartLanguageServer,
connection: ConnectionId,
) -> Result<RoomGuard<Vec<ConnectionId>>> {
self.room_transaction(|tx| async move {
let project_id = ProjectId::from_proto(update.project_id);
let project_id = ProjectId::from_proto(update.project_id);
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let server = update
.server
.as_ref()
@@ -2414,7 +2392,7 @@ impl Database {
.await?;
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
Ok((project.room_id, connection_ids))
Ok(connection_ids)
})
.await
}
@@ -2424,7 +2402,8 @@ impl Database {
project_id: ProjectId,
connection: ConnectionId,
) -> Result<RoomGuard<(Project, ReplicaId)>> {
self.room_transaction(|tx| async move {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let participant = room_participant::Entity::find()
.filter(
Condition::all()
@@ -2550,7 +2529,6 @@ impl Database {
.all(&*tx)
.await?;
let room_id = project.room_id;
let project = Project {
collaborators: collaborators
.into_iter()
@@ -2570,7 +2548,7 @@ impl Database {
})
.collect(),
};
Ok((room_id, (project, replica_id as ReplicaId)))
Ok((project, replica_id as ReplicaId))
})
.await
}
@@ -2579,8 +2557,9 @@ impl Database {
&self,
project_id: ProjectId,
connection: ConnectionId,
) -> Result<RoomGuard<LeftProject>> {
self.room_transaction(|tx| async move {
) -> Result<RoomGuard<(proto::Room, LeftProject)>> {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let result = project_collaborator::Entity::delete_many()
.filter(
Condition::all()
@@ -2610,13 +2589,39 @@ impl Database {
.map(|collaborator| collaborator.connection())
.collect();
follower::Entity::delete_many()
.filter(
Condition::any()
.add(
Condition::all()
.add(follower::Column::ProjectId.eq(project_id))
.add(
follower::Column::LeaderConnectionServerId
.eq(connection.owner_id),
)
.add(follower::Column::LeaderConnectionId.eq(connection.id)),
)
.add(
Condition::all()
.add(follower::Column::ProjectId.eq(project_id))
.add(
follower::Column::FollowerConnectionServerId
.eq(connection.owner_id),
)
.add(follower::Column::FollowerConnectionId.eq(connection.id)),
),
)
.exec(&*tx)
.await?;
let room = self.get_room(project.room_id, &tx).await?;
let left_project = LeftProject {
id: project_id,
host_user_id: project.host_user_id,
host_connection_id: project.host_connection()?,
connection_ids,
};
Ok((project.room_id, left_project))
Ok((room, left_project))
})
.await
}
@@ -2626,11 +2631,8 @@ impl Database {
project_id: ProjectId,
connection_id: ConnectionId,
) -> Result<RoomGuard<Vec<ProjectCollaborator>>> {
self.room_transaction(|tx| async move {
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let collaborators = project_collaborator::Entity::find()
.filter(project_collaborator::Column::ProjectId.eq(project_id))
.all(&*tx)
@@ -2648,7 +2650,7 @@ impl Database {
.iter()
.any(|collaborator| collaborator.connection_id == connection_id)
{
Ok((project.room_id, collaborators))
Ok(collaborators)
} else {
Err(anyhow!("no such project"))?
}
@@ -2661,11 +2663,8 @@ impl Database {
project_id: ProjectId,
connection_id: ConnectionId,
) -> Result<RoomGuard<HashSet<ConnectionId>>> {
self.room_transaction(|tx| async move {
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let mut collaborators = project_collaborator::Entity::find()
.filter(project_collaborator::Column::ProjectId.eq(project_id))
.stream(&*tx)
@@ -2678,7 +2677,7 @@ impl Database {
}
if connection_ids.contains(&connection_id) {
Ok((project.room_id, connection_ids))
Ok(connection_ids)
} else {
Err(anyhow!("no such project"))?
}
@@ -2708,6 +2707,17 @@ impl Database {
Ok(guest_connection_ids)
}
async fn room_id_for_project(&self, project_id: ProjectId) -> Result<RoomId> {
self.transaction(|tx| async move {
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("project {} not found", project_id))?;
Ok(project.room_id)
})
.await
}
// access tokens
pub async fn create_access_token_hash(
@@ -2858,21 +2868,48 @@ impl Database {
self.run(body).await
}
async fn room_transaction<F, Fut, T>(&self, f: F) -> Result<RoomGuard<T>>
async fn room_transaction<F, Fut, T>(&self, room_id: RoomId, f: F) -> Result<RoomGuard<T>>
where
F: Send + Fn(TransactionHandle) -> Fut,
Fut: Send + Future<Output = Result<(RoomId, T)>>,
Fut: Send + Future<Output = Result<T>>,
{
let data = self
.optional_room_transaction(move |tx| {
let future = f(tx);
async {
let data = future.await?;
Ok(Some(data))
let body = async {
loop {
let lock = self.rooms.entry(room_id).or_default().clone();
let _guard = lock.lock_owned().await;
let (tx, result) = self.with_transaction(&f).await?;
match result {
Ok(data) => {
match tx.commit().await.map_err(Into::into) {
Ok(()) => {
return Ok(RoomGuard {
data,
_guard,
_not_send: PhantomData,
});
}
Err(error) => {
if is_serialization_error(&error) {
// Retry (don't break the loop)
} else {
return Err(error);
}
}
}
}
Err(error) => {
tx.rollback().await?;
if is_serialization_error(&error) {
// Retry (don't break the loop)
} else {
return Err(error);
}
}
}
})
.await?;
Ok(data.unwrap())
}
};
self.run(body).await
}
async fn with_transaction<F, Fut, T>(&self, f: &F) -> Result<(DatabaseTransaction, Result<T>)>

View File

@@ -57,7 +57,7 @@ use tokio::sync::watch;
use tower::ServiceBuilder;
use tracing::{info_span, instrument, Instrument};
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(5);
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
lazy_static! {
@@ -270,8 +270,11 @@ impl Server {
let mut live_kit_room = String::new();
let mut delete_live_kit_room = false;
if let Ok(mut refreshed_room) =
app_state.db.refresh_room(room_id, server_id).await
if let Some(mut refreshed_room) = app_state
.db
.refresh_room(room_id, server_id)
.await
.trace_err()
{
tracing::info!(
room_id = room_id.0,
@@ -1405,7 +1408,7 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
let sender_id = session.connection_id;
let project_id = ProjectId::from_proto(request.project_id);
let project = session
let (room, project) = &*session
.db()
.await
.leave_project(project_id, sender_id)
@@ -1416,7 +1419,9 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
host_connection_id = %project.host_connection_id,
"leave project"
);
project_left(&project, &session);
room_updated(&room, &session.peer);
Ok(())
}

View File

@@ -733,6 +733,14 @@ async fn test_server_restarts(
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
client_a
.fs
.insert_tree("/a", json!({ "a.txt": "a-contents" }))
.await;
// Invite client B to collaborate on a project
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
let client_d = server.create_client(cx_d, "user_d").await;
@@ -753,19 +761,19 @@ async fn test_server_restarts(
// User A calls users B, C, and D.
active_call_a
.update(cx_a, |call, cx| {
call.invite(client_b.user_id().unwrap(), None, cx)
call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
})
.await
.unwrap();
active_call_a
.update(cx_a, |call, cx| {
call.invite(client_c.user_id().unwrap(), None, cx)
call.invite(client_c.user_id().unwrap(), Some(project_a.clone()), cx)
})
.await
.unwrap();
active_call_a
.update(cx_a, |call, cx| {
call.invite(client_d.user_id().unwrap(), None, cx)
call.invite(client_d.user_id().unwrap(), Some(project_a.clone()), cx)
})
.await
.unwrap();
@@ -821,7 +829,7 @@ async fn test_server_restarts(
// Users A and B reconnect to the call. User C has troubles reconnecting, so it leaves the room.
client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
deterministic.advance_clock(RECEIVE_TIMEOUT);
deterministic.advance_clock(RECONNECT_TIMEOUT);
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
@@ -993,7 +1001,7 @@ async fn test_server_restarts(
client_a.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
client_b.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending()));
deterministic.advance_clock(RECEIVE_TIMEOUT);
deterministic.advance_clock(RECONNECT_TIMEOUT);
assert_eq!(
room_participants(&room_a, cx_a),
RoomParticipants {
@@ -1083,7 +1091,7 @@ async fn test_calls_on_multiple_connections(
assert!(incoming_call_b2.next().await.unwrap().is_none());
// User B disconnects the client that is not on the call. Everything should be fine.
client_b1.disconnect(&cx_b1.to_async()).unwrap();
client_b1.disconnect(&cx_b1.to_async());
deterministic.advance_clock(RECEIVE_TIMEOUT);
client_b1
.authenticate_and_connect(false, &cx_b1.to_async())
@@ -3227,7 +3235,7 @@ async fn test_leaving_project(
buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
// Drop client B's connection and ensure client A and client C observe client B leaving.
client_b.disconnect(&cx_b.to_async()).unwrap();
client_b.disconnect(&cx_b.to_async());
deterministic.advance_clock(RECONNECT_TIMEOUT);
project_a.read_with(cx_a, |project, _| {
assert_eq!(project.collaborators().len(), 1);
@@ -3884,9 +3892,11 @@ async fn test_formatting_buffer(
})
.await
.unwrap();
// The edits from the LSP are applied, and a final newline is added.
assert_eq!(
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
"let honey = \"two\""
"let honey = \"two\"\n"
);
// Ensure buffer can be formatted using an external command. Notice how the
@@ -5772,7 +5782,7 @@ async fn test_contact_requests(
.is_empty());
async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
client.disconnect(&cx.to_async()).unwrap();
client.disconnect(&cx.to_async());
client.clear_contacts(cx).await;
client
.authenticate_and_connect(false, &cx.to_async())
@@ -5782,11 +5792,12 @@ async fn test_contact_requests(
}
#[gpui::test(iterations = 10)]
async fn test_following(
async fn test_basic_following(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
cx_d: &mut TestAppContext,
) {
deterministic.forbid_parking();
cx_a.update(editor::init);
@@ -5796,11 +5807,14 @@ async fn test_following(
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
let client_d = server.create_client(cx_d, "user_d").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
server
.make_contacts(&mut [(&client_a, cx_a), (&client_c, cx_c)])
.create_room(&mut [
(&client_a, cx_a),
(&client_b, cx_b),
(&client_c, cx_c),
(&client_d, cx_d),
])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global);
@@ -5867,6 +5881,7 @@ async fn test_following(
let peer_id_a = client_a.peer_id().unwrap();
let peer_id_b = client_b.peer_id().unwrap();
let peer_id_c = client_c.peer_id().unwrap();
let peer_id_d = client_d.peer_id().unwrap();
// Client A updates their selections in those editors
editor_a1.update(cx_a, |editor, cx| {
@@ -5886,25 +5901,15 @@ async fn test_following(
.await
.unwrap();
// Client A invites client C to the call.
active_call_a
.update(cx_a, |call, cx| {
call.invite(client_c.current_user_id(cx_c).to_proto(), None, cx)
})
.await
.unwrap();
cx_c.foreground().run_until_parked();
let active_call_c = cx_c.read(ActiveCall::global);
active_call_c
.update(cx_c, |call, cx| call.accept_incoming(cx))
.await
.unwrap();
let project_c = client_c.build_remote_project(project_id, cx_c).await;
let workspace_c = client_c.build_workspace(&project_c, cx_c);
active_call_c
.update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
.await
.unwrap();
drop(project_c);
// Client C also follows client A.
workspace_c
@@ -5916,17 +5921,28 @@ async fn test_following(
.await
.unwrap();
cx_d.foreground().run_until_parked();
let active_call_d = cx_d.read(ActiveCall::global);
let project_d = client_d.build_remote_project(project_id, cx_d).await;
let workspace_d = client_d.build_workspace(&project_d, cx_d);
active_call_d
.update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
.await
.unwrap();
drop(project_d);
// All clients see that clients B and C are following client A.
cx_c.foreground().run_until_parked();
for (name, active_call, cx) in [
("A", &active_call_a, &cx_a),
("B", &active_call_b, &cx_b),
("C", &active_call_c, &cx_c),
("D", &active_call_d, &cx_d),
] {
active_call.read_with(*cx, |call, cx| {
let room = call.room().unwrap().read(cx);
assert_eq!(
room.followers_for(peer_id_a),
room.followers_for(peer_id_a, project_id),
&[peer_id_b, peer_id_c],
"checking followers for A as {name}"
);
@@ -5944,17 +5960,102 @@ async fn test_following(
("A", &active_call_a, &cx_a),
("B", &active_call_b, &cx_b),
("C", &active_call_c, &cx_c),
("D", &active_call_d, &cx_d),
] {
active_call.read_with(*cx, |call, cx| {
let room = call.room().unwrap().read(cx);
assert_eq!(
room.followers_for(peer_id_a),
room.followers_for(peer_id_a, project_id),
&[peer_id_b],
"checking followers for A as {name}"
);
});
}
// Client C re-follows client A.
workspace_c.update(cx_c, |workspace, cx| {
workspace.toggle_follow(&ToggleFollow(peer_id_a), cx);
});
// All clients see that clients B and C are following client A.
cx_c.foreground().run_until_parked();
for (name, active_call, cx) in [
("A", &active_call_a, &cx_a),
("B", &active_call_b, &cx_b),
("C", &active_call_c, &cx_c),
("D", &active_call_d, &cx_d),
] {
active_call.read_with(*cx, |call, cx| {
let room = call.room().unwrap().read(cx);
assert_eq!(
room.followers_for(peer_id_a, project_id),
&[peer_id_b, peer_id_c],
"checking followers for A as {name}"
);
});
}
// Client D follows client C.
workspace_d
.update(cx_d, |workspace, cx| {
workspace
.toggle_follow(&ToggleFollow(peer_id_c), cx)
.unwrap()
})
.await
.unwrap();
// All clients see that D is following C
cx_d.foreground().run_until_parked();
for (name, active_call, cx) in [
("A", &active_call_a, &cx_a),
("B", &active_call_b, &cx_b),
("C", &active_call_c, &cx_c),
("D", &active_call_d, &cx_d),
] {
active_call.read_with(*cx, |call, cx| {
let room = call.room().unwrap().read(cx);
assert_eq!(
room.followers_for(peer_id_c, project_id),
&[peer_id_d],
"checking followers for C as {name}"
);
});
}
// Client C closes the project.
cx_c.drop_last(workspace_c);
// Clients A and B see that client B is following A, and client C is not present in the followers.
cx_c.foreground().run_until_parked();
for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
active_call.read_with(*cx, |call, cx| {
let room = call.room().unwrap().read(cx);
assert_eq!(
room.followers_for(peer_id_a, project_id),
&[peer_id_b],
"checking followers for A as {name}"
);
});
}
// All clients see that no-one is following C
for (name, active_call, cx) in [
("A", &active_call_a, &cx_a),
("B", &active_call_b, &cx_b),
("C", &active_call_c, &cx_c),
("D", &active_call_d, &cx_d),
] {
active_call.read_with(*cx, |call, cx| {
let room = call.room().unwrap().read(cx);
assert_eq!(
room.followers_for(peer_id_c, project_id),
&[],
"checking followers for C as {name}"
);
});
}
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
workspace
.active_item(cx)
@@ -6186,7 +6287,7 @@ async fn test_following(
);
// Following interrupts when client B disconnects.
client_b.disconnect(&cx_b.to_async()).unwrap();
client_b.disconnect(&cx_b.to_async());
deterministic.advance_clock(RECONNECT_TIMEOUT);
assert_eq!(
workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),

View File

@@ -27,6 +27,7 @@ call = { path = "../call" }
client = { path = "../client" }
clock = { path = "../clock" }
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }

View File

@@ -4,9 +4,10 @@ use crate::{
ToggleScreenSharing,
};
use call::{ActiveCall, ParticipantLocation, Room};
use client::{proto::PeerId, Authenticate, ContactEventKind, User, UserStore};
use client::{proto::PeerId, Authenticate, ContactEventKind, SignOut, User, UserStore};
use clock::ReplicaId;
use contacts_popover::ContactsPopover;
use context_menu::{ContextMenu, ContextMenuItem};
use gpui::{
actions,
color::Color,
@@ -28,8 +29,9 @@ actions!(
[
ToggleCollaboratorList,
ToggleContactsMenu,
ToggleUserMenu,
ShareProject,
UnshareProject
UnshareProject,
]
);
@@ -38,25 +40,20 @@ impl_internal_actions!(collab, [LeaveCall]);
#[derive(Copy, Clone, PartialEq)]
pub(crate) struct LeaveCall;
#[derive(PartialEq, Eq)]
enum ContactsPopoverSide {
Left,
Right,
}
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(CollabTitlebarItem::toggle_collaborator_list_popover);
cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
cx.add_action(CollabTitlebarItem::share_project);
cx.add_action(CollabTitlebarItem::unshare_project);
cx.add_action(CollabTitlebarItem::leave_call);
cx.add_action(CollabTitlebarItem::toggle_user_menu);
}
pub struct CollabTitlebarItem {
workspace: WeakViewHandle<Workspace>,
user_store: ModelHandle<UserStore>,
contacts_popover: Option<ViewHandle<ContactsPopover>>,
contacts_popover_side: ContactsPopoverSide,
user_menu: ViewHandle<ContextMenu>,
collaborator_list_popover: Option<ViewHandle<CollaboratorListPopover>>,
_subscriptions: Vec<Subscription>,
}
@@ -90,9 +87,9 @@ impl View for CollabTitlebarItem {
}
let theme = cx.global::<Settings>().theme.clone();
let user = workspace.read(cx).user_store().read(cx).current_user();
let mut left_container = Flex::row();
let mut right_container = Flex::row();
left_container.add_child(
Label::new(project_title, theme.workspace.titlebar.title.clone())
@@ -103,41 +100,31 @@ impl View for CollabTitlebarItem {
.boxed(),
);
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
left_container.add_child(self.render_current_user(&workspace, &theme, &user, cx));
left_container.add_children(self.render_collaborators(&workspace, &theme, room, cx));
left_container.add_child(self.render_toggle_contacts_button(&theme, cx));
}
let mut right_container = Flex::row();
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
right_container.add_child(self.render_leave_call_button(&theme, cx));
right_container
let user = workspace.read(cx).user_store().read(cx).current_user();
let peer_id = workspace.read(cx).client().peer_id();
if let Some(((user, peer_id), room)) = user
.zip(peer_id)
.zip(ActiveCall::global(cx).read(cx).room().cloned())
{
left_container
.add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
} else {
right_container.add_child(self.render_outside_call_share_button(&theme, cx));
right_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
right_container
.add_child(self.render_current_user(&workspace, &theme, &user, peer_id, cx));
right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
}
right_container.add_children(self.render_connection_status(&workspace, cx));
if let Some(user) = user {
//TODO: Add style
right_container.add_child(
Label::new(
user.github_login.clone(),
theme.workspace.titlebar.title.clone(),
)
.aligned()
.contained()
.with_margin_left(theme.workspace.titlebar.item_spacing)
.boxed(),
);
let status = workspace.read(cx).client().status();
let status = &*status.borrow();
if matches!(status, client::Status::Connected { .. }) {
right_container.add_child(self.render_toggle_contacts_button(&theme, cx));
} else {
right_container.add_child(Self::render_authenticate(&theme, cx));
right_container.add_children(self.render_connection_status(status, cx));
}
right_container.add_child(self.render_user_menu_button(&theme, cx));
Stack::new()
.with_child(left_container.boxed())
.with_child(right_container.aligned().right().boxed())
@@ -186,7 +173,11 @@ impl CollabTitlebarItem {
workspace: workspace.downgrade(),
user_store: user_store.clone(),
contacts_popover: None,
contacts_popover_side: ContactsPopoverSide::Right,
user_menu: cx.add_view(|cx| {
let mut menu = ContextMenu::new(cx);
menu.set_position_mode(OverlayPositionMode::Local);
menu
}),
collaborator_list_popover: None,
_subscriptions: subscriptions,
}
@@ -278,12 +269,6 @@ impl CollabTitlebarItem {
cx.notify();
})
.detach();
self.contacts_popover_side = match ActiveCall::global(cx).read(cx).room() {
Some(_) => ContactsPopoverSide::Left,
None => ContactsPopoverSide::Right,
};
self.contacts_popover = Some(view);
}
}
@@ -291,6 +276,59 @@ impl CollabTitlebarItem {
cx.notify();
}
pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
let theme = cx.global::<Settings>().theme.clone();
let avatar_style = theme.workspace.titlebar.leader_avatar.clone();
let item_style = theme.context_menu.item.disabled_style().clone();
self.user_menu.update(cx, |user_menu, cx| {
let items = if let Some(user) = self.user_store.read(cx).current_user() {
vec![
ContextMenuItem::Static(Box::new(move |_| {
Flex::row()
.with_children(user.avatar.clone().map(|avatar| {
Self::render_face(
avatar,
avatar_style.clone(),
Color::transparent_black(),
)
}))
.with_child(
Label::new(user.github_login.clone(), item_style.label.clone())
.boxed(),
)
.contained()
.with_style(item_style.container)
.boxed()
})),
ContextMenuItem::Item {
label: "Sign out".into(),
action: Box::new(SignOut),
},
]
} else {
vec![ContextMenuItem::Item {
label: "Sign in".into(),
action: Box::new(Authenticate),
}]
};
user_menu.show(
vec2f(
theme
.workspace
.titlebar
.user_menu_button
.default
.button_width,
theme.workspace.titlebar.height,
),
AnchorCorner::TopRight,
items,
cx,
);
});
}
fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx))
@@ -328,11 +366,9 @@ impl CollabTitlebarItem {
Stack::new()
.with_child(
MouseEventHandler::<ToggleContactsMenu>::new(0, cx, |state, _| {
let style = titlebar.toggle_contacts_button.style_for(
state,
self.contacts_popover.is_some()
&& self.contacts_popover_side == ContactsPopoverSide::Left,
);
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()
@@ -349,15 +385,18 @@ impl CollabTitlebarItem {
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ToggleContactsMenu);
})
.with_tooltip::<ToggleContactsMenu, _>(
0,
"Show contacts menu".into(),
Some(Box::new(ToggleContactsMenu)),
theme.tooltip.clone(),
cx,
)
.aligned()
.boxed(),
)
.with_children(badge)
.with_children(self.render_contacts_popover_host(
ContactsPopoverSide::Left,
titlebar,
cx,
))
.with_children(self.render_contacts_popover_host(titlebar, cx))
.boxed()
}
@@ -407,40 +446,6 @@ impl CollabTitlebarItem {
.boxed()
}
fn render_leave_call_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
let titlebar = &theme.workspace.titlebar;
MouseEventHandler::<LeaveCall>::new(0, cx, |state, _| {
let style = titlebar.call_control.style_for(state, false);
Svg::new("icons/leave_12.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(LeaveCall);
})
.with_tooltip::<LeaveCall, _>(
0,
"Leave call".to_owned(),
Some(Box::new(LeaveCall)),
theme.tooltip.clone(),
cx,
)
.contained()
.with_margin_left(theme.workspace.titlebar.item_spacing)
.aligned()
.boxed()
}
fn render_in_call_share_unshare_button(
&self,
workspace: &ViewHandle<Workspace>,
@@ -468,11 +473,9 @@ impl CollabTitlebarItem {
.with_child(
MouseEventHandler::<ShareUnshare>::new(0, cx, |state, _| {
//TODO: Ensure this button has consistant width for both text variations
let style = titlebar.share_button.style_for(
state,
self.contacts_popover.is_some()
&& self.contacts_popover_side == ContactsPopoverSide::Right,
);
let style = titlebar
.share_button
.style_for(state, self.contacts_popover.is_some());
Label::new(label, style.text.clone())
.contained()
.with_style(style.container)
@@ -495,11 +498,6 @@ impl CollabTitlebarItem {
)
.boxed(),
)
.with_children(self.render_contacts_popover_host(
ContactsPopoverSide::Right,
titlebar,
cx,
))
.aligned()
.contained()
.with_margin_left(theme.workspace.titlebar.item_spacing)
@@ -507,83 +505,71 @@ impl CollabTitlebarItem {
)
}
fn render_outside_call_share_button(
&self,
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let tooltip = "Share project with new call";
fn render_user_menu_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
let titlebar = &theme.workspace.titlebar;
enum OutsideCallShare {}
Stack::new()
.with_child(
MouseEventHandler::<OutsideCallShare>::new(0, cx, |state, _| {
//TODO: Ensure this button has consistant width for both text variations
let style = titlebar.share_button.style_for(
state,
self.contacts_popover.is_some()
&& self.contacts_popover_side == ContactsPopoverSide::Right,
);
Label::new("Share".to_owned(), style.text.clone())
MouseEventHandler::<ToggleUserMenu>::new(0, cx, |state, _| {
let style = titlebar.call_control.style_for(state, false);
Svg::new("icons/ellipsis_14.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(ToggleContactsMenu);
cx.dispatch_action(ToggleUserMenu);
})
.with_tooltip::<OutsideCallShare, _>(
.with_tooltip::<ToggleUserMenu, _>(
0,
tooltip.to_owned(),
None,
"Toggle user menu".to_owned(),
Some(Box::new(ToggleUserMenu)),
theme.tooltip.clone(),
cx,
)
.contained()
.with_margin_left(theme.workspace.titlebar.item_spacing)
.aligned()
.boxed(),
)
.with_children(self.render_contacts_popover_host(
ContactsPopoverSide::Right,
titlebar,
cx,
))
.aligned()
.contained()
.with_margin_left(theme.workspace.titlebar.item_spacing)
.with_child(ChildView::new(&self.user_menu, cx).boxed())
.boxed()
}
fn render_contacts_popover_host<'a>(
&'a self,
side: ContactsPopoverSide,
theme: &'a theme::Titlebar,
cx: &'a RenderContext<Self>,
) -> impl Iterator<Item = ElementBox> + 'a {
self.contacts_popover
.iter()
.filter(move |_| self.contacts_popover_side == side)
.map(|popover| {
Overlay::new(
ChildView::new(popover, cx)
.contained()
.with_margin_top(theme.height)
.with_margin_left(theme.toggle_contacts_button.default.button_width)
.with_margin_right(-theme.toggle_contacts_button.default.button_width)
.boxed(),
)
.with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_corner(AnchorCorner::BottomLeft)
.with_z_index(999)
.boxed()
})
) -> Option<ElementBox> {
self.contacts_popover.as_ref().map(|popover| {
Overlay::new(
ChildView::new(popover, cx)
.contained()
.with_margin_top(theme.height)
.with_margin_left(theme.toggle_contacts_button.default.button_width)
.with_margin_right(-theme.toggle_contacts_button.default.button_width)
.boxed(),
)
.with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_corner(AnchorCorner::BottomLeft)
.with_z_index(999)
.boxed()
})
}
fn render_collaborators(
&self,
workspace: &ViewHandle<Workspace>,
theme: &Theme,
room: ModelHandle<Room>,
room: &ModelHandle<Room>,
cx: &mut RenderContext<Self>,
) -> Vec<ElementBox> {
let project = workspace.read(cx).project().read(cx);
@@ -615,7 +601,7 @@ impl CollabTitlebarItem {
theme,
cx,
))
.with_margin_left(theme.workspace.titlebar.face_pile_spacing)
.with_margin_right(theme.workspace.titlebar.face_pile_spacing)
.boxed(),
)
})
@@ -626,35 +612,21 @@ impl CollabTitlebarItem {
&self,
workspace: &ViewHandle<Workspace>,
theme: &Theme,
user: &Option<Arc<User>>,
user: &Arc<User>,
peer_id: PeerId,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let user = user.as_ref().expect("Active call without user");
let replica_id = workspace.read(cx).project().read(cx).replica_id();
let peer_id = workspace
.read(cx)
.client()
.peer_id()
.expect("Active call without peer id");
self.render_face_pile(user, Some(replica_id), peer_id, None, workspace, theme, cx)
}
fn render_authenticate(theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
MouseEventHandler::<Authenticate>::new(0, cx, |state, _| {
let style = theme
.workspace
.titlebar
.sign_in_prompt
.style_for(state, false);
Label::new("Sign in", style.text.clone())
.contained()
.with_style(style.container)
.with_margin_left(theme.workspace.titlebar.item_spacing)
.boxed()
})
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate))
.with_cursor_style(CursorStyle::PointingHand)
.aligned()
Container::new(self.render_face_pile(
user,
Some(replica_id),
peer_id,
None,
workspace,
theme,
cx,
))
.with_margin_right(theme.workspace.titlebar.item_spacing)
.boxed()
}
@@ -668,33 +640,26 @@ impl CollabTitlebarItem {
theme: &Theme,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let project_id = workspace.read(cx).project().read(cx).remote_id();
let room = ActiveCall::global(cx).read(cx).room();
let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
let followed_by_self = room
.map(|room| {
is_being_followed
&& room
.read(cx)
.followers_for(peer_id)
.iter()
.any(|&follower| Some(follower) == workspace.read(cx).client().peer_id())
.and_then(|room| {
Some(
is_being_followed
&& room
.read(cx)
.followers_for(peer_id, project_id?)
.iter()
.any(|&follower| {
Some(follower) == workspace.read(cx).client().peer_id()
}),
)
})
.unwrap_or(false);
let avatar_style;
if let Some(location) = location {
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 leader_style = theme.workspace.titlebar.leader_avatar;
let follower_style = theme.workspace.titlebar.follower_avatar;
let mut background_color = theme
.workspace
@@ -710,23 +675,26 @@ impl CollabTitlebarItem {
}
}
let content = Stack::new()
let mut content = Stack::new()
.with_children(user.avatar.as_ref().map(|avatar| {
let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap)
.with_child(Self::render_face(
avatar.clone(),
avatar_style.clone(),
Self::location_style(workspace, location, leader_style, cx),
background_color,
))
.with_children(
(|| {
let project_id = project_id?;
let room = room?.read(cx);
let followers = room.followers_for(peer_id);
let followers = room.followers_for(peer_id, project_id);
Some(followers.into_iter().flat_map(|&follower| {
let avatar = room
.remote_participant_for_peer_id(follower)
.and_then(|participant| participant.user.avatar.clone())
let remote_participant =
room.remote_participant_for_peer_id(follower);
let avatar = remote_participant
.and_then(|p| p.user.avatar.clone())
.or_else(|| {
if follower == workspace.read(cx).client().peer_id()? {
workspace
@@ -741,9 +709,11 @@ impl CollabTitlebarItem {
}
})?;
let location = remote_participant.map(|p| p.location);
Some(Self::render_face(
avatar.clone(),
theme.workspace.titlebar.follower_avatar.clone(),
Self::location_style(workspace, location, follower_style, cx),
background_color,
))
}))
@@ -782,7 +752,10 @@ impl CollabTitlebarItem {
if let Some(location) = location {
if let Some(replica_id) = replica_id {
MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content)
content =
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))
@@ -798,12 +771,14 @@ impl CollabTitlebarItem {
theme.tooltip.clone(),
cx,
)
.boxed()
.boxed();
} else if let ParticipantLocation::SharedProject { project_id } = location {
let user_id = user.id;
MouseEventHandler::<JoinProject>::new(peer_id.as_u64() as usize, cx, move |_, _| {
content
})
content = MouseEventHandler::<JoinProject>::new(
peer_id.as_u64() as usize,
cx,
move |_, _| content,
)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(JoinProject {
@@ -818,13 +793,29 @@ impl CollabTitlebarItem {
theme.tooltip.clone(),
cx,
)
.boxed()
} else {
content
.boxed();
}
} else {
content
}
content
}
fn location_style(
workspace: &ViewHandle<Workspace>,
location: Option<ParticipantLocation>,
mut style: AvatarStyle,
cx: &RenderContext<Self>,
) -> AvatarStyle {
if let Some(location) = location {
if let ParticipantLocation::SharedProject { project_id } = location {
if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
style.image.grayscale = true;
}
} else {
style.image.grayscale = true;
}
}
style
}
fn render_face(
@@ -847,13 +838,13 @@ impl CollabTitlebarItem {
fn render_connection_status(
&self,
workspace: &ViewHandle<Workspace>,
status: &client::Status,
cx: &mut RenderContext<Self>,
) -> Option<ElementBox> {
enum ConnectionStatusButton {}
let theme = &cx.global::<Settings>().theme.clone();
match &*workspace.read(cx).client().status().borrow() {
match status {
client::Status::ConnectionError
| client::Status::ConnectionLost
| client::Status::Reauthenticating { .. }

View File

@@ -1294,7 +1294,7 @@ impl View for ContactList {
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.set.insert("menu".into());
cx.add_identifier("menu");
cx
}

View File

@@ -5,7 +5,9 @@ use gpui::{
};
use menu::*;
use settings::Settings;
use std::{any::TypeId, time::Duration};
use std::{any::TypeId, borrow::Cow, time::Duration};
pub type StaticItem = Box<dyn Fn(&mut MutableAppContext) -> ElementBox>;
#[derive(Copy, Clone, PartialEq)]
struct Clicked;
@@ -24,16 +26,17 @@ pub fn init(cx: &mut MutableAppContext) {
pub enum ContextMenuItem {
Item {
label: String,
label: Cow<'static, str>,
action: Box<dyn Action>,
},
Static(StaticItem),
Separator,
}
impl ContextMenuItem {
pub fn item(label: impl ToString, action: impl 'static + Action) -> Self {
pub fn item(label: impl Into<Cow<'static, str>>, action: impl 'static + Action) -> Self {
Self::Item {
label: label.to_string(),
label: label.into(),
action: Box::new(action),
}
}
@@ -42,14 +45,14 @@ impl ContextMenuItem {
Self::Separator
}
fn is_separator(&self) -> bool {
matches!(self, Self::Separator)
fn is_action(&self) -> bool {
matches!(self, Self::Item { .. })
}
fn action_id(&self) -> Option<TypeId> {
match self {
ContextMenuItem::Item { action, .. } => Some(action.id()),
ContextMenuItem::Separator => None,
ContextMenuItem::Static(..) | ContextMenuItem::Separator => None,
}
}
}
@@ -58,6 +61,7 @@ pub struct ContextMenu {
show_count: usize,
anchor_position: Vector2F,
anchor_corner: AnchorCorner,
position_mode: OverlayPositionMode,
items: Vec<ContextMenuItem>,
selected_index: Option<usize>,
visible: bool,
@@ -78,7 +82,7 @@ impl View for ContextMenu {
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.set.insert("menu".into());
cx.add_identifier("menu");
cx
}
@@ -105,6 +109,7 @@ impl View for ContextMenu {
.with_fit_mode(OverlayFitMode::SnapToWindow)
.with_anchor_position(self.anchor_position)
.with_anchor_corner(self.anchor_corner)
.with_position_mode(self.position_mode)
.boxed()
}
@@ -121,6 +126,7 @@ impl ContextMenu {
show_count: 0,
anchor_position: Default::default(),
anchor_corner: AnchorCorner::TopLeft,
position_mode: OverlayPositionMode::Window,
items: Default::default(),
selected_index: Default::default(),
visible: Default::default(),
@@ -188,13 +194,13 @@ impl ContextMenu {
}
fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
self.selected_index = self.items.iter().position(|item| !item.is_separator());
self.selected_index = self.items.iter().position(|item| item.is_action());
cx.notify();
}
fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
for (ix, item) in self.items.iter().enumerate().rev() {
if !item.is_separator() {
if item.is_action() {
self.selected_index = Some(ix);
cx.notify();
break;
@@ -205,7 +211,7 @@ impl ContextMenu {
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.selected_index {
for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
if !item.is_separator() {
if item.is_action() {
self.selected_index = Some(ix);
cx.notify();
break;
@@ -219,7 +225,7 @@ impl ContextMenu {
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.selected_index {
for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
if !item.is_separator() {
if item.is_action() {
self.selected_index = Some(ix);
cx.notify();
break;
@@ -234,7 +240,7 @@ impl ContextMenu {
&mut self,
anchor_position: Vector2F,
anchor_corner: AnchorCorner,
items: impl IntoIterator<Item = ContextMenuItem>,
items: Vec<ContextMenuItem>,
cx: &mut ViewContext<Self>,
) {
let mut items = items.into_iter().peekable();
@@ -254,6 +260,10 @@ impl ContextMenu {
cx.notify();
}
pub fn set_position_mode(&mut self, mode: OverlayPositionMode) {
self.position_mode = mode;
}
fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element {
let window_id = cx.window_id();
let style = cx.global::<Settings>().theme.context_menu.clone();
@@ -273,6 +283,9 @@ impl ContextMenu {
.with_style(style.container)
.boxed()
}
ContextMenuItem::Static(f) => f(cx),
ContextMenuItem::Separator => Empty::new()
.collapsed()
.contained()
@@ -302,6 +315,9 @@ impl ContextMenu {
)
.boxed()
}
ContextMenuItem::Static(_) => Empty::new().boxed(),
ContextMenuItem::Separator => Empty::new()
.collapsed()
.constrained()
@@ -339,7 +355,7 @@ impl ContextMenu {
Flex::row()
.with_child(
Label::new(label.to_string(), style.label.clone())
Label::new(label.clone(), style.label.clone())
.contained()
.boxed(),
)
@@ -366,6 +382,9 @@ impl ContextMenu {
.on_drag(MouseButton::Left, |_, _| {})
.boxed()
}
ContextMenuItem::Static(f) => f(cx),
ContextMenuItem::Separator => Empty::new()
.constrained()
.with_height(1.)

View File

@@ -8,6 +8,7 @@ use block_map::{BlockMap, BlockPoint};
use collections::{HashMap, HashSet};
use fold_map::FoldMap;
use gpui::{
color::Color,
fonts::{FontId, HighlightStyle},
Entity, ModelContext, ModelHandle,
};
@@ -23,6 +24,12 @@ pub use block_map::{
BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum FoldStatus {
Folded,
Foldable,
}
pub trait ToDisplayPoint {
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
}
@@ -212,6 +219,10 @@ impl DisplayMap {
.update(cx, |map, cx| map.set_font(font_id, font_size, cx))
}
pub fn set_fold_ellipses_color(&mut self, color: Color) -> bool {
self.fold_map.set_ellipses_color(color)
}
pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
self.wrap_map
.update(cx, |map, cx| map.set_wrap_width(width, cx))
@@ -591,6 +602,59 @@ impl DisplaySnapshot {
self.blocks_snapshot.longest_row()
}
pub fn fold_for_line(self: &Self, display_row: u32) -> Option<FoldStatus> {
if self.is_foldable(display_row) {
Some(FoldStatus::Foldable)
} else if self.is_line_folded(display_row) {
Some(FoldStatus::Folded)
} else {
None
}
}
pub fn is_foldable(self: &Self, row: u32) -> bool {
let max_point = self.max_point();
if row >= max_point.row() {
return false;
}
let (start_indent, is_blank) = self.line_indent(row);
if is_blank {
return false;
}
for display_row in next_rows(row, self) {
let (indent, is_blank) = self.line_indent(display_row);
if !is_blank {
return indent > start_indent;
}
}
return false;
}
pub fn foldable_range(self: &Self, row: u32) -> Option<Range<DisplayPoint>> {
let start = DisplayPoint::new(row, self.line_len(row));
if self.is_foldable(row) && !self.is_line_folded(start.row()) {
let (start_indent, _) = self.line_indent(row);
let max_point = self.max_point();
let mut end = None;
for row in next_rows(row, self) {
let (indent, is_blank) = self.line_indent(row);
if !is_blank && indent <= start_indent {
end = Some(DisplayPoint::new(row - 1, self.line_len(row - 1)));
break;
}
}
let end = end.unwrap_or(max_point);
Some(start..end)
} else {
None
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn highlight_ranges<Tag: ?Sized + 'static>(
&self,
@@ -678,6 +742,24 @@ impl ToDisplayPoint for Anchor {
}
}
pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterator<Item = u32> {
let max_row = display_map.max_point().row();
let start_row = display_row + 1;
let mut current = None;
std::iter::from_fn(move || {
if current == None {
current = Some(start_row);
} else {
current = Some(current.unwrap() + 1)
}
if current.unwrap() > max_row {
None
} else {
current
}
})
}
#[cfg(test)]
pub mod tests {
use super::*;
@@ -1167,7 +1249,7 @@ pub mod tests {
vec![
("fn ".to_string(), None),
("out".to_string(), Some(Color::blue())),
("".to_string(), None),
("".to_string(), None),
(" fn ".to_string(), Some(Color::red())),
("inner".to_string(), Some(Color::blue())),
("() {}\n}".to_string(), Some(Color::red())),
@@ -1248,7 +1330,7 @@ pub mod tests {
cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)),
[
("out".to_string(), Some(Color::blue())),
("\n".to_string(), None),
("\n".to_string(), None),
(" \nfn ".to_string(), Some(Color::red())),
("i\n".to_string(), Some(Color::blue()))
]

View File

@@ -4,7 +4,7 @@ use crate::{
ToOffset,
};
use collections::BTreeMap;
use gpui::fonts::HighlightStyle;
use gpui::{color::Color, fonts::HighlightStyle};
use language::{Chunk, Edit, Point, TextSummary};
use parking_lot::Mutex;
use std::{
@@ -133,6 +133,7 @@ impl<'a> FoldMapWriter<'a> {
folds: self.0.folds.clone(),
buffer_snapshot: buffer,
version: self.0.version.load(SeqCst),
ellipses_color: self.0.ellipses_color,
};
(snapshot, edits)
}
@@ -182,6 +183,7 @@ impl<'a> FoldMapWriter<'a> {
folds: self.0.folds.clone(),
buffer_snapshot: buffer,
version: self.0.version.load(SeqCst),
ellipses_color: self.0.ellipses_color,
};
(snapshot, edits)
}
@@ -192,6 +194,7 @@ pub struct FoldMap {
transforms: Mutex<SumTree<Transform>>,
folds: SumTree<Fold>,
version: AtomicUsize,
ellipses_color: Option<Color>,
}
impl FoldMap {
@@ -209,6 +212,7 @@ impl FoldMap {
},
&(),
)),
ellipses_color: None,
version: Default::default(),
};
@@ -217,6 +221,7 @@ impl FoldMap {
folds: this.folds.clone(),
buffer_snapshot: this.buffer.lock().clone(),
version: this.version.load(SeqCst),
ellipses_color: None,
};
(this, snapshot)
}
@@ -233,6 +238,7 @@ impl FoldMap {
folds: self.folds.clone(),
buffer_snapshot: self.buffer.lock().clone(),
version: self.version.load(SeqCst),
ellipses_color: self.ellipses_color,
};
(snapshot, edits)
}
@@ -246,6 +252,15 @@ impl FoldMap {
(FoldMapWriter(self), snapshot, edits)
}
pub fn set_ellipses_color(&mut self, color: Color) -> bool {
if self.ellipses_color != Some(color) {
self.ellipses_color = Some(color);
true
} else {
false
}
}
fn check_invariants(&self) {
if cfg!(test) {
assert_eq!(
@@ -370,7 +385,7 @@ impl FoldMap {
}
if fold.end > fold.start {
let output_text = "";
let output_text = "";
new_transforms.push(
Transform {
summary: TransformSummary {
@@ -477,6 +492,7 @@ pub struct FoldSnapshot {
folds: SumTree<Fold>,
buffer_snapshot: MultiBufferSnapshot,
pub version: usize,
pub ellipses_color: Option<Color>,
}
impl FoldSnapshot {
@@ -739,6 +755,7 @@ impl FoldSnapshot {
max_output_offset: range.end.0,
highlight_endpoints: highlight_endpoints.into_iter().peekable(),
active_highlights: Default::default(),
ellipses_color: self.ellipses_color,
}
}
@@ -1029,6 +1046,7 @@ pub struct FoldChunks<'a> {
max_output_offset: usize,
highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
ellipses_color: Option<Color>,
}
impl<'a> Iterator for FoldChunks<'a> {
@@ -1058,7 +1076,10 @@ impl<'a> Iterator for FoldChunks<'a> {
return Some(Chunk {
text: output_text,
syntax_highlight_id: None,
highlight_style: None,
highlight_style: self.ellipses_color.map(|color| HighlightStyle {
color: Some(color),
..Default::default()
}),
diagnostic_severity: None,
is_unnecessary: false,
});
@@ -1214,7 +1235,7 @@ mod tests {
Point::new(0, 2)..Point::new(2, 2),
Point::new(2, 4)..Point::new(4, 1),
]);
assert_eq!(snapshot2.text(), "aacceeeee");
assert_eq!(snapshot2.text(), "aacceeeee");
assert_eq!(
edits,
&[
@@ -1241,7 +1262,7 @@ mod tests {
buffer.snapshot(cx)
});
let (snapshot3, edits) = map.read(buffer_snapshot, subscription.consume().into_inner());
assert_eq!(snapshot3.text(), "123ac123ceeeee");
assert_eq!(snapshot3.text(), "123ac123ceeeee");
assert_eq!(
edits,
&[
@@ -1261,12 +1282,12 @@ mod tests {
buffer.snapshot(cx)
});
let (snapshot4, _) = map.read(buffer_snapshot.clone(), subscription.consume().into_inner());
assert_eq!(snapshot4.text(), "123ac123456eee");
assert_eq!(snapshot4.text(), "123ac123456eee");
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), false);
let (snapshot5, _) = map.read(buffer_snapshot.clone(), vec![]);
assert_eq!(snapshot5.text(), "123ac123456eee");
assert_eq!(snapshot5.text(), "123ac123456eee");
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), true);
@@ -1287,19 +1308,19 @@ mod tests {
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
writer.fold(vec![5..8]);
let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "abcdeijkl");
assert_eq!(snapshot.text(), "abcdeijkl");
// Create an fold adjacent to the start of the first fold.
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
writer.fold(vec![0..1, 2..5]);
let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "…b…ijkl");
assert_eq!(snapshot.text(), "⋯b⋯ijkl");
// Create an fold adjacent to the end of the first fold.
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
writer.fold(vec![11..11, 8..10]);
let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "…b…kl");
assert_eq!(snapshot.text(), "⋯b⋯kl");
}
{
@@ -1309,7 +1330,7 @@ mod tests {
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
writer.fold(vec![0..2, 2..5]);
let (snapshot, _) = map.read(buffer_snapshot, vec![]);
assert_eq!(snapshot.text(), "fghijkl");
assert_eq!(snapshot.text(), "fghijkl");
// Edit within one of the folds.
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
@@ -1317,7 +1338,7 @@ mod tests {
buffer.snapshot(cx)
});
let (snapshot, _) = map.read(buffer_snapshot, subscription.consume().into_inner());
assert_eq!(snapshot.text(), "12345fghijkl");
assert_eq!(snapshot.text(), "12345fghijkl");
}
}
@@ -1334,7 +1355,7 @@ mod tests {
Point::new(3, 1)..Point::new(4, 1),
]);
let (snapshot, _) = map.read(buffer_snapshot, vec![]);
assert_eq!(snapshot.text(), "aaeeeee");
assert_eq!(snapshot.text(), "aaeeeee");
}
#[gpui::test]
@@ -1351,14 +1372,14 @@ mod tests {
Point::new(3, 1)..Point::new(4, 1),
]);
let (snapshot, _) = map.read(buffer_snapshot, vec![]);
assert_eq!(snapshot.text(), "aacccc\ndeeeee");
assert_eq!(snapshot.text(), "aacccc\ndeeeee");
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], None, cx);
buffer.snapshot(cx)
});
let (snapshot, _) = map.read(buffer_snapshot, subscription.consume().into_inner());
assert_eq!(snapshot.text(), "aaeeeee");
assert_eq!(snapshot.text(), "aaeeeee");
}
#[gpui::test]
@@ -1450,7 +1471,7 @@ mod tests {
let mut expected_text: String = buffer_snapshot.text().to_string();
for fold_range in map.merged_fold_ranges().into_iter().rev() {
expected_text.replace_range(fold_range.start..fold_range.end, "");
expected_text.replace_range(fold_range.start..fold_range.end, "");
}
assert_eq!(snapshot.text(), expected_text);
@@ -1655,7 +1676,7 @@ mod tests {
]);
let (snapshot, _) = map.read(buffer_snapshot, vec![]);
assert_eq!(snapshot.text(), "aacccc\ndeeeee\nffffff\n");
assert_eq!(snapshot.text(), "aacccc\ndeeeee\nffffff\n");
assert_eq!(
snapshot.buffer_rows(0).collect::<Vec<_>>(),
[Some(0), Some(3), Some(5), Some(6)]

View File

@@ -1,6 +1,7 @@
mod blink_manager;
pub mod display_map;
mod element;
mod git;
mod highlight_matching_bracket;
mod hover_popover;
@@ -160,6 +161,21 @@ pub struct ToggleComments {
pub advance_downwards: bool,
}
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct FoldAt {
pub display_row: u32,
}
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct UnfoldAt {
pub display_row: u32,
}
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct GutterHover {
pub hovered: bool,
}
actions!(
editor,
[
@@ -258,6 +274,9 @@ impl_actions!(
ConfirmCompletion,
ConfirmCodeAction,
ToggleComments,
FoldAt,
UnfoldAt,
GutterHover
]
);
@@ -348,7 +367,10 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(Editor::go_to_definition);
cx.add_action(Editor::go_to_type_definition);
cx.add_action(Editor::fold);
cx.add_action(Editor::fold_at);
cx.add_action(Editor::unfold_lines);
cx.add_action(Editor::unfold_at);
cx.add_action(Editor::gutter_hover);
cx.add_action(Editor::fold_selected_ranges);
cx.add_action(Editor::show_completions);
cx.add_action(Editor::toggle_code_actions);
@@ -480,6 +502,7 @@ pub struct Editor {
leader_replica_id: Option<u16>,
remote_id: Option<ViewId>,
hover_state: HoverState,
gutter_hovered: bool,
link_go_to_definition_state: LinkGoToDefinitionState,
_subscriptions: Vec<Subscription>,
}
@@ -1151,6 +1174,7 @@ impl Editor {
remote_id: None,
hover_state: Default::default(),
link_go_to_definition_state: Default::default(),
gutter_hovered: false,
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
cx.subscribe(&buffer, Self::on_buffer_event),
@@ -2645,14 +2669,15 @@ impl Editor {
pub fn render_code_actions_indicator(
&self,
style: &EditorStyle,
active: bool,
cx: &mut RenderContext<Self>,
) -> Option<ElementBox> {
if self.available_code_actions.is_some() {
enum Tag {}
enum CodeActions {}
Some(
MouseEventHandler::<Tag>::new(0, cx, |_, _| {
MouseEventHandler::<CodeActions>::new(0, cx, |state, _| {
Svg::new("icons/bolt_8.svg")
.with_color(style.code_actions.indicator)
.with_color(style.code_actions.indicator.style_for(state, active).color)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
@@ -2669,6 +2694,80 @@ impl Editor {
}
}
pub fn render_fold_indicators(
&self,
fold_data: Option<Vec<(u32, FoldStatus)>>,
active_rows: &BTreeMap<u32, bool>,
style: &EditorStyle,
gutter_hovered: bool,
line_height: f32,
gutter_margin: f32,
cx: &mut RenderContext<Self>,
) -> Option<Vec<(u32, ElementBox)>> {
enum FoldIndicators {}
let style = style.folds.clone();
fold_data.map(|fold_data| {
fold_data
.iter()
.copied()
.filter_map(|(fold_location, fold_status)| {
(gutter_hovered
|| fold_status == FoldStatus::Folded
|| !*active_rows.get(&fold_location).unwrap_or(&true))
.then(|| {
(
fold_location,
MouseEventHandler::<FoldIndicators>::new(
fold_location as usize,
cx,
|mouse_state, _| -> ElementBox {
Svg::new(match fold_status {
FoldStatus::Folded => style.folded_icon.clone(),
FoldStatus::Foldable => style.foldable_icon.clone(),
})
.with_color(
style
.indicator
.style_for(
mouse_state,
fold_status == FoldStatus::Folded,
)
.color,
)
.constrained()
.with_width(style.icon_width)
.aligned()
.constrained()
.with_height(line_height)
.with_width(gutter_margin)
.aligned()
.boxed()
},
)
.with_cursor_style(CursorStyle::PointingHand)
.with_padding(Padding::uniform(3.))
.on_click(MouseButton::Left, {
move |_, cx| {
cx.dispatch_any_action(match fold_status {
FoldStatus::Folded => Box::new(UnfoldAt {
display_row: fold_location,
}),
FoldStatus::Foldable => Box::new(FoldAt {
display_row: fold_location,
}),
});
}
})
.boxed(),
)
})
})
.collect()
})
}
pub fn context_menu_visible(&self) -> bool {
self.context_menu
.as_ref()
@@ -3251,26 +3350,12 @@ impl Editor {
while let Some(selection) = selections.next() {
// Find all the selections that span a contiguous row range
contiguous_row_selections.push(selection.clone());
let start_row = selection.start.row;
let mut end_row = if selection.end.column > 0 || selection.is_empty() {
display_map.next_line_boundary(selection.end).0.row + 1
} else {
selection.end.row
};
while let Some(next_selection) = selections.peek() {
if next_selection.start.row <= end_row {
end_row = if next_selection.end.column > 0 || next_selection.is_empty() {
display_map.next_line_boundary(next_selection.end).0.row + 1
} else {
next_selection.end.row
};
contiguous_row_selections.push(selections.next().unwrap().clone());
} else {
break;
}
}
let (start_row, end_row) = consume_contiguous_rows(
&mut contiguous_row_selections,
selection,
&display_map,
&mut selections,
);
// Move the text spanned by the row range to be before the line preceding the row range
if start_row > 0 {
@@ -3335,13 +3420,13 @@ impl Editor {
}
self.transact(cx, |this, cx| {
this.unfold_ranges(unfold_ranges, true, cx);
this.unfold_ranges(unfold_ranges, true, true, cx);
this.buffer.update(cx, |buffer, cx| {
for (range, text) in edits {
buffer.edit([(range, text)], None, cx);
}
});
this.fold_ranges(refold_ranges, cx);
this.fold_ranges(refold_ranges, true, cx);
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(new_selections);
})
@@ -3363,26 +3448,12 @@ impl Editor {
while let Some(selection) = selections.next() {
// Find all the selections that span a contiguous row range
contiguous_row_selections.push(selection.clone());
let start_row = selection.start.row;
let mut end_row = if selection.end.column > 0 || selection.is_empty() {
display_map.next_line_boundary(selection.end).0.row + 1
} else {
selection.end.row
};
while let Some(next_selection) = selections.peek() {
if next_selection.start.row <= end_row {
end_row = if next_selection.end.column > 0 || next_selection.is_empty() {
display_map.next_line_boundary(next_selection.end).0.row + 1
} else {
next_selection.end.row
};
contiguous_row_selections.push(selections.next().unwrap().clone());
} else {
break;
}
}
let (start_row, end_row) = consume_contiguous_rows(
&mut contiguous_row_selections,
selection,
&display_map,
&mut selections,
);
// Move the text spanned by the row range to be after the last line of the row range
if end_row <= buffer.max_point().row {
@@ -3440,13 +3511,13 @@ impl Editor {
}
self.transact(cx, |this, cx| {
this.unfold_ranges(unfold_ranges, true, cx);
this.unfold_ranges(unfold_ranges, true, true, cx);
this.buffer.update(cx, |buffer, cx| {
for (range, text) in edits {
buffer.edit([(range, text)], None, cx);
}
});
this.fold_ranges(refold_ranges, cx);
this.fold_ranges(refold_ranges, true, cx);
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
});
}
@@ -4274,7 +4345,7 @@ impl Editor {
to_unfold.push(selection.start..selection.end);
}
}
self.unfold_ranges(to_unfold, true, cx);
self.unfold_ranges(to_unfold, true, true, cx);
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(new_selection_ranges);
});
@@ -4423,7 +4494,7 @@ impl Editor {
}
if let Some(next_selected_range) = next_selected_range {
self.unfold_ranges([next_selected_range.clone()], false, cx);
self.unfold_ranges([next_selected_range.clone()], false, true, cx);
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
if action.replace_newest {
s.delete(s.newest_anchor().id);
@@ -4456,7 +4527,7 @@ impl Editor {
wordwise: true,
done: false,
};
self.unfold_ranges([selection.start..selection.end], false, cx);
self.unfold_ranges([selection.start..selection.end], false, true, cx);
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
s.select(selections);
});
@@ -5459,21 +5530,20 @@ impl Editor {
None => return None,
};
Some(self.perform_format(project, cx))
Some(self.perform_format(project, FormatTrigger::Manual, cx))
}
fn perform_format(
&mut self,
project: ModelHandle<Project>,
trigger: FormatTrigger,
cx: &mut ViewContext<'_, Self>,
) -> Task<Result<()>> {
let buffer = self.buffer().clone();
let buffers = buffer.read(cx).all_buffers();
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
let format = project.update(cx, |project, cx| {
project.format(buffers, true, FormatTrigger::Manual, cx)
});
let format = project.update(cx, |project, cx| project.format(buffers, true, trigger, cx));
cx.spawn(|_, mut cx| async move {
let transaction = futures::select_biased! {
@@ -5677,14 +5747,18 @@ impl Editor {
let mut fold_ranges = Vec::new();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all::<Point>(cx);
for selection in selections {
let range = selection.display_range(&display_map).sorted();
let buffer_start_row = range.start.to_point(&display_map).row;
for row in (0..=range.end.row()).rev() {
if self.is_line_foldable(&display_map, row) && !display_map.is_line_folded(row) {
let fold_range = self.foldable_range_for_line(&display_map, row);
let fold_range = display_map.foldable_range(row).map(|range| {
range.start.to_point(&display_map)..range.end.to_point(&display_map)
});
if let Some(fold_range) = fold_range {
if fold_range.end.row >= buffer_start_row {
fold_ranges.push(fold_range);
if row <= range.start.row() {
@@ -5695,7 +5769,26 @@ impl Editor {
}
}
self.fold_ranges(fold_ranges, cx);
self.fold_ranges(fold_ranges, true, cx);
}
pub fn fold_at(&mut self, fold_at: &FoldAt, cx: &mut ViewContext<Self>) {
let display_row = fold_at.display_row;
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
if let Some(fold_range) = display_map.foldable_range(display_row) {
let autoscroll = self
.selections
.all::<Point>(cx)
.iter()
.any(|selection| fold_range.overlaps(&selection.display_range(&display_map)));
let fold_range =
fold_range.start.to_point(&display_map)..fold_range.end.to_point(&display_map);
self.fold_ranges(std::iter::once(fold_range), autoscroll, cx);
}
}
pub fn unfold_lines(&mut self, _: &UnfoldLines, cx: &mut ViewContext<Self>) {
@@ -5713,85 +5806,86 @@ impl Editor {
start..end
})
.collect::<Vec<_>>();
self.unfold_ranges(ranges, true, cx);
self.unfold_ranges(ranges, true, true, cx);
}
fn is_line_foldable(&self, display_map: &DisplaySnapshot, display_row: u32) -> bool {
let max_point = display_map.max_point();
if display_row >= max_point.row() {
false
} else {
let (start_indent, is_blank) = display_map.line_indent(display_row);
if is_blank {
false
} else {
for display_row in display_row + 1..=max_point.row() {
let (indent, is_blank) = display_map.line_indent(display_row);
if !is_blank {
return indent > start_indent;
}
}
false
}
}
}
pub fn unfold_at(&mut self, unfold_at: &UnfoldAt, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
fn foldable_range_for_line(
&self,
display_map: &DisplaySnapshot,
start_row: u32,
) -> Range<Point> {
let max_point = display_map.max_point();
let intersection_range = DisplayPoint::new(unfold_at.display_row, 0)
..DisplayPoint::new(
unfold_at.display_row,
display_map.line_len(unfold_at.display_row),
);
let (start_indent, _) = display_map.line_indent(start_row);
let start = DisplayPoint::new(start_row, display_map.line_len(start_row));
let mut end = None;
for row in start_row + 1..=max_point.row() {
let (indent, is_blank) = display_map.line_indent(row);
if !is_blank && indent <= start_indent {
end = Some(DisplayPoint::new(row - 1, display_map.line_len(row - 1)));
break;
}
}
let autoscroll =
self.selections.all::<Point>(cx).iter().any(|selection| {
intersection_range.overlaps(&selection.display_range(&display_map))
});
let end = end.unwrap_or(max_point);
start.to_point(display_map)..end.to_point(display_map)
let display_point = DisplayPoint::new(unfold_at.display_row, 0).to_point(&display_map);
let mut point_range = display_point..display_point;
point_range.start.column = 0;
point_range.end.column = display_map.buffer_snapshot.line_len(point_range.end.row);
self.unfold_ranges(std::iter::once(point_range), true, autoscroll, cx)
}
pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext<Self>) {
let selections = self.selections.all::<Point>(cx);
let ranges = selections.into_iter().map(|s| s.start..s.end);
self.fold_ranges(ranges, cx);
self.fold_ranges(ranges, true, cx);
}
pub fn fold_ranges<T: ToOffset>(
pub fn fold_ranges<T: ToOffset + Clone>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
auto_scroll: bool,
cx: &mut ViewContext<Self>,
) {
let mut ranges = ranges.into_iter().peekable();
if ranges.peek().is_some() {
self.display_map.update(cx, |map, cx| map.fold(ranges, cx));
self.request_autoscroll(Autoscroll::fit(), cx);
if auto_scroll {
self.request_autoscroll(Autoscroll::fit(), cx);
}
cx.notify();
}
}
pub fn unfold_ranges<T: ToOffset>(
pub fn unfold_ranges<T: ToOffset + Clone>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
inclusive: bool,
auto_scroll: bool,
cx: &mut ViewContext<Self>,
) {
let mut ranges = ranges.into_iter().peekable();
if ranges.peek().is_some() {
self.display_map
.update(cx, |map, cx| map.unfold(ranges, inclusive, cx));
self.request_autoscroll(Autoscroll::fit(), cx);
if auto_scroll {
self.request_autoscroll(Autoscroll::fit(), cx);
}
cx.notify();
}
}
pub fn gutter_hover(
&mut self,
GutterHover { hovered }: &GutterHover,
cx: &mut ViewContext<Self>,
) {
self.gutter_hovered = *hovered;
cx.notify();
}
pub fn insert_blocks(
&mut self,
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
@@ -6253,6 +6347,35 @@ impl Editor {
}
}
fn consume_contiguous_rows(
contiguous_row_selections: &mut Vec<Selection<Point>>,
selection: &Selection<Point>,
display_map: &DisplaySnapshot,
selections: &mut std::iter::Peekable<std::slice::Iter<Selection<Point>>>,
) -> (u32, u32) {
contiguous_row_selections.push(selection.clone());
let start_row = selection.start.row;
let mut end_row = ending_row(selection, display_map);
while let Some(next_selection) = selections.peek() {
if next_selection.start.row <= end_row {
end_row = ending_row(next_selection, display_map);
contiguous_row_selections.push(selections.next().unwrap().clone());
} else {
break;
}
}
(start_row, end_row)
}
fn ending_row(next_selection: &Selection<Point>, display_map: &DisplaySnapshot) -> u32 {
if next_selection.end.column > 0 || next_selection.is_empty() {
display_map.next_line_boundary(next_selection.end).0.row + 1
} else {
next_selection.end.row
}
}
impl EditorSnapshot {
pub fn language_at<T: ToOffset>(&self, position: T) -> Option<&Arc<Language>> {
self.display_snapshot.buffer_snapshot.language_at(position)
@@ -6324,6 +6447,7 @@ impl View for Editor {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let style = self.style(cx);
let font_changed = self.display_map.update(cx, |map, cx| {
map.set_fold_ellipses_color(style.folds.ellipses.text_color);
map.set_font(style.text.font_id, style.text.font_size, cx)
});
@@ -6433,17 +6557,13 @@ impl View for Editor {
EditorMode::AutoHeight { .. } => "auto_height",
EditorMode::Full => "full",
};
context.map.insert("mode".into(), mode.into());
context.add_key("mode", mode);
if self.pending_rename.is_some() {
context.set.insert("renaming".into());
context.add_identifier("renaming");
}
match self.context_menu.as_ref() {
Some(ContextMenu::Completions(_)) => {
context.set.insert("showing_completions".into());
}
Some(ContextMenu::CodeActions(_)) => {
context.set.insert("showing_code_actions".into());
}
Some(ContextMenu::Completions(_)) => context.add_identifier("showing_completions"),
Some(ContextMenu::CodeActions(_)) => context.add_identifier("showing_code_actions"),
None => {}
}

View File

@@ -1,23 +1,23 @@
use drag_and_drop::DragAndDrop;
use futures::StreamExt;
use indoc::indoc;
use std::{cell::RefCell, rc::Rc, time::Instant};
use unindent::Unindent;
use super::*;
use crate::test::{
assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
editor_test_context::EditorTestContext, select_ranges,
};
use drag_and_drop::DragAndDrop;
use futures::StreamExt;
use gpui::{
executor::Deterministic,
geometry::{rect::RectF, vector::vec2f},
platform::{WindowBounds, WindowOptions},
serde_json,
};
use indoc::indoc;
use language::{BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point};
use parking_lot::Mutex;
use project::FakeFs;
use settings::EditorSettings;
use std::{cell::RefCell, rc::Rc, time::Instant};
use unindent::Unindent;
use util::{
assert_set_eq,
test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
@@ -447,6 +447,7 @@ fn test_clone(cx: &mut gpui::MutableAppContext) {
Point::new(1, 0)..Point::new(2, 0),
Point::new(3, 0)..Point::new(4, 0),
],
true,
cx,
);
});
@@ -669,10 +670,10 @@ fn test_fold(cx: &mut gpui::MutableAppContext) {
1
}
fn b() {
fn b() {
}
fn c() {
fn c() {
}
}
"
@@ -683,7 +684,7 @@ fn test_fold(cx: &mut gpui::MutableAppContext) {
assert_eq!(
view.display_text(cx),
"
impl Foo {
impl Foo {
}
"
.unindent(),
@@ -700,10 +701,10 @@ fn test_fold(cx: &mut gpui::MutableAppContext) {
1
}
fn b() {
fn b() {
}
fn c() {
fn c() {
}
}
"
@@ -807,9 +808,10 @@ fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) {
Point::new(1, 2)..Point::new(1, 4),
Point::new(2, 4)..Point::new(2, 8),
],
true,
cx,
);
assert_eq!(view.display_text(cx), "ⓐⓑ\nabe\nαβε\n");
assert_eq!(view.display_text(cx), "ⓐⓑ\nabe\nαβε\n");
view.move_right(&MoveRight, cx);
assert_eq!(
@@ -824,13 +826,13 @@ fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) {
view.move_right(&MoveRight, cx);
assert_eq!(
view.selections.display_ranges(cx),
&[empty_range(0, "ⓐⓑ".len())]
&[empty_range(0, "ⓐⓑ".len())]
);
view.move_down(&MoveDown, cx);
assert_eq!(
view.selections.display_ranges(cx),
&[empty_range(1, "ab".len())]
&[empty_range(1, "ab".len())]
);
view.move_left(&MoveLeft, cx);
assert_eq!(
@@ -856,28 +858,28 @@ fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) {
view.move_right(&MoveRight, cx);
assert_eq!(
view.selections.display_ranges(cx),
&[empty_range(2, "αβ".len())]
&[empty_range(2, "αβ".len())]
);
view.move_right(&MoveRight, cx);
assert_eq!(
view.selections.display_ranges(cx),
&[empty_range(2, "αβε".len())]
&[empty_range(2, "αβε".len())]
);
view.move_up(&MoveUp, cx);
assert_eq!(
view.selections.display_ranges(cx),
&[empty_range(1, "abe".len())]
&[empty_range(1, "abe".len())]
);
view.move_up(&MoveUp, cx);
assert_eq!(
view.selections.display_ranges(cx),
&[empty_range(0, "ⓐⓑ".len())]
&[empty_range(0, "ⓐⓑ".len())]
);
view.move_left(&MoveLeft, cx);
assert_eq!(
view.selections.display_ranges(cx),
&[empty_range(0, "ⓐⓑ".len())]
&[empty_range(0, "ⓐⓑ".len())]
);
view.move_left(&MoveLeft, cx);
assert_eq!(
@@ -2119,6 +2121,7 @@ fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
Point::new(2, 3)..Point::new(4, 1),
Point::new(7, 0)..Point::new(8, 4),
],
true,
cx,
);
view.change_selections(None, cx, |s| {
@@ -2131,13 +2134,13 @@ fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
});
assert_eq!(
view.display_text(cx),
"aabbb\nccceeee\nfffff\nggggg\ni\njjjjj"
"aabbb\nccceeee\nfffff\nggggg\ni\njjjjj"
);
view.move_line_up(&MoveLineUp, cx);
assert_eq!(
view.display_text(cx),
"aabbb\nccceeee\nggggg\ni\njjjjj\nfffff"
"aabbb\nccceeee\nggggg\ni\njjjjj\nfffff"
);
assert_eq!(
view.selections.display_ranges(cx),
@@ -2154,7 +2157,7 @@ fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
view.move_line_down(&MoveLineDown, cx);
assert_eq!(
view.display_text(cx),
"ccceeee\naabbb\nfffff\nggggg\ni\njjjjj"
"ccceeee\naabbb\nfffff\nggggg\ni\njjjjj"
);
assert_eq!(
view.selections.display_ranges(cx),
@@ -2171,7 +2174,7 @@ fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
view.move_line_down(&MoveLineDown, cx);
assert_eq!(
view.display_text(cx),
"ccceeee\nfffff\naabbb\nggggg\ni\njjjjj"
"ccceeee\nfffff\naabbb\nggggg\ni\njjjjj"
);
assert_eq!(
view.selections.display_ranges(cx),
@@ -2188,7 +2191,7 @@ fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
view.move_line_up(&MoveLineUp, cx);
assert_eq!(
view.display_text(cx),
"ccceeee\naabbb\nggggg\ni\njjjjj\nfffff"
"ccceeee\naabbb\nggggg\ni\njjjjj\nfffff"
);
assert_eq!(
view.selections.display_ranges(cx),
@@ -2586,6 +2589,7 @@ fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) {
Point::new(2, 3)..Point::new(4, 1),
Point::new(7, 0)..Point::new(8, 4),
],
true,
cx,
);
view.change_selections(None, cx, |s| {
@@ -2596,14 +2600,14 @@ fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) {
DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
])
});
assert_eq!(view.display_text(cx), "aabbb\nccceeee\nfffff\nggggg\ni");
assert_eq!(view.display_text(cx), "aabbb\nccceeee\nfffff\nggggg\ni");
});
view.update(cx, |view, cx| {
view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
assert_eq!(
view.display_text(cx),
"aaaaa\nbbbbb\nccceeee\nfffff\nggggg\ni"
"aaaaa\nbbbbb\nccceeee\nfffff\nggggg\ni"
);
assert_eq!(
view.selections.display_ranges(cx),
@@ -2983,6 +2987,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
Point::new(0, 21)..Point::new(0, 24),
Point::new(3, 20)..Point::new(3, 22),
],
true,
cx,
);
view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
@@ -4193,7 +4198,9 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
let format = editor.update(cx, |editor, cx| editor.perform_format(project.clone(), cx));
let format = editor.update(cx, |editor, cx| {
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
});
fake_server
.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
assert_eq!(
@@ -4225,7 +4232,9 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
futures::future::pending::<()>().await;
unreachable!()
});
let format = editor.update(cx, |editor, cx| editor.perform_format(project, cx));
let format = editor.update(cx, |editor, cx| {
editor.perform_format(project, FormatTrigger::Manual, cx)
});
cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
cx.foreground().start_waiting();
format.await.unwrap();
@@ -4292,6 +4301,121 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
"});
}
#[gpui::test]
async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking();
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
document_formatting_provider: Some(lsp::OneOf::Left(true)),
..Default::default()
},
cx,
)
.await;
// Set up a buffer white some trailing whitespace and no trailing newline.
cx.set_state(
&[
"one ", //
"twoˇ", //
"three ", //
"four", //
]
.join("\n"),
);
// Submit a format request.
let format = cx
.update_editor(|editor, cx| editor.format(&Format, cx))
.unwrap();
// Record which buffer changes have been sent to the language server
let buffer_changes = Arc::new(Mutex::new(Vec::new()));
cx.lsp
.handle_notification::<lsp::notification::DidChangeTextDocument, _>({
let buffer_changes = buffer_changes.clone();
move |params, _| {
buffer_changes.lock().extend(
params
.content_changes
.into_iter()
.map(|e| (e.range.unwrap(), e.text)),
);
}
});
// Handle formatting requests to the language server.
cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
let buffer_changes = buffer_changes.clone();
move |_, _| {
// When formatting is requested, trailing whitespace has already been stripped,
// and the trailing newline has already been added.
assert_eq!(
&buffer_changes.lock()[1..],
&[
(
lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
"".into()
),
(
lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
"".into()
),
(
lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
"\n".into()
),
]
);
// Insert blank lines between each line of the buffer.
async move {
Ok(Some(vec![
lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
new_text: "\n".into(),
},
lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
new_text: "\n".into(),
},
]))
}
}
});
// After formatting the buffer, the trailing whitespace is stripped,
// a newline is appended, and the edits provided by the language server
// have been applied.
format.await.unwrap();
cx.assert_editor_state(
&[
"one", //
"", //
"twoˇ", //
"", //
"three", //
"four", //
"", //
]
.join("\n"),
);
// Undoing the formatting undoes the trailing whitespace removal, the
// trailing newline, and the LSP edits.
cx.update_buffer(|buffer, cx| buffer.undo(cx));
cx.assert_editor_state(
&[
"one ", //
"twoˇ", //
"three ", //
"four", //
]
.join("\n"),
);
}
#[gpui::test]
async fn test_completion(cx: &mut gpui::TestAppContext) {
let mut cx = EditorLspTestContext::new_rust(

View File

@@ -4,7 +4,7 @@ use super::{
ToPoint, MAX_LINE_LEN,
};
use crate::{
display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
git::{diff_hunk_to_display, DisplayDiffHunk},
hover_popover::{
HideHover, HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
@@ -14,7 +14,7 @@ use crate::{
},
mouse_context_menu::DeployMouseContextMenu,
scroll::actions::Scroll,
EditorStyle,
EditorStyle, GutterHover, UnfoldAt,
};
use clock::ReplicaId;
use collections::{BTreeMap, HashMap};
@@ -48,6 +48,9 @@ use std::{
ops::{DerefMut, Range},
sync::Arc,
};
use workspace::item::Item;
enum FoldMarkers {}
struct SelectionLayout {
head: DisplayPoint,
@@ -212,6 +215,17 @@ impl EditorElement {
}
}),
);
enum GutterHandlers {}
cx.scene.push_mouse_region(
MouseRegion::new::<GutterHandlers>(view.id(), view.id() + 1, gutter_bounds).on_hover(
|hover, cx| {
cx.dispatch_action(GutterHover {
hovered: hover.started,
})
},
),
)
}
fn mouse_down(
@@ -400,16 +414,7 @@ impl EditorElement {
) -> bool {
// This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
// Don't trigger hover popover if mouse is hovering over context menu
let point = if text_bounds.contains_point(position) {
let (point, target_point) = position_map.point_for_position(text_bounds, position);
if point == target_point {
Some(point)
} else {
None
}
} else {
None
};
let point = position_to_display_point(position, text_bounds, position_map);
cx.dispatch_action(UpdateGoToDefinitionLink {
point,
@@ -418,6 +423,7 @@ impl EditorElement {
});
cx.dispatch_action(HoverAt { point });
true
}
@@ -569,12 +575,25 @@ impl EditorElement {
}
if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
let mut x = bounds.width() - layout.gutter_padding;
let mut x = 0.;
let mut y = *row as f32 * line_height - scroll_top;
x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
y += (line_height - indicator.size().y()) / 2.;
indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
}
layout.fold_indicators.as_mut().map(|fold_indicators| {
for (line, fold_indicator) in fold_indicators.iter_mut() {
let mut x = bounds.width() - layout.gutter_padding;
let mut y = *line as f32 * line_height - scroll_top;
x += ((layout.gutter_padding + layout.gutter_margin) - fold_indicator.size().x())
/ 2.;
y += (line_height - fold_indicator.size().y()) / 2.;
fold_indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
}
});
}
fn paint_diff_hunks(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
@@ -676,6 +695,7 @@ impl EditorElement {
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.);
let line_end_overshoot = 0.15 * layout.position_map.line_height;
cx.scene.push_layer(Some(bounds));
@@ -688,12 +708,54 @@ impl EditorElement {
},
});
let fold_corner_radius =
self.style.folds.ellipses.corner_radius_factor * layout.position_map.line_height;
for (id, range, color) in layout.fold_ranges.iter() {
self.paint_highlighted_range(
range.clone(),
*color,
fold_corner_radius,
fold_corner_radius * 2.,
layout,
content_origin,
scroll_top,
scroll_left,
bounds,
cx,
);
for bound in range_to_bounds(
&range,
content_origin,
scroll_left,
scroll_top,
&layout.visible_display_row_range,
line_end_overshoot,
&layout.position_map,
) {
cx.scene.push_cursor_region(CursorRegion {
bounds: bound,
style: CursorStyle::PointingHand,
});
let display_row = range.start.row();
cx.scene.push_mouse_region(
MouseRegion::new::<FoldMarkers>(self.view.id(), *id as usize, bound)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(UnfoldAt { display_row })
})
.with_notify_on_hover(true)
.with_notify_on_click(true),
)
}
}
for (range, color) in &layout.highlighted_ranges {
self.paint_highlighted_range(
range.clone(),
*color,
0.,
0.15 * layout.position_map.line_height,
line_end_overshoot,
layout,
content_origin,
scroll_top,
@@ -704,9 +766,10 @@ impl EditorElement {
}
let mut cursors = SmallVec::<[Cursor; 32]>::new();
let corner_radius = 0.15 * layout.position_map.line_height;
for (replica_id, selections) in &layout.selections {
let selection_style = style.replica_selection_style(*replica_id);
let corner_radius = 0.15 * layout.position_map.line_height;
for selection in selections {
self.paint_highlighted_range(
@@ -1118,6 +1181,24 @@ impl EditorElement {
.width()
}
fn get_fold_indicators(
&self,
is_singleton: bool,
display_rows: Range<u32>,
snapshot: &EditorSnapshot,
) -> Option<Vec<(u32, FoldStatus)>> {
is_singleton.then(|| {
display_rows
.into_iter()
.filter_map(|display_row| {
snapshot
.fold_for_line(display_row)
.map(|fold_status| (display_row, fold_status))
})
.collect()
})
}
//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(
@@ -1438,7 +1519,7 @@ impl EditorElement {
} else {
let text_style = self.style.text.clone();
Flex::row()
.with_child(Label::new("", text_style).boxed())
.with_child(Label::new("", text_style).boxed())
.with_children(jump_icon)
.contained()
.with_padding_left(gutter_padding)
@@ -1606,9 +1687,13 @@ impl Element for EditorElement {
let mut active_rows = BTreeMap::new();
let mut highlighted_rows = None;
let mut highlighted_ranges = Vec::new();
let mut fold_ranges = Vec::new();
let mut show_scrollbars = false;
let mut include_root = false;
let mut is_singleton = false;
self.update_view(cx.app, |view, cx| {
is_singleton = view.is_singleton(cx);
let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
highlighted_rows = view.highlighted_rows();
@@ -1616,6 +1701,19 @@ impl Element for EditorElement {
highlighted_ranges =
view.background_highlights_in_range(start_anchor..end_anchor, &display_map, theme);
fold_ranges.extend(
snapshot
.folds_in_range(start_anchor..end_anchor)
.map(|anchor| {
let start = anchor.start.to_point(&snapshot.buffer_snapshot);
(
start.row,
start.to_display_point(&snapshot.display_snapshot)
..anchor.end.to_display_point(&snapshot),
)
}),
);
let mut remote_selections = HashMap::default();
for (replica_id, line_mode, cursor_shape, selection) in display_map
.buffer_snapshot
@@ -1684,11 +1782,28 @@ impl Element for EditorElement {
.unwrap_or_default()
});
let fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)> = fold_ranges
.into_iter()
.map(|(id, fold)| {
let color = self
.style
.folds
.ellipses
.background
.style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize), false)
.color;
(id, fold, color)
})
.collect();
let line_number_layouts =
self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx);
let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot);
let folds = self.get_fold_indicators(is_singleton, 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;
@@ -1755,7 +1870,7 @@ impl Element for EditorElement {
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 mut fold_indicators = cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
let newest_selection_head = view
.selections
.newest::<usize>(cx)
@@ -1769,14 +1884,26 @@ impl Element for EditorElement {
view.render_context_menu(newest_selection_head, style.clone(), cx);
}
let active = matches!(view.context_menu, Some(crate::ContextMenu::CodeActions(_)));
code_actions_indicator = view
.render_code_actions_indicator(&style, cx)
.render_code_actions_indicator(&style, active, cx)
.map(|indicator| (newest_selection_head.row(), indicator));
}
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;
view.render_fold_indicators(
folds,
&active_rows,
&style,
view.gutter_hovered,
line_height,
gutter_margin,
cx,
)
});
if let Some((_, context_menu)) = context_menu.as_mut() {
@@ -1802,6 +1929,18 @@ impl Element for EditorElement {
);
}
fold_indicators.as_mut().map(|fold_indicators| {
for (_, indicator) in fold_indicators.iter_mut() {
indicator.layout(
SizeConstraint::strict_along(
Axis::Vertical,
line_height * style.code_actions.vertical_scale,
),
cx,
);
}
});
if let Some((_, hover_popovers)) = hover.as_mut() {
for hover_popover in hover_popovers.iter_mut() {
hover_popover.layout(
@@ -1845,12 +1984,14 @@ impl Element for EditorElement {
active_rows,
highlighted_rows,
highlighted_ranges,
fold_ranges,
line_number_layouts,
display_hunks,
blocks,
selections,
context_menu,
code_actions_indicator,
fold_indicators,
hover_popovers: hover,
},
)
@@ -1958,6 +2099,8 @@ impl Element for EditorElement {
}
}
type BufferRow = u32;
pub struct LayoutState {
position_map: Arc<PositionMap>,
gutter_size: Vector2F,
@@ -1972,6 +2115,7 @@ pub struct LayoutState {
display_hunks: Vec<DisplayDiffHunk>,
blocks: Vec<BlockLayout>,
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)>,
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
scrollbar_row_range: Range<f32>,
show_scrollbars: bool,
@@ -1979,6 +2123,7 @@ pub struct LayoutState {
context_menu: Option<(DisplayPoint, ElementBox)>,
code_actions_indicator: Option<(u32, ElementBox)>,
hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
fold_indicators: Option<Vec<(u32, ElementBox)>>,
}
pub struct PositionMap {
@@ -2277,6 +2422,75 @@ impl HighlightedRange {
}
}
pub fn position_to_display_point(
position: Vector2F,
text_bounds: RectF,
position_map: &PositionMap,
) -> Option<DisplayPoint> {
if text_bounds.contains_point(position) {
let (point, target_point) = position_map.point_for_position(text_bounds, position);
if point == target_point {
Some(point)
} else {
None
}
} else {
None
}
}
pub fn range_to_bounds(
range: &Range<DisplayPoint>,
content_origin: Vector2F,
scroll_left: f32,
scroll_top: f32,
visible_row_range: &Range<u32>,
line_end_overshoot: f32,
position_map: &PositionMap,
) -> impl Iterator<Item = RectF> {
let mut bounds: SmallVec<[RectF; 1]> = SmallVec::new();
if range.start == range.end {
return bounds.into_iter();
}
let start_row = visible_row_range.start;
let end_row = visible_row_range.end;
let row_range = if range.end.column() == 0 {
cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
} else {
cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row)
};
let first_y =
content_origin.y() + row_range.start as f32 * position_map.line_height - scroll_top;
for (idx, row) in row_range.enumerate() {
let line_layout = &position_map.line_layouts[(row - start_row) as usize];
let start_x = if row == range.start.row() {
content_origin.x() + line_layout.x_for_index(range.start.column() as usize)
- scroll_left
} else {
content_origin.x() - scroll_left
};
let end_x = if row == range.end.row() {
content_origin.x() + line_layout.x_for_index(range.end.column() as usize) - scroll_left
} else {
content_origin.x() + line_layout.width() + line_end_overshoot - scroll_left
};
bounds.push(RectF::from_points(
vec2f(start_x, first_y + position_map.line_height * idx as f32),
vec2f(end_x, first_y + position_map.line_height * (idx + 1) as f32),
))
}
bounds.into_iter()
}
pub fn scale_vertical_mouse_autoscroll_delta(delta: f32) -> f32 {
delta.powf(1.5) / 100.0
}

View File

@@ -14,7 +14,7 @@ use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
SelectionGoal,
};
use project::{Item as _, Project, ProjectPath};
use project::{FormatTrigger, Item as _, Project, ProjectPath};
use rpc::proto::{self, update_view};
use settings::Settings;
use smallvec::SmallVec;
@@ -608,7 +608,7 @@ impl Item for Editor {
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.report_event("save editor", cx);
let format = self.perform_format(project.clone(), cx);
let format = self.perform_format(project.clone(), FormatTrigger::Save, cx);
let buffers = self.buffer().clone().read(cx).all_buffers();
cx.as_mut().spawn(|mut cx| async move {
format.await?;
@@ -886,7 +886,7 @@ impl SearchableItem for Editor {
matches: Vec<Range<Anchor>>,
cx: &mut ViewContext<Self>,
) {
self.unfold_ranges([matches[index].clone()], false, cx);
self.unfold_ranges([matches[index].clone()], false, true, cx);
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([matches[index].clone()])
});

View File

@@ -39,7 +39,7 @@ impl<'a> EditorLspTestContext<'a> {
pane::init(cx);
});
let params = cx.update(AppState::test);
let app_state = cx.update(AppState::test);
let file_name = format!(
"file.{}",
@@ -56,10 +56,10 @@ impl<'a> EditorLspTestContext<'a> {
}))
.await;
let project = Project::test(params.fs.clone(), [], cx).await;
let project = Project::test(app_state.fs.clone(), [], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
params
app_state
.fs
.as_fake()
.insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))

View File

@@ -185,6 +185,7 @@ impl<'a> EditorTestContext<'a> {
/// of its selections using a string containing embedded range markers.
///
/// See the `util::test::marked_text_ranges` function for more information.
#[track_caller]
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();

View File

@@ -20,7 +20,12 @@ impl_actions!(zed, [OpenBrowser]);
actions!(
zed,
[CopySystemSpecsIntoClipboard, FileBugReport, RequestFeature]
[
CopySystemSpecsIntoClipboard,
FileBugReport,
RequestFeature,
OpenZedCommunityRepo
]
);
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
@@ -66,4 +71,11 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
});
},
);
cx.add_action(
|_: &mut Workspace, _: &OpenZedCommunityRepo, cx: &mut ViewContext<Workspace>| {
let url = "https://github.com/zed-industries/community";
cx.dispatch_action(OpenBrowser { url: url.into() });
},
);
}

View File

@@ -1,10 +1,12 @@
use gpui::{
elements::Label, Element, ElementBox, Entity, RenderContext, View, ViewContext, ViewHandle,
elements::{Flex, Label, MouseEventHandler, ParentElement, Text},
CursorStyle, Element, ElementBox, Entity, MouseButton, RenderContext, View, ViewContext,
ViewHandle,
};
use settings::Settings;
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
use crate::feedback_editor::FeedbackEditor;
use crate::{feedback_editor::FeedbackEditor, OpenZedCommunityRepo};
pub struct FeedbackInfoText {
active_item: Option<ViewHandle<FeedbackEditor>>,
@@ -29,9 +31,44 @@ impl View for FeedbackInfoText {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone();
let text = "We read whatever you submit here. For issues and discussions, visit the community repo on GitHub.";
Label::new(text, theme.feedback.info_text.text.clone())
.contained()
Flex::row()
.with_child(
Text::new(
"We read whatever you submit here. For issues and discussions, visit the ",
theme.feedback.info_text_default.text.clone(),
)
.with_soft_wrap(false)
.aligned()
.boxed(),
)
.with_child(
MouseEventHandler::<OpenZedCommunityRepo>::new(0, cx, |state, _| {
let contained_text = if state.hovered() {
&theme.feedback.link_text_hover
} else {
&theme.feedback.link_text_default
};
Label::new("community repo", contained_text.text.clone())
.contained()
.aligned()
.left()
.clipped()
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(OpenZedCommunityRepo)
})
.boxed(),
)
.with_child(
Text::new(" on GitHub.", theme.feedback.info_text_default.text.clone())
.with_soft_wrap(false)
.aligned()
.boxed(),
)
.aligned()
.left()
.clipped()

View File

@@ -23,6 +23,7 @@ pub struct FileFinder {
latest_search_id: usize,
latest_search_did_cancel: bool,
latest_search_query: String,
relative_to: Option<Arc<Path>>,
matches: Vec<PathMatch>,
selected: Option<(usize, Arc<Path>)>,
cancel_flag: Arc<AtomicBool>,
@@ -90,7 +91,11 @@ impl FileFinder {
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |workspace, cx| {
let project = workspace.project().clone();
let finder = cx.add_view(|cx| Self::new(project, cx));
let relative_to = workspace
.active_item(cx)
.and_then(|item| item.project_path(cx))
.map(|project_path| project_path.path.clone());
let finder = cx.add_view(|cx| Self::new(project, relative_to, cx));
cx.subscribe(&finder, Self::on_event).detach();
finder
});
@@ -115,7 +120,11 @@ impl FileFinder {
}
}
pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
pub fn new(
project: ModelHandle<Project>,
relative_to: Option<Arc<Path>>,
cx: &mut ViewContext<Self>,
) -> Self {
let handle = cx.weak_handle();
cx.observe(&project, Self::project_updated).detach();
Self {
@@ -125,6 +134,7 @@ impl FileFinder {
latest_search_id: 0,
latest_search_did_cancel: false,
latest_search_query: String::new(),
relative_to,
matches: Vec::new(),
selected: None,
cancel_flag: Arc::new(AtomicBool::new(false)),
@@ -137,6 +147,7 @@ impl FileFinder {
}
fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
let relative_to = self.relative_to.clone();
let worktrees = self
.project
.read(cx)
@@ -165,6 +176,7 @@ impl FileFinder {
let matches = fuzzy::match_path_sets(
candidate_sets.as_slice(),
&query,
relative_to,
false,
100,
&cancel_flag,
@@ -377,7 +389,7 @@ mod tests {
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
});
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
let query = "hi".to_string();
finder
@@ -453,7 +465,7 @@ mod tests {
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
});
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
finder
.update(cx, |f, cx| f.spawn_search("hi".into(), cx))
.await;
@@ -479,7 +491,7 @@ mod tests {
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
});
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
// Even though there is only one worktree, that worktree's filename
// is included in the matching, because the worktree is a single file.
@@ -532,8 +544,9 @@ mod tests {
let (_, workspace) = cx.add_window(|cx| {
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
});
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
// Run a search that matches two files with the same relative path.
finder
@@ -551,6 +564,48 @@ mod tests {
});
}
#[gpui::test]
async fn test_path_distance_ordering(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking();
let app_state = cx.update(AppState::test);
app_state
.fs
.as_fake()
.insert_tree(
"/root",
json!({
"dir1": { "a.txt": "" },
"dir2": {
"a.txt": "",
"b.txt": ""
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| {
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
});
// When workspace has an active item, sort items which are closer to that item
// first when they have the same name. In this case, b.txt is closer to dir2's a.txt
// so that one should be sorted earlier
let b_path = Some(Arc::from(Path::new("/root/dir2/b.txt")));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), b_path, cx));
finder
.update(cx, |f, cx| f.spawn_search("a.txt".into(), cx))
.await;
finder.read_with(cx, |f, _| {
assert_eq!(f.matches[0].path.as_ref(), Path::new("dir2/a.txt"));
assert_eq!(f.matches[1].path.as_ref(), Path::new("dir1/a.txt"));
});
}
#[gpui::test]
async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) {
let app_state = cx.update(AppState::test);
@@ -573,7 +628,7 @@ mod tests {
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
});
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
finder
.update(cx, |f, cx| f.spawn_search("dir".into(), cx))
.await;

View File

@@ -443,6 +443,7 @@ mod tests {
positions: Vec::new(),
path: candidate.path.clone(),
path_prefix: "".into(),
distance_to_relative_ancestor: usize::MAX,
},
);

View File

@@ -25,6 +25,9 @@ pub struct PathMatch {
pub worktree_id: usize,
pub path: Arc<Path>,
pub path_prefix: Arc<str>,
/// Number of steps removed from a shared parent with the relative path
/// Used to order closer paths first in the search list
pub distance_to_relative_ancestor: usize,
}
pub trait PathMatchCandidateSet<'a>: Send + Sync {
@@ -78,6 +81,11 @@ impl Ord for PathMatch {
.partial_cmp(&other.score)
.unwrap_or(Ordering::Equal)
.then_with(|| self.worktree_id.cmp(&other.worktree_id))
.then_with(|| {
other
.distance_to_relative_ancestor
.cmp(&self.distance_to_relative_ancestor)
})
.then_with(|| self.path.cmp(&other.path))
}
}
@@ -85,6 +93,7 @@ impl Ord for PathMatch {
pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
candidate_sets: &'a [Set],
query: &str,
relative_to: Option<Arc<Path>>,
smart_case: bool,
max_results: usize,
cancel_flag: &AtomicBool,
@@ -111,6 +120,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
background
.scoped(|scope| {
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
let relative_to = relative_to.clone();
scope.spawn(async move {
let segment_start = segment_idx * segment_size;
let segment_end = segment_start + segment_size;
@@ -149,6 +159,15 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
positions: Vec::new(),
path: candidate.path.clone(),
path_prefix: candidate_set.prefix(),
distance_to_relative_ancestor: relative_to.as_ref().map_or(
usize::MAX,
|relative_to| {
distance_between_paths(
candidate.path.as_ref(),
relative_to.as_ref(),
)
},
),
},
);
}
@@ -172,3 +191,30 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
}
results
}
/// Compute the distance from a given path to some other path
/// If there is no shared path, returns usize::MAX
fn distance_between_paths(path: &Path, relative_to: &Path) -> usize {
let mut path_components = path.components();
let mut relative_components = relative_to.components();
while path_components
.next()
.zip(relative_components.next())
.map(|(path_component, relative_component)| path_component == relative_component)
.unwrap_or_default()
{}
path_components.count() + relative_components.count() + 1
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::distance_between_paths;
#[test]
fn test_distance_between_paths_empty() {
distance_between_paths(Path::new(""), Path::new(""));
}
}

View File

@@ -86,7 +86,7 @@ pub trait View: Entity + Sized {
}
fn default_keymap_context() -> keymap_matcher::KeymapContext {
let mut cx = keymap_matcher::KeymapContext::default();
cx.set.insert(Self::ui_name().into());
cx.add_identifier(Self::ui_name());
cx
}
fn debug_json(&self, _: &AppContext) -> serde_json::Value {
@@ -4159,10 +4159,10 @@ pub struct RenderContext<'a, T: View> {
#[derive(Debug, Clone, Default)]
pub struct MouseState {
hovered: bool,
clicked: Option<MouseButton>,
accessed_hovered: bool,
accessed_clicked: bool,
pub(crate) hovered: bool,
pub(crate) clicked: Option<MouseButton>,
pub(crate) accessed_hovered: bool,
pub(crate) accessed_clicked: bool,
}
impl MouseState {
@@ -6639,12 +6639,12 @@ mod tests {
let mut view_1 = View::new(1);
let mut view_2 = View::new(2);
let mut view_3 = View::new(3);
view_1.keymap_context.set.insert("a".into());
view_2.keymap_context.set.insert("a".into());
view_2.keymap_context.set.insert("b".into());
view_3.keymap_context.set.insert("a".into());
view_3.keymap_context.set.insert("b".into());
view_3.keymap_context.set.insert("c".into());
view_1.keymap_context.add_identifier("a");
view_2.keymap_context.add_identifier("a");
view_2.keymap_context.add_identifier("b");
view_3.keymap_context.add_identifier("a");
view_3.keymap_context.add_identifier("b");
view_3.keymap_context.add_identifier("c");
let (window_id, view_1) = cx.add_window(Default::default(), |_| view_1);
let view_2 = cx.add_view(&view_1, |_| view_2);

View File

@@ -16,6 +16,14 @@ pub trait Action: 'static {
Self: Sized;
}
impl std::fmt::Debug for dyn Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("dyn Action")
.field("namespace", &self.namespace())
.field("name", &self.name())
.finish()
}
}
/// Define a set of unit struct types that all implement the `Action` trait.
///
/// The first argument is a namespace that will be associated with each of

View File

@@ -18,9 +18,10 @@ use smol::stream::StreamExt;
use crate::{
executor, geometry::vector::Vector2F, keymap_matcher::Keystroke, platform, Action,
AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent,
ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, ReadViewWith,
RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, WeakHandle,
AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, Handle, InputHandler,
KeyDownEvent, ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith,
ReadViewWith, RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle,
WeakHandle,
};
use collections::BTreeMap;
@@ -329,6 +330,14 @@ impl TestAppContext {
.assert_dropped(handle.id())
}
/// Drop a handle, assuming it is the last. If it is not the last, panic with debug information about
/// where the stray handles were created.
pub fn drop_last<T, W: WeakHandle, H: Handle<T, Weak = W>>(&mut self, handle: H) {
let weak = handle.downgrade();
self.update(|_| drop(handle));
self.assert_dropped(weak);
}
fn window_mut(&self, window_id: usize) -> std::cell::RefMut<platform::test::Window> {
std::cell::RefMut::map(self.cx.borrow_mut(), |state| {
let (_, window) = state

View File

@@ -296,7 +296,10 @@ impl<T: Element> AnyElement for Lifecycle<T> {
paint,
}
}
_ => panic!("invalid element lifecycle state"),
Lifecycle::Empty => panic!("invalid element lifecycle state"),
Lifecycle::Init { .. } => {
panic!("invalid element lifecycle state, paint called before layout")
}
}
}

View File

@@ -5,7 +5,7 @@ mod keystroke;
use std::{any::TypeId, fmt::Debug};
use collections::{BTreeMap, HashMap};
use collections::HashMap;
use smallvec::SmallVec;
use crate::Action;
@@ -68,8 +68,8 @@ impl KeymapMatcher {
/// There exist bindings which are still waiting for more keys.
/// MatchResult::Complete(matches) =>
/// 1 or more bindings have recieved the necessary key presses.
/// The order of the matched actions is by order in the keymap file first and
/// position of the matching view second.
/// The order of the matched actions is by position of the matching first,
// and order in the keymap second.
pub fn push_keystroke(
&mut self,
keystroke: Keystroke,
@@ -80,8 +80,7 @@ impl KeymapMatcher {
// and then the order the binding matched in the view tree second.
// The key is the reverse position of the binding in the bindings list so that later bindings
// match before earlier ones in the user's config
let mut matched_bindings: BTreeMap<usize, Vec<(usize, Box<dyn Action>)>> =
Default::default();
let mut matched_bindings: Vec<(usize, Box<dyn Action>)> = Default::default();
let first_keystroke = self.pending_keystrokes.is_empty();
self.pending_keystrokes.push(keystroke.clone());
@@ -105,14 +104,11 @@ impl KeymapMatcher {
}
}
for (order, binding) in self.keymap.bindings().iter().rev().enumerate() {
for binding in self.keymap.bindings().iter().rev() {
match binding.match_keys_and_context(&self.pending_keystrokes, &self.contexts[i..])
{
BindingMatchResult::Complete(action) => {
matched_bindings
.entry(order)
.or_default()
.push((*view_id, action));
matched_bindings.push((*view_id, action));
}
BindingMatchResult::Partial => {
self.pending_views
@@ -131,7 +127,7 @@ impl KeymapMatcher {
if !matched_bindings.is_empty() {
// Collect the sorted matched bindings into the final vec for ease of use
// Matched bindings are in order by precedence
MatchResult::Matches(matched_bindings.into_values().flatten().collect())
MatchResult::Matches(matched_bindings)
} else if any_pending {
MatchResult::Pending
} else {
@@ -225,15 +221,47 @@ mod tests {
use super::*;
#[test]
fn test_keymap_and_view_ordering() -> Result<()> {
actions!(test, [EditorAction, ProjectPanelAction]);
let mut editor = KeymapContext::default();
editor.add_identifier("Editor");
let mut project_panel = KeymapContext::default();
project_panel.add_identifier("ProjectPanel");
// Editor 'deeper' in than project panel
let dispatch_path = vec![(2, editor), (1, project_panel)];
// But editor actions 'higher' up in keymap
let keymap = Keymap::new(vec![
Binding::new("left", EditorAction, Some("Editor")),
Binding::new("left", ProjectPanelAction, Some("ProjectPanel")),
]);
let mut matcher = KeymapMatcher::new(keymap);
assert_eq!(
matcher.push_keystroke(Keystroke::parse("left")?, dispatch_path.clone()),
MatchResult::Matches(vec![
(2, Box::new(EditorAction)),
(1, Box::new(ProjectPanelAction)),
]),
);
Ok(())
}
#[test]
fn test_push_keystroke() -> Result<()> {
actions!(test, [B, AB, C, D, DA, E, EF]);
let mut context1 = KeymapContext::default();
context1.set.insert("1".into());
context1.add_identifier("1");
let mut context2 = KeymapContext::default();
context2.set.insert("2".into());
context2.add_identifier("2");
let dispatch_path = vec![(2, context2), (1, context1)];
@@ -367,22 +395,22 @@ mod tests {
let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap();
let mut context = KeymapContext::default();
context.set.insert("a".into());
context.add_identifier("a");
assert!(!predicate.eval(&[context]));
let mut context = KeymapContext::default();
context.set.insert("a".into());
context.set.insert("b".into());
context.add_identifier("a");
context.add_identifier("b");
assert!(predicate.eval(&[context]));
let mut context = KeymapContext::default();
context.set.insert("a".into());
context.map.insert("c".into(), "x".into());
context.add_identifier("a");
context.add_key("c", "x");
assert!(!predicate.eval(&[context]));
let mut context = KeymapContext::default();
context.set.insert("a".into());
context.map.insert("c".into(), "d".into());
context.add_identifier("a");
context.add_key("c", "d");
assert!(predicate.eval(&[context]));
let predicate = KeymapContextPredicate::parse("!a").unwrap();
@@ -422,10 +450,11 @@ mod tests {
assert!(!predicate.eval(&contexts[6..]));
fn context_set(names: &[&str]) -> KeymapContext {
KeymapContext {
set: names.iter().copied().map(str::to_string).collect(),
..Default::default()
}
let mut keymap = KeymapContext::new();
names
.iter()
.for_each(|name| keymap.add_identifier(name.to_string()));
keymap
}
}
@@ -448,10 +477,10 @@ mod tests {
]);
let mut context_a = KeymapContext::default();
context_a.set.insert("a".into());
context_a.add_identifier("a");
let mut context_b = KeymapContext::default();
context_b.set.insert("b".into());
context_b.add_identifier("b");
let mut matcher = KeymapMatcher::new(keymap);
@@ -496,7 +525,7 @@ mod tests {
matcher.clear_pending();
let mut context_c = KeymapContext::default();
context_c.set.insert("c".into());
context_c.add_identifier("c");
// Pending keystrokes are maintained per-view
assert_eq!(

View File

@@ -1,13 +1,22 @@
use std::borrow::Cow;
use anyhow::{anyhow, Result};
use collections::{HashMap, HashSet};
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct KeymapContext {
pub set: HashSet<String>,
pub map: HashMap<String, String>,
set: HashSet<Cow<'static, str>>,
map: HashMap<Cow<'static, str>, Cow<'static, str>>,
}
impl KeymapContext {
pub fn new() -> Self {
KeymapContext {
set: HashSet::default(),
map: HashMap::default(),
}
}
pub fn extend(&mut self, other: &Self) {
for v in &other.set {
self.set.insert(v.clone());
@@ -16,6 +25,18 @@ impl KeymapContext {
self.map.insert(k.clone(), v.clone());
}
}
pub fn add_identifier<I: Into<Cow<'static, str>>>(&mut self, identifier: I) {
self.set.insert(identifier.into());
}
pub fn add_key<S1: Into<Cow<'static, str>>, S2: Into<Cow<'static, str>>>(
&mut self,
key: S1,
value: S2,
) {
self.map.insert(key.into(), value.into());
}
}
#[derive(Debug, Eq, PartialEq)]
@@ -46,12 +67,12 @@ impl KeymapContextPredicate {
Self::Identifier(name) => (&context.set).contains(name.as_str()),
Self::Equal(left, right) => context
.map
.get(left)
.get(left.as_str())
.map(|value| value == right)
.unwrap_or(false),
Self::NotEqual(left, right) => context
.map
.get(left)
.get(left.as_str())
.map(|value| value != right)
.unwrap_or(true),
Self::Not(pred) => !pred.eval(contexts),

View File

@@ -85,16 +85,12 @@ impl SpriteCache {
) -> Option<GlyphSprite> {
const SUBPIXEL_VARIANTS: u8 = 4;
let scale_factor = self.scale_factor;
let target_position = target_position * scale_factor;
let fonts = &self.fonts;
let atlases = &mut self.atlases;
let target_position = target_position * self.scale_factor;
let subpixel_variant = (
(target_position.x().fract() * SUBPIXEL_VARIANTS as f32).round() as u8
% SUBPIXEL_VARIANTS,
(target_position.y().fract() * SUBPIXEL_VARIANTS as f32).round() as u8
% SUBPIXEL_VARIANTS,
(target_position.x().fract() * SUBPIXEL_VARIANTS as f32).floor() as u8,
(target_position.y().fract() * SUBPIXEL_VARIANTS as f32).floor() as u8,
);
self.glyphs
.entry(GlyphDescriptor {
font_id,
@@ -107,16 +103,17 @@ impl SpriteCache {
subpixel_variant.0 as f32 / SUBPIXEL_VARIANTS as f32,
subpixel_variant.1 as f32 / SUBPIXEL_VARIANTS as f32,
);
let (glyph_bounds, mask) = fonts.rasterize_glyph(
let (glyph_bounds, mask) = self.fonts.rasterize_glyph(
font_id,
font_size,
glyph_id,
subpixel_shift,
scale_factor,
self.scale_factor,
RasterizationOptions::Alpha,
)?;
let (alloc_id, atlas_bounds) = atlases
let (alloc_id, atlas_bounds) = self
.atlases
.upload(glyph_bounds.size(), &mask)
.expect("could not upload glyph");
Some(GlyphSprite {

View File

@@ -737,6 +737,7 @@ impl platform::Window for Window {
let title = ns_string(title);
let _: () = msg_send![app, changeWindowsItem:window title:title filename:false];
let _: () = msg_send![window, setTitle: title];
self.0.borrow().move_traffic_light();
}
}

View File

@@ -12,9 +12,9 @@ use crate::{
text_layout::TextLayoutCache,
Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, Appearance,
AssetCache, ElementBox, Entity, FontSystem, ModelHandle, MouseButton, MouseMovedEvent,
MouseRegion, MouseRegionId, ParentId, ReadModel, ReadView, RenderContext, RenderParams,
SceneBuilder, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle,
WeakViewHandle,
MouseRegion, MouseRegionId, MouseState, ParentId, ReadModel, ReadView, RenderContext,
RenderParams, SceneBuilder, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle,
WeakModelHandle, WeakViewHandle,
};
use anyhow::bail;
use collections::{HashMap, HashSet};
@@ -507,15 +507,18 @@ impl Presenter {
}
// Handle Down events if the MouseRegion has a Click or Drag handler. This makes the api more intuitive as you would
// not expect a MouseRegion to be transparent to Down events if it also has a Click handler.
// This behavior can be overridden by adding a Down handler that calls cx.propogate_event
// This behavior can be overridden by adding a Down handler
if let MouseEvent::Down(e) = &mouse_event {
if valid_region
let has_click = valid_region
.handlers
.contains(MouseEvent::click_disc(), Some(e.button))
|| valid_region
.handlers
.contains(MouseEvent::drag_disc(), Some(e.button))
{
.contains(MouseEvent::click_disc(), Some(e.button));
let has_drag = valid_region
.handlers
.contains(MouseEvent::drag_disc(), Some(e.button));
let has_down = valid_region
.handlers
.contains(MouseEvent::down_disc(), Some(e.button));
if !has_down && (has_click || has_drag) {
event_cx.handled = true;
}
}
@@ -523,14 +526,13 @@ impl Presenter {
// `event_consumed` should only be true if there are any handlers for this event.
let mut event_consumed = event_cx.handled;
if let Some(callbacks) = valid_region.handlers.get(&mouse_event.handler_key()) {
event_consumed = true;
for callback in callbacks {
event_cx.handled = true;
event_cx.with_current_view(valid_region.id().view_id(), {
let region_event = mouse_event.clone();
|cx| callback(region_event, cx)
});
event_consumed &= event_cx.handled;
event_consumed |= event_cx.handled;
any_event_handled |= event_cx.handled;
}
}
@@ -603,6 +605,24 @@ pub struct LayoutContext<'a> {
}
impl<'a> LayoutContext<'a> {
pub fn mouse_state<Tag: 'static>(&self, region_id: usize) -> MouseState {
let view_id = self.view_stack.last().unwrap();
let region_id = MouseRegionId::new::<Tag>(*view_id, region_id);
MouseState {
hovered: self.hovered_region_ids.contains(&region_id),
clicked: self.clicked_region_ids.as_ref().and_then(|(ids, button)| {
if ids.contains(&region_id) {
Some(*button)
} else {
None
}
}),
accessed_hovered: false,
accessed_clicked: false,
}
}
fn layout(&mut self, view_id: usize, constraint: SizeConstraint) -> Vector2F {
let print_error = |view_id| {
format!(

View File

@@ -305,7 +305,7 @@ pub struct Chunk<'a> {
}
pub struct Diff {
base_version: clock::Global,
pub(crate) base_version: clock::Global,
line_ending: LineEnding,
edits: Vec<(Range<usize>, Arc<str>)>,
}
@@ -569,18 +569,21 @@ impl Buffer {
.read_with(&cx, |this, cx| this.diff(new_text, cx))
.await;
this.update(&mut cx, |this, cx| {
if let Some(transaction) = this.apply_diff(diff, cx).cloned() {
this.did_reload(
this.version(),
this.as_rope().fingerprint(),
this.line_ending(),
new_mtime,
cx,
);
Ok(Some(transaction))
} else {
Ok(None)
if this.version() == diff.base_version {
this.finalize_last_transaction();
this.apply_diff(diff, cx);
if let Some(transaction) = this.finalize_last_transaction().cloned() {
this.did_reload(
this.version(),
this.as_rope().fingerprint(),
this.line_ending(),
new_mtime,
cx,
);
return Ok(Some(transaction));
}
}
Ok(None)
})
} else {
Ok(None)
@@ -1154,20 +1157,84 @@ impl Buffer {
})
}
pub fn apply_diff(&mut self, diff: Diff, cx: &mut ModelContext<Self>) -> Option<&Transaction> {
if self.version == diff.base_version {
self.finalize_last_transaction();
self.start_transaction();
self.text.set_line_ending(diff.line_ending);
self.edit(diff.edits, None, cx);
if self.end_transaction(cx).is_some() {
self.finalize_last_transaction()
} else {
None
/// Spawn a background task that searches the buffer for any whitespace
/// at the ends of a lines, and returns a `Diff` that removes that whitespace.
pub fn remove_trailing_whitespace(&self, cx: &AppContext) -> Task<Diff> {
let old_text = self.as_rope().clone();
let line_ending = self.line_ending();
let base_version = self.version();
cx.background().spawn(async move {
let ranges = trailing_whitespace_ranges(&old_text);
let empty = Arc::<str>::from("");
Diff {
base_version,
line_ending,
edits: ranges
.into_iter()
.map(|range| (range, empty.clone()))
.collect(),
}
})
}
/// Ensure that the buffer ends with a single newline character, and
/// no other whitespace.
pub fn ensure_final_newline(&mut self, cx: &mut ModelContext<Self>) {
let len = self.len();
let mut offset = len;
for chunk in self.as_rope().reversed_chunks_in_range(0..len) {
let non_whitespace_len = chunk
.trim_end_matches(|c: char| c.is_ascii_whitespace())
.len();
offset -= chunk.len();
offset += non_whitespace_len;
if non_whitespace_len != 0 {
if offset == len - 1 && chunk.get(non_whitespace_len..) == Some("\n") {
return;
}
break;
}
} else {
None
}
self.edit([(offset..len, "\n")], None, cx);
}
/// Apply a diff to the buffer. If the buffer has changed since the given diff was
/// calculated, then adjust the diff to account for those changes, and discard any
/// parts of the diff that conflict with those changes.
pub fn apply_diff(&mut self, diff: Diff, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
// Check for any edits to the buffer that have occurred since this diff
// was computed.
let snapshot = self.snapshot();
let mut edits_since = snapshot.edits_since::<usize>(&diff.base_version).peekable();
let mut delta = 0;
let adjusted_edits = diff.edits.into_iter().filter_map(|(range, new_text)| {
while let Some(edit_since) = edits_since.peek() {
// If the edit occurs after a diff hunk, then it does not
// affect that hunk.
if edit_since.old.start > range.end {
break;
}
// If the edit precedes the diff hunk, then adjust the hunk
// to reflect the edit.
else if edit_since.old.end < range.start {
delta += edit_since.new_len() as i64 - edit_since.old_len() as i64;
edits_since.next();
}
// If the edit intersects a diff hunk, then discard that hunk.
else {
return None;
}
}
let start = (range.start as i64 + delta) as usize;
let end = (range.end as i64 + delta) as usize;
Some((start..end, new_text))
});
self.start_transaction();
self.text.set_line_ending(diff.line_ending);
self.edit(adjusted_edits, None, cx);
self.end_transaction(cx)
}
pub fn is_dirty(&self) -> bool {
@@ -2840,3 +2907,42 @@ pub fn char_kind(c: char) -> CharKind {
CharKind::Punctuation
}
}
/// Find all of the ranges of whitespace that occur at the ends of lines
/// in the given rope.
///
/// This could also be done with a regex search, but this implementation
/// avoids copying text.
pub fn trailing_whitespace_ranges(rope: &Rope) -> Vec<Range<usize>> {
let mut ranges = Vec::new();
let mut offset = 0;
let mut prev_chunk_trailing_whitespace_range = 0..0;
for chunk in rope.chunks() {
let mut prev_line_trailing_whitespace_range = 0..0;
for (i, line) in chunk.split('\n').enumerate() {
let line_end_offset = offset + line.len();
let trimmed_line_len = line.trim_end_matches(|c| matches!(c, ' ' | '\t')).len();
let mut trailing_whitespace_range = (offset + trimmed_line_len)..line_end_offset;
if i == 0 && trimmed_line_len == 0 {
trailing_whitespace_range.start = prev_chunk_trailing_whitespace_range.start;
}
if !prev_line_trailing_whitespace_range.is_empty() {
ranges.push(prev_line_trailing_whitespace_range);
}
offset = line_end_offset + 1;
prev_line_trailing_whitespace_range = trailing_whitespace_range;
}
offset -= 1;
prev_chunk_trailing_whitespace_range = prev_line_trailing_whitespace_range;
}
if !prev_chunk_trailing_whitespace_range.is_empty() {
ranges.push(prev_chunk_trailing_whitespace_range);
}
ranges
}

View File

@@ -6,6 +6,7 @@ use gpui::{ModelHandle, MutableAppContext};
use indoc::indoc;
use proto::deserialize_operation;
use rand::prelude::*;
use regex::RegexBuilder;
use settings::Settings;
use std::{
cell::RefCell,
@@ -18,6 +19,13 @@ use text::network::Network;
use unindent::Unindent as _;
use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter};
lazy_static! {
static ref TRAILING_WHITESPACE_REGEX: Regex = RegexBuilder::new("[ \t]+$")
.multi_line(true)
.build()
.unwrap();
}
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {
@@ -211,6 +219,79 @@ async fn test_apply_diff(cx: &mut gpui::TestAppContext) {
});
}
#[gpui::test(iterations = 10)]
async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) {
let text = [
"zero", //
"one ", // 2 trailing spaces
"two", //
"three ", // 3 trailing spaces
"four", //
"five ", // 4 trailing spaces
]
.join("\n");
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
// Spawn a task to format the buffer's whitespace.
// Pause so that the foratting task starts running.
let format = buffer.read_with(cx, |buffer, cx| buffer.remove_trailing_whitespace(cx));
smol::future::yield_now().await;
// Edit the buffer while the normalization task is running.
let version_before_edit = buffer.read_with(cx, |buffer, _| buffer.version());
buffer.update(cx, |buffer, cx| {
buffer.edit(
[
(Point::new(0, 1)..Point::new(0, 1), "EE"),
(Point::new(3, 5)..Point::new(3, 5), "EEE"),
],
None,
cx,
);
});
let format_diff = format.await;
buffer.update(cx, |buffer, cx| {
let version_before_format = format_diff.base_version.clone();
buffer.apply_diff(format_diff, cx);
// The outcome depends on the order of concurrent taks.
//
// If the edit occurred while searching for trailing whitespace ranges,
// then the trailing whitespace region touched by the edit is left intact.
if version_before_format == version_before_edit {
assert_eq!(
buffer.text(),
[
"zEEero", //
"one", //
"two", //
"threeEEE ", //
"four", //
"five", //
]
.join("\n")
);
}
// Otherwise, all trailing whitespace is removed.
else {
assert_eq!(
buffer.text(),
[
"zEEero", //
"one", //
"two", //
"threeEEE", //
"four", //
"five", //
]
.join("\n")
);
}
});
}
#[gpui::test]
async fn test_reparse(cx: &mut gpui::TestAppContext) {
let text = "fn a() {}";
@@ -1943,6 +2024,45 @@ fn test_contiguous_ranges() {
);
}
#[gpui::test(iterations = 500)]
fn test_trailing_whitespace_ranges(mut rng: StdRng) {
// Generate a random multi-line string containing
// some lines with trailing whitespace.
let mut text = String::new();
for _ in 0..rng.gen_range(0..16) {
for _ in 0..rng.gen_range(0..36) {
text.push(match rng.gen_range(0..10) {
0..=1 => ' ',
3 => '\t',
_ => rng.gen_range('a'..'z'),
});
}
text.push('\n');
}
match rng.gen_range(0..10) {
// sometimes remove the last newline
0..=1 => drop(text.pop()), //
// sometimes add extra newlines
2..=3 => text.push_str(&"\n".repeat(rng.gen_range(1..5))),
_ => {}
}
let rope = Rope::from(text.as_str());
let actual_ranges = trailing_whitespace_ranges(&rope);
let expected_ranges = TRAILING_WHITESPACE_REGEX
.find_iter(&text)
.map(|m| m.range())
.collect::<Vec<_>>();
assert_eq!(
actual_ranges,
expected_ranges,
"wrong ranges for text lines:\n{:?}",
text.split("\n").collect::<Vec<_>>()
);
}
fn ruby_lang() -> Language {
Language::new(
LanguageConfig {

View File

@@ -165,6 +165,7 @@ struct ParseStep {
mode: ParseMode,
}
#[derive(Debug)]
enum ParseStepLanguage {
Loaded { language: Arc<Language> },
Pending { name: Arc<str> },
@@ -514,15 +515,32 @@ impl SyntaxSnapshot {
let Some(grammar) = language.grammar() else { continue };
let tree;
let changed_ranges;
let mut included_ranges = step.included_ranges;
for range in &mut included_ranges {
range.start_byte -= step_start_byte;
range.end_byte -= step_start_byte;
range.start_point = (Point::from_ts_point(range.start_point)
- step_start_point)
.to_ts_point();
range.end_point = (Point::from_ts_point(range.end_point)
- step_start_point)
.to_ts_point();
}
if let Some(SyntaxLayerContent::Parsed { tree: old_tree, .. }) =
old_layer.map(|layer| &layer.content)
{
if let ParseMode::Combined {
parent_layer_changed_ranges,
mut parent_layer_changed_ranges,
..
} = step.mode
{
for range in &mut parent_layer_changed_ranges {
range.start -= step_start_byte;
range.end -= step_start_byte;
}
included_ranges = splice_included_ranges(
old_tree.included_ranges(),
&parent_layer_changed_ranges,
@@ -534,7 +552,6 @@ impl SyntaxSnapshot {
grammar,
text.as_rope(),
step_start_byte,
step_start_point,
included_ranges,
Some(old_tree.clone()),
);
@@ -551,7 +568,6 @@ impl SyntaxSnapshot {
grammar,
text.as_rope(),
step_start_byte,
step_start_point,
included_ranges,
None,
);
@@ -1060,17 +1076,9 @@ fn parse_text(
grammar: &Grammar,
text: &Rope,
start_byte: usize,
start_point: Point,
mut ranges: Vec<tree_sitter::Range>,
ranges: Vec<tree_sitter::Range>,
old_tree: Option<Tree>,
) -> Tree {
for range in &mut ranges {
range.start_byte -= start_byte;
range.end_byte -= start_byte;
range.start_point = (Point::from_ts_point(range.start_point) - start_point).to_ts_point();
range.end_point = (Point::from_ts_point(range.end_point) - start_point).to_ts_point();
}
PARSER.with(|parser| {
let mut parser = parser.borrow_mut();
let mut chunks = text.chunks_in_range(start_byte..text.len());
@@ -2208,6 +2216,37 @@ mod tests {
);
}
#[gpui::test]
fn test_combined_injections_inside_injections() {
let (_buffer, _syntax_map) = test_edit_sequence(
"Markdown",
&[
r#"
here is some ERB code:
```erb
<ul>
<% people.each do |person| %>
<li><%= person.name %></li>
<% end %>
</ul>
```
"#,
r#"
here is some ERB code:
```erb
<ul>
<% people«2».each do |person| %>
<li><%= person.name %></li>
<% end %>
</ul>
```
"#,
],
);
}
#[gpui::test(iterations = 50)]
fn test_random_syntax_map_edits(mut rng: StdRng) {
let operations = env::var("OPERATIONS")

View File

@@ -422,6 +422,10 @@ impl LanguageServer {
self.notification_handlers.lock().remove(T::METHOD);
}
pub fn remove_notification_handler<T: notification::Notification>(&self) {
self.notification_handlers.lock().remove(T::METHOD);
}
#[must_use]
pub fn on_custom_notification<Params, F>(&self, method: &'static str, mut f: F) -> Subscription
where
@@ -780,6 +784,26 @@ impl FakeLanguageServer {
responded_rx
}
pub fn handle_notification<T, F>(
&self,
mut handler: F,
) -> futures::channel::mpsc::UnboundedReceiver<()>
where
T: 'static + notification::Notification,
T::Params: 'static + Send,
F: 'static + Send + FnMut(T::Params, gpui::AsyncAppContext),
{
let (handled_tx, handled_rx) = futures::channel::mpsc::unbounded();
self.server.remove_notification_handler::<T>();
self.server
.on_notification::<T, _>(move |params, cx| {
handler(params, cx.clone());
handled_tx.unbounded_send(()).ok();
})
.detach();
handled_rx
}
pub fn remove_request_handler<T>(&mut self)
where
T: 'static + request::Request,

View File

@@ -126,7 +126,7 @@ impl<D: PickerDelegate> View for Picker<D> {
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.set.insert("menu".into());
cx.add_identifier("menu");
cx
}

View File

@@ -57,6 +57,7 @@ thiserror = "1.0.29"
toml = "0.5"
[dev-dependencies]
pretty_assertions = "1.3.0"
client = { path = "../client", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
db = { path = "../db", features = ["test-support"] }

View File

@@ -1,6 +1,7 @@
mod ignore;
mod lsp_command;
pub mod search;
pub mod terminals;
pub mod worktree;
#[cfg(test)]
@@ -26,7 +27,7 @@ use language::{
serialize_anchor, serialize_version,
},
range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction,
CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent,
CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent,
File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, OffsetRangeExt,
Operation, Patch, PointUtf16, RopeFingerprint, TextBufferSnapshot, ToOffset, ToPointUtf16,
Transaction, Unclipped,
@@ -61,7 +62,8 @@ use std::{
},
time::{Duration, Instant, SystemTime},
};
use terminal::{Terminal, TerminalBuilder};
use terminals::Terminals;
use util::{debug_panic, defer, post_inc, ResultExt, TryFutureExt as _};
pub use fs::*;
@@ -123,6 +125,7 @@ pub struct Project {
buffers_being_formatted: HashSet<usize>,
nonce: u128,
_maintain_buffer_languages: Task<()>,
terminals: Terminals,
}
enum OpenBuffer {
@@ -439,6 +442,9 @@ impl Project {
buffers_being_formatted: Default::default(),
next_language_server_id: 0,
nonce: StdRng::from_entropy().gen(),
terminals: Terminals {
local_handles: Vec::new(),
},
})
}
@@ -516,6 +522,9 @@ impl Project {
buffers_being_formatted: Default::default(),
buffer_snapshots: Default::default(),
nonce: StdRng::from_entropy().gen(),
terminals: Terminals {
local_handles: Vec::new(),
},
};
for worktree in worktrees {
let _ = this.add_worktree(&worktree, cx);
@@ -1184,34 +1193,6 @@ impl Project {
!self.is_local()
}
pub fn create_terminal(
&mut self,
working_directory: Option<PathBuf>,
window_id: usize,
cx: &mut ModelContext<Self>,
) -> Result<ModelHandle<Terminal>> {
if self.is_remote() {
return Err(anyhow!(
"creating terminals as a guest is not supported yet"
));
} else {
let settings = cx.global::<Settings>();
let shell = settings.terminal_shell();
let envs = settings.terminal_env();
let scroll = settings.terminal_scroll();
TerminalBuilder::new(
working_directory.clone(),
shell,
envs,
settings.terminal_overrides.blinking.clone(),
scroll,
window_id,
)
.map(|builder| cx.add_model(|cx| builder.subscribe(cx)))
}
}
pub fn create_buffer(
&mut self,
text: &str,
@@ -2557,7 +2538,7 @@ impl Project {
pub fn update_diagnostics(
&mut self,
language_server_id: usize,
params: lsp::PublishDiagnosticsParams,
mut params: lsp::PublishDiagnosticsParams,
disk_based_sources: &[String],
cx: &mut ModelContext<Self>,
) -> Result<()> {
@@ -2569,6 +2550,10 @@ impl Project {
let mut primary_diagnostic_group_ids = HashMap::default();
let mut sources_by_group_id = HashMap::default();
let mut supporting_diagnostics = HashMap::default();
// Ensure that primary diagnostics are always the most severe
params.diagnostics.sort_by_key(|item| item.severity);
for diagnostic in &params.diagnostics {
let source = diagnostic.source.as_ref();
let code = diagnostic.code.as_ref().map(|code| match code {
@@ -2858,9 +2843,11 @@ impl Project {
.filter_map(|buffer_handle| {
let buffer = buffer_handle.read(cx);
let file = File::from_dyn(buffer.file())?;
let buffer_abs_path = file.as_local()?.abs_path(cx);
let (_, server) = self.language_server_for_buffer(buffer, cx)?;
Some((buffer_handle, buffer_abs_path, server.clone()))
let buffer_abs_path = file.as_local().map(|f| f.abs_path(cx));
let server = self
.language_server_for_buffer(buffer, cx)
.map(|s| s.1.clone());
Some((buffer_handle, buffer_abs_path, server))
})
.collect::<Vec<_>>();
@@ -2875,10 +2862,10 @@ impl Project {
let _cleanup = defer({
let this = this.clone();
let mut cx = cx.clone();
let local_buffers = &buffers_with_paths_and_servers;
let buffers = &buffers_with_paths_and_servers;
move || {
this.update(&mut cx, |this, _| {
for (buffer, _, _) in local_buffers {
for (buffer, _, _) in buffers {
this.buffers_being_formatted.remove(&buffer.id());
}
});
@@ -2887,60 +2874,138 @@ impl Project {
let mut project_transaction = ProjectTransaction::default();
for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers {
let (format_on_save, formatter, tab_size) =
buffer.read_with(&cx, |buffer, cx| {
let settings = cx.global::<Settings>();
let language_name = buffer.language().map(|language| language.name());
(
settings.format_on_save(language_name.as_deref()),
settings.formatter(language_name.as_deref()),
settings.tab_size(language_name.as_deref()),
)
});
let (
format_on_save,
remove_trailing_whitespace,
ensure_final_newline,
formatter,
tab_size,
) = buffer.read_with(&cx, |buffer, cx| {
let settings = cx.global::<Settings>();
let language_name = buffer.language().map(|language| language.name());
(
settings.format_on_save(language_name.as_deref()),
settings.remove_trailing_whitespace_on_save(language_name.as_deref()),
settings.ensure_final_newline_on_save(language_name.as_deref()),
settings.formatter(language_name.as_deref()),
settings.tab_size(language_name.as_deref()),
)
});
let transaction = match (formatter, format_on_save) {
(_, FormatOnSave::Off) if trigger == FormatTrigger::Save => continue,
// First, format buffer's whitespace according to the settings.
let trailing_whitespace_diff = if remove_trailing_whitespace {
Some(
buffer
.read_with(&cx, |b, cx| b.remove_trailing_whitespace(cx))
.await,
)
} else {
None
};
let whitespace_transaction_id = buffer.update(&mut cx, |buffer, cx| {
buffer.finalize_last_transaction();
buffer.start_transaction();
if let Some(diff) = trailing_whitespace_diff {
buffer.apply_diff(diff, cx);
}
if ensure_final_newline {
buffer.ensure_final_newline(cx);
}
buffer.end_transaction(cx)
});
// Currently, formatting operations are represented differently depending on
// whether they come from a language server or an external command.
enum FormatOperation {
Lsp(Vec<(Range<Anchor>, String)>),
External(Diff),
}
// Apply language-specific formatting using either a language server
// or external command.
let mut format_operation = None;
match (formatter, format_on_save) {
(_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {}
(Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
| (_, FormatOnSave::LanguageServer) => Self::format_via_lsp(
&this,
&buffer,
&buffer_abs_path,
&language_server,
tab_size,
&mut cx,
)
.await
.context("failed to format via language server")?,
| (_, FormatOnSave::LanguageServer) => {
if let Some((language_server, buffer_abs_path)) =
language_server.as_ref().zip(buffer_abs_path.as_ref())
{
format_operation = Some(FormatOperation::Lsp(
Self::format_via_lsp(
&this,
&buffer,
buffer_abs_path,
&language_server,
tab_size,
&mut cx,
)
.await
.context("failed to format via language server")?,
));
}
}
(
Formatter::External { command, arguments },
FormatOnSave::On | FormatOnSave::Off,
)
| (_, FormatOnSave::External { command, arguments }) => {
Self::format_via_external_command(
&buffer,
&buffer_abs_path,
&command,
&arguments,
&mut cx,
)
.await
.context(format!(
"failed to format via external command {:?}",
command
))?
if let Some(buffer_abs_path) = buffer_abs_path {
format_operation = Self::format_via_external_command(
&buffer,
&buffer_abs_path,
&command,
&arguments,
&mut cx,
)
.await
.context(format!(
"failed to format via external command {:?}",
command
))?
.map(FormatOperation::External);
}
}
};
if let Some(transaction) = transaction {
if !push_to_history {
buffer.update(&mut cx, |buffer, _| {
buffer.forget_transaction(transaction.id)
});
buffer.update(&mut cx, |b, cx| {
// If the buffer had its whitespace formatted and was edited while the language-specific
// formatting was being computed, avoid applying the language-specific formatting, because
// it can't be grouped with the whitespace formatting in the undo history.
if let Some(transaction_id) = whitespace_transaction_id {
if b.peek_undo_stack()
.map_or(true, |e| e.transaction_id() != transaction_id)
{
format_operation.take();
}
}
project_transaction.0.insert(buffer.clone(), transaction);
}
// Apply any language-specific formatting, and group the two formatting operations
// in the buffer's undo history.
if let Some(operation) = format_operation {
match operation {
FormatOperation::Lsp(edits) => {
b.edit(edits, None, cx);
}
FormatOperation::External(diff) => {
b.apply_diff(diff, cx);
}
}
if let Some(transaction_id) = whitespace_transaction_id {
b.group_until_transaction(transaction_id);
}
}
if let Some(transaction) = b.finalize_last_transaction().cloned() {
if !push_to_history {
b.forget_transaction(transaction.id);
}
project_transaction.0.insert(buffer.clone(), transaction);
}
});
}
Ok(project_transaction)
@@ -2981,7 +3046,7 @@ impl Project {
language_server: &Arc<LanguageServer>,
tab_size: NonZeroU32,
cx: &mut AsyncAppContext,
) -> Result<Option<Transaction>> {
) -> Result<Vec<(Range<Anchor>, String)>> {
let text_document =
lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(abs_path).unwrap());
let capabilities = &language_server.capabilities();
@@ -3028,26 +3093,12 @@ impl Project {
};
if let Some(lsp_edits) = lsp_edits {
let edits = this
.update(cx, |this, cx| {
this.edits_from_lsp(buffer, lsp_edits, None, cx)
})
.await?;
buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction();
buffer.start_transaction();
for (range, text) in edits {
buffer.edit([(range, text)], None, cx);
}
if buffer.end_transaction(cx).is_some() {
let transaction = buffer.finalize_last_transaction().unwrap().clone();
Ok(Some(transaction))
} else {
Ok(None)
}
this.update(cx, |this, cx| {
this.edits_from_lsp(buffer, lsp_edits, None, cx)
})
.await
} else {
Ok(None)
Ok(Default::default())
}
}
@@ -3057,7 +3108,7 @@ impl Project {
command: &str,
arguments: &[String],
cx: &mut AsyncAppContext,
) -> Result<Option<Transaction>> {
) -> Result<Option<Diff>> {
let working_dir_path = buffer.read_with(cx, |buffer, cx| {
let file = File::from_dyn(buffer.file())?;
let worktree = file.worktree.read(cx).as_local()?;
@@ -3100,10 +3151,11 @@ impl Project {
}
let stdout = String::from_utf8(output.stdout)?;
let diff = buffer
.read_with(cx, |buffer, cx| buffer.diff(stdout, cx))
.await;
Ok(buffer.update(cx, |buffer, cx| buffer.apply_diff(diff, cx).cloned()))
Ok(Some(
buffer
.read_with(cx, |buffer, cx| buffer.diff(stdout, cx))
.await,
))
} else {
Ok(None)
}

View File

@@ -8,6 +8,7 @@ use language::{
OffsetRangeExt, Point, ToPoint,
};
use lsp::Url;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::{cell::RefCell, os::unix, rc::Rc, task::Poll};
use unindent::Unindent as _;
@@ -2846,7 +2847,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::WARNING,
message: "error 1".to_string(),
group_id: 0,
group_id: 1,
is_primary: true,
..Default::default()
}
@@ -2856,7 +2857,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 1 hint 1".to_string(),
group_id: 0,
group_id: 1,
is_primary: false,
..Default::default()
}
@@ -2866,7 +2867,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 2 hint 1".to_string(),
group_id: 1,
group_id: 0,
is_primary: false,
..Default::default()
}
@@ -2876,7 +2877,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 2 hint 2".to_string(),
group_id: 1,
group_id: 0,
is_primary: false,
..Default::default()
}
@@ -2886,7 +2887,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "error 2".to_string(),
group_id: 1,
group_id: 0,
is_primary: true,
..Default::default()
}
@@ -2896,38 +2897,13 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
assert_eq!(
buffer.diagnostic_group::<Point>(0).collect::<Vec<_>>(),
&[
DiagnosticEntry {
range: Point::new(1, 8)..Point::new(1, 9),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::WARNING,
message: "error 1".to_string(),
group_id: 0,
is_primary: true,
..Default::default()
}
},
DiagnosticEntry {
range: Point::new(1, 8)..Point::new(1, 9),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 1 hint 1".to_string(),
group_id: 0,
is_primary: false,
..Default::default()
}
},
]
);
assert_eq!(
buffer.diagnostic_group::<Point>(1).collect::<Vec<_>>(),
&[
DiagnosticEntry {
range: Point::new(1, 13)..Point::new(1, 15),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 2 hint 1".to_string(),
group_id: 1,
group_id: 0,
is_primary: false,
..Default::default()
}
@@ -2937,7 +2913,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 2 hint 2".to_string(),
group_id: 1,
group_id: 0,
is_primary: false,
..Default::default()
}
@@ -2947,13 +2923,39 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "error 2".to_string(),
group_id: 1,
group_id: 0,
is_primary: true,
..Default::default()
}
}
]
);
assert_eq!(
buffer.diagnostic_group::<Point>(1).collect::<Vec<_>>(),
&[
DiagnosticEntry {
range: Point::new(1, 8)..Point::new(1, 9),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::WARNING,
message: "error 1".to_string(),
group_id: 1,
is_primary: true,
..Default::default()
}
},
DiagnosticEntry {
range: Point::new(1, 8)..Point::new(1, 9),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 1 hint 1".to_string(),
group_id: 1,
is_primary: false,
..Default::default()
}
},
]
);
}
#[gpui::test]

View File

@@ -0,0 +1,63 @@
use std::path::PathBuf;
use gpui::{ModelContext, ModelHandle, WeakModelHandle};
use settings::Settings;
use terminal::{Terminal, TerminalBuilder};
use crate::Project;
pub struct Terminals {
pub(crate) local_handles: Vec<WeakModelHandle<terminal::Terminal>>,
}
impl Project {
pub fn create_terminal(
&mut self,
working_directory: Option<PathBuf>,
window_id: usize,
cx: &mut ModelContext<Self>,
) -> anyhow::Result<ModelHandle<Terminal>> {
if self.is_remote() {
return Err(anyhow::anyhow!(
"creating terminals as a guest is not supported yet"
));
} else {
let settings = cx.global::<Settings>();
let shell = settings.terminal_shell();
let envs = settings.terminal_env();
let scroll = settings.terminal_scroll();
let terminal = TerminalBuilder::new(
working_directory.clone(),
shell,
envs,
settings.terminal_overrides.blinking.clone(),
scroll,
window_id,
)
.map(|builder| {
let terminal_handle = cx.add_model(|cx| builder.subscribe(cx));
self.terminals
.local_handles
.push(terminal_handle.downgrade());
let id = terminal_handle.id();
cx.observe_release(&terminal_handle, move |project, _terminal, _cx| {
let handles = &mut project.terminals.local_handles;
if let Some(index) = handles.iter().position(|terminal| terminal.id() == id) {
handles.remove(index);
}
})
.detach();
terminal_handle
});
terminal
}
}
}
// TODO: Add a few tests for adding and removing terminal tabs

View File

@@ -1314,7 +1314,7 @@ impl View for ProjectPanel {
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.set.insert("menu".into());
cx.add_identifier("menu");
cx
}
}

View File

@@ -8,7 +8,7 @@ publish = false
path = "src/rope.rs"
[dependencies]
bromberg_sl2 = { git = "https://github.com/zed-industries/bromberg_sl2", rev = "dac565a90e8f9245f48ff46225c915dc50f76920" }
bromberg_sl2 = { git = "https://github.com/zed-industries/bromberg_sl2", rev = "950bc5482c216c395049ae33ae4501e08975f17f" }
smallvec = { version = "1.6", features = ["union"] }
sum_tree = { path = "../sum_tree" }
arrayvec = "0.7.1"

View File

@@ -231,6 +231,7 @@ message ParticipantProject {
message Follower {
PeerId leader_id = 1;
PeerId follower_id = 2;
uint64 project_id = 3;
}
message ParticipantLocation {

View File

@@ -114,7 +114,7 @@ pub struct ConnectionState {
const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1);
const WRITE_TIMEOUT: Duration = Duration::from_secs(2);
pub const RECEIVE_TIMEOUT: Duration = Duration::from_secs(5);
pub const RECEIVE_TIMEOUT: Duration = Duration::from_secs(10);
impl Peer {
pub fn new(epoch: u32) -> Arc<Self> {

View File

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

View File

@@ -248,15 +248,15 @@ impl Item for ProjectSearchView {
tab_theme: &theme::Tab,
cx: &gpui::AppContext,
) -> ElementBox {
let settings = cx.global::<Settings>();
let search_theme = &settings.theme.search;
Flex::row()
.with_child(
Svg::new("icons/magnifying_glass_12.svg")
.with_color(tab_theme.label.text.color)
.constrained()
.with_width(search_theme.tab_icon_width)
.with_width(tab_theme.type_icon_width)
.aligned()
.contained()
.with_margin_right(tab_theme.spacing)
.boxed(),
)
.with_children(self.model.read(cx).active_query.as_ref().map(|query| {
@@ -264,8 +264,6 @@ impl Item for ProjectSearchView {
Label::new(query_text, tab_theme.label.clone())
.aligned()
.contained()
.with_margin_left(search_theme.tab_icon_spacing)
.boxed()
}))
.boxed()
@@ -540,7 +538,7 @@ impl ProjectSearchView {
let range_to_select = match_ranges[new_index].clone();
self.results_editor.update(cx, |editor, cx| {
editor.unfold_ranges([range_to_select.clone()], false, cx);
editor.unfold_ranges([range_to_select.clone()], false, true, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([range_to_select])
});

View File

@@ -94,6 +94,8 @@ pub struct EditorSettings {
pub soft_wrap: Option<SoftWrap>,
pub preferred_line_length: Option<u32>,
pub format_on_save: Option<FormatOnSave>,
pub remove_trailing_whitespace_on_save: Option<bool>,
pub ensure_final_newline_on_save: Option<bool>,
pub formatter: Option<Formatter>,
pub enable_language_server: Option<bool>,
}
@@ -361,6 +363,12 @@ impl Settings {
hard_tabs: required(defaults.editor.hard_tabs),
soft_wrap: required(defaults.editor.soft_wrap),
preferred_line_length: required(defaults.editor.preferred_line_length),
remove_trailing_whitespace_on_save: required(
defaults.editor.remove_trailing_whitespace_on_save,
),
ensure_final_newline_on_save: required(
defaults.editor.ensure_final_newline_on_save,
),
format_on_save: required(defaults.editor.format_on_save),
formatter: required(defaults.editor.formatter),
enable_language_server: required(defaults.editor.enable_language_server),
@@ -460,6 +468,18 @@ impl Settings {
self.language_setting(language, |settings| settings.preferred_line_length)
}
pub fn remove_trailing_whitespace_on_save(&self, language: Option<&str>) -> bool {
self.language_setting(language, |settings| {
settings.remove_trailing_whitespace_on_save.clone()
})
}
pub fn ensure_final_newline_on_save(&self, language: Option<&str>) -> bool {
self.language_setting(language, |settings| {
settings.ensure_final_newline_on_save.clone()
})
}
pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave {
self.language_setting(language, |settings| settings.format_on_save.clone())
}
@@ -558,6 +578,8 @@ impl Settings {
hard_tabs: Some(false),
soft_wrap: Some(SoftWrap::None),
preferred_line_length: Some(80),
remove_trailing_whitespace_on_save: Some(true),
ensure_final_newline_on_save: Some(true),
format_on_save: Some(FormatOnSave::On),
formatter: Some(Formatter::LanguageServer),
enable_language_server: Some(true),

View File

@@ -469,53 +469,50 @@ impl View for TerminalView {
let mut context = Self::default_keymap_context();
let mode = self.terminal.read(cx).last_content.mode;
context.map.insert(
"screen".to_string(),
(if mode.contains(TermMode::ALT_SCREEN) {
context.add_key(
"screen",
if mode.contains(TermMode::ALT_SCREEN) {
"alt"
} else {
"normal"
})
.to_string(),
},
);
if mode.contains(TermMode::APP_CURSOR) {
context.set.insert("DECCKM".to_string());
context.add_identifier("DECCKM");
}
if mode.contains(TermMode::APP_KEYPAD) {
context.set.insert("DECPAM".to_string());
}
//Note the ! here
if !mode.contains(TermMode::APP_KEYPAD) {
context.set.insert("DECPNM".to_string());
context.add_identifier("DECPAM");
} else {
context.add_identifier("DECPNM");
}
if mode.contains(TermMode::SHOW_CURSOR) {
context.set.insert("DECTCEM".to_string());
context.add_identifier("DECTCEM");
}
if mode.contains(TermMode::LINE_WRAP) {
context.set.insert("DECAWM".to_string());
context.add_identifier("DECAWM");
}
if mode.contains(TermMode::ORIGIN) {
context.set.insert("DECOM".to_string());
context.add_identifier("DECOM");
}
if mode.contains(TermMode::INSERT) {
context.set.insert("IRM".to_string());
context.add_identifier("IRM");
}
//LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
context.set.insert("LNM".to_string());
context.add_identifier("LNM");
}
if mode.contains(TermMode::FOCUS_IN_OUT) {
context.set.insert("report_focus".to_string());
context.add_identifier("report_focus");
}
if mode.contains(TermMode::ALTERNATE_SCROLL) {
context.set.insert("alternate_scroll".to_string());
context.add_identifier("alternate_scroll");
}
if mode.contains(TermMode::BRACKETED_PASTE) {
context.set.insert("bracketed_paste".to_string());
context.add_identifier("bracketed_paste");
}
if mode.intersects(TermMode::MOUSE_MODE) {
context.set.insert("any_mouse_reporting".to_string());
context.add_identifier("any_mouse_reporting");
}
{
let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
@@ -527,9 +524,7 @@ impl View for TerminalView {
} else {
"off"
};
context
.map
.insert("mouse_reporting".to_string(), mouse_reporting.to_string());
context.add_key("mouse_reporting", mouse_reporting);
}
{
let format = if mode.contains(TermMode::SGR_MOUSE) {
@@ -539,9 +534,7 @@ impl View for TerminalView {
} else {
"normal"
};
context
.map
.insert("mouse_format".to_string(), format.to_string());
context.add_key("mouse_format", format);
}
context
}
@@ -589,11 +582,16 @@ impl Item for TerminalView {
Flex::row()
.with_child(
Label::new(title, tab_theme.label.clone())
gpui::elements::Svg::new("icons/terminal_12.svg")
.with_color(tab_theme.label.text.color)
.constrained()
.with_width(tab_theme.type_icon_width)
.aligned()
.contained()
.with_margin_right(tab_theme.spacing)
.boxed(),
)
.with_child(Label::new(title, tab_theme.label.clone()).aligned().boxed())
.boxed()
}

View File

@@ -80,18 +80,19 @@ pub struct Titlebar {
pub follower_avatar_overlap: f32,
pub leader_selection: ContainerStyle,
pub offline_icon: OfflineIcon,
pub avatar: AvatarStyle,
pub inactive_avatar: AvatarStyle,
pub leader_avatar: AvatarStyle,
pub follower_avatar: AvatarStyle,
pub inactive_avatar_grayscale: bool,
pub sign_in_prompt: Interactive<ContainedText>,
pub outdated_warning: ContainedText,
pub share_button: Interactive<ContainedText>,
pub call_control: Interactive<IconButton>,
pub toggle_contacts_button: Interactive<IconButton>,
pub user_menu_button: Interactive<IconButton>,
pub toggle_contacts_badge: ContainerStyle,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Copy, Clone, Deserialize, Default)]
pub struct AvatarStyle {
#[serde(flatten)]
pub image: ImageStyle,
@@ -214,7 +215,8 @@ pub struct Tab {
pub label: LabelStyle,
pub description: ContainedText,
pub spacing: f32,
pub icon_width: f32,
pub close_icon_width: f32,
pub type_icon_width: f32,
pub icon_close: Color,
pub icon_close_active: Color,
pub icon_dirty: Color,
@@ -257,8 +259,6 @@ pub struct Search {
pub match_background: Color,
pub match_index: ContainedText,
pub results_status: TextStyle,
pub tab_icon_width: f32,
pub tab_icon_spacing: f32,
pub dismiss_button: Interactive<IconButton>,
}
@@ -563,6 +563,7 @@ pub struct Editor {
pub invalid_hint_diagnostic: DiagnosticStyle,
pub autocomplete: AutocompleteStyle,
pub code_actions: CodeActions,
pub folds: Folds,
pub unnecessary_code_fade: f32,
pub hover_popover: HoverPopover,
pub link_definition: HighlightStyle,
@@ -632,13 +633,35 @@ pub struct FieldEditor {
pub selection: SelectionStyle,
}
#[derive(Clone, Deserialize, Default)]
pub struct InteractiveColor {
pub color: Color,
}
#[derive(Clone, Deserialize, Default)]
pub struct CodeActions {
#[serde(default)]
pub indicator: Color,
pub indicator: Interactive<InteractiveColor>,
pub vertical_scale: f32,
}
#[derive(Clone, Deserialize, Default)]
pub struct Folds {
pub indicator: Interactive<InteractiveColor>,
pub ellipses: FoldEllipses,
pub fold_background: Color,
pub icon_width: f32,
pub folded_icon: String,
pub foldable_icon: String,
}
#[derive(Clone, Deserialize, Default)]
pub struct FoldEllipses {
pub text_color: Color,
pub background: Interactive<InteractiveColor>,
pub corner_radius_factor: f32,
}
#[derive(Clone, Deserialize, Default)]
pub struct DiffStyle {
pub inserted: Color,
@@ -822,7 +845,9 @@ pub struct TerminalStyle {
pub struct FeedbackStyle {
pub submit_button: Interactive<ContainedText>,
pub button_margin: f32,
pub info_text: ContainedText,
pub info_text_default: ContainedText,
pub link_text_default: ContainedText,
pub link_text_hover: ContainedText,
}
#[derive(Clone, Deserialize, Default)]

View File

@@ -237,7 +237,7 @@ macro_rules! iife {
};
}
/// Async lImmediately invoked function expression. Good for using the ? operator
/// Async Immediately invoked function expression. Good for using the ? operator
/// in functions which do not return an Option or Result. Async version of above
#[macro_export]
macro_rules! async_iife {
@@ -262,10 +262,7 @@ impl<T: Ord + Clone> RangeExt<T> for Range<T> {
}
fn overlaps(&self, other: &Range<T>) -> bool {
self.contains(&other.start)
|| self.contains(&other.end)
|| other.contains(&self.start)
|| other.contains(&self.end)
self.start < other.end && other.start < self.end
}
}
@@ -279,10 +276,7 @@ impl<T: Ord + Clone> RangeExt<T> for RangeInclusive<T> {
}
fn overlaps(&self, other: &Range<T>) -> bool {
self.contains(&other.start)
|| self.contains(&other.end)
|| other.contains(&self.start())
|| other.contains(&self.end())
self.start() < &other.end && &other.start <= self.end()
}
}

View File

@@ -36,6 +36,7 @@ pub enum Motion {
Matching,
FindForward { before: bool, text: Arc<str> },
FindBackward { after: bool, text: Arc<str> },
NextLineStart,
}
#[derive(Clone, Deserialize, PartialEq)]
@@ -74,6 +75,7 @@ actions!(
StartOfDocument,
EndOfDocument,
Matching,
NextLineStart,
]
);
impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]);
@@ -111,6 +113,7 @@ pub fn init(cx: &mut MutableAppContext) {
&PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
);
cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx))
}
pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) {
@@ -138,15 +141,43 @@ pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) {
impl Motion {
pub fn linewise(&self) -> bool {
use Motion::*;
matches!(
self,
Down | Up | StartOfDocument | EndOfDocument | CurrentLine
)
match self {
Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart => true,
EndOfLine
| NextWordEnd { .. }
| Matching
| FindForward { .. }
| Left
| Backspace
| Right
| StartOfLine
| NextWordStart { .. }
| PreviousWordStart { .. }
| FirstNonWhitespace
| FindBackward { .. } => false,
}
}
pub fn infallible(&self) -> bool {
use Motion::*;
matches!(self, StartOfDocument | CurrentLine | EndOfDocument)
match self {
StartOfDocument | EndOfDocument | CurrentLine => true,
Down
| Up
| EndOfLine
| NextWordEnd { .. }
| Matching
| FindForward { .. }
| Left
| Backspace
| Right
| StartOfLine
| NextWordStart { .. }
| PreviousWordStart { .. }
| FirstNonWhitespace
| FindBackward { .. }
| NextLineStart => false,
}
}
pub fn inclusive(&self) -> bool {
@@ -160,7 +191,8 @@ impl Motion {
| EndOfLine
| NextWordEnd { .. }
| Matching
| FindForward { .. } => true,
| FindForward { .. }
| NextLineStart => true,
Left
| Backspace
| Right
@@ -214,6 +246,7 @@ impl Motion {
find_backward(map, point, *after, text.clone(), times),
SelectionGoal::None,
),
NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
};
(new_point != point || infallible).then_some((new_point, goal))
@@ -543,3 +576,8 @@ fn find_backward(
})
.unwrap_or(from)
}
fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
let new_row = (point.row() + times as u32).min(map.max_buffer_row());
map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left)
}

View File

@@ -473,6 +473,7 @@ pub(crate) fn normal_replace(text: Arc<str>, cx: &mut MutableAppContext) {
#[cfg(test)]
mod test {
use gpui::TestAppContext;
use indoc::indoc;
use crate::{
@@ -515,15 +516,15 @@ mod test {
.await;
}
// #[gpui::test]
// async fn test_enter(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
// cx.assert_all(indoc! {"
// ˇThe qˇuick broˇwn
// ˇfox jumps"
// })
// .await;
// }
#[gpui::test]
async fn test_enter(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
cx.assert_all(indoc! {"
ˇThe qˇuick broˇwn
ˇfox jumps"
})
.await;
}
#[gpui::test]
async fn test_k(cx: &mut gpui::TestAppContext) {
@@ -1030,7 +1031,7 @@ mod test {
}
#[gpui::test]
async fn test_percent(cx: &mut gpui::TestAppContext) {
async fn test_percent(cx: &mut TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["%"]);
cx.assert_all("ˇconsole.logˇ(ˇvaˇrˇ)ˇ;").await;
cx.assert_all("ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")

View File

@@ -73,34 +73,30 @@ impl VimState {
pub fn keymap_context_layer(&self) -> KeymapContext {
let mut context = KeymapContext::default();
context.map.insert(
"vim_mode".to_string(),
context.add_key(
"vim_mode",
match self.mode {
Mode::Normal => "normal",
Mode::Visual { .. } => "visual",
Mode::Insert => "insert",
}
.to_string(),
},
);
if self.vim_controlled() {
context.set.insert("VimControl".to_string());
context.add_identifier("VimControl");
}
let active_operator = self.operator_stack.last();
if let Some(active_operator) = active_operator {
for context_flag in active_operator.context_flags().into_iter() {
context.set.insert(context_flag.to_string());
context.add_identifier(*context_flag);
}
}
context.map.insert(
"vim_operator".to_string(),
active_operator
.map(|op| op.id())
.unwrap_or_else(|| "none")
.to_string(),
context.add_key(
"vim_operator",
active_operator.map(|op| op.id()).unwrap_or_else(|| "none"),
);
context

View File

@@ -0,0 +1 @@
[{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]

View File

@@ -42,6 +42,7 @@ impl View for ToggleDockButton {
let workspace = workspace.unwrap();
let dock_position = workspace.read(cx).dock.position;
let dock_pane = workspace.read(cx.app).dock_pane().clone();
let theme = cx.global::<Settings>().theme.clone();
@@ -67,7 +68,6 @@ impl View for ToggleDockButton {
})
.with_cursor_style(CursorStyle::PointingHand)
.on_up(MouseButton::Left, move |event, cx| {
let dock_pane = workspace.read(cx.app).dock_pane();
let drop_index = dock_pane.read(cx.app).items_len() + 1;
handle_dropped_item(event, &dock_pane.downgrade(), drop_index, false, None, cx);
});

View File

@@ -21,6 +21,7 @@ use gpui::{
vector::{vec2f, Vector2F},
},
impl_actions, impl_internal_actions,
keymap_matcher::KeymapContext,
platform::{CursorStyle, NavigationDirection},
Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
@@ -1149,40 +1150,53 @@ impl Pane {
let tab_active = ix == self.active_item_index;
row.add_child({
enum Tab {}
let mut receiver = dragged_item_receiver::<Tab, _>(ix, ix, true, None, cx, {
let item = item.clone();
let pane = pane.clone();
let detail = detail.clone();
enum TabDragReceiver {}
let mut receiver =
dragged_item_receiver::<TabDragReceiver, _>(ix, ix, true, None, cx, {
let item = item.clone();
let pane = pane.clone();
let detail = detail.clone();
let theme = cx.global::<Settings>().theme.clone();
let theme = cx.global::<Settings>().theme.clone();
move |mouse_state, cx| {
let tab_style = theme.workspace.tab_bar.tab_style(pane_active, tab_active);
let hovered = mouse_state.hovered();
Self::render_tab(&item, pane, ix == 0, detail, hovered, tab_style, cx)
}
});
move |mouse_state, cx| {
let tab_style =
theme.workspace.tab_bar.tab_style(pane_active, tab_active);
let hovered = mouse_state.hovered();
enum Tab {}
MouseEventHandler::<Tab>::new(ix, cx, |_, cx| {
Self::render_tab(
&item,
pane.clone(),
ix == 0,
detail,
hovered,
tab_style,
cx,
)
})
.on_down(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ActivateItem(ix));
})
.on_click(MouseButton::Middle, {
let item = item.clone();
move |_, cx: &mut EventContext| {
cx.dispatch_action(CloseItem {
item_id: item.id(),
pane: pane.clone(),
})
}
})
.boxed()
}
});
if !pane_active || !tab_active {
receiver = receiver.with_cursor_style(CursorStyle::PointingHand);
}
receiver
.on_down(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ActivateItem(ix));
cx.propagate_event();
})
.on_click(MouseButton::Middle, {
let item = item.clone();
let pane = pane.clone();
move |_, cx: &mut EventContext| {
cx.dispatch_action(CloseItem {
item_id: item.id(),
pane: pane.clone(),
})
}
})
.as_draggable(
DraggedItem {
item,
@@ -1354,7 +1368,7 @@ impl Pane {
} else {
Empty::new().boxed()
})
.with_width(tab_style.icon_width)
.with_width(tab_style.close_icon_width)
.boxed(),
)
.boxed(),
@@ -1437,7 +1451,7 @@ impl View for Pane {
.with_style(theme.workspace.tab_bar.container)
.boxed()
})
.on_click(MouseButton::Left, move |_, cx| {
.on_down(MouseButton::Left, move |_, cx| {
cx.dispatch_action(ActivateItem(active_item_index));
})
.boxed(),
@@ -1550,6 +1564,14 @@ impl View for Pane {
}
}
}
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut keymap = Self::default_keymap_context();
if self.docked.is_some() {
keymap.add_identifier("docked");
}
keymap
}
}
fn tab_bar_button<A: Action>(

View File

@@ -108,7 +108,7 @@ impl Item for SharedScreen {
Svg::new("icons/disable_screen_sharing_12.svg")
.with_color(style.label.text.color)
.constrained()
.with_width(style.icon_width)
.with_width(style.type_icon_width)
.aligned()
.contained()
.with_margin_right(style.spacing)

View File

@@ -0,0 +1,88 @@
use gpui::{
elements::{Empty, MouseEventHandler, Svg},
CursorStyle, Element, ElementBox, Entity, MouseButton, RenderContext, View, ViewContext,
ViewHandle, WeakViewHandle,
};
use settings::Settings;
use crate::{dock::FocusDock, item::ItemHandle, StatusItemView, Workspace};
pub struct TerminalButton {
workspace: WeakViewHandle<Workspace>,
}
// TODO: Rename this to `DeployTerminalButton`
impl TerminalButton {
pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
// When terminal moves, redraw so that the icon and toggle status matches.
cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach();
Self {
workspace: workspace.downgrade(),
}
}
}
impl Entity for TerminalButton {
type Event = ();
}
impl View for TerminalButton {
fn ui_name() -> &'static str {
"TerminalButton"
}
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
let workspace = self.workspace.upgrade(cx);
if workspace.is_none() {
return Empty::new().boxed();
}
// let workspace = workspace.unwrap();
let theme = cx.global::<Settings>().theme.clone();
MouseEventHandler::<Self>::new(0, cx, {
let theme = theme.clone();
move |state, _| {
let style = theme
.workspace
.status_bar
.sidebar_buttons
.item
.style_for(state, true);
Svg::new("icons/terminal_12.svg")
.with_color(style.icon_color)
.constrained()
.with_width(style.icon_size)
.with_height(style.icon_size)
.contained()
.with_style(style.container)
.boxed()
}
})
.with_cursor_style(CursorStyle::PointingHand)
.on_up(MouseButton::Left, move |_, _| {
// TODO: Do we need this stuff?
// let dock_pane = workspace.read(cx.app).dock_pane();
// let drop_index = dock_pane.read(cx.app).items_len() + 1;
// handle_dropped_item(event, &dock_pane.downgrade(), drop_index, false, None, cx);
})
.on_click(MouseButton::Left, |_, cx| {
cx.dispatch_action(FocusDock);
})
.with_tooltip::<Self, _>(
0,
"Show Terminal".into(),
Some(Box::new(FocusDock)),
theme.tooltip.clone(),
cx,
)
.boxed()
}
}
impl StatusItemView for TerminalButton {
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
}

View File

@@ -12,6 +12,7 @@ pub mod searchable;
pub mod shared_screen;
pub mod sidebar;
mod status_bar;
pub mod terminal_button;
mod toolbar;
pub use smallvec;
@@ -56,6 +57,7 @@ use std::{
sync::Arc,
time::Duration,
};
use terminal_button::TerminalButton;
use crate::{
notifications::simple_message_notification::{MessageNotification, OsOpen},
@@ -584,6 +586,7 @@ impl Workspace {
let left_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Left));
let right_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Right));
let left_sidebar_buttons = cx.add_view(|cx| SidebarButtons::new(left_sidebar.clone(), cx));
let toggle_terminal = cx.add_view(|cx| TerminalButton::new(handle.clone(), cx));
let toggle_dock = cx.add_view(|cx| ToggleDockButton::new(handle, cx));
let right_sidebar_buttons =
cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), cx));
@@ -592,6 +595,7 @@ impl Workspace {
status_bar.add_left_item(left_sidebar_buttons, cx);
status_bar.add_right_item(right_sidebar_buttons, cx);
status_bar.add_right_item(toggle_dock, cx);
status_bar.add_right_item(toggle_terminal, cx);
status_bar
});
@@ -2716,11 +2720,7 @@ impl View for Workspace {
}
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut keymap = Self::default_keymap_context();
if self.active_pane() == self.dock_pane() {
keymap.set.insert("Dock".into());
}
keymap
Self::default_keymap_context()
}
}

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.75.0"
version = "0.76.0"
publish = false
[lib]

View File

@@ -1 +1 @@
dev
preview

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 739 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 442 KiB

View File

@@ -43,8 +43,10 @@
; Special identifiers
((identifier) @constructor
(#match? @constructor "^[A-Z]"))
((identifier) @type
(#match? @type "^[A-Z]"))
(type_identifier) @type
(predefined_type) @type.builtin
([
(identifier)
@@ -59,12 +61,15 @@
(super) @variable.special
[
(true)
(false)
(null)
(undefined)
] @constant.builtin
[
(true)
(false)
] @boolean
(comment) @comment
[
@@ -72,15 +77,11 @@
(template_string)
] @string
(regex) @string.special
(regex) @string.regex
(number) @number
; Tokens
(template_substitution
"${" @punctuation.special
"}" @punctuation.special) @embedded
[
";"
"?."
@@ -189,13 +190,9 @@
"yield"
] @keyword
; Types
(type_identifier) @type
(predefined_type) @type.builtin
((identifier) @type
(#match? @type "^[A-Z]"))
(template_substitution
"${" @punctuation.special
"}" @punctuation.special) @embedded
(type_arguments
"<" @punctuation.bracket

View File

@@ -137,7 +137,7 @@
;; Constants
((identifier) @constant
(#lua-match? @constant "^[A-Z][A-Z_0-9]*$"))
(#match? @constant "^[A-Z][A-Z_0-9]*$"))
(vararg_expression) @constant
@@ -164,11 +164,17 @@
(parameters (identifier) @parameter)
(function_call name: (identifier) @function.call)
(function_declaration name: (identifier) @function)
(function_call
name: [
(identifier) @function
(dot_index_expression field: (identifier) @function)
])
(function_call name: (dot_index_expression field: (identifier) @function.call))
(function_declaration name: (dot_index_expression field: (identifier) @function))
(function_declaration
name: [
(identifier) @function.definition
(dot_index_expression field: (identifier) @function.definition)
])
(method_index_expression method: (identifier) @method)

View File

@@ -3,7 +3,7 @@
[(string)
(here_string)
(byte_string)] @string
(regex) @string.special
(regex) @string.regex
(escape_sequence) @escape
[(comment)
@@ -19,7 +19,7 @@
(quote . (symbol)) @constant
(extension) @keyword
(lang_name) @variable.builtin
(lang_name) @variable.special
((symbol) @operator
(#match? @operator "^(\\+|-|\\*|/|=|>|<|>=|<=)$"))

View File

@@ -95,7 +95,7 @@
(bare_symbol)
] @string.special.symbol
(regex) @string.special.regex
(regex) @string.regex
(escape_sequence) @escape
[

View File

@@ -46,6 +46,11 @@
((identifier) @constructor
(#match? @constructor "^[A-Z]"))
((identifier) @type
(#match? @type "^[A-Z]"))
(type_identifier) @type
(predefined_type) @type.builtin
([
(identifier)
(shorthand_property_identifier)
@@ -59,12 +64,15 @@
(super) @variable.special
[
(true)
(false)
(null)
(undefined)
] @constant.builtin
[
(true)
(false)
] @boolean
(comment) @comment
[
@@ -72,15 +80,11 @@
(template_string)
] @string
(regex) @string.special
(regex) @string.regex
(number) @number
; Tokens
(template_substitution
"${" @punctuation.special
"}" @punctuation.special) @embedded
[
";"
"?."
@@ -190,13 +194,9 @@
"yield"
] @keyword
; Types
(type_identifier) @type
(predefined_type) @type.builtin
((identifier) @type
(#match? @type "^[A-Z]"))
(template_substitution
"${" @punctuation.special
"}" @punctuation.special) @embedded
(type_arguments
"<" @punctuation.bracket

View File

@@ -13,7 +13,6 @@ use client::{
http::{self, HttpClient},
UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN,
};
use futures::{
channel::{mpsc, oneshot},
FutureExt, SinkExt, StreamExt,
@@ -31,8 +30,10 @@ use settings::{
};
use simplelog::ConfigBuilder;
use smol::process::Command;
use std::{env, ffi::OsStr, panic, path::PathBuf, sync::Arc, thread, time::Duration};
use std::{fs::OpenOptions, os::unix::prelude::OsStrExt};
use std::{
env, ffi::OsStr, fs::OpenOptions, io::Write as _, os::unix::prelude::OsStrExt, panic,
path::PathBuf, sync::Arc, thread, time::Duration,
};
use terminal_view::{get_working_directory, TerminalView};
use fs::RealFs;
@@ -119,7 +120,9 @@ fn main() {
));
watch_settings_file(default_settings, settings_file_content, themes.clone(), cx);
upload_previous_panics(http.clone(), cx);
if !stdout_is_a_pty() {
upload_previous_panics(http.clone(), cx);
}
let client = client::Client::new(http.clone(), cx);
let mut languages = LanguageRegistry::new(login_shell_env_loaded);
@@ -330,18 +333,22 @@ fn init_panic_hook(app_version: String) {
),
};
let panic_filename = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
std::fs::write(
paths::LOGS_DIR.join(format!("zed-{}-{}.panic", app_version, panic_filename)),
&message,
)
.context("error writing panic to disk")
.log_err();
if is_pty {
eprintln!("{}", message);
} else {
log::error!(target: "panic", "{}", message);
return;
}
let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
let panic_file_path =
paths::LOGS_DIR.join(format!("zed-{}-{}.panic", app_version, timestamp));
let panic_file = std::fs::OpenOptions::new()
.append(true)
.create(true)
.open(&panic_file_path)
.log_err();
if let Some(mut panic_file) = panic_file {
write!(&mut panic_file, "{}", message).log_err();
panic_file.flush().log_err();
}
}));
}

View File

@@ -381,7 +381,47 @@ pub fn build_window_options(
}
fn restart(_: &Restart, cx: &mut gpui::MutableAppContext) {
cx.platform().restart();
let mut workspaces = cx
.window_ids()
.filter_map(|window_id| cx.root_view::<Workspace>(window_id))
.collect::<Vec<_>>();
// If multiple windows have unsaved changes, and need a save prompt,
// prompt in the active window before switching to a different window.
workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
let should_confirm = cx.global::<Settings>().confirm_quit;
cx.spawn(|mut cx| async move {
if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
let answer = cx
.prompt(
workspace.window_id(),
PromptLevel::Info,
"Are you sure you want to restart?",
&["Restart", "Cancel"],
)
.next()
.await;
if answer != Some(0) {
return Ok(());
}
}
// If the user cancels any save prompt, then keep the app open.
for workspace in workspaces {
if !workspace
.update(&mut cx, |workspace, cx| {
workspace.prepare_to_close(true, cx)
})
.await?
{
return Ok(());
}
}
cx.platform().restart();
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {

2
styles/.prettierignore Normal file
View File

@@ -0,0 +1,2 @@
package-lock.json
package.json

229
styles/package-lock.json generated
View File

@@ -9,67 +9,83 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@types/chroma-js": "^2.1.3",
"@types/node": "^17.0.23",
"@types/chroma-js": "^2.4.0",
"@types/node": "^18.14.1",
"bezier-easing": "^2.1.0",
"case-anything": "^2.1.10",
"chroma-js": "^2.4.2",
"deepmerge": "^4.3.0",
"toml": "^3.0.0",
"ts-node": "^10.7.0"
}
},
"node_modules/@cspotcode/source-map-consumer": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
"engines": {
"node": ">= 12"
"ts-node": "^10.9.1"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
"integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dependencies": {
"@cspotcode/source-map-consumer": "0.8.0"
"@jridgewell/trace-mapping": "0.3.9"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg=="
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA=="
},
"node_modules/@tsconfig/node12": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw=="
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="
},
"node_modules/@tsconfig/node14": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg=="
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="
},
"node_modules/@tsconfig/node16": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA=="
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ=="
},
"node_modules/@types/chroma-js": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz",
"integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g=="
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.0.tgz",
"integrity": "sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw=="
},
"node_modules/@types/node": {
"version": "17.0.23",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz",
"integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw=="
"version": "18.14.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz",
"integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ=="
},
"node_modules/acorn": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
"bin": {
"acorn": "bin/acorn"
},
@@ -90,6 +106,11 @@
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
},
"node_modules/bezier-easing": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
"integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="
},
"node_modules/case-anything": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz",
@@ -111,6 +132,14 @@
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
},
"node_modules/deepmerge": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz",
"integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -130,11 +159,11 @@
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
},
"node_modules/ts-node": {
"version": "10.7.0",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz",
"integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==",
"version": "10.9.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
"dependencies": {
"@cspotcode/source-map-support": "0.7.0",
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
@@ -145,7 +174,7 @@
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"v8-compile-cache-lib": "^3.0.0",
"v8-compile-cache-lib": "^3.0.1",
"yn": "3.1.1"
},
"bin": {
@@ -172,9 +201,9 @@
}
},
"node_modules/typescript": {
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
"integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"peer": true,
"bin": {
"tsc": "bin/tsc",
@@ -185,9 +214,9 @@
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz",
"integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA=="
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="
},
"node_modules/yn": {
"version": "3.1.1",
@@ -199,53 +228,67 @@
}
},
"dependencies": {
"@cspotcode/source-map-consumer": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg=="
},
"@cspotcode/source-map-support": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
"integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"requires": {
"@cspotcode/source-map-consumer": "0.8.0"
"@jridgewell/trace-mapping": "0.3.9"
}
},
"@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="
},
"@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
},
"@jridgewell/trace-mapping": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"requires": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"@tsconfig/node10": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg=="
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA=="
},
"@tsconfig/node12": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw=="
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="
},
"@tsconfig/node14": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg=="
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="
},
"@tsconfig/node16": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA=="
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ=="
},
"@types/chroma-js": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz",
"integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g=="
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.0.tgz",
"integrity": "sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw=="
},
"@types/node": {
"version": "17.0.23",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz",
"integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw=="
"version": "18.14.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz",
"integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ=="
},
"acorn": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ=="
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw=="
},
"acorn-walk": {
"version": "8.2.0",
@@ -257,6 +300,11 @@
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
},
"bezier-easing": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
"integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="
},
"case-anything": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz",
@@ -272,6 +320,11 @@
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
},
"deepmerge": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz",
"integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og=="
},
"diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -288,11 +341,11 @@
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
},
"ts-node": {
"version": "10.7.0",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz",
"integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==",
"version": "10.9.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
"requires": {
"@cspotcode/source-map-support": "0.7.0",
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
@@ -303,20 +356,20 @@
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"v8-compile-cache-lib": "^3.0.0",
"v8-compile-cache-lib": "^3.0.1",
"yn": "3.1.1"
}
},
"typescript": {
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
"integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"peer": true
},
"v8-compile-cache-lib": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz",
"integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA=="
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="
},
"yn": {
"version": "3.1.1",

View File

@@ -10,11 +10,19 @@
"author": "",
"license": "ISC",
"dependencies": {
"@types/chroma-js": "^2.1.3",
"@types/node": "^17.0.23",
"@types/chroma-js": "^2.4.0",
"@types/node": "^18.14.1",
"bezier-easing": "^2.1.0",
"case-anything": "^2.1.10",
"chroma-js": "^2.4.2",
"deepmerge": "^4.3.0",
"toml": "^3.0.0",
"ts-node": "^10.7.0"
"ts-node": "^10.9.1"
},
"prettier": {
"semi": false,
"printWidth": 80,
"htmlWhitespaceSensitivity": "strict",
"tabWidth": 4
}
}

View File

@@ -1,73 +1,92 @@
import * as fs from "fs";
import toml from "toml";
import {
schemeMeta
} from "./colorSchemes";
import { Meta } from "./themes/common/colorScheme";
import https from "https";
import crypto from "crypto";
import * as fs from "fs"
import toml from "toml"
import { schemeMeta } from "./colorSchemes"
import { Meta, Verification } from "./themes/common/colorScheme"
import https from "https"
import crypto from "crypto"
const accepted_licenses_file = `${__dirname}/../../script/licenses/zed-licenses.toml`
// Use the cargo-about configuration file as the source of truth for supported licenses.
function parseAcceptedToml(file: string): string[] {
let buffer = fs.readFileSync(file).toString();
let buffer = fs.readFileSync(file).toString()
let obj = toml.parse(buffer);
let obj = toml.parse(buffer)
if (!Array.isArray(obj.accepted)) {
throw Error("Accepted license source is malformed")
}
if (!Array.isArray(obj.accepted)) {
throw Error("Accepted license source is malformed")
}
return obj.accepted
return obj.accepted
}
function checkLicenses(schemeMeta: Meta[], licenses: string[]) {
for (let meta of schemeMeta) {
// FIXME: Add support for conjuctions and conditions
if (licenses.indexOf(meta.license.SPDX) < 0) {
throw Error(`License for theme ${meta.name} (${meta.license.SPDX}) is not supported`)
for (let meta of schemeMeta) {
// FIXME: Add support for conjuctions and conditions
if (licenses.indexOf(meta.license.SPDX) < 0) {
throw Error(
`License for theme ${meta.name} (${meta.license.SPDX}) is not supported`
)
}
}
}
}
function getLicenseText(schemeMeta: Meta[], callback: (meta: Meta, license_text: string) => void) {
for (let meta of schemeMeta) {
// The following copied from the example code on nodejs.org:
// https://nodejs.org/api/http.html#httpgetoptions-callback
https.get(meta.license.https_url, (res) => {
const { statusCode } = res;
if (statusCode < 200 || statusCode >= 300) {
throw new Error(`Failed to fetch license for: ${meta.name}, Status Code: ${statusCode}`);
}
res.setEncoding('utf8');
let rawData = '';
res.on('data', (chunk) => { rawData += chunk; });
res.on('end', () => {
const hash = crypto.createHash('sha256').update(rawData).digest('hex');
if (meta.license.license_checksum == hash) {
callback(meta, rawData)
function getLicenseText(
schemeMeta: Meta[],
callback: (meta: Meta, license_text: string) => void
) {
for (let meta of schemeMeta) {
if (typeof meta.license.license_text == "string") {
callback(meta, meta.license.license_text)
} else {
throw Error(`Checksum for ${meta.name} did not match file downloaded from ${meta.license.https_url}`)
let license_text_obj: Verification = meta.license.license_text;
// The following copied from the example code on nodejs.org:
// https://nodejs.org/api/http.html#httpgetoptions-callback
https
.get(license_text_obj.https_url, (res) => {
const { statusCode } = res
if (statusCode < 200 || statusCode >= 300) {
throw new Error(
`Failed to fetch license for: ${meta.name}, Status Code: ${statusCode}`
)
}
res.setEncoding("utf8")
let rawData = ""
res.on("data", (chunk) => {
rawData += chunk
})
res.on("end", () => {
const hash = crypto
.createHash("sha256")
.update(rawData)
.digest("hex")
if (license_text_obj.license_checksum == hash) {
callback(meta, rawData)
} else {
throw Error(
`Checksum for ${meta.name} did not match file downloaded from ${license_text_obj.https_url}`
)
}
})
})
.on("error", (e) => {
throw e
})
}
});
}).on('error', (e) => {
throw e
});
}
}
}
function writeLicense(schemeMeta: Meta, text: String) {
process.stdout.write(`## [${schemeMeta.name}](${schemeMeta.url})\n\n${text}\n********************************************************************************\n\n`)
process.stdout.write(
`## [${schemeMeta.name}](${schemeMeta.url})\n\n${text}\n********************************************************************************\n\n`
)
}
const accepted_licenses = parseAcceptedToml(accepted_licenses_file);
const accepted_licenses = parseAcceptedToml(accepted_licenses_file)
checkLicenses(schemeMeta, accepted_licenses)
getLicenseText(schemeMeta, (meta, text) => {
writeLicense(meta, text)
});
writeLicense(meta, text)
})

View File

@@ -1,50 +1,52 @@
import * as fs from "fs";
import { tmpdir } from "os";
import * as path from "path";
import colorSchemes, {
staffColorSchemes,
} from "./colorSchemes";
import app from "./styleTree/app";
import { ColorScheme } from "./themes/common/colorScheme";
import snakeCase from "./utils/snakeCase";
import * as fs from "fs"
import { tmpdir } from "os"
import * as path from "path"
import colorSchemes, { staffColorSchemes } from "./colorSchemes"
import app from "./styleTree/app"
import { ColorScheme } from "./themes/common/colorScheme"
import snakeCase from "./utils/snakeCase"
const assetsDirectory = `${__dirname}/../../assets`
const themeDirectory = `${assetsDirectory}/themes`;
const staffDirectory = `${themeDirectory}/staff`;
const themeDirectory = `${assetsDirectory}/themes`
const staffDirectory = `${themeDirectory}/staff`
const tempDirectory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"));
const tempDirectory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"))
// Clear existing themes
function clearThemes(themeDirectory: string) {
if (!fs.existsSync(themeDirectory)) {
fs.mkdirSync(themeDirectory, { recursive: true });
} else {
for (const file of fs.readdirSync(themeDirectory)) {
if (file.endsWith(".json")) {
const name = file.replace(/\.json$/, "");
if (!colorSchemes.find((colorScheme) => colorScheme.name === name)) {
fs.unlinkSync(path.join(themeDirectory, file));
if (!fs.existsSync(themeDirectory)) {
fs.mkdirSync(themeDirectory, { recursive: true })
} else {
for (const file of fs.readdirSync(themeDirectory)) {
if (file.endsWith(".json")) {
const name = file.replace(/\.json$/, "")
if (
!colorSchemes.find(
(colorScheme) => colorScheme.name === name
)
) {
fs.unlinkSync(path.join(themeDirectory, file))
}
}
}
}
}
}
}
clearThemes(themeDirectory);
clearThemes(staffDirectory);
clearThemes(themeDirectory)
clearThemes(staffDirectory)
function writeThemes(colorSchemes: ColorScheme[], outputDirectory: string) {
for (let colorScheme of colorSchemes) {
let styleTree = snakeCase(app(colorScheme));
let styleTreeJSON = JSON.stringify(styleTree, null, 2);
let tempPath = path.join(tempDirectory, `${colorScheme.name}.json`);
let outPath = path.join(outputDirectory, `${colorScheme.name}.json`);
fs.writeFileSync(tempPath, styleTreeJSON);
fs.renameSync(tempPath, outPath);
console.log(`- ${outPath} created`);
}
for (let colorScheme of colorSchemes) {
let styleTree = snakeCase(app(colorScheme))
let styleTreeJSON = JSON.stringify(styleTree, null, 2)
let tempPath = path.join(tempDirectory, `${colorScheme.name}.json`)
let outPath = path.join(outputDirectory, `${colorScheme.name}.json`)
fs.writeFileSync(tempPath, styleTreeJSON)
fs.renameSync(tempPath, outPath)
console.log(`- ${outPath} created`)
}
}
// Write new themes to theme directory
writeThemes(colorSchemes, themeDirectory);
writeThemes(staffColorSchemes, staffDirectory);
writeThemes(colorSchemes, themeDirectory)
writeThemes(staffColorSchemes, staffDirectory)

View File

@@ -1,54 +1,54 @@
import fs from "fs";
import path from "path";
import { ColorScheme, Meta } from "./themes/common/colorScheme";
import fs from "fs"
import path from "path"
import { ColorScheme, Meta } from "./themes/common/colorScheme"
const colorSchemes: ColorScheme[] = [];
export default colorSchemes;
const colorSchemes: ColorScheme[] = []
export default colorSchemes
const schemeMeta: Meta[] = [];
export { schemeMeta };
const schemeMeta: Meta[] = []
export { schemeMeta }
const staffColorSchemes: ColorScheme[] = [];
export { staffColorSchemes };
const staffColorSchemes: ColorScheme[] = []
export { staffColorSchemes }
const experimentalColorSchemes: ColorScheme[] = [];
export { experimentalColorSchemes };
const experimentalColorSchemes: ColorScheme[] = []
export { experimentalColorSchemes }
const themes_directory = path.resolve(`${__dirname}/themes`);
const themes_directory = path.resolve(`${__dirname}/themes`)
function for_all_color_schemes_in(themesPath: string, callback: (module: any, path: string) => void) {
for (const fileName of fs.readdirSync(themesPath)) {
if (fileName == "template.ts") continue;
const filePath = path.join(themesPath, fileName);
function for_all_color_schemes_in(
themesPath: string,
callback: (module: any, path: string) => void
) {
for (const fileName of fs.readdirSync(themesPath)) {
if (fileName == "template.ts") continue
const filePath = path.join(themesPath, fileName)
if (fs.statSync(filePath).isFile()) {
const colorScheme = require(filePath);
callback(colorScheme, path.basename(filePath));
if (fs.statSync(filePath).isFile()) {
const colorScheme = require(filePath)
callback(colorScheme, path.basename(filePath))
}
}
}
}
function fillColorSchemes(themesPath: string, colorSchemes: ColorScheme[]) {
for_all_color_schemes_in(themesPath, (colorScheme, _path) => {
if (colorScheme.dark) colorSchemes.push(colorScheme.dark);
if (colorScheme.light) colorSchemes.push(colorScheme.light);
})
for_all_color_schemes_in(themesPath, (colorScheme, _path) => {
if (colorScheme.dark) colorSchemes.push(colorScheme.dark)
if (colorScheme.light) colorSchemes.push(colorScheme.light)
})
}
fillColorSchemes(themes_directory, colorSchemes);
fillColorSchemes(
path.resolve(`${themes_directory}/staff`),
staffColorSchemes
);
fillColorSchemes(themes_directory, colorSchemes)
fillColorSchemes(path.resolve(`${themes_directory}/staff`), staffColorSchemes)
function fillMeta(themesPath: string, meta: Meta[]) {
for_all_color_schemes_in(themesPath, (colorScheme, path) => {
if (colorScheme.meta) {
meta.push(colorScheme.meta)
} else {
throw Error(`Public theme ${path} must have a meta field`)
}
})
for_all_color_schemes_in(themesPath, (colorScheme, path) => {
if (colorScheme.meta) {
meta.push(colorScheme.meta)
} else {
throw Error(`Public theme ${path} must have a meta field`)
}
})
}
fillMeta(themes_directory, schemeMeta);
fillMeta(themes_directory, schemeMeta)

View File

@@ -1,66 +1,45 @@
export const fontFamilies = {
sans: "Zed Sans",
mono: "Zed Mono",
};
sans: "Zed Sans",
mono: "Zed Mono",
}
export const fontSizes = {
"3xs": 8,
"2xs": 10,
xs: 12,
sm: 14,
md: 16,
lg: 18,
xl: 20,
};
"3xs": 8,
"2xs": 10,
xs: 12,
sm: 14,
md: 16,
lg: 18,
xl: 20,
}
export type FontWeight =
| "thin"
| "extra_light"
| "light"
| "normal"
| "medium"
| "semibold"
| "bold"
| "extra_bold"
| "black";
| "thin"
| "extra_light"
| "light"
| "normal"
| "medium"
| "semibold"
| "bold"
| "extra_bold"
| "black"
export const fontWeights: { [key: string]: FontWeight } = {
thin: "thin",
extra_light: "extra_light",
light: "light",
normal: "normal",
medium: "medium",
semibold: "semibold",
bold: "bold",
extra_bold: "extra_bold",
black: "black",
};
thin: "thin",
extra_light: "extra_light",
light: "light",
normal: "normal",
medium: "medium",
semibold: "semibold",
bold: "bold",
extra_bold: "extra_bold",
black: "black",
}
export const sizes = {
px: 1,
xs: 2,
sm: 4,
md: 6,
lg: 8,
xl: 12,
};
// export const colors = {
// neutral: colorRamp(["white", "black"], { steps: 37, increment: 25 }), // (900/25) + 1
// rose: colorRamp("#F43F5EFF"),
// red: colorRamp("#EF4444FF"),
// orange: colorRamp("#F97316FF"),
// amber: colorRamp("#F59E0BFF"),
// yellow: colorRamp("#EAB308FF"),
// lime: colorRamp("#84CC16FF"),
// green: colorRamp("#22C55EFF"),
// emerald: colorRamp("#10B981FF"),
// teal: colorRamp("#14B8A6FF"),
// cyan: colorRamp("#06BBD4FF"),
// sky: colorRamp("#0EA5E9FF"),
// blue: colorRamp("#3B82F6FF"),
// indigo: colorRamp("#6366F1FF"),
// violet: colorRamp("#8B5CF6FF"),
// purple: colorRamp("#A855F7FF"),
// fuschia: colorRamp("#D946E4FF"),
// pink: colorRamp("#EC4899FF"),
// }
px: 1,
xs: 2,
sm: 4,
md: 6,
lg: 8,
xl: 12,
}

View File

@@ -1,72 +1,72 @@
import { text } from "./components";
import contactFinder from "./contactFinder";
import contactsPopover from "./contactsPopover";
import commandPalette from "./commandPalette";
import editor from "./editor";
import projectPanel from "./projectPanel";
import search from "./search";
import picker from "./picker";
import workspace from "./workspace";
import contextMenu from "./contextMenu";
import sharedScreen from "./sharedScreen";
import projectDiagnostics from "./projectDiagnostics";
import contactNotification from "./contactNotification";
import updateNotification from "./updateNotification";
import simpleMessageNotification from "./simpleMessageNotification";
import projectSharedNotification from "./projectSharedNotification";
import tooltip from "./tooltip";
import terminal from "./terminal";
import contactList from "./contactList";
import incomingCallNotification from "./incomingCallNotification";
import { ColorScheme } from "../themes/common/colorScheme";
import feedback from "./feedback";
import { text } from "./components"
import contactFinder from "./contactFinder"
import contactsPopover from "./contactsPopover"
import commandPalette from "./commandPalette"
import editor from "./editor"
import projectPanel from "./projectPanel"
import search from "./search"
import picker from "./picker"
import workspace from "./workspace"
import contextMenu from "./contextMenu"
import sharedScreen from "./sharedScreen"
import projectDiagnostics from "./projectDiagnostics"
import contactNotification from "./contactNotification"
import updateNotification from "./updateNotification"
import simpleMessageNotification from "./simpleMessageNotification"
import projectSharedNotification from "./projectSharedNotification"
import tooltip from "./tooltip"
import terminal from "./terminal"
import contactList from "./contactList"
import incomingCallNotification from "./incomingCallNotification"
import { ColorScheme } from "../themes/common/colorScheme"
import feedback from "./feedback"
export default function app(colorScheme: ColorScheme): Object {
return {
meta: {
name: colorScheme.name,
isLight: colorScheme.isLight,
},
commandPalette: commandPalette(colorScheme),
contactNotification: contactNotification(colorScheme),
projectSharedNotification: projectSharedNotification(colorScheme),
incomingCallNotification: incomingCallNotification(colorScheme),
picker: picker(colorScheme),
workspace: workspace(colorScheme),
contextMenu: contextMenu(colorScheme),
editor: editor(colorScheme),
projectDiagnostics: projectDiagnostics(colorScheme),
projectPanel: projectPanel(colorScheme),
contactsPopover: contactsPopover(colorScheme),
contactFinder: contactFinder(colorScheme),
contactList: contactList(colorScheme),
search: search(colorScheme),
sharedScreen: sharedScreen(colorScheme),
breadcrumbs: {
...text(colorScheme.highest, "sans", "variant"),
padding: {
left: 6,
},
},
updateNotification: updateNotification(colorScheme),
simpleMessageNotification: simpleMessageNotification(colorScheme),
tooltip: tooltip(colorScheme),
terminal: terminal(colorScheme),
feedback: feedback(colorScheme),
colorScheme: {
...colorScheme,
players: Object.values(colorScheme.players),
ramps: {
neutral: colorScheme.ramps.neutral.colors(100, "hex"),
red: colorScheme.ramps.red.colors(100, "hex"),
orange: colorScheme.ramps.orange.colors(100, "hex"),
yellow: colorScheme.ramps.yellow.colors(100, "hex"),
green: colorScheme.ramps.green.colors(100, "hex"),
cyan: colorScheme.ramps.cyan.colors(100, "hex"),
blue: colorScheme.ramps.blue.colors(100, "hex"),
violet: colorScheme.ramps.violet.colors(100, "hex"),
magenta: colorScheme.ramps.magenta.colors(100, "hex"),
},
},
};
return {
meta: {
name: colorScheme.name,
isLight: colorScheme.isLight,
},
commandPalette: commandPalette(colorScheme),
contactNotification: contactNotification(colorScheme),
projectSharedNotification: projectSharedNotification(colorScheme),
incomingCallNotification: incomingCallNotification(colorScheme),
picker: picker(colorScheme),
workspace: workspace(colorScheme),
contextMenu: contextMenu(colorScheme),
editor: editor(colorScheme),
projectDiagnostics: projectDiagnostics(colorScheme),
projectPanel: projectPanel(colorScheme),
contactsPopover: contactsPopover(colorScheme),
contactFinder: contactFinder(colorScheme),
contactList: contactList(colorScheme),
search: search(colorScheme),
sharedScreen: sharedScreen(colorScheme),
breadcrumbs: {
...text(colorScheme.highest, "sans", "variant"),
padding: {
left: 6,
},
},
updateNotification: updateNotification(colorScheme),
simpleMessageNotification: simpleMessageNotification(colorScheme),
tooltip: tooltip(colorScheme),
terminal: terminal(colorScheme),
feedback: feedback(colorScheme),
colorScheme: {
...colorScheme,
players: Object.values(colorScheme.players),
ramps: {
neutral: colorScheme.ramps.neutral.colors(100, "hex"),
red: colorScheme.ramps.red.colors(100, "hex"),
orange: colorScheme.ramps.orange.colors(100, "hex"),
yellow: colorScheme.ramps.yellow.colors(100, "hex"),
green: colorScheme.ramps.green.colors(100, "hex"),
cyan: colorScheme.ramps.cyan.colors(100, "hex"),
blue: colorScheme.ramps.blue.colors(100, "hex"),
violet: colorScheme.ramps.violet.colors(100, "hex"),
magenta: colorScheme.ramps.magenta.colors(100, "hex"),
},
},
}
}

View File

@@ -1,30 +1,30 @@
import { ColorScheme } from "../themes/common/colorScheme";
import { withOpacity } from "../utils/color";
import { text, background } from "./components";
import { ColorScheme } from "../themes/common/colorScheme"
import { withOpacity } from "../utils/color"
import { text, background } from "./components"
export default function commandPalette(colorScheme: ColorScheme) {
let layer = colorScheme.highest;
return {
keystrokeSpacing: 8,
key: {
text: text(layer, "mono", "variant", "default", { size: "xs" }),
cornerRadius: 2,
background: background(layer, "on"),
padding: {
top: 1,
bottom: 1,
left: 6,
right: 6,
},
margin: {
top: 1,
bottom: 1,
left: 2,
},
active: {
text: text(layer, "mono", "on", "default", { size: "xs" }),
background: withOpacity(background(layer, "on"), 0.2),
},
},
};
let layer = colorScheme.highest
return {
keystrokeSpacing: 8,
key: {
text: text(layer, "mono", "variant", "default", { size: "xs" }),
cornerRadius: 2,
background: background(layer, "on"),
padding: {
top: 1,
bottom: 1,
left: 6,
right: 6,
},
margin: {
top: 1,
bottom: 1,
left: 2,
},
active: {
text: text(layer, "mono", "on", "default", { size: "xs" }),
background: withOpacity(background(layer, "on"), 0.2),
},
},
}
}

View File

@@ -1,210 +1,210 @@
import { fontFamilies, fontSizes, FontWeight } from "../common";
import { Layer, Styles, StyleSets, Style } from "../themes/common/colorScheme";
import { fontFamilies, fontSizes, FontWeight } from "../common"
import { Layer, Styles, StyleSets, Style } from "../themes/common/colorScheme"
function isStyleSet(key: any): key is StyleSets {
return [
"base",
"variant",
"on",
"accent",
"positive",
"warning",
"negative",
].includes(key);
return [
"base",
"variant",
"on",
"accent",
"positive",
"warning",
"negative",
].includes(key)
}
function isStyle(key: any): key is Styles {
return [
"default",
"active",
"disabled",
"hovered",
"pressed",
"inverted",
].includes(key);
return [
"default",
"active",
"disabled",
"hovered",
"pressed",
"inverted",
].includes(key)
}
function getStyle(
layer: Layer,
possibleStyleSetOrStyle?: any,
possibleStyle?: any
layer: Layer,
possibleStyleSetOrStyle?: any,
possibleStyle?: any
): Style {
let styleSet: StyleSets = "base";
let style: Styles = "default";
if (isStyleSet(possibleStyleSetOrStyle)) {
styleSet = possibleStyleSetOrStyle;
} else if (isStyle(possibleStyleSetOrStyle)) {
style = possibleStyleSetOrStyle;
}
let styleSet: StyleSets = "base"
let style: Styles = "default"
if (isStyleSet(possibleStyleSetOrStyle)) {
styleSet = possibleStyleSetOrStyle
} else if (isStyle(possibleStyleSetOrStyle)) {
style = possibleStyleSetOrStyle
}
if (isStyle(possibleStyle)) {
style = possibleStyle;
}
if (isStyle(possibleStyle)) {
style = possibleStyle
}
return layer[styleSet][style];
return layer[styleSet][style]
}
export function background(layer: Layer, style?: Styles): string;
export function background(layer: Layer, style?: Styles): string
export function background(
layer: Layer,
styleSet?: StyleSets,
style?: Styles
): string;
layer: Layer,
styleSet?: StyleSets,
style?: Styles
): string
export function background(
layer: Layer,
styleSetOrStyles?: StyleSets | Styles,
style?: Styles
layer: Layer,
styleSetOrStyles?: StyleSets | Styles,
style?: Styles
): string {
return getStyle(layer, styleSetOrStyles, style).background;
return getStyle(layer, styleSetOrStyles, style).background
}
export function borderColor(layer: Layer, style?: Styles): string;
export function borderColor(layer: Layer, style?: Styles): string
export function borderColor(
layer: Layer,
styleSet?: StyleSets,
style?: Styles
): string;
layer: Layer,
styleSet?: StyleSets,
style?: Styles
): string
export function borderColor(
layer: Layer,
styleSetOrStyles?: StyleSets | Styles,
style?: Styles
layer: Layer,
styleSetOrStyles?: StyleSets | Styles,
style?: Styles
): string {
return getStyle(layer, styleSetOrStyles, style).border;
return getStyle(layer, styleSetOrStyles, style).border
}
export function foreground(layer: Layer, style?: Styles): string;
export function foreground(layer: Layer, style?: Styles): string
export function foreground(
layer: Layer,
styleSet?: StyleSets,
style?: Styles
): string;
layer: Layer,
styleSet?: StyleSets,
style?: Styles
): string
export function foreground(
layer: Layer,
styleSetOrStyles?: StyleSets | Styles,
style?: Styles
layer: Layer,
styleSetOrStyles?: StyleSets | Styles,
style?: Styles
): string {
return getStyle(layer, styleSetOrStyles, style).foreground;
return getStyle(layer, styleSetOrStyles, style).foreground
}
interface Text {
family: keyof typeof fontFamilies;
color: string;
size: number;
weight?: FontWeight;
underline?: boolean;
family: keyof typeof fontFamilies
color: string
size: number
weight?: FontWeight
underline?: boolean
}
interface TextProperties {
size?: keyof typeof fontSizes;
weight?: FontWeight;
underline?: boolean;
color?: string;
size?: keyof typeof fontSizes
weight?: FontWeight
underline?: boolean
color?: string
}
export function text(
layer: Layer,
fontFamily: keyof typeof fontFamilies,
styleSet: StyleSets,
style: Styles,
properties?: TextProperties
): Text;
layer: Layer,
fontFamily: keyof typeof fontFamilies,
styleSet: StyleSets,
style: Styles,
properties?: TextProperties
): Text
export function text(
layer: Layer,
fontFamily: keyof typeof fontFamilies,
styleSet: StyleSets,
properties?: TextProperties
): Text;
layer: Layer,
fontFamily: keyof typeof fontFamilies,
styleSet: StyleSets,
properties?: TextProperties
): Text
export function text(
layer: Layer,
fontFamily: keyof typeof fontFamilies,
style: Styles,
properties?: TextProperties
): Text;
layer: Layer,
fontFamily: keyof typeof fontFamilies,
style: Styles,
properties?: TextProperties
): Text
export function text(
layer: Layer,
fontFamily: keyof typeof fontFamilies,
properties?: TextProperties
): Text;
layer: Layer,
fontFamily: keyof typeof fontFamilies,
properties?: TextProperties
): Text
export function text(
layer: Layer,
fontFamily: keyof typeof fontFamilies,
styleSetStyleOrProperties?: StyleSets | Styles | TextProperties,
styleOrProperties?: Styles | TextProperties,
properties?: TextProperties
layer: Layer,
fontFamily: keyof typeof fontFamilies,
styleSetStyleOrProperties?: StyleSets | Styles | TextProperties,
styleOrProperties?: Styles | TextProperties,
properties?: TextProperties
) {
let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties);
let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties)
if (typeof styleSetStyleOrProperties === "object") {
properties = styleSetStyleOrProperties;
}
if (typeof styleOrProperties === "object") {
properties = styleOrProperties;
}
if (typeof styleSetStyleOrProperties === "object") {
properties = styleSetStyleOrProperties
}
if (typeof styleOrProperties === "object") {
properties = styleOrProperties
}
let size = fontSizes[properties?.size || "sm"];
let color = properties?.color || style.foreground;
let size = fontSizes[properties?.size || "sm"]
let color = properties?.color || style.foreground
return {
family: fontFamilies[fontFamily],
...properties,
color,
size,
};
return {
family: fontFamilies[fontFamily],
...properties,
color,
size,
}
}
export interface Border {
color: string;
width: number;
top?: boolean;
bottom?: boolean;
left?: boolean;
right?: boolean;
overlay?: boolean;
color: string
width: number
top?: boolean
bottom?: boolean
left?: boolean
right?: boolean
overlay?: boolean
}
export interface BorderProperties {
width?: number;
top?: boolean;
bottom?: boolean;
left?: boolean;
right?: boolean;
overlay?: boolean;
width?: number
top?: boolean
bottom?: boolean
left?: boolean
right?: boolean
overlay?: boolean
}
export function border(
layer: Layer,
styleSet: StyleSets,
style: Styles,
properties?: BorderProperties
): Border;
layer: Layer,
styleSet: StyleSets,
style: Styles,
properties?: BorderProperties
): Border
export function border(
layer: Layer,
styleSet: StyleSets,
properties?: BorderProperties
): Border;
layer: Layer,
styleSet: StyleSets,
properties?: BorderProperties
): Border
export function border(
layer: Layer,
style: Styles,
properties?: BorderProperties
): Border;
export function border(layer: Layer, properties?: BorderProperties): Border;
layer: Layer,
style: Styles,
properties?: BorderProperties
): Border
export function border(layer: Layer, properties?: BorderProperties): Border
export function border(
layer: Layer,
styleSetStyleOrProperties?: StyleSets | Styles | BorderProperties,
styleOrProperties?: Styles | BorderProperties,
properties?: BorderProperties
layer: Layer,
styleSetStyleOrProperties?: StyleSets | Styles | BorderProperties,
styleOrProperties?: Styles | BorderProperties,
properties?: BorderProperties
): Border {
let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties);
let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties)
if (typeof styleSetStyleOrProperties === "object") {
properties = styleSetStyleOrProperties;
}
if (typeof styleOrProperties === "object") {
properties = styleOrProperties;
}
if (typeof styleSetStyleOrProperties === "object") {
properties = styleSetStyleOrProperties
}
if (typeof styleOrProperties === "object") {
properties = styleOrProperties
}
return {
color: style.border,
width: 1,
...properties,
};
return {
color: style.border,
width: 1,
...properties,
}
}

View File

@@ -1,70 +1,70 @@
import picker from "./picker";
import { ColorScheme } from "../themes/common/colorScheme";
import { background, border, foreground, text } from "./components";
import picker from "./picker"
import { ColorScheme } from "../themes/common/colorScheme"
import { background, border, foreground, text } from "./components"
export default function contactFinder(colorScheme: ColorScheme) {
let layer = colorScheme.middle;
let layer = colorScheme.middle
const sideMargin = 6;
const contactButton = {
background: background(layer, "variant"),
color: foreground(layer, "variant"),
iconWidth: 8,
buttonWidth: 16,
cornerRadius: 8,
};
const pickerStyle = picker(colorScheme);
const pickerInput = {
background: background(layer, "on"),
cornerRadius: 6,
text: text(layer, "mono",),
placeholderText: text(layer, "mono", "on", "disabled", { size: "xs" }),
selection: colorScheme.players[0],
border: border(layer),
padding: {
bottom: 4,
left: 8,
right: 8,
top: 4,
},
margin: {
left: sideMargin,
right: sideMargin,
const sideMargin = 6
const contactButton = {
background: background(layer, "variant"),
color: foreground(layer, "variant"),
iconWidth: 8,
buttonWidth: 16,
cornerRadius: 8,
}
};
return {
picker: {
emptyContainer: {},
item: {
...pickerStyle.item,
margin: { left: sideMargin, right: sideMargin },
},
noMatches: pickerStyle.noMatches,
inputEditor: pickerInput,
emptyInputEditor: pickerInput
},
rowHeight: 28,
contactAvatar: {
cornerRadius: 10,
width: 18,
},
contactUsername: {
padding: {
left: 8,
},
},
contactButton: {
...contactButton,
hover: {
background: background(layer, "variant", "hovered"),
},
},
disabledContactButton: {
...contactButton,
background: background(layer, "disabled"),
color: foreground(layer, "disabled"),
},
};
const pickerStyle = picker(colorScheme)
const pickerInput = {
background: background(layer, "on"),
cornerRadius: 6,
text: text(layer, "mono"),
placeholderText: text(layer, "mono", "on", "disabled", { size: "xs" }),
selection: colorScheme.players[0],
border: border(layer),
padding: {
bottom: 4,
left: 8,
right: 8,
top: 4,
},
margin: {
left: sideMargin,
right: sideMargin,
},
}
return {
picker: {
emptyContainer: {},
item: {
...pickerStyle.item,
margin: { left: sideMargin, right: sideMargin },
},
noMatches: pickerStyle.noMatches,
inputEditor: pickerInput,
emptyInputEditor: pickerInput,
},
rowHeight: 28,
contactAvatar: {
cornerRadius: 10,
width: 18,
},
contactUsername: {
padding: {
left: 8,
},
},
contactButton: {
...contactButton,
hover: {
background: background(layer, "variant", "hovered"),
},
},
disabledContactButton: {
...contactButton,
background: background(layer, "disabled"),
color: foreground(layer, "disabled"),
},
}
}

View File

@@ -1,186 +1,182 @@
import { ColorScheme } from "../themes/common/colorScheme";
import {
background,
border,
borderColor,
foreground,
text,
} from "./components";
import { ColorScheme } from "../themes/common/colorScheme"
import { background, border, borderColor, foreground, text } from "./components"
export default function contactsPanel(colorScheme: ColorScheme) {
const nameMargin = 8;
const sidePadding = 12;
const nameMargin = 8
const sidePadding = 12
let layer = colorScheme.middle;
let layer = colorScheme.middle
const contactButton = {
background: background(layer, "on"),
color: foreground(layer, "on"),
iconWidth: 8,
buttonWidth: 16,
cornerRadius: 8,
};
const projectRow = {
guestAvatarSpacing: 4,
height: 24,
guestAvatar: {
cornerRadius: 8,
width: 14,
},
name: {
...text(layer, "mono", { size: "sm" }),
margin: {
left: nameMargin,
right: 6,
},
},
guests: {
margin: {
left: nameMargin,
right: nameMargin,
},
},
padding: {
left: sidePadding,
right: sidePadding,
},
};
const contactButton = {
background: background(layer, "on"),
color: foreground(layer, "on"),
iconWidth: 8,
buttonWidth: 16,
cornerRadius: 8,
}
const projectRow = {
guestAvatarSpacing: 4,
height: 24,
guestAvatar: {
cornerRadius: 8,
width: 14,
},
name: {
...text(layer, "mono", { size: "sm" }),
margin: {
left: nameMargin,
right: 6,
},
},
guests: {
margin: {
left: nameMargin,
right: nameMargin,
},
},
padding: {
left: sidePadding,
right: sidePadding,
},
}
return {
background: background(layer),
padding: { top: 12, bottom: 0 },
userQueryEditor: {
background: background(layer, "on"),
cornerRadius: 6,
text: text(layer, "mono", "on"),
placeholderText: text(layer, "mono", "on", "disabled", { size: "xs" }),
selection: colorScheme.players[0],
border: border(layer, "on"),
padding: {
bottom: 4,
left: 8,
right: 8,
top: 4,
},
margin: {
left: 6,
},
},
userQueryEditorHeight: 33,
addContactButton: {
margin: { left: 6, right: 12 },
color: foreground(layer, "on"),
buttonWidth: 28,
iconWidth: 16,
},
rowHeight: 28,
sectionIconSize: 8,
headerRow: {
...text(layer, "mono", { size: "sm" }),
margin: { top: 14 },
padding: {
left: sidePadding,
right: sidePadding,
},
active: {
...text(layer, "mono", "active", { size: "sm" }),
background: background(layer, "active"),
},
},
leaveCall: {
background: background(layer),
border: border(layer),
cornerRadius: 6,
margin: {
top: 1,
},
padding: {
top: 1,
bottom: 1,
left: 7,
right: 7,
},
...text(layer, "sans", "variant", { size: "xs" }),
hover: {
...text(layer, "sans", "hovered", { size: "xs" }),
background: background(layer, "hovered"),
border: border(layer, "hovered"),
},
},
contactRow: {
padding: {
left: sidePadding,
right: sidePadding,
},
active: {
background: background(layer, "active"),
},
},
contactAvatar: {
cornerRadius: 10,
width: 18,
},
contactStatusFree: {
cornerRadius: 4,
padding: 4,
margin: { top: 12, left: 12 },
background: foreground(layer, "positive"),
},
contactStatusBusy: {
cornerRadius: 4,
padding: 4,
margin: { top: 12, left: 12 },
background: foreground(layer, "negative"),
},
contactUsername: {
...text(layer, "mono", { size: "sm" }),
margin: {
left: nameMargin,
},
},
contactButtonSpacing: nameMargin,
contactButton: {
...contactButton,
hover: {
background: background(layer, "hovered"),
},
},
disabledButton: {
...contactButton,
background: background(layer, "on"),
color: foreground(layer, "on"),
},
callingIndicator: {
...text(layer, "mono", "variant", { size: "xs" }),
},
treeBranch: {
color: borderColor(layer),
width: 1,
hover: {
color: borderColor(layer),
},
active: {
color: borderColor(layer),
},
},
projectRow: {
...projectRow,
background: background(layer),
icon: {
margin: { left: nameMargin },
color: foreground(layer, "variant"),
width: 12,
},
name: {
...projectRow.name,
...text(layer, "mono", { size: "sm" }),
},
hover: {
background: background(layer, "hovered"),
},
active: {
background: background(layer, "active"),
},
},
};
return {
background: background(layer),
padding: { top: 12, bottom: 0 },
userQueryEditor: {
background: background(layer, "on"),
cornerRadius: 6,
text: text(layer, "mono", "on"),
placeholderText: text(layer, "mono", "on", "disabled", {
size: "xs",
}),
selection: colorScheme.players[0],
border: border(layer, "on"),
padding: {
bottom: 4,
left: 8,
right: 8,
top: 4,
},
margin: {
left: 6,
},
},
userQueryEditorHeight: 33,
addContactButton: {
margin: { left: 6, right: 12 },
color: foreground(layer, "on"),
buttonWidth: 28,
iconWidth: 16,
},
rowHeight: 28,
sectionIconSize: 8,
headerRow: {
...text(layer, "mono", { size: "sm" }),
margin: { top: 14 },
padding: {
left: sidePadding,
right: sidePadding,
},
active: {
...text(layer, "mono", "active", { size: "sm" }),
background: background(layer, "active"),
},
},
leaveCall: {
background: background(layer),
border: border(layer),
cornerRadius: 6,
margin: {
top: 1,
},
padding: {
top: 1,
bottom: 1,
left: 7,
right: 7,
},
...text(layer, "sans", "variant", { size: "xs" }),
hover: {
...text(layer, "sans", "hovered", { size: "xs" }),
background: background(layer, "hovered"),
border: border(layer, "hovered"),
},
},
contactRow: {
padding: {
left: sidePadding,
right: sidePadding,
},
active: {
background: background(layer, "active"),
},
},
contactAvatar: {
cornerRadius: 10,
width: 18,
},
contactStatusFree: {
cornerRadius: 4,
padding: 4,
margin: { top: 12, left: 12 },
background: foreground(layer, "positive"),
},
contactStatusBusy: {
cornerRadius: 4,
padding: 4,
margin: { top: 12, left: 12 },
background: foreground(layer, "negative"),
},
contactUsername: {
...text(layer, "mono", { size: "sm" }),
margin: {
left: nameMargin,
},
},
contactButtonSpacing: nameMargin,
contactButton: {
...contactButton,
hover: {
background: background(layer, "hovered"),
},
},
disabledButton: {
...contactButton,
background: background(layer, "on"),
color: foreground(layer, "on"),
},
callingIndicator: {
...text(layer, "mono", "variant", { size: "xs" }),
},
treeBranch: {
color: borderColor(layer),
width: 1,
hover: {
color: borderColor(layer),
},
active: {
color: borderColor(layer),
},
},
projectRow: {
...projectRow,
background: background(layer),
icon: {
margin: { left: nameMargin },
color: foreground(layer, "variant"),
width: 12,
},
name: {
...projectRow.name,
...text(layer, "mono", { size: "sm" }),
},
hover: {
background: background(layer, "hovered"),
},
active: {
background: background(layer, "active"),
},
},
}
}

View File

@@ -1,45 +1,45 @@
import { ColorScheme } from "../themes/common/colorScheme";
import { background, foreground, text } from "./components";
import { ColorScheme } from "../themes/common/colorScheme"
import { background, foreground, text } from "./components"
const avatarSize = 12;
const headerPadding = 8;
const avatarSize = 12
const headerPadding = 8
export default function contactNotification(colorScheme: ColorScheme): Object {
let layer = colorScheme.lowest;
return {
headerAvatar: {
height: avatarSize,
width: avatarSize,
cornerRadius: 6,
},
headerMessage: {
...text(layer, "sans", { size: "xs" }),
margin: { left: headerPadding, right: headerPadding },
},
headerHeight: 18,
bodyMessage: {
...text(layer, "sans", { size: "xs" }),
margin: { left: avatarSize + headerPadding, top: 6, bottom: 6 },
},
button: {
...text(layer, "sans", "on", { size: "xs" }),
background: background(layer, "on"),
padding: 4,
cornerRadius: 6,
margin: { left: 6 },
hover: {
background: background(layer, "on", "hovered"),
},
},
dismissButton: {
color: foreground(layer, "variant"),
iconWidth: 8,
iconHeight: 8,
buttonWidth: 8,
buttonHeight: 8,
hover: {
color: foreground(layer, "hovered"),
},
},
};
let layer = colorScheme.lowest
return {
headerAvatar: {
height: avatarSize,
width: avatarSize,
cornerRadius: 6,
},
headerMessage: {
...text(layer, "sans", { size: "xs" }),
margin: { left: headerPadding, right: headerPadding },
},
headerHeight: 18,
bodyMessage: {
...text(layer, "sans", { size: "xs" }),
margin: { left: avatarSize + headerPadding, top: 6, bottom: 6 },
},
button: {
...text(layer, "sans", "on", { size: "xs" }),
background: background(layer, "on"),
padding: 4,
cornerRadius: 6,
margin: { left: 6 },
hover: {
background: background(layer, "on", "hovered"),
},
},
dismissButton: {
color: foreground(layer, "variant"),
iconWidth: 8,
iconHeight: 8,
buttonWidth: 8,
buttonHeight: 8,
hover: {
color: foreground(layer, "hovered"),
},
},
}
}

View File

@@ -1,29 +1,29 @@
import { ColorScheme } from "../themes/common/colorScheme";
import { background, border, text } from "./components";
import { ColorScheme } from "../themes/common/colorScheme"
import { background, border, text } from "./components"
export default function contactsPopover(colorScheme: ColorScheme) {
let layer = colorScheme.middle;
const sidePadding = 12;
return {
background: background(layer),
cornerRadius: 6,
padding: { top: 6 },
margin: { top: -6 },
shadow: colorScheme.popoverShadow,
border: border(layer),
width: 300,
height: 400,
inviteRowHeight: 28,
inviteRow: {
padding: {
left: sidePadding,
right: sidePadding,
},
border: border(layer, { top: true }),
text: text(layer, "sans", "variant", { size: "sm" }),
hover: {
text: text(layer, "sans", "hovered", { size: "sm" }),
},
},
}
let layer = colorScheme.middle
const sidePadding = 12
return {
background: background(layer),
cornerRadius: 6,
padding: { top: 6 },
margin: { top: -6 },
shadow: colorScheme.popoverShadow,
border: border(layer),
width: 300,
height: 400,
inviteRowHeight: 28,
inviteRow: {
padding: {
left: sidePadding,
right: sidePadding,
},
border: border(layer, { top: true }),
text: text(layer, "sans", "variant", { size: "sm" }),
hover: {
text: text(layer, "sans", "hovered", { size: "sm" }),
},
},
}
}

View File

@@ -1,41 +1,44 @@
import { ColorScheme } from "../themes/common/colorScheme";
import { background, border, borderColor, text } from "./components";
import { ColorScheme } from "../themes/common/colorScheme"
import { background, border, borderColor, text } from "./components"
export default function contextMenu(colorScheme: ColorScheme) {
let layer = colorScheme.middle;
return {
background: background(layer),
cornerRadius: 10,
padding: 4,
shadow: colorScheme.popoverShadow,
border: border(layer),
keystrokeMargin: 30,
item: {
iconSpacing: 8,
iconWidth: 14,
padding: { left: 6, right: 6, top: 2, bottom: 2 },
cornerRadius: 6,
label: text(layer, "sans", { size: "sm" }),
keystroke: {
...text(layer, "sans", "variant", { size: "sm", weight: "bold" }),
padding: { left: 3, right: 3 },
},
hover: {
background: background(layer, "hovered"),
label: text(layer, "sans", "hovered", { size: "sm" }),
},
active: {
background: background(layer, "active"),
label: text(layer, "sans", "active", { size: "sm" }),
},
activeHover: {
background: background(layer, "active"),
label: text(layer, "sans", "active", { size: "sm" }),
},
},
separator: {
background: borderColor(layer),
margin: { top: 2, bottom: 2 },
},
};
let layer = colorScheme.middle
return {
background: background(layer),
cornerRadius: 10,
padding: 4,
shadow: colorScheme.popoverShadow,
border: border(layer),
keystrokeMargin: 30,
item: {
iconSpacing: 8,
iconWidth: 14,
padding: { left: 6, right: 6, top: 2, bottom: 2 },
cornerRadius: 6,
label: text(layer, "sans", { size: "sm" }),
keystroke: {
...text(layer, "sans", "variant", {
size: "sm",
weight: "bold",
}),
padding: { left: 3, right: 3 },
},
hover: {
background: background(layer, "hovered"),
label: text(layer, "sans", "hovered", { size: "sm" }),
},
active: {
background: background(layer, "active"),
label: text(layer, "sans", "active", { size: "sm" }),
},
activeHover: {
background: background(layer, "active"),
label: text(layer, "sans", "active", { size: "sm" }),
},
},
separator: {
background: borderColor(layer),
margin: { top: 2, bottom: 2 },
},
}
}

View File

@@ -1,285 +1,246 @@
import { fontWeights } from "../common";
import { withOpacity } from "../utils/color";
import { ColorScheme, Layer, StyleSets } from "../themes/common/colorScheme";
import {
background,
border,
borderColor,
foreground,
text,
} from "./components";
import hoverPopover from "./hoverPopover";
import { withOpacity } from "../utils/color"
import { ColorScheme, Layer, StyleSets } from "../themes/common/colorScheme"
import { background, border, borderColor, foreground, text } from "./components"
import hoverPopover from "./hoverPopover"
import { buildSyntax } from "../themes/common/syntax"
export default function editor(colorScheme: ColorScheme) {
let layer = colorScheme.highest;
let layer = colorScheme.highest
const autocompleteItem = {
cornerRadius: 6,
padding: {
bottom: 2,
left: 6,
right: 6,
top: 2,
},
};
const autocompleteItem = {
cornerRadius: 6,
padding: {
bottom: 2,
left: 6,
right: 6,
top: 2,
},
}
function diagnostic(layer: Layer, styleSet: StyleSets) {
return {
textScaleFactor: 0.857,
header: {
border: border(layer, {
top: true,
}),
},
message: {
text: text(layer, "sans", styleSet, "default", { size: "sm" }),
highlightText: text(layer, "sans", styleSet, "default", {
size: "sm",
weight: "bold",
}),
},
}
}
const syntax = buildSyntax(colorScheme)
function diagnostic(layer: Layer, styleSet: StyleSets) {
return {
textScaleFactor: 0.857,
header: {
border: border(layer, {
top: true,
}),
},
message: {
text: text(layer, "sans", styleSet, "default", { size: "sm" }),
highlightText: text(layer, "sans", styleSet, "default", {
size: "sm",
weight: "bold",
}),
},
};
}
textColor: syntax.primary.color,
background: background(layer),
activeLineBackground: withOpacity(background(layer, "on"), 0.75),
highlightedLineBackground: background(layer, "on"),
codeActions: {
indicator: {
color: foreground(layer, "variant"),
const syntax = {
primary: {
color: colorScheme.ramps.neutral(1).hex(),
weight: fontWeights.normal,
},
"variable.special": {
// Highlights for self, this, etc
color: colorScheme.ramps.blue(0.7).hex(),
weight: fontWeights.normal,
},
comment: {
color: colorScheme.ramps.neutral(0.71).hex(),
weight: fontWeights.normal,
},
punctuation: {
color: colorScheme.ramps.neutral(0.86).hex(),
weight: fontWeights.normal,
},
constant: {
color: colorScheme.ramps.green(0.5).hex(),
weight: fontWeights.normal,
},
keyword: {
color: colorScheme.ramps.blue(0.5).hex(),
weight: fontWeights.normal,
},
function: {
color: colorScheme.ramps.yellow(0.5).hex(),
weight: fontWeights.normal,
},
type: {
color: colorScheme.ramps.cyan(0.5).hex(),
weight: fontWeights.normal,
},
constructor: {
color: colorScheme.ramps.blue(0.5).hex(),
weight: fontWeights.normal,
},
variant: {
color: colorScheme.ramps.blue(0.5).hex(),
weight: fontWeights.normal,
},
property: {
color: colorScheme.ramps.blue(0.5).hex(),
weight: fontWeights.normal,
},
enum: {
color: colorScheme.ramps.orange(0.5).hex(),
weight: fontWeights.normal,
},
operator: {
color: colorScheme.ramps.orange(0.5).hex(),
weight: fontWeights.normal,
},
string: {
color: colorScheme.ramps.orange(0.5).hex(),
weight: fontWeights.normal,
},
number: {
color: colorScheme.ramps.green(0.5).hex(),
weight: fontWeights.normal,
},
boolean: {
color: colorScheme.ramps.green(0.5).hex(),
weight: fontWeights.normal,
},
predictive: {
color: colorScheme.ramps.neutral(0.57).hex(),
weight: fontWeights.normal,
},
title: {
color: colorScheme.ramps.yellow(0.5).hex(),
weight: fontWeights.bold,
},
emphasis: {
color: colorScheme.ramps.blue(0.5).hex(),
weight: fontWeights.normal,
},
"emphasis.strong": {
color: colorScheme.ramps.blue(0.5).hex(),
weight: fontWeights.bold,
},
linkUri: {
color: colorScheme.ramps.green(0.5).hex(),
weight: fontWeights.normal,
underline: true,
},
linkText: {
color: colorScheme.ramps.orange(0.5).hex(),
weight: fontWeights.normal,
italic: true,
},
};
clicked: {
color: foreground(layer, "base"),
},
hover: {
color: foreground(layer, "on"),
},
active: {
color: foreground(layer, "on"),
},
},
verticalScale: 0.55,
},
folds: {
iconWidth: 8,
foldedIcon: "icons/chevron_right_8.svg",
foldableIcon: "icons/chevron_down_8.svg",
indicator: {
color: foreground(layer, "variant"),
return {
textColor: syntax.primary.color,
background: background(layer),
activeLineBackground: withOpacity(background(layer, "on"), 0.75),
highlightedLineBackground: background(layer, "on"),
codeActions: {
indicator: foreground(layer, "variant"),
verticalScale: 0.55,
},
diff: {
deleted: foreground(layer, "negative"),
modified: foreground(layer, "warning"),
inserted: foreground(layer, "positive"),
removedWidthEm: 0.275,
widthEm: 0.16,
cornerRadius: 0.05,
},
/** Highlights matching occurences of what is under the cursor
* as well as matched brackets
*/
documentHighlightReadBackground: withOpacity(foreground(layer, "accent"), 0.1),
documentHighlightWriteBackground: colorScheme.ramps
.neutral(0.5)
.alpha(0.4)
.hex(), // TODO: This was blend * 2
errorColor: background(layer, "negative"),
gutterBackground: background(layer),
gutterPaddingFactor: 3.5,
lineNumber: withOpacity(foreground(layer), 0.35),
lineNumberActive: foreground(layer),
renameFade: 0.6,
unnecessaryCodeFade: 0.5,
selection: colorScheme.players[0],
guestSelections: [
colorScheme.players[1],
colorScheme.players[2],
colorScheme.players[3],
colorScheme.players[4],
colorScheme.players[5],
colorScheme.players[6],
colorScheme.players[7],
],
autocomplete: {
background: background(colorScheme.middle),
cornerRadius: 8,
padding: 4,
margin: {
left: -14,
},
border: border(colorScheme.middle),
shadow: colorScheme.popoverShadow,
matchHighlight: foreground(colorScheme.middle, "accent"),
item: autocompleteItem,
hoveredItem: {
...autocompleteItem,
matchHighlight: foreground(colorScheme.middle, "accent", "hovered"),
background: background(colorScheme.middle, "hovered"),
},
selectedItem: {
...autocompleteItem,
matchHighlight: foreground(colorScheme.middle, "accent", "active"),
background: background(colorScheme.middle, "active"),
},
},
diagnosticHeader: {
background: background(colorScheme.middle),
iconWidthFactor: 1.5,
textScaleFactor: 0.857,
border: border(colorScheme.middle, {
bottom: true,
top: true,
}),
code: {
...text(colorScheme.middle, "mono", { size: "sm" }),
margin: {
left: 10,
clicked: {
color: foreground(layer, "base"),
},
hover: {
color: foreground(layer, "on"),
},
active: {
color: foreground(layer, "on"),
},
},
ellipses: {
textColor: colorScheme.ramps.neutral(0.71).hex(),
cornerRadiusFactor: 0.15,
background: {
// Copied from hover_popover highlight
color: colorScheme.ramps.neutral(0.5).alpha(0.0).hex(),
hover: {
color: colorScheme.ramps.neutral(0.5).alpha(0.5).hex(),
},
clicked: {
color: colorScheme.ramps.neutral(0.5).alpha(0.7).hex(),
},
}
},
foldBackground: foreground(layer, "variant"),
},
},
message: {
highlightText: text(colorScheme.middle, "sans", {
size: "sm",
weight: "bold",
}),
text: text(colorScheme.middle, "sans", { size: "sm" }),
},
},
diagnosticPathHeader: {
background: background(colorScheme.middle),
textScaleFactor: 0.857,
filename: text(colorScheme.middle, "mono", { size: "sm" }),
path: {
...text(colorScheme.middle, "mono", { size: "sm" }),
margin: {
left: 12,
diff: {
deleted: foreground(layer, "negative"),
modified: foreground(layer, "warning"),
inserted: foreground(layer, "positive"),
removedWidthEm: 0.275,
widthEm: 0.16,
cornerRadius: 0.05,
},
},
},
errorDiagnostic: diagnostic(colorScheme.middle, "negative"),
warningDiagnostic: diagnostic(colorScheme.middle, "warning"),
informationDiagnostic: diagnostic(colorScheme.middle, "accent"),
hintDiagnostic: diagnostic(colorScheme.middle, "warning"),
invalidErrorDiagnostic: diagnostic(colorScheme.middle, "base"),
invalidHintDiagnostic: diagnostic(colorScheme.middle, "base"),
invalidInformationDiagnostic: diagnostic(colorScheme.middle, "base"),
invalidWarningDiagnostic: diagnostic(colorScheme.middle, "base"),
hoverPopover: hoverPopover(colorScheme),
linkDefinition: {
color: syntax.linkUri.color,
underline: syntax.linkUri.underline,
},
jumpIcon: {
color: foreground(layer, "on"),
iconWidth: 20,
buttonWidth: 20,
cornerRadius: 6,
padding: {
top: 6,
bottom: 6,
left: 6,
right: 6,
},
hover: {
background: background(layer, "on", "hovered"),
},
},
scrollbar: {
width: 12,
minHeightFactor: 1.0,
track: {
border: border(layer, "variant", { left: true }),
},
thumb: {
background: withOpacity(background(layer, "inverted"), 0.4),
border: {
width: 1,
color: borderColor(layer, "variant"),
/** Highlights matching occurences of what is under the cursor
* as well as matched brackets
*/
documentHighlightReadBackground: withOpacity(
foreground(layer, "accent"),
0.1
),
documentHighlightWriteBackground: colorScheme.ramps
.neutral(0.5)
.alpha(0.4)
.hex(), // TODO: This was blend * 2
errorColor: background(layer, "negative"),
gutterBackground: background(layer),
gutterPaddingFactor: 3.5,
lineNumber: withOpacity(foreground(layer), 0.35),
lineNumberActive: foreground(layer),
renameFade: 0.6,
unnecessaryCodeFade: 0.5,
selection: colorScheme.players[0],
guestSelections: [
colorScheme.players[1],
colorScheme.players[2],
colorScheme.players[3],
colorScheme.players[4],
colorScheme.players[5],
colorScheme.players[6],
colorScheme.players[7],
],
autocomplete: {
background: background(colorScheme.middle),
cornerRadius: 8,
padding: 4,
margin: {
left: -14,
},
border: border(colorScheme.middle),
shadow: colorScheme.popoverShadow,
matchHighlight: foreground(colorScheme.middle, "accent"),
item: autocompleteItem,
hoveredItem: {
...autocompleteItem,
matchHighlight: foreground(
colorScheme.middle,
"accent",
"hovered"
),
background: background(colorScheme.middle, "hovered"),
},
selectedItem: {
...autocompleteItem,
matchHighlight: foreground(
colorScheme.middle,
"accent",
"active"
),
background: background(colorScheme.middle, "active"),
},
},
},
},
compositionMark: {
underline: {
thickness: 1.0,
color: borderColor(layer),
},
},
syntax,
};
diagnosticHeader: {
background: background(colorScheme.middle),
iconWidthFactor: 1.5,
textScaleFactor: 0.857,
border: border(colorScheme.middle, {
bottom: true,
top: true,
}),
code: {
...text(colorScheme.middle, "mono", { size: "sm" }),
margin: {
left: 10,
},
},
message: {
highlightText: text(colorScheme.middle, "sans", {
size: "sm",
weight: "bold",
}),
text: text(colorScheme.middle, "sans", { size: "sm" }),
},
},
diagnosticPathHeader: {
background: background(colorScheme.middle),
textScaleFactor: 0.857,
filename: text(colorScheme.middle, "mono", { size: "sm" }),
path: {
...text(colorScheme.middle, "mono", { size: "sm" }),
margin: {
left: 12,
},
},
},
errorDiagnostic: diagnostic(colorScheme.middle, "negative"),
warningDiagnostic: diagnostic(colorScheme.middle, "warning"),
informationDiagnostic: diagnostic(colorScheme.middle, "accent"),
hintDiagnostic: diagnostic(colorScheme.middle, "warning"),
invalidErrorDiagnostic: diagnostic(colorScheme.middle, "base"),
invalidHintDiagnostic: diagnostic(colorScheme.middle, "base"),
invalidInformationDiagnostic: diagnostic(colorScheme.middle, "base"),
invalidWarningDiagnostic: diagnostic(colorScheme.middle, "base"),
hoverPopover: hoverPopover(colorScheme),
linkDefinition: {
color: syntax.linkUri.color,
underline: syntax.linkUri.underline,
},
jumpIcon: {
color: foreground(layer, "on"),
iconWidth: 20,
buttonWidth: 20,
cornerRadius: 6,
padding: {
top: 6,
bottom: 6,
left: 6,
right: 6,
},
hover: {
background: background(layer, "on", "hovered"),
},
},
scrollbar: {
width: 12,
minHeightFactor: 1.0,
track: {
border: border(layer, "variant", { left: true }),
},
thumb: {
background: withOpacity(background(layer, "inverted"), 0.4),
border: {
width: 1,
color: borderColor(layer, "variant"),
},
},
},
compositionMark: {
underline: {
thickness: 1.0,
color: borderColor(layer),
},
},
syntax,
}
}

View File

@@ -1,37 +1,44 @@
import { ColorScheme } from "../themes/common/colorScheme";
import { background, border, text } from "./components";
import { ColorScheme } from "../themes/common/colorScheme"
import { background, border, text } from "./components"
export default function feedback(colorScheme: ColorScheme) {
let layer = colorScheme.highest;
let layer = colorScheme.highest
return {
submit_button: {
...text(layer, "mono", "on"),
background: background(layer, "on"),
cornerRadius: 6,
border: border(layer, "on"),
margin: {
right: 4,
},
padding: {
bottom: 2,
left: 10,
right: 10,
top: 2,
},
clicked: {
...text(layer, "mono", "on", "pressed"),
background: background(layer, "on", "pressed"),
border: border(layer, "on", "pressed"),
},
hover: {
...text(layer, "mono", "on", "hovered"),
background: background(layer, "on", "hovered"),
border: border(layer, "on", "hovered"),
},
},
button_margin: 8,
info_text: text(layer, "sans", "default", { size: "xs" }),
};
return {
submit_button: {
...text(layer, "mono", "on"),
background: background(layer, "on"),
cornerRadius: 6,
border: border(layer, "on"),
margin: {
right: 4,
},
padding: {
bottom: 2,
left: 10,
right: 10,
top: 2,
},
clicked: {
...text(layer, "mono", "on", "pressed"),
background: background(layer, "on", "pressed"),
border: border(layer, "on", "pressed"),
},
hover: {
...text(layer, "mono", "on", "hovered"),
background: background(layer, "on", "hovered"),
border: border(layer, "on", "hovered"),
},
},
button_margin: 8,
info_text_default: text(layer, "sans", "default", { size: "xs" }),
link_text_default: text(layer, "sans", "default", {
size: "xs",
underline: true,
}),
link_text_hover: text(layer, "sans", "hovered", {
size: "xs",
underline: true,
}),
}
}

View File

@@ -1,45 +1,45 @@
import { ColorScheme } from "../themes/common/colorScheme";
import { background, border, text } from "./components";
import { ColorScheme } from "../themes/common/colorScheme"
import { background, border, text } from "./components"
export default function HoverPopover(colorScheme: ColorScheme) {
let layer = colorScheme.middle;
let baseContainer = {
background: background(layer),
cornerRadius: 8,
padding: {
left: 8,
right: 8,
top: 4,
bottom: 4,
},
shadow: colorScheme.popoverShadow,
border: border(layer),
margin: {
left: -8,
},
};
let layer = colorScheme.middle
let baseContainer = {
background: background(layer),
cornerRadius: 8,
padding: {
left: 8,
right: 8,
top: 4,
bottom: 4,
},
shadow: colorScheme.popoverShadow,
border: border(layer),
margin: {
left: -8,
},
}
return {
container: baseContainer,
infoContainer: {
...baseContainer,
background: background(layer, "accent"),
border: border(layer, "accent"),
},
warningContainer: {
...baseContainer,
background: background(layer, "warning"),
border: border(layer, "warning"),
},
errorContainer: {
...baseContainer,
background: background(layer, "negative"),
border: border(layer, "negative"),
},
block_style: {
padding: { top: 4 },
},
prose: text(layer, "sans", { size: "sm" }),
highlight: colorScheme.ramps.neutral(0.5).alpha(0.2).hex(), // TODO: blend was used here. Replace with something better
};
return {
container: baseContainer,
infoContainer: {
...baseContainer,
background: background(layer, "accent"),
border: border(layer, "accent"),
},
warningContainer: {
...baseContainer,
background: background(layer, "warning"),
border: border(layer, "warning"),
},
errorContainer: {
...baseContainer,
background: background(layer, "negative"),
border: border(layer, "negative"),
},
block_style: {
padding: { top: 4 },
},
prose: text(layer, "sans", { size: "sm" }),
highlight: colorScheme.ramps.neutral(0.5).alpha(0.2).hex(), // TODO: blend was used here. Replace with something better
}
}

View File

@@ -1,45 +1,53 @@
import { ColorScheme } from "../themes/common/colorScheme";
import { background, border, text } from "./components";
import { ColorScheme } from "../themes/common/colorScheme"
import { background, border, text } from "./components"
export default function incomingCallNotification(colorScheme: ColorScheme): Object {
let layer = colorScheme.middle;
const avatarSize = 48;
return {
windowHeight: 74,
windowWidth: 380,
background: background(layer),
callerContainer: {
padding: 12,
},
callerAvatar: {
height: avatarSize,
width: avatarSize,
cornerRadius: avatarSize / 2,
},
callerMetadata: {
margin: { left: 10 },
},
callerUsername: {
...text(layer, "sans", { size: "sm", weight: "bold" }),
margin: { top: -3 },
},
callerMessage: {
...text(layer, "sans", "variant", { size: "xs" }),
margin: { top: -3 },
},
worktreeRoots: {
...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
margin: { top: -3 },
},
buttonWidth: 96,
acceptButton: {
background: background(layer, "accent"),
border: border(layer, { left: true, bottom: true }),
...text(layer, "sans", "positive", { size: "xs", weight: "extra_bold" })
},
declineButton: {
border: border(layer, { left: true }),
...text(layer, "sans", "negative", { size: "xs", weight: "extra_bold" })
},
};
export default function incomingCallNotification(
colorScheme: ColorScheme
): Object {
let layer = colorScheme.middle
const avatarSize = 48
return {
windowHeight: 74,
windowWidth: 380,
background: background(layer),
callerContainer: {
padding: 12,
},
callerAvatar: {
height: avatarSize,
width: avatarSize,
cornerRadius: avatarSize / 2,
},
callerMetadata: {
margin: { left: 10 },
},
callerUsername: {
...text(layer, "sans", { size: "sm", weight: "bold" }),
margin: { top: -3 },
},
callerMessage: {
...text(layer, "sans", "variant", { size: "xs" }),
margin: { top: -3 },
},
worktreeRoots: {
...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
margin: { top: -3 },
},
buttonWidth: 96,
acceptButton: {
background: background(layer, "accent"),
border: border(layer, { left: true, bottom: true }),
...text(layer, "sans", "positive", {
size: "xs",
weight: "extra_bold",
}),
},
declineButton: {
border: border(layer, { left: true }),
...text(layer, "sans", "negative", {
size: "xs",
weight: "extra_bold",
}),
},
}
}

View File

@@ -1,78 +1,78 @@
import { ColorScheme } from "../themes/common/colorScheme";
import { background, border, text } from "./components";
import { ColorScheme } from "../themes/common/colorScheme"
import { background, border, text } from "./components"
export default function picker(colorScheme: ColorScheme) {
let layer = colorScheme.lowest;
const container = {
background: background(layer),
border: border(layer),
shadow: colorScheme.modalShadow,
cornerRadius: 12,
padding: {
bottom: 4,
let layer = colorScheme.lowest
const container = {
background: background(layer),
border: border(layer),
shadow: colorScheme.modalShadow,
cornerRadius: 12,
padding: {
bottom: 4,
},
}
};
const inputEditor = {
placeholderText: text(layer, "sans", "on", "disabled"),
selection: colorScheme.players[0],
text: text(layer, "mono", "on"),
border: border(layer, { bottom: true }),
padding: {
bottom: 8,
left: 16,
right: 16,
top: 8,
},
margin: {
bottom: 4,
},
};
const emptyInputEditor = { ...inputEditor };
delete emptyInputEditor.border;
delete emptyInputEditor.margin;
const inputEditor = {
placeholderText: text(layer, "sans", "on", "disabled"),
selection: colorScheme.players[0],
text: text(layer, "mono", "on"),
border: border(layer, { bottom: true }),
padding: {
bottom: 8,
left: 16,
right: 16,
top: 8,
},
margin: {
bottom: 4,
},
}
const emptyInputEditor = { ...inputEditor }
delete emptyInputEditor.border
delete emptyInputEditor.margin
return {
...container,
emptyContainer: {
...container,
padding: {}
},
item: {
padding: {
bottom: 4,
left: 12,
right: 12,
top: 4,
},
margin: {
top: 1,
left: 4,
right: 4,
},
cornerRadius: 8,
text: text(layer, "sans", "variant"),
highlightText: text(layer, "sans", "accent", { weight: "bold" }),
active: {
background: background(layer, "base", "active"),
text: text(layer, "sans", "base", "active"),
highlightText: text(layer, "sans", "accent", {
weight: "bold",
}),
},
hover: {
background: background(layer, "hovered"),
},
},
inputEditor,
emptyInputEditor,
noMatches: {
text: text(layer, "sans", "variant"),
padding: {
bottom: 8,
left: 16,
right: 16,
top: 8,
},
},
};
return {
...container,
emptyContainer: {
...container,
padding: {},
},
item: {
padding: {
bottom: 4,
left: 12,
right: 12,
top: 4,
},
margin: {
top: 1,
left: 4,
right: 4,
},
cornerRadius: 8,
text: text(layer, "sans", "variant"),
highlightText: text(layer, "sans", "accent", { weight: "bold" }),
active: {
background: background(layer, "base", "active"),
text: text(layer, "sans", "base", "active"),
highlightText: text(layer, "sans", "accent", {
weight: "bold",
}),
},
hover: {
background: background(layer, "hovered"),
},
},
inputEditor,
emptyInputEditor,
noMatches: {
text: text(layer, "sans", "variant"),
padding: {
bottom: 8,
left: 16,
right: 16,
top: 8,
},
},
}
}

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