Compare commits

...

181 Commits

Author SHA1 Message Date
Joseph Lyons
38ee5712b5 zed 0.71.3 2023-01-31 12:13:47 -05:00
Max Brunsfeld
c299dbe8d1 Merge pull request #2099 from zed-industries/empty-go-to-def-multibuffer
Avoid opening a definitions tab if there are no definitions found
2023-01-31 11:46:09 -05:00
Joseph T. Lyons
8690d59da1 Merge pull request #2112 from zed-industries/fix-version-for-feedback-related-commands
Fix version for feedback-related commands
2023-01-31 11:26:43 -05:00
Antonio Scandurra
88cc044f1a zed 0.71.2 2023-01-27 11:04:33 +01:00
Antonio Scandurra
85456dfcaa Merge pull request #2103 from zed-industries/connection-staleness
Fix connection staleness issues
2023-01-27 11:04:14 +01:00
Max Brunsfeld
03a8d0968c zed 0.71.1 2023-01-26 10:41:30 -08:00
Max Brunsfeld
aaaaef1246 Merge pull request #2099 from zed-industries/empty-go-to-def-multibuffer
Avoid opening a definitions tab if there are no definitions found
2023-01-26 10:41:05 -08:00
Max Brunsfeld
2cb83f1bbb Merge pull request #2096 from zed-industries/lazy-load-languages
Load languages lazily in the background
2023-01-26 10:40:45 -08:00
Joseph T. Lyons
28d1fd77fd Merge pull request #2095 from zed-industries/fix-crash-when-opening-feedback-while-in-call
Fix crash when opening feedback while in call
2023-01-26 10:39:53 -08:00
Joseph Lyons
ce9c23b669 v0.71.x preview 2023-01-25 15:20:30 -05:00
Joseph T. Lyons
81ed961659 Merge pull request #2088 from zed-industries/add-cursor-position-to-feedback-editor
Add cursor position to feedback editor
2023-01-25 14:29:24 -05:00
Max Brunsfeld
9db55b3029 Merge pull request #2087 from zed-industries/buffer-language-registry
Assign the language registry to all buffers in the project
2023-01-25 11:25:40 -08:00
Joseph Lyons
328b779185 Clean up construction of FeedbackEditor 2023-01-25 14:20:58 -05:00
Joseph Lyons
7f3d937938 Count chars 2023-01-25 14:20:40 -05:00
Joseph Lyons
f68f9f37ab Add cursor position to feedback editor
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
2023-01-25 14:20:23 -05:00
Julia
c22d13286d Merge pull request #2085 from zed-industries/cleanup-debug-printing
Clean up some debug printing
2023-01-25 14:18:43 -05:00
Joseph Lyons
44c7f162b6 Merge branch 'main' into add-cursor-position-to-feedback-editor 2023-01-25 14:01:45 -05:00
Max Brunsfeld
7003a475a7 Assign the language registry to all buffers in the project 2023-01-25 10:44:15 -08:00
Julia
3d8dbee76a Clean up some debug printing 2023-01-25 13:37:04 -05:00
Mikayla Maki
ba6ffd8256 Merge pull request #2081 from zed-industries/fix-failing-ci
Fixes a broken conditional that is only caught on darwin systems
2023-01-25 09:45:30 -08:00
Mikayla Maki
ecb7d1072f Fixes a broken conditional that is only caught on darwin systems 2023-01-25 09:33:07 -08:00
Mikayla Maki
38b83a70aa Merge pull request #2078 from zed-industries/fix-cursor-style
Fix cursor style thrashing on overlapping windows
2023-01-25 09:15:55 -08:00
Mikayla Maki
1fc6276eab Remove debug wiring 2023-01-25 09:10:51 -08:00
Mikayla Maki
45e4e3354e Changed the presenter to only send 'set_cursor_style' on the topmost window
co-authored-by: Antonio <antonio@zed.dev>
2023-01-25 09:10:35 -08:00
Mikayla Maki
27a80a1c94 WIP 2023-01-25 09:10:35 -08:00
Mikayla Maki
426aeb7c5e WIP - adds platform APIs for checking the top most window 2023-01-25 09:10:35 -08:00
Joseph Lyons
3329b2bbd6 Remove gpui:: prefix from parameters 2023-01-24 19:46:04 -05:00
Joseph T. Lyons
a66a0cfd70 Merge pull request #2075 from zed-industries/add-upper-character-count-limit
Add upper character count limit
2023-01-24 19:44:19 -05:00
Julia
27ee994e17 Merge pull request #2074 from zed-industries/decode-openurl-to-pathbuf
Decode URL from `openURLs` to handle percent encoded paths
2023-01-24 19:08:17 -05:00
Julia
0414723a54 Decode URL from openURLs to handle percent encoded paths
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-01-24 18:48:15 -05:00
Joseph Lyons
588419492a Add upper character count limit 2023-01-24 17:38:20 -05:00
Max Brunsfeld
52296836fe Merge pull request #2069 from zed-industries/markdown-fenced-blocks
Support syntax highlighting in Markdown fenced code blocks
2023-01-24 14:19:36 -08:00
Max Brunsfeld
678ee26c5e Merge branch 'main' into markdown-fenced-blocks 2023-01-24 14:13:50 -08:00
Julia
29d67452e0 Merge pull request #2072 from zed-industries/os-file-associations
Insert macOS file association metadata during bundle process
2023-01-24 17:09:41 -05:00
Max Brunsfeld
51984f0d39 Fix feedback editor compile error due to LanguageRegistry API change 2023-01-24 14:09:24 -08:00
Julia
4d73d4b1b9 Insert macOS file association metadata during bundle process 2023-01-24 17:07:02 -05:00
Nathan Sobo
e8cea130a4 Merge pull request #2068 from zed-industries/doc-reparse
Document Buffer::reparse
2023-01-24 09:09:38 -07:00
Antonio Scandurra
dff08d3cfe Merge branch 'main' into markdown-fenced-blocks 2023-01-24 15:43:35 +01:00
Antonio Scandurra
c48e3f3d05 Reparse unknown injection ranges in buffer when adding a new language 2023-01-24 15:29:59 +01:00
Antonio Scandurra
f3509824e8 WIP: Start on SyntaxMapSnapshot::unknown_injection_languages 2023-01-24 12:55:49 +01:00
Antonio Scandurra
14c72cac58 Store syntax layers even if a language for the injection can't be found 2023-01-24 12:25:12 +01:00
Joseph T. Lyons
f95bda64ba Merge pull request #2009 from zed-industries/in-app-feedback
In app feedback
2023-01-24 01:05:05 -05:00
Nathan Sobo
96ffe84edb Document Buffer::reparse 2023-01-23 21:51:10 -07:00
Joseph Lyons
2b3d09f70a Fix CI missing license check 2023-01-23 18:34:10 -05:00
Joseph Lyons
8e8f66a5e1 Merge branch 'main' into in-app-feedback 2023-01-23 18:24:12 -05:00
Joseph Lyons
c9299a49e1 Clean out unused code 2023-01-23 18:19:10 -05:00
Mikayla Maki
9f048a4b1c Merge pull request #2044 from zed-industries/licensing-scripts
Licensing scripts
2023-01-23 12:58:27 -08:00
Mikayla Maki
0f0d5d5726 Added cargo-about auto-install and CI steps 2023-01-23 12:51:32 -08:00
Mikayla Maki
d060114f00 Added complete scripts for generating third party license files 2023-01-23 12:47:12 -08:00
Mikayla Maki
9d58032064 Add action to open licenses file 2023-01-23 12:45:18 -08:00
Mikayla Maki
4609be20de WIP: Adding license compliance to CI 2023-01-23 12:43:42 -08:00
Mikayla Maki
4d05d61ed7 Merge pull request #2049 from zed-industries/425-create-file-for-cli
Create files passed as args to CLI
2023-01-23 10:44:55 -08:00
Antonio Scandurra
8dabdd1baa Ensure injection layer is recomputed when language changes
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-01-23 19:02:06 +01:00
Julia
4678f6e0a5 Merge pull request #2063 from zed-industries/active-tab-close-icon-pointing-hand
Avoid stomping on tab close icon's cursor style
2023-01-23 11:48:35 -05:00
Julia
95b259b841 Avoid stomping on tab close icon's cursor style 2023-01-23 11:43:50 -05:00
Antonio Scandurra
79cf6fb8b6 WIP: Add test for dynamic language injection 2023-01-23 09:45:36 +01:00
Antonio Scandurra
cb610f37f2 WIP: Search language injections also by file extension
There are still a few things left:

1. Add test to verify we can successfully locate a language by its extension
2. Add test to reproduce bug where changing the fenced code block language
   won't reparse the block with the new language
3. Reparse injections for which we couldn't find a language when the language
   registry changes.
4. Check why the markdown grammar considers the trailing triple backtick as
   `(code_block_content)`, as opposed to being part of the outer markdown.
2023-01-23 08:56:41 +01:00
Antonio Scandurra
36e4dcef16 Avoid allocating a string to compare language names 2023-01-23 08:56:41 +01:00
Antonio Scandurra
c49dc8d6e5 Rename LanguageRegistry::get_language to language_for_name 2023-01-23 08:56:41 +01:00
Antonio Scandurra
f086fa3f21 Add syntax injections for Markdown fenced code blocks 2023-01-23 08:56:41 +01:00
Joseph Lyons
c118f9aabd Fix new errors after merge 2023-01-23 01:31:02 -05:00
Joseph Lyons
f2a5a4d0fd Merge branch 'main' into in-app-feedback 2023-01-23 01:20:10 -05:00
Joseph Lyons
fb2278dc6d Complete first iteration of in-app feedback 2023-01-23 00:59:46 -05:00
Mikayla Maki
50d37e1ae7 Merge pull request #2060 from zed-industries/fix-ci-fail
Fix mismatched return types on CI
2023-01-20 18:28:59 -08:00
Mikayla Maki
8dcaa81aad switch return type of accepts_first_mouse 2023-01-20 18:19:24 -08:00
Max Brunsfeld
e1a58e9381 Merge pull request #2059 from zed-industries/no-indent-adjustment-on-error
Avoid adjusting indentation of lines inside of newly-created errors
2023-01-20 17:13:30 -08:00
Max Brunsfeld
56080771e6 Add test for avoiding indent adjustment inside newly-created errors 2023-01-20 17:02:38 -08:00
Mikayla Maki
bb24f1142f Removed dbg 2023-01-20 16:47:23 -08:00
Mikayla Maki
94b2f8e07f Merge pull request #2054 from zed-industries/notification-mouse-events
Notification mouse events
2023-01-20 16:41:27 -08:00
Mikayla Maki
310d867aab Switch PopUp windows to use the NSTrackingArea API and add support for the mouseExited event
Co-authored-by: Antonio <antonio@zed.dev>
2023-01-20 16:35:25 -08:00
Max Brunsfeld
9f74d6e4ac Highlight and auto-indent await expressions in rust 2023-01-20 15:56:56 -08:00
Max Brunsfeld
f7ceebfce3 Avoid adjusting indentation of lines inside newly-created errors 2023-01-20 15:56:45 -08:00
Joseph Lyons
083986dfae WIP
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-01-20 18:05:24 -05:00
Max Brunsfeld
df1e1295e3 Merge pull request #2056 from zed-industries/confirm-quit
Add confirm_quit setting
2023-01-20 14:11:02 -08:00
Joseph Lyons
c1934d6232 WIP 2023-01-20 16:56:56 -05:00
Kay Simmons
4bee273511 Merge pull request #2057 from zed-industries/multiple-definitions-multibuffer
Multiple Definitions Multibuffer
2023-01-20 13:49:07 -08:00
Kay Simmons
2e37c0ea4a Open multiple definitions in a multibuffer instead of opening the files directly 2023-01-20 13:28:13 -08:00
Max Brunsfeld
2f42af2ac3 Add confirm_quit setting 2023-01-20 13:02:38 -08:00
Max Brunsfeld
be2c601176 Merge pull request #2055 from zed-industries/language-config-overrides
Language config overrides
2023-01-20 11:15:26 -08:00
Max Brunsfeld
8dcef46842 Drop 'override.' prefix from capture names in override query
Co-authored-by: Julia Risley <julia@zed.dev>
2023-01-20 10:44:33 -08:00
Max Brunsfeld
2aa7a9e95b Add overrides for all languages
Co-authored-by: Julia Risley <julia@zed.dev>
2023-01-20 10:39:31 -08:00
Mikayla Maki
8af1294ba5 Changed platform mouse moved handling to only fire on active or popup windows
co-authored-by: Antonio <antonio@zed.dev>
2023-01-20 09:37:09 -08:00
Mikayla Maki
5a00729fad Merge pull request #2051 from zed-industries/show-following-to-followed
Show following to followed
2023-01-20 09:23:34 -08:00
Mikayla Maki
97203e1e02 Fix broken merge 2023-01-20 09:19:58 -08:00
Mikayla Maki
95e661a78c Switched from active hover to NSViews acceptsFirstMouse API
Co-authored-by: Nathan <nathan@zed.dev>
2023-01-20 09:14:38 -08:00
Julia
b54b77b9ec Merge pull request #2053 from zed-industries/on-move-out
Hide hovers when mouse leaves area & window focus is lost
2023-01-20 10:55:26 -05:00
Julia
467e3dc50a Hide editor hover on mouse move out & always notify when hiding hover
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-01-20 10:16:24 -05:00
Julia
131f3471fc Don't dispatch mousemove without focus & avoid swallowing external moves
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-01-20 10:11:28 -05:00
Mikayla Maki
88170df7f0 Switched from active hover to NSViews acceptsFirstMouse API 2023-01-19 15:21:26 -08:00
Max Brunsfeld
2967b46a17 Implement scope-specific bracket matching and comment toggling
Co-authored-by: Julia Risley <julia@zed.dev>
2023-01-19 15:04:27 -08:00
Mikayla Maki
4eeb1aec50 Adds UI for showing the followed-by status to collaboration 2023-01-19 14:22:12 -08:00
Max Brunsfeld
1851e2e77c Start work on language config overrides
Co-authored-by: Julia Risley <julia@zed.dev>
2023-01-19 12:32:08 -08:00
Mikayla Maki
4a46227909 Change incoming call notification to only require one click 2023-01-19 11:43:46 -08:00
Mikayla Maki
86371d9f5e Merge pull request #2050 from zed-industries/disable-soft-wrap-in-single-line-editors
Disable soft wrap in single line editors
2023-01-19 11:26:54 -08:00
Joseph Lyons
38476f5429 Disable soft wrap in single line editors
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-01-19 13:45:05 -05:00
Petros Amoiridis
6c9422808a Merge pull request #2048 from zed-industries/408-add-date-to-zedlog
Add date to the log format
2023-01-19 20:26:01 +02:00
Petros Amoiridis
d30e129d63 Create files passed as args to CLI
Co-Authored-by: Mikayla <mikayla@zed.dev>
2023-01-19 19:38:05 +02:00
Julia
ad1947fa50 Add in-window on-move-out mouse handler concept 2023-01-19 12:34:13 -05:00
Petros Amoiridis
f088de5947 Add date to the log format
Co-Authored-By: Mikayla <mikayla@zed.dev>
2023-01-19 19:05:17 +02:00
Antonio Scandurra
c85ad96b45 Merge pull request #2047 from zed-industries/optimize-large-multi-buffers
Avoid stalling the UI thread when running large searches
2023-01-19 17:31:14 +01:00
Antonio Scandurra
1f649e52de Document RopeFingerprint 2023-01-19 17:25:59 +01:00
Antonio Scandurra
0a7111d216 Fix tests 2023-01-19 16:26:27 +01:00
Antonio Scandurra
a58b39f884 Merge branch 'main' into optimize-large-multi-buffers 2023-01-19 16:18:21 +01:00
Antonio Scandurra
c124caeb0d Add test for stream_excerpts_with_context_lines 2023-01-19 15:54:32 +01:00
Antonio Scandurra
5ce065ac92 Introduce MultiBuffer::stream_excerpts_with_context_lines
This allows us to push excerpts in a streaming fashion without blocking
the main thread.
2023-01-19 15:42:14 +01:00
Max Brunsfeld
5189dea3d5 Merge pull request #2046 from zed-industries/line-breaks-in-outline-items
Prevent outline items from accidentally spanning multiple lines
2023-01-18 16:46:45 -08:00
Max Brunsfeld
d9948bf772 Prevent outline items from accidentally spanning multiple lines 2023-01-18 16:43:18 -08:00
Max Brunsfeld
062e7a03a9 Update comments in Pane::close_items 2023-01-18 15:17:44 -08:00
Max Brunsfeld
17b4bfdf98 Merge pull request #2045 from zed-industries/fewer-unsaved-prompts
Avoid prompting to save when closing an untitled buffer that is still open elsewhere
2023-01-18 15:10:19 -08:00
Max Brunsfeld
06c31a0daa Fix workspace tests after changing Item trait 2023-01-18 15:00:40 -08:00
Mikayla Maki
203f569f2e collab 0.5.3 2023-01-18 12:52:58 -08:00
Mikayla Maki
b0fb5913b6 v0.71.x dev 2023-01-18 12:39:38 -08:00
Petros Amoiridis
6cc84a77c8 Merge pull request #2042 from zed-industries/fix-pasting-files
Allow pasting the same entry more than once in project panel
2023-01-18 18:37:31 +02:00
Petros Amoiridis
27a6951403 Allow pasting the same entry more than once in project panel
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-01-18 17:35:21 +02:00
Petros Amoiridis
9f3c8c1e3a Merge pull request #2041 from zed-industries/fix-renaming-file
Fix mouse interrupting file/dir editing in project panel
2023-01-18 15:53:08 +02:00
Antonio Scandurra
a8f466b422 Don't starve the main thread adding too many search excerpts at once 2023-01-18 14:22:23 +01:00
Petros Amoiridis
f8d092fdc6 Fix mouse interrupting file/dir editing in project panel
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-01-18 15:22:20 +02:00
Antonio Scandurra
8ca0f9ac99 Fix compile errors 2023-01-18 13:58:01 +01:00
Antonio Scandurra
a653e87658 WIP: Avoid converting RopeFingerprint into a string
Co-Authored-By: Petros Amoiridis <petros@zed.dev>
2023-01-18 12:22:08 +01:00
Joseph Lyons
bec03dc882 WIP 2023-01-18 00:12:52 -05:00
Kay Simmons
2c3c8b4cb0 Merge pull request #2039 from zed-industries/vim-mode-single-line-editors
disable vim mode in non full editors
2023-01-17 18:43:16 -08:00
Max Brunsfeld
a0a50cb412 Set up fake project paths correctly in tests 2023-01-17 17:40:34 -08:00
Kay Simmons
cf193154e1 fix broken test 2023-01-17 17:35:39 -08:00
Kay Simmons
c3518cefe8 disable vim mode in non full editors 2023-01-17 17:32:10 -08:00
Kay Simmons
4746fb5936 Merge pull request #2038 from zed-industries/fix-sidebar-width-with-dock
Fix issue with sidebars resizing themselves when dock is toggled
2023-01-17 17:22:24 -08:00
Max Brunsfeld
8651320c9f Make workspace items expose their underlying models, remove file-related methods 2023-01-17 17:21:06 -08:00
Kay Simmons
c9a306b4ac Change sidebars to use the window width as a max width rather than participating in the flex
co-authored-by: Mikayla <mikayla@zed.dev>
2023-01-17 16:58:55 -08:00
Max Brunsfeld
292708573f Replace MultiBuffer::files with ::for_each_buffer 2023-01-17 16:16:44 -08:00
Joseph T. Lyons
c3b102f5a8 Add users to mailing list when using an invite link 2023-01-17 16:46:01 -05:00
Max Brunsfeld
f61b870db6 Merge pull request #2034 from zed-industries/tab-focus-search
Use tab instead of command-f to move focus from the search editor to the main editor
2023-01-17 10:25:04 -08:00
Max Brunsfeld
1a6a807db5 Merge pull request #2035 from zed-industries/always-auto-indent-block-on-paste
Always auto-indent in block-wise mode when pasting
2023-01-17 10:24:41 -08:00
Antonio Scandurra
01aac0de48 Merge pull request #2036 from zed-industries/spurious-modified-buffers
Fix buffers appearing as modified when guest joined after buffer had been saved
2023-01-17 18:21:21 +01:00
Antonio Scandurra
dc88a67f50 Fix assertions 2023-01-17 18:09:45 +01:00
Julia
5ce0472a75 Merge pull request #2037 from zed-industries/go-to-fit
Utilize fit autoscroll for various go-to actions
2023-01-17 10:49:22 -05:00
Antonio Scandurra
cc788dc5f7 Verify saved_version, saved_version_fingerprint and saved_mtime 2023-01-17 16:46:06 +01:00
Julia
7726a9ec3d Utilize fit autoscroll for various go-to actions 2023-01-17 10:42:53 -05:00
Antonio Scandurra
fcf97ab41e Bump protocol version 2023-01-17 16:32:54 +01:00
Antonio Scandurra
bb200aa082 Relay saved version metadata to ensure buffers modified state converges 2023-01-17 16:32:54 +01:00
Antonio Scandurra
2cd9db1cfe Ensure Buffer::{is_dirty,has_conflict} converge in randomized test 2023-01-17 16:32:51 +01:00
Antonio Scandurra
467e5691b9 Include saved mtime and fingerprint when serializing buffers
This still doesn't include:

- An assertion in the randomized test to ensure buffers are not spuriously
marked as modified
- Sending an update when synchronizing buffers after a reconnection
2023-01-17 10:46:19 +01:00
Max Brunsfeld
0bd6f9b6ce Add a test for block-wise auto-indent without original indent info 2023-01-16 18:06:58 -08:00
Max Brunsfeld
244f259331 Always auto-indent in block-wise mode when pasting
If the text was copied outside of Zed, so the original indent column is unknown,
then act as if the first line was copied in its entirety.
2023-01-16 17:42:06 -08:00
Max Brunsfeld
625151806a Merge pull request #2022 from zed-industries/restart-lsp-after-invalid-version-reported
Fix crash when restarting a language server after it reports an unknown buffer version
2023-01-16 16:26:50 -08:00
Max Brunsfeld
6810490bf4 Remove tree-sitter dependency from gpui 2023-01-16 16:11:13 -08:00
Max Brunsfeld
3312a06368 Move focus back from buffer search using tab, not cmd-f 2023-01-16 16:01:15 -08:00
Max Brunsfeld
373902d933 Add '>' child operator in keymap context predicates 2023-01-16 16:00:46 -08:00
Max Brunsfeld
f62d13de21 Use a hand-coded parser for keymap context predicates 2023-01-16 15:53:49 -08:00
Julia
df2e9625b3 Merge pull request #2033 from zed-industries/open-with-zed
Make Finder "Open With" work correctly
2023-01-16 16:39:02 -05:00
Julia
765773cfe6 Make Finder "Open With" work correctly 2023-01-16 16:34:10 -05:00
Max Brunsfeld
9e5612348c Merge pull request #2032 from zed-industries/drag-split-dock-panic
Fix panic when trying to create a split in the dock by dragging
2023-01-16 11:51:28 -08:00
Max Brunsfeld
aa9710f7c3 Avoid unwrapping pane split in SplitWithProjectEntry
Also, implement pane-splitting operations more consistently.
2023-01-16 11:46:47 -08:00
Max Brunsfeld
b90e1012bf Don't render split drag targets in the dock 2023-01-16 10:24:17 -08:00
Antonio Scandurra
96186a3dae Merge pull request #2030 from zed-industries/fix-typescript-lsp
Fix error when running TypeScript language server after version 3.0.2
2023-01-16 17:33:44 +01:00
Antonio Scandurra
2c1fd7b0bf Add a 5s timeout when running npm info and npm install
This prevents those two commands from getting stuck when there is
no internet connection.
2023-01-16 16:51:45 +01:00
Antonio Scandurra
9779663c6b Use cli.mjs when available in TypeScript language server
Otherwise, fall back to using `cli.js`.
2023-01-16 16:50:30 +01:00
Joseph T. Lyons
8e02266d07 Add Discourse release action 2023-01-14 02:30:21 -05:00
Mikayla Maki
24ef80f4b6 Merge pull request #2027 from zed-industries/fix-keybindings-in-command-palette
Fix bug where keybindings would not show in command palette
2023-01-11 16:40:04 -08:00
Mikayla Maki
febf992a43 Fix bug where keybindings would not show in command palette 2023-01-11 16:35:49 -08:00
Kay Simmons
e9fdb13cb5 Merge pull request #2025 from zed-industries/vim-r
Vim replace
2023-01-11 16:28:39 -08:00
Kay Simmons
216b1aec08 fix replace in normal and visual modes 2023-01-11 14:57:40 -08:00
Max Brunsfeld
02f6928328 collab 0.5.2 2023-01-11 14:00:44 -08:00
Max Brunsfeld
fe27f135c0 Bump protocol version after reconnect support 2023-01-11 14:00:16 -08:00
Max Brunsfeld
74f8b493b2 collab 0.5.1 2023-01-11 13:25:28 -08:00
Max Brunsfeld
49379924cb Avoid dropping is_complete column for backward compatibility
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2023-01-11 13:25:02 -08:00
Kay Simmons
14eec66e38 in progress 2023-01-11 12:10:55 -08:00
Mikayla Maki
048da9ddce collab 0.5.0 2023-01-11 10:50:16 -08:00
Mikayla Maki
9c627e82a0 v0.70.x dev 2023-01-11 10:34:11 -08:00
Max Brunsfeld
41ff42ddec Fix crash when restarting a language server after it reports an unknown buffer version
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2023-01-10 16:27:15 -08:00
Joseph Lyons
5517e743e1 Merge branch 'main' into in-app-feedback 2023-01-09 14:05:30 -05:00
Joseph Lyons
c1e61b479c Move feedback items into a feedback crate 2023-01-09 13:55:06 -05:00
Joseph Lyons
a73e264c3d Merge branch 'in-app-feedback' of https://github.com/zed-industries/zed into in-app-feedback 2023-01-07 18:53:11 -05:00
Joseph Lyons
0200fc5542 WIP
Don't rely on contacts popover or contacts list for theming
Add metrics id to request body
Clean up some code and comments

Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-01-07 18:53:00 -05:00
Joseph Lyons
9694771752 Move notes into PR 2023-01-07 18:53:00 -05:00
Joseph Lyons
9fc7f54631 Add to TODO 2023-01-07 18:53:00 -05:00
Joseph Lyons
1545b2ac61 Update TODO 2023-01-07 18:53:00 -05:00
Joseph Lyons
318a0b7ed0 In-app feedback WIP 2023-01-07 18:53:00 -05:00
Joseph Lyons
5387695ee0 WIP
Don't rely on contacts popover or contacts list for theming
Add metrics id to request body
Clean up some code and comments

Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-01-06 17:40:30 -05:00
Joseph Lyons
9d4cf2ff62 Move notes into PR 2023-01-06 15:41:31 -05:00
Joseph Lyons
658541ec9f Add to TODO 2023-01-06 15:32:28 -05:00
Joseph Lyons
404f59090c Update TODO 2023-01-05 18:14:28 -05:00
Joseph Lyons
eb02834582 In-app feedback WIP 2023-01-05 17:58:52 -05:00
212 changed files with 4650 additions and 3230 deletions

View File

@@ -41,16 +41,19 @@ jobs:
with:
clean: false
submodules: 'recursive'
- name: Run tests
run: cargo test --workspace --no-fail-fast
- name: Build collab
run: cargo build -p collab
- name: Build other binaries
run: cargo build --workspace --bins --all-features
- name: Generate license file
run: script/generate-licenses
bundle:
name: Bundle app
runs-on:
@@ -109,6 +112,9 @@ jobs:
exit 1
fi
- name: Generate license file
run: script/generate-licenses
- name: Create app bundle
run: script/bundle

View File

