Compare commits

...

128 Commits

Author SHA1 Message Date
Kirill Bulatov
eb12b0b9ed Merge pull request #2463 from zed-industries/kb/reapply-modal-accessibility
Reintroduce more accesible modal keybindings
2023-05-17 13:46:25 -04:00
Kirill Bulatov
5c3690c3f1 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-17 13:45:18 -04:00
Joseph Lyons
baedb7d0fc v0.86.x stable 2023-05-17 12:38:43 -04:00
Joseph Lyons
fd72d24060 zed 0.86.1 2023-05-11 11:42:14 -04:00
Joseph Lyons
a17b5e8a80 Revert "More keybindings in macOs modals with buttons"
This reverts commit 1398a12062.
2023-05-11 11:38:30 -04:00
Joseph Lyons
0092df7c51 v0.86.x preview 2023-05-10 16:46:52 -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
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
109 changed files with 3765 additions and 1493 deletions

25
Cargo.lock generated
View File

@@ -1189,7 +1189,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.10.0"
version = "0.12.0"
dependencies = [
"anyhow",
"async-tungstenite",
@@ -1961,12 +1961,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"
@@ -4725,6 +4719,7 @@ dependencies = [
"glob",
"gpui",
"ignore",
"itertools",
"language",
"lazy_static",
"log",
@@ -5777,6 +5772,7 @@ dependencies = [
"collections",
"editor",
"futures 0.3.25",
"glob",
"gpui",
"language",
"log",
@@ -5894,15 +5890,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 +5965,6 @@ dependencies = [
"serde",
"serde_derive",
"serde_json",
"serde_path_to_error",
"sqlez",
"staff_mode",
"theme",
@@ -6700,7 +6686,6 @@ dependencies = [
"serde",
"serde_derive",
"serde_json",
"serde_path_to_error",
"toml",
]
@@ -8546,7 +8531,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "zed"
version = "0.85.0"
version = "0.86.1"
dependencies = [
"activity_indicator",
"anyhow",
@@ -8572,7 +8557,6 @@ dependencies = [
"ctor",
"db",
"diagnostics",
"easy-parallel",
"editor",
"env_logger",
"feedback",
@@ -8615,7 +8599,6 @@ dependencies = [
"serde",
"serde_derive",
"serde_json",
"serde_path_to_error",
"settings",
"simplelog",
"smallvec",

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

@@ -33,6 +33,16 @@
// 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 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

@@ -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

@@ -270,7 +270,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 +404,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 +454,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.0"
publish = false
[[bin]]

View File

@@ -82,6 +82,20 @@ 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,
"scan_id" INTEGER 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");
CREATE TABLE "worktree_diagnostic_summaries" (
"project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL,
@@ -153,7 +167,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

@@ -14,6 +14,7 @@ mod user;
mod worktree;
mod worktree_diagnostic_summary;
mod worktree_entry;
mod worktree_repository;
use crate::executor::Executor;
use crate::{Error, Result};
@@ -1489,6 +1490,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 +1501,75 @@ 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,
});
}
}
}
@@ -2330,6 +2370,53 @@ 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?;
}
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 +2592,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 +2630,29 @@ 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.push(proto::RepositoryEntry {
work_directory_id: db_repository_entry.work_directory_id as u64,
branch: db_repository_entry.branch,
});
}
}
}
// Populate worktree diagnostic summaries.
{
let mut db_summaries = worktree_diagnostic_summary::Entity::find()
@@ -3223,6 +3334,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 +3390,7 @@ pub struct Worktree {
pub root_name: String,
pub visible: bool,
pub entries: Vec<proto::Entry>,
pub repository_entries: Vec<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

@@ -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,
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

@@ -13,8 +13,8 @@ use editor::{
use fs::{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,92 @@ 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(iterations = 10)]
async fn test_fs_operations(
deterministic: Arc<Deterministic>,
@@ -3076,13 +3162,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 +3177,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 +3308,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 +3922,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 +4548,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 +6897,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 +6908,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

@@ -716,7 +716,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 {
@@ -785,6 +788,28 @@ async fn apply_client_operation(
}
client.fs.set_index_for_repo(&dot_git_dir, &contents).await;
}
ClientOperation::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;
}
}
Ok(())
}
@@ -859,6 +884,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,
@@ -1151,6 +1182,10 @@ enum ClientOperation {
repo_path: PathBuf,
contents: Vec<(PathBuf, String)>,
},
WriteGitBranch {
repo_path: PathBuf,
new_branch: Option<String>,
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -1664,10 +1699,11 @@ impl TestPlan {
}
// Update a git index
91..=95 => {
91..=93 => {
let repo_path = client
.fs
.directories()
.into_iter()
.choose(&mut self.rng)
.unwrap()
.clone();
@@ -1698,6 +1734,24 @@ impl TestPlan {
};
}
// Update a git branch
94..=95 => {
let repo_path = client
.fs
.directories()
.choose(&mut self.rng)
.unwrap()
.clone();
let new_branch = (self.rng.gen_range(0..10) > 3)
.then(|| Alphanumeric.sample_string(&mut self.rng, 8));
break ClientOperation::WriteGitBranch {
repo_path,
new_branch,
};
}
// Create or update a file or directory
96.. => {
let is_dir = self.rng.gen::<bool>();

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,
@@ -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
});

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

@@ -1227,6 +1227,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);
@@ -1274,7 +1275,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(),
@@ -1425,13 +1427,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 +2596,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 +2804,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
});
@@ -5674,28 +5682,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 +5766,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 +6061,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);
}
}
@@ -7157,28 +7167,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();

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
@@ -1118,7 +1128,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 +1290,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 +1328,10 @@ impl EditorElement {
)],
)
})
.map(|line| LineWithInvisibles {
line,
invisibles: Vec::new(),
})
.collect()
} else {
let style = &self.style;
@@ -1359,15 +1374,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 +1407,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 +1430,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 +1609,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 +1831,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 +1846,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 {
@@ -1810,10 +2048,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 +2204,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 +2243,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 +2329,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 +2390,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 +2422,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 +2691,7 @@ impl HighlightedRange {
}
}
pub fn position_to_display_point(
fn position_to_display_point(
position: Vector2F,
text_bounds: RectF,
position_map: &PositionMap,
@@ -2448,7 +2708,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 +2736,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 +2776,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 +2826,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 +2858,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

@@ -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::{
@@ -704,10 +704,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 +864,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)))

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()
@@ -1321,7 +1321,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 +1478,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 +1605,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 +1637,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 +1830,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 +2938,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 +2978,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 +3116,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 +3141,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 +3154,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 +3251,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,
@@ -4715,7 +4719,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 +4805,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

@@ -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,21 @@ 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 fn paths(&self) -> Vec<PathBuf> {
let mut result = Vec::new();
let mut queue = collections::VecDeque::new();

View File

@@ -5,6 +5,7 @@ use std::{
path::{Component, Path, PathBuf},
sync::Arc,
};
use util::ResultExt;
pub use git2::Repository as LibGitRepository;
@@ -13,6 +14,14 @@ 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>;
}
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 +55,12 @@ 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())
}
}
#[derive(Debug, Clone, Default)]
@@ -56,6 +71,7 @@ pub struct FakeGitRepository {
#[derive(Debug, Clone, Default)]
pub struct FakeGitRepositoryState {
pub index_contents: HashMap<PathBuf, String>,
pub branch_name: Option<String>,
}
impl FakeGitRepository {
@@ -72,6 +88,11 @@ 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 check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {

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

@@ -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, ())
@@ -597,7 +600,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 +645,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 +670,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

@@ -58,6 +58,7 @@ similar = "1.3"
smol.workspace = true
thiserror.workspace = true
toml = "0.5"
itertools = "0.10"
[dev-dependencies]
ctor.workspace = true

View File

@@ -64,6 +64,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 +110,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)]
@@ -124,7 +126,7 @@ pub struct Project {
/// 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<()>,
@@ -441,6 +443,7 @@ 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(),
@@ -509,6 +512,7 @@ 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(),
@@ -1401,9 +1405,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?;
@@ -3200,9 +3205,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 +3217,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 +4208,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 +4304,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);
@@ -4688,40 +4707,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

