Compare commits

...

99 Commits

Author SHA1 Message Date
Max Brunsfeld
d91ac96f1b v0.66.x preview 2022-11-23 09:55:57 -08:00
Max Brunsfeld
b3a92979a3 Merge pull request #1911 from zed-industries/single-file-worktree-event-extension
Fix file extension retrieval for single-file worktrees
2022-11-21 12:41:35 -08:00
Max Brunsfeld
55d3c09b6b Fix file extension retrieval for single-file worktrees
Previously, we used the file's 'path' method, which only returns the relative
path from the worktree root.
2022-11-21 12:34:36 -08:00
Kay Simmons
0078bea877 change bump-version to install jq if its not already installed 2022-11-18 13:42:46 -08:00
Kay Simmons
bb80cee19e Merge pull request #1814 from zed-industries/golden-ratio
Active Pane Magnification
2022-11-18 13:14:03 -08:00
Kay Simmons
0c50c0959d Merge pull request #1906 from zed-industries/mouse-down-capture-on-click-fix
Fix mouse down falling through popovers
2022-11-18 13:10:50 -08:00
Kay Simmons
75b8a12ab3 address issue where mouse down events weren't getting captured after the multiple handlers change 2022-11-18 13:04:27 -08:00
Max Brunsfeld
d090d230e2 Merge pull request #1903 from zed-industries/override-pyright-completion-sorting
Add LspAdapter hook for processing completions, fix completion sorting from Pyright
2022-11-17 15:30:07 -08:00
Max Brunsfeld
bca635e5d3 Add LspAdapter hook for processing completions, fix completion sorting from Pyright 2022-11-17 15:26:46 -08:00
Julia
3938adf60a Merge pull request #1902 from zed-industries/event-handlers-are-multitude
Allow having multiple mouse event handlers of the same kind
2022-11-17 17:19:32 -05:00
Julia
6537def97e Allow having multiple mouse event handlers of the same kind
Co-Authored-By: Kay Simmons <kay@zed.dev>
2022-11-17 17:01:34 -05:00
Max Brunsfeld
5020c70a04 collab 0.2.4 2022-11-17 11:44:29 -08:00
Mikayla Maki
0a63d2e3e1 Merge pull request #1900 from zed-industries/fix-terminal-performance
Check for wakeups correctly
2022-11-17 11:17:13 -08:00
Mikayla Maki
ce0dfde8ee Check for wakeups correctly 2022-11-17 11:14:31 -08:00
Joseph Lyons
93824dd239 Fix top-level header in discord webhook action 2022-11-16 20:02:15 -05:00
Joseph Lyons
c613c98e37 Move comment to correct location 2022-11-16 17:28:50 -05:00
Max Brunsfeld
4e4299d500 v0.66.x dev 2022-11-16 14:22:18 -08:00
Mikayla Maki
ccc8c247a1 Merge pull request #1894 from zed-industries/opt-as-meta-fix
Fix small terminal bugs
2022-11-16 10:50:02 -08:00
Mikayla Maki
8e6c5dbc3b Fix unscaled scrolling when using an imprecise mouse wheel 2022-11-16 10:44:13 -08:00
Mikayla Maki
3c53fcdb43 Added alt-left: move word left and alt-right: move word right in the terminal for for antonio 2022-11-16 09:59:23 -08:00
Joseph T. Lyons
17dfbb91ba Merge pull request #1897 from zed-industries/allow-users-to-sign-up-multiple-times 2022-11-15 20:13:43 -05:00
Joseph Lyons
c3cf056fc5 allow users to sign up multiple times without throwing a 500 2022-11-15 20:04:56 -05:00
Nathan Sobo
275f0ae492 collab 0.2.3 2022-11-15 15:45:04 -07:00
Nathan Sobo
f4e9759f26 Merge pull request #1896 from zed-industries/fix-invites
Once we email someone an invite, honor the invitation
2022-11-15 15:43:30 -07:00
Nathan Sobo
fdf758e050 Once we email someone an invite, honor the invitation
Previously, we were waiting to decrement the invite_count until a user
confirmed their email address, which created weird situations where we would
email people only to have them get a 500 when trying to sign up. Now, we
decrement the invite_count upon sending the email and always honor the
invitation.

Co-Authored-By: Joseph Lyons <joseph@zed.dev>
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2022-11-15 15:36:59 -07:00
Max Brunsfeld
0dfacd7ffa Merge pull request #1895 from zed-industries/ruby-solargraph
Add ruby LSP support via SolarGraph
2022-11-15 12:45:54 -08:00
Max Brunsfeld
36c07f940c Add ruby LSP support via SolarGraph 2022-11-15 12:34:43 -08:00
Mikayla Maki
01929037f1 fixed clear problem 2022-11-15 12:02:09 -08:00
Max Brunsfeld
e401caff7c Merge pull request #1863 from zed-industries/erb
Add support for ERB
2022-11-14 16:59:51 -08:00
Max Brunsfeld
b222e8eb5a Use a longer example text in random combined injections test 2022-11-14 16:56:21 -08:00
Max Brunsfeld
fb35631337 Bump tree-sitter after merging included-ranges PR 2022-11-14 16:56:09 -08:00
Max Brunsfeld
6659dac2e5 Fix compile errors in seed script, ensure it is compiled on CI
Co-authored-by: Nate Butler <nate@zed.dev>
2022-11-14 11:12:25 -08:00
Mikayla Maki
0dcdd6ea39 Merge pull request #1889 from zed-industries/terminal-bugs
Refactored rendering to squash all wakeups into 1
2022-11-14 10:29:00 -08:00
Mikayla Maki
a66aa9c09c Refactored rendering to squash all wakeups into 1 2022-11-14 10:20:55 -08:00
Kay Simmons
e6c5079a49 Merge pull request #1873 from zed-industries/drag-project-entry-to-pane
Drag and Drop Project Entries Between Folders
2022-11-14 09:55:56 -08:00
Max Brunsfeld
ee66adbb49 SyntaxMap - Don't ignore deletions at the boundaries of layers 2022-11-11 16:43:57 -08:00
Max Brunsfeld
3612c46d6d Bump tree-sitter for included range bugfix 2022-11-11 16:36:04 -08:00
Julia
bf9c9b0103 Merge pull request #1875 from zed-industries/fix-code-actions-regression
Use `EMPTY` code action kind to get more RA actions without breaking TS
2022-11-11 15:34:40 -05:00
Julia
ea8778921b Use EMPTY code action kind to get more RA actions without breaking TS 2022-11-11 15:26:12 -05:00
Julia
2ef2b5a053 Merge pull request #1874 from zed-industries/propagate-mouse-up-through-drop-receiver
Propagate mouse up event through drop receiver in early return
2022-11-11 14:05:07 -05:00
Julia
5bb7701de7 Propagate mouse up event through drop receiver in early return 2022-11-11 14:00:01 -05:00
Julia
b6f78cd5dc Merge pull request #1871 from zed-industries/skip-additional-edit-within-primary
Skip LSP additional completion edits which fall within primary edit
2022-11-11 10:31:41 -05:00
Antonio Scandurra
a6198c9a1a Merge pull request #1870 from zed-industries/fix-remote-abs-paths
Fix bug where absolute paths of worktrees were not being stored on the server
2022-11-11 15:28:17 +00:00
Julia
ad698fd110 Test for filtering out of faulty LSP completion additional edits 2022-11-11 10:28:07 -05:00
Kay Simmons
d61c0fb24c Allow dragging and dropping project entries 2022-11-10 20:43:55 -08:00
Kay Simmons
3d5a3634cf Merge pull request #1867 from zed-industries/drag-project-entry-to-pane
Drag project entry to pane
2022-11-10 17:25:22 -08:00
Max Brunsfeld
9ad8731897 Fix boundary condition where injection was not found after an edit 2022-11-10 17:04:40 -08:00
Julia
44c3cedc48 Skip additional completions on any kind of overlap with primary edit 2022-11-10 18:53:37 -05:00
Max Brunsfeld
eeeaf6d9a2 Merge pull request #1872 from zed-industries/tests-use-real-db
Run integration tests with an in-memory sqlite database instead of a hand-coded fake database
2022-11-10 15:15:52 -08:00
Max Brunsfeld
2d4deaafcd Use upstream sqlx git repository 2022-11-10 15:13:32 -08:00
Max Brunsfeld
c839ab2028 Add missing cfg(test) attribute to sqlite RowsAffected 2022-11-10 15:04:57 -08:00
Max Brunsfeld
5d17347a45 Use our fork of sqlx, for now 2022-11-10 14:58:05 -08:00
Max Brunsfeld
9ce3524eb8 Run db tests against both postgres and sqlite 2022-11-10 14:29:03 -08:00
Julia
03115c8d71 Skip LSP additional completion edits which fall within primary edit 2022-11-10 15:28:11 -05:00
Max Brunsfeld
dafdc4b4a5 Run tests with an in-memory sqlite database 2022-11-10 12:18:35 -08:00
Max Brunsfeld
05a6bd914d Get integration tests passing with sqlite
Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2022-11-10 11:03:52 -08:00
Nathan Sobo
fb03eb7a3c Store absolute path on server when sharing worktree
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2022-11-10 09:34:16 -07:00
Nathan Sobo
8e70e1934a Avoid unwrapping when computing tab description
A bug caused the assumptions of this method to be violated. We will fix that in the next commit, but we want to be more conservative in our assumptions here going forward.

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2022-11-10 09:33:57 -07:00
Antonio Scandurra
1bb41b6f54 Go back to a compiling state and start running tests again 2022-11-10 15:24:49 +01:00
Antonio Scandurra
90d1d9ac82 WIP: add more trait bounds 2022-11-10 12:24:56 +01:00
Max Brunsfeld
bed06346d1 Total WIP - try making Db a generic struct instead of a trait 2022-11-09 19:28:06 -08:00
Max Brunsfeld
7e02ac772a Start work on using sqlite in tests 2022-11-09 19:26:29 -08:00
Nate Butler
c0d67d9522 Merge pull request #1868 from zed-industries/readd-search-match-highlight
Update search match highlight and occurrence style
2022-11-09 18:37:17 -05:00
Max Brunsfeld
d14dd27cdc Use a real database in tests, but block on db calls
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2022-11-09 15:22:50 -08:00
Nate Butler
6b4dd2a5de Update search match highlight and occurrence style 2022-11-09 18:17:00 -05:00
Max Brunsfeld
9355d501bc Fetch release branches before bumping zed minor versions 2022-11-09 14:02:46 -08:00
Max Brunsfeld
335db5d03d v0.65.x dev 2022-11-09 13:18:23 -08:00
Julia
98461ea0cd Merge pull request #1865 from zed-industries/do-not-restrict-code-action-kinds
Don't restrict which kind of code actions we ask the LSP server for
2022-11-09 09:49:47 -05:00
Kay Simmons
5707bae9b9 Merge pull request #1866 from zed-industries/tweak-restart-zed-message
Remove restart to update zed icon
2022-11-08 14:38:10 -08:00
Kay Simmons
bbeb685769 remove unused comment 2022-11-08 14:26:55 -08:00
Kay Simmons
cea103e47c remove dead comment 2022-11-08 14:24:51 -08:00
Kay Simmons
ad31c284c7 remove restart to update zed icon because it clashes with the no diagnostics icon 2022-11-08 14:22:11 -08:00
Kay Simmons
738893c527 Split and move to pane working 2022-11-08 14:19:31 -08:00
Max Brunsfeld
6da04d0eee Fix failure to load .env.toml in bootstrap script 2022-11-08 14:09:17 -08:00
Julia
7482660456 Don't restrict which kind of code actions we ask the LSP server for 2022-11-08 16:23:31 -05:00
Mikayla Maki
00123ffe2b Merge pull request #1864 from zed-industries/add-more-move-cursor
Added more autoscroll behaviors
2022-11-08 11:57:09 -08:00
Mikayla Maki
53f8744794 Tried alternate stratergy 2022-11-08 11:54:26 -08:00
Mikayla Maki
537d4762f6 Added more autoscroll behaviors 2022-11-08 11:35:12 -08:00
Max Brunsfeld
2f5004c238 Add highlight query for ERB 2022-11-08 11:29:57 -08:00
Max Brunsfeld
7dcd6c920f Add randomized test for syntax map with combined injections 2022-11-08 11:29:23 -08:00
Max Brunsfeld
ea42bc3c9b Rename some sum_tree seek targets in SyntaxMap 2022-11-08 10:36:44 -08:00
Antonio Scandurra
d3ba769291 Merge pull request #1862 from zed-industries/fix-catalina
Weakly link ReplayKit to ensure this library can be used on macOS 10.15
2022-11-08 15:07:09 +00:00
Antonio Scandurra
3f1b95927f Move weak linking into zed's build.rs 2022-11-08 16:04:55 +01:00
Antonio Scandurra
c183e854d7 Weakly link ReplayKit to ensure this library can be used on macOS 10.15 2022-11-08 13:44:31 +01:00
Max Brunsfeld
86f51ade60 Fix panic in handling edits to combined injections 2022-11-07 17:32:15 -08:00
Max Brunsfeld
c838a7d973 Get combined injections basically working
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2022-11-07 16:58:12 -08:00
Julia
9abfa037fd Handle project entry drop render & start fixing drag cancel issues
Co-Authored-By: Kay Simmons <kay@zed.dev>
2022-11-07 18:17:36 -05:00
Max Brunsfeld
5efe2ed6d3 Start work on handling combined injections in SyntaxMap 2022-11-07 14:45:17 -08:00
Julia
847376a4f5 Start dragging project panel entries
Co-Authored-By: Kay Simmons <kay@zed.dev>
2022-11-07 17:00:01 -05:00
Kay Simmons
1d6af4cf20 Merge pull request #1857 from zed-industries/fix-unicode-vim-left
fixes issue with left motion in vim mode clipping incorrectly
2022-11-04 15:24:17 -07:00
Kay Simmons
b6c5c7871e Addresses issue where left motion in vim mode would clip in the wrong direction 2022-11-04 15:21:29 -07:00
Nate Butler
5acae094bd Swap the color of diagnostic underlines to fix low contrast issue. 2022-11-04 18:02:10 -04:00
Kay Simmons
4d7425f4bf Merge pull request #1845 from zed-industries/vim-dd-fix
Vim dd fix
2022-11-04 14:57:21 -07:00
Joseph T. Lyons
2497e7c008 Merge pull request #1855 from zed-industries/make-app-a-user-property-in-mixpanel
Make `App` a user property in Mixpanel
2022-11-04 14:43:46 -04:00
Joseph T Lyons
474a5dd4f2 Make App a user property in Mixpanel
Currently, we cannot take advantage of Mixpanel's virtual session end events because they are associated with users, not events; this change moves the property onto users.

Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
2022-11-04 14:16:12 -04:00
Max Brunsfeld
be6ee3cbff Start work on ERB language support 2022-11-04 09:33:59 -07:00
Kay Simmons
4977acf6a5 fix some vim mode bugs around deletions and failed motions 2022-11-02 01:20:11 -07:00
Kay Simmons
0cd2d9a9c8 added new supported feature 2022-11-01 13:15:14 -07:00
K Simmons
e2ba8d6df7 Add active pane magnification setting which grows the active pane making it easier to see it's contents 2022-10-25 17:24:19 -07:00
102 changed files with 4277 additions and 5986 deletions

View File

@@ -45,8 +45,11 @@ jobs:
- name: Run tests
run: cargo test --workspace --no-fail-fast
- name: Build collab binaries
run: cargo build --bins --all-features
- name: Build collab
run: cargo build -p collab
- name: Build other binaries
run: cargo build --workspace --bins --all-features
bundle:
name: Bundle app

View File

@@ -17,7 +17,7 @@ jobs:
Restart your Zed or head to https://zed.dev/releases to grab it.
```md
### Changelog
# Changelog
${{ github.event.release.body }}
```

62
Cargo.lock generated
View File