@@ -21,6 +21,15 @@ jobs:
${{ github.event.release.body }}
```
discourse_release:
runs-on: ubuntu-latest
steps:
- name: Install Node
uses: actions/setup-node@v2
if: ${{ ! github.event.release.prerelease }}
with:
node-version: '16'
- run: script/discourse_release ${{ secrets.DISCOURSE_RELEASES_API_KEY }} ${{ github.event.release.tag_name }} ${{ github.event.release.body }}
mixpanel_release:
runs-on: ubuntu-latest
steps:

1
.gitignore vendored
View File

@@ -9,6 +9,7 @@
/assets/themes/*.json
/assets/themes/Internal/*.json
/assets/themes/Experiments/*.json
/assets/licenses.md
**/venv
.build
Packages

46
Cargo.lock generated
View File

@@ -739,8 +739,7 @@ dependencies = [
[[package]]
name = "bromberg_sl2"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ed88064f69518b7e3ea50ecfc1b61d43f19248618a377b95ae5c8b611134d4d"
source = "git+https://github.com/zed-industries/bromberg_sl2?rev=dac565a90e8f9245f48ff46225c915dc50f76920#dac565a90e8f9245f48ff46225c915dc50f76920"
dependencies = [
"digest 0.9.0",
"lazy_static",
@@ -1133,7 +1132,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.4.2"
version = "0.5.3"
dependencies = [
"anyhow",
"async-tungstenite",
@@ -2022,6 +2021,33 @@ dependencies = [
"instant",
]
[[package]]
name = "feedback"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"editor",
"futures 0.3.25",
"gpui",
"human_bytes",
"isahc",
"language",
"lazy_static",
"log",
"postage",
"project",
"search",
"serde",
"settings",
"sysinfo",
"theme",
"tree-sitter-markdown",
"urlencoding",
"util",
"workspace",
]
[[package]]
name = "file-per-thread-logger"
version = "0.1.5"
@@ -2563,7 +2589,6 @@ dependencies = [
"sum_tree",
"time 0.3.17",
"tiny-skia",
"tree-sitter",
"usvg",
"util",
"waker-fn",
@@ -3155,10 +3180,12 @@ dependencies = [
"tree-sitter-html",
"tree-sitter-javascript",
"tree-sitter-json 0.19.0",
"tree-sitter-markdown",
"tree-sitter-python",
"tree-sitter-ruby",
"tree-sitter-rust",
"tree-sitter-typescript",
"unicase",
"unindent",
"util",
]
@@ -5512,6 +5539,7 @@ dependencies = [
"anyhow",
"collections",
"editor",
"futures 0.3.25",
"gpui",
"language",
"log",
@@ -5522,6 +5550,7 @@ dependencies = [
"serde_json",
"settings",
"smallvec",
"smol",
"theme",
"unindent",
"util",
@@ -6239,9 +6268,9 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.27.1"
version = "0.27.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccb297c0afb439440834b4bcf02c5c9da8ec2e808e70f36b0d8e815ff403bd24"
checksum = "1620f9573034c573376acc550f3b9a2be96daeb08abb3c12c8523e1cee06e80f"
dependencies = [
"cfg-if 1.0.0",
"core-foundation-sys",
@@ -8187,7 +8216,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "zed"
version = "0.69.0"
version = "0.71.3"
dependencies = [
"activity_indicator",
"anyhow",
@@ -8212,6 +8241,7 @@ dependencies = [
"easy-parallel",
"editor",
"env_logger",
"feedback",
"file_finder",
"fs",
"fsevent",
@@ -8219,7 +8249,6 @@ dependencies = [
"fuzzy",
"go_to_line",
"gpui",
"human_bytes",
"ignore",
"image",
"indexmap",
@@ -8253,7 +8282,6 @@ dependencies = [
"smallvec",
"smol",
"sum_tree",
"sysinfo",
"tempdir",
"terminal_view",
"text",

View File

@@ -17,6 +17,7 @@ members = [
"crates/diagnostics",
"crates/drag_and_drop",
"crates/editor",
"crates/feedback",
"crates/file_finder",
"crates/fs",
"crates/fsevent",
@@ -83,5 +84,3 @@ split-debuginfo = "unpacked"
[profile.release]
debug = true

View File

@@ -5,6 +5,7 @@ WORKDIR app
COPY . .
# Compile collab server
ARG CARGO_PROFILE_RELEASE_PANIC=abort
RUN --mount=type=cache,target=./script/node_modules \
--mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=./target \

View File

@@ -186,10 +186,10 @@
}
},
{
"context": "BufferSearchBar",
"context": "BufferSearchBar > Editor",
"bindings": {
"escape": "buffer_search::Dismiss",
"cmd-f": "buffer_search::FocusEditor",
"tab": "buffer_search::FocusEditor",
"enter": "search::SelectNextMatch",
"shift-enter": "search::SelectPrevMatch"
}

View File

@@ -209,6 +209,10 @@
"ctrl-e": [
"vim::Scroll",
"LineDown"
],
"r": [
"vim::PushOperator",
"Replace"
]
}
},
@@ -294,7 +298,11 @@
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",
"y": "vim::VisualYank",
"p": "vim::VisualPaste"
"p": "vim::VisualPaste",
"r": [
"vim::PushOperator",
"Replace"
]
}
},
{

View File

@@ -13,6 +13,8 @@
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
"hover_popover_enabled": true,
// Whether to confirm before quitting Zed.
"confirm_quit": false,
// Whether the cursor blinks in the editor.
"cursor_blink": true,
// Whether to pop the completions menu while typing in an editor without

View File

@@ -2,6 +2,7 @@
name = "activity_indicator"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/activity_indicator.rs"

View File

@@ -2,6 +2,7 @@
name = "assets"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/assets.rs"

View File

@@ -2,6 +2,7 @@
name = "auto_update"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/auto_update.rs"

View File

@@ -2,15 +2,15 @@ mod update_notification;
use anyhow::{anyhow, Context, Result};
use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN};
use client::{ZED_APP_PATH, ZED_APP_VERSION};
use db::kvp::KEY_VALUE_STORE;
use gpui::{
actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
MutableAppContext, Task, WeakViewHandle,
};
use lazy_static::lazy_static;
use serde::Deserialize;
use smol::{fs::File, io::AsyncReadExt, process::Command};
use std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration};
use std::{ffi::OsString, sync::Arc, time::Duration};
use update_notification::UpdateNotification;
use util::channel::ReleaseChannel;
use workspace::Workspace;
@@ -18,13 +18,6 @@ use workspace::Workspace;
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
lazy_static! {
pub static ref ZED_APP_VERSION: Option<AppVersion> = env::var("ZED_APP_VERSION")
.ok()
.and_then(|v| v.parse().ok());
pub static ref ZED_APP_PATH: Option<PathBuf> = env::var("ZED_APP_PATH").ok().map(PathBuf::from);
}
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
#[derive(Clone, Copy, PartialEq, Eq)]

View File

@@ -2,6 +2,7 @@
name = "breadcrumbs"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/breadcrumbs.rs"

View File

@@ -2,6 +2,7 @@
name = "call"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/call.rs"

View File

@@ -2,6 +2,7 @@
name = "cli"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/cli.rs"

View File

@@ -9,7 +9,13 @@ use core_foundation::{
use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
use serde::Deserialize;
use std::{ffi::OsStr, fs, path::PathBuf, ptr};
use std::{
ffi::OsStr,
fs::{self, OpenOptions},
io,
path::{Path, PathBuf},
ptr,
};
#[derive(Parser)]
#[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))]
@@ -54,6 +60,12 @@ fn main() -> Result<()> {
return Ok(());
}
for path in args.paths.iter() {
if !path.exists() {
touch(path.as_path())?;
}
}
let (tx, rx) = launch_app(bundle_path)?;
tx.send(CliRequest::Open {
@@ -77,6 +89,13 @@ fn main() -> Result<()> {
Ok(())
}
fn touch(path: &Path) -> io::Result<()> {
match OpenOptions::new().create(true).write(true).open(path) {
Ok(_) => Ok(()),
Err(e) => Err(e),
}
}
fn locate_bundle() -> Result<PathBuf> {
let cli_path = std::env::current_exe()?.canonicalize()?;
let mut app_path = cli_path.clone();

View File

@@ -2,6 +2,7 @@
name = "client"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/client.rs"

View File

@@ -15,7 +15,7 @@ use futures::{future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamEx
use gpui::{
actions,
serde_json::{self, Value},
AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext,
AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext, AppVersion,
AsyncAppContext, Entity, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
};
use http::HttpClient;
@@ -55,6 +55,11 @@ lazy_static! {
pub static ref ADMIN_API_TOKEN: Option<String> = std::env::var("ZED_ADMIN_API_TOKEN")
.ok()
.and_then(|s| if s.is_empty() { None } else { Some(s) });
pub static ref ZED_APP_VERSION: Option<AppVersion> = std::env::var("ZED_APP_VERSION")
.ok()
.and_then(|v| v.parse().ok());
pub static ref ZED_APP_PATH: Option<PathBuf> =
std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
}
pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
@@ -1315,6 +1320,10 @@ impl Client {
pub fn telemetry_log_file_path(&self) -> Option<PathBuf> {
self.telemetry.log_file_path()
}
pub fn metrics_id(&self) -> Option<Arc<str>> {
self.telemetry.metrics_id()
}
}
impl WeakSubscriber {

View File

@@ -278,6 +278,10 @@ impl Telemetry {
}
}
pub fn metrics_id(self: &Arc<Self>) -> Option<Arc<str>> {
self.state.lock().metrics_id.clone()
}
fn flush(self: &Arc<Self>) {
let mut state = self.state.lock();
let mut events = mem::take(&mut state.queue);

View File

@@ -2,6 +2,7 @@
name = "clock"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/clock.rs"

View File

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

View File

@@ -57,6 +57,7 @@ CREATE TABLE "worktrees" (
"abs_path" VARCHAR NOT NULL,
"visible" BOOL NOT NULL,
"scan_id" INTEGER NOT NULL,
"is_complete" BOOL NOT NULL DEFAULT FALSE,
"completed_scan_id" INTEGER NOT NULL,
PRIMARY KEY(project_id, id)
);

View File

@@ -1,3 +1,3 @@
ALTER TABLE worktrees
DROP COLUMN is_complete,
ALTER COLUMN is_complete SET DEFAULT FALSE,
ADD COLUMN completed_scan_id INT8;

View File

@@ -353,6 +353,8 @@ pub struct CreateInviteFromCodeParams {
invite_code: String,
email_address: String,
device_id: Option<String>,
#[serde(default)]
added_to_mailing_list: bool,
}
async fn create_invite_from_code(
@@ -365,6 +367,7 @@ async fn create_invite_from_code(
&params.invite_code,
&params.email_address,
params.device_id.as_deref(),
params.added_to_mailing_list,
)
.await?,
))

View File

@@ -882,6 +882,7 @@ impl Database {
code: &str,
email_address: &str,
device_id: Option<&str>,
added_to_mailing_list: bool,
) -> Result<Invite> {
self.transaction(|tx| async move {
let existing_user = user::Entity::find()
@@ -933,6 +934,7 @@ impl Database {
platform_windows: ActiveValue::set(false),
platform_unknown: ActiveValue::set(true),
device_id: ActiveValue::set(device_id.map(|device_id| device_id.into())),
added_to_mailing_list: ActiveValue::set(added_to_mailing_list),
..Default::default()
})
.on_conflict(
@@ -1584,12 +1586,8 @@ impl Database {
.filter(
Condition::all()
.add(
room_participant::Column::CallingConnectionId
.eq(connection.id as i32),
)
.add(
room_participant::Column::CallingConnectionServerId
.eq(connection.owner_id as i32),
room_participant::Column::CallingUserId
.eq(leaving_participant.user_id),
)
.add(room_participant::Column::AnsweringConnectionId.is_null()),
)

View File

@@ -567,7 +567,12 @@ async fn test_invite_codes() {
// User 2 redeems the invite code and becomes a contact of user 1.
let user2_invite = db
.create_invite_from_code(&invite_code, "user2@example.com", Some("user-2-device-id"))
.create_invite_from_code(
&invite_code,
"user2@example.com",
Some("user-2-device-id"),
true,
)
.await
.unwrap();
let NewUserResult {
@@ -617,7 +622,7 @@ async fn test_invite_codes() {
// User 3 redeems the invite code and becomes a contact of user 1.
let user3_invite = db
.create_invite_from_code(&invite_code, "user3@example.com", None)
.create_invite_from_code(&invite_code, "user3@example.com", None, true)
.await
.unwrap();
let NewUserResult {
@@ -672,9 +677,14 @@ async fn test_invite_codes() {
);
// Trying to reedem the code for the third time results in an error.
db.create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id"))
.await
.unwrap_err();
db.create_invite_from_code(
&invite_code,
"user4@example.com",
Some("user-4-device-id"),
true,
)
.await
.unwrap_err();
// Invite count can be updated after the code has been created.
db.set_invite_count_for_user(user1, 2).await.unwrap();
@@ -684,7 +694,12 @@ async fn test_invite_codes() {
// User 4 can now redeem the invite code and becomes a contact of user 1.
let user4_invite = db
.create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id"))
.create_invite_from_code(
&invite_code,
"user4@example.com",
Some("user-4-device-id"),
true,
)
.await
.unwrap();
let user4 = db
@@ -739,9 +754,14 @@ async fn test_invite_codes() {
);
// An existing user cannot redeem invite codes.
db.create_invite_from_code(&invite_code, "user2@example.com", Some("user-2-device-id"))
.await
.unwrap_err();
db.create_invite_from_code(
&invite_code,
"user2@example.com",
Some("user-2-device-id"),
true,
)
.await
.unwrap_err();
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
assert_eq!(invite_count, 1);
@@ -763,7 +783,7 @@ async fn test_invite_codes() {
db.set_invite_count_for_user(user5, 5).await.unwrap();
let (user5_invite_code, _) = db.get_invite_code_for_user(user5).await.unwrap().unwrap();
let user5_invite_to_user1 = db
.create_invite_from_code(&user5_invite_code, "user1@different.com", None)
.create_invite_from_code(&user5_invite_code, "user1@different.com", None, true)
.await
.unwrap();
let user1_2 = db

View File

@@ -101,10 +101,7 @@ impl TestServer {
async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
cx.update(|cx| {
cx.set_global(HomeDir(Path::new("/tmp/").to_path_buf()));
let mut settings = Settings::test(cx);
settings.projects_online_by_default = false;
cx.set_global(settings);
cx.set_global(Settings::test(cx));
});
let http = FakeHttpClient::with_404_response();

View File

@@ -32,7 +32,9 @@ use std::{
sync::Arc,
};
use unindent::Unindent as _;
use workspace::{item::Item, shared_screen::SharedScreen, SplitDirection, ToggleFollow, Workspace};
use workspace::{
item::ItemHandle as _, shared_screen::SharedScreen, SplitDirection, ToggleFollow, Workspace,
};
#[ctor::ctor]
fn init_logger() {
@@ -5602,7 +5604,7 @@ async fn test_following(
});
assert!(cx_b.read(|cx| editor_b2.is_focused(cx)));
assert_eq!(
editor_b2.read_with(cx_b, |editor, cx| editor.project_path(cx)),
cx_b.read(|cx| editor_b2.project_path(cx)),
Some((worktree_id, "2.txt").into())
);
assert_eq!(

View File

@@ -10,7 +10,7 @@ use collections::BTreeMap;
use fs::{FakeFs, Fs as _};
use futures::StreamExt as _;
use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16};
use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16, Rope};
use lsp::FakeLanguageServer;
use parking_lot::Mutex;
use project::{search::SearchQuery, Project};
@@ -19,7 +19,12 @@ use rand::{
prelude::*,
};
use settings::Settings;
use std::{env, ffi::OsStr, path::PathBuf, sync::Arc};
use std::{
env,
ffi::OsStr,
path::{Path, PathBuf},
sync::Arc,
};
#[gpui::test(iterations = 100)]
async fn test_random_collaboration(
@@ -161,12 +166,10 @@ async fn test_random_collaboration(
let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap();
let pool = server.connection_pool.lock();
for contact in contacts {
if let db::Contact::Accepted { user_id, .. } = contact {
if pool.is_user_online(user_id) {
assert_ne!(
user_id, removed_user_id,
"removed client is still a contact of another peer"
);
if let db::Contact::Accepted { user_id, busy, .. } = contact {
if user_id == removed_user_id {
assert!(!pool.is_user_online(user_id));
assert!(!busy);
}
}
}
@@ -398,6 +401,33 @@ async fn test_random_collaboration(
let guest_diff_base = guest_buffer
.read_with(client_cx, |b, _| b.diff_base().map(ToString::to_string));
assert_eq!(guest_diff_base, host_diff_base);
let host_saved_version =
host_buffer.read_with(host_cx, |b, _| b.saved_version().clone());
let guest_saved_version =
guest_buffer.read_with(client_cx, |b, _| b.saved_version().clone());
assert_eq!(guest_saved_version, host_saved_version);
let host_saved_version_fingerprint =
host_buffer.read_with(host_cx, |b, _| b.saved_version_fingerprint());
let guest_saved_version_fingerprint =
guest_buffer.read_with(client_cx, |b, _| b.saved_version_fingerprint());
assert_eq!(
guest_saved_version_fingerprint,
host_saved_version_fingerprint
);
let host_saved_mtime = host_buffer.read_with(host_cx, |b, _| b.saved_mtime());
let guest_saved_mtime = guest_buffer.read_with(client_cx, |b, _| b.saved_mtime());
assert_eq!(guest_saved_mtime, host_saved_mtime);
let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
assert_eq!(guest_is_dirty, host_is_dirty);
let host_has_conflict = host_buffer.read_with(host_cx, |b, _| b.has_conflict());
let guest_has_conflict = guest_buffer.read_with(client_cx, |b, _| b.has_conflict());
assert_eq!(guest_has_conflict, host_has_conflict);
}
}
}
@@ -638,14 +668,7 @@ async fn randomly_mutate_git(client: &mut TestClient, rng: &Mutex<StdRng>) {
client.fs.create_dir(&git_dir_path).await.unwrap();
}
let mut child_paths = client.fs.read_dir(&dir_path).await.unwrap();
let mut child_file_paths = Vec::new();
while let Some(child_path) = child_paths.next().await {
let child_path = child_path.unwrap();
if client.fs.is_file(&child_path).await {
child_file_paths.push(child_path);
}
}
let mut child_file_paths = child_file_paths(client, &dir_path).await;
let count = rng.lock().gen_range(0..=child_file_paths.len());
child_file_paths.shuffle(&mut *rng.lock());
child_file_paths.truncate(count);
@@ -669,26 +692,63 @@ async fn randomly_mutate_git(client: &mut TestClient, rng: &Mutex<StdRng>) {
}
async fn randomly_mutate_fs(client: &mut TestClient, rng: &Mutex<StdRng>) {
let is_dir = rng.lock().gen::<bool>();
let mut new_path = client
let parent_dir_path = client
.fs
.directories()
.await
.choose(&mut *rng.lock())
.unwrap()
.clone();
new_path.push(gen_file_name(rng));
let is_dir = rng.lock().gen::<bool>();
if is_dir {
log::info!("{}: creating local dir at {:?}", client.username, new_path);
client.fs.create_dir(&new_path).await.unwrap();
let mut dir_path = parent_dir_path.clone();
dir_path.push(gen_file_name(rng));
log::info!("{}: creating local dir at {:?}", client.username, dir_path);
client.fs.create_dir(&dir_path).await.unwrap();
} else {
new_path.set_extension("rs");
log::info!("{}: creating local file at {:?}", client.username, new_path);
client
.fs
.create_file(&new_path, Default::default())
.await
.unwrap();
let child_file_paths = child_file_paths(client, &parent_dir_path).await;
let create_new_file = child_file_paths.is_empty() || rng.lock().gen();
let text = Alphanumeric.sample_string(&mut *rng.lock(), 16);
if create_new_file {
let mut file_path = parent_dir_path.clone();
file_path.push(gen_file_name(rng));
file_path.set_extension("rs");
log::info!(
"{}: creating local file at {:?}",
client.username,
file_path
);
client
.fs
.create_file(&file_path, Default::default())
.await
.unwrap();
log::info!(
"{}: setting local file {:?} text to {:?}",
client.username,
file_path,
text
);
client
.fs
.save(&file_path, &Rope::from(text.as_str()), fs::LineEnding::Unix)
.await
.unwrap();
} else {
let file_path = child_file_paths.choose(&mut *rng.lock()).unwrap();
log::info!(
"{}: setting local file {:?} text to {:?}",
client.username,
file_path,
text
);
client
.fs
.save(file_path, &Rope::from(text.as_str()), fs::LineEnding::Unix)
.await
.unwrap();
}
}
}
@@ -1154,3 +1214,15 @@ fn gen_file_name(rng: &Mutex<StdRng>) -> String {
}
name
}
async fn child_file_paths(client: &TestClient, dir_path: &Path) -> Vec<PathBuf> {
let mut child_paths = client.fs.read_dir(dir_path).await.unwrap();
let mut child_file_paths = Vec::new();
while let Some(child_path) = child_paths.next().await {
let child_path = child_path.unwrap();
if client.fs.is_file(&child_path).await {
child_file_paths.push(child_path);
}
}
child_file_paths
}

View File

@@ -2,6 +2,7 @@
name = "collab_ui"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/collab_ui.rs"

View File

@@ -48,6 +48,7 @@ pub fn init(cx: &mut MutableAppContext) {
},
|_| IncomingCallNotification::new(incoming_call.clone()),
);
notification_windows.push(window_id);
}
}
@@ -225,6 +226,7 @@ impl View for IncomingCallNotification {
.theme
.incoming_call_notification
.background;
Flex::row()
.with_child(self.render_caller(cx))
.with_child(self.render_buttons(cx))

View File

@@ -2,6 +2,7 @@
name = "collections"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/collections.rs"

View File

@@ -2,6 +2,7 @@
name = "command_palette"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/command_palette.rs"

View File

@@ -2,6 +2,7 @@
name = "context_menu"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/context_menu.rs"

View File

@@ -2,6 +2,7 @@
name = "db"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/db.rs"

View File

@@ -2,6 +2,7 @@
name = "diagnostics"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/diagnostics.rs"

View File

@@ -21,7 +21,6 @@ use language::{
use project::{DiagnosticSummary, Project, ProjectPath};
use serde_json::json;
use settings::Settings;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
cmp::Ordering,
@@ -521,12 +520,8 @@ impl Item for ProjectDiagnosticsEditor {
)
}
fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
None
}
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[project::ProjectEntryId; 3]> {
self.editor.project_entry_ids(cx)
fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
self.editor.for_each_project_item(cx, f)
}
fn is_singleton(&self, _: &AppContext) -> bool {

View File

@@ -2,6 +2,7 @@
name = "drag_and_drop"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/drag_and_drop.rs"

View File

@@ -2,6 +2,7 @@
name = "editor"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/editor.rs"

View File

@@ -44,7 +44,7 @@ use gpui::{
ViewContext, ViewHandle, WeakViewHandle,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
use hover_popover::{hide_hover, HideHover, HoverState};
pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools;
pub use language::{char_kind, CharKind};
@@ -62,7 +62,7 @@ pub use multi_buffer::{
};
use multi_buffer::{MultiBufferChunks, ToOffsetUtf16};
use ordered_float::OrderedFloat;
use project::{FormatTrigger, LocationLink, Project, ProjectPath, ProjectTransaction};
use project::{FormatTrigger, Location, LocationLink, Project, ProjectPath, ProjectTransaction};
use scroll::{
autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide,
};
@@ -1008,6 +1008,15 @@ impl Editor {
Self::new(EditorMode::SingleLine, buffer, None, field_editor_style, cx)
}
pub fn multi_line(
field_editor_style: Option<Arc<GetFieldEditorTheme>>,
cx: &mut ViewContext<Self>,
) -> Self {
let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
Self::new(EditorMode::Full, buffer, None, field_editor_style, cx)
}
pub fn auto_height(
max_lines: usize,
field_editor_style: Option<Arc<GetFieldEditorTheme>>,
@@ -1086,6 +1095,8 @@ impl Editor {
let blink_manager = cx.add_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
let soft_wrap_mode_override =
(mode == EditorMode::SingleLine).then(|| settings::SoftWrap::None);
let mut this = Self {
handle: cx.weak_handle(),
buffer: buffer.clone(),
@@ -1101,7 +1112,7 @@ impl Editor {
select_larger_syntax_node_stack: Vec::new(),
ime_transaction: Default::default(),
active_diagnostics: None,
soft_wrap_mode_override: None,
soft_wrap_mode_override,
get_field_editor_theme,
project,
focused: false,
@@ -1319,7 +1330,7 @@ impl Editor {
}
}
hide_hover(self, cx);
hide_hover(self, &HideHover, cx);
if old_cursor_position.to_display_point(&display_map).row()
!= new_cursor_position.to_display_point(&display_map).row()
@@ -1694,7 +1705,7 @@ impl Editor {
return;
}
if hide_hover(self, cx) {
if hide_hover(self, &HideHover, cx) {
return;
}
@@ -1735,7 +1746,7 @@ impl Editor {
for (selection, autoclose_region) in
self.selections_with_autoclose_regions(selections, &snapshot)
{
if let Some(language) = snapshot.language_at(selection.head()) {
if let Some(language) = snapshot.language_scope_at(selection.head()) {
// Determine if the inserted text matches the opening or closing
// bracket of any of this language's bracket pairs.
let mut bracket_pair = None;
@@ -1896,7 +1907,7 @@ impl Editor {
let end = selection.end;
let mut insert_extra_newline = false;
if let Some(language) = buffer.language_at(start) {
if let Some(language) = buffer.language_scope_at(start) {
let leading_whitespace_len = buffer
.reversed_chars_at(start)
.take_while(|c| c.is_whitespace() && *c != '\n')
@@ -2020,7 +2031,9 @@ impl Editor {
old_selections
.iter()
.map(|s| (s.start..s.end, text.clone())),
Some(AutoindentMode::EachLine),
Some(AutoindentMode::Block {
original_indent_columns: Vec::new(),
}),
cx,
);
anchors
@@ -4529,7 +4542,10 @@ impl Editor {
// TODO: Handle selections that cross excerpts
for selection in &mut selections {
let language = if let Some(language) = snapshot.language_at(selection.start) {
let start_column = snapshot.indent_size_for_line(selection.start.row).len;
let language = if let Some(language) =
snapshot.language_scope_at(Point::new(selection.start.row, start_column))
{
language
} else {
continue;
@@ -4799,7 +4815,7 @@ impl Editor {
if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() {
let (group_id, jump_to) = popover.activation_info();
if self.activate_diagnostics(group_id, cx) {
self.change_selections(Some(Autoscroll::center()), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
let mut new_selection = s.newest_anchor().clone();
new_selection.collapse_to(jump_to, SelectionGoal::None);
s.select_anchors(vec![new_selection.clone()]);
@@ -4845,7 +4861,7 @@ impl Editor {
if let Some((primary_range, group_id)) = group {
if self.activate_diagnostics(group_id, cx) {
self.change_selections(Some(Autoscroll::center()), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(vec![Selection {
id: selection.id,
start: primary_range.start,
@@ -4920,7 +4936,7 @@ impl Editor {
.dedup();
if let Some(hunk) = hunks.next() {
this.change_selections(Some(Autoscroll::center()), cx, |s| {
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
let row = hunk.start_display_row();
let point = DisplayPoint::new(row, 0);
s.select_display_ranges([point..point]);
@@ -5005,25 +5021,49 @@ impl Editor {
cx: &mut ViewContext<Workspace>,
) {
let pane = workspace.active_pane().clone();
for definition in definitions {
// If there is one definition, just open it directly
if let [definition] = definitions.as_slice() {
let range = definition
.target
.range
.to_offset(definition.target.buffer.read(cx));
let target_editor_handle = workspace.open_project_item(definition.target.buffer, cx);
let target_editor_handle =
workspace.open_project_item(definition.target.buffer.clone(), cx);
target_editor_handle.update(cx, |target_editor, cx| {
// 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 {
pane.update(cx, |pane, _| pane.disable_history());
}
target_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([range]);
});
pane.update(cx, |pane, _| pane.enable_history());
});
} else if !definitions.is_empty() {
let replica_id = editor_handle.read(cx).replica_id(cx);
let title = definitions
.iter()
.find(|definition| definition.origin.is_some())
.and_then(|definition| {
definition.origin.as_ref().map(|origin| {
let buffer = origin.buffer.read(cx);
format!(
"Definitions for {}",
buffer
.text_for_range(origin.range.clone())
.collect::<String>()
)
})
})
.unwrap_or("Definitions".to_owned());
let locations = definitions
.into_iter()
.map(|definition| definition.target)
.collect();
Self::open_locations_in_multibuffer(workspace, locations, replica_id, title, cx)
}
}
@@ -5044,64 +5084,87 @@ impl Editor {
let project = workspace.project().clone();
let references = project.update(cx, |project, cx| project.references(&buffer, head, cx));
Some(cx.spawn(|workspace, mut cx| async move {
let mut locations = references.await?;
let locations = references.await?;
if locations.is_empty() {
return Ok(());
}
locations.sort_by_key(|location| location.buffer.id());
let mut locations = locations.into_iter().peekable();
let mut ranges_to_highlight = Vec::new();
let excerpt_buffer = cx.add_model(|cx| {
let mut symbol_name = None;
let mut multibuffer = MultiBuffer::new(replica_id);
while let Some(location) = locations.next() {
let buffer = location.buffer.read(cx);
let mut ranges_for_buffer = Vec::new();
let range = location.range.to_offset(buffer);
ranges_for_buffer.push(range.clone());
if symbol_name.is_none() {
symbol_name = Some(buffer.text_for_range(range).collect::<String>());
}
while let Some(next_location) = locations.peek() {
if next_location.buffer == location.buffer {
ranges_for_buffer.push(next_location.range.to_offset(buffer));
locations.next();
} else {
break;
}
}
ranges_for_buffer.sort_by_key(|range| (range.start, Reverse(range.end)));
ranges_to_highlight.extend(multibuffer.push_excerpts_with_context_lines(
location.buffer.clone(),
ranges_for_buffer,
1,
cx,
));
}
multibuffer.with_title(format!("References to `{}`", symbol_name.unwrap()))
});
workspace.update(&mut cx, |workspace, cx| {
let editor =
cx.add_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx));
editor.update(cx, |editor, cx| {
editor.highlight_background::<Self>(
ranges_to_highlight,
|theme| theme.editor.highlighted_line_background,
cx,
);
});
workspace.add_item(Box::new(editor), cx);
let title = locations
.first()
.as_ref()
.map(|location| {
let buffer = location.buffer.read(cx);
format!(
"References to `{}`",
buffer
.text_for_range(location.range.clone())
.collect::<String>()
)
})
.unwrap();
Self::open_locations_in_multibuffer(workspace, locations, replica_id, title, cx);
});
Ok(())
}))
}
/// Opens a multibuffer with the given project locations in it
pub fn open_locations_in_multibuffer(
workspace: &mut Workspace,
mut locations: Vec<Location>,
replica_id: ReplicaId,
title: String,
cx: &mut ViewContext<Workspace>,
) {
// If there are multiple definitions, open them in a multibuffer
locations.sort_by_key(|location| location.buffer.id());
let mut locations = locations.into_iter().peekable();
let mut ranges_to_highlight = Vec::new();
let excerpt_buffer = cx.add_model(|cx| {
let mut multibuffer = MultiBuffer::new(replica_id);
while let Some(location) = locations.next() {
let buffer = location.buffer.read(cx);
let mut ranges_for_buffer = Vec::new();
let range = location.range.to_offset(buffer);
ranges_for_buffer.push(range.clone());
while let Some(next_location) = locations.peek() {
if next_location.buffer == location.buffer {
ranges_for_buffer.push(next_location.range.to_offset(buffer));
locations.next();
} else {
break;
}
}
ranges_for_buffer.sort_by_key(|range| (range.start, Reverse(range.end)));
ranges_to_highlight.extend(multibuffer.push_excerpts_with_context_lines(
location.buffer.clone(),
ranges_for_buffer,
1,
cx,
))
}
multibuffer.with_title(title)
});
let editor = cx.add_view(|cx| {
Editor::for_multibuffer(excerpt_buffer, Some(workspace.project().clone()), cx)
});
editor.update(cx, |editor, cx| {
editor.highlight_background::<Self>(
ranges_to_highlight,
|theme| theme.editor.highlighted_line_background,
cx,
);
});
workspace.add_item(Box::new(editor), cx);
}
pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
use language::ToOffset as _;
@@ -6172,7 +6235,7 @@ impl View for Editor {
cx.defer(move |cx| {
if let Some(editor) = handle.upgrade(cx) {
editor.update(cx, |editor, cx| {
hide_hover(editor, cx);
hide_hover(editor, &HideHover, cx);
hide_link_definition(editor, cx);
})
}
@@ -6221,7 +6284,7 @@ impl View for Editor {
self.buffer
.update(cx, |buffer, cx| buffer.remove_active_selections(cx));
self.hide_context_menu(cx);
hide_hover(self, cx);
hide_hover(self, &HideHover, cx);
cx.emit(Event::Blurred);
cx.notify();
}

View File

@@ -7,7 +7,7 @@ use crate::{
display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
git::{diff_hunk_to_display, DisplayDiffHunk},
hover_popover::{
HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
HideHover, HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
},
link_go_to_definition::{
GoToFetchedDefinition, GoToFetchedTypeDefinition, UpdateGoToDefinitionLink,
@@ -114,6 +114,7 @@ impl EditorElement {
fn attach_mouse_handlers(
view: &WeakViewHandle<Editor>,
position_map: &Arc<PositionMap>,
has_popovers: bool,
visible_bounds: RectF,
text_bounds: RectF,
gutter_bounds: RectF,
@@ -190,6 +191,11 @@ impl EditorElement {
}
}
})
.on_move_out(move |_, cx| {
if has_popovers {
cx.dispatch_action(HideHover);
}
})
.on_scroll({
let position_map = position_map.clone();
move |e, cx| {
@@ -1870,6 +1876,7 @@ impl Element for EditorElement {
Self::attach_mouse_handlers(
&self.view,
&layout.position_map,
layout.hover_popovers.is_some(),
visible_bounds,
text_bounds,
gutter_bounds,

View File

@@ -29,12 +29,16 @@ pub struct HoverAt {
pub point: Option<DisplayPoint>,
}
#[derive(Copy, Clone, PartialEq)]
pub struct HideHover;
actions!(editor, [Hover]);
impl_internal_actions!(editor, [HoverAt]);
impl_internal_actions!(editor, [HoverAt, HideHover]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(hover);
cx.add_action(hover_at);
cx.add_action(hide_hover);
}
/// Bindable action which uses the most recent selection head to trigger a hover
@@ -50,7 +54,7 @@ pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext<Edit
if let Some(point) = action.point {
show_hover(editor, point, false, cx);
} else {
hide_hover(editor, cx);
hide_hover(editor, &HideHover, cx);
}
}
}
@@ -58,7 +62,7 @@ pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext<Edit
/// Hides the type information popup.
/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
/// selections changed.
pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
pub fn hide_hover(editor: &mut Editor, _: &HideHover, cx: &mut ViewContext<Editor>) -> bool {
let did_hide = editor.hover_state.info_popover.take().is_some()
| editor.hover_state.diagnostic_popover.take().is_some();
@@ -67,6 +71,10 @@ pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
editor.clear_background_highlights::<HoverState>(cx);
if did_hide {
cx.notify();
}
did_hide
}
@@ -121,7 +129,7 @@ fn show_hover(
// Hover triggered from same location as last time. Don't show again.
return;
} else {
hide_hover(editor, cx);
hide_hover(editor, &HideHover, cx);
}
}
}
@@ -323,7 +331,7 @@ impl InfoPopover {
if let Some(language) = content
.language
.clone()
.and_then(|language| project.languages().get_language(&language))
.and_then(|language| project.languages().language_for_name(&language))
{
let runs = language
.highlight_text(&content.text.as_str().into(), 0..content.text.len());

View File

@@ -8,17 +8,17 @@ use anyhow::{anyhow, Context, Result};
use collections::HashSet;
use futures::future::try_join_all;
use futures::FutureExt;
use gpui::{
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use language::proto::serialize_anchor as serialize_text_anchor;
use language::{Bias, Buffer, File as _, OffsetRangeExt, Point, SelectionGoal};
use project::{File, FormatTrigger, Project, ProjectEntryId, ProjectPath};
use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
SelectionGoal,
};
use project::{FormatTrigger, Item as _, Project, ProjectPath};
use rpc::proto::{self, update_view};
use settings::Settings;
use smallvec::SmallVec;
use std::{
borrow::Cow,
cmp::{self, Ordering},
@@ -555,22 +555,10 @@ impl Item for Editor {
.boxed()
}
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
let buffer = self.buffer.read(cx).as_singleton()?;
let file = buffer.read(cx).file();
File::from_dyn(file).map(|file| ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path().clone(),
})
}
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
self.buffer
.read(cx)
.files(cx)
.into_iter()
.filter_map(|file| File::from_dyn(Some(file))?.project_entry_id(cx))
.collect()
.for_each_buffer(|buffer| f(buffer.id(), buffer.read(cx)));
}
fn is_singleton(&self, cx: &AppContext) -> bool {
@@ -607,7 +595,12 @@ impl Item for Editor {
}
fn can_save(&self, cx: &AppContext) -> bool {
!self.buffer().read(cx).is_singleton() || self.project_path(cx).is_some()
let buffer = &self.buffer().read(cx);
if let Some(buffer) = buffer.as_singleton() {
buffer.read(cx).project_path(cx).is_some()
} else {
true
}
}
fn save(
@@ -1101,7 +1094,7 @@ impl StatusItemView for CursorPosition {
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) {
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
self.update_position(editor, cx);
} else {
@@ -1165,9 +1158,11 @@ fn path_for_file<'a>(
mod tests {
use super::*;
use gpui::MutableAppContext;
use language::RopeFingerprint;
use std::{
path::{Path, PathBuf},
sync::Arc,
time::SystemTime,
};
#[gpui::test]
@@ -1197,7 +1192,7 @@ mod tests {
todo!()
}
fn mtime(&self) -> std::time::SystemTime {
fn mtime(&self) -> SystemTime {
todo!()
}
@@ -1216,7 +1211,7 @@ mod tests {
_: clock::Global,
_: project::LineEnding,
_: &mut MutableAppContext,
) -> gpui::Task<anyhow::Result<(clock::Global, String, std::time::SystemTime)>> {
) -> gpui::Task<anyhow::Result<(clock::Global, RopeFingerprint, SystemTime)>> {
todo!()
}

View File

@@ -352,6 +352,29 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<
start..end
}
pub fn split_display_range_by_lines(
map: &DisplaySnapshot,
range: Range<DisplayPoint>,
) -> Vec<Range<DisplayPoint>> {
let mut result = Vec::new();
let mut start = range.start;
// Loop over all the covered rows until the one containing the range end
for row in range.start.row()..range.end.row() {
let row_end_column = map.line_len(row);
let end = map.clip_point(DisplayPoint::new(row, row_end_column), Bias::Left);
if start != end {
result.push(start..end);
}
start = map.clip_point(DisplayPoint::new(row + 1, 0), Bias::Left);
}
// Add the final range from the start of the last end to the original range end.
result.push(start..range.end);
result
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -4,16 +4,16 @@ pub use anchor::{Anchor, AnchorRangeExt};
use anyhow::Result;
use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet};
use futures::{channel::mpsc, SinkExt};
use git::diff::DiffHunk;
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
pub use language::Completion;
use language::{
char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
DiagnosticEntry, File, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem,
Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _,
ToPointUtf16 as _, TransactionId, Unclipped,
DiagnosticEntry, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline,
OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _,
ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
};
use smallvec::SmallVec;
use std::{
borrow::Cow,
cell::{Ref, RefCell},
@@ -764,6 +764,63 @@ impl MultiBuffer {
None
}
pub fn stream_excerpts_with_context_lines(
&mut self,
excerpts: Vec<(ModelHandle<Buffer>, Vec<Range<text::Anchor>>)>,
context_line_count: u32,
cx: &mut ModelContext<Self>,
) -> (Task<()>, mpsc::Receiver<Range<Anchor>>) {
let (mut tx, rx) = mpsc::channel(256);
let task = cx.spawn(|this, mut cx| async move {
for (buffer, ranges) in excerpts {
let buffer_id = buffer.id();
let buffer_snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot());
let mut excerpt_ranges = Vec::new();
let mut range_counts = Vec::new();
cx.background()
.scoped(|scope| {
scope.spawn(async {
let (ranges, counts) =
build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count);
excerpt_ranges = ranges;
range_counts = counts;
});
})
.await;
let mut ranges = ranges.into_iter();
let mut range_counts = range_counts.into_iter();
for excerpt_ranges in excerpt_ranges.chunks(100) {
let excerpt_ids = this.update(&mut cx, |this, cx| {
this.push_excerpts(buffer.clone(), excerpt_ranges.iter().cloned(), cx)
});
for (excerpt_id, range_count) in
excerpt_ids.into_iter().zip(range_counts.by_ref())
{
for range in ranges.by_ref().take(range_count) {
let start = Anchor {
buffer_id: Some(buffer_id),
excerpt_id: excerpt_id.clone(),
text_anchor: range.start,
};
let end = Anchor {
buffer_id: Some(buffer_id),
excerpt_id: excerpt_id.clone(),
text_anchor: range.end,
};
if tx.send(start..end).await.is_err() {
break;
}
}
}
}
}
});
(task, rx)
}
pub fn push_excerpts<O>(
&mut self,
buffer: ModelHandle<Buffer>,
@@ -788,39 +845,8 @@ impl MultiBuffer {
{
let buffer_id = buffer.id();
let buffer_snapshot = buffer.read(cx).snapshot();
let max_point = buffer_snapshot.max_point();
let mut range_counts = Vec::new();
let mut excerpt_ranges = Vec::new();
let mut range_iter = ranges
.iter()
.map(|range| {
range.start.to_point(&buffer_snapshot)..range.end.to_point(&buffer_snapshot)
})
.peekable();
while let Some(range) = range_iter.next() {
let excerpt_start = Point::new(range.start.row.saturating_sub(context_line_count), 0);
let mut excerpt_end =
Point::new(range.end.row + 1 + context_line_count, 0).min(max_point);
let mut ranges_in_excerpt = 1;
while let Some(next_range) = range_iter.peek() {
if next_range.start.row <= excerpt_end.row + context_line_count {
excerpt_end =
Point::new(next_range.end.row + 1 + context_line_count, 0).min(max_point);
ranges_in_excerpt += 1;
range_iter.next();
} else {
break;
}
}
excerpt_ranges.push(ExcerptRange {
context: excerpt_start..excerpt_end,
primary: Some(range),
});
range_counts.push(ranges_in_excerpt);
}
let (excerpt_ranges, range_counts) =
build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count);
let excerpt_ids = self.push_excerpts(buffer, excerpt_ranges, cx);
@@ -1311,12 +1337,11 @@ impl MultiBuffer {
.and_then(|(buffer, offset)| buffer.read(cx).language_at(offset))
}
pub fn files<'a>(&'a self, cx: &'a AppContext) -> SmallVec<[&'a Arc<dyn File>; 2]> {
let buffers = self.buffers.borrow();
buffers
pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle<Buffer>)) {
self.buffers
.borrow()
.values()
.filter_map(|buffer| buffer.buffer.read(cx).file())
.collect()
.for_each(|state| f(&state.buffer))
}
pub fn title<'a>(&'a self, cx: &'a AppContext) -> Cow<'a, str> {
@@ -2666,6 +2691,11 @@ impl MultiBufferSnapshot {
.and_then(|(buffer, offset)| buffer.language_at(offset))
}
pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option<LanguageScope> {
self.point_to_buffer_offset(point)
.and_then(|(buffer, offset)| buffer.language_scope_at(offset))
}
pub fn is_dirty(&self) -> bool {
self.is_dirty
}
@@ -3605,9 +3635,51 @@ impl ToPointUtf16 for PointUtf16 {
}
}
fn build_excerpt_ranges<T>(
buffer: &BufferSnapshot,
ranges: &[Range<T>],
context_line_count: u32,
) -> (Vec<ExcerptRange<Point>>, Vec<usize>)
where
T: text::ToPoint,
{
let max_point = buffer.max_point();
let mut range_counts = Vec::new();
let mut excerpt_ranges = Vec::new();
let mut range_iter = ranges
.iter()
.map(|range| range.start.to_point(buffer)..range.end.to_point(buffer))
.peekable();
while let Some(range) = range_iter.next() {
let excerpt_start = Point::new(range.start.row.saturating_sub(context_line_count), 0);
let mut excerpt_end = Point::new(range.end.row + 1 + context_line_count, 0).min(max_point);
let mut ranges_in_excerpt = 1;
while let Some(next_range) = range_iter.peek() {
if next_range.start.row <= excerpt_end.row + context_line_count {
excerpt_end =
Point::new(next_range.end.row + 1 + context_line_count, 0).min(max_point);
ranges_in_excerpt += 1;
range_iter.next();
} else {
break;
}
}
excerpt_ranges.push(ExcerptRange {
context: excerpt_start..excerpt_end,
primary: Some(range),
});
range_counts.push(ranges_in_excerpt);
}
(excerpt_ranges, range_counts)
}
#[cfg(test)]
mod tests {
use super::*;
use futures::StreamExt;
use gpui::{MutableAppContext, TestAppContext};
use language::{Buffer, Rope};
use rand::prelude::*;
@@ -4012,6 +4084,44 @@ mod tests {
);
}
#[gpui::test]
async fn test_stream_excerpts_with_context_lines(cx: &mut TestAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx));
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
let (task, anchor_ranges) = multibuffer.update(cx, |multibuffer, cx| {
let snapshot = buffer.read(cx);
let ranges = vec![
snapshot.anchor_before(Point::new(3, 2))..snapshot.anchor_before(Point::new(4, 2)),
snapshot.anchor_before(Point::new(7, 1))..snapshot.anchor_before(Point::new(7, 3)),
snapshot.anchor_before(Point::new(15, 0))
..snapshot.anchor_before(Point::new(15, 0)),
];
multibuffer.stream_excerpts_with_context_lines(vec![(buffer.clone(), ranges)], 2, cx)
});
let anchor_ranges = anchor_ranges.collect::<Vec<_>>().await;
// Ensure task is finished when stream completes.
task.await;
let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
assert_eq!(
snapshot.text(),
"bbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\n\nnnn\nooo\nppp\nqqq\nrrr\n"
);
assert_eq!(
anchor_ranges
.iter()
.map(|range| range.to_point(&snapshot))
.collect::<Vec<_>>(),
vec![
Point::new(2, 2)..Point::new(3, 2),
Point::new(6, 1)..Point::new(6, 3),
Point::new(12, 0)..Point::new(12, 0)
]
);
}
#[gpui::test]
fn test_empty_multibuffer(cx: &mut MutableAppContext) {
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));

View File

@@ -17,7 +17,7 @@ use workspace::WorkspaceId;
use crate::{
display_map::{DisplaySnapshot, ToDisplayPoint},
hover_popover::hide_hover,
hover_popover::{hide_hover, HideHover},
persistence::DB,
Anchor, DisplayPoint, Editor, EditorMode, Event, MultiBufferSnapshot, ToPoint,
};
@@ -307,7 +307,7 @@ impl Editor {
) {
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
hide_hover(self, cx);
hide_hover(self, &HideHover, cx);
self.scroll_manager.set_scroll_position(
scroll_position,
&map,
@@ -323,7 +323,7 @@ impl Editor {
}
pub fn set_scroll_anchor(&mut self, scroll_anchor: ScrollAnchor, cx: &mut ViewContext<Self>) {
hide_hover(self, cx);
hide_hover(self, &HideHover, cx);
let top_row = scroll_anchor
.top_anchor
.to_point(&self.buffer().read(cx).snapshot(cx))
@@ -337,7 +337,7 @@ impl Editor {
scroll_anchor: ScrollAnchor,
cx: &mut ViewContext<Self>,
) {
hide_hover(self, cx);
hide_hover(self, &HideHover, cx);
let top_row = scroll_anchor
.top_anchor
.to_point(&self.buffer().read(cx).snapshot(cx))

View File

@@ -0,0 +1,34 @@
[package]
name = "feedback"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/feedback.rs"
[features]
test-support = []
[dependencies]
anyhow = "1.0.38"
client = { path = "../client" }
editor = { path = "../editor" }
language = { path = "../language" }
log = "0.4"
futures = "0.3"
gpui = { path = "../gpui" }
human_bytes = "0.4.1"
isahc = "1.7"
lazy_static = "1.4.0"
postage = { version = "0.4", features = ["futures-traits"] }
project = { path = "../project" }
search = { path = "../search" }
serde = { version = "1.0", features = ["derive", "rc"] }
settings = { path = "../settings" }
sysinfo = "0.27.1"
theme = { path = "../theme" }
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
urlencoding = "2.1.2"
util = { path = "../util" }
workspace = { path = "../workspace" }

View File

@@ -0,0 +1,65 @@
use std::sync::Arc;
pub mod feedback_editor;
mod system_specs;
use gpui::{actions, impl_actions, ClipboardItem, MutableAppContext, PromptLevel, ViewContext};
use serde::Deserialize;
use system_specs::SystemSpecs;
use workspace::{AppState, Workspace};
#[derive(Deserialize, Clone, PartialEq)]
pub struct OpenBrowser {
pub url: Arc<str>,
}
impl_actions!(zed, [OpenBrowser]);
actions!(
zed,
[CopySystemSpecsIntoClipboard, FileBugReport, RequestFeature]
);
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
let system_specs = SystemSpecs::new(&cx);
let system_specs_text = system_specs.to_string();
feedback_editor::init(system_specs, app_state, cx);
cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
let url = format!(
"https://github.com/zed-industries/feedback/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
urlencoding::encode(&system_specs_text)
);
cx.add_action(
move |_: &mut Workspace,
_: &CopySystemSpecsIntoClipboard,
cx: &mut ViewContext<Workspace>| {
cx.prompt(
PromptLevel::Info,
&format!("Copied into clipboard:\n\n{system_specs_text}"),
&["OK"],
);
let item = ClipboardItem::new(system_specs_text.clone());
cx.write_to_clipboard(item);
},
);
cx.add_action(
|_: &mut Workspace, _: &RequestFeature, cx: &mut ViewContext<Workspace>| {
let url = "https://github.com/zed-industries/feedback/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml";
cx.dispatch_action(OpenBrowser {
url: url.into(),
});
},
);
cx.add_action(
move |_: &mut Workspace, _: &FileBugReport, cx: &mut ViewContext<Workspace>| {
cx.dispatch_action(OpenBrowser {
url: url.clone().into(),
});
},
);
}

View File

@@ -0,0 +1,436 @@
use std::{
any::TypeId,
ops::{Range, RangeInclusive},
sync::Arc,
};
use anyhow::bail;
use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use editor::{Anchor, Editor};
use futures::AsyncReadExt;
use gpui::{
actions,
elements::{ChildView, Flex, Label, MouseEventHandler, ParentElement, Stack, Text},
serde_json, AnyViewHandle, AppContext, CursorStyle, Element, ElementBox, Entity, ModelHandle,
MouseButton, MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext,
ViewHandle, WeakViewHandle,
};
use isahc::Request;
use language::Buffer;
use postage::prelude::Stream;
use project::Project;
use serde::Serialize;
use settings::Settings;
use workspace::{
item::{Item, ItemHandle},
searchable::{SearchableItem, SearchableItemHandle},
AppState, StatusItemView, Workspace,
};
use crate::system_specs::SystemSpecs;
const FEEDBACK_CHAR_LIMIT: RangeInclusive<usize> = 10..=5000;
const FEEDBACK_PLACEHOLDER_TEXT: &str = "Thanks for spending time with Zed. Enter your feedback here as Markdown. Save the tab to submit your feedback.";
const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
"Feedback failed to submit, see error log for details.";
actions!(feedback, [SubmitFeedback, GiveFeedback, DeployFeedback]);
pub fn init(system_specs: SystemSpecs, app_state: Arc<AppState>, cx: &mut MutableAppContext) {
cx.add_action({
move |workspace: &mut Workspace, _: &GiveFeedback, cx: &mut ViewContext<Workspace>| {
FeedbackEditor::deploy(system_specs.clone(), workspace, app_state.clone(), cx);
}
});
}
pub struct FeedbackButton;
impl Entity for FeedbackButton {
type Event = ();
}
impl View for FeedbackButton {
fn ui_name() -> &'static str {
"FeedbackButton"
}
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
Stack::new()
.with_child(
MouseEventHandler::<Self>::new(0, cx, |state, cx| {
let theme = &cx.global::<Settings>().theme;
let theme = &theme.workspace.status_bar.feedback;
Text::new(
"Give Feedback".to_string(),
theme.style_for(state, true).clone(),
)
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(GiveFeedback))
.boxed(),
)
.boxed()
}
}
impl StatusItemView for FeedbackButton {
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
}
#[derive(Serialize)]
struct FeedbackRequestBody<'a> {
feedback_text: &'a str,
metrics_id: Option<Arc<str>>,
system_specs: SystemSpecs,
token: &'a str,
}
#[derive(Clone)]
struct FeedbackEditor {
system_specs: SystemSpecs,
editor: ViewHandle<Editor>,
project: ModelHandle<Project>,
}
impl FeedbackEditor {
fn new(
system_specs: SystemSpecs,
project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
cx: &mut ViewContext<Self>,
) -> Self {
let editor = cx.add_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
editor.set_vertical_scroll_margin(5, cx);
editor.set_placeholder_text(FEEDBACK_PLACEHOLDER_TEXT, cx);
editor
});
cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()))
.detach();
Self {
system_specs: system_specs.clone(),
editor,
project,
}
}
fn handle_save(
&mut self,
_: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
let feedback_char_count = self.editor.read(cx).text(cx).chars().count();
let error = if feedback_char_count < *FEEDBACK_CHAR_LIMIT.start() {
Some(format!(
"Feedback can't be shorter than {} characters.",
FEEDBACK_CHAR_LIMIT.start()
))
} else if feedback_char_count > *FEEDBACK_CHAR_LIMIT.end() {
Some(format!(
"Feedback can't be longer than {} characters.",
FEEDBACK_CHAR_LIMIT.end()
))
} else {
None
};
if let Some(error) = error {
cx.prompt(PromptLevel::Critical, &error, &["OK"]);
return Task::ready(Ok(()));
}
let mut answer = cx.prompt(
PromptLevel::Info,
"Ready to submit your feedback?",
&["Yes, Submit!", "No"],
);
let this = cx.handle();
let client = cx.global::<Arc<Client>>().clone();
let feedback_text = self.editor.read(cx).text(cx);
let specs = self.system_specs.clone();
cx.spawn(|_, mut cx| async move {
let answer = answer.recv().await;
if answer == Some(0) {
match FeedbackEditor::submit_feedback(&feedback_text, client, specs).await {
Ok(_) => {
cx.update(|cx| {
this.update(cx, |_, cx| {
cx.dispatch_action(workspace::CloseActiveItem);
})
});
}
Err(error) => {
log::error!("{}", error);
cx.update(|cx| {
this.update(cx, |_, cx| {
cx.prompt(
PromptLevel::Critical,
FEEDBACK_SUBMISSION_ERROR_TEXT,
&["OK"],
);
})
});
}
}
}
})
.detach();
Task::ready(Ok(()))
}
async fn submit_feedback(
feedback_text: &str,
zed_client: Arc<Client>,
system_specs: SystemSpecs,
) -> anyhow::Result<()> {
let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL);
let metrics_id = zed_client.metrics_id();
let http_client = zed_client.http_client();
let request = FeedbackRequestBody {
feedback_text: &feedback_text,
metrics_id,
system_specs,
token: ZED_SECRET_CLIENT_TOKEN,
};
let json_bytes = serde_json::to_vec(&request)?;
let request = Request::post(feedback_endpoint)
.header("content-type", "application/json")
.body(json_bytes.into())?;
let mut response = http_client.send(request).await?;
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
let response_status = response.status();
if !response_status.is_success() {
bail!("Feedback API failed with error: {}", response_status)
}
Ok(())
}
}
impl FeedbackEditor {
pub fn deploy(
system_specs: SystemSpecs,
workspace: &mut Workspace,
app_state: Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) {
workspace
.with_local_workspace(&app_state, cx, |workspace, cx| {
let project = workspace.project().clone();
let markdown_language = project.read(cx).languages().language_for_name("Markdown");
let buffer = project
.update(cx, |project, cx| {
project.create_buffer("", markdown_language, cx)
})
.expect("creating buffers on a local workspace always succeeds");
let feedback_editor =
cx.add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx));
workspace.add_item(Box::new(feedback_editor), cx);
})
.detach();
}
}
impl View for FeedbackEditor {
fn ui_name() -> &'static str {
"FeedbackEditor"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
ChildView::new(&self.editor, cx).boxed()
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.editor);
}
}
}
impl Entity for FeedbackEditor {
type Event = editor::Event;
}
impl Item for FeedbackEditor {
fn tab_content(&self, _: Option<usize>, style: &theme::Tab, _: &AppContext) -> ElementBox {
Flex::row()
.with_child(
Label::new("Feedback".to_string(), style.label.clone())
.aligned()
.contained()
.boxed(),
)
.boxed()
}
fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
self.editor.for_each_project_item(cx, f)
}
fn to_item_events(_: &Self::Event) -> Vec<workspace::item::ItemEvent> {
Vec::new()
}
fn is_singleton(&self, _: &AppContext) -> bool {
true
}
fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
fn can_save(&self, _: &AppContext) -> bool {
true
}
fn save(
&mut self,
project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
self.handle_save(project, cx)
}
fn save_as(
&mut self,
project: ModelHandle<Project>,
_: std::path::PathBuf,
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
self.handle_save(project, cx)
}
fn reload(
&mut self,
_: ModelHandle<Project>,
_: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
unreachable!("reload should not have been called")
}
fn clone_on_split(
&self,
_workspace_id: workspace::WorkspaceId,
cx: &mut ViewContext<Self>,
) -> Option<Self>
where
Self: Sized,
{
let buffer = self
.editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.expect("Feedback buffer is only ever singleton");
Some(Self::new(
self.system_specs.clone(),
self.project.clone(),
buffer.clone(),
cx,
))
}
fn serialized_item_kind() -> Option<&'static str> {
None
}
fn deserialize(
_: ModelHandle<Project>,
_: WeakViewHandle<Workspace>,
_: workspace::WorkspaceId,
_: workspace::ItemId,
_: &mut ViewContext<workspace::Pane>,
) -> Task<anyhow::Result<ViewHandle<Self>>> {
unreachable!()
}
fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(handle.clone()))
}
fn act_as_type(
&self,
type_id: TypeId,
self_handle: &ViewHandle<Self>,
_: &AppContext,
) -> Option<AnyViewHandle> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.into())
} else if type_id == TypeId::of::<Editor>() {
Some((&self.editor).into())
} else {
None
}
}
}
impl SearchableItem for FeedbackEditor {
type Match = Range<Anchor>;
fn to_search_event(event: &Self::Event) -> Option<workspace::searchable::SearchEvent> {
Editor::to_search_event(event)
}
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
self.editor
.update(cx, |editor, cx| editor.clear_matches(cx))
}
fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
self.editor
.update(cx, |editor, cx| editor.update_matches(matches, cx))
}
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
self.editor
.update(cx, |editor, cx| editor.query_suggestion(cx))
}
fn activate_match(
&mut self,
index: usize,
matches: Vec<Self::Match>,
cx: &mut ViewContext<Self>,
) {
self.editor
.update(cx, |editor, cx| editor.activate_match(index, matches, cx))
}
fn find_matches(
&mut self,
query: project::search::SearchQuery,
cx: &mut ViewContext<Self>,
) -> Task<Vec<Self::Match>> {
self.editor
.update(cx, |editor, cx| editor.find_matches(query, cx))
}
fn active_match_index(
&mut self,
matches: Vec<Self::Match>,
cx: &mut ViewContext<Self>,
) -> Option<usize> {
self.editor
.update(cx, |editor, cx| editor.active_match_index(matches, cx))
}
}

View File

@@ -0,0 +1,75 @@
use client::ZED_APP_VERSION;
use gpui::{AppContext, AppVersion};
use human_bytes::human_bytes;
use serde::Serialize;
use std::{env, fmt::Display};
use sysinfo::{System, SystemExt};
use util::channel::ReleaseChannel;
#[derive(Clone, Debug, Serialize)]
pub struct SystemSpecs {
#[serde(serialize_with = "serialize_app_version")]
app_version: Option<AppVersion>,
release_channel: &'static str,
os_name: &'static str,
os_version: Option<String>,
memory: u64,
architecture: &'static str,
}
impl SystemSpecs {
pub fn new(cx: &AppContext) -> Self {
let platform = cx.platform();
let app_version = ZED_APP_VERSION.or_else(|| platform.app_version().ok());
let release_channel = cx.global::<ReleaseChannel>().dev_name();
let os_name = platform.os_name();
let system = System::new_all();
let memory = system.total_memory();
let architecture = env::consts::ARCH;
let os_version = platform
.os_version()
.ok()
.map(|os_version| os_version.to_string());
SystemSpecs {
app_version,
release_channel,
os_name,
os_version,
memory,
architecture,
}
}
}
impl Display for SystemSpecs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let os_information = match &self.os_version {
Some(os_version) => format!("OS: {} {}", self.os_name, os_version),
None => format!("OS: {}", self.os_name),
};
let app_version_information = self
.app_version
.as_ref()
.map(|app_version| format!("Zed: v{} ({})", app_version, self.release_channel));
let system_specs = [
app_version_information,
Some(os_information),
Some(format!("Memory: {}", human_bytes(self.memory as f64))),
Some(format!("Architecture: {}", self.architecture)),
]
.into_iter()
.flatten()
.collect::<Vec<String>>()
.join("\n");
write!(f, "{system_specs}")
}
}
fn serialize_app_version<S>(version: &Option<AppVersion>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
version.map(|v| v.to_string()).serialize(serializer)
}

View File

@@ -2,6 +2,7 @@
name = "file_finder"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/file_finder.rs"

View File

@@ -2,6 +2,7 @@
name = "fs"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/fs.rs"

View File

@@ -3,6 +3,7 @@ name = "fsevent"
version = "2.0.2"
license = "MIT"
edition = "2021"
publish = false
[lib]
path = "src/fsevent.rs"

View File

@@ -2,6 +2,7 @@
name = "fuzzy"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/fuzzy.rs"

View File

@@ -2,6 +2,7 @@
name = "git"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/git.rs"

View File

@@ -2,6 +2,7 @@
name = "go_to_line"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/go_to_line.rs"

View File

@@ -4,6 +4,7 @@ edition = "2021"
name = "gpui"
version = "0.1.0"
description = "A GPU-accelerated UI framework"
publish = false
[lib]
path = "src/gpui.rs"
@@ -45,7 +46,6 @@ smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
time = { version = "0.3", features = ["serde", "serde-well-known"] }
tiny-skia = "0.5"
tree-sitter = "0.20"
usvg = "0.14"
waker-fn = "1.1.0"

View File

@@ -6,7 +6,6 @@ use std::{
fn main() {
generate_dispatch_bindings();
compile_context_predicate_parser();
compile_metal_shaders();
generate_shader_bindings();
}
@@ -30,17 +29,6 @@ fn generate_dispatch_bindings() {
.expect("couldn't write dispatch bindings");
}
fn compile_context_predicate_parser() {
let dir = PathBuf::from("./grammars/context-predicate/src");
let parser_c = dir.join("parser.c");
println!("cargo:rerun-if-changed={}", &parser_c.to_str().unwrap());
cc::Build::new()
.include(&dir)
.file(parser_c)
.compile("tree_sitter_context_predicate");
}
const SHADER_HEADER_PATH: &str = "./src/platform/mac/shaders/shaders.h";
fn compile_metal_shaders() {

View File

@@ -1,2 +0,0 @@
/node_modules
/build

View File

@@ -1,20 +0,0 @@
[package]
name = "tree-sitter-context-predicate"
description = "context-predicate grammar for the tree-sitter parsing library"
version = "0.0.1"
keywords = ["incremental", "parsing", "context-predicate"]
categories = ["parsing", "text-editors"]
repository = "https://github.com/tree-sitter/tree-sitter-javascript"
edition = "2021"
license = "MIT"
build = "bindings/rust/build.rs"
include = ["bindings/rust/*", "grammar.js", "queries/*", "src/*"]
[lib]
path = "bindings/rust/lib.rs"
[dependencies]
tree-sitter = "0.20"
[build-dependencies]
cc = "1.0"

View File

@@ -1,18 +0,0 @@
{
"targets": [
{
"target_name": "tree_sitter_context_predicate_binding",
"include_dirs": [
"<!(node -e \"require('nan')\")",
"src"
],
"sources": [
"src/parser.c",
"bindings/node/binding.cc"
],
"cflags_c": [
"-std=c99",
]
}
]
}

View File

@@ -1,30 +0,0 @@
#include "nan.h"
#include "tree_sitter/parser.h"
#include <node.h>
using namespace v8;
extern "C" TSLanguage *tree_sitter_context_predicate();
namespace {
NAN_METHOD(New) {}
void Init(Local<Object> exports, Local<Object> module) {
Local<FunctionTemplate> tpl = Nan::New<FunctionTemplate>(New);
tpl->SetClassName(Nan::New("Language").ToLocalChecked());
tpl->InstanceTemplate()->SetInternalFieldCount(1);
Local<Function> constructor = Nan::GetFunction(tpl).ToLocalChecked();
Local<Object> instance =
constructor->NewInstance(Nan::GetCurrentContext()).ToLocalChecked();
Nan::SetInternalFieldPointer(instance, 0, tree_sitter_context_predicate());
Nan::Set(instance, Nan::New("name").ToLocalChecked(),
Nan::New("context_predicate").ToLocalChecked());
Nan::Set(module, Nan::New("exports").ToLocalChecked(), instance);
}
NODE_MODULE(tree_sitter_context_predicate_binding, Init)
} // namespace

View File

@@ -1,19 +0,0 @@
try {
module.exports = require("../../build/Release/tree_sitter_context_predicate_binding");
} catch (error1) {
if (error1.code !== 'MODULE_NOT_FOUND') {
throw error1;
}
try {
module.exports = require("../../build/Debug/tree_sitter_context_predicate_binding");
} catch (error2) {
if (error2.code !== 'MODULE_NOT_FOUND') {
throw error2;
}
throw error1
}
}
try {
module.exports.nodeTypeInfo = require("../../src/node-types.json");
} catch (_) {}

View File

@@ -1,40 +0,0 @@
fn main() {
let src_dir = std::path::Path::new("src");
let mut c_config = cc::Build::new();
c_config.include(&src_dir);
c_config
.flag_if_supported("-Wno-unused-parameter")
.flag_if_supported("-Wno-unused-but-set-variable")
.flag_if_supported("-Wno-trigraphs");
let parser_path = src_dir.join("parser.c");
c_config.file(&parser_path);
// If your language uses an external scanner written in C,
// then include this block of code:
/*
let scanner_path = src_dir.join("scanner.c");
c_config.file(&scanner_path);
println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap());
*/
c_config.compile("parser");
println!("cargo:rerun-if-changed={}", parser_path.to_str().unwrap());
// If your language uses an external scanner written in C++,
// then include this block of code:
/*
let mut cpp_config = cc::Build::new();
cpp_config.cpp(true);
cpp_config.include(&src_dir);
cpp_config
.flag_if_supported("-Wno-unused-parameter")
.flag_if_supported("-Wno-unused-but-set-variable");
let scanner_path = src_dir.join("scanner.cc");
cpp_config.file(&scanner_path);
cpp_config.compile("scanner");
println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap());
*/
}

View File

@@ -1,52 +0,0 @@
//! This crate provides context_predicate language support for the [tree-sitter][] parsing library.
//!
//! Typically, you will use the [language][language func] function to add this language to a
//! tree-sitter [Parser][], and then use the parser to parse some code:
//!
//! ```
//! let code = "";
//! let mut parser = tree_sitter::Parser::new();
//! parser.set_language(tree_sitter_context_predicate::language()).expect("Error loading context_predicate grammar");
//! let tree = parser.parse(code, None).unwrap();
//! ```
//!
//! [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html
//! [language func]: fn.language.html
//! [Parser]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Parser.html
//! [tree-sitter]: https://tree-sitter.github.io/
use tree_sitter::Language;
extern "C" {
fn tree_sitter_context_predicate() -> Language;
}
/// Get the tree-sitter [Language][] for this grammar.
///
/// [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html
pub fn language() -> Language {
unsafe { tree_sitter_context_predicate() }
}
/// The content of the [`node-types.json`][] file for this grammar.
///
/// [`node-types.json`]: https://tree-sitter.github.io/tree-sitter/using-parsers#static-node-types
pub const NODE_TYPES: &'static str = include_str!("../../src/node-types.json");
// Uncomment these to include any queries that this grammar contains
// pub const HIGHLIGHTS_QUERY: &'static str = include_str!("../../queries/highlights.scm");
// pub const INJECTIONS_QUERY: &'static str = include_str!("../../queries/injections.scm");
// pub const LOCALS_QUERY: &'static str = include_str!("../../queries/locals.scm");
// pub const TAGS_QUERY: &'static str = include_str!("../../queries/tags.scm");
#[cfg(test)]
mod tests {
#[test]
fn test_can_load_grammar() {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(super::language())
.expect("Error loading context_predicate language");
}
}