@@ -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::Pattern::new("*.odd").unwrap()],
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::Pattern::new("*.rs").unwrap()],
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::Pattern::new("*.ts").unwrap(),
glob::Pattern::new("*.odd").unwrap(),
],
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::Pattern::new("*.rs").unwrap(),
glob::Pattern::new("*.ts").unwrap(),
glob::Pattern::new("*.odd").unwrap(),
],
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::Pattern::new("*.odd").unwrap()],
),
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::Pattern::new("*.rs").unwrap()],
),
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::Pattern::new("*.ts").unwrap(),
glob::Pattern::new("*.odd").unwrap(),
],
),
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::Pattern::new("*.rs").unwrap(),
glob::Pattern::new("*.ts").unwrap(),
glob::Pattern::new("*.odd").unwrap(),
],
),
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::Pattern::new("*.odd").unwrap()],
vec![glob::Pattern::new("*.odd").unwrap()],
),
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::Pattern::new("*.ts").unwrap()],
vec![glob::Pattern::new("*.ts").unwrap()],
),
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::Pattern::new("*.ts").unwrap(),
glob::Pattern::new("*.odd").unwrap()
],
vec![
glob::Pattern::new("*.ts").unwrap(),
glob::Pattern::new("*.odd").unwrap()
],
),
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::Pattern::new("*.ts").unwrap(),
glob::Pattern::new("*.odd").unwrap()
],
vec![
glob::Pattern::new("*.rs").unwrap(),
glob::Pattern::new("*.odd").unwrap()
],
),
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,26 @@
use aho_corasick::{AhoCorasick, AhoCorasickBuilder};
use anyhow::Result;
use client::proto;
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<glob::Pattern>,
files_to_exclude: Vec<glob::Pattern>,
},
Regex {
regex: Regex,
@@ -24,11 +28,19 @@ pub enum SearchQuery {
multiline: bool,
whole_word: bool,
case_sensitive: bool,
files_to_include: Vec<glob::Pattern>,
files_to_exclude: Vec<glob::Pattern>,
},
}
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<glob::Pattern>,
files_to_exclude: Vec<glob::Pattern>,
) -> Self {
let query = query.to_string();
let search = AhoCorasickBuilder::new()
.auto_configure(&[&query])
@@ -39,10 +51,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<glob::Pattern>,
files_to_exclude: Vec<glob::Pattern>,
) -> Result<Self> {
let mut query = query.to_string();
let initial_query = Arc::from(query.as_str());
if whole_word {
@@ -64,17 +84,51 @@ 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,
message
.files_to_include
.split(',')
.map(str::trim)
.filter(|glob_str| !glob_str.is_empty())
.map(|glob_str| glob::Pattern::new(glob_str))
.collect::<Result<_, _>>()?,
message
.files_to_exclude
.split(',')
.map(str::trim)
.filter(|glob_str| !glob_str.is_empty())
.map(|glob_str| glob::Pattern::new(glob_str))
.collect::<Result<_, _>>()?,
)
} else {
Ok(Self::text(
message.query,
message.whole_word,
message.case_sensitive,
message
.files_to_include
.split(',')
.map(str::trim)
.filter(|glob_str| !glob_str.is_empty())
.map(|glob_str| glob::Pattern::new(glob_str))
.collect::<Result<_, _>>()?,
message
.files_to_exclude
.split(',')
.map(str::trim)
.filter(|glob_str| !glob_str.is_empty())
.map(|glob_str| glob::Pattern::new(glob_str))
.collect::<Result<_, _>>()?,
))
}
}
@@ -86,6 +140,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(ToString::to_string)
.join(","),
files_to_exclude: self
.files_to_exclude()
.iter()
.map(ToString::to_string)
.join(","),
}
}
@@ -224,4 +288,43 @@ impl SearchQuery {
pub fn is_regex(&self) -> bool {
matches!(self, Self::Regex { .. })
}
pub fn files_to_include(&self) -> &[glob::Pattern] {
match self {
Self::Text {
files_to_include, ..
} => files_to_include,
Self::Regex {
files_to_include, ..
} => files_to_include,
}
}
pub fn files_to_exclude(&self) -> &[glob::Pattern] {
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.matches_path(file_path))
&& (self.files_to_include().is_empty()
|| self
.files_to_include()
.iter()
.any(|include_glob| include_glob.matches_path(file_path)))
}
None => self.files_to_include().is_empty(),
}
}
}

View File

