Compare commits

..

34 Commits

Author SHA1 Message Date
Conrad Irwin
358ca91621 Make MultiBufferPoint its own type
multi-buffer co-ordinates.
2024-12-04 00:42:00 -07:00
Conrad Irwin
e231321655 Fix panic in update_ime_position (#21510)
This can call back into the app, so must be done when the platform lock
is not
held.

Release Notes:

- Fixes a (rare) panic when changing tab
2024-12-03 23:20:25 -07:00
Waleed Dahshan
8f08787cf0 Implement Helix Support (WIP) (#19175)
Closes #4642 

- Added the ability to switch to helix normal mode, with an additional
helix visual mode.
- <kbd>ctrl</kbd><kbd>h</kbd> from Insert mode goes to Helix Normal
mode. <kbd> i </kbd> and <kbd> a </kbd> to go back.
- Need to find a way to perform the helix normal mode selection with
<kbd> w </kbd>, <kbd>e </kbd>, <kbd> b </kbd> as a first step. Need to
figure out how the mode will interoperate with the VIM mode as the new
additions are in the same crate.
2024-12-03 23:19:52 -07:00
Cole Miller
c5d15fd065 Add FoldFunctionBodies editor action (#21504)
Related to #19424

This uses the new text object support, so will only work for languages
that have `textobjects.scm`. It does not integrate with
indentation-based folding for now, and the syntax-based folds don't have
matching fold markers in the gutter (unless they are folded).

Release Notes:

- Add an editor action to fold all function bodies

Co-authored-by: Conrad <conrad@zed.dev>
2024-12-03 23:23:16 -05:00
Cole Miller
ce5f492404 Update rustls and sqlx (#21506)
Release Notes:

- N/A
2024-12-03 23:22:26 -05:00
Marshall Bowers
3019960f83 markdown: Make cx the last parameter to Markdown::new_text (#21497)
This PR is a follow-up to
https://github.com/zed-industries/zed/pull/21487 to make sure that the
`cx` is the last parameter to `Markdown::new_text` as well.

Release Notes:

- N/A
2024-12-03 18:39:00 -05:00
Marshall Bowers
9f459ba573 assistant2: Render messages as Markdown (#21496)
This PR updates Assistant 2 to render the messages in the thread as
Markdown:

<img width="1138" alt="Screenshot 2024-12-03 at 6 09 27 PM"
src="https://github.com/user-attachments/assets/c1c44fde-1efb-43cf-b9c9-768e6974c753">

Release Notes:

- N/A
2024-12-03 18:32:13 -05:00
Peter Tripp
ecaf44511c Fix Perplexity extension URL (#21495) 2024-12-03 18:28:59 -05:00
Cole Miller
dc32ab25a0 Open folds containing selections when jumping from multibuffer (#21433)
When searching within a single buffer, activating a search result causes
any fold containing the result to be unfolded. However, this didn't
happen when jumping to a search result from a project-wide search
multibuffer. This PR fixes that.

Release Notes:

- Fixed folds not opening when jumping from search results multibuffer
2024-12-03 17:14:17 -05:00
Marshall Bowers
aca23da971 assistant2: Render messages in the thread using a list (#21491)
This PR updates the rendering of the messages in the current thread to
use a `gpui::list`.

Release Notes:

- N/A
2024-12-03 16:25:09 -05:00
Conrad Irwin
db34f29300 vim: Add == and fix = in the status bar (#21490)
cc @maxbrunsfeld

Release Notes:

- vim: Add ==
2024-12-03 14:18:19 -07:00
Conrad Irwin
1fccda7b8d Add text objects to extensions (#21488)
Release Notes:

- Adds textobject support to erlang, haskell, lua, php, prisma, proto,
toml, and zig
2024-12-03 13:56:25 -07:00
Conrad Irwin
463c99b503 Fix script/get-released-version (#21489)
Release Notes:

- N/A
2024-12-03 13:56:01 -07:00
Marshall Bowers
88b0d3c78e markdown: Make cx the last parameter to the constructor (#21487)
I noticed that `Markdown::new` didn't have the `cx` as the final
parameter, as is conventional.

This PR fixes that.

Release Notes:

- N/A
2024-12-03 15:27:58 -05:00
Peter Tripp
165d50ff5b Add openbsd netcat to script/linux (#21478)
- Follow-up to: https://github.com/zed-industries/zed/pull/20751

openbsd-netcat is required for interactive SSH Remoting prompts
(password, passphrase, 2fa, etc).
2024-12-03 15:27:12 -05:00
Conrad Irwin
731e6d31f6 Revert "macos: Add default keybind for ctrl-home / ctrl-end (#21007)" (#21476)
This reverts commit 614b3b979b.

This conflicts with the macOS `ctrl-fn-left/right` bindings for moving
windows around (new in Sequoia).

If you want these use:
```
  {
    "context": "Editor",
    "bindings": {
      "ctrl-home": "editor::MoveToBeginning",
      "ctrl-end": "editor::MoveToEnd"
    }
  },
```

Release Notes:

- N/A
2024-12-03 13:10:02 -07:00
Conrad Irwin
b28287ce91 Fix panic in remove_item (#21480)
In #20742 we added a call to remove_item that retain an item index over
an
await point. This led to a race condition that could panic if another
tab was
removed during that time. (cc @mgsloan)

This changes the API to make it harder to misuse.

Release Notes:

- Fixed a panic when closing tabs containing new unsaved files
2024-12-03 13:09:53 -07:00
Conrad Irwin
492ca219d3 Fix panic in autoclosing (#21482)
Closes #14961

Release Notes:

- Fixed a panic when backspacing at the start of a buffer with
`always_treat_brackets_as_autoclosed` enabled.
2024-12-03 13:09:44 -07:00
Jason Lee
afb253b406 ui: Ensure Label with single_line set does not wrap (#21444)
Release Notes:

- N/A

---

Split from #21438, this change for make sure the `single_line` mode
Label will not be wrap.

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-12-03 13:03:53 -05:00
Peter Tripp
41a973b13f Publish theme json schema v0.2.0 (#21428)
Fix theme json schema so `./script/import-themes print-schema` works again
Update schema to reflect current structs
([diff](https://gist.github.com/notpeter/26e6d0939985f542e8492458442ac62a/revisions?diff=unified&w=))

https://zed.dev/schema/themes/v0.2.0.json
2024-12-03 12:57:39 -05:00
Conrad Irwin
75c9dc179b Add textobjects queries (#20924)
Co-Authored-By: Max <max@zed.dev>

Release Notes:

- vim: Added motions `[[`, `[]`, `]]`, `][` for navigating by section,
`[m`, `]m`, `[M`, `]M` for navigating by method, and `[*`, `]*`, `[/`,
`]/` for comments. These currently only work for languages built in to
Zed, as they are powered by new tree-sitter queries.
- vim: Added new text objects: `ic`, `ac` for inside/around classes,
`if`,`af` for functions/methods, and `g c` for comments. These currently
only work for languages built in to Zed, as they are powered by new
tree-sitter queries.

---------

Co-authored-by: Max <max@zed.dev>
2024-12-03 10:37:01 -07:00
Conrad Irwin
c443307c19 Fix ctrl-alt-X shortcuts (#21473)
The macOS input handler assumes that you want to insert control
sequences when
you type ctrl-alt-X (you probably don't...).

Release Notes:

- (nightly only) fix ctrl-alt-X shortcuts
2024-12-03 10:26:19 -07:00
Peter Tripp
2dd5138988 docs: Add anchor links for language-specific settings (#21469) 2024-12-03 11:54:06 -05:00
Kirill Bulatov
a464474df0 Properly handle opening of file-less excerpts (#21465)
Follow-up of https://github.com/zed-industries/zed/pull/20491 and
https://github.com/zed-industries/zed/pull/20469
Closes https://github.com/zed-industries/zed/issues/21369

Release Notes:

- Fixed file-less excerpts always opening instead of activating
2024-12-03 18:41:36 +02:00
Kirill Bulatov
a0f2c0799e Debounce diagnostics status bar updates (#21463)
Closes https://github.com/zed-industries/zed/pull/20797

Release Notes:

- Fixed diagnostics status bar flashing when typing
2024-12-03 17:27:59 +02:00
Sebastian Nickels
1270ef3ea5 Enable toolchain venv in new terminals (#21388)
Fixes part of issue #7808 

> This venv should be the one we automatically activate when opening new
terminals, if the detect_venv setting is on.

Release Notes:

- Selected Python toolchains (virtual environments) are now automatically activated in new terminals.

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2024-12-03 16:24:30 +01:00
Danilo Leal
a76cd778c4 Disable hunk diff arrow buttons when there's only one hunk (#21437)
Closes https://github.com/zed-industries/zed/issues/20817

| One hunk | Multiple hunks |
|--------|--------|
| <img width="800" alt="Screenshot 2024-12-03 at 09 42 49"
src="https://github.com/user-attachments/assets/7c2ff80a-d4d9-4a74-84b8-891fadfd4e6c">
| <img width="800" alt="Screenshot 2024-12-02 at 23 36 38"
src="https://github.com/user-attachments/assets/60ea94b8-0b23-43a2-afad-b816b4645d1f">
|

Release Notes:

- Fixed showing prev/next hunk navigation buttons when there is only one
hunk
2024-12-03 10:07:59 -03:00
Jason Lee
a8c7e61021 Fix AI Context menu text wrapping causing overlap (#21438)
Closes https://github.com/zed-industries/zed/issues/20678

| Before | After |
| --- | --- |
| <img width="672" alt="SCR-20241203-jreb"
src="https://github.com/user-attachments/assets/411ba2a6-712f-4ab7-a320-12ac9a35c1e1">
| <img width="771" alt="SCR-20241203-jwhe"
src="https://github.com/user-attachments/assets/022c8ee9-4089-4c09-aa4b-12a0f5528822">
|

Release Notes:

- Fixed AI Context menu text wrapping causing overlap.

Also cc #21409 @WeetHet @osiewicz to use `Label`, this PR has been fixed
`Label` to ensure `whitespace_nowrap` when use `single_line`.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2024-12-03 01:45:15 -03:00
Danilo Leal
2b143784da Improve audio files icon (#21441)
It took me a couple of minutes of staring at this speaker icon to figure
out it was a speaker! I even researched whether the `.wav` file type had
a specific icon, given I thought it was a specific triangle of sorts 😅
I'm sensing audio waves, at this size, will be easier to parse.

Release Notes:

- N/A
2024-12-03 00:40:46 -03:00
Cole Miller
b53b2c0376 Run dependency review for pull requests only (#21432)
This was an oversight in the original PR, dependency-review-action won't
work properly for `push` events
([example](https://github.com/zed-industries/zed/actions/runs/12130053580/job/33819624076)).

Release Notes:

- N/A
2024-12-02 19:39:18 -05:00
Cole Miller
e1c509e0de Check for vulnerable dependencies in CI (#21424)
This PR adds GitHub's dependency review action to CI, to flag PRs that
introduce new Cargo.lock entries for vulnerable crates according to the
GHSA database.

An alternative would be to run `cargo audit`, which checks against the
RustSec database. The state of synchronization between these two
databases seems a bit messy, but as far as I can tell GHSA has most
recent RustSec advisories on file, while RustSec is missing a larger
number of recent GHSA advisories.

The dependency review action should be smart enough not to flag PRs
because an untouched entry in Cargo.lock has a new advisory.

I've turned off the "license check" functionality since we have a
separate CI step for that.

Release Notes:

- N/A
2024-12-02 18:48:03 -05:00
Michael Sloan
f4dbcb6714 Use explicit sort order instead of comparison impls for gpui prims (#21430)
Found this while looking into adding support for the Surface primitive
on Linux, for rendering video shares. In that case it would be
expensive to compare images for equality. `Eq` and `PartialEq` were
being required but not used here due to use of `Ord` and `PartialOrd`.

Release Notes:

- N/A
2024-12-02 16:27:29 -07:00
Kyle Kelley
579bc8f015 Upgrade repl dependencies (#21431)
Bump dependencies for jupyter packages. cc @maxdeviant 

Release Notes:

- N/A
2024-12-02 15:22:03 -08:00
Max Brunsfeld
7c994cd4a5 Add AutoIndent action and '=' vim operator (#21427)
Release Notes:

- vim: Added the `=` operator, for auto-indent

Co-authored-by: Conrad <conrad@zed.dev>
2024-12-02 15:00:04 -08:00
141 changed files with 3659 additions and 1224 deletions

View File

@@ -113,6 +113,12 @@ jobs:
script/check-licenses
script/generate-licenses /tmp/zed_licenses_output
- name: Check for new vulnerable dependencies
if: github.event_name == 'pull_request'
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4
with:
license-check: false
- name: Run tests
uses: ./.github/actions/run_tests

View File

@@ -2,8 +2,7 @@
"languages": {
"Markdown": {
"tab_size": 2,
"formatter": "prettier",
"format_on_save": "on"
"formatter": "prettier"
},
"TOML": {
"formatter": "prettier",

259
Cargo.lock generated
View File

@@ -464,10 +464,12 @@ dependencies = [
"feature_flags",
"futures 0.3.31",
"gpui",
"language",
"language_model",
"language_model_selector",
"language_models",
"log",
"markdown",
"project",
"proto",
"serde",
@@ -930,20 +932,6 @@ version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-tls"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfeefd0ca297cbbb3bd34fd6b228401c2a5177038257afd751bc29f0a2da4795"
dependencies = [
"futures-core",
"futures-io",
"rustls 0.20.9",
"rustls-pemfile 1.0.4",
"webpki",
"webpki-roots 0.22.6",
]
[[package]]
name = "async-tls"
version = "0.13.0"
@@ -968,21 +956,6 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "async-tungstenite"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce01ac37fdc85f10a43c43bc582cbd566720357011578a935761075f898baf58"
dependencies = [
"async-std",
"async-tls 0.12.0",
"futures-io",
"futures-util",
"log",
"pin-project-lite",
"tungstenite 0.19.0",
]
[[package]]
name = "async-tungstenite"
version = "0.28.0"
@@ -990,7 +963,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e661b6cb0a6eb34d02c520b052daa3aa9ac0cc02495c9d066bbce13ead132b"
dependencies = [
"async-std",
"async-tls 0.13.0",
"async-tls",
"futures-io",
"futures-util",
"log",
@@ -1160,7 +1133,7 @@ dependencies = [
"fastrand 2.2.0",
"hex",
"http 0.2.12",
"ring 0.17.8",
"ring",
"time",
"tokio",
"tracing",
@@ -1350,7 +1323,7 @@ dependencies = [
"once_cell",
"p256",
"percent-encoding",
"ring 0.17.8",
"ring",
"sha2",
"subtle",
"time",
@@ -2507,7 +2480,7 @@ dependencies = [
"anyhow",
"async-native-tls",
"async-recursion 0.3.2",
"async-tungstenite 0.28.0",
"async-tungstenite",
"chrono",
"clock",
"cocoa 0.26.0",
@@ -2639,7 +2612,7 @@ dependencies = [
"assistant_tool",
"async-stripe",
"async-trait",
"async-tungstenite 0.28.0",
"async-tungstenite",
"audio",
"aws-config",
"aws-sdk-kinesis",
@@ -3445,9 +3418,9 @@ dependencies = [
[[package]]
name = "ctor"
version = "0.2.9"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
dependencies = [
"quote",
"syn 2.0.87",
@@ -4540,7 +4513,7 @@ dependencies = [
"futures-core",
"futures-sink",
"nanorand",
"spin 0.9.8",
"spin",
]
[[package]]
@@ -5801,7 +5774,7 @@ dependencies = [
"http 1.1.0",
"hyper 1.5.0",
"hyper-util",
"rustls 0.23.16",
"rustls 0.23.18",
"rustls-native-certs 0.8.0",
"rustls-pki-types",
"tokio",
@@ -6453,7 +6426,7 @@ dependencies = [
"base64 0.21.7",
"js-sys",
"pem",
"ring 0.17.8",
"ring",
"serde",
"serde_json",
"simple_asn1",
@@ -6461,47 +6434,31 @@ dependencies = [
[[package]]
name = "jupyter-protocol"
version = "0.3.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d4d496ac890e14efc12c5289818b3c39e3026a7bb02d5576b011e1a062d4bcc"
checksum = "503458f8125fd9047ed0a9d95d7a93adc5eaf8bce48757c6d401e09f71ad3407"
dependencies = [
"anyhow",
"async-trait",
"bytes 1.8.0",
"chrono",
"futures 0.3.31",
"jupyter-serde",
"rand 0.8.5",
"serde",
"serde_json",
"uuid",
]
[[package]]
name = "jupyter-serde"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32aa595c3912167b7eafcaa822b767ad1fa9605a18127fc9ac741241b796410e"
dependencies = [
"anyhow",
"serde",
"serde_json",
"thiserror 1.0.69",
"uuid",
]
[[package]]
name = "jupyter-websocket-client"
version = "0.5.0"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5850894210a3f033ff730d6f956b0335db38573ce7bb61c6abbf69dcbe284ba7"
checksum = "58d9afa5bc6eeafb78f710a2efc585f69099f8b6a99dc7eb826581e3773a6e31"
dependencies = [
"anyhow",
"async-trait",
"async-tungstenite 0.22.2",
"async-tungstenite",
"futures 0.3.31",
"jupyter-protocol",
"jupyter-serde",
"serde",
"serde_json",
"url",
@@ -6817,7 +6774,7 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
"spin 0.9.8",
"spin",
]
[[package]]
@@ -6834,9 +6791,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.164"
version = "0.2.162"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
[[package]]
name = "libdbus-sys"
@@ -6909,9 +6866,9 @@ dependencies = [
[[package]]
name = "libsqlite3-sys"
version = "0.28.0"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [
"cc",
"pkg-config",
@@ -7539,13 +7496,13 @@ dependencies = [
[[package]]
name = "nbformat"
version = "0.7.0"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa6827a3881aa100bb2241cd2633b3c79474dbc93704f1f2cf5cc85064cda4be"
checksum = "19835ad46507d80d9671e10a1c7c335655f4f3033aeb066fe025f14e070c2e66"
dependencies = [
"anyhow",
"chrono",
"jupyter-serde",
"jupyter-protocol",
"serde",
"serde_json",
"thiserror 1.0.69",
@@ -9555,7 +9512,7 @@ dependencies = [
"quinn-proto",
"quinn-udp",
"rustc-hash 2.0.0",
"rustls 0.23.16",
"rustls 0.23.18",
"socket2 0.5.7",
"thiserror 2.0.3",
"tokio",
@@ -9571,9 +9528,9 @@ dependencies = [
"bytes 1.8.0",
"getrandom 0.2.15",
"rand 0.8.5",
"ring 0.17.8",
"ring",
"rustc-hash 2.0.0",
"rustls 0.23.16",
"rustls 0.23.18",
"rustls-pki-types",
"slab",
"thiserror 2.0.3",
@@ -10128,7 +10085,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls 0.23.16",
"rustls 0.23.18",
"rustls-native-certs 0.8.0",
"rustls-pemfile 2.2.0",
"rustls-pki-types",
@@ -10214,21 +10171,6 @@ dependencies = [
"util",
]
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin 0.5.2",
"untrusted 0.7.1",
"web-sys",
"winapi",
]
[[package]]
name = "ring"
version = "0.17.8"
@@ -10239,8 +10181,8 @@ dependencies = [
"cfg-if",
"getrandom 0.2.15",
"libc",
"spin 0.9.8",
"untrusted 0.9.0",
"spin",
"untrusted",
"windows-sys 0.52.0",
]
@@ -10333,7 +10275,7 @@ name = "rpc"
version = "0.1.0"
dependencies = [
"anyhow",
"async-tungstenite 0.28.0",
"async-tungstenite",
"base64 0.22.1",
"chrono",
"collections",
@@ -10375,9 +10317,9 @@ dependencies = [
[[package]]
name = "runtimelib"
version = "0.22.0"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a8ab675beb5cf25c28f9c6ddb8f47bcf73b43872797e6ab6157865f44d1e19"
checksum = "445ff0ee3d5c832cdd27efadd004a741423db1f91bd1de593a14b21211ea084c"
dependencies = [
"anyhow",
"async-dispatcher",
@@ -10390,8 +10332,7 @@ dependencies = [
"futures 0.3.31",
"glob",
"jupyter-protocol",
"jupyter-serde",
"ring 0.17.8",
"ring",
"serde",
"serde_json",
"shellexpand 3.1.0",
@@ -10518,18 +10459,6 @@ dependencies = [
"rustix 0.38.40",
]
[[package]]
name = "rustls"
version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99"
dependencies = [
"log",
"ring 0.16.20",
"sct",
"webpki",
]
[[package]]
name = "rustls"
version = "0.21.12"
@@ -10537,19 +10466,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [
"log",
"ring 0.17.8",
"ring",
"rustls-webpki 0.101.7",
"sct",
]
[[package]]
name = "rustls"
version = "0.23.16"
version = "0.23.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e"
checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f"
dependencies = [
"once_cell",
"ring 0.17.8",
"ring",
"rustls-pki-types",
"rustls-webpki 0.102.8",
"subtle",
@@ -10614,8 +10543,8 @@ version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
"ring 0.17.8",
"untrusted 0.9.0",
"ring",
"untrusted",
]
[[package]]
@@ -10624,9 +10553,9 @@ version = "0.102.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
dependencies = [
"ring 0.17.8",
"ring",
"rustls-pki-types",
"untrusted 0.9.0",
"untrusted",
]
[[package]]
@@ -10740,8 +10669,8 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [
"ring 0.17.8",
"untrusted 0.9.0",
"ring",
"untrusted",
]
[[package]]
@@ -11029,9 +10958,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.133"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [
"indexmap 2.6.0",
"itoa",
@@ -11503,12 +11432,6 @@ dependencies = [
"smallvec",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "spin"
version = "0.9.8"
@@ -11591,9 +11514,9 @@ dependencies = [
[[package]]
name = "sqlx"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27144619c6e5802f1380337a209d2ac1c431002dd74c6e60aebff3c506dc4f0c"
checksum = "fcfa89bea9500db4a0d038513d7a060566bfc51d46d1c014847049a45cce85e8"
dependencies = [
"sqlx-core",
"sqlx-macros",
@@ -11604,9 +11527,9 @@ dependencies = [
[[package]]
name = "sqlx-core"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a999083c1af5b5d6c071d34a708a19ba3e02106ad82ef7bbd69f5e48266b613b"
checksum = "d06e2f2bd861719b1f3f0c7dbe1d80c30bf59e76cf019f07d9014ed7eefb8e08"
dependencies = [
"atoi",
"bigdecimal",
@@ -11632,8 +11555,8 @@ dependencies = [
"paste",
"percent-encoding",
"rust_decimal",
"rustls 0.21.12",
"rustls-pemfile 1.0.4",
"rustls 0.23.18",
"rustls-pemfile 2.2.0",
"serde",
"serde_json",
"sha2",
@@ -11646,14 +11569,14 @@ dependencies = [
"tracing",
"url",
"uuid",
"webpki-roots 0.25.4",
"webpki-roots 0.26.7",
]
[[package]]
name = "sqlx-macros"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23217eb7d86c584b8cbe0337b9eacf12ab76fe7673c513141ec42565698bb88"
checksum = "2f998a9defdbd48ed005a89362bd40dd2117502f15294f61c8d47034107dbbdc"
dependencies = [
"proc-macro2",
"quote",
@@ -11664,9 +11587,9 @@ dependencies = [
[[package]]
name = "sqlx-macros-core"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a099220ae541c5db479c6424bdf1b200987934033c2584f79a0e1693601e776"
checksum = "3d100558134176a2629d46cec0c8891ba0be8910f7896abfdb75ef4ab6f4e7ce"
dependencies = [
"dotenvy",
"either",
@@ -11690,9 +11613,9 @@ dependencies = [
[[package]]
name = "sqlx-mysql"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5afe4c38a9b417b6a9a5eeffe7235d0a106716495536e7727d1c7f4b1ff3eba6"
checksum = "936cac0ab331b14cb3921c62156d913e4c15b74fb6ec0f3146bd4ef6e4fb3c12"
dependencies = [
"atoi",
"base64 0.22.1",
@@ -11737,9 +11660,9 @@ dependencies = [
[[package]]
name = "sqlx-postgres"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1dbb157e65f10dbe01f729339c06d239120221c9ad9fa0ba8408c4cc18ecf21"
checksum = "9734dbce698c67ecf67c442f768a5e90a49b2a4d61a9f1d59f73874bd4cf0710"
dependencies = [
"atoi",
"base64 0.22.1",
@@ -11781,9 +11704,9 @@ dependencies = [
[[package]]
name = "sqlx-sqlite"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b2cdd83c008a622d94499c0006d8ee5f821f36c89b7d625c900e5dc30b5c5ee"
checksum = "a75b419c3c1b1697833dd927bdc4c6545a620bc1bbafabd44e1efbe9afcd337e"
dependencies = [
"atoi",
"chrono",
@@ -12857,7 +12780,7 @@ version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [
"rustls 0.23.16",
"rustls 0.23.18",
"rustls-pki-types",
"tokio",
]
@@ -13389,25 +13312,6 @@ version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
[[package]]
name = "tungstenite"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15fba1a6d6bb030745759a9a2a588bfe8490fc8b4751a277db3a0be1c9ebbf67"
dependencies = [
"byteorder",
"bytes 1.8.0",
"data-encoding",
"http 0.2.12",
"httparse",
"log",
"rand 0.8.5",
"sha1",
"thiserror 1.0.69",
"url",
"utf-8",
]
[[package]]
name = "tungstenite"
version = "0.20.1"
@@ -13619,12 +13523,6 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "untrusted"
version = "0.9.0"
@@ -14535,8 +14433,8 @@ version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53"
dependencies = [
"ring 0.17.8",
"untrusted 0.9.0",
"ring",
"untrusted",
]
[[package]]
@@ -14550,9 +14448,12 @@ dependencies = [
[[package]]
name = "webpki-roots"
version = "0.25.4"
version = "0.26.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "weezl"
@@ -15872,7 +15773,7 @@ dependencies = [
[[package]]
name = "zed_erlang"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"zed_extension_api 0.1.0",
]
@@ -15906,7 +15807,7 @@ dependencies = [
[[package]]
name = "zed_haskell"
version = "0.1.1"
version = "0.1.2"
dependencies = [
"zed_extension_api 0.1.0",
]
@@ -15920,28 +15821,28 @@ dependencies = [
[[package]]
name = "zed_lua"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_php"
version = "0.2.2"
version = "0.2.3"
dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_prisma"
version = "0.0.3"
version = "0.0.4"
dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_proto"
version = "0.2.0"
version = "0.2.1"
dependencies = [
"zed_extension_api 0.1.0",
]
@@ -15984,7 +15885,7 @@ dependencies = [
[[package]]
name = "zed_toml"
version = "0.1.1"
version = "0.1.2"
dependencies = [
"zed_extension_api 0.1.0",
]
@@ -15998,7 +15899,7 @@ dependencies = [
[[package]]
name = "zed_zig"
version = "0.3.1"
version = "0.3.2"
dependencies = [
"zed_extension_api 0.1.0",
]

View File

@@ -388,14 +388,15 @@ indexmap = { version = "1.6.2", features = ["serde"] }
indoc = "2"
itertools = "0.13.0"
jsonwebtoken = "9.3"
jupyter-protocol = { version = "0.3.0" }
jupyter-websocket-client = { version = "0.5.0" }
jupyter-protocol = { version = "0.5.0" }
jupyter-websocket-client = { version = "0.8.0" }
libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
markup5ever_rcdom = "0.3.0"
nanoid = "0.4"
nbformat = { version = "0.7.0" }
nbformat = { version = "0.9.0" }
nix = "0.29"
num-format = "0.4.4"
once_cell = "1.19.0"
@@ -429,7 +430,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f
"stream",
] }
rsa = "0.9.6"
runtimelib = { version = "0.22.0", default-features = false, features = [
runtimelib = { version = "0.24.0", default-features = false, features = [
"async-dispatcher-runtime",
] }
rustc-demangle = "0.1.23"

View File

@@ -1,4 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.20721 10.8551C7.67694 10.8551 6.33401 11.0424 5.52243 11.1878C5.15088 11.2543 4.808 10.9114 4.87454 10.5399C5.01987 9.72825 5.20721 8.38532 5.20721 6.85505C5.20721 5.69375 5.09932 4.64034 4.98377 3.84516C4.9431 3.56522 5.40267 3.3722 5.5684 3.60145C6.30333 4.61809 7.44022 6.08806 8.70721 7.35505C9.9742 8.62204 11.4442 9.75893 12.4608 10.4939C12.69 10.6596 12.497 11.1192 12.2171 11.0785C11.4219 10.963 10.3685 10.8551 9.20721 10.8551Z" fill="black" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.11129 10.6816L5.28324 8.85355C5.08798 8.65829 4.7714 8.65829 4.57613 8.85355L3.35355 10.0761C3.15829 10.2714 3.15829 10.588 3.35355 10.7832L5.1816 12.6113C5.37686 12.8066 5.69345 12.8066 5.88871 12.6113L7.11129 11.3887C7.30655 11.1934 7.30655 10.8769 7.11129 10.6816Z" fill="black"/>
<path d="M3.5 6.66666V8.66666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.5 5V11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.5 3V13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.5 5.33334V10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.5 4V12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.5 6.66666V8.66666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 946 B

After

Width:  |  Height:  |  Size: 751 B

View File

@@ -93,8 +93,6 @@
"ctrl-e": "editor::MoveToEndOfLine",
"cmd-up": "editor::MoveToBeginning",
"cmd-down": "editor::MoveToEnd",
"ctrl-home": "editor::MoveToBeginning",
"ctrl-end": "editor::MoveToEnd",
"shift-up": "editor::SelectUp",
"ctrl-shift-p": "editor::SelectUp",
"shift-down": "editor::SelectDown",

View File

@@ -33,6 +33,18 @@
"(": "vim::SentenceBackward",
")": "vim::SentenceForward",
"|": "vim::GoToColumn",
"] ]": "vim::NextSectionStart",
"] [": "vim::NextSectionEnd",
"[ [": "vim::PreviousSectionStart",
"[ ]": "vim::PreviousSectionEnd",
"] m": "vim::NextMethodStart",
"] M": "vim::NextMethodEnd",
"[ m": "vim::PreviousMethodStart",
"[ M": "vim::PreviousMethodEnd",
"[ *": "vim::PreviousComment",
"[ /": "vim::PreviousComment",
"] *": "vim::NextComment",
"] /": "vim::NextComment",
// Word motions
"w": "vim::NextWordStart",
"e": "vim::NextWordEnd",
@@ -55,10 +67,10 @@
"n": "vim::MoveToNextMatch",
"shift-n": "vim::MoveToPrevMatch",
"%": "vim::Matching",
"] }": ["vim::UnmatchedForward", { "char": "}" } ],
"[ {": ["vim::UnmatchedBackward", { "char": "{" } ],
"] )": ["vim::UnmatchedForward", { "char": ")" } ],
"[ (": ["vim::UnmatchedBackward", { "char": "(" } ],
"] }": ["vim::UnmatchedForward", { "char": "}" }],
"[ {": ["vim::UnmatchedBackward", { "char": "{" }],
"] )": ["vim::UnmatchedForward", { "char": ")" }],
"[ (": ["vim::UnmatchedBackward", { "char": "(" }],
"f": ["vim::PushOperator", { "FindForward": { "before": false } }],
"t": ["vim::PushOperator", { "FindForward": { "before": true } }],
"shift-f": ["vim::PushOperator", { "FindBackward": { "after": false } }],
@@ -209,6 +221,7 @@
"shift-s": "vim::SubstituteLine",
">": ["vim::PushOperator", "Indent"],
"<": ["vim::PushOperator", "Outdent"],
"=": ["vim::PushOperator", "AutoIndent"],
"g u": ["vim::PushOperator", "Lowercase"],
"g shift-u": ["vim::PushOperator", "Uppercase"],
"g ~": ["vim::PushOperator", "OppositeCase"],
@@ -275,6 +288,7 @@
"ctrl-[": ["vim::SwitchMode", "Normal"],
">": "vim::Indent",
"<": "vim::Outdent",
"=": "vim::AutoIndent",
"i": ["vim::PushOperator", { "Object": { "around": false } }],
"a": ["vim::PushOperator", { "Object": { "around": true } }],
"g c": "vim::ToggleComments",
@@ -312,6 +326,22 @@
"ctrl-o": "vim::TemporaryNormal"
}
},
{
"context": "vim_mode == helix_normal",
"bindings": {
"i": "vim::InsertBefore",
"a": "vim::InsertAfter",
"w": "vim::NextWordStart",
"e": "vim::NextWordEnd",
"b": "vim::PreviousWordStart",
"h": "vim::Left",
"j": "vim::Down",
"k": "vim::Up",
"l": "vim::Right"
}
},
{
"context": "vim_mode == insert && !(showing_code_actions || showing_completions)",
"use_layout_keys": true,
@@ -358,7 +388,8 @@
"bindings": {
"escape": "vim::ClearOperators",
"ctrl-c": "vim::ClearOperators",
"ctrl-[": "vim::ClearOperators"
"ctrl-[": "vim::ClearOperators",
"g c": "vim::Comment"
}
},
{
@@ -387,7 +418,9 @@
">": "vim::AngleBrackets",
"a": "vim::Argument",
"i": "vim::IndentObj",
"shift-i": ["vim::IndentObj", { "includeBelow": true }]
"shift-i": ["vim::IndentObj", { "includeBelow": true }],
"f": "vim::Method",
"c": "vim::Class"
}
},
{
@@ -472,6 +505,13 @@
"<": "vim::CurrentLine"
}
},
{
"context": "vim_operator == eq",
"use_layout_keys": true,
"bindings": {
"=": "vim::CurrentLine"
}
},
{
"context": "vim_operator == gc",
"use_layout_keys": true,

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"name": "Andromeda",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"name": "Atelier",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"name": "Ayu",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"name": "Gruvbox",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"name": "One",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"name": "Rosé Pine",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"name": "Sandcastle",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"name": "Solarized",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"name": "Summercamp",
"author": "Zed Industries",
"themes": [

View File

@@ -33,7 +33,7 @@ use editor::{
},
scroll::{Autoscroll, AutoscrollStrategy},
Anchor, Editor, EditorEvent, ProposedChangeLocation, ProposedChangesEditor, RowExt,
ToOffset as _, ToPoint,
ToMultiBufferPoint, ToOffset as _,
};
use editor::{display_map::CreaseId, FoldPlaceholder};
use fs::Fs;

View File

@@ -14,7 +14,7 @@ use editor::{
},
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorElement, EditorEvent, EditorMode,
EditorStyle, ExcerptId, ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot,
ToOffset as _, ToPoint,
ToMultiBufferPoint, ToOffset as _,
};
use feature_flags::{FeatureFlagAppExt as _, ZedPro};
use fs::Fs;

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakView};
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger};
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
use crate::assistant_panel::ContextEditor;
use crate::SlashCommandWorkingSet;
@@ -177,11 +177,17 @@ impl PickerDelegate for SlashCommandDelegate {
.inset(true)
.spacing(ListItemSpacing::Dense)
.selected(selected)
.tooltip({
let description = info.description.clone();
move |cx| cx.new_view(|_| Tooltip::new(description.clone())).into()
})
.child(
v_flex()
.group(format!("command-entry-label-{ix}"))
.w_full()
.py_0p5()
.min_w(px(250.))
.max_w(px(400.))
.child(
h_flex()
.gap_1p5()
@@ -192,7 +198,7 @@ impl PickerDelegate for SlashCommandDelegate {
{
label.push_str(&args);
}
Label::new(label).size(LabelSize::Small)
Label::new(label).single_line().size(LabelSize::Small)
}))
.children(info.args.clone().filter(|_| !selected).map(
|args| {
@@ -200,6 +206,7 @@ impl PickerDelegate for SlashCommandDelegate {
.font_buffer(cx)
.child(
Label::new(args)
.single_line()
.size(LabelSize::Small)
.color(Color::Muted),
)
@@ -210,9 +217,11 @@ impl PickerDelegate for SlashCommandDelegate {
)),
)
.child(
Label::new(info.description.clone())
.size(LabelSize::Small)
.color(Color::Muted),
div().overflow_hidden().text_ellipsis().child(
Label::new(info.description.clone())
.size(LabelSize::Small)
.color(Color::Muted),
),
),
),
),

View File

@@ -23,15 +23,17 @@ editor.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
language_models.workspace = true
log.workspace = true
markdown.workspace = true
project.workspace = true
proto.workspace = true
settings.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
theme.workspace = true
ui.workspace = true

View File

@@ -3,19 +3,25 @@ use std::sync::Arc;
use anyhow::Result;
use assistant_tool::ToolWorkingSet;
use client::zed_urls;
use collections::HashMap;
use gpui::{
prelude::*, px, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle,
FocusableView, FontWeight, Model, Pixels, Subscription, Task, View, ViewContext, WeakView,
list, prelude::*, px, Action, AnyElement, AppContext, AsyncWindowContext, Empty, EventEmitter,
FocusHandle, FocusableView, FontWeight, ListAlignment, ListState, Model, Pixels,
StyleRefinement, Subscription, Task, TextStyleRefinement, View, ViewContext, WeakView,
WindowContext,
};
use language::LanguageRegistry;
use language_model::{LanguageModelRegistry, Role};
use language_model_selector::LanguageModelSelector;
use markdown::{Markdown, MarkdownStyle};
use settings::Settings;
use theme::ThemeSettings;
use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, Tab, Tooltip};
use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::Workspace;
use crate::message_editor::MessageEditor;
use crate::thread::{Message, Thread, ThreadError, ThreadEvent};
use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent};
use crate::thread_store::ThreadStore;
use crate::{NewThread, ToggleFocus, ToggleModelSelector};
@@ -32,9 +38,13 @@ pub fn init(cx: &mut AppContext) {
pub struct AssistantPanel {
workspace: WeakView<Workspace>,
language_registry: Arc<LanguageRegistry>,
#[allow(unused)]
thread_store: Model<ThreadStore>,
thread: Model<Thread>,
thread_messages: Vec<MessageId>,
rendered_messages_by_id: HashMap<MessageId, View<Markdown>>,
thread_list_state: ListState,
message_editor: View<MessageEditor>,
tools: Arc<ToolWorkingSet>,
last_error: Option<ThreadError>,
@@ -75,8 +85,18 @@ impl AssistantPanel {
Self {
workspace: workspace.weak_handle(),
language_registry: workspace.project().read(cx).languages().clone(),
thread_store,
thread: thread.clone(),
thread_messages: Vec::new(),
rendered_messages_by_id: HashMap::default(),
thread_list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
let this = cx.view().downgrade();
move |ix, cx: &mut WindowContext| {
this.update(cx, |this, cx| this.render_message(ix, cx))
.unwrap()
}
}),
message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)),
tools,
last_error: None,
@@ -94,6 +114,9 @@ impl AssistantPanel {
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread.clone(), cx));
self.thread = thread;
self.thread_messages.clear();
self.thread_list_state.reset(0);
self.rendered_messages_by_id.clear();
self._subscriptions = subscriptions;
self.message_editor.focus_handle(cx).focus(cx);
@@ -110,6 +133,63 @@ impl AssistantPanel {
self.last_error = Some(error.clone());
}
ThreadEvent::StreamedCompletion => {}
ThreadEvent::StreamedAssistantText(message_id, text) => {
if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) {
markdown.update(cx, |markdown, cx| {
markdown.append(text, cx);
});
}
}
ThreadEvent::MessageAdded(message_id) => {
let old_len = self.thread_messages.len();
self.thread_messages.push(*message_id);
self.thread_list_state.splice(old_len..old_len, 1);
if let Some(message_text) = self
.thread
.read(cx)
.message(*message_id)
.map(|message| message.text.clone())
{
let theme_settings = ThemeSettings::get_global(cx);
let mut text_style = cx.text_style();
text_style.refine(&TextStyleRefinement {
font_family: Some(theme_settings.ui_font.family.clone()),
font_size: Some(TextSize::Default.rems(cx).into()),
color: Some(cx.theme().colors().text),
..Default::default()
});
let markdown_style = MarkdownStyle {
base_text_style: text_style,
syntax: cx.theme().syntax().clone(),
selection_background_color: cx.theme().players().local().selection,
code_block: StyleRefinement {
text: Some(TextStyleRefinement {
font_family: Some(theme_settings.buffer_font.family.clone()),
font_size: Some(theme_settings.buffer_font_size.into()),
..Default::default()
}),
..Default::default()
},
..Default::default()
};
let markdown = cx.new_view(|cx| {
Markdown::new(
message_text,
markdown_style,
Some(self.language_registry.clone()),
None,
cx,
)
});
self.rendered_messages_by_id.insert(*message_id, markdown);
}
cx.notify();
}
ThreadEvent::UsePendingTools => {
let pending_tool_uses = self
.thread
@@ -301,31 +381,46 @@ impl AssistantPanel {
)
}
fn render_message(&self, message: Message, cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
let message_id = self.thread_messages[ix];
let Some(message) = self.thread.read(cx).message(message_id) else {
return Empty.into_any();
};
let Some(markdown) = self.rendered_messages_by_id.get(&message_id) else {
return Empty.into_any();
};
let (role_icon, role_name) = match message.role {
Role::User => (IconName::Person, "You"),
Role::Assistant => (IconName::ZedAssistant, "Assistant"),
Role::System => (IconName::Settings, "System"),
};
v_flex()
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_md()
div()
.id(("message-container", ix))
.p_2()
.child(
h_flex()
.justify_between()
.p_1p5()
.border_b_1()
v_flex()
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_md()
.child(
h_flex()
.gap_2()
.child(Icon::new(role_icon).size(IconSize::Small))
.child(Label::new(role_name).size(LabelSize::Small)),
),
.justify_between()
.p_1p5()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(
h_flex()
.gap_2()
.child(Icon::new(role_icon).size(IconSize::Small))
.child(Label::new(role_name).size(LabelSize::Small)),
),
)
.child(v_flex().p_1p5().text_ui(cx).child(markdown.clone())),
)
.child(v_flex().p_1p5().child(Label::new(message.text.clone())))
.into_any()
}
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
@@ -477,8 +572,6 @@ impl AssistantPanel {
impl Render for AssistantPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let messages = self.thread.read(cx).messages().cloned().collect::<Vec<_>>();
v_flex()
.key_context("AssistantPanel2")
.justify_between()
@@ -487,20 +580,7 @@ impl Render for AssistantPanel {
this.new_thread(cx);
}))
.child(self.render_toolbar(cx))
.child(
v_flex()
.id("message-list")
.gap_2()
.size_full()
.p_2()
.overflow_y_scroll()
.bg(cx.theme().colors().panel_background)
.children(
messages
.into_iter()
.map(|message| self.render_message(message, cx)),
),
)
.child(list(self.thread_list_state.clone()).flex_1())
.child(
h_flex()
.border_t_1()

View File

@@ -56,7 +56,7 @@ impl MessageEditor {
});
self.thread.update(cx, |thread, cx| {
thread.insert_user_message(user_message);
thread.insert_user_message(user_message, cx);
let mut request = thread.to_completion_request(request_kind, cx);
if self.use_tools {

View File

@@ -63,8 +63,8 @@ impl Thread {
}
}
pub fn messages(&self) -> impl Iterator<Item = &Message> {
self.messages.iter()
pub fn message(&self, id: MessageId) -> Option<&Message> {
self.messages.iter().find(|message| message.id == id)
}
pub fn tools(&self) -> &Arc<ToolWorkingSet> {
@@ -75,12 +75,14 @@ impl Thread {
self.pending_tool_uses_by_id.values().collect()
}
pub fn insert_user_message(&mut self, text: impl Into<String>) {
pub fn insert_user_message(&mut self, text: impl Into<String>, cx: &mut ModelContext<Self>) {
let id = self.next_message_id.post_inc();
self.messages.push(Message {
id: self.next_message_id.post_inc(),
id,
role: Role::User,
text: text.into(),
});
cx.emit(ThreadEvent::MessageAdded(id));
}
pub fn to_completion_request(
@@ -150,11 +152,13 @@ impl Thread {
thread.update(&mut cx, |thread, cx| {
match event {
LanguageModelCompletionEvent::StartMessage { .. } => {
let id = thread.next_message_id.post_inc();
thread.messages.push(Message {
id: thread.next_message_id.post_inc(),
id,
role: Role::Assistant,
text: String::new(),
});
cx.emit(ThreadEvent::MessageAdded(id));
}
LanguageModelCompletionEvent::Stop(reason) => {
stop_reason = reason;
@@ -163,6 +167,10 @@ impl Thread {
if let Some(last_message) = thread.messages.last_mut() {
if last_message.role == Role::Assistant {
last_message.text.push_str(&chunk);
cx.emit(ThreadEvent::StreamedAssistantText(
last_message.id,
chunk,
));
}
}
}
@@ -316,6 +324,8 @@ pub enum ThreadError {
pub enum ThreadEvent {
ShowError(ThreadError),
StreamedCompletion,
StreamedAssistantText(MessageId, String),
MessageAdded(MessageId),
UsePendingTools,
ToolFinished {
#[allow(unused)]

View File

@@ -1,7 +1,9 @@
use std::time::Duration;
use editor::Editor;
use gpui::{
EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,
WeakView,
EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, Task, View,
ViewContext, WeakView,
};
use language::Diagnostic;
use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip};
@@ -15,6 +17,7 @@ pub struct DiagnosticIndicator {
workspace: WeakView<Workspace>,
current_diagnostic: Option<Diagnostic>,
_observe_active_editor: Option<Subscription>,
diagnostics_update: Task<()>,
}
impl Render for DiagnosticIndicator {
@@ -126,6 +129,7 @@ impl DiagnosticIndicator {
workspace: workspace.weak_handle(),
current_diagnostic: None,
_observe_active_editor: None,
diagnostics_update: Task::ready(()),
}
}
@@ -149,8 +153,17 @@ impl DiagnosticIndicator {
.min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
.map(|entry| entry.diagnostic);
if new_diagnostic != self.current_diagnostic {
self.current_diagnostic = new_diagnostic;
cx.notify();
self.diagnostics_update = cx.spawn(|diagnostics_indicator, mut cx| async move {
cx.background_executor()
.timer(Duration::from_millis(50))
.await;
diagnostics_indicator
.update(&mut cx, |diagnostics_indicator, cx| {
diagnostics_indicator.current_diagnostic = new_diagnostic;
cx.notify();
})
.ok();
});
}
}
}

View File

@@ -248,6 +248,7 @@ gpui::actions!(
FindAllReferences,
Fold,
FoldAll,
FoldFunctionBodies,
FoldRecursive,
FoldSelectedRanges,
ToggleFold,
@@ -303,6 +304,7 @@ gpui::actions!(
OpenPermalinkToLine,
OpenUrl,
Outdent,
AutoIndent,
PageDown,
PageUp,
Paste,

View File

@@ -51,7 +51,7 @@ use language::{
use lsp::DiagnosticSeverity;
use multi_buffer::{
Anchor, AnchorRangeExt, MultiBuffer, MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot,
ToOffset, ToPoint,
ToMultiBufferPoint, ToOffset,
};
use serde::Deserialize;
use std::{
@@ -688,7 +688,10 @@ impl DisplaySnapshot {
self.buffer_snapshot.max_buffer_row()
}
pub fn prev_line_boundary(&self, mut point: MultiBufferPoint) -> (Point, DisplayPoint) {
pub fn prev_line_boundary(
&self,
mut point: MultiBufferPoint,
) -> (MultiBufferPoint, DisplayPoint) {
loop {
let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Left);

View File

@@ -7,7 +7,8 @@ use collections::{Bound, HashMap, HashSet};
use gpui::{AnyElement, EntityId, Pixels, WindowContext};
use language::{Chunk, Patch, Point};
use multi_buffer::{
Anchor, ExcerptId, ExcerptInfo, MultiBufferRow, MultiBufferSnapshot, ToOffset, ToPoint as _,
Anchor, ExcerptId, ExcerptInfo, MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot,
ToMultiBufferPoint as _, ToOffset,
};
use parking_lot::Mutex;
use std::{
@@ -738,13 +739,13 @@ impl BlockMap {
let wrap_row;
if excerpt_boundary.next.is_some() {
wrap_row = wrap_snapshot
.make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left)
.make_wrap_point(MultiBufferPoint::new(excerpt_boundary.row, 0), Bias::Left)
.row();
} else {
wrap_row = wrap_snapshot
.make_wrap_point(
Point::new(
excerpt_boundary.row.0,
MultiBufferPoint::new(
excerpt_boundary.row,
buffer.line_len(excerpt_boundary.row),
),
Bias::Left,
@@ -1366,7 +1367,7 @@ impl BlockSnapshot {
pub(super) fn is_line_replaced(&self, row: MultiBufferRow) -> bool {
let wrap_point = self
.wrap_snapshot
.make_wrap_point(Point::new(row.0, 0), Bias::Left);
.make_wrap_point(MultiBufferPoint::new(row, 0), Bias::Left);
let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&());
cursor.seek(&WrapRow(wrap_point.row()), Bias::Right, &());
cursor.item().map_or(false, |transform| {
@@ -2511,7 +2512,7 @@ mod tests {
let wrap_row = wrap_row as u32;
let multibuffer_row = wraps_snapshot
.to_point(WrapPoint::new(wrap_row, 0), Bias::Left)
.row;
.row();
// Create empty lines for the above block
while let Some((placement, block)) = sorted_blocks_iter.peek() {
@@ -2543,7 +2544,8 @@ mod tests {
is_in_replace_block = true;
if wrap_row == replace_range.start.0 {
expected_buffer_rows.push(input_buffer_rows[multibuffer_row as usize]);
expected_buffer_rows
.push(input_buffer_rows[multibuffer_row.0 as usize]);
}
if wrap_row == replace_range.end.0 {
@@ -2565,9 +2567,9 @@ mod tests {
}
if is_in_replace_block {
expected_replaced_buffer_rows.insert(MultiBufferRow(multibuffer_row));
expected_replaced_buffer_rows.insert(multibuffer_row);
} else {
let buffer_row = input_buffer_rows[multibuffer_row as usize];
let buffer_row = input_buffer_rows[multibuffer_row.0 as usize];
let soft_wrapped = wraps_snapshot
.to_tab_point(WrapPoint::new(wrap_row, 0))
.column()

View File

@@ -1,6 +1,8 @@
use collections::HashMap;
use gpui::{AnyElement, IntoElement};
use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToPoint};
use multi_buffer::{
Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToMultiBufferPoint,
};
use serde::{Deserialize, Serialize};
use std::{cmp::Ordering, fmt::Debug, ops::Range, sync::Arc};
use sum_tree::{Bias, SeekTarget, SumTree};

View File

@@ -1380,7 +1380,7 @@ pub type FoldEdit = Edit<FoldOffset>;
#[cfg(test)]
mod tests {
use super::*;
use crate::{display_map::inlay_map::InlayMap, MultiBuffer, ToPoint};
use crate::{display_map::inlay_map::InlayMap, MultiBuffer, ToMultiBufferPoint};
use collections::HashSet;
use rand::prelude::*;
use settings::SettingsStore;

View File

@@ -3,7 +3,8 @@ use collections::{BTreeMap, BTreeSet};
use gpui::HighlightStyle;
use language::{Chunk, Edit, Point, TextSummary};
use multi_buffer::{
Anchor, MultiBufferChunks, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, ToOffset,
Anchor, MultiBufferChunks, MultiBufferPoint, MultiBufferRow, MultiBufferRows,
MultiBufferSnapshot, ToOffset,
};
use std::{
any::TypeId,
@@ -777,7 +778,7 @@ impl InlaySnapshot {
None => self.len(),
}
}
pub fn to_buffer_point(&self, point: InlayPoint) -> Point {
pub fn to_buffer_point(&self, point: InlayPoint) -> MultiBufferPoint {
let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&());
cursor.seek(&point, Bias::Right, &());
match cursor.item() {
@@ -835,7 +836,7 @@ impl InlaySnapshot {
}
}
}
pub fn to_inlay_point(&self, point: Point) -> InlayPoint {
pub fn to_inlay_point(&self, point: MultiBufferPoint) -> InlayPoint {
let mut cursor = self.transforms.cursor::<(Point, InlayPoint)>(&());
cursor.seek(&point, Bias::Left, &());
loop {

View File

@@ -3,7 +3,7 @@ use super::{
Highlights,
};
use language::{Chunk, Point};
use multi_buffer::MultiBufferSnapshot;
use multi_buffer::{MultiBufferPoint, MultiBufferSnapshot};
use std::{cmp, mem, num::NonZeroU32, ops::Range};
use sum_tree::Bias;
@@ -316,13 +316,13 @@ impl TabSnapshot {
)
}
pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
pub fn make_tab_point(&self, point: MultiBufferPoint, bias: Bias) -> TabPoint {
let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point);
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
self.to_tab_point(fold_point)
}
pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
pub fn to_point(&self, point: TabPoint, bias: Bias) -> MultiBufferPoint {
let fold_point = self.to_fold_point(point, bias).0;
let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
self.fold_snapshot

View File

@@ -5,7 +5,7 @@ use super::{
};
use gpui::{AppContext, Context, Font, LineWrapper, Model, ModelContext, Pixels, Task};
use language::{Chunk, Point};
use multi_buffer::MultiBufferSnapshot;
use multi_buffer::{MultiBufferPoint, MultiBufferSnapshot};
use smol::future::yield_now;
use std::sync::LazyLock;
use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration};
@@ -747,11 +747,11 @@ impl WrapSnapshot {
TabPoint(tab_point)
}
pub fn to_point(&self, point: WrapPoint, bias: Bias) -> Point {
pub fn to_point(&self, point: WrapPoint, bias: Bias) -> MultiBufferPoint {
self.tab_snapshot.to_point(self.to_tab_point(point), bias)
}
pub fn make_wrap_point(&self, point: Point, bias: Bias) -> WrapPoint {
pub fn make_wrap_point(&self, point: MultiBufferPoint, bias: Bias) -> WrapPoint {
self.tab_point_to_wrap_point(self.tab_snapshot.make_tab_point(point, bias))
}

View File

@@ -114,8 +114,8 @@ use lsp::{
use mouse_context_menu::MouseContextMenu;
use movement::TextLayoutDetails;
pub use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
ToPoint,
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot,
ToMultiBufferPoint, ToOffset,
};
use multi_buffer::{
ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16,
@@ -4098,8 +4098,10 @@ impl Editor {
if buffer.contains_str_at(selection.start, &pair.end) {
let pair_start_len = pair.start.len();
if buffer.contains_str_at(selection.start - pair_start_len, &pair.start)
{
if buffer.contains_str_at(
selection.start.saturating_sub(pair_start_len),
&pair.start,
) {
selection.start -= pair_start_len;
selection.end += pair.end.len();
@@ -6297,6 +6299,25 @@ impl Editor {
});
}
pub fn autoindent(&mut self, _: &AutoIndent, cx: &mut ViewContext<Self>) {
if self.read_only(cx) {
return;
}
let selections = self
.selections
.all::<usize>(cx)
.into_iter()
.map(|s| s.range());
self.transact(cx, |this, cx| {
this.buffer.update(cx, |buffer, cx| {
buffer.autoindent_ranges(selections, cx);
});
let selections = this.selections.all::<usize>(cx);
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
});
}
pub fn delete_line(&mut self, _: &DeleteLine, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all::<Point>(cx);
@@ -11076,6 +11097,23 @@ impl Editor {
self.fold_creases(fold_ranges, true, cx);
}
pub fn fold_function_bodies(
&mut self,
_: &actions::FoldFunctionBodies,
cx: &mut ViewContext<Self>,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let Some((_, _, buffer)) = snapshot.as_singleton() else {
return;
};
let creases = buffer
.function_body_fold_ranges(0..buffer.len())
.map(|range| Crease::simple(range, self.display_map.read(cx).fold_placeholder.clone()))
.collect();
self.fold_creases(creases, true, cx);
}
pub fn fold_recursive(&mut self, _: &actions::FoldRecursive, cx: &mut ViewContext<Self>) {
let mut to_fold = Vec::new();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
@@ -12814,14 +12852,48 @@ impl Editor {
};
for (buffer, (ranges, scroll_offset)) in new_selections_by_buffer {
let editor =
workspace.open_project_item::<Self>(pane.clone(), buffer, true, true, cx);
let editor = buffer
.read(cx)
.file()
.is_none()
.then(|| {
// Handle file-less buffers separately: those are not really the project items, so won't have a paroject path or entity id,
// so `workspace.open_project_item` will never find them, always opening a new editor.
// Instead, we try to activate the existing editor in the pane first.
let (editor, pane_item_index) =
pane.read(cx).items().enumerate().find_map(|(i, item)| {
let editor = item.downcast::<Editor>()?;
let singleton_buffer =
editor.read(cx).buffer().read(cx).as_singleton()?;
if singleton_buffer == buffer {
Some((editor, i))
} else {
None
}
})?;
pane.update(cx, |pane, cx| {
pane.activate_item(pane_item_index, true, true, cx)
});
Some(editor)
})
.flatten()
.unwrap_or_else(|| {
workspace.open_project_item::<Self>(
pane.clone(),
buffer,
true,
true,
cx,
)
});
editor.update(cx, |editor, cx| {
let autoscroll = match scroll_offset {
Some(scroll_offset) => Autoscroll::top_relative(scroll_offset as usize),
None => Autoscroll::newest(),
};
let nav_history = editor.nav_history.take();
editor.unfold_ranges(&ranges, false, true, cx);
editor.change_selections(Some(autoscroll), cx, |s| {
s.select_ranges(ranges);
});
@@ -14631,7 +14703,7 @@ trait SelectionExt {
) -> Range<MultiBufferRow>;
}
impl<T: ToPoint + ToOffset> SelectionExt for Selection<T> {
impl<T: ToMultiBufferPoint + ToOffset> SelectionExt for Selection<T> {
fn display_range(&self, map: &DisplaySnapshot) -> Range<DisplayPoint> {
let start = self
.start

View File

@@ -34,6 +34,7 @@ use serde_json::{self, json};
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::{self, AtomicBool};
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
use test::editor_lsp_test_context::rust_lang;
use unindent::Unindent;
use util::{
assert_set_eq,
@@ -5458,7 +5459,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
}
#[gpui::test]
async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
async fn test_autoindent(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let language = Arc::new(
@@ -5520,6 +5521,89 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
});
}
#[gpui::test]
async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
{
let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
cx.set_state(indoc! {"
impl A {
fn b() {}
«fn c() {
}ˇ»
}
"});
cx.update_editor(|editor, cx| {
editor.autoindent(&Default::default(), cx);
});
cx.assert_editor_state(indoc! {"
impl A {
fn b() {}
«fn c() {
}ˇ»
}
"});
}
{
let mut cx = EditorTestContext::new_multibuffer(
cx,
[indoc! { "
impl A {
«
// a
fn b(){}
»
«
}
fn c(){}
»
"}],
);
let buffer = cx.update_editor(|editor, cx| {
let buffer = editor.buffer().update(cx, |buffer, _| {
buffer.all_buffers().iter().next().unwrap().clone()
});
buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
buffer
});
cx.run_until_parked();
cx.update_editor(|editor, cx| {
editor.select_all(&Default::default(), cx);
editor.autoindent(&Default::default(), cx)
});
cx.run_until_parked();
cx.update(|cx| {
pretty_assertions::assert_eq!(
buffer.read(cx).text(),
indoc! { "
impl A {
// a
fn b(){}
}
fn c(){}
" }
)
});
}
}
#[gpui::test]
async fn test_autoclose_and_auto_surround_pairs(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
@@ -11483,7 +11567,7 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
}
#[gpui::test]
async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
async fn test_multibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let cols = 4;
@@ -11721,7 +11805,7 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
multi_buffer_editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Next), cx, |s| {
s.select_ranges(Some(60..70))
s.select_ranges(Some(70..70))
});
editor.open_excerpts(&OpenExcerpts, cx);
});
@@ -11772,6 +11856,74 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
.unwrap();
}
#[gpui::test]
async fn test_multibuffer_unfold_on_jump(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let texts = ["{\n\tx\n}".to_owned(), "y".to_owned()];
let buffers = texts
.clone()
.map(|txt| cx.new_model(|cx| Buffer::local(txt, cx)));
let multi_buffer = cx.new_model(|cx| {
let mut multi_buffer = MultiBuffer::new(ReadWrite);
for i in 0..2 {
multi_buffer.push_excerpts(
buffers[i].clone(),
[ExcerptRange {
context: 0..texts[i].len(),
primary: None,
}],
cx,
);
}
multi_buffer
});
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/project",
json!({
"x": &texts[0],
"y": &texts[1],
}),
)
.await;
let project = Project::test(fs, ["/project".as_ref()], cx).await;
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
let multi_buffer_editor = cx.new_view(|cx| {
Editor::for_multibuffer(multi_buffer.clone(), Some(project.clone()), true, cx)
});
let buffer_editor =
cx.new_view(|cx| Editor::for_buffer(buffers[0].clone(), Some(project.clone()), cx));
workspace
.update(cx, |workspace, cx| {
workspace.add_item_to_active_pane(
Box::new(multi_buffer_editor.clone()),
None,
true,
cx,
);
workspace.add_item_to_active_pane(Box::new(buffer_editor.clone()), None, false, cx);
})
.unwrap();
cx.executor().run_until_parked();
buffer_editor.update(cx, |buffer_editor, cx| {
buffer_editor.fold_at_level(&FoldAtLevel { level: 1 }, cx);
assert!(buffer_editor.snapshot(cx).fold_count() == 1);
});
cx.executor().run_until_parked();
multi_buffer_editor.update(cx, |multi_buffer_editor, cx| {
multi_buffer_editor.change_selections(None, cx, |s| s.select_ranges([3..4]));
multi_buffer_editor.open_excerpts(&OpenExcerpts, cx);
});
cx.executor().run_until_parked();
buffer_editor.update(cx, |buffer_editor, cx| {
assert!(buffer_editor.snapshot(cx).fold_count() == 0);
});
}
#[gpui::test]
async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
@@ -13933,20 +14085,6 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC
update_test_language_settings(cx, f);
}
pub(crate) fn rust_lang() -> Arc<Language> {
Arc::new(Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
))
}
#[track_caller]
fn assert_hunk_revert(
not_reverted_text_with_selections: &str,

View File

@@ -20,9 +20,9 @@ use crate::{
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown,
HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, JumpData, LineDown, LineUp, OpenExcerpts,
PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint,
CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap,
ToMultiBufferPoint, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT,
GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
};
use client::ParticipantIndex;
use collections::{BTreeMap, HashMap, HashSet};
@@ -84,7 +84,7 @@ struct SelectionLayout {
}
impl SelectionLayout {
fn new<T: ToPoint + ToDisplayPoint + Clone>(
fn new<T: ToMultiBufferPoint + ToDisplayPoint + Clone>(
selection: Selection<T>,
line_mode: bool,
cursor_shape: CursorShape,
@@ -189,6 +189,7 @@ impl EditorElement {
register_action(view, cx, Editor::tab_prev);
register_action(view, cx, Editor::indent);
register_action(view, cx, Editor::outdent);
register_action(view, cx, Editor::autoindent);
register_action(view, cx, Editor::delete_line);
register_action(view, cx, Editor::join_lines);
register_action(view, cx, Editor::sort_lines_case_sensitive);
@@ -341,6 +342,7 @@ impl EditorElement {
register_action(view, cx, Editor::fold);
register_action(view, cx, Editor::fold_at_level);
register_action(view, cx, Editor::fold_all);
register_action(view, cx, Editor::fold_function_bodies);
register_action(view, cx, Editor::fold_at);
register_action(view, cx, Editor::fold_recursive);
register_action(view, cx, Editor::toggle_fold);

View File

@@ -378,7 +378,7 @@ fn show_hover(
},
..Default::default()
};
Markdown::new_text(text, markdown_style.clone(), None, cx, None)
Markdown::new_text(text, markdown_style.clone(), None, None, cx)
})
.ok();
@@ -593,8 +593,8 @@ async fn parse_blocks(
combined_text,
markdown_style.clone(),
Some(language_registry.clone()),
cx,
fallback_language_name,
cx,
)
})
.ok();

View File

@@ -4,7 +4,7 @@ use gpui::{Action, AnchorCorner, AppContext, CursorStyle, Hsla, Model, MouseButt
use language::{Buffer, BufferId, Point};
use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow,
MultiBufferSnapshot, ToPoint,
MultiBufferSnapshot, ToMultiBufferPoint,
};
use std::{ops::Range, sync::Arc};
use text::OffsetRangeExt;
@@ -399,6 +399,12 @@ impl Editor {
}
}
fn has_multiple_hunks(&self, cx: &AppContext) -> bool {
let snapshot = self.buffer.read(cx).snapshot(cx);
let mut hunks = snapshot.git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX);
hunks.nth(1).is_some()
}
fn hunk_header_block(
&self,
hunk: &HoveredHunk,
@@ -428,6 +434,7 @@ impl Editor {
render: Arc::new({
let editor = cx.view().clone();
let hunk = hunk.clone();
let has_multiple_hunks = self.has_multiple_hunks(cx);
move |cx| {
let hunk_controls_menu_handle =
@@ -471,6 +478,7 @@ impl Editor {
IconButton::new("next-hunk", IconName::ArrowDown)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.disabled(!has_multiple_hunks)
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |cx| {
@@ -499,6 +507,7 @@ impl Editor {
IconButton::new("prev-hunk", IconName::ArrowUp)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.disabled(!has_multiple_hunks)
.tooltip({
let focus_handle = editor.focus_handle(cx);
move |cx| {

View File

@@ -1258,6 +1258,7 @@ pub mod tests {
use crate::{
scroll::{scroll_amount::ScrollAmount, Autoscroll},
test::editor_lsp_test_context::rust_lang,
ExcerptRange,
};
use futures::StreamExt;
@@ -2274,7 +2275,7 @@ pub mod tests {
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(crate::editor_tests::rust_lang());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
@@ -2570,7 +2571,7 @@ pub mod tests {
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
let language = crate::editor_tests::rust_lang();
let language = rust_lang();
language_registry.add(language);
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
@@ -2922,7 +2923,7 @@ pub mod tests {
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(crate::editor_tests::rust_lang());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
@@ -3153,7 +3154,7 @@ pub mod tests {
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(crate::editor_tests::rust_lang());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
@@ -3396,7 +3397,7 @@ pub mod tests {
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(crate::editor_tests::rust_lang());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {

View File

@@ -3,7 +3,7 @@ use crate::{
persistence::{SerializedEditor, DB},
scroll::ScrollAnchor,
Anchor, Autoscroll, Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer,
MultiBufferSnapshot, NavigationData, SearchWithinRange, ToPoint as _,
MultiBufferSnapshot, NavigationData, SearchWithinRange, ToMultiBufferPoint as _,
};
use anyhow::{anyhow, Context as _, Result};
use collections::HashSet;

View File

@@ -2,7 +2,9 @@
//! in editor given a given motion (e.g. it handles converting a "move left" command into coordinates in editor). It is exposed mostly for use by vim crate.
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
use crate::{scroll::ScrollAnchor, CharKind, DisplayRow, EditorStyle, RowExt, ToOffset, ToPoint};
use crate::{
scroll::ScrollAnchor, CharKind, DisplayRow, EditorStyle, RowExt, ToMultiBufferPoint, ToOffset,
};
use gpui::{Pixels, WindowTextSystem};
use language::Point;
use multi_buffer::{MultiBufferRow, MultiBufferSnapshot};
@@ -488,6 +490,101 @@ pub fn find_boundary_point(
map.clip_point(offset.to_display_point(map), Bias::Right)
}
pub fn find_preceding_boundary_trail(
map: &DisplaySnapshot,
head: DisplayPoint,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> (Option<DisplayPoint>, DisplayPoint) {
let mut offset = head.to_offset(map, Bias::Left);
let mut trail_offset = None;
let mut prev_ch = map.buffer_snapshot.chars_at(offset).next();
let mut forward = map.buffer_snapshot.reversed_chars_at(offset).peekable();
// Skip newlines
while let Some(&ch) = forward.peek() {
if ch == '\n' {
prev_ch = forward.next();
offset -= ch.len_utf8();
trail_offset = Some(offset);
} else {
break;
}
}
// Find the boundary
let start_offset = offset;
for ch in forward {
if let Some(prev_ch) = prev_ch {
if is_boundary(prev_ch, ch) {
if start_offset == offset {
trail_offset = Some(offset);
} else {
break;
}
}
}
offset -= ch.len_utf8();
prev_ch = Some(ch);
}
let trail = trail_offset
.map(|trail_offset: usize| map.clip_point(trail_offset.to_display_point(map), Bias::Left));
(
trail,
map.clip_point(offset.to_display_point(map), Bias::Left),
)
}
/// Finds the location of a boundary
pub fn find_boundary_trail(
map: &DisplaySnapshot,
head: DisplayPoint,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> (Option<DisplayPoint>, DisplayPoint) {
let mut offset = head.to_offset(map, Bias::Right);
let mut trail_offset = None;
let mut prev_ch = map.buffer_snapshot.reversed_chars_at(offset).next();
let mut forward = map.buffer_snapshot.chars_at(offset).peekable();
// Skip newlines
while let Some(&ch) = forward.peek() {
if ch == '\n' {
prev_ch = forward.next();
offset += ch.len_utf8();
trail_offset = Some(offset);
} else {
break;
}
}
// Find the boundary
let start_offset = offset;
for ch in forward {
if let Some(prev_ch) = prev_ch {
if is_boundary(prev_ch, ch) {
if start_offset == offset {
trail_offset = Some(offset);
} else {
break;
}
}
}
offset += ch.len_utf8();
prev_ch = Some(ch);
}
let trail = trail_offset
.map(|trail_offset: usize| map.clip_point(trail_offset.to_display_point(map), Bias::Right));
(
trail,
map.clip_point(offset.to_display_point(map), Bias::Right),
)
}
pub fn find_boundary(
map: &DisplaySnapshot,
from: DisplayPoint,

View File

@@ -8,7 +8,7 @@ use crate::{
hover_popover::hide_hover,
persistence::DB,
Anchor, DisplayPoint, DisplayRow, Editor, EditorEvent, EditorMode, EditorSettings,
InlayHintRefreshReason, MultiBufferSnapshot, RowExt, ToPoint,
InlayHintRefreshReason, MultiBufferSnapshot, RowExt, ToMultiBufferPoint,
};
pub use autoscroll::{Autoscroll, AutoscrollStrategy};
use gpui::{point, px, AppContext, Entity, Global, Pixels, Task, ViewContext, WindowContext};

View File

@@ -15,7 +15,7 @@ use crate::{
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
movement::TextLayoutDetails,
Anchor, DisplayPoint, DisplayRow, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode,
ToOffset, ToPoint,
ToMultiBufferPoint, ToOffset,
};
#[derive(Debug, Clone)]
@@ -502,7 +502,13 @@ impl<'a> MutableSelectionsCollection<'a> {
pub fn insert_range<T>(&mut self, range: Range<T>)
where
T: 'a + ToOffset + ToPoint + TextDimension + Ord + Sub<T, Output = T> + std::marker::Copy,
T: 'a
+ ToOffset
+ ToMultiBufferPoint
+ TextDimension
+ Ord
+ Sub<T, Output = T>
+ std::marker::Copy,
{
let mut selections = self.collection.all(self.cx);
let mut start = range.start.to_offset(&self.buffer());
@@ -525,7 +531,7 @@ impl<'a> MutableSelectionsCollection<'a> {
pub fn select<T>(&mut self, mut selections: Vec<Selection<T>>)
where
T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
T: ToOffset + ToMultiBufferPoint + Ord + std::marker::Copy + std::fmt::Debug,
{
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
selections.sort_unstable_by_key(|s| s.start);

View File

@@ -8,7 +8,7 @@ use std::{
use anyhow::Result;
use serde_json::json;
use crate::{Editor, ToPoint};
use crate::{Editor, ToMultiBufferPoint};
use collections::HashSet;
use futures::Future;
use gpui::{View, ViewContext, VisualTestContext};
@@ -31,6 +31,47 @@ pub struct EditorLspTestContext {
pub buffer_lsp_url: lsp::Url,
}
pub(crate) fn rust_lang() -> Arc<Language> {
let language = Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()],
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_queries(LanguageQueries {
indents: Some(Cow::from(indoc! {r#"
[
((where_clause) _ @end)
(field_expression)
(call_expression)
(assignment_expression)
(let_declaration)
(let_chain)
(await_expression)
] @indent
(_ "[" "]" @end) @indent
(_ "<" ">" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent"#})),
brackets: Some(Cow::from(indoc! {r#"
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
("\"" @open "\"" @close)
(closure_parameters "|" @open "|" @close)"#})),
..Default::default()
})
.expect("Could not parse queries");
Arc::new(language)
}
impl EditorLspTestContext {
pub async fn new(
language: Language,
@@ -119,46 +160,7 @@ impl EditorLspTestContext {
capabilities: lsp::ServerCapabilities,
cx: &mut gpui::TestAppContext,
) -> EditorLspTestContext {
let language = Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()],
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_queries(LanguageQueries {
indents: Some(Cow::from(indoc! {r#"
[
((where_clause) _ @end)
(field_expression)
(call_expression)
(assignment_expression)
(let_declaration)
(let_chain)
(await_expression)
] @indent
(_ "[" "]" @end) @indent
(_ "<" ">" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent"#})),
brackets: Some(Cow::from(indoc! {r#"
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
("\"" @open "\"" @close)
(closure_parameters "|" @open "|" @close)"#})),
..Default::default()
})
.expect("Could not parse queries");
Self::new(language, capabilities, cx).await
Self::new(Arc::into_inner(rust_lang()).unwrap(), capabilities, cx).await
}
pub async fn new_typescript(

View File

@@ -11,7 +11,7 @@ use gpui::{
};
use itertools::Itertools;
use language::{Buffer, BufferSnapshot, LanguageRegistry};
use multi_buffer::{ExcerptRange, ToPoint};
use multi_buffer::{ExcerptRange, ToMultiBufferPoint};
use parking_lot::RwLock;
use project::{FakeFs, Project};
use std::{

View File

@@ -1,4 +1,4 @@
use editor::{Editor, ToPoint};
use editor::{Editor, ToMultiBufferPoint};
use gpui::{AppContext, FocusHandle, FocusableView, Subscription, Task, View, WeakView};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

View File

@@ -1111,10 +1111,16 @@ impl PlatformWindow for MacWindow {
}
fn update_ime_position(&self, _bounds: Bounds<ScaledPixels>) {
unsafe {
let input_context: id = msg_send![class!(NSTextInputContext), currentInputContext];
let _: () = msg_send![input_context, invalidateCharacterCoordinates];
}
let executor = self.0.lock().executor.clone();
executor
.spawn(async move {
unsafe {
let input_context: id =
msg_send![class!(NSTextInputContext), currentInputContext];
let _: () = msg_send![input_context, invalidateCharacterCoordinates];
}
})
.detach()
}
}
@@ -1253,7 +1259,10 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
// otherwise we only send to the input handler if we don't have a matching binding.
// The input handler may call `do_command_by_selector` if it doesn't know how to handle
// a key. If it does so, it will return YES so we won't send the key twice.
if is_composing || event.keystroke.key_char.is_none() {
// We also do this for non-printing keys (like arrow keys and escape) as the IME menu
// may need them even if there is no marked text;
// however we skip keys with control or the input handler adds control-characters to the buffer.
if is_composing || (event.keystroke.key_char.is_none() && !event.keystroke.modifiers.control) {
{
let mut lock = window_state.as_ref().lock();
lock.keystroke_for_do_command = Some(event.keystroke.clone());

View File

@@ -128,13 +128,15 @@ impl Scene {
}
pub fn finish(&mut self) {
self.shadows.sort();
self.quads.sort();
self.paths.sort();
self.underlines.sort();
self.monochrome_sprites.sort();
self.polychrome_sprites.sort();
self.surfaces.sort();
self.shadows.sort_by_key(|shadow| shadow.order);
self.quads.sort_by_key(|quad| quad.order);
self.paths.sort_by_key(|path| path.order);
self.underlines.sort_by_key(|underline| underline.order);
self.monochrome_sprites
.sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id));
self.polychrome_sprites
.sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id));
self.surfaces.sort_by_key(|surface| surface.order);
}
#[cfg_attr(
@@ -196,7 +198,7 @@ pub(crate) enum PaintOperation {
EndLayer,
}
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq)]
#[derive(Clone)]
pub(crate) enum Primitive {
Shadow(Shadow),
Quad(Quad),
@@ -449,7 +451,7 @@ pub(crate) enum PrimitiveBatch<'a> {
Surfaces(&'a [PaintSurface]),
}
#[derive(Default, Debug, Clone, Eq, PartialEq)]
#[derive(Default, Debug, Clone)]
#[repr(C)]
pub(crate) struct Quad {
pub order: DrawOrder,
@@ -462,25 +464,13 @@ pub(crate) struct Quad {
pub border_widths: Edges<ScaledPixels>,
}
impl Ord for Quad {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.order.cmp(&other.order)
}
}
impl PartialOrd for Quad {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl From<Quad> for Primitive {
fn from(quad: Quad) -> Self {
Primitive::Quad(quad)
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[derive(Debug, Clone)]
#[repr(C)]
pub(crate) struct Underline {
pub order: DrawOrder,
@@ -492,25 +482,13 @@ pub(crate) struct Underline {
pub wavy: bool,
}
impl Ord for Underline {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.order.cmp(&other.order)
}
}
impl PartialOrd for Underline {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl From<Underline> for Primitive {
fn from(underline: Underline) -> Self {
Primitive::Underline(underline)
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[derive(Debug, Clone)]
#[repr(C)]
pub(crate) struct Shadow {
pub order: DrawOrder,
@@ -521,18 +499,6 @@ pub(crate) struct Shadow {
pub color: Hsla,
}
impl Ord for Shadow {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.order.cmp(&other.order)
}
}
impl PartialOrd for Shadow {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl From<Shadow> for Primitive {
fn from(shadow: Shadow) -> Self {
Primitive::Shadow(shadow)
@@ -642,7 +608,7 @@ impl Default for TransformationMatrix {
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug)]
#[repr(C)]
pub(crate) struct MonochromeSprite {
pub order: DrawOrder,
@@ -654,28 +620,13 @@ pub(crate) struct MonochromeSprite {
pub transformation: TransformationMatrix,
}
impl Ord for MonochromeSprite {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match self.order.cmp(&other.order) {
std::cmp::Ordering::Equal => self.tile.tile_id.cmp(&other.tile.tile_id),
order => order,
}
}
}
impl PartialOrd for MonochromeSprite {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl From<MonochromeSprite> for Primitive {
fn from(sprite: MonochromeSprite) -> Self {
Primitive::MonochromeSprite(sprite)
}
}
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug)]
#[repr(C)]
pub(crate) struct PolychromeSprite {
pub order: DrawOrder,
@@ -687,22 +638,6 @@ pub(crate) struct PolychromeSprite {
pub corner_radii: Corners<ScaledPixels>,
pub tile: AtlasTile,
}
impl Eq for PolychromeSprite {}
impl Ord for PolychromeSprite {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match self.order.cmp(&other.order) {
std::cmp::Ordering::Equal => self.tile.tile_id.cmp(&other.tile.tile_id),
order => order,
}
}
}
impl PartialOrd for PolychromeSprite {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl From<PolychromeSprite> for Primitive {
fn from(sprite: PolychromeSprite) -> Self {
@@ -710,7 +645,7 @@ impl From<PolychromeSprite> for Primitive {
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug)]
pub(crate) struct PaintSurface {
pub order: DrawOrder,
pub bounds: Bounds<ScaledPixels>,
@@ -719,18 +654,6 @@ pub(crate) struct PaintSurface {
pub image_buffer: media::core_video::CVImageBuffer,
}
impl Ord for PaintSurface {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.order.cmp(&other.order)
}
}
impl PartialOrd for PaintSurface {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl From<PaintSurface> for Primitive {
fn from(surface: PaintSurface) -> Self {
Primitive::Surface(surface)
@@ -859,26 +782,6 @@ impl Path<Pixels> {
}
}
impl Eq for Path<ScaledPixels> {}
impl PartialEq for Path<ScaledPixels> {
fn eq(&self, other: &Self) -> bool {
self.order == other.order
}
}
impl Ord for Path<ScaledPixels> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.order.cmp(&other.order)
}
}
impl PartialOrd for Path<ScaledPixels> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl From<Path<ScaledPixels>> for Primitive {
fn from(path: Path<ScaledPixels>) -> Self {
Primitive::Path(path)

View File

@@ -14,7 +14,8 @@ use crate::{
SyntaxMapMatches, SyntaxSnapshot, ToTreeSitterPoint,
},
task_context::RunnableRange,
LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag,
LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag, TextObject,
TreeSitterOptions,
};
use anyhow::{anyhow, Context, Result};
use async_watch as watch;
@@ -467,6 +468,7 @@ struct AutoindentRequest {
before_edit: BufferSnapshot,
entries: Vec<AutoindentRequestEntry>,
is_block_mode: bool,
ignore_empty_lines: bool,
}
#[derive(Debug, Clone)]
@@ -1381,7 +1383,7 @@ impl Buffer {
let autoindent_requests = self.autoindent_requests.clone();
Some(async move {
let mut indent_sizes = BTreeMap::new();
let mut indent_sizes = BTreeMap::<u32, (IndentSize, bool)>::new();
for request in autoindent_requests {
// Resolve each edited range to its row in the current buffer and in the
// buffer before this batch of edits.
@@ -1475,10 +1477,12 @@ impl Buffer {
let suggested_indent = indent_sizes
.get(&suggestion.basis_row)
.copied()
.map(|e| e.0)
.unwrap_or_else(|| {
snapshot.indent_size_for_line(suggestion.basis_row)
})
.with_delta(suggestion.delta, language_indent_size);
if old_suggestions.get(&new_row).map_or(
true,
|(old_indentation, was_within_error)| {
@@ -1486,7 +1490,10 @@ impl Buffer {
&& (!suggestion.within_error || *was_within_error)
},
) {
indent_sizes.insert(new_row, suggested_indent);
indent_sizes.insert(
new_row,
(suggested_indent, request.ignore_empty_lines),
);
}
}
}
@@ -1494,10 +1501,12 @@ impl Buffer {
if let (true, Some(original_indent_column)) =
(request.is_block_mode, original_indent_column)
{
let new_indent = indent_sizes
.get(&row_range.start)
.copied()
.unwrap_or_else(|| snapshot.indent_size_for_line(row_range.start));
let new_indent =
if let Some((indent, _)) = indent_sizes.get(&row_range.start) {
*indent
} else {
snapshot.indent_size_for_line(row_range.start)
};
let delta = new_indent.len as i64 - original_indent_column as i64;
if delta != 0 {
for row in row_range.skip(1) {
@@ -1512,7 +1521,7 @@ impl Buffer {
Ordering::Equal => {}
}
}
size
(size, request.ignore_empty_lines)
});
}
}
@@ -1523,6 +1532,15 @@ impl Buffer {
}
indent_sizes
.into_iter()
.filter_map(|(row, (indent, ignore_empty_lines))| {
if ignore_empty_lines && snapshot.line_len(row) == 0 {
None
} else {
Some((row, indent))
}
})
.collect()
})
}
@@ -2067,6 +2085,7 @@ impl Buffer {
before_edit,
entries,
is_block_mode: matches!(mode, AutoindentMode::Block { .. }),
ignore_empty_lines: false,
}));
}
@@ -2094,6 +2113,30 @@ impl Buffer {
cx.notify();
}
pub fn autoindent_ranges<I, T>(&mut self, ranges: I, cx: &mut ModelContext<Self>)
where
I: IntoIterator<Item = Range<T>>,
T: ToOffset + Copy,
{
let before_edit = self.snapshot();
let entries = ranges
.into_iter()
.map(|range| AutoindentRequestEntry {
range: before_edit.anchor_before(range.start)..before_edit.anchor_after(range.end),
first_line_is_new: true,
indent_size: before_edit.language_indent_size_at(range.start, cx),
original_indent_column: None,
})
.collect();
self.autoindent_requests.push(Arc::new(AutoindentRequest {
before_edit,
entries,
is_block_mode: false,
ignore_empty_lines: true,
}));
self.request_autoindent(cx);
}
// Inserts newlines at the given position to create an empty line, returning the start of the new line.
// You can also request the insertion of empty lines above and below the line starting at the returned point.
pub fn insert_empty_line(
@@ -3312,6 +3355,14 @@ impl BufferSnapshot {
})
}
pub fn function_body_fold_ranges<T: ToOffset>(
&self,
within: Range<T>,
) -> impl Iterator<Item = Range<usize>> + '_ {
self.text_object_ranges(within, TreeSitterOptions::default())
.filter_map(|(range, obj)| (obj == TextObject::InsideFunction).then_some(range))
}
/// For each grammar in the language, runs the provided
/// [`tree_sitter::Query`] against the given range.
pub fn matches(
@@ -3370,6 +3421,72 @@ impl BufferSnapshot {
})
}
pub fn text_object_ranges<T: ToOffset>(
&self,
range: Range<T>,
options: TreeSitterOptions,
) -> impl Iterator<Item = (Range<usize>, TextObject)> + '_ {
let range = range.start.to_offset(self).saturating_sub(1)
..self.len().min(range.end.to_offset(self) + 1);
let mut matches =
self.syntax
.matches_with_options(range.clone(), &self.text, options, |grammar| {
grammar.text_object_config.as_ref().map(|c| &c.query)
});
let configs = matches
.grammars()
.iter()
.map(|grammar| grammar.text_object_config.as_ref())
.collect::<Vec<_>>();
let mut captures = Vec::<(Range<usize>, TextObject)>::new();
iter::from_fn(move || loop {
while let Some(capture) = captures.pop() {
if capture.0.overlaps(&range) {
return Some(capture);
}
}
let mat = matches.peek()?;
let Some(config) = configs[mat.grammar_index].as_ref() else {
matches.advance();
continue;
};
for capture in mat.captures {
let Some(ix) = config
.text_objects_by_capture_ix
.binary_search_by_key(&capture.index, |e| e.0)
.ok()
else {
continue;
};
let text_object = config.text_objects_by_capture_ix[ix].1;
let byte_range = capture.node.byte_range();
let mut found = false;
for (range, existing) in captures.iter_mut() {
if existing == &text_object {
range.start = range.start.min(byte_range.start);
range.end = range.end.max(byte_range.end);
found = true;
break;
}
}
if !found {
captures.push((byte_range, text_object));
}
}
matches.advance();
})
}
/// Returns enclosing bracket ranges containing the given range
pub fn enclosing_bracket_ranges<T: ToOffset>(
&self,
@@ -4515,7 +4632,7 @@ impl CharClassifier {
self.kind(c) == CharKind::Punctuation
}
pub fn kind(&self, c: char) -> CharKind {
pub fn kind_with(&self, c: char, ignore_punctuation: bool) -> CharKind {
if c.is_whitespace() {
return CharKind::Whitespace;
} else if c.is_alphanumeric() || c == '_' {
@@ -4525,7 +4642,7 @@ impl CharClassifier {
if let Some(scope) = &self.scope {
if let Some(characters) = scope.word_characters() {
if characters.contains(&c) {
if c == '-' && !self.for_completion && !self.ignore_punctuation {
if c == '-' && !self.for_completion && !ignore_punctuation {
return CharKind::Punctuation;
}
return CharKind::Word;
@@ -4533,12 +4650,16 @@ impl CharClassifier {
}
}
if self.ignore_punctuation {
if ignore_punctuation {
CharKind::Word
} else {
CharKind::Punctuation
}
}
pub fn kind(&self, c: char) -> CharKind {
self.kind_with(c, self.ignore_punctuation)
}
}
/// Find all of the ranges of whitespace that occur at the ends of lines

View File

@@ -20,6 +20,7 @@ use std::{
sync::LazyLock,
time::{Duration, Instant},
};
use syntax_map::TreeSitterOptions;
use text::network::Network;
use text::{BufferId, LineEnding, LineIndent};
use text::{Point, ToPoint};
@@ -915,6 +916,39 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
}
}
#[gpui::test]
fn test_text_objects(cx: &mut AppContext) {
let (text, ranges) = marked_text_ranges(
indoc! {r#"
impl Hello {
fn say() -> u8 { return /* ˇhi */ 1 }
}"#
},
false,
);
let buffer =
cx.new_model(|cx| Buffer::local(text.clone(), cx).with_language(Arc::new(rust_lang()), cx));
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
let matches = snapshot
.text_object_ranges(ranges[0].clone(), TreeSitterOptions::default())
.map(|(range, text_object)| (&text[range], text_object))
.collect::<Vec<_>>();
assert_eq!(
matches,
&[
("/* hi */", TextObject::AroundComment),
("return /* hi */ 1", TextObject::InsideFunction),
(
"fn say() -> u8 { return /* hi */ 1 }",
TextObject::AroundFunction
),
],
)
}
#[gpui::test]
fn test_enclosing_bracket_ranges(cx: &mut AppContext) {
let mut assert = |selection_text, range_markers| {
@@ -3182,6 +3216,20 @@ fn rust_lang() -> Language {
"#,
)
.unwrap()
.with_text_object_query(
r#"
(function_item
body: (_
"{"
(_)* @function.inside
"}" )) @function.around
(line_comment)+ @comment.around
(block_comment) @comment.around
"#,
)
.unwrap()
.with_outline_query(
r#"
(line_comment) @annotation

View File

@@ -78,7 +78,7 @@ pub use language_registry::{
};
pub use lsp::LanguageServerId;
pub use outline::*;
pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer};
pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer, TreeSitterOptions};
pub use text::{AnchorRangeExt, LineEnding};
pub use tree_sitter::{Node, Parser, Tree, TreeCursor};
@@ -848,6 +848,7 @@ pub struct Grammar {
pub(crate) runnable_config: Option<RunnableConfig>,
pub(crate) indents_config: Option<IndentConfig>,
pub outline_config: Option<OutlineConfig>,
pub text_object_config: Option<TextObjectConfig>,
pub embedding_config: Option<EmbeddingConfig>,
pub(crate) injection_config: Option<InjectionConfig>,
pub(crate) override_config: Option<OverrideConfig>,
@@ -873,6 +874,44 @@ pub struct OutlineConfig {
pub annotation_capture_ix: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TextObject {
InsideFunction,
AroundFunction,
InsideClass,
AroundClass,
InsideComment,
AroundComment,
}
impl TextObject {
pub fn from_capture_name(name: &str) -> Option<TextObject> {
match name {
"function.inside" => Some(TextObject::InsideFunction),
"function.around" => Some(TextObject::AroundFunction),
"class.inside" => Some(TextObject::InsideClass),
"class.around" => Some(TextObject::AroundClass),
"comment.inside" => Some(TextObject::InsideComment),
"comment.around" => Some(TextObject::AroundComment),
_ => None,
}
}
pub fn around(&self) -> Option<Self> {
match self {
TextObject::InsideFunction => Some(TextObject::AroundFunction),
TextObject::InsideClass => Some(TextObject::AroundClass),
TextObject::InsideComment => Some(TextObject::AroundComment),
_ => None,
}
}
}
pub struct TextObjectConfig {
pub query: Query,
pub text_objects_by_capture_ix: Vec<(u32, TextObject)>,
}
#[derive(Debug)]
pub struct EmbeddingConfig {
pub query: Query,
@@ -950,6 +989,7 @@ impl Language {
highlights_query: None,
brackets_config: None,
outline_config: None,
text_object_config: None,
embedding_config: None,
indents_config: None,
injection_config: None,
@@ -1020,7 +1060,12 @@ impl Language {
if let Some(query) = queries.runnables {
self = self
.with_runnable_query(query.as_ref())
.context("Error loading tests query")?;
.context("Error loading runnables query")?;
}
if let Some(query) = queries.text_objects {
self = self
.with_text_object_query(query.as_ref())
.context("Error loading textobject query")?;
}
Ok(self)
}
@@ -1097,6 +1142,26 @@ impl Language {
Ok(self)
}
pub fn with_text_object_query(mut self, source: &str) -> Result<Self> {
let grammar = self
.grammar_mut()
.ok_or_else(|| anyhow!("cannot mutate grammar"))?;
let query = Query::new(&grammar.ts_language, source)?;
let mut text_objects_by_capture_ix = Vec::new();
for (ix, name) in query.capture_names().iter().enumerate() {
if let Some(text_object) = TextObject::from_capture_name(name) {
text_objects_by_capture_ix.push((ix as u32, text_object));
}
}
grammar.text_object_config = Some(TextObjectConfig {
query,
text_objects_by_capture_ix,
});
Ok(self)
}
pub fn with_embedding_query(mut self, source: &str) -> Result<Self> {
let grammar = self
.grammar_mut()

View File

@@ -181,6 +181,7 @@ pub const QUERY_FILENAME_PREFIXES: &[(
("overrides", |q| &mut q.overrides),
("redactions", |q| &mut q.redactions),
("runnables", |q| &mut q.runnables),
("textobjects", |q| &mut q.text_objects),
];
/// Tree-sitter language queries for a given language.
@@ -195,6 +196,7 @@ pub struct LanguageQueries {
pub overrides: Option<Cow<'static, str>>,
pub redactions: Option<Cow<'static, str>>,
pub runnables: Option<Cow<'static, str>>,
pub text_objects: Option<Cow<'static, str>>,
}
#[derive(Clone, Default)]

View File

@@ -814,6 +814,23 @@ impl SyntaxSnapshot {
buffer.as_rope(),
self.layers_for_range(range, buffer, true),
query,
TreeSitterOptions::default(),
)
}
pub fn matches_with_options<'a>(
&'a self,
range: Range<usize>,
buffer: &'a BufferSnapshot,
options: TreeSitterOptions,
query: fn(&Grammar) -> Option<&Query>,
) -> SyntaxMapMatches<'a> {
SyntaxMapMatches::new(
range.clone(),
buffer.as_rope(),
self.layers_for_range(range, buffer, true),
query,
options,
)
}
@@ -1001,12 +1018,25 @@ impl<'a> SyntaxMapCaptures<'a> {
}
}
#[derive(Default)]
pub struct TreeSitterOptions {
max_start_depth: Option<u32>,
}
impl TreeSitterOptions {
pub fn max_start_depth(max_start_depth: u32) -> Self {
Self {
max_start_depth: Some(max_start_depth),
}
}
}
impl<'a> SyntaxMapMatches<'a> {
fn new(
range: Range<usize>,
text: &'a Rope,
layers: impl Iterator<Item = SyntaxLayer<'a>>,
query: fn(&Grammar) -> Option<&Query>,
options: TreeSitterOptions,
) -> Self {
let mut result = Self::default();
for layer in layers {
@@ -1027,6 +1057,7 @@ impl<'a> SyntaxMapMatches<'a> {
query_cursor.deref_mut(),
)
};
cursor.set_max_start_depth(options.max_start_depth);
cursor.set_byte_range(range.clone());
let matches = cursor.matches(query, layer.node(), TextProvider(text));

View File

@@ -0,0 +1,7 @@
(function_definition
body: (_
"{"
(_)* @function.inside
"}" )) @function.around
(comment) @comment.around

View File

@@ -0,0 +1,25 @@
(declaration
declarator: (function_declarator)) @function.around
(function_definition
body: (_
"{"
(_)* @function.inside
"}" )) @function.around
(preproc_function_def
value: (_) @function.inside) @function.around
(comment) @comment.around
(struct_specifier
body: (_
"{"
(_)* @class.inside
"}")) @class.around
(enum_specifier
body: (_
"{"
[(_) ","?]* @class.inside
"}")) @class.around

View File

@@ -0,0 +1,31 @@
(declaration
declarator: (function_declarator)) @function.around
(function_definition
body: (_
"{"
(_)* @function.inside
"}" )) @function.around
(preproc_function_def
value: (_) @function.inside) @function.around
(comment) @comment.around
(struct_specifier
body: (_
"{"
(_)* @class.inside
"}")) @class.around
(enum_specifier
body: (_
"{"
[(_) ","?]* @class.inside
"}")) @class.around
(class_specifier
body: (_
"{"
[(_) ":"? ";"?]* @class.inside
"}"?)) @class.around

View File

@@ -0,0 +1,30 @@
(comment) @comment.around
(rule_set
(block (
"{"
(_)* @function.inside
"}" ))) @function.around
(keyframe_block
(block (
"{"
(_)* @function.inside
"}" ))) @function.around
(media_statement
(block (
"{"
(_)* @class.inside
"}" ))) @class.around
(supports_statement
(block (
"{"
(_)* @class.inside
"}" ))) @class.around
(keyframes_statement
(keyframe_block_list (
"{"
(_)* @class.inside
"}" ))) @class.around

View File

@@ -0,0 +1,25 @@
(function_declaration
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(method_declaration
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(type_declaration
(type_spec (struct_type (field_declaration_list (
"{"
(_)* @class.inside
"}")?)))) @class.around
(type_declaration
(type_spec (interface_type
(_)* @class.inside))) @class.around
(type_declaration) @class.around
(comment)+ @comment.around

View File

@@ -0,0 +1,51 @@
(comment)+ @comment.around
(function_declaration
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(method_definition
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(function_expression
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}")) @function.around
(arrow_function) @function.around
(generator_function
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(generator_function_declaration
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(class_declaration
body: (_
"{"
[(_) ";"?]* @class.inside
"}" )) @class.around
(class
body: (_
"{"
[(_) ";"?]* @class.inside
"}" )) @class.around

View File

@@ -0,0 +1 @@
(comment)+ @comment.around

View File

@@ -0,0 +1 @@
(comment)+ @comment.around

View File

@@ -0,0 +1,3 @@
(section
(atx_heading)
(_)* @class.inside) @class.around

View File

@@ -0,0 +1,7 @@
(comment)+ @comment.around
(function_definition
body: (_) @function.inside) @function.around
(class_definition
body: (_) @class.inside) @class.around

View File

@@ -15,11 +15,7 @@
(visibility_modifier)? @context
name: (_) @name) @item
(impl_item
"impl" @context
trait: (_)? @name
"for"? @context
type: (_) @name
(function_item
body: (_ "{" @open (_)* "}" @close)) @item
(trait_item

View File

@@ -0,0 +1,51 @@
; functions
(function_signature_item) @function.around
(function_item
body: (_
"{"
(_)* @function.inside
"}" )) @function.around
; classes
(struct_item
body: (_
["{" "("]?
[(_) ","?]* @class.inside
["}" ")"]? )) @class.around
(enum_item
body: (_
"{"
[(_) ","?]* @class.inside
"}" )) @class.around
(union_item
body: (_
"{"
[(_) ","?]* @class.inside
"}" )) @class.around
(trait_item
body: (_
"{"
[(_) ","?]* @class.inside
"}" )) @class.around
(impl_item
body: (_
"{"
[(_) ","?]* @class.inside
"}" )) @class.around
(mod_item
body: (_
"{"
[(_) ","?]* @class.inside
"}" )) @class.around
; comments
(line_comment)+ @comment.around
(block_comment) @comment.around

View File

@@ -0,0 +1,79 @@
(comment)+ @comment.around
(function_declaration
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(method_definition
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(function_expression
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}")) @function.around
(arrow_function) @function.around
(function_signature) @function.around
(generator_function
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(generator_function_declaration
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(class_declaration
body: (_
"{"
[(_) ";"?]* @class.inside
"}" )) @class.around
(class
body: (_
"{"
(_)* @class.inside
"}" )) @class.around
(interface_declaration
body: (_
"{"
[(_) ";"?]* @class.inside
"}" )) @class.around
(enum_declaration
body: (_
"{"
[(_) ","?]* @class.inside
"}" )) @class.around
(ambient_declaration
(module
body: (_
"{"
[(_) ";"?]* @class.inside
"}" ))) @class.around
(internal_module
body: (_
"{"
[(_) ";"?]* @class.inside
"}" )) @class.around
(type_alias_declaration) @class.around

View File

@@ -0,0 +1,79 @@
(comment)+ @comment.around
(function_declaration
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(method_definition
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(function_expression
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}")) @function.around
(arrow_function) @function.around
(function_signature) @function.around
(generator_function
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(generator_function_declaration
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(class_declaration
body: (_
"{"
[(_) ";"?]* @class.inside
"}" )) @class.around
(class
body: (_
"{"
(_)* @class.inside
"}" )) @class.around
(interface_declaration
body: (_
"{"
[(_) ";"?]* @class.inside
"}" )) @class.around
(enum_declaration
body: (_
"{"
[(_) ","?]* @class.inside
"}" )) @class.around
(ambient_declaration
(module
body: (_
"{"
[(_) ";"?]* @class.inside
"}" ))) @class.around
(internal_module
body: (_
"{"
[(_) ";"?]* @class.inside
"}" )) @class.around
(type_alias_declaration) @class.around

View File

@@ -0,0 +1 @@
(comment)+ @comment

View File

@@ -178,7 +178,7 @@ impl MarkdownExample {
cx: &mut WindowContext,
) -> Self {
let markdown =
cx.new_view(|cx| Markdown::new(text, style, Some(language_registry), cx, None));
cx.new_view(|cx| Markdown::new(text, style, Some(language_registry), None, cx));
Self { markdown }
}
}

View File

@@ -87,7 +87,7 @@ pub fn main() {
heading: Default::default(),
};
let markdown = cx.new_view(|cx| {
Markdown::new(MARKDOWN_EXAMPLE.into(), markdown_style, None, cx, None)
Markdown::new(MARKDOWN_EXAMPLE.into(), markdown_style, None, None, cx)
});
HelloWorld { markdown }

View File

@@ -71,8 +71,8 @@ impl Markdown {
source: String,
style: MarkdownStyle,
language_registry: Option<Arc<LanguageRegistry>>,
cx: &ViewContext<Self>,
fallback_code_block_language: Option<String>,
cx: &ViewContext<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
let mut this = Self {
@@ -97,8 +97,8 @@ impl Markdown {
source: String,
style: MarkdownStyle,
language_registry: Option<Arc<LanguageRegistry>>,
cx: &ViewContext<Self>,
fallback_code_block_language: Option<String>,
cx: &ViewContext<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
let mut this = Self {

View File

@@ -1,5 +1,7 @@
use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToOffsetUtf16, ToPoint};
use language::{OffsetUtf16, Point, TextDimension};
use crate::MultiBufferPoint;
use super::{ExcerptId, MultiBufferSnapshot, ToMultiBufferPoint, ToOffset, ToOffsetUtf16};
use language::{OffsetUtf16, TextDimension};
use std::{
cmp::Ordering,
ops::{Range, Sub},
@@ -109,9 +111,9 @@ impl ToOffsetUtf16 for Anchor {
}
}
impl ToPoint for Anchor {
fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point {
self.summary(snapshot)
impl ToMultiBufferPoint for Anchor {
fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferPoint {
MultiBufferPoint(self.summary(snapshot))
}
}
@@ -119,7 +121,7 @@ pub trait AnchorRangeExt {
fn cmp(&self, b: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Ordering;
fn overlaps(&self, b: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool;
fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize>;
fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point>;
fn to_point(&self, content: &MultiBufferSnapshot) -> Range<MultiBufferPoint>;
}
impl AnchorRangeExt for Range<Anchor> {
@@ -138,7 +140,7 @@ impl AnchorRangeExt for Range<Anchor> {
self.start.to_offset(content)..self.end.to_offset(content)
}
fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point> {
fn to_point(&self, content: &MultiBufferSnapshot) -> Range<MultiBufferPoint> {
self.start.to_point(content)..self.end.to_point(content)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,9 @@
use crate::Project;
use anyhow::Context as _;
use anyhow::{Context as _, Result};
use collections::HashMap;
use gpui::{AnyWindowHandle, AppContext, Context, Entity, Model, ModelContext, WeakModel};
use gpui::{AnyWindowHandle, AppContext, Context, Entity, Model, ModelContext, Task, WeakModel};
use itertools::Itertools;
use language::LanguageName;
use settings::{Settings, SettingsLocation};
use smol::channel::bounded;
use std::{
@@ -10,10 +11,11 @@ use std::{
env::{self},
iter,
path::{Path, PathBuf},
sync::Arc,
};
use task::{Shell, SpawnInTerminal};
use terminal::{
terminal_settings::{self, TerminalSettings},
terminal_settings::{self, TerminalSettings, VenvSettings},
TaskState, TaskStatus, Terminal, TerminalBuilder,
};
use util::ResultExt;
@@ -42,7 +44,7 @@ pub struct SshCommand {
}
impl Project {
pub fn active_project_directory(&self, cx: &AppContext) -> Option<PathBuf> {
pub fn active_project_directory(&self, cx: &AppContext) -> Option<Arc<Path>> {
let worktree = self
.active_entry()
.and_then(|entry_id| self.worktree_for_entry(entry_id, cx))
@@ -53,7 +55,7 @@ impl Project {
worktree
.root_entry()
.filter(|entry| entry.is_dir())
.map(|_| worktree.abs_path().to_path_buf())
.map(|_| worktree.abs_path().clone())
});
worktree
}
@@ -87,12 +89,12 @@ impl Project {
kind: TerminalKind,
window: AnyWindowHandle,
cx: &mut ModelContext<Self>,
) -> anyhow::Result<Model<Terminal>> {
let path = match &kind {
TerminalKind::Shell(path) => path.as_ref().map(|path| path.to_path_buf()),
) -> Task<Result<Model<Terminal>>> {
let path: Option<Arc<Path>> = match &kind {
TerminalKind::Shell(path) => path.as_ref().map(|path| Arc::from(path.as_ref())),
TerminalKind::Task(spawn_task) => {
if let Some(cwd) = &spawn_task.cwd {
Some(cwd.clone())
Some(Arc::from(cwd.as_ref()))
} else {
self.active_project_directory(cx)
}
@@ -109,7 +111,7 @@ impl Project {
});
}
}
let settings = TerminalSettings::get(settings_location, cx);
let settings = TerminalSettings::get(settings_location, cx).clone();
let (completion_tx, completion_rx) = bounded(1);
@@ -128,160 +130,206 @@ impl Project {
} else {
None
};
let python_venv_directory = path
.as_ref()
.and_then(|path| self.python_venv_directory(path, settings, cx));
let mut python_venv_activate_command = None;
let (spawn_task, shell) = match kind {
TerminalKind::Shell(_) => {
if let Some(python_venv_directory) = python_venv_directory {
python_venv_activate_command =
self.python_activate_command(&python_venv_directory, settings);
}
cx.spawn(move |this, mut cx| async move {
let python_venv_directory = if let Some(path) = path.clone() {
this.update(&mut cx, |this, cx| {
this.python_venv_directory(path, settings.detect_venv.clone(), cx)
})?
.await
} else {
None
};
let mut python_venv_activate_command = None;
match &ssh_details {
Some((host, ssh_command)) => {
log::debug!("Connecting to a remote server: {ssh_command:?}");
// Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
// to properly display colors.
// We do not have the luxury of assuming the host has it installed,
// so we set it to a default that does not break the highlighting via ssh.
env.entry("TERM".to_string())
.or_insert_with(|| "xterm-256color".to_string());
let (program, args) =
wrap_for_ssh(ssh_command, None, path.as_deref(), env, None);
env = HashMap::default();
(
None,
Shell::WithArguments {
program,
args,
title_override: Some(format!("{} — Terminal", host).into()),
},
)
let (spawn_task, shell) = match kind {
TerminalKind::Shell(_) => {
if let Some(python_venv_directory) = python_venv_directory {
python_venv_activate_command = this
.update(&mut cx, |this, _| {
this.python_activate_command(
&python_venv_directory,
&settings.detect_venv,
)
})
.ok()
.flatten();
}
None => (None, settings.shell.clone()),
}
}
TerminalKind::Task(spawn_task) => {
let task_state = Some(TaskState {
id: spawn_task.id,
full_label: spawn_task.full_label,
label: spawn_task.label,
command_label: spawn_task.command_label,
hide: spawn_task.hide,
status: TaskStatus::Running,
show_summary: spawn_task.show_summary,
show_command: spawn_task.show_command,
completion_rx,
});
env.extend(spawn_task.env);
match &ssh_details {
Some((host, ssh_command)) => {
log::debug!("Connecting to a remote server: {ssh_command:?}");
if let Some(venv_path) = &python_venv_directory {
env.insert(
"VIRTUAL_ENV".to_string(),
venv_path.to_string_lossy().to_string(),
);
}
// Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
// to properly display colors.
// We do not have the luxury of assuming the host has it installed,
// so we set it to a default that does not break the highlighting via ssh.
env.entry("TERM".to_string())
.or_insert_with(|| "xterm-256color".to_string());
match &ssh_details {
Some((host, ssh_command)) => {
log::debug!("Connecting to a remote server: {ssh_command:?}");
env.entry("TERM".to_string())
.or_insert_with(|| "xterm-256color".to_string());
let (program, args) = wrap_for_ssh(
ssh_command,
Some((&spawn_task.command, &spawn_task.args)),
path.as_deref(),
env,
python_venv_directory,
);
env = HashMap::default();
(
task_state,
Shell::WithArguments {
program,
args,
title_override: Some(format!("{} — Terminal", host).into()),
},
)
}
None => {
if let Some(venv_path) = &python_venv_directory {
add_environment_path(&mut env, &venv_path.join("bin")).log_err();
let (program, args) =
wrap_for_ssh(ssh_command, None, path.as_deref(), env, None);
env = HashMap::default();
(
Option::<TaskState>::None,
Shell::WithArguments {
program,
args,
title_override: Some(format!("{} — Terminal", host).into()),
},
)
}
(
task_state,
Shell::WithArguments {
program: spawn_task.command,
args: spawn_task.args,
title_override: None,
},
)
None => (None, settings.shell.clone()),
}
}
}
};
TerminalKind::Task(spawn_task) => {
let task_state = Some(TaskState {
id: spawn_task.id,
full_label: spawn_task.full_label,
label: spawn_task.label,
command_label: spawn_task.command_label,
hide: spawn_task.hide,
status: TaskStatus::Running,
show_summary: spawn_task.show_summary,
show_command: spawn_task.show_command,
completion_rx,
});
let terminal = TerminalBuilder::new(
local_path,
spawn_task,
shell,
env,
settings.cursor_shape.unwrap_or_default(),
settings.alternate_scroll,
settings.max_scroll_history_lines,
ssh_details.is_some(),
window,
completion_tx,
cx,
)
.map(|builder| {
let terminal_handle = cx.new_model(|cx| builder.subscribe(cx));
env.extend(spawn_task.env);
self.terminals
.local_handles
.push(terminal_handle.downgrade());
if let Some(venv_path) = &python_venv_directory {
env.insert(
"VIRTUAL_ENV".to_string(),
venv_path.to_string_lossy().to_string(),
);
}
let id = terminal_handle.entity_id();
cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
let handles = &mut project.terminals.local_handles;
match &ssh_details {
Some((host, ssh_command)) => {
log::debug!("Connecting to a remote server: {ssh_command:?}");
env.entry("TERM".to_string())
.or_insert_with(|| "xterm-256color".to_string());
let (program, args) = wrap_for_ssh(
ssh_command,
Some((&spawn_task.command, &spawn_task.args)),
path.as_deref(),
env,
python_venv_directory,
);
env = HashMap::default();
(
task_state,
Shell::WithArguments {
program,
args,
title_override: Some(format!("{} — Terminal", host).into()),
},
)
}
None => {
if let Some(venv_path) = &python_venv_directory {
add_environment_path(&mut env, &venv_path.join("bin")).log_err();
}
if let Some(index) = handles
.iter()
.position(|terminal| terminal.entity_id() == id)
{
handles.remove(index);
cx.notify();
(
task_state,
Shell::WithArguments {
program: spawn_task.command,
args: spawn_task.args,
title_override: None,
},
)
}
}
}
})
.detach();
};
let terminal = this.update(&mut cx, |this, cx| {
TerminalBuilder::new(
local_path.map(|path| path.to_path_buf()),
spawn_task,
shell,
env,
settings.cursor_shape.unwrap_or_default(),
settings.alternate_scroll,
settings.max_scroll_history_lines,
ssh_details.is_some(),
window,
completion_tx,
cx,
)
.map(|builder| {
let terminal_handle = cx.new_model(|cx| builder.subscribe(cx));
if let Some(activate_command) = python_venv_activate_command {
self.activate_python_virtual_environment(activate_command, &terminal_handle, cx);
}
terminal_handle
});
this.terminals
.local_handles
.push(terminal_handle.downgrade());
terminal
let id = terminal_handle.entity_id();
cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
let handles = &mut project.terminals.local_handles;
if let Some(index) = handles
.iter()
.position(|terminal| terminal.entity_id() == id)
{
handles.remove(index);
cx.notify();
}
})
.detach();
if let Some(activate_command) = python_venv_activate_command {
this.activate_python_virtual_environment(
activate_command,
&terminal_handle,
cx,
);
}
terminal_handle
})
})?;
terminal
})
}
pub fn python_venv_directory(
fn python_venv_directory(
&self,
abs_path: &Path,
settings: &TerminalSettings,
cx: &AppContext,
) -> Option<PathBuf> {
let venv_settings = settings.detect_venv.as_option()?;
if let Some(path) = self.find_venv_in_worktree(abs_path, &venv_settings, cx) {
return Some(path);
}
self.find_venv_on_filesystem(abs_path, &venv_settings, cx)
abs_path: Arc<Path>,
venv_settings: VenvSettings,
cx: &ModelContext<Project>,
) -> Task<Option<PathBuf>> {
cx.spawn(move |this, mut cx| async move {
if let Some((worktree, _)) = this
.update(&mut cx, |this, cx| this.find_worktree(&abs_path, cx))
.ok()?
{
let toolchain = this
.update(&mut cx, |this, cx| {
this.active_toolchain(
worktree.read(cx).id(),
LanguageName::new("Python"),
cx,
)
})
.ok()?
.await;
if let Some(toolchain) = toolchain {
let toolchain_path = Path::new(toolchain.path.as_ref());
return Some(toolchain_path.parent()?.parent()?.to_path_buf());
}
}
let venv_settings = venv_settings.as_option()?;
this.update(&mut cx, move |this, cx| {
if let Some(path) = this.find_venv_in_worktree(&abs_path, &venv_settings, cx) {
return Some(path);
}
this.find_venv_on_filesystem(&abs_path, &venv_settings, cx)
})
.ok()
.flatten()
})
}
fn find_venv_in_worktree(
@@ -337,9 +385,9 @@ impl Project {
fn python_activate_command(
&self,
venv_base_directory: &Path,
settings: &TerminalSettings,
venv_settings: &VenvSettings,
) -> Option<String> {
let venv_settings = settings.detect_venv.as_option()?;
let venv_settings = venv_settings.as_option()?;
let activate_keyword = match venv_settings.activate_script {
terminal_settings::ActivateScript::Default => match std::env::consts::OS {
"windows" => ".",
@@ -441,7 +489,7 @@ pub fn wrap_for_ssh(
(program, args)
}
fn add_environment_path(env: &mut HashMap<String, String>, new_path: &Path) -> anyhow::Result<()> {
fn add_environment_path(env: &mut HashMap<String, String>, new_path: &Path) -> Result<()> {
let mut env_paths = vec![new_path.to_path_buf()];
if let Some(path) = env.get("PATH").or(env::var("PATH").ok().as_ref()) {
let mut paths = std::env::split_paths(&path).collect::<Vec<_>>();

View File

@@ -201,7 +201,7 @@ impl SshPrompt {
selection_background_color: cx.theme().players().local().selection,
..Default::default()
};
let markdown = cx.new_view(|cx| Markdown::new_text(prompt, markdown_style, None, cx, None));
let markdown = cx.new_view(|cx| Markdown::new_text(prompt, markdown_style, None, None, cx));
self.prompt = Some((markdown, tx));
self.status_message.take();
cx.focus_view(&self.editor);

View File

@@ -6,9 +6,12 @@ use futures::{
AsyncBufReadExt as _, SinkExt as _,
};
use gpui::{EntityId, Task, View, WindowContext};
use jupyter_protocol::{JupyterKernelspec, JupyterMessage, JupyterMessageContent, KernelInfoReply};
use jupyter_protocol::{
connection_info::{ConnectionInfo, Transport},
ExecutionState, JupyterKernelspec, JupyterMessage, JupyterMessageContent, KernelInfoReply,
};
use project::Fs;
use runtimelib::{dirs, ConnectionInfo, ExecutionState};
use runtimelib::dirs;
use smol::{net::TcpListener, process::Command};
use std::{
env,
@@ -119,7 +122,7 @@ impl NativeRunningKernel {
let ports = peek_ports(ip).await?;
let connection_info = ConnectionInfo {
transport: "tcp".to_string(),
transport: Transport::TCP,
ip: ip.to_string(),
stdin_port: ports[0],
control_port: ports[1],

View File

@@ -334,9 +334,11 @@ impl ExecutionView {
result.transient.as_ref().and_then(|t| t.display_id.clone()),
cx,
),
JupyterMessageContent::DisplayData(result) => {
Output::new(&result.data, result.transient.display_id.clone(), cx)
}
JupyterMessageContent::DisplayData(result) => Output::new(
&result.data,
result.transient.as_ref().and_then(|t| t.display_id.clone()),
cx,
),
JupyterMessageContent::StreamContent(result) => {
// Previous stream data will combine together, handling colors, carriage returns, etc
if let Some(new_terminal) = self.apply_terminal_text(&result.text, cx) {

View File

@@ -14,7 +14,7 @@ use editor::{
RenderBlock,
},
scroll::Autoscroll,
Anchor, AnchorRangeExt as _, Editor, MultiBuffer, ToPoint,
Anchor, AnchorRangeExt as _, Editor, MultiBuffer, ToMultiBufferPoint,
};
use futures::FutureExt as _;
use gpui::{

View File

@@ -13,7 +13,7 @@ anyhow.workspace = true
collections.workspace = true
futures.workspace = true
indoc.workspace = true
libsqlite3-sys = { version = "0.28", features = ["bundled"] }
libsqlite3-sys.workspace = true
parking_lot.workspace = true
smol.workspace = true
sqlformat.workspace = true

View File

@@ -24,7 +24,7 @@ pub struct Toolbar {
pub breadcrumbs: bool,
}
#[derive(Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize)]
pub struct TerminalSettings {
pub shell: Shell,
pub working_directory: WorkingDirectory,

View File

@@ -1,7 +1,6 @@
Design notes:
This crate is split into two conceptual halves:
- The terminal.rs file and the src/mappings/ folder, these contain the code for interacting with Alacritty and maintaining the pty event loop. Some behavior in this file is constrained by terminal protocols and standards. The Zed init function is also placed here.
- Everything else. These other files integrate the `Terminal` struct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file.

View File

@@ -5,7 +5,7 @@ use futures::{stream::FuturesUnordered, StreamExt as _};
use gpui::{AsyncWindowContext, Axis, Model, Task, View, WeakView};
use project::{terminals::TerminalKind, Project};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use ui::{Pixels, ViewContext, VisualContext as _, WindowContext};
use util::ResultExt as _;
@@ -219,33 +219,39 @@ async fn deserialize_pane_group(
})
.log_err()?;
let active_item = serialized_pane.active_item;
pane.update(cx, |pane, cx| {
populate_pane_items(pane, new_items, active_item, cx);
// Avoid blank panes in splits
if pane.items_len() == 0 {
let working_directory = workspace
.update(cx, |workspace, cx| default_working_directory(workspace, cx))
.ok()
.flatten();
let kind = TerminalKind::Shell(working_directory);
let window = cx.window_handle();
let terminal = project
.update(cx, |project, cx| project.create_terminal(kind, window, cx))
.log_err()?;
let terminal = pane
.update(cx, |pane, cx| {
populate_pane_items(pane, new_items, active_item, cx);
// Avoid blank panes in splits
if pane.items_len() == 0 {
let working_directory = workspace
.update(cx, |workspace, cx| default_working_directory(workspace, cx))
.ok()
.flatten();
let kind = TerminalKind::Shell(
working_directory.as_deref().map(Path::to_path_buf),
);
let window = cx.window_handle();
let terminal = project
.update(cx, |project, cx| project.create_terminal(kind, window, cx));
Some(Some(terminal))
} else {
Some(None)
}
})
.ok()
.flatten()?;
if let Some(terminal) = terminal {
let terminal = terminal.await.ok()?;
pane.update(cx, |pane, cx| {
let terminal_view = Box::new(cx.new_view(|cx| {
TerminalView::new(
terminal.clone(),
workspace.clone(),
Some(workspace_id),
cx,
)
TerminalView::new(terminal, workspace.clone(), Some(workspace_id), cx)
}));
pane.add_item(terminal_view, true, false, None, cx);
}
Some(())
})
.ok()
.flatten()?;
})
.ok()?;
}
Some((Member::Pane(pane.clone()), active.then_some(pane)))
}
}

View File

@@ -318,10 +318,19 @@ impl TerminalPanel {
}
}
pane::Event::Split(direction) => {
let Some(new_pane) = self.new_pane_with_cloned_active_terminal(cx) else {
return;
};
self.center.split(&pane, &new_pane, *direction).log_err();
let new_pane = self.new_pane_with_cloned_active_terminal(cx);
let pane = pane.clone();
let direction = *direction;
cx.spawn(move |this, mut cx| async move {
let Some(new_pane) = new_pane.await else {
return;
};
this.update(&mut cx, |this, _| {
this.center.split(&pane, &new_pane, direction).log_err();
})
.ok();
})
.detach();
}
pane::Event::Focus => {
self.active_pane = pane.clone();
@@ -334,8 +343,12 @@ impl TerminalPanel {
fn new_pane_with_cloned_active_terminal(
&mut self,
cx: &mut ViewContext<Self>,
) -> Option<View<Pane>> {
let workspace = self.workspace.clone().upgrade()?;
) -> Task<Option<View<Pane>>> {
let Some(workspace) = self.workspace.clone().upgrade() else {
return Task::ready(None);
};
let database_id = workspace.read(cx).database_id();
let weak_workspace = self.workspace.clone();
let project = workspace.read(cx).project().clone();
let working_directory = self
.active_pane
@@ -352,21 +365,37 @@ impl TerminalPanel {
.or_else(|| default_working_directory(workspace.read(cx), cx));
let kind = TerminalKind::Shell(working_directory);
let window = cx.window_handle();
let terminal = project
.update(cx, |project, cx| project.create_terminal(kind, window, cx))
.log_err()?;
let database_id = workspace.read(cx).database_id();
let terminal_view = Box::new(cx.new_view(|cx| {
TerminalView::new(terminal.clone(), self.workspace.clone(), database_id, cx)
}));
let pane = new_terminal_pane(self.workspace.clone(), project, cx);
self.apply_tab_bar_buttons(&pane, cx);
pane.update(cx, |pane, cx| {
pane.add_item(terminal_view, true, true, None, cx);
});
cx.focus_view(&pane);
cx.spawn(move |this, mut cx| async move {
let terminal = project
.update(&mut cx, |project, cx| {
project.create_terminal(kind, window, cx)
})
.log_err()?
.await
.log_err()?;
Some(pane)
let terminal_view = Box::new(
cx.new_view(|cx| {
TerminalView::new(terminal.clone(), weak_workspace.clone(), database_id, cx)
})
.ok()?,
);
let pane = this
.update(&mut cx, |this, cx| {
let pane = new_terminal_pane(weak_workspace, project, cx);
this.apply_tab_bar_buttons(&pane, cx);
pane
})
.ok()?;
pane.update(&mut cx, |pane, cx| {
pane.add_item(terminal_view, true, true, None, cx);
})
.ok()?;
cx.focus_view(&pane).ok()?;
Some(pane)
})
}
pub fn open_terminal(
@@ -489,43 +518,58 @@ impl TerminalPanel {
.last()
.expect("covered no terminals case above")
.clone();
if allow_concurrent_runs {
debug_assert!(
!use_new_terminal,
"Should have handled 'allow_concurrent_runs && use_new_terminal' case above"
);
self.replace_terminal(
spawn_task,
task_pane,
existing_item_index,
existing_terminal,
cx,
);
} else {
self.deferred_tasks.insert(
spawn_in_terminal.id.clone(),
cx.spawn(|terminal_panel, mut cx| async move {
wait_for_terminals_tasks(terminals_for_task, &mut cx).await;
terminal_panel
.update(&mut cx, |terminal_panel, cx| {
if use_new_terminal {
terminal_panel
.spawn_in_new_terminal(spawn_task, cx)
.detach_and_log_err(cx);
} else {
terminal_panel.replace_terminal(
spawn_task,
task_pane,
existing_item_index,
existing_terminal,
cx,
);
}
})
.ok();
}),
);
}
let id = spawn_in_terminal.id.clone();
cx.spawn(move |this, mut cx| async move {
if allow_concurrent_runs {
debug_assert!(
!use_new_terminal,
"Should have handled 'allow_concurrent_runs && use_new_terminal' case above"
);
this.update(&mut cx, |this, cx| {
this.replace_terminal(
spawn_task,
task_pane,
existing_item_index,
existing_terminal,
cx,
)
})?
.await;
} else {
this.update(&mut cx, |this, cx| {
this.deferred_tasks.insert(
id,
cx.spawn(|terminal_panel, mut cx| async move {
wait_for_terminals_tasks(terminals_for_task, &mut cx).await;
let Ok(Some(new_terminal_task)) =
terminal_panel.update(&mut cx, |terminal_panel, cx| {
if use_new_terminal {
terminal_panel
.spawn_in_new_terminal(spawn_task, cx)
.detach_and_log_err(cx);
None
} else {
Some(terminal_panel.replace_terminal(
spawn_task,
task_pane,
existing_item_index,
existing_terminal,
cx,
))
}
})
else {
return;
};
new_terminal_task.await;
}),
);
})
.ok();
}
anyhow::Result::<_, anyhow::Error>::Ok(())
})
.detach()
}
pub fn spawn_in_new_terminal(
@@ -611,11 +655,14 @@ impl TerminalPanel {
cx.spawn(|terminal_panel, mut cx| async move {
let pane = terminal_panel.update(&mut cx, |this, _| this.active_pane.clone())?;
let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
let window = cx.window_handle();
let terminal = project
.update(&mut cx, |project, cx| {
project.create_terminal(kind, window, cx)
})?
.await?;
let result = workspace.update(&mut cx, |workspace, cx| {
let window = cx.window_handle();
let terminal = workspace
.project()
.update(cx, |project, cx| project.create_terminal(kind, window, cx))?;
let terminal_view = Box::new(cx.new_view(|cx| {
TerminalView::new(
terminal.clone(),
@@ -695,48 +742,64 @@ impl TerminalPanel {
terminal_item_index: usize,
terminal_to_replace: View<TerminalView>,
cx: &mut ViewContext<'_, Self>,
) -> Option<()> {
let project = self
.workspace
.update(cx, |workspace, _| workspace.project().clone())
.ok()?;
) -> Task<Option<()>> {
let reveal = spawn_task.reveal;
let window = cx.window_handle();
let new_terminal = project.update(cx, |project, cx| {
project
.create_terminal(TerminalKind::Task(spawn_task), window, cx)
.log_err()
})?;
terminal_to_replace.update(cx, |terminal_to_replace, cx| {
terminal_to_replace.set_terminal(new_terminal, cx);
});
match reveal {
RevealStrategy::Always => {
self.activate_terminal_view(&task_pane, terminal_item_index, true, cx);
let task_workspace = self.workspace.clone();
cx.spawn(|_, mut cx| async move {
task_workspace
.update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
let task_workspace = self.workspace.clone();
cx.spawn(move |this, mut cx| async move {
let project = this
.update(&mut cx, |this, cx| {
this.workspace
.update(cx, |workspace, _| workspace.project().clone())
.ok()
})
.detach();
}
RevealStrategy::NoFocus => {
self.activate_terminal_view(&task_pane, terminal_item_index, false, cx);
let task_workspace = self.workspace.clone();
cx.spawn(|_, mut cx| async move {
task_workspace
.update(&mut cx, |workspace, cx| workspace.open_panel::<Self>(cx))
.ok()
.ok()
.flatten()?;
let new_terminal = project
.update(&mut cx, |project, cx| {
project.create_terminal(TerminalKind::Task(spawn_task), window, cx)
})
.detach();
}
RevealStrategy::Never => {}
}
.ok()?
.await
.log_err()?;
terminal_to_replace
.update(&mut cx, |terminal_to_replace, cx| {
terminal_to_replace.set_terminal(new_terminal, cx);
})
.ok()?;
Some(())
match reveal {
RevealStrategy::Always => {
this.update(&mut cx, |this, cx| {
this.activate_terminal_view(&task_pane, terminal_item_index, true, cx)
})
.ok()?;
cx.spawn(|mut cx| async move {
task_workspace
.update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
.ok()
})
.detach();
}
RevealStrategy::NoFocus => {
this.update(&mut cx, |this, cx| {
this.activate_terminal_view(&task_pane, terminal_item_index, false, cx)
})
.ok()?;
cx.spawn(|mut cx| async move {
task_workspace
.update(&mut cx, |workspace, cx| workspace.open_panel::<Self>(cx))
.ok()
})
.detach();
}
RevealStrategy::Never => {}
}
Some(())
})
}
fn has_no_terminals(&self, cx: &WindowContext) -> bool {
@@ -998,18 +1061,18 @@ impl Render for TerminalPanel {
if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
cx.focus_view(&pane);
} else {
if let Some(new_pane) =
terminal_panel.new_pane_with_cloned_active_terminal(cx)
{
terminal_panel
.center
.split(
&terminal_panel.active_pane,
&new_pane,
SplitDirection::Right,
)
.log_err();
}
let new_pane = terminal_panel.new_pane_with_cloned_active_terminal(cx);
cx.spawn(|this, mut cx| async move {
if let Some(new_pane) = new_pane.await {
this.update(&mut cx, |this, _| {
this.center
.split(&this.active_pane, &new_pane, SplitDirection::Right)
.log_err();
})
.ok();
}
})
.detach();
}
}))
.on_action(cx.listener(

View File

@@ -136,24 +136,36 @@ impl TerminalView {
let working_directory = default_working_directory(workspace, cx);
let window = cx.window_handle();
let terminal = workspace
.project()
.update(cx, |project, cx| {
project.create_terminal(TerminalKind::Shell(working_directory), window, cx)
})
.notify_err(workspace, cx);
let project = workspace.project().downgrade();
cx.spawn(move |workspace, mut cx| async move {
let terminal = project
.update(&mut cx, |project, cx| {
project.create_terminal(TerminalKind::Shell(working_directory), window, cx)
})
.ok()?
.await;
let terminal = workspace
.update(&mut cx, |workspace, cx| terminal.notify_err(workspace, cx))
.ok()
.flatten()?;
if let Some(terminal) = terminal {
let view = cx.new_view(|cx| {
TerminalView::new(
terminal,
workspace.weak_handle(),
workspace.database_id(),
cx,
)
});
workspace.add_item_to_active_pane(Box::new(view), None, true, cx);
}
workspace
.update(&mut cx, |workspace, cx| {
let view = cx.new_view(|cx| {
TerminalView::new(
terminal,
workspace.weak_handle(),
workspace.database_id(),
cx,
)
});
workspace.add_item_to_active_pane(Box::new(view), None, true, cx);
})
.ok();
Some(())
})
.detach()
}
pub fn new(
@@ -1231,9 +1243,11 @@ impl SerializableItem for TerminalView {
.ok()
.flatten();
let terminal = project.update(&mut cx, |project, cx| {
project.create_terminal(TerminalKind::Shell(cwd), window, cx)
})??;
let terminal = project
.update(&mut cx, |project, cx| {
project.create_terminal(TerminalKind::Shell(cwd), window, cx)
})?
.await?;
cx.update(|cx| {
cx.new_view(|cx| TerminalView::new(terminal, workspace, Some(workspace_id), cx))
})
@@ -1362,11 +1376,14 @@ impl SearchableItem for TerminalView {
///Gets the working directory for the given workspace, respecting the user's settings.
/// None implies "~" on whichever machine we end up on.
pub fn default_working_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
pub(crate) fn default_working_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
match &TerminalSettings::get_global(cx).working_directory {
WorkingDirectory::CurrentProjectDirectory => {
workspace.project().read(cx).active_project_directory(cx)
}
WorkingDirectory::CurrentProjectDirectory => workspace
.project()
.read(cx)
.active_project_directory(cx)
.as_deref()
.map(Path::to_path_buf),
WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
WorkingDirectory::AlwaysHome => None,
WorkingDirectory::Always { directory } => {

View File

@@ -84,6 +84,31 @@ impl<T: Copy + Ord> Selection<T> {
}
self.goal = new_goal;
}
pub fn set_tail(&mut self, tail: T, new_goal: SelectionGoal) {
if tail.cmp(&self.head()) <= Ordering::Equal {
if self.reversed {
self.end = self.start;
self.reversed = false;
}
self.start = tail;
} else {
if !self.reversed {
self.start = self.end;
self.reversed = true;
}
self.end = tail;
}
self.goal = new_goal;
}
pub fn swap_head_tail(&mut self) {
if self.reversed {
self.reversed = false;
} else {
std::mem::swap(&mut self.start, &mut self.end);
}
}
}
impl<T: Copy> Selection<T> {

View File

@@ -19,6 +19,8 @@ use theme::{Appearance, AppearanceContent, ThemeFamilyContent};
use crate::vscode::VsCodeTheme;
use crate::vscode::VsCodeThemeConverter;
const ZED_THEME_SCHEMA_URL: &str = "https://zed.dev/public/schema/themes/v0.2.0.json";
#[derive(Debug, Deserialize)]
struct FamilyMetadata {
pub name: String,
@@ -69,34 +71,53 @@ pub struct ThemeMetadata {
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Args {
/// The path to the theme to import.
theme_path: PathBuf,
/// Whether to warn when values are missing from the theme.
#[arg(long)]
warn_on_missing: bool,
/// The path to write the output to.
#[arg(long, short)]
output: Option<PathBuf>,
#[command(subcommand)]
command: Option<Command>,
command: Command,
}
#[derive(Subcommand)]
#[derive(PartialEq, Subcommand)]
enum Command {
/// Prints the JSON schema for a theme.
PrintSchema,
/// Converts a VSCode theme to Zed format [default]
Convert {
/// The path to the theme to import.
theme_path: PathBuf,
/// Whether to warn when values are missing from the theme.
#[arg(long)]
warn_on_missing: bool,
/// The path to write the output to.
#[arg(long, short)]
output: Option<PathBuf>,
},
}
fn main() -> Result<()> {
let args = Args::parse();
match args.command {
Command::PrintSchema => {
let theme_family_schema = schema_for!(ThemeFamilyContent);
println!(
"{}",
serde_json::to_string_pretty(&theme_family_schema).unwrap()
);
Ok(())
}
Command::Convert {
theme_path,
warn_on_missing,
output,
} => convert(theme_path, output, warn_on_missing),
}
}
fn convert(theme_file_path: PathBuf, output: Option<PathBuf>, warn_on_missing: bool) -> Result<()> {
let log_config = {
let mut config = simplelog::ConfigBuilder::new();
if !args.warn_on_missing {
if !warn_on_missing {
config.add_filter_ignore_str("theme_printer");
}
@@ -111,28 +132,11 @@ fn main() -> Result<()> {
)
.expect("could not initialize logger");
if let Some(command) = args.command {
match command {
Command::PrintSchema => {
let theme_family_schema = schema_for!(ThemeFamilyContent);
println!(
"{}",
serde_json::to_string_pretty(&theme_family_schema).unwrap()
);
return Ok(());
}
}
}
let theme_file_path = args.theme_path;
let theme_file = match File::open(&theme_file_path) {
Ok(file) => file,
Err(err) => {
log::info!("Failed to open file at path: {:?}", theme_file_path);
return Err(err)?;
return Err(err.into());
}
};
@@ -148,10 +152,14 @@ fn main() -> Result<()> {
let converter = VsCodeThemeConverter::new(vscode_theme, theme_metadata, IndexMap::new());
let theme = converter.convert()?;
let mut theme = serde_json::to_value(theme).unwrap();
theme.as_object_mut().unwrap().insert(
"$schema".to_string(),
serde_json::Value::String(ZED_THEME_SCHEMA_URL.to_string()),
);
let theme_json = serde_json::to_string_pretty(&theme).unwrap();
if let Some(output) = args.output {
if let Some(output) = output {
let mut file = File::create(output)?;
file.write_all(theme_json.as_bytes())?;
} else {

View File

@@ -26,21 +26,21 @@ Now let's dive a bit deeper into how to customize `Label` instances:
- **Setting Color:** To set the color of the label using various predefined color options such as `Default`, `Muted`, `Created`, `Modified`, `Deleted`, etc, the `color()` function is called on the `Label` instance:
```rust
Label::new("Hello, world!").color(LabelColor::Default);
```
```rust
Label::new("Hello, world!").color(LabelColor::Default);
```
- **Setting Line Height Style:** To set the line height style, the `line_height_style()` function is utilized:
```rust
Label::new("Hello, world!").line_height_style(LineHeightStyle::TextLabel);
```
```rust
Label::new("Hello, world!").line_height_style(LineHeightStyle::TextLabel);
```
- **Adding a Strikethrough:** To add a strikethrough in a `Label`, the `set_strikethrough()` function is used:
- **Adding a Strikethrough:** To add a strikethrough in a `Label`, the `set_strikethrough()` function is used:
```rust
Label::new("Hello, world!").set_strikethrough(true);
```
```rust
Label::new("Hello, world!").set_strikethrough(true);
```
That's it! Now you can use the `Label` component to create and customize text on your application's interface.

View File

@@ -6,7 +6,7 @@ Let's work through the prototypical "Build a todo app" example to showcase how w
We'll create a headline, a list of todo items, and a form to add new items.
```rust
~~~rust
struct TodoList<V: 'static> {
headline: SharedString,
items: Vec<TodoItem>,
@@ -36,7 +36,7 @@ impl<V: 'static> TodoList<V> {
}
}
}
```
~~~
All of this is relatively straightforward.
@@ -44,7 +44,7 @@ We use [gpui::SharedString] in components instead of [std::string::String]. This
When we want to pass an action we pass a `ClickHandler`. Whenever we want to add an action, the struct it belongs to needs to be generic over the view type `V`.
```rust
~~~rust
use gpui::hsla
impl<V: 'static> TodoList<V> {
@@ -53,7 +53,7 @@ impl<V: 'static> TodoList<V> {
div().size_4().bg(hsla(50.0/360.0, 1.0, 0.5, 1.0))
}
}
```
~~~
Every component needs a render method, and it should return `impl Element<V>`. This basic component will render a 16x16px yellow square on the screen.
@@ -84,7 +84,7 @@ Let's grab our [theme::colors::ThemeColors] from the theme and start building ou
We can access the current theme's colors like this:
```rust
~~~rust
impl<V: 'static> TodoList<V> {
// ...
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
@@ -93,11 +93,11 @@ impl<V: 'static> TodoList<V> {
div().size_4().hsla(50.0/360.0, 1.0, 0.5, 1.0)
}
}
```
~~~
Now we have access to the complete set of colors defined in the theme.
```rust
~~~rust
use gpui::hsla
impl<V: 'static> TodoList<V> {
@@ -108,11 +108,11 @@ impl<V: 'static> TodoList<V> {
div().size_4().bg(color.surface)
}
}
```
~~~
Let's finish up some basic styles for the container then move on to adding the other elements.
```rust
~~~rust
use gpui::hsla
impl<V: 'static> TodoList<V> {
@@ -140,7 +140,7 @@ impl<V: 'static> TodoList<V> {
)
}
}
```
~~~
### Headline
@@ -154,6 +154,7 @@ TODO
TODO
### End result
TODO

View File

@@ -65,6 +65,11 @@ impl LabelCommon for HighlightedLabel {
self.base = self.base.underline(underline);
self
}
fn single_line(mut self) -> Self {
self.base = self.base.single_line();
self
}
}
pub fn highlight_ranges(

View File

@@ -56,20 +56,6 @@ impl Label {
single_line: false,
}
}
/// Make the label display in a single line mode
///
/// # Examples
///
/// ```
/// use ui::prelude::*;
///
/// let my_label = Label::new("Hello, World!").single_line();
/// ```
pub fn single_line(mut self) -> Self {
self.single_line = true;
self
}
}
// Style methods.
@@ -177,6 +163,12 @@ impl LabelCommon for Label {
self.base = self.base.underline(underline);
self
}
fn single_line(mut self) -> Self {
self.single_line = true;
self.base = self.base.single_line();
self
}
}
impl RenderOnce for Label {

View File

@@ -49,6 +49,9 @@ pub trait LabelCommon {
/// Sets the alpha property of the label, overwriting the alpha value of the color.
fn alpha(self, alpha: f32) -> Self;
/// Sets the label to render as a single line.
fn single_line(self) -> Self;
}
#[derive(IntoElement)]
@@ -63,6 +66,7 @@ pub struct LabelLike {
children: SmallVec<[AnyElement; 2]>,
alpha: Option<f32>,
underline: bool,
single_line: bool,
}
impl Default for LabelLike {
@@ -84,6 +88,7 @@ impl LabelLike {
children: SmallVec::new(),
alpha: None,
underline: false,
single_line: false,
}
}
}
@@ -139,6 +144,11 @@ impl LabelCommon for LabelLike {
self.alpha = Some(alpha);
self
}
fn single_line(mut self) -> Self {
self.single_line = true;
self
}
}
impl ParentElement for LabelLike {
@@ -178,6 +188,7 @@ impl RenderOnce for LabelLike {
this
})
.when(self.strikethrough, |this| this.line_through())
.when(self.single_line, |this| this.whitespace_nowrap())
.text_color(color)
.font_weight(self.weight.unwrap_or(settings.ui_font.weight))
.children(self.children)

View File

@@ -30,6 +30,7 @@ cargo test -p vim --features neovim test_visual_star_hash
This will run your keystrokes against a headless neovim and cache the results in the test_data directory.
## Testing zed-only behavior
Zed does more than vim/neovim in their default modes. The `VimTestContext` can be used instead. This lets you test integration with the language server and other parts of zed's UI that don't have a NeoVim equivalent.

View File

@@ -9,7 +9,7 @@ use anyhow::{anyhow, Result};
use command_palette_hooks::CommandInterceptResult;
use editor::{
actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive},
Editor, ToPoint,
Editor, ToMultiBufferPoint,
};
use gpui::{actions, impl_actions, Action, AppContext, Global, ViewContext};
use language::Point;

271
crates/vim/src/helix.rs Normal file
View File

@@ -0,0 +1,271 @@
use editor::{movement, scroll::Autoscroll, DisplayPoint, Editor};
use gpui::{actions, Action};
use language::{CharClassifier, CharKind};
use ui::ViewContext;
use crate::{motion::Motion, state::Mode, Vim};
actions!(vim, [HelixNormalAfter]);
pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, Vim::helix_normal_after);
}
impl Vim {
pub fn helix_normal_after(&mut self, action: &HelixNormalAfter, cx: &mut ViewContext<Self>) {
if self.active_operator().is_some() {
self.operator_stack.clear();
self.sync_vim_settings(cx);
return;
}
self.stop_recording_immediately(action.boxed_clone(), cx);
self.switch_mode(Mode::HelixNormal, false, cx);
return;
}
pub fn helix_normal_motion(
&mut self,
motion: Motion,
times: Option<usize>,
cx: &mut ViewContext<Self>,
) {
self.helix_move_cursor(motion, times, cx);
}
fn helix_find_range_forward(
&mut self,
times: Option<usize>,
cx: &mut ViewContext<Self>,
mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool,
) {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
let times = times.unwrap_or(1);
if selection.head() == map.max_point() {
return;
}
// collapse to block cursor
if selection.tail() < selection.head() {
selection.set_tail(movement::left(map, selection.head()), selection.goal);
} else {
selection.set_tail(selection.head(), selection.goal);
selection.set_head(movement::right(map, selection.head()), selection.goal);
}
// create a classifier
let classifier = map
.buffer_snapshot
.char_classifier_at(selection.head().to_point(map));
let mut last_selection = selection.clone();
for _ in 0..times {
let (new_tail, new_head) =
movement::find_boundary_trail(map, selection.head(), |left, right| {
is_boundary(left, right, &classifier)
});
selection.set_head(new_head, selection.goal);
if let Some(new_tail) = new_tail {
selection.set_tail(new_tail, selection.goal);
}
if selection.head() == last_selection.head()
&& selection.tail() == last_selection.tail()
{
break;
}
last_selection = selection.clone();
}
});
});
});
}
fn helix_find_range_backward(
&mut self,
times: Option<usize>,
cx: &mut ViewContext<Self>,
mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool,
) {
self.update_editor(cx, |_, editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
let times = times.unwrap_or(1);
if selection.head() == DisplayPoint::zero() {
return;
}
// collapse to block cursor
if selection.tail() < selection.head() {
selection.set_tail(movement::left(map, selection.head()), selection.goal);
} else {
selection.set_tail(selection.head(), selection.goal);
selection.set_head(movement::right(map, selection.head()), selection.goal);
}
// flip the selection
selection.swap_head_tail();
// create a classifier
let classifier = map
.buffer_snapshot
.char_classifier_at(selection.head().to_point(map));
let mut last_selection = selection.clone();
for _ in 0..times {
let (new_tail, new_head) = movement::find_preceding_boundary_trail(
map,
selection.head(),
|left, right| is_boundary(left, right, &classifier),
);
selection.set_head(new_head, selection.goal);
if let Some(new_tail) = new_tail {
selection.set_tail(new_tail, selection.goal);
}
if selection.head() == last_selection.head()
&& selection.tail() == last_selection.tail()
{
break;
}
last_selection = selection.clone();
}
});
})
});
}
pub fn helix_move_and_collapse(
&mut self,
motion: Motion,
times: Option<usize>,
cx: &mut ViewContext<Self>,
) {
self.update_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
let goal = selection.goal;
let cursor = if selection.is_empty() || selection.reversed {
selection.head()
} else {
movement::left(map, selection.head())
};
let (point, goal) = motion
.move_point(map, cursor, selection.goal, times, &text_layout_details)
.unwrap_or((cursor, goal));
selection.collapse_to(point, goal)
})
});
});
}
pub fn helix_move_cursor(
&mut self,
motion: Motion,
times: Option<usize>,
cx: &mut ViewContext<Self>,
) {
match motion {
Motion::NextWordStart { ignore_punctuation } => {
self.helix_find_range_forward(times, cx, |left, right, classifier| {
let left_kind = classifier.kind_with(left, ignore_punctuation);
let right_kind = classifier.kind_with(right, ignore_punctuation);
let at_newline = right == '\n';
let found =
left_kind != right_kind && right_kind != CharKind::Whitespace || at_newline;
found
})
}
Motion::NextWordEnd { ignore_punctuation } => {
self.helix_find_range_forward(times, cx, |left, right, classifier| {
let left_kind = classifier.kind_with(left, ignore_punctuation);
let right_kind = classifier.kind_with(right, ignore_punctuation);
let at_newline = right == '\n';
let found = left_kind != right_kind
&& (left_kind != CharKind::Whitespace || at_newline);
found
})
}
Motion::PreviousWordStart { ignore_punctuation } => {
self.helix_find_range_backward(times, cx, |left, right, classifier| {
let left_kind = classifier.kind_with(left, ignore_punctuation);
let right_kind = classifier.kind_with(right, ignore_punctuation);
let at_newline = right == '\n';
let found = left_kind != right_kind
&& (left_kind != CharKind::Whitespace || at_newline);
found
})
}
Motion::PreviousWordEnd { ignore_punctuation } => {
self.helix_find_range_backward(times, cx, |left, right, classifier| {
let left_kind = classifier.kind_with(left, ignore_punctuation);
let right_kind = classifier.kind_with(right, ignore_punctuation);
let at_newline = right == '\n';
let found = left_kind != right_kind
&& right_kind != CharKind::Whitespace
&& !at_newline;
found
})
}
_ => self.helix_move_and_collapse(motion, times, cx),
}
}
}
#[cfg(test)]
mod test {
use indoc::indoc;
use crate::{state::Mode, test::VimTestContext};
#[gpui::test]
async fn test_next_word_start(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
// «
// ˇ
// »
cx.set_state(
indoc! {"
The quˇick brown
fox jumps over
the lazy dog."},
Mode::HelixNormal,
);
cx.simulate_keystrokes("w");
cx.assert_state(
indoc! {"
The qu«ick ˇ»brown
fox jumps over
the lazy dog."},
Mode::HelixNormal,
);
cx.simulate_keystrokes("w");
cx.assert_state(
indoc! {"
The quick «brownˇ»
fox jumps over
the lazy dog."},
Mode::HelixNormal,
);
}
}

View File

@@ -9,9 +9,10 @@ use ui::ViewContext;
pub(crate) enum IndentDirection {
In,
Out,
Auto,
}
actions!(vim, [Indent, Outdent,]);
actions!(vim, [Indent, Outdent, AutoIndent]);
pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, _: &Indent, cx| {
@@ -49,6 +50,24 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
vim.switch_mode(Mode::Normal, true, cx)
}
});
Vim::action(editor, cx, |vim, _: &AutoIndent, cx| {
vim.record_current_action(cx);
let count = Vim::take_count(cx).unwrap_or(1);
vim.store_visual_marks(cx);
vim.update_editor(cx, |vim, editor, cx| {
editor.transact(cx, |editor, cx| {
let original_positions = vim.save_selection_starts(editor, cx);
for _ in 0..count {
editor.autoindent(&Default::default(), cx);
}
vim.restore_selection_cursors(editor, cx, original_positions);
});
});
if vim.mode.is_visual() {
vim.switch_mode(Mode::Normal, true, cx)
}
});
}
impl Vim {
@@ -71,10 +90,10 @@ impl Vim {
motion.expand_selection(map, selection, times, false, &text_layout_details);
});
});
if dir == IndentDirection::In {
editor.indent(&Default::default(), cx);
} else {
editor.outdent(&Default::default(), cx);
match dir {
IndentDirection::In => editor.indent(&Default::default(), cx),
IndentDirection::Out => editor.outdent(&Default::default(), cx),
IndentDirection::Auto => editor.autoindent(&Default::default(), cx),
}
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
@@ -104,10 +123,10 @@ impl Vim {
object.expand_selection(map, selection, around);
});
});
if dir == IndentDirection::In {
editor.indent(&Default::default(), cx);
} else {
editor.outdent(&Default::default(), cx);
match dir {
IndentDirection::In => editor.indent(&Default::default(), cx),
IndentDirection::Out => editor.outdent(&Default::default(), cx),
IndentDirection::Auto => editor.autoindent(&Default::default(), cx),
}
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
@@ -122,7 +141,11 @@ impl Vim {
#[cfg(test)]
mod test {
use crate::test::NeovimBackedTestContext;
use crate::{
state::Mode,
test::{NeovimBackedTestContext, VimTestContext},
};
use indoc::indoc;
#[gpui::test]
async fn test_indent_gv(cx: &mut gpui::TestAppContext) {
@@ -135,4 +158,46 @@ mod test {
.await
.assert_eq("« hello\n ˇ» world\n");
}
#[gpui::test]
async fn test_autoindent_op(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.set_state(
indoc!(
"
fn a() {
b();
c();
d();
ˇe();
f();
g();
}
"
),
Mode::Normal,
);
cx.simulate_keystrokes("= a p");
cx.assert_state(
indoc!(
"
fn a() {
b();
c();
d();
ˇe();
f();
g();
}
"
),
Mode::Normal,
);
}
}

View File

@@ -11,6 +11,7 @@ use language::{CharKind, Point, Selection, SelectionGoal};
use multi_buffer::MultiBufferRow;
use serde::Deserialize;
use std::ops::Range;
use workspace::searchable::Direction;
use crate::{
normal::mark,
@@ -104,6 +105,16 @@ pub enum Motion {
WindowTop,
WindowMiddle,
WindowBottom,
NextSectionStart,
NextSectionEnd,
PreviousSectionStart,
PreviousSectionEnd,
NextMethodStart,
NextMethodEnd,
PreviousMethodStart,
PreviousMethodEnd,
NextComment,
PreviousComment,
// we don't have a good way to run a search synchronously, so
// we handle search motions by running the search async and then
@@ -269,6 +280,16 @@ actions!(
WindowTop,
WindowMiddle,
WindowBottom,
NextSectionStart,
NextSectionEnd,
PreviousSectionStart,
PreviousSectionEnd,
NextMethodStart,
NextMethodEnd,
PreviousMethodStart,
PreviousMethodEnd,
NextComment,
PreviousComment,
]
);
@@ -454,6 +475,37 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, &WindowBottom, cx| {
vim.motion(Motion::WindowBottom, cx)
});
Vim::action(editor, cx, |vim, &PreviousSectionStart, cx| {
vim.motion(Motion::PreviousSectionStart, cx)
});
Vim::action(editor, cx, |vim, &NextSectionStart, cx| {
vim.motion(Motion::NextSectionStart, cx)
});
Vim::action(editor, cx, |vim, &PreviousSectionEnd, cx| {
vim.motion(Motion::PreviousSectionEnd, cx)
});
Vim::action(editor, cx, |vim, &NextSectionEnd, cx| {
vim.motion(Motion::NextSectionEnd, cx)
});
Vim::action(editor, cx, |vim, &PreviousMethodStart, cx| {
vim.motion(Motion::PreviousMethodStart, cx)
});
Vim::action(editor, cx, |vim, &NextMethodStart, cx| {
vim.motion(Motion::NextMethodStart, cx)
});
Vim::action(editor, cx, |vim, &PreviousMethodEnd, cx| {
vim.motion(Motion::PreviousMethodEnd, cx)
});
Vim::action(editor, cx, |vim, &NextMethodEnd, cx| {
vim.motion(Motion::NextMethodEnd, cx)
});
Vim::action(editor, cx, |vim, &NextComment, cx| {
vim.motion(Motion::NextComment, cx)
});
Vim::action(editor, cx, |vim, &PreviousComment, cx| {
vim.motion(Motion::PreviousComment, cx)
});
}
impl Vim {
@@ -477,6 +529,8 @@ impl Vim {
return;
}
}
Mode::HelixNormal => {}
}
}
@@ -506,6 +560,8 @@ impl Vim {
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
self.visual_motion(motion.clone(), count, cx)
}
Mode::HelixNormal => self.helix_normal_motion(motion.clone(), count, cx),
}
self.clear_operator(cx);
if let Some(operator) = waiting_operator {
@@ -536,6 +592,16 @@ impl Motion {
| WindowTop
| WindowMiddle
| WindowBottom
| NextSectionStart
| NextSectionEnd
| PreviousSectionStart
| PreviousSectionEnd
| NextMethodStart
| NextMethodEnd
| PreviousMethodStart
| PreviousMethodEnd
| NextComment
| PreviousComment
| Jump { line: true, .. } => true,
EndOfLine { .. }
| Matching
@@ -607,6 +673,16 @@ impl Motion {
| NextLineStart
| PreviousLineStart
| ZedSearchResult { .. }
| NextSectionStart
| NextSectionEnd
| PreviousSectionStart
| PreviousSectionEnd
| NextMethodStart
| NextMethodEnd
| PreviousMethodStart
| PreviousMethodEnd
| NextComment
| PreviousComment
| Jump { .. } => false,
}
}
@@ -652,6 +728,16 @@ impl Motion {
| FirstNonWhitespace { .. }
| FindBackward { .. }
| Jump { .. }
| NextSectionStart
| NextSectionEnd
| PreviousSectionStart
| PreviousSectionEnd
| NextMethodStart
| NextMethodEnd
| PreviousMethodStart
| PreviousMethodEnd
| NextComment
| PreviousComment
| ZedSearchResult { .. } => false,
RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
motion.inclusive()
@@ -867,6 +953,47 @@ impl Motion {
return None;
}
}
NextSectionStart => (
section_motion(map, point, times, Direction::Next, true),
SelectionGoal::None,
),
NextSectionEnd => (
section_motion(map, point, times, Direction::Next, false),
SelectionGoal::None,
),
PreviousSectionStart => (
section_motion(map, point, times, Direction::Prev, true),
SelectionGoal::None,
),
PreviousSectionEnd => (
section_motion(map, point, times, Direction::Prev, false),
SelectionGoal::None,
),
NextMethodStart => (
method_motion(map, point, times, Direction::Next, true),
SelectionGoal::None,
),
NextMethodEnd => (
method_motion(map, point, times, Direction::Next, false),
SelectionGoal::None,
),
PreviousMethodStart => (
method_motion(map, point, times, Direction::Prev, true),
SelectionGoal::None,
),
PreviousMethodEnd => (
method_motion(map, point, times, Direction::Prev, false),
SelectionGoal::None,
),
NextComment => (
comment_motion(map, point, times, Direction::Next),
SelectionGoal::None,
),
PreviousComment => (
comment_motion(map, point, times, Direction::Prev),
SelectionGoal::None,
),
};
(new_point != point || infallible).then_some((new_point, goal))
@@ -2129,6 +2256,231 @@ fn window_bottom(
}
}
fn method_motion(
map: &DisplaySnapshot,
mut display_point: DisplayPoint,
times: usize,
direction: Direction,
is_start: bool,
) -> DisplayPoint {
let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
return display_point;
};
for _ in 0..times {
let point = map.display_point_to_point(display_point, Bias::Left);
let offset = point.to_offset(&map.buffer_snapshot);
let range = if direction == Direction::Prev {
0..offset
} else {
offset..buffer.len()
};
let possibilities = buffer
.text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
.filter_map(|(range, object)| {
if !matches!(object, language::TextObject::AroundFunction) {
return None;
}
let relevant = if is_start { range.start } else { range.end };
if direction == Direction::Prev && relevant < offset {
Some(relevant)
} else if direction == Direction::Next && relevant > offset + 1 {
Some(relevant)
} else {
None
}
});
let dest = if direction == Direction::Prev {
possibilities.max().unwrap_or(offset)
} else {
possibilities.min().unwrap_or(offset)
};
let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
if new_point == display_point {
break;
}
display_point = new_point;
}
display_point
}
fn comment_motion(
map: &DisplaySnapshot,
mut display_point: DisplayPoint,
times: usize,
direction: Direction,
) -> DisplayPoint {
let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
return display_point;
};
for _ in 0..times {
let point = map.display_point_to_point(display_point, Bias::Left);
let offset = point.to_offset(&map.buffer_snapshot);
let range = if direction == Direction::Prev {
0..offset
} else {
offset..buffer.len()
};
let possibilities = buffer
.text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
.filter_map(|(range, object)| {
if !matches!(object, language::TextObject::AroundComment) {
return None;
}
let relevant = if direction == Direction::Prev {
range.start
} else {
range.end
};
if direction == Direction::Prev && relevant < offset {
Some(relevant)
} else if direction == Direction::Next && relevant > offset + 1 {
Some(relevant)
} else {
None
}
});
let dest = if direction == Direction::Prev {
possibilities.max().unwrap_or(offset)
} else {
possibilities.min().unwrap_or(offset)
};
let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
if new_point == display_point {
break;
}
display_point = new_point;
}
display_point
}
fn section_motion(
map: &DisplaySnapshot,
mut display_point: DisplayPoint,
times: usize,
direction: Direction,
is_start: bool,
) -> DisplayPoint {
if let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() {
for _ in 0..times {
let offset = map
.display_point_to_point(display_point, Bias::Left)
.to_offset(&map.buffer_snapshot);
let range = if direction == Direction::Prev {
0..offset
} else {
offset..buffer.len()
};
// we set a max start depth here because we want a section to only be "top level"
// similar to vim's default of '{' in the first column.
// (and without it, ]] at the start of editor.rs is -very- slow)
let mut possibilities = buffer
.text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
.filter(|(_, object)| {
matches!(
object,
language::TextObject::AroundClass | language::TextObject::AroundFunction
)
})
.collect::<Vec<_>>();
possibilities.sort_by_key(|(range_a, _)| range_a.start);
let mut prev_end = None;
let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
if t == language::TextObject::AroundFunction
&& prev_end.is_some_and(|prev_end| prev_end > range.start)
{
return None;
}
prev_end = Some(range.end);
let relevant = if is_start { range.start } else { range.end };
if direction == Direction::Prev && relevant < offset {
Some(relevant)
} else if direction == Direction::Next && relevant > offset + 1 {
Some(relevant)
} else {
None
}
});
let offset = if direction == Direction::Prev {
possibilities.max().unwrap_or(0)
} else {
possibilities.min().unwrap_or(buffer.len())
};
let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
if new_point == display_point {
break;
}
display_point = new_point;
}
return display_point;
};
for _ in 0..times {
let point = map.display_point_to_point(display_point, Bias::Left);
let Some(excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
return display_point;
};
let next_point = match (direction, is_start) {
(Direction::Prev, true) => {
let mut start = excerpt.start_anchor().to_display_point(&map);
if start >= display_point && start.row() > DisplayRow(0) {
let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else {
return display_point;
};
start = excerpt.start_anchor().to_display_point(&map);
}
start
}
(Direction::Prev, false) => {
let mut start = excerpt.start_anchor().to_display_point(&map);
if start.row() > DisplayRow(0) {
*start.row_mut() -= 1;
}
map.clip_point(start, Bias::Left)
}
(Direction::Next, true) => {
let mut end = excerpt.end_anchor().to_display_point(&map);
*end.row_mut() += 1;
map.clip_point(end, Bias::Right)
}
(Direction::Next, false) => {
let mut end = excerpt.end_anchor().to_display_point(&map);
*end.column_mut() = 0;
if end <= display_point {
*end.row_mut() += 1;
let point_end = map.display_point_to_point(end, Bias::Right);
let Some(excerpt) =
map.buffer_snapshot.excerpt_containing(point_end..point_end)
else {
return display_point;
};
end = excerpt.end_anchor().to_display_point(&map);
*end.column_mut() = 0;
}
end
}
};
if next_point == display_point {
break;
}
display_point = next_point;
}
display_point
}
#[cfg(test)]
mod test {

View File

@@ -170,6 +170,9 @@ impl Vim {
Some(Operator::Indent) => self.indent_motion(motion, times, IndentDirection::In, cx),
Some(Operator::Rewrap) => self.rewrap_motion(motion, times, cx),
Some(Operator::Outdent) => self.indent_motion(motion, times, IndentDirection::Out, cx),
Some(Operator::AutoIndent) => {
self.indent_motion(motion, times, IndentDirection::Auto, cx)
}
Some(Operator::Lowercase) => {
self.change_case_motion(motion, times, CaseTarget::Lowercase, cx)
}
@@ -202,6 +205,9 @@ impl Vim {
Some(Operator::Outdent) => {
self.indent_object(object, around, IndentDirection::Out, cx)
}
Some(Operator::AutoIndent) => {
self.indent_object(object, around, IndentDirection::Auto, cx)
}
Some(Operator::Rewrap) => self.rewrap_object(object, around, cx),
Some(Operator::Lowercase) => {
self.change_case_object(object, around, CaseTarget::Lowercase, cx)

View File

@@ -145,6 +145,8 @@ impl Vim {
cursor_positions.push(selection.start..selection.start);
}
}
Mode::HelixNormal => {}
Mode::Insert | Mode::Normal | Mode::Replace => {
let start = selection.start;
let mut end = start;

View File

@@ -1,6 +1,6 @@
use std::ops::Range;
use editor::{scroll::Autoscroll, Editor, MultiBufferSnapshot, ToOffset, ToPoint};
use editor::{scroll::Autoscroll, Editor, MultiBufferSnapshot, ToMultiBufferPoint, ToOffset};
use gpui::{impl_actions, ViewContext};
use language::{Bias, Point};
use serde::Deserialize;

View File

@@ -1,6 +1,10 @@
use std::ops::Range;
use crate::{motion::right, state::Mode, Vim};
use crate::{
motion::right,
state::{Mode, Operator},
Vim,
};
use editor::{
display_map::{DisplaySnapshot, ToDisplayPoint},
movement::{self, FindRange},
@@ -10,7 +14,7 @@ use editor::{
use itertools::Itertools;
use gpui::{actions, impl_actions, ViewContext};
use language::{BufferSnapshot, CharKind, Point, Selection};
use language::{BufferSnapshot, CharKind, Point, Selection, TextObject, TreeSitterOptions};
use multi_buffer::MultiBufferRow;
use serde::Deserialize;
@@ -30,6 +34,9 @@ pub enum Object {
Argument,
IndentObj { include_below: bool },
Tag,
Method,
Class,
Comment,
}
#[derive(Clone, Deserialize, PartialEq)]
@@ -61,7 +68,10 @@ actions!(
CurlyBrackets,
AngleBrackets,
Argument,
Tag
Tag,
Method,
Class,
Comment
]
);
@@ -107,6 +117,18 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, _: &Argument, cx| {
vim.object(Object::Argument, cx)
});
Vim::action(editor, cx, |vim, _: &Method, cx| {
vim.object(Object::Method, cx)
});
Vim::action(editor, cx, |vim, _: &Class, cx| {
vim.object(Object::Class, cx)
});
Vim::action(editor, cx, |vim, _: &Comment, cx| {
if !matches!(vim.active_operator(), Some(Operator::Object { .. })) {
vim.push_operator(Operator::Object { around: true }, cx);
}
vim.object(Object::Comment, cx)
});
Vim::action(
editor,
cx,
@@ -121,7 +143,7 @@ impl Vim {
match self.mode {
Mode::Normal => self.normal_object(object, cx),
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => self.visual_object(object, cx),
Mode::Insert | Mode::Replace => {
Mode::Insert | Mode::Replace | Mode::HelixNormal => {
// Shouldn't execute a text object in insert mode. Ignoring
}
}
@@ -144,6 +166,9 @@ impl Object {
| Object::CurlyBrackets
| Object::SquareBrackets
| Object::Argument
| Object::Method
| Object::Class
| Object::Comment
| Object::IndentObj { .. } => true,
}
}
@@ -162,12 +187,15 @@ impl Object {
| Object::Parentheses
| Object::SquareBrackets
| Object::Tag
| Object::Method
| Object::Class
| Object::Comment
| Object::CurlyBrackets
| Object::AngleBrackets => true,
}
}
pub fn target_visual_mode(self, current_mode: Mode) -> Mode {
pub fn target_visual_mode(self, current_mode: Mode, around: bool) -> Mode {
match self {
Object::Word { .. }
| Object::Sentence
@@ -186,8 +214,16 @@ impl Object {
| Object::AngleBrackets
| Object::VerticalBars
| Object::Tag
| Object::Comment
| Object::Argument
| Object::IndentObj { .. } => Mode::Visual,
Object::Method | Object::Class => {
if around {
Mode::VisualLine
} else {
Mode::Visual
}
}
Object::Paragraph => Mode::VisualLine,
}
}
@@ -238,6 +274,33 @@ impl Object {
Object::AngleBrackets => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
}
Object::Method => text_object(
map,
relative_to,
if around {
TextObject::AroundFunction
} else {
TextObject::InsideFunction
},
),
Object::Comment => text_object(
map,
relative_to,
if around {
TextObject::AroundComment
} else {
TextObject::InsideComment
},
),
Object::Class => text_object(
map,
relative_to,
if around {
TextObject::AroundClass
} else {
TextObject::InsideClass
},
),
Object::Argument => argument(map, relative_to, around),
Object::IndentObj { include_below } => indent(map, relative_to, around, include_below),
}
@@ -441,6 +504,47 @@ fn around_next_word(
Some(start..end)
}
fn text_object(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
target: TextObject,
) -> Option<Range<DisplayPoint>> {
let snapshot = &map.buffer_snapshot;
let offset = relative_to.to_offset(map, Bias::Left);
let excerpt = snapshot.excerpt_containing(offset..offset)?;
let buffer = excerpt.buffer();
let mut matches: Vec<Range<usize>> = buffer
.text_object_ranges(offset..offset, TreeSitterOptions::default())
.filter_map(|(r, m)| if m == target { Some(r) } else { None })
.collect();
matches.sort_by_key(|r| (r.end - r.start));
if let Some(range) = matches.first() {
return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
}
let around = target.around()?;
let mut matches: Vec<Range<usize>> = buffer
.text_object_ranges(offset..offset, TreeSitterOptions::default())
.filter_map(|(r, m)| if m == around { Some(r) } else { None })
.collect();
matches.sort_by_key(|r| (r.end - r.start));
let around_range = matches.first()?;
let mut matches: Vec<Range<usize>> = buffer
.text_object_ranges(around_range.clone(), TreeSitterOptions::default())
.filter_map(|(r, m)| if m == target { Some(r) } else { None })
.collect();
matches.sort_by_key(|r| r.start);
if let Some(range) = matches.first() {
if !range.is_empty() {
return Some(range.start.to_display_point(map)..range.end.to_display_point(map));
}
}
return Some(around_range.start.to_display_point(map)..around_range.end.to_display_point(map));
}
fn argument(
map: &DisplaySnapshot,
relative_to: DisplayPoint,

View File

@@ -3,7 +3,7 @@ use crate::{
state::Mode,
Vim,
};
use editor::{display_map::ToDisplayPoint, Bias, Editor, ToPoint};
use editor::{display_map::ToDisplayPoint, Bias, Editor, ToMultiBufferPoint};
use gpui::{actions, ViewContext};
use language::Point;
use std::ops::Range;

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