Compare commits

...

268 Commits

Author SHA1 Message Date
Max Brunsfeld
48624b796e 0.47.0 2022-07-13 15:59:11 -07:00
Max Brunsfeld
9c82d5b080 Merge pull request #1333 from zed-industries/editor-mouse-context-menu
Editor mouse context menu
2022-07-13 15:58:31 -07:00
Keith Simmons
ed1370eafc Merge pull request #1330 from zed-industries/completions-setting
Completions Menu Setting
2022-07-13 15:35:10 -07:00
Max Brunsfeld
bb83d867b3 Merge pull request #1351 from zed-industries/synchronous-language-names
Fix incomplete language names list being used for JSON schema
2022-07-13 15:16:37 -07:00
Max Brunsfeld
4775d839d7 Fix incomplete language names list being used for JSON schema
For now, since initializing the languages themselves is still async,
create a parallel duplicated code path that is synchronous, and
just provided the language names.
2022-07-13 15:04:28 -07:00
K Simmons
e7b1060bca fix merge error to use new default settings flow 2022-07-13 14:29:47 -07:00
Keith Simmons
7f3018c3f6 add show_completions_on_input setting to disable popping the completions menu automatically 2022-07-13 14:23:49 -07:00
Max Brunsfeld
cd87c5552e Merge pull request #1350 from zed-industries/soft-revert-json-plugin
Temporarily remove JSON plugin + restore native JSON LspAdapter
2022-07-13 14:18:04 -07:00
Keith Simmons
5366ed4404 Add basic test for editor context menu 2022-07-13 14:13:19 -07:00
Keith Simmons
b850e41d6f Add editor mouse context menu with some basic refactorings and an entry to pop the code actions 2022-07-13 14:13:19 -07:00
Keith Simmons
d796b543e0 WIP add basic context menu and make progress toward adding quick actions to it 2022-07-13 14:13:19 -07:00
Max Brunsfeld
dddeb66e2a Temporarily remove JSON plugin + restore native JSON LspAdapter 2022-07-13 13:56:39 -07:00
Mikayla Maki
958fd9ad55 Merge pull request #1349 from zed-industries/connection-refactor
Terminal Connection touch up
2022-07-13 13:28:42 -07:00
Mikayla Maki
7885234fbc Added clear screan command 2022-07-13 13:19:21 -07:00
Mikayla Maki
4f9d88f3e0 Made a quick fix for modal issues, better solution pending rewrite 2022-07-13 13:06:23 -07:00
Mikayla Maki
344e037406 remove temporary work 2022-07-13 12:18:43 -07:00
Mikayla Maki
494c168c6f Beginning rewrite of affected systems 2022-07-13 12:18:43 -07:00
Mikayla Maki
f630ab4821 checkpoint 2022-07-13 12:18:43 -07:00
Mikayla Maki
2ca340b9f1 Beginning research 2022-07-13 12:18:43 -07:00
Nate Butler
efad2a9ccd Merge pull request #1348 from zed-industries/fix-theme-rose-pine
Update rose pine theme
2022-07-13 14:51:37 -04:00
Antonio Scandurra
a452699f6b Merge pull request #1347 from zed-industries/rustup-directly-on-ci
Use rustup directly on CI instead of actions-rs/toolchain
2022-07-13 20:19:09 +02:00
Nate Butler
474a08b1db Update rose pine theme
- Update Rose Pine & Rose Pine Dawn with the correct semantic colors
- Add Rose Pine Moon
2022-07-13 14:16:32 -04:00
Max Brunsfeld
0e010c2fbc Use rustup directly on CI instead of actions-rs/toolchain
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Isaac Clayton <slightknack@gmail.com>
2022-07-13 10:37:43 -07:00
Isaac Clayton
01a2d53638 Merge pull request #1327 from zed-industries/plugin-epoch
Configurable Plugin Yielding
2022-07-13 15:29:22 +02:00
Antonio Scandurra
1460fd0e2f Merge pull request #1342 from zed-industries/mouse-based-splitting
Introduce mouse-based pane splitting
2022-07-13 15:25:20 +02:00
Antonio Scandurra
3e3bd7ccc8 Don't steal focus when splitting pane via context menu 2022-07-13 15:06:39 +02:00
Isaac Clayton
a6edf85078 Use enum to differentiate between normal and precompiled plugins 2022-07-13 14:26:52 +02:00
Isaac Clayton
daf1674ca6 Fix failing test 2022-07-13 13:27:31 +02:00
Isaac Clayton
c956a8866e Quick documentation fix 2022-07-13 13:21:45 +02:00
Isaac Clayton
b3e1fd0740 Rename a few items and add documentation 2022-07-13 13:19:51 +02:00
Isaac Clayton
8b376dd613 Fix resulting errors and introduce functional executor callback 2022-07-13 13:15:12 +02:00
Isaac Clayton
8974b0c490 Work on supporting both epoch and fuel 2022-07-13 12:41:47 +02:00
Antonio Scandurra
9677db9f8f Deploy context menu when clicking on split icon 2022-07-13 12:15:36 +02:00
Isaac Clayton
10670dba70 Add support for configuring plugin yield mechanism, stashing 2022-07-13 11:57:00 +02:00
Antonio Scandurra
c87efb0dbc Show split button at the right edge of the tab bar 2022-07-13 11:44:33 +02:00
Isaac Clayton
8eb8f8ec3a Merge pull request #1340 from zed-industries/plugin-docs-witx
Add note about WebAssembly Interface Types.
2022-07-13 11:09:58 +02:00
Isaac Clayton
d04c3388b4 Switch from epoch to fuel 2022-07-13 11:09:06 +02:00
Antonio Scandurra
e55e69caba Merge pull request #1341 from zed-industries/mitigate-refcell-panics
Mitigate `RefCell` panics
2022-07-13 10:52:21 +02:00
Antonio Scandurra
8e2e5b5cf0 Don't borrow window state mutably until we need to invoke resize callback 2022-07-13 10:38:34 +02:00
Antonio Scandurra
c53fa4941a Ensure no borrows are held when activating window or creating prompts 2022-07-13 10:37:55 +02:00
Antonio Scandurra
d4e0f73ffe Drop window borrow before calling makeKeyAndOrderFront
We're seeing some stack traces where calling `makeKeyAndOrderFront`
could invoke `setFrameSize`, which is causing a double borrow.
2022-07-13 10:19:46 +02:00
Isaac Clayton
97c163a62e Add note about WebAssembly Interface Types. 2022-07-13 09:43:12 +02:00
Isaac Clayton
b49b11f5af Merge pull request #1339 from zed-industries/plugin-rerun
Rerun plugin build step only on change
2022-07-13 09:35:05 +02:00
Isaac Clayton
7e319a2b9d Fix #1335, rerun plugin build step only if non-volatile directories in plugin change 2022-07-13 09:04:48 +02:00
Max Brunsfeld
0defb0e50f Ensure settings global is initialized immediately 2022-07-12 18:43:00 -07:00
Max Brunsfeld
2d23774ac0 Merge pull request #1336 from zed-industries/cancel-rename-restore-highlights
Restore document highlights when canceling a rename
2022-07-12 18:35:29 -07:00
Mikayla Maki
0beb385af4 Merge pull request #1338 from zed-industries/terminal-launch-bug
Terminal launch bug
2022-07-12 18:06:18 -07:00
Max Brunsfeld
28ec4d47cd Merge pull request #1337 from zed-industries/respect-hard-tabs-setting
Simplify setting merging, fix ignored hard_tabs setting
2022-07-12 17:58:21 -07:00
Mikayla Maki
598954d39f Added a bit of documentation for the working directory calculation 2022-07-12 17:49:14 -07:00
Mikayla Maki
41e83b6be2 Fixes terminal launch issues and adds tests for queries 2022-07-12 17:45:11 -07:00
Max Brunsfeld
277f561b8c Simplify setting merging, fix ignored hard_tabs setting 2022-07-12 17:43:00 -07:00
Max Brunsfeld
b7109ea4fc Restore document highlights when canceling a rename 2022-07-12 17:01:18 -07:00
Max Brunsfeld
69f517ead5 Remove stray 'pbcpoy' file 2022-07-12 16:10:26 -07:00
Max Brunsfeld
d0d750c559 Merge pull request #1334 from zed-industries/plugin-id-for-language
Fix association of 'json' lsp language id with JSON language
2022-07-12 16:03:31 -07:00
Max Brunsfeld
2a478462b6 Fix association of 'json' lsp language id with JSON language 2022-07-12 15:43:59 -07:00
Max Brunsfeld
dd554c19df Merge pull request #1331 from zed-industries/discoverable-settings
Make settings more discoverable
2022-07-12 14:38:16 -07:00
Nate Butler
f2c932a933 Add terminal icon for status bar 2022-07-12 17:22:31 -04:00
Max Brunsfeld
0ebf417c2e Pre-populate settings.json when initializing it from Zed 2022-07-12 14:19:36 -07:00
Max Brunsfeld
7750054a45 Add application menu item to open the default settings 2022-07-12 13:38:24 -07:00
Max Brunsfeld
8464c03e65 Adjust settings schema generation to reflect rename
Also, simplify that code.
2022-07-12 12:19:01 -07:00
Max Brunsfeld
c02f4ea8dc Rename LanguageSettings -> EditorSettings 2022-07-12 11:42:43 -07:00
Max Brunsfeld
ec8a493700 Move all default settings from source code into the JSON file 2022-07-12 11:35:19 -07:00
Max Brunsfeld
e51dc25e1d Start moving default settings into a JSON file
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2022-07-12 11:00:02 -07:00
Isaac Clayton
7f11a32364 Implement periodic yielding using epoch_deadline_async_yield_and_update 2022-07-12 17:07:33 +02:00
Isaac Clayton
1ac8265028 Fix typo 2022-07-12 16:39:04 +02:00
Isaac Clayton
170d27b04c Start working on plugin epoch async yield 2022-07-12 16:32:41 +02:00
Isaac Clayton
8bcfcce506 Merge pull request #1326 from zed-industries/plugin-docs
Write about how plugins work and how to use them
2022-07-12 14:09:52 +02:00
Isaac Clayton
6600251952 Fix yet another typo 2022-07-12 13:54:00 +02:00
Isaac Clayton
37310acea8 Fix more typos 2022-07-12 13:51:13 +02:00
Isaac Clayton
1170d73b57 Fix typo 2022-07-12 13:48:36 +02:00
Isaac Clayton
c188021d6c Write about how plugins work and how to use them 2022-07-12 13:45:22 +02:00
Antonio Scandurra
afc8e9050c Merge pull request #1252 from zed-industries/plugin
Language Server WebAssembly Plugin Integration (Part 2)
2022-07-12 11:04:20 +02:00
Isaac Clayton
815de6da61 Rewrite test_managing_language_servers to add languages after buffers are open
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
2022-07-12 10:25:11 +02:00
Antonio Scandurra
c4f10befe8 Use latest Rust version on CI
Co-Authored-By: Isaac Clayton <slightknack@gmail.com>
2022-07-12 09:53:58 +02:00
Isaac Clayton
d8b22a200e Rename LspAdapterTrait to LspAdapter and LspAdapter to CachedLspAdapter 2022-07-12 09:29:38 +02:00
Antonio Scandurra
5c789affc9 Merge pull request #1324 from zed-industries/edited-read-only
Don't prompt guest to save when closing window after disconnection
2022-07-12 09:14:39 +02:00
Antonio Scandurra
b1e3b38cb3 Don't prompt guest to save when closing window after disconnection 2022-07-12 09:05:39 +02:00
Max Brunsfeld
0bcd209a3f Merge pull request #1322 from zed-industries/help-menu-docs
In Help menu, replace 'zed.dev' item with 'Documentation'
2022-07-11 15:27:46 -07:00
Max Brunsfeld
dc1956fe69 In Help menu, replace 'zed.dev' item with 'Documentation' 2022-07-11 13:00:37 -07:00
Isaac Clayton
aeb1b89c25 Make plugin build profile contingent on host build profile 2022-07-11 21:13:52 +02:00
Keith Simmons
1e85d6f07d Add pull request template
Adds some formatting and most importantly add some nudges to fill out tests, settings, and documentation in every PR
2022-07-11 09:41:49 -07:00
Isaac Clayton
031162b473 Remove spurious warnings 2022-07-11 18:36:33 +02:00
Isaac Clayton
41918101ed Add wasm32-wasi to CI workflow 2022-07-11 18:29:27 +02:00
Isaac Clayton
38f8191ce8 Add comment linking engine creating code together 2022-07-11 18:20:50 +02:00
Isaac Clayton
19d19271f6 Remove stale label_for_completion impl from JSON plugin 2022-07-11 18:01:12 +02:00
Isaac Clayton
1dd92c3c28 Remove plugin build script in favor of build.rs plugin builder 2022-07-11 16:59:59 +02:00
Isaac Clayton
0bdbbdd9b6 Convert rust lsp tests from sync #[test] to async #[gpui::test] 2022-07-11 16:55:08 +02:00
Isaac Clayton
836719526c Remove stale commented code 2022-07-11 16:50:49 +02:00
Isaac Clayton
c4bf71d222 Convert go lsp tests from sync #[test] to async #[gpui::test] 2022-07-11 16:47:21 +02:00
Isaac Clayton
638f881fe4 Remove json host-side implementation, rely on plugin implementation 2022-07-11 16:41:30 +02:00
Isaac Clayton
be41ad44a7 Fix minor issues in plugin and project raised during review 2022-07-11 16:40:12 +02:00
Isaac Clayton
bc94d0d1a9 Restore main version of Project::symbols and convert to async 2022-07-11 16:22:58 +02:00
Isaac Clayton
0600157c38 Restore main version of Project::completions and convert to async 2022-07-11 15:55:07 +02:00
Isaac Clayton
ec327a30c3 Fix minor issues pointed out in the review 2022-07-11 15:54:03 +02:00
Isaac Clayton
3ad8d5363c Remove the blocking call and inline on_lsp_diagnostics_published 2022-07-11 12:11:00 +02:00
Isaac Clayton
14bccb4a90 More cleanup during review 2022-07-11 10:56:21 +02:00
Isaac Clayton
5ec828a3e2 Remove unused struct fields 2022-07-11 10:39:14 +02:00
Isaac Clayton
8c91c5c575 Minor fixes found during review 2022-07-11 10:37:51 +02:00
Keith Simmons
19245dd3ae Merge pull request #1315 from zed-industries/fix-terminal-modal-panic
Fix Terminal Panic
2022-07-08 17:26:56 -07:00
Max Brunsfeld
5bafabcb8e Merge pull request #1314 from zed-industries/code-action-with-edit-and-command
Run code action's commands if their edits are empty
2022-07-08 16:58:12 -07:00
Keith Simmons
667d031ec8 Merge pull request #1311 from zed-industries/fix-visual-paste
Fix visual paste
2022-07-08 16:54:10 -07:00
Keith Simmons
ed3666547b Make global type more resilient, and fix modal keymap context 2022-07-08 16:29:29 -07:00
Keith Simmons
20f7fba16f Move terminal scripts to scripts folder, and remove parking_lot from terminal crate
Co-authored-by: mikayla.c.maki@gmail.com
2022-07-08 16:16:57 -07:00
Keith Simmons
31361e564d remove temporary debug print statements
Co-authored-by: mikayla.c.maki@gmail.com
2022-07-08 16:14:41 -07:00
Max Brunsfeld
e101f4e705 Run code action's commands if their edits are empty 2022-07-08 16:12:20 -07:00
Max Brunsfeld
25d75feffc Merge pull request #1313 from zed-industries/release-script
Add script for summarizing changes since last release
2022-07-08 16:10:39 -07:00
Keith Simmons
8d34fe7e94 Refactor terminal connection into a model which can be copied between terminal views
Refactor terminal modal code to use TerminalConnection model handle so we aren't storing TerminalViews in the globals
Adjust INSTANCE_BUFFER_SIZE in renderer to handle pathological terminal renders

Co-authored-by: mikayla.c.maki@gmail.com
2022-07-08 16:10:09 -07:00
Max Brunsfeld
55d7e1757c Add script for summarizing changes since last release 2022-07-08 15:51:28 -07:00
Max Brunsfeld
9683db936d 0.46.0 2022-07-08 14:22:36 -07:00
Max Brunsfeld
6c3384b67a Merge pull request #1312 from zed-industries/fix-tree-query-hang
Fix hang due to tree-sitter query
2022-07-08 14:16:38 -07:00
Max Brunsfeld
1f16c68e6b Upgrade Tree-sitter and limit query cursors' concurrently-buffered matches 2022-07-08 14:09:34 -07:00
Isaac Clayton
8931218dc6 Remove debug statements 2022-07-08 21:28:35 +02:00
Isaac Clayton
3e8b230567 Highlight languages as languages load 2022-07-08 21:19:07 +02:00
Keith Simmons
a82e56918e Merge pull request #1294 from zed-industries/terminal-modal
Proposal: Terminal modal
2022-07-08 11:11:03 -07:00
Keith Simmons
ee007f901a fix pasting at the end of the line in normal mode 2022-07-08 10:57:02 -07:00
Isaac Clayton
988f388165 Added theme to language 2022-07-08 18:11:28 +02:00
Isaac Clayton
6f99d59d38 Require theme directly when creating language 2022-07-08 16:08:40 +02:00
Antonio Scandurra
d0c9818e8b Merge pull request #1309 from zed-industries/gitignore-traverse-ancestors
Honor `.gitignore` files above worktree's root
2022-07-08 15:49:23 +02:00
Isaac Clayton
73620dad06 Add channel to notify project when languages are added 2022-07-08 14:37:27 +02:00
Antonio Scandurra
540aa1748a Add unit test for ignored files in file finder 2022-07-08 12:16:42 +02:00
Antonio Scandurra
56f9c7bc1b Include ignored files in fuzzy search when root entry is ignored 2022-07-08 11:54:45 +02:00
Antonio Scandurra
32c6ae3188 🎨 2022-07-08 11:42:59 +02:00
Antonio Scandurra
e66144104f Honor gitignores above worktree root 2022-07-08 11:19:46 +02:00
Antonio Scandurra
9328ab121a Use absolute paths to compute ignored status
This lays the groundwork for harvesting gitignores up above the
worktree.
2022-07-08 11:19:21 +02:00
Antonio Scandurra
ca225d0765 Make build_gitignore async 2022-07-08 08:50:21 +02:00
Mikayla Maki
4a860d4da4 Properly fixed merge issues 2022-07-07 18:05:37 -07:00
Mikayla Maki
d373e4424f Merge branch 'main' into terminal-modal 2022-07-07 17:48:58 -07:00
Mikayla Maki
621fab2da1 Merge pull request #1299 from zed-industries/terminal-selections
Terminal selections first pass
2022-07-07 17:39:46 -07:00
Mikayla Maki
e628b49dfd First pass at selections complete 2022-07-07 17:31:38 -07:00
Mikayla Maki
be94f614a7 fix merge conflicr 2022-07-07 17:24:55 -07:00
Mikayla Maki
a564f34d3a Merge pull request #1300 from zed-industries/gpui-changes
GPUI change proposals
2022-07-07 17:21:58 -07:00
Mikayla Maki
8cb6e476f0 Fixed panic / bug around scrolling and selections in termainl 2022-07-07 17:19:53 -07:00
Mikayla Maki
ca877245be Finished merge of drag update 2022-07-07 16:12:24 -07:00
Mikayla Maki
bbd0c0d44d Merge branch 'main' into gpui-changes 2022-07-07 16:08:32 -07:00
Mikayla Maki
9fd2bf2fa1 Updated drag API to pass old,new, instead of delta,new 2022-07-07 16:07:24 -07:00
Nathan Sobo
805c06ee76 Merge pull request #1298 from zed-industries/mouse-region-refactor
Mouse Event Refactor
2022-07-07 16:43:36 -06:00
Mikayla Maki
f86106a07e Fixed a bug around selecting a single cell 2022-07-07 15:24:06 -07:00
Mikayla Maki
1fab7be4b5 Finished selections for now 2022-07-07 15:10:08 -07:00
Mikayla Maki
5a1797cb21 Fixing merge conflicts 2022-07-07 14:56:38 -07:00
Mikayla Maki
59c8e8bdad Fixed integration test 2022-07-07 14:55:25 -07:00
Mikayla Maki
ab0ca7d42a Added another minor test 2022-07-07 14:55:25 -07:00
Mikayla Maki
102f502c26 tidied up magic constants 2022-07-07 14:55:25 -07:00
Mikayla Maki
cc985721c6 Added a small integration test 2022-07-07 14:55:25 -07:00
Mikayla Maki
2a6e23ff28 Hoisted assert clipboard into TestAppContext 2022-07-07 14:55:25 -07:00
Mikayla Maki
9209c0dfeb Fixed merge conflict 2022-07-07 14:55:22 -07:00
Mikayla Maki
7c0d9f411a Added copying 2022-07-07 14:55:03 -07:00
Mikayla Maki
8c1054fbb6 Fixed merge conflict 2022-07-07 14:54:58 -07:00
Mikayla Maki
b5919c0555 Fixed merge conflict 2022-07-07 14:54:36 -07:00
Mikayla Maki
415e28e2d3 Fixed merge conflict 2022-07-07 14:54:13 -07:00
Mikayla Maki
a8237858bc Added basic selections 2022-07-07 14:52:04 -07:00
Mikayla Maki
86d5794040 Rebasing onto main 2022-07-07 14:51:59 -07:00
Mikayla Maki
9b6167aad8 added inline hint 2022-07-07 14:51:34 -07:00
Mikayla Maki
2c6dcb82ef GPUI change proposals 2022-07-07 14:51:34 -07:00
Mikayla Maki
49bd51c7c1 Fixed integration test 2022-07-07 14:38:21 -07:00
Mikayla Maki
28fd1ccbc6 Added another minor test 2022-07-07 13:55:58 -07:00
Mikayla Maki
d981f4a3f4 tidied up magic constants 2022-07-07 13:45:27 -07:00
Mikayla Maki
4bd1111115 Added a small integration test 2022-07-07 13:43:28 -07:00
Max Brunsfeld
304ea2d574 Merge pull request #1307 from zed-industries/nav-button-tweak
Add tooltips to pane nav buttons and make them trigger on click
2022-07-07 13:43:01 -07:00
Max Brunsfeld
6642b78331 Add tooltips to pane nav buttons and make them trigger on click 2022-07-07 13:36:08 -07:00
Mikayla Maki
e3f492e13a Hoisted assert clipboard into TestAppContext 2022-07-07 13:29:58 -07:00
Mikayla Maki
c0c2297deb Merge branch 'main' into terminal-selections 2022-07-07 13:20:23 -07:00
Mikayla Maki
4e3c32c277 Added copying 2022-07-07 13:19:38 -07:00
Mikayla Maki
49859d8f94 Merge pull request #1306 from zed-industries/fix-open-pane
Fixed terminal clone on split
2022-07-07 13:05:24 -07:00
Mikayla Maki
98f6dccd43 Fixed terminal clone on split 2022-07-07 13:01:16 -07:00
Mikayla Maki
ad5e4e7c6c Merge pull request #1305 from zed-industries/fix-default-shell
Now defaults to using user's shell
2022-07-07 12:41:37 -07:00
Mikayla Maki
ec4082695b Now defaults to using user's shell 2022-07-07 12:31:21 -07:00
Mikayla Maki
240f3d8754 Fixed default shell 2022-07-07 12:29:49 -07:00
Mikayla Maki
bc306ef8ed Merge branch 'main' into terminal-selections 2022-07-07 12:17:58 -07:00
Mikayla Maki
1cfe8688ca Merge pull request #1304 from zed-industries/deploy-panic
Fixed working directory issues, added tests.
2022-07-07 12:14:57 -07:00
Mikayla Maki
02525c5bbe Added a way to change the timeout with state 2022-07-07 12:04:17 -07:00
Mikayla Maki
9c518085ae Fixed working directory issues, added tests. Working on regression 2022-07-07 11:01:26 -07:00
Isaac Clayton
5cb59dfdab Fix errors resulting from rebase 2022-07-07 18:14:16 +02:00
Isaac Clayton
a16fc2ba0c Add basic support for precompiling plugins 2022-07-07 16:21:27 +02:00
Isaac Clayton
895747476f Done! Finish transition to async, very close to merging 2022-07-07 16:21:20 +02:00
Isaac Clayton
39fdbc593b Fix most warnings 2022-07-07 16:19:49 +02:00
Isaac Clayton
d009e10a46 Fix all residual errors, need to polish off warnings and TODOS 2022-07-07 16:17:03 +02:00
Isaac Clayton
6585daccf9 Further unpropogate async 2022-07-07 16:16:58 +02:00
Isaac Clayton
4f016d5fc4 Switch LspAdapter to struct and revert some async/await 2022-07-07 16:12:10 +02:00
Isaac Clayton
0872e9b1a7 use join_all to build partial symbols and completions asynchronously 2022-07-07 15:44:21 +02:00
Isaac Clayton
602fe14aa4 Going to move LspAdapter from trait to struct 2022-07-07 15:44:21 +02:00
Isaac Clayton
e4a680f47b Uncommented previously commented sections 2022-07-07 15:44:18 +02:00
Isaac Clayton
2b0b341415 Move await outside of a closure, remove future_wrap dependency 2022-07-07 15:42:53 +02:00
Isaac Clayton
172e276411 Fix warnings and propogate async further 2022-07-07 15:42:48 +02:00
Isaac Clayton
ce90dbd06a Temporarily comment out closure errors to address other errors in project 2022-07-07 15:41:34 +02:00
Isaac Clayton
2ff67ef9f6 Factor out await in doubly-nested for loop 2022-07-07 15:40:39 +02:00
Isaac Clayton
db7b863d8c Fix on_settings_changed, need to review that impl is correct 2022-07-07 15:40:01 +02:00
Isaac Clayton
4dad2eb7d7 Refactor closure to extract async 2022-07-07 15:40:01 +02:00
Isaac Clayton
7d128e81aa Add lsp_settings_changed: Task to Project, need to resolve cx in Project::on_settings_changed 2022-07-07 15:39:59 +02:00
Isaac Clayton
f4b4212932 More work on transitioning to async, need to figure out when to stop 2022-07-07 15:38:28 +02:00
Isaac Clayton
feb6cf6789 Allow async to infect some more functions 2022-07-07 15:34:27 +02:00
Isaac Clayton
61f5326033 Add timing instrumentation 2022-07-07 15:32:43 +02:00
Isaac Clayton
2c637b83bf Work on updating code to be async 2022-07-07 15:32:41 +02:00
Isaac Clayton
841a9bd2a7 Make into async trait, will refactor everything Monday 2022-07-07 15:25:49 +02:00
Isaac Clayton
568017da85 Annotate that the bug is due to a deadlock, fixing now 2022-07-07 15:23:27 +02:00
Isaac Clayton
37e04320aa Checkpoint 2022-07-07 15:23:27 +02:00
Isaac Clayton
92c4552146 Isolate smol::Command hang as a test, does not hang 2022-07-07 15:23:27 +02:00
Isaac Clayton
e5481e2e65 Switch from std::process::Command to smol::process::Command 2022-07-07 15:22:17 +02:00
Isaac Clayton
42fc278913 Comment out label_for_completion for now 2022-07-07 15:22:17 +02:00
Isaac Clayton
f61ef446d3 Documentation pass 2022-07-07 15:22:17 +02:00
Isaac Clayton
4565f1a976 Add async host functions 2022-07-07 15:22:17 +02:00
Isaac Clayton
a5a0abb895 Update usage of WasiPlugin -> Plugin 2022-07-07 15:22:17 +02:00
Isaac Clayton
018fd46901 Rename WasiPlugin -> Plugin, etc. 2022-07-07 15:22:17 +02:00
Isaac Clayton
31e3a4d208 WIP: wrap async closures host-side 2022-07-07 15:22:17 +02:00
Isaac Clayton
f110945fd6 Add functions with multiple arguments to import macro, add test cases 2022-07-07 15:22:17 +02:00
Isaac Clayton
28f071e50d Split out lifecycle of serialization, buffer is freed now 2022-07-07 15:22:17 +02:00
Isaac Clayton
8aef8ab259 Add build.rs to rebuild plugins, and a test plugin 2022-07-07 15:22:17 +02:00
Isaac Clayton
5b40734f80 Change ABI from pair of u32s to single u64 2022-07-07 15:22:17 +02:00
Isaac Clayton
7edcf7c423 Factor out serialization code 2022-07-07 15:22:17 +02:00
Isaac Clayton
1f5903d16d Add allocator to store so that it can be used by host functions 2022-07-07 15:22:17 +02:00
Isaac Clayton
47520f0ca1 Remove dependency on self in call-related functions 2022-07-07 15:22:17 +02:00
Isaac Clayton
7266dff537 Fix issue with host function binding 2022-07-07 15:22:17 +02:00
Isaac Clayton
96c2559d2c Work on plugin builder 2022-07-07 15:22:17 +02:00
Isaac Clayton
53e56f1284 Start working on host-side functions 2022-07-07 15:22:17 +02:00
Isaac Clayton
71e0555763 Add JSON LSP plugin 2022-07-07 15:22:16 +02:00
Isaac Clayton
923f093aca First pass of plugin side of things complete 2022-07-07 15:20:27 +02:00
Isaac Clayton
d7b97b25b8 Async runtime working but is blocking 2022-07-07 15:20:27 +02:00
Isaac Clayton
8bce35d1e9 Move Wasi to async, validate timeslicing, using async in traits still WIP 2022-07-07 15:20:27 +02:00
Isaac Clayton
e9b87f3dc3 Factor out buffer code 2022-07-07 15:20:27 +02:00
Isaac Clayton
fbaff615a3 Get JSON LSP running, still work to be done 2022-07-07 15:20:27 +02:00
Isaac Clayton
38d7321511 Remove non-WASI code 2022-07-07 15:20:25 +02:00
Isaac Clayton
805c86b781 Add support for variadic functions 2022-07-07 15:19:03 +02:00
Isaac Clayton
17d15b2f08 Get Wasi working 2022-07-07 15:19:02 +02:00
Isaac Clayton
b84948711c Start moving code from Zed to plugin 2022-07-07 15:17:02 +02:00
Isaac Clayton
7dd3114a7a Start switching JSON LSP adapter to plugin (take 2) 2022-07-07 15:16:59 +02:00
Isaac Clayton
35b2eff29c Update plugin dependencies 2022-07-07 15:11:51 +02:00
Isaac Clayton
0cf64d6fba Clean up impl a bit 2022-07-07 15:11:51 +02:00
Isaac Clayton
f6a9558c5c Work on macro binding generation, some cleanup needed, rename runner to plugin 2022-07-07 15:11:51 +02:00
Isaac Clayton
dda6dcb3b8 Quick documentation pass 2022-07-07 15:11:51 +02:00
Isaac Clayton
6768713de2 Get basic Wasm runtime running 2022-07-07 15:11:51 +02:00
Isaac Clayton
feae434684 Update lockfile 2022-07-07 15:11:48 +02:00
Isaac Clayton
f6b6d19041 Add wasmtime and fix zstd version conflict 2022-07-07 15:01:15 +02:00
Isaac Clayton
4003037ca8 Documented code, got basic example working 2022-07-07 15:01:09 +02:00
Isaac Clayton
4ff9a6b1b5 Update lockfile 2022-07-07 14:55:46 +02:00
Isaac Clayton
13e0ad7253 Get Runtime working... 2022-07-07 14:55:46 +02:00
Isaac Clayton
265be4a2fb Clean up interface a bit 2022-07-07 14:55:46 +02:00
Isaac Clayton
8293b6971d Start sketching out runner runtime 2022-07-07 14:55:46 +02:00
Antonio Scandurra
627d067e57 Merge pull request #1302 from zed-industries/external-formatting
Introduce support for formatting via an external command
2022-07-07 14:31:17 +02:00
Antonio Scandurra
52b8efca1b Add integration test to exercise formatting via external command 2022-07-07 11:53:32 +02:00
Antonio Scandurra
b91d44b448 Respond with a debug version of the error in rpc Client 2022-07-07 11:52:56 +02:00
Antonio Scandurra
c6254247c3 Allow providing an external format in format_on_save setting 2022-07-07 11:04:03 +02:00
Mikayla Maki
baa011ccf4 added inline hint 2022-07-06 17:57:32 -07:00
Mikayla Maki
b2fa511acd GPUI change proposals 2022-07-06 17:52:20 -07:00
Mikayla Maki
778cfd94d8 Added basic selections 2022-07-06 17:37:12 -07:00
Keith Simmons
c139f1e6b6 combine branches of events in from_native 2022-07-06 16:59:58 -07:00
Max Brunsfeld
4ec2d6e50d Tweak navigation bar colors in theme
I meant to include this in #1297
2022-07-06 16:45:38 -07:00
Max Brunsfeld
f85d54425b Merge pull request #1297 from zed-industries/back-and-forward-buttons
Back and forward buttons
2022-07-06 16:43:31 -07:00
Keith Simmons
bcb553f233 Combine platform mouse events to use common MouseEvent struct and remove MouseDragged event in favor of MouseMoved 2022-07-06 16:37:56 -07:00
Max Brunsfeld
70cf6b4041 Give nav buttons a background on hover 2022-07-06 16:33:44 -07:00
Max Brunsfeld
4e8dbbfd4b Add test for pane nav history covering notification of pane's toolbar 2022-07-06 16:29:11 -07:00
Max Brunsfeld
a378ec49ec Enable and disable nav buttons based on pane's navigation stack
Also, make the `NavHistory` type private to the `workspace` crate.
Expose only the `ItemNavHistory` type, via a method on Pane called
`nav_history_for_item`.
2022-07-06 15:46:43 -07:00
Keith Simmons
686e57373b pull event data out into individual Event structs 2022-07-06 15:36:42 -07:00
Max Brunsfeld
7e5cf6669f Add forward and backward navigation buttons to toolbar 2022-07-06 14:05:24 -07:00
Mikayla Maki
cba5b4ac11 Began working on selections, refactored colors 2022-07-06 13:44:30 -07:00
Keith Simmons
525e317d96 remove custom terminal keybindings 2022-07-06 13:22:54 -07:00
Keith Simmons
2d126c7c5c add terminal modal which can be displayed and dismissed while preserving the terminal state 2022-07-06 13:20:54 -07:00
Max Brunsfeld
bbe325930f 0.45 2022-07-06 11:32:21 -07:00
Max Brunsfeld
bb6a573c67 Merge pull request #1293 from zed-industries/autosave-focus-change-window-activate
Treat window deactivation as a focus change for the purpose of autosave
2022-07-06 11:30:51 -07:00
Max Brunsfeld
a858b3fda9 Treat window deactivation as a focus change for the purpose of autosave 2022-07-06 11:20:29 -07:00
Max Brunsfeld
00d1c2e56f Merge pull request #1291 from zed-industries/fix-autosave-on-close
Fix autosave when closing a tab
2022-07-06 10:54:44 -07:00
Max Brunsfeld
e9a950f613 Merge pull request #1292 from zed-industries/paste-panic
Fix panic on paste when editing with auto-indent
2022-07-06 10:50:38 -07:00
Antonio Scandurra
2c1906d710 Normalize line endings when parsing completions
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-07-06 19:32:45 +02:00
Antonio Scandurra
d3db700db4 Fix panic on paste when editing with auto-indent
Instead of accepting text as it's input by the user, we will read it
out of the edit operation after it gets sanitized by the buffer.
2022-07-06 19:00:11 +02:00
Antonio Scandurra
ab4931da65 Prevent autosave for deleted files 2022-07-06 17:25:33 +02:00
Antonio Scandurra
9286893177 Save item when closing it if autosave on focus change is enabled 2022-07-06 17:04:41 +02:00
Antonio Scandurra
5e00df6267 Move autosave tests down into Workspace 2022-07-06 16:55:05 +02:00
Antonio Scandurra
b937c1acec Move autosave logic up into Workspace and Pane 2022-07-06 16:33:22 +02:00
Antonio Scandurra
980730a4e1 Report whether a view was focused or blurred when observing focus 2022-07-06 15:53:40 +02:00
Keith Simmons
ed52f8a8a3 WIP making paste better 2022-07-05 16:52:56 -07:00
115 changed files with 8644 additions and 3346 deletions

9
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,9 @@
## Description of feature or change
## Link to related issues from zed or insiders
## Before Merging
- [ ] Does this have tests or have existing tests been updated to cover this change?
- [ ] Have you added the necessary settings to configure this feature?
- [ ] Has documentation been created or updated (including above changes to settings)?

View File

@@ -25,11 +25,10 @@ jobs:
RUSTFLAGS: -D warnings
steps:
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: x86_64-apple-darwin
profile: minimal
run: |
rustup set profile minimal
rustup update stable
rustup target add wasm32-wasi
- name: Install Node
uses: actions/setup-node@v2
@@ -58,19 +57,13 @@ jobs:
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
steps:
- name: Install Rust x86_64-apple-darwin target
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: x86_64-apple-darwin
profile: minimal
- name: Install Rust aarch64-apple-darwin target
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: aarch64-apple-darwin
profile: minimal
- name: Install Rust
run: |
rustup set profile minimal
rustup update stable
rustup target add aarch64-apple-darwin
rustup target add x86_64-apple-darwin
rustup target add wasm32-wasi
- name: Install Node
uses: actions/setup-node@v2

3
.gitignore vendored
View File

@@ -1,6 +1,7 @@
/target
**/target
/zed.xcworkspace
.DS_Store
/plugins/bin
/script/node_modules
/styles/node_modules
/crates/collab/.env.toml

1155
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,13 +4,17 @@ default-members = ["crates/zed"]
resolver = "2"
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "1f1b1eb4501ed0a2d195d37f7de15f72aa10acd0" }
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
cocoa = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
cocoa-foundation = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
core-foundation = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
core-foundation-sys = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
core-graphics = { git = "https://github.com/servo/core-foundation-rs", rev = "079665882507dd5e2ff77db3de5070c1f6c0fb85" }
# TODO - Remove when a new version of RustRocksDB is released
rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "39dc822dde743b2a26eb160b660e8fbdab079d49" }
[profile.dev]
split-debuginfo = "unpacked"

View File

@@ -42,6 +42,16 @@ script/zed_with_local_servers --release
If you trigger `cmd-alt-i`, Zed will copy a JSON representation of the current window contents to the clipboard. You can paste this in a tool like [DJSON](https://chrome.google.com/webstore/detail/djson-json-viewer-formatt/chaeijjekipecdajnijdldjjipaegdjc?hl=en) to navigate the state of on-screen elements in a structured way.
### Wasm Plugins
Zed has a Wasm-based plugin runtime which it currently uses to embed plugins. To compile Zed, you'll need to have the `wasm32-wasi` toolchain installed on your system. To install this toolchain, run:
```bash
rustup target add wasm32-wasi
```
Plugins can be found in the `plugins` folder in the root. For more information about how plugins work, check the [Plugin Guide](./crates/plugin_runtime/README.md) in `crates/plugin_runtime/README.md`.
## Roadmap
We will organize our efforts around the following major milestones. We'll create tracking issues for each of these milestones to detail the individual tasks that comprise them.

View File

@@ -0,0 +1,3 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 3.99999C8 4.31671 7.76023 4.57258 7.44352 4.57258H1.95565L3.8416 6.45853C4.06527 6.6822 4.06527 7.04454 3.8416 7.2682C3.72887 7.38004 3.58215 7.43551 3.43542 7.43551C3.2887 7.43551 3.14233 7.37959 3.03068 7.26776L0.16775 4.40483C-0.0559165 4.18116 -0.0559165 3.81883 0.16775 3.59516L3.03068 0.732233C3.25434 0.508567 3.61668 0.508567 3.84035 0.732233C4.06401 0.955899 4.06401 1.31824 3.84035 1.5419L1.95565 3.42741H7.44352C7.76023 3.42741 8 3.68328 8 3.99999Z" fill="#839496"/>
</svg>

After

Width:  |  Height:  |  Size: 589 B

View File

@@ -0,0 +1,3 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.83265 4.40382L4.97532 7.26115C4.8646 7.37365 4.71816 7.42901 4.57172 7.42901C4.42528 7.42901 4.2792 7.37321 4.16777 7.26159C3.94454 7.03836 3.94454 6.67673 4.16777 6.4535L6.05039 4.57169H0.571465C0.255909 4.57169 0 4.31631 0 4.00022C0 3.68413 0.255731 3.42876 0.571287 3.42876H6.05021L4.16795 1.54649C3.94472 1.32326 3.94472 0.961634 4.16795 0.738405C4.39117 0.515177 4.75281 0.515177 4.97603 0.738405L7.83336 3.59573C8.0557 3.81985 8.0557 4.18059 7.83247 4.40382H7.83265Z" fill="#FDF6E3"/>
</svg>

After

Width:  |  Height:  |  Size: 602 B

3
assets/icons/split.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="12" height="10" viewBox="0 0 12 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.8 0.800476C11.4619 0.800476 12 1.33766 12 2.00048V8.00048C12 8.66235 11.4619 9.20048 10.8 9.20048H1.2C0.537188 9.20048 0 8.66235 0 8.00048V2.00048C0 1.33766 0.537188 0.800476 1.2 0.800476H10.8ZM3.6 2.00048H1.2V8.00048H3.6V2.00048ZM4.8 8.00048H7.2V2.00048H4.8V8.00048ZM10.8 2.00048H8.4V8.00048H10.8V2.00048Z" fill="#8B8792"/>
</svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 3.25C1 2.42148 1.67148 1.75 2.5 1.75H11.5C12.3273 1.75 13 2.42148 13 3.25V10.75C13 11.5773 12.3273 12.25 11.5 12.25H2.5C1.67148 12.25 1 11.5773 1 10.75V3.25ZM3.39766 4.55781C3.18789 4.7875 3.20336 5.14141 3.43281 5.35234L5.23047 7L3.43281 8.64766C3.20336 8.85859 3.18789 9.2125 3.39766 9.44219C3.60859 9.65078 3.9625 9.68594 4.19219 9.47734L6.44219 7.41484C6.55937 7.30703 6.625 7.15703 6.625 6.97891C6.625 6.84297 6.55937 6.69297 6.44219 6.58516L4.19219 4.52266C3.9625 4.31406 3.60859 4.32813 3.39766 4.55781V4.55781ZM6.8125 8.875C6.50078 8.875 6.25 9.12578 6.25 9.4375C6.25 9.74922 6.50078 10 6.8125 10H10.1875C10.4992 10 10.75 9.74922 10.75 9.4375C10.75 9.12578 10.4992 8.875 10.1875 8.875H6.8125Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 832 B

View File

@@ -409,6 +409,7 @@
"bindings": {
"ctrl-c": "terminal::Sigint",
"escape": "terminal::Escape",
"shift-escape": "terminal::DeployModal",
"ctrl-d": "terminal::Quit",
"backspace": "terminal::Del",
"enter": "terminal::Return",
@@ -417,7 +418,9 @@
"up": "terminal::Up",
"down": "terminal::Down",
"tab": "terminal::Tab",
"cmd-v": "terminal::Paste"
"cmd-v": "terminal::Paste",
"cmd-c": "terminal::Copy",
"ctrl-l": "terminal::Clear"
}
}
]

View File

@@ -140,7 +140,8 @@
"c": "vim::VisualChange",
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",
"y": "vim::VisualYank"
"y": "vim::VisualYank",
"p": "vim::VisualPaste"
}
},
{

View File

@@ -0,0 +1,99 @@
{
// The name of the Zed theme to use for the UI
"theme": "cave-dark",
// The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Mono",
// The default font size for text in the editor
"buffer_font_size": 15,
// Whether to enable vim modes and key bindings
"vim_mode": false,
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
"hover_popover_enabled": true,
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
// Whether new projects should start out 'online'. Online projects
// appear in the contacts panel under your name, so that your contacts
// can see which projects you are working on. Regardless of this
// setting, projects keep their last online status when you reopen them.
"projects_online_by_default": true,
// Whether to use language servers to provide code intelligence.
"enable_language_server": true,
// When to automatically save edited buffers. This setting can
// take four values.
//
// 1. Never automatically save:
// "autosave": "off",
// 2. Save when changing focus away from the Zed window:
// "autosave": "on_window_change",
// 3. Save when changing focus away from a specific buffer:
// "autosave": "on_focus_change",
// 4. Save when idle for a certain amount of time:
// "autosave": { "after_delay": {"milliseconds": 500} },
"autosave": "off",
// How to auto-format modified buffers when saving them. This
// setting can take three values:
//
// 1. Don't format code
// "format_on_save": "off"
// 2. Format code using the current language server:
// "format_on_save": "language_server"
// 3. Format code using an external command:
// "format_on_save": {
// "external": {
// "command": "sed",
// "arguments": ["-e", "s/ *$//"]
// }
// },
"format_on_save": "language_server",
// How to soft-wrap long lines of text. This setting can take
// three values:
//
// 1. Do not soft wrap.
// "soft_wrap": "none",
// 2. Soft wrap lines that overflow the editor:
// "soft_wrap": "editor_width",
// 2. Soft wrap lines at the preferred line length
// "soft_wrap": "preferred_line_length",
"soft_wrap": "none",
// The column at which to soft-wrap lines, for buffers where soft-wrap
// is enabled.
"preferred_line_length": 80,
// Whether to indent lines using tab characters, as opposed to multiple
// spaces.
"hard_tabs": false,
// How many columns a tab should occupy.
"tab_size": 4,
// Different settings for specific languages.
"languages": {
"Plain Text": {
"soft_wrap": "preferred_line_length"
},
"C": {
"tab_size": 2
},
"C++": {
"tab_size": 2
},
"Go": {
"tab_size": 4,
"hard_tabs": true
},
"Markdown": {
"soft_wrap": "preferred_line_length"
},
"Rust": {
"tab_size": 4
},
"JavaScript": {
"tab_size": 2
},
"TypeScript": {
"tab_size": 2
},
"TSX": {
"tab_size": 2
}
}
}

View File

@@ -0,0 +1,8 @@
// Zed settings
//
// For information on how to configure Zed, see the Zed
// documentation: https://zed.dev/docs/configuring-zed
//
// To see all of Zed's default settings without changing your
// custom settings, run the `open default settings` command
// from the command palette or from `Zed` application menu.

View File

@@ -549,7 +549,7 @@ impl Client {
client.respond_with_error(
receipt,
proto::Error {
message: error.to_string(),
message: format!("{:?}", error),
},
)?;
Err(error)

View File

@@ -35,7 +35,7 @@ use project::{
use rand::prelude::*;
use rpc::PeerId;
use serde_json::json;
use settings::Settings;
use settings::{FormatOnSave, Settings};
use sqlx::types::time::OffsetDateTime;
use std::{
cell::RefCell,
@@ -267,7 +267,8 @@ async fn test_host_disconnect(
cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
) {
cx_a.foreground().forbid_parking();
cx_b.update(editor::init);
deterministic.forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
@@ -298,10 +299,23 @@ async fn test_host_disconnect(
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
project_b
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
let (_, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "b.txt"), true, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
cx_b.read(|cx| {
assert_eq!(
cx.focused_view_id(workspace_b.window_id()),
Some(editor_b.id())
);
});
editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
assert!(cx_b.is_window_edited(workspace_b.window_id()));
// Request to join that project as client C
let project_c = cx_c.spawn(|cx| {
@@ -328,14 +342,31 @@ async fn test_host_disconnect(
.condition(cx_b, |project, _| project.is_read_only())
.await;
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
cx_b.update(|_| {
drop(project_b);
});
assert!(matches!(
project_c.await.unwrap_err(),
project::JoinProjectError::HostWentOffline
));
// Ensure client B's edited state is reset and that the whole window is blurred.
cx_b.read(|cx| {
assert_eq!(cx.focused_view_id(workspace_b.window_id()), None);
});
assert!(!cx_b.is_window_edited(workspace_b.window_id()));
// Ensure client B is not prompted to save edits when closing window after disconnecting.
workspace_b
.update(cx_b, |workspace, cx| {
workspace.close(&Default::default(), cx)
})
.unwrap()
.await
.unwrap();
assert_eq!(cx_b.window_ids().len(), 0);
cx_b.update(|_| {
drop(workspace_b);
drop(project_b);
});
// Ensure guests can still join.
let project_b2 = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
@@ -1440,7 +1471,7 @@ async fn test_collaborating_with_diagnostics(
},
Some(tree_sitter_rust::language()),
);
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
client_a.language_registry.add(Arc::new(language));
// Share a project as client A
@@ -1675,16 +1706,18 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
},
Some(tree_sitter_rust::language()),
);
let mut fake_language_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string()]),
let mut fake_language_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string()]),
..Default::default()
}),
..Default::default()
}),
},
..Default::default()
},
..Default::default()
});
}))
.await;
client_a.language_registry.add(Arc::new(language));
client_a
@@ -1912,7 +1945,6 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te
#[gpui::test(iterations = 10)]
async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
@@ -1929,14 +1961,18 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
},
Some(tree_sitter_rust::language()),
);
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
client_a.language_registry.add(Arc::new(language));
// Here we insert a fake tree with a directory that exists on disk. This is needed
// because later we'll invoke a command, which requires passing a working directory
// that points to a valid location on disk.
let directory = env::current_dir().unwrap();
client_a
.fs
.insert_tree("/a", json!({ "a.rs": "let one = two" }))
.insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
let buffer_b = cx_b
@@ -1967,7 +2003,28 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
.unwrap();
assert_eq!(
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
"let honey = two"
"let honey = \"two\""
);
// Ensure buffer can be formatted using an external command. Notice how the
// host's configuration is honored as opposed to using the guest's settings.
cx_a.update(|cx| {
cx.update_global(|settings: &mut Settings, _| {
settings.editor_defaults.format_on_save = Some(FormatOnSave::External {
command: "awk".to_string(),
arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()],
});
});
});
project_b
.update(cx_b, |project, cx| {
project.format(HashSet::from_iter([buffer_b.clone()]), true, cx)
})
.await
.unwrap();
assert_eq!(
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
);
}
@@ -1990,7 +2047,7 @@ async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
},
Some(tree_sitter_rust::language()),
);
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
client_a.language_registry.add(Arc::new(language));
client_a
@@ -2099,7 +2156,7 @@ async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
},
Some(tree_sitter_rust::language()),
);
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
client_a.language_registry.add(Arc::new(language));
client_a
@@ -2279,7 +2336,7 @@ async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppC
},
Some(tree_sitter_rust::language()),
);
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
client_a.language_registry.add(Arc::new(language));
let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
@@ -2376,7 +2433,7 @@ async fn test_lsp_hover(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
},
Some(tree_sitter_rust::language()),
);
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
client_a.language_registry.add(Arc::new(language));
let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
@@ -2464,7 +2521,7 @@ async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
},
Some(tree_sitter_rust::language()),
);
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
client_a.language_registry.add(Arc::new(language));
client_a
@@ -2567,7 +2624,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
},
Some(tree_sitter_rust::language()),
);
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
client_a.language_registry.add(Arc::new(language));
client_a
@@ -2638,7 +2695,7 @@ async fn test_collaborating_with_code_actions(
},
Some(tree_sitter_rust::language()),
);
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
client_a.language_registry.add(Arc::new(language));
client_a
@@ -2843,16 +2900,18 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
},
Some(tree_sitter_rust::language()),
);
let mut fake_language_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: Default::default(),
})),
let mut fake_language_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: Default::default(),
})),
..Default::default()
},
..Default::default()
},
..Default::default()
});
}))
.await;
client_a.language_registry.add(Arc::new(language));
client_a
@@ -3027,10 +3086,12 @@ async fn test_language_server_statuses(
},
Some(tree_sitter_rust::language()),
);
let mut fake_language_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
name: "the-language-server",
..Default::default()
});
let mut fake_language_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
name: "the-language-server",
..Default::default()
}))
.await;
client_a.language_registry.add(Arc::new(language));
client_a
@@ -4553,119 +4614,124 @@ async fn test_random_collaboration(
},
None,
);
let _fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
name: "the-fake-language-server",
capabilities: lsp::LanguageServer::full_capabilities(),
initializer: Some(Box::new({
let rng = rng.clone();
let fs = fs.clone();
let project = host_project.downgrade();
move |fake_server: &mut FakeLanguageServer| {
fake_server.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
Ok(Some(lsp::CompletionResponse::Array(vec![
lsp::CompletionItem {
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(
lsp::Position::new(0, 0),
lsp::Position::new(0, 0),
),
new_text: "the-new-text".to_string(),
})),
..Default::default()
let _fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
name: "the-fake-language-server",
capabilities: lsp::LanguageServer::full_capabilities(),
initializer: Some(Box::new({
let rng = rng.clone();
let fs = fs.clone();
let project = host_project.downgrade();
move |fake_server: &mut FakeLanguageServer| {
fake_server.handle_request::<lsp::request::Completion, _, _>(
|_, _| async move {
Ok(Some(lsp::CompletionResponse::Array(vec![
lsp::CompletionItem {
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(
lsp::Position::new(0, 0),
lsp::Position::new(0, 0),
),
new_text: "the-new-text".to_string(),
})),
..Default::default()
},
])))
},
])))
});
);
fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
|_, _| async move {
Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
lsp::CodeAction {
title: "the-code-action".to_string(),
..Default::default()
},
)]))
},
);
fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
|_, _| async move {
Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
lsp::CodeAction {
title: "the-code-action".to_string(),
..Default::default()
},
)]))
},
);
fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
|params, _| async move {
Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
params.position,
params.position,
))))
},
);
fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
|params, _| async move {
Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
params.position,
params.position,
))))
},
);
fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
let fs = fs.clone();
let rng = rng.clone();
move |_, _| {
fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
let fs = fs.clone();
let rng = rng.clone();
async move {
let files = fs.files().await;
let mut rng = rng.lock();
let count = rng.gen_range::<usize, _>(1..3);
let files = (0..count)
.map(|_| files.choose(&mut *rng).unwrap())
.collect::<Vec<_>>();
log::info!("LSP: Returning definitions in files {:?}", &files);
Ok(Some(lsp::GotoDefinitionResponse::Array(
files
.into_iter()
.map(|file| lsp::Location {
uri: lsp::Url::from_file_path(file).unwrap(),
range: Default::default(),
})
.collect(),
)))
move |_, _| {
let fs = fs.clone();
let rng = rng.clone();
async move {
let files = fs.files().await;
let mut rng = rng.lock();
let count = rng.gen_range::<usize, _>(1..3);
let files = (0..count)
.map(|_| files.choose(&mut *rng).unwrap())
.collect::<Vec<_>>();
log::info!("LSP: Returning definitions in files {:?}", &files);
Ok(Some(lsp::GotoDefinitionResponse::Array(
files
.into_iter()
.map(|file| lsp::Location {
uri: lsp::Url::from_file_path(file).unwrap(),
range: Default::default(),
})
.collect(),
)))
}
}
}
});
});
fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
let rng = rng.clone();
let project = project.clone();
move |params, mut cx| {
let highlights = if let Some(project) = project.upgrade(&cx) {
project.update(&mut cx, |project, cx| {
let path = params
.text_document_position_params
.text_document
.uri
.to_file_path()
.unwrap();
let (worktree, relative_path) =
project.find_local_worktree(&path, cx)?;
let project_path =
ProjectPath::from((worktree.read(cx).id(), relative_path));
let buffer = project.get_open_buffer(&project_path, cx)?.read(cx);
fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
let rng = rng.clone();
let project = project.clone();
move |params, mut cx| {
let highlights = if let Some(project) = project.upgrade(&cx) {
project.update(&mut cx, |project, cx| {
let path = params
.text_document_position_params
.text_document
.uri
.to_file_path()
.unwrap();
let (worktree, relative_path) =
project.find_local_worktree(&path, cx)?;
let project_path =
ProjectPath::from((worktree.read(cx).id(), relative_path));
let buffer =
project.get_open_buffer(&project_path, cx)?.read(cx);
let mut highlights = Vec::new();
let highlight_count = rng.lock().gen_range(1..=5);
let mut prev_end = 0;
for _ in 0..highlight_count {
let range =
buffer.random_byte_range(prev_end, &mut *rng.lock());
let mut highlights = Vec::new();
let highlight_count = rng.lock().gen_range(1..=5);
let mut prev_end = 0;
for _ in 0..highlight_count {
let range =
buffer.random_byte_range(prev_end, &mut *rng.lock());
highlights.push(lsp::DocumentHighlight {
range: range_to_lsp(range.to_point_utf16(buffer)),
kind: Some(lsp::DocumentHighlightKind::READ),
});
prev_end = range.end;
}
Some(highlights)
})
} else {
None
};
async move { Ok(highlights) }
}
});
}
})),
..Default::default()
});
highlights.push(lsp::DocumentHighlight {
range: range_to_lsp(range.to_point_utf16(buffer)),
kind: Some(lsp::DocumentHighlightKind::READ),
});
prev_end = range.end;
}
Some(highlights)
})
} else {
None
};
async move { Ok(highlights) }
}
});
}
})),
..Default::default()
}))
.await;
host_language_registry.add(Arc::new(language));
let op_start_signal = futures::channel::mpsc::unbounded();

View File

@@ -1,18 +1,23 @@
use std::{any::TypeId, time::Duration};
use gpui::{
elements::*, geometry::vector::Vector2F, keymap, platform::CursorStyle, Action, AppContext,
Axis, Entity, MutableAppContext, RenderContext, SizeConstraint, Subscription, View,
ViewContext,
elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap, platform::CursorStyle,
Action, AppContext, Axis, Entity, MutableAppContext, RenderContext, SizeConstraint,
Subscription, View, ViewContext,
};
use menu::*;
use settings::Settings;
use std::{any::TypeId, time::Duration};
#[derive(Copy, Clone, PartialEq)]
struct Clicked;
impl_internal_actions!(context_menu, [Clicked]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContextMenu::select_first);
cx.add_action(ContextMenu::select_last);
cx.add_action(ContextMenu::select_next);
cx.add_action(ContextMenu::select_prev);
cx.add_action(ContextMenu::clicked);
cx.add_action(ContextMenu::confirm);
cx.add_action(ContextMenu::cancel);
}
@@ -56,6 +61,7 @@ pub struct ContextMenu {
selected_index: Option<usize>,
visible: bool,
previously_focused_view_id: Option<usize>,
clicked: bool,
_actions_observation: Subscription,
}
@@ -113,32 +119,46 @@ impl ContextMenu {
selected_index: Default::default(),
visible: Default::default(),
previously_focused_view_id: Default::default(),
clicked: false,
_actions_observation: cx.observe_actions(Self::action_dispatched),
}
}
pub fn visible(&self) -> bool {
self.visible
}
fn action_dispatched(&mut self, action_id: TypeId, cx: &mut ViewContext<Self>) {
if let Some(ix) = self
.items
.iter()
.position(|item| item.action_id() == Some(action_id))
{
self.selected_index = Some(ix);
cx.notify();
cx.spawn(|this, mut cx| async move {
cx.background().timer(Duration::from_millis(100)).await;
this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx));
})
.detach();
if self.clicked {
self.cancel(&Default::default(), cx);
} else {
self.selected_index = Some(ix);
cx.notify();
cx.spawn(|this, mut cx| async move {
cx.background().timer(Duration::from_millis(50)).await;
this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx));
})
.detach();
}
}
}
fn clicked(&mut self, _: &Clicked, _: &mut ViewContext<Self>) {
self.clicked = true;
}
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.selected_index {
if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
let window_id = cx.window_id();
let view_id = cx.view_id();
cx.dispatch_action_at(window_id, view_id, action.as_ref());
self.reset(cx);
}
}
}
@@ -158,6 +178,7 @@ impl ContextMenu {
self.items.clear();
self.visible = false;
self.selected_index.take();
self.clicked = false;
cx.notify();
}
@@ -277,6 +298,8 @@ impl ContextMenu {
.boxed(),
}
}))
.contained()
.with_margin_left(style.keystroke_margin)
.boxed(),
)
.contained()
@@ -315,8 +338,8 @@ impl ContextMenu {
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(move |_, _, cx| {
cx.dispatch_action(Clicked);
cx.dispatch_any_action(action.boxed_clone());
cx.dispatch_action(Cancel);
})
.boxed()
}

View File

@@ -568,10 +568,11 @@ impl workspace::Item for ProjectDiagnosticsEditor {
}
fn should_update_tab_on_event(event: &Event) -> bool {
matches!(
event,
Event::Saved | Event::DirtyChanged | Event::TitleChanged
)
Editor::should_update_tab_on_event(event)
}
fn is_edit_event(event: &Self::Event) -> bool {
Editor::is_edit_event(event)
}
fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {

View File

@@ -23,6 +23,7 @@ test-support = [
text = { path = "../text" }
clock = { path = "../clock" }
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
language = { path = "../language" }

View File

@@ -983,7 +983,7 @@ pub mod tests {
language.set_theme(&theme);
cx.update(|cx| {
let mut settings = Settings::test(cx);
settings.language_settings.tab_size = Some(2.try_into().unwrap());
settings.editor_defaults.tab_size = Some(2.try_into().unwrap());
cx.set_global(settings);
});

View File

@@ -4,6 +4,7 @@ mod highlight_matching_bracket;
mod hover_popover;
pub mod items;
mod link_go_to_definition;
mod mouse_context_menu;
pub mod movement;
mod multi_buffer;
pub mod selections_collection;
@@ -18,7 +19,6 @@ use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
pub use display_map::DisplayPoint;
use display_map::*;
pub use element::*;
use futures::{channel::oneshot, FutureExt};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions,
@@ -51,7 +51,7 @@ use ordered_float::OrderedFloat;
use project::{LocationLink, Project, ProjectPath, ProjectTransaction};
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
use serde::{Deserialize, Serialize};
use settings::{Autosave, Settings};
use settings::Settings;
use smallvec::SmallVec;
use smol::Timer;
use snippet::Snippet;
@@ -320,6 +320,7 @@ pub fn init(cx: &mut MutableAppContext) {
hover_popover::init(cx);
link_go_to_definition::init(cx);
mouse_context_menu::init(cx);
workspace::register_project_item::<Editor>(cx);
workspace::register_followable_item::<Editor>(cx);
@@ -426,6 +427,7 @@ pub struct Editor {
background_highlights: BTreeMap<TypeId, (fn(&Theme) -> Color, Vec<Range<Anchor>>)>,
nav_history: Option<ItemNavHistory>,
context_menu: Option<ContextMenu>,
mouse_context_menu: ViewHandle<context_menu::ContextMenu>,
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
next_completion_id: CompletionId,
available_code_actions: Option<(ModelHandle<Buffer>, Arc<[CodeAction]>)>,
@@ -439,8 +441,6 @@ pub struct Editor {
leader_replica_id: Option<u16>,
hover_state: HoverState,
link_go_to_definition_state: LinkGoToDefinitionState,
pending_autosave: Option<Task<Option<()>>>,
cancel_pending_autosave: Option<oneshot::Sender<()>>,
_subscriptions: Vec<Subscription>,
}
@@ -1013,11 +1013,11 @@ impl Editor {
background_highlights: Default::default(),
nav_history: None,
context_menu: None,
mouse_context_menu: cx.add_view(|cx| context_menu::ContextMenu::new(cx)),
completion_tasks: Default::default(),
next_completion_id: 0,
available_code_actions: Default::default(),
code_actions_task: Default::default(),
document_highlights_task: Default::default(),
pending_rename: Default::default(),
searchable: true,
@@ -1028,13 +1028,10 @@ impl Editor {
leader_replica_id: None,
hover_state: Default::default(),
link_go_to_definition_state: Default::default(),
pending_autosave: Default::default(),
cancel_pending_autosave: Default::default(),
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
cx.subscribe(&buffer, Self::on_buffer_event),
cx.observe(&display_map, Self::on_display_map_changed),
cx.observe_window_activation(Self::on_window_activation_changed),
],
};
this.end_selection(cx);
@@ -1602,7 +1599,7 @@ impl Editor {
s.delete(newest_selection.id)
}
s.set_pending_range(start..end, mode);
s.set_pending_anchor_range(start..end, mode);
});
}
@@ -1943,6 +1940,10 @@ impl Editor {
}
fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
if !cx.global::<Settings>().show_completions_on_input {
return;
}
let selection = self.selections.newest_anchor();
if self
.buffer
@@ -4071,13 +4072,16 @@ impl Editor {
}
}
nav_history.push(Some(NavigationData {
cursor_anchor: position,
cursor_position: point,
scroll_position: self.scroll_position,
scroll_top_anchor: self.scroll_top_anchor.clone(),
scroll_top_row,
}));
nav_history.push(
Some(NavigationData {
cursor_anchor: position,
cursor_position: point,
scroll_position: self.scroll_position,
scroll_top_anchor: self.scroll_top_anchor.clone(),
scroll_top_row,
}),
cx,
);
}
}
@@ -4675,7 +4679,7 @@ impl Editor {
definitions: Vec<LocationLink>,
cx: &mut ViewContext<Workspace>,
) {
let nav_history = workspace.active_pane().read(cx).nav_history().clone();
let pane = workspace.active_pane().clone();
for definition in definitions {
let range = definition
.target
@@ -4687,13 +4691,13 @@ impl Editor {
// When selecting a definition in a different buffer, disable the nav history
// to avoid creating a history entry at the previous cursor location.
if editor_handle != target_editor_handle {
nav_history.borrow_mut().disable();
pane.update(cx, |pane, _| pane.disable_history());
}
target_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
s.select_ranges([range]);
});
nav_history.borrow_mut().enable();
pane.update(cx, |pane, _| pane.enable_history());
});
}
}
@@ -4982,6 +4986,8 @@ impl Editor {
self.change_selections(None, cx, |s| {
s.select_ranges(vec![cursor_in_editor..cursor_in_editor])
});
} else {
self.refresh_document_highlights(cx);
}
Some(rename)
@@ -5584,33 +5590,6 @@ impl Editor {
self.refresh_active_diagnostics(cx);
self.refresh_code_actions(cx);
cx.emit(Event::BufferEdited);
if let Autosave::AfterDelay { milliseconds } = cx.global::<Settings>().autosave {
let pending_autosave =
self.pending_autosave.take().unwrap_or(Task::ready(None));
if let Some(cancel_pending_autosave) = self.cancel_pending_autosave.take() {
let _ = cancel_pending_autosave.send(());
}
let (cancel_tx, mut cancel_rx) = oneshot::channel();
self.cancel_pending_autosave = Some(cancel_tx);
self.pending_autosave = Some(cx.spawn_weak(|this, mut cx| async move {
let mut timer = cx
.background()
.timer(Duration::from_millis(milliseconds))
.fuse();
pending_autosave.await;
futures::select_biased! {
_ = cancel_rx => return None,
_ = timer => {}
}
this.upgrade(&cx)?
.update(&mut cx, |this, cx| this.autosave(cx))
.await
.log_err();
None
}));
}
}
language::Event::Reparsed => cx.emit(Event::Reparsed),
language::Event::DirtyChanged => cx.emit(Event::DirtyChanged),
@@ -5629,25 +5608,6 @@ impl Editor {
cx.notify();
}
fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
if !active && cx.global::<Settings>().autosave == Autosave::OnWindowChange {
self.autosave(cx).detach_and_log_err(cx);
}
}
fn autosave(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
if let Some(project) = self.project.clone() {
if self.buffer.read(cx).is_dirty(cx)
&& !self.buffer.read(cx).has_conflict(cx)
&& workspace::Item::can_save(self, cx)
{
return workspace::Item::save(self, project, cx);
}
}
Task::ready(Ok(()))
}
pub fn set_searchable(&mut self, searchable: bool) {
self.searchable = searchable;
}
@@ -5693,8 +5653,8 @@ impl Editor {
editor_handle.update(cx, |editor, cx| {
editor.push_to_nav_history(editor.selections.newest_anchor().head(), None, cx);
});
let nav_history = workspace.active_pane().read(cx).nav_history().clone();
nav_history.borrow_mut().disable();
let pane = workspace.active_pane().clone();
pane.update(cx, |pane, _| pane.disable_history());
// We defer the pane interaction because we ourselves are a workspace item
// and activating a new item causes the pane to call a method on us reentrantly,
@@ -5709,7 +5669,7 @@ impl Editor {
});
}
nav_history.borrow_mut().enable();
pane.update(cx, |pane, _| pane.enable_history());
});
}
@@ -5827,7 +5787,12 @@ impl View for Editor {
});
}
EditorElement::new(self.handle.clone(), style.clone(), self.cursor_shape).boxed()
Stack::new()
.with_child(
EditorElement::new(self.handle.clone(), style.clone(), self.cursor_shape).boxed(),
)
.with_child(ChildView::new(&self.mouse_context_menu).boxed())
.boxed()
}
fn ui_name() -> &'static str {
@@ -5865,10 +5830,6 @@ impl View for Editor {
hide_hover(self, cx);
cx.emit(Event::Blurred);
cx.notify();
if cx.global::<Settings>().autosave == Autosave::OnFocusChange {
self.autosave(cx).detach_and_log_err(cx);
}
}
fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
@@ -6276,29 +6237,30 @@ pub fn styled_runs_for_code_label<'a>(
#[cfg(test)]
mod tests {
use crate::test::{
assert_text_with_selections, build_editor, select_ranges, EditorTestContext,
assert_text_with_selections, build_editor, select_ranges, EditorLspTestContext,
EditorTestContext,
};
use super::*;
use futures::StreamExt;
use gpui::{
executor::Deterministic,
geometry::rect::RectF,
platform::{WindowBounds, WindowOptions},
};
use indoc::indoc;
use language::{FakeLspAdapter, LanguageConfig};
use lsp::FakeLanguageServer;
use project::{FakeFs, Fs};
use settings::LanguageSettings;
use std::{cell::RefCell, path::Path, rc::Rc, time::Instant};
use project::FakeFs;
use settings::EditorSettings;
use std::{cell::RefCell, rc::Rc, time::Instant};
use text::Point;
use unindent::Unindent;
use util::{
assert_set_eq,
test::{marked_text_by, marked_text_ranges, marked_text_ranges_by, sample_text},
test::{
marked_text_by, marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker,
},
};
use workspace::{FollowableItem, Item, ItemHandle};
use workspace::{FollowableItem, ItemHandle, NavigationEntry, Pane};
#[gpui::test]
fn test_edit_events(cx: &mut MutableAppContext) {
@@ -6646,12 +6608,20 @@ mod tests {
fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
use workspace::Item;
let nav_history = Rc::new(RefCell::new(workspace::NavHistory::default()));
let pane = cx.add_view(Default::default(), |cx| Pane::new(cx));
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
cx.add_window(Default::default(), |cx| {
let mut editor = build_editor(buffer.clone(), cx);
editor.nav_history = Some(ItemNavHistory::new(nav_history.clone(), &cx.handle()));
let handle = cx.handle();
editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
fn pop_history(
editor: &mut Editor,
cx: &mut MutableAppContext,
) -> Option<NavigationEntry> {
editor.nav_history.as_mut().unwrap().pop_backward(cx)
}
// Move the cursor a small distance.
// Nothing is added to the navigation history.
@@ -6661,21 +6631,21 @@ mod tests {
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
});
assert!(nav_history.borrow_mut().pop_backward().is_none());
assert!(pop_history(&mut editor, cx).is_none());
// Move the cursor a large distance.
// The history can jump back to the previous position.
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)])
});
let nav_entry = nav_history.borrow_mut().pop_backward().unwrap();
let nav_entry = pop_history(&mut editor, cx).unwrap();
editor.navigate(nav_entry.data.unwrap(), cx);
assert_eq!(nav_entry.item.id(), cx.view_id());
assert_eq!(
editor.selections.display_ranges(cx),
&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]
);
assert!(nav_history.borrow_mut().pop_backward().is_none());
assert!(pop_history(&mut editor, cx).is_none());
// Move the cursor a small distance via the mouse.
// Nothing is added to the navigation history.
@@ -6685,7 +6655,7 @@ mod tests {
editor.selections.display_ranges(cx),
&[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
);
assert!(nav_history.borrow_mut().pop_backward().is_none());
assert!(pop_history(&mut editor, cx).is_none());
// Move the cursor a large distance via the mouse.
// The history can jump back to the previous position.
@@ -6695,14 +6665,14 @@ mod tests {
editor.selections.display_ranges(cx),
&[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]
);
let nav_entry = nav_history.borrow_mut().pop_backward().unwrap();
let nav_entry = pop_history(&mut editor, cx).unwrap();
editor.navigate(nav_entry.data.unwrap(), cx);
assert_eq!(nav_entry.item.id(), cx.view_id());
assert_eq!(
editor.selections.display_ranges(cx),
&[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
);
assert!(nav_history.borrow_mut().pop_backward().is_none());
assert!(pop_history(&mut editor, cx).is_none());
// Set scroll position to check later
editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx);
@@ -6715,7 +6685,7 @@ mod tests {
assert_ne!(editor.scroll_position, original_scroll_position);
assert_ne!(editor.scroll_top_anchor, original_scroll_top_anchor);
let nav_entry = nav_history.borrow_mut().pop_backward().unwrap();
let nav_entry = pop_history(&mut editor, cx).unwrap();
editor.navigate(nav_entry.data.unwrap(), cx);
assert_eq!(editor.scroll_position, original_scroll_position);
assert_eq!(editor.scroll_top_anchor, original_scroll_top_anchor);
@@ -7659,7 +7629,7 @@ mod tests {
let mut cx = EditorTestContext::new(cx).await;
cx.update(|cx| {
cx.update_global::<Settings, _, _>(|settings, _| {
settings.language_settings.hard_tabs = Some(true);
settings.editor_overrides.hard_tabs = Some(true);
});
});
@@ -7742,14 +7712,14 @@ mod tests {
Settings::test(cx)
.with_language_defaults(
"TOML",
LanguageSettings {
EditorSettings {
tab_size: Some(2.try_into().unwrap()),
..Default::default()
},
)
.with_language_defaults(
"Rust",
LanguageSettings {
EditorSettings {
tab_size: Some(4.try_into().unwrap()),
..Default::default()
},
@@ -8278,7 +8248,7 @@ mod tests {
fox ju|mps over
the lazy dog"});
cx.update_editor(|e, cx| e.copy(&Copy, cx));
cx.assert_clipboard_content(Some("fox jumps over\n"));
cx.cx.assert_clipboard_content(Some("fox jumps over\n"));
// Paste with three selections, noticing how the copied full-line selection is inserted
// before the empty selections but replaces the selection that is non-empty.
@@ -9348,13 +9318,15 @@ mod tests {
},
Some(tree_sitter_rust::language()),
);
let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
document_formatting_provider: Some(lsp::OneOf::Left(true)),
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
document_formatting_provider: Some(lsp::OneOf::Left(true)),
..Default::default()
},
..Default::default()
},
..Default::default()
});
}))
.await;
let fs = FakeFs::new(cx.background().clone());
fs.insert_file("/file.rs", Default::default()).await;
@@ -9424,7 +9396,7 @@ mod tests {
cx.update_global::<Settings, _, _>(|settings, _| {
settings.language_overrides.insert(
"Rust".into(),
LanguageSettings {
EditorSettings {
tab_size: Some(8.try_into().unwrap()),
..Default::default()
},
@@ -9460,13 +9432,15 @@ mod tests {
},
Some(tree_sitter_rust::language()),
);
let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
..Default::default()
},
..Default::default()
},
..Default::default()
});
}))
.await;
let fs = FakeFs::new(cx.background().clone());
fs.insert_file("/file.rs", Default::default()).await;
@@ -9538,7 +9512,7 @@ mod tests {
cx.update_global::<Settings, _, _>(|settings, _| {
settings.language_overrides.insert(
"Rust".into(),
LanguageSettings {
EditorSettings {
tab_size: Some(8.try_into().unwrap()),
..Default::default()
},
@@ -9562,265 +9536,184 @@ mod tests {
save.await.unwrap();
}
#[gpui::test]
async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
deterministic.forbid_parking();
let fs = FakeFs::new(cx.background().clone());
fs.insert_file("/file.rs", Default::default()).await;
let project = Project::test(fs.clone(), ["/file.rs".as_ref()], cx).await;
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
.await
.unwrap();
let (_, editor) = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx));
// Autosave on window change.
editor.update(cx, |editor, cx| {
cx.update_global(|settings: &mut Settings, _| {
settings.autosave = Autosave::OnWindowChange;
});
editor.insert("X", cx);
assert!(editor.is_dirty(cx))
});
// Deactivating the window saves the file.
cx.simulate_window_activation(None);
deterministic.run_until_parked();
assert_eq!(fs.load(Path::new("/file.rs")).await.unwrap(), "X");
editor.read_with(cx, |editor, cx| assert!(!editor.is_dirty(cx)));
// Autosave on focus change.
editor.update(cx, |editor, cx| {
cx.focus_self();
cx.update_global(|settings: &mut Settings, _| {
settings.autosave = Autosave::OnFocusChange;
});
editor.insert("X", cx);
assert!(editor.is_dirty(cx))
});
// Blurring the editor saves the file.
editor.update(cx, |_, cx| cx.blur());
deterministic.run_until_parked();
assert_eq!(fs.load(Path::new("/file.rs")).await.unwrap(), "XX");
editor.read_with(cx, |editor, cx| assert!(!editor.is_dirty(cx)));
// Autosave after delay.
editor.update(cx, |editor, cx| {
cx.update_global(|settings: &mut Settings, _| {
settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
});
editor.insert("X", cx);
assert!(editor.is_dirty(cx))
});
// Delay hasn't fully expired, so the file is still dirty and unsaved.
deterministic.advance_clock(Duration::from_millis(250));
assert_eq!(fs.load(Path::new("/file.rs")).await.unwrap(), "XX");
editor.read_with(cx, |editor, cx| assert!(editor.is_dirty(cx)));
// After delay expires, the file is saved.
deterministic.advance_clock(Duration::from_millis(250));
assert_eq!(fs.load(Path::new("/file.rs")).await.unwrap(), "XXX");
editor.read_with(cx, |editor, cx| assert!(!editor.is_dirty(cx)));
}
#[gpui::test]
async fn test_completion(cx: &mut gpui::TestAppContext) {
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
..Default::default()
}),
..Default::default()
},
..Default::default()
});
let text = "
one
two
three
"
.unindent();
let fs = FakeFs::new(cx.background().clone());
fs.insert_file("/file.rs", text).await;
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
.await
.unwrap();
let mut fake_server = fake_servers.next().await.unwrap();
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
editor.update(cx, |editor, cx| {
editor.project = Some(project);
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(0, 3)..Point::new(0, 3)])
});
editor.handle_input(&Input(".".to_string()), cx);
});
handle_completion_request(
&mut fake_server,
"/file.rs",
Point::new(0, 4),
vec![
(Point::new(0, 4)..Point::new(0, 4), "first_completion"),
(Point::new(0, 4)..Point::new(0, 4), "second_completion"),
],
cx,
)
.await;
editor
.condition(&cx, |editor, _| editor.context_menu_visible())
.await;
let apply_additional_edits = editor.update(cx, |editor, cx| {
cx.set_state(indoc! {"
one|
two
three"});
cx.simulate_keystroke(".");
handle_completion_request(
&mut cx,
indoc! {"
one.|<>
two
three"},
vec!["first_completion", "second_completion"],
)
.await;
cx.condition(|editor, _| editor.context_menu_visible())
.await;
let apply_additional_edits = cx.update_editor(|editor, cx| {
editor.move_down(&MoveDown, cx);
let apply_additional_edits = editor
editor
.confirm_completion(&ConfirmCompletion::default(), cx)
.unwrap();
assert_eq!(
editor.text(cx),
"
one.second_completion
two
three
"
.unindent()
);
apply_additional_edits
.unwrap()
});
cx.assert_editor_state(indoc! {"
one.second_completion|
two
three"});
handle_resolve_completion_request(
&mut fake_server,
Some((Point::new(2, 5)..Point::new(2, 5), "\nadditional edit")),
)
.await;
apply_additional_edits.await.unwrap();
assert_eq!(
editor.read_with(cx, |editor, cx| editor.text(cx)),
"
one.second_completion
two
three
additional edit
"
.unindent()
);
editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges([
Point::new(1, 3)..Point::new(1, 3),
Point::new(2, 5)..Point::new(2, 5),
])
});
editor.handle_input(&Input(" ".to_string()), cx);
assert!(editor.context_menu.is_none());
editor.handle_input(&Input("s".to_string()), cx);
assert!(editor.context_menu.is_none());
});
handle_completion_request(
&mut fake_server,
"/file.rs",
Point::new(2, 7),
vec![
(Point::new(2, 6)..Point::new(2, 7), "fourth_completion"),
(Point::new(2, 6)..Point::new(2, 7), "fifth_completion"),
(Point::new(2, 6)..Point::new(2, 7), "sixth_completion"),
],
)
.await;
editor
.condition(&cx, |editor, _| editor.context_menu_visible())
.await;
editor.update(cx, |editor, cx| {
editor.handle_input(&Input("i".to_string()), cx);
});
handle_completion_request(
&mut fake_server,
"/file.rs",
Point::new(2, 8),
vec![
(Point::new(2, 6)..Point::new(2, 8), "fourth_completion"),
(Point::new(2, 6)..Point::new(2, 8), "fifth_completion"),
(Point::new(2, 6)..Point::new(2, 8), "sixth_completion"),
],
)
.await;
editor
.condition(&cx, |editor, _| editor.context_menu_visible())
.await;
let apply_additional_edits = editor.update(cx, |editor, cx| {
let apply_additional_edits = editor
.confirm_completion(&ConfirmCompletion::default(), cx)
.unwrap();
assert_eq!(
editor.text(cx),
"
&mut cx,
Some((
indoc! {"
one.second_completion
two sixth_completion
three sixth_completion
additional edit
"
.unindent()
);
apply_additional_edits
two
three<>"},
"\nadditional edit",
)),
)
.await;
apply_additional_edits.await.unwrap();
cx.assert_editor_state(indoc! {"
one.second_completion|
two
three
additional edit"});
cx.set_state(indoc! {"
one.second_completion
two|
three|
additional edit"});
cx.simulate_keystroke(" ");
assert!(cx.editor(|e, _| e.context_menu.is_none()));
cx.simulate_keystroke("s");
assert!(cx.editor(|e, _| e.context_menu.is_none()));
cx.assert_editor_state(indoc! {"
one.second_completion
two s|
three s|
additional edit"});
handle_completion_request(
&mut cx,
indoc! {"
one.second_completion
two s
three <s|>
additional edit"},
vec!["fourth_completion", "fifth_completion", "sixth_completion"],
)
.await;
cx.condition(|editor, _| editor.context_menu_visible())
.await;
cx.simulate_keystroke("i");
handle_completion_request(
&mut cx,
indoc! {"
one.second_completion
two si
three <si|>
additional edit"},
vec!["fourth_completion", "fifth_completion", "sixth_completion"],
)
.await;
cx.condition(|editor, _| editor.context_menu_visible())
.await;
let apply_additional_edits = cx.update_editor(|editor, cx| {
editor
.confirm_completion(&ConfirmCompletion::default(), cx)
.unwrap()
});
handle_resolve_completion_request(&mut fake_server, None).await;
cx.assert_editor_state(indoc! {"
one.second_completion
two sixth_completion|
three sixth_completion|
additional edit"});
handle_resolve_completion_request(&mut cx, None).await;
apply_additional_edits.await.unwrap();
async fn handle_completion_request(
fake: &mut FakeLanguageServer,
path: &'static str,
position: Point,
completions: Vec<(Range<Point>, &'static str)>,
cx.update(|cx| {
cx.update_global::<Settings, _, _>(|settings, _| {
settings.show_completions_on_input = false;
})
});
cx.set_state("editor|");
cx.simulate_keystroke(".");
assert!(cx.editor(|e, _| e.context_menu.is_none()));
cx.simulate_keystrokes(["c", "l", "o"]);
cx.assert_editor_state("editor.clo|");
assert!(cx.editor(|e, _| e.context_menu.is_none()));
cx.update_editor(|editor, cx| {
editor.show_completions(&ShowCompletions, cx);
});
handle_completion_request(&mut cx, "editor.<clo|>", vec!["close", "clobber"]).await;
cx.condition(|editor, _| editor.context_menu_visible())
.await;
let apply_additional_edits = cx.update_editor(|editor, cx| {
editor
.confirm_completion(&ConfirmCompletion::default(), cx)
.unwrap()
});
cx.assert_editor_state("editor.close|");
handle_resolve_completion_request(&mut cx, None).await;
apply_additional_edits.await.unwrap();
// Handle completion request passing a marked string specifying where the completion
// should be triggered from using '|' character, what range should be replaced, and what completions
// should be returned using '<' and '>' to delimit the range
async fn handle_completion_request<'a>(
cx: &mut EditorLspTestContext<'a>,
marked_string: &str,
completions: Vec<&'static str>,
) {
fake.handle_request::<lsp::request::Completion, _, _>(move |params, _| {
let complete_from_marker: TextRangeMarker = '|'.into();
let replace_range_marker: TextRangeMarker = ('<', '>').into();
let (_, mut marked_ranges) = marked_text_ranges_by(
marked_string,
vec![complete_from_marker.clone(), replace_range_marker.clone()],
);
let complete_from_position =
cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
let replace_range =
cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
let completions = completions.clone();
async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path(path).unwrap()
);
assert_eq!(params.text_document_position.text_document.uri, url.clone());
assert_eq!(
params.text_document_position.position,
lsp::Position::new(position.row, position.column)
complete_from_position
);
Ok(Some(lsp::CompletionResponse::Array(
completions
.iter()
.map(|(range, new_text)| lsp::CompletionItem {
label: new_text.to_string(),
.map(|completion_text| lsp::CompletionItem {
label: completion_text.to_string(),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(
lsp::Position::new(range.start.row, range.start.column),
lsp::Position::new(range.start.row, range.start.column),
),
new_text: new_text.to_string(),
range: replace_range.clone(),
new_text: completion_text.to_string(),
})),
..Default::default()
})
@@ -9832,23 +9725,26 @@ mod tests {
.await;
}
async fn handle_resolve_completion_request(
fake: &mut FakeLanguageServer,
edit: Option<(Range<Point>, &'static str)>,
async fn handle_resolve_completion_request<'a>(
cx: &mut EditorLspTestContext<'a>,
edit: Option<(&'static str, &'static str)>,
) {
fake.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _| {
let edit = edit.map(|(marked_string, new_text)| {
let replace_range_marker: TextRangeMarker = ('<', '>').into();
let (_, mut marked_ranges) =
marked_text_ranges_by(marked_string, vec![replace_range_marker.clone()]);
let replace_range = cx
.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
vec![lsp::TextEdit::new(replace_range, new_text.to_string())]
});
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
let edit = edit.clone();
async move {
Ok(lsp::CompletionItem {
additional_text_edits: edit.map(|(range, new_text)| {
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(range.start.row, range.start.column),
lsp::Position::new(range.end.row, range.end.column),
),
new_text.to_string(),
)]
}),
additional_text_edits: edit,
..Default::default()
})
}

View File

@@ -7,6 +7,7 @@ use crate::{
display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
hover_popover::HoverAt,
link_go_to_definition::{CmdChanged, GoToFetchedDefinition, UpdateGoToDefinitionLink},
mouse_context_menu::DeployMouseContextMenu,
EditorStyle,
};
use clock::ReplicaId;
@@ -23,8 +24,9 @@ use gpui::{
json::{self, ToJson},
platform::CursorStyle,
text_layout::{self, Line, RunStyle, TextLayoutCache},
AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext,
LayoutContext, MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext,
AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext, KeyDownEvent,
LayoutContext, ModifiersChangedEvent, MouseButton, MouseEvent, MouseMovedEvent,
MutableAppContext, PaintContext, Quad, Scene, ScrollWheelEvent, SizeConstraint, ViewContext,
WeakViewHandle,
};
use json::json;
@@ -151,6 +153,24 @@ impl EditorElement {
true
}
fn mouse_right_down(
&self,
position: Vector2F,
layout: &mut LayoutState,
paint: &mut PaintState,
cx: &mut EventContext,
) -> bool {
if !paint.text_bounds.contains_point(position) {
return false;
}
let snapshot = self.snapshot(cx.app);
let (point, _) = paint.point_for_position(&snapshot, layout, position);
cx.dispatch_action(DeployMouseContextMenu { position, point });
true
}
fn mouse_up(&self, _position: Vector2F, cx: &mut EventContext) -> bool {
if self.view(cx.app.as_ref()).is_selecting() {
cx.dispatch_action(Select(SelectPhase::End));
@@ -1463,14 +1483,15 @@ impl Element for EditorElement {
}
match event {
Event::LeftMouseDown {
Event::MouseDown(MouseEvent {
button: MouseButton::Left,
position,
cmd,
alt,
shift,
click_count,
..
} => self.mouse_down(
}) => self.mouse_down(
*position,
*cmd,
*alt,
@@ -1480,18 +1501,31 @@ impl Element for EditorElement {
paint,
cx,
),
Event::LeftMouseUp { position, .. } => self.mouse_up(*position, cx),
Event::LeftMouseDragged { position, .. } => {
self.mouse_dragged(*position, layout, paint, cx)
}
Event::ScrollWheel {
Event::MouseDown(MouseEvent {
button: MouseButton::Right,
position,
..
}) => self.mouse_right_down(*position, layout, paint, cx),
Event::MouseUp(MouseEvent {
button: MouseButton::Left,
position,
..
}) => self.mouse_up(*position, cx),
Event::MouseMoved(MouseMovedEvent {
pressed_button: Some(MouseButton::Left),
position,
..
}) => self.mouse_dragged(*position, layout, paint, cx),
Event::ScrollWheel(ScrollWheelEvent {
position,
delta,
precise,
} => self.scroll(*position, *delta, *precise, layout, paint, cx),
Event::KeyDown { input, .. } => self.key_down(input.as_deref(), cx),
Event::ModifiersChanged { cmd, .. } => self.modifiers_changed(*cmd, cx),
Event::MouseMoved { position, cmd, .. } => {
}) => self.scroll(*position, *delta, *precise, layout, paint, cx),
Event::KeyDown(KeyDownEvent { input, .. }) => self.key_down(input.as_deref(), cx),
Event::ModifiersChanged(ModifiersChangedEvent { cmd, .. }) => {
self.modifiers_changed(*cmd, cx)
}
Event::MouseMoved(MouseMovedEvent { position, cmd, .. }) => {
self.mouse_moved(*position, *cmd, layout, paint, cx)
}
@@ -1685,22 +1719,22 @@ impl Cursor {
}
#[derive(Debug)]
struct HighlightedRange {
start_y: f32,
line_height: f32,
lines: Vec<HighlightedRangeLine>,
color: Color,
corner_radius: f32,
pub struct HighlightedRange {
pub start_y: f32,
pub line_height: f32,
pub lines: Vec<HighlightedRangeLine>,
pub color: Color,
pub corner_radius: f32,
}
#[derive(Debug)]
struct HighlightedRangeLine {
start_x: f32,
end_x: f32,
pub struct HighlightedRangeLine {
pub start_x: f32,
pub end_x: f32,
}
impl HighlightedRange {
fn paint(&self, bounds: RectF, scene: &mut Scene) {
pub fn paint(&self, bounds: RectF, scene: &mut Scene) {
if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
self.paint_lines(self.start_y, &self.lines[0..1], bounds, scene);
self.paint_lines(

View File

@@ -352,13 +352,8 @@ impl Item for Editor {
project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
let settings = cx.global::<Settings>();
let buffer = self.buffer().clone();
let mut buffers = buffer.read(cx).all_buffers();
buffers.retain(|buffer| {
let language_name = buffer.read(cx).language().map(|l| l.name());
settings.format_on_save(language_name.as_deref())
});
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, cx));
cx.spawn(|this, mut cx| async move {
@@ -445,6 +440,10 @@ impl Item for Editor {
Event::Saved | Event::DirtyChanged | Event::TitleChanged
)
}
fn is_edit_event(event: &Self::Event) -> bool {
matches!(event, Event::BufferEdited)
}
}
impl ProjectItem for Editor {

View File

@@ -342,17 +342,16 @@ mod tests {
test();"});
let mut requests =
cx.lsp
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink {
origin_selection_range: Some(symbol_range),
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
target_range,
target_selection_range: target_range,
},
])))
});
cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink {
origin_selection_range: Some(symbol_range),
target_uri: url.clone(),
target_range,
target_selection_range: target_range,
},
])))
});
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
@@ -387,18 +386,17 @@ mod tests {
// Response without source range still highlights word
cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None);
let mut requests =
cx.lsp
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink {
// No origin range
origin_selection_range: None,
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
target_range,
target_selection_range: target_range,
},
])))
});
cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink {
// No origin range
origin_selection_range: None,
target_uri: url.clone(),
target_range,
target_selection_range: target_range,
},
])))
});
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
@@ -495,17 +493,16 @@ mod tests {
test();"});
let mut requests =
cx.lsp
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink {
origin_selection_range: Some(symbol_range),
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
target_range,
target_selection_range: target_range,
},
])))
});
cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink {
origin_selection_range: Some(symbol_range),
target_uri: url,
target_range,
target_selection_range: target_range,
},
])))
});
cx.update_editor(|editor, cx| {
cmd_changed(editor, &CmdChanged { cmd_down: true }, cx);
});
@@ -584,17 +581,16 @@ mod tests {
test();"});
let mut requests =
cx.lsp
.handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink {
origin_selection_range: None,
target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
target_range,
target_selection_range: target_range,
},
])))
});
cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink {
origin_selection_range: None,
target_uri: url,
target_range,
target_selection_range: target_range,
},
])))
});
cx.update_workspace(|workspace, cx| {
go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
});

View File

@@ -0,0 +1,103 @@
use context_menu::ContextMenuItem;
use gpui::{geometry::vector::Vector2F, impl_internal_actions, MutableAppContext, ViewContext};
use crate::{
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, Rename, SelectMode,
ToggleCodeActions,
};
#[derive(Clone, PartialEq)]
pub struct DeployMouseContextMenu {
pub position: Vector2F,
pub point: DisplayPoint,
}
impl_internal_actions!(editor, [DeployMouseContextMenu]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(deploy_context_menu);
}
pub fn deploy_context_menu(
editor: &mut Editor,
&DeployMouseContextMenu { position, point }: &DeployMouseContextMenu,
cx: &mut ViewContext<Editor>,
) {
// Don't show context menu for inline editors
if editor.mode() != EditorMode::Full {
return;
}
// Don't show the context menu if there isn't a project associated with this editor
if editor.project.is_none() {
return;
}
// Move the cursor to the clicked location so that dispatched actions make sense
editor.change_selections(None, cx, |s| {
s.clear_disjoint();
s.set_pending_display_range(point..point, SelectMode::Character);
});
editor.mouse_context_menu.update(cx, |menu, cx| {
menu.show(
position,
vec![
ContextMenuItem::item("Rename Symbol", Rename),
ContextMenuItem::item("Go To Definition", GoToDefinition),
ContextMenuItem::item("Find All References", FindAllReferences),
ContextMenuItem::item(
"Code Actions",
ToggleCodeActions {
deployed_from_indicator: false,
},
),
],
cx,
);
});
cx.notify();
}
#[cfg(test)]
mod tests {
use indoc::indoc;
use crate::test::EditorLspTestContext;
use super::*;
#[gpui::test]
async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) {
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
..Default::default()
},
cx,
)
.await;
cx.set_state(indoc! {"
fn te|st()
do_work();"});
let point = cx.display_point(indoc! {"
fn test()
do_w|ork();"});
cx.update_editor(|editor, cx| {
deploy_context_menu(
editor,
&DeployMouseContextMenu {
position: Default::default(),
point,
},
cx,
)
});
cx.assert_editor_state(indoc! {"
fn test()
do_w|ork();"});
cx.editor(|editor, app| assert!(editor.mouse_context_menu.read(app).visible()));
}
}

View File

@@ -149,6 +149,28 @@ impl SelectionsCollection {
selections
}
pub fn all_adjusted_display(
&self,
cx: &mut MutableAppContext,
) -> (DisplaySnapshot, Vec<Selection<DisplayPoint>>) {
if self.line_mode {
let selections = self.all::<Point>(cx);
let map = self.display_map(cx);
let result = selections
.into_iter()
.map(|mut selection| {
let new_range = map.expand_to_line(selection.range());
selection.start = new_range.start;
selection.end = new_range.end;
selection.map(|point| point.to_display_point(&map))
})
.collect();
(map, result)
} else {
self.all_display(cx)
}
}
pub fn disjoint_in_range<'a, D>(
&self,
range: Range<Anchor>,
@@ -175,7 +197,7 @@ impl SelectionsCollection {
}
pub fn all_display(
&mut self,
&self,
cx: &mut MutableAppContext,
) -> (DisplaySnapshot, Vec<Selection<DisplayPoint>>) {
let display_map = self.display_map(cx);
@@ -362,7 +384,7 @@ impl<'a> MutableSelectionsCollection<'a> {
}
}
pub fn set_pending_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
pub fn set_pending_anchor_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
self.collection.pending = Some(PendingSelection {
selection: Selection {
id: post_inc(&mut self.collection.next_selection_id),
@@ -376,6 +398,42 @@ impl<'a> MutableSelectionsCollection<'a> {
self.selections_changed = true;
}
pub fn set_pending_display_range(&mut self, range: Range<DisplayPoint>, mode: SelectMode) {
let (start, end, reversed) = {
let display_map = self.display_map();
let buffer = self.buffer();
let mut start = range.start;
let mut end = range.end;
let reversed = if start > end {
mem::swap(&mut start, &mut end);
true
} else {
false
};
let end_bias = if end > start { Bias::Left } else { Bias::Right };
(
buffer.anchor_before(start.to_point(&display_map)),
buffer.anchor_at(end.to_point(&display_map), end_bias),
reversed,
)
};
let new_pending = PendingSelection {
selection: Selection {
id: post_inc(&mut self.collection.next_selection_id),
start,
end,
reversed,
goal: SelectionGoal::None,
},
mode,
};
self.collection.pending = Some(new_pending);
self.selections_changed = true;
}
pub fn set_pending(&mut self, selection: Selection<Anchor>, mode: SelectMode) {
self.collection.pending = Some(PendingSelection { selection, mode });
self.selections_changed = true;

View File

@@ -4,12 +4,14 @@ use std::{
sync::Arc,
};
use futures::StreamExt;
use anyhow::Result;
use futures::{Future, StreamExt};
use indoc::indoc;
use collections::BTreeMap;
use gpui::{json, keymap::Keystroke, AppContext, ModelHandle, ViewContext, ViewHandle};
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, Selection};
use lsp::request;
use project::Project;
use settings::Settings;
use util::{
@@ -110,6 +112,13 @@ impl<'a> EditorTestContext<'a> {
}
}
pub fn condition(
&self,
predicate: impl FnMut(&Editor, &AppContext) -> bool,
) -> impl Future<Output = ()> {
self.editor.condition(self.cx, predicate)
}
pub fn editor<F, T>(&mut self, read: F) -> T
where
F: FnOnce(&Editor, &AppContext) -> T,
@@ -404,14 +413,6 @@ impl<'a> EditorTestContext<'a> {
editor_text_with_selections
}
pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
self.cx.update(|cx| {
let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
let expected_content = expected_content.map(|content| content.to_owned());
assert_eq!(actual_content, expected_content);
})
}
}
impl<'a> Deref for EditorTestContext<'a> {
@@ -432,6 +433,7 @@ pub struct EditorLspTestContext<'a> {
pub cx: EditorTestContext<'a>,
pub lsp: lsp::FakeLanguageServer,
pub workspace: ViewHandle<Workspace>,
pub editor_lsp_url: lsp::Url,
}
impl<'a> EditorLspTestContext<'a> {
@@ -457,10 +459,12 @@ impl<'a> EditorLspTestContext<'a> {
.unwrap_or(&"txt".to_string())
);
let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
capabilities,
..Default::default()
});
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities,
..Default::default()
}))
.await;
let project = Project::test(params.fs.clone(), [], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
@@ -503,6 +507,7 @@ impl<'a> EditorLspTestContext<'a> {
},
lsp,
workspace,
editor_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
}
}
@@ -526,11 +531,15 @@ impl<'a> EditorLspTestContext<'a> {
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]);
assert_eq!(unmarked, self.cx.buffer_text());
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let offset_range = ranges.remove(&('[', ']').into()).unwrap()[0].clone();
let start_point = offset_range.start.to_point(&snapshot.buffer_snapshot);
let end_point = offset_range.end.to_point(&snapshot.buffer_snapshot);
self.to_lsp_range(offset_range)
}
pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let start_point = range.start.to_point(&snapshot.buffer_snapshot);
let end_point = range.end.to_point(&snapshot.buffer_snapshot);
self.editor(|editor, cx| {
let buffer = editor.buffer().read(cx);
let start = point_to_lsp(
@@ -552,12 +561,45 @@ impl<'a> EditorLspTestContext<'a> {
})
}
pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let point = offset.to_point(&snapshot.buffer_snapshot);
self.editor(|editor, cx| {
let buffer = editor.buffer().read(cx);
point_to_lsp(
buffer
.point_to_buffer_offset(point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
)
})
}
pub fn update_workspace<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
{
self.workspace.update(self.cx.cx, update)
}
pub fn handle_request<T, F, Fut>(
&self,
mut handler: F,
) -> futures::channel::mpsc::UnboundedReceiver<()>
where
T: 'static + request::Request,
T::Params: 'static + Send,
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
Fut: 'static + Send + Future<Output = Result<T::Result>>,
{
let url = self.editor_lsp_url.clone();
self.lsp.handle_request::<T, _, _>(move |params, cx| {
let url = url.clone();
handler(url, params, cx)
})
}
}
impl<'a> Deref for EditorLspTestContext<'a> {

View File

@@ -4,7 +4,7 @@ use gpui::{
RenderContext, Task, View, ViewContext, ViewHandle,
};
use picker::{Picker, PickerDelegate};
use project::{Project, ProjectPath, WorktreeId};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
use settings::Settings;
use std::{
path::Path,
@@ -134,17 +134,40 @@ impl FileFinder {
}
fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
let worktrees = self
.project
.read(cx)
.visible_worktrees(cx)
.collect::<Vec<_>>();
let include_root_name = worktrees.len() > 1;
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name,
}
})
.collect::<Vec<_>>();
let search_id = util::post_inc(&mut self.search_count);
self.cancel_flag.store(true, atomic::Ordering::Relaxed);
self.cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag = self.cancel_flag.clone();
let project = self.project.clone();
cx.spawn(|this, mut cx| async move {
let matches = project
.read_with(&cx, |project, cx| {
project.match_paths(&query, false, false, 100, cancel_flag.as_ref(), cx)
})
.await;
let matches = fuzzy::match_paths(
candidate_sets.as_slice(),
&query,
false,
100,
&cancel_flag,
cx.background(),
)
.await;
let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
this.update(&mut cx, |this, cx| {
this.set_matches(search_id, did_cancel, query, matches, cx)
@@ -389,6 +412,51 @@ mod tests {
});
}
#[gpui::test]
async fn test_ignored_files(cx: &mut gpui::TestAppContext) {
let app_state = cx.update(AppState::test);
app_state
.fs
.as_fake()
.insert_tree(
"/ancestor",
json!({
".gitignore": "ignored-root",
"ignored-root": {
"happiness": "",
"height": "",
"hi": "",
"hiccup": "",
},
"tracked-root": {
".gitignore": "height",
"happiness": "",
"height": "",
"hi": "",
"hiccup": "",
},
}),
)
.await;
let project = Project::test(
app_state.fs.clone(),
[
"/ancestor/tracked-root".as_ref(),
"/ancestor/ignored-root".as_ref(),
],
cx,
)
.await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
finder
.update(cx, |f, cx| f.spawn_search("hi".into(), cx))
.await;
finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 7));
}
#[gpui::test]
async fn test_single_file_worktrees(cx: &mut gpui::TestAppContext) {
let app_state = cx.update(AppState::test);
@@ -475,4 +543,34 @@ mod tests {
assert_eq!(f.selected_index(), 0);
});
}
#[gpui::test]
async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) {
let app_state = cx.update(AppState::test);
app_state
.fs
.as_fake()
.insert_tree(
"/root",
json!({
"dir1": {},
"dir2": {
"dir3": {}
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
let (_, finder) =
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
finder
.update(cx, |f, cx| f.spawn_search("dir".into(), cx))
.await;
cx.read(|cx| {
let finder = finder.read(cx);
assert_eq!(finder.matches.len(), 0);
});
}
}

View File

@@ -4,7 +4,7 @@ use crate::{
elements::ElementBox,
executor::{self, Task},
keymap::{self, Binding, Keystroke},
platform::{self, Platform, PromptLevel, WindowOptions},
platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
presenter::Presenter,
util::post_inc,
AssetCache, AssetSource, ClipboardItem, FontCache, MouseRegionId, PathPromptOptions,
@@ -151,6 +151,7 @@ pub struct AsyncAppContext(Rc<RefCell<MutableAppContext>>);
pub struct TestAppContext {
cx: Rc<RefCell<MutableAppContext>>,
foreground_platform: Rc<platform::test::ForegroundPlatform>,
condition_duration: Option<Duration>,
}
impl App {
@@ -337,6 +338,7 @@ impl TestAppContext {
let cx = TestAppContext {
cx: Rc::new(RefCell::new(cx)),
foreground_platform,
condition_duration: None,
};
cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx));
cx
@@ -377,11 +379,11 @@ impl TestAppContext {
if !cx.dispatch_keystroke(window_id, dispatch_path, &keystroke) {
presenter.borrow_mut().dispatch_event(
Event::KeyDown {
Event::KeyDown(KeyDownEvent {
keystroke,
input,
is_held,
},
}),
cx,
);
}
@@ -612,6 +614,28 @@ impl TestAppContext {
test_window
})
}
pub fn set_condition_duration(&mut self, duration: Option<Duration>) {
self.condition_duration = duration;
}
pub fn condition_duration(&self) -> Duration {
self.condition_duration.unwrap_or_else(|| {
if std::env::var("CI").is_ok() {
Duration::from_secs(2)
} else {
Duration::from_millis(500)
}
})
}
pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
self.update(|cx| {
let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
let expected_content = expected_content.map(|content| content.to_owned());
assert_eq!(actual_content, expected_content);
})
}
}
impl AsyncAppContext {
@@ -811,7 +835,7 @@ type GlobalActionCallback = dyn FnMut(&dyn Action, &mut MutableAppContext);
type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> bool>;
type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
type FocusObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
type FocusObservationCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
type GlobalObservationCallback = Box<dyn FnMut(&mut MutableAppContext)>;
type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContext)>;
type ActionObservationCallback = Box<dyn FnMut(TypeId, &mut MutableAppContext)>;
@@ -1305,7 +1329,7 @@ impl MutableAppContext {
fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
where
F: 'static + FnMut(ViewHandle<V>, &mut MutableAppContext) -> bool,
F: 'static + FnMut(ViewHandle<V>, bool, &mut MutableAppContext) -> bool,
V: View,
{
let subscription_id = post_inc(&mut self.next_subscription_id);
@@ -1314,9 +1338,9 @@ impl MutableAppContext {
self.pending_effects.push_back(Effect::FocusObservation {
view_id,
subscription_id,
callback: Box::new(move |cx| {
callback: Box::new(move |focused, cx| {
if let Some(observed) = observed.upgrade(cx) {
callback(observed, cx)
callback(observed, focused, cx)
} else {
false
}
@@ -1820,7 +1844,7 @@ impl MutableAppContext {
window.on_event(Box::new(move |event| {
app.update(|cx| {
if let Some(presenter) = presenter.upgrade() {
if let Event::KeyDown { keystroke, .. } = &event {
if let Event::KeyDown(KeyDownEvent { keystroke, .. }) = &event {
if cx.dispatch_keystroke(
window_id,
presenter.borrow().dispatch_path(cx.as_ref()),
@@ -2525,6 +2549,31 @@ impl MutableAppContext {
if let Some(mut blurred_view) = this.cx.views.remove(&(window_id, blurred_id)) {
blurred_view.on_blur(this, window_id, blurred_id);
this.cx.views.insert((window_id, blurred_id), blurred_view);
let callbacks = this.focus_observations.lock().remove(&blurred_id);
if let Some(callbacks) = callbacks {
for (id, callback) in callbacks {
if let Some(mut callback) = callback {
let alive = callback(false, this);
if alive {
match this
.focus_observations
.lock()
.entry(blurred_id)
.or_default()
.entry(id)
{
btree_map::Entry::Vacant(entry) => {
entry.insert(Some(callback));
}
btree_map::Entry::Occupied(entry) => {
entry.remove();
}
}
}
}
}
}
}
}
@@ -2537,7 +2586,7 @@ impl MutableAppContext {
if let Some(callbacks) = callbacks {
for (id, callback) in callbacks {
if let Some(mut callback) = callback {
let alive = callback(this);
let alive = callback(true, this);
if alive {
match this
.focus_observations
@@ -3598,20 +3647,21 @@ impl<'a, T: View> ViewContext<'a, T> {
pub fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
where
F: 'static + FnMut(&mut T, ViewHandle<V>, &mut ViewContext<T>),
F: 'static + FnMut(&mut T, ViewHandle<V>, bool, &mut ViewContext<T>),
V: View,
{
let observer = self.weak_handle();
self.app.observe_focus(handle, move |observed, cx| {
if let Some(observer) = observer.upgrade(cx) {
observer.update(cx, |observer, cx| {
callback(observer, observed, cx);
});
true
} else {
false
}
})
self.app
.observe_focus(handle, move |observed, focused, cx| {
if let Some(observer) = observer.upgrade(cx) {
observer.update(cx, |observer, cx| {
callback(observer, observed, focused, cx);
});
true
} else {
false
}
})
}
pub fn observe_release<E, F, H>(&mut self, handle: &H, mut callback: F) -> Subscription
@@ -4398,6 +4448,7 @@ impl<T: View> ViewHandle<T> {
use postage::prelude::{Sink as _, Stream as _};
let (tx, mut rx) = postage::mpsc::channel(1024);
let timeout_duration = cx.condition_duration();
let mut cx = cx.cx.borrow_mut();
let subscriptions = self.update(&mut *cx, |_, cx| {
@@ -4419,14 +4470,9 @@ impl<T: View> ViewHandle<T> {
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
let handle = self.downgrade();
let duration = if std::env::var("CI").is_ok() {
Duration::from_secs(2)
} else {
Duration::from_millis(500)
};
async move {
crate::util::timeout(duration, async move {
crate::util::timeout(timeout_duration, async move {
loop {
{
let cx = cx.borrow();
@@ -5355,7 +5401,7 @@ impl RefCounts {
#[cfg(test)]
mod tests {
use super::*;
use crate::{actions, elements::*, impl_actions};
use crate::{actions, elements::*, impl_actions, MouseButton, MouseEvent};
use serde::Deserialize;
use smol::future::poll_once;
use std::{
@@ -5708,14 +5754,15 @@ mod tests {
let presenter = cx.presenters_and_platform_windows[&window_id].0.clone();
// Ensure window's root element is in a valid lifecycle state.
presenter.borrow_mut().dispatch_event(
Event::LeftMouseDown {
Event::MouseDown(MouseEvent {
position: Default::default(),
button: MouseButton::Left,
ctrl: false,
alt: false,
shift: false,
cmd: false,
click_count: 1,
},
}),
cx,
);
assert_eq!(mouse_down_count.load(SeqCst), 1);
@@ -6448,11 +6495,13 @@ mod tests {
view_1.update(cx, |_, cx| {
cx.observe_focus(&view_2, {
let observed_events = observed_events.clone();
move |this, view, cx| {
move |this, view, focused, cx| {
let label = if focused { "focus" } else { "blur" };
observed_events.lock().push(format!(
"{} observed {}'s focus",
"{} observed {}'s {}",
this.name,
view.read(cx).name
view.read(cx).name,
label
))
}
})
@@ -6461,16 +6510,20 @@ mod tests {
view_2.update(cx, |_, cx| {
cx.observe_focus(&view_1, {
let observed_events = observed_events.clone();
move |this, view, cx| {
move |this, view, focused, cx| {
let label = if focused { "focus" } else { "blur" };
observed_events.lock().push(format!(
"{} observed {}'s focus",
"{} observed {}'s {}",
this.name,
view.read(cx).name
view.read(cx).name,
label
))
}
})
.detach();
});
assert_eq!(mem::take(&mut *view_events.lock()), ["view 1 focused"]);
assert_eq!(mem::take(&mut *observed_events.lock()), Vec::<&str>::new());
view_1.update(cx, |_, cx| {
// Ensure only the latest focus is honored.
@@ -6478,31 +6531,47 @@ mod tests {
cx.focus(&view_1);
cx.focus(&view_2);
});
view_1.update(cx, |_, cx| cx.focus(&view_1));
view_1.update(cx, |_, cx| cx.focus(&view_2));
view_1.update(cx, |_, _| drop(view_2));
assert_eq!(
*view_events.lock(),
[
"view 1 focused".to_string(),
"view 1 blurred".to_string(),
"view 2 focused".to_string(),
"view 2 blurred".to_string(),
"view 1 focused".to_string(),
"view 1 blurred".to_string(),
"view 2 focused".to_string(),
"view 1 focused".to_string(),
],
mem::take(&mut *view_events.lock()),
["view 1 blurred", "view 2 focused"],
);
assert_eq!(
*observed_events.lock(),
mem::take(&mut *observed_events.lock()),
[
"view 1 observed view 2's focus".to_string(),
"view 2 observed view 1's focus".to_string(),
"view 1 observed view 2's focus".to_string(),
"view 2 observed view 1's blur",
"view 1 observed view 2's focus"
]
);
view_1.update(cx, |_, cx| cx.focus(&view_1));
assert_eq!(
mem::take(&mut *view_events.lock()),
["view 2 blurred", "view 1 focused"],
);
assert_eq!(
mem::take(&mut *observed_events.lock()),
[
"view 1 observed view 2's blur",
"view 2 observed view 1's focus"
]
);
view_1.update(cx, |_, cx| cx.focus(&view_2));
assert_eq!(
mem::take(&mut *view_events.lock()),
["view 1 blurred", "view 2 focused"],
);
assert_eq!(
mem::take(&mut *observed_events.lock()),
[
"view 2 observed view 1's blur",
"view 1 observed view 2's focus"
]
);
view_1.update(cx, |_, _| drop(view_2));
assert_eq!(mem::take(&mut *view_events.lock()), ["view 1 focused"]);
assert_eq!(mem::take(&mut *observed_events.lock()), Vec::<&str>::new());
}
#[crate::test(self)]

View File

@@ -1,6 +1,7 @@
use crate::{
geometry::vector::Vector2F, CursorRegion, DebugContext, Element, ElementBox, Event,
EventContext, LayoutContext, MouseRegion, NavigationDirection, PaintContext, SizeConstraint,
EventContext, LayoutContext, MouseButton, MouseEvent, MouseRegion, NavigationDirection,
PaintContext, SizeConstraint,
};
use pathfinder_geometry::rect::RectF;
use serde_json::json;
@@ -90,7 +91,7 @@ impl Element for EventHandler {
click: Some(Rc::new(|_, _, _| {})),
right_mouse_down: Some(Rc::new(|_, _| {})),
right_click: Some(Rc::new(|_, _, _| {})),
drag: Some(Rc::new(|_, _| {})),
drag: Some(Rc::new(|_, _, _| {})),
mouse_down_out: Some(Rc::new(|_, _| {})),
right_mouse_down_out: Some(Rc::new(|_, _| {})),
});
@@ -116,7 +117,11 @@ impl Element for EventHandler {
true
} else {
match event {
Event::LeftMouseDown { position, .. } => {
Event::MouseDown(MouseEvent {
button: MouseButton::Left,
position,
..
}) => {
if let Some(callback) = self.mouse_down.as_mut() {
if visible_bounds.contains_point(*position) {
return callback(cx);
@@ -124,7 +129,11 @@ impl Element for EventHandler {
}
false
}
Event::RightMouseDown { position, .. } => {
Event::MouseDown(MouseEvent {
button: MouseButton::Right,
position,
..
}) => {
if let Some(callback) = self.right_mouse_down.as_mut() {
if visible_bounds.contains_point(*position) {
return callback(cx);
@@ -132,11 +141,11 @@ impl Element for EventHandler {
}
false
}
Event::NavigateMouseDown {
Event::MouseDown(MouseEvent {
button: MouseButton::Navigate(direction),
position,
direction,
..
} => {
}) => {
if let Some(callback) = self.navigate_mouse_down.as_mut() {
if visible_bounds.contains_point(*position) {
return callback(*direction, cx);

View File

@@ -3,7 +3,8 @@ use std::{any::Any, f32::INFINITY};
use crate::{
json::{self, ToJson, Value},
Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext,
LayoutContext, PaintContext, RenderContext, SizeConstraint, Vector2FExt, View,
LayoutContext, MouseMovedEvent, PaintContext, RenderContext, ScrollWheelEvent, SizeConstraint,
Vector2FExt, View,
};
use pathfinder_geometry::{
rect::RectF,
@@ -287,11 +288,11 @@ impl Element for Flex {
handled = child.dispatch_event(event, cx) || handled;
}
if !handled {
if let &Event::ScrollWheel {
if let &Event::ScrollWheel(ScrollWheelEvent {
position,
delta,
precise,
} = event
}) = event
{
if *remaining_space < 0. && bounds.contains_point(position) {
if let Some(scroll_state) = self.scroll_state.as_ref() {
@@ -321,7 +322,7 @@ impl Element for Flex {
}
if !handled {
if let &Event::MouseMoved { position, .. } = event {
if let &Event::MouseMoved(MouseMovedEvent { position, .. }) = event {
// If this is a scrollable flex, and the mouse is over it, eat the scroll event to prevent
// propogating it to the element below.
if self.scroll_state.is_some() && bounds.contains_point(position) {

View File

@@ -5,7 +5,7 @@ use crate::{
},
json::json,
DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext,
RenderContext, SizeConstraint, View, ViewContext,
RenderContext, ScrollWheelEvent, SizeConstraint, View, ViewContext,
};
use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc};
use sum_tree::{Bias, SumTree};
@@ -311,11 +311,11 @@ impl Element for List {
state.items = new_items;
match event {
Event::ScrollWheel {
Event::ScrollWheel(ScrollWheelEvent {
position,
delta,
precise,
} => {
}) => {
if bounds.contains_point(*position) {
if state.scroll(scroll_top, bounds.height(), *delta, *precise, cx) {
handled = true;

View File

@@ -24,7 +24,7 @@ pub struct MouseEventHandler {
right_click: Option<Rc<dyn Fn(Vector2F, usize, &mut EventContext)>>,
mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
right_mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
drag: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
drag: Option<Rc<dyn Fn(Vector2F, Vector2F, &mut EventContext)>>,
hover: Option<Rc<dyn Fn(Vector2F, bool, &mut EventContext)>>,
padding: Padding,
}
@@ -106,7 +106,10 @@ impl MouseEventHandler {
self
}
pub fn on_drag(mut self, handler: impl Fn(Vector2F, &mut EventContext) + 'static) -> Self {
pub fn on_drag(
mut self,
handler: impl Fn(Vector2F, Vector2F, &mut EventContext) + 'static,
) -> Self {
self.drag = Some(Rc::new(handler));
self
}

View File

@@ -5,7 +5,7 @@ use crate::{
vector::{vec2f, Vector2F},
},
json::{self, json},
ElementBox, RenderContext, View,
ElementBox, RenderContext, ScrollWheelEvent, View,
};
use json::ToJson;
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -310,11 +310,11 @@ impl Element for UniformList {
}
match event {
Event::ScrollWheel {
Event::ScrollWheel(ScrollWheelEvent {
position,
delta,
precise,
} => {
}) => {
if bounds.contains_point(*position) {
if self.scroll(*position, *delta, *precise, layout.scroll_max, cx) {
handled = true;

View File

@@ -28,8 +28,7 @@ pub mod json;
pub mod keymap;
pub mod platform;
pub use gpui_macros::test;
pub use platform::FontSystem;
pub use platform::{Event, NavigationDirection, PathPromptOptions, Platform, PromptLevel};
pub use platform::*;
pub use presenter::{
Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt,
};

View File

@@ -20,7 +20,7 @@ use crate::{
};
use anyhow::{anyhow, Result};
use async_task::Runnable;
pub use event::{Event, NavigationDirection};
pub use event::*;
use postage::oneshot;
use serde::Deserialize;
use std::{

View File

@@ -1,85 +1,77 @@
use crate::{geometry::vector::Vector2F, keymap::Keystroke};
#[derive(Clone, Debug)]
pub struct KeyDownEvent {
pub keystroke: Keystroke,
pub input: Option<String>,
pub is_held: bool,
}
#[derive(Clone, Debug)]
pub struct KeyUpEvent {
pub keystroke: Keystroke,
pub input: Option<String>,
}
#[derive(Clone, Debug)]
pub struct ModifiersChangedEvent {
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
pub cmd: bool,
}
#[derive(Clone, Debug)]
pub struct ScrollWheelEvent {
pub position: Vector2F,
pub delta: Vector2F,
pub precise: bool,
}
#[derive(Copy, Clone, Debug)]
pub enum NavigationDirection {
Back,
Forward,
}
#[derive(Copy, Clone, Debug)]
pub enum MouseButton {
Left,
Right,
Middle,
Navigate(NavigationDirection),
}
#[derive(Clone, Debug)]
pub struct MouseEvent {
pub button: MouseButton,
pub position: Vector2F,
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
pub cmd: bool,
pub click_count: usize,
}
#[derive(Clone, Copy, Debug)]
pub struct MouseMovedEvent {
pub position: Vector2F,
pub pressed_button: Option<MouseButton>,
pub ctrl: bool,
pub cmd: bool,
pub alt: bool,
pub shift: bool,
}
#[derive(Clone, Debug)]
pub enum Event {
KeyDown {
keystroke: Keystroke,
input: Option<String>,
is_held: bool,
},
KeyUp {
keystroke: Keystroke,
input: Option<String>,
},
ModifiersChanged {
ctrl: bool,
alt: bool,
shift: bool,
cmd: bool,
},
ScrollWheel {
position: Vector2F,
delta: Vector2F,
precise: bool,
},
LeftMouseDown {
position: Vector2F,
ctrl: bool,
alt: bool,
shift: bool,
cmd: bool,
click_count: usize,
},
LeftMouseUp {
position: Vector2F,
click_count: usize,
},
LeftMouseDragged {
position: Vector2F,
ctrl: bool,
alt: bool,
shift: bool,
cmd: bool,
},
RightMouseDown {
position: Vector2F,
ctrl: bool,
alt: bool,
shift: bool,
cmd: bool,
click_count: usize,
},
RightMouseUp {
position: Vector2F,
click_count: usize,
},
NavigateMouseDown {
position: Vector2F,
direction: NavigationDirection,
ctrl: bool,
alt: bool,
shift: bool,
cmd: bool,
click_count: usize,
},
NavigateMouseUp {
position: Vector2F,
direction: NavigationDirection,
},
MouseMoved {
position: Vector2F,
left_mouse_down: bool,
ctrl: bool,
cmd: bool,
alt: bool,
shift: bool,
},
KeyDown(KeyDownEvent),
KeyUp(KeyUpEvent),
ModifiersChanged(ModifiersChangedEvent),
MouseDown(MouseEvent),
MouseUp(MouseEvent),
MouseMoved(MouseMovedEvent),
ScrollWheel(ScrollWheelEvent),
}
impl Event {
@@ -88,15 +80,9 @@ impl Event {
Event::KeyDown { .. } => None,
Event::KeyUp { .. } => None,
Event::ModifiersChanged { .. } => None,
Event::ScrollWheel { position, .. }
| Event::LeftMouseDown { position, .. }
| Event::LeftMouseUp { position, .. }
| Event::LeftMouseDragged { position, .. }
| Event::RightMouseDown { position, .. }
| Event::RightMouseUp { position, .. }
| Event::NavigateMouseDown { position, .. }
| Event::NavigateMouseUp { position, .. }
| Event::MouseMoved { position, .. } => Some(*position),
Event::MouseDown(event) | Event::MouseUp(event) => Some(event.position),
Event::MouseMoved(event) => Some(event.position),
Event::ScrollWheel(event) => Some(event.position),
}
}
}

View File

@@ -2,10 +2,12 @@ use crate::{
geometry::vector::vec2f,
keymap::Keystroke,
platform::{Event, NavigationDirection},
KeyDownEvent, KeyUpEvent, ModifiersChangedEvent, MouseButton, MouseEvent, MouseMovedEvent,
ScrollWheelEvent,
};
use cocoa::{
appkit::{NSEvent, NSEventModifierFlags, NSEventType},
base::{id, nil, YES},
base::{id, YES},
foundation::NSString as _,
};
use std::{borrow::Cow, ffi::CStr, os::raw::c_char};
@@ -59,12 +61,12 @@ impl Event {
let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
Some(Self::ModifiersChanged {
Some(Self::ModifiersChanged(ModifiersChangedEvent {
ctrl,
alt,
shift,
cmd,
})
}))
}
NSEventType::NSKeyDown => {
let modifiers = native_event.modifierFlags();
@@ -76,7 +78,7 @@ impl Event {
let (unmodified_chars, input) = get_key_text(native_event, cmd, ctrl, function)?;
Some(Self::KeyDown {
Some(Self::KeyDown(KeyDownEvent {
keystroke: Keystroke {
ctrl,
alt,
@@ -86,7 +88,7 @@ impl Event {
},
input,
is_held: native_event.isARepeat() == YES,
})
}))
}
NSEventType::NSKeyUp => {
let modifiers = native_event.modifierFlags();
@@ -98,7 +100,7 @@ impl Event {
let (unmodified_chars, input) = get_key_text(native_event, cmd, ctrl, function)?;
Some(Self::KeyUp {
Some(Self::KeyUp(KeyUpEvent {
keystroke: Keystroke {
ctrl,
alt,
@@ -107,125 +109,120 @@ impl Event {
key: unmodified_chars.into(),
},
input,
})
}))
}
NSEventType::NSLeftMouseDown => {
NSEventType::NSLeftMouseDown
| NSEventType::NSRightMouseDown
| NSEventType::NSOtherMouseDown => {
let button = match native_event.buttonNumber() {
0 => MouseButton::Left,
1 => MouseButton::Right,
2 => MouseButton::Middle,
3 => MouseButton::Navigate(NavigationDirection::Back),
4 => MouseButton::Navigate(NavigationDirection::Forward),
// Other mouse buttons aren't tracked currently
_ => return None,
};
let modifiers = native_event.modifierFlags();
window_height.map(|window_height| Self::LeftMouseDown {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
click_count: native_event.clickCount() as usize,
window_height.map(|window_height| {
Self::MouseDown(MouseEvent {
button,
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
click_count: native_event.clickCount() as usize,
})
})
}
NSEventType::NSLeftMouseUp => window_height.map(|window_height| Self::LeftMouseUp {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
click_count: native_event.clickCount() as usize,
}),
NSEventType::NSRightMouseDown => {
let modifiers = native_event.modifierFlags();
window_height.map(|window_height| Self::RightMouseDown {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
click_count: native_event.clickCount() as usize,
})
}
NSEventType::NSRightMouseUp => window_height.map(|window_height| Self::RightMouseUp {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
click_count: native_event.clickCount() as usize,
}),
NSEventType::NSOtherMouseDown => {
let direction = match native_event.buttonNumber() {
3 => NavigationDirection::Back,
4 => NavigationDirection::Forward,
NSEventType::NSLeftMouseUp
| NSEventType::NSRightMouseUp
| NSEventType::NSOtherMouseUp => {
let button = match native_event.buttonNumber() {
0 => MouseButton::Left,
1 => MouseButton::Right,
2 => MouseButton::Middle,
3 => MouseButton::Navigate(NavigationDirection::Back),
4 => MouseButton::Navigate(NavigationDirection::Forward),
// Other mouse buttons aren't tracked currently
_ => return None,
};
let modifiers = native_event.modifierFlags();
window_height.map(|window_height| Self::NavigateMouseDown {
window_height.map(|window_height| {
let modifiers = native_event.modifierFlags();
Self::MouseUp(MouseEvent {
button,
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
click_count: native_event.clickCount() as usize,
})
})
}
NSEventType::NSScrollWheel => window_height.map(|window_height| {
Self::ScrollWheel(ScrollWheelEvent {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
direction,
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
click_count: native_event.clickCount() as usize,
delta: vec2f(
native_event.scrollingDeltaX() as f32,
native_event.scrollingDeltaY() as f32,
),
precise: native_event.hasPreciseScrollingDeltas() == YES,
})
}
NSEventType::NSOtherMouseUp => {
let direction = match native_event.buttonNumber() {
3 => NavigationDirection::Back,
4 => NavigationDirection::Forward,
}),
NSEventType::NSLeftMouseDragged
| NSEventType::NSRightMouseDragged
| NSEventType::NSOtherMouseDragged => {
let pressed_button = match native_event.buttonNumber() {
0 => MouseButton::Left,
1 => MouseButton::Right,
2 => MouseButton::Middle,
3 => MouseButton::Navigate(NavigationDirection::Back),
4 => MouseButton::Navigate(NavigationDirection::Forward),
// Other mouse buttons aren't tracked currently
_ => return None,
};
window_height.map(|window_height| Self::NavigateMouseUp {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
direction,
window_height.map(|window_height| {
let modifiers = native_event.modifierFlags();
Self::MouseMoved(MouseMovedEvent {
pressed_button: Some(pressed_button),
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
})
})
}
NSEventType::NSLeftMouseDragged => window_height.map(|window_height| {
let modifiers = native_event.modifierFlags();
Self::LeftMouseDragged {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
}
}),
NSEventType::NSScrollWheel => window_height.map(|window_height| Self::ScrollWheel {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
delta: vec2f(
native_event.scrollingDeltaX() as f32,
native_event.scrollingDeltaY() as f32,
),
precise: native_event.hasPreciseScrollingDeltas() == YES,
}),
NSEventType::NSMouseMoved => window_height.map(|window_height| {
let modifiers = native_event.modifierFlags();
Self::MouseMoved {
Self::MouseMoved(MouseMovedEvent {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
left_mouse_down: NSEvent::pressedMouseButtons(nil) & 1 != 0,
pressed_button: None,
ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
}
})
}),
_ => None,
}

View File

@@ -16,7 +16,7 @@ use std::{collections::HashMap, ffi::c_void, iter::Peekable, mem, sync::Arc, vec
const SHADERS_METALLIB: &'static [u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
const INSTANCE_BUFFER_SIZE: usize = 1024 * 1024; // This is an arbitrary decision. There's probably a more optimal value.
const INSTANCE_BUFFER_SIZE: usize = 8192 * 1024; // This is an arbitrary decision. There's probably a more optimal value.
pub struct Renderer {
sprite_cache: SpriteCache,

View File

@@ -6,7 +6,7 @@ use crate::{
},
keymap::Keystroke,
platform::{self, Event, WindowBounds, WindowContext},
Scene,
KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseEvent, MouseMovedEvent, Scene,
};
use block::ConcreteBlock;
use cocoa::{
@@ -392,17 +392,33 @@ impl platform::Window for Window {
});
let block = block.copy();
let native_window = self.0.borrow().native_window;
let _: () = msg_send![
alert,
beginSheetModalForWindow: native_window
completionHandler: block
];
self.0
.borrow()
.executor
.spawn(async move {
let _: () = msg_send![
alert,
beginSheetModalForWindow: native_window
completionHandler: block
];
})
.detach();
done_rx
}
}
fn activate(&self) {
unsafe { msg_send![self.0.borrow().native_window, makeKeyAndOrderFront: nil] }
let window = self.0.borrow().native_window;
self.0
.borrow()
.executor
.spawn(async move {
unsafe {
let _: () = msg_send![window, makeKeyAndOrderFront: nil];
}
})
.detach();
}
fn set_title(&mut self, title: &str) {
@@ -562,11 +578,11 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) ->
let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) };
if let Some(event) = event {
match &event {
Event::KeyDown {
Event::KeyDown(KeyDownEvent {
keystroke,
input,
is_held,
} => {
}) => {
let keydown = (keystroke.clone(), input.clone());
// Ignore events from held-down keys after some of the initially-pressed keys
// were released.
@@ -603,33 +619,41 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
if let Some(event) = event {
match &event {
Event::LeftMouseDragged { position, .. } => {
Event::MouseMoved(
event @ MouseMovedEvent {
pressed_button: Some(_),
..
},
) => {
window_state_borrow.synthetic_drag_counter += 1;
window_state_borrow
.executor
.spawn(synthetic_drag(
weak_window_state,
window_state_borrow.synthetic_drag_counter,
*position,
*event,
))
.detach();
}
Event::LeftMouseUp { .. } => {
Event::MouseUp(MouseEvent {
button: MouseButton::Left,
..
}) => {
window_state_borrow.synthetic_drag_counter += 1;
}
Event::ModifiersChanged {
Event::ModifiersChanged(ModifiersChangedEvent {
ctrl,
alt,
shift,
cmd,
} => {
}) => {
// Only raise modifiers changed event when they have actually changed
if let Some(Event::ModifiersChanged {
if let Some(Event::ModifiersChanged(ModifiersChangedEvent {
ctrl: prev_ctrl,
alt: prev_alt,
shift: prev_shift,
cmd: prev_cmd,
}) = &window_state_borrow.previous_modifiers_changed_event
})) = &window_state_borrow.previous_modifiers_changed_event
{
if prev_ctrl == ctrl
&& prev_alt == alt
@@ -667,11 +691,11 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
shift: false,
key: chars.clone(),
};
let event = Event::KeyDown {
let event = Event::KeyDown(KeyDownEvent {
keystroke: keystroke.clone(),
input: Some(chars.clone()),
is_held: false,
};
});
window_state_borrow.last_fresh_keydown = Some((keystroke, Some(chars)));
if let Some(mut callback) = window_state_borrow.event_callback.take() {
@@ -778,7 +802,7 @@ extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) {
extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
let window_state = unsafe { get_window_state(this) };
let mut window_state_borrow = window_state.as_ref().borrow_mut();
let window_state_borrow = window_state.as_ref().borrow();
if window_state_borrow.size() == vec2f(size.width as f32, size.height as f32) {
return;
@@ -798,6 +822,8 @@ extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
let _: () = msg_send![window_state_borrow.layer, setDrawableSize: drawable_size];
}
drop(window_state_borrow);
let mut window_state_borrow = window_state.borrow_mut();
if let Some(mut callback) = window_state_borrow.resize_callback.take() {
drop(window_state_borrow);
callback();
@@ -835,7 +861,7 @@ extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
async fn synthetic_drag(
window_state: Weak<RefCell<WindowState>>,
drag_id: usize,
position: Vector2F,
event: MouseMovedEvent,
) {
loop {
Timer::after(Duration::from_millis(16)).await;
@@ -844,14 +870,7 @@ async fn synthetic_drag(
if window_state_borrow.synthetic_drag_counter == drag_id {
if let Some(mut callback) = window_state_borrow.event_callback.take() {
drop(window_state_borrow);
callback(Event::LeftMouseDragged {
// TODO: Make sure empty modifiers is correct for this
position,
shift: false,
ctrl: false,
alt: false,
cmd: false,
});
callback(Event::MouseMoved(event));
window_state.borrow_mut().event_callback = Some(callback);
}
} else {

View File

@@ -9,9 +9,9 @@ use crate::{
scene::CursorRegion,
text_layout::TextLayoutCache,
Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, Entity,
FontSystem, ModelHandle, MouseRegion, MouseRegionId, ReadModel, ReadView, RenderContext,
RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle,
WeakViewHandle,
FontSystem, ModelHandle, MouseButton, MouseEvent, MouseMovedEvent, MouseRegion, MouseRegionId,
ReadModel, ReadView, RenderContext, RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle,
View, ViewHandle, WeakModelHandle, WeakViewHandle,
};
use pathfinder_geometry::vector::{vec2f, Vector2F};
use serde_json::json;
@@ -235,7 +235,11 @@ impl Presenter {
let mut dragged_region = None;
match event {
Event::LeftMouseDown { position, .. } => {
Event::MouseDown(MouseEvent {
position,
button: MouseButton::Left,
..
}) => {
let mut hit = false;
for (region, _) in self.mouse_regions.iter().rev() {
if region.bounds.contains_point(position) {
@@ -251,11 +255,12 @@ impl Presenter {
}
}
}
Event::LeftMouseUp {
Event::MouseUp(MouseEvent {
position,
click_count,
button: MouseButton::Left,
..
} => {
}) => {
self.prev_drag_position.take();
if let Some(region) = self.clicked_region.take() {
invalidated_views.push(region.view_id);
@@ -264,7 +269,11 @@ impl Presenter {
}
}
}
Event::RightMouseDown { position, .. } => {
Event::MouseDown(MouseEvent {
position,
button: MouseButton::Right,
..
}) => {
let mut hit = false;
for (region, _) in self.mouse_regions.iter().rev() {
if region.bounds.contains_point(position) {
@@ -279,11 +288,12 @@ impl Presenter {
}
}
}
Event::RightMouseUp {
Event::MouseUp(MouseEvent {
position,
click_count,
button: MouseButton::Right,
..
} => {
}) => {
if let Some(region) = self.right_clicked_region.take() {
invalidated_views.push(region.view_id);
if region.bounds.contains_point(position) {
@@ -291,34 +301,37 @@ impl Presenter {
}
}
}
Event::MouseMoved { .. } => {
self.last_mouse_moved_event = Some(event.clone());
}
Event::LeftMouseDragged {
Event::MouseMoved(MouseMovedEvent {
pressed_button,
position,
shift,
ctrl,
alt,
cmd,
} => {
if let Some((clicked_region, prev_drag_position)) = self
.clicked_region
.as_ref()
.zip(self.prev_drag_position.as_mut())
{
dragged_region =
Some((clicked_region.clone(), position - *prev_drag_position));
*prev_drag_position = position;
..
}) => {
if let Some(MouseButton::Left) = pressed_button {
if let Some((clicked_region, prev_drag_position)) = self
.clicked_region
.as_ref()
.zip(self.prev_drag_position.as_mut())
{
dragged_region =
Some((clicked_region.clone(), *prev_drag_position, position));
*prev_drag_position = position;
}
self.last_mouse_moved_event = Some(Event::MouseMoved(MouseMovedEvent {
position,
pressed_button: Some(MouseButton::Left),
shift,
ctrl,
alt,
cmd,
}));
}
self.last_mouse_moved_event = Some(Event::MouseMoved {
position,
left_mouse_down: true,
shift,
ctrl,
alt,
cmd,
});
self.last_mouse_moved_event = Some(event.clone());
}
_ => {}
}
@@ -366,11 +379,11 @@ impl Presenter {
}
}
if let Some((dragged_region, delta)) = dragged_region {
if let Some((dragged_region, prev_position, position)) = dragged_region {
handled = true;
if let Some(drag_callback) = dragged_region.drag {
event_cx.with_current_view(dragged_region.view_id, |event_cx| {
drag_callback(delta, event_cx);
drag_callback(prev_position, position, event_cx);
})
}
}
@@ -410,13 +423,13 @@ impl Presenter {
let mut unhovered_regions = Vec::new();
let mut hovered_regions = Vec::new();
if let Event::MouseMoved {
if let Event::MouseMoved(MouseMovedEvent {
position,
left_mouse_down,
pressed_button,
..
} = event
}) = event
{
if !left_mouse_down {
if let None = pressed_button {
let mut style_to_assign = CursorStyle::Arrow;
for region in self.cursor_regions.iter().rev() {
if region.bounds.contains_point(*position) {
@@ -648,6 +661,16 @@ impl<'a> PaintContext<'a> {
}
}
#[inline]
pub fn paint_layer<F>(&mut self, clip_bounds: Option<RectF>, f: F)
where
F: FnOnce(&mut Self) -> (),
{
self.scene.push_layer(clip_bounds);
f(self);
self.scene.pop_layer();
}
pub fn current_view_id(&self) -> usize {
*self.view_stack.last().unwrap()
}

View File

@@ -8,7 +8,7 @@ use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::ToJson,
platform::CursorStyle,
EventContext, ImageData,
EventContext, ImageData, MouseEvent, MouseMovedEvent, ScrollWheelEvent,
};
pub struct Scene {
@@ -44,17 +44,28 @@ pub struct CursorRegion {
pub style: CursorStyle,
}
pub enum MouseRegionEvent {
Moved(MouseMovedEvent),
Hover(MouseEvent),
Down(MouseEvent),
Up(MouseEvent),
Click(MouseEvent),
DownOut(MouseEvent),
ScrollWheel(ScrollWheelEvent),
}
#[derive(Clone, Default)]
pub struct MouseRegion {
pub view_id: usize,
pub discriminant: Option<(TypeId, usize)>,
pub bounds: RectF,
pub hover: Option<Rc<dyn Fn(Vector2F, bool, &mut EventContext)>>,
pub mouse_down: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
pub click: Option<Rc<dyn Fn(Vector2F, usize, &mut EventContext)>>,
pub right_mouse_down: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
pub right_click: Option<Rc<dyn Fn(Vector2F, usize, &mut EventContext)>>,
pub drag: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
pub drag: Option<Rc<dyn Fn(Vector2F, Vector2F, &mut EventContext)>>,
pub mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
pub right_mouse_down_out: Option<Rc<dyn Fn(Vector2F, &mut EventContext)>>,
}

View File

@@ -273,7 +273,7 @@ pub struct Chunk<'a> {
pub is_unnecessary: bool,
}
pub(crate) struct Diff {
pub struct Diff {
base_version: clock::Global,
new_text: Arc<str>,
changes: Vec<(ChangeTag, usize)>,
@@ -958,13 +958,13 @@ impl Buffer {
}
}
pub(crate) fn diff(&self, mut new_text: String, cx: &AppContext) -> Task<Diff> {
pub fn diff(&self, mut new_text: String, cx: &AppContext) -> Task<Diff> {
let old_text = self.as_rope().clone();
let base_version = self.version();
cx.background().spawn(async move {
let old_text = old_text.to_string();
let line_ending = LineEnding::detect(&new_text);
LineEnding::strip_carriage_returns(&mut new_text);
LineEnding::normalize(&mut new_text);
let changes = TextDiff::from_lines(old_text.as_str(), new_text.as_str())
.iter_all_changes()
.map(|c| (c.tag(), c.value().len()))
@@ -979,11 +979,7 @@ impl Buffer {
})
}
pub(crate) fn apply_diff(
&mut self,
diff: Diff,
cx: &mut ModelContext<Self>,
) -> Option<&Transaction> {
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();
@@ -1233,7 +1229,8 @@ impl Buffer {
let inserted_ranges = edits
.into_iter()
.filter_map(|(range, new_text)| {
.zip(&edit_operation.as_edit().unwrap().new_text)
.filter_map(|((range, _), new_text)| {
let first_newline_ix = new_text.find('\n')?;
let new_text_len = new_text.len();
let start = (delta + range.start as isize) as usize + first_newline_ix + 1;
@@ -2396,12 +2393,12 @@ impl<'a> Iterator for BufferChunks<'a> {
impl QueryCursorHandle {
pub(crate) fn new() -> Self {
QueryCursorHandle(Some(
QUERY_CURSORS
.lock()
.pop()
.unwrap_or_else(|| QueryCursor::new()),
))
let mut cursor = QUERY_CURSORS
.lock()
.pop()
.unwrap_or_else(|| QueryCursor::new());
cursor.set_match_limit(64);
QueryCursorHandle(Some(cursor))
}
}

View File

@@ -7,6 +7,7 @@ pub mod proto;
mod tests;
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use client::http::HttpClient;
use collections::HashMap;
use futures::{
@@ -17,6 +18,7 @@ use gpui::{MutableAppContext, Task};
use highlight_map::HighlightMap;
use lazy_static::lazy_static;
use parking_lot::{Mutex, RwLock};
use postage::watch;
use regex::Regex;
use serde::{de, Deserialize, Deserializer};
use serde_json::Value;
@@ -29,7 +31,7 @@ use std::{
str,
sync::Arc,
};
use theme::SyntaxTheme;
use theme::{SyntaxTheme, Theme};
use tree_sitter::{self, Query};
use util::ResultExt;
@@ -43,7 +45,7 @@ pub use outline::{Outline, OutlineItem};
pub use tree_sitter::{Parser, Tree};
thread_local! {
static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
}
lazy_static! {
@@ -63,49 +65,142 @@ pub trait ToLspPosition {
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct LanguageServerName(pub Arc<str>);
pub trait LspAdapter: 'static + Send + Sync {
fn name(&self) -> LanguageServerName;
fn fetch_latest_server_version(
/// Represents a Language Server, with certain cached sync properties.
/// Uses [`LspAdapter`] under the hood, but calls all 'static' methods
/// once at startup, and caches the results.
pub struct CachedLspAdapter {
pub name: LanguageServerName,
pub server_args: Vec<String>,
pub initialization_options: Option<Value>,
pub disk_based_diagnostic_sources: Vec<String>,
pub disk_based_diagnostics_progress_token: Option<String>,
pub language_ids: HashMap<String, String>,
pub adapter: Box<dyn LspAdapter>,
}
impl CachedLspAdapter {
pub async fn new<T: LspAdapter>(adapter: T) -> Arc<Self> {
let adapter = Box::new(adapter);
let name = adapter.name().await;
let server_args = adapter.server_args().await;
let initialization_options = adapter.initialization_options().await;
let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources().await;
let disk_based_diagnostics_progress_token =
adapter.disk_based_diagnostics_progress_token().await;
let language_ids = adapter.language_ids().await;
Arc::new(CachedLspAdapter {
name,
server_args,
initialization_options,
disk_based_diagnostic_sources,
disk_based_diagnostics_progress_token,
language_ids,
adapter,
})
}
pub async fn fetch_latest_server_version(
&self,
http: Arc<dyn HttpClient>,
) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>>;
fn fetch_server_binary(
) -> Result<Box<dyn 'static + Send + Any>> {
self.adapter.fetch_latest_server_version(http).await
}
pub async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
http: Arc<dyn HttpClient>,
container_dir: Arc<Path>,
) -> BoxFuture<'static, Result<PathBuf>>;
fn cached_server_binary(&self, container_dir: Arc<Path>)
-> BoxFuture<'static, Option<PathBuf>>;
container_dir: PathBuf,
) -> Result<PathBuf> {
self.adapter
.fetch_server_binary(version, http, container_dir)
.await
}
fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
pub async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
self.adapter.cached_server_binary(container_dir).await
}
fn label_for_completion(&self, _: &lsp::CompletionItem, _: &Language) -> Option<CodeLabel> {
pub async fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
self.adapter.process_diagnostics(params).await
}
pub async fn label_for_completion(
&self,
completion_item: &lsp::CompletionItem,
language: &Language,
) -> Option<CodeLabel> {
self.adapter
.label_for_completion(completion_item, language)
.await
}
pub async fn label_for_symbol(
&self,
name: &str,
kind: lsp::SymbolKind,
language: &Language,
) -> Option<CodeLabel> {
self.adapter.label_for_symbol(name, kind, language).await
}
}
#[async_trait]
pub trait LspAdapter: 'static + Send + Sync {
async fn name(&self) -> LanguageServerName;
async fn fetch_latest_server_version(
&self,
http: Arc<dyn HttpClient>,
) -> Result<Box<dyn 'static + Send + Any>>;
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
http: Arc<dyn HttpClient>,
container_dir: PathBuf,
) -> Result<PathBuf>;
async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf>;
async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
async fn label_for_completion(
&self,
_: &lsp::CompletionItem,
_: &Language,
) -> Option<CodeLabel> {
None
}
fn label_for_symbol(&self, _: &str, _: lsp::SymbolKind, _: &Language) -> Option<CodeLabel> {
async fn label_for_symbol(
&self,
_: &str,
_: lsp::SymbolKind,
_: &Language,
) -> Option<CodeLabel> {
None
}
fn server_args(&self) -> &[&str] {
&[]
async fn server_args(&self) -> Vec<String> {
Vec::new()
}
fn initialization_options(&self) -> Option<Value> {
async fn initialization_options(&self) -> Option<Value> {
None
}
fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] {
async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
Default::default()
}
fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> {
async fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
None
}
fn id_for_language(&self, _name: &str) -> Option<String> {
None
async fn language_ids(&self) -> HashMap<String, String> {
Default::default()
}
}
@@ -165,8 +260,8 @@ pub struct FakeLspAdapter {
pub name: &'static str,
pub capabilities: lsp::ServerCapabilities,
pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
pub disk_based_diagnostics_progress_token: Option<&'static str>,
pub disk_based_diagnostics_sources: &'static [&'static str],
pub disk_based_diagnostics_progress_token: Option<String>,
pub disk_based_diagnostics_sources: Vec<String>,
}
#[derive(Clone, Debug, Deserialize)]
@@ -180,7 +275,7 @@ pub struct BracketPair {
pub struct Language {
pub(crate) config: LanguageConfig,
pub(crate) grammar: Option<Arc<Grammar>>,
pub(crate) adapter: Option<Arc<dyn LspAdapter>>,
pub(crate) adapter: Option<Arc<CachedLspAdapter>>,
#[cfg(any(test, feature = "test-support"))]
fake_adapter: Option<(
@@ -219,6 +314,8 @@ pub struct LanguageRegistry {
Shared<BoxFuture<'static, Result<PathBuf, Arc<anyhow::Error>>>>,
>,
>,
subscription: RwLock<(watch::Sender<()>, watch::Receiver<()>)>,
theme: RwLock<Option<Arc<Theme>>>,
}
impl LanguageRegistry {
@@ -231,6 +328,8 @@ impl LanguageRegistry {
lsp_binary_statuses_rx,
login_shell_env_loaded: login_shell_env_loaded.shared(),
lsp_binary_paths: Default::default(),
subscription: RwLock::new(watch::channel()),
theme: Default::default(),
}
}
@@ -240,12 +339,21 @@ impl LanguageRegistry {
}
pub fn add(&self, language: Arc<Language>) {
if let Some(theme) = self.theme.read().clone() {
language.set_theme(&theme.editor.syntax);
}
self.languages.write().push(language.clone());
*self.subscription.write().0.borrow_mut() = ();
}
pub fn set_theme(&self, theme: &SyntaxTheme) {
pub fn subscribe(&self) -> watch::Receiver<()> {
self.subscription.read().1.clone()
}
pub fn set_theme(&self, theme: Arc<Theme>) {
*self.theme.write() = Some(theme.clone());
for language in self.languages.read().iter() {
language.set_theme(theme);
language.set_theme(&theme.editor.syntax);
}
}
@@ -345,7 +453,7 @@ impl LanguageRegistry {
let server_binary_path = this
.lsp_binary_paths
.lock()
.entry(adapter.name())
.entry(adapter.name.clone())
.or_insert_with(|| {
get_server_binary_path(
adapter.clone(),
@@ -362,11 +470,11 @@ impl LanguageRegistry {
.map_err(|e| anyhow!(e));
let server_binary_path = server_binary_path.await?;
let server_args = adapter.server_args();
let server_args = &adapter.server_args;
let server = lsp::LanguageServer::new(
server_id,
&server_binary_path,
server_args,
&server_args,
&root_path,
cx,
)?;
@@ -382,13 +490,13 @@ impl LanguageRegistry {
}
async fn get_server_binary_path(
adapter: Arc<dyn LspAdapter>,
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
http_client: Arc<dyn HttpClient>,
download_dir: Arc<Path>,
statuses: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
) -> Result<PathBuf> {
let container_dir: Arc<Path> = download_dir.join(adapter.name().0.as_ref()).into();
let container_dir = download_dir.join(adapter.name.0.as_ref());
if !container_dir.exists() {
smol::fs::create_dir_all(&container_dir)
.await
@@ -424,7 +532,7 @@ async fn get_server_binary_path(
}
async fn fetch_latest_server_binary_path(
adapter: Arc<dyn LspAdapter>,
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
http_client: Arc<dyn HttpClient>,
container_dir: &Path,
@@ -444,7 +552,7 @@ async fn fetch_latest_server_binary_path(
.broadcast((language.clone(), LanguageServerBinaryStatus::Downloading))
.await?;
let path = adapter
.fetch_server_binary(version_info, http_client, container_dir.clone())
.fetch_server_binary(version_info, http_client, container_dir.to_path_buf())
.await?;
lsp_binary_statuses_tx
.broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded))
@@ -473,7 +581,7 @@ impl Language {
}
}
pub fn lsp_adapter(&self) -> Option<Arc<dyn LspAdapter>> {
pub fn lsp_adapter(&self) -> Option<Arc<CachedLspAdapter>> {
self.adapter.clone()
}
@@ -505,19 +613,19 @@ impl Language {
Arc::get_mut(self.grammar.as_mut().unwrap()).unwrap()
}
pub fn with_lsp_adapter(mut self, lsp_adapter: Arc<dyn LspAdapter>) -> Self {
pub fn with_lsp_adapter(mut self, lsp_adapter: Arc<CachedLspAdapter>) -> Self {
self.adapter = Some(lsp_adapter);
self
}
#[cfg(any(test, feature = "test-support"))]
pub fn set_fake_lsp_adapter(
pub async fn set_fake_lsp_adapter(
&mut self,
fake_lsp_adapter: FakeLspAdapter,
fake_lsp_adapter: Arc<FakeLspAdapter>,
) -> mpsc::UnboundedReceiver<lsp::FakeLanguageServer> {
let (servers_tx, servers_rx) = mpsc::unbounded();
let adapter = Arc::new(fake_lsp_adapter);
self.fake_adapter = Some((servers_tx, adapter.clone()));
self.fake_adapter = Some((servers_tx, fake_lsp_adapter.clone()));
let adapter = CachedLspAdapter::new(fake_lsp_adapter).await;
self.adapter = Some(adapter);
servers_rx
}
@@ -530,32 +638,42 @@ impl Language {
self.config.line_comment.as_deref()
}
pub fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] {
self.adapter.as_ref().map_or(&[] as &[_], |adapter| {
adapter.disk_based_diagnostic_sources()
})
}
pub fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> {
self.adapter
.as_ref()
.and_then(|adapter| adapter.disk_based_diagnostics_progress_token())
}
pub fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) {
if let Some(processor) = self.adapter.as_ref() {
processor.process_diagnostics(diagnostics);
pub async fn disk_based_diagnostic_sources(&self) -> &[String] {
match self.adapter.as_ref() {
Some(adapter) => &adapter.disk_based_diagnostic_sources,
None => &[],
}
}
pub fn label_for_completion(&self, completion: &lsp::CompletionItem) -> Option<CodeLabel> {
pub async fn disk_based_diagnostics_progress_token(&self) -> Option<&str> {
if let Some(adapter) = self.adapter.as_ref() {
adapter.disk_based_diagnostics_progress_token.as_deref()
} else {
None
}
}
pub async fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) {
if let Some(processor) = self.adapter.as_ref() {
processor.process_diagnostics(diagnostics).await;
}
}
pub async fn label_for_completion(
&self,
completion: &lsp::CompletionItem,
) -> Option<CodeLabel> {
self.adapter
.as_ref()?
.label_for_completion(completion, self)
.await
}
pub fn label_for_symbol(&self, name: &str, kind: lsp::SymbolKind) -> Option<CodeLabel> {
self.adapter.as_ref()?.label_for_symbol(name, kind, self)
pub async fn label_for_symbol(&self, name: &str, kind: lsp::SymbolKind) -> Option<CodeLabel> {
self.adapter
.as_ref()?
.label_for_symbol(name, kind, self)
.await
}
pub fn highlight_text<'a>(
@@ -664,45 +782,46 @@ impl Default for FakeLspAdapter {
capabilities: lsp::LanguageServer::full_capabilities(),
initializer: None,
disk_based_diagnostics_progress_token: None,
disk_based_diagnostics_sources: &[],
disk_based_diagnostics_sources: Vec::new(),
}
}
}
#[cfg(any(test, feature = "test-support"))]
impl LspAdapter for FakeLspAdapter {
fn name(&self) -> LanguageServerName {
#[async_trait]
impl LspAdapter for Arc<FakeLspAdapter> {
async fn name(&self) -> LanguageServerName {
LanguageServerName(self.name.into())
}
fn fetch_latest_server_version(
async fn fetch_latest_server_version(
&self,
_: Arc<dyn HttpClient>,
) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
) -> Result<Box<dyn 'static + Send + Any>> {
unreachable!();
}
fn fetch_server_binary(
async fn fetch_server_binary(
&self,
_: Box<dyn 'static + Send + Any>,
_: Arc<dyn HttpClient>,
_: Arc<Path>,
) -> BoxFuture<'static, Result<PathBuf>> {
_: PathBuf,
) -> Result<PathBuf> {
unreachable!();
}
fn cached_server_binary(&self, _: Arc<Path>) -> BoxFuture<'static, Option<PathBuf>> {
async fn cached_server_binary(&self, _: PathBuf) -> Option<PathBuf> {
unreachable!();
}
fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] {
self.disk_based_diagnostics_sources
async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
self.disk_based_diagnostics_sources.clone()
}
fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> {
self.disk_based_diagnostics_progress_token
async fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
self.disk_based_diagnostics_progress_token.clone()
}
}

View File

@@ -397,9 +397,9 @@ pub fn serialize_completion(completion: &Completion) -> proto::Completion {
}
}
pub fn deserialize_completion(
pub async fn deserialize_completion(
completion: proto::Completion,
language: Option<&Arc<Language>>,
language: Option<Arc<Language>>,
) -> Result<Completion> {
let old_start = completion
.old_start
@@ -410,15 +410,18 @@ pub fn deserialize_completion(
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("invalid old end"))?;
let lsp_completion = serde_json::from_slice(&completion.lsp_completion)?;
let label = match language {
Some(l) => l.label_for_completion(&lsp_completion).await,
None => None,
};
Ok(Completion {
old_range: old_start..old_end,
new_text: completion.new_text,
label: language
.and_then(|l| l.label_for_completion(&lsp_completion))
.unwrap_or(CodeLabel::plain(
lsp_completion.label.clone(),
lsp_completion.filter_text.as_deref(),
)),
label: label.unwrap_or(CodeLabel::plain(
lsp_completion.label.clone(),
lsp_completion.filter_text.as_deref(),
)),
lsp_completion,
})
}

View File

@@ -22,6 +22,29 @@ fn init_logger() {
}
}
#[gpui::test]
fn test_line_endings(cx: &mut gpui::MutableAppContext) {
cx.add_model(|cx| {
let mut buffer =
Buffer::new(0, "one\r\ntwo\rthree", cx).with_language(Arc::new(rust_lang()), cx);
assert_eq!(buffer.text(), "one\ntwo\nthree");
assert_eq!(buffer.line_ending(), LineEnding::Windows);
buffer.check_invariants();
buffer.edit_with_autoindent(
[(buffer.len()..buffer.len(), "\r\nfour")],
IndentSize::spaces(2),
cx,
);
buffer.edit([(0..0, "zero\r\n")], cx);
assert_eq!(buffer.text(), "zero\none\ntwo\nthree\nfour");
assert_eq!(buffer.line_ending(), LineEnding::Windows);
buffer.check_invariants();
buffer
});
}
#[gpui::test]
fn test_select_language() {
let registry = LanguageRegistry::test();

View File

@@ -101,10 +101,10 @@ struct Error {
}
impl LanguageServer {
pub fn new(
pub fn new<T: AsRef<std::ffi::OsStr>>(
server_id: usize,
binary_path: &Path,
args: &[&str],
args: &[T],
root_path: &Path,
cx: AsyncAppContext,
) -> Result<Self> {
@@ -258,6 +258,9 @@ impl LanguageServer {
}
}
/// Initializes a language server.
/// Note that `options` is used directly to construct [`InitializeParams`],
/// which is why it is owned.
pub async fn initialize(mut self, options: Option<Value>) -> Result<Arc<Self>> {
let root_uri = Url::from_file_path(&self.root_path).unwrap();
#[allow(deprecated)]

9
crates/plugin/Cargo.toml Normal file
View File

@@ -0,0 +1,9 @@
[package]
name = "plugin"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = "1.0"
bincode = "1.3"
plugin_macros = { path = "../plugin_macros" }

61
crates/plugin/src/lib.rs Normal file
View File

@@ -0,0 +1,61 @@
pub use bincode;
pub use serde;
/// This is the buffer that is used Wasm side.
/// Note that it mirrors the functionality of
/// the `WasiBuffer` found in `plugin_runtime/src/plugin.rs`,
/// But has a few different methods.
pub struct __Buffer {
pub ptr: u32, // *const u8,
pub len: u32, // usize,
}
impl __Buffer {
pub fn into_u64(self) -> u64 {
((self.ptr as u64) << 32) | (self.len as u64)
}
pub fn from_u64(packed: u64) -> Self {
__Buffer {
ptr: (packed >> 32) as u32,
len: packed as u32,
}
}
}
/// Allocates a buffer with an exact size.
/// We don't return the size because it has to be passed in anyway.
#[no_mangle]
pub extern "C" fn __alloc_buffer(len: u32) -> u32 {
let vec = vec![0; len as usize];
let buffer = unsafe { __Buffer::from_vec(vec) };
return buffer.ptr;
}
/// Frees a given buffer, requires the size.
#[no_mangle]
pub extern "C" fn __free_buffer(buffer: u64) {
let vec = unsafe { __Buffer::from_u64(buffer).to_vec() };
std::mem::drop(vec);
}
impl __Buffer {
#[inline(always)]
pub unsafe fn to_vec(&self) -> Vec<u8> {
core::slice::from_raw_parts(self.ptr as *const u8, self.len as usize).to_vec()
}
#[inline(always)]
pub unsafe fn from_vec(mut vec: Vec<u8>) -> __Buffer {
vec.shrink_to(0);
let ptr = vec.as_ptr() as u32;
let len = vec.len() as u32;
std::mem::forget(vec);
__Buffer { ptr, len }
}
}
pub mod prelude {
pub use super::{__Buffer, __alloc_buffer};
pub use plugin_macros::{export, import};
}

View File

@@ -0,0 +1,14 @@
[package]
name = "plugin_macros"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
syn = { version = "1.0", features = ["full", "extra-traits"] }
quote = "1.0"
proc-macro2 = "1.0"
serde = "1.0"
bincode = "1.3"

View File

@@ -0,0 +1,168 @@
use core::panic;
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, Block, FnArg, ForeignItemFn, Ident, ItemFn, Pat, Type, Visibility};
/// Attribute macro to be used guest-side within a plugin.
/// ```ignore
/// #[export]
/// pub fn say_hello() -> String {
/// "Hello from Wasm".into()
/// }
/// ```
/// This macro makes a function defined guest-side avaliable host-side.
/// Note that all arguments and return types must be `serde`.
#[proc_macro_attribute]
pub fn export(args: TokenStream, function: TokenStream) -> TokenStream {
if !args.is_empty() {
panic!("The export attribute does not take any arguments");
}
let inner_fn = parse_macro_input!(function as ItemFn);
if !inner_fn.sig.generics.params.is_empty() {
panic!("Exported functions can not take generic parameters");
}
if let Visibility::Public(_) = inner_fn.vis {
} else {
panic!("The export attribute only works for public functions");
}
let inner_fn_name = format_ident!("{}", inner_fn.sig.ident);
let outer_fn_name = format_ident!("__{}", inner_fn_name);
let variadic = inner_fn.sig.inputs.len();
let i = (0..variadic).map(syn::Index::from);
let t: Vec<Type> = inner_fn
.sig
.inputs
.iter()
.map(|x| match x {
FnArg::Receiver(_) => {
panic!("All arguments must have specified types, no `self` allowed")
}
FnArg::Typed(item) => *item.ty.clone(),
})
.collect();
// this is cursed...
let (args, ty) = if variadic != 1 {
(
quote! {
#( data.#i ),*
},
quote! {
( #( #t ),* )
},
)
} else {
let ty = &t[0];
(quote! { data }, quote! { #ty })
};
TokenStream::from(quote! {
#[no_mangle]
#inner_fn
#[no_mangle]
pub extern "C" fn #outer_fn_name(packed_buffer: u64) -> u64 {
// setup
let data = unsafe { ::plugin::__Buffer::from_u64(packed_buffer).to_vec() };
// operation
let data: #ty = match ::plugin::bincode::deserialize(&data) {
Ok(d) => d,
Err(e) => panic!("Data passed to function not deserializable."),
};
let result = #inner_fn_name(#args);
let new_data: Result<Vec<u8>, _> = ::plugin::bincode::serialize(&result);
let new_data = new_data.unwrap();
// teardown
let new_buffer = unsafe { ::plugin::__Buffer::from_vec(new_data) }.into_u64();
return new_buffer;
}
})
}
/// Attribute macro to be used guest-side within a plugin.
/// ```ignore
/// #[import]
/// pub fn operating_system_name() -> String;
/// ```
/// This macro makes a function defined host-side avaliable guest-side.
/// Note that all arguments and return types must be `serde`.
/// All that's provided is a signature, as the function is implemented host-side.
#[proc_macro_attribute]
pub fn import(args: TokenStream, function: TokenStream) -> TokenStream {
if !args.is_empty() {
panic!("The import attribute does not take any arguments");
}
let fn_declare = parse_macro_input!(function as ForeignItemFn);
if !fn_declare.sig.generics.params.is_empty() {
panic!("Exported functions can not take generic parameters");
}
// let inner_fn_name = format_ident!("{}", fn_declare.sig.ident);
let extern_fn_name = format_ident!("__{}", fn_declare.sig.ident);
let (args, tys): (Vec<Ident>, Vec<Type>) = fn_declare
.sig
.inputs
.clone()
.into_iter()
.map(|x| match x {
FnArg::Receiver(_) => {
panic!("All arguments must have specified types, no `self` allowed")
}
FnArg::Typed(t) => {
if let Pat::Ident(i) = *t.pat {
(i.ident, *t.ty)
} else {
panic!("All function arguments must be identifiers");
}
}
})
.unzip();
let body = TokenStream::from(quote! {
{
// setup
let data: (#( #tys ),*) = (#( #args ),*);
let data = ::plugin::bincode::serialize(&data).unwrap();
let buffer = unsafe { ::plugin::__Buffer::from_vec(data) };
// operation
let new_buffer = unsafe { #extern_fn_name(buffer.into_u64()) };
let new_data = unsafe { ::plugin::__Buffer::from_u64(new_buffer).to_vec() };
// teardown
match ::plugin::bincode::deserialize(&new_data) {
Ok(d) => d,
Err(e) => panic!("Data returned from function not deserializable."),
}
}
});
let block = parse_macro_input!(body as Block);
let inner_fn = ItemFn {
attrs: fn_declare.attrs,
vis: fn_declare.vis,
sig: fn_declare.sig,
block: Box::new(block),
};
TokenStream::from(quote! {
extern "C" {
fn #extern_fn_name(buffer: u64) -> u64;
}
#[no_mangle]
#inner_fn
})
}

View File

@@ -0,0 +1,18 @@
[package]
name = "plugin_runtime"
version = "0.1.0"
edition = "2021"
[dependencies]
wasmtime = "0.38"
wasmtime-wasi = "0.38"
wasi-common = "0.38"
anyhow = { version = "1.0", features = ["std"] }
serde = "1.0"
serde_json = "1.0"
bincode = "1.3"
pollster = "0.2.5"
smol = "1.2.5"
[build-dependencies]
wasmtime = "0.38"

View File

@@ -0,0 +1,321 @@
# Zed's Plugin Runner
This is a short guide that aims to answer the following questions:
- How do plugins work in Zed?
- How can I create a new plugin?
- How can I integrate plugins into a part of Zed?
### Nomenclature
- Host-side: The native Rust runtime managing plugins, e.g. Zed.
- Guest-side: The wasm-based runtime that plugins use.
## How plugins work
Zed's plugins are WebAssembly (Wasm) based, and have access to the WebAssembly System Interface (WASI), which allows for permissions-based access to subsets of system resources, like the filesystem.
To execute plugins, Zed's plugin system uses the sandboxed [`wasmtime`](https://wasmtime.dev/) runtime, which is Open Source and developed by the [Bytecode Alliance](https://bytecodealliance.org/). Wasmtime uses the [Cranelift](https://docs.rs/cranelift/latest/cranelift/) codegen library to compile plugins to native code.
Zed has three `plugin` crates that implement different things:
1. `plugin_runtime` is a host-side library that loads and runs compiled `Wasm` plugins, in addition to setting up system bindings. This crate should be used host-side
2. `plugin` contains a prelude for guest-side plugins to depend on. It re-exports some required crates (e.g. `serde`, `bincode`) and provides some necessary macros for generating bindings that `plugin_runtime` can hook into.
3. `plugin_macros` implements the proc macros required by `plugin`, like the `#[import]` and `#[export]` attribute macros, and should also be used guest-side.
### ABI
The interface between the host Rust runtime ('Runtime') and plugins implemented in Wasm ('Plugin') is pretty simple.
When calling a guest-side function, all arguments are serialized to bytes and passed through `Buffer`s. We currently use `serde` + [`bincode`](https://docs.rs/bincode/latest/bincode/) to do this serialization. This means that any type that can be serialized using serde can be passed across the ABI boundary. For types that represent resources that cannot pass the ABI boundary (e.g. `Rope`), we are working on an opaque callback-based system.
> **Note**: It's important to note that there is a draft ABI standard for Wasm called WebAssembly Interface Types (often abbreviated `WITX`). This standard is currently not stable and only experimentally supported in some runtimes. Once this proposal becomes stable, it would be a good idea to transition towards using WITX as the ABI, rather than the rather rudimentary `bincode` ABI we have now.
All `Buffer`s are stored in Wasm linear memory (Wasm memory). A `Buffer` is a pointer, length pair to a byte array somewhere in Wasm memory. A `Buffer` itself is represented as a pair of two 4-byte (`u32`) fields:
```rust
struct Buffer {
ptr: u32,
len: u32,
}
```
Which we encode as a single `u64` when crossing the ABI boundary:
```
+-------+-------+
| ptr | len |
+-------+-------+
|
~ ~ ~ ~ | ~ ~ ~ ~ spOoky ABI boundary O.o
V
+---------------+
| u64 |
+---------------+
```
All functions that a plugin exports or imports have the following properties:
- A function signature of `fn(u64) -> u64`, where both the argument (input) and return type (output) are a `Buffer`:
- The input `Buffer` will contain the input arguments serialized to `bincode`.
- The output `Buffer` will contain the output arguments serialized to `bincode`.
- Have a name starting with two underscores.
Luckily for us, we don't have to worry about mangling names or writing serialization code. The `plugin::prelude::*` defines a couple of macros—aptly named `#[import]` and `#[export]`—that generate all serialization code and perform all mangling of names requisite for crossing the ABI boundary.
There are also a couple important things every plugin must have:
- `__alloc_buffer` function that, given a `u32` length, returns a `u32` pointer to a buffer of that length.
- `__free_buffer` function that, given a buffer encoded as a `u64`, frees the buffer at the given location, and does not return anything.
Luckily enough for us yet again, the `plugin` prelude defines two ready-made versions of these functions, so you don't have to worry about implementing them yourselves.
So, what does importing and exporting functions from a plugin look like in practice? I'm glad you asked...
## Creating new plugins
Since Zed's plugin system uses Wasm + WASI, in theory any language that compiles to Wasm can be used to write plugins. In practice, and out of practicality, however, we currently only really support plugins written in Rust.
A plugin is just a rust crate like any other. All plugins embedded in Zed are located in the `plugins` folder in the root. These plugins will automatically be compiled, optimized, and recompiled on change, so it's recommended that when creating a new plugin you create it there.
As plugins are compiled to Wasm + WASI, you need to have the `wasm32-wasi` toolchain installed on your system. If you don't have it already, a little rustup magick will do the trick:
```bash
rustup target add wasm32-wasi
```
### Configuring a plugin
After you've created a new plugin in `plugins` using `cargo new --lib`, edit your `Cargo.toml` to ensure that it looks something like this:
```toml
[package]
name = "my_very_cool_incredible_plugin_with_a_short_name_of_course"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
plugin = { path = "../../crates/plugin" }
[profile.release]
opt-level = "z"
lto = true
```
Here's a quick explainer of what we're doing:
- `crate-type = ["cdylib"]` is used because a plugin essentially acts a *library*, exposing functions with specific signatures that perform certain tasks. This key ensures that the library is generated in a reproducible manner with a layout `plugin_runtime` knows how to hook into.
- `plugin = { path = "../../crates/plugin" }` is used so we have access to the prelude, which has a few useful functions and can automatically generate serialization glue code for us.
- `[profile.release]` these options wholistically optimize for size, which will become increasingly important as we add more plugins.
### Importing and Exporting functions
To import or export a function, all you need are two things:
1. Make sure that you've imported `plugin::prelude::*`
2. Annotate your function or signature with `#[export]` or `#[import]` respectively.
Here's an example plugin that doubles the value of every float in a `Vec<f64>` passed into it:
```rust
use plugin::prelude::*;
#[export]
pub fn double(mut x: Vec<f64>) -> Vec<f64> {
x.into_iter().map(|x| x * 2.0).collect()
}
```
All the serialization code is automatically generated by `#[export]`.
You can specify functions that must be defined host-side by using the `#[import]` attribute. This attribute must be attached to a function signature:
```rust
use plugin::prelude::*;
#[import]
fn run(command: String) -> Vec<u8>;
```
The `#[import]` macro will generate a function body that performs the proper serialization/deserialization needed to call out to the host rust runtime. Note that the same `serde` + `bincode` + `Buffer` ABI is used for both `#[import]` and `#[export]`.
> **Note**: If you'd like to see an example of importing and exporting functions, check out the `test_plugin`, which can be found in the `plugins` directory.
## Integrating plugins into Zed
Currently, plugins are used to add support for language servers to Zed. Plugins should be fairly simple to integrate for library-like applications. Here's a quick overview of how plugins work:
### Normal vs Precompiled plugins
Plugins in the `plugins` directory are automatically recompiled and serialized to disk when compiling Zed. The resulting artifacts can be found in the `plugins/bin` directory. For each `plugin`, you should see two files:
- `plugin.wasm` is the plugin compiled to Wasm. As a baseline, this should be about 4MB for debug builds and 2MB for release builds, but it depends on the specific plugin being built.
- `plugin.wasm.pre` is the plugin compiled to Wasm *and additionally* precompiled to host-platform-agnostic cranelift-specific IR. This should be about 700KB for debug builds and 500KB in release builds. Each plugin takes about 1 or 2 seconds to compile to native code using cranelift, so precompiling plugins drastically reduces the startup time required to begin to run a plugin.
For all intents and purposes, it is *highly recommended* that you use precompiled plugins where possible, as they are much more lightweight and take much less time to instantiate.
### Instantiating a plugin
So you have something you'd like to add a plugin for. What now? The general pattern for adding support for plugins is as follows:
#### 1. Create a struct to hold the plugin
To call the functions that a plugin exports host-side, you need to have 'handles' to those functions. Each handle is typed and stored in `WasiFn<A, R>` where `A: Serialize` and `R: DeserializeOwned`.
For example, let's suppose we're creating a plugin that:
1. formats a message
2. processes a list of numbers somehow
We could create a struct for this plugin as follows:
```rust
use plugin_runtime::{WasiFn, Plugin};
pub struct CoolPlugin {
format: WasiFn<String, String>,
process: WasiFn<Vec<f64>, f64>,
runtime: Plugin,
}
```
Note that this plugin also holds an owned reference to the runtime, which is stored in the `Plugin` type. In asynchronous or multithreaded contexts, it may be required to put `Plugin` behind an `Arc<Mutex<Plugin>>`. Although plugins expose an asynchronous interface, the underlying Wasm engine can only execute a single function at a time.
> **Note**: This is a limitation of the WebAssembly standard itself. In the future, to work around this, we've been considering starting a pool of plugins, or instantiating a new plugin per call (this isn't as bad as it sounds, as instantiating a new plugin only takes about 30µs).
In the following steps, we're going to build a plugin and extract handles to fill this struct we've created.
#### 2. Bind all imported functions
While a plugin can export functions, it can also import them. We'll refer to the host-side functions that a plugin imports as 'native' functions. Native functions are represented using callbacks, and both synchronous and asynchronous callbacks are supported.
To bind imported functions, the first thing we need to do is create a new plugin using `PluginBuilder`. `PluginBuilder` uses the builder pattern to configure a new plugin, after which calling the `init` method will instantiate the `Plugin`.
You can create a new plugin builder as follows:
```rust
let builder = PluginBuilder::new_with_default_ctx();
```
This creates a plugin with a sensible default set of WASI permissions, namely the ability to write to `stdout` and `stderr` (note that, by default, plugins do not have access to `stdin`). For more control, you can use `PluginBuilder::new` and pass in a `WasiCtx` manually.
##### Synchronous Functions
To add a sync native function to a plugin, use the `.host_function` method:
```rust
let builder = builder.host_function(
"add_f64",
|(a, b): (f64, f64)| a + b,
).unwrap();
```
The `.host_function` method takes two arguments: the name of the function, and a sync callback that implements it. Note that this name must match the name of the function declared in the plugin exactly. For example, to use the `add_f64` from a plugin, you must include the following `#[import]` signature:
```rust
use plugin::prelude::*;
#[import]
fn add_f64(a: f64, b: f64) -> f64;
```
Note that the specific names of the arguments do not matter, as long as they are unique. Once a function has been imported, it may be used in the plugin as any other Rust function.
##### Asynchronous Functions
To add an async native function to a plugin, use the `.host_function_async` method:
```rust
let builder = builder.host_function_async(
"half",
|n: f64| async move { n / 2.0 },
).unwrap();
```
This method works exactly the same as the `.host_function` method, but requires a callback that returns an async future. On the plugin side, there is no distinction made between sync and async functions (as Wasm has no built-in notion of sync vs. async), so the required import signature should *not* use the `async` keyword:
```rust
use plugin::prelude::*;
#[import]
fn half(n: f64) -> f64;
```
All functions declared by the builder must be imported by the Wasm plugin, otherwise an error will be raised.
#### 3. Get the compiled plugin
Once all imports are marked, we can instantiate the plugin. To instantiate the plugin, simply call the `.init` method on a `PluginBuilder`:
```rust
let plugin = builder
.init(
true,
include_bytes!("../../../plugins/bin/cool_plugin.wasm.pre"),
)
.await
.unwrap();
```
The `.init` method currently takes two arguments:
1. First, the 'precompiled' flag, indicating whether the plugin is *normal* (`.wasm`) or precompiled (`.wasm.pre`). When using a precompiled plugin, set this flag to `true`.
2. Second, the raw plugin Wasm itself, as an array of bytes. When not precompiled, this can be either the Wasm binary format (`.wasm`) or the Wasm textual format (`.wat`). When precompiled, this must be the precompiled plugin (`.wasm.pre`).
The `.init` method is asynchronous, and must be `.await`ed upon. If the plugin is malformed or doesn't import the right functions, an error will be raised.
#### 4. Get handles to all exported functions
Once the plugin has been compiled, it's time to start filling in the plugin struct defined earlier. In the case of `CoolPlugin` from earlier, this can be done as follows:
```rust
let mut cool_plugin = CoolPlugin {
format: plugin.function("format").unwrap(),
process: plugin.function("process").unwrap(),
runtime: plugin,
};
```
Because the struct definition defines the types of functions we're grabbing handles to, it's not required to specify the types of the functions here.
Note that, yet again, the names of guest-side functions we import must match exactly. Here's an example of what that implementation might look like:
```rust
use plugin::prelude::*;
#[export]
pub fn format(message: String) -> String {
format!("Cool Plugin says... '{}!'", message)
}
#[export]
pub fn process(numbers: Vec<f64>) -> f64 {
// Process by calculating the average
let mut total = 0.0;
for number in numbers.into_iter() {
total += number;
}
total / numbers.len()
}
```
That's it! Now you have a struct that holds an instance of a plugin. The last thing you need to know is how to call out the plugin you've defined...
### Using a plugin
To call a plugin function, use the async `.call` method on `Plugin`:
```rust
let average = cool_plugin.runtime
.call(
&cool_plugin.process,
vec![1.0, 2.0, 3.0],
)
.await
.unwrap();
```
The `.call` method takes two arguments:
1. A reference to the handle of the function we want to call.
2. The input argument to this function.
This method is async, and must be `.await`ed. If something goes wrong (e.g. the plugin panics, or there is a type mismatch between the plugin and `WasiFn`), then this method will return an error.
## Last Notes
This has been a brief overview of how the plugin system currently works in Zed. We hope to implement higher-level affordances as time goes on, to make writing plugins easier, and providing tooling so that users of Zed may also write plugins to extend their own editors.

View File

@@ -0,0 +1,88 @@
use std::{io::Write, path::Path};
use wasmtime::{Config, Engine};
fn main() {
let base = Path::new("../../plugins");
// Find all files and folders that don't change when rebuilt
let crates = std::fs::read_dir(base).expect("Could not find plugin directory");
for dir in crates {
let path = dir.unwrap().path();
let name = path.file_name().and_then(|x| x.to_str());
let is_dir = path.is_dir();
if is_dir && name != Some("target") && name != Some("bin") {
println!("cargo:rerun-if-changed={}", path.display());
}
}
// Clear out and recreate the plugin bin directory
let _ = std::fs::remove_dir_all(base.join("bin"));
let _ =
std::fs::create_dir_all(base.join("bin")).expect("Could not make plugins bin directory");
// Compile the plugins using the same profile as the current Zed build
let (profile_flags, profile_target) = match std::env::var("PROFILE").unwrap().as_str() {
"debug" => (&[][..], "debug"),
"release" => (&["--release"][..], "release"),
unknown => panic!("unknown profile `{}`", unknown),
};
// Invoke cargo to build the plugins
let build_successful = std::process::Command::new("cargo")
.args([
"build",
"--target",
"wasm32-wasi",
"--manifest-path",
base.join("Cargo.toml").to_str().unwrap(),
])
.args(profile_flags)
.status()
.expect("Could not build plugins")
.success();
assert!(build_successful);
// Find all compiled binaries
let engine = create_default_engine();
let binaries = std::fs::read_dir(base.join("target/wasm32-wasi").join(profile_target))
.expect("Could not find compiled plugins in target");
// Copy and precompile all compiled plugins we can find
for file in binaries {
let is_wasm = || {
let path = file.ok()?.path();
if path.extension()? == "wasm" {
Some(path)
} else {
None
}
};
if let Some(path) = is_wasm() {
let out_path = base.join("bin").join(path.file_name().unwrap());
std::fs::copy(&path, &out_path).expect("Could not copy compiled plugin to bin");
precompile(&out_path, &engine);
}
}
}
/// Creates a default engine for compiling Wasm.
fn create_default_engine() -> Engine {
let mut config = Config::default();
config.async_support(true);
Engine::new(&config).expect("Could not create engine")
}
fn precompile(path: &Path, engine: &Engine) {
let bytes = std::fs::read(path).expect("Could not read wasm module");
let compiled = engine
.precompile_module(&bytes)
.expect("Could not precompile module");
let out_path = path.parent().unwrap().join(&format!(
"{}.pre",
path.file_name().unwrap().to_string_lossy()
));
let mut out_file = std::fs::File::create(out_path)
.expect("Could not create output file for precompiled module");
out_file.write_all(&compiled).unwrap();
}

View File

@@ -0,0 +1,92 @@
pub mod plugin;
pub use plugin::*;
#[cfg(test)]
mod tests {
use super::*;
use pollster::FutureExt as _;
#[test]
pub fn test_plugin() {
pub struct TestPlugin {
noop: WasiFn<(), ()>,
constant: WasiFn<(), u32>,
identity: WasiFn<u32, u32>,
add: WasiFn<(u32, u32), u32>,
swap: WasiFn<(u32, u32), (u32, u32)>,
sort: WasiFn<Vec<u32>, Vec<u32>>,
print: WasiFn<String, ()>,
and_back: WasiFn<u32, u32>,
imports: WasiFn<u32, u32>,
half_async: WasiFn<u32, u32>,
echo_async: WasiFn<String, String>,
}
async {
let mut runtime = PluginBuilder::new_fuel_with_default_ctx(PluginYield::default_fuel())
.unwrap()
.host_function("mystery_number", |input: u32| input + 7)
.unwrap()
.host_function("import_noop", |_: ()| ())
.unwrap()
.host_function("import_identity", |input: u32| input)
.unwrap()
.host_function("import_swap", |(a, b): (u32, u32)| (b, a))
.unwrap()
.host_function_async("import_half", |a: u32| async move { a / 2 })
.unwrap()
.host_function_async("command_async", |command: String| async move {
let mut args = command.split(' ');
let command = args.next().unwrap();
smol::process::Command::new(command)
.args(args)
.output()
.await
.ok()
.map(|output| output.stdout)
})
.unwrap()
.init(PluginBinary::Wasm(
include_bytes!("../../../plugins/bin/test_plugin.wasm").as_ref(),
))
.await
.unwrap();
let plugin = TestPlugin {
noop: runtime.function("noop").unwrap(),
constant: runtime.function("constant").unwrap(),
identity: runtime.function("identity").unwrap(),
add: runtime.function("add").unwrap(),
swap: runtime.function("swap").unwrap(),
sort: runtime.function("sort").unwrap(),
print: runtime.function("print").unwrap(),
and_back: runtime.function("and_back").unwrap(),
imports: runtime.function("imports").unwrap(),
half_async: runtime.function("half_async").unwrap(),
echo_async: runtime.function("echo_async").unwrap(),
};
let unsorted = vec![1, 3, 4, 2, 5];
let sorted = vec![1, 2, 3, 4, 5];
assert_eq!(runtime.call(&plugin.noop, ()).await.unwrap(), ());
assert_eq!(runtime.call(&plugin.constant, ()).await.unwrap(), 27);
assert_eq!(runtime.call(&plugin.identity, 58).await.unwrap(), 58);
assert_eq!(runtime.call(&plugin.add, (3, 4)).await.unwrap(), 7);
assert_eq!(runtime.call(&plugin.swap, (1, 2)).await.unwrap(), (2, 1));
assert_eq!(runtime.call(&plugin.sort, unsorted).await.unwrap(), sorted);
assert_eq!(runtime.call(&plugin.print, "Hi!".into()).await.unwrap(), ());
assert_eq!(runtime.call(&plugin.and_back, 1).await.unwrap(), 8);
assert_eq!(runtime.call(&plugin.imports, 1).await.unwrap(), 8);
assert_eq!(runtime.call(&plugin.half_async, 4).await.unwrap(), 2);
assert_eq!(
runtime
.call(&plugin.echo_async, "eko".into())
.await
.unwrap(),
"eko\n"
);
}
.block_on()
}
}

View File

@@ -0,0 +1,682 @@
use std::future::Future;
use std::time::Duration;
use std::{fs::File, marker::PhantomData, path::Path};
use anyhow::{anyhow, Error};
use serde::{de::DeserializeOwned, Serialize};
use wasi_common::{dir, file};
use wasmtime::Memory;
use wasmtime::{
AsContext, AsContextMut, Caller, Config, Engine, Extern, Instance, Linker, Module, Store, Trap,
TypedFunc,
};
use wasmtime_wasi::{Dir, WasiCtx, WasiCtxBuilder};
/// Represents a resource currently managed by the plugin, like a file descriptor.
pub struct PluginResource(u32);
/// This is the buffer that is used Host side.
/// Note that it mirrors the functionality of
/// the `__Buffer` found in the `plugin/src/lib.rs` prelude.
struct WasiBuffer {
ptr: u32,
len: u32,
}
impl WasiBuffer {
pub fn into_u64(self) -> u64 {
((self.ptr as u64) << 32) | (self.len as u64)
}
pub fn from_u64(packed: u64) -> Self {
WasiBuffer {
ptr: (packed >> 32) as u32,
len: packed as u32,
}
}
}
/// Represents a typed WebAssembly function.
pub struct WasiFn<A: Serialize, R: DeserializeOwned> {
function: TypedFunc<u64, u64>,
_function_type: PhantomData<fn(A) -> R>,
}
impl<A: Serialize, R: DeserializeOwned> Copy for WasiFn<A, R> {}
impl<A: Serialize, R: DeserializeOwned> Clone for WasiFn<A, R> {
fn clone(&self) -> Self {
Self {
function: self.function,
_function_type: PhantomData,
}
}
}
pub struct PluginYieldEpoch {
delta: u64,
epoch: std::time::Duration,
}
pub struct PluginYieldFuel {
initial: u64,
refill: u64,
}
pub enum PluginYield {
Epoch {
yield_epoch: PluginYieldEpoch,
initialize_incrementer: Box<dyn FnOnce(Engine) -> () + Send>,
},
Fuel(PluginYieldFuel),
}
impl PluginYield {
pub fn default_epoch() -> PluginYieldEpoch {
PluginYieldEpoch {
delta: 1,
epoch: Duration::from_millis(1),
}
}
pub fn default_fuel() -> PluginYieldFuel {
PluginYieldFuel {
initial: 1000,
refill: 1000,
}
}
}
/// This struct is used to build a new [`Plugin`], using the builder pattern.
/// Create a new default plugin with `PluginBuilder::new_with_default_ctx`,
/// and add host-side exported functions using `host_function` and `host_function_async`.
/// Finalize the plugin by calling [`init`].
pub struct PluginBuilder {
wasi_ctx: WasiCtx,
engine: Engine,
linker: Linker<WasiCtxAlloc>,
yield_when: PluginYield,
}
impl PluginBuilder {
/// Creates an engine with the proper configuration given the yield mechanism in use
fn create_engine(yield_when: &PluginYield) -> Result<(Engine, Linker<WasiCtxAlloc>), Error> {
let mut config = Config::default();
config.async_support(true);
match yield_when {
PluginYield::Epoch { .. } => {
config.epoch_interruption(true);
}
PluginYield::Fuel(_) => {
config.consume_fuel(true);
}
}
let engine = Engine::new(&config)?;
let linker = Linker::new(&engine);
Ok((engine, linker))
}
/// Create a new [`PluginBuilder`] with the given WASI context.
/// Using the default context is a safe bet, see [`new_with_default_context`].
/// This plugin will yield after each fixed configurable epoch.
pub fn new_epoch<C>(
wasi_ctx: WasiCtx,
yield_epoch: PluginYieldEpoch,
spawn_detached_future: C,
) -> Result<Self, Error>
where
C: FnOnce(std::pin::Pin<Box<dyn Future<Output = ()> + Send + 'static>>) -> ()
+ Send
+ 'static,
{
// we can't create the future until after initializing
// because we need the engine to load the plugin
let epoch = yield_epoch.epoch;
let initialize_incrementer = Box::new(move |engine: Engine| {
spawn_detached_future(Box::pin(async move {
loop {
smol::Timer::after(epoch).await;
engine.increment_epoch();
}
}))
});
let yield_when = PluginYield::Epoch {
yield_epoch,
initialize_incrementer,
};
let (engine, linker) = Self::create_engine(&yield_when)?;
Ok(PluginBuilder {
wasi_ctx,
engine,
linker,
yield_when,
})
}
/// Create a new [`PluginBuilder`] with the given WASI context.
/// Using the default context is a safe bet, see [`new_with_default_context`].
/// This plugin will yield after a configurable amount of fuel is consumed.
pub fn new_fuel(wasi_ctx: WasiCtx, yield_fuel: PluginYieldFuel) -> Result<Self, Error> {
let yield_when = PluginYield::Fuel(yield_fuel);
let (engine, linker) = Self::create_engine(&yield_when)?;
Ok(PluginBuilder {
wasi_ctx,
engine,
linker,
yield_when,
})
}
/// Create a new `WasiCtx` that inherits the
/// host processes' access to `stdout` and `stderr`.
fn default_ctx() -> WasiCtx {
WasiCtxBuilder::new()
.inherit_stdout()
.inherit_stderr()
.build()
}
/// Create a new `PluginBuilder` with the default `WasiCtx` (see [`default_ctx`]).
/// This plugin will yield after each fixed configurable epoch.
pub fn new_epoch_with_default_ctx<C>(
yield_epoch: PluginYieldEpoch,
spawn_detached_future: C,
) -> Result<Self, Error>
where
C: FnOnce(std::pin::Pin<Box<dyn Future<Output = ()> + Send + 'static>>) -> ()
+ Send
+ 'static,
{
Self::new_epoch(Self::default_ctx(), yield_epoch, spawn_detached_future)
}
/// Create a new `PluginBuilder` with the default `WasiCtx` (see [`default_ctx`]).
/// This plugin will yield after a configurable amount of fuel is consumed.
pub fn new_fuel_with_default_ctx(yield_fuel: PluginYieldFuel) -> Result<Self, Error> {
Self::new_fuel(Self::default_ctx(), yield_fuel)
}
/// Add an `async` host function. See [`host_function`] for details.
pub fn host_function_async<F, A, R, Fut>(
mut self,
name: &str,
function: F,
) -> Result<Self, Error>
where
F: Fn(A) -> Fut + Send + Sync + 'static,
Fut: Future<Output = R> + Send + 'static,
A: DeserializeOwned + Send + 'static,
R: Serialize + Send + Sync + 'static,
{
self.linker.func_wrap1_async(
"env",
&format!("__{}", name),
move |mut caller: Caller<'_, WasiCtxAlloc>, packed_buffer: u64| {
// TODO: use try block once avaliable
let result: Result<(WasiBuffer, Memory, _), Trap> = (|| {
// grab a handle to the memory
let mut plugin_memory = match caller.get_export("memory") {
Some(Extern::Memory(mem)) => mem,
_ => return Err(Trap::new("Could not grab slice of plugin memory"))?,
};
let buffer = WasiBuffer::from_u64(packed_buffer);
// get the args passed from Guest
let args =
Plugin::buffer_to_bytes(&mut plugin_memory, caller.as_context(), &buffer)?;
let args: A = Plugin::deserialize_to_type(&args)?;
// Call the Host-side function
let result = function(args);
Ok((buffer, plugin_memory, result))
})();
Box::new(async move {
let (buffer, mut plugin_memory, future) = result?;
let result: R = future.await;
let result: Result<Vec<u8>, Error> = Plugin::serialize_to_bytes(result)
.map_err(|_| {
Trap::new("Could not serialize value returned from function").into()
});
let result = result?;
Plugin::buffer_to_free(caller.data().free_buffer(), &mut caller, buffer)
.await?;
let buffer = Plugin::bytes_to_buffer(
caller.data().alloc_buffer(),
&mut plugin_memory,
&mut caller,
result,
)
.await?;
Ok(buffer.into_u64())
})
},
)?;
Ok(self)
}
/// Add a new host function to the given `PluginBuilder`.
/// A host function is a function defined host-side, in Rust,
/// that is accessible guest-side, in WebAssembly.
/// You can specify host-side functions to import using
/// the `#[input]` macro attribute:
/// ```ignore
/// #[input]
/// fn total(counts: Vec<f64>) -> f64;
/// ```
/// When loading a plugin, you need to provide all host functions the plugin imports:
/// ```ignore
/// let plugin = PluginBuilder::new_with_default_context()
/// .host_function("total", |counts| counts.iter().fold(0.0, |tot, n| tot + n))
/// // and so on...
/// ```
/// And that's a wrap!
pub fn host_function<A, R>(
mut self,
name: &str,
function: impl Fn(A) -> R + Send + Sync + 'static,
) -> Result<Self, Error>
where
A: DeserializeOwned + Send,
R: Serialize + Send + Sync,
{
self.linker.func_wrap1_async(
"env",
&format!("__{}", name),
move |mut caller: Caller<'_, WasiCtxAlloc>, packed_buffer: u64| {
// TODO: use try block once avaliable
let result: Result<(WasiBuffer, Memory, Vec<u8>), Trap> = (|| {
// grab a handle to the memory
let mut plugin_memory = match caller.get_export("memory") {
Some(Extern::Memory(mem)) => mem,
_ => return Err(Trap::new("Could not grab slice of plugin memory"))?,
};
let buffer = WasiBuffer::from_u64(packed_buffer);
// get the args passed from Guest
let args = Plugin::buffer_to_type(&mut plugin_memory, &mut caller, &buffer)?;
// Call the Host-side function
let result: R = function(args);
// Serialize the result back to guest
let result = Plugin::serialize_to_bytes(result).map_err(|_| {
Trap::new("Could not serialize value returned from function")
})?;
Ok((buffer, plugin_memory, result))
})();
Box::new(async move {
let (buffer, mut plugin_memory, result) = result?;
Plugin::buffer_to_free(caller.data().free_buffer(), &mut caller, buffer)
.await?;
let buffer = Plugin::bytes_to_buffer(
caller.data().alloc_buffer(),
&mut plugin_memory,
&mut caller,
result,
)
.await?;
Ok(buffer.into_u64())
})
},
)?;
Ok(self)
}
/// Initializes a [`Plugin`] from a given compiled Wasm module.
/// Both binary (`.wasm`) and text (`.wat`) module formats are supported.
pub async fn init<'a>(self, binary: PluginBinary<'a>) -> Result<Plugin, Error> {
Plugin::init(binary, self).await
}
}
#[derive(Copy, Clone)]
struct WasiAlloc {
alloc_buffer: TypedFunc<u32, u32>,
free_buffer: TypedFunc<u64, ()>,
}
struct WasiCtxAlloc {
wasi_ctx: WasiCtx,
alloc: Option<WasiAlloc>,
}
impl WasiCtxAlloc {
fn alloc_buffer(&self) -> TypedFunc<u32, u32> {
self.alloc
.expect("allocator has been not initialized, cannot allocate buffer!")
.alloc_buffer
}
fn free_buffer(&self) -> TypedFunc<u64, ()> {
self.alloc
.expect("allocator has been not initialized, cannot free buffer!")
.free_buffer
}
fn init_alloc(&mut self, alloc: WasiAlloc) {
self.alloc = Some(alloc)
}
}
pub enum PluginBinary<'a> {
Wasm(&'a [u8]),
Precompiled(&'a [u8]),
}
/// Represents a WebAssembly plugin, with access to the WebAssembly System Inferface.
/// Build a new plugin using [`PluginBuilder`].
pub struct Plugin {
store: Store<WasiCtxAlloc>,
instance: Instance,
}
impl Plugin {
/// Dumps the *entirety* of Wasm linear memory to `stdout`.
/// Don't call this unless you're debugging a memory issue!
pub fn dump_memory(data: &[u8]) {
for (i, byte) in data.iter().enumerate() {
if i % 32 == 0 {
println!();
}
if i % 4 == 0 {
print!("|");
}
if *byte == 0 {
print!("__")
} else {
print!("{:02x}", byte);
}
}
println!();
}
async fn init<'a>(binary: PluginBinary<'a>, plugin: PluginBuilder) -> Result<Self, Error> {
// initialize the WebAssembly System Interface context
let engine = plugin.engine;
let mut linker = plugin.linker;
wasmtime_wasi::add_to_linker(&mut linker, |s| &mut s.wasi_ctx)?;
// create a store, note that we can't initialize the allocator,
// because we can't grab the functions until initialized.
let mut store: Store<WasiCtxAlloc> = Store::new(
&engine,
WasiCtxAlloc {
wasi_ctx: plugin.wasi_ctx,
alloc: None,
},
);
let module = match binary {
PluginBinary::Precompiled(bytes) => unsafe { Module::deserialize(&engine, bytes)? },
PluginBinary::Wasm(bytes) => Module::new(&engine, bytes)?,
};
// set up automatic yielding based on configuration
match plugin.yield_when {
PluginYield::Epoch {
yield_epoch: PluginYieldEpoch { delta, .. },
initialize_incrementer,
} => {
store.epoch_deadline_async_yield_and_update(delta);
initialize_incrementer(engine);
}
PluginYield::Fuel(PluginYieldFuel { initial, refill }) => {
store.add_fuel(initial).unwrap();
store.out_of_fuel_async_yield(u64::MAX, refill);
}
}
// load the provided module into the asynchronous runtime
linker.module_async(&mut store, "", &module).await?;
let instance = linker.instantiate_async(&mut store, &module).await?;
// now that the module is initialized,
// we can initialize the store's allocator
let alloc_buffer = instance.get_typed_func(&mut store, "__alloc_buffer")?;
let free_buffer = instance.get_typed_func(&mut store, "__free_buffer")?;
store.data_mut().init_alloc(WasiAlloc {
alloc_buffer,
free_buffer,
});
Ok(Plugin { store, instance })
}
/// Attaches a file or directory the the given system path to the runtime.
/// Note that the resource must be freed by calling `remove_resource` afterwards.
pub fn attach_path<T: AsRef<Path>>(&mut self, path: T) -> Result<PluginResource, Error> {
// grab the WASI context
let ctx = self.store.data_mut();
// open the file we want, and convert it into the right type
// this is a footgun and a half
let file = File::open(&path).unwrap();
let dir = Dir::from_std_file(file);
let dir = Box::new(wasmtime_wasi::dir::Dir::from_cap_std(dir));
// grab an empty file descriptor, specify capabilities
let fd = ctx.wasi_ctx.table().push(Box::new(()))?;
let caps = dir::DirCaps::all();
let file_caps = file::FileCaps::all();
// insert the directory at the given fd,
// return a handle to the resource
ctx.wasi_ctx
.insert_dir(fd, dir, caps, file_caps, path.as_ref().to_path_buf());
Ok(PluginResource(fd))
}
/// Returns `true` if the resource existed and was removed.
/// Currently the only resource we support is adding scoped paths (e.g. folders and files)
/// to plugins using [`attach_path`].
pub fn remove_resource(&mut self, resource: PluginResource) -> Result<(), Error> {
self.store
.data_mut()
.wasi_ctx
.table()
.delete(resource.0)
.ok_or_else(|| anyhow!("Resource did not exist, but a valid handle was passed in"))?;
Ok(())
}
// So this call function is kinda a dance, I figured it'd be a good idea to document it.
// the high level is we take a serde type, serialize it to a byte array,
// (we're doing this using bincode for now)
// then toss that byte array into webassembly.
// webassembly grabs that byte array, does some magic,
// and serializes the result into yet another byte array.
// we then grab *that* result byte array and deserialize it into a result.
//
// phew...
//
// now the problem is, webassambly doesn't support buffers.
// only really like i32s, that's it (yeah, it's sad. Not even unsigned!)
// (ok, I'm exaggerating a bit).
//
// the Wasm function that this calls must have a very specific signature:
//
// fn(pointer to byte array: i32, length of byte array: i32)
// -> pointer to (
// pointer to byte_array: i32,
// length of byte array: i32,
// ): i32
//
// This pair `(pointer to byte array, length of byte array)` is called a `Buffer`
// and can be found in the cargo_test plugin.
//
// so on the wasm side, we grab the two parameters to the function,
// stuff them into a `Buffer`,
// and then pray to the `unsafe` Rust gods above that a valid byte array pops out.
//
// On the flip side, when returning from a wasm function,
// we convert whatever serialized result we get into byte array,
// which we stuff into a Buffer and allocate on the heap,
// which pointer to we then return.
// Note the double indirection!
//
// So when returning from a function, we actually leak memory *twice*:
//
// 1) once when we leak the byte array
// 2) again when we leak the allocated `Buffer`
//
// This isn't a problem because Wasm stops executing after the function returns,
// so the heap is still valid for our inspection when we want to pull things out.
/// Serializes a given type to bytes.
fn serialize_to_bytes<A: Serialize>(item: A) -> Result<Vec<u8>, Error> {
// serialize the argument using bincode
let bytes = bincode::serialize(&item)?;
Ok(bytes)
}
/// Deserializes a given type from bytes.
fn deserialize_to_type<R: DeserializeOwned>(bytes: &[u8]) -> Result<R, Error> {
// serialize the argument using bincode
let bytes = bincode::deserialize(bytes)?;
Ok(bytes)
}
// fn deserialize<R: DeserializeOwned>(
// plugin_memory: &mut Memory,
// mut store: impl AsContextMut<Data = WasiCtxAlloc>,
// buffer: WasiBuffer,
// ) -> Result<R, Error> {
// let buffer_start = buffer.ptr as usize;
// let buffer_end = buffer_start + buffer.len as usize;
// // read the buffer at this point into a byte array
// // deserialize the byte array into the provided serde type
// let item = &plugin_memory.data(store.as_context())[buffer_start..buffer_end];
// let item = bincode::deserialize(bytes)?;
// Ok(item)
// }
/// Takes an item, allocates a buffer, serializes the argument to that buffer,
/// and returns a (ptr, len) pair to that buffer.
async fn bytes_to_buffer(
alloc_buffer: TypedFunc<u32, u32>,
plugin_memory: &mut Memory,
mut store: impl AsContextMut<Data = WasiCtxAlloc>,
item: Vec<u8>,
) -> Result<WasiBuffer, Error> {
// allocate a buffer and write the argument to that buffer
let len = item.len() as u32;
let ptr = alloc_buffer.call_async(&mut store, len).await?;
plugin_memory.write(&mut store, ptr as usize, &item)?;
Ok(WasiBuffer { ptr, len })
}
/// Takes a `(ptr, len)` pair and returns the corresponding deserialized buffer.
fn buffer_to_type<R: DeserializeOwned>(
plugin_memory: &Memory,
store: impl AsContext<Data = WasiCtxAlloc>,
buffer: &WasiBuffer,
) -> Result<R, Error> {
let buffer_start = buffer.ptr as usize;
let buffer_end = buffer_start + buffer.len as usize;
// read the buffer at this point into a byte array
// deserialize the byte array into the provided serde type
let result = &plugin_memory.data(store.as_context())[buffer_start..buffer_end];
let result = bincode::deserialize(result)?;
Ok(result)
}
/// Takes a `(ptr, len)` pair and returns the corresponding deserialized buffer.
fn buffer_to_bytes<'a>(
plugin_memory: &'a Memory,
store: wasmtime::StoreContext<'a, WasiCtxAlloc>,
buffer: &'a WasiBuffer,
) -> Result<&'a [u8], Error> {
let buffer_start = buffer.ptr as usize;
let buffer_end = buffer_start + buffer.len as usize;
// read the buffer at this point into a byte array
// deserialize the byte array into the provided serde type
let result = &plugin_memory.data(store)[buffer_start..buffer_end];
Ok(result)
}
async fn buffer_to_free(
free_buffer: TypedFunc<u64, ()>,
mut store: impl AsContextMut<Data = WasiCtxAlloc>,
buffer: WasiBuffer,
) -> Result<(), Error> {
// deallocate the argument buffer
Ok(free_buffer
.call_async(&mut store, buffer.into_u64())
.await?)
}
/// Retrieves the handle to a function of a given type.
pub fn function<A: Serialize, R: DeserializeOwned, T: AsRef<str>>(
&mut self,
name: T,
) -> Result<WasiFn<A, R>, Error> {
let fun_name = format!("__{}", name.as_ref());
let fun = self
.instance
.get_typed_func::<u64, u64, _>(&mut self.store, &fun_name)?;
Ok(WasiFn {
function: fun,
_function_type: PhantomData,
})
}
/// Asynchronously calls a function defined Guest-side.
pub async fn call<A: Serialize, R: DeserializeOwned>(
&mut self,
handle: &WasiFn<A, R>,
arg: A,
) -> Result<R, Error> {
let mut plugin_memory = self
.instance
.get_memory(&mut self.store, "memory")
.ok_or_else(|| anyhow!("Could not grab slice of plugin memory"))?;
// write the argument to linear memory
// this returns a (ptr, lentgh) pair
let arg_buffer = Self::bytes_to_buffer(
self.store.data().alloc_buffer(),
&mut plugin_memory,
&mut self.store,
Self::serialize_to_bytes(arg)?,
)
.await?;
// call the function, passing in the buffer and its length
// this returns a ptr to a (ptr, lentgh) pair
let result_buffer = handle
.function
.call_async(&mut self.store, arg_buffer.into_u64())
.await?;
Self::buffer_to_type(
&mut plugin_memory,
&mut self.store,
&WasiBuffer::from_u64(result_buffer),
)
}
}

View File

@@ -334,28 +334,6 @@ impl FakeFs {
})
}
pub async fn insert_dir(&self, path: impl AsRef<Path>) {
let mut state = self.state.lock().await;
let path = path.as_ref();
state.validate_path(path).unwrap();
let inode = state.next_inode;
state.next_inode += 1;
state.entries.insert(
path.to_path_buf(),
FakeFsEntry {
metadata: Metadata {
inode,
mtime: SystemTime::now(),
is_dir: true,
is_symlink: false,
},
content: None,
},
);
state.emit_event(&[path]).await;
}
pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
let mut state = self.state.lock().await;
let path = path.as_ref();
@@ -392,7 +370,7 @@ impl FakeFs {
match tree {
Object(map) => {
self.insert_dir(path).await;
self.create_dir(path).await.unwrap();
for (name, contents) in map {
let mut path = PathBuf::from(path);
path.push(name);
@@ -400,7 +378,7 @@ impl FakeFs {
}
}
Null => {
self.insert_dir(&path).await;
self.create_dir(&path).await.unwrap();
}
String(contents) => {
self.insert_file(&path, contents).await;

View File

@@ -4,7 +4,7 @@ use std::{ffi::OsStr, path::Path, sync::Arc};
pub enum IgnoreStack {
None,
Some {
base: Arc<Path>,
abs_base_path: Arc<Path>,
ignore: Arc<Gitignore>,
parent: Arc<IgnoreStack>,
},
@@ -24,19 +24,19 @@ impl IgnoreStack {
matches!(self, IgnoreStack::All)
}
pub fn append(self: Arc<Self>, base: Arc<Path>, ignore: Arc<Gitignore>) -> Arc<Self> {
pub fn append(self: Arc<Self>, abs_base_path: Arc<Path>, ignore: Arc<Gitignore>) -> Arc<Self> {
match self.as_ref() {
IgnoreStack::All => self,
_ => Arc::new(Self::Some {
base,
abs_base_path,
ignore,
parent: self,
}),
}
}
pub fn is_path_ignored(&self, path: &Path, is_dir: bool) -> bool {
if is_dir && path.file_name() == Some(OsStr::new(".git")) {
pub fn is_abs_path_ignored(&self, abs_path: &Path, is_dir: bool) -> bool {
if is_dir && abs_path.file_name() == Some(OsStr::new(".git")) {
return true;
}
@@ -44,11 +44,11 @@ impl IgnoreStack {
Self::None => false,
Self::All => true,
Self::Some {
base,
abs_base_path,
ignore,
parent: prev,
} => match ignore.matched(path.strip_prefix(base).unwrap(), is_dir) {
ignore::Match::None => prev.is_path_ignored(path, is_dir),
} => match ignore.matched(abs_path.strip_prefix(abs_base_path).unwrap(), is_dir) {
ignore::Match::None => prev.is_abs_path_ignored(abs_path, is_dir),
ignore::Match::Ignore(_) => true,
ignore::Match::Whitelist(_) => false,
},

View File

@@ -389,7 +389,7 @@ impl LspCommand for GetDefinition {
this.open_local_buffer_via_lsp(
target_uri,
language_server.server_id(),
lsp_adapter.name(),
lsp_adapter.name.clone(),
cx,
)
})
@@ -610,7 +610,7 @@ impl LspCommand for GetReferences {
this.open_local_buffer_via_lsp(
lsp_location.uri,
language_server.server_id(),
lsp_adapter.name(),
lsp_adapter.name.clone(),
cx,
)
})

File diff suppressed because it is too large Load Diff

View File

@@ -8,12 +8,12 @@ use language::{
};
use lsp::Url;
use serde_json::json;
use std::{cell::RefCell, os::unix, path::PathBuf, rc::Rc, task::Poll};
use std::{cell::RefCell, os::unix, rc::Rc, task::Poll};
use unindent::Unindent as _;
use util::{assert_set_eq, test::temp_tree};
#[gpui::test]
async fn test_populate_and_search(cx: &mut gpui::TestAppContext) {
async fn test_symlinks(cx: &mut gpui::TestAppContext) {
let dir = temp_tree(json!({
"root": {
"apple": "",
@@ -38,7 +38,6 @@ async fn test_populate_and_search(cx: &mut gpui::TestAppContext) {
.unwrap();
let project = Project::test(Arc::new(RealFs), [root_link_path.as_ref()], cx).await;
project.read_with(cx, |project, cx| {
let tree = project.worktrees(cx).next().unwrap().read(cx);
assert_eq!(tree.file_count(), 5);
@@ -47,27 +46,13 @@ async fn test_populate_and_search(cx: &mut gpui::TestAppContext) {
tree.inode_for_path("finnochio/grape")
);
});
let cancel_flag = Default::default();
let results = project
.read_with(cx, |project, cx| {
project.match_paths("bna", false, false, 10, &cancel_flag, cx)
})
.await;
assert_eq!(
results
.into_iter()
.map(|result| result.path)
.collect::<Vec<Arc<Path>>>(),
vec![
PathBuf::from("banana/carrot/date").into(),
PathBuf::from("banana/carrot/endive").into(),
]
);
}
#[gpui::test]
async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
async fn test_managing_language_servers(
deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext,
) {
cx.foreground().forbid_parking();
let mut rust_language = Language::new(
@@ -86,28 +71,32 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
},
None,
);
let mut fake_rust_servers = rust_language.set_fake_lsp_adapter(FakeLspAdapter {
name: "the-rust-language-server",
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
let mut fake_rust_servers = rust_language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
name: "the-rust-language-server",
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
..Default::default()
}),
..Default::default()
}),
},
..Default::default()
},
..Default::default()
});
let mut fake_json_servers = json_language.set_fake_lsp_adapter(FakeLspAdapter {
name: "the-json-language-server",
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![":".to_string()]),
}))
.await;
let mut fake_json_servers = json_language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
name: "the-json-language-server",
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![":".to_string()]),
..Default::default()
}),
..Default::default()
}),
},
..Default::default()
},
..Default::default()
});
}))
.await;
let fs = FakeFs::new(cx.background());
fs.insert_tree(
@@ -122,10 +111,6 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
project.update(cx, |project, _| {
project.languages.add(Arc::new(rust_language));
project.languages.add(Arc::new(json_language));
});
// Open a buffer without an associated language server.
let toml_buffer = project
@@ -135,13 +120,27 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
.await
.unwrap();
// Open a buffer with an associated language server.
// Open a buffer with an associated language server before the language for it has been loaded.
let rust_buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/the-root/test.rs", cx)
})
.await
.unwrap();
rust_buffer.read_with(cx, |buffer, _| {
assert_eq!(buffer.language().map(|l| l.name()), None);
});
// Now we add the languages to the project, and ensure they get assigned to all
// the relevant open buffers.
project.update(cx, |project, _| {
project.languages.add(Arc::new(json_language));
project.languages.add(Arc::new(rust_language));
});
deterministic.run_until_parked();
rust_buffer.read_with(cx, |buffer, _| {
assert_eq!(buffer.language().map(|l| l.name()), Some("Rust".into()));
});
// A server is started up, and it is notified about Rust files.
let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
@@ -611,11 +610,13 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
},
Some(tree_sitter_rust::language()),
);
let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
disk_based_diagnostics_progress_token: Some(progress_token),
disk_based_diagnostics_sources: &["disk"],
..Default::default()
});
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
disk_based_diagnostics_progress_token: Some(progress_token.into()),
disk_based_diagnostics_sources: vec!["disk".into()],
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.background());
fs.insert_tree(
@@ -734,11 +735,13 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
},
None,
);
let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
disk_based_diagnostics_sources: &["disk"],
disk_based_diagnostics_progress_token: Some(progress_token),
..Default::default()
});
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
disk_based_diagnostics_sources: vec!["disk".into()],
disk_based_diagnostics_progress_token: Some(progress_token.into()),
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.background());
fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
@@ -813,10 +816,12 @@ async fn test_toggling_enable_language_server(
},
None,
);
let mut fake_rust_servers = rust.set_fake_lsp_adapter(FakeLspAdapter {
name: "rust-lsp",
..Default::default()
});
let mut fake_rust_servers = rust
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
name: "rust-lsp",
..Default::default()
}))
.await;
let mut js = Language::new(
LanguageConfig {
name: Arc::from("JavaScript"),
@@ -825,10 +830,12 @@ async fn test_toggling_enable_language_server(
},
None,
);
let mut fake_js_servers = js.set_fake_lsp_adapter(FakeLspAdapter {
name: "js-lsp",
..Default::default()
});
let mut fake_js_servers = js
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
name: "js-lsp",
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.background());
fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" }))
@@ -876,7 +883,7 @@ async fn test_toggling_enable_language_server(
cx.update_global(|settings: &mut Settings, _| {
settings.language_overrides.insert(
Arc::from("Rust"),
settings::LanguageSettings {
settings::EditorSettings {
enable_language_server: Some(false),
..Default::default()
},
@@ -893,14 +900,14 @@ async fn test_toggling_enable_language_server(
cx.update_global(|settings: &mut Settings, _| {
settings.language_overrides.insert(
Arc::from("Rust"),
settings::LanguageSettings {
settings::EditorSettings {
enable_language_server: Some(true),
..Default::default()
},
);
settings.language_overrides.insert(
Arc::from("JavaScript"),
settings::LanguageSettings {
settings::EditorSettings {
enable_language_server: Some(false),
..Default::default()
},
@@ -934,10 +941,12 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
},
Some(tree_sitter_rust::language()),
);
let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
disk_based_diagnostics_sources: &["disk"],
..Default::default()
});
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
disk_based_diagnostics_sources: vec!["disk".into()],
..Default::default()
}))
.await;
let text = "
fn a() { A }
@@ -1276,7 +1285,7 @@ async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) {
},
Some(tree_sitter_rust::language()),
);
let mut fake_servers = language.set_fake_lsp_adapter(Default::default());
let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
let text = "
fn a() {
@@ -1645,28 +1654,6 @@ fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
chunks
}
#[gpui::test]
async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) {
let dir = temp_tree(json!({
"root": {
"dir1": {},
"dir2": {
"dir3": {}
}
}
}));
let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await;
let cancel_flag = Default::default();
let results = project
.read_with(cx, |project, cx| {
project.match_paths("dir", false, false, 10, &cancel_flag, cx)
})
.await;
assert!(results.is_empty());
}
#[gpui::test(iterations = 10)]
async fn test_definition(cx: &mut gpui::TestAppContext) {
let mut language = Language::new(
@@ -1677,7 +1664,7 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
},
Some(tree_sitter_rust::language()),
);
let mut fake_servers = language.set_fake_lsp_adapter(Default::default());
let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
let fs = FakeFs::new(cx.background());
fs.insert_tree(
@@ -1776,7 +1763,7 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
},
Some(tree_sitter_typescript::language_typescript()),
);
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
let fs = FakeFs::new(cx.background());
fs.insert_tree(
@@ -1850,6 +1837,59 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
);
}
#[gpui::test]
async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
let mut language = Language::new(
LanguageConfig {
name: "TypeScript".into(),
path_suffixes: vec!["ts".to_string()],
..Default::default()
},
Some(tree_sitter_typescript::language_typescript()),
);
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/dir",
json!({
"a.ts": "",
}),
)
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
let buffer = project
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
.await
.unwrap();
let fake_server = fake_language_servers.next().await.unwrap();
let text = "let a = b.fqn";
buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
let completions = project.update(cx, |project, cx| {
project.completions(&buffer, text.len(), cx)
});
fake_server
.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
Ok(Some(lsp::CompletionResponse::Array(vec![
lsp::CompletionItem {
label: "fullyQualifiedName?".into(),
insert_text: Some("fully\rQualified\r\nName".into()),
..Default::default()
},
])))
})
.next()
.await;
let completions = completions.await.unwrap();
assert_eq!(completions.len(), 1);
assert_eq!(completions[0].new_text, "fully\nQualified\nName");
}
#[gpui::test(iterations = 10)]
async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
let mut language = Language::new(
@@ -1860,7 +1900,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
},
None,
);
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
let fs = FakeFs::new(cx.background());
fs.insert_tree(
@@ -2788,16 +2828,18 @@ async fn test_rename(cx: &mut gpui::TestAppContext) {
},
Some(tree_sitter_rust::language()),
);
let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: Default::default(),
})),
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: Default::default(),
})),
..Default::default()
},
..Default::default()
},
..Default::default()
});
}))
.await;
let fs = FakeFs::new(cx.background());
fs.insert_tree(

View File

@@ -102,7 +102,7 @@ pub struct Snapshot {
#[derive(Clone)]
pub struct LocalSnapshot {
abs_path: Arc<Path>,
ignores: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
removed_entry_ids: HashMap<u64, ProjectEntryId>,
next_entry_id: Arc<AtomicUsize>,
snapshot: Snapshot,
@@ -370,7 +370,7 @@ impl LocalWorktree {
let tree = cx.add_model(move |cx: &mut ModelContext<Worktree>| {
let mut snapshot = LocalSnapshot {
abs_path,
ignores: Default::default(),
ignores_by_parent_abs_path: Default::default(),
removed_entry_ids: Default::default(),
next_entry_id,
snapshot: Snapshot {
@@ -819,8 +819,8 @@ impl LocalWorktree {
{
let mut snapshot = this.background_snapshot.lock();
entry.is_ignored = snapshot
.ignore_stack_for_path(&path, entry.is_dir())
.is_path_ignored(&path, entry.is_dir());
.ignore_stack_for_abs_path(&abs_path, entry.is_dir())
.is_abs_path_ignored(&abs_path, entry.is_dir());
if let Some(old_path) = old_path {
snapshot.remove_path(&old_path);
}
@@ -1331,11 +1331,12 @@ impl LocalSnapshot {
fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
if !entry.is_dir() && entry.path.file_name() == Some(&GITIGNORE) {
let abs_path = self.abs_path.join(&entry.path);
match build_gitignore(&abs_path, fs) {
match smol::block_on(build_gitignore(&abs_path, fs)) {
Ok(ignore) => {
let ignore_dir_path = entry.path.parent().unwrap();
self.ignores
.insert(ignore_dir_path.into(), (Arc::new(ignore), self.scan_id));
self.ignores_by_parent_abs_path.insert(
abs_path.parent().unwrap().into(),
(Arc::new(ignore), self.scan_id),
);
}
Err(error) => {
log::error!(
@@ -1387,7 +1388,10 @@ impl LocalSnapshot {
};
if let Some(ignore) = ignore {
self.ignores.insert(parent_path, (ignore, self.scan_id));
self.ignores_by_parent_abs_path.insert(
self.abs_path.join(&parent_path).into(),
(ignore, self.scan_id),
);
}
if matches!(parent_entry.kind, EntryKind::PendingDir) {
parent_entry.kind = EntryKind::Dir;
@@ -1472,16 +1476,20 @@ impl LocalSnapshot {
self.entries_by_id.edit(entries_by_id_edits, &());
if path.file_name() == Some(&GITIGNORE) {
if let Some((_, scan_id)) = self.ignores.get_mut(path.parent().unwrap()) {
let abs_parent_path = self.abs_path.join(path.parent().unwrap());
if let Some((_, scan_id)) = self
.ignores_by_parent_abs_path
.get_mut(abs_parent_path.as_path())
{
*scan_id = self.snapshot.scan_id;
}
}
}
fn ignore_stack_for_path(&self, path: &Path, is_dir: bool) -> Arc<IgnoreStack> {
fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool) -> Arc<IgnoreStack> {
let mut new_ignores = Vec::new();
for ancestor in path.ancestors().skip(1) {
if let Some((ignore, _)) = self.ignores.get(ancestor) {
for ancestor in abs_path.ancestors().skip(1) {
if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) {
new_ignores.push((ancestor, Some(ignore.clone())));
} else {
new_ignores.push((ancestor, None));
@@ -1489,16 +1497,16 @@ impl LocalSnapshot {
}
let mut ignore_stack = IgnoreStack::none();
for (parent_path, ignore) in new_ignores.into_iter().rev() {
if ignore_stack.is_path_ignored(&parent_path, true) {
for (parent_abs_path, ignore) in new_ignores.into_iter().rev() {
if ignore_stack.is_abs_path_ignored(&parent_abs_path, true) {
ignore_stack = IgnoreStack::all();
break;
} else if let Some(ignore) = ignore {
ignore_stack = ignore_stack.append(Arc::from(parent_path), ignore);
ignore_stack = ignore_stack.append(parent_abs_path.into(), ignore);
}
}
if ignore_stack.is_path_ignored(path, is_dir) {
if ignore_stack.is_abs_path_ignored(abs_path, is_dir) {
ignore_stack = IgnoreStack::all();
}
@@ -1506,8 +1514,8 @@ impl LocalSnapshot {
}
}
fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
let contents = smol::block_on(fs.load(&abs_path))?;
async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
let contents = fs.load(&abs_path).await?;
let parent = abs_path.parent().unwrap_or(Path::new("/"));
let mut builder = GitignoreBuilder::new(parent);
for line in contents.lines() {
@@ -2040,24 +2048,48 @@ impl BackgroundScanner {
async fn scan_dirs(&mut self) -> Result<()> {
let root_char_bag;
let root_abs_path;
let next_entry_id;
let is_dir;
{
let snapshot = self.snapshot.lock();
root_char_bag = snapshot.root_char_bag;
root_abs_path = snapshot.abs_path.clone();
next_entry_id = snapshot.next_entry_id.clone();
is_dir = snapshot.root_entry().map_or(false, |e| e.is_dir())
};
// Populate ignores above the root.
for ancestor in root_abs_path.ancestors().skip(1) {
if let Ok(ignore) = build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await
{
self.snapshot
.lock()
.ignores_by_parent_abs_path
.insert(ancestor.into(), (ignore.into(), 0));
}
}
let ignore_stack = {
let mut snapshot = self.snapshot.lock();
let ignore_stack = snapshot.ignore_stack_for_abs_path(&root_abs_path, true);
if ignore_stack.is_all() {
if let Some(mut root_entry) = snapshot.root_entry().cloned() {
root_entry.is_ignored = true;
snapshot.insert_entry(root_entry, self.fs.as_ref());
}
}
ignore_stack
};
if is_dir {
let path: Arc<Path> = Arc::from(Path::new(""));
let abs_path = self.abs_path();
let (tx, rx) = channel::unbounded();
self.executor
.block(tx.send(ScanJob {
abs_path: abs_path.to_path_buf(),
abs_path: root_abs_path.to_path_buf(),
path,
ignore_stack: IgnoreStack::none(),
ignore_stack,
scan_queue: tx.clone(),
}))
.unwrap();
@@ -2117,10 +2149,11 @@ impl BackgroundScanner {
// If we find a .gitignore, add it to the stack of ignores used to determine which paths are ignored
if child_name == *GITIGNORE {
match build_gitignore(&child_abs_path, self.fs.as_ref()) {
match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
Ok(ignore) => {
let ignore = Arc::new(ignore);
ignore_stack = ignore_stack.append(job.path.clone(), ignore.clone());
ignore_stack =
ignore_stack.append(job.abs_path.as_path().into(), ignore.clone());
new_ignore = Some(ignore);
}
Err(error) => {
@@ -2138,7 +2171,9 @@ impl BackgroundScanner {
// new jobs as well.
let mut new_jobs = new_jobs.iter_mut();
for entry in &mut new_entries {
entry.is_ignored = ignore_stack.is_path_ignored(&entry.path, entry.is_dir());
let entry_abs_path = self.abs_path().join(&entry.path);
entry.is_ignored =
ignore_stack.is_abs_path_ignored(&entry_abs_path, entry.is_dir());
if entry.is_dir() {
new_jobs.next().unwrap().ignore_stack = if entry.is_ignored {
IgnoreStack::all()
@@ -2157,7 +2192,7 @@ impl BackgroundScanner {
);
if child_metadata.is_dir {
let is_ignored = ignore_stack.is_path_ignored(&child_path, true);
let is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true);
child_entry.is_ignored = is_ignored;
new_entries.push(child_entry);
new_jobs.push(ScanJob {
@@ -2171,7 +2206,7 @@ impl BackgroundScanner {
scan_queue: job.scan_queue.clone(),
});
} else {
child_entry.is_ignored = ignore_stack.is_path_ignored(&child_path, false);
child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false);
new_entries.push(child_entry);
};
}
@@ -2200,8 +2235,8 @@ impl BackgroundScanner {
next_entry_id = snapshot.next_entry_id.clone();
}
let root_abs_path = if let Ok(abs_path) = self.fs.canonicalize(&root_abs_path).await {
abs_path
let root_canonical_path = if let Ok(path) = self.fs.canonicalize(&root_abs_path).await {
path
} else {
return false;
};
@@ -2221,27 +2256,29 @@ impl BackgroundScanner {
let mut snapshot = self.snapshot.lock();
snapshot.scan_id += 1;
for event in &events {
if let Ok(path) = event.path.strip_prefix(&root_abs_path) {
if let Ok(path) = event.path.strip_prefix(&root_canonical_path) {
snapshot.remove_path(&path);
}
}
for (event, metadata) in events.into_iter().zip(metadata.into_iter()) {
let path: Arc<Path> = match event.path.strip_prefix(&root_abs_path) {
let path: Arc<Path> = match event.path.strip_prefix(&root_canonical_path) {
Ok(path) => Arc::from(path.to_path_buf()),
Err(_) => {
log::error!(
"unexpected event {:?} for root path {:?}",
event.path,
root_abs_path
root_canonical_path
);
continue;
}
};
let abs_path = root_abs_path.join(&path);
match metadata {
Ok(Some(metadata)) => {
let ignore_stack = snapshot.ignore_stack_for_path(&path, metadata.is_dir);
let ignore_stack =
snapshot.ignore_stack_for_abs_path(&abs_path, metadata.is_dir);
let mut fs_entry = Entry::new(
path.clone(),
&metadata,
@@ -2253,7 +2290,7 @@ impl BackgroundScanner {
if metadata.is_dir {
self.executor
.block(scan_queue_tx.send(ScanJob {
abs_path: event.path,
abs_path,
path,
ignore_stack,
scan_queue: scan_queue_tx.clone(),
@@ -2301,37 +2338,42 @@ impl BackgroundScanner {
let mut ignores_to_update = Vec::new();
let mut ignores_to_delete = Vec::new();
for (parent_path, (_, scan_id)) in &snapshot.ignores {
if *scan_id == snapshot.scan_id && snapshot.entry_for_path(parent_path).is_some() {
ignores_to_update.push(parent_path.clone());
}
for (parent_abs_path, (_, scan_id)) in &snapshot.ignores_by_parent_abs_path {
if let Ok(parent_path) = parent_abs_path.strip_prefix(&snapshot.abs_path) {
if *scan_id == snapshot.scan_id && snapshot.entry_for_path(parent_path).is_some() {
ignores_to_update.push(parent_abs_path.clone());
}
let ignore_path = parent_path.join(&*GITIGNORE);
if snapshot.entry_for_path(ignore_path).is_none() {
ignores_to_delete.push(parent_path.clone());
let ignore_path = parent_path.join(&*GITIGNORE);
if snapshot.entry_for_path(ignore_path).is_none() {
ignores_to_delete.push(parent_abs_path.clone());
}
}
}
for parent_path in ignores_to_delete {
snapshot.ignores.remove(&parent_path);
self.snapshot.lock().ignores.remove(&parent_path);
for parent_abs_path in ignores_to_delete {
snapshot.ignores_by_parent_abs_path.remove(&parent_abs_path);
self.snapshot
.lock()
.ignores_by_parent_abs_path
.remove(&parent_abs_path);
}
let (ignore_queue_tx, ignore_queue_rx) = channel::unbounded();
ignores_to_update.sort_unstable();
let mut ignores_to_update = ignores_to_update.into_iter().peekable();
while let Some(parent_path) = ignores_to_update.next() {
while let Some(parent_abs_path) = ignores_to_update.next() {
while ignores_to_update
.peek()
.map_or(false, |p| p.starts_with(&parent_path))
.map_or(false, |p| p.starts_with(&parent_abs_path))
{
ignores_to_update.next().unwrap();
}
let ignore_stack = snapshot.ignore_stack_for_path(&parent_path, true);
let ignore_stack = snapshot.ignore_stack_for_abs_path(&parent_abs_path, true);
ignore_queue_tx
.send(UpdateIgnoreStatusJob {
path: parent_path,
abs_path: parent_abs_path,
ignore_stack,
ignore_queue: ignore_queue_tx.clone(),
})
@@ -2355,15 +2397,17 @@ impl BackgroundScanner {
async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) {
let mut ignore_stack = job.ignore_stack;
if let Some((ignore, _)) = snapshot.ignores.get(&job.path) {
ignore_stack = ignore_stack.append(job.path.clone(), ignore.clone());
if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) {
ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone());
}
let mut entries_by_id_edits = Vec::new();
let mut entries_by_path_edits = Vec::new();
for mut entry in snapshot.child_entries(&job.path).cloned() {
let path = job.abs_path.strip_prefix(&snapshot.abs_path).unwrap();
for mut entry in snapshot.child_entries(path).cloned() {
let was_ignored = entry.is_ignored;
entry.is_ignored = ignore_stack.is_path_ignored(&entry.path, entry.is_dir());
let abs_path = self.abs_path().join(&entry.path);
entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, entry.is_dir());
if entry.is_dir() {
let child_ignore_stack = if entry.is_ignored {
IgnoreStack::all()
@@ -2372,7 +2416,7 @@ impl BackgroundScanner {
};
job.ignore_queue
.send(UpdateIgnoreStatusJob {
path: entry.path.clone(),
abs_path: abs_path.into(),
ignore_stack: child_ignore_stack,
ignore_queue: job.ignore_queue.clone(),
})
@@ -2413,7 +2457,7 @@ struct ScanJob {
}
struct UpdateIgnoreStatusJob {
path: Arc<Path>,
abs_path: Arc<Path>,
ignore_stack: Arc<IgnoreStack>,
ignore_queue: Sender<UpdateIgnoreStatusJob>,
}
@@ -2766,23 +2810,28 @@ mod tests {
#[gpui::test]
async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
let dir = temp_tree(json!({
".git": {},
".gitignore": "ignored-dir\n",
"tracked-dir": {
"tracked-file1": "tracked contents",
},
"ignored-dir": {
"ignored-file1": "ignored contents",
let parent_dir = temp_tree(json!({
".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
"tree": {
".git": {},
".gitignore": "ignored-dir\n",
"tracked-dir": {
"tracked-file1": "",
"ancestor-ignored-file1": "",
},
"ignored-dir": {
"ignored-file1": ""
}
}
}));
let dir = parent_dir.path().join("tree");
let http_client = FakeHttpClient::with_404_response();
let client = Client::new(http_client.clone());
let tree = Worktree::local(
client,
dir.path(),
dir.as_path(),
true,
Arc::new(RealFs),
Default::default(),
@@ -2795,23 +2844,47 @@ mod tests {
tree.flush_fs_events(&cx).await;
cx.read(|cx| {
let tree = tree.read(cx);
let tracked = tree.entry_for_path("tracked-dir/tracked-file1").unwrap();
let ignored = tree.entry_for_path("ignored-dir/ignored-file1").unwrap();
assert_eq!(tracked.is_ignored, false);
assert_eq!(ignored.is_ignored, true);
assert!(
!tree
.entry_for_path("tracked-dir/tracked-file1")
.unwrap()
.is_ignored
);
assert!(
tree.entry_for_path("tracked-dir/ancestor-ignored-file1")
.unwrap()
.is_ignored
);
assert!(
tree.entry_for_path("ignored-dir/ignored-file1")
.unwrap()
.is_ignored
);
});
std::fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap();
std::fs::write(dir.path().join("ignored-dir/ignored-file2"), "").unwrap();
std::fs::write(dir.join("tracked-dir/tracked-file2"), "").unwrap();
std::fs::write(dir.join("tracked-dir/ancestor-ignored-file2"), "").unwrap();
std::fs::write(dir.join("ignored-dir/ignored-file2"), "").unwrap();
tree.flush_fs_events(&cx).await;
cx.read(|cx| {
let tree = tree.read(cx);
let dot_git = tree.entry_for_path(".git").unwrap();
let tracked = tree.entry_for_path("tracked-dir/tracked-file2").unwrap();
let ignored = tree.entry_for_path("ignored-dir/ignored-file2").unwrap();
assert_eq!(tracked.is_ignored, false);
assert_eq!(ignored.is_ignored, true);
assert_eq!(dot_git.is_ignored, true);
assert!(
!tree
.entry_for_path("tracked-dir/tracked-file2")
.unwrap()
.is_ignored
);
assert!(
tree.entry_for_path("tracked-dir/ancestor-ignored-file2")
.unwrap()
.is_ignored
);
assert!(
tree.entry_for_path("ignored-dir/ignored-file2")
.unwrap()
.is_ignored
);
assert!(tree.entry_for_path(".git").unwrap().is_ignored);
});
}
@@ -2891,7 +2964,7 @@ mod tests {
let mut initial_snapshot = LocalSnapshot {
abs_path: root_dir.path().into(),
removed_entry_ids: Default::default(),
ignores: Default::default(),
ignores_by_parent_abs_path: Default::default(),
next_entry_id: next_entry_id.clone(),
snapshot: Snapshot {
id: WorktreeId::from_usize(0),
@@ -3176,8 +3249,10 @@ mod tests {
.collect::<Vec<_>>();
assert_eq!(dfs_paths_via_traversal, dfs_paths_via_iter);
for (ignore_parent_path, _) in &self.ignores {
assert!(self.entry_for_path(ignore_parent_path).is_some());
for (ignore_parent_abs_path, _) in &self.ignores_by_parent_abs_path {
let ignore_parent_path =
ignore_parent_abs_path.strip_prefix(&self.abs_path).unwrap();
assert!(self.entry_for_path(&ignore_parent_path).is_some());
assert!(self
.entry_for_path(ignore_parent_path.join(&*GITIGNORE))
.is_some());

View File

@@ -290,7 +290,9 @@ mod tests {
},
None,
);
let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter::default());
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::<FakeLspAdapter>::default())
.await;
let fs = FakeFs::new(cx.background());
fs.insert_tree("/dir", json!({ "test.rs": "" })).await;

View File

@@ -28,7 +28,7 @@ rsa = "0.4"
serde = { version = "1.0", features = ["derive", "rc"] }
smol-timeout = "0.6"
tracing = { version = "0.1.34", features = ["log"] }
zstd = "0.9"
zstd = "0.11"
[build-dependencies]
prost-build = "0.9"

View File

@@ -608,8 +608,11 @@ mod tests {
let fonts = cx.font_cache();
let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
theme.search.match_background = Color::red();
let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap();
cx.update(|cx| cx.set_global(settings));
cx.update(|cx| {
let mut settings = Settings::test(cx);
settings.theme = Arc::new(theme);
cx.set_global(settings)
});
let buffer = cx.add_model(|cx| {
Buffer::new(

View File

@@ -329,6 +329,14 @@ impl Item for ProjectSearchView {
fn should_update_tab_on_event(event: &ViewEvent) -> bool {
matches!(event, ViewEvent::UpdateTab)
}
fn is_edit_event(event: &Self::Event) -> bool {
if let ViewEvent::EditorEvent(editor_event) = event {
Editor::is_edit_event(editor_event)
} else {
false
}
}
}
impl ProjectSearchView {
@@ -365,8 +373,10 @@ impl ProjectSearchView {
cx.emit(ViewEvent::EditorEvent(event.clone()))
})
.detach();
cx.observe_focus(&query_editor, |this, _, _| {
this.results_editor_was_focused = false;
cx.observe_focus(&query_editor, |this, _, focused, _| {
if focused {
this.results_editor_was_focused = false;
}
})
.detach();
@@ -377,8 +387,10 @@ impl ProjectSearchView {
});
cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
.detach();
cx.observe_focus(&results_editor, |this, _, _| {
this.results_editor_was_focused = true;
cx.observe_focus(&results_editor, |this, _, focused, _| {
if focused {
this.results_editor_was_focused = true;
}
})
.detach();
cx.subscribe(&results_editor, |this, _, event, cx| {
@@ -899,8 +911,11 @@ mod tests {
let fonts = cx.font_cache();
let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
theme.search.match_background = Color::red();
let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap();
cx.update(|cx| cx.set_global(settings));
cx.update(|cx| {
let mut settings = Settings::test(cx);
settings.theme = Arc::new(theme);
cx.set_global(settings)
});
let fs = FakeFs::new(cx.background());
fs.insert_tree(

View File

@@ -1,17 +1,18 @@
mod keymap_file;
use anyhow::Result;
use gpui::font_cache::{FamilyId, FontCache};
use gpui::{
font_cache::{FamilyId, FontCache},
AssetSource,
};
use schemars::{
gen::{SchemaGenerator, SchemaSettings},
schema::{
InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec, SubschemaValidation,
},
schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
JsonSchema,
};
use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value;
use std::{collections::HashMap, num::NonZeroU32, sync::Arc};
use std::{collections::HashMap, num::NonZeroU32, str, sync::Arc};
use theme::{Theme, ThemeRegistry};
use util::ResultExt as _;
@@ -24,21 +25,23 @@ pub struct Settings {
pub buffer_font_size: f32,
pub default_buffer_font_size: f32,
pub hover_popover_enabled: bool,
pub show_completions_on_input: bool,
pub vim_mode: bool,
pub autosave: Autosave,
pub language_settings: LanguageSettings,
pub language_defaults: HashMap<Arc<str>, LanguageSettings>,
pub language_overrides: HashMap<Arc<str>, LanguageSettings>,
pub editor_defaults: EditorSettings,
pub editor_overrides: EditorSettings,
pub language_defaults: HashMap<Arc<str>, EditorSettings>,
pub language_overrides: HashMap<Arc<str>, EditorSettings>,
pub theme: Arc<Theme>,
}
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
pub struct LanguageSettings {
pub struct EditorSettings {
pub tab_size: Option<NonZeroU32>,
pub hard_tabs: Option<bool>,
pub soft_wrap: Option<SoftWrap>,
pub preferred_line_length: Option<u32>,
pub format_on_save: Option<bool>,
pub format_on_save: Option<FormatOnSave>,
pub enable_language_server: Option<bool>,
}
@@ -50,6 +53,17 @@ pub enum SoftWrap {
PreferredLineLength,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum FormatOnSave {
Off,
LanguageServer,
External {
command: String,
arguments: Vec<String>,
},
}
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Autosave {
@@ -70,126 +84,65 @@ pub struct SettingsFileContent {
#[serde(default)]
pub hover_popover_enabled: Option<bool>,
#[serde(default)]
pub show_completions_on_input: Option<bool>,
#[serde(default)]
pub vim_mode: Option<bool>,
#[serde(default)]
pub format_on_save: Option<bool>,
#[serde(default)]
pub autosave: Option<Autosave>,
#[serde(default)]
pub enable_language_server: Option<bool>,
#[serde(flatten)]
pub editor: LanguageSettings,
pub editor: EditorSettings,
#[serde(default)]
pub language_overrides: HashMap<Arc<str>, LanguageSettings>,
#[serde(alias = "language_overrides")]
pub languages: HashMap<Arc<str>, EditorSettings>,
#[serde(default)]
pub theme: Option<String>,
}
impl Settings {
pub fn new(
buffer_font_family: &str,
pub fn defaults(
assets: impl AssetSource,
font_cache: &FontCache,
theme: Arc<Theme>,
) -> Result<Self> {
Ok(Self {
buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
buffer_font_size: 15.,
default_buffer_font_size: 15.,
hover_popover_enabled: true,
vim_mode: false,
autosave: Autosave::Off,
language_settings: Default::default(),
language_defaults: Default::default(),
language_overrides: Default::default(),
projects_online_by_default: true,
theme,
})
}
pub fn with_language_defaults(
mut self,
language_name: impl Into<Arc<str>>,
overrides: LanguageSettings,
themes: &ThemeRegistry,
) -> Self {
self.language_defaults
.insert(language_name.into(), overrides);
self
}
pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
self.language_setting(language, |settings| settings.tab_size)
.unwrap_or(4.try_into().unwrap())
}
pub fn hard_tabs(&self, language: Option<&str>) -> bool {
self.language_setting(language, |settings| settings.hard_tabs)
.unwrap_or(false)
}
pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap {
self.language_setting(language, |settings| settings.soft_wrap)
.unwrap_or(SoftWrap::None)
}
pub fn preferred_line_length(&self, language: Option<&str>) -> u32 {
self.language_setting(language, |settings| settings.preferred_line_length)
.unwrap_or(80)
}
pub fn format_on_save(&self, language: Option<&str>) -> bool {
self.language_setting(language, |settings| settings.format_on_save)
.unwrap_or(true)
}
pub fn enable_language_server(&self, language: Option<&str>) -> bool {
self.language_setting(language, |settings| settings.enable_language_server)
.unwrap_or(true)
}
fn language_setting<F, R>(&self, language: Option<&str>, f: F) -> Option<R>
where
F: Fn(&LanguageSettings) -> Option<R>,
{
let mut language_override = None;
let mut language_default = None;
if let Some(language) = language {
language_override = self.language_overrides.get(language).and_then(&f);
language_default = self.language_defaults.get(language).and_then(&f);
fn required<T>(value: Option<T>) -> Option<T> {
assert!(value.is_some(), "missing default setting value");
value
}
language_override
.or_else(|| f(&self.language_settings))
.or(language_default)
}
let defaults: SettingsFileContent = parse_json_with_comments(
str::from_utf8(assets.load("settings/default.json").unwrap().as_ref()).unwrap(),
)
.unwrap();
#[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &gpui::AppContext) -> Settings {
Settings {
buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
buffer_font_size: 14.,
default_buffer_font_size: 14.,
hover_popover_enabled: true,
vim_mode: false,
autosave: Autosave::Off,
language_settings: Default::default(),
language_defaults: Default::default(),
Self {
buffer_font_family: font_cache
.load_family(&[defaults.buffer_font_family.as_ref().unwrap()])
.unwrap(),
buffer_font_size: defaults.buffer_font_size.unwrap(),
default_buffer_font_size: defaults.buffer_font_size.unwrap(),
hover_popover_enabled: defaults.hover_popover_enabled.unwrap(),
show_completions_on_input: defaults.show_completions_on_input.unwrap(),
projects_online_by_default: defaults.projects_online_by_default.unwrap(),
vim_mode: defaults.vim_mode.unwrap(),
autosave: defaults.autosave.unwrap(),
editor_defaults: EditorSettings {
tab_size: required(defaults.editor.tab_size),
hard_tabs: required(defaults.editor.hard_tabs),
soft_wrap: required(defaults.editor.soft_wrap),
preferred_line_length: required(defaults.editor.preferred_line_length),
format_on_save: required(defaults.editor.format_on_save),
enable_language_server: required(defaults.editor.enable_language_server),
},
language_defaults: defaults.languages,
editor_overrides: Default::default(),
language_overrides: Default::default(),
projects_online_by_default: true,
theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
theme: themes.get(&defaults.theme.unwrap()).unwrap(),
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn test_async(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
let settings = Self::test(cx);
cx.set_global(settings.clone());
});
}
pub fn merge(
pub fn set_user_settings(
&mut self,
data: &SettingsFileContent,
data: SettingsFileContent,
theme_registry: &ThemeRegistry,
font_cache: &FontCache,
) {
@@ -211,47 +164,100 @@ impl Settings {
merge(&mut self.buffer_font_size, data.buffer_font_size);
merge(&mut self.default_buffer_font_size, data.buffer_font_size);
merge(&mut self.hover_popover_enabled, data.hover_popover_enabled);
merge(
&mut self.show_completions_on_input,
data.show_completions_on_input,
);
merge(&mut self.vim_mode, data.vim_mode);
merge(&mut self.autosave, data.autosave);
merge_option(
&mut self.language_settings.format_on_save,
data.format_on_save,
);
merge_option(
&mut self.language_settings.enable_language_server,
data.enable_language_server,
);
merge_option(&mut self.language_settings.soft_wrap, data.editor.soft_wrap);
merge_option(&mut self.language_settings.tab_size, data.editor.tab_size);
merge_option(
&mut self.language_settings.preferred_line_length,
data.editor.preferred_line_length,
);
for (language_name, settings) in data.language_overrides.clone().into_iter() {
let target = self
.language_overrides
.entry(language_name.into())
.or_default();
self.editor_overrides = data.editor;
self.language_overrides = data.languages;
}
merge_option(&mut target.tab_size, settings.tab_size);
merge_option(&mut target.soft_wrap, settings.soft_wrap);
merge_option(&mut target.format_on_save, settings.format_on_save);
merge_option(
&mut target.enable_language_server,
settings.enable_language_server,
);
merge_option(
&mut target.preferred_line_length,
settings.preferred_line_length,
);
pub fn with_language_defaults(
mut self,
language_name: impl Into<Arc<str>>,
overrides: EditorSettings,
) -> Self {
self.language_defaults
.insert(language_name.into(), overrides);
self
}
pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
self.language_setting(language, |settings| settings.tab_size)
}
pub fn hard_tabs(&self, language: Option<&str>) -> bool {
self.language_setting(language, |settings| settings.hard_tabs)
}
pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap {
self.language_setting(language, |settings| settings.soft_wrap)
}
pub fn preferred_line_length(&self, language: Option<&str>) -> u32 {
self.language_setting(language, |settings| settings.preferred_line_length)
}
pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave {
self.language_setting(language, |settings| settings.format_on_save.clone())
}
pub fn enable_language_server(&self, language: Option<&str>) -> bool {
self.language_setting(language, |settings| settings.enable_language_server)
}
fn language_setting<F, R>(&self, language: Option<&str>, f: F) -> R
where
F: Fn(&EditorSettings) -> Option<R>,
{
None.or_else(|| language.and_then(|l| self.language_overrides.get(l).and_then(&f)))
.or_else(|| f(&self.editor_overrides))
.or_else(|| language.and_then(|l| self.language_defaults.get(l).and_then(&f)))
.or_else(|| f(&self.editor_defaults))
.expect("missing default")
}
#[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &gpui::AppContext) -> Settings {
Settings {
buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
buffer_font_size: 14.,
default_buffer_font_size: 14.,
hover_popover_enabled: true,
show_completions_on_input: true,
vim_mode: false,
autosave: Autosave::Off,
editor_defaults: EditorSettings {
tab_size: Some(4.try_into().unwrap()),
hard_tabs: Some(false),
soft_wrap: Some(SoftWrap::None),
preferred_line_length: Some(80),
format_on_save: Some(FormatOnSave::LanguageServer),
enable_language_server: Some(true),
},
editor_overrides: Default::default(),
language_defaults: Default::default(),
language_overrides: Default::default(),
projects_online_by_default: true,
theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn test_async(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
let settings = Self::test(cx);
cx.set_global(settings.clone());
});
}
}
pub fn settings_file_json_schema(
theme_names: Vec<String>,
language_names: Vec<String>,
language_names: &[String],
) -> serde_json::Value {
let settings = SchemaSettings::draft07().with(|settings| {
settings.option_add_null_type = false;
@@ -259,77 +265,66 @@ pub fn settings_file_json_schema(
let generator = SchemaGenerator::new(settings);
let mut root_schema = generator.into_root_schema_for::<SettingsFileContent>();
// Construct theme names reference type
let theme_names = theme_names
.into_iter()
.map(|name| Value::String(name))
.collect();
let theme_names_schema = Schema::Object(SchemaObject {
// Create a schema for a theme name.
let theme_name_schema = SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
enum_values: Some(theme_names),
enum_values: Some(
theme_names
.into_iter()
.map(|name| Value::String(name))
.collect(),
),
..Default::default()
});
root_schema
.definitions
.insert("ThemeName".to_owned(), theme_names_schema);
};
// Construct language settings reference type
let language_settings_schema_reference = Schema::Object(SchemaObject {
reference: Some("#/definitions/LanguageSettings".to_owned()),
..Default::default()
});
let language_settings_properties = language_names
.into_iter()
.map(|name| {
(
name,
Schema::Object(SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
all_of: Some(vec![language_settings_schema_reference.clone()]),
..Default::default()
})),
..Default::default()
}),
)
})
.collect();
let language_overrides_schema = Schema::Object(SchemaObject {
// Create a schema for a 'languages overrides' object, associating editor
// settings with specific langauges.
assert!(root_schema.definitions.contains_key("EditorSettings"));
let languages_object_schema = SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
object: Some(Box::new(ObjectValidation {
properties: language_settings_properties,
properties: language_names
.iter()
.map(|name| {
(
name.clone(),
Schema::new_ref("#/definitions/EditorSettings".into()),
)
})
.collect(),
..Default::default()
})),
..Default::default()
});
};
// Add these new schemas as definitions, and modify properties of the root
// schema to reference them.
root_schema.definitions.extend([
("ThemeName".into(), theme_name_schema.into()),
("Languages".into(), languages_object_schema.into()),
]);
root_schema
.definitions
.insert("LanguageOverrides".to_owned(), language_overrides_schema);
.schema
.object
.as_mut()
.unwrap()
.properties
.extend([
(
"theme".to_owned(),
Schema::new_ref("#/definitions/ThemeName".into()),
),
(
"languages".to_owned(),
Schema::new_ref("#/definitions/Languages".into()),
),
// For backward compatibility
(
"language_overrides".to_owned(),
Schema::new_ref("#/definitions/Languages".into()),
),
]);
// Modify theme property to use new theme reference type
let settings_file_schema = root_schema.schema.object.as_mut().unwrap();
let language_overrides_schema_reference = Schema::Object(SchemaObject {
reference: Some("#/definitions/ThemeName".to_owned()),
..Default::default()
});
settings_file_schema.properties.insert(
"theme".to_owned(),
Schema::Object(SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
all_of: Some(vec![language_overrides_schema_reference]),
..Default::default()
})),
..Default::default()
}),
);
// Modify language_overrides property to use LanguageOverrides reference
settings_file_schema.properties.insert(
"language_overrides".to_owned(),
Schema::Object(SchemaObject {
reference: Some("#/definitions/LanguageOverrides".to_owned()),
..Default::default()
}),
);
serde_json::to_value(root_schema).unwrap()
}
@@ -339,12 +334,6 @@ fn merge<T: Copy>(target: &mut T, value: Option<T>) {
}
}
fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) {
if value.is_some() {
*target = value;
}
}
pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
Ok(serde_json::from_reader(
json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),

View File

@@ -21,7 +21,12 @@ mio-extras = "2.0.6"
futures = "0.3"
ordered-float = "2.1.1"
itertools = "0.10"
dirs = "4.0.0"
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
client = { path = "../client", features = ["test-support"]}
project = { path = "../project", features = ["test-support"]}
workspace = { path = "../workspace", features = ["test-support"] }

View File

@@ -0,0 +1,140 @@
use alacritty_terminal::{ansi::Color as AnsiColor, term::color::Rgb as AlacRgb};
use gpui::color::Color;
use theme::TerminalColors;
///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent
pub fn convert_color(alac_color: &AnsiColor, colors: &TerminalColors, modal: bool) -> Color {
let background = if modal {
colors.modal_background
} else {
colors.background
};
match alac_color {
//Named and theme defined colors
alacritty_terminal::ansi::Color::Named(n) => match n {
alacritty_terminal::ansi::NamedColor::Black => colors.black,
alacritty_terminal::ansi::NamedColor::Red => colors.red,
alacritty_terminal::ansi::NamedColor::Green => colors.green,
alacritty_terminal::ansi::NamedColor::Yellow => colors.yellow,
alacritty_terminal::ansi::NamedColor::Blue => colors.blue,
alacritty_terminal::ansi::NamedColor::Magenta => colors.magenta,
alacritty_terminal::ansi::NamedColor::Cyan => colors.cyan,
alacritty_terminal::ansi::NamedColor::White => colors.white,
alacritty_terminal::ansi::NamedColor::BrightBlack => colors.bright_black,
alacritty_terminal::ansi::NamedColor::BrightRed => colors.bright_red,
alacritty_terminal::ansi::NamedColor::BrightGreen => colors.bright_green,
alacritty_terminal::ansi::NamedColor::BrightYellow => colors.bright_yellow,
alacritty_terminal::ansi::NamedColor::BrightBlue => colors.bright_blue,
alacritty_terminal::ansi::NamedColor::BrightMagenta => colors.bright_magenta,
alacritty_terminal::ansi::NamedColor::BrightCyan => colors.bright_cyan,
alacritty_terminal::ansi::NamedColor::BrightWhite => colors.bright_white,
alacritty_terminal::ansi::NamedColor::Foreground => colors.foreground,
alacritty_terminal::ansi::NamedColor::Background => background,
alacritty_terminal::ansi::NamedColor::Cursor => colors.cursor,
alacritty_terminal::ansi::NamedColor::DimBlack => colors.dim_black,
alacritty_terminal::ansi::NamedColor::DimRed => colors.dim_red,
alacritty_terminal::ansi::NamedColor::DimGreen => colors.dim_green,
alacritty_terminal::ansi::NamedColor::DimYellow => colors.dim_yellow,
alacritty_terminal::ansi::NamedColor::DimBlue => colors.dim_blue,
alacritty_terminal::ansi::NamedColor::DimMagenta => colors.dim_magenta,
alacritty_terminal::ansi::NamedColor::DimCyan => colors.dim_cyan,
alacritty_terminal::ansi::NamedColor::DimWhite => colors.dim_white,
alacritty_terminal::ansi::NamedColor::BrightForeground => colors.bright_foreground,
alacritty_terminal::ansi::NamedColor::DimForeground => colors.dim_foreground,
},
//'True' colors
alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX),
//8 bit, indexed colors
alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(&(*i as usize), colors),
}
}
///Converts an 8 bit ANSI color to it's GPUI equivalent.
///Accepts usize for compatability with the alacritty::Colors interface,
///Other than that use case, should only be called with values in the [0,255] range
pub fn get_color_at_index(index: &usize, colors: &TerminalColors) -> Color {
match index {
//0-15 are the same as the named colors above
0 => colors.black,
1 => colors.red,
2 => colors.green,
3 => colors.yellow,
4 => colors.blue,
5 => colors.magenta,
6 => colors.cyan,
7 => colors.white,
8 => colors.bright_black,
9 => colors.bright_red,
10 => colors.bright_green,
11 => colors.bright_yellow,
12 => colors.bright_blue,
13 => colors.bright_magenta,
14 => colors.bright_cyan,
15 => colors.bright_white,
//16-231 are mapped to their RGB colors on a 0-5 range per channel
16..=231 => {
let (r, g, b) = rgb_for_index(&(*index as u8)); //Split the index into it's ANSI-RGB components
let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow
Color::new(r * step, g * step, b * step, u8::MAX) //Map the ANSI-RGB components to an RGB color
}
//232-255 are a 24 step grayscale from black to white
232..=255 => {
let i = *index as u8 - 232; //Align index to 0..24
let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks
Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale
}
//For compatability with the alacritty::Colors interface
256 => colors.foreground,
257 => colors.background,
258 => colors.cursor,
259 => colors.dim_black,
260 => colors.dim_red,
261 => colors.dim_green,
262 => colors.dim_yellow,
263 => colors.dim_blue,
264 => colors.dim_magenta,
265 => colors.dim_cyan,
266 => colors.dim_white,
267 => colors.bright_foreground,
268 => colors.black, //'Dim Background', non-standard color
_ => Color::new(0, 0, 0, 255),
}
}
///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube
///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit).
///
///Wikipedia gives a formula for calculating the index for a given color:
///
///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
///
///This function does the reverse, calculating the r, g, and b components from a given index.
fn rgb_for_index(i: &u8) -> (u8, u8, u8) {
debug_assert!(i >= &16 && i <= &231);
let i = i - 16;
let r = (i - (i % 36)) / 36;
let g = ((i % 36) - (i % 6)) / 6;
let b = (i % 36) % 6;
(r, g, b)
}
//Convenience method to convert from a GPUI color to an alacritty Rgb
pub fn to_alac_rgb(color: Color) -> AlacRgb {
AlacRgb {
r: color.r,
g: color.g,
b: color.g,
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_rgb_for_index() {
//Test every possible value in the color cube
for i in 16..=231 {
let (r, g, b) = crate::color_translation::rgb_for_index(&(i as u8));
assert_eq!(i, 16 + 36 * r + 6 * g + b);
}
}
}

View File

@@ -0,0 +1,195 @@
use alacritty_terminal::{
ansi::{ClearMode, Handler},
config::{Config, PtyConfig},
event::{Event as AlacTermEvent, Notify},
event_loop::{EventLoop, Msg, Notifier},
grid::Scroll,
sync::FairMutex,
term::SizeInfo,
tty::{self, setup_env},
Term,
};
use futures::{channel::mpsc::unbounded, StreamExt};
use settings::Settings;
use std::{collections::HashMap, path::PathBuf, sync::Arc};
use gpui::{ClipboardItem, CursorStyle, Entity, ModelContext};
use crate::{
color_translation::{get_color_at_index, to_alac_rgb},
ZedListener,
};
const DEFAULT_TITLE: &str = "Terminal";
///Upward flowing events, for changing the title and such
#[derive(Copy, Clone, Debug)]
pub enum Event {
TitleChanged,
CloseTerminal,
Activate,
Wakeup,
Bell,
}
pub struct TerminalConnection {
pub pty_tx: Notifier,
pub term: Arc<FairMutex<Term<ZedListener>>>,
pub title: String,
pub associated_directory: Option<PathBuf>,
}
impl TerminalConnection {
pub fn new(
working_directory: Option<PathBuf>,
initial_size: SizeInfo,
cx: &mut ModelContext<Self>,
) -> TerminalConnection {
let pty_config = PtyConfig {
shell: None, //Use the users default shell
working_directory: working_directory.clone(),
hold: false,
};
let mut env: HashMap<String, String> = HashMap::new();
//TODO: Properly set the current locale,
env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
let config = Config {
pty_config: pty_config.clone(),
env,
..Default::default()
};
setup_env(&config);
//Spawn a task so the Alacritty EventLoop can communicate with us in a view context
let (events_tx, mut events_rx) = unbounded();
//Set up the terminal...
let term = Term::new(&config, initial_size, ZedListener(events_tx.clone()));
let term = Arc::new(FairMutex::new(term));
//Setup the pty...
let pty = tty::new(&pty_config, &initial_size, None).expect("Could not create tty");
//And connect them together
let event_loop = EventLoop::new(
term.clone(),
ZedListener(events_tx.clone()),
pty,
pty_config.hold,
false,
);
//Kick things off
let pty_tx = event_loop.channel();
let _io_thread = event_loop.spawn();
cx.spawn_weak(|this, mut cx| async move {
//Listen for terminal events
while let Some(event) = events_rx.next().await {
match this.upgrade(&cx) {
Some(this) => {
this.update(&mut cx, |this, cx| {
this.process_terminal_event(event, cx);
cx.notify();
});
}
None => break,
}
}
})
.detach();
TerminalConnection {
pty_tx: Notifier(pty_tx),
term,
title: DEFAULT_TITLE.to_string(),
associated_directory: working_directory,
}
}
///Takes events from Alacritty and translates them to behavior on this view
fn process_terminal_event(
&mut self,
event: alacritty_terminal::event::Event,
cx: &mut ModelContext<Self>,
) {
match event {
// TODO: Handle is_self_focused in subscription on terminal view
AlacTermEvent::Wakeup => {
cx.emit(Event::Wakeup);
}
AlacTermEvent::PtyWrite(out) => self.write_to_pty(out),
AlacTermEvent::MouseCursorDirty => {
//Calculate new cursor style.
//TODO: alacritty/src/input.rs:L922-L939
//Check on correctly handling mouse events for terminals
cx.platform().set_cursor_style(CursorStyle::Arrow); //???
}
AlacTermEvent::Title(title) => {
self.title = title;
cx.emit(Event::TitleChanged);
}
AlacTermEvent::ResetTitle => {
self.title = DEFAULT_TITLE.to_string();
cx.emit(Event::TitleChanged);
}
AlacTermEvent::ClipboardStore(_, data) => {
cx.write_to_clipboard(ClipboardItem::new(data))
}
AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
&cx.read_from_clipboard()
.map(|ci| ci.text().to_string())
.unwrap_or("".to_string()),
)),
AlacTermEvent::ColorRequest(index, format) => {
let color = self.term.lock().colors()[index].unwrap_or_else(|| {
let term_style = &cx.global::<Settings>().theme.terminal;
to_alac_rgb(get_color_at_index(&index, &term_style.colors))
});
self.write_to_pty(format(color))
}
AlacTermEvent::CursorBlinkingChange => {
//TODO: Set a timer to blink the cursor on and off
}
AlacTermEvent::Bell => {
cx.emit(Event::Bell);
}
AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
}
}
///Write the Input payload to the tty. This locks the terminal so we can scroll it.
pub fn write_to_pty(&mut self, input: String) {
self.write_bytes_to_pty(input.into_bytes());
}
///Write the Input payload to the tty. This locks the terminal so we can scroll it.
fn write_bytes_to_pty(&mut self, input: Vec<u8>) {
self.term.lock().scroll_display(Scroll::Bottom);
self.pty_tx.notify(input);
}
///Resize the terminal and the PTY. This locks the terminal.
pub fn set_size(&mut self, new_size: SizeInfo) {
self.pty_tx.0.send(Msg::Resize(new_size)).ok();
self.term.lock().resize(new_size);
}
pub fn clear(&mut self) {
self.write_to_pty("\x0c".into());
self.term.lock().clear_screen(ClearMode::Saved);
}
}
impl Drop for TerminalConnection {
fn drop(&mut self) {
self.pty_tx.0.send(Msg::Shutdown).ok();
}
}
impl Entity for TerminalConnection {
type Event = Event;
}

View File

@@ -1,10 +0,0 @@
use gpui::geometry::rect::RectF;
pub fn paint_layer<F>(cx: &mut gpui::PaintContext, clip_bounds: Option<RectF>, f: F)
where
F: FnOnce(&mut gpui::PaintContext) -> (),
{
cx.scene.push_layer(clip_bounds);
f(cx);
cx.scene.pop_layer()
}

View File

@@ -0,0 +1,60 @@
use gpui::{ModelHandle, ViewContext};
use workspace::Workspace;
use crate::{get_wd_for_workspace, DeployModal, Event, Terminal, TerminalConnection};
#[derive(Debug)]
struct StoredConnection(ModelHandle<TerminalConnection>);
pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext<Workspace>) {
// Pull the terminal connection out of the global if it has been stored
let possible_connection =
cx.update_default_global::<Option<StoredConnection>, _, _>(|possible_connection, _| {
possible_connection.take()
});
if let Some(StoredConnection(stored_connection)) = possible_connection {
// Create a view from the stored connection
workspace.toggle_modal(cx, |_, cx| {
cx.add_view(|cx| Terminal::from_connection(stored_connection, true, cx))
});
} else {
// No connection was stored, create a new terminal
if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| {
let wd = get_wd_for_workspace(workspace, cx);
let this = cx.add_view(|cx| Terminal::new(wd, true, cx));
let connection_handle = this.read(cx).connection.clone();
cx.subscribe(&connection_handle, on_event).detach();
//Set the global immediately, in case the user opens the command palette
cx.set_global::<Option<StoredConnection>>(Some(StoredConnection(
connection_handle.clone(),
)));
this
}) {
let connection = closed_terminal_handle.read(cx).connection.clone();
cx.set_global(Some(StoredConnection(connection)));
}
}
//The problem is that the terminal modal is never re-stored.
}
pub fn on_event(
workspace: &mut Workspace,
_: ModelHandle<TerminalConnection>,
event: &Event,
cx: &mut ViewContext<Workspace>,
) {
// Dismiss the modal if the terminal quit
if let Event::CloseTerminal = event {
cx.set_global::<Option<StoredConnection>>(None);
if workspace
.modal()
.cloned()
.and_then(|modal| modal.downcast::<Terminal>())
.is_some()
{
workspace.dismiss_modal(cx)
}
}
}

View File

@@ -1,29 +1,31 @@
mod color_translation;
pub mod connection;
mod modal;
pub mod terminal_element;
use alacritty_terminal::{
config::{Config, Program, PtyConfig},
event::{Event as AlacTermEvent, EventListener, Notify},
event_loop::{EventLoop, Msg, Notifier},
event::{Event as AlacTermEvent, EventListener},
grid::Scroll,
sync::FairMutex,
term::{color::Rgb as AlacRgb, SizeInfo},
tty::{self, setup_env},
Term,
term::SizeInfo,
};
use futures::{
channel::mpsc::{unbounded, UnboundedSender},
StreamExt,
};
use connection::{Event, TerminalConnection};
use dirs::home_dir;
use editor::Input;
use futures::channel::mpsc::UnboundedSender;
use gpui::{
actions, color::Color, elements::*, impl_internal_actions, platform::CursorStyle,
ClipboardItem, Entity, MutableAppContext, View, ViewContext,
actions, elements::*, impl_internal_actions, AppContext, ClipboardItem, Entity, ModelHandle,
MutableAppContext, View, ViewContext,
};
use modal::deploy_modal;
use project::{Project, ProjectPath};
use settings::Settings;
use smallvec::SmallVec;
use std::{collections::HashMap, path::PathBuf, sync::Arc};
use std::path::PathBuf;
use workspace::{Item, Workspace};
use crate::terminal_element::{get_color_at_index, TerminalEl};
use crate::terminal_element::TerminalEl;
//ASCII Control characters on a keyboard
const ETX_CHAR: char = 3_u8 as char; //'End of text', the control code for 'ctrl-c'
@@ -35,14 +37,13 @@ const LEFT_SEQ: &str = "\x1b[D";
const RIGHT_SEQ: &str = "\x1b[C";
const UP_SEQ: &str = "\x1b[A";
const DOWN_SEQ: &str = "\x1b[B";
const DEFAULT_TITLE: &str = "Terminal";
const DEBUG_TERMINAL_WIDTH: f32 = 1000.; //This needs to be wide enough that the prompt can fill the whole space.
const DEBUG_TERMINAL_HEIGHT: f32 = 200.;
const DEBUG_CELL_WIDTH: f32 = 5.;
const DEBUG_LINE_HEIGHT: f32 = 5.;
pub mod gpui_func_tools;
pub mod terminal_element;
///Action for carrying the input to the PTY
#[derive(Clone, Default, Debug, PartialEq, Eq)]
pub struct Input(pub String);
//For bel, use a yellow dot. (equivalent to dirty file with conflict)
//For title, introduce max title length and
///Event to transmit the scroll from the element to the view
#[derive(Clone, Debug, PartialEq)]
@@ -50,26 +51,45 @@ pub struct ScrollTerminal(pub i32);
actions!(
terminal,
[Sigint, Escape, Del, Return, Left, Right, Up, Down, Tab, Clear, Paste, Deploy, Quit]
[
Sigint,
Escape,
Del,
Return,
Left,
Right,
Up,
Down,
Tab,
Clear,
Copy,
Paste,
Deploy,
Quit,
DeployModal,
]
);
impl_internal_actions!(terminal, [Input, ScrollTerminal]);
impl_internal_actions!(terminal, [ScrollTerminal]);
///Initialize and register all of our action handlers
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(Terminal::deploy);
cx.add_action(Terminal::write_to_pty);
cx.add_action(Terminal::send_sigint);
cx.add_action(Terminal::escape);
cx.add_action(Terminal::quit);
cx.add_action(Terminal::del);
cx.add_action(Terminal::carriage_return); //TODO figure out how to do this properly. Should we be checking the terminal mode?
cx.add_action(Terminal::carriage_return);
cx.add_action(Terminal::left);
cx.add_action(Terminal::right);
cx.add_action(Terminal::up);
cx.add_action(Terminal::down);
cx.add_action(Terminal::tab);
cx.add_action(Terminal::copy);
cx.add_action(Terminal::paste);
cx.add_action(Terminal::scroll_terminal);
cx.add_action(Terminal::input);
cx.add_action(Terminal::clear);
cx.add_action(deploy_modal);
}
///A translation struct for Alacritty to communicate with us from their event loop
@@ -84,19 +104,12 @@ impl EventListener for ZedListener {
///A terminal view, maintains the PTY's file handles and communicates with the terminal
pub struct Terminal {
pty_tx: Notifier,
term: Arc<FairMutex<Term<ZedListener>>>,
title: String,
connection: ModelHandle<TerminalConnection>,
has_new_content: bool,
has_bell: bool, //Currently using iTerm bell, show bell emoji in tab until input is received
cur_size: SizeInfo,
}
///Upward flowing events, for changing the title and such
pub enum Event {
TitleChanged,
CloseTerminal,
Activate,
//Currently using iTerm bell, show bell emoji in tab until input is received
has_bell: bool,
// Only for styling purposes. Doesn't effect behavior
modal: bool,
}
impl Entity for Terminal {
@@ -105,181 +118,86 @@ impl Entity for Terminal {
impl Terminal {
///Create a new Terminal view. This spawns a task, a thread, and opens the TTY devices
fn new(cx: &mut ViewContext<Self>, working_directory: Option<PathBuf>) -> Self {
//Spawn a task so the Alacritty EventLoop can communicate with us in a view context
let (events_tx, mut events_rx) = unbounded();
cx.spawn_weak(|this, mut cx| async move {
while let Some(event) = events_rx.next().await {
match this.upgrade(&cx) {
Some(handle) => {
handle.update(&mut cx, |this, cx| {
this.process_terminal_event(event, cx);
cx.notify();
});
}
None => break,
}
}
})
.detach();
let pty_config = PtyConfig {
shell: Some(Program::Just("zsh".to_string())),
working_directory,
hold: false,
};
//Does this mangle the zed Env? I'm guessing it does... do child processes have a seperate ENV?
let mut env: HashMap<String, String> = HashMap::new();
//TODO: Properly set the current locale,
env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
let config = Config {
pty_config: pty_config.clone(),
env,
..Default::default()
};
setup_env(&config);
///To get the right working directory from a workspace, use: `get_wd_for_workspace()`
fn new(working_directory: Option<PathBuf>, modal: bool, cx: &mut ViewContext<Self>) -> Self {
//The details here don't matter, the terminal will be resized on the first layout
//Set to something small for easier debugging
let size_info = SizeInfo::new(200., 100.0, 5., 5., 0., 0., false);
//Set up the terminal...
let term = Term::new(&config, size_info, ZedListener(events_tx.clone()));
let term = Arc::new(FairMutex::new(term));
//Setup the pty...
let pty = tty::new(&pty_config, &size_info, None).expect("Could not create tty");
//And connect them together
let event_loop = EventLoop::new(
term.clone(),
ZedListener(events_tx.clone()),
pty,
pty_config.hold,
let size_info = SizeInfo::new(
DEBUG_TERMINAL_WIDTH,
DEBUG_TERMINAL_HEIGHT,
DEBUG_CELL_WIDTH,
DEBUG_LINE_HEIGHT,
0.,
0.,
false,
);
//Kick things off
let pty_tx = Notifier(event_loop.channel());
let _io_thread = event_loop.spawn();
Terminal {
title: DEFAULT_TITLE.to_string(),
term,
pty_tx,
has_new_content: false,
has_bell: false,
cur_size: size_info,
}
let connection =
cx.add_model(|cx| TerminalConnection::new(working_directory, size_info, cx));
Terminal::from_connection(connection, modal, cx)
}
///Takes events from Alacritty and translates them to behavior on this view
fn process_terminal_event(
&mut self,
event: alacritty_terminal::event::Event,
fn from_connection(
connection: ModelHandle<TerminalConnection>,
modal: bool,
cx: &mut ViewContext<Self>,
) {
match event {
AlacTermEvent::Wakeup => {
if !cx.is_self_focused() {
self.has_new_content = true; //Change tab content
cx.emit(Event::TitleChanged);
} else {
) -> Terminal {
cx.observe(&connection, |_, _, cx| cx.notify()).detach();
cx.subscribe(&connection, |this, _, event, cx| match event {
Event::Wakeup => {
if cx.is_self_focused() {
cx.notify()
} else {
this.has_new_content = true;
cx.emit(Event::TitleChanged);
}
}
AlacTermEvent::PtyWrite(out) => self.write_to_pty(&Input(out), cx),
AlacTermEvent::MouseCursorDirty => {
//Calculate new cursor style.
//TODO
//Check on correctly handling mouse events for terminals
cx.platform().set_cursor_style(CursorStyle::Arrow); //???
}
AlacTermEvent::Title(title) => {
self.title = title;
Event::Bell => {
this.has_bell = true;
cx.emit(Event::TitleChanged);
}
AlacTermEvent::ResetTitle => {
self.title = DEFAULT_TITLE.to_string();
cx.emit(Event::TitleChanged);
}
AlacTermEvent::ClipboardStore(_, data) => {
cx.write_to_clipboard(ClipboardItem::new(data))
}
AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(
&Input(format(
&cx.read_from_clipboard()
.map(|ci| ci.text().to_string())
.unwrap_or("".to_string()),
)),
cx,
),
AlacTermEvent::ColorRequest(index, format) => {
let color = self.term.lock().colors()[index].unwrap_or_else(|| {
let term_style = &cx.global::<Settings>().theme.terminal;
match index {
0..=255 => to_alac_rgb(get_color_at_index(&(index as u8), term_style)),
//These additional values are required to match the Alacritty Colors object's behavior
256 => to_alac_rgb(term_style.foreground),
257 => to_alac_rgb(term_style.background),
258 => to_alac_rgb(term_style.cursor),
259 => to_alac_rgb(term_style.dim_black),
260 => to_alac_rgb(term_style.dim_red),
261 => to_alac_rgb(term_style.dim_green),
262 => to_alac_rgb(term_style.dim_yellow),
263 => to_alac_rgb(term_style.dim_blue),
264 => to_alac_rgb(term_style.dim_magenta),
265 => to_alac_rgb(term_style.dim_cyan),
266 => to_alac_rgb(term_style.dim_white),
267 => to_alac_rgb(term_style.bright_foreground),
268 => to_alac_rgb(term_style.black), //Dim Background, non-standard
_ => AlacRgb { r: 0, g: 0, b: 0 },
}
});
self.write_to_pty(&Input(format(color)), cx)
}
AlacTermEvent::CursorBlinkingChange => {
//TODO: Set a timer to blink the cursor on and off
}
AlacTermEvent::Bell => {
self.has_bell = true;
cx.emit(Event::TitleChanged);
}
AlacTermEvent::Exit => self.quit(&Quit, cx),
}
}
_ => cx.emit(*event),
})
.detach();
///Resize the terminal and the PTY. This locks the terminal.
fn set_size(&mut self, new_size: SizeInfo) {
if new_size != self.cur_size {
self.pty_tx.0.send(Msg::Resize(new_size)).ok();
self.term.lock().resize(new_size);
self.cur_size = new_size;
Terminal {
connection,
has_new_content: true,
has_bell: false,
modal,
}
}
///Scroll the terminal. This locks the terminal
fn scroll_terminal(&mut self, scroll: &ScrollTerminal, _: &mut ViewContext<Self>) {
self.term.lock().scroll_display(Scroll::Delta(scroll.0));
fn scroll_terminal(&mut self, scroll: &ScrollTerminal, cx: &mut ViewContext<Self>) {
self.connection
.read(cx)
.term
.lock()
.scroll_display(Scroll::Delta(scroll.0));
}
fn input(&mut self, Input(text): &Input, cx: &mut ViewContext<Self>) {
self.connection.update(cx, |connection, _| {
//TODO: This is probably not encoding UTF8 correctly (see alacritty/src/input.rs:L825-837)
connection.write_to_pty(text.clone());
});
if self.has_bell {
self.has_bell = false;
cx.emit(Event::TitleChanged);
}
}
fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
self.connection
.update(cx, |connection, _| connection.clear());
}
///Create a new Terminal in the current working directory or the user's home directory
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
let project = workspace.project().read(cx);
let abs_path = project
.active_entry()
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
.and_then(|worktree_handle| worktree_handle.read(cx).as_local())
.map(|wt| wt.abs_path().to_path_buf());
workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(cx, abs_path))), cx);
}
///Send the shutdown message to Alacritty
fn shutdown_pty(&mut self) {
self.pty_tx.0.send(Msg::Shutdown).ok();
let wd = get_wd_for_workspace(workspace, cx);
workspace.add_item(Box::new(cx.add_view(|cx| Terminal::new(wd, false, cx))), cx);
}
///Tell Zed to close us
@@ -288,76 +206,85 @@ impl Terminal {
}
///Attempt to paste the clipboard into the terminal
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
if let Some(item) = cx.read_from_clipboard() {
self.write_to_pty(&Input(item.text().to_owned()), cx);
fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
let term = self.connection.read(cx).term.lock();
let copy_text = term.selection_to_string();
match copy_text {
Some(s) => cx.write_to_clipboard(ClipboardItem::new(s)),
None => (),
}
}
///Write the Input payload to the tty. This locks the terminal so we can scroll it.
fn write_to_pty(&mut self, input: &Input, cx: &mut ViewContext<Self>) {
self.write_bytes_to_pty(input.0.clone().into_bytes(), cx);
}
///Write the Input payload to the tty. This locks the terminal so we can scroll it.
fn write_bytes_to_pty(&mut self, input: Vec<u8>, cx: &mut ViewContext<Self>) {
//iTerm bell behavior, bell stays until terminal is interacted with
self.has_bell = false;
cx.emit(Event::TitleChanged);
self.term.lock().scroll_display(Scroll::Bottom);
self.pty_tx.notify(input);
///Attempt to paste the clipboard into the terminal
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
if let Some(item) = cx.read_from_clipboard() {
self.connection.update(cx, |connection, _| {
connection.write_to_pty(item.text().to_owned());
})
}
}
///Send the `up` key
fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
self.write_to_pty(&Input(UP_SEQ.to_string()), cx);
self.connection.update(cx, |connection, _| {
connection.write_to_pty(UP_SEQ.to_string());
});
}
///Send the `down` key
fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
self.write_to_pty(&Input(DOWN_SEQ.to_string()), cx);
self.connection.update(cx, |connection, _| {
connection.write_to_pty(DOWN_SEQ.to_string());
});
}
///Send the `tab` key
fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
self.write_to_pty(&Input(TAB_CHAR.to_string()), cx);
self.connection.update(cx, |connection, _| {
connection.write_to_pty(TAB_CHAR.to_string());
});
}
///Send `SIGINT` (`ctrl-c`)
fn send_sigint(&mut self, _: &Sigint, cx: &mut ViewContext<Self>) {
self.write_to_pty(&Input(ETX_CHAR.to_string()), cx);
self.connection.update(cx, |connection, _| {
connection.write_to_pty(ETX_CHAR.to_string());
});
}
///Send the `escape` key
fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
self.write_to_pty(&Input(ESC_CHAR.to_string()), cx);
self.connection.update(cx, |connection, _| {
connection.write_to_pty(ESC_CHAR.to_string());
});
}
///Send the `delete` key. TODO: Difference between this and backspace?
fn del(&mut self, _: &Del, cx: &mut ViewContext<Self>) {
// self.write_to_pty(&Input("\x1b[3~".to_string()), cx)
self.write_to_pty(&Input(DEL_CHAR.to_string()), cx);
self.connection.update(cx, |connection, _| {
connection.write_to_pty(DEL_CHAR.to_string());
});
}
///Send a carriage return. TODO: May need to check the terminal mode.
fn carriage_return(&mut self, _: &Return, cx: &mut ViewContext<Self>) {
self.write_to_pty(&Input(CARRIAGE_RETURN_CHAR.to_string()), cx);
self.connection.update(cx, |connection, _| {
connection.write_to_pty(CARRIAGE_RETURN_CHAR.to_string());
});
}
//Send the `left` key
fn left(&mut self, _: &Left, cx: &mut ViewContext<Self>) {
self.write_to_pty(&Input(LEFT_SEQ.to_string()), cx);
self.connection.update(cx, |connection, _| {
connection.write_to_pty(LEFT_SEQ.to_string());
});
}
//Send the `right` key
fn right(&mut self, _: &Right, cx: &mut ViewContext<Self>) {
self.write_to_pty(&Input(RIGHT_SEQ.to_string()), cx);
}
}
impl Drop for Terminal {
fn drop(&mut self) {
self.shutdown_pty();
self.connection.update(cx, |connection, _| {
connection.write_to_pty(RIGHT_SEQ.to_string());
});
}
}
@@ -367,13 +294,33 @@ impl View for Terminal {
}
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
TerminalEl::new(cx.handle()).contained().boxed()
let element = {
let connection_handle = self.connection.clone().downgrade();
let view_id = cx.view_id();
TerminalEl::new(view_id, connection_handle, self.modal).contained()
};
if self.modal {
let settings = cx.global::<Settings>();
let container_style = settings.theme.terminal.modal_container;
element.with_style(container_style).boxed()
} else {
element.boxed()
}
}
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Activate);
self.has_new_content = false;
}
fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context {
let mut context = Self::default_keymap_context();
if self.modal {
context.set.insert("ModalTerminal".into());
}
context
}
}
impl Item for Terminal {
@@ -395,19 +342,33 @@ impl Item for Terminal {
};
flex.with_child(
Label::new(self.title.clone(), tab_theme.label.clone())
.aligned()
.contained()
.with_margin_left(if self.has_bell {
search_theme.tab_icon_spacing
} else {
0.
})
.boxed(),
Label::new(
self.connection.read(cx).title.clone(),
tab_theme.label.clone(),
)
.aligned()
.contained()
.with_margin_left(if self.has_bell {
search_theme.tab_icon_spacing
} else {
0.
})
.boxed(),
)
.boxed()
}
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self> {
//From what I can tell, there's no way to tell the current working
//Directory of the terminal from outside the terminal. There might be
//solutions to this, but they are non-trivial and require more IPC
Some(Terminal::new(
self.connection.read(cx).associated_directory.clone(),
false,
cx,
))
}
fn project_path(&self, _cx: &gpui::AppContext) -> Option<ProjectPath> {
None
}
@@ -468,40 +429,297 @@ impl Item for Terminal {
}
}
//Convenience method for less lines
fn to_alac_rgb(color: Color) -> AlacRgb {
AlacRgb {
r: color.r,
g: color.g,
b: color.g,
}
///Gets the intuitively correct working directory from the given workspace
///If there is an active entry for this project, returns that entry's worktree root.
///If there's no active entry but there is a worktree, returns that worktrees root.
///If either of these roots are files, or if there are any other query failures,
/// returns the user's home directory
fn get_wd_for_workspace(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
let project = workspace.project().read(cx);
project
.active_entry()
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
.or_else(|| workspace.worktrees(cx).next())
.and_then(|worktree_handle| worktree_handle.read(cx).as_local())
.and_then(|wt| {
wt.root_entry()
.filter(|re| re.is_dir())
.map(|_| wt.abs_path().to_path_buf())
})
.or_else(|| home_dir())
}
#[cfg(test)]
mod tests {
use super::*;
use alacritty_terminal::{grid::GridIterator, term::cell::Cell};
use alacritty_terminal::{
grid::GridIterator,
index::{Column, Line, Point, Side},
selection::{Selection, SelectionType},
term::cell::Cell,
};
use gpui::TestAppContext;
use itertools::Itertools;
use std::{path::Path, time::Duration};
use workspace::AppState;
///Basic integration test, can we get the terminal to show up, execute a command,
//and produce noticable output?
#[gpui::test]
async fn test_terminal(cx: &mut TestAppContext) {
let terminal = cx.add_view(Default::default(), |cx| Terminal::new(cx, None));
let terminal = cx.add_view(Default::default(), |cx| Terminal::new(None, false, cx));
terminal.update(cx, |terminal, cx| {
terminal.write_to_pty(&Input(("expr 3 + 4".to_string()).to_string()), cx);
terminal.connection.update(cx, |connection, _| {
connection.write_to_pty("expr 3 + 4".to_string());
});
terminal.carriage_return(&Return, cx);
});
cx.set_condition_duration(Some(Duration::from_secs(2)));
terminal
.condition(cx, |terminal, _cx| {
let term = terminal.term.clone();
.condition(cx, |terminal, cx| {
let term = terminal.connection.read(cx).term.clone();
let content = grid_as_str(term.lock().renderable_content().display_iter);
content.contains("7")
})
.await;
cx.set_condition_duration(None);
}
/// Integration test for selections, clipboard, and terminal execution
#[gpui::test]
async fn test_copy(cx: &mut TestAppContext) {
let mut result_line: i32 = 0;
let terminal = cx.add_view(Default::default(), |cx| Terminal::new(None, false, cx));
cx.set_condition_duration(Some(Duration::from_secs(2)));
terminal.update(cx, |terminal, cx| {
terminal.connection.update(cx, |connection, _| {
connection.write_to_pty("expr 3 + 4".to_string());
});
terminal.carriage_return(&Return, cx);
});
terminal
.condition(cx, |terminal, cx| {
let term = terminal.connection.read(cx).term.clone();
let content = grid_as_str(term.lock().renderable_content().display_iter);
if content.contains("7") {
let idx = content.chars().position(|c| c == '7').unwrap();
result_line = content.chars().take(idx).filter(|c| *c == '\n').count() as i32;
true
} else {
false
}
})
.await;
terminal.update(cx, |terminal, cx| {
let mut term = terminal.connection.read(cx).term.lock();
term.selection = Some(Selection::new(
SelectionType::Semantic,
Point::new(Line(2), Column(0)),
Side::Right,
));
drop(term);
terminal.copy(&Copy, cx)
});
cx.assert_clipboard_content(Some(&"7"));
cx.set_condition_duration(None);
}
///Working directory calculation tests
///No Worktrees in project -> home_dir()
#[gpui::test]
async fn no_worktree(cx: &mut TestAppContext) {
//Setup variables
let params = cx.update(AppState::test);
let project = Project::test(params.fs.clone(), [], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
//Test
cx.read(|cx| {
let workspace = workspace.read(cx);
let active_entry = project.read(cx).active_entry();
//Make sure enviroment is as expeted
assert!(active_entry.is_none());
assert!(workspace.worktrees(cx).next().is_none());
let res = get_wd_for_workspace(workspace, cx);
assert_eq!(res, home_dir())
});
}
///No active entry, but a worktree, worktree is a file -> home_dir()
#[gpui::test]
async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
//Setup variables
let params = cx.update(AppState::test);
let project = Project::test(params.fs.clone(), [], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
let (wt, _) = project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root.txt", true, cx)
})
.await
.unwrap();
cx.update(|cx| {
wt.update(cx, |wt, cx| {
wt.as_local()
.unwrap()
.create_entry(Path::new(""), false, cx)
})
})
.await
.unwrap();
//Test
cx.read(|cx| {
let workspace = workspace.read(cx);
let active_entry = project.read(cx).active_entry();
//Make sure enviroment is as expeted
assert!(active_entry.is_none());
assert!(workspace.worktrees(cx).next().is_some());
let res = get_wd_for_workspace(workspace, cx);
assert_eq!(res, home_dir())
});
}
//No active entry, but a worktree, worktree is a folder -> worktree_folder
#[gpui::test]
async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) {
//Setup variables
let params = cx.update(AppState::test);
let project = Project::test(params.fs.clone(), [], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
let (wt, _) = project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root/", true, cx)
})
.await
.unwrap();
//Setup root folder
cx.update(|cx| {
wt.update(cx, |wt, cx| {
wt.as_local().unwrap().create_entry(Path::new(""), true, cx)
})
})
.await
.unwrap();
//Test
cx.update(|cx| {
let workspace = workspace.read(cx);
let active_entry = project.read(cx).active_entry();
assert!(active_entry.is_none());
assert!(workspace.worktrees(cx).next().is_some());
let res = get_wd_for_workspace(workspace, cx);
assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
});
}
//Active entry with a work tree, worktree is a file -> home_dir()
#[gpui::test]
async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
//Setup variables
let params = cx.update(AppState::test);
let project = Project::test(params.fs.clone(), [], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
let (wt, _) = project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root.txt", true, cx)
})
.await
.unwrap();
//Setup root
let entry = cx
.update(|cx| {
wt.update(cx, |wt, cx| {
wt.as_local()
.unwrap()
.create_entry(Path::new(""), false, cx)
})
})
.await
.unwrap();
cx.update(|cx| {
let p = ProjectPath {
worktree_id: wt.read(cx).id(),
path: entry.path,
};
project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
});
//Test
cx.update(|cx| {
let workspace = workspace.read(cx);
let active_entry = project.read(cx).active_entry();
assert!(active_entry.is_some());
let res = get_wd_for_workspace(workspace, cx);
assert_eq!(res, home_dir());
});
}
//Active entry, with a worktree, worktree is a folder -> worktree_folder
#[gpui::test]
async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) {
//Setup variables
let params = cx.update(AppState::test);
let project = Project::test(params.fs.clone(), [], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
let (wt, _) = project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root/", true, cx)
})
.await
.unwrap();
//Setup root
let entry = cx
.update(|cx| {
wt.update(cx, |wt, cx| {
wt.as_local().unwrap().create_entry(Path::new(""), true, cx)
})
})
.await
.unwrap();
cx.update(|cx| {
let p = ProjectPath {
worktree_id: wt.read(cx).id(),
path: entry.path,
};
project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
});
//Test
cx.update(|cx| {
let workspace = workspace.read(cx);
let active_entry = project.read(cx).active_entry();
assert!(active_entry.is_some());
let res = get_wd_for_workspace(workspace, cx);
assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
});
}
pub(crate) fn grid_as_str(grid_iterator: GridIterator<Cell>) -> String {

View File

@@ -1,13 +1,15 @@
use alacritty_terminal::{
ansi::Color as AnsiColor,
grid::{Dimensions, GridIterator, Indexed},
index::Point,
index::{Column as GridCol, Line as GridLine, Point, Side},
selection::{Selection, SelectionRange, SelectionType},
sync::FairMutex,
term::{
cell::{Cell, Flags},
SizeInfo,
},
Term,
};
use editor::{Cursor, CursorShape};
use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine, Input};
use gpui::{
color::Color,
elements::*,
@@ -18,16 +20,20 @@ use gpui::{
},
json::json,
text_layout::{Line, RunStyle},
Event, FontCache, MouseRegion, PaintContext, Quad, SizeConstraint, TextLayoutCache,
WeakViewHandle,
Event, FontCache, KeyDownEvent, MouseRegion, PaintContext, Quad, ScrollWheelEvent,
SizeConstraint, TextLayoutCache, WeakModelHandle,
};
use itertools::Itertools;
use ordered_float::OrderedFloat;
use settings::Settings;
use std::rc::Rc;
use theme::TerminalStyle;
use crate::{gpui_func_tools::paint_layer, Input, ScrollTerminal, Terminal};
use std::{cmp::min, ops::Range, rc::Rc, sync::Arc};
use std::{fmt::Debug, ops::Sub};
use crate::{
color_translation::convert_color, connection::TerminalConnection, ScrollTerminal, ZedListener,
};
///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
///Scroll multiplier that is set to 3 by default. This will be removed when I
@@ -40,18 +46,34 @@ const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
const DEBUG_GRID: bool = false;
///The GPUI element that paints the terminal.
///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection?
pub struct TerminalEl {
view: WeakViewHandle<Terminal>,
connection: WeakModelHandle<TerminalConnection>,
view_id: usize,
modal: bool,
}
///Helper types so I don't mix these two up
///New type pattern so I don't mix these two up
struct CellWidth(f32);
struct LineHeight(f32);
struct LayoutLine {
cells: Vec<LayoutCell>,
highlighted_range: Option<Range<usize>>,
}
///New type pattern to ensure that we use adjusted mouse positions throughout the code base, rather than
struct PaneRelativePos(Vector2F);
///Functionally the constructor for the PaneRelativePos type, mutates the mouse_position
fn relative_pos(mouse_position: Vector2F, origin: Vector2F) -> PaneRelativePos {
PaneRelativePos(mouse_position.sub(origin)) //Avoid the extra allocation by mutating
}
#[derive(Clone, Debug, Default)]
struct LayoutCell {
point: Point<i32, i32>,
text: Line,
text: Line, //NOTE TO SELF THIS IS BAD PERFORMANCE RN!
background_color: Color,
}
@@ -67,18 +89,27 @@ impl LayoutCell {
///The information generated during layout that is nescessary for painting
pub struct LayoutState {
cells: Vec<(Point<i32, i32>, Line)>,
background_rects: Vec<(RectF, Color)>, //Vec index == Line index for the LineSpan
layout_lines: Vec<LayoutLine>,
line_height: LineHeight,
em_width: CellWidth,
cursor: Option<Cursor>,
background_color: Color,
cur_size: SizeInfo,
terminal: Arc<FairMutex<Term<ZedListener>>>,
selection_color: Color,
}
impl TerminalEl {
pub fn new(view: WeakViewHandle<Terminal>) -> TerminalEl {
TerminalEl { view }
pub fn new(
view_id: usize,
connection: WeakModelHandle<TerminalConnection>,
modal: bool,
) -> TerminalEl {
TerminalEl {
view_id,
connection,
modal,
}
}
}
@@ -100,51 +131,35 @@ impl Element for TerminalEl {
cx.font_cache()
.em_advance(text_style.font_id, text_style.font_size),
);
let view_handle = self.view.upgrade(cx).unwrap();
let connection_handle = self.connection.upgrade(cx).unwrap();
//Tell the view our new size. Requires a mutable borrow of cx and the view
let cur_size = make_new_size(constraint, &cell_width, &line_height);
//Note that set_size locks and mutates the terminal.
//TODO: Would be nice to lock once for the whole of layout
view_handle.update(cx.app, |view, _cx| view.set_size(cur_size));
connection_handle.update(cx.app, |connection, _| connection.set_size(cur_size));
//Now that we're done with the mutable portion, grab the immutable settings and view again
let terminal_theme = &(cx.global::<Settings>()).theme.terminal;
let term = view_handle.read(cx).term.lock();
let (selection_color, terminal_theme) = {
let theme = &(cx.global::<Settings>()).theme;
(theme.editor.selection.selection, &theme.terminal)
};
let terminal_mutex = connection_handle.read(cx).term.clone();
let term = terminal_mutex.lock();
let grid = term.grid();
let cursor_point = grid.cursor.point;
let cursor_text = grid[cursor_point.line][cursor_point.column].c.to_string();
let content = term.renderable_content();
let layout_cells = layout_cells(
let layout_lines = layout_lines(
content.display_iter,
&text_style,
terminal_theme,
cx.text_layout_cache,
self.modal,
content.selection,
);
let cells = layout_cells
.iter()
.map(|c| (c.point, c.text.clone()))
.collect::<Vec<(Point<i32, i32>, Line)>>();
let background_rects = layout_cells
.iter()
.map(|cell| {
(
RectF::new(
vec2f(
cell.point.column as f32 * cell_width.0,
cell.point.line as f32 * line_height.0,
),
vec2f(cell_width.0, line_height.0),
),
cell.background_color,
)
})
.collect::<Vec<(RectF, Color)>>();
let block_text = cx.text_layout_cache.layout_str(
&cursor_text,
text_style.font_size,
@@ -152,7 +167,7 @@ impl Element for TerminalEl {
cursor_text.len(),
RunStyle {
font_id: text_style.font_id,
color: terminal_theme.background,
color: terminal_theme.colors.background,
underline: Default::default(),
},
)],
@@ -178,22 +193,30 @@ impl Element for TerminalEl {
cursor_position,
block_width,
line_height.0,
terminal_theme.cursor,
terminal_theme.colors.cursor,
CursorShape::Block,
Some(block_text.clone()),
)
});
drop(term);
let background_color = if self.modal {
terminal_theme.colors.modal_background
} else {
terminal_theme.colors.background
};
(
constraint.max,
LayoutState {
cells,
layout_lines,
line_height,
em_width: cell_width,
cursor,
cur_size,
background_rects,
background_color: terminal_theme.background,
background_color,
terminal: terminal_mutex,
selection_color,
},
)
}
@@ -207,18 +230,22 @@ impl Element for TerminalEl {
) -> Self::PaintState {
//Setup element stuff
let clip_bounds = Some(visible_bounds);
paint_layer(cx, clip_bounds, |cx| {
//Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
cx.scene.push_mouse_region(MouseRegion {
view_id: self.view.id(),
mouse_down: Some(Rc::new(|_, cx| cx.focus_parent_view())),
bounds: visible_bounds,
..Default::default()
});
cx.paint_layer(clip_bounds, |cx| {
let cur_size = layout.cur_size.clone();
let origin = bounds.origin() + vec2f(layout.em_width.0, 0.);
paint_layer(cx, clip_bounds, |cx| {
//Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
attach_mouse_handlers(
origin,
cur_size,
self.view_id,
&layout.terminal,
visible_bounds,
cx,
);
cx.paint_layer(clip_bounds, |cx| {
//Start with a background color
cx.scene.push_quad(Quad {
bounds: RectF::new(bounds.origin(), bounds.size()),
@@ -228,40 +255,97 @@ impl Element for TerminalEl {
});
//Draw cell backgrounds
for background_rect in &layout.background_rects {
let new_origin = origin + background_rect.0.origin();
cx.scene.push_quad(Quad {
bounds: RectF::new(new_origin, background_rect.0.size()),
background: Some(background_rect.1),
border: Default::default(),
corner_radius: 0.,
})
for layout_line in &layout.layout_lines {
for layout_cell in &layout_line.cells {
let position = vec2f(
origin.x() + layout_cell.point.column as f32 * layout.em_width.0,
origin.y() + layout_cell.point.line as f32 * layout.line_height.0,
);
let size = vec2f(layout.em_width.0, layout.line_height.0);
cx.scene.push_quad(Quad {
bounds: RectF::new(position, size),
background: Some(layout_cell.background_color),
border: Default::default(),
corner_radius: 0.,
})
}
}
});
//Draw text
paint_layer(cx, clip_bounds, |cx| {
for (point, cell) in &layout.cells {
let cell_origin = vec2f(
origin.x() + point.column as f32 * layout.em_width.0,
origin.y() + point.line as f32 * layout.line_height.0,
);
cell.paint(cell_origin, visible_bounds, layout.line_height.0, cx);
//Draw Selection
cx.paint_layer(clip_bounds, |cx| {
let mut highlight_y = None;
let highlight_lines = layout
.layout_lines
.iter()
.filter_map(|line| {
if let Some(range) = &line.highlighted_range {
if let None = highlight_y {
highlight_y = Some(
origin.y()
+ line.cells[0].point.line as f32 * layout.line_height.0,
);
}
let start_x = origin.x()
+ line.cells[range.start].point.column as f32 * layout.em_width.0;
let end_x = origin.x()
+ line.cells[range.end].point.column as f32 * layout.em_width.0
+ layout.em_width.0;
return Some(HighlightedRangeLine { start_x, end_x });
} else {
return None;
}
})
.collect::<Vec<HighlightedRangeLine>>();
if let Some(y) = highlight_y {
let hr = HighlightedRange {
start_y: y, //Need to change this
line_height: layout.line_height.0,
lines: highlight_lines,
color: layout.selection_color,
//Copied from editor. TODO: move to theme or something
corner_radius: 0.15 * layout.line_height.0,
};
hr.paint(bounds, cx.scene);
}
});
cx.paint_layer(clip_bounds, |cx| {
for layout_line in &layout.layout_lines {
for layout_cell in &layout_line.cells {
let point = layout_cell.point;
//Don't actually know the start_x for a line, until here:
let cell_origin = vec2f(
origin.x() + point.column as f32 * layout.em_width.0,
origin.y() + point.line as f32 * layout.line_height.0,
);
layout_cell.text.paint(
cell_origin,
visible_bounds,
layout.line_height.0,
cx,
);
}
}
});
//Draw cursor
if let Some(cursor) = &layout.cursor {
paint_layer(cx, clip_bounds, |cx| {
cx.paint_layer(clip_bounds, |cx| {
cursor.paint(origin, cx);
})
}
#[cfg(debug_assertions)]
if DEBUG_GRID {
paint_layer(cx, clip_bounds, |cx| {
cx.paint_layer(clip_bounds, |cx| {
draw_debug_grid(bounds, layout, cx);
});
})
}
});
}
@@ -275,10 +359,29 @@ impl Element for TerminalEl {
_paint: &mut Self::PaintState,
cx: &mut gpui::EventContext,
) -> bool {
//The problem:
//Depending on the terminal mode, we either send an escape sequence
//OR update our own data structures.
//e.g. scrolling. If we do smooth scrolling, then we need to check if
//we own scrolling and then if so, do our scrolling thing.
//Ok, so the terminal connection should have APIs for querying it semantically
//something like `should_handle_scroll()`. This means we need a handle to the connection.
//Actually, this is the only time that this app needs to talk to the outer world.
//TODO for scrolling rework: need a way of intercepting Home/End/PageUp etc.
//Sometimes going to scroll our own internal buffer, sometimes going to send ESC
//
//Same goes for key events
//Actually, we don't use the terminal at all in dispatch_event code, the view
//Handles it all. Check how the editor implements scrolling, is it view-level
//or element level?
//Question: Can we continue dispatching to the view, so it can talk to the connection
//Or should we instead add a connection into here?
match event {
Event::ScrollWheel {
Event::ScrollWheel(ScrollWheelEvent {
delta, position, ..
} => visible_bounds
}) => visible_bounds
.contains_point(*position)
.then(|| {
let vertical_scroll =
@@ -286,9 +389,9 @@ impl Element for TerminalEl {
cx.dispatch_action(ScrollTerminal(vertical_scroll.round() as i32));
})
.is_some(),
Event::KeyDown {
Event::KeyDown(KeyDownEvent {
input: Some(input), ..
} => cx
}) => cx
.is_parent_view_focused()
.then(|| {
cx.dispatch_action(Input(input.to_string()));
@@ -311,6 +414,18 @@ impl Element for TerminalEl {
}
}
pub fn mouse_to_cell_data(
pos: Vector2F,
origin: Vector2F,
cur_size: SizeInfo,
display_offset: usize,
) -> (Point, alacritty_terminal::index::Direction) {
let relative_pos = relative_pos(pos, origin);
let point = grid_cell(&relative_pos, cur_size, display_offset);
let side = cell_side(&relative_pos, cur_size);
(point, side)
}
///Configures a text style from the current settings.
fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
TextStyle {
@@ -343,38 +458,57 @@ fn make_new_size(
)
}
fn layout_cells(
fn layout_lines(
grid: GridIterator<Cell>,
text_style: &TextStyle,
terminal_theme: &TerminalStyle,
text_layout_cache: &TextLayoutCache,
) -> Vec<LayoutCell> {
let mut line_count: i32 = 0;
modal: bool,
selection_range: Option<SelectionRange>,
) -> Vec<LayoutLine> {
let lines = grid.group_by(|i| i.point.line);
lines
.into_iter()
.map(|(_, line)| {
line_count += 1;
line.map(|indexed_cell| {
let cell_text = &indexed_cell.c.to_string();
.enumerate()
.map(|(line_index, (_, line))| {
let mut highlighted_range = None;
let cells = line
.enumerate()
.map(|(x_index, indexed_cell)| {
if selection_range
.map(|range| range.contains(indexed_cell.point))
.unwrap_or(false)
{
let mut range = highlighted_range.take().unwrap_or(x_index..x_index);
range.end = range.end.max(x_index);
highlighted_range = Some(range);
}
let cell_style = cell_style(&indexed_cell, terminal_theme, text_style);
let cell_text = &indexed_cell.c.to_string();
let layout_cell = text_layout_cache.layout_str(
cell_text,
text_style.font_size,
&[(cell_text.len(), cell_style)],
);
LayoutCell::new(
Point::new(line_count - 1, indexed_cell.point.column.0 as i32),
layout_cell,
convert_color(&indexed_cell.bg, terminal_theme),
)
})
.collect::<Vec<LayoutCell>>()
let cell_style = cell_style(&indexed_cell, terminal_theme, text_style, modal);
//This is where we might be able to get better performance
let layout_cell = text_layout_cache.layout_str(
cell_text,
text_style.font_size,
&[(cell_text.len(), cell_style)],
);
LayoutCell::new(
Point::new(line_index as i32, indexed_cell.point.column.0 as i32),
layout_cell,
convert_color(&indexed_cell.bg, &terminal_theme.colors, modal),
)
})
.collect::<Vec<LayoutCell>>();
LayoutLine {
cells,
highlighted_range,
}
})
.flatten()
.collect::<Vec<LayoutCell>>()
.collect::<Vec<LayoutLine>>()
}
// Compute the cursor position and expected block width, may return a zero width if x_for_index returns
@@ -410,9 +544,14 @@ fn get_cursor_shape(
}
///Convert the Alacritty cell styles to GPUI text styles and background color
fn cell_style(indexed: &Indexed<&Cell>, style: &TerminalStyle, text_style: &TextStyle) -> RunStyle {
fn cell_style(
indexed: &Indexed<&Cell>,
style: &TerminalStyle,
text_style: &TextStyle,
modal: bool,
) -> RunStyle {
let flags = indexed.cell.flags;
let fg = convert_color(&indexed.cell.fg, style);
let fg = convert_color(&indexed.cell.fg, &style.colors, modal);
let underline = flags
.contains(Flags::UNDERLINE)
@@ -430,98 +569,113 @@ fn cell_style(indexed: &Indexed<&Cell>, style: &TerminalStyle, text_style: &Text
}
}
///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent
fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color {
match alac_color {
//Named and theme defined colors
alacritty_terminal::ansi::Color::Named(n) => match n {
alacritty_terminal::ansi::NamedColor::Black => style.black,
alacritty_terminal::ansi::NamedColor::Red => style.red,
alacritty_terminal::ansi::NamedColor::Green => style.green,
alacritty_terminal::ansi::NamedColor::Yellow => style.yellow,
alacritty_terminal::ansi::NamedColor::Blue => style.blue,
alacritty_terminal::ansi::NamedColor::Magenta => style.magenta,
alacritty_terminal::ansi::NamedColor::Cyan => style.cyan,
alacritty_terminal::ansi::NamedColor::White => style.white,
alacritty_terminal::ansi::NamedColor::BrightBlack => style.bright_black,
alacritty_terminal::ansi::NamedColor::BrightRed => style.bright_red,
alacritty_terminal::ansi::NamedColor::BrightGreen => style.bright_green,
alacritty_terminal::ansi::NamedColor::BrightYellow => style.bright_yellow,
alacritty_terminal::ansi::NamedColor::BrightBlue => style.bright_blue,
alacritty_terminal::ansi::NamedColor::BrightMagenta => style.bright_magenta,
alacritty_terminal::ansi::NamedColor::BrightCyan => style.bright_cyan,
alacritty_terminal::ansi::NamedColor::BrightWhite => style.bright_white,
alacritty_terminal::ansi::NamedColor::Foreground => style.foreground,
alacritty_terminal::ansi::NamedColor::Background => style.background,
alacritty_terminal::ansi::NamedColor::Cursor => style.cursor,
alacritty_terminal::ansi::NamedColor::DimBlack => style.dim_black,
alacritty_terminal::ansi::NamedColor::DimRed => style.dim_red,
alacritty_terminal::ansi::NamedColor::DimGreen => style.dim_green,
alacritty_terminal::ansi::NamedColor::DimYellow => style.dim_yellow,
alacritty_terminal::ansi::NamedColor::DimBlue => style.dim_blue,
alacritty_terminal::ansi::NamedColor::DimMagenta => style.dim_magenta,
alacritty_terminal::ansi::NamedColor::DimCyan => style.dim_cyan,
alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white,
alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground,
alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground,
},
//'True' colors
alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX),
//8 bit, indexed colors
alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(i, style),
fn attach_mouse_handlers(
origin: Vector2F,
cur_size: SizeInfo,
view_id: usize,
terminal_mutex: &Arc<FairMutex<Term<ZedListener>>>,
visible_bounds: RectF,
cx: &mut PaintContext,
) {
let click_mutex = terminal_mutex.clone();
let drag_mutex = terminal_mutex.clone();
let mouse_down_mutex = terminal_mutex.clone();
cx.scene.push_mouse_region(MouseRegion {
view_id,
mouse_down: Some(Rc::new(move |pos, _| {
let mut term = mouse_down_mutex.lock();
let (point, side) = mouse_to_cell_data(
pos,
origin,
cur_size,
term.renderable_content().display_offset,
);
term.selection = Some(Selection::new(SelectionType::Simple, point, side))
})),
click: Some(Rc::new(move |pos, click_count, cx| {
let mut term = click_mutex.lock();
let (point, side) = mouse_to_cell_data(
pos,
origin,
cur_size,
term.renderable_content().display_offset,
);
let selection_type = match click_count {
0 => return, //This is a release
1 => Some(SelectionType::Simple),
2 => Some(SelectionType::Semantic),
3 => Some(SelectionType::Lines),
_ => None,
};
let selection =
selection_type.map(|selection_type| Selection::new(selection_type, point, side));
term.selection = selection;
cx.focus_parent_view();
cx.notify();
})),
bounds: visible_bounds,
drag: Some(Rc::new(move |_delta, pos, cx| {
let mut term = drag_mutex.lock();
let (point, side) = mouse_to_cell_data(
pos,
origin,
cur_size,
term.renderable_content().display_offset,
);
if let Some(mut selection) = term.selection.take() {
selection.update(point, side);
term.selection = Some(selection);
}
cx.notify();
})),
..Default::default()
});
}
///Copied (with modifications) from alacritty/src/input.rs > Processor::cell_side()
fn cell_side(pos: &PaneRelativePos, cur_size: SizeInfo) -> Side {
let x = pos.0.x() as usize;
let cell_x = x.saturating_sub(cur_size.cell_width() as usize) % cur_size.cell_width() as usize;
let half_cell_width = (cur_size.cell_width() / 2.0) as usize;
let additional_padding =
(cur_size.width() - cur_size.cell_width() * 2.) % cur_size.cell_width();
let end_of_grid = cur_size.width() - cur_size.cell_width() - additional_padding;
if cell_x > half_cell_width
// Edge case when mouse leaves the window.
|| x as f32 >= end_of_grid
{
Side::Right
} else {
Side::Left
}
}
///Converts an 8 bit ANSI color to it's GPUI equivalent.
pub fn get_color_at_index(index: &u8, style: &TerminalStyle) -> Color {
match index {
//0-15 are the same as the named colors above
0 => style.black,
1 => style.red,
2 => style.green,
3 => style.yellow,
4 => style.blue,
5 => style.magenta,
6 => style.cyan,
7 => style.white,
8 => style.bright_black,
9 => style.bright_red,
10 => style.bright_green,
11 => style.bright_yellow,
12 => style.bright_blue,
13 => style.bright_magenta,
14 => style.bright_cyan,
15 => style.bright_white,
//16-231 are mapped to their RGB colors on a 0-5 range per channel
16..=231 => {
let (r, g, b) = rgb_for_index(index); //Split the index into it's ANSI-RGB components
let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow
Color::new(r * step, g * step, b * step, u8::MAX) //Map the ANSI-RGB components to an RGB color
}
//232-255 are a 24 step grayscale from black to white
232..=255 => {
let i = index - 232; //Align index to 0..24
let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks
Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale
}
}
}
///Copied (with modifications) from alacritty/src/event.rs > Mouse::point()
///Position is a pane-relative position. That means the top left corner of the mouse
///Region should be (0,0)
fn grid_cell(pos: &PaneRelativePos, cur_size: SizeInfo, display_offset: usize) -> Point {
let pos = pos.0;
let col = pos.x() / cur_size.cell_width(); //TODO: underflow...
let col = min(GridCol(col as usize), cur_size.last_column());
///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube
///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit).
///
///Wikipedia gives a formula for calculating the index for a given color:
///
///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
///
///This function does the reverse, calculating the r, g, and b components from a given index.
fn rgb_for_index(i: &u8) -> (u8, u8, u8) {
debug_assert!(i >= &16 && i <= &231);
let i = i - 16;
let r = (i - (i % 36)) / 36;
let g = ((i % 36) - (i % 6)) / 6;
let b = (i % 36) % 6;
(r, g, b)
let line = pos.y() / cur_size.cell_height();
let line = min(line as i32, cur_size.bottommost_line().0);
//when clicking, need to ADD to get to the top left cell
//e.g. total_lines - viewport_height, THEN subtract display offset
//0 -> total_lines - viewport_height - display_offset + mouse_line
Point::new(GridLine(line - display_offset as i32), col)
}
///Draws the grid as Alacritty sees it. Useful for checking if there is an inconsistency between
@@ -555,14 +709,73 @@ fn draw_debug_grid(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContex
}
}
#[cfg(test)]
mod tests {
mod test {
#[test]
fn test_rgb_for_index() {
//Test every possible value in the color cube
for i in 16..=231 {
let (r, g, b) = crate::terminal_element::rgb_for_index(&(i as u8));
assert_eq!(i, 16 + 36 * r + 6 * g + b);
}
fn test_mouse_to_selection() {
let term_width = 100.;
let term_height = 200.;
let cell_width = 10.;
let line_height = 20.;
let mouse_pos_x = 100.; //Window relative
let mouse_pos_y = 100.; //Window relative
let origin_x = 10.;
let origin_y = 20.;
let cur_size = alacritty_terminal::term::SizeInfo::new(
term_width,
term_height,
cell_width,
line_height,
0.,
0.,
false,
);
let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
let (point, _) =
crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0);
assert_eq!(
point,
alacritty_terminal::index::Point::new(
alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
)
);
}
#[test]
fn test_mouse_to_selection_off_edge() {
let term_width = 100.;
let term_height = 200.;
let cell_width = 10.;
let line_height = 20.;
let mouse_pos_x = 100.; //Window relative
let mouse_pos_y = 100.; //Window relative
let origin_x = 10.;
let origin_y = 20.;
let cur_size = alacritty_terminal::term::SizeInfo::new(
term_width,
term_height,
cell_width,
line_height,
0.,
0.,
false,
);
let mouse_pos = gpui::geometry::vector::vec2f(mouse_pos_x, mouse_pos_y);
let origin = gpui::geometry::vector::vec2f(origin_x, origin_y); //Position of terminal window, 1 'cell' in
let (point, _) =
crate::terminal_element::mouse_to_cell_data(mouse_pos, origin, cur_size, 0);
assert_eq!(
point,
alacritty_terminal::index::Point::new(
alacritty_terminal::index::Line(((mouse_pos_y - origin_y) / line_height) as i32),
alacritty_terminal::index::Column(((mouse_pos_x - origin_x) / cell_width) as usize),
)
);
}
}

View File

@@ -43,7 +43,7 @@ fn test_random_edits(mut rng: StdRng) {
.take(reference_string_len)
.collect::<String>();
let mut buffer = Buffer::new(0, 0, reference_string.clone().into());
LineEnding::strip_carriage_returns(&mut reference_string);
LineEnding::normalize(&mut reference_string);
buffer.history.group_interval = Duration::from_millis(rng.gen_range(0..=200));
let mut buffer_versions = Vec::new();

View File

@@ -555,7 +555,7 @@ pub struct UndoOperation {
impl Buffer {
pub fn new(replica_id: u16, remote_id: u64, mut base_text: String) -> Buffer {
let line_ending = LineEnding::detect(&base_text);
LineEnding::strip_carriage_returns(&mut base_text);
LineEnding::normalize(&mut base_text);
let history = History::new(base_text.into());
let mut fragments = SumTree::new();
@@ -691,7 +691,7 @@ impl Buffer {
let mut fragment_start = old_fragments.start().visible;
for (range, new_text) in edits {
let new_text = LineEnding::strip_carriage_returns_from_arc(new_text.into());
let new_text = LineEnding::normalize_arc(new_text.into());
let fragment_end = old_fragments.end(&None).visible;
// If the current fragment ends before this range, then jump ahead to the first fragment
@@ -2385,13 +2385,13 @@ impl LineEnding {
}
}
pub fn strip_carriage_returns(text: &mut String) {
pub fn normalize(text: &mut String) {
if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(text, "\n") {
*text = replaced;
}
}
fn strip_carriage_returns_from_arc(text: Arc<str>) -> Arc<str> {
fn normalize_arc(text: Arc<str>) -> Arc<str> {
if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(&text, "\n") {
replaced.into()
} else {

View File

@@ -12,8 +12,6 @@ use std::{collections::HashMap, sync::Arc};
pub use theme_registry::*;
pub const DEFAULT_THEME_NAME: &'static str = "cave-dark";
#[derive(Deserialize, Default)]
pub struct Theme {
#[serde(default)]
@@ -42,6 +40,7 @@ pub struct Workspace {
pub titlebar: Titlebar,
pub tab: Tab,
pub active_tab: Tab,
pub pane_button: Interactive<IconButton>,
pub pane_divider: Border,
pub leader_border_opacity: f32,
pub leader_border_width: f32,
@@ -108,6 +107,7 @@ pub struct Toolbar {
pub container: ContainerStyle,
pub height: f32,
pub item_spacing: f32,
pub nav_button: Interactive<IconButton>,
}
#[derive(Clone, Deserialize, Default)]
@@ -242,6 +242,7 @@ pub struct ContextMenu {
#[serde(flatten)]
pub container: ContainerStyle,
pub item: Interactive<ContextMenuItem>,
pub keystroke_margin: f32,
pub separator: ContainerStyle,
}
@@ -509,28 +510,23 @@ pub struct Interactive<T> {
pub default: T,
pub hover: Option<T>,
pub active: Option<T>,
pub active_hover: Option<T>,
pub disabled: Option<T>,
}
impl<T> Interactive<T> {
pub fn style_for(&self, state: MouseState, active: bool) -> &T {
if active {
if state.hovered {
self.active_hover
.as_ref()
.or(self.active.as_ref())
.unwrap_or(&self.default)
} else {
self.active.as_ref().unwrap_or(&self.default)
}
self.active.as_ref().unwrap_or(&self.default)
} else if state.hovered {
self.hover.as_ref().unwrap_or(&self.default)
} else {
if state.hovered {
self.hover.as_ref().unwrap_or(&self.default)
} else {
&self.default
}
&self.default
}
}
pub fn disabled_style(&self) -> &T {
self.disabled.as_ref().unwrap_or(&self.default)
}
}
impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
@@ -544,7 +540,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
default: Value,
hover: Option<Value>,
active: Option<Value>,
active_hover: Option<Value>,
disabled: Option<Value>,
}
let json = Helper::deserialize(deserializer)?;
@@ -570,14 +566,14 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
let hover = deserialize_state(json.hover)?;
let active = deserialize_state(json.active)?;
let active_hover = deserialize_state(json.active_hover)?;
let disabled = deserialize_state(json.disabled)?;
let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
Ok(Interactive {
default,
hover,
active,
active_hover,
disabled,
})
}
}
@@ -637,6 +633,12 @@ pub struct HoverPopover {
#[derive(Clone, Deserialize, Default)]
pub struct TerminalStyle {
pub colors: TerminalColors,
pub modal_container: ContainerStyle,
}
#[derive(Clone, Deserialize, Default)]
pub struct TerminalColors {
pub black: Color,
pub red: Color,
pub green: Color,
@@ -655,6 +657,7 @@ pub struct TerminalStyle {
pub bright_white: Color,
pub foreground: Color,
pub background: Color,
pub modal_background: Color,
pub cursor: Color,
pub dim_black: Color,
pub dim_red: Color,

View File

@@ -24,7 +24,7 @@ pub fn marked_text(marked_text: &str) -> (String, Vec<usize>) {
(unmarked_text, markers.remove(&'|').unwrap_or_default())
}
#[derive(Eq, PartialEq, Hash)]
#[derive(Clone, Eq, PartialEq, Hash)]
pub enum TextRangeMarker {
Empty(char),
Range(char, char),

View File

@@ -194,11 +194,11 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
});
}
// Supports non empty selections so it can be bound and called from visual mode
fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
if let Some(item) = cx.as_mut().read_from_clipboard() {
let mut clipboard_text = Cow::Borrowed(item.text());
if let Some(mut clipboard_selections) =
@@ -244,7 +244,7 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
// If the clipboard text was copied linewise, and the current selection
// is empty, then paste the text after this line and move the selection
// to the start of the pasted text
let range = if selection.is_empty() && linewise {
let insert_at = if linewise {
let (point, _) = display_map
.next_line_boundary(selection.start.to_point(&display_map));
@@ -255,37 +255,26 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
// Drop selection at the start of the next line
let selection_point = Point::new(point.row + 1, 0);
new_selections.push(selection.map(|_| selection_point.clone()));
point..point
point
} else {
let mut selection = selection.clone();
if !selection.reversed {
let mut adjusted = selection.end;
// Head is at the end of the selection. Adjust the end position to
// to include the character under the cursor.
*adjusted.column_mut() = adjusted.column() + 1;
adjusted = display_map.clip_point(adjusted, Bias::Right);
// If the selection is empty, move both the start and end forward one
// character
if selection.is_empty() {
selection.start = adjusted;
selection.end = adjusted;
} else {
selection.end = adjusted;
}
}
let mut point = selection.end;
// Paste the text after the current selection
*point.column_mut() = point.column() + 1;
let point = display_map
.clip_point(point, Bias::Right)
.to_point(&display_map);
let range = selection.map(|p| p.to_point(&display_map)).range();
new_selections.push(selection.map(|_| range.start.clone()));
range
new_selections.push(selection.map(|_| point));
point
};
if linewise && to_insert.ends_with('\n') {
edits.push((
range,
insert_at..insert_at,
&to_insert[0..to_insert.len().saturating_sub(1)],
))
} else {
edits.push((range, to_insert));
edits.push((insert_at..insert_at, to_insert));
}
}
drop(snapshot);
@@ -299,6 +288,7 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
editor.insert(&clipboard_text, cx);
}
}
editor.set_clip_at_line_ends(true, cx);
});
});
});
@@ -1155,10 +1145,13 @@ mod test {
the la|zy dog"});
cx.simulate_keystroke("p");
cx.assert_editor_state(indoc! {"
The quick brown
the lazy dog
|fox jumps over"});
cx.assert_state(
indoc! {"
The quick brown
the lazy dog
|fox jumps over"},
Mode::Normal,
);
cx.set_state(
indoc! {"
@@ -1171,14 +1164,17 @@ mod test {
cx.set_state(
indoc! {"
The quick brown
fox jump|s over
fox jumps ove|r
the lazy dog"},
Mode::Normal,
);
cx.simulate_keystroke("p");
cx.assert_editor_state(indoc! {"
The quick brown
fox jumps|jumps over
the lazy dog"});
cx.assert_state(
indoc! {"
The quick brown
fox jumps over|jumps
the lazy dog"},
Mode::Normal,
);
}
}

View File

@@ -125,6 +125,11 @@ impl<'a> VimTestContext<'a> {
self.cx.set_state(text);
}
pub fn assert_state(&mut self, text: &str, mode: Mode) {
self.assert_editor_state(text);
assert_eq!(self.mode(), mode);
}
pub fn assert_binding<const COUNT: usize>(
&mut self,
keystrokes: [&str; COUNT],
@@ -147,14 +152,6 @@ impl<'a> VimTestContext<'a> {
let mode = self.mode();
VimBindingTestContext::new(keystrokes, mode, mode, self)
}
pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
self.cx.update(|cx| {
let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
let expected_content = expected_content.map(|content| content.to_owned());
assert_eq!(actual_content, expected_content);
})
}
}
impl<'a> Deref for VimTestContext<'a> {

View File

@@ -1,17 +1,20 @@
use std::borrow::Cow;
use collections::HashMap;
use editor::{display_map::ToDisplayPoint, Autoscroll, Bias};
use editor::{display_map::ToDisplayPoint, Autoscroll, Bias, ClipboardSelection};
use gpui::{actions, MutableAppContext, ViewContext};
use language::SelectionGoal;
use workspace::Workspace;
use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim};
actions!(vim, [VisualDelete, VisualChange, VisualYank]);
actions!(vim, [VisualDelete, VisualChange, VisualYank, VisualPaste]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(change);
cx.add_action(delete);
cx.add_action(yank);
cx.add_action(paste);
}
pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
@@ -136,7 +139,7 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>)
vim.update_active_editor(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let line_mode = editor.selections.line_mode;
if !editor.selections.line_mode {
if !line_mode {
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
if !selection.reversed {
@@ -159,6 +162,114 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>)
});
}
pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
if let Some(item) = cx.as_mut().read_from_clipboard() {
copy_selections_content(editor, editor.selections.line_mode, cx);
let mut clipboard_text = Cow::Borrowed(item.text());
if let Some(mut clipboard_selections) =
item.metadata::<Vec<ClipboardSelection>>()
{
let (display_map, selections) = editor.selections.all_adjusted_display(cx);
let all_selections_were_entire_line =
clipboard_selections.iter().all(|s| s.is_entire_line);
if clipboard_selections.len() != selections.len() {
let mut newline_separated_text = String::new();
let mut clipboard_selections =
clipboard_selections.drain(..).peekable();
let mut ix = 0;
while let Some(clipboard_selection) = clipboard_selections.next() {
newline_separated_text
.push_str(&clipboard_text[ix..ix + clipboard_selection.len]);
ix += clipboard_selection.len;
if clipboard_selections.peek().is_some() {
newline_separated_text.push('\n');
}
}
clipboard_text = Cow::Owned(newline_separated_text);
}
let mut new_selections = Vec::new();
editor.buffer().update(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
let mut start_offset = 0;
let mut edits = Vec::new();
for (ix, selection) in selections.iter().enumerate() {
let to_insert;
let linewise;
if let Some(clipboard_selection) = clipboard_selections.get(ix) {
let end_offset = start_offset + clipboard_selection.len;
to_insert = &clipboard_text[start_offset..end_offset];
linewise = clipboard_selection.is_entire_line;
start_offset = end_offset;
} else {
to_insert = clipboard_text.as_str();
linewise = all_selections_were_entire_line;
}
let mut selection = selection.clone();
if !selection.reversed {
let mut adjusted = selection.end;
// Head is at the end of the selection. Adjust the end position to
// to include the character under the cursor.
*adjusted.column_mut() = adjusted.column() + 1;
adjusted = display_map.clip_point(adjusted, Bias::Right);
// If the selection is empty, move both the start and end forward one
// character
if selection.is_empty() {
selection.start = adjusted;
selection.end = adjusted;
} else {
selection.end = adjusted;
}
}
let range = selection.map(|p| p.to_point(&display_map)).range();
let new_position = if linewise {
edits.push((range.start..range.start, "\n"));
let mut new_position = range.start.clone();
new_position.column = 0;
new_position.row += 1;
new_position
} else {
range.start.clone()
};
new_selections.push(selection.map(|_| new_position.clone()));
if linewise && to_insert.ends_with('\n') {
edits.push((
range.clone(),
&to_insert[0..to_insert.len().saturating_sub(1)],
))
} else {
edits.push((range.clone(), to_insert));
}
if linewise {
edits.push((range.end..range.end, "\n"));
}
}
drop(snapshot);
buffer.edit_with_autoindent(edits, cx);
});
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.select(new_selections)
});
} else {
editor.insert(&clipboard_text, cx);
}
}
});
});
vim.switch_mode(Mode::Normal, cx);
});
}
#[cfg(test)]
mod test {
use indoc::indoc;
@@ -607,4 +718,62 @@ mod test {
quick brown
fox jumps o"}));
}
#[gpui::test]
async fn test_visual_paste(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.set_state(
indoc! {"
The quick brown
fox [jump}s over
the lazy dog"},
Mode::Visual { line: false },
);
cx.simulate_keystroke("y");
cx.set_state(
indoc! {"
The quick brown
fox jump|s over
the lazy dog"},
Mode::Normal,
);
cx.simulate_keystroke("p");
cx.assert_state(
indoc! {"
The quick brown
fox jumps|jumps over
the lazy dog"},
Mode::Normal,
);
cx.set_state(
indoc! {"
The quick brown
fox ju|mps over
the lazy dog"},
Mode::Visual { line: true },
);
cx.simulate_keystroke("d");
cx.assert_state(
indoc! {"
The quick brown
the la|zy dog"},
Mode::Normal,
);
cx.set_state(
indoc! {"
The quick brown
the [laz}y dog"},
Mode::Visual { line: false },
);
cx.simulate_keystroke("p");
cx.assert_state(
indoc! {"
The quick brown
the
|fox jumps over
dog"},
Mode::Normal,
);
}
}

View File

@@ -14,6 +14,7 @@ test-support = ["client/test-support", "project/test-support", "settings/test-su
client = { path = "../client" }
clock = { path = "../clock" }
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
gpui = { path = "../gpui" }
language = { path = "../language" }
menu = { path = "../menu" }

View File

@@ -2,11 +2,15 @@ use super::{ItemHandle, SplitDirection};
use crate::{toolbar::Toolbar, Item, WeakItemHandle, Workspace};
use anyhow::Result;
use collections::{HashMap, HashSet, VecDeque};
use context_menu::{ContextMenu, ContextMenuItem};
use futures::StreamExt;
use gpui::{
actions,
elements::*,
geometry::{rect::RectF, vector::vec2f},
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
},
impl_actions, impl_internal_actions,
platform::{CursorStyle, NavigationDirection},
AppContext, AsyncAppContext, Entity, ModelHandle, MutableAppContext, PromptLevel, Quad,
@@ -14,7 +18,7 @@ use gpui::{
};
use project::{Project, ProjectEntryId, ProjectPath};
use serde::Deserialize;
use settings::Settings;
use settings::{Autosave, Settings};
use std::{any::Any, cell::RefCell, mem, path::Path, rc::Rc};
use util::ResultExt;
@@ -55,8 +59,13 @@ pub struct GoForward {
pub pane: Option<WeakViewHandle<Pane>>,
}
#[derive(Clone, PartialEq)]
pub struct DeploySplitMenu {
position: Vector2F,
}
impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
impl_internal_actions!(pane, [CloseItem]);
impl_internal_actions!(pane, [CloseItem, DeploySplitMenu]);
const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
@@ -87,6 +96,7 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx));
cx.add_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx));
cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
cx.add_action(Pane::deploy_split_menu);
cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
Pane::reopen_closed_item(workspace, cx).detach();
});
@@ -129,6 +139,7 @@ pub struct Pane {
autoscroll: bool,
nav_history: Rc<RefCell<NavHistory>>,
toolbar: ViewHandle<Toolbar>,
split_menu: ViewHandle<ContextMenu>,
}
pub struct ItemNavHistory {
@@ -136,13 +147,13 @@ pub struct ItemNavHistory {
item: Rc<dyn WeakItemHandle>,
}
#[derive(Default)]
pub struct NavHistory {
struct NavHistory {
mode: NavigationMode,
backward_stack: VecDeque<NavigationEntry>,
forward_stack: VecDeque<NavigationEntry>,
closed_stack: VecDeque<NavigationEntry>,
paths_by_item: HashMap<usize, ProjectPath>,
pane: WeakViewHandle<Pane>,
}
#[derive(Copy, Clone)]
@@ -168,17 +179,30 @@ pub struct NavigationEntry {
impl Pane {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
let handle = cx.weak_handle();
let split_menu = cx.add_view(|cx| ContextMenu::new(cx));
Self {
items: Vec::new(),
active_item_index: 0,
autoscroll: false,
nav_history: Default::default(),
toolbar: cx.add_view(|_| Toolbar::new()),
nav_history: Rc::new(RefCell::new(NavHistory {
mode: NavigationMode::Normal,
backward_stack: Default::default(),
forward_stack: Default::default(),
closed_stack: Default::default(),
paths_by_item: Default::default(),
pane: handle.clone(),
})),
toolbar: cx.add_view(|_| Toolbar::new(handle)),
split_menu,
}
}
pub fn nav_history(&self) -> &Rc<RefCell<NavHistory>> {
&self.nav_history
pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> ItemNavHistory {
ItemNavHistory {
history: self.nav_history.clone(),
item: Rc::new(item.downgrade()),
}
}
pub fn activate(&self, cx: &mut ViewContext<Self>) {
@@ -223,6 +247,26 @@ impl Pane {
)
}
pub fn disable_history(&mut self) {
self.nav_history.borrow_mut().disable();
}
pub fn enable_history(&mut self) {
self.nav_history.borrow_mut().enable();
}
pub fn can_navigate_backward(&self) -> bool {
!self.nav_history.borrow().backward_stack.is_empty()
}
pub fn can_navigate_forward(&self) -> bool {
!self.nav_history.borrow().forward_stack.is_empty()
}
fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
self.toolbar.update(cx, |_, cx| cx.notify());
}
fn navigate_history(
workspace: &mut Workspace,
pane: ViewHandle<Pane>,
@@ -234,7 +278,7 @@ impl Pane {
let to_load = pane.update(cx, |pane, cx| {
loop {
// Retrieve the weak item handle from the history.
let entry = pane.nav_history.borrow_mut().pop(mode)?;
let entry = pane.nav_history.borrow_mut().pop(mode, cx)?;
// If the item is still present in this pane, then activate it.
if let Some(index) = entry
@@ -367,7 +411,6 @@ impl Pane {
return;
}
item.set_nav_history(pane.read(cx).nav_history.clone(), cx);
item.added_to_pane(workspace, pane.clone(), cx);
pane.update(cx, |pane, cx| {
// If there is already an active item, then insert the new item
@@ -625,11 +668,16 @@ impl Pane {
.borrow_mut()
.set_mode(NavigationMode::Normal);
let mut nav_history = pane.nav_history().borrow_mut();
if let Some(path) = item.project_path(cx) {
nav_history.paths_by_item.insert(item.id(), path);
pane.nav_history
.borrow_mut()
.paths_by_item
.insert(item.id(), path);
} else {
nav_history.paths_by_item.remove(&item.id());
pane.nav_history
.borrow_mut()
.paths_by_item
.remove(&item.id());
}
}
});
@@ -677,7 +725,13 @@ impl Pane {
_ => return Ok(false),
}
} else if is_dirty && (can_save || is_singleton) {
let should_save = if should_prompt_for_save {
let will_autosave = cx.read(|cx| {
matches!(
cx.global::<Settings>().autosave,
Autosave::OnFocusChange | Autosave::OnWindowChange
) && Self::can_autosave_item(item.as_ref(), cx)
});
let should_save = if should_prompt_for_save && !will_autosave {
let mut answer = pane.update(cx, |pane, cx| {
pane.activate_item(item_ix, true, true, cx);
cx.prompt(
@@ -718,6 +772,23 @@ impl Pane {
Ok(true)
}
fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
let is_deleted = item.project_entry_ids(cx).is_empty();
item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
}
pub fn autosave_item(
item: &dyn ItemHandle,
project: ModelHandle<Project>,
cx: &mut MutableAppContext,
) -> Task<Result<()>> {
if Self::can_autosave_item(item, cx) {
item.save(project, cx)
} else {
Task::ready(Ok(()))
}
}
pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
if let Some(active_item) = self.active_item() {
cx.focus(active_item);
@@ -728,6 +799,21 @@ impl Pane {
cx.emit(Event::Split(direction));
}
fn deploy_split_menu(&mut self, action: &DeploySplitMenu, cx: &mut ViewContext<Self>) {
self.split_menu.update(cx, |menu, cx| {
menu.show(
action.position,
vec![
ContextMenuItem::item("Split Right", SplitRight),
ContextMenuItem::item("Split Left", SplitLeft),
ContextMenuItem::item("Split Up", SplitUp),
ContextMenuItem::item("Split Down", SplitDown),
],
cx,
);
});
}
pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
&self.toolbar
}
@@ -742,13 +828,13 @@ impl Pane {
});
}
fn render_tabs(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
fn render_tabs(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
let theme = cx.global::<Settings>().theme.clone();
enum Tabs {}
enum Tab {}
let pane = cx.handle();
let tabs = MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
let autoscroll = if mem::take(&mut self.autoscroll) {
Some(self.active_item_index)
} else {
@@ -883,11 +969,7 @@ impl Pane {
);
row.boxed()
});
ConstrainedBox::new(tabs.boxed())
.with_height(theme.workspace.tab.height)
.named("tabs")
})
}
}
@@ -901,27 +983,72 @@ impl View for Pane {
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
enum SplitIcon {}
let this = cx.handle();
EventHandler::new(if let Some(active_item) = self.active_item() {
Flex::column()
.with_child(self.render_tabs(cx))
.with_child(ChildView::new(&self.toolbar).boxed())
.with_child(ChildView::new(active_item).flex(1., true).boxed())
.boxed()
} else {
Empty::new().boxed()
})
.on_navigate_mouse_down(move |direction, cx| {
let this = this.clone();
match direction {
NavigationDirection::Back => cx.dispatch_action(GoBack { pane: Some(this) }),
NavigationDirection::Forward => cx.dispatch_action(GoForward { pane: Some(this) }),
}
Stack::new()
.with_child(
EventHandler::new(if let Some(active_item) = self.active_item() {
Flex::column()
.with_child(
Flex::row()
.with_child(self.render_tabs(cx).flex(1., true).named("tabs"))
.with_child(
MouseEventHandler::new::<SplitIcon, _, _>(
0,
cx,
|mouse_state, cx| {
let theme = &cx.global::<Settings>().theme.workspace;
let style =
theme.pane_button.style_for(mouse_state, false);
Svg::new("icons/split.svg")
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.contained()
.with_style(style.container)
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.aligned()
.boxed()
},
)
.with_cursor_style(CursorStyle::PointingHand)
.on_mouse_down(|position, cx| {
cx.dispatch_action(DeploySplitMenu { position });
})
.boxed(),
)
.constrained()
.with_height(cx.global::<Settings>().theme.workspace.tab.height)
.boxed(),
)
.with_child(ChildView::new(&self.toolbar).boxed())
.with_child(ChildView::new(active_item).flex(1., true).boxed())
.boxed()
} else {
Empty::new().boxed()
})
.on_navigate_mouse_down(move |direction, cx| {
let this = this.clone();
match direction {
NavigationDirection::Back => {
cx.dispatch_action(GoBack { pane: Some(this) })
}
NavigationDirection::Forward => {
cx.dispatch_action(GoForward { pane: Some(this) })
}
}
true
})
.named("pane")
true
})
.boxed(),
)
.with_child(ChildView::new(&self.split_menu).boxed())
.named("pane")
}
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
@@ -930,57 +1057,56 @@ impl View for Pane {
}
impl ItemNavHistory {
pub fn new<T: Item>(history: Rc<RefCell<NavHistory>>, item: &ViewHandle<T>) -> Self {
Self {
history,
item: Rc::new(item.downgrade()),
}
pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut MutableAppContext) {
self.history.borrow_mut().push(data, self.item.clone(), cx);
}
pub fn history(&self) -> Rc<RefCell<NavHistory>> {
self.history.clone()
pub fn pop_backward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
self.history.borrow_mut().pop(NavigationMode::GoingBack, cx)
}
pub fn push<D: 'static + Any>(&self, data: Option<D>) {
self.history.borrow_mut().push(data, self.item.clone());
pub fn pop_forward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
self.history
.borrow_mut()
.pop(NavigationMode::GoingForward, cx)
}
}
impl NavHistory {
pub fn disable(&mut self) {
self.mode = NavigationMode::Disabled;
}
pub fn enable(&mut self) {
self.mode = NavigationMode::Normal;
}
pub fn pop_backward(&mut self) -> Option<NavigationEntry> {
self.backward_stack.pop_back()
}
pub fn pop_forward(&mut self) -> Option<NavigationEntry> {
self.forward_stack.pop_back()
}
pub fn pop_closed(&mut self) -> Option<NavigationEntry> {
self.closed_stack.pop_back()
}
fn pop(&mut self, mode: NavigationMode) -> Option<NavigationEntry> {
match mode {
NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => None,
NavigationMode::GoingBack => self.pop_backward(),
NavigationMode::GoingForward => self.pop_forward(),
NavigationMode::ReopeningClosedItem => self.pop_closed(),
}
}
fn set_mode(&mut self, mode: NavigationMode) {
self.mode = mode;
}
pub fn push<D: 'static + Any>(&mut self, data: Option<D>, item: Rc<dyn WeakItemHandle>) {
fn disable(&mut self) {
self.mode = NavigationMode::Disabled;
}
fn enable(&mut self) {
self.mode = NavigationMode::Normal;
}
fn pop(&mut self, mode: NavigationMode, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
let entry = match mode {
NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
return None
}
NavigationMode::GoingBack => &mut self.backward_stack,
NavigationMode::GoingForward => &mut self.forward_stack,
NavigationMode::ReopeningClosedItem => &mut self.closed_stack,
}
.pop_back();
if entry.is_some() {
self.did_update(cx);
}
entry
}
fn push<D: 'static + Any>(
&mut self,
data: Option<D>,
item: Rc<dyn WeakItemHandle>,
cx: &mut MutableAppContext,
) {
match self.mode {
NavigationMode::Disabled => {}
NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
@@ -1021,5 +1147,12 @@ impl NavHistory {
});
}
}
self.did_update(cx);
}
fn did_update(&self, cx: &mut MutableAppContext) {
if let Some(pane) = self.pane.upgrade(cx) {
cx.defer(move |cx| pane.update(cx, |pane, cx| pane.history_updated(cx)));
}
}
}

View File

@@ -188,12 +188,13 @@ impl Sidebar {
})
.with_cursor_style(CursorStyle::ResizeLeftRight)
.on_mouse_down(|_, _| {}) // This prevents the mouse down event from being propagated elsewhere
.on_drag(move |delta, cx| {
.on_drag(move |old_position, new_position, cx| {
let delta = new_position.x() - old_position.x();
let prev_width = *actual_width.borrow();
*custom_width.borrow_mut() = 0f32
.max(match side {
Side::Left => prev_width + delta.x(),
Side::Right => prev_width - delta.x(),
Side::Left => prev_width + delta,
Side::Right => prev_width - delta,
})
.round();

View File

@@ -1,7 +1,7 @@
use crate::ItemHandle;
use crate::{ItemHandle, Pane};
use gpui::{
elements::*, AnyViewHandle, AppContext, ElementBox, Entity, MutableAppContext, RenderContext,
View, ViewContext, ViewHandle,
elements::*, platform::CursorStyle, Action, AnyViewHandle, AppContext, ElementBox, Entity,
MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle,
};
use settings::Settings;
@@ -42,6 +42,7 @@ pub enum ToolbarItemLocation {
pub struct Toolbar {
active_pane_item: Option<Box<dyn ItemHandle>>,
pane: WeakViewHandle<Pane>,
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
}
@@ -60,6 +61,7 @@ impl View for Toolbar {
let mut primary_left_items = Vec::new();
let mut primary_right_items = Vec::new();
let mut secondary_item = None;
let spacing = theme.item_spacing;
for (item, position) in &self.items {
match *position {
@@ -68,7 +70,7 @@ impl View for Toolbar {
let left_item = ChildView::new(item.as_ref())
.aligned()
.contained()
.with_margin_right(theme.item_spacing);
.with_margin_right(spacing);
if let Some((flex, expanded)) = flex {
primary_left_items.push(left_item.flex(flex, expanded).boxed());
} else {
@@ -79,7 +81,7 @@ impl View for Toolbar {
let right_item = ChildView::new(item.as_ref())
.aligned()
.contained()
.with_margin_left(theme.item_spacing)
.with_margin_left(spacing)
.flex_float();
if let Some((flex, expanded)) = flex {
primary_right_items.push(right_item.flex(flex, expanded).boxed());
@@ -98,26 +100,115 @@ impl View for Toolbar {
}
}
let pane = self.pane.clone();
let mut enable_go_backward = false;
let mut enable_go_forward = false;
if let Some(pane) = pane.upgrade(cx) {
let pane = pane.read(cx);
enable_go_backward = pane.can_navigate_backward();
enable_go_forward = pane.can_navigate_forward();
}
let container_style = theme.container;
let height = theme.height;
let button_style = theme.nav_button;
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
Flex::column()
.with_child(
Flex::row()
.with_child(nav_button(
"icons/arrow-left.svg",
button_style,
tooltip_style.clone(),
enable_go_backward,
spacing,
super::GoBack {
pane: Some(pane.clone()),
},
super::GoBack { pane: None },
"Go Back",
cx,
))
.with_child(nav_button(
"icons/arrow-right.svg",
button_style,
tooltip_style.clone(),
enable_go_forward,
spacing,
super::GoForward {
pane: Some(pane.clone()),
},
super::GoForward { pane: None },
"Go Forward",
cx,
))
.with_children(primary_left_items)
.with_children(primary_right_items)
.constrained()
.with_height(theme.height)
.with_height(height)
.boxed(),
)
.with_children(secondary_item)
.contained()
.with_style(theme.container)
.with_style(container_style)
.boxed()
}
}
fn nav_button<A: Action + Clone>(
svg_path: &'static str,
style: theme::Interactive<theme::IconButton>,
tooltip_style: TooltipStyle,
enabled: bool,
spacing: f32,
action: A,
tooltip_action: A,
action_name: &str,
cx: &mut RenderContext<Toolbar>,
) -> ElementBox {
MouseEventHandler::new::<A, _, _>(0, cx, |state, _| {
let style = if enabled {
style.style_for(state, false)
} else {
style.disabled_style()
};
Svg::new(svg_path)
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.contained()
.with_style(style.container)
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
.aligned()
.boxed()
})
.with_cursor_style(if enabled {
CursorStyle::PointingHand
} else {
CursorStyle::default()
})
.on_click(move |_, _, cx| cx.dispatch_action(action.clone()))
.with_tooltip::<A, _>(
0,
action_name.to_string(),
Some(Box::new(tooltip_action)),
tooltip_style,
cx,
)
.contained()
.with_margin_right(spacing)
.boxed()
}
impl Toolbar {
pub fn new() -> Self {
pub fn new(pane: WeakViewHandle<Pane>) -> Self {
Self {
active_pane_item: None,
pane,
items: Default::default(),
}
}

View File

@@ -11,6 +11,7 @@ use client::{
};
use clock::ReplicaId;
use collections::{hash_map, HashMap, HashSet};
use futures::{channel::oneshot, FutureExt};
use gpui::{
actions,
color::Color,
@@ -30,7 +31,7 @@ pub use pane_group::*;
use postage::prelude::Stream;
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
use serde::Deserialize;
use settings::Settings;
use settings::{Autosave, Settings};
use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem};
use smallvec::SmallVec;
use status_bar::StatusBar;
@@ -41,12 +42,14 @@ use std::{
cell::RefCell,
fmt,
future::Future,
mem,
path::{Path, PathBuf},
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
time::Duration,
};
use theme::{Theme, ThemeRegistry};
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
@@ -296,6 +299,9 @@ pub trait Item: View {
fn should_update_tab_on_event(_: &Self::Event) -> bool {
false
}
fn is_edit_event(_: &Self::Event) -> bool {
false
}
fn act_as_type(
&self,
type_id: TypeId,
@@ -408,7 +414,6 @@ pub trait ItemHandle: 'static + fmt::Debug {
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
fn is_singleton(&self, cx: &AppContext) -> bool;
fn boxed_clone(&self) -> Box<dyn ItemHandle>;
fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext);
fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>>;
fn added_to_pane(
&self,
@@ -478,12 +483,6 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
Box::new(self.clone())
}
fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext) {
self.update(cx, |item, cx| {
item.set_nav_history(ItemNavHistory::new(nav_history, &cx.handle()), cx);
})
}
fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
self.update(cx, |item, cx| {
cx.add_option_view(|cx| item.clone_on_split(cx))
@@ -497,6 +496,9 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
pane: ViewHandle<Pane>,
cx: &mut ViewContext<Workspace>,
) {
let history = pane.read(cx).nav_history_for_item(self);
self.update(cx, |this, cx| this.set_nav_history(history, cx));
if let Some(followed_item) = self.to_followable_item_handle(cx) {
if let Some(message) = followed_item.to_state_proto(cx) {
workspace.update_followers(
@@ -510,6 +512,8 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
}
}
let mut pending_autosave = None;
let mut cancel_pending_autosave = oneshot::channel::<()>().0;
let pending_update = Rc::new(RefCell::new(None));
let pending_update_scheduled = Rc::new(AtomicBool::new(false));
let pane = pane.downgrade();
@@ -570,6 +574,40 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
cx.notify();
});
}
if T::is_edit_event(event) {
if let Autosave::AfterDelay { milliseconds } = cx.global::<Settings>().autosave {
let prev_autosave = pending_autosave.take().unwrap_or(Task::ready(Some(())));
let (cancel_tx, mut cancel_rx) = oneshot::channel::<()>();
let prev_cancel_tx = mem::replace(&mut cancel_pending_autosave, cancel_tx);
let project = workspace.project.downgrade();
let _ = prev_cancel_tx.send(());
pending_autosave = Some(cx.spawn_weak(|_, mut cx| async move {
let mut timer = cx
.background()
.timer(Duration::from_millis(milliseconds))
.fuse();
prev_autosave.await;
futures::select_biased! {
_ = cancel_rx => return None,
_ = timer => {}
}
let project = project.upgrade(&cx)?;
cx.update(|cx| Pane::autosave_item(&item, project, cx))
.await
.log_err();
None
}));
}
}
})
.detach();
cx.observe_focus(self, move |workspace, item, focused, cx| {
if !focused && cx.global::<Settings>().autosave == Autosave::OnFocusChange {
Pane::autosave_item(&item, workspace.project.clone(), cx).detach_and_log_err(cx);
}
})
.detach();
}
@@ -767,15 +805,10 @@ enum FollowerItem {
impl Workspace {
pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
cx.observe(&project, |_, project, cx| {
if project.read(cx).is_read_only() {
cx.blur();
}
cx.notify()
})
.detach();
cx.subscribe(&project, move |this, project, event, cx| {
cx.observe_window_activation(Self::on_window_activation_changed)
.detach();
cx.observe(&project, |_, _, cx| cx.notify()).detach();
cx.subscribe(&project, move |this, _, event, cx| {
match event {
project::Event::RemoteIdChanged(remote_id) => {
this.project_remote_id_changed(*remote_id, cx);
@@ -786,11 +819,12 @@ impl Workspace {
project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
this.update_window_title(cx);
}
project::Event::DisconnectedFromHost => {
this.update_window_edited(cx);
cx.blur();
}
_ => {}
}
if project.read(cx).is_read_only() {
cx.blur();
}
cx.notify()
})
.detach();
@@ -989,6 +1023,10 @@ impl Workspace {
should_prompt_to_save: bool,
cx: &mut ViewContext<Self>,
) -> Task<Result<bool>> {
if self.project.read(cx).is_read_only() {
return Task::ready(Ok(true));
}
let dirty_items = self
.panes
.iter()
@@ -1005,11 +1043,10 @@ impl Workspace {
let project = self.project.clone();
cx.spawn_weak(|_, mut cx| async move {
// let mut saved_project_entry_ids = HashSet::default();
for (pane, item) in dirty_items {
let (is_singl, project_entry_ids) =
let (singleton, project_entry_ids) =
cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
if is_singl || !project_entry_ids.is_empty() {
if singleton || !project_entry_ids.is_empty() {
if let Some(ix) =
pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))
{
@@ -1870,9 +1907,10 @@ impl Workspace {
}
fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
let is_edited = self
.items(cx)
.any(|item| item.has_conflict(cx) || item.is_dirty(cx));
let is_edited = !self.project.read(cx).is_read_only()
&& self
.items(cx)
.any(|item| item.has_conflict(cx) || item.is_dirty(cx));
if is_edited != self.window_edited {
self.window_edited = is_edited;
cx.set_window_edited(self.window_edited)
@@ -2314,6 +2352,24 @@ impl Workspace {
}
None
}
fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
if !active
&& matches!(
cx.global::<Settings>().autosave,
Autosave::OnWindowChange | Autosave::OnFocusChange
)
{
for pane in &self.panes {
pane.update(cx, |pane, cx| {
for item in pane.items() {
Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
.detach_and_log_err(cx);
}
});
}
}
}
}
impl Entity for Workspace {
@@ -2631,7 +2687,7 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
#[cfg(test)]
mod tests {
use super::*;
use gpui::{ModelHandle, TestAppContext, ViewContext};
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
use project::{FakeFs, Project, ProjectEntryId};
use serde_json::json;
@@ -2969,21 +3025,219 @@ mod tests {
});
}
#[derive(Clone)]
#[gpui::test]
async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
deterministic.forbid_parking();
Settings::test_async(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
let item = cx.add_view(window_id, |_| {
let mut item = TestItem::new();
item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
item
});
let item_id = item.id();
workspace.update(cx, |workspace, cx| {
workspace.add_item(Box::new(item.clone()), cx);
});
// Autosave on window change.
item.update(cx, |item, cx| {
cx.update_global(|settings: &mut Settings, _| {
settings.autosave = Autosave::OnWindowChange;
});
item.is_dirty = true;
});
// Deactivating the window saves the file.
cx.simulate_window_activation(None);
deterministic.run_until_parked();
item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
// Autosave on focus change.
item.update(cx, |item, cx| {
cx.focus_self();
cx.update_global(|settings: &mut Settings, _| {
settings.autosave = Autosave::OnFocusChange;
});
item.is_dirty = true;
});
// Blurring the item saves the file.
item.update(cx, |_, cx| cx.blur());
deterministic.run_until_parked();
item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
// Deactivating the window still saves the file.
cx.simulate_window_activation(Some(window_id));
item.update(cx, |item, cx| {
cx.focus_self();
item.is_dirty = true;
});
cx.simulate_window_activation(None);
deterministic.run_until_parked();
item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
// Autosave after delay.
item.update(cx, |item, cx| {
cx.update_global(|settings: &mut Settings, _| {
settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
});
item.is_dirty = true;
cx.emit(TestItemEvent::Edit);
});
// Delay hasn't fully expired, so the file is still dirty and unsaved.
deterministic.advance_clock(Duration::from_millis(250));
item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
// After delay expires, the file is saved.
deterministic.advance_clock(Duration::from_millis(250));
item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
// Autosave on focus change, ensuring closing the tab counts as such.
item.update(cx, |item, cx| {
cx.update_global(|settings: &mut Settings, _| {
settings.autosave = Autosave::OnFocusChange;
});
item.is_dirty = true;
});
workspace
.update(cx, |workspace, cx| {
let pane = workspace.active_pane().clone();
Pane::close_items(workspace, pane, cx, move |id| id == item_id)
})
.await
.unwrap();
assert!(!cx.has_pending_prompt(window_id));
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
// Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
workspace.update(cx, |workspace, cx| {
workspace.add_item(Box::new(item.clone()), cx);
});
item.update(cx, |item, cx| {
item.project_entry_ids = Default::default();
item.is_dirty = true;
cx.blur();
});
deterministic.run_until_parked();
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
// Ensure autosave is prevented for deleted files also when closing the buffer.
let _close_items = workspace.update(cx, |workspace, cx| {
let pane = workspace.active_pane().clone();
Pane::close_items(workspace, pane, cx, move |id| id == item_id)
});
deterministic.run_until_parked();
assert!(cx.has_pending_prompt(window_id));
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
}
#[gpui::test]
async fn test_pane_navigation(
deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext,
) {
deterministic.forbid_parking();
Settings::test_async(cx);
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
let item = cx.add_view(window_id, |_| {
let mut item = TestItem::new();
item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
item
});
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
let toolbar_notify_count = Rc::new(RefCell::new(0));
workspace.update(cx, |workspace, cx| {
workspace.add_item(Box::new(item.clone()), cx);
let toolbar_notification_count = toolbar_notify_count.clone();
cx.observe(&toolbar, move |_, _, _| {
*toolbar_notification_count.borrow_mut() += 1
})
.detach();
});
pane.read_with(cx, |pane, _| {
assert!(!pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
});
item.update(cx, |item, cx| {
item.set_state("one".to_string(), cx);
});
// Toolbar must be notified to re-render the navigation buttons
assert_eq!(*toolbar_notify_count.borrow(), 1);
pane.read_with(cx, |pane, _| {
assert!(pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
});
workspace
.update(cx, |workspace, cx| {
Pane::go_back(workspace, Some(pane.clone()), cx)
})
.await;
assert_eq!(*toolbar_notify_count.borrow(), 3);
pane.read_with(cx, |pane, _| {
assert!(!pane.can_navigate_backward());
assert!(pane.can_navigate_forward());
});
}
struct TestItem {
state: String,
save_count: usize,
save_as_count: usize,
reload_count: usize,
is_dirty: bool,
is_singleton: bool,
has_conflict: bool,
project_entry_ids: Vec<ProjectEntryId>,
project_path: Option<ProjectPath>,
is_singleton: bool,
nav_history: Option<ItemNavHistory>,
}
enum TestItemEvent {
Edit,
}
impl Clone for TestItem {
fn clone(&self) -> Self {
Self {
state: self.state.clone(),
save_count: self.save_count,
save_as_count: self.save_as_count,
reload_count: self.reload_count,
is_dirty: self.is_dirty,
is_singleton: self.is_singleton,
has_conflict: self.has_conflict,
project_entry_ids: self.project_entry_ids.clone(),
project_path: self.project_path.clone(),
nav_history: None,
}
}
}
impl TestItem {
fn new() -> Self {
Self {
state: String::new(),
save_count: 0,
save_as_count: 0,
reload_count: 0,
@@ -2992,12 +3246,24 @@ mod tests {
project_entry_ids: Vec::new(),
project_path: None,
is_singleton: true,
nav_history: None,
}
}
fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
self.push_to_nav_history(cx);
self.state = state;
}
fn push_to_nav_history(&mut self, cx: &mut ViewContext<Self>) {
if let Some(history) = &mut self.nav_history {
history.push(Some(Box::new(self.state.clone())), cx);
}
}
}
impl Entity for TestItem {
type Event = ();
type Event = TestItemEvent;
}
impl View for TestItem {
@@ -3027,7 +3293,23 @@ mod tests {
self.is_singleton
}
fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
self.nav_history = Some(history);
}
fn navigate(&mut self, state: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
let state = *state.downcast::<String>().unwrap_or_default();
if state != self.state {
self.state = state;
true
} else {
false
}
}
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
self.push_to_nav_history(cx);
}
fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
where
@@ -3054,6 +3336,7 @@ mod tests {
_: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
self.save_count += 1;
self.is_dirty = false;
Task::ready(Ok(()))
}
@@ -3064,6 +3347,7 @@ mod tests {
_: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
self.save_as_count += 1;
self.is_dirty = false;
Task::ready(Ok(()))
}
@@ -3073,11 +3357,16 @@ mod tests {
_: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
self.reload_count += 1;
self.is_dirty = false;
Task::ready(Ok(()))
}
fn should_update_tab_on_event(_: &Self::Event) -> bool {
true
}
fn is_edit_event(event: &Self::Event) -> bool {
matches!(event, TestItemEvent::Edit)
}
}
}

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.44.1"
version = "0.47.0"
[lib]
name = "zed"
@@ -39,6 +39,7 @@ journal = { path = "../journal" }
language = { path = "../language" }
lsp = { path = "../lsp" }
outline = { path = "../outline" }
plugin_runtime = { path = "../plugin_runtime" }
project = { path = "../project" }
project_panel = { path = "../project_panel" }
project_symbols = { path = "../project_symbols" }
@@ -88,7 +89,7 @@ tempdir = { version = "0.3.7" }
thiserror = "1.0.29"
tiny_http = "0.8"
toml = "0.5"
tree-sitter = "0.20.8"
tree-sitter = "0.20"
tree-sitter-c = "0.20.1"
tree-sitter-cpp = "0.20.0"
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }

View File

@@ -1,5 +1,6 @@
use gpui::Task;
use gpui::executor::Background;
pub use language::*;
use lazy_static::lazy_static;
use rust_embed::RustEmbed;
use std::{borrow::Cow, str, sync::Arc};
@@ -7,6 +8,7 @@ mod c;
mod go;
mod installation;
mod json;
mod language_plugin;
mod python;
mod rust;
mod typescript;
@@ -16,28 +18,42 @@ mod typescript;
#[exclude = "*.rs"]
struct LanguageDir;
pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegistry {
let languages = LanguageRegistry::new(login_shell_env_loaded);
// TODO - Remove this once the `init` function is synchronous again.
lazy_static! {
pub static ref LANGUAGE_NAMES: Vec<String> = LanguageDir::iter()
.filter_map(|path| {
if path.ends_with("config.toml") {
let config = LanguageDir::get(&path)?;
let config = toml::from_slice::<LanguageConfig>(&config.data).ok()?;
Some(config.name.to_string())
} else {
None
}
})
.collect();
}
pub async fn init(languages: Arc<LanguageRegistry>, _executor: Arc<Background>) {
for (name, grammar, lsp_adapter) in [
(
"c",
tree_sitter_c::language(),
Some(Arc::new(c::CLspAdapter) as Arc<dyn LspAdapter>),
Some(CachedLspAdapter::new(c::CLspAdapter).await),
),
(
"cpp",
tree_sitter_cpp::language(),
Some(Arc::new(c::CLspAdapter) as Arc<dyn LspAdapter>),
Some(CachedLspAdapter::new(c::CLspAdapter).await),
),
(
"go",
tree_sitter_go::language(),
Some(Arc::new(go::GoLspAdapter) as Arc<dyn LspAdapter>),
Some(CachedLspAdapter::new(go::GoLspAdapter).await),
),
(
"json",
tree_sitter_json::language(),
Some(Arc::new(json::JsonLspAdapter)),
Some(CachedLspAdapter::new(json::JsonLspAdapter).await),
),
(
"markdown",
@@ -47,12 +63,12 @@ pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegi
(
"python",
tree_sitter_python::language(),
Some(Arc::new(python::PythonLspAdapter)),
Some(CachedLspAdapter::new(python::PythonLspAdapter).await),
),
(
"rust",
tree_sitter_rust::language(),
Some(Arc::new(rust::RustLspAdapter)),
Some(CachedLspAdapter::new(rust::RustLspAdapter).await),
),
(
"toml",
@@ -62,28 +78,27 @@ pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegi
(
"tsx",
tree_sitter_typescript::language_tsx(),
Some(Arc::new(typescript::TypeScriptLspAdapter)),
Some(CachedLspAdapter::new(typescript::TypeScriptLspAdapter).await),
),
(
"typescript",
tree_sitter_typescript::language_typescript(),
Some(Arc::new(typescript::TypeScriptLspAdapter)),
Some(CachedLspAdapter::new(typescript::TypeScriptLspAdapter).await),
),
(
"javascript",
tree_sitter_typescript::language_tsx(),
Some(Arc::new(typescript::TypeScriptLspAdapter)),
Some(CachedLspAdapter::new(typescript::TypeScriptLspAdapter).await),
),
] {
languages.add(Arc::new(language(name, grammar, lsp_adapter)));
}
languages
}
pub(crate) fn language(
name: &str,
grammar: tree_sitter::Language,
lsp_adapter: Option<Arc<dyn LspAdapter>>,
lsp_adapter: Option<Arc<CachedLspAdapter>>,
) -> Language {
let config = toml::from_slice(
&LanguageDir::get(&format!("{}/config.toml", name))

View File

@@ -1,102 +1,91 @@
use super::installation::{latest_github_release, GitHubLspBinaryVersion};
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use client::http::HttpClient;
use futures::{future::BoxFuture, FutureExt, StreamExt};
use futures::StreamExt;
pub use language::*;
use smol::fs::{self, File};
use std::{
any::Any,
path::{Path, PathBuf},
sync::Arc,
};
use util::{ResultExt, TryFutureExt};
use std::{any::Any, path::PathBuf, sync::Arc};
use util::ResultExt;
pub struct CLspAdapter;
#[async_trait]
impl super::LspAdapter for CLspAdapter {
fn name(&self) -> LanguageServerName {
async fn name(&self) -> LanguageServerName {
LanguageServerName("clangd".into())
}
fn fetch_latest_server_version(
async fn fetch_latest_server_version(
&self,
http: Arc<dyn HttpClient>,
) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
async move {
let release = latest_github_release("clangd/clangd", http).await?;
let asset_name = format!("clangd-mac-{}.zip", release.name);
let asset = release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
let version = GitHubLspBinaryVersion {
name: release.name,
url: asset.browser_download_url.clone(),
};
Ok(Box::new(version) as Box<_>)
}
.boxed()
) -> Result<Box<dyn 'static + Send + Any>> {
let release = latest_github_release("clangd/clangd", http).await?;
let asset_name = format!("clangd-mac-{}.zip", release.name);
let asset = release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
let version = GitHubLspBinaryVersion {
name: release.name,
url: asset.browser_download_url.clone(),
};
Ok(Box::new(version) as Box<_>)
}
fn fetch_server_binary(
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
http: Arc<dyn HttpClient>,
container_dir: Arc<Path>,
) -> BoxFuture<'static, Result<PathBuf>> {
container_dir: PathBuf,
) -> Result<PathBuf> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
async move {
let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
let version_dir = container_dir.join(format!("clangd_{}", version.name));
let binary_path = version_dir.join("bin/clangd");
let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
let version_dir = container_dir.join(format!("clangd_{}", version.name));
let binary_path = version_dir.join("bin/clangd");
if fs::metadata(&binary_path).await.is_err() {
let mut response = http
.get(&version.url, Default::default(), true)
.await
.context("error downloading release")?;
let mut file = File::create(&zip_path).await?;
if !response.status().is_success() {
Err(anyhow!(
"download failed with status {}",
response.status().to_string()
))?;
}
futures::io::copy(response.body_mut(), &mut file).await?;
if fs::metadata(&binary_path).await.is_err() {
let mut response = http
.get(&version.url, Default::default(), true)
.await
.context("error downloading release")?;
let mut file = File::create(&zip_path).await?;
if !response.status().is_success() {
Err(anyhow!(
"download failed with status {}",
response.status().to_string()
))?;
}
futures::io::copy(response.body_mut(), &mut file).await?;
let unzip_status = smol::process::Command::new("unzip")
.current_dir(&container_dir)
.arg(&zip_path)
.output()
.await?
.status;
if !unzip_status.success() {
Err(anyhow!("failed to unzip clangd archive"))?;
}
let unzip_status = smol::process::Command::new("unzip")
.current_dir(&container_dir)
.arg(&zip_path)
.output()
.await?
.status;
if !unzip_status.success() {
Err(anyhow!("failed to unzip clangd archive"))?;
}
if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err() {
let entry_path = entry.path();
if entry_path.as_path() != version_dir {
fs::remove_dir_all(&entry_path).await.log_err();
}
if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err() {
let entry_path = entry.path();
if entry_path.as_path() != version_dir {
fs::remove_dir_all(&entry_path).await.log_err();
}
}
}
}
Ok(binary_path)
}
.boxed()
Ok(binary_path)
}
fn cached_server_binary(
&self,
container_dir: Arc<Path>,
) -> BoxFuture<'static, Option<PathBuf>> {
async move {
async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
(|| async move {
let mut last_clangd_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {
@@ -115,12 +104,12 @@ impl super::LspAdapter for CLspAdapter {
clangd_dir
))
}
}
})()
.await
.log_err()
.boxed()
}
fn label_for_completion(
async fn label_for_completion(
&self,
completion: &lsp::CompletionItem,
language: &Language,
@@ -197,7 +186,7 @@ impl super::LspAdapter for CLspAdapter {
Some(CodeLabel::plain(label.to_string(), None))
}
fn label_for_symbol(
async fn label_for_symbol(
&self,
name: &str,
kind: lsp::SymbolKind,

View File

@@ -1,19 +1,14 @@
use super::installation::latest_github_release;
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use client::http::HttpClient;
use futures::{future::BoxFuture, FutureExt, StreamExt};
use futures::StreamExt;
pub use language::*;
use lazy_static::lazy_static;
use regex::Regex;
use smol::{fs, process};
use std::{
any::Any,
ops::Range,
path::{Path, PathBuf},
str,
sync::Arc,
};
use util::{ResultExt, TryFutureExt};
use std::{any::Any, ops::Range, path::PathBuf, str, sync::Arc};
use util::ResultExt;
#[derive(Copy, Clone)]
pub struct GoLspAdapter;
@@ -22,104 +17,96 @@ lazy_static! {
static ref GOPLS_VERSION_REGEX: Regex = Regex::new(r"\d+\.\d+\.\d+").unwrap();
}
#[async_trait]
impl super::LspAdapter for GoLspAdapter {
fn name(&self) -> LanguageServerName {
async fn name(&self) -> LanguageServerName {
LanguageServerName("gopls".into())
}
fn server_args(&self) -> &[&str] {
&["-mode=stdio"]
async fn server_args(&self) -> Vec<String> {
vec!["-mode=stdio".into()]
}
fn fetch_latest_server_version(
async fn fetch_latest_server_version(
&self,
http: Arc<dyn HttpClient>,
) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
async move {
let release = latest_github_release("golang/tools", http).await?;
let version: Option<String> = release.name.strip_prefix("gopls/v").map(str::to_string);
if version.is_none() {
log::warn!(
"couldn't infer gopls version from github release name '{}'",
release.name
);
}
Ok(Box::new(version) as Box<_>)
) -> Result<Box<dyn 'static + Send + Any>> {
let release = latest_github_release("golang/tools", http).await?;
let version: Option<String> = release.name.strip_prefix("gopls/v").map(str::to_string);
if version.is_none() {
log::warn!(
"couldn't infer gopls version from github release name '{}'",
release.name
);
}
.boxed()
Ok(Box::new(version) as Box<_>)
}
fn fetch_server_binary(
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
_: Arc<dyn HttpClient>,
container_dir: Arc<Path>,
) -> BoxFuture<'static, Result<PathBuf>> {
container_dir: PathBuf,
) -> Result<PathBuf> {
let version = version.downcast::<Option<String>>().unwrap();
let this = *self;
async move {
if let Some(version) = *version {
let binary_path = container_dir.join(&format!("gopls_{version}"));
if let Ok(metadata) = fs::metadata(&binary_path).await {
if metadata.is_file() {
if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err() {
let entry_path = entry.path();
if entry_path.as_path() != binary_path
&& entry.file_name() != "gobin"
{
fs::remove_file(&entry_path).await.log_err();
}
if let Some(version) = *version {
let binary_path = container_dir.join(&format!("gopls_{version}"));
if let Ok(metadata) = fs::metadata(&binary_path).await {
if metadata.is_file() {
if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err() {
let entry_path = entry.path();
if entry_path.as_path() != binary_path
&& entry.file_name() != "gobin"
{
fs::remove_file(&entry_path).await.log_err();
}
}
}
return Ok(binary_path.to_path_buf());
}
return Ok(binary_path.to_path_buf());
}
} else if let Some(path) = this.cached_server_binary(container_dir.clone()).await {
return Ok(path.to_path_buf());
}
let gobin_dir = container_dir.join("gobin");
fs::create_dir_all(&gobin_dir).await?;
let install_output = process::Command::new("go")
.env("GO111MODULE", "on")
.env("GOBIN", &gobin_dir)
.args(["install", "golang.org/x/tools/gopls@latest"])
.output()
.await?;
if !install_output.status.success() {
Err(anyhow!("failed to install gopls. Is go installed?"))?;
}
let installed_binary_path = gobin_dir.join("gopls");
let version_output = process::Command::new(&installed_binary_path)
.arg("version")
.output()
.await
.map_err(|e| anyhow!("failed to run installed gopls binary {:?}", e))?;
let version_stdout = str::from_utf8(&version_output.stdout)
.map_err(|_| anyhow!("gopls version produced invalid utf8"))?;
let version = GOPLS_VERSION_REGEX
.find(version_stdout)
.ok_or_else(|| anyhow!("failed to parse gopls version output"))?
.as_str();
let binary_path = container_dir.join(&format!("gopls_{version}"));
fs::rename(&installed_binary_path, &binary_path).await?;
Ok(binary_path.to_path_buf())
} else if let Some(path) = this.cached_server_binary(container_dir.clone()).await {
return Ok(path.to_path_buf());
}
.boxed()
let gobin_dir = container_dir.join("gobin");
fs::create_dir_all(&gobin_dir).await?;
let install_output = process::Command::new("go")
.env("GO111MODULE", "on")
.env("GOBIN", &gobin_dir)
.args(["install", "golang.org/x/tools/gopls@latest"])
.output()
.await?;
if !install_output.status.success() {
Err(anyhow!("failed to install gopls. Is go installed?"))?;
}
let installed_binary_path = gobin_dir.join("gopls");
let version_output = process::Command::new(&installed_binary_path)
.arg("version")
.output()
.await
.map_err(|e| anyhow!("failed to run installed gopls binary {:?}", e))?;
let version_stdout = str::from_utf8(&version_output.stdout)
.map_err(|_| anyhow!("gopls version produced invalid utf8"))?;
let version = GOPLS_VERSION_REGEX
.find(version_stdout)
.ok_or_else(|| anyhow!("failed to parse gopls version output"))?
.as_str();
let binary_path = container_dir.join(&format!("gopls_{version}"));
fs::rename(&installed_binary_path, &binary_path).await?;
Ok(binary_path.to_path_buf())
}
fn cached_server_binary(
&self,
container_dir: Arc<Path>,
) -> BoxFuture<'static, Option<PathBuf>> {
async move {
async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
(|| async move {
let mut last_binary_path = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {
@@ -139,12 +126,12 @@ impl super::LspAdapter for GoLspAdapter {
} else {
Err(anyhow!("no cached binary"))
}
}
})()
.await
.log_err()
.boxed()
}
fn label_for_completion(
async fn label_for_completion(
&self,
completion: &lsp::CompletionItem,
language: &Language,
@@ -244,7 +231,7 @@ impl super::LspAdapter for GoLspAdapter {
None
}
fn label_for_symbol(
async fn label_for_symbol(
&self,
name: &str,
kind: lsp::SymbolKind,
@@ -322,12 +309,12 @@ mod tests {
use gpui::color::Color;
use theme::SyntaxTheme;
#[test]
fn test_go_label_for_completion() {
#[gpui::test]
async fn test_go_label_for_completion() {
let language = language(
"go",
tree_sitter_go::language(),
Some(Arc::new(GoLspAdapter)),
Some(CachedLspAdapter::new(GoLspAdapter).await),
);
let theme = SyntaxTheme::new(vec![
@@ -347,12 +334,14 @@ mod tests {
let highlight_field = grammar.highlight_id_for_name("property").unwrap();
assert_eq!(
language.label_for_completion(&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::FUNCTION),
label: "Hello".to_string(),
detail: Some("func(a B) c.D".to_string()),
..Default::default()
}),
language
.label_for_completion(&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::FUNCTION),
label: "Hello".to_string(),
detail: Some("func(a B) c.D".to_string()),
..Default::default()
})
.await,
Some(CodeLabel {
text: "Hello(a B) c.D".to_string(),
filter_range: 0..5,
@@ -366,12 +355,14 @@ mod tests {
// Nested methods
assert_eq!(
language.label_for_completion(&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::METHOD),
label: "one.two.Three".to_string(),
detail: Some("func() [3]interface{}".to_string()),
..Default::default()
}),
language
.label_for_completion(&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::METHOD),
label: "one.two.Three".to_string(),
detail: Some("func() [3]interface{}".to_string()),
..Default::default()
})
.await,
Some(CodeLabel {
text: "one.two.Three() [3]interface{}".to_string(),
filter_range: 0..13,
@@ -385,12 +376,14 @@ mod tests {
// Nested fields
assert_eq!(
language.label_for_completion(&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::FIELD),
label: "two.Three".to_string(),
detail: Some("a.Bcd".to_string()),
..Default::default()
}),
language
.label_for_completion(&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::FIELD),
label: "two.Three".to_string(),
detail: Some("a.Bcd".to_string()),
..Default::default()
})
.await,
Some(CodeLabel {
text: "two.Three a.Bcd".to_string(),
filter_range: 0..9,

View File

@@ -1,16 +1,14 @@
use super::installation::{npm_install_packages, npm_package_latest_version};
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use client::http::HttpClient;
use futures::{future::BoxFuture, FutureExt, StreamExt};
use collections::HashMap;
use futures::StreamExt;
use language::{LanguageServerName, LspAdapter};
use serde_json::json;
use smol::fs;
use std::{
any::Any,
path::{Path, PathBuf},
sync::Arc,
};
use util::{ResultExt, TryFutureExt};
use std::{any::Any, path::PathBuf, sync::Arc};
use util::ResultExt;
pub struct JsonLspAdapter;
@@ -19,68 +17,60 @@ impl JsonLspAdapter {
"node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
}
#[async_trait]
impl LspAdapter for JsonLspAdapter {
fn name(&self) -> LanguageServerName {
async fn name(&self) -> LanguageServerName {
LanguageServerName("vscode-json-languageserver".into())
}
fn server_args(&self) -> &[&str] {
&["--stdio"]
async fn server_args(&self) -> Vec<String> {
vec!["--stdio".into()]
}
fn fetch_latest_server_version(
async fn fetch_latest_server_version(
&self,
_: Arc<dyn HttpClient>,
) -> BoxFuture<'static, Result<Box<dyn 'static + Any + Send>>> {
async move {
Ok(Box::new(npm_package_latest_version("vscode-json-languageserver").await?) as Box<_>)
}
.boxed()
) -> Result<Box<dyn 'static + Any + Send>> {
Ok(Box::new(npm_package_latest_version("vscode-json-languageserver").await?) as Box<_>)
}
fn fetch_server_binary(
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
_: Arc<dyn HttpClient>,
container_dir: Arc<Path>,
) -> BoxFuture<'static, Result<PathBuf>> {
container_dir: PathBuf,
) -> Result<PathBuf> {
let version = version.downcast::<String>().unwrap();
async move {
let version_dir = container_dir.join(version.as_str());
fs::create_dir_all(&version_dir)
.await
.context("failed to create version directory")?;
let binary_path = version_dir.join(Self::BIN_PATH);
let version_dir = container_dir.join(version.as_str());
fs::create_dir_all(&version_dir)
.await
.context("failed to create version directory")?;
let binary_path = version_dir.join(Self::BIN_PATH);
if fs::metadata(&binary_path).await.is_err() {
npm_install_packages(
[("vscode-json-languageserver", version.as_str())],
&version_dir,
)
.await?;
if fs::metadata(&binary_path).await.is_err() {
npm_install_packages(
[("vscode-json-languageserver", version.as_str())],
&version_dir,
)
.await?;
if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err() {
let entry_path = entry.path();
if entry_path.as_path() != version_dir {
fs::remove_dir_all(&entry_path).await.log_err();
}
if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err() {
let entry_path = entry.path();
if entry_path.as_path() != version_dir {
fs::remove_dir_all(&entry_path).await.log_err();
}
}
}
}
Ok(binary_path)
}
.boxed()
Ok(binary_path)
}
fn cached_server_binary(
&self,
container_dir: Arc<Path>,
) -> BoxFuture<'static, Option<PathBuf>> {
async move {
async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
(|| async move {
let mut last_version_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {
@@ -99,22 +89,18 @@ impl LspAdapter for JsonLspAdapter {
last_version_dir
))
}
}
})()
.await
.log_err()
.boxed()
}
fn initialization_options(&self) -> Option<serde_json::Value> {
async fn initialization_options(&self) -> Option<serde_json::Value> {
Some(json!({
"provideFormatter": true
}))
}
fn id_for_language(&self, name: &str) -> Option<String> {
if name == "JSON" {
Some("jsonc".into())
} else {
None
}
async fn language_ids(&self) -> HashMap<String, String> {
[("JSON".into(), "jsonc".into())].into_iter().collect()
}
}

View File

@@ -0,0 +1,141 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use client::http::HttpClient;
use collections::HashMap;
use futures::lock::Mutex;
use gpui::executor::Background;
use language::{LanguageServerName, LspAdapter};
use plugin_runtime::{Plugin, WasiFn};
use std::{any::Any, path::PathBuf, sync::Arc};
use util::ResultExt;
pub struct PluginLspAdapter {
name: WasiFn<(), String>,
server_args: WasiFn<(), Vec<String>>,
fetch_latest_server_version: WasiFn<(), Option<String>>,
fetch_server_binary: WasiFn<(PathBuf, String), Result<PathBuf, String>>,
cached_server_binary: WasiFn<PathBuf, Option<PathBuf>>,
initialization_options: WasiFn<(), String>,
language_ids: WasiFn<(), Vec<(String, String)>>,
executor: Arc<Background>,
runtime: Arc<Mutex<Plugin>>,
}
impl PluginLspAdapter {
#[allow(unused)]
pub async fn new(mut plugin: Plugin, executor: Arc<Background>) -> Result<Self> {
Ok(Self {
name: plugin.function("name")?,
server_args: plugin.function("server_args")?,
fetch_latest_server_version: plugin.function("fetch_latest_server_version")?,
fetch_server_binary: plugin.function("fetch_server_binary")?,
cached_server_binary: plugin.function("cached_server_binary")?,
initialization_options: plugin.function("initialization_options")?,
language_ids: plugin.function("language_ids")?,
executor,
runtime: Arc::new(Mutex::new(plugin)),
})
}
}
#[async_trait]
impl LspAdapter for PluginLspAdapter {
async fn name(&self) -> LanguageServerName {
let name: String = self
.runtime
.lock()
.await
.call(&self.name, ())
.await
.unwrap();
LanguageServerName(name.into())
}
async fn server_args<'a>(&'a self) -> Vec<String> {
self.runtime
.lock()
.await
.call(&self.server_args, ())
.await
.unwrap()
}
async fn fetch_latest_server_version(
&self,
_: Arc<dyn HttpClient>,
) -> Result<Box<dyn 'static + Send + Any>> {
let runtime = self.runtime.clone();
let function = self.fetch_latest_server_version;
self.executor
.spawn(async move {
let mut runtime = runtime.lock().await;
let versions: Result<Option<String>> =
runtime.call::<_, Option<String>>(&function, ()).await;
versions
.map_err(|e| anyhow!("{}", e))?
.ok_or_else(|| anyhow!("Could not fetch latest server version"))
.map(|v| Box::new(v) as Box<_>)
})
.await
}
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
_: Arc<dyn HttpClient>,
container_dir: PathBuf,
) -> Result<PathBuf> {
let version = *version.downcast::<String>().unwrap();
let runtime = self.runtime.clone();
let function = self.fetch_server_binary;
self.executor
.spawn(async move {
let mut runtime = runtime.lock().await;
let handle = runtime.attach_path(&container_dir)?;
let result: Result<PathBuf, String> =
runtime.call(&function, (container_dir, version)).await?;
runtime.remove_resource(handle)?;
result.map_err(|e| anyhow!("{}", e))
})
.await
}
async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
let runtime = self.runtime.clone();
let function = self.cached_server_binary;
self.executor
.spawn(async move {
let mut runtime = runtime.lock().await;
let handle = runtime.attach_path(&container_dir).ok()?;
let result: Option<PathBuf> = runtime.call(&function, container_dir).await.ok()?;
runtime.remove_resource(handle).ok()?;
result
})
.await
}
async fn initialization_options(&self) -> Option<serde_json::Value> {
let string: String = self
.runtime
.lock()
.await
.call(&self.initialization_options, ())
.await
.log_err()?;
serde_json::from_str(&string).ok()
}
async fn language_ids(&self) -> HashMap<String, String> {
self.runtime
.lock()
.await
.call(&self.language_ids, ())
.await
.log_err()
.unwrap_or_default()
.into_iter()
.collect()
}
}

View File

@@ -1,15 +1,12 @@
use super::installation::{npm_install_packages, npm_package_latest_version};
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use client::http::HttpClient;
use futures::{future::BoxFuture, FutureExt, StreamExt};
use futures::StreamExt;
use language::{LanguageServerName, LspAdapter};
use smol::fs;
use std::{
any::Any,
path::{Path, PathBuf},
sync::Arc,
};
use util::{ResultExt, TryFutureExt};
use std::{any::Any, path::PathBuf, sync::Arc};
use util::ResultExt;
pub struct PythonLspAdapter;
@@ -17,61 +14,56 @@ impl PythonLspAdapter {
const BIN_PATH: &'static str = "node_modules/pyright/langserver.index.js";
}
#[async_trait]
impl LspAdapter for PythonLspAdapter {
fn name(&self) -> LanguageServerName {
async fn name(&self) -> LanguageServerName {
LanguageServerName("pyright".into())
}
fn server_args(&self) -> &[&str] {
&["--stdio"]
async fn server_args(&self) -> Vec<String> {
vec!["--stdio".into()]
}
fn fetch_latest_server_version(
async fn fetch_latest_server_version(
&self,
_: Arc<dyn HttpClient>,
) -> BoxFuture<'static, Result<Box<dyn 'static + Any + Send>>> {
async move { Ok(Box::new(npm_package_latest_version("pyright").await?) as Box<_>) }.boxed()
) -> Result<Box<dyn 'static + Any + Send>> {
Ok(Box::new(npm_package_latest_version("pyright").await?) as Box<_>)
}
fn fetch_server_binary(
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
_: Arc<dyn HttpClient>,
container_dir: Arc<Path>,
) -> BoxFuture<'static, Result<PathBuf>> {
container_dir: PathBuf,
) -> Result<PathBuf> {
let version = version.downcast::<String>().unwrap();
async move {
let version_dir = container_dir.join(version.as_str());
fs::create_dir_all(&version_dir)
.await
.context("failed to create version directory")?;
let binary_path = version_dir.join(Self::BIN_PATH);
let version_dir = container_dir.join(version.as_str());
fs::create_dir_all(&version_dir)
.await
.context("failed to create version directory")?;
let binary_path = version_dir.join(Self::BIN_PATH);
if fs::metadata(&binary_path).await.is_err() {
npm_install_packages([("pyright", version.as_str())], &version_dir).await?;
if fs::metadata(&binary_path).await.is_err() {
npm_install_packages([("pyright", version.as_str())], &version_dir).await?;
if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err() {
let entry_path = entry.path();
if entry_path.as_path() != version_dir {
fs::remove_dir_all(&entry_path).await.log_err();
}
if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err() {
let entry_path = entry.path();
if entry_path.as_path() != version_dir {
fs::remove_dir_all(&entry_path).await.log_err();
}
}
}
}
Ok(binary_path)
}
.boxed()
Ok(binary_path)
}
fn cached_server_binary(
&self,
container_dir: Arc<Path>,
) -> BoxFuture<'static, Option<PathBuf>> {
async move {
async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
(|| async move {
let mut last_version_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {
@@ -90,12 +82,12 @@ impl LspAdapter for PythonLspAdapter {
last_version_dir
))
}
}
})()
.await
.log_err()
.boxed()
}
fn label_for_completion(
async fn label_for_completion(
&self,
item: &lsp::CompletionItem,
language: &language::Language,
@@ -116,7 +108,7 @@ impl LspAdapter for PythonLspAdapter {
})
}
fn label_for_symbol(
async fn label_for_symbol(
&self,
name: &str,
kind: lsp::SymbolKind,

View File

@@ -1,116 +1,102 @@
use super::installation::{latest_github_release, GitHubLspBinaryVersion};
use anyhow::{anyhow, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_trait::async_trait;
use client::http::HttpClient;
use futures::{future::BoxFuture, io::BufReader, FutureExt, StreamExt};
use futures::{io::BufReader, StreamExt};
pub use language::*;
use lazy_static::lazy_static;
use regex::Regex;
use smol::fs::{self, File};
use std::{
any::Any,
borrow::Cow,
env::consts,
path::{Path, PathBuf},
str,
sync::Arc,
};
use util::{ResultExt, TryFutureExt};
use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc};
use util::ResultExt;
pub struct RustLspAdapter;
#[async_trait]
impl LspAdapter for RustLspAdapter {
fn name(&self) -> LanguageServerName {
async fn name(&self) -> LanguageServerName {
LanguageServerName("rust-analyzer".into())
}
fn fetch_latest_server_version(
async fn fetch_latest_server_version(
&self,
http: Arc<dyn HttpClient>,
) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
async move {
let release = latest_github_release("rust-analyzer/rust-analyzer", http).await?;
let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH);
let asset = release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
let version = GitHubLspBinaryVersion {
name: release.name,
url: asset.browser_download_url.clone(),
};
Ok(Box::new(version) as Box<_>)
}
.boxed()
) -> Result<Box<dyn 'static + Send + Any>> {
let release = latest_github_release("rust-analyzer/rust-analyzer", http).await?;
let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH);
let asset = release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
let version = GitHubLspBinaryVersion {
name: release.name,
url: asset.browser_download_url.clone(),
};
Ok(Box::new(version) as Box<_>)
}
fn fetch_server_binary(
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
http: Arc<dyn HttpClient>,
container_dir: Arc<Path>,
) -> BoxFuture<'static, Result<PathBuf>> {
async move {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name));
container_dir: PathBuf,
) -> Result<PathBuf> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name));
if fs::metadata(&destination_path).await.is_err() {
let mut response = http
.get(&version.url, Default::default(), true)
.await
.map_err(|err| anyhow!("error downloading release: {}", err))?;
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
let mut file = File::create(&destination_path).await?;
futures::io::copy(decompressed_bytes, &mut file).await?;
fs::set_permissions(
&destination_path,
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
)
.await?;
if fs::metadata(&destination_path).await.is_err() {
let mut response = http
.get(&version.url, Default::default(), true)
.await
.map_err(|err| anyhow!("error downloading release: {}", err))?;
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
let mut file = File::create(&destination_path).await?;
futures::io::copy(decompressed_bytes, &mut file).await?;
fs::set_permissions(
&destination_path,
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
)
.await?;
if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err() {
let entry_path = entry.path();
if entry_path.as_path() != destination_path {
fs::remove_file(&entry_path).await.log_err();
}
if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err() {
let entry_path = entry.path();
if entry_path.as_path() != destination_path {
fs::remove_file(&entry_path).await.log_err();
}
}
}
}
Ok(destination_path)
}
.boxed()
Ok(destination_path)
}
fn cached_server_binary(
&self,
container_dir: Arc<Path>,
) -> BoxFuture<'static, Option<PathBuf>> {
async move {
async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
(|| async move {
let mut last = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {
last = Some(entry?.path());
}
last.ok_or_else(|| anyhow!("no cached binary"))
}
})()
.await
.log_err()
.boxed()
}
fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] {
&["rustc"]
async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
vec!["rustc".into()]
}
fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> {
Some("rustAnalyzer/cargo check")
async fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
Some("rustAnalyzer/cargo check".into())
}
fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
async fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
lazy_static! {
static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap();
}
@@ -130,7 +116,7 @@ impl LspAdapter for RustLspAdapter {
}
}
fn label_for_completion(
async fn label_for_completion(
&self,
completion: &lsp::CompletionItem,
language: &Language,
@@ -206,7 +192,7 @@ impl LspAdapter for RustLspAdapter {
None
}
fn label_for_symbol(
async fn label_for_symbol(
&self,
name: &str,
kind: lsp::SymbolKind,
@@ -269,12 +255,12 @@ impl LspAdapter for RustLspAdapter {
#[cfg(test)]
mod tests {
use super::*;
use crate::languages::{language, LspAdapter};
use crate::languages::{language, CachedLspAdapter};
use gpui::{color::Color, MutableAppContext};
use theme::SyntaxTheme;
#[test]
fn test_process_rust_diagnostics() {
#[gpui::test]
async fn test_process_rust_diagnostics() {
let mut params = lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path("/a").unwrap(),
version: None,
@@ -297,7 +283,7 @@ mod tests {
},
],
};
RustLspAdapter.process_diagnostics(&mut params);
RustLspAdapter.process_diagnostics(&mut params).await;
assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
@@ -314,12 +300,12 @@ mod tests {
);
}
#[test]
fn test_rust_label_for_completion() {
#[gpui::test]
async fn test_rust_label_for_completion() {
let language = language(
"rust",
tree_sitter_rust::language(),
Some(Arc::new(RustLspAdapter)),
Some(CachedLspAdapter::new(RustLspAdapter).await),
);
let grammar = language.grammar().unwrap();
let theme = SyntaxTheme::new(vec![
@@ -337,12 +323,14 @@ mod tests {
let highlight_field = grammar.highlight_id_for_name("property").unwrap();
assert_eq!(
language.label_for_completion(&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::FUNCTION),
label: "hello(…)".to_string(),
detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
..Default::default()
}),
language
.label_for_completion(&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::FUNCTION),
label: "hello(…)".to_string(),
detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
..Default::default()
})
.await,
Some(CodeLabel {
text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
filter_range: 0..5,
@@ -358,12 +346,14 @@ mod tests {
);
assert_eq!(
language.label_for_completion(&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::FIELD),
label: "len".to_string(),
detail: Some("usize".to_string()),
..Default::default()
}),
language
.label_for_completion(&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::FIELD),
label: "len".to_string(),
detail: Some("usize".to_string()),
..Default::default()
})
.await,
Some(CodeLabel {
text: "len: usize".to_string(),
filter_range: 0..3,
@@ -372,12 +362,14 @@ mod tests {
);
assert_eq!(
language.label_for_completion(&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::FUNCTION),
label: "hello(…)".to_string(),
detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
..Default::default()
}),
language
.label_for_completion(&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::FUNCTION),
label: "hello(…)".to_string(),
detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
..Default::default()
})
.await,
Some(CodeLabel {
text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
filter_range: 0..5,
@@ -393,12 +385,12 @@ mod tests {
);
}
#[test]
fn test_rust_label_for_symbol() {
#[gpui::test]
async fn test_rust_label_for_symbol() {
let language = language(
"rust",
tree_sitter_rust::language(),
Some(Arc::new(RustLspAdapter)),
Some(CachedLspAdapter::new(RustLspAdapter).await),
);
let grammar = language.grammar().unwrap();
let theme = SyntaxTheme::new(vec![
@@ -415,7 +407,9 @@ mod tests {
let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
assert_eq!(
language.label_for_symbol("hello", lsp::SymbolKind::FUNCTION),
language
.label_for_symbol("hello", lsp::SymbolKind::FUNCTION)
.await,
Some(CodeLabel {
text: "fn hello".to_string(),
filter_range: 3..8,
@@ -424,7 +418,9 @@ mod tests {
);
assert_eq!(
language.label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER),
language
.label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER)
.await,
Some(CodeLabel {
text: "type World".to_string(),
filter_range: 5..10,

View File

@@ -1,16 +1,13 @@
use super::installation::{npm_install_packages, npm_package_latest_version};
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use client::http::HttpClient;
use futures::{future::BoxFuture, FutureExt, StreamExt};
use futures::StreamExt;
use language::{LanguageServerName, LspAdapter};
use serde_json::json;
use smol::fs;
use std::{
any::Any,
path::{Path, PathBuf},
sync::Arc,
};
use util::{ResultExt, TryFutureExt};
use std::{any::Any, path::PathBuf, sync::Arc};
use util::ResultExt;
pub struct TypeScriptLspAdapter;
@@ -23,80 +20,75 @@ struct Versions {
server_version: String,
}
#[async_trait]
impl LspAdapter for TypeScriptLspAdapter {
fn name(&self) -> LanguageServerName {
async fn name(&self) -> LanguageServerName {
LanguageServerName("typescript-language-server".into())
}
fn server_args(&self) -> &[&str] {
&["--stdio", "--tsserver-path", "node_modules/typescript/lib"]
async fn server_args(&self) -> Vec<String> {
["--stdio", "--tsserver-path", "node_modules/typescript/lib"]
.into_iter()
.map(str::to_string)
.collect()
}
fn fetch_latest_server_version(
async fn fetch_latest_server_version(
&self,
_: Arc<dyn HttpClient>,
) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
async move {
Ok(Box::new(Versions {
typescript_version: npm_package_latest_version("typescript").await?,
server_version: npm_package_latest_version("typescript-language-server").await?,
}) as Box<_>)
}
.boxed()
) -> Result<Box<dyn 'static + Send + Any>> {
Ok(Box::new(Versions {
typescript_version: npm_package_latest_version("typescript").await?,
server_version: npm_package_latest_version("typescript-language-server").await?,
}) as Box<_>)
}
fn fetch_server_binary(
async fn fetch_server_binary(
&self,
versions: Box<dyn 'static + Send + Any>,
_: Arc<dyn HttpClient>,
container_dir: Arc<Path>,
) -> BoxFuture<'static, Result<PathBuf>> {
container_dir: PathBuf,
) -> Result<PathBuf> {
let versions = versions.downcast::<Versions>().unwrap();
async move {
let version_dir = container_dir.join(&format!(
"typescript-{}:server-{}",
versions.typescript_version, versions.server_version
));
fs::create_dir_all(&version_dir)
.await
.context("failed to create version directory")?;
let binary_path = version_dir.join(Self::BIN_PATH);
let version_dir = container_dir.join(&format!(
"typescript-{}:server-{}",
versions.typescript_version, versions.server_version
));
fs::create_dir_all(&version_dir)
.await
.context("failed to create version directory")?;
let binary_path = version_dir.join(Self::BIN_PATH);
if fs::metadata(&binary_path).await.is_err() {
npm_install_packages(
[
("typescript", versions.typescript_version.as_str()),
(
"typescript-language-server",
&versions.server_version.as_str(),
),
],
&version_dir,
)
.await?;
if fs::metadata(&binary_path).await.is_err() {
npm_install_packages(
[
("typescript", versions.typescript_version.as_str()),
(
"typescript-language-server",
&versions.server_version.as_str(),
),
],
&version_dir,
)
.await?;
if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err() {
let entry_path = entry.path();
if entry_path.as_path() != version_dir {
fs::remove_dir_all(&entry_path).await.log_err();
}
if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err() {
let entry_path = entry.path();
if entry_path.as_path() != version_dir {
fs::remove_dir_all(&entry_path).await.log_err();
}
}
}
}
Ok(binary_path)
}
.boxed()
Ok(binary_path)
}
fn cached_server_binary(
&self,
container_dir: Arc<Path>,
) -> BoxFuture<'static, Option<PathBuf>> {
async move {
async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
(|| async move {
let mut last_version_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {
@@ -115,12 +107,12 @@ impl LspAdapter for TypeScriptLspAdapter {
last_version_dir
))
}
}
})()
.await
.log_err()
.boxed()
}
fn label_for_completion(
async fn label_for_completion(
&self,
item: &lsp::CompletionItem,
language: &language::Language,
@@ -143,7 +135,7 @@ impl LspAdapter for TypeScriptLspAdapter {
})
}
fn initialization_options(&self) -> Option<serde_json::Value> {
async fn initialization_options(&self) -> Option<serde_json::Value> {
Some(json!({
"provideFormatter": true
}))

View File

@@ -21,6 +21,7 @@ use futures::{
};
use gpui::{executor::Background, App, AssetSource, AsyncAppContext, Task};
use isahc::{config::Configurable, AsyncBody, Request};
use language::LanguageRegistry;
use log::LevelFilter;
use parking_lot::Mutex;
use project::{Fs, ProjectStore};
@@ -37,14 +38,14 @@ use std::{
time::Duration,
};
use terminal;
use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
use theme::ThemeRegistry;
use util::{ResultExt, TryFutureExt};
use workspace::{self, AppState, NewFile, OpenPaths};
use zed::{
self, build_window_options,
fs::RealFs,
initialize_workspace, languages, menus,
settings_file::{settings_from_files, watch_keymap_file, WatchedJsonFile},
settings_file::{watch_keymap_file, watch_settings_file, WatchedJsonFile},
};
fn main() {
@@ -71,73 +72,7 @@ fn main() {
let fs = Arc::new(RealFs);
let themes = ThemeRegistry::new(Assets, app.font_cache());
let theme = themes.get(DEFAULT_THEME_NAME).unwrap();
let default_settings = Settings::new("Zed Mono", &app.font_cache(), theme)
.unwrap()
.with_language_defaults(
languages::PLAIN_TEXT.name(),
settings::LanguageSettings {
soft_wrap: Some(settings::SoftWrap::PreferredLineLength),
..Default::default()
},
)
.with_language_defaults(
"C",
settings::LanguageSettings {
tab_size: Some(2.try_into().unwrap()),
..Default::default()
},
)
.with_language_defaults(
"C++",
settings::LanguageSettings {
tab_size: Some(2.try_into().unwrap()),
..Default::default()
},
)
.with_language_defaults(
"Go",
settings::LanguageSettings {
tab_size: Some(4.try_into().unwrap()),
hard_tabs: Some(true),
..Default::default()
},
)
.with_language_defaults(
"Markdown",
settings::LanguageSettings {
soft_wrap: Some(settings::SoftWrap::PreferredLineLength),
..Default::default()
},
)
.with_language_defaults(
"Rust",
settings::LanguageSettings {
tab_size: Some(4.try_into().unwrap()),
..Default::default()
},
)
.with_language_defaults(
"JavaScript",
settings::LanguageSettings {
tab_size: Some(2.try_into().unwrap()),
..Default::default()
},
)
.with_language_defaults(
"TypeScript",
settings::LanguageSettings {
tab_size: Some(2.try_into().unwrap()),
..Default::default()
},
)
.with_language_defaults(
"TSX",
settings::LanguageSettings {
tab_size: Some(2.try_into().unwrap()),
..Default::default()
},
);
let default_settings = Settings::defaults(Assets, &app.font_cache(), &themes);
let config_files = load_config_files(&app, fs.clone());
@@ -163,7 +98,12 @@ fn main() {
app.run(move |cx| {
let client = client::Client::new(http.clone());
let mut languages = languages::build_language_registry(login_shell_env_loaded);
let mut languages = LanguageRegistry::new(login_shell_env_loaded);
languages.set_language_server_download_dir(zed::ROOT_PATH.clone());
let languages = Arc::new(languages);
let init_languages = cx
.background()
.spawn(languages::init(languages.clone(), cx.background().clone()));
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
context_menu::init(cx);
@@ -186,39 +126,25 @@ fn main() {
let db = cx.background().block(db);
let (settings_file, keymap_file) = cx.background().block(config_files).unwrap();
let mut settings_rx = settings_from_files(
default_settings,
vec![settings_file],
themes.clone(),
cx.font_cache().clone(),
);
watch_settings_file(default_settings, settings_file, themes.clone(), cx);
watch_keymap_file(keymap_file, cx);
cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
.detach();
cx.spawn(|cx| watch_keymap_file(keymap_file, cx)).detach();
let settings = cx.background().block(settings_rx.next()).unwrap();
cx.spawn(|mut cx| async move {
while let Some(settings) = settings_rx.next().await {
cx.update(|cx| {
cx.update_global(|s, _| *s = settings);
cx.refresh_windows();
});
cx.spawn({
let languages = languages.clone();
|cx| async move {
cx.read(|cx| languages.set_theme(cx.global::<Settings>().theme.clone()));
init_languages.await;
}
})
.detach();
languages.set_language_server_download_dir(zed::ROOT_PATH.clone());
let languages = Arc::new(languages);
cx.observe_global::<Settings, _>({
let languages = languages.clone();
move |cx| {
languages.set_theme(&cx.global::<Settings>().theme.editor.syntax);
}
move |cx| languages.set_theme(cx.global::<Settings>().theme.clone())
})
.detach();
cx.set_global(settings);
let project_store = cx.add_model(|_| ProjectStore::new(db.clone()));
let app_state = Arc::new(AppState {

View File

@@ -26,6 +26,10 @@ pub fn menus() -> Vec<Menu<'static>> {
name: "Open Key Bindings",
action: Box::new(super::OpenKeymap),
},
MenuItem::Action {
name: "Open Default Settings",
action: Box::new(super::OpenDefaultSettings),
},
MenuItem::Action {
name: "Open Default Key Bindings",
action: Box::new(super::OpenDefaultKeymap),
@@ -298,15 +302,15 @@ pub fn menus() -> Vec<Menu<'static>> {
},
MenuItem::Separator,
MenuItem::Action {
name: "Give Feedback",
name: "Documentation",
action: Box::new(crate::OpenBrowser {
url: super::feedback::NEW_ISSUE_URL.into(),
url: "https://zed.dev/docs".into(),
}),
},
MenuItem::Action {
name: "Zed.dev",
name: "Give Feedback",
action: Box::new(crate::OpenBrowser {
url: "https://zed.dev".into(),
url: super::feedback::NEW_ISSUE_URL.into(),
}),
},
MenuItem::Action {

View File

@@ -1,5 +1,5 @@
use futures::{stream, StreamExt};
use gpui::{executor, AsyncAppContext, FontCache};
use futures::StreamExt;
use gpui::{executor, MutableAppContext};
use postage::sink::Sink as _;
use postage::{prelude::Stream, watch};
use project::Fs;
@@ -10,7 +10,7 @@ use theme::ThemeRegistry;
use util::ResultExt;
#[derive(Clone)]
pub struct WatchedJsonFile<T>(watch::Receiver<T>);
pub struct WatchedJsonFile<T>(pub watch::Receiver<T>);
impl<T> WatchedJsonFile<T>
where
@@ -51,57 +51,62 @@ where
}
}
pub fn settings_from_files(
pub fn watch_settings_file(
defaults: Settings,
sources: Vec<WatchedJsonFile<SettingsFileContent>>,
mut file: WatchedJsonFile<SettingsFileContent>,
theme_registry: Arc<ThemeRegistry>,
font_cache: Arc<FontCache>,
) -> impl futures::stream::Stream<Item = Settings> {
stream::select_all(sources.iter().enumerate().map(|(i, source)| {
let mut rx = source.0.clone();
// Consume the initial item from all of the constituent file watches but one.
// This way, the stream will yield exactly one item for the files' initial
// state, and won't return any more items until the files change.
if i > 0 {
rx.try_recv().ok();
cx: &mut MutableAppContext,
) {
settings_updated(&defaults, file.0.borrow().clone(), &theme_registry, cx);
cx.spawn(|mut cx| async move {
while let Some(content) = file.0.recv().await {
cx.update(|cx| settings_updated(&defaults, content, &theme_registry, cx));
}
rx
}))
.map(move |_| {
let mut settings = defaults.clone();
for source in &sources {
settings.merge(&*source.0.borrow(), &theme_registry, &font_cache);
}
settings
})
.detach();
}
pub async fn watch_keymap_file(
mut file: WatchedJsonFile<KeymapFileContent>,
mut cx: AsyncAppContext,
pub fn keymap_updated(content: KeymapFileContent, cx: &mut MutableAppContext) {
cx.clear_bindings();
settings::KeymapFileContent::load_defaults(cx);
content.add(cx).log_err();
}
pub fn settings_updated(
defaults: &Settings,
content: SettingsFileContent,
theme_registry: &Arc<ThemeRegistry>,
cx: &mut MutableAppContext,
) {
while let Some(content) = file.0.recv().await {
cx.update(|cx| {
cx.clear_bindings();
settings::KeymapFileContent::load_defaults(cx);
content.add(cx).log_err();
});
}
let mut settings = defaults.clone();
settings.set_user_settings(content, theme_registry, &cx.font_cache());
cx.set_global(settings);
cx.refresh_windows();
}
pub fn watch_keymap_file(mut file: WatchedJsonFile<KeymapFileContent>, cx: &mut MutableAppContext) {
cx.spawn(|mut cx| async move {
while let Some(content) = file.0.recv().await {
cx.update(|cx| keymap_updated(content, cx));
}
})
.detach();
}
#[cfg(test)]
mod tests {
use super::*;
use project::FakeFs;
use settings::{LanguageSettings, SoftWrap};
use settings::{EditorSettings, SoftWrap};
#[gpui::test]
async fn test_settings_from_files(cx: &mut gpui::TestAppContext) {
async fn test_watch_settings_files(cx: &mut gpui::TestAppContext) {
let executor = cx.background();
let fs = FakeFs::new(executor.clone());
let font_cache = cx.font_cache();
fs.save(
"/settings1.json".as_ref(),
"/settings.json".as_ref(),
&r#"
{
"buffer_font_size": 24,
@@ -122,25 +127,26 @@ mod tests {
.await
.unwrap();
let source1 = WatchedJsonFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await;
let source2 = WatchedJsonFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await;
let source3 = WatchedJsonFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await;
let source = WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
let settings = cx.read(Settings::test).with_language_defaults(
let default_settings = cx.read(Settings::test).with_language_defaults(
"JavaScript",
LanguageSettings {
EditorSettings {
tab_size: Some(2.try_into().unwrap()),
..Default::default()
},
);
let mut settings_rx = settings_from_files(
settings,
vec![source1, source2, source3],
ThemeRegistry::new((), cx.font_cache()),
cx.font_cache(),
);
cx.update(|cx| {
watch_settings_file(
default_settings.clone(),
source,
ThemeRegistry::new((), font_cache),
cx,
)
});
let settings = settings_rx.next().await.unwrap();
cx.foreground().run_until_parked();
let settings = cx.read(|cx| cx.global::<Settings>().clone());
assert_eq!(settings.buffer_font_size, 24.0);
assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
@@ -162,47 +168,18 @@ mod tests {
assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
fs.save(
"/settings2.json".as_ref(),
&r#"
{
"tab_size": 2,
"soft_wrap": "none",
"language_overrides": {
"Markdown": {
"preferred_line_length": 120
}
}
}
"#
.into(),
"/settings.json".as_ref(),
&"(garbage)".into(),
Default::default(),
)
.await
.unwrap();
// fs.remove_file("/settings.json".as_ref(), Default::default())
// .await
// .unwrap();
let settings = settings_rx.next().await.unwrap();
assert_eq!(settings.buffer_font_size, 24.0);
assert_eq!(settings.soft_wrap(None), SoftWrap::None);
assert_eq!(
settings.soft_wrap(Some("Markdown")),
SoftWrap::PreferredLineLength
);
assert_eq!(settings.soft_wrap(Some("JavaScript")), SoftWrap::None);
assert_eq!(settings.preferred_line_length(None), 80);
assert_eq!(settings.preferred_line_length(Some("Markdown")), 120);
assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
assert_eq!(settings.tab_size(None).get(), 2);
assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
assert_eq!(settings.tab_size(Some("JavaScript")).get(), 2);
fs.remove_file("/settings2.json".as_ref(), Default::default())
.await
.unwrap();
let settings = settings_rx.next().await.unwrap();
cx.foreground().run_until_parked();
let settings = cx.read(|cx| cx.global::<Settings>().clone());
assert_eq!(settings.buffer_font_size, 24.0);
assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
@@ -222,5 +199,12 @@ mod tests {
assert_eq!(settings.tab_size(None).get(), 8);
assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
fs.remove_file("/settings.json".as_ref(), Default::default())
.await
.unwrap();
cx.foreground().run_until_parked();
let settings = cx.read(|cx| cx.global::<Settings>().clone());
assert_eq!(settings.buffer_font_size, default_settings.buffer_font_size);
}
}

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