@@ -51,7 +51,7 @@ use std::{
},
time::{Duration, SystemTime},
};
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeSet};
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
use util::{paths::HOME, ResultExt, TryFutureExt};
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
@@ -102,6 +102,7 @@ pub struct Snapshot {
root_char_bag: CharBag,
entries_by_path: SumTree<Entry>,
entries_by_id: SumTree<PathEntry>,
repository_entries: TreeMap<RepositoryWorkDirectory, RepositoryEntry>,
/// A number that increases every time the worktree begins scanning
/// a set of paths from the filesystem. This scanning could be caused
@@ -116,45 +117,133 @@ pub struct Snapshot {
completed_scan_id: usize,
}
#[derive(Clone)]
pub struct GitRepositoryEntry {
pub(crate) repo: Arc<Mutex<dyn GitRepository>>,
pub(crate) scan_id: usize,
// Path to folder containing the .git file or directory
pub(crate) content_path: Arc<Path>,
// Path to the actual .git folder.
// Note: if .git is a file, this points to the folder indicated by the .git file
pub(crate) git_dir_path: Arc<Path>,
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RepositoryEntry {
pub(crate) work_directory: WorkDirectoryEntry,
pub(crate) branch: Option<Arc<str>>,
}
impl std::fmt::Debug for GitRepositoryEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GitRepositoryEntry")
.field("content_path", &self.content_path)
.field("git_dir_path", &self.git_dir_path)
.finish()
impl RepositoryEntry {
pub fn branch(&self) -> Option<Arc<str>> {
self.branch.clone()
}
pub fn work_directory_id(&self) -> ProjectEntryId {
*self.work_directory
}
pub fn work_directory(&self, snapshot: &Snapshot) -> Option<RepositoryWorkDirectory> {
snapshot
.entry_for_id(self.work_directory_id())
.map(|entry| RepositoryWorkDirectory(entry.path.clone()))
}
pub(crate) fn contains(&self, snapshot: &Snapshot, path: &Path) -> bool {
self.work_directory.contains(snapshot, path)
}
}
#[derive(Debug)]
impl From<&RepositoryEntry> for proto::RepositoryEntry {
fn from(value: &RepositoryEntry) -> Self {
proto::RepositoryEntry {
work_directory_id: value.work_directory.to_proto(),
branch: value.branch.as_ref().map(|str| str.to_string()),
}
}
}
/// This path corresponds to the 'content path' (the folder that contains the .git)
#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
pub struct RepositoryWorkDirectory(Arc<Path>);
impl Default for RepositoryWorkDirectory {
fn default() -> Self {
RepositoryWorkDirectory(Arc::from(Path::new("")))
}
}
#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
pub struct WorkDirectoryEntry(ProjectEntryId);
impl WorkDirectoryEntry {
// Note that these paths should be relative to the worktree root.
pub(crate) fn contains(&self, snapshot: &Snapshot, path: &Path) -> bool {
snapshot
.entry_for_id(self.0)
.map(|entry| path.starts_with(&entry.path))
.unwrap_or(false)
}
pub(crate) fn relativize(&self, worktree: &Snapshot, path: &Path) -> Option<RepoPath> {
worktree.entry_for_id(self.0).and_then(|entry| {
path.strip_prefix(&entry.path)
.ok()
.map(move |path| RepoPath(path.to_owned()))
})
}
}
impl Deref for WorkDirectoryEntry {
type Target = ProjectEntryId;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> From<ProjectEntryId> for WorkDirectoryEntry {
fn from(value: ProjectEntryId) -> Self {
WorkDirectoryEntry(value)
}
}
#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
pub struct RepoPath(PathBuf);
impl AsRef<Path> for RepoPath {
fn as_ref(&self) -> &Path {
self.0.as_ref()
}
}
impl Deref for RepoPath {
type Target = PathBuf;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<Path> for RepositoryWorkDirectory {
fn as_ref(&self) -> &Path {
self.0.as_ref()
}
}
#[derive(Debug, Clone)]
pub struct LocalSnapshot {
ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
git_repositories: Vec<GitRepositoryEntry>,
// The ProjectEntryId corresponds to the entry for the .git dir
// work_directory_id
git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
removed_entry_ids: HashMap<u64, ProjectEntryId>,
next_entry_id: Arc<AtomicUsize>,
snapshot: Snapshot,
}
impl Clone for LocalSnapshot {
fn clone(&self) -> Self {
Self {
ignores_by_parent_abs_path: self.ignores_by_parent_abs_path.clone(),
git_repositories: self.git_repositories.iter().cloned().collect(),
removed_entry_ids: self.removed_entry_ids.clone(),
next_entry_id: self.next_entry_id.clone(),
snapshot: self.snapshot.clone(),
}
#[derive(Debug, Clone)]
pub struct LocalRepositoryEntry {
pub(crate) scan_id: usize,
pub(crate) repo_ptr: Arc<Mutex<dyn GitRepository>>,
/// Path to the actual .git folder.
/// Note: if .git is a file, this points to the folder indicated by the .git file
pub(crate) git_dir_path: Arc<Path>,
}
impl LocalRepositoryEntry {
// Note that this path should be relative to the worktree root.
pub(crate) fn in_dot_git(&self, path: &Path) -> bool {
path.starts_with(self.git_dir_path.as_ref())
}
}
@@ -191,7 +280,7 @@ struct ShareState {
pub enum Event {
UpdatedEntries(HashMap<Arc<Path>, PathChange>),
UpdatedGitRepositories(Vec<GitRepositoryEntry>),
UpdatedGitRepositories(HashMap<Arc<Path>, LocalRepositoryEntry>),
}
impl Entity for Worktree {
@@ -222,8 +311,8 @@ impl Worktree {
let mut snapshot = LocalSnapshot {
ignores_by_parent_abs_path: Default::default(),
git_repositories: Default::default(),
removed_entry_ids: Default::default(),
git_repositories: Default::default(),
next_entry_id,
snapshot: Snapshot {
id: WorktreeId::from_usize(cx.model_id()),
@@ -232,6 +321,7 @@ impl Worktree {
root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
entries_by_path: Default::default(),
entries_by_id: Default::default(),
repository_entries: Default::default(),
scan_id: 1,
completed_scan_id: 0,
},
@@ -330,6 +420,7 @@ impl Worktree {
.collect(),
entries_by_path: Default::default(),
entries_by_id: Default::default(),
repository_entries: Default::default(),
scan_id: 1,
completed_scan_id: 0,
};
@@ -506,6 +597,7 @@ impl LocalWorktree {
pub(crate) fn load_buffer(
&mut self,
id: u64,
path: &Path,
cx: &mut ModelContext<Worktree>,
) -> Task<Result<ModelHandle<Buffer>>> {
@@ -514,8 +606,12 @@ impl LocalWorktree {
let (file, contents, diff_base) = this
.update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx))
.await?;
let text_buffer = cx
.background()
.spawn(async move { text::Buffer::new(0, id, contents) })
.await;
Ok(cx.add_model(|cx| {
let mut buffer = Buffer::from_file(0, contents, diff_base, Arc::new(file), cx);
let mut buffer = Buffer::build(text_buffer, diff_base, Some(Arc::new(file)));
buffer.git_diff_recalc(cx);
buffer
}))
@@ -593,10 +689,8 @@ impl LocalWorktree {
}
fn set_snapshot(&mut self, new_snapshot: LocalSnapshot, cx: &mut ModelContext<Worktree>) {
let updated_repos = Self::changed_repos(
&self.snapshot.git_repositories,
&new_snapshot.git_repositories,
);
let updated_repos =
self.changed_repos(&self.git_repositories, &new_snapshot.git_repositories);
self.snapshot = new_snapshot;
if let Some(share) = self.share.as_mut() {
@@ -609,31 +703,57 @@ impl LocalWorktree {
}
fn changed_repos(
old_repos: &[GitRepositoryEntry],
new_repos: &[GitRepositoryEntry],
) -> Vec<GitRepositoryEntry> {
fn diff<'a>(
a: &'a [GitRepositoryEntry],
b: &'a [GitRepositoryEntry],
updated: &mut HashMap<&'a Path, GitRepositoryEntry>,
) {
for a_repo in a {
let matched = b.iter().find(|b_repo| {
a_repo.git_dir_path == b_repo.git_dir_path && a_repo.scan_id == b_repo.scan_id
});
&self,
old_repos: &TreeMap<ProjectEntryId, LocalRepositoryEntry>,
new_repos: &TreeMap<ProjectEntryId, LocalRepositoryEntry>,
) -> HashMap<Arc<Path>, LocalRepositoryEntry> {
let mut diff = HashMap::default();
let mut old_repos = old_repos.iter().peekable();
let mut new_repos = new_repos.iter().peekable();
loop {
match (old_repos.peek(), new_repos.peek()) {
(Some((old_entry_id, old_repo)), Some((new_entry_id, new_repo))) => {
match Ord::cmp(old_entry_id, new_entry_id) {
Ordering::Less => {
if let Some(entry) = self.entry_for_id(**old_entry_id) {
diff.insert(entry.path.clone(), (*old_repo).clone());
}
old_repos.next();
}
Ordering::Equal => {
if old_repo.scan_id != new_repo.scan_id {
if let Some(entry) = self.entry_for_id(**new_entry_id) {
diff.insert(entry.path.clone(), (*new_repo).clone());
}
}
if matched.is_none() {
updated.insert(a_repo.git_dir_path.as_ref(), a_repo.clone());
old_repos.next();
new_repos.next();
}
Ordering::Greater => {
if let Some(entry) = self.entry_for_id(**new_entry_id) {
diff.insert(entry.path.clone(), (*new_repo).clone());
}
new_repos.next();
}
}
}
(Some((old_entry_id, old_repo)), None) => {
if let Some(entry) = self.entry_for_id(**old_entry_id) {
diff.insert(entry.path.clone(), (*old_repo).clone());
}
old_repos.next();
}
(None, Some((new_entry_id, new_repo))) => {
if let Some(entry) = self.entry_for_id(**new_entry_id) {
diff.insert(entry.path.clone(), (*new_repo).clone());
}
new_repos.next();
}
(None, None) => break,
}
}
let mut updated = HashMap::<&Path, GitRepositoryEntry>::default();
diff(old_repos, new_repos, &mut updated);
diff(new_repos, old_repos, &mut updated);
updated.into_values().collect()
diff
}
pub fn scan_complete(&self) -> impl Future<Output = ()> {
@@ -674,18 +794,24 @@ impl LocalWorktree {
let fs = self.fs.clone();
let snapshot = self.snapshot();
let mut index_task = None;
if let Some(repo) = snapshot.repo_for(&path) {
let repo_path = repo.work_directory.relativize(self, &path).unwrap();
if let Some(repo) = self.git_repositories.get(&*repo.work_directory) {
let repo = repo.repo_ptr.to_owned();
index_task = Some(
cx.background()
.spawn(async move { repo.lock().load_index_text(&repo_path) }),
);
}
}
cx.spawn(|this, mut cx| async move {
let text = fs.load(&abs_path).await?;
let diff_base = if let Some(repo) = snapshot.repo_for(&path) {
if let Ok(repo_relative) = path.strip_prefix(repo.content_path) {
let repo_relative = repo_relative.to_owned();
cx.background()
.spawn(async move { repo.repo.lock().load_index_text(&repo_relative) })
.await
} else {
None
}
let diff_base = if let Some(index_task) = index_task {
index_task.await
} else {
None
};
@@ -997,9 +1123,9 @@ impl LocalWorktree {
let mut share_tx = Some(share_tx);
let mut prev_snapshot = LocalSnapshot {
ignores_by_parent_abs_path: Default::default(),
git_repositories: Default::default(),
removed_entry_ids: Default::default(),
next_entry_id: Default::default(),
git_repositories: Default::default(),
snapshot: Snapshot {
id: WorktreeId(worktree_id as usize),
abs_path: Path::new("").into(),
@@ -1007,6 +1133,7 @@ impl LocalWorktree {
root_char_bag: Default::default(),
entries_by_path: Default::default(),
entries_by_id: Default::default(),
repository_entries: Default::default(),
scan_id: 0,
completed_scan_id: 0,
},
@@ -1257,7 +1384,7 @@ impl Snapshot {
Some(removed_entry.path)
}
pub(crate) fn apply_remote_update(&mut self, update: proto::UpdateWorktree) -> Result<()> {
pub(crate) fn apply_remote_update(&mut self, mut update: proto::UpdateWorktree) -> Result<()> {
let mut entries_by_path_edits = Vec::new();
let mut entries_by_id_edits = Vec::new();
for entry_id in update.removed_entries {
@@ -1283,6 +1410,32 @@ impl Snapshot {
self.entries_by_path.edit(entries_by_path_edits, &());
self.entries_by_id.edit(entries_by_id_edits, &());
update.removed_repositories.sort_unstable();
self.repository_entries.retain(|_, entry| {
if let Ok(_) = update
.removed_repositories
.binary_search(&entry.work_directory.to_proto())
{
false
} else {
true
}
});
for repository in update.updated_repositories {
let repository = RepositoryEntry {
work_directory: ProjectEntryId::from_proto(repository.work_directory_id).into(),
branch: repository.branch.map(Into::into),
};
if let Some(entry) = self.entry_for_id(repository.work_directory_id()) {
self.repository_entries
.insert(RepositoryWorkDirectory(entry.path.clone()), repository)
} else {
log::error!("no work directory entry for repository {:?}", repository)
}
}
self.scan_id = update.scan_id as usize;
if update.is_last_update {
self.completed_scan_id = update.scan_id as usize;
@@ -1345,6 +1498,10 @@ impl Snapshot {
self.traverse_from_offset(true, include_ignored, 0)
}
pub fn repositories(&self) -> impl Iterator<Item = &RepositoryEntry> {
self.repository_entries.values()
}
pub fn paths(&self) -> impl Iterator<Item = &Arc<Path>> {
let empty_path = Path::new("");
self.entries_by_path
@@ -1375,6 +1532,16 @@ impl Snapshot {
&self.root_name
}
pub fn root_git_entry(&self) -> Option<RepositoryEntry> {
self.repository_entries
.get(&RepositoryWorkDirectory(Path::new("").into()))
.map(|entry| entry.to_owned())
}
pub fn git_entries(&self) -> impl Iterator<Item = &RepositoryEntry> {
self.repository_entries.values()
}
pub fn scan_id(&self) -> usize {
self.scan_id
}
@@ -1403,23 +1570,32 @@ impl Snapshot {
}
impl LocalSnapshot {
// Gives the most specific git repository for a given path
pub(crate) fn repo_for(&self, path: &Path) -> Option<GitRepositoryEntry> {
self.git_repositories
.iter()
.rev() //git_repository is ordered lexicographically
.find(|repo| repo.manages(path))
.cloned()
pub(crate) fn repo_for(&self, path: &Path) -> Option<RepositoryEntry> {
let mut max_len = 0;
let mut current_candidate = None;
for (work_directory, repo) in (&self.repository_entries).iter() {
if repo.contains(self, path) {
if work_directory.0.as_os_str().len() >= max_len {
current_candidate = Some(repo);
max_len = work_directory.0.as_os_str().len();
} else {
break;
}
}
}
current_candidate.map(|entry| entry.to_owned())
}
pub(crate) fn repo_with_dot_git_containing(
&mut self,
pub(crate) fn repo_for_metadata(
&self,
path: &Path,
) -> Option<&mut GitRepositoryEntry> {
// Git repositories cannot be nested, so we don't need to reverse the order
self.git_repositories
.iter_mut()
.find(|repo| repo.in_dot_git(path))
) -> Option<(ProjectEntryId, Arc<Mutex<dyn GitRepository>>)> {
let (entry_id, local_repo) = self
.git_repositories
.iter()
.find(|(_, repo)| repo.in_dot_git(path))?;
Some((*entry_id, local_repo.repo_ptr.to_owned()))
}
#[cfg(test)]
@@ -1434,6 +1610,8 @@ impl LocalSnapshot {
removed_entries: Default::default(),
scan_id: self.scan_id as u64,
is_last_update: true,
updated_repositories: self.repository_entries.values().map(Into::into).collect(),
removed_repositories: Default::default(),
}
}
@@ -1493,6 +1671,44 @@ impl LocalSnapshot {
}
}
let mut updated_repositories: Vec<proto::RepositoryEntry> = Vec::new();
let mut removed_repositories = Vec::new();
let mut self_repos = self.snapshot.repository_entries.iter().peekable();
let mut other_repos = other.snapshot.repository_entries.iter().peekable();
loop {
match (self_repos.peek(), other_repos.peek()) {
(Some((self_work_dir, self_repo)), Some((other_work_dir, other_repo))) => {
match Ord::cmp(self_work_dir, other_work_dir) {
Ordering::Less => {
updated_repositories.push((*self_repo).into());
self_repos.next();
}
Ordering::Equal => {
if self_repo != other_repo {
updated_repositories.push((*self_repo).into());
}
self_repos.next();
other_repos.next();
}
Ordering::Greater => {
removed_repositories.push(other_repo.work_directory.to_proto());
other_repos.next();
}
}
}
(Some((_, self_repo)), None) => {
updated_repositories.push((*self_repo).into());
self_repos.next();
}
(None, Some((_, other_repo))) => {
removed_repositories.push(other_repo.work_directory.to_proto());
other_repos.next();
}
(None, None) => break,
}
}
proto::UpdateWorktree {
project_id,
worktree_id,
@@ -1502,6 +1718,8 @@ impl LocalSnapshot {
removed_entries,
scan_id: self.scan_id as u64,
is_last_update: self.completed_scan_id == self.scan_id,
updated_repositories,
removed_repositories,
}
}
@@ -1590,24 +1808,7 @@ impl LocalSnapshot {
}
if parent_path.file_name() == Some(&DOT_GIT) {
let abs_path = self.abs_path.join(&parent_path);
let content_path: Arc<Path> = parent_path.parent().unwrap().into();
if let Err(ix) = self
.git_repositories
.binary_search_by_key(&&content_path, |repo| &repo.content_path)
{
if let Some(repo) = fs.open_repo(abs_path.as_path()) {
self.git_repositories.insert(
ix,
GitRepositoryEntry {
repo,
scan_id: 0,
content_path,
git_dir_path: parent_path,
},
);
}
}
self.build_repo(parent_path, fs);
}
let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)];
@@ -1628,6 +1829,50 @@ impl LocalSnapshot {
self.entries_by_id.edit(entries_by_id_edits, &());
}
fn build_repo(&mut self, parent_path: Arc<Path>, fs: &dyn Fs) -> Option<()> {
let abs_path = self.abs_path.join(&parent_path);
let work_dir: Arc<Path> = parent_path.parent().unwrap().into();
// Guard against repositories inside the repository metadata
if work_dir
.components()
.find(|component| component.as_os_str() == *DOT_GIT)
.is_some()
{
return None;
};
let work_dir_id = self
.entry_for_path(work_dir.clone())
.map(|entry| entry.id)?;
if self.git_repositories.get(&work_dir_id).is_none() {
let repo = fs.open_repo(abs_path.as_path())?;
let work_directory = RepositoryWorkDirectory(work_dir.clone());
let scan_id = self.scan_id;
let repo_lock = repo.lock();
self.repository_entries.insert(
work_directory,
RepositoryEntry {
work_directory: work_dir_id.into(),
branch: repo_lock.branch_name().map(Into::into),
},
);
drop(repo_lock);
self.git_repositories.insert(
work_dir_id,
LocalRepositoryEntry {
scan_id,
repo_ptr: repo,
git_dir_path: parent_path.clone(),
},
)
}
Some(())
}
fn reuse_entry_id(&mut self, entry: &mut Entry) {
if let Some(removed_entry_id) = self.removed_entry_ids.remove(&entry.inode) {
entry.id = removed_entry_id;
@@ -1666,14 +1911,6 @@ impl LocalSnapshot {
{
*scan_id = self.snapshot.scan_id;
}
} else if path.file_name() == Some(&DOT_GIT) {
let parent_path = path.parent().unwrap();
if let Ok(ix) = self
.git_repositories
.binary_search_by_key(&parent_path, |repo| repo.git_dir_path.as_ref())
{
self.git_repositories[ix].scan_id = self.snapshot.scan_id;
}
}
}
@@ -1713,22 +1950,6 @@ impl LocalSnapshot {
ignore_stack
}
pub fn git_repo_entries(&self) -> &[GitRepositoryEntry] {
&self.git_repositories
}
}
impl GitRepositoryEntry {
// Note that these paths should be relative to the worktree root.
pub(crate) fn manages(&self, path: &Path) -> bool {
path.starts_with(self.content_path.as_ref())
}
// Note that this path should be relative to the worktree root.
pub(crate) fn in_dot_git(&self, path: &Path) -> bool {
path.starts_with(self.git_dir_path.as_ref())
}
}
async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
@@ -2313,11 +2534,29 @@ impl BackgroundScanner {
self.update_ignore_statuses().await;
let mut snapshot = self.snapshot.lock();
let mut git_repositories = mem::take(&mut snapshot.git_repositories);
git_repositories.retain(|repo| snapshot.entry_for_path(&repo.git_dir_path).is_some());
git_repositories.retain(|work_directory_id, _| {
snapshot
.entry_for_id(*work_directory_id)
.map_or(false, |entry| {
snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some()
})
});
snapshot.git_repositories = git_repositories;
let mut git_repository_entries = mem::take(&mut snapshot.snapshot.repository_entries);
git_repository_entries.retain(|_, entry| {
snapshot
.git_repositories
.get(&entry.work_directory.0)
.is_some()
});
snapshot.snapshot.repository_entries = git_repository_entries;
snapshot.removed_entry_ids.clear();
snapshot.completed_scan_id = snapshot.scan_id;
drop(snapshot);
self.send_status_update(false, None);
@@ -2602,9 +2841,24 @@ impl BackgroundScanner {
snapshot.insert_entry(fs_entry, self.fs.as_ref());
let scan_id = snapshot.scan_id;
if let Some(repo) = snapshot.repo_with_dot_git_containing(&path) {
repo.repo.lock().reload_index();
repo.scan_id = scan_id;
let repo_with_path_in_dotgit = snapshot.repo_for_metadata(&path);
if let Some((entry_id, repo)) = repo_with_path_in_dotgit {
let work_dir = snapshot
.entry_for_id(entry_id)
.map(|entry| RepositoryWorkDirectory(entry.path.clone()))?;
let repo = repo.lock();
repo.reload_index();
let branch = repo.branch_name();
snapshot.git_repositories.update(&entry_id, |entry| {
entry.scan_id = scan_id;
});
snapshot
.repository_entries
.update(&work_dir, |entry| entry.branch = branch.map(Into::into));
}
if let Some(scan_queue_tx) = &scan_queue_tx {
@@ -3116,7 +3370,6 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
#[cfg(test)]
mod tests {
use super::*;
use fs::repository::FakeGitRepository;
use fs::{FakeFs, RealFs};
use gpui::{executor::Deterministic, TestAppContext};
use pretty_assertions::assert_eq;
@@ -3384,31 +3637,44 @@ mod tests {
assert!(tree.repo_for("c.txt".as_ref()).is_none());
let repo = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap();
assert_eq!(repo.content_path.as_ref(), Path::new("dir1"));
assert_eq!(repo.git_dir_path.as_ref(), Path::new("dir1/.git"));
let entry = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap();
assert_eq!(
entry
.work_directory(tree)
.map(|directory| directory.as_ref().to_owned()),
Some(Path::new("dir1").to_owned())
);
let repo = tree.repo_for("dir1/deps/dep1/src/a.txt".as_ref()).unwrap();
assert_eq!(repo.content_path.as_ref(), Path::new("dir1/deps/dep1"));
assert_eq!(repo.git_dir_path.as_ref(), Path::new("dir1/deps/dep1/.git"),);
let entry = tree.repo_for("dir1/deps/dep1/src/a.txt".as_ref()).unwrap();
assert_eq!(
entry
.work_directory(tree)
.map(|directory| directory.as_ref().to_owned()),
Some(Path::new("dir1/deps/dep1").to_owned())
);
});
let original_scan_id = tree.read_with(cx, |tree, _cx| {
let tree = tree.as_local().unwrap();
tree.repo_for("dir1/src/b.txt".as_ref()).unwrap().scan_id
let repo_update_events = Arc::new(Mutex::new(vec![]));
tree.update(cx, |_, cx| {
let repo_update_events = repo_update_events.clone();
cx.subscribe(&tree, move |_, _, event, _| {
if let Event::UpdatedGitRepositories(update) = event {
repo_update_events.lock().push(update.clone());
}
})
.detach();
});
std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
tree.flush_fs_events(cx).await;
tree.read_with(cx, |tree, _cx| {
let tree = tree.as_local().unwrap();
let new_scan_id = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap().scan_id;
assert_ne!(
original_scan_id, new_scan_id,
"original {original_scan_id}, new {new_scan_id}"
);
});
assert_eq!(
repo_update_events.lock()[0]
.keys()
.cloned()
.collect::<Vec<Arc<Path>>>(),
vec![Path::new("dir1").into()]
);
std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
tree.flush_fs_events(cx).await;
@@ -3420,56 +3686,6 @@ mod tests {
});
}
#[test]
fn test_changed_repos() {
fn fake_entry(git_dir_path: impl AsRef<Path>, scan_id: usize) -> GitRepositoryEntry {
GitRepositoryEntry {
repo: Arc::new(Mutex::new(FakeGitRepository::default())),
scan_id,
content_path: git_dir_path.as_ref().parent().unwrap().into(),
git_dir_path: git_dir_path.as_ref().into(),
}
}
let prev_repos: Vec<GitRepositoryEntry> = vec![
fake_entry("/.git", 0),
fake_entry("/a/.git", 0),
fake_entry("/a/b/.git", 0),
];
let new_repos: Vec<GitRepositoryEntry> = vec![
fake_entry("/a/.git", 1),
fake_entry("/a/b/.git", 0),
fake_entry("/a/c/.git", 0),
];
let res = LocalWorktree::changed_repos(&prev_repos, &new_repos);
// Deletion retained
assert!(res
.iter()
.find(|repo| repo.git_dir_path.as_ref() == Path::new("/.git") && repo.scan_id == 0)
.is_some());
// Update retained
assert!(res
.iter()
.find(|repo| repo.git_dir_path.as_ref() == Path::new("/a/.git") && repo.scan_id == 1)
.is_some());
// Addition retained
assert!(res
.iter()
.find(|repo| repo.git_dir_path.as_ref() == Path::new("/a/c/.git") && repo.scan_id == 0)
.is_some());
// Nochange, not retained
assert!(res
.iter()
.find(|repo| repo.git_dir_path.as_ref() == Path::new("/a/b/.git") && repo.scan_id == 0)
.is_none());
}
#[gpui::test]
async fn test_write_file(cx: &mut TestAppContext) {
let dir = temp_tree(json!({

View File

@@ -196,6 +196,7 @@ impl ProjectPanel {
})
.detach();
let view_id = cx.view_id();
let mut this = Self {
project: project.clone(),
list: Default::default(),
@@ -206,7 +207,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(),
};
@@ -1316,10 +1317,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,11 @@ message Entry {
bool is_ignored = 7;
}
message RepositoryEntry {
uint64 work_directory_id = 1;
optional string branch = 2;
}
message BufferState {
uint64 id = 1;
optional File file = 2;

View File

@@ -5,13 +5,13 @@ 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"));
@@ -503,6 +503,21 @@ pub fn split_worktree_update(
.collect();
done = message.updated_entries.is_empty() && message.removed_entries.is_empty();
// Wait to send repositories until after we've guaranteed that their associated entries
// will be read
let updated_repositories = if done {
mem::take(&mut message.updated_repositories)
} else {
Default::default()
};
let removed_repositories = if done {
mem::take(&mut message.removed_repositories)
} else {
Default::default()
};
Some(UpdateWorktree {
project_id: message.project_id,
worktree_id: message.worktree_id,
@@ -512,6 +527,8 @@ pub fn split_worktree_update(
removed_entries,
scan_id: message.scan_id,
is_last_update: done && 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 = 54;

View File

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

View File

@@ -573,7 +573,13 @@ impl BufferSearchBar {
active_searchable_item.clear_matches(cx);
} else {
let query = if self.regex {
match SearchQuery::regex(query, self.whole_word, self.case_sensitive) {
match SearchQuery::regex(
query,
self.whole_word,
self.case_sensitive,
Vec::new(),
Vec::new(),
) {
Ok(query) => query,
Err(_) => {
self.query_contains_error = true;
@@ -582,7 +588,13 @@ impl BufferSearchBar {
}
}
} else {
SearchQuery::text(query, self.whole_word, self.case_sensitive)
SearchQuery::text(
query,
self.whole_word,
self.case_sensitive,
Vec::new(),
Vec::new(),
)
};
let matches = active_searchable_item.find_matches(query, cx);
@@ -670,13 +682,11 @@ mod tests {
cx,
)
});
let (_, root_view) = cx.add_window(|_| EmptyView);
let (window_id, _root_view) = cx.add_window(|_| EmptyView);
let editor = cx.add_view(&root_view, |cx| {
Editor::for_buffer(buffer.clone(), None, cx)
});
let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx));
let search_bar = cx.add_view(&root_view, |cx| {
let search_bar = cx.add_view(window_id, |cx| {
let mut search_bar = BufferSearchBar::new(cx);
search_bar.set_active_pane_item(Some(&editor), cx);
search_bar.show(false, true, cx);

View File

@@ -22,6 +22,7 @@ use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
borrow::Cow,
collections::HashSet,
mem,
ops::Range,
path::PathBuf,
@@ -34,7 +35,7 @@ use workspace::{
ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
};
actions!(project_search, [SearchInNew, ToggleFocus]);
actions!(project_search, [SearchInNew, ToggleFocus, NextField]);
#[derive(Default)]
struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>);
@@ -48,6 +49,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(ProjectSearchBar::select_prev_match);
cx.add_action(ProjectSearchBar::toggle_focus);
cx.capture_action(ProjectSearchBar::tab);
cx.capture_action(ProjectSearchBar::tab_previous);
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
add_toggle_option_action::<ToggleRegex>(SearchOption::Regex, cx);
@@ -75,6 +77,13 @@ struct ProjectSearch {
search_id: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum InputPanel {
Query,
Exclude,
Include,
}
pub struct ProjectSearchView {
model: ModelHandle<ProjectSearch>,
query_editor: ViewHandle<Editor>,
@@ -82,10 +91,12 @@ pub struct ProjectSearchView {
case_sensitive: bool,
whole_word: bool,
regex: bool,
query_contains_error: bool,
panels_with_errors: HashSet<InputPanel>,
active_match_index: Option<usize>,
search_id: usize,
query_editor_was_focused: bool,
included_files_editor: ViewHandle<Editor>,
excluded_files_editor: ViewHandle<Editor>,
}
pub struct ProjectSearchBar {
@@ -200,7 +211,7 @@ impl View for ProjectSearchView {
.flex(1., true)
})
.on_down(MouseButton::Left, |_, _, cx| {
cx.focus_parent_view();
cx.focus_parent();
})
.into_any_named("project search view")
} else {
@@ -425,7 +436,7 @@ impl ProjectSearchView {
editor.set_text(query_text, cx);
editor
});
// Subcribe to query_editor in order to reraise editor events for workspace item activation purposes
// Subscribe to query_editor in order to reraise editor events for workspace item activation purposes
cx.subscribe(&query_editor, |_, _, event, cx| {
cx.emit(ViewEvent::EditorEvent(event.clone()))
})
@@ -448,6 +459,40 @@ impl ProjectSearchView {
})
.detach();
let included_files_editor = cx.add_view(|cx| {
let mut editor = Editor::single_line(
Some(Arc::new(|theme| {
theme.search.include_exclude_editor.input.clone()
})),
cx,
);
editor.set_placeholder_text("Include: crates/**/*.toml", cx);
editor
});
// Subscribe to include_files_editor in order to reraise editor events for workspace item activation purposes
cx.subscribe(&included_files_editor, |_, _, event, cx| {
cx.emit(ViewEvent::EditorEvent(event.clone()))
})
.detach();
let excluded_files_editor = cx.add_view(|cx| {
let mut editor = Editor::single_line(
Some(Arc::new(|theme| {
theme.search.include_exclude_editor.input.clone()
})),
cx,
);
editor.set_placeholder_text("Exclude: vendor/*, *.lock", cx);
editor
});
// Subscribe to excluded_files_editor in order to reraise editor events for workspace item activation purposes
cx.subscribe(&excluded_files_editor, |_, _, event, cx| {
cx.emit(ViewEvent::EditorEvent(event.clone()))
})
.detach();
let mut this = ProjectSearchView {
search_id: model.read(cx).search_id,
model,
@@ -456,9 +501,11 @@ impl ProjectSearchView {
case_sensitive,
whole_word,
regex,
query_contains_error: false,
panels_with_errors: HashSet::new(),
active_match_index: None,
query_editor_was_focused: false,
included_files_editor,
excluded_files_editor,
};
this.model_changed(cx);
this
@@ -525,11 +572,60 @@ impl ProjectSearchView {
fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
let text = self.query_editor.read(cx).text(cx);
let included_files = match self
.included_files_editor
.read(cx)
.text(cx)
.split(',')
.map(str::trim)
.filter(|glob_str| !glob_str.is_empty())
.map(|glob_str| glob::Pattern::new(glob_str))
.collect::<Result<_, _>>()
{
Ok(included_files) => {
self.panels_with_errors.remove(&InputPanel::Include);
included_files
}
Err(_e) => {
self.panels_with_errors.insert(InputPanel::Include);
cx.notify();
return None;
}
};
let excluded_files = match self
.excluded_files_editor
.read(cx)
.text(cx)
.split(',')
.map(str::trim)
.filter(|glob_str| !glob_str.is_empty())
.map(|glob_str| glob::Pattern::new(glob_str))
.collect::<Result<_, _>>()
{
Ok(excluded_files) => {
self.panels_with_errors.remove(&InputPanel::Exclude);
excluded_files
}
Err(_e) => {
self.panels_with_errors.insert(InputPanel::Exclude);
cx.notify();
return None;
}
};
if self.regex {
match SearchQuery::regex(text, self.whole_word, self.case_sensitive) {
Ok(query) => Some(query),
Err(_) => {
self.query_contains_error = true;
match SearchQuery::regex(
text,
self.whole_word,
self.case_sensitive,
included_files,
excluded_files,
) {
Ok(query) => {
self.panels_with_errors.remove(&InputPanel::Query);
Some(query)
}
Err(_e) => {
self.panels_with_errors.insert(InputPanel::Query);
cx.notify();
None
}
@@ -539,6 +635,8 @@ impl ProjectSearchView {
text,
self.whole_word,
self.case_sensitive,
included_files,
excluded_files,
))
}
}
@@ -723,19 +821,50 @@ impl ProjectSearchBar {
}
fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
if let Some(search_view) = self.active_project_search.as_ref() {
search_view.update(cx, |search_view, cx| {
if search_view.query_editor.is_focused(cx) {
if !search_view.model.read(cx).match_ranges.is_empty() {
search_view.focus_results_editor(cx);
}
} else {
self.cycle_field(Direction::Next, cx);
}
fn tab_previous(&mut self, _: &editor::TabPrev, cx: &mut ViewContext<Self>) {
self.cycle_field(Direction::Prev, cx);
}
fn cycle_field(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
let active_project_search = match &self.active_project_search {
Some(active_project_search) => active_project_search,
None => {
cx.propagate_action();
return;
}
};
active_project_search.update(cx, |project_view, cx| {
let views = &[
&project_view.query_editor,
&project_view.included_files_editor,
&project_view.excluded_files_editor,
];
let current_index = match views
.iter()
.enumerate()
.find(|(_, view)| view.is_focused(cx))
{
Some((index, _)) => index,
None => {
cx.propagate_action();
return;
}
});
} else {
cx.propagate_action();
}
};
let new_index = match direction {
Direction::Next => (current_index + 1) % views.len(),
Direction::Prev if current_index == 0 => views.len() - 1,
Direction::Prev => (current_index - 1) % views.len(),
};
cx.focus(views[new_index]);
});
}
fn toggle_search_option(&mut self, option: SearchOption, cx: &mut ViewContext<Self>) -> bool {
@@ -864,59 +993,121 @@ impl View for ProjectSearchBar {
if let Some(search) = self.active_project_search.as_ref() {
let search = search.read(cx);
let theme = cx.global::<Settings>().theme.clone();
let editor_container = if search.query_contains_error {
let query_container_style = if search.panels_with_errors.contains(&InputPanel::Query) {
theme.search.invalid_editor
} else {
theme.search.editor.input.container
};
Flex::row()
let include_container_style =
if search.panels_with_errors.contains(&InputPanel::Include) {
theme.search.invalid_include_exclude_editor
} else {
theme.search.include_exclude_editor.input.container
};
let exclude_container_style =
if search.panels_with_errors.contains(&InputPanel::Exclude) {
theme.search.invalid_include_exclude_editor
} else {
theme.search.include_exclude_editor.input.container
};
let included_files_view = ChildView::new(&search.included_files_editor, cx)
.aligned()
.left()
.flex(1.0, true);
let excluded_files_view = ChildView::new(&search.excluded_files_editor, cx)
.aligned()
.right()
.flex(1.0, true);
let row_spacing = theme.workspace.toolbar.container.padding.bottom;
Flex::column()
.with_child(
Flex::row()
.with_child(
ChildView::new(&search.query_editor, cx)
Flex::row()
.with_child(
ChildView::new(&search.query_editor, cx)
.aligned()
.left()
.flex(1., true),
)
.with_children(search.active_match_index.map(|match_ix| {
Label::new(
format!(
"{}/{}",
match_ix + 1,
search.model.read(cx).match_ranges.len()
),
theme.search.match_index.text.clone(),
)
.contained()
.with_style(theme.search.match_index.container)
.aligned()
}))
.contained()
.with_style(query_container_style)
.aligned()
.left()
.flex(1., true),
.constrained()
.with_min_width(theme.search.editor.min_width)
.with_max_width(theme.search.editor.max_width)
.flex(1., false),
)
.with_child(
Flex::row()
.with_child(self.render_nav_button("<", Direction::Prev, cx))
.with_child(self.render_nav_button(">", Direction::Next, cx))
.aligned(),
)
.with_child(
Flex::row()
.with_child(self.render_option_button(
"Case",
SearchOption::CaseSensitive,
cx,
))
.with_child(self.render_option_button(
"Word",
SearchOption::WholeWord,
cx,
))
.with_child(self.render_option_button(
"Regex",
SearchOption::Regex,
cx,
))
.contained()
.with_style(theme.search.option_button_group)
.aligned(),
)
.with_children(search.active_match_index.map(|match_ix| {
Label::new(
format!(
"{}/{}",
match_ix + 1,
search.model.read(cx).match_ranges.len()
),
theme.search.match_index.text.clone(),
)
.contained()
.with_style(theme.search.match_index.container)
.aligned()
}))
.contained()
.with_style(editor_container)
.aligned()
.constrained()
.with_min_width(theme.search.editor.min_width)
.with_max_width(theme.search.editor.max_width)
.flex(1., false),
.with_margin_bottom(row_spacing),
)
.with_child(
Flex::row()
.with_child(self.render_nav_button("<", Direction::Prev, cx))
.with_child(self.render_nav_button(">", Direction::Next, cx))
.aligned(),
)
.with_child(
Flex::row()
.with_child(self.render_option_button(
"Case",
SearchOption::CaseSensitive,
cx,
))
.with_child(self.render_option_button("Word", SearchOption::WholeWord, cx))
.with_child(self.render_option_button("Regex", SearchOption::Regex, cx))
.contained()
.with_style(theme.search.option_button_group)
.aligned(),
.with_child(
Flex::row()
.with_child(included_files_view)
.contained()
.with_style(include_container_style)
.aligned()
.constrained()
.with_min_width(theme.search.include_exclude_editor.min_width)
.with_max_width(theme.search.include_exclude_editor.max_width)
.flex(1., false),
)
.with_child(
Flex::row()
.with_child(excluded_files_view)
.contained()
.with_style(exclude_container_style)
.aligned()
.constrained()
.with_min_width(theme.search.include_exclude_editor.min_width)
.with_max_width(theme.search.include_exclude_editor.max_width)
.flex(1., false),
),
)
.contained()
.with_style(theme.search.container)
@@ -939,8 +1130,6 @@ impl ToolbarItemView for ProjectSearchBar {
self.subscription = None;
self.active_project_search = None;
if let Some(search) = active_pane_item.and_then(|i| i.downcast::<ProjectSearchView>()) {
let query_editor = search.read(cx).query_editor.clone();
cx.reparent(&query_editor);
self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify()));
self.active_project_search = Some(search);
ToolbarItemLocation::PrimaryLeft {
@@ -950,6 +1139,10 @@ impl ToolbarItemView for ProjectSearchBar {
ToolbarItemLocation::Hidden
}
}
fn row_count(&self) -> usize {
2
}
}
#[cfg(test)]

View File

@@ -31,7 +31,6 @@ schemars = "0.8"
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
serde_path_to_error = "0.1.4"
toml = "0.5"
tree-sitter = "*"
tree-sitter-json = "*"

View File

@@ -173,6 +173,7 @@ pub struct EditorSettings {
pub formatter: Option<Formatter>,
pub enable_language_server: Option<bool>,
pub show_copilot_suggestions: Option<bool>,
pub show_whitespaces: Option<ShowWhitespaces>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -446,6 +447,15 @@ pub struct FeaturesContent {
pub copilot: Option<bool>,
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ShowWhitespaces {
#[default]
Selection,
None,
All,
}
impl Settings {
pub fn initial_user_settings_content(assets: &'static impl AssetSource) -> Cow<'static, str> {
match assets.load(INITIAL_USER_SETTINGS_ASSET_PATH).unwrap() {
@@ -507,6 +517,7 @@ impl Settings {
formatter: required(defaults.editor.formatter),
enable_language_server: required(defaults.editor.enable_language_server),
show_copilot_suggestions: required(defaults.editor.show_copilot_suggestions),
show_whitespaces: required(defaults.editor.show_whitespaces),
},
editor_overrides: Default::default(),
copilot: CopilotSettings {
@@ -657,6 +668,10 @@ impl Settings {
self.language_setting(language, |settings| settings.tab_size)
}
pub fn show_whitespaces(&self, language: Option<&str>) -> ShowWhitespaces {
self.language_setting(language, |settings| settings.show_whitespaces)
}
pub fn hard_tabs(&self, language: Option<&str>) -> bool {
self.language_setting(language, |settings| settings.hard_tabs)
}
@@ -793,6 +808,7 @@ impl Settings {
formatter: Some(Formatter::LanguageServer),
enable_language_server: Some(true),
show_copilot_suggestions: Some(true),
show_whitespaces: Some(ShowWhitespaces::None),
},
editor_overrides: Default::default(),
copilot: Default::default(),

View File

@@ -84,7 +84,7 @@ mod tests {
watch_files, watched_json::watch_settings_file, EditorSettings, Settings, SoftWrap,
};
use fs::FakeFs;
use gpui::{actions, elements::*, Action, Entity, View, ViewContext, WindowContext};
use gpui::{actions, elements::*, Action, Entity, TestAppContext, View, ViewContext};
use theme::ThemeRegistry;
struct TestView;
@@ -171,13 +171,12 @@ mod tests {
let (window_id, _view) = cx.add_window(|_| TestView);
// Test loading the keymap base at all
cx.read_window(window_id, |cx| {
assert_key_bindings_for(
cx,
vec![("backspace", &A), ("k", &ActivatePreviousPane)],
line!(),
);
});
assert_key_bindings_for(
window_id,
cx,
vec![("backspace", &A), ("k", &ActivatePreviousPane)],
line!(),
);
// Test modifying the users keymap, while retaining the base keymap
fs.save(
@@ -199,13 +198,12 @@ mod tests {
cx.foreground().run_until_parked();
cx.read_window(window_id, |cx| {
assert_key_bindings_for(
cx,
vec![("backspace", &B), ("k", &ActivatePreviousPane)],
line!(),
);
});
assert_key_bindings_for(
window_id,
cx,
vec![("backspace", &B), ("k", &ActivatePreviousPane)],
line!(),
);
// Test modifying the base, while retaining the users keymap
fs.save(
@@ -223,31 +221,33 @@ mod tests {
cx.foreground().run_until_parked();
cx.read_window(window_id, |cx| {
assert_key_bindings_for(
cx,
vec![("backspace", &B), ("[", &ActivatePrevItem)],
line!(),
);
});
assert_key_bindings_for(
window_id,
cx,
vec![("backspace", &B), ("[", &ActivatePrevItem)],
line!(),
);
}
fn assert_key_bindings_for<'a>(
cx: &WindowContext,
window_id: usize,
cx: &TestAppContext,
actions: Vec<(&'static str, &'a dyn Action)>,
line: u32,
) {
for (key, action) in actions {
// assert that...
assert!(
cx.available_actions(0).any(|(_, bound_action, b)| {
// action names match...
bound_action.name() == action.name()
cx.available_actions(window_id, 0)
.into_iter()
.any(|(_, bound_action, b)| {
// action names match...
bound_action.name() == action.name()
&& bound_action.namespace() == action.namespace()
// and key strokes contain the given key
&& b.iter()
.any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
}),
}),
"On {} Failed to find {} with key binding {}",
line,
action.name(),

View File

@@ -2,13 +2,13 @@ use std::{cmp::Ordering, fmt::Debug};
use crate::{Bias, Dimension, Item, KeyedItem, SeekTarget, SumTree, Summary};
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct TreeMap<K, V>(SumTree<MapEntry<K, V>>)
where
K: Clone + Debug + Default + Ord,
V: Clone + Debug;
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct MapEntry<K, V> {
key: K,
value: V,
@@ -73,9 +73,58 @@ impl<K: Clone + Debug + Default + Ord, V: Clone + Debug> TreeMap<K, V> {
removed
}
/// Returns the key-value pair with the greatest key less than or equal to the given key.
pub fn closest(&self, key: &K) -> Option<(&K, &V)> {
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>();
let key = MapKeyRef(Some(key));
cursor.seek(&key, Bias::Right, &());
cursor.prev(&());
cursor.item().map(|item| (&item.key, &item.value))
}
pub fn update<F, T>(&mut self, key: &K, f: F) -> Option<T>
where
F: FnOnce(&mut V) -> T,
{
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>();
let key = MapKeyRef(Some(key));
let mut new_tree = cursor.slice(&key, Bias::Left, &());
let mut result = None;
if key.cmp(&cursor.end(&()), &()) == Ordering::Equal {
let mut updated = cursor.item().unwrap().clone();
result = Some(f(&mut updated.value));
new_tree.push(updated, &());
cursor.next(&());
}
new_tree.push_tree(cursor.suffix(&()), &());
drop(cursor);
self.0 = new_tree;
result
}
pub fn retain<F: FnMut(&K, &V) -> bool>(&mut self, mut predicate: F) {
let mut new_map = SumTree::<MapEntry<K, V>>::default();
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>();
cursor.next(&());
while let Some(item) = cursor.item() {
if predicate(&item.key, &item.value) {
new_map.push(item.clone(), &());
}
cursor.next(&());
}
drop(cursor);
self.0 = new_map;
}
pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> + '_ {
self.0.iter().map(|entry| (&entry.key, &entry.value))
}
pub fn values(&self) -> impl Iterator<Item = &V> + '_ {
self.0.iter().map(|entry| &entry.value)
}
}
impl<K, V> Default for TreeMap<K, V>
@@ -199,10 +248,16 @@ mod tests {
vec![(&1, &"a"), (&2, &"b"), (&3, &"c")]
);
assert_eq!(map.closest(&0), None);
assert_eq!(map.closest(&1), Some((&1, &"a")));
assert_eq!(map.closest(&10), Some((&3, &"c")));
map.remove(&2);
assert_eq!(map.get(&2), None);
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&1, &"a"), (&3, &"c")]);
assert_eq!(map.closest(&2), Some((&1, &"a")));
map.remove(&3);
assert_eq!(map.get(&3), None);
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&1, &"a")]);
@@ -210,5 +265,11 @@ mod tests {
map.remove(&1);
assert_eq!(map.get(&1), None);
assert_eq!(map.iter().collect::<Vec<_>>(), vec![]);
map.insert(4, "d");
map.insert(5, "e");
map.insert(6, "f");
map.retain(|key, _| *key % 2 == 0);
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&4, &"d"), (&6, &"f")]);
}
}

View File

@@ -107,11 +107,12 @@ impl View for TerminalButton {
impl TerminalButton {
pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
let button_view_id = cx.view_id();
cx.observe(&workspace, |_, _, cx| cx.notify()).detach();
Self {
workspace: workspace.downgrade(),
popup_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
}),

View File

@@ -10,8 +10,8 @@ use gpui::{
platform::{CursorStyle, MouseButton},
serde_json::json,
text_layout::{Line, RunStyle},
AnyElement, Element, EventContext, FontCache, ModelContext, MouseRegion, Quad, SceneBuilder,
SizeConstraint, TextLayoutCache, ViewContext, WeakModelHandle,
AnyElement, Element, EventContext, FontCache, LayoutContext, ModelContext, MouseRegion, Quad,
SceneBuilder, SizeConstraint, TextLayoutCache, ViewContext, WeakModelHandle,
};
use itertools::Itertools;
use language::CursorShape;
@@ -370,7 +370,7 @@ impl TerminalElement {
f: impl Fn(&mut Terminal, Vector2F, E, &mut ModelContext<Terminal>),
) -> impl Fn(E, &mut TerminalView, &mut EventContext<TerminalView>) {
move |event, _: &mut TerminalView, cx| {
cx.focus_parent_view();
cx.focus_parent();
if let Some(conn_handle) = connection.upgrade(cx) {
conn_handle.update(cx, |terminal, cx| {
f(terminal, origin, event, cx);
@@ -408,7 +408,7 @@ impl TerminalElement {
)
// Update drag selections
.on_drag(MouseButton::Left, move |event, _: &mut TerminalView, cx| {
if cx.is_parent_view_focused() {
if cx.is_self_focused() {
if let Some(conn_handle) = connection.upgrade(cx) {
conn_handle.update(cx, |terminal, cx| {
terminal.mouse_drag(event, origin);
@@ -444,7 +444,7 @@ impl TerminalElement {
},
)
.on_move(move |event, _: &mut TerminalView, cx| {
if cx.is_parent_view_focused() {
if cx.is_self_focused() {
if let Some(conn_handle) = connection.upgrade(cx) {
conn_handle.update(cx, |terminal, cx| {
terminal.mouse_move(&event, origin);
@@ -561,7 +561,7 @@ impl Element<TerminalView> for TerminalElement {
&mut self,
constraint: gpui::SizeConstraint,
view: &mut TerminalView,
cx: &mut ViewContext<TerminalView>,
cx: &mut LayoutContext<TerminalView>,
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
let settings = cx.global::<Settings>();
let font_cache = cx.font_cache();

View File

@@ -2,7 +2,6 @@ mod persistence;
pub mod terminal_button;
pub mod terminal_element;
use anyhow::anyhow;
use context_menu::{ContextMenu, ContextMenuItem};
use dirs::home_dir;
use gpui::{
@@ -125,6 +124,7 @@ impl TerminalView {
workspace_id: WorkspaceId,
cx: &mut ViewContext<Self>,
) -> Self {
let view_id = cx.view_id();
cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
cx.subscribe(&terminal, |this, _, event, cx| match event {
Event::Wakeup => {
@@ -163,7 +163,7 @@ impl TerminalView {
terminal,
has_new_content: true,
has_bell: false,
context_menu: cx.add_view(ContextMenu::new),
context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
blink_state: true,
blinking_on: false,
blinking_paused: false,
@@ -446,11 +446,11 @@ impl View for TerminalView {
});
}
fn keymap_context(&self, cx: &gpui::AppContext) -> KeymapContext {
let mut context = Self::default_keymap_context();
fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &gpui::AppContext) {
Self::reset_to_default_keymap_context(keymap);
let mode = self.terminal.read(cx).last_content.mode;
context.add_key(
keymap.add_key(
"screen",
if mode.contains(TermMode::ALT_SCREEN) {
"alt"
@@ -460,40 +460,40 @@ impl View for TerminalView {
);
if mode.contains(TermMode::APP_CURSOR) {
context.add_identifier("DECCKM");
keymap.add_identifier("DECCKM");
}
if mode.contains(TermMode::APP_KEYPAD) {
context.add_identifier("DECPAM");
keymap.add_identifier("DECPAM");
} else {
context.add_identifier("DECPNM");
keymap.add_identifier("DECPNM");
}
if mode.contains(TermMode::SHOW_CURSOR) {
context.add_identifier("DECTCEM");
keymap.add_identifier("DECTCEM");
}
if mode.contains(TermMode::LINE_WRAP) {
context.add_identifier("DECAWM");
keymap.add_identifier("DECAWM");
}
if mode.contains(TermMode::ORIGIN) {
context.add_identifier("DECOM");
keymap.add_identifier("DECOM");
}
if mode.contains(TermMode::INSERT) {
context.add_identifier("IRM");
keymap.add_identifier("IRM");
}
//LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
context.add_identifier("LNM");
keymap.add_identifier("LNM");
}
if mode.contains(TermMode::FOCUS_IN_OUT) {
context.add_identifier("report_focus");
keymap.add_identifier("report_focus");
}
if mode.contains(TermMode::ALTERNATE_SCROLL) {
context.add_identifier("alternate_scroll");
keymap.add_identifier("alternate_scroll");
}
if mode.contains(TermMode::BRACKETED_PASTE) {
context.add_identifier("bracketed_paste");
keymap.add_identifier("bracketed_paste");
}
if mode.intersects(TermMode::MOUSE_MODE) {
context.add_identifier("any_mouse_reporting");
keymap.add_identifier("any_mouse_reporting");
}
{
let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
@@ -505,7 +505,7 @@ impl View for TerminalView {
} else {
"off"
};
context.add_key("mouse_reporting", mouse_reporting);
keymap.add_key("mouse_reporting", mouse_reporting);
}
{
let format = if mode.contains(TermMode::SGR_MOUSE) {
@@ -515,9 +515,8 @@ impl View for TerminalView {
} else {
"normal"
};
context.add_key("mouse_format", format);
keymap.add_key("mouse_format", format);
}
context
}
}
@@ -628,16 +627,12 @@ impl Item for TerminalView {
})
});
let pane = pane
.upgrade(&cx)
.ok_or_else(|| anyhow!("pane was dropped"))?;
cx.update(|cx| {
let terminal = project.update(cx, |project, cx| {
project.create_terminal(cwd, window_id, cx)
})?;
Ok(cx.add_view(&pane, |cx| TerminalView::new(terminal, workspace_id, cx)))
})
let terminal = project.update(&mut cx, |project, cx| {
project.create_terminal(cwd, window_id, cx)
})?;
Ok(pane.update(&mut cx, |_, cx| {
cx.add_view(|cx| TerminalView::new(terminal, workspace_id, cx))
})?)
})
}

View File

@@ -90,8 +90,7 @@ impl HistoryEntry {
}
struct History {
// TODO: Turn this into a String or Rope, maybe.
base_text: Arc<str>,
base_text: Rope,
operations: TreeMap<clock::Local, Operation>,
insertion_slices: HashMap<clock::Local, Vec<InsertionSlice>>,
undo_stack: Vec<HistoryEntry>,
@@ -107,7 +106,7 @@ struct InsertionSlice {
}
impl History {
pub fn new(base_text: Arc<str>) -> Self {
pub fn new(base_text: Rope) -> Self {
Self {
base_text,
operations: Default::default(),
@@ -470,7 +469,7 @@ impl Buffer {
let line_ending = LineEnding::detect(&base_text);
LineEnding::normalize(&mut base_text);
let history = History::new(base_text.into());
let history = History::new(Rope::from(base_text.as_ref()));
let mut fragments = SumTree::new();
let mut insertions = SumTree::new();
@@ -478,7 +477,7 @@ impl Buffer {
let mut lamport_clock = clock::Lamport::new(replica_id);
let mut version = clock::Global::new();
let visible_text = Rope::from(history.base_text.as_ref());
let visible_text = history.base_text.clone();
if !visible_text.is_empty() {
let insertion_timestamp = InsertionTimestamp {
replica_id: 0,
@@ -1165,7 +1164,7 @@ impl Buffer {
self.history.group_until(transaction_id);
}
pub fn base_text(&self) -> &Arc<str> {
pub fn base_text(&self) -> &Rope {
&self.history.base_text
}

View File

@@ -16,5 +16,4 @@ parking_lot.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
serde_path_to_error = "0.1.4"
toml = "0.5"

View File

@@ -93,6 +93,7 @@ pub struct Titlebar {
pub container: ContainerStyle,
pub height: f32,
pub title: TextStyle,
pub highlight_color: Color,
pub item_spacing: f32,
pub face_pile_spacing: f32,
pub avatar_ribbon: AvatarRibbon,
@@ -318,6 +319,9 @@ pub struct Search {
pub editor: FindEditor,
pub invalid_editor: ContainerStyle,
pub option_button_group: ContainerStyle,
pub include_exclude_editor: FindEditor,
pub invalid_include_exclude_editor: ContainerStyle,
pub include_exclude_inputs: ContainedText,
pub option_button: Interactive<ContainedText>,
pub match_background: Color,
pub match_index: ContainedText,
@@ -636,6 +640,7 @@ pub struct Editor {
pub composition_mark: HighlightStyle,
pub jump_icon: Interactive<IconButton>,
pub scrollbar: Scrollbar,
pub whitespace: Color,
}
#[derive(Clone, Deserialize, Default)]

View File

@@ -40,14 +40,8 @@ pub trait HttpClient: Send + Sync {
&'a self,
uri: &str,
body: AsyncBody,
follow_redirects: bool,
) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
let request = isahc::Request::builder()
.redirect_policy(if follow_redirects {
RedirectPolicy::Follow
} else {
RedirectPolicy::None
})
.method(Method::POST)
.uri(uri)
.header("Content-Type", "application/json")

View File

@@ -35,9 +35,7 @@ fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
}
}
cx.update_window(editor.window_id(), |cx| {
editor.update(cx, |editor, cx| Vim::unhook_vim_settings(editor, cx))
});
editor.update(cx, |editor, cx| Vim::unhook_vim_settings(editor, cx))
});
});
}

View File

@@ -84,7 +84,12 @@ pub fn init(cx: &mut AppContext) {
Vim::active_editor_input_ignored("\n".into(), cx)
});
// Any time settings change, update vim mode to match.
// Any time settings change, update vim mode to match. The Vim struct
// will be initialized as disabled by default, so we filter its commands
// out when starting up.
cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
filter.filtered_namespaces.insert("vim");
});
cx.update_default_global(|vim: &mut Vim, cx: &mut AppContext| {
vim.set_enabled(cx.global::<Settings>().vim_mode, cx)
});
@@ -309,7 +314,7 @@ impl Vim {
editor.set_input_enabled(!state.vim_controlled());
editor.selections.line_mode = matches!(state.mode, Mode::Visual { line: true });
let context_layer = state.keymap_context_layer();
editor.set_keymap_context_layer::<Self>(context_layer);
editor.set_keymap_context_layer::<Self>(context_layer, cx);
} else {
Self::unhook_vim_settings(editor, cx);
}
@@ -321,7 +326,7 @@ impl Vim {
editor.set_clip_at_line_ends(false, cx);
editor.set_input_enabled(true);
editor.selections.line_mode = false;
editor.remove_keymap_context_layer::<Self>();
editor.remove_keymap_context_layer::<Self>(cx);
}
}

View File

@@ -178,11 +178,7 @@ impl Dock {
pane.update(cx, |pane, cx| {
pane.set_active(false, cx);
});
let pane_id = pane.id();
cx.subscribe(&pane, move |workspace, _, event, cx| {
workspace.handle_pane_event(pane_id, event, cx);
})
.detach();
cx.subscribe(&pane, Workspace::handle_pane_event).detach();
Self {
pane,
@@ -730,7 +726,7 @@ mod tests {
self.update_workspace(|workspace, cx| Dock::move_dock(workspace, anchor, true, cx));
}
pub fn hide_dock(&self) {
pub fn hide_dock(&mut self) {
self.cx.dispatch_action(self.window_id, HideDock);
}

View File

@@ -60,7 +60,7 @@ pub trait Item: View {
style: &theme::Tab,
cx: &AppContext,
) -> AnyElement<V>;
fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item)) {}
fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item)) {} // (model id, Item)
fn is_singleton(&self, _cx: &AppContext) -> bool {
false
}

View File

@@ -24,8 +24,8 @@ use gpui::{
keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton, NavigationDirection, PromptLevel},
Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
ModelHandle, MouseRegion, Quad, Task, View, ViewContext, ViewHandle, WeakViewHandle,
WindowContext,
LayoutContext, ModelHandle, MouseRegion, Quad, Task, View, ViewContext, ViewHandle,
WeakViewHandle, WindowContext,
};
use project::{Project, ProjectEntryId, ProjectPath};
use serde::Deserialize;
@@ -134,6 +134,7 @@ pub enum Event {
RemoveItem { item_id: usize },
Split(SplitDirection),
ChangeItemTitle,
Focus,
}
pub struct Pane {
@@ -150,6 +151,7 @@ pub struct Pane {
docked: Option<DockAnchor>,
_background_actions: BackgroundActions,
workspace: WeakViewHandle<Workspace>,
has_focus: bool,
}
pub struct ItemNavHistory {
@@ -226,8 +228,9 @@ impl Pane {
background_actions: BackgroundActions,
cx: &mut ViewContext<Self>,
) -> Self {
let pane_view_id = cx.view_id();
let handle = cx.weak_handle();
let context_menu = cx.add_view(ContextMenu::new);
let context_menu = cx.add_view(|cx| ContextMenu::new(pane_view_id, cx));
context_menu.update(cx, |menu, _| {
menu.set_position_mode(OverlayPositionMode::Local)
});
@@ -252,10 +255,11 @@ impl Pane {
kind: TabBarContextMenuKind::New,
handle: context_menu,
},
tab_context_menu: cx.add_view(ContextMenu::new),
tab_context_menu: cx.add_view(|cx| ContextMenu::new(pane_view_id, cx)),
docked,
_background_actions: background_actions,
workspace,
has_focus: false,
}
}
@@ -272,6 +276,10 @@ impl Pane {
cx.notify();
}
pub fn has_focus(&self) -> bool {
self.has_focus
}
pub fn set_docked(&mut self, docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) {
self.docked = docked;
cx.notify();
@@ -537,7 +545,6 @@ impl Pane {
// If the item already exists, move it to the desired destination and activate it
pane.update(cx, |pane, cx| {
if existing_item_index != insertion_index {
cx.reparent(item.as_any());
let existing_item_is_active = existing_item_index == pane.active_item_index;
// If the caller didn't specify a destination and the added item is already
@@ -567,7 +574,6 @@ impl Pane {
});
} else {
pane.update(cx, |pane, cx| {
cx.reparent(item.as_any());
pane.items.insert(insertion_index, item);
if insertion_index <= pane.active_item_index {
pane.active_item_index += 1;
@@ -1764,7 +1770,7 @@ impl View for Pane {
self.render_blank_pane(&theme, cx)
})
.on_down(MouseButton::Left, |_, _, cx| {
cx.focus_parent_view();
cx.focus_parent();
})
.into_any()
}
@@ -1798,6 +1804,7 @@ impl View for Pane {
}
fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
self.has_focus = true;
self.toolbar.update(cx, |toolbar, cx| {
toolbar.pane_focus_update(true, cx);
});
@@ -1823,20 +1830,22 @@ impl View for Pane {
.insert(active_item.id(), focused.downgrade());
}
}
cx.emit(Event::Focus);
}
fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
self.has_focus = false;
self.toolbar.update(cx, |toolbar, cx| {
toolbar.pane_focus_update(false, cx);
});
}
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut keymap = Self::default_keymap_context();
fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
Self::reset_to_default_keymap_context(keymap);
if self.docked.is_some() {
keymap.add_identifier("docked");
}
keymap
}
}
@@ -1999,7 +2008,7 @@ impl<V: View> Element<V> for PaneBackdrop<V> {
&mut self,
constraint: gpui::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

@@ -147,7 +147,7 @@ impl SerializedPaneGroup {
} else {
let pane = pane.upgrade(cx)?;
workspace
.update(cx, |workspace, cx| workspace.remove_pane(pane, cx))
.update(cx, |workspace, cx| workspace.force_remove_pane(&pane, cx))
.log_err()?;
None
}
@@ -197,7 +197,7 @@ impl SerializedPane {
let pane_handle = pane_handle
.upgrade(cx)
.ok_or_else(|| anyhow!("pane was dropped"))?;
Pane::add_item(workspace, &pane_handle, item_handle, false, false, None, cx);
Pane::add_item(workspace, &pane_handle, item_handle, true, true, None, cx);
anyhow::Ok(())
})??;
}

View File

@@ -90,7 +90,7 @@ impl View for SharedScreen {
.contained()
.with_style(cx.global::<Settings>().theme.shared_screen)
})
.on_down(MouseButton::Left, |_, _, cx| cx.focus_parent_view())
.on_down(MouseButton::Left, |_, _, cx| cx.focus_parent())
.into_any()
}
}

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