Compare commits

..

47 Commits

Author SHA1 Message Date
Junkui Zhang
2eac6a9222 fix linux ci 2025-06-11 18:16:59 +08:00
Junkui Zhang
7c3cffdc52 clippy 2025-06-11 17:32:18 +08:00
Junkui Zhang
5a3186b659 fix neovim 2025-06-11 16:37:37 +08:00
Junkui Zhang
caf54844de remove international keycodes 2025-06-11 16:33:11 +08:00
Junkui Zhang
745ebe2313 fix all tests 2025-06-11 16:16:31 +08:00
Junkui Zhang
5c95e942e6 fix 2025-06-10 23:35:53 +08:00
Junkui Zhang
f979f24bfa try fix linux 2025-06-10 23:32:00 +08:00
Junkui Zhang
411b9abb9e clippy 2025-06-10 23:13:43 +08:00
Junkui Zhang
81d4d48ef2 add missing codes 2025-06-10 23:11:21 +08:00
Junkui Zhang
cd9284761a fix macOS 2025-06-10 23:01:45 +08:00
Junkui Zhang
34f9eef879 try fix macOS 2025-06-10 22:14:46 +08:00
Junkui Zhang
23cf6bf268 init macOS 2025-06-10 21:16:23 +08:00
Junkui Zhang
c97e477eb1 fix test platform 2025-06-10 20:58:39 +08:00
Junkui Zhang
16804a81cc checkpoint 2025-06-10 18:27:35 +08:00
Junkui Zhang
8bf39bf768 fix 2025-06-10 18:16:21 +08:00
Junkui Zhang
75922e8fcd fix 2025-06-10 17:58:05 +08:00
Junkui Zhang
2eb83364ae support parsing scan code 2025-06-10 17:58:05 +08:00
Junkui Zhang
5d22585ef5 add shifted key tests 2025-06-10 17:58:04 +08:00
Junkui Zhang
71303fa18b add tests 2025-06-10 17:58:04 +08:00
Junkui Zhang
5753b978a0 impl for windows 2025-06-10 17:58:04 +08:00
Junkui Zhang
9cf2490ed7 add PlatformKeyboardMapper 2025-06-10 17:58:04 +08:00
Junkui Zhang
28ea3ea529 add ScanCode 2025-06-10 17:58:04 +08:00
张小白
2dad48d8d9 windows: Fix panic when deleting the last pre-edit char using Pinyin IME (#32442)
Release Notes:

- N/A
2025-06-10 09:51:12 +00:00
Burak Varlı
16853acbb1 Enable cross-region inference for Claude 4 family models on Amazon Bedrock provider (#32235)
These models require cross-region inference, and it currently fails if
you try to use them:
```
Invocation of model ID anthropic.claude-sonnet-4-20250514-v1:0 with on-demand throughput isn’t supported. 
```

Release Notes:

- Enable cross-region inference for Claude 4 family models on Amazon
Bedrock provider

Signed-off-by: Burak Varlı <burakvar@amazon.co.uk>
2025-06-09 23:38:39 -07:00
Michael Sloan
64d649245c Add missing #[track_caller] meant to be in #32433 (#32434)
Release Notes:

- N/A
2025-06-10 04:52:43 +00:00
Michael Sloan
08210b512d Don't push to selection history if selections are empty (#32433)
I got a panic during undo but haven't been able to repro it. Potentially
a consequence of my changes in #31731

> Thread "main" panicked with "There must be at least one selection" at
crates/editor/src/selections_collection.rs

Leaving release notes blank as I'm not sure this actually fixes the
panic

Release Notes:

- N/A
2025-06-10 04:29:45 +00:00
Michael Sloan
6070aea6c0 Skip adding initial 1:1 cursor position to selection history (#32432)
Also improves some minor corner cases in `undo_selection` and
`redo_selection` related to the use of `end_selection`. If the pending
selection was ended, this would separately get pushed to the redo or
undo stack and redundantly run all the other effects of selection
change.

Release Notes:

- N/A
2025-06-10 04:22:46 +00:00
Conrad Irwin
16b44d53f9 debugger: Use Delve to build Go binaries (#32221)
Release Notes:

- debugger: Use delve to build go debug executables, and pass arguments
through.

---------

Co-authored-by: sysradium <sysradium@users.noreply.github.com>
Co-authored-by: Zed AI <ai@zed.dev>
2025-06-09 21:49:04 -06:00
Haru Kim
3bed830a1f Use unix pipe to capture environment variables (#32136)
The use of `NamedTempFile` in #31799 was not secure and could
potentially cause write permission issues (see [this
comment](https://github.com/zed-industries/zed/issues/29528#issuecomment-2939672433)).
Therefore, it has been replaced with a Unix pipe.

Release Notes:
- N/A
2025-06-09 20:37:43 -06:00
Conrad Irwin
ee2a329981 Slow down reconnects on collab (#32418)
We believe that collab deploys currently cause outages because:

* All clients try to reconnect simultaneously
* This causes very high CPU usage on collab (and to some extent, the
database)
* This means that collab is slow to respond to clients
* So clients timeout and retry, over and over again.

We hope by letting clients in in buckets of 512, we can accept some
minor slowness to avoid
complete downtime, while we rewrite the system.

Release Notes:

- N/A
2025-06-09 19:59:04 -06:00
Joseph T. Lyons
6d64058fc6 Add pane: unpin all tabs (#32423)
After integrating pinned tabs into my workflow, I've come to the
conclusion that it is painfully slow to unpin all tabs by hand.


https://github.com/user-attachments/assets/ad087b8e-4595-4c4d-827f-188e36170c25

Release Notes:

- Added a `pane: unpin all tabs` action
2025-06-09 20:25:22 -04:00
Danilo Leal
7c2822a020 docs: Improve MCP-related pages (#32422)
While creating a new MCP extension this weekend, I visited these pages
and it felt like they could be improved a little bit. I'm renaming the
MCP-related page under the /extension directory to use the "MCP"
acronym, instead of "context servers".

Release Notes:

- N/A
2025-06-09 21:00:10 -03:00
Danilo Leal
3db00384f4 agent: Improve generating dots display (#32421)
Previously, upon hitting the "Continue" button to restart an interrupted
thread due to consecutive tool calls reaching its limit, you wouldn't
see the loading dots and the UI would be a weird state. This PR improves
when these loading dots actually show up, including in their conditional
a check for `message.is_hidden`.

Also took advantage of the opportunity to quickly organize some of these
variables. The `render_message` function could potentially be chopped up
in more smaller pieces. Lots of things going on here.

Release Notes:

- N/A
2025-06-09 20:52:50 -03:00
Conrad Irwin
3dfbd9e57c Fix ruby debugger (#32407)
Closes #ISSUE

Release Notes:

- debugger: Fix Ruby (was broken by #30833)

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Piotr Osiewicz <peterosiewicz@gmail.com>
Co-authored-by: Cole Miller <m@cole-miller.net>
2025-06-09 16:11:24 -06:00
Agus Zubiaga
b103d7621b Improve handling of large output in embedded terminals (#32416)
#31922 made embedded terminals automatically grow to fit the content. We
since found some issues with large output which this PR addresses by:

- Only shaping / laying out lines that are visible in the viewport
(based on `window.content_mask`)
- Falling back to embedded scrolling after 1K lines. The perf fix above
actually makes it possible to handle a lot of lines, but:
- Alacrity uses a `u16` for rows internally, so we needed a limit to
prevent overflow.
- Scrolling through thousands of lines to get to the other side of a
terminal tool call isn't great UX, so we might as well set the limit
low.
- We can consider raising the limit when we make card headers sticky.

Release Notes:

- Agent: Improve handling of large terminal output
2025-06-09 18:11:31 -03:00
Danilo Leal
ab70e524c8 agent: Improve MCP status indicator tooltip and loading state (#32414)
Mostly a small tweak making sure that the indicator tooltip hit area is
bigger and the loading state is clearer (not using an indicator
anymore). Way more little improvement opportunities in this component to
do, though.

Release Notes:

- N/A
2025-06-09 18:04:48 -03:00
JonasKaplan
f0ce62ead8 editor: Add trailing whitespace rendering (#32329)
Closes #5237

- Adds "trailing" option for "show_whitespaces" in settings.json
- Supports importing this setting from vscode

The option in question will render only whitespace characters that
appear after every non-whitespace character in a given line.

Release Notes:

- Added trailing whitespace rendering
2025-06-09 20:48:49 +00:00
Cole Miller
f0345df479 debugger: Undo conversion of stack frames list to uniform list (#32413)
Partially reverts #30682

A uniform list is desirable for the scrolling behavior, but this breaks
badly when there are collapsed entries or entries without paths, both of
which seem common with the JS adapter.

It would be nice to go back to a uniform list if we can come up with a
set of design tweaks that allow all entries to be the same height.

Release Notes:

- Debugger Beta: fixed an issue that caused entries in the stack frame
list to overlap in some situations.
2025-06-09 20:45:01 +00:00
Michael Sloan
bbd2262a93 Fix buffer rendering on every mouse move (#32408)
Closes #32210

This notify was added in #13433. Solution is to only notify when the
breakpoint indicator state has changed.

Also improves the logic for enqueuing a task to delay showing - now only
does this if it isn't already visible, and that delay task now only
notifies if still hovering.

Release Notes:

- Fixed a bug where buffers render on every mouse move.
2025-06-09 14:10:03 -06:00
Antonio Scandurra
c4fd9e1a6b Switch to using weak transactions for queries happening on connection (#32411)
Release Notes:

- N/A

Co-authored-by: Conrad <conrad@zed.dev>
2025-06-09 21:37:48 +02:00
Nate Butler
0b7583bae5 Refine styling of merge conflicts (#31012)
- Improved colors
- Blank out diff hunk gutter highlights in conflict regions
- Paint conflict marker highlights all the way to the gutter

Release Notes:

- Improved the highlighting of merge conflict markers in editors.

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Cole Miller <cole@zed.dev>
2025-06-09 19:03:19 +00:00
Ben Brandt
e4bd115a63 More resilient eval (#32257)
Bubbles up rate limit information so that we can retry after a certain
duration if needed higher up in the stack.

Also caps the number of concurrent evals running at once to also help.

Release Notes:

- N/A
2025-06-09 18:07:22 +00:00
Kirill Bulatov
fa54fa80d0 Store pulled diagnostics' result_ids more persistently (#32403)
Follow-up of https://github.com/zed-industries/zed/pull/19230

`BufferId` can change between file reopens: e.g. open the buffer, close
it, go back in history to reopen it — the 2nd one will have a different
`BufferId`, but the same `result_ids` semantically.

Release Notes:

- N/A
2025-06-09 17:05:33 +00:00
Antonio Scandurra
de16f2bbe6 Bypass account age check when feature flag is set (#32393)
Release Notes:

- N/A
2025-06-09 18:44:48 +02:00
Jason Lee
e3b13b54c9 title_bar: Merge Linux only code into platform_linux (#32401)
Release Notes:

- N/A
2025-06-09 19:19:24 +03:00
Tommy D. Rossi
2c5d2a58d8 Do not skip punctuation characters with alt-arrow if next character is \n (#32368)
Closes #32356

Release Notes:

- N/A
2025-06-09 09:25:32 -06:00
Peter Tripp
3485b7704b Update GitHub Issue Templates (June 2025) (#32399)
- Remove git/edit predictions templates
- Rename Agent to AI related (include edit predictions, copilot, etc)
- Other minor adjustments

Release Notes:

- N/A
2025-06-09 15:25:17 +00:00
106 changed files with 3323 additions and 1367 deletions

View File

@@ -1,4 +1,4 @@
name: Bug Report (AI Related)
name: Bug Report (AI)
description: Zed Agent Panel Bugs
type: "Bug"
labels: ["ai"]
@@ -19,15 +19,14 @@ body:
2.
3.
Actual Behavior:
Expected Behavior:
**Expected Behavior**:
**Actual Behavior**:
### Model Provider Details
- Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc)
- Model Name:
- Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads)
- MCP Servers in-use:
- Other Details:
- Other Details (MCPs, other settings, etc):
validations:
required: true

View File

@@ -1,36 +0,0 @@
name: Bug Report (Edit Predictions)
description: Zed Edit Predictions bugs
type: "Bug"
labels: ["ai", "inline completion", "zeta"]
title: "Edit Predictions: <a short description of the Edit Prediction bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
<!-- Please include the LLM provider and model name you are using -->
Steps to trigger the problem:
1.
2.
3.
Actual Behavior:
Expected Behavior:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
validations:
required: true

View File

@@ -1,35 +0,0 @@
name: Bug Report (Git)
description: Zed Git-Related Bugs
type: "Bug"
labels: ["git"]
title: "Git: <a short description of the Git bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
Steps to trigger the problem:
1.
2.
3.
Actual Behavior:
Expected Behavior:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
validations:
required: true

View File

@@ -19,8 +19,8 @@ body:
2.
3.
Actual Behavior:
Expected Behavior:
**Expected Behavior**:
**Actual Behavior**:
validations:
required: true

View File

@@ -18,14 +18,16 @@ body:
- Issues with insufficient detail may be summarily closed.
-->
DESCRIPTION_HERE
Steps to reproduce:
1.
2.
3.
4.
Expected Behavior:
Actual Behavior:
**Expected Behavior**:
**Actual Behavior**:
<!-- Before Submitting, did you:
1. Include settings.json, keymap.json, .editorconfig if relevant?

View File

@@ -1,6 +1,12 @@
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:
@@ -20,4 +26,9 @@ runs:
- name: Run tests
shell: bash -euxo pipefail {0}
run: cargo nextest run --workspace --no-fail-fast
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

View File

@@ -319,6 +319,8 @@ jobs:
- name: Run tests
uses: ./.github/actions/run_tests
with:
use-xvfb: true
- name: Build other binaries and features
run: |

View File

@@ -62,7 +62,7 @@ jobs:
- name: Run unit evals
shell: bash -euxo pipefail {0}
run: cargo nextest run --workspace --no-fail-fast --features eval --no-capture -E 'test(::eval_)' --test-threads 1
run: cargo nextest run --workspace --no-fail-fast --features eval --no-capture -E 'test(::eval_)'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

26
Cargo.lock generated
View File

@@ -705,6 +705,7 @@ dependencies = [
"serde_json",
"settings",
"smallvec",
"smol",
"streaming_diff",
"strsim",
"task",
@@ -3160,6 +3161,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "command-fds"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ec1052629a80c28594777d1252efc8a6b005d13f9edfd8c3fc0f44d5b32489a"
dependencies = [
"nix 0.30.1",
"thiserror 2.0.12",
]
[[package]]
name = "command_palette"
version = "0.1.0"
@@ -4052,6 +4063,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"collections",
"dap",
"futures 0.3.31",
"gpui",
@@ -10130,6 +10142,18 @@ dependencies = [
"memoffset",
]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.9.0",
"cfg-if",
"cfg_aliases 0.2.1",
"libc",
]
[[package]]
name = "node_runtime"
version = "0.1.0"
@@ -12110,7 +12134,6 @@ dependencies = [
"unindent",
"url",
"util",
"uuid",
"which 6.0.3",
"workspace-hack",
"worktree",
@@ -17122,6 +17145,7 @@ dependencies = [
"async-fs",
"async_zip",
"collections",
"command-fds",
"dirs 4.0.0",
"dunce",
"futures 0.3.31",

View File

@@ -99,6 +99,8 @@
"version_control.added": "#27a657ff",
"version_control.modified": "#d3b020ff",
"version_control.deleted": "#e06c76ff",
"version_control.conflict_marker.ours": "#a1c1811a",
"version_control.conflict_marker.theirs": "#74ade81a",
"conflict": "#dec184ff",
"conflict.background": "#dec1841a",
"conflict.border": "#5d4c2fff",

View File

@@ -1788,12 +1788,31 @@ impl ActiveThread {
fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
let message_id = self.messages[ix];
let Some(message) = self.thread.read(cx).message(message_id) else {
let workspace = self.workspace.clone();
let thread = self.thread.read(cx);
let is_first_message = ix == 0;
let is_last_message = ix == self.messages.len() - 1;
let Some(message) = thread.message(message_id) else {
return Empty.into_any();
};
let is_generating = thread.is_generating();
let is_generating_stale = thread.is_generation_stale().unwrap_or(false);
let loading_dots = (is_generating && is_last_message).then(|| {
h_flex()
.h_8()
.my_3()
.mx_5()
.when(is_generating_stale || message.is_hidden, |this| {
this.child(AnimatedLabel::new("").size(LabelSize::Small))
})
});
if message.is_hidden {
return Empty.into_any();
return div().children(loading_dots).into_any();
}
let message_creases = message.creases.clone();
@@ -1802,9 +1821,6 @@ impl ActiveThread {
return Empty.into_any();
};
let workspace = self.workspace.clone();
let thread = self.thread.read(cx);
// Get all the data we need from thread before we start using it in closures
let checkpoint = thread.checkpoint_for_message(message_id);
let configured_model = thread.configured_model().map(|m| m.model);
@@ -1815,14 +1831,6 @@ impl ActiveThread {
let tool_uses = thread.tool_uses_for_message(message_id, cx);
let has_tool_uses = !tool_uses.is_empty();
let is_generating = thread.is_generating();
let is_generating_stale = thread.is_generation_stale().unwrap_or(false);
let is_first_message = ix == 0;
let is_last_message = ix == self.messages.len() - 1;
let loading_dots = (is_generating_stale && is_last_message)
.then(|| AnimatedLabel::new("").size(LabelSize::Small));
let editing_message_state = self
.editing_message
@@ -2238,17 +2246,7 @@ impl ActiveThread {
parent.child(self.render_rules_item(cx))
})
.child(styled_message)
.when(is_generating && is_last_message, |this| {
this.child(
h_flex()
.h_8()
.mt_2()
.mb_4()
.ml_4()
.py_1p5()
.when_some(loading_dots, |this, loading_dots| this.child(loading_dots)),
)
})
.children(loading_dots)
.when(show_feedback, move |parent| {
parent.child(feedback_items).when_some(
self.open_feedback_editors.get(&message_id),

View File

@@ -12,7 +12,7 @@ use context_server::ContextServerId;
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyView, App, Entity, EventEmitter, FocusHandle,
Focusable, ScrollHandle, Subscription, pulsating_between,
Focusable, ScrollHandle, Subscription, Transformation, percentage,
};
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use project::context_server_store::{ContextServerStatus, ContextServerStore};
@@ -475,7 +475,6 @@ impl AgentConfiguration {
.get(&context_server_id)
.copied()
.unwrap_or_default();
let tools = tools_by_source
.get(&ToolSource::ContextServer {
id: context_server_id.0.clone().into(),
@@ -484,25 +483,23 @@ impl AgentConfiguration {
let tool_count = tools.len();
let border_color = cx.theme().colors().border.opacity(0.6);
let success_color = Color::Success.color(cx);
let (status_indicator, tooltip_text) = match server_status {
ContextServerStatus::Starting => (
Indicator::dot()
.color(Color::Success)
Icon::new(IconName::LoadCircle)
.size(IconSize::XSmall)
.color(Color::Accent)
.with_animation(
SharedString::from(format!("{}-starting", context_server_id.0.clone(),)),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 1.)),
move |this, delta| this.color(success_color.alpha(delta).into()),
Animation::new(Duration::from_secs(3)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element(),
"Server is starting.",
),
ContextServerStatus::Running => (
Indicator::dot().color(Color::Success).into_any_element(),
"Server is running.",
"Server is active.",
),
ContextServerStatus::Error(_) => (
Indicator::dot().color(Color::Error).into_any_element(),
@@ -526,12 +523,11 @@ impl AgentConfiguration {
.p_1()
.justify_between()
.when(
error.is_some() || are_tools_expanded && tool_count > 1,
error.is_some() || are_tools_expanded && tool_count >= 1,
|element| element.border_b_1().border_color(border_color),
)
.child(
h_flex()
.gap_1p5()
.child(
Disclosure::new(
"tool-list-disclosure",
@@ -551,12 +547,16 @@ impl AgentConfiguration {
})),
)
.child(
div()
.id(item_id.clone())
h_flex()
.id(SharedString::from(format!("tooltip-{}", item_id)))
.h_full()
.w_3()
.mx_1()
.justify_center()
.tooltip(Tooltip::text(tooltip_text))
.child(status_indicator),
)
.child(Label::new(context_server_id.0.clone()).ml_0p5())
.child(Label::new(item_id).ml_0p5().mr_1p5())
.when(is_running, |this| {
this.child(
Label::new(if tool_count == 1 {

View File

@@ -386,8 +386,10 @@ impl CodegenAlternative {
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
} else {
let request = self.build_request(&model, user_prompt, cx)?;
cx.spawn(async move |_, cx| model.stream_completion_text(request.await, &cx).await)
.boxed_local()
cx.spawn(async move |_, cx| {
Ok(model.stream_completion_text(request.await, &cx).await?)
})
.boxed_local()
};
self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx);
Ok(())

View File

@@ -1331,7 +1331,7 @@ impl InlineAssistant {
editor.clear_gutter_highlights::<GutterPendingRange>(cx);
} else {
editor.highlight_gutter::<GutterPendingRange>(
&gutter_pending_ranges,
gutter_pending_ranges,
|cx| cx.theme().status().info_background,
cx,
)
@@ -1342,7 +1342,7 @@ impl InlineAssistant {
editor.clear_gutter_highlights::<GutterTransformedRange>(cx);
} else {
editor.highlight_gutter::<GutterTransformedRange>(
&gutter_transformed_ranges,
gutter_transformed_ranges,
|cx| cx.theme().status().info,
cx,
)

View File

@@ -1563,6 +1563,9 @@ impl Thread {
Err(LanguageModelCompletionError::Other(error)) => {
return Err(error);
}
Err(err @ LanguageModelCompletionError::RateLimit(..)) => {
return Err(err.into());
}
};
match event {

View File

@@ -1,4 +1,5 @@
use std::str::FromStr;
use std::time::Duration;
use anyhow::{Context as _, Result, anyhow};
use chrono::{DateTime, Utc};
@@ -406,6 +407,7 @@ impl RateLimit {
/// <https://docs.anthropic.com/en/api/rate-limits#response-headers>
#[derive(Debug)]
pub struct RateLimitInfo {
pub retry_after: Option<Duration>,
pub requests: Option<RateLimit>,
pub tokens: Option<RateLimit>,
pub input_tokens: Option<RateLimit>,
@@ -417,10 +419,11 @@ impl RateLimitInfo {
// Check if any rate limit headers exist
let has_rate_limit_headers = headers
.keys()
.any(|k| k.as_str().starts_with("anthropic-ratelimit-"));
.any(|k| k == "retry-after" || k.as_str().starts_with("anthropic-ratelimit-"));
if !has_rate_limit_headers {
return Self {
retry_after: None,
requests: None,
tokens: None,
input_tokens: None,
@@ -429,6 +432,11 @@ impl RateLimitInfo {
}
Self {
retry_after: headers
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok())
.map(Duration::from_secs),
requests: RateLimit::from_headers("requests", headers).ok(),
tokens: RateLimit::from_headers("tokens", headers).ok(),
input_tokens: RateLimit::from_headers("input-tokens", headers).ok(),
@@ -481,8 +489,8 @@ pub async fn stream_completion_with_rate_limit_info(
.send(request)
.await
.context("failed to send request to Anthropic")?;
let rate_limits = RateLimitInfo::from_headers(response.headers());
if response.status().is_success() {
let rate_limits = RateLimitInfo::from_headers(response.headers());
let reader = BufReader::new(response.into_body());
let stream = reader
.lines()
@@ -500,6 +508,8 @@ pub async fn stream_completion_with_rate_limit_info(
})
.boxed();
Ok((stream, Some(rate_limits)))
} else if let Some(retry_after) = rate_limits.retry_after {
Err(AnthropicError::RateLimit(retry_after))
} else {
let mut body = Vec::new();
response
@@ -769,6 +779,8 @@ pub struct MessageDelta {
#[derive(Error, Debug)]
pub enum AnthropicError {
#[error("rate limit exceeded, retry after {0:?}")]
RateLimit(Duration),
#[error("an error occurred while interacting with the Anthropic API: {error_type}: {message}", error_type = .0.error_type, message = .0.message)]
ApiError(ApiError),
#[error("{0}")]

View File

@@ -682,11 +682,12 @@ mod tests {
_: &AsyncApp,
) -> BoxFuture<
'static,
http_client::Result<
Result<
BoxStream<
'static,
http_client::Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
>,
LanguageModelCompletionError,
>,
> {
unimplemented!()

View File

@@ -80,6 +80,7 @@ rand.workspace = true
pretty_assertions.workspace = true
reqwest_client.workspace = true
settings = { workspace = true, features = ["test-support"] }
smol.workspace = true
task = { workspace = true, features = ["test-support"]}
tempfile.workspace = true
theme.workspace = true

View File

@@ -11,7 +11,7 @@ use client::{Client, UserStore};
use collections::HashMap;
use fs::FakeFs;
use futures::{FutureExt, future::LocalBoxFuture};
use gpui::{AppContext, TestAppContext};
use gpui::{AppContext, TestAppContext, Timer};
use indoc::{formatdoc, indoc};
use language_model::{
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult,
@@ -1255,9 +1255,12 @@ impl EvalAssertion {
}],
..Default::default()
};
let mut response = judge
.stream_completion_text(request, &cx.to_async())
.await?;
let mut response = retry_on_rate_limit(async || {
Ok(judge
.stream_completion_text(request.clone(), &cx.to_async())
.await?)
})
.await?;
let mut output = String::new();
while let Some(chunk) = response.stream.next().await {
let chunk = chunk?;
@@ -1308,10 +1311,17 @@ fn eval(
run_eval(eval.clone(), tx.clone());
let executor = gpui::background_executor();
let semaphore = Arc::new(smol::lock::Semaphore::new(32));
for _ in 1..iterations {
let eval = eval.clone();
let tx = tx.clone();
executor.spawn(async move { run_eval(eval, tx) }).detach();
let semaphore = semaphore.clone();
executor
.spawn(async move {
let _guard = semaphore.acquire().await;
run_eval(eval, tx)
})
.detach();
}
drop(tx);
@@ -1577,21 +1587,31 @@ impl EditAgentTest {
if let Some(input_content) = eval.input_content.as_deref() {
buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));
}
let (edit_output, _) = self.agent.edit(
buffer.clone(),
eval.edit_file_input.display_description,
&conversation,
&mut cx.to_async(),
);
edit_output.await?
retry_on_rate_limit(async || {
self.agent
.edit(
buffer.clone(),
eval.edit_file_input.display_description.clone(),
&conversation,
&mut cx.to_async(),
)
.0
.await
})
.await?
} else {
let (edit_output, _) = self.agent.overwrite(
buffer.clone(),
eval.edit_file_input.display_description,
&conversation,
&mut cx.to_async(),
);
edit_output.await?
retry_on_rate_limit(async || {
self.agent
.overwrite(
buffer.clone(),
eval.edit_file_input.display_description.clone(),
&conversation,
&mut cx.to_async(),
)
.0
.await
})
.await?
};
let buffer_text = buffer.read_with(cx, |buffer, _| buffer.text());
@@ -1613,6 +1633,26 @@ impl EditAgentTest {
}
}
async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) -> Result<R> {
loop {
match request().await {
Ok(result) => return Ok(result),
Err(err) => match err.downcast::<LanguageModelCompletionError>() {
Ok(err) => match err {
LanguageModelCompletionError::RateLimit(duration) => {
// Wait until after we are allowed to try again
eprintln!("Rate limit exceeded. Waiting for {duration:?}...",);
Timer::after(duration).await;
continue;
}
_ => return Err(err.into()),
},
Err(err) => return Err(err),
},
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
struct EvalAssertionOutcome {
score: usize,

View File

@@ -638,29 +638,36 @@ impl ToolCard for TerminalToolCard {
.bg(cx.theme().colors().editor_background)
.rounded_b_md()
.text_ui_sm(cx)
.child(
ToolOutputPreview::new(
terminal.clone().into_any_element(),
terminal.entity_id(),
)
.with_total_lines(self.content_line_count)
.toggle_state(!terminal.read(cx).is_content_limited(window))
.on_toggle({
let terminal = terminal.clone();
move |is_expanded, _, cx| {
terminal.update(cx, |terminal, cx| {
terminal.set_embedded_mode(
if is_expanded {
None
} else {
Some(COLLAPSED_LINES)
},
cx,
);
});
}
}),
),
.child({
let content_mode = terminal.read(cx).content_mode(window, cx);
if content_mode.is_scrollable() {
div().h_72().child(terminal.clone()).into_any_element()
} else {
ToolOutputPreview::new(
terminal.clone().into_any_element(),
terminal.entity_id(),
)
.with_total_lines(self.content_line_count)
.toggle_state(!content_mode.is_limited())
.on_toggle({
let terminal = terminal.clone();
move |is_expanded, _, cx| {
terminal.update(cx, |terminal, cx| {
terminal.set_embedded_mode(
if is_expanded {
None
} else {
Some(COLLAPSED_LINES)
},
cx,
);
});
}
})
.into_any_element()
}
}),
)
},
)

View File

@@ -452,6 +452,10 @@ impl Model {
| Model::Claude3_5SonnetV2
| Model::Claude3_7Sonnet
| Model::Claude3_7SonnetThinking
| Model::ClaudeSonnet4
| Model::ClaudeSonnet4Thinking
| Model::ClaudeOpus4
| Model::ClaudeOpus4Thinking
| Model::Claude3Haiku
| Model::Claude3Opus
| Model::Claude3Sonnet

View File

@@ -501,8 +501,10 @@ impl Database {
/// Returns all channels for the user with the given ID.
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
self.transaction(|tx| async move { self.get_user_channels(user_id, None, true, &tx).await })
.await
self.weak_transaction(
|tx| async move { self.get_user_channels(user_id, None, true, &tx).await },
)
.await
}
/// Returns all channels for the user with the given ID that are descendants

View File

@@ -15,7 +15,7 @@ impl Database {
user_b_busy: bool,
}
self.transaction(|tx| async move {
self.weak_transaction(|tx| async move {
let user_a_participant = Alias::new("user_a_participant");
let user_b_participant = Alias::new("user_b_participant");
let mut db_contacts = contact::Entity::find()
@@ -91,7 +91,7 @@ impl Database {
/// Returns whether the given user is a busy (on a call).
pub async fn is_user_busy(&self, user_id: UserId) -> Result<bool> {
self.transaction(|tx| async move {
self.weak_transaction(|tx| async move {
let participant = room_participant::Entity::find()
.filter(room_participant::Column::UserId.eq(user_id))
.one(&*tx)

View File

@@ -80,7 +80,7 @@ impl Database {
&self,
user_id: UserId,
) -> Result<Option<proto::IncomingCall>> {
self.transaction(|tx| async move {
self.weak_transaction(|tx| async move {
let pending_participant = room_participant::Entity::find()
.filter(
room_participant::Column::UserId

View File

@@ -7,6 +7,12 @@ pub use token::*;
pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial";
/// The name of the feature flag that bypasses the account age check.
pub const BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG: &str = "bypass-account-age-check";
/// The minimum account age an account must have in order to use the LLM service.
pub const MIN_ACCOUNT_AGE_FOR_LLM_USE: chrono::Duration = chrono::Duration::days(30);
/// The default value to use for maximum spend per month if the user did not
/// explicitly set a maximum spend.
///

View File

@@ -1,6 +1,6 @@
use crate::db::billing_subscription::SubscriptionKind;
use crate::db::{billing_customer, billing_subscription, user};
use crate::llm::AGENT_EXTENDED_TRIAL_FEATURE_FLAG;
use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG};
use crate::{Config, db::billing_preference};
use anyhow::{Context as _, Result};
use chrono::{NaiveDateTime, Utc};
@@ -84,7 +84,7 @@ impl LlmTokenClaims {
.any(|flag| flag == "llm-closed-beta"),
bypass_account_age_check: feature_flags
.iter()
.any(|flag| flag == "bypass-account-age-check"),
.any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG),
can_use_web_search_tool: true,
use_llm_request_queue: feature_flags.iter().any(|flag| flag == "llm-request-queue"),
plan,

View File

@@ -4,7 +4,10 @@ use crate::api::billing::find_or_create_billing_customer;
use crate::api::{CloudflareIpCountryHeader, SystemIdHeader};
use crate::db::billing_subscription::SubscriptionKind;
use crate::llm::db::LlmDatabase;
use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, LlmTokenClaims};
use crate::llm::{
AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG, LlmTokenClaims,
MIN_ACCOUNT_AGE_FOR_LLM_USE,
};
use crate::stripe_client::StripeCustomerId;
use crate::{
AppState, Error, Result, auth,
@@ -65,7 +68,7 @@ use std::{
rc::Rc,
sync::{
Arc, OnceLock,
atomic::{AtomicBool, Ordering::SeqCst},
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
},
time::{Duration, Instant},
};
@@ -86,10 +89,36 @@ pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(15);
const MESSAGE_COUNT_PER_PAGE: usize = 100;
const MAX_MESSAGE_LEN: usize = 1024;
const NOTIFICATION_COUNT_PER_PAGE: usize = 50;
const MAX_CONCURRENT_CONNECTIONS: usize = 512;
static CONCURRENT_CONNECTIONS: AtomicUsize = AtomicUsize::new(0);
type MessageHandler =
Box<dyn Send + Sync + Fn(Box<dyn AnyTypedEnvelope>, Session) -> BoxFuture<'static, ()>>;
pub struct ConnectionGuard;
impl ConnectionGuard {
pub fn try_acquire() -> Result<Self, ()> {
let current_connections = CONCURRENT_CONNECTIONS.fetch_add(1, SeqCst);
if current_connections >= MAX_CONCURRENT_CONNECTIONS {
CONCURRENT_CONNECTIONS.fetch_sub(1, SeqCst);
tracing::error!(
"too many concurrent connections: {}",
current_connections + 1
);
return Err(());
}
Ok(ConnectionGuard)
}
}
impl Drop for ConnectionGuard {
fn drop(&mut self) {
CONCURRENT_CONNECTIONS.fetch_sub(1, SeqCst);
}
}
struct Response<R> {
peer: Arc<Peer>,
receipt: Receipt<R>,
@@ -722,6 +751,7 @@ impl Server {
system_id: Option<String>,
send_connection_id: Option<oneshot::Sender<ConnectionId>>,
executor: Executor,
connection_guard: Option<ConnectionGuard>,
) -> impl Future<Output = ()> + use<> {
let this = self.clone();
let span = info_span!("handle connection", %address,
@@ -742,6 +772,7 @@ impl Server {
tracing::error!("server is tearing down");
return
}
let (connection_id, handle_io, mut incoming_rx) = this
.peer
.add_connection(connection, {
@@ -783,6 +814,7 @@ impl Server {
tracing::error!(?error, "failed to send initial client update");
return;
}
drop(connection_guard);
let handle_io = handle_io.fuse();
futures::pin_mut!(handle_io);
@@ -1154,6 +1186,19 @@ pub async fn handle_websocket_request(
}
let socket_address = socket_address.to_string();
// Acquire connection guard before WebSocket upgrade
let connection_guard = match ConnectionGuard::try_acquire() {
Ok(guard) => guard,
Err(()) => {
return (
StatusCode::SERVICE_UNAVAILABLE,
"Too many concurrent connections",
)
.into_response();
}
};
ws.on_upgrade(move |socket| {
let socket = socket
.map_ok(to_tungstenite_message)
@@ -1171,6 +1216,7 @@ pub async fn handle_websocket_request(
system_id_header.map(|header| header.to_string()),
None,
Executor::Production,
Some(connection_guard),
)
.await;
}
@@ -2773,8 +2819,12 @@ async fn make_update_user_plan_message(
(None, None)
};
let account_too_young =
!matches!(plan, proto::Plan::ZedPro) && user.account_age() < MIN_ACCOUNT_AGE_FOR_LLM_USE;
let bypass_account_age_check = feature_flags
.iter()
.any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG);
let account_too_young = !matches!(plan, proto::Plan::ZedPro)
&& !bypass_account_age_check
&& user.account_age() < MIN_ACCOUNT_AGE_FOR_LLM_USE;
Ok(proto::UpdateUserPlan {
plan: plan.into(),
@@ -4075,9 +4125,6 @@ async fn accept_terms_of_service(
Ok(())
}
/// The minimum account age an account must have in order to use the LLM service.
pub const MIN_ACCOUNT_AGE_FOR_LLM_USE: chrono::Duration = chrono::Duration::days(30);
async fn get_llm_api_token(
_request: proto::GetLlmToken,
response: Response<proto::GetLlmToken>,

View File

@@ -258,6 +258,7 @@ impl TestServer {
None,
Some(connection_id_tx),
Executor::Deterministic(cx.background_executor().clone()),
None,
))
.detach();
let connection_id = connection_id_rx.await.map_err(|e| {

View File

@@ -28,7 +28,7 @@ use settings::SettingsStore;
use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace};
use std::{
any::TypeId,
env::home_dir,
env,
ffi::OsString,
mem,
ops::Range,
@@ -486,14 +486,11 @@ impl Copilot {
env,
};
let root_path = home_dir();
let root_path = root_path.as_deref().unwrap_or_else(|| {
if cfg!(target_os = "windows") {
Path::new("C:/")
} else {
Path::new("/")
}
});
let root_path = if cfg!(target_os = "windows") {
Path::new("C:/")
} else {
Path::new("/")
};
let server_name = LanguageServerName("copilot".into());
let server = LanguageServer::new(

View File

@@ -23,6 +23,7 @@ doctest = false
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
collections.workspace = true
dap.workspace = true
futures.workspace = true
gpui.workspace = true

View File

@@ -1,16 +1,18 @@
use anyhow::Result;
use anyhow::{Result, bail};
use async_trait::async_trait;
use collections::FxHashMap;
use dap::{
DebugRequest, StartDebuggingRequestArguments,
DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
adapters::{
DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
},
};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::path::PathBuf;
use std::sync::Arc;
use std::{ffi::OsStr, sync::Arc};
use task::{DebugScenario, ZedDebugConfig};
use util::command::new_smol_command;
@@ -21,6 +23,18 @@ impl RubyDebugAdapter {
const ADAPTER_NAME: &'static str = "Ruby";
}
#[derive(Serialize, Deserialize)]
struct RubyDebugConfig {
script_or_command: Option<String>,
script: Option<String>,
command: Option<String>,
#[serde(default)]
args: Vec<String>,
#[serde(default)]
env: FxHashMap<String, String>,
cwd: Option<PathBuf>,
}
#[async_trait(?Send)]
impl DebugAdapter for RubyDebugAdapter {
fn name(&self) -> DebugAdapterName {
@@ -31,185 +45,70 @@ impl DebugAdapter for RubyDebugAdapter {
Some(SharedString::new_static("Ruby").into())
}
fn request_kind(&self, _: &serde_json::Value) -> Result<StartDebuggingRequestArgumentsRequest> {
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
async fn dap_schema(&self) -> serde_json::Value {
json!({
"oneOf": [
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["launch"],
"description": "Request to launch a new process"
}
}
},
{
"type": "object",
"required": ["script"],
"properties": {
"command": {
"type": "string",
"description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
"default": "ruby"
},
"script": {
"type": "string",
"description": "Absolute path to a Ruby file."
},
"cwd": {
"type": "string",
"description": "Directory to execute the program in",
"default": "${ZED_WORKTREE_ROOT}"
},
"args": {
"type": "array",
"description": "Command line arguments passed to the program",
"items": {
"type": "string"
},
"default": []
},
"env": {
"type": "object",
"description": "Additional environment variables to pass to the debugging (and debugged) process",
"default": {}
},
"showProtocolLog": {
"type": "boolean",
"description": "Show a log of DAP requests, events, and responses",
"default": false
},
"useBundler": {
"type": "boolean",
"description": "Execute Ruby programs with `bundle exec` instead of directly",
"default": false
},
"bundlePath": {
"type": "string",
"description": "Location of the bundle executable"
},
"rdbgPath": {
"type": "string",
"description": "Location of the rdbg executable"
},
"askParameters": {
"type": "boolean",
"description": "Ask parameters at first."
},
"debugPort": {
"type": "string",
"description": "UNIX domain socket name or TPC/IP host:port"
},
"waitLaunchTime": {
"type": "number",
"description": "Wait time before connection in milliseconds"
},
"localfs": {
"type": "boolean",
"description": "true if the VSCode and debugger run on a same machine",
"default": false
},
"useTerminal": {
"type": "boolean",
"description": "Create a new terminal and then execute commands there",
"default": false
}
}
}
]
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
},
{
"allOf": [
{
"type": "object",
"required": ["request"],
"properties": {
"request": {
"type": "string",
"enum": ["attach"],
"description": "Request to attach to an existing process"
}
}
},
{
"type": "object",
"properties": {
"rdbgPath": {
"type": "string",
"description": "Location of the rdbg executable"
},
"debugPort": {
"type": "string",
"description": "UNIX domain socket name or TPC/IP host:port"
},
"showProtocolLog": {
"type": "boolean",
"description": "Show a log of DAP requests, events, and responses",
"default": false
},
"localfs": {
"type": "boolean",
"description": "true if the VSCode and debugger run on a same machine",
"default": false
},
"localfsMap": {
"type": "string",
"description": "Specify pairs of remote root path and local root path like `/remote_dir:/local_dir`. You can specify multiple pairs like `/rem1:/loc1,/rem2:/loc2` by concatenating with `,`."
},
"env": {
"type": "object",
"description": "Additional environment variables to pass to the rdbg process",
"default": {}
}
}
}
]
}
]
"script": {
"type": "string",
"description": "Absolute path to a Ruby file."
},
"cwd": {
"type": "string",
"description": "Directory to execute the program in",
"default": "${ZED_WORKTREE_ROOT}"
},
"args": {
"type": "array",
"description": "Command line arguments passed to the program",
"items": {
"type": "string"
},
"default": []
},
"env": {
"type": "object",
"description": "Additional environment variables to pass to the debugging (and debugged) process",
"default": {}
},
}
})
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut config = serde_json::Map::new();
match &zed_scenario.request {
match zed_scenario.request {
DebugRequest::Launch(launch) => {
config.insert("request".to_string(), json!("launch"));
config.insert("script".to_string(), json!(launch.program));
config.insert("command".to_string(), json!("ruby"));
let config = RubyDebugConfig {
script_or_command: Some(launch.program),
script: None,
command: None,
args: launch.args,
env: launch.env,
cwd: launch.cwd.clone(),
};
if !launch.args.is_empty() {
config.insert("args".to_string(), json!(launch.args));
}
let config = serde_json::to_value(config)?;
if !launch.env.is_empty() {
config.insert("env".to_string(), json!(launch.env));
}
if let Some(cwd) = &launch.cwd {
config.insert("cwd".to_string(), json!(cwd));
}
// Ruby stops on entry so there's no need to handle that case
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
config,
tcp_connection: None,
build: None,
})
}
DebugRequest::Attach(attach) => {
config.insert("request".to_string(), json!("attach"));
config.insert("processId".to_string(), json!(attach.process_id));
DebugRequest::Attach(_) => {
anyhow::bail!("Attach requests are unsupported");
}
}
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
config: serde_json::Value::Object(config),
tcp_connection: None,
build: None,
})
}
async fn get_binary(
@@ -247,13 +146,34 @@ impl DebugAdapter for RubyDebugAdapter {
let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
let ruby_config = serde_json::from_value::<RubyDebugConfig>(definition.config.clone())?;
let arguments = vec![
let mut arguments = vec![
"--open".to_string(),
format!("--port={}", port),
format!("--host={}", host),
];
if let Some(script) = &ruby_config.script {
arguments.push(script.clone());
} else if let Some(command) = &ruby_config.command {
arguments.push("--command".to_string());
arguments.push(command.clone());
} else if let Some(command_or_script) = &ruby_config.script_or_command {
if delegate
.which(OsStr::new(&command_or_script))
.await
.is_some()
{
arguments.push("--command".to_string());
}
arguments.push(command_or_script.clone());
} else {
bail!("Ruby debug config must have 'script' or 'command' args");
}
arguments.extend(ruby_config.args);
Ok(DebugAdapterBinary {
command: rdbg_path.to_string_lossy().to_string(),
arguments,
@@ -262,8 +182,12 @@ impl DebugAdapter for RubyDebugAdapter {
port,
timeout,
}),
cwd: None,
envs: std::collections::HashMap::default(),
cwd: Some(
ruby_config
.cwd
.unwrap_or(delegate.worktree_root_path().to_owned()),
),
envs: ruby_config.env.into_iter().collect(),
request_args: StartDebuggingRequestArguments {
request: self.request_kind(&definition.config)?,
configuration: definition.config.clone(),

View File

@@ -5,8 +5,8 @@ use std::time::Duration;
use anyhow::{Context as _, Result, anyhow};
use dap::StackFrameId;
use gpui::{
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, MouseButton, ScrollStrategy,
Stateful, Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list,
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, MouseButton, Stateful,
Subscription, Task, WeakEntity, list,
};
use crate::StackTraceView;
@@ -35,7 +35,7 @@ pub struct StackFrameList {
selected_ix: Option<usize>,
opened_stack_frame_id: Option<StackFrameId>,
scrollbar_state: ScrollbarState,
scroll_handle: UniformListScrollHandle,
list_state: ListState,
_refresh_task: Task<()>,
}
@@ -54,7 +54,6 @@ impl StackFrameList {
cx: &mut Context<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
let scroll_handle = UniformListScrollHandle::new();
let _subscription =
cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
@@ -67,8 +66,16 @@ impl StackFrameList {
_ => {}
});
let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.), {
let this = cx.weak_entity();
move |ix, _window, cx| {
this.update(cx, |this, cx| this.render_entry(ix, cx))
.unwrap_or(div().into_any())
}
});
let scrollbar_state = ScrollbarState::new(list_state.clone());
let mut this = Self {
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
session,
workspace,
focus_handle,
@@ -77,7 +84,8 @@ impl StackFrameList {
entries: Default::default(),
selected_ix: None,
opened_stack_frame_id: None,
scroll_handle,
list_state,
scrollbar_state,
_refresh_task: Task::ready(()),
};
this.schedule_refresh(true, window, cx);
@@ -214,6 +222,7 @@ impl StackFrameList {
self.selected_ix = ix;
}
self.list_state.reset(self.entries.len());
cx.emit(StackFrameListEvent::BuiltEntries);
cx.notify();
}
@@ -555,10 +564,6 @@ impl StackFrameList {
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
self.selected_ix = ix;
if let Some(ix) = self.selected_ix {
self.scroll_handle
.scroll_to_item(ix, ScrollStrategy::Center);
}
cx.notify();
}
@@ -642,15 +647,8 @@ impl StackFrameList {
self.activate_selected_entry(window, cx);
}
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
uniform_list(
cx.entity(),
"stack-frame-list",
self.entries.len(),
|this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(),
)
.track_scroll(self.scroll_handle.clone())
.size_full()
fn render_list(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
list(self.list_state.clone()).size_full()
}
}

View File

@@ -698,7 +698,7 @@ impl EditorActionId {
// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range<Anchor>]>);
type GutterHighlight = (fn(&App) -> Hsla, Arc<[Range<Anchor>]>);
type GutterHighlight = (fn(&App) -> Hsla, Vec<Range<Anchor>>);
#[derive(Default)]
struct ScrollbarMarkerState {
@@ -923,7 +923,7 @@ enum SelectionDragState {
/// Represents a breakpoint indicator that shows up when hovering over lines in the gutter that don't have
/// a breakpoint on them.
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct PhantomBreakpointIndicator {
display_row: DisplayRow,
/// There's a small debounce between hovering over the line and showing the indicator.
@@ -931,6 +931,7 @@ struct PhantomBreakpointIndicator {
is_active: bool,
collides_with_existing_breakpoint: bool,
}
/// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`].
///
/// See the [module level documentation](self) for more information.
@@ -1199,10 +1200,12 @@ struct SelectionHistoryEntry {
add_selections_state: Option<AddSelectionsState>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum SelectionHistoryMode {
Normal,
Undoing,
Redoing,
Skipping,
}
#[derive(Clone, PartialEq, Eq, Hash)]
@@ -1236,11 +1239,19 @@ struct SelectionHistory {
}
impl SelectionHistory {
#[track_caller]
fn insert_transaction(
&mut self,
transaction_id: TransactionId,
selections: Arc<[Selection<Anchor>]>,
) {
if selections.is_empty() {
log::error!(
"SelectionHistory::insert_transaction called with empty selections. Caller: {}",
std::panic::Location::caller()
);
return;
}
self.selections_by_transaction
.insert(transaction_id, (selections, None));
}
@@ -1270,6 +1281,7 @@ impl SelectionHistory {
}
SelectionHistoryMode::Undoing => self.push_redo(entry),
SelectionHistoryMode::Redoing => self.push_undo(entry),
SelectionHistoryMode::Skipping => {}
}
}
}
@@ -2089,7 +2101,11 @@ impl Editor {
}
}
// skip adding the initial selection to selection history
editor.selection_history.mode = SelectionHistoryMode::Skipping;
editor.end_selection(window, cx);
editor.selection_history.mode = SelectionHistoryMode::Normal;
editor.scroll_manager.show_scrollbars(window, cx);
jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut editor, &buffer, cx);
@@ -14211,18 +14227,20 @@ impl Editor {
cx: &mut Context<Self>,
) {
self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction);
self.end_selection(window, cx);
self.selection_history.mode = SelectionHistoryMode::Undoing;
if let Some(entry) = self.selection_history.undo_stack.pop_back() {
self.change_selections(None, window, cx, |s| {
s.select_anchors(entry.selections.to_vec())
self.selection_history.mode = SelectionHistoryMode::Undoing;
self.with_selection_effects_deferred(window, cx, |this, window, cx| {
this.end_selection(window, cx);
this.change_selections(Some(Autoscroll::newest()), window, cx, |s| {
s.select_anchors(entry.selections.to_vec())
});
});
self.selection_history.mode = SelectionHistoryMode::Normal;
self.select_next_state = entry.select_next_state;
self.select_prev_state = entry.select_prev_state;
self.add_selections_state = entry.add_selections_state;
self.request_autoscroll(Autoscroll::newest(), cx);
}
self.selection_history.mode = SelectionHistoryMode::Normal;
}
pub fn redo_selection(
@@ -14232,18 +14250,20 @@ impl Editor {
cx: &mut Context<Self>,
) {
self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction);
self.end_selection(window, cx);
self.selection_history.mode = SelectionHistoryMode::Redoing;
if let Some(entry) = self.selection_history.redo_stack.pop_back() {
self.change_selections(None, window, cx, |s| {
s.select_anchors(entry.selections.to_vec())
self.selection_history.mode = SelectionHistoryMode::Redoing;
self.with_selection_effects_deferred(window, cx, |this, window, cx| {
this.end_selection(window, cx);
this.change_selections(Some(Autoscroll::newest()), window, cx, |s| {
s.select_anchors(entry.selections.to_vec())
});
});
self.selection_history.mode = SelectionHistoryMode::Normal;
self.select_next_state = entry.select_next_state;
self.select_prev_state = entry.select_prev_state;
self.add_selections_state = entry.add_selections_state;
self.request_autoscroll(Autoscroll::newest(), cx);
}
self.selection_history.mode = SelectionHistoryMode::Normal;
}
pub fn expand_excerpts(
@@ -18377,12 +18397,12 @@ impl Editor {
pub fn highlight_gutter<T: 'static>(
&mut self,
ranges: &[Range<Anchor>],
ranges: impl Into<Vec<Range<Anchor>>>,
color_fetcher: fn(&App) -> Hsla,
cx: &mut Context<Self>,
) {
self.gutter_highlights
.insert(TypeId::of::<T>(), (color_fetcher, Arc::from(ranges)));
.insert(TypeId::of::<T>(), (color_fetcher, ranges.into()));
cx.notify();
}
@@ -18394,6 +18414,65 @@ impl Editor {
self.gutter_highlights.remove(&TypeId::of::<T>())
}
pub fn insert_gutter_highlight<T: 'static>(
&mut self,
range: Range<Anchor>,
color_fetcher: fn(&App) -> Hsla,
cx: &mut Context<Self>,
) {
let snapshot = self.buffer().read(cx).snapshot(cx);
let mut highlights = self
.gutter_highlights
.remove(&TypeId::of::<T>())
.map(|(_, highlights)| highlights)
.unwrap_or_default();
let ix = highlights.binary_search_by(|highlight| {
Ordering::Equal
.then_with(|| highlight.start.cmp(&range.start, &snapshot))
.then_with(|| highlight.end.cmp(&range.end, &snapshot))
});
if let Err(ix) = ix {
highlights.insert(ix, range);
}
self.gutter_highlights
.insert(TypeId::of::<T>(), (color_fetcher, highlights));
}
pub fn remove_gutter_highlights<T: 'static>(
&mut self,
ranges_to_remove: Vec<Range<Anchor>>,
cx: &mut Context<Self>,
) {
let snapshot = self.buffer().read(cx).snapshot(cx);
let Some((color_fetcher, mut gutter_highlights)) =
self.gutter_highlights.remove(&TypeId::of::<T>())
else {
return;
};
let mut ranges_to_remove = ranges_to_remove.iter().peekable();
gutter_highlights.retain(|highlight| {
while let Some(range_to_remove) = ranges_to_remove.peek() {
match range_to_remove.end.cmp(&highlight.start, &snapshot) {
Ordering::Less | Ordering::Equal => {
ranges_to_remove.next();
}
Ordering::Greater => {
match range_to_remove.start.cmp(&highlight.end, &snapshot) {
Ordering::Less | Ordering::Equal => {
return false;
}
Ordering::Greater => break,
}
}
}
}
true
});
self.gutter_highlights
.insert(TypeId::of::<T>(), (color_fetcher, gutter_highlights));
}
#[cfg(feature = "test-support")]
pub fn all_text_background_highlights(
&self,
@@ -19939,12 +20018,15 @@ impl Editor {
if !selections.is_empty() {
let snapshot =
buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx));
// skip adding the initial selection to selection history
self.selection_history.mode = SelectionHistoryMode::Skipping;
self.change_selections(None, window, cx, |s| {
s.select_ranges(selections.into_iter().map(|(start, end)| {
snapshot.clip_offset(start, Bias::Left)
..snapshot.clip_offset(end, Bias::Right)
}));
});
self.selection_history.mode = SelectionHistoryMode::Normal;
}
};
}

View File

@@ -1907,7 +1907,6 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
])
});
editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
@@ -1927,29 +1926,29 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
editor.move_right(&MoveRight, window, cx);
editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
assert_selection_ranges(
"use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
"use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
editor,
cx,
);
editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
assert_selection_ranges(
"use std«ˇ::s»tr::{foo, bar}\n\n«ˇ {b»az.qux()}",
"use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
editor,
cx,
);
editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
assert_selection_ranges(
"use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
"use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
editor,
cx,
);
@@ -21942,7 +21941,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
.expect("created a singleton buffer")
.read(cx)
.remote_id();
let buffer_result_id = project.lsp_store().read(cx).result_id(buffer_id);
let buffer_result_id = project.lsp_store().read(cx).result_id(buffer_id, cx);
assert_eq!(expected, buffer_result_id);
});
};
@@ -21988,7 +21987,6 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
"Cursor movement should not trigger diagnostic request"
);
ensure_result_id(Some("2".to_string()), cx);
// Multiple rapid edits should be debounced
for _ in 0..5 {
editor.update_in(cx, |editor, window, cx| {

View File

@@ -1031,7 +1031,7 @@ impl EditorElement {
editor.set_gutter_hovered(gutter_hovered, cx);
editor.mouse_cursor_hidden = false;
if gutter_hovered {
let breakpoint_indicator = if gutter_hovered {
let new_point = position_map
.point_for_position(event.position)
.previous_valid;
@@ -1045,7 +1045,6 @@ impl EditorElement {
.buffer_for_excerpt(buffer_anchor.excerpt_id)
.and_then(|buffer| buffer.file().map(|file| (buffer, file)))
{
let was_hovered = editor.gutter_breakpoint_indicator.0.is_some();
let as_point = text::ToPoint::to_point(&buffer_anchor.text_anchor, buffer_snapshot);
let is_visible = editor
@@ -1073,38 +1072,43 @@ impl EditorElement {
.is_some()
});
editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
display_row: new_point.row(),
is_active: is_visible,
collides_with_existing_breakpoint: has_existing_breakpoint,
});
editor.gutter_breakpoint_indicator.1.get_or_insert_with(|| {
cx.spawn(async move |this, cx| {
if !was_hovered {
if !is_visible {
editor.gutter_breakpoint_indicator.1.get_or_insert_with(|| {
cx.spawn(async move |this, cx| {
cx.background_executor()
.timer(Duration::from_millis(200))
.await;
}
this.update(cx, |this, cx| {
if let Some(indicator) = this.gutter_breakpoint_indicator.0.as_mut() {
indicator.is_active = true;
}
cx.notify();
this.update(cx, |this, cx| {
if let Some(indicator) = this.gutter_breakpoint_indicator.0.as_mut()
{
indicator.is_active = true;
cx.notify();
}
})
.ok();
})
.ok();
})
});
});
}
Some(PhantomBreakpointIndicator {
display_row: new_point.row(),
is_active: is_visible,
collides_with_existing_breakpoint: has_existing_breakpoint,
})
} else {
editor.gutter_breakpoint_indicator = (None, None);
editor.gutter_breakpoint_indicator.1 = None;
None
}
} else {
editor.gutter_breakpoint_indicator = (None, None);
}
editor.gutter_breakpoint_indicator.1 = None;
None
};
cx.notify();
if &breakpoint_indicator != &editor.gutter_breakpoint_indicator.0 {
editor.gutter_breakpoint_indicator.0 = breakpoint_indicator;
cx.notify();
}
// Don't trigger hover popover if mouse is hovering over context menu
if text_hitbox.is_hovered(window) {
@@ -7325,6 +7329,17 @@ impl LineWithInvisibles {
paint(window, cx);
}),
ShowWhitespaceSetting::Trailing => {
let mut previous_start = self.len;
for ([start, end], paint) in invisible_iter.rev() {
if previous_start != end {
break;
}
previous_start = start;
paint(window, cx);
}
}
// For a whitespace to be on a boundary, any of the following conditions need to be met:
// - It is a tab
// - It is adjacent to an edge (start or end)

View File

@@ -266,10 +266,11 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa
let mut is_first_iteration = true;
find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
// Make alt-left skip punctuation on Mac OS to respect Mac VSCode behaviour. For example: hello.| goes to |hello.
// Make alt-left skip punctuation to respect VSCode behaviour. For example: hello.| goes to |hello.
if is_first_iteration
&& classifier.is_punctuation(right)
&& !classifier.is_punctuation(left)
&& left != '\n'
{
is_first_iteration = false;
return false;
@@ -318,10 +319,11 @@ pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
let mut is_first_iteration = true;
find_boundary(map, point, FindRange::MultiLine, |left, right| {
// Make alt-right skip punctuation on Mac OS to respect the Mac behaviour. For example: |.hello goes to .hello|
// Make alt-right skip punctuation to respect VSCode behaviour. For example: |.hello goes to .hello|
if is_first_iteration
&& classifier.is_punctuation(left)
&& !classifier.is_punctuation(right)
&& right != '\n'
{
is_first_iteration = false;
return false;

View File

@@ -240,7 +240,8 @@ 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 keystroke = Keystroke::parse(keystroke_text).unwrap();
let keyboard_mapper = self.keyboard_mapper();
let keystroke = Keystroke::parse(keystroke_text, keyboard_mapper.as_ref()).unwrap();
self.cx.dispatch_keystroke(self.window, keystroke);
}

View File

@@ -248,6 +248,8 @@ fn conflicts_updated(
removed_block_ids.insert(block_id);
}
editor.remove_gutter_highlights::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx);
editor
@@ -325,8 +327,7 @@ fn update_conflict_highlighting(
cx: &mut Context<Editor>,
) {
log::debug!("update conflict highlighting for {conflict:?}");
let theme = cx.theme().clone();
let colors = theme.colors();
let outer_start = buffer
.anchor_in_excerpt(excerpt_id, conflict.range.start)
.unwrap();
@@ -346,26 +347,29 @@ fn update_conflict_highlighting(
.anchor_in_excerpt(excerpt_id, conflict.theirs.end)
.unwrap();
let ours_background = colors.version_control_conflict_ours_background;
let ours_marker = colors.version_control_conflict_ours_marker_background;
let theirs_background = colors.version_control_conflict_theirs_background;
let theirs_marker = colors.version_control_conflict_theirs_marker_background;
let divider_background = colors.version_control_conflict_divider_background;
let ours_background = cx.theme().colors().version_control_conflict_marker_ours;
let theirs_background = cx.theme().colors().version_control_conflict_marker_theirs;
let options = RowHighlightOptions {
include_gutter: false,
include_gutter: true,
..Default::default()
};
editor.insert_gutter_highlight::<ConflictsOuter>(
outer_start..their_end,
|cx| cx.theme().colors().editor_background,
cx,
);
// Prevent diff hunk highlighting within the entire conflict region.
editor.highlight_rows::<ConflictsOuter>(
outer_start..outer_end,
divider_background,
editor.highlight_rows::<ConflictsOuter>(outer_start..outer_end, theirs_background, options, cx);
editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
editor.highlight_rows::<ConflictsOursMarker>(
outer_start..our_start,
ours_background,
options,
cx,
);
editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
editor.highlight_rows::<ConflictsOursMarker>(outer_start..our_start, ours_marker, options, cx);
editor.highlight_rows::<ConflictsTheirs>(
their_start..their_end,
theirs_background,
@@ -374,7 +378,7 @@ fn update_conflict_highlighting(
);
editor.highlight_rows::<ConflictsTheirsMarker>(
their_end..outer_end,
theirs_marker,
theirs_background,
options,
cx,
);
@@ -512,6 +516,9 @@ pub(crate) fn resolve_conflict(
let end = snapshot
.anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
.unwrap();
editor.remove_gutter_highlights::<ConflictsOuter>(vec![start..end], cx);
editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);

View File

@@ -37,10 +37,10 @@ use crate::{
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window,
WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, 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,6 +262,7 @@ 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)>>>,
@@ -308,6 +309,7 @@ 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 {
@@ -333,6 +335,7 @@ 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(),
@@ -369,6 +372,7 @@ 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()
@@ -413,6 +417,11 @@ 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

@@ -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, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform,
TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, WindowBounds,
WindowHandle, WindowOptions,
Platform, PlatformKeyboardMapper, 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,14 +397,20 @@ 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(Keystroke::parse)
.map(|source| Keystroke::parse(source, keyboard_mapper.as_ref()))
.map(Result::unwrap)
{
self.dispatch_keystroke(window, keystroke);
@@ -418,7 +424,12 @@ 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) {
for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) {
let keyboard_mapper = self.keyboard_mapper();
for keystroke in input
.split("")
.map(|source| Keystroke::parse(source, keyboard_mapper.as_ref()))
.map(Result::unwrap)
{
self.dispatch_keystroke(window, keystroke);
}

View File

@@ -538,8 +538,22 @@ mod test {
})
.unwrap();
cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap());
cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").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,
},
);
window
.update(cx, |test_view, _, _| {

View File

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

View File

@@ -2,7 +2,10 @@ use std::rc::Rc;
use collections::HashMap;
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke};
use crate::{
Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, PlatformKeyboardMapper,
TestKeyboardMapper,
};
use smallvec::SmallVec;
/// A keybinding and its associated metadata, from the keymap.
@@ -25,12 +28,20 @@ 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).unwrap()
Self::load(
keystrokes,
Box::new(action),
context_predicate,
None,
&keyboard_mapper,
)
.unwrap()
}
/// Load a keybinding from the given raw data.
@@ -39,10 +50,11 @@ 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(Keystroke::parse)
.map(|source| Keystroke::parse(source, keyboard_mapper))
.collect::<std::result::Result<_, _>>()?;
if let Some(equivalents) = key_equivalents {

View File

@@ -1,5 +1,6 @@
mod app_menu;
mod keyboard;
mod keycode;
mod keystroke;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
@@ -66,6 +67,7 @@ use uuid::Uuid;
pub use app_menu::*;
pub use keyboard::*;
pub use keycode::*;
pub use keystroke::*;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
@@ -194,7 +196,6 @@ 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>> {
@@ -214,7 +215,6 @@ 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,6 +235,10 @@ 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

@@ -1,3 +1,7 @@
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
@@ -5,3 +9,109 @@ 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

@@ -0,0 +1,590 @@
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,9 +1,13 @@
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)]
@@ -93,7 +97,10 @@ 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) -> std::result::Result<Self, InvalidKeystrokeError> {
pub fn parse(
source: &str,
keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> std::result::Result<Self, InvalidKeystrokeError> {
let mut modifiers = Modifiers::none();
let mut key = None;
let mut key_char = None;
@@ -184,9 +191,24 @@ impl Keystroke {
}
});
let key = key.ok_or_else(|| InvalidKeystrokeError {
// Create error once for reuse
let error = || 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,4 +1,20 @@
use crate::PlatformKeyboardLayout;
#[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;
pub(crate) struct LinuxKeyboardLayout {
id: String,
@@ -19,3 +35,257 @@ 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,8 +25,9 @@ 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, PlatformTextSystem, PlatformWindow,
Point, Result, ScreenCaptureSource, Task, WindowAppearance, WindowParams, px,
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper,
PlatformTextSystem, PlatformWindow, Point, Result, ScreenCaptureSource, Task, WindowAppearance,
WindowParams, px,
};
#[cfg(any(feature = "wayland", feature = "x11"))]
@@ -138,6 +139,10 @@ 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,21 +1,14 @@
use crate::{
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,
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,
};
use cocoa::{
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
base::{YES, id},
};
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};
use std::borrow::Cow;
const BACKSPACE_KEY: u16 = 0x7f;
const SPACE_KEY: u16 = b' ' as u16;
@@ -452,80 +445,3 @@ 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,8 +1,14 @@
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::PlatformKeyboardLayout;
use crate::{
Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper, ScanCode, is_letter_key,
platform::mac::{LMGetKbdType, UCKeyTranslate, kTISPropertyUnicodeKeyLayoutData},
};
use super::{
TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, kTISPropertyInputSourceID,
@@ -47,3 +53,300 @@ 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,9 +7,10 @@ use super::{
use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
MacDisplay, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, ScreenCaptureSource,
SemanticVersion, Task, WindowAppearance, WindowParams, hash,
MacDisplay, MacKeyboardMapper, MacWindow, Menu, MenuItem, PathPromptOptions, Platform,
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
PlatformWindow, Result, ScreenCaptureSource, SemanticVersion, Task, WindowAppearance,
WindowParams, hash,
};
use anyhow::{Context as _, anyhow};
use block::ConcreteBlock;
@@ -846,6 +847,10 @@ 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,8 +1,9 @@
use crate::{
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
Size, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
PlatformKeyboardMapper, PlatformTextSystem, PromptButton, ScreenCaptureFrame,
ScreenCaptureSource, ScreenCaptureStream, Size, Task, TestDisplay, TestKeyboardMapper,
TestWindow, WindowAppearance, WindowParams, size,
};
use anyhow::Result;
use collections::VecDeque;
@@ -223,6 +224,10 @@ 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

@@ -702,7 +702,7 @@ fn handle_ime_composition_inner(
} else {
if lparam & GCS_COMPSTR.0 > 0 {
let comp_string = parse_ime_composition_string(ctx, GCS_COMPSTR)?;
let caret_pos = (lparam & GCS_CURSORPOS.0 > 0).then(|| {
let caret_pos = (!comp_string.is_empty() && lparam & GCS_CURSORPOS.0 > 0).then(|| {
let pos = retrieve_composition_cursor_position(ctx);
pos..pos
});

View File

@@ -1,16 +1,16 @@
use anyhow::Result;
use anyhow::{Context, Result};
use windows::Win32::UI::{
Input::KeyboardAndMouse::{
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,
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,
},
WindowsAndMessaging::KL_NAMELENGTH,
};
use windows_core::HSTRING;
use crate::{Modifiers, PlatformKeyboardLayout};
use crate::{Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper, ScanCode};
pub(crate) struct WindowsKeyboardLayout {
id: String,
@@ -48,6 +48,29 @@ 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,
@@ -82,15 +105,15 @@ fn need_to_convert_to_shifted_key(vkey: VIRTUAL_KEY) -> bool {
| VK_OEM_MINUS
| VK_OEM_PLUS
| VK_OEM_4
| VK_OEM_5
| VK_OEM_6
| VK_OEM_5
| VK_OEM_1
| VK_OEM_7
| VK_OEM_COMMA
| VK_OEM_PERIOD
| VK_OEM_2
| VK_OEM_102
| VK_OEM_8
| VK_OEM_8 // Same as VK_OEM_2
| VK_ABNT_C1
| VK_0
| VK_1
@@ -138,3 +161,66 @@ 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

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

@@ -765,6 +765,8 @@ pub enum ShowWhitespaceSetting {
/// - It is adjacent to an edge (start or end)
/// - It is adjacent to a whitespace (left or right)
Boundary,
/// Draw whitespaces only after non-whitespace characters.
Trailing,
}
/// Controls which formatter should be used when formatting code.
@@ -1452,7 +1454,8 @@ impl settings::Settings for AllLanguageSettings {
vscode.bool_setting("editor.inlineSuggest.enabled", &mut d.show_edit_predictions);
vscode.enum_setting("editor.renderWhitespace", &mut d.show_whitespaces, |s| {
Some(match s {
"boundary" | "trailing" => ShowWhitespaceSetting::Boundary,
"boundary" => ShowWhitespaceSetting::Boundary,
"trailing" => ShowWhitespaceSetting::Trailing,
"selection" => ShowWhitespaceSetting::Selection,
"all" => ShowWhitespaceSetting::All,
_ => ShowWhitespaceSetting::None,

View File

@@ -185,6 +185,7 @@ impl LanguageModel for FakeLanguageModel {
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
> {
let (tx, rx) = mpsc::unbounded();

View File

@@ -22,6 +22,7 @@ use std::fmt;
use std::ops::{Add, Sub};
use std::str::FromStr as _;
use std::sync::Arc;
use std::time::Duration;
use thiserror::Error;
use util::serde::is_default;
use zed_llm_client::{
@@ -74,6 +75,8 @@ pub enum LanguageModelCompletionEvent {
#[derive(Error, Debug)]
pub enum LanguageModelCompletionError {
#[error("rate limit exceeded, retry after {0:?}")]
RateLimit(Duration),
#[error("received bad input JSON")]
BadInputJson {
id: LanguageModelToolUseId,
@@ -270,6 +273,7 @@ pub trait LanguageModel: Send + Sync {
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
>;
@@ -277,7 +281,7 @@ pub trait LanguageModel: Send + Sync {
&self,
request: LanguageModelRequest,
cx: &AsyncApp,
) -> BoxFuture<'static, Result<LanguageModelTextStream>> {
) -> BoxFuture<'static, Result<LanguageModelTextStream, LanguageModelCompletionError>> {
let future = self.stream_completion(request, cx);
async move {

View File

@@ -1,4 +1,3 @@
use anyhow::Result;
use futures::Stream;
use smol::lock::{Semaphore, SemaphoreGuardArc};
use std::{
@@ -8,6 +7,8 @@ use std::{
task::{Context, Poll},
};
use crate::LanguageModelCompletionError;
#[derive(Clone)]
pub struct RateLimiter {
semaphore: Arc<Semaphore>,
@@ -36,9 +37,12 @@ impl RateLimiter {
}
}
pub fn run<'a, Fut, T>(&self, future: Fut) -> impl 'a + Future<Output = Result<T>>
pub fn run<'a, Fut, T>(
&self,
future: Fut,
) -> impl 'a + Future<Output = Result<T, LanguageModelCompletionError>>
where
Fut: 'a + Future<Output = Result<T>>,
Fut: 'a + Future<Output = Result<T, LanguageModelCompletionError>>,
{
let guard = self.semaphore.acquire_arc();
async move {
@@ -52,9 +56,12 @@ impl RateLimiter {
pub fn stream<'a, Fut, T>(
&self,
future: Fut,
) -> impl 'a + Future<Output = Result<impl Stream<Item = T::Item> + use<Fut, T>>>
) -> impl 'a
+ Future<
Output = Result<impl Stream<Item = T::Item> + use<Fut, T>, LanguageModelCompletionError>,
>
where
Fut: 'a + Future<Output = Result<T>>,
Fut: 'a + Future<Output = Result<T, LanguageModelCompletionError>>,
T: Stream,
{
let guard = self.semaphore.acquire_arc();

View File

@@ -387,22 +387,34 @@ impl AnthropicModel {
&self,
request: anthropic::Request,
cx: &AsyncApp,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<anthropic::Event, AnthropicError>>>>
{
) -> BoxFuture<
'static,
Result<
BoxStream<'static, Result<anthropic::Event, AnthropicError>>,
LanguageModelCompletionError,
>,
> {
let http_client = self.http_client.clone();
let Ok((api_key, api_url)) = cx.read_entity(&self.state, |state, cx| {
let settings = &AllLanguageModelSettings::get_global(cx).anthropic;
(state.api_key.clone(), settings.api_url.clone())
}) else {
return futures::future::ready(Err(anyhow!("App state dropped"))).boxed();
return futures::future::ready(Err(anyhow!("App state dropped").into())).boxed();
};
async move {
let api_key = api_key.context("Missing Anthropic API Key")?;
let request =
anthropic::stream_completion(http_client.as_ref(), &api_url, &api_key, request);
request.await.context("failed to stream completion")
request.await.map_err(|err| match err {
AnthropicError::RateLimit(duration) => {
LanguageModelCompletionError::RateLimit(duration)
}
err @ (AnthropicError::ApiError(..) | AnthropicError::Other(..)) => {
LanguageModelCompletionError::Other(anthropic_err_to_anyhow(err))
}
})
}
.boxed()
}
@@ -473,6 +485,7 @@ impl LanguageModel for AnthropicModel {
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
> {
let request = into_anthropic(
@@ -484,12 +497,7 @@ impl LanguageModel for AnthropicModel {
);
let request = self.stream_completion(request, cx);
let future = self.request_limiter.stream(async move {
let response = request
.await
.map_err(|err| match err.downcast::<AnthropicError>() {
Ok(anthropic_err) => anthropic_err_to_anyhow(anthropic_err),
Err(err) => anyhow!(err),
})?;
let response = request.await?;
Ok(AnthropicEventMapper::new().map_stream(response))
});
async move { Ok(future.await?.boxed()) }.boxed()

View File

@@ -527,6 +527,7 @@ impl LanguageModel for BedrockModel {
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
> {
let Ok(region) = cx.read_entity(&self.state, |state, _cx| {
@@ -539,16 +540,13 @@ impl LanguageModel for BedrockModel {
.or(settings_region)
.unwrap_or(String::from("us-east-1"))
}) else {
return async move {
anyhow::bail!("App State Dropped");
}
.boxed();
return async move { Err(anyhow::anyhow!("App State Dropped").into()) }.boxed();
};
let model_id = match self.model.cross_region_inference_id(&region) {
Ok(s) => s,
Err(e) => {
return async move { Err(e) }.boxed();
return async move { Err(e.into()) }.boxed();
}
};
@@ -560,7 +558,7 @@ impl LanguageModel for BedrockModel {
self.model.mode(),
) {
Ok(request) => request,
Err(err) => return futures::future::ready(Err(err)).boxed(),
Err(err) => return futures::future::ready(Err(err.into())).boxed(),
};
let owned_handle = self.handler.clone();

View File

@@ -807,6 +807,7 @@ impl LanguageModel for CloudLanguageModel {
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
> {
let thread_id = request.thread_id.clone();
@@ -848,7 +849,8 @@ impl LanguageModel for CloudLanguageModel {
mode,
provider: zed_llm_client::LanguageModelProvider::Anthropic,
model: request.model.clone(),
provider_request: serde_json::to_value(&request)?,
provider_request: serde_json::to_value(&request)
.map_err(|e| anyhow!(e))?,
},
)
.await
@@ -884,7 +886,7 @@ impl LanguageModel for CloudLanguageModel {
let client = self.client.clone();
let model = match open_ai::Model::from_id(&self.model.id.0) {
Ok(model) => model,
Err(err) => return async move { Err(anyhow!(err)) }.boxed(),
Err(err) => return async move { Err(anyhow!(err).into()) }.boxed(),
};
let request = into_open_ai(request, &model, None);
let llm_api_token = self.llm_api_token.clone();
@@ -905,7 +907,8 @@ impl LanguageModel for CloudLanguageModel {
mode,
provider: zed_llm_client::LanguageModelProvider::OpenAi,
model: request.model.clone(),
provider_request: serde_json::to_value(&request)?,
provider_request: serde_json::to_value(&request)
.map_err(|e| anyhow!(e))?,
},
)
.await?;
@@ -944,7 +947,8 @@ impl LanguageModel for CloudLanguageModel {
mode,
provider: zed_llm_client::LanguageModelProvider::Google,
model: request.model.model_id.clone(),
provider_request: serde_json::to_value(&request)?,
provider_request: serde_json::to_value(&request)
.map_err(|e| anyhow!(e))?,
},
)
.await?;

View File

@@ -265,13 +265,15 @@ impl LanguageModel for CopilotChatLanguageModel {
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
> {
if let Some(message) = request.messages.last() {
if message.contents_empty() {
const EMPTY_PROMPT_MSG: &str =
"Empty prompts aren't allowed. Please provide a non-empty prompt.";
return futures::future::ready(Err(anyhow::anyhow!(EMPTY_PROMPT_MSG))).boxed();
return futures::future::ready(Err(anyhow::anyhow!(EMPTY_PROMPT_MSG).into()))
.boxed();
}
// Copilot Chat has a restriction that the final message must be from the user.
@@ -279,13 +281,13 @@ impl LanguageModel for CopilotChatLanguageModel {
// and provide a more helpful error message.
if !matches!(message.role, Role::User) {
const USER_ROLE_MSG: &str = "The final message must be from the user. To provide a system prompt, you must provide the system prompt followed by a user prompt.";
return futures::future::ready(Err(anyhow::anyhow!(USER_ROLE_MSG))).boxed();
return futures::future::ready(Err(anyhow::anyhow!(USER_ROLE_MSG).into())).boxed();
}
}
let copilot_request = match into_copilot_chat(&self.model, request) {
Ok(request) => request,
Err(err) => return futures::future::ready(Err(err)).boxed(),
Err(err) => return futures::future::ready(Err(err.into())).boxed(),
};
let is_streaming = copilot_request.stream;

View File

@@ -348,6 +348,7 @@ impl LanguageModel for DeepSeekLanguageModel {
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
> {
let request = into_deepseek(request, &self.model, self.max_output_tokens());

View File

@@ -409,6 +409,7 @@ impl LanguageModel for GoogleLanguageModel {
'static,
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
>,
LanguageModelCompletionError,
>,
> {
let request = into_google(

View File

@@ -420,6 +420,7 @@ impl LanguageModel for LmStudioLanguageModel {
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
> {
let request = self.to_lmstudio_request(request);

View File

@@ -364,6 +364,7 @@ impl LanguageModel for MistralLanguageModel {
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
> {
let request = into_mistral(

View File

@@ -406,6 +406,7 @@ impl LanguageModel for OllamaLanguageModel {
'static,
Result<
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
LanguageModelCompletionError,
>,
> {
let request = self.to_ollama_request(request);
@@ -415,7 +416,7 @@ impl LanguageModel for OllamaLanguageModel {
let settings = &AllLanguageModelSettings::get_global(cx).ollama;
settings.api_url.clone()
}) else {
return futures::future::ready(Err(anyhow!("App state dropped"))).boxed();
return futures::future::ready(Err(anyhow!("App state dropped").into())).boxed();
};
let future = self.request_limiter.stream(async move {

View File

@@ -339,6 +339,7 @@ impl LanguageModel for OpenAiLanguageModel {
'static,
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
>,
LanguageModelCompletionError,
>,
> {
let request = into_open_ai(request, &self.model, self.max_output_tokens());

View File

@@ -367,6 +367,7 @@ impl LanguageModel for OpenRouterLanguageModel {
'static,
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
>,
LanguageModelCompletionError,
>,
> {
let request = into_open_router(request, &self.model, self.max_output_tokens());

View File

@@ -82,7 +82,6 @@ text.workspace = true
toml.workspace = true
url.workspace = true
util.workspace = true
uuid.workspace = true
which.workspace = true
worktree.workspace = true
zlog.workspace = true

View File

@@ -1,17 +1,88 @@
use anyhow::Result;
use async_trait::async_trait;
use collections::FxHashMap;
use collections::HashMap;
use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
use gpui::SharedString;
use std::path::PathBuf;
use task::{
BuildTaskDefinition, DebugScenario, RevealStrategy, RevealTarget, Shell, SpawnInTerminal,
TaskTemplate,
};
use uuid::Uuid;
use serde::{Deserialize, Serialize};
use task::{DebugScenario, SpawnInTerminal, TaskTemplate};
pub(crate) struct GoLocator;
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
struct DelveLaunchRequest {
request: String,
mode: String,
program: String,
#[serde(skip_serializing_if = "Option::is_none")]
cwd: Option<String>,
args: Vec<String>,
build_flags: Vec<String>,
env: HashMap<String, String>,
}
fn is_debug_flag(arg: &str) -> Option<bool> {
let mut part = if let Some(suffix) = arg.strip_prefix("test.") {
suffix
} else {
arg
};
let mut might_have_arg = true;
if let Some(idx) = part.find('=') {
might_have_arg = false;
part = &part[..idx];
}
match part {
"benchmem" | "failfast" | "fullpath" | "fuzzworker" | "json" | "short" | "v"
| "paniconexit0" => Some(false),
"bench"
| "benchtime"
| "blockprofile"
| "blockprofilerate"
| "count"
| "coverprofile"
| "cpu"
| "cpuprofile"
| "fuzz"
| "fuzzcachedir"
| "fuzzminimizetime"
| "fuzztime"
| "gocoverdir"
| "list"
| "memprofile"
| "memprofilerate"
| "mutexprofile"
| "mutexprofilefraction"
| "outputdir"
| "parallel"
| "run"
| "shuffle"
| "skip"
| "testlogfile"
| "timeout"
| "trace" => Some(might_have_arg),
_ if arg.starts_with("test.") => Some(false),
_ => None,
}
}
fn is_build_flag(mut arg: &str) -> Option<bool> {
let mut might_have_arg = true;
if let Some(idx) = arg.find('=') {
might_have_arg = false;
arg = &arg[..idx];
}
match arg {
"a" | "n" | "race" | "msan" | "asan" | "cover" | "work" | "x" | "v" | "buildvcs"
| "json" | "linkshared" | "modcacherw" | "trimpath" => Some(false),
"p" | "covermode" | "coverpkg" | "asmflags" | "buildmode" | "compiler" | "gccgoflags"
| "gcflags" | "installsuffix" | "ldflags" | "mod" | "modfile" | "overlay" | "pgo"
| "pkgdir" | "tags" | "toolexec" => Some(might_have_arg),
_ => None,
}
}
#[async_trait]
impl DapLocator for GoLocator {
fn name(&self) -> SharedString {
@@ -32,78 +103,121 @@ impl DapLocator for GoLocator {
match go_action.as_str() {
"test" => {
let binary_path = format!("__debug_{}", Uuid::new_v4().simple());
let mut program = ".".to_string();
let mut args = Vec::default();
let mut build_flags = Vec::default();
let build_task = TaskTemplate {
label: "go test debug".into(),
command: "go".into(),
args: vec![
"test".into(),
"-c".into(),
"-gcflags \"all=-N -l\"".into(),
"-o".into(),
binary_path,
],
env: build_config.env.clone(),
let mut all_args_are_test = false;
let mut next_arg_is_test = false;
let mut next_arg_is_build = false;
let mut seen_pkg = false;
let mut seen_v = false;
for arg in build_config.args.iter().skip(1) {
if all_args_are_test || next_arg_is_test {
// HACK: tasks assume that they are run in a shell context,
// so the -run regex has escaped specials. Delve correctly
// handles escaping, so we undo that here.
if arg.starts_with("\\^") && arg.ends_with("\\$") {
let mut arg = arg[1..arg.len() - 2].to_string();
arg.push('$');
args.push(arg);
} else {
args.push(arg.clone());
}
next_arg_is_test = false;
} else if next_arg_is_build {
build_flags.push(arg.clone());
next_arg_is_build = false;
} else if arg.starts_with('-') {
let flag = arg.trim_start_matches('-');
if flag == "args" {
all_args_are_test = true;
} else if let Some(has_arg) = is_debug_flag(flag) {
if flag == "v" || flag == "test.v" {
seen_v = true;
}
if flag.starts_with("test.") {
args.push(arg.clone());
} else {
args.push(format!("-test.{flag}"))
}
next_arg_is_test = has_arg;
} else if let Some(has_arg) = is_build_flag(flag) {
build_flags.push(arg.clone());
next_arg_is_build = has_arg;
}
} else if !seen_pkg {
program = arg.clone();
seen_pkg = true;
} else {
args.push(arg.clone());
}
}
if !seen_v {
args.push("-test.v".to_string());
}
let config: serde_json::Value = serde_json::to_value(DelveLaunchRequest {
request: "launch".to_string(),
mode: "test".to_string(),
program,
args: args,
build_flags,
cwd: build_config.cwd.clone(),
use_new_terminal: false,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
reveal_target: RevealTarget::Dock,
hide: task::HideStrategy::Never,
shell: Shell::System,
tags: vec![],
show_summary: true,
show_command: true,
};
env: build_config.env.clone(),
})
.unwrap();
Some(DebugScenario {
label: resolved_label.to_string().into(),
adapter: adapter.0,
build: Some(BuildTaskDefinition::Template {
task_template: build_task,
locator_name: Some(self.name()),
}),
config: serde_json::Value::Null,
build: None,
config: config,
tcp_connection: None,
})
}
"run" => {
let program = build_config
.args
.get(1)
.cloned()
.unwrap_or_else(|| ".".to_string());
let mut next_arg_is_build = false;
let mut seen_pkg = false;
let build_task = TaskTemplate {
label: "go build debug".into(),
command: "go".into(),
args: vec![
"build".into(),
"-gcflags \"all=-N -l\"".into(),
program.clone(),
],
env: build_config.env.clone(),
let mut program = ".".to_string();
let mut args = Vec::default();
let mut build_flags = Vec::default();
for arg in build_config.args.iter().skip(1) {
if seen_pkg {
args.push(arg.clone())
} else if next_arg_is_build {
build_flags.push(arg.clone());
next_arg_is_build = false;
} else if arg.starts_with("-") {
if let Some(has_arg) = is_build_flag(arg.trim_start_matches("-")) {
next_arg_is_build = has_arg;
}
build_flags.push(arg.clone())
} else {
program = arg.to_string();
seen_pkg = true;
}
}
let config: serde_json::Value = serde_json::to_value(DelveLaunchRequest {
cwd: build_config.cwd.clone(),
use_new_terminal: false,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
reveal_target: RevealTarget::Dock,
hide: task::HideStrategy::Never,
shell: Shell::System,
tags: vec![],
show_summary: true,
show_command: true,
};
env: build_config.env.clone(),
request: "launch".to_string(),
mode: "debug".to_string(),
program,
args: args,
build_flags,
})
.unwrap();
Some(DebugScenario {
label: resolved_label.to_string().into(),
adapter: adapter.0,
build: Some(BuildTaskDefinition::Template {
task_template: build_task,
locator_name: Some(self.name()),
}),
config: serde_json::Value::Null,
build: None,
config,
tcp_connection: None,
})
}
@@ -111,113 +225,15 @@ impl DapLocator for GoLocator {
}
}
async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest> {
if build_config.args.is_empty() {
return Err(anyhow::anyhow!("Invalid Go command"));
}
let go_action = &build_config.args[0];
let cwd = build_config
.cwd
.as_ref()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| ".".to_string());
let mut env = FxHashMap::default();
for (key, value) in &build_config.env {
env.insert(key.clone(), value.clone());
}
match go_action.as_str() {
"test" => {
let binary_arg = build_config
.args
.get(4)
.ok_or_else(|| anyhow::anyhow!("can't locate debug binary"))?;
let program = PathBuf::from(&cwd)
.join(binary_arg)
.to_string_lossy()
.into_owned();
Ok(DebugRequest::Launch(task::LaunchRequest {
program,
cwd: Some(PathBuf::from(&cwd)),
args: vec!["-test.v".into(), "-test.run=${ZED_SYMBOL}".into()],
env,
}))
}
"build" => {
let package = build_config
.args
.get(2)
.cloned()
.unwrap_or_else(|| ".".to_string());
Ok(DebugRequest::Launch(task::LaunchRequest {
program: package,
cwd: Some(PathBuf::from(&cwd)),
args: vec![],
env,
}))
}
_ => Err(anyhow::anyhow!("Unsupported Go command: {}", go_action)),
}
async fn run(&self, _build_config: SpawnInTerminal) -> Result<DebugRequest> {
unreachable!()
}
}
#[cfg(test)]
mod tests {
use super::*;
use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskId, TaskTemplate};
#[test]
fn test_create_scenario_for_go_run() {
let locator = GoLocator;
let task = TaskTemplate {
label: "go run main.go".into(),
command: "go".into(),
args: vec!["run".into(), "main.go".into()],
env: Default::default(),
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
use_new_terminal: false,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
reveal_target: RevealTarget::Dock,
hide: HideStrategy::Never,
shell: Shell::System,
tags: vec![],
show_summary: true,
show_command: true,
};
let scenario =
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
assert!(scenario.is_some());
let scenario = scenario.unwrap();
assert_eq!(scenario.adapter, "Delve");
assert_eq!(scenario.label, "test label");
assert!(scenario.build.is_some());
if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
assert_eq!(task_template.command, "go");
assert!(task_template.args.contains(&"build".into()));
assert!(
task_template
.args
.contains(&"-gcflags \"all=-N -l\"".into())
);
assert!(task_template.args.contains(&"main.go".into()));
} else {
panic!("Expected BuildTaskDefinition::Template");
}
assert!(
scenario.config.is_null(),
"Initial config should be null to ensure it's invalid"
);
}
use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskTemplate};
#[test]
fn test_create_scenario_for_go_build() {
@@ -276,99 +292,106 @@ mod tests {
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
assert!(scenario.is_none());
}
#[test]
fn test_create_scenario_for_go_test() {
fn test_go_locator_run() {
let locator = GoLocator;
let delve = DebugAdapterName("Delve".into());
let task = TaskTemplate {
label: "go test".into(),
label: "go run with flags".into(),
command: "go".into(),
args: vec!["test".into(), ".".into()],
env: Default::default(),
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
use_new_terminal: false,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
reveal_target: RevealTarget::Dock,
hide: HideStrategy::Never,
shell: Shell::System,
tags: vec![],
show_summary: true,
show_command: true,
args: vec![
"run".to_string(),
"-race".to_string(),
"-ldflags".to_string(),
"-X main.version=1.0".to_string(),
"./cmd/myapp".to_string(),
"--config".to_string(),
"production.yaml".to_string(),
"--verbose".to_string(),
],
env: {
let mut env = HashMap::default();
env.insert("GO_ENV".to_string(), "production".to_string());
env
},
cwd: Some("/project/root".into()),
..Default::default()
};
let scenario =
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
let scenario = locator
.create_scenario(&task, "test run label", delve)
.unwrap();
assert!(scenario.is_some());
let scenario = scenario.unwrap();
assert_eq!(scenario.adapter, "Delve");
assert_eq!(scenario.label, "test label");
assert!(scenario.build.is_some());
let config: DelveLaunchRequest = serde_json::from_value(scenario.config).unwrap();
if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
assert_eq!(task_template.command, "go");
assert!(task_template.args.contains(&"test".into()));
assert!(task_template.args.contains(&"-c".into()));
assert!(
task_template
.args
.contains(&"-gcflags \"all=-N -l\"".into())
);
assert!(task_template.args.contains(&"-o".into()));
assert!(
task_template
.args
.iter()
.any(|arg| arg.starts_with("__debug_"))
);
} else {
panic!("Expected BuildTaskDefinition::Template");
}
assert!(
scenario.config.is_null(),
"Initial config should be null to ensure it's invalid"
assert_eq!(
config,
DelveLaunchRequest {
request: "launch".to_string(),
mode: "debug".to_string(),
program: "./cmd/myapp".to_string(),
build_flags: vec![
"-race".to_string(),
"-ldflags".to_string(),
"-X main.version=1.0".to_string()
],
args: vec![
"--config".to_string(),
"production.yaml".to_string(),
"--verbose".to_string(),
],
env: {
let mut env = HashMap::default();
env.insert("GO_ENV".to_string(), "production".to_string());
env
},
cwd: Some("/project/root".to_string()),
}
);
}
#[test]
fn test_create_scenario_for_go_test_with_cwd_binary() {
fn test_go_locator_test() {
let locator = GoLocator;
let delve = DebugAdapterName("Delve".into());
let task = TaskTemplate {
label: "go test".into(),
// Test with tags and run flag
let task_with_tags = TaskTemplate {
label: "test".into(),
command: "go".into(),
args: vec!["test".into(), ".".into()],
env: Default::default(),
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
use_new_terminal: false,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
reveal_target: RevealTarget::Dock,
hide: HideStrategy::Never,
shell: Shell::System,
tags: vec![],
show_summary: true,
show_command: true,
args: vec![
"test".to_string(),
"-tags".to_string(),
"integration,unit".to_string(),
"-run".to_string(),
"Foo".to_string(),
".".to_string(),
],
..Default::default()
};
let result = locator
.create_scenario(&task_with_tags, "", delve.clone())
.unwrap();
let scenario =
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
let config: DelveLaunchRequest = serde_json::from_value(result.config).unwrap();
assert!(scenario.is_some());
let scenario = scenario.unwrap();
if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
assert!(
task_template
.args
.iter()
.any(|arg| arg.starts_with("__debug_"))
);
} else {
panic!("Expected BuildTaskDefinition::Template");
}
assert_eq!(
config,
DelveLaunchRequest {
request: "launch".to_string(),
mode: "test".to_string(),
program: ".".to_string(),
build_flags: vec!["-tags".to_string(), "integration,unit".to_string(),],
args: vec![
"-test.run".to_string(),
"Foo".to_string(),
"-test.v".to_string()
],
env: HashMap::default(),
cwd: None,
}
);
}
#[test]
@@ -395,42 +418,4 @@ mod tests {
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
assert!(scenario.is_none());
}
#[test]
fn test_run_go_test_missing_binary_path() {
let locator = GoLocator;
let build_config = SpawnInTerminal {
id: TaskId("test_task".to_string()),
full_label: "go test".to_string(),
label: "go test".to_string(),
command: "go".into(),
args: vec![
"test".into(),
"-c".into(),
"-gcflags \"all=-N -l\"".into(),
"-o".into(),
], // Missing the binary path (arg 4)
command_label: "go test -c -gcflags \"all=-N -l\" -o".to_string(),
env: Default::default(),
cwd: Some(PathBuf::from("/test/path")),
use_new_terminal: false,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
reveal_target: RevealTarget::Dock,
hide: HideStrategy::Never,
shell: Shell::System,
show_summary: true,
show_command: true,
show_rerun: true,
};
let result = futures::executor::block_on(locator.run(build_config));
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("can't locate debug binary")
);
}
}

View File

@@ -249,7 +249,7 @@ async fn load_shell_environment(
use util::shell_env;
let dir_ = dir.to_owned();
let mut envs = match smol::unblock(move || shell_env::capture(Some(dir_))).await {
let mut envs = match smol::unblock(move || shell_env::capture(&dir_)).await {
Ok(envs) => envs,
Err(err) => {
util::log_err(&err);

View File

@@ -4050,7 +4050,7 @@ impl LspCommand for GetDocumentDiagnostics {
let buffer_id = buffer.update(&mut cx, |buffer, _| buffer.remote_id())?;
Ok(Self {
previous_result_id: lsp_store
.update(&mut cx, |lsp_store, _| lsp_store.result_id(buffer_id))?,
.update(&mut cx, |lsp_store, cx| lsp_store.result_id(buffer_id, cx))?,
})
}

View File

@@ -166,7 +166,7 @@ pub struct LocalLspStore {
_subscription: gpui::Subscription,
lsp_tree: Entity<LanguageServerTree>,
registered_buffers: HashMap<BufferId, usize>,
buffer_pull_diagnostics_result_ids: HashMap<BufferId, Option<String>>,
buffer_pull_diagnostics_result_ids: HashMap<PathBuf, Option<String>>,
}
impl LocalLspStore {
@@ -2295,8 +2295,11 @@ impl LocalLspStore {
let set = DiagnosticSet::new(sanitized_diagnostics, &snapshot);
buffer.update(cx, |buffer, cx| {
self.buffer_pull_diagnostics_result_ids
.insert(buffer.remote_id(), result_id);
if let Some(abs_path) = File::from_dyn(buffer.file()).map(|f| f.abs_path(cx)) {
self.buffer_pull_diagnostics_result_ids
.insert(abs_path, result_id);
}
buffer.update_diagnostics(server_id, set, cx)
});
@@ -3792,8 +3795,16 @@ impl LspStore {
}
}
BufferStoreEvent::BufferDropped(buffer_id) => {
let abs_path = self
.buffer_store
.read(cx)
.get(*buffer_id)
.and_then(|b| File::from_dyn(b.read(cx).file()))
.map(|f| f.abs_path(cx));
if let Some(local) = self.as_local_mut() {
local.buffer_pull_diagnostics_result_ids.remove(buffer_id);
if let Some(abs_path) = abs_path {
local.buffer_pull_diagnostics_result_ids.remove(&abs_path);
}
}
}
_ => {}
@@ -5745,7 +5756,7 @@ impl LspStore {
) -> Task<Result<Vec<LspPullDiagnostics>>> {
let buffer = buffer_handle.read(cx);
let buffer_id = buffer.remote_id();
let result_id = self.result_id(buffer_id);
let result_id = self.result_id(buffer_id, cx);
if let Some((client, upstream_project_id)) = self.upstream_client() {
let request_task = client.request(proto::MultiLspQuery {
@@ -9704,22 +9715,28 @@ impl LspStore {
}
}
pub fn result_id(&self, buffer_id: BufferId) -> Option<String> {
pub fn result_id(&self, buffer_id: BufferId, cx: &App) -> Option<String> {
let abs_path = self
.buffer_store
.read(cx)
.get(buffer_id)
.and_then(|b| File::from_dyn(b.read(cx).file()))
.map(|f| f.abs_path(cx))?;
self.as_local()?
.buffer_pull_diagnostics_result_ids
.get(&buffer_id)
.get(&abs_path)
.cloned()
.flatten()
}
pub fn all_result_ids(&self) -> HashMap<BufferId, String> {
pub fn all_result_ids(&self) -> HashMap<PathBuf, String> {
let Some(local) = self.as_local() else {
return HashMap::default();
};
local
.buffer_pull_diagnostics_result_ids
.iter()
.filter_map(|(buffer_id, result_id)| Some((*buffer_id, result_id.clone()?)))
.filter_map(|(file_path, result_id)| Some((file_path.clone(), result_id.clone()?)))
.collect()
}
@@ -9802,17 +9819,11 @@ fn lsp_workspace_diagnostics_refresh(
.await;
attempts += 1;
let Ok(previous_result_ids) = lsp_store.update(cx, |lsp_store, cx| {
let Ok(previous_result_ids) = lsp_store.update(cx, |lsp_store, _| {
lsp_store
.all_result_ids()
.into_iter()
.filter_map(|(buffer_id, result_id)| {
let buffer = lsp_store
.buffer_store()
.read(cx)
.get_existing(buffer_id)
.ok()?;
let abs_path = buffer.read(cx).file()?.as_local()?.abs_path(cx);
.filter_map(|(abs_path, result_id)| {
let uri = file_path_to_lsp_url(&abs_path).ok()?;
Some(lsp::PreviousResultId {
uri,

View File

@@ -258,7 +258,7 @@ impl Render for TerminalOutput {
cell: ic.cell.clone(),
});
let (cells, rects) =
TerminalElement::layout_grid(grid, &text_style, text_system, None, window, cx);
TerminalElement::layout_grid(grid, 0, &text_style, text_system, None, window, cx);
// lines are 0-indexed, so we must add 1 to get the number of lines
let text_line_height = text_style.line_height_in_pixels(window.rem_size());

View File

@@ -387,7 +387,13 @@ impl KeymapFile {
},
};
let key_binding = match KeyBinding::load(keystrokes, action, context, key_equivalents) {
let key_binding = match KeyBinding::load(
keystrokes,
action,
context,
key_equivalents,
cx.keyboard_mapper(),
) {
Ok(key_binding) => key_binding,
Err(InvalidKeystrokeError { keystroke }) => {
return Err(format!(

View File

@@ -530,6 +530,21 @@ impl EnvVariableReplacer {
fn new(variables: HashMap<VsCodeEnvVariable, ZedEnvVariable>) -> Self {
Self { variables }
}
fn replace_value(&self, input: serde_json::Value) -> serde_json::Value {
match input {
serde_json::Value::String(s) => serde_json::Value::String(self.replace(&s)),
serde_json::Value::Array(arr) => {
serde_json::Value::Array(arr.into_iter().map(|v| self.replace_value(v)).collect())
}
serde_json::Value::Object(obj) => serde_json::Value::Object(
obj.into_iter()
.map(|(k, v)| (self.replace(&k), self.replace_value(v)))
.collect(),
),
_ => input,
}
}
// Replaces occurrences of VsCode-specific environment variables with Zed equivalents.
fn replace(&self, input: &str) -> String {
shellexpand::env_with_context_no_errors(&input, |var: &str| {

View File

@@ -53,7 +53,7 @@ impl VsCodeDebugTaskDefinition {
host: None,
timeout: None,
}),
config: self.other_attributes,
config: replacer.replace_value(self.other_attributes),
};
Ok(definition)
}
@@ -75,7 +75,7 @@ impl TryFrom<VsCodeDebugTaskFile> for DebugTaskFile {
"workspaceFolder".to_owned(),
VariableName::WorktreeRoot.to_string(),
),
// TODO other interesting variables?
("file".to_owned(), VariableName::Filename.to_string()), // TODO other interesting variables?
]));
let templates = file
.configurations
@@ -94,6 +94,7 @@ fn task_type_to_adapter_name(task_type: &str) -> SharedString {
"php" => "PHP",
"cppdbg" | "lldb" => "CodeLLDB",
"debugpy" => "Debugpy",
"rdbg" => "Ruby",
_ => task_type,
}
.to_owned()

View File

@@ -270,10 +270,15 @@ mod test {
fn test_scroll_keys() {
//These keys should be handled by the scrolling element directly
//Need to signify this by returning 'None'
let shift_pageup = Keystroke::parse("shift-pageup").unwrap();
let shift_pagedown = Keystroke::parse("shift-pagedown").unwrap();
let shift_home = Keystroke::parse("shift-home").unwrap();
let shift_end = Keystroke::parse("shift-end").unwrap();
let shift_key = |key: &str| Keystroke {
modifiers: Modifiers::shift(),
key: key.to_owned(),
key_char: None,
};
let shift_pageup = shift_key("pageup");
let shift_pagedown = shift_key("pagedown");
let shift_home = shift_key("home");
let shift_end = shift_key("end");
let none = TermMode::NONE;
assert_eq!(to_esc_str(&shift_pageup, &none, false), None);
@@ -299,8 +304,13 @@ mod test {
Some("\x1b[1;2F".into())
);
let pageup = Keystroke::parse("pageup").unwrap();
let pagedown = Keystroke::parse("pagedown").unwrap();
let normal_key = |key: &str| Keystroke {
modifiers: crate::Modifiers::none(),
key: key.to_owned(),
key_char: None,
};
let pageup = normal_key("pageup");
let pagedown = normal_key("pagedown");
let any = TermMode::ANY;
assert_eq!(to_esc_str(&pageup, &any, false), Some("\x1b[5~".into()));
@@ -328,10 +338,15 @@ mod test {
let app_cursor = TermMode::APP_CURSOR;
let none = TermMode::NONE;
let up = Keystroke::parse("up").unwrap();
let down = Keystroke::parse("down").unwrap();
let left = Keystroke::parse("left").unwrap();
let right = Keystroke::parse("right").unwrap();
let generate_keystroke = |key: &str| Keystroke {
modifiers: Modifiers::none(),
key: key.to_owned(),
key_char: None,
};
let up = generate_keystroke("up");
let down = generate_keystroke("down");
let left = generate_keystroke("left");
let right = generate_keystroke("right");
assert_eq!(to_esc_str(&up, &none, false), Some("\x1b[A".into()));
assert_eq!(to_esc_str(&down, &none, false), Some("\x1b[B".into()));
@@ -356,12 +371,20 @@ mod test {
for (lower, upper) in letters_lower.zip(letters_upper) {
assert_eq!(
to_esc_str(
&Keystroke::parse(&format!("ctrl-shift-{}", lower)).unwrap(),
&Keystroke {
modifiers: Modifiers::control_shift(),
key: lower.to_string(),
key_char: None,
},
&mode,
false
),
to_esc_str(
&Keystroke::parse(&format!("ctrl-{}", upper)).unwrap(),
&Keystroke {
modifiers: Modifiers::control(),
key: upper.to_string(),
key_char: None,
},
&mode,
false
),
@@ -378,7 +401,11 @@ mod test {
for character in ascii_printable {
assert_eq!(
to_esc_str(
&Keystroke::parse(&format!("alt-{}", character)).unwrap(),
&Keystroke {
modifiers: Modifiers::alt(),
key: character.to_string(),
key_char: None,
},
&TermMode::NONE,
true
)
@@ -396,7 +423,11 @@ mod test {
for key in gpui_keys {
assert_ne!(
to_esc_str(
&Keystroke::parse(&format!("alt-{}", key)).unwrap(),
&Keystroke {
modifiers: Modifiers::alt(),
key: key.to_string(),
key_char: None,
},
&TermMode::NONE,
true
)
@@ -419,15 +450,78 @@ mod test {
// 8 | Shift + Alt + Control
// ---------+---------------------------
// from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys
assert_eq!(2, modifier_code(&Keystroke::parse("shift-a").unwrap()));
assert_eq!(3, modifier_code(&Keystroke::parse("alt-a").unwrap()));
assert_eq!(4, modifier_code(&Keystroke::parse("shift-alt-a").unwrap()));
assert_eq!(5, modifier_code(&Keystroke::parse("ctrl-a").unwrap()));
assert_eq!(6, modifier_code(&Keystroke::parse("shift-ctrl-a").unwrap()));
assert_eq!(7, modifier_code(&Keystroke::parse("alt-ctrl-a").unwrap()));
assert_eq!(
2,
modifier_code(&Keystroke {
modifiers: Modifiers::shift(),
key: "a".into(),
key_char: None
})
);
assert_eq!(
3,
modifier_code(&Keystroke {
modifiers: Modifiers::alt(),
key: "a".into(),
key_char: None
})
);
assert_eq!(
4,
modifier_code(&Keystroke {
modifiers: Modifiers {
shift: true,
alt: true,
..Default::default()
},
key: "a".into(),
key_char: None
})
);
assert_eq!(
5,
modifier_code(&Keystroke {
modifiers: Modifiers::control(),
key: "a".into(),
key_char: None
})
);
assert_eq!(
6,
modifier_code(&Keystroke {
modifiers: Modifiers {
shift: true,
control: true,
..Default::default()
},
key: "a".into(),
key_char: None
})
);
assert_eq!(
7,
modifier_code(&Keystroke {
modifiers: Modifiers {
alt: true,
control: true,
..Default::default()
},
key: "a".into(),
key_char: None
})
);
assert_eq!(
8,
modifier_code(&Keystroke::parse("shift-ctrl-alt-a").unwrap())
modifier_code(&Keystroke {
modifiers: Modifiers {
shift: true,
control: true,
alt: true,
..Default::default()
},
key: "a".into(),
key_char: None
})
);
}
}

View File

@@ -2,7 +2,7 @@ use editor::{CursorLayout, HighlightedRange, HighlightedRangeLine};
use gpui::{
AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, DispatchPhase, Element,
ElementId, Entity, FocusHandle, Font, FontStyle, FontWeight, GlobalElementId, HighlightStyle,
Hitbox, Hsla, InputHandler, InteractiveElement, Interactivity, IntoElement, LayoutId,
Hitbox, Hsla, InputHandler, InteractiveElement, Interactivity, IntoElement, LayoutId, Length,
ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, Point, ShapedLine,
StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, TextStyle, UTF16Selection,
UnderlineStyle, WeakEntity, WhiteSpace, Window, WindowTextSystem, div, fill, point, px,
@@ -32,7 +32,7 @@ use workspace::Workspace;
use std::mem;
use std::{fmt::Debug, ops::RangeInclusive, rc::Rc};
use crate::{BlockContext, BlockProperties, TerminalMode, TerminalView};
use crate::{BlockContext, BlockProperties, ContentMode, TerminalMode, TerminalView};
/// The information generated during layout that is necessary for painting.
pub struct LayoutState {
@@ -49,6 +49,7 @@ pub struct LayoutState {
gutter: Pixels,
block_below_cursor_element: Option<AnyElement>,
base_text_style: TextStyle,
content_mode: ContentMode,
}
/// Helper struct for converting data between Alacritty's cursor points, and displayed cursor points.
@@ -202,6 +203,7 @@ impl TerminalElement {
pub fn layout_grid(
grid: impl Iterator<Item = IndexedCell>,
start_line_offset: i32,
text_style: &TextStyle,
// terminal_theme: &TerminalStyle,
text_system: &WindowTextSystem,
@@ -218,6 +220,8 @@ impl TerminalElement {
let linegroups = grid.into_iter().chunk_by(|i| i.point.line);
for (line_index, (_, line)) in linegroups.into_iter().enumerate() {
let alac_line = start_line_offset + line_index as i32;
for cell in line {
let mut fg = cell.fg;
let mut bg = cell.bg;
@@ -245,7 +249,7 @@ impl TerminalElement {
|| {
Some(LayoutRect::new(
AlacPoint::new(
line_index as i32,
alac_line,
cell.point.column.0 as i32,
),
1,
@@ -260,10 +264,7 @@ impl TerminalElement {
rects.push(cur_rect.take().unwrap());
}
cur_rect = Some(LayoutRect::new(
AlacPoint::new(
line_index as i32,
cell.point.column.0 as i32,
),
AlacPoint::new(alac_line, cell.point.column.0 as i32),
1,
convert_color(&bg, theme),
));
@@ -272,7 +273,7 @@ impl TerminalElement {
None => {
cur_alac_color = Some(bg);
cur_rect = Some(LayoutRect::new(
AlacPoint::new(line_index as i32, cell.point.column.0 as i32),
AlacPoint::new(alac_line, cell.point.column.0 as i32),
1,
convert_color(&bg, theme),
));
@@ -295,7 +296,7 @@ impl TerminalElement {
);
cells.push(LayoutCell::new(
AlacPoint::new(line_index as i32, cell.point.column.0 as i32),
AlacPoint::new(alac_line, cell.point.column.0 as i32),
layout_cell,
))
};
@@ -430,7 +431,13 @@ impl TerminalElement {
}
}
fn register_mouse_listeners(&mut self, mode: TermMode, hitbox: &Hitbox, window: &mut Window) {
fn register_mouse_listeners(
&mut self,
mode: TermMode,
hitbox: &Hitbox,
content_mode: &ContentMode,
window: &mut Window,
) {
let focus = self.focus.clone();
let terminal = self.terminal.clone();
let terminal_view = self.terminal_view.clone();
@@ -512,14 +519,18 @@ impl TerminalElement {
),
);
if !matches!(self.mode, TerminalMode::Embedded { .. }) {
if content_mode.is_scrollable() {
self.interactivity.on_scroll_wheel({
let terminal_view = self.terminal_view.downgrade();
move |e, _window, cx| {
move |e, window, cx| {
terminal_view
.update(cx, |terminal_view, cx| {
terminal_view.scroll_wheel(e, cx);
cx.notify();
if matches!(terminal_view.mode, TerminalMode::Standalone)
|| terminal_view.focus_handle.is_focused(window)
{
terminal_view.scroll_wheel(e, cx);
cx.notify();
}
})
.ok();
}
@@ -605,6 +616,32 @@ impl Element for TerminalElement {
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let height: Length = match self.terminal_view.read(cx).content_mode(window, cx) {
ContentMode::Inline {
displayed_lines,
total_lines: _,
} => {
let rem_size = window.rem_size();
let line_height = window.text_style().font_size.to_pixels(rem_size)
* TerminalSettings::get_global(cx)
.line_height
.value()
.to_pixels(rem_size)
.0;
(displayed_lines * line_height).into()
}
ContentMode::Scrollable => {
if let TerminalMode::Embedded { .. } = &self.mode {
let term = self.terminal.read(cx);
if !term.scrolled_to_top() && !term.scrolled_to_bottom() && self.focused {
self.interactivity.occlude_mouse();
}
}
relative(1.).into()
}
};
let layout_id = self.interactivity.request_layout(
global_id,
inspector_id,
@@ -612,29 +649,7 @@ impl Element for TerminalElement {
cx,
|mut style, window, cx| {
style.size.width = relative(1.).into();
match &self.mode {
TerminalMode::Scrollable => {
style.size.height = relative(1.).into();
}
TerminalMode::Embedded { max_lines } => {
let rem_size = window.rem_size();
let line_height = window.text_style().font_size.to_pixels(rem_size)
* TerminalSettings::get_global(cx)
.line_height
.value()
.to_pixels(rem_size)
.0;
let mut line_count = self.terminal.read(cx).total_lines();
if !self.focused {
if let Some(max_lines) = max_lines {
line_count = line_count.min(*max_lines);
}
}
style.size.height = (line_count * line_height).into();
}
}
style.size.height = height;
window.request_layout(style, None, cx)
},
@@ -693,7 +708,7 @@ impl Element for TerminalElement {
TerminalMode::Embedded { .. } => {
window.text_style().font_size.to_pixels(window.rem_size())
}
TerminalMode::Scrollable => terminal_settings
TerminalMode::Standalone => terminal_settings
.font_size
.map_or(buffer_font_size, |size| theme::adjusted_font_size(size, cx)),
};
@@ -733,7 +748,7 @@ impl Element for TerminalElement {
let player_color = theme.players().local();
let match_color = theme.colors().search_match_background;
let gutter;
let dimensions = {
let (dimensions, line_height_px) = {
let rem_size = window.rem_size();
let font_pixels = text_style.font_size.to_pixels(rem_size);
// TODO: line_height should be an f32 not an AbsoluteLength.
@@ -759,7 +774,10 @@ impl Element for TerminalElement {
let mut origin = bounds.origin;
origin.x += gutter;
TerminalBounds::new(line_height, cell_width, Bounds { origin, size })
(
TerminalBounds::new(line_height, cell_width, Bounds { origin, size }),
line_height,
)
};
let search_matches = self.terminal.read(cx).matches.clone();
@@ -827,16 +845,42 @@ impl Element for TerminalElement {
// then have that representation be converted to the appropriate highlight data structure
let (cells, rects) = TerminalElement::layout_grid(
cells.iter().cloned(),
&text_style,
window.text_system(),
last_hovered_word
.as_ref()
.map(|last_hovered_word| (link_style, &last_hovered_word.word_match)),
window,
cx,
);
let content_mode = self.terminal_view.read(cx).content_mode(window, cx);
let (cells, rects) = match content_mode {
ContentMode::Scrollable => TerminalElement::layout_grid(
cells.iter().cloned(),
0,
&text_style,
window.text_system(),
last_hovered_word
.as_ref()
.map(|last_hovered_word| (link_style, &last_hovered_word.word_match)),
window,
cx,
),
ContentMode::Inline { .. } => {
let intersection = window.content_mask().bounds.intersect(&bounds);
let start_row = (intersection.top() - bounds.top()) / line_height_px;
let end_row = start_row + intersection.size.height / line_height_px;
let line_range = (start_row as i32)..=(end_row as i32);
TerminalElement::layout_grid(
cells
.iter()
.skip_while(|i| &i.point.line < line_range.start())
.take_while(|i| &i.point.line <= line_range.end())
.cloned(),
*line_range.start(),
&text_style,
window.text_system(),
last_hovered_word.as_ref().map(|last_hovered_word| {
(link_style, &last_hovered_word.word_match)
}),
window,
cx,
)
}
};
// Layout cursor. Rectangle is used for IME, so we should lay it out even
// if we don't end up showing it.
@@ -932,6 +976,7 @@ impl Element for TerminalElement {
gutter,
block_below_cursor_element,
base_text_style: text_style,
content_mode,
}
},
)
@@ -969,7 +1014,12 @@ impl Element for TerminalElement {
workspace: self.workspace.clone(),
};
self.register_mouse_listeners(layout.mode, &layout.hitbox, window);
self.register_mouse_listeners(
layout.mode,
&layout.hitbox,
&layout.content_mode,
window,
);
if window.modifiers().secondary()
&& bounds.contains(&window.mouse_position())
&& self.terminal_view.read(cx).hover.is_some()

View File

@@ -9,9 +9,9 @@ use assistant_slash_command::SlashCommandRegistry;
use editor::{Editor, EditorSettings, actions::SelectAll, scroll::ScrollbarAutoHide};
use gpui::{
AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render, ScrollWheelEvent,
Stateful, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred, div,
impl_actions,
KeyDownEvent, Keystroke, Modifiers, MouseButton, MouseDownEvent, Pixels, Render,
ScrollWheelEvent, Stateful, Styled, Subscription, Task, WeakEntity, actions, anchored,
deferred, div, impl_actions,
};
use itertools::Itertools;
use persistence::TERMINAL_DB;
@@ -140,12 +140,37 @@ pub struct TerminalView {
#[derive(Default, Clone)]
pub enum TerminalMode {
#[default]
Scrollable,
Standalone,
Embedded {
max_lines: Option<usize>,
max_lines_when_unfocused: Option<usize>,
},
}
#[derive(Clone)]
pub enum ContentMode {
Scrollable,
Inline {
displayed_lines: usize,
total_lines: usize,
},
}
impl ContentMode {
pub fn is_limited(&self) -> bool {
match self {
ContentMode::Scrollable => false,
ContentMode::Inline {
displayed_lines,
total_lines,
} => displayed_lines < total_lines,
}
}
pub fn is_scrollable(&self) -> bool {
matches!(self, ContentMode::Scrollable)
}
}
#[derive(Debug)]
struct HoverTarget {
tooltip: String,
@@ -223,7 +248,7 @@ impl TerminalView {
blink_epoch: 0,
hover: None,
hover_tooltip_update: Task::ready(()),
mode: TerminalMode::Scrollable,
mode: TerminalMode::Standalone,
workspace_id,
show_breadcrumbs: TerminalSettings::get_global(cx).toolbar.breadcrumbs,
block_below_cursor: None,
@@ -245,16 +270,46 @@ impl TerminalView {
}
/// Enable 'embedded' mode where the terminal displays the full content with an optional limit of lines.
pub fn set_embedded_mode(&mut self, max_lines: Option<usize>, cx: &mut Context<Self>) {
self.mode = TerminalMode::Embedded { max_lines };
pub fn set_embedded_mode(
&mut self,
max_lines_when_unfocused: Option<usize>,
cx: &mut Context<Self>,
) {
self.mode = TerminalMode::Embedded {
max_lines_when_unfocused,
};
cx.notify();
}
pub fn is_content_limited(&self, window: &Window) -> bool {
const MAX_EMBEDDED_LINES: usize = 1_000;
/// Returns the current `ContentMode` depending on the set `TerminalMode` and the current number of lines
///
/// Note: Even in embedded mode, the terminal will fallback to scrollable when its content exceeds `MAX_EMBEDDED_LINES`
pub fn content_mode(&self, window: &Window, cx: &App) -> ContentMode {
match &self.mode {
TerminalMode::Scrollable => false,
TerminalMode::Embedded { max_lines } => {
!self.focus_handle.is_focused(window) && max_lines.is_some()
TerminalMode::Standalone => ContentMode::Scrollable,
TerminalMode::Embedded {
max_lines_when_unfocused,
} => {
let total_lines = self.terminal.read(cx).total_lines();
if total_lines > Self::MAX_EMBEDDED_LINES {
ContentMode::Scrollable
} else {
let mut displayed_lines = total_lines;
if !self.focus_handle.is_focused(window) {
if let Some(max_lines) = max_lines_when_unfocused {
displayed_lines = displayed_lines.min(*max_lines)
}
}
ContentMode::Inline {
displayed_lines,
total_lines,
}
}
}
}
}
@@ -395,7 +450,15 @@ impl TerminalView {
{
self.terminal.update(cx, |term, cx| {
term.try_keystroke(
&Keystroke::parse("ctrl-cmd-space").unwrap(),
&Keystroke {
modifiers: Modifiers {
control: true,
platform: true,
..Default::default()
},
key: "space".to_owned(),
key_char: None,
},
TerminalSettings::get_global(cx).option_as_meta,
)
});
@@ -671,7 +734,7 @@ impl TerminalView {
}
fn send_keystroke(&mut self, text: &SendKeystroke, _: &mut Window, cx: &mut Context<Self>) {
if let Some(keystroke) = Keystroke::parse(&text.0).log_err() {
if let Some(keystroke) = Keystroke::parse(&text.0, cx.keyboard_mapper()).log_err() {
self.clear_bell(cx);
self.terminal.update(cx, |term, cx| {
let processed =
@@ -840,10 +903,10 @@ impl TerminalView {
}))
}
fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
fn render_scrollbar(&self, window: &Window, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
if !Self::should_show_scrollbar(cx)
|| !(self.show_scrollbar || self.scrollbar_state.is_dragging())
|| matches!(self.mode, TerminalMode::Embedded { .. })
|| !self.content_mode(window, cx).is_scrollable()
{
return None;
}
@@ -1493,7 +1556,7 @@ impl Render for TerminalView {
self.block_below_cursor.clone(),
self.mode.clone(),
))
.when_some(self.render_scrollbar(cx), |div, scrollbar| {
.when_some(self.render_scrollbar(window, cx), |div, scrollbar| {
div.child(scrollbar)
}),
)

View File

@@ -148,11 +148,8 @@ impl ThemeColors {
version_control_renamed: MODIFIED_COLOR,
version_control_conflict: orange().light().step_12(),
version_control_ignored: gray().light().step_12(),
version_control_conflict_ours_background: green().light().step_10().alpha(0.5),
version_control_conflict_theirs_background: blue().light().step_10().alpha(0.5),
version_control_conflict_ours_marker_background: green().light().step_10().alpha(0.7),
version_control_conflict_theirs_marker_background: blue().light().step_10().alpha(0.7),
version_control_conflict_divider_background: Hsla::default(),
version_control_conflict_marker_ours: green().light().step_10().alpha(0.5),
version_control_conflict_marker_theirs: blue().light().step_10().alpha(0.5),
}
}
@@ -273,11 +270,8 @@ impl ThemeColors {
version_control_renamed: MODIFIED_COLOR,
version_control_conflict: orange().dark().step_12(),
version_control_ignored: gray().dark().step_12(),
version_control_conflict_ours_background: green().dark().step_10().alpha(0.5),
version_control_conflict_theirs_background: blue().dark().step_10().alpha(0.5),
version_control_conflict_ours_marker_background: green().dark().step_10().alpha(0.7),
version_control_conflict_theirs_marker_background: blue().dark().step_10().alpha(0.7),
version_control_conflict_divider_background: Hsla::default(),
version_control_conflict_marker_ours: green().dark().step_10().alpha(0.5),
version_control_conflict_marker_theirs: blue().dark().step_10().alpha(0.5),
}
}
}

View File

@@ -211,23 +211,8 @@ pub(crate) fn zed_default_dark() -> Theme {
version_control_renamed: MODIFIED_COLOR,
version_control_conflict: crate::orange().light().step_12(),
version_control_ignored: crate::gray().light().step_12(),
version_control_conflict_ours_background: crate::green()
.light()
.step_12()
.alpha(0.5),
version_control_conflict_theirs_background: crate::blue()
.light()
.step_12()
.alpha(0.5),
version_control_conflict_ours_marker_background: crate::green()
.light()
.step_12()
.alpha(0.7),
version_control_conflict_theirs_marker_background: crate::blue()
.light()
.step_12()
.alpha(0.7),
version_control_conflict_divider_background: Hsla::default(),
version_control_conflict_marker_ours: crate::green().light().step_12().alpha(0.5),
version_control_conflict_marker_theirs: crate::blue().light().step_12().alpha(0.5),
},
status: StatusColors {
conflict: yellow,

View File

@@ -620,24 +620,20 @@ pub struct ThemeColorsContent {
pub version_control_ignored: Option<String>,
/// Background color for row highlights of "ours" regions in merge conflicts.
#[serde(rename = "version_control.conflict.ours_background")]
pub version_control_conflict_ours_background: Option<String>,
#[serde(rename = "version_control.conflict_marker.ours")]
pub version_control_conflict_marker_ours: Option<String>,
/// Background color for row highlights of "theirs" regions in merge conflicts.
#[serde(rename = "version_control.conflict.theirs_background")]
#[serde(rename = "version_control.conflict_marker.theirs")]
pub version_control_conflict_marker_theirs: Option<String>,
/// Deprecated in favor of `version_control_conflict_marker_ours`.
#[deprecated]
pub version_control_conflict_ours_background: Option<String>,
/// Deprecated in favor of `version_control_conflict_marker_theirs`.
#[deprecated]
pub version_control_conflict_theirs_background: Option<String>,
/// Background color for row highlights of "ours" conflict markers in merge conflicts.
#[serde(rename = "version_control.conflict.ours_marker_background")]
pub version_control_conflict_ours_marker_background: Option<String>,
/// Background color for row highlights of "theirs" conflict markers in merge conflicts.
#[serde(rename = "version_control.conflict.theirs_marker_background")]
pub version_control_conflict_theirs_marker_background: Option<String>,
/// Background color for row highlights of the "ours"/"theirs" divider in merge conflicts.
#[serde(rename = "version_control.conflict.divider_background")]
pub version_control_conflict_divider_background: Option<String>,
}
impl ThemeColorsContent {
@@ -1118,25 +1114,17 @@ impl ThemeColorsContent {
.and_then(|color| try_parse_color(color).ok())
// Fall back to `conflict`, for backwards compatibility.
.or(status_colors.ignored),
version_control_conflict_ours_background: self
.version_control_conflict_ours_background
#[allow(deprecated)]
version_control_conflict_marker_ours: self
.version_control_conflict_marker_ours
.as_ref()
.or(self.version_control_conflict_ours_background.as_ref())
.and_then(|color| try_parse_color(color).ok()),
version_control_conflict_theirs_background: self
.version_control_conflict_theirs_background
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
version_control_conflict_ours_marker_background: self
.version_control_conflict_ours_marker_background
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
version_control_conflict_theirs_marker_background: self
.version_control_conflict_theirs_marker_background
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
version_control_conflict_divider_background: self
.version_control_conflict_divider_background
#[allow(deprecated)]
version_control_conflict_marker_theirs: self
.version_control_conflict_marker_theirs
.as_ref()
.or(self.version_control_conflict_theirs_background.as_ref())
.and_then(|color| try_parse_color(color).ok()),
}
}

View File

@@ -273,12 +273,9 @@ pub struct ThemeColors {
pub version_control_ignored: Hsla,
/// Represents the "ours" region of a merge conflict.
pub version_control_conflict_ours_background: Hsla,
pub version_control_conflict_marker_ours: Hsla,
/// Represents the "theirs" region of a merge conflict.
pub version_control_conflict_theirs_background: Hsla,
pub version_control_conflict_ours_marker_background: Hsla,
pub version_control_conflict_theirs_marker_background: Hsla,
pub version_control_conflict_divider_background: Hsla,
pub version_control_conflict_marker_theirs: Hsla,
}
#[derive(EnumIter, Debug, Clone, Copy, AsRefStr)]

View File

@@ -1,9 +1,6 @@
use gpui::{Action, MouseButton, prelude::*};
use gpui::{Action, Hsla, MouseButton, prelude::*, svg};
use ui::prelude::*;
use crate::window_controls::{WindowControl, WindowControlType};
#[derive(IntoElement)]
pub struct LinuxWindowControls {
close_window_action: Box<dyn Action>,
@@ -46,3 +43,166 @@ impl RenderOnce for LinuxWindowControls {
))
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub enum WindowControlType {
Minimize,
Restore,
Maximize,
Close,
}
impl WindowControlType {
/// Returns the icon name for the window control type.
///
/// Will take a [PlatformStyle] in the future to return a different
/// icon name based on the platform.
pub fn icon(&self) -> IconName {
match self {
WindowControlType::Minimize => IconName::GenericMinimize,
WindowControlType::Restore => IconName::GenericRestore,
WindowControlType::Maximize => IconName::GenericMaximize,
WindowControlType::Close => IconName::GenericClose,
}
}
}
#[allow(unused)]
pub struct WindowControlStyle {
background: Hsla,
background_hover: Hsla,
icon: Hsla,
icon_hover: Hsla,
}
impl WindowControlStyle {
pub fn default(cx: &mut App) -> Self {
let colors = cx.theme().colors();
Self {
background: colors.ghost_element_background,
background_hover: colors.ghost_element_hover,
icon: colors.icon,
icon_hover: colors.icon_muted,
}
}
#[allow(unused)]
/// Sets the background color of the control.
pub fn background(mut self, color: impl Into<Hsla>) -> Self {
self.background = color.into();
self
}
#[allow(unused)]
/// Sets the background color of the control when hovered.
pub fn background_hover(mut self, color: impl Into<Hsla>) -> Self {
self.background_hover = color.into();
self
}
#[allow(unused)]
/// Sets the color of the icon.
pub fn icon(mut self, color: impl Into<Hsla>) -> Self {
self.icon = color.into();
self
}
#[allow(unused)]
/// Sets the color of the icon when hovered.
pub fn icon_hover(mut self, color: impl Into<Hsla>) -> Self {
self.icon_hover = color.into();
self
}
}
#[derive(IntoElement)]
pub struct WindowControl {
id: ElementId,
icon: WindowControlType,
style: WindowControlStyle,
close_action: Option<Box<dyn Action>>,
}
impl WindowControl {
pub fn new(id: impl Into<ElementId>, icon: WindowControlType, cx: &mut App) -> Self {
let style = WindowControlStyle::default(cx);
Self {
id: id.into(),
icon,
style,
close_action: None,
}
}
pub fn new_close(
id: impl Into<ElementId>,
icon: WindowControlType,
close_action: Box<dyn Action>,
cx: &mut App,
) -> Self {
let style = WindowControlStyle::default(cx);
Self {
id: id.into(),
icon,
style,
close_action: Some(close_action.boxed_clone()),
}
}
#[allow(unused)]
pub fn custom_style(
id: impl Into<ElementId>,
icon: WindowControlType,
style: WindowControlStyle,
) -> Self {
Self {
id: id.into(),
icon,
style,
close_action: None,
}
}
}
impl RenderOnce for WindowControl {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let icon = svg()
.size_4()
.flex_none()
.path(self.icon.icon().path())
.text_color(self.style.icon)
.group_hover("", |this| this.text_color(self.style.icon_hover));
h_flex()
.id(self.id)
.group("")
.cursor_pointer()
.justify_center()
.content_center()
.rounded_2xl()
.w_5()
.h_5()
.hover(|this| this.bg(self.style.background_hover))
.active(|this| this.bg(self.style.background_hover))
.child(icon)
.on_mouse_move(|_, _, cx| cx.stop_propagation())
.on_click(move |_, window, cx| {
cx.stop_propagation();
match self.icon {
WindowControlType::Minimize => window.minimize_window(),
WindowControlType::Restore => window.zoom_window(),
WindowControlType::Maximize => window.zoom_window(),
WindowControlType::Close => window.dispatch_action(
self.close_action
.as_ref()
.expect("Use WindowControl::new_close() for close control.")
.boxed_clone(),
cx,
),
}
})
}
}

View File

@@ -3,7 +3,6 @@ mod collab;
mod onboarding_banner;
mod platforms;
mod title_bar_settings;
mod window_controls;
#[cfg(feature = "stories")]
mod stories;

View File

@@ -1,165 +0,0 @@
use gpui::{Action, Hsla, svg};
use ui::prelude::*;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub enum WindowControlType {
Minimize,
Restore,
Maximize,
Close,
}
impl WindowControlType {
/// Returns the icon name for the window control type.
///
/// Will take a [PlatformStyle] in the future to return a different
/// icon name based on the platform.
pub fn icon(&self) -> IconName {
match self {
WindowControlType::Minimize => IconName::GenericMinimize,
WindowControlType::Restore => IconName::GenericRestore,
WindowControlType::Maximize => IconName::GenericMaximize,
WindowControlType::Close => IconName::GenericClose,
}
}
}
#[allow(unused)]
pub struct WindowControlStyle {
background: Hsla,
background_hover: Hsla,
icon: Hsla,
icon_hover: Hsla,
}
impl WindowControlStyle {
pub fn default(cx: &mut App) -> Self {
let colors = cx.theme().colors();
Self {
background: colors.ghost_element_background,
background_hover: colors.ghost_element_hover,
icon: colors.icon,
icon_hover: colors.icon_muted,
}
}
#[allow(unused)]
/// Sets the background color of the control.
pub fn background(mut self, color: impl Into<Hsla>) -> Self {
self.background = color.into();
self
}
#[allow(unused)]
/// Sets the background color of the control when hovered.
pub fn background_hover(mut self, color: impl Into<Hsla>) -> Self {
self.background_hover = color.into();
self
}
#[allow(unused)]
/// Sets the color of the icon.
pub fn icon(mut self, color: impl Into<Hsla>) -> Self {
self.icon = color.into();
self
}
#[allow(unused)]
/// Sets the color of the icon when hovered.
pub fn icon_hover(mut self, color: impl Into<Hsla>) -> Self {
self.icon_hover = color.into();
self
}
}
#[derive(IntoElement)]
pub struct WindowControl {
id: ElementId,
icon: WindowControlType,
style: WindowControlStyle,
close_action: Option<Box<dyn Action>>,
}
impl WindowControl {
pub fn new(id: impl Into<ElementId>, icon: WindowControlType, cx: &mut App) -> Self {
let style = WindowControlStyle::default(cx);
Self {
id: id.into(),
icon,
style,
close_action: None,
}
}
pub fn new_close(
id: impl Into<ElementId>,
icon: WindowControlType,
close_action: Box<dyn Action>,
cx: &mut App,
) -> Self {
let style = WindowControlStyle::default(cx);
Self {
id: id.into(),
icon,
style,
close_action: Some(close_action.boxed_clone()),
}
}
#[allow(unused)]
pub fn custom_style(
id: impl Into<ElementId>,
icon: WindowControlType,
style: WindowControlStyle,
) -> Self {
Self {
id: id.into(),
icon,
style,
close_action: None,
}
}
}
impl RenderOnce for WindowControl {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let icon = svg()
.size_4()
.flex_none()
.path(self.icon.icon().path())
.text_color(self.style.icon)
.group_hover("", |this| this.text_color(self.style.icon_hover));
h_flex()
.id(self.id)
.group("")
.cursor_pointer()
.justify_center()
.content_center()
.rounded_2xl()
.w_5()
.h_5()
.hover(|this| this.bg(self.style.background_hover))
.active(|this| this.bg(self.style.background_hover))
.child(icon)
.on_mouse_move(|_, _, cx| cx.stop_propagation())
.on_click(move |_, window, cx| {
cx.stop_propagation();
match self.icon {
WindowControlType::Minimize => window.minimize_window(),
WindowControlType::Restore => window.zoom_window(),
WindowControlType::Maximize => window.zoom_window(),
WindowControlType::Close => window.dispatch_action(
self.close_action
.as_ref()
.expect("Use WindowControl::new_close() for close control.")
.boxed_clone(),
cx,
),
}
})
}
}

View File

@@ -533,78 +533,61 @@ mod tests {
#[test]
fn test_text_for_keystroke() {
let cmd_keystroke = |key: &str| Keystroke {
modifiers: Modifiers::command(),
key: key.to_owned(),
key_char: None,
};
assert_eq!(
keystroke_text(
&Keystroke::parse("cmd-c").unwrap(),
PlatformStyle::Mac,
false
),
keystroke_text(&cmd_keystroke("c"), PlatformStyle::Mac, false),
"Command-C".to_string()
);
assert_eq!(
keystroke_text(
&Keystroke::parse("cmd-c").unwrap(),
PlatformStyle::Linux,
false
),
keystroke_text(&cmd_keystroke("c"), PlatformStyle::Linux, false),
"Super-C".to_string()
);
assert_eq!(
keystroke_text(
&Keystroke::parse("cmd-c").unwrap(),
PlatformStyle::Windows,
false
),
keystroke_text(&cmd_keystroke("c"), PlatformStyle::Windows, false),
"Win-C".to_string()
);
let ctrl_alt_keystroke = |key: &str| Keystroke {
modifiers: Modifiers {
control: true,
alt: true,
..Default::default()
},
key: key.to_owned(),
key_char: None,
};
assert_eq!(
keystroke_text(
&Keystroke::parse("ctrl-alt-delete").unwrap(),
PlatformStyle::Mac,
false
),
keystroke_text(&ctrl_alt_keystroke("delete"), PlatformStyle::Mac, false),
"Control-Option-Delete".to_string()
);
assert_eq!(
keystroke_text(
&Keystroke::parse("ctrl-alt-delete").unwrap(),
PlatformStyle::Linux,
false
),
keystroke_text(&ctrl_alt_keystroke("delete"), PlatformStyle::Linux, false),
"Ctrl-Alt-Delete".to_string()
);
assert_eq!(
keystroke_text(
&Keystroke::parse("ctrl-alt-delete").unwrap(),
PlatformStyle::Windows,
false
),
keystroke_text(&ctrl_alt_keystroke("delete"), PlatformStyle::Windows, false),
"Ctrl-Alt-Delete".to_string()
);
let shift_keystroke = |key: &str| Keystroke {
modifiers: Modifiers::shift(),
key: key.to_owned(),
key_char: None,
};
assert_eq!(
keystroke_text(
&Keystroke::parse("shift-pageup").unwrap(),
PlatformStyle::Mac,
false
),
keystroke_text(&shift_keystroke("pageup"), PlatformStyle::Mac, false),
"Shift-PageUp".to_string()
);
assert_eq!(
keystroke_text(
&Keystroke::parse("shift-pageup").unwrap(),
PlatformStyle::Linux,
false,
),
keystroke_text(&shift_keystroke("pageup"), PlatformStyle::Linux, false,),
"Shift-PageUp".to_string()
);
assert_eq!(
keystroke_text(
&Keystroke::parse("shift-pageup").unwrap(),
PlatformStyle::Windows,
false
),
keystroke_text(&shift_keystroke("pageup"), PlatformStyle::Windows, false),
"Shift-PageUp".to_string()
);
}

View File

@@ -42,6 +42,7 @@ walkdir.workspace = true
workspace-hack.workspace = true
[target.'cfg(unix)'.dependencies]
command-fds = "0.3.1"
libc.workspace = true
[target.'cfg(windows)'.dependencies]

View File

@@ -1,16 +1,21 @@
#![cfg_attr(not(unix), allow(unused))]
use anyhow::{Context as _, Result};
use collections::HashMap;
use std::borrow::Cow;
use std::ffi::OsStr;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::NamedTempFile;
/// Capture all environment variables from the login shell.
pub fn capture(change_dir: Option<impl AsRef<Path>>) -> Result<HashMap<String, String>> {
let shell_path = std::env::var("SHELL").map(PathBuf::from)?;
let shell_name = shell_path.file_name().and_then(OsStr::to_str);
#[cfg(unix)]
pub fn capture(directory: &std::path::Path) -> Result<collections::HashMap<String, String>> {
use std::os::unix::process::CommandExt;
use std::process::Stdio;
let shell_path = std::env::var("SHELL").map(std::path::PathBuf::from)?;
let shell_name = shell_path.file_name().and_then(std::ffi::OsStr::to_str);
let mut command = std::process::Command::new(&shell_path);
command.stdin(Stdio::null());
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
let mut command_string = String::new();
@@ -18,10 +23,7 @@ pub fn capture(change_dir: Option<impl AsRef<Path>>) -> Result<HashMap<String, S
// the project directory to get the env in there as if the user
// `cd`'d into it. We do that because tools like direnv, asdf, ...
// hook into `cd` and only set up the env after that.
if let Some(dir) = change_dir {
let dir_str = dir.as_ref().to_string_lossy();
command_string.push_str(&format!("cd '{dir_str}';"));
}
command_string.push_str(&format!("cd '{}';", directory.display()));
// In certain shells we need to execute additional_command in order to
// trigger the behavior of direnv, etc.
@@ -30,26 +32,26 @@ pub fn capture(change_dir: Option<impl AsRef<Path>>) -> Result<HashMap<String, S
_ => "",
});
let mut env_output_file = NamedTempFile::new()?;
command_string.push_str(&format!(
"sh -c 'export -p' > '{}';",
env_output_file.path().to_string_lossy(),
));
let mut command = Command::new(&shell_path);
// In some shells, file descriptors greater than 2 cannot be used in interactive mode,
// so file descriptor 0 is used instead.
const ENV_OUTPUT_FD: std::os::fd::RawFd = 0;
command_string.push_str(&format!("sh -c 'export -p >&{ENV_OUTPUT_FD}';"));
// For csh/tcsh, the login shell option is set by passing `-` as
// the 0th argument instead of using `-l`.
if let Some("tcsh" | "csh") = shell_name {
#[cfg(unix)]
std::os::unix::process::CommandExt::arg0(&mut command, "-");
command.arg0("-");
} else {
command.arg("-l");
}
command.args(["-i", "-c", &command_string]);
let process_output = super::set_pre_exec_to_start_new_session(&mut command).output()?;
super::set_pre_exec_to_start_new_session(&mut command);
let (env_output, process_output) = spawn_and_read_fd(command, ENV_OUTPUT_FD)?;
let env_output = String::from_utf8_lossy(&env_output);
anyhow::ensure!(
process_output.status.success(),
"login shell exited with {}. stdout: {:?}, stderr: {:?}",
@@ -58,15 +60,36 @@ pub fn capture(change_dir: Option<impl AsRef<Path>>) -> Result<HashMap<String, S
String::from_utf8_lossy(&process_output.stderr),
);
let mut env_output = String::new();
env_output_file.read_to_string(&mut env_output)?;
parse(&env_output)
.filter_map(|entry| match entry {
Ok((name, value)) => Some(Ok((name.into(), value?.into()))),
Err(err) => Some(Err(err)),
})
.collect::<Result<HashMap<String, String>>>()
.collect::<Result<_>>()
}
#[cfg(unix)]
fn spawn_and_read_fd(
mut command: std::process::Command,
child_fd: std::os::fd::RawFd,
) -> anyhow::Result<(Vec<u8>, std::process::Output)> {
use command_fds::{CommandFdExt, FdMapping};
use std::io::Read;
let (mut reader, writer) = std::io::pipe()?;
command.fd_mappings(vec![FdMapping {
parent_fd: writer.into(),
child_fd,
}])?;
let process = command.spawn()?;
drop(command);
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer)?;
Ok((buffer, process.wait_with_output()?))
}
/// Parse the result of calling `sh -c 'export -p'`.
@@ -154,6 +177,17 @@ fn parse_literal_double_quoted(input: &str) -> Option<(String, &str)> {
mod tests {
use super::*;
#[cfg(unix)]
#[test]
fn test_spawn_and_read_fd() -> anyhow::Result<()> {
let mut command = std::process::Command::new("sh");
super::super::set_pre_exec_to_start_new_session(&mut command);
command.args(["-lic", "printf 'abc%.0s' $(seq 1 65536) >&0"]);
let (bytes, _) = spawn_and_read_fd(command, 0)?;
assert_eq!(bytes.len(), 65536 * 3);
Ok(())
}
#[test]
fn test_parse() {
let input = indoc::indoc! {r#"

View File

@@ -315,7 +315,7 @@ pub fn load_login_shell_environment() -> Result<()> {
// into shell's `cd` command (and hooks) to manipulate env.
// We do this so that we get the env a user would have when spawning a shell
// in home directory.
for (name, value) in shell_env::capture(Some(paths::home_dir()))? {
for (name, value) in shell_env::capture(paths::home_dir())? {
unsafe { env::set_var(&name, &value) };
}

View File

@@ -67,7 +67,7 @@ impl Vim {
fn literal(&mut self, action: &Literal, window: &mut Window, cx: &mut Context<Self>) {
if let Some(Operator::Literal { prefix }) = self.active_operator() {
if let Some(prefix) = prefix {
if let Some(keystroke) = Keystroke::parse(&action.0).ok() {
if let Some(keystroke) = Keystroke::parse(&action.0, cx.keyboard_mapper()).ok() {
window.defer(cx, |window, cx| {
window.dispatch_keystroke(keystroke, cx);
});

View File

@@ -222,7 +222,15 @@ impl NeovimBackedTestContext {
pub async fn simulate_shared_keystrokes(&mut self, keystroke_texts: &str) {
for keystroke_text in keystroke_texts.split(' ') {
self.recent_keystrokes.push(keystroke_text.to_string());
#[cfg(not(feature = "neovim"))]
self.neovim.send_keystroke(keystroke_text).await;
#[cfg(feature = "neovim")]
{
let keyboard_mapper = self.cx.keyboard_mapper();
self.neovim
.send_keystroke(keystroke_text, keyboard_mapper.as_ref())
.await;
}
}
self.simulate_keystrokes(keystroke_texts);
}

View File

@@ -10,7 +10,7 @@ use async_compat::Compat;
#[cfg(feature = "neovim")]
use async_trait::async_trait;
#[cfg(feature = "neovim")]
use gpui::Keystroke;
use gpui::{Keystroke, PlatformKeyboardMapper};
#[cfg(feature = "neovim")]
use language::Point;
@@ -110,8 +110,12 @@ impl NeovimConnection {
// Sends a keystroke to the neovim process.
#[cfg(feature = "neovim")]
pub async fn send_keystroke(&mut self, keystroke_text: &str) {
let mut keystroke = Keystroke::parse(keystroke_text).unwrap();
pub async fn send_keystroke(
&mut self,
keystroke_text: &str,
keyboard_mapper: &dyn PlatformKeyboardMapper,
) {
let mut keystroke = Keystroke::parse(keystroke_text, keyboard_mapper).unwrap();
if keystroke.key == "<" {
keystroke.key = "lt".to_string()

View File

@@ -197,6 +197,7 @@ actions!(
SwapItemRight,
TogglePreviewTab,
TogglePinTab,
UnpinAllTabs,
]
);
@@ -2104,6 +2105,20 @@ impl Pane {
}
}
fn unpin_all_tabs(&mut self, _: &UnpinAllTabs, window: &mut Window, cx: &mut Context<Self>) {
if self.items.is_empty() {
return;
}
let pinned_item_ids = self.pinned_item_ids().into_iter().rev();
for pinned_item_id in pinned_item_ids {
if let Some(ix) = self.index_for_item_id(pinned_item_id) {
self.unpin_tab_at(ix, window, cx);
}
}
}
fn pin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
self.change_tab_pin_state(ix, PinOperation::Pin, window, cx);
}
@@ -3132,7 +3147,7 @@ impl Pane {
self.display_nav_history_buttons = display;
}
fn pinned_item_ids(&self) -> HashSet<EntityId> {
fn pinned_item_ids(&self) -> Vec<EntityId> {
self.items
.iter()
.enumerate()
@@ -3146,7 +3161,7 @@ impl Pane {
.collect()
}
fn clean_item_ids(&self, cx: &mut Context<Pane>) -> HashSet<EntityId> {
fn clean_item_ids(&self, cx: &mut Context<Pane>) -> Vec<EntityId> {
self.items()
.filter_map(|item| {
if !item.is_dirty(cx) {
@@ -3158,7 +3173,7 @@ impl Pane {
.collect()
}
fn to_the_side_item_ids(&self, item_id: EntityId, side: Side) -> HashSet<EntityId> {
fn to_the_side_item_ids(&self, item_id: EntityId, side: Side) -> Vec<EntityId> {
match side {
Side::Left => self
.items()
@@ -3359,6 +3374,9 @@ impl Render for Pane {
.on_action(cx.listener(|pane, action, window, cx| {
pane.toggle_pin_tab(action, window, cx);
}))
.on_action(cx.listener(|pane, action, window, cx| {
pane.unpin_all_tabs(action, window, cx);
}))
.when(PreviewTabsSettings::get_global(cx).enabled, |this| {
this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
@@ -4172,6 +4190,78 @@ mod tests {
assert_item_labels(&pane, ["B*", "A", "C"], cx);
}
#[gpui::test]
async fn test_unpin_all_tabs(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, None, cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
// Unpin all, in an empty pane
pane.update_in(cx, |pane, window, cx| {
pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
});
assert_item_labels(&pane, [], cx);
let item_a = add_labeled_item(&pane, "A", false, cx);
let item_b = add_labeled_item(&pane, "B", false, cx);
let item_c = add_labeled_item(&pane, "C", false, cx);
assert_item_labels(&pane, ["A", "B", "C*"], cx);
// Unpin all, when no tabs are pinned
pane.update_in(cx, |pane, window, cx| {
pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
});
assert_item_labels(&pane, ["A", "B", "C*"], cx);
// Pin inactive tabs only
pane.update_in(cx, |pane, window, cx| {
let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
pane.pin_tab_at(ix, window, cx);
let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
pane.pin_tab_at(ix, window, cx);
});
assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
pane.update_in(cx, |pane, window, cx| {
pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
});
assert_item_labels(&pane, ["A", "B", "C*"], cx);
// Pin all tabs
pane.update_in(cx, |pane, window, cx| {
let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
pane.pin_tab_at(ix, window, cx);
let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
pane.pin_tab_at(ix, window, cx);
let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
pane.pin_tab_at(ix, window, cx);
});
assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
// Activate middle tab
pane.update_in(cx, |pane, window, cx| {
pane.activate_item(1, false, false, window, cx);
});
assert_item_labels(&pane, ["A!", "B*!", "C!"], cx);
pane.update_in(cx, |pane, window, cx| {
pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
});
// Order has not changed
assert_item_labels(&pane, ["A", "B*", "C"], cx);
}
#[gpui::test]
async fn test_pinning_active_tab_without_position_change_maintains_focus(
cx: &mut TestAppContext,

View File

@@ -2159,10 +2159,11 @@ impl Workspace {
cx.propagate();
return;
}
let keyboard_mapper = cx.keyboard_mapper();
let mut keystrokes: Vec<Keystroke> = action
.0
.split(' ')
.flat_map(|k| Keystroke::parse(k).log_err())
.flat_map(|k| Keystroke::parse(k, keyboard_mapper).log_err())
.collect();
keystrokes.reverse();

View File

@@ -36,6 +36,7 @@ enable = false
"/assistant/context-servers.html" = "/docs/ai/mcp.html"
"/assistant/model-context-protocol.html" = "/docs/ai/mcp.html"
"/model-improvement.html" = "/docs/ai/ai-improvement.html"
"/extensions/context-servers.html" = "/docs/extensions/mcp-extensions.html"
# Our custom preprocessor for expanding commands like `{#kb action::ActionName}`,

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