Compare commits

...

167 Commits

Author SHA1 Message Date
Joseph Lyons
1fe6356b89 zed 0.80.1 2023-03-31 12:42:59 -04:00
Antonio Scandurra
ef62a097ae Merge pull request #2333 from zed-industries/copilot-improvements
Fix several Copilot bugs
2023-03-31 12:36:11 -04:00
Mikayla Maki
b8397a36c1 Merge pull request #2316 from zed-industries/copilot
🚨 WIP 🚨 Copilot
2023-03-31 12:36:04 -04:00
Max Brunsfeld
017b16dc7b Merge pull request #2325 from zed-industries/tab-map-long-lines
Avoid slowdowns with long lines by skipping tab expansion beyond a certain column
2023-03-31 12:35:55 -04:00
Julia
a6cf8708ff Merge pull request #2332 from zed-industries/per-server-code-action-kinds
Allow each language adapter to provide their own code action kinds array
2023-03-31 12:35:13 -04:00
Joseph Lyons
a4b35821fd v0.80.x preview 2023-03-29 14:12:35 -04:00
Antonio Scandurra
056f4e914f Merge pull request #2327 from zed-industries/remove-unused-code
Delete unused code
2023-03-29 09:09:47 +02:00
Antonio Scandurra
a64296938d Delete unused code 2023-03-29 09:04:13 +02:00
Julia
35b2aceffb Merge pull request #2324 from zed-industries/download-node
Automatically download Node for Node based language servers
2023-03-28 12:10:24 -04:00
Julia
ee3ac9c344 Rename installation.rs -> github.rs now that is all it concerns 2023-03-28 11:51:09 -04:00
Julia
350f8ed304 Download the JSON LSP package instead of our own bundled binary 2023-03-28 11:48:00 -04:00
Julia
d4560fe321 Prevent deadlock when multiple languages attempt to install Node at once 2023-03-28 10:18:22 -04:00
Max Brunsfeld
c68c8462bb Merge pull request #2322 from zed-industries/project-panel-paste-infinite-loop
Fix infinite loop in ProjectPanel::paste when filename has multiple dots
2023-03-27 17:09:10 -07:00
Max Brunsfeld
17bc83d699 Fix infinite loop in ProjectPanel::paste when filename has multiple dots 2023-03-27 16:45:11 -07:00
Max Brunsfeld
e5d552ef97 Merge pull request #2321 from zed-industries/new-file-with-no-window
Make 'new file' action open a window when there are no windows open
2023-03-27 16:10:30 -07:00
Max Brunsfeld
4a2132bc91 Make 'new file' action open a window when there are no windows open 2023-03-27 16:05:00 -07:00
Joseph T. Lyons
e10338ed17 Merge pull request #2313 from zed-industries/unify-spelling-of-key-binding-to-be-two-words
Unify spelling of `key binding` to be two words
2023-03-27 15:58:12 -04:00
Petros Amoiridis
eb7c6028f4 Merge pull request #2320 from zed-industries/petros/z-349-make-restart-to-update-zed-look
Make "Restart to update Zed" look clickable
2023-03-27 20:19:06 +03:00
Julia
df4380b066 Download aarch64 or x64 Node binary according to system architecture 2023-03-27 11:05:17 -04:00
Petros Amoiridis
b153bf7118 Add a hovered style to lspStatus background 2023-03-27 14:26:56 +03:00
Petros Amoiridis
374b284a3d Run prettier on TS files 2023-03-27 14:26:05 +03:00
Julia
c72d33e029 Initial impl of NodeRuntime w/JSON borked and a deadlock :)
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-03-27 00:00:16 -04:00
Julia
1a2e509e35 Remove server_args from LspAdapter
Prepare to remove concept of a runtime from greater server startup code,
which is important for future language server extensibility

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-03-27 00:00:16 -04:00
Julia
ed442cfc8c Invoke npm from downloaded Node 2023-03-27 00:00:16 -04:00
Julia
edd6c85af7 Initial running of servers on downloaded Node 2023-03-27 00:00:16 -04:00
Julia
b579211861 Report if language server requires Node or not 2023-03-27 00:00:16 -04:00
Nathan Sobo
d89936e4a9 Merge pull request #2319 from zed-industries/copy-highlight-json
Add "editor: copy highlight json" command
2023-03-24 17:17:45 -06:00
Nathan Sobo
f0992e7d67 Trim empty tokens; copy selected range if non-empty 2023-03-24 17:10:50 -06:00
Nathan Sobo
3dfedd1b21 Merge adjacent chunks with the same highlight name in copied JSON 2023-03-24 16:52:00 -06:00
Nathan Sobo
195215f1e0 Add "editor: copy highlight json" command
Nate needs this to feed to Figma for highlighted code in designs.
2023-03-24 16:37:57 -06:00
Max Brunsfeld
c74f8eb9e3 Merge pull request #2258 from zed-industries/lsp-file-change-notifications
Implement `DidChangedWatchedFiles` LSP feature
2023-03-24 14:42:23 -07:00
Max Brunsfeld
455ffb17f1 Handle path changes and progress updates from all worker threads during initial scan 2023-03-24 14:35:18 -07:00
Max Brunsfeld
027def6800 Merge branch 'main' into lsp-file-change-notifications 2023-03-24 08:52:43 -07:00
Max Brunsfeld
a0e98ccc35 🎨 BackgroundScanner::run 2023-03-23 18:05:12 -07:00
Max Brunsfeld
89e99d2902 🎨 Don't store path changes statefully on the background scanner 2023-03-23 16:04:47 -07:00
Max Brunsfeld
3ff5aee4a1 Respect LSP servers watch glob patterns 2023-03-23 16:03:07 -07:00
Mikayla Maki
76b75b4b43 Merge pull request #2318 from zed-industries/fix-unknown-in-non-editor-buffers
changed language status bar item to only show on editors
2023-03-23 13:33:42 -07:00
Mikayla Maki
5db11c628b changed language status bar item to only show on editors 2023-03-23 13:29:23 -07:00
Mikayla Maki
5cad3d3a67 Merge pull request #2317 from zed-industries/fix-titlebar-right-spacing
Fix misaligned UI in the right titlebar
2023-03-23 13:16:40 -07:00
Mikayla Maki
bb5c2833a3 Aligned title bar items to the center and fixed left spacing on the sign in button
co-authored-by: max <max@zed.dev>
2023-03-23 13:08:31 -07:00
Petros Amoiridis
566a04ebca Merge pull request #2311 from zed-industries/petros/z-279-add-terminals-count
Add terminals count
2023-03-23 10:52:01 +02:00
Mikayla Maki
f9d3963dbb Merge pull request #2315 from zed-industries/fix-fold-indicator-offsets
Fix fold indicator offsets
2023-03-22 17:13:13 -07:00
Mikayla Maki
e87c3b6dd7 Update element.rs
remove spare parens
2023-03-22 17:05:54 -07:00
Mikayla Maki
e729c4ad4f Fix fold indicator offsets 2023-03-22 17:04:52 -07:00
Max Brunsfeld
361b7c3a0c Clear auto-indent requests when replacing a buffer's entire text 2023-03-22 15:10:16 -07:00
Max Brunsfeld
eaee5571a0 Use a more stable, readable serialization format for neovim-backed vim tests 2023-03-22 14:31:11 -07:00
Joseph Lyons
6de38f7410 v0.80.x dev 2023-03-22 16:33:06 -04:00
Julia
df553de363 Merge pull request #2314 from zed-industries/another-one
Remove another spot with a flag old npm does not like
2023-03-22 15:56:23 -04:00
Julia
4fc37cf982 Remove another spot with a flag old npm does not like 2023-03-22 15:40:51 -04:00
Joseph Lyons
9d88cd8842 Unify spelling of key binding to be two words 2023-03-22 13:34:12 -04:00
Petros Amoiridis
fd9eff3a78 Remove struct 2023-03-22 19:28:06 +02:00
Petros Amoiridis
bd1515cdd2 Only show count when we have terminals
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-03-22 18:23:41 +02:00
Joseph T. Lyons
bd85ef363f Merge pull request #2312 from zed-industries/unkown
Fix typo in "Unknown" language
2023-03-22 11:20:18 -04:00
Nathan Sobo
e017b99384 Fix typo 2023-03-22 09:13:10 -06:00
Petros Amoiridis
15406ff2d9 Remove comment 2023-03-22 16:31:42 +02:00
Petros Amoiridis
d5bb2d13b8 Introduce terminal button count
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-03-22 16:13:58 +02:00
Petros Amoiridis
aa7254167a Fix typo 2023-03-22 15:31:21 +02:00
Antonio Scandurra
005eb559ee Merge pull request #2310 from zed-industries/fix-sharing-status-indicator
Remove screen sharing indicator when call ends
2023-03-22 09:40:23 +01:00
Antonio Scandurra
7df798ded5 Remove screen sharing indicator when call ends
Previously, we would only remove it when the screen sharing stopped.
2023-03-22 09:32:27 +01:00
Max Brunsfeld
c1f53358ba Remove unnecessary Arc around background scanner's snapshot 2023-03-21 15:47:02 -07:00
Max Brunsfeld
f7b2713b77 Fix error in joining empty paths 2023-03-21 15:41:24 -07:00
Max Brunsfeld
5da2b123b5 Allow refreshing worktree entries while the initial scan is in-progress 2023-03-21 15:15:12 -07:00
Max Brunsfeld
b10b0dbd75 Only mutate background snapshot in the background scanner 2023-03-21 11:26:33 -07:00
Max Brunsfeld
d742c758bc Restructure communication from BackgroundScanner to LocalWorktree
The worktree no longer pulls the background snapshot from the background scanner.
Instead, the background scanner sends both snapshots to the worktree. Along with
these, it sends the path change sets.

Also, add randomized test coverage for the worktree UpdatedEntries events.
2023-03-21 11:26:13 -07:00
Max Brunsfeld
cbeb6e692d Move postage crate version specification to workspace Cargo.toml 2023-03-21 11:26:13 -07:00
Max Brunsfeld
d36b2a3129 🎨 Simplify some worktree methods
* Consolidate local worktree construction into one method
* Simplify remote worktree construction
* Reduce indirection around pulling worktree snapshots from the background
2023-03-21 11:26:13 -07:00
Max Brunsfeld
399f082415 Update wrong assertions after fixing missing event in FakeFs 2023-03-21 11:26:13 -07:00
Max Brunsfeld
51b093197d Add missing import in project tests 2023-03-21 11:26:13 -07:00
Max Brunsfeld
27ad6a57ce Tweak logging in worktree randomized test 2023-03-21 11:26:13 -07:00
Max Brunsfeld
c730dca3c5 Update worktree randomized test to use worktree's public interface and the fake fs 2023-03-21 11:26:13 -07:00
Max Brunsfeld
be5868e1c0 Conservatively report fs events that occurred during initial worktree scan
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2023-03-21 11:26:13 -07:00
Max Brunsfeld
61172c8478 Notify language servers of FS changes 2023-03-21 11:26:13 -07:00
Max Brunsfeld
9837a6e288 Add failing test for reporting FS change events to language servers 2023-03-21 11:26:13 -07:00
Antonio Scandurra
194c7a3af0 Merge pull request #2309 from zed-industries/suggestion-map
Introduce `DisplayMap::replace_suggestion`
2023-03-21 18:18:50 +01:00
Antonio Scandurra
2893c9bdb7 Don't move up/down by more rows than the requested ones
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-03-21 17:52:53 +01:00
Antonio Scandurra
f7cba4cec4 Make Suggestion fields public 2023-03-21 16:51:33 +01:00
Antonio Scandurra
ba3913df8c Expose a DisplayMap::replace_suggestion method 2023-03-21 16:41:54 +01:00
Antonio Scandurra
9c8732a355 Integrate SuggestionMap into the rest of DisplayMap 2023-03-21 16:39:02 +01:00
Antonio Scandurra
d1978a719b Add a version field to SuggestionSnapshot 2023-03-21 12:47:04 +01:00
Antonio Scandurra
3d165f705f Extract a SuggestionMap::randomly_mutate method 2023-03-21 11:51:06 +01:00
Antonio Scandurra
35830a0271 Implement SuggestionSnapshot::to_{fold,suggestion}_point 2023-03-21 11:39:29 +01:00
Antonio Scandurra
d448a5cb5c Implement SuggestionSnapshot::to_point 2023-03-21 11:28:36 +01:00
Antonio Scandurra
f829ce5641 Implement SuggestionSnapshot::to_offset 2023-03-21 11:28:33 +01:00
Antonio Scandurra
c0e124a55a Implement SuggestionSnapshot::text_summary_for_range 2023-03-21 11:28:30 +01:00
Antonio Scandurra
52a156aebe Implement SuggestionSnapshot::clip_point 2023-03-21 11:28:27 +01:00
Antonio Scandurra
ccb6196224 Implement SuggestionSnapshot::buffer_rows 2023-03-21 11:28:23 +01:00
Antonio Scandurra
1a9dbfa86a Add unit test to verify basic properties of the SuggestionMap 2023-03-21 08:29:33 +01:00
Joseph T. Lyons
8c0dd887ff Merge pull request #2299 from zed-industries/correct-verb-tense-in-default-settings
Correct verb tense in default settings
2023-03-20 14:50:46 -04:00
Antonio Scandurra
3edf83cb99 Implement SuggestionSnapshot::line_len
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-03-20 19:45:39 +01:00
Antonio Scandurra
f44549eb29 Enhance randomized test to verify SuggestionMap::{chunks,sync}
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-03-20 19:29:22 +01:00
Antonio Scandurra
4d6726ef39 WIP: Flesh out more of the suggestions randomized test 2023-03-20 18:06:24 +01:00
Petros Amoiridis
98ae69a61f Merge pull request #2282 from zed-industries/petros/z-283-make-pop-up-positioning-consistent
Consistent pop-up menu positions
2023-03-20 17:55:40 +02:00
Petros Amoiridis
24bbca7326 Position pane new, split, and dock context menus
Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com>
2023-03-20 17:49:33 +02:00
Nathan Sobo
d429ce0f62 Merge pull request #2308 from zed-industries/feedback-icon
Use an envelope as the feedback icon so we can use the speech bubble for discussions
2023-03-20 09:14:34 -06:00
Nathan Sobo
10e6c5b651 Use an envelope as the feedback icon
This makes room to use the speech bubble for discussion threads.
2023-03-20 09:04:30 -06:00
Antonio Scandurra
9970e5f60c Start on randomized test and add SuggestionMapSnapshot::chunks 2023-03-20 15:56:15 +01:00
Antonio Scandurra
fb48854e5a Simplify signature of SuggestionMap::replace 2023-03-20 14:00:14 +01:00
Antonio Scandurra
83051f1e86 Add SuggestionMap::replace 2023-03-20 13:50:14 +01:00
Antonio Scandurra
94a9e28e35 Start on SuggestionMap 2023-03-20 13:22:14 +01:00
Mikayla Maki
2a024a255f Merge pull request #2307 from zed-industries/fix-panic-in-editor-tab-content
Do UTF8-aware truncation on long item names in editor item
2023-03-18 15:49:06 -07:00
Mikayla Maki
436c59d8ef Do UTF8-aware truncation on long item names in editor item 2023-03-18 15:44:23 -07:00
Mikayla Maki
5356ec4730 Merge pull request #2287 from zed-industries/fix-fold-range-finding
Fix code folds with wraps
2023-03-17 17:18:56 -07:00
Mikayla Maki
5a3d5dff42 Make folds tab aware 2023-03-17 17:14:40 -07:00
Mikayla Maki
c39b4ac229 Fix boundary condition in buffer_line_len when at the end of a file
co-authored-by: max <max@zed.dev>
2023-03-17 16:56:44 -07:00
Mikayla Maki
5a1bbb96ba Merge pull request #2302 from zed-industries/fix-dispatch-path-panic
Align dispatch_keystroke with other uses of ancestors iterator
2023-03-17 16:34:00 -07:00
Mikayla Maki
b16e53a577 Merge pull request #2306 from zed-industries/fix-panic-with-multi-line-env
Parse user enviroment using null terminators instead of newlines
2023-03-17 16:18:05 -07:00
Mikayla Maki
109e17b4b2 Parse user enviroment using null terminators instead of newlines 2023-03-17 16:14:07 -07:00
Mikayla Maki
eba119b914 Fix fold tests with new representation
Switch UI code from using display rows to using buffer rows
Make folds only show up on lines with line layouts

co-authored-by: Max <max@zed.dev>
2023-03-17 16:00:22 -07:00
Max Brunsfeld
fc828971f1 collab 0.8.2 2023-03-17 15:00:31 -07:00
Max Brunsfeld
691383ca68 Merge pull request #2305 from zed-industries/faster-access-token-validation
Faster access token validation
2023-03-17 14:56:30 -07:00
Max Brunsfeld
b8e8363a72 Add logging and metric for time spent hashing auth tokens
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2023-03-17 14:32:13 -07:00
Max Brunsfeld
623133ffa0 Reduce scrypt work factor to speed up websocket authentication
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2023-03-17 14:31:39 -07:00
Max Brunsfeld
9633a4b527 Return a 400, not a 500 when token validation fails
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2023-03-17 13:56:12 -07:00
Mikayla Maki
459e320d79 Merge pull request #2303 from zed-industries/add-unknown-language
Add an 'Unknown' state for a mouse-driven way to select a file language
2023-03-17 11:41:40 -07:00
Mikayla Maki
04f52c3d50 Show active buffer language in all cases 2023-03-17 11:34:17 -07:00
Max Brunsfeld
26dae3c04e Lookup access tokens by id when authenticating a connection
This avoids the cost of hashing an access token multiple times,
to compare it to all known access tokens for a given user.

Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2023-03-17 11:13:50 -07:00
Mikayla Maki
578c69476d Align dispatch_keystroke with other uses of ancestors iterator and filter out non-existant views 2023-03-17 11:07:10 -07:00
Mikayla Maki
1125a168f4 Merge pull request #2301 from zed-industries/fix-file-finder-panic
Never call set_selected_index with an invalid index
2023-03-17 10:18:27 -07:00
Antonio Scandurra
d8758658e3 Merge pull request #2300 from zed-industries/ligatures
Allow customization of OpenType features
2023-03-17 17:34:17 +01:00
Mikayla Maki
f7f9b8cffe Never call set_selected_index with an invalid index 2023-03-17 09:32:01 -07:00
Antonio Scandurra
1af8f4be19 Deserialize Theme directly into the heap to avoid stack overflow
Co-Authored-By: Julia Risley <julia@zed.dev>
2023-03-17 15:58:52 +01:00
Antonio Scandurra
786d95b8c8 Avoid storing fonts::Features in TextStyle
We were only using it for debugging purposes and that was causing
the `Theme` struct to become too big to hold on the stack.

Co-Authored-By: Julia Risley <julia@zed.dev>
2023-03-17 15:12:02 +01:00
Antonio Scandurra
4d915f4530 Don't make fonts::Features Copy 2023-03-17 13:54:56 +01:00
Antonio Scandurra
989c9f0196 Mention calt: false in the default settings to disable ligatures 2023-03-17 13:48:34 +01:00
Antonio Scandurra
f9d793cb4a Honor more OpenType features 2023-03-17 13:40:00 +01:00
Petros Amoiridis
3bddf01962 Run prettier to format things 2023-03-17 14:05:49 +02:00
Petros Amoiridis
86ed5b8b83 Position contacts and user menus
Using the new approach for consistency
2023-03-17 14:05:46 +02:00
Antonio Scandurra
9181ac9872 Honor the calt font feature 2023-03-17 12:01:27 +01:00
Antonio Scandurra
76167ca65c Allow setting font features on TextStyle 2023-03-17 11:49:22 +01:00
Antonio Scandurra
7d13b00914 Allow setting font features on the terminal as well 2023-03-17 11:42:24 +01:00
Antonio Scandurra
b2c733baab WIP: Allow specifying font features in the editor
This just lays the foundation for threading through a `fonts::Features`
struct, but it's not used yet.
2023-03-17 09:51:36 +01:00
Joseph Lyons
6eb65eb989 Correct verb tense in default settings 2023-03-17 00:51:03 -04:00
Julia
3464961aa4 Merge pull request #2298 from zed-industries/fix-deadlock
Fix deadlock while initializing JSON language server
2023-03-16 17:41:54 -04:00
Julia
757f05042d Fix deadlock while initializing JSON language server
As it turns out both parking-lot and std's `RwLock` disallows taking
multiple read locks on the same thread

Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-03-16 17:32:51 -04:00
Max Brunsfeld
9633732db7 collab 0.8.1 2023-03-16 14:21:35 -07:00
Max Brunsfeld
e34d80cff4 Merge pull request #2296 from zed-industries/tx-serialization-retry-delay
Introduce a delay before retrying a transaction after a serialization failure
2023-03-16 14:16:20 -07:00
Mikayla Maki
f2492666d4 Merge pull request #2297 from zed-industries/fix-random-panics
WIP: Fix random panics
2023-03-16 13:20:54 -07:00
Joseph T. Lyons
b3b20e4c46 Merge pull request #2295 from zed-industries/swap-atom-keybinding-for-CollapseSelectedEntry
Swap atom keybinding for CollapseSelectedEntry
2023-03-16 16:12:28 -04:00
Max Brunsfeld
b9bc66aa9b Log the delay when retrying a transaction 2023-03-16 13:07:38 -07:00
Max Brunsfeld
35280f7d80 Introduce a delay before retrying a transaction after a serialization failure
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2023-03-16 13:07:38 -07:00
Mikayla Maki
6571555c4d Fix unwrap in git2 library causing panics in Zed 2023-03-16 13:06:23 -07:00
Joseph Lyons
a252c2a15b Swap atom keybinding for CollapseSelectedEntry
This is a temporary solution to this bug:

https://linear.app/zed-industries/issue/Z-340/the-project-panel-shouldnt-be-listening-to-key-commands-when-editing-a
2023-03-16 15:59:14 -04:00
Mikayla Maki
c3325430ca Fix divide by 0 in terminal
Fix fail to remove contact in contact list
2023-03-16 12:31:50 -07:00
Julia
1fbdea6a03 Merge pull request #2294 from zed-industries/remove-flag-old-npm-does-not-like
Remove CLI flag which old versions of NPM do not like
2023-03-16 13:11:45 -04:00
Julia
24dba2157f Remove CLI flag which old versions of NPM do not like
TODO: Bundle or version restrict Node
2023-03-16 13:07:09 -04:00
Mikayla Maki
c427a8c584 WIP - DEBUGGING 2023-03-16 08:41:19 -07:00
Antonio Scandurra
356b8c6980 Merge pull request #2293 from zed-industries/yaml-hover-bug
Fix hover popover rendering lots of `&emsp` for YAML
2023-03-16 16:15:14 +01:00
Antonio Scandurra
9498f02f2c Retrieve workspace configuration before initializing language server 2023-03-16 15:01:31 +01:00
Antonio Scandurra
f5a4c6a7c1 Provide editor.tabSize in workspace configuration for YAML
This fixes a bug that caused the hover popover to display lots of
`&emsp;` occurrences.
2023-03-16 10:46:55 +01:00
Antonio Scandurra
88e664bfd9 Add test for language registration and loading 2023-03-16 10:46:55 +01:00
Antonio Scandurra
8a685fa52a Use LanguageRegistry::workspace_configuration everywhere 2023-03-16 10:46:55 +01:00
Antonio Scandurra
4d52fc0d12 Remove available language only when it has loaded
This also ensures that, if you load the same language more than once,
a future that resolves to the language (or an error) is returned at
all times. Previously, we would only return it the first time the language
was loaded.
2023-03-16 10:46:55 +01:00
Antonio Scandurra
a8ac08f5bd Coalesce multiple RwLocks into one LanguageRegistryState struct 2023-03-16 10:46:55 +01:00
Antonio Scandurra
e30ea43a14 Include loaded languages when computing lsp workspace configuration 2023-03-16 10:46:55 +01:00
Antonio Scandurra
60d3fb48e2 Start computing workspace configuration more dynamically 2023-03-16 10:46:55 +01:00
Max Brunsfeld
ed9927b495 Merge pull request #2292 from zed-industries/restart-app
Make 'restart' action more reliable
2023-03-15 18:01:22 -07:00
Max Brunsfeld
d69868fa44 Make restart action more reliable 2023-03-15 17:44:50 -07:00
Julia
1ed3aedb16 Merge pull request #2291 from zed-industries/change-LSHandlerRank
Change `LSHandlerRank` to `Alternate`
2023-03-15 19:33:33 -04:00
Julia
905e2586e9 Change LSHandlerRank to Alternate 2023-03-15 19:18:39 -04:00
Max Brunsfeld
51eb53be0d Merge pull request #2290 from zed-industries/close-remote-projects-when-leaving-call
Close remote project windows when leaving a call
2023-03-15 15:34:51 -07:00
Max Brunsfeld
b34477458e Close remote project windows when leaving a call 2023-03-15 15:24:58 -07:00
Nathan Sobo
385dfe1661 Merge pull request #2289 from zed-industries/sort-language-names-case-agnostically
Sort language names case agnostically
2023-03-14 20:00:52 -06:00
Joseph Lyons
3c7237e600 Sort language names case agnostically 2023-03-14 21:45:17 -04:00
Nathan Sobo
44a2506c40 Merge pull request #2288 from zed-industries/cut-off-collaborator-avatars
Fix collaborator avatars being clipped and not centered
2023-03-14 19:39:01 -06:00
Max Brunsfeld
c4e7611d04 Fix collaborator avatars being clipped and not centered
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Julia Risley <julia@zed.dev>
2023-03-14 17:59:33 -07:00
Mikayla Maki
75bea91245 Convert code folding to be in terms of buffer points instead of display points
Co-authored-by: max <max@zed.dev>
2023-03-14 16:48:03 -07:00
Max Brunsfeld
828e9c1bb8 v0.79.x dev 2023-03-14 12:49:33 -07:00
255 changed files with 16211 additions and 2999 deletions

108
Cargo.lock generated
View File

@@ -518,6 +518,7 @@ dependencies = [
"menu",
"project",
"serde",
"serde_derive",
"serde_json",
"settings",
"smol",
@@ -1097,6 +1098,7 @@ dependencies = [
"ipc-channel",
"plist",
"serde",
"serde_derive",
]
[[package]]
@@ -1111,7 +1113,6 @@ dependencies = [
"futures 0.3.25",
"gpui",
"image",
"isahc",
"lazy_static",
"log",
"parking_lot 0.11.2",
@@ -1119,6 +1120,7 @@ dependencies = [
"rand 0.8.5",
"rpc",
"serde",
"serde_derive",
"settings",
"smol",
"sum_tree",
@@ -1188,7 +1190,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.8.0"
version = "0.8.2"
dependencies = [
"anyhow",
"async-tungstenite",
@@ -1228,6 +1230,7 @@ dependencies = [
"sea-orm",
"sea-query",
"serde",
"serde_derive",
"serde_json",
"settings",
"sha-1 0.9.8",
@@ -1269,6 +1272,7 @@ dependencies = [
"postage",
"project",
"serde",
"serde_derive",
"settings",
"theme",
"util",
@@ -1327,6 +1331,48 @@ dependencies = [
"theme",
]
[[package]]
name = "copilot"
version = "0.1.0"
dependencies = [
"anyhow",
"async-compression",
"async-tar",
"client",
"collections",
"context_menu",
"futures 0.3.25",
"gpui",
"language",
"log",
"lsp",
"node_runtime",
"serde",
"serde_derive",
"settings",
"smol",
"theme",
"util",
"workspace",
]
[[package]]
name = "copilot_button"
version = "0.1.0"
dependencies = [
"anyhow",
"context_menu",
"copilot",
"editor",
"futures 0.3.25",
"gpui",
"settings",
"smol",
"theme",
"util",
"workspace",
]
[[package]]
name = "core-foundation"
version = "0.9.3"
@@ -1739,6 +1785,7 @@ dependencies = [
"log",
"parking_lot 0.11.2",
"serde",
"serde_derive",
"smol",
"sqlez",
"sqlez_macros",
@@ -1926,6 +1973,7 @@ dependencies = [
"clock",
"collections",
"context_menu",
"copilot",
"ctor",
"db",
"drag_and_drop",
@@ -1947,6 +1995,7 @@ dependencies = [
"rand 0.8.5",
"rpc",
"serde",
"serde_derive",
"settings",
"smallvec",
"smol",
@@ -2100,6 +2149,7 @@ dependencies = [
"project",
"search",
"serde",
"serde_derive",
"settings",
"sysinfo",
"theme",
@@ -2296,6 +2346,7 @@ dependencies = [
"regex",
"rope",
"serde",
"serde_derive",
"serde_json",
"smol",
"tempfile",
@@ -2582,9 +2633,9 @@ dependencies = [
[[package]]
name = "glob"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "globset"
@@ -2664,8 +2715,10 @@ dependencies = [
"postage",
"rand 0.8.5",
"resvg",
"schemars",
"seahash",
"serde",
"serde_derive",
"serde_json",
"simplelog",
"smallvec",
@@ -3275,6 +3328,7 @@ dependencies = [
"regex",
"rpc",
"serde",
"serde_derive",
"serde_json",
"settings",
"similar",
@@ -3470,6 +3524,7 @@ dependencies = [
"parking_lot 0.11.2",
"postage",
"serde",
"serde_derive",
"serde_json",
"sha2 0.10.6",
"simplelog",
@@ -3490,6 +3545,7 @@ dependencies = [
"prost-types 0.8.0",
"reqwest",
"serde",
"serde_derive",
"sha2 0.10.6",
]
@@ -3530,6 +3586,7 @@ dependencies = [
"parking_lot 0.11.2",
"postage",
"serde",
"serde_derive",
"serde_json",
"smol",
"unindent",
@@ -3895,6 +3952,23 @@ dependencies = [
"memoffset 0.6.5",
]
[[package]]
name = "node_runtime"
version = "0.1.0"
dependencies = [
"anyhow",
"async-compression",
"async-tar",
"futures 0.3.25",
"gpui",
"parking_lot 0.11.2",
"serde",
"serde_derive",
"serde_json",
"smol",
"util",
]
[[package]]
name = "nom"
version = "7.1.1"
@@ -4449,6 +4523,7 @@ dependencies = [
"bincode",
"plugin_macros",
"serde",
"serde_derive",
]
[[package]]
@@ -4459,6 +4534,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"serde_derive",
"syn",
]
@@ -4470,6 +4546,7 @@ dependencies = [
"bincode",
"pollster",
"serde",
"serde_derive",
"serde_json",
"smol",
"wasi-common",
@@ -4607,12 +4684,15 @@ dependencies = [
"client",
"clock",
"collections",
"ctor",
"db",
"env_logger",
"fs",
"fsevent",
"futures 0.3.25",
"fuzzy",
"git",
"glob",
"gpui",
"ignore",
"language",
@@ -4627,6 +4707,7 @@ dependencies = [
"regex",
"rpc",
"serde",
"serde_derive",
"serde_json",
"settings",
"sha2 0.10.6",
@@ -5249,6 +5330,7 @@ dependencies = [
"rand 0.8.5",
"rsa",
"serde",
"serde_derive",
"smol",
"smol-timeout",
"tempdir",
@@ -5672,6 +5754,7 @@ dependencies = [
"postage",
"project",
"serde",
"serde_derive",
"serde_json",
"settings",
"smallvec",
@@ -5858,8 +5941,10 @@ dependencies = [
"gpui",
"json_comments",
"postage",
"pretty_assertions",
"schemars",
"serde",
"serde_derive",
"serde_json",
"serde_path_to_error",
"sqlez",
@@ -6480,6 +6565,7 @@ dependencies = [
"procinfo",
"rand 0.8.5",
"serde",
"serde_derive",
"settings",
"shellexpand",
"smallvec",
@@ -6511,6 +6597,7 @@ dependencies = [
"project",
"rand 0.8.5",
"serde",
"serde_derive",
"settings",
"shellexpand",
"smallvec",
@@ -6570,6 +6657,7 @@ dependencies = [
"indexmap",
"parking_lot 0.11.2",
"serde",
"serde_derive",
"serde_json",
"serde_path_to_error",
"toml",
@@ -7472,11 +7560,15 @@ dependencies = [
"dirs 3.0.2",
"futures 0.3.25",
"git2",
"isahc",
"lazy_static",
"log",
"rand 0.8.5",
"serde",
"serde_json",
"smol",
"tempdir",
"url",
]
[[package]]
@@ -7563,6 +7655,7 @@ dependencies = [
"project",
"search",
"serde",
"serde_derive",
"serde_json",
"settings",
"tokio",
@@ -8346,6 +8439,7 @@ dependencies = [
"postage",
"project",
"serde",
"serde_derive",
"serde_json",
"settings",
"smallvec",
@@ -8409,7 +8503,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "zed"
version = "0.78.0"
version = "0.80.1"
dependencies = [
"activity_indicator",
"anyhow",
@@ -8430,6 +8524,8 @@ dependencies = [
"collections",
"command_palette",
"context_menu",
"copilot",
"copilot_button",
"ctor",
"db",
"diagnostics",
@@ -8456,6 +8552,7 @@ dependencies = [
"libc",
"log",
"lsp",
"node_runtime",
"num_cpus",
"outline",
"parking_lot 0.11.2",
@@ -8472,6 +8569,7 @@ dependencies = [
"rust-embed",
"search",
"serde",
"serde_derive",
"serde_json",
"serde_path_to_error",
"settings",

View File

@@ -13,6 +13,8 @@ members = [
"crates/collections",
"crates/command_palette",
"crates/context_menu",
"crates/copilot",
"crates/copilot_button",
"crates/db",
"crates/diagnostics",
"crates/drag_and_drop",
@@ -35,6 +37,7 @@ members = [
"crates/lsp",
"crates/media",
"crates/menu",
"crates/node_runtime",
"crates/outline",
"crates/picker",
"crates/plugin",
@@ -68,8 +71,10 @@ resolver = "2"
[workspace.dependencies]
serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
rand = { version = "0.8" }
postage = { version = "0.4.1", features = ["futures-traits"] }
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" }

View File

@@ -0,0 +1,12 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.2926 3.48996C3.79162 3.79616 3.44871 4.26316 3.44871 4.93872C3.44871 5.75753 3.65302 6.19648 3.88658 6.43349C4.11948 6.66983 4.47018 6.79529 4.95638 6.79529C5.64158 6.79529 6.23176 6.65786 6.64548 6.37099C7.03216 6.10286 7.32149 5.66636 7.35698 4.91278C7.38386 4.34213 7.36863 3.96084 7.21748 3.68905C7.09721 3.47279 6.81682 3.2089 5.96976 3.11109C5.4731 3.05374 4.81346 3.17162 4.2926 3.48996ZM3.72539 2.5525C4.46348 2.10138 5.36842 1.93724 6.09436 2.02107C7.1336 2.14107 7.8142 2.51324 8.17039 3.15373C8.49569 3.73867 8.47238 4.43479 8.44743 4.96466C8.39736 6.02772 7.95809 6.7938 7.26541 7.27411C6.59976 7.73566 5.75982 7.89249 4.95638 7.89249C4.2936 7.89249 3.61755 7.71967 3.11095 7.20558C2.605 6.69216 2.35705 5.92853 2.35705 4.93872C2.35705 3.80566 2.96744 3.01576 3.72539 2.5525Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.69546 8.97734C7.02432 8.97734 7.29091 9.24528 7.29091 9.57581V10.8725C7.29091 11.203 7.02432 11.471 6.69546 11.471C6.3666 11.471 6.1 11.203 6.1 10.8725V9.57581C6.1 9.24528 6.3666 8.97734 6.69546 8.97734Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.45301 7.32072C2.56382 6.90477 2.81104 6.35118 3.40175 6.17048L3.74851 7.31556C3.74509 7.31822 3.73425 7.32798 3.71842 7.35038C3.68409 7.39897 3.64151 7.48723 3.6034 7.6303C3.52629 7.91973 3.49839 8.31081 3.4984 8.73318V10.8761C3.5122 10.9688 3.52011 11.0083 3.53501 11.0478C3.5474 11.0807 3.57295 11.1339 3.6523 11.2153C3.83266 11.4004 4.24428 11.6866 5.21016 12.1174C5.99398 12.467 6.35125 12.6243 6.68361 12.7078C6.99799 12.7869 7.30564 12.8031 7.99999 12.8031V14C7.31311 14 6.86876 13.9882 6.3946 13.869C5.95125 13.7575 5.49691 13.5549 4.78914 13.2391C4.76868 13.23 4.74801 13.2208 4.72712 13.2115C3.73729 12.77 3.14865 12.4092 2.80139 12.0527C2.61692 11.8634 2.49682 11.6721 2.42136 11.4719C2.35507 11.2961 2.33141 11.1302 2.31663 11.0266C2.31561 11.0194 2.31463 11.0126 2.31369 11.0061L2.30749 10.9632V8.73321C2.30748 8.28334 2.33457 7.76532 2.45301 7.32072Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.83439 7.54965C2.14812 7.21281 2.52306 6.88008 2.81315 6.70729L3.42031 7.737C3.27036 7.82631 2.98468 8.06607 2.7038 8.36764C2.43565 8.65553 2.2592 8.90729 2.19783 9.04784C2.18425 9.16608 2.18871 9.38528 2.22654 9.6452C2.26959 9.94104 2.33715 10.1608 2.37974 10.2387L2.42237 10.3167L2.44057 10.4038C2.46806 10.5353 2.60072 10.7284 2.96139 10.9852C3.24332 11.1859 3.57562 11.3661 3.93098 11.5588C4.00968 11.6015 4.0895 11.6448 4.17017 11.689C4.56251 11.8768 5.17152 12.1512 5.7408 12.3785C6.02948 12.4938 6.30016 12.5938 6.5233 12.664C6.63493 12.6991 6.72826 12.7247 6.802 12.7411C6.87402 12.7571 6.90715 12.7597 6.91166 12.76L6.91213 13.957C6.68654 13.957 6.40667 13.8815 6.16744 13.8062C5.90465 13.7235 5.60351 13.6116 5.30115 13.4909C4.69547 13.2491 4.05361 12.9594 3.64268 12.7623L3.62786 12.7552L3.61345 12.7473C3.54294 12.7085 3.46868 12.6683 3.39187 12.6268C3.03384 12.433 2.62042 12.2092 2.27302 11.9619C1.88328 11.6844 1.44476 11.2894 1.29544 10.735C1.17095 10.4701 1.09192 10.1191 1.04817 9.81844C0.999401 9.48332 0.975841 9.08189 1.03513 8.7778L1.04265 8.73927L1.05511 8.70206C1.18745 8.30673 1.53258 7.87368 1.83439 7.54965Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.59243 10.4347V8.41995H2.78334V10.4347H1.59243Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.7074 3.48996C12.2084 3.79616 12.5513 4.26316 12.5513 4.93872C12.5513 5.75753 12.347 6.19648 12.1134 6.43349C11.8805 6.66983 11.5298 6.79529 11.0436 6.79529C10.3584 6.79529 9.76824 6.65786 9.35452 6.37099C8.96784 6.10286 8.67851 5.66636 8.64302 4.91278C8.61614 4.34213 8.63137 3.96084 8.78252 3.68905C8.90279 3.47279 9.18318 3.2089 10.0302 3.11109C10.5269 3.05374 11.1865 3.17162 11.7074 3.48996ZM12.2746 2.5525C11.5365 2.10138 10.6316 1.93724 9.90564 2.02107C8.8664 2.14107 8.1858 2.51324 7.82961 3.15373C7.50431 3.73867 7.52762 4.43479 7.55258 4.96466C7.60264 6.02772 8.04191 6.7938 8.73459 7.27411C9.40024 7.73566 10.2402 7.89249 11.0436 7.89249C11.7064 7.89249 12.3824 7.71967 12.889 7.20558C13.395 6.69216 13.643 5.92853 13.643 4.93872C13.643 3.80566 13.0326 3.01576 12.2746 2.5525Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.30454 8.97734C8.97568 8.97734 8.70909 9.24528 8.70909 9.57581V10.8725C8.70909 11.203 8.97568 11.471 9.30454 11.471C9.6334 11.471 9.9 11.203 9.9 10.8725V9.57581C9.9 9.24528 9.6334 8.97734 9.30454 8.97734Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.547 7.32072C13.4362 6.90477 13.189 6.35118 12.5982 6.17048L12.2515 7.31556C12.2549 7.31822 12.2658 7.32798 12.2816 7.35038C12.3159 7.39897 12.3585 7.48723 12.3966 7.6303C12.4737 7.91973 12.5016 8.31081 12.5016 8.73318V10.8761C12.4878 10.9688 12.4799 11.0083 12.465 11.0478C12.4526 11.0807 12.427 11.1339 12.3477 11.2153C12.1673 11.4004 11.7557 11.6866 10.7898 12.1174C10.006 12.467 9.64875 12.6243 9.31639 12.7078C9.00201 12.7869 8.69433 12.8031 7.99999 12.8031V14C8.68686 14 9.13124 13.9882 9.6054 13.869C10.0488 13.7575 10.5031 13.5549 11.2109 13.2391C11.2313 13.23 11.252 13.2208 11.2729 13.2115C12.2627 12.77 12.8513 12.4092 13.1986 12.0527C13.3831 11.8634 13.5032 11.6721 13.5786 11.4719C13.6449 11.2961 13.6686 11.1302 13.6834 11.0266C13.6844 11.0194 13.6854 11.0126 13.6863 11.0061L13.6925 10.9632V8.73321C13.6925 8.28334 13.6654 7.76532 13.547 7.32072Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1656 7.54965C13.8519 7.21281 13.4769 6.88008 13.1868 6.70729L12.5797 7.737C12.7296 7.82631 13.0153 8.06607 13.2962 8.36764C13.5643 8.65553 13.7408 8.90729 13.8022 9.04784C13.8158 9.16608 13.8113 9.38528 13.7735 9.6452C13.7304 9.94104 13.6628 10.1608 13.6203 10.2387L13.5776 10.3167L13.5594 10.4038C13.5319 10.5353 13.3993 10.7284 13.0386 10.9852C12.7567 11.1859 12.4244 11.3661 12.069 11.5588C11.9903 11.6015 11.9105 11.6448 11.8298 11.689C11.4375 11.8768 10.8285 12.1512 10.2592 12.3785C9.97052 12.4938 9.69984 12.5938 9.4767 12.664C9.36507 12.6991 9.27174 12.7247 9.198 12.7411C9.12598 12.7571 9.09285 12.7597 9.08834 12.76L9.08787 13.957C9.31345 13.957 9.59333 13.8815 9.83256 13.8062C10.0953 13.7235 10.3965 13.6116 10.6989 13.4909C11.3045 13.2491 11.9464 12.9594 12.3573 12.7623L12.3721 12.7552L12.3865 12.7473C12.4571 12.7085 12.5313 12.6683 12.6081 12.6268C12.9662 12.433 13.3796 12.2092 13.727 11.9619C14.1167 11.6844 14.5552 11.2894 14.7046 10.735C14.829 10.4701 14.9081 10.1191 14.9518 9.81844C15.0006 9.48332 15.0242 9.08189 14.9649 8.7778L14.9574 8.73927L14.9449 8.70206C14.8126 8.30673 14.4674 7.87368 14.1656 7.54965Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4076 10.4347V8.41995H13.2167V10.4347H14.4076Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1,9 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.5">
<path d="M2.38084 5.44737C2.42754 5.92437 2.54311 6.33779 2.72903 6.68417C2.70043 6.72678 2.67414 6.77043 2.64995 6.81463C2.39071 6.9994 2.09192 7.27314 1.83439 7.54965C1.53258 7.87368 1.18745 8.30673 1.05511 8.70206L1.04265 8.73927L1.03513 8.7778C0.975841 9.08189 0.999401 9.48332 1.04817 9.81844C1.09192 10.1191 1.17095 10.4701 1.29544 10.735C1.44476 11.2894 1.88328 11.6844 2.27302 11.9619C2.6204 12.2092 3.03378 12.4329 3.39179 12.6267C3.4686 12.6683 3.54294 12.7085 3.61345 12.7473L3.62786 12.7552L3.64268 12.7623C4.05361 12.9594 4.69547 13.2491 5.30115 13.4909C5.60351 13.6116 5.90465 13.7235 6.16744 13.8062C6.39236 13.877 6.6532 13.948 6.87108 13.9562C7.19351 13.9948 7.54309 14 7.99999 14C8.45688 14 8.80648 13.9948 9.12892 13.9562C9.34679 13.948 9.60764 13.877 9.83256 13.8062C10.0953 13.7235 10.3965 13.6116 10.6989 13.4909C11.0041 13.369 11.3186 13.235 11.6081 13.1067L10.5467 12.2257C9.92791 12.5006 9.61228 12.6334 9.31639 12.7078C9.00201 12.7869 8.69433 12.8031 7.99999 12.8031C7.30564 12.8031 6.99799 12.7869 6.68361 12.7078C6.35125 12.6243 5.99398 12.467 5.21016 12.1174C4.24428 11.6866 3.83266 11.4004 3.6523 11.2153C3.57295 11.1339 3.5474 11.0807 3.53501 11.0478C3.52011 11.0083 3.5122 10.9688 3.4984 10.8761V8.73318C3.49839 8.31081 3.52629 7.91973 3.6034 7.6303C3.60757 7.61463 3.6118 7.59961 3.61607 7.58523C4.02831 7.80894 4.49555 7.89249 4.95638 7.89249C5.07488 7.89249 5.19417 7.88908 5.31358 7.88178L2.38084 5.44737Z" fill="white"/>
<path d="M6.63684 8.9802C6.3355 9.00979 6.1 9.26516 6.1 9.57581V10.8725C6.1 11.203 6.3666 11.471 6.69546 11.471C7.02432 11.471 7.29091 11.203 7.29091 10.8725V9.57581C7.29091 9.55736 7.29008 9.53911 7.28846 9.52109L6.63684 8.9802Z" fill="white"/>
<path d="M8.70909 10.7003V10.8725C8.70909 11.203 8.97568 11.471 9.30454 11.471C9.39795 11.471 9.48633 11.4493 9.56501 11.4108L8.70909 10.7003Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.09436 2.02107C5.38898 1.93962 4.51459 2.09228 3.78849 2.51486L4.71538 3.28426C5.1409 3.1227 5.6004 3.06844 5.96976 3.11109C6.81682 3.2089 7.09721 3.4728 7.21748 3.68905C7.36863 3.96084 7.38386 4.34213 7.35698 4.91278C7.34816 5.1 7.32368 5.26765 7.28592 5.41801L10.1885 7.82737C10.4719 7.87295 10.76 7.89249 11.0436 7.89249C11.5044 7.89249 11.9717 7.80894 12.3839 7.58523C12.3882 7.59961 12.3924 7.61463 12.3966 7.6303C12.4737 7.91973 12.5016 8.31081 12.5016 8.73318V9.74745L14.4065 11.3287C14.5379 11.1547 14.6446 10.9577 14.7046 10.735C14.829 10.4701 14.9081 10.1191 14.9518 9.81844C15.0006 9.48332 15.0242 9.08189 14.9649 8.7778L14.9574 8.73927L14.9449 8.70206C14.8126 8.30673 14.4674 7.87368 14.1656 7.54965C13.9081 7.27314 13.6093 6.9994 13.35 6.81463C13.3259 6.77043 13.2996 6.72678 13.271 6.68417C13.5201 6.21998 13.643 5.63537 13.643 4.93872C13.643 3.80567 13.0326 3.01576 12.2746 2.5525C11.5365 2.10138 10.6316 1.93724 9.90564 2.02107C9.01315 2.12413 8.38516 2.41316 8 2.89841C7.61484 2.41316 6.98685 2.12413 6.09436 2.02107ZM11.7074 3.48996C12.2084 3.79616 12.5513 4.26316 12.5513 4.93872C12.5513 5.75753 12.347 6.19648 12.1134 6.43349C11.8805 6.66983 11.5298 6.79529 11.0436 6.79529C10.3584 6.79529 9.76824 6.65786 9.35452 6.37099C8.96784 6.10286 8.67851 5.66636 8.64302 4.91278C8.61614 4.34213 8.63137 3.96084 8.78252 3.68905C8.90279 3.4728 9.18318 3.2089 10.0302 3.11109C10.5269 3.05374 11.1865 3.17162 11.7074 3.48996Z" fill="white"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1225 13.809C14.0341 13.9146 13.877 13.9289 13.7711 13.8409L1.1931 3.40021C1.08658 3.31178 1.0722 3.15362 1.16103 3.04743L1.87751 2.19101C1.96587 2.0854 2.12299 2.07112 2.22894 2.15906L14.8069 12.5998C14.9134 12.6882 14.9278 12.8464 14.839 12.9526L14.1225 13.809Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.5">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.09436 2.02107C5.36842 1.93724 4.46348 2.10138 3.72539 2.5525C2.96744 3.01576 2.35705 3.80567 2.35705 4.93872C2.35705 5.63537 2.47988 6.21998 2.72903 6.68417C2.70043 6.72678 2.67414 6.77043 2.64995 6.81463C2.39071 6.9994 2.09192 7.27314 1.83439 7.54965C1.53258 7.87368 1.18745 8.30673 1.05511 8.70206L1.04265 8.73927L1.03513 8.7778C0.975841 9.08189 0.999401 9.48332 1.04817 9.81844C1.09192 10.1191 1.17095 10.4701 1.29544 10.735C1.44476 11.2894 1.88328 11.6844 2.27302 11.9619C2.6204 12.2092 3.03378 12.4329 3.39179 12.6267C3.4686 12.6683 3.54294 12.7085 3.61345 12.7473L3.62786 12.7552L3.64268 12.7623C4.05361 12.9594 4.69547 13.2491 5.30115 13.4909C5.60351 13.6116 5.90465 13.7235 6.16744 13.8062C6.39236 13.877 6.6532 13.948 6.87108 13.9562C7.19351 13.9948 7.54309 14 7.99999 14C8.01293 14 8.02579 14 8.03857 14C7.97904 13.903 7.99191 13.7743 8.07655 13.6911L9.0197 12.7639C8.77857 12.7952 8.48273 12.8031 7.99999 12.8031C7.30564 12.8031 6.99799 12.7869 6.68361 12.7078C6.35125 12.6243 5.99398 12.467 5.21016 12.1174C4.24428 11.6866 3.83266 11.4004 3.6523 11.2153C3.57295 11.1339 3.5474 11.0807 3.53501 11.0478C3.52011 11.0083 3.5122 10.9688 3.4984 10.8761V8.73318C3.49839 8.31081 3.52629 7.91973 3.6034 7.6303C3.60757 7.61463 3.6118 7.59961 3.61607 7.58523C4.02831 7.80894 4.49555 7.89249 4.95638 7.89249C5.75982 7.89249 6.59976 7.73566 7.26541 7.27411C7.55937 7.07027 7.8077 6.81497 8 6.50734C8.1923 6.81497 8.44063 7.07027 8.73459 7.27411C9.40024 7.73566 10.2402 7.89249 11.0436 7.89249C11.5044 7.89249 11.9717 7.80894 12.3839 7.58523C12.3882 7.59961 12.3924 7.61463 12.3966 7.6303C12.4737 7.91973 12.5016 8.31081 12.5016 8.73318V9.34082L14.1266 7.7433C14.169 7.70159 14.2225 7.67811 14.2775 7.67276C14.2398 7.63028 14.2024 7.58915 14.1656 7.54965C13.9081 7.27314 13.6093 6.9994 13.35 6.81463C13.3259 6.77043 13.2996 6.72678 13.271 6.68417C13.5201 6.21998 13.643 5.63537 13.643 4.93872C13.643 3.80567 13.0326 3.01576 12.2746 2.5525C11.5365 2.10138 10.6316 1.93724 9.90564 2.02107C9.01315 2.12413 8.38516 2.41316 8 2.89841C7.61484 2.41316 6.98685 2.12413 6.09436 2.02107ZM3.44871 4.93872C3.44871 4.26316 3.79162 3.79616 4.2926 3.48996C4.81346 3.17162 5.4731 3.05374 5.96976 3.11109C6.81682 3.2089 7.09721 3.4728 7.21748 3.68905C7.36863 3.96084 7.38386 4.34213 7.35698 4.91278C7.32149 5.66636 7.03216 6.10286 6.64548 6.37099C6.23176 6.65786 5.64158 6.79529 4.95638 6.79529C4.47018 6.79529 4.11948 6.66983 3.88658 6.43349C3.65302 6.19648 3.44871 5.75753 3.44871 4.93872ZM12.5513 4.93872C12.5513 4.26316 12.2084 3.79616 11.7074 3.48996C11.1865 3.17162 10.5269 3.05374 10.0302 3.11109C9.18318 3.2089 8.90279 3.4728 8.78252 3.68905C8.63137 3.96084 8.61614 4.34213 8.64302 4.91278C8.67851 5.66636 8.96784 6.10286 9.35452 6.37099C9.76824 6.65786 10.3584 6.79529 11.0436 6.79529C11.5298 6.79529 11.8805 6.66983 12.1134 6.43349C12.347 6.19648 12.5513 5.75753 12.5513 4.93872Z" fill="white"/>
<path d="M7.29091 9.57581C7.29091 9.24528 7.02432 8.97734 6.69546 8.97734C6.3666 8.97734 6.1 9.24528 6.1 9.57581V10.8725C6.1 11.203 6.3666 11.471 6.69546 11.471C7.02432 11.471 7.29091 11.203 7.29091 10.8725V9.57581Z" fill="white"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.6668 14.9102C13.7644 15.0078 13.9227 15.0078 14.0203 14.9102L14.908 14.0224C15.0056 13.9248 15.0056 13.7665 14.908 13.6688L13.2229 11.9836L14.908 10.2983C15.0057 10.2007 15.0057 10.0424 14.908 9.94474L14.0203 9.05695C13.9227 8.95931 13.7644 8.95931 13.6668 9.05695L11.9817 10.7422L10.2966 9.05693C10.199 8.95929 10.0407 8.95929 9.94306 9.05693L9.05535 9.94473C8.95773 10.0424 8.95773 10.2007 9.05535 10.2983L10.7405 11.9836L9.05537 13.6688C8.95775 13.7665 8.95775 13.9248 9.05537 14.0224L9.94308 14.9102C10.0407 15.0079 10.199 15.0079 10.2966 14.9102L11.9817 13.225L13.6668 14.9102Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M3.44872 4.93872C3.44872 4.26316 3.79163 3.79616 4.29261 3.48996C4.81346 3.17162 5.47311 3.05374 5.96976 3.11109C6.81683 3.2089 7.09722 3.4728 7.21749 3.68905C7.36864 3.96084 7.38387 4.34213 7.35699 4.91278C7.3215 5.66636 7.03217 6.10286 6.64549 6.37099C6.23177 6.65786 5.64159 6.79529 4.95639 6.79529C4.47019 6.79529 4.11949 6.66983 3.88658 6.43349C3.65303 6.19648 3.44872 5.75753 3.44872 4.93872ZM6.09437 2.02107C5.36843 1.93724 4.46349 2.10138 3.7254 2.5525C2.96745 3.01576 2.35706 3.80567 2.35706 4.93872C2.35706 5.63537 2.47988 6.21998 2.72904 6.68417C2.70044 6.72678 2.67415 6.77043 2.64996 6.81463C2.39072 6.9994 2.09193 7.27314 1.83439 7.54965C1.53259 7.87368 1.18745 8.30673 1.05511 8.70206L1.04266 8.73927L1.03514 8.7778C0.975849 9.08189 0.999409 9.48332 1.04818 9.81844C1.09193 10.1191 1.17096 10.4701 1.29545 10.735C1.44476 11.2894 1.88329 11.6844 2.27303 11.9619C2.6204 12.2092 3.03379 12.4329 3.3918 12.6267L3.39183 12.6267L3.39185 12.6267L3.39188 12.6268C3.46869 12.6683 3.54295 12.7085 3.61346 12.7473L3.62787 12.7552L3.64269 12.7623C4.05362 12.9594 4.69548 13.2491 5.30115 13.4909C5.60352 13.6116 5.90466 13.7235 6.16745 13.8062C6.39237 13.877 6.65321 13.948 6.87108 13.9562C7.19351 13.9948 7.5431 14 8 14C8.45052 14 8.79672 13.9949 9.11543 13.9578C9.04001 13.6509 9.00001 13.3301 9.00001 13C9.00001 12.9213 9.00229 12.8431 9.00677 12.7656C8.76798 12.7955 8.47414 12.8031 8 12.8031C7.30565 12.8031 6.998 12.7869 6.68362 12.7078C6.35125 12.6243 5.99399 12.467 5.21017 12.1174C4.24429 11.6866 3.83267 11.4004 3.6523 11.2153C3.57296 11.1339 3.54741 11.0807 3.53502 11.0478C3.52011 11.0083 3.51221 10.9688 3.4984 10.8761V8.73318C3.49839 8.31081 3.5263 7.91973 3.6034 7.6303C3.60758 7.61463 3.61181 7.59961 3.61608 7.58523C4.02832 7.80894 4.49556 7.89249 4.95639 7.89249C5.75982 7.89249 6.59977 7.73566 7.26541 7.27411C7.55938 7.07027 7.8077 6.81497 8.00001 6.50734C8.19231 6.81497 8.44064 7.07027 8.7346 7.27411C9.40025 7.73566 10.2402 7.89249 11.0436 7.89249C11.5045 7.89249 11.9717 7.80894 12.3839 7.58523C12.3882 7.59961 12.3924 7.61463 12.3966 7.6303C12.4737 7.91973 12.5016 8.31081 12.5016 8.73318V9.03072C12.6649 9.01043 12.8312 8.99997 13 8.99997C13.7226 8.99997 14.4004 9.19157 14.9855 9.52673C15.0073 9.26739 15.0077 8.99725 14.9649 8.7778L14.9574 8.73927L14.9449 8.70206C14.8126 8.30673 14.4674 7.87368 14.1656 7.54965C13.9081 7.27314 13.6093 6.9994 13.3501 6.81463C13.3259 6.77043 13.2996 6.72678 13.271 6.68417C13.5201 6.21998 13.643 5.63537 13.643 4.93872C13.643 3.80567 13.0326 3.01576 12.2746 2.5525C11.5365 2.10138 10.6316 1.93724 9.90565 2.02107C9.01315 2.12413 8.38517 2.41316 8.00001 2.89841C7.61485 2.41316 6.98686 2.12413 6.09437 2.02107ZM9.9 10.4719V9.57581C9.9 9.24528 9.63341 8.97734 9.30455 8.97734C8.97569 8.97734 8.7091 9.24528 8.7091 9.57581V10.8725C8.7091 11.2024 8.97466 11.4699 9.30265 11.471C9.45294 11.1079 9.65515 10.7718 9.9 10.4719ZM7.29092 9.57581C7.29092 9.24528 7.02433 8.97734 6.69547 8.97734C6.36661 8.97734 6.10001 9.24528 6.10001 9.57581V10.8725C6.10001 11.203 6.36661 11.471 6.69547 11.471C7.02433 11.471 7.29092 11.203 7.29092 10.8725V9.57581ZM12.5513 4.93872C12.5513 4.26316 12.2084 3.79616 11.7074 3.48996C11.1866 3.17162 10.5269 3.05374 10.0303 3.11109C9.18318 3.2089 8.90279 3.4728 8.78253 3.68905C8.63138 3.96084 8.61615 4.34213 8.64303 4.91278C8.67852 5.66636 8.96785 6.10286 9.35453 6.37099C9.76825 6.65786 10.3584 6.79529 11.0436 6.79529C11.5298 6.79529 11.8805 6.66983 12.1134 6.43349C12.347 6.19648 12.5513 5.75753 12.5513 4.93872Z" fill="white"/>
<circle cx="13" cy="13" r="3" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9083 3.19699L7.99999 10.3949L0.0916311 3.1969C0.346537 2.49164 1.10447 1.98018 2 1.98018H14C14.8943 1.98018 15.653 2.49168 15.9083 3.19699ZM16 4.7153L12.1526 8.21715L16 11.688V4.7153ZM8.52024 11.5232L11.4199 8.88404L15.9081 12.933C15.6528 13.6378 14.8941 14.1501 14 14.1501H2C1.10461 14.1501 0.346779 13.6378 0.0917535 12.9331L4.58012 8.88404L7.47975 11.5232L7.99999 11.9967L8.52024 11.5232ZM3.84742 8.21715L0 4.71532V11.688L3.84742 8.21715Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 614 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1.24em" height="1em" viewBox="0 0 256 208"><path d="M205.28 31.36c14.096 14.88 20.016 35.2 22.512 63.68c6.626 0 12.805 1.47 16.976 7.152l7.792 10.56A17.548 17.548 0 0 1 256 123.2v28.688c-.008 3.704-1.843 7.315-4.832 9.504C215.885 187.222 172.35 208 128 208c-49.066 0-98.19-28.273-123.168-46.608c-2.989-2.189-4.825-5.8-4.832-9.504V123.2c0-3.776 1.2-7.424 3.424-10.464l7.792-10.544c4.173-5.657 10.38-7.152 16.992-7.152c2.496-28.48 8.4-48.8 22.512-63.68C77.331 3.165 112.567.06 127.552 0H128c14.72 0 50.4 2.88 77.28 31.36Zm-77.264 47.376c-3.04 0-6.544.176-10.272.544c-1.312 4.896-3.248 9.312-6.08 12.128c-11.2 11.2-24.704 12.928-31.936 12.928c-6.802 0-13.927-1.42-19.744-5.088c-5.502 1.808-10.786 4.415-11.136 10.912c-.586 12.28-.637 24.55-.688 36.824c-.026 6.16-.05 12.322-.144 18.488c.024 3.579 2.182 6.903 5.44 8.384C79.936 185.92 104.976 192 128.016 192c23.008 0 48.048-6.08 74.512-18.144c3.258-1.48 5.415-4.805 5.44-8.384c.317-18.418.062-36.912-.816-55.312h.016c-.342-6.534-5.648-9.098-11.168-10.912c-5.82 3.652-12.927 5.088-19.728 5.088c-7.232 0-20.72-1.728-31.936-12.928c-2.832-2.816-4.768-7.232-6.08-12.128a106.26 106.26 0 0 0-10.24-.544Zm-26.941 43.93c5.748 0 10.408 4.66 10.408 10.409v19.183c0 5.749-4.66 10.409-10.408 10.409c-5.748 0-10.408-4.66-10.408-10.409v-19.183c0-5.748 4.66-10.408 10.408-10.408Zm53.333 0c5.749 0 10.409 4.66 10.409 10.409v19.183c0 5.749-4.66 10.409-10.409 10.409c-5.748 0-10.408-4.66-10.408-10.409v-19.183c0-5.748 4.66-10.408 10.408-10.408ZM81.44 28.32c-11.2 1.12-20.64 4.8-25.44 9.92c-10.4 11.36-8.16 40.16-2.24 46.24c4.32 4.32 12.48 7.2 21.28 7.2c6.72 0 19.52-1.44 30.08-12.16c4.64-4.48 7.52-15.68 7.2-27.04c-.32-9.12-2.88-16.64-6.72-19.84c-4.16-3.68-13.6-5.28-24.16-4.32Zm68.96 4.32c-3.84 3.2-6.4 10.72-6.72 19.84c-.32 11.36 2.56 22.56 7.2 27.04c10.56 10.72 23.36 12.16 30.08 12.16c8.8 0 16.96-2.88 21.28-7.2c5.92-6.08 8.16-34.88-2.24-46.24c-4.8-5.12-14.24-8.8-25.44-9.92c-10.56-.96-20 .64-24.16 4.32ZM128 56c-2.56 0-5.6.16-8.96.48c.32 1.76.48 3.68.64 5.76c0 1.44 0 2.88-.16 4.48c3.2-.32 5.92-.32 8.48-.32c2.56 0 5.28 0 8.48.32c-.16-1.6-.16-3.04-.16-4.48c.16-2.08.32-4 .64-5.76c-3.36-.32-6.4-.48-8.96-.48Z"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,5 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.5 1H7.5H8.75C8.88807 1 9 1.11193 9 1.25V4.5" stroke="#838994" stroke-linecap="round"/>
<path d="M3.64645 5.64645C3.45118 5.84171 3.45118 6.15829 3.64645 6.35355C3.84171 6.54882 4.15829 6.54882 4.35355 6.35355L3.64645 5.64645ZM8.64645 0.646447L3.64645 5.64645L4.35355 6.35355L9.35355 1.35355L8.64645 0.646447Z" fill="#838994"/>
<path d="M7.5 6.5V9C7.5 9.27614 7.27614 9.5 7 9.5H1C0.723858 9.5 0.5 9.27614 0.5 9V3C0.5 2.72386 0.723858 2.5 1 2.5H3.5" stroke="#838994" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 605 B

View File

@@ -0,0 +1,14 @@
<svg width="93" height="32" viewBox="0 0 93 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.03996 7.04962C8.00936 7.67635 7.30396 8.63219 7.30396 10.0149C7.30396 11.6908 7.72425 12.5893 8.2047 13.0744C8.68381 13.5581 9.40526 13.8149 10.4054 13.8149C11.815 13.8149 13.0291 13.5336 13.8802 12.9464C14.6756 12.3977 15.2708 11.5042 15.3438 9.96182C15.3991 8.79382 15.3678 8.01341 15.0568 7.45711C14.8094 7.01449 14.2326 6.47436 12.4901 6.27416C11.4684 6.15678 10.1114 6.39804 9.03996 7.04962ZM7.87312 5.13084C9.39147 4.2075 11.2531 3.87155 12.7464 4.04312C14.8843 4.28874 16.2844 5.05049 17.0171 6.36142C17.6863 7.55867 17.6384 8.98348 17.587 10.068C17.484 12.2439 16.5804 13.8118 15.1554 14.7949C13.7861 15.7396 12.0582 16.0606 10.4054 16.0606C9.04201 16.0606 7.65128 15.7069 6.60913 14.6547C5.56832 13.6038 5.05825 12.0408 5.05825 10.0149C5.05825 7.6958 6.3139 6.07903 7.87312 5.13084Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.983 18.2811C14.6595 18.2811 15.2079 18.8295 15.2079 19.506V22.16C15.2079 22.8365 14.6595 23.385 13.983 23.385C13.3065 23.385 12.758 22.8365 12.758 22.16V19.506C12.758 18.8295 13.3065 18.2811 13.983 18.2811Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.25566 14.8903C5.48361 14.039 5.99218 12.9059 7.20734 12.5361L7.92068 14.8798C7.92068 14.8798 7.91996 14.8801 7.92068 14.8798L7.92375 14.8785C7.92411 14.8783 7.92375 14.8785 7.92375 14.8785C7.92374 14.8785 7.92322 14.8778 7.92068 14.8798C7.91364 14.8852 7.89133 14.9052 7.85878 14.951C7.78816 15.0505 7.70057 15.2311 7.62216 15.524C7.46354 16.1164 7.40614 16.9168 7.40616 17.7813V22.1675C7.43456 22.3571 7.45082 22.438 7.48148 22.5189C7.50697 22.5862 7.55954 22.695 7.72276 22.8617C8.09379 23.2406 8.94055 23.8264 10.9275 24.7081C12.5399 25.4236 13.2749 25.7456 13.9586 25.9166C14.6053 26.0784 15.2382 26.1115 16.6666 26.1115V28.5613C15.2536 28.5613 14.3395 28.5372 13.3641 28.2932C12.452 28.0651 11.5174 27.6502 10.0614 27.004C10.0193 26.9853 9.97679 26.9664 9.93382 26.9474C7.8976 26.0438 6.68669 25.3053 5.97232 24.5757C5.59284 24.1882 5.34578 23.7967 5.19055 23.387C5.05418 23.0271 5.0055 22.6875 4.97509 22.4754C4.973 22.4608 4.97099 22.4468 4.96905 22.4335L4.95629 22.3458V17.7814C4.95629 17.7814 4.95629 17.7814 4.95629 17.7814C4.95627 16.8606 5.012 15.8003 5.25566 14.8903Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.98306 15.3589C4.62844 14.6695 5.39976 13.9884 5.99652 13.6348L7.24552 15.7424C6.93706 15.9252 6.34938 16.4159 5.77155 17.0332C5.21994 17.6224 4.85695 18.1377 4.73071 18.4254C4.70277 18.6674 4.71195 19.116 4.78977 19.648C4.87834 20.2536 5.01731 20.7033 5.10492 20.8628L5.19261 21.0224L5.23006 21.2007C5.28661 21.4698 5.55952 21.8651 6.30146 22.3907C6.88143 22.8015 7.56502 23.1703 8.29605 23.5648C8.45794 23.6521 8.62215 23.7407 8.78809 23.8313C9.5952 24.2156 10.848 24.7773 12.0191 25.2425C12.613 25.4784 13.1698 25.6831 13.6288 25.8268C13.8584 25.8987 14.0505 25.9512 14.2021 25.9847C14.3503 26.0175 14.4185 26.0227 14.4277 26.0234C14.4288 26.0234 14.4281 26.0234 14.4277 26.0234L14.4287 28.4733C13.9646 28.4733 13.3889 28.3188 12.8968 28.1648C12.3562 27.9955 11.7367 27.7664 11.1147 27.5193C9.86871 27.0244 8.54832 26.4314 7.70298 26.028L7.67249 26.0135L7.64284 25.9973C7.49779 25.918 7.34502 25.8357 7.18702 25.7506C6.4505 25.354 5.60004 24.896 4.88539 24.3898C4.08363 23.8219 3.18153 23.0135 2.87437 21.8785C2.61828 21.3365 2.4557 20.618 2.3657 20.0026C2.26537 19.3167 2.2169 18.4951 2.33888 17.8727L2.35434 17.7938L2.37996 17.7176C2.6522 16.9085 3.36219 16.0221 3.98306 15.3589Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.48531 21.264V17.1402H5.93518V21.264H3.48531Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.2932 7.04962C25.3238 7.67635 26.0292 8.63219 26.0292 10.0149C26.0292 11.6908 25.609 12.5893 25.1285 13.0744C24.6494 13.5581 23.9279 13.8149 22.9278 13.8149C21.5182 13.8149 20.3041 13.5336 19.453 12.9464C18.6576 12.3977 18.0624 11.5042 17.9894 9.96182C17.9341 8.79382 17.9654 8.01341 18.2764 7.45711C18.5238 7.01449 19.1006 6.47436 20.8431 6.27416C21.8648 6.15678 23.2218 6.39804 24.2932 7.04962ZM25.4601 5.13084C23.9417 4.2075 22.0801 3.87155 20.5868 4.04312C18.4489 4.28874 17.0488 5.05049 16.3161 6.36142C15.6469 7.55867 15.6948 8.98348 15.7462 10.068C15.8492 12.2439 16.7528 13.8118 18.1778 14.7949C19.5471 15.7396 21.275 16.0606 22.9278 16.0606C24.2912 16.0606 25.6819 15.7069 26.7241 14.6547C27.7649 13.6038 28.275 12.0408 28.275 10.0149C28.275 7.6958 27.0193 6.07903 25.4601 5.13084Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.3502 18.2811C18.6737 18.2811 18.1253 18.8295 18.1253 19.506V22.16C18.1253 22.8365 18.6737 23.385 19.3502 23.385C20.0267 23.385 20.5752 22.8365 20.5752 22.16V19.506C20.5752 18.8295 20.0267 18.2811 19.3502 18.2811Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.0775 14.8903C27.8496 14.039 27.341 12.9059 26.1259 12.5361L25.4125 14.8798C25.4125 14.8798 25.4132 14.8801 25.4125 14.8798L25.4095 14.8785C25.4091 14.8783 25.4095 14.8785 25.4095 14.8785C25.4095 14.8785 25.41 14.8778 25.4125 14.8798C25.4196 14.8852 25.4419 14.9052 25.4744 14.951C25.545 15.0505 25.6326 15.2311 25.711 15.524C25.8697 16.1164 25.9271 16.9168 25.927 17.7813V22.1675C25.8986 22.3571 25.8824 22.438 25.8517 22.5189C25.8262 22.5862 25.7737 22.695 25.6104 22.8617C25.2394 23.2406 24.3927 23.8264 22.4057 24.7081C20.7933 25.4236 20.0583 25.7456 19.3746 25.9166C18.7279 26.0784 18.0949 26.1115 16.6666 26.1115V28.5613C18.0796 28.5613 18.9937 28.5372 19.9691 28.2932C20.8812 28.0651 21.8158 27.6502 23.2718 27.004C23.3139 26.9853 23.3564 26.9664 23.3994 26.9474C25.4356 26.0438 26.6465 25.3053 27.3609 24.5757C27.7404 24.1882 27.9874 23.7967 28.1427 23.387C28.279 23.0271 28.3277 22.6875 28.3581 22.4754C28.3602 22.4608 28.3622 22.4468 28.3642 22.4335L28.3769 22.3458V17.7814C28.3769 17.7814 28.3769 17.7814 28.3769 17.7814C28.3769 16.8606 28.3212 15.8003 28.0775 14.8903Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.3501 15.3589C28.7048 14.6695 27.9334 13.9884 27.3367 13.6348L26.0877 15.7424C26.3961 15.9252 26.9838 16.4159 27.5616 17.0332C28.1133 17.6224 28.4763 18.1377 28.6025 18.4254C28.6304 18.6674 28.6213 19.116 28.5434 19.648C28.4549 20.2536 28.3159 20.7033 28.2283 20.8628L28.1406 21.0224L28.1031 21.2007C28.0466 21.4698 27.7737 21.8651 27.0317 22.3907C26.4518 22.8015 25.7682 23.1703 25.0372 23.5648C24.8753 23.6521 24.711 23.7407 24.5451 23.8313C23.738 24.2156 22.4852 24.7773 21.3141 25.2425C20.7202 25.4784 20.1634 25.6831 19.7044 25.8268C19.4748 25.8987 19.2827 25.9512 19.1311 25.9847C18.9829 26.0175 18.9147 26.0227 18.9055 26.0234C18.9044 26.0234 18.9051 26.0234 18.9055 26.0234L18.9045 28.4733C19.3686 28.4733 19.9443 28.3188 20.4364 28.1648C20.977 27.9955 21.5965 27.7664 22.2185 27.5193C23.4645 27.0244 24.7849 26.4314 25.6302 26.028L25.6607 26.0135L25.6904 25.9973C25.8354 25.918 25.9882 25.8357 26.1462 25.7506C26.8827 25.354 27.7332 24.896 28.4478 24.3898C29.2496 23.8219 30.1517 23.0135 30.4588 21.8785C30.7149 21.3365 30.8775 20.618 30.9675 20.0026C31.0678 19.3167 31.1163 18.4951 30.9943 17.8727L30.9789 17.7938L30.9532 17.7176C30.681 16.9085 29.971 16.0221 29.3501 15.3589Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.8479 21.264V17.1402H27.398V21.264H29.8479Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M48.6666 11C49.2189 11 49.6666 11.4477 49.6666 12V15H52.6666C53.2189 15 53.6666 15.4477 53.6666 16C53.6666 16.5523 53.2189 17 52.6666 17H49.6666V20C49.6666 20.5523 49.2189 21 48.6666 21C48.1143 21 47.6666 20.5523 47.6666 20V17H44.6666C44.1143 17 43.6666 16.5523 43.6666 16C43.6666 15.4477 44.1143 15 44.6666 15H47.6666V12C47.6666 11.4477 48.1143 11 48.6666 11Z" fill="white" fill-opacity="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M67.1666 4.33329C66.7064 4.33329 66.3333 4.70639 66.3333 5.16663V23.5H64.6666V5.16663C64.6666 3.78591 65.7859 2.66663 67.1666 2.66663H89.494C90.6077 2.66663 91.1654 4.01306 90.3779 4.80051L76.6264 18.552H80.5V16.8333H82.1666V18.9687C82.1666 19.6591 81.607 20.2187 80.9166 20.2187H74.9597L72.0951 23.0833H85.0833V12.6666H86.75V23.0833C86.75 24.0038 86.0038 24.75 85.0833 24.75H70.4285L67.5118 27.6666H88.8333C89.2935 27.6666 89.6666 27.2935 89.6666 26.8333V8.49996H91.3333V26.8333C91.3333 28.214 90.214 29.3333 88.8333 29.3333H66.5059C65.3922 29.3333 64.8345 27.9869 65.622 27.1994L79.3214 13.5H75.5V15.1666H73.8333V13.0833C73.8333 12.3929 74.3929 11.8333 75.0833 11.8333H80.9881L83.9048 8.91663H70.9166V19.3333H69.25V8.91663C69.25 7.99615 69.9962 7.24996 70.9166 7.24996H85.5714L88.4881 4.33329H67.1666Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -55,7 +55,7 @@
"bindings": {
"ctrl-[": "project_panel::CollapseSelectedEntry",
"ctrl-b": "project_panel::CollapseSelectedEntry",
"h": "project_panel::CollapseSelectedEntry",
"alt-b": "project_panel::CollapseSelectedEntry",
"ctrl-]": "project_panel::ExpandSelectedEntry",
"ctrl-f": "project_panel::ExpandSelectedEntry",
"ctrl-shift-c": "project_panel::CopyPath"

View File

@@ -176,7 +176,10 @@
{
"focus": false
}
]
],
"alt-]": "copilot::NextSuggestion",
"alt-[": "copilot::PreviousSuggestion",
"alt-\\": "copilot::Toggle"
}
},
{

View File

@@ -3,11 +3,21 @@
"theme": "One Dark",
// The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Mono",
// The OpenType features to enable for text in the editor.
"buffer_font_features": {
// Disable ligatures:
// "calt": false
},
// The default font size for text in the editor
"buffer_font_size": 15,
// The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes.
"active_pane_magnification": 1.0,
// Enable / disable copilot integration.
"enable_copilot_integration": true,
// Controls whether copilot provides suggestion immediately
// or waits for a `copilot::Toggle`
"copilot": "on",
// Whether to enable vim modes and key bindings
"vim_mode": false,
// Whether to show the informational hover box when moving the mouse
@@ -20,7 +30,7 @@
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
// Whether the screen sharing icon is showed in the os status bar.
// Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true,
// Whether to use language servers to provide code intelligence.
"enable_language_server": true,
@@ -115,7 +125,7 @@
// Settings specific to the terminal
"terminal": {
// What shell to use when opening a terminal. May take 3 values:
// 1. Use the system's default terminal configuration (e.g. $TERM).
// 1. Use the system's default terminal configuration in /etc/passwd
// "shell": "system"
// 2. A program:
// "shell": {
@@ -195,7 +205,9 @@
// Different settings for specific languages.
"languages": {
"Plain Text": {
"soft_wrap": "preferred_line_length"
"soft_wrap": "preferred_line_length",
// Copilot can be a little strange on non-code files
"copilot": "off"
},
"Elixir": {
"tab_size": 2
@@ -205,7 +217,9 @@
"hard_tabs": true
},
"Markdown": {
"soft_wrap": "preferred_line_length"
"soft_wrap": "preferred_line_length",
// Copilot can be a little strange on non-code files
"copilot": "off"
},
"JavaScript": {
"tab_size": 2
@@ -218,6 +232,9 @@
},
"YAML": {
"tab_size": 2
},
"JSON": {
"copilot": "off"
}
},
// LSP Specific settings.

View File

@@ -22,7 +22,8 @@ anyhow = "1.0.38"
isahc = "1.7"
lazy_static = "1.4"
log = "0.4"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
smol = "1.2.5"
tempdir = "0.3.7"

View File

@@ -1,8 +1,7 @@
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 client::{ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
use db::kvp::KEY_VALUE_STORE;
use gpui::{
actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
@@ -14,6 +13,7 @@ use smol::{fs::File, io::AsyncReadExt, process::Command};
use std::{ffi::OsString, sync::Arc, time::Duration};
use update_notification::UpdateNotification;
use util::channel::ReleaseChannel;
use util::http::HttpClient;
use workspace::Workspace;
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";

View File

@@ -34,7 +34,7 @@ util = { path = "../util" }
anyhow = "1.0.38"
async-broadcast = "0.4"
futures = "0.3"
postage = { version = "0.4.1", features = ["futures-traits"] }
postage = { workspace = true }
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }

View File

@@ -275,6 +275,7 @@ impl Room {
if let Some(project) = project.upgrade(cx) {
project.update(cx, |project, cx| {
project.disconnected_from_host(cx);
project.close(cx);
});
}
}

View File

@@ -17,7 +17,8 @@ anyhow = "1.0"
clap = { version = "3.1", features = ["derive"] }
dirs = "3.0"
ipc-channel = "0.16"
serde = { version = "1.0", features = ["derive", "rc"] }
serde = { workspace = true }
serde_derive = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.9"

View File

@@ -23,11 +23,10 @@ async-recursion = "0.3"
async-tungstenite = { version = "0.16", features = ["async-tls"] }
futures = "0.3"
image = "0.23"
isahc = "1.7"
lazy_static = "1.4.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] }
postage = { workspace = true }
rand = "0.8.3"
smol = "1.2.5"
thiserror = "1.0.29"
@@ -35,7 +34,8 @@ time = { version = "0.3", features = ["serde", "serde-well-known"] }
tiny_http = "0.8"
uuid = { version = "1.1.2", features = ["v4"] }
url = "2.2"
serde = { version = "*", features = ["derive"] }
serde = { workspace = true }
serde_derive = { workspace = true }
settings = { path = "../settings" }
tempfile = "3"

View File

@@ -1,7 +1,6 @@
#[cfg(any(test, feature = "test-support"))]
pub mod test;
pub mod http;
pub mod telemetry;
pub mod user;
@@ -18,7 +17,6 @@ use gpui::{
AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext, AppVersion,
AsyncAppContext, Entity, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
};
use http::HttpClient;
use lazy_static::lazy_static;
use parking_lot::RwLock;
use postage::watch;
@@ -41,6 +39,7 @@ use telemetry::Telemetry;
use thiserror::Error;
use url::Url;
use util::channel::ReleaseChannel;
use util::http::HttpClient;
use util::{ResultExt, TryFutureExt};
pub use rpc::*;
@@ -130,7 +129,7 @@ pub enum EstablishConnectionError {
#[error("{0}")]
Other(#[from] anyhow::Error),
#[error("{0}")]
Http(#[from] http::Error),
Http(#[from] util::http::Error),
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
@@ -1396,10 +1395,11 @@ pub fn decode_worktree_url(url: &str) -> Option<(u64, String)> {
#[cfg(test)]
mod tests {
use super::*;
use crate::test::{FakeHttpClient, FakeServer};
use crate::test::FakeServer;
use gpui::{executor::Deterministic, TestAppContext};
use parking_lot::Mutex;
use std::future;
use util::http::FakeHttpClient;
#[gpui::test(iterations = 10)]
async fn test_reconnection(cx: &mut TestAppContext) {

View File

@@ -1,57 +0,0 @@
pub use anyhow::{anyhow, Result};
use futures::future::BoxFuture;
use isahc::{
config::{Configurable, RedirectPolicy},
AsyncBody,
};
pub use isahc::{
http::{Method, Uri},
Error,
};
use smol::future::FutureExt;
use std::{sync::Arc, time::Duration};
pub use url::Url;
pub type Request = isahc::Request<AsyncBody>;
pub type Response = isahc::Response<AsyncBody>;
pub trait HttpClient: Send + Sync {
fn send(&self, req: Request) -> BoxFuture<Result<Response, Error>>;
fn get<'a>(
&'a self,
uri: &str,
body: AsyncBody,
follow_redirects: bool,
) -> BoxFuture<'a, Result<Response, Error>> {
let request = isahc::Request::builder()
.redirect_policy(if follow_redirects {
RedirectPolicy::Follow
} else {
RedirectPolicy::None
})
.method(Method::GET)
.uri(uri)
.body(body);
match request {
Ok(request) => self.send(request),
Err(error) => async move { Err(error.into()) }.boxed(),
}
}
}
pub fn client() -> Arc<dyn HttpClient> {
Arc::new(
isahc::HttpClient::builder()
.connect_timeout(Duration::from_secs(5))
.low_speed_timeout(100, Duration::from_secs(5))
.build()
.unwrap(),
)
}
impl HttpClient for isahc::HttpClient {
fn send(&self, req: Request) -> BoxFuture<Result<Response, Error>> {
Box::pin(async move { self.send_async(req).await })
}
}

View File

@@ -1,11 +1,9 @@
use crate::http::HttpClient;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
executor::Background,
serde_json::{self, value::Map, Value},
AppContext, Task,
};
use isahc::Request;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Serialize;
@@ -19,6 +17,7 @@ use std::{
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tempfile::NamedTempFile;
use util::http::HttpClient;
use util::{channel::ReleaseChannel, post_inc, ResultExt, TryFutureExt};
use uuid::Uuid;
@@ -220,10 +219,10 @@ impl Telemetry {
"App": true
}),
}])?;
let request = Request::post(MIXPANEL_ENGAGE_URL)
.header("Content-Type", "application/json")
.body(json_bytes.into())?;
this.http_client.send(request).await?;
this.http_client
.post_json(MIXPANEL_ENGAGE_URL, json_bytes.into())
.await?;
anyhow::Ok(())
}
.log_err(),
@@ -316,10 +315,9 @@ impl Telemetry {
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, &events)?;
let request = Request::post(MIXPANEL_EVENTS_URL)
.header("Content-Type", "application/json")
.body(json_bytes.into())?;
this.http_client.send(request).await?;
this.http_client
.post_json(MIXPANEL_EVENTS_URL, json_bytes.into())
.await?;
anyhow::Ok(())
}
.log_err(),

View File

@@ -1,16 +1,14 @@
use crate::{
http::{self, HttpClient, Request, Response},
Client, Connection, Credentials, EstablishConnectionError, UserStore,
};
use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
use anyhow::{anyhow, Result};
use futures::{future::BoxFuture, stream::BoxStream, Future, StreamExt};
use futures::{stream::BoxStream, StreamExt};
use gpui::{executor, ModelHandle, TestAppContext};
use parking_lot::Mutex;
use rpc::{
proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse},
ConnectionId, Peer, Receipt, TypedEnvelope,
};
use std::{fmt, rc::Rc, sync::Arc};
use std::{rc::Rc, sync::Arc};
use util::http::FakeHttpClient;
pub struct FakeServer {
peer: Arc<Peer>,
@@ -219,46 +217,3 @@ impl Drop for FakeServer {
self.disconnect();
}
}
pub struct FakeHttpClient {
handler: Box<
dyn 'static
+ Send
+ Sync
+ Fn(Request) -> BoxFuture<'static, Result<Response, http::Error>>,
>,
}
impl FakeHttpClient {
pub fn create<Fut, F>(handler: F) -> Arc<dyn HttpClient>
where
Fut: 'static + Send + Future<Output = Result<Response, http::Error>>,
F: 'static + Send + Sync + Fn(Request) -> Fut,
{
Arc::new(Self {
handler: Box::new(move |req| Box::pin(handler(req))),
})
}
pub fn with_404_response() -> Arc<dyn HttpClient> {
Self::create(|_| async move {
Ok(isahc::Response::builder()
.status(404)
.body(Default::default())
.unwrap())
})
}
}
impl fmt::Debug for FakeHttpClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FakeHttpClient").finish()
}
}
impl HttpClient for FakeHttpClient {
fn send(&self, req: Request) -> BoxFuture<Result<Response, crate::http::Error>> {
let future = (self.handler)(req);
Box::pin(async move { future.await.map(Into::into) })
}
}

View File

@@ -1,4 +1,4 @@
use super::{http::HttpClient, proto, Client, Status, TypedEnvelope};
use super::{proto, Client, Status, TypedEnvelope};
use anyhow::{anyhow, Context, Result};
use collections::{hash_map::Entry, HashMap, HashSet};
use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
@@ -7,6 +7,7 @@ use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse};
use settings::Settings;
use std::sync::{Arc, Weak};
use util::http::HttpClient;
use util::{StaffMode, TryFutureExt as _};
#[derive(Default, Debug)]

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
version = "0.8.0"
version = "0.8.2"
publish = false
[[bin]]
@@ -31,6 +31,7 @@ futures = "0.3"
hyper = "0.14"
lazy_static = "1.4"
lipsum = { version = "0.8", optional = true }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
nanoid = "0.4"
parking_lot = "0.11.1"
prometheus = "0.13"
@@ -40,8 +41,9 @@ scrypt = "0.7"
# Remove fork dependency when a version with https://github.com/SeaQL/sea-orm/pull/1283 is released.
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls"] }
sea-query = "0.27"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
sha-1 = "0.9"
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
time = { version = "0.3", features = ["serde", "serde-well-known"] }
@@ -74,11 +76,10 @@ workspace = { path = "../workspace", features = ["test-support"] }
ctor = "0.1"
env_logger = "0.9"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
util = { path = "../util" }
lazy_static = "1.4"
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-sqlite"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
serde_json = { workspace = true }
sqlx = { version = "0.6", features = ["sqlite"] }
unindent = "0.1"

View File

@@ -1,5 +1,5 @@
use crate::{
db::{self, UserId},
db::{self, AccessTokenId, Database, UserId},
AppState, Error, Result,
};
use anyhow::{anyhow, Context};
@@ -8,12 +8,24 @@ use axum::{
middleware::Next,
response::IntoResponse,
};
use lazy_static::lazy_static;
use prometheus::{exponential_buckets, register_histogram, Histogram};
use rand::thread_rng;
use scrypt::{
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Scrypt,
};
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use std::{sync::Arc, time::Instant};
lazy_static! {
static ref METRIC_ACCESS_TOKEN_HASHING_TIME: Histogram = register_histogram!(
"access_token_hashing_time",
"time spent hashing access tokens",
exponential_buckets(10.0, 2.0, 10).unwrap(),
)
.unwrap();
}
pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl IntoResponse {
let mut auth_header = req
@@ -42,20 +54,14 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
)
})?;
let mut credentials_valid = false;
let state = req.extensions().get::<Arc<AppState>>().unwrap();
if let Some(admin_token) = access_token.strip_prefix("ADMIN_TOKEN:") {
if state.config.api_token == admin_token {
credentials_valid = true;
}
let credentials_valid = if let Some(admin_token) = access_token.strip_prefix("ADMIN_TOKEN:") {
state.config.api_token == admin_token
} else {
for password_hash in state.db.get_access_token_hashes(user_id).await? {
if verify_access_token(access_token, &password_hash)? {
credentials_valid = true;
break;
}
}
}
verify_access_token(&access_token, user_id, &state.db)
.await
.unwrap_or(false)
};
if credentials_valid {
let user = state
@@ -75,13 +81,26 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
const MAX_ACCESS_TOKENS_TO_STORE: usize = 8;
#[derive(Serialize, Deserialize)]
struct AccessTokenJson {
version: usize,
id: AccessTokenId,
token: String,
}
pub async fn create_access_token(db: &db::Database, user_id: UserId) -> Result<String> {
const VERSION: usize = 1;
let access_token = rpc::auth::random_token();
let access_token_hash =
hash_access_token(&access_token).context("failed to hash access token")?;
db.create_access_token_hash(user_id, &access_token_hash, MAX_ACCESS_TOKENS_TO_STORE)
let id = db
.create_access_token(user_id, &access_token_hash, MAX_ACCESS_TOKENS_TO_STORE)
.await?;
Ok(access_token)
Ok(serde_json::to_string(&AccessTokenJson {
version: VERSION,
id,
token: access_token,
})?)
}
fn hash_access_token(token: &str) -> Result<String> {
@@ -89,7 +108,7 @@ fn hash_access_token(token: &str) -> Result<String> {
let params = if cfg!(debug_assertions) {
scrypt::Params::new(1, 1, 1).unwrap()
} else {
scrypt::Params::recommended()
scrypt::Params::new(14, 8, 1).unwrap()
};
Ok(Scrypt
@@ -112,7 +131,21 @@ pub fn encrypt_access_token(access_token: &str, public_key: String) -> Result<St
Ok(encrypted_access_token)
}
pub fn verify_access_token(token: &str, hash: &str) -> Result<bool> {
let hash = PasswordHash::new(hash).map_err(anyhow::Error::new)?;
Ok(Scrypt.verify_password(token.as_bytes(), &hash).is_ok())
pub async fn verify_access_token(token: &str, user_id: UserId, db: &Arc<Database>) -> Result<bool> {
let token: AccessTokenJson = serde_json::from_str(&token)?;
let db_token = db.get_access_token(token.id).await?;
if db_token.user_id != user_id {
return Err(anyhow!("no such access token"))?;
}
let db_hash = PasswordHash::new(&db_token.hash).map_err(anyhow::Error::new)?;
let t0 = Instant::now();
let is_valid = Scrypt
.verify_password(token.token.as_bytes(), &db_hash)
.is_ok();
let duration = t0.elapsed();
log::info!("hashed access token in {:?}", duration);
METRIC_ACCESS_TOKEN_HASHING_TIME.observe(duration.as_millis() as f64);
Ok(is_valid)
}

View File

@@ -1,4 +1,4 @@
use collab::db;
use collab::{db, executor::Executor};
use db::{ConnectOptions, Database};
use serde::{de::DeserializeOwned, Deserialize};
use std::fmt::Write;
@@ -13,7 +13,7 @@ struct GitHubUser {
#[tokio::main]
async fn main() {
let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var");
let db = Database::new(ConnectOptions::new(database_url))
let db = Database::new(ConnectOptions::new(database_url), Executor::Production)
.await
.expect("failed to connect to postgres database");
let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");

View File

@@ -15,6 +15,7 @@ mod worktree;
mod worktree_diagnostic_summary;
mod worktree_entry;
use crate::executor::Executor;
use crate::{Error, Result};
use anyhow::anyhow;
use collections::{BTreeMap, HashMap, HashSet};
@@ -22,6 +23,8 @@ pub use contact::Contact;
use dashmap::DashMap;
use futures::StreamExt;
use hyper::StatusCode;
use rand::prelude::StdRng;
use rand::{Rng, SeedableRng};
use rpc::{proto, ConnectionId};
use sea_orm::Condition;
pub use sea_orm::ConnectOptions;
@@ -46,20 +49,20 @@ pub struct Database {
options: ConnectOptions,
pool: DatabaseConnection,
rooms: DashMap<RoomId, Arc<Mutex<()>>>,
#[cfg(test)]
background: Option<std::sync::Arc<gpui::executor::Background>>,
rng: Mutex<StdRng>,
executor: Executor,
#[cfg(test)]
runtime: Option<tokio::runtime::Runtime>,
}
impl Database {
pub async fn new(options: ConnectOptions) -> Result<Self> {
pub async fn new(options: ConnectOptions, executor: Executor) -> Result<Self> {
Ok(Self {
options: options.clone(),
pool: sea_orm::Database::connect(options).await?,
rooms: DashMap::with_capacity(16384),
#[cfg(test)]
background: None,
rng: Mutex::new(StdRng::seed_from_u64(0)),
executor,
#[cfg(test)]
runtime: None,
})
@@ -2743,16 +2746,16 @@ impl Database {
// access tokens
pub async fn create_access_token_hash(
pub async fn create_access_token(
&self,
user_id: UserId,
access_token_hash: &str,
max_access_token_count: usize,
) -> Result<()> {
) -> Result<AccessTokenId> {
self.transaction(|tx| async {
let tx = tx;
access_token::ActiveModel {
let token = access_token::ActiveModel {
user_id: ActiveValue::set(user_id),
hash: ActiveValue::set(access_token_hash.into()),
..Default::default()
@@ -2775,26 +2778,20 @@ impl Database {
)
.exec(&*tx)
.await?;
Ok(())
Ok(token.id)
})
.await
}
pub async fn get_access_token_hashes(&self, user_id: UserId) -> Result<Vec<String>> {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryAs {
Hash,
}
pub async fn get_access_token(
&self,
access_token_id: AccessTokenId,
) -> Result<access_token::Model> {
self.transaction(|tx| async move {
Ok(access_token::Entity::find()
.select_only()
.column(access_token::Column::Hash)
.filter(access_token::Column::UserId.eq(user_id))
.order_by_desc(access_token::Column::Id)
.into_values::<_, QueryAs>()
.all(&*tx)
.await?)
Ok(access_token::Entity::find_by_id(access_token_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such access token"))?)
})
.await
}
@@ -2805,30 +2802,26 @@ impl Database {
Fut: Send + Future<Output = Result<T>>,
{
let body = async {
let mut i = 0;
loop {
let (tx, result) = self.with_transaction(&f).await?;
match result {
Ok(result) => {
match tx.commit().await.map_err(Into::into) {
Ok(()) => return Ok(result),
Err(error) => {
if is_serialization_error(&error) {
// Retry (don't break the loop)
} else {
return Err(error);
}
Ok(result) => match tx.commit().await.map_err(Into::into) {
Ok(()) => return Ok(result),
Err(error) => {
if !self.retry_on_serialization_error(&error, i).await {
return Err(error);
}
}
}
},
Err(error) => {
tx.rollback().await?;
if is_serialization_error(&error) {
// Retry (don't break the loop)
} else {
if !self.retry_on_serialization_error(&error, i).await {
return Err(error);
}
}
}
i += 1;
}
};
@@ -2841,6 +2834,7 @@ impl Database {
Fut: Send + Future<Output = Result<Option<(RoomId, T)>>>,
{
let body = async {
let mut i = 0;
loop {
let (tx, result) = self.with_transaction(&f).await?;
match result {
@@ -2856,35 +2850,28 @@ impl Database {
}));
}
Err(error) => {
if is_serialization_error(&error) {
// Retry (don't break the loop)
} else {
if !self.retry_on_serialization_error(&error, i).await {
return Err(error);
}
}
}
}
Ok(None) => {
match tx.commit().await.map_err(Into::into) {
Ok(()) => return Ok(None),
Err(error) => {
if is_serialization_error(&error) {
// Retry (don't break the loop)
} else {
return Err(error);
}
Ok(None) => match tx.commit().await.map_err(Into::into) {
Ok(()) => return Ok(None),
Err(error) => {
if !self.retry_on_serialization_error(&error, i).await {
return Err(error);
}
}
}
},
Err(error) => {
tx.rollback().await?;
if is_serialization_error(&error) {
// Retry (don't break the loop)
} else {
if !self.retry_on_serialization_error(&error, i).await {
return Err(error);
}
}
}
i += 1;
}
};
@@ -2897,38 +2884,34 @@ impl Database {
Fut: Send + Future<Output = Result<T>>,
{
let body = async {
let mut i = 0;
loop {
let lock = self.rooms.entry(room_id).or_default().clone();
let _guard = lock.lock_owned().await;
let (tx, result) = self.with_transaction(&f).await?;
match result {
Ok(data) => {
match tx.commit().await.map_err(Into::into) {
Ok(()) => {
return Ok(RoomGuard {
data,
_guard,
_not_send: PhantomData,
});
}
Err(error) => {
if is_serialization_error(&error) {
// Retry (don't break the loop)
} else {
return Err(error);
}
Ok(data) => match tx.commit().await.map_err(Into::into) {
Ok(()) => {
return Ok(RoomGuard {
data,
_guard,
_not_send: PhantomData,
});
}
Err(error) => {
if !self.retry_on_serialization_error(&error, i).await {
return Err(error);
}
}
}
},
Err(error) => {
tx.rollback().await?;
if is_serialization_error(&error) {
// Retry (don't break the loop)
} else {
if !self.retry_on_serialization_error(&error, i).await {
return Err(error);
}
}
}
i += 1;
}
};
@@ -2954,14 +2937,14 @@ impl Database {
Ok((tx, result))
}
async fn run<F, T>(&self, future: F) -> T
async fn run<F, T>(&self, future: F) -> Result<T>
where
F: Future<Output = T>,
F: Future<Output = Result<T>>,
{
#[cfg(test)]
{
if let Some(background) = self.background.as_ref() {
background.simulate_random_delay().await;
if let Executor::Deterministic(executor) = &self.executor {
executor.simulate_random_delay().await;
}
self.runtime.as_ref().unwrap().block_on(future)
@@ -2972,6 +2955,27 @@ impl Database {
future.await
}
}
async fn retry_on_serialization_error(&self, error: &Error, prev_attempt_count: u32) -> bool {
// If the error is due to a failure to serialize concurrent transactions, then retry
// this transaction after a delay. With each subsequent retry, double the delay duration.
// Also vary the delay randomly in order to ensure different database connections retry
// at different times.
if is_serialization_error(error) {
let base_delay = 4_u64 << prev_attempt_count.min(16);
let randomized_delay = base_delay as f32 * self.rng.lock().await.gen_range(0.5..=2.0);
log::info!(
"retrying transaction after serialization error. delay: {} ms.",
randomized_delay
);
self.executor
.sleep(Duration::from_millis(randomized_delay as u64))
.await;
true
} else {
false
}
}
}
fn is_serialization_error(error: &Error) -> bool {
@@ -3273,7 +3277,6 @@ mod test {
use gpui::executor::Background;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use rand::prelude::*;
use sea_orm::ConnectionTrait;
use sqlx::migrate::MigrateDatabase;
use std::sync::Arc;
@@ -3295,7 +3298,9 @@ mod test {
let mut db = runtime.block_on(async {
let mut options = ConnectOptions::new(url);
options.max_connections(5);
let db = Database::new(options).await.unwrap();
let db = Database::new(options, Executor::Deterministic(background))
.await
.unwrap();
let sql = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/migrations.sqlite/20221109000000_test_schema.sql"
@@ -3310,7 +3315,6 @@ mod test {
db
});
db.background = Some(background);
db.runtime = Some(runtime);
Self {
@@ -3344,13 +3348,14 @@ mod test {
options
.max_connections(5)
.idle_timeout(Duration::from_secs(0));
let db = Database::new(options).await.unwrap();
let db = Database::new(options, Executor::Deterministic(background))
.await
.unwrap();
let migrations_path = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations");
db.migrate(Path::new(migrations_path), false).await.unwrap();
db
});
db.background = Some(background);
db.runtime = Some(runtime);
Self {

View File

@@ -177,30 +177,63 @@ test_both_dbs!(
.unwrap()
.user_id;
db.create_access_token_hash(user, "h1", 3).await.unwrap();
db.create_access_token_hash(user, "h2", 3).await.unwrap();
let token_1 = db.create_access_token(user, "h1", 2).await.unwrap();
let token_2 = db.create_access_token(user, "h2", 2).await.unwrap();
assert_eq!(
db.get_access_token_hashes(user).await.unwrap(),
&["h2".to_string(), "h1".to_string()]
db.get_access_token(token_1).await.unwrap(),
access_token::Model {
id: token_1,
user_id: user,
hash: "h1".into(),
}
);
assert_eq!(
db.get_access_token(token_2).await.unwrap(),
access_token::Model {
id: token_2,
user_id: user,
hash: "h2".into()
}
);
db.create_access_token_hash(user, "h3", 3).await.unwrap();
let token_3 = db.create_access_token(user, "h3", 2).await.unwrap();
assert_eq!(
db.get_access_token_hashes(user).await.unwrap(),
&["h3".to_string(), "h2".to_string(), "h1".to_string(),]
db.get_access_token(token_3).await.unwrap(),
access_token::Model {
id: token_3,
user_id: user,
hash: "h3".into()
}
);
assert_eq!(
db.get_access_token(token_2).await.unwrap(),
access_token::Model {
id: token_2,
user_id: user,
hash: "h2".into()
}
);
assert!(db.get_access_token(token_1).await.is_err());
db.create_access_token_hash(user, "h4", 3).await.unwrap();
let token_4 = db.create_access_token(user, "h4", 2).await.unwrap();
assert_eq!(
db.get_access_token_hashes(user).await.unwrap(),
&["h4".to_string(), "h3".to_string(), "h2".to_string(),]
db.get_access_token(token_4).await.unwrap(),
access_token::Model {
id: token_4,
user_id: user,
hash: "h4".into()
}
);
db.create_access_token_hash(user, "h5", 3).await.unwrap();
assert_eq!(
db.get_access_token_hashes(user).await.unwrap(),
&["h5".to_string(), "h4".to_string(), "h3".to_string()]
db.get_access_token(token_3).await.unwrap(),
access_token::Model {
id: token_3,
user_id: user,
hash: "h3".into()
}
);
assert!(db.get_access_token(token_2).await.is_err());
assert!(db.get_access_token(token_1).await.is_err());
}
);

View File

@@ -10,6 +10,7 @@ mod tests;
use axum::{http::StatusCode, response::IntoResponse};
use db::Database;
use executor::Executor;
use serde::Deserialize;
use std::{path::PathBuf, sync::Arc};
@@ -118,7 +119,7 @@ impl AppState {
pub async fn new(config: Config) -> Result<Arc<Self>> {
let mut db_options = db::ConnectOptions::new(config.database_url.clone());
db_options.max_connections(config.database_max_connections);
let db = Database::new(db_options).await?;
let db = Database::new(db_options, Executor::Production).await?;
let live_kit_client = if let Some(((server, key), secret)) = config
.live_kit_server
.as_ref()

View File

@@ -32,7 +32,7 @@ async fn main() -> Result<()> {
let config = envy::from_env::<MigrateConfig>().expect("error loading config");
let mut db_options = db::ConnectOptions::new(config.database_url.clone());
db_options.max_connections(5);
let db = Database::new(db_options).await?;
let db = Database::new(db_options, Executor::Production).await?;
let migrations_path = config
.migrations_path

View File

@@ -7,15 +7,12 @@ use crate::{
use anyhow::anyhow;
use call::ActiveCall;
use client::{
self, proto::PeerId, test::FakeHttpClient, Client, Connection, Credentials,
EstablishConnectionError, UserStore,
self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
};
use collections::{HashMap, HashSet};
use fs::FakeFs;
use futures::{channel::oneshot, StreamExt as _};
use gpui::{
executor::Deterministic, test::EmptyView, ModelHandle, Task, TestAppContext, ViewHandle,
};
use gpui::{executor::Deterministic, test::EmptyView, ModelHandle, TestAppContext, ViewHandle};
use language::LanguageRegistry;
use parking_lot::Mutex;
use project::{Project, WorktreeId};
@@ -30,6 +27,7 @@ use std::{
},
};
use theme::ThemeRegistry;
use util::http::FakeHttpClient;
use workspace::Workspace;
mod integration_tests;
@@ -188,7 +186,7 @@ impl TestServer {
let app_state = Arc::new(workspace::AppState {
client: client.clone(),
user_store: user_store.clone(),
languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
languages: Arc::new(LanguageRegistry::test()),
themes: ThemeRegistry::new((), cx.font_cache()),
fs: fs.clone(),
build_window_options: |_, _, _| Default::default(),

View File

@@ -1744,10 +1744,6 @@ async fn test_project_reconnect(
vec![
"a.txt",
"b.txt",
"subdir1",
"subdir1/c.txt",
"subdir1/d.txt",
"subdir1/e.txt",
"subdir2",
"subdir2/f.txt",
"subdir2/g.txt",
@@ -1780,10 +1776,6 @@ async fn test_project_reconnect(
vec![
"a.txt",
"b.txt",
"subdir1",
"subdir1/c.txt",
"subdir1/d.txt",
"subdir1/e.txt",
"subdir2",
"subdir2/f.txt",
"subdir2/g.txt",
@@ -1875,10 +1867,6 @@ async fn test_project_reconnect(
vec![
"a.txt",
"b.txt",
"subdir1",
"subdir1/c.txt",
"subdir1/d.txt",
"subdir1/e.txt",
"subdir2",
"subdir2/f.txt",
"subdir2/g.txt",

View File

@@ -42,8 +42,9 @@ workspace = { path = "../workspace" }
anyhow = "1.0"
futures = "0.3"
log = "0.4"
postage = { version = "0.4.1", features = ["futures-traits"] }
serde = { version = "1.0", features = ["derive", "rc"] }
postage = { workspace = true }
serde = { workspace = true }
serde_derive = { workspace = true }
[dev-dependencies]
call = { path = "../call", features = ["test-support"] }

View File

@@ -89,7 +89,7 @@ impl View for CollabTitlebarItem {
let theme = cx.global::<Settings>().theme.clone();
let mut left_container = Flex::row();
let mut right_container = Flex::row();
let mut right_container = Flex::row().align_children_center();
left_container.add_child(
Label::new(project_title, theme.workspace.titlebar.title.clone())
@@ -117,6 +117,7 @@ impl View for CollabTitlebarItem {
let status = workspace.read(cx).client().status();
let status = &*status.borrow();
if matches!(status, client::Status::Connected { .. }) {
right_container.add_child(self.render_toggle_contacts_button(&theme, cx));
right_container.add_child(self.render_user_menu_button(&theme, cx));
@@ -300,42 +301,17 @@ impl CollabTitlebarItem {
.with_style(item_style.container)
.boxed()
})),
ContextMenuItem::Item {
label: "Sign out".into(),
action: Box::new(SignOut),
},
ContextMenuItem::Item {
label: "Give Feedback".into(),
action: Box::new(feedback::feedback_editor::GiveFeedback),
},
ContextMenuItem::item("Sign out", SignOut),
ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
]
} else {
vec![
ContextMenuItem::Item {
label: "Sign in".into(),
action: Box::new(SignIn),
},
ContextMenuItem::Item {
label: "Give Feedback".into(),
action: Box::new(feedback::feedback_editor::GiveFeedback),
},
ContextMenuItem::item("Sign in", SignIn),
ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
]
};
user_menu.show(
vec2f(
theme
.workspace
.titlebar
.user_menu_button
.default
.button_width,
theme.workspace.titlebar.height,
),
AnchorCorner::TopRight,
items,
cx,
);
user_menu.show(Default::default(), AnchorCorner::TopRight, items, cx);
});
}
@@ -402,7 +378,6 @@ impl CollabTitlebarItem {
theme.tooltip.clone(),
cx,
)
.aligned()
.boxed(),
)
.with_children(badge)
@@ -547,10 +522,15 @@ impl CollabTitlebarItem {
)
.contained()
.with_margin_left(theme.workspace.titlebar.item_spacing)
.aligned()
.boxed(),
)
.with_child(ChildView::new(&self.user_menu, cx).boxed())
.with_child(
ChildView::new(&self.user_menu, cx)
.aligned()
.bottom()
.right()
.boxed(),
)
.boxed()
}
@@ -572,22 +552,18 @@ impl CollabTitlebarItem {
fn render_contacts_popover_host<'a>(
&'a self,
theme: &'a theme::Titlebar,
_theme: &'a theme::Titlebar,
cx: &'a RenderContext<Self>,
) -> Option<ElementBox> {
self.contacts_popover.as_ref().map(|popover| {
Overlay::new(
ChildView::new(popover, cx)
.contained()
.with_margin_top(theme.height)
.with_margin_left(theme.toggle_contacts_button.default.button_width)
.with_margin_right(-theme.toggle_contacts_button.default.button_width)
.boxed(),
)
.with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_corner(AnchorCorner::BottomLeft)
.with_z_index(999)
.boxed()
Overlay::new(ChildView::new(popover, cx).boxed())
.with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_corner(AnchorCorner::TopRight)
.with_z_index(999)
.aligned()
.bottom()
.right()
.boxed()
})
}

View File

@@ -316,12 +316,20 @@ impl ContactList {
github_login
);
let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
let window_id = cx.window_id();
cx.spawn(|_, mut cx| async move {
if answer.next().await == Some(0) {
user_store
if let Err(e) = user_store
.update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
.await
.unwrap();
{
cx.prompt(
window_id,
PromptLevel::Info,
&format!("Failed to remove contact: {}", e),
&["Ok"],
);
}
}
})
.detach();

View File

@@ -21,6 +21,8 @@ pub fn init(cx: &mut MutableAppContext) {
} else if let Some((window_id, _)) = status_indicator.take() {
cx.remove_status_bar_item(window_id);
}
} else if let Some((window_id, _)) = status_indicator.take() {
cx.remove_status_bar_item(window_id);
}
})
.detach();

View File

@@ -24,3 +24,10 @@ pub type HashMap<K, V> = std::collections::HashMap<K, V>;
pub type HashSet<T> = std::collections::HashSet<T>;
pub use std::collections::*;
// NEW TYPES
#[derive(Default)]
pub struct CommandPaletteFilter {
pub filtered_namespaces: HashSet<&'static str>,
}

View File

@@ -24,7 +24,7 @@ workspace = { path = "../workspace" }
gpui = { path = "../gpui", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
serde_json = { workspace = true }
workspace = { path = "../workspace", features = ["test-support"] }
ctor = "0.1"
env_logger = "0.9"

View File

@@ -1,4 +1,4 @@
use collections::HashSet;
use collections::CommandPaletteFilter;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions,
@@ -12,11 +12,6 @@ use settings::Settings;
use std::cmp;
use workspace::Workspace;
#[derive(Default)]
pub struct CommandPaletteFilter {
pub filtered_namespaces: HashSet<&'static str>,
}
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(CommandPalette::toggle);
Picker::<CommandPalette>::init(cx);

View File

@@ -1,7 +1,7 @@
use gpui::{
elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap_matcher::KeymapContext,
platform::CursorStyle, Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton,
MutableAppContext, RenderContext, SizeConstraint, Subscription, View, ViewContext,
MouseState, MutableAppContext, RenderContext, SizeConstraint, Subscription, View, ViewContext,
};
use menu::*;
use settings::Settings;
@@ -24,20 +24,71 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ContextMenu::cancel);
}
type ContextMenuItemBuilder = Box<dyn Fn(&mut MouseState, &theme::ContextMenuItem) -> ElementBox>;
pub enum ContextMenuItemLabel {
String(Cow<'static, str>),
Element(ContextMenuItemBuilder),
}
pub enum ContextMenuAction {
ParentAction {
action: Box<dyn Action>,
},
ViewAction {
action: Box<dyn Action>,
for_view: usize,
},
}
impl ContextMenuAction {
fn id(&self) -> TypeId {
match self {
ContextMenuAction::ParentAction { action } => action.id(),
ContextMenuAction::ViewAction { action, .. } => action.id(),
}
}
}
pub enum ContextMenuItem {
Item {
label: Cow<'static, str>,
action: Box<dyn Action>,
label: ContextMenuItemLabel,
action: ContextMenuAction,
},
Static(StaticItem),
Separator,
}
impl ContextMenuItem {
pub fn element_item(label: ContextMenuItemBuilder, action: impl 'static + Action) -> Self {
Self::Item {
label: ContextMenuItemLabel::Element(label),
action: ContextMenuAction::ParentAction {
action: Box::new(action),
},
}
}
pub fn item(label: impl Into<Cow<'static, str>>, action: impl 'static + Action) -> Self {
Self::Item {
label: label.into(),
action: Box::new(action),
label: ContextMenuItemLabel::String(label.into()),
action: ContextMenuAction::ParentAction {
action: Box::new(action),
},
}
}
pub fn item_for_view(
label: impl Into<Cow<'static, str>>,
view_id: usize,
action: impl 'static + Action,
) -> Self {
Self::Item {
label: ContextMenuItemLabel::String(label.into()),
action: ContextMenuAction::ViewAction {
action: Box::new(action),
for_view: view_id,
},
}
}
@@ -168,7 +219,15 @@ impl ContextMenu {
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.selected_index {
if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
cx.dispatch_any_action(action.boxed_clone());
match action {
ContextMenuAction::ParentAction { action } => {
cx.dispatch_any_action(action.boxed_clone())
}
ContextMenuAction::ViewAction { action, for_view } => {
let window_id = cx.window_id();
cx.dispatch_any_action_at(window_id, *for_view, action.boxed_clone())
}
};
self.reset(cx);
}
}
@@ -278,10 +337,17 @@ impl ContextMenu {
Some(ix) == self.selected_index,
);
Label::new(label.to_string(), style.label.clone())
.contained()
.with_style(style.container)
.boxed()
match label {
ContextMenuItemLabel::String(label) => {
Label::new(label.to_string(), style.label.clone())
.contained()
.with_style(style.container)
.boxed()
}
ContextMenuItemLabel::Element(element) => {
element(&mut Default::default(), style)
}
}
}
ContextMenuItem::Static(f) => f(cx),
@@ -306,9 +372,18 @@ impl ContextMenu {
&mut Default::default(),
Some(ix) == self.selected_index,
);
let (action, view_id) = match action {
ContextMenuAction::ParentAction { action } => {
(action.boxed_clone(), self.parent_view_id)
}
ContextMenuAction::ViewAction { action, for_view } => {
(action.boxed_clone(), *for_view)
}
};
KeystrokeLabel::new(
window_id,
self.parent_view_id,
view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),
@@ -347,22 +422,34 @@ impl ContextMenu {
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item {
ContextMenuItem::Item { label, action } => {
let action = action.boxed_clone();
let (action, view_id) = match action {
ContextMenuAction::ParentAction { action } => {
(action.boxed_clone(), self.parent_view_id)
}
ContextMenuAction::ViewAction { action, for_view } => {
(action.boxed_clone(), *for_view)
}
};
MouseEventHandler::<MenuItem>::new(ix, cx, |state, _| {
let style =
style.item.style_for(state, Some(ix) == self.selected_index);
Flex::row()
.with_child(
Label::new(label.clone(), style.label.clone())
.contained()
.boxed(),
)
.with_child(match label {
ContextMenuItemLabel::String(label) => {
Label::new(label.clone(), style.label.clone())
.contained()
.boxed()
}
ContextMenuItemLabel::Element(element) => {
element(state, style)
}
})
.with_child({
KeystrokeLabel::new(
window_id,
self.parent_view_id,
view_id,
action.boxed_clone(),
style.keystroke.container,
style.keystroke.text.clone(),
@@ -375,9 +462,12 @@ impl ContextMenu {
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_up(MouseButton::Left, |_, _| {}) // Capture these events
.on_down(MouseButton::Left, |_, _| {}) // Capture these events
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(Clicked);
cx.dispatch_any_action(action.boxed_clone());
let window_id = cx.window_id();
cx.dispatch_any_action_at(window_id, view_id, action.boxed_clone());
})
.on_drag(MouseButton::Left, |_, _| {})
.boxed()

39
crates/copilot/Cargo.toml Normal file
View File

@@ -0,0 +1,39 @@
[package]
name = "copilot"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/copilot.rs"
doctest = false
[dependencies]
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
gpui = { path = "../gpui" }
language = { path = "../language" }
settings = { path = "../settings" }
theme = { path = "../theme" }
lsp = { path = "../lsp" }
node_runtime = { path = "../node_runtime"}
util = { path = "../util" }
client = { path = "../client" }
workspace = { path = "../workspace" }
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
async-tar = "0.4.2"
anyhow = "1.0"
log = "0.4"
serde = { workspace = true }
serde_derive = { workspace = true }
smol = "1.2.5"
futures = "0.3"
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

View File

@@ -0,0 +1,630 @@
mod request;
mod sign_in;
use anyhow::{anyhow, Context, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use client::Client;
use futures::{future::Shared, Future, FutureExt, TryFutureExt};
use gpui::{
actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
Task,
};
use language::{point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, ToPointUtf16};
use lsp::LanguageServer;
use node_runtime::NodeRuntime;
use settings::Settings;
use smol::{fs, io::BufReader, stream::StreamExt};
use std::{
ffi::OsString,
ops::Range,
path::{Path, PathBuf},
sync::Arc,
};
use util::{
fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt,
};
const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
actions!(copilot_auth, [SignIn, SignOut]);
const COPILOT_NAMESPACE: &'static str = "copilot";
actions!(
copilot,
[NextSuggestion, PreviousSuggestion, Toggle, Reinstall]
);
pub fn init(client: Arc<Client>, node_runtime: Arc<NodeRuntime>, cx: &mut MutableAppContext) {
let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), node_runtime, cx));
cx.set_global(copilot.clone());
cx.add_global_action(|_: &SignIn, cx| {
let copilot = Copilot::global(cx).unwrap();
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach_and_log_err(cx);
});
cx.add_global_action(|_: &SignOut, cx| {
let copilot = Copilot::global(cx).unwrap();
copilot
.update(cx, |copilot, cx| copilot.sign_out(cx))
.detach_and_log_err(cx);
});
cx.add_global_action(|_: &Reinstall, cx| {
let copilot = Copilot::global(cx).unwrap();
copilot
.update(cx, |copilot, cx| copilot.reinstall(cx))
.detach();
});
cx.observe(&copilot, |handle, cx| {
let status = handle.read(cx).status();
cx.update_global::<collections::CommandPaletteFilter, _, _>(
move |filter, _cx| match status {
Status::Disabled => {
filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE);
}
Status::Authorized => {
filter.filtered_namespaces.remove(COPILOT_NAMESPACE);
filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
}
_ => {
filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
}
},
);
})
.detach();
sign_in::init(cx);
}
enum CopilotServer {
Disabled,
Starting {
task: Shared<Task<()>>,
},
Error(Arc<str>),
Started {
server: Arc<LanguageServer>,
status: SignInStatus,
},
}
#[derive(Clone, Debug)]
enum SignInStatus {
Authorized {
_user: String,
},
Unauthorized {
_user: String,
},
SigningIn {
prompt: Option<request::PromptUserDeviceFlow>,
task: Shared<Task<Result<(), Arc<anyhow::Error>>>>,
},
SignedOut,
}
#[derive(Debug, Clone)]
pub enum Status {
Starting {
task: Shared<Task<()>>,
},
Error(Arc<str>),
Disabled,
SignedOut,
SigningIn {
prompt: Option<request::PromptUserDeviceFlow>,
},
Unauthorized,
Authorized,
}
impl Status {
pub fn is_authorized(&self) -> bool {
matches!(self, Status::Authorized)
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct Completion {
pub range: Range<Anchor>,
pub text: String,
}
pub struct Copilot {
http: Arc<dyn HttpClient>,
node_runtime: Arc<NodeRuntime>,
server: CopilotServer,
}
impl Entity for Copilot {
type Event = ();
}
impl Copilot {
pub fn starting_task(&self) -> Option<Shared<Task<()>>> {
match self.server {
CopilotServer::Starting { ref task } => Some(task.clone()),
_ => None,
}
}
pub fn global(cx: &AppContext) -> Option<ModelHandle<Self>> {
if cx.has_global::<ModelHandle<Self>>() {
Some(cx.global::<ModelHandle<Self>>().clone())
} else {
None
}
}
fn start(
http: Arc<dyn HttpClient>,
node_runtime: Arc<NodeRuntime>,
cx: &mut ModelContext<Self>,
) -> Self {
cx.observe_global::<Settings, _>({
let http = http.clone();
let node_runtime = node_runtime.clone();
move |this, cx| {
if cx.global::<Settings>().enable_copilot_integration {
if matches!(this.server, CopilotServer::Disabled) {
let start_task = cx
.spawn({
let http = http.clone();
let node_runtime = node_runtime.clone();
move |this, cx| {
Self::start_language_server(http, node_runtime, this, cx)
}
})
.shared();
this.server = CopilotServer::Starting { task: start_task };
cx.notify();
}
} else {
this.server = CopilotServer::Disabled;
cx.notify();
}
}
})
.detach();
if cx.global::<Settings>().enable_copilot_integration {
let start_task = cx
.spawn({
let http = http.clone();
let node_runtime = node_runtime.clone();
move |this, cx| Self::start_language_server(http, node_runtime, this, cx)
})
.shared();
Self {
http,
node_runtime,
server: CopilotServer::Starting { task: start_task },
}
} else {
Self {
http,
node_runtime,
server: CopilotServer::Disabled,
}
}
}
fn start_language_server(
http: Arc<dyn HttpClient>,
node_runtime: Arc<NodeRuntime>,
this: ModelHandle<Self>,
mut cx: AsyncAppContext,
) -> impl Future<Output = ()> {
async move {
let start_language_server = async {
let server_path = get_copilot_lsp(http).await?;
let node_path = node_runtime.binary_path().await?;
let arguments: &[OsString] = &[server_path.into(), "--stdio".into()];
let server = LanguageServer::new(
0,
&node_path,
arguments,
Path::new("/"),
None,
cx.clone(),
)?;
let server = server.initialize(Default::default()).await?;
let status = server
.request::<request::CheckStatus>(request::CheckStatusParams {
local_checks_only: false,
})
.await?;
anyhow::Ok((server, status))
};
let server = start_language_server.await;
this.update(&mut cx, |this, cx| {
cx.notify();
match server {
Ok((server, status)) => {
this.server = CopilotServer::Started {
server,
status: SignInStatus::SignedOut,
};
this.update_sign_in_status(status, cx);
}
Err(error) => {
this.server = CopilotServer::Error(error.to_string().into());
cx.notify()
}
}
})
}
}
fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if let CopilotServer::Started { server, status } = &mut self.server {
let task = match status {
SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => {
Task::ready(Ok(())).shared()
}
SignInStatus::SigningIn { task, .. } => {
cx.notify();
task.clone()
}
SignInStatus::SignedOut => {
let server = server.clone();
let task = cx
.spawn(|this, mut cx| async move {
let sign_in = async {
let sign_in = server
.request::<request::SignInInitiate>(
request::SignInInitiateParams {},
)
.await?;
match sign_in {
request::SignInInitiateResult::AlreadySignedIn { user } => {
Ok(request::SignInStatus::Ok { user })
}
request::SignInInitiateResult::PromptUserDeviceFlow(flow) => {
this.update(&mut cx, |this, cx| {
if let CopilotServer::Started { status, .. } =
&mut this.server
{
if let SignInStatus::SigningIn {
prompt: prompt_flow,
..
} = status
{
*prompt_flow = Some(flow.clone());
cx.notify();
}
}
});
let response = server
.request::<request::SignInConfirm>(
request::SignInConfirmParams {
user_code: flow.user_code,
},
)
.await?;
Ok(response)
}
}
};
let sign_in = sign_in.await;
this.update(&mut cx, |this, cx| match sign_in {
Ok(status) => {
this.update_sign_in_status(status, cx);
Ok(())
}
Err(error) => {
this.update_sign_in_status(
request::SignInStatus::NotSignedIn,
cx,
);
Err(Arc::new(error))
}
})
})
.shared();
*status = SignInStatus::SigningIn {
prompt: None,
task: task.clone(),
};
cx.notify();
task
}
};
cx.foreground()
.spawn(task.map_err(|err| anyhow!("{:?}", err)))
} else {
// If we're downloading, wait until download is finished
// If we're in a stuck state, display to the user
Task::ready(Err(anyhow!("copilot hasn't started yet")))
}
}
fn sign_out(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if let CopilotServer::Started { server, status } = &mut self.server {
*status = SignInStatus::SignedOut;
cx.notify();
let server = server.clone();
cx.background().spawn(async move {
server
.request::<request::SignOut>(request::SignOutParams {})
.await?;
anyhow::Ok(())
})
} else {
Task::ready(Err(anyhow!("copilot hasn't started yet")))
}
}
fn reinstall(&mut self, cx: &mut ModelContext<Self>) -> Task<()> {
let start_task = cx
.spawn({
let http = self.http.clone();
let node_runtime = self.node_runtime.clone();
move |this, cx| async move {
clear_copilot_dir().await;
Self::start_language_server(http, node_runtime, this, cx).await
}
})
.shared();
self.server = CopilotServer::Starting {
task: start_task.clone(),
};
cx.notify();
cx.foreground().spawn(start_task)
}
pub fn completion<T>(
&self,
buffer: &ModelHandle<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Completion>>>
where
T: ToPointUtf16,
{
let server = match self.authorized_server() {
Ok(server) => server,
Err(error) => return Task::ready(Err(error)),
};
let buffer = buffer.read(cx).snapshot();
let request = server
.request::<request::GetCompletions>(build_completion_params(&buffer, position, cx));
cx.background().spawn(async move {
let result = request.await?;
let completion = result
.completions
.into_iter()
.next()
.map(|completion| completion_from_lsp(completion, &buffer));
anyhow::Ok(completion)
})
}
pub fn completions_cycling<T>(
&self,
buffer: &ModelHandle<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Completion>>>
where
T: ToPointUtf16,
{
let server = match self.authorized_server() {
Ok(server) => server,
Err(error) => return Task::ready(Err(error)),
};
let buffer = buffer.read(cx).snapshot();
let request = server.request::<request::GetCompletionsCycling>(build_completion_params(
&buffer, position, cx,
));
cx.background().spawn(async move {
let result = request.await?;
let completions = result
.completions
.into_iter()
.map(|completion| completion_from_lsp(completion, &buffer))
.collect();
anyhow::Ok(completions)
})
}
pub fn status(&self) -> Status {
match &self.server {
CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
CopilotServer::Disabled => Status::Disabled,
CopilotServer::Error(error) => Status::Error(error.clone()),
CopilotServer::Started { status, .. } => match status {
SignInStatus::Authorized { .. } => Status::Authorized,
SignInStatus::Unauthorized { .. } => Status::Unauthorized,
SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
prompt: prompt.clone(),
},
SignInStatus::SignedOut => Status::SignedOut,
},
}
}
fn update_sign_in_status(
&mut self,
lsp_status: request::SignInStatus,
cx: &mut ModelContext<Self>,
) {
if let CopilotServer::Started { status, .. } = &mut self.server {
*status = match lsp_status {
request::SignInStatus::Ok { user }
| request::SignInStatus::MaybeOk { user }
| request::SignInStatus::AlreadySignedIn { user } => {
SignInStatus::Authorized { _user: user }
}
request::SignInStatus::NotAuthorized { user } => {
SignInStatus::Unauthorized { _user: user }
}
request::SignInStatus::NotSignedIn => SignInStatus::SignedOut,
};
cx.notify();
}
}
fn authorized_server(&self) -> Result<Arc<LanguageServer>> {
match &self.server {
CopilotServer::Starting { .. } => Err(anyhow!("copilot is still starting")),
CopilotServer::Disabled => Err(anyhow!("copilot is disabled")),
CopilotServer::Error(error) => Err(anyhow!(
"copilot was not started because of an error: {}",
error
)),
CopilotServer::Started { server, status } => {
if matches!(status, SignInStatus::Authorized { .. }) {
Ok(server.clone())
} else {
Err(anyhow!("must sign in before using copilot"))
}
}
}
}
}
fn build_completion_params<T>(
buffer: &BufferSnapshot,
position: T,
cx: &AppContext,
) -> request::GetCompletionsParams
where
T: ToPointUtf16,
{
let position = position.to_point_utf16(&buffer);
let language_name = buffer.language_at(position).map(|language| language.name());
let language_name = language_name.as_deref();
let path;
let relative_path;
if let Some(file) = buffer.file() {
if let Some(file) = file.as_local() {
path = file.abs_path(cx);
} else {
path = file.full_path(cx);
}
relative_path = file.path().to_path_buf();
} else {
path = PathBuf::from("/untitled");
relative_path = PathBuf::from("untitled");
}
let settings = cx.global::<Settings>();
let language_id = match language_name {
Some("Plain Text") => "plaintext".to_string(),
Some(language_name) => language_name.to_lowercase(),
None => "plaintext".to_string(),
};
request::GetCompletionsParams {
doc: request::GetCompletionsDocument {
source: buffer.text(),
tab_size: settings.tab_size(language_name).into(),
indent_size: 1,
insert_spaces: !settings.hard_tabs(language_name),
uri: lsp::Url::from_file_path(&path).unwrap(),
path: path.to_string_lossy().into(),
relative_path: relative_path.to_string_lossy().into(),
language_id,
position: point_to_lsp(position),
version: 0,
},
}
}
fn completion_from_lsp(completion: request::Completion, buffer: &BufferSnapshot) -> Completion {
let start = buffer.clip_point_utf16(point_from_lsp(completion.range.start), Bias::Left);
let end = buffer.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
Completion {
range: buffer.anchor_before(start)..buffer.anchor_after(end),
text: completion.text,
}
}
async fn clear_copilot_dir() {
remove_matching(&paths::COPILOT_DIR, |_| true).await
}
async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
const SERVER_PATH: &'static str = "dist/agent.js";
///Check for the latest copilot language server and download it if we haven't already
async fn fetch_latest(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
let release = latest_github_release("zed-industries/copilot", http.clone()).await?;
let version_dir = &*paths::COPILOT_DIR.join(format!("copilot-{}", release.name));
fs::create_dir_all(version_dir).await?;
let server_path = version_dir.join(SERVER_PATH);
if fs::metadata(&server_path).await.is_err() {
// Copilot LSP looks for this dist dir specifcially, so lets add it in.
let dist_dir = version_dir.join("dist");
fs::create_dir_all(dist_dir.as_path()).await?;
let url = &release
.assets
.get(0)
.context("Github release for copilot contained no assets")?
.browser_download_url;
let mut response = http
.get(&url, Default::default(), true)
.await
.map_err(|err| anyhow!("error downloading copilot release: {}", err))?;
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
let archive = Archive::new(decompressed_bytes);
archive.unpack(dist_dir).await?;
remove_matching(&paths::COPILOT_DIR, |entry| entry != version_dir).await;
}
Ok(server_path)
}
match fetch_latest(http).await {
ok @ Result::Ok(..) => ok,
e @ Err(..) => {
e.log_err();
// Fetch a cached binary, if it exists
(|| async move {
let mut last_version_dir = None;
let mut entries = fs::read_dir(paths::COPILOT_DIR.as_path()).await?;
while let Some(entry) = entries.next().await {
let entry = entry?;
if entry.file_type().await?.is_dir() {
last_version_dir = Some(entry.path());
}
}
let last_version_dir =
last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
let server_path = last_version_dir.join(SERVER_PATH);
if server_path.exists() {
Ok(server_path)
} else {
Err(anyhow!(
"missing executable in directory {:?}",
last_version_dir
))
}
})()
.await
}
}
}

View File

@@ -0,0 +1,3 @@
use gpui::MutableAppContext;
fn init(cx: &mut MutableAppContext) {}

View File

@@ -0,0 +1,142 @@
use serde::{Deserialize, Serialize};
pub enum CheckStatus {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CheckStatusParams {
pub local_checks_only: bool,
}
impl lsp::request::Request for CheckStatus {
type Params = CheckStatusParams;
type Result = SignInStatus;
const METHOD: &'static str = "checkStatus";
}
pub enum SignInInitiate {}
#[derive(Debug, Serialize, Deserialize)]
pub struct SignInInitiateParams {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "status")]
pub enum SignInInitiateResult {
AlreadySignedIn { user: String },
PromptUserDeviceFlow(PromptUserDeviceFlow),
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptUserDeviceFlow {
pub user_code: String,
pub verification_uri: String,
}
impl lsp::request::Request for SignInInitiate {
type Params = SignInInitiateParams;
type Result = SignInInitiateResult;
const METHOD: &'static str = "signInInitiate";
}
pub enum SignInConfirm {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignInConfirmParams {
pub user_code: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "status")]
pub enum SignInStatus {
#[serde(rename = "OK")]
Ok {
user: String,
},
MaybeOk {
user: String,
},
AlreadySignedIn {
user: String,
},
NotAuthorized {
user: String,
},
NotSignedIn,
}
impl lsp::request::Request for SignInConfirm {
type Params = SignInConfirmParams;
type Result = SignInStatus;
const METHOD: &'static str = "signInConfirm";
}
pub enum SignOut {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignOutParams {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignOutResult {}
impl lsp::request::Request for SignOut {
type Params = SignOutParams;
type Result = SignOutResult;
const METHOD: &'static str = "signOut";
}
pub enum GetCompletions {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetCompletionsParams {
pub doc: GetCompletionsDocument,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetCompletionsDocument {
pub source: String,
pub tab_size: u32,
pub indent_size: u32,
pub insert_spaces: bool,
pub uri: lsp::Url,
pub path: String,
pub relative_path: String,
pub language_id: String,
pub position: lsp::Position,
pub version: usize,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetCompletionsResult {
pub completions: Vec<Completion>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Completion {
pub text: String,
pub position: lsp::Position,
pub uuid: String,
pub range: lsp::Range,
pub display_text: String,
}
impl lsp::request::Request for GetCompletions {
type Params = GetCompletionsParams;
type Result = GetCompletionsResult;
const METHOD: &'static str = "getCompletions";
}
pub enum GetCompletionsCycling {}
impl lsp::request::Request for GetCompletionsCycling {
type Params = GetCompletionsParams;
type Result = GetCompletionsResult;
const METHOD: &'static str = "getCompletionsCycling";
}

View File

@@ -0,0 +1,344 @@
use crate::{request::PromptUserDeviceFlow, Copilot, Status};
use gpui::{
elements::*, geometry::rect::RectF, ClipboardItem, Element, Entity, MutableAppContext, View,
ViewContext, ViewHandle, WindowKind, WindowOptions,
};
use settings::Settings;
use theme::ui::modal;
#[derive(PartialEq, Eq, Debug, Clone)]
struct CopyUserCode;
#[derive(PartialEq, Eq, Debug, Clone)]
struct OpenGithub;
const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
pub fn init(cx: &mut MutableAppContext) {
let copilot = Copilot::global(cx).unwrap();
let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None;
cx.observe(&copilot, move |copilot, cx| {
let status = copilot.read(cx).status();
match &status {
crate::Status::SigningIn { prompt } => {
if let Some(code_verification) = code_verification.as_ref() {
code_verification.update(cx, |code_verification, cx| {
code_verification.set_status(status, cx)
});
cx.activate_window(code_verification.window_id());
} else if let Some(_prompt) = prompt {
let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
let window_options = WindowOptions {
bounds: gpui::WindowBounds::Fixed(RectF::new(
Default::default(),
window_size,
)),
titlebar: None,
center: true,
focus: true,
kind: WindowKind::Normal,
is_movable: true,
screen: None,
};
let (_, view) =
cx.add_window(window_options, |_cx| CopilotCodeVerification::new(status));
code_verification = Some(view);
}
}
Status::Authorized | Status::Unauthorized => {
if let Some(code_verification) = code_verification.as_ref() {
code_verification.update(cx, |code_verification, cx| {
code_verification.set_status(status, cx)
});
cx.platform().activate(true);
cx.activate_window(code_verification.window_id());
}
}
_ => {
if let Some(code_verification) = code_verification.take() {
cx.remove_window(code_verification.window_id());
}
}
}
})
.detach();
}
pub struct CopilotCodeVerification {
status: Status,
}
impl CopilotCodeVerification {
pub fn new(status: Status) -> Self {
Self { status }
}
pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
self.status = status;
cx.notify();
}
fn render_device_code(
data: &PromptUserDeviceFlow,
style: &theme::Copilot,
cx: &mut gpui::RenderContext<Self>,
) -> ElementBox {
let copied = cx
.read_from_clipboard()
.map(|item| item.text() == &data.user_code)
.unwrap_or(false);
let device_code_style = &style.auth.prompting.device_code;
MouseEventHandler::<Self>::new(0, cx, |state, _cx| {
Flex::row()
.with_children([
Label::new(data.user_code.clone(), device_code_style.text.clone())
.aligned()
.contained()
.with_style(device_code_style.left_container)
.constrained()
.with_width(device_code_style.left)
.boxed(),
Label::new(
if copied { "Copied!" } else { "Copy" },
device_code_style.cta.style_for(state, false).text.clone(),
)
.aligned()
.contained()
.with_style(*device_code_style.right_container.style_for(state, false))
.constrained()
.with_width(device_code_style.right)
.boxed(),
])
.contained()
.with_style(device_code_style.cta.style_for(state, false).container)
.boxed()
})
.on_click(gpui::MouseButton::Left, {
let user_code = data.user_code.clone();
move |_, cx| {
cx.platform()
.write_to_clipboard(ClipboardItem::new(user_code.clone()));
cx.notify();
}
})
.with_cursor_style(gpui::CursorStyle::PointingHand)
.boxed()
}
fn render_prompting_modal(
data: &PromptUserDeviceFlow,
style: &theme::Copilot,
cx: &mut gpui::RenderContext<Self>,
) -> ElementBox {
Flex::column()
.with_children([
Flex::column()
.with_children([
Label::new(
"Enable Copilot by connecting",
style.auth.prompting.subheading.text.clone(),
)
.aligned()
.boxed(),
Label::new(
"your existing license.",
style.auth.prompting.subheading.text.clone(),
)
.aligned()
.boxed(),
])
.align_children_center()
.contained()
.with_style(style.auth.prompting.subheading.container)
.boxed(),
Self::render_device_code(data, &style, cx),
Flex::column()
.with_children([
Label::new(
"Paste this code into GitHub after",
style.auth.prompting.hint.text.clone(),
)
.aligned()
.boxed(),
Label::new(
"clicking the button below.",
style.auth.prompting.hint.text.clone(),
)
.aligned()
.boxed(),
])
.align_children_center()
.contained()
.with_style(style.auth.prompting.hint.container.clone())
.boxed(),
theme::ui::cta_button_with_click(
"Connect to GitHub",
style.auth.content_width,
&style.auth.cta_button,
cx,
{
let verification_uri = data.verification_uri.clone();
move |_, cx| cx.platform().open_url(&verification_uri)
},
)
.boxed(),
])
.align_children_center()
.boxed()
}
fn render_enabled_modal(
style: &theme::Copilot,
cx: &mut gpui::RenderContext<Self>,
) -> ElementBox {
let enabled_style = &style.auth.authorized;
Flex::column()
.with_children([
Label::new("Copilot Enabled!", enabled_style.subheading.text.clone())
.contained()
.with_style(enabled_style.subheading.container)
.aligned()
.boxed(),
Flex::column()
.with_children([
Label::new(
"You can update your settings or",
enabled_style.hint.text.clone(),
)
.aligned()
.boxed(),
Label::new(
"sign out from the Copilot menu in",
enabled_style.hint.text.clone(),
)
.aligned()
.boxed(),
Label::new("the status bar.", enabled_style.hint.text.clone())
.aligned()
.boxed(),
])
.align_children_center()
.contained()
.with_style(enabled_style.hint.container)
.boxed(),
theme::ui::cta_button_with_click(
"Done",
style.auth.content_width,
&style.auth.cta_button,
cx,
|_, cx| {
let window_id = cx.window_id();
cx.remove_window(window_id)
},
)
.boxed(),
])
.align_children_center()
.boxed()
}
fn render_unauthorized_modal(
style: &theme::Copilot,
cx: &mut gpui::RenderContext<Self>,
) -> ElementBox {
let unauthorized_style = &style.auth.not_authorized;
Flex::column()
.with_children([
Flex::column()
.with_children([
Label::new(
"Enable Copilot by connecting",
unauthorized_style.subheading.text.clone(),
)
.aligned()
.boxed(),
Label::new(
"your existing license.",
unauthorized_style.subheading.text.clone(),
)
.aligned()
.boxed(),
])
.align_children_center()
.contained()
.with_style(unauthorized_style.subheading.container)
.boxed(),
Flex::column()
.with_children([
Label::new(
"You must have an active copilot",
unauthorized_style.warning.text.clone(),
)
.aligned()
.boxed(),
Label::new(
"license to use it in Zed.",
unauthorized_style.warning.text.clone(),
)
.aligned()
.boxed(),
])
.align_children_center()
.contained()
.with_style(unauthorized_style.warning.container)
.boxed(),
theme::ui::cta_button_with_click(
"Subscribe on GitHub",
style.auth.content_width,
&style.auth.cta_button,
cx,
|_, cx| {
let window_id = cx.window_id();
cx.remove_window(window_id);
cx.platform().open_url(COPILOT_SIGN_UP_URL)
},
)
.boxed(),
])
.align_children_center()
.boxed()
}
}
impl Entity for CopilotCodeVerification {
type Event = ();
}
impl View for CopilotCodeVerification {
fn ui_name() -> &'static str {
"CopilotCodeVerification"
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext<Self>) {
cx.notify()
}
fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext<Self>) {
cx.notify()
}
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
let style = cx.global::<Settings>().theme.clone();
modal("Connect Copilot to Zed", &style.copilot.modal, cx, |cx| {
Flex::column()
.with_children([
theme::ui::icon(&style.copilot.auth.header).boxed(),
match &self.status {
Status::SigningIn {
prompt: Some(prompt),
} => Self::render_prompting_modal(&prompt, &style.copilot, cx),
Status::Unauthorized => Self::render_unauthorized_modal(&style.copilot, cx),
Status::Authorized => Self::render_enabled_modal(&style.copilot, cx),
_ => Empty::new().boxed(),
},
])
.align_children_center()
.boxed()
})
}
}

View File

@@ -0,0 +1,22 @@
[package]
name = "copilot_button"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/copilot_button.rs"
doctest = false
[dependencies]
copilot = { path = "../copilot" }
editor = { path = "../editor" }
context_menu = { path = "../context_menu" }
gpui = { path = "../gpui" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow = "1.0"
smol = "1.2.5"
futures = "0.3"

View File

@@ -0,0 +1,360 @@
use std::sync::Arc;
use context_menu::{ContextMenu, ContextMenuItem};
use editor::Editor;
use gpui::{
elements::*, impl_internal_actions, CursorStyle, Element, ElementBox, Entity, MouseButton,
MouseState, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle,
};
use settings::{settings_file::SettingsFile, Settings};
use workspace::{
item::ItemHandle, notifications::simple_message_notification::OsOpen, DismissToast,
StatusItemView,
};
use copilot::{Copilot, Reinstall, SignIn, SignOut, Status};
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
const COPILOT_STARTING_TOAST_ID: usize = 1337;
const COPILOT_ERROR_TOAST_ID: usize = 1338;
#[derive(Clone, PartialEq)]
pub struct DeployCopilotMenu;
#[derive(Clone, PartialEq)]
pub struct ToggleCopilotForLanguage {
language: Arc<str>,
}
#[derive(Clone, PartialEq)]
pub struct ToggleCopilotGlobally;
// TODO: Make the other code path use `get_or_insert` logic for this modal
#[derive(Clone, PartialEq)]
pub struct DeployCopilotModal;
impl_internal_actions!(
copilot,
[
DeployCopilotMenu,
DeployCopilotModal,
ToggleCopilotForLanguage,
ToggleCopilotGlobally,
]
);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(CopilotButton::deploy_copilot_menu);
cx.add_action(
|_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| {
let language = action.language.to_owned();
let current_langauge = cx.global::<Settings>().copilot_on(Some(&language));
SettingsFile::update(cx, move |file_contents| {
file_contents.languages.insert(
language.to_owned(),
settings::EditorSettings {
copilot: Some((!current_langauge).into()),
..Default::default()
},
);
})
},
);
cx.add_action(|_: &mut CopilotButton, _: &ToggleCopilotGlobally, cx| {
let copilot_on = cx.global::<Settings>().copilot_on(None);
SettingsFile::update(cx, move |file_contents| {
file_contents.editor.copilot = Some((!copilot_on).into())
})
});
}
pub struct CopilotButton {
popup_menu: ViewHandle<ContextMenu>,
editor_subscription: Option<(Subscription, usize)>,
editor_enabled: Option<bool>,
language: Option<Arc<str>>,
}
impl Entity for CopilotButton {
type Event = ();
}
impl View for CopilotButton {
fn ui_name() -> &'static str {
"CopilotButton"
}
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
let settings = cx.global::<Settings>();
if !settings.enable_copilot_integration {
return Empty::new().boxed();
}
let theme = settings.theme.clone();
let active = self.popup_menu.read(cx).visible();
let Some(copilot) = Copilot::global(cx) else {
return Empty::new().boxed();
};
let status = copilot.read(cx).status();
let enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
let view_id = cx.view_id();
Stack::new()
.with_child(
MouseEventHandler::<Self>::new(0, cx, {
let theme = theme.clone();
let status = status.clone();
move |state, _cx| {
let style = theme
.workspace
.status_bar
.sidebar_buttons
.item
.style_for(state, active);
Flex::row()
.with_child(
Svg::new({
match status {
Status::Error(_) => "icons/copilot_error_16.svg",
Status::Authorized => {
if enabled {
"icons/copilot_16.svg"
} else {
"icons/copilot_disabled_16.svg"
}
}
_ => "icons/copilot_init_16.svg",
}
})
.with_color(style.icon_color)
.constrained()
.with_width(style.icon_size)
.aligned()
.named("copilot-icon"),
)
.constrained()
.with_height(style.icon_size)
.contained()
.with_style(style.container)
.boxed()
}
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, {
let status = status.clone();
move |_, cx| match status {
Status::Authorized => cx.dispatch_action(DeployCopilotMenu),
Status::Starting { ref task } => {
cx.dispatch_action(workspace::Toast::new(
COPILOT_STARTING_TOAST_ID,
"Copilot is starting...",
));
let window_id = cx.window_id();
let task = task.to_owned();
cx.spawn(|mut cx| async move {
task.await;
cx.update(|cx| {
if let Some(copilot) = Copilot::global(cx) {
let status = copilot.read(cx).status();
match status {
Status::Authorized => cx.dispatch_action_at(
window_id,
view_id,
workspace::Toast::new(
COPILOT_STARTING_TOAST_ID,
"Copilot has started!",
),
),
_ => {
cx.dispatch_action_at(
window_id,
view_id,
DismissToast::new(COPILOT_STARTING_TOAST_ID),
);
cx.dispatch_global_action(SignIn)
}
}
}
})
})
.detach();
}
Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action(
COPILOT_ERROR_TOAST_ID,
format!("Copilot can't be started: {}", e),
"Reinstall Copilot",
Reinstall,
)),
_ => cx.dispatch_action(SignIn),
}
})
.with_tooltip::<Self, _>(
0,
"GitHub Copilot".into(),
None,
theme.tooltip.clone(),
cx,
)
.boxed(),
)
.with_child(
ChildView::new(&self.popup_menu, cx)
.aligned()
.top()
.right()
.boxed(),
)
.boxed()
}
}
impl CopilotButton {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
let menu = cx.add_view(|cx| {
let mut menu = ContextMenu::new(cx);
menu.set_position_mode(OverlayPositionMode::Local);
menu
});
cx.observe(&menu, |_, _, cx| cx.notify()).detach();
Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
let this_handle = cx.handle().downgrade();
cx.observe_global::<Settings, _>(move |cx| {
if let Some(handle) = this_handle.upgrade(cx) {
handle.update(cx, |_, cx| cx.notify())
}
})
.detach();
Self {
popup_menu: menu,
editor_subscription: None,
editor_enabled: None,
language: None,
}
}
pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext<Self>) {
let settings = cx.global::<Settings>();
let mut menu_options = Vec::with_capacity(6);
if let Some((_, view_id)) = self.editor_subscription.as_ref() {
let locally_enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
menu_options.push(ContextMenuItem::item_for_view(
if locally_enabled {
"Pause Copilot for this file"
} else {
"Resume Copilot for this file"
},
*view_id,
copilot::Toggle,
));
}
if let Some(language) = &self.language {
let language_enabled = settings.copilot_on(Some(language.as_ref()));
menu_options.push(ContextMenuItem::item(
format!(
"{} Copilot for {}",
if language_enabled {
"Disable"
} else {
"Enable"
},
language
),
ToggleCopilotForLanguage {
language: language.to_owned(),
},
));
}
let globally_enabled = cx.global::<Settings>().copilot_on(None);
menu_options.push(ContextMenuItem::item(
if globally_enabled {
"Disable Copilot Globally"
} else {
"Enable Copilot Locally"
},
ToggleCopilotGlobally,
));
menu_options.push(ContextMenuItem::Separator);
let icon_style = settings.theme.copilot.out_link_icon.clone();
menu_options.push(ContextMenuItem::element_item(
Box::new(
move |state: &mut MouseState, style: &theme::ContextMenuItem| {
Flex::row()
.with_children([
Label::new("Copilot Settings", style.label.clone()).boxed(),
theme::ui::icon(icon_style.style_for(state, false)).boxed(),
])
.align_children_center()
.boxed()
},
),
OsOpen::new(COPILOT_SETTINGS_URL),
));
menu_options.push(ContextMenuItem::item("Sign Out", SignOut));
self.popup_menu.update(cx, |menu, cx| {
menu.show(
Default::default(),
AnchorCorner::BottomRight,
menu_options,
cx,
);
});
}
pub fn update_enabled(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
let editor = editor.read(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
let settings = cx.global::<Settings>();
let suggestion_anchor = editor.selections.newest_anchor().start;
let language_name = snapshot
.language_at(suggestion_anchor)
.map(|language| language.name());
self.language = language_name.clone();
if let Some(enabled) = editor.copilot_state.user_enabled {
self.editor_enabled = Some(enabled);
} else {
self.editor_enabled = Some(settings.copilot_on(language_name.as_deref()));
}
cx.notify()
}
}
impl StatusItemView for CopilotButton {
fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
if let Some(editor) = item.map(|item| item.act_as::<Editor>(cx)).flatten() {
self.editor_subscription =
Some((cx.observe(&editor, Self::update_enabled), editor.id()));
self.update_enabled(editor, cx);
} else {
self.language = None;
self.editor_subscription = None;
self.editor_enabled = None;
}
cx.notify();
}
}

View File

@@ -23,7 +23,8 @@ async-trait = "0.1"
lazy_static = "1.4.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
serde = { version = "1.0", features = ["derive"] }
serde = { workspace = true }
serde_derive = { workspace = true }
smol = "1.2"
[dev-dependencies]

View File

@@ -20,7 +20,7 @@ settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
postage = { version = "0.4", features = ["futures-traits"] }
postage = { workspace = true }
[dev-dependencies]
unindent = "0.1"
@@ -29,4 +29,4 @@ editor = { path = "../editor", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
serde_json = { version = "1", features = ["preserve_order"] }
serde_json = { workspace = true }

View File

@@ -22,10 +22,10 @@ test-support = [
]
[dependencies]
drag_and_drop = { path = "../drag_and_drop" }
text = { path = "../text" }
clock = { path = "../clock" }
copilot = { path = "../copilot" }
db = { path = "../db" }
drag_and_drop = { path = "../drag_and_drop" }
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
fuzzy = { path = "../fuzzy" }
@@ -38,10 +38,12 @@ rpc = { path = "../rpc" }
settings = { path = "../settings" }
snippet = { path = "../snippet" }
sum_tree = { path = "../sum_tree" }
text = { path = "../text" }
theme = { path = "../theme" }
util = { path = "../util" }
sqlez = { path = "../sqlez" }
workspace = { path = "../workspace" }
aho-corasick = "0.7"
anyhow = "1.0"
futures = "0.3"
@@ -51,9 +53,10 @@ lazy_static = "1.4"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
ordered-float = "2.1.1"
parking_lot = "0.11"
postage = { version = "0.4", features = ["futures-traits"] }
postage = { workspace = true }
rand = { version = "0.8.3", optional = true }
serde = { workspace = true }
serde_derive = { workspace = true }
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
tree-sitter-rust = { version = "*", optional = true }

View File

@@ -1,10 +1,11 @@
mod block_map;
mod fold_map;
mod suggestion_map;
mod tab_map;
mod wrap_map;
use crate::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
use block_map::{BlockMap, BlockPoint};
pub use block_map::{BlockMap, BlockPoint};
use collections::{HashMap, HashSet};
use fold_map::FoldMap;
use gpui::{
@@ -15,6 +16,8 @@ use gpui::{
use language::{OffsetUtf16, Point, Subscription as BufferSubscription};
use settings::Settings;
use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
pub use suggestion_map::Suggestion;
use suggestion_map::SuggestionMap;
use sum_tree::{Bias, TreeMap};
use tab_map::TabMap;
use wrap_map::WrapMap;
@@ -40,6 +43,7 @@ pub struct DisplayMap {
buffer: ModelHandle<MultiBuffer>,
buffer_subscription: BufferSubscription,
fold_map: FoldMap,
suggestion_map: SuggestionMap,
tab_map: TabMap,
wrap_map: ModelHandle<WrapMap>,
block_map: BlockMap,
@@ -65,6 +69,7 @@ impl DisplayMap {
let tab_size = Self::tab_size(&buffer, cx);
let (fold_map, snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
let (suggestion_map, snapshot) = SuggestionMap::new(snapshot);
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx);
let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height);
@@ -73,6 +78,7 @@ impl DisplayMap {
buffer,
buffer_subscription,
fold_map,
suggestion_map,
tab_map,
wrap_map,
block_map,
@@ -84,21 +90,25 @@ impl DisplayMap {
pub fn snapshot(&self, cx: &mut ModelContext<Self>) -> DisplaySnapshot {
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let (folds_snapshot, edits) = self.fold_map.read(buffer_snapshot, edits);
let (fold_snapshot, edits) = self.fold_map.read(buffer_snapshot, edits);
let (suggestion_snapshot, edits) = self.suggestion_map.sync(fold_snapshot.clone(), edits);
let tab_size = Self::tab_size(&self.buffer, cx);
let (tabs_snapshot, edits) = self.tab_map.sync(folds_snapshot.clone(), edits, tab_size);
let (wraps_snapshot, edits) = self
let (tab_snapshot, edits) = self
.tab_map
.sync(suggestion_snapshot.clone(), edits, tab_size);
let (wrap_snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), edits, cx));
let blocks_snapshot = self.block_map.read(wraps_snapshot.clone(), edits);
.update(cx, |map, cx| map.sync(tab_snapshot.clone(), edits, cx));
let block_snapshot = self.block_map.read(wrap_snapshot.clone(), edits);
DisplaySnapshot {
buffer_snapshot: self.buffer.read(cx).snapshot(cx),
folds_snapshot,
tabs_snapshot,
wraps_snapshot,
blocks_snapshot,
fold_snapshot,
suggestion_snapshot,
tab_snapshot,
wrap_snapshot,
block_snapshot,
text_highlights: self.text_highlights.clone(),
clip_at_line_ends: self.clip_at_line_ends,
}
@@ -122,12 +132,14 @@ impl DisplayMap {
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.fold(ranges);
let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
@@ -145,12 +157,14 @@ impl DisplayMap {
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
@@ -167,6 +181,7 @@ impl DisplayMap {
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
@@ -184,6 +199,7 @@ impl DisplayMap {
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
@@ -214,6 +230,25 @@ impl DisplayMap {
self.text_highlights.remove(&Some(type_id))
}
pub fn replace_suggestion<T>(
&self,
new_suggestion: Option<Suggestion<T>>,
cx: &mut ModelContext<Self>,
) where
T: ToPoint,
{
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.suggestion_map.replace(new_suggestion, snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
}
pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) -> bool {
self.wrap_map
.update(cx, |map, cx| map.set_font(font_id, font_size, cx))
@@ -246,10 +281,11 @@ impl DisplayMap {
pub struct DisplaySnapshot {
pub buffer_snapshot: MultiBufferSnapshot,
folds_snapshot: fold_map::FoldSnapshot,
tabs_snapshot: tab_map::TabSnapshot,
wraps_snapshot: wrap_map::WrapSnapshot,
blocks_snapshot: block_map::BlockSnapshot,
fold_snapshot: fold_map::FoldSnapshot,
suggestion_snapshot: suggestion_map::SuggestionSnapshot,
tab_snapshot: tab_map::TabSnapshot,
wrap_snapshot: wrap_map::WrapSnapshot,
block_snapshot: block_map::BlockSnapshot,
text_highlights: TextHighlights,
clip_at_line_ends: bool,
}
@@ -257,7 +293,7 @@ pub struct DisplaySnapshot {
impl DisplaySnapshot {
#[cfg(test)]
pub fn fold_count(&self) -> usize {
self.folds_snapshot.fold_count()
self.fold_snapshot.fold_count()
}
pub fn is_empty(&self) -> bool {
@@ -265,7 +301,7 @@ impl DisplaySnapshot {
}
pub fn buffer_rows(&self, start_row: u32) -> DisplayBufferRows {
self.blocks_snapshot.buffer_rows(start_row)
self.block_snapshot.buffer_rows(start_row)
}
pub fn max_buffer_row(&self) -> u32 {
@@ -274,9 +310,9 @@ impl DisplaySnapshot {
pub fn prev_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
loop {
let mut fold_point = self.folds_snapshot.to_fold_point(point, Bias::Left);
let mut fold_point = self.fold_snapshot.to_fold_point(point, Bias::Left);
*fold_point.column_mut() = 0;
point = fold_point.to_buffer_point(&self.folds_snapshot);
point = fold_point.to_buffer_point(&self.fold_snapshot);
let mut display_point = self.point_to_display_point(point, Bias::Left);
*display_point.column_mut() = 0;
@@ -290,9 +326,9 @@ impl DisplaySnapshot {
pub fn next_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
loop {
let mut fold_point = self.folds_snapshot.to_fold_point(point, Bias::Right);
*fold_point.column_mut() = self.folds_snapshot.line_len(fold_point.row());
point = fold_point.to_buffer_point(&self.folds_snapshot);
let mut fold_point = self.fold_snapshot.to_fold_point(point, Bias::Right);
*fold_point.column_mut() = self.fold_snapshot.line_len(fold_point.row());
point = fold_point.to_buffer_point(&self.fold_snapshot);
let mut display_point = self.point_to_display_point(point, Bias::Right);
*display_point.column_mut() = self.line_len(display_point.row());
@@ -322,37 +358,39 @@ impl DisplaySnapshot {
}
fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint {
let fold_point = self.folds_snapshot.to_fold_point(point, bias);
let tab_point = self.tabs_snapshot.to_tab_point(fold_point);
let wrap_point = self.wraps_snapshot.tab_point_to_wrap_point(tab_point);
let block_point = self.blocks_snapshot.to_block_point(wrap_point);
let fold_point = self.fold_snapshot.to_fold_point(point, bias);
let suggestion_point = self.suggestion_snapshot.to_suggestion_point(fold_point);
let tab_point = self.tab_snapshot.to_tab_point(suggestion_point);
let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
let block_point = self.block_snapshot.to_block_point(wrap_point);
DisplayPoint(block_point)
}
fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
let block_point = point.0;
let wrap_point = self.blocks_snapshot.to_wrap_point(block_point);
let tab_point = self.wraps_snapshot.to_tab_point(wrap_point);
let fold_point = self.tabs_snapshot.to_fold_point(tab_point, bias).0;
fold_point.to_buffer_point(&self.folds_snapshot)
let wrap_point = self.block_snapshot.to_wrap_point(block_point);
let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
let suggestion_point = self.tab_snapshot.to_suggestion_point(tab_point, bias).0;
let fold_point = self.suggestion_snapshot.to_fold_point(suggestion_point);
fold_point.to_buffer_point(&self.fold_snapshot)
}
pub fn max_point(&self) -> DisplayPoint {
DisplayPoint(self.blocks_snapshot.max_point())
DisplayPoint(self.block_snapshot.max_point())
}
/// Returns text chunks starting at the given display row until the end of the file
pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
self.blocks_snapshot
.chunks(display_row..self.max_point().row() + 1, false, None)
self.block_snapshot
.chunks(display_row..self.max_point().row() + 1, false, None, None)
.map(|h| h.text)
}
/// Returns text chunks starting at the end of the given display row in reverse until the start of the file
pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
(0..=display_row).into_iter().rev().flat_map(|row| {
self.blocks_snapshot
.chunks(row..row + 1, false, None)
self.block_snapshot
.chunks(row..row + 1, false, None, None)
.map(|h| h.text)
.collect::<Vec<_>>()
.into_iter()
@@ -360,16 +398,25 @@ impl DisplaySnapshot {
})
}
pub fn chunks(&self, display_rows: Range<u32>, language_aware: bool) -> DisplayChunks<'_> {
self.blocks_snapshot
.chunks(display_rows, language_aware, Some(&self.text_highlights))
pub fn chunks(
&self,
display_rows: Range<u32>,
language_aware: bool,
suggestion_highlight: Option<HighlightStyle>,
) -> DisplayChunks<'_> {
self.block_snapshot.chunks(
display_rows,
language_aware,
Some(&self.text_highlights),
suggestion_highlight,
)
}
pub fn chars_at(
&self,
mut point: DisplayPoint,
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
point = DisplayPoint(self.block_snapshot.clip_point(point.0, Bias::Left));
self.text_chunks(point.row())
.flat_map(str::chars)
.skip_while({
@@ -396,7 +443,7 @@ impl DisplaySnapshot {
&self,
mut point: DisplayPoint,
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
point = DisplayPoint(self.block_snapshot.clip_point(point.0, Bias::Left));
self.reverse_text_chunks(point.row())
.flat_map(|chunk| chunk.chars().rev())
.skip_while({
@@ -510,7 +557,7 @@ impl DisplaySnapshot {
}
pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
let mut clipped = self.blocks_snapshot.clip_point(point.0, bias);
let mut clipped = self.block_snapshot.clip_point(point.0, bias);
if self.clip_at_line_ends {
clipped = self.clip_at_line_end(DisplayPoint(clipped)).0
}
@@ -521,7 +568,7 @@ impl DisplaySnapshot {
let mut point = point.0;
if point.column == self.line_len(point.row) {
point.column = point.column.saturating_sub(1);
point = self.blocks_snapshot.clip_point(point, Bias::Left);
point = self.block_snapshot.clip_point(point, Bias::Left);
}
DisplayPoint(point)
}
@@ -530,37 +577,34 @@ impl DisplaySnapshot {
where
T: ToOffset,
{
self.folds_snapshot.folds_in_range(range)
self.fold_snapshot.folds_in_range(range)
}
pub fn blocks_in_range(
&self,
rows: Range<u32>,
) -> impl Iterator<Item = (u32, &TransformBlock)> {
self.blocks_snapshot.blocks_in_range(rows)
self.block_snapshot.blocks_in_range(rows)
}
pub fn intersects_fold<T: ToOffset>(&self, offset: T) -> bool {
self.folds_snapshot.intersects_fold(offset)
self.fold_snapshot.intersects_fold(offset)
}
pub fn is_line_folded(&self, display_row: u32) -> bool {
let block_point = BlockPoint(Point::new(display_row, 0));
let wrap_point = self.blocks_snapshot.to_wrap_point(block_point);
let tab_point = self.wraps_snapshot.to_tab_point(wrap_point);
self.folds_snapshot.is_line_folded(tab_point.row())
pub fn is_line_folded(&self, buffer_row: u32) -> bool {
self.fold_snapshot.is_line_folded(buffer_row)
}
pub fn is_block_line(&self, display_row: u32) -> bool {
self.blocks_snapshot.is_block_line(display_row)
self.block_snapshot.is_block_line(display_row)
}
pub fn soft_wrap_indent(&self, display_row: u32) -> Option<u32> {
let wrap_row = self
.blocks_snapshot
.block_snapshot
.to_wrap_point(BlockPoint::new(display_row, 0))
.row();
self.wraps_snapshot.soft_wrap_indent(wrap_row)
self.wrap_snapshot.soft_wrap_indent(wrap_row)
}
pub fn text(&self) -> String {
@@ -594,57 +638,84 @@ impl DisplaySnapshot {
(indent, is_blank)
}
pub fn line_indent_for_buffer_row(&self, buffer_row: u32) -> (u32, bool) {
let (buffer, range) = self
.buffer_snapshot
.buffer_line_for_row(buffer_row)
.unwrap();
let mut indent_size = 0;
let mut is_blank = false;
for c in buffer.chars_at(Point::new(range.start.row, 0)) {
if c == ' ' || c == '\t' {
indent_size += 1;
} else {
if c == '\n' {
is_blank = true;
}
break;
}
}
(indent_size, is_blank)
}
pub fn line_len(&self, row: u32) -> u32 {
self.blocks_snapshot.line_len(row)
self.block_snapshot.line_len(row)
}
pub fn longest_row(&self) -> u32 {
self.blocks_snapshot.longest_row()
self.block_snapshot.longest_row()
}
pub fn fold_for_line(self: &Self, display_row: u32) -> Option<FoldStatus> {
if self.is_foldable(display_row) {
Some(FoldStatus::Foldable)
} else if self.is_line_folded(display_row) {
pub fn fold_for_line(self: &Self, buffer_row: u32) -> Option<FoldStatus> {
if self.is_line_folded(buffer_row) {
Some(FoldStatus::Folded)
} else if self.is_foldable(buffer_row) {
Some(FoldStatus::Foldable)
} else {
None
}
}
pub fn is_foldable(self: &Self, row: u32) -> bool {
let max_point = self.max_point();
if row >= max_point.row() {
pub fn is_foldable(self: &Self, buffer_row: u32) -> bool {
let max_row = self.buffer_snapshot.max_buffer_row();
if buffer_row >= max_row {
return false;
}
let (start_indent, is_blank) = self.line_indent(row);
let (indent_size, is_blank) = self.line_indent_for_buffer_row(buffer_row);
if is_blank {
return false;
}
for display_row in next_rows(row, self) {
let (indent, is_blank) = self.line_indent(display_row);
if !is_blank {
return indent > start_indent;
for next_row in (buffer_row + 1)..=max_row {
let (next_indent_size, next_line_is_blank) = self.line_indent_for_buffer_row(next_row);
if next_indent_size > indent_size {
return true;
} else if !next_line_is_blank {
break;
}
}
return false;
false
}
pub fn foldable_range(self: &Self, row: u32) -> Option<Range<DisplayPoint>> {
let start = DisplayPoint::new(row, self.line_len(row));
if self.is_foldable(row) && !self.is_line_folded(start.row()) {
let (start_indent, _) = self.line_indent(row);
let max_point = self.max_point();
pub fn foldable_range(self: &Self, buffer_row: u32) -> Option<Range<Point>> {
let start = Point::new(buffer_row, self.buffer_snapshot.line_len(buffer_row));
if self.is_foldable(start.row) && !self.is_line_folded(start.row) {
let (start_indent, _) = self.line_indent_for_buffer_row(buffer_row);
let max_point = self.buffer_snapshot.max_point();
let mut end = None;
for row in next_rows(row, self) {
let (indent, is_blank) = self.line_indent(row);
for row in (buffer_row + 1)..=max_point.row {
let (indent, is_blank) = self.line_indent_for_buffer_row(row);
if !is_blank && indent <= start_indent {
end = Some(DisplayPoint::new(row - 1, self.line_len(row - 1)));
let prev_row = row - 1;
end = Some(Point::new(
prev_row,
self.buffer_snapshot.line_len(prev_row),
));
break;
}
}
@@ -711,10 +782,11 @@ impl DisplayPoint {
}
pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize {
let unblocked_point = map.blocks_snapshot.to_wrap_point(self.0);
let unwrapped_point = map.wraps_snapshot.to_tab_point(unblocked_point);
let unexpanded_point = map.tabs_snapshot.to_fold_point(unwrapped_point, bias).0;
unexpanded_point.to_buffer_offset(&map.folds_snapshot)
let wrap_point = map.block_snapshot.to_wrap_point(self.0);
let tab_point = map.wrap_snapshot.to_tab_point(wrap_point);
let suggestion_point = map.tab_snapshot.to_suggestion_point(tab_point, bias).0;
let fold_point = map.suggestion_snapshot.to_fold_point(suggestion_point);
fold_point.to_buffer_offset(&map.fold_snapshot)
}
}
@@ -785,7 +857,9 @@ pub mod tests {
let mut tab_size = rng.gen_range(1..=4);
let buffer_start_excerpt_header_height = rng.gen_range(1..=5);
let excerpt_header_height = rng.gen_range(1..=5);
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let family_id = font_cache
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
@@ -835,10 +909,10 @@ pub mod tests {
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
log::info!("fold text: {:?}", snapshot.folds_snapshot.text());
log::info!("tab text: {:?}", snapshot.tabs_snapshot.text());
log::info!("wrap text: {:?}", snapshot.wraps_snapshot.text());
log::info!("block text: {:?}", snapshot.blocks_snapshot.text());
log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
log::info!("block text: {:?}", snapshot.block_snapshot.text());
log::info!("display text: {:?}", snapshot.text());
for _i in 0..operations {
@@ -943,10 +1017,10 @@ pub mod tests {
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
fold_count = snapshot.fold_count();
log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
log::info!("fold text: {:?}", snapshot.folds_snapshot.text());
log::info!("tab text: {:?}", snapshot.tabs_snapshot.text());
log::info!("wrap text: {:?}", snapshot.wraps_snapshot.text());
log::info!("block text: {:?}", snapshot.blocks_snapshot.text());
log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
log::info!("block text: {:?}", snapshot.block_snapshot.text());
log::info!("display text: {:?}", snapshot.text());
// Line boundaries
@@ -1042,7 +1116,9 @@ pub mod tests {
let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let family_id = font_cache
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
@@ -1131,7 +1207,10 @@ pub mod tests {
cx.set_global(Settings::test(cx));
let text = sample_text(6, 6, 'a');
let buffer = MultiBuffer::build_simple(&text, cx);
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let family_id = cx
.font_cache()
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
@@ -1214,7 +1293,9 @@ pub mod tests {
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let family_id = font_cache
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
@@ -1302,7 +1383,9 @@ pub mod tests {
let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Courier"]).unwrap();
let family_id = font_cache
.load_family(&["Courier"], &Default::default())
.unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
@@ -1374,7 +1457,9 @@ pub mod tests {
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Courier"]).unwrap();
let family_id = font_cache
.load_family(&["Courier"], &Default::default())
.unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
@@ -1490,7 +1575,9 @@ pub mod tests {
let text = "\t\tα\nβ\t\n🏀β\t\tγ";
let buffer = MultiBuffer::build_simple(text, cx);
let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let family_id = font_cache
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
@@ -1548,7 +1635,9 @@ pub mod tests {
cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let family_id = font_cache
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
@@ -1607,7 +1696,7 @@ pub mod tests {
) -> Vec<(String, Option<Color>, Option<Color>)> {
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
let mut chunks: Vec<(String, Option<Color>, Option<Color>)> = Vec::new();
for chunk in snapshot.chunks(rows, true) {
for chunk in snapshot.chunks(rows, true, None) {
let syntax_color = chunk
.syntax_highlight_id
.and_then(|id| id.style(theme)?.color);

View File

@@ -4,7 +4,7 @@ use super::{
};
use crate::{Anchor, ExcerptId, ExcerptRange, ToPoint as _};
use collections::{Bound, HashMap, HashSet};
use gpui::{ElementBox, RenderContext};
use gpui::{fonts::HighlightStyle, ElementBox, RenderContext};
use language::{BufferSnapshot, Chunk, Patch, Point};
use parking_lot::Mutex;
use std::{
@@ -572,7 +572,7 @@ impl<'a> BlockMapWriter<'a> {
impl BlockSnapshot {
#[cfg(test)]
pub fn text(&self) -> String {
self.chunks(0..self.transforms.summary().output_rows, false, None)
self.chunks(0..self.transforms.summary().output_rows, false, None, None)
.map(|chunk| chunk.text)
.collect()
}
@@ -582,6 +582,7 @@ impl BlockSnapshot {
rows: Range<u32>,
language_aware: bool,
text_highlights: Option<&'a TextHighlights>,
suggestion_highlight: Option<HighlightStyle>,
) -> BlockChunks<'a> {
let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
@@ -614,6 +615,7 @@ impl BlockSnapshot {
input_start..input_end,
language_aware,
text_highlights,
suggestion_highlight,
),
input_chunk: Default::default(),
transforms: cursor,
@@ -989,6 +991,7 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
#[cfg(test)]
mod tests {
use super::*;
use crate::display_map::suggestion_map::SuggestionMap;
use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
use crate::multi_buffer::MultiBuffer;
use gpui::{elements::Empty, Element};
@@ -1015,7 +1018,10 @@ mod tests {
fn test_basic_blocks(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let family_id = cx
.font_cache()
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
@@ -1026,9 +1032,10 @@ mod tests {
let buffer = MultiBuffer::build_simple(text, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let (fold_map, folds_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot, 1.try_into().unwrap());
let (wrap_map, wraps_snapshot) = WrapMap::new(tabs_snapshot, font_id, 14.0, None, cx);
let (fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
let (tab_map, tab_snapshot) = TabMap::new(suggestion_snapshot, 1.try_into().unwrap());
let (wrap_map, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, None, cx);
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
@@ -1170,12 +1177,14 @@ mod tests {
buffer.snapshot(cx)
});
let (folds_snapshot, fold_edits) =
let (fold_snapshot, fold_edits) =
fold_map.read(buffer_snapshot, subscription.consume().into_inner());
let (tabs_snapshot, tab_edits) =
tab_map.sync(folds_snapshot, fold_edits, 4.try_into().unwrap());
let (suggestion_snapshot, suggestion_edits) =
suggestion_map.sync(fold_snapshot, fold_edits);
let (tab_snapshot, tab_edits) =
tab_map.sync(suggestion_snapshot, suggestion_edits, 4.try_into().unwrap());
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tabs_snapshot, tab_edits, cx)
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
let snapshot = block_map.read(wraps_snapshot, wrap_edits);
assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n");
@@ -1185,7 +1194,10 @@ mod tests {
fn test_blocks_on_wrapped_lines(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let family_id = cx
.font_cache()
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
@@ -1195,9 +1207,10 @@ mod tests {
let buffer = MultiBuffer::build_simple(text, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, folds_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (_, tabs_snapshot) = TabMap::new(folds_snapshot, 1.try_into().unwrap());
let (_, wraps_snapshot) = WrapMap::new(tabs_snapshot, font_id, 14.0, Some(60.), cx);
let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 1.try_into().unwrap());
let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, Some(60.), cx);
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
@@ -1241,7 +1254,10 @@ mod tests {
Some(rng.gen_range(0.0..=100.0))
};
let tab_size = 1.try_into().unwrap();
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let family_id = cx
.font_cache()
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
@@ -1263,10 +1279,11 @@ mod tests {
};
let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
let (fold_map, folds_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot, tab_size);
let (fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
let (tab_map, tab_snapshot) = TabMap::new(suggestion_snapshot, tab_size);
let (wrap_map, wraps_snapshot) =
WrapMap::new(tabs_snapshot, font_id, font_size, wrap_width, cx);
WrapMap::new(tab_snapshot, font_id, font_size, wrap_width, cx);
let mut block_map = BlockMap::new(
wraps_snapshot,
buffer_start_header_height,
@@ -1317,12 +1334,14 @@ mod tests {
})
.collect::<Vec<_>>();
let (folds_snapshot, fold_edits) =
let (fold_snapshot, fold_edits) =
fold_map.read(buffer_snapshot.clone(), vec![]);
let (tabs_snapshot, tab_edits) =
tab_map.sync(folds_snapshot, fold_edits, tab_size);
let (suggestion_snapshot, suggestion_edits) =
suggestion_map.sync(fold_snapshot, fold_edits);
let (tab_snapshot, tab_edits) =
tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tabs_snapshot, tab_edits, cx)
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
let block_ids = block_map.insert(block_properties.clone());
@@ -1340,12 +1359,14 @@ mod tests {
})
.collect();
let (folds_snapshot, fold_edits) =
let (fold_snapshot, fold_edits) =
fold_map.read(buffer_snapshot.clone(), vec![]);
let (tabs_snapshot, tab_edits) =
tab_map.sync(folds_snapshot, fold_edits, tab_size);
let (suggestion_snapshot, suggestion_edits) =
suggestion_map.sync(fold_snapshot, fold_edits);
let (tab_snapshot, tab_edits) =
tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tabs_snapshot, tab_edits, cx)
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
block_map.remove(block_ids_to_remove);
@@ -1362,10 +1383,13 @@ mod tests {
}
}
let (folds_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits, tab_size);
let (fold_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
let (suggestion_snapshot, suggestion_edits) =
suggestion_map.sync(fold_snapshot, fold_edits);
let (tab_snapshot, tab_edits) =
tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tabs_snapshot, tab_edits, cx)
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
assert_eq!(
@@ -1476,6 +1500,7 @@ mod tests {
start_row as u32..blocks_snapshot.max_point().row + 1,
false,
None,
None,
)
.map(|chunk| chunk.text)
.collect::<String>();

View File

@@ -29,10 +29,6 @@ impl FoldPoint {
self.0.row
}
pub fn column(self) -> u32 {
self.0.column
}
pub fn row_mut(&mut self) -> &mut u32 {
&mut self.0.row
}
@@ -639,14 +635,14 @@ impl FoldSnapshot {
cursor.item().map_or(false, |t| t.output_text.is_some())
}
pub fn is_line_folded(&self, output_row: u32) -> bool {
let mut cursor = self.transforms.cursor::<FoldPoint>();
cursor.seek(&FoldPoint::new(output_row, 0), Bias::Right, &());
pub fn is_line_folded(&self, buffer_row: u32) -> bool {
let mut cursor = self.transforms.cursor::<Point>();
cursor.seek(&Point::new(buffer_row, 0), Bias::Right, &());
while let Some(transform) = cursor.item() {
if transform.output_text.is_some() {
return true;
}
if cursor.end(&()).row() == output_row {
if cursor.end(&()).row == buffer_row {
cursor.next(&())
} else {
break;
@@ -655,12 +651,6 @@ impl FoldSnapshot {
false
}
pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator<Item = char> {
let start = start.to_offset(self);
self.chunks(start..self.len(), false, None)
.flat_map(|chunk| chunk.text.chars())
}
pub fn chunks<'a>(
&'a self,
range: Range<FoldOffset>,
@@ -1214,6 +1204,7 @@ pub type FoldEdit = Edit<FoldOffset>;
mod tests {
use super::*;
use crate::{MultiBuffer, ToPoint};
use collections::HashSet;
use rand::prelude::*;
use settings::Settings;
use std::{cmp::Reverse, env, mem, sync::Arc};
@@ -1593,10 +1584,13 @@ mod tests {
fold_row += 1;
}
for fold_range in map.merged_fold_ranges() {
let fold_point =
snapshot.to_fold_point(fold_range.start.to_point(&buffer_snapshot), Right);
assert!(snapshot.is_line_folded(fold_point.row()));
let fold_start_rows = map
.merged_fold_ranges()
.iter()
.map(|range| range.start.to_point(&buffer_snapshot).row)
.collect::<HashSet<_>>();
for row in fold_start_rows {
assert!(snapshot.is_line_folded(row));
}
for _ in 0..5 {

View File

@@ -0,0 +1,855 @@
use super::{
fold_map::{FoldBufferRows, FoldChunks, FoldEdit, FoldOffset, FoldPoint, FoldSnapshot},
TextHighlights,
};
use crate::{MultiBufferSnapshot, ToPoint};
use gpui::fonts::HighlightStyle;
use language::{Bias, Chunk, Edit, Patch, Point, Rope, TextSummary};
use parking_lot::Mutex;
use std::{
cmp,
ops::{Add, AddAssign, Range, Sub},
};
use util::post_inc;
pub type SuggestionEdit = Edit<SuggestionOffset>;
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct SuggestionOffset(pub usize);
impl Add for SuggestionOffset {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl Sub for SuggestionOffset {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self(self.0 - rhs.0)
}
}
impl AddAssign for SuggestionOffset {
fn add_assign(&mut self, rhs: Self) {
self.0 += rhs.0;
}
}
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct SuggestionPoint(pub Point);
impl SuggestionPoint {
pub fn new(row: u32, column: u32) -> Self {
Self(Point::new(row, column))
}
pub fn row(self) -> u32 {
self.0.row
}
pub fn column(self) -> u32 {
self.0.column
}
}
#[derive(Clone, Debug)]
pub struct Suggestion<T> {
pub position: T,
pub text: Rope,
}
pub struct SuggestionMap(Mutex<SuggestionSnapshot>);
impl SuggestionMap {
pub fn new(fold_snapshot: FoldSnapshot) -> (Self, SuggestionSnapshot) {
let snapshot = SuggestionSnapshot {
fold_snapshot,
suggestion: None,
version: 0,
};
(Self(Mutex::new(snapshot.clone())), snapshot)
}
pub fn replace<T>(
&self,
new_suggestion: Option<Suggestion<T>>,
fold_snapshot: FoldSnapshot,
fold_edits: Vec<FoldEdit>,
) -> (SuggestionSnapshot, Vec<SuggestionEdit>)
where
T: ToPoint,
{
let new_suggestion = new_suggestion.map(|new_suggestion| {
let buffer_point = new_suggestion
.position
.to_point(fold_snapshot.buffer_snapshot());
let fold_point = fold_snapshot.to_fold_point(buffer_point, Bias::Left);
let fold_offset = fold_point.to_offset(&fold_snapshot);
Suggestion {
position: fold_offset,
text: new_suggestion.text,
}
});
let (_, edits) = self.sync(fold_snapshot, fold_edits);
let mut snapshot = self.0.lock();
let mut patch = Patch::new(edits);
if let Some(suggestion) = snapshot.suggestion.take() {
patch = patch.compose([SuggestionEdit {
old: SuggestionOffset(suggestion.position.0)
..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
new: SuggestionOffset(suggestion.position.0)
..SuggestionOffset(suggestion.position.0),
}]);
}
if let Some(suggestion) = new_suggestion.as_ref() {
patch = patch.compose([SuggestionEdit {
old: SuggestionOffset(suggestion.position.0)
..SuggestionOffset(suggestion.position.0),
new: SuggestionOffset(suggestion.position.0)
..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
}]);
}
snapshot.suggestion = new_suggestion;
snapshot.version += 1;
(snapshot.clone(), patch.into_inner())
}
pub fn sync(
&self,
fold_snapshot: FoldSnapshot,
fold_edits: Vec<FoldEdit>,
) -> (SuggestionSnapshot, Vec<SuggestionEdit>) {
let mut snapshot = self.0.lock();
if snapshot.fold_snapshot.version != fold_snapshot.version {
snapshot.version += 1;
}
let mut suggestion_edits = Vec::new();
let mut suggestion_old_len = 0;
let mut suggestion_new_len = 0;
for fold_edit in fold_edits {
let start = fold_edit.new.start;
let end = FoldOffset(start.0 + fold_edit.old_len().0);
if let Some(suggestion) = snapshot.suggestion.as_mut() {
if end <= suggestion.position {
suggestion.position.0 += fold_edit.new_len().0;
suggestion.position.0 -= fold_edit.old_len().0;
} else if start > suggestion.position {
suggestion_old_len = suggestion.text.len();
suggestion_new_len = suggestion_old_len;
} else {
suggestion_old_len = suggestion.text.len();
snapshot.suggestion.take();
suggestion_edits.push(SuggestionEdit {
old: SuggestionOffset(fold_edit.old.start.0)
..SuggestionOffset(fold_edit.old.end.0 + suggestion_old_len),
new: SuggestionOffset(fold_edit.new.start.0)
..SuggestionOffset(fold_edit.new.end.0),
});
continue;
}
}
suggestion_edits.push(SuggestionEdit {
old: SuggestionOffset(fold_edit.old.start.0 + suggestion_old_len)
..SuggestionOffset(fold_edit.old.end.0 + suggestion_old_len),
new: SuggestionOffset(fold_edit.new.start.0 + suggestion_new_len)
..SuggestionOffset(fold_edit.new.end.0 + suggestion_new_len),
});
}
snapshot.fold_snapshot = fold_snapshot;
(snapshot.clone(), suggestion_edits)
}
}
#[derive(Clone)]
pub struct SuggestionSnapshot {
pub fold_snapshot: FoldSnapshot,
pub suggestion: Option<Suggestion<FoldOffset>>,
pub version: usize,
}
impl SuggestionSnapshot {
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
self.fold_snapshot.buffer_snapshot()
}
pub fn max_point(&self) -> SuggestionPoint {
if let Some(suggestion) = self.suggestion.as_ref() {
let suggestion_point = suggestion.position.to_point(&self.fold_snapshot);
let mut max_point = suggestion_point.0;
max_point += suggestion.text.max_point();
max_point += self.fold_snapshot.max_point().0 - suggestion_point.0;
SuggestionPoint(max_point)
} else {
SuggestionPoint(self.fold_snapshot.max_point().0)
}
}
pub fn len(&self) -> SuggestionOffset {
if let Some(suggestion) = self.suggestion.as_ref() {
let mut len = suggestion.position.0;
len += suggestion.text.len();
len += self.fold_snapshot.len().0 - suggestion.position.0;
SuggestionOffset(len)
} else {
SuggestionOffset(self.fold_snapshot.len().0)
}
}
pub fn line_len(&self, row: u32) -> u32 {
if let Some(suggestion) = &self.suggestion {
let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
let suggestion_end = suggestion_start + suggestion.text.max_point();
if row < suggestion_start.row {
self.fold_snapshot.line_len(row)
} else if row > suggestion_end.row {
self.fold_snapshot
.line_len(suggestion_start.row + (row - suggestion_end.row))
} else {
let mut result = suggestion.text.line_len(row - suggestion_start.row);
if row == suggestion_start.row {
result += suggestion_start.column;
}
if row == suggestion_end.row {
result +=
self.fold_snapshot.line_len(suggestion_start.row) - suggestion_start.column;
}
result
}
} else {
self.fold_snapshot.line_len(row)
}
}
pub fn clip_point(&self, point: SuggestionPoint, bias: Bias) -> SuggestionPoint {
if let Some(suggestion) = self.suggestion.as_ref() {
let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
let suggestion_end = suggestion_start + suggestion.text.max_point();
if point.0 <= suggestion_start {
SuggestionPoint(self.fold_snapshot.clip_point(FoldPoint(point.0), bias).0)
} else if point.0 > suggestion_end {
let fold_point = self.fold_snapshot.clip_point(
FoldPoint(suggestion_start + (point.0 - suggestion_end)),
bias,
);
let suggestion_point = suggestion_end + (fold_point.0 - suggestion_start);
if bias == Bias::Left && suggestion_point == suggestion_end {
SuggestionPoint(suggestion_start)
} else {
SuggestionPoint(suggestion_point)
}
} else if bias == Bias::Left || suggestion_start == self.fold_snapshot.max_point().0 {
SuggestionPoint(suggestion_start)
} else {
let fold_point = if self.fold_snapshot.line_len(suggestion_start.row)
> suggestion_start.column
{
FoldPoint(suggestion_start + Point::new(0, 1))
} else {
FoldPoint(suggestion_start + Point::new(1, 0))
};
let clipped_fold_point = self.fold_snapshot.clip_point(fold_point, bias);
SuggestionPoint(suggestion_end + (clipped_fold_point.0 - suggestion_start))
}
} else {
SuggestionPoint(self.fold_snapshot.clip_point(FoldPoint(point.0), bias).0)
}
}
pub fn to_offset(&self, point: SuggestionPoint) -> SuggestionOffset {
if let Some(suggestion) = self.suggestion.as_ref() {
let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
let suggestion_end = suggestion_start + suggestion.text.max_point();
if point.0 <= suggestion_start {
SuggestionOffset(FoldPoint(point.0).to_offset(&self.fold_snapshot).0)
} else if point.0 > suggestion_end {
let fold_offset = FoldPoint(suggestion_start + (point.0 - suggestion_end))
.to_offset(&self.fold_snapshot);
SuggestionOffset(fold_offset.0 + suggestion.text.len())
} else {
let offset_in_suggestion =
suggestion.text.point_to_offset(point.0 - suggestion_start);
SuggestionOffset(suggestion.position.0 + offset_in_suggestion)
}
} else {
SuggestionOffset(FoldPoint(point.0).to_offset(&self.fold_snapshot).0)
}
}
pub fn to_point(&self, offset: SuggestionOffset) -> SuggestionPoint {
if let Some(suggestion) = self.suggestion.as_ref() {
let suggestion_point_start = suggestion.position.to_point(&self.fold_snapshot).0;
if offset.0 <= suggestion.position.0 {
SuggestionPoint(FoldOffset(offset.0).to_point(&self.fold_snapshot).0)
} else if offset.0 > (suggestion.position.0 + suggestion.text.len()) {
let fold_point = FoldOffset(offset.0 - suggestion.text.len())
.to_point(&self.fold_snapshot)
.0;
SuggestionPoint(
suggestion_point_start
+ suggestion.text.max_point()
+ (fold_point - suggestion_point_start),
)
} else {
let point_in_suggestion = suggestion
.text
.offset_to_point(offset.0 - suggestion.position.0);
SuggestionPoint(suggestion_point_start + point_in_suggestion)
}
} else {
SuggestionPoint(FoldOffset(offset.0).to_point(&self.fold_snapshot).0)
}
}
pub fn to_fold_point(&self, point: SuggestionPoint) -> FoldPoint {
if let Some(suggestion) = self.suggestion.as_ref() {
let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
let suggestion_end = suggestion_start + suggestion.text.max_point();
if point.0 <= suggestion_start {
FoldPoint(point.0)
} else if point.0 > suggestion_end {
FoldPoint(suggestion_start + (point.0 - suggestion_end))
} else {
FoldPoint(suggestion_start)
}
} else {
FoldPoint(point.0)
}
}
pub fn to_suggestion_point(&self, point: FoldPoint) -> SuggestionPoint {
if let Some(suggestion) = self.suggestion.as_ref() {
let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
if point.0 <= suggestion_start {
SuggestionPoint(point.0)
} else {
let suggestion_end = suggestion_start + suggestion.text.max_point();
SuggestionPoint(suggestion_end + (point.0 - suggestion_start))
}
} else {
SuggestionPoint(point.0)
}
}
pub fn text_summary_for_range(&self, range: Range<SuggestionPoint>) -> TextSummary {
if let Some(suggestion) = self.suggestion.as_ref() {
let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
let suggestion_end = suggestion_start + suggestion.text.max_point();
let mut summary = TextSummary::default();
let prefix_range =
cmp::min(range.start.0, suggestion_start)..cmp::min(range.end.0, suggestion_start);
if prefix_range.start < prefix_range.end {
summary += self.fold_snapshot.text_summary_for_range(
FoldPoint(prefix_range.start)..FoldPoint(prefix_range.end),
);
}
let suggestion_range =
cmp::max(range.start.0, suggestion_start)..cmp::min(range.end.0, suggestion_end);
if suggestion_range.start < suggestion_range.end {
let point_range = suggestion_range.start - suggestion_start
..suggestion_range.end - suggestion_start;
let offset_range = suggestion.text.point_to_offset(point_range.start)
..suggestion.text.point_to_offset(point_range.end);
summary += suggestion
.text
.cursor(offset_range.start)
.summary::<TextSummary>(offset_range.end);
}
let suffix_range = cmp::max(range.start.0, suggestion_end)..range.end.0;
if suffix_range.start < suffix_range.end {
let start = suggestion_start + (suffix_range.start - suggestion_end);
let end = suggestion_start + (suffix_range.end - suggestion_end);
summary += self
.fold_snapshot
.text_summary_for_range(FoldPoint(start)..FoldPoint(end));
}
summary
} else {
self.fold_snapshot
.text_summary_for_range(FoldPoint(range.start.0)..FoldPoint(range.end.0))
}
}
pub fn chars_at(&self, start: SuggestionPoint) -> impl '_ + Iterator<Item = char> {
let start = self.to_offset(start);
self.chunks(start..self.len(), false, None, None)
.flat_map(|chunk| chunk.text.chars())
}
pub fn chunks<'a>(
&'a self,
range: Range<SuggestionOffset>,
language_aware: bool,
text_highlights: Option<&'a TextHighlights>,
suggestion_highlight: Option<HighlightStyle>,
) -> SuggestionChunks<'a> {
if let Some(suggestion) = self.suggestion.as_ref() {
let suggestion_range =
suggestion.position.0..suggestion.position.0 + suggestion.text.len();
let prefix_chunks = if range.start.0 < suggestion_range.start {
Some(self.fold_snapshot.chunks(
FoldOffset(range.start.0)
..cmp::min(FoldOffset(suggestion_range.start), FoldOffset(range.end.0)),
language_aware,
text_highlights,
))
} else {
None
};
let clipped_suggestion_range = cmp::max(range.start.0, suggestion_range.start)
..cmp::min(range.end.0, suggestion_range.end);
let suggestion_chunks = if clipped_suggestion_range.start < clipped_suggestion_range.end
{
let start = clipped_suggestion_range.start - suggestion_range.start;
let end = clipped_suggestion_range.end - suggestion_range.start;
Some(suggestion.text.chunks_in_range(start..end))
} else {
None
};
let suffix_chunks = if range.end.0 > suggestion_range.end {
let start = cmp::max(suggestion_range.end, range.start.0) - suggestion_range.len();
let end = range.end.0 - suggestion_range.len();
Some(self.fold_snapshot.chunks(
FoldOffset(start)..FoldOffset(end),
language_aware,
text_highlights,
))
} else {
None
};
SuggestionChunks {
prefix_chunks,
suggestion_chunks,
suffix_chunks,
highlight_style: suggestion_highlight,
}
} else {
SuggestionChunks {
prefix_chunks: Some(self.fold_snapshot.chunks(
FoldOffset(range.start.0)..FoldOffset(range.end.0),
language_aware,
text_highlights,
)),
suggestion_chunks: None,
suffix_chunks: None,
highlight_style: None,
}
}
}
pub fn buffer_rows<'a>(&'a self, row: u32) -> SuggestionBufferRows<'a> {
let suggestion_range = if let Some(suggestion) = self.suggestion.as_ref() {
let start = suggestion.position.to_point(&self.fold_snapshot).0;
let end = start + suggestion.text.max_point();
start.row..end.row
} else {
u32::MAX..u32::MAX
};
let fold_buffer_rows = if row <= suggestion_range.start {
self.fold_snapshot.buffer_rows(row)
} else if row > suggestion_range.end {
self.fold_snapshot
.buffer_rows(row - (suggestion_range.end - suggestion_range.start))
} else {
let mut rows = self.fold_snapshot.buffer_rows(suggestion_range.start);
rows.next();
rows
};
SuggestionBufferRows {
current_row: row,
suggestion_row_start: suggestion_range.start,
suggestion_row_end: suggestion_range.end,
fold_buffer_rows,
}
}
#[cfg(test)]
pub fn text(&self) -> String {
self.chunks(Default::default()..self.len(), false, None, None)
.map(|chunk| chunk.text)
.collect()
}
}
pub struct SuggestionChunks<'a> {
prefix_chunks: Option<FoldChunks<'a>>,
suggestion_chunks: Option<text::Chunks<'a>>,
suffix_chunks: Option<FoldChunks<'a>>,
highlight_style: Option<HighlightStyle>,
}
impl<'a> Iterator for SuggestionChunks<'a> {
type Item = Chunk<'a>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(chunks) = self.prefix_chunks.as_mut() {
if let Some(chunk) = chunks.next() {
return Some(chunk);
} else {
self.prefix_chunks = None;
}
}
if let Some(chunks) = self.suggestion_chunks.as_mut() {
if let Some(chunk) = chunks.next() {
return Some(Chunk {
text: chunk,
syntax_highlight_id: None,
highlight_style: self.highlight_style,
diagnostic_severity: None,
is_unnecessary: false,
});
} else {
self.suggestion_chunks = None;
}
}
if let Some(chunks) = self.suffix_chunks.as_mut() {
if let Some(chunk) = chunks.next() {
return Some(chunk);
} else {
self.suffix_chunks = None;
}
}
None
}
}
#[derive(Clone)]
pub struct SuggestionBufferRows<'a> {
current_row: u32,
suggestion_row_start: u32,
suggestion_row_end: u32,
fold_buffer_rows: FoldBufferRows<'a>,
}
impl<'a> Iterator for SuggestionBufferRows<'a> {
type Item = Option<u32>;
fn next(&mut self) -> Option<Self::Item> {
let row = post_inc(&mut self.current_row);
if row <= self.suggestion_row_start || row > self.suggestion_row_end {
self.fold_buffer_rows.next()
} else {
Some(None)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{display_map::fold_map::FoldMap, MultiBuffer};
use gpui::MutableAppContext;
use rand::{prelude::StdRng, Rng};
use settings::Settings;
use std::{
env,
ops::{Bound, RangeBounds},
};
#[gpui::test]
fn test_basic(cx: &mut MutableAppContext) {
let buffer = MultiBuffer::build_simple("abcdefghi", cx);
let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
let (mut fold_map, fold_snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
assert_eq!(suggestion_snapshot.text(), "abcdefghi");
let (suggestion_snapshot, _) = suggestion_map.replace(
Some(Suggestion {
position: 3,
text: "123\n456".into(),
}),
fold_snapshot,
Default::default(),
);
assert_eq!(suggestion_snapshot.text(), "abc123\n456defghi");
buffer.update(cx, |buffer, cx| {
buffer.edit(
[(0..0, "ABC"), (3..3, "DEF"), (4..4, "GHI"), (9..9, "JKL")],
None,
cx,
)
});
let (fold_snapshot, fold_edits) = fold_map.read(
buffer.read(cx).snapshot(cx),
buffer_edits.consume().into_inner(),
);
let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot.clone(), fold_edits);
assert_eq!(suggestion_snapshot.text(), "ABCabcDEF123\n456dGHIefghiJKL");
let (mut fold_map_writer, _, _) =
fold_map.write(buffer.read(cx).snapshot(cx), Default::default());
let (fold_snapshot, fold_edits) = fold_map_writer.fold([0..3]);
let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot, fold_edits);
assert_eq!(suggestion_snapshot.text(), "⋯abcDEF123\n456dGHIefghiJKL");
let (mut fold_map_writer, _, _) =
fold_map.write(buffer.read(cx).snapshot(cx), Default::default());
let (fold_snapshot, fold_edits) = fold_map_writer.fold([6..10]);
let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot, fold_edits);
assert_eq!(suggestion_snapshot.text(), "⋯abc⋯GHIefghiJKL");
}
#[gpui::test(iterations = 100)]
fn test_random_suggestions(cx: &mut MutableAppContext, mut rng: StdRng) {
cx.set_global(Settings::test(cx));
let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
let len = rng.gen_range(0..30);
let buffer = if rng.gen() {
let text = util::RandomCharIter::new(&mut rng)
.take(len)
.collect::<String>();
MultiBuffer::build_simple(&text, cx)
} else {
MultiBuffer::build_random(&mut rng, cx)
};
let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
log::info!("buffer text: {:?}", buffer_snapshot.text());
let (mut fold_map, mut fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (suggestion_map, mut suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
for _ in 0..operations {
let mut suggestion_edits = Patch::default();
let mut prev_suggestion_text = suggestion_snapshot.text();
let mut buffer_edits = Vec::new();
match rng.gen_range(0..=100) {
0..=29 => {
let (_, edits) = suggestion_map.randomly_mutate(&mut rng);
suggestion_edits = suggestion_edits.compose(edits);
}
30..=59 => {
for (new_fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
fold_snapshot = new_fold_snapshot;
let (_, edits) = suggestion_map.sync(fold_snapshot.clone(), fold_edits);
suggestion_edits = suggestion_edits.compose(edits);
}
}
_ => buffer.update(cx, |buffer, cx| {
let subscription = buffer.subscribe();
let edit_count = rng.gen_range(1..=5);
buffer.randomly_mutate(&mut rng, edit_count, cx);
buffer_snapshot = buffer.snapshot(cx);
let edits = subscription.consume().into_inner();
log::info!("editing {:?}", edits);
buffer_edits.extend(edits);
}),
};
let (new_fold_snapshot, fold_edits) =
fold_map.read(buffer_snapshot.clone(), buffer_edits);
fold_snapshot = new_fold_snapshot;
let (new_suggestion_snapshot, edits) =
suggestion_map.sync(fold_snapshot.clone(), fold_edits);
suggestion_snapshot = new_suggestion_snapshot;
suggestion_edits = suggestion_edits.compose(edits);
log::info!("buffer text: {:?}", buffer_snapshot.text());
log::info!("folds text: {:?}", fold_snapshot.text());
log::info!("suggestions text: {:?}", suggestion_snapshot.text());
let mut expected_text = Rope::from(fold_snapshot.text().as_str());
let mut expected_buffer_rows = fold_snapshot.buffer_rows(0).collect::<Vec<_>>();
if let Some(suggestion) = suggestion_snapshot.suggestion.as_ref() {
expected_text.replace(
suggestion.position.0..suggestion.position.0,
&suggestion.text.to_string(),
);
let suggestion_start = suggestion.position.to_point(&fold_snapshot).0;
let suggestion_end = suggestion_start + suggestion.text.max_point();
expected_buffer_rows.splice(
(suggestion_start.row + 1) as usize..(suggestion_start.row + 1) as usize,
(0..suggestion_end.row - suggestion_start.row).map(|_| None),
);
}
assert_eq!(suggestion_snapshot.text(), expected_text.to_string());
for row_start in 0..expected_buffer_rows.len() {
assert_eq!(
suggestion_snapshot
.buffer_rows(row_start as u32)
.collect::<Vec<_>>(),
&expected_buffer_rows[row_start..],
"incorrect buffer rows starting at {}",
row_start
);
}
for _ in 0..5 {
let mut end = rng.gen_range(0..=suggestion_snapshot.len().0);
end = expected_text.clip_offset(end, Bias::Right);
let mut start = rng.gen_range(0..=end);
start = expected_text.clip_offset(start, Bias::Right);
let actual_text = suggestion_snapshot
.chunks(
SuggestionOffset(start)..SuggestionOffset(end),
false,
None,
None,
)
.map(|chunk| chunk.text)
.collect::<String>();
assert_eq!(
actual_text,
expected_text.slice(start..end).to_string(),
"incorrect text in range {:?}",
start..end
);
let start_point = SuggestionPoint(expected_text.offset_to_point(start));
let end_point = SuggestionPoint(expected_text.offset_to_point(end));
assert_eq!(
suggestion_snapshot.text_summary_for_range(start_point..end_point),
expected_text.slice(start..end).summary()
);
}
for edit in suggestion_edits.into_inner() {
prev_suggestion_text.replace_range(
edit.new.start.0..edit.new.start.0 + edit.old_len().0,
&suggestion_snapshot.text()[edit.new.start.0..edit.new.end.0],
);
}
assert_eq!(prev_suggestion_text, suggestion_snapshot.text());
assert_eq!(expected_text.max_point(), suggestion_snapshot.max_point().0);
assert_eq!(expected_text.len(), suggestion_snapshot.len().0);
let mut suggestion_point = SuggestionPoint::default();
let mut suggestion_offset = SuggestionOffset::default();
for ch in expected_text.chars() {
assert_eq!(
suggestion_snapshot.to_offset(suggestion_point),
suggestion_offset,
"invalid to_offset({:?})",
suggestion_point
);
assert_eq!(
suggestion_snapshot.to_point(suggestion_offset),
suggestion_point,
"invalid to_point({:?})",
suggestion_offset
);
assert_eq!(
suggestion_snapshot
.to_suggestion_point(suggestion_snapshot.to_fold_point(suggestion_point)),
suggestion_snapshot.clip_point(suggestion_point, Bias::Left),
);
let mut bytes = [0; 4];
for byte in ch.encode_utf8(&mut bytes).as_bytes() {
suggestion_offset.0 += 1;
if *byte == b'\n' {
suggestion_point.0 += Point::new(1, 0);
} else {
suggestion_point.0 += Point::new(0, 1);
}
let clipped_left_point =
suggestion_snapshot.clip_point(suggestion_point, Bias::Left);
let clipped_right_point =
suggestion_snapshot.clip_point(suggestion_point, Bias::Right);
assert!(
clipped_left_point <= clipped_right_point,
"clipped left point {:?} is greater than clipped right point {:?}",
clipped_left_point,
clipped_right_point
);
assert_eq!(
clipped_left_point.0,
expected_text.clip_point(clipped_left_point.0, Bias::Left)
);
assert_eq!(
clipped_right_point.0,
expected_text.clip_point(clipped_right_point.0, Bias::Right)
);
assert!(clipped_left_point <= suggestion_snapshot.max_point());
assert!(clipped_right_point <= suggestion_snapshot.max_point());
if let Some(suggestion) = suggestion_snapshot.suggestion.as_ref() {
let suggestion_start = suggestion.position.to_point(&fold_snapshot).0;
let suggestion_end = suggestion_start + suggestion.text.max_point();
let invalid_range = (
Bound::Excluded(suggestion_start),
Bound::Included(suggestion_end),
);
assert!(
!invalid_range.contains(&clipped_left_point.0),
"clipped left point {:?} is inside invalid suggestion range {:?}",
clipped_left_point,
invalid_range
);
assert!(
!invalid_range.contains(&clipped_right_point.0),
"clipped right point {:?} is inside invalid suggestion range {:?}",
clipped_right_point,
invalid_range
);
}
}
}
}
}
impl SuggestionMap {
pub fn randomly_mutate(
&self,
rng: &mut impl Rng,
) -> (SuggestionSnapshot, Vec<SuggestionEdit>) {
let fold_snapshot = self.0.lock().fold_snapshot.clone();
let new_suggestion = if rng.gen_bool(0.3) {
None
} else {
let index = rng.gen_range(0..=fold_snapshot.buffer_snapshot().len());
let len = rng.gen_range(0..30);
Some(Suggestion {
position: index,
text: util::RandomCharIter::new(rng)
.take(len)
.filter(|ch| *ch != '\r')
.collect::<String>()
.as_str()
.into(),
})
};
log::info!("replacing suggestion with {:?}", new_suggestion);
self.replace(new_suggestion, fold_snapshot, Default::default())
}
}
}

View File

@@ -1,86 +1,144 @@
use super::{
fold_map::{self, FoldEdit, FoldPoint, FoldSnapshot},
suggestion_map::{self, SuggestionChunks, SuggestionEdit, SuggestionPoint, SuggestionSnapshot},
TextHighlights,
};
use crate::MultiBufferSnapshot;
use gpui::fonts::HighlightStyle;
use language::{Chunk, Point};
use parking_lot::Mutex;
use std::{cmp, mem, num::NonZeroU32, ops::Range};
use sum_tree::Bias;
const MAX_EXPANSION_COLUMN: u32 = 256;
pub struct TabMap(Mutex<TabSnapshot>);
impl TabMap {
pub fn new(input: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
pub fn new(input: SuggestionSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
let snapshot = TabSnapshot {
fold_snapshot: input,
suggestion_snapshot: input,
tab_size,
max_expansion_column: MAX_EXPANSION_COLUMN,
version: 0,
};
(Self(Mutex::new(snapshot.clone())), snapshot)
}
#[cfg(test)]
pub fn set_max_expansion_column(&self, column: u32) -> TabSnapshot {
self.0.lock().max_expansion_column = column;
self.0.lock().clone()
}
pub fn sync(
&self,
fold_snapshot: FoldSnapshot,
mut fold_edits: Vec<FoldEdit>,
suggestion_snapshot: SuggestionSnapshot,
mut suggestion_edits: Vec<SuggestionEdit>,
tab_size: NonZeroU32,
) -> (TabSnapshot, Vec<TabEdit>) {
let mut old_snapshot = self.0.lock();
let mut new_snapshot = TabSnapshot {
fold_snapshot,
suggestion_snapshot,
tab_size,
max_expansion_column: old_snapshot.max_expansion_column,
version: old_snapshot.version,
};
if old_snapshot.fold_snapshot.version != new_snapshot.fold_snapshot.version {
if old_snapshot.suggestion_snapshot.version != new_snapshot.suggestion_snapshot.version {
new_snapshot.version += 1;
}
let old_max_offset = old_snapshot.fold_snapshot.len();
let mut tab_edits = Vec::with_capacity(fold_edits.len());
let old_max_offset = old_snapshot.suggestion_snapshot.len();
let mut tab_edits = Vec::with_capacity(suggestion_edits.len());
if old_snapshot.tab_size == new_snapshot.tab_size {
for fold_edit in &mut fold_edits {
let mut delta = 0;
for chunk in old_snapshot.fold_snapshot.chunks(
fold_edit.old.end..old_max_offset,
// Expand each edit to include the next tab on the same line as the edit,
// and any subsequent tabs on that line that moved across the tab expansion
// boundary.
for suggestion_edit in &mut suggestion_edits {
let old_end_column = old_snapshot
.suggestion_snapshot
.to_point(suggestion_edit.old.end)
.column();
let new_end_column = new_snapshot
.suggestion_snapshot
.to_point(suggestion_edit.new.end)
.column();
let mut offset_from_edit = 0;
let mut first_tab_offset = None;
let mut last_tab_with_changed_expansion_offset = None;
'outer: for chunk in old_snapshot.suggestion_snapshot.chunks(
suggestion_edit.old.end..old_max_offset,
false,
None,
None,
) {
let patterns: &[_] = &['\t', '\n'];
if let Some(ix) = chunk.text.find(patterns) {
if &chunk.text[ix..ix + 1] == "\t" {
fold_edit.old.end.0 += delta + ix + 1;
fold_edit.new.end.0 += delta + ix + 1;
}
for (ix, mat) in chunk.text.match_indices(&['\t', '\n']) {
let offset_from_edit = offset_from_edit + (ix as u32);
match mat {
"\t" => {
if first_tab_offset.is_none() {
first_tab_offset = Some(offset_from_edit);
}
break;
let old_column = old_end_column + offset_from_edit;
let new_column = new_end_column + offset_from_edit;
let was_expanded = old_column < old_snapshot.max_expansion_column;
let is_expanded = new_column < new_snapshot.max_expansion_column;
if was_expanded != is_expanded {
last_tab_with_changed_expansion_offset = Some(offset_from_edit);
} else if !was_expanded && !is_expanded {
break 'outer;
}
}
"\n" => break 'outer,
_ => unreachable!(),
}
}
delta += chunk.text.len();
offset_from_edit += chunk.text.len() as u32;
if old_end_column + offset_from_edit >= old_snapshot.max_expansion_column
&& new_end_column | offset_from_edit >= new_snapshot.max_expansion_column
{
break;
}
}
if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) {
suggestion_edit.old.end.0 += offset as usize + 1;
suggestion_edit.new.end.0 += offset as usize + 1;
}
}
// Combine any edits that overlap due to the expansion.
let mut ix = 1;
while ix < fold_edits.len() {
let (prev_edits, next_edits) = fold_edits.split_at_mut(ix);
while ix < suggestion_edits.len() {
let (prev_edits, next_edits) = suggestion_edits.split_at_mut(ix);
let prev_edit = prev_edits.last_mut().unwrap();
let edit = &next_edits[0];
if prev_edit.old.end >= edit.old.start {
prev_edit.old.end = edit.old.end;
prev_edit.new.end = edit.new.end;
fold_edits.remove(ix);
suggestion_edits.remove(ix);
} else {
ix += 1;
}
}
for fold_edit in fold_edits {
let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot);
let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
for suggestion_edit in suggestion_edits {
let old_start = old_snapshot
.suggestion_snapshot
.to_point(suggestion_edit.old.start);
let old_end = old_snapshot
.suggestion_snapshot
.to_point(suggestion_edit.old.end);
let new_start = new_snapshot
.suggestion_snapshot
.to_point(suggestion_edit.new.start);
let new_end = new_snapshot
.suggestion_snapshot
.to_point(suggestion_edit.new.end);
tab_edits.push(TabEdit {
old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
@@ -101,27 +159,26 @@ impl TabMap {
#[derive(Clone)]
pub struct TabSnapshot {
pub fold_snapshot: FoldSnapshot,
pub suggestion_snapshot: SuggestionSnapshot,
pub tab_size: NonZeroU32,
pub max_expansion_column: u32,
pub version: usize,
}
impl TabSnapshot {
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
self.fold_snapshot.buffer_snapshot()
self.suggestion_snapshot.buffer_snapshot()
}
pub fn line_len(&self, row: u32) -> u32 {
let max_point = self.max_point();
if row < max_point.row() {
self.chunks(
TabPoint::new(row, 0)..TabPoint::new(row + 1, 0),
false,
None,
)
.map(|chunk| chunk.text.len() as u32)
.sum::<u32>()
- 1
self.to_tab_point(SuggestionPoint::new(
row,
self.suggestion_snapshot.line_len(row),
))
.0
.column
} else {
max_point.column()
}
@@ -132,10 +189,10 @@ impl TabSnapshot {
}
pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
let input_start = self.to_fold_point(range.start, Bias::Left).0;
let input_end = self.to_fold_point(range.end, Bias::Right).0;
let input_start = self.to_suggestion_point(range.start, Bias::Left).0;
let input_end = self.to_suggestion_point(range.end, Bias::Right).0;
let input_summary = self
.fold_snapshot
.suggestion_snapshot
.text_summary_for_range(input_start..input_end);
let mut first_line_chars = 0;
@@ -145,7 +202,7 @@ impl TabSnapshot {
self.max_point()
};
for c in self
.chunks(range.start..line_end, false, None)
.chunks(range.start..line_end, false, None, None)
.flat_map(|chunk| chunk.text.chars())
{
if c == '\n' {
@@ -159,7 +216,12 @@ impl TabSnapshot {
last_line_chars = first_line_chars;
} else {
for _ in self
.chunks(TabPoint::new(range.end.row(), 0)..range.end, false, None)
.chunks(
TabPoint::new(range.end.row(), 0)..range.end,
false,
None,
None,
)
.flat_map(|chunk| chunk.text.chars())
{
last_line_chars += 1;
@@ -180,120 +242,133 @@ impl TabSnapshot {
range: Range<TabPoint>,
language_aware: bool,
text_highlights: Option<&'a TextHighlights>,
suggestion_highlight: Option<HighlightStyle>,
) -> TabChunks<'a> {
let (input_start, expanded_char_column, to_next_stop) =
self.to_fold_point(range.start, Bias::Left);
let input_start = input_start.to_offset(&self.fold_snapshot);
self.to_suggestion_point(range.start, Bias::Left);
let input_column = input_start.column();
let input_start = self.suggestion_snapshot.to_offset(input_start);
let input_end = self
.to_fold_point(range.end, Bias::Right)
.0
.to_offset(&self.fold_snapshot);
let to_next_stop = if range.start.0 + Point::new(0, to_next_stop as u32) > range.end.0 {
(range.end.column() - range.start.column()) as usize
.suggestion_snapshot
.to_offset(self.to_suggestion_point(range.end, Bias::Right).0);
let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 {
range.end.column() - range.start.column()
} else {
to_next_stop
};
TabChunks {
fold_chunks: self.fold_snapshot.chunks(
suggestion_chunks: self.suggestion_snapshot.chunks(
input_start..input_end,
language_aware,
text_highlights,
suggestion_highlight,
),
input_column,
column: expanded_char_column,
max_expansion_column: self.max_expansion_column,
output_position: range.start.0,
max_output_position: range.end.0,
tab_size: self.tab_size,
chunk: Chunk {
text: &SPACES[0..to_next_stop],
text: &SPACES[0..(to_next_stop as usize)],
..Default::default()
},
skip_leading_tab: to_next_stop > 0,
inside_leading_tab: to_next_stop > 0,
}
}
pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows {
self.fold_snapshot.buffer_rows(row)
pub fn buffer_rows(&self, row: u32) -> suggestion_map::SuggestionBufferRows {
self.suggestion_snapshot.buffer_rows(row)
}
#[cfg(test)]
pub fn text(&self) -> String {
self.chunks(TabPoint::zero()..self.max_point(), false, None)
self.chunks(TabPoint::zero()..self.max_point(), false, None, None)
.map(|chunk| chunk.text)
.collect()
}
pub fn max_point(&self) -> TabPoint {
self.to_tab_point(self.fold_snapshot.max_point())
self.to_tab_point(self.suggestion_snapshot.max_point())
}
pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
self.to_tab_point(
self.fold_snapshot
.clip_point(self.to_fold_point(point, bias).0, bias),
self.suggestion_snapshot
.clip_point(self.to_suggestion_point(point, bias).0, bias),
)
}
pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
let expanded = Self::expand_tabs(chars, input.column() as usize, self.tab_size);
TabPoint::new(input.row(), expanded as u32)
pub fn to_tab_point(&self, input: SuggestionPoint) -> TabPoint {
let chars = self
.suggestion_snapshot
.chars_at(SuggestionPoint::new(input.row(), 0));
let expanded = self.expand_tabs(chars, input.column());
TabPoint::new(input.row(), expanded)
}
pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, usize, usize) {
let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
let expanded = output.column() as usize;
pub fn to_suggestion_point(&self, output: TabPoint, bias: Bias) -> (SuggestionPoint, u32, u32) {
let chars = self
.suggestion_snapshot
.chars_at(SuggestionPoint::new(output.row(), 0));
let expanded = output.column();
let (collapsed, expanded_char_column, to_next_stop) =
Self::collapse_tabs(chars, expanded, bias, self.tab_size);
self.collapse_tabs(chars, expanded, bias);
(
FoldPoint::new(output.row(), collapsed as u32),
SuggestionPoint::new(output.row(), collapsed as u32),
expanded_char_column,
to_next_stop,
)
}
pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
self.to_tab_point(self.fold_snapshot.to_fold_point(point, bias))
let fold_point = self
.suggestion_snapshot
.fold_snapshot
.to_fold_point(point, bias);
let suggestion_point = self.suggestion_snapshot.to_suggestion_point(fold_point);
self.to_tab_point(suggestion_point)
}
pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
self.to_fold_point(point, bias)
.0
.to_buffer_point(&self.fold_snapshot)
let suggestion_point = self.to_suggestion_point(point, bias).0;
let fold_point = self.suggestion_snapshot.to_fold_point(suggestion_point);
fold_point.to_buffer_point(&self.suggestion_snapshot.fold_snapshot)
}
fn expand_tabs(
chars: impl Iterator<Item = char>,
column: usize,
tab_size: NonZeroU32,
) -> usize {
fn expand_tabs(&self, chars: impl Iterator<Item = char>, column: u32) -> u32 {
let tab_size = self.tab_size.get();
let mut expanded_chars = 0;
let mut expanded_bytes = 0;
let mut collapsed_bytes = 0;
let end_column = column.min(self.max_expansion_column);
for c in chars {
if collapsed_bytes == column {
if collapsed_bytes >= end_column {
break;
}
if c == '\t' {
let tab_size = tab_size.get() as usize;
let tab_len = tab_size - expanded_chars % tab_size;
expanded_bytes += tab_len;
expanded_chars += tab_len;
} else {
expanded_bytes += c.len_utf8();
expanded_bytes += c.len_utf8() as u32;
expanded_chars += 1;
}
collapsed_bytes += c.len_utf8();
collapsed_bytes += c.len_utf8() as u32;
}
expanded_bytes
expanded_bytes + column.saturating_sub(collapsed_bytes)
}
fn collapse_tabs(
&self,
chars: impl Iterator<Item = char>,
column: usize,
column: u32,
bias: Bias,
tab_size: NonZeroU32,
) -> (usize, usize, usize) {
) -> (u32, u32, u32) {
let tab_size = self.tab_size.get();
let mut expanded_bytes = 0;
let mut expanded_chars = 0;
let mut collapsed_bytes = 0;
@@ -301,9 +376,11 @@ impl TabSnapshot {
if expanded_bytes >= column {
break;
}
if collapsed_bytes >= self.max_expansion_column {
break;
}
if c == '\t' {
let tab_size = tab_size.get() as usize;
let tab_len = tab_size - (expanded_chars % tab_size);
expanded_chars += tab_len;
expanded_bytes += tab_len;
@@ -316,7 +393,7 @@ impl TabSnapshot {
}
} else {
expanded_chars += 1;
expanded_bytes += c.len_utf8();
expanded_bytes += c.len_utf8() as u32;
}
if expanded_bytes > column && matches!(bias, Bias::Left) {
@@ -324,9 +401,13 @@ impl TabSnapshot {
break;
}
collapsed_bytes += c.len_utf8();
collapsed_bytes += c.len_utf8() as u32;
}
(collapsed_bytes, expanded_chars, 0)
(
collapsed_bytes + column.saturating_sub(expanded_bytes),
expanded_chars,
0,
)
}
}
@@ -412,13 +493,15 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
const SPACES: &str = " ";
pub struct TabChunks<'a> {
fold_chunks: fold_map::FoldChunks<'a>,
suggestion_chunks: SuggestionChunks<'a>,
chunk: Chunk<'a>,
column: usize,
column: u32,
max_expansion_column: u32,
output_position: Point,
input_column: u32,
max_output_position: Point,
tab_size: NonZeroU32,
skip_leading_tab: bool,
inside_leading_tab: bool,
}
impl<'a> Iterator for TabChunks<'a> {
@@ -426,11 +509,12 @@ impl<'a> Iterator for TabChunks<'a> {
fn next(&mut self) -> Option<Self::Item> {
if self.chunk.text.is_empty() {
if let Some(chunk) = self.fold_chunks.next() {
if let Some(chunk) = self.suggestion_chunks.next() {
self.chunk = chunk;
if self.skip_leading_tab {
if self.inside_leading_tab {
self.chunk.text = &self.chunk.text[1..];
self.skip_leading_tab = false;
self.inside_leading_tab = false;
self.input_column += 1;
}
} else {
return None;
@@ -449,27 +533,36 @@ impl<'a> Iterator for TabChunks<'a> {
});
} else {
self.chunk.text = &self.chunk.text[1..];
let tab_size = self.tab_size.get() as u32;
let mut len = tab_size - self.column as u32 % tab_size;
let tab_size = if self.input_column < self.max_expansion_column {
self.tab_size.get() as u32
} else {
1
};
let mut len = tab_size - self.column % tab_size;
let next_output_position = cmp::min(
self.output_position + Point::new(0, len),
self.max_output_position,
);
len = next_output_position.column - self.output_position.column;
self.column += len as usize;
self.column += len;
self.input_column += 1;
self.output_position = next_output_position;
return Some(Chunk {
text: &SPACES[0..len as usize],
text: &SPACES[..len as usize],
..self.chunk
});
}
}
'\n' => {
self.column = 0;
self.input_column = 0;
self.output_position += Point::new(1, 0);
}
_ => {
self.column += 1;
if !self.inside_leading_tab {
self.input_column += c.len_utf8() as u32;
}
self.output_position.column += c.len_utf8() as u32;
}
}
@@ -482,23 +575,89 @@ impl<'a> Iterator for TabChunks<'a> {
#[cfg(test)]
mod tests {
use super::*;
use crate::{display_map::fold_map::FoldMap, MultiBuffer};
use crate::{
display_map::{fold_map::FoldMap, suggestion_map::SuggestionMap},
MultiBuffer,
};
use rand::{prelude::StdRng, Rng};
#[test]
fn test_expand_tabs() {
assert_eq!(
TabSnapshot::expand_tabs("\t".chars(), 0, 4.try_into().unwrap()),
0
);
assert_eq!(
TabSnapshot::expand_tabs("\t".chars(), 1, 4.try_into().unwrap()),
4
);
assert_eq!(
TabSnapshot::expand_tabs("\ta".chars(), 2, 4.try_into().unwrap()),
5
);
#[gpui::test]
fn test_expand_tabs(cx: &mut gpui::MutableAppContext) {
let buffer = MultiBuffer::build_simple("", cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0);
assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4);
assert_eq!(tab_snapshot.expand_tabs("\ta".chars(), 2), 5);
}
#[gpui::test]
fn test_long_lines(cx: &mut gpui::MutableAppContext) {
let max_expansion_column = 12;
let input = "A\tBC\tDEF\tG\tHI\tJ\tK\tL\tM";
let output = "A BC DEF G HI J K L M";
let buffer = MultiBuffer::build_simple(input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
let (_, mut tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
tab_snapshot.max_expansion_column = max_expansion_column;
assert_eq!(tab_snapshot.text(), output);
for (ix, c) in input.char_indices() {
assert_eq!(
tab_snapshot
.chunks(
TabPoint::new(0, ix as u32)..tab_snapshot.max_point(),
false,
None,
None,
)
.map(|c| c.text)
.collect::<String>(),
&output[ix..],
"text from index {ix}"
);
if c != '\t' {
let input_point = Point::new(0, ix as u32);
let output_point = Point::new(0, output.find(c).unwrap() as u32);
assert_eq!(
tab_snapshot.to_tab_point(SuggestionPoint(input_point)),
TabPoint(output_point),
"to_tab_point({input_point:?})"
);
assert_eq!(
tab_snapshot
.to_suggestion_point(TabPoint(output_point), Bias::Left)
.0,
SuggestionPoint(input_point),
"to_suggestion_point({output_point:?})"
);
}
}
}
#[gpui::test]
fn test_long_lines_with_character_spanning_max_expansion_column(
cx: &mut gpui::MutableAppContext,
) {
let max_expansion_column = 8;
let input = "abcdefg⋯hij";
let buffer = MultiBuffer::build_simple(input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
let (_, mut tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
tab_snapshot.max_expansion_column = max_expansion_column;
assert_eq!(tab_snapshot.text(), input);
}
#[gpui::test(iterations = 100)]
@@ -518,10 +677,15 @@ mod tests {
let (mut fold_map, _) = FoldMap::new(buffer_snapshot.clone());
fold_map.randomly_mutate(&mut rng);
let (folds_snapshot, _) = fold_map.read(buffer_snapshot, vec![]);
log::info!("FoldMap text: {:?}", folds_snapshot.text());
let (fold_snapshot, _) = fold_map.read(buffer_snapshot, vec![]);
log::info!("FoldMap text: {:?}", fold_snapshot.text());
let (suggestion_map, _) = SuggestionMap::new(fold_snapshot);
let (suggestion_snapshot, _) = suggestion_map.randomly_mutate(&mut rng);
log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
let (tab_map, _) = TabMap::new(suggestion_snapshot.clone(), tab_size);
let tabs_snapshot = tab_map.set_max_expansion_column(32);
let (_, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
let text = text::Rope::from(tabs_snapshot.text().as_str());
log::info!(
"TabMap text (tab size: {}): {:?}",
@@ -546,18 +710,18 @@ mod tests {
.collect::<String>();
let expected_summary = TextSummary::from(expected_text.as_str());
assert_eq!(
expected_text,
tabs_snapshot
.chunks(start..end, false, None)
.chunks(start..end, false, None, None)
.map(|c| c.text)
.collect::<String>(),
expected_text,
"chunks({:?}..{:?})",
start,
end
);
let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end);
if tab_size.get() > 1 && folds_snapshot.text().contains('\t') {
if tab_size.get() > 1 && suggestion_snapshot.text().contains('\t') {
actual_summary.longest_row = expected_summary.longest_row;
actual_summary.longest_row_chars = expected_summary.longest_row_chars;
}
@@ -565,7 +729,11 @@ mod tests {
}
for row in 0..=text.max_point().row {
assert_eq!(tabs_snapshot.line_len(row), text.line_len(row));
assert_eq!(
tabs_snapshot.line_len(row),
text.line_len(row),
"line_len({row})"
);
}
}
}

View File

@@ -1,12 +1,13 @@
use super::{
fold_map,
suggestion_map::SuggestionBufferRows,
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
TextHighlights,
};
use crate::MultiBufferSnapshot;
use gpui::{
fonts::FontId, text_layout::LineWrapper, Entity, ModelContext, ModelHandle, MutableAppContext,
Task,
fonts::{FontId, HighlightStyle},
text_layout::LineWrapper,
Entity, ModelContext, ModelHandle, MutableAppContext, Task,
};
use language::{Chunk, Point};
use lazy_static::lazy_static;
@@ -64,7 +65,7 @@ pub struct WrapChunks<'a> {
#[derive(Clone)]
pub struct WrapBufferRows<'a> {
input_buffer_rows: fold_map::FoldBufferRows<'a>,
input_buffer_rows: SuggestionBufferRows<'a>,
input_buffer_row: Option<u32>,
output_row: u32,
soft_wrapped: bool,
@@ -444,6 +445,7 @@ impl WrapSnapshot {
TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(),
false,
None,
None,
);
let mut edit_transforms = Vec::<Transform>::new();
for _ in edit.new_rows.start..edit.new_rows.end {
@@ -573,6 +575,7 @@ impl WrapSnapshot {
rows: Range<u32>,
language_aware: bool,
text_highlights: Option<&'a TextHighlights>,
suggestion_highlight: Option<HighlightStyle>,
) -> WrapChunks<'a> {
let output_start = WrapPoint::new(rows.start, 0);
let output_end = WrapPoint::new(rows.end, 0);
@@ -590,6 +593,7 @@ impl WrapSnapshot {
input_start..input_end,
language_aware,
text_highlights,
suggestion_highlight,
),
input_chunk: Default::default(),
output_position: output_start,
@@ -755,16 +759,24 @@ impl WrapSnapshot {
let text = language::Rope::from(self.text().as_str());
let input_buffer_rows = self.buffer_snapshot().buffer_rows(0).collect::<Vec<_>>();
let mut expected_buffer_rows = Vec::new();
let mut prev_tab_row = 0;
let mut prev_fold_row = 0;
for display_row in 0..=self.max_point().row() {
let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
if tab_point.row() == prev_tab_row && display_row != 0 {
let suggestion_point = self
.tab_snapshot
.to_suggestion_point(tab_point, Bias::Left)
.0;
let fold_point = self
.tab_snapshot
.suggestion_snapshot
.to_fold_point(suggestion_point);
if fold_point.row() == prev_fold_row && display_row != 0 {
expected_buffer_rows.push(None);
} else {
let fold_point = self.tab_snapshot.to_fold_point(tab_point, Bias::Left).0;
let buffer_point = fold_point.to_buffer_point(&self.tab_snapshot.fold_snapshot);
let buffer_point = fold_point
.to_buffer_point(&self.tab_snapshot.suggestion_snapshot.fold_snapshot);
expected_buffer_rows.push(input_buffer_rows[buffer_point.row as usize]);
prev_tab_row = tab_point.row();
prev_fold_row = fold_point.row();
}
assert_eq!(self.line_len(display_row), text.line_len(display_row));
@@ -1026,7 +1038,7 @@ fn consolidate_wrap_edits(edits: &mut Vec<WrapEdit>) {
mod tests {
use super::*;
use crate::{
display_map::{fold_map::FoldMap, tab_map::TabMap},
display_map::{fold_map::FoldMap, suggestion_map::SuggestionMap, tab_map::TabMap},
MultiBuffer,
};
use gpui::test::observe;
@@ -1053,7 +1065,9 @@ mod tests {
Some(rng.gen_range(0.0..=1000.0))
};
let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let family_id = font_cache
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
@@ -1074,14 +1088,14 @@ mod tests {
}
});
let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
let (mut fold_map, folds_snapshot) = FoldMap::new(buffer_snapshot.clone());
let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
log::info!("Unwrapped text (no folds): {:?}", buffer_snapshot.text());
log::info!(
"Unwrapped text (unexpanded tabs): {:?}",
folds_snapshot.text()
);
log::info!("Unwrapped text (expanded tabs): {:?}", tabs_snapshot.text());
log::info!("Buffer text: {:?}", buffer_snapshot.text());
let (mut fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
log::info!("FoldMap text: {:?}", fold_snapshot.text());
let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
let (tab_map, _) = TabMap::new(suggestion_snapshot.clone(), tab_size);
let tabs_snapshot = tab_map.set_max_expansion_column(32);
log::info!("TabMap text: {:?}", tabs_snapshot.text());
let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system);
let unwrapped_text = tabs_snapshot.text();
@@ -1124,9 +1138,11 @@ mod tests {
wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
}
20..=39 => {
for (folds_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
let (suggestion_snapshot, suggestion_edits) =
suggestion_map.sync(fold_snapshot, fold_edits);
let (tabs_snapshot, tab_edits) =
tab_map.sync(folds_snapshot, fold_edits, tab_size);
tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
let (mut snapshot, wrap_edits) =
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
snapshot.check_invariants();
@@ -1134,6 +1150,17 @@ mod tests {
edits.push((snapshot, wrap_edits));
}
}
40..=59 => {
let (suggestion_snapshot, suggestion_edits) =
suggestion_map.randomly_mutate(&mut rng);
let (tabs_snapshot, tab_edits) =
tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
let (mut snapshot, wrap_edits) =
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
snapshot.check_invariants();
snapshot.verify_chunks(&mut rng);
edits.push((snapshot, wrap_edits));
}
_ => {
buffer.update(cx, |buffer, cx| {
let subscription = buffer.subscribe();
@@ -1145,14 +1172,15 @@ mod tests {
}
}
log::info!("Unwrapped text (no folds): {:?}", buffer_snapshot.text());
let (folds_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
log::info!(
"Unwrapped text (unexpanded tabs): {:?}",
folds_snapshot.text()
);
let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits, tab_size);
log::info!("Unwrapped text (expanded tabs): {:?}", tabs_snapshot.text());
log::info!("Buffer text: {:?}", buffer_snapshot.text());
let (fold_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
log::info!("FoldMap text: {:?}", fold_snapshot.text());
let (suggestion_snapshot, suggestion_edits) =
suggestion_map.sync(fold_snapshot, fold_edits);
log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
let (tabs_snapshot, tab_edits) =
tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
log::info!("TabMap text: {:?}", tabs_snapshot.text());
let unwrapped_text = tabs_snapshot.text();
let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
@@ -1199,7 +1227,7 @@ mod tests {
if tab_size.get() == 1
|| !wrapped_snapshot
.tab_snapshot
.fold_snapshot
.suggestion_snapshot
.text()
.contains('\t')
{
@@ -1292,7 +1320,7 @@ mod tests {
}
pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
self.chunks(wrap_row..self.max_point().row() + 1, false, None)
self.chunks(wrap_row..self.max_point().row() + 1, false, None, None)
.map(|h| h.text)
}
@@ -1316,7 +1344,7 @@ mod tests {
}
let actual_text = self
.chunks(start_row..end_row, true, None)
.chunks(start_row..end_row, true, None, None)
.map(|c| c.text)
.collect::<String>();
assert_eq!(

View File

@@ -24,6 +24,7 @@ use anyhow::Result;
use blink_manager::BlinkManager;
use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
use copilot::Copilot;
pub use display_map::DisplayPoint;
use display_map::*;
pub use element::*;
@@ -39,7 +40,7 @@ use gpui::{
impl_actions, impl_internal_actions,
keymap_matcher::KeymapContext,
platform::CursorStyle,
serde_json::json,
serde_json::{self, json},
AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View,
ViewContext, ViewHandle, WeakViewHandle,
@@ -163,12 +164,12 @@ pub struct ToggleComments {
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct FoldAt {
pub display_row: u32,
pub buffer_row: u32,
}
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct UnfoldAt {
pub display_row: u32,
pub buffer_row: u32,
}
#[derive(Clone, Default, Deserialize, PartialEq)]
@@ -258,7 +259,8 @@ actions!(
Hover,
Format,
ToggleSoftWrap,
RevealInFinder
RevealInFinder,
CopyHighlightJson
]
);
@@ -378,6 +380,7 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(Editor::jump);
cx.add_action(Editor::toggle_soft_wrap);
cx.add_action(Editor::reveal_in_finder);
cx.add_action(Editor::copy_highlight_json);
cx.add_async_action(Editor::format);
cx.add_action(Editor::restart_language_server);
cx.add_action(Editor::show_character_palette);
@@ -386,6 +389,9 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_async_action(Editor::rename);
cx.add_async_action(Editor::confirm_rename);
cx.add_async_action(Editor::find_all_references);
cx.add_action(Editor::next_copilot_suggestion);
cx.add_action(Editor::previous_copilot_suggestion);
cx.add_action(Editor::toggle_copilot_suggestions);
hover_popover::init(cx);
link_go_to_definition::init(cx);
@@ -504,6 +510,7 @@ pub struct Editor {
hover_state: HoverState,
gutter_hovered: bool,
link_go_to_definition_state: LinkGoToDefinitionState,
pub copilot_state: CopilotState,
_subscriptions: Vec<Subscription>,
}
@@ -1001,6 +1008,76 @@ impl CodeActionsMenu {
}
}
pub struct CopilotState {
excerpt_id: Option<ExcerptId>,
pending_refresh: Task<Option<()>>,
completions: Vec<copilot::Completion>,
active_completion_index: usize,
pub user_enabled: Option<bool>,
}
impl Default for CopilotState {
fn default() -> Self {
Self {
excerpt_id: None,
pending_refresh: Task::ready(Some(())),
completions: Default::default(),
active_completion_index: 0,
user_enabled: None,
}
}
}
impl CopilotState {
fn text_for_active_completion(
&self,
cursor: Anchor,
buffer: &MultiBufferSnapshot,
) -> Option<&str> {
use language::ToOffset as _;
let completion = self.completions.get(self.active_completion_index)?;
let excerpt_id = self.excerpt_id?;
let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?;
let mut completion_range = completion.range.to_offset(&completion_buffer);
let prefix_len = Self::common_prefix(
completion_buffer.chars_for_range(completion_range.clone()),
completion.text.chars(),
);
completion_range.start += prefix_len;
let suffix_len = Self::common_prefix(
completion_buffer.reversed_chars_for_range(completion_range.clone()),
completion.text[prefix_len..].chars().rev(),
);
completion_range.end = completion_range.end.saturating_sub(suffix_len);
if completion_range.is_empty()
&& completion_range.start == cursor.text_anchor.to_offset(&completion_buffer)
{
Some(&completion.text[prefix_len..completion.text.len() - suffix_len])
} else {
None
}
}
fn push_completion(&mut self, new_completion: copilot::Completion) {
for completion in &self.completions {
if *completion == new_completion {
return;
}
}
self.completions.push(new_completion);
}
fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b: T2) -> usize {
a.zip(b)
.take_while(|(a, b)| a == b)
.map(|(a, _)| a.len_utf8())
.sum()
}
}
#[derive(Debug)]
struct ActiveDiagnosticGroup {
primary_range: Range<Anchor>,
@@ -1174,6 +1251,7 @@ impl Editor {
remote_id: None,
hover_state: Default::default(),
link_go_to_definition_state: Default::default(),
copilot_state: Default::default(),
gutter_hovered: false,
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
@@ -1383,6 +1461,7 @@ impl Editor {
self.refresh_code_actions(cx);
self.refresh_document_highlights(cx);
refresh_matching_bracket_highlights(self, cx);
self.refresh_copilot_suggestions(cx);
}
self.blink_manager.update(cx, BlinkManager::pause_blinking);
@@ -1756,6 +1835,10 @@ impl Editor {
return;
}
if self.clear_copilot_suggestions(cx) {
return;
}
if self.snippet_stack.pop().is_some() {
return;
}
@@ -2059,6 +2142,21 @@ impl Editor {
}
pub fn insert(&mut self, text: &str, cx: &mut ViewContext<Self>) {
self.insert_with_autoindent_mode(
text,
Some(AutoindentMode::Block {
original_indent_columns: Vec::new(),
}),
cx,
);
}
fn insert_with_autoindent_mode(
&mut self,
text: &str,
autoindent_mode: Option<AutoindentMode>,
cx: &mut ViewContext<Self>,
) {
let text: Arc<str> = text.into();
self.transact(cx, |this, cx| {
let old_selections = this.selections.all_adjusted(cx);
@@ -2077,9 +2175,7 @@ impl Editor {
old_selections
.iter()
.map(|s| (s.start..s.end, text.clone())),
Some(AutoindentMode::Block {
original_indent_columns: Vec::new(),
}),
autoindent_mode,
cx,
);
anchors
@@ -2675,6 +2771,240 @@ impl Editor {
None
}
fn refresh_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
let copilot = Copilot::global(cx)?;
if self.mode != EditorMode::Full {
return None;
}
let settings = cx.global::<Settings>();
if !self
.copilot_state
.user_enabled
.unwrap_or_else(|| settings.copilot_on(None))
{
return None;
}
let snapshot = self.buffer.read(cx).snapshot(cx);
let selection = self.selections.newest_anchor();
if !self.copilot_state.user_enabled.is_some() {
let language_name = snapshot
.language_at(selection.start)
.map(|language| language.name());
let copilot_enabled = settings.copilot_on(language_name.as_deref());
if !copilot_enabled {
return None;
}
}
let cursor = if selection.start == selection.end {
selection.start.bias_left(&snapshot)
} else {
self.clear_copilot_suggestions(cx);
return None;
};
if let Some(new_text) = self
.copilot_state
.text_for_active_completion(cursor, &snapshot)
{
self.display_map.update(cx, |map, cx| {
map.replace_suggestion(
Some(Suggestion {
position: cursor,
text: new_text.into(),
}),
cx,
)
});
self.copilot_state
.completions
.swap(0, self.copilot_state.active_completion_index);
self.copilot_state.completions.truncate(1);
self.copilot_state.active_completion_index = 0;
cx.notify();
} else {
self.display_map
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
}
if !copilot.read(cx).status().is_authorized() {
return None;
}
let (buffer, buffer_position) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move {
let (completion, completions_cycling) = copilot.update(&mut cx, |copilot, cx| {
(
copilot.completion(&buffer, buffer_position, cx),
copilot.completions_cycling(&buffer, buffer_position, cx),
)
});
let (completion, completions_cycling) = futures::join!(completion, completions_cycling);
let mut completions = Vec::new();
completions.extend(completion.log_err().flatten());
completions.extend(completions_cycling.log_err().into_iter().flatten());
this.upgrade(&cx)?.update(&mut cx, |this, cx| {
if !completions.is_empty() {
this.copilot_state.completions.clear();
this.copilot_state.active_completion_index = 0;
this.copilot_state.excerpt_id = Some(cursor.excerpt_id);
for completion in completions {
this.copilot_state.push_completion(completion);
}
let buffer = this.buffer.read(cx).snapshot(cx);
if let Some(text) = this
.copilot_state
.text_for_active_completion(cursor, &buffer)
{
this.display_map.update(cx, |map, cx| {
map.replace_suggestion(
Some(Suggestion {
position: cursor,
text: text.into(),
}),
cx,
)
});
}
cx.notify();
}
});
Some(())
});
Some(())
}
fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
// Auto re-enable copilot if you're asking for a suggestion
if self.copilot_state.user_enabled == Some(false) {
cx.notify();
self.copilot_state.user_enabled = Some(true);
}
if self.copilot_state.completions.is_empty() {
self.refresh_copilot_suggestions(cx);
return;
}
self.copilot_state.active_completion_index =
(self.copilot_state.active_completion_index + 1) % self.copilot_state.completions.len();
self.sync_suggestion(cx);
}
fn previous_copilot_suggestion(
&mut self,
_: &copilot::PreviousSuggestion,
cx: &mut ViewContext<Self>,
) {
// Auto re-enable copilot if you're asking for a suggestion
if self.copilot_state.user_enabled == Some(false) {
cx.notify();
self.copilot_state.user_enabled = Some(true);
}
if self.copilot_state.completions.is_empty() {
self.refresh_copilot_suggestions(cx);
return;
}
self.copilot_state.active_completion_index =
if self.copilot_state.active_completion_index == 0 {
self.copilot_state.completions.len() - 1
} else {
self.copilot_state.active_completion_index - 1
};
self.sync_suggestion(cx);
}
fn toggle_copilot_suggestions(&mut self, _: &copilot::Toggle, cx: &mut ViewContext<Self>) {
self.copilot_state.user_enabled = match self.copilot_state.user_enabled {
Some(enabled) => Some(!enabled),
None => {
let selection = self.selections.newest_anchor().start;
let language_name = self
.snapshot(cx)
.language_at(selection)
.map(|language| language.name());
let copilot_enabled = cx.global::<Settings>().copilot_on(language_name.as_deref());
Some(!copilot_enabled)
}
};
// We know this can't be None, as we just set it to Some above
if self.copilot_state.user_enabled == Some(true) {
self.refresh_copilot_suggestions(cx);
} else {
self.clear_copilot_suggestions(cx);
}
cx.notify();
}
fn sync_suggestion(&mut self, cx: &mut ViewContext<Self>) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let cursor = self.selections.newest_anchor().head();
if let Some(text) = self
.copilot_state
.text_for_active_completion(cursor, &snapshot)
{
self.display_map.update(cx, |map, cx| {
map.replace_suggestion(
Some(Suggestion {
position: cursor,
text: text.into(),
}),
cx,
)
});
cx.notify();
}
}
fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
let snapshot = self.buffer.read(cx).snapshot(cx);
let cursor = self.selections.newest_anchor().head();
if let Some(text) = self
.copilot_state
.text_for_active_completion(cursor, &snapshot)
{
self.insert_with_autoindent_mode(&text.to_string(), None, cx);
self.clear_copilot_suggestions(cx);
true
} else {
false
}
}
fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) -> bool {
self.display_map
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
let was_empty = self.copilot_state.completions.is_empty();
self.copilot_state.completions.clear();
self.copilot_state.active_completion_index = 0;
self.copilot_state.pending_refresh = Task::ready(None);
self.copilot_state.excerpt_id = None;
cx.notify();
!was_empty
}
pub fn render_code_actions_indicator(
&self,
style: &EditorStyle,
@@ -2705,31 +3035,26 @@ impl Editor {
pub fn render_fold_indicators(
&self,
fold_data: Option<Vec<(u32, FoldStatus)>>,
active_rows: &BTreeMap<u32, bool>,
fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
style: &EditorStyle,
gutter_hovered: bool,
line_height: f32,
gutter_margin: f32,
cx: &mut RenderContext<Self>,
) -> Option<Vec<(u32, ElementBox)>> {
) -> Vec<Option<ElementBox>> {
enum FoldIndicators {}
let style = style.folds.clone();
fold_data.map(|fold_data| {
fold_data
.iter()
.copied()
.filter_map(|(fold_location, fold_status)| {
(gutter_hovered
|| fold_status == FoldStatus::Folded
|| !*active_rows.get(&fold_location).unwrap_or(&true))
.then(|| {
(
fold_location,
fold_data
.iter()
.enumerate()
.map(|(ix, fold_data)| {
fold_data
.map(|(fold_status, buffer_row, active)| {
(active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
MouseEventHandler::<FoldIndicators>::new(
fold_location as usize,
ix as usize,
cx,
|mouse_state, _| -> ElementBox {
Svg::new(match fold_status {
@@ -2760,21 +3085,17 @@ impl Editor {
.on_click(MouseButton::Left, {
move |_, cx| {
cx.dispatch_any_action(match fold_status {
FoldStatus::Folded => Box::new(UnfoldAt {
display_row: fold_location,
}),
FoldStatus::Foldable => Box::new(FoldAt {
display_row: fold_location,
}),
FoldStatus::Folded => Box::new(UnfoldAt { buffer_row }),
FoldStatus::Foldable => Box::new(FoldAt { buffer_row }),
});
}
})
.boxed(),
)
.boxed()
})
})
})
.collect()
})
.flatten()
})
.collect()
}
pub fn context_menu_visible(&self) -> bool {
@@ -2991,6 +3312,10 @@ impl Editor {
}
pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
if self.accept_copilot_suggestion(cx) {
return;
}
if self.move_to_next_snippet_tabstop(cx) {
return;
}
@@ -5759,18 +6084,16 @@ impl Editor {
let selections = self.selections.all::<Point>(cx);
for selection in selections {
let range = selection.display_range(&display_map).sorted();
let buffer_start_row = range.start.to_point(&display_map).row;
let range = selection.range().sorted();
let buffer_start_row = range.start.row;
for row in (0..=range.end.row()).rev() {
let fold_range = display_map.foldable_range(row).map(|range| {
range.start.to_point(&display_map)..range.end.to_point(&display_map)
});
for row in (0..=range.end.row).rev() {
let fold_range = display_map.foldable_range(row);
if let Some(fold_range) = fold_range {
if fold_range.end.row >= buffer_start_row {
fold_ranges.push(fold_range);
if row <= range.start.row() {
if row <= range.start.row {
break;
}
}
@@ -5782,19 +6105,15 @@ impl Editor {
}
pub fn fold_at(&mut self, fold_at: &FoldAt, cx: &mut ViewContext<Self>) {
let display_row = fold_at.display_row;
let buffer_row = fold_at.buffer_row;
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
if let Some(fold_range) = display_map.foldable_range(display_row) {
if let Some(fold_range) = display_map.foldable_range(buffer_row) {
let autoscroll = self
.selections
.all::<Point>(cx)
.iter()
.any(|selection| fold_range.overlaps(&selection.display_range(&display_map)));
let fold_range =
fold_range.start.to_point(&display_map)..fold_range.end.to_point(&display_map);
.any(|selection| fold_range.overlaps(&selection.range()));
self.fold_ranges(std::iter::once(fold_range), autoscroll, cx);
}
@@ -5822,25 +6141,19 @@ impl Editor {
pub fn unfold_at(&mut self, unfold_at: &UnfoldAt, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let intersection_range = DisplayPoint::new(unfold_at.display_row, 0)
..DisplayPoint::new(
unfold_at.display_row,
display_map.line_len(unfold_at.display_row),
let intersection_range = Point::new(unfold_at.buffer_row, 0)
..Point::new(
unfold_at.buffer_row,
display_map.buffer_snapshot.line_len(unfold_at.buffer_row),
);
let autoscroll =
self.selections.all::<Point>(cx).iter().any(|selection| {
intersection_range.overlaps(&selection.display_range(&display_map))
});
let autoscroll = self
.selections
.all::<Point>(cx)
.iter()
.any(|selection| selection.range().overlaps(&intersection_range));
let display_point = DisplayPoint::new(unfold_at.display_row, 0).to_point(&display_map);
let mut point_range = display_point..display_point;
point_range.start.column = 0;
point_range.end.column = display_map.buffer_snapshot.line_len(point_range.end.row);
self.unfold_ranges(std::iter::once(point_range), true, autoscroll, cx)
self.unfold_ranges(std::iter::once(intersection_range), true, autoscroll, cx)
}
pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext<Self>) {
@@ -6176,6 +6489,7 @@ impl Editor {
multi_buffer::Event::Edited => {
self.refresh_active_diagnostics(cx);
self.refresh_code_actions(cx);
self.refresh_copilot_suggestions(cx);
cx.emit(Event::BufferEdited);
}
multi_buffer::Event::ExcerptsAdded {
@@ -6354,6 +6668,73 @@ impl Editor {
);
}
}
/// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines,
/// with each line being an array of {text, highlight} objects.
fn copy_highlight_json(&mut self, _: &CopyHighlightJson, cx: &mut ViewContext<Self>) {
let Some(buffer) = self.buffer.read(cx).as_singleton() else {
return;
};
#[derive(Serialize)]
struct Chunk<'a> {
text: String,
highlight: Option<&'a str>,
}
let snapshot = buffer.read(cx).snapshot();
let range = self
.selected_text_range(cx)
.and_then(|selected_range| {
if selected_range.is_empty() {
None
} else {
Some(selected_range)
}
})
.unwrap_or_else(|| 0..snapshot.len());
let chunks = snapshot.chunks(range, true);
let mut lines = Vec::new();
let mut line: VecDeque<Chunk> = VecDeque::new();
let theme = &cx.global::<Settings>().theme.editor.syntax;
for chunk in chunks {
let highlight = chunk.syntax_highlight_id.and_then(|id| id.name(theme));
let mut chunk_lines = chunk.text.split("\n").peekable();
while let Some(text) = chunk_lines.next() {
let mut merged_with_last_token = false;
if let Some(last_token) = line.back_mut() {
if last_token.highlight == highlight {
last_token.text.push_str(text);
merged_with_last_token = true;
}
}
if !merged_with_last_token {
line.push_back(Chunk {
text: text.into(),
highlight,
});
}
if chunk_lines.peek().is_some() {
if line.len() > 1 && line.front().unwrap().text.is_empty() {
line.pop_front();
}
if line.len() > 1 && line.back().unwrap().text.is_empty() {
line.pop_back();
}
lines.push(mem::take(&mut line));
}
}
}
let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { return; };
cx.write_to_clipboard(ClipboardItem::new(lines));
}
}
fn consume_contiguous_rows(

View File

@@ -630,7 +630,7 @@ fn test_cancel(cx: &mut gpui::MutableAppContext) {
}
#[gpui::test]
fn test_fold(cx: &mut gpui::MutableAppContext) {
fn test_fold_action(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple(
&"

View File

@@ -574,6 +574,23 @@ impl EditorElement {
}
}
for (ix, fold_indicator) in layout.fold_indicators.iter_mut().enumerate() {
if let Some(indicator) = fold_indicator.as_mut() {
let position = vec2f(
bounds.width() - layout.gutter_padding,
ix as f32 * line_height - (scroll_top % line_height),
);
let centering_offset = vec2f(
(layout.gutter_padding + layout.gutter_margin - indicator.size().x()) / 2.,
(line_height - indicator.size().y()) / 2.,
);
let indicator_origin = bounds.origin() + position + centering_offset;
indicator.paint(indicator_origin, visible_bounds, cx);
}
}
if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
let mut x = 0.;
let mut y = *row as f32 * line_height - scroll_top;
@@ -581,19 +598,6 @@ impl EditorElement {
y += (line_height - indicator.size().y()) / 2.;
indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
}
layout.fold_indicators.as_mut().map(|fold_indicators| {
for (line, fold_indicator) in fold_indicators.iter_mut() {
let mut x = bounds.width() - layout.gutter_padding;
let mut y = *line as f32 * line_height - scroll_top;
x += ((layout.gutter_padding + layout.gutter_margin) - fold_indicator.size().x())
/ 2.;
y += (line_height - fold_indicator.size().y()) / 2.;
fold_indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
}
});
}
fn paint_diff_hunks(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
@@ -739,10 +743,15 @@ impl EditorElement {
});
let display_row = range.start.row();
let buffer_row = DisplayPoint::new(display_row, 0)
.to_point(&layout.position_map.snapshot.display_snapshot)
.row;
cx.scene.push_mouse_region(
MouseRegion::new::<FoldMarkers>(self.view.id(), *id as usize, bound)
.on_click(MouseButton::Left, move |_, cx| {
cx.dispatch_action(UnfoldAt { display_row })
cx.dispatch_action(UnfoldAt { buffer_row })
})
.with_notify_on_hover(true)
.with_notify_on_click(true),
@@ -1181,24 +1190,6 @@ impl EditorElement {
.width()
}
fn get_fold_indicators(
&self,
is_singleton: bool,
display_rows: Range<u32>,
snapshot: &EditorSnapshot,
) -> Option<Vec<(u32, FoldStatus)>> {
is_singleton.then(|| {
display_rows
.into_iter()
.filter_map(|display_row| {
snapshot
.fold_for_line(display_row)
.map(|fold_status| (display_row, fold_status))
})
.collect()
})
}
//Folds contained in a hunk are ignored apart from shrinking visual size
//If a fold contains any hunks then that fold line is marked as modified
fn layout_git_gutters(
@@ -1226,12 +1217,17 @@ impl EditorElement {
&self,
rows: Range<u32>,
active_rows: &BTreeMap<u32, bool>,
is_singleton: bool,
snapshot: &EditorSnapshot,
cx: &LayoutContext,
) -> Vec<Option<text_layout::Line>> {
) -> (
Vec<Option<text_layout::Line>>,
Vec<Option<(FoldStatus, BufferRow, bool)>>,
) {
let style = &self.style;
let include_line_numbers = snapshot.mode == EditorMode::Full;
let mut line_number_layouts = Vec::with_capacity(rows.len());
let mut fold_statuses = Vec::with_capacity(rows.len());
let mut line_number = String::new();
for (ix, row) in snapshot
.buffer_rows(rows.start)
@@ -1239,10 +1235,10 @@ impl EditorElement {
.enumerate()
{
let display_row = rows.start + ix as u32;
let color = if active_rows.contains_key(&display_row) {
style.line_number_active
let (active, color) = if active_rows.contains_key(&display_row) {
(true, style.line_number_active)
} else {
style.line_number
(false, style.line_number)
};
if let Some(buffer_row) = row {
if include_line_numbers {
@@ -1260,13 +1256,23 @@ impl EditorElement {
},
)],
)));
fold_statuses.push(
is_singleton
.then(|| {
snapshot
.fold_for_line(buffer_row)
.map(|fold_status| (fold_status, buffer_row, active))
})
.flatten(),
)
}
} else {
fold_statuses.push(None);
line_number_layouts.push(None);
}
}
line_number_layouts
(line_number_layouts, fold_statuses)
}
fn layout_lines(
@@ -1312,45 +1318,47 @@ impl EditorElement {
.collect()
} else {
let style = &self.style;
let chunks = snapshot.chunks(rows.clone(), true).map(|chunk| {
let mut highlight_style = chunk
.syntax_highlight_id
.and_then(|id| id.style(&style.syntax));
let chunks = snapshot
.chunks(rows.clone(), true, Some(style.theme.suggestion))
.map(|chunk| {
let mut highlight_style = chunk
.syntax_highlight_id
.and_then(|id| id.style(&style.syntax));
if let Some(chunk_highlight) = chunk.highlight_style {
if let Some(highlight_style) = highlight_style.as_mut() {
highlight_style.highlight(chunk_highlight);
} else {
highlight_style = Some(chunk_highlight);
}
}
let mut diagnostic_highlight = HighlightStyle::default();
if chunk.is_unnecessary {
diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade);
}
if let Some(severity) = chunk.diagnostic_severity {
// Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
let diagnostic_style = super::diagnostic_style(severity, true, style);
diagnostic_highlight.underline = Some(Underline {
color: Some(diagnostic_style.message.text.color),
thickness: 1.0.into(),
squiggly: true,
});
}
}
if let Some(chunk_highlight) = chunk.highlight_style {
if let Some(highlight_style) = highlight_style.as_mut() {
highlight_style.highlight(chunk_highlight);
highlight_style.highlight(diagnostic_highlight);
} else {
highlight_style = Some(chunk_highlight);
highlight_style = Some(diagnostic_highlight);
}
}
let mut diagnostic_highlight = HighlightStyle::default();
if chunk.is_unnecessary {
diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade);
}
if let Some(severity) = chunk.diagnostic_severity {
// Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
let diagnostic_style = super::diagnostic_style(severity, true, style);
diagnostic_highlight.underline = Some(Underline {
color: Some(diagnostic_style.message.text.color),
thickness: 1.0.into(),
squiggly: true,
});
}
}
if let Some(highlight_style) = highlight_style.as_mut() {
highlight_style.highlight(diagnostic_highlight);
} else {
highlight_style = Some(diagnostic_highlight);
}
(chunk.text, highlight_style)
});
(chunk.text, highlight_style)
});
layout_highlighted_chunks(
chunks,
&style.text,
@@ -1797,13 +1805,16 @@ impl Element for EditorElement {
})
.collect();
let line_number_layouts =
self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx);
let (line_number_layouts, fold_statuses) = self.layout_line_numbers(
start_row..end_row,
&active_rows,
is_singleton,
&snapshot,
cx,
);
let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot);
let folds = self.get_fold_indicators(is_singleton, start_row..end_row, &snapshot);
let scrollbar_row_range = scroll_position.y()..(scroll_position.y() + height_in_lines);
let mut max_visible_line_width = 0.0;
@@ -1896,8 +1907,7 @@ impl Element for EditorElement {
mode = view.mode;
view.render_fold_indicators(
folds,
&active_rows,
fold_statuses,
&style,
view.gutter_hovered,
line_height,
@@ -1929,8 +1939,8 @@ impl Element for EditorElement {
);
}
fold_indicators.as_mut().map(|fold_indicators| {
for (_, indicator) in fold_indicators.iter_mut() {
for fold_indicator in fold_indicators.iter_mut() {
if let Some(indicator) = fold_indicator.as_mut() {
indicator.layout(
SizeConstraint::strict_along(
Axis::Vertical,
@@ -1939,7 +1949,7 @@ impl Element for EditorElement {
cx,
);
}
});
}
if let Some((_, hover_popovers)) = hover.as_mut() {
for hover_popover in hover_popovers.iter_mut() {
@@ -2123,7 +2133,7 @@ pub struct LayoutState {
context_menu: Option<(DisplayPoint, ElementBox)>,
code_actions_indicator: Option<(u32, ElementBox)>,
hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
fold_indicators: Option<Vec<(u32, ElementBox)>>,
fold_indicators: Vec<Option<ElementBox>>,
}
pub struct PositionMap {
@@ -2524,7 +2534,9 @@ mod tests {
let snapshot = editor.snapshot(cx);
let mut presenter = cx.build_presenter(window_id, 30., Default::default());
let layout_cx = presenter.build_layout_context(Vector2F::zero(), false, cx);
element.layout_line_numbers(0..6, &Default::default(), &snapshot, &layout_cx)
element
.layout_line_numbers(0..6, &Default::default(), false, &snapshot, &layout_cx)
.0
});
assert_eq!(layouts.len(), 6);
}

View File

@@ -538,11 +538,7 @@ impl Item for Editor {
let description = path.to_string_lossy();
Some(
Label::new(
if description.len() > MAX_TAB_TITLE_LEN {
description[..MAX_TAB_TITLE_LEN].to_string() + ""
} else {
description.into()
},
util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN),
style.description.text.clone(),
)
.contained()

View File

@@ -69,16 +69,11 @@ pub fn up_by_rows(
goal_column = 0;
}
let clip_bias = if point.column() == map.line_len(point.row()) {
Bias::Left
} else {
Bias::Right
};
(
map.clip_point(point, clip_bias),
SelectionGoal::Column(goal_column),
)
let mut clipped_point = map.clip_point(point, Bias::Left);
if clipped_point.row() < point.row() {
clipped_point = map.clip_point(point, Bias::Right);
}
(clipped_point, SelectionGoal::Column(goal_column))
}
pub fn down_by_rows(
@@ -105,16 +100,11 @@ pub fn down_by_rows(
goal_column = map.column_to_chars(point.row(), point.column())
}
let clip_bias = if point.column() == map.line_len(point.row()) {
Bias::Left
} else {
Bias::Right
};
(
map.clip_point(point, clip_bias),
SelectionGoal::Column(goal_column),
)
let mut clipped_point = map.clip_point(point, Bias::Right);
if clipped_point.row() > point.row() {
clipped_point = map.clip_point(point, Bias::Left);
}
(clipped_point, SelectionGoal::Column(goal_column))
}
pub fn line_beginning(
@@ -587,7 +577,10 @@ mod tests {
#[gpui::test]
fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let family_id = cx
.font_cache()
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())

View File

@@ -2194,7 +2194,11 @@ impl MultiBufferSnapshot {
pub fn buffer_line_for_row(&self, row: u32) -> Option<(&BufferSnapshot, Range<Point>)> {
let mut cursor = self.excerpts.cursor::<Point>();
cursor.seek(&Point::new(row, 0), Bias::Right, &());
let point = Point::new(row, 0);
cursor.seek(&point, Bias::Right, &());
if cursor.item().is_none() && *cursor.start() == point {
cursor.prev(&());
}
if let Some(excerpt) = cursor.item() {
let overshoot = row - cursor.start().row;
let excerpt_start = excerpt.range.context.start.to_point(&excerpt.buffer);
@@ -2929,6 +2933,10 @@ impl MultiBufferSnapshot {
Some(self.excerpt(excerpt_id)?.buffer_id)
}
pub fn buffer_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<&BufferSnapshot> {
Some(&self.excerpt(excerpt_id)?.buffer)
}
fn excerpt<'a>(&'a self, excerpt_id: ExcerptId) -> Option<&'a Excerpt> {
let mut cursor = self.excerpts.cursor::<Option<&Locator>>();
let locator = self.excerpt_locator_for_id(excerpt_id);

View File

@@ -25,7 +25,10 @@ pub fn marked_display_snapshot(
) -> (DisplaySnapshot, Vec<DisplayPoint>) {
let (unmarked_text, markers) = marked_text_offsets(text);
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let family_id = cx
.font_cache()
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())

View File

@@ -21,14 +21,15 @@ gpui = { path = "../gpui" }
human_bytes = "0.4.1"
isahc = "1.7"
lazy_static = "1.4.0"
postage = { version = "0.4", features = ["futures-traits"] }
postage = { workspace = true }
project = { path = "../project" }
search = { path = "../search" }
serde = { version = "1.0", features = ["derive", "rc"] }
serde = { workspace = true }
serde_derive = { workspace = true }
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" }
workspace = { path = "../workspace" }

View File

@@ -36,7 +36,7 @@ impl View for DeployFeedbackButton {
.item
.style_for(state, active);
Svg::new("icons/speech_bubble_12.svg")
Svg::new("icons/feedback_16.svg")
.with_color(style.icon_color)
.constrained()
.with_width(style.icon_size)
@@ -56,7 +56,7 @@ impl View for DeployFeedbackButton {
})
.with_tooltip::<Self, _>(
0,
"Give Feedback".into(),
"Send Feedback".into(),
Some(Box::new(GiveFeedback)),
theme.tooltip.clone(),
cx,

View File

@@ -10,7 +10,7 @@ use editor::{Anchor, Editor};
use futures::AsyncReadExt;
use gpui::{
actions,
elements::{ChildView, Flex, Label, ParentElement},
elements::{ChildView, Flex, Label, ParentElement, Svg},
serde_json, AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle,
MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
};
@@ -250,7 +250,17 @@ impl Item for FeedbackEditor {
fn tab_content(&self, _: Option<usize>, style: &theme::Tab, _: &AppContext) -> ElementBox {
Flex::row()
.with_child(
Label::new("Feedback", style.label.clone())
Svg::new("icons/feedback_16.svg")
.with_color(style.label.text.color)
.constrained()
.with_width(style.type_icon_width)
.aligned()
.contained()
.with_margin_right(style.spacing)
.boxed(),
)
.with_child(
Label::new("Send Feedback", style.label.clone())
.aligned()
.contained()
.boxed(),

View File

@@ -19,11 +19,11 @@ settings = { path = "../settings" }
util = { path = "../util" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }
postage = { version = "0.4.1", features = ["futures-traits"] }
postage = { workspace = true }
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
serde_json = { workspace = true }
workspace = { path = "../workspace", features = ["test-support"] }
ctor = "0.1"
env_logger = "0.9"

View File

@@ -24,6 +24,7 @@ smol = "1.2.5"
regex = "1.5"
git2 = { version = "0.15", default-features = false }
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
libc = "0.2"

View File

@@ -380,6 +380,8 @@ struct FakeFsState {
next_inode: u64,
next_mtime: SystemTime,
event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>,
events_paused: bool,
buffered_events: Vec<fsevent::Event>,
}
#[cfg(any(test, feature = "test-support"))]
@@ -483,15 +485,21 @@ impl FakeFsState {
I: IntoIterator<Item = T>,
T: Into<PathBuf>,
{
let events = paths
.into_iter()
.map(|path| fsevent::Event {
self.buffered_events
.extend(paths.into_iter().map(|path| fsevent::Event {
event_id: 0,
flags: fsevent::StreamFlags::empty(),
path: path.into(),
})
.collect::<Vec<_>>();
}));
if !self.events_paused {
self.flush_events(self.buffered_events.len());
}
}
fn flush_events(&mut self, mut count: usize) {
count = count.min(self.buffered_events.len());
let events = self.buffered_events.drain(0..count).collect::<Vec<_>>();
self.event_txs.retain(|tx| {
let _ = tx.try_send(events.clone());
!tx.is_closed()
@@ -514,6 +522,8 @@ impl FakeFs {
next_mtime: SystemTime::UNIX_EPOCH,
next_inode: 1,
event_txs: Default::default(),
buffered_events: Vec::new(),
events_paused: false,
}),
})
}
@@ -567,6 +577,18 @@ impl FakeFs {
state.emit_event(&[path]);
}
pub async fn pause_events(&self) {
self.state.lock().await.events_paused = true;
}
pub async fn buffered_event_count(&self) -> usize {
self.state.lock().await.buffered_events.len()
}
pub async fn flush_events(&self, count: usize) {
self.state.lock().await.flush_events(count);
}
#[must_use]
pub fn insert_tree<'a>(
&'a self,
@@ -868,7 +890,7 @@ impl Fs for FakeFs {
.ok_or_else(|| anyhow!("cannot remove the root"))?;
let base_name = path.file_name().unwrap();
let state = self.state.lock().await;
let mut state = self.state.lock().await;
let parent_entry = state.read_path(parent_path).await?;
let mut parent_entry = parent_entry.lock().await;
let entry = parent_entry
@@ -892,7 +914,7 @@ impl Fs for FakeFs {
e.remove();
}
}
state.emit_event(&[path]);
Ok(())
}

View File

@@ -2,7 +2,7 @@ use anyhow::Result;
use collections::HashMap;
use parking_lot::Mutex;
use std::{
path::{Path, PathBuf},
path::{Component, Path, PathBuf},
sync::Arc,
};
@@ -27,7 +27,11 @@ impl GitRepository for LibGitRepository {
fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result<Option<String>> {
const STAGE_NORMAL: i32 = 0;
let index = repo.index()?;
let oid = match index.get_path(relative_file_path, STAGE_NORMAL) {
// This check is required because index.get_path() unwraps internally :(
check_path_to_repo_path_errors(relative_file_path)?;
let oid = match index.get_path(&relative_file_path, STAGE_NORMAL) {
Some(entry) => entry.id,
None => return Ok(None),
};
@@ -69,3 +73,32 @@ impl GitRepository for FakeGitRepository {
state.index_contents.get(path).cloned()
}
}
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
match relative_file_path.components().next() {
None => anyhow::bail!("repo path should not be empty"),
Some(Component::Prefix(_)) => anyhow::bail!(
"repo path `{}` should be relative, not a windows prefix",
relative_file_path.to_string_lossy()
),
Some(Component::RootDir) => {
anyhow::bail!(
"repo path `{}` should be relative",
relative_file_path.to_string_lossy()
)
}
Some(Component::CurDir) => {
anyhow::bail!(
"repo path `{}` should not start with `.`",
relative_file_path.to_string_lossy()
)
}
Some(Component::ParentDir) => {
anyhow::bail!(
"repo path `{}` should not start with `..`",
relative_file_path.to_string_lossy()
)
}
_ => Ok(()),
}
}

View File

@@ -15,4 +15,4 @@ menu = { path = "../menu" }
settings = { path = "../settings" }
text = { path = "../text" }
workspace = { path = "../workspace" }
postage = { version = "0.4", features = ["futures-traits"] }
postage = { workspace = true }

View File

@@ -36,12 +36,14 @@ parking = "2.0.0"
parking_lot = "0.11.1"
pathfinder_color = "0.5"
pathfinder_geometry = "0.5"
postage = { version = "0.4.1", features = ["futures-traits"] }
postage = { workspace = true }
rand = "0.8.3"
resvg = "0.14"
schemars = "0.8"
seahash = "4.1"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
time = { version = "0.3", features = ["serde", "serde-well-known"] }

View File

@@ -56,7 +56,10 @@ impl gpui::Element for TextElement {
cx: &mut gpui::PaintContext,
) -> Self::PaintState {
let font_size = 12.;
let family = cx.font_cache.load_family(&["SF Pro Display"]).unwrap();
let family = cx
.font_cache
.load_family(&["SF Pro Display"], &Default::default())
.unwrap();
let normal = RunStyle {
font_id: cx
.font_cache

View File

@@ -765,6 +765,12 @@ impl MutableAppContext {
})
}
pub fn has_window(&self, window_id: usize) -> bool {
self.window_ids()
.find(|window| window == &window_id)
.is_some()
}
pub fn window_ids(&self) -> impl Iterator<Item = usize> + '_ {
self.cx.windows.keys().copied()
}
@@ -1479,15 +1485,11 @@ impl MutableAppContext {
if let Some(focused_view_id) = self.focused_view_id(window_id) {
let dispatch_path = self
.ancestors(window_id, focused_view_id)
.map(|view_id| {
(
view_id,
self.cx
.views
.get(&(window_id, view_id))
.unwrap()
.keymap_context(self.as_ref()),
)
.filter_map(|view_id| {
self.cx
.views
.get(&(window_id, view_id))
.map(|view| (view_id, view.keymap_context(self.as_ref())))
})
.collect();
@@ -6838,7 +6840,7 @@ mod tests {
None
);
// Produces a list of actions and keybindings
// Produces a list of actions and key bindings
fn available_actions(
window_id: usize,
view_id: usize,

View File

@@ -389,6 +389,12 @@ impl ElementBox {
}
}
impl Clone for ElementBox {
fn clone(&self) -> Self {
ElementBox(self.0.clone())
}
}
impl From<ElementBox> for ElementRc {
fn from(val: ElementBox) -> Self {
val.0

View File

@@ -216,6 +216,7 @@ mod tests {
12.,
Default::default(),
Default::default(),
Default::default(),
Color::black(),
cx.font_cache(),
)
@@ -225,6 +226,7 @@ mod tests {
12.,
*FontProperties::new().weight(Weight::BOLD),
Default::default(),
Default::default(),
Color::new(255, 0, 0, 255),
cx.font_cache(),
)

View File

@@ -1,5 +1,5 @@
use crate::{
fonts::{FontId, Metrics, Properties},
fonts::{Features, FontId, Metrics, Properties},
geometry::vector::{vec2f, Vector2F},
platform,
text_layout::LineWrapper,
@@ -18,6 +18,7 @@ pub struct FamilyId(usize);
struct Family {
name: Arc<str>,
font_features: Features,
font_ids: Vec<FontId>,
}
@@ -58,17 +59,21 @@ impl FontCache {
.map(|family| family.name.clone())
}
pub fn load_family(&self, names: &[&str]) -> Result<FamilyId> {
pub fn load_family(&self, names: &[&str], features: &Features) -> Result<FamilyId> {
for name in names {
let state = self.0.upgradable_read();
if let Some(ix) = state.families.iter().position(|f| f.name.as_ref() == *name) {
if let Some(ix) = state
.families
.iter()
.position(|f| f.name.as_ref() == *name && f.font_features == *features)
{
return Ok(FamilyId(ix));
}
let mut state = RwLockUpgradableReadGuard::upgrade(state);
if let Ok(font_ids) = state.fonts.load_family(name) {
if let Ok(font_ids) = state.fonts.load_family(name, features) {
if font_ids.is_empty() {
continue;
}
@@ -82,6 +87,7 @@ impl FontCache {
state.families.push(Family {
name: Arc::from(*name),
font_features: features.clone(),
font_ids,
});
return Ok(family_id);
@@ -254,7 +260,15 @@ mod tests {
fn test_select_font() {
let platform = test::platform();
let fonts = FontCache::new(platform.fonts());
let arial = fonts.load_family(&["Arial"]).unwrap();
let arial = fonts
.load_family(
&["Arial"],
&Features {
calt: Some(false),
..Default::default()
},
)
.unwrap();
let arial_regular = fonts.select_font(arial, &Properties::new()).unwrap();
let arial_italic = fonts
.select_font(arial, Properties::new().style(Style::Italic))
@@ -265,5 +279,16 @@ mod tests {
assert_ne!(arial_regular, arial_italic);
assert_ne!(arial_regular, arial_bold);
assert_ne!(arial_italic, arial_bold);
let arial_with_calt = fonts
.load_family(
&["Arial"],
&Features {
calt: Some(true),
..Default::default()
},
)
.unwrap();
assert_ne!(arial_with_calt, arial);
}
}

View File

@@ -11,7 +11,8 @@ pub use font_kit::{
properties::{Properties, Stretch, Style, Weight},
};
use ordered_float::OrderedFloat;
use serde::{de, Deserialize};
use schemars::JsonSchema;
use serde::{de, Deserialize, Serialize};
use serde_json::Value;
use std::{cell::RefCell, sync::Arc};
@@ -20,6 +21,44 @@ pub struct FontId(pub usize);
pub type GlyphId = u32;
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Features {
pub calt: Option<bool>,
pub case: Option<bool>,
pub cpsp: Option<bool>,
pub frac: Option<bool>,
pub liga: Option<bool>,
pub onum: Option<bool>,
pub ordn: Option<bool>,
pub pnum: Option<bool>,
pub ss01: Option<bool>,
pub ss02: Option<bool>,
pub ss03: Option<bool>,
pub ss04: Option<bool>,
pub ss05: Option<bool>,
pub ss06: Option<bool>,
pub ss07: Option<bool>,
pub ss08: Option<bool>,
pub ss09: Option<bool>,
pub ss10: Option<bool>,
pub ss11: Option<bool>,
pub ss12: Option<bool>,
pub ss13: Option<bool>,
pub ss14: Option<bool>,
pub ss15: Option<bool>,
pub ss16: Option<bool>,
pub ss17: Option<bool>,
pub ss18: Option<bool>,
pub ss19: Option<bool>,
pub ss20: Option<bool>,
pub subs: Option<bool>,
pub sups: Option<bool>,
pub swsh: Option<bool>,
pub titl: Option<bool>,
pub tnum: Option<bool>,
pub zero: Option<bool>,
}
#[derive(Clone, Debug)]
pub struct TextStyle {
pub color: Color,
@@ -71,6 +110,8 @@ thread_local! {
struct TextStyleJson {
color: Color,
family: String,
#[serde(default)]
features: Features,
weight: Option<WeightJson>,
size: f32,
#[serde(default)]
@@ -107,12 +148,13 @@ impl TextStyle {
font_family_name: impl Into<Arc<str>>,
font_size: f32,
font_properties: Properties,
font_features: Features,
underline: Underline,
color: Color,
font_cache: &FontCache,
) -> Result<Self> {
let font_family_name = font_family_name.into();
let font_family_id = font_cache.load_family(&[&font_family_name])?;
let font_family_id = font_cache.load_family(&[&font_family_name], &font_features)?;
let font_id = font_cache.select_font(font_family_id, &font_properties)?;
Ok(Self {
color,
@@ -175,6 +217,7 @@ impl TextStyle {
json.family,
json.size,
font_properties,
json.features,
underline_from_json(json.underline),
json.color,
font_cache,
@@ -253,7 +296,9 @@ impl Default for TextStyle {
.expect("TextStyle::default can only be called within a call to with_font_cache");
let font_family_name = Arc::from("Courier");
let font_family_id = font_cache.load_family(&[&font_family_name]).unwrap();
let font_family_id = font_cache
.load_family(&[&font_family_name], &Default::default())
.unwrap();
let font_id = font_cache
.select_font(font_family_id, &Default::default())
.unwrap();

View File

@@ -9,7 +9,10 @@ pub mod current {
use crate::{
executor,
fonts::{FontId, GlyphId, Metrics as FontMetrics, Properties as FontProperties},
fonts::{
Features as FontFeatures, FontId, GlyphId, Metrics as FontMetrics,
Properties as FontProperties,
},
geometry::{
rect::{RectF, RectI},
vector::Vector2F,
@@ -335,7 +338,7 @@ pub enum RasterizationOptions {
pub trait FontSystem: Send + Sync {
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()>;
fn load_family(&self, name: &str) -> anyhow::Result<Vec<FontId>>;
fn load_family(&self, name: &str, features: &FontFeatures) -> anyhow::Result<Vec<FontId>>;
fn select_font(
&self,
font_ids: &[FontId],

View File

@@ -1,5 +1,7 @@
mod open_type;
use crate::{
fonts::{FontId, GlyphId, Metrics, Properties},
fonts::{Features, FontId, GlyphId, Metrics, Properties},
geometry::{
rect::{RectF, RectI},
transform2d::Transform2F,
@@ -64,8 +66,8 @@ impl platform::FontSystem for FontSystem {
self.0.write().add_fonts(fonts)
}
fn load_family(&self, name: &str) -> anyhow::Result<Vec<FontId>> {
self.0.write().load_family(name)
fn load_family(&self, name: &str, features: &Features) -> anyhow::Result<Vec<FontId>> {
self.0.write().load_family(name, features)
}
fn select_font(&self, font_ids: &[FontId], properties: &Properties) -> anyhow::Result<FontId> {
@@ -126,7 +128,7 @@ impl FontSystemState {
Ok(())
}
fn load_family(&mut self, name: &str) -> anyhow::Result<Vec<FontId>> {
fn load_family(&mut self, name: &str, features: &Features) -> anyhow::Result<Vec<FontId>> {
let mut font_ids = Vec::new();
let family = self
@@ -134,7 +136,8 @@ impl FontSystemState {
.select_family_by_name(name)
.or_else(|_| self.system_source.select_family_by_name(name))?;
for font in family.fonts() {
let font = font.load()?;
let mut font = font.load()?;
open_type::apply_features(&mut font, features);
let font_id = FontId(self.fonts.len());
font_ids.push(font_id);
let postscript_name = font.postscript_name().unwrap();
@@ -503,7 +506,7 @@ mod tests {
fn test_layout_str(_: &mut MutableAppContext) {
// This is failing intermittently on CI and we don't have time to figure it out
let fonts = FontSystem::new();
let menlo = fonts.load_family("Menlo").unwrap();
let menlo = fonts.load_family("Menlo", &Default::default()).unwrap();
let menlo_regular = RunStyle {
font_id: fonts.select_font(&menlo, &Properties::new()).unwrap(),
color: Default::default(),
@@ -544,13 +547,13 @@ mod tests {
#[test]
fn test_glyph_offsets() -> anyhow::Result<()> {
let fonts = FontSystem::new();
let zapfino = fonts.load_family("Zapfino")?;
let zapfino = fonts.load_family("Zapfino", &Default::default())?;
let zapfino_regular = RunStyle {
font_id: fonts.select_font(&zapfino, &Properties::new())?,
color: Default::default(),
underline: Default::default(),
};
let menlo = fonts.load_family("Menlo")?;
let menlo = fonts.load_family("Menlo", &Default::default())?;
let menlo_regular = RunStyle {
font_id: fonts.select_font(&menlo, &Properties::new())?,
color: Default::default(),
@@ -584,7 +587,7 @@ mod tests {
use std::{fs::File, io::BufWriter, path::Path};
let fonts = FontSystem::new();
let font_ids = fonts.load_family("Fira Code").unwrap();
let font_ids = fonts.load_family("Fira Code", &Default::default()).unwrap();
let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
let glyph_id = fonts.glyph_for_char(font_id, 'G').unwrap();
@@ -618,7 +621,7 @@ mod tests {
#[test]
fn test_wrap_line() {
let fonts = FontSystem::new();
let font_ids = fonts.load_family("Helvetica").unwrap();
let font_ids = fonts.load_family("Helvetica", &Default::default()).unwrap();
let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
let line = "one two three four five\n";
@@ -636,7 +639,7 @@ mod tests {
#[test]
fn test_layout_line_bom_char() {
let fonts = FontSystem::new();
let font_ids = fonts.load_family("Helvetica").unwrap();
let font_ids = fonts.load_family("Helvetica", &Default::default()).unwrap();
let style = RunStyle {
font_id: fonts.select_font(&font_ids, &Default::default()).unwrap(),
color: Default::default(),

View File

@@ -0,0 +1,395 @@
#![allow(unused, non_upper_case_globals)]
use std::ptr;
use crate::fonts::Features;
use cocoa::appkit::CGFloat;
use core_foundation::{base::TCFType, number::CFNumber};
use core_graphics::geometry::CGAffineTransform;
use core_text::{
font::{CTFont, CTFontRef},
font_descriptor::{
CTFontDescriptor, CTFontDescriptorCreateCopyWithFeature, CTFontDescriptorRef,
},
};
use font_kit::font::Font;
const kCaseSensitiveLayoutOffSelector: i32 = 1;
const kCaseSensitiveLayoutOnSelector: i32 = 0;
const kCaseSensitiveLayoutType: i32 = 33;
const kCaseSensitiveSpacingOffSelector: i32 = 3;
const kCaseSensitiveSpacingOnSelector: i32 = 2;
const kCharacterAlternativesType: i32 = 17;
const kCommonLigaturesOffSelector: i32 = 3;
const kCommonLigaturesOnSelector: i32 = 2;
const kContextualAlternatesOffSelector: i32 = 1;
const kContextualAlternatesOnSelector: i32 = 0;
const kContextualAlternatesType: i32 = 36;
const kContextualLigaturesOffSelector: i32 = 19;
const kContextualLigaturesOnSelector: i32 = 18;
const kContextualSwashAlternatesOffSelector: i32 = 5;
const kContextualSwashAlternatesOnSelector: i32 = 4;
const kDefaultLowerCaseSelector: i32 = 0;
const kDefaultUpperCaseSelector: i32 = 0;
const kDiagonalFractionsSelector: i32 = 2;
const kFractionsType: i32 = 11;
const kHistoricalLigaturesOffSelector: i32 = 21;
const kHistoricalLigaturesOnSelector: i32 = 20;
const kHojoCharactersSelector: i32 = 12;
const kInferiorsSelector: i32 = 2;
const kJIS2004CharactersSelector: i32 = 11;
const kLigaturesType: i32 = 1;
const kLowerCasePetiteCapsSelector: i32 = 2;
const kLowerCaseSmallCapsSelector: i32 = 1;
const kLowerCaseType: i32 = 37;
const kLowerCaseNumbersSelector: i32 = 0;
const kMathematicalGreekOffSelector: i32 = 11;
const kMathematicalGreekOnSelector: i32 = 10;
const kMonospacedNumbersSelector: i32 = 0;
const kNLCCharactersSelector: i32 = 13;
const kNoFractionsSelector: i32 = 0;
const kNormalPositionSelector: i32 = 0;
const kNoStyleOptionsSelector: i32 = 0;
const kNumberCaseType: i32 = 21;
const kNumberSpacingType: i32 = 6;
const kOrdinalsSelector: i32 = 3;
const kProportionalNumbersSelector: i32 = 1;
const kQuarterWidthTextSelector: i32 = 4;
const kScientificInferiorsSelector: i32 = 4;
const kSlashedZeroOffSelector: i32 = 5;
const kSlashedZeroOnSelector: i32 = 4;
const kStyleOptionsType: i32 = 19;
const kStylisticAltEighteenOffSelector: i32 = 37;
const kStylisticAltEighteenOnSelector: i32 = 36;
const kStylisticAltEightOffSelector: i32 = 17;
const kStylisticAltEightOnSelector: i32 = 16;
const kStylisticAltElevenOffSelector: i32 = 23;
const kStylisticAltElevenOnSelector: i32 = 22;
const kStylisticAlternativesType: i32 = 35;
const kStylisticAltFifteenOffSelector: i32 = 31;
const kStylisticAltFifteenOnSelector: i32 = 30;
const kStylisticAltFiveOffSelector: i32 = 11;
const kStylisticAltFiveOnSelector: i32 = 10;
const kStylisticAltFourOffSelector: i32 = 9;
const kStylisticAltFourOnSelector: i32 = 8;
const kStylisticAltFourteenOffSelector: i32 = 29;
const kStylisticAltFourteenOnSelector: i32 = 28;
const kStylisticAltNineOffSelector: i32 = 19;
const kStylisticAltNineOnSelector: i32 = 18;
const kStylisticAltNineteenOffSelector: i32 = 39;
const kStylisticAltNineteenOnSelector: i32 = 38;
const kStylisticAltOneOffSelector: i32 = 3;
const kStylisticAltOneOnSelector: i32 = 2;
const kStylisticAltSevenOffSelector: i32 = 15;
const kStylisticAltSevenOnSelector: i32 = 14;
const kStylisticAltSeventeenOffSelector: i32 = 35;
const kStylisticAltSeventeenOnSelector: i32 = 34;
const kStylisticAltSixOffSelector: i32 = 13;
const kStylisticAltSixOnSelector: i32 = 12;
const kStylisticAltSixteenOffSelector: i32 = 33;
const kStylisticAltSixteenOnSelector: i32 = 32;
const kStylisticAltTenOffSelector: i32 = 21;
const kStylisticAltTenOnSelector: i32 = 20;
const kStylisticAltThirteenOffSelector: i32 = 27;
const kStylisticAltThirteenOnSelector: i32 = 26;
const kStylisticAltThreeOffSelector: i32 = 7;
const kStylisticAltThreeOnSelector: i32 = 6;
const kStylisticAltTwelveOffSelector: i32 = 25;
const kStylisticAltTwelveOnSelector: i32 = 24;
const kStylisticAltTwentyOffSelector: i32 = 41;
const kStylisticAltTwentyOnSelector: i32 = 40;
const kStylisticAltTwoOffSelector: i32 = 5;
const kStylisticAltTwoOnSelector: i32 = 4;
const kSuperiorsSelector: i32 = 1;
const kSwashAlternatesOffSelector: i32 = 3;
const kSwashAlternatesOnSelector: i32 = 2;
const kTitlingCapsSelector: i32 = 4;
const kTypographicExtrasType: i32 = 14;
const kVerticalFractionsSelector: i32 = 1;
const kVerticalPositionType: i32 = 10;
pub fn apply_features(font: &mut Font, features: &Features) {
// See https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/third_party/harfbuzz-ng/src/hb-coretext.cc
// for a reference implementation.
toggle_open_type_feature(
font,
features.calt,
kContextualAlternatesType,
kContextualAlternatesOnSelector,
kContextualAlternatesOffSelector,
);
toggle_open_type_feature(
font,
features.case,
kCaseSensitiveLayoutType,
kCaseSensitiveLayoutOnSelector,
kCaseSensitiveLayoutOffSelector,
);
toggle_open_type_feature(
font,
features.cpsp,
kCaseSensitiveLayoutType,
kCaseSensitiveSpacingOnSelector,
kCaseSensitiveSpacingOffSelector,
);
toggle_open_type_feature(
font,
features.frac,
kFractionsType,
kDiagonalFractionsSelector,
kNoFractionsSelector,
);
toggle_open_type_feature(
font,
features.liga,
kLigaturesType,
kCommonLigaturesOnSelector,
kCommonLigaturesOffSelector,
);
toggle_open_type_feature(
font,
features.onum,
kNumberCaseType,
kLowerCaseNumbersSelector,
2,
);
toggle_open_type_feature(
font,
features.ordn,
kVerticalPositionType,
kOrdinalsSelector,
kNormalPositionSelector,
);
toggle_open_type_feature(
font,
features.pnum,
kNumberSpacingType,
kProportionalNumbersSelector,
4,
);
toggle_open_type_feature(
font,
features.ss01,
kStylisticAlternativesType,
kStylisticAltOneOnSelector,
kStylisticAltOneOffSelector,
);
toggle_open_type_feature(
font,
features.ss02,
kStylisticAlternativesType,
kStylisticAltTwoOnSelector,
kStylisticAltTwoOffSelector,
);
toggle_open_type_feature(
font,
features.ss03,
kStylisticAlternativesType,
kStylisticAltThreeOnSelector,
kStylisticAltThreeOffSelector,
);
toggle_open_type_feature(
font,
features.ss04,
kStylisticAlternativesType,
kStylisticAltFourOnSelector,
kStylisticAltFourOffSelector,
);
toggle_open_type_feature(
font,
features.ss05,
kStylisticAlternativesType,
kStylisticAltFiveOnSelector,
kStylisticAltFiveOffSelector,
);
toggle_open_type_feature(
font,
features.ss06,
kStylisticAlternativesType,
kStylisticAltSixOnSelector,
kStylisticAltSixOffSelector,
);
toggle_open_type_feature(
font,
features.ss07,
kStylisticAlternativesType,
kStylisticAltSevenOnSelector,
kStylisticAltSevenOffSelector,
);
toggle_open_type_feature(
font,
features.ss08,
kStylisticAlternativesType,
kStylisticAltEightOnSelector,
kStylisticAltEightOffSelector,
);
toggle_open_type_feature(
font,
features.ss09,
kStylisticAlternativesType,
kStylisticAltNineOnSelector,
kStylisticAltNineOffSelector,
);
toggle_open_type_feature(
font,
features.ss10,
kStylisticAlternativesType,
kStylisticAltTenOnSelector,
kStylisticAltTenOffSelector,
);
toggle_open_type_feature(
font,
features.ss11,
kStylisticAlternativesType,
kStylisticAltElevenOnSelector,
kStylisticAltElevenOffSelector,
);
toggle_open_type_feature(
font,
features.ss12,
kStylisticAlternativesType,
kStylisticAltTwelveOnSelector,
kStylisticAltTwelveOffSelector,
);
toggle_open_type_feature(
font,
features.ss13,
kStylisticAlternativesType,
kStylisticAltThirteenOnSelector,
kStylisticAltThirteenOffSelector,
);
toggle_open_type_feature(
font,
features.ss14,
kStylisticAlternativesType,
kStylisticAltFourteenOnSelector,
kStylisticAltFourteenOffSelector,
);
toggle_open_type_feature(
font,
features.ss15,
kStylisticAlternativesType,
kStylisticAltFifteenOnSelector,
kStylisticAltFifteenOffSelector,
);
toggle_open_type_feature(
font,
features.ss16,
kStylisticAlternativesType,
kStylisticAltSixteenOnSelector,
kStylisticAltSixteenOffSelector,
);
toggle_open_type_feature(
font,
features.ss17,
kStylisticAlternativesType,
kStylisticAltSeventeenOnSelector,
kStylisticAltSeventeenOffSelector,
);
toggle_open_type_feature(
font,
features.ss18,
kStylisticAlternativesType,
kStylisticAltEighteenOnSelector,
kStylisticAltEighteenOffSelector,
);
toggle_open_type_feature(
font,
features.ss19,
kStylisticAlternativesType,
kStylisticAltNineteenOnSelector,
kStylisticAltNineteenOffSelector,
);
toggle_open_type_feature(
font,
features.ss20,
kStylisticAlternativesType,
kStylisticAltTwentyOnSelector,
kStylisticAltTwentyOffSelector,
);
toggle_open_type_feature(
font,
features.subs,
kVerticalPositionType,
kInferiorsSelector,
kNormalPositionSelector,
);
toggle_open_type_feature(
font,
features.sups,
kVerticalPositionType,
kSuperiorsSelector,
kNormalPositionSelector,
);
toggle_open_type_feature(
font,
features.swsh,
kContextualAlternatesType,
kSwashAlternatesOnSelector,
kSwashAlternatesOffSelector,
);
toggle_open_type_feature(
font,
features.titl,
kStyleOptionsType,
kTitlingCapsSelector,
kNoStyleOptionsSelector,
);
toggle_open_type_feature(
font,
features.tnum,
kNumberSpacingType,
kMonospacedNumbersSelector,
4,
);
toggle_open_type_feature(
font,
features.zero,
kTypographicExtrasType,
kSlashedZeroOnSelector,
kSlashedZeroOffSelector,
);
}
fn toggle_open_type_feature(
font: &mut Font,
enabled: Option<bool>,
type_identifier: i32,
on_selector_identifier: i32,
off_selector_identifier: i32,
) {
if let Some(enabled) = enabled {
let native_font = font.native_font();
unsafe {
let selector_identifier = if enabled {
on_selector_identifier
} else {
off_selector_identifier
};
let new_descriptor = CTFontDescriptorCreateCopyWithFeature(
native_font.copy_descriptor().as_concrete_TypeRef(),
CFNumber::from(type_identifier).as_concrete_TypeRef(),
CFNumber::from(selector_identifier).as_concrete_TypeRef(),
);
let new_descriptor = CTFontDescriptor::wrap_under_create_rule(new_descriptor);
let new_font = CTFontCreateCopyWithAttributes(
font.native_font().as_concrete_TypeRef(),
0.0,
ptr::null(),
new_descriptor.as_concrete_TypeRef(),
);
let new_font = CTFont::wrap_under_create_rule(new_font);
*font = Font::from_native_font(new_font);
}
}
}
#[link(name = "CoreText", kind = "framework")]
extern "C" {
fn CTFontCreateCopyWithAttributes(
font: CTFontRef,
size: CGFloat,
matrix: *const CGAffineTransform,
attributes: CTFontDescriptorRef,
) -> CTFontRef;
}

View File

@@ -1,7 +1,6 @@
use cocoa::{
appkit::NSWindow,
base::id,
foundation::{NSPoint, NSRect, NSSize},
foundation::{NSPoint, NSRect},
};
use objc::{msg_send, sel, sel_impl};
use pathfinder_geometry::{
@@ -25,61 +24,15 @@ impl Vector2FExt for Vector2F {
}
}
pub trait RectFExt {
/// Converts self to an NSRect with y axis pointing up.
/// The resulting NSRect will have an origin at the bottom left of the rectangle.
/// Also takes care of converting from window scaled coordinates to screen coordinates
fn to_screen_ns_rect(&self, native_window: id) -> NSRect;
/// Converts self to an NSRect with y axis point up.
/// The resulting NSRect will have an origin at the bottom left of the rectangle.
/// Unlike to_screen_ns_rect, coordinates are not converted and are assumed to already be in screen scale
fn to_ns_rect(&self) -> NSRect;
}
impl RectFExt for RectF {
fn to_screen_ns_rect(&self, native_window: id) -> NSRect {
unsafe { native_window.convertRectToScreen_(self.to_ns_rect()) }
}
fn to_ns_rect(&self) -> NSRect {
NSRect::new(
NSPoint::new(
self.origin_x() as f64,
-(self.origin_y() + self.height()) as f64,
),
NSSize::new(self.width() as f64, self.height() as f64),
)
}
}
pub trait NSRectExt {
/// Converts self to a RectF with y axis pointing down.
/// The resulting RectF will have an origin at the top left of the rectangle.
/// Also takes care of converting from screen scale coordinates to window coordinates
fn to_window_rectf(&self, native_window: id) -> RectF;
/// Converts self to a RectF with y axis pointing down.
/// The resulting RectF will have an origin at the top left of the rectangle.
/// Unlike to_screen_ns_rect, coordinates are not converted and are assumed to already be in screen scale
fn to_rectf(&self) -> RectF;
fn intersects(&self, other: Self) -> bool;
}
impl NSRectExt for NSRect {
fn to_window_rectf(&self, native_window: id) -> RectF {
unsafe {
self.origin.x;
let rect: NSRect = native_window.convertRectFromScreen_(*self);
rect.to_rectf()
}
}
impl NSRectExt for NSRect {
fn to_rectf(&self) -> RectF {
RectF::new(
vec2f(
self.origin.x as f32,
-(self.origin.y + self.size.height) as f32,
),
vec2f(self.origin.x as f32, self.origin.y as f32),
vec2f(self.size.width as f32, self.size.height as f32),
)
}

View File

@@ -867,25 +867,38 @@ impl platform::Platform for MacPlatform {
}
fn restart(&self) {
#[cfg(debug_assertions)]
let path = std::env::current_exe().unwrap();
use std::os::unix::process::CommandExt as _;
#[cfg(not(debug_assertions))]
let path = self
let app_pid = std::process::id().to_string();
let app_path = self
.app_path()
.unwrap_or_else(|_| std::env::current_exe().unwrap());
.ok()
// When the app is not bundled, `app_path` returns the
// directory containing the executable. Disregard this
// and get the path to the executable itself.
.and_then(|path| (path.extension()?.to_str()? == "app").then_some(path))
.unwrap_or_else(|| std::env::current_exe().unwrap());
let script = r#"lsof -p "$0" +r 1 &>/dev/null && open "$1""#;
// Wait until this process has exited and then re-open this path.
let script = r#"
while kill -0 $0 2> /dev/null; do
sleep 0.1
done
open "$1"
"#;
Command::new("/bin/bash")
let restart_process = Command::new("/bin/bash")
.arg("-c")
.arg(script)
.arg(std::process::id().to_string())
.arg(path)
.spawn()
.ok();
.arg(app_pid)
.arg(app_path)
.process_group(0)
.spawn();
self.quit();
match restart_process {
Ok(_) => self.quit(),
Err(e) => log::error!("failed to spawn restart script: {:?}", e),
}
}
}

View File

@@ -8,7 +8,7 @@ use crate::{
mac::platform::NSViewLayerContentsRedrawDuringViewResize,
platform::{
self,
mac::{geometry::RectFExt, renderer::Renderer, screen::Screen},
mac::{renderer::Renderer, screen::Screen},
Event, WindowBounds,
},
InputHandler, KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
@@ -372,7 +372,8 @@ impl WindowState {
}
let window_frame = self.frame();
if window_frame == self.native_window.screen().visibleFrame().to_rectf() {
let screen_frame = self.native_window.screen().visibleFrame().to_rectf();
if window_frame.size() == screen_frame.size() {
WindowBounds::Maximized
} else {
WindowBounds::Fixed(window_frame)
@@ -383,8 +384,19 @@ impl WindowState {
// Returns the window bounds in window coordinates
fn frame(&self) -> RectF {
unsafe {
let ns_frame = NSWindow::frame(self.native_window);
ns_frame.to_rectf()
let screen_frame = self.native_window.screen().visibleFrame();
let window_frame = NSWindow::frame(self.native_window);
RectF::new(
vec2f(
window_frame.origin.x as f32,
(screen_frame.size.height - window_frame.origin.y - window_frame.size.height)
as f32,
),
vec2f(
window_frame.size.width as f32,
window_frame.size.height as f32,
),
)
}
}
@@ -472,7 +484,16 @@ impl Window {
}
WindowBounds::Fixed(rect) => {
let screen_frame = screen.visibleFrame();
let ns_rect = rect.to_ns_rect();
let ns_rect = NSRect::new(
NSPoint::new(
rect.origin_x() as f64,
screen_frame.size.height
- rect.origin_y() as f64
- rect.height() as f64,
),
NSSize::new(rect.width() as f64, rect.height() as f64),
);
if ns_rect.intersects(screen_frame) {
native_window.setFrame_display_(ns_rect, YES);
} else {

View File

@@ -663,7 +663,9 @@ mod tests {
fn test_wrap_line(cx: &mut crate::MutableAppContext) {
let font_cache = cx.font_cache().clone();
let font_system = cx.platform().fonts();
let family = font_cache.load_family(&["Courier"]).unwrap();
let family = font_cache
.load_family(&["Courier"], &Default::default())
.unwrap();
let font_id = font_cache.select_font(family, &Default::default()).unwrap();
let mut wrapper = LineWrapper::new(font_id, 16., font_system);
@@ -725,7 +727,9 @@ mod tests {
let font_system = cx.platform().fonts();
let text_layout_cache = TextLayoutCache::new(font_system.clone());
let family = font_cache.load_family(&["Helvetica"]).unwrap();
let family = font_cache
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = font_cache.select_font(family, &Default::default()).unwrap();
let normal = RunStyle {
font_id,

View File

@@ -43,11 +43,12 @@ futures = "0.3"
lazy_static = "1.4"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] }
postage = { workspace = true }
rand = { version = "0.8.3", optional = true }
regex = "1.5"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = { version = "1", features = ["preserve_order"] }
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
similar = "1.3"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"

View File

@@ -1366,6 +1366,7 @@ impl Buffer {
where
T: Into<Arc<str>>,
{
self.autoindent_requests.clear();
self.edit([(0..self.len(), text)], None, cx)
}

View File

@@ -809,7 +809,6 @@ fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(
}"}],
);
eprintln!("-----------------------");
// Regression test: even though the parent node of the parentheses (the for loop) does
// intersect the given range, the parentheses themselves do not contain the range, so
// they should not be returned. Only the curly braces contain the range.

View File

@@ -59,7 +59,6 @@ impl HighlightId {
theme.highlights.get(self.0 as usize).map(|entry| entry.1)
}
#[cfg(any(test, feature = "test-support"))]
pub fn name<'a>(&self, theme: &'a SyntaxTheme) -> Option<&'a str> {
theme.highlights.get(self.0 as usize).map(|e| e.0.as_str())
}

View File

@@ -10,7 +10,6 @@ mod buffer_tests;
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use client::http::HttpClient;
use collections::HashMap;
use futures::{
channel::oneshot,
@@ -20,6 +19,7 @@ use futures::{
use gpui::{executor::Background, MutableAppContext, Task};
use highlight_map::HighlightMap;
use lazy_static::lazy_static;
use lsp::CodeActionKind;
use parking_lot::{Mutex, RwLock};
use postage::watch;
use regex::Regex;
@@ -29,6 +29,7 @@ use std::{
any::Any,
borrow::Cow,
cell::RefCell,
ffi::OsString,
fmt::Debug,
hash::Hash,
mem,
@@ -44,7 +45,8 @@ use syntax_map::SyntaxSnapshot;
use theme::{SyntaxTheme, Theme};
use tree_sitter::{self, Query};
use unicase::UniCase;
use util::{ResultExt, TryFutureExt as _, UnwrapFuture};
use util::http::HttpClient;
use util::{merge_json_value_into, post_inc, ResultExt, TryFutureExt as _, UnwrapFuture};
#[cfg(any(test, feature = "test-support"))]
use futures::channel::mpsc;
@@ -77,23 +79,27 @@ pub trait ToLspPosition {
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct LanguageServerName(pub Arc<str>);
#[derive(Debug, Clone, Deserialize)]
pub struct LanguageServerBinary {
pub path: PathBuf,
pub arguments: Vec<OsString>,
}
/// Represents a Language Server, with certain cached sync properties.
/// Uses [`LspAdapter`] under the hood, but calls all 'static' methods
/// once at startup, and caches the results.
pub struct CachedLspAdapter {
pub name: LanguageServerName,
pub server_args: Vec<String>,
pub initialization_options: Option<Value>,
pub disk_based_diagnostic_sources: Vec<String>,
pub disk_based_diagnostics_progress_token: Option<String>,
pub language_ids: HashMap<String, String>,
pub adapter: Box<dyn LspAdapter>,
pub adapter: Arc<dyn LspAdapter>,
}
impl CachedLspAdapter {
pub async fn new(adapter: Box<dyn LspAdapter>) -> Arc<Self> {
pub async fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> {
let name = adapter.name().await;
let server_args = adapter.server_args().await;
let initialization_options = adapter.initialization_options().await;
let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources().await;
let disk_based_diagnostics_progress_token =
@@ -102,7 +108,6 @@ impl CachedLspAdapter {
Arc::new(CachedLspAdapter {
name,
server_args,
initialization_options,
disk_based_diagnostic_sources,
disk_based_diagnostics_progress_token,
@@ -123,16 +128,30 @@ impl CachedLspAdapter {
version: Box<dyn 'static + Send + Any>,
http: Arc<dyn HttpClient>,
container_dir: PathBuf,
) -> Result<PathBuf> {
) -> Result<LanguageServerBinary> {
self.adapter
.fetch_server_binary(version, http, container_dir)
.await
}
pub async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
pub async fn cached_server_binary(
&self,
container_dir: PathBuf,
) -> Option<LanguageServerBinary> {
self.adapter.cached_server_binary(container_dir).await
}
pub fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
self.adapter.code_action_kinds()
}
pub fn workspace_configuration(
&self,
cx: &mut MutableAppContext,
) -> Option<BoxFuture<'static, Value>> {
self.adapter.workspace_configuration(cx)
}
pub async fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
self.adapter.process_diagnostics(params).await
}
@@ -175,9 +194,9 @@ pub trait LspAdapter: 'static + Send + Sync {
version: Box<dyn 'static + Send + Any>,
http: Arc<dyn HttpClient>,
container_dir: PathBuf,
) -> Result<PathBuf>;
) -> Result<LanguageServerBinary>;
async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf>;
async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary>;
async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
@@ -200,14 +219,27 @@ pub trait LspAdapter: 'static + Send + Sync {
None
}
async fn server_args(&self) -> Vec<String> {
Vec::new()
}
async fn initialization_options(&self) -> Option<Value> {
None
}
fn workspace_configuration(
&self,
_: &mut MutableAppContext,
) -> Option<BoxFuture<'static, Value>> {
None
}
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
Some(vec![
CodeActionKind::EMPTY,
CodeActionKind::QUICKFIX,
CodeActionKind::REFACTOR,
CodeActionKind::REFACTOR_EXTRACT,
CodeActionKind::SOURCE,
])
}
async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
Default::default()
}
@@ -228,7 +260,7 @@ pub struct CodeLabel {
pub filter_range: Range<usize>,
}
#[derive(Deserialize)]
#[derive(Clone, Deserialize)]
pub struct LanguageConfig {
pub name: Arc<str>,
pub path_suffixes: Vec<String>,
@@ -265,7 +297,7 @@ pub struct LanguageScope {
override_id: Option<u32>,
}
#[derive(Deserialize, Default, Debug)]
#[derive(Clone, Deserialize, Default, Debug)]
pub struct LanguageConfigOverride {
#[serde(default)]
pub line_comment: Override<Arc<str>>,
@@ -275,7 +307,7 @@ pub struct LanguageConfigOverride {
pub disabled_bracket_ixs: Vec<u16>,
}
#[derive(Deserialize, Debug)]
#[derive(Clone, Deserialize, Debug)]
#[serde(untagged)]
pub enum Override<T> {
Remove { remove: bool },
@@ -452,17 +484,20 @@ pub enum LanguageServerBinaryStatus {
Failed { error: String },
}
type AvailableLanguageId = usize;
#[derive(Clone)]
struct AvailableLanguage {
id: AvailableLanguageId,
path: &'static str,
config: LanguageConfig,
grammar: tree_sitter::Language,
lsp_adapter: Option<Box<dyn LspAdapter>>,
lsp_adapter: Option<Arc<dyn LspAdapter>>,
get_queries: fn(&str) -> LanguageQueries,
}
pub struct LanguageRegistry {
languages: RwLock<Vec<Arc<Language>>>,
available_languages: RwLock<Vec<AvailableLanguage>>,
state: RwLock<LanguageRegistryState>,
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)>,
@@ -471,29 +506,40 @@ pub struct LanguageRegistry {
lsp_binary_paths: Mutex<
HashMap<
LanguageServerName,
Shared<BoxFuture<'static, Result<PathBuf, Arc<anyhow::Error>>>>,
Shared<BoxFuture<'static, Result<LanguageServerBinary, Arc<anyhow::Error>>>>,
>,
>,
subscription: RwLock<(watch::Sender<()>, watch::Receiver<()>)>,
theme: RwLock<Option<Arc<Theme>>>,
executor: Option<Arc<Background>>,
version: AtomicUsize,
}
struct LanguageRegistryState {
languages: Vec<Arc<Language>>,
available_languages: Vec<AvailableLanguage>,
next_available_language_id: AvailableLanguageId,
loading_languages: HashMap<AvailableLanguageId, Vec<oneshot::Sender<Result<Arc<Language>>>>>,
subscription: (watch::Sender<()>, watch::Receiver<()>),
theme: Option<Arc<Theme>>,
version: usize,
}
impl LanguageRegistry {
pub fn new(login_shell_env_loaded: Task<()>) -> Self {
let (lsp_binary_statuses_tx, lsp_binary_statuses_rx) = async_broadcast::broadcast(16);
Self {
state: RwLock::new(LanguageRegistryState {
languages: vec![PLAIN_TEXT.clone()],
available_languages: Default::default(),
next_available_language_id: 0,
loading_languages: Default::default(),
subscription: watch::channel(),
theme: Default::default(),
version: 0,
}),
language_server_download_dir: None,
languages: RwLock::new(vec![PLAIN_TEXT.clone()]),
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,
}
}
@@ -512,10 +558,12 @@ impl LanguageRegistry {
path: &'static str,
config: LanguageConfig,
grammar: tree_sitter::Language,
lsp_adapter: Option<Box<dyn LspAdapter>>,
lsp_adapter: Option<Arc<dyn LspAdapter>>,
get_queries: fn(&str) -> LanguageQueries,
) {
self.available_languages.write().push(AvailableLanguage {
let state = &mut *self.state.write();
state.available_languages.push(AvailableLanguage {
id: post_inc(&mut state.next_available_language_id),
path,
config,
grammar,
@@ -525,42 +573,66 @@ impl LanguageRegistry {
}
pub fn language_names(&self) -> Vec<String> {
let mut result = self
let state = self.state.read();
let mut result = state
.available_languages
.read()
.iter()
.map(|l| l.config.name.to_string())
.chain(
self.languages
.read()
.iter()
.map(|l| l.config.name.to_string()),
)
.chain(state.languages.iter().map(|l| l.config.name.to_string()))
.collect::<Vec<_>>();
result.sort_unstable();
result.sort_unstable_by_key(|language_name| language_name.to_lowercase());
result
}
pub fn add(&self, language: Arc<Language>) {
if let Some(theme) = self.theme.read().clone() {
language.set_theme(&theme.editor.syntax);
pub fn workspace_configuration(&self, cx: &mut MutableAppContext) -> Task<serde_json::Value> {
let lsp_adapters = {
let state = self.state.read();
state
.available_languages
.iter()
.filter_map(|l| l.lsp_adapter.clone())
.chain(
state
.languages
.iter()
.filter_map(|l| l.adapter.as_ref().map(|a| a.adapter.clone())),
)
.collect::<Vec<_>>()
};
let mut language_configs = Vec::new();
for adapter in &lsp_adapters {
if let Some(language_config) = adapter.workspace_configuration(cx) {
language_configs.push(language_config);
}
}
self.languages.write().push(language);
self.version.fetch_add(1, SeqCst);
*self.subscription.write().0.borrow_mut() = ();
cx.background().spawn(async move {
let mut config = serde_json::json!({});
let language_configs = futures::future::join_all(language_configs).await;
for language_config in language_configs {
merge_json_value_into(language_config, &mut config);
}
config
})
}
pub fn add(&self, language: Arc<Language>) {
self.state.write().add(language);
}
pub fn subscribe(&self) -> watch::Receiver<()> {
self.subscription.read().1.clone()
self.state.read().subscription.1.clone()
}
pub fn version(&self) -> usize {
self.version.load(SeqCst)
self.state.read().version
}
pub fn set_theme(&self, theme: Arc<Theme>) {
*self.theme.write() = Some(theme.clone());
for language in self.languages.read().iter() {
let mut state = self.state.write();
state.theme = Some(theme.clone());
for language in &state.languages {
language.set_theme(&theme.editor.syntax);
}
}
@@ -613,43 +685,70 @@ impl LanguageRegistry {
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
let (tx, rx) = oneshot::channel();
if let Some(language) = self
let mut state = self.state.write();
if let Some(language) = state
.languages
.read()
.iter()
.find(|language| callback(&language.config))
{
let _ = tx.send(Ok(language.clone()));
} else if let Some(executor) = self.executor.clone() {
let mut available_languages = self.available_languages.write();
if let Some(language) = state
.available_languages
.iter()
.find(|l| callback(&l.config))
.cloned()
{
let txs = state
.loading_languages
.entry(language.id)
.or_insert_with(|| {
let this = self.clone();
executor
.spawn(async move {
let id = language.id;
let queries = (language.get_queries)(&language.path);
let language =
Language::new(language.config, Some(language.grammar))
.with_lsp_adapter(language.lsp_adapter)
.await;
let name = language.name();
match language.with_queries(queries) {
Ok(language) => {
let language = Arc::new(language);
let mut state = this.state.write();
state.add(language.clone());
state
.available_languages
.retain(|language| language.id != id);
if let Some(mut txs) = state.loading_languages.remove(&id) {
for tx in txs.drain(..) {
let _ = tx.send(Ok(language.clone()));
}
}
}
Err(err) => {
let mut state = this.state.write();
state
.available_languages
.retain(|language| language.id != id);
if let Some(mut txs) = state.loading_languages.remove(&id) {
for tx in txs.drain(..) {
let _ = tx.send(Err(anyhow!(
"failed to load language {}: {}",
name,
err
)));
}
}
}
};
})
.detach();
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) => {
let language = Arc::new(language);
this.add(language.clone());
let _ = tx.send(Ok(language));
}
Err(err) => {
let _ = tx.send(Err(anyhow!(
"failed to load language {}: {}",
name,
err
)));
}
};
})
.detach();
Vec::new()
});
txs.push(tx);
} else {
let _ = tx.send(Err(anyhow!("language not found")));
}
@@ -661,7 +760,7 @@ impl LanguageRegistry {
}
pub fn to_vec(&self) -> Vec<Arc<Language>> {
self.languages.read().iter().cloned().collect()
self.state.read().languages.iter().cloned().collect()
}
pub fn start_language_server(
@@ -713,14 +812,15 @@ impl LanguageRegistry {
let adapter = language.adapter.clone()?;
let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone();
let login_shell_env_loaded = self.login_shell_env_loaded.clone();
Some(cx.spawn(|cx| async move {
login_shell_env_loaded.await;
let server_binary_path = this
.lsp_binary_paths
.lock()
let mut lock = this.lsp_binary_paths.lock();
let entry = lock
.entry(adapter.name.clone())
.or_insert_with(|| {
get_server_binary_path(
get_binary(
adapter.clone(),
language.clone(),
http_client,
@@ -731,18 +831,19 @@ impl LanguageRegistry {
.boxed()
.shared()
})
.clone()
.map_err(|e| anyhow!(e));
.clone();
drop(lock);
let binary = entry.clone().map_err(|e| anyhow!(e)).await?;
let server_binary_path = server_binary_path.await?;
let server_args = &adapter.server_args;
let server = lsp::LanguageServer::new(
server_id,
&server_binary_path,
server_args,
&binary.path,
&binary.arguments,
&root_path,
adapter.code_action_kinds(),
cx,
)?;
Ok(server)
}))
}
@@ -754,6 +855,17 @@ impl LanguageRegistry {
}
}
impl LanguageRegistryState {
fn add(&mut self, language: Arc<Language>) {
if let Some(theme) = self.theme.as_ref() {
language.set_theme(&theme.editor.syntax);
}
self.languages.push(language);
self.version += 1;
*self.subscription.0.borrow_mut() = ();
}
}
#[cfg(any(test, feature = "test-support"))]
impl Default for LanguageRegistry {
fn default() -> Self {
@@ -761,13 +873,13 @@ impl Default for LanguageRegistry {
}
}
async fn get_server_binary_path(
async fn get_binary(
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
http_client: Arc<dyn HttpClient>,
download_dir: Arc<Path>,
statuses: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
) -> Result<PathBuf> {
) -> Result<LanguageServerBinary> {
let container_dir = download_dir.join(adapter.name.0.as_ref());
if !container_dir.exists() {
smol::fs::create_dir_all(&container_dir)
@@ -775,7 +887,7 @@ async fn get_server_binary_path(
.context("failed to create container directory")?;
}
let path = fetch_latest_server_binary_path(
let binary = fetch_latest_binary(
adapter.clone(),
language.clone(),
http_client,
@@ -783,12 +895,13 @@ async fn get_server_binary_path(
statuses.clone(),
)
.await;
if let Err(error) = path.as_ref() {
if let Some(cached_path) = adapter.cached_server_binary(container_dir).await {
if let Err(error) = binary.as_ref() {
if let Some(cached) = adapter.cached_server_binary(container_dir).await {
statuses
.broadcast((language.clone(), LanguageServerBinaryStatus::Cached))
.await?;
return Ok(cached_path);
return Ok(cached);
} else {
statuses
.broadcast((
@@ -800,16 +913,16 @@ async fn get_server_binary_path(
.await?;
}
}
path
binary
}
async fn fetch_latest_server_binary_path(
async fn fetch_latest_binary(
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
http_client: Arc<dyn HttpClient>,
container_dir: &Path,
lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
) -> Result<PathBuf> {
) -> Result<LanguageServerBinary> {
let container_dir: Arc<Path> = container_dir.into();
lsp_binary_statuses_tx
.broadcast((
@@ -823,13 +936,13 @@ async fn fetch_latest_server_binary_path(
lsp_binary_statuses_tx
.broadcast((language.clone(), LanguageServerBinaryStatus::Downloading))
.await?;
let path = adapter
let binary = adapter
.fetch_server_binary(version_info, http_client, container_dir.to_path_buf())
.await?;
lsp_binary_statuses_tx
.broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded))
.await?;
Ok(path)
Ok(binary)
}
impl Language {
@@ -1085,7 +1198,7 @@ impl Language {
Arc::get_mut(self.grammar.as_mut().unwrap()).unwrap()
}
pub async fn with_lsp_adapter(mut self, lsp_adapter: Option<Box<dyn LspAdapter>>) -> Self {
pub async fn with_lsp_adapter(mut self, lsp_adapter: Option<Arc<dyn LspAdapter>>) -> Self {
if let Some(adapter) = lsp_adapter {
self.adapter = Some(CachedLspAdapter::new(adapter).await);
}
@@ -1099,7 +1212,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(Box::new(fake_lsp_adapter)).await;
let adapter = CachedLspAdapter::new(Arc::new(fake_lsp_adapter)).await;
self.adapter = Some(adapter);
servers_rx
}
@@ -1362,11 +1475,11 @@ impl LspAdapter for Arc<FakeLspAdapter> {
_: Box<dyn 'static + Send + Any>,
_: Arc<dyn HttpClient>,
_: PathBuf,
) -> Result<PathBuf> {
) -> Result<LanguageServerBinary> {
unreachable!();
}
async fn cached_server_binary(&self, _: PathBuf) -> Option<PathBuf> {
async fn cached_server_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
unreachable!();
}
@@ -1415,3 +1528,76 @@ pub fn range_from_lsp(range: lsp::Range) -> Range<Unclipped<PointUtf16>> {
}
start..end
}
#[cfg(test)]
mod tests {
use gpui::TestAppContext;
use super::*;
#[gpui::test(iterations = 10)]
async fn test_language_loading(cx: &mut TestAppContext) {
let mut languages = LanguageRegistry::test();
languages.set_executor(cx.background());
let languages = Arc::new(languages);
languages.register(
"/JSON",
LanguageConfig {
name: "JSON".into(),
path_suffixes: vec!["json".into()],
..Default::default()
},
tree_sitter_json::language(),
None,
|_| Default::default(),
);
languages.register(
"/rust",
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".into()],
..Default::default()
},
tree_sitter_rust::language(),
None,
|_| Default::default(),
);
assert_eq!(
languages.language_names(),
&[
"JSON".to_string(),
"Plain Text".to_string(),
"Rust".to_string(),
]
);
let rust1 = languages.language_for_name("Rust");
let rust2 = languages.language_for_name("Rust");
// Ensure language is still listed even if it's being loaded.
assert_eq!(
languages.language_names(),
&[
"JSON".to_string(),
"Plain Text".to_string(),
"Rust".to_string(),
]
);
let (rust1, rust2) = futures::join!(rust1, rust2);
assert!(Arc::ptr_eq(&rust1.unwrap(), &rust2.unwrap()));
// Ensure language is still listed even after loading it.
assert_eq!(
languages.language_names(),
&[
"JSON".to_string(),
"Plain Text".to_string(),
"Rust".to_string(),
]
);
// Loading an unknown language returns an error.
assert!(languages.language_for_name("Unknown").await.is_err());
}
}

View File

@@ -8,7 +8,7 @@ use std::sync::Arc;
use workspace::{item::ItemHandle, StatusItemView};
pub struct ActiveBufferLanguage {
active_language: Option<Arc<str>>,
active_language: Option<Option<Arc<str>>>,
_observe_active_editor: Option<Subscription>,
}
@@ -27,12 +27,12 @@ impl ActiveBufferLanguage {
}
fn update_language(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
self.active_language.take();
self.active_language = Some(None);
let editor = editor.read(cx);
if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
if let Some(language) = buffer.read(cx).language() {
self.active_language = Some(language.name());
self.active_language = Some(Some(language.name()));
}
}
@@ -51,10 +51,16 @@ impl View for ActiveBufferLanguage {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
if let Some(active_language) = self.active_language.as_ref() {
let active_language_text = if let Some(active_language_text) = active_language {
active_language_text.to_string()
} else {
"Unknown".to_string()
};
MouseEventHandler::<Self>::new(0, cx, |state, cx| {
let theme = &cx.global::<Settings>().theme.workspace.status_bar;
let style = theme.active_language.style_for(state, false);
Label::new(active_language.to_string(), style.text.clone())
Label::new(active_language_text, style.text.clone())
.contained()
.with_style(style.container)
.boxed()

View File

@@ -14,7 +14,7 @@ name = "test_app"
[features]
test-support = [
"async-trait",
"async-trait",
"collections/test-support",
"gpui/test-support",
"lazy_static",
@@ -35,7 +35,7 @@ core-graphics = "0.22.3"
futures = "0.3"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] }
postage = { workspace = true }
async-trait = { version = "0.1", optional = true }
lazy_static = { version = "1.4", optional = true }
@@ -62,10 +62,12 @@ jwt = "0.16"
lazy_static = "1.4"
objc = "0.2"
parking_lot = "0.11.1"
serde = { version = "1.0", features = ["derive", "rc"] }
serde = { workspace = true }
serde_derive = { workspace = true }
sha2 = "0.10"
simplelog = "0.9"
[build-dependencies]
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }

View File

@@ -19,7 +19,8 @@ jwt = "0.16"
prost = "0.8"
prost-types = "0.8"
reqwest = "0.11"
serde = { version = "1.0", features = ["derive", "rc"] }
serde = { workspace = true }
serde_derive = { workspace = true }
sha2 = "0.10"
[build-dependencies]

View File

@@ -21,9 +21,10 @@ futures = "0.3"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
lsp-types = "0.91"
parking_lot = "0.11"
postage = { version = "0.4.1", features = ["futures-traits"] }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = { version = "1.0", features = ["raw_value"] }
postage = { workspace = true }
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
smol = "1.2"
[dev-dependencies]

View File

@@ -40,6 +40,7 @@ pub struct LanguageServer {
outbound_tx: channel::Sender<Vec<u8>>,
name: String,
capabilities: ServerCapabilities,
code_action_kinds: Option<Vec<CodeActionKind>>,
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
response_handlers: Arc<Mutex<Option<HashMap<usize, ResponseHandler>>>>,
executor: Arc<executor::Background>,
@@ -108,8 +109,9 @@ impl LanguageServer {
pub fn new<T: AsRef<std::ffi::OsStr>>(
server_id: usize,
binary_path: &Path,
args: &[T],
arguments: &[T],
root_path: &Path,
code_action_kinds: Option<Vec<CodeActionKind>>,
cx: AsyncAppContext,
) -> Result<Self> {
let working_dir = if root_path.is_dir() {
@@ -117,9 +119,10 @@ impl LanguageServer {
} else {
root_path.parent().unwrap_or_else(|| Path::new("/"))
};
let mut server = process::Command::new(binary_path)
.current_dir(working_dir)
.args(args)
.args(arguments)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
@@ -128,13 +131,13 @@ impl LanguageServer {
let stdin = server.stdin.take().unwrap();
let stout = server.stdout.take().unwrap();
let mut server = Self::new_internal(
server_id,
stdin,
stout,
Some(server),
root_path,
code_action_kinds,
cx,
|notification| {
log::info!(
@@ -147,6 +150,7 @@ impl LanguageServer {
);
},
);
if let Some(name) = binary_path.file_name() {
server.name = name.to_string_lossy().to_string();
}
@@ -159,6 +163,7 @@ impl LanguageServer {
stdout: Stdout,
server: Option<Child>,
root_path: &Path,
code_action_kinds: Option<Vec<CodeActionKind>>,
cx: AsyncAppContext,
on_unhandled_notification: F,
) -> Self
@@ -196,6 +201,7 @@ impl LanguageServer {
response_handlers,
name: Default::default(),
capabilities: Default::default(),
code_action_kinds,
next_id: Default::default(),
outbound_tx,
executor: cx.background(),
@@ -206,6 +212,10 @@ impl LanguageServer {
}
}
pub fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
self.code_action_kinds.clone()
}
async fn handle_input<Stdout, F>(
stdout: Stdout,
mut on_unhandled_notification: F,
@@ -319,6 +329,9 @@ impl LanguageServer {
capabilities: ClientCapabilities {
workspace: Some(WorkspaceClientCapabilities {
configuration: Some(true),
did_change_watched_files: Some(DynamicRegistrationClientCapabilities {
dynamic_registration: Some(true),
}),
did_change_configuration: Some(DynamicRegistrationClientCapabilities {
dynamic_registration: Some(true),
}),
@@ -711,6 +724,7 @@ impl LanguageServer {
stdout_reader,
None,
Path::new("/"),
None,
cx.clone(),
|_| {},
);
@@ -721,6 +735,7 @@ impl LanguageServer {
stdin_reader,
None,
Path::new("/"),
None,
cx,
move |msg| {
notifications_tx

View File

@@ -0,0 +1,22 @@
[package]
name = "node_runtime"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/node_runtime.rs"
doctest = false
[dependencies]
gpui = { path = "../gpui" }
util = { path = "../util" }
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
async-tar = "0.4.2"
futures = "0.3"
anyhow = "1.0.38"
parking_lot = "0.11.1"
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
smol = "1.2.5"

View File

@@ -0,0 +1,166 @@
use anyhow::{anyhow, bail, Context, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use futures::{future::Shared, FutureExt};
use gpui::{executor::Background, Task};
use parking_lot::Mutex;
use serde::Deserialize;
use smol::{fs, io::BufReader};
use std::{
env::consts,
path::{Path, PathBuf},
sync::Arc,
};
use util::http::HttpClient;
const VERSION: &str = "v18.15.0";
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct NpmInfo {
#[serde(default)]
dist_tags: NpmInfoDistTags,
versions: Vec<String>,
}
#[derive(Deserialize, Default)]
pub struct NpmInfoDistTags {
latest: Option<String>,
}
pub struct NodeRuntime {
http: Arc<dyn HttpClient>,
background: Arc<Background>,
installation_path: Mutex<Option<Shared<Task<Result<PathBuf, Arc<anyhow::Error>>>>>>,
}
impl NodeRuntime {
pub fn new(http: Arc<dyn HttpClient>, background: Arc<Background>) -> Arc<NodeRuntime> {
Arc::new(NodeRuntime {
http,
background,
installation_path: Mutex::new(None),
})
}
pub async fn binary_path(&self) -> Result<PathBuf> {
let installation_path = self.install_if_needed().await?;
Ok(installation_path.join("bin/node"))
}
pub async fn npm_package_latest_version(&self, name: &str) -> Result<String> {
let installation_path = self.install_if_needed().await?;
let node_binary = installation_path.join("bin/node");
let npm_file = installation_path.join("bin/npm");
let output = smol::process::Command::new(node_binary)
.arg(npm_file)
.args(["-fetch-retry-mintimeout", "2000"])
.args(["-fetch-retry-maxtimeout", "5000"])
.args(["-fetch-timeout", "5000"])
.args(["info", name, "--json"])
.output()
.await
.context("failed to run npm info")?;
if !output.status.success() {
Err(anyhow!(
"failed to execute npm info:\nstdout: {:?}\nstderr: {:?}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
))?;
}
let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
info.dist_tags
.latest
.or_else(|| info.versions.pop())
.ok_or_else(|| anyhow!("no version found for npm package {}", name))
}
pub async fn npm_install_packages(
&self,
packages: impl IntoIterator<Item = (&str, &str)>,
directory: &Path,
) -> Result<()> {
let installation_path = self.install_if_needed().await?;
let node_binary = installation_path.join("bin/node");
let npm_file = installation_path.join("bin/npm");
let output = smol::process::Command::new(node_binary)
.arg(npm_file)
.args(["-fetch-retry-mintimeout", "2000"])
.args(["-fetch-retry-maxtimeout", "5000"])
.args(["-fetch-timeout", "5000"])
.arg("install")
.arg("--prefix")
.arg(directory)
.args(
packages
.into_iter()
.map(|(name, version)| format!("{name}@{version}")),
)
.output()
.await
.context("failed to run npm install")?;
if !output.status.success() {
Err(anyhow!(
"failed to execute npm install:\nstdout: {:?}\nstderr: {:?}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
))?;
}
Ok(())
}
async fn install_if_needed(&self) -> Result<PathBuf> {
let task = self
.installation_path
.lock()
.get_or_insert_with(|| {
let http = self.http.clone();
self.background
.spawn(async move { Self::install(http).await.map_err(Arc::new) })
.shared()
})
.clone();
match task.await {
Ok(path) => Ok(path),
Err(error) => Err(anyhow!("{}", error)),
}
}
async fn install(http: Arc<dyn HttpClient>) -> Result<PathBuf> {
let arch = match consts::ARCH {
"x86_64" => "x64",
"aarch64" => "arm64",
other => bail!("Running on unsupported platform: {other}"),
};
let folder_name = format!("node-{VERSION}-darwin-{arch}");
let node_containing_dir = util::paths::SUPPORT_DIR.join("node");
let node_dir = node_containing_dir.join(folder_name);
let node_binary = node_dir.join("bin/node");
if fs::metadata(&node_binary).await.is_err() {
_ = fs::remove_dir_all(&node_containing_dir).await;
fs::create_dir(&node_containing_dir)
.await
.context("error creating node containing dir")?;
let file_name = format!("node-{VERSION}-darwin-{arch}.tar.gz");
let url = format!("https://nodejs.org/dist/{VERSION}/{file_name}");
let mut response = http
.get(&url, Default::default(), true)
.await
.context("error downloading Node binary tarball")?;
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
let archive = Archive::new(decompressed_bytes);
archive.unpack(&node_containing_dir).await?;
}
anyhow::Ok(node_dir)
}
}

View File

@@ -18,5 +18,5 @@ settings = { path = "../settings" }
text = { path = "../text" }
workspace = { path = "../workspace" }
ordered-float = "2.1.1"
postage = { version = "0.4", features = ["futures-traits"] }
postage = { workspace = true }
smol = "1.2"

View File

@@ -1,21 +0,0 @@
[package]
name = "pando"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/pando.rs"
[features]
test-support = []
[dependencies]
anyhow = "1.0.38"
client = { path = "../client" }
gpui = { path = "../gpui" }
settings = { path = "../settings" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }
sqlez = { path = "../sqlez" }
sqlez_macros = { path = "../sqlez_macros" }

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