View File

@@ -1,49 +0,0 @@
==================
Identifiers
==================
abc12
---
(source (identifier))
==================
Negation
==================
!abc
---
(source (not (identifier)))
==================
And/Or
==================
a || b && c && d
---
(source
(or
(identifier)
(and
(and (identifier) (identifier))
(identifier))))
==================
Expressions
==================
a && (b == c || d != e)
---
(source
(and
(identifier)
(parenthesized (or
(equal (identifier) (identifier))
(not_equal (identifier) (identifier))))))

View File

@@ -1,31 +0,0 @@
module.exports = grammar({
name: 'context_predicate',
rules: {
source: $ => $._expression,
_expression: $ => choice(
$.identifier,
$.not,
$.and,
$.or,
$.equal,
$.not_equal,
$.parenthesized,
),
identifier: $ => /[A-Za-z0-9_-]+/,
not: $ => prec(3, seq("!", field("expression", $._expression))),
and: $ => prec.left(2, seq(field("left", $._expression), "&&", field("right", $._expression))),
or: $ => prec.left(1, seq(field("left", $._expression), "||", field("right", $._expression))),
equal: $ => seq(field("left", $.identifier), "==", field("right", $.identifier)),
not_equal: $ => seq(field("left", $.identifier), "!=", field("right", $.identifier)),
parenthesized: $ => seq("(", field("expression", $._expression), ")"),
}
});

