Compare commits

..

51 Commits

Author SHA1 Message Date
Julia Ryan
8308caf16c Fix duplicated tasks 2025-06-11 14:22:44 -07:00
Cole Miller
93632fbaf4 fix file 2025-06-11 14:07:01 -04:00
Cole Miller
efa5f4c96a add relativeFile 2025-06-11 13:57:33 -04:00
Cole Miller
0d1d8a1437 fix issues with launch.json handling
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
2025-06-11 13:30:06 -04:00
Piotr Osiewicz
6c4728f00f debugger: Mark DebugAdapterBinary::program as optional (#32534)
This allows us to support debugging with a debug adapter not managed by
Zed. Note that this is not a user facing change, as DebugAdapterBinary
is used to determine how to spawn a debugger. Thus, this should not
break any configs or anything like that.

Closes #ISSUE

Release Notes:

- N/A
2025-06-11 12:38:12 +02:00
张小白
a3cc063107 windows: Show error messages when zed failed to lanuch (#32537)
Now, if either `WindowsPlatform` or `BladeRenderer` fails to initialize,
a window will pop up to notify the user.


![image](https://github.com/user-attachments/assets/40fe7f1d-5218-4ee2-b4ec-0945fed2b743)


Release Notes:

- N/A
2025-06-11 18:37:34 +08:00
Max Mynter
7d5a5d0984 Make minimum width for line numbers in gutter configurable (#31959)
Closes #7334

# Changes
This PR makes the minimum width allocated for line numbers in the side
gutter configurable in units of character width via the
`"line_number_base_width"` attribute in `gutter` settings. Set the
previously hard coded value of `4` as default.

Together with other settings (`"folds"`, `"breakpoints"`,...) this gives
the user control over the gutter width.

If the number of lines exceedes the base width, the number of digits in
the largest line number is chosen instead. This is consistent with
previous behaviour.

Screenshot for reference:
<img width="1104" alt="Screenshot 2025-06-03 at 12 15 29"
src="https://github.com/user-attachments/assets/77c869ad-164b-4b74-8e39-8be43d740ad4"
/>


P.S.: This is my first time contributing to zed (yay!🎉). Let me know if
i'm missing something.

Release Notes:

- Make minimum line number width in gutter configurable
2025-06-11 10:00:50 +00:00
张小白
4c3ada5753 windows: Add back hide_title_bar checks (#32427)
These `if` condition checks were removed in #30828, and this PR adds
them back. This is especially important in the handling of
`WM_NCHITTEST`, where all the calculations are based on the assumption
that `hide_title_bar = true`.


Release Notes:

- N/A
2025-06-11 09:46:16 +00:00
Ben Brandt
b3a8816c0e agent: Add completion cancellation when editing messages (#32533)
When editing a message, cancel any in-progress completion before
starting a new request to prevent overlapping model responses.

Release Notes:

- agent: Fixed previous completion not cancelling when editing a
previous message
2025-06-11 09:36:21 +00:00
Smit Barmase
6d9bcdb2af editor: Fix certain unwanted pre-emptive keys been shown in buffer (#32528)
Closes #32456

https://github.com/zed-industries/zed/pull/32007 added showing
pre-emptive keys for multi-key bindings. But for certain keys like
"control", "backspace", "escape", "shift", "f1", etc., shouldn't be
shown as these keys would not end up in buffer after pending input
delay. This PR changes it to use just `key_char`, as it represents
actual text that will end up in buffer and is `None` for all mentioned
keys.


fad4c17c97/crates/gpui/src/platform/keystroke.rs (L14-L21)

cc @ConradIrwin 

Release Notes:

- Fixed issue where triggering multi-key binding like "shift",
"control", etc. would write them to the buffer for a short time.
2025-06-11 14:16:21 +05:30
Umesh Yadav
0852912fd6 language_models: Add image support to OpenRouter models (#32012)
- [x] Manual Testing(Tested this with Qwen2.5 VL 32B Instruct (free) and
Llama 4 Scout (free), Llama 4 Maverick (free). Llama models have some
issues in write profile due to one of the in built tools schema, so I
tested it with minimal profile.

Closes #ISSUE

Release Notes:

- Add image support to OpenRouter models

---------

Signed-off-by: Umesh Yadav <umesh4257@gmail.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-06-11 08:01:29 +00:00
Julia Ryan
47ac01842b ci: Fix cachix secrets (#32259) 2025-06-10 23:38:44 -07:00
Michael Sloan
5b22994d9f Log error instead of panics in InlineAssistant::scroll_to_assist (#32519)
Leaving release notes blank as it's not very actionable to know that a
rare crash might be fixed.

Release Notes:

- N/A
2025-06-11 06:22:12 +00:00
Cole Miller
6c0ea88f5b debugger: Make sure debuggees are killed when quitting Zed (#32186)
Closes #31373 

We kill the DAP process in our `on_app_quit` handler, but the debuggee
might not be killed. Try to make this more reliable by making the DAP
process its own process group leader, and killing that entire process
group when quitting Zed.

I also considered going through the normal DAP shutdown sequence here,
but that seems dicey in a quit handler. There's also the DAP
`ProcessEvent` but it seems we can't rely on that as e.g. the JS DAP
doesn't send it.

Release Notes:

- Debugger Beta: Fixed debuggee processes not getting cleaned up when
quitting Zed.
2025-06-11 05:23:38 +00:00
Smit Barmase
fc4ca346be editor: Adjust scope for prefer label for snippet workaround (#32515)
Closes #32159

This PR refines the scope to match just the function name with **the
type argument** instead of the whole call expression.

Matching to whole call expression prevented methods from expanding
inside the function argument. For example, `const foo =
bar(someMethod(2)^);` instead of `const foo = bar(someMethod^)`;

Follow-up for https://github.com/zed-industries/zed/pull/30312,
https://github.com/zed-industries/zed/pull/30351. Mistakenly regressed
since https://github.com/zed-industries/zed/pull/31872 when we stopped
receiving `insert_range` for this particular case and fallback to
`replace_range`.

Release Notes:

- Fixed issue where code completion in TypeScript function arguments
sometimes omitted the dot separator, for example resulting in
`NumberparseInt` instead of `Number.parseInt(string)`.

---------

Co-authored-by: Michael Sloan <michael@zed.dev>
Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-06-11 10:38:39 +05:30
Conrad Irwin
e9570eefbf Fix go stop on panic (#32512)
Release Notes:

- debugger: Fix stopping on a panic
2025-06-10 22:24:59 -06:00
Max Brunsfeld
72de3143c8 Add a test demonstrating ERB language loading bug (#32278)
Fixes https://github.com/zed-industries/zed/issues/12174

Release Notes:

- Fixed a bug where ERB files were not parsed correctly when the
languages were initially loaded.
2025-06-11 04:03:42 +00:00
Conrad Irwin
ad206a6a97 Recenter current stack frame on click (#32508)
Release Notes:

- debugger: Recenter current stack frame on click
2025-06-10 22:00:20 -06:00
Conrad Irwin
1e1bc7c373 Fix detach (#32506)
Release Notes:

- debugger: Fix detach to not terminate debuggee (and only be available
when detaching makes sense)
2025-06-10 20:20:28 -06:00
Stanislav Alekseev
84eca53319 Add ANSI C quoting to export env parsing (#32404)
Follow up to #31799 to support ansi-c quoting. This is used by
nix/direnv

Release Notes:

- N/A
2025-06-10 20:15:35 -06:00
fantacell
b4e558ce3d Add more keymaps from helix (#32453)
I added three additional keymaps to simulate helix behavior.

Release Notes:

- N/A
2025-06-11 02:10:43 +00:00
Conrad Irwin
00a8101016 Add a run menu (#32505)
As part of this I refactored the logic that enabled/disabled actions in
the debugger to happen at action registration time instead of using
command palette filters. This allows the menu to grey out actions correctly.

Release Notes:

- Add a "Run" menu to contain tasks and debugger
2025-06-10 19:57:46 -06:00
Anthony Eid
444f797827 debugger beta: Improve resolve debug scenario error message (#32504)
When no locator or valid config is found we expose the invalid config
error message to the user now.

Closes #32067 

Release Notes:

- debugger beta: Improve error message when starting a debugger session
with an invalid configuration
2025-06-11 01:13:27 +00:00
Anthony Eid
7a14987c02 debugger beta: Fix inline value provider panic (#32502)
Closes #32143

Release Notes:

- debugger beta: Fix panic that could occur when generating inline
values
2025-06-11 01:01:30 +00:00
Anthony Eid
5eb68f0ea4 debugger: Fix panic when handling invalid RunInTerminal request (#32500)
The new dap-types version has a default to cwd for the
RunInTerminalRequest

Closes #31695

Release Notes:

- debugger beta: Fix panic that occurred when a debug adapter sent an
invalid `RunInTerminal` request
2025-06-11 00:44:32 +00:00
Kirill Bulatov
9c513223c4 Add initial package.json scripts task autodetection (#32497)
Now, every JS/TS-related file will get their package.json script
contents added as tasks:

<img width="1020" alt="image"
src="https://github.com/user-attachments/assets/5bf80f80-fd72-4ba8-8ccf-418872895a25"
/>

To achieve that, `fn associated_tasks` from the `ContextProvider` was
made asynchronous and the related code adjusted.

Release Notes:

- Added initial `package.json` scripts task autodetection

---------

Co-authored-by: Piotr Osiewicz <piotr@zed.dev>
2025-06-10 22:16:27 +00:00
Cole Miller
0c0933d1c0 debugger: Ungate locator for JS tasks (#32495)
Closes #ISSUE

Release Notes:

- N/A

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
2025-06-10 18:16:07 -04:00
Piotr Osiewicz
a4c5a2d4d3 debugger: Add 'open docs' button in the panel and mention onboarding in the docs (#32496)
Closes #ISSUE

Release Notes:

- N/A
2025-06-10 21:56:29 +00:00
Cole Miller
311e136e30 debugger: Reuse parent's debug terminal for child sessions (#32493)
Closes #ISSUE

Release Notes:

- Debugger Beta: fixed an issue where the terminal pane of the debug
panel would be empty when debugging JavaScript.

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
2025-06-10 17:13:58 -04:00
Michael Sloan
4f5433a180 Filter language server completions even when is_incomplete: true (#32491)
In #31872 I changed the behavior of completions to not filter instead of
requerying completions when `is_incomplete: false`. Unfortunately this
also stopped filtering completions when `is_incomplete: true` - we still
want to filter the incomplete completions so that the menu updates
quickly even when completions are slow. This does mean that the
completions menu will display partial results, hopefully only briefly
while waiting for fresh completions.

Thanks to @mikayla-maki for noticing the regression. Thankfully just in
time to fix it before this makes it into a stable release. Leaving off
release notes since I will cherry-pick this to the current preview
version, 190.x, and there probably won't be a preview release before the
next stable.

Release Notes:

- N/A
2025-06-10 21:01:59 +00:00
Piotr Osiewicz
295db79c47 debugger: Fix phantom JavaScript frames (#32469)
JavaScript debugger is using a phantom stack frame to delineate await
points; that frame reuses a frame ID of 0, which collides with other
frames returned from that adapter.

934075df8c/src/adapter/stackTrace.ts (L287)

The bug has since been fixed in
https://github.com/microsoft/vscode-js-debug/issues/2234, but we'll need
to wait for a new release of node debugger for that to make a
difference. Until then..

Release Notes:

- Fixed a bug with JavaScript debugging which led to stack trace list
containing excessive amount of `await` entries.

---------

Co-authored-by: Conrad Irwin <conrad@zed.dev>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-06-10 14:48:07 -06:00
Cole Miller
71d5c57119 debugger: Specify runtimeExecutable in output of node locator (#32464)
This appears to fix some cases where we fail to launch JS tests under
the debugger.

Release Notes:

- N/A (node locator is still gated)

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-06-10 20:42:55 +00:00
Julia Ryan
dd17fd3d5a debug: Launch custom commands from start modal (#32484)
Release Notes:

- Add custom command launching from the `debug: start` modal

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
2025-06-10 16:29:11 -04:00
Cole Miller
e4f8c4fb4c debugger: Don't spin forever when adapter disconnects unexpectedly (#32489)
Closes #ISSUE

Release Notes:

- Debugger Beta: made the debug panel UI more helpful when an invalid
configuration is sent to the debug adapter.

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-06-10 16:26:43 -04:00
Andy Waite
e62e9facf0 docs: Condense Ruby test framework docs (#32472)
Since `tldr` and `quickdraw` use the same kind of task syntax as RSpec,
I don't think it's necessary to have separate examples.

cc @joeldrapper @vitallium 

Release Notes:

- N/A
2025-06-10 22:58:57 +03:00
Andy Waite
3f419b32f8 docs: Update Ruby docs about args syntax in tasks (#32471)
Due to https://github.com/zed-industries/zed/pull/32345

cc @vitallium 

Release Notes:

- N/A
2025-06-10 22:58:38 +03:00
Joseph T. Lyons
5270844b42 Revert "Preserve selection direction when running editor: open selections in multibuffer" (#32483)
Reverts zed-industries/zed#31399

I found that in some cases, Zed will panic when using `editor: open
selections in multibuffer` if the selection is reversed. It doesn't
happen in most cases that I've tested, but in some strange edge cases
(that I dont fully understand ATM), it does. I'm reverting for now, as
the previous behavior is better than a panic, but will re-implement this
fix to preserving selection directions in a new PR with comprehensive
testing

Release Notes:

- N/A
2025-06-10 15:31:38 -04:00
Ben Kunkle
f567bb52ff gpui: Simplify uniform list API by removing entity param (#32480)
This PR also introduces `Context::processor`, a sibling of
`Context::listener` that takes a strong pointer to entity and allows for
a return result.

Release Notes:

- N/A

Co-authored-by: Mikayla <mikayla@zed.dev>
2025-06-10 18:50:57 +00:00
Cole Miller
c55630889a debugger: Run jest tests serially (#32473)
Pass `--runInBand` to jest when debugging. This prevents jest from
creating a bunch of child processes that clutter the session list.

It might be a bit more natural to add this argument in the test
templates themselves, but I don't think we want to give up parallelism
when running via `task: spawn`.

Release Notes:

- N/A (JS locator is still gated)
2025-06-10 14:25:07 -04:00
Cole Miller
e0ca4270b4 debugger: Use JS adapter's suggested names for child sessions (#32474)
Also introduces an extension point for other adapters to do this if it
turns out they also send this information.

Release Notes:

- N/A (JS locator is still gated)
2025-06-10 14:24:43 -04:00
Peter Tripp
02dfaf7799 ci: Suppress evals on forks (#32479)
Be kind to those with Zed forks.

Example [action run on
fork](https://github.com/G36maid/freebsd-ports-zed/actions/runs/15525942275)
where [this
job](https://github.com/G36maid/freebsd-ports-zed/actions/runs/15549650437/job/43777665341)
will wait forever. Sorry @G36maid

Release Notes:

- N/A
2025-06-10 18:20:03 +00:00
Ben Kunkle
c9972ca532 docs: Consolidate and improve organization of Linux GPU issue documentation (#32468)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-06-10 12:18:57 -04:00
Alexander
9334e152b4 Allow identifiers in TypeScript/JavaScript test names (#32467)
Current behavior (not detected as runnable):

<img width="1105" alt="image"
src="https://github.com/user-attachments/assets/7d3b7936-43d8-4645-bbbb-e81ed5f9b35a"
/>

New behavior:



https://github.com/user-attachments/assets/524e2a56-cb30-4dc0-98ec-b34b510015e0

Release Notes:

- Improved detection of runnable TypeScript/JavaScript test cases when
they contain identifier
2025-06-10 18:00:42 +02:00
Peter Tripp
9c47c52de5 ci: Restore lychee link check. Only validate internal links (#32463)
Follow-up to: https://github.com/zed-industries/zed/pull/32460
Follow-up to: https://github.com/zed-industries/zed/pull/30844

Release Notes:

- N/A
2025-06-10 11:20:07 -04:00
Umesh Yadav
286b97c0de agent: Fix agent panel model selector layout pushing send button off screen (#32251)
| Before | After |
|--------|-------|
| <video
src="https://github.com/user-attachments/assets/db4dcc91-9a32-4621-be78-87fe9d80b801"
controls width="400"></video> | <video
src="https://github.com/user-attachments/assets/8ee31d6d-5150-4239-a4af-eeca112d56d5"
controls width="400"></video> |

While working on something else I found this weird behaviour in message
editor of agent panel. When model names are too long, the model selector
would expand and push the send button outside the visible area. This
change fixes the flex layout to ensure the send button always remains
accessible while properly truncating long model names.

Closes #ISSUE

Release Notes:

- Fix agent panel model selector layout pushing send button off screen

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
2025-06-10 14:59:42 +00:00
Danilo Leal
415d482395 agent: Only show the MCP configuration modal in the active window (#32450)
We were previously displaying this modal in all open Zed windows if
triggered. That was a bit annoying because I had to go to each window
individually to close it, which meant doing it multiple times. 😅

Release Notes:

- agent: Fixed the MCP configuration modal to show only in the active
window.
2025-06-10 14:34:21 +00:00
Danilo Leal
a9d0eee2a9 docs: Add link to MCP extensions in the overview page (#32458)
Follow up to https://github.com/zed-industries/zed/pull/32422. Missed
this one in this latest round of MCP-related docs changes.

Release Notes:

- N/A

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-06-10 11:16:06 -03:00
Smit Barmase
e4e3409952 extension_host: Fix SSH reconnect breaks language server (#32457)
Closes #29032

This PR fixes an issue where reconnecting to SSH Remote would result in
a broken language server.

This was caused by SSH clients not registering because the `ssh_clients`
map would still contain an entry from a previously killed SSH server.
For fix, now we also check if its value has been dropped.

Release Notes:

- Fixed issue where reconnecting to SSH Remote would result in broken
code completions and diagnostics.
2025-06-10 19:45:57 +05:30
Peter Tripp
46f98b6001 ci: Move lychee link check to script/check-links (#32460)
Follow-up to: https://github.com/zed-industries/zed/pull/30844

Transient link failure was blocking PR tests passing. [Example
run](https://github.com/zed-industries/zed/actions/runs/15560960788/job/43812878693?pr=32458).

Release Notes:

- N/A
2025-06-10 10:11:24 -04:00
Kirill Bulatov
c1a4a24bce Ensure pull diagnostics do not happen for non-full mode editors (#32449)
Follow-up of https://github.com/zed-industries/zed/pull/19230

Release Notes:

- N/A
2025-06-10 12:05:45 +00:00
CharlesChen0823
eb5f59577d editor: Dismiss drag selection when dropped outside editor (#32382)
This PR fixes two issues:

1. On macOS, using Alt to copy the selection instead of cutting it.
2. Dropping the drag selection outside the editor dismisses it.  


https://github.com/user-attachments/assets/341e21c3-3eca-4e58-9bcc-8ec1de18e999


Release Notes:

- N/A

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-06-10 15:41:59 +05:30
138 changed files with 2922 additions and 3298 deletions

View File

@@ -22,7 +22,7 @@ runs:
- name: Check for broken links
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
with:
args: --no-progress './docs/src/**/*'
args: --no-progress --exclude '^http' './docs/src/**/*'
fail: true
- name: Build book

View File

@@ -1,12 +1,6 @@
name: "Run tests"
description: "Runs the tests"
inputs:
use-xvfb:
description: "Whether to run tests with xvfb"
required: false
default: "false"
runs:
using: "composite"
steps:
@@ -26,9 +20,4 @@ runs:
- name: Run tests
shell: bash -euxo pipefail {0}
run: |
if [ "${{ inputs.use-xvfb }}" == "true" ]; then
xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24 -nolisten tcp" cargo nextest run --workspace --no-fail-fast
else
cargo nextest run --workspace --no-fail-fast
fi
run: cargo nextest run --workspace --no-fail-fast

View File

@@ -319,8 +319,6 @@ jobs:
- name: Run tests
uses: ./.github/actions/run_tests
with:
use-xvfb: true
- name: Build other binaries and features
run: |
@@ -803,6 +801,7 @@ jobs:
name: Build with Nix
uses: ./.github/workflows/nix.yml
if: github.repository_owner == 'zed-industries' && contains(github.event.pull_request.labels.*.name, 'run-nix')
secrets: inherit
with:
flake-output: debug
# excludes the final package to only cache dependencies

View File

@@ -30,6 +30,7 @@ jobs:
noop:
name: No-op
runs-on: ubuntu-latest
if: github.repository_owner == 'zed-industries'
steps:
- name: No-op
run: echo "Nothing to do"

View File

@@ -214,6 +214,7 @@ jobs:
bundle-nix:
name: Build and cache Nix package
needs: tests
secrets: inherit
uses: ./.github/workflows/nix.yml
update-nightly-tag:

View File

@@ -19,6 +19,7 @@ env:
jobs:
unit_evals:
if: github.repository_owner == 'zed-industries'
timeout-minutes: 60
name: Run unit evals
runs-on:

5
Cargo.lock generated
View File

@@ -4027,6 +4027,7 @@ dependencies = [
"gpui",
"http_client",
"language",
"libc",
"log",
"node_runtime",
"parking_lot",
@@ -4050,7 +4051,7 @@ dependencies = [
[[package]]
name = "dap-types"
version = "0.0.1"
source = "git+https://github.com/zed-industries/dap-types?rev=68516de327fa1be15214133a0a2e52a12982ce75#68516de327fa1be15214133a0a2e52a12982ce75"
source = "git+https://github.com/zed-industries/dap-types?rev=b40956a7f4d1939da67429d941389ee306a3a308#b40956a7f4d1939da67429d941389ee306a3a308"
dependencies = [
"schemars",
"serde",
@@ -4695,7 +4696,6 @@ dependencies = [
"client",
"clock",
"collections",
"command_palette_hooks",
"convert_case 0.8.0",
"ctor",
"dap",
@@ -9003,7 +9003,6 @@ dependencies = [
"tree-sitter-yaml",
"unindent",
"util",
"which 6.0.3",
"workspace",
"workspace-hack",
]

View File

@@ -435,7 +435,7 @@ core-foundation-sys = "0.8.6"
core-video = { version = "0.4.3", features = ["metal"] }
criterion = { version = "0.5", features = ["html_reports"] }
ctor = "0.4.0"
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "68516de327fa1be15214133a0a2e52a12982ce75" }
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b40956a7f4d1939da67429d941389ee306a3a308" }
dashmap = "6.0"
derive_more = "0.99.17"
dirs = "4.0"
@@ -698,6 +698,8 @@ codegen-units = 16
[profile.dev.package]
taffy = { opt-level = 3 }
cranelift-codegen = { opt-level = 3 }
cranelift-codegen-meta = { opt-level = 3 }
cranelift-codegen-shared = { opt-level = 3 }
resvg = { opt-level = 3 }
rustybuzz = { opt-level = 3 }
ttf-parser = { opt-level = 3 }

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-help-icon lucide-circle-help"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>

After

Width:  |  Height:  |  Size: 348 B

View File

@@ -395,6 +395,8 @@
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePreviousItem",
"insert": "vim::InsertBefore",
".": "vim::Repeat",
"alt-.": "vim::RepeatFind",
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode",
@@ -421,6 +423,7 @@
"x": "editor::SelectLine",
"shift-x": "editor::SelectLine",
"%": "editor::SelectAll",
// Window mode
"space w h": "workspace::ActivatePaneLeft",
"space w l": "workspace::ActivatePaneRight",
@@ -450,7 +453,8 @@
"ctrl-c": "editor::ToggleComments",
"d": "vim::HelixDelete",
"c": "vim::Substitute",
"shift-c": "editor::AddSelectionBelow"
"shift-c": "editor::AddSelectionBelow",
"alt-shift-c": "editor::AddSelectionAbove"
}
},
{

View File

@@ -445,7 +445,9 @@
// Whether to show breakpoints in the gutter.
"breakpoints": true,
// Whether to show fold buttons in the gutter.
"folds": true
"folds": true,
// Minimum number of characters to reserve space for in the gutter.
"min_line_number_digits": 4
},
"indent_guides": {
// Whether to show indent guides in the editor.
@@ -1478,7 +1480,8 @@
"Go": {
"code_actions_on_format": {
"source.organizeImports": true
}
},
"debuggers": ["Delve"]
},
"GraphQL": {
"prettier": {
@@ -1543,9 +1546,15 @@
"Plain Text": {
"allow_rewrap": "anywhere"
},
"Python": {
"debuggers": ["Debugpy"]
},
"Ruby": {
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."]
},
"Rust": {
"debuggers": ["CodeLLDB"]
},
"SCSS": {
"prettier": {
"allowed": true

View File

@@ -1,4 +1,4 @@
// Static tasks configuration.
// Project tasks configuration. See https://zed.dev/docs/tasks for documentation.
//
// Example:
[

View File

@@ -1605,6 +1605,7 @@ impl ActiveThread {
this.thread.update(cx, |thread, cx| {
thread.advance_prompt_id();
thread.cancel_last_completion(Some(window.window_handle()), cx);
thread.send_to_model(
model.model,
CompletionIntent::UserPrompt,
@@ -3706,7 +3707,7 @@ mod tests {
use util::path;
use workspace::CollaboratorId;
use crate::{ContextLoadResult, thread_store};
use crate::{ContextLoadResult, thread::MessageSegment, thread_store};
use super::*;
@@ -3840,6 +3841,114 @@ mod tests {
});
}
#[gpui::test]
async fn test_editing_message_cancels_previous_completion(cx: &mut TestAppContext) {
init_test_settings(cx);
let project = create_test_project(cx, json!({})).await;
let (cx, active_thread, _, thread, model) =
setup_test_environment(cx, project.clone()).await;
cx.update(|_, cx| {
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry.set_default_model(
Some(ConfiguredModel {
provider: Arc::new(FakeLanguageModelProvider),
model: model.clone(),
}),
cx,
);
});
});
// Track thread events to verify cancellation
let cancellation_events = Arc::new(std::sync::Mutex::new(Vec::new()));
let new_request_events = Arc::new(std::sync::Mutex::new(Vec::new()));
let _subscription = cx.update(|_, cx| {
let cancellation_events = cancellation_events.clone();
let new_request_events = new_request_events.clone();
cx.subscribe(
&thread,
move |_thread, event: &ThreadEvent, _cx| match event {
ThreadEvent::CompletionCanceled => {
cancellation_events.lock().unwrap().push(());
}
ThreadEvent::NewRequest => {
new_request_events.lock().unwrap().push(());
}
_ => {}
},
)
});
// Insert a user message and start streaming a response
let message = thread.update(cx, |thread, cx| {
let message_id = thread.insert_user_message(
"Hello, how are you?",
ContextLoadResult::default(),
None,
vec![],
cx,
);
thread.advance_prompt_id();
thread.send_to_model(
model.clone(),
CompletionIntent::UserPrompt,
cx.active_window(),
cx,
);
thread.message(message_id).cloned().unwrap()
});
cx.run_until_parked();
// Verify that a completion is in progress
assert!(cx.read(|cx| thread.read(cx).is_generating()));
assert_eq!(new_request_events.lock().unwrap().len(), 1);
// Edit the message while the completion is still running
active_thread.update_in(cx, |active_thread, window, cx| {
active_thread.start_editing_message(
message.id,
message.segments.as_slice(),
message.creases.as_slice(),
window,
cx,
);
let editor = active_thread
.editing_message
.as_ref()
.unwrap()
.1
.editor
.clone();
editor.update(cx, |editor, cx| {
editor.set_text("What is the weather like?", window, cx);
});
active_thread.confirm_editing_message(&Default::default(), window, cx);
});
cx.run_until_parked();
// Verify that the previous completion was cancelled
assert_eq!(cancellation_events.lock().unwrap().len(), 1);
// Verify that a new request was started after cancellation
assert_eq!(new_request_events.lock().unwrap().len(), 2);
// Verify that the edited message contains the new text
let edited_message =
thread.update(cx, |thread, _| thread.message(message.id).cloned().unwrap());
match &edited_message.segments[0] {
MessageSegment::Text(text) => {
assert_eq!(text, "What is the weather like?");
}
_ => panic!("Expected text segment"),
}
}
fn init_test_settings(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);

View File

@@ -91,12 +91,13 @@ impl AgentModelSelector {
impl Render for AgentModelSelector {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle.clone();
let model = self.selector.read(cx).delegate.active_model(cx);
let model_name = model
.map(|model| model.model.name().0)
.unwrap_or_else(|| SharedString::from("No model selected"));
let focus_handle = self.focus_handle.clone();
PickerPopoverMenu::new(
self.selector.clone(),
Button::new("active-model", model_name)

View File

@@ -71,6 +71,10 @@ fn show_configure_mcp_modal(
window: &mut Window,
cx: &mut Context<'_, Workspace>,
) {
if !window.is_window_active() {
return;
}
let context_server_store = workspace.project().read(cx).context_server_store();
let repository: Option<SharedString> = manifest.repository.as_ref().map(|s| s.clone().into());

View File

@@ -38,8 +38,7 @@ use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use text::{OffsetRangeExt, ToPoint as _};
use ui::prelude::*;
use util::RangeExt;
use util::ResultExt;
use util::{RangeExt, ResultExt, maybe};
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
use zed_actions::agent::OpenConfiguration;
@@ -1171,27 +1170,31 @@ impl InlineAssistant {
selections.select_anchor_ranges([position..position])
});
let mut scroll_target_top;
let mut scroll_target_bottom;
let mut scroll_target_range = None;
if let Some(decorations) = assist.decorations.as_ref() {
scroll_target_top = editor
.row_for_block(decorations.prompt_block_id, cx)
.unwrap()
.0 as f32;
scroll_target_bottom = editor
.row_for_block(decorations.end_block_id, cx)
.unwrap()
.0 as f32;
} else {
scroll_target_range = maybe!({
let top = editor.row_for_block(decorations.prompt_block_id, cx)?.0 as f32;
let bottom = editor.row_for_block(decorations.end_block_id, cx)?.0 as f32;
Some((top, bottom))
});
if scroll_target_range.is_none() {
log::error!("bug: failed to find blocks for scrolling to inline assist");
}
}
let scroll_target_range = scroll_target_range.unwrap_or_else(|| {
let snapshot = editor.snapshot(window, cx);
let start_row = assist
.range
.start
.to_display_point(&snapshot.display_snapshot)
.row();
scroll_target_top = start_row.0 as f32;
scroll_target_bottom = scroll_target_top + 1.;
}
let top = start_row.0 as f32;
let bottom = top + 1.0;
(top, bottom)
});
let mut scroll_target_top = scroll_target_range.0;
let mut scroll_target_bottom = scroll_target_range.1;
scroll_target_top -= editor.vertical_scroll_margin() as f32;
scroll_target_bottom += editor.vertical_scroll_margin() as f32;

View File

@@ -722,6 +722,7 @@ impl MessageEditor {
.child(
h_flex()
.flex_none()
.flex_wrap()
.justify_between()
.child(
h_flex()
@@ -731,6 +732,7 @@ impl MessageEditor {
.child(
h_flex()
.gap_1()
.flex_wrap()
.when(!incompatible_tools.is_empty(), |this| {
this.child(
IconButton::new(

View File

@@ -594,10 +594,11 @@ impl Render for ThreadHistory {
view.pr_5()
.child(
uniform_list(
cx.entity().clone(),
"thread-history",
self.list_item_count(),
Self::list_items,
cx.processor(|this, range: Range<usize>, window, cx| {
this.list_items(range, window, cx)
}),
)
.p_1()
.track_scroll(self.scroll_handle.clone())

View File

@@ -671,7 +671,7 @@ async fn test_remote_server_debugger(
});
session.update(cx_a, |session, _| {
assert_eq!(session.binary().command, "ssh");
assert_eq!(session.binary().command.as_deref(), Some("ssh"));
});
let shutdown_session = workspace.update(cx_a, |workspace, cx| {

View File

@@ -51,6 +51,9 @@ telemetry.workspace = true
util.workspace = true
workspace-hack.workspace = true
[target.'cfg(not(windows))'.dependencies]
libc.workspace = true
[dev-dependencies]
async-pipe.workspace = true
gpui = { workspace = true, features = ["test-support"] }

View File

@@ -181,7 +181,7 @@ impl DebugTaskDefinition {
/// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session.
#[derive(Debug, Clone, PartialEq)]
pub struct DebugAdapterBinary {
pub command: String,
pub command: Option<String>,
pub arguments: Vec<String>,
pub envs: HashMap<String, String>,
pub cwd: Option<PathBuf>,
@@ -369,6 +369,10 @@ pub trait DebugAdapter: 'static + Send + Sync {
}
async fn dap_schema(&self) -> serde_json::Value;
fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option<String> {
None
}
}
#[cfg(any(test, feature = "test-support"))]
@@ -433,7 +437,7 @@ impl DebugAdapter for FakeAdapter {
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
Ok(DebugAdapterBinary {
command: "command".into(),
command: Some("command".into()),
arguments: vec![],
connection: None,
envs: HashMap::default(),

View File

@@ -297,7 +297,7 @@ mod tests {
let client = DebugAdapterClient::start(
crate::client::SessionId(1),
DebugAdapterBinary {
command: "command".into(),
command: Some("command".into()),
arguments: Default::default(),
envs: Default::default(),
connection: None,
@@ -367,7 +367,7 @@ mod tests {
let client = DebugAdapterClient::start(
crate::client::SessionId(1),
DebugAdapterBinary {
command: "command".into(),
command: Some("command".into()),
arguments: Default::default(),
envs: Default::default(),
connection: None,
@@ -420,7 +420,7 @@ mod tests {
let client = DebugAdapterClient::start(
crate::client::SessionId(1),
DebugAdapterBinary {
command: "command".into(),
command: Some("command".into()),
arguments: Default::default(),
envs: Default::default(),
connection: None,

View File

@@ -1,4 +1,4 @@
use anyhow::{Context as _, Result, bail};
use anyhow::{Context as _, Result, anyhow, bail};
use dap_types::{
ErrorResponse,
messages::{Message, Response},
@@ -12,7 +12,6 @@ use smol::{
io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader},
lock::Mutex,
net::{TcpListener, TcpStream},
process::Child,
};
use std::{
collections::HashMap,
@@ -22,7 +21,7 @@ use std::{
time::Duration,
};
use task::TcpArgumentsTemplate;
use util::{ConnectionResult, ResultExt as _};
use util::ConnectionResult;
use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings};
@@ -86,10 +85,12 @@ impl Transport {
TcpTransport::start(binary, cx)
.await
.map(|(transports, tcp)| (transports, Self::Tcp(tcp)))
.context("Tried to connect to a debug adapter via TCP transport layer")
} else {
StdioTransport::start(binary, cx)
.await
.map(|(transports, stdio)| (transports, Self::Stdio(stdio)))
.context("Tried to connect to a debug adapter via stdin/stdout transport layer")
}
}
@@ -102,7 +103,7 @@ impl Transport {
}
}
async fn kill(&self) -> Result<()> {
async fn kill(&self) {
match self {
Transport::Stdio(stdio_transport) => stdio_transport.kill().await,
Transport::Tcp(tcp_transport) => tcp_transport.kill().await,
@@ -191,7 +192,7 @@ impl TransportDelegate {
match Self::handle_output(
params.output,
client_tx,
pending_requests,
pending_requests.clone(),
output_log_handler,
)
.await
@@ -199,6 +200,12 @@ impl TransportDelegate {
Ok(()) => {}
Err(e) => log::error!("Error handling debugger output: {e}"),
}
let mut pending_requests = pending_requests.lock().await;
pending_requests.drain().for_each(|(_, request)| {
request
.send(Err(anyhow!("debugger shutdown unexpectedly")))
.ok();
});
}));
if let Some(stderr) = params.stderr.take() {
@@ -531,7 +538,7 @@ impl TransportDelegate {
current_requests.clear();
pending_requests.clear();
let _ = self.transport.kill().await.log_err();
self.transport.kill().await;
drop(current_requests);
drop(pending_requests);
@@ -562,7 +569,7 @@ pub struct TcpTransport {
pub port: u16,
pub host: Ipv4Addr,
pub timeout: u64,
process: Mutex<Child>,
process: Option<Mutex<Child>>,
}
impl TcpTransport {
@@ -591,26 +598,23 @@ impl TcpTransport {
let host = connection_args.host;
let port = connection_args.port;
let mut command = util::command::new_std_command(&binary.command);
util::set_pre_exec_to_start_new_session(&mut command);
let mut command = smol::process::Command::from(command);
let mut process = if let Some(command) = &binary.command {
let mut command = util::command::new_std_command(&command);
if let Some(cwd) = &binary.cwd {
command.current_dir(cwd);
}
if let Some(cwd) = &binary.cwd {
command.current_dir(cwd);
}
command.args(&binary.arguments);
command.envs(&binary.envs);
command.args(&binary.arguments);
command.envs(&binary.envs);
command
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
let mut process = command
.spawn()
.with_context(|| "failed to start debug adapter.")?;
Some(
Child::spawn(command, Stdio::null())
.with_context(|| "failed to start debug adapter.")?,
)
} else {
None
};
let address = SocketAddrV4::new(host, port);
@@ -628,15 +632,18 @@ impl TcpTransport {
match TcpStream::connect(address).await {
Ok(stream) => return Ok((process, stream.split())),
Err(_) => {
if let Ok(Some(_)) = process.try_status() {
let output = process.output().await?;
let output = if output.stderr.is_empty() {
String::from_utf8_lossy(&output.stdout).to_string()
} else {
String::from_utf8_lossy(&output.stderr).to_string()
};
anyhow::bail!("{output}\nerror: process exited before debugger attached.");
if let Some(p) = &mut process {
if let Ok(Some(_)) = p.try_status() {
let output = process.take().unwrap().into_inner().output().await?;
let output = if output.stderr.is_empty() {
String::from_utf8_lossy(&output.stdout).to_string()
} else {
String::from_utf8_lossy(&output.stderr).to_string()
};
anyhow::bail!("{output}\nerror: process exited before debugger attached.");
}
}
cx.background_executor().timer(Duration::from_millis(100)).await;
}
}
@@ -649,13 +656,13 @@ impl TcpTransport {
host,
port
);
let stdout = process.stdout.take();
let stderr = process.stderr.take();
let stdout = process.as_mut().and_then(|p| p.stdout.take());
let stderr = process.as_mut().and_then(|p| p.stderr.take());
let this = Self {
port,
host,
process: Mutex::new(process),
process: process.map(Mutex::new),
timeout,
};
@@ -673,10 +680,19 @@ impl TcpTransport {
true
}
async fn kill(&self) -> Result<()> {
self.process.lock().await.kill()?;
async fn kill(&self) {
if let Some(process) = &self.process {
let mut process = process.lock().await;
Child::kill(&mut process);
}
}
}
Ok(())
impl Drop for TcpTransport {
fn drop(&mut self) {
if let Some(mut p) = self.process.take() {
p.get_mut().kill();
}
}
}
@@ -687,9 +703,12 @@ pub struct StdioTransport {
impl StdioTransport {
#[allow(dead_code, reason = "This is used in non test builds of Zed")]
async fn start(binary: &DebugAdapterBinary, _: AsyncApp) -> Result<(TransportPipe, Self)> {
let mut command = util::command::new_std_command(&binary.command);
util::set_pre_exec_to_start_new_session(&mut command);
let mut command = smol::process::Command::from(command);
let Some(binary_command) = &binary.command else {
bail!(
"When using the `stdio` transport, the path to a debug adapter binary must be set by Zed."
);
};
let mut command = util::command::new_std_command(&binary_command);
if let Some(cwd) = &binary.cwd {
command.current_dir(cwd);
@@ -698,16 +717,10 @@ impl StdioTransport {
command.args(&binary.arguments);
command.envs(&binary.envs);
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
let mut process = command.spawn().with_context(|| {
let mut process = Child::spawn(command, Stdio::piped()).with_context(|| {
format!(
"failed to spawn command `{} {}`.",
binary.command,
binary_command,
binary.arguments.join(" ")
)
})?;
@@ -722,7 +735,7 @@ impl StdioTransport {
if stderr.is_none() {
bail!(
"Failed to connect to stderr for debug adapter command {}",
&binary.command
&binary_command
);
}
@@ -745,9 +758,15 @@ impl StdioTransport {
false
}
async fn kill(&self) -> Result<()> {
self.process.lock().await.kill()?;
Ok(())
async fn kill(&self) {
let mut process = self.process.lock().await;
Child::kill(&mut process);
}
}
impl Drop for StdioTransport {
fn drop(&mut self) {
self.process.get_mut().kill();
}
}
@@ -921,7 +940,66 @@ impl FakeTransport {
false
}
async fn kill(&self) -> Result<()> {
Ok(())
async fn kill(&self) {}
}
struct Child {
process: smol::process::Child,
}
impl std::ops::Deref for Child {
type Target = smol::process::Child;
fn deref(&self) -> &Self::Target {
&self.process
}
}
impl std::ops::DerefMut for Child {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.process
}
}
impl Child {
fn into_inner(self) -> smol::process::Child {
self.process
}
#[cfg(not(windows))]
fn spawn(mut command: std::process::Command, stdin: Stdio) -> Result<Self> {
util::set_pre_exec_to_start_new_session(&mut command);
let process = smol::process::Command::from(command)
.stdin(stdin)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
Ok(Self { process })
}
#[cfg(windows)]
fn spawn(command: std::process::Command, stdin: Stdio) -> Result<Self> {
// TODO(windows): create a job object and add the child process handle to it,
// see https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects
let process = smol::process::Command::from(command)
.stdin(stdin)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
Ok(Self { process })
}
#[cfg(not(windows))]
fn kill(&mut self) {
let pid = self.process.id();
unsafe {
libc::killpg(pid as i32, libc::SIGKILL);
}
}
#[cfg(windows)]
fn kill(&mut self) {
// TODO(windows): terminate the job object in kill
let _ = self.process.kill();
}
}

View File

@@ -21,18 +21,21 @@ impl CodeLldbDebugAdapter {
fn request_args(
&self,
delegate: &Arc<dyn DapDelegate>,
task_definition: &DebugTaskDefinition,
) -> Result<dap::StartDebuggingRequestArguments> {
// CodeLLDB uses `name` for a terminal label.
let mut configuration = task_definition.config.clone();
configuration
let obj = configuration
.as_object_mut()
.context("CodeLLDB is not a valid json object")?
.insert(
"name".into(),
Value::String(String::from(task_definition.label.as_ref())),
);
.context("CodeLLDB is not a valid json object")?;
obj.entry("name")
.or_insert(Value::String(String::from(task_definition.label.as_ref())));
obj.entry("cwd")
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
let request = self.request_kind(&configuration)?;
@@ -359,13 +362,13 @@ impl DebugAdapter for CodeLldbDebugAdapter {
};
Ok(DebugAdapterBinary {
command: command.unwrap(),
command: Some(command.unwrap()),
cwd: Some(delegate.worktree_root_path().to_path_buf()),
arguments: vec![
"--settings".into(),
json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
],
request_args: self.request_args(&config)?,
request_args: self.request_args(delegate, &config)?,
envs: HashMap::default(),
connection: None,
})

View File

@@ -177,18 +177,23 @@ impl DebugAdapter for GdbDebugAdapter {
let gdb_path = user_setting_path.unwrap_or(gdb_path?);
let request_args = StartDebuggingRequestArguments {
request: self.request_kind(&config.config)?,
configuration: config.config.clone(),
};
let mut configuration = config.config.clone();
if let Some(configuration) = configuration.as_object_mut() {
configuration
.entry("cwd")
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
}
Ok(DebugAdapterBinary {
command: gdb_path,
command: Some(gdb_path),
arguments: vec!["-i=dap".into()],
envs: HashMap::default(),
cwd: Some(delegate.worktree_root_path().to_path_buf()),
connection: None,
request_args,
request_args: StartDebuggingRequestArguments {
request: self.request_kind(&config.config)?,
configuration,
},
})
}
}

View File

@@ -462,14 +462,21 @@ impl DebugAdapter for GoDebugAdapter {
]
};
let mut configuration = task_definition.config.clone();
if let Some(configuration) = configuration.as_object_mut() {
configuration
.entry("cwd")
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
}
Ok(DebugAdapterBinary {
command: minidelve_path.to_string_lossy().into_owned(),
command: Some(minidelve_path.to_string_lossy().into_owned()),
arguments,
cwd: Some(cwd),
envs: HashMap::default(),
connection: None,
request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(),
configuration,
request: self.request_kind(&task_definition.config)?,
},
})

View File

@@ -2,6 +2,7 @@ use adapters::latest_github_release;
use anyhow::Context as _;
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::AsyncApp;
use serde_json::Value;
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use task::DebugRequest;
use util::ResultExt;
@@ -68,13 +69,24 @@ impl JsDebugAdapter {
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
let mut configuration = task_definition.config.clone();
if let Some(configuration) = configuration.as_object_mut() {
configuration
.entry("cwd")
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
configuration.entry("type").and_modify(normalize_task_type);
}
Ok(DebugAdapterBinary {
command: delegate
.node_runtime()
.binary_path()
.await?
.to_string_lossy()
.into_owned(),
command: Some(
delegate
.node_runtime()
.binary_path()
.await?
.to_string_lossy()
.into_owned(),
),
arguments: vec![
adapter_path
.join(Self::ADAPTER_PATH)
@@ -91,7 +103,7 @@ impl JsDebugAdapter {
timeout,
}),
request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(),
configuration,
request: self.request_kind(&task_definition.config)?,
},
})
@@ -171,7 +183,7 @@ impl DebugAdapter for JsDebugAdapter {
"properties": {
"type": {
"type": "string",
"enum": ["pwa-node", "node", "chrome", "pwa-chrome", "edge", "pwa-edge"],
"enum": ["pwa-node", "node", "chrome", "pwa-chrome", "msedge", "pwa-msedge"],
"description": "The type of debug session",
"default": "pwa-node"
},
@@ -431,4 +443,25 @@ impl DebugAdapter for JsDebugAdapter {
self.get_installed_binary(delegate, &config, user_installed_path, cx)
.await
}
fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
let label = args.configuration.get("name")?.as_str()?;
Some(label.to_owned())
}
}
fn normalize_task_type(task_type: &mut Value) {
let Some(task_type_str) = task_type.as_str() else {
return;
};
let new_name = match task_type_str {
"node" | "pwa-node" => "pwa-node",
"chrome" | "pwa-chrome" => "pwa-chrome",
"edge" | "msedge" | "pwa-edge" | "pwa-msedge" => "pwa-msedge",
_ => task_type_str,
}
.to_owned();
*task_type = Value::String(new_name);
}

View File

@@ -71,13 +71,21 @@ impl PhpDebugAdapter {
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
let mut configuration = task_definition.config.clone();
if let Some(obj) = configuration.as_object_mut() {
obj.entry("cwd")
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
}
Ok(DebugAdapterBinary {
command: delegate
.node_runtime()
.binary_path()
.await?
.to_string_lossy()
.into_owned(),
command: Some(
delegate
.node_runtime()
.binary_path()
.await?
.to_string_lossy()
.into_owned(),
),
arguments: vec![
adapter_path
.join(Self::ADAPTER_PATH)
@@ -93,7 +101,7 @@ impl PhpDebugAdapter {
cwd: Some(delegate.worktree_root_path().to_path_buf()),
envs: HashMap::default(),
request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(),
configuration,
request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)?,
},
})

View File

@@ -83,6 +83,7 @@ impl PythonDebugAdapter {
fn request_args(
&self,
delegate: &Arc<dyn DapDelegate>,
task_definition: &DebugTaskDefinition,
) -> Result<StartDebuggingRequestArguments> {
let request = self.request_kind(&task_definition.config)?;
@@ -95,6 +96,11 @@ impl PythonDebugAdapter {
}
}
if let Some(obj) = configuration.as_object_mut() {
obj.entry("cwd")
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
}
Ok(StartDebuggingRequestArguments {
configuration,
request,
@@ -187,7 +193,7 @@ impl PythonDebugAdapter {
);
Ok(DebugAdapterBinary {
command: python_command,
command: Some(python_command),
arguments,
connection: Some(adapters::TcpArguments {
host,
@@ -196,7 +202,7 @@ impl PythonDebugAdapter {
}),
cwd: Some(delegate.worktree_root_path().to_path_buf()),
envs: HashMap::default(),
request_args: self.request_args(config)?,
request_args: self.request_args(delegate, config)?,
})
}
}

View File

@@ -174,8 +174,15 @@ impl DebugAdapter for RubyDebugAdapter {
arguments.extend(ruby_config.args);
let mut configuration = definition.config.clone();
if let Some(configuration) = configuration.as_object_mut() {
configuration
.entry("cwd")
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
}
Ok(DebugAdapterBinary {
command: rdbg_path.to_string_lossy().to_string(),
command: Some(rdbg_path.to_string_lossy().to_string()),
arguments,
connection: Some(dap::adapters::TcpArguments {
host,
@@ -190,7 +197,7 @@ impl DebugAdapter for RubyDebugAdapter {
envs: ruby_config.env.into_iter().collect(),
request_args: StartDebuggingRequestArguments {
request: self.request_kind(&definition.config)?,
configuration: definition.config.clone(),
configuration,
},
})
}

View File

@@ -4,19 +4,17 @@ use crate::session::running::RunningState;
use crate::{
ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, NewProcessModal,
NewProcessMode, Pause, Restart, ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop,
ToggleExpandItem, ToggleIgnoreBreakpoints, ToggleSessionPicker, ToggleThreadPicker,
persistence, spawn_task_or_modal,
NewProcessMode, Pause, Restart, StepInto, StepOut, StepOver, Stop, ToggleExpandItem,
ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
};
use anyhow::Result;
use command_palette_hooks::CommandPaletteFilter;
use dap::StartDebuggingRequestArguments;
use dap::adapters::DebugAdapterName;
use dap::debugger_settings::DebugPanelDockPosition;
use dap::{
ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
client::SessionId, debugger_settings::DebuggerSettings,
};
use dap::{DapRegistry, StartDebuggingRequestArguments};
use gpui::{
Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter,
FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity,
@@ -29,7 +27,6 @@ use project::{Fs, WorktreeId};
use project::{Project, debugger::session::ThreadStatus};
use rpc::proto::{self};
use settings::Settings;
use std::any::TypeId;
use std::sync::Arc;
use task::{DebugScenario, TaskContext};
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
@@ -140,82 +137,6 @@ impl DebugPanel {
.map(|session| session.read(cx).running_state().clone())
}
pub(crate) fn filter_action_types(&self, cx: &mut App) {
let (has_active_session, supports_restart, support_step_back, status) = self
.active_session()
.map(|item| {
let running = item.read(cx).running_state().clone();
let caps = running.read(cx).capabilities(cx);
(
!running.read(cx).session().read(cx).is_terminated(),
caps.supports_restart_request.unwrap_or_default(),
caps.supports_step_back.unwrap_or_default(),
running.read(cx).thread_status(cx),
)
})
.unwrap_or((false, false, false, None));
let filter = CommandPaletteFilter::global_mut(cx);
let debugger_action_types = [
TypeId::of::<Detach>(),
TypeId::of::<Stop>(),
TypeId::of::<ToggleIgnoreBreakpoints>(),
];
let running_action_types = [TypeId::of::<Pause>()];
let stopped_action_type = [
TypeId::of::<Continue>(),
TypeId::of::<StepOver>(),
TypeId::of::<StepInto>(),
TypeId::of::<StepOut>(),
TypeId::of::<ShowStackTrace>(),
TypeId::of::<editor::actions::DebuggerRunToCursor>(),
TypeId::of::<editor::actions::DebuggerEvaluateSelectedText>(),
];
let step_back_action_type = [TypeId::of::<StepBack>()];
let restart_action_type = [TypeId::of::<Restart>()];
if has_active_session {
filter.show_action_types(debugger_action_types.iter());
if supports_restart {
filter.show_action_types(restart_action_type.iter());
} else {
filter.hide_action_types(&restart_action_type);
}
if support_step_back {
filter.show_action_types(step_back_action_type.iter());
} else {
filter.hide_action_types(&step_back_action_type);
}
match status {
Some(ThreadStatus::Running) => {
filter.show_action_types(running_action_types.iter());
filter.hide_action_types(&stopped_action_type);
}
Some(ThreadStatus::Stopped) => {
filter.show_action_types(stopped_action_type.iter());
filter.hide_action_types(&running_action_types);
}
_ => {
filter.hide_action_types(&running_action_types);
filter.hide_action_types(&stopped_action_type);
}
}
} else {
// show only the `debug: start`
filter.hide_action_types(&debugger_action_types);
filter.hide_action_types(&step_back_action_type);
filter.hide_action_types(&restart_action_type);
filter.hide_action_types(&running_action_types);
filter.hide_action_types(&stopped_action_type);
}
}
pub fn load(
workspace: WeakEntity<Workspace>,
cx: &mut AsyncWindowContext,
@@ -233,17 +154,6 @@ impl DebugPanel {
)
});
cx.observe_new::<DebugPanel>(|debug_panel, _, cx| {
Self::filter_action_types(debug_panel, cx);
})
.detach();
cx.observe(&debug_panel, |_, debug_panel, cx| {
debug_panel.update(cx, |debug_panel, cx| {
Self::filter_action_types(debug_panel, cx);
});
})
.detach();
workspace.set_debugger_provider(DebuggerProvider(debug_panel.clone()));
debug_panel
@@ -445,10 +355,7 @@ impl DebugPanel {
};
let dap_store_handle = self.project.read(cx).dap_store().clone();
let mut label = parent_session.read(cx).label().clone();
if !label.ends_with("(child)") {
label = format!("{label} (child)").into();
}
let label = self.label_for_child_session(&parent_session, request, cx);
let adapter = parent_session.read(cx).adapter().clone();
let mut binary = parent_session.read(cx).binary().clone();
binary.request_args = request.clone();
@@ -608,6 +515,12 @@ impl DebugPanel {
}
})
};
let documentation_button = || {
IconButton::new("debug-open-documentation", IconName::CircleHelp)
.icon_size(IconSize::Small)
.on_click(move |_, _, cx| cx.open_url("https://zed.dev/docs/debugger"))
.tooltip(Tooltip::text("Open Documentation"))
};
Some(
div.border_b_1()
@@ -629,6 +542,8 @@ impl DebugPanel {
project::debugger::session::ThreadStatus::Exited,
);
let capabilities = running_state.read(cx).capabilities(cx);
let supports_detach =
running_state.read(cx).session().read(cx).is_attached();
this.map(|this| {
if thread_status == ThreadStatus::Running {
this.child(
@@ -817,33 +732,48 @@ impl DebugPanel {
}
}),
)
.child(
IconButton::new("debug-disconnect", IconName::DebugDetach)
.icon_size(IconSize::XSmall)
.on_click(window.listener_for(
&running_state,
|this, _, _, cx| {
this.detach_client(cx);
},
))
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Detach",
&Detach,
&focus_handle,
window,
cx,
)
}
}),
.when(
supports_detach,
|div| {
div.child(
IconButton::new(
"debug-disconnect",
IconName::DebugDetach,
)
.disabled(
thread_status != ThreadStatus::Stopped
&& thread_status != ThreadStatus::Running,
)
.icon_size(IconSize::XSmall)
.on_click(window.listener_for(
&running_state,
|this, _, _, cx| {
this.detach_client(cx);
},
))
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Detach",
&Detach,
&focus_handle,
window,
cx,
)
}
}),
)
},
)
},
),
)
.justify_around()
.when(is_side, |this| this.child(new_session_button())),
.when(is_side, |this| {
this.child(new_session_button())
.child(documentation_button())
}),
)
.child(
h_flex()
@@ -884,7 +814,10 @@ impl DebugPanel {
window,
cx,
))
.when(!is_side, |this| this.child(new_session_button())),
.when(!is_side, |this| {
this.child(new_session_button())
.child(documentation_button())
}),
),
),
)
@@ -1039,6 +972,25 @@ impl DebugPanel {
cx.emit(PanelEvent::ZoomIn);
}
}
fn label_for_child_session(
&self,
parent_session: &Entity<Session>,
request: &StartDebuggingRequestArguments,
cx: &mut Context<'_, Self>,
) -> SharedString {
let adapter = parent_session.read(cx).adapter();
if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) {
if let Some(label) = adapter.label_for_child_session(request) {
return label.into();
}
}
let mut label = parent_session.read(cx).label().clone();
if !label.ends_with("(child)") {
label = format!("{label} (child)").into();
}
label
}
}
async fn register_session_inner(
@@ -1066,6 +1018,11 @@ async fn register_session_inner(
.ok();
let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
let debug_session = this.update_in(cx, |this, window, cx| {
let parent_session = this
.sessions
.iter()
.find(|p| Some(p.read(cx).session_id(cx)) == session.read(cx).parent_id(cx))
.cloned();
this.sessions.retain(|session| {
!session
.read(cx)
@@ -1079,8 +1036,8 @@ async fn register_session_inner(
let debug_session = DebugSession::running(
this.project.clone(),
this.workspace.clone(),
parent_session.map(|p| p.read(cx).running_state().read(cx).debug_terminal.clone()),
session,
cx.weak_entity(),
serialized_layout,
this.position(window, cx).axis(),
window,

View File

@@ -1,14 +1,17 @@
use std::any::TypeId;
use dap::debugger_settings::DebuggerSettings;
use debugger_panel::{DebugPanel, ToggleFocus};
use editor::Editor;
use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt};
use gpui::{App, EntityInputHandler, actions};
use gpui::{App, DispatchPhase, EntityInputHandler, actions};
use new_process_modal::{NewProcessModal, NewProcessMode};
use project::debugger::{self, breakpoint_store::SourceBreakpoint};
use project::debugger::{self, breakpoint_store::SourceBreakpoint, session::ThreadStatus};
use session::DebugSession;
use settings::Settings;
use stack_trace_view::StackTraceView;
use tasks_ui::{Spawn, TaskOverrides};
use ui::{FluentBuilder, InteractiveElement};
use util::maybe;
use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
@@ -68,148 +71,6 @@ pub fn init(cx: &mut App) {
.register_action(|workspace, _: &ToggleFocus, window, cx| {
workspace.toggle_panel_focus::<DebugPanel>(window, cx);
})
.register_action(|workspace, _: &Pause, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel
.read(cx)
.active_session()
.map(|session| session.read(cx).running_state().clone())
{
active_item.update(cx, |item, cx| item.pause_thread(cx))
}
}
})
.register_action(|workspace, _: &Restart, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel
.read(cx)
.active_session()
.map(|session| session.read(cx).running_state().clone())
{
active_item.update(cx, |item, cx| item.restart_session(cx))
}
}
})
.register_action(|workspace, _: &Continue, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel
.read(cx)
.active_session()
.map(|session| session.read(cx).running_state().clone())
{
active_item.update(cx, |item, cx| item.continue_thread(cx))
}
}
})
.register_action(|workspace, _: &StepInto, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel
.read(cx)
.active_session()
.map(|session| session.read(cx).running_state().clone())
{
active_item.update(cx, |item, cx| item.step_in(cx))
}
}
})
.register_action(|workspace, _: &StepOver, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel
.read(cx)
.active_session()
.map(|session| session.read(cx).running_state().clone())
{
active_item.update(cx, |item, cx| item.step_over(cx))
}
}
})
.register_action(|workspace, _: &StepOut, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
panel
.active_session()
.map(|session| session.read(cx).running_state().clone())
}) {
active_item.update(cx, |item, cx| item.step_out(cx))
}
}
})
.register_action(|workspace, _: &StepBack, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel
.read(cx)
.active_session()
.map(|session| session.read(cx).running_state().clone())
{
active_item.update(cx, |item, cx| item.step_back(cx))
}
}
})
.register_action(|workspace, _: &Stop, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel
.read(cx)
.active_session()
.map(|session| session.read(cx).running_state().clone())
{
cx.defer(move |cx| {
active_item.update(cx, |item, cx| item.stop_thread(cx))
})
}
}
})
.register_action(|workspace, _: &ToggleIgnoreBreakpoints, _, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
if let Some(active_item) = debug_panel
.read(cx)
.active_session()
.map(|session| session.read(cx).running_state().clone())
{
active_item.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
}
}
})
.register_action(
|workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
workspace.project().update(cx, |project, cx| {
project.dap_store().update(cx, |store, cx| {
store.shutdown_sessions(cx).detach();
})
})
},
)
.register_action(
|workspace: &mut Workspace, _: &ShowStackTrace, window, cx| {
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
return;
};
if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx) {
let is_active = workspace
.active_item(cx)
.is_some_and(|item| item.item_id() == existing.item_id());
workspace.activate_item(&existing, true, !is_active, window, cx);
} else {
let Some(active_session) = debug_panel.read(cx).active_session() else {
return;
};
let project = workspace.project();
let stack_trace_view = active_session.update(cx, |session, cx| {
session.stack_trace_view(project, window, cx).clone()
});
workspace.add_item_to_active_pane(
Box::new(stack_trace_view),
None,
true,
window,
cx,
);
}
},
)
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
})
@@ -223,90 +84,255 @@ pub fn init(cx: &mut App) {
debug_panel.rerun_last_session(workspace, window, cx);
})
},
);
)
.register_action(
|workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
workspace.project().update(cx, |project, cx| {
project.dap_store().update(cx, |store, cx| {
store.shutdown_sessions(cx).detach();
})
})
},
)
.register_action_renderer(|div, workspace, _, cx| {
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
return div;
};
let Some(active_item) = debug_panel
.read(cx)
.active_session()
.map(|session| session.read(cx).running_state().clone())
else {
return div;
};
let running_state = active_item.read(cx);
if running_state.session().read(cx).is_terminated() {
return div;
}
let caps = running_state.capabilities(cx);
let supports_restart = caps.supports_restart_request.unwrap_or_default();
let supports_step_back = caps.supports_step_back.unwrap_or_default();
let supports_detach = running_state.session().read(cx).is_attached();
let status = running_state.thread_status(cx);
let active_item = active_item.downgrade();
div.when(status == Some(ThreadStatus::Running), |div| {
let active_item = active_item.clone();
div.on_action(move |_: &Pause, _, cx| {
active_item
.update(cx, |item, cx| item.pause_thread(cx))
.ok();
})
})
.when(status == Some(ThreadStatus::Stopped), |div| {
div.on_action({
let active_item = active_item.clone();
move |_: &StepInto, _, cx| {
active_item.update(cx, |item, cx| item.step_in(cx)).ok();
}
})
.on_action({
let active_item = active_item.clone();
move |_: &StepOver, _, cx| {
active_item.update(cx, |item, cx| item.step_over(cx)).ok();
}
})
.on_action({
let active_item = active_item.clone();
move |_: &StepOut, _, cx| {
active_item.update(cx, |item, cx| item.step_out(cx)).ok();
}
})
.when(supports_step_back, |div| {
let active_item = active_item.clone();
div.on_action(move |_: &StepBack, _, cx| {
active_item.update(cx, |item, cx| item.step_back(cx)).ok();
})
})
.on_action({
let active_item = active_item.clone();
move |_: &Continue, _, cx| {
active_item
.update(cx, |item, cx| item.continue_thread(cx))
.ok();
}
})
.on_action(cx.listener(
|workspace, _: &ShowStackTrace, window, cx| {
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
return;
};
if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx)
{
let is_active = workspace
.active_item(cx)
.is_some_and(|item| item.item_id() == existing.item_id());
workspace
.activate_item(&existing, true, !is_active, window, cx);
} else {
let Some(active_session) =
debug_panel.read(cx).active_session()
else {
return;
};
let project = workspace.project();
let stack_trace_view =
active_session.update(cx, |session, cx| {
session.stack_trace_view(project, window, cx).clone()
});
workspace.add_item_to_active_pane(
Box::new(stack_trace_view),
None,
true,
window,
cx,
);
}
},
))
})
.when(supports_detach, |div| {
let active_item = active_item.clone();
div.on_action(move |_: &Detach, _, cx| {
active_item
.update(cx, |item, cx| item.detach_client(cx))
.ok();
})
})
.when(supports_restart, |div| {
let active_item = active_item.clone();
div.on_action(move |_: &Restart, _, cx| {
active_item
.update(cx, |item, cx| item.restart_session(cx))
.ok();
})
})
.on_action({
let active_item = active_item.clone();
move |_: &Stop, _, cx| {
active_item.update(cx, |item, cx| item.stop_thread(cx)).ok();
}
})
.on_action({
let active_item = active_item.clone();
move |_: &ToggleIgnoreBreakpoints, _, cx| {
active_item
.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
.ok();
}
})
});
})
})
.detach();
cx.observe_new({
move |editor: &mut Editor, _, cx| {
move |editor: &mut Editor, _, _| {
editor
.register_action(cx.listener(
move |editor, _: &editor::actions::DebuggerRunToCursor, _, cx| {
maybe!({
let debug_panel =
editor.workspace()?.read(cx).panel::<DebugPanel>(cx)?;
let cursor_point: language::Point = editor.selections.newest(cx).head();
let active_session = debug_panel.read(cx).active_session()?;
.register_action_renderer(move |editor, window, cx| {
let Some(workspace) = editor.workspace() else {
return;
};
let Some(debug_panel) = workspace.read(cx).panel::<DebugPanel>(cx) else {
return;
};
let Some(active_session) = debug_panel
.clone()
.update(cx, |panel, _| panel.active_session())
else {
return;
};
let editor = cx.entity().downgrade();
window.on_action(TypeId::of::<editor::actions::RunToCursor>(), {
let editor = editor.clone();
let active_session = active_session.clone();
move |_, phase, _, cx| {
if phase != DispatchPhase::Bubble {
return;
}
maybe!({
let (buffer, position, _) = editor
.update(cx, |editor, cx| {
let cursor_point: language::Point =
editor.selections.newest(cx).head();
let (buffer, position, _) = editor
.buffer()
.read(cx)
.point_to_buffer_point(cursor_point, cx)?;
editor
.buffer()
.read(cx)
.point_to_buffer_point(cursor_point, cx)
})
.ok()??;
let path =
let path =
debugger::breakpoint_store::BreakpointStore::abs_path_from_buffer(
&buffer, cx,
)?;
let source_breakpoint = SourceBreakpoint {
row: position.row,
path,
message: None,
condition: None,
hit_condition: None,
state: debugger::breakpoint_store::BreakpointState::Enabled,
};
let source_breakpoint = SourceBreakpoint {
row: position.row,
path,
message: None,
condition: None,
hit_condition: None,
state: debugger::breakpoint_store::BreakpointState::Enabled,
};
active_session.update(cx, |session, cx| {
session.running_state().update(cx, |state, cx| {
if let Some(thread_id) = state.selected_thread_id() {
state.session().update(cx, |session, cx| {
session.run_to_position(
source_breakpoint,
thread_id,
cx,
);
})
}
});
});
Some(())
});
},
))
.detach();
editor
.register_action(cx.listener(
move |editor, _: &editor::actions::DebuggerEvaluateSelectedText, window, cx| {
maybe!({
let debug_panel =
editor.workspace()?.read(cx).panel::<DebugPanel>(cx)?;
let active_session = debug_panel.read(cx).active_session()?;
let text = editor.text_for_range(
editor.selections.newest(cx).range(),
&mut None,
window,
cx,
)?;
active_session.update(cx, |session, cx| {
session.running_state().update(cx, |state, cx| {
let stack_id = state.selected_stack_frame_id(cx);
state.session().update(cx, |session, cx| {
session.evaluate(text, None, stack_id, None, cx).detach();
active_session.update(cx, |session, cx| {
session.running_state().update(cx, |state, cx| {
if let Some(thread_id) = state.selected_thread_id() {
state.session().update(cx, |session, cx| {
session.run_to_position(
source_breakpoint,
thread_id,
cx,
);
})
}
});
});
});
Some(())
});
},
))
Some(())
});
}
});
window.on_action(
TypeId::of::<editor::actions::EvaluateSelectedText>(),
move |_, _, window, cx| {
maybe!({
let text = editor
.update(cx, |editor, cx| {
editor.text_for_range(
editor.selections.newest(cx).range(),
&mut None,
window,
cx,
)
})
.ok()??;
active_session.update(cx, |session, cx| {
session.running_state().update(cx, |state, cx| {
let stack_id = state.selected_stack_frame_id(cx);
state.session().update(cx, |session, cx| {
session
.evaluate(text, None, stack_id, None, cx)
.detach();
});
});
});
Some(())
});
},
);
})
.detach();
}
})

View File

@@ -1,4 +1,4 @@
use collections::FxHashMap;
use collections::{FxHashMap, HashMap};
use language::LanguageRegistry;
use paths::local_debug_file_relative_path;
use std::{
@@ -15,9 +15,9 @@ use dap::{
use editor::{Editor, EditorElement, EditorStyle};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle,
InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, Subscription,
TextStyle, UnderlineStyle, WeakEntity,
Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
HighlightStyle, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText,
Subscription, TextStyle, UnderlineStyle, WeakEntity,
};
use itertools::Itertools as _;
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
@@ -28,10 +28,10 @@ use theme::ThemeSettings;
use ui::{
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize,
IconWithIndicator, Indicator, InteractiveElement, IntoElement, Label, LabelCommon as _,
ListItem, ListItemSpacing, ParentElement, RenderOnce, SharedString, Styled, StyledExt,
StyledTypography, ToggleButton, ToggleState, Toggleable, Tooltip, Window, div, h_flex, px,
relative, rems, v_flex,
IconWithIndicator, Indicator, InteractiveElement, IntoElement, KeyBinding, Label,
LabelCommon as _, LabelSize, ListItem, ListItemSpacing, ParentElement, RenderOnce,
SharedString, Styled, StyledExt, StyledTypography, ToggleButton, ToggleState, Toggleable,
Tooltip, Window, div, h_flex, px, relative, rems, v_flex,
};
use util::ResultExt;
use workspace::{ModalView, Workspace, pane};
@@ -50,7 +50,7 @@ pub(super) struct NewProcessModal {
mode: NewProcessMode,
debug_picker: Entity<Picker<DebugDelegate>>,
attach_mode: Entity<AttachMode>,
launch_mode: Entity<ConfigureMode>,
configure_mode: Entity<ConfigureMode>,
task_mode: TaskMode,
debugger: Option<DebugAdapterName>,
// save_scenario_state: Option<SaveScenarioState>,
@@ -194,11 +194,12 @@ impl NewProcessModal {
return Ok(());
};
let (used_tasks, current_resolved_tasks) =
task_inventory.update(cx, |task_inventory, cx| {
let (used_tasks, current_resolved_tasks) = task_inventory
.update(cx, |task_inventory, cx| {
task_inventory
.used_and_current_resolved_tasks(&task_contexts, cx)
})?;
.used_and_current_resolved_tasks(task_contexts.clone(), cx)
})?
.await;
debug_picker
.update_in(cx, |picker, window, cx| {
@@ -253,7 +254,7 @@ impl NewProcessModal {
Self {
debug_picker,
attach_mode,
launch_mode: configure_mode,
configure_mode,
task_mode,
debugger: None,
mode,
@@ -283,7 +284,7 @@ impl NewProcessModal {
NewProcessMode::Attach => self.attach_mode.update(cx, |this, cx| {
this.clone().render(window, cx).into_any_element()
}),
NewProcessMode::Launch => self.launch_mode.update(cx, |this, cx| {
NewProcessMode::Launch => self.configure_mode.update(cx, |this, cx| {
this.clone().render(dap_menu, window, cx).into_any_element()
}),
NewProcessMode::Debug => v_flex()
@@ -297,7 +298,7 @@ impl NewProcessModal {
match self.mode {
NewProcessMode::Task => self.task_mode.task_modal.focus_handle(cx),
NewProcessMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx),
NewProcessMode::Launch => self.launch_mode.read(cx).program.focus_handle(cx),
NewProcessMode::Launch => self.configure_mode.read(cx).program.focus_handle(cx),
NewProcessMode::Debug => self.debug_picker.focus_handle(cx),
}
}
@@ -305,7 +306,7 @@ impl NewProcessModal {
fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> {
let request = match self.mode {
NewProcessMode::Launch => Some(DebugRequest::Launch(
self.launch_mode.read(cx).debug_request(cx),
self.configure_mode.read(cx).debug_request(cx),
)),
NewProcessMode::Attach => Some(DebugRequest::Attach(
self.attach_mode.read(cx).debug_request(),
@@ -315,7 +316,7 @@ impl NewProcessModal {
let label = suggested_label(&request, debugger);
let stop_on_entry = if let NewProcessMode::Launch = &self.mode {
Some(self.launch_mode.read(cx).stop_on_entry.selected())
Some(self.configure_mode.read(cx).stop_on_entry.selected())
} else {
None
};
@@ -831,7 +832,7 @@ impl Render for NewProcessModal {
.disabled(
self.debugger.is_none()
|| self
.launch_mode
.configure_mode
.read(cx)
.program
.read(cx)
@@ -1202,7 +1203,7 @@ impl PickerDelegate for DebugDelegate {
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
"".into()
"Find a debug task, or debug a command.".into()
}
fn update_matches(
@@ -1265,6 +1266,96 @@ impl PickerDelegate for DebugDelegate {
}
}
fn confirm_input(
&mut self,
_secondary: bool,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) {
let text = self.prompt.clone();
let (task_context, worktree_id) = self
.task_contexts
.as_ref()
.and_then(|task_contexts| {
Some((
task_contexts.active_context().cloned()?,
task_contexts.worktree(),
))
})
.unwrap_or_default();
let mut args = shlex::split(&text).into_iter().flatten().peekable();
let mut env = HashMap::default();
while args.peek().is_some_and(|arg| arg.contains('=')) {
let arg = args.next().unwrap();
let (lhs, rhs) = arg.split_once('=').unwrap();
env.insert(lhs.to_string(), rhs.to_string());
}
let program = if let Some(program) = args.next() {
program
} else {
env = HashMap::default();
text
};
let args = args.collect::<Vec<_>>();
let task = task::TaskTemplate {
label: "one-off".to_owned(),
env,
command: program,
args,
..Default::default()
};
let Some(location) = self
.task_contexts
.as_ref()
.and_then(|cx| cx.location().cloned())
else {
return;
};
let file = location.buffer.read(cx).file();
let language = location.buffer.read(cx).language();
let language_name = language.as_ref().map(|l| l.name());
let Some(adapter): Option<DebugAdapterName> =
language::language_settings::language_settings(language_name, file, cx)
.debuggers
.first()
.map(SharedString::from)
.map(Into::into)
.or_else(|| {
language.and_then(|l| {
l.config()
.debuggers
.first()
.map(SharedString::from)
.map(Into::into)
})
})
else {
return;
};
let Some(debug_scenario) = cx
.global::<DapRegistry>()
.locators()
.iter()
.find_map(|locator| locator.1.create_scenario(&task, "one-off", adapter.clone()))
else {
return;
};
send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
self.debug_panel
.update(cx, |panel, cx| {
panel.start_session(debug_scenario, task_context, None, worktree_id, window, cx);
})
.ok();
cx.emit(DismissEvent);
}
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
let debug_scenario = self
.matches
@@ -1300,6 +1391,60 @@ impl PickerDelegate for DebugDelegate {
cx.emit(DismissEvent);
}
fn render_footer(
&self,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<ui::AnyElement> {
let current_modifiers = window.modifiers();
let footer = h_flex()
.w_full()
.h_8()
.p_2()
.justify_between()
.rounded_b_sm()
.bg(cx.theme().colors().ghost_element_selected)
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(
// TODO: add button to open selected task in debug.json
h_flex().into_any_element(),
)
.map(|this| {
if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() {
let action = picker::ConfirmInput {
secondary: current_modifiers.secondary(),
}
.boxed_clone();
this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| {
Button::new("launch-custom", "Launch Custom")
.label_size(LabelSize::Small)
.key_binding(keybind)
.on_click(move |_, window, cx| {
window.dispatch_action(action.boxed_clone(), cx)
})
}))
} else {
this.children(KeyBinding::for_action(&menu::Confirm, window, cx).map(
|keybind| {
let is_recent_selected =
self.divider_index >= Some(self.selected_index);
let run_entry_label =
if is_recent_selected { "Rerun" } else { "Spawn" };
Button::new("spawn", run_entry_label)
.label_size(LabelSize::Small)
.key_binding(keybind)
.on_click(|_, window, cx| {
window.dispatch_action(menu::Confirm.boxed_clone(), cx);
})
},
))
}
});
Some(footer.into_any_element())
}
fn render_match(
&self,
ix: usize,

View File

@@ -1,6 +1,6 @@
pub mod running;
use crate::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout};
use crate::{StackTraceView, persistence::SerializedLayout, session::running::DebugTerminal};
use dap::client::SessionId;
use gpui::{
App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
@@ -22,7 +22,6 @@ pub struct DebugSession {
running_state: Entity<RunningState>,
label: OnceLock<SharedString>,
stack_trace_view: OnceCell<Entity<StackTraceView>>,
_debug_panel: WeakEntity<DebugPanel>,
_worktree_store: WeakEntity<WorktreeStore>,
workspace: WeakEntity<Workspace>,
_subscriptions: [Subscription; 1],
@@ -38,8 +37,8 @@ impl DebugSession {
pub(crate) fn running(
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
parent_terminal: Option<Entity<DebugTerminal>>,
session: Entity<Session>,
_debug_panel: WeakEntity<DebugPanel>,
serialized_layout: Option<SerializedLayout>,
dock_axis: Axis,
window: &mut Window,
@@ -50,6 +49,7 @@ impl DebugSession {
session.clone(),
project.clone(),
workspace.clone(),
parent_terminal,
serialized_layout,
dock_axis,
window,
@@ -64,7 +64,6 @@ impl DebugSession {
remote_id: None,
running_state,
label: OnceLock::new(),
_debug_panel,
stack_trace_view: OnceCell::new(),
_worktree_store: project.read(cx).worktree_store().downgrade(),
workspace,

View File

@@ -605,6 +605,7 @@ impl RunningState {
session: Entity<Session>,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
parent_terminal: Option<Entity<DebugTerminal>>,
serialized_pane_layout: Option<SerializedLayout>,
dock_axis: Axis,
window: &mut Window,
@@ -617,7 +618,8 @@ impl RunningState {
StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
});
let debug_terminal = cx.new(|cx| DebugTerminal::empty(window, cx));
let debug_terminal =
parent_terminal.unwrap_or_else(|| cx.new(|cx| DebugTerminal::empty(window, cx)));
let variable_list =
cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
@@ -816,20 +818,20 @@ impl RunningState {
let request_type = dap_registry
.adapter(&adapter)
.ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))
.with_context(|| format!("{}: is not a valid adapter name", &adapter))
.and_then(|adapter| adapter.request_kind(&config));
let config_is_valid = request_type.is_ok();
let build_output = if let Some(build) = build {
let (task, locator_name) = match build {
let (task_template, locator_name) = match build {
BuildTaskDefinition::Template {
task_template,
locator_name,
} => (task_template, locator_name),
BuildTaskDefinition::ByName(ref label) => {
let Some(task) = task_store.update(cx, |this, cx| {
this.task_inventory().and_then(|inventory| {
let task = task_store.update(cx, |this, cx| {
this.task_inventory().map(|inventory| {
inventory.read(cx).task_template_by_label(
buffer,
worktree_id,
@@ -837,14 +839,15 @@ impl RunningState {
cx,
)
})
})?
else {
anyhow::bail!("Couldn't find task template for {:?}", build)
};
})?;
let task = match task {
Some(task) => task.await,
None => None,
}.with_context(|| format!("Couldn't find task template for {build:?}"))?;
(task, None)
}
};
let Some(task) = task.resolve_task("debug-build-task", &task_context) else {
let Some(task) = task_template.resolve_task("debug-build-task", &task_context) else {
anyhow::bail!("Could not resolve task variables within a debug scenario");
};
@@ -929,15 +932,13 @@ 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")?;
locator_name.with_context(|| {
format!("Could not find a valid locator for a build task and configure is invalid with error: {}", request_type.err()
.map(|err| err.to_string())
.unwrap_or_default())
})?;
let request = dap_store
.update(cx, |this, cx| {
this.run_debug_locator(&locator_name, task, cx)
@@ -953,7 +954,7 @@ impl RunningState {
let scenario = dap_registry
.adapter(&adapter)
.ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))
.with_context(|| 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);

View File

@@ -1,4 +1,5 @@
use std::{
ops::Range,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
@@ -277,10 +278,9 @@ impl BreakpointList {
let selected_ix = self.selected_ix;
let focus_handle = self.focus_handle.clone();
uniform_list(
cx.entity(),
"breakpoint-list",
self.breakpoints.len(),
move |this, range, window, cx| {
cx.processor(move |this, range: Range<usize>, window, cx| {
range
.clone()
.zip(&mut this.breakpoints[range])
@@ -291,7 +291,7 @@ impl BreakpointList {
.into_any_element()
})
.collect()
},
}),
)
.track_scroll(self.scroll_handle.clone())
.flex_grow()

View File

@@ -8,7 +8,7 @@ use project::{
ProjectItem as _, ProjectPath,
debugger::session::{Session, SessionEvent},
};
use std::{path::Path, sync::Arc};
use std::{ops::Range, path::Path, sync::Arc};
use ui::{Scrollbar, ScrollbarState, prelude::*};
use workspace::Workspace;
@@ -281,10 +281,11 @@ impl ModuleList {
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
uniform_list(
cx.entity(),
"module-list",
self.entries.len(),
|this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(),
cx.processor(|this, range: Range<usize>, _window, cx| {
range.map(|ix| this.render_entry(ix, cx)).collect()
}),
)
.track_scroll(self.scroll_handle.clone())
.size_full()

View File

@@ -183,6 +183,7 @@ impl StackFrameList {
let mut entries = Vec::new();
let mut collapsed_entries = Vec::new();
let mut first_stack_frame = None;
let mut first_not_subtle_frame = None;
let stack_frames = self.stack_frames(cx);
for stack_frame in &stack_frames {
@@ -197,6 +198,11 @@ impl StackFrameList {
}
first_stack_frame.get_or_insert(entries.len());
if stack_frame.dap.presentation_hint
!= Some(dap::StackFramePresentationHint::Subtle)
{
first_not_subtle_frame.get_or_insert(entries.len());
}
entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
}
}
@@ -209,7 +215,10 @@ impl StackFrameList {
std::mem::swap(&mut self.entries, &mut entries);
if let Some(ix) = first_stack_frame.filter(|_| open_first_stack_frame) {
if let Some(ix) = first_not_subtle_frame
.or(first_stack_frame)
.filter(|_| open_first_stack_frame)
{
self.select_ix(Some(ix), cx);
self.activate_selected_entry(window, cx);
} else if let Some(old_selected_frame_id) = old_selected_frame_id {

View File

@@ -980,10 +980,11 @@ impl Render for VariableList {
.on_action(cx.listener(Self::edit_variable))
.child(
uniform_list(
cx.entity().clone(),
"variable-list",
self.entries.len(),
move |this, range, window, cx| this.render_entries(range, window, cx),
cx.processor(move |this, range: Range<usize>, window, cx| {
this.render_entries(range, window, cx)
}),
)
.track_scroll(self.list_handle.clone())
.gap_1_5()

View File

@@ -35,7 +35,6 @@ assets.workspace = true
client.workspace = true
clock.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
convert_case.workspace = true
dap.workspace = true
db.workspace = true

View File

@@ -243,6 +243,8 @@ impl_actions!(
]
);
actions!(debugger, [RunToCursor, EvaluateSelectedText]);
actions!(
editor,
[
@@ -426,8 +428,6 @@ actions!(
DisableBreakpoint,
EnableBreakpoint,
EditLogBreakpoint,
DebuggerRunToCursor,
DebuggerEvaluateSelectedText,
ToggleAutoSignatureHelp,
ToggleGitBlameInline,
OpenGitBlameCommit,

View File

@@ -722,10 +722,9 @@ impl CompletionsMenu {
let last_rendered_range = self.last_rendered_range.clone();
let style = style.clone();
let list = uniform_list(
cx.entity().clone(),
"completions",
self.entries.borrow().len(),
move |_editor, range, _window, cx| {
cx.processor(move |_editor, range: Range<usize>, _window, cx| {
last_rendered_range.borrow_mut().replace(range.clone());
let start_ix = range.start;
let completions_guard = completions.borrow_mut();
@@ -837,7 +836,7 @@ impl CompletionsMenu {
)
})
.collect()
},
}),
)
.occlude()
.max_h(max_height_in_lines as f32 * window.line_height())
@@ -1452,10 +1451,9 @@ impl CodeActionsMenu {
let actions = self.actions.clone();
let selected_item = self.selected_item;
let list = uniform_list(
cx.entity().clone(),
"code_actions_menu",
self.actions.len(),
move |_this, range, _, cx| {
cx.processor(move |_this, range: Range<usize>, _, cx| {
actions
.iter()
.skip(range.start)
@@ -1518,7 +1516,7 @@ impl CodeActionsMenu {
)
})
.collect()
},
}),
)
.occlude()
.max_h(max_height_in_lines as f32 * window.line_height())

View File

@@ -240,7 +240,6 @@ pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration:
pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction";
pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict";
pub(crate) const MIN_LINE_NUMBER_DIGITS: u32 = 4;
pub(crate) const MINIMAP_FONT_SIZE: AbsoluteLength = AbsoluteLength::Pixels(px(2.));
pub type RenderDiffHunkControlsFn = Arc<
@@ -918,6 +917,7 @@ enum SelectionDragState {
Dragging {
selection: Selection<Anchor>,
drop_cursor: Selection<Anchor>,
hide_drop_cursor: bool,
},
}
@@ -1053,8 +1053,9 @@ pub struct Editor {
style: Option<EditorStyle>,
text_style_refinement: Option<TextStyleRefinement>,
next_editor_action_id: EditorActionId,
editor_actions:
Rc<RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&mut Window, &mut Context<Self>)>>>>,
editor_actions: Rc<
RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&Editor, &mut Window, &mut Context<Self>)>>>,
>,
use_autoclose: bool,
use_auto_surround: bool,
auto_replace_emoji_shortcode: bool,
@@ -5138,10 +5139,13 @@ impl Editor {
.as_ref()
.map_or(true, |provider| provider.filter_completions());
// When `is_incomplete` is false, can filter completions instead of re-querying when the
// current query is a suffix of the initial query.
if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() {
if !menu.is_incomplete && filter_completions {
if filter_completions {
menu.filter(query.clone(), provider.clone(), window, cx);
}
// When `is_incomplete` is false, no need to re-query completions when the current query
// is a suffix of the initial query.
if !menu.is_incomplete {
// If the new query is a suffix of the old query (typing more characters) and
// the previous result was complete, the existing completions can be filtered.
//
@@ -5159,7 +5163,6 @@ impl Editor {
menu.initial_position.to_offset(&snapshot) == position.to_offset(&snapshot)
};
if position_matches {
menu.filter(query.clone(), provider.clone(), window, cx);
return;
}
}
@@ -7538,8 +7541,7 @@ impl Editor {
"Set Breakpoint"
};
let run_to_cursor = command_palette_hooks::CommandPaletteFilter::try_global(cx)
.map_or(false, |filter| !filter.is_hidden(&DebuggerRunToCursor));
let run_to_cursor = window.is_action_available(&RunToCursor, cx);
let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state {
BreakpointState::Enabled => Some("Disable"),
@@ -7563,7 +7565,7 @@ impl Editor {
})
.ok();
window.dispatch_action(Box::new(DebuggerRunToCursor), cx);
window.dispatch_action(Box::new(RunToCursor), cx);
})
.separator()
})
@@ -14035,7 +14037,8 @@ impl Editor {
prefer_lsp && !lsp_tasks_by_rows.is_empty(),
new_rows,
cx.clone(),
);
)
.await;
editor
.update(cx, |editor, _| {
editor.clear_tasks();
@@ -14065,35 +14068,40 @@ impl Editor {
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 mut tasks = cx
cx: AsyncWindowContext,
) -> Task<Vec<((BufferId, BufferRow), RunnableTasks)>> {
cx.spawn(async move |cx| {
let mut runnable_rows = Vec::with_capacity(runnable_ranges.len());
for mut runnable in runnable_ranges {
let Some(tasks) = cx
.update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx))
.ok()?;
.ok()
else {
continue;
};
let mut tasks = tasks.await;
if prefer_lsp {
tasks.retain(|(task_kind, _)| {
!matches!(task_kind, TaskSourceKind::Language { .. })
});
}
if tasks.is_empty() {
return None;
continue;
}
let point = runnable.run_range.start.to_point(&snapshot.buffer_snapshot);
let row = snapshot
let Some(row) = snapshot
.buffer_snapshot
.buffer_line_for_row(MultiBufferRow(point.row))?
.1
.start
.row;
.buffer_line_for_row(MultiBufferRow(point.row))
.map(|(_, range)| range.start.row)
else {
continue;
};
let context_range =
BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end);
Some((
runnable_rows.push((
(runnable.buffer_id, row),
RunnableTasks {
templates: tasks,
@@ -14104,16 +14112,17 @@ impl Editor {
column: point.column,
extra_variables: runnable.extra_captures,
},
))
})
.collect()
));
}
runnable_rows
})
}
fn templates_with_tags(
project: &Entity<Project>,
runnable: &mut Runnable,
cx: &mut App,
) -> Vec<(TaskSourceKind, TaskTemplate)> {
) -> Task<Vec<(TaskSourceKind, TaskTemplate)>> {
let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| {
let (worktree_id, file) = project
.buffer_for_id(runnable.buffer, cx)
@@ -14128,39 +14137,40 @@ impl Editor {
)
});
let mut templates_with_tags = mem::take(&mut runnable.tags)
.into_iter()
.flat_map(|RunnableTag(tag)| {
inventory
.as_ref()
.into_iter()
.flat_map(|inventory| {
inventory.read(cx).list_tasks(
file.clone(),
Some(runnable.language.clone()),
worktree_id,
cx,
)
})
.filter(move |(_, template)| {
template.tags.iter().any(|source_tag| source_tag == &tag)
})
})
.sorted_by_key(|(kind, _)| kind.to_owned())
.collect::<Vec<_>>();
if let Some((leading_tag_source, _)) = templates_with_tags.first() {
// Strongest source wins; if we have worktree tag binding, prefer that to
// global and language bindings;
// if we have a global binding, prefer that to language binding.
let first_mismatch = templates_with_tags
.iter()
.position(|(tag_source, _)| tag_source != leading_tag_source);
if let Some(index) = first_mismatch {
templates_with_tags.truncate(index);
let tags = mem::take(&mut runnable.tags);
let language = runnable.language.clone();
cx.spawn(async move |cx| {
let mut templates_with_tags = Vec::new();
if let Some(inventory) = inventory {
for RunnableTag(tag) in tags {
let Ok(new_tasks) = inventory.update(cx, |inventory, cx| {
inventory.list_tasks(file.clone(), Some(language.clone()), worktree_id, cx)
}) else {
return templates_with_tags;
};
templates_with_tags.extend(new_tasks.await.into_iter().filter(
move |(_, template)| {
template.tags.iter().any(|source_tag| source_tag == &tag)
},
));
}
}
}
templates_with_tags.sort_by_key(|(kind, _)| kind.to_owned());
templates_with_tags
if let Some((leading_tag_source, _)) = templates_with_tags.first() {
// Strongest source wins; if we have worktree tag binding, prefer that to
// global and language bindings;
// if we have a global binding, prefer that to language binding.
let first_mismatch = templates_with_tags
.iter()
.position(|(tag_source, _)| tag_source != leading_tag_source);
if let Some(index) = first_mismatch {
templates_with_tags.truncate(index);
}
}
templates_with_tags
})
}
pub fn move_to_enclosing_bracket(
@@ -16076,13 +16086,16 @@ impl Editor {
window: &Window,
cx: &mut Context<Self>,
) -> Option<()> {
let project = self.project.as_ref()?.downgrade();
if !self.mode().is_full() {
return None;
}
let pull_diagnostics_settings = ProjectSettings::get_global(cx)
.diagnostics
.lsp_pull_diagnostics;
if !pull_diagnostics_settings.enabled {
return None;
}
let project = self.project.as_ref()?.downgrade();
let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms);
let mut buffers = self.buffer.read(cx).all_buffers();
if let Some(buffer_id) = buffer_id {
@@ -18133,16 +18146,9 @@ impl Editor {
.selections
.disjoint_anchors()
.iter()
.map(|selection| {
let range = if selection.reversed {
selection.end.text_anchor..selection.start.text_anchor
} else {
selection.start.text_anchor..selection.end.text_anchor
};
Location {
buffer: buffer.clone(),
range,
}
.map(|range| Location {
buffer: buffer.clone(),
range: range.start.text_anchor..range.end.text_anchor,
})
.collect::<Vec<_>>();
@@ -19735,7 +19741,7 @@ impl Editor {
.flatten()
.filter_map(|keystroke| {
if keystroke.modifiers.is_subset_of(&Modifiers::shift()) {
Some(keystroke.key_char.clone().unwrap_or(keystroke.key.clone()))
keystroke.key_char.clone()
} else {
None
}
@@ -19812,6 +19818,21 @@ impl Editor {
}
}
pub fn register_action_renderer(
&mut self,
listener: impl Fn(&Editor, &mut Window, &mut Context<Editor>) + 'static,
) -> Subscription {
let id = self.next_editor_action_id.post_inc();
self.editor_actions
.borrow_mut()
.insert(id, Box::new(listener));
let editor_actions = self.editor_actions.clone();
Subscription::new(move || {
editor_actions.borrow_mut().remove(&id);
})
}
pub fn register_action<A: Action>(
&mut self,
listener: impl Fn(&A, &mut Window, &mut App) + 'static,
@@ -19820,7 +19841,7 @@ impl Editor {
let listener = Arc::new(listener);
self.editor_actions.borrow_mut().insert(
id,
Box::new(move |window, _| {
Box::new(move |_, window, _| {
let listener = listener.clone();
window.on_action(TypeId::of::<A>(), move |action, phase, window, cx| {
let action = action.downcast_ref().unwrap();
@@ -20052,8 +20073,13 @@ fn process_completion_for_edit(
let buffer = buffer.read(cx);
let buffer_snapshot = buffer.snapshot();
let (snippet, new_text) = if completion.is_snippet() {
// Workaround for typescript language server issues so that methods don't expand within
// strings and functions with type expressions. The previous point is used because the query
// for function identifier doesn't match when the cursor is immediately after. See PR #30312
let mut snippet_source = completion.new_text.clone();
if let Some(scope) = buffer_snapshot.language_scope_at(cursor_position) {
let mut previous_point = text::ToPoint::to_point(cursor_position, buffer);
previous_point.column = previous_point.column.saturating_sub(1);
if let Some(scope) = buffer_snapshot.language_scope_at(previous_point) {
if scope.prefers_label_for_snippet_in_completion() {
if let Some(label) = completion.label() {
if matches!(
@@ -21531,7 +21557,8 @@ impl EditorSnapshot {
.unwrap_or(gutter_settings.line_numbers);
let line_gutter_width = if show_line_numbers {
// Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines.
let min_width_for_number_on_gutter = em_advance * MIN_LINE_NUMBER_DIGITS as f32;
let min_width_for_number_on_gutter =
em_advance * gutter_settings.min_line_number_digits as f32;
max_line_number_width.max(min_width_for_number_on_gutter)
} else {
0.0.into()

View File

@@ -150,6 +150,7 @@ impl Minimap {
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct Gutter {
pub min_line_number_digits: usize,
pub line_numbers: bool,
pub runnables: bool,
pub breakpoints: bool,
@@ -609,6 +610,10 @@ pub struct GutterContent {
///
/// Default: true
pub line_numbers: Option<bool>,
/// Minimum number of characters to reserve space for in the gutter.
///
/// Default: 4
pub min_line_number_digits: Option<usize>,
/// Whether to show runnable buttons in the gutter.
///
/// Default: true

View File

@@ -6,10 +6,10 @@ use crate::{
Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT,
FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp,
MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
OpenExcerpts, PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt,
SelectPhase, SelectedTextHighlight, Selection, SelectionDragState, SoftWrap,
StickyHeaderExcerpt, ToPoint, ToggleFold,
MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown,
PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase,
SelectedTextHighlight, Selection, SelectionDragState, SoftWrap, StickyHeaderExcerpt, ToPoint,
ToggleFold,
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
display_map::{
Block, BlockContext, BlockStyle, DisplaySnapshot, EditorMargins, FoldId, HighlightedChunk,
@@ -187,7 +187,7 @@ impl EditorElement {
let editor = &self.editor;
editor.update(cx, |editor, cx| {
for action in editor.editor_actions.borrow().values() {
(action)(window, cx)
(action)(editor, window, cx)
}
});
@@ -856,8 +856,11 @@ impl EditorElement {
SelectionDragState::Dragging { ref selection, .. } => {
let snapshot = editor.snapshot(window, cx);
let selection_display = selection.map(|anchor| anchor.to_display_point(&snapshot));
if !point_for_position.intersects_selection(&selection_display) {
let is_cut = !event.modifiers.control;
if !point_for_position.intersects_selection(&selection_display)
&& text_hitbox.is_hovered(window)
{
let is_cut = !(cfg!(target_os = "macos") && event.modifiers.alt
|| cfg!(not(target_os = "macos")) && event.modifiers.control);
editor.move_selection_on_drop(
&selection.clone(),
point_for_position.previous_valid,
@@ -865,10 +868,11 @@ impl EditorElement {
window,
cx,
);
editor.selection_drag_state = SelectionDragState::None;
cx.stop_propagation();
return;
}
editor.selection_drag_state = SelectionDragState::None;
cx.stop_propagation();
cx.notify();
return;
}
_ => {}
}
@@ -941,7 +945,8 @@ impl EditorElement {
return;
}
let text_bounds = position_map.text_hitbox.bounds;
let text_hitbox = &position_map.text_hitbox;
let text_bounds = text_hitbox.bounds;
let point_for_position = position_map.point_for_position(event.position);
let mut scroll_delta = gpui::Point::<f32>::default();
@@ -982,10 +987,12 @@ impl EditorElement {
match editor.selection_drag_state {
SelectionDragState::Dragging {
ref mut drop_cursor,
ref mut hide_drop_cursor,
..
} => {
drop_cursor.start = drop_anchor;
drop_cursor.end = drop_anchor;
*hide_drop_cursor = !text_hitbox.is_hovered(window);
}
SelectionDragState::ReadyToDrag { ref selection, .. } => {
let drop_cursor = Selection {
@@ -998,6 +1005,7 @@ impl EditorElement {
editor.selection_drag_state = SelectionDragState::Dragging {
selection: selection.clone(),
drop_cursor,
hide_drop_cursor: false,
};
}
_ => {}
@@ -1251,16 +1259,18 @@ impl EditorElement {
if let SelectionDragState::Dragging {
ref selection,
ref drop_cursor,
ref hide_drop_cursor,
} = editor.selection_drag_state
{
if drop_cursor
.start
.cmp(&selection.start, &snapshot.buffer_snapshot)
.eq(&Ordering::Less)
|| drop_cursor
.end
.cmp(&selection.end, &snapshot.buffer_snapshot)
.eq(&Ordering::Greater)
if !hide_drop_cursor
&& (drop_cursor
.start
.cmp(&selection.start, &snapshot.buffer_snapshot)
.eq(&Ordering::Less)
|| drop_cursor
.end
.cmp(&selection.end, &snapshot.buffer_snapshot)
.eq(&Ordering::Greater))
{
let drag_cursor_layout = SelectionLayout::new(
drop_cursor.clone(),
@@ -2816,7 +2826,8 @@ impl EditorElement {
let available_width = gutter_dimensions.left_padding - git_gutter_width;
let editor = self.editor.clone();
let is_wide = max_line_number_length >= MIN_LINE_NUMBER_DIGITS
let is_wide = max_line_number_length
>= EditorSettings::get_global(cx).gutter.min_line_number_digits as u32
&& row_info
.buffer_row
.is_some_and(|row| (row + 1).ilog10() + 1 == max_line_number_length)

View File

@@ -1,8 +1,8 @@
use crate::{
Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DebuggerEvaluateSelectedText, DisplayPoint,
DisplaySnapshot, Editor, FindAllReferences, GoToDeclaration, GoToDefinition,
GoToImplementation, GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode,
SelectionExt, ToDisplayPoint, ToggleCodeActions,
Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DisplayPoint, DisplaySnapshot, Editor,
EvaluateSelectedText, FindAllReferences, GoToDeclaration, GoToDefinition, GoToImplementation,
GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, SelectionExt,
ToDisplayPoint, ToggleCodeActions,
actions::{Format, FormatSelections},
selections_collection::SelectionsCollection,
};
@@ -199,17 +199,14 @@ pub fn deploy_context_menu(
.is_some()
});
let evaluate_selection = command_palette_hooks::CommandPaletteFilter::try_global(cx)
.map_or(false, |filter| {
!filter.is_hidden(&DebuggerEvaluateSelectedText)
});
let evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx);
ui::ContextMenu::build(window, cx, |menu, _window, _cx| {
let builder = menu
.on_blur_subscription(Subscription::new(|| {}))
.when(evaluate_selection && has_selections, |builder| {
builder
.action("Evaluate Selection", Box::new(DebuggerEvaluateSelectedText))
.action("Evaluate Selection", Box::new(EvaluateSelectedText))
.separator()
})
.action("Go to Definition", Box::new(GoToDefinition))

View File

@@ -240,8 +240,7 @@ impl EditorTestContext {
// unlike cx.simulate_keystrokes(), this does not run_until_parked
// so you can use it to test detailed timing
pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
let keyboard_mapper = self.keyboard_mapper();
let keystroke = Keystroke::parse(keystroke_text, keyboard_mapper.as_ref()).unwrap();
let keystroke = Keystroke::parse(keystroke_text).unwrap();
self.cx.dispatch_keystroke(self.window, keystroke);
}

View File

@@ -50,7 +50,7 @@ interface dap {
}
record debug-adapter-binary {
command: string,
command: option<string>,
arguments: list<string>,
envs: env-vars,
cwd: option<string>,

View File

@@ -1682,12 +1682,15 @@ impl ExtensionStore {
pub fn register_ssh_client(&mut self, client: Entity<SshRemoteClient>, cx: &mut Context<Self>) {
let connection_options = client.read(cx).connection_options();
if self.ssh_clients.contains_key(&connection_options.ssh_url()) {
return;
let ssh_url = connection_options.ssh_url();
if let Some(existing_client) = self.ssh_clients.get(&ssh_url) {
if existing_client.upgrade().is_some() {
return;
}
}
self.ssh_clients
.insert(connection_options.ssh_url(), client.downgrade());
self.ssh_clients.insert(ssh_url, client.downgrade());
self.ssh_registered_tx.unbounded_send(()).ok();
}
}

View File

@@ -554,13 +554,15 @@ impl ExtensionsPage {
)
.child(
h_flex()
.gap_2()
.gap_1()
.justify_between()
.child(
Button::new(
SharedString::from(format!("rebuild-{}", extension.id)),
"Rebuild",
)
.color(Color::Accent)
.disabled(matches!(status, ExtensionStatus::Upgrading))
.on_click({
let extension_id = extension.id.clone();
move |_, _, cx| {
@@ -568,12 +570,12 @@ impl ExtensionsPage {
store.rebuild_dev_extension(extension_id.clone(), cx)
});
}
})
.color(Color::Accent)
.disabled(matches!(status, ExtensionStatus::Upgrading)),
}),
)
.child(
Button::new(SharedString::from(extension.id.clone()), "Uninstall")
.color(Color::Accent)
.disabled(matches!(status, ExtensionStatus::Removing))
.on_click({
let extension_id = extension.id.clone();
move |_, _, cx| {
@@ -581,9 +583,7 @@ impl ExtensionsPage {
store.uninstall_extension(extension_id.clone(), cx)
});
}
})
.color(Color::Accent)
.disabled(matches!(status, ExtensionStatus::Removing)),
}),
)
.when(can_configure, |this| {
this.child(
@@ -591,8 +591,8 @@ impl ExtensionsPage {
SharedString::from(format!("configure-{}", extension.id)),
"Configure",
)
.color(Color::Accent)
.disabled(matches!(status, ExtensionStatus::Installing))
.on_click({
let manifest = Arc::new(extension.clone());
move |_, _, cx| {
@@ -609,9 +609,7 @@ impl ExtensionsPage {
});
}
}
})
.color(Color::Accent)
.disabled(matches!(status, ExtensionStatus::Installing)),
}),
)
}),
),
@@ -1479,18 +1477,12 @@ impl Render for ExtensionsPage {
return this.py_4().child(self.render_empty_state(cx));
}
let extensions_page = cx.entity().clone();
let scroll_handle = self.list.clone();
this.child(
uniform_list(
extensions_page,
"entries",
count,
Self::render_extensions,
)
.flex_grow()
.pb_4()
.track_scroll(scroll_handle),
uniform_list("entries", count, cx.processor(Self::render_extensions))
.flex_grow()
.pb_4()
.track_scroll(scroll_handle),
)
.child(
div()

View File

@@ -55,6 +55,7 @@ use project::{
use serde::{Deserialize, Serialize};
use settings::{Settings as _, SettingsStore};
use std::future::Future;
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::{collections::HashSet, sync::Arc, time::Duration, usize};
use strum::{IntoEnumIterator, VariantNames};
@@ -3710,8 +3711,10 @@ impl GitPanel {
.relative()
.overflow_hidden()
.child(
uniform_list(cx.entity().clone(), "entries", entry_count, {
move |this, range, window, cx| {
uniform_list(
"entries",
entry_count,
cx.processor(move |this, range: Range<usize>, window, cx| {
let mut items = Vec::with_capacity(range.end - range.start);
for ix in range {
@@ -3739,8 +3742,8 @@ impl GitPanel {
}
items
}
})
}),
)
.when(
!self.horizontal_scrollbar.show_track
&& self.horizontal_scrollbar.show_scrollbar,

View File

@@ -378,8 +378,6 @@ impl DataTable {
impl Render for DataTable {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let entity = cx.entity();
div()
.font_family(".SystemUIFont")
.bg(gpui::white())
@@ -431,8 +429,10 @@ impl Render for DataTable {
.relative()
.size_full()
.child(
uniform_list(entity, "items", self.quotes.len(), {
move |this, range, _, _| {
uniform_list(
"items",
self.quotes.len(),
cx.processor(move |this, range: Range<usize>, _, _| {
this.visible_range = range.clone();
let mut items = Vec::with_capacity(range.end - range.start);
for i in range {
@@ -441,8 +441,8 @@ impl Render for DataTable {
}
}
items
}
})
}),
)
.size_full()
.track_scroll(self.scroll_handle.clone()),
)

View File

@@ -9,10 +9,9 @@ impl Render for UniformListExample {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div().size_full().bg(rgb(0xffffff)).child(
uniform_list(
cx.entity().clone(),
"entries",
50,
|_this, range, _window, _cx| {
cx.processor(|_this, range, _window, _cx| {
let mut items = Vec::new();
for ix in range {
let item = ix + 1;
@@ -29,7 +28,7 @@ impl Render for UniformListExample {
);
}
items
},
}),
)
.h_full(),
)

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, PlatformKeyboardMapper, Point, PromptBuilder,
PromptButton, 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,
};
@@ -262,7 +262,6 @@ pub struct App {
pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
pub(crate) focus_handles: Arc<FocusMap>,
pub(crate) keymap: Rc<RefCell<Keymap>>,
pub(crate) keyboard_mapper: Box<dyn PlatformKeyboardMapper>,
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
pub(crate) global_action_listeners:
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
@@ -309,7 +308,6 @@ impl App {
let text_system = Arc::new(TextSystem::new(platform.text_system()));
let entities = EntityMap::new();
let keyboard_mapper = platform.keyboard_mapper();
let keyboard_layout = platform.keyboard_layout();
let app = Rc::new_cyclic(|this| AppCell {
@@ -335,7 +333,6 @@ impl App {
window_handles: FxHashMap::default(),
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
keymap: Rc::new(RefCell::new(Keymap::default())),
keyboard_mapper,
keyboard_layout,
global_action_listeners: FxHashMap::default(),
pending_effects: VecDeque::new(),
@@ -372,7 +369,6 @@ impl App {
move || {
if let Some(app) = app.upgrade() {
let cx = &mut app.borrow_mut();
cx.keyboard_mapper = cx.platform.keyboard_mapper();
cx.keyboard_layout = cx.platform.keyboard_layout();
cx.keyboard_layout_observers
.clone()
@@ -417,11 +413,6 @@ impl App {
self.quitting = false;
}
/// Get the keyboard mapper of current keyboard layout
pub fn keyboard_mapper(&self) -> &dyn PlatformKeyboardMapper {
self.keyboard_mapper.as_ref()
}
/// Get the id of the current keyboard layout
pub fn keyboard_layout(&self) -> &dyn PlatformKeyboardLayout {
self.keyboard_layout.as_ref()

View File

@@ -225,6 +225,18 @@ impl<'a, T: 'static> Context<'a, T> {
}
}
/// Convenience method for producing view state in a closure.
/// See `listener` for more details.
pub fn processor<E, R>(
&self,
f: impl Fn(&mut T, E, &mut Window, &mut Context<T>) -> R + 'static,
) -> impl Fn(E, &mut Window, &mut App) -> R + 'static {
let view = self.entity();
move |e: E, window: &mut Window, cx: &mut App| {
view.update(cx, |view, cx| f(view, e, window, cx))
}
}
/// Run something using this entity and cx, when the returned struct is dropped
pub fn on_drop(
&self,

View File

@@ -3,9 +3,9 @@ use crate::{
BackgroundExecutor, BorrowAppContext, Bounds, ClipboardItem, DrawPhase, Drawable, Element,
Empty, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
Platform, PlatformKeyboardMapper, Point, Render, Result, Size, Task, TestDispatcher,
TestPlatform, TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window,
WindowBounds, WindowHandle, WindowOptions,
Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform,
TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, WindowBounds,
WindowHandle, WindowOptions,
};
use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt, channel::oneshot};
@@ -397,20 +397,14 @@ impl TestAppContext {
self.background_executor.run_until_parked()
}
/// Returns the current keyboard mapper for this platform.
pub fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper> {
self.test_platform.keyboard_mapper()
}
/// simulate_keystrokes takes a space-separated list of keys to type.
/// cx.simulate_keystrokes("cmd-shift-p b k s p enter")
/// in Zed, this will run backspace on the current editor through the command palette.
/// This will also run the background executor until it's parked.
pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
let keyboard_mapper = self.keyboard_mapper();
for keystroke in keystrokes
.split(' ')
.map(|source| Keystroke::parse(source, keyboard_mapper.as_ref()))
.map(Keystroke::parse)
.map(Result::unwrap)
{
self.dispatch_keystroke(window, keystroke);
@@ -424,12 +418,7 @@ impl TestAppContext {
/// will type abc into your current editor
/// This will also run the background executor until it's parked.
pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
let keyboard_mapper = self.keyboard_mapper();
for keystroke in input
.split("")
.map(|source| Keystroke::parse(source, keyboard_mapper.as_ref()))
.map(Result::unwrap)
{
for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) {
self.dispatch_keystroke(window, keystroke);
}

View File

@@ -5,10 +5,10 @@
//! elements with uniform height.
use crate::{
AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, Element, ElementId, Entity,
GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement,
IsZero, LayoutId, ListSizingBehavior, Overflow, Pixels, Render, ScrollHandle, Size,
StyleRefinement, Styled, Window, point, size,
AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, GlobalElementId,
Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId,
ListSizingBehavior, Overflow, Pixels, ScrollHandle, Size, StyleRefinement, Styled, Window,
point, size,
};
use smallvec::SmallVec;
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -19,28 +19,23 @@ use super::ListHorizontalSizingBehavior;
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
/// uniform_list will only render the visible subset of items.
#[track_caller]
pub fn uniform_list<I, R, V>(
view: Entity<V>,
id: I,
pub fn uniform_list<R>(
id: impl Into<ElementId>,
item_count: usize,
f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>,
f: impl 'static + Fn(Range<usize>, &mut Window, &mut App) -> Vec<R>,
) -> UniformList
where
I: Into<ElementId>,
R: IntoElement,
V: Render,
{
let id = id.into();
let mut base_style = StyleRefinement::default();
base_style.overflow.y = Some(Overflow::Scroll);
let render_range = move |range, window: &mut Window, cx: &mut App| {
view.update(cx, |this, cx| {
f(this, range, window, cx)
.into_iter()
.map(|component| component.into_any_element())
.collect()
})
let render_range = move |range: Range<usize>, window: &mut Window, cx: &mut App| {
f(range, window, cx)
.into_iter()
.map(|component| component.into_any_element())
.collect()
};
UniformList {

View File

@@ -538,22 +538,8 @@ mod test {
})
.unwrap();
cx.dispatch_keystroke(
*window,
Keystroke {
modifiers: crate::Modifiers::none(),
key: "a".to_owned(),
key_char: None,
},
);
cx.dispatch_keystroke(
*window,
Keystroke {
modifiers: crate::Modifiers::control(),
key: "g".to_owned(),
key_char: None,
},
);
cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap());
cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap());
window
.update(cx, |test_view, _, _| {

View File

@@ -310,11 +310,7 @@ mod tests {
assert!(
keymap
.bindings_for_input(
&[Keystroke {
modifiers: crate::Modifiers::control(),
key: "a".to_owned(),
key_char: None
}],
&[Keystroke::parse("ctrl-a").unwrap()],
&[KeyContext::parse("barf").unwrap()],
)
.0
@@ -323,11 +319,7 @@ mod tests {
assert!(
!keymap
.bindings_for_input(
&[Keystroke {
modifiers: crate::Modifiers::control(),
key: "a".to_owned(),
key_char: None
}],
&[Keystroke::parse("ctrl-a").unwrap()],
&[KeyContext::parse("editor").unwrap()],
)
.0
@@ -338,11 +330,7 @@ mod tests {
assert!(
keymap
.bindings_for_input(
&[Keystroke {
modifiers: crate::Modifiers::control(),
key: "a".to_owned(),
key_char: None
}],
&[Keystroke::parse("ctrl-a").unwrap()],
&[KeyContext::parse("editor mode=full").unwrap()],
)
.0
@@ -353,11 +341,7 @@ mod tests {
assert!(
keymap
.bindings_for_input(
&[Keystroke {
modifiers: crate::Modifiers::control(),
key: "b".to_owned(),
key_char: None
}],
&[Keystroke::parse("ctrl-b").unwrap()],
&[KeyContext::parse("barf").unwrap()],
)
.0
@@ -376,16 +360,8 @@ mod tests {
let mut keymap = Keymap::default();
keymap.add_bindings(bindings.clone());
let space = || Keystroke {
modifiers: crate::Modifiers::none(),
key: "space".to_owned(),
key_char: None,
};
let w = || Keystroke {
modifiers: crate::Modifiers::none(),
key: "w".to_owned(),
key_char: None,
};
let space = || Keystroke::parse("space").unwrap();
let w = || Keystroke::parse("w").unwrap();
let space_w = [space(), w()];
let space_w_w = [space(), w(), w()];

View File

@@ -2,10 +2,7 @@ use std::rc::Rc;
use collections::HashMap;
use crate::{
Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, PlatformKeyboardMapper,
TestKeyboardMapper,
};
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke};
use smallvec::SmallVec;
/// A keybinding and its associated metadata, from the keymap.
@@ -28,20 +25,12 @@ impl Clone for KeyBinding {
impl KeyBinding {
/// Construct a new keybinding from the given data. Panics on parse error.
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
let keyboard_mapper = TestKeyboardMapper::new();
let context_predicate = if let Some(context) = context {
Some(KeyBindingContextPredicate::parse(context).unwrap().into())
} else {
None
};
Self::load(
keystrokes,
Box::new(action),
context_predicate,
None,
&keyboard_mapper,
)
.unwrap()
Self::load(keystrokes, Box::new(action), context_predicate, None).unwrap()
}
/// Load a keybinding from the given raw data.
@@ -50,11 +39,10 @@ impl KeyBinding {
action: Box<dyn Action>,
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
key_equivalents: Option<&HashMap<char, char>>,
keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> std::result::Result<Self, InvalidKeystrokeError> {
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
.split_whitespace()
.map(|source| Keystroke::parse(source, keyboard_mapper))
.map(Keystroke::parse)
.collect::<std::result::Result<_, _>>()?;
if let Some(equivalents) = key_equivalents {

View File

@@ -1,6 +1,5 @@
mod app_menu;
mod keyboard;
mod keycode;
mod keystroke;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
@@ -67,7 +66,6 @@ use uuid::Uuid;
pub use app_menu::*;
pub use keyboard::*;
pub use keycode::*;
pub use keystroke::*;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
@@ -144,7 +142,11 @@ pub fn guess_compositor() -> &'static str {
#[cfg(target_os = "windows")]
pub(crate) fn current_platform(_headless: bool) -> Rc<dyn Platform> {
Rc::new(WindowsPlatform::new())
Rc::new(
WindowsPlatform::new()
.inspect_err(|err| show_error("Error: Zed failed to launch", err.to_string()))
.unwrap(),
)
}
pub(crate) trait Platform: 'static {
@@ -196,6 +198,7 @@ pub(crate) trait Platform: 'static {
fn on_quit(&self, callback: Box<dyn FnMut()>);
fn on_reopen(&self, callback: Box<dyn FnMut()>);
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
@@ -215,6 +218,7 @@ pub(crate) trait Platform: 'static {
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
fn compositor_name(&self) -> &'static str {
""
@@ -235,10 +239,6 @@ pub(crate) trait Platform: 'static {
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper>;
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
}
/// A handle to a platform's display, e.g. a monitor or laptop screen.

View File

@@ -342,7 +342,7 @@ impl BladeRenderer {
let surface = context
.gpu
.create_surface_configured(window, surface_config)
.unwrap();
.map_err(|err| anyhow::anyhow!("Failed to create surface: {err:?}"))?;
let command_encoder = context.gpu.create_command_encoder(gpu::CommandEncoderDesc {
name: "main",

View File

@@ -1,7 +1,3 @@
use anyhow::Result;
use crate::{Modifiers, ScanCode};
/// A trait for platform-specific keyboard layouts
pub trait PlatformKeyboardLayout {
/// Get the keyboard layout ID, which should be unique to the layout
@@ -9,109 +5,3 @@ pub trait PlatformKeyboardLayout {
/// Get the keyboard layout display name
fn name(&self) -> &str;
}
/// TODO:
pub trait PlatformKeyboardMapper {
/// TODO:
fn scan_code_to_key(&self, scan_code: ScanCode, modifiers: &mut Modifiers) -> Result<String>;
}
/// TODO:
pub struct TestKeyboardMapper {
#[cfg(target_os = "windows")]
mapper: super::WindowsKeyboardMapper,
#[cfg(target_os = "macos")]
mapper: super::MacKeyboardMapper,
#[cfg(target_os = "linux")]
mapper: super::LinuxKeyboardMapper,
}
impl PlatformKeyboardMapper for TestKeyboardMapper {
fn scan_code_to_key(&self, scan_code: ScanCode, modifiers: &mut Modifiers) -> Result<String> {
self.mapper.scan_code_to_key(scan_code, modifiers)
}
}
impl TestKeyboardMapper {
/// TODO:
pub fn new() -> Self {
Self {
#[cfg(target_os = "windows")]
mapper: super::WindowsKeyboardMapper::new(),
#[cfg(target_os = "macos")]
mapper: super::MacKeyboardMapper::new(),
#[cfg(target_os = "linux")]
mapper: super::LinuxKeyboardMapper::new(),
}
}
}
/// A dummy keyboard mapper that does not support any key mappings
pub struct EmptyKeyboardMapper;
impl PlatformKeyboardMapper for EmptyKeyboardMapper {
fn scan_code_to_key(&self, _scan_code: ScanCode, _modifiers: &mut Modifiers) -> Result<String> {
anyhow::bail!("EmptyKeyboardMapper does not support scan codes")
}
}
#[allow(unused)]
pub(crate) fn is_letter_key(key: &str) -> bool {
matches!(
key,
"a" | "b"
| "c"
| "d"
| "e"
| "f"
| "g"
| "h"
| "i"
| "j"
| "k"
| "l"
| "m"
| "n"
| "o"
| "p"
| "q"
| "r"
| "s"
| "t"
| "u"
| "v"
| "w"
| "x"
| "y"
| "z"
)
}
#[cfg(test)]
mod tests {
use strum::IntoEnumIterator;
use crate::{Modifiers, ScanCode};
use super::{PlatformKeyboardMapper, TestKeyboardMapper};
#[test]
fn test_scan_code_to_key() {
let mapper = TestKeyboardMapper::new();
for scan_code in ScanCode::iter() {
let mut modifiers = Modifiers::default();
let key = mapper.scan_code_to_key(scan_code, &mut modifiers).unwrap();
assert_eq!(key, scan_code.to_key(false));
assert_eq!(modifiers, Modifiers::default());
let mut modifiers = Modifiers::shift();
let shifted_key = mapper.scan_code_to_key(scan_code, &mut modifiers).unwrap();
assert_eq!(shifted_key, scan_code.to_key(true));
if shifted_key != key {
assert_eq!(modifiers, Modifiers::default());
} else {
assert_eq!(modifiers, Modifiers::shift());
}
}
}
}

View File

@@ -1,590 +0,0 @@
use strum::EnumIter;
/// Scan codes for the keyboard, which are used to identify keys in a keyboard layout-independent way.
/// Currently, we only support a limited set of scan codes here:
/// https://code.visualstudio.com/docs/configure/keybindings#_keyboard-layoutindependent-bindings
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
pub enum ScanCode {
/// F1 key
F1,
/// F1 key
F2,
/// F1 key
F3,
/// F1 key
F4,
/// F1 key
F5,
/// F1 key
F6,
/// F1 key
F7,
/// F1 key
F8,
/// F1 key
F9,
/// F1 key
F10,
/// F1 key
F11,
/// F1 key
F12,
/// F1 key
F13,
/// F1 key
F14,
/// F1 key
F15,
/// F1 key
F16,
/// F1 key
F17,
/// F1 key
F18,
/// F1 key
F19,
/// F20 key
F20,
/// F20 key
F21,
/// F20 key
F22,
/// F20 key
F23,
/// F20 key
F24,
/// A key on the main keyboard.
A,
/// B key on the main keyboard.
B,
/// C key on the main keyboard.
C,
/// D key on the main keyboard.
D,
/// E key on the main keyboard.
E,
/// F key on the main keyboard.
F,
/// G key on the main keyboard.
G,
/// H key on the main keyboard.
H,
/// I key on the main keyboard.
I,
/// J key on the main keyboard.
J,
/// K key on the main keyboard.
K,
/// L key on the main keyboard.
L,
/// M key on the main keyboard.
M,
/// N key on the main keyboard.
N,
/// O key on the main keyboard.
O,
/// P key on the main keyboard.
P,
/// Q key on the main keyboard.
Q,
/// R key on the main keyboard.
R,
/// S key on the main keyboard.
S,
/// T key on the main keyboard.
T,
/// U key on the main keyboard.
U,
/// V key on the main keyboard.
V,
/// W key on the main keyboard.
W,
/// X key on the main keyboard.
X,
/// Y key on the main keyboard.
Y,
/// Z key on the main keyboard.
Z,
/// 0 key on the main keyboard.
Digit0,
/// 1 key on the main keyboard.
Digit1,
/// 2 key on the main keyboard.
Digit2,
/// 3 key on the main keyboard.
Digit3,
/// 4 key on the main keyboard.
Digit4,
/// 5 key on the main keyboard.
Digit5,
/// 6 key on the main keyboard.
Digit6,
/// 7 key on the main keyboard.
Digit7,
/// 8 key on the main keyboard.
Digit8,
/// 9 key on the main keyboard.
Digit9,
/// Backquote key on the main keyboard: `
Backquote,
/// Minus key on the main keyboard: -
Minus,
/// Equal key on the main keyboard: =
Equal,
/// BracketLeft key on the main keyboard: [
BracketLeft,
/// BracketRight key on the main keyboard: ]
BracketRight,
/// Backslash key on the main keyboard: \
Backslash,
/// Semicolon key on the main keyboard: ;
Semicolon,
/// Quote key on the main keyboard: '
Quote,
/// Comma key on the main keyboard: ,
Comma,
/// Period key on the main keyboard: .
Period,
/// Slash key on the main keyboard: /
Slash,
/// Left arrow key
Left,
/// Up arrow key
Up,
/// Right arrow key
Right,
/// Down arrow key
Down,
/// PAGE UP key
PageUp,
/// PAGE DOWN key
PageDown,
/// END key
End,
/// HOME key
Home,
/// TAB key
Tab,
/// ENTER key, also known as RETURN key
/// This does not distinguish between the main Enter key and the numeric keypad Enter key.
Enter,
/// ESCAPE key
Escape,
/// SPACE key
Space,
/// BACKSPACE key
Backspace,
/// DELETE key
Delete,
// Pause, not supported yet
// CapsLock, not supported yet
/// INSERT key
Insert,
// The following keys are not supported yet:
// Numpad0,
// Numpad1,
// Numpad2,
// Numpad3,
// Numpad4,
// Numpad5,
// Numpad6,
// Numpad7,
// Numpad8,
// Numpad9,
// NumpadMultiply,
// NumpadAdd,
// NumpadComma,
// NumpadSubtract,
// NumpadDecimal,
// NumpadDivide,
}
impl ScanCode {
/// Parse a scan code from a string.
pub fn parse(source: &str) -> Option<Self> {
match source {
"[f1]" => Some(Self::F1),
"[f2]" => Some(Self::F2),
"[f3]" => Some(Self::F3),
"[f4]" => Some(Self::F4),
"[f5]" => Some(Self::F5),
"[f6]" => Some(Self::F6),
"[f7]" => Some(Self::F7),
"[f8]" => Some(Self::F8),
"[f9]" => Some(Self::F9),
"[f10]" => Some(Self::F10),
"[f11]" => Some(Self::F11),
"[f12]" => Some(Self::F12),
"[f13]" => Some(Self::F13),
"[f14]" => Some(Self::F14),
"[f15]" => Some(Self::F15),
"[f16]" => Some(Self::F16),
"[f17]" => Some(Self::F17),
"[f18]" => Some(Self::F18),
"[f19]" => Some(Self::F19),
"[f20]" => Some(Self::F20),
"[f21]" => Some(Self::F21),
"[f22]" => Some(Self::F22),
"[f23]" => Some(Self::F23),
"[f24]" => Some(Self::F24),
"[a]" | "[keya]" => Some(Self::A),
"[b]" | "[keyb]" => Some(Self::B),
"[c]" | "[keyc]" => Some(Self::C),
"[d]" | "[keyd]" => Some(Self::D),
"[e]" | "[keye]" => Some(Self::E),
"[f]" | "[keyf]" => Some(Self::F),
"[g]" | "[keyg]" => Some(Self::G),
"[h]" | "[keyh]" => Some(Self::H),
"[i]" | "[keyi]" => Some(Self::I),
"[j]" | "[keyj]" => Some(Self::J),
"[k]" | "[keyk]" => Some(Self::K),
"[l]" | "[keyl]" => Some(Self::L),
"[m]" | "[keym]" => Some(Self::M),
"[n]" | "[keyn]" => Some(Self::N),
"[o]" | "[keyo]" => Some(Self::O),
"[p]" | "[keyp]" => Some(Self::P),
"[q]" | "[keyq]" => Some(Self::Q),
"[r]" | "[keyr]" => Some(Self::R),
"[s]" | "[keys]" => Some(Self::S),
"[t]" | "[keyt]" => Some(Self::T),
"[u]" | "[keyu]" => Some(Self::U),
"[v]" | "[keyv]" => Some(Self::V),
"[w]" | "[keyw]" => Some(Self::W),
"[x]" | "[keyx]" => Some(Self::X),
"[y]" | "[keyy]" => Some(Self::Y),
"[z]" | "[keyz]" => Some(Self::Z),
"[0]" | "[digit0]" => Some(Self::Digit0),
"[1]" | "[digit1]" => Some(Self::Digit1),
"[2]" | "[digit2]" => Some(Self::Digit2),
"[3]" | "[digit3]" => Some(Self::Digit3),
"[4]" | "[digit4]" => Some(Self::Digit4),
"[5]" | "[digit5]" => Some(Self::Digit5),
"[6]" | "[digit6]" => Some(Self::Digit6),
"[7]" | "[digit7]" => Some(Self::Digit7),
"[8]" | "[digit8]" => Some(Self::Digit8),
"[9]" | "[digit9]" => Some(Self::Digit9),
"[backquote]" => Some(Self::Backquote),
"[minus]" => Some(Self::Minus),
"[equal]" => Some(Self::Equal),
"[bracketleft]" => Some(Self::BracketLeft),
"[bracketright]" => Some(Self::BracketRight),
"[backslash]" => Some(Self::Backslash),
"[semicolon]" => Some(Self::Semicolon),
"[quote]" => Some(Self::Quote),
"[comma]" => Some(Self::Comma),
"[period]" => Some(Self::Period),
"[slash]" => Some(Self::Slash),
"[left]" | "[arrowleft]" => Some(Self::Left),
"[up]" | "[arrowup]" => Some(Self::Up),
"[right]" | "[arrowright]" => Some(Self::Right),
"[down]" | "[arrowdown]" => Some(Self::Down),
"[pageup]" => Some(Self::PageUp),
"[pagedown]" => Some(Self::PageDown),
"[end]" => Some(Self::End),
"[home]" => Some(Self::Home),
"[tab]" => Some(Self::Tab),
"[enter]" => Some(Self::Enter),
"[escape]" => Some(Self::Escape),
"[space]" => Some(Self::Space),
"[backspace]" => Some(Self::Backspace),
"[delete]" => Some(Self::Delete),
// "[pause]" => Some(Self::Pause),
// "[capslock]" => Some(Self::CapsLock),
"[insert]" => Some(Self::Insert),
// "[numpad0]" => Some(Self::Numpad0),
// "[numpad1]" => Some(Self::Numpad1),
// "[numpad2]" => Some(Self::Numpad2),
// "[numpad3]" => Some(Self::Numpad3),
// "[numpad4]" => Some(Self::Numpad4),
// "[numpad5]" => Some(Self::Numpad5),
// "[numpad6]" => Some(Self::Numpad6),
// "[numpad7]" => Some(Self::Numpad7),
// "[numpad8]" => Some(Self::Numpad8),
// "[numpad9]" => Some(Self::Numpad9),
// "[numpadmultiply]" => Some(Self::NumpadMultiply),
// "[numpadadd]" => Some(Self::NumpadAdd),
// "[numpadcomma]" => Some(Self::NumpadComma),
// "[numpadsubtract]" => Some(Self::NumpadSubtract),
// "[numpaddecimal]" => Some(Self::NumpadDecimal),
// "[numpaddivide]" => Some(Self::NumpadDivide),
_ => None,
}
}
/// Convert the scan code to its key face for immutable keys.
pub fn try_to_key(&self) -> Option<String> {
Some(
match self {
ScanCode::F1 => "f1",
ScanCode::F2 => "f2",
ScanCode::F3 => "f3",
ScanCode::F4 => "f4",
ScanCode::F5 => "f5",
ScanCode::F6 => "f6",
ScanCode::F7 => "f7",
ScanCode::F8 => "f8",
ScanCode::F9 => "f9",
ScanCode::F10 => "f10",
ScanCode::F11 => "f11",
ScanCode::F12 => "f12",
ScanCode::F13 => "f13",
ScanCode::F14 => "f14",
ScanCode::F15 => "f15",
ScanCode::F16 => "f16",
ScanCode::F17 => "f17",
ScanCode::F18 => "f18",
ScanCode::F19 => "f19",
ScanCode::F20 => "f20",
ScanCode::F21 => "f21",
ScanCode::F22 => "f22",
ScanCode::F23 => "f23",
ScanCode::F24 => "f24",
ScanCode::Left => "left",
ScanCode::Up => "up",
ScanCode::Right => "right",
ScanCode::Down => "down",
ScanCode::PageUp => "pageup",
ScanCode::PageDown => "pagedown",
ScanCode::End => "end",
ScanCode::Home => "home",
ScanCode::Tab => "tab",
ScanCode::Enter => "enter",
ScanCode::Escape => "escape",
ScanCode::Space => "space",
ScanCode::Backspace => "backspace",
ScanCode::Delete => "delete",
ScanCode::Insert => "insert",
_ => return None,
}
.to_string(),
)
}
/// This function is used to convert the scan code to its key face on US keyboard layout.
/// Only used for tests.
pub fn to_key(&self, shift: bool) -> &str {
match self {
ScanCode::F1 => "f1",
ScanCode::F2 => "f2",
ScanCode::F3 => "f3",
ScanCode::F4 => "f4",
ScanCode::F5 => "f5",
ScanCode::F6 => "f6",
ScanCode::F7 => "f7",
ScanCode::F8 => "f8",
ScanCode::F9 => "f9",
ScanCode::F10 => "f10",
ScanCode::F11 => "f11",
ScanCode::F12 => "f12",
ScanCode::F13 => "f13",
ScanCode::F14 => "f14",
ScanCode::F15 => "f15",
ScanCode::F16 => "f16",
ScanCode::F17 => "f17",
ScanCode::F18 => "f18",
ScanCode::F19 => "f19",
ScanCode::F20 => "f20",
ScanCode::F21 => "f21",
ScanCode::F22 => "f22",
ScanCode::F23 => "f23",
ScanCode::F24 => "f24",
ScanCode::A => "a",
ScanCode::B => "b",
ScanCode::C => "c",
ScanCode::D => "d",
ScanCode::E => "e",
ScanCode::F => "f",
ScanCode::G => "g",
ScanCode::H => "h",
ScanCode::I => "i",
ScanCode::J => "j",
ScanCode::K => "k",
ScanCode::L => "l",
ScanCode::M => "m",
ScanCode::N => "n",
ScanCode::O => "o",
ScanCode::P => "p",
ScanCode::Q => "q",
ScanCode::R => "r",
ScanCode::S => "s",
ScanCode::T => "t",
ScanCode::U => "u",
ScanCode::V => "v",
ScanCode::W => "w",
ScanCode::X => "x",
ScanCode::Y => "y",
ScanCode::Z => "z",
ScanCode::Digit0 => {
if shift {
")"
} else {
"0"
}
}
ScanCode::Digit1 => {
if shift {
"!"
} else {
"1"
}
}
ScanCode::Digit2 => {
if shift {
"@"
} else {
"2"
}
}
ScanCode::Digit3 => {
if shift {
"#"
} else {
"3"
}
}
ScanCode::Digit4 => {
if shift {
"$"
} else {
"4"
}
}
ScanCode::Digit5 => {
if shift {
"%"
} else {
"5"
}
}
ScanCode::Digit6 => {
if shift {
"^"
} else {
"6"
}
}
ScanCode::Digit7 => {
if shift {
"&"
} else {
"7"
}
}
ScanCode::Digit8 => {
if shift {
"*"
} else {
"8"
}
}
ScanCode::Digit9 => {
if shift {
"("
} else {
"9"
}
}
ScanCode::Backquote => {
if shift {
"~"
} else {
"`"
}
}
ScanCode::Minus => {
if shift {
"_"
} else {
"-"
}
}
ScanCode::Equal => {
if shift {
"+"
} else {
"="
}
}
ScanCode::BracketLeft => {
if shift {
"{"
} else {
"["
}
}
ScanCode::BracketRight => {
if shift {
"}"
} else {
"]"
}
}
ScanCode::Backslash => {
if shift {
"|"
} else {
"\\"
}
}
ScanCode::Semicolon => {
if shift {
":"
} else {
";"
}
}
ScanCode::Quote => {
if shift {
"\""
} else {
"'"
}
}
ScanCode::Comma => {
if shift {
"<"
} else {
","
}
}
ScanCode::Period => {
if shift {
">"
} else {
"."
}
}
ScanCode::Slash => {
if shift {
"?"
} else {
"/"
}
}
ScanCode::Left => "left",
ScanCode::Up => "up",
ScanCode::Right => "right",
ScanCode::Down => "down",
ScanCode::PageUp => "pageup",
ScanCode::PageDown => "pagedown",
ScanCode::End => "end",
ScanCode::Home => "home",
ScanCode::Tab => "tab",
ScanCode::Enter => "enter",
ScanCode::Escape => "escape",
ScanCode::Space => "space",
ScanCode::Backspace => "backspace",
ScanCode::Delete => "delete",
ScanCode::Insert => "insert",
}
}
}

View File

@@ -1,13 +1,9 @@
use anyhow::Context;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
error::Error,
fmt::{Display, Write},
};
use util::ResultExt;
use crate::{PlatformKeyboardMapper, ScanCode};
/// A keystroke and associated metadata generated by the platform
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
@@ -97,10 +93,7 @@ impl Keystroke {
/// key_char syntax is only used for generating test events,
/// secondary means "cmd" on macOS and "ctrl" on other platforms
/// when matching a key with an key_char set will be matched without it.
pub fn parse(
source: &str,
keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> std::result::Result<Self, InvalidKeystrokeError> {
pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
let mut modifiers = Modifiers::none();
let mut key = None;
let mut key_char = None;
@@ -191,24 +184,9 @@ impl Keystroke {
}
});
// Create error once for reuse
let error = || InvalidKeystrokeError {
let key = key.ok_or_else(|| InvalidKeystrokeError {
keystroke: source.to_owned(),
};
let key = {
let key = key.ok_or_else(error)?;
if key.starts_with('[') && key.ends_with(']') {
let scan_code = ScanCode::parse(&key).ok_or_else(error)?;
keyboard_mapper
.scan_code_to_key(scan_code, &mut modifiers)
.context("Failed to convert scan code to key")
.log_err()
.ok_or_else(error)?
} else {
key
}
};
})?;
Ok(Keystroke {
modifiers,

View File

@@ -1,20 +1,4 @@
#[cfg(any(feature = "wayland", feature = "x11"))]
use std::sync::LazyLock;
#[cfg(any(feature = "wayland", feature = "x11"))]
use collections::HashMap;
#[cfg(any(feature = "wayland", feature = "x11"))]
use x11rb::{protocol::xkb::ConnectionExt, xcb_ffi::XCBConnection};
#[cfg(any(feature = "wayland", feature = "x11"))]
use xkbcommon::xkb::{
Keycode,
x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION},
};
use crate::{Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper, ScanCode};
#[cfg(any(feature = "wayland", feature = "x11"))]
use crate::is_letter_key;
use crate::PlatformKeyboardLayout;
pub(crate) struct LinuxKeyboardLayout {
id: String,
@@ -35,257 +19,3 @@ impl LinuxKeyboardLayout {
Self { id }
}
}
#[cfg(any(feature = "wayland", feature = "x11"))]
pub(crate) struct LinuxKeyboardMapper {
code_to_key: HashMap<Keycode, String>,
code_to_shifted_key: HashMap<Keycode, String>,
}
#[cfg(any(feature = "wayland", feature = "x11"))]
impl PlatformKeyboardMapper for LinuxKeyboardMapper {
fn scan_code_to_key(
&self,
scan_code: ScanCode,
modifiers: &mut Modifiers,
) -> anyhow::Result<String> {
if let Some(key) = scan_code.try_to_key() {
return Ok(key);
}
let native_scan_code = get_scan_code(scan_code)
.map(Keycode::new)
.ok_or_else(|| anyhow::anyhow!("Unsupported scan code: {:?}", scan_code))?;
let key = self.code_to_key.get(&native_scan_code).ok_or_else(|| {
anyhow::anyhow!("Key not found for scan code: {:?}", native_scan_code)
})?;
if modifiers.shift && !is_letter_key(key) {
if let Some(key) = self.code_to_shifted_key.get(&native_scan_code) {
modifiers.shift = false;
return Ok(key.clone());
} else {
anyhow::bail!(
"Shifted key not found for scan code: {:?}",
native_scan_code
);
}
} else {
Ok(key.clone())
}
}
}
#[cfg(any(feature = "wayland", feature = "x11"))]
static XCB_CONNECTION: LazyLock<XCBConnection> =
LazyLock::new(|| XCBConnection::connect(None).unwrap().0);
#[cfg(any(feature = "wayland", feature = "x11"))]
impl LinuxKeyboardMapper {
pub(crate) fn new() -> Self {
let _ = XCB_CONNECTION
.xkb_use_extension(XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION)
.unwrap()
.reply()
.unwrap();
let xkb_context = xkbcommon::xkb::Context::new(xkbcommon::xkb::CONTEXT_NO_FLAGS);
let xkb_device_id = xkbcommon::xkb::x11::get_core_keyboard_device_id(&*XCB_CONNECTION);
let xkb_state = {
let xkb_keymap = xkbcommon::xkb::x11::keymap_new_from_device(
&xkb_context,
&*XCB_CONNECTION,
xkb_device_id,
xkbcommon::xkb::KEYMAP_COMPILE_NO_FLAGS,
);
xkbcommon::xkb::x11::state_new_from_device(&xkb_keymap, &*XCB_CONNECTION, xkb_device_id)
};
let mut code_to_key = HashMap::default();
let mut code_to_shifted_key = HashMap::default();
let keymap = xkb_state.get_keymap();
let mut shifted_state = xkbcommon::xkb::State::new(&keymap);
let shift_mod = keymap.mod_get_index(xkbcommon::xkb::MOD_NAME_SHIFT);
let shift_mask = 1 << shift_mod;
shifted_state.update_mask(shift_mask, 0, 0, 0, 0, 0);
for &scan_code in TYPEABLE_CODES {
let keycode = Keycode::new(scan_code);
let key = xkb_state.key_get_utf8(keycode);
if !is_letter_key(&key) {
let shifted_key = shifted_state.key_get_utf8(keycode);
code_to_shifted_key.insert(keycode, shifted_key);
}
code_to_key.insert(keycode, key);
}
Self {
code_to_key,
code_to_shifted_key,
}
}
}
// All typeable scan codes for the standard US keyboard layout, ANSI104
#[cfg(any(feature = "wayland", feature = "x11"))]
const TYPEABLE_CODES: &[u32] = &[
0x0026, // a
0x0038, // b
0x0036, // c
0x0028, // d
0x001a, // e
0x0029, // f
0x002a, // g
0x002b, // h
0x001f, // i
0x002c, // j
0x002d, // k
0x002e, // l
0x003a, // m
0x0039, // n
0x0020, // o
0x0021, // p
0x0018, // q
0x001b, // r
0x0027, // s
0x001c, // t
0x001e, // u
0x0037, // v
0x0019, // w
0x0035, // x
0x001d, // y
0x0034, // z
0x0013, // Digit 0
0x000a, // Digit 1
0x000b, // Digit 2
0x000c, // Digit 3
0x000d, // Digit 4
0x000e, // Digit 5
0x000f, // Digit 6
0x0010, // Digit 7
0x0011, // Digit 8
0x0012, // Digit 9
0x0031, // ` Backquote
0x0014, // - Minus
0x0015, // = Equal
0x0022, // [ Left bracket
0x0023, // ] Right bracket
0x0033, // \ Backslash
0x002f, // ; Semicolon
0x0030, // ' Quote
0x003b, // , Comma
0x003c, // . Period
0x003d, // / Slash
];
#[cfg(any(feature = "wayland", feature = "x11"))]
fn get_scan_code(scan_code: ScanCode) -> Option<u32> {
// https://github.com/microsoft/node-native-keymap/blob/main/deps/chromium/dom_code_data.inc
Some(match scan_code {
ScanCode::F1 => 0x0043,
ScanCode::F2 => 0x0044,
ScanCode::F3 => 0x0045,
ScanCode::F4 => 0x0046,
ScanCode::F5 => 0x0047,
ScanCode::F6 => 0x0048,
ScanCode::F7 => 0x0049,
ScanCode::F8 => 0x004a,
ScanCode::F9 => 0x004b,
ScanCode::F10 => 0x004c,
ScanCode::F11 => 0x005f,
ScanCode::F12 => 0x0060,
ScanCode::F13 => 0x00bf,
ScanCode::F14 => 0x00c0,
ScanCode::F15 => 0x00c1,
ScanCode::F16 => 0x00c2,
ScanCode::F17 => 0x00c3,
ScanCode::F18 => 0x00c4,
ScanCode::F19 => 0x00c5,
ScanCode::F20 => 0x00c6,
ScanCode::F21 => 0x00c7,
ScanCode::F22 => 0x00c8,
ScanCode::F23 => 0x00c9,
ScanCode::F24 => 0x00ca,
ScanCode::A => 0x0026,
ScanCode::B => 0x0038,
ScanCode::C => 0x0036,
ScanCode::D => 0x0028,
ScanCode::E => 0x001a,
ScanCode::F => 0x0029,
ScanCode::G => 0x002a,
ScanCode::H => 0x002b,
ScanCode::I => 0x001f,
ScanCode::J => 0x002c,
ScanCode::K => 0x002d,
ScanCode::L => 0x002e,
ScanCode::M => 0x003a,
ScanCode::N => 0x0039,
ScanCode::O => 0x0020,
ScanCode::P => 0x0021,
ScanCode::Q => 0x0018,
ScanCode::R => 0x001b,
ScanCode::S => 0x0027,
ScanCode::T => 0x001c,
ScanCode::U => 0x001e,
ScanCode::V => 0x0037,
ScanCode::W => 0x0019,
ScanCode::X => 0x0035,
ScanCode::Y => 0x001d,
ScanCode::Z => 0x0034,
ScanCode::Digit0 => 0x0013,
ScanCode::Digit1 => 0x000a,
ScanCode::Digit2 => 0x000b,
ScanCode::Digit3 => 0x000c,
ScanCode::Digit4 => 0x000d,
ScanCode::Digit5 => 0x000e,
ScanCode::Digit6 => 0x000f,
ScanCode::Digit7 => 0x0010,
ScanCode::Digit8 => 0x0011,
ScanCode::Digit9 => 0x0012,
ScanCode::Backquote => 0x0031,
ScanCode::Minus => 0x0014,
ScanCode::Equal => 0x0015,
ScanCode::BracketLeft => 0x0022,
ScanCode::BracketRight => 0x0023,
ScanCode::Backslash => 0x0033,
ScanCode::Semicolon => 0x002f,
ScanCode::Quote => 0x0030,
ScanCode::Comma => 0x003b,
ScanCode::Period => 0x003c,
ScanCode::Slash => 0x003d,
ScanCode::Left => 0x0071,
ScanCode::Up => 0x006f,
ScanCode::Right => 0x0072,
ScanCode::Down => 0x0074,
ScanCode::PageUp => 0x0070,
ScanCode::PageDown => 0x0075,
ScanCode::End => 0x0073,
ScanCode::Home => 0x006e,
ScanCode::Tab => 0x0017,
ScanCode::Enter => 0x0024,
ScanCode::Escape => 0x0009,
ScanCode::Space => 0x0041,
ScanCode::Backspace => 0x0016,
ScanCode::Delete => 0x0077,
ScanCode::Insert => 0x0076,
})
}
#[cfg(not(any(feature = "wayland", feature = "x11")))]
pub(crate) struct LinuxKeyboardMapper;
#[cfg(not(any(feature = "wayland", feature = "x11")))]
impl PlatformKeyboardMapper for LinuxKeyboardMapper {
fn scan_code_to_key(
&self,
_scan_code: ScanCode,
_modifiers: &mut Modifiers,
) -> anyhow::Result<String> {
Err(anyhow::anyhow!("LinuxKeyboardMapper not supported"))
}
}
#[cfg(not(any(feature = "wayland", feature = "x11")))]
impl LinuxKeyboardMapper {
pub(crate) fn new() -> Self {
Self
}
}

View File

@@ -25,9 +25,8 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper,
PlatformTextSystem, PlatformWindow, Point, Result, ScreenCaptureSource, Task, WindowAppearance,
WindowParams, px,
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow,
Point, Result, ScreenCaptureSource, Task, WindowAppearance, WindowParams, px,
};
#[cfg(any(feature = "wayland", feature = "x11"))]
@@ -139,10 +138,6 @@ impl<P: LinuxClient + 'static> Platform for P {
self.with_common(|common| common.text_system.clone())
}
fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper> {
Box::new(super::LinuxKeyboardMapper::new())
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
self.keyboard_layout()
}

View File

@@ -1,14 +1,21 @@
use crate::{
CMD_MOD, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NO_MOD, NavigationDirection,
OPTION_MOD, Pixels, PlatformInput, SHIFT_MOD, ScrollDelta, ScrollWheelEvent, TouchPhase,
always_use_command_layout, chars_for_modified_key, platform::mac::NSStringExt, point, px,
KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase,
platform::mac::{
LMGetKbdType, NSStringExt, TISCopyCurrentKeyboardLayoutInputSource,
TISGetInputSourceProperty, UCKeyTranslate, kTISPropertyUnicodeKeyLayoutData,
},
point, px,
};
use cocoa::{
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
base::{YES, id},
};
use std::borrow::Cow;
use core_foundation::data::{CFDataGetBytePtr, CFDataRef};
use core_graphics::event::CGKeyCode;
use objc::{msg_send, sel, sel_impl};
use std::{borrow::Cow, ffi::c_void};
const BACKSPACE_KEY: u16 = 0x7f;
const SPACE_KEY: u16 = b' ' as u16;
@@ -445,3 +452,80 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
}
}
}
fn always_use_command_layout() -> bool {
if chars_for_modified_key(0, NO_MOD).is_ascii() {
return false;
}
chars_for_modified_key(0, CMD_MOD).is_ascii()
}
const NO_MOD: u32 = 0;
const CMD_MOD: u32 = 1;
const SHIFT_MOD: u32 = 2;
const OPTION_MOD: u32 = 8;
fn chars_for_modified_key(code: CGKeyCode, modifiers: u32) -> String {
// Values from: https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h#L126
// shifted >> 8 for UCKeyTranslate
const CG_SPACE_KEY: u16 = 49;
// https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/Headers/UnicodeUtilities.h#L278
#[allow(non_upper_case_globals)]
const kUCKeyActionDown: u16 = 0;
#[allow(non_upper_case_globals)]
const kUCKeyTranslateNoDeadKeysMask: u32 = 0;
let keyboard_type = unsafe { LMGetKbdType() as u32 };
const BUFFER_SIZE: usize = 4;
let mut dead_key_state = 0;
let mut buffer: [u16; BUFFER_SIZE] = [0; BUFFER_SIZE];
let mut buffer_size: usize = 0;
let keyboard = unsafe { TISCopyCurrentKeyboardLayoutInputSource() };
if keyboard.is_null() {
return "".to_string();
}
let layout_data = unsafe {
TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData as *const c_void)
as CFDataRef
};
if layout_data.is_null() {
unsafe {
let _: () = msg_send![keyboard, release];
}
return "".to_string();
}
let keyboard_layout = unsafe { CFDataGetBytePtr(layout_data) };
unsafe {
UCKeyTranslate(
keyboard_layout as *const c_void,
code,
kUCKeyActionDown,
modifiers,
keyboard_type,
kUCKeyTranslateNoDeadKeysMask,
&mut dead_key_state,
BUFFER_SIZE,
&mut buffer_size as *mut usize,
&mut buffer as *mut u16,
);
if dead_key_state != 0 {
UCKeyTranslate(
keyboard_layout as *const c_void,
CG_SPACE_KEY,
kUCKeyActionDown,
modifiers,
keyboard_type,
kUCKeyTranslateNoDeadKeysMask,
&mut dead_key_state,
BUFFER_SIZE,
&mut buffer_size as *mut usize,
&mut buffer as *mut u16,
);
}
let _: () = msg_send![keyboard, release];
}
String::from_utf16(&buffer[..buffer_size]).unwrap_or_default()
}

View File

@@ -1,14 +1,8 @@
use std::ffi::{CStr, c_void};
use collections::HashMap;
use core_foundation::data::{CFDataGetBytePtr, CFDataRef};
use core_graphics::event::CGKeyCode;
use objc::{msg_send, runtime::Object, sel, sel_impl};
use crate::{
Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper, ScanCode, is_letter_key,
platform::mac::{LMGetKbdType, UCKeyTranslate, kTISPropertyUnicodeKeyLayoutData},
};
use crate::PlatformKeyboardLayout;
use super::{
TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, kTISPropertyInputSourceID,
@@ -53,300 +47,3 @@ impl MacKeyboardLayout {
}
}
}
pub(crate) struct MacKeyboardMapper {
code_to_key: HashMap<u16, String>,
code_to_shifted_key: HashMap<u16, String>,
}
impl MacKeyboardMapper {
pub(crate) fn new() -> Self {
let mut code_to_key = HashMap::default();
let mut code_to_shifted_key = HashMap::default();
let always_use_cmd_layout = always_use_command_layout();
for &scan_code in TYPEABLE_CODES.iter() {
let (key, shifted_key) = generate_key_pairs(scan_code, always_use_cmd_layout);
if !is_letter_key(&key) {
code_to_shifted_key.insert(scan_code, shifted_key);
}
code_to_key.insert(scan_code, key);
}
Self {
code_to_key,
code_to_shifted_key,
}
}
}
impl PlatformKeyboardMapper for MacKeyboardMapper {
fn scan_code_to_key(
&self,
scan_code: ScanCode,
modifiers: &mut Modifiers,
) -> anyhow::Result<String> {
if let Some(key) = scan_code.try_to_key() {
return Ok(key);
}
let native_scan_code = get_scan_code(scan_code)
.ok_or_else(|| anyhow::anyhow!("Unsupported scan code: {:?}", scan_code))?;
let key = self.code_to_key.get(&native_scan_code).ok_or_else(|| {
anyhow::anyhow!("Key not found for scan code: {:?}", native_scan_code)
})?;
if modifiers.shift && !is_letter_key(key) {
if let Some(key) = self.code_to_shifted_key.get(&native_scan_code) {
modifiers.shift = false;
return Ok(key.clone());
} else {
anyhow::bail!(
"Shifted key not found for scan code: {:?}",
native_scan_code
);
}
} else {
Ok(key.clone())
}
}
}
pub(crate) const NO_MOD: u32 = 0;
pub(crate) const CMD_MOD: u32 = 1;
pub(crate) const SHIFT_MOD: u32 = 2;
pub(crate) const OPTION_MOD: u32 = 8;
pub(crate) fn chars_for_modified_key(code: CGKeyCode, modifiers: u32) -> String {
// Values from: https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h#L126
// shifted >> 8 for UCKeyTranslate
const CG_SPACE_KEY: u16 = 49;
// https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/Headers/UnicodeUtilities.h#L278
#[allow(non_upper_case_globals)]
const kUCKeyActionDown: u16 = 0;
#[allow(non_upper_case_globals)]
const kUCKeyTranslateNoDeadKeysMask: u32 = 0;
let keyboard_type = unsafe { LMGetKbdType() as u32 };
const BUFFER_SIZE: usize = 4;
let mut dead_key_state = 0;
let mut buffer: [u16; BUFFER_SIZE] = [0; BUFFER_SIZE];
let mut buffer_size: usize = 0;
let keyboard = unsafe { TISCopyCurrentKeyboardLayoutInputSource() };
if keyboard.is_null() {
return "".to_string();
}
let layout_data = unsafe {
TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData as *const c_void)
as CFDataRef
};
if layout_data.is_null() {
unsafe {
let _: () = msg_send![keyboard, release];
}
return "".to_string();
}
let keyboard_layout = unsafe { CFDataGetBytePtr(layout_data) };
unsafe {
UCKeyTranslate(
keyboard_layout as *const c_void,
code,
kUCKeyActionDown,
modifiers,
keyboard_type,
kUCKeyTranslateNoDeadKeysMask,
&mut dead_key_state,
BUFFER_SIZE,
&mut buffer_size as *mut usize,
&mut buffer as *mut u16,
);
if dead_key_state != 0 {
UCKeyTranslate(
keyboard_layout as *const c_void,
CG_SPACE_KEY,
kUCKeyActionDown,
modifiers,
keyboard_type,
kUCKeyTranslateNoDeadKeysMask,
&mut dead_key_state,
BUFFER_SIZE,
&mut buffer_size as *mut usize,
&mut buffer as *mut u16,
);
}
let _: () = msg_send![keyboard, release];
}
String::from_utf16(&buffer[..buffer_size]).unwrap_or_default()
}
pub(crate) fn always_use_command_layout() -> bool {
if chars_for_modified_key(0, NO_MOD).is_ascii() {
return false;
}
chars_for_modified_key(0, CMD_MOD).is_ascii()
}
fn generate_key_pairs(scan_code: u16, always_use_cmd_layout: bool) -> (String, String) {
let mut chars_ignoring_modifiers = chars_for_modified_key(scan_code, NO_MOD);
let mut chars_with_shift = chars_for_modified_key(scan_code, SHIFT_MOD);
// Handle Dvorak+QWERTY / Russian / Armenian
if always_use_cmd_layout {
let chars_with_cmd = chars_for_modified_key(scan_code, CMD_MOD);
let chars_with_both = chars_for_modified_key(scan_code, CMD_MOD | SHIFT_MOD);
// We don't do this in the case that the shifted command key generates
// the same character as the unshifted command key (Norwegian, e.g.)
if chars_with_both != chars_with_cmd {
chars_with_shift = chars_with_both;
// Handle edge-case where cmd-shift-s reports cmd-s instead of
// cmd-shift-s (Ukrainian, etc.)
} else if chars_with_cmd.to_ascii_uppercase() != chars_with_cmd {
chars_with_shift = chars_with_cmd.to_ascii_uppercase();
}
chars_ignoring_modifiers = chars_with_cmd;
}
(chars_ignoring_modifiers, chars_with_shift)
}
// All typeable scan codes for the standard US keyboard layout, ANSI104
const TYPEABLE_CODES: &[u16] = &[
0x0000, // a
0x000b, // b
0x0008, // c
0x0002, // d
0x000e, // e
0x0003, // f
0x0005, // g
0x0004, // h
0x0022, // i
0x0026, // j
0x0028, // k
0x0025, // l
0x002e, // m
0x002d, // n
0x001f, // o
0x0023, // p
0x000c, // q
0x000f, // r
0x0001, // s
0x0011, // t
0x0020, // u
0x0009, // v
0x000d, // w
0x0007, // x
0x0010, // y
0x0006, // z
0x001d, // Digit 0
0x0012, // Digit 1
0x0013, // Digit 2
0x0014, // Digit 3
0x0015, // Digit 4
0x0017, // Digit 5
0x0016, // Digit 6
0x001a, // Digit 7
0x001c, // Digit 8
0x0019, // Digit 9
0x0032, // ` Tilde
0x001b, // - Minus
0x0018, // = Equal
0x0021, // [ Left bracket
0x001e, // ] Right bracket
0x002a, // \ Backslash
0x0029, // ; Semicolon
0x0027, // ' Quote
0x002b, // , Comma
0x002f, // . Period
0x002c, // / Slash
];
fn get_scan_code(scan_code: ScanCode) -> Option<u16> {
// https://github.com/microsoft/node-native-keymap/blob/main/deps/chromium/dom_code_data.inc
Some(match scan_code {
ScanCode::F1 => 0x007a,
ScanCode::F2 => 0x0078,
ScanCode::F3 => 0x0063,
ScanCode::F4 => 0x0076,
ScanCode::F5 => 0x0060,
ScanCode::F6 => 0x0061,
ScanCode::F7 => 0x0062,
ScanCode::F8 => 0x0064,
ScanCode::F9 => 0x0065,
ScanCode::F10 => 0x006d,
ScanCode::F11 => 0x0067,
ScanCode::F12 => 0x006f,
ScanCode::F13 => 0x0069,
ScanCode::F14 => 0x006b,
ScanCode::F15 => 0x0071,
ScanCode::F16 => 0x006a,
ScanCode::F17 => 0x0040,
ScanCode::F18 => 0x004f,
ScanCode::F19 => 0x0050,
ScanCode::F20 => 0x005a,
ScanCode::F21 | ScanCode::F22 | ScanCode::F23 | ScanCode::F24 => return None,
ScanCode::A => 0x0000,
ScanCode::B => 0x000b,
ScanCode::C => 0x0008,
ScanCode::D => 0x0002,
ScanCode::E => 0x000e,
ScanCode::F => 0x0003,
ScanCode::G => 0x0005,
ScanCode::H => 0x0004,
ScanCode::I => 0x0022,
ScanCode::J => 0x0026,
ScanCode::K => 0x0028,
ScanCode::L => 0x0025,
ScanCode::M => 0x002e,
ScanCode::N => 0x002d,
ScanCode::O => 0x001f,
ScanCode::P => 0x0023,
ScanCode::Q => 0x000c,
ScanCode::R => 0x000f,
ScanCode::S => 0x0001,
ScanCode::T => 0x0011,
ScanCode::U => 0x0020,
ScanCode::V => 0x0009,
ScanCode::W => 0x000d,
ScanCode::X => 0x0007,
ScanCode::Y => 0x0010,
ScanCode::Z => 0x0006,
ScanCode::Digit0 => 0x001d,
ScanCode::Digit1 => 0x0012,
ScanCode::Digit2 => 0x0013,
ScanCode::Digit3 => 0x0014,
ScanCode::Digit4 => 0x0015,
ScanCode::Digit5 => 0x0017,
ScanCode::Digit6 => 0x0016,
ScanCode::Digit7 => 0x001a,
ScanCode::Digit8 => 0x001c,
ScanCode::Digit9 => 0x0019,
ScanCode::Backquote => 0x0032,
ScanCode::Minus => 0x001b,
ScanCode::Equal => 0x0018,
ScanCode::BracketLeft => 0x0021,
ScanCode::BracketRight => 0x001e,
ScanCode::Backslash => 0x002a,
ScanCode::Semicolon => 0x0029,
ScanCode::Quote => 0x0027,
ScanCode::Comma => 0x002b,
ScanCode::Period => 0x002f,
ScanCode::Slash => 0x002c,
ScanCode::Left => 0x007b,
ScanCode::Up => 0x007e,
ScanCode::Right => 0x007c,
ScanCode::Down => 0x007d,
ScanCode::PageUp => 0x0074,
ScanCode::PageDown => 0x0079,
ScanCode::End => 0x0077,
ScanCode::Home => 0x0073,
ScanCode::Tab => 0x0030,
ScanCode::Enter => 0x0024,
ScanCode::Escape => 0x0035,
ScanCode::Space => 0x0031,
ScanCode::Backspace => 0x0033,
ScanCode::Delete => 0x0075,
ScanCode::Insert => 0x0072,
})
}

View File

@@ -7,10 +7,9 @@ use super::{
use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
MacDisplay, MacKeyboardMapper, MacWindow, Menu, MenuItem, PathPromptOptions, Platform,
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
PlatformWindow, Result, ScreenCaptureSource, SemanticVersion, Task, WindowAppearance,
WindowParams, hash,
MacDisplay, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, ScreenCaptureSource,
SemanticVersion, Task, WindowAppearance, WindowParams, hash,
};
use anyhow::{Context as _, anyhow};
use block::ConcreteBlock;
@@ -847,10 +846,6 @@ impl Platform for MacPlatform {
self.0.lock().validate_menu_command = Some(callback);
}
fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper> {
Box::new(MacKeyboardMapper::new())
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
Box::new(MacKeyboardLayout::new())
}

View File

@@ -1,9 +1,8 @@
use crate::{
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
PlatformKeyboardMapper, PlatformTextSystem, PromptButton, ScreenCaptureFrame,
ScreenCaptureSource, ScreenCaptureStream, Size, Task, TestDisplay, TestKeyboardMapper,
TestWindow, WindowAppearance, WindowParams, size,
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
Size, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
};
use anyhow::Result;
use collections::VecDeque;
@@ -224,10 +223,6 @@ impl Platform for TestPlatform {
self.text_system.clone()
}
fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper> {
Box::new(TestKeyboardMapper::new())
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
Box::new(TestKeyboardLayout)
}

View File

@@ -888,6 +888,32 @@ fn handle_hit_test_msg(
return None;
}
let mut lock = state_ptr.state.borrow_mut();
if let Some(mut callback) = lock.callbacks.hit_test_window_control.take() {
drop(lock);
let area = callback();
state_ptr
.state
.borrow_mut()
.callbacks
.hit_test_window_control = Some(callback);
if let Some(area) = area {
return match area {
WindowControlArea::Drag => Some(HTCAPTION as _),
WindowControlArea::Close => Some(HTCLOSE as _),
WindowControlArea::Max => Some(HTMAXBUTTON as _),
WindowControlArea::Min => Some(HTMINBUTTON as _),
};
}
} else {
drop(lock);
}
if !state_ptr.hide_title_bar {
// If the OS draws the title bar, we don't need to handle hit test messages.
return None;
}
// default handler for resize areas
let hit = unsafe { DefWindowProcW(handle, msg, wparam, lparam) };
if matches!(
@@ -922,25 +948,6 @@ fn handle_hit_test_msg(
return Some(HTTOP as _);
}
let mut lock = state_ptr.state.borrow_mut();
if let Some(mut callback) = lock.callbacks.hit_test_window_control.take() {
drop(lock);
let area = callback();
state_ptr
.state
.borrow_mut()
.callbacks
.hit_test_window_control = Some(callback);
if let Some(area) = area {
return match area {
WindowControlArea::Drag => Some(HTCAPTION as _),
WindowControlArea::Close => Some(HTCLOSE as _),
WindowControlArea::Max => Some(HTMAXBUTTON as _),
WindowControlArea::Min => Some(HTMINBUTTON as _),
};
}
}
Some(HTCLIENT as _)
}

View File

@@ -1,16 +1,16 @@
use anyhow::{Context, Result};
use anyhow::Result;
use windows::Win32::UI::{
Input::KeyboardAndMouse::{
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VSC_TO_VK, MapVirtualKeyW, ToUnicode,
VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1,
VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7,
VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, VIRTUAL_KEY, VK_0,
VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, VK_CONTROL, VK_MENU,
VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_102,
VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
},
WindowsAndMessaging::KL_NAMELENGTH,
};
use windows_core::HSTRING;
use crate::{Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper, ScanCode};
use crate::{Modifiers, PlatformKeyboardLayout};
pub(crate) struct WindowsKeyboardLayout {
id: String,
@@ -48,29 +48,6 @@ impl WindowsKeyboardLayout {
}
}
pub(crate) struct WindowsKeyboardMapper;
impl PlatformKeyboardMapper for WindowsKeyboardMapper {
fn scan_code_to_key(&self, scan_code: ScanCode, modifiers: &mut Modifiers) -> Result<String> {
if let Some(key) = scan_code.try_to_key() {
return Ok(key);
}
let (win_scan_code, vkey) = get_virtual_key_from_scan_code(scan_code)?;
get_keystroke_key(vkey, win_scan_code, modifiers).with_context(|| {
format!(
"Failed to get key from scan code: {:?}, vkey: {:?}",
scan_code, vkey
)
})
}
}
impl WindowsKeyboardMapper {
pub(crate) fn new() -> Self {
Self
}
}
pub(crate) fn get_keystroke_key(
vkey: VIRTUAL_KEY,
scan_code: u32,
@@ -105,15 +82,15 @@ fn need_to_convert_to_shifted_key(vkey: VIRTUAL_KEY) -> bool {
| VK_OEM_MINUS
| VK_OEM_PLUS
| VK_OEM_4
| VK_OEM_6
| VK_OEM_5
| VK_OEM_6
| VK_OEM_1
| VK_OEM_7
| VK_OEM_COMMA
| VK_OEM_PERIOD
| VK_OEM_2
| VK_OEM_102
| VK_OEM_8 // Same as VK_OEM_2
| VK_OEM_8
| VK_ABNT_C1
| VK_0
| VK_1
@@ -161,66 +138,3 @@ pub(crate) fn generate_key_char(
}
None
}
fn get_virtual_key_from_scan_code(gpui_scan_code: ScanCode) -> Result<(u32, VIRTUAL_KEY)> {
// https://github.com/microsoft/node-native-keymap/blob/main/deps/chromium/dom_code_data.inc
let scan_code = match gpui_scan_code {
ScanCode::A => 0x001e,
ScanCode::B => 0x0030,
ScanCode::C => 0x002e,
ScanCode::D => 0x0020,
ScanCode::E => 0x0012,
ScanCode::F => 0x0021,
ScanCode::G => 0x0022,
ScanCode::H => 0x0023,
ScanCode::I => 0x0017,
ScanCode::J => 0x0024,
ScanCode::K => 0x0025,
ScanCode::L => 0x0026,
ScanCode::M => 0x0032,
ScanCode::N => 0x0031,
ScanCode::O => 0x0018,
ScanCode::P => 0x0019,
ScanCode::Q => 0x0010,
ScanCode::R => 0x0013,
ScanCode::S => 0x001f,
ScanCode::T => 0x0014,
ScanCode::U => 0x0016,
ScanCode::V => 0x002f,
ScanCode::W => 0x0011,
ScanCode::X => 0x002d,
ScanCode::Y => 0x0015,
ScanCode::Z => 0x002c,
ScanCode::Digit0 => 0x000b,
ScanCode::Digit1 => 0x0002,
ScanCode::Digit2 => 0x0003,
ScanCode::Digit3 => 0x0004,
ScanCode::Digit4 => 0x0005,
ScanCode::Digit5 => 0x0006,
ScanCode::Digit6 => 0x0007,
ScanCode::Digit7 => 0x0008,
ScanCode::Digit8 => 0x0009,
ScanCode::Digit9 => 0x000a,
ScanCode::Backquote => 0x0029,
ScanCode::Minus => 0x000c,
ScanCode::Equal => 0x000d,
ScanCode::BracketLeft => 0x001a,
ScanCode::BracketRight => 0x001b,
ScanCode::Backslash => 0x002b,
ScanCode::Semicolon => 0x0027,
ScanCode::Quote => 0x0028,
ScanCode::Comma => 0x0033,
ScanCode::Period => 0x0034,
ScanCode::Slash => 0x0035,
_ => anyhow::bail!("Unsupported scan code: {:?}", gpui_scan_code),
};
let virtual_key = unsafe { MapVirtualKeyW(scan_code, MAPVK_VSC_TO_VK) };
if virtual_key == 0 {
anyhow::bail!(
"Failed to get virtual key from scan code: {:?}, {}",
gpui_scan_code,
scan_code
);
}
Ok((scan_code, VIRTUAL_KEY(virtual_key as u16)))
}

View File

@@ -81,9 +81,9 @@ impl WindowsPlatformState {
}
impl WindowsPlatform {
pub(crate) fn new() -> Self {
pub(crate) fn new() -> Result<Self> {
unsafe {
OleInitialize(None).expect("unable to initialize Windows OLE");
OleInitialize(None).context("unable to initialize Windows OLE")?;
}
let (main_sender, main_receiver) = flume::unbounded::<Runnable>();
let main_thread_id_win32 = unsafe { GetCurrentThreadId() };
@@ -97,19 +97,19 @@ impl WindowsPlatform {
let foreground_executor = ForegroundExecutor::new(dispatcher);
let bitmap_factory = ManuallyDrop::new(unsafe {
CoCreateInstance(&CLSID_WICImagingFactory, None, CLSCTX_INPROC_SERVER)
.expect("Error creating bitmap factory.")
.context("Error creating bitmap factory.")?
});
let text_system = Arc::new(
DirectWriteTextSystem::new(&bitmap_factory)
.expect("Error creating DirectWriteTextSystem"),
.context("Error creating DirectWriteTextSystem")?,
);
let icon = load_icon().unwrap_or_default();
let state = RefCell::new(WindowsPlatformState::new());
let raw_window_handles = RwLock::new(SmallVec::new());
let gpu_context = BladeContext::new().expect("Unable to init GPU context");
let windows_version = WindowsVersion::new().expect("Error retrieve windows version");
let gpu_context = BladeContext::new().context("Unable to init GPU context")?;
let windows_version = WindowsVersion::new().context("Error retrieve windows version")?;
Self {
Ok(Self {
state,
raw_window_handles,
gpu_context,
@@ -122,7 +122,7 @@ impl WindowsPlatform {
bitmap_factory,
validation_number,
main_thread_id_win32,
}
})
}
fn redraw_all(&self) {
@@ -310,10 +310,6 @@ impl Platform for WindowsPlatform {
self.text_system.clone()
}
fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper> {
Box::new(WindowsKeyboardMapper::new())
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
Box::new(
WindowsKeyboardLayout::new()

View File

@@ -8,7 +8,7 @@ use windows::{
},
Wdk::System::SystemServices::RtlGetVersion,
Win32::{Foundation::*, Graphics::Dwm::*, UI::WindowsAndMessaging::*},
core::BOOL,
core::{BOOL, HSTRING},
};
use crate::*;
@@ -186,3 +186,14 @@ pub(crate) fn system_appearance() -> Result<WindowAppearance> {
fn is_color_light(color: &Color) -> bool {
((5 * color.G as u32) + (2 * color.R as u32) + color.B as u32) > (8 * 128)
}
pub(crate) fn show_error(title: &str, content: String) {
let _ = unsafe {
MessageBoxW(
None,
&HSTRING::from(content),
&HSTRING::from(title),
MB_ICONERROR | MB_SYSTEMMODAL,
)
};
}

View File

@@ -1258,7 +1258,7 @@ mod windows_renderer {
use std::num::NonZeroIsize;
use windows::Win32::{Foundation::HWND, UI::WindowsAndMessaging::GWLP_HINSTANCE};
use crate::get_window_long;
use crate::{get_window_long, show_error};
pub(super) fn init(
context: &BladeContext,
@@ -1270,7 +1270,12 @@ mod windows_renderer {
size: Default::default(),
transparent,
};
BladeRenderer::new(context, &raw, config)
BladeRenderer::new(context, &raw, config).inspect_err(|err| {
show_error(
"Error: Zed failed to initialize BladeRenderer",
err.to_string(),
)
})
}
struct RawWindow {

View File

@@ -60,6 +60,7 @@ pub enum IconName {
ChevronUpDown,
Circle,
CircleOff,
CircleHelp,
Clipboard,
Close,
Cloud,

View File

@@ -39,7 +39,7 @@ use util::{ResultExt, maybe, post_inc};
#[derive(
Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
pub struct LanguageName(SharedString);
pub struct LanguageName(pub SharedString);
impl LanguageName {
pub fn new(s: &str) -> Self {
@@ -1000,6 +1000,7 @@ impl LanguageRegistry {
txs.push(tx);
}
AvailableGrammar::Unloaded(wasm_path) => {
log::trace!("start loading grammar {name:?}");
let this = self.clone();
let wasm_path = wasm_path.clone();
*grammar = AvailableGrammar::Loading(wasm_path.clone(), vec![tx]);
@@ -1025,6 +1026,7 @@ impl LanguageRegistry {
Err(error) => AvailableGrammar::LoadFailed(error.clone()),
};
log::trace!("finish loading grammar {name:?}");
let old_value = this.state.write().grammars.insert(name, value);
if let Some(AvailableGrammar::Loading(_, txs)) = old_value {
for tx in txs {

View File

@@ -7,6 +7,7 @@ use crate::{
use anyhow::Context as _;
use collections::HashMap;
use futures::FutureExt;
use gpui::SharedString;
use std::{
borrow::Cow,
cmp::{self, Ordering, Reverse},
@@ -183,6 +184,13 @@ enum ParseStepLanguage {
}
impl ParseStepLanguage {
fn name(&self) -> SharedString {
match self {
ParseStepLanguage::Loaded { language } => language.name().0,
ParseStepLanguage::Pending { name } => name.into(),
}
}
fn id(&self) -> Option<LanguageId> {
match self {
ParseStepLanguage::Loaded { language } => Some(language.id),
@@ -415,7 +423,9 @@ impl SyntaxSnapshot {
.and_then(|language| language.ok())
.is_some()
{
resolved_injection_ranges.push(layer.range.to_offset(text));
let range = layer.range.to_offset(text);
log::trace!("reparse range {range:?} for language {language_name:?}");
resolved_injection_ranges.push(range);
}
cursor.next(text);
@@ -442,7 +452,10 @@ impl SyntaxSnapshot {
invalidated_ranges: Vec<Range<usize>>,
registry: Option<&Arc<LanguageRegistry>>,
) {
log::trace!("reparse. invalidated ranges:{:?}", invalidated_ranges);
log::trace!(
"reparse. invalidated ranges:{:?}",
LogOffsetRanges(&invalidated_ranges, text),
);
let max_depth = self.layers.summary().max_depth;
let mut cursor = self.layers.cursor::<SyntaxLayerSummary>(text);
@@ -470,6 +483,13 @@ impl SyntaxSnapshot {
loop {
let step = queue.pop();
let position = if let Some(step) = &step {
log::trace!(
"parse step depth:{}, range:{:?}, language:{} ({:?})",
step.depth,
LogAnchorRange(&step.range, text),
step.language.name(),
step.language.id(),
);
SyntaxLayerPosition {
depth: step.depth,
range: step.range.clone(),
@@ -568,13 +588,13 @@ impl SyntaxSnapshot {
.to_ts_point();
}
if let Some((SyntaxLayerContent::Parsed { tree: old_tree, .. }, layer_start)) =
old_layer.map(|layer| (&layer.content, layer.range.start))
if let Some((SyntaxLayerContent::Parsed { tree: old_tree, .. }, layer_range)) =
old_layer.map(|layer| (&layer.content, layer.range.clone()))
{
log::trace!(
"existing layer. language:{}, start:{:?}, ranges:{:?}",
"existing layer. language:{}, range:{:?}, included_ranges:{:?}",
language.name(),
LogPoint(layer_start.to_point(text)),
LogAnchorRange(&layer_range, text),
LogIncludedRanges(&old_tree.included_ranges())
);
@@ -613,7 +633,7 @@ impl SyntaxSnapshot {
}
log::trace!(
"update layer. language:{}, start:{:?}, included_ranges:{:?}",
"update layer. language:{}, range:{:?}, included_ranges:{:?}",
language.name(),
LogAnchorRange(&step.range, text),
LogIncludedRanges(&included_ranges),
@@ -761,28 +781,36 @@ impl SyntaxSnapshot {
#[cfg(debug_assertions)]
fn check_invariants(&self, text: &BufferSnapshot) {
let mut max_depth = 0;
let mut prev_range: Option<Range<Anchor>> = None;
let mut prev_layer: Option<(Range<Anchor>, Option<LanguageId>)> = None;
for layer in self.layers.iter() {
match Ord::cmp(&layer.depth, &max_depth) {
Ordering::Less => {
panic!("layers out of order")
}
Ordering::Equal => {
if let Some(prev_range) = prev_range {
if let Some((prev_range, prev_language_id)) = prev_layer {
match layer.range.start.cmp(&prev_range.start, text) {
Ordering::Less => panic!("layers out of order"),
Ordering::Equal => {
assert!(layer.range.end.cmp(&prev_range.end, text).is_ge())
}
Ordering::Equal => match layer.range.end.cmp(&prev_range.end, text) {
Ordering::Less => panic!("layers out of order"),
Ordering::Equal => {
if layer.content.language_id() < prev_language_id {
panic!("layers out of order")
}
}
Ordering::Greater => {}
},
Ordering::Greater => {}
}
}
prev_layer = Some((layer.range.clone(), layer.content.language_id()));
}
Ordering::Greater => {
prev_layer = None;
}
Ordering::Greater => {}
}
max_depth = layer.depth;
prev_range = Some(layer.range.clone());
}
}
@@ -1642,7 +1670,7 @@ impl Ord for ParseStep {
Ord::cmp(&other.depth, &self.depth)
.then_with(|| Ord::cmp(&range_b.start, &range_a.start))
.then_with(|| Ord::cmp(&range_a.end, &range_b.end))
.then_with(|| self.language.id().cmp(&other.language.id()))
.then_with(|| other.language.id().cmp(&self.language.id()))
}
}
@@ -1888,6 +1916,7 @@ impl ToTreeSitterPoint for Point {
struct LogIncludedRanges<'a>(&'a [tree_sitter::Range]);
struct LogPoint(Point);
struct LogAnchorRange<'a>(&'a Range<Anchor>, &'a text::BufferSnapshot);
struct LogOffsetRanges<'a>(&'a [Range<usize>], &'a text::BufferSnapshot);
struct LogChangedRegions<'a>(&'a ChangeRegionSet, &'a text::BufferSnapshot);
impl fmt::Debug for LogIncludedRanges<'_> {
@@ -1909,6 +1938,16 @@ impl fmt::Debug for LogAnchorRange<'_> {
}
}
impl fmt::Debug for LogOffsetRanges<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list()
.entries(self.0.iter().map(|range| {
LogPoint(range.start.to_point(self.1))..LogPoint(range.end.to_point(self.1))
}))
.finish()
}
}
impl fmt::Debug for LogChangedRegions<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list()

View File

@@ -788,15 +788,99 @@ fn test_empty_combined_injections_inside_injections(cx: &mut App) {
"(template...",
// Markdown inline content
"(inline)",
// HTML within the ERB
"(document (text))",
// The ruby syntax tree should be empty, since there are
// no interpolations in the ERB template.
"(program)",
// HTML within the ERB
"(document (text))",
],
);
}
#[gpui::test]
fn test_syntax_map_languages_loading_with_erb(cx: &mut App) {
let text = r#"
<body>
<% if @one %>
<div class=one>
<% else %>
<div class=two>
<% end %>
</div>
</body>
"#
.unindent();
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), text);
let mut syntax_map = SyntaxMap::new(&buffer);
syntax_map.set_language_registry(registry.clone());
let language = Arc::new(erb_lang());
log::info!("parsing");
registry.add(language.clone());
syntax_map.reparse(language.clone(), &buffer);
log::info!("loading html");
registry.add(Arc::new(html_lang()));
syntax_map.reparse(language.clone(), &buffer);
log::info!("loading ruby");
registry.add(Arc::new(ruby_lang()));
syntax_map.reparse(language.clone(), &buffer);
assert_capture_ranges(
&syntax_map,
&buffer,
&["tag", "ivar"],
"
<«body»>
<% if «@one» %>
<«div» class=one>
<% else %>
<«div» class=two>
<% end %>
</«div»>
</«body»>
",
);
let text = r#"
<body>
<% if @one«_hundred» %>
<div class=one>
<% else %>
<div class=two>
<% end %>
</div>
</body>
"#
.unindent();
log::info!("editing");
buffer.edit_via_marked_text(&text);
syntax_map.interpolate(&buffer);
syntax_map.reparse(language.clone(), &buffer);
assert_capture_ranges(
&syntax_map,
&buffer,
&["tag", "ivar"],
"
<«body»>
<% if «@one_hundred» %>
<«div» class=one>
<% else %>
<«div» class=two>
<% end %>
</«div»>
</«body»>
",
);
}
#[gpui::test(iterations = 50)]
fn test_random_syntax_map_edits_rust_macros(rng: StdRng, cx: &mut App) {
let text = r#"

View File

@@ -1,6 +1,6 @@
use std::{ops::Range, path::PathBuf, sync::Arc};
use crate::{LanguageToolchainStore, Location, Runnable};
use crate::{File, LanguageToolchainStore, Location, Runnable};
use anyhow::Result;
use collections::HashMap;
@@ -39,10 +39,11 @@ pub trait ContextProvider: Send + Sync {
/// Provides all tasks, associated with the current language.
fn associated_tasks(
&self,
_: Option<Arc<dyn crate::File>>,
_cx: &App,
) -> Option<TaskTemplates> {
None
_: Arc<dyn Fs>,
_: Option<Arc<dyn File>>,
_: &App,
) -> Task<Option<TaskTemplates>> {
Task::ready(None)
}
/// A language server name, that can return tasks using LSP (ext) for this language.

View File

@@ -43,6 +43,8 @@ pub struct AvailableModel {
pub max_tokens: usize,
pub max_output_tokens: Option<u32>,
pub max_completion_tokens: Option<u32>,
pub supports_tools: Option<bool>,
pub supports_images: Option<bool>,
}
pub struct OpenRouterLanguageModelProvider {
@@ -227,7 +229,8 @@ impl LanguageModelProvider for OpenRouterLanguageModelProvider {
name: model.name.clone(),
display_name: model.display_name.clone(),
max_tokens: model.max_tokens,
supports_tools: Some(false),
supports_tools: model.supports_tools,
supports_images: model.supports_images,
});
}
@@ -345,7 +348,7 @@ impl LanguageModel for OpenRouterLanguageModel {
}
fn supports_images(&self) -> bool {
false
self.model.supports_images.unwrap_or(false)
}
fn count_tokens(
@@ -386,20 +389,26 @@ pub fn into_open_router(
max_output_tokens: Option<u32>,
) -> open_router::Request {
let mut messages = Vec::new();
for req_message in request.messages {
for content in req_message.content {
for message in request.messages {
for content in message.content {
match content {
MessageContent::Text(text) | MessageContent::Thinking { text, .. } => messages
.push(match req_message.role {
Role::User => open_router::RequestMessage::User { content: text },
Role::Assistant => open_router::RequestMessage::Assistant {
content: Some(text),
tool_calls: Vec::new(),
},
Role::System => open_router::RequestMessage::System { content: text },
}),
MessageContent::Text(text) | MessageContent::Thinking { text, .. } => {
add_message_content_part(
open_router::MessagePart::Text { text: text },
message.role,
&mut messages,
)
}
MessageContent::RedactedThinking(_) => {}
MessageContent::Image(_) => {}
MessageContent::Image(image) => {
add_message_content_part(
open_router::MessagePart::Image {
image_url: image.to_base64_url(),
},
message.role,
&mut messages,
);
}
MessageContent::ToolUse(tool_use) => {
let tool_call = open_router::ToolCall {
id: tool_use.id.to_string(),
@@ -425,16 +434,20 @@ pub fn into_open_router(
}
MessageContent::ToolResult(tool_result) => {
let content = match &tool_result.content {
LanguageModelToolResultContent::Text(text) => {
text.to_string()
LanguageModelToolResultContent::Text(text) => {
vec![open_router::MessagePart::Text {
text: text.to_string(),
}]
}
LanguageModelToolResultContent::Image(_) => {
"[Tool responded with an image, but Zed doesn't support these in Open AI models yet]".to_string()
LanguageModelToolResultContent::Image(image) => {
vec![open_router::MessagePart::Image {
image_url: image.to_base64_url(),
}]
}
};
messages.push(open_router::RequestMessage::Tool {
content: content,
content: content.into(),
tool_call_id: tool_result.tool_use_id.to_string(),
});
}
@@ -473,6 +486,42 @@ pub fn into_open_router(
}
}
fn add_message_content_part(
new_part: open_router::MessagePart,
role: Role,
messages: &mut Vec<open_router::RequestMessage>,
) {
match (role, messages.last_mut()) {
(Role::User, Some(open_router::RequestMessage::User { content }))
| (Role::System, Some(open_router::RequestMessage::System { content })) => {
content.push_part(new_part);
}
(
Role::Assistant,
Some(open_router::RequestMessage::Assistant {
content: Some(content),
..
}),
) => {
content.push_part(new_part);
}
_ => {
messages.push(match role {
Role::User => open_router::RequestMessage::User {
content: open_router::MessageContent::from(vec![new_part]),
},
Role::Assistant => open_router::RequestMessage::Assistant {
content: Some(open_router::MessageContent::from(vec![new_part])),
tool_calls: Vec::new(),
},
Role::System => open_router::RequestMessage::System {
content: open_router::MessageContent::from(vec![new_part]),
},
});
}
}
}
pub struct OpenRouterEventMapper {
tool_calls_by_index: HashMap<usize, RawToolCall>,
}

View File

@@ -303,10 +303,9 @@ impl Render for SyntaxTreeView {
{
let layer = layer.clone();
rendered = rendered.child(uniform_list(
cx.entity().clone(),
"SyntaxTreeView",
layer.node().descendant_count(),
move |this, range, _, cx| {
cx.processor(move |this, range: Range<usize>, _, cx| {
let mut items = Vec::new();
let mut cursor = layer.node().walk();
let mut descendant_ix = range.start;
@@ -377,7 +376,7 @@ impl Render for SyntaxTreeView {
}
}
items
},
}),
)
.size_full()
.track_scroll(self.list_scroll_handle.clone())

View File

@@ -88,7 +88,6 @@ 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

@@ -510,9 +510,10 @@ impl ContextProvider for GoContextProvider {
fn associated_tasks(
&self,
_: Option<Arc<dyn language::File>>,
_: Arc<dyn Fs>,
_: Option<Arc<dyn File>>,
_: &App,
) -> Option<TaskTemplates> {
) -> Task<Option<TaskTemplates>> {
let package_cwd = if GO_PACKAGE_TASK_VARIABLE.template_value() == "." {
None
} else {
@@ -520,7 +521,7 @@ impl ContextProvider for GoContextProvider {
};
let module_cwd = Some(GO_MODULE_ROOT_TASK_VARIABLE.template_value());
Some(TaskTemplates(vec![
Task::ready(Some(TaskTemplates(vec![
TaskTemplate {
label: format!(
"go test {} -run {}",
@@ -631,7 +632,7 @@ impl ContextProvider for GoContextProvider {
cwd: module_cwd.clone(),
..TaskTemplate::default()
},
]))
])))
}
}

View File

@@ -75,7 +75,10 @@
] @context
(#any-of? @_name "it" "test" "describe" "context" "suite")
arguments: (
arguments . (string (string_fragment) @name)
arguments . [
(string (string_fragment) @name)
(identifier) @name
]
)
)
) @item
@@ -92,7 +95,10 @@
(#eq? @_property "each")
)
arguments: (
arguments . (string (string_fragment) @name)
arguments . [
(string (string_fragment) @name)
(identifier) @name
]
)
)
) @item

View File

@@ -13,7 +13,10 @@
]
(#any-of? @_name "it" "test" "describe" "context" "suite")
arguments: (
arguments . (string (string_fragment) @run)
arguments . [
(string (string_fragment) @run)
(identifier) @run
]
)
) @_js-test
@@ -32,7 +35,10 @@
(#eq? @_property "each")
)
arguments: (
arguments . (string (string_fragment) @run)
arguments . [
(string (string_fragment) @run)
(identifier) @run
]
)
) @_js-test

View File

@@ -481,9 +481,10 @@ impl ContextProvider for PythonContextProvider {
fn associated_tasks(
&self,
_: Arc<dyn Fs>,
file: Option<Arc<dyn language::File>>,
cx: &App,
) -> Option<TaskTemplates> {
) -> Task<Option<TaskTemplates>> {
let test_runner = selected_test_runner(file.as_ref(), cx);
let mut tasks = vec![
@@ -587,7 +588,7 @@ impl ContextProvider for PythonContextProvider {
}
});
Some(TaskTemplates(tasks))
Task::ready(Some(TaskTemplates(tasks)))
}
}

View File

@@ -8,6 +8,7 @@ use http_client::github::AssetKind;
use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
pub use language::*;
use lsp::{InitializeParams, LanguageServerBinary};
use project::Fs;
use project::lsp_store::rust_analyzer_ext::CARGO_DIAGNOSTICS_SOURCE_NAME;
use project::project_settings::ProjectSettings;
use regex::Regex;
@@ -628,9 +629,10 @@ impl ContextProvider for RustContextProvider {
fn associated_tasks(
&self,
_: Arc<dyn Fs>,
file: Option<Arc<dyn language::File>>,
cx: &App,
) -> Option<TaskTemplates> {
) -> Task<Option<TaskTemplates>> {
const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
const CUSTOM_TARGET_DIR: &str = "RUST_TARGET_DIR";
@@ -798,7 +800,7 @@ impl ContextProvider for RustContextProvider {
.collect();
}
Some(TaskTemplates(task_templates))
Task::ready(Some(TaskTemplates(task_templates)))
}
fn lsp_task_source(&self) -> Option<LanguageServerName> {

View File

@@ -38,5 +38,5 @@ completion_query_characters = ["-", "."]
opt_into_language_servers = ["tailwindcss-language-server"]
prefer_label_for_snippet = true
[overrides.call_expression]
[overrides.function_name_before_type_arguments]
prefer_label_for_snippet = true

View File

@@ -14,4 +14,6 @@
(jsx_expression)
] @default
(_ value: (call_expression) @call_expression)
(_ value: (call_expression
function: (identifier) @function_name_before_type_arguments
type_arguments: (type_arguments)))

View File

@@ -4,10 +4,12 @@ use async_tar::Archive;
use async_trait::async_trait;
use chrono::{DateTime, Local};
use collections::HashMap;
use futures::future::join_all;
use gpui::{App, AppContext, AsyncApp, Task};
use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
use language::{
ContextLocation, ContextProvider, File, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
ContextLocation, ContextProvider, File, LanguageToolchainStore, LocalFile, LspAdapter,
LspAdapterDelegate,
};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
use node_runtime::NodeRuntime;
@@ -17,11 +19,12 @@ use smol::{fs, io::BufReader, lock::RwLock, stream::StreamExt};
use std::{
any::Any,
borrow::Cow,
collections::BTreeSet,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
};
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
use task::{TaskTemplate, TaskTemplates, VariableName};
use util::archive::extract_zip;
use util::merge_json_value_into;
use util::{ResultExt, fs::remove_matching, maybe};
@@ -32,23 +35,12 @@ pub(crate) struct TypeScriptContextProvider {
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_JEST_TEST_NAME_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST_TEST_NAME"));
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_VITEST_TEST_NAME_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST_TEST_NAME"));
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>>>);
@@ -58,32 +50,21 @@ struct PackageJson {
data: PackageJsonData,
}
#[derive(Clone, Copy, Default)]
#[derive(Clone, 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,
scripts: BTreeSet<String>,
package_manager: Option<&'static str>,
}
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 scripts = BTreeSet::new();
if let Some(serde_json::Value::Object(package_json_scripts)) = package_json.get("scripts") {
scripts.extend(package_json_scripts.keys().cloned());
}
let mut jest = false;
@@ -104,249 +85,351 @@ impl PackageJsonData {
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;
}
let package_manager = package_json
.get("packageManager")
.and_then(|value| value.as_str())
.and_then(|value| {
if value.starts_with("pnpm") {
Some("pnpm")
} else if value.starts_with("yarn") {
Some("yarn")
} else if value.starts_with("npm") {
Some("npm")
} else {
None
}
});
Self {
jest,
mocha,
vitest,
jasmine,
build_script,
test_script,
runner,
scripts,
package_manager,
}
}
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());
fn merge(&mut self, other: Self) {
self.jest |= other.jest;
self.mocha |= other.mocha;
self.vitest |= other.vitest;
self.jasmine |= other.jasmine;
self.scripts.extend(other.scripts);
}
fn fill_task_templates(&self, task_templates: &mut TaskTemplates) {
if self.jest {
variables.insert(TYPESCRIPT_JEST_TASK_VARIABLE, "jest".to_owned());
}
if self.mocha {
variables.insert(TYPESCRIPT_MOCHA_TASK_VARIABLE, "mocha".to_owned());
task_templates.0.push(TaskTemplate {
label: "jest file test".to_owned(),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
"jest".to_owned(),
VariableName::RelativeFile.template_value(),
],
cwd: Some(VariableName::WorktreeRoot.template_value()),
..TaskTemplate::default()
});
task_templates.0.push(TaskTemplate {
label: format!("jest test {}", VariableName::Symbol.template_value()),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
"jest".to_owned(),
"--testNamePattern".to_owned(),
format!(
"\"{}\"",
TYPESCRIPT_JEST_TEST_NAME_VARIABLE.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()
});
}
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![
TYPESCRIPT_JEST_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_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!(
"\"{}\"",
TYPESCRIPT_JEST_TEST_NAME_VARIABLE.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!("\"{}\"", TYPESCRIPT_VITEST_TASK_VARIABLE.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!("{} file test", "vitest".to_owned()),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
"vitest".to_owned(),
"run".to_owned(),
VariableName::RelativeFile.template_value(),
],
cwd: Some(VariableName::WorktreeRoot.template_value()),
..TaskTemplate::default()
});
task_templates.0.push(TaskTemplate {
label: format!(
"package.json script {}",
package_json_script.template_value()
"{} test {}",
"vitest".to_owned(),
VariableName::Symbol.template_value(),
),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
"vitest".to_owned(),
"run".to_owned(),
"--testNamePattern".to_owned(),
format!("\"{}\"", "vitest".to_owned()),
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()
});
}
if self.mocha {
task_templates.0.push(TaskTemplate {
label: format!("{} file test", "mocha".to_owned()),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
"mocha".to_owned(),
VariableName::RelativeFile.template_value(),
],
cwd: Some(VariableName::WorktreeRoot.template_value()),
..TaskTemplate::default()
});
task_templates.0.push(TaskTemplate {
label: format!(
"{} test {}",
"mocha".to_owned(),
VariableName::Symbol.template_value(),
),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
"mocha".to_owned(),
"--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()
});
}
if self.jasmine {
task_templates.0.push(TaskTemplate {
label: format!("{} file test", "jasmine".to_owned()),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
"jasmine".to_owned(),
VariableName::RelativeFile.template_value(),
],
cwd: Some(VariableName::WorktreeRoot.template_value()),
..TaskTemplate::default()
});
task_templates.0.push(TaskTemplate {
label: format!(
"{} test {}",
"jasmine".to_owned(),
VariableName::Symbol.template_value(),
),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
"jasmine".to_owned(),
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 script in &self.scripts {
task_templates.0.push(TaskTemplate {
label: format!("package.json > {script}",),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
"--prefix".to_owned(),
VariableName::WorktreeRoot.template_value(),
"run".to_owned(),
package_json_script.template_value(),
script.to_owned(),
],
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".to_owned(),
format!("\"{}\"", VariableName::SelectedText.template_value()),
],
..TaskTemplate::default()
});
impl TypeScriptContextProvider {
pub fn new() -> Self {
Self {
last_package_json: PackageJsonContents::default(),
}
}
Some(task_templates)
fn combined_package_json_data(
&self,
fs: Arc<dyn Fs>,
worktree_root: &Path,
file_abs_path: &Path,
cx: &App,
) -> Task<anyhow::Result<PackageJsonData>> {
let Some(file_relative_path) = file_abs_path.strip_prefix(&worktree_root).ok() else {
log::debug!("No package json data for off-worktree files");
return Task::ready(Ok(PackageJsonData::default()));
};
let new_json_data = file_relative_path
.ancestors()
.map(|path| worktree_root.join(path))
.map(|parent_path| {
self.package_json_data(&parent_path, self.last_package_json.clone(), fs.clone(), cx)
})
.collect::<Vec<_>>();
cx.background_spawn(async move {
let mut package_json_data = PackageJsonData::default();
for new_data in join_all(new_json_data).await.into_iter().flatten() {
package_json_data.merge(new_data);
}
Ok(package_json_data)
})
}
fn package_json_data(
&self,
directory_path: &Path,
existing_package_json: PackageJsonContents,
fs: Arc<dyn Fs>,
cx: &App,
) -> Task<anyhow::Result<PackageJsonData>> {
let package_json_path = directory_path.join("package.json");
let metadata_check_fs = fs.clone();
cx.background_spawn(async move {
let metadata = metadata_check_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 = existing_package_json.0.read().await;
contents
.get(&package_json_path)
.filter(|package_json| package_json.mtime == mtime)
.map(|package_json| package_json.data.clone())
};
match existing_data {
Some(existing_data) => Ok(existing_data),
None => {
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);
{
let mut contents = existing_package_json.0.write().await;
contents.insert(
package_json_path,
PackageJson {
mtime,
data: new_data.clone(),
},
);
}
Ok(new_data)
}
}
})
}
fn detect_package_manager(
&self,
worktree_root: PathBuf,
fs: Arc<dyn Fs>,
cx: &App,
) -> Task<&'static str> {
let last_package_json = self.last_package_json.clone();
let package_json_data =
self.package_json_data(&worktree_root, last_package_json, fs.clone(), cx);
cx.background_spawn(async move {
if let Ok(package_json_data) = package_json_data.await {
if let Some(package_manager) = package_json_data.package_manager {
return package_manager;
}
}
if fs.is_file(&worktree_root.join("pnpm-lock.yaml")).await {
return "pnpm";
}
if fs.is_file(&worktree_root.join("yarn.lock")).await {
return "yarn";
}
"npm"
})
}
}
impl ContextProvider for TypeScriptContextProvider {
fn associated_tasks(
&self,
fs: Arc<dyn Fs>,
file: Option<Arc<dyn File>>,
cx: &App,
) -> Task<Option<TaskTemplates>> {
let Some(file) = project::File::from_dyn(file.as_ref()).cloned() else {
return Task::ready(None);
};
let Some(worktree_root) = file.worktree.read(cx).root_dir() else {
return Task::ready(None);
};
let file_abs_path = file.abs_path(cx);
let package_json_data =
self.combined_package_json_data(fs.clone(), &worktree_root, &file_abs_path, cx);
cx.background_spawn(async move {
let mut task_templates = TaskTemplates(Vec::new());
task_templates.0.push(TaskTemplate {
label: format!(
"execute selection {}",
VariableName::SelectedText.template_value()
),
command: "node".to_owned(),
args: vec![
"-e".to_owned(),
format!("\"{}\"", VariableName::SelectedText.template_value()),
],
..TaskTemplate::default()
});
match package_json_data.await {
Ok(package_json) => {
package_json.fill_task_templates(&mut task_templates);
}
Err(e) => {
log::error!(
"Failed to read package.json for worktree {file_abs_path:?}: {e:#}"
);
}
}
Some(task_templates)
})
}
fn build_context(
@@ -370,73 +453,19 @@ impl ContextProvider for TypeScriptContextProvider {
);
}
let Some((fs, worktree_root)) = location.fs.zip(location.worktree_root) else {
return Task::ready(Ok(vars));
};
let package_json_contents = self.last_package_json.clone();
let task = location
.worktree_root
.zip(location.fs)
.map(|(worktree_root, fs)| self.detect_package_manager(worktree_root, fs, cx));
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);
vars.extend(variables);
if let Some(task) = task {
vars.insert(TYPESCRIPT_RUNNER_VARIABLE, task.await.to_owned());
}
Ok(vars)
})
}
}
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> {
vec![server_path.into(), "--stdio".into()]
}

View File

@@ -25,5 +25,5 @@ documentation = { start = "/**", end = "*/", prefix = "* ", tab_size = 1 }
completion_query_characters = ["."]
prefer_label_for_snippet = true
[overrides.call_expression]
[overrides.function_name_before_type_arguments]
prefer_label_for_snippet = true

View File

@@ -83,7 +83,10 @@
] @context
(#any-of? @_name "it" "test" "describe" "context" "suite")
arguments: (
arguments . (string (string_fragment) @name)
arguments . [
(string (string_fragment) @name)
(identifier) @name
]
)
)
) @item
@@ -100,7 +103,10 @@
(#any-of? @_property "each")
)
arguments: (
arguments . (string (string_fragment) @name)
arguments . [
(string (string_fragment) @name)
(identifier) @name
]
)
)
) @item

View File

@@ -1,4 +1,6 @@
(comment) @comment.inclusive
(string) @string
(_ value: (call_expression) @call_expression)
(_ value: (call_expression
function: (identifier) @function_name_before_type_arguments
type_arguments: (type_arguments)))

View File

@@ -13,7 +13,10 @@
]
(#any-of? @_name "it" "test" "describe" "context" "suite")
arguments: (
arguments . (string (string_fragment) @run)
arguments . [
(string (string_fragment) @run)
(identifier) @run
]
)
) @_js-test
@@ -32,7 +35,10 @@
(#any-of? @_property "each")
)
arguments: (
arguments . (string (string_fragment) @run)
arguments . [
(string (string_fragment) @run)
(identifier) @run
]
)
) @_js-test

View File

@@ -52,6 +52,7 @@ pub struct Model {
pub display_name: Option<String>,
pub max_tokens: usize,
pub supports_tools: Option<bool>,
pub supports_images: Option<bool>,
}
impl Model {
@@ -61,6 +62,7 @@ impl Model {
Some("Auto Router"),
Some(2000000),
Some(true),
Some(false),
)
}
@@ -73,12 +75,14 @@ impl Model {
display_name: Option<&str>,
max_tokens: Option<usize>,
supports_tools: Option<bool>,
supports_images: Option<bool>,
) -> Self {
Self {
name: name.to_owned(),
display_name: display_name.map(|s| s.to_owned()),
max_tokens: max_tokens.unwrap_or(2000000),
supports_tools,
supports_images,
}
}
@@ -154,22 +158,118 @@ pub struct FunctionDefinition {
#[serde(tag = "role", rename_all = "lowercase")]
pub enum RequestMessage {
Assistant {
content: Option<String>,
content: Option<MessageContent>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
tool_calls: Vec<ToolCall>,
},
User {
content: String,
content: MessageContent,
},
System {
content: String,
content: MessageContent,
},
Tool {
content: String,
content: MessageContent,
tool_call_id: String,
},
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(untagged)]
pub enum MessageContent {
Plain(String),
Multipart(Vec<MessagePart>),
}
impl MessageContent {
pub fn empty() -> Self {
Self::Plain(String::new())
}
pub fn push_part(&mut self, part: MessagePart) {
match self {
Self::Plain(text) if text.is_empty() => {
*self = Self::Multipart(vec![part]);
}
Self::Plain(text) => {
let text_part = MessagePart::Text {
text: std::mem::take(text),
};
*self = Self::Multipart(vec![text_part, part]);
}
Self::Multipart(parts) => parts.push(part),
}
}
}
impl From<Vec<MessagePart>> for MessageContent {
fn from(parts: Vec<MessagePart>) -> Self {
if parts.len() == 1 {
if let MessagePart::Text { text } = &parts[0] {
return Self::Plain(text.clone());
}
}
Self::Multipart(parts)
}
}
impl From<String> for MessageContent {
fn from(text: String) -> Self {
Self::Plain(text)
}
}
impl From<&str> for MessageContent {
fn from(text: &str) -> Self {
Self::Plain(text.to_string())
}
}
impl MessageContent {
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Plain(text) => Some(text),
Self::Multipart(parts) if parts.len() == 1 => {
if let MessagePart::Text { text } = &parts[0] {
Some(text)
} else {
None
}
}
_ => None,
}
}
pub fn to_text(&self) -> String {
match self {
Self::Plain(text) => text.clone(),
Self::Multipart(parts) => parts
.iter()
.filter_map(|part| {
if let MessagePart::Text { text } = part {
Some(text.as_str())
} else {
None
}
})
.collect::<Vec<_>>()
.join(""),
}
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MessagePart {
Text {
text: String,
},
#[serde(rename = "image_url")]
Image {
image_url: String,
},
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct ToolCall {
pub id: String,
@@ -266,6 +366,14 @@ pub struct ModelEntry {
pub context_length: Option<usize>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub supported_parameters: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub architecture: Option<ModelArchitecture>,
}
#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
pub struct ModelArchitecture {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub input_modalities: Vec<String>,
}
pub async fn complete(
@@ -470,6 +578,13 @@ pub async fn list_models(client: &dyn HttpClient, api_url: &str) -> Result<Vec<M
),
max_tokens: entry.context_length.unwrap_or(2000000),
supports_tools: Some(entry.supported_parameters.contains(&"tools".to_string())),
supports_images: Some(
entry
.architecture
.as_ref()
.map(|arch| arch.input_modalities.contains(&"image".to_string()))
.unwrap_or(false),
),
})
.collect();

View File

@@ -4497,8 +4497,10 @@ impl OutlinePanel {
let multi_buffer_snapshot = self
.active_editor()
.map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx));
uniform_list(cx.entity().clone(), "entries", items_len, {
move |outline_panel, range, window, cx| {
uniform_list(
"entries",
items_len,
cx.processor(move |outline_panel, range: Range<usize>, window, cx| {
let entries = outline_panel.cached_entries.get(range);
entries
.map(|entries| entries.to_vec())
@@ -4555,8 +4557,8 @@ impl OutlinePanel {
),
})
.collect()
}
})
}),
)
.with_sizing_behavior(ListSizingBehavior::Infer)
.with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
.with_width_from_item(self.max_width_item_index)

View File

@@ -17,7 +17,7 @@ use gpui::{
use head::Head;
use schemars::JsonSchema;
use serde::Deserialize;
use std::{sync::Arc, time::Duration};
use std::{ops::Range, sync::Arc, time::Duration};
use ui::{
Color, Divider, Label, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, prelude::*, v_flex,
};
@@ -760,14 +760,13 @@ impl<D: PickerDelegate> Picker<D> {
match &self.element_container {
ElementContainer::UniformList(scroll_handle) => uniform_list(
cx.entity().clone(),
"candidates",
self.delegate.match_count(),
move |picker, visible_range, window, cx| {
cx.processor(move |picker, visible_range: Range<usize>, window, cx| {
visible_range
.map(|ix| picker.render_element(window, cx, ix))
.collect()
},
}),
)
.with_sizing_behavior(sizing_behavior)
.when_some(self.widest_item, |el, widest_item| {

View File

@@ -665,6 +665,7 @@ impl BreakpointStore {
.as_ref()
.is_some_and(|active_position| active_position == &position)
{
cx.emit(BreakpointStoreEvent::SetDebugLine);
return;
}

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