@@ -1028,7 +1028,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.2.2"
version = "0.2.4"
dependencies = [
"anyhow",
"async-trait",
@@ -1953,6 +1953,18 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e"
[[package]]
name = "flume"
version = "0.10.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
dependencies = [
"futures-core",
"futures-sink",
"pin-project",
"spin 0.9.4",
]
[[package]]
name = "fnv"
version = "1.0.7"
@@ -3005,6 +3017,7 @@ dependencies = [
"text",
"theme",
"tree-sitter",
"tree-sitter-embedded-template",
"tree-sitter-html",
"tree-sitter-javascript",
"tree-sitter-json 0.19.0",
@@ -3022,7 +3035,7 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
dependencies = [
"spin",
"spin 0.5.2",
]
[[package]]
@@ -4265,6 +4278,7 @@ name = "project_panel"
version = "0.1.0"
dependencies = [
"context_menu",
"drag_and_drop",
"editor",
"futures 0.3.24",
"gpui",
@@ -4725,7 +4739,7 @@ dependencies = [
"cc",
"libc",
"once_cell",
"spin",
"spin 0.5.2",
"untrusted",
"web-sys",
"winapi 0.3.9",
@@ -5563,6 +5577,15 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "spin"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09"
dependencies = [
"lock_api",
]
[[package]]
name = "spsc-buffer"
version = "0.1.1"
@@ -5583,8 +5606,7 @@ dependencies = [
[[package]]
name = "sqlx"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9249290c05928352f71c077cc44a464d880c63f26f7534728cca008e135c0428"
source = "git+https://github.com/launchbadge/sqlx?rev=4b7053807c705df312bcb9b6281e184bf7534eb3#4b7053807c705df312bcb9b6281e184bf7534eb3"
dependencies = [
"sqlx-core",
"sqlx-macros",
@@ -5593,8 +5615,7 @@ dependencies = [
[[package]]
name = "sqlx-core"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcbc16ddba161afc99e14d1713a453747a2b07fc097d2009f4c300ec99286105"
source = "git+https://github.com/launchbadge/sqlx?rev=4b7053807c705df312bcb9b6281e184bf7534eb3#4b7053807c705df312bcb9b6281e184bf7534eb3"
dependencies = [
"ahash",
"atoi",
@@ -5608,8 +5629,10 @@ dependencies = [
"dotenvy",
"either",
"event-listener",
"flume",
"futures-channel",
"futures-core",
"futures-executor",
"futures-intrusive",
"futures-util",
"hashlink",
@@ -5619,6 +5642,7 @@ dependencies = [
"indexmap",
"itoa",
"libc",
"libsqlite3-sys",
"log",
"md-5",
"memchr",
@@ -5648,8 +5672,7 @@ dependencies = [
[[package]]
name = "sqlx-macros"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b850fa514dc11f2ee85be9d055c512aa866746adfacd1cb42d867d68e6a5b0d9"
source = "git+https://github.com/launchbadge/sqlx?rev=4b7053807c705df312bcb9b6281e184bf7534eb3#4b7053807c705df312bcb9b6281e184bf7534eb3"
dependencies = [
"dotenvy",
"either",
@@ -5657,6 +5680,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"serde_json",
"sha2 0.10.6",
"sqlx-core",
"sqlx-rt",
@@ -5667,8 +5691,7 @@ dependencies = [
[[package]]
name = "sqlx-rt"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24c5b2d25fa654cc5f841750b8e1cdedbe21189bf9a9382ee90bfa9dd3562396"
source = "git+https://github.com/launchbadge/sqlx?rev=4b7053807c705df312bcb9b6281e184bf7534eb3#4b7053807c705df312bcb9b6281e184bf7534eb3"
dependencies = [
"once_cell",
"tokio",
@@ -6381,8 +6404,8 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.20.8"
source = "git+https://github.com/tree-sitter/tree-sitter?rev=366210ae925d7ea0891bc7a0c738f60c77c04d7b#366210ae925d7ea0891bc7a0c738f60c77c04d7b"
version = "0.20.9"
source = "git+https://github.com/tree-sitter/tree-sitter?rev=36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da#36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da"
dependencies = [
"cc",
"regex",
@@ -6426,6 +6449,16 @@ dependencies = [
"tree-sitter",
]
[[package]]
name = "tree-sitter-embedded-template"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33817ade928c73a32d4f904a602321e09de9fc24b71d106f3b4b3f8ab30dcc38"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-go"
version = "0.19.1"
@@ -7640,7 +7673,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.64.0"
version = "0.66.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -7719,6 +7752,7 @@ dependencies = [
"tree-sitter-cpp",
"tree-sitter-css",
"tree-sitter-elixir",
"tree-sitter-embedded-template",
"tree-sitter-go",
"tree-sitter-html",
"tree-sitter-json 0.20.0",

View File

@@ -65,7 +65,7 @@ serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
rand = { version = "0.8" }
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "366210ae925d7ea0891bc7a0c738f60c77c04d7b" }
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da" }
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457

View File

@@ -75,7 +75,7 @@
"ctrl-n": "editor::MoveDown",
"ctrl-b": "editor::MoveLeft",
"ctrl-f": "editor::MoveRight",
"ctrl-l": "editor::CenterScreen",
"ctrl-l": "editor::NextScreen",
"alt-left": "editor::MoveToPreviousWordStart",
"alt-b": "editor::MoveToPreviousWordStart",
"alt-right": "editor::MoveToNextWordEnd",
@@ -472,6 +472,15 @@
"terminal::SendText",
"\u0001"
],
// Terminal.app compatability
"alt-left": [
"terminal::SendText",
"\u001bb"
],
"alt-right": [
"terminal::SendText",
"\u001bf"
],
// There are conflicting bindings for these keys in the global context.
// these bindings override them, remove at your own risk:
"up": [

View File

@@ -1,230 +1,233 @@
{
// The name of the Zed theme to use for the UI
"theme": "One Dark",
// The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Mono",
// The default font size for text in the editor
"buffer_font_size": 15,
// Whether to enable vim modes and key bindings
"vim_mode": false,
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
"hover_popover_enabled": true,
// Whether the cursor blinks in the editor.
"cursor_blink": true,
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
// Whether new projects should start out 'online'. Online projects
// appear in the contacts panel under your name, so that your contacts
// can see which projects you are working on. Regardless of this
// setting, projects keep their last online status when you reopen them.
"projects_online_by_default": true,
// Whether to use language servers to provide code intelligence.
"enable_language_server": true,
// When to automatically save edited buffers. This setting can
// take four values.
//
// 1. Never automatically save:
// "autosave": "off",
// 2. Save when changing focus away from the Zed window:
// "autosave": "on_window_change",
// 3. Save when changing focus away from a specific buffer:
// "autosave": "on_focus_change",
// 4. Save when idle for a certain amount of time:
// "autosave": { "after_delay": {"milliseconds": 500} },
"autosave": "off",
// Where to place the dock by default. This setting can take three
// values:
//
// 1. Position the dock attached to the bottom of the workspace
// "default_dock_anchor": "bottom"
// 2. Position the dock to the right of the workspace like a side panel
// "default_dock_anchor": "right"
// 3. Position the dock full screen over the entire workspace"
// "default_dock_anchor": "expanded"
"default_dock_anchor": "right",
// Whether or not to perform a buffer format before saving
"format_on_save": "on",
// How to perform a buffer format. This setting can take two values:
//
// 1. Format code using the current language server:
// "format_on_save": "language_server"
// 2. Format code using an external command:
// "format_on_save": {
// "external": {
// "command": "prettier",
// "arguments": ["--stdin-filepath", "{buffer_path}"]
// }
// }
"formatter": "language_server",
// How to soft-wrap long lines of text. This setting can take
// three values:
//
// 1. Do not soft wrap.
// "soft_wrap": "none",
// 2. Soft wrap lines that overflow the editor:
// "soft_wrap": "editor_width",
// 3. Soft wrap lines at the preferred line length
// "soft_wrap": "preferred_line_length",
"soft_wrap": "none",
// The column at which to soft-wrap lines, for buffers where soft-wrap
// is enabled.
"preferred_line_length": 80,
// Whether to indent lines using tab characters, as opposed to multiple
// spaces.
"hard_tabs": false,
// How many columns a tab should occupy.
"tab_size": 4,
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
// 1. Show the gutter
// "git_gutter": "tracked_files"
// 2. Hide the gutter
// "git_gutter": "hide"
"git_gutter": "tracked_files"
},
// Settings specific to journaling
"journal": {
// The path of the directory where journal entries are stored
"path": "~",
// What format to display the hours in
// May take 2 values:
// 1. hour12
// 2. hour24
"hour_format": "hour12"
},
// Settings specific to the terminal
"terminal": {
// What shell to use when opening a terminal. May take 3 values:
// 1. Use the system's default terminal configuration (e.g. $TERM).
// "shell": "system"
// 2. A program:
// "shell": {
// "program": "sh"
// }
// 3. A program with arguments:
// "shell": {
// "with_arguments": {
// "program": "/bin/bash",
// "arguments": ["--login"]
// }
// }
"shell": "system",
// What working directory to use when launching the terminal.
// May take 4 values:
// 1. Use the current file's project directory. Will Fallback to the
// first project directory strategy if unsuccessful
// "working_directory": "current_project_directory"
// 2. Use the first project in this workspace's directory
// "working_directory": "first_project_directory"
// 3. Always use this platform's home directory (if we can find it)
// "working_directory": "always_home"
// 4. Always use a specific directory. This value will be shell expanded.
// If this path is not a valid directory the terminal will default to
// this platform's home directory (if we can find it)
// "working_directory": {
// "always": {
// "directory": "~/zed/projects/"
// }
// }
// The name of the Zed theme to use for the UI
"theme": "One Dark",
// The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Mono",
// The default font size for text in the editor
"buffer_font_size": 15,
// The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes.
"active_pane_magnification": 1.0,
// Whether to enable vim modes and key bindings
"vim_mode": false,
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
"hover_popover_enabled": true,
// Whether the cursor blinks in the editor.
"cursor_blink": true,
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
// Whether new projects should start out 'online'. Online projects
// appear in the contacts panel under your name, so that your contacts
// can see which projects you are working on. Regardless of this
// setting, projects keep their last online status when you reopen them.
"projects_online_by_default": true,
// Whether to use language servers to provide code intelligence.
"enable_language_server": true,
// When to automatically save edited buffers. This setting can
// take four values.
//
// 1. Never automatically save:
// "autosave": "off",
// 2. Save when changing focus away from the Zed window:
// "autosave": "on_window_change",
// 3. Save when changing focus away from a specific buffer:
// "autosave": "on_focus_change",
// 4. Save when idle for a certain amount of time:
// "autosave": { "after_delay": {"milliseconds": 500} },
"autosave": "off",
// Where to place the dock by default. This setting can take three
// values:
//
"working_directory": "current_project_directory",
// Set the cursor blinking behavior in the terminal.
// May take 4 values:
// 1. Never blink the cursor, ignoring the terminal mode
// "blinking": "off",
// 2. Default the cursor blink to off, but allow the terminal to
// set blinking
// "blinking": "terminal_controlled",
// 3. Always blink the cursor, ignoring the terminal mode
// "blinking": "on",
"blinking": "terminal_controlled",
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
// Alternate Scroll mode converts mouse scroll events into up / down key
// presses when in the alternate screen (e.g. when running applications
// like vim or less). The terminal can still set and unset this mode.
// May take 2 values:
// 1. Default alternate scroll mode to on
// "alternate_scroll": "on",
// 2. Default alternate scroll mode to off
// "alternate_scroll": "off",
"alternate_scroll": "off",
// Set whether the option key behaves as the meta key.
// May take 2 values:
// 1. Rely on default platform handling of option key, on macOS
// this means generating certain unicode characters
// "option_to_meta": false,
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
// "option_to_meta": true,
"option_as_meta": false,
// Whether or not selecting text in the terminal will automatically
// copy to the system clipboard.
"copy_on_select": false,
// Any key-value pairs added to this list will be added to the terminal's
// enviroment. Use `:` to seperate multiple values.
"env": {
// "KEY": "value1:value2"
}
// Set the terminal's font size. If this option is not included,
// the terminal will default to matching the buffer's font size.
// "font_size": "15"
// Set the terminal's font family. If this option is not included,
// the terminal will default to matching the buffer's font family.
// "font_family": "Zed Mono"
},
// Different settings for specific languages.
"languages": {
"Plain Text": {
"soft_wrap": "preferred_line_length"
},
"C": {
"tab_size": 2
},
"C++": {
"tab_size": 2
},
"Elixir": {
"tab_size": 2
},
"Go": {
"tab_size": 4,
"hard_tabs": true
},
"Markdown": {
"soft_wrap": "preferred_line_length"
},
"Rust": {
"tab_size": 4
},
"JavaScript": {
"tab_size": 2
},
"TypeScript": {
"tab_size": 2
},
"TSX": {
"tab_size": 2
}
},
// LSP Specific settings.
"lsp": {
// Specify the LSP name as a key here.
// As of 8/10/22, supported LSPs are:
// pyright
// gopls
// rust-analyzer
// typescript-language-server
// vscode-json-languageserver
// "rust_analyzer": {
// //These initialization options are merged into Zed's defaults
// "initialization_options": {
// "checkOnSave": {
// "command": "clippy"
// }
// 1. Position the dock attached to the bottom of the workspace
// "default_dock_anchor": "bottom"
// 2. Position the dock to the right of the workspace like a side panel
// "default_dock_anchor": "right"
// 3. Position the dock full screen over the entire workspace"
// "default_dock_anchor": "expanded"
"default_dock_anchor": "right",
// Whether or not to perform a buffer format before saving
"format_on_save": "on",
// How to perform a buffer format. This setting can take two values:
//
// 1. Format code using the current language server:
// "format_on_save": "language_server"
// 2. Format code using an external command:
// "format_on_save": {
// "external": {
// "command": "prettier",
// "arguments": ["--stdin-filepath", "{buffer_path}"]
// }
// }
// }
}
"formatter": "language_server",
// How to soft-wrap long lines of text. This setting can take
// three values:
//
// 1. Do not soft wrap.
// "soft_wrap": "none",
// 2. Soft wrap lines that overflow the editor:
// "soft_wrap": "editor_width",
// 3. Soft wrap lines at the preferred line length
// "soft_wrap": "preferred_line_length",
"soft_wrap": "none",
// The column at which to soft-wrap lines, for buffers where soft-wrap
// is enabled.
"preferred_line_length": 80,
// Whether to indent lines using tab characters, as opposed to multiple
// spaces.
"hard_tabs": false,
// How many columns a tab should occupy.
"tab_size": 4,
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
// 1. Show the gutter
// "git_gutter": "tracked_files"
// 2. Hide the gutter
// "git_gutter": "hide"
"git_gutter": "tracked_files"
},
// Settings specific to journaling
"journal": {
// The path of the directory where journal entries are stored
"path": "~",
// What format to display the hours in
// May take 2 values:
// 1. hour12
// 2. hour24
"hour_format": "hour12"
},
// Settings specific to the terminal
"terminal": {
// What shell to use when opening a terminal. May take 3 values:
// 1. Use the system's default terminal configuration (e.g. $TERM).
// "shell": "system"
// 2. A program:
// "shell": {
// "program": "sh"
// }
// 3. A program with arguments:
// "shell": {
// "with_arguments": {
// "program": "/bin/bash",
// "arguments": ["--login"]
// }
// }
"shell": "system",
// What working directory to use when launching the terminal.
// May take 4 values:
// 1. Use the current file's project directory. Will Fallback to the
// first project directory strategy if unsuccessful
// "working_directory": "current_project_directory"
// 2. Use the first project in this workspace's directory
// "working_directory": "first_project_directory"
// 3. Always use this platform's home directory (if we can find it)
// "working_directory": "always_home"
// 4. Always use a specific directory. This value will be shell expanded.
// If this path is not a valid directory the terminal will default to
// this platform's home directory (if we can find it)
// "working_directory": {
// "always": {
// "directory": "~/zed/projects/"
// }
// }
//
//
"working_directory": "current_project_directory",
// Set the cursor blinking behavior in the terminal.
// May take 4 values:
// 1. Never blink the cursor, ignoring the terminal mode
// "blinking": "off",
// 2. Default the cursor blink to off, but allow the terminal to
// set blinking
// "blinking": "terminal_controlled",
// 3. Always blink the cursor, ignoring the terminal mode
// "blinking": "on",
"blinking": "terminal_controlled",
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
// Alternate Scroll mode converts mouse scroll events into up / down key
// presses when in the alternate screen (e.g. when running applications
// like vim or less). The terminal can still set and unset this mode.
// May take 2 values:
// 1. Default alternate scroll mode to on
// "alternate_scroll": "on",
// 2. Default alternate scroll mode to off
// "alternate_scroll": "off",
"alternate_scroll": "off",
// Set whether the option key behaves as the meta key.
// May take 2 values:
// 1. Rely on default platform handling of option key, on macOS
// this means generating certain unicode characters
// "option_to_meta": false,
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
// "option_to_meta": true,
"option_as_meta": false,
// Whether or not selecting text in the terminal will automatically
// copy to the system clipboard.
"copy_on_select": false,
// Any key-value pairs added to this list will be added to the terminal's
// enviroment. Use `:` to seperate multiple values.
"env": {
// "KEY": "value1:value2"
}
// Set the terminal's font size. If this option is not included,
// the terminal will default to matching the buffer's font size.
// "font_size": "15"
// Set the terminal's font family. If this option is not included,
// the terminal will default to matching the buffer's font family.
// "font_family": "Zed Mono"
},
// Different settings for specific languages.
"languages": {
"Plain Text": {
"soft_wrap": "preferred_line_length"
},
"C": {
"tab_size": 2
},
"C++": {
"tab_size": 2
},
"Elixir": {
"tab_size": 2
},
"Go": {
"tab_size": 4,
"hard_tabs": true
},
"Markdown": {
"soft_wrap": "preferred_line_length"
},
"Rust": {
"tab_size": 4
},
"JavaScript": {
"tab_size": 2
},
"TypeScript": {
"tab_size": 2
},
"TSX": {
"tab_size": 2
}
},
// LSP Specific settings.
"lsp": {
// Specify the LSP name as a key here.
// As of 8/10/22, supported LSPs are:
// pyright
// gopls
// rust-analyzer
// typescript-language-server
// vscode-json-languageserver
// "rust_analyzer": {
// //These initialization options are merged into Zed's defaults
// "initialization_options": {
// "checkOnSave": {
// "command": "clippy"
// }
// }
// }
}
}

View File

@@ -17,7 +17,6 @@ actions!(lsp_status, [ShowErrorMessage]);
const DOWNLOAD_ICON: &str = "icons/download_12.svg";
const WARNING_ICON: &str = "icons/triangle_exclamation_12.svg";
const DONE_ICON: &str = "icons/circle_check_12.svg";
pub enum Event {
ShowError { lsp_name: Arc<str>, error: String },
@@ -237,7 +236,6 @@ impl ActivityIndicator {
// Show any application auto-update info.
if let Some(updater) = &self.auto_updater {
// let theme = &cx.global::<Settings>().theme.workspace.status_bar;
match &updater.read(cx).status() {
AutoUpdateStatus::Checking => (
Some(DOWNLOAD_ICON),
@@ -254,9 +252,7 @@ impl ActivityIndicator {
"Installing Zed update…".to_string(),
None,
),
AutoUpdateStatus::Updated => {
(Some(DONE_ICON), "Restart to update Zed".to_string(), None)
}
AutoUpdateStatus::Updated => (None, "Restart to update Zed".to_string(), None),
AutoUpdateStatus::Errored => (
Some(WARNING_ICON),
"Auto update failed".to_string(),

View File

@@ -1,820 +0,0 @@
use super::{
proto,
user::{User, UserStore},
Client, Status, Subscription, TypedEnvelope,
};
use anyhow::{anyhow, Context, Result};
use futures::lock::Mutex;
use gpui::{
AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakModelHandle,
};
use postage::prelude::Stream;
use rand::prelude::*;
use std::{
collections::{HashMap, HashSet},
mem,
ops::Range,
sync::Arc,
};
use sum_tree::{Bias, SumTree};
use time::OffsetDateTime;
use util::{post_inc, ResultExt as _, TryFutureExt};
pub struct ChannelList {
available_channels: Option<Vec<ChannelDetails>>,
channels: HashMap<u64, WeakModelHandle<Channel>>,
client: Arc<Client>,
user_store: ModelHandle<UserStore>,
_task: Task<Option<()>>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ChannelDetails {
pub id: u64,
pub name: String,
}
pub struct Channel {
details: ChannelDetails,
messages: SumTree<ChannelMessage>,
loaded_all_messages: bool,
next_pending_message_id: usize,
user_store: ModelHandle<UserStore>,
rpc: Arc<Client>,
outgoing_messages_lock: Arc<Mutex<()>>,
rng: StdRng,
_subscription: Subscription,
}
#[derive(Clone, Debug)]
pub struct ChannelMessage {
pub id: ChannelMessageId,
pub body: String,
pub timestamp: OffsetDateTime,
pub sender: Arc<User>,
pub nonce: u128,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum ChannelMessageId {
Saved(u64),
Pending(usize),
}
#[derive(Clone, Debug, Default)]
pub struct ChannelMessageSummary {
max_id: ChannelMessageId,
count: usize,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
struct Count(usize);
pub enum ChannelListEvent {}
#[derive(Clone, Debug, PartialEq)]
pub enum ChannelEvent {
MessagesUpdated {
old_range: Range<usize>,
new_count: usize,
},
}
impl Entity for ChannelList {
type Event = ChannelListEvent;
}
impl ChannelList {
pub fn new(
user_store: ModelHandle<UserStore>,
rpc: Arc<Client>,
cx: &mut ModelContext<Self>,
) -> Self {
let _task = cx.spawn_weak(|this, mut cx| {
let rpc = rpc.clone();
async move {
let mut status = rpc.status();
while let Some((status, this)) = status.recv().await.zip(this.upgrade(&cx)) {
match status {
Status::Connected { .. } => {
let response = rpc
.request(proto::GetChannels {})
.await
.context("failed to fetch available channels")?;
this.update(&mut cx, |this, cx| {
this.available_channels =
Some(response.channels.into_iter().map(Into::into).collect());
let mut to_remove = Vec::new();
for (channel_id, channel) in &this.channels {
if let Some(channel) = channel.upgrade(cx) {
channel.update(cx, |channel, cx| channel.rejoin(cx))
} else {
to_remove.push(*channel_id);
}
}
for channel_id in to_remove {
this.channels.remove(&channel_id);
}
cx.notify();
});
}
Status::SignedOut { .. } => {
this.update(&mut cx, |this, cx| {
this.available_channels = None;
this.channels.clear();
cx.notify();
});
}
_ => {}
}
}
Ok(())
}
.log_err()
});
Self {
available_channels: None,
channels: Default::default(),
user_store,
client: rpc,
_task,
}
}
pub fn available_channels(&self) -> Option<&[ChannelDetails]> {
self.available_channels.as_deref()
}
pub fn get_channel(
&mut self,
id: u64,
cx: &mut MutableAppContext,
) -> Option<ModelHandle<Channel>> {
if let Some(channel) = self.channels.get(&id).and_then(|c| c.upgrade(cx)) {
return Some(channel);
}
let channels = self.available_channels.as_ref()?;
let details = channels.iter().find(|details| details.id == id)?.clone();
let channel = cx.add_model(|cx| {
Channel::new(details, self.user_store.clone(), self.client.clone(), cx)
});
self.channels.insert(id, channel.downgrade());
Some(channel)
}
}
impl Entity for Channel {
type Event = ChannelEvent;
fn release(&mut self, _: &mut MutableAppContext) {
self.rpc
.send(proto::LeaveChannel {
channel_id: self.details.id,
})
.log_err();
}
}
impl Channel {
pub fn init(rpc: &Arc<Client>) {
rpc.add_model_message_handler(Self::handle_message_sent);
}
pub fn new(
details: ChannelDetails,
user_store: ModelHandle<UserStore>,
rpc: Arc<Client>,
cx: &mut ModelContext<Self>,
) -> Self {
let _subscription = rpc.add_model_for_remote_entity(details.id, cx);
{
let user_store = user_store.clone();
let rpc = rpc.clone();
let channel_id = details.id;
cx.spawn(|channel, mut cx| {
async move {
let response = rpc.request(proto::JoinChannel { channel_id }).await?;
let messages =
messages_from_proto(response.messages, &user_store, &mut cx).await?;
let loaded_all_messages = response.done;
channel.update(&mut cx, |channel, cx| {
channel.insert_messages(messages, cx);
channel.loaded_all_messages = loaded_all_messages;
});
Ok(())
}
.log_err()
})
.detach();
}
Self {
details,
user_store,
rpc,
outgoing_messages_lock: Default::default(),
messages: Default::default(),
loaded_all_messages: false,
next_pending_message_id: 0,
rng: StdRng::from_entropy(),
_subscription,
}
}
pub fn name(&self) -> &str {
&self.details.name
}
pub fn send_message(
&mut self,
body: String,
cx: &mut ModelContext<Self>,
) -> Result<Task<Result<()>>> {
if body.is_empty() {
Err(anyhow!("message body can't be empty"))?;
}
let current_user = self
.user_store
.read(cx)
.current_user()
.ok_or_else(|| anyhow!("current_user is not present"))?;
let channel_id = self.details.id;
let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id));
let nonce = self.rng.gen();
self.insert_messages(
SumTree::from_item(
ChannelMessage {
id: pending_id,
body: body.clone(),
sender: current_user,
timestamp: OffsetDateTime::now_utc(),
nonce,
},
&(),
),
cx,
);
let user_store = self.user_store.clone();
let rpc = self.rpc.clone();
let outgoing_messages_lock = self.outgoing_messages_lock.clone();
Ok(cx.spawn(|this, mut cx| async move {
let outgoing_message_guard = outgoing_messages_lock.lock().await;
let request = rpc.request(proto::SendChannelMessage {
channel_id,
body,
nonce: Some(nonce.into()),
});
let response = request.await?;
drop(outgoing_message_guard);
let message = ChannelMessage::from_proto(
response.message.ok_or_else(|| anyhow!("invalid message"))?,
&user_store,
&mut cx,
)
.await?;
this.update(&mut cx, |this, cx| {
this.insert_messages(SumTree::from_item(message, &()), cx);
Ok(())
})
}))
}
pub fn load_more_messages(&mut self, cx: &mut ModelContext<Self>) -> bool {
if !self.loaded_all_messages {
let rpc = self.rpc.clone();
let user_store = self.user_store.clone();
let channel_id = self.details.id;
if let Some(before_message_id) =
self.messages.first().and_then(|message| match message.id {
ChannelMessageId::Saved(id) => Some(id),
ChannelMessageId::Pending(_) => None,
})
{
cx.spawn(|this, mut cx| {
async move {
let response = rpc
.request(proto::GetChannelMessages {
channel_id,
before_message_id,
})
.await?;
let loaded_all_messages = response.done;
let messages =
messages_from_proto(response.messages, &user_store, &mut cx).await?;
this.update(&mut cx, |this, cx| {
this.loaded_all_messages = loaded_all_messages;
this.insert_messages(messages, cx);
});
Ok(())
}
.log_err()
})
.detach();
return true;
}
}
false
}
pub fn rejoin(&mut self, cx: &mut ModelContext<Self>) {
let user_store = self.user_store.clone();
let rpc = self.rpc.clone();
let channel_id = self.details.id;
cx.spawn(|this, mut cx| {
async move {
let response = rpc.request(proto::JoinChannel { channel_id }).await?;
let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
let loaded_all_messages = response.done;
let pending_messages = this.update(&mut cx, |this, cx| {
if let Some((first_new_message, last_old_message)) =
messages.first().zip(this.messages.last())
{
if first_new_message.id > last_old_message.id {
let old_messages = mem::take(&mut this.messages);
cx.emit(ChannelEvent::MessagesUpdated {
old_range: 0..old_messages.summary().count,
new_count: 0,
});
this.loaded_all_messages = loaded_all_messages;
}
}
this.insert_messages(messages, cx);
if loaded_all_messages {
this.loaded_all_messages = loaded_all_messages;
}
this.pending_messages().cloned().collect::<Vec<_>>()
});
for pending_message in pending_messages {
let request = rpc.request(proto::SendChannelMessage {
channel_id,
body: pending_message.body,
nonce: Some(pending_message.nonce.into()),
});
let response = request.await?;
let message = ChannelMessage::from_proto(
response.message.ok_or_else(|| anyhow!("invalid message"))?,
&user_store,
&mut cx,
)
.await?;
this.update(&mut cx, |this, cx| {
this.insert_messages(SumTree::from_item(message, &()), cx);
});
}
Ok(())
}
.log_err()
})
.detach();
}
pub fn message_count(&self) -> usize {
self.messages.summary().count
}
pub fn messages(&self) -> &SumTree<ChannelMessage> {
&self.messages
}
pub fn message(&self, ix: usize) -> &ChannelMessage {
let mut cursor = self.messages.cursor::<Count>();
cursor.seek(&Count(ix), Bias::Right, &());
cursor.item().unwrap()
}
pub fn messages_in_range(&self, range: Range<usize>) -> impl Iterator<Item = &ChannelMessage> {
let mut cursor = self.messages.cursor::<Count>();
cursor.seek(&Count(range.start), Bias::Right, &());
cursor.take(range.len())
}
pub fn pending_messages(&self) -> impl Iterator<Item = &ChannelMessage> {
let mut cursor = self.messages.cursor::<ChannelMessageId>();
cursor.seek(&ChannelMessageId::Pending(0), Bias::Left, &());
cursor
}
async fn handle_message_sent(
this: ModelHandle<Self>,
message: TypedEnvelope<proto::ChannelMessageSent>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
let message = message
.payload
.message
.ok_or_else(|| anyhow!("empty message"))?;
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
this.update(&mut cx, |this, cx| {
this.insert_messages(SumTree::from_item(message, &()), cx)
});
Ok(())
}
fn insert_messages(&mut self, messages: SumTree<ChannelMessage>, cx: &mut ModelContext<Self>) {
if let Some((first_message, last_message)) = messages.first().zip(messages.last()) {
let nonces = messages
.cursor::<()>()
.map(|m| m.nonce)
.collect::<HashSet<_>>();
let mut old_cursor = self.messages.cursor::<(ChannelMessageId, Count)>();
let mut new_messages = old_cursor.slice(&first_message.id, Bias::Left, &());
let start_ix = old_cursor.start().1 .0;
let removed_messages = old_cursor.slice(&last_message.id, Bias::Right, &());
let removed_count = removed_messages.summary().count;
let new_count = messages.summary().count;
let end_ix = start_ix + removed_count;
new_messages.push_tree(messages, &());
let mut ranges = Vec::<Range<usize>>::new();
if new_messages.last().unwrap().is_pending() {
new_messages.push_tree(old_cursor.suffix(&()), &());
} else {
new_messages.push_tree(
old_cursor.slice(&ChannelMessageId::Pending(0), Bias::Left, &()),
&(),
);
while let Some(message) = old_cursor.item() {
let message_ix = old_cursor.start().1 .0;
if nonces.contains(&message.nonce) {
if ranges.last().map_or(false, |r| r.end == message_ix) {
ranges.last_mut().unwrap().end += 1;
} else {
ranges.push(message_ix..message_ix + 1);
}
} else {
new_messages.push(message.clone(), &());
}
old_cursor.next(&());
}
}
drop(old_cursor);
self.messages = new_messages;
for range in ranges.into_iter().rev() {
cx.emit(ChannelEvent::MessagesUpdated {
old_range: range,
new_count: 0,
});
}
cx.emit(ChannelEvent::MessagesUpdated {
old_range: start_ix..end_ix,
new_count,
});
cx.notify();
}
}
}
async fn messages_from_proto(
proto_messages: Vec<proto::ChannelMessage>,
user_store: &ModelHandle<UserStore>,
cx: &mut AsyncAppContext,
) -> Result<SumTree<ChannelMessage>> {
let unique_user_ids = proto_messages
.iter()
.map(|m| m.sender_id)
.collect::<HashSet<_>>()
.into_iter()
.collect();
user_store
.update(cx, |user_store, cx| {
user_store.get_users(unique_user_ids, cx)
})
.await?;
let mut messages = Vec::with_capacity(proto_messages.len());
for message in proto_messages {
messages.push(ChannelMessage::from_proto(message, user_store, cx).await?);
}
let mut result = SumTree::new();
result.extend(messages, &());
Ok(result)
}
impl From<proto::Channel> for ChannelDetails {
fn from(message: proto::Channel) -> Self {
Self {
id: message.id,
name: message.name,
}
}
}
impl ChannelMessage {
pub async fn from_proto(
message: proto::ChannelMessage,
user_store: &ModelHandle<UserStore>,
cx: &mut AsyncAppContext,
) -> Result<Self> {
let sender = user_store
.update(cx, |user_store, cx| {
user_store.get_user(message.sender_id, cx)
})
.await?;
Ok(ChannelMessage {
id: ChannelMessageId::Saved(message.id),
body: message.body,
timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?,
sender,
nonce: message
.nonce
.ok_or_else(|| anyhow!("nonce is required"))?
.into(),
})
}
pub fn is_pending(&self) -> bool {
matches!(self.id, ChannelMessageId::Pending(_))
}
}
impl sum_tree::Item for ChannelMessage {
type Summary = ChannelMessageSummary;
fn summary(&self) -> Self::Summary {
ChannelMessageSummary {
max_id: self.id,
count: 1,
}
}
}
impl Default for ChannelMessageId {
fn default() -> Self {
Self::Saved(0)
}
}
impl sum_tree::Summary for ChannelMessageSummary {
type Context = ();
fn add_summary(&mut self, summary: &Self, _: &()) {
self.max_id = summary.max_id;
self.count += summary.count;
}
}
impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for ChannelMessageId {
fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
debug_assert!(summary.max_id > *self);
*self = summary.max_id;
}
}
impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count {
fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
self.0 += summary.count;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::{FakeHttpClient, FakeServer};
use gpui::TestAppContext;
#[gpui::test]
async fn test_channel_messages(cx: &mut TestAppContext) {
cx.foreground().forbid_parking();
let user_id = 5;
let http_client = FakeHttpClient::with_404_response();
let client = cx.update(|cx| Client::new(http_client.clone(), cx));
let server = FakeServer::for_client(user_id, &client, cx).await;
Channel::init(&client);
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
let channel_list = cx.add_model(|cx| ChannelList::new(user_store, client.clone(), cx));
channel_list.read_with(cx, |list, _| assert_eq!(list.available_channels(), None));
// Get the available channels.
let get_channels = server.receive::<proto::GetChannels>().await.unwrap();
server
.respond(
get_channels.receipt(),
proto::GetChannelsResponse {
channels: vec![proto::Channel {
id: 5,
name: "the-channel".to_string(),
}],
},
)
.await;
channel_list.next_notification(cx).await;
channel_list.read_with(cx, |list, _| {
assert_eq!(
list.available_channels().unwrap(),
&[ChannelDetails {
id: 5,
name: "the-channel".into(),
}]
)
});
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
assert_eq!(get_users.payload.user_ids, vec![5]);
server
.respond(
get_users.receipt(),
proto::UsersResponse {
users: vec![proto::User {
id: 5,
github_login: "nathansobo".into(),
avatar_url: "http://avatar.com/nathansobo".into(),
}],
},
)
.await;
// Join a channel and populate its existing messages.
let channel = channel_list
.update(cx, |list, cx| {
let channel_id = list.available_channels().unwrap()[0].id;
list.get_channel(channel_id, cx)
})
.unwrap();
channel.read_with(cx, |channel, _| assert!(channel.messages().is_empty()));
let join_channel = server.receive::<proto::JoinChannel>().await.unwrap();
server
.respond(
join_channel.receipt(),
proto::JoinChannelResponse {
messages: vec![
proto::ChannelMessage {
id: 10,
body: "a".into(),
timestamp: 1000,
sender_id: 5,
nonce: Some(1.into()),
},
proto::ChannelMessage {
id: 11,
body: "b".into(),
timestamp: 1001,
sender_id: 6,
nonce: Some(2.into()),
},
],
done: false,
},
)
.await;
// Client requests all users for the received messages
let mut get_users = server.receive::<proto::GetUsers>().await.unwrap();
get_users.payload.user_ids.sort();
assert_eq!(get_users.payload.user_ids, vec![6]);
server
.respond(
get_users.receipt(),
proto::UsersResponse {
users: vec![proto::User {
id: 6,
github_login: "maxbrunsfeld".into(),
avatar_url: "http://avatar.com/maxbrunsfeld".into(),
}],
},
)
.await;
assert_eq!(
channel.next_event(cx).await,
ChannelEvent::MessagesUpdated {
old_range: 0..0,
new_count: 2,
}
);
channel.read_with(cx, |channel, _| {
assert_eq!(
channel
.messages_in_range(0..2)
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
.collect::<Vec<_>>(),
&[
("nathansobo".into(), "a".into()),
("maxbrunsfeld".into(), "b".into())
]
);
});
// Receive a new message.
server.send(proto::ChannelMessageSent {
channel_id: channel.read_with(cx, |channel, _| channel.details.id),
message: Some(proto::ChannelMessage {
id: 12,
body: "c".into(),
timestamp: 1002,
sender_id: 7,
nonce: Some(3.into()),
}),
});
// Client requests user for message since they haven't seen them yet
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
assert_eq!(get_users.payload.user_ids, vec![7]);
server
.respond(
get_users.receipt(),
proto::UsersResponse {
users: vec![proto::User {
id: 7,
github_login: "as-cii".into(),
avatar_url: "http://avatar.com/as-cii".into(),
}],
},
)
.await;
assert_eq!(
channel.next_event(cx).await,
ChannelEvent::MessagesUpdated {
old_range: 2..2,
new_count: 1,
}
);
channel.read_with(cx, |channel, _| {
assert_eq!(
channel
.messages_in_range(2..3)
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
.collect::<Vec<_>>(),
&[("as-cii".into(), "c".into())]
)
});
// Scroll up to view older messages.
channel.update(cx, |channel, cx| {
assert!(channel.load_more_messages(cx));
});
let get_messages = server.receive::<proto::GetChannelMessages>().await.unwrap();
assert_eq!(get_messages.payload.channel_id, 5);
assert_eq!(get_messages.payload.before_message_id, 10);
server
.respond(
get_messages.receipt(),
proto::GetChannelMessagesResponse {
done: true,
messages: vec![
proto::ChannelMessage {
id: 8,
body: "y".into(),
timestamp: 998,
sender_id: 5,
nonce: Some(4.into()),
},
proto::ChannelMessage {
id: 9,
body: "z".into(),
timestamp: 999,
sender_id: 6,
nonce: Some(5.into()),
},
],
},
)
.await;
assert_eq!(
channel.next_event(cx).await,
ChannelEvent::MessagesUpdated {
old_range: 0..0,
new_count: 2,
}
);
channel.read_with(cx, |channel, _| {
assert_eq!(
channel
.messages_in_range(0..2)
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
.collect::<Vec<_>>(),
&[
("nathansobo".into(), "y".into()),
("maxbrunsfeld".into(), "z".into())
]
);
});
}
}

View File

@@ -1,7 +1,6 @@
#[cfg(any(test, feature = "test-support"))]
pub mod test;
pub mod channel;
pub mod http;
pub mod telemetry;
pub mod user;
@@ -44,7 +43,6 @@ use thiserror::Error;
use url::Url;
use util::{ResultExt, TryFutureExt};
pub use channel::*;
pub use rpc::*;
pub use user::*;

View File

@@ -32,7 +32,6 @@ pub struct Telemetry {
struct TelemetryState {
metrics_id: Option<Arc<str>>,
device_id: Option<Arc<str>>,
app: &'static str,
app_version: Option<Arc<str>>,
release_channel: Option<&'static str>,
os_version: Option<Arc<str>>,
@@ -80,8 +79,6 @@ struct MixpanelEventProperties {
app_version: Option<Arc<str>>,
#[serde(rename = "Signed In")]
signed_in: bool,
#[serde(rename = "App")]
app: &'static str,
}
#[derive(Serialize)]
@@ -120,7 +117,6 @@ impl Telemetry {
state: Mutex::new(TelemetryState {
os_version: platform.os_version().ok().map(|v| v.to_string().into()),
os_name: platform.os_name().into(),
app: "Zed",
app_version: platform.app_version().ok().map(|v| v.to_string().into()),
release_channel,
device_id: None,
@@ -205,7 +201,11 @@ impl Telemetry {
let json_bytes = serde_json::to_vec(&[MixpanelEngageRequest {
token,
distinct_id: device_id,
set: json!({ "Staff": is_staff, "ID": metrics_id }),
set: json!({
"Staff": is_staff,
"ID": metrics_id,
"App": true
}),
}])?;
let request = Request::post(MIXPANEL_ENGAGE_URL)
.header("Content-Type", "application/json")
@@ -241,7 +241,6 @@ impl Telemetry {
release_channel: state.release_channel,
app_version: state.app_version.clone(),
signed_in: state.metrics_id.is_some(),
app: state.app,
},
};
state.queue.push(event);

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
version = "0.2.2"
version = "0.2.4"
[[bin]]
name = "collab"
@@ -50,8 +50,9 @@ tracing-log = "0.1.3"
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
[dependencies.sqlx]
version = "0.6"
features = ["runtime-tokio-rustls", "postgres", "time", "uuid"]
git = "https://github.com/launchbadge/sqlx"
rev = "4b7053807c705df312bcb9b6281e184bf7534eb3"
features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid"]
[dev-dependencies]
collections = { path = "../collections", features = ["test-support"] }
@@ -78,5 +79,10 @@ lazy_static = "1.4"
serde_json = { version = "1.0", features = ["preserve_order"] }
unindent = "0.1"
[dev-dependencies.sqlx]
git = "https://github.com/launchbadge/sqlx"
rev = "4b7053807c705df312bcb9b6281e184bf7534eb3"
features = ["sqlite"]
[features]
seed-support = ["clap", "lipsum", "reqwest"]

View File

@@ -0,0 +1,41 @@
CREATE TABLE IF NOT EXISTS "users" (
"id" INTEGER PRIMARY KEY,
"github_login" VARCHAR,
"admin" BOOLEAN,
"email_address" VARCHAR(255) DEFAULT NULL,
"invite_code" VARCHAR(64),
"invite_count" INTEGER NOT NULL DEFAULT 0,
"inviter_id" INTEGER REFERENCES users (id),
"connected_once" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP NOT NULL DEFAULT now,
"metrics_id" VARCHAR(255),
"github_user_id" INTEGER
);
CREATE UNIQUE INDEX "index_users_github_login" ON "users" ("github_login");
CREATE UNIQUE INDEX "index_invite_code_users" ON "users" ("invite_code");
CREATE INDEX "index_users_on_email_address" ON "users" ("email_address");
CREATE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id");
CREATE TABLE IF NOT EXISTS "access_tokens" (
"id" INTEGER PRIMARY KEY,
"user_id" INTEGER REFERENCES users (id),
"hash" VARCHAR(128)
);
CREATE INDEX "index_access_tokens_user_id" ON "access_tokens" ("user_id");
CREATE TABLE IF NOT EXISTS "contacts" (
"id" INTEGER PRIMARY KEY,
"user_id_a" INTEGER REFERENCES users (id) NOT NULL,
"user_id_b" INTEGER REFERENCES users (id) NOT NULL,
"a_to_b" BOOLEAN NOT NULL,
"should_notify" BOOLEAN NOT NULL,
"accepted" BOOLEAN NOT NULL
);
CREATE UNIQUE INDEX "index_contacts_user_ids" ON "contacts" ("user_id_a", "user_id_b");
CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b");
CREATE TABLE IF NOT EXISTS "projects" (
"id" INTEGER PRIMARY KEY,
"host_user_id" INTEGER REFERENCES users (id) NOT NULL,
"unregistered" BOOLEAN NOT NULL DEFAULT false
);

View File

@@ -1,6 +1,6 @@
use crate::{
auth,
db::{Invite, NewUserParams, ProjectId, Signup, User, UserId, WaitlistSummary},
db::{Invite, NewUserParams, Signup, User, UserId, WaitlistSummary},
rpc::{self, ResultExt},
AppState, Error, Result,
};
@@ -16,9 +16,7 @@ use axum::{
};
use axum_extra::response::ErasedJson;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::{sync::Arc, time::Duration};
use time::OffsetDateTime;
use std::sync::Arc;
use tower::ServiceBuilder;
use tracing::instrument;
@@ -32,16 +30,6 @@ pub fn routes(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body
.route("/invite_codes/:code", get(get_user_for_invite_code))
.route("/panic", post(trace_panic))
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
.route(
"/user_activity/summary",
get(get_top_users_activity_summary),
)
.route(
"/user_activity/timeline/:user_id",
get(get_user_activity_timeline),
)
.route("/user_activity/counts", get(get_active_user_counts))
.route("/project_metadata", get(get_project_metadata))
.route("/signups", post(create_signup))
.route("/signups_summary", get(get_waitlist_summary))
.route("/user_invites", post(create_invite_from_code))
@@ -283,93 +271,6 @@ async fn get_rpc_server_snapshot(
Ok(ErasedJson::pretty(rpc_server.snapshot().await))
}
#[derive(Deserialize)]
struct TimePeriodParams {
#[serde(with = "time::serde::iso8601")]
start: OffsetDateTime,
#[serde(with = "time::serde::iso8601")]
end: OffsetDateTime,
}
async fn get_top_users_activity_summary(
Query(params): Query<TimePeriodParams>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<ErasedJson> {
let summary = app
.db
.get_top_users_activity_summary(params.start..params.end, 100)
.await?;
Ok(ErasedJson::pretty(summary))
}
async fn get_user_activity_timeline(
Path(user_id): Path<i32>,
Query(params): Query<TimePeriodParams>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<ErasedJson> {
let summary = app
.db
.get_user_activity_timeline(params.start..params.end, UserId(user_id))
.await?;
Ok(ErasedJson::pretty(summary))
}
#[derive(Deserialize)]
struct ActiveUserCountParams {
#[serde(flatten)]
period: TimePeriodParams,
durations_in_minutes: String,
#[serde(default)]
only_collaborative: bool,
}
#[derive(Serialize)]
struct ActiveUserSet {
active_time_in_minutes: u64,
user_count: usize,
}
async fn get_active_user_counts(
Query(params): Query<ActiveUserCountParams>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<ErasedJson> {
let durations_in_minutes = params.durations_in_minutes.split(',');
let mut user_sets = Vec::new();
for duration in durations_in_minutes {
let duration = duration
.parse()
.map_err(|_| anyhow!("invalid duration: {duration}"))?;
user_sets.push(ActiveUserSet {
active_time_in_minutes: duration,
user_count: app
.db
.get_active_user_count(
params.period.start..params.period.end,
Duration::from_secs(duration * 60),
params.only_collaborative,
)
.await?,
})
}
Ok(ErasedJson::pretty(user_sets))
}
#[derive(Deserialize)]
struct GetProjectMetadataParams {
project_id: u64,
}
async fn get_project_metadata(
Query(params): Query<GetProjectMetadataParams>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<ErasedJson> {
let extensions = app
.db
.get_project_extensions(ProjectId::from_proto(params.project_id))
.await?;
Ok(ErasedJson::pretty(json!({ "extensions": extensions })))
}
#[derive(Deserialize)]
struct CreateAccessTokenQueryParams {
public_key: String,
@@ -437,7 +338,7 @@ async fn create_signup(
Json(params): Json<Signup>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<()> {
app.db.create_signup(params).await?;
app.db.create_signup(&params).await?;
Ok(())
}

View File

@@ -75,7 +75,7 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
const MAX_ACCESS_TOKENS_TO_STORE: usize = 8;
pub async fn create_access_token(db: &dyn db::Db, user_id: UserId) -> Result<String> {
pub async fn create_access_token(db: &db::DefaultDb, user_id: UserId) -> Result<String> {
let access_token = rpc::auth::random_token();
let access_token_hash =
hash_access_token(&access_token).context("failed to hash access token")?;

View File

@@ -1,9 +1,7 @@
use collab::{Error, Result};
use db::{Db, PostgresDb, UserId};
use rand::prelude::*;
use db::DefaultDb;
use serde::{de::DeserializeOwned, Deserialize};
use std::fmt::Write;
use time::{Duration, OffsetDateTime};
#[allow(unused)]
#[path = "../db.rs"]
@@ -18,9 +16,8 @@ struct GitHubUser {
#[tokio::main]
async fn main() {
let mut rng = StdRng::from_entropy();
let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var");
let db = PostgresDb::new(&database_url, 5)
let db = DefaultDb::new(&database_url, 5)
.await
.expect("failed to connect to postgres database");
let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");
@@ -64,16 +61,14 @@ async fn main() {
}
}
let mut zed_user_ids = Vec::<UserId>::new();
for (github_user, admin) in zed_users {
if let Some(user) = db
if db
.get_user_by_github_account(&github_user.login, Some(github_user.id))
.await
.expect("failed to fetch user")
.is_none()
{
zed_user_ids.push(user.id);
} else if let Some(email) = &github_user.email {
zed_user_ids.push(
if let Some(email) = &github_user.email {
db.create_user(
email,
admin,
@@ -84,11 +79,8 @@ async fn main() {
},
)
.await
.expect("failed to insert user")
.user_id,
);
} else if admin {
zed_user_ids.push(
.expect("failed to insert user");
} else if admin {
db.create_user(
&format!("{}@zed.dev", github_user.login),
admin,
@@ -99,62 +91,10 @@ async fn main() {
},
)
.await
.expect("failed to insert user")
.user_id,
);
.expect("failed to insert user");
}
}
}
let zed_org_id = if let Some(org) = db
.find_org_by_slug("zed")
.await
.expect("failed to fetch org")
{
org.id
} else {
db.create_org("Zed", "zed")
.await
.expect("failed to insert org")
};
let general_channel_id = if let Some(channel) = db
.get_org_channels(zed_org_id)
.await
.expect("failed to fetch channels")
.iter()
.find(|c| c.name == "General")
{
channel.id
} else {
let channel_id = db
.create_org_channel(zed_org_id, "General")
.await
.expect("failed to insert channel");
let now = OffsetDateTime::now_utc();
let max_seconds = Duration::days(100).as_seconds_f64();
let mut timestamps = (0..1000)
.map(|_| now - Duration::seconds_f64(rng.gen_range(0_f64..=max_seconds)))
.collect::<Vec<_>>();
timestamps.sort();
for timestamp in timestamps {
let sender_id = *zed_user_ids.choose(&mut rng).unwrap();
let body = lipsum::lipsum_words(rng.gen_range(1..=50));
db.create_channel_message(channel_id, sender_id, &body, timestamp, rng.gen())
.await
.expect("failed to insert message");
}
channel_id
};
for user_id in zed_user_ids {
db.add_org_member(zed_org_id, user_id, true)
.await
.expect("failed to insert org membership");
db.add_channel_member(general_channel_id, user_id, true)
.await
.expect("failed to insert channel membership");
}
}
async fn fetch_github<T: DeserializeOwned>(

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,14 @@
use crate::{
db::{NewUserParams, ProjectId, TestDb, UserId},
rpc::{Executor, Server, Store},
db::{NewUserParams, ProjectId, SqliteTestDb as TestDb, UserId},
rpc::{Executor, Server},
AppState,
};
use ::rpc::Peer;
use anyhow::anyhow;
use call::{room, ActiveCall, ParticipantLocation, Room};
use client::{
self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection,
Credentials, EstablishConnectionError, PeerId, User, UserStore, RECEIVE_TIMEOUT,
self, test::FakeHttpClient, Client, Connection, Credentials, EstablishConnectionError, PeerId,
User, UserStore, RECEIVE_TIMEOUT,
};
use collections::{BTreeMap, HashMap, HashSet};
use editor::{
@@ -16,10 +16,7 @@ use editor::{
ToggleCodeActions, Undo,
};
use fs::{FakeFs, Fs as _, HomeDir, LineEnding};
use futures::{
channel::{mpsc, oneshot},
Future, StreamExt as _,
};
use futures::{channel::oneshot, Future, StreamExt as _};
use gpui::{
executor::{self, Deterministic},
geometry::vector::vec2f,
@@ -39,7 +36,6 @@ use project::{
use rand::prelude::*;
use serde_json::json;
use settings::{Formatter, Settings};
use sqlx::types::time::OffsetDateTime;
use std::{
cell::{Cell, RefCell},
env, mem,
@@ -73,7 +69,10 @@ async fn test_basic_calls(
cx_c: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let start = std::time::Instant::now();
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;
@@ -259,6 +258,8 @@ async fn test_basic_calls(
pending: Default::default()
}
);
eprintln!("finished test {:?}", start.elapsed());
}
#[gpui::test(iterations = 10)]
@@ -271,7 +272,7 @@ async fn test_room_uniqueness(
cx_c: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let _client_a2 = server.create_client(cx_a2, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
@@ -376,7 +377,7 @@ async fn test_leaving_room_on_disconnection(
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -505,7 +506,7 @@ async fn test_calls_on_multiple_connections(
cx_b2: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b1 = server.create_client(cx_b1, "user_b").await;
let client_b2 = server.create_client(cx_b2, "user_b").await;
@@ -654,7 +655,7 @@ async fn test_share_project(
) {
deterministic.forbid_parking();
let (_, window_b) = cx_b.add_window(|_| EmptyView);
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
@@ -791,7 +792,7 @@ async fn test_unshare_project(
cx_c: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
@@ -874,7 +875,7 @@ async fn test_host_disconnect(
) {
cx_b.update(editor::init);
deterministic.forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
@@ -908,7 +909,7 @@ async fn test_host_disconnect(
cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "b.txt"), true, cx)
workspace.open_path((worktree_id, "b.txt"), None, true, cx)
})
.await
.unwrap()
@@ -979,7 +980,7 @@ async fn test_active_call_events(
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
client_a.fs.insert_tree("/a", json!({})).await;
@@ -1068,7 +1069,7 @@ async fn test_room_location(
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
client_a.fs.insert_tree("/a", json!({})).await;
@@ -1234,7 +1235,7 @@ async fn test_propagate_saves_and_fs_changes(
cx_c: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
@@ -1409,7 +1410,7 @@ async fn test_git_diff_base_change(
cx_b: &mut TestAppContext,
) {
executor.forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -1661,7 +1662,7 @@ async fn test_fs_operations(
cx_b: &mut TestAppContext,
) {
executor.forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -1927,7 +1928,7 @@ async fn test_fs_operations(
#[gpui::test(iterations = 10)]
async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -1981,7 +1982,7 @@ async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut T
#[gpui::test(iterations = 10)]
async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -2040,7 +2041,7 @@ async fn test_editing_while_guest_opens_buffer(
cx_b: &mut TestAppContext,
) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -2087,7 +2088,7 @@ async fn test_leaving_worktree_while_opening_buffer(
cx_b: &mut TestAppContext,
) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -2132,7 +2133,7 @@ async fn test_canceling_buffer_opening(
) {
deterministic.forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -2183,7 +2184,7 @@ async fn test_leaving_project(
cx_c: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
@@ -2316,7 +2317,7 @@ async fn test_collaborating_with_diagnostics(
cx_c: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
@@ -2581,7 +2582,7 @@ async fn test_collaborating_with_diagnostics(
#[gpui::test(iterations = 10)]
async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -2755,7 +2756,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
#[gpui::test(iterations = 10)]
async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -2848,7 +2849,7 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te
async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
use project::FormatTrigger;
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -2949,7 +2950,7 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
#[gpui::test(iterations = 10)]
async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -3093,7 +3094,7 @@ async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
#[gpui::test(iterations = 10)]
async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -3194,7 +3195,7 @@ async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
#[gpui::test(iterations = 10)]
async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -3273,7 +3274,7 @@ async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContex
#[gpui::test(iterations = 10)]
async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -3375,7 +3376,7 @@ async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppC
#[gpui::test(iterations = 10)]
async fn test_lsp_hover(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -3478,7 +3479,7 @@ async fn test_lsp_hover(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
#[gpui::test(iterations = 10)]
async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -3586,7 +3587,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
mut rng: StdRng,
) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -3662,7 +3663,7 @@ async fn test_collaborating_with_code_actions(
) {
cx_a.foreground().forbid_parking();
cx_b.update(editor::init);
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -3704,7 +3705,7 @@ async fn test_collaborating_with_code_actions(
cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), true, cx)
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
})
.await
.unwrap()
@@ -3873,7 +3874,7 @@ async fn test_collaborating_with_code_actions(
async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
cx_b.update(editor::init);
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -3925,7 +3926,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "one.rs"), true, cx)
workspace.open_path((worktree_id, "one.rs"), None, true, cx)
})
.await
.unwrap()
@@ -4065,7 +4066,7 @@ async fn test_language_server_statuses(
deterministic.forbid_parking();
cx_b.update(editor::init);
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -4169,415 +4170,6 @@ async fn test_language_server_statuses(
});
}
#[gpui::test(iterations = 10)]
async fn test_basic_chat(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
// Create an org that includes these 2 users.
let db = &server.app_state.db;
let org_id = db.create_org("Test Org", "test-org").await.unwrap();
db.add_org_member(org_id, client_a.current_user_id(cx_a), false)
.await
.unwrap();
db.add_org_member(org_id, client_b.current_user_id(cx_b), false)
.await
.unwrap();
// Create a channel that includes all the users.
let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
db.add_channel_member(channel_id, client_a.current_user_id(cx_a), false)
.await
.unwrap();
db.add_channel_member(channel_id, client_b.current_user_id(cx_b), false)
.await
.unwrap();
db.create_channel_message(
channel_id,
client_b.current_user_id(cx_b),
"hello A, it's B.",
OffsetDateTime::now_utc(),
1,
)
.await
.unwrap();
let channels_a =
cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
channels_a
.condition(cx_a, |list, _| list.available_channels().is_some())
.await;
channels_a.read_with(cx_a, |list, _| {
assert_eq!(
list.available_channels().unwrap(),
&[ChannelDetails {
id: channel_id.to_proto(),
name: "test-channel".to_string()
}]
)
});
let channel_a = channels_a.update(cx_a, |this, cx| {
this.get_channel(channel_id.to_proto(), cx).unwrap()
});
channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
channel_a
.condition(cx_a, |channel, _| {
channel_messages(channel)
== [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
})
.await;
let channels_b =
cx_b.add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
channels_b
.condition(cx_b, |list, _| list.available_channels().is_some())
.await;
channels_b.read_with(cx_b, |list, _| {
assert_eq!(
list.available_channels().unwrap(),
&[ChannelDetails {
id: channel_id.to_proto(),
name: "test-channel".to_string()
}]
)
});
let channel_b = channels_b.update(cx_b, |this, cx| {
this.get_channel(channel_id.to_proto(), cx).unwrap()
});
channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
channel_b
.condition(cx_b, |channel, _| {
channel_messages(channel)
== [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
})
.await;
channel_a
.update(cx_a, |channel, cx| {
channel
.send_message("oh, hi B.".to_string(), cx)
.unwrap()
.detach();
let task = channel.send_message("sup".to_string(), cx).unwrap();
assert_eq!(
channel_messages(channel),
&[
("user_b".to_string(), "hello A, it's B.".to_string(), false),
("user_a".to_string(), "oh, hi B.".to_string(), true),
("user_a".to_string(), "sup".to_string(), true)
]
);
task
})
.await
.unwrap();
channel_b
.condition(cx_b, |channel, _| {
channel_messages(channel)
== [
("user_b".to_string(), "hello A, it's B.".to_string(), false),
("user_a".to_string(), "oh, hi B.".to_string(), false),
("user_a".to_string(), "sup".to_string(), false),
]
})
.await;
assert_eq!(
server
.store()
.await
.channel(channel_id)
.unwrap()
.connection_ids
.len(),
2
);
cx_b.update(|_| drop(channel_b));
server
.condition(|state| state.channel(channel_id).unwrap().connection_ids.len() == 1)
.await;
cx_a.update(|_| drop(channel_a));
server
.condition(|state| state.channel(channel_id).is_none())
.await;
}
#[gpui::test(iterations = 10)]
async fn test_chat_message_validation(cx_a: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let db = &server.app_state.db;
let org_id = db.create_org("Test Org", "test-org").await.unwrap();
let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
db.add_org_member(org_id, client_a.current_user_id(cx_a), false)
.await
.unwrap();
db.add_channel_member(channel_id, client_a.current_user_id(cx_a), false)
.await
.unwrap();
let channels_a =
cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
channels_a
.condition(cx_a, |list, _| list.available_channels().is_some())
.await;
let channel_a = channels_a.update(cx_a, |this, cx| {
this.get_channel(channel_id.to_proto(), cx).unwrap()
});
// Messages aren't allowed to be too long.
channel_a
.update(cx_a, |channel, cx| {
let long_body = "this is long.\n".repeat(1024);
channel.send_message(long_body, cx).unwrap()
})
.await
.unwrap_err();
// Messages aren't allowed to be blank.
channel_a.update(cx_a, |channel, cx| {
channel.send_message(String::new(), cx).unwrap_err()
});
// Leading and trailing whitespace are trimmed.
channel_a
.update(cx_a, |channel, cx| {
channel
.send_message("\n surrounded by whitespace \n".to_string(), cx)
.unwrap()
})
.await
.unwrap();
assert_eq!(
db.get_channel_messages(channel_id, 10, None)
.await
.unwrap()
.iter()
.map(|m| &m.body)
.collect::<Vec<_>>(),
&["surrounded by whitespace"]
);
}
#[gpui::test(iterations = 10)]
async fn test_chat_reconnection(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let mut status_b = client_b.status();
// Create an org that includes these 2 users.
let db = &server.app_state.db;
let org_id = db.create_org("Test Org", "test-org").await.unwrap();
db.add_org_member(org_id, client_a.current_user_id(cx_a), false)
.await
.unwrap();
db.add_org_member(org_id, client_b.current_user_id(cx_b), false)
.await
.unwrap();
// Create a channel that includes all the users.
let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
db.add_channel_member(channel_id, client_a.current_user_id(cx_a), false)
.await
.unwrap();
db.add_channel_member(channel_id, client_b.current_user_id(cx_b), false)
.await
.unwrap();
db.create_channel_message(
channel_id,
client_b.current_user_id(cx_b),
"hello A, it's B.",
OffsetDateTime::now_utc(),
2,
)
.await
.unwrap();
let channels_a =
cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
channels_a
.condition(cx_a, |list, _| list.available_channels().is_some())
.await;
channels_a.read_with(cx_a, |list, _| {
assert_eq!(
list.available_channels().unwrap(),
&[ChannelDetails {
id: channel_id.to_proto(),
name: "test-channel".to_string()
}]
)
});
let channel_a = channels_a.update(cx_a, |this, cx| {
this.get_channel(channel_id.to_proto(), cx).unwrap()
});
channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
channel_a
.condition(cx_a, |channel, _| {
channel_messages(channel)
== [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
})
.await;
let channels_b =
cx_b.add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
channels_b
.condition(cx_b, |list, _| list.available_channels().is_some())
.await;
channels_b.read_with(cx_b, |list, _| {
assert_eq!(
list.available_channels().unwrap(),
&[ChannelDetails {
id: channel_id.to_proto(),
name: "test-channel".to_string()
}]
)
});
let channel_b = channels_b.update(cx_b, |this, cx| {
this.get_channel(channel_id.to_proto(), cx).unwrap()
});
channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
channel_b
.condition(cx_b, |channel, _| {
channel_messages(channel)
== [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
})
.await;
// Disconnect client B, ensuring we can still access its cached channel data.
server.forbid_connections();
server.disconnect_client(client_b.peer_id().unwrap());
cx_b.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
while !matches!(
status_b.next().await,
Some(client::Status::ReconnectionError { .. })
) {}
channels_b.read_with(cx_b, |channels, _| {
assert_eq!(
channels.available_channels().unwrap(),
[ChannelDetails {
id: channel_id.to_proto(),
name: "test-channel".to_string()
}]
)
});
channel_b.read_with(cx_b, |channel, _| {
assert_eq!(
channel_messages(channel),
[("user_b".to_string(), "hello A, it's B.".to_string(), false)]
)
});
// Send a message from client B while it is disconnected.
channel_b
.update(cx_b, |channel, cx| {
let task = channel
.send_message("can you see this?".to_string(), cx)
.unwrap();
assert_eq!(
channel_messages(channel),
&[
("user_b".to_string(), "hello A, it's B.".to_string(), false),
("user_b".to_string(), "can you see this?".to_string(), true)
]
);
task
})
.await
.unwrap_err();
// Send a message from client A while B is disconnected.
channel_a
.update(cx_a, |channel, cx| {
channel
.send_message("oh, hi B.".to_string(), cx)
.unwrap()
.detach();
let task = channel.send_message("sup".to_string(), cx).unwrap();
assert_eq!(
channel_messages(channel),
&[
("user_b".to_string(), "hello A, it's B.".to_string(), false),
("user_a".to_string(), "oh, hi B.".to_string(), true),
("user_a".to_string(), "sup".to_string(), true)
]
);
task
})
.await
.unwrap();
// Give client B a chance to reconnect.
server.allow_connections();
cx_b.foreground().advance_clock(Duration::from_secs(10));
// Verify that B sees the new messages upon reconnection, as well as the message client B
// sent while offline.
channel_b
.condition(cx_b, |channel, _| {
channel_messages(channel)
== [
("user_b".to_string(), "hello A, it's B.".to_string(), false),
("user_a".to_string(), "oh, hi B.".to_string(), false),
("user_a".to_string(), "sup".to_string(), false),
("user_b".to_string(), "can you see this?".to_string(), false),
]
})
.await;
// Ensure client A and B can communicate normally after reconnection.
channel_a
.update(cx_a, |channel, cx| {
channel.send_message("you online?".to_string(), cx).unwrap()
})
.await
.unwrap();
channel_b
.condition(cx_b, |channel, _| {
channel_messages(channel)
== [
("user_b".to_string(), "hello A, it's B.".to_string(), false),
("user_a".to_string(), "oh, hi B.".to_string(), false),
("user_a".to_string(), "sup".to_string(), false),
("user_b".to_string(), "can you see this?".to_string(), false),
("user_a".to_string(), "you online?".to_string(), false),
]
})
.await;
channel_b
.update(cx_b, |channel, cx| {
channel.send_message("yep".to_string(), cx).unwrap()
})
.await
.unwrap();
channel_a
.condition(cx_a, |channel, _| {
channel_messages(channel)
== [
("user_b".to_string(), "hello A, it's B.".to_string(), false),
("user_a".to_string(), "oh, hi B.".to_string(), false),
("user_a".to_string(), "sup".to_string(), false),
("user_b".to_string(), "can you see this?".to_string(), false),
("user_a".to_string(), "you online?".to_string(), false),
("user_b".to_string(), "yep".to_string(), false),
]
})
.await;
}
#[gpui::test(iterations = 10)]
async fn test_contacts(
deterministic: Arc<Deterministic>,
@@ -4586,7 +4178,7 @@ async fn test_contacts(
cx_c: &mut TestAppContext,
) {
cx_a.foreground().forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
@@ -4912,7 +4504,7 @@ async fn test_contact_requests(
cx_a.foreground().forbid_parking();
// Connect to a server as 3 clients.
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_a2 = server.create_client(cx_a2, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
@@ -5093,7 +4685,7 @@ async fn test_following(
cx_a.update(editor::init);
cx_b.update(editor::init);
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -5134,7 +4726,7 @@ async fn test_following(
let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
let editor_a1 = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), true, cx)
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
})
.await
.unwrap()
@@ -5142,7 +4734,7 @@ async fn test_following(
.unwrap();
let editor_a2 = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "2.txt"), true, cx)
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
})
.await
.unwrap()
@@ -5153,7 +4745,7 @@ async fn test_following(
let workspace_b = client_b.build_workspace(&project_b, cx_b);
let editor_b1 = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), true, cx)
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
})
.await
.unwrap()
@@ -5367,7 +4959,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
cx_a.update(editor::init);
cx_b.update(editor::init);
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -5411,7 +5003,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
let _editor_a1 = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), true, cx)
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
})
.await
.unwrap()
@@ -5423,7 +5015,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
let _editor_b1 = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "2.txt"), true, cx)
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
})
.await
.unwrap()
@@ -5474,7 +5066,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "3.txt"), true, cx)
workspace.open_path((worktree_id, "3.txt"), None, true, cx)
})
.await
.unwrap();
@@ -5485,7 +5077,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
workspace_b
.update(cx_b, |workspace, cx| {
assert_eq!(*workspace.active_pane(), pane_b1);
workspace.open_path((worktree_id, "4.txt"), true, cx)
workspace.open_path((worktree_id, "4.txt"), None, true, cx)
})
.await
.unwrap();
@@ -5545,7 +5137,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
cx_b.update(editor::init);
// 2 clients connect to a server.
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -5586,7 +5178,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
let workspace_a = client_a.build_workspace(&project_a, cx_a);
let _editor_a1 = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), true, cx)
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
})
.await
.unwrap()
@@ -5699,7 +5291,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
// When client B activates a different item in the original pane, it automatically stops following client A.
workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "2.txt"), true, cx)
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
})
.await
.unwrap();
@@ -5719,7 +5311,7 @@ async fn test_peers_simultaneously_following_each_other(
cx_a.update(editor::init);
cx_b.update(editor::init);
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut server = TestServer::start(cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
@@ -5789,7 +5381,7 @@ async fn test_random_collaboration(
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
let mut server = TestServer::start(cx.foreground(), cx.background()).await;
let mut server = TestServer::start(cx.background()).await;
let db = server.app_state.db.clone();
let mut available_guests = Vec::new();
@@ -5987,6 +5579,13 @@ async fn test_random_collaboration(
guest_client.username,
id
);
assert_eq!(
guest_snapshot.abs_path(),
host_snapshot.abs_path(),
"{} has different abs path than the host for worktree {}",
guest_client.username,
id
);
assert_eq!(
guest_snapshot.entries(false).collect::<Vec<_>>(),
host_snapshot.entries(false).collect::<Vec<_>>(),
@@ -6076,8 +5675,6 @@ struct TestServer {
peer: Arc<Peer>,
app_state: Arc<AppState>,
server: Arc<Server>,
foreground: Rc<executor::Foreground>,
notifications: mpsc::UnboundedReceiver<()>,
connection_killers: Arc<Mutex<HashMap<PeerId, Arc<AtomicBool>>>>,
forbid_connections: Arc<AtomicBool>,
_test_db: TestDb,
@@ -6085,13 +5682,10 @@ struct TestServer {
}
impl TestServer {
async fn start(
foreground: Rc<executor::Foreground>,
background: Arc<executor::Background>,
) -> Self {
async fn start(background: Arc<executor::Background>) -> Self {
static NEXT_LIVE_KIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0);
let test_db = TestDb::fake(background.clone());
let test_db = TestDb::new(background.clone());
let live_kit_server_id = NEXT_LIVE_KIT_SERVER_ID.fetch_add(1, SeqCst);
let live_kit_server = live_kit_client::TestServer::create(
format!("http://livekit.{}.test", live_kit_server_id),
@@ -6102,14 +5696,11 @@ impl TestServer {
.unwrap();
let app_state = Self::build_app_state(&test_db, &live_kit_server).await;
let peer = Peer::new();
let notifications = mpsc::unbounded();
let server = Server::new(app_state.clone(), Some(notifications.0));
let server = Server::new(app_state.clone());
Self {
peer,
app_state,
server,
foreground,
notifications: notifications.1,
connection_killers: Default::default(),
forbid_connections: Default::default(),
_test_db: test_db,
@@ -6147,7 +5738,7 @@ impl TestServer {
},
)
.await
.unwrap()
.expect("creating user failed")
.user_id
};
let client_name = name.to_string();
@@ -6187,7 +5778,11 @@ impl TestServer {
let (client_conn, server_conn, killed) =
Connection::in_memory(cx.background());
let (connection_id_tx, connection_id_rx) = oneshot::channel();
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
let user = db
.get_user_by_id(user_id)
.await
.expect("retrieving user failed")
.unwrap();
cx.background()
.spawn(server.handle_connection(
server_conn,
@@ -6221,7 +5816,6 @@ impl TestServer {
default_item_factory: |_, _| unimplemented!(),
});
Channel::init(&client);
Project::init(&client);
cx.update(|cx| {
workspace::init(app_state.clone(), cx);
@@ -6322,21 +5916,6 @@ impl TestServer {
config: Default::default(),
})
}
async fn condition<F>(&mut self, mut predicate: F)
where
F: FnMut(&Store) -> bool,
{
assert!(
self.foreground.parking_forbidden(),
"you must call forbid_parking to use server conditions so we don't block indefinitely"
);
while !(predicate)(&*self.server.store.lock().await) {
self.foreground.start_waiting();
self.notifications.next().await;
self.foreground.finish_waiting();
}
}
}
impl Deref for TestServer {
@@ -7052,20 +6631,6 @@ impl Executor for Arc<gpui::executor::Background> {
}
}
fn channel_messages(channel: &Channel) -> Vec<(String, String, bool)> {
channel
.messages()
.cursor::<()>()
.map(|m| {
(
m.sender.github_login.clone(),
m.body.clone(),
m.is_pending(),
)
})
.collect()
}
#[derive(Debug, Eq, PartialEq)]
struct RoomParticipants {
remote: Vec<String>,

View File

@@ -13,12 +13,12 @@ use crate::rpc::ResultExt as _;
use anyhow::anyhow;
use axum::{routing::get, Router};
use collab::{Error, Result};
use db::{Db, PostgresDb};
use db::DefaultDb as Db;
use serde::Deserialize;
use std::{
env::args,
net::{SocketAddr, TcpListener},
path::PathBuf,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
@@ -49,14 +49,14 @@ pub struct MigrateConfig {
}
pub struct AppState {
db: Arc<dyn Db>,
db: Arc<Db>,
live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
config: Config,
}
impl AppState {
async fn new(config: Config) -> Result<Arc<Self>> {
let db = PostgresDb::new(&config.database_url, 5).await?;
let db = Db::new(&config.database_url, 5).await?;
let live_kit_client = if let Some(((server, key), secret)) = config
.live_kit_server
.as_ref()
@@ -96,13 +96,12 @@ async fn main() -> Result<()> {
}
Some("migrate") => {
let config = envy::from_env::<MigrateConfig>().expect("error loading config");
let db = PostgresDb::new(&config.database_url, 5).await?;
let db = Db::new(&config.database_url, 5).await?;
let migrations_path = config
.migrations_path
.as_deref()
.or(db::DEFAULT_MIGRATIONS_PATH.map(|s| s.as_ref()))
.ok_or_else(|| anyhow!("missing MIGRATIONS_PATH environment variable"))?;
.unwrap_or_else(|| Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/migrations")));
let migrations = db.migrate(&migrations_path, false).await?;
for (migration, duration) in migrations {
@@ -122,9 +121,7 @@ async fn main() -> Result<()> {
let listener = TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port))
.expect("failed to bind TCP listener");
let rpc_server = rpc::Server::new(state.clone(), None);
rpc_server
.start_recording_project_activity(Duration::from_secs(5 * 60), rpc::RealExecutor);
let rpc_server = rpc::Server::new(state.clone());
let app = api::routes(rpc_server.clone(), state.clone())
.merge(rpc::routes(rpc_server.clone()))

View File

@@ -2,7 +2,7 @@ mod store;
use crate::{
auth,
db::{self, ChannelId, MessageId, ProjectId, User, UserId},
db::{self, ProjectId, User, UserId},
AppState, Result,
};
use anyhow::anyhow;
@@ -24,7 +24,7 @@ use axum::{
};
use collections::{HashMap, HashSet};
use futures::{
channel::{mpsc, oneshot},
channel::oneshot,
future::{self, BoxFuture},
stream::FuturesUnordered,
FutureExt, SinkExt, StreamExt, TryStreamExt,
@@ -42,7 +42,6 @@ use std::{
marker::PhantomData,
net::SocketAddr,
ops::{Deref, DerefMut},
os::unix::prelude::OsStrExt,
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
@@ -51,7 +50,6 @@ use std::{
time::Duration,
};
pub use store::{Store, Worktree};
use time::OffsetDateTime;
use tokio::{
sync::{Mutex, MutexGuard},
time::Sleep,
@@ -62,10 +60,6 @@ use tracing::{info_span, instrument, Instrument};
lazy_static! {
static ref METRIC_CONNECTIONS: IntGauge =
register_int_gauge!("connections", "number of connections").unwrap();
static ref METRIC_REGISTERED_PROJECTS: IntGauge =
register_int_gauge!("registered_projects", "number of registered projects").unwrap();
static ref METRIC_ACTIVE_PROJECTS: IntGauge =
register_int_gauge!("active_projects", "number of active projects").unwrap();
static ref METRIC_SHARED_PROJECTS: IntGauge = register_int_gauge!(
"shared_projects",
"number of open projects with one or more guests"
@@ -95,7 +89,6 @@ pub struct Server {
pub(crate) store: Mutex<Store>,
app_state: Arc<AppState>,
handlers: HashMap<TypeId, MessageHandler>,
notifications: Option<mpsc::UnboundedSender<()>>,
}
pub trait Executor: Send + Clone {
@@ -107,9 +100,6 @@ pub trait Executor: Send + Clone {
#[derive(Clone)]
pub struct RealExecutor;
const MESSAGE_COUNT_PER_PAGE: usize = 100;
const MAX_MESSAGE_LEN: usize = 1024;
pub(crate) struct StoreGuard<'a> {
guard: MutexGuard<'a, Store>,
_not_send: PhantomData<Rc<()>>,
@@ -132,16 +122,12 @@ where
}
impl Server {
pub fn new(
app_state: Arc<AppState>,
notifications: Option<mpsc::UnboundedSender<()>>,
) -> Arc<Self> {
pub fn new(app_state: Arc<AppState>) -> Arc<Self> {
let mut server = Self {
peer: Peer::new(),
app_state,
store: Default::default(),
handlers: Default::default(),
notifications,
};
server
@@ -158,9 +144,7 @@ impl Server {
.add_request_handler(Server::join_project)
.add_message_handler(Server::leave_project)
.add_message_handler(Server::update_project)
.add_message_handler(Server::register_project_activity)
.add_request_handler(Server::update_worktree)
.add_message_handler(Server::update_worktree_extensions)
.add_message_handler(Server::start_language_server)
.add_message_handler(Server::update_language_server)
.add_message_handler(Server::update_diagnostic_summary)
@@ -194,19 +178,14 @@ impl Server {
.add_message_handler(Server::buffer_reloaded)
.add_message_handler(Server::buffer_saved)
.add_request_handler(Server::save_buffer)
.add_request_handler(Server::get_channels)
.add_request_handler(Server::get_users)
.add_request_handler(Server::fuzzy_search_users)
.add_request_handler(Server::request_contact)
.add_request_handler(Server::remove_contact)
.add_request_handler(Server::respond_to_contact_request)
.add_request_handler(Server::join_channel)
.add_message_handler(Server::leave_channel)
.add_request_handler(Server::send_channel_message)
.add_request_handler(Server::follow)
.add_message_handler(Server::unfollow)
.add_message_handler(Server::update_followers)
.add_request_handler(Server::get_channel_messages)
.add_message_handler(Server::update_diff_base)
.add_request_handler(Server::get_private_user_info);
@@ -290,58 +269,6 @@ impl Server {
})
}
/// Start a long lived task that records which users are active in which projects.
pub fn start_recording_project_activity<E: 'static + Executor>(
self: &Arc<Self>,
interval: Duration,
executor: E,
) {
executor.spawn_detached({
let this = Arc::downgrade(self);
let executor = executor.clone();
async move {
let mut period_start = OffsetDateTime::now_utc();
let mut active_projects = Vec::<(UserId, ProjectId)>::new();
loop {
let sleep = executor.sleep(interval);
sleep.await;
let this = if let Some(this) = this.upgrade() {
this
} else {
break;
};
active_projects.clear();
active_projects.extend(this.store().await.projects().flat_map(
|(project_id, project)| {
project.guests.values().chain([&project.host]).filter_map(
|collaborator| {
if !collaborator.admin
&& collaborator
.last_activity
.map_or(false, |activity| activity > period_start)
{
Some((collaborator.user_id, *project_id))
} else {
None
}
},
)
},
));
let period_end = OffsetDateTime::now_utc();
this.app_state
.db
.record_user_activity(period_start..period_end, &active_projects)
.await
.trace_err();
period_start = period_end;
}
}
});
}
pub fn handle_connection<E: Executor>(
self: &Arc<Self>,
connection: Connection,
@@ -432,18 +359,11 @@ impl Server {
let span = tracing::info_span!("receive message", %user_id, %login, %connection_id, %address, type_name);
let span_enter = span.enter();
if let Some(handler) = this.handlers.get(&message.payload_type_id()) {
let notifications = this.notifications.clone();
let is_background = message.is_background();
let handle_message = (handler)(this.clone(), message);
drop(span_enter);
let handle_message = async move {
handle_message.await;
if let Some(mut notifications) = notifications {
let _ = notifications.send(()).await;
}
}.instrument(span);
let handle_message = handle_message.instrument(span);
if is_background {
executor.spawn_detached(handle_message);
} else {
@@ -1024,7 +944,7 @@ impl Server {
id: *id,
root_name: worktree.root_name.clone(),
visible: worktree.visible,
abs_path: worktree.abs_path.as_os_str().as_bytes().to_vec(),
abs_path: worktree.abs_path.clone(),
})
.collect::<Vec<_>>();
@@ -1075,7 +995,7 @@ impl Server {
let message = proto::UpdateWorktree {
project_id: project_id.to_proto(),
worktree_id: *worktree_id,
abs_path: worktree.abs_path.as_os_str().as_bytes().to_vec(),
abs_path: worktree.abs_path.clone(),
root_name: worktree.root_name.clone(),
updated_entries: worktree.entries.values().cloned().collect(),
removed_entries: Default::default(),
@@ -1172,17 +1092,6 @@ impl Server {
Ok(())
}
async fn register_project_activity(
self: Arc<Server>,
request: TypedEnvelope<proto::RegisterProjectActivity>,
) -> Result<()> {
self.store().await.register_project_activity(
ProjectId::from_proto(request.payload.project_id),
request.sender_id,
)?;
Ok(())
}
async fn update_worktree(
self: Arc<Server>,
request: TypedEnvelope<proto::UpdateWorktree>,
@@ -1195,6 +1104,7 @@ impl Server {
project_id,
worktree_id,
&request.payload.root_name,
&request.payload.abs_path,
&request.payload.removed_entries,
&request.payload.updated_entries,
request.payload.scan_id,
@@ -1209,25 +1119,6 @@ impl Server {
Ok(())
}
async fn update_worktree_extensions(
self: Arc<Server>,
request: TypedEnvelope<proto::UpdateWorktreeExtensions>,
) -> Result<()> {
let project_id = ProjectId::from_proto(request.payload.project_id);
let worktree_id = request.payload.worktree_id;
let extensions = request
.payload
.extensions
.into_iter()
.zip(request.payload.counts)
.collect();
self.app_state
.db
.update_worktree_extensions(project_id, worktree_id, extensions)
.await?;
Ok(())
}
async fn update_diagnostic_summary(
self: Arc<Server>,
request: TypedEnvelope<proto::UpdateDiagnosticSummary>,
@@ -1363,8 +1254,7 @@ impl Server {
) -> Result<()> {
let project_id = ProjectId::from_proto(request.payload.project_id);
let receiver_ids = {
let mut store = self.store().await;
store.register_project_activity(project_id, request.sender_id)?;
let store = self.store().await;
store.project_connection_ids(project_id, request.sender_id)?
};
@@ -1430,15 +1320,13 @@ impl Server {
let leader_id = ConnectionId(request.payload.leader_id);
let follower_id = request.sender_id;
{
let mut store = self.store().await;
let store = self.store().await;
if !store
.project_connection_ids(project_id, follower_id)?
.contains(&leader_id)
{
Err(anyhow!("no such peer"))?;
}
store.register_project_activity(project_id, follower_id)?;
}
let mut response_payload = self
@@ -1455,14 +1343,13 @@ impl Server {
async fn unfollow(self: Arc<Self>, request: TypedEnvelope<proto::Unfollow>) -> Result<()> {
let project_id = ProjectId::from_proto(request.payload.project_id);
let leader_id = ConnectionId(request.payload.leader_id);
let mut store = self.store().await;
let store = self.store().await;
if !store
.project_connection_ids(project_id, request.sender_id)?
.contains(&leader_id)
{
Err(anyhow!("no such peer"))?;
}
store.register_project_activity(project_id, request.sender_id)?;
self.peer
.forward_send(request.sender_id, leader_id, request.payload)?;
Ok(())
@@ -1473,8 +1360,7 @@ impl Server {
request: TypedEnvelope<proto::UpdateFollowers>,
) -> Result<()> {
let project_id = ProjectId::from_proto(request.payload.project_id);
let mut store = self.store().await;
store.register_project_activity(project_id, request.sender_id)?;
let store = self.store().await;
let connection_ids = store.project_connection_ids(project_id, request.sender_id)?;
let leader_id = request
.payload
@@ -1495,28 +1381,6 @@ impl Server {
Ok(())
}
async fn get_channels(
self: Arc<Server>,
request: TypedEnvelope<proto::GetChannels>,
response: Response<proto::GetChannels>,
) -> Result<()> {
let user_id = self
.store()
.await
.user_id_for_connection(request.sender_id)?;
let channels = self.app_state.db.get_accessible_channels(user_id).await?;
response.send(proto::GetChannelsResponse {
channels: channels
.into_iter()
.map(|chan| proto::Channel {
id: chan.id.to_proto(),
name: chan.name,
})
.collect(),
})?;
Ok(())
}
async fn get_users(
self: Arc<Server>,
request: TypedEnvelope<proto::GetUsers>,
@@ -1712,175 +1576,6 @@ impl Server {
Ok(())
}
async fn join_channel(
self: Arc<Self>,
request: TypedEnvelope<proto::JoinChannel>,
response: Response<proto::JoinChannel>,
) -> Result<()> {
let user_id = self
.store()
.await
.user_id_for_connection(request.sender_id)?;
let channel_id = ChannelId::from_proto(request.payload.channel_id);
if !self
.app_state
.db
.can_user_access_channel(user_id, channel_id)
.await?
{
Err(anyhow!("access denied"))?;
}
self.store()
.await
.join_channel(request.sender_id, channel_id);
let messages = self
.app_state
.db
.get_channel_messages(channel_id, MESSAGE_COUNT_PER_PAGE, None)
.await?
.into_iter()
.map(|msg| proto::ChannelMessage {
id: msg.id.to_proto(),
body: msg.body,
timestamp: msg.sent_at.unix_timestamp() as u64,
sender_id: msg.sender_id.to_proto(),
nonce: Some(msg.nonce.as_u128().into()),
})
.collect::<Vec<_>>();
response.send(proto::JoinChannelResponse {
done: messages.len() < MESSAGE_COUNT_PER_PAGE,
messages,
})?;
Ok(())
}
async fn leave_channel(
self: Arc<Self>,
request: TypedEnvelope<proto::LeaveChannel>,
) -> Result<()> {
let user_id = self
.store()
.await
.user_id_for_connection(request.sender_id)?;
let channel_id = ChannelId::from_proto(request.payload.channel_id);
if !self
.app_state
.db
.can_user_access_channel(user_id, channel_id)
.await?
{
Err(anyhow!("access denied"))?;
}
self.store()
.await
.leave_channel(request.sender_id, channel_id);
Ok(())
}
async fn send_channel_message(
self: Arc<Self>,
request: TypedEnvelope<proto::SendChannelMessage>,
response: Response<proto::SendChannelMessage>,
) -> Result<()> {
let channel_id = ChannelId::from_proto(request.payload.channel_id);
let user_id;
let connection_ids;
{
let state = self.store().await;
user_id = state.user_id_for_connection(request.sender_id)?;
connection_ids = state.channel_connection_ids(channel_id)?;
}
// Validate the message body.
let body = request.payload.body.trim().to_string();
if body.len() > MAX_MESSAGE_LEN {
return Err(anyhow!("message is too long"))?;
}
if body.is_empty() {
return Err(anyhow!("message can't be blank"))?;
}
let timestamp = OffsetDateTime::now_utc();
let nonce = request
.payload
.nonce
.ok_or_else(|| anyhow!("nonce can't be blank"))?;
let message_id = self
.app_state
.db
.create_channel_message(channel_id, user_id, &body, timestamp, nonce.clone().into())
.await?
.to_proto();
let message = proto::ChannelMessage {
sender_id: user_id.to_proto(),
id: message_id,
body,
timestamp: timestamp.unix_timestamp() as u64,
nonce: Some(nonce),
};
broadcast(request.sender_id, connection_ids, |conn_id| {
self.peer.send(
conn_id,
proto::ChannelMessageSent {
channel_id: channel_id.to_proto(),
message: Some(message.clone()),
},
)
});
response.send(proto::SendChannelMessageResponse {
message: Some(message),
})?;
Ok(())
}
async fn get_channel_messages(
self: Arc<Self>,
request: TypedEnvelope<proto::GetChannelMessages>,
response: Response<proto::GetChannelMessages>,
) -> Result<()> {
let user_id = self
.store()
.await
.user_id_for_connection(request.sender_id)?;
let channel_id = ChannelId::from_proto(request.payload.channel_id);
if !self
.app_state
.db
.can_user_access_channel(user_id, channel_id)
.await?
{
Err(anyhow!("access denied"))?;
}
let messages = self
.app_state
.db
.get_channel_messages(
channel_id,
MESSAGE_COUNT_PER_PAGE,
Some(MessageId::from_proto(request.payload.before_message_id)),
)
.await?
.into_iter()
.map(|msg| proto::ChannelMessage {
id: msg.id.to_proto(),
body: msg.body,
timestamp: msg.sent_at.unix_timestamp() as u64,
sender_id: msg.sender_id.to_proto(),
nonce: Some(msg.nonce.as_u128().into()),
})
.collect::<Vec<_>>();
response.send(proto::GetChannelMessagesResponse {
done: messages.len() < MESSAGE_COUNT_PER_PAGE,
messages,
})?;
Ok(())
}
async fn update_diff_base(
self: Arc<Server>,
request: TypedEnvelope<proto::UpdateDiffBase>,
@@ -2061,11 +1756,8 @@ pub async fn handle_websocket_request(
}
pub async fn handle_metrics(Extension(server): Extension<Arc<Server>>) -> axum::response::Response {
// We call `store_mut` here for its side effects of updating metrics.
let metrics = server.store().await.metrics();
METRIC_CONNECTIONS.set(metrics.connections as _);
METRIC_REGISTERED_PROJECTS.set(metrics.registered_projects as _);
METRIC_ACTIVE_PROJECTS.set(metrics.active_projects as _);
METRIC_SHARED_PROJECTS.set(metrics.shared_projects as _);
let encoder = prometheus::TextEncoder::new();

View File

@@ -1,11 +1,10 @@
use crate::db::{self, ChannelId, ProjectId, UserId};
use crate::db::{self, ProjectId, UserId};
use anyhow::{anyhow, Result};
use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet};
use nanoid::nanoid;
use rpc::{proto, ConnectionId};
use serde::Serialize;
use std::{borrow::Cow, mem, path::PathBuf, str, time::Duration};
use time::OffsetDateTime;
use std::{borrow::Cow, mem, path::PathBuf, str};
use tracing::instrument;
use util::post_inc;
@@ -18,8 +17,6 @@ pub struct Store {
next_room_id: RoomId,
rooms: BTreeMap<RoomId, proto::Room>,
projects: BTreeMap<ProjectId, Project>,
#[serde(skip)]
channels: BTreeMap<ChannelId, Channel>,
}
#[derive(Default, Serialize)]
@@ -33,7 +30,6 @@ struct ConnectionState {
user_id: UserId,
admin: bool,
projects: BTreeSet<ProjectId>,
channels: HashSet<ChannelId>,
}
#[derive(Copy, Clone, Eq, PartialEq, Serialize)]
@@ -60,14 +56,12 @@ pub struct Project {
pub struct Collaborator {
pub replica_id: ReplicaId,
pub user_id: UserId,
#[serde(skip)]
pub last_activity: Option<OffsetDateTime>,
pub admin: bool,
}
#[derive(Default, Serialize)]
pub struct Worktree {
pub abs_path: PathBuf,
pub abs_path: Vec<u8>,
pub root_name: String,
pub visible: bool,
#[serde(skip)]
@@ -78,11 +72,6 @@ pub struct Worktree {
pub is_complete: bool,
}
#[derive(Default)]
pub struct Channel {
pub connection_ids: HashSet<ConnectionId>,
}
pub type ReplicaId = u16;
#[derive(Default)]
@@ -113,38 +102,23 @@ pub struct LeftRoom<'a> {
#[derive(Copy, Clone)]
pub struct Metrics {
pub connections: usize,
pub registered_projects: usize,
pub active_projects: usize,
pub shared_projects: usize,
}
impl Store {
pub fn metrics(&self) -> Metrics {
const ACTIVE_PROJECT_TIMEOUT: Duration = Duration::from_secs(60);
let active_window_start = OffsetDateTime::now_utc() - ACTIVE_PROJECT_TIMEOUT;
let connections = self.connections.values().filter(|c| !c.admin).count();
let mut registered_projects = 0;
let mut active_projects = 0;
let mut shared_projects = 0;
for project in self.projects.values() {
if let Some(connection) = self.connections.get(&project.host_connection_id) {
if !connection.admin {
registered_projects += 1;
if project.is_active_since(active_window_start) {
active_projects += 1;
if !project.guests.is_empty() {
shared_projects += 1;
}
}
shared_projects += 1;
}
}
}
Metrics {
connections,
registered_projects,
active_projects,
shared_projects,
}
}
@@ -162,7 +136,6 @@ impl Store {
user_id,
admin,
projects: Default::default(),
channels: Default::default(),
},
);
let connected_user = self.connected_users.entry(user_id).or_default();
@@ -201,18 +174,12 @@ impl Store {
.ok_or_else(|| anyhow!("no such connection"))?;
let user_id = connection.user_id;
let connection_channels = mem::take(&mut connection.channels);
let mut result = RemovedConnectionState {
user_id,
..Default::default()
};
// Leave all channels.
for channel_id in connection_channels {
self.leave_channel(connection_id, channel_id);
}
let connected_user = self.connected_users.get(&user_id).unwrap();
if let Some(active_call) = connected_user.active_call.as_ref() {
let room_id = active_call.room_id;
@@ -238,34 +205,6 @@ impl Store {
Ok(result)
}
#[cfg(test)]
pub fn channel(&self, id: ChannelId) -> Option<&Channel> {
self.channels.get(&id)
}
pub fn join_channel(&mut self, connection_id: ConnectionId, channel_id: ChannelId) {
if let Some(connection) = self.connections.get_mut(&connection_id) {
connection.channels.insert(channel_id);
self.channels
.entry(channel_id)
.or_default()
.connection_ids
.insert(connection_id);
}
}
pub fn leave_channel(&mut self, connection_id: ConnectionId, channel_id: ChannelId) {
if let Some(connection) = self.connections.get_mut(&connection_id) {
connection.channels.remove(&channel_id);
if let btree_map::Entry::Occupied(mut entry) = self.channels.entry(channel_id) {
entry.get_mut().connection_ids.remove(&connection_id);
if entry.get_mut().connection_ids.is_empty() {
entry.remove();
}
}
}
}
pub fn user_id_for_connection(&self, connection_id: ConnectionId) -> Result<UserId> {
Ok(self
.connections
@@ -760,7 +699,6 @@ impl Store {
host: Collaborator {
user_id: connection.user_id,
replica_id: 0,
last_activity: None,
admin: connection.admin,
},
guests: Default::default(),
@@ -773,7 +711,11 @@ impl Store {
Worktree {
root_name: worktree.root_name,
visible: worktree.visible,
..Default::default()
abs_path: worktree.abs_path.clone(),
entries: Default::default(),
diagnostic_summaries: Default::default(),
scan_id: Default::default(),
is_complete: Default::default(),
},
)
})
@@ -852,7 +794,11 @@ impl Store {
Worktree {
root_name: worktree.root_name.clone(),
visible: worktree.visible,
..Default::default()
abs_path: worktree.abs_path.clone(),
entries: Default::default(),
diagnostic_summaries: Default::default(),
scan_id: Default::default(),
is_complete: false,
},
);
}
@@ -959,12 +905,10 @@ impl Store {
Collaborator {
replica_id,
user_id: connection.user_id,
last_activity: Some(OffsetDateTime::now_utc()),
admin: connection.admin,
},
);
project.host.last_activity = Some(OffsetDateTime::now_utc());
Ok((project, replica_id))
}
@@ -1006,6 +950,7 @@ impl Store {
project_id: ProjectId,
worktree_id: u64,
worktree_root_name: &str,
worktree_abs_path: &[u8],
removed_entries: &[u64],
updated_entries: &[proto::Entry],
scan_id: u64,
@@ -1016,6 +961,7 @@ impl Store {
let connection_ids = project.connection_ids();
let mut worktree = project.worktrees.entry(worktree_id).or_default();
worktree.root_name = worktree_root_name.to_string();
worktree.abs_path = worktree_abs_path.to_vec();
for entry_id in removed_entries {
worktree.entries.remove(entry_id);
@@ -1056,44 +1002,12 @@ impl Store {
.connection_ids())
}
pub fn channel_connection_ids(&self, channel_id: ChannelId) -> Result<Vec<ConnectionId>> {
Ok(self
.channels
.get(&channel_id)
.ok_or_else(|| anyhow!("no such channel"))?
.connection_ids())
}
pub fn project(&self, project_id: ProjectId) -> Result<&Project> {
self.projects
.get(&project_id)
.ok_or_else(|| anyhow!("no such project"))
}
pub fn register_project_activity(
&mut self,
project_id: ProjectId,
connection_id: ConnectionId,
) -> Result<()> {
let project = self
.projects
.get_mut(&project_id)
.ok_or_else(|| anyhow!("no such project"))?;
let collaborator = if connection_id == project.host_connection_id {
&mut project.host
} else if let Some(guest) = project.guests.get_mut(&connection_id) {
guest
} else {
return Err(anyhow!("no such project"))?;
};
collaborator.last_activity = Some(OffsetDateTime::now_utc());
Ok(())
}
pub fn projects(&self) -> impl Iterator<Item = (&ProjectId, &Project)> {
self.projects.iter()
}
pub fn read_project(
&self,
project_id: ProjectId,
@@ -1154,10 +1068,7 @@ impl Store {
}
}
}
for channel_id in &connection.channels {
let channel = self.channels.get(channel_id).unwrap();
assert!(channel.connection_ids.contains(connection_id));
}
assert!(self
.connected_users
.get(&connection.user_id)
@@ -1253,28 +1164,10 @@ impl Store {
"project was not shared in room"
);
}
for (channel_id, channel) in &self.channels {
for connection_id in &channel.connection_ids {
let connection = self.connections.get(connection_id).unwrap();
assert!(connection.channels.contains(channel_id));
}
}
}
}
impl Project {
fn is_active_since(&self, start_time: OffsetDateTime) -> bool {
self.guests
.values()
.chain([&self.host])
.any(|collaborator| {
collaborator
.last_activity
.map_or(false, |active_time| active_time > start_time)
})
}
pub fn guest_connection_ids(&self) -> Vec<ConnectionId> {
self.guests.keys().copied().collect()
}
@@ -1287,9 +1180,3 @@ impl Project {
.collect()
}
}
impl Channel {
fn connection_ids(&self) -> Vec<ConnectionId> {
self.connection_ids.iter().copied().collect()
}
}

View File

@@ -452,7 +452,7 @@ impl ProjectDiagnosticsEditor {
} else {
groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
new_excerpt_ids_by_selection_id =
editor.change_selections(Some(Autoscroll::Fit), cx, |s| s.refresh());
editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh());
selections = editor.selections.all::<usize>(cx);
}

View File

@@ -12,4 +12,4 @@ collections = { path = "../collections" }
gpui = { path = "../gpui" }
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }

View File

@@ -2,29 +2,55 @@ use std::{any::Any, rc::Rc};
use collections::HashSet;
use gpui::{
elements::{MouseEventHandler, Overlay},
geometry::vector::Vector2F,
scene::MouseDrag,
elements::{Empty, MouseEventHandler, Overlay},
geometry::{rect::RectF, vector::Vector2F},
scene::{MouseDown, MouseDrag},
CursorStyle, Element, ElementBox, EventContext, MouseButton, MutableAppContext, RenderContext,
View, WeakViewHandle,
};
struct State<V: View> {
window_id: usize,
position: Vector2F,
region_offset: Vector2F,
payload: Rc<dyn Any + 'static>,
render: Rc<dyn Fn(Rc<dyn Any>, &mut RenderContext<V>) -> ElementBox>,
enum State<V: View> {
Down {
region_offset: Vector2F,
region: RectF,
},
Dragging {
window_id: usize,
position: Vector2F,
region_offset: Vector2F,
region: RectF,
payload: Rc<dyn Any + 'static>,
render: Rc<dyn Fn(Rc<dyn Any>, &mut RenderContext<V>) -> ElementBox>,
},
Canceled,
}
impl<V: View> Clone for State<V> {
fn clone(&self) -> Self {
Self {
window_id: self.window_id.clone(),
position: self.position.clone(),
region_offset: self.region_offset.clone(),
payload: self.payload.clone(),
render: self.render.clone(),
match self {
&State::Down {
region_offset,
region,
} => State::Down {
region_offset,
region,
},
State::Dragging {
window_id,
position,
region_offset,
region,
payload,
render,
} => Self::Dragging {
window_id: window_id.clone(),
position: position.clone(),
region_offset: region_offset.clone(),
region: region.clone(),
payload: payload.clone(),
render: render.clone(),
},
State::Canceled => State::Canceled,
}
}
}
@@ -49,24 +75,36 @@ impl<V: View> DragAndDrop<V> {
}
pub fn currently_dragged<T: Any>(&self, window_id: usize) -> Option<(Vector2F, Rc<T>)> {
self.currently_dragged.as_ref().and_then(
|State {
position,
payload,
window_id: window_dragged_from,
..
}| {
self.currently_dragged.as_ref().and_then(|state| {
if let State::Dragging {
position,
payload,
window_id: window_dragged_from,
..
} = state
{
if &window_id != window_dragged_from {
return None;
}
payload
.clone()
.downcast::<T>()
.ok()
.is::<T>()
.then(|| payload.clone().downcast::<T>().ok())
.flatten()
.map(|payload| (position.clone(), payload))
},
)
} else {
None
}
})
}
pub fn drag_started(event: MouseDown, cx: &mut EventContext) {
cx.update_global(|this: &mut Self, _| {
this.currently_dragged = Some(State::Down {
region_offset: event.region.origin() - event.position,
region: event.region,
});
})
}
pub fn dragging<T: Any>(
@@ -76,75 +114,132 @@ impl<V: View> DragAndDrop<V> {
render: Rc<impl 'static + Fn(&T, &mut RenderContext<V>) -> ElementBox>,
) {
let window_id = cx.window_id();
cx.update_global::<Self, _, _>(|this, cx| {
let region_offset = if let Some(previous_state) = this.currently_dragged.as_ref() {
previous_state.region_offset
} else {
event.region.origin() - event.prev_mouse_position
};
this.currently_dragged = Some(State {
window_id,
region_offset,
position: event.position,
payload,
render: Rc::new(move |payload, cx| {
render(payload.downcast_ref::<T>().unwrap(), cx)
}),
});
cx.update_global(|this: &mut Self, cx| {
this.notify_containers_for_window(window_id, cx);
match this.currently_dragged.as_ref() {
Some(&State::Down {
region_offset,
region,
})
| Some(&State::Dragging {
region_offset,
region,
..
}) => {
this.currently_dragged = Some(State::Dragging {
window_id,
region_offset,
region,
position: event.position,
payload,
render: Rc::new(move |payload, cx| {
render(payload.downcast_ref::<T>().unwrap(), cx)
}),
});
}
_ => {}
}
});
}
pub fn render(cx: &mut RenderContext<V>) -> Option<ElementBox> {
let currently_dragged = cx.global::<Self>().currently_dragged.clone();
enum DraggedElementHandler {}
cx.global::<Self>()
.currently_dragged
.clone()
.and_then(|state| {
match state {
State::Down { .. } => None,
State::Dragging {
window_id,
region_offset,
position,
region,
payload,
render,
} => {
if cx.window_id() != window_id {
return None;
}
currently_dragged.and_then(
|State {
window_id,
region_offset,
position,
payload,
render,
}| {
if cx.window_id() != window_id {
return None;
}
let position = position + region_offset;
Some(
Overlay::new(
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {
render(payload, cx)
})
.with_cursor_style(CursorStyle::Arrow)
.on_up(MouseButton::Left, |_, cx| {
cx.defer(|cx| {
cx.update_global::<Self, _, _>(|this, cx| {
this.finish_dragging(cx)
});
});
cx.propagate_event();
})
.on_up_out(MouseButton::Left, |_, cx| {
cx.defer(|cx| {
cx.update_global::<Self, _, _>(|this, cx| {
this.finish_dragging(cx)
});
});
})
// Don't block hover events or invalidations
.with_hoverable(false)
.constrained()
.with_width(region.width())
.with_height(region.height())
.boxed(),
)
.with_anchor_position(position)
.boxed(),
)
}
let position = position + region_offset;
enum DraggedElementHandler {}
Some(
Overlay::new(
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {
render(payload, cx)
State::Canceled => Some(
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, _| {
Empty::new()
.constrained()
.with_width(0.)
.with_height(0.)
.boxed()
})
.with_cursor_style(CursorStyle::Arrow)
.on_up(MouseButton::Left, |_, cx| {
cx.defer(|cx| {
cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
cx.update_global::<Self, _, _>(|this, _| {
this.currently_dragged = None;
});
});
cx.propagate_event();
})
.on_up_out(MouseButton::Left, |_, cx| {
cx.defer(|cx| {
cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
cx.update_global::<Self, _, _>(|this, _| {
this.currently_dragged = None;
});
});
})
// Don't block hover events or invalidations
.with_hoverable(false)
.boxed(),
)
.with_anchor_position(position)
.boxed(),
)
},
)
),
}
})
}
fn stop_dragging(&mut self, cx: &mut MutableAppContext) {
if let Some(State { window_id, .. }) = self.currently_dragged.take() {
pub fn cancel_dragging<P: Any>(&mut self, cx: &mut MutableAppContext) {
if let Some(State::Dragging {
payload, window_id, ..
}) = &self.currently_dragged
{
if payload.is::<P>() {
let window_id = *window_id;
self.currently_dragged = Some(State::Canceled);
self.notify_containers_for_window(window_id, cx);
}
}
}
fn finish_dragging(&mut self, cx: &mut MutableAppContext) {
if let Some(State::Dragging { window_id, .. }) = self.currently_dragged.take() {
self.notify_containers_for_window(window_id, cx);
}
}
@@ -184,7 +279,11 @@ impl<Tag> Draggable for MouseEventHandler<Tag> {
{
let payload = Rc::new(payload);
let render = Rc::new(render);
self.on_drag(MouseButton::Left, move |e, cx| {
self.on_down(MouseButton::Left, move |e, cx| {
cx.propagate_event();
DragAndDrop::<V>::drag_started(e, cx);
})
.on_drag(MouseButton::Left, move |e, cx| {
let payload = payload.clone();
let render = render.clone();
DragAndDrop::<V>::dragging(e, payload, cx, render)

View File

@@ -73,6 +73,7 @@ use std::{
mem,
num::NonZeroU32,
ops::{Deref, DerefMut, Range, RangeInclusive},
path::Path,
sync::Arc,
time::{Duration, Instant},
};
@@ -187,7 +188,7 @@ actions!(
Paste,
Undo,
Redo,
CenterScreen,
NextScreen,
MoveUp,
PageUp,
MoveDown,
@@ -307,7 +308,8 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(Editor::move_down);
cx.add_action(Editor::move_page_down);
cx.add_action(Editor::page_down);
cx.add_action(Editor::center_screen);
cx.add_action(Editor::next_screen);
cx.add_action(Editor::move_left);
cx.add_action(Editor::move_right);
cx.add_action(Editor::move_to_previous_word_start);
@@ -409,9 +411,42 @@ pub enum SelectMode {
#[derive(PartialEq, Eq)]
pub enum Autoscroll {
Next,
Strategy(AutoscrollStrategy),
}
impl Autoscroll {
pub fn fit() -> Self {
Self::Strategy(AutoscrollStrategy::Fit)
}
pub fn newest() -> Self {
Self::Strategy(AutoscrollStrategy::Newest)
}
pub fn center() -> Self {
Self::Strategy(AutoscrollStrategy::Center)
}
}
#[derive(PartialEq, Eq, Default)]
pub enum AutoscrollStrategy {
Fit,
Center,
Newest,
#[default]
Center,
Top,
Bottom,
}
impl AutoscrollStrategy {
fn next(&self) -> Self {
match self {
AutoscrollStrategy::Center => AutoscrollStrategy::Top,
AutoscrollStrategy::Top => AutoscrollStrategy::Bottom,
_ => AutoscrollStrategy::Center,
}
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
@@ -553,6 +588,7 @@ pub struct Editor {
hover_state: HoverState,
link_go_to_definition_state: LinkGoToDefinitionState,
visible_line_count: Option<f32>,
last_autoscroll: Option<(Vector2F, f32, f32, AutoscrollStrategy)>,
_subscriptions: Vec<Subscription>,
}
@@ -1205,6 +1241,7 @@ impl Editor {
hover_state: Default::default(),
link_go_to_definition_state: Default::default(),
visible_line_count: None,
last_autoscroll: None,
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
cx.subscribe(&buffer, Self::on_buffer_event),
@@ -1435,7 +1472,7 @@ impl Editor {
if let Some(highlighted_rows) = &self.highlighted_rows {
first_cursor_top = highlighted_rows.start as f32;
last_cursor_bottom = first_cursor_top + 1.;
} else if autoscroll == Autoscroll::Newest {
} else if autoscroll == Autoscroll::newest() {
let newest_selection = self.selections.newest::<Point>(cx);
first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32;
last_cursor_bottom = first_cursor_top + 1.;
@@ -1465,8 +1502,27 @@ impl Editor {
return false;
}
match autoscroll {
Autoscroll::Fit | Autoscroll::Newest => {
let strategy = match autoscroll {
Autoscroll::Strategy(strategy) => strategy,
Autoscroll::Next => {
let last_autoscroll = &self.last_autoscroll;
if let Some(last_autoscroll) = last_autoscroll {
if self.scroll_position == last_autoscroll.0
&& first_cursor_top == last_autoscroll.1
&& last_cursor_bottom == last_autoscroll.2
{
last_autoscroll.3.next()
} else {
AutoscrollStrategy::default()
}
} else {
AutoscrollStrategy::default()
}
}
};
match strategy {
AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
let margin = margin.min(self.vertical_scroll_margin);
let target_top = (first_cursor_top - margin).max(0.0);
let target_bottom = last_cursor_bottom + margin;
@@ -1481,12 +1537,27 @@ impl Editor {
self.set_scroll_position_internal(scroll_position, local, cx);
}
}
Autoscroll::Center => {
AutoscrollStrategy::Center => {
scroll_position.set_y((first_cursor_top - margin).max(0.0));
self.set_scroll_position_internal(scroll_position, local, cx);
}
AutoscrollStrategy::Top => {
scroll_position.set_y((first_cursor_top).max(0.0));
self.set_scroll_position_internal(scroll_position, local, cx);
}
AutoscrollStrategy::Bottom => {
scroll_position.set_y((last_cursor_bottom - visible_lines).max(0.0));
self.set_scroll_position_internal(scroll_position, local, cx);
}
}
self.last_autoscroll = Some((
self.scroll_position,
first_cursor_top,
last_cursor_bottom,
strategy,
));
true
}
@@ -1734,7 +1805,7 @@ impl Editor {
_ => {}
}
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.set_pending(pending_selection, pending_mode)
});
}
@@ -1795,7 +1866,7 @@ impl Editor {
}
}
self.change_selections(auto_scroll.then(|| Autoscroll::Newest), cx, |s| {
self.change_selections(auto_scroll.then(|| Autoscroll::newest()), cx, |s| {
if !add {
s.clear_disjoint();
} else if click_count > 1 {
@@ -2012,7 +2083,7 @@ impl Editor {
return;
}
if self.change_selections(Some(Autoscroll::Fit), cx, |s| s.try_cancel()) {
if self.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel()) {
return;
}
}
@@ -2178,7 +2249,7 @@ impl Editor {
}
drop(snapshot);
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(new_selections));
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
this.trigger_completion_on_input(&text, cx);
});
}
@@ -2258,7 +2329,7 @@ impl Editor {
})
.collect();
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(new_selections));
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
});
}
@@ -2288,7 +2359,7 @@ impl Editor {
self.transact(cx, |editor, cx| {
editor.edit_with_autoindent(edits, cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
let mut index = 0;
s.move_cursors_with(|map, _, _| {
let row = rows[index];
@@ -2329,7 +2400,7 @@ impl Editor {
anchors
});
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_anchors(selection_anchors);
})
});
@@ -3025,7 +3096,7 @@ impl Editor {
});
if let Some(tabstop) = tabstops.first() {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(tabstop.iter().cloned());
});
self.snippet_stack.push(SnippetState {
@@ -3066,7 +3137,7 @@ impl Editor {
}
}
if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_anchor_ranges(current_ranges.iter().cloned())
});
// If snippet state is not at the last tabstop, push it back on the stack
@@ -3131,14 +3202,14 @@ impl Editor {
}
}
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
this.insert("", cx);
});
}
pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
self.transact(cx, |this, cx| {
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
let line_mode = s.line_mode;
s.move_with(|map, selection| {
if selection.is_empty() && !line_mode {
@@ -3232,7 +3303,7 @@ impl Editor {
self.transact(cx, |this, cx| {
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections))
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections))
});
}
@@ -3255,7 +3326,7 @@ impl Editor {
self.transact(cx, |this, cx| {
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
});
}
@@ -3387,7 +3458,7 @@ impl Editor {
);
});
let selections = this.selections.all::<usize>(cx);
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
});
}
@@ -3467,7 +3538,7 @@ impl Editor {
})
.collect();
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(new_selections);
});
});
@@ -3509,7 +3580,7 @@ impl Editor {
buffer.edit(edits, None, cx);
});
this.request_autoscroll(Autoscroll::Fit, cx);
this.request_autoscroll(Autoscroll::fit(), cx);
});
}
@@ -3619,7 +3690,7 @@ impl Editor {
}
});
this.fold_ranges(refold_ranges, cx);
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(new_selections);
})
});
@@ -3724,13 +3795,13 @@ impl Editor {
}
});
this.fold_ranges(refold_ranges, cx);
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(new_selections));
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
});
}
pub fn transpose(&mut self, _: &Transpose, cx: &mut ViewContext<Self>) {
self.transact(cx, |this, cx| {
let edits = this.change_selections(Some(Autoscroll::Fit), cx, |s| {
let edits = this.change_selections(Some(Autoscroll::fit()), cx, |s| {
let mut edits: Vec<(Range<usize>, String)> = Default::default();
let line_mode = s.line_mode;
s.move_with(|display_map, selection| {
@@ -3774,7 +3845,7 @@ impl Editor {
this.buffer
.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
let selections = this.selections.all::<usize>(cx);
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(selections);
});
});
@@ -3808,7 +3879,7 @@ impl Editor {
}
self.transact(cx, |this, cx| {
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(selections);
});
this.insert("", cx);
@@ -3923,7 +3994,7 @@ impl Editor {
});
let selections = this.selections.all::<usize>(cx);
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
} else {
this.insert(&clipboard_text, cx);
}
@@ -3938,7 +4009,7 @@ impl Editor {
s.select_anchors(selections.to_vec());
});
}
self.request_autoscroll(Autoscroll::Fit, cx);
self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx);
cx.emit(Event::Edited);
}
@@ -3952,7 +4023,7 @@ impl Editor {
s.select_anchors(selections.to_vec());
});
}
self.request_autoscroll(Autoscroll::Fit, cx);
self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx);
cx.emit(Event::Edited);
}
@@ -3964,7 +4035,7 @@ impl Editor {
}
pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext<Self>) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
let line_mode = s.line_mode;
s.move_with(|map, selection| {
let cursor = if selection.is_empty() && !line_mode {
@@ -3978,13 +4049,13 @@ impl Editor {
}
pub fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext<Self>) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, _| (movement::left(map, head), SelectionGoal::None));
})
}
pub fn move_right(&mut self, _: &MoveRight, cx: &mut ViewContext<Self>) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
let line_mode = s.line_mode;
s.move_with(|map, selection| {
let cursor = if selection.is_empty() && !line_mode {
@@ -3998,12 +4069,12 @@ impl Editor {
}
pub fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext<Self>) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, _| (movement::right(map, head), SelectionGoal::None));
})
}
pub fn center_screen(&mut self, _: &CenterScreen, cx: &mut ViewContext<Self>) {
pub fn next_screen(&mut self, _: &NextScreen, cx: &mut ViewContext<Editor>) {
if self.take_rename(true, cx).is_some() {
return;
}
@@ -4017,7 +4088,7 @@ impl Editor {
return;
}
self.request_autoscroll(Autoscroll::Center, cx);
self.request_autoscroll(Autoscroll::Next, cx);
}
pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
@@ -4036,7 +4107,7 @@ impl Editor {
return;
}
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
let line_mode = s.line_mode;
s.move_with(|map, selection| {
if !selection.is_empty() && !line_mode {
@@ -4070,9 +4141,9 @@ impl Editor {
};
let autoscroll = if action.center_cursor {
Autoscroll::Center
Autoscroll::center()
} else {
Autoscroll::Fit
Autoscroll::fit()
};
self.change_selections(Some(autoscroll), cx, |s| {
@@ -4115,7 +4186,7 @@ impl Editor {
}
pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext<Self>) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, goal| movement::up(map, head, goal, false))
})
}
@@ -4134,7 +4205,7 @@ impl Editor {
return;
}
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
let line_mode = s.line_mode;
s.move_with(|map, selection| {
if !selection.is_empty() && !line_mode {
@@ -4168,9 +4239,9 @@ impl Editor {
};
let autoscroll = if action.center_cursor {
Autoscroll::Center
Autoscroll::center()
} else {
Autoscroll::Fit
Autoscroll::fit()
};
self.change_selections(Some(autoscroll), cx, |s| {
@@ -4213,7 +4284,7 @@ impl Editor {
}
pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext<Self>) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, goal| movement::down(map, head, goal, false))
});
}
@@ -4223,7 +4294,7 @@ impl Editor {
_: &MoveToPreviousWordStart,
cx: &mut ViewContext<Self>,
) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, head, _| {
(
movement::previous_word_start(map, head),
@@ -4238,7 +4309,7 @@ impl Editor {
_: &MoveToPreviousSubwordStart,
cx: &mut ViewContext<Self>,
) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, head, _| {
(
movement::previous_subword_start(map, head),
@@ -4253,7 +4324,7 @@ impl Editor {
_: &SelectToPreviousWordStart,
cx: &mut ViewContext<Self>,
) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, _| {
(
movement::previous_word_start(map, head),
@@ -4268,7 +4339,7 @@ impl Editor {
_: &SelectToPreviousSubwordStart,
cx: &mut ViewContext<Self>,
) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, _| {
(
movement::previous_subword_start(map, head),
@@ -4285,7 +4356,7 @@ impl Editor {
) {
self.transact(cx, |this, cx| {
this.select_autoclose_pair(cx);
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
let line_mode = s.line_mode;
s.move_with(|map, selection| {
if selection.is_empty() && !line_mode {
@@ -4305,7 +4376,7 @@ impl Editor {
) {
self.transact(cx, |this, cx| {
this.select_autoclose_pair(cx);
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
let line_mode = s.line_mode;
s.move_with(|map, selection| {
if selection.is_empty() && !line_mode {
@@ -4319,7 +4390,7 @@ impl Editor {
}
pub fn move_to_next_word_end(&mut self, _: &MoveToNextWordEnd, cx: &mut ViewContext<Self>) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, head, _| {
(movement::next_word_end(map, head), SelectionGoal::None)
});
@@ -4331,7 +4402,7 @@ impl Editor {
_: &MoveToNextSubwordEnd,
cx: &mut ViewContext<Self>,
) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, head, _| {
(movement::next_subword_end(map, head), SelectionGoal::None)
});
@@ -4339,7 +4410,7 @@ impl Editor {
}
pub fn select_to_next_word_end(&mut self, _: &SelectToNextWordEnd, cx: &mut ViewContext<Self>) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, _| {
(movement::next_word_end(map, head), SelectionGoal::None)
});
@@ -4351,7 +4422,7 @@ impl Editor {
_: &SelectToNextSubwordEnd,
cx: &mut ViewContext<Self>,
) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, _| {
(movement::next_subword_end(map, head), SelectionGoal::None)
});
@@ -4360,7 +4431,7 @@ impl Editor {
pub fn delete_to_next_word_end(&mut self, _: &DeleteToNextWordEnd, cx: &mut ViewContext<Self>) {
self.transact(cx, |this, cx| {
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
let line_mode = s.line_mode;
s.move_with(|map, selection| {
if selection.is_empty() && !line_mode {
@@ -4379,7 +4450,7 @@ impl Editor {
cx: &mut ViewContext<Self>,
) {
self.transact(cx, |this, cx| {
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
if selection.is_empty() {
let cursor = movement::next_subword_end(map, selection.head());
@@ -4396,7 +4467,7 @@ impl Editor {
_: &MoveToBeginningOfLine,
cx: &mut ViewContext<Self>,
) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, head, _| {
(
movement::indented_line_beginning(map, head, true),
@@ -4411,7 +4482,7 @@ impl Editor {
action: &SelectToBeginningOfLine,
cx: &mut ViewContext<Self>,
) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, _| {
(
movement::indented_line_beginning(map, head, action.stop_at_soft_wraps),
@@ -4427,7 +4498,7 @@ impl Editor {
cx: &mut ViewContext<Self>,
) {
self.transact(cx, |this, cx| {
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|_, selection| {
selection.reversed = true;
});
@@ -4444,7 +4515,7 @@ impl Editor {
}
pub fn move_to_end_of_line(&mut self, _: &MoveToEndOfLine, cx: &mut ViewContext<Self>) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, head, _| {
(movement::line_end(map, head, true), SelectionGoal::None)
});
@@ -4456,7 +4527,7 @@ impl Editor {
action: &SelectToEndOfLine,
cx: &mut ViewContext<Self>,
) {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, _| {
(
movement::line_end(map, head, action.stop_at_soft_wraps),
@@ -4496,7 +4567,7 @@ impl Editor {
return;
}
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(vec![0..0]);
});
}
@@ -4505,7 +4576,7 @@ impl Editor {
let mut selection = self.selections.last::<Point>(cx);
selection.set_head(Point::zero(), SelectionGoal::None);
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(vec![selection]);
});
}
@@ -4517,7 +4588,7 @@ impl Editor {
}
let cursor = self.buffer.read(cx).read(cx).len();
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(vec![cursor..cursor])
});
}
@@ -4566,14 +4637,14 @@ impl Editor {
let buffer = self.buffer.read(cx).snapshot(cx);
let mut selection = self.selections.first::<usize>(cx);
selection.set_head(buffer.len(), SelectionGoal::None);
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(vec![selection]);
});
}
pub fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext<Self>) {
let end = self.buffer.read(cx).read(cx).len();
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(vec![0..end]);
});
}
@@ -4588,7 +4659,7 @@ impl Editor {
selection.end = cmp::min(max_point, Point::new(rows.end, 0));
selection.reversed = false;
}
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(selections);
});
}
@@ -4613,7 +4684,7 @@ impl Editor {
}
}
self.unfold_ranges(to_unfold, true, cx);
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(new_selection_ranges);
});
}
@@ -4713,7 +4784,7 @@ impl Editor {
state.stack.pop();
}
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(new_selections);
});
if state.stack.len() > 1 {
@@ -4762,7 +4833,7 @@ impl Editor {
if let Some(next_selected_range) = next_selected_range {
self.unfold_ranges([next_selected_range.clone()], false, cx);
self.change_selections(Some(Autoscroll::Newest), cx, |s| {
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
if action.replace_newest {
s.delete(s.newest_anchor().id);
}
@@ -4795,7 +4866,7 @@ impl Editor {
done: false,
};
self.unfold_ranges([selection.start..selection.end], false, cx);
self.change_selections(Some(Autoscroll::Newest), cx, |s| {
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
s.select(selections);
});
self.select_next_state = Some(select_state);
@@ -5028,7 +5099,7 @@ impl Editor {
}
drop(snapshot);
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
});
}
@@ -5072,7 +5143,7 @@ impl Editor {
if selected_larger_node {
stack.push(old_selections);
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(new_selections);
});
}
@@ -5086,7 +5157,7 @@ impl Editor {
) {
let mut stack = mem::take(&mut self.select_larger_syntax_node_stack);
if let Some(selections) = stack.pop() {
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(selections.to_vec());
});
}
@@ -5117,7 +5188,7 @@ impl Editor {
}
}
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(selections);
});
}
@@ -5129,7 +5200,7 @@ impl Editor {
self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec()));
self.select_next_state = entry.select_next_state;
self.add_selections_state = entry.add_selections_state;
self.request_autoscroll(Autoscroll::Newest, cx);
self.request_autoscroll(Autoscroll::newest(), cx);
}
self.selection_history.mode = SelectionHistoryMode::Normal;
}
@@ -5141,7 +5212,7 @@ impl Editor {
self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec()));
self.select_next_state = entry.select_next_state;
self.add_selections_state = entry.add_selections_state;
self.request_autoscroll(Autoscroll::Newest, cx);
self.request_autoscroll(Autoscroll::newest(), cx);
}
self.selection_history.mode = SelectionHistoryMode::Normal;
}
@@ -5163,7 +5234,7 @@ impl Editor {
if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() {
let (group_id, jump_to) = popover.activation_info();
if self.activate_diagnostics(group_id, cx) {
self.change_selections(Some(Autoscroll::Center), cx, |s| {
self.change_selections(Some(Autoscroll::center()), cx, |s| {
let mut new_selection = s.newest_anchor().clone();
new_selection.collapse_to(jump_to, SelectionGoal::None);
s.select_anchors(vec![new_selection.clone()]);
@@ -5209,7 +5280,7 @@ impl Editor {
if let Some((primary_range, group_id)) = group {
if self.activate_diagnostics(group_id, cx) {
self.change_selections(Some(Autoscroll::Center), cx, |s| {
self.change_selections(Some(Autoscroll::center()), cx, |s| {
s.select(vec![Selection {
id: selection.id,
start: primary_range.start,
@@ -5284,7 +5355,7 @@ impl Editor {
.dedup();
if let Some(hunk) = hunks.next() {
this.change_selections(Some(Autoscroll::Center), cx, |s| {
this.change_selections(Some(Autoscroll::center()), cx, |s| {
let row = hunk.start_display_row();
let point = DisplayPoint::new(row, 0);
s.select_display_ranges([point..point]);
@@ -5382,7 +5453,7 @@ impl Editor {
if editor_handle != target_editor_handle {
pane.update(cx, |pane, _| pane.disable_history());
}
target_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
target_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
s.select_ranges([range]);
});
@@ -6004,7 +6075,7 @@ impl Editor {
let mut ranges = ranges.into_iter().peekable();
if ranges.peek().is_some() {
self.display_map.update(cx, |map, cx| map.fold(ranges, cx));
self.request_autoscroll(Autoscroll::Fit, cx);
self.request_autoscroll(Autoscroll::fit(), cx);
cx.notify();
}
}
@@ -6019,7 +6090,7 @@ impl Editor {
if ranges.peek().is_some() {
self.display_map
.update(cx, |map, cx| map.unfold(ranges, inclusive, cx));
self.request_autoscroll(Autoscroll::Fit, cx);
self.request_autoscroll(Autoscroll::fit(), cx);
cx.notify();
}
}
@@ -6032,7 +6103,7 @@ impl Editor {
let blocks = self
.display_map
.update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx));
self.request_autoscroll(Autoscroll::Fit, cx);
self.request_autoscroll(Autoscroll::fit(), cx);
blocks
}
@@ -6043,7 +6114,7 @@ impl Editor {
) {
self.display_map
.update(cx, |display_map, _| display_map.replace_blocks(blocks));
self.request_autoscroll(Autoscroll::Fit, cx);
self.request_autoscroll(Autoscroll::fit(), cx);
}
pub fn remove_blocks(&mut self, block_ids: HashSet<BlockId>, cx: &mut ViewContext<Self>) {
@@ -6383,7 +6454,7 @@ impl Editor {
for (buffer, ranges) in new_selections_by_buffer.into_iter() {
let editor = workspace.open_project_item::<Self>(buffer, cx);
editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Newest), cx, |s| {
editor.change_selections(Some(Autoscroll::newest()), cx, |s| {
s.select_ranges(ranges);
});
});
@@ -6394,7 +6465,7 @@ impl Editor {
}
fn jump(workspace: &mut Workspace, action: &Jump, cx: &mut ViewContext<Workspace>) {
let editor = workspace.open_path(action.path.clone(), true, cx);
let editor = workspace.open_path(action.path.clone(), None, true, cx);
let position = action.position;
let anchor = action.anchor;
cx.spawn_weak(|_, mut cx| async move {
@@ -6409,7 +6480,7 @@ impl Editor {
};
let nav_history = editor.nav_history.take();
editor.change_selections(Some(Autoscroll::Newest), cx, |s| {
editor.change_selections(Some(Autoscroll::newest()), cx, |s| {
s.select_ranges([cursor..cursor]);
});
editor.nav_history = nav_history;
@@ -6466,15 +6537,13 @@ impl Editor {
.as_singleton()
.and_then(|b| b.read(cx).file()),
) {
project.read(cx).client().report_event(
name,
json!({
"File Extension": file
.path()
.extension()
.and_then(|e| e.to_str())
}),
);
let extension = Path::new(file.file_name(cx))
.extension()
.and_then(|e| e.to_str());
project
.read(cx)
.client()
.report_event(name, json!({ "File Extension": extension }));
}
}
}

View File

@@ -4146,14 +4146,26 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
handle_resolve_completion_request(
&mut cx,
Some((
indoc! {"
one.second_completion
two
threeˇ
"},
"\nadditional edit",
)),
Some(vec![
(
//This overlaps with the primary completion edit which is
//misbehavior from the LSP spec, test that we filter it out
indoc! {"
one.second_ˇcompletion
two
threeˇ
"},
"overlapping aditional edit",
),
(
indoc! {"
one.second_completion
two
threeˇ
"},
"\nadditional edit",
),
]),
)
.await;
apply_additional_edits.await.unwrap();
@@ -4303,19 +4315,24 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
async fn handle_resolve_completion_request<'a>(
cx: &mut EditorLspTestContext<'a>,
edit: Option<(&'static str, &'static str)>,
edits: Option<Vec<(&'static str, &'static str)>>,
) {
let edit = edit.map(|(marked_string, new_text)| {
let (_, marked_ranges) = marked_text_ranges(marked_string, false);
let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
vec![lsp::TextEdit::new(replace_range, new_text.to_string())]
let edits = edits.map(|edits| {
edits
.iter()
.map(|(marked_string, new_text)| {
let (_, marked_ranges) = marked_text_ranges(marked_string, false);
let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
lsp::TextEdit::new(replace_range, new_text.to_string())
})
.collect::<Vec<_>>()
});
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
let edit = edit.clone();
let edits = edits.clone();
async move {
Ok(lsp::CompletionItem {
additional_text_edits: edit,
additional_text_edits: edits,
..Default::default()
})
}
@@ -5011,7 +5028,7 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
// Update the selections and scroll position
leader.update(cx, |leader, cx| {
leader.change_selections(None, cx, |s| s.select_ranges([0..0]));
leader.request_autoscroll(Autoscroll::Newest, cx);
leader.request_autoscroll(Autoscroll::newest(), cx);
leader.set_scroll_position(vec2f(1.5, 3.5), cx);
});
follower.update(cx, |follower, cx| {

View File

@@ -192,8 +192,14 @@ impl EditorElement {
.on_scroll({
let position_map = position_map.clone();
move |e, cx| {
if !Self::scroll(e.position, e.delta, e.precise, &position_map, bounds, cx)
{
if !Self::scroll(
e.position,
*e.delta.raw(),
e.delta.precise(),
&position_map,
bounds,
cx,
) {
cx.propagate_event()
}
}

View File

@@ -204,7 +204,7 @@ impl FollowableItem for Editor {
if !selections.is_empty() {
self.set_selections_from_remote(selections, cx);
self.request_autoscroll_remotely(Autoscroll::Newest, cx);
self.request_autoscroll_remotely(Autoscroll::newest(), cx);
} else if let Some(anchor) = message.scroll_top_anchor {
self.set_scroll_top_anchor(
Anchor {
@@ -294,7 +294,7 @@ impl Item for Editor {
let nav_history = self.nav_history.take();
self.scroll_position = data.scroll_position;
self.scroll_top_anchor = scroll_top_anchor;
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([offset..offset])
});
self.nav_history = nav_history;
@@ -466,7 +466,7 @@ impl Item for Editor {
cx.spawn(|this, mut cx| async move {
let transaction = reload_buffers.log_err().await;
this.update(&mut cx, |editor, cx| {
editor.request_autoscroll(Autoscroll::Fit, cx)
editor.request_autoscroll(Autoscroll::fit(), cx)
});
buffer.update(&mut cx, |buffer, _| {
if let Some(transaction) = transaction {
@@ -619,7 +619,7 @@ impl SearchableItem for Editor {
cx: &mut ViewContext<Self>,
) {
self.unfold_ranges([matches[index].clone()], false, cx);
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([matches[index].clone()])
});
}
@@ -819,11 +819,20 @@ impl StatusItemView for CursorPosition {
fn path_for_buffer<'a>(
buffer: &ModelHandle<MultiBuffer>,
mut height: usize,
height: usize,
include_filename: bool,
cx: &'a AppContext,
) -> Option<Cow<'a, Path>> {
let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
path_for_file(file, height, include_filename, cx)
}
fn path_for_file<'a>(
file: &'a dyn language::File,
mut height: usize,
include_filename: bool,
cx: &'a AppContext,
) -> Option<Cow<'a, Path>> {
// Ensure we always render at least the filename.
height += 1;
@@ -845,13 +854,82 @@ fn path_for_buffer<'a>(
if include_filename {
Some(full_path.into())
} else {
Some(full_path.parent().unwrap().to_path_buf().into())
Some(full_path.parent()?.to_path_buf().into())
}
} else {
let mut path = file.path().strip_prefix(prefix).unwrap();
let mut path = file.path().strip_prefix(prefix).ok()?;
if !include_filename {
path = path.parent().unwrap();
path = path.parent()?;
}
Some(path.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::MutableAppContext;
use std::{
path::{Path, PathBuf},
sync::Arc,
};
#[gpui::test]
fn test_path_for_file(cx: &mut MutableAppContext) {
let file = TestFile {
path: Path::new("").into(),
full_path: PathBuf::from(""),
};
assert_eq!(path_for_file(&file, 0, false, cx), None);
}
struct TestFile {
path: Arc<Path>,
full_path: PathBuf,
}
impl language::File for TestFile {
fn path(&self) -> &Arc<Path> {
&self.path
}
fn full_path(&self, _: &gpui::AppContext) -> PathBuf {
self.full_path.clone()
}
fn as_local(&self) -> Option<&dyn language::LocalFile> {
todo!()
}
fn mtime(&self) -> std::time::SystemTime {
todo!()
}
fn file_name<'a>(&'a self, _: &'a gpui::AppContext) -> &'a std::ffi::OsStr {
todo!()
}
fn is_deleted(&self) -> bool {
todo!()
}
fn save(
&self,
_: u64,
_: language::Rope,
_: clock::Global,
_: project::LineEnding,
_: &mut MutableAppContext,
) -> gpui::Task<anyhow::Result<(clock::Global, String, std::time::SystemTime)>> {
todo!()
}
fn as_any(&self) -> &dyn std::any::Any {
todo!()
}
fn to_proto(&self) -> rpc::proto::File {
todo!()
}
}
}

View File

@@ -811,7 +811,7 @@ mod tests {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let anchor_range = snapshot.anchor_before(selection_range.start)
..snapshot.anchor_after(selection_range.end);
editor.change_selections(Some(crate::Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| {
s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
});
});

View File

@@ -677,6 +677,19 @@ impl<'a> MutableSelectionsCollection<'a> {
});
}
pub fn maybe_move_cursors_with(
&mut self,
mut update_cursor_position: impl FnMut(
&DisplaySnapshot,
DisplayPoint,
SelectionGoal,
) -> Option<(DisplayPoint, SelectionGoal)>,
) {
self.move_cursors_with(|map, point, goal| {
update_cursor_position(map, point, goal).unwrap_or((point, goal))
})
}
pub fn replace_cursors_with(
&mut self,
mut find_replacement_cursors: impl FnMut(&DisplaySnapshot) -> Vec<DisplayPoint>,

View File

@@ -76,7 +76,9 @@ impl<'a> EditorLspTestContext<'a> {
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
let item = workspace
.update(cx, |workspace, cx| workspace.open_path(file, true, cx))
.update(cx, |workspace, cx| {
workspace.open_path(file, None, true, cx)
})
.await
.expect("Could not open test file");

View File

@@ -169,7 +169,7 @@ impl<'a> EditorTestContext<'a> {
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
self.editor.update(self.cx, |editor, cx| {
editor.set_text(unmarked_text, cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(selection_ranges)
})
});

View File

@@ -104,7 +104,7 @@ impl FileFinder {
match event {
Event::Selected(project_path) => {
workspace
.open_path(project_path.clone(), true, cx)
.open_path(project_path.clone(), None, true, cx)
.detach_and_log_err(cx);
workspace.dismiss_modal(cx);
}

View File

@@ -83,7 +83,7 @@ impl GoToLine {
if let Some(rows) = active_editor.highlighted_rows() {
let snapshot = active_editor.snapshot(cx).display_snapshot;
let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
active_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
active_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
s.select_ranges([position..position])
});
}
@@ -127,7 +127,7 @@ impl GoToLine {
let display_point = point.to_display_point(&snapshot);
let row = display_point.row();
active_editor.highlight_rows(Some(row..row + 1));
active_editor.request_autoscroll(Autoscroll::Center, cx);
active_editor.request_autoscroll(Autoscroll::center(), cx);
});
cx.notify();
}

View File

@@ -257,17 +257,19 @@ impl Element for Flex {
let axis = self.axis;
move |e, cx| {
if remaining_space < 0. {
let scroll_delta = e.delta.raw();
let mut delta = match axis {
Axis::Horizontal => {
if e.delta.x().abs() >= e.delta.y().abs() {
e.delta.x()
if scroll_delta.x().abs() >= scroll_delta.y().abs() {
scroll_delta.x()
} else {
e.delta.y()
scroll_delta.y()
}
}
Axis::Vertical => e.delta.y(),
Axis::Vertical => scroll_delta.y(),
};
if !e.precise {
if !e.delta.precise() {
delta *= 20.;
}

View File

@@ -258,8 +258,8 @@ impl Element for List {
state.0.borrow_mut().scroll(
&scroll_top,
height,
e.platform_event.delta,
e.platform_event.precise,
*e.platform_event.delta.raw(),
e.platform_event.delta.precise(),
cx,
)
}

View File

@@ -295,15 +295,19 @@ impl Element for UniformList {
move |MouseScrollWheel {
platform_event:
ScrollWheelEvent {
position,
delta,
precise,
..
position, delta, ..
},
..
},
cx| {
if !Self::scroll(state.clone(), position, delta, precise, scroll_max, cx) {
if !Self::scroll(
state.clone(),
position,
*delta.raw(),
delta.precise(),
scroll_max,
cx,
) {
cx.propagate_event();
}
}

View File

@@ -1,5 +1,7 @@
use std::ops::Deref;
use pathfinder_geometry::vector::vec2f;
use crate::{geometry::vector::Vector2F, keymap::Keystroke};
#[derive(Clone, Debug)]
@@ -44,11 +46,45 @@ pub enum TouchPhase {
Ended,
}
#[derive(Clone, Copy, Debug)]
pub enum ScrollDelta {
Pixels(Vector2F),
Lines(Vector2F),
}
impl Default for ScrollDelta {
fn default() -> Self {
Self::Lines(Default::default())
}
}
impl ScrollDelta {
pub fn raw(&self) -> &Vector2F {
match self {
ScrollDelta::Pixels(v) => v,
ScrollDelta::Lines(v) => v,
}
}
pub fn precise(&self) -> bool {
match self {
ScrollDelta::Pixels(_) => true,
ScrollDelta::Lines(_) => false,
}
}
pub fn pixel_delta(&self, line_height: f32) -> Vector2F {
match self {
ScrollDelta::Pixels(delta) => *delta,
ScrollDelta::Lines(delta) => vec2f(delta.x() * line_height, delta.y() * line_height),
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct ScrollWheelEvent {
pub position: Vector2F,
pub delta: Vector2F,
pub precise: bool,
pub delta: ScrollDelta,
pub modifiers: Modifiers,
/// If the platform supports returning the phase of a scroll wheel event, it will be stored here
pub phase: Option<TouchPhase>,

View File

@@ -3,7 +3,7 @@ use crate::{
keymap::Keystroke,
platform::{Event, NavigationDirection},
KeyDownEvent, KeyUpEvent, Modifiers, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
MouseMovedEvent, ScrollWheelEvent, TouchPhase,
MouseMovedEvent, ScrollDelta, ScrollWheelEvent, TouchPhase,
};
use cocoa::{
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
@@ -164,17 +164,24 @@ impl Event {
_ => Some(TouchPhase::Moved),
};
let raw_data = vec2f(
native_event.scrollingDeltaX() as f32,
native_event.scrollingDeltaY() as f32,
);
let delta = if native_event.hasPreciseScrollingDeltas() == YES {
ScrollDelta::Pixels(raw_data)
} else {
ScrollDelta::Lines(raw_data)
};
Self::ScrollWheel(ScrollWheelEvent {
position: vec2f(
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
delta: vec2f(
native_event.scrollingDeltaX() as f32,
native_event.scrollingDeltaY() as f32,
),
delta,
phase,
precise: native_event.hasPreciseScrollingDeltas() == YES,
modifiers: read_modifiers(native_event),
})
}),

View File

@@ -475,27 +475,35 @@ impl Presenter {
if let MouseEvent::Down(e) = &mouse_event {
if valid_region
.handlers
.contains_handler(MouseEvent::click_disc(), Some(e.button))
.contains(MouseEvent::click_disc(), Some(e.button))
|| valid_region
.handlers
.contains_handler(MouseEvent::drag_disc(), Some(e.button))
.contains(MouseEvent::drag_disc(), Some(e.button))
{
event_cx.handled = true;
}
}
if let Some(callback) = valid_region.handlers.get(&mouse_event.handler_key()) {
event_cx.handled = true;
event_cx.with_current_view(valid_region.id().view_id(), {
let region_event = mouse_event.clone();
|cx| callback(region_event, cx)
});
// `event_consumed` should only be true if there are any handlers for this event.
let mut event_consumed = event_cx.handled;
if let Some(callbacks) = valid_region.handlers.get(&mouse_event.handler_key()) {
event_consumed = true;
for callback in callbacks {
event_cx.handled = true;
event_cx.with_current_view(valid_region.id().view_id(), {
let region_event = mouse_event.clone();
|cx| callback(region_event, cx)
});
event_consumed &= event_cx.handled;
any_event_handled |= event_cx.handled;
}
}
any_event_handled = any_event_handled || event_cx.handled;
// For bubbling events, if the event was handled, don't continue dispatching
// This only makes sense for local events.
if event_cx.handled && mouse_event.is_capturable() {
any_event_handled |= event_cx.handled;
// For bubbling events, if the event was handled, don't continue dispatching.
// This only makes sense for local events which return false from is_capturable.
if event_consumed && mouse_event.is_capturable() {
break;
}
}

View File

@@ -5,7 +5,7 @@ use std::{
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
use crate::{MouseButton, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
use crate::{scene::mouse_region::HandlerKey, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
#[derive(Debug, Default, Clone)]
pub struct MouseMove {
@@ -217,17 +217,17 @@ impl MouseEvent {
discriminant(&MouseEvent::ScrollWheel(Default::default()))
}
pub fn handler_key(&self) -> (Discriminant<MouseEvent>, Option<MouseButton>) {
pub fn handler_key(&self) -> HandlerKey {
match self {
MouseEvent::Move(_) => (Self::move_disc(), None),
MouseEvent::Drag(e) => (Self::drag_disc(), e.pressed_button),
MouseEvent::Hover(_) => (Self::hover_disc(), None),
MouseEvent::Down(e) => (Self::down_disc(), Some(e.button)),
MouseEvent::Up(e) => (Self::up_disc(), Some(e.button)),
MouseEvent::Click(e) => (Self::click_disc(), Some(e.button)),
MouseEvent::UpOut(e) => (Self::up_out_disc(), Some(e.button)),
MouseEvent::DownOut(e) => (Self::down_out_disc(), Some(e.button)),
MouseEvent::ScrollWheel(_) => (Self::scroll_wheel_disc(), None),
MouseEvent::Move(_) => HandlerKey::new(Self::move_disc(), None),
MouseEvent::Drag(e) => HandlerKey::new(Self::drag_disc(), e.pressed_button),
MouseEvent::Hover(_) => HandlerKey::new(Self::hover_disc(), None),
MouseEvent::Down(e) => HandlerKey::new(Self::down_disc(), Some(e.button)),
MouseEvent::Up(e) => HandlerKey::new(Self::up_disc(), Some(e.button)),
MouseEvent::Click(e) => HandlerKey::new(Self::click_disc(), Some(e.button)),
MouseEvent::UpOut(e) => HandlerKey::new(Self::up_out_disc(), Some(e.button)),
MouseEvent::DownOut(e) => HandlerKey::new(Self::down_out_disc(), Some(e.button)),
MouseEvent::ScrollWheel(_) => HandlerKey::new(Self::scroll_wheel_disc(), None),
}
}
}

View File

@@ -3,6 +3,7 @@ use std::{any::TypeId, fmt::Debug, mem::Discriminant, rc::Rc};
use collections::HashMap;
use pathfinder_geometry::rect::RectF;
use smallvec::SmallVec;
use crate::{EventContext, MouseButton};
@@ -177,61 +178,105 @@ impl MouseRegionId {
}
}
pub type HandlerCallback = Rc<dyn Fn(MouseEvent, &mut EventContext)>;
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct HandlerKey {
event_kind: Discriminant<MouseEvent>,
button: Option<MouseButton>,
}
impl HandlerKey {
pub fn new(event_kind: Discriminant<MouseEvent>, button: Option<MouseButton>) -> HandlerKey {
HandlerKey { event_kind, button }
}
}
#[derive(Clone, Default)]
pub struct HandlerSet {
#[allow(clippy::type_complexity)]
pub set: HashMap<
(Discriminant<MouseEvent>, Option<MouseButton>),
Rc<dyn Fn(MouseEvent, &mut EventContext)>,
>,
set: HashMap<HandlerKey, SmallVec<[HandlerCallback; 1]>>,
}
impl HandlerSet {
pub fn capture_all() -> Self {
#[allow(clippy::type_complexity)]
let mut set: HashMap<
(Discriminant<MouseEvent>, Option<MouseButton>),
Rc<dyn Fn(MouseEvent, &mut EventContext)>,
> = Default::default();
let mut set: HashMap<HandlerKey, SmallVec<[HandlerCallback; 1]>> = HashMap::default();
set.insert((MouseEvent::move_disc(), None), Rc::new(|_, _| {}));
set.insert((MouseEvent::hover_disc(), None), Rc::new(|_, _| {}));
set.insert(
HandlerKey::new(MouseEvent::move_disc(), None),
SmallVec::from_buf([Rc::new(|_, _| {})]),
);
set.insert(
HandlerKey::new(MouseEvent::hover_disc(), None),
SmallVec::from_buf([Rc::new(|_, _| {})]),
);
for button in MouseButton::all() {
set.insert((MouseEvent::drag_disc(), Some(button)), Rc::new(|_, _| {}));
set.insert((MouseEvent::down_disc(), Some(button)), Rc::new(|_, _| {}));
set.insert((MouseEvent::up_disc(), Some(button)), Rc::new(|_, _| {}));
set.insert((MouseEvent::click_disc(), Some(button)), Rc::new(|_, _| {}));
set.insert(
(MouseEvent::down_out_disc(), Some(button)),
Rc::new(|_, _| {}),
HandlerKey::new(MouseEvent::drag_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _| {})]),
);
set.insert(
(MouseEvent::up_out_disc(), Some(button)),
Rc::new(|_, _| {}),
HandlerKey::new(MouseEvent::down_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _| {})]),
);
set.insert(
HandlerKey::new(MouseEvent::up_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _| {})]),
);
set.insert(
HandlerKey::new(MouseEvent::click_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _| {})]),
);
set.insert(
HandlerKey::new(MouseEvent::down_out_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _| {})]),
);
set.insert(
HandlerKey::new(MouseEvent::up_out_disc(), Some(button)),
SmallVec::from_buf([Rc::new(|_, _| {})]),
);
}
set.insert((MouseEvent::scroll_wheel_disc(), None), Rc::new(|_, _| {}));
set.insert(
HandlerKey::new(MouseEvent::scroll_wheel_disc(), None),
SmallVec::from_buf([Rc::new(|_, _| {})]),
);
HandlerSet { set }
}
pub fn get(
&self,
key: &(Discriminant<MouseEvent>, Option<MouseButton>),
) -> Option<Rc<dyn Fn(MouseEvent, &mut EventContext)>> {
self.set.get(key).cloned()
pub fn get(&self, key: &HandlerKey) -> Option<&[HandlerCallback]> {
self.set.get(key).map(|vec| vec.as_slice())
}
pub fn contains_handler(
pub fn contains(
&self,
event: Discriminant<MouseEvent>,
discriminant: Discriminant<MouseEvent>,
button: Option<MouseButton>,
) -> bool {
self.set.contains_key(&(event, button))
self.set
.contains_key(&HandlerKey::new(discriminant, button))
}
fn insert(
&mut self,
event_kind: Discriminant<MouseEvent>,
button: Option<MouseButton>,
callback: HandlerCallback,
) {
use std::collections::hash_map::Entry;
match self.set.entry(HandlerKey::new(event_kind, button)) {
Entry::Occupied(mut vec) => {
vec.get_mut().push(callback);
}
Entry::Vacant(entry) => {
entry.insert(SmallVec::from_buf([callback]));
}
}
}
pub fn on_move(mut self, handler: impl Fn(MouseMove, &mut EventContext) + 'static) -> Self {
self.set.insert((MouseEvent::move_disc(), None),
self.insert(MouseEvent::move_disc(), None,
Rc::new(move |region_event, cx| {
if let MouseEvent::Move(e) = region_event {
handler(e, cx);
@@ -249,7 +294,7 @@ impl HandlerSet {
button: MouseButton,
handler: impl Fn(MouseDown, &mut EventContext) + 'static,
) -> Self {
self.set.insert((MouseEvent::down_disc(), Some(button)),
self.insert(MouseEvent::down_disc(), Some(button),
Rc::new(move |region_event, cx| {
if let MouseEvent::Down(e) = region_event {
handler(e, cx);
@@ -267,7 +312,7 @@ impl HandlerSet {
button: MouseButton,
handler: impl Fn(MouseUp, &mut EventContext) + 'static,
) -> Self {
self.set.insert((MouseEvent::up_disc(), Some(button)),
self.insert(MouseEvent::up_disc(), Some(button),
Rc::new(move |region_event, cx| {
if let MouseEvent::Up(e) = region_event {
handler(e, cx);
@@ -285,7 +330,7 @@ impl HandlerSet {
button: MouseButton,
handler: impl Fn(MouseClick, &mut EventContext) + 'static,
) -> Self {
self.set.insert((MouseEvent::click_disc(), Some(button)),
self.insert(MouseEvent::click_disc(), Some(button),
Rc::new(move |region_event, cx| {
if let MouseEvent::Click(e) = region_event {
handler(e, cx);
@@ -303,7 +348,7 @@ impl HandlerSet {
button: MouseButton,
handler: impl Fn(MouseDownOut, &mut EventContext) + 'static,
) -> Self {
self.set.insert((MouseEvent::down_out_disc(), Some(button)),
self.insert(MouseEvent::down_out_disc(), Some(button),
Rc::new(move |region_event, cx| {
if let MouseEvent::DownOut(e) = region_event {
handler(e, cx);
@@ -321,7 +366,7 @@ impl HandlerSet {
button: MouseButton,
handler: impl Fn(MouseUpOut, &mut EventContext) + 'static,
) -> Self {
self.set.insert((MouseEvent::up_out_disc(), Some(button)),
self.insert(MouseEvent::up_out_disc(), Some(button),
Rc::new(move |region_event, cx| {
if let MouseEvent::UpOut(e) = region_event {
handler(e, cx);
@@ -339,7 +384,7 @@ impl HandlerSet {
button: MouseButton,
handler: impl Fn(MouseDrag, &mut EventContext) + 'static,
) -> Self {
self.set.insert((MouseEvent::drag_disc(), Some(button)),
self.insert(MouseEvent::drag_disc(), Some(button),
Rc::new(move |region_event, cx| {
if let MouseEvent::Drag(e) = region_event {
handler(e, cx);
@@ -353,7 +398,7 @@ impl HandlerSet {
}
pub fn on_hover(mut self, handler: impl Fn(MouseHover, &mut EventContext) + 'static) -> Self {
self.set.insert((MouseEvent::hover_disc(), None),
self.insert(MouseEvent::hover_disc(), None,
Rc::new(move |region_event, cx| {
if let MouseEvent::Hover(e) = region_event {
handler(e, cx);
@@ -370,7 +415,7 @@ impl HandlerSet {
mut self,
handler: impl Fn(MouseScrollWheel, &mut EventContext) + 'static,
) -> Self {
self.set.insert((MouseEvent::scroll_wheel_disc(), None),
self.insert(MouseEvent::scroll_wheel_disc(), None,
Rc::new(move |region_event, cx| {
if let MouseEvent::ScrollWheel(e) = region_event {
handler(e, cx);

View File

@@ -16,4 +16,4 @@ chrono = "0.4"
dirs = "4.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
settings = { path = "../settings" }
shellexpand = "2.1.0"
shellexpand = "2.1.0"

View File

@@ -61,7 +61,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
if let Some(editor) = item.downcast::<Editor>() {
editor.update(&mut cx, |editor, cx| {
let len = editor.buffer().read(cx).len(cx);
editor.change_selections(Some(Autoscroll::Center), cx, |s| {
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
s.select_ranges([len..len])
});
if len > 0 {

View File

@@ -72,4 +72,5 @@ tree-sitter-rust = "*"
tree-sitter-python = "*"
tree-sitter-typescript = "*"
tree-sitter-ruby = "*"
tree-sitter-embedded-template = "*"
unindent = "0.1.7"

View File

@@ -28,6 +28,7 @@ use std::{
any::Any,
cell::RefCell,
fmt::Debug,
hash::Hash,
mem,
ops::Range,
path::{Path, PathBuf},
@@ -134,6 +135,10 @@ impl CachedLspAdapter {
self.adapter.process_diagnostics(params).await
}
pub async fn process_completion(&self, completion_item: &mut lsp::CompletionItem) {
self.adapter.process_completion(completion_item).await
}
pub async fn label_for_completion(
&self,
completion_item: &lsp::CompletionItem,
@@ -174,6 +179,8 @@ pub trait LspAdapter: 'static + Send + Sync {
async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
async fn process_completion(&self, _: &mut lsp::CompletionItem) {}
async fn label_for_completion(
&self,
_: &lsp::CompletionItem,
@@ -326,7 +333,13 @@ struct InjectionConfig {
query: Query,
content_capture_ix: u32,
language_capture_ix: Option<u32>,
languages_by_pattern_ix: Vec<Option<Box<str>>>,
patterns: Vec<InjectionPatternConfig>,
}
#[derive(Default, Clone)]
struct InjectionPatternConfig {
language: Option<Box<str>>,
combined: bool,
}
struct BracketConfig {
@@ -637,6 +650,10 @@ impl Language {
self.adapter.clone()
}
pub fn id(&self) -> Option<usize> {
self.grammar.as_ref().map(|g| g.id)
}
pub fn with_highlights_query(mut self, source: &str) -> Result<Self> {
let grammar = self.grammar_mut();
grammar.highlights_query = Some(Query::new(grammar.ts_language, source)?);
@@ -730,15 +747,21 @@ impl Language {
("content", &mut content_capture_ix),
],
);
let languages_by_pattern_ix = (0..query.pattern_count())
let patterns = (0..query.pattern_count())
.map(|ix| {
query.property_settings(ix).iter().find_map(|setting| {
if setting.key.as_ref() == "language" {
return setting.value.clone();
} else {
None
let mut config = InjectionPatternConfig::default();
for setting in query.property_settings(ix) {
match setting.key.as_ref() {
"language" => {
config.language = setting.value.clone();
}
"combined" => {
config.combined = true;
}
_ => {}
}
})
}
config
})
.collect();
if let Some(content_capture_ix) = content_capture_ix {
@@ -746,7 +769,7 @@ impl Language {
query,
language_capture_ix,
content_capture_ix,
languages_by_pattern_ix,
patterns,
});
}
Ok(self)
@@ -809,6 +832,12 @@ impl Language {
}
}
pub async fn process_completion(self: &Arc<Self>, completion: &mut lsp::CompletionItem) {
if let Some(adapter) = self.adapter.as_ref() {
adapter.process_completion(completion).await;
}
}
pub async fn label_for_completion(
self: &Arc<Self>,
completion: &lsp::CompletionItem,
@@ -883,6 +912,20 @@ impl Language {
}
}
impl Hash for Language {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id().hash(state)
}
}
impl PartialEq for Language {
fn eq(&self, other: &Self) -> bool {
self.id().eq(&other.id())
}
}
impl Eq for Language {}
impl Debug for Language {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Language")

View File

@@ -426,10 +426,11 @@ pub async fn deserialize_completion(
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("invalid old end"))?;
let lsp_completion = serde_json::from_slice(&completion.lsp_completion)?;
let label = match language {
Some(l) => l.label_for_completion(&lsp_completion).await,
None => None,
};
let mut label = None;
if let Some(language) = language {
label = language.label_for_completion(&lsp_completion).await;
}
Ok(Completion {
old_range: old_start..old_end,

File diff suppressed because it is too large Load Diff

View File

@@ -122,7 +122,7 @@ impl OutlineView {
let display_rows = start.to_display_point(&snapshot).row()
..end.to_display_point(&snapshot).row() + 1;
active_editor.highlight_rows(Some(display_rows));
active_editor.request_autoscroll(Autoscroll::Center, cx);
active_editor.request_autoscroll(Autoscroll::center(), cx);
});
}
cx.notify();
@@ -219,7 +219,7 @@ impl PickerDelegate for OutlineView {
if let Some(rows) = active_editor.highlighted_rows() {
let snapshot = active_editor.snapshot(cx).display_snapshot;
let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
active_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
active_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
s.select_ranges([position..position])
});
}

View File

@@ -3329,88 +3329,91 @@ impl Project {
let snapshot = this.snapshot();
let clipped_position = this.clip_point_utf16(position, Bias::Left);
let mut range_for_token = None;
completions.into_iter().filter_map(move |lsp_completion| {
// For now, we can only handle additional edits if they are returned
// when resolving the completion, not if they are present initially.
if lsp_completion
.additional_text_edits
.as_ref()
.map_or(false, |edits| !edits.is_empty())
{
return None;
}
let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() {
// If the language server provides a range to overwrite, then
// check that the range is valid.
Some(lsp::CompletionTextEdit::Edit(edit)) => {
let range = range_from_lsp(edit.range);
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
if start != range.start || end != range.end {
log::info!("completion out of expected range");
return None;
}
(
snapshot.anchor_before(start)..snapshot.anchor_after(end),
edit.new_text.clone(),
)
}
// If the language server does not provide a range, then infer
// the range based on the syntax tree.
None => {
if position != clipped_position {
log::info!("completion out of expected range");
return None;
}
let Range { start, end } = range_for_token
.get_or_insert_with(|| {
let offset = position.to_offset(&snapshot);
let (range, kind) = snapshot.surrounding_word(offset);
if kind == Some(CharKind::Word) {
range
} else {
offset..offset
}
})
.clone();
let text = lsp_completion
.insert_text
.as_ref()
.unwrap_or(&lsp_completion.label)
.clone();
(
snapshot.anchor_before(start)..snapshot.anchor_after(end),
text,
)
}
Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => {
log::info!("unsupported insert/replace completion");
completions
.into_iter()
.filter_map(move |mut lsp_completion| {
// For now, we can only handle additional edits if they are returned
// when resolving the completion, not if they are present initially.
if lsp_completion
.additional_text_edits
.as_ref()
.map_or(false, |edits| !edits.is_empty())
{
return None;
}
};
LineEnding::normalize(&mut new_text);
let language = language.clone();
Some(async move {
let label = if let Some(language) = language {
language.label_for_completion(&lsp_completion).await
} else {
None
};
Completion {
old_range,
new_text,
label: label.unwrap_or_else(|| {
CodeLabel::plain(
lsp_completion.label.clone(),
lsp_completion.filter_text.as_deref(),
let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref()
{
// If the language server provides a range to overwrite, then
// check that the range is valid.
Some(lsp::CompletionTextEdit::Edit(edit)) => {
let range = range_from_lsp(edit.range);
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
if start != range.start || end != range.end {
log::info!("completion out of expected range");
return None;
}
(
snapshot.anchor_before(start)..snapshot.anchor_after(end),
edit.new_text.clone(),
)
}),
lsp_completion,
}
}
// If the language server does not provide a range, then infer
// the range based on the syntax tree.
None => {
if position != clipped_position {
log::info!("completion out of expected range");
return None;
}
let Range { start, end } = range_for_token
.get_or_insert_with(|| {
let offset = position.to_offset(&snapshot);
let (range, kind) = snapshot.surrounding_word(offset);
if kind == Some(CharKind::Word) {
range
} else {
offset..offset
}
})
.clone();
let text = lsp_completion
.insert_text
.as_ref()
.unwrap_or(&lsp_completion.label)
.clone();
(
snapshot.anchor_before(start)..snapshot.anchor_after(end),
text,
)
}
Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => {
log::info!("unsupported insert/replace completion");
return None;
}
};
LineEnding::normalize(&mut new_text);
let language = language.clone();
Some(async move {
let mut label = None;
if let Some(language) = language {
language.process_completion(&mut lsp_completion).await;
label = language.label_for_completion(&lsp_completion).await;
}
Completion {
old_range,
new_text,
label: label.unwrap_or_else(|| {
CodeLabel::plain(
lsp_completion.label.clone(),
lsp_completion.filter_text.as_deref(),
)
}),
lsp_completion,
}
})
})
})
});
Ok(futures::future::join_all(completions).await)
@@ -3453,29 +3456,41 @@ impl Project {
let buffer_id = buffer.remote_id();
if self.is_local() {
let lang_server = if let Some((_, server)) = self.language_server_for_buffer(buffer, cx)
{
server.clone()
} else {
return Task::ready(Ok(Default::default()));
let lang_server = match self.language_server_for_buffer(buffer, cx) {
Some((_, server)) => server.clone(),
_ => return Task::ready(Ok(Default::default())),
};
cx.spawn(|this, mut cx| async move {
let resolved_completion = lang_server
.request::<lsp::request::ResolveCompletionItem>(completion.lsp_completion)
.await?;
if let Some(edits) = resolved_completion.additional_text_edits {
let edits = this
.update(&mut cx, |this, cx| {
this.edits_from_lsp(&buffer_handle, edits, None, cx)
})
.await?;
buffer_handle.update(&mut cx, |buffer, cx| {
buffer.finalize_last_transaction();
buffer.start_transaction();
for (range, text) in edits {
buffer.edit([(range, text)], None, cx);
let primary = &completion.old_range;
let start_within = primary.start.cmp(&range.start, buffer).is_le()
&& primary.end.cmp(&range.start, buffer).is_ge();
let end_within = range.start.cmp(&primary.end, buffer).is_le()
&& range.end.cmp(&primary.end, buffer).is_ge();
//Skip addtional edits which overlap with the primary completion edit
//https://github.com/zed-industries/zed/pull/1871
if !start_within && !end_within {
buffer.edit([(range, text)], None, cx);
}
}
let transaction = if buffer.end_transaction(cx).is_some() {
let transaction = buffer.finalize_last_transaction().unwrap().clone();
if !push_to_history {
@@ -3574,6 +3589,7 @@ impl Project {
context: lsp::CodeActionContext {
diagnostics: relevant_diagnostics,
only: Some(vec![
lsp::CodeActionKind::EMPTY,
lsp::CodeActionKind::QUICKFIX,
lsp::CodeActionKind::REFACTOR,
lsp::CodeActionKind::REFACTOR_EXTRACT,

View File

@@ -1179,6 +1179,10 @@ impl Snapshot {
self.id
}
pub fn abs_path(&self) -> &Arc<Path> {
&self.abs_path
}
pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool {
self.entries_by_id.get(&entry_id, &()).is_some()
}
@@ -1370,10 +1374,6 @@ impl Snapshot {
}
impl LocalSnapshot {
pub fn abs_path(&self) -> &Arc<Path> {
&self.abs_path
}
pub fn extension_counts(&self) -> &HashMap<OsString, usize> {
&self.extension_counts
}

View File

@@ -9,6 +9,7 @@ doctest = false
[dependencies]
context_menu = { path = "../context_menu" }
drag_and_drop = { path = "../drag_and_drop" }
editor = { path = "../editor" }
gpui = { path = "../gpui" }
menu = { path = "../menu" }

View File

@@ -1,12 +1,13 @@
use context_menu::{ContextMenu, ContextMenuItem};
use drag_and_drop::{DragAndDrop, Draggable};
use editor::{Cancel, Editor};
use futures::stream::StreamExt;
use gpui::{
actions,
anyhow::{anyhow, Result},
elements::{
AnchorCorner, ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler,
ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
AnchorCorner, ChildView, ConstrainedBox, ContainerStyle, Empty, Flex, Label,
MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
},
geometry::vector::Vector2F,
impl_internal_actions, keymap,
@@ -25,6 +26,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use theme::ProjectPanelEntry;
use unicase::UniCase;
use workspace::Workspace;
@@ -41,6 +43,7 @@ pub struct ProjectPanel {
filename_editor: ViewHandle<Editor>,
clipboard_entry: Option<ClipboardEntry>,
context_menu: ViewHandle<ContextMenu>,
dragged_entry_destination: Option<Arc<Path>>,
}
#[derive(Copy, Clone)]
@@ -71,8 +74,9 @@ pub enum ClipboardEntry {
}
#[derive(Debug, PartialEq, Eq)]
struct EntryDetails {
pub struct EntryDetails {
filename: String,
path: Arc<Path>,
depth: usize,
kind: EntryKind,
is_ignored: bool,
@@ -92,6 +96,13 @@ pub struct Open {
pub change_focus: bool,
}
#[derive(Clone, PartialEq)]
pub struct MoveProjectEntry {
pub entry_to_move: ProjectEntryId,
pub destination: ProjectEntryId,
pub destination_is_file: bool,
}
#[derive(Clone, PartialEq)]
pub struct DeployContextMenu {
pub position: Vector2F,
@@ -114,7 +125,10 @@ actions!(
ToggleFocus
]
);
impl_internal_actions!(project_panel, [Open, ToggleExpanded, DeployContextMenu]);
impl_internal_actions!(
project_panel,
[Open, ToggleExpanded, DeployContextMenu, MoveProjectEntry]
);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ProjectPanel::deploy_context_menu);
@@ -138,6 +152,7 @@ pub fn init(cx: &mut MutableAppContext) {
this.paste(action, cx);
},
);
cx.add_action(ProjectPanel::move_entry);
}
pub enum Event {
@@ -216,10 +231,12 @@ impl ProjectPanel {
filename_editor,
clipboard_entry: None,
context_menu: cx.add_view(ContextMenu::new),
dragged_entry_destination: None,
};
this.update_visible_entries(None, cx);
this
});
cx.subscribe(&project_panel, {
let project_panel = project_panel.downgrade();
move |workspace, _, event, cx| match event {
@@ -235,6 +252,7 @@ impl ProjectPanel {
worktree_id: worktree.read(cx).id(),
path: entry.path.clone(),
},
None,
focus_opened_item,
cx,
)
@@ -601,6 +619,10 @@ impl ProjectPanel {
cx.notify();
}
}
cx.update_global(|drag_and_drop: &mut DragAndDrop<Workspace>, cx| {
drag_and_drop.cancel_dragging::<ProjectEntryId>(cx);
})
}
}
@@ -765,6 +787,39 @@ impl ProjectPanel {
}
}
fn move_entry(
&mut self,
&MoveProjectEntry {
entry_to_move,
destination,
destination_is_file,
}: &MoveProjectEntry,
cx: &mut ViewContext<Self>,
) {
let destination_worktree = self.project.update(cx, |project, cx| {
let entry_path = project.path_for_entry(entry_to_move, cx)?;
let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone();
let mut destination_path = destination_entry_path.as_ref();
if destination_is_file {
destination_path = destination_path.parent()?;
}
let mut new_path = destination_path.to_path_buf();
new_path.push(entry_path.path.file_name()?);
if new_path != entry_path.path.as_ref() {
let task = project.rename_entry(entry_to_move, new_path, cx)?;
cx.foreground().spawn(task).detach_and_log_err(cx);
}
Some(project.worktree_id_for_entry(destination, cx)?)
});
if let Some(destination_worktree) = destination_worktree {
self.expand_entry(destination_worktree, destination, cx);
}
}
fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> {
let mut entry_index = 0;
let mut visible_entries_index = 0;
@@ -950,14 +1005,15 @@ impl ProjectPanel {
let end_ix = range.end.min(ix + visible_worktree_entries.len());
if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
let snapshot = worktree.read(cx).snapshot();
let root_name = OsStr::new(snapshot.root_name());
let expanded_entry_ids = self
.expanded_dir_ids
.get(&snapshot.id())
.map(Vec::as_slice)
.unwrap_or(&[]);
let root_name = OsStr::new(snapshot.root_name());
for entry in &visible_worktree_entries[range.start.saturating_sub(ix)..end_ix - ix]
{
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
for entry in &visible_worktree_entries[entry_range] {
let mut details = EntryDetails {
filename: entry
.path
@@ -965,6 +1021,7 @@ impl ProjectPanel {
.unwrap_or(root_name)
.to_string_lossy()
.to_string(),
path: entry.path.clone(),
depth: entry.path.components().count(),
kind: entry.kind,
is_ignored: entry.is_ignored,
@@ -978,12 +1035,14 @@ impl ProjectPanel {
.clipboard_entry
.map_or(false, |e| e.is_cut() && e.entry_id() == entry.id),
};
if let Some(edit_state) = &self.edit_state {
let is_edited_entry = if edit_state.is_new_entry {
entry.id == NEW_ENTRY_ID
} else {
entry.id == edit_state.entry_id
};
if is_edited_entry {
if let Some(processing_filename) = &edit_state.processing_filename {
details.is_processing = true;
@@ -1005,77 +1064,115 @@ impl ProjectPanel {
}
}
fn render_entry_visual_element<V: View>(
details: &EntryDetails,
editor: Option<&ViewHandle<Editor>>,
padding: f32,
row_container_style: ContainerStyle,
style: &ProjectPanelEntry,
cx: &mut RenderContext<V>,
) -> ElementBox {
let kind = details.kind;
let show_editor = details.is_editing && !details.is_processing;
Flex::row()
.with_child(
ConstrainedBox::new(if kind == EntryKind::Dir {
if details.is_expanded {
Svg::new("icons/chevron_down_8.svg")
.with_color(style.icon_color)
.boxed()
} else {
Svg::new("icons/chevron_right_8.svg")
.with_color(style.icon_color)
.boxed()
}
} else {
Empty::new().boxed()
})
.with_max_width(style.icon_size)
.with_max_height(style.icon_size)
.aligned()
.constrained()
.with_width(style.icon_size)
.boxed(),
)
.with_child(if show_editor && editor.is_some() {
ChildView::new(editor.unwrap().clone(), cx)
.contained()
.with_margin_left(style.icon_spacing)
.aligned()
.left()
.flex(1.0, true)
.boxed()
} else {
Label::new(details.filename.clone(), style.text.clone())
.contained()
.with_margin_left(style.icon_spacing)
.aligned()
.left()
.boxed()
})
.constrained()
.with_height(style.height)
.contained()
.with_style(row_container_style)
.with_padding_left(padding)
.boxed()
}
fn render_entry(
entry_id: ProjectEntryId,
details: EntryDetails,
editor: &ViewHandle<Editor>,
dragged_entry_destination: &mut Option<Arc<Path>>,
theme: &theme::ProjectPanel,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let this = cx.handle();
let kind = details.kind;
let path = details.path.clone();
let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
let entry_style = if details.is_cut {
&theme.cut_entry
} else if details.is_ignored {
&theme.ignored_entry
} else {
&theme.entry
};
let show_editor = details.is_editing && !details.is_processing;
MouseEventHandler::<Self>::new(entry_id.to_usize(), cx, |state, cx| {
let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
let mut style = entry_style.style_for(state, details.is_selected).clone();
let entry_style = if details.is_cut {
&theme.cut_entry
} else if details.is_ignored {
&theme.ignored_entry
} else {
&theme.entry
};
let style = entry_style.style_for(state, details.is_selected).clone();
if cx
.global::<DragAndDrop<Workspace>>()
.currently_dragged::<ProjectEntryId>(cx.window_id())
.is_some()
&& dragged_entry_destination
.as_ref()
.filter(|destination| details.path.starts_with(destination))
.is_some()
{
style = entry_style.active.clone().unwrap();
}
let row_container_style = if show_editor {
theme.filename_editor.container
} else {
style.container
};
Flex::row()
.with_child(
ConstrainedBox::new(if kind == EntryKind::Dir {
if details.is_expanded {
Svg::new("icons/chevron_down_8.svg")
.with_color(style.icon_color)
.boxed()
} else {
Svg::new("icons/chevron_right_8.svg")
.with_color(style.icon_color)
.boxed()
}
} else {
Empty::new().boxed()
})
.with_max_width(style.icon_size)
.with_max_height(style.icon_size)
.aligned()
.constrained()
.with_width(style.icon_size)
.boxed(),
)
.with_child(if show_editor {
ChildView::new(editor.clone(), cx)
.contained()
.with_margin_left(theme.entry.default.icon_spacing)
.aligned()
.left()
.flex(1.0, true)
.boxed()
} else {
Label::new(details.filename, style.text.clone())
.contained()
.with_margin_left(style.icon_spacing)
.aligned()
.left()
.boxed()
})
.constrained()
.with_height(theme.entry.default.height)
.contained()
.with_style(row_container_style)
.with_padding_left(padding)
.boxed()
Self::render_entry_visual_element(
&details,
Some(editor),
padding,
row_container_style,
&style,
cx,
)
})
.on_click(MouseButton::Left, move |e, cx| {
if kind == EntryKind::Dir {
@@ -1093,6 +1190,50 @@ impl ProjectPanel {
position: e.position,
})
})
.on_up(MouseButton::Left, move |_, cx| {
if let Some((_, dragged_entry)) = cx
.global::<DragAndDrop<Workspace>>()
.currently_dragged::<ProjectEntryId>(cx.window_id())
{
cx.dispatch_action(MoveProjectEntry {
entry_to_move: *dragged_entry,
destination: entry_id,
destination_is_file: matches!(details.kind, EntryKind::File(_)),
});
}
})
.on_move(move |_, cx| {
if cx
.global::<DragAndDrop<Workspace>>()
.currently_dragged::<ProjectEntryId>(cx.window_id())
.is_some()
{
if let Some(this) = this.upgrade(cx.app) {
this.update(cx.app, |this, _| {
this.dragged_entry_destination = if matches!(kind, EntryKind::File(_)) {
path.parent().map(|parent| Arc::from(parent))
} else {
Some(path.clone())
};
})
}
}
})
.as_draggable(entry_id, {
let row_container_style = theme.dragged_entry.container;
move |_, cx: &mut RenderContext<Workspace>| {
let theme = cx.global::<Settings>().theme.clone();
Self::render_entry_visual_element(
&details,
None,
padding,
row_container_style,
&theme.project_panel.dragged_entry,
cx,
)
}
})
.with_cursor_style(CursorStyle::PointingHand)
.boxed()
}
@@ -1104,14 +1245,15 @@ impl View for ProjectPanel {
}
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
enum Tag {}
enum ProjectPanel {}
let theme = &cx.global::<Settings>().theme.project_panel;
let mut container_style = theme.container;
let padding = std::mem::take(&mut container_style.padding);
let last_worktree_root_id = self.last_worktree_root_id;
Stack::new()
.with_child(
MouseEventHandler::<Tag>::new(0, cx, |_, cx| {
MouseEventHandler::<ProjectPanel>::new(0, cx, |_, cx| {
UniformList::new(
self.list.clone(),
self.visible_entries
@@ -1121,15 +1263,19 @@ impl View for ProjectPanel {
cx,
move |this, range, items, cx| {
let theme = cx.global::<Settings>().theme.clone();
let mut dragged_entry_destination =
this.dragged_entry_destination.clone();
this.for_each_visible_entry(range, cx, |id, details, cx| {
items.push(Self::render_entry(
id,
details,
&this.filename_editor,
&mut dragged_entry_destination,
&theme.project_panel,
cx,
));
});
this.dragged_entry_destination = dragged_entry_destination;
},
)
.with_padding_top(padding.top)

View File

@@ -28,4 +28,4 @@ settings = { path = "../settings", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }

View File

@@ -150,7 +150,7 @@ impl ProjectSymbolsView {
let editor = workspace.open_project_item::<Editor>(buffer, cx);
editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Center), cx, |s| {
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
s.select_ranges([position..position])
});
});

View File

@@ -512,7 +512,7 @@ impl ProjectSearchView {
let range_to_select = match_ranges[new_index].clone();
self.results_editor.update(cx, |editor, cx| {
editor.unfold_ranges([range_to_select.clone()], false, cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([range_to_select])
});
});
@@ -546,7 +546,7 @@ impl ProjectSearchView {
} else {
self.results_editor.update(cx, |editor, cx| {
if reset_selections {
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(match_ranges.first().cloned())
});
}

View File

@@ -28,6 +28,7 @@ pub struct Settings {
pub buffer_font_family: FamilyId,
pub default_buffer_font_size: f32,
pub buffer_font_size: f32,
pub active_pane_magnification: f32,
pub cursor_blink: bool,
pub hover_popover_enabled: bool,
pub show_completions_on_input: bool,
@@ -253,6 +254,8 @@ pub struct SettingsFileContent {
#[serde(default)]
pub buffer_font_size: Option<f32>,
#[serde(default)]
pub active_pane_magnification: Option<f32>,
#[serde(default)]
pub cursor_blink: Option<bool>,
#[serde(default)]
pub hover_popover_enabled: Option<bool>,
@@ -312,6 +315,7 @@ impl Settings {
.load_family(&[defaults.buffer_font_family.as_ref().unwrap()])
.unwrap(),
buffer_font_size: defaults.buffer_font_size.unwrap(),
active_pane_magnification: defaults.active_pane_magnification.unwrap(),
default_buffer_font_size: defaults.buffer_font_size.unwrap(),
cursor_blink: defaults.cursor_blink.unwrap(),
hover_popover_enabled: defaults.hover_popover_enabled.unwrap(),
@@ -367,6 +371,10 @@ impl Settings {
data.projects_online_by_default,
);
merge(&mut self.buffer_font_size, data.buffer_font_size);
merge(
&mut self.active_pane_magnification,
data.active_pane_magnification,
);
merge(&mut self.default_buffer_font_size, data.buffer_font_size);
merge(&mut self.cursor_blink, data.cursor_blink);
merge(&mut self.hover_popover_enabled, data.hover_popover_enabled);
@@ -458,6 +466,7 @@ impl Settings {
experiments: FeatureFlags::default(),
buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
buffer_font_size: 14.,
active_pane_magnification: 1.,
default_buffer_font_size: 14.,
cursor_blink: true,
hover_popover_enabled: true,

View File

@@ -36,18 +36,6 @@ impl Modifiers {
}
}
///This function checks if to_esc_str would work, assuming all terminal settings are off.
///Note that this function is conservative. It can fail in cases where the actual to_esc_str succeeds.
///This is unavoidable for our use case. GPUI cannot wait until we acquire the terminal
///lock to determine whether we could actually send the keystroke with the current settings. Therefore,
///This conservative guess is used instead. Note that in practice the case where this method
///Returns false when the actual terminal would consume the keystroke never happens. All keystrokes
///that depend on terminal modes also have a mapping that doesn't depend on the terminal mode.
///This is fragile, but as these mappings are locked up in legacy compatibility, it's probably good enough
pub fn might_convert(keystroke: &Keystroke) -> bool {
to_esc_str(keystroke, &TermMode::NONE, false).is_some()
}
pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode, alt_is_meta: bool) -> Option<String> {
let modifiers = Modifiers::new(keystroke);

View File

@@ -97,7 +97,7 @@ impl MouseButton {
}
fn from_scroll(e: &ScrollWheelEvent) -> Self {
if e.delta.y() > 0. {
if e.delta.raw().y() > 0. {
MouseButton::ScrollUp
} else {
MouseButton::ScrollDown

View File

@@ -407,13 +407,18 @@ impl TerminalBuilder {
'outer: loop {
let mut events = vec![];
let mut timer = cx.background().timer(Duration::from_millis(4)).fuse();
let mut wakeup = false;
loop {
futures::select_biased! {
_ = timer => break,
event = self.events_rx.next() => {
if let Some(event) = event {
events.push(event);
if matches!(event, AlacTermEvent::Wakeup) {
wakeup = true;
} else {
events.push(event);
}
if events.len() > 100 {
break;
}
@@ -424,11 +429,15 @@ impl TerminalBuilder {
}
}
if events.is_empty() {
if events.is_empty() && wakeup == false {
smol::future::yield_now().await;
break 'outer;
} else {
this.upgrade(&cx)?.update(&mut cx, |this, cx| {
if wakeup {
this.process_event(&AlacTermEvent::Wakeup, cx);
}
for event in events {
this.process_event(&event, cx);
}
@@ -627,7 +636,7 @@ impl Terminal {
term.grid_mut().reset_region(..cursor.line);
// Copy the current line up
let line = term.grid()[cursor.line][..cursor.column]
let line = term.grid()[cursor.line][..Column(term.grid().columns())]
.iter()
.cloned()
.enumerate()
@@ -1136,7 +1145,7 @@ impl Terminal {
fn determine_scroll_lines(&mut self, e: &MouseScrollWheel, mouse_mode: bool) -> Option<i32> {
let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER };
let line_height = self.last_content.size.line_height;
match e.phase {
/* Reset scroll state on started */
Some(gpui::TouchPhase::Started) => {
@@ -1145,11 +1154,11 @@ impl Terminal {
}
/* Calculate the appropriate scroll lines */
Some(gpui::TouchPhase::Moved) => {
let old_offset = (self.scroll_px / self.last_content.size.line_height) as i32;
let old_offset = (self.scroll_px / line_height) as i32;
self.scroll_px += e.delta.y() * scroll_multiplier;
self.scroll_px += e.delta.pixel_delta(line_height).y() * scroll_multiplier;
let new_offset = (self.scroll_px / self.last_content.size.line_height) as i32;
let new_offset = (self.scroll_px / line_height) as i32;
// Whenever we hit the edges, reset our stored scroll to 0
// so we can respond to changes in direction quickly
@@ -1159,7 +1168,7 @@ impl Terminal {
}
/* Fall back to delta / line_height */
None => Some(
((e.delta.y() * scroll_multiplier) / self.last_content.size.line_height) as i32,
((e.delta.pixel_delta(line_height).y() * scroll_multiplier) / line_height) as i32,
),
_ => None,
}

View File

@@ -326,6 +326,7 @@ pub struct ProjectPanel {
#[serde(flatten)]
pub container: ContainerStyle,
pub entry: Interactive<ProjectPanelEntry>,
pub dragged_entry: ProjectPanelEntry,
pub ignored_entry: Interactive<ProjectPanelEntry>,
pub cut_entry: Interactive<ProjectPanelEntry>,
pub filename_editor: FieldEditor,

View File

@@ -15,4 +15,4 @@ settings = { path = "../settings" }
workspace = { path = "../workspace" }
project = { path = "../project" }
smallvec = { version = "1.6", features = ["union"] }
smallvec = { version = "1.6", features = ["union"] }

View File

@@ -42,4 +42,4 @@ language = { path = "../language", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
settings = { path = "../settings" }
workspace = { path = "../workspace", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

View File

@@ -13,7 +13,7 @@ pub fn init(cx: &mut MutableAppContext) {
fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |state, cx| {
state.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, mut cursor, _| {
*cursor.column_mut() = cursor.column().saturating_sub(1);
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)

View File

@@ -137,6 +137,11 @@ impl Motion {
)
}
pub fn infallible(self) -> bool {
use Motion::*;
matches!(self, StartOfDocument | CurrentLine | EndOfDocument)
}
pub fn inclusive(self) -> bool {
use Motion::*;
match self {
@@ -164,9 +169,9 @@ impl Motion {
point: DisplayPoint,
goal: SelectionGoal,
times: usize,
) -> (DisplayPoint, SelectionGoal) {
) -> Option<(DisplayPoint, SelectionGoal)> {
use Motion::*;
match self {
let (new_point, goal) = match self {
Left => (left(map, point, times), SelectionGoal::None),
Backspace => (backspace(map, point, times), SelectionGoal::None),
Down => down(map, point, goal, times),
@@ -191,7 +196,9 @@ impl Motion {
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
EndOfDocument => (end_of_document(map, point, times), SelectionGoal::None),
Matching => (matching(map, point), SelectionGoal::None),
}
};
(new_point != point || self.infallible()).then_some((new_point, goal))
}
// Expands a selection using self motion for an operator
@@ -201,12 +208,13 @@ impl Motion {
selection: &mut Selection<DisplayPoint>,
times: usize,
expand_to_surrounding_newline: bool,
) {
let (new_head, goal) = self.move_point(map, selection.head(), selection.goal, times);
selection.set_head(new_head, goal);
) -> bool {
if let Some((new_head, goal)) =
self.move_point(map, selection.head(), selection.goal, times)
{
selection.set_head(new_head, goal);
if self.linewise() {
if selection.start != selection.end {
if self.linewise() {
selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
if expand_to_surrounding_newline {
@@ -215,7 +223,7 @@ impl Motion {
*selection.end.column_mut() = 0;
selection.end = map.clip_point(selection.end, Bias::Right);
// Don't reset the end here
return;
return true;
} else if selection.start.row() > 0 {
*selection.start.row_mut() -= 1;
*selection.start.column_mut() = map.line_len(selection.start.row());
@@ -224,31 +232,33 @@ impl Motion {
}
(_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
}
} else {
// If the motion is exclusive and the end of the motion is in column 1, the
// end of the motion is moved to the end of the previous line and the motion
// becomes inclusive. Example: "}" moves to the first line after a paragraph,
// but "d}" will not include that line.
let mut inclusive = self.inclusive();
if !inclusive
&& self != Motion::Backspace
&& selection.end.row() > selection.start.row()
&& selection.end.column() == 0
&& selection.end.row() > 0
{
inclusive = true;
*selection.end.row_mut() -= 1;
*selection.end.column_mut() = 0;
selection.end = map.clip_point(
map.next_line_boundary(selection.end.to_point(map)).1,
Bias::Left,
);
}
} else {
// If the motion is exclusive and the end of the motion is in column 1, the
// end of the motion is moved to the end of the previous line and the motion
// becomes inclusive. Example: "}" moves to the first line after a paragraph,
// but "d}" will not include that line.
let mut inclusive = self.inclusive();
if !inclusive
&& self != Motion::Backspace
&& selection.end.row() > selection.start.row()
&& selection.end.column() == 0
{
inclusive = true;
*selection.end.row_mut() -= 1;
*selection.end.column_mut() = 0;
selection.end = map.clip_point(
map.next_line_boundary(selection.end.to_point(map)).1,
Bias::Left,
);
}
if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
*selection.end.column_mut() += 1;
if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
*selection.end.column_mut() += 1;
}
}
true
} else {
false
}
}
}
@@ -256,7 +266,7 @@ impl Motion {
fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
for _ in 0..times {
*point.column_mut() = point.column().saturating_sub(1);
point = map.clip_point(point, Bias::Right);
point = map.clip_point(point, Bias::Left);
if point.column() == 0 {
break;
}
@@ -325,9 +335,7 @@ pub(crate) fn next_word_start(
|| at_newline && crossed_newline
|| at_newline && left == '\n'; // Prevents skipping repeated empty lines
if at_newline {
crossed_newline = true;
}
crossed_newline |= at_newline;
found
})
}
@@ -350,7 +358,7 @@ fn next_word_end(
});
// find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
// we have backtraced already
// we have backtracked already
if !map
.chars_at(point)
.nth(1)

View File

@@ -114,8 +114,12 @@ pub fn normal_object(object: Object, cx: &mut MutableAppContext) {
fn move_cursor(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_cursors_with(|map, cursor, goal| motion.move_point(map, cursor, goal, times))
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, goal| {
motion
.move_point(map, cursor, goal, times)
.unwrap_or((cursor, goal))
})
})
});
}
@@ -124,8 +128,8 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
Vim::update(cx, |vim, cx| {
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_cursors_with(|map, cursor, goal| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| {
Motion::Right.move_point(map, cursor, goal, 1)
});
});
@@ -141,8 +145,8 @@ fn insert_first_non_whitespace(
Vim::update(cx, |vim, cx| {
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_cursors_with(|map, cursor, goal| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| {
Motion::FirstNonWhitespace.move_point(map, cursor, goal, 1)
});
});
@@ -154,8 +158,8 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
Vim::update(cx, |vim, cx| {
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_cursors_with(|map, cursor, goal| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| {
Motion::EndOfLine.move_point(map, cursor, goal, 1)
});
});
@@ -183,7 +187,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
(start_of_line..start_of_line, new_text)
});
editor.edit_with_autoindent(edits, cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, mut cursor, _| {
*cursor.row_mut() -= 1;
*cursor.column_mut() = map.line_len(cursor.row());
@@ -214,8 +218,8 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
new_text.push_str(&" ".repeat(indent as usize));
(end_of_line..end_of_line, new_text)
});
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_cursors_with(|map, cursor, goal| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| {
Motion::EndOfLine.move_point(map, cursor, goal, 1)
});
});
@@ -332,7 +336,7 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
});
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
if let Some(new_position) = new_selections.get(&selection.id) {
match new_position {
@@ -847,4 +851,10 @@ mod test {
.await;
}
}
#[gpui::test]
async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
}
}

View File

@@ -1,27 +1,40 @@
use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim};
use editor::{char_kind, display_map::DisplaySnapshot, movement, Autoscroll, DisplayPoint};
use editor::{
char_kind, display_map::DisplaySnapshot, movement, Autoscroll, CharKind, DisplayPoint,
};
use gpui::MutableAppContext;
use language::Selection;
pub fn change_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
// Some motions ignore failure when switching to normal mode
let mut motion_succeeded = matches!(
motion,
Motion::Left | Motion::Right | Motion::EndOfLine | Motion::Backspace | Motion::StartOfLine
);
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
editor.set_clip_at_line_ends(false, cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
if let Motion::NextWordStart { ignore_punctuation } = motion {
expand_changed_word_selection(map, selection, times, ignore_punctuation);
motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion
{
expand_changed_word_selection(map, selection, times, ignore_punctuation)
} else {
motion.expand_selection(map, selection, times, false);
}
motion.expand_selection(map, selection, times, false)
};
});
});
copy_selections_content(editor, motion.linewise(), cx);
editor.insert("", cx);
});
});
vim.switch_mode(Mode::Insert, false, cx)
if motion_succeeded {
vim.switch_mode(Mode::Insert, false, cx)
} else {
vim.switch_mode(Mode::Normal, false, cx)
}
}
pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut MutableAppContext) {
@@ -30,7 +43,7 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Mutab
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
editor.set_clip_at_line_ends(false, cx);
editor.transact(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
objects_found |= object.expand_selection(map, selection, around);
});
@@ -49,36 +62,45 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Mutab
}
}
// From the docs https://vimhelp.org/change.txt.html#cw
// Special case: When the cursor is in a word, "cw" and "cW" do not include the
// white space after a word, they only change up to the end of the word. This is
// because Vim interprets "cw" as change-word, and a word does not include the
// following white space.
// From the docs https://vimdoc.sourceforge.net/htmldoc/motion.html
// Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is
// on a non-blank. This is because "cw" is interpreted as change-word, and a
// word does not include the following white space. {Vi: "cw" when on a blank
// followed by other blanks changes only the first blank; this is probably a
// bug, because "dw" deletes all the blanks}
//
// NOT HANDLED YET
// Another special case: When using the "w" motion in combination with an
// operator and the last word moved over is at the end of a line, the end of
// that word becomes the end of the operated text, not the first word in the
// next line.
fn expand_changed_word_selection(
map: &DisplaySnapshot,
selection: &mut Selection<DisplayPoint>,
times: usize,
ignore_punctuation: bool,
) {
if times > 1 {
Motion::NextWordStart { ignore_punctuation }.expand_selection(
map,
selection,
times - 1,
false,
);
) -> bool {
if times == 1 {
let in_word = map
.chars_at(selection.head())
.next()
.map(|(c, _)| char_kind(c) != CharKind::Whitespace)
.unwrap_or_default();
if in_word {
selection.end = movement::find_boundary(map, selection.end, |left, right| {
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
left_kind != right_kind && left_kind != CharKind::Whitespace
});
true
} else {
Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, 1, false)
}
} else {
Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, times, false)
}
if times == 1 && selection.end.column() == map.line_len(selection.end.row()) {
return;
}
selection.end = movement::find_boundary(map, selection.end, |left, right| {
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
left_kind != right_kind || left == '\n' || right == '\n'
});
}
#[cfg(test)]

View File

@@ -8,7 +8,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut Mutab
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let mut original_columns: HashMap<_, _> = Default::default();
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
let original_head = selection.head();
original_columns.insert(selection.id, original_head.column());
@@ -20,7 +20,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut Mutab
// Fixup cursor position after the deletion
editor.set_clip_at_line_ends(true, cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
let mut cursor = selection.head();
if motion.linewise() {
@@ -43,7 +43,7 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Mutab
// Emulates behavior in vim where if we expanded backwards to include a newline
// the cursor gets set back to the start of the line
let mut should_move_to_start: HashSet<_> = Default::default();
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
object.expand_selection(map, selection, around);
let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range();
@@ -78,7 +78,7 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Mutab
// Fixup cursor position after the deletion
editor.set_clip_at_line_ends(true, cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
let mut cursor = selection.head();
if should_move_to_start.contains(&selection.id) {
@@ -143,7 +143,7 @@ mod test {
Test test
ˇ
test"},
ExemptionFeatures::DeletionOnEmptyLine,
ExemptionFeatures::DeleteWordOnEmptyLine,
)
.await;
@@ -169,7 +169,7 @@ mod test {
Test test
ˇ
test"},
ExemptionFeatures::DeletionOnEmptyLine,
ExemptionFeatures::OperatorLastNewlineRemains,
)
.await;

View File

@@ -8,7 +8,10 @@ use util::test::marked_text_offsets;
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
use crate::state::Mode;
pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[];
pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[
ExemptionFeatures::DeletionOnEmptyLine,
ExemptionFeatures::OperatorAbortsOnFailedMotion,
];
/// Enum representing features we have tests for but which don't work, yet. Used
/// to add exemptions and automatically
@@ -19,6 +22,10 @@ pub enum ExemptionFeatures {
DeletionOnEmptyLine,
// When a motion fails, it should should not apply linewise operations
OperatorAbortsOnFailedMotion,
// When an operator completes at the end of the file, an extra newline is left
OperatorLastNewlineRemains,
// Deleting a word on an empty line doesn't remove the newline
DeleteWordOnEmptyLine,
// OBJECTS
// Resulting position after the operation is slightly incorrect for unintuitive reasons.

View File

@@ -67,7 +67,9 @@ impl<'a> VimTestContext<'a> {
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
let item = workspace
.update(cx, |workspace, cx| workspace.open_path(file, true, cx))
.update(cx, |workspace, cx| {
workspace.open_path(file, None, true, cx)
})
.await
.expect("Could not open test file");

View File

@@ -26,24 +26,27 @@ pub fn init(cx: &mut MutableAppContext) {
pub fn visual_motion(motion: Motion, times: usize, cx: &mut MutableAppContext) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
let was_reversed = selection.reversed;
let (new_head, goal) =
motion.move_point(map, selection.head(), selection.goal, times);
selection.set_head(new_head, goal);
if let Some((new_head, goal)) =
motion.move_point(map, selection.head(), selection.goal, times)
{
selection.set_head(new_head, goal);
if was_reversed && !selection.reversed {
// Head was at the start of the selection, and now is at the end. We need to move the start
// back by one if possible in order to compensate for this change.
*selection.start.column_mut() = selection.start.column().saturating_sub(1);
selection.start = map.clip_point(selection.start, Bias::Left);
} else if !was_reversed && selection.reversed {
// Head was at the end of the selection, and now is at the start. We need to move the end
// forward by one if possible in order to compensate for this change.
*selection.end.column_mut() = selection.end.column() + 1;
selection.end = map.clip_point(selection.end, Bias::Right);
if was_reversed && !selection.reversed {
// Head was at the start of the selection, and now is at the end. We need to move the start
// back by one if possible in order to compensate for this change.
*selection.start.column_mut() =
selection.start.column().saturating_sub(1);
selection.start = map.clip_point(selection.start, Bias::Left);
} else if !was_reversed && selection.reversed {
// Head was at the end of the selection, and now is at the start. We need to move the end
// forward by one if possible in order to compensate for this change.
*selection.end.column_mut() = selection.end.column() + 1;
selection.end = map.clip_point(selection.end, Bias::Right);
}
}
});
});
@@ -55,7 +58,7 @@ pub fn visual_object(object: Object, cx: &mut MutableAppContext) {
Vim::update(cx, |vim, cx| {
if let Operator::Object { around } = vim.pop_operator(cx) {
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
let head = selection.head();
if let Some(mut range) = object.range(map, head, around) {
@@ -123,7 +126,7 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspac
});
copy_selections_content(editor, editor.selections.line_mode, cx);
editor.edit_with_autoindent(edits, cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_anchors(new_selections);
});
});
@@ -137,7 +140,7 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspac
editor.set_clip_at_line_ends(false, cx);
let mut original_columns: HashMap<_, _> = Default::default();
let line_mode = editor.selections.line_mode;
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
if line_mode {
original_columns
@@ -156,7 +159,7 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspac
// Fixup cursor position after the deletion
editor.set_clip_at_line_ends(true, cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
let mut cursor = selection.head().to_point(map);
@@ -295,7 +298,7 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext<Workspace>
buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
});
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(new_selections)
});
} else {

View File

@@ -1 +1 @@
[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"}]

View File

@@ -1 +1 @@
[{"Text":"\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]
[{"Text":"\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nbrown fox\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nbrown fox\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]

View File

@@ -1 +1 @@
[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]
[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,6],"end":[2,6]}},{"Mode":"Normal"},{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]

View File

@@ -1 +1 @@
[{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
[{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]

View File

@@ -1 +1 @@
[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"}]
[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]

View File

@@ -1 +1 @@
[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"}]
[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,5],"end":[2,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]

View File

@@ -1 +1 @@
[{"Text":"jumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
[{"Text":"jumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]

View File

@@ -0,0 +1 @@
[{"Text":"Test├──┐Test"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"},{"Text":"Test├──┐Test"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"Test├──┐Test"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"Test├──┐Test"},{"Mode":"Normal"},{"Selection":{"start":[0,13],"end":[0,13]}},{"Mode":"Normal"}]

File diff suppressed because one or more lines are too long

View File

@@ -46,4 +46,4 @@ client = { path = "../client", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
fs = { path = "../fs", features = ["test-support"] }
fs = { path = "../fs", features = ["test-support"] }

View File

@@ -7,9 +7,13 @@ use gpui::{
AppContext, Element, ElementBox, EventContext, MouseButton, MouseState, Quad, RenderContext,
WeakViewHandle,
};
use project::ProjectEntryId;
use settings::Settings;
use crate::{MoveItem, Pane, SplitDirection, SplitWithItem, Workspace};
use crate::{
MoveItem, OpenProjectEntryInPane, Pane, SplitDirection, SplitWithItem, SplitWithProjectEntry,
Workspace,
};
use super::DraggedItem;
@@ -28,12 +32,18 @@ where
MouseEventHandler::<Tag>::above(region_id, cx, |state, cx| {
// Observing hovered will cause a render when the mouse enters regardless
// of if mouse position was accessed before
let hovered = state.hovered();
let drag_position = cx
.global::<DragAndDrop<Workspace>>()
.currently_dragged::<DraggedItem>(cx.window_id())
.filter(|_| hovered)
.map(|(drag_position, _)| drag_position);
let drag_position = if state.hovered() {
cx.global::<DragAndDrop<Workspace>>()
.currently_dragged::<DraggedItem>(cx.window_id())
.map(|(drag_position, _)| drag_position)
.or_else(|| {
cx.global::<DragAndDrop<Workspace>>()
.currently_dragged::<ProjectEntryId>(cx.window_id())
.map(|(drag_position, _)| drag_position)
})
} else {
None
};
Stack::new()
.with_child(render_child(state, cx))
@@ -70,10 +80,14 @@ where
}
})
.on_move(|_, cx| {
if cx
.global::<DragAndDrop<Workspace>>()
let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
if drag_and_drop
.currently_dragged::<DraggedItem>(cx.window_id())
.is_some()
|| drag_and_drop
.currently_dragged::<ProjectEntryId>(cx.window_id())
.is_some()
{
cx.notify();
} else {
@@ -90,30 +104,60 @@ pub fn handle_dropped_item(
split_margin: Option<f32>,
cx: &mut EventContext,
) {
if let Some((_, dragged_item)) = cx
.global::<DragAndDrop<Workspace>>()
.currently_dragged::<DraggedItem>(cx.window_id)
enum Action {
Move(WeakViewHandle<Pane>, usize),
Open(ProjectEntryId),
}
let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
let action = if let Some((_, dragged_item)) =
drag_and_drop.currently_dragged::<DraggedItem>(cx.window_id)
{
if let Some(split_direction) = split_margin
.and_then(|margin| drop_split_direction(event.position, event.region, margin))
{
cx.dispatch_action(SplitWithItem {
from: dragged_item.pane.clone(),
item_id_to_move: dragged_item.item.id(),
pane_to_split: pane.clone(),
split_direction,
});
} else if pane != &dragged_item.pane || allow_same_pane {
// If no split margin or not close enough to the edge, just move the item
cx.dispatch_action(MoveItem {
item_id: dragged_item.item.id(),
from: dragged_item.pane.clone(),
to: pane.clone(),
destination_index: index,
})
}
Action::Move(dragged_item.pane.clone(), dragged_item.item.id())
} else if let Some((_, project_entry)) =
drag_and_drop.currently_dragged::<ProjectEntryId>(cx.window_id)
{
Action::Open(*project_entry)
} else {
cx.propagate_event();
return;
};
if let Some(split_direction) =
split_margin.and_then(|margin| drop_split_direction(event.position, event.region, margin))
{
let pane_to_split = pane.clone();
match action {
Action::Move(from, item_id_to_move) => cx.dispatch_action(SplitWithItem {
from,
item_id_to_move,
pane_to_split,
split_direction,
}),
Action::Open(project_entry) => cx.dispatch_action(SplitWithProjectEntry {
pane_to_split,
split_direction,
project_entry,
}),
};
} else {
match action {
Action::Move(from, item_id) => {
if pane != &from || allow_same_pane {
cx.dispatch_action(MoveItem {
item_id,
from,
to: pane.clone(),
destination_index: index,
})
} else {
cx.propagate_event();
}
}
Action::Open(project_entry) => cx.dispatch_action(OpenProjectEntryInPane {
pane: pane.clone(),
project_entry,
}),
}
}
}

View File

@@ -8,6 +8,7 @@ use gpui::{
};
use project::Project;
use serde::Deserialize;
use settings::Settings;
use theme::Theme;
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -63,10 +64,17 @@ impl PaneGroup {
theme: &Theme,
follower_states: &FollowerStatesByLeader,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
cx: &mut RenderContext<Workspace>,
) -> ElementBox {
self.root
.render(project, theme, follower_states, active_call, cx)
self.root.render(
project,
theme,
follower_states,
active_call,
active_pane,
cx,
)
}
pub(crate) fn panes(&self) -> Vec<&ViewHandle<Pane>> {
@@ -104,12 +112,20 @@ impl Member {
Member::Axis(PaneAxis { axis, members })
}
fn contains(&self, needle: &ViewHandle<Pane>) -> bool {
match self {
Member::Axis(axis) => axis.members.iter().any(|member| member.contains(needle)),
Member::Pane(pane) => pane == needle,
}
}
pub fn render(
&self,
project: &ModelHandle<Project>,
theme: &Theme,
follower_states: &FollowerStatesByLeader,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
cx: &mut RenderContext<Workspace>,
) -> ElementBox {
enum FollowIntoExternalProject {}
@@ -236,7 +252,14 @@ impl Member {
.with_children(prompt)
.boxed()
}
Member::Axis(axis) => axis.render(project, theme, follower_states, active_call, cx),
Member::Axis(axis) => axis.render(
project,
theme,
follower_states,
active_call,
active_pane,
cx,
),
}
}
@@ -337,12 +360,19 @@ impl PaneAxis {
theme: &Theme,
follower_state: &FollowerStatesByLeader,
active_call: Option<&ModelHandle<ActiveCall>>,
active_pane: &ViewHandle<Pane>,
cx: &mut RenderContext<Workspace>,
) -> ElementBox {
let last_member_ix = self.members.len() - 1;
Flex::new(self.axis)
.with_children(self.members.iter().enumerate().map(|(ix, member)| {
let mut member = member.render(project, theme, follower_state, active_call, cx);
let mut flex = 1.0;
if member.contains(active_pane) {
flex = cx.global::<Settings>().active_pane_magnification;
}
let mut member =
member.render(project, theme, follower_state, active_call, active_pane, cx);
if ix < last_member_ix {
let mut border = theme.workspace.pane_divider;
border.left = false;
@@ -356,7 +386,7 @@ impl PaneAxis {
member = Container::new(member).with_border(border).boxed();
}
FlexItem::new(member).flex(1.0, true).boxed()
FlexItem::new(member).flex(flex, true).boxed()
}))
.boxed()
}

View File

@@ -128,12 +128,25 @@ pub struct OpenSharedScreen {
#[derive(Clone, PartialEq)]
pub struct SplitWithItem {
from: WeakViewHandle<Pane>,
pane_to_split: WeakViewHandle<Pane>,
split_direction: SplitDirection,
from: WeakViewHandle<Pane>,
item_id_to_move: usize,
}
#[derive(Clone, PartialEq)]
pub struct SplitWithProjectEntry {
pane_to_split: WeakViewHandle<Pane>,
split_direction: SplitDirection,
project_entry: ProjectEntryId,
}
#[derive(Clone, PartialEq)]
pub struct OpenProjectEntryInPane {
pane: WeakViewHandle<Pane>,
project_entry: ProjectEntryId,
}
impl_internal_actions!(
workspace,
[
@@ -143,6 +156,8 @@ impl_internal_actions!(
OpenSharedScreen,
RemoveWorktreeFromProject,
SplitWithItem,
SplitWithProjectEntry,
OpenProjectEntryInPane,
]
);
impl_actions!(workspace, [ActivatePane]);
@@ -234,6 +249,57 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
},
);
cx.add_async_action(
|workspace: &mut Workspace,
SplitWithProjectEntry {
pane_to_split,
split_direction,
project_entry,
}: &_,
cx| {
pane_to_split.upgrade(cx).and_then(|pane_to_split| {
let new_pane = workspace.add_pane(cx);
workspace
.center
.split(&pane_to_split, &new_pane, *split_direction)
.unwrap();
workspace
.project
.read(cx)
.path_for_entry(*project_entry, cx)
.map(|path| {
let task = workspace.open_path(path, Some(new_pane.downgrade()), true, cx);
cx.foreground().spawn(async move {
task.await?;
Ok(())
})
})
})
},
);
cx.add_async_action(
|workspace: &mut Workspace,
OpenProjectEntryInPane {
pane,
project_entry,
}: &_,
cx| {
workspace
.project
.read(cx)
.path_for_entry(*project_entry, cx)
.map(|path| {
let task = workspace.open_path(path, Some(pane.clone()), true, cx);
cx.foreground().spawn(async move {
task.await?;
Ok(())
})
})
},
);
let client = &app_state.client;
client.add_view_request_handler(Workspace::handle_follow);
client.add_view_message_handler(Workspace::handle_unfollow);
@@ -1399,7 +1465,7 @@ impl Workspace {
mut abs_paths: Vec<PathBuf>,
visible: bool,
cx: &mut ViewContext<Self>,
) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>> {
) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
let fs = self.fs.clone();
// Sort the paths to ensure we add worktrees for parents before their children.
@@ -1429,7 +1495,7 @@ impl Workspace {
if fs.is_file(&abs_path).await {
Some(
this.update(&mut cx, |this, cx| {
this.open_path(project_path, true, cx)
this.open_path(project_path, None, true, cx)
})
.await,
)
@@ -1749,10 +1815,11 @@ impl Workspace {
pub fn open_path(
&mut self,
path: impl Into<ProjectPath>,
pane: Option<WeakViewHandle<Pane>>,
focus_item: bool,
cx: &mut ViewContext<Self>,
) -> Task<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>> {
let pane = self.active_pane().downgrade();
) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
let pane = pane.unwrap_or_else(|| self.active_pane().downgrade());
let task = self.load_path(path.into(), cx);
cx.spawn(|this, mut cx| async move {
let (project_entry_id, build_item) = task.await?;
@@ -2720,6 +2787,7 @@ impl View for Workspace {
&theme,
&self.follower_states_by_leader,
self.active_call(),
self.active_pane(),
cx,
))
.flex(1., true)
@@ -2874,7 +2942,7 @@ pub fn open_paths(
cx: &mut MutableAppContext,
) -> Task<(
ViewHandle<Workspace>,
Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
)> {
log::info!("open paths {:?}", abs_paths);

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.64.0"
version = "0.66.0"
[lib]
name = "zed"
@@ -95,6 +95,7 @@ tree-sitter-c = "0.20.1"
tree-sitter-cpp = "0.20.0"
tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" }
tree-sitter-embedded-template = "0.20.0"
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8" }
tree-sitter-rust = "0.20.3"

View File

@@ -1 +1 @@
dev
preview

View File

@@ -16,6 +16,9 @@ fn main() {
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
}
// Weakly link ReplayKit to ensure Zed can be used on macOS 10.15+.
println!("cargo:rustc-link-arg=-Wl,-weak_framework,ReplayKit");
// Seems to be required to enable Swift concurrency
println!("cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/swift");

View File

@@ -12,6 +12,7 @@ mod installation;
mod json;
mod language_plugin;
mod python;
mod ruby;
mod rust;
mod typescript;
@@ -116,7 +117,16 @@ pub async fn init(languages: Arc<LanguageRegistry>, _executor: Arc<Background>)
tree_sitter_html::language(),
Some(CachedLspAdapter::new(html::HtmlLspAdapter).await),
),
("ruby", tree_sitter_ruby::language(), None),
(
"ruby",
tree_sitter_ruby::language(),
Some(CachedLspAdapter::new(ruby::RubyLanguageServer).await),
),
(
"erb",
tree_sitter_embedded_template::language(),
Some(CachedLspAdapter::new(ruby::RubyLanguageServer).await),
),
] {
languages.add(language(name, grammar, lsp_adapter));
}

View File

@@ -0,0 +1,8 @@
name = "ERB"
path_suffixes = ["erb"]
autoclose_before = ">})"
brackets = [
{ start = "<", end = ">", close = true, newline = true },
]
block_comment = ["<%#", "%>"]

View File

@@ -0,0 +1,12 @@
(comment_directive) @comment
[
"<%#"
"<%"
"<%="
"<%_"
"<%-"
"%>"
"-%>"
"_%>"
] @keyword

View File

@@ -0,0 +1,7 @@
((code) @content
(#set! "language" "ruby")
(#set! "combined"))
((content) @content
(#set! "language" "html")
(#set! "combined"))

View File

@@ -87,6 +87,25 @@ impl LspAdapter for PythonLspAdapter {
.log_err()
}
async fn process_completion(&self, item: &mut lsp::CompletionItem) {
// Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`.
// Where `XX` is the sorting category, `YYYY` is based on most recent usage,
// and `name` is the symbol name itself.
//
// Because the the symbol name is included, there generally are not ties when
// sorting by the `sortText`, so the symbol's fuzzy match score is not taken
// into account. Here, we remove the symbol name from the sortText in order
// to allow our own fuzzy score to be used to break ties.
//
// see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873
let Some(sort_text) = &mut item.sort_text else { return };
let mut parts = sort_text.split('.');
let Some(first) = parts.next() else { return };
let Some(second) = parts.next() else { return };
let Some(_) = parts.next() else { return };
sort_text.replace_range(first.len() + second.len() + 1.., "");
}
async fn label_for_completion(
&self,
item: &lsp::CompletionItem,

View File

@@ -0,0 +1,145 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use client::http::HttpClient;
use language::{LanguageServerName, LspAdapter};
use std::{any::Any, path::PathBuf, sync::Arc};
pub struct RubyLanguageServer;
#[async_trait]
impl LspAdapter for RubyLanguageServer {
async fn name(&self) -> LanguageServerName {
LanguageServerName("solargraph".into())
}
async fn server_args(&self) -> Vec<String> {
vec!["stdio".into()]
}
async fn fetch_latest_server_version(
&self,
_: Arc<dyn HttpClient>,
) -> Result<Box<dyn 'static + Any + Send>> {
Ok(Box::new(()))
}
async fn fetch_server_binary(
&self,
_version: Box<dyn 'static + Send + Any>,
_: Arc<dyn HttpClient>,
_container_dir: PathBuf,
) -> Result<PathBuf> {
Err(anyhow!("solargraph must be installed manually"))
}
async fn cached_server_binary(&self, _container_dir: PathBuf) -> Option<PathBuf> {
Some("solargraph".into())
}
async fn label_for_completion(
&self,
item: &lsp::CompletionItem,
language: &Arc<language::Language>,
) -> Option<language::CodeLabel> {
let label = &item.label;
let grammar = language.grammar()?;
let highlight_id = match item.kind? {
lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
lsp::CompletionItemKind::CLASS | lsp::CompletionItemKind::MODULE => {
grammar.highlight_id_for_name("type")?
}
lsp::CompletionItemKind::KEYWORD => {
if label.starts_with(":") {
grammar.highlight_id_for_name("string.special.symbol")?
} else {
grammar.highlight_id_for_name("keyword")?
}
}
lsp::CompletionItemKind::VARIABLE => {
if label.starts_with("@") {
grammar.highlight_id_for_name("property")?
} else {
return None;
}
}
_ => return None,
};
Some(language::CodeLabel {
text: label.clone(),
runs: vec![(0..label.len(), highlight_id)],
filter_range: 0..label.len(),
})
}
async fn label_for_symbol(
&self,
label: &str,
kind: lsp::SymbolKind,
language: &Arc<language::Language>,
) -> Option<language::CodeLabel> {
let grammar = language.grammar()?;
match kind {
lsp::SymbolKind::METHOD => {
let mut parts = label.split('#');
let classes = parts.next()?;
let method = parts.next()?;
if parts.next().is_some() {
return None;
}
let class_id = grammar.highlight_id_for_name("type")?;
let method_id = grammar.highlight_id_for_name("function.method")?;
let mut ix = 0;
let mut runs = Vec::new();
for (i, class) in classes.split("::").enumerate() {
if i > 0 {
ix += 2;
}
let end_ix = ix + class.len();
runs.push((ix..end_ix, class_id));
ix = end_ix;
}
ix += 1;
let end_ix = ix + method.len();
runs.push((ix..end_ix, method_id));
Some(language::CodeLabel {
text: label.to_string(),
runs,
filter_range: 0..label.len(),
})
}
lsp::SymbolKind::CONSTANT => {
let constant_id = grammar.highlight_id_for_name("constant")?;
Some(language::CodeLabel {
text: label.to_string(),
runs: vec![(0..label.len(), constant_id)],
filter_range: 0..label.len(),
})
}
lsp::SymbolKind::CLASS | lsp::SymbolKind::MODULE => {
let class_id = grammar.highlight_id_for_name("type")?;
let mut ix = 0;
let mut runs = Vec::new();
for (i, class) in label.split("::").enumerate() {
if i > 0 {
ix += "::".len();
}
let end_ix = ix + class.len();
runs.push((ix..end_ix, class_id));
ix = end_ix;
}
Some(language::CodeLabel {
text: label.to_string(),
runs,
filter_range: 0..label.len(),
})
}
_ => return None,
}
}
}

View File

@@ -115,7 +115,6 @@ fn main() {
context_menu::init(cx);
project::Project::init(&client);
client::Channel::init(&client);
client::init(client.clone(), cx);
command_palette::init(cx);
editor::init(cx);

View File

@@ -818,7 +818,7 @@ mod tests {
// Open the first entry
let entry_1 = workspace
.update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
.update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
.await
.unwrap();
cx.read(|cx| {
@@ -832,7 +832,7 @@ mod tests {
// Open the second entry
workspace
.update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
.update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
.await
.unwrap();
cx.read(|cx| {
@@ -846,7 +846,7 @@ mod tests {
// Open the first entry again. The existing pane item is activated.
let entry_1b = workspace
.update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
.update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
.await
.unwrap();
assert_eq!(entry_1.id(), entry_1b.id());
@@ -864,7 +864,7 @@ mod tests {
workspace
.update(cx, |w, cx| {
w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
w.open_path(file2.clone(), true, cx)
w.open_path(file2.clone(), None, true, cx)
})
.await
.unwrap();
@@ -883,8 +883,8 @@ mod tests {
// Open the third entry twice concurrently. Only one pane item is added.
let (t1, t2) = workspace.update(cx, |w, cx| {
(
w.open_path(file3.clone(), true, cx),
w.open_path(file3.clone(), true, cx),
w.open_path(file3.clone(), None, true, cx),
w.open_path(file3.clone(), None, true, cx),
)
});
t1.await.unwrap();
@@ -1195,7 +1195,7 @@ mod tests {
workspace
.update(cx, |workspace, cx| {
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), true, cx)
workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
})
.await
.unwrap();
@@ -1284,7 +1284,7 @@ mod tests {
let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
workspace
.update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
.update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
.await
.unwrap();
@@ -1359,24 +1359,24 @@ mod tests {
let file3 = entries[2].clone();
let editor1 = workspace
.update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
.update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
editor1.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)])
});
});
let editor2 = workspace
.update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
.update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
let editor3 = workspace
.update(cx, |w, cx| w.open_path(file3.clone(), true, cx))
.update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
.await
.unwrap()
.downcast::<Editor>()
@@ -1384,7 +1384,7 @@ mod tests {
editor3
.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)])
});
editor.newline(&Default::default(), cx);
@@ -1626,22 +1626,22 @@ mod tests {
let file4 = entries[3].clone();
let file1_item_id = workspace
.update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
.update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
.await
.unwrap()
.id();
let file2_item_id = workspace
.update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
.update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
.await
.unwrap()
.id();
let file3_item_id = workspace
.update(cx, |w, cx| w.open_path(file3.clone(), true, cx))
.update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
.await
.unwrap()
.id();
let file4_item_id = workspace
.update(cx, |w, cx| w.open_path(file4.clone(), true, cx))
.update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx))
.await
.unwrap()
.id();

View File

@@ -7,7 +7,7 @@ echo "creating database..."
script/sqlx database create
echo "migrating database..."
cargo run -p collab -- migrate
(cd crates/collab && cargo run -- migrate)
echo "seeding database..."
script/seed-db

View File

@@ -15,8 +15,6 @@ if [[ $(git rev-parse --abbrev-ref HEAD) != "main" ]]; then
exit 1
fi
git pull -q --ff-only origin main
git fetch --tags
cargo check -q
# Parse the current version
version=$(script/get-crate-version zed)
@@ -31,6 +29,10 @@ prev_minor_branch_name="v${major}.${prev_minor}.x"
next_minor_branch_name="v${major}.${next_minor}.x"
preview_tag_name="v${major}.${minor}.${patch}-pre"
git fetch origin ${prev_minor_branch_name}:${prev_minor_branch_name}
git fetch origin --tags
cargo check -q
function cleanup {
git checkout -q main
}

View File

@@ -13,6 +13,7 @@ if [[ -n $(git status --short --untracked-files=no) ]]; then
fi
which cargo-set-version > /dev/null || cargo install cargo-edit
which jq > /dev/null || brew install jq
cargo set-version --package $package --bump $version_increment
cargo check --quiet

View File

@@ -32,8 +32,8 @@ export default function editor(colorScheme: ColorScheme) {
}),
},
message: {
text: text(layer, "sans", styleSet, "inverted", { size: "sm" }),
highlightText: text(layer, "sans", styleSet, "inverted", {
text: text(layer, "sans", styleSet, "default", { size: "sm" }),
highlightText: text(layer, "sans", styleSet, "default", {
size: "sm",
weight: "bold",
}),
@@ -152,10 +152,10 @@ export default function editor(colorScheme: ColorScheme) {
widthEm: 0.16,
cornerRadius: 0.05,
},
documentHighlightReadBackground: colorScheme.ramps
.neutral(0.5)
.alpha(0.2)
.hex(), // TODO: This was blend
/** Highlights matching occurences of what is under the cursor
* as well as matched brackets
*/
documentHighlightReadBackground: withOpacity(foreground(layer, "accent"), 0.1),
documentHighlightWriteBackground: colorScheme.ramps
.neutral(0.5)
.alpha(0.4)

View File

@@ -1,14 +1,19 @@
import { ColorScheme } from "../themes/common/colorScheme";
import { background, foreground, text } from "./components";
import { withOpacity } from "../utils/color";
import { background, border, foreground, text } from "./components";
export default function projectPanel(colorScheme: ColorScheme) {
let layer = colorScheme.middle;
let entry = {
let baseEntry = {
height: 24,
iconColor: foreground(layer, "variant"),
iconSize: 8,
iconSpacing: 8,
}
let entry = {
...baseEntry,
text: text(layer, "mono", "variant", { size: "sm" }),
hover: {
background: background(layer, "variant", "hovered"),
@@ -28,6 +33,12 @@ export default function projectPanel(colorScheme: ColorScheme) {
padding: { left: 12, right: 12, top: 6, bottom: 6 },
indentWidth: 8,
entry,
draggedEntry: {
...baseEntry,
text: text(layer, "mono", "on", { size: "sm" }),
background: withOpacity(background(layer, "on"), 0.9),
border: border(layer),
},
ignoredEntry: {
...entry,
text: text(layer, "mono", "disabled"),

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