Compare commits
47 Commits
asdf_copil
...
scan-code
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2eac6a9222 | ||
|
|
7c3cffdc52 | ||
|
|
5a3186b659 | ||
|
|
caf54844de | ||
|
|
745ebe2313 | ||
|
|
5c95e942e6 | ||
|
|
f979f24bfa | ||
|
|
411b9abb9e | ||
|
|
81d4d48ef2 | ||
|
|
cd9284761a | ||
|
|
34f9eef879 | ||
|
|
23cf6bf268 | ||
|
|
c97e477eb1 | ||
|
|
16804a81cc | ||
|
|
8bf39bf768 | ||
|
|
75922e8fcd | ||
|
|
2eb83364ae | ||
|
|
5d22585ef5 | ||
|
|
71303fa18b | ||
|
|
5753b978a0 | ||
|
|
9cf2490ed7 | ||
|
|
28ea3ea529 | ||
|
|
2dad48d8d9 | ||
|
|
16853acbb1 | ||
|
|
64d649245c | ||
|
|
08210b512d | ||
|
|
6070aea6c0 | ||
|
|
16b44d53f9 | ||
|
|
3bed830a1f | ||
|
|
ee2a329981 | ||
|
|
6d64058fc6 | ||
|
|
7c2822a020 | ||
|
|
3db00384f4 | ||
|
|
3dfbd9e57c | ||
|
|
b103d7621b | ||
|
|
ab70e524c8 | ||
|
|
f0ce62ead8 | ||
|
|
f0345df479 | ||
|
|
bbd2262a93 | ||
|
|
c4fd9e1a6b | ||
|
|
0b7583bae5 | ||
|
|
e4bd115a63 | ||
|
|
fa54fa80d0 | ||
|
|
de16f2bbe6 | ||
|
|
e3b13b54c9 | ||
|
|
2c5d2a58d8 | ||
|
|
3485b7704b |
9
.github/ISSUE_TEMPLATE/01_bug_ai.yml
vendored
9
.github/ISSUE_TEMPLATE/01_bug_ai.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Bug Report (AI Related)
|
name: Bug Report (AI)
|
||||||
description: Zed Agent Panel Bugs
|
description: Zed Agent Panel Bugs
|
||||||
type: "Bug"
|
type: "Bug"
|
||||||
labels: ["ai"]
|
labels: ["ai"]
|
||||||
@@ -19,15 +19,14 @@ body:
|
|||||||
2.
|
2.
|
||||||
3.
|
3.
|
||||||
|
|
||||||
Actual Behavior:
|
**Expected Behavior**:
|
||||||
Expected Behavior:
|
**Actual Behavior**:
|
||||||
|
|
||||||
### Model Provider Details
|
### Model Provider Details
|
||||||
- Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc)
|
- Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc)
|
||||||
- Model Name:
|
- Model Name:
|
||||||
- Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads)
|
- Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads)
|
||||||
- MCP Servers in-use:
|
- Other Details (MCPs, other settings, etc):
|
||||||
- Other Details:
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
35
.github/ISSUE_TEMPLATE/03_bug_git.yml
vendored
35
.github/ISSUE_TEMPLATE/03_bug_git.yml
vendored
@@ -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
|
|
||||||
4
.github/ISSUE_TEMPLATE/04_bug_debugger.yml
vendored
4
.github/ISSUE_TEMPLATE/04_bug_debugger.yml
vendored
@@ -19,8 +19,8 @@ body:
|
|||||||
2.
|
2.
|
||||||
3.
|
3.
|
||||||
|
|
||||||
Actual Behavior:
|
**Expected Behavior**:
|
||||||
Expected Behavior:
|
**Actual Behavior**:
|
||||||
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
@@ -18,14 +18,16 @@ body:
|
|||||||
- Issues with insufficient detail may be summarily closed.
|
- Issues with insufficient detail may be summarily closed.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
DESCRIPTION_HERE
|
||||||
|
|
||||||
Steps to reproduce:
|
Steps to reproduce:
|
||||||
1.
|
1.
|
||||||
2.
|
2.
|
||||||
3.
|
3.
|
||||||
4.
|
4.
|
||||||
|
|
||||||
Expected Behavior:
|
**Expected Behavior**:
|
||||||
Actual Behavior:
|
**Actual Behavior**:
|
||||||
|
|
||||||
<!-- Before Submitting, did you:
|
<!-- Before Submitting, did you:
|
||||||
1. Include settings.json, keymap.json, .editorconfig if relevant?
|
1. Include settings.json, keymap.json, .editorconfig if relevant?
|
||||||
|
|||||||
13
.github/actions/run_tests/action.yml
vendored
13
.github/actions/run_tests/action.yml
vendored
@@ -1,6 +1,12 @@
|
|||||||
name: "Run tests"
|
name: "Run tests"
|
||||||
description: "Runs the tests"
|
description: "Runs the tests"
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
use-xvfb:
|
||||||
|
description: "Whether to run tests with xvfb"
|
||||||
|
required: false
|
||||||
|
default: "false"
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
@@ -20,4 +26,9 @@ runs:
|
|||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
shell: bash -euxo pipefail {0}
|
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
|
||||||
|
|||||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -319,6 +319,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
uses: ./.github/actions/run_tests
|
uses: ./.github/actions/run_tests
|
||||||
|
with:
|
||||||
|
use-xvfb: true
|
||||||
|
|
||||||
- name: Build other binaries and features
|
- name: Build other binaries and features
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
2
.github/workflows/unit_evals.yml
vendored
2
.github/workflows/unit_evals.yml
vendored
@@ -62,7 +62,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Run unit evals
|
- name: Run unit evals
|
||||||
shell: bash -euxo pipefail {0}
|
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:
|
env:
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
|
||||||
|
|||||||
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -705,6 +705,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
"smol",
|
||||||
"streaming_diff",
|
"streaming_diff",
|
||||||
"strsim",
|
"strsim",
|
||||||
"task",
|
"task",
|
||||||
@@ -3160,6 +3161,16 @@ dependencies = [
|
|||||||
"memchr",
|
"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]]
|
[[package]]
|
||||||
name = "command_palette"
|
name = "command_palette"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -4052,6 +4063,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"collections",
|
||||||
"dap",
|
"dap",
|
||||||
"futures 0.3.31",
|
"futures 0.3.31",
|
||||||
"gpui",
|
"gpui",
|
||||||
@@ -10130,6 +10142,18 @@ dependencies = [
|
|||||||
"memoffset",
|
"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]]
|
[[package]]
|
||||||
name = "node_runtime"
|
name = "node_runtime"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -12110,7 +12134,6 @@ dependencies = [
|
|||||||
"unindent",
|
"unindent",
|
||||||
"url",
|
"url",
|
||||||
"util",
|
"util",
|
||||||
"uuid",
|
|
||||||
"which 6.0.3",
|
"which 6.0.3",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
"worktree",
|
"worktree",
|
||||||
@@ -17122,6 +17145,7 @@ dependencies = [
|
|||||||
"async-fs",
|
"async-fs",
|
||||||
"async_zip",
|
"async_zip",
|
||||||
"collections",
|
"collections",
|
||||||
|
"command-fds",
|
||||||
"dirs 4.0.0",
|
"dirs 4.0.0",
|
||||||
"dunce",
|
"dunce",
|
||||||
"futures 0.3.31",
|
"futures 0.3.31",
|
||||||
|
|||||||
@@ -99,6 +99,8 @@
|
|||||||
"version_control.added": "#27a657ff",
|
"version_control.added": "#27a657ff",
|
||||||
"version_control.modified": "#d3b020ff",
|
"version_control.modified": "#d3b020ff",
|
||||||
"version_control.deleted": "#e06c76ff",
|
"version_control.deleted": "#e06c76ff",
|
||||||
|
"version_control.conflict_marker.ours": "#a1c1811a",
|
||||||
|
"version_control.conflict_marker.theirs": "#74ade81a",
|
||||||
"conflict": "#dec184ff",
|
"conflict": "#dec184ff",
|
||||||
"conflict.background": "#dec1841a",
|
"conflict.background": "#dec1841a",
|
||||||
"conflict.border": "#5d4c2fff",
|
"conflict.border": "#5d4c2fff",
|
||||||
|
|||||||
@@ -1788,12 +1788,31 @@ impl ActiveThread {
|
|||||||
|
|
||||||
fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let message_id = self.messages[ix];
|
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();
|
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 {
|
if message.is_hidden {
|
||||||
return Empty.into_any();
|
return div().children(loading_dots).into_any();
|
||||||
}
|
}
|
||||||
|
|
||||||
let message_creases = message.creases.clone();
|
let message_creases = message.creases.clone();
|
||||||
@@ -1802,9 +1821,6 @@ impl ActiveThread {
|
|||||||
return Empty.into_any();
|
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
|
// Get all the data we need from thread before we start using it in closures
|
||||||
let checkpoint = thread.checkpoint_for_message(message_id);
|
let checkpoint = thread.checkpoint_for_message(message_id);
|
||||||
let configured_model = thread.configured_model().map(|m| m.model);
|
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 tool_uses = thread.tool_uses_for_message(message_id, cx);
|
||||||
let has_tool_uses = !tool_uses.is_empty();
|
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
|
let editing_message_state = self
|
||||||
.editing_message
|
.editing_message
|
||||||
@@ -2238,17 +2246,7 @@ impl ActiveThread {
|
|||||||
parent.child(self.render_rules_item(cx))
|
parent.child(self.render_rules_item(cx))
|
||||||
})
|
})
|
||||||
.child(styled_message)
|
.child(styled_message)
|
||||||
.when(is_generating && is_last_message, |this| {
|
.children(loading_dots)
|
||||||
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)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when(show_feedback, move |parent| {
|
.when(show_feedback, move |parent| {
|
||||||
parent.child(feedback_items).when_some(
|
parent.child(feedback_items).when_some(
|
||||||
self.open_feedback_editors.get(&message_id),
|
self.open_feedback_editors.get(&message_id),
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use context_server::ContextServerId;
|
|||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, Animation, AnimationExt as _, AnyView, App, Entity, EventEmitter, FocusHandle,
|
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 language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
|
||||||
use project::context_server_store::{ContextServerStatus, ContextServerStore};
|
use project::context_server_store::{ContextServerStatus, ContextServerStore};
|
||||||
@@ -475,7 +475,6 @@ impl AgentConfiguration {
|
|||||||
.get(&context_server_id)
|
.get(&context_server_id)
|
||||||
.copied()
|
.copied()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let tools = tools_by_source
|
let tools = tools_by_source
|
||||||
.get(&ToolSource::ContextServer {
|
.get(&ToolSource::ContextServer {
|
||||||
id: context_server_id.0.clone().into(),
|
id: context_server_id.0.clone().into(),
|
||||||
@@ -484,25 +483,23 @@ impl AgentConfiguration {
|
|||||||
let tool_count = tools.len();
|
let tool_count = tools.len();
|
||||||
|
|
||||||
let border_color = cx.theme().colors().border.opacity(0.6);
|
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 {
|
let (status_indicator, tooltip_text) = match server_status {
|
||||||
ContextServerStatus::Starting => (
|
ContextServerStatus::Starting => (
|
||||||
Indicator::dot()
|
Icon::new(IconName::LoadCircle)
|
||||||
.color(Color::Success)
|
.size(IconSize::XSmall)
|
||||||
|
.color(Color::Accent)
|
||||||
.with_animation(
|
.with_animation(
|
||||||
SharedString::from(format!("{}-starting", context_server_id.0.clone(),)),
|
SharedString::from(format!("{}-starting", context_server_id.0.clone(),)),
|
||||||
Animation::new(Duration::from_secs(2))
|
Animation::new(Duration::from_secs(3)).repeat(),
|
||||||
.repeat()
|
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||||
.with_easing(pulsating_between(0.4, 1.)),
|
|
||||||
move |this, delta| this.color(success_color.alpha(delta).into()),
|
|
||||||
)
|
)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
"Server is starting.",
|
"Server is starting.",
|
||||||
),
|
),
|
||||||
ContextServerStatus::Running => (
|
ContextServerStatus::Running => (
|
||||||
Indicator::dot().color(Color::Success).into_any_element(),
|
Indicator::dot().color(Color::Success).into_any_element(),
|
||||||
"Server is running.",
|
"Server is active.",
|
||||||
),
|
),
|
||||||
ContextServerStatus::Error(_) => (
|
ContextServerStatus::Error(_) => (
|
||||||
Indicator::dot().color(Color::Error).into_any_element(),
|
Indicator::dot().color(Color::Error).into_any_element(),
|
||||||
@@ -526,12 +523,11 @@ impl AgentConfiguration {
|
|||||||
.p_1()
|
.p_1()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.when(
|
.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),
|
|element| element.border_b_1().border_color(border_color),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1p5()
|
|
||||||
.child(
|
.child(
|
||||||
Disclosure::new(
|
Disclosure::new(
|
||||||
"tool-list-disclosure",
|
"tool-list-disclosure",
|
||||||
@@ -551,12 +547,16 @@ impl AgentConfiguration {
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
h_flex()
|
||||||
.id(item_id.clone())
|
.id(SharedString::from(format!("tooltip-{}", item_id)))
|
||||||
|
.h_full()
|
||||||
|
.w_3()
|
||||||
|
.mx_1()
|
||||||
|
.justify_center()
|
||||||
.tooltip(Tooltip::text(tooltip_text))
|
.tooltip(Tooltip::text(tooltip_text))
|
||||||
.child(status_indicator),
|
.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| {
|
.when(is_running, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
Label::new(if tool_count == 1 {
|
Label::new(if tool_count == 1 {
|
||||||
|
|||||||
@@ -386,8 +386,10 @@ impl CodegenAlternative {
|
|||||||
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
|
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
|
||||||
} else {
|
} else {
|
||||||
let request = self.build_request(&model, user_prompt, cx)?;
|
let request = self.build_request(&model, user_prompt, cx)?;
|
||||||
cx.spawn(async move |_, cx| model.stream_completion_text(request.await, &cx).await)
|
cx.spawn(async move |_, cx| {
|
||||||
.boxed_local()
|
Ok(model.stream_completion_text(request.await, &cx).await?)
|
||||||
|
})
|
||||||
|
.boxed_local()
|
||||||
};
|
};
|
||||||
self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx);
|
self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1331,7 +1331,7 @@ impl InlineAssistant {
|
|||||||
editor.clear_gutter_highlights::<GutterPendingRange>(cx);
|
editor.clear_gutter_highlights::<GutterPendingRange>(cx);
|
||||||
} else {
|
} else {
|
||||||
editor.highlight_gutter::<GutterPendingRange>(
|
editor.highlight_gutter::<GutterPendingRange>(
|
||||||
&gutter_pending_ranges,
|
gutter_pending_ranges,
|
||||||
|cx| cx.theme().status().info_background,
|
|cx| cx.theme().status().info_background,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@@ -1342,7 +1342,7 @@ impl InlineAssistant {
|
|||||||
editor.clear_gutter_highlights::<GutterTransformedRange>(cx);
|
editor.clear_gutter_highlights::<GutterTransformedRange>(cx);
|
||||||
} else {
|
} else {
|
||||||
editor.highlight_gutter::<GutterTransformedRange>(
|
editor.highlight_gutter::<GutterTransformedRange>(
|
||||||
&gutter_transformed_ranges,
|
gutter_transformed_ranges,
|
||||||
|cx| cx.theme().status().info,
|
|cx| cx.theme().status().info,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1563,6 +1563,9 @@ impl Thread {
|
|||||||
Err(LanguageModelCompletionError::Other(error)) => {
|
Err(LanguageModelCompletionError::Other(error)) => {
|
||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
|
Err(err @ LanguageModelCompletionError::RateLimit(..)) => {
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
@@ -406,6 +407,7 @@ impl RateLimit {
|
|||||||
/// <https://docs.anthropic.com/en/api/rate-limits#response-headers>
|
/// <https://docs.anthropic.com/en/api/rate-limits#response-headers>
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RateLimitInfo {
|
pub struct RateLimitInfo {
|
||||||
|
pub retry_after: Option<Duration>,
|
||||||
pub requests: Option<RateLimit>,
|
pub requests: Option<RateLimit>,
|
||||||
pub tokens: Option<RateLimit>,
|
pub tokens: Option<RateLimit>,
|
||||||
pub input_tokens: Option<RateLimit>,
|
pub input_tokens: Option<RateLimit>,
|
||||||
@@ -417,10 +419,11 @@ impl RateLimitInfo {
|
|||||||
// Check if any rate limit headers exist
|
// Check if any rate limit headers exist
|
||||||
let has_rate_limit_headers = headers
|
let has_rate_limit_headers = headers
|
||||||
.keys()
|
.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 {
|
if !has_rate_limit_headers {
|
||||||
return Self {
|
return Self {
|
||||||
|
retry_after: None,
|
||||||
requests: None,
|
requests: None,
|
||||||
tokens: None,
|
tokens: None,
|
||||||
input_tokens: None,
|
input_tokens: None,
|
||||||
@@ -429,6 +432,11 @@ impl RateLimitInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Self {
|
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(),
|
requests: RateLimit::from_headers("requests", headers).ok(),
|
||||||
tokens: RateLimit::from_headers("tokens", headers).ok(),
|
tokens: RateLimit::from_headers("tokens", headers).ok(),
|
||||||
input_tokens: RateLimit::from_headers("input-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)
|
.send(request)
|
||||||
.await
|
.await
|
||||||
.context("failed to send request to Anthropic")?;
|
.context("failed to send request to Anthropic")?;
|
||||||
|
let rate_limits = RateLimitInfo::from_headers(response.headers());
|
||||||
if response.status().is_success() {
|
if response.status().is_success() {
|
||||||
let rate_limits = RateLimitInfo::from_headers(response.headers());
|
|
||||||
let reader = BufReader::new(response.into_body());
|
let reader = BufReader::new(response.into_body());
|
||||||
let stream = reader
|
let stream = reader
|
||||||
.lines()
|
.lines()
|
||||||
@@ -500,6 +508,8 @@ pub async fn stream_completion_with_rate_limit_info(
|
|||||||
})
|
})
|
||||||
.boxed();
|
.boxed();
|
||||||
Ok((stream, Some(rate_limits)))
|
Ok((stream, Some(rate_limits)))
|
||||||
|
} else if let Some(retry_after) = rate_limits.retry_after {
|
||||||
|
Err(AnthropicError::RateLimit(retry_after))
|
||||||
} else {
|
} else {
|
||||||
let mut body = Vec::new();
|
let mut body = Vec::new();
|
||||||
response
|
response
|
||||||
@@ -769,6 +779,8 @@ pub struct MessageDelta {
|
|||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum AnthropicError {
|
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)]
|
#[error("an error occurred while interacting with the Anthropic API: {error_type}: {message}", error_type = .0.error_type, message = .0.message)]
|
||||||
ApiError(ApiError),
|
ApiError(ApiError),
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
|
|||||||
@@ -682,11 +682,12 @@ mod tests {
|
|||||||
_: &AsyncApp,
|
_: &AsyncApp,
|
||||||
) -> BoxFuture<
|
) -> BoxFuture<
|
||||||
'static,
|
'static,
|
||||||
http_client::Result<
|
Result<
|
||||||
BoxStream<
|
BoxStream<
|
||||||
'static,
|
'static,
|
||||||
http_client::Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
||||||
>,
|
>,
|
||||||
|
LanguageModelCompletionError,
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ rand.workspace = true
|
|||||||
pretty_assertions.workspace = true
|
pretty_assertions.workspace = true
|
||||||
reqwest_client.workspace = true
|
reqwest_client.workspace = true
|
||||||
settings = { workspace = true, features = ["test-support"] }
|
settings = { workspace = true, features = ["test-support"] }
|
||||||
|
smol.workspace = true
|
||||||
task = { workspace = true, features = ["test-support"]}
|
task = { workspace = true, features = ["test-support"]}
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use client::{Client, UserStore};
|
|||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
use futures::{FutureExt, future::LocalBoxFuture};
|
use futures::{FutureExt, future::LocalBoxFuture};
|
||||||
use gpui::{AppContext, TestAppContext};
|
use gpui::{AppContext, TestAppContext, Timer};
|
||||||
use indoc::{formatdoc, indoc};
|
use indoc::{formatdoc, indoc};
|
||||||
use language_model::{
|
use language_model::{
|
||||||
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult,
|
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult,
|
||||||
@@ -1255,9 +1255,12 @@ impl EvalAssertion {
|
|||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let mut response = judge
|
let mut response = retry_on_rate_limit(async || {
|
||||||
.stream_completion_text(request, &cx.to_async())
|
Ok(judge
|
||||||
.await?;
|
.stream_completion_text(request.clone(), &cx.to_async())
|
||||||
|
.await?)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
while let Some(chunk) = response.stream.next().await {
|
while let Some(chunk) = response.stream.next().await {
|
||||||
let chunk = chunk?;
|
let chunk = chunk?;
|
||||||
@@ -1308,10 +1311,17 @@ fn eval(
|
|||||||
run_eval(eval.clone(), tx.clone());
|
run_eval(eval.clone(), tx.clone());
|
||||||
|
|
||||||
let executor = gpui::background_executor();
|
let executor = gpui::background_executor();
|
||||||
|
let semaphore = Arc::new(smol::lock::Semaphore::new(32));
|
||||||
for _ in 1..iterations {
|
for _ in 1..iterations {
|
||||||
let eval = eval.clone();
|
let eval = eval.clone();
|
||||||
let tx = tx.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);
|
drop(tx);
|
||||||
|
|
||||||
@@ -1577,21 +1587,31 @@ impl EditAgentTest {
|
|||||||
if let Some(input_content) = eval.input_content.as_deref() {
|
if let Some(input_content) = eval.input_content.as_deref() {
|
||||||
buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));
|
buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));
|
||||||
}
|
}
|
||||||
let (edit_output, _) = self.agent.edit(
|
retry_on_rate_limit(async || {
|
||||||
buffer.clone(),
|
self.agent
|
||||||
eval.edit_file_input.display_description,
|
.edit(
|
||||||
&conversation,
|
buffer.clone(),
|
||||||
&mut cx.to_async(),
|
eval.edit_file_input.display_description.clone(),
|
||||||
);
|
&conversation,
|
||||||
edit_output.await?
|
&mut cx.to_async(),
|
||||||
|
)
|
||||||
|
.0
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.await?
|
||||||
} else {
|
} else {
|
||||||
let (edit_output, _) = self.agent.overwrite(
|
retry_on_rate_limit(async || {
|
||||||
buffer.clone(),
|
self.agent
|
||||||
eval.edit_file_input.display_description,
|
.overwrite(
|
||||||
&conversation,
|
buffer.clone(),
|
||||||
&mut cx.to_async(),
|
eval.edit_file_input.display_description.clone(),
|
||||||
);
|
&conversation,
|
||||||
edit_output.await?
|
&mut cx.to_async(),
|
||||||
|
)
|
||||||
|
.0
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.await?
|
||||||
};
|
};
|
||||||
|
|
||||||
let buffer_text = buffer.read_with(cx, |buffer, _| buffer.text());
|
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)]
|
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
struct EvalAssertionOutcome {
|
struct EvalAssertionOutcome {
|
||||||
score: usize,
|
score: usize,
|
||||||
|
|||||||
@@ -638,29 +638,36 @@ impl ToolCard for TerminalToolCard {
|
|||||||
.bg(cx.theme().colors().editor_background)
|
.bg(cx.theme().colors().editor_background)
|
||||||
.rounded_b_md()
|
.rounded_b_md()
|
||||||
.text_ui_sm(cx)
|
.text_ui_sm(cx)
|
||||||
.child(
|
.child({
|
||||||
ToolOutputPreview::new(
|
let content_mode = terminal.read(cx).content_mode(window, cx);
|
||||||
terminal.clone().into_any_element(),
|
|
||||||
terminal.entity_id(),
|
if content_mode.is_scrollable() {
|
||||||
)
|
div().h_72().child(terminal.clone()).into_any_element()
|
||||||
.with_total_lines(self.content_line_count)
|
} else {
|
||||||
.toggle_state(!terminal.read(cx).is_content_limited(window))
|
ToolOutputPreview::new(
|
||||||
.on_toggle({
|
terminal.clone().into_any_element(),
|
||||||
let terminal = terminal.clone();
|
terminal.entity_id(),
|
||||||
move |is_expanded, _, cx| {
|
)
|
||||||
terminal.update(cx, |terminal, cx| {
|
.with_total_lines(self.content_line_count)
|
||||||
terminal.set_embedded_mode(
|
.toggle_state(!content_mode.is_limited())
|
||||||
if is_expanded {
|
.on_toggle({
|
||||||
None
|
let terminal = terminal.clone();
|
||||||
} else {
|
move |is_expanded, _, cx| {
|
||||||
Some(COLLAPSED_LINES)
|
terminal.update(cx, |terminal, cx| {
|
||||||
},
|
terminal.set_embedded_mode(
|
||||||
cx,
|
if is_expanded {
|
||||||
);
|
None
|
||||||
});
|
} else {
|
||||||
}
|
Some(COLLAPSED_LINES)
|
||||||
}),
|
},
|
||||||
),
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -452,6 +452,10 @@ impl Model {
|
|||||||
| Model::Claude3_5SonnetV2
|
| Model::Claude3_5SonnetV2
|
||||||
| Model::Claude3_7Sonnet
|
| Model::Claude3_7Sonnet
|
||||||
| Model::Claude3_7SonnetThinking
|
| Model::Claude3_7SonnetThinking
|
||||||
|
| Model::ClaudeSonnet4
|
||||||
|
| Model::ClaudeSonnet4Thinking
|
||||||
|
| Model::ClaudeOpus4
|
||||||
|
| Model::ClaudeOpus4Thinking
|
||||||
| Model::Claude3Haiku
|
| Model::Claude3Haiku
|
||||||
| Model::Claude3Opus
|
| Model::Claude3Opus
|
||||||
| Model::Claude3Sonnet
|
| Model::Claude3Sonnet
|
||||||
|
|||||||
@@ -501,8 +501,10 @@ impl Database {
|
|||||||
|
|
||||||
/// Returns all channels for the user with the given ID.
|
/// Returns all channels for the user with the given ID.
|
||||||
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
|
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 })
|
self.weak_transaction(
|
||||||
.await
|
|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
|
/// Returns all channels for the user with the given ID that are descendants
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ impl Database {
|
|||||||
user_b_busy: bool,
|
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_a_participant = Alias::new("user_a_participant");
|
||||||
let user_b_participant = Alias::new("user_b_participant");
|
let user_b_participant = Alias::new("user_b_participant");
|
||||||
let mut db_contacts = contact::Entity::find()
|
let mut db_contacts = contact::Entity::find()
|
||||||
@@ -91,7 +91,7 @@ impl Database {
|
|||||||
|
|
||||||
/// Returns whether the given user is a busy (on a call).
|
/// Returns whether the given user is a busy (on a call).
|
||||||
pub async fn is_user_busy(&self, user_id: UserId) -> Result<bool> {
|
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()
|
let participant = room_participant::Entity::find()
|
||||||
.filter(room_participant::Column::UserId.eq(user_id))
|
.filter(room_participant::Column::UserId.eq(user_id))
|
||||||
.one(&*tx)
|
.one(&*tx)
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ impl Database {
|
|||||||
&self,
|
&self,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
) -> Result<Option<proto::IncomingCall>> {
|
) -> Result<Option<proto::IncomingCall>> {
|
||||||
self.transaction(|tx| async move {
|
self.weak_transaction(|tx| async move {
|
||||||
let pending_participant = room_participant::Entity::find()
|
let pending_participant = room_participant::Entity::find()
|
||||||
.filter(
|
.filter(
|
||||||
room_participant::Column::UserId
|
room_participant::Column::UserId
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ pub use token::*;
|
|||||||
|
|
||||||
pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial";
|
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
|
/// The default value to use for maximum spend per month if the user did not
|
||||||
/// explicitly set a maximum spend.
|
/// explicitly set a maximum spend.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::db::billing_subscription::SubscriptionKind;
|
use crate::db::billing_subscription::SubscriptionKind;
|
||||||
use crate::db::{billing_customer, billing_subscription, user};
|
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 crate::{Config, db::billing_preference};
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
@@ -84,7 +84,7 @@ impl LlmTokenClaims {
|
|||||||
.any(|flag| flag == "llm-closed-beta"),
|
.any(|flag| flag == "llm-closed-beta"),
|
||||||
bypass_account_age_check: feature_flags
|
bypass_account_age_check: feature_flags
|
||||||
.iter()
|
.iter()
|
||||||
.any(|flag| flag == "bypass-account-age-check"),
|
.any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG),
|
||||||
can_use_web_search_tool: true,
|
can_use_web_search_tool: true,
|
||||||
use_llm_request_queue: feature_flags.iter().any(|flag| flag == "llm-request-queue"),
|
use_llm_request_queue: feature_flags.iter().any(|flag| flag == "llm-request-queue"),
|
||||||
plan,
|
plan,
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ use crate::api::billing::find_or_create_billing_customer;
|
|||||||
use crate::api::{CloudflareIpCountryHeader, SystemIdHeader};
|
use crate::api::{CloudflareIpCountryHeader, SystemIdHeader};
|
||||||
use crate::db::billing_subscription::SubscriptionKind;
|
use crate::db::billing_subscription::SubscriptionKind;
|
||||||
use crate::llm::db::LlmDatabase;
|
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::stripe_client::StripeCustomerId;
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState, Error, Result, auth,
|
AppState, Error, Result, auth,
|
||||||
@@ -65,7 +68,7 @@ use std::{
|
|||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::{
|
sync::{
|
||||||
Arc, OnceLock,
|
Arc, OnceLock,
|
||||||
atomic::{AtomicBool, Ordering::SeqCst},
|
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
|
||||||
},
|
},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
@@ -86,10 +89,36 @@ pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(15);
|
|||||||
const MESSAGE_COUNT_PER_PAGE: usize = 100;
|
const MESSAGE_COUNT_PER_PAGE: usize = 100;
|
||||||
const MAX_MESSAGE_LEN: usize = 1024;
|
const MAX_MESSAGE_LEN: usize = 1024;
|
||||||
const NOTIFICATION_COUNT_PER_PAGE: usize = 50;
|
const NOTIFICATION_COUNT_PER_PAGE: usize = 50;
|
||||||
|
const MAX_CONCURRENT_CONNECTIONS: usize = 512;
|
||||||
|
|
||||||
|
static CONCURRENT_CONNECTIONS: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
type MessageHandler =
|
type MessageHandler =
|
||||||
Box<dyn Send + Sync + Fn(Box<dyn AnyTypedEnvelope>, Session) -> BoxFuture<'static, ()>>;
|
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> {
|
struct Response<R> {
|
||||||
peer: Arc<Peer>,
|
peer: Arc<Peer>,
|
||||||
receipt: Receipt<R>,
|
receipt: Receipt<R>,
|
||||||
@@ -722,6 +751,7 @@ impl Server {
|
|||||||
system_id: Option<String>,
|
system_id: Option<String>,
|
||||||
send_connection_id: Option<oneshot::Sender<ConnectionId>>,
|
send_connection_id: Option<oneshot::Sender<ConnectionId>>,
|
||||||
executor: Executor,
|
executor: Executor,
|
||||||
|
connection_guard: Option<ConnectionGuard>,
|
||||||
) -> impl Future<Output = ()> + use<> {
|
) -> impl Future<Output = ()> + use<> {
|
||||||
let this = self.clone();
|
let this = self.clone();
|
||||||
let span = info_span!("handle connection", %address,
|
let span = info_span!("handle connection", %address,
|
||||||
@@ -742,6 +772,7 @@ impl Server {
|
|||||||
tracing::error!("server is tearing down");
|
tracing::error!("server is tearing down");
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let (connection_id, handle_io, mut incoming_rx) = this
|
let (connection_id, handle_io, mut incoming_rx) = this
|
||||||
.peer
|
.peer
|
||||||
.add_connection(connection, {
|
.add_connection(connection, {
|
||||||
@@ -783,6 +814,7 @@ impl Server {
|
|||||||
tracing::error!(?error, "failed to send initial client update");
|
tracing::error!(?error, "failed to send initial client update");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
drop(connection_guard);
|
||||||
|
|
||||||
let handle_io = handle_io.fuse();
|
let handle_io = handle_io.fuse();
|
||||||
futures::pin_mut!(handle_io);
|
futures::pin_mut!(handle_io);
|
||||||
@@ -1154,6 +1186,19 @@ pub async fn handle_websocket_request(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let socket_address = socket_address.to_string();
|
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| {
|
ws.on_upgrade(move |socket| {
|
||||||
let socket = socket
|
let socket = socket
|
||||||
.map_ok(to_tungstenite_message)
|
.map_ok(to_tungstenite_message)
|
||||||
@@ -1171,6 +1216,7 @@ pub async fn handle_websocket_request(
|
|||||||
system_id_header.map(|header| header.to_string()),
|
system_id_header.map(|header| header.to_string()),
|
||||||
None,
|
None,
|
||||||
Executor::Production,
|
Executor::Production,
|
||||||
|
Some(connection_guard),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -2773,8 +2819,12 @@ async fn make_update_user_plan_message(
|
|||||||
(None, None)
|
(None, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
let account_too_young =
|
let bypass_account_age_check = feature_flags
|
||||||
!matches!(plan, proto::Plan::ZedPro) && user.account_age() < MIN_ACCOUNT_AGE_FOR_LLM_USE;
|
.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 {
|
Ok(proto::UpdateUserPlan {
|
||||||
plan: plan.into(),
|
plan: plan.into(),
|
||||||
@@ -4075,9 +4125,6 @@ async fn accept_terms_of_service(
|
|||||||
Ok(())
|
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(
|
async fn get_llm_api_token(
|
||||||
_request: proto::GetLlmToken,
|
_request: proto::GetLlmToken,
|
||||||
response: Response<proto::GetLlmToken>,
|
response: Response<proto::GetLlmToken>,
|
||||||
|
|||||||
@@ -258,6 +258,7 @@ impl TestServer {
|
|||||||
None,
|
None,
|
||||||
Some(connection_id_tx),
|
Some(connection_id_tx),
|
||||||
Executor::Deterministic(cx.background_executor().clone()),
|
Executor::Deterministic(cx.background_executor().clone()),
|
||||||
|
None,
|
||||||
))
|
))
|
||||||
.detach();
|
.detach();
|
||||||
let connection_id = connection_id_rx.await.map_err(|e| {
|
let connection_id = connection_id_rx.await.map_err(|e| {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ use settings::SettingsStore;
|
|||||||
use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace};
|
use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace};
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
env::home_dir,
|
env,
|
||||||
ffi::OsString,
|
ffi::OsString,
|
||||||
mem,
|
mem,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
@@ -486,14 +486,11 @@ impl Copilot {
|
|||||||
env,
|
env,
|
||||||
};
|
};
|
||||||
|
|
||||||
let root_path = home_dir();
|
let root_path = if cfg!(target_os = "windows") {
|
||||||
let root_path = root_path.as_deref().unwrap_or_else(|| {
|
Path::new("C:/")
|
||||||
if cfg!(target_os = "windows") {
|
} else {
|
||||||
Path::new("C:/")
|
Path::new("/")
|
||||||
} else {
|
};
|
||||||
Path::new("/")
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let server_name = LanguageServerName("copilot".into());
|
let server_name = LanguageServerName("copilot".into());
|
||||||
let server = LanguageServer::new(
|
let server = LanguageServer::new(
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ doctest = false
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
|
collections.workspace = true
|
||||||
dap.workspace = true
|
dap.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
use anyhow::Result;
|
use anyhow::{Result, bail};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use collections::FxHashMap;
|
||||||
use dap::{
|
use dap::{
|
||||||
DebugRequest, StartDebuggingRequestArguments,
|
DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
|
||||||
adapters::{
|
adapters::{
|
||||||
DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
|
DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use gpui::{AsyncApp, SharedString};
|
use gpui::{AsyncApp, SharedString};
|
||||||
use language::LanguageName;
|
use language::LanguageName;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::{ffi::OsStr, sync::Arc};
|
||||||
use task::{DebugScenario, ZedDebugConfig};
|
use task::{DebugScenario, ZedDebugConfig};
|
||||||
use util::command::new_smol_command;
|
use util::command::new_smol_command;
|
||||||
|
|
||||||
@@ -21,6 +23,18 @@ impl RubyDebugAdapter {
|
|||||||
const ADAPTER_NAME: &'static str = "Ruby";
|
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)]
|
#[async_trait(?Send)]
|
||||||
impl DebugAdapter for RubyDebugAdapter {
|
impl DebugAdapter for RubyDebugAdapter {
|
||||||
fn name(&self) -> DebugAdapterName {
|
fn name(&self) -> DebugAdapterName {
|
||||||
@@ -31,185 +45,70 @@ impl DebugAdapter for RubyDebugAdapter {
|
|||||||
Some(SharedString::new_static("Ruby").into())
|
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 {
|
async fn dap_schema(&self) -> serde_json::Value {
|
||||||
json!({
|
json!({
|
||||||
"oneOf": [
|
"type": "object",
|
||||||
{
|
"properties": {
|
||||||
"allOf": [
|
"command": {
|
||||||
{
|
"type": "string",
|
||||||
"type": "object",
|
"description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
"script": {
|
||||||
"allOf": [
|
"type": "string",
|
||||||
{
|
"description": "Absolute path to a Ruby file."
|
||||||
"type": "object",
|
},
|
||||||
"required": ["request"],
|
"cwd": {
|
||||||
"properties": {
|
"type": "string",
|
||||||
"request": {
|
"description": "Directory to execute the program in",
|
||||||
"type": "string",
|
"default": "${ZED_WORKTREE_ROOT}"
|
||||||
"enum": ["attach"],
|
},
|
||||||
"description": "Request to attach to an existing process"
|
"args": {
|
||||||
}
|
"type": "array",
|
||||||
}
|
"description": "Command line arguments passed to the program",
|
||||||
},
|
"items": {
|
||||||
{
|
"type": "string"
|
||||||
"type": "object",
|
},
|
||||||
"properties": {
|
"default": []
|
||||||
"rdbgPath": {
|
},
|
||||||
"type": "string",
|
"env": {
|
||||||
"description": "Location of the rdbg executable"
|
"type": "object",
|
||||||
},
|
"description": "Additional environment variables to pass to the debugging (and debugged) process",
|
||||||
"debugPort": {
|
"default": {}
|
||||||
"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": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
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) => {
|
DebugRequest::Launch(launch) => {
|
||||||
config.insert("request".to_string(), json!("launch"));
|
let config = RubyDebugConfig {
|
||||||
config.insert("script".to_string(), json!(launch.program));
|
script_or_command: Some(launch.program),
|
||||||
config.insert("command".to_string(), json!("ruby"));
|
script: None,
|
||||||
|
command: None,
|
||||||
|
args: launch.args,
|
||||||
|
env: launch.env,
|
||||||
|
cwd: launch.cwd.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
if !launch.args.is_empty() {
|
let config = serde_json::to_value(config)?;
|
||||||
config.insert("args".to_string(), json!(launch.args));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !launch.env.is_empty() {
|
Ok(DebugScenario {
|
||||||
config.insert("env".to_string(), json!(launch.env));
|
adapter: zed_scenario.adapter,
|
||||||
}
|
label: zed_scenario.label,
|
||||||
|
config,
|
||||||
if let Some(cwd) = &launch.cwd {
|
tcp_connection: None,
|
||||||
config.insert("cwd".to_string(), json!(cwd));
|
build: None,
|
||||||
}
|
})
|
||||||
|
|
||||||
// Ruby stops on entry so there's no need to handle that case
|
|
||||||
}
|
}
|
||||||
DebugRequest::Attach(attach) => {
|
DebugRequest::Attach(_) => {
|
||||||
config.insert("request".to_string(), json!("attach"));
|
anyhow::bail!("Attach requests are unsupported");
|
||||||
|
|
||||||
config.insert("processId".to_string(), json!(attach.process_id));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
async fn get_binary(
|
||||||
@@ -247,13 +146,34 @@ impl DebugAdapter for RubyDebugAdapter {
|
|||||||
|
|
||||||
let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
|
let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
|
||||||
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
|
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(),
|
"--open".to_string(),
|
||||||
format!("--port={}", port),
|
format!("--port={}", port),
|
||||||
format!("--host={}", host),
|
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 {
|
Ok(DebugAdapterBinary {
|
||||||
command: rdbg_path.to_string_lossy().to_string(),
|
command: rdbg_path.to_string_lossy().to_string(),
|
||||||
arguments,
|
arguments,
|
||||||
@@ -262,8 +182,12 @@ impl DebugAdapter for RubyDebugAdapter {
|
|||||||
port,
|
port,
|
||||||
timeout,
|
timeout,
|
||||||
}),
|
}),
|
||||||
cwd: None,
|
cwd: Some(
|
||||||
envs: std::collections::HashMap::default(),
|
ruby_config
|
||||||
|
.cwd
|
||||||
|
.unwrap_or(delegate.worktree_root_path().to_owned()),
|
||||||
|
),
|
||||||
|
envs: ruby_config.env.into_iter().collect(),
|
||||||
request_args: StartDebuggingRequestArguments {
|
request_args: StartDebuggingRequestArguments {
|
||||||
request: self.request_kind(&definition.config)?,
|
request: self.request_kind(&definition.config)?,
|
||||||
configuration: definition.config.clone(),
|
configuration: definition.config.clone(),
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ use std::time::Duration;
|
|||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use dap::StackFrameId;
|
use dap::StackFrameId;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, MouseButton, ScrollStrategy,
|
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, MouseButton, Stateful,
|
||||||
Stateful, Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list,
|
Subscription, Task, WeakEntity, list,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::StackTraceView;
|
use crate::StackTraceView;
|
||||||
@@ -35,7 +35,7 @@ pub struct StackFrameList {
|
|||||||
selected_ix: Option<usize>,
|
selected_ix: Option<usize>,
|
||||||
opened_stack_frame_id: Option<StackFrameId>,
|
opened_stack_frame_id: Option<StackFrameId>,
|
||||||
scrollbar_state: ScrollbarState,
|
scrollbar_state: ScrollbarState,
|
||||||
scroll_handle: UniformListScrollHandle,
|
list_state: ListState,
|
||||||
_refresh_task: Task<()>,
|
_refresh_task: Task<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +54,6 @@ impl StackFrameList {
|
|||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let focus_handle = cx.focus_handle();
|
let focus_handle = cx.focus_handle();
|
||||||
let scroll_handle = UniformListScrollHandle::new();
|
|
||||||
|
|
||||||
let _subscription =
|
let _subscription =
|
||||||
cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
|
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 {
|
let mut this = Self {
|
||||||
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
|
|
||||||
session,
|
session,
|
||||||
workspace,
|
workspace,
|
||||||
focus_handle,
|
focus_handle,
|
||||||
@@ -77,7 +84,8 @@ impl StackFrameList {
|
|||||||
entries: Default::default(),
|
entries: Default::default(),
|
||||||
selected_ix: None,
|
selected_ix: None,
|
||||||
opened_stack_frame_id: None,
|
opened_stack_frame_id: None,
|
||||||
scroll_handle,
|
list_state,
|
||||||
|
scrollbar_state,
|
||||||
_refresh_task: Task::ready(()),
|
_refresh_task: Task::ready(()),
|
||||||
};
|
};
|
||||||
this.schedule_refresh(true, window, cx);
|
this.schedule_refresh(true, window, cx);
|
||||||
@@ -214,6 +222,7 @@ impl StackFrameList {
|
|||||||
self.selected_ix = ix;
|
self.selected_ix = ix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.list_state.reset(self.entries.len());
|
||||||
cx.emit(StackFrameListEvent::BuiltEntries);
|
cx.emit(StackFrameListEvent::BuiltEntries);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@@ -555,10 +564,6 @@ impl StackFrameList {
|
|||||||
|
|
||||||
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
|
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
|
||||||
self.selected_ix = ix;
|
self.selected_ix = ix;
|
||||||
if let Some(ix) = self.selected_ix {
|
|
||||||
self.scroll_handle
|
|
||||||
.scroll_to_item(ix, ScrollStrategy::Center);
|
|
||||||
}
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,15 +647,8 @@ impl StackFrameList {
|
|||||||
self.activate_selected_entry(window, cx);
|
self.activate_selected_entry(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render_list(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
uniform_list(
|
list(self.list_state.clone()).size_full()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -698,7 +698,7 @@ impl EditorActionId {
|
|||||||
// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
|
// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
|
||||||
|
|
||||||
type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range<Anchor>]>);
|
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)]
|
#[derive(Default)]
|
||||||
struct ScrollbarMarkerState {
|
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
|
/// Represents a breakpoint indicator that shows up when hovering over lines in the gutter that don't have
|
||||||
/// a breakpoint on them.
|
/// a breakpoint on them.
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
struct PhantomBreakpointIndicator {
|
struct PhantomBreakpointIndicator {
|
||||||
display_row: DisplayRow,
|
display_row: DisplayRow,
|
||||||
/// There's a small debounce between hovering over the line and showing the indicator.
|
/// There's a small debounce between hovering over the line and showing the indicator.
|
||||||
@@ -931,6 +931,7 @@ struct PhantomBreakpointIndicator {
|
|||||||
is_active: bool,
|
is_active: bool,
|
||||||
collides_with_existing_breakpoint: bool,
|
collides_with_existing_breakpoint: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`].
|
/// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`].
|
||||||
///
|
///
|
||||||
/// See the [module level documentation](self) for more information.
|
/// See the [module level documentation](self) for more information.
|
||||||
@@ -1199,10 +1200,12 @@ struct SelectionHistoryEntry {
|
|||||||
add_selections_state: Option<AddSelectionsState>,
|
add_selections_state: Option<AddSelectionsState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
enum SelectionHistoryMode {
|
enum SelectionHistoryMode {
|
||||||
Normal,
|
Normal,
|
||||||
Undoing,
|
Undoing,
|
||||||
Redoing,
|
Redoing,
|
||||||
|
Skipping,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||||
@@ -1236,11 +1239,19 @@ struct SelectionHistory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SelectionHistory {
|
impl SelectionHistory {
|
||||||
|
#[track_caller]
|
||||||
fn insert_transaction(
|
fn insert_transaction(
|
||||||
&mut self,
|
&mut self,
|
||||||
transaction_id: TransactionId,
|
transaction_id: TransactionId,
|
||||||
selections: Arc<[Selection<Anchor>]>,
|
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
|
self.selections_by_transaction
|
||||||
.insert(transaction_id, (selections, None));
|
.insert(transaction_id, (selections, None));
|
||||||
}
|
}
|
||||||
@@ -1270,6 +1281,7 @@ impl SelectionHistory {
|
|||||||
}
|
}
|
||||||
SelectionHistoryMode::Undoing => self.push_redo(entry),
|
SelectionHistoryMode::Undoing => self.push_redo(entry),
|
||||||
SelectionHistoryMode::Redoing => self.push_undo(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.end_selection(window, cx);
|
||||||
|
editor.selection_history.mode = SelectionHistoryMode::Normal;
|
||||||
|
|
||||||
editor.scroll_manager.show_scrollbars(window, cx);
|
editor.scroll_manager.show_scrollbars(window, cx);
|
||||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut editor, &buffer, cx);
|
jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut editor, &buffer, cx);
|
||||||
|
|
||||||
@@ -14211,18 +14227,20 @@ impl Editor {
|
|||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction);
|
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() {
|
if let Some(entry) = self.selection_history.undo_stack.pop_back() {
|
||||||
self.change_selections(None, window, cx, |s| {
|
self.selection_history.mode = SelectionHistoryMode::Undoing;
|
||||||
s.select_anchors(entry.selections.to_vec())
|
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_next_state = entry.select_next_state;
|
||||||
self.select_prev_state = entry.select_prev_state;
|
self.select_prev_state = entry.select_prev_state;
|
||||||
self.add_selections_state = entry.add_selections_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(
|
pub fn redo_selection(
|
||||||
@@ -14232,18 +14250,20 @@ impl Editor {
|
|||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction);
|
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() {
|
if let Some(entry) = self.selection_history.redo_stack.pop_back() {
|
||||||
self.change_selections(None, window, cx, |s| {
|
self.selection_history.mode = SelectionHistoryMode::Redoing;
|
||||||
s.select_anchors(entry.selections.to_vec())
|
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_next_state = entry.select_next_state;
|
||||||
self.select_prev_state = entry.select_prev_state;
|
self.select_prev_state = entry.select_prev_state;
|
||||||
self.add_selections_state = entry.add_selections_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(
|
pub fn expand_excerpts(
|
||||||
@@ -18377,12 +18397,12 @@ impl Editor {
|
|||||||
|
|
||||||
pub fn highlight_gutter<T: 'static>(
|
pub fn highlight_gutter<T: 'static>(
|
||||||
&mut self,
|
&mut self,
|
||||||
ranges: &[Range<Anchor>],
|
ranges: impl Into<Vec<Range<Anchor>>>,
|
||||||
color_fetcher: fn(&App) -> Hsla,
|
color_fetcher: fn(&App) -> Hsla,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
self.gutter_highlights
|
self.gutter_highlights
|
||||||
.insert(TypeId::of::<T>(), (color_fetcher, Arc::from(ranges)));
|
.insert(TypeId::of::<T>(), (color_fetcher, ranges.into()));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18394,6 +18414,65 @@ impl Editor {
|
|||||||
self.gutter_highlights.remove(&TypeId::of::<T>())
|
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")]
|
#[cfg(feature = "test-support")]
|
||||||
pub fn all_text_background_highlights(
|
pub fn all_text_background_highlights(
|
||||||
&self,
|
&self,
|
||||||
@@ -19939,12 +20018,15 @@ impl Editor {
|
|||||||
if !selections.is_empty() {
|
if !selections.is_empty() {
|
||||||
let snapshot =
|
let snapshot =
|
||||||
buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx));
|
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| {
|
self.change_selections(None, window, cx, |s| {
|
||||||
s.select_ranges(selections.into_iter().map(|(start, end)| {
|
s.select_ranges(selections.into_iter().map(|(start, end)| {
|
||||||
snapshot.clip_offset(start, Bias::Left)
|
snapshot.clip_offset(start, Bias::Left)
|
||||||
..snapshot.clip_offset(end, Bias::Right)
|
..snapshot.clip_offset(end, Bias::Right)
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
self.selection_history.mode = SelectionHistoryMode::Normal;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1907,7 +1907,6 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
|
|||||||
DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
|
DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
|
||||||
])
|
])
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
|
editor.move_to_previous_word_start(&MoveToPreviousWordStart, 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);
|
||||||
|
|
||||||
@@ -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);
|
assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
|
||||||
|
|
||||||
editor.move_to_next_word_end(&MoveToNextWordEnd, window, 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);
|
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.move_right(&MoveRight, window, cx);
|
||||||
editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
|
editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
|
||||||
assert_selection_ranges(
|
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,
|
editor,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
|
editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
|
||||||
assert_selection_ranges(
|
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,
|
editor,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
|
editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
|
||||||
assert_selection_ranges(
|
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,
|
editor,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -21942,7 +21941,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
|
|||||||
.expect("created a singleton buffer")
|
.expect("created a singleton buffer")
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.remote_id();
|
.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);
|
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"
|
"Cursor movement should not trigger diagnostic request"
|
||||||
);
|
);
|
||||||
ensure_result_id(Some("2".to_string()), cx);
|
ensure_result_id(Some("2".to_string()), cx);
|
||||||
|
|
||||||
// Multiple rapid edits should be debounced
|
// Multiple rapid edits should be debounced
|
||||||
for _ in 0..5 {
|
for _ in 0..5 {
|
||||||
editor.update_in(cx, |editor, window, cx| {
|
editor.update_in(cx, |editor, window, cx| {
|
||||||
|
|||||||
@@ -1031,7 +1031,7 @@ impl EditorElement {
|
|||||||
editor.set_gutter_hovered(gutter_hovered, cx);
|
editor.set_gutter_hovered(gutter_hovered, cx);
|
||||||
editor.mouse_cursor_hidden = false;
|
editor.mouse_cursor_hidden = false;
|
||||||
|
|
||||||
if gutter_hovered {
|
let breakpoint_indicator = if gutter_hovered {
|
||||||
let new_point = position_map
|
let new_point = position_map
|
||||||
.point_for_position(event.position)
|
.point_for_position(event.position)
|
||||||
.previous_valid;
|
.previous_valid;
|
||||||
@@ -1045,7 +1045,6 @@ impl EditorElement {
|
|||||||
.buffer_for_excerpt(buffer_anchor.excerpt_id)
|
.buffer_for_excerpt(buffer_anchor.excerpt_id)
|
||||||
.and_then(|buffer| buffer.file().map(|file| (buffer, file)))
|
.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 as_point = text::ToPoint::to_point(&buffer_anchor.text_anchor, buffer_snapshot);
|
||||||
|
|
||||||
let is_visible = editor
|
let is_visible = editor
|
||||||
@@ -1073,38 +1072,43 @@ impl EditorElement {
|
|||||||
.is_some()
|
.is_some()
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
|
if !is_visible {
|
||||||
display_row: new_point.row(),
|
editor.gutter_breakpoint_indicator.1.get_or_insert_with(|| {
|
||||||
is_active: is_visible,
|
cx.spawn(async move |this, cx| {
|
||||||
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 {
|
|
||||||
cx.background_executor()
|
cx.background_executor()
|
||||||
.timer(Duration::from_millis(200))
|
.timer(Duration::from_millis(200))
|
||||||
.await;
|
.await;
|
||||||
}
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
if let Some(indicator) = this.gutter_breakpoint_indicator.0.as_mut() {
|
if let Some(indicator) = this.gutter_breakpoint_indicator.0.as_mut()
|
||||||
indicator.is_active = true;
|
{
|
||||||
}
|
indicator.is_active = true;
|
||||||
|
cx.notify();
|
||||||
cx.notify();
|
}
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
})
|
})
|
||||||
.ok();
|
});
|
||||||
})
|
}
|
||||||
});
|
|
||||||
|
Some(PhantomBreakpointIndicator {
|
||||||
|
display_row: new_point.row(),
|
||||||
|
is_active: is_visible,
|
||||||
|
collides_with_existing_breakpoint: has_existing_breakpoint,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
editor.gutter_breakpoint_indicator = (None, None);
|
editor.gutter_breakpoint_indicator.1 = None;
|
||||||
|
None
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
// Don't trigger hover popover if mouse is hovering over context menu
|
||||||
if text_hitbox.is_hovered(window) {
|
if text_hitbox.is_hovered(window) {
|
||||||
@@ -7325,6 +7329,17 @@ impl LineWithInvisibles {
|
|||||||
paint(window, cx);
|
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:
|
// For a whitespace to be on a boundary, any of the following conditions need to be met:
|
||||||
// - It is a tab
|
// - It is a tab
|
||||||
// - It is adjacent to an edge (start or end)
|
// - It is adjacent to an edge (start or end)
|
||||||
|
|||||||
@@ -266,10 +266,11 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa
|
|||||||
|
|
||||||
let mut is_first_iteration = true;
|
let mut is_first_iteration = true;
|
||||||
find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
|
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
|
if is_first_iteration
|
||||||
&& classifier.is_punctuation(right)
|
&& classifier.is_punctuation(right)
|
||||||
&& !classifier.is_punctuation(left)
|
&& !classifier.is_punctuation(left)
|
||||||
|
&& left != '\n'
|
||||||
{
|
{
|
||||||
is_first_iteration = false;
|
is_first_iteration = false;
|
||||||
return 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 classifier = map.buffer_snapshot.char_classifier_at(raw_point);
|
||||||
let mut is_first_iteration = true;
|
let mut is_first_iteration = true;
|
||||||
find_boundary(map, point, FindRange::MultiLine, |left, right| {
|
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
|
if is_first_iteration
|
||||||
&& classifier.is_punctuation(left)
|
&& classifier.is_punctuation(left)
|
||||||
&& !classifier.is_punctuation(right)
|
&& !classifier.is_punctuation(right)
|
||||||
|
&& right != '\n'
|
||||||
{
|
{
|
||||||
is_first_iteration = false;
|
is_first_iteration = false;
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -240,7 +240,8 @@ impl EditorTestContext {
|
|||||||
// unlike cx.simulate_keystrokes(), this does not run_until_parked
|
// unlike cx.simulate_keystrokes(), this does not run_until_parked
|
||||||
// so you can use it to test detailed timing
|
// so you can use it to test detailed timing
|
||||||
pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
|
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);
|
self.cx.dispatch_keystroke(self.window, keystroke);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -248,6 +248,8 @@ fn conflicts_updated(
|
|||||||
removed_block_ids.insert(block_id);
|
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::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
|
||||||
editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx);
|
editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx);
|
||||||
editor
|
editor
|
||||||
@@ -325,8 +327,7 @@ fn update_conflict_highlighting(
|
|||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) {
|
) {
|
||||||
log::debug!("update conflict highlighting for {conflict:?}");
|
log::debug!("update conflict highlighting for {conflict:?}");
|
||||||
let theme = cx.theme().clone();
|
|
||||||
let colors = theme.colors();
|
|
||||||
let outer_start = buffer
|
let outer_start = buffer
|
||||||
.anchor_in_excerpt(excerpt_id, conflict.range.start)
|
.anchor_in_excerpt(excerpt_id, conflict.range.start)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -346,26 +347,29 @@ fn update_conflict_highlighting(
|
|||||||
.anchor_in_excerpt(excerpt_id, conflict.theirs.end)
|
.anchor_in_excerpt(excerpt_id, conflict.theirs.end)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let ours_background = colors.version_control_conflict_ours_background;
|
let ours_background = cx.theme().colors().version_control_conflict_marker_ours;
|
||||||
let ours_marker = colors.version_control_conflict_ours_marker_background;
|
let theirs_background = cx.theme().colors().version_control_conflict_marker_theirs;
|
||||||
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 options = RowHighlightOptions {
|
let options = RowHighlightOptions {
|
||||||
include_gutter: false,
|
include_gutter: true,
|
||||||
..Default::default()
|
..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.
|
// Prevent diff hunk highlighting within the entire conflict region.
|
||||||
editor.highlight_rows::<ConflictsOuter>(
|
editor.highlight_rows::<ConflictsOuter>(outer_start..outer_end, theirs_background, options, cx);
|
||||||
outer_start..outer_end,
|
editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
|
||||||
divider_background,
|
editor.highlight_rows::<ConflictsOursMarker>(
|
||||||
|
outer_start..our_start,
|
||||||
|
ours_background,
|
||||||
options,
|
options,
|
||||||
cx,
|
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>(
|
editor.highlight_rows::<ConflictsTheirs>(
|
||||||
their_start..their_end,
|
their_start..their_end,
|
||||||
theirs_background,
|
theirs_background,
|
||||||
@@ -374,7 +378,7 @@ fn update_conflict_highlighting(
|
|||||||
);
|
);
|
||||||
editor.highlight_rows::<ConflictsTheirsMarker>(
|
editor.highlight_rows::<ConflictsTheirsMarker>(
|
||||||
their_end..outer_end,
|
their_end..outer_end,
|
||||||
theirs_marker,
|
theirs_background,
|
||||||
options,
|
options,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -512,6 +516,9 @@ pub(crate) fn resolve_conflict(
|
|||||||
let end = snapshot
|
let end = snapshot
|
||||||
.anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
|
.anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
editor.remove_gutter_highlights::<ConflictsOuter>(vec![start..end], cx);
|
||||||
|
|
||||||
editor.remove_highlighted_rows::<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::<ConflictsOurs>(vec![start..end], cx);
|
||||||
editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
|
editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
|
||||||
|
|||||||
@@ -37,10 +37,10 @@ use crate::{
|
|||||||
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
|
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
|
||||||
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
|
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
|
||||||
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
|
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
|
||||||
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
|
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder,
|
||||||
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
|
PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle,
|
||||||
SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window,
|
Reservation, ScreenCaptureSource, SharedString, SubscriberSet, Subscription, SvgRenderer, Task,
|
||||||
WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
|
TextSystem, Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
|
||||||
colors::{Colors, GlobalColors},
|
colors::{Colors, GlobalColors},
|
||||||
current_platform, hash, init_app_menus,
|
current_platform, hash, init_app_menus,
|
||||||
};
|
};
|
||||||
@@ -262,6 +262,7 @@ pub struct App {
|
|||||||
pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
|
pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
|
||||||
pub(crate) focus_handles: Arc<FocusMap>,
|
pub(crate) focus_handles: Arc<FocusMap>,
|
||||||
pub(crate) keymap: Rc<RefCell<Keymap>>,
|
pub(crate) keymap: Rc<RefCell<Keymap>>,
|
||||||
|
pub(crate) keyboard_mapper: Box<dyn PlatformKeyboardMapper>,
|
||||||
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
|
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
|
||||||
pub(crate) global_action_listeners:
|
pub(crate) global_action_listeners:
|
||||||
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
|
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 text_system = Arc::new(TextSystem::new(platform.text_system()));
|
||||||
let entities = EntityMap::new();
|
let entities = EntityMap::new();
|
||||||
|
let keyboard_mapper = platform.keyboard_mapper();
|
||||||
let keyboard_layout = platform.keyboard_layout();
|
let keyboard_layout = platform.keyboard_layout();
|
||||||
|
|
||||||
let app = Rc::new_cyclic(|this| AppCell {
|
let app = Rc::new_cyclic(|this| AppCell {
|
||||||
@@ -333,6 +335,7 @@ impl App {
|
|||||||
window_handles: FxHashMap::default(),
|
window_handles: FxHashMap::default(),
|
||||||
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
||||||
keymap: Rc::new(RefCell::new(Keymap::default())),
|
keymap: Rc::new(RefCell::new(Keymap::default())),
|
||||||
|
keyboard_mapper,
|
||||||
keyboard_layout,
|
keyboard_layout,
|
||||||
global_action_listeners: FxHashMap::default(),
|
global_action_listeners: FxHashMap::default(),
|
||||||
pending_effects: VecDeque::new(),
|
pending_effects: VecDeque::new(),
|
||||||
@@ -369,6 +372,7 @@ impl App {
|
|||||||
move || {
|
move || {
|
||||||
if let Some(app) = app.upgrade() {
|
if let Some(app) = app.upgrade() {
|
||||||
let cx = &mut app.borrow_mut();
|
let cx = &mut app.borrow_mut();
|
||||||
|
cx.keyboard_mapper = cx.platform.keyboard_mapper();
|
||||||
cx.keyboard_layout = cx.platform.keyboard_layout();
|
cx.keyboard_layout = cx.platform.keyboard_layout();
|
||||||
cx.keyboard_layout_observers
|
cx.keyboard_layout_observers
|
||||||
.clone()
|
.clone()
|
||||||
@@ -413,6 +417,11 @@ impl App {
|
|||||||
self.quitting = false;
|
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
|
/// Get the id of the current keyboard layout
|
||||||
pub fn keyboard_layout(&self) -> &dyn PlatformKeyboardLayout {
|
pub fn keyboard_layout(&self) -> &dyn PlatformKeyboardLayout {
|
||||||
self.keyboard_layout.as_ref()
|
self.keyboard_layout.as_ref()
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ use crate::{
|
|||||||
BackgroundExecutor, BorrowAppContext, Bounds, ClipboardItem, DrawPhase, Drawable, Element,
|
BackgroundExecutor, BorrowAppContext, Bounds, ClipboardItem, DrawPhase, Drawable, Element,
|
||||||
Empty, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers,
|
Empty, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers,
|
||||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
|
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
|
||||||
Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform,
|
Platform, PlatformKeyboardMapper, Point, Render, Result, Size, Task, TestDispatcher,
|
||||||
TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, WindowBounds,
|
TestPlatform, TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window,
|
||||||
WindowHandle, WindowOptions,
|
WindowBounds, WindowHandle, WindowOptions,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, bail};
|
use anyhow::{anyhow, bail};
|
||||||
use futures::{Stream, StreamExt, channel::oneshot};
|
use futures::{Stream, StreamExt, channel::oneshot};
|
||||||
@@ -397,14 +397,20 @@ impl TestAppContext {
|
|||||||
self.background_executor.run_until_parked()
|
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.
|
/// simulate_keystrokes takes a space-separated list of keys to type.
|
||||||
/// cx.simulate_keystrokes("cmd-shift-p b k s p enter")
|
/// 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.
|
/// 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.
|
/// This will also run the background executor until it's parked.
|
||||||
pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
|
pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
|
||||||
|
let keyboard_mapper = self.keyboard_mapper();
|
||||||
for keystroke in keystrokes
|
for keystroke in keystrokes
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.map(Keystroke::parse)
|
.map(|source| Keystroke::parse(source, keyboard_mapper.as_ref()))
|
||||||
.map(Result::unwrap)
|
.map(Result::unwrap)
|
||||||
{
|
{
|
||||||
self.dispatch_keystroke(window, keystroke);
|
self.dispatch_keystroke(window, keystroke);
|
||||||
@@ -418,7 +424,12 @@ impl TestAppContext {
|
|||||||
/// will type abc into your current editor
|
/// will type abc into your current editor
|
||||||
/// This will also run the background executor until it's parked.
|
/// This will also run the background executor until it's parked.
|
||||||
pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
|
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);
|
self.dispatch_keystroke(window, keystroke);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -538,8 +538,22 @@ mod test {
|
|||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap());
|
cx.dispatch_keystroke(
|
||||||
cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap());
|
*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
|
window
|
||||||
.update(cx, |test_view, _, _| {
|
.update(cx, |test_view, _, _| {
|
||||||
|
|||||||
@@ -310,7 +310,11 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
keymap
|
keymap
|
||||||
.bindings_for_input(
|
.bindings_for_input(
|
||||||
&[Keystroke::parse("ctrl-a").unwrap()],
|
&[Keystroke {
|
||||||
|
modifiers: crate::Modifiers::control(),
|
||||||
|
key: "a".to_owned(),
|
||||||
|
key_char: None
|
||||||
|
}],
|
||||||
&[KeyContext::parse("barf").unwrap()],
|
&[KeyContext::parse("barf").unwrap()],
|
||||||
)
|
)
|
||||||
.0
|
.0
|
||||||
@@ -319,7 +323,11 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
!keymap
|
!keymap
|
||||||
.bindings_for_input(
|
.bindings_for_input(
|
||||||
&[Keystroke::parse("ctrl-a").unwrap()],
|
&[Keystroke {
|
||||||
|
modifiers: crate::Modifiers::control(),
|
||||||
|
key: "a".to_owned(),
|
||||||
|
key_char: None
|
||||||
|
}],
|
||||||
&[KeyContext::parse("editor").unwrap()],
|
&[KeyContext::parse("editor").unwrap()],
|
||||||
)
|
)
|
||||||
.0
|
.0
|
||||||
@@ -330,7 +338,11 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
keymap
|
keymap
|
||||||
.bindings_for_input(
|
.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()],
|
&[KeyContext::parse("editor mode=full").unwrap()],
|
||||||
)
|
)
|
||||||
.0
|
.0
|
||||||
@@ -341,7 +353,11 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
keymap
|
keymap
|
||||||
.bindings_for_input(
|
.bindings_for_input(
|
||||||
&[Keystroke::parse("ctrl-b").unwrap()],
|
&[Keystroke {
|
||||||
|
modifiers: crate::Modifiers::control(),
|
||||||
|
key: "b".to_owned(),
|
||||||
|
key_char: None
|
||||||
|
}],
|
||||||
&[KeyContext::parse("barf").unwrap()],
|
&[KeyContext::parse("barf").unwrap()],
|
||||||
)
|
)
|
||||||
.0
|
.0
|
||||||
@@ -360,8 +376,16 @@ mod tests {
|
|||||||
let mut keymap = Keymap::default();
|
let mut keymap = Keymap::default();
|
||||||
keymap.add_bindings(bindings.clone());
|
keymap.add_bindings(bindings.clone());
|
||||||
|
|
||||||
let space = || Keystroke::parse("space").unwrap();
|
let space = || Keystroke {
|
||||||
let w = || Keystroke::parse("w").unwrap();
|
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 = [space(), w()];
|
||||||
let space_w_w = [space(), w(), w()];
|
let space_w_w = [space(), w(), w()];
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
|
|
||||||
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke};
|
use crate::{
|
||||||
|
Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, PlatformKeyboardMapper,
|
||||||
|
TestKeyboardMapper,
|
||||||
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
/// A keybinding and its associated metadata, from the keymap.
|
/// A keybinding and its associated metadata, from the keymap.
|
||||||
@@ -25,12 +28,20 @@ impl Clone for KeyBinding {
|
|||||||
impl KeyBinding {
|
impl KeyBinding {
|
||||||
/// Construct a new keybinding from the given data. Panics on parse error.
|
/// 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 {
|
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 {
|
let context_predicate = if let Some(context) = context {
|
||||||
Some(KeyBindingContextPredicate::parse(context).unwrap().into())
|
Some(KeyBindingContextPredicate::parse(context).unwrap().into())
|
||||||
} else {
|
} else {
|
||||||
None
|
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.
|
/// Load a keybinding from the given raw data.
|
||||||
@@ -39,10 +50,11 @@ impl KeyBinding {
|
|||||||
action: Box<dyn Action>,
|
action: Box<dyn Action>,
|
||||||
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||||
key_equivalents: Option<&HashMap<char, char>>,
|
key_equivalents: Option<&HashMap<char, char>>,
|
||||||
|
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||||
) -> std::result::Result<Self, InvalidKeystrokeError> {
|
) -> std::result::Result<Self, InvalidKeystrokeError> {
|
||||||
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
|
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
.map(Keystroke::parse)
|
.map(|source| Keystroke::parse(source, keyboard_mapper))
|
||||||
.collect::<std::result::Result<_, _>>()?;
|
.collect::<std::result::Result<_, _>>()?;
|
||||||
|
|
||||||
if let Some(equivalents) = key_equivalents {
|
if let Some(equivalents) = key_equivalents {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
mod app_menu;
|
mod app_menu;
|
||||||
mod keyboard;
|
mod keyboard;
|
||||||
|
mod keycode;
|
||||||
mod keystroke;
|
mod keystroke;
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
@@ -66,6 +67,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
pub use app_menu::*;
|
pub use app_menu::*;
|
||||||
pub use keyboard::*;
|
pub use keyboard::*;
|
||||||
|
pub use keycode::*;
|
||||||
pub use keystroke::*;
|
pub use keystroke::*;
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
#[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_quit(&self, callback: Box<dyn FnMut()>);
|
||||||
fn on_reopen(&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 set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
||||||
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
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_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||||
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
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 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 {
|
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 write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
|
||||||
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
|
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
|
||||||
fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
|
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.
|
/// A handle to a platform's display, e.g. a monitor or laptop screen.
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::{Modifiers, ScanCode};
|
||||||
|
|
||||||
/// A trait for platform-specific keyboard layouts
|
/// A trait for platform-specific keyboard layouts
|
||||||
pub trait PlatformKeyboardLayout {
|
pub trait PlatformKeyboardLayout {
|
||||||
/// Get the keyboard layout ID, which should be unique to the layout
|
/// 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
|
/// Get the keyboard layout display name
|
||||||
fn name(&self) -> &str;
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
590
crates/gpui/src/platform/keycode.rs
Normal file
590
crates/gpui/src/platform/keycode.rs
Normal 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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
|
use anyhow::Context;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
error::Error,
|
error::Error,
|
||||||
fmt::{Display, Write},
|
fmt::{Display, Write},
|
||||||
};
|
};
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
use crate::{PlatformKeyboardMapper, ScanCode};
|
||||||
|
|
||||||
/// A keystroke and associated metadata generated by the platform
|
/// A keystroke and associated metadata generated by the platform
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
||||||
@@ -93,7 +97,10 @@ impl Keystroke {
|
|||||||
/// key_char syntax is only used for generating test events,
|
/// key_char syntax is only used for generating test events,
|
||||||
/// secondary means "cmd" on macOS and "ctrl" on other platforms
|
/// secondary means "cmd" on macOS and "ctrl" on other platforms
|
||||||
/// when matching a key with an key_char set will be matched without it.
|
/// 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 modifiers = Modifiers::none();
|
||||||
let mut key = None;
|
let mut key = None;
|
||||||
let mut key_char = 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(),
|
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 {
|
Ok(Keystroke {
|
||||||
modifiers,
|
modifiers,
|
||||||
|
|||||||
@@ -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 {
|
pub(crate) struct LinuxKeyboardLayout {
|
||||||
id: String,
|
id: String,
|
||||||
@@ -19,3 +35,257 @@ impl LinuxKeyboardLayout {
|
|||||||
Self { id }
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,8 +25,9 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
|
|||||||
use crate::{
|
use crate::{
|
||||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
|
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
|
||||||
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
|
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
|
||||||
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow,
|
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||||
Point, Result, ScreenCaptureSource, Task, WindowAppearance, WindowParams, px,
|
PlatformTextSystem, PlatformWindow, Point, Result, ScreenCaptureSource, Task, WindowAppearance,
|
||||||
|
WindowParams, px,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
#[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())
|
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> {
|
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
|
||||||
self.keyboard_layout()
|
self.keyboard_layout()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
|
CMD_MOD, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
|
||||||
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
|
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NO_MOD, NavigationDirection,
|
||||||
PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase,
|
OPTION_MOD, Pixels, PlatformInput, SHIFT_MOD, ScrollDelta, ScrollWheelEvent, TouchPhase,
|
||||||
platform::mac::{
|
always_use_command_layout, chars_for_modified_key, platform::mac::NSStringExt, point, px,
|
||||||
LMGetKbdType, NSStringExt, TISCopyCurrentKeyboardLayoutInputSource,
|
|
||||||
TISGetInputSourceProperty, UCKeyTranslate, kTISPropertyUnicodeKeyLayoutData,
|
|
||||||
},
|
|
||||||
point, px,
|
|
||||||
};
|
};
|
||||||
use cocoa::{
|
use cocoa::{
|
||||||
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
|
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
|
||||||
base::{YES, id},
|
base::{YES, id},
|
||||||
};
|
};
|
||||||
use core_foundation::data::{CFDataGetBytePtr, CFDataRef};
|
use std::borrow::Cow;
|
||||||
use core_graphics::event::CGKeyCode;
|
|
||||||
use objc::{msg_send, sel, sel_impl};
|
|
||||||
use std::{borrow::Cow, ffi::c_void};
|
|
||||||
|
|
||||||
const BACKSPACE_KEY: u16 = 0x7f;
|
const BACKSPACE_KEY: u16 = 0x7f;
|
||||||
const SPACE_KEY: u16 = b' ' as u16;
|
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()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
use std::ffi::{CStr, c_void};
|
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 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::{
|
use super::{
|
||||||
TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, kTISPropertyInputSourceID,
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ use super::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
|
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
|
||||||
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
|
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
|
||||||
MacDisplay, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
|
MacDisplay, MacKeyboardMapper, MacWindow, Menu, MenuItem, PathPromptOptions, Platform,
|
||||||
PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, ScreenCaptureSource,
|
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
|
||||||
SemanticVersion, Task, WindowAppearance, WindowParams, hash,
|
PlatformWindow, Result, ScreenCaptureSource, SemanticVersion, Task, WindowAppearance,
|
||||||
|
WindowParams, hash,
|
||||||
};
|
};
|
||||||
use anyhow::{Context as _, anyhow};
|
use anyhow::{Context as _, anyhow};
|
||||||
use block::ConcreteBlock;
|
use block::ConcreteBlock;
|
||||||
@@ -846,6 +847,10 @@ impl Platform for MacPlatform {
|
|||||||
self.0.lock().validate_menu_command = Some(callback);
|
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> {
|
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
|
||||||
Box::new(MacKeyboardLayout::new())
|
Box::new(MacKeyboardLayout::new())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||||
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
|
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
|
||||||
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
PlatformKeyboardMapper, PlatformTextSystem, PromptButton, ScreenCaptureFrame,
|
||||||
Size, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
ScreenCaptureSource, ScreenCaptureStream, Size, Task, TestDisplay, TestKeyboardMapper,
|
||||||
|
TestWindow, WindowAppearance, WindowParams, size,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::VecDeque;
|
use collections::VecDeque;
|
||||||
@@ -223,6 +224,10 @@ impl Platform for TestPlatform {
|
|||||||
self.text_system.clone()
|
self.text_system.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper> {
|
||||||
|
Box::new(TestKeyboardMapper::new())
|
||||||
|
}
|
||||||
|
|
||||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
|
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
|
||||||
Box::new(TestKeyboardLayout)
|
Box::new(TestKeyboardLayout)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -702,7 +702,7 @@ fn handle_ime_composition_inner(
|
|||||||
} else {
|
} else {
|
||||||
if lparam & GCS_COMPSTR.0 > 0 {
|
if lparam & GCS_COMPSTR.0 > 0 {
|
||||||
let comp_string = parse_ime_composition_string(ctx, GCS_COMPSTR)?;
|
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);
|
let pos = retrieve_composition_cursor_position(ctx);
|
||||||
pos..pos
|
pos..pos
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use windows::Win32::UI::{
|
use windows::Win32::UI::{
|
||||||
Input::KeyboardAndMouse::{
|
Input::KeyboardAndMouse::{
|
||||||
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, VIRTUAL_KEY, VK_0,
|
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VSC_TO_VK, MapVirtualKeyW, ToUnicode,
|
||||||
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,
|
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_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_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_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
|
VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
|
||||||
},
|
},
|
||||||
WindowsAndMessaging::KL_NAMELENGTH,
|
WindowsAndMessaging::KL_NAMELENGTH,
|
||||||
};
|
};
|
||||||
use windows_core::HSTRING;
|
use windows_core::HSTRING;
|
||||||
|
|
||||||
use crate::{Modifiers, PlatformKeyboardLayout};
|
use crate::{Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper, ScanCode};
|
||||||
|
|
||||||
pub(crate) struct WindowsKeyboardLayout {
|
pub(crate) struct WindowsKeyboardLayout {
|
||||||
id: String,
|
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(
|
pub(crate) fn get_keystroke_key(
|
||||||
vkey: VIRTUAL_KEY,
|
vkey: VIRTUAL_KEY,
|
||||||
scan_code: u32,
|
scan_code: u32,
|
||||||
@@ -82,15 +105,15 @@ fn need_to_convert_to_shifted_key(vkey: VIRTUAL_KEY) -> bool {
|
|||||||
| VK_OEM_MINUS
|
| VK_OEM_MINUS
|
||||||
| VK_OEM_PLUS
|
| VK_OEM_PLUS
|
||||||
| VK_OEM_4
|
| VK_OEM_4
|
||||||
| VK_OEM_5
|
|
||||||
| VK_OEM_6
|
| VK_OEM_6
|
||||||
|
| VK_OEM_5
|
||||||
| VK_OEM_1
|
| VK_OEM_1
|
||||||
| VK_OEM_7
|
| VK_OEM_7
|
||||||
| VK_OEM_COMMA
|
| VK_OEM_COMMA
|
||||||
| VK_OEM_PERIOD
|
| VK_OEM_PERIOD
|
||||||
| VK_OEM_2
|
| VK_OEM_2
|
||||||
| VK_OEM_102
|
| VK_OEM_102
|
||||||
| VK_OEM_8
|
| VK_OEM_8 // Same as VK_OEM_2
|
||||||
| VK_ABNT_C1
|
| VK_ABNT_C1
|
||||||
| VK_0
|
| VK_0
|
||||||
| VK_1
|
| VK_1
|
||||||
@@ -138,3 +161,66 @@ pub(crate) fn generate_key_char(
|
|||||||
}
|
}
|
||||||
None
|
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)))
|
||||||
|
}
|
||||||
|
|||||||
@@ -310,6 +310,10 @@ impl Platform for WindowsPlatform {
|
|||||||
self.text_system.clone()
|
self.text_system.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper> {
|
||||||
|
Box::new(WindowsKeyboardMapper::new())
|
||||||
|
}
|
||||||
|
|
||||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
|
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
|
||||||
Box::new(
|
Box::new(
|
||||||
WindowsKeyboardLayout::new()
|
WindowsKeyboardLayout::new()
|
||||||
|
|||||||
@@ -765,6 +765,8 @@ pub enum ShowWhitespaceSetting {
|
|||||||
/// - It is adjacent to an edge (start or end)
|
/// - It is adjacent to an edge (start or end)
|
||||||
/// - It is adjacent to a whitespace (left or right)
|
/// - It is adjacent to a whitespace (left or right)
|
||||||
Boundary,
|
Boundary,
|
||||||
|
/// Draw whitespaces only after non-whitespace characters.
|
||||||
|
Trailing,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Controls which formatter should be used when formatting code.
|
/// 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.bool_setting("editor.inlineSuggest.enabled", &mut d.show_edit_predictions);
|
||||||
vscode.enum_setting("editor.renderWhitespace", &mut d.show_whitespaces, |s| {
|
vscode.enum_setting("editor.renderWhitespace", &mut d.show_whitespaces, |s| {
|
||||||
Some(match s {
|
Some(match s {
|
||||||
"boundary" | "trailing" => ShowWhitespaceSetting::Boundary,
|
"boundary" => ShowWhitespaceSetting::Boundary,
|
||||||
|
"trailing" => ShowWhitespaceSetting::Trailing,
|
||||||
"selection" => ShowWhitespaceSetting::Selection,
|
"selection" => ShowWhitespaceSetting::Selection,
|
||||||
"all" => ShowWhitespaceSetting::All,
|
"all" => ShowWhitespaceSetting::All,
|
||||||
_ => ShowWhitespaceSetting::None,
|
_ => ShowWhitespaceSetting::None,
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ impl LanguageModel for FakeLanguageModel {
|
|||||||
'static,
|
'static,
|
||||||
Result<
|
Result<
|
||||||
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
||||||
|
LanguageModelCompletionError,
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
let (tx, rx) = mpsc::unbounded();
|
let (tx, rx) = mpsc::unbounded();
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use std::fmt;
|
|||||||
use std::ops::{Add, Sub};
|
use std::ops::{Add, Sub};
|
||||||
use std::str::FromStr as _;
|
use std::str::FromStr as _;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use util::serde::is_default;
|
use util::serde::is_default;
|
||||||
use zed_llm_client::{
|
use zed_llm_client::{
|
||||||
@@ -74,6 +75,8 @@ pub enum LanguageModelCompletionEvent {
|
|||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum LanguageModelCompletionError {
|
pub enum LanguageModelCompletionError {
|
||||||
|
#[error("rate limit exceeded, retry after {0:?}")]
|
||||||
|
RateLimit(Duration),
|
||||||
#[error("received bad input JSON")]
|
#[error("received bad input JSON")]
|
||||||
BadInputJson {
|
BadInputJson {
|
||||||
id: LanguageModelToolUseId,
|
id: LanguageModelToolUseId,
|
||||||
@@ -270,6 +273,7 @@ pub trait LanguageModel: Send + Sync {
|
|||||||
'static,
|
'static,
|
||||||
Result<
|
Result<
|
||||||
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
||||||
|
LanguageModelCompletionError,
|
||||||
>,
|
>,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@@ -277,7 +281,7 @@ pub trait LanguageModel: Send + Sync {
|
|||||||
&self,
|
&self,
|
||||||
request: LanguageModelRequest,
|
request: LanguageModelRequest,
|
||||||
cx: &AsyncApp,
|
cx: &AsyncApp,
|
||||||
) -> BoxFuture<'static, Result<LanguageModelTextStream>> {
|
) -> BoxFuture<'static, Result<LanguageModelTextStream, LanguageModelCompletionError>> {
|
||||||
let future = self.stream_completion(request, cx);
|
let future = self.stream_completion(request, cx);
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use anyhow::Result;
|
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
use smol::lock::{Semaphore, SemaphoreGuardArc};
|
use smol::lock::{Semaphore, SemaphoreGuardArc};
|
||||||
use std::{
|
use std::{
|
||||||
@@ -8,6 +7,8 @@ use std::{
|
|||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::LanguageModelCompletionError;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RateLimiter {
|
pub struct RateLimiter {
|
||||||
semaphore: Arc<Semaphore>,
|
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
|
where
|
||||||
Fut: 'a + Future<Output = Result<T>>,
|
Fut: 'a + Future<Output = Result<T, LanguageModelCompletionError>>,
|
||||||
{
|
{
|
||||||
let guard = self.semaphore.acquire_arc();
|
let guard = self.semaphore.acquire_arc();
|
||||||
async move {
|
async move {
|
||||||
@@ -52,9 +56,12 @@ impl RateLimiter {
|
|||||||
pub fn stream<'a, Fut, T>(
|
pub fn stream<'a, Fut, T>(
|
||||||
&self,
|
&self,
|
||||||
future: Fut,
|
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
|
where
|
||||||
Fut: 'a + Future<Output = Result<T>>,
|
Fut: 'a + Future<Output = Result<T, LanguageModelCompletionError>>,
|
||||||
T: Stream,
|
T: Stream,
|
||||||
{
|
{
|
||||||
let guard = self.semaphore.acquire_arc();
|
let guard = self.semaphore.acquire_arc();
|
||||||
|
|||||||
@@ -387,22 +387,34 @@ impl AnthropicModel {
|
|||||||
&self,
|
&self,
|
||||||
request: anthropic::Request,
|
request: anthropic::Request,
|
||||||
cx: &AsyncApp,
|
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 http_client = self.http_client.clone();
|
||||||
|
|
||||||
let Ok((api_key, api_url)) = cx.read_entity(&self.state, |state, cx| {
|
let Ok((api_key, api_url)) = cx.read_entity(&self.state, |state, cx| {
|
||||||
let settings = &AllLanguageModelSettings::get_global(cx).anthropic;
|
let settings = &AllLanguageModelSettings::get_global(cx).anthropic;
|
||||||
(state.api_key.clone(), settings.api_url.clone())
|
(state.api_key.clone(), settings.api_url.clone())
|
||||||
}) else {
|
}) else {
|
||||||
return futures::future::ready(Err(anyhow!("App state dropped"))).boxed();
|
return futures::future::ready(Err(anyhow!("App state dropped").into())).boxed();
|
||||||
};
|
};
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
let api_key = api_key.context("Missing Anthropic API Key")?;
|
let api_key = api_key.context("Missing Anthropic API Key")?;
|
||||||
let request =
|
let request =
|
||||||
anthropic::stream_completion(http_client.as_ref(), &api_url, &api_key, 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()
|
.boxed()
|
||||||
}
|
}
|
||||||
@@ -473,6 +485,7 @@ impl LanguageModel for AnthropicModel {
|
|||||||
'static,
|
'static,
|
||||||
Result<
|
Result<
|
||||||
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
||||||
|
LanguageModelCompletionError,
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
let request = into_anthropic(
|
let request = into_anthropic(
|
||||||
@@ -484,12 +497,7 @@ impl LanguageModel for AnthropicModel {
|
|||||||
);
|
);
|
||||||
let request = self.stream_completion(request, cx);
|
let request = self.stream_completion(request, cx);
|
||||||
let future = self.request_limiter.stream(async move {
|
let future = self.request_limiter.stream(async move {
|
||||||
let response = request
|
let response = request.await?;
|
||||||
.await
|
|
||||||
.map_err(|err| match err.downcast::<AnthropicError>() {
|
|
||||||
Ok(anthropic_err) => anthropic_err_to_anyhow(anthropic_err),
|
|
||||||
Err(err) => anyhow!(err),
|
|
||||||
})?;
|
|
||||||
Ok(AnthropicEventMapper::new().map_stream(response))
|
Ok(AnthropicEventMapper::new().map_stream(response))
|
||||||
});
|
});
|
||||||
async move { Ok(future.await?.boxed()) }.boxed()
|
async move { Ok(future.await?.boxed()) }.boxed()
|
||||||
|
|||||||
@@ -527,6 +527,7 @@ impl LanguageModel for BedrockModel {
|
|||||||
'static,
|
'static,
|
||||||
Result<
|
Result<
|
||||||
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
||||||
|
LanguageModelCompletionError,
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
let Ok(region) = cx.read_entity(&self.state, |state, _cx| {
|
let Ok(region) = cx.read_entity(&self.state, |state, _cx| {
|
||||||
@@ -539,16 +540,13 @@ impl LanguageModel for BedrockModel {
|
|||||||
.or(settings_region)
|
.or(settings_region)
|
||||||
.unwrap_or(String::from("us-east-1"))
|
.unwrap_or(String::from("us-east-1"))
|
||||||
}) else {
|
}) else {
|
||||||
return async move {
|
return async move { Err(anyhow::anyhow!("App State Dropped").into()) }.boxed();
|
||||||
anyhow::bail!("App State Dropped");
|
|
||||||
}
|
|
||||||
.boxed();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let model_id = match self.model.cross_region_inference_id(®ion) {
|
let model_id = match self.model.cross_region_inference_id(®ion) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
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(),
|
self.model.mode(),
|
||||||
) {
|
) {
|
||||||
Ok(request) => request,
|
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();
|
let owned_handle = self.handler.clone();
|
||||||
|
|||||||
@@ -807,6 +807,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||||||
'static,
|
'static,
|
||||||
Result<
|
Result<
|
||||||
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
||||||
|
LanguageModelCompletionError,
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
let thread_id = request.thread_id.clone();
|
let thread_id = request.thread_id.clone();
|
||||||
@@ -848,7 +849,8 @@ impl LanguageModel for CloudLanguageModel {
|
|||||||
mode,
|
mode,
|
||||||
provider: zed_llm_client::LanguageModelProvider::Anthropic,
|
provider: zed_llm_client::LanguageModelProvider::Anthropic,
|
||||||
model: request.model.clone(),
|
model: request.model.clone(),
|
||||||
provider_request: serde_json::to_value(&request)?,
|
provider_request: serde_json::to_value(&request)
|
||||||
|
.map_err(|e| anyhow!(e))?,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -884,7 +886,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
let model = match open_ai::Model::from_id(&self.model.id.0) {
|
let model = match open_ai::Model::from_id(&self.model.id.0) {
|
||||||
Ok(model) => model,
|
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 request = into_open_ai(request, &model, None);
|
||||||
let llm_api_token = self.llm_api_token.clone();
|
let llm_api_token = self.llm_api_token.clone();
|
||||||
@@ -905,7 +907,8 @@ impl LanguageModel for CloudLanguageModel {
|
|||||||
mode,
|
mode,
|
||||||
provider: zed_llm_client::LanguageModelProvider::OpenAi,
|
provider: zed_llm_client::LanguageModelProvider::OpenAi,
|
||||||
model: request.model.clone(),
|
model: request.model.clone(),
|
||||||
provider_request: serde_json::to_value(&request)?,
|
provider_request: serde_json::to_value(&request)
|
||||||
|
.map_err(|e| anyhow!(e))?,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -944,7 +947,8 @@ impl LanguageModel for CloudLanguageModel {
|
|||||||
mode,
|
mode,
|
||||||
provider: zed_llm_client::LanguageModelProvider::Google,
|
provider: zed_llm_client::LanguageModelProvider::Google,
|
||||||
model: request.model.model_id.clone(),
|
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?;
|
.await?;
|
||||||
|
|||||||
@@ -265,13 +265,15 @@ impl LanguageModel for CopilotChatLanguageModel {
|
|||||||
'static,
|
'static,
|
||||||
Result<
|
Result<
|
||||||
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
||||||
|
LanguageModelCompletionError,
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
if let Some(message) = request.messages.last() {
|
if let Some(message) = request.messages.last() {
|
||||||
if message.contents_empty() {
|
if message.contents_empty() {
|
||||||
const EMPTY_PROMPT_MSG: &str =
|
const EMPTY_PROMPT_MSG: &str =
|
||||||
"Empty prompts aren't allowed. Please provide a non-empty prompt.";
|
"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.
|
// 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.
|
// and provide a more helpful error message.
|
||||||
if !matches!(message.role, Role::User) {
|
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.";
|
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) {
|
let copilot_request = match into_copilot_chat(&self.model, request) {
|
||||||
Ok(request) => 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;
|
let is_streaming = copilot_request.stream;
|
||||||
|
|
||||||
|
|||||||
@@ -348,6 +348,7 @@ impl LanguageModel for DeepSeekLanguageModel {
|
|||||||
'static,
|
'static,
|
||||||
Result<
|
Result<
|
||||||
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
||||||
|
LanguageModelCompletionError,
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
let request = into_deepseek(request, &self.model, self.max_output_tokens());
|
let request = into_deepseek(request, &self.model, self.max_output_tokens());
|
||||||
|
|||||||
@@ -409,6 +409,7 @@ impl LanguageModel for GoogleLanguageModel {
|
|||||||
'static,
|
'static,
|
||||||
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
||||||
>,
|
>,
|
||||||
|
LanguageModelCompletionError,
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
let request = into_google(
|
let request = into_google(
|
||||||
|
|||||||
@@ -420,6 +420,7 @@ impl LanguageModel for LmStudioLanguageModel {
|
|||||||
'static,
|
'static,
|
||||||
Result<
|
Result<
|
||||||
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
||||||
|
LanguageModelCompletionError,
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
let request = self.to_lmstudio_request(request);
|
let request = self.to_lmstudio_request(request);
|
||||||
|
|||||||
@@ -364,6 +364,7 @@ impl LanguageModel for MistralLanguageModel {
|
|||||||
'static,
|
'static,
|
||||||
Result<
|
Result<
|
||||||
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
||||||
|
LanguageModelCompletionError,
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
let request = into_mistral(
|
let request = into_mistral(
|
||||||
|
|||||||
@@ -406,6 +406,7 @@ impl LanguageModel for OllamaLanguageModel {
|
|||||||
'static,
|
'static,
|
||||||
Result<
|
Result<
|
||||||
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
||||||
|
LanguageModelCompletionError,
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
let request = self.to_ollama_request(request);
|
let request = self.to_ollama_request(request);
|
||||||
@@ -415,7 +416,7 @@ impl LanguageModel for OllamaLanguageModel {
|
|||||||
let settings = &AllLanguageModelSettings::get_global(cx).ollama;
|
let settings = &AllLanguageModelSettings::get_global(cx).ollama;
|
||||||
settings.api_url.clone()
|
settings.api_url.clone()
|
||||||
}) else {
|
}) 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 {
|
let future = self.request_limiter.stream(async move {
|
||||||
|
|||||||
@@ -339,6 +339,7 @@ impl LanguageModel for OpenAiLanguageModel {
|
|||||||
'static,
|
'static,
|
||||||
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
||||||
>,
|
>,
|
||||||
|
LanguageModelCompletionError,
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
let request = into_open_ai(request, &self.model, self.max_output_tokens());
|
let request = into_open_ai(request, &self.model, self.max_output_tokens());
|
||||||
|
|||||||
@@ -367,6 +367,7 @@ impl LanguageModel for OpenRouterLanguageModel {
|
|||||||
'static,
|
'static,
|
||||||
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
||||||
>,
|
>,
|
||||||
|
LanguageModelCompletionError,
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
let request = into_open_router(request, &self.model, self.max_output_tokens());
|
let request = into_open_router(request, &self.model, self.max_output_tokens());
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ text.workspace = true
|
|||||||
toml.workspace = true
|
toml.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
uuid.workspace = true
|
|
||||||
which.workspace = true
|
which.workspace = true
|
||||||
worktree.workspace = true
|
worktree.workspace = true
|
||||||
zlog.workspace = true
|
zlog.workspace = true
|
||||||
|
|||||||
@@ -1,17 +1,88 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use collections::FxHashMap;
|
use collections::HashMap;
|
||||||
use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
|
use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
|
||||||
use gpui::SharedString;
|
use gpui::SharedString;
|
||||||
use std::path::PathBuf;
|
use serde::{Deserialize, Serialize};
|
||||||
use task::{
|
use task::{DebugScenario, SpawnInTerminal, TaskTemplate};
|
||||||
BuildTaskDefinition, DebugScenario, RevealStrategy, RevealTarget, Shell, SpawnInTerminal,
|
|
||||||
TaskTemplate,
|
|
||||||
};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
pub(crate) struct GoLocator;
|
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]
|
#[async_trait]
|
||||||
impl DapLocator for GoLocator {
|
impl DapLocator for GoLocator {
|
||||||
fn name(&self) -> SharedString {
|
fn name(&self) -> SharedString {
|
||||||
@@ -32,78 +103,121 @@ impl DapLocator for GoLocator {
|
|||||||
|
|
||||||
match go_action.as_str() {
|
match go_action.as_str() {
|
||||||
"test" => {
|
"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 {
|
let mut all_args_are_test = false;
|
||||||
label: "go test debug".into(),
|
let mut next_arg_is_test = false;
|
||||||
command: "go".into(),
|
let mut next_arg_is_build = false;
|
||||||
args: vec![
|
let mut seen_pkg = false;
|
||||||
"test".into(),
|
let mut seen_v = false;
|
||||||
"-c".into(),
|
|
||||||
"-gcflags \"all=-N -l\"".into(),
|
for arg in build_config.args.iter().skip(1) {
|
||||||
"-o".into(),
|
if all_args_are_test || next_arg_is_test {
|
||||||
binary_path,
|
// HACK: tasks assume that they are run in a shell context,
|
||||||
],
|
// so the -run regex has escaped specials. Delve correctly
|
||||||
env: build_config.env.clone(),
|
// 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(),
|
cwd: build_config.cwd.clone(),
|
||||||
use_new_terminal: false,
|
env: build_config.env.clone(),
|
||||||
allow_concurrent_runs: false,
|
})
|
||||||
reveal: RevealStrategy::Always,
|
.unwrap();
|
||||||
reveal_target: RevealTarget::Dock,
|
|
||||||
hide: task::HideStrategy::Never,
|
|
||||||
shell: Shell::System,
|
|
||||||
tags: vec![],
|
|
||||||
show_summary: true,
|
|
||||||
show_command: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(DebugScenario {
|
Some(DebugScenario {
|
||||||
label: resolved_label.to_string().into(),
|
label: resolved_label.to_string().into(),
|
||||||
adapter: adapter.0,
|
adapter: adapter.0,
|
||||||
build: Some(BuildTaskDefinition::Template {
|
build: None,
|
||||||
task_template: build_task,
|
config: config,
|
||||||
locator_name: Some(self.name()),
|
|
||||||
}),
|
|
||||||
config: serde_json::Value::Null,
|
|
||||||
tcp_connection: None,
|
tcp_connection: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
"run" => {
|
"run" => {
|
||||||
let program = build_config
|
let mut next_arg_is_build = false;
|
||||||
.args
|
let mut seen_pkg = false;
|
||||||
.get(1)
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| ".".to_string());
|
|
||||||
|
|
||||||
let build_task = TaskTemplate {
|
let mut program = ".".to_string();
|
||||||
label: "go build debug".into(),
|
let mut args = Vec::default();
|
||||||
command: "go".into(),
|
let mut build_flags = Vec::default();
|
||||||
args: vec![
|
|
||||||
"build".into(),
|
for arg in build_config.args.iter().skip(1) {
|
||||||
"-gcflags \"all=-N -l\"".into(),
|
if seen_pkg {
|
||||||
program.clone(),
|
args.push(arg.clone())
|
||||||
],
|
} else if next_arg_is_build {
|
||||||
env: build_config.env.clone(),
|
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(),
|
cwd: build_config.cwd.clone(),
|
||||||
use_new_terminal: false,
|
env: build_config.env.clone(),
|
||||||
allow_concurrent_runs: false,
|
request: "launch".to_string(),
|
||||||
reveal: RevealStrategy::Always,
|
mode: "debug".to_string(),
|
||||||
reveal_target: RevealTarget::Dock,
|
program,
|
||||||
hide: task::HideStrategy::Never,
|
args: args,
|
||||||
shell: Shell::System,
|
build_flags,
|
||||||
tags: vec![],
|
})
|
||||||
show_summary: true,
|
.unwrap();
|
||||||
show_command: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(DebugScenario {
|
Some(DebugScenario {
|
||||||
label: resolved_label.to_string().into(),
|
label: resolved_label.to_string().into(),
|
||||||
adapter: adapter.0,
|
adapter: adapter.0,
|
||||||
build: Some(BuildTaskDefinition::Template {
|
build: None,
|
||||||
task_template: build_task,
|
config,
|
||||||
locator_name: Some(self.name()),
|
|
||||||
}),
|
|
||||||
config: serde_json::Value::Null,
|
|
||||||
tcp_connection: None,
|
tcp_connection: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -111,113 +225,15 @@ impl DapLocator for GoLocator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest> {
|
async fn run(&self, _build_config: SpawnInTerminal) -> Result<DebugRequest> {
|
||||||
if build_config.args.is_empty() {
|
unreachable!()
|
||||||
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)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskId, TaskTemplate};
|
use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, 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"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_scenario_for_go_build() {
|
fn test_create_scenario_for_go_build() {
|
||||||
@@ -276,99 +292,106 @@ mod tests {
|
|||||||
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
|
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
|
||||||
assert!(scenario.is_none());
|
assert!(scenario.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_scenario_for_go_test() {
|
fn test_go_locator_run() {
|
||||||
let locator = GoLocator;
|
let locator = GoLocator;
|
||||||
|
let delve = DebugAdapterName("Delve".into());
|
||||||
|
|
||||||
let task = TaskTemplate {
|
let task = TaskTemplate {
|
||||||
label: "go test".into(),
|
label: "go run with flags".into(),
|
||||||
command: "go".into(),
|
command: "go".into(),
|
||||||
args: vec!["test".into(), ".".into()],
|
args: vec![
|
||||||
env: Default::default(),
|
"run".to_string(),
|
||||||
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
|
"-race".to_string(),
|
||||||
use_new_terminal: false,
|
"-ldflags".to_string(),
|
||||||
allow_concurrent_runs: false,
|
"-X main.version=1.0".to_string(),
|
||||||
reveal: RevealStrategy::Always,
|
"./cmd/myapp".to_string(),
|
||||||
reveal_target: RevealTarget::Dock,
|
"--config".to_string(),
|
||||||
hide: HideStrategy::Never,
|
"production.yaml".to_string(),
|
||||||
shell: Shell::System,
|
"--verbose".to_string(),
|
||||||
tags: vec![],
|
],
|
||||||
show_summary: true,
|
env: {
|
||||||
show_command: true,
|
let mut env = HashMap::default();
|
||||||
|
env.insert("GO_ENV".to_string(), "production".to_string());
|
||||||
|
env
|
||||||
|
},
|
||||||
|
cwd: Some("/project/root".into()),
|
||||||
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let scenario =
|
let scenario = locator
|
||||||
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
|
.create_scenario(&task, "test run label", delve)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(scenario.is_some());
|
let config: DelveLaunchRequest = serde_json::from_value(scenario.config).unwrap();
|
||||||
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!(
|
||||||
assert_eq!(task_template.command, "go");
|
config,
|
||||||
assert!(task_template.args.contains(&"test".into()));
|
DelveLaunchRequest {
|
||||||
assert!(task_template.args.contains(&"-c".into()));
|
request: "launch".to_string(),
|
||||||
assert!(
|
mode: "debug".to_string(),
|
||||||
task_template
|
program: "./cmd/myapp".to_string(),
|
||||||
.args
|
build_flags: vec![
|
||||||
.contains(&"-gcflags \"all=-N -l\"".into())
|
"-race".to_string(),
|
||||||
);
|
"-ldflags".to_string(),
|
||||||
assert!(task_template.args.contains(&"-o".into()));
|
"-X main.version=1.0".to_string()
|
||||||
assert!(
|
],
|
||||||
task_template
|
args: vec![
|
||||||
.args
|
"--config".to_string(),
|
||||||
.iter()
|
"production.yaml".to_string(),
|
||||||
.any(|arg| arg.starts_with("__debug_"))
|
"--verbose".to_string(),
|
||||||
);
|
],
|
||||||
} else {
|
env: {
|
||||||
panic!("Expected BuildTaskDefinition::Template");
|
let mut env = HashMap::default();
|
||||||
}
|
env.insert("GO_ENV".to_string(), "production".to_string());
|
||||||
|
env
|
||||||
assert!(
|
},
|
||||||
scenario.config.is_null(),
|
cwd: Some("/project/root".to_string()),
|
||||||
"Initial config should be null to ensure it's invalid"
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_scenario_for_go_test_with_cwd_binary() {
|
fn test_go_locator_test() {
|
||||||
let locator = GoLocator;
|
let locator = GoLocator;
|
||||||
|
let delve = DebugAdapterName("Delve".into());
|
||||||
|
|
||||||
let task = TaskTemplate {
|
// Test with tags and run flag
|
||||||
label: "go test".into(),
|
let task_with_tags = TaskTemplate {
|
||||||
|
label: "test".into(),
|
||||||
command: "go".into(),
|
command: "go".into(),
|
||||||
args: vec!["test".into(), ".".into()],
|
args: vec![
|
||||||
env: Default::default(),
|
"test".to_string(),
|
||||||
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
|
"-tags".to_string(),
|
||||||
use_new_terminal: false,
|
"integration,unit".to_string(),
|
||||||
allow_concurrent_runs: false,
|
"-run".to_string(),
|
||||||
reveal: RevealStrategy::Always,
|
"Foo".to_string(),
|
||||||
reveal_target: RevealTarget::Dock,
|
".".to_string(),
|
||||||
hide: HideStrategy::Never,
|
],
|
||||||
shell: Shell::System,
|
..Default::default()
|
||||||
tags: vec![],
|
|
||||||
show_summary: true,
|
|
||||||
show_command: true,
|
|
||||||
};
|
};
|
||||||
|
let result = locator
|
||||||
|
.create_scenario(&task_with_tags, "", delve.clone())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let scenario =
|
let config: DelveLaunchRequest = serde_json::from_value(result.config).unwrap();
|
||||||
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
|
|
||||||
|
|
||||||
assert!(scenario.is_some());
|
assert_eq!(
|
||||||
let scenario = scenario.unwrap();
|
config,
|
||||||
|
DelveLaunchRequest {
|
||||||
if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
|
request: "launch".to_string(),
|
||||||
assert!(
|
mode: "test".to_string(),
|
||||||
task_template
|
program: ".".to_string(),
|
||||||
.args
|
build_flags: vec!["-tags".to_string(), "integration,unit".to_string(),],
|
||||||
.iter()
|
args: vec![
|
||||||
.any(|arg| arg.starts_with("__debug_"))
|
"-test.run".to_string(),
|
||||||
);
|
"Foo".to_string(),
|
||||||
} else {
|
"-test.v".to_string()
|
||||||
panic!("Expected BuildTaskDefinition::Template");
|
],
|
||||||
}
|
env: HashMap::default(),
|
||||||
|
cwd: None,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -395,42 +418,4 @@ mod tests {
|
|||||||
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
|
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
|
||||||
assert!(scenario.is_none());
|
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")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ async fn load_shell_environment(
|
|||||||
use util::shell_env;
|
use util::shell_env;
|
||||||
|
|
||||||
let dir_ = dir.to_owned();
|
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,
|
Ok(envs) => envs,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
util::log_err(&err);
|
util::log_err(&err);
|
||||||
|
|||||||
@@ -4050,7 +4050,7 @@ impl LspCommand for GetDocumentDiagnostics {
|
|||||||
let buffer_id = buffer.update(&mut cx, |buffer, _| buffer.remote_id())?;
|
let buffer_id = buffer.update(&mut cx, |buffer, _| buffer.remote_id())?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
previous_result_id: lsp_store
|
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))?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ pub struct LocalLspStore {
|
|||||||
_subscription: gpui::Subscription,
|
_subscription: gpui::Subscription,
|
||||||
lsp_tree: Entity<LanguageServerTree>,
|
lsp_tree: Entity<LanguageServerTree>,
|
||||||
registered_buffers: HashMap<BufferId, usize>,
|
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 {
|
impl LocalLspStore {
|
||||||
@@ -2295,8 +2295,11 @@ impl LocalLspStore {
|
|||||||
|
|
||||||
let set = DiagnosticSet::new(sanitized_diagnostics, &snapshot);
|
let set = DiagnosticSet::new(sanitized_diagnostics, &snapshot);
|
||||||
buffer.update(cx, |buffer, cx| {
|
buffer.update(cx, |buffer, cx| {
|
||||||
self.buffer_pull_diagnostics_result_ids
|
if let Some(abs_path) = File::from_dyn(buffer.file()).map(|f| f.abs_path(cx)) {
|
||||||
.insert(buffer.remote_id(), result_id);
|
self.buffer_pull_diagnostics_result_ids
|
||||||
|
.insert(abs_path, result_id);
|
||||||
|
}
|
||||||
|
|
||||||
buffer.update_diagnostics(server_id, set, cx)
|
buffer.update_diagnostics(server_id, set, cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3792,8 +3795,16 @@ impl LspStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
BufferStoreEvent::BufferDropped(buffer_id) => {
|
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() {
|
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>>> {
|
) -> Task<Result<Vec<LspPullDiagnostics>>> {
|
||||||
let buffer = buffer_handle.read(cx);
|
let buffer = buffer_handle.read(cx);
|
||||||
let buffer_id = buffer.remote_id();
|
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() {
|
if let Some((client, upstream_project_id)) = self.upstream_client() {
|
||||||
let request_task = client.request(proto::MultiLspQuery {
|
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()?
|
self.as_local()?
|
||||||
.buffer_pull_diagnostics_result_ids
|
.buffer_pull_diagnostics_result_ids
|
||||||
.get(&buffer_id)
|
.get(&abs_path)
|
||||||
.cloned()
|
.cloned()
|
||||||
.flatten()
|
.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 {
|
let Some(local) = self.as_local() else {
|
||||||
return HashMap::default();
|
return HashMap::default();
|
||||||
};
|
};
|
||||||
local
|
local
|
||||||
.buffer_pull_diagnostics_result_ids
|
.buffer_pull_diagnostics_result_ids
|
||||||
.iter()
|
.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()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9802,17 +9819,11 @@ fn lsp_workspace_diagnostics_refresh(
|
|||||||
.await;
|
.await;
|
||||||
attempts += 1;
|
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
|
lsp_store
|
||||||
.all_result_ids()
|
.all_result_ids()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|(buffer_id, result_id)| {
|
.filter_map(|(abs_path, 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);
|
|
||||||
let uri = file_path_to_lsp_url(&abs_path).ok()?;
|
let uri = file_path_to_lsp_url(&abs_path).ok()?;
|
||||||
Some(lsp::PreviousResultId {
|
Some(lsp::PreviousResultId {
|
||||||
uri,
|
uri,
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ impl Render for TerminalOutput {
|
|||||||
cell: ic.cell.clone(),
|
cell: ic.cell.clone(),
|
||||||
});
|
});
|
||||||
let (cells, rects) =
|
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
|
// 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());
|
let text_line_height = text_style.line_height_in_pixels(window.rem_size());
|
||||||
|
|||||||
@@ -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,
|
Ok(key_binding) => key_binding,
|
||||||
Err(InvalidKeystrokeError { keystroke }) => {
|
Err(InvalidKeystrokeError { keystroke }) => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
|
|||||||
@@ -530,6 +530,21 @@ impl EnvVariableReplacer {
|
|||||||
fn new(variables: HashMap<VsCodeEnvVariable, ZedEnvVariable>) -> Self {
|
fn new(variables: HashMap<VsCodeEnvVariable, ZedEnvVariable>) -> Self {
|
||||||
Self { variables }
|
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.
|
// Replaces occurrences of VsCode-specific environment variables with Zed equivalents.
|
||||||
fn replace(&self, input: &str) -> String {
|
fn replace(&self, input: &str) -> String {
|
||||||
shellexpand::env_with_context_no_errors(&input, |var: &str| {
|
shellexpand::env_with_context_no_errors(&input, |var: &str| {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ impl VsCodeDebugTaskDefinition {
|
|||||||
host: None,
|
host: None,
|
||||||
timeout: None,
|
timeout: None,
|
||||||
}),
|
}),
|
||||||
config: self.other_attributes,
|
config: replacer.replace_value(self.other_attributes),
|
||||||
};
|
};
|
||||||
Ok(definition)
|
Ok(definition)
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ impl TryFrom<VsCodeDebugTaskFile> for DebugTaskFile {
|
|||||||
"workspaceFolder".to_owned(),
|
"workspaceFolder".to_owned(),
|
||||||
VariableName::WorktreeRoot.to_string(),
|
VariableName::WorktreeRoot.to_string(),
|
||||||
),
|
),
|
||||||
// TODO other interesting variables?
|
("file".to_owned(), VariableName::Filename.to_string()), // TODO other interesting variables?
|
||||||
]));
|
]));
|
||||||
let templates = file
|
let templates = file
|
||||||
.configurations
|
.configurations
|
||||||
@@ -94,6 +94,7 @@ fn task_type_to_adapter_name(task_type: &str) -> SharedString {
|
|||||||
"php" => "PHP",
|
"php" => "PHP",
|
||||||
"cppdbg" | "lldb" => "CodeLLDB",
|
"cppdbg" | "lldb" => "CodeLLDB",
|
||||||
"debugpy" => "Debugpy",
|
"debugpy" => "Debugpy",
|
||||||
|
"rdbg" => "Ruby",
|
||||||
_ => task_type,
|
_ => task_type,
|
||||||
}
|
}
|
||||||
.to_owned()
|
.to_owned()
|
||||||
|
|||||||
@@ -270,10 +270,15 @@ mod test {
|
|||||||
fn test_scroll_keys() {
|
fn test_scroll_keys() {
|
||||||
//These keys should be handled by the scrolling element directly
|
//These keys should be handled by the scrolling element directly
|
||||||
//Need to signify this by returning 'None'
|
//Need to signify this by returning 'None'
|
||||||
let shift_pageup = Keystroke::parse("shift-pageup").unwrap();
|
let shift_key = |key: &str| Keystroke {
|
||||||
let shift_pagedown = Keystroke::parse("shift-pagedown").unwrap();
|
modifiers: Modifiers::shift(),
|
||||||
let shift_home = Keystroke::parse("shift-home").unwrap();
|
key: key.to_owned(),
|
||||||
let shift_end = Keystroke::parse("shift-end").unwrap();
|
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;
|
let none = TermMode::NONE;
|
||||||
assert_eq!(to_esc_str(&shift_pageup, &none, false), None);
|
assert_eq!(to_esc_str(&shift_pageup, &none, false), None);
|
||||||
@@ -299,8 +304,13 @@ mod test {
|
|||||||
Some("\x1b[1;2F".into())
|
Some("\x1b[1;2F".into())
|
||||||
);
|
);
|
||||||
|
|
||||||
let pageup = Keystroke::parse("pageup").unwrap();
|
let normal_key = |key: &str| Keystroke {
|
||||||
let pagedown = Keystroke::parse("pagedown").unwrap();
|
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;
|
let any = TermMode::ANY;
|
||||||
|
|
||||||
assert_eq!(to_esc_str(&pageup, &any, false), Some("\x1b[5~".into()));
|
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 app_cursor = TermMode::APP_CURSOR;
|
||||||
let none = TermMode::NONE;
|
let none = TermMode::NONE;
|
||||||
|
|
||||||
let up = Keystroke::parse("up").unwrap();
|
let generate_keystroke = |key: &str| Keystroke {
|
||||||
let down = Keystroke::parse("down").unwrap();
|
modifiers: Modifiers::none(),
|
||||||
let left = Keystroke::parse("left").unwrap();
|
key: key.to_owned(),
|
||||||
let right = Keystroke::parse("right").unwrap();
|
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(&up, &none, false), Some("\x1b[A".into()));
|
||||||
assert_eq!(to_esc_str(&down, &none, false), Some("\x1b[B".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) {
|
for (lower, upper) in letters_lower.zip(letters_upper) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
to_esc_str(
|
to_esc_str(
|
||||||
&Keystroke::parse(&format!("ctrl-shift-{}", lower)).unwrap(),
|
&Keystroke {
|
||||||
|
modifiers: Modifiers::control_shift(),
|
||||||
|
key: lower.to_string(),
|
||||||
|
key_char: None,
|
||||||
|
},
|
||||||
&mode,
|
&mode,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
to_esc_str(
|
to_esc_str(
|
||||||
&Keystroke::parse(&format!("ctrl-{}", upper)).unwrap(),
|
&Keystroke {
|
||||||
|
modifiers: Modifiers::control(),
|
||||||
|
key: upper.to_string(),
|
||||||
|
key_char: None,
|
||||||
|
},
|
||||||
&mode,
|
&mode,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
@@ -378,7 +401,11 @@ mod test {
|
|||||||
for character in ascii_printable {
|
for character in ascii_printable {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
to_esc_str(
|
to_esc_str(
|
||||||
&Keystroke::parse(&format!("alt-{}", character)).unwrap(),
|
&Keystroke {
|
||||||
|
modifiers: Modifiers::alt(),
|
||||||
|
key: character.to_string(),
|
||||||
|
key_char: None,
|
||||||
|
},
|
||||||
&TermMode::NONE,
|
&TermMode::NONE,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
@@ -396,7 +423,11 @@ mod test {
|
|||||||
for key in gpui_keys {
|
for key in gpui_keys {
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
to_esc_str(
|
to_esc_str(
|
||||||
&Keystroke::parse(&format!("alt-{}", key)).unwrap(),
|
&Keystroke {
|
||||||
|
modifiers: Modifiers::alt(),
|
||||||
|
key: key.to_string(),
|
||||||
|
key_char: None,
|
||||||
|
},
|
||||||
&TermMode::NONE,
|
&TermMode::NONE,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
@@ -419,15 +450,78 @@ mod test {
|
|||||||
// 8 | Shift + Alt + Control
|
// 8 | Shift + Alt + Control
|
||||||
// ---------+---------------------------
|
// ---------+---------------------------
|
||||||
// from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys
|
// 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!(
|
||||||
assert_eq!(3, modifier_code(&Keystroke::parse("alt-a").unwrap()));
|
2,
|
||||||
assert_eq!(4, modifier_code(&Keystroke::parse("shift-alt-a").unwrap()));
|
modifier_code(&Keystroke {
|
||||||
assert_eq!(5, modifier_code(&Keystroke::parse("ctrl-a").unwrap()));
|
modifiers: Modifiers::shift(),
|
||||||
assert_eq!(6, modifier_code(&Keystroke::parse("shift-ctrl-a").unwrap()));
|
key: "a".into(),
|
||||||
assert_eq!(7, modifier_code(&Keystroke::parse("alt-ctrl-a").unwrap()));
|
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!(
|
assert_eq!(
|
||||||
8,
|
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
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use editor::{CursorLayout, HighlightedRange, HighlightedRangeLine};
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, DispatchPhase, Element,
|
AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, DispatchPhase, Element,
|
||||||
ElementId, Entity, FocusHandle, Font, FontStyle, FontWeight, GlobalElementId, HighlightStyle,
|
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,
|
ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, Point, ShapedLine,
|
||||||
StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, TextStyle, UTF16Selection,
|
StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, TextStyle, UTF16Selection,
|
||||||
UnderlineStyle, WeakEntity, WhiteSpace, Window, WindowTextSystem, div, fill, point, px,
|
UnderlineStyle, WeakEntity, WhiteSpace, Window, WindowTextSystem, div, fill, point, px,
|
||||||
@@ -32,7 +32,7 @@ use workspace::Workspace;
|
|||||||
use std::mem;
|
use std::mem;
|
||||||
use std::{fmt::Debug, ops::RangeInclusive, rc::Rc};
|
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.
|
/// The information generated during layout that is necessary for painting.
|
||||||
pub struct LayoutState {
|
pub struct LayoutState {
|
||||||
@@ -49,6 +49,7 @@ pub struct LayoutState {
|
|||||||
gutter: Pixels,
|
gutter: Pixels,
|
||||||
block_below_cursor_element: Option<AnyElement>,
|
block_below_cursor_element: Option<AnyElement>,
|
||||||
base_text_style: TextStyle,
|
base_text_style: TextStyle,
|
||||||
|
content_mode: ContentMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper struct for converting data between Alacritty's cursor points, and displayed cursor points.
|
/// Helper struct for converting data between Alacritty's cursor points, and displayed cursor points.
|
||||||
@@ -202,6 +203,7 @@ impl TerminalElement {
|
|||||||
|
|
||||||
pub fn layout_grid(
|
pub fn layout_grid(
|
||||||
grid: impl Iterator<Item = IndexedCell>,
|
grid: impl Iterator<Item = IndexedCell>,
|
||||||
|
start_line_offset: i32,
|
||||||
text_style: &TextStyle,
|
text_style: &TextStyle,
|
||||||
// terminal_theme: &TerminalStyle,
|
// terminal_theme: &TerminalStyle,
|
||||||
text_system: &WindowTextSystem,
|
text_system: &WindowTextSystem,
|
||||||
@@ -218,6 +220,8 @@ impl TerminalElement {
|
|||||||
|
|
||||||
let linegroups = grid.into_iter().chunk_by(|i| i.point.line);
|
let linegroups = grid.into_iter().chunk_by(|i| i.point.line);
|
||||||
for (line_index, (_, line)) in linegroups.into_iter().enumerate() {
|
for (line_index, (_, line)) in linegroups.into_iter().enumerate() {
|
||||||
|
let alac_line = start_line_offset + line_index as i32;
|
||||||
|
|
||||||
for cell in line {
|
for cell in line {
|
||||||
let mut fg = cell.fg;
|
let mut fg = cell.fg;
|
||||||
let mut bg = cell.bg;
|
let mut bg = cell.bg;
|
||||||
@@ -245,7 +249,7 @@ impl TerminalElement {
|
|||||||
|| {
|
|| {
|
||||||
Some(LayoutRect::new(
|
Some(LayoutRect::new(
|
||||||
AlacPoint::new(
|
AlacPoint::new(
|
||||||
line_index as i32,
|
alac_line,
|
||||||
cell.point.column.0 as i32,
|
cell.point.column.0 as i32,
|
||||||
),
|
),
|
||||||
1,
|
1,
|
||||||
@@ -260,10 +264,7 @@ impl TerminalElement {
|
|||||||
rects.push(cur_rect.take().unwrap());
|
rects.push(cur_rect.take().unwrap());
|
||||||
}
|
}
|
||||||
cur_rect = Some(LayoutRect::new(
|
cur_rect = Some(LayoutRect::new(
|
||||||
AlacPoint::new(
|
AlacPoint::new(alac_line, cell.point.column.0 as i32),
|
||||||
line_index as i32,
|
|
||||||
cell.point.column.0 as i32,
|
|
||||||
),
|
|
||||||
1,
|
1,
|
||||||
convert_color(&bg, theme),
|
convert_color(&bg, theme),
|
||||||
));
|
));
|
||||||
@@ -272,7 +273,7 @@ impl TerminalElement {
|
|||||||
None => {
|
None => {
|
||||||
cur_alac_color = Some(bg);
|
cur_alac_color = Some(bg);
|
||||||
cur_rect = Some(LayoutRect::new(
|
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,
|
1,
|
||||||
convert_color(&bg, theme),
|
convert_color(&bg, theme),
|
||||||
));
|
));
|
||||||
@@ -295,7 +296,7 @@ impl TerminalElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
cells.push(LayoutCell::new(
|
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,
|
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 focus = self.focus.clone();
|
||||||
let terminal = self.terminal.clone();
|
let terminal = self.terminal.clone();
|
||||||
let terminal_view = self.terminal_view.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({
|
self.interactivity.on_scroll_wheel({
|
||||||
let terminal_view = self.terminal_view.downgrade();
|
let terminal_view = self.terminal_view.downgrade();
|
||||||
move |e, _window, cx| {
|
move |e, window, cx| {
|
||||||
terminal_view
|
terminal_view
|
||||||
.update(cx, |terminal_view, cx| {
|
.update(cx, |terminal_view, cx| {
|
||||||
terminal_view.scroll_wheel(e, cx);
|
if matches!(terminal_view.mode, TerminalMode::Standalone)
|
||||||
cx.notify();
|
|| terminal_view.focus_handle.is_focused(window)
|
||||||
|
{
|
||||||
|
terminal_view.scroll_wheel(e, cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@@ -605,6 +616,32 @@ impl Element for TerminalElement {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> (LayoutId, Self::RequestLayoutState) {
|
) -> (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(
|
let layout_id = self.interactivity.request_layout(
|
||||||
global_id,
|
global_id,
|
||||||
inspector_id,
|
inspector_id,
|
||||||
@@ -612,29 +649,7 @@ impl Element for TerminalElement {
|
|||||||
cx,
|
cx,
|
||||||
|mut style, window, cx| {
|
|mut style, window, cx| {
|
||||||
style.size.width = relative(1.).into();
|
style.size.width = relative(1.).into();
|
||||||
|
style.size.height = height;
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.request_layout(style, None, cx)
|
window.request_layout(style, None, cx)
|
||||||
},
|
},
|
||||||
@@ -693,7 +708,7 @@ impl Element for TerminalElement {
|
|||||||
TerminalMode::Embedded { .. } => {
|
TerminalMode::Embedded { .. } => {
|
||||||
window.text_style().font_size.to_pixels(window.rem_size())
|
window.text_style().font_size.to_pixels(window.rem_size())
|
||||||
}
|
}
|
||||||
TerminalMode::Scrollable => terminal_settings
|
TerminalMode::Standalone => terminal_settings
|
||||||
.font_size
|
.font_size
|
||||||
.map_or(buffer_font_size, |size| theme::adjusted_font_size(size, cx)),
|
.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 player_color = theme.players().local();
|
||||||
let match_color = theme.colors().search_match_background;
|
let match_color = theme.colors().search_match_background;
|
||||||
let gutter;
|
let gutter;
|
||||||
let dimensions = {
|
let (dimensions, line_height_px) = {
|
||||||
let rem_size = window.rem_size();
|
let rem_size = window.rem_size();
|
||||||
let font_pixels = text_style.font_size.to_pixels(rem_size);
|
let font_pixels = text_style.font_size.to_pixels(rem_size);
|
||||||
// TODO: line_height should be an f32 not an AbsoluteLength.
|
// TODO: line_height should be an f32 not an AbsoluteLength.
|
||||||
@@ -759,7 +774,10 @@ impl Element for TerminalElement {
|
|||||||
let mut origin = bounds.origin;
|
let mut origin = bounds.origin;
|
||||||
origin.x += gutter;
|
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();
|
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
|
// then have that representation be converted to the appropriate highlight data structure
|
||||||
|
|
||||||
let (cells, rects) = TerminalElement::layout_grid(
|
let content_mode = self.terminal_view.read(cx).content_mode(window, cx);
|
||||||
cells.iter().cloned(),
|
let (cells, rects) = match content_mode {
|
||||||
&text_style,
|
ContentMode::Scrollable => TerminalElement::layout_grid(
|
||||||
window.text_system(),
|
cells.iter().cloned(),
|
||||||
last_hovered_word
|
0,
|
||||||
.as_ref()
|
&text_style,
|
||||||
.map(|last_hovered_word| (link_style, &last_hovered_word.word_match)),
|
window.text_system(),
|
||||||
window,
|
last_hovered_word
|
||||||
cx,
|
.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
|
// Layout cursor. Rectangle is used for IME, so we should lay it out even
|
||||||
// if we don't end up showing it.
|
// if we don't end up showing it.
|
||||||
@@ -932,6 +976,7 @@ impl Element for TerminalElement {
|
|||||||
gutter,
|
gutter,
|
||||||
block_below_cursor_element,
|
block_below_cursor_element,
|
||||||
base_text_style: text_style,
|
base_text_style: text_style,
|
||||||
|
content_mode,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -969,7 +1014,12 @@ impl Element for TerminalElement {
|
|||||||
workspace: self.workspace.clone(),
|
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()
|
if window.modifiers().secondary()
|
||||||
&& bounds.contains(&window.mouse_position())
|
&& bounds.contains(&window.mouse_position())
|
||||||
&& self.terminal_view.read(cx).hover.is_some()
|
&& self.terminal_view.read(cx).hover.is_some()
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ use assistant_slash_command::SlashCommandRegistry;
|
|||||||
use editor::{Editor, EditorSettings, actions::SelectAll, scroll::ScrollbarAutoHide};
|
use editor::{Editor, EditorSettings, actions::SelectAll, scroll::ScrollbarAutoHide};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
|
AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
|
||||||
KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render, ScrollWheelEvent,
|
KeyDownEvent, Keystroke, Modifiers, MouseButton, MouseDownEvent, Pixels, Render,
|
||||||
Stateful, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred, div,
|
ScrollWheelEvent, Stateful, Styled, Subscription, Task, WeakEntity, actions, anchored,
|
||||||
impl_actions,
|
deferred, div, impl_actions,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use persistence::TERMINAL_DB;
|
use persistence::TERMINAL_DB;
|
||||||
@@ -140,12 +140,37 @@ pub struct TerminalView {
|
|||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
pub enum TerminalMode {
|
pub enum TerminalMode {
|
||||||
#[default]
|
#[default]
|
||||||
Scrollable,
|
Standalone,
|
||||||
Embedded {
|
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)]
|
#[derive(Debug)]
|
||||||
struct HoverTarget {
|
struct HoverTarget {
|
||||||
tooltip: String,
|
tooltip: String,
|
||||||
@@ -223,7 +248,7 @@ impl TerminalView {
|
|||||||
blink_epoch: 0,
|
blink_epoch: 0,
|
||||||
hover: None,
|
hover: None,
|
||||||
hover_tooltip_update: Task::ready(()),
|
hover_tooltip_update: Task::ready(()),
|
||||||
mode: TerminalMode::Scrollable,
|
mode: TerminalMode::Standalone,
|
||||||
workspace_id,
|
workspace_id,
|
||||||
show_breadcrumbs: TerminalSettings::get_global(cx).toolbar.breadcrumbs,
|
show_breadcrumbs: TerminalSettings::get_global(cx).toolbar.breadcrumbs,
|
||||||
block_below_cursor: None,
|
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.
|
/// 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>) {
|
pub fn set_embedded_mode(
|
||||||
self.mode = TerminalMode::Embedded { max_lines };
|
&mut self,
|
||||||
|
max_lines_when_unfocused: Option<usize>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.mode = TerminalMode::Embedded {
|
||||||
|
max_lines_when_unfocused,
|
||||||
|
};
|
||||||
cx.notify();
|
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 {
|
match &self.mode {
|
||||||
TerminalMode::Scrollable => false,
|
TerminalMode::Standalone => ContentMode::Scrollable,
|
||||||
TerminalMode::Embedded { max_lines } => {
|
TerminalMode::Embedded {
|
||||||
!self.focus_handle.is_focused(window) && max_lines.is_some()
|
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| {
|
self.terminal.update(cx, |term, cx| {
|
||||||
term.try_keystroke(
|
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,
|
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>) {
|
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.clear_bell(cx);
|
||||||
self.terminal.update(cx, |term, cx| {
|
self.terminal.update(cx, |term, cx| {
|
||||||
let processed =
|
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)
|
if !Self::should_show_scrollbar(cx)
|
||||||
|| !(self.show_scrollbar || self.scrollbar_state.is_dragging())
|
|| !(self.show_scrollbar || self.scrollbar_state.is_dragging())
|
||||||
|| matches!(self.mode, TerminalMode::Embedded { .. })
|
|| !self.content_mode(window, cx).is_scrollable()
|
||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -1493,7 +1556,7 @@ impl Render for TerminalView {
|
|||||||
self.block_below_cursor.clone(),
|
self.block_below_cursor.clone(),
|
||||||
self.mode.clone(),
|
self.mode.clone(),
|
||||||
))
|
))
|
||||||
.when_some(self.render_scrollbar(cx), |div, scrollbar| {
|
.when_some(self.render_scrollbar(window, cx), |div, scrollbar| {
|
||||||
div.child(scrollbar)
|
div.child(scrollbar)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -148,11 +148,8 @@ impl ThemeColors {
|
|||||||
version_control_renamed: MODIFIED_COLOR,
|
version_control_renamed: MODIFIED_COLOR,
|
||||||
version_control_conflict: orange().light().step_12(),
|
version_control_conflict: orange().light().step_12(),
|
||||||
version_control_ignored: gray().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_marker_ours: green().light().step_10().alpha(0.5),
|
||||||
version_control_conflict_theirs_background: blue().light().step_10().alpha(0.5),
|
version_control_conflict_marker_theirs: 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(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,11 +270,8 @@ impl ThemeColors {
|
|||||||
version_control_renamed: MODIFIED_COLOR,
|
version_control_renamed: MODIFIED_COLOR,
|
||||||
version_control_conflict: orange().dark().step_12(),
|
version_control_conflict: orange().dark().step_12(),
|
||||||
version_control_ignored: gray().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_marker_ours: green().dark().step_10().alpha(0.5),
|
||||||
version_control_conflict_theirs_background: blue().dark().step_10().alpha(0.5),
|
version_control_conflict_marker_theirs: 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(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,23 +211,8 @@ pub(crate) fn zed_default_dark() -> Theme {
|
|||||||
version_control_renamed: MODIFIED_COLOR,
|
version_control_renamed: MODIFIED_COLOR,
|
||||||
version_control_conflict: crate::orange().light().step_12(),
|
version_control_conflict: crate::orange().light().step_12(),
|
||||||
version_control_ignored: crate::gray().light().step_12(),
|
version_control_ignored: crate::gray().light().step_12(),
|
||||||
version_control_conflict_ours_background: crate::green()
|
version_control_conflict_marker_ours: crate::green().light().step_12().alpha(0.5),
|
||||||
.light()
|
version_control_conflict_marker_theirs: crate::blue().light().step_12().alpha(0.5),
|
||||||
.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(),
|
|
||||||
},
|
},
|
||||||
status: StatusColors {
|
status: StatusColors {
|
||||||
conflict: yellow,
|
conflict: yellow,
|
||||||
|
|||||||
@@ -620,24 +620,20 @@ pub struct ThemeColorsContent {
|
|||||||
pub version_control_ignored: Option<String>,
|
pub version_control_ignored: Option<String>,
|
||||||
|
|
||||||
/// Background color for row highlights of "ours" regions in merge conflicts.
|
/// Background color for row highlights of "ours" regions in merge conflicts.
|
||||||
#[serde(rename = "version_control.conflict.ours_background")]
|
#[serde(rename = "version_control.conflict_marker.ours")]
|
||||||
pub version_control_conflict_ours_background: Option<String>,
|
pub version_control_conflict_marker_ours: Option<String>,
|
||||||
|
|
||||||
/// Background color for row highlights of "theirs" regions in merge conflicts.
|
/// 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>,
|
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 {
|
impl ThemeColorsContent {
|
||||||
@@ -1118,25 +1114,17 @@ impl ThemeColorsContent {
|
|||||||
.and_then(|color| try_parse_color(color).ok())
|
.and_then(|color| try_parse_color(color).ok())
|
||||||
// Fall back to `conflict`, for backwards compatibility.
|
// Fall back to `conflict`, for backwards compatibility.
|
||||||
.or(status_colors.ignored),
|
.or(status_colors.ignored),
|
||||||
version_control_conflict_ours_background: self
|
#[allow(deprecated)]
|
||||||
.version_control_conflict_ours_background
|
version_control_conflict_marker_ours: self
|
||||||
|
.version_control_conflict_marker_ours
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
.or(self.version_control_conflict_ours_background.as_ref())
|
||||||
.and_then(|color| try_parse_color(color).ok()),
|
.and_then(|color| try_parse_color(color).ok()),
|
||||||
version_control_conflict_theirs_background: self
|
#[allow(deprecated)]
|
||||||
.version_control_conflict_theirs_background
|
version_control_conflict_marker_theirs: self
|
||||||
.as_ref()
|
.version_control_conflict_marker_theirs
|
||||||
.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
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
.or(self.version_control_conflict_theirs_background.as_ref())
|
||||||
.and_then(|color| try_parse_color(color).ok()),
|
.and_then(|color| try_parse_color(color).ok()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -273,12 +273,9 @@ pub struct ThemeColors {
|
|||||||
pub version_control_ignored: Hsla,
|
pub version_control_ignored: Hsla,
|
||||||
|
|
||||||
/// Represents the "ours" region of a merge conflict.
|
/// 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.
|
/// Represents the "theirs" region of a merge conflict.
|
||||||
pub version_control_conflict_theirs_background: Hsla,
|
pub version_control_conflict_marker_theirs: Hsla,
|
||||||
pub version_control_conflict_ours_marker_background: Hsla,
|
|
||||||
pub version_control_conflict_theirs_marker_background: Hsla,
|
|
||||||
pub version_control_conflict_divider_background: Hsla,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(EnumIter, Debug, Clone, Copy, AsRefStr)]
|
#[derive(EnumIter, Debug, Clone, Copy, AsRefStr)]
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
use gpui::{Action, MouseButton, prelude::*};
|
use gpui::{Action, Hsla, MouseButton, prelude::*, svg};
|
||||||
|
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
|
|
||||||
use crate::window_controls::{WindowControl, WindowControlType};
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct LinuxWindowControls {
|
pub struct LinuxWindowControls {
|
||||||
close_window_action: Box<dyn Action>,
|
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,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ mod collab;
|
|||||||
mod onboarding_banner;
|
mod onboarding_banner;
|
||||||
mod platforms;
|
mod platforms;
|
||||||
mod title_bar_settings;
|
mod title_bar_settings;
|
||||||
mod window_controls;
|
|
||||||
|
|
||||||
#[cfg(feature = "stories")]
|
#[cfg(feature = "stories")]
|
||||||
mod stories;
|
mod stories;
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -533,78 +533,61 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_text_for_keystroke() {
|
fn test_text_for_keystroke() {
|
||||||
|
let cmd_keystroke = |key: &str| Keystroke {
|
||||||
|
modifiers: Modifiers::command(),
|
||||||
|
key: key.to_owned(),
|
||||||
|
key_char: None,
|
||||||
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(&cmd_keystroke("c"), PlatformStyle::Mac, false),
|
||||||
&Keystroke::parse("cmd-c").unwrap(),
|
|
||||||
PlatformStyle::Mac,
|
|
||||||
false
|
|
||||||
),
|
|
||||||
"Command-C".to_string()
|
"Command-C".to_string()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(&cmd_keystroke("c"), PlatformStyle::Linux, false),
|
||||||
&Keystroke::parse("cmd-c").unwrap(),
|
|
||||||
PlatformStyle::Linux,
|
|
||||||
false
|
|
||||||
),
|
|
||||||
"Super-C".to_string()
|
"Super-C".to_string()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(&cmd_keystroke("c"), PlatformStyle::Windows, false),
|
||||||
&Keystroke::parse("cmd-c").unwrap(),
|
|
||||||
PlatformStyle::Windows,
|
|
||||||
false
|
|
||||||
),
|
|
||||||
"Win-C".to_string()
|
"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!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(&ctrl_alt_keystroke("delete"), PlatformStyle::Mac, false),
|
||||||
&Keystroke::parse("ctrl-alt-delete").unwrap(),
|
|
||||||
PlatformStyle::Mac,
|
|
||||||
false
|
|
||||||
),
|
|
||||||
"Control-Option-Delete".to_string()
|
"Control-Option-Delete".to_string()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(&ctrl_alt_keystroke("delete"), PlatformStyle::Linux, false),
|
||||||
&Keystroke::parse("ctrl-alt-delete").unwrap(),
|
|
||||||
PlatformStyle::Linux,
|
|
||||||
false
|
|
||||||
),
|
|
||||||
"Ctrl-Alt-Delete".to_string()
|
"Ctrl-Alt-Delete".to_string()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(&ctrl_alt_keystroke("delete"), PlatformStyle::Windows, false),
|
||||||
&Keystroke::parse("ctrl-alt-delete").unwrap(),
|
|
||||||
PlatformStyle::Windows,
|
|
||||||
false
|
|
||||||
),
|
|
||||||
"Ctrl-Alt-Delete".to_string()
|
"Ctrl-Alt-Delete".to_string()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let shift_keystroke = |key: &str| Keystroke {
|
||||||
|
modifiers: Modifiers::shift(),
|
||||||
|
key: key.to_owned(),
|
||||||
|
key_char: None,
|
||||||
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(&shift_keystroke("pageup"), PlatformStyle::Mac, false),
|
||||||
&Keystroke::parse("shift-pageup").unwrap(),
|
|
||||||
PlatformStyle::Mac,
|
|
||||||
false
|
|
||||||
),
|
|
||||||
"Shift-PageUp".to_string()
|
"Shift-PageUp".to_string()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(&shift_keystroke("pageup"), PlatformStyle::Linux, false,),
|
||||||
&Keystroke::parse("shift-pageup").unwrap(),
|
|
||||||
PlatformStyle::Linux,
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
"Shift-PageUp".to_string()
|
"Shift-PageUp".to_string()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(&shift_keystroke("pageup"), PlatformStyle::Windows, false),
|
||||||
&Keystroke::parse("shift-pageup").unwrap(),
|
|
||||||
PlatformStyle::Windows,
|
|
||||||
false
|
|
||||||
),
|
|
||||||
"Shift-PageUp".to_string()
|
"Shift-PageUp".to_string()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ walkdir.workspace = true
|
|||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
command-fds = "0.3.1"
|
||||||
libc.workspace = true
|
libc.workspace = true
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
|
#![cfg_attr(not(unix), allow(unused))]
|
||||||
|
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use collections::HashMap;
|
|
||||||
use std::borrow::Cow;
|
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.
|
/// Capture all environment variables from the login shell.
|
||||||
pub fn capture(change_dir: Option<impl AsRef<Path>>) -> Result<HashMap<String, String>> {
|
#[cfg(unix)]
|
||||||
let shell_path = std::env::var("SHELL").map(PathBuf::from)?;
|
pub fn capture(directory: &std::path::Path) -> Result<collections::HashMap<String, String>> {
|
||||||
let shell_name = shell_path.file_name().and_then(OsStr::to_str);
|
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();
|
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
|
// 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, ...
|
// `cd`'d into it. We do that because tools like direnv, asdf, ...
|
||||||
// hook into `cd` and only set up the env after that.
|
// hook into `cd` and only set up the env after that.
|
||||||
if let Some(dir) = change_dir {
|
command_string.push_str(&format!("cd '{}';", directory.display()));
|
||||||
let dir_str = dir.as_ref().to_string_lossy();
|
|
||||||
command_string.push_str(&format!("cd '{dir_str}';"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// In certain shells we need to execute additional_command in order to
|
// In certain shells we need to execute additional_command in order to
|
||||||
// trigger the behavior of direnv, etc.
|
// 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()?;
|
// In some shells, file descriptors greater than 2 cannot be used in interactive mode,
|
||||||
command_string.push_str(&format!(
|
// so file descriptor 0 is used instead.
|
||||||
"sh -c 'export -p' > '{}';",
|
const ENV_OUTPUT_FD: std::os::fd::RawFd = 0;
|
||||||
env_output_file.path().to_string_lossy(),
|
command_string.push_str(&format!("sh -c 'export -p >&{ENV_OUTPUT_FD}';"));
|
||||||
));
|
|
||||||
|
|
||||||
let mut command = Command::new(&shell_path);
|
|
||||||
|
|
||||||
// For csh/tcsh, the login shell option is set by passing `-` as
|
// For csh/tcsh, the login shell option is set by passing `-` as
|
||||||
// the 0th argument instead of using `-l`.
|
// the 0th argument instead of using `-l`.
|
||||||
if let Some("tcsh" | "csh") = shell_name {
|
if let Some("tcsh" | "csh") = shell_name {
|
||||||
#[cfg(unix)]
|
command.arg0("-");
|
||||||
std::os::unix::process::CommandExt::arg0(&mut command, "-");
|
|
||||||
} else {
|
} else {
|
||||||
command.arg("-l");
|
command.arg("-l");
|
||||||
}
|
}
|
||||||
|
|
||||||
command.args(["-i", "-c", &command_string]);
|
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!(
|
anyhow::ensure!(
|
||||||
process_output.status.success(),
|
process_output.status.success(),
|
||||||
"login shell exited with {}. stdout: {:?}, stderr: {:?}",
|
"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),
|
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)
|
parse(&env_output)
|
||||||
.filter_map(|entry| match entry {
|
.filter_map(|entry| match entry {
|
||||||
Ok((name, value)) => Some(Ok((name.into(), value?.into()))),
|
Ok((name, value)) => Some(Ok((name.into(), value?.into()))),
|
||||||
Err(err) => Some(Err(err)),
|
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'`.
|
/// 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 {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn test_parse() {
|
fn test_parse() {
|
||||||
let input = indoc::indoc! {r#"
|
let input = indoc::indoc! {r#"
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ pub fn load_login_shell_environment() -> Result<()> {
|
|||||||
// into shell's `cd` command (and hooks) to manipulate env.
|
// 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
|
// We do this so that we get the env a user would have when spawning a shell
|
||||||
// in home directory.
|
// 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) };
|
unsafe { env::set_var(&name, &value) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ impl Vim {
|
|||||||
fn literal(&mut self, action: &Literal, window: &mut Window, cx: &mut Context<Self>) {
|
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(Operator::Literal { prefix }) = self.active_operator() {
|
||||||
if let Some(prefix) = prefix {
|
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.defer(cx, |window, cx| {
|
||||||
window.dispatch_keystroke(keystroke, cx);
|
window.dispatch_keystroke(keystroke, cx);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -222,7 +222,15 @@ impl NeovimBackedTestContext {
|
|||||||
pub async fn simulate_shared_keystrokes(&mut self, keystroke_texts: &str) {
|
pub async fn simulate_shared_keystrokes(&mut self, keystroke_texts: &str) {
|
||||||
for keystroke_text in keystroke_texts.split(' ') {
|
for keystroke_text in keystroke_texts.split(' ') {
|
||||||
self.recent_keystrokes.push(keystroke_text.to_string());
|
self.recent_keystrokes.push(keystroke_text.to_string());
|
||||||
|
#[cfg(not(feature = "neovim"))]
|
||||||
self.neovim.send_keystroke(keystroke_text).await;
|
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);
|
self.simulate_keystrokes(keystroke_texts);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use async_compat::Compat;
|
|||||||
#[cfg(feature = "neovim")]
|
#[cfg(feature = "neovim")]
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
#[cfg(feature = "neovim")]
|
#[cfg(feature = "neovim")]
|
||||||
use gpui::Keystroke;
|
use gpui::{Keystroke, PlatformKeyboardMapper};
|
||||||
|
|
||||||
#[cfg(feature = "neovim")]
|
#[cfg(feature = "neovim")]
|
||||||
use language::Point;
|
use language::Point;
|
||||||
@@ -110,8 +110,12 @@ impl NeovimConnection {
|
|||||||
|
|
||||||
// Sends a keystroke to the neovim process.
|
// Sends a keystroke to the neovim process.
|
||||||
#[cfg(feature = "neovim")]
|
#[cfg(feature = "neovim")]
|
||||||
pub async fn send_keystroke(&mut self, keystroke_text: &str) {
|
pub async fn send_keystroke(
|
||||||
let mut keystroke = Keystroke::parse(keystroke_text).unwrap();
|
&mut self,
|
||||||
|
keystroke_text: &str,
|
||||||
|
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||||
|
) {
|
||||||
|
let mut keystroke = Keystroke::parse(keystroke_text, keyboard_mapper).unwrap();
|
||||||
|
|
||||||
if keystroke.key == "<" {
|
if keystroke.key == "<" {
|
||||||
keystroke.key = "lt".to_string()
|
keystroke.key = "lt".to_string()
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ actions!(
|
|||||||
SwapItemRight,
|
SwapItemRight,
|
||||||
TogglePreviewTab,
|
TogglePreviewTab,
|
||||||
TogglePinTab,
|
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>) {
|
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);
|
self.change_tab_pin_state(ix, PinOperation::Pin, window, cx);
|
||||||
}
|
}
|
||||||
@@ -3132,7 +3147,7 @@ impl Pane {
|
|||||||
self.display_nav_history_buttons = display;
|
self.display_nav_history_buttons = display;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pinned_item_ids(&self) -> HashSet<EntityId> {
|
fn pinned_item_ids(&self) -> Vec<EntityId> {
|
||||||
self.items
|
self.items
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
@@ -3146,7 +3161,7 @@ impl Pane {
|
|||||||
.collect()
|
.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()
|
self.items()
|
||||||
.filter_map(|item| {
|
.filter_map(|item| {
|
||||||
if !item.is_dirty(cx) {
|
if !item.is_dirty(cx) {
|
||||||
@@ -3158,7 +3173,7 @@ impl Pane {
|
|||||||
.collect()
|
.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 {
|
match side {
|
||||||
Side::Left => self
|
Side::Left => self
|
||||||
.items()
|
.items()
|
||||||
@@ -3359,6 +3374,9 @@ impl Render for Pane {
|
|||||||
.on_action(cx.listener(|pane, action, window, cx| {
|
.on_action(cx.listener(|pane, action, window, cx| {
|
||||||
pane.toggle_pin_tab(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| {
|
.when(PreviewTabsSettings::get_global(cx).enabled, |this| {
|
||||||
this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
|
this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
|
||||||
if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
|
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);
|
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]
|
#[gpui::test]
|
||||||
async fn test_pinning_active_tab_without_position_change_maintains_focus(
|
async fn test_pinning_active_tab_without_position_change_maintains_focus(
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
|
|||||||
@@ -2159,10 +2159,11 @@ impl Workspace {
|
|||||||
cx.propagate();
|
cx.propagate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let keyboard_mapper = cx.keyboard_mapper();
|
||||||
let mut keystrokes: Vec<Keystroke> = action
|
let mut keystrokes: Vec<Keystroke> = action
|
||||||
.0
|
.0
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.flat_map(|k| Keystroke::parse(k).log_err())
|
.flat_map(|k| Keystroke::parse(k, keyboard_mapper).log_err())
|
||||||
.collect();
|
.collect();
|
||||||
keystrokes.reverse();
|
keystrokes.reverse();
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ enable = false
|
|||||||
"/assistant/context-servers.html" = "/docs/ai/mcp.html"
|
"/assistant/context-servers.html" = "/docs/ai/mcp.html"
|
||||||
"/assistant/model-context-protocol.html" = "/docs/ai/mcp.html"
|
"/assistant/model-context-protocol.html" = "/docs/ai/mcp.html"
|
||||||
"/model-improvement.html" = "/docs/ai/ai-improvement.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}`,
|
# 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
Reference in New Issue
Block a user