Compare commits

...

249 Commits

Author SHA1 Message Date
Max Brunsfeld
d1e4c8dec5 zed 0.87.3 2023-05-19 13:18:43 -07:00
Max Brunsfeld
815b80e295 Remove unnescessary double lookup in repo for (#2492)
Release Notes:

* Optimize repository queries (preview only)
2023-05-19 13:18:12 -07:00
Max Brunsfeld
605213708a Optimize retrieving repos for entries when rendering the project panel (#2493)
This fixes slowness in rendering the project panel due to retrieving the
repository for a given entry.

Release Notes:

* Fixed a lag that would occur when lots of files changed on disk while
the project panel was open (preview only).
2023-05-19 13:17:55 -07:00
Max Brunsfeld
a46e2e82f3 zed 0.87.2 2023-05-19 10:01:24 -07:00
Max Brunsfeld
5e3c5359c5 Fix performance problems in reporting changed FS paths to language servers (#2491)
Fixes
https://linear.app/zed-industries/issue/Z-1611/main-thread-hangs-while-sending-filesystem-change-events-to-lsp

Release Notes:

* Fixed a lag that would sometime occur when large numbers of files
changed on disk, due to reporting the changed files to language servers.
2023-05-19 09:58:37 -07:00
Max Brunsfeld
7da90451d9 Avoid unnecessary code action requests when applying leader updates t… (#2489)
We noticed a huge amount of code actions requests being issued by
followers when applying leader updates. It was caused by a call to
`MultiBuffer::remove_excerpts` with an empty list of excerpts to remove.
This PR fixes that by avoiding emitting spurious events when multibuffer
excerpt manipulation methods are called with empty lists.
2023-05-19 09:45:01 -07:00
Joseph Lyons
e044da3447 zed 0.87.1 2023-05-17 17:52:23 -04:00
Mikayla Maki
5b733542ad Merge pull request #2483 from zed-industries/add-scrollbar-settings
Add scrollbars setting
2023-05-17 17:50:47 -04:00
Mikayla Maki
10a2a592b3 Merge pull request #2482 from zed-industries/add-hunks-to-scrollbar
Add diff hunks to the scroll bar
2023-05-17 15:42:10 -04:00
Joseph Lyons
c93269fd7a collab 0.12.2 2023-05-17 13:36:43 -04:00
Joseph Lyons
f69b94b0a2 v0.87.x preview 2023-05-17 12:38:43 -04:00
Julia
0a0769d4b9 Merge pull request #2479 from zed-industries/dont-use-svg-text-feature
Disable usvg's text feature flags to include less dependency code
2023-05-16 18:48:12 -04:00
Julia
d61b12a05b Disable usvg's text feature flags to include less dependency code 2023-05-16 18:44:16 -04:00
Joseph T. Lyons
362f56d519 Merge pull request #2478 from zed-industries/Fix-telemetry-bugs
Send editor event when saving a new file
2023-05-16 18:18:07 -04:00
Joseph Lyons
c27859871f Send editor event when saving a new file 2023-05-16 18:16:09 -04:00
Joseph T. Lyons
2e27f26339 Merge pull request #2475 from zed-industries/add-copilot-events
Add events for copilot suggestion accepting and discarding
2023-05-16 17:25:54 -04:00
Joseph Lyons
ffd503951b Don't make events for every rejected suggestion 2023-05-16 17:19:05 -04:00
Kirill Bulatov
55950e52c2 Remove extra dbg! 2023-05-16 22:15:56 +03:00
Kirill Bulatov
685f3de796 Merge pull request #2462 from zed-industries/kb/go-to-line-column-numbers
Support go to file_query:row:column syntax in Find File, Go To Line dialogs and CLI

Deals slightly differently with zed-industries/community#557
Deals with zed-industries/community#1184

    Fixes Go To Line not respecting column number when navigating to a place
    Changes a line-row separator from , to : to show it more uniformly with other tools
    Adjusts file finder dialogue to allow file_query:row:column syntax and opens the buffer at the lines given
    Extends CLI with file_path:row_column syntax and opens these files similarly
2023-05-16 21:24:57 +03:00
Kirill Bulatov
5d4fc99750 Unit test file:row:column parsing 2023-05-16 21:07:48 +03:00
Kirill Bulatov
be7a58b508 Finalize the CLI opening part 2023-05-16 21:07:48 +03:00
Kirill Bulatov
0c6f103899 Return proper items on workspace restoration.
co-authored-by: Mikayla <mikayla@zed.dev>
2023-05-16 21:07:48 +03:00
Kirill Bulatov
106064c734 Do not break Zed & Zed CLI compatibility 2023-05-16 21:07:47 +03:00
Kirill Bulatov
628558aa39 Attempt to open rows and columns from CLI input 2023-05-16 21:07:26 +03:00
Kirill Bulatov
d719352152 Unify path:row:column parsing, use it in CLI 2023-05-16 21:07:26 +03:00
Kirill Bulatov
89fe5c6b09 Test caret selection in file finder
co-authored-by: Max <max@zed.dev>
2023-05-16 21:07:26 +03:00
Kirill Bulatov
477bc8da05 Make Go To Line to respect column numbers 2023-05-16 21:07:26 +03:00
Kirill Bulatov
e5bca9c871 Simplify file-row-column parsing 2023-05-16 21:07:26 +03:00
Kirill Bulatov
e9606982e6 Use ':' instead of ',' to separate files, rows and columns 2023-05-16 21:07:26 +03:00
Kirill Bulatov
0db7f4202a Properly place the caret into the window of the file opened
co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-05-16 21:07:26 +03:00
Kirill Bulatov
54c1e77aff Move the caret to the opened file 2023-05-16 21:07:26 +03:00
Kirill Bulatov
3eea2fb5f8 Parse file find queries with extra data 2023-05-16 21:07:26 +03:00
Kirill Bulatov
9de4a1b70f Merge pull request #2476 from zed-industries/kb/faster-dev-cli
Allow CLI to start Zed from local sources
2023-05-16 21:06:40 +03:00
Joseph Lyons
afe75e8cbd Send copilot events even if file_extension is not known at the time 2023-05-16 14:02:36 -04:00
Joseph Lyons
6976d60bfe Rework code to contain submitting of copilot events within editor 2023-05-16 13:26:05 -04:00
Julia
16cab5d021 Merge pull request #2477 from zed-industries/fixup-some-more-worktree-bugs
Fixup some more worktree bugs
2023-05-16 13:13:22 -04:00
Julia
8b63caa0bd Fix worktree refresh request causing gitignore to not update
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-05-16 13:01:29 -04:00
Julia
f50240181a Avoid removing fake fs entry when rename fails later in the process
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-05-16 13:00:39 -04:00
Joseph Lyons
2d4b2e0844 Fix compile error 2023-05-16 11:51:20 -04:00
Kirill Bulatov
903eed964a Allow CLI to start Zed from local sources
Zed now is able to behave as if it's being started from CLI
(`ZED_FORCE_CLI_MODE` env var)

Zed CLI accepts regular binary file path into `-b` parameter (only *.app before),
and tries to start it as Zed editor with `ZED_FORCE_CLI_MODE` env var
and other params needed.
2023-05-16 17:41:32 +03:00
Kirill Bulatov
421db9225a Merge pull request #2470 from zed-industries/kb/fix-project-search-esc
In project search on ESC, reduce multiple carets to one first
2023-05-16 15:16:34 +03:00
Joseph Lyons
a7fc07a8cd Init copilot with client instead of http client 2023-05-16 03:12:39 -04:00
Joseph Lyons
f50afefed3 Subscribe to copilot events (WIP) 2023-05-16 00:35:21 -04:00
Joseph Lyons
a6a2f93607 Update telemetry client to accept copilot events 2023-05-16 00:34:58 -04:00
Mikayla Maki
ead9ac6f23 Fix typo 2023-05-15 16:47:46 -07:00
Mikayla Maki
606d5e36e1 Add events for copilot suggestion accepting and discarding 2023-05-15 16:44:09 -07:00
Mikayla Maki
790223f23a Merge pull request #2473 from zed-industries/fix-styling-feedback
Fix git status issues
2023-05-15 16:28:57 -07:00
Mikayla Maki
e4d509adf4 fmt 2023-05-15 16:22:52 -07:00
Mikayla Maki
4d40aa5d6f Restore trickle up git status to folder
co-authored-by: max <max@zed.dev>
2023-05-15 16:22:00 -07:00
Mikayla Maki
f59256f761 Update git repositories to be streamed with their entries
co-authored-by: max <max@zed.dev>
2023-05-15 16:22:00 -07:00
Mikayla Maki
68078853b7 Made status tracking resilient to folder renames
co-authored-by: max <max@zed.dev>
2023-05-15 16:21:58 -07:00
Mikayla Maki
307dd2b83e Update proto names to reflect new status info 2023-05-15 16:20:01 -07:00
Mikayla Maki
1e4ab6cd75 Add index tracking to status 2023-05-15 16:20:01 -07:00
Mikayla Maki
6c26f3d0e4 Fixed formatting 2023-05-15 16:20:01 -07:00
Mikayla Maki
2b18975cdc Change folder styling from a reduce over all child files to a simple 'always modified'
Remove git status from tab titles
2023-05-15 16:20:01 -07:00
Joseph T. Lyons
17f138906e Merge pull request #2474 from zed-industries/update-jetbrains-keymap
Update jetbrains keymap
2023-05-15 14:17:36 -04:00
Joseph Lyons
6a7d718381 Update jetbrains keymap 2023-05-15 14:12:02 -04:00
Julia
cf53b2ca87 Merge pull request #2471 from zed-industries/optimize-update-local-worktree-buffers
Optimize update local worktree buffers
2023-05-15 13:13:09 -04:00
Kirill Bulatov
18e0ee44a6 Remove redundant scopes and actions to fix the focus toggle on ESC
co-authored-by: Antonio <antonio@zed.dev>
2023-05-15 13:10:15 +03:00
Kirill Bulatov
93705cbe55 Merge pull request #2472 from zed-industries/kb/zed-main-builds
Build Zed for main and labeled PR commits

Add a job to build Zed images marked with the SHA of the commit it was built from.

The job triggers on every commit to main or every PR with run-build-dmg label and produces an install-ready *.dmg artifact attached to the corresponding CI run.
2023-05-15 11:30:26 +03:00
Kirill Bulatov
5465948f20 Build Zed dmg 2023-05-15 11:26:41 +03:00
Kirill Bulatov
4f36ba3b1e Add a job to build Zed images from current main
The job triggers on every commit to `main` or every PR with
`run-build-dmg` label and produces an install-ready *.dmg artifact attached to the
corresponding CI run.
2023-05-14 22:06:33 +03:00
Julia
fa32adecd5 Fixup more, tests finally pass 2023-05-14 12:06:00 -04:00
Mikayla Maki
db87e83bad Merge pull request #2469 from zed-industries/fix-git-init-bug
Fix repository initialization bug
2023-05-13 10:47:55 -07:00
Mikayla Maki
a6a4b846bc fmt 2023-05-13 10:43:16 -07:00
Mikayla Maki
5e2aaf45a0 Fix repository initialization bug 2023-05-13 10:38:24 -07:00
Mikayla Maki
3534665e2b Merge pull request #2468 from zed-industries/touch-up-status
Improve status integration
2023-05-13 09:38:02 -07:00
Mikayla Maki
04041af78b Fixed bug with failing to clear git file status 2023-05-13 02:40:22 -07:00
Mikayla Maki
62c445da57 Match priority of folder highlights to vscode 2023-05-13 02:30:59 -07:00
Mikayla Maki
41bef2e444 Refactor out git status into FileName component
Integrate file name component into the editor's tab content
2023-05-13 02:26:45 -07:00
Joseph T. Lyons
e1c1100c7b Merge pull request #2466 from zed-industries/update-release-links
Update release links
2023-05-12 14:08:10 -04:00
Joseph Lyons
b70c874a0e Update release links 2023-05-12 14:04:36 -04:00
Mikayla Maki
ad7ed56e6b Delete pull_request_template.md 2023-05-12 10:15:13 -07:00
Mikayla Maki
4663ac8abf Create pull_request_template.md 2023-05-12 10:14:54 -07:00
Mikayla Maki
e71846c653 Create pull_request_template.md 2023-05-12 10:12:47 -07:00
Mikayla Maki
deac8a6ff9 Merge pull request #2465 from zed-industries/stream-branch-first
Send the root branch along with it's entry
2023-05-12 09:45:40 -07:00
Mikayla Maki
60320c6b09 Send the root branch along with it's entry 2023-05-12 09:37:02 -07:00
Mikayla Maki
54e9e7c35b Merge pull request #2464 from zed-industries/remove-between
Add TreeMap::remove_between that can take abstract start and end points
2023-05-12 08:59:56 -07:00
Mikayla Maki
6ef0f70528 Made the map seek target a publicly implementable interface
Integrated remove_range with the existing git code

co-authored-by: Nathan <nathan@zed.dev>
2023-05-12 08:37:32 -07:00
Nathan Sobo
ee3637216e Add TreeMap::remove_between that can take abstract start and end points
This commit introduces a new adaptor trait for SeekTarget that works around
frustrating issues with lifetimes. It wraps the arguments in a newtype wrapper
that lives on the stack to avoid the lifetime getting extended to the caller
of the method.

This allows us to introduce a PathSuccessor object that can be passed as the
end argument of remove_between to remove a whole subtree.
2023-05-12 08:21:01 -07:00
Kirill Bulatov
89352a2bdc Merge pull request #2463 from zed-industries/kb/reapply-modal-accessibility
Reintroduce more accesible modal keybindings
2023-05-12 06:11:04 +03:00
Mikayla Maki
defc9c8591 Merge pull request #2455 from zed-industries/git-status-viewer
Add Git Status to the project panel
2023-05-11 16:13:34 -07:00
Mikayla Maki
5fe8b73f04 compile error 😅 2023-05-11 16:07:41 -07:00
Mikayla Maki
d526fa6f1f fmt 2023-05-11 16:06:56 -07:00
Mikayla Maki
d538994c7f Use more efficient sum tree traversals for removal and improve ergonomics with iter_from
co-authored-by: Nathan <nathan@zed.dev>
2023-05-11 16:06:25 -07:00
Mikayla Maki
72655fc41d fmt 2023-05-11 13:25:57 -07:00
Mikayla Maki
6f87f9c51f Don't scan for statuses in files that are ignored 2023-05-11 13:25:07 -07:00
Mikayla Maki
1bb34e08bb Fix test 2023-05-11 12:03:39 -07:00
Mikayla Maki
dfb6a2f7fc fmt 2023-05-11 12:02:25 -07:00
Mikayla Maki
5b2ee63f80 Added status trickle up 2023-05-11 12:01:42 -07:00
Kirill Bulatov
f12dffa60c Reintroduce more accesible modal keybindings
Brings commit 475fc40923 back
2023-05-11 20:59:10 +03:00
Mikayla Maki
5accf7cf4e Update is_deleted when sending new repositories 2023-05-11 10:21:25 -07:00
Max Brunsfeld
bebb5456c7 Merge pull request #2461 from zed-industries/ci-target-dir-size
During CI, clear the target directory if it gets larger than a maximum size
2023-05-11 09:57:36 -07:00
Max Brunsfeld
3550110e57 ci: clear the target dir if it gets too big 2023-05-11 09:43:13 -07:00
Mikayla Maki
191ac86f09 Remove the CORRECT, overly agressive deletion codepath 2023-05-11 09:24:36 -07:00
Joseph Lyons
0ab94551f4 Revert "More keybindings in macOs modals with buttons"
This reverts commit 1398a12062.
2023-05-11 11:37:34 -04:00
Julia
0f34af50a8 Use path list generated during entry reload of a refresh request 2023-05-10 23:37:02 -04:00
Mikayla Maki
adfbbf21b2 fmt 2023-05-10 20:09:37 -07:00
Mikayla Maki
f5c633e80c Fixed bug in status deletion marking 2023-05-10 19:54:02 -07:00
Mikayla Maki
fca3bb3b93 Add randomized test for git statuses 2023-05-10 19:21:27 -07:00
Mikayla Maki
9800a149a6 Remove some external context from git status test 2023-05-10 17:59:33 -07:00
Mikayla Maki
f55ca7ae3c Fix incorrect import 2023-05-10 17:52:23 -07:00
Mikayla Maki
18becabfa5 Add postgres migration 2023-05-10 17:50:35 -07:00
Mikayla Maki
c7166fde3b Bump protocol version 2023-05-10 17:38:29 -07:00
Mikayla Maki
65d4c4f6ed Add integration test for git status 2023-05-10 17:37:36 -07:00
Mikayla Maki
e20eaca595 Got basic replication working :) 2023-05-10 17:37:36 -07:00
Mikayla Maki
2b80dfa81d Update protos 2023-05-10 17:37:36 -07:00
Mikayla Maki
00b345fdfe Use sum tree traversal to remove paths 2023-05-10 17:37:36 -07:00
Mikayla Maki
23a19d85b8 Fix bug in status detection when removing a directory 2023-05-10 17:37:36 -07:00
Mikayla Maki
0082d68d4a Revert "Convert git status calculation to use Entry IDs as the key instead of repo relative paths"
This reverts commit 728c6892c924ebeabb086e308ec4b5f56c4fd72a.
2023-05-10 17:37:36 -07:00
Petros Amoiridis
21e1bdc8cd Fix yellow to be yellow 2023-05-10 17:37:36 -07:00
Petros Amoiridis
6b4242cded Use theme.editor.diff for the colors 2023-05-10 17:37:36 -07:00
Mikayla Maki
f935047ff2 Convert git status calculation to use Entry IDs as the key instead of repo relative paths 2023-05-10 17:37:36 -07:00
Mikayla Maki
94a0de4c9f Fix compile errors 2023-05-10 17:37:36 -07:00
Mikayla Maki
a58a33fc93 WIP: integrate status with collab 2023-05-10 17:37:36 -07:00
Mikayla Maki
18cec8d64f Format 2023-05-10 17:37:36 -07:00
Mikayla Maki
e98507d8bf Added git status to the project panel, added worktree test 2023-05-10 17:37:36 -07:00
Mikayla Maki
93f57430da Track live entry status in repository 2023-05-10 17:37:36 -07:00
Mikayla Maki
bd98f78101 Fix compile error 2023-05-10 17:37:36 -07:00
Mikayla Maki
67491632cb WIP: Track live entry status in repository
co-authored-by: petros <petros@zed.dev>
2023-05-10 17:37:36 -07:00
Mikayla Maki
7169f5c760 Add git status to the file system abstraction
co-authored-by: petros <petros@zed.dev>
2023-05-10 17:37:36 -07:00
Joseph Lyons
6385e51957 collab 0.12.1 2023-05-10 18:16:20 -04:00
Joseph Lyons
9405b49957 v0.87.x dev 2023-05-10 16:47:09 -04:00
Mikayla Maki
c7fcc031eb Merge pull request #2460 from zed-industries/show-dock-on-activate
Fix bug with terminal button
2023-05-10 09:08:45 -07:00
Mikayla Maki
0dce5ba7ae Fix bug with terminal button 2023-05-10 08:15:20 -07:00
Kirill Bulatov
eec60556ab Highlight include/exclude inputs when errors happen there 2023-05-10 11:11:31 +03:00
Kirill Bulatov
dfdf7e4866 Test the search inclusions/exclusions 2023-05-10 11:11:31 +03:00
Kirill Bulatov
80fc1bc276 Use placeholder in include/exclude editors 2023-05-10 11:11:31 +03:00
Kirill Bulatov
0e31d13a1e Rework tab and escape key handling in search panel
Fixes
https://linear.app/zed-industries/issue/Z-1238/focus-the-results-multibuffer-in-project-search-on-esc

and adds a tab shortcut for project search inputs

co-authored-by: Julia <julia@zed.dev>
2023-05-10 11:11:31 +03:00
Kirill Bulatov
3da55c14a6 Fix arrow layout 2023-05-10 11:11:31 +03:00
Kirill Bulatov
6fb8679184 Trim glob input 2023-05-10 11:11:31 +03:00
Kirill Bulatov
13296d502c Extra rows approach draft
co-authored-by: Max <max@zed.dev>
2023-05-10 11:11:31 +03:00
Kirill Bulatov
b5abac6af6 Draft search include/exclude logic 2023-05-10 11:11:31 +03:00
Kirill Bulatov
915154b047 Add initial include/exclude project search UI 2023-05-10 11:11:31 +03:00
Antonio Scandurra
3115c8381d Merge pull request #2458 from zed-industries/fix-context-menu-click
Always dismiss context menu on click
2023-05-10 09:24:23 +02:00
Antonio Scandurra
1b5e79251c Always dismiss context menu on click 2023-05-10 09:18:13 +02:00
Nathan Sobo
5e8b7bd06d Replace todo with unimplemented to reduce distractions 2023-05-09 16:31:53 -06:00
Mikayla Maki
26d80eef0a Merge pull request #2456 from zed-industries/fix-dock-pane-focus
Make dock not eagerly steal focus from sub items
2023-05-09 15:59:06 -04:00
Mikayla Maki
0214228689 Fix format 2023-05-09 12:54:53 -07:00
Mikayla Maki
6dfb48dbd5 Fix center items not being activated when deserialized 2023-05-09 12:27:07 -07:00
Mikayla Maki
8d561d6408 Make dock not eagerly steal focus from sub items 2023-05-09 12:00:09 -07:00
Antonio Scandurra
2bc7be9a76 WIP 2023-05-09 17:14:33 +02:00
Antonio Scandurra
48ad3866b7 Randomly mutate worktree in addition to mutating the file-system
This ensures that we test the code path that refreshes entries.

Co-Authored-By: Julia Risley <julia@zed.dev>
2023-05-09 17:01:11 +02:00
Antonio Scandurra
7f27d72b20 Deliver file-system change events in batches in randomized worktree test
Co-Authored-By: Julia Risley <julia@zed.dev>
2023-05-09 16:55:03 +02:00
Julia
5c859da457 Only update changed local worktree buffers
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-05-09 15:13:12 +02:00
Mikayla Maki
2d7cfb8c7c Merge pull request #2454 from zed-industries/fix-mismatching-panes-when-deserializing-empty-panes
Fully remove panes and update internal data structures
2023-05-08 20:37:06 -04:00
Mikayla Maki
fa049bea6e Refactor and fix format 2023-05-08 17:32:40 -07:00
Mikayla Maki
49335d017a Add manual removal code to remove_panes 2023-05-08 17:25:28 -07:00
Mikayla Maki
9b2d3fcd48 Fully remove panes and update internal data structures 2023-05-08 17:09:29 -07:00
Max Brunsfeld
8fd0c9fb0e collab 0.12.0 2023-05-08 15:54:11 -07:00
Mikayla Maki
1d66f24f23 Merge pull request #2428 from zed-industries/add-branch-name
Add branch name and synchronize repository representations on the worktree.
2023-05-08 17:40:47 -04:00
Mikayla Maki
9366a0dbee Bump protocol version number 2023-05-08 14:34:14 -07:00
Mikayla Maki
f28419cfd1 Fix styling of titlebar highlights 2023-05-08 14:33:59 -07:00
Mikayla Maki
712fb5ad7f Add postgres migration 2023-05-08 14:33:59 -07:00
Mikayla Maki
1a9afd186b Restore randomized integration tests 2023-05-08 14:33:59 -07:00
Mikayla Maki
15d2f19b4a fix format 2023-05-08 14:33:59 -07:00
Mikayla Maki
d2279674a7 Fix panic in tests 2023-05-08 14:33:59 -07:00
Mikayla Maki
62e763d0d3 Removed test modifications, added special case to git initialization for when the repository is inside a .git folder 2023-05-08 14:33:59 -07:00
Mikayla Maki
f9e4464658 Refresh titlebar on project notifications 2023-05-08 14:33:59 -07:00
Mikayla Maki
2c2076bd77 Adjust tests to not create repositories inside repositories 2023-05-08 14:33:59 -07:00
Mikayla Maki
ab952f1b31 Fixed randomized test failures
co-authored-by: Max <max@zed.dev>
2023-05-08 14:33:59 -07:00
Mikayla Maki
d8dac07408 Removed scan ID from repository interfaces
co-authored-by: Max <max@zed.dev>
2023-05-08 14:33:59 -07:00
Mikayla Maki
270147d20c Finished RepositoryEntry refactoring, smoke tests passing
co-authored-by: Max <max@zed.dev>
2023-05-08 14:33:59 -07:00
Mikayla Maki
53569ece03 WIP: Change RepositoryEntry representation to be keyed off of the work directory
Removes branches button scaffolding
2023-05-08 14:33:59 -07:00
Mikayla Maki
b6d6f5c650 WIP: re-arranging the RepositoryEntry representation
Added branches to the randomized test to check the git branch
Added the remaining database integrations in collab

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Petros <petros@zed.dev>
2023-05-08 14:33:59 -07:00
Mikayla Maki
8bde496e74 Add branch name in title UI
co-authored-by: Petros <petros@zed.dev>
2023-05-08 14:33:59 -07:00
Petros Amoiridis
5302c256a4 Rebase main and fix error 2023-05-08 14:33:59 -07:00
Mikayla Maki
8301ee43d6 WIP: Add repository entries to collab and synchronize when rejoining the room
co-authored-by: Max <max@zed.dev>
2023-05-08 14:33:59 -07:00
Mikayla Maki
2fe5bf419b Add proto fields for repository entry maintenance 2023-05-08 14:33:59 -07:00
Mikayla Maki
c6d7ed33c2 Add smoke test for collaboration 2023-05-08 14:33:59 -07:00
Petros Amoiridis
ca4da52e39 Remove unused functions 2023-05-08 14:33:59 -07:00
Petros Amoiridis
e057b0193f Introduce BrancesButton in title bar
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-05-08 14:33:58 -07:00
Petros Amoiridis
797d47a08c Render title root names without branches 2023-05-08 14:33:58 -07:00
Petros Amoiridis
92a222aba8 Introduce a version control branch icon 2023-05-08 14:33:58 -07:00
Mikayla Maki
8f0aa3c6d9 Add branch name into title 2023-05-08 14:33:58 -07:00
Petros Amoiridis
d34ec462f8 Display branch information per worktree root
Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>
2023-05-08 14:33:58 -07:00
Mikayla Maki
ffd9d4eb59 Fix bug in repo detection 2023-05-08 14:33:58 -07:00
Mikayla Maki
3570810516 Add API for accessing git branch 2023-05-08 14:33:58 -07:00
Mikayla Maki
26afd592c5 Wire in the branch name 2023-05-08 14:33:58 -07:00
Mikayla Maki
5b4e58d1de Fix repo_for and clean up repository_entries 2023-05-08 14:33:58 -07:00
Mikayla Maki
023d665fb3 Fix TreeMap retain 2023-05-08 14:33:58 -07:00
Mikayla Maki
ae890212e3 Restored a lost API and got everything compiling 2023-05-08 14:33:58 -07:00
Mikayla Maki
bcf608e9e9 WIP: Refactor existing git code to use new representation.
co-authored-by: petros <petros@zed.dev>
2023-05-08 14:33:58 -07:00
Mikayla Maki
563f13925f WIP: Convert old git repository vec to new treemap based approach.
co-authored-by: Nathan <nathan@zed.dev>
2023-05-08 14:33:58 -07:00
Mikayla Maki
a58d3d8128 Add a data driven representation of the current git repository state to the worktree snapshots
WIP: Switch git repositories to use SumTrees

Co-authored-by: Nathan <nathan@zed.dev>
2023-05-08 14:33:58 -07:00
Mikayla Maki
bb93447a0d Merge pull request #2453 from zed-industries/fix-click-fallthrough
Fixed clicks falling through the modal terminal
2023-05-08 13:59:57 -04:00
Mikayla Maki
2cf928c85a Fixed clicks falling through the modal terminal
co-authored-by: Antonio <antonio@zed.dev>
2023-05-08 10:54:12 -07:00
Kirill Bulatov
39bddfc7b7 Only allow invisbles in local and leader selections 2023-05-08 19:36:51 +03:00
Kirill Bulatov
98ff18c430 Code review fixes 2023-05-08 19:36:51 +03:00
Kirill Bulatov
e6489e999d Add invisibles wrapping test 2023-05-08 19:36:51 +03:00
Kirill Bulatov
d2b2dc39d9 Do not print invisibles in non-full mode editors 2023-05-08 19:36:51 +03:00
Nate Butler
ab6b3adb2b Add a theme entry for whitespace, use it to style whitespaces
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
Co-Authored-By: Kirill Bulatov <mail4score@gmail.com>
2023-05-08 19:36:51 +03:00
Kirill Bulatov
fb3ef4bcf6 Fix wrapped line detection 2023-05-08 19:36:51 +03:00
Kirill Bulatov
075bab2ea9 Use more convntional name for the settings 2023-05-08 19:36:51 +03:00
Kirill Bulatov
706f6f495a Add a test 2023-05-08 19:36:51 +03:00
Kirill Bulatov
ec725fe399 Do not print extra invisibles on line wraps 2023-05-08 19:36:51 +03:00
Kirill Bulatov
95bcd19020 Refactor line glyphs drawing methods 2023-05-08 19:36:51 +03:00
Kirill Bulatov
4aaf44df94 Support invisibles in the selection 2023-05-08 19:36:51 +03:00
Kirill Bulatov
1eeeec157e Use cached standard glyphs for invisible symbols
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
2023-05-08 19:36:51 +03:00
Kirill Bulatov
714734d279 Add whitespaces rendering 2023-05-08 19:36:51 +03:00
Kirill Bulatov
2d8c88ad73 Draw tabs with svg icons in editor code only 2023-05-08 19:36:51 +03:00
Kirill Bulatov
f0a88b3337 Make invisibles display configurable 2023-05-08 19:36:51 +03:00
Kirill Bulatov
ad731ea6d2 Draft invisibles' tabs display 2023-05-08 19:36:51 +03:00
Max Brunsfeld
4f8607039c Add is_tab field to chunks
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2023-05-08 19:36:51 +03:00
Julia
cf304a0edc Merge pull request #2452 from zed-industries/prevent-some-rounding-clipping
Prevent some cases of clipping icons due to pixel coord rounding
2023-05-08 11:48:33 -04:00
Julia
332b364a30 Prevent some cases of clipping icons due to pixel coord rounding 2023-05-08 11:42:00 -04:00
Antonio Scandurra
235470bbfd Merge pull request #2451 from zed-industries/kb/extra-deps
Remove unused dependencies
2023-05-08 14:46:27 +02:00
Kirill Bulatov
6cb0bc89d2 Remove unused dependencies 2023-05-07 21:07:55 +03:00
Antonio Scandurra
0296974ab1 Merge pull request #2441 from zed-industries/implicit-ancestry
Determine view ancestry during layout
2023-05-05 10:58:00 +02:00
Antonio Scandurra
5e16f70067 💄 2023-05-05 10:53:15 +02:00
Antonio Scandurra
080a1f00a3 Delay focus_in event for window activation till after layout 2023-05-05 10:47:42 +02:00
Antonio Scandurra
b9ed327b94 Replace usages of is_parent_view_focused with is_self_focused
Previously, this was used because we didn't have access to the current
view and `EventContext` was an element-only abstraction. Now that the
`EventContext` wraps the current view's `ViewContext` we can simply check
for the view's focus and avoid querying ancestors.
2023-05-05 10:08:22 +02:00
Antonio Scandurra
80ad59a620 Make focusing the parent an effect to avoid querying ancestors 2023-05-05 10:04:54 +02:00
Joseph T. Lyons
c55a4c0feb Merge pull request #2447 from zed-industries/fix-auto-update
Do not use post_json() to auto update
2023-05-04 17:15:09 -04:00
Mikayla Maki
3631b3a86c Merge pull request #2446 from zed-industries/fix-copilot-logged-out
Fix copilot stuck in logged out state
2023-05-04 16:52:04 -04:00
Mikayla Maki
89af803565 Rearrange the state machine 2023-05-04 13:45:31 -07:00
Julia
137cbaba34 Merge pull request #2445 from zed-industries/construct-text-buffer-in-background
Construct text buffer in background
2023-05-04 16:01:39 -04:00
Julia
eacea55aaf Fixup cases using buffer model handle id as buffer id 2023-05-04 12:32:31 -04:00
Julia
1883e260ce Offload text::Buffer construction to background worker
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-05-04 12:32:31 -04:00
Julia
7e06062bdb Store history base text as rope
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-05-04 12:32:31 -04:00
Joseph Lyons
8313414e1e Do not use post_json() to auto update 2023-05-04 12:12:36 -04:00
Antonio Scandurra
d6b0569bed Merge branch 'main' into implicit-ancestry 2023-05-04 17:19:06 +02:00
Antonio Scandurra
f51425d390 Merge pull request #2444 from zed-industries/fix-clicking-sidebar-buttons
Use `Workspace::toggle_sidebar_item` when clicking on sidebar button
2023-05-04 17:14:40 +02:00
Antonio Scandurra
64e0c16baa Use Workspace::toggle_sidebar_item when clicking on sidebar button
Previously, we were mistakenly using `Sidebar::toggle_item`, which only
performs part of the toggle operation.
2023-05-04 17:06:37 +02:00
Antonio Scandurra
cbae4e751b Merge pull request #2443 from zed-industries/fix-vim-mode-rename
Avoid calling `update_window` twice in `blurred` event handler
2023-05-04 16:24:51 +02:00
Antonio Scandurra
912a4cf549 Avoid calling update_window twice in blurred event handler
This was preventing us from unhooking vim when performing a rename,
which prevented typing in the rename editor.
2023-05-04 16:18:01 +02:00
Antonio Scandurra
0f93714d4f Merge pull request #2442 from zed-industries/filter-vim-commands
Filter out vim commands when vim mode is disabled
2023-05-04 15:00:19 +02:00
Antonio Scandurra
b1f5cfaa79 Merge pull request #2440 from zed-industries/fix-navigate-to-definitions-panic
Fix panic when opening multiple definitions in a multibuffer
2023-05-04 14:56:43 +02:00
Antonio Scandurra
b3baebde22 Filter out vim commands when vim mode is disabled 2023-05-04 14:52:34 +02:00
Antonio Scandurra
da19edc3e3 Merge branch 'main' into implicit-ancestry 2023-05-04 14:39:58 +02:00
Antonio Scandurra
121264d35a Fix panic when opening multiple definitions in a multibuffer
The editor is on the stack, so adding an item to the `Pane` containing
the editor will cause a double borrow and a consequent panic. This
commit fixes the issue by deferring the opening of the definitions.
2023-05-04 14:34:42 +02:00
Antonio Scandurra
7e2a461486 Merge pull request #2439 from zed-industries/fix-keystrokes-for-action
Cache view's type id and keymap context into a separate map
2023-05-04 14:22:42 +02:00
Antonio Scandurra
5cc6304fa6 Verify keystrokes can be queried while views are on the stack 2023-05-04 12:09:32 +02:00
Antonio Scandurra
3d679ddb26 Avoid re-allocating KeymapContext after every view notification 2023-05-04 12:04:30 +02:00
Antonio Scandurra
18e39ef2fa Cache view's type id and keymap context into a separate map
During `layout`, we now pass a mutable reference to the element's
parent view. This is a recursive process that causes the view to
be removed from `AppContext` and then re-inserted back into it once
the layout is complete.

As such, querying parent views during `layout` does not work as such
views will have been removed from `AppContext` and not yet re-inserted
into it. This caused a bug in `KeystrokeLabel`, which used the `keystrokes_for_action`
method to query its ancestors to determine their type id and keymap context.

Now, any time a view notifies, we will cache its keymap context so that
we don't need to query the parent view during `layout`.
2023-05-04 10:47:56 +02:00
Antonio Scandurra
7b7a495be3 Remove stray dbg! statement 2023-05-04 09:56:49 +02:00
Antonio Scandurra
f6f18be9c3 Remove WindowContext::is_child_focused 2023-05-04 09:53:35 +02:00
Antonio Scandurra
67a3891f15 Make dispatch_event related methods public to the crate only 2023-05-04 09:53:35 +02:00
Antonio Scandurra
92183e0d72 Ensure querying keystrokes or actions is safe
This is achieved by moving `available_actions` into `AsyncAppContext` (where
we know no view/window is on the stack) and `keystrokes_for_action` into `LayoutContext`
where we'll fetch the previous frame's ancestors and notify the current view if those
change after we perform a layout.
2023-05-04 09:53:31 +02:00
Joseph Lyons
053b34875b collab 0.11.0 2023-05-03 14:59:04 -04:00
Joseph Lyons
653ea3a85d v0.86.x dev 2023-05-03 14:38:41 -04:00
Antonio Scandurra
040cc4d4c3 Allow notifying views when the ancestry of another view is outdated 2023-05-03 19:25:00 +02:00
Antonio Scandurra
7250754f8e Make dispatch_keystroke public to the crate only 2023-05-03 19:13:17 +02:00
Antonio Scandurra
9e8f852afb Remove ViewContext::is_child 2023-05-03 19:09:07 +02:00
Antonio Scandurra
5157442703 Fix integration test relying on deferred happening after focus
Focus is now one of the last things that happens during `flush_effects`,
and we shouldn't have relied on `defer` in the first place to verify
focus changes.
2023-05-03 19:00:32 +02:00
Antonio Scandurra
c65465b0b5 Ensure workspace gets rendered in collab integration tests 2023-05-03 18:31:07 +02:00
Antonio Scandurra
e9ed40da37 Remove the ability to retrieve the view's parent 2023-05-03 16:52:55 +02:00
Antonio Scandurra
7f137ed3dd Compute view ancestry at layout time 2023-05-03 16:36:14 +02:00
Antonio Scandurra
7f345f8bf5 Separate Window::build_scene into layout and paint 2023-05-03 12:18:16 +02:00
136 changed files with 7104 additions and 2186 deletions

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

@@ -0,0 +1,5 @@
[[PR Description]]
Release Notes:
* [[Added foo / Fixed bar / No notes]]

54
.github/workflows/build_dmg.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
name: Build Zed.dmg
on:
push:
branches:
- main
- "v[0-9]+.[0-9]+.x"
pull_request:
defaults:
run:
shell: bash -euxo pipefail {0}
concurrency:
# Allow only one workflow per any non-`main` branch.
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true
env:
RUST_BACKTRACE: 1
COPT: '-Werror'
jobs:
build-dmg:
if: github.ref_name == 'main' || contains(github.event.pull_request.labels.*.name, 'run-build-dmg')
runs-on:
- self-hosted
- test
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
clean: false
submodules: 'recursive'
- name: Install Rust
run: |
rustup set profile minimal
rustup update stable
- name: Install node
uses: actions/setup-node@v3
with:
node-version: 18
- name: Build dmg bundle
run: ./script/bundle
- name: Upload the build artifact
uses: actions/upload-artifact@v3
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
path: ./target/release/Zed.dmg

View File

@@ -62,6 +62,9 @@ jobs:
clean: false
submodules: 'recursive'
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 70
- name: Run check
run: cargo check --workspace
@@ -110,6 +113,9 @@ jobs:
clean: false
submodules: 'recursive'
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 70
- name: Determine version and release channel
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |

View File

@@ -14,7 +14,7 @@ jobs:
content: |
📣 Zed ${{ github.event.release.tag_name }} was just released!
Restart your Zed or head to https://zed.dev/releases/latest to grab it.
Restart your Zed or head to https://zed.dev/releases/stable/latest to grab it.
```md
# Changelog

40
Cargo.lock generated
View File

@@ -1097,6 +1097,7 @@ dependencies = [
"plist",
"serde",
"serde_derive",
"util",
]
[[package]]
@@ -1189,7 +1190,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.10.0"
version = "0.12.2"
dependencies = [
"anyhow",
"async-tungstenite",
@@ -1961,12 +1962,6 @@ version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2"
[[package]]
name = "easy-parallel"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946"
[[package]]
name = "editor"
version = "0.1.0"
@@ -2191,6 +2186,7 @@ dependencies = [
"project",
"serde_json",
"settings",
"text",
"theme",
"util",
"workspace",
@@ -2356,6 +2352,7 @@ dependencies = [
"serde_derive",
"serde_json",
"smol",
"sum_tree",
"tempfile",
"util",
]
@@ -2679,6 +2676,7 @@ dependencies = [
"postage",
"settings",
"text",
"util",
"workspace",
]
@@ -4722,9 +4720,11 @@ dependencies = [
"futures 0.3.25",
"fuzzy",
"git",
"glob",
"git2",
"globset",
"gpui",
"ignore",
"itertools",
"language",
"lazy_static",
"log",
@@ -5777,6 +5777,7 @@ dependencies = [
"collections",
"editor",
"futures 0.3.25",
"globset",
"gpui",
"language",
"log",
@@ -5894,15 +5895,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_path_to_error"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "184c643044780f7ceb59104cef98a5a6f12cb2288a7bc701ab93a362b49fd47d"
dependencies = [
"serde",
]
[[package]]
name = "serde_repr"
version = "0.1.9"
@@ -5978,7 +5970,6 @@ dependencies = [
"serde",
"serde_derive",
"serde_json",
"serde_path_to_error",
"sqlez",
"staff_mode",
"theme",
@@ -6549,6 +6540,12 @@ dependencies = [
"winx",
]
[[package]]
name = "take-until"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb"
[[package]]
name = "target-lexicon"
version = "0.12.5"
@@ -6694,13 +6691,13 @@ name = "theme"
version = "0.1.0"
dependencies = [
"anyhow",
"fs",
"gpui",
"indexmap",
"parking_lot 0.11.2",
"serde",
"serde_derive",
"serde_json",
"serde_path_to_error",
"toml",
]
@@ -7609,6 +7606,7 @@ dependencies = [
"serde",
"serde_json",
"smol",
"take-until",
"tempdir",
"url",
]
@@ -8546,7 +8544,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "zed"
version = "0.85.0"
version = "0.87.3"
dependencies = [
"activity_indicator",
"anyhow",
@@ -8572,7 +8570,6 @@ dependencies = [
"ctor",
"db",
"diagnostics",
"easy-parallel",
"editor",
"env_logger",
"feedback",
@@ -8615,7 +8612,6 @@ dependencies = [
"serde",
"serde_derive",
"serde_json",
"serde_path_to_error",
"settings",
"simplelog",
"smallvec",

View File

@@ -77,7 +77,8 @@ async-trait = { version = "0.1" }
ctor = { version = "0.1" }
env_logger = { version = "0.9" }
futures = { version = "0.3" }
glob = { version = "0.3.1" }
glob = { version = "0.3" }
globset = { version = "0.4" }
lazy_static = { version = "1.4.0" }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
ordered-float = { version = "2.1.1" }

View File

@@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.75 1.875C4.75 2.71406 4.19922 3.42422 3.4375 3.66328V5.97891C3.9086 5.64609 4.4711 5.4375 5.125 5.4375H7.375C8.30782 5.4375 9.0625 4.68281 9.0625 3.75V3.66328C8.30078 3.42422 7.75 2.71406 7.75 1.875C7.75 0.839531 8.58907 0 9.625 0C10.6609 0 11.5 0.839531 11.5 1.875C11.5 2.71406 10.9492 3.42422 10.1875 3.66328V3.75C10.1875 5.30391 8.92891 6.5625 7.375 6.5625H5.125C4.19219 6.5625 3.4375 7.31719 3.4375 8.25V8.33672C4.19922 8.57578 4.75 9.28594 4.75 10.125C4.75 11.1609 3.91094 12 2.875 12C1.83953 12 1 11.1609 1 10.125C1 9.28594 1.55172 8.57578 2.3125 8.33672V3.66328C1.55172 3.42422 1 2.71406 1 1.875C1 0.839531 1.83953 0 2.875 0C3.91094 0 4.75 0.839531 4.75 1.875ZM2.875 2.625C3.28914 2.625 3.625 2.28914 3.625 1.875C3.625 1.46086 3.28914 1.125 2.875 1.125C2.46086 1.125 2.125 1.46086 2.125 1.875C2.125 2.28914 2.46086 2.625 2.875 2.625ZM9.625 1.125C9.21016 1.125 8.875 1.46086 8.875 1.875C8.875 2.28914 9.21016 2.625 9.625 2.625C10.0398 2.625 10.375 2.28914 10.375 1.875C10.375 1.46086 10.0398 1.125 9.625 1.125ZM2.875 10.875C3.28914 10.875 3.625 10.5398 3.625 10.125C3.625 9.71016 3.28914 9.375 2.875 9.375C2.46086 9.375 2.125 9.71016 2.125 10.125C2.125 10.5398 2.46086 10.875 2.875 10.875Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -191,7 +191,7 @@
}
},
{
"context": "BufferSearchBar > Editor",
"context": "BufferSearchBar",
"bindings": {
"escape": "buffer_search::Dismiss",
"tab": "buffer_search::FocusEditor",
@@ -199,6 +199,18 @@
"shift-enter": "search::SelectPrevMatch"
}
},
{
"context": "ProjectSearchBar",
"bindings": {
"escape": "project_search::ToggleFocus"
}
},
{
"context": "ProjectSearchView",
"bindings": {
"escape": "project_search::ToggleFocus"
}
},
{
"context": "Pane",
"bindings": {

View File

@@ -11,6 +11,7 @@
"ctrl->": "zed::IncreaseBufferFontSize",
"ctrl-<": "zed::DecreaseBufferFontSize",
"cmd-d": "editor::DuplicateLine",
"cmd-backspace": "editor::DeleteLine",
"cmd-pagedown": "editor::MovePageDown",
"cmd-pageup": "editor::MovePageUp",
"ctrl-alt-shift-b": "editor::SelectToPreviousWordStart",
@@ -33,6 +34,7 @@
],
"shift-alt-up": "editor::MoveLineUp",
"shift-alt-down": "editor::MoveLineDown",
"cmd-alt-l": "editor::Format",
"cmd-[": "pane::GoBack",
"cmd-]": "pane::GoForward",
"alt-f7": "editor::FindAllReferences",
@@ -63,6 +65,7 @@
{
"context": "Workspace",
"bindings": {
"cmd-shift-o": "file_finder::Toggle",
"cmd-shift-a": "command_palette::Toggle",
"cmd-alt-o": "project_symbols::Toggle",
"cmd-1": "workspace::ToggleLeftSidebar",

View File

@@ -33,6 +33,29 @@
// Controls whether copilot provides suggestion immediately
// or waits for a `copilot::Toggle`
"show_copilot_suggestions": true,
// Whether to show tabs and spaces in the editor.
// This setting can take two values:
//
// 1. Draw tabs and spaces only for the selected text (default):
// "selection"
// 2. Do not draw any tabs or spaces:
// "none"
// 3. Draw all invisible symbols:
// "all"
"show_whitespaces": "selection",
// Whether to show the scrollbar in the editor.
// This setting can take four values:
//
// 1. Show the scrollbar if there's important information or
// follow the system's configured behavior (default):
// "auto"
// 2. Match the system's configured behavior:
// "system"
// 3. Always show the scrollbar:
// "always"
// 4. Never show the scrollbar:
// "never"
"show_scrollbars": "auto",
// 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.

View File

@@ -102,7 +102,7 @@ fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
{
format!("{server_url}/releases/preview/latest")
} else {
format!("{server_url}/releases/latest")
format!("{server_url}/releases/stable/latest")
};
cx.platform().open_url(&latest_release_url);
}
@@ -273,7 +273,7 @@ impl AutoUpdater {
telemetry,
})?);
let mut response = client.post_json(&release.url, request_body, true).await?;
let mut response = client.get(&release.url, request_body, true).await?;
smol::io::copy(response.body_mut(), &mut dmg_file).await?;
log::info!("downloaded update. path:{:?}", dmg_path);

View File

@@ -19,6 +19,7 @@ dirs = "3.0"
ipc-channel = "0.16"
serde.workspace = true
serde_derive.workspace = true
util = { path = "../util" }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.9"

View File

@@ -1,6 +1,5 @@
pub use ipc_channel::ipc;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Serialize, Deserialize)]
pub struct IpcHandshake {
@@ -10,7 +9,12 @@ pub struct IpcHandshake {
#[derive(Debug, Serialize, Deserialize)]
pub enum CliRequest {
Open { paths: Vec<PathBuf>, wait: bool },
// The filed is named `path` for compatibility, but now CLI can request
// opening a path at a certain row and/or column: `some/path:123` and `some/path:123:456`.
//
// Since Zed CLI has to be installed separately, there can be situations when old CLI is
// querying new Zed editors, support both formats by using `String` here and parsing it on Zed side later.
Open { paths: Vec<String>, wait: bool },
}
#[derive(Debug, Serialize, Deserialize)]
@@ -20,3 +24,7 @@ pub enum CliResponse {
Stderr { message: String },
Exit { status: i32 },
}
/// When Zed started not as an *.app but as a binary (e.g. local development),
/// there's a possibility to tell it to behave "regularly".
pub const FORCE_CLI_MODE_ENV_VAR_NAME: &str = "ZED_FORCE_CLI_MODE";

View File

@@ -1,6 +1,6 @@
use anyhow::{anyhow, Result};
use anyhow::{anyhow, Context, Result};
use clap::Parser;
use cli::{CliRequest, CliResponse, IpcHandshake};
use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
use core_foundation::{
array::{CFArray, CFIndex},
string::kCFStringEncodingUTF8,
@@ -16,16 +16,20 @@ use std::{
path::{Path, PathBuf},
ptr,
};
use util::paths::PathLikeWithPosition;
#[derive(Parser)]
#[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))]
struct Args {
/// Wait for all of the given paths to be closed before exiting.
/// Wait for all of the given paths to be opened/closed before exiting.
#[clap(short, long)]
wait: bool,
/// A sequence of space-separated paths that you want to open.
#[clap()]
paths: Vec<PathBuf>,
///
/// Use `path:line:row` syntax to open a file at a specific location.
/// Non-existing paths and directories will ignore `:line:row` suffix.
#[clap(value_parser = parse_path_with_position)]
paths_with_position: Vec<PathLikeWithPosition<PathBuf>>,
/// Print Zed's version and the app path.
#[clap(short, long)]
version: bool,
@@ -34,6 +38,14 @@ struct Args {
bundle_path: Option<PathBuf>,
}
fn parse_path_with_position(
argument_str: &str,
) -> Result<PathLikeWithPosition<PathBuf>, std::convert::Infallible> {
PathLikeWithPosition::parse_str(argument_str, |path_str| {
Ok(Path::new(path_str).to_path_buf())
})
}
#[derive(Debug, Deserialize)]
struct InfoPlist {
#[serde(rename = "CFBundleShortVersionString")]
@@ -43,37 +55,37 @@ struct InfoPlist {
fn main() -> Result<()> {
let args = Args::parse();
let bundle_path = if let Some(bundle_path) = args.bundle_path {
bundle_path.canonicalize()?
} else {
locate_bundle()?
};
let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?;
if args.version {
let plist_path = bundle_path.join("Contents/Info.plist");
let plist = plist::from_file::<_, InfoPlist>(plist_path)?;
println!(
"Zed {} {}",
plist.bundle_short_version_string,
bundle_path.to_string_lossy()
);
println!("{}", bundle.zed_version_string());
return Ok(());
}
for path in args.paths.iter() {
for path in args
.paths_with_position
.iter()
.map(|path_with_position| &path_with_position.path_like)
{
if !path.exists() {
touch(path.as_path())?;
}
}
let (tx, rx) = launch_app(bundle_path)?;
let (tx, rx) = bundle.launch()?;
tx.send(CliRequest::Open {
paths: args
.paths
.paths_with_position
.into_iter()
.map(|path| fs::canonicalize(path).map_err(|error| anyhow!(error)))
.collect::<Result<Vec<PathBuf>>>()?,
.map(|path_with_position| {
let path_with_position = path_with_position.map_path_like(|path| {
fs::canonicalize(&path)
.with_context(|| format!("path {path:?} canonicalization"))
})?;
Ok(path_with_position.to_string(|path| path.display().to_string()))
})
.collect::<Result<_>>()?,
wait: args.wait,
})?;
@@ -89,6 +101,148 @@ fn main() -> Result<()> {
Ok(())
}
enum Bundle {
App {
app_bundle: PathBuf,
plist: InfoPlist,
},
LocalPath {
executable: PathBuf,
plist: InfoPlist,
},
}
impl Bundle {
fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
let bundle_path = if let Some(bundle_path) = args_bundle_path {
bundle_path
.canonicalize()
.with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
} else {
locate_bundle().context("bundle autodiscovery")?
};
match bundle_path.extension().and_then(|ext| ext.to_str()) {
Some("app") => {
let plist_path = bundle_path.join("Contents/Info.plist");
let plist = plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
format!("Reading *.app bundle plist file at {plist_path:?}")
})?;
Ok(Self::App {
app_bundle: bundle_path,
plist,
})
}
_ => {
println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
let plist_path = bundle_path
.parent()
.with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
.join("WebRTC.framework/Resources/Info.plist");
let plist = plist::from_file::<_, InfoPlist>(&plist_path)
.with_context(|| format!("Reading dev bundle plist file at {plist_path:?}"))?;
Ok(Self::LocalPath {
executable: bundle_path,
plist,
})
}
}
}
fn plist(&self) -> &InfoPlist {
match self {
Self::App { plist, .. } => plist,
Self::LocalPath { plist, .. } => plist,
}
}
fn path(&self) -> &Path {
match self {
Self::App { app_bundle, .. } => app_bundle,
Self::LocalPath {
executable: excutable,
..
} => excutable,
}
}
fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
let (server, server_name) =
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
let url = format!("zed-cli://{server_name}");
match self {
Self::App { app_bundle, .. } => {
let app_path = app_bundle;
let status = unsafe {
let app_url = CFURL::from_path(app_path, true)
.with_context(|| format!("invalid app path {app_path:?}"))?;
let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
ptr::null(),
url.as_ptr(),
url.len() as CFIndex,
kCFStringEncodingUTF8,
ptr::null(),
));
let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
LSOpenFromURLSpec(
&LSLaunchURLSpec {
appURL: app_url.as_concrete_TypeRef(),
itemURLs: urls_to_open.as_concrete_TypeRef(),
passThruParams: ptr::null(),
launchFlags: kLSLaunchDefaults,
asyncRefCon: ptr::null_mut(),
},
ptr::null_mut(),
)
};
anyhow::ensure!(
status == 0,
"cannot start app bundle {}",
self.zed_version_string()
);
}
Self::LocalPath { executable, .. } => {
let executable_parent = executable
.parent()
.with_context(|| format!("Executable {executable:?} path has no parent"))?;
let subprocess_stdout_file =
fs::File::create(executable_parent.join("zed_dev.log"))
.with_context(|| format!("Log file creation in {executable_parent:?}"))?;
let subprocess_stdin_file =
subprocess_stdout_file.try_clone().with_context(|| {
format!("Cloning descriptor for file {subprocess_stdout_file:?}")
})?;
let mut command = std::process::Command::new(executable);
let command = command
.env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
.stderr(subprocess_stdout_file)
.stdout(subprocess_stdin_file)
.arg(url);
command
.spawn()
.with_context(|| format!("Spawning {command:?}"))?;
}
}
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
Ok((handshake.requests, handshake.responses))
}
fn zed_version_string(&self) -> String {
let is_dev = matches!(self, Self::LocalPath { .. });
format!(
"Zed {}{} {}",
self.plist().bundle_short_version_string,
if is_dev { " (dev)" } else { "" },
self.path().display(),
)
}
}
fn touch(path: &Path) -> io::Result<()> {
match OpenOptions::new().create(true).write(true).open(path) {
Ok(_) => Ok(()),
@@ -106,38 +260,3 @@ fn locate_bundle() -> Result<PathBuf> {
}
Ok(app_path)
}
fn launch_app(app_path: PathBuf) -> Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
let (server, server_name) = IpcOneShotServer::<IpcHandshake>::new()?;
let url = format!("zed-cli://{server_name}");
let status = unsafe {
let app_url =
CFURL::from_path(&app_path, true).ok_or_else(|| anyhow!("invalid app path"))?;
let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
ptr::null(),
url.as_ptr(),
url.len() as CFIndex,
kCFStringEncodingUTF8,
ptr::null(),
));
let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
LSOpenFromURLSpec(
&LSLaunchURLSpec {
appURL: app_url.as_concrete_TypeRef(),
itemURLs: urls_to_open.as_concrete_TypeRef(),
passThruParams: ptr::null(),
launchFlags: kLSLaunchDefaults,
asyncRefCon: ptr::null_mut(),
},
ptr::null_mut(),
)
};
if status == 0 {
let (_, handshake) = server.accept()?;
Ok((handshake.requests, handshake.responses))
} else {
Err(anyhow!("cannot start {:?}", app_path))
}
}

View File

@@ -86,6 +86,11 @@ pub enum ClickhouseEvent {
copilot_enabled: bool,
copilot_enabled_for_language: bool,
},
Copilot {
suggestion_id: Option<String>,
suggestion_accepted: bool,
file_extension: Option<String>,
},
}
#[derive(Serialize, Debug)]
@@ -270,7 +275,7 @@ impl Telemetry {
}])?;
this.http_client
.post_json(MIXPANEL_ENGAGE_URL, json_bytes.into(), false)
.post_json(MIXPANEL_ENGAGE_URL, json_bytes.into())
.await?;
anyhow::Ok(())
}
@@ -404,7 +409,7 @@ impl Telemetry {
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, &events)?;
this.http_client
.post_json(MIXPANEL_EVENTS_URL, json_bytes.into(), false)
.post_json(MIXPANEL_EVENTS_URL, json_bytes.into())
.await?;
anyhow::Ok(())
}
@@ -454,7 +459,7 @@ impl Telemetry {
}
this.http_client
.post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into(), false)
.post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into())
.await?;
anyhow::Ok(())
}

View File

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

View File

@@ -82,6 +82,37 @@ CREATE TABLE "worktree_entries" (
CREATE INDEX "index_worktree_entries_on_project_id" ON "worktree_entries" ("project_id");
CREATE INDEX "index_worktree_entries_on_project_id_and_worktree_id" ON "worktree_entries" ("project_id", "worktree_id");
CREATE TABLE "worktree_repositories" (
"project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL,
"work_directory_id" INTEGER NOT NULL,
"branch" VARCHAR,
"scan_id" INTEGER NOT NULL,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, work_directory_id),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");
CREATE TABLE "worktree_repository_statuses" (
"project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL,
"work_directory_id" INTEGER NOT NULL,
"repo_path" VARCHAR NOT NULL,
"status" INTEGER NOT NULL,
"scan_id" INTEGER NOT NULL,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_repository_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id_and_work_directory_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");
CREATE TABLE "worktree_diagnostic_summaries" (
"project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL,
@@ -153,7 +184,7 @@ CREATE TABLE "followers" (
"follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
"follower_connection_id" INTEGER NOT NULL
);
CREATE UNIQUE INDEX
CREATE UNIQUE INDEX
"index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");

View File

@@ -0,0 +1,13 @@
CREATE TABLE "worktree_repositories" (
"project_id" INTEGER NOT NULL,
"worktree_id" INT8 NOT NULL,
"work_directory_id" INT8 NOT NULL,
"scan_id" INT8 NOT NULL,
"branch" VARCHAR,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, work_directory_id),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");

View File

@@ -0,0 +1,15 @@
CREATE TABLE "worktree_repository_statuses" (
"project_id" INTEGER NOT NULL,
"worktree_id" INT8 NOT NULL,
"work_directory_id" INT8 NOT NULL,
"repo_path" VARCHAR NOT NULL,
"status" INT8 NOT NULL,
"scan_id" INT8 NOT NULL,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_wt_repos_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id_and_wd_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");

View File

@@ -14,6 +14,8 @@ mod user;
mod worktree;
mod worktree_diagnostic_summary;
mod worktree_entry;
mod worktree_repository;
mod worktree_repository_statuses;
use crate::executor::Executor;
use crate::{Error, Result};
@@ -1489,6 +1491,8 @@ impl Database {
visible: db_worktree.visible,
updated_entries: Default::default(),
removed_entries: Default::default(),
updated_repositories: Default::default(),
removed_repositories: Default::default(),
diagnostic_summaries: Default::default(),
scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64,
@@ -1498,38 +1502,119 @@ impl Database {
.worktrees
.iter()
.find(|worktree| worktree.id == db_worktree.id as u64);
let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id)
} else {
worktree_entry::Column::IsDeleted.eq(false)
};
let mut db_entries = worktree_entry::Entity::find()
.filter(
Condition::all()
.add(worktree_entry::Column::WorktreeId.eq(worktree.id))
.add(entry_filter),
)
.stream(&*tx)
.await?;
while let Some(db_entry) = db_entries.next().await {
let db_entry = db_entry?;
if db_entry.is_deleted {
worktree.removed_entries.push(db_entry.id as u64);
// File entries
{
let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id)
} else {
worktree.updated_entries.push(proto::Entry {
id: db_entry.id as u64,
is_dir: db_entry.is_dir,
path: db_entry.path,
inode: db_entry.inode as u64,
mtime: Some(proto::Timestamp {
seconds: db_entry.mtime_seconds as u64,
nanos: db_entry.mtime_nanos as u32,
}),
is_symlink: db_entry.is_symlink,
is_ignored: db_entry.is_ignored,
});
worktree_entry::Column::IsDeleted.eq(false)
};
let mut db_entries = worktree_entry::Entity::find()
.filter(
Condition::all()
.add(worktree_entry::Column::WorktreeId.eq(worktree.id))
.add(entry_filter),
)
.stream(&*tx)
.await?;
while let Some(db_entry) = db_entries.next().await {
let db_entry = db_entry?;
if db_entry.is_deleted {
worktree.removed_entries.push(db_entry.id as u64);
} else {
worktree.updated_entries.push(proto::Entry {
id: db_entry.id as u64,
is_dir: db_entry.is_dir,
path: db_entry.path,
inode: db_entry.inode as u64,
mtime: Some(proto::Timestamp {
seconds: db_entry.mtime_seconds as u64,
nanos: db_entry.mtime_nanos as u32,
}),
is_symlink: db_entry.is_symlink,
is_ignored: db_entry.is_ignored,
});
}
}
}
// Repository Entries
{
let repository_entry_filter =
if let Some(rejoined_worktree) = rejoined_worktree {
worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id)
} else {
worktree_repository::Column::IsDeleted.eq(false)
};
let mut db_repositories = worktree_repository::Entity::find()
.filter(
Condition::all()
.add(worktree_repository::Column::WorktreeId.eq(worktree.id))
.add(repository_entry_filter),
)
.stream(&*tx)
.await?;
while let Some(db_repository) = db_repositories.next().await {
let db_repository = db_repository?;
if db_repository.is_deleted {
worktree
.removed_repositories
.push(db_repository.work_directory_id as u64);
} else {
worktree.updated_repositories.push(proto::RepositoryEntry {
work_directory_id: db_repository.work_directory_id as u64,
branch: db_repository.branch,
removed_repo_paths: Default::default(),
updated_statuses: Default::default(),
});
}
}
}
// Repository Status Entries
for repository in worktree.updated_repositories.iter_mut() {
let repository_status_entry_filter =
if let Some(rejoined_worktree) = rejoined_worktree {
worktree_repository_statuses::Column::ScanId
.gt(rejoined_worktree.scan_id)
} else {
worktree_repository_statuses::Column::IsDeleted.eq(false)
};
let mut db_repository_statuses =
worktree_repository_statuses::Entity::find()
.filter(
Condition::all()
.add(
worktree_repository_statuses::Column::WorktreeId
.eq(worktree.id),
)
.add(
worktree_repository_statuses::Column::WorkDirectoryId
.eq(repository.work_directory_id),
)
.add(repository_status_entry_filter),
)
.stream(&*tx)
.await?;
while let Some(db_status_entry) = db_repository_statuses.next().await {
let db_status_entry = db_status_entry?;
if db_status_entry.is_deleted {
repository
.removed_repo_paths
.push(db_status_entry.repo_path);
} else {
repository.updated_statuses.push(proto::StatusEntry {
repo_path: db_status_entry.repo_path,
status: db_status_entry.status as i32,
});
}
}
}
@@ -2330,6 +2415,115 @@ impl Database {
.await?;
}
if !update.updated_repositories.is_empty() {
worktree_repository::Entity::insert_many(update.updated_repositories.iter().map(
|repository| worktree_repository::ActiveModel {
project_id: ActiveValue::set(project_id),
worktree_id: ActiveValue::set(worktree_id),
work_directory_id: ActiveValue::set(repository.work_directory_id as i64),
scan_id: ActiveValue::set(update.scan_id as i64),
branch: ActiveValue::set(repository.branch.clone()),
is_deleted: ActiveValue::set(false),
},
))
.on_conflict(
OnConflict::columns([
worktree_repository::Column::ProjectId,
worktree_repository::Column::WorktreeId,
worktree_repository::Column::WorkDirectoryId,
])
.update_columns([
worktree_repository::Column::ScanId,
worktree_repository::Column::Branch,
])
.to_owned(),
)
.exec(&*tx)
.await?;
for repository in update.updated_repositories.iter() {
if !repository.updated_statuses.is_empty() {
worktree_repository_statuses::Entity::insert_many(
repository.updated_statuses.iter().map(|status_entry| {
worktree_repository_statuses::ActiveModel {
project_id: ActiveValue::set(project_id),
worktree_id: ActiveValue::set(worktree_id),
work_directory_id: ActiveValue::set(
repository.work_directory_id as i64,
),
repo_path: ActiveValue::set(status_entry.repo_path.clone()),
status: ActiveValue::set(status_entry.status as i64),
scan_id: ActiveValue::set(update.scan_id as i64),
is_deleted: ActiveValue::set(false),
}
}),
)
.on_conflict(
OnConflict::columns([
worktree_repository_statuses::Column::ProjectId,
worktree_repository_statuses::Column::WorktreeId,
worktree_repository_statuses::Column::WorkDirectoryId,
worktree_repository_statuses::Column::RepoPath,
])
.update_columns([
worktree_repository_statuses::Column::ScanId,
worktree_repository_statuses::Column::Status,
worktree_repository_statuses::Column::IsDeleted,
])
.to_owned(),
)
.exec(&*tx)
.await?;
}
if !repository.removed_repo_paths.is_empty() {
worktree_repository_statuses::Entity::update_many()
.filter(
worktree_repository_statuses::Column::ProjectId
.eq(project_id)
.and(
worktree_repository_statuses::Column::WorktreeId
.eq(worktree_id),
)
.and(
worktree_repository_statuses::Column::WorkDirectoryId
.eq(repository.work_directory_id as i64),
)
.and(worktree_repository_statuses::Column::RepoPath.is_in(
repository.removed_repo_paths.iter().map(String::as_str),
)),
)
.set(worktree_repository_statuses::ActiveModel {
is_deleted: ActiveValue::Set(true),
scan_id: ActiveValue::Set(update.scan_id as i64),
..Default::default()
})
.exec(&*tx)
.await?;
}
}
}
if !update.removed_repositories.is_empty() {
worktree_repository::Entity::update_many()
.filter(
worktree_repository::Column::ProjectId
.eq(project_id)
.and(worktree_repository::Column::WorktreeId.eq(worktree_id))
.and(
worktree_repository::Column::WorkDirectoryId
.is_in(update.removed_repositories.iter().map(|id| *id as i64)),
),
)
.set(worktree_repository::ActiveModel {
is_deleted: ActiveValue::Set(true),
scan_id: ActiveValue::Set(update.scan_id as i64),
..Default::default()
})
.exec(&*tx)
.await?;
}
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
Ok(connection_ids)
})
@@ -2505,6 +2699,7 @@ impl Database {
root_name: db_worktree.root_name,
visible: db_worktree.visible,
entries: Default::default(),
repository_entries: Default::default(),
diagnostic_summaries: Default::default(),
scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64,
@@ -2542,6 +2737,61 @@ impl Database {
}
}
// Populate repository entries.
{
let mut db_repository_entries = worktree_repository::Entity::find()
.filter(
Condition::all()
.add(worktree_repository::Column::ProjectId.eq(project_id))
.add(worktree_repository::Column::IsDeleted.eq(false)),
)
.stream(&*tx)
.await?;
while let Some(db_repository_entry) = db_repository_entries.next().await {
let db_repository_entry = db_repository_entry?;
if let Some(worktree) =
worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
{
worktree.repository_entries.insert(
db_repository_entry.work_directory_id as u64,
proto::RepositoryEntry {
work_directory_id: db_repository_entry.work_directory_id as u64,
branch: db_repository_entry.branch,
removed_repo_paths: Default::default(),
updated_statuses: Default::default(),
},
);
}
}
}
{
let mut db_status_entries = worktree_repository_statuses::Entity::find()
.filter(
Condition::all()
.add(worktree_repository_statuses::Column::ProjectId.eq(project_id))
.add(worktree_repository_statuses::Column::IsDeleted.eq(false)),
)
.stream(&*tx)
.await?;
while let Some(db_status_entry) = db_status_entries.next().await {
let db_status_entry = db_status_entry?;
if let Some(worktree) = worktrees.get_mut(&(db_status_entry.worktree_id as u64))
{
if let Some(repository_entry) = worktree
.repository_entries
.get_mut(&(db_status_entry.work_directory_id as u64))
{
repository_entry.updated_statuses.push(proto::StatusEntry {
repo_path: db_status_entry.repo_path,
status: db_status_entry.status as i32,
});
}
}
}
}
// Populate worktree diagnostic summaries.
{
let mut db_summaries = worktree_diagnostic_summary::Entity::find()
@@ -3223,6 +3473,8 @@ pub struct RejoinedWorktree {
pub visible: bool,
pub updated_entries: Vec<proto::Entry>,
pub removed_entries: Vec<u64>,
pub updated_repositories: Vec<proto::RepositoryEntry>,
pub removed_repositories: Vec<u64>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub scan_id: u64,
pub completed_scan_id: u64,
@@ -3277,6 +3529,7 @@ pub struct Worktree {
pub root_name: String,
pub visible: bool,
pub entries: Vec<proto::Entry>,
pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub scan_id: u64,
pub completed_scan_id: u64,

View File

@@ -0,0 +1,21 @@
use super::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worktree_repositories")]
pub struct Model {
#[sea_orm(primary_key)]
pub project_id: ProjectId,
#[sea_orm(primary_key)]
pub worktree_id: i64,
#[sea_orm(primary_key)]
pub work_directory_id: i64,
pub scan_id: i64,
pub branch: Option<String>,
pub is_deleted: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,23 @@
use super::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worktree_repository_statuses")]
pub struct Model {
#[sea_orm(primary_key)]
pub project_id: ProjectId,
#[sea_orm(primary_key)]
pub worktree_id: i64,
#[sea_orm(primary_key)]
pub work_directory_id: i64,
#[sea_orm(primary_key)]
pub repo_path: String,
pub status: i64,
pub scan_id: i64,
pub is_deleted: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -1063,6 +1063,8 @@ async fn rejoin_room(
removed_entries: worktree.removed_entries,
scan_id: worktree.scan_id,
is_last_update: worktree.completed_scan_id == worktree.scan_id,
updated_repositories: worktree.updated_repositories,
removed_repositories: worktree.removed_repositories,
};
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
session.peer.send(session.connection_id, update.clone())?;
@@ -1383,6 +1385,8 @@ async fn join_project(
removed_entries: Default::default(),
scan_id: worktree.scan_id,
is_last_update: worktree.scan_id == worktree.completed_scan_id,
updated_repositories: worktree.repository_entries.into_values().collect(),
removed_repositories: Default::default(),
};
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
session.peer.send(session.connection_id, update.clone())?;

View File

@@ -12,7 +12,10 @@ use client::{
use collections::{HashMap, HashSet};
use fs::FakeFs;
use futures::{channel::oneshot, StreamExt as _};
use gpui::{executor::Deterministic, test::EmptyView, ModelHandle, TestAppContext, ViewHandle};
use gpui::{
elements::*, executor::Deterministic, AnyElement, Entity, ModelHandle, TestAppContext, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use language::LanguageRegistry;
use parking_lot::Mutex;
use project::{Project, WorktreeId};
@@ -462,8 +465,41 @@ impl TestClient {
project: &ModelHandle<Project>,
cx: &mut TestAppContext,
) -> ViewHandle<Workspace> {
let (_, root_view) = cx.add_window(|_| EmptyView);
cx.add_view(&root_view, |cx| Workspace::test_new(project.clone(), cx))
struct WorkspaceContainer {
workspace: Option<WeakViewHandle<Workspace>>,
}
impl Entity for WorkspaceContainer {
type Event = ();
}
impl View for WorkspaceContainer {
fn ui_name() -> &'static str {
"WorkspaceContainer"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if let Some(workspace) = self
.workspace
.as_ref()
.and_then(|workspace| workspace.upgrade(cx))
{
ChildView::new(&workspace, cx).into_any()
} else {
Empty::new().into_any()
}
}
}
// We use a workspace container so that we don't need to remove the window in order to
// drop the workspace and we can use a ViewHandle instead.
let (window_id, container) = cx.add_window(|_| WorkspaceContainer { workspace: None });
let workspace = cx.add_view(window_id, |cx| Workspace::test_new(project.clone(), cx));
container.update(cx, |container, cx| {
container.workspace = Some(workspace.downgrade());
cx.notify();
});
workspace
}
}

View File

@@ -10,11 +10,11 @@ use editor::{
ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions,
Undo,
};
use fs::{FakeFs, Fs as _, LineEnding, RemoveOptions};
use fs::{repository::GitFileStatus, FakeFs, Fs as _, LineEnding, RemoveOptions};
use futures::StreamExt as _;
use gpui::{
executor::Deterministic, geometry::vector::vec2f, test::EmptyView, ModelHandle, TestAppContext,
ViewHandle,
executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle,
TestAppContext, ViewHandle,
};
use indoc::indoc;
use language::{
@@ -1202,7 +1202,7 @@ async fn test_share_project(
cx_c: &mut TestAppContext,
) {
deterministic.forbid_parking();
let (_, window_b) = cx_b.add_window(|_| EmptyView);
let (window_b, _) = cx_b.add_window(|_| EmptyView);
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
@@ -1289,7 +1289,7 @@ async fn test_share_project(
.await
.unwrap();
let editor_b = cx_b.add_view(&window_b, |cx| Editor::for_buffer(buffer_b, None, cx));
let editor_b = cx_b.add_view(window_b, |cx| Editor::for_buffer(buffer_b, None, cx));
// Client A sees client B's selection
deterministic.run_until_parked();
@@ -2604,6 +2604,240 @@ async fn test_git_diff_base_change(
});
}
#[gpui::test]
async fn test_git_branch_name(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
client_a
.fs
.insert_tree(
"/dir",
json!({
".git": {},
}),
)
.await;
let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| {
call.share_project(project_local.clone(), cx)
})
.await
.unwrap();
let project_remote = client_b.build_remote_project(project_id, cx_b).await;
client_a
.fs
.as_fake()
.set_branch_name(Path::new("/dir/.git"), Some("branch-1"))
.await;
// Wait for it to catch up to the new branch
deterministic.run_until_parked();
#[track_caller]
fn assert_branch(branch_name: Option<impl Into<String>>, project: &Project, cx: &AppContext) {
let branch_name = branch_name.map(Into::into);
let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
let worktree = worktrees[0].clone();
let root_entry = worktree.read(cx).snapshot().root_git_entry().unwrap();
assert_eq!(root_entry.branch(), branch_name.map(Into::into));
}
// Smoke test branch reading
project_local.read_with(cx_a, |project, cx| {
assert_branch(Some("branch-1"), project, cx)
});
project_remote.read_with(cx_b, |project, cx| {
assert_branch(Some("branch-1"), project, cx)
});
client_a
.fs
.as_fake()
.set_branch_name(Path::new("/dir/.git"), Some("branch-2"))
.await;
// Wait for buffer_local_a to receive it
deterministic.run_until_parked();
// Smoke test branch reading
project_local.read_with(cx_a, |project, cx| {
assert_branch(Some("branch-2"), project, cx)
});
project_remote.read_with(cx_b, |project, cx| {
assert_branch(Some("branch-2"), project, cx)
});
let project_remote_c = client_c.build_remote_project(project_id, cx_c).await;
project_remote_c.read_with(cx_c, |project, cx| {
assert_branch(Some("branch-2"), project, cx)
});
}
#[gpui::test]
async fn test_git_status_sync(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
client_a
.fs
.insert_tree(
"/dir",
json!({
".git": {},
"a.txt": "a",
"b.txt": "b",
}),
)
.await;
const A_TXT: &'static str = "a.txt";
const B_TXT: &'static str = "b.txt";
client_a
.fs
.as_fake()
.set_status_for_repo(
Path::new("/dir/.git"),
&[
(&Path::new(A_TXT), GitFileStatus::Added),
(&Path::new(B_TXT), GitFileStatus::Added),
],
)
.await;
let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| {
call.share_project(project_local.clone(), cx)
})
.await
.unwrap();
let project_remote = client_b.build_remote_project(project_id, cx_b).await;
// Wait for it to catch up to the new status
deterministic.run_until_parked();
#[track_caller]
fn assert_status(
file: &impl AsRef<Path>,
status: Option<GitFileStatus>,
project: &Project,
cx: &AppContext,
) {
let file = file.as_ref();
let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
let worktree = worktrees[0].clone();
let snapshot = worktree.read(cx).snapshot();
let root_entry = snapshot.root_git_entry().unwrap();
assert_eq!(root_entry.status_for_file(&snapshot, file), status);
}
// Smoke test status reading
project_local.read_with(cx_a, |project, cx| {
assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
});
project_remote.read_with(cx_b, |project, cx| {
assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
});
client_a
.fs
.as_fake()
.set_status_for_repo(
Path::new("/dir/.git"),
&[
(&Path::new(A_TXT), GitFileStatus::Modified),
(&Path::new(B_TXT), GitFileStatus::Modified),
],
)
.await;
// Wait for buffer_local_a to receive it
deterministic.run_until_parked();
// Smoke test status reading
project_local.read_with(cx_a, |project, cx| {
assert_status(
&Path::new(A_TXT),
Some(GitFileStatus::Modified),
project,
cx,
);
assert_status(
&Path::new(B_TXT),
Some(GitFileStatus::Modified),
project,
cx,
);
});
project_remote.read_with(cx_b, |project, cx| {
assert_status(
&Path::new(A_TXT),
Some(GitFileStatus::Modified),
project,
cx,
);
assert_status(
&Path::new(B_TXT),
Some(GitFileStatus::Modified),
project,
cx,
);
});
// And synchronization while joining
let project_remote_c = client_c.build_remote_project(project_id, cx_c).await;
deterministic.run_until_parked();
project_remote_c.read_with(cx_c, |project, cx| {
assert_status(
&Path::new(A_TXT),
Some(GitFileStatus::Modified),
project,
cx,
);
assert_status(
&Path::new(B_TXT),
Some(GitFileStatus::Modified),
project,
cx,
);
});
}
#[gpui::test(iterations = 10)]
async fn test_fs_operations(
deterministic: Arc<Deterministic>,
@@ -3076,13 +3310,13 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
.await
.unwrap();
let (_, window_a) = cx_a.add_window(|_| EmptyView);
let editor_a = cx_a.add_view(&window_a, |cx| {
let (window_a, _) = cx_a.add_window(|_| EmptyView);
let editor_a = cx_a.add_view(window_a, |cx| {
Editor::for_buffer(buffer_a, Some(project_a), cx)
});
let mut editor_cx_a = EditorTestContext {
cx: cx_a,
window_id: window_a.id(),
window_id: window_a,
editor: editor_a,
};
@@ -3091,13 +3325,13 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
.await
.unwrap();
let (_, window_b) = cx_b.add_window(|_| EmptyView);
let editor_b = cx_b.add_view(&window_b, |cx| {
let (window_b, _) = cx_b.add_window(|_| EmptyView);
let editor_b = cx_b.add_view(window_b, |cx| {
Editor::for_buffer(buffer_b, Some(project_b), cx)
});
let mut editor_cx_b = EditorTestContext {
cx: cx_b,
window_id: window_b.id(),
window_id: window_b,
editor: editor_b,
};
@@ -3222,14 +3456,18 @@ async fn test_canceling_buffer_opening(
.unwrap();
// Open a buffer as client B but cancel after a random amount of time.
let buffer_b = project_b.update(cx_b, |p, cx| p.open_buffer_by_id(buffer_a.id() as u64, cx));
let buffer_b = project_b.update(cx_b, |p, cx| {
p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
});
deterministic.simulate_random_delay().await;
drop(buffer_b);
// Try opening the same buffer again as client B, and ensure we can
// still do it despite the cancellation above.
let buffer_b = project_b
.update(cx_b, |p, cx| p.open_buffer_by_id(buffer_a.id() as u64, cx))
.update(cx_b, |p, cx| {
p.open_buffer_by_id(buffer_a.read_with(cx_a, |a, _| a.remote_id()), cx)
})
.await
.unwrap();
buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
@@ -3832,8 +4070,8 @@ async fn test_collaborating_with_completion(
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
.await
.unwrap();
let (_, window_b) = cx_b.add_window(|_| EmptyView);
let editor_b = cx_b.add_view(&window_b, |cx| {
let (window_b, _) = cx_b.add_window(|_| EmptyView);
let editor_b = cx_b.add_view(window_b, |cx| {
Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
});
@@ -4458,7 +4696,10 @@ async fn test_project_search(
// Perform a search as the guest.
let results = project_b
.update(cx_b, |project, cx| {
project.search(SearchQuery::text("world", false, false), cx)
project.search(
SearchQuery::text("world", false, false, Vec::new(), Vec::new()),
cx,
)
})
.await
.unwrap();
@@ -6804,13 +7045,10 @@ async fn test_peers_following_each_other(
// Clients A and B follow each other in split panes
workspace_a.update(cx_a, |workspace, cx| {
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
let pane_a1 = pane_a1.clone();
cx.defer(move |workspace, _| {
assert_ne!(*workspace.active_pane(), pane_a1);
});
});
workspace_a
.update(cx_a, |workspace, cx| {
assert_ne!(*workspace.active_pane(), pane_a1);
let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
workspace.toggle_follow(leader_id, cx).unwrap()
})
@@ -6818,13 +7056,10 @@ async fn test_peers_following_each_other(
.unwrap();
workspace_b.update(cx_b, |workspace, cx| {
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
let pane_b1 = pane_b1.clone();
cx.defer(move |workspace, _| {
assert_ne!(*workspace.active_pane(), pane_b1);
});
});
workspace_b
.update(cx_b, |workspace, cx| {
assert_ne!(*workspace.active_pane(), pane_b1);
let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
workspace.toggle_follow(leader_id, cx).unwrap()
})

View File

@@ -8,12 +8,13 @@ use call::ActiveCall;
use client::RECEIVE_TIMEOUT;
use collections::BTreeMap;
use editor::Bias;
use fs::{FakeFs, Fs as _};
use fs::{repository::GitFileStatus, FakeFs, Fs as _};
use futures::StreamExt as _;
use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext};
use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16};
use lsp::FakeLanguageServer;
use parking_lot::Mutex;
use pretty_assertions::assert_eq;
use project::{search::SearchQuery, Project, ProjectPath};
use rand::{
distributions::{Alphanumeric, DistString},
@@ -716,7 +717,10 @@ async fn apply_client_operation(
);
let search = project.update(cx, |project, cx| {
project.search(SearchQuery::text(query, false, false), cx)
project.search(
SearchQuery::text(query, false, false, Vec::new(), Vec::new()),
cx,
)
});
drop(project);
let search = cx.background().spawn(async move {
@@ -760,31 +764,85 @@ async fn apply_client_operation(
}
}
ClientOperation::WriteGitIndex {
repo_path,
contents,
} => {
if !client.fs.directories().contains(&repo_path) {
return Err(TestError::Inapplicable);
}
log::info!(
"{}: writing git index for repo {:?}: {:?}",
client.username,
ClientOperation::GitOperation { operation } => match operation {
GitOperation::WriteGitIndex {
repo_path,
contents
);
contents,
} => {
if !client.fs.directories().contains(&repo_path) {
return Err(TestError::Inapplicable);
}
let dot_git_dir = repo_path.join(".git");
let contents = contents
.iter()
.map(|(path, contents)| (path.as_path(), contents.clone()))
.collect::<Vec<_>>();
if client.fs.metadata(&dot_git_dir).await?.is_none() {
client.fs.create_dir(&dot_git_dir).await?;
log::info!(
"{}: writing git index for repo {:?}: {:?}",
client.username,
repo_path,
contents
);
let dot_git_dir = repo_path.join(".git");
let contents = contents
.iter()
.map(|(path, contents)| (path.as_path(), contents.clone()))
.collect::<Vec<_>>();
if client.fs.metadata(&dot_git_dir).await?.is_none() {
client.fs.create_dir(&dot_git_dir).await?;
}
client.fs.set_index_for_repo(&dot_git_dir, &contents).await;
}
client.fs.set_index_for_repo(&dot_git_dir, &contents).await;
}
GitOperation::WriteGitBranch {
repo_path,
new_branch,
} => {
if !client.fs.directories().contains(&repo_path) {
return Err(TestError::Inapplicable);
}
log::info!(
"{}: writing git branch for repo {:?}: {:?}",
client.username,
repo_path,
new_branch
);
let dot_git_dir = repo_path.join(".git");
if client.fs.metadata(&dot_git_dir).await?.is_none() {
client.fs.create_dir(&dot_git_dir).await?;
}
client.fs.set_branch_name(&dot_git_dir, new_branch).await;
}
GitOperation::WriteGitStatuses {
repo_path,
statuses,
} => {
if !client.fs.directories().contains(&repo_path) {
return Err(TestError::Inapplicable);
}
log::info!(
"{}: writing git statuses for repo {:?}: {:?}",
client.username,
repo_path,
statuses
);
let dot_git_dir = repo_path.join(".git");
let statuses = statuses
.iter()
.map(|(path, val)| (path.as_path(), val.clone()))
.collect::<Vec<_>>();
if client.fs.metadata(&dot_git_dir).await?.is_none() {
client.fs.create_dir(&dot_git_dir).await?;
}
client
.fs
.set_status_for_repo(&dot_git_dir, statuses.as_slice())
.await;
}
},
}
Ok(())
}
@@ -859,6 +917,12 @@ fn check_consistency_between_clients(clients: &[(Rc<TestClient>, TestAppContext)
host_snapshot.abs_path(),
guest_project.remote_id(),
);
assert_eq!(guest_snapshot.repositories().collect::<Vec<_>>(), host_snapshot.repositories().collect::<Vec<_>>(),
"{} has different repositories than the host for worktree {:?} and project {:?}",
client.username,
host_snapshot.abs_path(),
guest_project.remote_id(),
);
assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(),
"{} has different scan id than the host for worktree {:?} and project {:?}",
client.username,
@@ -1147,10 +1211,25 @@ enum ClientOperation {
is_dir: bool,
content: String,
},
GitOperation {
operation: GitOperation,
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
enum GitOperation {
WriteGitIndex {
repo_path: PathBuf,
contents: Vec<(PathBuf, String)>,
},
WriteGitBranch {
repo_path: PathBuf,
new_branch: Option<String>,
},
WriteGitStatuses {
repo_path: PathBuf,
statuses: Vec<(PathBuf, GitFileStatus)>,
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -1663,38 +1742,10 @@ impl TestPlan {
}
}
// Update a git index
// Update a git related action
91..=95 => {
let repo_path = client
.fs
.directories()
.choose(&mut self.rng)
.unwrap()
.clone();
let mut file_paths = client
.fs
.files()
.into_iter()
.filter(|path| path.starts_with(&repo_path))
.collect::<Vec<_>>();
let count = self.rng.gen_range(0..=file_paths.len());
file_paths.shuffle(&mut self.rng);
file_paths.truncate(count);
let mut contents = Vec::new();
for abs_child_file_path in &file_paths {
let child_file_path = abs_child_file_path
.strip_prefix(&repo_path)
.unwrap()
.to_path_buf();
let new_base = Alphanumeric.sample_string(&mut self.rng, 16);
contents.push((child_file_path, new_base));
}
break ClientOperation::WriteGitIndex {
repo_path,
contents,
break ClientOperation::GitOperation {
operation: self.generate_git_operation(client),
};
}
@@ -1732,6 +1783,86 @@ impl TestPlan {
})
}
fn generate_git_operation(&mut self, client: &TestClient) -> GitOperation {
fn generate_file_paths(
repo_path: &Path,
rng: &mut StdRng,
client: &TestClient,
) -> Vec<PathBuf> {
let mut paths = client
.fs
.files()
.into_iter()
.filter(|path| path.starts_with(repo_path))
.collect::<Vec<_>>();
let count = rng.gen_range(0..=paths.len());
paths.shuffle(rng);
paths.truncate(count);
paths
.iter()
.map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf())
.collect::<Vec<_>>()
}
let repo_path = client
.fs
.directories()
.choose(&mut self.rng)
.unwrap()
.clone();
match self.rng.gen_range(0..100_u32) {
0..=25 => {
let file_paths = generate_file_paths(&repo_path, &mut self.rng, client);
let contents = file_paths
.into_iter()
.map(|path| (path, Alphanumeric.sample_string(&mut self.rng, 16)))
.collect();
GitOperation::WriteGitIndex {
repo_path,
contents,
}
}
26..=63 => {
let new_branch = (self.rng.gen_range(0..10) > 3)
.then(|| Alphanumeric.sample_string(&mut self.rng, 8));
GitOperation::WriteGitBranch {
repo_path,
new_branch,
}
}
64..=100 => {
let file_paths = generate_file_paths(&repo_path, &mut self.rng, client);
let statuses = file_paths
.into_iter()
.map(|paths| {
(
paths,
match self.rng.gen_range(0..3_u32) {
0 => GitFileStatus::Added,
1 => GitFileStatus::Modified,
2 => GitFileStatus::Conflict,
_ => unreachable!(),
},
)
})
.collect::<Vec<_>>();
GitOperation::WriteGitStatuses {
repo_path,
statuses,
}
}
_ => unreachable!(),
}
}
fn next_root_dir_name(&mut self, user_id: UserId) -> String {
let user_ix = self
.users

View File

@@ -14,8 +14,8 @@ use gpui::{
geometry::{rect::RectF, vector::vec2f, PathBuilder},
json::{self, ToJson},
platform::{CursorStyle, MouseButton},
AppContext, Entity, ImageData, ModelHandle, SceneBuilder, Subscription, View, ViewContext,
ViewHandle, WeakViewHandle,
AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use project::Project;
use settings::Settings;
@@ -24,6 +24,8 @@ use theme::{AvatarStyle, Theme};
use util::ResultExt;
use workspace::{FollowNextCollaborator, Workspace};
const MAX_TITLE_LENGTH: usize = 75;
actions!(
collab,
[
@@ -68,29 +70,11 @@ impl View for CollabTitlebarItem {
};
let project = self.project.read(cx);
let mut project_title = String::new();
for (i, name) in project.worktree_root_names(cx).enumerate() {
if i > 0 {
project_title.push_str(", ");
}
project_title.push_str(name);
}
if project_title.is_empty() {
project_title = "empty project".to_owned();
}
let theme = cx.global::<Settings>().theme.clone();
let mut left_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())
.contained()
.with_margin_right(theme.workspace.titlebar.item_spacing)
.aligned()
.left(),
);
left_container.add_child(self.collect_title_root_names(&project, theme.clone(), cx));
let user = self.user_store.read(cx).current_user();
let peer_id = self.client.peer_id();
@@ -120,7 +104,21 @@ impl View for CollabTitlebarItem {
Stack::new()
.with_child(left_container)
.with_child(right_container.aligned().right())
.with_child(
Flex::row()
.with_child(
right_container.contained().with_background_color(
theme
.workspace
.titlebar
.container
.background_color
.unwrap_or_else(|| Color::transparent_black()),
),
)
.aligned()
.right(),
)
.into_any()
}
}
@@ -137,6 +135,7 @@ impl CollabTitlebarItem {
let active_call = ActiveCall::global(cx);
let mut subscriptions = Vec::new();
subscriptions.push(cx.observe(workspace_handle, |_, _, cx| cx.notify()));
subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify()));
subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
subscriptions.push(cx.observe_window_activation(|this, active, cx| {
this.window_activation_changed(active, cx)
@@ -165,6 +164,7 @@ impl CollabTitlebarItem {
}),
);
let view_id = cx.view_id();
Self {
workspace: workspace.weak_handle(),
project,
@@ -172,7 +172,7 @@ impl CollabTitlebarItem {
client,
contacts_popover: None,
user_menu: cx.add_view(|cx| {
let mut menu = ContextMenu::new(cx);
let mut menu = ContextMenu::new(view_id, cx);
menu.set_position_mode(OverlayPositionMode::Local);
menu
}),
@@ -180,6 +180,63 @@ impl CollabTitlebarItem {
}
}
fn collect_title_root_names(
&self,
project: &Project,
theme: Arc<Theme>,
cx: &ViewContext<Self>,
) -> AnyElement<Self> {
let names_and_branches = project.visible_worktrees(cx).map(|worktree| {
let worktree = worktree.read(cx);
(worktree.root_name(), worktree.root_git_entry())
});
fn push_str(buffer: &mut String, index: &mut usize, str: &str) {
buffer.push_str(str);
*index += str.chars().count();
}
let mut indices = Vec::new();
let mut index = 0;
let mut title = String::new();
let mut names_and_branches = names_and_branches.peekable();
while let Some((name, entry)) = names_and_branches.next() {
let pre_index = index;
push_str(&mut title, &mut index, name);
indices.extend((pre_index..index).into_iter());
if let Some(branch) = entry.and_then(|entry| entry.branch()) {
push_str(&mut title, &mut index, "/");
push_str(&mut title, &mut index, &branch);
}
if names_and_branches.peek().is_some() {
push_str(&mut title, &mut index, ", ");
if index >= MAX_TITLE_LENGTH {
title.push_str("");
break;
}
}
}
let text_style = theme.workspace.titlebar.title.clone();
let item_spacing = theme.workspace.titlebar.item_spacing;
let mut highlight = text_style.clone();
highlight.color = theme.workspace.titlebar.highlight_color;
let style = LabelStyle {
text: text_style,
highlight_text: Some(highlight),
};
Label::new(title, style)
.with_highlights(indices)
.contained()
.with_margin_right(item_spacing)
.aligned()
.left()
.into_any_named("title-with-git-information")
}
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
let project = if active {
Some(self.project.clone())
@@ -865,7 +922,7 @@ impl Element<CollabTitlebarItem> for AvatarRibbon {
&mut self,
constraint: gpui::SizeConstraint,
_: &mut CollabTitlebarItem,
_: &mut ViewContext<CollabTitlebarItem>,
_: &mut LayoutContext<CollabTitlebarItem>,
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
(constraint.max, ())
}

View File

@@ -1306,10 +1306,9 @@ impl View for ContactList {
"ContactList"
}
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.add_identifier("menu");
cx
fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
Self::reset_to_default_keymap_context(keymap);
keymap.add_identifier("menu");
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {

View File

@@ -7,7 +7,7 @@ use gpui::{
},
json::ToJson,
serde_json::{self, json},
AnyElement, Axis, Element, SceneBuilder, ViewContext,
AnyElement, Axis, Element, LayoutContext, SceneBuilder, ViewContext,
};
use crate::CollabTitlebarItem;
@@ -34,7 +34,7 @@ impl Element<CollabTitlebarItem> for FacePile {
&mut self,
constraint: gpui::SizeConstraint,
view: &mut CollabTitlebarItem,
cx: &mut ViewContext<CollabTitlebarItem>,
cx: &mut LayoutContext<CollabTitlebarItem>,
) -> (Vector2F, Self::LayoutState) {
debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);

View File

@@ -2,7 +2,7 @@ use collections::CommandPaletteFilter;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions, elements::*, keymap_matcher::Keystroke, Action, AppContext, Element, MouseState,
ViewContext, WindowContext,
ViewContext,
};
use picker::{Picker, PickerDelegate, PickerEvent};
use settings::Settings;
@@ -41,47 +41,17 @@ struct Command {
keystrokes: Vec<Keystroke>,
}
fn toggle_command_palette(_: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
let workspace = cx.handle();
let focused_view_id = cx.focused_view_id().unwrap_or_else(|| workspace.id());
cx.window_context().defer(move |cx| {
// Build the delegate before the workspace is put on the stack so we can find it when
// computing the actions. We should really not allow available_actions to be called
// if it's not reliable however.
let delegate = CommandPaletteDelegate::new(focused_view_id, cx);
workspace.update(cx, |workspace, cx| {
workspace.toggle_modal(cx, |_, cx| cx.add_view(|cx| Picker::new(delegate, cx)));
})
fn toggle_command_palette(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
let focused_view_id = cx.focused_view_id().unwrap_or_else(|| cx.view_id());
workspace.toggle_modal(cx, |_, cx| {
cx.add_view(|cx| Picker::new(CommandPaletteDelegate::new(focused_view_id), cx))
});
}
impl CommandPaletteDelegate {
pub fn new(focused_view_id: usize, cx: &mut WindowContext) -> Self {
let actions = cx
.available_actions(focused_view_id)
.filter_map(|(name, action, bindings)| {
if cx.has_global::<CommandPaletteFilter>() {
let filter = cx.global::<CommandPaletteFilter>();
if filter.filtered_namespaces.contains(action.namespace()) {
return None;
}
}
Some(Command {
name: humanize_action_name(name),
action,
keystrokes: bindings
.iter()
.map(|binding| binding.keystrokes())
.last()
.map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
})
})
.collect();
pub fn new(focused_view_id: usize) -> Self {
Self {
actions,
actions: Default::default(),
matches: vec![],
selected_ix: 0,
focused_view_id,
@@ -111,17 +81,46 @@ impl PickerDelegate for CommandPaletteDelegate {
query: String,
cx: &mut ViewContext<Picker<Self>>,
) -> gpui::Task<()> {
let candidates = self
.actions
.iter()
.enumerate()
.map(|(ix, command)| StringMatchCandidate {
id: ix,
string: command.name.to_string(),
char_bag: command.name.chars().collect(),
})
.collect::<Vec<_>>();
let window_id = cx.window_id();
let view_id = self.focused_view_id;
cx.spawn(move |picker, mut cx| async move {
let actions = cx
.available_actions(window_id, view_id)
.into_iter()
.filter_map(|(name, action, bindings)| {
let filtered = cx.read(|cx| {
if cx.has_global::<CommandPaletteFilter>() {
let filter = cx.global::<CommandPaletteFilter>();
filter.filtered_namespaces.contains(action.namespace())
} else {
false
}
});
if filtered {
None
} else {
Some(Command {
name: humanize_action_name(name),
action,
keystrokes: bindings
.iter()
.map(|binding| binding.keystrokes())
.last()
.map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
})
}
})
.collect::<Vec<_>>();
let candidates = actions
.iter()
.enumerate()
.map(|(ix, command)| StringMatchCandidate {
id: ix,
string: command.name.to_string(),
char_bag: command.name.chars().collect(),
})
.collect::<Vec<_>>();
let matches = if query.is_empty() {
candidates
.into_iter()
@@ -147,6 +146,7 @@ impl PickerDelegate for CommandPaletteDelegate {
picker
.update(&mut cx, |picker, _| {
let delegate = picker.delegate_mut();
delegate.actions = actions;
delegate.matches = matches;
if delegate.matches.is_empty() {
delegate.selected_ix = 0;
@@ -304,8 +304,8 @@ mod tests {
});
let project = Project::test(app_state.fs.clone(), [], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let editor = cx.add_view(&workspace, |cx| {
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let editor = cx.add_view(window_id, |cx| {
let mut editor = Editor::single_line(None, cx);
editor.set_text("abc", cx);
editor

View File

@@ -126,7 +126,6 @@ pub struct ContextMenu {
selected_index: Option<usize>,
visible: bool,
previously_focused_view_id: Option<usize>,
clicked: bool,
parent_view_id: usize,
_actions_observation: Subscription,
}
@@ -140,10 +139,9 @@ impl View for ContextMenu {
"ContextMenu"
}
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.add_identifier("menu");
cx
fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
Self::reset_to_default_keymap_context(keymap);
keymap.add_identifier("menu");
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
@@ -178,9 +176,7 @@ impl View for ContextMenu {
}
impl ContextMenu {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
let parent_view_id = cx.parent().unwrap();
pub fn new(parent_view_id: usize, cx: &mut ViewContext<Self>) -> Self {
Self {
show_count: 0,
anchor_position: Default::default(),
@@ -190,7 +186,6 @@ impl ContextMenu {
selected_index: Default::default(),
visible: Default::default(),
previously_focused_view_id: Default::default(),
clicked: false,
parent_view_id,
_actions_observation: cx.observe_actions(Self::action_dispatched),
}
@@ -206,18 +201,14 @@ impl ContextMenu {
.iter()
.position(|item| item.action_id() == Some(action_id))
{
if self.clicked {
self.cancel(&Default::default(), cx);
} else {
self.selected_index = Some(ix);
cx.notify();
cx.spawn(|this, mut cx| async move {
cx.background().timer(Duration::from_millis(50)).await;
this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx))?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
self.selected_index = Some(ix);
cx.notify();
cx.spawn(|this, mut cx| async move {
cx.background().timer(Duration::from_millis(50)).await;
this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx))?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
}
@@ -257,7 +248,6 @@ impl ContextMenu {
self.items.clear();
self.visible = false;
self.selected_index.take();
self.clicked = false;
cx.notify();
}
@@ -457,7 +447,7 @@ impl ContextMenu {
.on_up(MouseButton::Left, |_, _, _| {}) // Capture these events
.on_down(MouseButton::Left, |_, _, _| {}) // Capture these events
.on_click(MouseButton::Left, move |_, menu, cx| {
menu.clicked = true;
menu.cancel(&Default::default(), cx);
let window_id = cx.window_id();
match &action {
ContextMenuItemAction::Action(action) => {

View File

@@ -126,7 +126,7 @@ impl CopilotServer {
struct RunningCopilotServer {
lsp: Arc<LanguageServer>,
sign_in_status: SignInStatus,
registered_buffers: HashMap<usize, RegisteredBuffer>,
registered_buffers: HashMap<u64, RegisteredBuffer>,
}
#[derive(Clone, Debug)]
@@ -162,7 +162,7 @@ impl Status {
}
struct RegisteredBuffer {
id: usize,
id: u64,
uri: lsp::Url,
language_id: String,
snapshot: BufferSnapshot,
@@ -258,7 +258,7 @@ impl RegisteredBuffer {
#[derive(Debug)]
pub struct Completion {
uuid: String,
pub uuid: String,
pub range: Range<Anchor>,
pub text: String,
}
@@ -267,7 +267,7 @@ pub struct Copilot {
http: Arc<dyn HttpClient>,
node_runtime: Arc<NodeRuntime>,
server: CopilotServer,
buffers: HashMap<usize, WeakModelHandle<Buffer>>,
buffers: HashMap<u64, WeakModelHandle<Buffer>>,
}
impl Entity for Copilot {
@@ -461,14 +461,12 @@ impl Copilot {
pub fn sign_in(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if let CopilotServer::Running(server) = &mut self.server {
let task = match &server.sign_in_status {
SignInStatus::Authorized { .. } | SignInStatus::Unauthorized { .. } => {
Task::ready(Ok(())).shared()
}
SignInStatus::Authorized { .. } => Task::ready(Ok(())).shared(),
SignInStatus::SigningIn { task, .. } => {
cx.notify();
task.clone()
}
SignInStatus::SignedOut => {
SignInStatus::SignedOut | SignInStatus::Unauthorized { .. } => {
let lsp = server.lsp.clone();
let task = cx
.spawn(|this, mut cx| async move {
@@ -582,7 +580,7 @@ impl Copilot {
}
pub fn register_buffer(&mut self, buffer: &ModelHandle<Buffer>, cx: &mut ModelContext<Self>) {
let buffer_id = buffer.id();
let buffer_id = buffer.read(cx).remote_id();
self.buffers.insert(buffer_id, buffer.downgrade());
if let CopilotServer::Running(RunningCopilotServer {
@@ -596,7 +594,8 @@ impl Copilot {
return;
}
registered_buffers.entry(buffer.id()).or_insert_with(|| {
let buffer_id = buffer.read(cx).remote_id();
registered_buffers.entry(buffer_id).or_insert_with(|| {
let uri: lsp::Url = uri_for_buffer(buffer, cx);
let language_id = id_for_language(buffer.read(cx).language());
let snapshot = buffer.read(cx).snapshot();
@@ -641,7 +640,8 @@ impl Copilot {
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Ok(server) = self.server.as_running() {
if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.id()) {
let buffer_id = buffer.read(cx).remote_id();
if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer_id) {
match event {
language::Event::Edited => {
let _ = registered_buffer.report_changes(&buffer, cx);
@@ -695,7 +695,7 @@ impl Copilot {
Ok(())
}
fn unregister_buffer(&mut self, buffer_id: usize) {
fn unregister_buffer(&mut self, buffer_id: u64) {
if let Ok(server) = self.server.as_running() {
if let Some(buffer) = server.registered_buffers.remove(&buffer_id) {
server
@@ -800,7 +800,8 @@ impl Copilot {
Err(error) => return Task::ready(Err(error)),
};
let lsp = server.lsp.clone();
let registered_buffer = server.registered_buffers.get_mut(&buffer.id()).unwrap();
let buffer_id = buffer.read(cx).remote_id();
let registered_buffer = server.registered_buffers.get_mut(&buffer_id).unwrap();
let snapshot = registered_buffer.report_changes(buffer, cx);
let buffer = buffer.read(cx);
let uri = registered_buffer.uri.clone();
@@ -919,7 +920,9 @@ fn uri_for_buffer(buffer: &ModelHandle<Buffer>, cx: &AppContext) -> lsp::Url {
if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
lsp::Url::from_file_path(file.abs_path(cx)).unwrap()
} else {
format!("buffer://{}", buffer.id()).parse().unwrap()
format!("buffer://{}", buffer.read(cx).remote_id())
.parse()
.unwrap()
}
}
@@ -1167,7 +1170,7 @@ mod tests {
}
fn mtime(&self) -> std::time::SystemTime {
todo!()
unimplemented!()
}
fn path(&self) -> &Arc<Path> {
@@ -1175,23 +1178,23 @@ mod tests {
}
fn full_path(&self, _: &AppContext) -> PathBuf {
todo!()
unimplemented!()
}
fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
todo!()
unimplemented!()
}
fn is_deleted(&self) -> bool {
todo!()
unimplemented!()
}
fn as_any(&self) -> &dyn std::any::Any {
todo!()
unimplemented!()
}
fn to_proto(&self) -> rpc::proto::File {
todo!()
unimplemented!()
}
}
@@ -1201,7 +1204,7 @@ mod tests {
}
fn load(&self, _: &AppContext) -> Task<Result<String>> {
todo!()
unimplemented!()
}
fn buffer_reloaded(
@@ -1213,7 +1216,7 @@ mod tests {
_: std::time::SystemTime,
_: &mut AppContext,
) {
todo!()
unimplemented!()
}
}
}

View File

@@ -144,8 +144,9 @@ impl View for CopilotButton {
impl CopilotButton {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
let button_view_id = cx.view_id();
let menu = cx.add_view(|cx| {
let mut menu = ContextMenu::new(cx);
let mut menu = ContextMenu::new(button_view_id, cx);
menu.set_position_mode(OverlayPositionMode::Local);
menu
});
@@ -327,10 +328,9 @@ async fn configure_disabled_globs(
cx.global::<Settings>()
.copilot
.disabled_globs
.clone()
.iter()
.map(|glob| glob.as_str().to_string())
.collect::<Vec<_>>()
.collect()
});
if let Some(path_to_disable) = &path_to_disable {

View File

@@ -852,7 +852,7 @@ mod tests {
let language_server_id = LanguageServerId(0);
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
// Create some diagnostics
project.update(cx, |project, cx| {
@@ -939,7 +939,7 @@ mod tests {
});
// Open the project diagnostics view while there are already diagnostics.
let view = cx.add_view(&workspace, |cx| {
let view = cx.add_view(window_id, |cx| {
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
});
@@ -1244,9 +1244,9 @@ mod tests {
let server_id_1 = LanguageServerId(100);
let server_id_2 = LanguageServerId(101);
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let view = cx.add_view(&workspace, |cx| {
let view = cx.add_view(window_id, |cx| {
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
});

View File

@@ -199,7 +199,7 @@ impl<V: View> DragAndDrop<V> {
return None;
}
let position = position - region_offset;
let position = (position - region_offset).round();
Some(
Overlay::new(
MouseEventHandler::<DraggedElementHandler, V>::new(

View File

@@ -833,10 +833,7 @@ impl<'a> Iterator for BlockChunks<'a> {
return Some(Chunk {
text: unsafe { std::str::from_utf8_unchecked(&NEWLINES[..line_count as usize]) },
syntax_highlight_id: None,
highlight_style: None,
diagnostic_severity: None,
is_unnecessary: false,
..Default::default()
});
}

View File

@@ -1065,13 +1065,11 @@ impl<'a> Iterator for FoldChunks<'a> {
self.output_offset += output_text.len();
return Some(Chunk {
text: output_text,
syntax_highlight_id: None,
highlight_style: self.ellipses_color.map(|color| HighlightStyle {
color: Some(color),
..Default::default()
}),
diagnostic_severity: None,
is_unnecessary: false,
..Default::default()
});
}

View File

@@ -531,10 +531,8 @@ impl<'a> Iterator for SuggestionChunks<'a> {
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,
..Default::default()
});
} else {
self.suggestion_chunks = None;

View File

@@ -268,6 +268,7 @@ impl TabSnapshot {
tab_size: self.tab_size,
chunk: Chunk {
text: &SPACES[0..(to_next_stop as usize)],
is_tab: true,
..Default::default()
},
inside_leading_tab: to_next_stop > 0,
@@ -545,6 +546,7 @@ impl<'a> Iterator for TabChunks<'a> {
self.output_position = next_output_position;
return Some(Chunk {
text: &SPACES[..len as usize],
is_tab: true,
..self.chunk
});
}
@@ -654,6 +656,56 @@ mod tests {
assert_eq!(tab_snapshot.text(), input);
}
#[gpui::test]
fn test_marking_tabs(cx: &mut gpui::AppContext) {
let input = "\t \thello";
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 (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
assert_eq!(
chunks(&tab_snapshot, TabPoint::zero()),
vec![
(" ".to_string(), true),
(" ".to_string(), false),
(" ".to_string(), true),
("hello".to_string(), false),
]
);
assert_eq!(
chunks(&tab_snapshot, TabPoint::new(0, 2)),
vec![
(" ".to_string(), true),
(" ".to_string(), false),
(" ".to_string(), true),
("hello".to_string(), false),
]
);
fn chunks(snapshot: &TabSnapshot, start: TabPoint) -> Vec<(String, bool)> {
let mut chunks = Vec::new();
let mut was_tab = false;
let mut text = String::new();
for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None) {
if chunk.is_tab != was_tab {
if !text.is_empty() {
chunks.push((mem::take(&mut text), was_tab));
}
was_tab = chunk.is_tab;
}
text.push_str(chunk.text);
}
if !text.is_empty() {
chunks.push((text, was_tab));
}
chunks
}
}
#[gpui::test(iterations = 100)]
fn test_random_tabs(cx: &mut gpui::AppContext, mut rng: StdRng) {
let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();

View File

@@ -516,6 +516,15 @@ pub struct EditorSnapshot {
ongoing_scroll: OngoingScroll,
}
impl EditorSnapshot {
fn has_scrollbar_info(&self) -> bool {
self.buffer_snapshot
.git_diff_hunks_in_range(0..self.max_point().row(), false)
.next()
.is_some()
}
}
#[derive(Clone, Debug)]
struct SelectionHistoryEntry {
selections: Arc<[Selection<Anchor>]>,
@@ -1227,6 +1236,7 @@ impl Editor {
get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
cx: &mut ViewContext<Self>,
) -> Self {
let editor_view_id = cx.view_id();
let display_map = cx.add_model(|cx| {
let settings = cx.global::<Settings>();
let style = build_style(&*settings, get_field_editor_theme.as_deref(), None, cx);
@@ -1247,6 +1257,16 @@ impl Editor {
let soft_wrap_mode_override =
(mode == EditorMode::SingleLine).then(|| settings::SoftWrap::None);
let mut project_subscription = None;
if mode == EditorMode::Full && buffer.read(cx).is_singleton() {
if let Some(project) = project.as_ref() {
project_subscription = Some(cx.observe(project, |_, _, cx| {
cx.emit(Event::TitleChanged);
}))
}
}
let mut this = Self {
handle: cx.weak_handle(),
buffer: buffer.clone(),
@@ -1274,7 +1294,8 @@ impl Editor {
background_highlights: Default::default(),
nav_history: None,
context_menu: None,
mouse_context_menu: cx.add_view(context_menu::ContextMenu::new),
mouse_context_menu: cx
.add_view(|cx| context_menu::ContextMenu::new(editor_view_id, cx)),
completion_tasks: Default::default(),
next_completion_id: 0,
available_code_actions: Default::default(),
@@ -1302,6 +1323,11 @@ impl Editor {
cx.observe_global::<Settings, _>(Self::settings_changed),
],
};
if let Some(project_subscription) = project_subscription {
this._subscriptions.push(project_subscription);
}
this.end_selection(cx);
this.scroll_manager.show_scrollbar(cx);
@@ -1313,7 +1339,7 @@ impl Editor {
cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars));
}
this.report_editor_event("open", cx);
this.report_editor_event("open", None, cx);
this
}
@@ -1425,13 +1451,19 @@ impl Editor {
}
}
pub fn set_keymap_context_layer<Tag: 'static>(&mut self, context: KeymapContext) {
pub fn set_keymap_context_layer<Tag: 'static>(
&mut self,
context: KeymapContext,
cx: &mut ViewContext<Self>,
) {
self.keymap_context_layers
.insert(TypeId::of::<Tag>(), context);
cx.notify();
}
pub fn remove_keymap_context_layer<Tag: 'static>(&mut self) {
pub fn remove_keymap_context_layer<Tag: 'static>(&mut self, cx: &mut ViewContext<Self>) {
self.keymap_context_layers.remove(&TypeId::of::<Tag>());
cx.notify();
}
pub fn set_input_enabled(&mut self, input_enabled: bool) {
@@ -2588,7 +2620,7 @@ impl Editor {
let old_text = buffer.text_for_range(old_range.clone()).collect::<String>();
let newest_selection = self.selections.newest_anchor();
if newest_selection.start.buffer_id != Some(buffer_handle.id()) {
if newest_selection.start.buffer_id != Some(buffer_handle.read(cx).remote_id()) {
return None;
}
@@ -2796,7 +2828,7 @@ impl Editor {
),
);
}
multibuffer.push_transaction(entries.iter().map(|(b, t)| (b, t)));
multibuffer.push_transaction(entries.iter().map(|(b, t)| (b, t)), cx);
multibuffer
});
@@ -3074,6 +3106,8 @@ impl Editor {
copilot
.update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
.detach_and_log_err(cx);
self.report_copilot_event(Some(completion.uuid.clone()), true, cx)
}
self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx);
cx.notify();
@@ -3091,6 +3125,8 @@ impl Editor {
copilot.discard_completions(&self.copilot_state.completions, cx)
})
.detach_and_log_err(cx);
self.report_copilot_event(None, false, cx)
}
self.display_map
@@ -5674,28 +5710,30 @@ impl Editor {
}
} else if !definitions.is_empty() {
let replica_id = self.replica_id(cx);
let title = definitions
.iter()
.find(|definition| definition.origin.is_some())
.and_then(|definition| {
definition.origin.as_ref().map(|origin| {
let buffer = origin.buffer.read(cx);
format!(
"Definitions for {}",
buffer
.text_for_range(origin.range.clone())
.collect::<String>()
)
cx.window_context().defer(move |cx| {
let title = definitions
.iter()
.find(|definition| definition.origin.is_some())
.and_then(|definition| {
definition.origin.as_ref().map(|origin| {
let buffer = origin.buffer.read(cx);
format!(
"Definitions for {}",
buffer
.text_for_range(origin.range.clone())
.collect::<String>()
)
})
})
})
.unwrap_or("Definitions".to_owned());
let locations = definitions
.into_iter()
.map(|definition| definition.target)
.collect();
workspace.update(cx, |workspace, cx| {
Self::open_locations_in_multibuffer(workspace, locations, replica_id, title, cx)
})
.unwrap_or("Definitions".to_owned());
let locations = definitions
.into_iter()
.map(|definition| definition.target)
.collect();
workspace.update(cx, |workspace, cx| {
Self::open_locations_in_multibuffer(workspace, locations, replica_id, title, cx)
});
});
}
}
@@ -5756,7 +5794,7 @@ impl Editor {
cx: &mut ViewContext<Workspace>,
) {
// If there are multiple definitions, open them in a multibuffer
locations.sort_by_key(|location| location.buffer.id());
locations.sort_by_key(|location| location.buffer.read(cx).remote_id());
let mut locations = locations.into_iter().peekable();
let mut ranges_to_highlight = Vec::new();
@@ -6051,7 +6089,7 @@ impl Editor {
buffer.update(&mut cx, |buffer, cx| {
if let Some(transaction) = transaction {
if !buffer.is_singleton() {
buffer.push_transaction(&transaction.0);
buffer.push_transaction(&transaction.0, cx);
}
}
@@ -6851,44 +6889,84 @@ impl Editor {
.collect()
}
fn report_editor_event(&self, name: &'static str, cx: &AppContext) {
if let Some((project, file)) = self.project.as_ref().zip(
self.buffer
.read(cx)
.as_singleton()
.and_then(|b| b.read(cx).file()),
) {
let settings = cx.global::<Settings>();
fn report_copilot_event(
&self,
suggestion_id: Option<String>,
suggestion_accepted: bool,
cx: &AppContext,
) {
let Some(project) = &self.project else {
return
};
let extension = Path::new(file.file_name(cx))
.extension()
.and_then(|e| e.to_str());
let telemetry = project.read(cx).client().telemetry().clone();
telemetry.report_mixpanel_event(
// If None, we are either getting suggestions in a new, unsaved file, or in a file without an extension
let file_extension = self
.buffer
.read(cx)
.as_singleton()
.and_then(|b| b.read(cx).file())
.and_then(|file| Path::new(file.file_name(cx)).extension())
.and_then(|e| e.to_str())
.map(|a| a.to_string());
let telemetry = project.read(cx).client().telemetry().clone();
let telemetry_settings = cx.global::<Settings>().telemetry();
let event = ClickhouseEvent::Copilot {
suggestion_id,
suggestion_accepted,
file_extension,
};
telemetry.report_clickhouse_event(event, telemetry_settings);
}
fn report_editor_event(
&self,
name: &'static str,
file_extension: Option<String>,
cx: &AppContext,
) {
let Some(project) = &self.project else {
return
};
// If None, we are in a file without an extension
let file_extension = file_extension.or(self
.buffer
.read(cx)
.as_singleton()
.and_then(|b| b.read(cx).file())
.and_then(|file| Path::new(file.file_name(cx)).extension())
.and_then(|e| e.to_str())
.map(|a| a.to_string()));
let settings = cx.global::<Settings>();
let telemetry = project.read(cx).client().telemetry().clone();
telemetry.report_mixpanel_event(
match name {
"open" => "open editor",
"save" => "save editor",
_ => name,
},
json!({ "File Extension": extension, "Vim Mode": settings.vim_mode, "In Clickhouse": true }),
json!({ "File Extension": file_extension, "Vim Mode": settings.vim_mode, "In Clickhouse": true }),
settings.telemetry(),
);
let event = ClickhouseEvent::Editor {
file_extension: extension.map(ToString::to_string),
vim_mode: settings.vim_mode,
operation: name,
copilot_enabled: settings.features.copilot,
copilot_enabled_for_language: settings.show_copilot_suggestions(
self.language_at(0, cx)
.map(|language| language.name())
.as_deref(),
self.file_at(0, cx)
.map(|file| file.path().clone())
.as_deref(),
),
};
telemetry.report_clickhouse_event(event, settings.telemetry())
}
let event = ClickhouseEvent::Editor {
file_extension,
vim_mode: settings.vim_mode,
operation: name,
copilot_enabled: settings.features.copilot,
copilot_enabled_for_language: settings.show_copilot_suggestions(
self.language_at(0, cx)
.map(|language| language.name())
.as_deref(),
self.file_at(0, cx)
.map(|file| file.path().clone())
.as_deref(),
),
};
telemetry.report_clickhouse_event(event, settings.telemetry())
}
/// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines,
@@ -7157,28 +7235,26 @@ impl View for Editor {
false
}
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut context = Self::default_keymap_context();
fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
Self::reset_to_default_keymap_context(keymap);
let mode = match self.mode {
EditorMode::SingleLine => "single_line",
EditorMode::AutoHeight { .. } => "auto_height",
EditorMode::Full => "full",
};
context.add_key("mode", mode);
keymap.add_key("mode", mode);
if self.pending_rename.is_some() {
context.add_identifier("renaming");
keymap.add_identifier("renaming");
}
match self.context_menu.as_ref() {
Some(ContextMenu::Completions(_)) => context.add_identifier("showing_completions"),
Some(ContextMenu::CodeActions(_)) => context.add_identifier("showing_code_actions"),
Some(ContextMenu::Completions(_)) => keymap.add_identifier("showing_completions"),
Some(ContextMenu::CodeActions(_)) => keymap.add_identifier("showing_code_actions"),
None => {}
}
for layer in self.keymap_context_layers.values() {
context.extend(layer);
keymap.extend(layer);
}
context
}
fn text_for_range(&self, range_utf16: Range<usize>, cx: &AppContext) -> Option<String> {

View File

@@ -493,9 +493,9 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
cx.add_view(&pane, |cx| {
cx.add_view(window_id, |cx| {
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
let mut editor = build_editor(buffer.clone(), cx);
let handle = cx.handle();
@@ -5459,10 +5459,12 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
});
let is_still_following = Rc::new(RefCell::new(true));
let follower_edit_event_count = Rc::new(RefCell::new(0));
let pending_update = Rc::new(RefCell::new(None));
follower.update(cx, {
let update = pending_update.clone();
let is_still_following = is_still_following.clone();
let follower_edit_event_count = follower_edit_event_count.clone();
|_, cx| {
cx.subscribe(&leader, move |_, leader, event, cx| {
leader
@@ -5475,6 +5477,9 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
if Editor::should_unfollow_on_event(event, cx) {
*is_still_following.borrow_mut() = false;
}
if let Event::BufferEdited = event {
*follower_edit_event_count.borrow_mut() += 1;
}
})
.detach();
}
@@ -5494,6 +5499,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
assert_eq!(follower.selections.ranges(cx), vec![1..1]);
});
assert_eq!(*is_still_following.borrow(), true);
assert_eq!(*follower_edit_event_count.borrow(), 0);
// Update the scroll position only
leader.update(cx, |leader, cx| {
@@ -5510,6 +5516,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
vec2f(1.5, 3.5)
);
assert_eq!(*is_still_following.borrow(), true);
assert_eq!(*follower_edit_event_count.borrow(), 0);
// Update the selections and scroll position. The follower's scroll position is updated
// via autoscroll, not via the leader's exact scroll position.

View File

@@ -21,7 +21,7 @@ use git::diff::DiffHunkStatus;
use gpui::{
color::Color,
elements::*,
fonts::{HighlightStyle, Underline},
fonts::{HighlightStyle, TextStyle, Underline},
geometry::{
rect::RectF,
vector::{vec2f, Vector2F},
@@ -30,16 +30,17 @@ use gpui::{
json::{self, ToJson},
platform::{CursorStyle, Modifiers, MouseButton, MouseButtonEvent, MouseMovedEvent},
text_layout::{self, Line, RunStyle, TextLayoutCache},
AnyElement, Axis, Border, CursorRegion, Element, EventContext, MouseRegion, Quad, SceneBuilder,
SizeConstraint, ViewContext, WindowContext,
AnyElement, Axis, Border, CursorRegion, Element, EventContext, FontCache, LayoutContext,
MouseRegion, Quad, SceneBuilder, SizeConstraint, ViewContext, WindowContext,
};
use itertools::Itertools;
use json::json;
use language::{Bias, CursorShape, DiagnosticSeverity, OffsetUtf16, Selection};
use project::ProjectPath;
use settings::{GitGutter, Settings};
use settings::{GitGutter, Settings, ShowWhitespaces};
use smallvec::SmallVec;
use std::{
borrow::Cow,
cmp::{self, Ordering},
fmt::Write,
iter,
@@ -783,11 +784,19 @@ impl EditorElement {
let mut cursors = SmallVec::<[Cursor; 32]>::new();
let corner_radius = 0.15 * layout.position_map.line_height;
let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
for (replica_id, selections) in &layout.selections {
let selection_style = style.replica_selection_style(*replica_id);
let replica_id = *replica_id;
let selection_style = style.replica_selection_style(replica_id);
for selection in selections {
if !selection.range.is_empty()
&& (replica_id == local_replica_id
|| Some(replica_id) == editor.leader_replica_id)
{
invisible_display_ranges.push(selection.range.clone());
}
self.paint_highlighted_range(
scene,
selection.range.clone(),
@@ -801,14 +810,15 @@ impl EditorElement {
bounds,
);
if editor.show_local_cursors(cx) || *replica_id != local_replica_id {
if editor.show_local_cursors(cx) || replica_id != local_replica_id {
let cursor_position = selection.head;
if layout
.visible_display_row_range
.contains(&cursor_position.row())
{
let cursor_row_layout = &layout.position_map.line_layouts
[(cursor_position.row() - start_row) as usize];
[(cursor_position.row() - start_row) as usize]
.line;
let cursor_column = cursor_position.column() as usize;
let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
@@ -862,20 +872,20 @@ impl EditorElement {
}
if let Some(visible_text_bounds) = bounds.intersection(visible_bounds) {
// Draw glyphs
for (ix, line) in layout.position_map.line_layouts.iter().enumerate() {
for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() {
let row = start_row + ix as u32;
line.paint(
line_with_invisibles.draw(
layout,
row,
scroll_top,
scene,
content_origin
+ vec2f(
-scroll_left,
row as f32 * layout.position_map.line_height - scroll_top,
),
content_origin,
scroll_left,
visible_text_bounds,
layout.position_map.line_height,
cx,
);
&invisible_display_ranges,
visible_bounds,
)
}
}
@@ -888,7 +898,7 @@ impl EditorElement {
if let Some((position, context_menu)) = layout.context_menu.as_mut() {
scene.push_stacking_context(None, None);
let cursor_row_layout =
&layout.position_map.line_layouts[(position.row() - start_row) as usize];
&layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
let y = (position.row() + 1) as f32 * layout.position_map.line_height - scroll_top;
let mut list_origin = content_origin + vec2f(x, y);
@@ -921,7 +931,7 @@ impl EditorElement {
// This is safe because we check on layout whether the required row is available
let hovered_row_layout =
&layout.position_map.line_layouts[(position.row() - start_row) as usize];
&layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
// Minimum required size: Take the first popover, and add 1.5 times the minimum popover
// height. This is the size we will use to decide whether to render popovers above or below
@@ -1012,15 +1022,16 @@ impl EditorElement {
let mut first_row_y_offset = 0.0;
// Impose a minimum height on the scrollbar thumb
let row_height = height / max_row;
let min_thumb_height =
style.min_height_factor * cx.font_cache.line_height(self.style.text.font_size);
let thumb_height = (row_range.end - row_range.start) * height / max_row;
let thumb_height = (row_range.end - row_range.start) * row_height;
if thumb_height < min_thumb_height {
first_row_y_offset = (min_thumb_height - thumb_height) / 2.0;
height -= min_thumb_height - thumb_height;
}
let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * height / max_row };
let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * row_height };
let thumb_top = y_for_row(row_range.start) - first_row_y_offset;
let thumb_bottom = y_for_row(row_range.end) + first_row_y_offset;
@@ -1034,6 +1045,50 @@ impl EditorElement {
background: style.track.background_color,
..Default::default()
});
let diff_style = cx.global::<Settings>().theme.editor.diff.clone();
for hunk in layout
.position_map
.snapshot
.buffer_snapshot
.git_diff_hunks_in_range(0..(max_row.floor() as u32), false)
{
let start_y = y_for_row(hunk.buffer_range.start as f32);
let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end {
y_for_row((hunk.buffer_range.end + 1) as f32)
} else {
y_for_row((hunk.buffer_range.end) as f32)
};
if end_y - start_y < 1. {
end_y = start_y + 1.;
}
let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
let color = match hunk.status() {
DiffHunkStatus::Added => diff_style.inserted,
DiffHunkStatus::Modified => diff_style.modified,
DiffHunkStatus::Removed => diff_style.deleted,
};
let border = Border {
width: 1.,
color: style.thumb.border.color,
overlay: false,
top: false,
right: true,
bottom: false,
left: true,
};
scene.push_quad(Quad {
bounds,
background: Some(color),
border,
corner_radius: style.thumb.corner_radius,
})
}
scene.push_quad(Quad {
bounds: thumb_bounds,
border: style.thumb.border,
@@ -1118,7 +1173,7 @@ impl EditorElement {
.into_iter()
.map(|row| {
let line_layout =
&layout.position_map.line_layouts[(row - start_row) as usize];
&layout.position_map.line_layouts[(row - start_row) as usize].line;
HighlightedRangeLine {
start_x: if row == range.start.row() {
content_origin.x()
@@ -1280,9 +1335,10 @@ impl EditorElement {
fn layout_lines(
&mut self,
rows: Range<u32>,
line_number_layouts: &[Option<Line>],
snapshot: &EditorSnapshot,
cx: &ViewContext<Editor>,
) -> Vec<text_layout::Line> {
) -> Vec<LineWithInvisibles> {
if rows.start >= rows.end {
return Vec::new();
}
@@ -1317,6 +1373,10 @@ impl EditorElement {
)],
)
})
.map(|line| LineWithInvisibles {
line,
invisibles: Vec::new(),
})
.collect()
} else {
let style = &self.style;
@@ -1359,15 +1419,22 @@ impl EditorElement {
highlight_style = Some(diagnostic_highlight);
}
(chunk.text, highlight_style)
HighlightedChunk {
chunk: chunk.text,
style: highlight_style,
is_tab: chunk.is_tab,
}
});
layout_highlighted_chunks(
LineWithInvisibles::from_chunks(
chunks,
&style.text,
cx.text_layout_cache(),
cx.font_cache(),
MAX_LINE_LEN,
rows.len() as usize,
line_number_layouts,
snapshot.mode,
)
}
}
@@ -1385,10 +1452,10 @@ impl EditorElement {
text_x: f32,
line_height: f32,
style: &EditorStyle,
line_layouts: &[text_layout::Line],
line_layouts: &[LineWithInvisibles],
include_root: bool,
editor: &mut Editor,
cx: &mut ViewContext<Editor>,
cx: &mut LayoutContext<Editor>,
) -> (f32, Vec<BlockLayout>) {
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
let scroll_x = snapshot.scroll_anchor.offset.x();
@@ -1408,6 +1475,7 @@ impl EditorElement {
let anchor_x = text_x
+ if rows.contains(&align_to.row()) {
line_layouts[(align_to.row() - rows.start) as usize]
.line
.x_for_index(align_to.column() as usize)
} else {
layout_line(align_to.row(), snapshot, style, cx.text_layout_cache())
@@ -1586,6 +1654,220 @@ impl EditorElement {
}
}
struct HighlightedChunk<'a> {
chunk: &'a str,
style: Option<HighlightStyle>,
is_tab: bool,
}
#[derive(Debug)]
pub struct LineWithInvisibles {
pub line: Line,
invisibles: Vec<Invisible>,
}
impl LineWithInvisibles {
fn from_chunks<'a>(
chunks: impl Iterator<Item = HighlightedChunk<'a>>,
text_style: &TextStyle,
text_layout_cache: &TextLayoutCache,
font_cache: &Arc<FontCache>,
max_line_len: usize,
max_line_count: usize,
line_number_layouts: &[Option<Line>],
editor_mode: EditorMode,
) -> Vec<Self> {
let mut layouts = Vec::with_capacity(max_line_count);
let mut line = String::new();
let mut invisibles = Vec::new();
let mut styles = Vec::new();
let mut non_whitespace_added = false;
let mut row = 0;
let mut line_exceeded_max_len = false;
for highlighted_chunk in chunks.chain([HighlightedChunk {
chunk: "\n",
style: None,
is_tab: false,
}]) {
for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() {
if ix > 0 {
layouts.push(Self {
line: text_layout_cache.layout_str(&line, text_style.font_size, &styles),
invisibles: invisibles.drain(..).collect(),
});
line.clear();
styles.clear();
row += 1;
line_exceeded_max_len = false;
non_whitespace_added = false;
if row == max_line_count {
return layouts;
}
}
if !line_chunk.is_empty() && !line_exceeded_max_len {
let text_style = if let Some(style) = highlighted_chunk.style {
text_style
.clone()
.highlight(style, font_cache)
.map(Cow::Owned)
.unwrap_or_else(|_| Cow::Borrowed(text_style))
} else {
Cow::Borrowed(text_style)
};
if line.len() + line_chunk.len() > max_line_len {
let mut chunk_len = max_line_len - line.len();
while !line_chunk.is_char_boundary(chunk_len) {
chunk_len -= 1;
}
line_chunk = &line_chunk[..chunk_len];
line_exceeded_max_len = true;
}
styles.push((
line_chunk.len(),
RunStyle {
font_id: text_style.font_id,
color: text_style.color,
underline: text_style.underline,
},
));
if editor_mode == EditorMode::Full {
// Line wrap pads its contents with fake whitespaces,
// avoid printing them
let inside_wrapped_string = line_number_layouts
.get(row)
.and_then(|layout| layout.as_ref())
.is_none();
if highlighted_chunk.is_tab {
if non_whitespace_added || !inside_wrapped_string {
invisibles.push(Invisible::Tab {
line_start_offset: line.len(),
});
}
} else {
invisibles.extend(
line_chunk
.chars()
.enumerate()
.filter(|(_, line_char)| {
let is_whitespace = line_char.is_whitespace();
non_whitespace_added |= !is_whitespace;
is_whitespace
&& (non_whitespace_added || !inside_wrapped_string)
})
.map(|(whitespace_index, _)| Invisible::Whitespace {
line_offset: line.len() + whitespace_index,
}),
)
}
}
line.push_str(line_chunk);
}
}
}
layouts
}
fn draw(
&self,
layout: &LayoutState,
row: u32,
scroll_top: f32,
scene: &mut SceneBuilder,
content_origin: Vector2F,
scroll_left: f32,
visible_text_bounds: RectF,
cx: &mut ViewContext<Editor>,
selection_ranges: &[Range<DisplayPoint>],
visible_bounds: RectF,
) {
let line_height = layout.position_map.line_height;
let line_y = row as f32 * line_height - scroll_top;
self.line.paint(
scene,
content_origin + vec2f(-scroll_left, line_y),
visible_text_bounds,
line_height,
cx,
);
self.draw_invisibles(
cx,
&selection_ranges,
layout,
content_origin,
scroll_left,
line_y,
row,
scene,
visible_bounds,
line_height,
);
}
fn draw_invisibles(
&self,
cx: &mut ViewContext<Editor>,
selection_ranges: &[Range<DisplayPoint>],
layout: &LayoutState,
content_origin: Vector2F,
scroll_left: f32,
line_y: f32,
row: u32,
scene: &mut SceneBuilder,
visible_bounds: RectF,
line_height: f32,
) {
let settings = cx.global::<Settings>();
let allowed_invisibles_regions = match settings
.editor_overrides
.show_whitespaces
.or(settings.editor_defaults.show_whitespaces)
.unwrap_or_default()
{
ShowWhitespaces::None => return,
ShowWhitespaces::Selection => Some(selection_ranges),
ShowWhitespaces::All => None,
};
for invisible in &self.invisibles {
let (&token_offset, invisible_symbol) = match invisible {
Invisible::Tab { line_start_offset } => (line_start_offset, &layout.tab_invisible),
Invisible::Whitespace { line_offset } => (line_offset, &layout.space_invisible),
};
let x_offset = self.line.x_for_index(token_offset);
let invisible_offset =
(layout.position_map.em_width - invisible_symbol.width()).max(0.0) / 2.0;
let origin = content_origin + vec2f(-scroll_left + x_offset + invisible_offset, line_y);
if let Some(allowed_regions) = allowed_invisibles_regions {
let invisible_point = DisplayPoint::new(row, token_offset as u32);
if !allowed_regions
.iter()
.any(|region| region.start <= invisible_point && invisible_point < region.end)
{
continue;
}
}
invisible_symbol.paint(scene, origin, visible_bounds, line_height, cx);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Invisible {
Tab { line_start_offset: usize },
Whitespace { line_offset: usize },
}
impl Element<Editor> for EditorElement {
type LayoutState = LayoutState;
type PaintState = ();
@@ -1594,7 +1876,7 @@ impl Element<Editor> for EditorElement {
&mut self,
constraint: SizeConstraint,
editor: &mut Editor,
cx: &mut ViewContext<Editor>,
cx: &mut LayoutContext<Editor>,
) -> (Vector2F, Self::LayoutState) {
let mut size = constraint.max;
if size.x().is_infinite() {
@@ -1609,7 +1891,8 @@ impl Element<Editor> for EditorElement {
let gutter_width;
let gutter_margin;
if snapshot.mode == EditorMode::Full {
gutter_padding = style.text.em_width(cx.font_cache()) * style.gutter_padding_factor;
let em_width = style.text.em_width(cx.font_cache());
gutter_padding = (em_width * style.gutter_padding_factor).round();
gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
gutter_margin = -style.text.descent(cx.font_cache());
} else {
@@ -1775,7 +2058,15 @@ impl Element<Editor> for EditorElement {
));
}
let show_scrollbars = editor.scroll_manager.scrollbars_visible();
let show_scrollbars = match cx.global::<Settings>().show_scrollbars {
settings::ShowScrollbars::Auto => {
snapshot.has_scrollbar_info() || editor.scroll_manager.scrollbars_visible()
}
settings::ShowScrollbars::System => editor.scroll_manager.scrollbars_visible(),
settings::ShowScrollbars::Always => true,
settings::ShowScrollbars::Never => false,
};
let include_root = editor
.project
.as_ref()
@@ -1810,10 +2101,11 @@ impl Element<Editor> for EditorElement {
let scrollbar_row_range = scroll_position.y()..(scroll_position.y() + height_in_lines);
let mut max_visible_line_width = 0.0;
let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx);
for line in &line_layouts {
if line.width() > max_visible_line_width {
max_visible_line_width = line.width();
let line_layouts =
self.layout_lines(start_row..end_row, &line_number_layouts, &snapshot, cx);
for line_with_invisibles in &line_layouts {
if line_with_invisibles.line.width() > max_visible_line_width {
max_visible_line_width = line_with_invisibles.line.width();
}
}
@@ -1965,6 +2257,13 @@ impl Element<Editor> for EditorElement {
}
}
let invisible_symbol_font_size = self.style.text.font_size / 2.0;
let invisible_symbol_style = RunStyle {
color: self.style.whitespace,
font_id: self.style.text.font_id,
underline: Default::default(),
};
(
size,
LayoutState {
@@ -1997,6 +2296,16 @@ impl Element<Editor> for EditorElement {
context_menu,
code_actions_indicator,
fold_indicators,
tab_invisible: cx.text_layout_cache().layout_str(
"",
invisible_symbol_font_size,
&[("".len(), invisible_symbol_style)],
),
space_invisible: cx.text_layout_cache().layout_str(
"",
invisible_symbol_font_size,
&[("".len(), invisible_symbol_style)],
),
hover_popovers: hover,
},
)
@@ -2073,10 +2382,11 @@ impl Element<Editor> for EditorElement {
return None;
}
let line = layout
let line = &layout
.position_map
.line_layouts
.get((range_start.row() - start_row) as usize)?;
.get((range_start.row() - start_row) as usize)?
.line;
let range_start_x = line.x_for_index(range_start.column() as usize);
let range_start_y = range_start.row() as f32 * layout.position_map.line_height;
Some(RectF::new(
@@ -2133,15 +2443,17 @@ pub struct LayoutState {
code_actions_indicator: Option<(u32, AnyElement<Editor>)>,
hover_popovers: Option<(DisplayPoint, Vec<AnyElement<Editor>>)>,
fold_indicators: Vec<Option<AnyElement<Editor>>>,
tab_invisible: Line,
space_invisible: Line,
}
pub struct PositionMap {
struct PositionMap {
size: Vector2F,
line_height: f32,
scroll_max: Vector2F,
em_width: f32,
em_advance: f32,
line_layouts: Vec<text_layout::Line>,
line_layouts: Vec<LineWithInvisibles>,
snapshot: EditorSnapshot,
}
@@ -2163,6 +2475,7 @@ impl PositionMap {
let (column, x_overshoot) = if let Some(line) = self
.line_layouts
.get(row as usize - scroll_position.y() as usize)
.map(|line_with_spaces| &line_with_spaces.line)
{
if let Some(ix) = line.index_for_x(x) {
(ix as u32, 0.0)
@@ -2431,7 +2744,7 @@ impl HighlightedRange {
}
}
pub fn position_to_display_point(
fn position_to_display_point(
position: Vector2F,
text_bounds: RectF,
position_map: &PositionMap,
@@ -2448,7 +2761,7 @@ pub fn position_to_display_point(
}
}
pub fn range_to_bounds(
fn range_to_bounds(
range: &Range<DisplayPoint>,
content_origin: Vector2F,
scroll_left: f32,
@@ -2476,7 +2789,7 @@ pub fn range_to_bounds(
content_origin.y() + row_range.start as f32 * position_map.line_height - scroll_top;
for (idx, row) in row_range.enumerate() {
let line_layout = &position_map.line_layouts[(row - start_row) as usize];
let line_layout = &position_map.line_layouts[(row - start_row) as usize].line;
let start_x = if row == range.start.row() {
content_origin.x() + line_layout.x_for_index(range.start.column() as usize)
@@ -2516,8 +2829,9 @@ mod tests {
Editor, MultiBuffer,
};
use gpui::TestAppContext;
use log::info;
use settings::Settings;
use std::sync::Arc;
use std::{num::NonZeroU32, sync::Arc};
use util::test::sample_text;
#[gpui::test]
@@ -2565,10 +2879,18 @@ mod tests {
let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
let (size, mut state) = editor.update(cx, |editor, cx| {
let mut new_parents = Default::default();
let mut notify_views_if_parents_change = Default::default();
let mut layout_cx = LayoutContext::new(
cx,
&mut new_parents,
&mut notify_views_if_parents_change,
false,
);
element.layout(
SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
editor,
cx,
&mut layout_cx,
)
});
@@ -2589,4 +2911,194 @@ mod tests {
element.paint(&mut scene, bounds, bounds, &mut state, editor, cx);
});
}
#[gpui::test]
fn test_all_invisibles_drawing(cx: &mut TestAppContext) {
let tab_size = 4;
let input_text = "\t \t|\t| a b";
let expected_invisibles = vec![
Invisible::Tab {
line_start_offset: 0,
},
Invisible::Whitespace {
line_offset: tab_size as usize,
},
Invisible::Tab {
line_start_offset: tab_size as usize + 1,
},
Invisible::Tab {
line_start_offset: tab_size as usize * 2 + 1,
},
Invisible::Whitespace {
line_offset: tab_size as usize * 3 + 1,
},
Invisible::Whitespace {
line_offset: tab_size as usize * 3 + 3,
},
];
assert_eq!(
expected_invisibles.len(),
input_text
.chars()
.filter(|initial_char| initial_char.is_whitespace())
.count(),
"Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
);
cx.update(|cx| {
let mut test_settings = Settings::test(cx);
test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All);
test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(tab_size).unwrap());
cx.set_global(test_settings);
});
let actual_invisibles =
collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, 500.0);
assert_eq!(expected_invisibles, actual_invisibles);
}
#[gpui::test]
fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) {
cx.update(|cx| {
let mut test_settings = Settings::test(cx);
test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All);
test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(4).unwrap());
cx.set_global(test_settings);
});
for editor_mode_without_invisibles in [
EditorMode::SingleLine,
EditorMode::AutoHeight { max_lines: 100 },
] {
let invisibles = collect_invisibles_from_new_editor(
cx,
editor_mode_without_invisibles,
"\t\t\t| | a b",
500.0,
);
assert!(invisibles.is_empty(),
"For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}");
}
}
#[gpui::test]
fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) {
let tab_size = 4;
let input_text = "a\tbcd ".repeat(9);
let repeated_invisibles = [
Invisible::Tab {
line_start_offset: 1,
},
Invisible::Whitespace {
line_offset: tab_size as usize + 3,
},
Invisible::Whitespace {
line_offset: tab_size as usize + 4,
},
Invisible::Whitespace {
line_offset: tab_size as usize + 5,
},
];
let expected_invisibles = std::iter::once(repeated_invisibles)
.cycle()
.take(9)
.flatten()
.collect::<Vec<_>>();
assert_eq!(
expected_invisibles.len(),
input_text
.chars()
.filter(|initial_char| initial_char.is_whitespace())
.count(),
"Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
);
info!("Expected invisibles: {expected_invisibles:?}");
// Put the same string with repeating whitespace pattern into editors of various size,
// take deliberately small steps during resizing, to put all whitespace kinds near the wrap point.
let resize_step = 10.0;
let mut editor_width = 200.0;
while editor_width <= 1000.0 {
cx.update(|cx| {
let mut test_settings = Settings::test(cx);
test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(tab_size).unwrap());
test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All);
test_settings.editor_defaults.preferred_line_length = Some(editor_width as u32);
test_settings.editor_defaults.soft_wrap =
Some(settings::SoftWrap::PreferredLineLength);
cx.set_global(test_settings);
});
let actual_invisibles =
collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, editor_width);
// Whatever the editor size is, ensure it has the same invisible kinds in the same order
// (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets).
let mut i = 0;
for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() {
i = actual_index;
match expected_invisibles.get(i) {
Some(expected_invisible) => match (expected_invisible, actual_invisible) {
(Invisible::Whitespace { .. }, Invisible::Whitespace { .. })
| (Invisible::Tab { .. }, Invisible::Tab { .. }) => {}
_ => {
panic!("At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}")
}
},
None => panic!("Unexpected extra invisible {actual_invisible:?} at index {i}"),
}
}
let missing_expected_invisibles = &expected_invisibles[i + 1..];
assert!(
missing_expected_invisibles.is_empty(),
"Missing expected invisibles after index {i}: {missing_expected_invisibles:?}"
);
editor_width += resize_step;
}
}
fn collect_invisibles_from_new_editor(
cx: &mut TestAppContext,
editor_mode: EditorMode,
input_text: &str,
editor_width: f32,
) -> Vec<Invisible> {
info!(
"Creating editor with mode {editor_mode:?}, witdh {editor_width} and text '{input_text}'"
);
let (_, editor) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&input_text, cx);
Editor::new(editor_mode, buffer, None, None, cx)
});
let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
let (_, layout_state) = editor.update(cx, |editor, cx| {
editor.set_soft_wrap_mode(settings::SoftWrap::EditorWidth, cx);
editor.set_wrap_width(Some(editor_width), cx);
let mut new_parents = Default::default();
let mut notify_views_if_parents_change = Default::default();
let mut layout_cx = LayoutContext::new(
cx,
&mut new_parents,
&mut notify_views_if_parents_change,
false,
);
element.layout(
SizeConstraint::new(vec2f(editor_width, 500.), vec2f(editor_width, 500.)),
editor,
&mut layout_cx,
)
});
layout_state
.position_map
.line_layouts
.iter()
.map(|line_with_invisibles| &line_with_invisibles.invisibles)
.flatten()
.cloned()
.collect()
}
}

View File

@@ -1006,8 +1006,7 @@ mod tests {
.zip(expected_styles.iter().cloned())
.collect::<Vec<_>>();
assert_eq!(
rendered.text,
dbg!(expected_text),
rendered.text, expected_text,
"wrong text for input {blocks:?}"
);
assert_eq!(

View File

@@ -3,7 +3,7 @@ use crate::{
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
};
use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result};
use collections::HashSet;
use futures::future::try_join_all;
use gpui::{
@@ -27,7 +27,7 @@ use std::{
path::{Path, PathBuf},
};
use text::Selection;
use util::{ResultExt, TryFutureExt};
use util::{paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
use workspace::item::{BreadcrumbText, FollowableItemHandle};
use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
@@ -566,7 +566,7 @@ impl Item for Editor {
cx: &AppContext,
) -> AnyElement<T> {
Flex::row()
.with_child(Label::new(self.title(cx).to_string(), style.label.clone()).aligned())
.with_child(Label::new(self.title(cx).to_string(), style.label.clone()).into_any())
.with_children(detail.and_then(|detail| {
let path = path_for_buffer(&self.buffer, detail, false, cx)?;
let description = path.to_string_lossy();
@@ -580,6 +580,7 @@ impl Item for Editor {
.aligned(),
)
}))
.align_children_center()
.into_any()
}
@@ -636,7 +637,7 @@ impl Item for Editor {
project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.report_editor_event("save", cx);
self.report_editor_event("save", None, cx);
let format = self.perform_format(project.clone(), FormatTrigger::Save, cx);
let buffers = self.buffer().clone().read(cx).all_buffers();
cx.spawn(|_, mut cx| async move {
@@ -685,6 +686,11 @@ impl Item for Editor {
.as_singleton()
.expect("cannot call save_as on an excerpt list");
let file_extension = abs_path
.extension()
.map(|a| a.to_string_lossy().to_string());
self.report_editor_event("save", file_extension, cx);
project.update(cx, |project, cx| {
project.save_buffer_as(buffer, abs_path, cx)
})
@@ -704,10 +710,10 @@ impl Item for Editor {
this.update(&mut cx, |editor, cx| {
editor.request_autoscroll(Autoscroll::fit(), cx)
})?;
buffer.update(&mut cx, |buffer, _| {
buffer.update(&mut cx, |buffer, cx| {
if let Some(transaction) = transaction {
if !buffer.is_singleton() {
buffer.push_transaction(&transaction.0);
buffer.push_transaction(&transaction.0, cx);
}
}
});
@@ -864,16 +870,13 @@ impl Item for Editor {
let buffer = project_item
.downcast::<Buffer>()
.context("Project item at stored path was not a buffer")?;
let pane = pane
.upgrade(&cx)
.ok_or_else(|| anyhow!("pane was dropped"))?;
Ok(cx.update(|cx| {
cx.add_view(&pane, |cx| {
Ok(pane.update(&mut cx, |_, cx| {
cx.add_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
editor.read_scroll_position_from_db(item_id, workspace_id, cx);
editor
})
}))
})?)
})
})
.unwrap_or_else(|error| Task::ready(Err(error)))
@@ -1114,7 +1117,11 @@ impl View for CursorPosition {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if let Some(position) = self.position {
let theme = &cx.global::<Settings>().theme.workspace.status_bar;
let mut text = format!("{},{}", position.row + 1, position.column + 1);
let mut text = format!(
"{}{FILE_ROW_COLUMN_DELIMITER}{}",
position.row + 1,
position.column + 1
);
if self.selected_count > 0 {
write!(text, " ({} selected)", self.selected_count).unwrap();
}

View File

@@ -43,7 +43,7 @@ pub struct ExcerptId(usize);
pub struct MultiBuffer {
snapshot: RefCell<MultiBufferSnapshot>,
buffers: RefCell<HashMap<usize, BufferState>>,
buffers: RefCell<HashMap<u64, BufferState>>,
next_excerpt_id: usize,
subscriptions: Topic,
singleton: bool,
@@ -85,7 +85,7 @@ struct History {
#[derive(Clone)]
struct Transaction {
id: TransactionId,
buffer_transactions: HashMap<usize, text::TransactionId>,
buffer_transactions: HashMap<u64, text::TransactionId>,
first_edit_at: Instant,
last_edit_at: Instant,
suppress_grouping: bool,
@@ -145,7 +145,7 @@ pub struct ExcerptBoundary {
struct Excerpt {
id: ExcerptId,
locator: Locator,
buffer_id: usize,
buffer_id: u64,
buffer: BufferSnapshot,
range: ExcerptRange<text::Anchor>,
max_buffer_row: u32,
@@ -337,7 +337,7 @@ impl MultiBuffer {
offset: T,
theme: Option<&SyntaxTheme>,
cx: &AppContext,
) -> Option<(usize, Vec<OutlineItem<Anchor>>)> {
) -> Option<(u64, Vec<OutlineItem<Anchor>>)> {
self.read(cx).symbols_containing(offset, theme)
}
@@ -394,7 +394,7 @@ impl MultiBuffer {
is_insertion: bool,
original_indent_column: u32,
}
let mut buffer_edits: HashMap<usize, Vec<BufferEdit>> = Default::default();
let mut buffer_edits: HashMap<u64, Vec<BufferEdit>> = Default::default();
let mut cursor = snapshot.excerpts.cursor::<usize>();
for (ix, (range, new_text)) in edits.enumerate() {
let new_text: Arc<str> = new_text.into();
@@ -593,7 +593,7 @@ impl MultiBuffer {
if let Some(transaction_id) =
buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx))
{
buffer_transactions.insert(buffer.id(), transaction_id);
buffer_transactions.insert(buffer.read(cx).remote_id(), transaction_id);
}
}
@@ -614,12 +614,12 @@ impl MultiBuffer {
}
}
pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T)
pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T, cx: &mut ModelContext<Self>)
where
T: IntoIterator<Item = (&'a ModelHandle<Buffer>, &'a language::Transaction)>,
{
self.history
.push_transaction(buffer_transactions, Instant::now());
.push_transaction(buffer_transactions, Instant::now(), cx);
self.history.finalize_last_transaction();
}
@@ -644,7 +644,7 @@ impl MultiBuffer {
cursor_shape: CursorShape,
cx: &mut ModelContext<Self>,
) {
let mut selections_by_buffer: HashMap<usize, Vec<Selection<text::Anchor>>> =
let mut selections_by_buffer: HashMap<u64, Vec<Selection<text::Anchor>>> =
Default::default();
let snapshot = self.read(cx);
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
@@ -785,8 +785,8 @@ impl MultiBuffer {
let (mut tx, rx) = mpsc::channel(256);
let task = cx.spawn(|this, mut cx| async move {
for (buffer, ranges) in excerpts {
let buffer_id = buffer.id();
let buffer_snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot());
let (buffer_id, buffer_snapshot) =
buffer.read_with(&cx, |buffer, _| (buffer.remote_id(), buffer.snapshot()));
let mut excerpt_ranges = Vec::new();
let mut range_counts = Vec::new();
@@ -855,7 +855,7 @@ impl MultiBuffer {
where
O: text::ToPoint + text::ToOffset,
{
let buffer_id = buffer.id();
let buffer_id = buffer.read(cx).remote_id();
let buffer_snapshot = buffer.read(cx).snapshot();
let (excerpt_ranges, range_counts) =
build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count);
@@ -924,7 +924,7 @@ impl MultiBuffer {
self.sync(cx);
let buffer_id = buffer.id();
let buffer_id = buffer.read(cx).remote_id();
let buffer_snapshot = buffer.read(cx).snapshot();
let mut buffers = self.buffers.borrow_mut();
@@ -1051,7 +1051,7 @@ impl MultiBuffer {
let buffers = self.buffers.borrow();
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
for locator in buffers
.get(&buffer.id())
.get(&buffer.read(cx).remote_id())
.map(|state| &state.excerpts)
.into_iter()
.flatten()
@@ -1165,6 +1165,9 @@ impl MultiBuffer {
) {
self.sync(cx);
let ids = excerpt_ids.into_iter().collect::<Vec<_>>();
if ids.is_empty() {
return;
}
let mut buffers = self.buffers.borrow_mut();
let mut snapshot = self.snapshot.borrow_mut();
@@ -1321,7 +1324,7 @@ impl MultiBuffer {
.collect()
}
pub fn buffer(&self, buffer_id: usize) -> Option<ModelHandle<Buffer>> {
pub fn buffer(&self, buffer_id: u64) -> Option<ModelHandle<Buffer>> {
self.buffers
.borrow()
.get(&buffer_id)
@@ -1478,8 +1481,8 @@ impl MultiBuffer {
for (locator, buffer, buffer_edited) in excerpts_to_edit {
new_excerpts.push_tree(cursor.slice(&Some(locator), Bias::Left, &()), &());
let old_excerpt = cursor.item().unwrap();
let buffer_id = buffer.id();
let buffer = buffer.read(cx);
let buffer_id = buffer.remote_id();
let mut new_excerpt;
if buffer_edited {
@@ -1605,11 +1608,11 @@ impl MultiBuffer {
let buffer_handle = if rng.gen() || self.buffers.borrow().is_empty() {
let text = RandomCharIter::new(&mut *rng).take(10).collect::<String>();
buffers.push(cx.add_model(|cx| Buffer::new(0, text, cx)));
let buffer = buffers.last().unwrap();
let buffer = buffers.last().unwrap().read(cx);
log::info!(
"Creating new buffer {} with text: {:?}",
buffer.id(),
buffer.read(cx).text()
buffer.remote_id(),
buffer.text()
);
buffers.last().unwrap().clone()
} else {
@@ -1637,7 +1640,7 @@ impl MultiBuffer {
.collect::<Vec<_>>();
log::info!(
"Inserting excerpts from buffer {} and ranges {:?}: {:?}",
buffer_handle.id(),
buffer_handle.read(cx).remote_id(),
ranges.iter().map(|r| &r.context).collect::<Vec<_>>(),
ranges
.iter()
@@ -1830,7 +1833,7 @@ impl MultiBufferSnapshot {
(start..end, word_kind)
}
pub fn as_singleton(&self) -> Option<(&ExcerptId, usize, &BufferSnapshot)> {
pub fn as_singleton(&self) -> Option<(&ExcerptId, u64, &BufferSnapshot)> {
if self.singleton {
self.excerpts
.iter()
@@ -2938,7 +2941,7 @@ impl MultiBufferSnapshot {
&self,
offset: T,
theme: Option<&SyntaxTheme>,
) -> Option<(usize, Vec<OutlineItem<Anchor>>)> {
) -> Option<(u64, Vec<OutlineItem<Anchor>>)> {
let anchor = self.anchor_before(offset);
let excerpt_id = anchor.excerpt_id();
let excerpt = self.excerpt(excerpt_id)?;
@@ -2978,7 +2981,7 @@ impl MultiBufferSnapshot {
}
}
pub fn buffer_id_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<usize> {
pub fn buffer_id_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<u64> {
Some(self.excerpt(excerpt_id)?.buffer_id)
}
@@ -3116,7 +3119,7 @@ impl History {
fn end_transaction(
&mut self,
now: Instant,
buffer_transactions: HashMap<usize, TransactionId>,
buffer_transactions: HashMap<u64, TransactionId>,
) -> bool {
assert_ne!(self.transaction_depth, 0);
self.transaction_depth -= 1;
@@ -3141,8 +3144,12 @@ impl History {
}
}
fn push_transaction<'a, T>(&mut self, buffer_transactions: T, now: Instant)
where
fn push_transaction<'a, T>(
&mut self,
buffer_transactions: T,
now: Instant,
cx: &mut ModelContext<MultiBuffer>,
) where
T: IntoIterator<Item = (&'a ModelHandle<Buffer>, &'a language::Transaction)>,
{
assert_eq!(self.transaction_depth, 0);
@@ -3150,7 +3157,7 @@ impl History {
id: self.next_transaction_id.tick(),
buffer_transactions: buffer_transactions
.into_iter()
.map(|(buffer, transaction)| (buffer.id(), transaction.id))
.map(|(buffer, transaction)| (buffer.read(cx).remote_id(), transaction.id))
.collect(),
first_edit_at: now,
last_edit_at: now,
@@ -3247,7 +3254,7 @@ impl Excerpt {
fn new(
id: ExcerptId,
locator: Locator,
buffer_id: usize,
buffer_id: u64,
buffer: BufferSnapshot,
range: ExcerptRange<text::Anchor>,
has_trailing_newline: bool,
@@ -4076,19 +4083,25 @@ mod tests {
let leader_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
let follower_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
let follower_edit_event_count = Rc::new(RefCell::new(0));
follower_multibuffer.update(cx, |_, cx| {
cx.subscribe(&leader_multibuffer, |follower, _, event, cx| {
match event.clone() {
let follower_edit_event_count = follower_edit_event_count.clone();
cx.subscribe(
&leader_multibuffer,
move |follower, _, event, cx| match event.clone() {
Event::ExcerptsAdded {
buffer,
predecessor,
excerpts,
} => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx),
Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx),
Event::Edited => {
*follower_edit_event_count.borrow_mut() += 1;
}
_ => {}
}
})
},
)
.detach();
});
@@ -4127,6 +4140,7 @@ mod tests {
leader_multibuffer.read(cx).snapshot(cx).text(),
follower_multibuffer.read(cx).snapshot(cx).text(),
);
assert_eq!(*follower_edit_event_count.borrow(), 2);
leader_multibuffer.update(cx, |leader, cx| {
let excerpt_ids = leader.excerpt_ids();
@@ -4136,6 +4150,27 @@ mod tests {
leader_multibuffer.read(cx).snapshot(cx).text(),
follower_multibuffer.read(cx).snapshot(cx).text(),
);
assert_eq!(*follower_edit_event_count.borrow(), 3);
// Removing an empty set of excerpts is a noop.
leader_multibuffer.update(cx, |leader, cx| {
leader.remove_excerpts([], cx);
});
assert_eq!(
leader_multibuffer.read(cx).snapshot(cx).text(),
follower_multibuffer.read(cx).snapshot(cx).text(),
);
assert_eq!(*follower_edit_event_count.borrow(), 3);
// Adding an empty set of excerpts is a noop.
leader_multibuffer.update(cx, |leader, cx| {
leader.push_excerpts::<usize>(buffer_2.clone(), [], cx);
});
assert_eq!(
leader_multibuffer.read(cx).snapshot(cx).text(),
follower_multibuffer.read(cx).snapshot(cx).text(),
);
assert_eq!(*follower_edit_event_count.borrow(), 3);
leader_multibuffer.update(cx, |leader, cx| {
leader.clear(cx);
@@ -4144,6 +4179,7 @@ mod tests {
leader_multibuffer.read(cx).snapshot(cx).text(),
follower_multibuffer.read(cx).snapshot(cx).text(),
);
assert_eq!(*follower_edit_event_count.borrow(), 4);
}
#[gpui::test]
@@ -4715,7 +4751,7 @@ mod tests {
"Inserting excerpt at {} of {} for buffer {}: {:?}[{:?}] = {:?}",
excerpt_ix,
expected_excerpts.len(),
buffer_handle.id(),
buffer_handle.read(cx).remote_id(),
buffer.text(),
start_ix..end_ix,
&buffer.text()[start_ix..end_ix]
@@ -4801,8 +4837,8 @@ mod tests {
let mut excerpt_starts = excerpt_starts.into_iter();
for (buffer, range) in &expected_excerpts {
let buffer_id = buffer.id();
let buffer = buffer.read(cx);
let buffer_id = buffer.remote_id();
let buffer_range = range.to_offset(buffer);
let buffer_start_point = buffer.offset_to_point(buffer_range.start);
let buffer_start_point_utf16 =

View File

@@ -8,7 +8,7 @@ use sum_tree::Bias;
#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
pub struct Anchor {
pub(crate) buffer_id: Option<usize>,
pub(crate) buffer_id: Option<u64>,
pub(crate) excerpt_id: ExcerptId,
pub(crate) text_anchor: text::Anchor,
}

View File

@@ -1,9 +1,9 @@
use std::cmp;
use gpui::{text_layout, ViewContext};
use gpui::ViewContext;
use language::Point;
use crate::{display_map::ToDisplayPoint, Editor, EditorMode};
use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles};
#[derive(PartialEq, Eq)]
pub enum Autoscroll {
@@ -172,7 +172,7 @@ impl Editor {
viewport_width: f32,
scroll_width: f32,
max_glyph_width: f32,
layouts: &[text_layout::Line],
layouts: &[LineWithInvisibles],
cx: &mut ViewContext<Self>,
) -> bool {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
@@ -194,10 +194,13 @@ impl Editor {
let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3);
target_left = target_left.min(
layouts[(head.row() - start_row) as usize]
.line
.x_for_index(start_column as usize),
);
target_right = target_right.max(
layouts[(head.row() - start_row) as usize].x_for_index(end_column as usize)
layouts[(head.row() - start_row) as usize]
.line
.x_for_index(end_column as usize)
+ max_glyph_width,
);
}

View File

@@ -16,6 +16,7 @@ menu = { path = "../menu" }
picker = { path = "../picker" }
project = { path = "../project" }
settings = { path = "../settings" }
text = { path = "../text" }
util = { path = "../util" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }

View File

@@ -1,3 +1,4 @@
use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
use fuzzy::PathMatch;
use gpui::{
actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle,
@@ -12,7 +13,8 @@ use std::{
Arc,
},
};
use util::{post_inc, ResultExt};
use text::Point;
use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
use workspace::Workspace;
pub type FileFinder = Picker<FileFinderDelegate>;
@@ -23,7 +25,7 @@ pub struct FileFinderDelegate {
search_count: usize,
latest_search_id: usize,
latest_search_did_cancel: bool,
latest_search_query: String,
latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
relative_to: Option<Arc<Path>>,
matches: Vec<PathMatch>,
selected: Option<(usize, Arc<Path>)>,
@@ -60,6 +62,21 @@ pub enum Event {
Dismissed,
}
#[derive(Debug, Clone)]
struct FileSearchQuery {
raw_query: String,
file_query_end: Option<usize>,
}
impl FileSearchQuery {
fn path_query(&self) -> &str {
match self.file_query_end {
Some(file_path_end) => &self.raw_query[..file_path_end],
None => &self.raw_query,
}
}
}
impl FileFinderDelegate {
fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) {
let path = &path_match.path;
@@ -103,7 +120,7 @@ impl FileFinderDelegate {
search_count: 0,
latest_search_id: 0,
latest_search_did_cancel: false,
latest_search_query: String::new(),
latest_search_query: None,
relative_to,
matches: Vec::new(),
selected: None,
@@ -111,7 +128,11 @@ impl FileFinderDelegate {
}
}
fn spawn_search(&mut self, query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
fn spawn_search(
&mut self,
query: PathLikeWithPosition<FileSearchQuery>,
cx: &mut ViewContext<FileFinder>,
) -> Task<()> {
let relative_to = self.relative_to.clone();
let worktrees = self
.project
@@ -140,7 +161,7 @@ impl FileFinderDelegate {
cx.spawn(|picker, mut cx| async move {
let matches = fuzzy::match_path_sets(
candidate_sets.as_slice(),
&query,
query.path_like.path_query(),
relative_to,
false,
100,
@@ -163,18 +184,24 @@ impl FileFinderDelegate {
&mut self,
search_id: usize,
did_cancel: bool,
query: String,
query: PathLikeWithPosition<FileSearchQuery>,
matches: Vec<PathMatch>,
cx: &mut ViewContext<FileFinder>,
) {
if search_id >= self.latest_search_id {
self.latest_search_id = search_id;
if self.latest_search_did_cancel && query == self.latest_search_query {
if self.latest_search_did_cancel
&& Some(query.path_like.path_query())
== self
.latest_search_query
.as_ref()
.map(|query| query.path_like.path_query())
{
util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a));
} else {
self.matches = matches;
}
self.latest_search_query = query;
self.latest_search_query = Some(query);
self.latest_search_did_cancel = did_cancel;
cx.notify();
}
@@ -209,13 +236,25 @@ impl PickerDelegate for FileFinderDelegate {
cx.notify();
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
if query.is_empty() {
fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
if raw_query.is_empty() {
self.latest_search_id = post_inc(&mut self.search_count);
self.matches.clear();
cx.notify();
Task::ready(())
} else {
let raw_query = &raw_query;
let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
Ok::<_, std::convert::Infallible>(FileSearchQuery {
raw_query: raw_query.to_owned(),
file_query_end: if path_like_str == raw_query {
None
} else {
Some(path_like_str.len())
},
})
})
.expect("infallible");
self.spawn_search(query, cx)
}
}
@@ -228,12 +267,49 @@ impl PickerDelegate for FileFinderDelegate {
path: m.path.clone(),
};
workspace.update(cx, |workspace, cx| {
let open_task = workspace.update(cx, |workspace, cx| {
workspace.open_path(project_path.clone(), None, true, cx)
});
let workspace = workspace.downgrade();
let row = self
.latest_search_query
.as_ref()
.and_then(|query| query.row)
.map(|row| row.saturating_sub(1));
let col = self
.latest_search_query
.as_ref()
.and_then(|query| query.column)
.unwrap_or(0)
.saturating_sub(1);
cx.spawn(|_, mut cx| async move {
let item = open_task.await.log_err()?;
if let Some(row) = row {
if let Some(active_editor) = item.downcast::<Editor>() {
active_editor
.downgrade()
.update(&mut cx, |editor, cx| {
let snapshot = editor.snapshot(cx).display_snapshot;
let point = snapshot
.buffer_snapshot
.clip_point(Point::new(row, col), Bias::Left);
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
s.select_ranges([point..point])
});
})
.log_err();
}
}
workspace
.open_path(project_path.clone(), None, true, cx)
.detach_and_log_err(cx);
workspace.dismiss_modal(cx);
.update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx))
.log_err();
Some(())
})
.detach();
}
}
}
@@ -270,6 +346,7 @@ impl PickerDelegate for FileFinderDelegate {
mod tests {
use super::*;
use editor::Editor;
use gpui::executor::Deterministic;
use menu::{Confirm, SelectNext};
use serde_json::json;
use workspace::{AppState, Workspace};
@@ -337,6 +414,186 @@ mod tests {
});
}
#[gpui::test]
async fn test_row_column_numbers_query_inside_file(
deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext,
) {
let app_state = cx.update(|cx| {
super::init(cx);
editor::init(cx);
AppState::test(cx)
});
let first_file_name = "first.rs";
let first_file_contents = "// First Rust file";
app_state
.fs
.as_fake()
.insert_tree(
"/src",
json!({
"test": {
first_file_name: first_file_contents,
"second.rs": "// Second Rust file",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
cx.dispatch_action(window_id, Toggle);
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
let file_query = &first_file_name[..3];
let file_row = 1;
let file_column = 3;
assert!(file_column <= first_file_contents.len());
let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
finder
.update(cx, |finder, cx| {
finder
.delegate_mut()
.update_matches(query_inside_file.to_string(), cx)
})
.await;
finder.read_with(cx, |finder, _| {
let finder = finder.delegate();
assert_eq!(finder.matches.len(), 1);
let latest_search_query = finder
.latest_search_query
.as_ref()
.expect("Finder should have a query after the update_matches call");
assert_eq!(latest_search_query.path_like.raw_query, query_inside_file);
assert_eq!(
latest_search_query.path_like.file_query_end,
Some(file_query.len())
);
assert_eq!(latest_search_query.row, Some(file_row));
assert_eq!(latest_search_query.column, Some(file_column as u32));
});
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
cx.dispatch_action(window_id, SelectNext);
cx.dispatch_action(window_id, Confirm);
active_pane
.condition(cx, |pane, _| pane.active_item().is_some())
.await;
let editor = cx.update(|cx| {
let active_item = active_pane.read(cx).active_item().unwrap();
active_item.downcast::<Editor>().unwrap()
});
deterministic.advance_clock(std::time::Duration::from_secs(2));
deterministic.start_waiting();
deterministic.finish_waiting();
editor.update(cx, |editor, cx| {
let all_selections = editor.selections.all_adjusted(cx);
assert_eq!(
all_selections.len(),
1,
"Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
);
let caret_selection = all_selections.into_iter().next().unwrap();
assert_eq!(caret_selection.start, caret_selection.end,
"Caret selection should have its start and end at the same position");
assert_eq!(file_row, caret_selection.start.row + 1,
"Query inside file should get caret with the same focus row");
assert_eq!(file_column, caret_selection.start.column as usize + 1,
"Query inside file should get caret with the same focus column");
});
}
#[gpui::test]
async fn test_row_column_numbers_query_outside_file(
deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext,
) {
let app_state = cx.update(|cx| {
super::init(cx);
editor::init(cx);
AppState::test(cx)
});
let first_file_name = "first.rs";
let first_file_contents = "// First Rust file";
app_state
.fs
.as_fake()
.insert_tree(
"/src",
json!({
"test": {
first_file_name: first_file_contents,
"second.rs": "// Second Rust file",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
cx.dispatch_action(window_id, Toggle);
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
let file_query = &first_file_name[..3];
let file_row = 200;
let file_column = 300;
assert!(file_column > first_file_contents.len());
let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
finder
.update(cx, |finder, cx| {
finder
.delegate_mut()
.update_matches(query_outside_file.to_string(), cx)
})
.await;
finder.read_with(cx, |finder, _| {
let finder = finder.delegate();
assert_eq!(finder.matches.len(), 1);
let latest_search_query = finder
.latest_search_query
.as_ref()
.expect("Finder should have a query after the update_matches call");
assert_eq!(latest_search_query.path_like.raw_query, query_outside_file);
assert_eq!(
latest_search_query.path_like.file_query_end,
Some(file_query.len())
);
assert_eq!(latest_search_query.row, Some(file_row));
assert_eq!(latest_search_query.column, Some(file_column as u32));
});
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
cx.dispatch_action(window_id, SelectNext);
cx.dispatch_action(window_id, Confirm);
active_pane
.condition(cx, |pane, _| pane.active_item().is_some())
.await;
let editor = cx.update(|cx| {
let active_item = active_pane.read(cx).active_item().unwrap();
active_item.downcast::<Editor>().unwrap()
});
deterministic.advance_clock(std::time::Duration::from_secs(2));
deterministic.start_waiting();
deterministic.finish_waiting();
editor.update(cx, |editor, cx| {
let all_selections = editor.selections.all_adjusted(cx);
assert_eq!(
all_selections.len(),
1,
"Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
);
let caret_selection = all_selections.into_iter().next().unwrap();
assert_eq!(caret_selection.start, caret_selection.end,
"Caret selection should have its start and end at the same position");
assert_eq!(0, caret_selection.start.row,
"Excessive rows (as in query outside file borders) should get trimmed to last file row");
assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
"Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
});
}
#[gpui::test]
async fn test_matching_cancellation(cx: &mut gpui::TestAppContext) {
let app_state = cx.update(AppState::test);
@@ -371,7 +628,7 @@ mod tests {
)
});
let query = "hi".to_string();
let query = test_path_like("hi");
finder
.update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx))
.await;
@@ -455,7 +712,9 @@ mod tests {
)
});
finder
.update(cx, |f, cx| f.delegate_mut().spawn_search("hi".into(), cx))
.update(cx, |f, cx| {
f.delegate_mut().spawn_search(test_path_like("hi"), cx)
})
.await;
finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7));
}
@@ -491,7 +750,9 @@ mod tests {
// Even though there is only one worktree, that worktree's filename
// is included in the matching, because the worktree is a single file.
finder
.update(cx, |f, cx| f.delegate_mut().spawn_search("thf".into(), cx))
.update(cx, |f, cx| {
f.delegate_mut().spawn_search(test_path_like("thf"), cx)
})
.await;
cx.read(|cx| {
let finder = finder.read(cx);
@@ -509,7 +770,9 @@ mod tests {
// Since the worktree root is a file, searching for its name followed by a slash does
// not match anything.
finder
.update(cx, |f, cx| f.delegate_mut().spawn_search("thf/".into(), cx))
.update(cx, |f, cx| {
f.delegate_mut().spawn_search(test_path_like("thf/"), cx)
})
.await;
finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0));
}
@@ -553,7 +816,9 @@ mod tests {
// Run a search that matches two files with the same relative path.
finder
.update(cx, |f, cx| f.delegate_mut().spawn_search("a.t".into(), cx))
.update(cx, |f, cx| {
f.delegate_mut().spawn_search(test_path_like("a.t"), cx)
})
.await;
// Can switch between different matches with the same relative path.
@@ -609,7 +874,7 @@ mod tests {
finder
.update(cx, |f, cx| {
f.delegate_mut().spawn_search("a.txt".into(), cx)
f.delegate_mut().spawn_search(test_path_like("a.txt"), cx)
})
.await;
@@ -651,11 +916,27 @@ mod tests {
)
});
finder
.update(cx, |f, cx| f.delegate_mut().spawn_search("dir".into(), cx))
.update(cx, |f, cx| {
f.delegate_mut().spawn_search(test_path_like("dir"), cx)
})
.await;
cx.read(|cx| {
let finder = finder.read(cx);
assert_eq!(finder.delegate().matches.len(), 0);
});
}
fn test_path_like(test_str: &str) -> PathLikeWithPosition<FileSearchQuery> {
PathLikeWithPosition::parse_str(test_str, |path_like_str| {
Ok::<_, std::convert::Infallible>(FileSearchQuery {
raw_query: test_str.to_owned(),
file_query_end: if path_like_str == test_str {
None
} else {
Some(path_like_str.len())
},
})
})
.unwrap()
}
}

View File

@@ -13,6 +13,7 @@ gpui = { path = "../gpui" }
lsp = { path = "../lsp" }
rope = { path = "../rope" }
util = { path = "../util" }
sum_tree = { path = "../sum_tree" }
anyhow.workspace = true
async-trait.workspace = true
futures.workspace = true

View File

@@ -27,7 +27,7 @@ use util::ResultExt;
#[cfg(any(test, feature = "test-support"))]
use collections::{btree_map, BTreeMap};
#[cfg(any(test, feature = "test-support"))]
use repository::FakeGitRepositoryState;
use repository::{FakeGitRepositoryState, GitFileStatus};
#[cfg(any(test, feature = "test-support"))]
use std::sync::Weak;
@@ -572,15 +572,15 @@ impl FakeFs {
Ok(())
}
pub async fn pause_events(&self) {
pub fn pause_events(&self) {
self.state.lock().events_paused = true;
}
pub async fn buffered_event_count(&self) -> usize {
pub fn buffered_event_count(&self) -> usize {
self.state.lock().buffered_events.len()
}
pub async fn flush_events(&self, count: usize) {
pub fn flush_events(&self, count: usize) {
self.state.lock().flush_events(count);
}
@@ -619,7 +619,10 @@ impl FakeFs {
.boxed()
}
pub async fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
pub fn with_git_state<F>(&self, dot_git: &Path, f: F)
where
F: FnOnce(&mut FakeGitRepositoryState),
{
let mut state = self.state.lock();
let entry = state.read_path(dot_git).unwrap();
let mut entry = entry.lock();
@@ -628,12 +631,7 @@ impl FakeFs {
let repo_state = git_repo_state.get_or_insert_with(Default::default);
let mut repo_state = repo_state.lock();
repo_state.index_contents.clear();
repo_state.index_contents.extend(
head_state
.iter()
.map(|(path, content)| (path.to_path_buf(), content.clone())),
);
f(&mut repo_state);
state.emit_event([dot_git]);
} else {
@@ -641,6 +639,32 @@ impl FakeFs {
}
}
pub async fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
self.with_git_state(dot_git, |state| state.branch_name = branch.map(Into::into))
}
pub async fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
self.with_git_state(dot_git, |state| {
state.index_contents.clear();
state.index_contents.extend(
head_state
.iter()
.map(|(path, content)| (path.to_path_buf(), content.clone())),
);
});
}
pub async fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&Path, GitFileStatus)]) {
self.with_git_state(dot_git, |state| {
state.worktree_statuses.clear();
state.worktree_statuses.extend(
statuses
.iter()
.map(|(path, content)| ((**path).into(), content.clone())),
);
});
}
pub fn paths(&self) -> Vec<PathBuf> {
let mut result = Vec::new();
let mut queue = collections::VecDeque::new();
@@ -808,14 +832,16 @@ impl Fs for FakeFs {
let old_path = normalize_path(old_path);
let new_path = normalize_path(new_path);
let mut state = self.state.lock();
let moved_entry = state.write_path(&old_path, |e| {
if let btree_map::Entry::Occupied(e) = e {
Ok(e.remove())
Ok(e.get().clone())
} else {
Err(anyhow!("path does not exist: {}", &old_path.display()))
}
})?;
state.write_path(&new_path, |e| {
match e {
btree_map::Entry::Occupied(mut e) => {
@@ -831,6 +857,17 @@ impl Fs for FakeFs {
}
Ok(())
})?;
state
.write_path(&old_path, |e| {
if let btree_map::Entry::Occupied(e) = e {
Ok(e.remove())
} else {
unreachable!()
}
})
.unwrap();
state.emit_event(&[old_path, new_path]);
Ok(())
}

View File

@@ -1,10 +1,16 @@
use anyhow::Result;
use collections::HashMap;
use parking_lot::Mutex;
use serde_derive::{Deserialize, Serialize};
use std::{
cmp::Ordering,
ffi::OsStr,
os::unix::prelude::OsStrExt,
path::{Component, Path, PathBuf},
sync::Arc,
};
use sum_tree::{MapSeekTarget, TreeMap};
use util::ResultExt;
pub use git2::Repository as LibGitRepository;
@@ -13,6 +19,18 @@ pub trait GitRepository: Send {
fn reload_index(&self);
fn load_index_text(&self, relative_file_path: &Path) -> Option<String>;
fn branch_name(&self) -> Option<String>;
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>>;
fn status(&self, path: &RepoPath) -> Option<GitFileStatus>;
}
impl std::fmt::Debug for dyn GitRepository {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("dyn GitRepository<...>").finish()
}
}
#[async_trait::async_trait]
@@ -46,6 +64,54 @@ impl GitRepository for LibGitRepository {
}
None
}
fn branch_name(&self) -> Option<String> {
let head = self.head().log_err()?;
let branch = String::from_utf8_lossy(head.shorthand_bytes());
Some(branch.to_string())
}
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>> {
let statuses = self.statuses(None).log_err()?;
let mut map = TreeMap::default();
for status in statuses
.iter()
.filter(|status| !status.status().contains(git2::Status::IGNORED))
{
let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes())));
let Some(status) = read_status(status.status()) else {
continue
};
map.insert(path, status)
}
Some(map)
}
fn status(&self, path: &RepoPath) -> Option<GitFileStatus> {
let status = self.status_file(path).log_err()?;
read_status(status)
}
}
fn read_status(status: git2::Status) -> Option<GitFileStatus> {
if status.contains(git2::Status::CONFLICTED) {
Some(GitFileStatus::Conflict)
} else if status.intersects(
git2::Status::WT_MODIFIED
| git2::Status::WT_RENAMED
| git2::Status::INDEX_MODIFIED
| git2::Status::INDEX_RENAMED,
) {
Some(GitFileStatus::Modified)
} else if status.intersects(git2::Status::WT_NEW | git2::Status::INDEX_NEW) {
Some(GitFileStatus::Added)
} else {
None
}
}
#[derive(Debug, Clone, Default)]
@@ -56,6 +122,8 @@ pub struct FakeGitRepository {
#[derive(Debug, Clone, Default)]
pub struct FakeGitRepositoryState {
pub index_contents: HashMap<PathBuf, String>,
pub worktree_statuses: HashMap<RepoPath, GitFileStatus>,
pub branch_name: Option<String>,
}
impl FakeGitRepository {
@@ -72,6 +140,25 @@ impl GitRepository for FakeGitRepository {
let state = self.state.lock();
state.index_contents.get(path).cloned()
}
fn branch_name(&self) -> Option<String> {
let state = self.state.lock();
state.branch_name.clone()
}
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>> {
let state = self.state.lock();
let mut map = TreeMap::default();
for (repo_path, status) in state.worktree_statuses.iter() {
map.insert(repo_path.to_owned(), status.to_owned());
}
Some(map)
}
fn status(&self, path: &RepoPath) -> Option<GitFileStatus> {
let state = self.state.lock();
state.worktree_statuses.get(path).cloned()
}
}
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
@@ -102,3 +189,66 @@ fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
_ => Ok(()),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum GitFileStatus {
Added,
Modified,
Conflict,
}
#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
pub struct RepoPath(PathBuf);
impl RepoPath {
pub fn new(path: PathBuf) -> Self {
debug_assert!(path.is_relative(), "Repo paths must be relative");
RepoPath(path)
}
}
impl From<&Path> for RepoPath {
fn from(value: &Path) -> Self {
RepoPath::new(value.to_path_buf())
}
}
impl From<PathBuf> for RepoPath {
fn from(value: PathBuf) -> Self {
RepoPath::new(value)
}
}
impl Default for RepoPath {
fn default() -> Self {
RepoPath(PathBuf::new())
}
}
impl AsRef<Path> for RepoPath {
fn as_ref(&self) -> &Path {
self.0.as_ref()
}
}
impl std::ops::Deref for RepoPath {
type Target = PathBuf;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug)]
pub struct RepoPathDescendants<'a>(pub &'a Path);
impl<'a> MapSeekTarget<RepoPath> for RepoPathDescendants<'a> {
fn cmp_cursor(&self, key: &RepoPath) -> Ordering {
if key.starts_with(&self.0) {
Ordering::Greater
} else {
self.0.cmp(key)
}
}
}

View File

@@ -16,3 +16,4 @@ settings = { path = "../settings" }
text = { path = "../text" }
workspace = { path = "../workspace" }
postage.workspace = true
util = { path = "../util" }

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
use gpui::{
actions, elements::*, geometry::vector::Vector2F, AnyViewHandle, AppContext, Axis, Entity,
View, ViewContext, ViewHandle,
@@ -8,6 +8,7 @@ use gpui::{
use menu::{Cancel, Confirm};
use settings::Settings;
use text::{Bias, Point};
use util::paths::FILE_ROW_COLUMN_DELIMITER;
use workspace::{Modal, Workspace};
actions!(go_to_line, [Toggle]);
@@ -75,15 +76,16 @@ impl GoToLine {
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
self.prev_scroll_position.take();
self.active_editor.update(cx, |active_editor, cx| {
if let Some(rows) = active_editor.highlighted_rows() {
if let Some(point) = self.point_from_query(cx) {
self.active_editor.update(cx, |active_editor, cx| {
let snapshot = active_editor.snapshot(cx).display_snapshot;
let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
active_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
s.select_ranges([position..position])
s.select_ranges([point..point])
});
}
});
});
}
cx.emit(Event::Dismissed);
}
@@ -96,16 +98,7 @@ impl GoToLine {
match event {
editor::Event::Blurred => cx.emit(Event::Dismissed),
editor::Event::BufferEdited { .. } => {
let line_editor = self.line_editor.read(cx).text(cx);
let mut components = line_editor.trim().split(&[',', ':'][..]);
let row = components.next().and_then(|row| row.parse::<u32>().ok());
let column = components.next().and_then(|row| row.parse::<u32>().ok());
if let Some(point) = row.map(|row| {
Point::new(
row.saturating_sub(1),
column.map(|column| column.saturating_sub(1)).unwrap_or(0),
)
}) {
if let Some(point) = self.point_from_query(cx) {
self.active_editor.update(cx, |active_editor, cx| {
let snapshot = active_editor.snapshot(cx).display_snapshot;
let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
@@ -120,6 +113,20 @@ impl GoToLine {
_ => {}
}
}
fn point_from_query(&self, cx: &ViewContext<Self>) -> Option<Point> {
let line_editor = self.line_editor.read(cx).text(cx);
let mut components = line_editor
.splitn(2, FILE_ROW_COLUMN_DELIMITER)
.map(str::trim)
.fuse();
let row = components.next().and_then(|row| row.parse::<u32>().ok())?;
let column = components.next().and_then(|col| col.parse::<u32>().ok());
Some(Point::new(
row.saturating_sub(1),
column.unwrap_or(0).saturating_sub(1),
))
}
}
impl Entity for GoToLine {
@@ -147,7 +154,7 @@ impl View for GoToLine {
let theme = &cx.global::<Settings>().theme.picker;
let label = format!(
"{},{} of {} lines",
"{}{FILE_ROW_COLUMN_DELIMITER}{} of {} lines",
self.cursor_point.row + 1,
self.cursor_point.column + 1,
self.max_point.row + 1

View File

@@ -48,7 +48,7 @@ smallvec.workspace = true
smol.workspace = true
time.workspace = true
tiny-skia = "0.5"
usvg = "0.14"
usvg = { version = "0.14", features = [] }
uuid = { version = "1.1.2", features = ["v4"] }
waker-fn = "1.1.0"

File diff suppressed because it is too large Load Diff

View File

@@ -81,7 +81,7 @@ pub(crate) fn setup_menu_handlers(foreground_platform: &dyn ForegroundPlatform,
let dispatched = cx
.update_window(main_window_id, |cx| {
if let Some(view_id) = cx.focused_view_id() {
cx.handle_dispatch_action_from_effect(Some(view_id), action);
cx.dispatch_action(Some(view_id), action);
true
} else {
false

View File

@@ -1,17 +1,18 @@
use crate::{
executor,
geometry::vector::Vector2F,
keymap_matcher::Keystroke,
keymap_matcher::{Binding, Keystroke},
platform,
platform::{Event, InputHandler, KeyDownEvent, Platform},
Action, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Entity, FontCache,
Handle, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle,
WeakHandle, WindowContext,
Action, AppContext, BorrowAppContext, BorrowWindowContext, Entity, FontCache, Handle,
ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakHandle,
WindowContext,
};
use collections::BTreeMap;
use futures::Future;
use itertools::Itertools;
use parking_lot::{Mutex, RwLock};
use smallvec::SmallVec;
use smol::stream::StreamExt;
use std::{
any::Any,
@@ -71,17 +72,24 @@ impl TestAppContext {
cx
}
pub fn dispatch_action<A: Action>(&self, window_id: usize, action: A) {
self.cx
.borrow_mut()
.update_window(window_id, |window| {
window.handle_dispatch_action_from_effect(window.focused_view_id(), &action);
})
.expect("window not found");
pub fn dispatch_action<A: Action>(&mut self, window_id: usize, action: A) {
self.update_window(window_id, |window| {
window.dispatch_action(window.focused_view_id(), &action);
})
.expect("window not found");
}
pub fn dispatch_global_action<A: Action>(&self, action: A) {
self.cx.borrow_mut().dispatch_global_action_any(&action);
pub fn available_actions(
&self,
window_id: usize,
view_id: usize,
) -> Vec<(&'static str, Box<dyn Action>, SmallVec<[Binding; 1]>)> {
self.read_window(window_id, |cx| cx.available_actions(view_id))
.unwrap_or_default()
}
pub fn dispatch_global_action<A: Action>(&mut self, action: A) {
self.update(|cx| cx.dispatch_global_action_any(&action));
}
pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) {
@@ -153,12 +161,13 @@ impl TestAppContext {
(window_id, view)
}
pub fn add_view<T, F>(&mut self, parent_handle: &AnyViewHandle, build_view: F) -> ViewHandle<T>
pub fn add_view<T, F>(&mut self, window_id: usize, build_view: F) -> ViewHandle<T>
where
T: View,
F: FnOnce(&mut ViewContext<T>) -> T,
{
self.cx.borrow_mut().add_view(parent_handle, build_view)
self.update_window(window_id, |cx| cx.add_view(build_view))
.expect("window not found")
}
pub fn observe_global<E, F>(&mut self, callback: F) -> Subscription

View File

@@ -2,7 +2,7 @@ use crate::{
elements::AnyRootElement,
geometry::rect::RectF,
json::ToJson,
keymap_matcher::{Binding, Keystroke, MatchResult},
keymap_matcher::{Binding, KeymapContext, Keystroke, MatchResult},
platform::{
self, Appearance, CursorStyle, Event, KeyDownEvent, KeyUpEvent, ModifiersChangedEvent,
MouseButton, MouseMovedEvent, PromptLevel, WindowBounds,
@@ -14,7 +14,7 @@ use crate::{
text_layout::TextLayoutCache,
util::post_inc,
Action, AnyView, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Effect,
Element, Entity, Handle, MouseRegion, MouseRegionId, ParentId, SceneBuilder, Subscription,
Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, SceneBuilder, Subscription,
View, ViewContext, ViewHandle, WindowInvalidation,
};
use anyhow::{anyhow, bail, Result};
@@ -34,11 +34,12 @@ use std::{
use util::ResultExt;
use uuid::Uuid;
use super::Reference;
use super::{Reference, ViewMetadata};
pub struct Window {
pub(crate) root_view: Option<AnyViewHandle>,
pub(crate) focused_view_id: Option<usize>,
pub(crate) parents: HashMap<usize, usize>,
pub(crate) is_active: bool,
pub(crate) is_fullscreen: bool,
pub(crate) invalidation: Option<WindowInvalidation>,
@@ -72,6 +73,7 @@ impl Window {
let mut window = Self {
root_view: None,
focused_view_id: None,
parents: Default::default(),
is_active: false,
invalidation: None,
is_fullscreen: false,
@@ -90,11 +92,9 @@ impl Window {
};
let mut window_context = WindowContext::mutable(cx, &mut window, window_id);
let root_view = window_context
.build_and_insert_view(ParentId::Root, |cx| Some(build_view(cx)))
.unwrap();
if let Some(mut invalidation) = window_context.window.invalidation.take() {
window_context.invalidate(&mut invalidation, appearance);
let root_view = window_context.add_view(|cx| build_view(cx));
if let Some(invalidation) = window_context.window.invalidation.take() {
window_context.invalidate(invalidation, appearance);
}
window.focused_view_id = Some(root_view.id());
window.root_view = Some(root_view.into_any());
@@ -113,7 +113,6 @@ pub struct WindowContext<'a> {
pub(crate) app_context: Reference<'a, AppContext>,
pub(crate) window: Reference<'a, Window>,
pub(crate) window_id: usize,
pub(crate) refreshing: bool,
pub(crate) removed: bool,
}
@@ -169,7 +168,6 @@ impl<'a> WindowContext<'a> {
app_context: Reference::Mutable(app_context),
window: Reference::Mutable(window),
window_id,
refreshing: false,
removed: false,
}
}
@@ -179,7 +177,6 @@ impl<'a> WindowContext<'a> {
app_context: Reference::Immutable(app_context),
window: Reference::Immutable(window),
window_id,
refreshing: false,
removed: false,
}
}
@@ -359,57 +356,17 @@ impl<'a> WindowContext<'a> {
)
}
/// Return keystrokes that would dispatch the given action on the given view.
pub(crate) fn keystrokes_for_action(
&mut self,
view_id: usize,
action: &dyn Action,
) -> Option<SmallVec<[Keystroke; 2]>> {
let window_id = self.window_id;
let mut contexts = Vec::new();
let mut handler_depth = None;
for (i, view_id) in self.ancestors(view_id).enumerate() {
if let Some(view) = self.views.get(&(window_id, view_id)) {
if let Some(actions) = self.actions.get(&view.as_any().type_id()) {
if actions.contains_key(&action.as_any().type_id()) {
handler_depth = Some(i);
}
}
contexts.push(view.keymap_context(self));
}
}
if self.global_actions.contains_key(&action.as_any().type_id()) {
handler_depth = Some(contexts.len())
}
self.keystroke_matcher
.bindings_for_action_type(action.as_any().type_id())
.find_map(|b| {
handler_depth
.map(|highest_handler| {
if (0..=highest_handler).any(|depth| b.match_context(&contexts[depth..])) {
Some(b.keystrokes().into())
} else {
None
}
})
.flatten()
})
}
pub fn available_actions(
pub(crate) fn available_actions(
&self,
view_id: usize,
) -> impl Iterator<Item = (&'static str, Box<dyn Action>, SmallVec<[&Binding; 1]>)> {
) -> Vec<(&'static str, Box<dyn Action>, SmallVec<[Binding; 1]>)> {
let window_id = self.window_id;
let mut contexts = Vec::new();
let mut handler_depths_by_action_type = HashMap::<TypeId, usize>::default();
for (depth, view_id) in self.ancestors(view_id).enumerate() {
if let Some(view) = self.views.get(&(window_id, view_id)) {
contexts.push(view.keymap_context(self));
let view_type = view.as_any().type_id();
if let Some(actions) = self.actions.get(&view_type) {
if let Some(view_metadata) = self.views_metadata.get(&(window_id, view_id)) {
contexts.push(view_metadata.keymap_context.clone());
if let Some(actions) = self.actions.get(&view_metadata.type_id) {
handler_depths_by_action_type.extend(
actions
.keys()
@@ -444,23 +401,25 @@ impl<'a> WindowContext<'a> {
.filter(|b| {
(0..=action_depth).any(|depth| b.match_context(&contexts[depth..]))
})
.cloned()
.collect(),
))
} else {
None
}
})
.collect()
}
pub fn dispatch_keystroke(&mut self, keystroke: &Keystroke) -> bool {
pub(crate) fn dispatch_keystroke(&mut self, keystroke: &Keystroke) -> bool {
let window_id = self.window_id;
if let Some(focused_view_id) = self.focused_view_id() {
let dispatch_path = self
.ancestors(focused_view_id)
.filter_map(|view_id| {
self.views
self.views_metadata
.get(&(window_id, view_id))
.map(|view| (view_id, view.keymap_context(self)))
.map(|view| (view_id, view.keymap_context.clone()))
})
.collect();
@@ -474,8 +433,7 @@ impl<'a> WindowContext<'a> {
MatchResult::Pending => true,
MatchResult::Matches(matches) => {
for (view_id, action) in matches {
if self.handle_dispatch_action_from_effect(Some(*view_id), action.as_ref())
{
if self.dispatch_action(Some(*view_id), action.as_ref()) {
self.keystroke_matcher.clear_pending();
handled_by = Some(action.boxed_clone());
break;
@@ -498,7 +456,7 @@ impl<'a> WindowContext<'a> {
}
}
pub fn dispatch_event(&mut self, event: Event, event_reused: bool) -> bool {
pub(crate) fn dispatch_event(&mut self, event: Event, event_reused: bool) -> bool {
let mut mouse_events = SmallVec::<[_; 2]>::new();
let mut notified_views: HashSet<usize> = Default::default();
let window_id = self.window_id;
@@ -834,7 +792,7 @@ impl<'a> WindowContext<'a> {
any_event_handled
}
pub fn dispatch_key_down(&mut self, event: &KeyDownEvent) -> bool {
pub(crate) fn dispatch_key_down(&mut self, event: &KeyDownEvent) -> bool {
let window_id = self.window_id;
if let Some(focused_view_id) = self.window.focused_view_id {
for view_id in self.ancestors(focused_view_id).collect::<Vec<_>>() {
@@ -853,7 +811,7 @@ impl<'a> WindowContext<'a> {
false
}
pub fn dispatch_key_up(&mut self, event: &KeyUpEvent) -> bool {
pub(crate) fn dispatch_key_up(&mut self, event: &KeyUpEvent) -> bool {
let window_id = self.window_id;
if let Some(focused_view_id) = self.window.focused_view_id {
for view_id in self.ancestors(focused_view_id).collect::<Vec<_>>() {
@@ -872,7 +830,7 @@ impl<'a> WindowContext<'a> {
false
}
pub fn dispatch_modifiers_changed(&mut self, event: &ModifiersChangedEvent) -> bool {
pub(crate) fn dispatch_modifiers_changed(&mut self, event: &ModifiersChangedEvent) -> bool {
let window_id = self.window_id;
if let Some(focused_view_id) = self.window.focused_view_id {
for view_id in self.ancestors(focused_view_id).collect::<Vec<_>>() {
@@ -891,7 +849,7 @@ impl<'a> WindowContext<'a> {
false
}
pub fn invalidate(&mut self, invalidation: &mut WindowInvalidation, appearance: Appearance) {
pub fn invalidate(&mut self, mut invalidation: WindowInvalidation, appearance: Appearance) {
self.start_frame();
self.window.appearance = appearance;
for view_id in &invalidation.removed {
@@ -932,13 +890,52 @@ impl<'a> WindowContext<'a> {
Ok(element)
}
pub fn build_scene(&mut self) -> Result<Scene> {
pub(crate) fn layout(&mut self, refreshing: bool) -> Result<()> {
let window_size = self.window.platform_window.content_size();
let root_view_id = self.window.root_view().id();
let mut rendered_root = self.window.rendered_views.remove(&root_view_id).unwrap();
let mut new_parents = HashMap::default();
let mut views_to_notify_if_ancestors_change = HashMap::default();
rendered_root.layout(
SizeConstraint::strict(window_size),
&mut new_parents,
&mut views_to_notify_if_ancestors_change,
refreshing,
self,
)?;
for (view_id, view_ids_to_notify) in views_to_notify_if_ancestors_change {
let mut current_view_id = view_id;
loop {
let old_parent_id = self.window.parents.get(&current_view_id);
let new_parent_id = new_parents.get(&current_view_id);
if old_parent_id.is_none() && new_parent_id.is_none() {
break;
} else if old_parent_id == new_parent_id {
current_view_id = *old_parent_id.unwrap();
} else {
let window_id = self.window_id;
for view_id_to_notify in view_ids_to_notify {
self.notify_view(window_id, view_id_to_notify);
}
break;
}
}
}
self.window.parents = new_parents;
self.window
.rendered_views
.insert(root_view_id, rendered_root);
Ok(())
}
pub(crate) fn paint(&mut self) -> Result<Scene> {
let window_size = self.window.platform_window.content_size();
let scale_factor = self.window.platform_window.scale_factor();
let root_view_id = self.window.root_view().id();
let mut rendered_root = self.window.rendered_views.remove(&root_view_id).unwrap();
rendered_root.layout(SizeConstraint::strict(window_size), self)?;
let mut scene_builder = SceneBuilder::new(scale_factor);
rendered_root.paint(
@@ -1001,11 +998,7 @@ impl<'a> WindowContext<'a> {
self.window.is_fullscreen
}
pub(crate) fn handle_dispatch_action_from_effect(
&mut self,
view_id: Option<usize>,
action: &dyn Action,
) -> bool {
pub(crate) fn dispatch_action(&mut self, view_id: Option<usize>, action: &dyn Action) -> bool {
if let Some(view_id) = view_id {
self.halt_action_dispatch = false;
self.visit_dispatch_path(view_id, |view_id, capture_phase, cx| {
@@ -1051,9 +1044,7 @@ impl<'a> WindowContext<'a> {
std::iter::once(view_id)
.into_iter()
.chain(std::iter::from_fn(move || {
if let Some(ParentId::View(parent_id)) =
self.parents.get(&(self.window_id, view_id))
{
if let Some(parent_id) = self.window.parents.get(&view_id) {
view_id = *parent_id;
Some(view_id)
} else {
@@ -1062,16 +1053,6 @@ impl<'a> WindowContext<'a> {
}))
}
/// Returns the id of the parent of the given view, or none if the given
/// view is the root.
pub(crate) fn parent(&self, view_id: usize) -> Option<usize> {
if let Some(ParentId::View(view_id)) = self.parents.get(&(self.window_id, view_id)) {
Some(*view_id)
} else {
None
}
}
// Traverses the parent tree. Walks down the tree toward the passed
// view calling visit with true. Then walks back up the tree calling visit with false.
// If `visit` returns false this function will immediately return.
@@ -1102,16 +1083,6 @@ impl<'a> WindowContext<'a> {
self.window.focused_view_id
}
pub fn is_child_focused(&self, view: &AnyViewHandle) -> bool {
if let Some(focused_view_id) = self.focused_view_id() {
self.ancestors(focused_view_id)
.skip(1) // Skip self id
.any(|parent| parent == view.view_id)
} else {
false
}
}
pub fn window_bounds(&self) -> WindowBounds {
self.window.platform_window.bounds()
}
@@ -1154,29 +1125,38 @@ impl<'a> WindowContext<'a> {
V: View,
F: FnOnce(&mut ViewContext<V>) -> V,
{
let root_view = self
.build_and_insert_view(ParentId::Root, |cx| Some(build_root_view(cx)))
.unwrap();
let root_view = self.add_view(|cx| build_root_view(cx));
self.window.root_view = Some(root_view.clone().into_any());
self.window.focused_view_id = Some(root_view.id());
root_view
}
pub(crate) fn build_and_insert_view<T, F>(
&mut self,
parent_id: ParentId,
build_view: F,
) -> Option<ViewHandle<T>>
pub fn add_view<T, F>(&mut self, build_view: F) -> ViewHandle<T>
where
T: View,
F: FnOnce(&mut ViewContext<T>) -> T,
{
self.add_option_view(|cx| Some(build_view(cx))).unwrap()
}
pub fn add_option_view<T, F>(&mut self, build_view: F) -> Option<ViewHandle<T>>
where
T: View,
F: FnOnce(&mut ViewContext<T>) -> Option<T>,
{
let window_id = self.window_id;
let view_id = post_inc(&mut self.next_entity_id);
// Make sure we can tell child views about their parentu
self.parents.insert((window_id, view_id), parent_id);
let mut cx = ViewContext::mutable(self, view_id);
let handle = if let Some(view) = build_view(&mut cx) {
let mut keymap_context = KeymapContext::default();
view.update_keymap_context(&mut keymap_context, cx.app_context());
self.views_metadata.insert(
(window_id, view_id),
ViewMetadata {
type_id: TypeId::of::<T>(),
keymap_context,
},
);
self.views.insert((window_id, view_id), Box::new(view));
self.window
.invalidation
@@ -1185,7 +1165,6 @@ impl<'a> WindowContext<'a> {
.insert(view_id);
Some(ViewHandle::new(window_id, view_id, &self.ref_counts))
} else {
self.parents.remove(&(window_id, view_id));
None
};
handle
@@ -1366,11 +1345,18 @@ impl<V: View> Element<V> for ChildView {
&mut self,
constraint: SizeConstraint,
_: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
if let Some(mut rendered_view) = cx.window.rendered_views.remove(&self.view_id) {
cx.new_parents.insert(self.view_id, cx.view_id());
let size = rendered_view
.layout(constraint, cx)
.layout(
constraint,
cx.new_parents,
cx.views_to_notify_if_ancestors_change,
cx.refreshing,
cx.view_context,
)
.log_err()
.unwrap_or(Vector2F::zero());
cx.window.rendered_views.insert(self.view_id, rendered_view);

View File

@@ -42,7 +42,7 @@ impl Color {
}
pub fn yellow() -> Self {
Self(ColorU::from_u32(0x00ffffff))
Self(ColorU::from_u32(0xffff00ff))
}
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {

View File

@@ -33,11 +33,14 @@ use crate::{
rect::RectF,
vector::{vec2f, Vector2F},
},
json, Action, SceneBuilder, SizeConstraint, View, ViewContext, WeakViewHandle, WindowContext,
json, Action, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, WeakViewHandle,
WindowContext,
};
use anyhow::{anyhow, Result};
use collections::HashMap;
use core::panic;
use json::ToJson;
use smallvec::SmallVec;
use std::{
any::Any,
borrow::Cow,
@@ -54,7 +57,7 @@ pub trait Element<V: View>: 'static {
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState);
fn paint(
@@ -211,7 +214,7 @@ trait AnyElementState<V: View> {
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> Vector2F;
fn paint(
@@ -263,7 +266,7 @@ impl<V: View, E: Element<V>> AnyElementState<V> for ElementState<V, E> {
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> Vector2F {
let result;
*self = match mem::take(self) {
@@ -444,7 +447,7 @@ impl<V: View> AnyElement<V> {
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> Vector2F {
self.state.layout(constraint, view, cx)
}
@@ -505,7 +508,7 @@ impl<V: View> Element<V> for AnyElement<V> {
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
let size = self.layout(constraint, view, cx);
(size, ())
@@ -575,6 +578,15 @@ pub struct ComponentHost<V: View, C: Component<V>> {
view_type: PhantomData<V>,
}
impl<V: View, C: Component<V>> ComponentHost<V, C> {
pub fn new(c: C) -> Self {
Self {
component: c,
view_type: PhantomData,
}
}
}
impl<V: View, C: Component<V>> Deref for ComponentHost<V, C> {
type Target = C;
@@ -597,7 +609,7 @@ impl<V: View, C: Component<V>> Element<V> for ComponentHost<V, C> {
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, AnyElement<V>) {
let mut element = self.component.render(view, cx);
let size = element.layout(constraint, view, cx);
@@ -642,7 +654,14 @@ impl<V: View, C: Component<V>> Element<V> for ComponentHost<V, C> {
}
pub trait AnyRootElement {
fn layout(&mut self, constraint: SizeConstraint, cx: &mut WindowContext) -> Result<Vector2F>;
fn layout(
&mut self,
constraint: SizeConstraint,
new_parents: &mut HashMap<usize, usize>,
views_to_notify_if_ancestors_change: &mut HashMap<usize, SmallVec<[usize; 2]>>,
refreshing: bool,
cx: &mut WindowContext,
) -> Result<Vector2F>;
fn paint(
&mut self,
scene: &mut SceneBuilder,
@@ -660,12 +679,27 @@ pub trait AnyRootElement {
}
impl<V: View> AnyRootElement for RootElement<V> {
fn layout(&mut self, constraint: SizeConstraint, cx: &mut WindowContext) -> Result<Vector2F> {
fn layout(
&mut self,
constraint: SizeConstraint,
new_parents: &mut HashMap<usize, usize>,
views_to_notify_if_ancestors_change: &mut HashMap<usize, SmallVec<[usize; 2]>>,
refreshing: bool,
cx: &mut WindowContext,
) -> Result<Vector2F> {
let view = self
.view
.upgrade(cx)
.ok_or_else(|| anyhow!("layout called on a root element for a dropped view"))?;
view.update(cx, |view, cx| Ok(self.element.layout(constraint, view, cx)))
view.update(cx, |view, cx| {
let mut cx = LayoutContext::new(
cx,
new_parents,
views_to_notify_if_ancestors_change,
refreshing,
);
Ok(self.element.layout(constraint, view, &mut cx))
})
}
fn paint(

View File

@@ -1,6 +1,6 @@
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json, AnyElement, Element, SceneBuilder, SizeConstraint, View, ViewContext,
json, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
};
use json::ToJson;
@@ -48,7 +48,7 @@ impl<V: View> Element<V> for Align<V> {
&mut self,
mut constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
let mut size = constraint.max;
constraint.min = Vector2F::zero();

View File

@@ -34,7 +34,7 @@ where
&mut self,
constraint: crate::SizeConstraint,
_: &mut V,
_: &mut crate::ViewContext<V>,
_: &mut crate::LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
let x = if constraint.max.x().is_finite() {
constraint.max.x()

View File

@@ -3,7 +3,9 @@ use std::ops::Range;
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
use serde_json::json;
use crate::{json, AnyElement, Element, SceneBuilder, SizeConstraint, View, ViewContext};
use crate::{
json, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
};
pub struct Clipped<V: View> {
child: AnyElement<V>,
@@ -23,7 +25,7 @@ impl<V: View> Element<V> for Clipped<V> {
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
(self.child.layout(constraint, view, cx), ())
}

View File

@@ -5,7 +5,7 @@ use serde_json::json;
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json, AnyElement, Element, SceneBuilder, SizeConstraint, View, ViewContext,
json, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
};
pub struct ConstrainedBox<V: View> {
@@ -15,7 +15,7 @@ pub struct ConstrainedBox<V: View> {
pub enum Constraint<V: View> {
Static(SizeConstraint),
Dynamic(Box<dyn FnMut(SizeConstraint, &mut V, &mut ViewContext<V>) -> SizeConstraint>),
Dynamic(Box<dyn FnMut(SizeConstraint, &mut V, &mut LayoutContext<V>) -> SizeConstraint>),
}
impl<V: View> ToJson for Constraint<V> {
@@ -37,7 +37,8 @@ impl<V: View> ConstrainedBox<V> {
pub fn dynamically(
mut self,
constraint: impl 'static + FnMut(SizeConstraint, &mut V, &mut ViewContext<V>) -> SizeConstraint,
constraint: impl 'static
+ FnMut(SizeConstraint, &mut V, &mut LayoutContext<V>) -> SizeConstraint,
) -> Self {
self.constraint = Constraint::Dynamic(Box::new(constraint));
self
@@ -119,7 +120,7 @@ impl<V: View> ConstrainedBox<V> {
&mut self,
input_constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> SizeConstraint {
match &mut self.constraint {
Constraint::Static(constraint) => *constraint,
@@ -138,7 +139,7 @@ impl<V: View> Element<V> for ConstrainedBox<V> {
&mut self,
mut parent_constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
let constraint = self.constraint(parent_constraint, view, cx);
parent_constraint.min = parent_constraint.min.max(constraint.min);

View File

@@ -10,7 +10,7 @@ use crate::{
json::ToJson,
platform::CursorStyle,
scene::{self, Border, CursorRegion, Quad},
AnyElement, Element, SceneBuilder, SizeConstraint, View, ViewContext,
AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
};
use serde::Deserialize;
use serde_json::json;
@@ -192,7 +192,7 @@ impl<V: View> Element<V> for Container<V> {
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
let mut size_buffer = self.margin_size() + self.padding_size();
if !self.style.border.overlay {

View File

@@ -6,7 +6,7 @@ use crate::{
vector::{vec2f, Vector2F},
},
json::{json, ToJson},
SceneBuilder, View, ViewContext,
LayoutContext, SceneBuilder, View, ViewContext,
};
use crate::{Element, SizeConstraint};
@@ -34,7 +34,7 @@ impl<V: View> Element<V> for Empty {
&mut self,
constraint: SizeConstraint,
_: &mut V,
_: &mut ViewContext<V>,
_: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
let x = if constraint.max.x().is_finite() && !self.collapsed {
constraint.max.x()

View File

@@ -2,7 +2,7 @@ use std::ops::Range;
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json, AnyElement, Element, SceneBuilder, SizeConstraint, View, ViewContext,
json, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
};
use serde_json::json;
@@ -42,7 +42,7 @@ impl<V: View> Element<V> for Expanded<V> {
&mut self,
mut constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
if self.full_width {
constraint.min.set_x(constraint.max.x());

View File

@@ -2,8 +2,8 @@ use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
use crate::{
json::{self, ToJson, Value},
AnyElement, Axis, Element, ElementStateHandle, SceneBuilder, SizeConstraint, Vector2FExt, View,
ViewContext,
AnyElement, Axis, Element, ElementStateHandle, LayoutContext, SceneBuilder, SizeConstraint,
Vector2FExt, View, ViewContext,
};
use pathfinder_geometry::{
rect::RectF,
@@ -66,6 +66,10 @@ impl<V: View> Flex<V> {
self
}
pub fn is_empty(&self) -> bool {
self.children.is_empty()
}
fn layout_flex_children(
&mut self,
layout_expanded: bool,
@@ -74,7 +78,7 @@ impl<V: View> Flex<V> {
remaining_flex: &mut f32,
cross_axis_max: &mut f32,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) {
let cross_axis = self.axis.invert();
for child in &mut self.children {
@@ -125,7 +129,7 @@ impl<V: View> Element<V> for Flex<V> {
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
let mut total_flex = None;
let mut fixed_space = 0.0;
@@ -214,7 +218,7 @@ impl<V: View> Element<V> for Flex<V> {
}
if let Some(scroll_state) = self.scroll_state.as_ref() {
scroll_state.0.update(cx, |scroll_state, _| {
scroll_state.0.update(cx.view_context(), |scroll_state, _| {
if let Some(scroll_to) = scroll_state.scroll_to.take() {
let visible_start = scroll_state.scroll_position.get();
let visible_end = visible_start + size.along(self.axis);
@@ -432,7 +436,7 @@ impl<V: View> Element<V> for FlexItem<V> {
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
let size = self.child.layout(constraint, view, cx);
(size, ())

View File

@@ -3,7 +3,7 @@ use std::ops::Range;
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::json,
AnyElement, Element, SceneBuilder, SizeConstraint, View, ViewContext,
AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
};
pub struct Hook<V: View> {
@@ -36,7 +36,7 @@ impl<V: View> Element<V> for Hook<V> {
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
let size = self.child.layout(constraint, view, cx);
if let Some(handler) = self.after_layout.as_mut() {

View File

@@ -5,7 +5,8 @@ use crate::{
vector::{vec2f, Vector2F},
},
json::{json, ToJson},
scene, Border, Element, ImageData, SceneBuilder, SizeConstraint, View, ViewContext,
scene, Border, Element, ImageData, LayoutContext, SceneBuilder, SizeConstraint, View,
ViewContext,
};
use serde::Deserialize;
use std::{ops::Range, sync::Arc};
@@ -63,7 +64,7 @@ impl<V: View> Element<V> for Image {
&mut self,
constraint: SizeConstraint,
_: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
let data = match &self.source {
ImageSource::Path(path) => match cx.asset_cache.png(path) {

View File

@@ -39,7 +39,7 @@ impl<V: View> Element<V> for KeystrokeLabel {
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, AnyElement<V>) {
let mut element = if let Some(keystrokes) =
cx.keystrokes_for_action(self.view_id, self.action.as_ref())

View File

@@ -8,7 +8,7 @@ use crate::{
},
json::{ToJson, Value},
text_layout::{Line, RunStyle},
Element, SceneBuilder, SizeConstraint, View, ViewContext,
Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
};
use serde::Deserialize;
use serde_json::json;
@@ -135,7 +135,7 @@ impl<V: View> Element<V> for Label {
&mut self,
constraint: SizeConstraint,
_: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
let runs = self.compute_runs();
let line = cx.text_layout_cache().layout_str(

View File

@@ -4,7 +4,8 @@ use crate::{
vector::{vec2f, Vector2F},
},
json::json,
AnyElement, Element, MouseRegion, SceneBuilder, SizeConstraint, View, ViewContext,
AnyElement, Element, LayoutContext, MouseRegion, SceneBuilder, SizeConstraint, View,
ViewContext,
};
use std::{cell::RefCell, collections::VecDeque, fmt::Debug, ops::Range, rc::Rc};
use sum_tree::{Bias, SumTree};
@@ -99,7 +100,7 @@ impl<V: View> Element<V> for List<V> {
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
let state = &mut *self.state.0.borrow_mut();
let size = constraint.max;
@@ -452,7 +453,7 @@ impl<V: View> StateInner<V> {
existing_element: Option<&ListItem<V>>,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> Option<Rc<RefCell<AnyElement<V>>>> {
if let Some(ListItem::Rendered(element)) = existing_element {
Some(element.clone())
@@ -665,7 +666,15 @@ mod tests {
});
let mut list = List::new(state.clone());
let (size, _) = list.layout(constraint, &mut view, cx);
let mut new_parents = Default::default();
let mut notify_views_if_parents_change = Default::default();
let mut layout_cx = LayoutContext::new(
cx,
&mut new_parents,
&mut notify_views_if_parents_change,
false,
);
let (size, _) = list.layout(constraint, &mut view, &mut layout_cx);
assert_eq!(size, vec2f(100., 40.));
assert_eq!(
state.0.borrow().items.summary().clone(),
@@ -689,7 +698,13 @@ mod tests {
cx,
);
let (_, logical_scroll_top) = list.layout(constraint, &mut view, cx);
let mut layout_cx = LayoutContext::new(
cx,
&mut new_parents,
&mut notify_views_if_parents_change,
false,
);
let (_, logical_scroll_top) = list.layout(constraint, &mut view, &mut layout_cx);
assert_eq!(
logical_scroll_top,
ListOffset {
@@ -713,7 +728,13 @@ mod tests {
}
);
let (size, logical_scroll_top) = list.layout(constraint, &mut view, cx);
let mut layout_cx = LayoutContext::new(
cx,
&mut new_parents,
&mut notify_views_if_parents_change,
false,
);
let (size, logical_scroll_top) = list.layout(constraint, &mut view, &mut layout_cx);
assert_eq!(size, vec2f(100., 40.));
assert_eq!(
state.0.borrow().items.summary().clone(),
@@ -831,10 +852,18 @@ mod tests {
let mut list = List::new(state.clone());
let window_size = vec2f(width, height);
let mut new_parents = Default::default();
let mut notify_views_if_parents_change = Default::default();
let mut layout_cx = LayoutContext::new(
cx,
&mut new_parents,
&mut notify_views_if_parents_change,
false,
);
let (size, logical_scroll_top) = list.layout(
SizeConstraint::new(vec2f(0., 0.), window_size),
&mut view,
cx,
&mut layout_cx,
);
assert_eq!(size, window_size);
last_logical_scroll_top = Some(logical_scroll_top);
@@ -947,7 +976,7 @@ mod tests {
&mut self,
_: SizeConstraint,
_: &mut V,
_: &mut ViewContext<V>,
_: &mut LayoutContext<V>,
) -> (Vector2F, ()) {
(self.size, ())
}

View File

@@ -10,8 +10,8 @@ use crate::{
CursorRegion, HandlerSet, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseHover,
MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
},
AnyElement, Element, EventContext, MouseRegion, MouseState, SceneBuilder, SizeConstraint, View,
ViewContext,
AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, SceneBuilder,
SizeConstraint, View, ViewContext,
};
use serde_json::json;
use std::{marker::PhantomData, ops::Range};
@@ -220,7 +220,7 @@ impl<Tag, V: View> Element<V> for MouseEventHandler<Tag, V> {
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
(self.child.layout(constraint, view, cx), ())
}

View File

@@ -3,7 +3,8 @@ use std::ops::Range;
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::ToJson,
AnyElement, Axis, Element, MouseRegion, SceneBuilder, SizeConstraint, View, ViewContext,
AnyElement, Axis, Element, LayoutContext, MouseRegion, SceneBuilder, SizeConstraint, View,
ViewContext,
};
use serde_json::json;
@@ -124,7 +125,7 @@ impl<V: View> Element<V> for Overlay<V> {
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
let constraint = if self.anchor_position.is_some() {
SizeConstraint::new(Vector2F::zero(), cx.window_size())

View File

@@ -7,7 +7,8 @@ use crate::{
geometry::rect::RectF,
platform::{CursorStyle, MouseButton},
scene::MouseDrag,
AnyElement, Axis, Element, ElementStateHandle, MouseRegion, SceneBuilder, View, ViewContext,
AnyElement, Axis, Element, ElementStateHandle, LayoutContext, MouseRegion, SceneBuilder, View,
ViewContext,
};
use super::{ConstrainedBox, Hook};
@@ -139,7 +140,7 @@ impl<V: View> Element<V> for Resizable<V> {
&mut self,
constraint: crate::SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
(self.child.layout(constraint, view, cx), ())
}

View File

@@ -3,7 +3,7 @@ use std::ops::Range;
use crate::{
geometry::{rect::RectF, vector::Vector2F},
json::{self, json, ToJson},
AnyElement, Element, SceneBuilder, SizeConstraint, View, ViewContext,
AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
};
/// Element which renders it's children in a stack on top of each other.
@@ -34,7 +34,7 @@ impl<V: View> Element<V> for Stack<V> {
&mut self,
mut constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
let mut size = constraint.min;
let mut children = self.children.iter_mut();

View File

@@ -8,7 +8,7 @@ use crate::{
rect::RectF,
vector::{vec2f, Vector2F},
},
scene, Element, SceneBuilder, SizeConstraint, View, ViewContext,
scene, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
};
pub struct Svg {
@@ -38,7 +38,7 @@ impl<V: View> Element<V> for Svg {
&mut self,
constraint: SizeConstraint,
_: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
match cx.asset_cache.svg(&self.path) {
Ok(tree) => {

View File

@@ -7,8 +7,8 @@ use crate::{
},
json::{ToJson, Value},
text_layout::{Line, RunStyle, ShapedBoundary},
AppContext, Element, FontCache, SceneBuilder, SizeConstraint, TextLayoutCache, View,
ViewContext,
AppContext, Element, FontCache, LayoutContext, SceneBuilder, SizeConstraint, TextLayoutCache,
View, ViewContext,
};
use log::warn;
use serde_json::json;
@@ -78,7 +78,7 @@ impl<V: View> Element<V> for Text {
&mut self,
constraint: SizeConstraint,
_: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
// Convert the string and highlight ranges into an iterator of highlighted chunks.
@@ -338,7 +338,7 @@ impl<V: View> Element<V> for Text {
}
/// Perform text layout on a series of highlighted chunks of text.
pub fn layout_highlighted_chunks<'a>(
fn layout_highlighted_chunks<'a>(
chunks: impl Iterator<Item = (&'a str, Option<HighlightStyle>)>,
text_style: &TextStyle,
text_layout_cache: &TextLayoutCache,
@@ -411,10 +411,18 @@ mod tests {
let mut view = TestView;
fonts::with_font_cache(cx.font_cache().clone(), || {
let mut text = Text::new("Hello\r\n", Default::default()).with_soft_wrap(true);
let mut new_parents = Default::default();
let mut notify_views_if_parents_change = Default::default();
let mut layout_cx = LayoutContext::new(
cx,
&mut new_parents,
&mut notify_views_if_parents_change,
false,
);
let (_, state) = text.layout(
SizeConstraint::new(Default::default(), vec2f(f32::INFINITY, f32::INFINITY)),
&mut view,
cx,
&mut layout_cx,
);
assert_eq!(state.shaped_lines.len(), 2);
assert_eq!(state.wrap_boundaries.len(), 2);

View File

@@ -6,7 +6,8 @@ use crate::{
fonts::TextStyle,
geometry::{rect::RectF, vector::Vector2F},
json::json,
Action, Axis, ElementStateHandle, SceneBuilder, SizeConstraint, Task, View, ViewContext,
Action, Axis, ElementStateHandle, LayoutContext, SceneBuilder, SizeConstraint, Task, View,
ViewContext,
};
use serde::Deserialize;
use std::{
@@ -172,7 +173,7 @@ impl<V: View> Element<V> for Tooltip<V> {
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
let size = self.child.layout(constraint, view, cx);
if let Some(tooltip) = self.tooltip.as_mut() {

View File

@@ -6,7 +6,7 @@ use crate::{
},
json::{self, json},
platform::ScrollWheelEvent,
AnyElement, MouseRegion, SceneBuilder, View, ViewContext,
AnyElement, LayoutContext, MouseRegion, SceneBuilder, View, ViewContext,
};
use json::ToJson;
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -159,7 +159,7 @@ impl<V: View> Element<V> for UniformList<V> {
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut ViewContext<V>,
cx: &mut LayoutContext<V>,
) -> (Vector2F, Self::LayoutState) {
if constraint.max.y().is_infinite() {
unimplemented!(

View File

@@ -11,6 +11,29 @@ pub struct Binding {
context_predicate: Option<KeymapContextPredicate>,
}
impl std::fmt::Debug for Binding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Binding {{ keystrokes: {:?}, action: {}::{}, context_predicate: {:?} }}",
self.keystrokes,
self.action.namespace(),
self.action.name(),
self.context_predicate
)
}
}
impl Clone for Binding {
fn clone(&self) -> Self {
Self {
action: self.action.boxed_clone(),
keystrokes: self.keystrokes.clone(),
context_predicate: self.context_predicate.clone(),
}
}
}
impl Binding {
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
Self::load(keystrokes, Box::new(action), context).unwrap()

View File

@@ -17,6 +17,11 @@ impl KeymapContext {
}
}
pub fn clear(&mut self) {
self.set.clear();
self.map.clear();
}
pub fn extend(&mut self, other: &Self) {
for v in &other.set {
self.set.insert(v.clone());
@@ -39,7 +44,7 @@ impl KeymapContext {
}
}
#[derive(Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum KeymapContextPredicate {
Identifier(String),
Equal(String, String),

View File

@@ -755,7 +755,7 @@ impl platform::Window for Window {
let _ = postage::sink::Sink::try_send(&mut done_tx, answer.try_into().unwrap());
}
});
let block = block.copy();
let native_window = self.0.borrow().native_window;
self.0
.borrow()

View File

@@ -223,41 +223,41 @@ impl HandlerSet {
set.insert(
HandlerKey::new(MouseEvent::move_disc(), None),
SmallVec::from_buf([Rc::new(|_, _, _, _| false)]),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
set.insert(
HandlerKey::new(MouseEvent::hover_disc(), None),
SmallVec::from_buf([Rc::new(|_, _, _, _| false)]),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
for button in MouseButton::all() {
set.insert(
HandlerKey::new(MouseEvent::drag_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _, _, _| false)]),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
set.insert(
HandlerKey::new(MouseEvent::down_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _, _, _| false)]),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
set.insert(
HandlerKey::new(MouseEvent::up_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _, _, _| false)]),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
set.insert(
HandlerKey::new(MouseEvent::click_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _, _, _| false)]),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
set.insert(
HandlerKey::new(MouseEvent::down_out_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _, _, _| false)]),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
set.insert(
HandlerKey::new(MouseEvent::up_out_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _, _, _| false)]),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
}
set.insert(
HandlerKey::new(MouseEvent::scroll_wheel_disc(), None),
SmallVec::from_buf([Rc::new(|_, _, _, _| false)]),
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
);
HandlerSet { set }

View File

@@ -39,7 +39,7 @@ use std::{
};
use sum_tree::TreeMap;
use text::operation_queue::OperationQueue;
pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, Operation as _, *};
pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, *};
use theme::SyntaxTheme;
#[cfg(any(test, feature = "test-support"))]
use util::RandomCharIter;
@@ -311,6 +311,7 @@ pub struct Chunk<'a> {
pub highlight_style: Option<HighlightStyle>,
pub diagnostic_severity: Option<DiagnosticSeverity>,
pub is_unnecessary: bool,
pub is_tab: bool,
}
pub struct Diff {
@@ -357,20 +358,6 @@ impl Buffer {
)
}
pub fn from_file<T: Into<String>>(
replica_id: ReplicaId,
base_text: T,
diff_base: Option<T>,
file: Arc<dyn File>,
cx: &mut ModelContext<Self>,
) -> Self {
Self::build(
TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
diff_base.map(|h| h.into().into_boxed_str().into()),
Some(file),
)
}
pub fn from_proto(
replica_id: ReplicaId,
message: proto::BufferState,
@@ -460,7 +447,11 @@ impl Buffer {
self
}
fn build(buffer: TextBuffer, diff_base: Option<String>, file: Option<Arc<dyn File>>) -> Self {
pub fn build(
buffer: TextBuffer,
diff_base: Option<String>,
file: Option<Arc<dyn File>>,
) -> Self {
let saved_mtime = if let Some(file) = file.as_ref() {
file.mtime()
} else {
@@ -2850,9 +2841,9 @@ impl<'a> Iterator for BufferChunks<'a> {
Some(Chunk {
text: slice,
syntax_highlight_id: highlight_id,
highlight_style: None,
diagnostic_severity: self.current_diagnostic_severity(),
is_unnecessary: self.current_code_is_unnecessary(),
..Default::default()
})
} else {
None

View File

@@ -126,10 +126,9 @@ impl<D: PickerDelegate> View for Picker<D> {
.into_any_named("picker")
}
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.add_identifier("menu");
cx
fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
Self::reset_to_default_keymap_context(keymap);
keymap.add_identifier("menu");
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {

View File

@@ -42,7 +42,7 @@ anyhow.workspace = true
async-trait.workspace = true
backtrace = "0.3"
futures.workspace = true
glob.workspace = true
globset.workspace = true
ignore = "0.4"
lazy_static.workspace = true
log.workspace = true
@@ -58,6 +58,7 @@ similar = "1.3"
smol.workspace = true
thiserror.workspace = true
toml = "0.5"
itertools = "0.10"
[dev-dependencies]
ctor.workspace = true
@@ -73,5 +74,6 @@ lsp = { path = "../lsp", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
git2 = { version = "0.15", default-features = false }
tempdir.workspace = true
unindent.workspace = true

View File

@@ -1,121 +0,0 @@
use anyhow::{anyhow, Result};
use std::path::Path;
#[derive(Default)]
pub struct LspGlobSet {
patterns: Vec<glob::Pattern>,
}
impl LspGlobSet {
pub fn clear(&mut self) {
self.patterns.clear();
}
/// Add a pattern to the glob set.
///
/// LSP's glob syntax supports bash-style brace expansion. For example,
/// the pattern '*.{js,ts}' would match all JavaScript or TypeScript files.
/// This is not a part of the standard libc glob syntax, and isn't supported
/// by the `glob` crate. So we pre-process the glob patterns, producing a
/// separate glob `Pattern` object for each part of a brace expansion.
pub fn add_pattern(&mut self, pattern: &str) -> Result<()> {
// Find all of the ranges of `pattern` that contain matched curly braces.
let mut expansion_ranges = Vec::new();
let mut expansion_start_ix = None;
for (ix, c) in pattern.match_indices(|c| ['{', '}'].contains(&c)) {
match c {
"{" => {
if expansion_start_ix.is_some() {
return Err(anyhow!("nested braces in glob patterns aren't supported"));
}
expansion_start_ix = Some(ix);
}
"}" => {
if let Some(start_ix) = expansion_start_ix {
expansion_ranges.push(start_ix..ix + 1);
}
expansion_start_ix = None;
}
_ => {}
}
}
// Starting with a single pattern, process each brace expansion by cloning
// the pattern once per element of the expansion.
let mut unexpanded_patterns = vec![];
let mut expanded_patterns = vec![pattern.to_string()];
for outer_range in expansion_ranges.into_iter().rev() {
let inner_range = (outer_range.start + 1)..(outer_range.end - 1);
std::mem::swap(&mut unexpanded_patterns, &mut expanded_patterns);
for unexpanded_pattern in unexpanded_patterns.drain(..) {
for part in unexpanded_pattern[inner_range.clone()].split(',') {
let mut expanded_pattern = unexpanded_pattern.clone();
expanded_pattern.replace_range(outer_range.clone(), part);
expanded_patterns.push(expanded_pattern);
}
}
}
// Parse the final glob patterns and add them to the set.
for pattern in expanded_patterns {
let pattern = glob::Pattern::new(&pattern)?;
self.patterns.push(pattern);
}
Ok(())
}
pub fn matches(&self, path: &Path) -> bool {
self.patterns
.iter()
.any(|pattern| pattern.matches_path(path))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_glob_set() {
let mut watch = LspGlobSet::default();
watch.add_pattern("/a/**/*.rs").unwrap();
watch.add_pattern("/a/**/Cargo.toml").unwrap();
assert!(watch.matches("/a/b.rs".as_ref()));
assert!(watch.matches("/a/b/c.rs".as_ref()));
assert!(!watch.matches("/b/c.rs".as_ref()));
assert!(!watch.matches("/a/b.ts".as_ref()));
}
#[test]
fn test_brace_expansion() {
let mut watch = LspGlobSet::default();
watch.add_pattern("/a/*.{ts,js,tsx}").unwrap();
assert!(watch.matches("/a/one.js".as_ref()));
assert!(watch.matches("/a/two.ts".as_ref()));
assert!(watch.matches("/a/three.tsx".as_ref()));
assert!(!watch.matches("/a/one.j".as_ref()));
assert!(!watch.matches("/a/two.s".as_ref()));
assert!(!watch.matches("/a/three.t".as_ref()));
assert!(!watch.matches("/a/four.t".as_ref()));
assert!(!watch.matches("/a/five.xt".as_ref()));
}
#[test]
fn test_multiple_brace_expansion() {
let mut watch = LspGlobSet::default();
watch.add_pattern("/a/{one,two,three}.{b*c,d*e}").unwrap();
assert!(watch.matches("/a/one.bic".as_ref()));
assert!(watch.matches("/a/two.dole".as_ref()));
assert!(watch.matches("/a/three.deeee".as_ref()));
assert!(!watch.matches("/a/four.bic".as_ref()));
assert!(!watch.matches("/a/one.be".as_ref()));
}
}

View File

@@ -1,6 +1,5 @@
mod ignore;
mod lsp_command;
mod lsp_glob_set;
pub mod search;
pub mod terminals;
pub mod worktree;
@@ -18,6 +17,7 @@ use futures::{
future::{try_join_all, Shared},
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
};
use globset::{Glob, GlobSet, GlobSetBuilder};
use gpui::{
AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, ModelContext,
ModelHandle, Task, WeakModelHandle,
@@ -39,7 +39,6 @@ use lsp::{
DocumentHighlightKind, LanguageServer, LanguageServerId,
};
use lsp_command::*;
use lsp_glob_set::LspGlobSet;
use postage::watch;
use rand::prelude::*;
use search::SearchQuery;
@@ -64,6 +63,7 @@ use std::{
},
time::{Duration, Instant, SystemTime},
};
use terminals::Terminals;
use util::{debug_panic, defer, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _};
@@ -109,6 +109,7 @@ pub struct Project {
collaborators: HashMap<proto::PeerId, Collaborator>,
client_subscriptions: Vec<client::Subscription>,
_subscriptions: Vec<gpui::Subscription>,
next_buffer_id: u64,
opened_buffer: (watch::Sender<()>, watch::Receiver<()>),
shared_buffers: HashMap<proto::PeerId, HashSet<u64>>,
#[allow(clippy::type_complexity)]
@@ -120,11 +121,13 @@ pub struct Project {
loading_local_worktrees:
HashMap<Arc<Path>, Shared<Task<Result<ModelHandle<Worktree>, Arc<anyhow::Error>>>>>,
opened_buffers: HashMap<u64, OpenBuffer>,
local_buffer_ids_by_path: HashMap<ProjectPath, u64>,
local_buffer_ids_by_entry_id: HashMap<ProjectEntryId, u64>,
/// A mapping from a buffer ID to None means that we've started waiting for an ID but haven't finished loading it.
/// Used for re-issuing buffer requests when peers temporarily disconnect
incomplete_remote_buffers: HashMap<u64, Option<ModelHandle<Buffer>>>,
buffer_snapshots: HashMap<u64, HashMap<LanguageServerId, Vec<LspBufferSnapshot>>>, // buffer_id -> server_id -> vec of snapshots
buffers_being_formatted: HashSet<usize>,
buffers_being_formatted: HashSet<u64>,
nonce: u128,
_maintain_buffer_languages: Task<()>,
_maintain_workspace_config: Task<()>,
@@ -221,7 +224,7 @@ pub enum LanguageServerState {
language: Arc<Language>,
adapter: Arc<CachedLspAdapter>,
server: Arc<LanguageServer>,
watched_paths: LspGlobSet,
watched_paths: HashMap<WorktreeId, GlobSet>,
simulate_disk_based_diagnostics_completion: Option<Task<()>>,
},
}
@@ -441,11 +444,14 @@ impl Project {
worktrees: Default::default(),
buffer_ordered_messages_tx: tx,
collaborators: Default::default(),
next_buffer_id: 0,
opened_buffers: Default::default(),
shared_buffers: Default::default(),
incomplete_remote_buffers: Default::default(),
loading_buffers_by_path: Default::default(),
loading_local_worktrees: Default::default(),
local_buffer_ids_by_path: Default::default(),
local_buffer_ids_by_entry_id: Default::default(),
buffer_snapshots: Default::default(),
join_project_response_message_id: 0,
client_state: None,
@@ -509,10 +515,13 @@ impl Project {
worktrees: Vec::new(),
buffer_ordered_messages_tx: tx,
loading_buffers_by_path: Default::default(),
next_buffer_id: 0,
opened_buffer: watch::channel(),
shared_buffers: Default::default(),
incomplete_remote_buffers: Default::default(),
loading_local_worktrees: Default::default(),
local_buffer_ids_by_path: Default::default(),
local_buffer_ids_by_entry_id: Default::default(),
active_entry: None,
collaborators: Default::default(),
join_project_response_message_id: response.message_id,
@@ -1401,9 +1410,10 @@ impl Project {
worktree: &ModelHandle<Worktree>,
cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<Buffer>>> {
let buffer_id = post_inc(&mut self.next_buffer_id);
let load_buffer = worktree.update(cx, |worktree, cx| {
let worktree = worktree.as_local_mut().unwrap();
worktree.load_buffer(path, cx)
worktree.load_buffer(buffer_id, path, cx)
});
cx.spawn(|this, mut cx| async move {
let buffer = load_buffer.await?;
@@ -1623,6 +1633,21 @@ impl Project {
})
.detach();
if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
if file.is_local {
self.local_buffer_ids_by_path.insert(
ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
},
remote_id,
);
self.local_buffer_ids_by_entry_id
.insert(file.entry_id, remote_id);
}
}
self.detect_language_for_buffer(buffer, cx);
self.register_buffer_with_language_servers(buffer, cx);
self.register_buffer_with_copilot(buffer, cx);
@@ -2833,10 +2858,39 @@ impl Project {
if let Some(LanguageServerState::Running { watched_paths, .. }) =
self.language_servers.get_mut(&language_server_id)
{
watched_paths.clear();
eprintln!("change watch");
let mut builders = HashMap::default();
for watcher in params.watchers {
watched_paths.add_pattern(&watcher.glob_pattern).log_err();
eprintln!(" {}", watcher.glob_pattern);
for worktree in &self.worktrees {
if let Some(worktree) = worktree.upgrade(cx) {
let worktree = worktree.read(cx);
if let Some(abs_path) = worktree.abs_path().to_str() {
if let Some(suffix) = watcher
.glob_pattern
.strip_prefix(abs_path)
.and_then(|s| s.strip_prefix(std::path::MAIN_SEPARATOR))
{
if let Some(glob) = Glob::new(suffix).log_err() {
builders
.entry(worktree.id())
.or_insert_with(|| GlobSetBuilder::new())
.add(glob);
}
break;
}
}
}
}
}
watched_paths.clear();
for (worktree_id, builder) in builders {
if let Ok(globset) = builder.build() {
watched_paths.insert(worktree_id, globset);
}
}
cx.notify();
}
}
@@ -3200,9 +3254,11 @@ impl Project {
cx.spawn(|this, mut cx| async move {
// Do not allow multiple concurrent formatting requests for the
// same buffer.
this.update(&mut cx, |this, _| {
buffers_with_paths_and_servers
.retain(|(buffer, _, _)| this.buffers_being_formatted.insert(buffer.id()));
this.update(&mut cx, |this, cx| {
buffers_with_paths_and_servers.retain(|(buffer, _, _)| {
this.buffers_being_formatted
.insert(buffer.read(cx).remote_id())
});
});
let _cleanup = defer({
@@ -3210,9 +3266,10 @@ impl Project {
let mut cx = cx.clone();
let buffers = &buffers_with_paths_and_servers;
move || {
this.update(&mut cx, |this, _| {
this.update(&mut cx, |this, cx| {
for (buffer, _, _) in buffers {
this.buffers_being_formatted.remove(&buffer.id());
this.buffers_being_formatted
.remove(&buffer.read(cx).remote_id());
}
});
}
@@ -4200,14 +4257,19 @@ impl Project {
if matching_paths_tx.is_closed() {
break;
}
abs_path.clear();
abs_path.push(&snapshot.abs_path());
abs_path.push(&entry.path);
let matches = if let Some(file) =
fs.open_sync(&abs_path).await.log_err()
let matches = if query
.file_matches(Some(&entry.path))
{
query.detect(file).unwrap_or(false)
abs_path.clear();
abs_path.push(&snapshot.abs_path());
abs_path.push(&entry.path);
if let Some(file) =
fs.open_sync(&abs_path).await.log_err()
{
query.detect(file).unwrap_or(false)
} else {
false
}
} else {
false
};
@@ -4291,15 +4353,21 @@ impl Project {
let mut buffers_rx = buffers_rx.clone();
scope.spawn(async move {
while let Some((buffer, snapshot)) = buffers_rx.next().await {
let buffer_matches = query
.search(snapshot.as_rope())
.await
.iter()
.map(|range| {
snapshot.anchor_before(range.start)
..snapshot.anchor_after(range.end)
})
.collect::<Vec<_>>();
let buffer_matches = if query.file_matches(
snapshot.file().map(|file| file.path().as_ref()),
) {
query
.search(snapshot.as_rope())
.await
.iter()
.map(|range| {
snapshot.anchor_before(range.start)
..snapshot.anchor_after(range.end)
})
.collect()
} else {
Vec::new()
};
if !buffer_matches.is_empty() {
worker_matched_buffers
.insert(buffer.clone(), buffer_matches);
@@ -4517,7 +4585,7 @@ impl Project {
if worktree.read(cx).is_local() {
cx.subscribe(worktree, |this, worktree, event, cx| match event {
worktree::Event::UpdatedEntries(changes) => {
this.update_local_worktree_buffers(&worktree, cx);
this.update_local_worktree_buffers(&worktree, &changes, cx);
this.update_local_worktree_language_servers(&worktree, changes, cx);
}
worktree::Event::UpdatedGitRepositories(updated_repos) => {
@@ -4551,80 +4619,106 @@ impl Project {
fn update_local_worktree_buffers(
&mut self,
worktree_handle: &ModelHandle<Worktree>,
changes: &HashMap<(Arc<Path>, ProjectEntryId), PathChange>,
cx: &mut ModelContext<Self>,
) {
let snapshot = worktree_handle.read(cx).snapshot();
let mut buffers_to_delete = Vec::new();
let mut renamed_buffers = Vec::new();
for (path, entry_id) in changes.keys() {
let worktree_id = worktree_handle.read(cx).id();
let project_path = ProjectPath {
worktree_id,
path: path.clone(),
};
for (buffer_id, buffer) in &self.opened_buffers {
if let Some(buffer) = buffer.upgrade(cx) {
buffer.update(cx, |buffer, cx| {
if let Some(old_file) = File::from_dyn(buffer.file()) {
if old_file.worktree != *worktree_handle {
return;
}
let buffer_id = match self.local_buffer_ids_by_entry_id.get(entry_id) {
Some(&buffer_id) => buffer_id,
None => match self.local_buffer_ids_by_path.get(&project_path) {
Some(&buffer_id) => buffer_id,
None => continue,
},
};
let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id)
{
File {
is_local: true,
entry_id: entry.id,
mtime: entry.mtime,
path: entry.path.clone(),
worktree: worktree_handle.clone(),
is_deleted: false,
}
} else if let Some(entry) =
snapshot.entry_for_path(old_file.path().as_ref())
{
File {
is_local: true,
entry_id: entry.id,
mtime: entry.mtime,
path: entry.path.clone(),
worktree: worktree_handle.clone(),
is_deleted: false,
}
} else {
File {
is_local: true,
entry_id: old_file.entry_id,
path: old_file.path().clone(),
mtime: old_file.mtime(),
worktree: worktree_handle.clone(),
is_deleted: true,
}
};
let old_path = old_file.abs_path(cx);
if new_file.abs_path(cx) != old_path {
renamed_buffers.push((cx.handle(), old_file.clone()));
}
if new_file != *old_file {
if let Some(project_id) = self.remote_id() {
self.client
.send(proto::UpdateBufferFile {
project_id,
buffer_id: *buffer_id as u64,
file: Some(new_file.to_proto()),
})
.log_err();
}
buffer.file_updated(Arc::new(new_file), cx).detach();
}
}
});
let open_buffer = self.opened_buffers.get(&buffer_id);
let buffer = if let Some(buffer) = open_buffer.and_then(|buffer| buffer.upgrade(cx)) {
buffer
} else {
buffers_to_delete.push(*buffer_id);
}
}
self.opened_buffers.remove(&buffer_id);
self.local_buffer_ids_by_path.remove(&project_path);
self.local_buffer_ids_by_entry_id.remove(entry_id);
continue;
};
for buffer_id in buffers_to_delete {
self.opened_buffers.remove(&buffer_id);
buffer.update(cx, |buffer, cx| {
if let Some(old_file) = File::from_dyn(buffer.file()) {
if old_file.worktree != *worktree_handle {
return;
}
let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) {
File {
is_local: true,
entry_id: entry.id,
mtime: entry.mtime,
path: entry.path.clone(),
worktree: worktree_handle.clone(),
is_deleted: false,
}
} else if let Some(entry) = snapshot.entry_for_path(old_file.path().as_ref()) {
File {
is_local: true,
entry_id: entry.id,
mtime: entry.mtime,
path: entry.path.clone(),
worktree: worktree_handle.clone(),
is_deleted: false,
}
} else {
File {
is_local: true,
entry_id: old_file.entry_id,
path: old_file.path().clone(),
mtime: old_file.mtime(),
worktree: worktree_handle.clone(),
is_deleted: true,
}
};
let old_path = old_file.abs_path(cx);
if new_file.abs_path(cx) != old_path {
renamed_buffers.push((cx.handle(), old_file.clone()));
self.local_buffer_ids_by_path.remove(&project_path);
self.local_buffer_ids_by_path.insert(
ProjectPath {
worktree_id,
path: path.clone(),
},
buffer_id,
);
}
if new_file.entry_id != *entry_id {
self.local_buffer_ids_by_entry_id.remove(entry_id);
self.local_buffer_ids_by_entry_id
.insert(new_file.entry_id, buffer_id);
}
if new_file != *old_file {
if let Some(project_id) = self.remote_id() {
self.client
.send(proto::UpdateBufferFile {
project_id,
buffer_id: buffer_id as u64,
file: Some(new_file.to_proto()),
})
.log_err();
}
buffer.file_updated(Arc::new(new_file), cx).detach();
}
}
});
}
for (buffer, old_file) in renamed_buffers {
@@ -4637,28 +4731,42 @@ impl Project {
fn update_local_worktree_language_servers(
&mut self,
worktree_handle: &ModelHandle<Worktree>,
changes: &HashMap<Arc<Path>, PathChange>,
changes: &HashMap<(Arc<Path>, ProjectEntryId), PathChange>,
cx: &mut ModelContext<Self>,
) {
if changes.is_empty() {
return;
}
let worktree_id = worktree_handle.read(cx).id();
let mut language_server_ids = self
.language_server_ids
.iter()
.filter_map(|((server_worktree_id, _), server_id)| {
(*server_worktree_id == worktree_id).then_some(*server_id)
})
.collect::<Vec<_>>();
language_server_ids.sort();
language_server_ids.dedup();
let abs_path = worktree_handle.read(cx).abs_path();
for ((server_worktree_id, _), server_id) in &self.language_server_ids {
if *server_worktree_id == worktree_id {
if let Some(server) = self.language_servers.get(server_id) {
if let LanguageServerState::Running {
server,
watched_paths,
..
} = server
{
for server_id in &language_server_ids {
if let Some(server) = self.language_servers.get(server_id) {
if let LanguageServerState::Running {
server,
watched_paths,
..
} = server
{
if let Some(watched_paths) = watched_paths.get(&worktree_id) {
let params = lsp::DidChangeWatchedFilesParams {
changes: changes
.iter()
.filter_map(|(path, change)| {
let path = abs_path.join(path);
if watched_paths.matches(&path) {
.filter_map(|((path, _), change)| {
if watched_paths.is_match(&path) {
Some(lsp::FileEvent {
uri: lsp::Url::from_file_path(path).unwrap(),
uri: lsp::Url::from_file_path(abs_path.join(path))
.unwrap(),
typ: match change {
PathChange::Added => lsp::FileChangeType::CREATED,
PathChange::Removed => lsp::FileChangeType::DELETED,
@@ -4688,40 +4796,50 @@ impl Project {
fn update_local_worktree_buffers_git_repos(
&mut self,
worktree: ModelHandle<Worktree>,
repos: &[GitRepositoryEntry],
worktree_handle: ModelHandle<Worktree>,
repos: &HashMap<Arc<Path>, LocalRepositoryEntry>,
cx: &mut ModelContext<Self>,
) {
debug_assert!(worktree_handle.read(cx).is_local());
for (_, buffer) in &self.opened_buffers {
if let Some(buffer) = buffer.upgrade(cx) {
let file = match File::from_dyn(buffer.read(cx).file()) {
Some(file) => file,
None => continue,
};
if file.worktree != worktree {
if file.worktree != worktree_handle {
continue;
}
let path = file.path().clone();
let repo = match repos.iter().find(|repo| repo.manages(&path)) {
let worktree = worktree_handle.read(cx);
let (work_directory, repo) = match repos
.iter()
.find(|(work_directory, _)| path.starts_with(work_directory))
{
Some(repo) => repo.clone(),
None => return,
};
let relative_repo = match path.strip_prefix(repo.content_path) {
Ok(relative_repo) => relative_repo.to_owned(),
Err(_) => return,
let relative_repo = match path.strip_prefix(work_directory).log_err() {
Some(relative_repo) => relative_repo.to_owned(),
None => return,
};
drop(worktree);
let remote_id = self.remote_id();
let client = self.client.clone();
let git_ptr = repo.repo_ptr.clone();
let diff_base_task = cx
.background()
.spawn(async move { git_ptr.lock().load_index_text(&relative_repo) });
cx.spawn(|_, mut cx| async move {
let diff_base = cx
.background()
.spawn(async move { repo.repo.lock().load_index_text(&relative_repo) })
.await;
let diff_base = diff_base_task.await;
let buffer_id = buffer.update(&mut cx, |buffer, cx| {
buffer.set_diff_base(diff_base.clone(), cx);

View File

@@ -2,8 +2,8 @@ use crate::{worktree::WorktreeHandle, Event, *};
use fs::LineEnding;
use fs::{FakeFs, RealFs};
use futures::{future, StreamExt};
use gpui::AppContext;
use gpui::{executor::Deterministic, test::subscribe};
use globset::Glob;
use gpui::{executor::Deterministic, test::subscribe, AppContext};
use language::{
tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig,
OffsetRangeExt, Point, ToPoint,
@@ -503,7 +503,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
register_options: serde_json::to_value(
lsp::DidChangeWatchedFilesRegistrationOptions {
watchers: vec![lsp::FileSystemWatcher {
glob_pattern: "*.{rs,c}".to_string(),
glob_pattern: "/the-root/*.{rs,c}".to_string(),
kind: None,
}],
},
@@ -3297,9 +3297,13 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
.await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
assert_eq!(
search(&project, SearchQuery::text("TWO", false, true), cx)
.await
.unwrap(),
search(
&project,
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()),
cx
)
.await
.unwrap(),
HashMap::from_iter([
("two.rs".to_string(), vec![6..9]),
("three.rs".to_string(), vec![37..40])
@@ -3318,37 +3322,361 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
});
assert_eq!(
search(&project, SearchQuery::text("TWO", false, true), cx)
.await
.unwrap(),
search(
&project,
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()),
cx
)
.await
.unwrap(),
HashMap::from_iter([
("two.rs".to_string(), vec![6..9]),
("three.rs".to_string(), vec![37..40]),
("four.rs".to_string(), vec![25..28, 36..39])
])
);
async fn search(
project: &ModelHandle<Project>,
query: SearchQuery,
cx: &mut gpui::TestAppContext,
) -> Result<HashMap<String, Vec<Range<usize>>>> {
let results = project
.update(cx, |project, cx| project.search(query, cx))
.await?;
Ok(results
.into_iter()
.map(|(buffer, ranges)| {
buffer.read_with(cx, |buffer, _| {
let path = buffer.file().unwrap().path().to_string_lossy().to_string();
let ranges = ranges
.into_iter()
.map(|range| range.to_offset(buffer))
.collect::<Vec<_>>();
(path, ranges)
})
})
.collect())
}
}
#[gpui::test]
async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
let search_query = "file";
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/dir",
json!({
"one.rs": r#"// Rust file one"#,
"one.ts": r#"// TypeScript file one"#,
"two.rs": r#"// Rust file two"#,
"two.ts": r#"// TypeScript file two"#,
}),
)
.await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
assert!(
search(
&project,
SearchQuery::text(
search_query,
false,
true,
vec![Glob::new("*.odd").unwrap().compile_matcher()],
Vec::new()
),
cx
)
.await
.unwrap()
.is_empty(),
"If no inclusions match, no files should be returned"
);
assert_eq!(
search(
&project,
SearchQuery::text(
search_query,
false,
true,
vec![Glob::new("*.rs").unwrap().compile_matcher()],
Vec::new()
),
cx
)
.await
.unwrap(),
HashMap::from_iter([
("one.rs".to_string(), vec![8..12]),
("two.rs".to_string(), vec![8..12]),
]),
"Rust only search should give only Rust files"
);
assert_eq!(
search(
&project,
SearchQuery::text(
search_query,
false,
true,
vec![
Glob::new("*.ts").unwrap().compile_matcher(),
Glob::new("*.odd").unwrap().compile_matcher(),
],
Vec::new()
),
cx
)
.await
.unwrap(),
HashMap::from_iter([
("one.ts".to_string(), vec![14..18]),
("two.ts".to_string(), vec![14..18]),
]),
"TypeScript only search should give only TypeScript files, even if other inclusions don't match anything"
);
assert_eq!(
search(
&project,
SearchQuery::text(
search_query,
false,
true,
vec![
Glob::new("*.rs").unwrap().compile_matcher(),
Glob::new("*.ts").unwrap().compile_matcher(),
Glob::new("*.odd").unwrap().compile_matcher(),
],
Vec::new()
),
cx
)
.await
.unwrap(),
HashMap::from_iter([
("one.rs".to_string(), vec![8..12]),
("one.ts".to_string(), vec![14..18]),
("two.rs".to_string(), vec![8..12]),
("two.ts".to_string(), vec![14..18]),
]),
"Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything"
);
}
#[gpui::test]
async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
let search_query = "file";
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/dir",
json!({
"one.rs": r#"// Rust file one"#,
"one.ts": r#"// TypeScript file one"#,
"two.rs": r#"// Rust file two"#,
"two.ts": r#"// TypeScript file two"#,
}),
)
.await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
assert_eq!(
search(
&project,
SearchQuery::text(
search_query,
false,
true,
Vec::new(),
vec![Glob::new("*.odd").unwrap().compile_matcher()],
),
cx
)
.await
.unwrap(),
HashMap::from_iter([
("one.rs".to_string(), vec![8..12]),
("one.ts".to_string(), vec![14..18]),
("two.rs".to_string(), vec![8..12]),
("two.ts".to_string(), vec![14..18]),
]),
"If no exclusions match, all files should be returned"
);
assert_eq!(
search(
&project,
SearchQuery::text(
search_query,
false,
true,
Vec::new(),
vec![Glob::new("*.rs").unwrap().compile_matcher()],
),
cx
)
.await
.unwrap(),
HashMap::from_iter([
("one.ts".to_string(), vec![14..18]),
("two.ts".to_string(), vec![14..18]),
]),
"Rust exclusion search should give only TypeScript files"
);
assert_eq!(
search(
&project,
SearchQuery::text(
search_query,
false,
true,
Vec::new(),
vec![
Glob::new("*.ts").unwrap().compile_matcher(),
Glob::new("*.odd").unwrap().compile_matcher(),
],
),
cx
)
.await
.unwrap(),
HashMap::from_iter([
("one.rs".to_string(), vec![8..12]),
("two.rs".to_string(), vec![8..12]),
]),
"TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything"
);
assert!(
search(
&project,
SearchQuery::text(
search_query,
false,
true,
Vec::new(),
vec![
Glob::new("*.rs").unwrap().compile_matcher(),
Glob::new("*.ts").unwrap().compile_matcher(),
Glob::new("*.odd").unwrap().compile_matcher(),
],
),
cx
)
.await
.unwrap().is_empty(),
"Rust and typescript exclusion should give no files, even if other exclusions don't match anything"
);
}
#[gpui::test]
async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) {
let search_query = "file";
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/dir",
json!({
"one.rs": r#"// Rust file one"#,
"one.ts": r#"// TypeScript file one"#,
"two.rs": r#"// Rust file two"#,
"two.ts": r#"// TypeScript file two"#,
}),
)
.await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
assert!(
search(
&project,
SearchQuery::text(
search_query,
false,
true,
vec![Glob::new("*.odd").unwrap().compile_matcher()],
vec![Glob::new("*.odd").unwrap().compile_matcher()],
),
cx
)
.await
.unwrap()
.is_empty(),
"If both no exclusions and inclusions match, exclusions should win and return nothing"
);
assert!(
search(
&project,
SearchQuery::text(
search_query,
false,
true,
vec![Glob::new("*.ts").unwrap().compile_matcher()],
vec![Glob::new("*.ts").unwrap().compile_matcher()],
),
cx
)
.await
.unwrap()
.is_empty(),
"If both TypeScript exclusions and inclusions match, exclusions should win and return nothing files."
);
assert!(
search(
&project,
SearchQuery::text(
search_query,
false,
true,
vec![
Glob::new("*.ts").unwrap().compile_matcher(),
Glob::new("*.odd").unwrap().compile_matcher()
],
vec![
Glob::new("*.ts").unwrap().compile_matcher(),
Glob::new("*.odd").unwrap().compile_matcher()
],
),
cx
)
.await
.unwrap()
.is_empty(),
"Non-matching inclusions and exclusions should not change that."
);
assert_eq!(
search(
&project,
SearchQuery::text(
search_query,
false,
true,
vec![
Glob::new("*.ts").unwrap().compile_matcher(),
Glob::new("*.odd").unwrap().compile_matcher()
],
vec![
Glob::new("*.rs").unwrap().compile_matcher(),
Glob::new("*.odd").unwrap().compile_matcher()
],
),
cx
)
.await
.unwrap(),
HashMap::from_iter([
("one.ts".to_string(), vec![14..18]),
("two.ts".to_string(), vec![14..18]),
]),
"Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files"
);
}
async fn search(
project: &ModelHandle<Project>,
query: SearchQuery,
cx: &mut gpui::TestAppContext,
) -> Result<HashMap<String, Vec<Range<usize>>>> {
let results = project
.update(cx, |project, cx| project.search(query, cx))
.await?;
Ok(results
.into_iter()
.map(|(buffer, ranges)| {
buffer.read_with(cx, |buffer, _| {
let path = buffer.file().unwrap().path().to_string_lossy().to_string();
let ranges = ranges
.into_iter()
.map(|range| range.to_offset(buffer))
.collect::<Vec<_>>();
(path, ranges)
})
})
.collect())
}

View File

@@ -1,22 +1,27 @@
use aho_corasick::{AhoCorasick, AhoCorasickBuilder};
use anyhow::Result;
use client::proto;
use globset::{Glob, GlobMatcher};
use itertools::Itertools;
use language::{char_kind, Rope};
use regex::{Regex, RegexBuilder};
use smol::future::yield_now;
use std::{
io::{BufRead, BufReader, Read},
ops::Range,
path::Path,
sync::Arc,
};
#[derive(Clone)]
#[derive(Clone, Debug)]
pub enum SearchQuery {
Text {
search: Arc<AhoCorasick<usize>>,
query: Arc<str>,
whole_word: bool,
case_sensitive: bool,
files_to_include: Vec<GlobMatcher>,
files_to_exclude: Vec<GlobMatcher>,
},
Regex {
regex: Regex,
@@ -24,11 +29,19 @@ pub enum SearchQuery {
multiline: bool,
whole_word: bool,
case_sensitive: bool,
files_to_include: Vec<GlobMatcher>,
files_to_exclude: Vec<GlobMatcher>,
},
}
impl SearchQuery {
pub fn text(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Self {
pub fn text(
query: impl ToString,
whole_word: bool,
case_sensitive: bool,
files_to_include: Vec<GlobMatcher>,
files_to_exclude: Vec<GlobMatcher>,
) -> Self {
let query = query.to_string();
let search = AhoCorasickBuilder::new()
.auto_configure(&[&query])
@@ -39,10 +52,18 @@ impl SearchQuery {
query: Arc::from(query),
whole_word,
case_sensitive,
files_to_include,
files_to_exclude,
}
}
pub fn regex(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Result<Self> {
pub fn regex(
query: impl ToString,
whole_word: bool,
case_sensitive: bool,
files_to_include: Vec<GlobMatcher>,
files_to_exclude: Vec<GlobMatcher>,
) -> Result<Self> {
let mut query = query.to_string();
let initial_query = Arc::from(query.as_str());
if whole_word {
@@ -64,17 +85,27 @@ impl SearchQuery {
multiline,
whole_word,
case_sensitive,
files_to_include,
files_to_exclude,
})
}
pub fn from_proto(message: proto::SearchProject) -> Result<Self> {
if message.regex {
Self::regex(message.query, message.whole_word, message.case_sensitive)
Self::regex(
message.query,
message.whole_word,
message.case_sensitive,
deserialize_globs(&message.files_to_include)?,
deserialize_globs(&message.files_to_exclude)?,
)
} else {
Ok(Self::text(
message.query,
message.whole_word,
message.case_sensitive,
deserialize_globs(&message.files_to_include)?,
deserialize_globs(&message.files_to_exclude)?,
))
}
}
@@ -86,6 +117,16 @@ impl SearchQuery {
regex: self.is_regex(),
whole_word: self.whole_word(),
case_sensitive: self.case_sensitive(),
files_to_include: self
.files_to_include()
.iter()
.map(|g| g.glob().to_string())
.join(","),
files_to_exclude: self
.files_to_exclude()
.iter()
.map(|g| g.glob().to_string())
.join(","),
}
}
@@ -224,4 +265,52 @@ impl SearchQuery {
pub fn is_regex(&self) -> bool {
matches!(self, Self::Regex { .. })
}
pub fn files_to_include(&self) -> &[GlobMatcher] {
match self {
Self::Text {
files_to_include, ..
} => files_to_include,
Self::Regex {
files_to_include, ..
} => files_to_include,
}
}
pub fn files_to_exclude(&self) -> &[GlobMatcher] {
match self {
Self::Text {
files_to_exclude, ..
} => files_to_exclude,
Self::Regex {
files_to_exclude, ..
} => files_to_exclude,
}
}
pub fn file_matches(&self, file_path: Option<&Path>) -> bool {
match file_path {
Some(file_path) => {
!self
.files_to_exclude()
.iter()
.any(|exclude_glob| exclude_glob.is_match(file_path))
&& (self.files_to_include().is_empty()
|| self
.files_to_include()
.iter()
.any(|include_glob| include_glob.is_match(file_path)))
}
None => self.files_to_include().is_empty(),
}
}
}
fn deserialize_globs(glob_set: &str) -> Result<Vec<GlobMatcher>> {
glob_set
.split(',')
.map(str::trim)
.filter(|glob_str| !glob_str.is_empty())
.map(|glob_str| Ok(Glob::new(glob_str)?.compile_matcher()))
.collect()
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ use gpui::{
actions,
anyhow::{anyhow, Result},
elements::{
AnchorCorner, ChildView, ContainerStyle, Empty, Flex, Label, MouseEventHandler,
AnchorCorner, ChildView, ComponentHost, ContainerStyle, Empty, Flex, MouseEventHandler,
ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
},
geometry::vector::Vector2F,
@@ -16,7 +16,10 @@ use gpui::{
ViewHandle, WeakViewHandle,
};
use menu::{Confirm, SelectNext, SelectPrev};
use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
use project::{
repository::GitFileStatus, Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree,
WorktreeId,
};
use settings::Settings;
use std::{
cmp::Ordering,
@@ -26,7 +29,7 @@ use std::{
path::Path,
sync::Arc,
};
use theme::ProjectPanelEntry;
use theme::{ui::FileName, ProjectPanelEntry};
use unicase::UniCase;
use workspace::Workspace;
@@ -86,6 +89,7 @@ pub struct EntryDetails {
is_editing: bool,
is_processing: bool,
is_cut: bool,
git_status: Option<GitFileStatus>,
}
actions!(
@@ -196,6 +200,7 @@ impl ProjectPanel {
})
.detach();
let view_id = cx.view_id();
let mut this = Self {
project: project.clone(),
list: Default::default(),
@@ -206,7 +211,7 @@ impl ProjectPanel {
edit_state: None,
filename_editor,
clipboard_entry: None,
context_menu: cx.add_view(ContextMenu::new),
context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
dragged_entry_destination: None,
workspace: workspace.weak_handle(),
};
@@ -1006,7 +1011,13 @@ impl ProjectPanel {
.unwrap_or(&[]);
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
for entry in &visible_worktree_entries[entry_range] {
for (entry, repo) in
snapshot.entries_with_repos(visible_worktree_entries[entry_range].iter())
{
let status = (entry.path.parent().is_some() && !entry.is_ignored)
.then(|| repo.and_then(|repo| repo.status_for_path(&snapshot, &entry.path)))
.flatten();
let mut details = EntryDetails {
filename: entry
.path
@@ -1027,6 +1038,7 @@ impl ProjectPanel {
is_cut: self
.clipboard_entry
.map_or(false, |e| e.is_cut() && e.entry_id() == entry.id),
git_status: status,
};
if let Some(edit_state) = &self.edit_state {
@@ -1095,12 +1107,16 @@ impl ProjectPanel {
.flex(1.0, true)
.into_any()
} else {
Label::new(details.filename.clone(), style.text.clone())
.contained()
.with_margin_left(style.icon_spacing)
.aligned()
.left()
.into_any()
ComponentHost::new(FileName::new(
details.filename.clone(),
details.git_status,
FileName::style(style.text.clone(), &cx.global::<Settings>().theme),
))
.contained()
.with_margin_left(style.icon_spacing)
.aligned()
.left()
.into_any()
})
.constrained()
.with_height(style.height)
@@ -1316,10 +1332,9 @@ impl View for ProjectPanel {
}
}
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.add_identifier("menu");
cx
fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
Self::reset_to_default_keymap_context(keymap);
keymap.add_identifier("menu");
}
}

View File

@@ -318,10 +318,10 @@ mod tests {
},
);
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
// Create the project symbols view.
let symbols = cx.add_view(&workspace, |cx| {
let symbols = cx.add_view(window_id, |cx| {
ProjectSymbols::new(
ProjectSymbolsDelegate::new(workspace.downgrade(), project.clone()),
cx,

View File

@@ -329,9 +329,11 @@ message UpdateWorktree {
string root_name = 3;
repeated Entry updated_entries = 4;
repeated uint64 removed_entries = 5;
uint64 scan_id = 6;
bool is_last_update = 7;
string abs_path = 8;
repeated RepositoryEntry updated_repositories = 6;
repeated uint64 removed_repositories = 7;
uint64 scan_id = 8;
bool is_last_update = 9;
string abs_path = 10;
}
message CreateProjectEntry {
@@ -678,6 +680,8 @@ message SearchProject {
bool regex = 3;
bool whole_word = 4;
bool case_sensitive = 5;
string files_to_include = 6;
string files_to_exclude = 7;
}
message SearchProjectResponse {
@@ -979,6 +983,25 @@ message Entry {
bool is_ignored = 7;
}
message RepositoryEntry {
uint64 work_directory_id = 1;
optional string branch = 2;
repeated string removed_repo_paths = 3;
repeated StatusEntry updated_statuses = 4;
}
message StatusEntry {
string repo_path = 1;
GitStatus status = 2;
}
enum GitStatus {
Added = 0;
Modified = 1;
Conflict = 2;
}
message BufferState {
uint64 id = 1;
optional File file = 2;

View File

@@ -1,17 +1,18 @@
use super::{entity_messages, messages, request_messages, ConnectionId, TypedEnvelope};
use anyhow::{anyhow, Result};
use async_tungstenite::tungstenite::Message as WebSocketMessage;
use collections::HashMap;
use futures::{SinkExt as _, StreamExt as _};
use prost::Message as _;
use serde::Serialize;
use std::any::{Any, TypeId};
use std::fmt;
use std::{
cmp,
fmt::Debug,
io, iter,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use std::{fmt, mem};
include!(concat!(env!("OUT_DIR"), "/zed.messages.rs"));
@@ -484,14 +485,21 @@ pub fn split_worktree_update(
mut message: UpdateWorktree,
max_chunk_size: usize,
) -> impl Iterator<Item = UpdateWorktree> {
let mut done = false;
let mut done_files = false;
let mut repository_map = message
.updated_repositories
.into_iter()
.map(|repo| (repo.work_directory_id, repo))
.collect::<HashMap<_, _>>();
iter::from_fn(move || {
if done {
if done_files {
return None;
}
let updated_entries_chunk_size = cmp::min(message.updated_entries.len(), max_chunk_size);
let updated_entries = message
let updated_entries: Vec<_> = message
.updated_entries
.drain(..updated_entries_chunk_size)
.collect();
@@ -502,7 +510,28 @@ pub fn split_worktree_update(
.drain(..removed_entries_chunk_size)
.collect();
done = message.updated_entries.is_empty() && message.removed_entries.is_empty();
done_files = message.updated_entries.is_empty() && message.removed_entries.is_empty();
let mut updated_repositories = Vec::new();
if !repository_map.is_empty() {
for entry in &updated_entries {
if let Some(repo) = repository_map.remove(&entry.id) {
updated_repositories.push(repo)
}
}
}
let removed_repositories = if done_files {
mem::take(&mut message.removed_repositories)
} else {
Default::default()
};
if done_files {
updated_repositories.extend(mem::take(&mut repository_map).into_values());
}
Some(UpdateWorktree {
project_id: message.project_id,
worktree_id: message.worktree_id,
@@ -511,7 +540,9 @@ pub fn split_worktree_update(
updated_entries,
removed_entries,
scan_id: message.scan_id,
is_last_update: done && message.is_last_update,
is_last_update: done_files && message.is_last_update,
updated_repositories,
removed_repositories,
})
})
}

View File

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

View File

@@ -27,6 +27,7 @@ serde.workspace = true
serde_derive.workspace = true
smallvec.workspace = true
smol.workspace = true
globset.workspace = true
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }

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