Compare commits

...

43 Commits

Author SHA1 Message Date
Joseph T. Lyons
ac4bfede2e zed 0.189.5 2025-06-03 12:32:22 -04:00
Jason Lee
a9ac3734f6 gpui: Improve window.prompt to support ESC with non-English cancel text on macOS (#29538)
Release Notes:

- N/A

----

The before version GPUI used `Cancel` for cancel text, if we use
non-English text (e.g.: "取消" in Chinese), then the press `Esc` to cancel
will not work.

So this PR to change it by use `PromptButton` to instead the `&str`,
then we can use `PromptButton::cancel("取消")` for the `Cancel` button.

Run `cargo run -p gpui --example window` to test.

---

Platform Test:

- [x] macOS
- [x] Windows
- [x] Linux (x11 and Wayland)

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-06-03 12:31:13 -04:00
Piotr Osiewicz
69302bc7c7 debugger: Add tooltips to the new process modal (#31953)
Closes #ISSUE

Release Notes:

- N/A
2025-06-03 12:27:53 -04:00
Cole Miller
3d272dbc2e debugger: Use new icons for quick debug/spawn button (#31932)
This PR wires up the new icons that were added in #31784.

Release Notes:

- N/A
2025-06-03 12:27:33 -04:00
Cole Miller
72d4dd6ddc debugger: Go back to loading task contexts asynchronously for new session modal (#31908)
Release Notes:

- N/A
2025-06-03 12:27:33 -04:00
Cole Miller
7382157c2f debugger: Align zoom behavior with other panels (#31901)
Release Notes:

- Debugger Beta: `shift-escape` (`workspace::ToggleZoom`) now zooms the
entire debug panel; `alt-shift-escape` (`debugger::ToggleExpandItem`)
triggers the old behavior of zooming a specific item.
2025-06-03 12:27:33 -04:00
Cole Miller
6241034ba1 debugger: Extend f5 binding to contextually rerun the last session (#31753)
Release Notes:

- Debugger Beta: if there is no stopped or running session, `f5` now
reruns the last session, or opens the new session modal if there is no
previously-run session.
2025-06-03 12:27:20 -04:00
Mikayla Maki
414b3d46fa Simplify debug launcher UI (#31928)
This PR updates the name of the `NewSessionModal` to `NewProcessModal`
(to reflect it's new purpose), changes the tabs in the modal to read
`Run | Debug | Attach | Launch` and changes the associated types in code
to match the tabs. In addition, this PR adds a few labels to the text
fields in the `Launch` tab, and adds a link to open the associated
settings file. In both debug.json files, added links to the zed.dev
debugger docs.

Release Notes:

- Debugger Beta: Improve the new process modal
2025-06-03 12:25:52 -04:00
Fernando Freire
2e0f4acb40 google_ai: Parse thought parts in Gemini responses (#31925)
Fixes thinking Gemini models.

Closes #31902

Release Notes:

- Updated Google Gemini client to match the latest API
2025-06-03 12:25:23 -04:00
Peter Tripp
ef301a529e Fix aggressive indent in shell scripts (#31973)
Closes: https://github.com/zed-industries/zed/issues/31774

Release Notes:

- N/A

Co-authored-by: Ben Kunkle <ben.kunkle@gmail.com>
2025-06-03 12:19:14 -04:00
Joseph T. Lyons
dd25fa329c zed 0.189.4 2025-06-02 11:20:58 -04:00
Danilo Leal
43a219afaf Harmonize quick action icons (#31784)
Just ensuring they're more harmonized in size, stroke width, and overall
dimensions. Adding here two new "play" icons, too, that will be used in
the context of the debugger.

<img
src="https://github.com/user-attachments/assets/79bcf0b0-e995-4c8e-9c78-0aba32f42f2d"
width="400" />

Release Notes:

- N/A
2025-06-02 11:19:54 -04:00
Kirill Bulatov
2926e78c03 Do not error on debugger server connection close (#31795)
Start to show islands of logs in a successful debugger runs for Rust:

<img width="1728" alt="1"
src="https://github.com/user-attachments/assets/7400201b-b900-4b20-8adf-21850ae59671"
/>

<img width="1728" alt="2"
src="https://github.com/user-attachments/assets/e75cc5f1-1f74-41d6-b7aa-697a4b2f055b"
/>

Release Notes:

- N/A
2025-06-02 11:19:05 -04:00
Piotr Osiewicz
0283d65f90 debugger: Relax implementation of validate_config to not run validation (#31785)
When we moved to schema-based debug configs, we've added validate_config
- a trait method
that is supposed to both validate the configuration and determine
whether it is a launch configuration
or an attach configuration.

The validation bit is a bit problematic though - we received reports on
Discords about
scenarios not starting up properly; it turned out that Javascript's
implementation was overly strict.
Thus, I got rid of any code that tries to validate the config - let's
let the debug adapter itself
decide whether it can digest the configuration or not. validate_config
is now left unimplemented for most
DebugAdapter implementations (except for PHP), because all adapters use
`request`: 'launch'/'attach' for that.
Let's leave the trait method in place though, as nothing guarantees this
to be true for all adapters.

cc @Anthony-Eid

Release Notes:

- debugger: Improved error messages when the debug scenario is not
valid.
- debugger: Fixed cases where valid configs were rejected.
2025-06-02 11:18:12 -04:00
Alex
619d9ea134 debugger: Add support for go tests (#31772)
In the https://github.com/zed-industries/zed/pull/31559 I did not
introduce ability to debug test invocations.
Adding it here. E.g:
![Kapture 2025-05-30 at 19 59
13](https://github.com/user-attachments/assets/1111d4a5-8b0a-42e6-aa98-2d797f61ffe3)

Release Notes:
- Added support for debugging single tests written in go
2025-06-02 11:18:03 -04:00
Kirill Bulatov
79b472baa3 Use both language and LSP icons for LSP tasks (#31773)
Make more explicit which language LSP tasks are used.

Before:

![image](https://github.com/user-attachments/assets/27f93c5f-942e-47a0-9b74-2c6d4d6248de)

After:

![image
(1)](https://github.com/user-attachments/assets/5a29fb0a-2e16-4c35-9dda-ae7925eaa034)


![image](https://github.com/user-attachments/assets/d1bf518e-63d1-4ebf-af3d-3c9d464c6532)


Release Notes:

- N/A
2025-06-02 11:17:46 -04:00
Kirill Bulatov
771a3acfe1 Improve LSP tasks ergonomics (#31551)
* stopped fetching LSP tasks for too long (but still use the hardcoded
value for the time being — the LSP tasks settings part is a simple bool
key and it's not very simple to fit in another value there)

* introduced `prefer_lsp` language task settings value, to control
whether in the gutter/modal/both/none LSP tasks are shown exclusively,
if possible

Release Notes:

- Added a way to prefer LSP tasks over Zed tasks
2025-06-02 11:17:37 -04:00
Piotr Osiewicz
8541076cf9 debugger: Change console text color, add tooltips (#31765)
- Improved legibility of console text:

| Theme | Dark | Light |
|--------|--------|--------|
| Before |
![image](https://github.com/user-attachments/assets/756da36d-9ef4-495a-9cf9-7249c25d106a)
|
![image](https://github.com/user-attachments/assets/42558ec2-ee08-4973-8f7d-d7f4feb38cf8)
|
| After |
![image](https://github.com/user-attachments/assets/4469f000-b34f-4cbb-819d-4ae1f2f58a4a)
|
![image](https://github.com/user-attachments/assets/3b862114-0fd3-427c-9c76-f030d3442090)
|

Release Notes:

- debugger: Improved legibility of console text
- debugger: Added tooltips to all debugger items.
2025-06-02 11:16:58 -04:00
Kirill Bulatov
c5499f49ee Add a JS/TS debug locator (#31769)
With this, a semi-working debug session is possible from the JS/TS
gutter tasks:


https://github.com/user-attachments/assets/8db6ed29-b44a-4314-ae8b-a8213291bffc

For now, available in debug builds only as a base to improve on later on
the DAP front.

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <peterosiewicz@gmail.com>
2025-06-02 11:16:50 -04:00
Kirill Bulatov
8757faf450 Improve TypeScript task detection (#31711)
Parses project's package.json to better detect Jasmine, Jest, Vitest and
Mocha and `test`, `build` scripts presence.
Also tries to detect `pnpm` and `npx` as test runners, falls back to
`npm`.


https://github.com/user-attachments/assets/112d3d8b-8daa-4ba5-8cb5-2f483036bd98

Release Notes:

- Improved TypeScript task detection
2025-06-02 11:16:42 -04:00
Joseph T. Lyons
eece7266dd zed 0.189.3 2025-05-30 11:45:07 -04:00
Anthony Eid
26b5ceabe6 debugger: Update docs with more examples (#31597)
This PR also shows more completion items when defining a debug config in
a `debug.json` file. Mainly when using a pre build task argument.

### Follow ups
- Add docs for Go, JS, PHP
- Add attach docs

Release Notes:

- debugger beta: Show build task completions when editing a debug.json
configuration with a pre build task
- debugger beta: Add Python and Native Code debug config
[examples](https://zed.dev/docs/debugger)
2025-05-30 10:35:39 -04:00
Piotr Osiewicz
60bbaaded7 debugger: Fix wrong path to the downloaded delve-shim-dap (#31738)
Closes #ISSUE

Release Notes:

- N/A
2025-05-30 10:35:09 -04:00
Piotr Osiewicz
fc061ab139 debugger: Fix delve-dap-shim path on Windows (#31735)
Closes #ISSUE

Release Notes:

- debugger: Fixed wrong path being picked up for delve on Windows
- debugger: Fixed delve not respecting the user-provided binary path
from settings.
2025-05-30 10:34:55 -04:00
Cole Miller
cf6ffab260 Unify the tasks modal and the new session modal (#31646)
Release Notes:

- Debugger Beta: added a button to the quick action bar to start a debug
session or spawn a task, depending on which of these actions was taken
most recently.
- Debugger Beta: incorporated the tasks modal into the new session modal
as an additional tab.

---------

Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
Co-authored-by: Julia Ryan <p1n3appl3@users.noreply.github.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Mikayla <mikayla@zed.dev>
2025-05-30 10:34:44 -04:00
Piotr Osiewicz
4932a127d1 go: Use delve-dap-shim for spawning delve (#31700)
This allows us to support terminal with go sessions

Closes #ISSUE

Release Notes:

- debugger: Add support for terminal when debugging Go programs
2025-05-30 10:34:33 -04:00
Ben Brandt
7fef60004b Fix model deduplication to use provider ID and model ID (#31752)
Replicates https://github.com/zed-industries/zed/pull/31750 for v0.189.x
branch

Release Notes:

- Fix to make sure all provider models are shown in the model picker
2025-05-30 16:32:05 +02:00
gcp-cherry-pick-bot[bot]
51c8e35b2e Revert "debugger beta: Fix bug where debug Rust main running action f… (cherry-pick #31743) (#31746)
Cherry-picked Revert "debugger beta: Fix bug where debug Rust main
running action f… (#31743)

…ailed (#31291)"

This reverts commit aab76208b5.

Closes #31737

I cannot repro the original issue that this commit was trying to solve
anymore.

Release Notes:

- N/A

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-05-30 16:21:09 +03:00
Max Brunsfeld
f556d634fc Bump Tree-sitter to 0.25.5 for YAML-editing crash fix (#31603)
Closes https://github.com/zed-industries/zed/issues/31380

See https://github.com/tree-sitter/tree-sitter/pull/4472 for the fix

Release Notes:

- Fixed a crash that could occur when editing YAML files.
2025-05-29 16:26:23 -07:00
Zed Bot
4315869c62 Bump to 0.189.2 for @maxbrunsfeld 2025-05-29 22:49:52 +00:00
Max Brunsfeld
cab167d53d Fix editor rendering slowness with large folds (#31569)
Closes https://github.com/zed-industries/zed/issues/31565

* Looking up settings on every row was very slow in the case of large
folds, especially if there was an `.editorconfig` file with numerous
glob patterns
* Checking whether each indent guide was within a fold was very slow,
when a fold spanned many indent guides.

Release Notes:

- Fixed slowness that could happen when editing in the presence of large
folds.
2025-05-29 15:37:47 -07:00
gcp-cherry-pick-bot[bot]
6c86825756 Pass up intent with completion requests (cherry-pick #31710) (#31714)
Cherry-picked Pass up intent with completion requests (#31710)

This PR adds a new `intent` field to completion requests to assist in
categorizing them correctly.

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-05-29 17:20:29 -04:00
Zed Bot
0e562f2176 Bump to 0.189.1 for @SomeoneToIgnore 2025-05-29 15:23:04 +00:00
gcp-cherry-pick-bot[bot]
abea2409cd Introduce $ZED_CUSTOM_PYTHON_ACTIVE_ZED_TOOLCHAIN_RAW to work around (cherry-pick #31685) (#31688)
Cherry-picked Introduce $ZED_CUSTOM_PYTHON_ACTIVE_ZED_TOOLCHAIN_RAW to
work around (#31685)

Follow up to #31674 

Release Notes:

- N/A

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
2025-05-29 18:20:15 +03:00
Piotr Osiewicz
a5b511edb3 Introduce $ZED_CUSTOM_PYTHON_ACTIVE_ZED_TOOLCHAIN_RAW to work around (#31685)
Follow up to #31674 

Release Notes:

- N/A

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
2025-05-29 09:55:02 -04:00
Cole Miller
be1efa5d33 debugger: Add spinners while session is starting up (#31548)
Release Notes:

- Debugger Beta: Added a spinner to the debug panel when a session is
starting up.

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
Co-authored-by: Julia <julia@zed.dev>
2025-05-29 09:54:51 -04:00
Cole Miller
ce8ff9572e debugger: Disambiguate child session labels (#31526)
Add `(child)` instead of using the same label.

Release Notes:

- Debugger Beta: Made child sessions appear distinct from their parents
in the session selector.
2025-05-29 09:54:51 -04:00
Cole Miller
5ec3dffa11 debugger: Don't open non-absolute paths from stack frame list (#31534)
Follow-up to #31524 with a more general fix

Release Notes:

- N/A

---------

Co-authored-by: Piotr <piotr@zed.dev>
2025-05-29 09:54:43 -04:00
gcp-cherry-pick-bot[bot]
261646656e Revert "task: Wrap programs in ""s (#31537)" (cherry-pick #31674) (#31676)
Cherry-picked Revert "task: Wrap programs in ""s (#31537)" (#31674)

That commit broke a lot, as our one-off tasks (alt-enter in the tasks
modal), npm, jest tasks are all not real commands, but a composition of
commands and arguments.

This reverts commit 5db14d315b.

Closes https://github.com/zed-industries/zed/issues/31554

Release Notes:

- N/A

Co-authored-by: Piotr Osiewicz
<24362066+osiewicz@users.noreply.github.com>

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-05-29 12:43:28 +03:00
gcp-cherry-pick-bot[bot]
0ac8774cd4 file_finder: Reduce vertical padding in footer (cherry-pick #31632) (#31673)
Cherry-picked file_finder: Reduce vertical padding in footer (#31632)

Follow-up to #31542

This PR reduces the vertical padding in the file finders footer. We can
remove this padding as we already apply it just above



a5a116439e/crates/file_finder/src/file_finder.rs (L1500)

This also ensures that the items on the right side have the same padding
to the border as the icon on the left side. Currently, due to the
padding being applied twice, the items on the right side have `pr_4` as
well as `py_4` in practice, which seems a little excessive.

| `main` | This PR |
| --- | --- |
|

![file_finder_main](https://github.com/user-attachments/assets/352d2ac9-04a9-487d-96ca-b009b797809b)
|

![file_finder_pr](https://github.com/user-attachments/assets/c0b44beb-ff2c-4e93-a5b1-2393652a2a58)
|


Release Notes:

- N/A

Co-authored-by: Finn Evers <dev@bahn.sh>
2025-05-29 12:14:25 +03:00
Remco Smits
ba5c7326e9 debugger: Fix invalid schema for pathMappings (#31595)
See
https://github.com/xdebug/vscode-php-debug?tab=readme-ov-file#remote-host-debugging

Release Notes:

- Debugger Beta: Fixed invalid schema for `pathMappings`
2025-05-28 17:52:07 -04:00
Cole Miller
41f944bd9e Revert "Fix text wrapping in commit message editors (#31030)" (#31587)
This reverts commit f2601ce52c.

Release Notes:

- N/A
2025-05-28 11:01:28 -04:00
Joseph T. Lyons
fc306a271c v0.189.x preview 2025-05-28 10:16:29 -04:00
122 changed files with 3585 additions and 1801 deletions

19
Cargo.lock generated
View File

@@ -559,6 +559,7 @@ dependencies = [
"workspace",
"workspace-hack",
"zed_actions",
"zed_llm_client",
]
[[package]]
@@ -4732,6 +4733,7 @@ dependencies = [
"tree-sitter-rust",
"tree-sitter-typescript",
"ui",
"unicode-script",
"unicode-segmentation",
"unindent",
"url",
@@ -5045,6 +5047,7 @@ dependencies = [
"util",
"uuid",
"workspace-hack",
"zed_llm_client",
]
[[package]]
@@ -6147,6 +6150,7 @@ dependencies = [
"workspace",
"workspace-hack",
"zed_actions",
"zed_llm_client",
"zlog",
]
@@ -8929,6 +8933,7 @@ dependencies = [
"async-compression",
"async-tar",
"async-trait",
"chrono",
"collections",
"dap",
"futures 0.3.31",
@@ -8982,6 +8987,7 @@ dependencies = [
"tree-sitter-yaml",
"unindent",
"util",
"which 6.0.3",
"workspace",
"workspace-hack",
]
@@ -15581,6 +15587,7 @@ dependencies = [
"futures 0.3.31",
"gpui",
"hex",
"log",
"parking_lot",
"pretty_assertions",
"proto",
@@ -16480,9 +16487,9 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.25.3"
version = "0.25.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ac5ea5e7f2f1700842ec071401010b9c59bf735295f6e9fa079c3dc035b167"
checksum = "ac5fff5c47490dfdf473b5228039bfacad9d765d9b6939d26bf7cc064c1c7822"
dependencies = [
"cc",
"regex",
@@ -17115,8 +17122,6 @@ dependencies = [
"tempfile",
"tendril",
"unicase",
"unicode-script",
"unicode-segmentation",
"util_macros",
"walkdir",
"workspace-hack",
@@ -19680,7 +19685,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.189.0"
version = "0.189.5"
dependencies = [
"activity_indicator",
"agent",
@@ -19876,9 +19881,9 @@ dependencies = [
[[package]]
name = "zed_llm_client"
version = "0.8.3"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22a8b9575b215536ed8ad254ba07171e4e13bd029eda3b54cca4b184d2768050"
checksum = "de7d9523255f4e00ee3d0918e5407bd252d798a4a8e71f6d37f23317a1588203"
dependencies = [
"anyhow",
"serde",

View File

@@ -572,7 +572,7 @@ tokio = { version = "1" }
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
toml = "0.8"
tower-http = "0.4.4"
tree-sitter = { version = "0.25.3", features = ["wasm"] }
tree-sitter = { version = "0.25.5", features = ["wasm"] }
tree-sitter-bash = "0.23"
tree-sitter-c = "0.23"
tree-sitter-cpp = "0.23"
@@ -617,7 +617,7 @@ wasmtime = { version = "29", default-features = false, features = [
wasmtime-wasi = "29"
which = "6.0.0"
workspace-hack = "0.1.0"
zed_llm_client = "0.8.3"
zed_llm_client = "0.8.4"
zstd = "0.11"
[workspace.dependencies.async-stripe]

View File

@@ -1,5 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 20H16C14.9391 20 13.9217 19.6629 13.1716 19.0627C12.4214 18.4626 12 17.6487 12 16.8V7.2C12 6.35131 12.4214 5.53737 13.1716 4.93726C13.9217 4.33714 14.9391 4 16 4H17" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 20H8C9.06087 20 10.0783 19.5786 10.8284 18.8284C11.5786 18.0783 12 17.0609 12 16V15" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 4H8C9.06087 4 10.0783 4.42143 10.8284 5.17157C11.5786 5.92172 12 6.93913 12 8V9" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 13H10.4C9.76346 13 9.15302 12.7893 8.70296 12.4142C8.25284 12.0391 8 11.5304 8 11V5C8 4.46957 8.25284 3.96086 8.70296 3.58579C9.15302 3.21071 9.76346 3 10.4 3H11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 13H5.6C6.23654 13 6.84698 12.7893 7.29704 12.4142C7.74716 12.0391 8 11.5304 8 11V5C8 4.46957 7.74716 3.96086 7.29704 3.58579C6.84698 3.21071 6.23654 3 5.6 3H5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 715 B

After

Width:  |  Height:  |  Size: 617 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 3L13 8L4 13V3Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 214 B

View File

@@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 12C2.35977 11.85 1 10.575 1 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.00875 15.2C1.00875 13.625 0.683456 12.275 4.00001 12.2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 9C7 10.575 5.62857 11.85 4 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 12.2C6.98117 12.2 7 13.625 7 15.2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="2.5" y="9" width="3" height="6" rx="1.5" fill="black"/>
<path d="M9 10L13 8L4 3V7.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 813 B

View File

@@ -1,3 +1,8 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.36667 3.79167C5.53364 3.79167 4.85833 4.46697 4.85833 5.3C4.85833 6.13303 5.53364 6.80833 6.36667 6.80833C7.1997 6.80833 7.875 6.13303 7.875 5.3C7.875 4.46697 7.1997 3.79167 6.36667 3.79167ZM2.1 5.925H3.67944C3.9626 7.14732 5.05824 8.05833 6.36667 8.05833C7.67509 8.05833 8.77073 7.14732 9.05389 5.925H14.9C15.2452 5.925 15.525 5.64518 15.525 5.3C15.525 4.95482 15.2452 4.675 14.9 4.675H9.05389C8.77073 3.45268 7.67509 2.54167 6.36667 2.54167C5.05824 2.54167 3.9626 3.45268 3.67944 4.675H2.1C1.75482 4.675 1.475 4.95482 1.475 5.3C1.475 5.64518 1.75482 5.925 2.1 5.925ZM13.3206 12.325C13.0374 13.5473 11.9418 14.4583 10.6333 14.4583C9.32491 14.4583 8.22927 13.5473 7.94611 12.325H2.1C1.75482 12.325 1.475 12.0452 1.475 11.7C1.475 11.3548 1.75482 11.075 2.1 11.075H7.94611C8.22927 9.85268 9.32491 8.94167 10.6333 8.94167C11.9418 8.94167 13.0374 9.85268 13.3206 11.075H14.9C15.2452 11.075 15.525 11.3548 15.525 11.7C15.525 12.0452 15.2452 12.325 14.9 12.325H13.3206ZM9.125 11.7C9.125 10.867 9.8003 10.1917 10.6333 10.1917C11.4664 10.1917 12.1417 10.867 12.1417 11.7C12.1417 12.533 11.4664 13.2083 10.6333 13.2083C9.8003 13.2083 9.125 12.533 9.125 11.7Z" fill="black"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 5H4" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M8 5L14 5" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M12 11L14 11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M2 11H8" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="6" cy="5" r="2" fill="black" fill-opacity="0.1" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="10" cy="11" r="2" fill="black" fill-opacity="0.1" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 657 B

View File

@@ -1,5 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 1.75L5.88467 5.14092C5.82759 5.31446 5.73055 5.47218 5.60136 5.60136C5.47218 5.73055 5.31446 5.82759 5.14092 5.88467L1.75 7L5.14092 8.11533C5.31446 8.17241 5.47218 8.26945 5.60136 8.39864C5.73055 8.52782 5.82759 8.68554 5.88467 8.85908L7 12.25L8.11533 8.85908C8.17241 8.68554 8.26945 8.52782 8.39864 8.39864C8.52782 8.26945 8.68554 8.17241 8.85908 8.11533L12.25 7L8.85908 5.88467C8.68554 5.82759 8.52782 5.73055 8.39864 5.60136C8.26945 5.47218 8.17241 5.31446 8.11533 5.14092L7 1.75Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.91667 1.75V4.08333M1.75 2.91667H4.08333" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.0833 9.91667V12.25M9.91667 11.0833H12.25" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 2L6.72534 5.87534C6.6601 6.07367 6.5492 6.25392 6.40155 6.40155C6.25392 6.5492 6.07367 6.6601 5.87534 6.72534L2 8L5.87534 9.27466C6.07367 9.3399 6.25392 9.4508 6.40155 9.59845C6.5492 9.74608 6.6601 9.92633 6.72534 10.1247L8 14L9.27466 10.1247C9.3399 9.92633 9.4508 9.74608 9.59845 9.59845C9.74608 9.4508 9.92633 9.3399 10.1247 9.27466L14 8L10.1247 6.72534C9.92633 6.6601 9.74608 6.5492 9.59845 6.40155C9.4508 6.25392 9.3399 6.07367 9.27466 5.87534L8 2Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.33334 2V4.66666M2 3.33334H4.66666" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.6665 11.3333V14M11.3333 12.6666H13.9999" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 998 B

View File

@@ -31,8 +31,6 @@
"ctrl-,": "zed::OpenSettings",
"ctrl-q": "zed::Quit",
"f4": "debugger::Start",
"alt-f4": "debugger::RerunLastSession",
"f5": "debugger::Continue",
"shift-f5": "debugger::Stop",
"ctrl-shift-f5": "debugger::Restart",
"f6": "debugger::Pause",
@@ -582,11 +580,24 @@
"ctrl-alt-r": "task::Rerun",
"alt-t": "task::Rerun",
"alt-shift-t": "task::Spawn",
"alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
"alt-shift-r": ["task::Spawn", { "reveal_target": "center" }],
// also possible to spawn tasks by name:
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
// or by tag:
// "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
"f5": "debugger::RerunLastSession"
}
},
{
"context": "Workspace && debugger_running",
"bindings": {
"f5": "zed::NoAction"
}
},
{
"context": "Workspace && debugger_stopped",
"bindings": {
"f5": "debugger::Continue"
}
},
{
@@ -872,7 +883,8 @@
"context": "DebugPanel",
"bindings": {
"ctrl-t": "debugger::ToggleThreadPicker",
"ctrl-i": "debugger::ToggleSessionPicker"
"ctrl-i": "debugger::ToggleSessionPicker",
"shift-alt-escape": "debugger::ToggleExpandItem"
}
},
{
@@ -1018,5 +1030,12 @@
"bindings": {
"enter": "menu::Confirm"
}
},
{
"context": "RunModal",
"bindings": {
"ctrl-tab": "pane::ActivateNextItem",
"ctrl-shift-tab": "pane::ActivatePreviousItem"
}
}
]

View File

@@ -4,8 +4,6 @@
"use_key_equivalents": true,
"bindings": {
"f4": "debugger::Start",
"alt-f4": "debugger::RerunLastSession",
"f5": "debugger::Continue",
"shift-f5": "debugger::Stop",
"shift-cmd-f5": "debugger::Restart",
"f6": "debugger::Pause",
@@ -634,7 +632,8 @@
"cmd-k shift-right": "workspace::SwapPaneRight",
"cmd-k shift-up": "workspace::SwapPaneUp",
"cmd-k shift-down": "workspace::SwapPaneDown",
"cmd-shift-x": "zed::Extensions"
"cmd-shift-x": "zed::Extensions",
"f5": "debugger::RerunLastSession"
}
},
{
@@ -651,6 +650,20 @@
// "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
}
},
{
"context": "Workspace && debugger_running",
"use_key_equivalents": true,
"bindings": {
"f5": "zed::NoAction"
}
},
{
"context": "Workspace && debugger_stopped",
"use_key_equivalents": true,
"bindings": {
"f5": "debugger::Continue"
}
},
// Bindings from Sublime Text
{
"context": "Editor",
@@ -935,7 +948,8 @@
"context": "DebugPanel",
"bindings": {
"cmd-t": "debugger::ToggleThreadPicker",
"cmd-i": "debugger::ToggleSessionPicker"
"cmd-i": "debugger::ToggleSessionPicker",
"shift-alt-escape": "debugger::ToggleExpandItem"
}
},
{
@@ -1108,5 +1122,13 @@
"bindings": {
"enter": "menu::Confirm"
}
},
{
"context": "RunModal",
"use_key_equivalents": true,
"bindings": {
"ctrl-tab": "pane::ActivateNextItem",
"ctrl-shift-tab": "pane::ActivatePreviousItem"
}
}
]

View File

@@ -1314,7 +1314,17 @@
// Settings related to running tasks.
"tasks": {
"variables": {},
"enabled": true
"enabled": true,
// Use LSP tasks over Zed language extension ones.
// If no LSP tasks are returned due to error/timeout or regular execution,
// Zed language extension tasks will be used instead.
//
// Other Zed tasks will still be shown:
// * Zed task from either of the task config file
// * Zed task from history (e.g. one-off task was spawned before)
//
// Default: true
"prefer_lsp": true
},
// An object whose keys are language names, and whose values
// are arrays of filenames or extensions of files that should
@@ -1452,9 +1462,7 @@
"language_servers": ["erlang-ls", "!elp", "..."]
},
"Git Commit": {
"allow_rewrap": "anywhere",
"preferred_line_length": 72,
"soft_wrap": "bounded"
"allow_rewrap": "anywhere"
},
"Go": {
"code_actions_on_format": {

View File

@@ -1,3 +1,7 @@
// Some example tasks for common languages.
//
// For more documentation on how to configure debug tasks,
// see: https://zed.dev/docs/debugger
[
{
"label": "Debug active PHP file",

View File

@@ -0,0 +1,5 @@
// Project-local debug tasks
//
// For more documentation on how to configure debug tasks,
// see: https://zed.dev/docs/debugger
[]

View File

@@ -311,6 +311,31 @@ impl ActivityIndicator {
});
}
if let Some(session) = self
.project
.read(cx)
.dap_store()
.read(cx)
.sessions()
.find(|s| !s.read(cx).is_started())
{
return Some(Content {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element(),
),
message: format!("Debug: {}", session.read(cx).adapter()),
tooltip_message: Some(session.read(cx).label().to_string()),
on_click: None,
});
}
let current_job = self
.project
.read(cx)

View File

@@ -55,6 +55,7 @@ use util::ResultExt as _;
use util::markdown::MarkdownCodeBlock;
use workspace::{CollaboratorId, Workspace};
use zed_actions::assistant::OpenRulesLibrary;
use zed_llm_client::CompletionIntent;
pub struct ActiveThread {
context_store: Entity<ContextStore>,
@@ -1436,6 +1437,7 @@ impl ActiveThread {
let request = language_model::LanguageModelRequest {
thread_id: None,
prompt_id: None,
intent: None,
mode: None,
messages: vec![request_message],
tools: vec![],
@@ -1597,7 +1599,12 @@ impl ActiveThread {
this.thread.update(cx, |thread, cx| {
thread.advance_prompt_id();
thread.send_to_model(model.model, Some(window.window_handle()), cx);
thread.send_to_model(
model.model,
CompletionIntent::UserPrompt,
Some(window.window_handle()),
cx,
);
});
this._load_edited_message_context_task = None;
cx.notify();
@@ -3691,7 +3698,8 @@ mod tests {
// Stream response to user message
thread.update(cx, |thread, cx| {
let request = thread.to_completion_request(model.clone(), cx);
let request =
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx);
thread.stream_completion(request, model, cx.active_window(), cx)
});
// Follow the agent

View File

@@ -52,7 +52,7 @@ use workspace::{
use zed_actions::agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding};
use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
use zed_llm_client::UsageLimit;
use zed_llm_client::{CompletionIntent, UsageLimit};
use crate::active_thread::{self, ActiveThread, ActiveThreadEvent};
use crate::agent_configuration::{AgentConfiguration, AssistantConfigurationEvent};
@@ -1296,7 +1296,12 @@ impl AgentPanel {
active_thread.thread().update(cx, |thread, cx| {
thread.insert_invisible_continue_message(cx);
thread.advance_prompt_id();
thread.send_to_model(model, Some(window.window_handle()), cx);
thread.send_to_model(
model,
CompletionIntent::UserPrompt,
Some(window.window_handle()),
cx,
);
});
});
} else {

View File

@@ -34,6 +34,7 @@ use std::{
};
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use zed_llm_client::CompletionIntent;
pub struct BufferCodegen {
alternatives: Vec<Entity<CodegenAlternative>>,
@@ -464,6 +465,7 @@ impl CodegenAlternative {
LanguageModelRequest {
thread_id: None,
prompt_id: None,
intent: Some(CompletionIntent::InlineAssist),
mode: None,
tools: Vec::new(),
tool_choice: None,

View File

@@ -42,6 +42,7 @@ use theme::ThemeSettings;
use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
use util::{ResultExt as _, maybe};
use workspace::{CollaboratorId, Workspace};
use zed_llm_client::CompletionIntent;
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
use crate::context_store::ContextStore;
@@ -375,7 +376,12 @@ impl MessageEditor {
thread
.update(cx, |thread, cx| {
thread.advance_prompt_id();
thread.send_to_model(model, Some(window_handle), cx);
thread.send_to_model(
model,
CompletionIntent::UserPrompt,
Some(window_handle),
cx,
);
})
.log_err();
})
@@ -1268,6 +1274,7 @@ impl MessageEditor {
let request = language_model::LanguageModelRequest {
thread_id: None,
prompt_id: None,
intent: None,
mode: None,
messages: vec![request_message],
tools: vec![],

View File

@@ -25,6 +25,7 @@ use terminal_view::TerminalView;
use ui::prelude::*;
use util::ResultExt;
use workspace::{Toast, Workspace, notifications::NotificationId};
use zed_llm_client::CompletionIntent;
pub fn init(
fs: Arc<dyn Fs>,
@@ -291,6 +292,7 @@ impl TerminalInlineAssistant {
thread_id: None,
prompt_id: None,
mode: None,
intent: Some(CompletionIntent::TerminalInlineAssist),
messages: vec![request_message],
tools: Vec::new(),
tool_choice: None,

View File

@@ -38,7 +38,7 @@ use thiserror::Error;
use ui::Window;
use util::{ResultExt as _, post_inc};
use uuid::Uuid;
use zed_llm_client::CompletionRequestStatus;
use zed_llm_client::{CompletionIntent, CompletionRequestStatus};
use crate::ThreadStore;
use crate::context::{AgentContext, AgentContextHandle, ContextLoadResult, LoadedContext};
@@ -1187,6 +1187,7 @@ impl Thread {
pub fn send_to_model(
&mut self,
model: Arc<dyn LanguageModel>,
intent: CompletionIntent,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
@@ -1196,7 +1197,7 @@ impl Thread {
self.remaining_turns -= 1;
let request = self.to_completion_request(model.clone(), cx);
let request = self.to_completion_request(model.clone(), intent, cx);
self.stream_completion(request, model, window, cx);
}
@@ -1216,11 +1217,13 @@ impl Thread {
pub fn to_completion_request(
&self,
model: Arc<dyn LanguageModel>,
intent: CompletionIntent,
cx: &mut Context<Self>,
) -> LanguageModelRequest {
let mut request = LanguageModelRequest {
thread_id: Some(self.id.to_string()),
prompt_id: Some(self.last_prompt_id.to_string()),
intent: Some(intent),
mode: None,
messages: vec![],
tools: Vec::new(),
@@ -1374,12 +1377,14 @@ impl Thread {
fn to_summarize_request(
&self,
model: &Arc<dyn LanguageModel>,
intent: CompletionIntent,
added_user_message: String,
cx: &App,
) -> LanguageModelRequest {
let mut request = LanguageModelRequest {
thread_id: None,
prompt_id: None,
intent: Some(intent),
mode: None,
messages: vec![],
tools: Vec::new(),
@@ -1857,7 +1862,12 @@ impl Thread {
If the conversation is about a specific subject, include it in the title. \
Be descriptive. DO NOT speak in the first person.";
let request = self.to_summarize_request(&model.model, added_user_message.into(), cx);
let request = self.to_summarize_request(
&model.model,
CompletionIntent::ThreadSummarization,
added_user_message.into(),
cx,
);
self.summary = ThreadSummary::Generating;
@@ -1958,7 +1968,12 @@ impl Thread {
4. Any action items or next steps if any\n\
Format it in Markdown with headings and bullet points.";
let request = self.to_summarize_request(&model, added_user_message.into(), cx);
let request = self.to_summarize_request(
&model,
CompletionIntent::ThreadContextSummarization,
added_user_message.into(),
cx,
);
*self.detailed_summary_tx.borrow_mut() = DetailedSummaryState::Generating {
message_id: last_message_id,
@@ -2050,7 +2065,8 @@ impl Thread {
model: Arc<dyn LanguageModel>,
) -> Vec<PendingToolUse> {
self.auto_capture_telemetry(cx);
let request = Arc::new(self.to_completion_request(model.clone(), cx));
let request =
Arc::new(self.to_completion_request(model.clone(), CompletionIntent::ToolResults, cx));
let pending_tool_uses = self
.tool_use
.pending_tool_uses()
@@ -2246,7 +2262,7 @@ impl Thread {
if self.all_tools_finished() {
if let Some(ConfiguredModel { model, .. }) = self.configured_model.as_ref() {
if !canceled {
self.send_to_model(model.clone(), window, cx);
self.send_to_model(model.clone(), CompletionIntent::ToolResults, window, cx);
}
self.auto_capture_telemetry(cx);
}
@@ -2941,7 +2957,7 @@ fn main() {{
// Check message in request
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
});
assert_eq!(request.messages.len(), 2);
@@ -3036,7 +3052,7 @@ fn main() {{
// Check entire request to make sure all contexts are properly included
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
});
// The request should contain all 3 messages
@@ -3143,7 +3159,7 @@ fn main() {{
// Check message in request
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
});
assert_eq!(request.messages.len(), 2);
@@ -3169,7 +3185,7 @@ fn main() {{
// Check that both messages appear in the request
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
});
assert_eq!(request.messages.len(), 3);
@@ -3214,7 +3230,7 @@ fn main() {{
// Create a request and check that it doesn't have a stale buffer warning yet
let initial_request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
});
// Make sure we don't have a stale file warning yet
@@ -3250,7 +3266,7 @@ fn main() {{
// Create a new request and check for the stale buffer warning
let new_request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
});
// We should have a stale file warning as the last message
@@ -3300,7 +3316,7 @@ fn main() {{
});
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
});
assert_eq!(request.temperature, Some(0.66));
@@ -3320,7 +3336,7 @@ fn main() {{
});
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
});
assert_eq!(request.temperature, Some(0.66));
@@ -3340,7 +3356,7 @@ fn main() {{
});
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
});
assert_eq!(request.temperature, Some(0.66));
@@ -3360,7 +3376,7 @@ fn main() {{
});
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(model.clone(), cx)
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
});
assert_eq!(request.temperature, None);
}
@@ -3392,7 +3408,12 @@ fn main() {{
// Send a message
thread.update(cx, |thread, cx| {
thread.insert_user_message("Hi!", ContextLoadResult::default(), None, vec![], cx);
thread.send_to_model(model.clone(), None, cx);
thread.send_to_model(
model.clone(),
CompletionIntent::ThreadSummarization,
None,
cx,
);
});
let fake_model = model.as_fake();
@@ -3487,7 +3508,7 @@ fn main() {{
vec![],
cx,
);
thread.send_to_model(model.clone(), None, cx);
thread.send_to_model(model.clone(), CompletionIntent::UserPrompt, None, cx);
});
let fake_model = model.as_fake();
@@ -3525,7 +3546,12 @@ fn main() {{
) {
thread.update(cx, |thread, cx| {
thread.insert_user_message("Hi!", ContextLoadResult::default(), None, vec![], cx);
thread.send_to_model(model.clone(), None, cx);
thread.send_to_model(
model.clone(),
CompletionIntent::ThreadSummarization,
None,
cx,
);
});
let fake_model = model.as_fake();

View File

@@ -57,6 +57,7 @@ uuid.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
language_model = { workspace = true, features = ["test-support"] }

View File

@@ -45,6 +45,7 @@ use text::{BufferSnapshot, ToPoint};
use ui::IconName;
use util::{ResultExt, TryFutureExt, post_inc};
use uuid::Uuid;
use zed_llm_client::CompletionIntent;
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ContextId(String);
@@ -2272,6 +2273,7 @@ impl AssistantContext {
let mut completion_request = LanguageModelRequest {
thread_id: None,
prompt_id: None,
intent: Some(CompletionIntent::UserPrompt),
mode: None,
messages: Vec::new(),
tools: Vec::new(),

View File

@@ -155,53 +155,35 @@ impl LanguageModelSelector {
}
fn all_models(cx: &App) -> GroupedModels {
let mut recommended = Vec::new();
let mut recommended_set = HashSet::default();
for provider in LanguageModelRegistry::global(cx)
.read(cx)
.providers()
let providers = LanguageModelRegistry::global(cx).read(cx).providers();
let recommended = providers
.iter()
{
let models = provider.recommended_models(cx);
recommended_set.extend(models.iter().map(|model| (model.provider_id(), model.id())));
recommended.extend(
.flat_map(|provider| {
provider
.recommended_models(cx)
.into_iter()
.map(move |model| ModelInfo {
model: model.clone(),
.map(|model| ModelInfo {
model,
icon: provider.icon(),
}),
);
}
let other_models = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.iter()
.map(|provider| {
(
provider.id(),
provider
.provided_models(cx)
.into_iter()
.filter_map(|model| {
let not_included =
!recommended_set.contains(&(model.provider_id(), model.id()));
not_included.then(|| ModelInfo {
model: model.clone(),
icon: provider.icon(),
})
})
.collect::<Vec<_>>(),
)
})
})
.collect::<IndexMap<_, _>>();
.collect();
GroupedModels {
recommended,
other: other_models,
}
let other = providers
.iter()
.flat_map(|provider| {
provider
.provided_models(cx)
.into_iter()
.map(|model| ModelInfo {
model,
icon: provider.icon(),
})
})
.collect();
GroupedModels::new(other, recommended)
}
pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> {
@@ -326,11 +308,14 @@ struct GroupedModels {
impl GroupedModels {
pub fn new(other: Vec<ModelInfo>, recommended: Vec<ModelInfo>) -> Self {
let recommended_ids: HashSet<_> = recommended.iter().map(|info| info.model.id()).collect();
let recommended_ids = recommended
.iter()
.map(|info| (info.model.provider_id(), info.model.id()))
.collect::<HashSet<_>>();
let mut other_by_provider: IndexMap<_, Vec<ModelInfo>> = IndexMap::default();
for model in other {
if recommended_ids.contains(&model.model.id()) {
if recommended_ids.contains(&(model.model.provider_id(), model.model.id())) {
continue;
}
@@ -917,4 +902,26 @@ mod tests {
// Recommended models should not appear in "other"
assert_models_eq(actual_other_models, vec!["zed/gemini", "copilot/o3"]);
}
#[gpui::test]
fn test_dont_exclude_models_from_other_providers(_cx: &mut TestAppContext) {
let recommended_models = create_models(vec![("zed", "claude")]);
let all_models = create_models(vec![
("zed", "claude"), // Should be filtered out from "other"
("zed", "gemini"),
("copilot", "claude"), // Should not be filtered out from "other"
]);
let grouped_models = GroupedModels::new(all_models, recommended_models);
let actual_other_models = grouped_models
.other
.values()
.flatten()
.cloned()
.collect::<Vec<_>>();
// Recommended models should not appear in "other"
assert_models_eq(actual_other_models, vec!["zed/gemini", "copilot/claude"]);
}
}

View File

@@ -28,6 +28,7 @@ use std::{cmp, iter, mem, ops::Range, path::PathBuf, pin::Pin, sync::Arc, task::
use streaming_diff::{CharOperation, StreamingDiff};
use streaming_fuzzy_matcher::StreamingFuzzyMatcher;
use util::debug_panic;
use zed_llm_client::CompletionIntent;
#[derive(Serialize)]
struct CreateFilePromptTemplate {
@@ -106,7 +107,9 @@ impl EditAgent {
edit_description,
}
.render(&this.templates)?;
let new_chunks = this.request(conversation, prompt, cx).await?;
let new_chunks = this
.request(conversation, CompletionIntent::CreateFile, prompt, cx)
.await?;
let (output, mut inner_events) = this.overwrite_with_chunks(buffer, new_chunks, cx);
while let Some(event) = inner_events.next().await {
@@ -213,7 +216,9 @@ impl EditAgent {
edit_description,
}
.render(&this.templates)?;
let edit_chunks = this.request(conversation, prompt, cx).await?;
let edit_chunks = this
.request(conversation, CompletionIntent::EditFile, prompt, cx)
.await?;
this.apply_edit_chunks(buffer, edit_chunks, events_tx, cx)
.await
});
@@ -589,6 +594,7 @@ impl EditAgent {
async fn request(
&self,
mut conversation: LanguageModelRequest,
intent: CompletionIntent,
prompt: String,
cx: &mut AsyncApp,
) -> Result<BoxStream<'static, Result<String, LanguageModelCompletionError>>> {
@@ -646,6 +652,7 @@ impl EditAgent {
let request = LanguageModelRequest {
thread_id: conversation.thread_id,
prompt_id: conversation.prompt_id,
intent: Some(intent),
mode: conversation.mode,
messages: conversation.messages,
tool_choice,

View File

@@ -298,6 +298,7 @@ pub async fn download_adapter_from_github(
response.status().to_string()
);
delegate.output_to_console("Download complete".to_owned());
match file_type {
DownloadedFileType::GzipTar => {
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
@@ -369,21 +370,19 @@ pub trait DebugAdapter: 'static + Send + Sync {
None
}
fn validate_config(
/// Extracts the kind (attach/launch) of debug configuration from the given JSON config.
/// This method should only return error when the kind cannot be determined for a given configuration;
/// in particular, it *should not* validate whether the request as a whole is valid, because that's best left to the debug adapter itself to decide.
fn request_kind(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let map = config.as_object().context("Config isn't an object")?;
let request_variant = map
.get("request")
.and_then(|val| val.as_str())
.context("request argument is not found or invalid")?;
match request_variant {
"launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
"attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
_ => Err(anyhow!("request must be either 'launch' or 'attach'")),
match config.get("request") {
Some(val) if val == "launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
Some(val) if val == "attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
_ => Err(anyhow!(
"missing or invalid `request` field in config. Expected 'launch' or 'attach'"
)),
}
}
@@ -413,7 +412,7 @@ impl DebugAdapter for FakeAdapter {
serde_json::Value::Null
}
fn validate_config(
fn request_kind(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
@@ -458,7 +457,7 @@ impl DebugAdapter for FakeAdapter {
envs: HashMap::default(),
cwd: None,
request_args: StartDebuggingRequestArguments {
request: self.validate_config(&task_definition.config)?,
request: self.request_kind(&task_definition.config)?,
configuration: task_definition.config.clone(),
},
})

View File

@@ -52,7 +52,7 @@ pub fn send_telemetry(scenario: &DebugScenario, location: TelemetrySpawnLocation
return;
};
let kind = adapter
.validate_config(&scenario.config)
.request_kind(&scenario.config)
.ok()
.map(serde_json::to_value)
.and_then(Result::ok);

View File

@@ -4,7 +4,7 @@ use dap_types::{
messages::{Message, Response},
};
use futures::{AsyncRead, AsyncReadExt as _, AsyncWrite, FutureExt as _, channel::oneshot, select};
use gpui::AsyncApp;
use gpui::{AppContext as _, AsyncApp, Task};
use settings::Settings as _;
use smallvec::SmallVec;
use smol::{
@@ -22,7 +22,7 @@ use std::{
time::Duration,
};
use task::TcpArgumentsTemplate;
use util::{ResultExt as _, TryFutureExt};
use util::{ConnectionResult, ResultExt as _};
use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings};
@@ -126,7 +126,7 @@ pub(crate) struct TransportDelegate {
pending_requests: Requests,
transport: Transport,
server_tx: Arc<Mutex<Option<Sender<Message>>>>,
_tasks: Vec<gpui::Task<Option<()>>>,
_tasks: Vec<Task<()>>,
}
impl TransportDelegate {
@@ -141,7 +141,7 @@ impl TransportDelegate {
log_handlers: Default::default(),
current_requests: Default::default(),
pending_requests: Default::default(),
_tasks: Default::default(),
_tasks: Vec::new(),
};
let messages = this.start_handlers(transport_pipes, cx).await?;
Ok((messages, this))
@@ -166,45 +166,76 @@ impl TransportDelegate {
None
};
let adapter_log_handler = log_handler.clone();
cx.update(|cx| {
if let Some(stdout) = params.stdout.take() {
self._tasks.push(
cx.background_executor()
.spawn(Self::handle_adapter_log(stdout, log_handler.clone()).log_err()),
);
self._tasks.push(cx.background_spawn(async move {
match Self::handle_adapter_log(stdout, adapter_log_handler).await {
ConnectionResult::Timeout => {
log::error!("Timed out when handling debugger log");
}
ConnectionResult::ConnectionReset => {
log::info!("Debugger logs connection closed");
}
ConnectionResult::Result(Ok(())) => {}
ConnectionResult::Result(Err(e)) => {
log::error!("Error handling debugger log: {e}");
}
}
}));
}
self._tasks.push(
cx.background_executor().spawn(
Self::handle_output(
params.output,
client_tx,
self.pending_requests.clone(),
log_handler.clone(),
)
.log_err(),
),
);
let pending_requests = self.pending_requests.clone();
let output_log_handler = log_handler.clone();
self._tasks.push(cx.background_spawn(async move {
match Self::handle_output(
params.output,
client_tx,
pending_requests,
output_log_handler,
)
.await
{
Ok(()) => {}
Err(e) => log::error!("Error handling debugger output: {e}"),
}
}));
if let Some(stderr) = params.stderr.take() {
self._tasks.push(
cx.background_executor()
.spawn(Self::handle_error(stderr, self.log_handlers.clone()).log_err()),
);
let log_handlers = self.log_handlers.clone();
self._tasks.push(cx.background_spawn(async move {
match Self::handle_error(stderr, log_handlers).await {
ConnectionResult::Timeout => {
log::error!("Timed out reading debugger error stream")
}
ConnectionResult::ConnectionReset => {
log::info!("Debugger closed its error stream")
}
ConnectionResult::Result(Ok(())) => {}
ConnectionResult::Result(Err(e)) => {
log::error!("Error handling debugger error: {e}")
}
}
}));
}
self._tasks.push(
cx.background_executor().spawn(
Self::handle_input(
params.input,
client_rx,
self.current_requests.clone(),
self.pending_requests.clone(),
log_handler.clone(),
)
.log_err(),
),
);
let current_requests = self.current_requests.clone();
let pending_requests = self.pending_requests.clone();
let log_handler = log_handler.clone();
self._tasks.push(cx.background_spawn(async move {
match Self::handle_input(
params.input,
client_rx,
current_requests,
pending_requests,
log_handler,
)
.await
{
Ok(()) => {}
Err(e) => log::error!("Error handling debugger input: {e}"),
}
}));
})?;
{
@@ -235,7 +266,7 @@ impl TransportDelegate {
async fn handle_adapter_log<Stdout>(
stdout: Stdout,
log_handlers: Option<LogHandlers>,
) -> Result<()>
) -> ConnectionResult<()>
where
Stdout: AsyncRead + Unpin + Send + 'static,
{
@@ -245,13 +276,14 @@ impl TransportDelegate {
let result = loop {
line.truncate(0);
let bytes_read = match reader.read_line(&mut line).await {
Ok(bytes_read) => bytes_read,
Err(e) => break Err(e.into()),
};
if bytes_read == 0 {
anyhow::bail!("Debugger log stream closed");
match reader
.read_line(&mut line)
.await
.context("reading adapter log line")
{
Ok(0) => break ConnectionResult::ConnectionReset,
Ok(_) => {}
Err(e) => break ConnectionResult::Result(Err(e)),
}
if let Some(log_handlers) = log_handlers.as_ref() {
@@ -337,35 +369,35 @@ impl TransportDelegate {
let mut reader = BufReader::new(server_stdout);
let result = loop {
let message =
Self::receive_server_message(&mut reader, &mut recv_buffer, log_handlers.as_ref())
.await;
match message {
Ok(Message::Response(res)) => {
match Self::receive_server_message(&mut reader, &mut recv_buffer, log_handlers.as_ref())
.await
{
ConnectionResult::Timeout => anyhow::bail!("Timed out when connecting to debugger"),
ConnectionResult::ConnectionReset => {
log::info!("Debugger closed the connection");
return Ok(());
}
ConnectionResult::Result(Ok(Message::Response(res))) => {
if let Some(tx) = pending_requests.lock().await.remove(&res.request_seq) {
if let Err(e) = tx.send(Self::process_response(res)) {
log::trace!("Did not send response `{:?}` for a cancelled", e);
}
} else {
client_tx.send(Message::Response(res)).await?;
};
}
}
Ok(message) => {
client_tx.send(message).await?;
}
Err(e) => break Err(e),
ConnectionResult::Result(Ok(message)) => client_tx.send(message).await?,
ConnectionResult::Result(Err(e)) => break Err(e),
}
};
drop(client_tx);
log::debug!("Handle adapter output dropped");
result
}
async fn handle_error<Stderr>(stderr: Stderr, log_handlers: LogHandlers) -> Result<()>
async fn handle_error<Stderr>(stderr: Stderr, log_handlers: LogHandlers) -> ConnectionResult<()>
where
Stderr: AsyncRead + Unpin + Send + 'static,
{
@@ -375,8 +407,12 @@ impl TransportDelegate {
let mut reader = BufReader::new(stderr);
let result = loop {
match reader.read_line(&mut buffer).await {
Ok(0) => anyhow::bail!("debugger error stream closed"),
match reader
.read_line(&mut buffer)
.await
.context("reading error log line")
{
Ok(0) => break ConnectionResult::ConnectionReset,
Ok(_) => {
for (kind, log_handler) in log_handlers.lock().iter_mut() {
if matches!(kind, LogKind::Adapter) {
@@ -386,7 +422,7 @@ impl TransportDelegate {
buffer.truncate(0);
}
Err(error) => break Err(error.into()),
Err(error) => break ConnectionResult::Result(Err(error)),
}
};
@@ -420,7 +456,7 @@ impl TransportDelegate {
reader: &mut BufReader<Stdout>,
buffer: &mut String,
log_handlers: Option<&LogHandlers>,
) -> Result<Message>
) -> ConnectionResult<Message>
where
Stdout: AsyncRead + Unpin + Send + 'static,
{
@@ -428,48 +464,58 @@ impl TransportDelegate {
loop {
buffer.truncate(0);
if reader
match reader
.read_line(buffer)
.await
.with_context(|| "reading a message from server")?
== 0
.with_context(|| "reading a message from server")
{
anyhow::bail!("debugger reader stream closed");
Ok(0) => return ConnectionResult::ConnectionReset,
Ok(_) => {}
Err(e) => return ConnectionResult::Result(Err(e)),
};
if buffer == "\r\n" {
break;
}
let parts = buffer.trim().split_once(": ");
match parts {
Some(("Content-Length", value)) => {
content_length = Some(value.parse().context("invalid content length")?);
if let Some(("Content-Length", value)) = buffer.trim().split_once(": ") {
match value.parse().context("invalid content length") {
Ok(length) => content_length = Some(length),
Err(e) => return ConnectionResult::Result(Err(e)),
}
_ => {}
}
}
let content_length = content_length.context("missing content length")?;
let content_length = match content_length.context("missing content length") {
Ok(length) => length,
Err(e) => return ConnectionResult::Result(Err(e)),
};
let mut content = vec![0; content_length];
reader
if let Err(e) = reader
.read_exact(&mut content)
.await
.with_context(|| "reading after a loop")?;
.with_context(|| "reading after a loop")
{
return ConnectionResult::Result(Err(e));
}
let message = std::str::from_utf8(&content).context("invalid utf8 from server")?;
let message_str = match std::str::from_utf8(&content).context("invalid utf8 from server") {
Ok(str) => str,
Err(e) => return ConnectionResult::Result(Err(e)),
};
if let Some(log_handlers) = log_handlers {
for (kind, log_handler) in log_handlers.lock().iter_mut() {
if matches!(kind, LogKind::Rpc) {
log_handler(IoKind::StdOut, &message);
log_handler(IoKind::StdOut, message_str);
}
}
}
Ok(serde_json::from_str::<Message>(message)?)
ConnectionResult::Result(
serde_json::from_str::<Message>(message_str).context("deserializing server message"),
)
}
pub async fn shutdown(&self) -> Result<()> {
@@ -658,9 +704,13 @@ impl StdioTransport {
.stderr(Stdio::piped())
.kill_on_drop(true);
let mut process = command
.spawn()
.with_context(|| "failed to spawn command.")?;
let mut process = command.spawn().with_context(|| {
format!(
"failed to spawn command `{} {}`.",
binary.command,
binary.arguments.join(" ")
)
})?;
let stdin = process.stdin.take().context("Failed to open stdin")?;
let stdout = process.stdout.take().context("Failed to open stdout")?;
@@ -773,71 +823,31 @@ impl FakeTransport {
let response_handlers = this.response_handlers.clone();
let stdout_writer = Arc::new(Mutex::new(stdout_writer));
cx.background_executor()
.spawn(async move {
let mut reader = BufReader::new(stdin_reader);
let mut buffer = String::new();
cx.background_spawn(async move {
let mut reader = BufReader::new(stdin_reader);
let mut buffer = String::new();
loop {
let message =
TransportDelegate::receive_server_message(&mut reader, &mut buffer, None)
.await;
match message {
Err(error) => {
break anyhow::anyhow!(error);
}
Ok(message) => {
match message {
Message::Request(request) => {
// redirect reverse requests to stdout writer/reader
if request.command == RunInTerminal::COMMAND
|| request.command == StartDebugging::COMMAND
{
let message =
serde_json::to_string(&Message::Request(request))
.unwrap();
let mut writer = stdout_writer.lock().await;
writer
.write_all(
TransportDelegate::build_rpc_message(message)
.as_bytes(),
)
.await
.unwrap();
writer.flush().await.unwrap();
} else {
let response = if let Some(handle) = request_handlers
.lock()
.get_mut(request.command.as_str())
{
handle(
request.seq,
request.arguments.unwrap_or(json!({})),
)
} else {
panic!("No request handler for {}", request.command);
};
let message =
serde_json::to_string(&Message::Response(response))
.unwrap();
let mut writer = stdout_writer.lock().await;
writer
.write_all(
TransportDelegate::build_rpc_message(message)
.as_bytes(),
)
.await
.unwrap();
writer.flush().await.unwrap();
}
}
Message::Event(event) => {
loop {
match TransportDelegate::receive_server_message(&mut reader, &mut buffer, None)
.await
{
ConnectionResult::Timeout => {
anyhow::bail!("Timed out when connecting to debugger");
}
ConnectionResult::ConnectionReset => {
log::info!("Debugger closed the connection");
break Ok(());
}
ConnectionResult::Result(Err(e)) => break Err(e),
ConnectionResult::Result(Ok(message)) => {
match message {
Message::Request(request) => {
// redirect reverse requests to stdout writer/reader
if request.command == RunInTerminal::COMMAND
|| request.command == StartDebugging::COMMAND
{
let message =
serde_json::to_string(&Message::Event(event)).unwrap();
serde_json::to_string(&Message::Request(request)).unwrap();
let mut writer = stdout_writer.lock().await;
writer
@@ -848,22 +858,58 @@ impl FakeTransport {
.await
.unwrap();
writer.flush().await.unwrap();
}
Message::Response(response) => {
if let Some(handle) =
response_handlers.lock().get(response.command.as_str())
} else {
let response = if let Some(handle) =
request_handlers.lock().get_mut(request.command.as_str())
{
handle(response);
handle(request.seq, request.arguments.unwrap_or(json!({})))
} else {
log::error!("No response handler for {}", response.command);
}
panic!("No request handler for {}", request.command);
};
let message =
serde_json::to_string(&Message::Response(response))
.unwrap();
let mut writer = stdout_writer.lock().await;
writer
.write_all(
TransportDelegate::build_rpc_message(message)
.as_bytes(),
)
.await
.unwrap();
writer.flush().await.unwrap();
}
}
Message::Event(event) => {
let message =
serde_json::to_string(&Message::Event(event)).unwrap();
let mut writer = stdout_writer.lock().await;
writer
.write_all(
TransportDelegate::build_rpc_message(message).as_bytes(),
)
.await
.unwrap();
writer.flush().await.unwrap();
}
Message::Response(response) => {
if let Some(handle) =
response_handlers.lock().get(response.command.as_str())
{
handle(response);
} else {
log::error!("No response handler for {}", response.command);
}
}
}
}
}
})
.detach();
}
})
.detach();
Ok((
TransportPipe::new(Box::new(stdin_writer), Box::new(stdout_reader), None, None),

View File

@@ -1,11 +1,8 @@
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Context as _, Result};
use async_trait::async_trait;
use dap::{
StartDebuggingRequestArgumentsRequest,
adapters::{DebugTaskDefinition, latest_github_release},
};
use dap::adapters::{DebugTaskDefinition, latest_github_release};
use futures::StreamExt;
use gpui::AsyncApp;
use serde_json::Value;
@@ -37,7 +34,7 @@ impl CodeLldbDebugAdapter {
Value::String(String::from(task_definition.label.as_ref())),
);
let request = self.validate_config(&configuration)?;
let request = self.request_kind(&configuration)?;
Ok(dap::StartDebuggingRequestArguments {
request,
@@ -89,48 +86,6 @@ impl DebugAdapter for CodeLldbDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let map = config
.as_object()
.ok_or_else(|| anyhow!("Config isn't an object"))?;
let request_variant = map
.get("request")
.and_then(|r| r.as_str())
.ok_or_else(|| anyhow!("request field is required and must be a string"))?;
match request_variant {
"launch" => {
// For launch, verify that one of the required configs exists
if !(map.contains_key("program")
|| map.contains_key("targetCreateCommands")
|| map.contains_key("cargo"))
{
return Err(anyhow!(
"launch request requires either 'program', 'targetCreateCommands', or 'cargo' field"
));
}
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
"attach" => {
// For attach, verify that either pid or program exists
if !(map.contains_key("pid") || map.contains_key("program")) {
return Err(anyhow!(
"attach request requires either 'pid' or 'program' field"
));
}
Ok(StartDebuggingRequestArgumentsRequest::Attach)
}
_ => Err(anyhow!(
"request must be either 'launch' or 'attach', got '{}'",
request_variant
)),
}
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut configuration = json!({
"request": match zed_scenario.request {

View File

@@ -37,7 +37,7 @@ pub fn init(cx: &mut App) {
registry.add_adapter(Arc::from(PhpDebugAdapter::default()));
registry.add_adapter(Arc::from(JsDebugAdapter::default()));
registry.add_adapter(Arc::from(RubyDebugAdapter));
registry.add_adapter(Arc::from(GoDebugAdapter));
registry.add_adapter(Arc::from(GoDebugAdapter::default()));
registry.add_adapter(Arc::from(GdbDebugAdapter));
#[cfg(any(test, feature = "test-support"))]

View File

@@ -178,7 +178,7 @@ impl DebugAdapter for GdbDebugAdapter {
let gdb_path = user_setting_path.unwrap_or(gdb_path?);
let request_args = StartDebuggingRequestArguments {
request: self.validate_config(&config.config)?,
request: self.request_kind(&config.config)?,
configuration: config.config.clone(),
};

View File

@@ -1,22 +1,87 @@
use anyhow::{Context as _, anyhow, bail};
use anyhow::{Context as _, bail};
use dap::{
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
adapters::DebugTaskDefinition,
StartDebuggingRequestArguments,
adapters::{
DebugTaskDefinition, DownloadedFileType, download_adapter_from_github,
latest_github_release,
},
};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
use std::{collections::HashMap, env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock};
use util;
use crate::*;
#[derive(Default, Debug)]
pub(crate) struct GoDebugAdapter;
pub(crate) struct GoDebugAdapter {
shim_path: OnceLock<PathBuf>,
}
impl GoDebugAdapter {
const ADAPTER_NAME: &'static str = "Delve";
const DEFAULT_TIMEOUT_MS: u64 = 60000;
async fn fetch_latest_adapter_version(
delegate: &Arc<dyn DapDelegate>,
) -> Result<AdapterVersion> {
let release = latest_github_release(
&"zed-industries/delve-shim-dap",
true,
false,
delegate.http_client(),
)
.await?;
let os = match consts::OS {
"macos" => "apple-darwin",
"linux" => "unknown-linux-gnu",
"windows" => "pc-windows-msvc",
other => bail!("Running on unsupported os: {other}"),
};
let suffix = if consts::OS == "windows" {
".zip"
} else {
".tar.gz"
};
let asset_name = format!("delve-shim-dap-{}-{os}{suffix}", consts::ARCH);
let asset = release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
Ok(AdapterVersion {
tag_name: release.tag_name,
url: asset.browser_download_url.clone(),
})
}
async fn install_shim(&self, delegate: &Arc<dyn DapDelegate>) -> anyhow::Result<PathBuf> {
if let Some(path) = self.shim_path.get().cloned() {
return Ok(path);
}
let asset = Self::fetch_latest_adapter_version(delegate).await?;
let ty = if consts::OS == "windows" {
DownloadedFileType::Zip
} else {
DownloadedFileType::GzipTar
};
download_adapter_from_github(
"delve-shim-dap".into(),
asset.clone(),
ty,
delegate.as_ref(),
)
.await?;
let path = paths::debug_adapters_dir()
.join("delve-shim-dap")
.join(format!("delve-shim-dap_{}", asset.tag_name))
.join(format!("delve-shim-dap{}", std::env::consts::EXE_SUFFIX));
self.shim_path.set(path.clone()).ok();
Ok(path)
}
}
#[async_trait(?Send)]
@@ -285,24 +350,6 @@ impl DebugAdapter for GoDebugAdapter {
})
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let map = config.as_object().context("Config isn't an object")?;
let request_variant = map
.get("request")
.and_then(|val| val.as_str())
.context("request argument is not found or invalid")?;
match request_variant {
"launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
"attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
_ => Err(anyhow!("request must be either 'launch' or 'attach'")),
}
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut args = match &zed_scenario.request {
dap::DebugRequest::Attach(attach_config) => {
@@ -349,13 +396,15 @@ impl DebugAdapter for GoDebugAdapter {
&self,
delegate: &Arc<dyn DapDelegate>,
task_definition: &DebugTaskDefinition,
_user_installed_path: Option<PathBuf>,
user_installed_path: Option<PathBuf>,
_cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
let dlv_path = adapter_path.join("dlv");
let delve_path = if let Some(path) = delegate.which(OsStr::new("dlv")).await {
let delve_path = if let Some(path) = user_installed_path {
path.to_string_lossy().to_string()
} else if let Some(path) = delegate.which(OsStr::new("dlv")).await {
path.to_string_lossy().to_string()
} else if delegate.fs().is_file(&dlv_path).await {
dlv_path.to_string_lossy().to_string()
@@ -384,16 +433,10 @@ impl DebugAdapter for GoDebugAdapter {
adapter_path.join("dlv").to_string_lossy().to_string()
};
let minidelve_path = self.install_shim(delegate).await?;
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
let mut tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
if tcp_connection.timeout.is_none()
|| tcp_connection.timeout.unwrap_or(0) < Self::DEFAULT_TIMEOUT_MS
{
tcp_connection.timeout = Some(Self::DEFAULT_TIMEOUT_MS);
}
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
let (host, port, _) = crate::configure_tcp_connection(tcp_connection).await?;
let cwd = task_definition
.config
@@ -404,6 +447,7 @@ impl DebugAdapter for GoDebugAdapter {
let arguments = if cfg!(windows) {
vec![
delve_path,
"dap".into(),
"--listen".into(),
format!("{}:{}", host, port),
@@ -411,6 +455,7 @@ impl DebugAdapter for GoDebugAdapter {
]
} else {
vec![
delve_path,
"dap".into(),
"--listen".into(),
format!("{}:{}", host, port),
@@ -418,18 +463,14 @@ impl DebugAdapter for GoDebugAdapter {
};
Ok(DebugAdapterBinary {
command: delve_path,
command: minidelve_path.to_string_lossy().into_owned(),
arguments,
cwd: Some(cwd),
envs: HashMap::default(),
connection: Some(adapters::TcpArguments {
host,
port,
timeout,
}),
connection: None,
request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(),
request: self.validate_config(&task_definition.config)?,
request: self.request_kind(&task_definition.config)?,
},
})
}

View File

@@ -1,9 +1,6 @@
use adapters::latest_github_release;
use anyhow::{Context as _, anyhow};
use dap::{
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
adapters::DebugTaskDefinition,
};
use anyhow::Context as _;
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::AsyncApp;
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use task::DebugRequest;
@@ -26,7 +23,7 @@ impl JsDebugAdapter {
delegate: &Arc<dyn DapDelegate>,
) -> Result<AdapterVersion> {
let release = latest_github_release(
&format!("{}/{}", "microsoft", Self::ADAPTER_NPM_NAME),
&format!("microsoft/{}", Self::ADAPTER_NPM_NAME),
true,
false,
delegate.http_client(),
@@ -95,7 +92,7 @@ impl JsDebugAdapter {
}),
request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(),
request: self.validate_config(&task_definition.config)?,
request: self.request_kind(&task_definition.config)?,
},
})
}
@@ -107,29 +104,6 @@ impl DebugAdapter for JsDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<dap::StartDebuggingRequestArgumentsRequest> {
match config.get("request") {
Some(val) if val == "launch" => {
if config.get("program").is_none() && config.get("url").is_none() {
return Err(anyhow!(
"either program or url is required for launch request"
));
}
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
Some(val) if val == "attach" => {
if !config.get("processId").is_some_and(|val| val.is_u64()) {
return Err(anyhow!("processId must be a number"));
}
Ok(StartDebuggingRequestArgumentsRequest::Attach)
}
_ => Err(anyhow!("missing or invalid request field in config")),
}
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut args = json!({
"type": "pwa-node",
@@ -449,6 +423,8 @@ impl DebugAdapter for JsDebugAdapter {
delegate.as_ref(),
)
.await?;
} else {
delegate.output_to_console(format!("{} debug adapter is up to date", self.name()));
}
}

View File

@@ -94,7 +94,7 @@ impl PhpDebugAdapter {
envs: HashMap::default(),
request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(),
request: <Self as DebugAdapter>::validate_config(self, &task_definition.config)?,
request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)?,
},
})
}
@@ -149,22 +149,8 @@ impl DebugAdapter for PhpDebugAdapter {
"default": false
},
"pathMappings": {
"type": "array",
"description": "A list of server paths mapping to the local source paths on your machine for remote host debugging",
"items": {
"type": "object",
"properties": {
"serverPath": {
"type": "string",
"description": "Path on the server"
},
"localPath": {
"type": "string",
"description": "Corresponding path on the local machine"
}
},
"required": ["serverPath", "localPath"]
}
"type": "object",
"description": "A mapping of server paths to local paths.",
},
"log": {
"type": "boolean",
@@ -296,10 +282,7 @@ impl DebugAdapter for PhpDebugAdapter {
Some(SharedString::new_static("PHP").into())
}
fn validate_config(
&self,
_: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
fn request_kind(&self, _: &serde_json::Value) -> Result<StartDebuggingRequestArgumentsRequest> {
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}

View File

@@ -1,9 +1,6 @@
use crate::*;
use anyhow::{Context as _, anyhow};
use dap::{
DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
adapters::DebugTaskDefinition,
};
use anyhow::Context as _;
use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::{AsyncApp, SharedString};
use json_dotpath::DotPaths;
use language::{LanguageName, Toolchain};
@@ -86,7 +83,7 @@ impl PythonDebugAdapter {
&self,
task_definition: &DebugTaskDefinition,
) -> Result<StartDebuggingRequestArguments> {
let request = self.validate_config(&task_definition.config)?;
let request = self.request_kind(&task_definition.config)?;
let mut configuration = task_definition.config.clone();
if let Ok(console) = configuration.dot_get_mut("console") {
@@ -254,24 +251,6 @@ impl DebugAdapter for PythonDebugAdapter {
})
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let map = config.as_object().context("Config isn't an object")?;
let request_variant = map
.get("request")
.and_then(|val| val.as_str())
.context("request is not valid")?;
match request_variant {
"launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
"attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
_ => Err(anyhow!("request must be either 'launch' or 'attach'")),
}
}
async fn dap_schema(&self) -> serde_json::Value {
json!({
"properties": {
@@ -660,7 +639,7 @@ impl DebugAdapter for PythonDebugAdapter {
}
}
self.get_installed_binary(delegate, &config, None, None, false)
self.get_installed_binary(delegate, &config, None, toolchain, false)
.await
}
}

View File

@@ -265,7 +265,7 @@ impl DebugAdapter for RubyDebugAdapter {
cwd: None,
envs: std::collections::HashMap::default(),
request_args: StartDebuggingRequestArguments {
request: self.validate_config(&definition.config)?,
request: self.request_kind(&definition.config)?,
configuration: definition.config.clone(),
},
})

View File

@@ -50,6 +50,7 @@ project.workspace = true
rpc.workspace = true
serde.workspace = true
serde_json.workspace = true
# serde_json_lenient.workspace = true
settings.workspace = true
shlex.workspace = true
sysinfo.workspace = true

View File

@@ -3,11 +3,12 @@ use crate::session::DebugSession;
use crate::session::running::RunningState;
use crate::{
ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart,
ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
ToggleSessionPicker, ToggleThreadPicker, persistence,
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, NewProcessModal,
NewProcessMode, Pause, Restart, ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop,
ToggleExpandItem, ToggleIgnoreBreakpoints, ToggleSessionPicker, ToggleThreadPicker,
persistence, spawn_task_or_modal,
};
use anyhow::{Context as _, Result, anyhow};
use anyhow::Result;
use command_palette_hooks::CommandPaletteFilter;
use dap::StartDebuggingRequestArguments;
use dap::adapters::DebugAdapterName;
@@ -24,7 +25,7 @@ use gpui::{
use language::Buffer;
use project::debugger::session::{Session, SessionStateEvent};
use project::{Fs, ProjectPath, WorktreeId};
use project::{Fs, WorktreeId};
use project::{Project, debugger::session::ThreadStatus};
use rpc::proto::{self};
use settings::Settings;
@@ -65,9 +66,11 @@ pub struct DebugPanel {
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
debug_scenario_scheduled_last: bool,
pub(crate) thread_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
pub(crate) session_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
fs: Arc<dyn Fs>,
is_zoomed: bool,
_subscriptions: [Subscription; 1],
}
@@ -102,7 +105,9 @@ impl DebugPanel {
fs: workspace.app_state().fs.clone(),
thread_picker_menu_handle,
session_picker_menu_handle,
is_zoomed: false,
_subscriptions: [focus_subscription],
debug_scenario_scheduled_last: true,
}
})
}
@@ -264,6 +269,7 @@ impl DebugPanel {
cx,
)
});
self.debug_scenario_scheduled_last = true;
if let Some(inventory) = self
.project
.read(cx)
@@ -331,10 +337,17 @@ impl DebugPanel {
let Some(task_inventory) = task_store.read(cx).task_inventory() else {
return;
};
let workspace = self.workspace.clone();
let Some(scenario) = task_inventory.read(cx).last_scheduled_scenario().cloned() else {
window.defer(cx, move |window, cx| {
workspace
.update(cx, |workspace, cx| {
NewProcessModal::show(workspace, window, NewProcessMode::Launch, None, cx);
})
.ok();
});
return;
};
let workspace = self.workspace.clone();
cx.spawn_in(window, async move |this, cx| {
let task_contexts = workspace
@@ -432,7 +445,10 @@ impl DebugPanel {
};
let dap_store_handle = self.project.read(cx).dap_store().clone();
let label = parent_session.read(cx).label().clone();
let mut label = parent_session.read(cx).label().clone();
if !label.ends_with("(child)") {
label = format!("{label} (child)").into();
}
let adapter = parent_session.read(cx).adapter().clone();
let mut binary = parent_session.read(cx).binary().clone();
binary.request_args = request.clone();
@@ -936,68 +952,69 @@ impl DebugPanel {
cx.notify();
}
pub(crate) fn save_scenario(
&self,
scenario: &DebugScenario,
worktree_id: WorktreeId,
window: &mut Window,
cx: &mut App,
) -> Task<Result<ProjectPath>> {
self.workspace
.update(cx, |workspace, cx| {
let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
return Task::ready(Err(anyhow!("Couldn't get worktree path")));
};
// TODO: restore once we have proper comment preserving file edits
// pub(crate) fn save_scenario(
// &self,
// scenario: &DebugScenario,
// worktree_id: WorktreeId,
// window: &mut Window,
// cx: &mut App,
// ) -> Task<Result<ProjectPath>> {
// self.workspace
// .update(cx, |workspace, cx| {
// let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
// return Task::ready(Err(anyhow!("Couldn't get worktree path")));
// };
let serialized_scenario = serde_json::to_value(scenario);
// let serialized_scenario = serde_json::to_value(scenario);
cx.spawn_in(window, async move |workspace, cx| {
let serialized_scenario = serialized_scenario?;
let fs =
workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
// cx.spawn_in(window, async move |workspace, cx| {
// let serialized_scenario = serialized_scenario?;
// let fs =
// workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
path.push(paths::local_settings_folder_relative_path());
if !fs.is_dir(path.as_path()).await {
fs.create_dir(path.as_path()).await?;
}
path.pop();
// path.push(paths::local_settings_folder_relative_path());
// if !fs.is_dir(path.as_path()).await {
// fs.create_dir(path.as_path()).await?;
// }
// path.pop();
path.push(paths::local_debug_file_relative_path());
let path = path.as_path();
// path.push(paths::local_debug_file_relative_path());
// let path = path.as_path();
if !fs.is_file(path).await {
let content =
serde_json::to_string_pretty(&serde_json::Value::Array(vec![
serialized_scenario,
]))?;
// if !fs.is_file(path).await {
// fs.create_file(path, Default::default()).await?;
// fs.write(
// path,
// initial_local_debug_tasks_content().to_string().as_bytes(),
// )
// .await?;
// }
fs.create_file(path, Default::default()).await?;
fs.save(path, &content.into(), Default::default()).await?;
} else {
let content = fs.load(path).await?;
let mut values = serde_json::from_str::<Vec<serde_json::Value>>(&content)?;
values.push(serialized_scenario);
fs.save(
path,
&serde_json::to_string_pretty(&values).map(Into::into)?,
Default::default(),
)
.await?;
}
// let content = fs.load(path).await?;
// let mut values =
// serde_json_lenient::from_str::<Vec<serde_json::Value>>(&content)?;
// values.push(serialized_scenario);
// fs.save(
// path,
// &serde_json_lenient::to_string_pretty(&values).map(Into::into)?,
// Default::default(),
// )
// .await?;
workspace.update(cx, |workspace, cx| {
workspace
.project()
.read(cx)
.project_path_for_absolute_path(&path, cx)
.context(
"Couldn't get project path for .zed/debug.json in active worktree",
)
})?
})
})
.unwrap_or_else(|err| Task::ready(Err(err)))
}
// workspace.update(cx, |workspace, cx| {
// workspace
// .project()
// .read(cx)
// .project_path_for_absolute_path(&path, cx)
// .context(
// "Couldn't get project path for .zed/debug.json in active worktree",
// )
// })?
// })
// })
// .unwrap_or_else(|err| Task::ready(Err(err)))
// }
pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.thread_picker_menu_handle.toggle(window, cx);
@@ -1006,6 +1023,22 @@ impl DebugPanel {
pub(crate) fn toggle_session_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.session_picker_menu_handle.toggle(window, cx);
}
fn toggle_zoom(
&mut self,
_: &workspace::ToggleZoom,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.is_zoomed {
cx.emit(PanelEvent::ZoomOut);
} else {
if !self.focus_handle(cx).contains_focused(window, cx) {
cx.focus_self(window);
}
cx.emit(PanelEvent::ZoomIn);
}
}
}
async fn register_session_inner(
@@ -1161,6 +1194,15 @@ impl Panel for DebugPanel {
}
fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
self.is_zoomed
}
fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
self.is_zoomed = zoomed;
cx.notify();
}
}
impl Render for DebugPanel {
@@ -1301,6 +1343,23 @@ impl Render for DebugPanel {
.ok();
}
})
.on_action(cx.listener(Self::toggle_zoom))
.on_action(cx.listener(|panel, _: &ToggleExpandItem, _, cx| {
let Some(session) = panel.active_session() else {
return;
};
let active_pane = session
.read(cx)
.running_state()
.read(cx)
.active_pane()
.clone();
active_pane.update(cx, |pane, cx| {
let is_zoomed = pane.is_zoomed();
pane.set_zoomed(!is_zoomed, cx);
});
cx.notify();
}))
.when(self.active_session.is_some(), |this| {
this.on_mouse_down(
MouseButton::Right,
@@ -1378,4 +1437,36 @@ impl workspace::DebuggerProvider for DebuggerProvider {
})
})
}
fn spawn_task_or_modal(
&self,
workspace: &mut Workspace,
action: &tasks_ui::Spawn,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
spawn_task_or_modal(workspace, action, window, cx);
}
fn debug_scenario_scheduled(&self, cx: &mut App) {
self.0.update(cx, |this, _| {
this.debug_scenario_scheduled_last = true;
});
}
fn task_scheduled(&self, cx: &mut App) {
self.0.update(cx, |this, _| {
this.debug_scenario_scheduled_last = false;
})
}
fn debug_scenario_scheduled_last(&self, cx: &App) -> bool {
self.0.read(cx).debug_scenario_scheduled_last
}
fn active_thread_state(&self, cx: &App) -> Option<ThreadStatus> {
let session = self.0.read(cx).active_session()?;
let thread = session.read(cx).running_state().read(cx).thread_id()?;
session.read(cx).session(cx).read(cx).thread_state(thread)
}
}

View File

@@ -3,18 +3,19 @@ use debugger_panel::{DebugPanel, ToggleFocus};
use editor::Editor;
use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt};
use gpui::{App, EntityInputHandler, actions};
use new_session_modal::NewSessionModal;
use new_process_modal::{NewProcessModal, NewProcessMode};
use project::debugger::{self, breakpoint_store::SourceBreakpoint};
use session::DebugSession;
use settings::Settings;
use stack_trace_view::StackTraceView;
use tasks_ui::{Spawn, TaskOverrides};
use util::maybe;
use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
pub mod attach_modal;
pub mod debugger_panel;
mod dropdown_menus;
mod new_session_modal;
mod new_process_modal;
mod persistence;
pub(crate) mod session;
mod stack_trace_view;
@@ -48,6 +49,7 @@ actions!(
ToggleThreadPicker,
ToggleSessionPicker,
RerunLastSession,
ToggleExpandItem,
]
);
@@ -62,6 +64,7 @@ pub fn init(cx: &mut App) {
cx.when_flag_enabled::<DebuggerFeatureFlag>(window, |workspace, _, _| {
workspace
.register_action(spawn_task_or_modal)
.register_action(|workspace, _: &ToggleFocus, window, cx| {
workspace.toggle_panel_focus::<DebugPanel>(window, cx);
})
@@ -208,7 +211,7 @@ pub fn init(cx: &mut App) {
},
)
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
NewSessionModal::show(workspace, window, cx);
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
})
.register_action(
|workspace: &mut Workspace, _: &RerunLastSession, window, cx| {
@@ -309,3 +312,48 @@ pub fn init(cx: &mut App) {
})
.detach();
}
fn spawn_task_or_modal(
workspace: &mut Workspace,
action: &Spawn,
window: &mut ui::Window,
cx: &mut ui::Context<Workspace>,
) {
match action {
Spawn::ByName {
task_name,
reveal_target,
} => {
let overrides = reveal_target.map(|reveal_target| TaskOverrides {
reveal_target: Some(reveal_target),
});
let name = task_name.clone();
tasks_ui::spawn_tasks_filtered(
move |(_, task)| task.label.eq(&name),
overrides,
window,
cx,
)
.detach_and_log_err(cx)
}
Spawn::ByTag {
task_tag,
reveal_target,
} => {
let overrides = reveal_target.map(|reveal_target| TaskOverrides {
reveal_target: Some(reveal_target),
});
let tag = task_tag.clone();
tasks_ui::spawn_tasks_filtered(
move |(_, task)| task.tags.contains(&tag),
overrides,
window,
cx,
)
.detach_and_log_err(cx)
}
Spawn::ViaModal { reveal_target } => {
NewProcessModal::show(workspace, window, NewProcessMode::Task, *reveal_target, cx);
}
}
}

View File

@@ -1,4 +1,6 @@
use gpui::Entity;
use std::time::Duration;
use gpui::{Animation, AnimationExt as _, Entity, Transformation, percentage};
use project::debugger::session::{ThreadId, ThreadStatus};
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
@@ -23,31 +25,40 @@ impl DebugPanel {
let sessions = self.sessions().clone();
let weak = cx.weak_entity();
let running_state = running_state.read(cx);
let label = if let Some(active_session) = active_session {
let label = if let Some(active_session) = active_session.clone() {
active_session.read(cx).session(cx).read(cx).label()
} else {
SharedString::new_static("Unknown Session")
};
let is_terminated = running_state.session().read(cx).is_terminated();
let session_state_indicator = {
if is_terminated {
Some(Indicator::dot().color(Color::Error))
} else {
match running_state.thread_status(cx).unwrap_or_default() {
project::debugger::session::ThreadStatus::Stopped => {
Some(Indicator::dot().color(Color::Conflict))
}
_ => Some(Indicator::dot().color(Color::Success)),
let is_started = active_session
.is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
let session_state_indicator = if is_terminated {
Indicator::dot().color(Color::Error).into_any_element()
} else if !is_started {
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Muted)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element()
} else {
match running_state.thread_status(cx).unwrap_or_default() {
ThreadStatus::Stopped => {
Indicator::dot().color(Color::Conflict).into_any_element()
}
_ => Indicator::dot().color(Color::Success).into_any_element(),
}
};
let trigger = h_flex()
.gap_2()
.when_some(session_state_indicator, |this, indicator| {
this.child(indicator)
})
.child(session_state_indicator)
.justify_between()
.child(
DebugPanel::dropdown_label(label)

View File

@@ -61,6 +61,28 @@ impl DebuggerPaneItem {
DebuggerPaneItem::Terminal => SharedString::new_static("Terminal"),
}
}
pub(crate) fn tab_tooltip(self) -> SharedString {
let tooltip = match self {
DebuggerPaneItem::Console => {
"Displays program output and allows manual input of debugger commands."
}
DebuggerPaneItem::Variables => {
"Shows current values of local and global variables in the current stack frame."
}
DebuggerPaneItem::BreakpointList => "Lists all active breakpoints set in the code.",
DebuggerPaneItem::Frames => {
"Displays the call stack, letting you navigate between function calls."
}
DebuggerPaneItem::Modules => "Shows all modules or libraries loaded by the program.",
DebuggerPaneItem::LoadedSources => {
"Lists all source files currently loaded and used by the debugger."
}
DebuggerPaneItem::Terminal => {
"Provides an interactive terminal session within the debugging environment."
}
};
SharedString::new_static(tooltip)
}
}
impl From<DebuggerPaneItem> for SharedString {

View File

@@ -8,7 +8,8 @@ pub mod variable_list;
use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
use crate::{
new_session_modal::resolve_path,
ToggleExpandItem,
new_process_modal::resolve_path,
persistence::{self, DebuggerPaneItem, SerializedLayout},
};
@@ -173,6 +174,10 @@ impl Item for SubView {
self.kind.to_shared_string()
}
fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
Some(self.kind.tab_tooltip())
}
fn tab_content(
&self,
params: workspace::item::TabContentParams,
@@ -343,6 +348,7 @@ pub(crate) fn new_debugger_pane(
false
}
})));
pane.set_can_toggle_zoom(false, cx);
pane.display_nav_history_buttons(None);
pane.set_custom_drop_handle(cx, custom_drop_handle);
pane.set_should_display_tab_bar(|_, _| true);
@@ -399,6 +405,9 @@ pub(crate) fn new_debugger_pane(
.p_1()
.rounded_md()
.cursor_pointer()
.when_some(item.tab_tooltip_text(cx), |this, tooltip| {
this.tooltip(Tooltip::text(tooltip))
})
.map(|this| {
let theme = cx.theme();
if selected {
@@ -465,17 +474,19 @@ pub(crate) fn new_debugger_pane(
},
)
.icon_size(IconSize::XSmall)
.on_click(cx.listener(move |pane, _, window, cx| {
pane.toggle_zoom(&workspace::ToggleZoom, window, cx);
.on_click(cx.listener(move |pane, _, _, cx| {
let is_zoomed = pane.is_zoomed();
pane.set_zoomed(!is_zoomed, cx);
cx.notify();
}))
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
let zoomed_text =
if zoomed { "Zoom Out" } else { "Zoom In" };
if zoomed { "Minimize" } else { "Expand" };
Tooltip::for_action_in(
zoomed_text,
&workspace::ToggleZoom,
&ToggleExpandItem,
&focus_handle,
window,
cx,
@@ -559,7 +570,7 @@ impl RunningState {
}
}
pub(crate) fn relativlize_paths(
pub(crate) fn relativize_paths(
key: Option<&str>,
config: &mut serde_json::Value,
context: &TaskContext,
@@ -567,12 +578,12 @@ impl RunningState {
match config {
serde_json::Value::Object(obj) => {
obj.iter_mut()
.for_each(|(key, value)| Self::relativlize_paths(Some(key), value, context));
.for_each(|(key, value)| Self::relativize_paths(Some(key), value, context));
}
serde_json::Value::Array(array) => {
array
.iter_mut()
.for_each(|value| Self::relativlize_paths(None, value, context));
.for_each(|value| Self::relativize_paths(None, value, context));
}
serde_json::Value::String(s) if key == Some("program") || key == Some("cwd") => {
// Some built-in zed tasks wrap their arguments in quotes as they might contain spaces.
@@ -799,13 +810,13 @@ impl RunningState {
mut config,
tcp_connection,
} = scenario;
Self::relativlize_paths(None, &mut config, &task_context);
Self::relativize_paths(None, &mut config, &task_context);
Self::substitute_variables_in_config(&mut config, &task_context);
let request_type = dap_registry
.adapter(&adapter)
.ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))
.and_then(|adapter| adapter.validate_config(&config));
.and_then(|adapter| adapter.request_kind(&config));
let config_is_valid = request_type.is_ok();
@@ -874,7 +885,6 @@ impl RunningState {
args,
..task.resolved.clone()
};
let terminal = project
.update_in(cx, |project, window, cx| {
project.create_terminal(
@@ -919,6 +929,12 @@ impl RunningState {
};
if config_is_valid {
// Ok(DebugTaskDefinition {
// label,
// adapter: DebugAdapterName(adapter),
// config,
// tcp_connection,
// })
} else if let Some((task, locator_name)) = build_output {
let locator_name =
locator_name.context("Could not find a valid locator for a build task")?;
@@ -937,12 +953,15 @@ impl RunningState {
let scenario = dap_registry
.adapter(&adapter)
.context(format!("{}: is not a valid adapter name", &adapter))
.ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))
.map(|adapter| adapter.config_from_zed_format(zed_config))??;
config = scenario.config;
Self::substitute_variables_in_config(&mut config, &task_context);
} else {
anyhow::bail!("No request or build provided");
let Err(e) = request_type else {
unreachable!();
};
anyhow::bail!("Zed cannot determine how to run this debug scenario. `build` field was not provided and Debug Adapter won't accept provided configuration because: {e}");
};
Ok(DebugTaskDefinition {
@@ -1245,18 +1264,6 @@ impl RunningState {
Event::Focus => {
this.active_pane = source_pane.clone();
}
Event::ZoomIn => {
source_pane.update(cx, |pane, cx| {
pane.set_zoomed(true, cx);
});
cx.notify();
}
Event::ZoomOut => {
source_pane.update(cx, |pane, cx| {
pane.set_zoomed(false, cx);
});
cx.notify();
}
_ => {}
}
}

View File

@@ -110,7 +110,7 @@ impl Console {
}
fn is_running(&self, cx: &Context<Self>) -> bool {
self.session.read(cx).is_local()
self.session.read(cx).is_running()
}
fn handle_stack_frame_list_events(
@@ -176,16 +176,18 @@ impl Console {
}
fn render_console(&self, cx: &Context<Self>) -> impl IntoElement {
EditorElement::new(&self.console, self.editor_style(cx))
EditorElement::new(&self.console, Self::editor_style(&self.console, cx))
}
fn editor_style(&self, cx: &Context<Self>) -> EditorStyle {
fn editor_style(editor: &Entity<Editor>, cx: &Context<Self>) -> EditorStyle {
let is_read_only = editor.read(cx).read_only(cx);
let settings = ThemeSettings::get_global(cx);
let theme = cx.theme();
let text_style = TextStyle {
color: if self.console.read(cx).read_only(cx) {
cx.theme().colors().text_disabled
color: if is_read_only {
theme.colors().text_muted
} else {
cx.theme().colors().text
theme.colors().text
},
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
@@ -195,15 +197,15 @@ impl Console {
..Default::default()
};
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
background: theme.colors().editor_background,
local_player: theme.players().local(),
text: text_style,
..Default::default()
}
}
fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
EditorElement::new(&self.query_bar, self.editor_style(cx))
EditorElement::new(&self.query_bar, Self::editor_style(&self.query_bar, cx))
}
fn update_output(&mut self, window: &mut Window, cx: &mut Context<Self>) {

View File

@@ -250,9 +250,6 @@ impl StackFrameList {
let Some(abs_path) = Self::abs_path_from_stack_frame(&stack_frame) else {
return Task::ready(Err(anyhow!("Project path not found")));
};
if abs_path.starts_with("<node_internals>") {
return Task::ready(Ok(()));
}
let row = stack_frame.line.saturating_sub(1) as u32;
cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
stack_frame_id,
@@ -345,6 +342,7 @@ impl StackFrameList {
s.path
.as_deref()
.map(|path| Arc::<Path>::from(Path::new(path)))
.filter(|path| path.is_absolute())
})
}

View File

@@ -25,7 +25,7 @@ mod inline_values;
#[cfg(test)]
mod module_list;
#[cfg(test)]
mod new_session_modal;
mod new_process_modal;
#[cfg(test)]
mod persistence;
#[cfg(test)]

View File

@@ -1,12 +1,13 @@
use dap::DapRegistry;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Fs, Project};
use project::{FakeFs, Project};
use serde_json::json;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use task::{DebugRequest, DebugScenario, LaunchRequest, TaskContext, VariableName, ZedDebugConfig};
use util::path;
// use crate::new_process_modal::NewProcessMode;
use crate::tests::{init_test, init_test_workspace};
#[gpui::test]
@@ -151,105 +152,111 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths(
}
}
#[gpui::test]
async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
// #[gpui::test]
// async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
// init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/project"),
json!({
"main.rs": "fn main() {}"
}),
)
.await;
// let fs = FakeFs::new(executor.clone());
// fs.insert_tree(
// path!("/project"),
// json!({
// "main.rs": "fn main() {}"
// }),
// )
// .await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
// let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
// let workspace = init_test_workspace(&project, cx).await;
// let cx = &mut VisualTestContext::from_window(*workspace, cx);
workspace
.update(cx, |workspace, window, cx| {
crate::new_session_modal::NewSessionModal::show(workspace, window, cx);
})
.unwrap();
// workspace
// .update(cx, |workspace, window, cx| {
// crate::new_process_modal::NewProcessModal::show(
// workspace,
// window,
// NewProcessMode::Debug,
// None,
// cx,
// );
// })
// .unwrap();
cx.run_until_parked();
// cx.run_until_parked();
let modal = workspace
.update(cx, |workspace, _, cx| {
workspace.active_modal::<crate::new_session_modal::NewSessionModal>(cx)
})
.unwrap()
.expect("Modal should be active");
// let modal = workspace
// .update(cx, |workspace, _, cx| {
// workspace.active_modal::<crate::new_process_modal::NewProcessModal>(cx)
// })
// .unwrap()
// .expect("Modal should be active");
modal.update_in(cx, |modal, window, cx| {
modal.set_custom("/project/main", "/project", false, window, cx);
modal.save_scenario(window, cx);
});
// modal.update_in(cx, |modal, window, cx| {
// modal.set_configure("/project/main", "/project", false, window, cx);
// modal.save_scenario(window, cx);
// });
cx.executor().run_until_parked();
// cx.executor().run_until_parked();
let debug_json_content = fs
.load(path!("/project/.zed/debug.json").as_ref())
.await
.expect("debug.json should exist");
// let debug_json_content = fs
// .load(path!("/project/.zed/debug.json").as_ref())
// .await
// .expect("debug.json should exist");
let expected_content = vec![
"[",
" {",
r#" "adapter": "fake-adapter","#,
r#" "label": "main (fake-adapter)","#,
r#" "request": "launch","#,
r#" "program": "/project/main","#,
r#" "cwd": "/project","#,
r#" "args": [],"#,
r#" "env": {}"#,
" }",
"]",
];
// let expected_content = vec![
// "[",
// " {",
// r#" "adapter": "fake-adapter","#,
// r#" "label": "main (fake-adapter)","#,
// r#" "request": "launch","#,
// r#" "program": "/project/main","#,
// r#" "cwd": "/project","#,
// r#" "args": [],"#,
// r#" "env": {}"#,
// " }",
// "]",
// ];
let actual_lines: Vec<&str> = debug_json_content.lines().collect();
pretty_assertions::assert_eq!(expected_content, actual_lines);
// let actual_lines: Vec<&str> = debug_json_content.lines().collect();
// pretty_assertions::assert_eq!(expected_content, actual_lines);
modal.update_in(cx, |modal, window, cx| {
modal.set_custom("/project/other", "/project", true, window, cx);
modal.save_scenario(window, cx);
});
// modal.update_in(cx, |modal, window, cx| {
// modal.set_configure("/project/other", "/project", true, window, cx);
// modal.save_scenario(window, cx);
// });
cx.executor().run_until_parked();
// cx.executor().run_until_parked();
let debug_json_content = fs
.load(path!("/project/.zed/debug.json").as_ref())
.await
.expect("debug.json should exist after second save");
// let debug_json_content = fs
// .load(path!("/project/.zed/debug.json").as_ref())
// .await
// .expect("debug.json should exist after second save");
let expected_content = vec![
"[",
" {",
r#" "adapter": "fake-adapter","#,
r#" "label": "main (fake-adapter)","#,
r#" "request": "launch","#,
r#" "program": "/project/main","#,
r#" "cwd": "/project","#,
r#" "args": [],"#,
r#" "env": {}"#,
" },",
" {",
r#" "adapter": "fake-adapter","#,
r#" "label": "other (fake-adapter)","#,
r#" "request": "launch","#,
r#" "program": "/project/other","#,
r#" "cwd": "/project","#,
r#" "args": [],"#,
r#" "env": {}"#,
" }",
"]",
];
// let expected_content = vec![
// "[",
// " {",
// r#" "adapter": "fake-adapter","#,
// r#" "label": "main (fake-adapter)","#,
// r#" "request": "launch","#,
// r#" "program": "/project/main","#,
// r#" "cwd": "/project","#,
// r#" "args": [],"#,
// r#" "env": {}"#,
// " },",
// " {",
// r#" "adapter": "fake-adapter","#,
// r#" "label": "other (fake-adapter)","#,
// r#" "request": "launch","#,
// r#" "program": "/project/other","#,
// r#" "cwd": "/project","#,
// r#" "args": [],"#,
// r#" "env": {}"#,
// " }",
// "]",
// ];
let actual_lines: Vec<&str> = debug_json_content.lines().collect();
pretty_assertions::assert_eq!(expected_content, actual_lines);
}
// let actual_lines: Vec<&str> = debug_json_content.lines().collect();
// pretty_assertions::assert_eq!(expected_content, actual_lines);
// }
#[gpui::test]
async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
@@ -315,7 +322,7 @@ async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppConte
);
let request_type = adapter
.validate_config(&debug_scenario.config)
.request_kind(&debug_scenario.config)
.unwrap_or_else(|_| {
panic!(
"Adapter {} should validate the config successfully",

View File

@@ -82,6 +82,7 @@ tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-typescript = { workspace = true, optional = true }
tree-sitter-python = { workspace = true, optional = true }
unicode-segmentation.workspace = true
unicode-script.workspace = true
unindent = { workspace = true, optional = true }
ui.workspace = true
url.workspace = true
@@ -97,6 +98,7 @@ gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
languages = {workspace = true, features = ["test-support"] }
lsp = { workspace = true, features = ["test-support"] }
markdown = { workspace = true, features = ["test-support"] }
multi_buffer = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
release_channel.workspace = true

View File

@@ -201,7 +201,7 @@ use ui::{
ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName,
IconSize, Indicator, Key, Tooltip, h_flex, prelude::*,
};
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc, wrap_with_prefix};
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc};
use workspace::{
CollaboratorId, Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal,
RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast,
@@ -1670,6 +1670,13 @@ impl Editor {
editor
.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
}
project::Event::LanguageServerAdded(..)
| project::Event::LanguageServerRemoved(..) => {
if editor.tasks_update_task.is_none() {
editor.tasks_update_task =
Some(editor.refresh_runnables(window, cx));
}
}
project::Event::SnippetEdit(id, snippet_edits) => {
if let Some(buffer) = editor.buffer.read(cx).buffer(*id) {
let focus_handle = editor.focus_handle(cx);
@@ -13543,6 +13550,7 @@ impl Editor {
}
let project = self.project.as_ref().map(Entity::downgrade);
let task_sources = self.lsp_task_sources(cx);
let multi_buffer = self.buffer.downgrade();
cx.spawn_in(window, async move |editor, cx| {
cx.background_executor().timer(UPDATE_DEBOUNCE).await;
let Some(project) = project.and_then(|p| p.upgrade()) else {
@@ -13626,7 +13634,19 @@ impl Editor {
return;
};
let rows = Self::runnable_rows(project, display_snapshot, new_rows, cx.clone());
let Ok(prefer_lsp) = multi_buffer.update(cx, |buffer, cx| {
buffer.language_settings(cx).tasks.prefer_lsp
}) else {
return;
};
let rows = Self::runnable_rows(
project,
display_snapshot,
prefer_lsp && !lsp_tasks_by_rows.is_empty(),
new_rows,
cx.clone(),
);
editor
.update(cx, |editor, _| {
editor.clear_tasks();
@@ -13654,15 +13674,21 @@ impl Editor {
fn runnable_rows(
project: Entity<Project>,
snapshot: DisplaySnapshot,
prefer_lsp: bool,
runnable_ranges: Vec<RunnableRange>,
mut cx: AsyncWindowContext,
) -> Vec<((BufferId, BufferRow), RunnableTasks)> {
runnable_ranges
.into_iter()
.filter_map(|mut runnable| {
let tasks = cx
let mut tasks = cx
.update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx))
.ok()?;
if prefer_lsp {
tasks.retain(|(task_kind, _)| {
!matches!(task_kind, TaskSourceKind::Language { .. })
});
}
if tasks.is_empty() {
return None;
}
@@ -19587,6 +19613,347 @@ fn update_uncommitted_diff_for_buffer(
})
}
fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize {
let tab_size = tab_size.get() as usize;
let mut width = offset;
for ch in text.chars() {
width += if ch == '\t' {
tab_size - (width % tab_size)
} else {
1
};
}
width - offset
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_string_size_with_expanded_tabs() {
let nz = |val| NonZeroU32::new(val).unwrap();
assert_eq!(char_len_with_expanded_tabs(0, "", nz(4)), 0);
assert_eq!(char_len_with_expanded_tabs(0, "hello", nz(4)), 5);
assert_eq!(char_len_with_expanded_tabs(0, "\thello", nz(4)), 9);
assert_eq!(char_len_with_expanded_tabs(0, "abc\tab", nz(4)), 6);
assert_eq!(char_len_with_expanded_tabs(0, "hello\t", nz(4)), 8);
assert_eq!(char_len_with_expanded_tabs(0, "\t\t", nz(8)), 16);
assert_eq!(char_len_with_expanded_tabs(0, "x\t", nz(8)), 8);
assert_eq!(char_len_with_expanded_tabs(7, "x\t", nz(8)), 9);
}
}
/// Tokenizes a string into runs of text that should stick together, or that is whitespace.
struct WordBreakingTokenizer<'a> {
input: &'a str,
}
impl<'a> WordBreakingTokenizer<'a> {
fn new(input: &'a str) -> Self {
Self { input }
}
}
fn is_char_ideographic(ch: char) -> bool {
use unicode_script::Script::*;
use unicode_script::UnicodeScript;
matches!(ch.script(), Han | Tangut | Yi)
}
fn is_grapheme_ideographic(text: &str) -> bool {
text.chars().any(is_char_ideographic)
}
fn is_grapheme_whitespace(text: &str) -> bool {
text.chars().any(|x| x.is_whitespace())
}
fn should_stay_with_preceding_ideograph(text: &str) -> bool {
text.chars().next().map_or(false, |ch| {
matches!(ch, '。' | '、' | '' | '' | '' | '' | '' | '…')
})
}
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
enum WordBreakToken<'a> {
Word { token: &'a str, grapheme_len: usize },
InlineWhitespace { token: &'a str, grapheme_len: usize },
Newline,
}
impl<'a> Iterator for WordBreakingTokenizer<'a> {
/// Yields a span, the count of graphemes in the token, and whether it was
/// whitespace. Note that it also breaks at word boundaries.
type Item = WordBreakToken<'a>;
fn next(&mut self) -> Option<Self::Item> {
use unicode_segmentation::UnicodeSegmentation;
if self.input.is_empty() {
return None;
}
let mut iter = self.input.graphemes(true).peekable();
let mut offset = 0;
let mut grapheme_len = 0;
if let Some(first_grapheme) = iter.next() {
let is_newline = first_grapheme == "\n";
let is_whitespace = is_grapheme_whitespace(first_grapheme);
offset += first_grapheme.len();
grapheme_len += 1;
if is_grapheme_ideographic(first_grapheme) && !is_whitespace {
if let Some(grapheme) = iter.peek().copied() {
if should_stay_with_preceding_ideograph(grapheme) {
offset += grapheme.len();
grapheme_len += 1;
}
}
} else {
let mut words = self.input[offset..].split_word_bound_indices().peekable();
let mut next_word_bound = words.peek().copied();
if next_word_bound.map_or(false, |(i, _)| i == 0) {
next_word_bound = words.next();
}
while let Some(grapheme) = iter.peek().copied() {
if next_word_bound.map_or(false, |(i, _)| i == offset) {
break;
};
if is_grapheme_whitespace(grapheme) != is_whitespace
|| (grapheme == "\n") != is_newline
{
break;
};
offset += grapheme.len();
grapheme_len += 1;
iter.next();
}
}
let token = &self.input[..offset];
self.input = &self.input[offset..];
if token == "\n" {
Some(WordBreakToken::Newline)
} else if is_whitespace {
Some(WordBreakToken::InlineWhitespace {
token,
grapheme_len,
})
} else {
Some(WordBreakToken::Word {
token,
grapheme_len,
})
}
} else {
None
}
}
}
#[test]
fn test_word_breaking_tokenizer() {
let tests: &[(&str, &[WordBreakToken<'static>])] = &[
("", &[]),
(" ", &[whitespace(" ", 2)]),
("Ʒ", &[word("Ʒ", 1)]),
("Ǽ", &[word("Ǽ", 1)]),
("", &[word("", 1)]),
("⋑⋑", &[word("⋑⋑", 2)]),
(
"原理,进而",
&[word("", 1), word("理,", 2), word("", 1), word("", 1)],
),
(
"hello world",
&[word("hello", 5), whitespace(" ", 1), word("world", 5)],
),
(
"hello, world",
&[word("hello,", 6), whitespace(" ", 1), word("world", 5)],
),
(
" hello world",
&[
whitespace(" ", 2),
word("hello", 5),
whitespace(" ", 1),
word("world", 5),
],
),
(
"这是什么 \n 钢笔",
&[
word("", 1),
word("", 1),
word("", 1),
word("", 1),
whitespace(" ", 1),
newline(),
whitespace(" ", 1),
word("", 1),
word("", 1),
],
),
("mutton", &[whitespace("", 1), word("mutton", 6)]),
];
fn word(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> {
WordBreakToken::Word {
token,
grapheme_len,
}
}
fn whitespace(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> {
WordBreakToken::InlineWhitespace {
token,
grapheme_len,
}
}
fn newline() -> WordBreakToken<'static> {
WordBreakToken::Newline
}
for (input, result) in tests {
assert_eq!(
WordBreakingTokenizer::new(input)
.collect::<Vec<_>>()
.as_slice(),
*result,
);
}
}
fn wrap_with_prefix(
line_prefix: String,
unwrapped_text: String,
wrap_column: usize,
tab_size: NonZeroU32,
preserve_existing_whitespace: bool,
) -> String {
let line_prefix_len = char_len_with_expanded_tabs(0, &line_prefix, tab_size);
let mut wrapped_text = String::new();
let mut current_line = line_prefix.clone();
let tokenizer = WordBreakingTokenizer::new(&unwrapped_text);
let mut current_line_len = line_prefix_len;
let mut in_whitespace = false;
for token in tokenizer {
let have_preceding_whitespace = in_whitespace;
match token {
WordBreakToken::Word {
token,
grapheme_len,
} => {
in_whitespace = false;
if current_line_len + grapheme_len > wrap_column
&& current_line_len != line_prefix_len
{
wrapped_text.push_str(current_line.trim_end());
wrapped_text.push('\n');
current_line.truncate(line_prefix.len());
current_line_len = line_prefix_len;
}
current_line.push_str(token);
current_line_len += grapheme_len;
}
WordBreakToken::InlineWhitespace {
mut token,
mut grapheme_len,
} => {
in_whitespace = true;
if have_preceding_whitespace && !preserve_existing_whitespace {
continue;
}
if !preserve_existing_whitespace {
token = " ";
grapheme_len = 1;
}
if current_line_len + grapheme_len > wrap_column {
wrapped_text.push_str(current_line.trim_end());
wrapped_text.push('\n');
current_line.truncate(line_prefix.len());
current_line_len = line_prefix_len;
} else if current_line_len != line_prefix_len || preserve_existing_whitespace {
current_line.push_str(token);
current_line_len += grapheme_len;
}
}
WordBreakToken::Newline => {
in_whitespace = true;
if preserve_existing_whitespace {
wrapped_text.push_str(current_line.trim_end());
wrapped_text.push('\n');
current_line.truncate(line_prefix.len());
current_line_len = line_prefix_len;
} else if have_preceding_whitespace {
continue;
} else if current_line_len + 1 > wrap_column && current_line_len != line_prefix_len
{
wrapped_text.push_str(current_line.trim_end());
wrapped_text.push('\n');
current_line.truncate(line_prefix.len());
current_line_len = line_prefix_len;
} else if current_line_len != line_prefix_len {
current_line.push(' ');
current_line_len += 1;
}
}
}
}
if !current_line.is_empty() {
wrapped_text.push_str(&current_line);
}
wrapped_text
}
#[test]
fn test_wrap_with_prefix() {
assert_eq!(
wrap_with_prefix(
"# ".to_string(),
"abcdefg".to_string(),
4,
NonZeroU32::new(4).unwrap(),
false,
),
"# abcdefg"
);
assert_eq!(
wrap_with_prefix(
"".to_string(),
"\thello world".to_string(),
8,
NonZeroU32::new(4).unwrap(),
false,
),
"hello\nworld"
);
assert_eq!(
wrap_with_prefix(
"// ".to_string(),
"xx \nyy zz aa bb cc".to_string(),
12,
NonZeroU32::new(4).unwrap(),
false,
),
"// xx yy zz\n// aa bb cc"
);
assert_eq!(
wrap_with_prefix(
String::new(),
"这是什么 \n 钢笔".to_string(),
3,
NonZeroU32::new(4).unwrap(),
false,
),
"这是什\n么 钢\n"
);
}
pub trait CollaborationHub {
fn collaborators<'a>(&self, cx: &'a App) -> &'a HashMap<PeerId, Collaborator>;
fn user_participant_indices<'a>(&self, cx: &'a App) -> &'a HashMap<u64, ParticipantIndex>;

View File

@@ -9111,11 +9111,10 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) {
lsp::Url::from_file_path(path!("/file.rs")).unwrap()
);
assert_eq!(params.options.tab_size, 8);
Ok(Some(vec![]))
Ok(Some(Vec::new()))
})
.next()
.await;
cx.executor().start_waiting();
save.await;
}
@@ -16769,9 +16768,9 @@ fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -
async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
}"
fn main() {
let a = 1;
}"
.unindent(),
cx,
)
@@ -16784,10 +16783,10 @@ async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
let b = 2;
}"
fn main() {
let a = 1;
let b = 2;
}"
.unindent(),
cx,
)
@@ -16800,14 +16799,14 @@ async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
async fn test_indent_guide_nested(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
if a == 3 {
let b = 2;
} else {
let c = 3;
}
}"
fn main() {
let a = 1;
if a == 3 {
let b = 2;
} else {
let c = 3;
}
}"
.unindent(),
cx,
)
@@ -16829,11 +16828,11 @@ async fn test_indent_guide_nested(cx: &mut TestAppContext) {
async fn test_indent_guide_tab(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
let b = 2;
let c = 3;
}"
fn main() {
let a = 1;
let b = 2;
let c = 3;
}"
.unindent(),
cx,
)
@@ -16963,6 +16962,72 @@ async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
);
}
#[gpui::test]
async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
if a {
b(
c,
d,
)
} else {
e(
f
)
}
}"
.unindent(),
cx,
)
.await;
assert_indent_guides(
0..11,
vec![
indent_guide(buffer_id, 1, 10, 0),
indent_guide(buffer_id, 2, 5, 1),
indent_guide(buffer_id, 7, 9, 1),
indent_guide(buffer_id, 3, 4, 2),
indent_guide(buffer_id, 8, 8, 2),
],
None,
&mut cx,
);
cx.update_editor(|editor, window, cx| {
editor.fold_at(MultiBufferRow(2), window, cx);
assert_eq!(
editor.display_text(cx),
"
fn main() {
if a {
b(⋯
)
} else {
e(
f
)
}
}"
.unindent()
);
});
assert_indent_guides(
0..11,
vec![
indent_guide(buffer_id, 1, 10, 0),
indent_guide(buffer_id, 2, 5, 1),
indent_guide(buffer_id, 7, 9, 1),
indent_guide(buffer_id, 8, 8, 2),
],
None,
&mut cx,
);
}
#[gpui::test]
async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(

View File

@@ -7607,7 +7607,10 @@ impl Element for EditorElement {
editor.gutter_dimensions = gutter_dimensions;
editor.set_visible_line_count(bounds.size.height / line_height, window, cx);
if matches!(editor.mode, EditorMode::Minimap { .. }) {
if matches!(
editor.mode,
EditorMode::AutoHeight { .. } | EditorMode::Minimap { .. }
) {
snapshot
} else {
let wrap_width_for = |column: u32| (column as f32 * em_advance).ceil();
@@ -9626,7 +9629,6 @@ fn compute_auto_height_layout(
let font_size = style.text.font_size.to_pixels(window.rem_size());
let line_height = style.text.line_height_in_pixels(window.rem_size());
let em_width = window.text_system().em_width(font_id, font_size).unwrap();
let em_advance = window.text_system().em_advance(font_id, font_size).unwrap();
let mut snapshot = editor.snapshot(window, cx);
let gutter_dimensions = snapshot
@@ -9643,18 +9645,10 @@ fn compute_auto_height_layout(
let overscroll = size(em_width, px(0.));
let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width;
let content_offset = point(gutter_dimensions.margin, Pixels::ZERO);
let editor_content_width = editor_width - content_offset.x;
let wrap_width_for = |column: u32| (column as f32 * em_advance).ceil();
let wrap_width = match editor.soft_wrap_mode(cx) {
SoftWrap::GitDiff => None,
SoftWrap::None => Some(wrap_width_for(MAX_LINE_LEN as u32 / 2)),
SoftWrap::EditorWidth => Some(editor_content_width),
SoftWrap::Column(column) => Some(wrap_width_for(column)),
SoftWrap::Bounded(column) => Some(editor_content_width.min(wrap_width_for(column))),
};
if editor.set_wrap_width(wrap_width, cx) {
snapshot = editor.snapshot(window, cx);
if !matches!(editor.soft_wrap_mode(cx), SoftWrap::None) {
if editor.set_wrap_width(Some(editor_width), cx) {
snapshot = editor.snapshot(window, cx);
}
}
let scroll_height = (snapshot.max_point().row().next_row().0 as f32) * line_height;

View File

@@ -1050,7 +1050,9 @@ mod tests {
for (range, event) in slice.iter() {
match event {
MarkdownEvent::SubstitutedText(parsed) => rendered_text.push_str(parsed),
MarkdownEvent::SubstitutedText(parsed) => {
rendered_text.push_str(parsed.as_str())
}
MarkdownEvent::Text | MarkdownEvent::Code => {
rendered_text.push_str(&text[range.clone()])
}

View File

@@ -1,9 +1,9 @@
use std::{ops::Range, time::Duration};
use std::{cmp::Ordering, ops::Range, time::Duration};
use collections::HashSet;
use gpui::{App, AppContext as _, Context, Task, Window};
use language::language_settings::language_settings;
use multi_buffer::{IndentGuide, MultiBufferRow};
use multi_buffer::{IndentGuide, MultiBufferRow, ToPoint};
use text::{LineIndent, Point};
use util::ResultExt;
@@ -154,12 +154,28 @@ pub fn indent_guides_in_range(
snapshot: &DisplaySnapshot,
cx: &App,
) -> Vec<IndentGuide> {
let start_anchor = snapshot
let start_offset = snapshot
.buffer_snapshot
.anchor_before(Point::new(visible_buffer_range.start.0, 0));
let end_anchor = snapshot
.point_to_offset(Point::new(visible_buffer_range.start.0, 0));
let end_offset = snapshot
.buffer_snapshot
.anchor_after(Point::new(visible_buffer_range.end.0, 0));
.point_to_offset(Point::new(visible_buffer_range.end.0, 0));
let start_anchor = snapshot.buffer_snapshot.anchor_before(start_offset);
let end_anchor = snapshot.buffer_snapshot.anchor_after(end_offset);
let mut fold_ranges = Vec::<Range<Point>>::new();
let mut folds = snapshot.folds_in_range(start_offset..end_offset).peekable();
while let Some(fold) = folds.next() {
let start = fold.range.start.to_point(&snapshot.buffer_snapshot);
let end = fold.range.end.to_point(&snapshot.buffer_snapshot);
if let Some(last_range) = fold_ranges.last_mut() {
if last_range.end >= start {
last_range.end = last_range.end.max(end);
continue;
}
}
fold_ranges.push(start..end);
}
snapshot
.buffer_snapshot
@@ -169,15 +185,19 @@ pub fn indent_guides_in_range(
return false;
}
let start = MultiBufferRow(indent_guide.start_row.0.saturating_sub(1));
// Filter out indent guides that are inside a fold
// All indent guides that are starting "offscreen" have a start value of the first visible row minus one
// Therefore checking if a line is folded at first visible row minus one causes the other indent guides that are not related to the fold to disappear as well
let is_folded = snapshot.is_line_folded(start);
let line_indent = snapshot.line_indent_for_buffer_row(start);
let contained_in_fold =
line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level();
!(is_folded && contained_in_fold)
let has_containing_fold = fold_ranges
.binary_search_by(|fold_range| {
if fold_range.start >= Point::new(indent_guide.start_row.0, 0) {
Ordering::Greater
} else if fold_range.end < Point::new(indent_guide.end_row.0, 0) {
Ordering::Less
} else {
Ordering::Equal
}
})
.is_ok();
!has_containing_fold
})
.collect()
}

View File

@@ -1,4 +1,5 @@
use std::sync::Arc;
use std::time::Duration;
use crate::Editor;
use collections::HashMap;
@@ -16,10 +17,12 @@ use project::LocationLink;
use project::Project;
use project::TaskSourceKind;
use project::lsp_store::lsp_ext_command::GetLspRunnables;
use smol::future::FutureExt as _;
use smol::stream::StreamExt;
use task::ResolvedTask;
use task::TaskContext;
use text::BufferId;
use ui::SharedString;
use util::ResultExt as _;
pub(crate) fn find_specific_language_server_in_selection<F>(
@@ -130,44 +133,70 @@ pub fn lsp_tasks(
.collect::<FuturesUnordered<_>>();
cx.spawn(async move |cx| {
let mut lsp_tasks = Vec::new();
while let Some(server_to_query) = lsp_task_sources.next().await {
if let Some((server_id, buffers)) = server_to_query {
let source_kind = TaskSourceKind::Lsp(server_id);
let id_base = source_kind.to_id_base();
let mut new_lsp_tasks = Vec::new();
for buffer in buffers {
let lsp_buffer_context = lsp_task_context(&project, &buffer, cx)
.await
.unwrap_or_default();
if let Ok(runnables_task) = project.update(cx, |project, cx| {
let buffer_id = buffer.read(cx).remote_id();
project.request_lsp(
buffer,
LanguageServerToQuery::Other(server_id),
GetLspRunnables {
buffer_id,
position: for_position,
cx.spawn(async move |cx| {
let mut lsp_tasks = HashMap::default();
while let Some(server_to_query) = lsp_task_sources.next().await {
if let Some((server_id, buffers)) = server_to_query {
let mut new_lsp_tasks = Vec::new();
for buffer in buffers {
let source_kind = match buffer.update(cx, |buffer, _| {
buffer.language().map(|language| language.name())
}) {
Ok(Some(language_name)) => TaskSourceKind::Lsp {
server: server_id,
language_name: SharedString::from(language_name),
},
cx,
)
}) {
if let Some(new_runnables) = runnables_task.await.log_err() {
new_lsp_tasks.extend(new_runnables.runnables.into_iter().filter_map(
|(location, runnable)| {
let resolved_task =
runnable.resolve_task(&id_base, &lsp_buffer_context)?;
Some((location, resolved_task))
Ok(None) => continue,
Err(_) => return Vec::new(),
};
let id_base = source_kind.to_id_base();
let lsp_buffer_context = lsp_task_context(&project, &buffer, cx)
.await
.unwrap_or_default();
if let Ok(runnables_task) = project.update(cx, |project, cx| {
let buffer_id = buffer.read(cx).remote_id();
project.request_lsp(
buffer,
LanguageServerToQuery::Other(server_id),
GetLspRunnables {
buffer_id,
position: for_position,
},
));
cx,
)
}) {
if let Some(new_runnables) = runnables_task.await.log_err() {
new_lsp_tasks.extend(
new_runnables.runnables.into_iter().filter_map(
|(location, runnable)| {
let resolved_task = runnable
.resolve_task(&id_base, &lsp_buffer_context)?;
Some((location, resolved_task))
},
),
);
}
}
lsp_tasks
.entry(source_kind)
.or_insert_with(Vec::new)
.append(&mut new_lsp_tasks);
}
}
lsp_tasks.push((source_kind, new_lsp_tasks));
}
}
lsp_tasks
lsp_tasks.into_iter().collect()
})
.race({
// `lsp::LSP_REQUEST_TIMEOUT` is larger than we want for the modal to open fast
let timer = cx.background_executor().timer(Duration::from_millis(200));
async move {
timer.await;
log::info!("Timed out waiting for LSP tasks");
Vec::new()
}
})
.await
})
}

View File

@@ -67,3 +67,4 @@ unindent.workspace = true
util.workspace = true
uuid.workspace = true
workspace-hack.workspace = true
zed_llm_client.workspace = true

View File

@@ -19,6 +19,7 @@ use collections::HashMap;
use futures::{FutureExt as _, StreamExt, channel::mpsc, select_biased};
use gpui::{App, AppContext, AsyncApp, Entity};
use language_model::{LanguageModel, Role, StopReason};
use zed_llm_client::CompletionIntent;
pub const THREAD_EVENT_TIMEOUT: Duration = Duration::from_secs(60 * 2);
@@ -307,7 +308,7 @@ impl ExampleContext {
let message_count_before = self.app.update_entity(&self.agent_thread, |thread, cx| {
thread.set_remaining_turns(iterations);
thread.send_to_model(model, None, cx);
thread.send_to_model(model, CompletionIntent::UserPrompt, None, cx);
thread.messages().len()
})?;

View File

@@ -576,6 +576,7 @@ impl ExampleInstance {
thread_id: None,
prompt_id: None,
mode: None,
intent: None,
messages: vec![LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::Text(to_prompt(assertion.description))],

View File

@@ -1527,7 +1527,6 @@ impl PickerDelegate for FileFinderDelegate {
)
.child(
h_flex()
.p_2()
.gap_2()
.child(
Button::new("open-selection", "Open").on_click(|_, window, cx| {

View File

@@ -59,6 +59,7 @@ util.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true
zed_llm_client.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true

View File

@@ -13,7 +13,6 @@ use agent_settings::AgentSettings;
use anyhow::Context as _;
use askpass::AskPassDelegate;
use db::kvp::KEY_VALUE_STORE;
use editor::{
Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar,
scroll::ScrollbarAutoHide,
@@ -42,6 +41,7 @@ use language_model::{
};
use menu::{Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
use multi_buffer::ExcerptInfo;
use notifications::status_toast::{StatusToast, ToastIcon};
use panel::{
PanelHeader, panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
panel_icon_button,
@@ -54,7 +54,6 @@ use project::{
use serde::{Deserialize, Serialize};
use settings::{Settings as _, SettingsStore};
use std::future::Future;
use std::num::NonZeroU32;
use std::path::{Path, PathBuf};
use std::{collections::HashSet, sync::Arc, time::Duration, usize};
use strum::{IntoEnumIterator, VariantNames};
@@ -63,15 +62,14 @@ use ui::{
Checkbox, ContextMenu, ElevationIndex, PopoverMenu, Scrollbar, ScrollbarState, SplitButton,
Tooltip, prelude::*,
};
use util::{ResultExt, TryFutureExt, maybe, wrap_with_prefix};
use util::{ResultExt, TryFutureExt, maybe};
use workspace::AppState;
use notifications::status_toast::{StatusToast, ToastIcon};
use workspace::{
Workspace,
dock::{DockPosition, Panel, PanelEvent},
notifications::DetachAndPromptErr,
};
use zed_llm_client::CompletionIntent;
actions!(
git_panel,
@@ -385,6 +383,7 @@ pub(crate) fn commit_message_editor(
commit_editor.set_show_gutter(false, cx);
commit_editor.set_show_wrap_guides(false, cx);
commit_editor.set_show_indent_guides(false, cx);
commit_editor.set_hard_wrap(Some(72), cx);
let placeholder = placeholder.unwrap_or("Enter commit message".into());
commit_editor.set_placeholder_text(placeholder, cx);
commit_editor
@@ -1486,22 +1485,8 @@ impl GitPanel {
fn custom_or_suggested_commit_message(&self, cx: &mut Context<Self>) -> Option<String> {
let message = self.commit_editor.read(cx).text(cx);
let width = self
.commit_editor
.read(cx)
.buffer()
.read(cx)
.language_settings(cx)
.preferred_line_length as usize;
if !message.trim().is_empty() {
let message = wrap_with_prefix(
String::new(),
message,
width,
NonZeroU32::new(8).unwrap(), // tab size doesn't matter when prefix is empty
false,
);
return Some(message);
}
@@ -1781,6 +1766,7 @@ impl GitPanel {
let request = LanguageModelRequest {
thread_id: None,
prompt_id: None,
intent: Some(CompletionIntent::GenerateGitCommitMessage),
mode: None,
messages: vec![LanguageModelRequestMessage {
role: Role::User,

View File

@@ -202,6 +202,7 @@ pub enum Part {
InlineDataPart(InlineDataPart),
FunctionCallPart(FunctionCallPart),
FunctionResponsePart(FunctionResponsePart),
ThoughtPart(ThoughtPart),
}
#[derive(Debug, Serialize, Deserialize)]
@@ -235,6 +236,13 @@ pub struct FunctionResponsePart {
pub function_response: FunctionResponse,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThoughtPart {
pub thought: bool,
pub thought_signature: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CitationSource {

View File

@@ -1,6 +1,6 @@
use gpui::{
App, Application, Bounds, Context, KeyBinding, SharedString, Timer, Window, WindowBounds,
WindowKind, WindowOptions, actions, div, prelude::*, px, rgb, size,
App, Application, Bounds, Context, KeyBinding, PromptButton, PromptLevel, SharedString, Timer,
Window, WindowBounds, WindowKind, WindowOptions, actions, div, prelude::*, px, rgb, size,
};
struct SubWindow {
@@ -169,6 +169,42 @@ impl Render for WindowDemo {
let content_size = window.bounds().size;
window.resize(size(content_size.height, content_size.width));
}))
.child(button("Prompt", |window, cx| {
let answer = window.prompt(
PromptLevel::Info,
"Are you sure?",
None,
&["Ok", "Cancel"],
cx,
);
cx.spawn(async move |_| {
if answer.await.unwrap() == 0 {
println!("You have clicked Ok");
} else {
println!("You have clicked Cancel");
}
})
.detach();
}))
.child(button("Prompt (non-English)", |window, cx| {
let answer = window.prompt(
PromptLevel::Info,
"Are you sure?",
None,
&[PromptButton::ok("确定"), PromptButton::cancel("取消")],
cx,
);
cx.spawn(async move |_| {
if answer.await.unwrap() == 0 {
println!("You have clicked Ok");
} else {
println!("You have clicked Cancel");
}
})
.detach();
}))
}
}
@@ -195,6 +231,7 @@ fn main() {
},
)
.unwrap();
cx.activate(true);
cx.on_action(|_: &Quit, cx| cx.quit());
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);

View File

@@ -37,10 +37,10 @@ use crate::{
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptHandle, PromptLevel,
Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString,
SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance,
WindowHandle, WindowId, WindowInvalidator,
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window,
WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
colors::{Colors, GlobalColors},
current_platform, hash, init_app_menus,
};
@@ -1578,14 +1578,14 @@ impl App {
PromptLevel,
&str,
Option<&str>,
&[&str],
&[PromptButton],
PromptHandle,
&mut Window,
&mut App,
) -> RenderablePromptHandle
+ 'static,
) {
self.prompt_builder = Some(PromptBuilder::Custom(Box::new(renderer)))
self.prompt_builder = Some(PromptBuilder::Custom(Box::new(renderer)));
}
/// Reset the prompt builder to the default implementation.

View File

@@ -1,7 +1,7 @@
use crate::{
AnyView, AnyWindowHandle, App, AppCell, AppContext, BackgroundExecutor, BorrowAppContext,
Entity, EventEmitter, Focusable, ForegroundExecutor, Global, PromptLevel, Render, Reservation,
Result, Subscription, Task, VisualContext, Window, WindowHandle,
Entity, EventEmitter, Focusable, ForegroundExecutor, Global, PromptButton, PromptLevel, Render,
Reservation, Result, Subscription, Task, VisualContext, Window, WindowHandle,
};
use anyhow::Context as _;
use derive_more::{Deref, DerefMut};
@@ -314,13 +314,16 @@ impl AsyncWindowContext {
/// Present a platform dialog.
/// The provided message will be presented, along with buttons for each answer.
/// When a button is clicked, the returned Receiver will receive the index of the clicked button.
pub fn prompt(
pub fn prompt<T>(
&mut self,
level: PromptLevel,
message: &str,
detail: Option<&str>,
answers: &[&str],
) -> oneshot::Receiver<usize> {
answers: &[T],
) -> oneshot::Receiver<usize>
where
T: Clone + Into<PromptButton>,
{
self.window
.update(self, |_, window, cx| {
window.prompt(level, message, detail, answers, cx)

View File

@@ -27,7 +27,7 @@
///
/// The keybindings themselves are managed independently by calling cx.bind_keys().
/// (Though mostly when developing Zed itself, you just need to add a new line to
/// assets/keymaps/default.json).
/// assets/keymaps/default-{platform}.json).
///
/// ```rust
/// cx.bind_keys([

View File

@@ -418,7 +418,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
level: PromptLevel,
msg: &str,
detail: Option<&str>,
answers: &[&str],
answers: &[PromptButton],
) -> Option<oneshot::Receiver<usize>>;
fn activate(&self);
fn is_active(&self) -> bool;
@@ -1244,6 +1244,58 @@ pub enum PromptLevel {
Critical,
}
/// Prompt Button
#[derive(Clone, Debug, PartialEq)]
pub enum PromptButton {
/// Ok button
Ok(SharedString),
/// Cancel button
Cancel(SharedString),
/// Other button
Other(SharedString),
}
impl PromptButton {
/// Create a button with label
pub fn new(label: impl Into<SharedString>) -> Self {
PromptButton::Other(label.into())
}
/// Create an Ok button
pub fn ok(label: impl Into<SharedString>) -> Self {
PromptButton::Ok(label.into())
}
/// Create a Cancel button
pub fn cancel(label: impl Into<SharedString>) -> Self {
PromptButton::Cancel(label.into())
}
#[allow(dead_code)]
pub(crate) fn is_cancel(&self) -> bool {
matches!(self, PromptButton::Cancel(_))
}
/// Returns the label of the button
pub fn label(&self) -> &SharedString {
match self {
PromptButton::Ok(label) => label,
PromptButton::Cancel(label) => label,
PromptButton::Other(label) => label,
}
}
}
impl From<&str> for PromptButton {
fn from(value: &str) -> Self {
match value.to_lowercase().as_str() {
"ok" => PromptButton::Ok("Ok".into()),
"cancel" => PromptButton::Cancel("Cancel".into()),
_ => PromptButton::Other(SharedString::from(value.to_owned())),
}
}
}
/// The style of the cursor (pointer)
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub enum CursorStyle {

View File

@@ -29,8 +29,8 @@ use crate::platform::{
use crate::scene::Scene;
use crate::{
AnyWindowHandle, Bounds, Decorations, Globals, GpuSpecs, Modifiers, Output, Pixels,
PlatformDisplay, PlatformInput, Point, PromptLevel, RequestFrameOptions, ResizeEdge,
ScaledPixels, Size, Tiling, WaylandClientStatePtr, WindowAppearance,
PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions,
ResizeEdge, ScaledPixels, Size, Tiling, WaylandClientStatePtr, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, WindowParams, px,
size,
};
@@ -862,7 +862,7 @@ impl PlatformWindow for WaylandWindow {
_level: PromptLevel,
_msg: &str,
_detail: Option<&str>,
_answers: &[&str],
_answers: &[PromptButton],
) -> Option<Receiver<usize>> {
None
}

View File

@@ -4,9 +4,9 @@ use crate::platform::blade::{BladeContext, BladeRenderer, BladeSurfaceConfig};
use crate::{
AnyWindowHandle, Bounds, Decorations, DevicePixels, ForegroundExecutor, GpuSpecs, Modifiers,
Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
Point, PromptLevel, RequestFrameOptions, ResizeEdge, ScaledPixels, Scene, Size, Tiling,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
WindowParams, X11ClientStatePtr, px, size,
Point, PromptButton, PromptLevel, RequestFrameOptions, ResizeEdge, ScaledPixels, Scene, Size,
Tiling, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowDecorations,
WindowKind, WindowParams, X11ClientStatePtr, px, size,
};
use blade_graphics as gpu;
@@ -1227,7 +1227,7 @@ impl PlatformWindow for X11Window {
_level: PromptLevel,
_msg: &str,
_detail: Option<&str>,
_answers: &[&str],
_answers: &[PromptButton],
) -> Option<futures::channel::oneshot::Receiver<usize>> {
None
}

View File

@@ -21,7 +21,7 @@ const BACKSPACE_KEY: u16 = 0x7f;
const SPACE_KEY: u16 = b' ' as u16;
const ENTER_KEY: u16 = 0x0d;
const NUMPAD_ENTER_KEY: u16 = 0x03;
const ESCAPE_KEY: u16 = 0x1b;
pub(crate) const ESCAPE_KEY: u16 = 0x1b;
const TAB_KEY: u16 = 0x09;
const SHIFT_TAB_KEY: u16 = 0x19;

View File

@@ -3,8 +3,8 @@ use crate::{
AnyWindowHandle, Bounds, DisplayLink, ExternalPaths, FileDropEvent, ForegroundExecutor,
KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
PlatformWindow, Point, PromptLevel, RequestFrameOptions, ScaledPixels, Size, Timer,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowKind, WindowParams,
PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, ScaledPixels, Size,
Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowKind, WindowParams,
platform::PlatformInputHandler, point, px, size,
};
use block::ConcreteBlock;
@@ -902,7 +902,7 @@ impl PlatformWindow for MacWindow {
level: PromptLevel,
msg: &str,
detail: Option<&str>,
answers: &[&str],
answers: &[PromptButton],
) -> Option<oneshot::Receiver<usize>> {
// macOs applies overrides to modal window buttons after they are added.
// Two most important for this logic are:
@@ -926,7 +926,7 @@ impl PlatformWindow for MacWindow {
.iter()
.enumerate()
.rev()
.find(|(_, label)| **label != "Cancel")
.find(|(_, label)| !label.is_cancel())
.filter(|&(label_index, _)| label_index > 0);
unsafe {
@@ -948,11 +948,19 @@ impl PlatformWindow for MacWindow {
.enumerate()
.filter(|&(ix, _)| Some(ix) != latest_non_cancel_label.map(|(ix, _)| ix))
{
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer.label())];
let _: () = msg_send![button, setTag: ix as NSInteger];
if answer.is_cancel() {
// Bind Escape Key to Cancel Button
if let Some(key) = std::char::from_u32(super::events::ESCAPE_KEY as u32) {
let _: () =
msg_send![button, setKeyEquivalent: ns_string(&key.to_string())];
}
}
}
if let Some((ix, answer)) = latest_non_cancel_label {
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer.label())];
let _: () = msg_send![button, setTag: ix as NSInteger];
}

View File

@@ -1,8 +1,8 @@
use crate::{
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
PlatformTextSystem, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, Size, Task,
TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
Size, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
};
use anyhow::Result;
use collections::VecDeque;
@@ -165,10 +165,10 @@ impl TestPlatform {
&self,
msg: &str,
detail: Option<&str>,
answers: &[&str],
answers: &[PromptButton],
) -> oneshot::Receiver<usize> {
let (tx, rx) = oneshot::channel();
let answers: Vec<String> = answers.iter().map(|&s| s.to_string()).collect();
let answers: Vec<String> = answers.iter().map(|s| s.label().to_string()).collect();
self.background_executor()
.set_waiting_hint(Some(format!("PROMPT: {:?} {:?}", msg, detail)));
self.prompts

View File

@@ -1,8 +1,8 @@
use crate::{
AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DispatchEventResult, GpuSpecs,
Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
Point, RequestFrameOptions, ScaledPixels, Size, TestPlatform, TileId, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowParams,
Point, PromptButton, RequestFrameOptions, ScaledPixels, Size, TestPlatform, TileId,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowParams,
};
use collections::HashMap;
use parking_lot::Mutex;
@@ -164,7 +164,7 @@ impl PlatformWindow for TestWindow {
_level: crate::PromptLevel,
msg: &str,
detail: Option<&str>,
answers: &[&str],
answers: &[PromptButton],
) -> Option<futures::channel::oneshot::Receiver<usize>> {
Some(
self.0

View File

@@ -608,7 +608,7 @@ impl PlatformWindow for WindowsWindow {
level: PromptLevel,
msg: &str,
detail: Option<&str>,
answers: &[&str],
answers: &[PromptButton],
) -> Option<Receiver<usize>> {
let (done_tx, done_rx) = oneshot::channel();
let msg = msg.to_string();
@@ -616,8 +616,8 @@ impl PlatformWindow for WindowsWindow {
Some(info) => Some(info.to_string()),
None => None,
};
let answers = answers.iter().map(|s| s.to_string()).collect::<Vec<_>>();
let handle = self.0.hwnd;
let answers = answers.to_vec();
self.0
.executor
.spawn(async move {
@@ -653,9 +653,9 @@ impl PlatformWindow for WindowsWindow {
let mut button_id_map = Vec::with_capacity(answers.len());
let mut buttons = Vec::new();
let mut btn_encoded = Vec::new();
for (index, btn_string) in answers.iter().enumerate() {
let encoded = HSTRING::from(btn_string);
let button_id = if btn_string == "Cancel" {
for (index, btn) in answers.iter().enumerate() {
let encoded = HSTRING::from(btn.label().as_ref());
let button_id = if btn.is_cancel() {
IDCANCEL.0
} else {
index as i32 - 100

View File

@@ -9,13 +9,13 @@ use crate::{
KeyDownEvent, KeyEvent, Keystroke, KeystrokeEvent, LayoutId, LineLayoutIndex, Modifiers,
ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent,
Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR,
SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style,
SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement,
TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance,
WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
point, prelude::*, px, rems, size, transparent_black,
PlatformWindow, Point, PolychromeSprite, PromptButton, PromptLevel, Quad, Render,
RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge,
SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size,
StrikethroughStyle, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle,
TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, WindowOptions,
WindowParams, WindowTextSystem, point, prelude::*, px, rems, size, transparent_black,
};
use anyhow::{Context as _, Result, anyhow};
use collections::{FxHashMap, FxHashSet};
@@ -3705,28 +3705,36 @@ impl Window {
/// Present a platform dialog.
/// The provided message will be presented, along with buttons for each answer.
/// When a button is clicked, the returned Receiver will receive the index of the clicked button.
pub fn prompt(
pub fn prompt<T>(
&mut self,
level: PromptLevel,
message: &str,
detail: Option<&str>,
answers: &[&str],
answers: &[T],
cx: &mut App,
) -> oneshot::Receiver<usize> {
) -> oneshot::Receiver<usize>
where
T: Clone + Into<PromptButton>,
{
let prompt_builder = cx.prompt_builder.take();
let Some(prompt_builder) = prompt_builder else {
unreachable!("Re-entrant window prompting is not supported by GPUI");
};
let answers = answers
.iter()
.map(|answer| answer.clone().into())
.collect::<Vec<_>>();
let receiver = match &prompt_builder {
PromptBuilder::Default => self
.platform_window
.prompt(level, message, detail, answers)
.prompt(level, message, detail, &answers)
.unwrap_or_else(|| {
self.build_custom_prompt(&prompt_builder, level, message, detail, answers, cx)
self.build_custom_prompt(&prompt_builder, level, message, detail, &answers, cx)
}),
PromptBuilder::Custom(_) => {
self.build_custom_prompt(&prompt_builder, level, message, detail, answers, cx)
self.build_custom_prompt(&prompt_builder, level, message, detail, &answers, cx)
}
};
@@ -3741,7 +3749,7 @@ impl Window {
level: PromptLevel,
message: &str,
detail: Option<&str>,
answers: &[&str],
answers: &[PromptButton],
cx: &mut App,
) -> oneshot::Receiver<usize> {
let (sender, receiver) = oneshot::channel();

View File

@@ -4,7 +4,7 @@ use futures::channel::oneshot;
use crate::{
AnyView, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ParentElement, PromptLevel, Render,
InteractiveElement, IntoElement, ParentElement, PromptButton, PromptLevel, Render,
StatefulInteractiveElement, Styled, div, opaque_grey, white,
};
@@ -74,7 +74,7 @@ pub fn fallback_prompt_renderer(
level: PromptLevel,
message: &str,
detail: Option<&str>,
actions: &[&str],
actions: &[PromptButton],
handle: PromptHandle,
window: &mut Window,
cx: &mut App,
@@ -83,7 +83,7 @@ pub fn fallback_prompt_renderer(
_level: level,
message: message.to_string(),
detail: detail.map(ToString::to_string),
actions: actions.iter().map(ToString::to_string).collect(),
actions: actions.to_vec(),
focus: cx.focus_handle(),
});
@@ -95,7 +95,7 @@ pub struct FallbackPromptRenderer {
_level: PromptLevel,
message: String,
detail: Option<String>,
actions: Vec<String>,
actions: Vec<PromptButton>,
focus: FocusHandle,
}
@@ -138,7 +138,7 @@ impl Render for FallbackPromptRenderer {
.rounded_xs()
.cursor_pointer()
.text_sm()
.child(action.clone())
.child(action.label().clone())
.id(ix)
.on_click(cx.listener(move |_, _, _, cx| {
cx.emit(PromptResponse(ix));
@@ -202,7 +202,7 @@ pub(crate) enum PromptBuilder {
PromptLevel,
&str,
Option<&str>,
&[&str],
&[PromptButton],
PromptHandle,
&mut Window,
&mut App,
@@ -216,7 +216,7 @@ impl Deref for PromptBuilder {
PromptLevel,
&str,
Option<&str>,
&[&str],
&[PromptButton],
PromptHandle,
&mut Window,
&mut App,

View File

@@ -179,6 +179,8 @@ pub enum IconName {
PhoneIncoming,
Pin,
Play,
PlayAlt,
PlayBug,
Plus,
PocketKnife,
Power,

View File

@@ -64,7 +64,7 @@ use std::{
use std::{num::NonZeroU32, sync::OnceLock};
use syntax_map::{QueryCursorHandle, SyntaxSnapshot};
use task::RunnableTag;
pub use task_context::{ContextProvider, RunnableRange};
pub use task_context::{ContextLocation, ContextProvider, RunnableRange};
pub use text_diff::{
DiffOptions, apply_diff_patch, line_diff, text_diff, text_diff_with_options, unified_diff,
};
@@ -680,7 +680,7 @@ pub struct CodeLabel {
pub filter_range: Range<usize>,
}
#[derive(Clone, Debug, Deserialize, JsonSchema)]
#[derive(Clone, Deserialize, JsonSchema)]
pub struct LanguageConfig {
/// Human-readable name of the language.
pub name: LanguageName,
@@ -791,7 +791,7 @@ pub struct LanguageMatcher {
}
/// The configuration for JSX tag auto-closing.
#[derive(Clone, Debug, Deserialize, JsonSchema)]
#[derive(Clone, Deserialize, JsonSchema)]
pub struct JsxTagAutoCloseConfig {
/// The name of the node for a opening tag
pub open_tag_node_name: String,
@@ -824,7 +824,7 @@ pub struct JsxTagAutoCloseConfig {
}
/// The configuration for documentation block for this language.
#[derive(Clone, Debug, Deserialize, JsonSchema)]
#[derive(Clone, Deserialize, JsonSchema)]
pub struct DocumentationConfig {
/// A start tag of documentation block.
pub start: Arc<str>,

View File

@@ -1045,6 +1045,15 @@ pub struct LanguageTaskConfig {
pub variables: HashMap<String, String>,
#[serde(default = "default_true")]
pub enabled: bool,
/// Use LSP tasks over Zed language extension ones.
/// If no LSP tasks are returned due to error/timeout or regular execution,
/// Zed language extension tasks will be used instead.
///
/// Other Zed tasks will still be shown:
/// * Zed task from either of the task config file
/// * Zed task from history (e.g. one-off task was spawned before)
#[serde(default = "default_true")]
pub prefer_lsp: bool,
}
impl InlayHintSettings {

View File

@@ -1,9 +1,10 @@
use std::{ops::Range, sync::Arc};
use std::{ops::Range, path::PathBuf, sync::Arc};
use crate::{LanguageToolchainStore, Location, Runnable};
use anyhow::Result;
use collections::HashMap;
use fs::Fs;
use gpui::{App, Task};
use lsp::LanguageServerName;
use task::{TaskTemplates, TaskVariables};
@@ -26,11 +27,12 @@ pub trait ContextProvider: Send + Sync {
fn build_context(
&self,
_variables: &TaskVariables,
_location: &Location,
_location: ContextLocation<'_>,
_project_env: Option<HashMap<String, String>>,
_toolchains: Arc<dyn LanguageToolchainStore>,
_cx: &mut App,
) -> Task<Result<TaskVariables>> {
let _ = _location;
Task::ready(Ok(TaskVariables::default()))
}
@@ -48,3 +50,10 @@ pub trait ContextProvider: Send + Sync {
None
}
}
/// Metadata about the place in the project we gather the context for.
pub struct ContextLocation<'a> {
pub fs: Option<Arc<dyn Fs>>,
pub worktree_root: Option<PathBuf>,
pub file_location: &'a Location,
}

View File

@@ -12,7 +12,7 @@ use gpui::{
use image::codecs::png::PngEncoder;
use serde::{Deserialize, Serialize};
use util::ResultExt;
use zed_llm_client::CompletionMode;
use zed_llm_client::{CompletionIntent, CompletionMode};
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub struct LanguageModelImage {
@@ -281,6 +281,7 @@ pub enum LanguageModelToolChoice {
pub struct LanguageModelRequest {
pub thread_id: Option<String>,
pub prompt_id: Option<String>,
pub intent: Option<CompletionIntent>,
pub mode: Option<CompletionMode>,
pub messages: Vec<LanguageModelRequestMessage>,
pub tools: Vec<LanguageModelRequestTool>,

View File

@@ -809,6 +809,7 @@ impl LanguageModel for CloudLanguageModel {
> {
let thread_id = request.thread_id.clone();
let prompt_id = request.prompt_id.clone();
let intent = request.intent;
let mode = request.mode;
let app_version = cx.update(|cx| AppVersion::global(cx)).ok();
match self.model.provider {
@@ -841,6 +842,7 @@ impl LanguageModel for CloudLanguageModel {
CompletionBody {
thread_id,
prompt_id,
intent,
mode,
provider: zed_llm_client::LanguageModelProvider::Anthropic,
model: request.model.clone(),
@@ -897,6 +899,7 @@ impl LanguageModel for CloudLanguageModel {
CompletionBody {
thread_id,
prompt_id,
intent,
mode,
provider: zed_llm_client::LanguageModelProvider::OpenAi,
model: request.model.clone(),
@@ -934,6 +937,7 @@ impl LanguageModel for CloudLanguageModel {
CompletionBody {
thread_id,
prompt_id,
intent,
mode,
provider: zed_llm_client::LanguageModelProvider::Google,
model: request.model.model_id.clone(),

View File

@@ -623,6 +623,7 @@ impl GoogleEventMapper {
)));
}
Part::FunctionResponsePart(_) => {}
Part::ThoughtPart(_) => {}
});
}
}

View File

@@ -820,6 +820,7 @@ mod tests {
tool_choice: None,
thread_id: None,
prompt_id: None,
intent: None,
mode: None,
stop: Vec::new(),
};

View File

@@ -864,6 +864,7 @@ mod tests {
let request = LanguageModelRequest {
thread_id: None,
prompt_id: None,
intent: None,
mode: None,
messages: vec![LanguageModelRequestMessage {
role: Role::User,

View File

@@ -38,6 +38,7 @@ anyhow.workspace = true
async-compression.workspace = true
async-tar.workspace = true
async-trait.workspace = true
chrono.workspace = true
collections.workspace = true
dap.workspace = true
futures.workspace = true
@@ -87,6 +88,7 @@ tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-typescript = { workspace = true, optional = true }
tree-sitter-yaml = { workspace = true, optional = true }
util.workspace = true
which.workspace = true
workspace-hack.workspace = true
[dev-dependencies]

View File

@@ -49,6 +49,14 @@ mod tests {
assert_eq!(buffer.text(), expected);
};
// Do not indent after shebang
expect_indents_to(
&mut buffer,
cx,
"#!/usr/bin/env bash\n#",
"#!/usr/bin/env bash\n#",
);
// indent function correctly
expect_indents_to(
&mut buffer,

View File

@@ -29,6 +29,6 @@ brackets = [
### bar
### fi
### ```
increase_indent_pattern = "(\\s*|;)(do|then|in|else|elif)\\b.*$"
decrease_indent_pattern = "(\\s*|;)\\b(fi|done|esac|else|elif)\\b.*$"
increase_indent_pattern = "(^|\\s+|;)(do|then|in|else|elif)\\b.*$"
decrease_indent_pattern = "(^|\\s+|;)(fi|done|esac|else|elif)\\b.*$"
# make sure to test each line mode & block mode

View File

@@ -444,12 +444,13 @@ impl ContextProvider for GoContextProvider {
fn build_context(
&self,
variables: &TaskVariables,
location: &Location,
location: ContextLocation<'_>,
_: Option<HashMap<String, String>>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut gpui::App,
) -> Task<Result<TaskVariables>> {
let local_abs_path = location
.file_location
.buffer
.read(cx)
.file()

View File

@@ -88,7 +88,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
let rust_context_provider = Arc::new(rust::RustContextProvider);
let rust_lsp_adapter = Arc::new(rust::RustLspAdapter);
let tailwind_adapter = Arc::new(tailwind::TailwindLspAdapter::new(node.clone()));
let typescript_context = Arc::new(typescript::typescript_task_context());
let typescript_context = Arc::new(typescript::TypeScriptContextProvider::new());
let typescript_lsp_adapter = Arc::new(typescript::TypeScriptLspAdapter::new(node.clone()));
let vtsls_adapter = Arc::new(vtsls::VtslsLspAdapter::new(node.clone()));
let yaml_lsp_adapter = Arc::new(yaml::YamlLspAdapter::new(node.clone()));

View File

@@ -4,11 +4,11 @@ use async_trait::async_trait;
use collections::HashMap;
use gpui::{App, Task};
use gpui::{AsyncApp, SharedString};
use language::LanguageToolchainStore;
use language::Toolchain;
use language::ToolchainList;
use language::ToolchainLister;
use language::language_settings::language_settings;
use language::{ContextLocation, LanguageToolchainStore};
use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
use lsp::LanguageServerBinary;
@@ -357,6 +357,9 @@ const PYTHON_TEST_TARGET_TASK_VARIABLE: VariableName =
const PYTHON_ACTIVE_TOOLCHAIN_PATH: VariableName =
VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN"));
const PYTHON_ACTIVE_TOOLCHAIN_PATH_RAW: VariableName =
VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN_RAW"));
const PYTHON_MODULE_NAME_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("PYTHON_MODULE_NAME"));
@@ -364,35 +367,45 @@ impl ContextProvider for PythonContextProvider {
fn build_context(
&self,
variables: &task::TaskVariables,
location: &project::Location,
location: ContextLocation<'_>,
_: Option<HashMap<String, String>>,
toolchains: Arc<dyn LanguageToolchainStore>,
cx: &mut gpui::App,
) -> Task<Result<task::TaskVariables>> {
let test_target = match selected_test_runner(location.buffer.read(cx).file(), cx) {
TestRunner::UNITTEST => self.build_unittest_target(variables),
TestRunner::PYTEST => self.build_pytest_target(variables),
};
let test_target =
match selected_test_runner(location.file_location.buffer.read(cx).file(), cx) {
TestRunner::UNITTEST => self.build_unittest_target(variables),
TestRunner::PYTEST => self.build_pytest_target(variables),
};
let module_target = self.build_module_target(variables);
let worktree_id = location.buffer.read(cx).file().map(|f| f.worktree_id(cx));
let worktree_id = location
.file_location
.buffer
.read(cx)
.file()
.map(|f| f.worktree_id(cx));
cx.spawn(async move |cx| {
let active_toolchain = if let Some(worktree_id) = worktree_id {
let raw_toolchain = if let Some(worktree_id) = worktree_id {
toolchains
.active_toolchain(worktree_id, Arc::from("".as_ref()), "Python".into(), cx)
.await
.map_or_else(|| "python3".to_owned(), |toolchain| toolchain.path.into())
.map_or_else(
|| String::from("python3"),
|toolchain| toolchain.path.to_string(),
)
} else {
String::from("python3")
};
let active_toolchain = format!("\"{raw_toolchain}\"");
let toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH, active_toolchain);
let raw_toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH_RAW, raw_toolchain);
Ok(task::TaskVariables::from_iter(
test_target
.into_iter()
.chain(module_target.into_iter())
.chain([toolchain]),
.chain([toolchain, raw_toolchain]),
))
})
}

View File

@@ -557,12 +557,13 @@ impl ContextProvider for RustContextProvider {
fn build_context(
&self,
task_variables: &TaskVariables,
location: &Location,
location: ContextLocation<'_>,
project_env: Option<HashMap<String, String>>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut gpui::App,
) -> Task<Result<TaskVariables>> {
let local_abs_path = location
.file_location
.buffer
.read(cx)
.file()

View File

@@ -2,56 +2,416 @@ use anyhow::{Context as _, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use chrono::{DateTime, Local};
use collections::HashMap;
use gpui::AsyncApp;
use gpui::{App, AppContext, AsyncApp, Task};
use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
use language::{
ContextLocation, ContextProvider, File, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
use node_runtime::NodeRuntime;
use project::ContextProviderWithTasks;
use project::{Fs, lsp_store::language_server_settings};
use serde_json::{Value, json};
use smol::{fs, io::BufReader, stream::StreamExt};
use smol::{fs, io::BufReader, lock::RwLock, stream::StreamExt};
use std::{
any::Any,
borrow::Cow,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
};
use task::{TaskTemplate, TaskTemplates, VariableName};
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
use util::archive::extract_zip;
use util::merge_json_value_into;
use util::{ResultExt, fs::remove_matching, maybe};
pub(super) fn typescript_task_context() -> ContextProviderWithTasks {
ContextProviderWithTasks::new(TaskTemplates(vec![
TaskTemplate {
label: "jest file test".to_owned(),
command: "npx jest".to_owned(),
args: vec![VariableName::File.template_value()],
..TaskTemplate::default()
},
TaskTemplate {
label: "jest test $ZED_SYMBOL".to_owned(),
command: "npx jest".to_owned(),
pub(crate) struct TypeScriptContextProvider {
last_package_json: PackageJsonContents,
}
const TYPESCRIPT_RUNNER_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_RUNNER"));
const TYPESCRIPT_JEST_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST"));
const TYPESCRIPT_MOCHA_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_MOCHA"));
const TYPESCRIPT_VITEST_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST"));
const TYPESCRIPT_JASMINE_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JASMINE"));
const TYPESCRIPT_BUILD_SCRIPT_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_BUILD_SCRIPT"));
const TYPESCRIPT_TEST_SCRIPT_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_TEST_SCRIPT"));
#[derive(Clone, Default)]
struct PackageJsonContents(Arc<RwLock<HashMap<PathBuf, PackageJson>>>);
struct PackageJson {
mtime: DateTime<Local>,
data: PackageJsonData,
}
#[derive(Clone, Copy, Default)]
struct PackageJsonData {
jest: bool,
mocha: bool,
vitest: bool,
jasmine: bool,
build_script: bool,
test_script: bool,
runner: Runner,
}
#[derive(Clone, Copy, Default)]
enum Runner {
#[default]
Npm,
Npx,
Pnpm,
}
impl PackageJsonData {
fn new(package_json: HashMap<String, Value>) -> Self {
let mut build_script = false;
let mut test_script = false;
if let Some(serde_json::Value::Object(scripts)) = package_json.get("scripts") {
build_script |= scripts.contains_key("build");
test_script |= scripts.contains_key("test");
}
let mut jest = false;
let mut mocha = false;
let mut vitest = false;
let mut jasmine = false;
if let Some(serde_json::Value::Object(dependencies)) = package_json.get("devDependencies") {
jest |= dependencies.contains_key("jest");
mocha |= dependencies.contains_key("mocha");
vitest |= dependencies.contains_key("vitest");
jasmine |= dependencies.contains_key("jasmine");
}
if let Some(serde_json::Value::Object(dev_dependencies)) = package_json.get("dependencies")
{
jest |= dev_dependencies.contains_key("jest");
mocha |= dev_dependencies.contains_key("mocha");
vitest |= dev_dependencies.contains_key("vitest");
jasmine |= dev_dependencies.contains_key("jasmine");
}
let mut runner = Runner::Npm;
if which::which("pnpm").is_ok() {
runner = Runner::Pnpm;
} else if which::which("npx").is_ok() {
runner = Runner::Npx;
}
Self {
jest,
mocha,
vitest,
jasmine,
build_script,
test_script,
runner,
}
}
fn fill_variables(&self, variables: &mut TaskVariables) {
let runner = match self.runner {
Runner::Npm => "npm",
Runner::Npx => "npx",
Runner::Pnpm => "pnpm",
};
variables.insert(TYPESCRIPT_RUNNER_VARIABLE, runner.to_owned());
if self.jest {
variables.insert(TYPESCRIPT_JEST_TASK_VARIABLE, "jest".to_owned());
}
if self.mocha {
variables.insert(TYPESCRIPT_MOCHA_TASK_VARIABLE, "mocha".to_owned());
}
if self.vitest {
variables.insert(TYPESCRIPT_VITEST_TASK_VARIABLE, "vitest".to_owned());
}
if self.jasmine {
variables.insert(TYPESCRIPT_JASMINE_TASK_VARIABLE, "jasmine".to_owned());
}
if self.build_script {
variables.insert(TYPESCRIPT_BUILD_SCRIPT_TASK_VARIABLE, "build".to_owned());
}
if self.test_script {
variables.insert(TYPESCRIPT_TEST_SCRIPT_TASK_VARIABLE, "test".to_owned());
}
}
}
impl TypeScriptContextProvider {
pub fn new() -> Self {
TypeScriptContextProvider {
last_package_json: PackageJsonContents::default(),
}
}
}
impl ContextProvider for TypeScriptContextProvider {
fn associated_tasks(&self, _: Option<Arc<dyn File>>, _: &App) -> Option<TaskTemplates> {
let mut task_templates = TaskTemplates(Vec::new());
// Jest tasks
task_templates.0.push(TaskTemplate {
label: format!(
"{} file test",
TYPESCRIPT_JEST_TASK_VARIABLE.template_value()
),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
"--testNamePattern".into(),
format!("\"{}\"", VariableName::Symbol.template_value()),
VariableName::File.template_value(),
TYPESCRIPT_JEST_TASK_VARIABLE.template_value(),
VariableName::RelativeFile.template_value(),
],
tags: vec!["ts-test".into(), "js-test".into(), "tsx-test".into()],
cwd: Some(VariableName::WorktreeRoot.template_value()),
..TaskTemplate::default()
},
TaskTemplate {
label: "execute selection $ZED_SELECTED_TEXT".to_owned(),
});
task_templates.0.push(TaskTemplate {
label: format!(
"{} test {}",
TYPESCRIPT_JEST_TASK_VARIABLE.template_value(),
VariableName::Symbol.template_value(),
),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
TYPESCRIPT_JEST_TASK_VARIABLE.template_value(),
"--testNamePattern".to_owned(),
format!("\"{}\"", VariableName::Symbol.template_value()),
VariableName::RelativeFile.template_value(),
],
tags: vec![
"ts-test".to_owned(),
"js-test".to_owned(),
"tsx-test".to_owned(),
],
cwd: Some(VariableName::WorktreeRoot.template_value()),
..TaskTemplate::default()
});
// Vitest tasks
task_templates.0.push(TaskTemplate {
label: format!(
"{} file test",
TYPESCRIPT_VITEST_TASK_VARIABLE.template_value()
),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
TYPESCRIPT_VITEST_TASK_VARIABLE.template_value(),
"run".to_owned(),
VariableName::RelativeFile.template_value(),
],
cwd: Some(VariableName::WorktreeRoot.template_value()),
..TaskTemplate::default()
});
task_templates.0.push(TaskTemplate {
label: format!(
"{} test {}",
TYPESCRIPT_VITEST_TASK_VARIABLE.template_value(),
VariableName::Symbol.template_value(),
),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
TYPESCRIPT_VITEST_TASK_VARIABLE.template_value(),
"run".to_owned(),
"--testNamePattern".to_owned(),
format!("\"{}\"", VariableName::Symbol.template_value()),
VariableName::RelativeFile.template_value(),
],
tags: vec![
"ts-test".to_owned(),
"js-test".to_owned(),
"tsx-test".to_owned(),
],
cwd: Some(VariableName::WorktreeRoot.template_value()),
..TaskTemplate::default()
});
// Mocha tasks
task_templates.0.push(TaskTemplate {
label: format!(
"{} file test",
TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value()
),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value(),
VariableName::RelativeFile.template_value(),
],
cwd: Some(VariableName::WorktreeRoot.template_value()),
..TaskTemplate::default()
});
task_templates.0.push(TaskTemplate {
label: format!(
"{} test {}",
TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value(),
VariableName::Symbol.template_value(),
),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value(),
"--grep".to_owned(),
format!("\"{}\"", VariableName::Symbol.template_value()),
VariableName::RelativeFile.template_value(),
],
tags: vec![
"ts-test".to_owned(),
"js-test".to_owned(),
"tsx-test".to_owned(),
],
cwd: Some(VariableName::WorktreeRoot.template_value()),
..TaskTemplate::default()
});
// Jasmine tasks
task_templates.0.push(TaskTemplate {
label: format!(
"{} file test",
TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value()
),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value(),
VariableName::RelativeFile.template_value(),
],
cwd: Some(VariableName::WorktreeRoot.template_value()),
..TaskTemplate::default()
});
task_templates.0.push(TaskTemplate {
label: format!(
"{} test {}",
TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value(),
VariableName::Symbol.template_value(),
),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value(),
format!("--filter={}", VariableName::Symbol.template_value()),
VariableName::RelativeFile.template_value(),
],
tags: vec![
"ts-test".to_owned(),
"js-test".to_owned(),
"tsx-test".to_owned(),
],
cwd: Some(VariableName::WorktreeRoot.template_value()),
..TaskTemplate::default()
});
for package_json_script in [
TYPESCRIPT_TEST_SCRIPT_TASK_VARIABLE,
TYPESCRIPT_BUILD_SCRIPT_TASK_VARIABLE,
] {
task_templates.0.push(TaskTemplate {
label: format!(
"package.json script {}",
package_json_script.template_value()
),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
"--prefix".to_owned(),
VariableName::WorktreeRoot.template_value(),
"run".to_owned(),
package_json_script.template_value(),
],
tags: vec!["package-script".into()],
cwd: Some(VariableName::WorktreeRoot.template_value()),
..TaskTemplate::default()
});
}
task_templates.0.push(TaskTemplate {
label: format!(
"execute selection {}",
VariableName::SelectedText.template_value()
),
command: "node".to_owned(),
args: vec![
"-e".into(),
"-e".to_owned(),
format!("\"{}\"", VariableName::SelectedText.template_value()),
],
..TaskTemplate::default()
},
]))
});
Some(task_templates)
}
fn build_context(
&self,
_variables: &task::TaskVariables,
location: ContextLocation<'_>,
_project_env: Option<HashMap<String, String>>,
_toolchains: Arc<dyn LanguageToolchainStore>,
cx: &mut App,
) -> Task<Result<task::TaskVariables>> {
let Some((fs, worktree_root)) = location.fs.zip(location.worktree_root) else {
return Task::ready(Ok(task::TaskVariables::default()));
};
let package_json_contents = self.last_package_json.clone();
cx.background_spawn(async move {
let variables = package_json_variables(fs, worktree_root, package_json_contents)
.await
.context("package.json context retrieval")
.log_err()
.unwrap_or_else(task::TaskVariables::default);
Ok(variables)
})
}
}
async fn package_json_variables(
fs: Arc<dyn Fs>,
worktree_root: PathBuf,
package_json_contents: PackageJsonContents,
) -> anyhow::Result<task::TaskVariables> {
let package_json_path = worktree_root.join("package.json");
let metadata = fs
.metadata(&package_json_path)
.await
.with_context(|| format!("getting metadata for {package_json_path:?}"))?
.with_context(|| format!("missing FS metadata for {package_json_path:?}"))?;
let mtime = DateTime::<Local>::from(metadata.mtime.timestamp_for_user());
let existing_data = {
let contents = package_json_contents.0.read().await;
contents
.get(&package_json_path)
.filter(|package_json| package_json.mtime == mtime)
.map(|package_json| package_json.data)
};
let mut variables = TaskVariables::default();
if let Some(existing_data) = existing_data {
existing_data.fill_variables(&mut variables);
} else {
let package_json_string = fs
.load(&package_json_path)
.await
.with_context(|| format!("loading package.json from {package_json_path:?}"))?;
let package_json: HashMap<String, serde_json::Value> =
serde_json::from_str(&package_json_string)
.with_context(|| format!("parsing package.json from {package_json_path:?}"))?;
let new_data = PackageJsonData::new(package_json);
new_data.fill_variables(&mut variables);
{
let mut contents = package_json_contents.0.write().await;
contents.insert(
package_json_path,
PackageJson {
mtime,
data: new_data,
},
);
}
}
Ok(variables)
}
fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {

View File

@@ -225,7 +225,7 @@ impl Markdown {
self.parse(cx);
}
#[cfg(feature = "test-support")]
#[cfg(any(test, feature = "test-support"))]
pub fn parsed_markdown(&self) -> &ParsedMarkdown {
&self.parsed_markdown
}

View File

@@ -5753,15 +5753,28 @@ impl MultiBufferSnapshot {
let mut result = Vec::new();
let mut indent_stack = SmallVec::<[IndentGuide; 8]>::new();
let mut prev_settings = None;
while let Some((first_row, mut line_indent, buffer)) = row_indents.next() {
if first_row > end_row {
break;
}
let current_depth = indent_stack.len() as u32;
let settings =
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx);
let tab_size = settings.tab_size.get() as u32;
// Avoid retrieving the language settings repeatedly for every buffer row.
if let Some((prev_buffer_id, _)) = &prev_settings {
if prev_buffer_id != &buffer.remote_id() {
prev_settings.take();
}
}
let settings = &prev_settings
.get_or_insert_with(|| {
(
buffer.remote_id(),
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx),
)
})
.1;
let tab_size = settings.tab_size.get();
// When encountering empty, continue until found useful line indent
// then add to the indent stack with the depth found

View File

@@ -408,6 +408,7 @@ pub fn task_file_name() -> &'static str {
}
/// Returns the relative path to a `debug.json` file within a project.
/// .zed/debug.json
pub fn local_debug_file_relative_path() -> &'static Path {
Path::new(".zed/debug.json")
}

View File

@@ -103,8 +103,9 @@ impl DapStore {
ADD_LOCATORS.call_once(|| {
let registry = DapRegistry::global(cx);
registry.add_locator(Arc::new(locators::cargo::CargoLocator {}));
registry.add_locator(Arc::new(locators::python::PythonLocator));
registry.add_locator(Arc::new(locators::go::GoLocator {}));
registry.add_locator(Arc::new(locators::node::NodeLocator));
registry.add_locator(Arc::new(locators::python::PythonLocator));
});
client.add_entity_request_handler(Self::handle_run_debug_locator);
client.add_entity_request_handler(Self::handle_get_debug_adapter_binary);

View File

@@ -1,3 +1,4 @@
pub(crate) mod cargo;
pub(crate) mod go;
pub(crate) mod node;
pub(crate) mod python;

View File

@@ -30,6 +30,47 @@ impl DapLocator for GoLocator {
let go_action = build_config.args.first()?;
match go_action.as_str() {
"test" => {
let binary_path = if build_config.env.contains_key("OUT_DIR") {
"${OUT_DIR}/__debug".to_string()
} else {
"__debug".to_string()
};
let build_task = TaskTemplate {
label: "go test debug".into(),
command: "go".into(),
args: vec![
"test".into(),
"-c".into(),
"-gcflags \"all=-N -l\"".into(),
"-o".into(),
binary_path,
],
env: build_config.env.clone(),
cwd: build_config.cwd.clone(),
use_new_terminal: false,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
reveal_target: RevealTarget::Dock,
hide: task::HideStrategy::Never,
shell: Shell::System,
tags: vec![],
show_summary: true,
show_command: true,
};
Some(DebugScenario {
label: resolved_label.to_string().into(),
adapter: adapter.0,
build: Some(BuildTaskDefinition::Template {
task_template: build_task,
locator_name: Some(self.name()),
}),
config: serde_json::Value::Null,
tcp_connection: None,
})
}
"run" => {
let program = build_config
.args
@@ -91,6 +132,23 @@ impl DapLocator for GoLocator {
}
match go_action.as_str() {
"test" => {
let program = if let Some(out_dir) = build_config.env.get("OUT_DIR") {
format!("{}/__debug", out_dir)
} else {
PathBuf::from(&cwd)
.join("__debug")
.to_string_lossy()
.to_string()
};
Ok(DebugRequest::Launch(task::LaunchRequest {
program,
cwd: Some(PathBuf::from(&cwd)),
args: vec!["-test.v".into(), "-test.run=${ZED_SYMBOL}".into()],
env,
}))
}
"build" => {
let package = build_config
.args
@@ -221,6 +279,92 @@ mod tests {
assert!(scenario.is_none());
}
#[test]
fn test_create_scenario_for_go_test() {
let locator = GoLocator;
let task = TaskTemplate {
label: "go test".into(),
command: "go".into(),
args: vec!["test".into(), ".".into()],
env: Default::default(),
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
use_new_terminal: false,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
reveal_target: RevealTarget::Dock,
hide: HideStrategy::Never,
shell: Shell::System,
tags: vec![],
show_summary: true,
show_command: true,
};
let scenario =
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
assert!(scenario.is_some());
let scenario = scenario.unwrap();
assert_eq!(scenario.adapter, "Delve");
assert_eq!(scenario.label, "test label");
assert!(scenario.build.is_some());
if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
assert_eq!(task_template.command, "go");
assert!(task_template.args.contains(&"test".into()));
assert!(task_template.args.contains(&"-c".into()));
assert!(
task_template
.args
.contains(&"-gcflags \"all=-N -l\"".into())
);
assert!(task_template.args.contains(&"-o".into()));
assert!(task_template.args.contains(&"__debug".into()));
} else {
panic!("Expected BuildTaskDefinition::Template");
}
assert!(
scenario.config.is_null(),
"Initial config should be null to ensure it's invalid"
);
}
#[test]
fn test_create_scenario_for_go_test_with_out_dir() {
let locator = GoLocator;
let mut env = FxHashMap::default();
env.insert("OUT_DIR".to_string(), "/tmp/build".to_string());
let task = TaskTemplate {
label: "go test".into(),
command: "go".into(),
args: vec!["test".into(), ".".into()],
env,
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
use_new_terminal: false,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
reveal_target: RevealTarget::Dock,
hide: HideStrategy::Never,
shell: Shell::System,
tags: vec![],
show_summary: true,
show_command: true,
};
let scenario =
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
assert!(scenario.is_some());
let scenario = scenario.unwrap();
if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
assert!(task_template.args.contains(&"${OUT_DIR}/__debug".into()));
} else {
panic!("Expected BuildTaskDefinition::Template");
}
}
#[test]
fn test_skip_unsupported_go_commands() {
let locator = GoLocator;

View File

@@ -0,0 +1,68 @@
use std::{borrow::Cow, path::Path};
use anyhow::{Result, bail};
use async_trait::async_trait;
use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
use gpui::SharedString;
use task::{DebugScenario, SpawnInTerminal, TaskTemplate, VariableName};
pub(crate) struct NodeLocator;
const TYPESCRIPT_RUNNER_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_RUNNER"));
#[async_trait]
impl DapLocator for NodeLocator {
fn name(&self) -> SharedString {
SharedString::new_static("Node")
}
/// Determines whether this locator can generate debug target for given task.
fn create_scenario(
&self,
build_config: &TaskTemplate,
resolved_label: &str,
adapter: DebugAdapterName,
) -> Option<DebugScenario> {
// TODO(debugger) fix issues with `await` breakpoint step
if cfg!(not(debug_assertions)) {
return None;
}
if adapter.as_ref() != "JavaScript" {
return None;
}
if build_config.command != TYPESCRIPT_RUNNER_VARIABLE.template_value() {
return None;
}
let test_library = build_config.args.first()?;
let program_path = Path::new("$ZED_WORKTREE_ROOT")
.join("node_modules")
.join(".bin")
.join(test_library);
let args = build_config.args[1..].to_vec();
let config = serde_json::json!({
"request": "launch",
"type": "pwa-node",
"program": program_path,
"args": args,
"cwd": build_config.cwd.clone(),
"runtimeArgs": ["--inspect-brk"],
"console": "integratedTerminal",
});
Some(DebugScenario {
adapter: adapter.0,
label: resolved_label.to_string().into(),
build: None,
config,
tcp_connection: None,
})
}
async fn run(&self, _: SpawnInTerminal) -> Result<DebugRequest> {
bail!("Python locator should not require DapLocator::run to be ran");
}
}

View File

@@ -5,7 +5,7 @@ use async_trait::async_trait;
use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
use gpui::SharedString;
use task::{DebugScenario, SpawnInTerminal, TaskTemplate};
use task::{DebugScenario, SpawnInTerminal, TaskTemplate, VariableName};
pub(crate) struct PythonLocator;
@@ -35,6 +35,13 @@ impl DapLocator for PythonLocator {
// We cannot debug selections.
return None;
}
let command = if build_config.command
== VariableName::Custom("PYTHON_ACTIVE_ZED_TOOLCHAIN".into()).template_value()
{
VariableName::Custom("PYTHON_ACTIVE_ZED_TOOLCHAIN_RAW".into()).template_value()
} else {
build_config.command.clone()
};
let module_specifier_position = build_config
.args
.iter()
@@ -68,7 +75,7 @@ impl DapLocator for PythonLocator {
}
let mut config = serde_json::json!({
"request": "launch",
"python": build_config.command,
"python": command,
"args": args,
"cwd": build_config.cwd.clone()
});

View File

@@ -121,16 +121,17 @@ impl From<dap::Thread> for Thread {
pub enum Mode {
Building,
Running(LocalMode),
Running(RunningMode),
}
#[derive(Clone)]
pub struct LocalMode {
pub struct RunningMode {
client: Arc<DebugAdapterClient>,
binary: DebugAdapterBinary,
tmp_breakpoint: Option<SourceBreakpoint>,
worktree: WeakEntity<Worktree>,
executor: BackgroundExecutor,
is_started: bool,
}
fn client_source(abs_path: &Path) -> dap::Source {
@@ -148,7 +149,7 @@ fn client_source(abs_path: &Path) -> dap::Source {
}
}
impl LocalMode {
impl RunningMode {
async fn new(
session_id: SessionId,
parent_session: Option<Entity<Session>>,
@@ -181,6 +182,7 @@ impl LocalMode {
tmp_breakpoint: None,
binary,
executor: cx.background_executor().clone(),
is_started: false,
})
}
@@ -373,7 +375,7 @@ impl LocalMode {
capabilities: &Capabilities,
initialized_rx: oneshot::Receiver<()>,
dap_store: WeakEntity<DapStore>,
cx: &App,
cx: &mut Context<Session>,
) -> Task<Result<()>> {
let raw = self.binary.request_args.clone();
@@ -405,7 +407,7 @@ impl LocalMode {
let this = self.clone();
let worktree = self.worktree().clone();
let configuration_sequence = cx.spawn({
async move |cx| {
async move |_, cx| {
let breakpoint_store =
dap_store.read_with(cx, |dap_store, _| dap_store.breakpoint_store().clone())?;
initialized_rx.await?;
@@ -453,9 +455,20 @@ impl LocalMode {
}
});
cx.background_spawn(async move {
futures::future::try_join(launch, configuration_sequence).await?;
Ok(())
let task = cx.background_spawn(futures::future::try_join(launch, configuration_sequence));
cx.spawn(async move |this, cx| {
task.await?;
this.update(cx, |this, cx| {
if let Some(this) = this.as_running_mut() {
this.is_started = true;
cx.notify();
}
})
.ok();
anyhow::Ok(())
})
}
@@ -704,7 +717,7 @@ impl Session {
cx.subscribe(&breakpoint_store, |this, store, event, cx| match event {
BreakpointStoreEvent::BreakpointsUpdated(path, reason) => {
if let Some(local) = (!this.ignore_breakpoints)
.then(|| this.as_local_mut())
.then(|| this.as_running_mut())
.flatten()
{
local
@@ -714,7 +727,7 @@ impl Session {
}
BreakpointStoreEvent::BreakpointsCleared(paths) => {
if let Some(local) = (!this.ignore_breakpoints)
.then(|| this.as_local_mut())
.then(|| this.as_running_mut())
.flatten()
{
local.unset_breakpoints_from_paths(paths, cx).detach();
@@ -806,7 +819,7 @@ impl Session {
let parent_session = self.parent_session.clone();
cx.spawn(async move |this, cx| {
let mode = LocalMode::new(
let mode = RunningMode::new(
id,
parent_session,
worktree.downgrade(),
@@ -906,18 +919,29 @@ impl Session {
return tx;
}
pub fn is_local(&self) -> bool {
pub fn is_started(&self) -> bool {
match &self.mode {
Mode::Building => false,
Mode::Running(running) => running.is_started,
}
}
pub fn is_building(&self) -> bool {
matches!(self.mode, Mode::Building)
}
pub fn is_running(&self) -> bool {
matches!(self.mode, Mode::Running(_))
}
pub fn as_local_mut(&mut self) -> Option<&mut LocalMode> {
pub fn as_running_mut(&mut self) -> Option<&mut RunningMode> {
match &mut self.mode {
Mode::Running(local_mode) => Some(local_mode),
Mode::Building => None,
}
}
pub fn as_local(&self) -> Option<&LocalMode> {
pub fn as_running(&self) -> Option<&RunningMode> {
match &self.mode {
Mode::Running(local_mode) => Some(local_mode),
Mode::Building => None,
@@ -1140,7 +1164,7 @@ impl Session {
body: Option<serde_json::Value>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(local_session) = self.as_local() else {
let Some(local_session) = self.as_running() else {
unreachable!("Cannot respond to remote client");
};
let client = local_session.client.clone();
@@ -1162,7 +1186,7 @@ impl Session {
fn handle_stopped_event(&mut self, event: StoppedEvent, cx: &mut Context<Self>) {
// todo(debugger): Find a clean way to get around the clone
let breakpoint_store = self.breakpoint_store.clone();
if let Some((local, path)) = self.as_local_mut().and_then(|local| {
if let Some((local, path)) = self.as_running_mut().and_then(|local| {
let breakpoint = local.tmp_breakpoint.take()?;
let path = breakpoint.path.clone();
Some((local, path))
@@ -1397,7 +1421,7 @@ impl Session {
));
return cx.spawn(async move |this, cx| {
this.update(cx, |this, cx| process_result(this, error, cx))
.log_err()
.ok()
.flatten()
});
}
@@ -1406,7 +1430,7 @@ impl Session {
cx.spawn(async move |this, cx| {
let result = request.await;
this.update(cx, |this, cx| process_result(this, result, cx))
.log_err()
.ok()
.flatten()
})
}
@@ -1528,7 +1552,7 @@ impl Session {
self.ignore_breakpoints = ignore;
if let Some(local) = self.as_local() {
if let Some(local) = self.as_running() {
local.send_source_breakpoints(ignore, &self.breakpoint_store, cx)
} else {
// todo(debugger): We need to propagate this change to downstream sessions and send a message to upstream sessions
@@ -1550,7 +1574,7 @@ impl Session {
}
fn send_exception_breakpoints(&mut self, cx: &App) {
if let Some(local) = self.as_local() {
if let Some(local) = self.as_running() {
let exception_filters = self
.exception_breakpoints
.values()
@@ -2170,4 +2194,8 @@ impl Session {
self.shutdown(cx).detach();
}
}
pub fn thread_state(&self, thread_id: ThreadId) -> Option<ThreadStatus> {
self.thread_states.thread_state(thread_id)
}
}

View File

@@ -14,7 +14,7 @@ use dap::DapRegistry;
use gpui::{App, AppContext as _, Entity, SharedString, Task};
use itertools::Itertools;
use language::{
Buffer, ContextProvider, File, Language, LanguageToolchainStore, Location,
Buffer, ContextLocation, ContextProvider, File, Language, LanguageToolchainStore, Location,
language_settings::language_settings,
};
use lsp::{LanguageServerId, LanguageServerName};
@@ -132,7 +132,10 @@ pub enum TaskSourceKind {
/// Languages-specific tasks coming from extensions.
Language { name: SharedString },
/// Language-specific tasks coming from LSP servers.
Lsp(LanguageServerId),
Lsp {
language_name: SharedString,
server: LanguageServerId,
},
}
/// A collection of task contexts, derived from the current state of the workspace.
@@ -211,7 +214,10 @@ impl TaskSourceKind {
format!("{id_base}_{id}_{}", directory_in_worktree.display())
}
Self::Language { name } => format!("language_{name}"),
Self::Lsp(server_id) => format!("lsp_{server_id}"),
Self::Lsp {
server,
language_name,
} => format!("lsp_{language_name}_{server}"),
}
}
}
@@ -693,7 +699,7 @@ fn task_lru_comparator(
fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
match kind {
TaskSourceKind::Lsp(..) => 0,
TaskSourceKind::Lsp { .. } => 0,
TaskSourceKind::Language { .. } => 1,
TaskSourceKind::UserInput => 2,
TaskSourceKind::Worktree { .. } => 3,
@@ -791,11 +797,12 @@ impl ContextProvider for BasicContextProvider {
fn build_context(
&self,
_: &TaskVariables,
location: &Location,
location: ContextLocation<'_>,
_: Option<HashMap<String, String>>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut App,
) -> Task<Result<TaskVariables>> {
let location = location.file_location;
let buffer = location.buffer.read(cx);
let buffer_snapshot = buffer.snapshot();
let symbols = buffer_snapshot.symbols_containing(location.range.start, None);

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