View File

@@ -1,44 +0,0 @@
{
"name": "tree-sitter-context-predicate",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "tree-sitter-context-predicate",
"dependencies": {
"nan": "^2.14.0"
},
"devDependencies": {
"tree-sitter-cli": "^0.19.5"
}
},
"node_modules/nan": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
},
"node_modules/tree-sitter-cli": {
"version": "0.19.5",
"resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.19.5.tgz",
"integrity": "sha512-kRzKrUAwpDN9AjA3b0tPBwT1hd8N2oQvvvHup2OEsX6mdsSMLmAvR+NSqK9fe05JrRbVvG8mbteNUQsxlMQohQ==",
"dev": true,
"hasInstallScript": true,
"bin": {
"tree-sitter": "cli.js"
}
}
},
"dependencies": {
"nan": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
},
"tree-sitter-cli": {
"version": "0.19.5",
"resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.19.5.tgz",
"integrity": "sha512-kRzKrUAwpDN9AjA3b0tPBwT1hd8N2oQvvvHup2OEsX6mdsSMLmAvR+NSqK9fe05JrRbVvG8mbteNUQsxlMQohQ==",
"dev": true
}
}
}

View File

@@ -1,10 +0,0 @@
{
"name": "tree-sitter-context-predicate",
"main": "bindings/node",
"devDependencies": {
"tree-sitter-cli": "^0.19.5"
},
"dependencies": {
"nan": "^2.14.0"
}
}

View File

@@ -1,208 +0,0 @@
{
"name": "context_predicate",
"rules": {
"source": {
"type": "SYMBOL",
"name": "_expression"
},
"_expression": {
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "identifier"
},
{
"type": "SYMBOL",
"name": "not"
},
{
"type": "SYMBOL",
"name": "and"
},
{
"type": "SYMBOL",
"name": "or"
},
{
"type": "SYMBOL",
"name": "equal"
},
{
"type": "SYMBOL",
"name": "not_equal"
},
{
"type": "SYMBOL",
"name": "parenthesized"
}
]
},
"identifier": {
"type": "PATTERN",
"value": "[A-Za-z0-9_-]+"
},
"not": {
"type": "PREC",
"value": 3,
"content": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "!"
},
{
"type": "FIELD",
"name": "expression",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
"and": {
"type": "PREC_LEFT",
"value": 2,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "STRING",
"value": "&&"
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
"or": {
"type": "PREC_LEFT",
"value": 1,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "STRING",
"value": "||"
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
"equal": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "identifier"
}
},
{
"type": "STRING",
"value": "=="
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "identifier"
}
}
]
},
"not_equal": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "identifier"
}
},
{
"type": "STRING",
"value": "!="
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "identifier"
}
}
]
},
"parenthesized": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "("
},
{
"type": "FIELD",
"name": "expression",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "STRING",
"value": ")"
}
]
}
},
"extras": [
{
"type": "PATTERN",
"value": "\\s"
}
],
"conflicts": [],
"precedences": [],
"externals": [],
"inline": [],
"supertypes": []
}

View File

@@ -1,353 +0,0 @@
[
{
"type": "and",
"named": true,
"fields": {
"left": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
},
"right": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
}
}
},
{
"type": "equal",
"named": true,
"fields": {
"left": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
}
]
},
"right": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
}
]
}
}
},
{
"type": "not",
"named": true,
"fields": {
"expression": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
}
}
},
{
"type": "not_equal",
"named": true,
"fields": {
"left": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
}
]
},
"right": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
}
]
}
}
},
{
"type": "or",
"named": true,
"fields": {
"left": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
},
"right": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
}
}
},
{
"type": "parenthesized",
"named": true,
"fields": {
"expression": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
}
}
},
{
"type": "source",
"named": true,
"fields": {},
"children": {
"multiple": false,
"required": true,
"types": [
{
"type": "and",
"named": true
},
{
"type": "equal",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "not",
"named": true
},
{
"type": "not_equal",
"named": true
},
{
"type": "or",
"named": true
},
{
"type": "parenthesized",
"named": true
}
]
}
},
{
"type": "!",
"named": false
},
{
"type": "!=",
"named": false
},
{
"type": "&&",
"named": false
},
{
"type": "(",
"named": false
},
{
"type": ")",
"named": false
},
{
"type": "==",
"named": false
},
{
"type": "identifier",
"named": true
},
{
"type": "||",
"named": false
}
]

View File

@@ -1,584 +0,0 @@
#include <tree_sitter/parser.h>
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
#endif
#define LANGUAGE_VERSION 13
#define STATE_COUNT 18
#define LARGE_STATE_COUNT 6
#define SYMBOL_COUNT 17
#define ALIAS_COUNT 0
#define TOKEN_COUNT 9
#define EXTERNAL_TOKEN_COUNT 0
#define FIELD_COUNT 3
#define MAX_ALIAS_SEQUENCE_LENGTH 3
#define PRODUCTION_ID_COUNT 3
enum {
sym_identifier = 1,
anon_sym_BANG = 2,
anon_sym_AMP_AMP = 3,
anon_sym_PIPE_PIPE = 4,
anon_sym_EQ_EQ = 5,
anon_sym_BANG_EQ = 6,
anon_sym_LPAREN = 7,
anon_sym_RPAREN = 8,
sym_source = 9,
sym__expression = 10,
sym_not = 11,
sym_and = 12,
sym_or = 13,
sym_equal = 14,
sym_not_equal = 15,
sym_parenthesized = 16,
};
static const char *const ts_symbol_names[] = {
[ts_builtin_sym_end] = "end",
[sym_identifier] = "identifier",
[anon_sym_BANG] = "!",
[anon_sym_AMP_AMP] = "&&",
[anon_sym_PIPE_PIPE] = "||",
[anon_sym_EQ_EQ] = "==",
[anon_sym_BANG_EQ] = "!=",
[anon_sym_LPAREN] = "(",
[anon_sym_RPAREN] = ")",
[sym_source] = "source",
[sym__expression] = "_expression",
[sym_not] = "not",
[sym_and] = "and",
[sym_or] = "or",
[sym_equal] = "equal",
[sym_not_equal] = "not_equal",
[sym_parenthesized] = "parenthesized",
};
static const TSSymbol ts_symbol_map[] = {
[ts_builtin_sym_end] = ts_builtin_sym_end,
[sym_identifier] = sym_identifier,
[anon_sym_BANG] = anon_sym_BANG,
[anon_sym_AMP_AMP] = anon_sym_AMP_AMP,
[anon_sym_PIPE_PIPE] = anon_sym_PIPE_PIPE,
[anon_sym_EQ_EQ] = anon_sym_EQ_EQ,
[anon_sym_BANG_EQ] = anon_sym_BANG_EQ,
[anon_sym_LPAREN] = anon_sym_LPAREN,
[anon_sym_RPAREN] = anon_sym_RPAREN,
[sym_source] = sym_source,
[sym__expression] = sym__expression,
[sym_not] = sym_not,
[sym_and] = sym_and,
[sym_or] = sym_or,
[sym_equal] = sym_equal,
[sym_not_equal] = sym_not_equal,
[sym_parenthesized] = sym_parenthesized,
};
static const TSSymbolMetadata ts_symbol_metadata[] = {
[ts_builtin_sym_end] =
{
.visible = false,
.named = true,
},
[sym_identifier] =
{
.visible = true,
.named = true,
},
[anon_sym_BANG] =
{
.visible = true,
.named = false,
},
[anon_sym_AMP_AMP] =
{
.visible = true,
.named = false,
},
[anon_sym_PIPE_PIPE] =
{
.visible = true,
.named = false,
},
[anon_sym_EQ_EQ] =
{
.visible = true,
.named = false,
},
[anon_sym_BANG_EQ] =
{
.visible = true,
.named = false,
},
[anon_sym_LPAREN] =
{
.visible = true,
.named = false,
},
[anon_sym_RPAREN] =
{
.visible = true,
.named = false,
},
[sym_source] =
{
.visible = true,
.named = true,
},
[sym__expression] =
{
.visible = false,
.named = true,
},
[sym_not] =
{
.visible = true,
.named = true,
},
[sym_and] =
{
.visible = true,
.named = true,
},
[sym_or] =
{
.visible = true,
.named = true,
},
[sym_equal] =
{
.visible = true,
.named = true,
},
[sym_not_equal] =
{
.visible = true,
.named = true,
},
[sym_parenthesized] =
{
.visible = true,
.named = true,
},
};
enum {
field_expression = 1,
field_left = 2,
field_right = 3,
};
static const char *const ts_field_names[] = {
[0] = NULL,
[field_expression] = "expression",
[field_left] = "left",
[field_right] = "right",
};
static const TSFieldMapSlice ts_field_map_slices[PRODUCTION_ID_COUNT] = {
[1] = {.index = 0, .length = 1},
[2] = {.index = 1, .length = 2},
};
static const TSFieldMapEntry ts_field_map_entries[] = {
[0] = {field_expression, 1},
[1] = {field_left, 0},
{field_right, 2},
};
static const TSSymbol ts_alias_sequences[PRODUCTION_ID_COUNT]
[MAX_ALIAS_SEQUENCE_LENGTH] = {
[0] = {0},
};
static const uint16_t ts_non_terminal_alias_map[] = {
0,
};
static bool ts_lex(TSLexer *lexer, TSStateId state) {
START_LEXER();
eof = lexer->eof(lexer);
switch (state) {
case 0:
if (eof)
ADVANCE(7);
if (lookahead == '!')
ADVANCE(10);
if (lookahead == '&')
ADVANCE(2);
if (lookahead == '(')
ADVANCE(15);
if (lookahead == ')')
ADVANCE(16);
if (lookahead == '=')
ADVANCE(4);
if (lookahead == '|')
ADVANCE(5);
if (lookahead == '\t' || lookahead == '\n' || lookahead == '\r' ||
lookahead == ' ')
SKIP(0)
if (lookahead == '-' || ('0' <= lookahead && lookahead <= '9') ||
('A' <= lookahead && lookahead <= 'Z') || lookahead == '_' ||
('a' <= lookahead && lookahead <= 'z'))
ADVANCE(8);
END_STATE();
case 1:
if (lookahead == '!')
ADVANCE(9);
if (lookahead == '(')
ADVANCE(15);
if (lookahead == '\t' || lookahead == '\n' || lookahead == '\r' ||
lookahead == ' ')
SKIP(1)
if (lookahead == '-' || ('0' <= lookahead && lookahead <= '9') ||
('A' <= lookahead && lookahead <= 'Z') || lookahead == '_' ||
('a' <= lookahead && lookahead <= 'z'))
ADVANCE(8);
END_STATE();
case 2:
if (lookahead == '&')
ADVANCE(11);
END_STATE();
case 3:
if (lookahead == '=')
ADVANCE(14);
END_STATE();
case 4:
if (lookahead == '=')
ADVANCE(13);
END_STATE();
case 5:
if (lookahead == '|')
ADVANCE(12);
END_STATE();
case 6:
if (eof)
ADVANCE(7);
if (lookahead == '!')
ADVANCE(3);
if (lookahead == '&')
ADVANCE(2);
if (lookahead == ')')
ADVANCE(16);
if (lookahead == '=')
ADVANCE(4);
if (lookahead == '|')
ADVANCE(5);
if (lookahead == '\t' || lookahead == '\n' || lookahead == '\r' ||
lookahead == ' ')
SKIP(6)
END_STATE();
case 7:
ACCEPT_TOKEN(ts_builtin_sym_end);
END_STATE();
case 8:
ACCEPT_TOKEN(sym_identifier);
if (lookahead == '-' || ('0' <= lookahead && lookahead <= '9') ||
('A' <= lookahead && lookahead <= 'Z') || lookahead == '_' ||
('a' <= lookahead && lookahead <= 'z'))
ADVANCE(8);
END_STATE();
case 9:
ACCEPT_TOKEN(anon_sym_BANG);
END_STATE();
case 10:
ACCEPT_TOKEN(anon_sym_BANG);
if (lookahead == '=')
ADVANCE(14);
END_STATE();
case 11:
ACCEPT_TOKEN(anon_sym_AMP_AMP);
END_STATE();
case 12:
ACCEPT_TOKEN(anon_sym_PIPE_PIPE);
END_STATE();
case 13:
ACCEPT_TOKEN(anon_sym_EQ_EQ);
END_STATE();
case 14:
ACCEPT_TOKEN(anon_sym_BANG_EQ);
END_STATE();
case 15:
ACCEPT_TOKEN(anon_sym_LPAREN);
END_STATE();
case 16:
ACCEPT_TOKEN(anon_sym_RPAREN);
END_STATE();
default:
return false;
}
}
static const TSLexMode ts_lex_modes[STATE_COUNT] = {
[0] = {.lex_state = 0}, [1] = {.lex_state = 1}, [2] = {.lex_state = 1},
[3] = {.lex_state = 1}, [4] = {.lex_state = 1}, [5] = {.lex_state = 1},
[6] = {.lex_state = 6}, [7] = {.lex_state = 0}, [8] = {.lex_state = 0},
[9] = {.lex_state = 0}, [10] = {.lex_state = 0}, [11] = {.lex_state = 0},
[12] = {.lex_state = 0}, [13] = {.lex_state = 0}, [14] = {.lex_state = 0},
[15] = {.lex_state = 0}, [16] = {.lex_state = 0}, [17] = {.lex_state = 0},
};
static const uint16_t ts_parse_table[LARGE_STATE_COUNT][SYMBOL_COUNT] = {
[0] =
{
[ts_builtin_sym_end] = ACTIONS(1),
[sym_identifier] = ACTIONS(1),
[anon_sym_BANG] = ACTIONS(1),
[anon_sym_AMP_AMP] = ACTIONS(1),
[anon_sym_PIPE_PIPE] = ACTIONS(1),
[anon_sym_EQ_EQ] = ACTIONS(1),
[anon_sym_BANG_EQ] = ACTIONS(1),
[anon_sym_LPAREN] = ACTIONS(1),
[anon_sym_RPAREN] = ACTIONS(1),
},
[1] =
{
[sym_source] = STATE(15),
[sym__expression] = STATE(13),
[sym_not] = STATE(13),
[sym_and] = STATE(13),
[sym_or] = STATE(13),
[sym_equal] = STATE(13),
[sym_not_equal] = STATE(13),
[sym_parenthesized] = STATE(13),
[sym_identifier] = ACTIONS(3),
[anon_sym_BANG] = ACTIONS(5),
[anon_sym_LPAREN] = ACTIONS(7),
},
[2] =
{
[sym__expression] = STATE(7),
[sym_not] = STATE(7),
[sym_and] = STATE(7),
[sym_or] = STATE(7),
[sym_equal] = STATE(7),
[sym_not_equal] = STATE(7),
[sym_parenthesized] = STATE(7),
[sym_identifier] = ACTIONS(3),
[anon_sym_BANG] = ACTIONS(5),
[anon_sym_LPAREN] = ACTIONS(7),
},
[3] =
{
[sym__expression] = STATE(14),
[sym_not] = STATE(14),
[sym_and] = STATE(14),
[sym_or] = STATE(14),
[sym_equal] = STATE(14),
[sym_not_equal] = STATE(14),
[sym_parenthesized] = STATE(14),
[sym_identifier] = ACTIONS(3),
[anon_sym_BANG] = ACTIONS(5),
[anon_sym_LPAREN] = ACTIONS(7),
},
[4] =
{
[sym__expression] = STATE(11),
[sym_not] = STATE(11),
[sym_and] = STATE(11),
[sym_or] = STATE(11),
[sym_equal] = STATE(11),
[sym_not_equal] = STATE(11),
[sym_parenthesized] = STATE(11),
[sym_identifier] = ACTIONS(3),
[anon_sym_BANG] = ACTIONS(5),
[anon_sym_LPAREN] = ACTIONS(7),
},
[5] =
{
[sym__expression] = STATE(12),
[sym_not] = STATE(12),
[sym_and] = STATE(12),
[sym_or] = STATE(12),
[sym_equal] = STATE(12),
[sym_not_equal] = STATE(12),
[sym_parenthesized] = STATE(12),
[sym_identifier] = ACTIONS(3),
[anon_sym_BANG] = ACTIONS(5),
[anon_sym_LPAREN] = ACTIONS(7),
},
};
static const uint16_t ts_small_parse_table[] = {
[0] = 3,
ACTIONS(11),
1,
anon_sym_EQ_EQ,
ACTIONS(13),
1,
anon_sym_BANG_EQ,
ACTIONS(9),
4,
ts_builtin_sym_end,
anon_sym_AMP_AMP,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[13] = 1,
ACTIONS(15),
4,
ts_builtin_sym_end,
anon_sym_AMP_AMP,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[20] = 1,
ACTIONS(17),
4,
ts_builtin_sym_end,
anon_sym_AMP_AMP,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[27] = 1,
ACTIONS(19),
4,
ts_builtin_sym_end,
anon_sym_AMP_AMP,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[34] = 1,
ACTIONS(21),
4,
ts_builtin_sym_end,
anon_sym_AMP_AMP,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[41] = 1,
ACTIONS(23),
4,
ts_builtin_sym_end,
anon_sym_AMP_AMP,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[48] = 2,
ACTIONS(27),
1,
anon_sym_AMP_AMP,
ACTIONS(25),
3,
ts_builtin_sym_end,
anon_sym_PIPE_PIPE,
anon_sym_RPAREN,
[57] = 3,
ACTIONS(27),
1,
anon_sym_AMP_AMP,
ACTIONS(29),
1,
ts_builtin_sym_end,
ACTIONS(31),
1,
anon_sym_PIPE_PIPE,
[67] = 3,
ACTIONS(27),
1,
anon_sym_AMP_AMP,
ACTIONS(31),
1,
anon_sym_PIPE_PIPE,
ACTIONS(33),
1,
anon_sym_RPAREN,
[77] = 1,
ACTIONS(35),
1,
ts_builtin_sym_end,
[81] = 1,
ACTIONS(37),
1,
sym_identifier,
[85] = 1,
ACTIONS(39),
1,
sym_identifier,
};
static const uint32_t ts_small_parse_table_map[] = {
[SMALL_STATE(6)] = 0, [SMALL_STATE(7)] = 13, [SMALL_STATE(8)] = 20,
[SMALL_STATE(9)] = 27, [SMALL_STATE(10)] = 34, [SMALL_STATE(11)] = 41,
[SMALL_STATE(12)] = 48, [SMALL_STATE(13)] = 57, [SMALL_STATE(14)] = 67,
[SMALL_STATE(15)] = 77, [SMALL_STATE(16)] = 81, [SMALL_STATE(17)] = 85,
};
static const TSParseActionEntry ts_parse_actions[] = {
[0] = {.entry = {.count = 0, .reusable = false}},
[1] = {.entry = {.count = 1, .reusable = false}},
RECOVER(),
[3] = {.entry = {.count = 1, .reusable = true}},
SHIFT(6),
[5] = {.entry = {.count = 1, .reusable = true}},
SHIFT(2),
[7] = {.entry = {.count = 1, .reusable = true}},
SHIFT(3),
[9] = {.entry = {.count = 1, .reusable = true}},
REDUCE(sym__expression, 1),
[11] = {.entry = {.count = 1, .reusable = true}},
SHIFT(16),
[13] = {.entry = {.count = 1, .reusable = true}},
SHIFT(17),
[15] = {.entry = {.count = 1, .reusable = true}},
REDUCE(sym_not, 2, .production_id = 1),
[17] = {.entry = {.count = 1, .reusable = true}},
REDUCE(sym_equal, 3, .production_id = 2),
[19] = {.entry = {.count = 1, .reusable = true}},
REDUCE(sym_not_equal, 3, .production_id = 2),
[21] = {.entry = {.count = 1, .reusable = true}},
REDUCE(sym_parenthesized, 3, .production_id = 1),
[23] = {.entry = {.count = 1, .reusable = true}},
REDUCE(sym_and, 3, .production_id = 2),
[25] = {.entry = {.count = 1, .reusable = true}},
REDUCE(sym_or, 3, .production_id = 2),
[27] = {.entry = {.count = 1, .reusable = true}},
SHIFT(4),
[29] = {.entry = {.count = 1, .reusable = true}},
REDUCE(sym_source, 1),
[31] = {.entry = {.count = 1, .reusable = true}},
SHIFT(5),
[33] = {.entry = {.count = 1, .reusable = true}},
SHIFT(10),
[35] = {.entry = {.count = 1, .reusable = true}},
ACCEPT_INPUT(),
[37] = {.entry = {.count = 1, .reusable = true}},
SHIFT(8),
[39] = {.entry = {.count = 1, .reusable = true}},
SHIFT(9),
};
#ifdef __cplusplus
extern "C" {
#endif
#ifdef _WIN32
#define extern __declspec(dllexport)
#endif
extern const TSLanguage *tree_sitter_context_predicate(void) {
static const TSLanguage language = {
.version = LANGUAGE_VERSION,
.symbol_count = SYMBOL_COUNT,
.alias_count = ALIAS_COUNT,
.token_count = TOKEN_COUNT,
.external_token_count = EXTERNAL_TOKEN_COUNT,
.state_count = STATE_COUNT,
.large_state_count = LARGE_STATE_COUNT,
.production_id_count = PRODUCTION_ID_COUNT,
.field_count = FIELD_COUNT,
.max_alias_sequence_length = MAX_ALIAS_SEQUENCE_LENGTH,
.parse_table = &ts_parse_table[0][0],
.small_parse_table = ts_small_parse_table,
.small_parse_table_map = ts_small_parse_table_map,
.parse_actions = ts_parse_actions,
.symbol_names = ts_symbol_names,
.field_names = ts_field_names,
.field_map_slices = ts_field_map_slices,
.field_map_entries = ts_field_map_entries,
.symbol_metadata = ts_symbol_metadata,
.public_symbol_map = ts_symbol_map,
.alias_map = ts_non_terminal_alias_map,
.alias_sequences = &ts_alias_sequences[0][0],
.lex_modes = ts_lex_modes,
.lex_fn = ts_lex,
};
return &language;
}
#ifdef __cplusplus
}
#endif

