Compare commits

..

26 Commits

Author SHA1 Message Date
Antonio Scandurra
02701b49f2 WIP 2024-10-31 15:53:24 +01:00
Antonio Scandurra
b338475855 WIP 2024-10-31 15:02:21 +01:00
Antonio Scandurra
a27369c42c WIP: start using replace blocks for patches 2024-10-31 15:02:06 +01:00
Antonio Scandurra
d4099eed3a Introduce a new selected field to BlockContext 2024-10-31 15:01:43 +01:00
Thorsten Ball
5b6401519b activity indicator: Reset formatting failure on click (#20029)
Release Notes:

- N/A
2024-10-31 14:33:36 +01:00
Thorsten Ball
293e080f03 tasks: Add editor: Spawn Nearest Task action (#19901)
This spawns the runnable task that that's closest to the cursor.

One thing missing right now is that it doesn't find tasks that are
attached to non-outline symbols, such as subtests in Go.

Release Notes:

- Added a new reveal option for tasks: `"no_focus"`. If used, the tasks
terminal panel will be opened and shown, but not focused.
- Added a new `editor: spawn nearest task` action that spawns the task
with a run indicator icon nearest to the cursor. It can be configured to
also use a `reveal` strategy. Example:
```json
{
  "context": "EmptyPane || SharedScreen || vim_mode == normal",
  "bindings": {
    ", r t": ["editor::SpawnNearestTask", { "reveal": "no_focus" }],
  }
}
```


Demo:



https://github.com/user-attachments/assets/0d1818f0-7ae4-4200-8c3e-0ed47550c298

---------

Co-authored-by: Bennet <bennet@zed.dev>
2024-10-31 14:25:57 +01:00
Auf keinen Fall Jens
633b665379 Option to insert comment character(s) at the beginning of the line(s) (#19746)
Closes #19459


This PR adds the optional setting to insert comment character(s) at the
beginning of the line(s) instead of after the indentation. It can be
enabled via keybindings:

```
"ctrl-/": ["editor::ToggleComments", { "ignore_indent": true }]
```

As suggested by @notpeter in #19459, this is implemented in
`toggle_comments` (editor.rs) taking the existing `advance_downwards`
option as example.

There's also a test case for the setting, which mimics the test case for
the regular comment toggling behavior.

---

I am not entirely happy with the name `ignore_indent`. The default would
be a double negative now `ignore_indent=false`. A positive wording would
probably easier to understand, but I could not think of anything
concise. `insert_at_line_start` or just `at_line_start` might work, but
didn't convince me either. That said, I am happy to change the name if
there are better ideas.

---

Release Notes:

- Added optional setting to insert comment character(s) at the beginning
of the line(s) instead of after the indentation. It can be used by
changing the default mapping to toggle comments like this: `"ctrl-/":
["editor::ToggleComments", { "ignore_indent": true }]`
2024-10-31 09:39:57 +01:00
Thorsten Ball
7fd334fddb proto: Remove unused UpdateUserSettings message (#20005)
Release Notes:

- N/A
2024-10-31 09:36:18 +01:00
Thorsten Ball
10226a3992 docs: Document inline blame options (#20006)
Release Notes:

- N/A
2024-10-31 09:36:05 +01:00
Peter Tripp
383e868af0 docs: SSH no longer requires Zed Preview (#20003) 2024-10-30 23:28:13 -04:00
Conrad Irwin
40802d91d4 SSH installation refactor (#19991)
This also cleans up logic for deciding how to do things.

Release Notes:

- Remoting: If downloading the binary on the remote fails, fall back to
uploading it.

---------

Co-authored-by: Mikayala <mikayla@zed.dev>
2024-10-30 16:20:11 -07:00
Danilo Leal
6d5784daa6 Adjust design of the slash command picker (#19973)
This PR removes the quote selection icon button from the footer and adds
it in the picker, and adds an icon field to each command entry. Final
result looks like:


https://github.com/user-attachments/assets/d177f1c1-b6f6-4652-9434-f6291b279e34

Release Notes:

- N/A
2024-10-30 19:42:42 -03:00
Conrad Irwin
f80eb264fb Robustify download on remote (#19983)
Closes #19976
Closes #19972

We now prefer curl to wget (as it supports socks5:// proxies) and pass
-f to
curl so it fails; and use sh instead of bash, which should have more
consistent
behaviour across systems

Release Notes:

- SSH Remoting: make downloading binary on remote more reliable.

---------

Co-authored-by: Will <will@zed.dev>
2024-10-30 15:17:50 -07:00
Conrad Irwin
3d956ca68b Fail download if download fails (#19990)
Co-Authored-By: Mikayla <mikayla@zed.dev>

Release Notes:

- Remoting: Fixes a bug where we could cache an HTML error page as a
binary

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-10-30 14:50:41 -07:00
Kyle Kelley
7ce131aaf8 Trim whitespace from base64 encoded image data before decoding it (#19977)
Closes #17956
Closes #16330

This fix is for both REPL (released) and notebook (unreleased)

<img width="1210" alt="image"
src="https://github.com/user-attachments/assets/bd046f0f-3ad1-4c25-b3cb-114e008c2a69">

Release Notes:

- Fixed image support in REPL for certain versions of matplotlib that
included preceding and/or trailing whitespace in the base64 image data
2024-10-30 12:32:17 -07:00
Jen Stehlik
60be47d115 Update Gleam icon (#19978)
Improves upon: https://github.com/zed-industries/zed/pull/19887

Implements the feedback by @PixelJanitor to make the icon follow the
design guidelines.

Release Notes:

- Improved Gleam icon
2024-10-30 15:29:32 -04:00
Conrad Irwin
bd187883da Migration to remove dev servers (#19639)
Depends on #19638

Release Notes:

- None
2024-10-30 11:55:55 -06:00
Conrad Irwin
4f9217bca0 Support zed://ssh (#19970)
Closes: #15070

Release Notes:

- Added support for `zed://ssh/<connnection>/<path>`
2024-10-30 11:28:25 -06:00
Conrad Irwin
ce5222f1df Add KeyContextView (#19872)
Release Notes:

- Added `cmd-shift-p debug: Open Key Context View` to help debug custom
key bindings



https://github.com/user-attachments/assets/de273c97-5b27-45aa-9ff1-f943b0ed7dfe
2024-10-30 11:26:54 -06:00
Kirill Bulatov
cf7b0c8971 Add scrollbars to outline panel (#19969)
Part of https://github.com/zed-industries/zed/issues/15324


![image](https://github.com/user-attachments/assets/4f32d585-9bd2-46be-8234-3658a71906ee)

Repeats the approach used in the project panel.

Release Notes:

- Added scrollbars to outline panel

---------

Co-authored-by: Nate Butler <nate@zed.dev>
2024-10-30 19:09:14 +02:00
renovate[bot]
7bc4cb9868 Update Rust crate hyper to v0.14.31 (#19323)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [hyper](https://hyper.rs)
([source](https://redirect.github.com/hyperium/hyper)) |
workspace.dependencies | patch | `0.14.30` -> `0.14.31` |

---

### Release Notes

<details>
<summary>hyperium/hyper (hyper)</summary>

###
[`v0.14.31`](https://redirect.github.com/hyperium/hyper/releases/tag/v0.14.31)

[Compare
Source](https://redirect.github.com/hyperium/hyper/compare/v0.14.30...v0.14.31)

#### Bug Fixes

- **http1:** improve performance of parsing sequentially partial
messages
([97b595e](97b595e589))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xMjAuMSIsInVwZGF0ZWRJblZlciI6IjM4LjEyMC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-30 12:11:07 -04:00
Gherman
f84f3ffeb7 docs: Add linkedProjects section to Rust docs (#19954)
Related to #19897

Adds a section about multi-project workspaces and how to configure
rust-analyzer to diagnose them even if the cargo workspace does not list
them

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-10-30 11:43:44 -04:00
Richard Feldman
c564a4a26c Require /file or /tab when using Suggest Edits (#19960)
Now if you try to do Suggest Edits without a file context, you see this
(and it doesn't run the query).

<img width="635" alt="Screenshot 2024-10-30 at 10 51 24 AM"
src="https://github.com/user-attachments/assets/a3997ba6-98a9-4bfa-81b6-1d8579c26fd7">


Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>
2024-10-30 11:38:43 -04:00
Marshall Bowers
515fd7b75f git_hosting_providers: Fix support for GitLab remotes containing subgroups (#19962)
This PR fixes the support for GitLab remote URLs containing subgroups.

Reported in
https://github.com/zed-industries/zed/issues/18012#issuecomment-2446206256.

Release Notes:

- N/A
2024-10-30 11:16:44 -04:00
Peter Tripp
662a4440cc v0.161.x dev 2024-10-30 11:06:39 -04:00
Marshall Bowers
5dee43b05c dart: Extract to zed-extensions/dart repository (#19959)
This PR extracts the Dart extension to the
[zed-extensions/dart](https://github.com/zed-extensions/dart)
repository.

Release Notes:

- N/A
2024-10-30 11:00:06 -04:00
81 changed files with 2355 additions and 1600 deletions

38
Cargo.lock generated
View File

@@ -854,7 +854,7 @@ dependencies = [
"chrono",
"futures-util",
"http-types",
"hyper 0.14.30",
"hyper 0.14.31",
"hyper-rustls 0.24.2",
"serde",
"serde_json",
@@ -1350,7 +1350,7 @@ dependencies = [
"http-body 0.4.6",
"http-body 1.0.1",
"httparse",
"hyper 0.14.30",
"hyper 0.14.31",
"hyper-rustls 0.24.2",
"once_cell",
"pin-project-lite",
@@ -1441,7 +1441,7 @@ dependencies = [
"headers",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.30",
"hyper 0.14.31",
"itoa",
"matchit",
"memchr",
@@ -2366,7 +2366,7 @@ dependencies = [
"clickhouse-derive",
"clickhouse-rs-cityhash-sys",
"futures 0.3.30",
"hyper 0.14.30",
"hyper 0.14.31",
"hyper-tls",
"lz4",
"sealed",
@@ -2569,7 +2569,7 @@ dependencies = [
"gpui",
"hex",
"http_client",
"hyper 0.14.30",
"hyper 0.14.31",
"indoc",
"jsonwebtoken",
"language",
@@ -5570,9 +5570,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.30"
version = "0.14.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9"
checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85"
dependencies = [
"bytes 1.7.2",
"futures-channel",
@@ -5585,7 +5585,7 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
"socket2 0.5.7",
"socket2 0.4.10",
"tokio",
"tower-service",
"tracing",
@@ -5620,7 +5620,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http 0.2.12",
"hyper 0.14.30",
"hyper 0.14.31",
"log",
"rustls 0.21.12",
"rustls-native-certs 0.6.3",
@@ -5653,7 +5653,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes 1.7.2",
"hyper 0.14.30",
"hyper 0.14.31",
"native-tls",
"tokio",
"tokio-native-tls",
@@ -6346,6 +6346,7 @@ dependencies = [
"env_logger 0.11.5",
"futures 0.3.30",
"gpui",
"itertools 0.13.0",
"language",
"lsp",
"project",
@@ -6357,6 +6358,7 @@ dependencies = [
"ui",
"util",
"workspace",
"zed_actions",
]
[[package]]
@@ -6489,7 +6491,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
"windows-targets 0.48.5",
]
[[package]]
@@ -9536,6 +9538,7 @@ dependencies = [
"log",
"parking_lot",
"prost",
"release_channel",
"rpc",
"serde",
"serde_json",
@@ -9660,7 +9663,7 @@ dependencies = [
"h2 0.3.26",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.30",
"hyper 0.14.31",
"hyper-tls",
"ipnet",
"js-sys",
@@ -13452,7 +13455,7 @@ dependencies = [
"futures-util",
"headers",
"http 0.2.12",
"hyper 0.14.30",
"hyper 0.14.31",
"log",
"mime",
"mime_guess",
@@ -15033,7 +15036,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.160.0"
version = "0.161.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -15173,13 +15176,6 @@ dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_dart"
version = "0.1.2"
dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_deno"
version = "0.0.2"

View File

@@ -138,7 +138,6 @@ members = [
"extensions/astro",
"extensions/clojure",
"extensions/csharp",
"extensions/dart",
"extensions/deno",
"extensions/elixir",
"extensions/elm",

View File

@@ -1,6 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path fill-rule="evenodd" fill="black" d="M 3.828125 14.601562 C 3.894531 15.726562 5.183594 16.375 6.132812 15.785156 L 6.136719 15.785156 L 8.988281 13.824219 C 8.996094 13.816406 9.007812 13.8125 9.015625 13.804688 C 9.203125 13.675781 9.4375 13.636719 9.65625 13.691406 L 12.988281 14.550781 C 14.105469 14.839844 15.140625 13.769531 14.8125 12.667969 L 13.832031 9.386719 C 13.769531 9.167969 13.800781 8.9375 13.921875 8.75 C 13.921875 8.746094 13.925781 8.746094 13.925781 8.746094 L 15.777344 5.863281 L 15.777344 5.859375 C 15.78125 5.851562 15.785156 5.84375 15.789062 5.835938 L 15.792969 5.835938 C 16.382812 4.871094 15.6875 3.582031 14.542969 3.554688 L 11.109375 3.472656 C 10.878906 3.464844 10.664062 3.359375 10.519531 3.183594 L 8.339844 0.542969 C 8.019531 0.152344 7.550781 -0.015625 7.105469 0.0078125 L 7.101562 0.0078125 C 7.039062 0.0117188 6.976562 0.0195312 6.914062 0.0273438 C 6.414062 0.117188 5.945312 0.453125 5.75 1 L 4.609375 4.222656 C 4.535156 4.4375 4.367188 4.613281 4.152344 4.695312 L 0.957031 5.945312 C -0.121094 6.363281 -0.328125 7.835938 0.589844 8.535156 L 3.316406 10.609375 C 3.5 10.75 3.609375 10.960938 3.625 11.191406 Z M 7.515625 1.847656 C 7.421875 1.730469 7.296875 1.695312 7.183594 1.714844 C 7.066406 1.734375 6.960938 1.8125 6.914062 1.953125 L 5.867188 4.902344 C 5.699219 5.382812 5.328125 5.765625 4.851562 5.949219 L 1.925781 7.09375 C 1.785156 7.148438 1.710938 7.253906 1.695312 7.371094 C 1.679688 7.484375 1.71875 7.605469 1.839844 7.695312 L 4.335938 9.597656 C 4.742188 9.90625 4.992188 10.375 5.023438 10.882812 L 5.207031 14.003906 C 5.214844 14.152344 5.296875 14.253906 5.398438 14.304688 C 5.503906 14.355469 5.632812 14.355469 5.757812 14.269531 L 8.347656 12.492188 C 8.765625 12.207031 9.292969 12.113281 9.785156 12.242188 L 12.824219 13.027344 C 12.972656 13.066406 13.09375 13.023438 13.175781 12.9375 C 13.257812 12.855469 13.296875 12.734375 13.253906 12.589844 L 12.355469 9.589844 C 12.210938 9.105469 12.285156 8.578125 12.558594 8.148438 L 14.253906 5.511719 C 14.335938 5.386719 14.332031 5.257812 14.277344 5.15625 C 14.222656 5.054688 14.117188 4.980469 13.964844 4.976562 L 10.824219 4.902344 C 10.316406 4.886719 9.835938 4.65625 9.511719 4.261719 Z M 7.515625 1.847656 "/>
<path fill="black" d="M 5.71875 7.257812 C 5.671875 7.25 5.628906 7.246094 5.582031 7.246094 C 5.09375 7.246094 4.695312 7.644531 4.695312 8.128906 C 4.695312 8.613281 5.09375 9.011719 5.582031 9.011719 C 6.070312 9.011719 6.46875 8.613281 6.46875 8.128906 C 6.46875 7.6875 6.140625 7.320312 5.71875 7.257812 Z M 5.71875 7.257812 "/>
<path fill="black" d="M 11.019531 7.953125 C 10.976562 7.957031 10.929688 7.960938 10.886719 7.960938 C 10.398438 7.960938 10 7.5625 10 7.078125 C 10 6.59375 10.398438 6.195312 10.886719 6.195312 C 11.371094 6.195312 11.773438 6.59375 11.773438 7.078125 C 11.773438 7.519531 11.445312 7.886719 11.019531 7.953125 Z M 11.019531 7.953125 "/>
<path fill="black" d="M 7.269531 9.089844 C 7.53125 8.988281 7.828125 9.113281 7.933594 9.375 C 8.125 9.859375 8.503906 9.996094 8.796875 9.949219 C 9.082031 9.898438 9.378906 9.664062 9.378906 9.136719 C 9.378906 8.855469 9.605469 8.628906 9.886719 8.628906 C 10.167969 8.628906 10.398438 8.855469 10.398438 9.136719 C 10.398438 10.140625 9.757812 10.816406 8.96875 10.949219 C 8.1875 11.078125 7.351562 10.664062 6.988281 9.75 C 6.882812 9.488281 7.011719 9.195312 7.269531 9.089844 Z M 7.269531 9.089844 "/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.3848 9.30444C7.3848 9.30444 7.53254 10.2646 8.53248 10.0882C9.53242 9.91193 9.36378 8.95549 9.36378 8.95549" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.54155 5.54157C6.12355 4.90104 6.01688 2.62541 7.22875 2.3985C8.44063 2.17158 9.19097 4.33148 9.91982 4.6814C10.6487 5.03133 12.8517 4.3028 13.4381 5.38734C14.0244 6.47188 12.1395 7.95973 12.026 8.64088C11.9126 9.32203 13.3614 11.2416 12.4675 12.1701C11.5736 13.0986 9.73005 11.7545 8.90486 11.8834C8.07966 12.0123 6.79244 13.9095 5.67367 13.3502C4.55491 12.7909 5.16702 10.5455 4.82437 9.87612C4.48171 9.20673 2.34028 8.54978 2.4525 7.35049C2.56471 6.15121 4.95956 6.1821 5.54155 5.54157Z" stroke="#FF7676" stroke-opacity="0.52" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.54155 5.54157C6.12355 4.90104 6.01688 2.62541 7.22875 2.3985C8.44063 2.17158 9.19097 4.33148 9.91982 4.6814C10.6487 5.03133 12.8517 4.3028 13.4381 5.38734C14.0244 6.47188 12.1395 7.95973 12.026 8.64088C11.9126 9.32203 13.3614 11.2416 12.4675 12.1701C11.5736 13.0986 9.73005 11.7545 8.90486 11.8834C8.07966 12.0123 6.79244 13.9095 5.67367 13.3502C4.55491 12.7909 5.16702 10.5455 4.82437 9.87612C4.48171 9.20673 2.34028 8.54978 2.4525 7.35049C2.56471 6.15121 4.95956 6.1821 5.54155 5.54157Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="6.25098" cy="7.75" r="0.75" fill="black"/>
<circle cx="10.1035" cy="7.25" r="0.75" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

1
assets/icons/wand.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wand"><path d="M15 4V2"/><path d="M15 16v-2"/><path d="M8 9h2"/><path d="M20 9h2"/><path d="M17.8 11.8 19 13"/><path d="M15 9h.01"/><path d="M17.8 6.2 19 5"/><path d="m3 21 9-9"/><path d="M12.2 6.2 11 5"/></svg>

After

Width:  |  Height:  |  Size: 414 B

View File

@@ -414,6 +414,23 @@
// 2. Never show indent guides:
// "never"
"show": "always"
},
/// Scrollbar-related settings
"scrollbar": {
/// When to show the scrollbar in the project panel.
/// This setting can take four values:
///
/// 1. null (default): Inherit editor settings
/// 2. Show the scrollbar if there's important information or
/// follow the system's configured behavior (default):
/// "auto"
/// 3. Match the system's configured behavior:
/// "system"
/// 4. Always show the scrollbar:
/// "always"
/// 5. Never show the scrollbar:
/// "never"
"show": null
}
},
"collaboration_panel": {
@@ -635,6 +652,12 @@
// Sets a delay after which the inline blame information is shown.
// Delay is restarted with every cursor movement.
// "delay_ms": 600
//
// Whether or not do display the git commit summary on the same line.
// "show_commit_summary": false
//
// The minimum column number to show the inline blame information at
// "min_column": 0
}
},
// Configuration for how direnv configuration should be loaded. May take 2 values:

View File

@@ -16,6 +16,7 @@
"allow_concurrent_runs": false,
// What to do with the terminal pane and tab, after the command was started:
// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
// * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it
// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
"reveal": "always",
// What to do with the terminal pane and tab, after the command had finished:

View File

@@ -352,7 +352,10 @@ impl ActivityIndicator {
.into_any_element(),
),
message: format!("Formatting failed: {}. Click to see logs.", failure),
on_click: Some(Arc::new(|_, cx| {
on_click: Some(Arc::new(|indicator, cx| {
indicator.project.update(cx, |project, cx| {
project.reset_last_formatting_failure(cx);
});
cx.dispatch_action(Box::new(workspace::OpenLog));
})),
});

View File

@@ -73,12 +73,11 @@ use std::{
};
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use text::SelectionGoal;
use ui::TintColor;
use ui::{
prelude::*,
utils::{format_distance_from_now, DateTimeType},
Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip,
ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip,
};
use util::{maybe, ResultExt};
use workspace::{
@@ -1462,6 +1461,7 @@ type MessageHeader = MessageMetadata;
#[derive(Clone)]
enum AssistError {
FileRequired,
PaymentRequired,
MaxMonthlySpendReached,
Message(SharedString),
@@ -1628,7 +1628,10 @@ impl ContextEditor {
self.last_error = None;
if let Some(user_message) = self
if request_type == RequestType::SuggestEdits && !self.context.read(cx).contains_files(cx) {
self.last_error = Some(AssistError::FileRequired);
cx.notify();
} else if let Some(user_message) = self
.context
.update(cx, |context, cx| context.assist(request_type, cx))
{
@@ -2200,12 +2203,14 @@ impl ContextEditor {
let max_width = cx.max_width;
let gutter_width = cx.gutter_dimensions.full_width();
let block_id = cx.block_id;
let selected = cx.selected;
this.update(&mut **cx, |this, cx| {
this.render_patch(
patch_range.clone(),
max_width,
gutter_width,
block_id,
selected,
cx,
)
})
@@ -3435,6 +3440,7 @@ impl ContextEditor {
max_width: Pixels,
gutter_width: Pixels,
id: BlockId,
selected: bool,
cx: &mut ViewContext<Self>,
) -> Option<AnyElement> {
let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
@@ -3453,8 +3459,13 @@ impl ContextEditor {
Some(
v_flex()
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(cx.theme().colors().border)
.border_color(if selected {
cx.theme().colors().border_focused
} else {
cx.theme().colors().border
})
.id(id)
.ml(gutter_width)
.p_2()
@@ -3702,6 +3713,7 @@ impl ContextEditor {
.elevation_2(cx)
.occlude()
.child(match last_error {
AssistError::FileRequired => self.render_file_required_error(cx),
AssistError::PaymentRequired => self.render_payment_required_error(cx),
AssistError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
@@ -3714,6 +3726,41 @@ impl ContextEditor {
)
}
fn render_file_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::Warning).color(Color::Warning))
.child(
Label::new("Suggest Edits needs a file to edit").weight(FontWeight::MEDIUM),
),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(
"To include files, type /file or /tab in your prompt.",
)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.last_error = None;
cx.notify();
},
))),
)
.into_any()
}
fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
@@ -3928,13 +3975,7 @@ impl Render for ContextEditor {
} else {
None
};
let focus_handle = self
.workspace
.update(cx, |workspace, cx| {
Some(workspace.active_item_as::<Editor>(cx)?.focus_handle(cx))
})
.ok()
.flatten();
v_flex()
.key_context("ContextEditor")
.capture_action(cx.listener(ContextEditor::cancel))
@@ -3982,28 +4023,7 @@ impl Render for ContextEditor {
.child(
h_flex()
.gap_1()
.child(render_inject_context_menu(cx.view().downgrade(), cx))
.child(
IconButton::new("quote-button", IconName::Quote)
.icon_size(IconSize::Small)
.on_click(|_, cx| {
cx.dispatch_action(QuoteSelection.boxed_clone());
})
.tooltip(move |cx| {
cx.new_view(|cx| {
Tooltip::new("Insert Selection").key_binding(
focus_handle.as_ref().and_then(|handle| {
KeyBinding::for_action_in(
&QuoteSelection,
&handle,
cx,
)
}),
)
})
.into()
}),
),
.child(render_inject_context_menu(cx.view().downgrade(), cx)),
)
.child(
h_flex()
@@ -4298,6 +4318,7 @@ fn render_inject_context_menu(
Button::new("trigger", "Add Context")
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.tooltip(|cx| Tooltip::text("Type / to insert via keyboard", cx)),
)
@@ -4472,7 +4493,7 @@ impl Render for ContextEditorToolbarItem {
.w_full()
.justify_between()
.gap_2()
.child(Label::new("Insert Context"))
.child(Label::new("Add Context"))
.child(Label::new("/ command").color(Color::Muted))
.into_any()
},
@@ -4496,7 +4517,7 @@ impl Render for ContextEditorToolbarItem {
}
},
)
.action("Insert Selection", QuoteSelection.boxed_clone())
.action("Add Selection", QuoteSelection.boxed_clone())
}))
}
}),

View File

@@ -2,8 +2,9 @@
mod context_tests;
use crate::{
prompts::PromptBuilder, slash_command::SlashCommandLine, AssistantEdit, AssistantPatch,
AssistantPatchStatus, MessageId, MessageStatus,
prompts::PromptBuilder,
slash_command::{file_command::FileCommandMetadata, SlashCommandLine},
AssistantEdit, AssistantPatch, AssistantPatchStatus, MessageId, MessageStatus,
};
use anyhow::{anyhow, Context as _, Result};
use assistant_slash_command::{
@@ -66,7 +67,7 @@ impl ContextId {
}
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RequestType {
/// Request a normal chat response from the model.
Chat,
@@ -989,6 +990,20 @@ impl Context {
&self.slash_command_output_sections
}
pub fn contains_files(&self, cx: &AppContext) -> bool {
let buffer = self.buffer.read(cx);
self.slash_command_output_sections.iter().any(|section| {
section.is_valid(buffer)
&& section
.metadata
.as_ref()
.and_then(|metadata| {
serde_json::from_value::<FileCommandMetadata>(metadata.clone()).ok()
})
.is_some()
})
}
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
self.pending_tool_uses_by_id.values().collect()
}

View File

@@ -311,7 +311,7 @@ impl PromptBuilder {
}
pub fn generate_workflow_prompt(&self) -> Result<String, RenderError> {
self.handlebars.lock().render("edit_workflow", &())
self.handlebars.lock().render("suggest_edits", &())
}
pub fn generate_project_slash_command_prompt(

View File

@@ -14,7 +14,7 @@ use language_model::{
use semantic_index::{FileSummary, SemanticDb};
use smol::channel;
use std::sync::{atomic::AtomicBool, Arc};
use ui::{BorrowAppContext, WindowContext};
use ui::{prelude::*, BorrowAppContext, WindowContext};
use util::ResultExt;
use workspace::Workspace;
@@ -37,6 +37,10 @@ impl SlashCommand for AutoCommand {
"Automatically infer what context to add".into()
}
fn icon(&self) -> IconName {
IconName::Wand
}
fn menu_text(&self) -> String {
self.description()
}

View File

@@ -10,6 +10,7 @@ use gpui::{Task, WeakView, WindowContext};
use language::{BufferSnapshot, LspAdapterDelegate};
use std::sync::{atomic::AtomicBool, Arc};
use text::OffsetRangeExt;
use ui::prelude::*;
use workspace::Workspace;
pub(crate) struct DeltaSlashCommand;
@@ -27,6 +28,10 @@ impl SlashCommand for DeltaSlashCommand {
self.description()
}
fn icon(&self) -> IconName {
IconName::Diff
}
fn requires_argument(&self) -> bool {
false
}

View File

@@ -98,6 +98,10 @@ impl SlashCommand for DiagnosticsSlashCommand {
"Insert diagnostics".into()
}
fn icon(&self) -> IconName {
IconName::XCircle
}
fn menu_text(&self) -> String {
self.description()
}

View File

@@ -117,7 +117,7 @@ impl SlashCommand for FileSlashCommand {
}
fn description(&self) -> String {
"Insert file".into()
"Insert file and/or directory".into()
}
fn menu_text(&self) -> String {
@@ -128,6 +128,10 @@ impl SlashCommand for FileSlashCommand {
true
}
fn icon(&self) -> IconName {
IconName::File
}
fn complete_argument(
self: Arc<Self>,
arguments: &[String],

View File

@@ -24,7 +24,8 @@ use std::{
ops::DerefMut,
sync::{atomic::AtomicBool, Arc},
};
use ui::{BorrowAppContext as _, IconName};
use ui::prelude::*;
use workspace::Workspace;
pub struct ProjectSlashCommand {
@@ -50,6 +51,10 @@ impl SlashCommand for ProjectSlashCommand {
"Generate a semantic search based on context".into()
}
fn icon(&self) -> IconName {
IconName::Folder
}
fn menu_text(&self) -> String {
self.description()
}

View File

@@ -21,6 +21,10 @@ impl SlashCommand for PromptSlashCommand {
"Insert prompt from library".into()
}
fn icon(&self) -> IconName {
IconName::Library
}
fn menu_text(&self) -> String {
self.description()
}

View File

@@ -38,6 +38,10 @@ impl SlashCommand for SearchSlashCommand {
"Search your project semantically".into()
}
fn icon(&self) -> IconName {
IconName::SearchCode
}
fn menu_text(&self) -> String {
self.description()
}

View File

@@ -22,6 +22,10 @@ impl SlashCommand for OutlineSlashCommand {
"Insert symbols for active tab".into()
}
fn icon(&self) -> IconName {
IconName::ListTree
}
fn menu_text(&self) -> String {
self.description()
}

View File

@@ -12,7 +12,7 @@ use std::{
path::PathBuf,
sync::{atomic::AtomicBool, Arc},
};
use ui::{ActiveTheme, WindowContext};
use ui::{prelude::*, ActiveTheme, WindowContext};
use util::ResultExt;
use workspace::Workspace;
@@ -31,6 +31,10 @@ impl SlashCommand for TabSlashCommand {
"Insert open tabs (active tab by default)".to_owned()
}
fn icon(&self) -> IconName {
IconName::FileTree
}
fn menu_text(&self) -> String {
self.description()
}

View File

@@ -33,6 +33,10 @@ impl SlashCommand for TerminalSlashCommand {
"Insert terminal output".into()
}
fn icon(&self) -> IconName {
IconName::Terminal
}
fn menu_text(&self) -> String {
self.description()
}

View File

@@ -1,19 +1,13 @@
use std::sync::Arc;
use assistant_slash_command::SlashCommandRegistry;
use gpui::AnyElement;
use gpui::DismissEvent;
use gpui::WeakView;
use picker::PickerEditorPosition;
use ui::ListItemSpacing;
use gpui::SharedString;
use gpui::Task;
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, PopoverMenu, PopoverTrigger};
use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakView};
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use ui::{prelude::*, KeyBinding, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger};
use crate::assistant_panel::ContextEditor;
use crate::QuoteSelection;
#[derive(IntoElement)]
pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
@@ -27,6 +21,7 @@ struct SlashCommandInfo {
name: SharedString,
description: SharedString,
args: Option<SharedString>,
icon: IconName,
}
#[derive(Clone)]
@@ -37,6 +32,7 @@ enum SlashCommandEntry {
renderer: fn(&mut WindowContext<'_>) -> AnyElement,
on_confirm: fn(&mut WindowContext<'_>),
},
QuoteButton,
}
impl AsRef<str> for SlashCommandEntry {
@@ -44,6 +40,7 @@ impl AsRef<str> for SlashCommandEntry {
match self {
SlashCommandEntry::Info(SlashCommandInfo { name, .. })
| SlashCommandEntry::Advert { name, .. } => name,
SlashCommandEntry::QuoteButton => "Quote Selection",
}
}
}
@@ -145,16 +142,23 @@ impl PickerDelegate for SlashCommandDelegate {
}
ret
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some(command) = self.filtered_commands.get(self.selected_index) {
if let SlashCommandEntry::Info(info) = command {
self.active_context_editor
.update(cx, |context_editor, cx| {
context_editor.insert_command(&info.name, cx)
})
.ok();
} else if let SlashCommandEntry::Advert { on_confirm, .. } = command {
on_confirm(cx);
match command {
SlashCommandEntry::Info(info) => {
self.active_context_editor
.update(cx, |context_editor, cx| {
context_editor.insert_command(&info.name, cx)
})
.ok();
}
SlashCommandEntry::QuoteButton => {
cx.dispatch_action(Box::new(QuoteSelection));
}
SlashCommandEntry::Advert { on_confirm, .. } => {
on_confirm(cx);
}
}
cx.emit(DismissEvent);
}
@@ -181,46 +185,78 @@ impl PickerDelegate for SlashCommandDelegate {
.spacing(ListItemSpacing::Dense)
.selected(selected)
.child(
h_flex()
v_flex()
.group(format!("command-entry-label-{ix}"))
.w_full()
.min_w(px(250.))
.child(
v_flex()
.child(
h_flex()
.child(div().font_buffer(cx).child({
let mut label = format!("/{}", info.name);
if let Some(args) =
info.args.as_ref().filter(|_| selected)
{
label.push_str(&args);
}
Label::new(label).size(LabelSize::Small)
}))
.children(info.args.clone().filter(|_| !selected).map(
|args| {
div()
.font_buffer(cx)
.child(
Label::new(args)
.size(LabelSize::Small)
.color(Color::Muted),
)
.visible_on_hover(format!(
"command-entry-label-{ix}"
))
},
)),
)
.child(
Label::new(info.description.clone())
.size(LabelSize::Small)
.color(Color::Muted),
),
h_flex()
.gap_1p5()
.child(Icon::new(info.icon).size(IconSize::XSmall))
.child(div().font_buffer(cx).child({
let mut label = format!("{}", info.name);
if let Some(args) = info.args.as_ref().filter(|_| selected)
{
label.push_str(&args);
}
Label::new(label).size(LabelSize::Small)
}))
.children(info.args.clone().filter(|_| !selected).map(
|args| {
div()
.font_buffer(cx)
.child(
Label::new(args)
.size(LabelSize::Small)
.color(Color::Muted),
)
.visible_on_hover(format!(
"command-entry-label-{ix}"
))
},
)),
)
.child(
Label::new(info.description.clone())
.size(LabelSize::Small)
.color(Color::Muted),
),
),
),
SlashCommandEntry::QuoteButton => {
let focus = cx.focus_handle();
let key_binding = KeyBinding::for_action_in(&QuoteSelection, &focus, cx);
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Dense)
.selected(selected)
.child(
v_flex()
.child(
h_flex()
.gap_1p5()
.child(Icon::new(IconName::Quote).size(IconSize::XSmall))
.child(
div().font_buffer(cx).child(
Label::new("selection").size(LabelSize::Small),
),
),
)
.child(
h_flex()
.gap_1p5()
.child(
Label::new("Insert editor selection")
.color(Color::Muted)
.size(LabelSize::Small),
)
.children(key_binding.map(|kb| kb.render(cx))),
),
),
)
}
SlashCommandEntry::Advert { renderer, .. } => Some(
ListItem::new(ix)
.inset(true)
@@ -251,31 +287,50 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
name: command_name.into(),
description: menu_text,
args,
icon: command.icon(),
}))
})
.chain([SlashCommandEntry::Advert {
name: "create-your-command".into(),
renderer: |cx| {
v_flex()
.child(
h_flex()
.font_buffer(cx)
.items_center()
.gap_1()
.child(div().font_buffer(cx).child(
Label::new("create-your-command").size(LabelSize::Small),
))
.child(Icon::new(IconName::ArrowUpRight).size(IconSize::XSmall)),
)
.child(
Label::new("Learn how to create a custom command")
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
.chain([
SlashCommandEntry::Advert {
name: "create-your-command".into(),
renderer: |cx| {
v_flex()
.w_full()
.child(
h_flex()
.w_full()
.font_buffer(cx)
.items_center()
.justify_between()
.child(
h_flex()
.items_center()
.gap_1p5()
.child(Icon::new(IconName::Plus).size(IconSize::XSmall))
.child(
div().font_buffer(cx).child(
Label::new("create-your-command")
.size(LabelSize::Small),
),
),
)
.child(
Icon::new(IconName::ArrowUpRight)
.size(IconSize::XSmall)
.color(Color::Muted),
),
)
.child(
Label::new("Create your custom command")
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
},
on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
},
on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
}])
SlashCommandEntry::QuoteButton,
])
.collect::<Vec<_>>();
let delegate = SlashCommandDelegate {

View File

@@ -62,6 +62,9 @@ pub type SlashCommandResult = Result<BoxStream<'static, Result<SlashCommandEvent
pub trait SlashCommand: 'static + Send + Sync {
fn name(&self) -> String;
fn icon(&self) -> IconName {
IconName::Slash
}
fn label(&self, _cx: &AppContext) -> CodeLabel {
CodeLabel::plain(self.name(), None)
}

View File

@@ -686,6 +686,12 @@ async fn download_remote_server_binary(
let request_body = AsyncBody::from(serde_json::to_string(&update_request_body)?);
let mut response = client.get(&release.url, request_body, true).await?;
if !response.status().is_success() {
return Err(anyhow!(
"failed to download remote server release: {:?}",
response.status()
));
}
smol::io::copy(response.body_mut(), &mut temp_file).await?;
smol::fs::rename(&temp, &target_path).await?;

View File

@@ -52,9 +52,7 @@ CREATE TABLE "projects" (
"host_user_id" INTEGER REFERENCES users (id),
"host_connection_id" INTEGER,
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE,
"hosted_project_id" INTEGER REFERENCES hosted_projects (id),
"dev_server_project_id" INTEGER REFERENCES dev_server_projects(id)
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");
CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id");
@@ -399,30 +397,6 @@ CREATE TABLE rate_buckets (
);
CREATE INDEX idx_user_id_rate_limit ON rate_buckets (user_id, rate_limit_name);
CREATE TABLE hosted_projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_id INTEGER NOT NULL REFERENCES channels(id),
name TEXT NOT NULL,
visibility TEXT NOT NULL,
deleted_at TIMESTAMP NULL
);
CREATE INDEX idx_hosted_projects_on_channel_id ON hosted_projects (channel_id);
CREATE UNIQUE INDEX uix_hosted_projects_on_channel_id_and_name ON hosted_projects (channel_id, name) WHERE (deleted_at IS NULL);
CREATE TABLE dev_servers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id),
name TEXT NOT NULL,
ssh_connection_string TEXT,
hashed_token TEXT NOT NULL
);
CREATE TABLE dev_server_projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id),
paths TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS billing_preferences (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

View File

@@ -0,0 +1,6 @@
ALTER TABLE projects DROP COLUMN dev_server_project_id;
ALTER TABLE projects DROP COLUMN hosted_project_id;
DROP TABLE hosted_projects;
DROP TABLE dev_server_projects;
DROP TABLE dev_servers;

View File

@@ -750,49 +750,6 @@ impl Database {
Ok((project, replica_id as ReplicaId))
}
pub async fn leave_hosted_project(
&self,
project_id: ProjectId,
connection: ConnectionId,
) -> Result<LeftProject> {
self.transaction(|tx| async move {
let result = project_collaborator::Entity::delete_many()
.filter(
Condition::all()
.add(project_collaborator::Column::ProjectId.eq(project_id))
.add(project_collaborator::Column::ConnectionId.eq(connection.id as i32))
.add(
project_collaborator::Column::ConnectionServerId
.eq(connection.owner_id as i32),
),
)
.exec(&*tx)
.await?;
if result.rows_affected == 0 {
return Err(anyhow!("not in the project"))?;
}
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
let collaborators = project
.find_related(project_collaborator::Entity)
.all(&*tx)
.await?;
let connection_ids = collaborators
.into_iter()
.map(|collaborator| collaborator.connection())
.collect();
Ok(LeftProject {
id: project.id,
connection_ids,
should_unshare: false,
})
})
.await
}
/// Removes the given connection from the specified project.
pub async fn leave_project(
&self,

View File

@@ -80,6 +80,8 @@ pub struct ConfirmCodeAction {
pub struct ToggleComments {
#[serde(default)]
pub advance_downwards: bool,
#[serde(default)]
pub ignore_indent: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
@@ -157,6 +159,13 @@ pub struct DeleteToPreviousWordStart {
pub struct FoldAtLevel {
pub level: u32,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SpawnNearestTask {
#[serde(default)]
pub reveal: task::RevealStrategy,
}
impl_actions!(
editor,
[
@@ -182,6 +191,7 @@ impl_actions!(
SelectToBeginningOfLine,
SelectToEndOfLine,
SelectUpByLines,
SpawnNearestTask,
ShowCompletions,
ToggleCodeActions,
ToggleComments,

View File

@@ -660,7 +660,7 @@ impl DisplaySnapshot {
new_start..new_end
}
pub fn point_to_display_point(&self, point: MultiBufferPoint, bias: Bias) -> DisplayPoint {
fn point_to_display_point(&self, point: MultiBufferPoint, bias: Bias) -> DisplayPoint {
let inlay_point = self.inlay_snapshot.to_inlay_point(point);
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
let tab_point = self.tab_snapshot.to_tab_point(fold_point);
@@ -669,7 +669,7 @@ impl DisplaySnapshot {
DisplayPoint(block_point)
}
pub fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
self.inlay_snapshot
.to_buffer_point(self.display_point_to_inlay_point(point, bias))
}
@@ -942,6 +942,14 @@ impl DisplaySnapshot {
DisplayPoint(clipped)
}
pub fn clip_point_2(&self, point: DisplayPoint, bias: Bias, skip_blocks: bool) -> DisplayPoint {
let mut clipped = self.block_snapshot.clip_point_2(point.0, bias, skip_blocks);
if self.clip_at_line_ends {
clipped = self.clip_at_line_end(DisplayPoint(clipped)).0
}
DisplayPoint(clipped)
}
pub fn clip_ignoring_line_ends(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
DisplayPoint(self.block_snapshot.clip_point(point.0, bias))
}

View File

@@ -1298,6 +1298,68 @@ impl BlockSnapshot {
cursor.item().map_or(false, |t| t.block.is_some())
}
pub fn clip_point_2(&self, point: BlockPoint, bias: Bias, skip_blocks: bool) -> BlockPoint {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
cursor.seek(&BlockRow(point.row), Bias::Right, &());
let max_input_row = WrapRow(self.transforms.summary().input_rows);
let mut search_left =
(bias == Bias::Left && cursor.start().1 .0 > 0) || cursor.end(&()).1 == max_input_row;
let mut reversed = false;
loop {
if let Some(transform) = cursor.item() {
let (output_start_row, input_start_row) = cursor.start();
let (output_end_row, input_end_row) = cursor.end(&());
let output_start = Point::new(output_start_row.0, 0);
let output_end = Point::new(output_end_row.0, 0);
let input_start = Point::new(input_start_row.0, 0);
let input_end = Point::new(input_end_row.0, 0);
match transform.block.as_ref() {
Some(Block::Custom(block))
if matches!(block.placement, BlockPlacement::Replace(_)) =>
{
if bias == Bias::Left {
return BlockPoint(output_start);
} else {
return BlockPoint(Point::new(output_end.row - 1, 0));
}
}
None => {
let input_point = if point.row >= output_end_row.0 {
let line_len = self.wrap_snapshot.line_len(input_end_row.0 - 1);
self.wrap_snapshot
.clip_point(WrapPoint::new(input_end_row.0 - 1, line_len), bias)
} else {
let output_overshoot = point.0.saturating_sub(output_start);
self.wrap_snapshot
.clip_point(WrapPoint(input_start + output_overshoot), bias)
};
if (input_start..input_end).contains(&input_point.0) {
let input_overshoot = input_point.0.saturating_sub(input_start);
return BlockPoint(output_start + input_overshoot);
}
}
_ => {}
}
if search_left {
cursor.prev(&());
} else {
cursor.next(&());
}
} else if reversed {
return self.max_point();
} else {
reversed = true;
search_left = !search_left;
cursor.seek(&BlockRow(point.row), Bias::Right, &());
}
}
}
pub fn clip_point(&self, point: BlockPoint, bias: Bias) -> BlockPoint {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
cursor.seek(&BlockRow(point.row), Bias::Right, &());

View File

@@ -502,6 +502,19 @@ struct RunnableTasks {
context_range: Range<BufferOffset>,
}
impl RunnableTasks {
fn resolve<'a>(
&'a self,
cx: &'a task::TaskContext,
) -> impl Iterator<Item = (TaskSourceKind, ResolvedTask)> + 'a {
self.templates.iter().filter_map(|(kind, template)| {
template
.resolve_task(&kind.to_id_base(), cx)
.map(|task| (kind.clone(), task))
})
}
}
#[derive(Clone)]
struct ResolvedTasks {
templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
@@ -3471,8 +3484,8 @@ impl Editor {
}
let new_anchor_selections = new_selections.iter().map(|e| &e.0);
let new_selection_deltas = new_selections.iter().map(|e| e.1);
let map = this.display_map.update(cx, |map, cx| map.snapshot(cx));
let new_selections = resolve_multiple::<usize, _>(new_anchor_selections, &map)
let snapshot = this.buffer.read(cx).read(cx);
let new_selections = resolve_multiple::<usize, _>(new_anchor_selections, &snapshot)
.zip(new_selection_deltas)
.map(|(selection, delta)| Selection {
id: selection.id,
@@ -3485,20 +3498,18 @@ impl Editor {
let mut i = 0;
for (position, delta, selection_id, pair) in new_autoclose_regions {
let position = position.to_offset(&map.buffer_snapshot) + delta;
let start = map.buffer_snapshot.anchor_before(position);
let end = map.buffer_snapshot.anchor_after(position);
let position = position.to_offset(&snapshot) + delta;
let start = snapshot.anchor_before(position);
let end = snapshot.anchor_after(position);
while let Some(existing_state) = this.autoclose_regions.get(i) {
match existing_state.range.start.cmp(&start, &map.buffer_snapshot) {
match existing_state.range.start.cmp(&start, &snapshot) {
Ordering::Less => i += 1,
Ordering::Greater => break,
Ordering::Equal => {
match end.cmp(&existing_state.range.end, &map.buffer_snapshot) {
Ordering::Less => i += 1,
Ordering::Equal => break,
Ordering::Greater => break,
}
}
Ordering::Equal => match end.cmp(&existing_state.range.end, &snapshot) {
Ordering::Less => i += 1,
Ordering::Equal => break,
Ordering::Greater => break,
},
}
}
this.autoclose_regions.insert(
@@ -3511,6 +3522,7 @@ impl Editor {
);
}
drop(snapshot);
let had_active_inline_completion = this.has_active_inline_completion(cx);
this.change_selections_inner(Some(Autoscroll::fit()), false, cx, |s| {
s.select(new_selections)
@@ -4724,29 +4736,7 @@ impl Editor {
.as_ref()
.zip(editor.project.clone())
.map(|(tasks, project)| {
let position = Point::new(buffer_row, tasks.column);
let range_start = buffer.read(cx).anchor_at(position, Bias::Right);
let location = Location {
buffer: buffer.clone(),
range: range_start..range_start,
};
// Fill in the environmental variables from the tree-sitter captures
let mut captured_task_variables = TaskVariables::default();
for (capture_name, value) in tasks.extra_variables.clone() {
captured_task_variables.insert(
task::VariableName::Custom(capture_name.into()),
value.clone(),
);
}
project.update(cx, |project, cx| {
project.task_store().update(cx, |task_store, cx| {
task_store.task_context_for_location(
captured_task_variables,
location,
cx,
)
})
})
Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx)
});
Some(cx.spawn(|editor, mut cx| async move {
@@ -4757,15 +4747,7 @@ impl Editor {
let resolved_tasks =
tasks.zip(task_context).map(|(tasks, task_context)| {
Arc::new(ResolvedTasks {
templates: tasks
.templates
.iter()
.filter_map(|(kind, template)| {
template
.resolve_task(&kind.to_id_base(), &task_context)
.map(|task| (kind.clone(), task))
})
.collect(),
templates: tasks.resolve(&task_context).collect(),
position: snapshot.buffer_snapshot.anchor_before(Point::new(
multibuffer_point.row,
tasks.column,
@@ -5471,6 +5453,132 @@ impl Editor {
}
}
fn build_tasks_context(
project: &Model<Project>,
buffer: &Model<Buffer>,
buffer_row: u32,
tasks: &Arc<RunnableTasks>,
cx: &mut ViewContext<Self>,
) -> Task<Option<task::TaskContext>> {
let position = Point::new(buffer_row, tasks.column);
let range_start = buffer.read(cx).anchor_at(position, Bias::Right);
let location = Location {
buffer: buffer.clone(),
range: range_start..range_start,
};
// Fill in the environmental variables from the tree-sitter captures
let mut captured_task_variables = TaskVariables::default();
for (capture_name, value) in tasks.extra_variables.clone() {
captured_task_variables.insert(
task::VariableName::Custom(capture_name.into()),
value.clone(),
);
}
project.update(cx, |project, cx| {
project.task_store().update(cx, |task_store, cx| {
task_store.task_context_for_location(captured_task_variables, location, cx)
})
})
}
pub fn spawn_nearest_task(&mut self, action: &SpawnNearestTask, cx: &mut ViewContext<Self>) {
let Some((workspace, _)) = self.workspace.clone() else {
return;
};
let Some(project) = self.project.clone() else {
return;
};
// Try to find a closest, enclosing node using tree-sitter that has a
// task
let Some((buffer, buffer_row, tasks)) = self
.find_enclosing_node_task(cx)
// Or find the task that's closest in row-distance.
.or_else(|| self.find_closest_task(cx))
else {
return;
};
let reveal_strategy = action.reveal;
let task_context = Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx);
cx.spawn(|_, mut cx| async move {
let context = task_context.await?;
let (task_source_kind, mut resolved_task) = tasks.resolve(&context).next()?;
let resolved = resolved_task.resolved.as_mut()?;
resolved.reveal = reveal_strategy;
workspace
.update(&mut cx, |workspace, cx| {
workspace::tasks::schedule_resolved_task(
workspace,
task_source_kind,
resolved_task,
false,
cx,
);
})
.ok()
})
.detach();
}
fn find_closest_task(
&mut self,
cx: &mut ViewContext<Self>,
) -> Option<(Model<Buffer>, u32, Arc<RunnableTasks>)> {
let cursor_row = self.selections.newest_adjusted(cx).head().row;
let ((buffer_id, row), tasks) = self
.tasks
.iter()
.min_by_key(|((_, row), _)| cursor_row.abs_diff(*row))?;
let buffer = self.buffer.read(cx).buffer(*buffer_id)?;
let tasks = Arc::new(tasks.to_owned());
Some((buffer, *row, tasks))
}
fn find_enclosing_node_task(
&mut self,
cx: &mut ViewContext<Self>,
) -> Option<(Model<Buffer>, u32, Arc<RunnableTasks>)> {
let snapshot = self.buffer.read(cx).snapshot(cx);
let offset = self.selections.newest::<usize>(cx).head();
let excerpt = snapshot.excerpt_containing(offset..offset)?;
let buffer_id = excerpt.buffer().remote_id();
let layer = excerpt.buffer().syntax_layer_at(offset)?;
let mut cursor = layer.node().walk();
while cursor.goto_first_child_for_byte(offset).is_some() {
if cursor.node().end_byte() == offset {
cursor.goto_next_sibling();
}
}
// Ascend to the smallest ancestor that contains the range and has a task.
loop {
let node = cursor.node();
let node_range = node.byte_range();
let symbol_start_row = excerpt.buffer().offset_to_point(node.start_byte()).row;
// Check if this node contains our offset
if node_range.start <= offset && node_range.end >= offset {
// If it contains offset, check for task
if let Some(tasks) = self.tasks.get(&(buffer_id, symbol_start_row)) {
let buffer = self.buffer.read(cx).buffer(buffer_id)?;
return Some((buffer, symbol_start_row, Arc::new(tasks.to_owned())));
}
}
if !cursor.goto_parent() {
break;
}
}
None
}
fn render_run_indicator(
&self,
_style: &EditorStyle,
@@ -7355,11 +7463,12 @@ impl Editor {
if !selection.is_empty() && !line_mode {
selection.goal = SelectionGoal::None;
}
let (cursor, goal) = movement::up(
let (cursor, goal) = movement::up2(
map,
selection.start,
selection.goal,
false,
false,
text_layout_details,
);
selection.collapse_to(cursor, goal);
@@ -7520,8 +7629,16 @@ impl Editor {
pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext<Self>) {
let text_layout_details = &self.text_layout_details(cx);
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, goal| {
movement::up(map, head, goal, false, text_layout_details)
s.move_with(|map, selection| {
let (head, goal) = movement::up2(
map,
selection.head(),
selection.goal,
false,
!selection.reversed,
text_layout_details,
);
selection.set_head(head, goal);
})
})
}
@@ -8665,14 +8782,22 @@ impl Editor {
let snapshot = this.buffer.read(cx).read(cx);
let empty_str: Arc<str> = Arc::default();
let mut suffixes_inserted = Vec::new();
let ignore_indent = action.ignore_indent;
fn comment_prefix_range(
snapshot: &MultiBufferSnapshot,
row: MultiBufferRow,
comment_prefix: &str,
comment_prefix_whitespace: &str,
ignore_indent: bool,
) -> Range<Point> {
let start = Point::new(row.0, snapshot.indent_size_for_line(row).len);
let indent_size = if ignore_indent {
0
} else {
snapshot.indent_size_for_line(row).len
};
let start = Point::new(row.0, indent_size);
let mut line_bytes = snapshot
.bytes_in_range(start..snapshot.max_point())
@@ -8768,7 +8893,16 @@ impl Editor {
}
// If the language has line comments, toggle those.
let full_comment_prefixes = language.line_comment_prefixes();
let mut full_comment_prefixes = language.line_comment_prefixes().to_vec();
// If ignore_indent is set, trim spaces from the right side of all full_comment_prefixes
if ignore_indent {
full_comment_prefixes = full_comment_prefixes
.into_iter()
.map(|s| Arc::from(s.trim_end()))
.collect();
}
if !full_comment_prefixes.is_empty() {
let first_prefix = full_comment_prefixes
.first()
@@ -8795,6 +8929,7 @@ impl Editor {
row,
&prefix[..trimmed_prefix_len],
&prefix[trimmed_prefix_len..],
ignore_indent,
)
})
.max_by_key(|range| range.end.column - range.start.column)
@@ -8835,6 +8970,7 @@ impl Editor {
start_row,
comment_prefix,
comment_prefix_whitespace,
ignore_indent,
);
let suffix_range = comment_suffix_range(
snapshot.deref(),

View File

@@ -8533,6 +8533,131 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
"});
}
#[gpui::test]
async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let language = Arc::new(Language::new(
LanguageConfig {
line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
));
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
let toggle_comments = &ToggleComments {
advance_downwards: false,
ignore_indent: true,
};
// If multiple selections intersect a line, the line is only toggled once.
cx.set_state(indoc! {"
fn a() {
// «b();
// c();
// ˇ» d();
}
"});
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
cx.assert_editor_state(indoc! {"
fn a() {
«b();
c();
ˇ» d();
}
"});
// The comment prefix is inserted at the beginning of each line
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
cx.assert_editor_state(indoc! {"
fn a() {
// «b();
// c();
// ˇ» d();
}
"});
// If a selection ends at the beginning of a line, that line is not toggled.
cx.set_selections_state(indoc! {"
fn a() {
// b();
// «c();
ˇ»// d();
}
"});
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
cx.assert_editor_state(indoc! {"
fn a() {
// b();
«c();
ˇ»// d();
}
"});
// If a selection span a single line and is empty, the line is toggled.
cx.set_state(indoc! {"
fn a() {
a();
b();
ˇ
}
"});
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
cx.assert_editor_state(indoc! {"
fn a() {
a();
b();
//ˇ
}
"});
// If a selection span multiple lines, empty lines are not toggled.
cx.set_state(indoc! {"
fn a() {
«a();
c();ˇ»
}
"});
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
cx.assert_editor_state(indoc! {"
fn a() {
// «a();
// c();ˇ»
}
"});
// If a selection includes multiple comment prefixes, all lines are uncommented.
cx.set_state(indoc! {"
fn a() {
// «a();
/// b();
//! c();ˇ»
}
"});
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
cx.assert_editor_state(indoc! {"
fn a() {
«a();
b();
c();ˇ»
}
"});
}
#[gpui::test]
async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
@@ -8554,6 +8679,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
let toggle_comments = &ToggleComments {
advance_downwards: true,
ignore_indent: false,
};
// Single cursor on one line -> advance
@@ -13204,6 +13330,89 @@ async fn test_goto_definition_with_find_all_references_fallback(cx: &mut gpui::T
});
}
#[gpui::test]
async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let language = Arc::new(Language::new(
LanguageConfig::default(),
Some(tree_sitter_rust::LANGUAGE.into()),
));
let text = r#"
#[cfg(test)]
mod tests() {
#[test]
fn runnable_1() {
let a = 1;
}
#[test]
fn runnable_2() {
let a = 1;
let b = 2;
}
}
"#
.unindent();
let fs = FakeFs::new(cx.executor());
fs.insert_file("/file.rs", Default::default()).await;
let project = Project::test(fs, ["/a".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 buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx));
let multi_buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx));
let editor = cx.new_view(|cx| {
Editor::new(
EditorMode::Full,
multi_buffer,
Some(project.clone()),
true,
cx,
)
});
editor.update(cx, |editor, cx| {
editor.tasks.insert(
(buffer.read(cx).remote_id(), 3),
RunnableTasks {
templates: vec![],
offset: MultiBufferOffset(43),
column: 0,
extra_variables: HashMap::default(),
context_range: BufferOffset(43)..BufferOffset(85),
},
);
editor.tasks.insert(
(buffer.read(cx).remote_id(), 8),
RunnableTasks {
templates: vec![],
offset: MultiBufferOffset(86),
column: 0,
extra_variables: HashMap::default(),
context_range: BufferOffset(86)..BufferOffset(191),
},
);
// Test finding task when cursor is inside function body
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
});
let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
// Test finding task when cursor is on function name
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
});
let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
assert_eq!(row, 8, "Should find task when cursor is on function name");
});
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
point..point

View File

@@ -449,7 +449,8 @@ impl EditorElement {
register_action(view, cx, Editor::apply_all_diff_hunks);
register_action(view, cx, Editor::apply_selected_diff_hunks);
register_action(view, cx, Editor::open_active_item_in_terminal);
register_action(view, cx, Editor::reload_file)
register_action(view, cx, Editor::reload_file);
register_action(view, cx, Editor::spawn_nearest_task);
}
fn register_key_listeners(&self, cx: &mut WindowContext, layout: &EditorLayout) {

View File

@@ -76,6 +76,26 @@ pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> Displ
map.clip_point(point, Bias::Right)
}
/// Returns a display point for the preceding displayed line (which might be a soft-wrapped line).
pub fn up2(
map: &DisplaySnapshot,
start: DisplayPoint,
goal: SelectionGoal,
preserve_column_at_start: bool,
skip_replace_blocks: bool,
text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
up_by_rows2(
map,
start,
1,
goal,
preserve_column_at_start,
skip_replace_blocks,
text_layout_details,
)
}
/// Returns a display point for the preceding displayed line (which might be a soft-wrapped line).
pub fn up(
map: &DisplaySnapshot,
@@ -112,6 +132,26 @@ pub fn down(
)
}
/// Returns a display point for the next displayed line (which might be a soft-wrapped line).
pub fn down2(
map: &DisplaySnapshot,
start: DisplayPoint,
goal: SelectionGoal,
preserve_column_at_end: bool,
skip_replace_blocks: bool,
text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
down_by_rows2(
map,
start,
1,
goal,
preserve_column_at_end,
skip_replace_blocks,
text_layout_details,
)
}
pub(crate) fn up_by_rows(
map: &DisplaySnapshot,
start: DisplayPoint,
@@ -151,6 +191,46 @@ pub(crate) fn up_by_rows(
)
}
pub(crate) fn up_by_rows2(
map: &DisplaySnapshot,
start: DisplayPoint,
row_count: u32,
goal: SelectionGoal,
preserve_column_at_start: bool,
skip_replace_blocks: bool,
text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
let mut goal_x = match goal {
SelectionGoal::HorizontalPosition(x) => x.into(),
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
SelectionGoal::HorizontalRange { end, .. } => end.into(),
_ => map.x_for_display_point(start, text_layout_details),
};
let prev_row = DisplayRow(start.row().0.saturating_sub(row_count));
let mut point = map.clip_point(
DisplayPoint::new(prev_row, map.line_len(prev_row)),
Bias::Left,
);
if point.row() < start.row() {
*point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
} else if preserve_column_at_start {
return (start, goal);
} else {
point = DisplayPoint::new(DisplayRow(0), 0);
goal_x = px(0.);
}
let mut clipped_point = map.clip_point_2(point, Bias::Left, skip_replace_blocks);
if clipped_point.row() < point.row() {
clipped_point = map.clip_point_2(point, Bias::Right, skip_replace_blocks);
}
(
clipped_point,
SelectionGoal::HorizontalPosition(goal_x.into()),
)
}
pub(crate) fn down_by_rows(
map: &DisplaySnapshot,
start: DisplayPoint,
@@ -187,6 +267,43 @@ pub(crate) fn down_by_rows(
)
}
pub(crate) fn down_by_rows2(
map: &DisplaySnapshot,
start: DisplayPoint,
row_count: u32,
goal: SelectionGoal,
preserve_column_at_end: bool,
skip_replace_blocks: bool,
text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
let mut goal_x = match goal {
SelectionGoal::HorizontalPosition(x) => x.into(),
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
SelectionGoal::HorizontalRange { end, .. } => end.into(),
_ => map.x_for_display_point(start, text_layout_details),
};
let new_row = DisplayRow(start.row().0 + row_count);
let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
if point.row() > start.row() {
*point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
} else if preserve_column_at_end {
return (start, goal);
} else {
point = map.max_point();
goal_x = map.x_for_display_point(point, text_layout_details)
}
let mut clipped_point = map.clip_point_2(point, Bias::Right, skip_replace_blocks);
if clipped_point.row() > point.row() {
clipped_point = map.clip_point_2(point, Bias::Left, skip_replace_blocks);
}
(
clipped_point,
SelectionGoal::HorizontalPosition(goal_x.into()),
)
}
/// Returns a position of the start of line.
/// If `stop_at_soft_boundaries` is true, the returned position is that of the
/// displayed line (e.g. it could actually be in the middle of a text line if that line is soft-wrapped).

View File

@@ -1,6 +1,6 @@
use std::{
cell::Ref,
cmp, iter, mem,
iter, mem,
ops::{Deref, DerefMut, Range, Sub},
sync::Arc,
};
@@ -111,9 +111,9 @@ impl SelectionsCollection {
where
D: 'a + TextDimension + Ord + Sub<D, Output = D>,
{
let map = self.display_map(cx);
let disjoint_anchors = &self.disjoint;
let mut disjoint = resolve_multiple::<D, _>(disjoint_anchors.iter(), &map).peekable();
let mut disjoint =
resolve_multiple::<D, _>(disjoint_anchors.iter(), &self.buffer(cx)).peekable();
let mut pending_opt = self.pending::<D>(cx);
@@ -199,21 +199,21 @@ impl SelectionsCollection {
where
D: 'a + TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
{
let map = self.display_map(cx);
let buffer = self.buffer(cx);
let start_ix = match self
.disjoint
.binary_search_by(|probe| probe.end.cmp(&range.start, &map.buffer_snapshot))
.binary_search_by(|probe| probe.end.cmp(&range.start, &buffer))
{
Ok(ix) | Err(ix) => ix,
};
let end_ix = match self
.disjoint
.binary_search_by(|probe| probe.start.cmp(&range.end, &map.buffer_snapshot))
.binary_search_by(|probe| probe.start.cmp(&range.end, &buffer))
{
Ok(ix) => ix + 1,
Err(ix) => ix,
};
resolve_multiple(&self.disjoint[start_ix..end_ix], &map).collect()
resolve_multiple(&self.disjoint[start_ix..end_ix], &buffer).collect()
}
pub fn all_display(
@@ -538,9 +538,9 @@ impl<'a> MutableSelectionsCollection<'a> {
}
pub fn select_anchors(&mut self, selections: Vec<Selection<Anchor>>) {
let map = self.display_map();
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
let resolved_selections =
resolve_multiple::<usize, _>(&selections, &map).collect::<Vec<_>>();
resolve_multiple::<usize, _>(&selections, &buffer).collect::<Vec<_>>();
self.select(resolved_selections);
}
@@ -804,8 +804,8 @@ impl<'a> MutableSelectionsCollection<'a> {
.collect();
if !adjusted_disjoint.is_empty() {
let map = self.display_map();
let resolved_selections = resolve_multiple(adjusted_disjoint.iter(), &map).collect();
let resolved_selections =
resolve_multiple(adjusted_disjoint.iter(), &self.buffer()).collect();
self.select::<usize>(resolved_selections);
}
@@ -851,55 +851,25 @@ impl<'a> DerefMut for MutableSelectionsCollection<'a> {
// Panics if passed selections are not in order
pub(crate) fn resolve_multiple<'a, D, I>(
selections: I,
map: &'a DisplaySnapshot,
snapshot: &MultiBufferSnapshot,
) -> impl 'a + Iterator<Item = Selection<D>>
where
D: TextDimension + Clone + Ord + Sub<D, Output = D>,
D: TextDimension + Ord + Sub<D, Output = D>,
I: 'a + IntoIterator<Item = &'a Selection<Anchor>>,
{
let (to_summarize, selections) = selections.into_iter().tee();
let mut summaries = map
.buffer_snapshot
.summaries_for_anchors::<Point, _>(to_summarize.flat_map(|s| [&s.start, &s.end]))
let mut summaries = snapshot
.summaries_for_anchors::<D, _>(
to_summarize
.flat_map(|s| [&s.start, &s.end])
.collect::<Vec<_>>(),
)
.into_iter();
let mut selection_endpoints = map.buffer_snapshot.dimensions_from_points::<D>(
iter::from_fn(move || {
let start = map.display_point_to_point(
map.point_to_display_point(summaries.next().unwrap(), Bias::Left),
Bias::Left,
);
let end = map.display_point_to_point(
map.point_to_display_point(summaries.next().unwrap(), Bias::Right),
Bias::Right,
);
Some([start, end])
})
.flatten(),
);
let mut selections = selections
.map(move |s| {
let start = selection_endpoints.next().unwrap();
let end = selection_endpoints.next().unwrap();
Selection {
id: s.id,
start,
end,
reversed: s.reversed,
goal: s.goal,
}
})
.peekable();
iter::from_fn(move || {
let mut selection = selections.next()?;
while let Some(next_selection) = selections.peek() {
if selection.end >= next_selection.start {
selection.end = cmp::max(selection.end, next_selection.end.clone());
selections.next();
} else {
break;
}
}
Some(selection)
selections.map(move |s| Selection {
id: s.id,
start: summaries.next().unwrap(),
end: summaries.next().unwrap(),
reversed: s.reversed,
goal: s.goal,
})
}

View File

@@ -77,9 +77,9 @@ impl GitHostingProvider for Gitlab {
return None;
}
let mut path_segments = url.path_segments()?;
let owner = path_segments.next()?;
let repo = path_segments.next()?.trim_end_matches(".git");
let mut path_segments = url.path_segments()?.collect::<Vec<_>>();
let repo = path_segments.pop()?.trim_end_matches(".git");
let owner = path_segments.join("/");
Some(ParsedGitRemote {
owner: owner.into(),
@@ -178,6 +178,23 @@ mod tests {
);
}
#[test]
fn test_parse_remote_url_given_self_hosted_https_url_with_subgroup() {
let remote_url = "https://gitlab.my-enterprise.com/group/subgroup/zed.git";
let parsed_remote = Gitlab::from_remote_url(remote_url)
.unwrap()
.parse_remote_url(remote_url)
.unwrap();
assert_eq!(
parsed_remote,
ParsedGitRemote {
owner: "group/subgroup".into(),
repo: "zed".into(),
}
);
}
#[test]
fn test_build_gitlab_permalink() {
let permalink = Gitlab::new().build_permalink(

View File

@@ -75,6 +75,18 @@ impl Keymap {
.filter(move |binding| binding.action().partial_eq(action))
}
/// all bindings for input returns all bindings that might match the input
/// (without checking context)
pub fn all_bindings_for_input(&self, input: &[Keystroke]) -> Vec<KeyBinding> {
self.bindings()
.rev()
.filter_map(|binding| {
binding.match_keystrokes(input).filter(|pending| !pending)?;
Some(binding.clone())
})
.collect()
}
/// bindings_for_input returns a list of bindings that match the given input,
/// and a boolean indicating whether or not more bindings might match if
/// the input was longer.

View File

@@ -69,6 +69,11 @@ impl KeyBinding {
pub fn action(&self) -> &dyn Action {
self.action.as_ref()
}
/// Get the predicate used to match this binding
pub fn predicate(&self) -> Option<&KeyBindingContextPredicate> {
self.context_predicate.as_ref()
}
}
impl std::fmt::Debug for KeyBinding {

View File

@@ -11,9 +11,12 @@ use std::fmt;
pub struct KeyContext(SmallVec<[ContextEntry; 1]>);
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
struct ContextEntry {
key: SharedString,
value: Option<SharedString>,
/// An entry in a KeyContext
pub struct ContextEntry {
/// The key (or name if no value)
pub key: SharedString,
/// The value
pub value: Option<SharedString>,
}
impl<'a> TryFrom<&'a str> for KeyContext {
@@ -39,6 +42,17 @@ impl KeyContext {
context
}
/// Returns the primary context entry (usually the name of the component)
pub fn primary(&self) -> Option<&ContextEntry> {
self.0.iter().find(|p| p.value.is_none())
}
/// Returns everything except the primary context entry.
pub fn secondary(&self) -> impl Iterator<Item = &ContextEntry> {
let primary = self.primary();
self.0.iter().filter(move |&p| Some(p) != primary)
}
/// Parse a key context from a string.
/// The key context format is very simple:
/// - either a single identifier, such as `StatusBar`
@@ -178,6 +192,20 @@ pub enum KeyBindingContextPredicate {
),
}
impl fmt::Display for KeyBindingContextPredicate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Identifier(name) => write!(f, "{}", name),
Self::Equal(left, right) => write!(f, "{} == {}", left, right),
Self::NotEqual(left, right) => write!(f, "{} != {}", left, right),
Self::Not(pred) => write!(f, "!{}", pred),
Self::Child(parent, child) => write!(f, "{} > {}", parent, child),
Self::And(left, right) => write!(f, "({} && {})", left, right),
Self::Or(left, right) => write!(f, "({} || {})", left, right),
}
}
}
impl KeyBindingContextPredicate {
/// Parse a string in the same format as the keymap's context field.
///

View File

@@ -121,6 +121,32 @@ impl Keystroke {
})
}
/// Produces a representation of this key that Parse can understand.
pub fn unparse(&self) -> String {
let mut str = String::new();
if self.modifiers.control {
str.push_str("ctrl-");
}
if self.modifiers.alt {
str.push_str("alt-");
}
if self.modifiers.platform {
#[cfg(target_os = "macos")]
str.push_str("cmd-");
#[cfg(target_os = "linux")]
str.push_str("super-");
#[cfg(target_os = "windows")]
str.push_str("win-");
}
if self.modifiers.shift {
str.push_str("shift-");
}
str.push_str(&self.key);
str
}
/// Returns true if this keystroke left
/// the ime system in an incomplete state.
pub fn is_ime_in_progress(&self) -> bool {

View File

@@ -3324,17 +3324,18 @@ impl<'a> WindowContext<'a> {
return;
}
self.pending_input_changed();
self.propagate_event = true;
for binding in match_result.bindings {
self.dispatch_action_on_node(node_id, binding.action.as_ref());
if !self.propagate_event {
self.dispatch_keystroke_observers(event, Some(binding.action));
self.pending_input_changed();
return;
}
}
self.finish_dispatch_key_event(event, dispatch_path)
self.finish_dispatch_key_event(event, dispatch_path);
self.pending_input_changed();
}
fn finish_dispatch_key_event(
@@ -3664,6 +3665,22 @@ impl<'a> WindowContext<'a> {
receiver
}
/// Returns the current context stack.
pub fn context_stack(&self) -> Vec<KeyContext> {
let dispatch_tree = &self.window.rendered_frame.dispatch_tree;
let node_id = self
.window
.focus
.and_then(|focus_id| dispatch_tree.focusable_node_id(focus_id))
.unwrap_or_else(|| dispatch_tree.root_node_id());
dispatch_tree
.dispatch_path(node_id)
.iter()
.filter_map(move |&node_id| dispatch_tree.node(node_id).context.clone())
.collect()
}
/// Returns all available actions for the focused element.
pub fn available_actions(&self) -> Vec<Box<dyn Action>> {
let node_id = self
@@ -3704,6 +3721,11 @@ impl<'a> WindowContext<'a> {
)
}
/// Returns key bindings that invoke the given action on the currently focused element.
pub fn all_bindings_for_input(&self, input: &[Keystroke]) -> Vec<KeyBinding> {
RefCell::borrow(&self.keymap).all_bindings_for_input(input)
}
/// Returns any bindings that would invoke the given action on the given focus handle if it were focused.
pub fn bindings_for_action_in(
&self,

View File

@@ -19,6 +19,7 @@ copilot.workspace = true
editor.workspace = true
futures.workspace = true
gpui.workspace = true
itertools.workspace = true
language.workspace = true
lsp.workspace = true
project.workspace = true
@@ -28,6 +29,7 @@ theme.workspace = true
tree-sitter.workspace = true
ui.workspace = true
workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }

View File

@@ -0,0 +1,280 @@
use gpui::{
actions, Action, AppContext, EventEmitter, FocusHandle, FocusableView,
KeyBindingContextPredicate, KeyContext, Keystroke, MouseButton, Render, Subscription,
};
use itertools::Itertools;
use serde_json::json;
use ui::{
div, h_flex, px, v_flex, ButtonCommon, Clickable, FluentBuilder, InteractiveElement, Label,
LabelCommon, LabelSize, ParentElement, SharedString, StatefulInteractiveElement, Styled,
ViewContext, VisualContext, WindowContext,
};
use ui::{Button, ButtonStyle};
use workspace::Item;
use workspace::Workspace;
actions!(debug, [OpenKeyContextView]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, _| {
workspace.register_action(|workspace, _: &OpenKeyContextView, cx| {
let key_context_view = cx.new_view(KeyContextView::new);
workspace.add_item_to_active_pane(Box::new(key_context_view), None, true, cx)
});
})
.detach();
}
struct KeyContextView {
pending_keystrokes: Option<Vec<Keystroke>>,
last_keystrokes: Option<SharedString>,
last_possibilities: Vec<(SharedString, SharedString, Option<bool>)>,
context_stack: Vec<KeyContext>,
focus_handle: FocusHandle,
_subscriptions: [Subscription; 2],
}
impl KeyContextView {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
let sub1 = cx.observe_keystrokes(|this, e, cx| {
let mut pending = this.pending_keystrokes.take().unwrap_or_default();
pending.push(e.keystroke.clone());
let mut possibilities = cx.all_bindings_for_input(&pending);
possibilities.reverse();
this.context_stack = cx.context_stack();
this.last_keystrokes = Some(
json!(pending.iter().map(|p| p.unparse()).join(" "))
.to_string()
.into(),
);
this.last_possibilities = possibilities
.into_iter()
.map(|binding| {
let match_state = if let Some(predicate) = binding.predicate() {
if this.matches(predicate) {
if this.action_matches(&e.action, binding.action()) {
Some(true)
} else {
Some(false)
}
} else {
None
}
} else {
if this.action_matches(&e.action, binding.action()) {
Some(true)
} else {
Some(false)
}
};
let predicate = if let Some(predicate) = binding.predicate() {
format!("{}", predicate)
} else {
"".to_string()
};
let mut name = binding.action().name();
if name == "zed::NoAction" {
name = "(null)"
}
(
name.to_owned().into(),
json!(predicate).to_string().into(),
match_state,
)
})
.collect();
});
let sub2 = cx.observe_pending_input(|this, cx| {
this.pending_keystrokes = cx
.pending_input_keystrokes()
.map(|k| k.iter().cloned().collect());
if this.pending_keystrokes.is_some() {
this.last_keystrokes.take();
}
cx.notify();
});
Self {
context_stack: Vec::new(),
pending_keystrokes: None,
last_keystrokes: None,
last_possibilities: Vec::new(),
focus_handle: cx.focus_handle(),
_subscriptions: [sub1, sub2],
}
}
}
impl EventEmitter<()> for KeyContextView {}
impl FocusableView for KeyContextView {
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl KeyContextView {
fn set_context_stack(&mut self, stack: Vec<KeyContext>, cx: &mut ViewContext<Self>) {
self.context_stack = stack;
cx.notify()
}
fn matches(&self, predicate: &KeyBindingContextPredicate) -> bool {
let mut stack = self.context_stack.clone();
while !stack.is_empty() {
if predicate.eval(&stack) {
return true;
}
stack.pop();
}
false
}
fn action_matches(&self, a: &Option<Box<dyn Action>>, b: &dyn Action) -> bool {
if let Some(last_action) = a {
last_action.partial_eq(b)
} else {
b.name() == "zed::NoAction"
}
}
}
impl Item for KeyContextView {
type Event = ();
fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {}
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
Some("Keyboard Context".into())
}
fn telemetry_event_text(&self) -> Option<&'static str> {
None
}
fn clone_on_split(
&self,
_workspace_id: Option<workspace::WorkspaceId>,
cx: &mut ViewContext<Self>,
) -> Option<gpui::View<Self>>
where
Self: Sized,
{
Some(cx.new_view(Self::new))
}
}
impl Render for KeyContextView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl ui::IntoElement {
use itertools::Itertools;
v_flex()
.id("key-context-view")
.overflow_scroll()
.size_full()
.max_h_full()
.pt_4()
.pl_4()
.track_focus(&self.focus_handle)
.key_context("KeyContextView")
.on_mouse_up_out(
MouseButton::Left,
cx.listener(|this, _, cx| {
this.last_keystrokes.take();
this.set_context_stack(cx.context_stack(), cx);
}),
)
.on_mouse_up_out(
MouseButton::Right,
cx.listener(|_, _, cx| {
cx.defer(|this, cx| {
this.last_keystrokes.take();
this.set_context_stack(cx.context_stack(), cx);
});
}),
)
.child(Label::new("Keyboard Context").size(LabelSize::Large))
.child(Label::new("This view lets you determine the current context stack for creating custom key bindings in Zed. When a keyboard shortcut is triggered, it also shows all the possible contexts it could have triggered in, and which one matched."))
.child(
h_flex()
.mt_4()
.gap_4()
.child(
Button::new("default", "Open Documentation")
.style(ButtonStyle::Filled)
.on_click(|_, cx| cx.open_url("https://zed.dev/docs/key-bindings")),
)
.child(
Button::new("default", "View default keymap")
.style(ButtonStyle::Filled)
.key_binding(ui::KeyBinding::for_action(
&zed_actions::OpenDefaultKeymap,
cx,
))
.on_click(|_, cx| {
cx.dispatch_action(workspace::SplitRight.boxed_clone());
cx.dispatch_action(zed_actions::OpenDefaultKeymap.boxed_clone());
}),
)
.child(
Button::new("default", "Edit your keymap")
.style(ButtonStyle::Filled)
.key_binding(ui::KeyBinding::for_action(&zed_actions::OpenKeymap, cx))
.on_click(|_, cx| {
cx.dispatch_action(workspace::SplitRight.boxed_clone());
cx.dispatch_action(zed_actions::OpenKeymap.boxed_clone());
}),
),
)
.child(
Label::new("Current Context Stack")
.size(LabelSize::Large)
.mt_8(),
)
.children({
cx.context_stack().iter().enumerate().map(|(i, context)| {
let primary = context.primary().map(|e| e.key.clone()).unwrap_or_default();
let secondary = context
.secondary()
.map(|e| {
if let Some(value) = e.value.as_ref() {
format!("{}={}", e.key, value)
} else {
e.key.to_string()
}
})
.join(" ");
Label::new(format!("{} {}", primary, secondary)).ml(px(12. * (i + 1) as f32))
})
})
.child(Label::new("Last Keystroke").mt_4().size(LabelSize::Large))
.when_some(self.pending_keystrokes.as_ref(), |el, keystrokes| {
el.child(
Label::new(format!(
"Waiting for more input: {}",
keystrokes.iter().map(|k| k.unparse()).join(" ")
))
.ml(px(12.)),
)
})
.when_some(self.last_keystrokes.as_ref(), |el, keystrokes| {
el.child(Label::new(format!("Typed: {}", keystrokes)).ml_4())
.children(
self.last_possibilities
.iter()
.map(|(name, predicate, state)| {
let (text, color) = match state {
Some(true) => ("(match)", ui::Color::Success),
Some(false) => ("(low precedence)", ui::Color::Hint),
None => ("(no match)", ui::Color::Error),
};
h_flex()
.gap_2()
.ml_8()
.child(div().min_w(px(200.)).child(Label::new(name.clone())))
.child(Label::new(predicate.clone()))
.child(Label::new(text).color(color))
}),
)
})
}
}

View File

@@ -1,3 +1,4 @@
mod key_context_view;
mod lsp_log;
mod syntax_tree_view;
@@ -12,4 +13,5 @@ pub use syntax_tree_view::{SyntaxTreeToolbarItemView, SyntaxTreeView};
pub fn init(cx: &mut AppContext) {
lsp_log::init(cx);
syntax_tree_view::init(cx);
key_context_view::init(cx);
}

View File

@@ -3083,58 +3083,6 @@ impl MultiBufferSnapshot {
summaries
}
pub fn dimensions_from_points<'a, D>(
&'a self,
points: impl 'a + IntoIterator<Item = Point>,
) -> impl 'a + Iterator<Item = D>
where
D: TextDimension,
{
let mut cursor = self.excerpts.cursor::<TextSummary>(&());
let mut memoized_source_start: Option<Point> = None;
let mut points = points.into_iter();
std::iter::from_fn(move || {
let point = points.next()?;
// Clear the memoized source start if the point is in a different excerpt than previous.
if memoized_source_start.map_or(false, |_| point >= cursor.end(&()).lines) {
memoized_source_start = None;
}
// Now determine where the excerpt containing the point starts in its source buffer.
// We'll use this value to calculate overshoot next.
let source_start = if let Some(source_start) = memoized_source_start {
source_start
} else {
cursor.seek_forward(&point, Bias::Right, &());
if let Some(excerpt) = cursor.item() {
let source_start = excerpt.range.context.start.to_point(&excerpt.buffer);
memoized_source_start = Some(source_start);
source_start
} else {
return Some(D::from_text_summary(cursor.start()));
}
};
// First, assume the output dimension is at least the start of the excerpt containing the point
let mut output = D::from_text_summary(cursor.start());
// If the point lands within its excerpt, calculate and add the overshoot in dimension D.
if let Some(excerpt) = cursor.item() {
let overshoot = point - cursor.start().lines;
if !overshoot.is_zero() {
let end_in_excerpt = source_start + overshoot;
output.add_assign(
&excerpt
.buffer
.text_summary_for_range::<D, _>(source_start..end_in_excerpt),
);
}
}
Some(output)
})
}
pub fn refresh_anchors<'a, I>(&'a self, anchors: I) -> Vec<(usize, Anchor, bool)>
where
I: 'a + IntoIterator<Item = &'a Anchor>,
@@ -4758,12 +4706,6 @@ impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for usize {
}
}
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, TextSummary> for Point {
fn cmp(&self, cursor_location: &TextSummary, _: &()) -> cmp::Ordering {
Ord::cmp(self, &cursor_location.lines)
}
}
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, Option<&'a Locator>> for Locator {
fn cmp(&self, cursor_location: &Option<&'a Locator>, _: &()) -> cmp::Ordering {
Ord::cmp(&Some(self), cursor_location)

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
use editor::ShowScrollbar;
use gpui::Pixels;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -29,6 +30,23 @@ pub struct OutlinePanelSettings {
pub indent_guides: IndentGuidesSettings,
pub auto_reveal_entries: bool,
pub auto_fold_dirs: bool,
pub scrollbar: ScrollbarSettings,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct ScrollbarSettings {
/// When to show the scrollbar in the project panel.
///
/// Default: inherits editor scrollbar settings
pub show: Option<ShowScrollbar>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct ScrollbarSettingsContent {
/// When to show the scrollbar in the project panel.
///
/// Default: inherits editor scrollbar settings
pub show: Option<Option<ShowScrollbar>>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -85,6 +103,8 @@ pub struct OutlinePanelSettingsContent {
pub auto_fold_dirs: Option<bool>,
/// Settings related to indent guides in the outline panel.
pub indent_guides: Option<IndentGuidesSettingsContent>,
/// Scrollbar-related settings
pub scrollbar: Option<ScrollbarSettingsContent>,
}
impl Settings for OutlinePanelSettings {

View File

@@ -5270,6 +5270,10 @@ impl LspStore {
self.last_formatting_failure.as_deref()
}
pub fn reset_last_formatting_failure(&mut self) {
self.last_formatting_failure = None;
}
pub fn environment_for_buffer(
&self,
buffer: &Model<Buffer>,

View File

@@ -2414,6 +2414,11 @@ impl Project {
self.lsp_store.read(cx).last_formatting_failure()
}
pub fn reset_last_formatting_failure(&self, cx: &mut AppContext) {
self.lsp_store
.update(cx, |store, _| store.reset_last_formatting_failure());
}
pub fn update_diagnostics(
&mut self,
language_server_id: LanguageServerId,

View File

@@ -257,7 +257,6 @@ message Envelope {
FindSearchCandidatesResponse find_search_candidates_response = 244;
CloseBuffer close_buffer = 245;
UpdateUserSettings update_user_settings = 246;
ShutdownRemoteServer shutdown_remote_server = 257;
@@ -309,6 +308,7 @@ message Envelope {
reserved 205 to 206;
reserved 221;
reserved 224 to 229;
reserved 246;
reserved 247 to 254;
reserved 255 to 256;
}
@@ -2361,17 +2361,6 @@ message AddWorktreeResponse {
string canonicalized_path = 2;
}
message UpdateUserSettings {
uint64 project_id = 1;
string content = 2;
optional Kind kind = 3;
enum Kind {
Settings = 0;
Tasks = 1;
}
}
message GetPathMetadata {
uint64 project_id = 1;
string path = 2;

View File

@@ -342,7 +342,6 @@ messages!(
(FindSearchCandidates, Background),
(FindSearchCandidatesResponse, Background),
(CloseBuffer, Foreground),
(UpdateUserSettings, Foreground),
(ShutdownRemoteServer, Foreground),
(RemoveWorktree, Foreground),
(LanguageServerLog, Foreground),
@@ -559,7 +558,6 @@ entity_messages!(
UpdateContext,
SynchronizeContexts,
LspExtSwitchSourceHeader,
UpdateUserSettings,
LanguageServerLog,
Toast,
HideToast,

View File

@@ -13,8 +13,7 @@ use gpui::{AppContext, Model};
use language::CursorShape;
use markdown::{Markdown, MarkdownStyle};
use release_channel::{AppVersion, ReleaseChannel};
use remote::ssh_session::{ServerBinary, ServerVersion};
use release_channel::ReleaseChannel;
use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -441,23 +440,66 @@ impl remote::SshClientDelegate for SshClientDelegate {
self.update_status(status, cx)
}
fn get_server_binary(
fn download_server_binary_locally(
&self,
platform: SshPlatform,
upload_binary_over_ssh: bool,
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
cx: &mut AsyncAppContext,
) -> oneshot::Receiver<Result<(ServerBinary, ServerVersion)>> {
let (tx, rx) = oneshot::channel();
let this = self.clone();
) -> Task<anyhow::Result<PathBuf>> {
cx.spawn(|mut cx| async move {
tx.send(
this.get_server_binary_impl(platform, upload_binary_over_ssh, &mut cx)
.await,
let binary_path = AutoUpdater::download_remote_server_release(
platform.os,
platform.arch,
release_channel,
version,
&mut cx,
)
.ok();
.await
.map_err(|e| {
anyhow!(
"Failed to download remote server binary (version: {}, os: {}, arch: {}): {}",
version
.map(|v| format!("{}", v))
.unwrap_or("unknown".to_string()),
platform.os,
platform.arch,
e
)
})?;
Ok(binary_path)
})
.detach();
rx
}
fn get_download_params(
&self,
platform: SshPlatform,
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
cx: &mut AsyncAppContext,
) -> Task<Result<(String, String)>> {
cx.spawn(|mut cx| async move {
let (release, request_body) = AutoUpdater::get_remote_server_release_url(
platform.os,
platform.arch,
release_channel,
version,
&mut cx,
)
.await
.map_err(|e| {
anyhow!(
"Failed to get remote server binary download url (version: {}, os: {}, arch: {}): {}",
version.map(|v| format!("{}", v)).unwrap_or("unknown".to_string()),
platform.os,
platform.arch,
e
)
})?;
Ok((release.url, request_body))
}
)
}
fn remote_server_binary_path(
@@ -485,208 +527,6 @@ impl SshClientDelegate {
})
.ok();
}
async fn get_server_binary_impl(
&self,
platform: SshPlatform,
upload_binary_via_ssh: bool,
cx: &mut AsyncAppContext,
) -> Result<(ServerBinary, ServerVersion)> {
let (version, release_channel) = cx.update(|cx| {
let version = AppVersion::global(cx);
let channel = ReleaseChannel::global(cx);
(version, channel)
})?;
// In dev mode, build the remote server binary from source
#[cfg(debug_assertions)]
if release_channel == ReleaseChannel::Dev {
let result = self.build_local(cx, platform, version).await?;
// Fall through to a remote binary if we're not able to compile a local binary
if let Some((path, version)) = result {
return Ok((
ServerBinary::LocalBinary(path),
ServerVersion::Semantic(version),
));
}
}
// For nightly channel, always get latest
let current_version = if release_channel == ReleaseChannel::Nightly {
None
} else {
Some(version)
};
self.update_status(
Some(&format!("Checking remote server release {}", version)),
cx,
);
if upload_binary_via_ssh {
let binary_path = AutoUpdater::download_remote_server_release(
platform.os,
platform.arch,
release_channel,
current_version,
cx,
)
.await
.map_err(|e| {
anyhow!(
"Failed to download remote server binary (version: {}, os: {}, arch: {}): {}",
version,
platform.os,
platform.arch,
e
)
})?;
Ok((
ServerBinary::LocalBinary(binary_path),
ServerVersion::Semantic(version),
))
} else {
let (release, request_body) = AutoUpdater::get_remote_server_release_url(
platform.os,
platform.arch,
release_channel,
current_version,
cx,
)
.await
.map_err(|e| {
anyhow!(
"Failed to get remote server binary download url (version: {}, os: {}, arch: {}): {}",
version,
platform.os,
platform.arch,
e
)
})?;
let version = release
.version
.parse::<SemanticVersion>()
.map(ServerVersion::Semantic)
.unwrap_or_else(|_| ServerVersion::Commit(release.version));
Ok((
ServerBinary::ReleaseUrl {
url: release.url,
body: request_body,
},
version,
))
}
}
#[cfg(debug_assertions)]
async fn build_local(
&self,
cx: &mut AsyncAppContext,
platform: SshPlatform,
version: gpui::SemanticVersion,
) -> Result<Option<(PathBuf, gpui::SemanticVersion)>> {
use smol::process::{Command, Stdio};
async fn run_cmd(command: &mut Command) -> Result<()> {
let output = command
.kill_on_drop(true)
.stderr(Stdio::inherit())
.output()
.await?;
if !output.status.success() {
Err(anyhow!("Failed to run command: {:?}", command))?;
}
Ok(())
}
if platform.arch == std::env::consts::ARCH && platform.os == std::env::consts::OS {
self.update_status(Some("Building remote server binary from source"), cx);
log::info!("building remote server binary from source");
run_cmd(Command::new("cargo").args([
"build",
"--package",
"remote_server",
"--features",
"debug-embed",
"--target-dir",
"target/remote_server",
]))
.await?;
self.update_status(Some("Compressing binary"), cx);
run_cmd(Command::new("gzip").args([
"-9",
"-f",
"target/remote_server/debug/remote_server",
]))
.await?;
let path = std::env::current_dir()?.join("target/remote_server/debug/remote_server.gz");
return Ok(Some((path, version)));
} else if let Some(triple) = platform.triple() {
smol::fs::create_dir_all("target/remote_server").await?;
self.update_status(Some("Installing cross.rs for cross-compilation"), cx);
log::info!("installing cross");
run_cmd(Command::new("cargo").args([
"install",
"cross",
"--git",
"https://github.com/cross-rs/cross",
]))
.await?;
self.update_status(
Some(&format!(
"Building remote server binary from source for {} with Docker",
&triple
)),
cx,
);
log::info!("building remote server binary from source for {}", &triple);
run_cmd(
Command::new("cross")
.args([
"build",
"--package",
"remote_server",
"--features",
"debug-embed",
"--target-dir",
"target/remote_server",
"--target",
&triple,
])
.env(
"CROSS_CONTAINER_OPTS",
"--mount type=bind,src=./target,dst=/app/target",
),
)
.await?;
self.update_status(Some("Compressing binary"), cx);
run_cmd(Command::new("gzip").args([
"-9",
"-f",
&format!("target/remote_server/{}/debug/remote_server", triple),
]))
.await?;
let path = std::env::current_dir()?.join(format!(
"target/remote_server/{}/debug/remote_server.gz",
triple
));
return Ok(Some((path, version)));
} else {
return Ok(None);
}
}
}
pub fn is_connecting_over_ssh(workspace: &Workspace, cx: &AppContext) -> bool {

View File

@@ -35,6 +35,7 @@ smol.workspace = true
tempfile.workspace = true
thiserror.workspace = true
util.workspace = true
release_channel.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }

View File

@@ -21,6 +21,7 @@ use gpui::{
ModelContext, SemanticVersion, Task, WeakModel,
};
use parking_lot::Mutex;
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use rpc::{
proto::{self, build_typed_envelope, Envelope, EnvelopedMessage, PeerId, RequestMessage},
AnyProtoClient, EntityMessageSubscriber, ErrorExt, ProtoClient, ProtoMessageHandlerSet,
@@ -227,10 +228,19 @@ pub enum ServerBinary {
ReleaseUrl { url: String, body: String },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ServerVersion {
Semantic(SemanticVersion),
Commit(String),
}
impl ServerVersion {
pub fn semantic_version(&self) -> Option<SemanticVersion> {
match self {
Self::Semantic(version) => Some(*version),
_ => None,
}
}
}
impl std::fmt::Display for ServerVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -252,12 +262,21 @@ pub trait SshClientDelegate: Send + Sync {
platform: SshPlatform,
cx: &mut AsyncAppContext,
) -> Result<PathBuf>;
fn get_server_binary(
fn get_download_params(
&self,
platform: SshPlatform,
upload_binary_over_ssh: bool,
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
cx: &mut AsyncAppContext,
) -> oneshot::Receiver<Result<(ServerBinary, ServerVersion)>>;
) -> Task<Result<(String, String)>>;
fn download_server_binary_locally(
&self,
platform: SshPlatform,
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
cx: &mut AsyncAppContext,
) -> Task<Result<PathBuf>>;
fn set_status(&self, status: Option<&str>, cx: &mut AsyncAppContext);
}
@@ -1727,86 +1746,123 @@ impl SshRemoteConnection {
platform: SshPlatform,
cx: &mut AsyncAppContext,
) -> Result<()> {
if std::env::var("ZED_USE_CACHED_REMOTE_SERVER").is_ok() {
if let Ok(installed_version) =
run_cmd(self.socket.ssh_command(dst_path).arg("version")).await
{
log::info!("using cached server binary version {}", installed_version);
return Ok(());
}
}
if cfg!(not(debug_assertions)) {
// When we're not in dev mode, we don't want to switch out the binary if it's
// still open.
// In dev mode, that's fine, since we often kill Zed processes with Ctrl-C and want
// to still replace the binary.
if self.is_binary_in_use(dst_path).await? {
log::info!("server binary is opened by another process. not updating");
delegate.set_status(
Some("Skipping update of remote development server, since it's still in use"),
cx,
);
return Ok(());
}
}
let upload_binary_over_ssh = self.socket.connection_options.upload_binary_over_ssh;
let (binary, new_server_version) = delegate
.get_server_binary(platform, upload_binary_over_ssh, cx)
.await??;
if cfg!(not(debug_assertions)) {
let installed_version = if let Ok(version_output) =
run_cmd(self.socket.ssh_command(dst_path).arg("version")).await
{
let current_version = match run_cmd(self.socket.ssh_command(dst_path).arg("version")).await
{
Ok(version_output) => {
if let Ok(version) = version_output.trim().parse::<SemanticVersion>() {
Some(ServerVersion::Semantic(version))
} else {
Some(ServerVersion::Commit(version_output.trim().to_string()))
}
} else {
None
}
Err(_) => None,
};
let (release_channel, wanted_version) = cx.update(|cx| {
let release_channel = ReleaseChannel::global(cx);
let wanted_version = match release_channel {
ReleaseChannel::Nightly => {
AppCommitSha::try_global(cx).map(|sha| ServerVersion::Commit(sha.0))
}
ReleaseChannel::Dev => None,
_ => Some(ServerVersion::Semantic(AppVersion::global(cx))),
};
(release_channel, wanted_version)
})?;
if let Some(installed_version) = installed_version {
use ServerVersion::*;
match (installed_version, new_server_version) {
(Semantic(installed), Semantic(new)) if installed == new => {
log::info!("remote development server present and matching client version");
return Ok(());
}
(Semantic(installed), Semantic(new)) if installed > new => {
let error = anyhow!("The version of the remote server ({}) is newer than the Zed version ({}). Please update Zed.", installed, new);
return Err(error);
}
(Commit(installed), Commit(new)) if installed == new => {
log::info!(
"remote development server present and matching client version {}",
installed
);
return Ok(());
}
(installed, _) => {
log::info!(
"remote development server has version: {}. updating...",
installed
);
}
match (&current_version, &wanted_version) {
(Some(current), Some(wanted)) if current == wanted => {
log::info!("remote development server present and matching client version");
return Ok(());
}
(Some(ServerVersion::Semantic(current)), Some(ServerVersion::Semantic(wanted)))
if current > wanted =>
{
anyhow::bail!("The version of the remote server ({}) is newer than the Zed version ({}). Please update Zed.", current, wanted);
}
_ => {
log::info!("Installing remote development server");
}
}
if self.is_binary_in_use(dst_path).await? {
// When we're not in dev mode, we don't want to switch out the binary if it's
// still open.
// In dev mode, that's fine, since we often kill Zed processes with Ctrl-C and want
// to still replace the binary.
if cfg!(not(debug_assertions)) {
anyhow::bail!("The remote server version ({:?}) does not match the wanted version ({:?}), but is in use by another Zed client so cannot be upgraded.", &current_version, &wanted_version)
} else {
log::info!("Binary is currently in use, ignoring because this is a dev build")
}
}
if wanted_version.is_none() {
if std::env::var("ZED_BUILD_REMOTE_SERVER").is_err() {
if let Some(current_version) = current_version {
log::warn!(
"In development, using cached remote server binary version ({})",
current_version
);
return Ok(());
} else {
anyhow::bail!(
"ZED_BUILD_REMOTE_SERVER is not set, but no remote server exists at ({:?})",
dst_path
)
}
}
#[cfg(debug_assertions)]
{
let src_path = self.build_local(platform, delegate, cx).await?;
return self
.upload_local_server_binary(&src_path, dst_path, delegate, cx)
.await;
}
#[cfg(not(debug_assertions))]
anyhow::bail!("Running development build in release mode, cannot cross compile (unset ZED_BUILD_REMOTE_SERVER)")
}
let upload_binary_over_ssh = self.socket.connection_options.upload_binary_over_ssh;
if !upload_binary_over_ssh {
let (url, body) = delegate
.get_download_params(
platform,
release_channel,
wanted_version.clone().and_then(|v| v.semantic_version()),
cx,
)
.await?;
match self
.download_binary_on_server(&url, &body, dst_path, delegate, cx)
.await
{
Ok(_) => return Ok(()),
Err(e) => {
log::error!(
"Failed to download binary on server, attempting to upload server: {}",
e
)
}
}
}
match binary {
ServerBinary::LocalBinary(src_path) => {
self.upload_local_server_binary(&src_path, dst_path, delegate, cx)
.await
}
ServerBinary::ReleaseUrl { url, body } => {
self.download_binary_on_server(&url, &body, dst_path, delegate, cx)
.await
}
}
let src_path = delegate
.download_server_binary_locally(
platform,
release_channel,
wanted_version.and_then(|v| v.semantic_version()),
cx,
)
.await?;
self.upload_local_server_binary(&src_path, dst_path, delegate, cx)
.await
}
async fn is_binary_in_use(&self, binary_path: &Path) -> Result<bool> {
@@ -1853,26 +1909,25 @@ impl SshRemoteConnection {
delegate.set_status(Some("Downloading remote development server on host"), cx);
let body = shlex::try_quote(body).unwrap();
let url = shlex::try_quote(url).unwrap();
let dst_str = dst_path_gz.to_string_lossy();
let dst_escaped = shlex::try_quote(&dst_str).unwrap();
let script = format!(
r#"
if command -v wget >/dev/null 2>&1; then
wget --max-redirect=5 --method=GET --header="Content-Type: application/json" --body-data='{}' '{}' -O '{}' && echo "wget"
elif command -v curl >/dev/null 2>&1; then
curl -L -X GET -H "Content-Type: application/json" -d '{}' '{}' -o '{}' && echo "curl"
if command -v curl >/dev/null 2>&1; then
curl -f -L -X GET -H "Content-Type: application/json" -d {body} {url} -o {dst_escaped} && echo "curl"
elif command -v wget >/dev/null 2>&1; then
wget --max-redirect=5 --method=GET --header="Content-Type: application/json" --body-data={body} {url} -O {dst_escaped} && echo "wget"
else
echo "Neither curl nor wget is available" >&2
exit 1
fi
"#,
body.replace("'", r#"\'"#),
url,
dst_path_gz.display(),
body.replace("'", r#"\'"#),
url,
dst_path_gz.display(),
"#
);
let output = run_cmd(self.socket.ssh_command("bash").arg("-c").arg(script))
let output = run_cmd(self.socket.ssh_command("sh").arg("-c").arg(script))
.await
.context("Failed to download server binary")?;
@@ -1974,6 +2029,113 @@ impl SshRemoteConnection {
))
}
}
#[cfg(debug_assertions)]
async fn build_local(
&self,
platform: SshPlatform,
delegate: &Arc<dyn SshClientDelegate>,
cx: &mut AsyncAppContext,
) -> Result<PathBuf> {
use smol::process::{Command, Stdio};
async fn run_cmd(command: &mut Command) -> Result<()> {
let output = command
.kill_on_drop(true)
.stderr(Stdio::inherit())
.output()
.await?;
if !output.status.success() {
Err(anyhow!("Failed to run command: {:?}", command))?;
}
Ok(())
}
if platform.arch == std::env::consts::ARCH && platform.os == std::env::consts::OS {
delegate.set_status(Some("Building remote server binary from source"), cx);
log::info!("building remote server binary from source");
run_cmd(Command::new("cargo").args([
"build",
"--package",
"remote_server",
"--features",
"debug-embed",
"--target-dir",
"target/remote_server",
]))
.await?;
delegate.set_status(Some("Compressing binary"), cx);
run_cmd(Command::new("gzip").args([
"-9",
"-f",
"target/remote_server/debug/remote_server",
]))
.await?;
let path = std::env::current_dir()?.join("target/remote_server/debug/remote_server.gz");
return Ok(path);
}
let Some(triple) = platform.triple() else {
anyhow::bail!("can't cross compile for: {:?}", platform);
};
smol::fs::create_dir_all("target/remote_server").await?;
delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx);
log::info!("installing cross");
run_cmd(Command::new("cargo").args([
"install",
"cross",
"--git",
"https://github.com/cross-rs/cross",
]))
.await?;
delegate.set_status(
Some(&format!(
"Building remote server binary from source for {} with Docker",
&triple
)),
cx,
);
log::info!("building remote server binary from source for {}", &triple);
run_cmd(
Command::new("cross")
.args([
"build",
"--package",
"remote_server",
"--features",
"debug-embed",
"--target-dir",
"target/remote_server",
"--target",
&triple,
])
.env(
"CROSS_CONTAINER_OPTS",
"--mount type=bind,src=./target,dst=/app/target",
),
)
.await?;
delegate.set_status(Some("Compressing binary"), cx);
run_cmd(Command::new("gzip").args([
"-9",
"-f",
&format!("target/remote_server/{}/debug/remote_server", triple),
]))
.await?;
let path = std::env::current_dir()?.join(format!(
"target/remote_server/{}/debug/remote_server.gz",
triple
));
return Ok(path);
}
}
type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, oneshot::Sender<()>)>>>;
@@ -2295,12 +2457,12 @@ mod fake {
},
select_biased, FutureExt, SinkExt, StreamExt,
};
use gpui::{AsyncAppContext, Task, TestAppContext};
use gpui::{AsyncAppContext, SemanticVersion, Task, TestAppContext};
use release_channel::ReleaseChannel;
use rpc::proto::Envelope;
use super::{
ChannelClient, RemoteConnection, ServerBinary, ServerVersion, SshClientDelegate,
SshConnectionOptions, SshPlatform,
ChannelClient, RemoteConnection, SshClientDelegate, SshConnectionOptions, SshPlatform,
};
pub(super) struct FakeRemoteConnection {
@@ -2412,23 +2574,36 @@ mod fake {
) -> oneshot::Receiver<Result<String>> {
unreachable!()
}
fn remote_server_binary_path(
fn download_server_binary_locally(
&self,
_: SshPlatform,
_: ReleaseChannel,
_: Option<SemanticVersion>,
_: &mut AsyncAppContext,
) -> Result<PathBuf> {
) -> Task<Result<PathBuf>> {
unreachable!()
}
fn get_server_binary(
fn get_download_params(
&self,
_: SshPlatform,
_: bool,
_: &mut AsyncAppContext,
) -> oneshot::Receiver<Result<(ServerBinary, ServerVersion)>> {
_platform: SshPlatform,
_release_channel: ReleaseChannel,
_version: Option<SemanticVersion>,
_cx: &mut AsyncAppContext,
) -> Task<Result<(String, String)>> {
unreachable!()
}
fn set_status(&self, _: Option<&str>, _: &mut AsyncAppContext) {}
fn remote_server_binary_path(
&self,
_platform: SshPlatform,
_cx: &mut AsyncAppContext,
) -> Result<PathBuf> {
unreachable!()
}
}
}

View File

@@ -16,7 +16,7 @@ pub struct ImageView {
impl ImageView {
pub fn from(base64_encoded_data: &str) -> Result<Self> {
let bytes = BASE64_STANDARD.decode(base64_encoded_data)?;
let bytes = BASE64_STANDARD.decode(base64_encoded_data.trim())?;
let format = image::guess_format(&bytes)?;
let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8();

View File

@@ -66,6 +66,8 @@ pub enum RevealStrategy {
/// Always show the terminal pane, add and focus the corresponding task's tab in it.
#[default]
Always,
/// Always show the terminal pane, add the task's tab in it, but don't focus it.
NoFocus,
/// Do not change terminal pane focus, but still add/reuse the task's tab there.
Never,
}

View File

@@ -575,9 +575,9 @@ impl TerminalPanel {
.collect()
}
fn activate_terminal_view(&self, item_index: usize, cx: &mut WindowContext) {
fn activate_terminal_view(&self, item_index: usize, focus: bool, cx: &mut WindowContext) {
self.pane.update(cx, |pane, cx| {
pane.activate_item(item_index, true, true, cx)
pane.activate_item(item_index, true, focus, cx)
})
}
@@ -616,8 +616,14 @@ impl TerminalPanel {
pane.add_item(terminal_view, true, focus, None, cx);
});
if reveal_strategy == RevealStrategy::Always {
workspace.focus_panel::<Self>(cx);
match reveal_strategy {
RevealStrategy::Always => {
workspace.focus_panel::<Self>(cx);
}
RevealStrategy::NoFocus => {
workspace.open_panel::<Self>(cx);
}
RevealStrategy::Never => {}
}
Ok(terminal)
})?;
@@ -698,7 +704,7 @@ impl TerminalPanel {
match reveal {
RevealStrategy::Always => {
self.activate_terminal_view(terminal_item_index, cx);
self.activate_terminal_view(terminal_item_index, true, cx);
let task_workspace = self.workspace.clone();
cx.spawn(|_, mut cx| async move {
task_workspace
@@ -707,6 +713,16 @@ impl TerminalPanel {
})
.detach();
}
RevealStrategy::NoFocus => {
self.activate_terminal_view(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()
})
.detach();
}
RevealStrategy::Never => {}
}

View File

@@ -284,6 +284,7 @@ pub enum IconName {
Update,
UserGroup,
Visible,
Wand,
Warning,
WholeWord,
XCircle,

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.160.0"
version = "0.161.0"
publish = false
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -68,7 +68,6 @@ actions!(
Hide,
HideOthers,
Minimize,
OpenDefaultKeymap,
OpenDefaultSettings,
OpenProjectSettings,
OpenProjectTasks,
@@ -474,7 +473,7 @@ pub fn initialize_workspace(
.register_action(open_project_tasks_file)
.register_action(
move |workspace: &mut Workspace,
_: &OpenDefaultKeymap,
_: &zed_actions::OpenDefaultKeymap,
cx: &mut ViewContext<Workspace>| {
open_bundled_file(
workspace,

View File

@@ -18,7 +18,10 @@ pub fn app_menus() -> Vec<Menu> {
MenuItem::action("Open Settings", super::OpenSettings),
MenuItem::action("Open Key Bindings", zed_actions::OpenKeymap),
MenuItem::action("Open Default Settings", super::OpenDefaultSettings),
MenuItem::action("Open Default Key Bindings", super::OpenDefaultKeymap),
MenuItem::action(
"Open Default Key Bindings",
zed_actions::OpenDefaultKeymap,
),
MenuItem::action("Open Project Settings", super::OpenProjectSettings),
MenuItem::action("Select Theme...", theme_selector::Toggle::default()),
],

View File

@@ -47,6 +47,9 @@ impl OpenRequest {
this.parse_file_path(file)
} else if let Some(file) = url.strip_prefix("zed://file") {
this.parse_file_path(file)
} else if let Some(file) = url.strip_prefix("zed://ssh") {
let ssh_url = "ssh:/".to_string() + file;
this.parse_ssh_file_path(&ssh_url, cx)?
} else if url.starts_with("ssh://") {
this.parse_ssh_file_path(&url, cx)?
} else if let Some(request_path) = parse_zed_link(&url, cx) {

View File

@@ -26,6 +26,7 @@ actions!(
zed,
[
OpenSettings,
OpenDefaultKeymap,
OpenAccountSettings,
OpenServerSettings,
Quit,

View File

@@ -137,7 +137,7 @@ Zed has the following internal prompt templates:
- `content_prompt.hbs`: Used for generating content in the editor.
- `terminal_assistant_prompt.hbs`: Used for the terminal assistant feature.
- `edit_workflow.hbs`: Used for generating the edit workflow prompt.
- `suggest_edits.hbs`: Used for generating the model instructions for the XML Suggest Edits should return.
- `step_resolution.hbs`: Used for generating the step resolution prompt.
At this point it is unknown if we will expand templates further to be user-creatable.
@@ -215,7 +215,7 @@ The following templates can be overridden:
given system information and latest terminal output if relevant.
```
3. `edit_workflow.hbs`: Used for generating the edit workflow prompt.
3. `suggest_edits.hbs`: Used for generating the model instructions for the XML Suggest Edits should return.
4. `step_resolution.hbs`: Used for generating the step resolution prompt.

View File

@@ -1043,6 +1043,32 @@ To interpret all `.c` files as C++, files called `MyLockFile` as TOML and files
}
```
3. Show a commit summary next to the commit date and author:
```json
{
"git": {
"inline_blame": {
"enabled": true,
"show_commit_summary": true
}
}
}
```
4. Use this as the minimum column at which to display inline blame information:
```json
{
"git": {
"inline_blame": {
"enabled": true,
"min_column": 80
}
}
}
```
## Indent Guides
- Description: Configuration related to indent guides. Indent guides can be configured separately for each language.
@@ -2271,6 +2297,9 @@ Run the `theme selector: toggle` action in the command palette to see a current
"auto_fold_dirs": true,
"indent_guides": {
"show": "always"
},
"scrollbar": {
"show": null
}
}
```

View File

@@ -1,6 +1,6 @@
# Dart
Dart support is available through the [Dart extension](https://github.com/zed-industries/zed/tree/main/extensions/dart).
Dart support is available through the [Dart extension](https://github.com/zed-extensions/dart).
- Tree Sitter: [UserNobody14/tree-sitter-dart](https://github.com/UserNobody14/tree-sitter-dart)
- Language Server: [dart language-server](https://github.com/dart-lang/sdk)

View File

@@ -163,6 +163,23 @@ Here's a snippet for Zed settings.json (the language server will restart automat
}
```
### Multi-project workspaces
If you want rust-analyzer to analyze multiple Rust projects in the same folder that are not listed in `[members]` in the Cargo workspace,
you can list them in `linkedProjects` in the local project settings:
```json
{
"lsp": {
"rust-analyzer": {
"initialization_options": {
"linkedProjects": ["./path/to/a/Cargo.toml", "./path/to/b/Cargo.toml"]
}
}
}
}
```
### Snippets
There's a way get custom completion items from rust-analyzer, that will transform the code according to the snippet body:

View File

@@ -16,14 +16,14 @@ On your local machine, Zed runs its UI, talks to language models, uses Tree-sitt
## Setup
1. Download and install the latest [Zed Preview](https://zed.dev/releases/preview). You need at least Zed v0.159.
1. Download and install the latest [Zed](https://zed.dev/releases). You need at least Zed v0.159.
1. Open the remote projects dialogue with <kbd>cmd-shift-p remote</kbd> or <kbd>cmd-control-o</kbd>.
1. Click "Connect New Server" and enter the command you use to SSH into the server. See [Supported SSH options](#supported-ssh-options) for options you can pass.
1. Your local machine will attempt to connect to the remote server using the `ssh` binary on your path. Assuming the connection is successful, Zed will download the server on the remote host and start it.
1. Once the Zed server is running, you will be prompted to choose a path to open on the remote server.
> **Note:** Zed does not currently handle opening very large directories (for example, `/` or `~` that may have >100,000 files) very well. We are working on improving this, but suggest in the meantime opening only specific projects, or subfolders of very large mono-repos.
For simple cases where you don't need any SSH arguments, you can run `zed ssh://[<user>@]<host>[:<port>]/<path>` to open a remote folder/file directly.
For simple cases where you don't need any SSH arguments, you can run `zed ssh://[<user>@]<host>[:<port>]/<path>` to open a remote folder/file directly. If you'd like to hotlink into an SSH project, use a link of the format: `zed://ssh/[<user>@]<host>[:<port>]/<path>`.
## Supported platforms

View File

@@ -18,6 +18,7 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to
"allow_concurrent_runs": false,
// What to do with the terminal pane and tab, after the command was started:
// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
// * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it
// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
"reveal": "always",
// What to do with the terminal pane and tab, after the command had finished:

View File

@@ -1,16 +0,0 @@
[package]
name = "zed_dart"
version = "0.1.2"
edition = "2021"
publish = false
license = "Apache-2.0"
[lints]
workspace = true
[lib]
path = "src/dart.rs"
crate-type = ["cdylib"]
[dependencies]
zed_extension_api = "0.1.0"

View File

@@ -1 +0,0 @@
../../LICENSE-APACHE

View File

@@ -1,6 +0,0 @@
## Roadmap
1. Add `dart run` command.
2. Add `dart test` command.
3. Add `flutter test --name` command, to allow running a single test or group of tests.
4. Auto hot reload Flutter app when files change.

View File

@@ -1,16 +0,0 @@
id = "dart"
name = "Dart"
description = "Dart support."
version = "0.1.2"
schema_version = 1
authors = ["Abdullah Alsigar <abdullah.alsigar@gmail.com>", "Flo <flo80@users.noreply.github.com>", "ybbond <hi@ybbond.id>"]
repository = "https://github.com/zed-industries/zed"
[language_servers.dart]
name = "Dart LSP"
language = "Dart"
languages = ["Dart"]
[grammars.dart]
repository = "https://github.com/UserNobody14/tree-sitter-dart"
commit = "6da46473ab8accb13da48113f4634e729a71d335"

View File

@@ -1,6 +0,0 @@
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
("\"" @open "\"" @close)
("'" @open "'" @close)

View File

@@ -1,15 +0,0 @@
name = "Dart"
grammar = "dart"
path_suffixes = ["dart"]
line_comments = ["// ", "/// "]
autoclose_before = ";:.,=}])>"
brackets = [
{ start = "{", end = "}", close = true, newline = true },
{ start = "[", end = "]", close = true, newline = true },
{ start = "(", end = ")", close = true, newline = true },
{ start = "<", end = ">", close = true, newline = false},
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
{ start = "'", end = "'", close = true, newline = false, not_in = ["string"] },
{ start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
{ start = "`", end = "`", close = true, newline = false, not_in = ["string", "comment"] },
]

View File

@@ -1,269 +0,0 @@
(dotted_identifier_list) @string
; Methods
; --------------------
(super) @function
(function_expression_body (identifier) @type)
; ((identifier)(selector (argument_part)) @function)
(((identifier) @function (#match? @function "^_?[a-z]"))
. (selector . (argument_part))) @function
; Annotations
; --------------------
(annotation
name: (identifier) @attribute)
; Operators and Tokens
; --------------------
(template_substitution
"$" @punctuation.special
"{" @punctuation.special
"}" @punctuation.special) @none
(template_substitution
"$" @punctuation.special
(identifier_dollar_escaped) @variable) @none
(escape_sequence) @string.escape
[
"@"
"=>"
".."
"??"
"=="
"?"
":"
"&&"
"%"
"<"
">"
"="
">="
"<="
"||"
(multiplicative_operator)
(increment_operator)
(is_operator)
(prefix_operator)
(equality_operator)
(additive_operator)
] @operator
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
; Delimiters
; --------------------
[
";"
"."
","
] @punctuation.delimiter
; Types
; --------------------
(class_definition
name: (identifier) @type)
(constructor_signature
name: (identifier) @type)
(scoped_identifier
scope: (identifier) @type)
(function_signature
name: (identifier) @function.method)
(getter_signature
(identifier) @function.method)
(setter_signature
name: (identifier) @function.method)
(enum_declaration
name: (identifier) @type)
(enum_constant
name: (identifier) @type)
(void_type) @type
((scoped_identifier
scope: (identifier) @type
name: (identifier) @type)
(#match? @type "^[a-zA-Z]"))
(type_identifier) @type
(type_alias
(type_identifier) @type.definition)
; Variables
; --------------------
; var keyword
(inferred_type) @keyword
((identifier) @type
(#match? @type "^_?[A-Z].*[a-z]"))
("Function" @type)
; properties
(unconditional_assignable_selector
(identifier) @property)
(conditional_assignable_selector
(identifier) @property)
; assignments
(assignment_expression
left: (assignable_expression) @variable)
(this) @variable.builtin
; Parameters
; --------------------
(formal_parameter
name: (identifier) @variable.parameter)
(named_argument
(label
(identifier) @variable.parameter))
; Literals
; --------------------
[
(hex_integer_literal)
(decimal_integer_literal)
(decimal_floating_point_literal)
; TODO: inaccessible nodes
; (octal_integer_literal)
; (hex_floating_point_literal)
] @number
(symbol_literal) @string.special.symbol
(string_literal) @string
(true) @boolean
(false) @boolean
(null_literal) @constant.builtin
(comment) @comment
(documentation_comment) @comment.documentation
; Keywords
; --------------------
[
"import"
"library"
"export"
"as"
"show"
"hide"
] @keyword.import
; Reserved words (cannot be used as identifiers)
[
(case_builtin)
"late"
"required"
"extension"
"on"
"class"
"enum"
"extends"
"in"
"is"
"new"
"super"
"with"
] @keyword
"return" @keyword.return
; Built in identifiers:
; alone these are marked as keywords
[
"deferred"
"factory"
"get"
"implements"
"interface"
"library"
"operator"
"mixin"
"part"
"set"
"typedef"
] @keyword
[
"async"
"async*"
"sync*"
"await"
"yield"
] @keyword.coroutine
[
(const_builtin)
(final_builtin)
"abstract"
"covariant"
"dynamic"
"external"
"static"
"final"
"base"
"sealed"
] @type.qualifier
; when used as an identifier:
((identifier) @variable.builtin
(#any-of? @variable.builtin
"abstract"
"as"
"covariant"
"deferred"
"dynamic"
"export"
"external"
"factory"
"Function"
"get"
"implements"
"import"
"interface"
"library"
"operator"
"mixin"
"part"
"set"
"static"
"typedef"))
[
"if"
"else"
"switch"
"default"
] @keyword.conditional
[
"try"
"throw"
"catch"
"finally"
(break_statement)
] @keyword.exception
[
"do"
"while"
"continue"
"for"
] @keyword.repeat

View File

@@ -1,3 +0,0 @@
(_ "[" "]" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent

View File

@@ -1,18 +0,0 @@
(class_definition
"class" @context
name: (_) @name) @item
(function_signature
name: (_) @name) @item
(getter_signature
"get" @context
name: (_) @name) @item
(setter_signature
"set" @context
name: (_) @name) @item
(enum_declaration
"enum" @context
name: (_) @name) @item

View File

@@ -1,45 +0,0 @@
; Flutter main
(
(
(import_or_export
(library_import
(import_specification
("import"
(configurable_uri
(uri
(string_literal) @_import
(#match? @_import "package:flutter/(material|widgets|cupertino).dart")
(#not-match? @_import "package:flutter_test/flutter_test.dart")
(#not-match? @_import "package:test/test.dart")
))))))
(
(function_signature
name: (_) @run
)
(#eq? @run "main")
)
(#set! tag flutter-main)
)
)
; Flutter test main
(
(
(import_or_export
(library_import
(import_specification
("import"
(configurable_uri
(uri
(string_literal) @_import
(#match? @_import "package:flutter_test/flutter_test.dart")
))))))
(
(function_signature
name: (_) @run
)
(#eq? @run "main")
)
(#set! tag flutter-test-main)
)
)

View File

@@ -1,26 +0,0 @@
[
{
"label": "flutter run",
"command": "flutter",
"args": ["run"],
"tags": ["flutter-main"]
},
{
"label": "fvm flutter run",
"command": "fvm flutter",
"args": ["run"],
"tags": ["flutter-main"]
},
{
"label": "flutter test $ZED_STEM",
"command": "flutter",
"args": ["test", "$ZED_FILE"],
"tags": ["flutter-test-main"]
},
{
"label": "fvm flutter test $ZED_STEM",
"command": "fvm flutter",
"args": ["test", "$ZED_FILE"],
"tags": ["flutter-test-main"]
}
]

View File

@@ -1,162 +0,0 @@
use zed::lsp::CompletionKind;
use zed::settings::LspSettings;
use zed::{CodeLabel, CodeLabelSpan};
use zed_extension_api::{self as zed, serde_json, Result};
struct DartBinary {
pub path: String,
pub args: Option<Vec<String>>,
}
struct DartExtension;
impl DartExtension {
fn language_server_binary(
&mut self,
_language_server_id: &zed::LanguageServerId,
worktree: &zed::Worktree,
) -> Result<DartBinary> {
let binary_settings = LspSettings::for_worktree("dart", worktree)
.ok()
.and_then(|lsp_settings| lsp_settings.binary);
let binary_args = binary_settings
.as_ref()
.and_then(|binary_settings| binary_settings.arguments.clone());
if let Some(path) = binary_settings.and_then(|binary_settings| binary_settings.path) {
return Ok(DartBinary {
path,
args: binary_args,
});
}
if let Some(path) = worktree.which("dart") {
return Ok(DartBinary {
path,
args: binary_args,
});
}
Err(
"dart must be installed from dart.dev/get-dart or pointed to by the LSP binary settings"
.to_string(),
)
}
}
impl zed::Extension for DartExtension {
fn new() -> Self {
Self
}
fn language_server_command(
&mut self,
language_server_id: &zed::LanguageServerId,
worktree: &zed::Worktree,
) -> Result<zed::Command> {
let dart_binary = self.language_server_binary(language_server_id, worktree)?;
Ok(zed::Command {
command: dart_binary.path,
args: dart_binary.args.unwrap_or_else(|| {
vec!["language-server".to_string(), "--protocol=lsp".to_string()]
}),
env: Default::default(),
})
}
fn language_server_workspace_configuration(
&mut self,
_language_server_id: &zed::LanguageServerId,
worktree: &zed::Worktree,
) -> Result<Option<serde_json::Value>> {
let settings = LspSettings::for_worktree("dart", worktree)
.ok()
.and_then(|lsp_settings| lsp_settings.settings.clone())
.unwrap_or_default();
Ok(Some(serde_json::json!({
"dart": settings
})))
}
fn label_for_completion(
&self,
_language_server_id: &zed::LanguageServerId,
completion: zed::lsp::Completion,
) -> Option<CodeLabel> {
let arrow = "";
match completion.kind? {
CompletionKind::Class => Some(CodeLabel {
filter_range: (0..completion.label.len()).into(),
spans: vec![CodeLabelSpan::literal(
completion.label,
Some("type".into()),
)],
code: String::new(),
}),
CompletionKind::Function | CompletionKind::Constructor | CompletionKind::Method => {
let mut parts = completion.detail.as_ref()?.split(arrow);
let (name, _) = completion.label.split_once('(')?;
let parameter_list = parts.next()?;
let return_type = parts.next()?;
let fn_name = " a";
let fat_arrow = " => ";
let call_expr = "();";
let code =
format!("{return_type}{fn_name}{parameter_list}{fat_arrow}{name}{call_expr}");
let parameter_list_start = return_type.len() + fn_name.len();
Some(CodeLabel {
spans: vec![
CodeLabelSpan::code_range(
code.len() - call_expr.len() - name.len()..code.len() - call_expr.len(),
),
CodeLabelSpan::code_range(
parameter_list_start..parameter_list_start + parameter_list.len(),
),
CodeLabelSpan::literal(arrow, None),
CodeLabelSpan::code_range(0..return_type.len()),
],
filter_range: (0..name.len()).into(),
code,
})
}
CompletionKind::Property => {
let class_start = "class A {";
let get = " get ";
let property_end = " => a; }";
let ty = completion.detail?;
let name = completion.label;
let code = format!("{class_start}{ty}{get}{name}{property_end}");
let name_start = class_start.len() + ty.len() + get.len();
Some(CodeLabel {
spans: vec![
CodeLabelSpan::code_range(name_start..name_start + name.len()),
CodeLabelSpan::literal(arrow, None),
CodeLabelSpan::code_range(class_start.len()..class_start.len() + ty.len()),
],
filter_range: (0..name.len()).into(),
code,
})
}
CompletionKind::Variable => {
let name = completion.label;
Some(CodeLabel {
filter_range: (0..name.len()).into(),
spans: vec![CodeLabelSpan::literal(name, Some("variable".into()))],
code: String::new(),
})
}
_ => None,
}
}
}
zed::register_extension!(DartExtension);