View File

@@ -1,223 +0,0 @@
#ifndef TREE_SITTER_PARSER_H_
#define TREE_SITTER_PARSER_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#define ts_builtin_sym_error ((TSSymbol)-1)
#define ts_builtin_sym_end 0
#define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024
typedef uint16_t TSStateId;
#ifndef TREE_SITTER_API_H_
typedef uint16_t TSSymbol;
typedef uint16_t TSFieldId;
typedef struct TSLanguage TSLanguage;
#endif
typedef struct {
TSFieldId field_id;
uint8_t child_index;
bool inherited;
} TSFieldMapEntry;
typedef struct {
uint16_t index;
uint16_t length;
} TSFieldMapSlice;
typedef struct {
bool visible;
bool named;
bool supertype;
} TSSymbolMetadata;
typedef struct TSLexer TSLexer;
struct TSLexer {
int32_t lookahead;
TSSymbol result_symbol;
void (*advance)(TSLexer *, bool);
void (*mark_end)(TSLexer *);
uint32_t (*get_column)(TSLexer *);
bool (*is_at_included_range_start)(const TSLexer *);
bool (*eof)(const TSLexer *);
};
typedef enum {
TSParseActionTypeShift,
TSParseActionTypeReduce,
TSParseActionTypeAccept,
TSParseActionTypeRecover,
} TSParseActionType;
typedef union {
struct {
uint8_t type;
TSStateId state;
bool extra;
bool repetition;
} shift;
struct {
uint8_t type;
uint8_t child_count;
TSSymbol symbol;
int16_t dynamic_precedence;
uint16_t production_id;
} reduce;
uint8_t type;
} TSParseAction;
typedef struct {
uint16_t lex_state;
uint16_t external_lex_state;
} TSLexMode;
typedef union {
TSParseAction action;
struct {
uint8_t count;
bool reusable;
} entry;
} TSParseActionEntry;
struct TSLanguage {
uint32_t version;
uint32_t symbol_count;
uint32_t alias_count;
uint32_t token_count;
uint32_t external_token_count;
uint32_t state_count;
uint32_t large_state_count;
uint32_t production_id_count;
uint32_t field_count;
uint16_t max_alias_sequence_length;
const uint16_t *parse_table;
const uint16_t *small_parse_table;
const uint32_t *small_parse_table_map;
const TSParseActionEntry *parse_actions;
const char * const *symbol_names;
const char * const *field_names;
const TSFieldMapSlice *field_map_slices;
const TSFieldMapEntry *field_map_entries;
const TSSymbolMetadata *symbol_metadata;
const TSSymbol *public_symbol_map;
const uint16_t *alias_map;
const TSSymbol *alias_sequences;
const TSLexMode *lex_modes;
bool (*lex_fn)(TSLexer *, TSStateId);
bool (*keyword_lex_fn)(TSLexer *, TSStateId);
TSSymbol keyword_capture_token;
struct {
const bool *states;
const TSSymbol *symbol_map;
void *(*create)(void);
void (*destroy)(void *);
bool (*scan)(void *, TSLexer *, const bool *symbol_whitelist);
unsigned (*serialize)(void *, char *);
void (*deserialize)(void *, const char *, unsigned);
} external_scanner;
};
/*
* Lexer Macros
*/
#define START_LEXER() \
bool result = false; \
bool skip = false; \
bool eof = false; \
int32_t lookahead; \
goto start; \
next_state: \
lexer->advance(lexer, skip); \
start: \
skip = false; \
lookahead = lexer->lookahead;
#define ADVANCE(state_value) \
{ \
state = state_value; \
goto next_state; \
}
#define SKIP(state_value) \
{ \
skip = true; \
state = state_value; \
goto next_state; \
}
#define ACCEPT_TOKEN(symbol_value) \
result = true; \
lexer->result_symbol = symbol_value; \
lexer->mark_end(lexer);
#define END_STATE() return result;
/*
* Parse Table Macros
*/
#define SMALL_STATE(id) id - LARGE_STATE_COUNT
#define STATE(id) id
#define ACTIONS(id) id
#define SHIFT(state_value) \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.state = state_value \
} \
}}
#define SHIFT_REPEAT(state_value) \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.state = state_value, \
.repetition = true \
} \
}}
#define SHIFT_EXTRA() \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.extra = true \
} \
}}
#define REDUCE(symbol_val, child_count_val, ...) \
{{ \
.reduce = { \
.type = TSParseActionTypeReduce, \
.symbol = symbol_val, \
.child_count = child_count_val, \
__VA_ARGS__ \
}, \
}}
#define RECOVER() \
{{ \
.type = TSParseActionTypeRecover \
}}
#define ACCEPT_INPUT() \
{{ \
.type = TSParseActionTypeAccept \
}}
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_PARSER_H_

View File

@@ -21,6 +21,7 @@ use std::{
use anyhow::{anyhow, Context, Result};
use lazy_static::lazy_static;
use parking_lot::Mutex;
use pathfinder_geometry::vector::Vector2F;
use postage::oneshot;
use smallvec::SmallVec;
use smol::prelude::*;
@@ -865,6 +866,14 @@ impl MutableAppContext {
}
}
pub fn is_topmost_window_for_position(&self, window_id: usize, position: Vector2F) -> bool {
self.presenters_and_platform_windows
.get(&window_id)
.map_or(false, |(_, window)| {
window.is_topmost_for_position(position)
})
}
pub fn window_ids(&self) -> impl Iterator<Item = usize> + '_ {
self.cx.windows.keys().cloned()
}
@@ -989,7 +998,7 @@ impl MutableAppContext {
window.toggle_full_screen();
}
fn prompt(
pub fn prompt(
&self,
window_id: usize,
level: PromptLevel,
@@ -1349,21 +1358,24 @@ impl MutableAppContext {
/// Return keystrokes that would dispatch the given action closest to the focused view, if there are any.
pub(crate) fn keystrokes_for_action(
&self,
&mut self,
window_id: usize,
dispatch_path: &[usize],
view_stack: &[usize],
action: &dyn Action,
) -> Option<SmallVec<[Keystroke; 2]>> {
for view_id in dispatch_path.iter().rev() {
self.keystroke_matcher.contexts.clear();
for view_id in view_stack.iter().rev() {
let view = self
.cx
.views
.get(&(window_id, *view_id))
.expect("view in responder chain does not exist");
let keymap_context = view.keymap_context(self.as_ref());
self.keystroke_matcher
.contexts
.push(view.keymap_context(self.as_ref()));
let keystrokes = self
.keystroke_matcher
.keystrokes_for_action(action, &keymap_context);
.keystrokes_for_action(action, &self.keystroke_matcher.contexts);
if keystrokes.is_some() {
return keystrokes;
}
@@ -6681,7 +6693,7 @@ mod tests {
view_3
});
// This keymap's only binding dispatches an action on view 2 because that view will have
// This binding only dispatches an action on view 2 because that view will have
// "a" and "b" in its context, but not "c".
cx.add_bindings(vec![Binding::new(
"a",
@@ -6691,16 +6703,31 @@ mod tests {
cx.add_bindings(vec![Binding::new("b", Action("b".to_string()), None)]);
// This binding only dispatches an action on views 2 and 3, because they have
// a parent view with a in its context
cx.add_bindings(vec![Binding::new(
"c",
Action("c".to_string()),
Some("b > c"),
)]);
// This binding only dispatches an action on view 2, because they have
// a parent view with a in its context
cx.add_bindings(vec![Binding::new(
"d",
Action("d".to_string()),
Some("a && !b > b"),
)]);
let actions = Rc::new(RefCell::new(Vec::new()));
cx.add_action({
let actions = actions.clone();
move |view: &mut View, action: &Action, cx| {
if action.0 == "a" {
actions.borrow_mut().push(format!("{} a", view.id));
} else {
actions
.borrow_mut()
.push(format!("{} {}", view.id, action.0));
actions
.borrow_mut()
.push(format!("{} {}", view.id, action.0));
if action.0 == "b" {
cx.propagate_action();
}
}
@@ -6714,14 +6741,20 @@ mod tests {
});
cx.dispatch_keystroke(window_id, &Keystroke::parse("a").unwrap());
assert_eq!(&*actions.borrow(), &["2 a"]);
actions.borrow_mut().clear();
cx.dispatch_keystroke(window_id, &Keystroke::parse("b").unwrap());
assert_eq!(&*actions.borrow(), &["3 b", "2 b", "1 b", "global b"]);
actions.borrow_mut().clear();
cx.dispatch_keystroke(window_id, &Keystroke::parse("c").unwrap());
assert_eq!(&*actions.borrow(), &["3 c"]);
actions.borrow_mut().clear();
cx.dispatch_keystroke(window_id, &Keystroke::parse("d").unwrap());
assert_eq!(&*actions.borrow(), &["2 d"]);
actions.borrow_mut().clear();
}
#[crate::test(self)]

View File

@@ -7,7 +7,7 @@ use crate::{
platform::CursorStyle,
scene::{
CursorRegion, HandlerSet, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseHover,
MouseMove, MouseScrollWheel, MouseUp, MouseUpOut,
MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
},
DebugContext, Element, ElementBox, EventContext, LayoutContext, MeasurementContext,
MouseButton, MouseRegion, MouseState, PaintContext, RenderContext, SizeConstraint, View,
@@ -82,6 +82,14 @@ impl<Tag> MouseEventHandler<Tag> {
self
}
pub fn on_move_out(
mut self,
handler: impl Fn(MouseMoveOut, &mut EventContext) + 'static,
) -> Self {
self.handlers = self.handlers.on_move_out(handler);
self
}
pub fn on_down(
mut self,
button: MouseButton,

View File

@@ -25,6 +25,7 @@ pub struct KeyPressed {
impl_actions!(gpui, [KeyPressed]);
pub struct KeymapMatcher {
pub contexts: Vec<KeymapContext>,
pending_views: HashMap<usize, KeymapContext>,
pending_keystrokes: Vec<Keystroke>,
keymap: Keymap,
@@ -33,6 +34,7 @@ pub struct KeymapMatcher {
impl KeymapMatcher {
pub fn new(keymap: Keymap) -> Self {
Self {
contexts: Vec::new(),
pending_views: Default::default(),
pending_keystrokes: Vec::new(),
keymap,
@@ -70,7 +72,7 @@ impl KeymapMatcher {
pub fn push_keystroke(
&mut self,
keystroke: Keystroke,
dispatch_path: Vec<(usize, KeymapContext)>,
mut dispatch_path: Vec<(usize, KeymapContext)>,
) -> MatchResult {
let mut any_pending = false;
let mut matched_bindings: Vec<(usize, Box<dyn Action>)> = Vec::new();
@@ -78,7 +80,11 @@ impl KeymapMatcher {
let first_keystroke = self.pending_keystrokes.is_empty();
self.pending_keystrokes.push(keystroke.clone());
for (view_id, context) in dispatch_path {
self.contexts.clear();
self.contexts
.extend(dispatch_path.iter_mut().map(|e| std::mem::take(&mut e.1)));
for (i, (view_id, _)) in dispatch_path.into_iter().enumerate() {
// Don't require pending view entry if there are no pending keystrokes
if !first_keystroke && !self.pending_views.contains_key(&view_id) {
continue;
@@ -87,14 +93,15 @@ impl KeymapMatcher {
// If there is a previous view context, invalidate that view if it
// has changed
if let Some(previous_view_context) = self.pending_views.remove(&view_id) {
if previous_view_context != context {
if previous_view_context != self.contexts[i] {
continue;
}
}
// Find the bindings which map the pending keystrokes and current context
for binding in self.keymap.bindings().iter().rev() {
match binding.match_keys_and_context(&self.pending_keystrokes, &context) {
match binding.match_keys_and_context(&self.pending_keystrokes, &self.contexts[i..])
{
BindingMatchResult::Complete(mut action) => {
// Swap in keystroke for special KeyPressed action
if action.name() == "KeyPressed" && action.namespace() == "gpui" {
@@ -105,7 +112,7 @@ impl KeymapMatcher {
matched_bindings.push((view_id, action))
}
BindingMatchResult::Partial => {
self.pending_views.insert(view_id, context.clone());
self.pending_views.insert(view_id, self.contexts[i].clone());
any_pending = true;
}
_ => {}
@@ -129,13 +136,13 @@ impl KeymapMatcher {
pub fn keystrokes_for_action(
&self,
action: &dyn Action,
context: &KeymapContext,
contexts: &[KeymapContext],
) -> Option<SmallVec<[Keystroke; 2]>> {
self.keymap
.bindings()
.iter()
.rev()
.find_map(|binding| binding.keystrokes_for_action(action, context))
.find_map(|binding| binding.keystrokes_for_action(action, contexts))
}
}
@@ -349,27 +356,70 @@ mod tests {
}
#[test]
fn test_context_predicate_eval() -> Result<()> {
let predicate = KeymapContextPredicate::parse("a && b || c == d")?;
fn test_context_predicate_eval() {
let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap();
let mut context = KeymapContext::default();
context.set.insert("a".into());
assert!(!predicate.eval(&context));
assert!(!predicate.eval(&[context]));
let mut context = KeymapContext::default();
context.set.insert("a".into());
context.set.insert("b".into());
assert!(predicate.eval(&context));
assert!(predicate.eval(&[context]));
context.set.remove("b");
let mut context = KeymapContext::default();
context.set.insert("a".into());
context.map.insert("c".into(), "x".into());
assert!(!predicate.eval(&context));
assert!(!predicate.eval(&[context]));
let mut context = KeymapContext::default();
context.set.insert("a".into());
context.map.insert("c".into(), "d".into());
assert!(predicate.eval(&context));
assert!(predicate.eval(&[context]));
let predicate = KeymapContextPredicate::parse("!a")?;
assert!(predicate.eval(&KeymapContext::default()));
let predicate = KeymapContextPredicate::parse("!a").unwrap();
assert!(predicate.eval(&[KeymapContext::default()]));
}
Ok(())
#[test]
fn test_context_child_predicate_eval() {
let predicate = KeymapContextPredicate::parse("a && b > c").unwrap();
let contexts = [
context_set(&["e", "f"]),
context_set(&["c", "d"]), // match this context
context_set(&["a", "b"]),
];
assert!(!predicate.eval(&contexts[0..]));
assert!(predicate.eval(&contexts[1..]));
assert!(!predicate.eval(&contexts[2..]));
let predicate = KeymapContextPredicate::parse("a && b > c && !d > e").unwrap();
let contexts = [
context_set(&["f"]),
context_set(&["e"]), // only match this context
context_set(&["c"]),
context_set(&["a", "b"]),
context_set(&["e"]),
context_set(&["c", "d"]),
context_set(&["a", "b"]),
];
assert!(!predicate.eval(&contexts[0..]));
assert!(predicate.eval(&contexts[1..]));
assert!(!predicate.eval(&contexts[2..]));
assert!(!predicate.eval(&contexts[3..]));
assert!(!predicate.eval(&contexts[4..]));
assert!(!predicate.eval(&contexts[5..]));
assert!(!predicate.eval(&contexts[6..]));
fn context_set(names: &[&str]) -> KeymapContext {
KeymapContext {
set: names.iter().copied().map(str::to_string).collect(),
..Default::default()
}
}
}
#[test]

View File

@@ -41,24 +41,24 @@ impl Binding {
})
}
fn match_context(&self, context: &KeymapContext) -> bool {
fn match_context(&self, contexts: &[KeymapContext]) -> bool {
self.context_predicate
.as_ref()
.map(|predicate| predicate.eval(context))
.map(|predicate| predicate.eval(contexts))
.unwrap_or(true)
}
pub fn match_keys_and_context(
&self,
pending_keystrokes: &Vec<Keystroke>,
context: &KeymapContext,
contexts: &[KeymapContext],
) -> BindingMatchResult {
if self
.keystrokes
.as_ref()
.map(|keystrokes| keystrokes.starts_with(&pending_keystrokes))
.unwrap_or(true)
&& self.match_context(context)
&& self.match_context(contexts)
{
// If the binding is completed, push it onto the matches list
if self
@@ -79,9 +79,9 @@ impl Binding {
pub fn keystrokes_for_action(
&self,
action: &dyn Action,
context: &KeymapContext,
contexts: &[KeymapContext],
) -> Option<SmallVec<[Keystroke; 2]>> {
if self.action.eq(action) && self.match_context(context) {
if self.action.eq(action) && self.match_context(contexts) {
self.keystrokes.clone()
} else {
None

View File

@@ -43,7 +43,7 @@ impl Keymap {
pub(crate) fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
for binding in bindings {
self.binding_indices_by_action_type
.entry(binding.action().type_id())
.entry(binding.action().as_any().type_id())
.or_default()
.push(self.bindings.len());
self.bindings.push(binding);

View File

@@ -1,11 +1,5 @@
use anyhow::anyhow;
use anyhow::{anyhow, Result};
use collections::{HashMap, HashSet};
use tree_sitter::{Language, Node, Parser};
extern "C" {
fn tree_sitter_context_predicate() -> Language;
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct KeymapContext {
@@ -29,80 +23,25 @@ pub enum KeymapContextPredicate {
Identifier(String),
Equal(String, String),
NotEqual(String, String),
Child(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
Not(Box<KeymapContextPredicate>),
And(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
Or(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
}
impl KeymapContextPredicate {
pub fn parse(source: &str) -> anyhow::Result<Self> {
let mut parser = Parser::new();
let language = unsafe { tree_sitter_context_predicate() };
parser.set_language(language).unwrap();
let source = source.as_bytes();
let tree = parser.parse(source, None).unwrap();
Self::from_node(tree.root_node(), source)
}
fn from_node(node: Node, source: &[u8]) -> anyhow::Result<Self> {
let parse_error = "error parsing context predicate";
let kind = node.kind();
match kind {
"source" => Self::from_node(node.child(0).ok_or_else(|| anyhow!(parse_error))?, source),
"identifier" => Ok(Self::Identifier(node.utf8_text(source)?.into())),
"not" => {
let child = Self::from_node(
node.child_by_field_name("expression")
.ok_or_else(|| anyhow!(parse_error))?,
source,
)?;
Ok(Self::Not(Box::new(child)))
}
"and" | "or" => {
let left = Box::new(Self::from_node(
node.child_by_field_name("left")
.ok_or_else(|| anyhow!(parse_error))?,
source,
)?);
let right = Box::new(Self::from_node(
node.child_by_field_name("right")
.ok_or_else(|| anyhow!(parse_error))?,
source,
)?);
if kind == "and" {
Ok(Self::And(left, right))
} else {
Ok(Self::Or(left, right))
}
}
"equal" | "not_equal" => {
let left = node
.child_by_field_name("left")
.ok_or_else(|| anyhow!(parse_error))?
.utf8_text(source)?
.into();
let right = node
.child_by_field_name("right")
.ok_or_else(|| anyhow!(parse_error))?
.utf8_text(source)?
.into();
if kind == "equal" {
Ok(Self::Equal(left, right))
} else {
Ok(Self::NotEqual(left, right))
}
}
"parenthesized" => Self::from_node(
node.child_by_field_name("expression")
.ok_or_else(|| anyhow!(parse_error))?,
source,
),
_ => Err(anyhow!(parse_error)),
pub fn parse(source: &str) -> Result<Self> {
let source = Self::skip_whitespace(source);
let (predicate, rest) = Self::parse_expr(source, 0)?;
if let Some(next) = rest.chars().next() {
Err(anyhow!("unexpected character {next:?}"))
} else {
Ok(predicate)
}
}
pub fn eval(&self, context: &KeymapContext) -> bool {
pub fn eval(&self, contexts: &[KeymapContext]) -> bool {
let Some(context) = contexts.first() else { return false };
match self {
Self::Identifier(name) => context.set.contains(name.as_str()),
Self::Equal(left, right) => context
@@ -115,9 +54,245 @@ impl KeymapContextPredicate {
.get(left)
.map(|value| value != right)
.unwrap_or(true),
Self::Not(pred) => !pred.eval(context),
Self::And(left, right) => left.eval(context) && right.eval(context),
Self::Or(left, right) => left.eval(context) || right.eval(context),
Self::Not(pred) => !pred.eval(contexts),
Self::Child(parent, child) => parent.eval(&contexts[1..]) && child.eval(contexts),
Self::And(left, right) => left.eval(contexts) && right.eval(contexts),
Self::Or(left, right) => left.eval(contexts) || right.eval(contexts),
}
}
fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
type Op =
fn(KeymapContextPredicate, KeymapContextPredicate) -> Result<KeymapContextPredicate>;
let (mut predicate, rest) = Self::parse_primary(source)?;
source = rest;
'parse: loop {
for (operator, precedence, constructor) in [
(">", PRECEDENCE_CHILD, Self::new_child as Op),
("&&", PRECEDENCE_AND, Self::new_and as Op),
("||", PRECEDENCE_OR, Self::new_or as Op),
("==", PRECEDENCE_EQ, Self::new_eq as Op),
("!=", PRECEDENCE_EQ, Self::new_neq as Op),
] {
if source.starts_with(operator) && precedence >= min_precedence {
source = Self::skip_whitespace(&source[operator.len()..]);
let (right, rest) = Self::parse_expr(source, precedence + 1)?;
predicate = constructor(predicate, right)?;
source = rest;
continue 'parse;
}
}
break;
}
Ok((predicate, source))
}
fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> {
let next = source
.chars()
.next()
.ok_or_else(|| anyhow!("unexpected eof"))?;
match next {
'(' => {
source = Self::skip_whitespace(&source[1..]);
let (predicate, rest) = Self::parse_expr(source, 0)?;
if rest.starts_with(')') {
source = Self::skip_whitespace(&rest[1..]);
Ok((predicate, source))
} else {
Err(anyhow!("expected a ')'"))
}
}
'!' => {
let source = Self::skip_whitespace(&source[1..]);
let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?;
Ok((KeymapContextPredicate::Not(Box::new(predicate)), source))
}
_ if next.is_alphanumeric() || next == '_' => {
let len = source
.find(|c: char| !(c.is_alphanumeric() || c == '_'))
.unwrap_or(source.len());
let (identifier, rest) = source.split_at(len);
source = Self::skip_whitespace(rest);
Ok((
KeymapContextPredicate::Identifier(identifier.into()),
source,
))
}
_ => Err(anyhow!("unexpected character {next:?}")),
}
}
fn skip_whitespace(source: &str) -> &str {
let len = source
.find(|c: char| !c.is_whitespace())
.unwrap_or(source.len());
&source[len..]
}
fn new_or(self, other: Self) -> Result<Self> {
Ok(Self::Or(Box::new(self), Box::new(other)))
}
fn new_and(self, other: Self) -> Result<Self> {
Ok(Self::And(Box::new(self), Box::new(other)))
}
fn new_child(self, other: Self) -> Result<Self> {
Ok(Self::Child(Box::new(self), Box::new(other)))
}
fn new_eq(self, other: Self) -> Result<Self> {
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
Ok(Self::Equal(left, right))
} else {
Err(anyhow!("operands must be identifiers"))
}
}
fn new_neq(self, other: Self) -> Result<Self> {
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
Ok(Self::NotEqual(left, right))
} else {
Err(anyhow!("operands must be identifiers"))
}
}
}
const PRECEDENCE_CHILD: u32 = 1;
const PRECEDENCE_OR: u32 = 2;
const PRECEDENCE_AND: u32 = 3;
const PRECEDENCE_EQ: u32 = 4;
const PRECEDENCE_NOT: u32 = 5;
#[cfg(test)]
mod tests {
use super::KeymapContextPredicate::{self, *};
#[test]
fn test_parse_identifiers() {
// Identifiers
assert_eq!(
KeymapContextPredicate::parse("abc12").unwrap(),
Identifier("abc12".into())
);
assert_eq!(
KeymapContextPredicate::parse("_1a").unwrap(),
Identifier("_1a".into())
);
}
#[test]
fn test_parse_negations() {
assert_eq!(
KeymapContextPredicate::parse("!abc").unwrap(),
Not(Box::new(Identifier("abc".into())))
);
assert_eq!(
KeymapContextPredicate::parse(" ! ! abc").unwrap(),
Not(Box::new(Not(Box::new(Identifier("abc".into())))))
);
}
#[test]
fn test_parse_equality_operators() {
assert_eq!(
KeymapContextPredicate::parse("a == b").unwrap(),
Equal("a".into(), "b".into())
);
assert_eq!(
KeymapContextPredicate::parse("c!=d").unwrap(),
NotEqual("c".into(), "d".into())
);
assert_eq!(
KeymapContextPredicate::parse("c == !d")
.unwrap_err()
.to_string(),
"operands must be identifiers"
);
}
#[test]
fn test_parse_boolean_operators() {
assert_eq!(
KeymapContextPredicate::parse("a || b").unwrap(),
Or(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into()))
)
);
assert_eq!(
KeymapContextPredicate::parse("a || !b && c").unwrap(),
Or(
Box::new(Identifier("a".into())),
Box::new(And(
Box::new(Not(Box::new(Identifier("b".into())))),
Box::new(Identifier("c".into()))
))
)
);
assert_eq!(
KeymapContextPredicate::parse("a && b || c&&d").unwrap(),
Or(
Box::new(And(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into()))
)),
Box::new(And(
Box::new(Identifier("c".into())),
Box::new(Identifier("d".into()))
))
)
);
assert_eq!(
KeymapContextPredicate::parse("a == b && c || d == e && f").unwrap(),
Or(
Box::new(And(
Box::new(Equal("a".into(), "b".into())),
Box::new(Identifier("c".into()))
)),
Box::new(And(
Box::new(Equal("d".into(), "e".into())),
Box::new(Identifier("f".into()))
))
)
);
assert_eq!(
KeymapContextPredicate::parse("a && b && c && d").unwrap(),
And(
Box::new(And(
Box::new(And(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into()))
)),
Box::new(Identifier("c".into())),
)),
Box::new(Identifier("d".into()))
),
);
}
#[test]
fn test_parse_parenthesized_expressions() {
assert_eq!(
KeymapContextPredicate::parse("a && (b == c || d != e)").unwrap(),
And(
Box::new(Identifier("a".into())),
Box::new(Or(
Box::new(Equal("b".into(), "c".into())),
Box::new(NotEqual("d".into(), "e".into())),
)),
),
);
assert_eq!(
KeymapContextPredicate::parse(" ( a || b ) ").unwrap(),
Or(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into())),
)
);
}
}

View File

@@ -145,6 +145,7 @@ pub trait Window {
fn present_scene(&mut self, scene: Scene);
fn appearance(&self) -> Appearance;
fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>);
fn is_topmost_for_position(&self, position: Vector2F) -> bool;
}
#[derive(Debug)]
@@ -179,7 +180,7 @@ impl Default for Appearance {
}
}
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum WindowKind {
Normal,
PopUp,

View File

@@ -178,6 +178,21 @@ impl MouseMovedEvent {
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct MouseExitedEvent {
pub position: Vector2F,
pub pressed_button: Option<MouseButton>,
pub modifiers: Modifiers,
}
impl Deref for MouseExitedEvent {
type Target = Modifiers;
fn deref(&self) -> &Self::Target {
&self.modifiers
}
}
#[derive(Clone, Debug)]
pub enum Event {
KeyDown(KeyDownEvent),
@@ -186,6 +201,7 @@ pub enum Event {
MouseDown(MouseButtonEvent),
MouseUp(MouseButtonEvent),
MouseMoved(MouseMovedEvent),
MouseExited(MouseExitedEvent),
ScrollWheel(ScrollWheelEvent),
}
@@ -197,6 +213,7 @@ impl Event {
Event::ModifiersChanged { .. } => None,
Event::MouseDown(event) | Event::MouseUp(event) => Some(event.position),
Event::MouseMoved(event) => Some(event.position),
Event::MouseExited(event) => Some(event.position),
Event::ScrollWheel(event) => Some(event.position),
}
}

View File

@@ -3,7 +3,7 @@ use crate::{
keymap_matcher::Keystroke,
platform::{Event, NavigationDirection},
KeyDownEvent, KeyUpEvent, Modifiers, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
MouseMovedEvent, ScrollDelta, ScrollWheelEvent, TouchPhase,
MouseExitedEvent, MouseMovedEvent, ScrollDelta, ScrollWheelEvent, TouchPhase,
};
use cocoa::{
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
@@ -125,6 +125,7 @@ impl Event {
button,
position: vec2f(
native_event.locationInWindow().x as f32,
// MacOS screen coordinates are relative to bottom left
window_height - native_event.locationInWindow().y as f32,
),
modifiers: read_modifiers(native_event),
@@ -150,6 +151,7 @@ impl Event {
button,
position: vec2f(
native_event.locationInWindow().x as f32,
// MacOS view coordinates are relative to bottom left
window_height - native_event.locationInWindow().y as f32,
),
modifiers: read_modifiers(native_event),
@@ -221,6 +223,16 @@ impl Event {
modifiers: read_modifiers(native_event),
})
}),
NSEventType::NSMouseExited => window_height.map(|window_height| {
Self::MouseExited(MouseExitedEvent {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
pressed_button: None,
modifiers: read_modifiers(native_event),
})
}),
_ => None,
}
}

View File

@@ -699,7 +699,9 @@ impl platform::Platform for MacPlatform {
unsafe {
let cursor: id = match style {
CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
CursorStyle::ResizeLeftRight => {
msg_send![class!(NSCursor), resizeLeftRightCursor]
}
CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
@@ -853,8 +855,8 @@ extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
(0..urls.count())
.into_iter()
.filter_map(|i| {
let path = urls.objectAtIndex(i);
match CStr::from_ptr(path.absoluteString().UTF8String() as *mut c_char).to_str() {
let url = urls.objectAtIndex(i);
match CStr::from_ptr(url.absoluteString().UTF8String() as *mut c_char).to_str() {
Ok(string) => Some(string.to_string()),
Err(err) => {
log::error!("error converting path to string: {}", err);

View File

@@ -258,6 +258,10 @@ impl platform::Window for StatusItem {
crate::Appearance::from_native(appearance)
}
}
fn is_topmost_for_position(&self, _: Vector2F) -> bool {
true
}
}
impl StatusItemState {

View File

@@ -17,9 +17,9 @@ use crate::{
use block::ConcreteBlock;
use cocoa::{
appkit::{
CGPoint, NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable,
NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior,
NSWindowStyleMask,
CGFloat, CGPoint, NSApplication, NSBackingStoreBuffered, NSScreen, NSView,
NSViewHeightSizable, NSViewWidthSizable, NSWindow, NSWindowButton,
NSWindowCollectionBehavior, NSWindowStyleMask,
},
base::{id, nil},
foundation::{
@@ -66,6 +66,8 @@ const NSNormalWindowLevel: NSInteger = 0;
#[allow(non_upper_case_globals)]
const NSPopUpWindowLevel: NSInteger = 101;
#[allow(non_upper_case_globals)]
const NSTrackingMouseEnteredAndExited: NSUInteger = 0x01;
#[allow(non_upper_case_globals)]
const NSTrackingMouseMoved: NSUInteger = 0x02;
#[allow(non_upper_case_globals)]
const NSTrackingActiveAlways: NSUInteger = 0x80;
@@ -170,6 +172,10 @@ unsafe fn build_classes() {
sel!(mouseMoved:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(mouseExited:),
handle_view_event as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(mouseDragged:),
handle_view_event as extern "C" fn(&Object, Sel, id),
@@ -252,6 +258,11 @@ unsafe fn build_classes() {
do_command_by_selector as extern "C" fn(&Object, Sel, Sel),
);
decl.add_method(
sel!(acceptsFirstMouse:),
accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL,
);
decl.register()
};
}
@@ -317,6 +328,7 @@ enum ImeState {
struct WindowState {
id: usize,
native_window: id,
kind: WindowKind,
event_callback: Option<Box<dyn FnMut(Event) -> bool>>,
activate_callback: Option<Box<dyn FnMut(bool)>>,
resize_callback: Option<Box<dyn FnMut()>>,
@@ -422,6 +434,7 @@ impl Window {
let window = Self(Rc::new(RefCell::new(WindowState {
id,
native_window,
kind: options.kind,
event_callback: None,
resize_callback: None,
should_close_callback: None,
@@ -469,16 +482,6 @@ impl Window {
native_window.setTitlebarAppearsTransparent_(YES);
}
let tracking_area: id = msg_send![class!(NSTrackingArea), alloc];
let _: () = msg_send![
tracking_area,
initWithRect: NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.))
options: NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect
owner: native_view
userInfo: nil
];
let _: () = msg_send![native_view, addTrackingArea: tracking_area.autorelease()];
native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable);
native_view.setWantsBestResolutionOpenGLSurface_(YES);
@@ -501,8 +504,25 @@ impl Window {
}
match options.kind {
WindowKind::Normal => native_window.setLevel_(NSNormalWindowLevel),
WindowKind::Normal => {
native_window.setLevel_(NSNormalWindowLevel);
native_window.setAcceptsMouseMovedEvents_(YES);
}
WindowKind::PopUp => {
// Use a tracking area to allow receiving MouseMoved events even when
// the window or application aren't active, which is often the case
// e.g. for notification windows.
let tracking_area: id = msg_send![class!(NSTrackingArea), alloc];
let _: () = msg_send![
tracking_area,
initWithRect: NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.))
options: NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect
owner: native_view
userInfo: nil
];
let _: () =
msg_send![native_view, addTrackingArea: tracking_area.autorelease()];
native_window.setLevel_(NSPopUpWindowLevel);
let _: () = msg_send![
native_window,
@@ -735,6 +755,37 @@ impl platform::Window for Window {
fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>) {
self.0.borrow_mut().appearance_changed_callback = Some(callback);
}
fn is_topmost_for_position(&self, position: Vector2F) -> bool {
let window_bounds = self.bounds();
let self_borrow = self.0.borrow();
let self_id = self_borrow.id;
unsafe {
let app = NSApplication::sharedApplication(nil);
// Convert back to bottom-left coordinates
let point = NSPoint::new(
position.x() as CGFloat,
(window_bounds.height() - position.y()) as CGFloat,
);
let screen_point: NSPoint =
msg_send![self_borrow.native_window, convertPointToScreen: point];
let window_number: NSInteger = msg_send![class!(NSWindow), windowNumberAtPoint:screen_point belowWindowWithWindowNumber:0];
let top_most_window: id = msg_send![app, windowWithWindowNumber: window_number];
let is_panel: BOOL = msg_send![top_most_window, isKindOfClass: PANEL_CLASS];
let is_window: BOOL = msg_send![top_most_window, isKindOfClass: WINDOW_CLASS];
if is_panel == YES || is_window == YES {
let topmost_window_id = get_window_state(&*top_most_window).borrow().id;
topmost_window_id == self_id
} else {
// Someone else's window is on top
false
}
}
}
}
impl WindowState {
@@ -873,11 +924,10 @@ extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) {
extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: bool) -> BOOL {
let window_state = unsafe { get_window_state(this) };
let mut window_state_borrow = window_state.as_ref().borrow_mut();
let event =
unsafe { Event::from_native(native_event, Some(window_state_borrow.content_size().y())) };
let window_height = window_state_borrow.content_size().y();
let event = unsafe { Event::from_native(native_event, Some(window_height)) };
if let Some(event) = event {
if key_equivalent {
@@ -902,6 +952,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
function_is_held = event.keystroke.function;
Some((event, None))
}
_ => return NO,
};
@@ -968,9 +1019,10 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
let window_state = unsafe { get_window_state(this) };
let weak_window_state = Rc::downgrade(&window_state);
let mut window_state_borrow = window_state.as_ref().borrow_mut();
let is_active = unsafe { window_state_borrow.native_window.isKeyWindow() == YES };
let event =
unsafe { Event::from_native(native_event, Some(window_state_borrow.content_size().y())) };
let window_height = window_state_borrow.content_size().y();
let event = unsafe { Event::from_native(native_event, Some(window_height)) };
if let Some(event) = event {
match &event {
Event::MouseMoved(
@@ -989,12 +1041,20 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
))
.detach();
}
Event::MouseMoved(_)
if !(is_active || window_state_borrow.kind == WindowKind::PopUp) =>
{
return
}
Event::MouseUp(MouseButtonEvent {
button: MouseButton::Left,
..
}) => {
window_state_borrow.synthetic_drag_counter += 1;
}
Event::ModifiersChanged(ModifiersChangedEvent { modifiers }) => {
// Only raise modifiers changed event when they have actually changed
if let Some(Event::ModifiersChanged(ModifiersChangedEvent {
@@ -1008,6 +1068,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
window_state_borrow.previous_modifiers_changed_event = Some(event.clone());
}
_ => {}
}
@@ -1404,6 +1465,18 @@ extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) {
}
}
extern "C" fn accepts_first_mouse(this: &Object, _: Sel, _: id) -> BOOL {
unsafe {
let state = get_window_state(this);
let state_borrow = state.as_ref().borrow();
return if state_borrow.kind == WindowKind::PopUp {
YES
} else {
NO
};
}
}
async fn synthetic_drag(
window_state: Weak<RefCell<WindowState>>,
drag_id: usize,

View File

@@ -332,6 +332,10 @@ impl super::Window for Window {
}
fn on_appearance_changed(&mut self, _: Box<dyn FnMut()>) {}
fn is_topmost_for_position(&self, _position: Vector2F) -> bool {
true
}
}
pub fn platform() -> Platform {

View File

@@ -8,7 +8,7 @@ use crate::{
platform::{CursorStyle, Event},
scene::{
CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover,
MouseMove, MouseScrollWheel, MouseUp, MouseUpOut, Scene,
MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, Scene,
},
text_layout::TextLayoutCache,
Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, Appearance,
@@ -156,6 +156,7 @@ impl Presenter {
self.cursor_regions = scene.cursor_regions();
self.mouse_regions = scene.mouse_regions();
// window.is_topmost for the mouse moved event's postion?
if cx.window_is_active(self.window_id) {
if let Some(event) = self.last_mouse_moved_event.clone() {
self.dispatch_event(event, true, cx);
@@ -245,8 +246,11 @@ impl Presenter {
// -> Also updates mouse-related state
match &event {
Event::KeyDown(e) => return cx.dispatch_key_down(self.window_id, e),
Event::KeyUp(e) => return cx.dispatch_key_up(self.window_id, e),
Event::ModifiersChanged(e) => return cx.dispatch_modifiers_changed(self.window_id, e),
Event::MouseDown(e) => {
// Click events are weird because they can be fired after a drag event.
// MDN says that browsers handle this by starting from 'the most
@@ -279,6 +283,7 @@ impl Presenter {
platform_event: e.clone(),
}));
}
Event::MouseUp(e) => {
// NOTE: The order of event pushes is important! MouseUp events MUST be fired
// before click events, and so the MouseUp events need to be pushed before
@@ -296,6 +301,7 @@ impl Presenter {
platform_event: e.clone(),
}));
}
Event::MouseMoved(
e @ MouseMovedEvent {
position,
@@ -310,7 +316,10 @@ impl Presenter {
break;
}
}
cx.platform().set_cursor_style(style_to_assign);
if cx.is_topmost_window_for_position(self.window_id, *position) {
cx.platform().set_cursor_style(style_to_assign);
}
if !event_reused {
if pressed_button.is_some() {
@@ -347,9 +356,28 @@ impl Presenter {
platform_event: e.clone(),
started: false,
}));
mouse_events.push(MouseEvent::MoveOut(MouseMoveOut {
region: Default::default(),
}));
self.last_mouse_moved_event = Some(event.clone());
}
Event::MouseExited(event) => {
// When the platform sends a MouseExited event, synthesize
// a MouseMoved event whose position is outside the window's
// bounds so that hover and cursor state can be updated.
return self.dispatch_event(
Event::MouseMoved(MouseMovedEvent {
position: event.position,
pressed_button: event.pressed_button,
modifiers: event.modifiers,
}),
event_reused,
cx,
);
}
Event::ScrollWheel(e) => mouse_events.push(MouseEvent::ScrollWheel(MouseScrollWheel {
region: Default::default(),
platform_event: e.clone(),
@@ -407,6 +435,7 @@ impl Presenter {
}
}
}
MouseEvent::Down(_) | MouseEvent::Up(_) => {
for (region, _) in self.mouse_regions.iter().rev() {
if region.bounds.contains_point(self.mouse_position) {
@@ -417,6 +446,7 @@ impl Presenter {
}
}
}
MouseEvent::Click(e) => {
// Only raise click events if the released button is the same as the one stored
if self
@@ -439,6 +469,7 @@ impl Presenter {
}
}
}
MouseEvent::Drag(_) => {
for (mouse_region, _) in self.mouse_regions.iter().rev() {
if self.clicked_region_ids.contains(&mouse_region.id()) {
@@ -447,7 +478,7 @@ impl Presenter {
}
}
MouseEvent::UpOut(_) | MouseEvent::DownOut(_) => {
MouseEvent::MoveOut(_) | MouseEvent::UpOut(_) | MouseEvent::DownOut(_) => {
for (mouse_region, _) in self.mouse_regions.iter().rev() {
// NOT contains
if !mouse_region.bounds.contains_point(self.mouse_position) {
@@ -455,6 +486,7 @@ impl Presenter {
}
}
}
_ => {
for (mouse_region, _) in self.mouse_regions.iter().rev() {
// Contains
@@ -573,7 +605,7 @@ pub struct LayoutContext<'a> {
impl<'a> LayoutContext<'a> {
pub(crate) fn keystrokes_for_action(
&self,
&mut self,
action: &dyn Action,
) -> Option<SmallVec<[Keystroke; 2]>> {
self.app

View File

@@ -209,6 +209,7 @@ impl EventDispatcher {
break;
}
}
cx.platform().set_cursor_style(style_to_assign);
if !event_reused {

View File

@@ -21,6 +21,11 @@ impl Deref for MouseMove {
}
}
#[derive(Debug, Default, Clone)]
pub struct MouseMoveOut {
pub region: RectF,
}
#[derive(Debug, Default, Clone)]
pub struct MouseDrag {
pub region: RectF,
@@ -138,6 +143,7 @@ impl Deref for MouseScrollWheel {
#[derive(Debug, Clone)]
pub enum MouseEvent {
Move(MouseMove),
MoveOut(MouseMoveOut),
Drag(MouseDrag),
Hover(MouseHover),
Down(MouseDown),
@@ -152,6 +158,7 @@ impl MouseEvent {
pub fn set_region(&mut self, region: RectF) {
match self {
MouseEvent::Move(r) => r.region = region,
MouseEvent::MoveOut(r) => r.region = region,
MouseEvent::Drag(r) => r.region = region,
MouseEvent::Hover(r) => r.region = region,
MouseEvent::Down(r) => r.region = region,
@@ -168,6 +175,7 @@ impl MouseEvent {
pub fn is_capturable(&self) -> bool {
match self {
MouseEvent::Move(_) => true,
MouseEvent::MoveOut(_) => false,
MouseEvent::Drag(_) => true,
MouseEvent::Hover(_) => false,
MouseEvent::Down(_) => true,
@@ -185,6 +193,10 @@ impl MouseEvent {
discriminant(&MouseEvent::Move(Default::default()))
}
pub fn move_out_disc() -> Discriminant<MouseEvent> {
discriminant(&MouseEvent::MoveOut(Default::default()))
}
pub fn drag_disc() -> Discriminant<MouseEvent> {
discriminant(&MouseEvent::Drag(Default::default()))
}
@@ -220,6 +232,7 @@ impl MouseEvent {
pub fn handler_key(&self) -> HandlerKey {
match self {
MouseEvent::Move(_) => HandlerKey::new(Self::move_disc(), None),
MouseEvent::MoveOut(_) => HandlerKey::new(Self::move_out_disc(), None),
MouseEvent::Drag(e) => HandlerKey::new(Self::drag_disc(), e.pressed_button),
MouseEvent::Hover(_) => HandlerKey::new(Self::hover_disc(), None),
MouseEvent::Down(e) => HandlerKey::new(Self::down_disc(), Some(e.button)),

View File

@@ -12,7 +12,7 @@ use super::{
MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover, MouseMove, MouseUp,
MouseUpOut,
},
MouseScrollWheel,
MouseMoveOut, MouseScrollWheel,
};
#[derive(Clone)]
@@ -124,6 +124,14 @@ impl MouseRegion {
self
}
pub fn on_move_out(
mut self,
handler: impl Fn(MouseMoveOut, &mut EventContext) + 'static,
) -> Self {
self.handlers = self.handlers.on_move_out(handler);
self
}
pub fn on_scroll(
mut self,
handler: impl Fn(MouseScrollWheel, &mut EventContext) + 'static,
@@ -289,6 +297,23 @@ impl HandlerSet {
self
}
pub fn on_move_out(
mut self,
handler: impl Fn(MouseMoveOut, &mut EventContext) + 'static,
) -> Self {
self.insert(MouseEvent::move_out_disc(), None,
Rc::new(move |region_event, cx| {
if let MouseEvent::MoveOut(e) = region_event {
handler(e, cx);
} else {
panic!(
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::MoveOut, found {:?}",
region_event);
}
}));
self
}
pub fn on_down(
mut self,
button: MouseButton,

View File

@@ -2,6 +2,7 @@
name = "gpui_macros"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/gpui_macros.rs"

View File

@@ -2,6 +2,7 @@
name = "journal"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/journal.rs"

View File

@@ -2,6 +2,7 @@
name = "language"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/language.rs"
@@ -53,6 +54,7 @@ smol = "1.2"
tree-sitter = "0.20"
tree-sitter-rust = { version = "*", optional = true }
tree-sitter-typescript = { version = "*", optional = true }
unicase = "2.6"
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }
@@ -65,12 +67,13 @@ util = { path = "../util", features = ["test-support"] }
ctor = "0.1"
env_logger = "0.9"
rand = "0.8.3"
tree-sitter-embedded-template = "*"
tree-sitter-html = "*"
tree-sitter-javascript = "*"
tree-sitter-json = "*"
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
tree-sitter-rust = "*"
tree-sitter-python = "*"
tree-sitter-typescript = "*"
tree-sitter-ruby = "*"
tree-sitter-embedded-template = "*"
unindent = "0.1.7"

View File

@@ -9,7 +9,7 @@ use crate::{
syntax_map::{
SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxSnapshot, ToTreeSitterPoint,
},
CodeLabel, Outline,
CodeLabel, LanguageScope, Outline,
};
use anyhow::{anyhow, Result};
use clock::ReplicaId;
@@ -60,7 +60,7 @@ pub struct Buffer {
git_diff_status: GitDiffStatus,
file: Option<Arc<dyn File>>,
saved_version: clock::Global,
saved_version_fingerprint: String,
saved_version_fingerprint: RopeFingerprint,
saved_mtime: SystemTime,
transaction_depth: usize,
was_dirty_before_starting_transaction: Option<bool>,
@@ -221,7 +221,7 @@ pub trait File: Send + Sync {
version: clock::Global,
line_ending: LineEnding,
cx: &mut MutableAppContext,
) -> Task<Result<(clock::Global, String, SystemTime)>>;
) -> Task<Result<(clock::Global, RopeFingerprint, SystemTime)>>;
fn as_any(&self) -> &dyn Any;
@@ -238,7 +238,7 @@ pub trait LocalFile: File {
&self,
buffer_id: u64,
version: &clock::Global,
fingerprint: String,
fingerprint: RopeFingerprint,
line_ending: LineEnding,
mtime: SystemTime,
cx: &mut MutableAppContext,
@@ -282,6 +282,7 @@ struct AutoindentRequestEntry {
struct IndentSuggestion {
basis_row: u32,
delta: Ordering,
within_error: bool,
}
struct BufferChunkHighlights<'a> {
@@ -385,6 +386,13 @@ impl Buffer {
rpc::proto::LineEnding::from_i32(message.line_ending)
.ok_or_else(|| anyhow!("missing line_ending"))?,
));
this.saved_version = proto::deserialize_version(message.saved_version);
this.saved_version_fingerprint =
proto::deserialize_fingerprint(&message.saved_version_fingerprint)?;
this.saved_mtime = message
.saved_mtime
.ok_or_else(|| anyhow!("invalid saved_mtime"))?
.into();
Ok(this)
}
@@ -395,6 +403,9 @@ impl Buffer {
base_text: self.base_text().to_string(),
diff_base: self.diff_base.as_ref().map(|h| h.to_string()),
line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
saved_version: proto::serialize_version(&self.saved_version),
saved_version_fingerprint: proto::serialize_fingerprint(self.saved_version_fingerprint),
saved_mtime: Some(self.saved_mtime.into()),
}
}
@@ -521,7 +532,7 @@ impl Buffer {
pub fn save(
&mut self,
cx: &mut ModelContext<Self>,
) -> Task<Result<(clock::Global, String, SystemTime)>> {
) -> Task<Result<(clock::Global, RopeFingerprint, SystemTime)>> {
let file = if let Some(file) = self.file.as_ref() {
file
} else {
@@ -539,7 +550,7 @@ impl Buffer {
cx.spawn(|this, mut cx| async move {
let (version, fingerprint, mtime) = save.await?;
this.update(&mut cx, |this, cx| {
this.did_save(version.clone(), fingerprint.clone(), mtime, None, cx);
this.did_save(version.clone(), fingerprint, mtime, None, cx);
});
Ok((version, fingerprint, mtime))
})
@@ -549,6 +560,14 @@ impl Buffer {
&self.saved_version
}
pub fn saved_version_fingerprint(&self) -> RopeFingerprint {
self.saved_version_fingerprint
}
pub fn saved_mtime(&self) -> SystemTime {
self.saved_mtime
}
pub fn set_language(&mut self, language: Option<Arc<Language>>, cx: &mut ModelContext<Self>) {
self.syntax_map.lock().clear();
self.language = language;
@@ -564,7 +583,7 @@ impl Buffer {
pub fn did_save(
&mut self,
version: clock::Global,
fingerprint: String,
fingerprint: RopeFingerprint,
mtime: SystemTime,
new_file: Option<Arc<dyn File>>,
cx: &mut ModelContext<Self>,
@@ -613,7 +632,7 @@ impl Buffer {
pub fn did_reload(
&mut self,
version: clock::Global,
fingerprint: String,
fingerprint: RopeFingerprint,
line_ending: LineEnding,
mtime: SystemTime,
cx: &mut ModelContext<Self>,
@@ -626,7 +645,7 @@ impl Buffer {
file.buffer_reloaded(
self.remote_id(),
&self.saved_version,
self.saved_version_fingerprint.clone(),
self.saved_version_fingerprint,
self.line_ending(),
self.saved_mtime,
cx,
@@ -778,12 +797,39 @@ impl Buffer {
self.parsing_in_background
}
pub fn contains_unknown_injections(&self) -> bool {
self.syntax_map.lock().contains_unknown_injections()
}
#[cfg(test)]
pub fn set_sync_parse_timeout(&mut self, timeout: Duration) {
self.sync_parse_timeout = timeout;
}
fn reparse(&mut self, cx: &mut ModelContext<Self>) {
/// Called after an edit to synchronize the buffer's main parse tree with
/// the buffer's new underlying state.
///
/// Locks the syntax map and interpolates the edits since the last reparse
/// into the foreground syntax tree.
///
/// Then takes a stable snapshot of the syntax map before unlocking it.
/// The snapshot with the interpolated edits is sent to a background thread,
/// where we ask Tree-sitter to perform an incremental parse.
///
/// Meanwhile, in the foreground, we block the main thread for up to 1ms
/// waiting on the parse to complete. As soon as it completes, we proceed
/// synchronously, unless a 1ms timeout elapses.
///
/// If we time out waiting on the parse, we spawn a second task waiting
/// until the parse does complete and return with the interpolated tree still
/// in the foreground. When the background parse completes, call back into
/// the main thread and assign the foreground parse state.
///
/// If the buffer or grammar changed since the start of the background parse,
/// initiate an additional reparse recursively. To avoid concurrent parses
/// for the same buffer, we only initiate a new parse if we are not already
/// parsing in the background.
pub fn reparse(&mut self, cx: &mut ModelContext<Self>) {
if self.parsing_in_background {
return;
}
@@ -800,13 +846,13 @@ impl Buffer {
syntax_map.interpolate(&text);
let language_registry = syntax_map.language_registry();
let mut syntax_snapshot = syntax_map.snapshot();
let syntax_map_version = syntax_map.parsed_version();
drop(syntax_map);
let parse_task = cx.background().spawn({
let language = language.clone();
let language_registry = language_registry.clone();
async move {
syntax_snapshot.reparse(&syntax_map_version, &text, language_registry, language);
syntax_snapshot.reparse(&text, language_registry, language);
syntax_snapshot
}
});
@@ -816,7 +862,7 @@ impl Buffer {
.block_with_timeout(self.sync_parse_timeout, parse_task)
{
Ok(new_syntax_snapshot) => {
self.did_finish_parsing(new_syntax_snapshot, parsed_version, cx);
self.did_finish_parsing(new_syntax_snapshot, cx);
return;
}
Err(parse_task) => {
@@ -828,9 +874,15 @@ impl Buffer {
this.language.as_ref().map_or(true, |current_language| {
!Arc::ptr_eq(&language, current_language)
});
let parse_again =
this.version.changed_since(&parsed_version) || grammar_changed;
this.did_finish_parsing(new_syntax_map, parsed_version, cx);
let language_registry_changed = new_syntax_map
.contains_unknown_injections()
&& language_registry.map_or(false, |registry| {
registry.version() != new_syntax_map.language_registry_version()
});
let parse_again = language_registry_changed
|| grammar_changed
|| this.version.changed_since(&parsed_version);
this.did_finish_parsing(new_syntax_map, cx);
this.parsing_in_background = false;
if parse_again {
this.reparse(cx);
@@ -842,14 +894,9 @@ impl Buffer {
}
}
fn did_finish_parsing(
&mut self,
syntax_snapshot: SyntaxSnapshot,
version: clock::Global,
cx: &mut ModelContext<Self>,
) {
fn did_finish_parsing(&mut self, syntax_snapshot: SyntaxSnapshot, cx: &mut ModelContext<Self>) {
self.parse_count += 1;
self.syntax_map.lock().did_parse(syntax_snapshot, version);
self.syntax_map.lock().did_parse(syntax_snapshot);
self.request_autoindent(cx);
cx.emit(Event::Reparsed);
cx.notify();
@@ -919,7 +966,7 @@ impl Buffer {
// Build a map containing the suggested indentation for each of the edited lines
// with respect to the state of the buffer before these edits. This map is keyed
// by the rows for these lines in the current state of the buffer.
let mut old_suggestions = BTreeMap::<u32, IndentSize>::default();
let mut old_suggestions = BTreeMap::<u32, (IndentSize, bool)>::default();
let old_edited_ranges =
contiguous_ranges(old_to_new_rows.keys().copied(), max_rows_between_yields);
let mut language_indent_sizes = language_indent_sizes_by_new_row.iter().peekable();
@@ -945,14 +992,17 @@ impl Buffer {
let suggested_indent = old_to_new_rows
.get(&suggestion.basis_row)
.and_then(|from_row| old_suggestions.get(from_row).copied())
.and_then(|from_row| {
Some(old_suggestions.get(from_row).copied()?.0)
})
.unwrap_or_else(|| {
request
.before_edit
.indent_size_for_line(suggestion.basis_row)
})
.with_delta(suggestion.delta, language_indent_size);
old_suggestions.insert(new_row, suggested_indent);
old_suggestions
.insert(new_row, (suggested_indent, suggestion.within_error));
}
}
yield_now().await;
@@ -998,12 +1048,13 @@ impl Buffer {
snapshot.indent_size_for_line(suggestion.basis_row)
})
.with_delta(suggestion.delta, language_indent_size);
if old_suggestions
.get(&new_row)
.map_or(true, |old_indentation| {
if old_suggestions.get(&new_row).map_or(
true,
|(old_indentation, was_within_error)| {
suggested_indent != *old_indentation
})
{
&& (!suggestion.within_error || *was_within_error)
},
) {
indent_sizes.insert(new_row, suggested_indent);
}
}
@@ -1332,13 +1383,6 @@ impl Buffer {
let edit_id = edit_operation.local_timestamp();
if let Some((before_edit, mode)) = autoindent_request {
let (start_columns, is_block_mode) = match mode {
AutoindentMode::Block {
original_indent_columns: start_columns,
} => (start_columns, true),
AutoindentMode::EachLine => (Default::default(), false),
};
let mut delta = 0isize;
let entries = edits
.into_iter()
@@ -1352,7 +1396,7 @@ impl Buffer {
let mut range_of_insertion_to_indent = 0..new_text_len;
let mut first_line_is_new = false;
let mut start_column = None;
let mut original_indent_column = None;
// When inserting an entire line at the beginning of an existing line,
// treat the insertion as new.
@@ -1364,14 +1408,23 @@ impl Buffer {
// When inserting text starting with a newline, avoid auto-indenting the
// previous line.
if new_text[range_of_insertion_to_indent.clone()].starts_with('\n') {
if new_text.starts_with('\n') {
range_of_insertion_to_indent.start += 1;
first_line_is_new = true;
}
// Avoid auto-indenting after the insertion.
if is_block_mode {
start_column = start_columns.get(ix).copied();
if let AutoindentMode::Block {
original_indent_columns,
} = &mode
{
original_indent_column =
Some(original_indent_columns.get(ix).copied().unwrap_or_else(|| {
indent_size_for_text(
new_text[range_of_insertion_to_indent.clone()].chars(),
)
.len
}));
if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') {
range_of_insertion_to_indent.end -= 1;
}
@@ -1379,7 +1432,7 @@ impl Buffer {
AutoindentRequestEntry {
first_line_is_new,
original_indent_column: start_column,
original_indent_column,
indent_size: before_edit.language_indent_size_at(range.start, cx),
range: self.anchor_before(new_start + range_of_insertion_to_indent.start)
..self.anchor_after(new_start + range_of_insertion_to_indent.end),
@@ -1390,7 +1443,7 @@ impl Buffer {
self.autoindent_requests.push(Arc::new(AutoindentRequest {
before_edit,
entries,
is_block_mode,
is_block_mode: matches!(mode, AutoindentMode::Block { .. }),
}));
}
@@ -1641,6 +1694,16 @@ impl Buffer {
#[cfg(any(test, feature = "test-support"))]
impl Buffer {
pub fn edit_via_marked_text(
&mut self,
marked_string: &str,
autoindent_mode: Option<AutoindentMode>,
cx: &mut ModelContext<Self>,
) {
let edits = self.edits_for_marked_text(marked_string);
self.edit(edits, autoindent_mode, cx);
}
pub fn set_group_interval(&mut self, group_interval: Duration) {
self.text.set_group_interval(group_interval);
}
@@ -1759,7 +1822,7 @@ impl BufferSnapshot {
let start = Point::new(prev_non_blank_row.unwrap_or(row_range.start), 0);
let end = Point::new(row_range.end, 0);
let range = (start..end).to_offset(&self.text);
let mut matches = self.syntax.matches(range, &self.text, |grammar| {
let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
Some(&grammar.indents_config.as_ref()?.query)
});
let indent_configs = matches
@@ -1805,6 +1868,30 @@ impl BufferSnapshot {
}
}
let mut error_ranges = Vec::<Range<Point>>::new();
let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
Some(&grammar.error_query)
});
while let Some(mat) = matches.peek() {
let node = mat.captures[0].node;
let start = Point::from_ts_point(node.start_position());
let end = Point::from_ts_point(node.end_position());
let range = start..end;
let ix = match error_ranges.binary_search_by_key(&range.start, |r| r.start) {
Ok(ix) | Err(ix) => ix,
};
let mut end_ix = ix;
while let Some(existing_range) = error_ranges.get(end_ix) {
if existing_range.end < end {
end_ix += 1;
} else {
break;
}
}
error_ranges.splice(ix..end_ix, [range]);
matches.advance();
}
outdent_positions.sort();
for outdent_position in outdent_positions {
// find the innermost indent range containing this outdent_position
@@ -1882,33 +1969,42 @@ impl BufferSnapshot {
}
}
let within_error = error_ranges
.iter()
.any(|e| e.start.row < row && e.end > row_start);
let suggestion = if outdent_to_row == prev_row
|| (outdent_from_prev_row && indent_from_prev_row)
{
Some(IndentSuggestion {
basis_row: prev_row,
delta: Ordering::Equal,
within_error,
})
} else if indent_from_prev_row {
Some(IndentSuggestion {
basis_row: prev_row,
delta: Ordering::Greater,
within_error,
})
} else if outdent_to_row < prev_row {
Some(IndentSuggestion {
basis_row: outdent_to_row,
delta: Ordering::Equal,
within_error,
})
} else if outdent_from_prev_row {
Some(IndentSuggestion {
basis_row: prev_row,
delta: Ordering::Less,
within_error,
})
} else if config.auto_indent_using_last_non_empty_line || !self.is_line_blank(prev_row)
{
Some(IndentSuggestion {
basis_row: prev_row,
delta: Ordering::Equal,
within_error,
})
} else {
None
@@ -1995,6 +2091,27 @@ impl BufferSnapshot {
.or(self.language.as_ref())
}
pub fn language_scope_at<D: ToOffset>(&self, position: D) -> Option<LanguageScope> {
let offset = position.to_offset(self);
if let Some(layer_info) = self
.syntax
.layers_for_range(offset..offset, &self.text)
.filter(|l| l.node.end_byte() > offset)
.last()
{
Some(LanguageScope {
language: layer_info.language.clone(),
override_id: layer_info.override_id(offset, &self.text),
})
} else {
self.language.clone().map(|language| LanguageScope {
language,
override_id: None,
})
}
}
pub fn surrounding_word<T: ToOffset>(&self, start: T) -> (Range<usize>, Option<CharKind>) {
let mut start = start.to_offset(self);
let mut end = start;
@@ -2149,8 +2266,6 @@ impl BufferSnapshot {
continue;
}
// TODO - move later, after processing captures
let mut text = String::new();
let mut name_ranges = Vec::new();
let mut highlight_ranges = Vec::new();
@@ -2164,7 +2279,13 @@ impl BufferSnapshot {
continue;
}
let range = capture.node.start_byte()..capture.node.end_byte();
let mut range = capture.node.start_byte()..capture.node.end_byte();
let start = capture.node.start_position();
if capture.node.end_position().row > start.row {
range.end =
range.start + self.line_len(start.row as u32) as usize - start.column;
}
if !text.is_empty() {
text.push(' ');
}
@@ -2397,7 +2518,7 @@ impl BufferSnapshot {
}
}
pub fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize {
fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize {
indent_size_for_text(text.chars_at(Point::new(row, 0)))
}

View File

@@ -51,7 +51,7 @@ fn test_line_endings(cx: &mut gpui::MutableAppContext) {
#[gpui::test]
fn test_select_language() {
let registry = LanguageRegistry::test();
let registry = Arc::new(LanguageRegistry::test());
registry.add(Arc::new(Language::new(
LanguageConfig {
name: "Rust".into(),
@@ -71,27 +71,33 @@ fn test_select_language() {
// matching file extension
assert_eq!(
registry.select_language("zed/lib.rs").map(|l| l.name()),
registry.language_for_path("zed/lib.rs").map(|l| l.name()),
Some("Rust".into())
);
assert_eq!(
registry.select_language("zed/lib.mk").map(|l| l.name()),
registry.language_for_path("zed/lib.mk").map(|l| l.name()),
Some("Make".into())
);
// matching filename
assert_eq!(
registry.select_language("zed/Makefile").map(|l| l.name()),
registry.language_for_path("zed/Makefile").map(|l| l.name()),
Some("Make".into())
);
// matching suffix that is not the full file extension or filename
assert_eq!(registry.select_language("zed/cars").map(|l| l.name()), None);
assert_eq!(
registry.select_language("zed/a.cars").map(|l| l.name()),
registry.language_for_path("zed/cars").map(|l| l.name()),
None
);
assert_eq!(
registry.language_for_path("zed/a.cars").map(|l| l.name()),
None
);
assert_eq!(
registry.language_for_path("zed/sumk").map(|l| l.name()),
None
);
assert_eq!(registry.select_language("zed/sumk").map(|l| l.name()), None);
}
#[gpui::test]
@@ -455,6 +461,32 @@ async fn test_outline(cx: &mut gpui::TestAppContext) {
}
}
#[gpui::test]
async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) {
let text = r#"
impl A for B<
C
> {
};
"#
.unindent();
let buffer =
cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx));
let outline = buffer
.read_with(cx, |buffer, _| buffer.snapshot().outline(None))
.unwrap();
assert_eq!(
outline
.items
.iter()
.map(|item| (item.text.as_str(), item.depth))
.collect::<Vec<_>>(),
&[("impl A for B<", 0)]
);
}
#[gpui::test]
async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
let text = r#"
@@ -774,23 +806,29 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta
cx.set_global(settings);
cx.add_model(|cx| {
let text = "
let mut buffer = Buffer::new(
0,
"
fn a() {
c;
d;
}
"
.unindent();
let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
"
.unindent(),
cx,
)
.with_language(Arc::new(rust_lang()), cx);
// Lines 2 and 3 don't match the indentation suggestion. When editing these lines,
// their indentation is not adjusted.
buffer.edit(
[
(empty(Point::new(1, 1)), "()"),
(empty(Point::new(2, 1)), "()"),
],
buffer.edit_via_marked_text(
&"
fn a() {
c«()»;
d«()»;
}
"
.unindent(),
Some(AutoindentMode::EachLine),
cx,
);
@@ -807,14 +845,22 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta
// When appending new content after these lines, the indentation is based on the
// preceding lines' actual indentation.
buffer.edit(
[
(empty(Point::new(1, 1)), "\n.f\n.g"),
(empty(Point::new(2, 1)), "\n.f\n.g"),
],
buffer.edit_via_marked_text(
&"
fn a() {
.f
.g()»;
.f
.g()»;
}
"
.unindent(),
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
"
@@ -833,20 +879,27 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta
});
cx.add_model(|cx| {
let text = "
let mut buffer = Buffer::new(
0,
"
fn a() {
{
b()?
}
Ok(())
}
"
.unindent();
let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
b();
|
"
.replace("|", "") // marker to preserve trailing whitespace
.unindent(),
cx,
)
.with_language(Arc::new(rust_lang()), cx);
// Delete a closing curly brace changes the suggested indent for the line.
buffer.edit(
[(Point::new(3, 4)..Point::new(3, 5), "")],
// Insert a closing brace. It is outdented.
buffer.edit_via_marked_text(
&"
fn a() {
b();
«}»
"
.unindent(),
Some(AutoindentMode::EachLine),
cx,
);
@@ -854,19 +907,20 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta
buffer.text(),
"
fn a() {
{
b()?
|
Ok(())
b();
}
"
.replace('|', "") // included in the string to preserve trailing whites
.unindent()
);
// Manually editing the leading whitespace
buffer.edit(
[(Point::new(3, 0)..Point::new(3, 12), "")],
// Manually edit the leading whitespace. The edit is preserved.
buffer.edit_via_marked_text(
&"
fn a() {
b();
« »}
"
.unindent(),
Some(AutoindentMode::EachLine),
cx,
);
@@ -874,11 +928,8 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta
buffer.text(),
"
fn a() {
{
b()?
Ok(())
}
b();
}
"
.unindent()
);
@@ -887,30 +938,108 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta
}
#[gpui::test]
fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppContext) {
cx.set_global(Settings::test(cx));
fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut MutableAppContext) {
let settings = Settings::test(cx);
cx.set_global(settings);
cx.add_model(|cx| {
let text = "
fn a() {}
"
.unindent();
let mut buffer = Buffer::new(
0,
"
fn a() {
i
}
"
.unindent(),
cx,
)
.with_language(Arc::new(rust_lang()), cx);
let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
buffer.edit([(5..5, "\nb")], Some(AutoindentMode::EachLine), cx);
// Regression test: line does not get outdented due to syntax error
buffer.edit_via_marked_text(
&"
fn a() {
i«f let Some(x) = y»
}
"
.unindent(),
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
"
fn a(
b) {}
fn a() {
if let Some(x) = y
}
"
.unindent()
);
buffer.edit_via_marked_text(
&"
fn a() {
if let Some(x) = y« {»
}
"
.unindent(),
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
"
fn a() {
if let Some(x) = y {
}
"
.unindent()
);
buffer
});
}
#[gpui::test]
fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppContext) {
cx.set_global(Settings::test(cx));
cx.add_model(|cx| {
let mut buffer = Buffer::new(
0,
"
fn a() {}
"
.unindent(),
cx,
)
.with_language(Arc::new(rust_lang()), cx);
buffer.edit_via_marked_text(
&"
fn a(«
b») {}
"
.unindent(),
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
"
fn a(
b) {}
"
.unindent()
);
// The indentation suggestion changed because `@end` node (a close paren)
// is now at the beginning of the line.
buffer.edit(
[(Point::new(1, 4)..Point::new(1, 5), "")],
buffer.edit_via_marked_text(
&"
fn a(
ˇ) {}
"
.unindent(),
Some(AutoindentMode::EachLine),
cx,
);
@@ -995,12 +1124,17 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) {
.unindent();
let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
// When this text was copied, both of the quotation marks were at the same
// indent level, but the indentation of the first line was not included in
// the copied text. This information is retained in the
// 'original_indent_columns' vector.
let original_indent_columns = vec![4];
let inserted_text = r#"
"
c
d
e
"
c
d
e
"
"#
.unindent();
@@ -1009,7 +1143,7 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) {
buffer.edit(
[(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())],
Some(AutoindentMode::Block {
original_indent_columns: vec![0],
original_indent_columns: original_indent_columns.clone(),
}),
cx,
);
@@ -1037,7 +1171,7 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) {
buffer.edit(
[(Point::new(2, 8)..Point::new(2, 8), inserted_text)],
Some(AutoindentMode::Block {
original_indent_columns: vec![0],
original_indent_columns: original_indent_columns.clone(),
}),
cx,
);
@@ -1060,6 +1194,84 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) {
});
}
#[gpui::test]
fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut MutableAppContext) {
cx.set_global(Settings::test(cx));
cx.add_model(|cx| {
let text = r#"
fn a() {
if b() {
}
}
"#
.unindent();
let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
// The original indent columns are not known, so this text is
// auto-indented in a block as if the first line was copied in
// its entirety.
let original_indent_columns = Vec::new();
let inserted_text = " c\n .d()\n .e();";
// Insert the block at column zero. The entire block is indented
// so that the first line matches the previous line's indentation.
buffer.edit(
[(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())],
Some(AutoindentMode::Block {
original_indent_columns: original_indent_columns.clone(),
}),
cx,
);
assert_eq!(
buffer.text(),
r#"
fn a() {
if b() {
c
.d()
.e();
}
}
"#
.unindent()
);
// Grouping is disabled in tests, so we need 2 undos
buffer.undo(cx); // Undo the auto-indent
buffer.undo(cx); // Undo the original edit
// Insert the block at a deeper indent level. The entire block is outdented.
buffer.edit(
[(Point::new(2, 0)..Point::new(2, 0), " ".repeat(12))],
None,
cx,
);
buffer.edit(
[(Point::new(2, 12)..Point::new(2, 12), inserted_text)],
Some(AutoindentMode::Block {
original_indent_columns: Vec::new(),
}),
cx,
);
assert_eq!(
buffer.text(),
r#"
fn a() {
if b() {
c
.d()
.e();
}
}
"#
.unindent()
);
buffer
});
}
#[gpui::test]
fn test_autoindent_language_without_indents_query(cx: &mut MutableAppContext) {
cx.set_global(Settings::test(cx));
@@ -1260,6 +1472,89 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut MutableAppContext) {
});
}
#[gpui::test]
fn test_language_config_at(cx: &mut MutableAppContext) {
cx.set_global(Settings::test(cx));
cx.add_model(|cx| {
let language = Language::new(
LanguageConfig {
name: "JavaScript".into(),
line_comment: Some("// ".into()),
brackets: vec![
BracketPair {
start: "{".into(),
end: "}".into(),
close: true,
newline: false,
},
BracketPair {
start: "'".into(),
end: "'".into(),
close: true,
newline: false,
},
],
overrides: [
(
"element".into(),
LanguageConfigOverride {
line_comment: Override::Remove { remove: true },
block_comment: Override::Set(("{/*".into(), "*/}".into())),
..Default::default()
},
),
(
"string".into(),
LanguageConfigOverride {
brackets: Override::Set(vec![BracketPair {
start: "{".into(),
end: "}".into(),
close: true,
newline: false,
}]),
..Default::default()
},
),
]
.into_iter()
.collect(),
..Default::default()
},
Some(tree_sitter_javascript::language()),
)
.with_override_query(
r#"
(jsx_element) @element
(string) @string
"#,
)
.unwrap();
let text = r#"a["b"] = <C d="e"></C>;"#;
let buffer = Buffer::new(0, text, cx).with_language(Arc::new(language), cx);
let snapshot = buffer.snapshot();
let config = snapshot.language_scope_at(0).unwrap();
assert_eq!(config.line_comment_prefix().unwrap().as_ref(), "// ");
assert_eq!(config.brackets().len(), 2);
let string_config = snapshot.language_scope_at(3).unwrap();
assert_eq!(config.line_comment_prefix().unwrap().as_ref(), "// ");
assert_eq!(string_config.brackets().len(), 1);
let element_config = snapshot.language_scope_at(10).unwrap();
assert_eq!(element_config.line_comment_prefix(), None);
assert_eq!(
element_config.block_comment_delimiters(),
Some((&"{/*".into(), &"*/}".into()))
);
assert_eq!(element_config.brackets().len(), 2);
buffer
});
}
#[gpui::test]
fn test_serialization(cx: &mut gpui::MutableAppContext) {
let mut now = Instant::now();
@@ -1702,7 +1997,3 @@ fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> Str
layers[0].node.to_sexp()
})
}
fn empty(point: Point) -> Range<Point> {
point..point
}

View File

@@ -16,7 +16,7 @@ use futures::{
future::{BoxFuture, Shared},
FutureExt, TryFutureExt,
};
use gpui::{MutableAppContext, Task};
use gpui::{executor::Background, MutableAppContext, Task};
use highlight_map::HighlightMap;
use lazy_static::lazy_static;
use parking_lot::{Mutex, RwLock};
@@ -26,6 +26,7 @@ use serde::{de, Deserialize, Deserializer};
use serde_json::Value;
use std::{
any::Any,
borrow::Cow,
cell::RefCell,
fmt::Debug,
hash::Hash,
@@ -41,6 +42,7 @@ use std::{
use syntax_map::SyntaxSnapshot;
use theme::{SyntaxTheme, Theme};
use tree_sitter::{self, Query};
use unicase::UniCase;
use util::ResultExt;
#[cfg(any(test, feature = "test-support"))]
@@ -88,8 +90,7 @@ pub struct CachedLspAdapter {
}
impl CachedLspAdapter {
pub async fn new<T: LspAdapter>(adapter: T) -> Arc<Self> {
let adapter = Box::new(adapter);
pub async fn new(adapter: Box<dyn LspAdapter>) -> Arc<Self> {
let name = adapter.name().await;
let server_args = adapter.server_args().await;
let initialization_options = adapter.initialization_options().await;
@@ -243,6 +244,57 @@ pub struct LanguageConfig {
pub line_comment: Option<Arc<str>>,
#[serde(default)]
pub block_comment: Option<(Arc<str>, Arc<str>)>,
#[serde(default)]
pub overrides: HashMap<String, LanguageConfigOverride>,
}
#[derive(Debug, Default)]
pub struct LanguageQueries {
pub highlights: Option<Cow<'static, str>>,
pub brackets: Option<Cow<'static, str>>,
pub indents: Option<Cow<'static, str>>,
pub outline: Option<Cow<'static, str>>,
pub injections: Option<Cow<'static, str>>,
pub overrides: Option<Cow<'static, str>>,
}
#[derive(Clone)]
pub struct LanguageScope {
language: Arc<Language>,
override_id: Option<u32>,
}
#[derive(Deserialize, Default, Debug)]
pub struct LanguageConfigOverride {
#[serde(default)]
pub line_comment: Override<Arc<str>>,
#[serde(default)]
pub block_comment: Override<(Arc<str>, Arc<str>)>,
#[serde(default)]
pub brackets: Override<Vec<BracketPair>>,
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub enum Override<T> {
Remove { remove: bool },
Set(T),
}
impl<T> Default for Override<T> {
fn default() -> Self {
Override::Remove { remove: false }
}
}
impl<T> Override<T> {
fn as_option<'a>(this: Option<&'a Self>, original: Option<&'a T>) -> Option<&'a T> {
match this {
Some(Self::Set(value)) => Some(value),
Some(Self::Remove { remove: true }) => None,
Some(Self::Remove { remove: false }) | None => original,
}
}
}
impl Default for LanguageConfig {
@@ -257,6 +309,7 @@ impl Default for LanguageConfig {
autoclose_before: Default::default(),
line_comment: Default::default(),
block_comment: Default::default(),
overrides: Default::default(),
}
}
}
@@ -306,11 +359,13 @@ pub struct Language {
pub struct Grammar {
id: usize,
pub(crate) ts_language: tree_sitter::Language,
pub(crate) error_query: Query,
pub(crate) highlights_query: Option<Query>,
pub(crate) brackets_config: Option<BracketConfig>,
pub(crate) indents_config: Option<IndentConfig>,
pub(crate) outline_config: Option<OutlineConfig>,
pub(crate) injection_config: Option<InjectionConfig>,
pub(crate) override_config: Option<OverrideConfig>,
pub(crate) highlight_map: Mutex<HighlightMap>,
}
@@ -336,6 +391,11 @@ struct InjectionConfig {
patterns: Vec<InjectionPatternConfig>,
}
struct OverrideConfig {
query: Query,
values: HashMap<u32, LanguageConfigOverride>,
}
#[derive(Default, Clone)]
struct InjectionPatternConfig {
language: Option<Box<str>>,
@@ -357,8 +417,17 @@ pub enum LanguageServerBinaryStatus {
Failed { error: String },
}
struct AvailableLanguage {
path: &'static str,
config: LanguageConfig,
grammar: tree_sitter::Language,
lsp_adapter: Option<Box<dyn LspAdapter>>,
get_queries: fn(&str) -> LanguageQueries,
}
pub struct LanguageRegistry {
languages: RwLock<Vec<Arc<Language>>>,
available_languages: RwLock<Vec<AvailableLanguage>>,
language_server_download_dir: Option<Arc<Path>>,
lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)>,
@@ -372,6 +441,8 @@ pub struct LanguageRegistry {
>,
subscription: RwLock<(watch::Sender<()>, watch::Receiver<()>)>,
theme: RwLock<Option<Arc<Theme>>>,
executor: Option<Arc<Background>>,
version: AtomicUsize,
}
impl LanguageRegistry {
@@ -380,12 +451,15 @@ impl LanguageRegistry {
Self {
language_server_download_dir: None,
languages: Default::default(),
available_languages: Default::default(),
lsp_binary_statuses_tx,
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(),
version: Default::default(),
executor: None,
}
}
@@ -394,11 +468,50 @@ impl LanguageRegistry {
Self::new(Task::ready(()))
}
pub fn set_executor(&mut self, executor: Arc<Background>) {
self.executor = Some(executor);
}
pub fn register(
&self,
path: &'static str,
config: LanguageConfig,
grammar: tree_sitter::Language,
lsp_adapter: Option<Box<dyn LspAdapter>>,
get_queries: fn(&str) -> LanguageQueries,
) {
self.available_languages.write().push(AvailableLanguage {
path,
config,
grammar,
lsp_adapter,
get_queries,
});
}
pub fn language_names(&self) -> Vec<String> {
let mut result = self
.available_languages
.read()
.iter()
.map(|l| l.config.name.to_string())
.chain(
self.languages
.read()
.iter()
.map(|l| l.config.name.to_string()),
)
.collect::<Vec<_>>();
result.sort_unstable();
result
}
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);
self.version.fetch_add(1, SeqCst);
*self.subscription.write().0.borrow_mut() = ();
}
@@ -406,6 +519,10 @@ impl LanguageRegistry {
self.subscription.read().1.clone()
}
pub fn version(&self) -> usize {
self.version.load(SeqCst)
}
pub fn set_theme(&self, theme: Arc<Theme>) {
*self.theme.write() = Some(theme.clone());
for language in self.languages.read().iter() {
@@ -417,42 +534,79 @@ impl LanguageRegistry {
self.language_server_download_dir = Some(path.into());
}
pub fn get_language(&self, name: &str) -> Option<Arc<Language>> {
self.languages
.read()
.iter()
.find(|language| language.name().to_lowercase() == name.to_lowercase())
.cloned()
pub fn language_for_name(self: &Arc<Self>, name: &str) -> Option<Arc<Language>> {
let name = UniCase::new(name);
self.get_or_load_language(|config| UniCase::new(config.name.as_ref()) == name)
}
pub fn to_vec(&self) -> Vec<Arc<Language>> {
self.languages.read().iter().cloned().collect()
pub fn language_for_name_or_extension(self: &Arc<Self>, string: &str) -> Option<Arc<Language>> {
let string = UniCase::new(string);
self.get_or_load_language(|config| {
UniCase::new(config.name.as_ref()) == string
|| config
.path_suffixes
.iter()
.any(|suffix| UniCase::new(suffix) == string)
})
}
pub fn language_names(&self) -> Vec<String> {
self.languages
.read()
.iter()
.map(|language| language.name().to_string())
.collect()
}
pub fn select_language(&self, path: impl AsRef<Path>) -> Option<Arc<Language>> {
pub fn language_for_path(self: &Arc<Self>, path: impl AsRef<Path>) -> Option<Arc<Language>> {
let path = path.as_ref();
let filename = path.file_name().and_then(|name| name.to_str());
let extension = path.extension().and_then(|name| name.to_str());
let path_suffixes = [extension, filename];
self.languages
self.get_or_load_language(|config| {
config
.path_suffixes
.iter()
.any(|suffix| path_suffixes.contains(&Some(suffix.as_str())))
})
}
fn get_or_load_language(
self: &Arc<Self>,
callback: impl Fn(&LanguageConfig) -> bool,
) -> Option<Arc<Language>> {
if let Some(language) = self
.languages
.read()
.iter()
.find(|language| {
language
.config
.path_suffixes
.iter()
.any(|suffix| path_suffixes.contains(&Some(suffix.as_str())))
})
.cloned()
.find(|language| callback(&language.config))
{
return Some(language.clone());
}
if let Some(executor) = self.executor.clone() {
let mut available_languages = self.available_languages.write();
if let Some(ix) = available_languages.iter().position(|l| callback(&l.config)) {
let language = available_languages.remove(ix);
drop(available_languages);
let name = language.config.name.clone();
let this = self.clone();
executor
.spawn(async move {
let queries = (language.get_queries)(&language.path);
let language = Language::new(language.config, Some(language.grammar))
.with_lsp_adapter(language.lsp_adapter)
.await;
match language.with_queries(queries) {
Ok(language) => this.add(Arc::new(language)),
Err(err) => {
log::error!("failed to load language {}: {}", name, err);
return;
}
};
})
.detach();
}
}
None
}
pub fn to_vec(&self) -> Vec<Arc<Language>> {
self.languages.read().iter().cloned().collect()
}
pub fn start_language_server(
@@ -635,6 +789,8 @@ impl Language {
outline_config: None,
indents_config: None,
injection_config: None,
override_config: None,
error_query: Query::new(ts_language, "(ERROR) @error").unwrap(),
ts_language,
highlight_map: Default::default(),
})
@@ -654,12 +810,70 @@ impl Language {
self.grammar.as_ref().map(|g| g.id)
}
pub fn with_queries(mut self, queries: LanguageQueries) -> Result<Self> {
if let Some(query) = queries.highlights {
self = self
.with_highlights_query(query.as_ref())
.expect("failed to evaluate highlights query");
}
if let Some(query) = queries.brackets {
self = self
.with_brackets_query(query.as_ref())
.expect("failed to load brackets query");
}
if let Some(query) = queries.indents {
self = self
.with_indents_query(query.as_ref())
.expect("failed to load indents query");
}
if let Some(query) = queries.outline {
self = self
.with_outline_query(query.as_ref())
.expect("failed to load outline query");
}
if let Some(query) = queries.injections {
self = self
.with_injection_query(query.as_ref())
.expect("failed to load injection query");
}
if let Some(query) = queries.overrides {
self = self
.with_override_query(query.as_ref())
.expect("failed to load override query");
}
Ok(self)
}
pub fn with_highlights_query(mut self, source: &str) -> Result<Self> {
let grammar = self.grammar_mut();
grammar.highlights_query = Some(Query::new(grammar.ts_language, source)?);
Ok(self)
}
pub fn with_outline_query(mut self, source: &str) -> Result<Self> {
let grammar = self.grammar_mut();
let query = Query::new(grammar.ts_language, source)?;
let mut item_capture_ix = None;
let mut name_capture_ix = None;
let mut context_capture_ix = None;
get_capture_indices(
&query,
&mut [
("item", &mut item_capture_ix),
("name", &mut name_capture_ix),
("context", &mut context_capture_ix),
],
);
if let Some((item_capture_ix, name_capture_ix)) = item_capture_ix.zip(name_capture_ix) {
grammar.outline_config = Some(OutlineConfig {
query,
item_capture_ix,
name_capture_ix,
context_capture_ix,
});
}
Ok(self)
}
pub fn with_brackets_query(mut self, source: &str) -> Result<Self> {
let grammar = self.grammar_mut();
let query = Query::new(grammar.ts_language, source)?;
@@ -710,31 +924,6 @@ impl Language {
Ok(self)
}
pub fn with_outline_query(mut self, source: &str) -> Result<Self> {
let grammar = self.grammar_mut();
let query = Query::new(grammar.ts_language, source)?;
let mut item_capture_ix = None;
let mut name_capture_ix = None;
let mut context_capture_ix = None;
get_capture_indices(
&query,
&mut [
("item", &mut item_capture_ix),
("name", &mut name_capture_ix),
("context", &mut context_capture_ix),
],
);
if let Some((item_capture_ix, name_capture_ix)) = item_capture_ix.zip(name_capture_ix) {
grammar.outline_config = Some(OutlineConfig {
query,
item_capture_ix,
name_capture_ix,
context_capture_ix,
});
}
Ok(self)
}
pub fn with_injection_query(mut self, source: &str) -> Result<Self> {
let grammar = self.grammar_mut();
let query = Query::new(grammar.ts_language, source)?;
@@ -775,12 +964,42 @@ impl Language {
Ok(self)
}
pub fn with_override_query(mut self, source: &str) -> Result<Self> {
let query = Query::new(self.grammar_mut().ts_language, source)?;
let mut values = HashMap::default();
for (ix, name) in query.capture_names().iter().enumerate() {
if !name.starts_with('_') {
let value = self.config.overrides.remove(name).ok_or_else(|| {
anyhow!(
"language {:?} has override in query but not in config: {name:?}",
self.config.name
)
})?;
values.insert(ix as u32, value);
}
}
if !self.config.overrides.is_empty() {
let keys = self.config.overrides.keys().collect::<Vec<_>>();
Err(anyhow!(
"language {:?} has overrides in config not in query: {keys:?}",
self.config.name
))?;
}
self.grammar_mut().override_config = Some(OverrideConfig { query, values });
Ok(self)
}
fn grammar_mut(&mut self) -> &mut Grammar {
Arc::get_mut(self.grammar.as_mut().unwrap()).unwrap()
}
pub fn with_lsp_adapter(mut self, lsp_adapter: Arc<CachedLspAdapter>) -> Self {
self.adapter = Some(lsp_adapter);
pub async fn with_lsp_adapter(mut self, lsp_adapter: Option<Box<dyn LspAdapter>>) -> Self {
if let Some(adapter) = lsp_adapter {
self.adapter = Some(CachedLspAdapter::new(adapter).await);
}
self
}
@@ -791,7 +1010,7 @@ impl Language {
) -> mpsc::UnboundedReceiver<lsp::FakeLanguageServer> {
let (servers_tx, servers_rx) = mpsc::unbounded();
self.fake_adapter = Some((servers_tx, fake_lsp_adapter.clone()));
let adapter = CachedLspAdapter::new(fake_lsp_adapter).await;
let adapter = CachedLspAdapter::new(Box::new(fake_lsp_adapter)).await;
self.adapter = Some(adapter);
servers_rx
}
@@ -800,17 +1019,6 @@ impl Language {
self.config.name.clone()
}
pub fn line_comment_prefix(&self) -> Option<&Arc<str>> {
self.config.line_comment.as_ref()
}
pub fn block_comment_delimiters(&self) -> Option<(&Arc<str>, &Arc<str>)> {
self.config
.block_comment
.as_ref()
.map(|(start, end)| (start, end))
}
pub async fn disk_based_diagnostic_sources(&self) -> &[String] {
match self.adapter.as_ref() {
Some(adapter) => &adapter.disk_based_diagnostic_sources,
@@ -886,10 +1094,6 @@ impl Language {
result
}
pub fn brackets(&self) -> &[BracketPair] {
&self.config.brackets
}
pub fn path_suffixes(&self) -> &[String] {
&self.config.path_suffixes
}
@@ -912,6 +1116,42 @@ impl Language {
}
}
impl LanguageScope {
pub fn line_comment_prefix(&self) -> Option<&Arc<str>> {
Override::as_option(
self.config_override().map(|o| &o.line_comment),
self.language.config.line_comment.as_ref(),
)
}
pub fn block_comment_delimiters(&self) -> Option<(&Arc<str>, &Arc<str>)> {
Override::as_option(
self.config_override().map(|o| &o.block_comment),
self.language.config.block_comment.as_ref(),
)
.map(|e| (&e.0, &e.1))
}
pub fn brackets(&self) -> &[BracketPair] {
Override::as_option(
self.config_override().map(|o| &o.brackets),
Some(&self.language.config.brackets),
)
.map_or(&[], Vec::as_slice)
}
pub fn should_autoclose_before(&self, c: char) -> bool {
c.is_whitespace() || self.language.config.autoclose_before.contains(c)
}
fn config_override(&self) -> Option<&LanguageConfigOverride> {
let id = self.override_id?;
let grammar = self.language.grammar.as_ref()?;
let override_config = grammar.override_config.as_ref()?;
override_config.values.get(&id)
}
}
impl Hash for Language {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id().hash(state)

View File

@@ -11,6 +11,15 @@ use text::*;
pub use proto::{BufferState, Operation};
pub fn serialize_fingerprint(fingerprint: RopeFingerprint) -> String {
fingerprint.to_hex()
}
pub fn deserialize_fingerprint(fingerprint: &str) -> Result<RopeFingerprint> {
RopeFingerprint::from_hex(fingerprint)
.map_err(|error| anyhow!("invalid fingerprint: {}", error))
}
pub fn deserialize_line_ending(message: proto::LineEnding) -> fs::LineEnding {
match message {
proto::LineEnding::Unix => fs::LineEnding::Unix,

View File

@@ -5,8 +5,9 @@ use parking_lot::Mutex;
use std::{
borrow::Cow,
cell::RefCell,
cmp::{Ordering, Reverse},
cmp::{self, Ordering, Reverse},
collections::BinaryHeap,
iter,
ops::{Deref, DerefMut, Range},
sync::Arc,
};
@@ -26,8 +27,6 @@ lazy_static! {
#[derive(Default)]
pub struct SyntaxMap {
parsed_version: clock::Global,
interpolated_version: clock::Global,
snapshot: SyntaxSnapshot,
language_registry: Option<Arc<LanguageRegistry>>,
}
@@ -35,6 +34,9 @@ pub struct SyntaxMap {
#[derive(Clone, Default)]
pub struct SyntaxSnapshot {
layers: SumTree<SyntaxLayer>,
parsed_version: clock::Global,
interpolated_version: clock::Global,
language_registry_version: usize,
}
#[derive(Default)]
@@ -89,8 +91,34 @@ struct SyntaxMapMatchesLayer<'a> {
struct SyntaxLayer {
depth: usize,
range: Range<Anchor>,
tree: tree_sitter::Tree,
language: Arc<Language>,
content: SyntaxLayerContent,
}
#[derive(Clone)]
enum SyntaxLayerContent {
Parsed {
tree: tree_sitter::Tree,
language: Arc<Language>,
},
Pending {
language_name: Arc<str>,
},
}
impl SyntaxLayerContent {
fn language_id(&self) -> Option<usize> {
match self {
SyntaxLayerContent::Parsed { language, .. } => language.id(),
SyntaxLayerContent::Pending { .. } => None,
}
}
fn tree(&self) -> Option<&Tree> {
match self {
SyntaxLayerContent::Parsed { tree, .. } => Some(tree),
SyntaxLayerContent::Pending { .. } => None,
}
}
}
#[derive(Debug)]
@@ -107,6 +135,7 @@ struct SyntaxLayerSummary {
range: Range<Anchor>,
last_layer_range: Range<Anchor>,
last_layer_language: Option<usize>,
contains_unknown_injections: bool,
}
#[derive(Clone, Debug)]
@@ -130,12 +159,26 @@ struct SyntaxLayerPositionBeforeChange {
struct ParseStep {
depth: usize,
language: Arc<Language>,
language: ParseStepLanguage,
range: Range<Anchor>,
included_ranges: Vec<tree_sitter::Range>,
mode: ParseMode,
}
enum ParseStepLanguage {
Loaded { language: Arc<Language> },
Pending { name: Arc<str> },
}
impl ParseStepLanguage {
fn id(&self) -> Option<usize> {
match self {
ParseStepLanguage::Loaded { language } => language.id(),
ParseStepLanguage::Pending { .. } => None,
}
}
}
enum ParseMode {
Single,
Combined {
@@ -176,30 +219,17 @@ impl SyntaxMap {
self.language_registry.clone()
}
pub fn parsed_version(&self) -> clock::Global {
self.parsed_version.clone()
}
pub fn interpolate(&mut self, text: &BufferSnapshot) {
self.snapshot.interpolate(&self.interpolated_version, text);
self.interpolated_version = text.version.clone();
self.snapshot.interpolate(text);
}
#[cfg(test)]
pub fn reparse(&mut self, language: Arc<Language>, text: &BufferSnapshot) {
self.snapshot.reparse(
&self.parsed_version,
text,
self.language_registry.clone(),
language,
);
self.parsed_version = text.version.clone();
self.interpolated_version = text.version.clone();
self.snapshot
.reparse(text, self.language_registry.clone(), language);
}
pub fn did_parse(&mut self, snapshot: SyntaxSnapshot, version: clock::Global) {
self.interpolated_version = version.clone();
self.parsed_version = version;
pub fn did_parse(&mut self, snapshot: SyntaxSnapshot) {
self.snapshot = snapshot;
}
@@ -213,10 +243,12 @@ impl SyntaxSnapshot {
self.layers.is_empty()
}
pub fn interpolate(&mut self, from_version: &clock::Global, text: &BufferSnapshot) {
fn interpolate(&mut self, text: &BufferSnapshot) {
let edits = text
.anchored_edits_since::<(usize, Point)>(&from_version)
.anchored_edits_since::<(usize, Point)>(&self.interpolated_version)
.collect::<Vec<_>>();
self.interpolated_version = text.version().clone();
if edits.is_empty() {
return;
}
@@ -276,47 +308,49 @@ impl SyntaxSnapshot {
}
let mut layer = layer.clone();
for (edit, edit_range) in &edits[first_edit_ix_for_depth..] {
// Ignore any edits that follow this layer.
if edit_range.start.cmp(&layer.range.end, text).is_ge() {
break;
if let SyntaxLayerContent::Parsed { tree, .. } = &mut layer.content {
for (edit, edit_range) in &edits[first_edit_ix_for_depth..] {
// Ignore any edits that follow this layer.
if edit_range.start.cmp(&layer.range.end, text).is_ge() {
break;
}
// Apply any edits that intersect this layer to the layer's syntax tree.
let tree_edit = if edit_range.start.cmp(&layer.range.start, text).is_ge() {
tree_sitter::InputEdit {
start_byte: edit.new.start.0 - start_byte,
old_end_byte: edit.new.start.0 - start_byte
+ (edit.old.end.0 - edit.old.start.0),
new_end_byte: edit.new.end.0 - start_byte,
start_position: (edit.new.start.1 - start_point).to_ts_point(),
old_end_position: (edit.new.start.1 - start_point
+ (edit.old.end.1 - edit.old.start.1))
.to_ts_point(),
new_end_position: (edit.new.end.1 - start_point).to_ts_point(),
}
} else {
let node = tree.root_node();
tree_sitter::InputEdit {
start_byte: 0,
old_end_byte: node.end_byte(),
new_end_byte: 0,
start_position: Default::default(),
old_end_position: node.end_position(),
new_end_position: Default::default(),
}
};
tree.edit(&tree_edit);
}
// Apply any edits that intersect this layer to the layer's syntax tree.
let tree_edit = if edit_range.start.cmp(&layer.range.start, text).is_ge() {
tree_sitter::InputEdit {
start_byte: edit.new.start.0 - start_byte,
old_end_byte: edit.new.start.0 - start_byte
+ (edit.old.end.0 - edit.old.start.0),
new_end_byte: edit.new.end.0 - start_byte,
start_position: (edit.new.start.1 - start_point).to_ts_point(),
old_end_position: (edit.new.start.1 - start_point
+ (edit.old.end.1 - edit.old.start.1))
.to_ts_point(),
new_end_position: (edit.new.end.1 - start_point).to_ts_point(),
}
} else {
let node = layer.tree.root_node();
tree_sitter::InputEdit {
start_byte: 0,
old_end_byte: node.end_byte(),
new_end_byte: 0,
start_position: Default::default(),
old_end_position: node.end_position(),
new_end_position: Default::default(),
}
};
layer.tree.edit(&tree_edit);
debug_assert!(
tree.root_node().end_byte() <= text.len(),
"tree's size {}, is larger than text size {}",
tree.root_node().end_byte(),
text.len(),
);
}
debug_assert!(
layer.tree.root_node().end_byte() <= text.len(),
"tree's size {}, is larger than text size {}",
layer.tree.root_node().end_byte(),
text.len(),
);
layers.push(layer, text);
cursor.next(text);
}
@@ -328,12 +362,58 @@ impl SyntaxSnapshot {
pub fn reparse(
&mut self,
from_version: &clock::Global,
text: &BufferSnapshot,
registry: Option<Arc<LanguageRegistry>>,
root_language: Arc<Language>,
) {
let edits = text.edits_since::<usize>(from_version).collect::<Vec<_>>();
let edit_ranges = text
.edits_since::<usize>(&self.parsed_version)
.map(|edit| edit.new)
.collect::<Vec<_>>();
self.reparse_with_ranges(text, root_language.clone(), edit_ranges, registry.as_ref());
if let Some(registry) = registry {
if registry.version() != self.language_registry_version {
let mut resolved_injection_ranges = Vec::new();
let mut cursor = self
.layers
.filter::<_, ()>(|summary| summary.contains_unknown_injections);
cursor.next(text);
while let Some(layer) = cursor.item() {
let SyntaxLayerContent::Pending { language_name } = &layer.content else { unreachable!() };
if {
let language_registry = &registry;
language_registry.language_for_name_or_extension(language_name)
}
.is_some()
{
resolved_injection_ranges.push(layer.range.to_offset(text));
}
cursor.next(text);
}
drop(cursor);
if !resolved_injection_ranges.is_empty() {
self.reparse_with_ranges(
text,
root_language,
resolved_injection_ranges,
Some(&registry),
);
}
self.language_registry_version = registry.version();
}
}
}
fn reparse_with_ranges(
&mut self,
text: &BufferSnapshot,
root_language: Arc<Language>,
invalidated_ranges: Vec<Range<usize>>,
registry: Option<&Arc<LanguageRegistry>>,
) {
let max_depth = self.layers.summary().max_depth;
let mut cursor = self.layers.cursor::<SyntaxLayerSummary>();
cursor.next(&text);
@@ -344,7 +424,9 @@ impl SyntaxSnapshot {
let mut combined_injection_ranges = HashMap::default();
queue.push(ParseStep {
depth: 0,
language: root_language.clone(),
language: ParseStepLanguage::Loaded {
language: root_language,
},
included_ranges: vec![tree_sitter::Range {
start_byte: 0,
end_byte: text.len(),
@@ -415,12 +497,11 @@ impl SyntaxSnapshot {
let (step_start_byte, step_start_point) =
step.range.start.summary::<(usize, Point)>(text);
let step_end_byte = step.range.end.to_offset(text);
let Some(grammar) = step.language.grammar.as_deref() else { continue };
let mut old_layer = cursor.item();
if let Some(layer) = old_layer {
if layer.range.to_offset(text) == (step_start_byte..step_end_byte)
&& layer.language.id() == step.language.id()
&& layer.content.language_id() == step.language.id()
{
cursor.next(&text);
} else {
@@ -428,89 +509,105 @@ impl SyntaxSnapshot {
}
}
let tree;
let changed_ranges;
let mut included_ranges = step.included_ranges;
if let Some(old_layer) = old_layer {
if let ParseMode::Combined {
parent_layer_changed_ranges,
..
} = step.mode
{
included_ranges = splice_included_ranges(
old_layer.tree.included_ranges(),
&parent_layer_changed_ranges,
&included_ranges,
);
}
let content = match step.language {
ParseStepLanguage::Loaded { language } => {
let Some(grammar) = language.grammar() else { continue };
let tree;
let changed_ranges;
let mut included_ranges = step.included_ranges;
if let Some(SyntaxLayerContent::Parsed { tree: old_tree, .. }) =
old_layer.map(|layer| &layer.content)
{
if let ParseMode::Combined {
parent_layer_changed_ranges,
..
} = step.mode
{
included_ranges = splice_included_ranges(
old_tree.included_ranges(),
&parent_layer_changed_ranges,
&included_ranges,
);
}
tree = parse_text(
grammar,
text.as_rope(),
step_start_byte,
step_start_point,
included_ranges,
Some(old_layer.tree.clone()),
);
changed_ranges = join_ranges(
edits.iter().map(|e| e.new.clone()).filter(|range| {
range.start <= step_end_byte && range.end >= step_start_byte
}),
old_layer
.tree
.changed_ranges(&tree)
.map(|r| step_start_byte + r.start_byte..step_start_byte + r.end_byte),
);
} else {
tree = parse_text(
grammar,
text.as_rope(),
step_start_byte,
step_start_point,
included_ranges,
None,
);
changed_ranges = vec![step_start_byte..step_end_byte];
}
tree = parse_text(
grammar,
text.as_rope(),
step_start_byte,
step_start_point,
included_ranges,
Some(old_tree.clone()),
);
changed_ranges = join_ranges(
invalidated_ranges.iter().cloned().filter(|range| {
range.start <= step_end_byte && range.end >= step_start_byte
}),
old_tree.changed_ranges(&tree).map(|r| {
step_start_byte + r.start_byte..step_start_byte + r.end_byte
}),
);
} else {
tree = parse_text(
grammar,
text.as_rope(),
step_start_byte,
step_start_point,
included_ranges,
None,
);
changed_ranges = vec![step_start_byte..step_end_byte];
}
if let (Some((config, registry)), false) = (
grammar.injection_config.as_ref().zip(registry.as_ref()),
changed_ranges.is_empty(),
) {
for range in &changed_ranges {
changed_regions.insert(
ChangedRegion {
depth: step.depth + 1,
range: text.anchor_before(range.start)
..text.anchor_after(range.end),
},
text,
);
}
get_injections(
config,
text,
tree.root_node_with_offset(
step_start_byte,
step_start_point.to_ts_point(),
),
registry,
step.depth + 1,
&changed_ranges,
&mut combined_injection_ranges,
&mut queue,
);
}
SyntaxLayerContent::Parsed { tree, language }
}
ParseStepLanguage::Pending { name } => SyntaxLayerContent::Pending {
language_name: name,
},
};
layers.push(
SyntaxLayer {
depth: step.depth,
range: step.range,
tree: tree.clone(),
language: step.language.clone(),
content,
},
&text,
);
if let (Some((config, registry)), false) = (
grammar.injection_config.as_ref().zip(registry.as_ref()),
changed_ranges.is_empty(),
) {
for range in &changed_ranges {
changed_regions.insert(
ChangedRegion {
depth: step.depth + 1,
range: text.anchor_before(range.start)..text.anchor_after(range.end),
},
text,
);
}
get_injections(
config,
text,
tree.root_node_with_offset(step_start_byte, step_start_point.to_ts_point()),
registry,
step.depth + 1,
&changed_ranges,
&mut combined_injection_ranges,
&mut queue,
);
}
}
drop(cursor);
self.layers = layers;
self.interpolated_version = text.version.clone();
self.parsed_version = text.version.clone();
}
pub fn single_tree_captures<'a>(
@@ -585,23 +682,34 @@ impl SyntaxSnapshot {
});
cursor.next(buffer);
std::iter::from_fn(move || {
if let Some(layer) = cursor.item() {
let info = SyntaxLayerInfo {
language: &layer.language,
depth: layer.depth,
node: layer.tree.root_node_with_offset(
layer.range.start.to_offset(buffer),
layer.range.start.to_point(buffer).to_ts_point(),
),
};
cursor.next(buffer);
Some(info)
} else {
None
iter::from_fn(move || {
while let Some(layer) = cursor.item() {
if let SyntaxLayerContent::Parsed { tree, language } = &layer.content {
let info = SyntaxLayerInfo {
language,
depth: layer.depth,
node: tree.root_node_with_offset(
layer.range.start.to_offset(buffer),
layer.range.start.to_point(buffer).to_ts_point(),
),
};
cursor.next(buffer);
return Some(info);
} else {
cursor.next(buffer);
}
}
None
})
}
pub fn contains_unknown_injections(&self) -> bool {
self.layers.summary().contains_unknown_injections
}
pub fn language_registry_version(&self) -> usize {
self.language_registry_version
}
}
impl<'a> SyntaxMapCaptures<'a> {
@@ -963,20 +1071,20 @@ fn get_injections(
config: &InjectionConfig,
text: &BufferSnapshot,
node: Node,
language_registry: &LanguageRegistry,
language_registry: &Arc<LanguageRegistry>,
depth: usize,
changed_ranges: &[Range<usize>],
combined_injection_ranges: &mut HashMap<Arc<Language>, Vec<tree_sitter::Range>>,
queue: &mut BinaryHeap<ParseStep>,
) -> bool {
let mut result = false;
) {
let mut query_cursor = QueryCursorHandle::new();
let mut prev_match = None;
combined_injection_ranges.clear();
for pattern in &config.patterns {
if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) {
if let Some(language) = language_registry.get_language(language_name) {
if let Some(language) = language_registry.language_for_name_or_extension(language_name)
{
combined_injection_ranges.insert(language, Vec::new());
}
}
@@ -1004,21 +1112,29 @@ fn get_injections(
prev_match = Some((mat.pattern_index, content_range.clone()));
let combined = config.patterns[mat.pattern_index].combined;
let language_name = config.patterns[mat.pattern_index]
.language
.as_ref()
.map(|s| Cow::Borrowed(s.as_ref()))
.or_else(|| {
let ix = config.language_capture_ix?;
let node = mat.nodes_for_capture_index(ix).next()?;
Some(Cow::Owned(text.text_for_range(node.byte_range()).collect()))
});
let mut language_name = None;
let mut step_range = content_range.clone();
if let Some(name) = config.patterns[mat.pattern_index].language.as_ref() {
language_name = Some(Cow::Borrowed(name.as_ref()))
} else if let Some(language_node) = config
.language_capture_ix
.and_then(|ix| mat.nodes_for_capture_index(ix).next())
{
step_range.start = cmp::min(content_range.start, language_node.start_byte());
step_range.end = cmp::max(content_range.end, language_node.end_byte());
language_name = Some(Cow::Owned(
text.text_for_range(language_node.byte_range()).collect(),
))
};
if let Some(language_name) = language_name {
if let Some(language) = language_registry.get_language(language_name.as_ref()) {
result = true;
let range = text.anchor_before(content_range.start)
..text.anchor_after(content_range.end);
let language = {
let language_name: &str = &language_name;
language_registry.language_for_name_or_extension(language_name)
};
let range = text.anchor_before(step_range.start)..text.anchor_after(step_range.end);
if let Some(language) = language {
if combined {
combined_injection_ranges
.get_mut(&language.clone())
@@ -1027,12 +1143,22 @@ fn get_injections(
} else {
queue.push(ParseStep {
depth,
language,
language: ParseStepLanguage::Loaded { language },
included_ranges: content_ranges,
range,
mode: ParseMode::Single,
});
}
} else {
queue.push(ParseStep {
depth,
language: ParseStepLanguage::Pending {
name: language_name.into(),
},
included_ranges: content_ranges,
range,
mode: ParseMode::Single,
});
}
}
}
@@ -1043,7 +1169,7 @@ fn get_injections(
let range = text.anchor_before(node.start_byte())..text.anchor_after(node.end_byte());
queue.push(ParseStep {
depth,
language,
language: ParseStepLanguage::Loaded { language },
range,
included_ranges,
mode: ParseMode::Combined {
@@ -1052,8 +1178,6 @@ fn get_injections(
},
})
}
result
}
fn splice_included_ranges(
@@ -1127,6 +1251,41 @@ fn splice_included_ranges(
ranges
}
impl<'a> SyntaxLayerInfo<'a> {
pub(crate) fn override_id(&self, offset: usize, text: &text::BufferSnapshot) -> Option<u32> {
let text = TextProvider(text.as_rope());
let config = self.language.grammar.as_ref()?.override_config.as_ref()?;
let mut query_cursor = QueryCursorHandle::new();
query_cursor.set_byte_range(offset..offset);
let mut smallest_match: Option<(u32, Range<usize>)> = None;
for mat in query_cursor.matches(&config.query, self.node, text) {
for capture in mat.captures {
if !config.values.contains_key(&capture.index) {
continue;
}
let range = capture.node.byte_range();
if offset <= range.start || offset >= range.end {
continue;
}
if let Some((_, smallest_range)) = &smallest_match {
if range.len() < smallest_range.len() {
smallest_match = Some((capture.index, range))
}
continue;
}
smallest_match = Some((capture.index, range));
}
}
smallest_match.map(|(index, _)| index)
}
}
impl std::ops::Deref for SyntaxMap {
type Target = SyntaxSnapshot;
@@ -1247,6 +1406,7 @@ impl Default for SyntaxLayerSummary {
range: Anchor::MAX..Anchor::MIN,
last_layer_range: Anchor::MIN..Anchor::MAX,
last_layer_language: None,
contains_unknown_injections: false,
}
}
}
@@ -1268,6 +1428,7 @@ impl sum_tree::Summary for SyntaxLayerSummary {
}
self.last_layer_range = other.last_layer_range.clone();
self.last_layer_language = other.last_layer_language;
self.contains_unknown_injections |= other.contains_unknown_injections;
}
}
@@ -1317,7 +1478,8 @@ impl sum_tree::Item for SyntaxLayer {
max_depth: self.depth,
range: self.range.clone(),
last_layer_range: self.range.clone(),
last_layer_language: self.language.id(),
last_layer_language: self.content.language_id(),
contains_unknown_injections: matches!(self.content, SyntaxLayerContent::Pending { .. }),
}
}
}
@@ -1327,7 +1489,7 @@ impl std::fmt::Debug for SyntaxLayer {
f.debug_struct("SyntaxLayer")
.field("depth", &self.depth)
.field("range", &self.range)
.field("tree", &self.tree)
.field("tree", &self.content.tree())
.finish()
}
}
@@ -1558,6 +1720,84 @@ mod tests {
);
}
#[gpui::test]
fn test_dynamic_language_injection() {
let registry = Arc::new(LanguageRegistry::test());
let markdown = Arc::new(markdown_lang());
registry.add(markdown.clone());
registry.add(Arc::new(rust_lang()));
registry.add(Arc::new(ruby_lang()));
let mut buffer = Buffer::new(
0,
0,
r#"
This is a code block:
```rs
fn foo() {}
```
"#
.unindent(),
);
let mut syntax_map = SyntaxMap::new();
syntax_map.set_language_registry(registry.clone());
syntax_map.reparse(markdown.clone(), &buffer);
assert_layers_for_range(
&syntax_map,
&buffer,
Point::new(3, 0)..Point::new(3, 0),
&[
"...(fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (code_fence_content) (fenced_code_block_delimiter...",
"...(function_item name: (identifier) parameters: (parameters) body: (block)...",
],
);
// Replace Rust with Ruby in code block.
let macro_name_range = range_for_text(&buffer, "rs");
buffer.edit([(macro_name_range, "ruby")]);
syntax_map.interpolate(&buffer);
syntax_map.reparse(markdown.clone(), &buffer);
assert_layers_for_range(
&syntax_map,
&buffer,
Point::new(3, 0)..Point::new(3, 0),
&[
"...(fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (code_fence_content) (fenced_code_block_delimiter...",
"...(call method: (identifier) arguments: (argument_list (call method: (identifier) arguments: (argument_list) block: (block)...",
],
);
// Replace Ruby with a language that hasn't been loaded yet.
let macro_name_range = range_for_text(&buffer, "ruby");
buffer.edit([(macro_name_range, "html")]);
syntax_map.interpolate(&buffer);
syntax_map.reparse(markdown.clone(), &buffer);
assert_layers_for_range(
&syntax_map,
&buffer,
Point::new(3, 0)..Point::new(3, 0),
&[
"...(fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (code_fence_content) (fenced_code_block_delimiter..."
],
);
assert!(syntax_map.contains_unknown_injections());
registry.add(Arc::new(html_lang()));
syntax_map.reparse(markdown.clone(), &buffer);
assert_layers_for_range(
&syntax_map,
&buffer,
Point::new(3, 0)..Point::new(3, 0),
&[
"...(fenced_code_block (fenced_code_block_delimiter) (info_string (language)) (code_fence_content) (fenced_code_block_delimiter...",
"(fragment (text))",
],
);
assert!(!syntax_map.contains_unknown_injections());
}
#[gpui::test]
fn test_typing_multiple_new_injections() {
let (buffer, syntax_map) = test_edit_sequence(
@@ -2122,16 +2362,14 @@ mod tests {
.zip(new_syntax_map.layers.iter())
{
assert_eq!(old_layer.range, new_layer.range);
let Some(old_tree) = old_layer.content.tree() else { continue };
let Some(new_tree) = new_layer.content.tree() else { continue };
let old_start_byte = old_layer.range.start.to_offset(old_buffer);
let new_start_byte = new_layer.range.start.to_offset(new_buffer);
let old_start_point = old_layer.range.start.to_point(old_buffer).to_ts_point();
let new_start_point = new_layer.range.start.to_point(new_buffer).to_ts_point();
let old_node = old_layer
.tree
.root_node_with_offset(old_start_byte, old_start_point);
let new_node = new_layer
.tree
.root_node_with_offset(new_start_byte, new_start_point);
let old_node = old_tree.root_node_with_offset(old_start_byte, old_start_point);
let new_node = new_tree.root_node_with_offset(new_start_byte, new_start_point);
check_node_edits(
old_layer.depth,
&old_layer.range,
@@ -2219,7 +2457,8 @@ mod tests {
registry.add(Arc::new(ruby_lang()));
registry.add(Arc::new(html_lang()));
registry.add(Arc::new(erb_lang()));
let language = registry.get_language(language_name).unwrap();
registry.add(Arc::new(markdown_lang()));
let language = registry.language_for_name(language_name).unwrap();
let mut buffer = Buffer::new(0, 0, Default::default());
let mut mutated_syntax_map = SyntaxMap::new();
@@ -2227,7 +2466,7 @@ mod tests {
mutated_syntax_map.reparse(language.clone(), &buffer);
for (i, marked_string) in steps.into_iter().enumerate() {
edit_buffer(&mut buffer, &marked_string.unindent());
buffer.edit_via_marked_text(&marked_string.unindent());
// Reparse the syntax map
mutated_syntax_map.interpolate(&buffer);
@@ -2357,6 +2596,26 @@ mod tests {
.unwrap()
}
fn markdown_lang() -> Language {
Language::new(
LanguageConfig {
name: "Markdown".into(),
path_suffixes: vec!["md".into()],
..Default::default()
},
Some(tree_sitter_markdown::language()),
)
.with_injection_query(
r#"
(fenced_code_block
(info_string
(language) @language)
(code_fence_content) @content)
"#,
)
.unwrap()
}
fn range_for_text(buffer: &Buffer, text: &str) -> Range<usize> {
let start = buffer.as_rope().to_string().find(text).unwrap();
start..start + text.len()
@@ -2417,52 +2676,6 @@ mod tests {
assert_eq!(actual_ranges, expected_ranges);
}
fn edit_buffer(buffer: &mut Buffer, marked_string: &str) {
let old_text = buffer.text();
let (new_text, mut ranges) = marked_text_ranges(marked_string, false);
if ranges.is_empty() {
ranges.push(0..new_text.len());
}
assert_eq!(
old_text[..ranges[0].start],
new_text[..ranges[0].start],
"invalid edit"
);
let mut delta = 0;
let mut edits = Vec::new();
let mut ranges = ranges.into_iter().peekable();
while let Some(inserted_range) = ranges.next() {
let new_start = inserted_range.start;
let old_start = (new_start as isize - delta) as usize;
let following_text = if let Some(next_range) = ranges.peek() {
&new_text[inserted_range.end..next_range.start]
} else {
&new_text[inserted_range.end..]
};
let inserted_len = inserted_range.len();
let deleted_len = old_text[old_start..]
.find(following_text)
.expect("invalid edit");
let old_range = old_start..old_start + deleted_len;
edits.push((old_range, new_text[inserted_range].to_string()));
delta += inserted_len as isize - deleted_len as isize;
}
assert_eq!(
old_text.len() as isize + delta,
new_text.len() as isize,
"invalid edit"
);
buffer.edit(edits);
}
pub fn string_contains_sequence(text: &str, parts: &[&str]) -> bool {
let mut last_part_end = 0;
for part in parts {

View File

@@ -3,6 +3,7 @@ name = "live_kit_client"
version = "0.1.0"
edition = "2021"
description = "Bindings to LiveKit Swift client SDK"
publish = false
[lib]
path = "src/live_kit_client.rs"

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