Compare commits
1 Commits
scan-code
...
asdf_copil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7506fd6055 |
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)
|
||||
name: Bug Report (AI Related)
|
||||
description: Zed Agent Panel Bugs
|
||||
type: "Bug"
|
||||
labels: ["ai"]
|
||||
@@ -19,14 +19,15 @@ body:
|
||||
2.
|
||||
3.
|
||||
|
||||
**Expected Behavior**:
|
||||
**Actual Behavior**:
|
||||
Actual Behavior:
|
||||
Expected Behavior:
|
||||
|
||||
### Model Provider Details
|
||||
- Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc)
|
||||
- Model Name:
|
||||
- Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads)
|
||||
- Other Details (MCPs, other settings, etc):
|
||||
- MCP Servers in-use:
|
||||
- Other Details:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
36
.github/ISSUE_TEMPLATE/02_bug_edit_predictions.yml
vendored
Normal file
36
.github/ISSUE_TEMPLATE/02_bug_edit_predictions.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
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
Normal file
35
.github/ISSUE_TEMPLATE/03_bug_git.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
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.
|
||||
3.
|
||||
|
||||
**Expected Behavior**:
|
||||
**Actual Behavior**:
|
||||
Actual Behavior:
|
||||
Expected Behavior:
|
||||
|
||||
validations:
|
||||
required: true
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
@@ -18,16 +18,14 @@ body:
|
||||
- Issues with insufficient detail may be summarily closed.
|
||||
-->
|
||||
|
||||
DESCRIPTION_HERE
|
||||
|
||||
Steps to reproduce:
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
**Expected Behavior**:
|
||||
**Actual Behavior**:
|
||||
Expected Behavior:
|
||||
Actual Behavior:
|
||||
|
||||
<!-- Before Submitting, did you:
|
||||
1. Include settings.json, keymap.json, .editorconfig if relevant?
|
||||
|
||||
13
.github/actions/run_tests/action.yml
vendored
13
.github/actions/run_tests/action.yml
vendored
@@ -1,12 +1,6 @@
|
||||
name: "Run tests"
|
||||
description: "Runs the tests"
|
||||
|
||||
inputs:
|
||||
use-xvfb:
|
||||
description: "Whether to run tests with xvfb"
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
@@ -26,9 +20,4 @@ runs:
|
||||
|
||||
- name: Run tests
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
if [ "${{ inputs.use-xvfb }}" == "true" ]; then
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24 -nolisten tcp" cargo nextest run --workspace --no-fail-fast
|
||||
else
|
||||
cargo nextest run --workspace --no-fail-fast
|
||||
fi
|
||||
run: cargo nextest run --workspace --no-fail-fast
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -319,8 +319,6 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
with:
|
||||
use-xvfb: true
|
||||
|
||||
- name: Build other binaries and features
|
||||
run: |
|
||||
|
||||
2
.github/workflows/unit_evals.yml
vendored
2
.github/workflows/unit_evals.yml
vendored
@@ -62,7 +62,7 @@ jobs:
|
||||
|
||||
- name: Run unit evals
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo nextest run --workspace --no-fail-fast --features eval --no-capture -E 'test(::eval_)'
|
||||
run: cargo nextest run --workspace --no-fail-fast --features eval --no-capture -E 'test(::eval_)' --test-threads 1
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
|
||||
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -705,7 +705,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"streaming_diff",
|
||||
"strsim",
|
||||
"task",
|
||||
@@ -3161,16 +3160,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "command-fds"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ec1052629a80c28594777d1252efc8a6b005d13f9edfd8c3fc0f44d5b32489a"
|
||||
dependencies = [
|
||||
"nix 0.30.1",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "command_palette"
|
||||
version = "0.1.0"
|
||||
@@ -4063,7 +4052,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"collections",
|
||||
"dap",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
@@ -10142,18 +10130,6 @@ dependencies = [
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases 0.2.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "node_runtime"
|
||||
version = "0.1.0"
|
||||
@@ -12134,6 +12110,7 @@ dependencies = [
|
||||
"unindent",
|
||||
"url",
|
||||
"util",
|
||||
"uuid",
|
||||
"which 6.0.3",
|
||||
"workspace-hack",
|
||||
"worktree",
|
||||
@@ -17145,7 +17122,6 @@ dependencies = [
|
||||
"async-fs",
|
||||
"async_zip",
|
||||
"collections",
|
||||
"command-fds",
|
||||
"dirs 4.0.0",
|
||||
"dunce",
|
||||
"futures 0.3.31",
|
||||
|
||||
@@ -99,8 +99,6 @@
|
||||
"version_control.added": "#27a657ff",
|
||||
"version_control.modified": "#d3b020ff",
|
||||
"version_control.deleted": "#e06c76ff",
|
||||
"version_control.conflict_marker.ours": "#a1c1811a",
|
||||
"version_control.conflict_marker.theirs": "#74ade81a",
|
||||
"conflict": "#dec184ff",
|
||||
"conflict.background": "#dec1841a",
|
||||
"conflict.border": "#5d4c2fff",
|
||||
|
||||
@@ -1788,31 +1788,12 @@ impl ActiveThread {
|
||||
|
||||
fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let message_id = self.messages[ix];
|
||||
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 {
|
||||
let Some(message) = self.thread.read(cx).message(message_id) else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
let is_generating = thread.is_generating();
|
||||
let is_generating_stale = thread.is_generation_stale().unwrap_or(false);
|
||||
|
||||
let loading_dots = (is_generating && is_last_message).then(|| {
|
||||
h_flex()
|
||||
.h_8()
|
||||
.my_3()
|
||||
.mx_5()
|
||||
.when(is_generating_stale || message.is_hidden, |this| {
|
||||
this.child(AnimatedLabel::new("").size(LabelSize::Small))
|
||||
})
|
||||
});
|
||||
|
||||
if message.is_hidden {
|
||||
return div().children(loading_dots).into_any();
|
||||
return Empty.into_any();
|
||||
}
|
||||
|
||||
let message_creases = message.creases.clone();
|
||||
@@ -1821,6 +1802,9 @@ impl ActiveThread {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let thread = self.thread.read(cx);
|
||||
|
||||
// Get all the data we need from thread before we start using it in closures
|
||||
let checkpoint = thread.checkpoint_for_message(message_id);
|
||||
let configured_model = thread.configured_model().map(|m| m.model);
|
||||
@@ -1831,6 +1815,14 @@ impl ActiveThread {
|
||||
|
||||
let tool_uses = thread.tool_uses_for_message(message_id, cx);
|
||||
let has_tool_uses = !tool_uses.is_empty();
|
||||
let is_generating = thread.is_generating();
|
||||
let is_generating_stale = thread.is_generation_stale().unwrap_or(false);
|
||||
|
||||
let is_first_message = ix == 0;
|
||||
let is_last_message = ix == self.messages.len() - 1;
|
||||
|
||||
let loading_dots = (is_generating_stale && is_last_message)
|
||||
.then(|| AnimatedLabel::new("").size(LabelSize::Small));
|
||||
|
||||
let editing_message_state = self
|
||||
.editing_message
|
||||
@@ -2246,7 +2238,17 @@ impl ActiveThread {
|
||||
parent.child(self.render_rules_item(cx))
|
||||
})
|
||||
.child(styled_message)
|
||||
.children(loading_dots)
|
||||
.when(is_generating && is_last_message, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.h_8()
|
||||
.mt_2()
|
||||
.mb_4()
|
||||
.ml_4()
|
||||
.py_1p5()
|
||||
.when_some(loading_dots, |this, loading_dots| this.child(loading_dots)),
|
||||
)
|
||||
})
|
||||
.when(show_feedback, move |parent| {
|
||||
parent.child(feedback_items).when_some(
|
||||
self.open_feedback_editors.get(&message_id),
|
||||
|
||||
@@ -12,7 +12,7 @@ use context_server::ContextServerId;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt as _, AnyView, App, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, ScrollHandle, Subscription, Transformation, percentage,
|
||||
Focusable, ScrollHandle, Subscription, pulsating_between,
|
||||
};
|
||||
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
|
||||
use project::context_server_store::{ContextServerStatus, ContextServerStore};
|
||||
@@ -475,6 +475,7 @@ impl AgentConfiguration {
|
||||
.get(&context_server_id)
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
|
||||
let tools = tools_by_source
|
||||
.get(&ToolSource::ContextServer {
|
||||
id: context_server_id.0.clone().into(),
|
||||
@@ -483,23 +484,25 @@ impl AgentConfiguration {
|
||||
let tool_count = tools.len();
|
||||
|
||||
let border_color = cx.theme().colors().border.opacity(0.6);
|
||||
let success_color = Color::Success.color(cx);
|
||||
|
||||
let (status_indicator, tooltip_text) = match server_status {
|
||||
ContextServerStatus::Starting => (
|
||||
Icon::new(IconName::LoadCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Accent)
|
||||
Indicator::dot()
|
||||
.color(Color::Success)
|
||||
.with_animation(
|
||||
SharedString::from(format!("{}-starting", context_server_id.0.clone(),)),
|
||||
Animation::new(Duration::from_secs(3)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 1.)),
|
||||
move |this, delta| this.color(success_color.alpha(delta).into()),
|
||||
)
|
||||
.into_any_element(),
|
||||
"Server is starting.",
|
||||
),
|
||||
ContextServerStatus::Running => (
|
||||
Indicator::dot().color(Color::Success).into_any_element(),
|
||||
"Server is active.",
|
||||
"Server is running.",
|
||||
),
|
||||
ContextServerStatus::Error(_) => (
|
||||
Indicator::dot().color(Color::Error).into_any_element(),
|
||||
@@ -523,11 +526,12 @@ impl AgentConfiguration {
|
||||
.p_1()
|
||||
.justify_between()
|
||||
.when(
|
||||
error.is_some() || are_tools_expanded && tool_count >= 1,
|
||||
error.is_some() || are_tools_expanded && tool_count > 1,
|
||||
|element| element.border_b_1().border_color(border_color),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Disclosure::new(
|
||||
"tool-list-disclosure",
|
||||
@@ -547,16 +551,12 @@ impl AgentConfiguration {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.id(SharedString::from(format!("tooltip-{}", item_id)))
|
||||
.h_full()
|
||||
.w_3()
|
||||
.mx_1()
|
||||
.justify_center()
|
||||
div()
|
||||
.id(item_id.clone())
|
||||
.tooltip(Tooltip::text(tooltip_text))
|
||||
.child(status_indicator),
|
||||
)
|
||||
.child(Label::new(item_id).ml_0p5().mr_1p5())
|
||||
.child(Label::new(context_server_id.0.clone()).ml_0p5())
|
||||
.when(is_running, |this| {
|
||||
this.child(
|
||||
Label::new(if tool_count == 1 {
|
||||
|
||||
@@ -386,10 +386,8 @@ impl CodegenAlternative {
|
||||
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
|
||||
} else {
|
||||
let request = self.build_request(&model, user_prompt, cx)?;
|
||||
cx.spawn(async move |_, cx| {
|
||||
Ok(model.stream_completion_text(request.await, &cx).await?)
|
||||
})
|
||||
.boxed_local()
|
||||
cx.spawn(async move |_, cx| model.stream_completion_text(request.await, &cx).await)
|
||||
.boxed_local()
|
||||
};
|
||||
self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx);
|
||||
Ok(())
|
||||
|
||||
@@ -1331,7 +1331,7 @@ impl InlineAssistant {
|
||||
editor.clear_gutter_highlights::<GutterPendingRange>(cx);
|
||||
} else {
|
||||
editor.highlight_gutter::<GutterPendingRange>(
|
||||
gutter_pending_ranges,
|
||||
&gutter_pending_ranges,
|
||||
|cx| cx.theme().status().info_background,
|
||||
cx,
|
||||
)
|
||||
@@ -1342,7 +1342,7 @@ impl InlineAssistant {
|
||||
editor.clear_gutter_highlights::<GutterTransformedRange>(cx);
|
||||
} else {
|
||||
editor.highlight_gutter::<GutterTransformedRange>(
|
||||
gutter_transformed_ranges,
|
||||
&gutter_transformed_ranges,
|
||||
|cx| cx.theme().status().info,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -1563,9 +1563,6 @@ impl Thread {
|
||||
Err(LanguageModelCompletionError::Other(error)) => {
|
||||
return Err(error);
|
||||
}
|
||||
Err(err @ LanguageModelCompletionError::RateLimit(..)) => {
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
|
||||
match event {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -407,7 +406,6 @@ impl RateLimit {
|
||||
/// <https://docs.anthropic.com/en/api/rate-limits#response-headers>
|
||||
#[derive(Debug)]
|
||||
pub struct RateLimitInfo {
|
||||
pub retry_after: Option<Duration>,
|
||||
pub requests: Option<RateLimit>,
|
||||
pub tokens: Option<RateLimit>,
|
||||
pub input_tokens: Option<RateLimit>,
|
||||
@@ -419,11 +417,10 @@ impl RateLimitInfo {
|
||||
// Check if any rate limit headers exist
|
||||
let has_rate_limit_headers = headers
|
||||
.keys()
|
||||
.any(|k| k == "retry-after" || k.as_str().starts_with("anthropic-ratelimit-"));
|
||||
.any(|k| k.as_str().starts_with("anthropic-ratelimit-"));
|
||||
|
||||
if !has_rate_limit_headers {
|
||||
return Self {
|
||||
retry_after: None,
|
||||
requests: None,
|
||||
tokens: None,
|
||||
input_tokens: None,
|
||||
@@ -432,11 +429,6 @@ impl RateLimitInfo {
|
||||
}
|
||||
|
||||
Self {
|
||||
retry_after: headers
|
||||
.get("retry-after")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.map(Duration::from_secs),
|
||||
requests: RateLimit::from_headers("requests", headers).ok(),
|
||||
tokens: RateLimit::from_headers("tokens", headers).ok(),
|
||||
input_tokens: RateLimit::from_headers("input-tokens", headers).ok(),
|
||||
@@ -489,8 +481,8 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
.send(request)
|
||||
.await
|
||||
.context("failed to send request to Anthropic")?;
|
||||
let rate_limits = RateLimitInfo::from_headers(response.headers());
|
||||
if response.status().is_success() {
|
||||
let rate_limits = RateLimitInfo::from_headers(response.headers());
|
||||
let reader = BufReader::new(response.into_body());
|
||||
let stream = reader
|
||||
.lines()
|
||||
@@ -508,8 +500,6 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
})
|
||||
.boxed();
|
||||
Ok((stream, Some(rate_limits)))
|
||||
} else if let Some(retry_after) = rate_limits.retry_after {
|
||||
Err(AnthropicError::RateLimit(retry_after))
|
||||
} else {
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
@@ -779,8 +769,6 @@ pub struct MessageDelta {
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AnthropicError {
|
||||
#[error("rate limit exceeded, retry after {0:?}")]
|
||||
RateLimit(Duration),
|
||||
#[error("an error occurred while interacting with the Anthropic API: {error_type}: {message}", error_type = .0.error_type, message = .0.message)]
|
||||
ApiError(ApiError),
|
||||
#[error("{0}")]
|
||||
|
||||
@@ -682,12 +682,11 @@ mod tests {
|
||||
_: &AsyncApp,
|
||||
) -> BoxFuture<
|
||||
'static,
|
||||
Result<
|
||||
http_client::Result<
|
||||
BoxStream<
|
||||
'static,
|
||||
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
||||
http_client::Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
||||
>,
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
unimplemented!()
|
||||
|
||||
@@ -80,7 +80,6 @@ rand.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
smol.workspace = true
|
||||
task = { workspace = true, features = ["test-support"]}
|
||||
tempfile.workspace = true
|
||||
theme.workspace = true
|
||||
|
||||
@@ -11,7 +11,7 @@ use client::{Client, UserStore};
|
||||
use collections::HashMap;
|
||||
use fs::FakeFs;
|
||||
use futures::{FutureExt, future::LocalBoxFuture};
|
||||
use gpui::{AppContext, TestAppContext, Timer};
|
||||
use gpui::{AppContext, TestAppContext};
|
||||
use indoc::{formatdoc, indoc};
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
@@ -1255,12 +1255,9 @@ impl EvalAssertion {
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
let mut response = retry_on_rate_limit(async || {
|
||||
Ok(judge
|
||||
.stream_completion_text(request.clone(), &cx.to_async())
|
||||
.await?)
|
||||
})
|
||||
.await?;
|
||||
let mut response = judge
|
||||
.stream_completion_text(request, &cx.to_async())
|
||||
.await?;
|
||||
let mut output = String::new();
|
||||
while let Some(chunk) = response.stream.next().await {
|
||||
let chunk = chunk?;
|
||||
@@ -1311,17 +1308,10 @@ fn eval(
|
||||
run_eval(eval.clone(), tx.clone());
|
||||
|
||||
let executor = gpui::background_executor();
|
||||
let semaphore = Arc::new(smol::lock::Semaphore::new(32));
|
||||
for _ in 1..iterations {
|
||||
let eval = eval.clone();
|
||||
let tx = tx.clone();
|
||||
let semaphore = semaphore.clone();
|
||||
executor
|
||||
.spawn(async move {
|
||||
let _guard = semaphore.acquire().await;
|
||||
run_eval(eval, tx)
|
||||
})
|
||||
.detach();
|
||||
executor.spawn(async move { run_eval(eval, tx) }).detach();
|
||||
}
|
||||
drop(tx);
|
||||
|
||||
@@ -1587,31 +1577,21 @@ impl EditAgentTest {
|
||||
if let Some(input_content) = eval.input_content.as_deref() {
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));
|
||||
}
|
||||
retry_on_rate_limit(async || {
|
||||
self.agent
|
||||
.edit(
|
||||
buffer.clone(),
|
||||
eval.edit_file_input.display_description.clone(),
|
||||
&conversation,
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.0
|
||||
.await
|
||||
})
|
||||
.await?
|
||||
let (edit_output, _) = self.agent.edit(
|
||||
buffer.clone(),
|
||||
eval.edit_file_input.display_description,
|
||||
&conversation,
|
||||
&mut cx.to_async(),
|
||||
);
|
||||
edit_output.await?
|
||||
} else {
|
||||
retry_on_rate_limit(async || {
|
||||
self.agent
|
||||
.overwrite(
|
||||
buffer.clone(),
|
||||
eval.edit_file_input.display_description.clone(),
|
||||
&conversation,
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.0
|
||||
.await
|
||||
})
|
||||
.await?
|
||||
let (edit_output, _) = self.agent.overwrite(
|
||||
buffer.clone(),
|
||||
eval.edit_file_input.display_description,
|
||||
&conversation,
|
||||
&mut cx.to_async(),
|
||||
);
|
||||
edit_output.await?
|
||||
};
|
||||
|
||||
let buffer_text = buffer.read_with(cx, |buffer, _| buffer.text());
|
||||
@@ -1633,26 +1613,6 @@ impl EditAgentTest {
|
||||
}
|
||||
}
|
||||
|
||||
async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) -> Result<R> {
|
||||
loop {
|
||||
match request().await {
|
||||
Ok(result) => return Ok(result),
|
||||
Err(err) => match err.downcast::<LanguageModelCompletionError>() {
|
||||
Ok(err) => match err {
|
||||
LanguageModelCompletionError::RateLimit(duration) => {
|
||||
// Wait until after we are allowed to try again
|
||||
eprintln!("Rate limit exceeded. Waiting for {duration:?}...",);
|
||||
Timer::after(duration).await;
|
||||
continue;
|
||||
}
|
||||
_ => return Err(err.into()),
|
||||
},
|
||||
Err(err) => return Err(err),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
struct EvalAssertionOutcome {
|
||||
score: usize,
|
||||
|
||||
@@ -638,36 +638,29 @@ impl ToolCard for TerminalToolCard {
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_b_md()
|
||||
.text_ui_sm(cx)
|
||||
.child({
|
||||
let content_mode = terminal.read(cx).content_mode(window, cx);
|
||||
|
||||
if content_mode.is_scrollable() {
|
||||
div().h_72().child(terminal.clone()).into_any_element()
|
||||
} else {
|
||||
ToolOutputPreview::new(
|
||||
terminal.clone().into_any_element(),
|
||||
terminal.entity_id(),
|
||||
)
|
||||
.with_total_lines(self.content_line_count)
|
||||
.toggle_state(!content_mode.is_limited())
|
||||
.on_toggle({
|
||||
let terminal = terminal.clone();
|
||||
move |is_expanded, _, cx| {
|
||||
terminal.update(cx, |terminal, cx| {
|
||||
terminal.set_embedded_mode(
|
||||
if is_expanded {
|
||||
None
|
||||
} else {
|
||||
Some(COLLAPSED_LINES)
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}),
|
||||
.child(
|
||||
ToolOutputPreview::new(
|
||||
terminal.clone().into_any_element(),
|
||||
terminal.entity_id(),
|
||||
)
|
||||
.with_total_lines(self.content_line_count)
|
||||
.toggle_state(!terminal.read(cx).is_content_limited(window))
|
||||
.on_toggle({
|
||||
let terminal = terminal.clone();
|
||||
move |is_expanded, _, cx| {
|
||||
terminal.update(cx, |terminal, cx| {
|
||||
terminal.set_embedded_mode(
|
||||
if is_expanded {
|
||||
None
|
||||
} else {
|
||||
Some(COLLAPSED_LINES)
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -452,10 +452,6 @@ impl Model {
|
||||
| Model::Claude3_5SonnetV2
|
||||
| Model::Claude3_7Sonnet
|
||||
| Model::Claude3_7SonnetThinking
|
||||
| Model::ClaudeSonnet4
|
||||
| Model::ClaudeSonnet4Thinking
|
||||
| Model::ClaudeOpus4
|
||||
| Model::ClaudeOpus4Thinking
|
||||
| Model::Claude3Haiku
|
||||
| Model::Claude3Opus
|
||||
| Model::Claude3Sonnet
|
||||
|
||||
@@ -501,10 +501,8 @@ impl Database {
|
||||
|
||||
/// Returns all channels for the user with the given ID.
|
||||
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
|
||||
self.weak_transaction(
|
||||
|tx| async move { self.get_user_channels(user_id, None, true, &tx).await },
|
||||
)
|
||||
.await
|
||||
self.transaction(|tx| async move { self.get_user_channels(user_id, None, true, &tx).await })
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all channels for the user with the given ID that are descendants
|
||||
|
||||
@@ -15,7 +15,7 @@ impl Database {
|
||||
user_b_busy: bool,
|
||||
}
|
||||
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let user_a_participant = Alias::new("user_a_participant");
|
||||
let user_b_participant = Alias::new("user_b_participant");
|
||||
let mut db_contacts = contact::Entity::find()
|
||||
@@ -91,7 +91,7 @@ impl Database {
|
||||
|
||||
/// Returns whether the given user is a busy (on a call).
|
||||
pub async fn is_user_busy(&self, user_id: UserId) -> Result<bool> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let participant = room_participant::Entity::find()
|
||||
.filter(room_participant::Column::UserId.eq(user_id))
|
||||
.one(&*tx)
|
||||
|
||||
@@ -80,7 +80,7 @@ impl Database {
|
||||
&self,
|
||||
user_id: UserId,
|
||||
) -> Result<Option<proto::IncomingCall>> {
|
||||
self.weak_transaction(|tx| async move {
|
||||
self.transaction(|tx| async move {
|
||||
let pending_participant = room_participant::Entity::find()
|
||||
.filter(
|
||||
room_participant::Column::UserId
|
||||
|
||||
@@ -7,12 +7,6 @@ pub use token::*;
|
||||
|
||||
pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial";
|
||||
|
||||
/// The name of the feature flag that bypasses the account age check.
|
||||
pub const BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG: &str = "bypass-account-age-check";
|
||||
|
||||
/// The minimum account age an account must have in order to use the LLM service.
|
||||
pub const MIN_ACCOUNT_AGE_FOR_LLM_USE: chrono::Duration = chrono::Duration::days(30);
|
||||
|
||||
/// The default value to use for maximum spend per month if the user did not
|
||||
/// explicitly set a maximum spend.
|
||||
///
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::db::billing_subscription::SubscriptionKind;
|
||||
use crate::db::{billing_customer, billing_subscription, user};
|
||||
use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG};
|
||||
use crate::llm::AGENT_EXTENDED_TRIAL_FEATURE_FLAG;
|
||||
use crate::{Config, db::billing_preference};
|
||||
use anyhow::{Context as _, Result};
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
@@ -84,7 +84,7 @@ impl LlmTokenClaims {
|
||||
.any(|flag| flag == "llm-closed-beta"),
|
||||
bypass_account_age_check: feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG),
|
||||
.any(|flag| flag == "bypass-account-age-check"),
|
||||
can_use_web_search_tool: true,
|
||||
use_llm_request_queue: feature_flags.iter().any(|flag| flag == "llm-request-queue"),
|
||||
plan,
|
||||
|
||||
@@ -4,10 +4,7 @@ use crate::api::billing::find_or_create_billing_customer;
|
||||
use crate::api::{CloudflareIpCountryHeader, SystemIdHeader};
|
||||
use crate::db::billing_subscription::SubscriptionKind;
|
||||
use crate::llm::db::LlmDatabase;
|
||||
use crate::llm::{
|
||||
AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG, LlmTokenClaims,
|
||||
MIN_ACCOUNT_AGE_FOR_LLM_USE,
|
||||
};
|
||||
use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, LlmTokenClaims};
|
||||
use crate::stripe_client::StripeCustomerId;
|
||||
use crate::{
|
||||
AppState, Error, Result, auth,
|
||||
@@ -68,7 +65,7 @@ use std::{
|
||||
rc::Rc,
|
||||
sync::{
|
||||
Arc, OnceLock,
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -89,36 +86,10 @@ pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
const MESSAGE_COUNT_PER_PAGE: usize = 100;
|
||||
const MAX_MESSAGE_LEN: usize = 1024;
|
||||
const NOTIFICATION_COUNT_PER_PAGE: usize = 50;
|
||||
const MAX_CONCURRENT_CONNECTIONS: usize = 512;
|
||||
|
||||
static CONCURRENT_CONNECTIONS: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
type MessageHandler =
|
||||
Box<dyn Send + Sync + Fn(Box<dyn AnyTypedEnvelope>, Session) -> BoxFuture<'static, ()>>;
|
||||
|
||||
pub struct ConnectionGuard;
|
||||
|
||||
impl ConnectionGuard {
|
||||
pub fn try_acquire() -> Result<Self, ()> {
|
||||
let current_connections = CONCURRENT_CONNECTIONS.fetch_add(1, SeqCst);
|
||||
if current_connections >= MAX_CONCURRENT_CONNECTIONS {
|
||||
CONCURRENT_CONNECTIONS.fetch_sub(1, SeqCst);
|
||||
tracing::error!(
|
||||
"too many concurrent connections: {}",
|
||||
current_connections + 1
|
||||
);
|
||||
return Err(());
|
||||
}
|
||||
Ok(ConnectionGuard)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ConnectionGuard {
|
||||
fn drop(&mut self) {
|
||||
CONCURRENT_CONNECTIONS.fetch_sub(1, SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
struct Response<R> {
|
||||
peer: Arc<Peer>,
|
||||
receipt: Receipt<R>,
|
||||
@@ -751,7 +722,6 @@ impl Server {
|
||||
system_id: Option<String>,
|
||||
send_connection_id: Option<oneshot::Sender<ConnectionId>>,
|
||||
executor: Executor,
|
||||
connection_guard: Option<ConnectionGuard>,
|
||||
) -> impl Future<Output = ()> + use<> {
|
||||
let this = self.clone();
|
||||
let span = info_span!("handle connection", %address,
|
||||
@@ -772,7 +742,6 @@ impl Server {
|
||||
tracing::error!("server is tearing down");
|
||||
return
|
||||
}
|
||||
|
||||
let (connection_id, handle_io, mut incoming_rx) = this
|
||||
.peer
|
||||
.add_connection(connection, {
|
||||
@@ -814,7 +783,6 @@ impl Server {
|
||||
tracing::error!(?error, "failed to send initial client update");
|
||||
return;
|
||||
}
|
||||
drop(connection_guard);
|
||||
|
||||
let handle_io = handle_io.fuse();
|
||||
futures::pin_mut!(handle_io);
|
||||
@@ -1186,19 +1154,6 @@ pub async fn handle_websocket_request(
|
||||
}
|
||||
|
||||
let socket_address = socket_address.to_string();
|
||||
|
||||
// Acquire connection guard before WebSocket upgrade
|
||||
let connection_guard = match ConnectionGuard::try_acquire() {
|
||||
Ok(guard) => guard,
|
||||
Err(()) => {
|
||||
return (
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"Too many concurrent connections",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
ws.on_upgrade(move |socket| {
|
||||
let socket = socket
|
||||
.map_ok(to_tungstenite_message)
|
||||
@@ -1216,7 +1171,6 @@ pub async fn handle_websocket_request(
|
||||
system_id_header.map(|header| header.to_string()),
|
||||
None,
|
||||
Executor::Production,
|
||||
Some(connection_guard),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -2819,12 +2773,8 @@ async fn make_update_user_plan_message(
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let bypass_account_age_check = feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG);
|
||||
let account_too_young = !matches!(plan, proto::Plan::ZedPro)
|
||||
&& !bypass_account_age_check
|
||||
&& user.account_age() < MIN_ACCOUNT_AGE_FOR_LLM_USE;
|
||||
let account_too_young =
|
||||
!matches!(plan, proto::Plan::ZedPro) && user.account_age() < MIN_ACCOUNT_AGE_FOR_LLM_USE;
|
||||
|
||||
Ok(proto::UpdateUserPlan {
|
||||
plan: plan.into(),
|
||||
@@ -4125,6 +4075,9 @@ async fn accept_terms_of_service(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The minimum account age an account must have in order to use the LLM service.
|
||||
pub const MIN_ACCOUNT_AGE_FOR_LLM_USE: chrono::Duration = chrono::Duration::days(30);
|
||||
|
||||
async fn get_llm_api_token(
|
||||
_request: proto::GetLlmToken,
|
||||
response: Response<proto::GetLlmToken>,
|
||||
|
||||
@@ -258,7 +258,6 @@ impl TestServer {
|
||||
None,
|
||||
Some(connection_id_tx),
|
||||
Executor::Deterministic(cx.background_executor().clone()),
|
||||
None,
|
||||
))
|
||||
.detach();
|
||||
let connection_id = connection_id_rx.await.map_err(|e| {
|
||||
|
||||
@@ -28,7 +28,7 @@ use settings::SettingsStore;
|
||||
use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
env,
|
||||
env::home_dir,
|
||||
ffi::OsString,
|
||||
mem,
|
||||
ops::Range,
|
||||
@@ -486,11 +486,14 @@ impl Copilot {
|
||||
env,
|
||||
};
|
||||
|
||||
let root_path = if cfg!(target_os = "windows") {
|
||||
Path::new("C:/")
|
||||
} else {
|
||||
Path::new("/")
|
||||
};
|
||||
let root_path = home_dir();
|
||||
let root_path = root_path.as_deref().unwrap_or_else(|| {
|
||||
if cfg!(target_os = "windows") {
|
||||
Path::new("C:/")
|
||||
} else {
|
||||
Path::new("/")
|
||||
}
|
||||
});
|
||||
|
||||
let server_name = LanguageServerName("copilot".into());
|
||||
let server = LanguageServer::new(
|
||||
|
||||
@@ -23,7 +23,6 @@ doctest = false
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
collections.workspace = true
|
||||
dap.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
use anyhow::{Result, bail};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use collections::FxHashMap;
|
||||
use dap::{
|
||||
DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
|
||||
DebugRequest, StartDebuggingRequestArguments,
|
||||
adapters::{
|
||||
DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
|
||||
},
|
||||
};
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
use language::LanguageName;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
use std::{ffi::OsStr, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
use task::{DebugScenario, ZedDebugConfig};
|
||||
use util::command::new_smol_command;
|
||||
|
||||
@@ -23,18 +21,6 @@ impl RubyDebugAdapter {
|
||||
const ADAPTER_NAME: &'static str = "Ruby";
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct RubyDebugConfig {
|
||||
script_or_command: Option<String>,
|
||||
script: Option<String>,
|
||||
command: Option<String>,
|
||||
#[serde(default)]
|
||||
args: Vec<String>,
|
||||
#[serde(default)]
|
||||
env: FxHashMap<String, String>,
|
||||
cwd: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for RubyDebugAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
@@ -45,70 +31,185 @@ impl DebugAdapter for RubyDebugAdapter {
|
||||
Some(SharedString::new_static("Ruby").into())
|
||||
}
|
||||
|
||||
fn request_kind(&self, _: &serde_json::Value) -> Result<StartDebuggingRequestArgumentsRequest> {
|
||||
Ok(StartDebuggingRequestArgumentsRequest::Launch)
|
||||
}
|
||||
|
||||
async fn dap_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
|
||||
"oneOf": [
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["request"],
|
||||
"properties": {
|
||||
"request": {
|
||||
"type": "string",
|
||||
"enum": ["launch"],
|
||||
"description": "Request to launch a new process"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["script"],
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
|
||||
"default": "ruby"
|
||||
},
|
||||
"script": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to a Ruby file."
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string",
|
||||
"description": "Directory to execute the program in",
|
||||
"default": "${ZED_WORKTREE_ROOT}"
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"description": "Command line arguments passed to the program",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"description": "Additional environment variables to pass to the debugging (and debugged) process",
|
||||
"default": {}
|
||||
},
|
||||
"showProtocolLog": {
|
||||
"type": "boolean",
|
||||
"description": "Show a log of DAP requests, events, and responses",
|
||||
"default": false
|
||||
},
|
||||
"useBundler": {
|
||||
"type": "boolean",
|
||||
"description": "Execute Ruby programs with `bundle exec` instead of directly",
|
||||
"default": false
|
||||
},
|
||||
"bundlePath": {
|
||||
"type": "string",
|
||||
"description": "Location of the bundle executable"
|
||||
},
|
||||
"rdbgPath": {
|
||||
"type": "string",
|
||||
"description": "Location of the rdbg executable"
|
||||
},
|
||||
"askParameters": {
|
||||
"type": "boolean",
|
||||
"description": "Ask parameters at first."
|
||||
},
|
||||
"debugPort": {
|
||||
"type": "string",
|
||||
"description": "UNIX domain socket name or TPC/IP host:port"
|
||||
},
|
||||
"waitLaunchTime": {
|
||||
"type": "number",
|
||||
"description": "Wait time before connection in milliseconds"
|
||||
},
|
||||
"localfs": {
|
||||
"type": "boolean",
|
||||
"description": "true if the VSCode and debugger run on a same machine",
|
||||
"default": false
|
||||
},
|
||||
"useTerminal": {
|
||||
"type": "boolean",
|
||||
"description": "Create a new terminal and then execute commands there",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"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": {}
|
||||
},
|
||||
}
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["request"],
|
||||
"properties": {
|
||||
"request": {
|
||||
"type": "string",
|
||||
"enum": ["attach"],
|
||||
"description": "Request to attach to an existing process"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"rdbgPath": {
|
||||
"type": "string",
|
||||
"description": "Location of the rdbg executable"
|
||||
},
|
||||
"debugPort": {
|
||||
"type": "string",
|
||||
"description": "UNIX domain socket name or TPC/IP host:port"
|
||||
},
|
||||
"showProtocolLog": {
|
||||
"type": "boolean",
|
||||
"description": "Show a log of DAP requests, events, and responses",
|
||||
"default": false
|
||||
},
|
||||
"localfs": {
|
||||
"type": "boolean",
|
||||
"description": "true if the VSCode and debugger run on a same machine",
|
||||
"default": false
|
||||
},
|
||||
"localfsMap": {
|
||||
"type": "string",
|
||||
"description": "Specify pairs of remote root path and local root path like `/remote_dir:/local_dir`. You can specify multiple pairs like `/rem1:/loc1,/rem2:/loc2` by concatenating with `,`."
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"description": "Additional environment variables to pass to the rdbg process",
|
||||
"default": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
match zed_scenario.request {
|
||||
let mut config = serde_json::Map::new();
|
||||
|
||||
match &zed_scenario.request {
|
||||
DebugRequest::Launch(launch) => {
|
||||
let config = RubyDebugConfig {
|
||||
script_or_command: Some(launch.program),
|
||||
script: None,
|
||||
command: None,
|
||||
args: launch.args,
|
||||
env: launch.env,
|
||||
cwd: launch.cwd.clone(),
|
||||
};
|
||||
config.insert("request".to_string(), json!("launch"));
|
||||
config.insert("script".to_string(), json!(launch.program));
|
||||
config.insert("command".to_string(), json!("ruby"));
|
||||
|
||||
let config = serde_json::to_value(config)?;
|
||||
if !launch.args.is_empty() {
|
||||
config.insert("args".to_string(), json!(launch.args));
|
||||
}
|
||||
|
||||
Ok(DebugScenario {
|
||||
adapter: zed_scenario.adapter,
|
||||
label: zed_scenario.label,
|
||||
config,
|
||||
tcp_connection: None,
|
||||
build: None,
|
||||
})
|
||||
if !launch.env.is_empty() {
|
||||
config.insert("env".to_string(), json!(launch.env));
|
||||
}
|
||||
|
||||
if let Some(cwd) = &launch.cwd {
|
||||
config.insert("cwd".to_string(), json!(cwd));
|
||||
}
|
||||
|
||||
// Ruby stops on entry so there's no need to handle that case
|
||||
}
|
||||
DebugRequest::Attach(_) => {
|
||||
anyhow::bail!("Attach requests are unsupported");
|
||||
DebugRequest::Attach(attach) => {
|
||||
config.insert("request".to_string(), json!("attach"));
|
||||
|
||||
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(
|
||||
@@ -146,34 +247,13 @@ impl DebugAdapter for RubyDebugAdapter {
|
||||
|
||||
let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
|
||||
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
|
||||
let ruby_config = serde_json::from_value::<RubyDebugConfig>(definition.config.clone())?;
|
||||
|
||||
let mut arguments = vec![
|
||||
let arguments = vec![
|
||||
"--open".to_string(),
|
||||
format!("--port={}", port),
|
||||
format!("--host={}", host),
|
||||
];
|
||||
|
||||
if let Some(script) = &ruby_config.script {
|
||||
arguments.push(script.clone());
|
||||
} else if let Some(command) = &ruby_config.command {
|
||||
arguments.push("--command".to_string());
|
||||
arguments.push(command.clone());
|
||||
} else if let Some(command_or_script) = &ruby_config.script_or_command {
|
||||
if delegate
|
||||
.which(OsStr::new(&command_or_script))
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
arguments.push("--command".to_string());
|
||||
}
|
||||
arguments.push(command_or_script.clone());
|
||||
} else {
|
||||
bail!("Ruby debug config must have 'script' or 'command' args");
|
||||
}
|
||||
|
||||
arguments.extend(ruby_config.args);
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: rdbg_path.to_string_lossy().to_string(),
|
||||
arguments,
|
||||
@@ -182,12 +262,8 @@ impl DebugAdapter for RubyDebugAdapter {
|
||||
port,
|
||||
timeout,
|
||||
}),
|
||||
cwd: Some(
|
||||
ruby_config
|
||||
.cwd
|
||||
.unwrap_or(delegate.worktree_root_path().to_owned()),
|
||||
),
|
||||
envs: ruby_config.env.into_iter().collect(),
|
||||
cwd: None,
|
||||
envs: std::collections::HashMap::default(),
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
request: self.request_kind(&definition.config)?,
|
||||
configuration: definition.config.clone(),
|
||||
|
||||
@@ -5,8 +5,8 @@ use std::time::Duration;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use dap::StackFrameId;
|
||||
use gpui::{
|
||||
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, MouseButton, Stateful,
|
||||
Subscription, Task, WeakEntity, list,
|
||||
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, MouseButton, ScrollStrategy,
|
||||
Stateful, Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list,
|
||||
};
|
||||
|
||||
use crate::StackTraceView;
|
||||
@@ -35,7 +35,7 @@ pub struct StackFrameList {
|
||||
selected_ix: Option<usize>,
|
||||
opened_stack_frame_id: Option<StackFrameId>,
|
||||
scrollbar_state: ScrollbarState,
|
||||
list_state: ListState,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
_refresh_task: Task<()>,
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ impl StackFrameList {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let scroll_handle = UniformListScrollHandle::new();
|
||||
|
||||
let _subscription =
|
||||
cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
|
||||
@@ -66,16 +67,8 @@ impl StackFrameList {
|
||||
_ => {}
|
||||
});
|
||||
|
||||
let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.), {
|
||||
let this = cx.weak_entity();
|
||||
move |ix, _window, cx| {
|
||||
this.update(cx, |this, cx| this.render_entry(ix, cx))
|
||||
.unwrap_or(div().into_any())
|
||||
}
|
||||
});
|
||||
let scrollbar_state = ScrollbarState::new(list_state.clone());
|
||||
|
||||
let mut this = Self {
|
||||
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
|
||||
session,
|
||||
workspace,
|
||||
focus_handle,
|
||||
@@ -84,8 +77,7 @@ impl StackFrameList {
|
||||
entries: Default::default(),
|
||||
selected_ix: None,
|
||||
opened_stack_frame_id: None,
|
||||
list_state,
|
||||
scrollbar_state,
|
||||
scroll_handle,
|
||||
_refresh_task: Task::ready(()),
|
||||
};
|
||||
this.schedule_refresh(true, window, cx);
|
||||
@@ -222,7 +214,6 @@ impl StackFrameList {
|
||||
self.selected_ix = ix;
|
||||
}
|
||||
|
||||
self.list_state.reset(self.entries.len());
|
||||
cx.emit(StackFrameListEvent::BuiltEntries);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -564,6 +555,10 @@ impl StackFrameList {
|
||||
|
||||
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
|
||||
self.selected_ix = ix;
|
||||
if let Some(ix) = self.selected_ix {
|
||||
self.scroll_handle
|
||||
.scroll_to_item(ix, ScrollStrategy::Center);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -647,8 +642,15 @@ impl StackFrameList {
|
||||
self.activate_selected_entry(window, cx);
|
||||
}
|
||||
|
||||
fn render_list(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
list(self.list_state.clone()).size_full()
|
||||
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
uniform_list(
|
||||
cx.entity(),
|
||||
"stack-frame-list",
|
||||
self.entries.len(),
|
||||
|this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(),
|
||||
)
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.size_full()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -698,7 +698,7 @@ impl EditorActionId {
|
||||
// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
|
||||
|
||||
type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range<Anchor>]>);
|
||||
type GutterHighlight = (fn(&App) -> Hsla, Vec<Range<Anchor>>);
|
||||
type GutterHighlight = (fn(&App) -> Hsla, Arc<[Range<Anchor>]>);
|
||||
|
||||
#[derive(Default)]
|
||||
struct ScrollbarMarkerState {
|
||||
@@ -923,7 +923,7 @@ enum SelectionDragState {
|
||||
|
||||
/// Represents a breakpoint indicator that shows up when hovering over lines in the gutter that don't have
|
||||
/// a breakpoint on them.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct PhantomBreakpointIndicator {
|
||||
display_row: DisplayRow,
|
||||
/// There's a small debounce between hovering over the line and showing the indicator.
|
||||
@@ -931,7 +931,6 @@ struct PhantomBreakpointIndicator {
|
||||
is_active: bool,
|
||||
collides_with_existing_breakpoint: bool,
|
||||
}
|
||||
|
||||
/// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`].
|
||||
///
|
||||
/// See the [module level documentation](self) for more information.
|
||||
@@ -1200,12 +1199,10 @@ struct SelectionHistoryEntry {
|
||||
add_selections_state: Option<AddSelectionsState>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
enum SelectionHistoryMode {
|
||||
Normal,
|
||||
Undoing,
|
||||
Redoing,
|
||||
Skipping,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
@@ -1239,19 +1236,11 @@ struct SelectionHistory {
|
||||
}
|
||||
|
||||
impl SelectionHistory {
|
||||
#[track_caller]
|
||||
fn insert_transaction(
|
||||
&mut self,
|
||||
transaction_id: TransactionId,
|
||||
selections: Arc<[Selection<Anchor>]>,
|
||||
) {
|
||||
if selections.is_empty() {
|
||||
log::error!(
|
||||
"SelectionHistory::insert_transaction called with empty selections. Caller: {}",
|
||||
std::panic::Location::caller()
|
||||
);
|
||||
return;
|
||||
}
|
||||
self.selections_by_transaction
|
||||
.insert(transaction_id, (selections, None));
|
||||
}
|
||||
@@ -1281,7 +1270,6 @@ impl SelectionHistory {
|
||||
}
|
||||
SelectionHistoryMode::Undoing => self.push_redo(entry),
|
||||
SelectionHistoryMode::Redoing => self.push_undo(entry),
|
||||
SelectionHistoryMode::Skipping => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2101,11 +2089,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
// skip adding the initial selection to selection history
|
||||
editor.selection_history.mode = SelectionHistoryMode::Skipping;
|
||||
editor.end_selection(window, cx);
|
||||
editor.selection_history.mode = SelectionHistoryMode::Normal;
|
||||
|
||||
editor.scroll_manager.show_scrollbars(window, cx);
|
||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut editor, &buffer, cx);
|
||||
|
||||
@@ -14227,20 +14211,18 @@ impl Editor {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction);
|
||||
self.end_selection(window, cx);
|
||||
self.selection_history.mode = SelectionHistoryMode::Undoing;
|
||||
if let Some(entry) = self.selection_history.undo_stack.pop_back() {
|
||||
self.selection_history.mode = SelectionHistoryMode::Undoing;
|
||||
self.with_selection_effects_deferred(window, cx, |this, window, cx| {
|
||||
this.end_selection(window, cx);
|
||||
this.change_selections(Some(Autoscroll::newest()), window, cx, |s| {
|
||||
s.select_anchors(entry.selections.to_vec())
|
||||
});
|
||||
self.change_selections(None, window, cx, |s| {
|
||||
s.select_anchors(entry.selections.to_vec())
|
||||
});
|
||||
self.selection_history.mode = SelectionHistoryMode::Normal;
|
||||
|
||||
self.select_next_state = entry.select_next_state;
|
||||
self.select_prev_state = entry.select_prev_state;
|
||||
self.add_selections_state = entry.add_selections_state;
|
||||
self.request_autoscroll(Autoscroll::newest(), cx);
|
||||
}
|
||||
self.selection_history.mode = SelectionHistoryMode::Normal;
|
||||
}
|
||||
|
||||
pub fn redo_selection(
|
||||
@@ -14250,20 +14232,18 @@ impl Editor {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction);
|
||||
self.end_selection(window, cx);
|
||||
self.selection_history.mode = SelectionHistoryMode::Redoing;
|
||||
if let Some(entry) = self.selection_history.redo_stack.pop_back() {
|
||||
self.selection_history.mode = SelectionHistoryMode::Redoing;
|
||||
self.with_selection_effects_deferred(window, cx, |this, window, cx| {
|
||||
this.end_selection(window, cx);
|
||||
this.change_selections(Some(Autoscroll::newest()), window, cx, |s| {
|
||||
s.select_anchors(entry.selections.to_vec())
|
||||
});
|
||||
self.change_selections(None, window, cx, |s| {
|
||||
s.select_anchors(entry.selections.to_vec())
|
||||
});
|
||||
self.selection_history.mode = SelectionHistoryMode::Normal;
|
||||
|
||||
self.select_next_state = entry.select_next_state;
|
||||
self.select_prev_state = entry.select_prev_state;
|
||||
self.add_selections_state = entry.add_selections_state;
|
||||
self.request_autoscroll(Autoscroll::newest(), cx);
|
||||
}
|
||||
self.selection_history.mode = SelectionHistoryMode::Normal;
|
||||
}
|
||||
|
||||
pub fn expand_excerpts(
|
||||
@@ -18397,12 +18377,12 @@ impl Editor {
|
||||
|
||||
pub fn highlight_gutter<T: 'static>(
|
||||
&mut self,
|
||||
ranges: impl Into<Vec<Range<Anchor>>>,
|
||||
ranges: &[Range<Anchor>],
|
||||
color_fetcher: fn(&App) -> Hsla,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.gutter_highlights
|
||||
.insert(TypeId::of::<T>(), (color_fetcher, ranges.into()));
|
||||
.insert(TypeId::of::<T>(), (color_fetcher, Arc::from(ranges)));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -18414,65 +18394,6 @@ impl Editor {
|
||||
self.gutter_highlights.remove(&TypeId::of::<T>())
|
||||
}
|
||||
|
||||
pub fn insert_gutter_highlight<T: 'static>(
|
||||
&mut self,
|
||||
range: Range<Anchor>,
|
||||
color_fetcher: fn(&App) -> Hsla,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
let mut highlights = self
|
||||
.gutter_highlights
|
||||
.remove(&TypeId::of::<T>())
|
||||
.map(|(_, highlights)| highlights)
|
||||
.unwrap_or_default();
|
||||
let ix = highlights.binary_search_by(|highlight| {
|
||||
Ordering::Equal
|
||||
.then_with(|| highlight.start.cmp(&range.start, &snapshot))
|
||||
.then_with(|| highlight.end.cmp(&range.end, &snapshot))
|
||||
});
|
||||
if let Err(ix) = ix {
|
||||
highlights.insert(ix, range);
|
||||
}
|
||||
self.gutter_highlights
|
||||
.insert(TypeId::of::<T>(), (color_fetcher, highlights));
|
||||
}
|
||||
|
||||
pub fn remove_gutter_highlights<T: 'static>(
|
||||
&mut self,
|
||||
ranges_to_remove: Vec<Range<Anchor>>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
let Some((color_fetcher, mut gutter_highlights)) =
|
||||
self.gutter_highlights.remove(&TypeId::of::<T>())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let mut ranges_to_remove = ranges_to_remove.iter().peekable();
|
||||
gutter_highlights.retain(|highlight| {
|
||||
while let Some(range_to_remove) = ranges_to_remove.peek() {
|
||||
match range_to_remove.end.cmp(&highlight.start, &snapshot) {
|
||||
Ordering::Less | Ordering::Equal => {
|
||||
ranges_to_remove.next();
|
||||
}
|
||||
Ordering::Greater => {
|
||||
match range_to_remove.start.cmp(&highlight.end, &snapshot) {
|
||||
Ordering::Less | Ordering::Equal => {
|
||||
return false;
|
||||
}
|
||||
Ordering::Greater => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
self.gutter_highlights
|
||||
.insert(TypeId::of::<T>(), (color_fetcher, gutter_highlights));
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn all_text_background_highlights(
|
||||
&self,
|
||||
@@ -20018,15 +19939,12 @@ impl Editor {
|
||||
if !selections.is_empty() {
|
||||
let snapshot =
|
||||
buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx));
|
||||
// skip adding the initial selection to selection history
|
||||
self.selection_history.mode = SelectionHistoryMode::Skipping;
|
||||
self.change_selections(None, window, cx, |s| {
|
||||
s.select_ranges(selections.into_iter().map(|(start, end)| {
|
||||
snapshot.clip_offset(start, Bias::Left)
|
||||
..snapshot.clip_offset(end, Bias::Right)
|
||||
}));
|
||||
});
|
||||
self.selection_history.mode = SelectionHistoryMode::Normal;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1907,6 +1907,7 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
|
||||
DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
|
||||
])
|
||||
});
|
||||
|
||||
editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
|
||||
assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
|
||||
|
||||
@@ -1926,29 +1927,29 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
|
||||
assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
|
||||
|
||||
editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
|
||||
assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
|
||||
assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
|
||||
|
||||
editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
|
||||
assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
|
||||
assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
|
||||
|
||||
editor.move_right(&MoveRight, window, cx);
|
||||
editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
|
||||
assert_selection_ranges(
|
||||
"use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
|
||||
"use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
|
||||
editor,
|
||||
cx,
|
||||
);
|
||||
|
||||
editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
|
||||
assert_selection_ranges(
|
||||
"use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
|
||||
"use std«ˇ::s»tr::{foo, bar}\n\n«ˇ {b»az.qux()}",
|
||||
editor,
|
||||
cx,
|
||||
);
|
||||
|
||||
editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
|
||||
assert_selection_ranges(
|
||||
"use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
|
||||
"use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
|
||||
editor,
|
||||
cx,
|
||||
);
|
||||
@@ -21941,7 +21942,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
|
||||
.expect("created a singleton buffer")
|
||||
.read(cx)
|
||||
.remote_id();
|
||||
let buffer_result_id = project.lsp_store().read(cx).result_id(buffer_id, cx);
|
||||
let buffer_result_id = project.lsp_store().read(cx).result_id(buffer_id);
|
||||
assert_eq!(expected, buffer_result_id);
|
||||
});
|
||||
};
|
||||
@@ -21987,6 +21988,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
|
||||
"Cursor movement should not trigger diagnostic request"
|
||||
);
|
||||
ensure_result_id(Some("2".to_string()), cx);
|
||||
|
||||
// Multiple rapid edits should be debounced
|
||||
for _ in 0..5 {
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
|
||||
@@ -1031,7 +1031,7 @@ impl EditorElement {
|
||||
editor.set_gutter_hovered(gutter_hovered, cx);
|
||||
editor.mouse_cursor_hidden = false;
|
||||
|
||||
let breakpoint_indicator = if gutter_hovered {
|
||||
if gutter_hovered {
|
||||
let new_point = position_map
|
||||
.point_for_position(event.position)
|
||||
.previous_valid;
|
||||
@@ -1045,6 +1045,7 @@ impl EditorElement {
|
||||
.buffer_for_excerpt(buffer_anchor.excerpt_id)
|
||||
.and_then(|buffer| buffer.file().map(|file| (buffer, file)))
|
||||
{
|
||||
let was_hovered = editor.gutter_breakpoint_indicator.0.is_some();
|
||||
let as_point = text::ToPoint::to_point(&buffer_anchor.text_anchor, buffer_snapshot);
|
||||
|
||||
let is_visible = editor
|
||||
@@ -1072,44 +1073,39 @@ impl EditorElement {
|
||||
.is_some()
|
||||
});
|
||||
|
||||
if !is_visible {
|
||||
editor.gutter_breakpoint_indicator.1.get_or_insert_with(|| {
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(200))
|
||||
.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(indicator) = this.gutter_breakpoint_indicator.0.as_mut()
|
||||
{
|
||||
indicator.is_active = true;
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
Some(PhantomBreakpointIndicator {
|
||||
editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
|
||||
display_row: new_point.row(),
|
||||
is_active: is_visible,
|
||||
collides_with_existing_breakpoint: has_existing_breakpoint,
|
||||
})
|
||||
});
|
||||
|
||||
editor.gutter_breakpoint_indicator.1.get_or_insert_with(|| {
|
||||
cx.spawn(async move |this, cx| {
|
||||
if !was_hovered {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(200))
|
||||
.await;
|
||||
}
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(indicator) = this.gutter_breakpoint_indicator.0.as_mut() {
|
||||
indicator.is_active = true;
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
});
|
||||
} else {
|
||||
editor.gutter_breakpoint_indicator.1 = None;
|
||||
None
|
||||
editor.gutter_breakpoint_indicator = (None, None);
|
||||
}
|
||||
} else {
|
||||
editor.gutter_breakpoint_indicator.1 = None;
|
||||
None
|
||||
};
|
||||
|
||||
if &breakpoint_indicator != &editor.gutter_breakpoint_indicator.0 {
|
||||
editor.gutter_breakpoint_indicator.0 = breakpoint_indicator;
|
||||
cx.notify();
|
||||
editor.gutter_breakpoint_indicator = (None, None);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
|
||||
// Don't trigger hover popover if mouse is hovering over context menu
|
||||
if text_hitbox.is_hovered(window) {
|
||||
let point_for_position = position_map.point_for_position(event.position);
|
||||
@@ -7329,17 +7325,6 @@ impl LineWithInvisibles {
|
||||
paint(window, cx);
|
||||
}),
|
||||
|
||||
ShowWhitespaceSetting::Trailing => {
|
||||
let mut previous_start = self.len;
|
||||
for ([start, end], paint) in invisible_iter.rev() {
|
||||
if previous_start != end {
|
||||
break;
|
||||
}
|
||||
previous_start = start;
|
||||
paint(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
// For a whitespace to be on a boundary, any of the following conditions need to be met:
|
||||
// - It is a tab
|
||||
// - It is adjacent to an edge (start or end)
|
||||
|
||||
@@ -266,11 +266,10 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa
|
||||
|
||||
let mut is_first_iteration = true;
|
||||
find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
|
||||
// Make alt-left skip punctuation to respect VSCode behaviour. For example: hello.| goes to |hello.
|
||||
// Make alt-left skip punctuation on Mac OS to respect Mac VSCode behaviour. For example: hello.| goes to |hello.
|
||||
if is_first_iteration
|
||||
&& classifier.is_punctuation(right)
|
||||
&& !classifier.is_punctuation(left)
|
||||
&& left != '\n'
|
||||
{
|
||||
is_first_iteration = false;
|
||||
return false;
|
||||
@@ -319,11 +318,10 @@ pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint
|
||||
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
|
||||
let mut is_first_iteration = true;
|
||||
find_boundary(map, point, FindRange::MultiLine, |left, right| {
|
||||
// Make alt-right skip punctuation to respect VSCode behaviour. For example: |.hello goes to .hello|
|
||||
// Make alt-right skip punctuation on Mac OS to respect the Mac behaviour. For example: |.hello goes to .hello|
|
||||
if is_first_iteration
|
||||
&& classifier.is_punctuation(left)
|
||||
&& !classifier.is_punctuation(right)
|
||||
&& right != '\n'
|
||||
{
|
||||
is_first_iteration = false;
|
||||
return false;
|
||||
|
||||
@@ -240,8 +240,7 @@ impl EditorTestContext {
|
||||
// unlike cx.simulate_keystrokes(), this does not run_until_parked
|
||||
// so you can use it to test detailed timing
|
||||
pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
|
||||
let keyboard_mapper = self.keyboard_mapper();
|
||||
let keystroke = Keystroke::parse(keystroke_text, keyboard_mapper.as_ref()).unwrap();
|
||||
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
||||
self.cx.dispatch_keystroke(self.window, keystroke);
|
||||
}
|
||||
|
||||
|
||||
@@ -248,8 +248,6 @@ fn conflicts_updated(
|
||||
removed_block_ids.insert(block_id);
|
||||
}
|
||||
|
||||
editor.remove_gutter_highlights::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
|
||||
|
||||
editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
|
||||
editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx);
|
||||
editor
|
||||
@@ -327,7 +325,8 @@ fn update_conflict_highlighting(
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
log::debug!("update conflict highlighting for {conflict:?}");
|
||||
|
||||
let theme = cx.theme().clone();
|
||||
let colors = theme.colors();
|
||||
let outer_start = buffer
|
||||
.anchor_in_excerpt(excerpt_id, conflict.range.start)
|
||||
.unwrap();
|
||||
@@ -347,29 +346,26 @@ fn update_conflict_highlighting(
|
||||
.anchor_in_excerpt(excerpt_id, conflict.theirs.end)
|
||||
.unwrap();
|
||||
|
||||
let ours_background = cx.theme().colors().version_control_conflict_marker_ours;
|
||||
let theirs_background = cx.theme().colors().version_control_conflict_marker_theirs;
|
||||
let ours_background = colors.version_control_conflict_ours_background;
|
||||
let ours_marker = colors.version_control_conflict_ours_marker_background;
|
||||
let theirs_background = colors.version_control_conflict_theirs_background;
|
||||
let theirs_marker = colors.version_control_conflict_theirs_marker_background;
|
||||
let divider_background = colors.version_control_conflict_divider_background;
|
||||
|
||||
let options = RowHighlightOptions {
|
||||
include_gutter: true,
|
||||
include_gutter: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
editor.insert_gutter_highlight::<ConflictsOuter>(
|
||||
outer_start..their_end,
|
||||
|cx| cx.theme().colors().editor_background,
|
||||
cx,
|
||||
);
|
||||
|
||||
// Prevent diff hunk highlighting within the entire conflict region.
|
||||
editor.highlight_rows::<ConflictsOuter>(outer_start..outer_end, theirs_background, options, cx);
|
||||
editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
|
||||
editor.highlight_rows::<ConflictsOursMarker>(
|
||||
outer_start..our_start,
|
||||
ours_background,
|
||||
editor.highlight_rows::<ConflictsOuter>(
|
||||
outer_start..outer_end,
|
||||
divider_background,
|
||||
options,
|
||||
cx,
|
||||
);
|
||||
editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
|
||||
editor.highlight_rows::<ConflictsOursMarker>(outer_start..our_start, ours_marker, options, cx);
|
||||
editor.highlight_rows::<ConflictsTheirs>(
|
||||
their_start..their_end,
|
||||
theirs_background,
|
||||
@@ -378,7 +374,7 @@ fn update_conflict_highlighting(
|
||||
);
|
||||
editor.highlight_rows::<ConflictsTheirsMarker>(
|
||||
their_end..outer_end,
|
||||
theirs_background,
|
||||
theirs_marker,
|
||||
options,
|
||||
cx,
|
||||
);
|
||||
@@ -516,9 +512,6 @@ pub(crate) fn resolve_conflict(
|
||||
let end = snapshot
|
||||
.anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
|
||||
.unwrap();
|
||||
|
||||
editor.remove_gutter_highlights::<ConflictsOuter>(vec![start..end], cx);
|
||||
|
||||
editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
|
||||
editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
|
||||
editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
|
||||
|
||||
@@ -37,10 +37,10 @@ use crate::{
|
||||
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
|
||||
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
|
||||
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
|
||||
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder,
|
||||
PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle,
|
||||
Reservation, ScreenCaptureSource, SharedString, SubscriberSet, Subscription, SvgRenderer, Task,
|
||||
TextSystem, Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
|
||||
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
|
||||
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
|
||||
SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window,
|
||||
WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
|
||||
colors::{Colors, GlobalColors},
|
||||
current_platform, hash, init_app_menus,
|
||||
};
|
||||
@@ -262,7 +262,6 @@ pub struct App {
|
||||
pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
|
||||
pub(crate) focus_handles: Arc<FocusMap>,
|
||||
pub(crate) keymap: Rc<RefCell<Keymap>>,
|
||||
pub(crate) keyboard_mapper: Box<dyn PlatformKeyboardMapper>,
|
||||
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
|
||||
pub(crate) global_action_listeners:
|
||||
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
|
||||
@@ -309,7 +308,6 @@ impl App {
|
||||
|
||||
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
||||
let entities = EntityMap::new();
|
||||
let keyboard_mapper = platform.keyboard_mapper();
|
||||
let keyboard_layout = platform.keyboard_layout();
|
||||
|
||||
let app = Rc::new_cyclic(|this| AppCell {
|
||||
@@ -335,7 +333,6 @@ impl App {
|
||||
window_handles: FxHashMap::default(),
|
||||
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
||||
keymap: Rc::new(RefCell::new(Keymap::default())),
|
||||
keyboard_mapper,
|
||||
keyboard_layout,
|
||||
global_action_listeners: FxHashMap::default(),
|
||||
pending_effects: VecDeque::new(),
|
||||
@@ -372,7 +369,6 @@ impl App {
|
||||
move || {
|
||||
if let Some(app) = app.upgrade() {
|
||||
let cx = &mut app.borrow_mut();
|
||||
cx.keyboard_mapper = cx.platform.keyboard_mapper();
|
||||
cx.keyboard_layout = cx.platform.keyboard_layout();
|
||||
cx.keyboard_layout_observers
|
||||
.clone()
|
||||
@@ -417,11 +413,6 @@ impl App {
|
||||
self.quitting = false;
|
||||
}
|
||||
|
||||
/// Get the keyboard mapper of current keyboard layout
|
||||
pub fn keyboard_mapper(&self) -> &dyn PlatformKeyboardMapper {
|
||||
self.keyboard_mapper.as_ref()
|
||||
}
|
||||
|
||||
/// Get the id of the current keyboard layout
|
||||
pub fn keyboard_layout(&self) -> &dyn PlatformKeyboardLayout {
|
||||
self.keyboard_layout.as_ref()
|
||||
|
||||
@@ -3,9 +3,9 @@ use crate::{
|
||||
BackgroundExecutor, BorrowAppContext, Bounds, ClipboardItem, DrawPhase, Drawable, Element,
|
||||
Empty, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
|
||||
Platform, PlatformKeyboardMapper, Point, Render, Result, Size, Task, TestDispatcher,
|
||||
TestPlatform, TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window,
|
||||
WindowBounds, WindowHandle, WindowOptions,
|
||||
Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform,
|
||||
TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, WindowBounds,
|
||||
WindowHandle, WindowOptions,
|
||||
};
|
||||
use anyhow::{anyhow, bail};
|
||||
use futures::{Stream, StreamExt, channel::oneshot};
|
||||
@@ -397,20 +397,14 @@ impl TestAppContext {
|
||||
self.background_executor.run_until_parked()
|
||||
}
|
||||
|
||||
/// Returns the current keyboard mapper for this platform.
|
||||
pub fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper> {
|
||||
self.test_platform.keyboard_mapper()
|
||||
}
|
||||
|
||||
/// simulate_keystrokes takes a space-separated list of keys to type.
|
||||
/// cx.simulate_keystrokes("cmd-shift-p b k s p enter")
|
||||
/// in Zed, this will run backspace on the current editor through the command palette.
|
||||
/// This will also run the background executor until it's parked.
|
||||
pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
|
||||
let keyboard_mapper = self.keyboard_mapper();
|
||||
for keystroke in keystrokes
|
||||
.split(' ')
|
||||
.map(|source| Keystroke::parse(source, keyboard_mapper.as_ref()))
|
||||
.map(Keystroke::parse)
|
||||
.map(Result::unwrap)
|
||||
{
|
||||
self.dispatch_keystroke(window, keystroke);
|
||||
@@ -424,12 +418,7 @@ impl TestAppContext {
|
||||
/// will type abc into your current editor
|
||||
/// This will also run the background executor until it's parked.
|
||||
pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
|
||||
let keyboard_mapper = self.keyboard_mapper();
|
||||
for keystroke in input
|
||||
.split("")
|
||||
.map(|source| Keystroke::parse(source, keyboard_mapper.as_ref()))
|
||||
.map(Result::unwrap)
|
||||
{
|
||||
for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) {
|
||||
self.dispatch_keystroke(window, keystroke);
|
||||
}
|
||||
|
||||
|
||||
@@ -538,22 +538,8 @@ mod test {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
cx.dispatch_keystroke(
|
||||
*window,
|
||||
Keystroke {
|
||||
modifiers: crate::Modifiers::none(),
|
||||
key: "a".to_owned(),
|
||||
key_char: None,
|
||||
},
|
||||
);
|
||||
cx.dispatch_keystroke(
|
||||
*window,
|
||||
Keystroke {
|
||||
modifiers: crate::Modifiers::control(),
|
||||
key: "g".to_owned(),
|
||||
key_char: None,
|
||||
},
|
||||
);
|
||||
cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap());
|
||||
cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap());
|
||||
|
||||
window
|
||||
.update(cx, |test_view, _, _| {
|
||||
|
||||
@@ -310,11 +310,7 @@ mod tests {
|
||||
assert!(
|
||||
keymap
|
||||
.bindings_for_input(
|
||||
&[Keystroke {
|
||||
modifiers: crate::Modifiers::control(),
|
||||
key: "a".to_owned(),
|
||||
key_char: None
|
||||
}],
|
||||
&[Keystroke::parse("ctrl-a").unwrap()],
|
||||
&[KeyContext::parse("barf").unwrap()],
|
||||
)
|
||||
.0
|
||||
@@ -323,11 +319,7 @@ mod tests {
|
||||
assert!(
|
||||
!keymap
|
||||
.bindings_for_input(
|
||||
&[Keystroke {
|
||||
modifiers: crate::Modifiers::control(),
|
||||
key: "a".to_owned(),
|
||||
key_char: None
|
||||
}],
|
||||
&[Keystroke::parse("ctrl-a").unwrap()],
|
||||
&[KeyContext::parse("editor").unwrap()],
|
||||
)
|
||||
.0
|
||||
@@ -338,11 +330,7 @@ mod tests {
|
||||
assert!(
|
||||
keymap
|
||||
.bindings_for_input(
|
||||
&[Keystroke {
|
||||
modifiers: crate::Modifiers::control(),
|
||||
key: "a".to_owned(),
|
||||
key_char: None
|
||||
}],
|
||||
&[Keystroke::parse("ctrl-a").unwrap()],
|
||||
&[KeyContext::parse("editor mode=full").unwrap()],
|
||||
)
|
||||
.0
|
||||
@@ -353,11 +341,7 @@ mod tests {
|
||||
assert!(
|
||||
keymap
|
||||
.bindings_for_input(
|
||||
&[Keystroke {
|
||||
modifiers: crate::Modifiers::control(),
|
||||
key: "b".to_owned(),
|
||||
key_char: None
|
||||
}],
|
||||
&[Keystroke::parse("ctrl-b").unwrap()],
|
||||
&[KeyContext::parse("barf").unwrap()],
|
||||
)
|
||||
.0
|
||||
@@ -376,16 +360,8 @@ mod tests {
|
||||
let mut keymap = Keymap::default();
|
||||
keymap.add_bindings(bindings.clone());
|
||||
|
||||
let space = || Keystroke {
|
||||
modifiers: crate::Modifiers::none(),
|
||||
key: "space".to_owned(),
|
||||
key_char: None,
|
||||
};
|
||||
let w = || Keystroke {
|
||||
modifiers: crate::Modifiers::none(),
|
||||
key: "w".to_owned(),
|
||||
key_char: None,
|
||||
};
|
||||
let space = || Keystroke::parse("space").unwrap();
|
||||
let w = || Keystroke::parse("w").unwrap();
|
||||
|
||||
let space_w = [space(), w()];
|
||||
let space_w_w = [space(), w(), w()];
|
||||
|
||||
@@ -2,10 +2,7 @@ use std::rc::Rc;
|
||||
|
||||
use collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, PlatformKeyboardMapper,
|
||||
TestKeyboardMapper,
|
||||
};
|
||||
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// A keybinding and its associated metadata, from the keymap.
|
||||
@@ -28,20 +25,12 @@ impl Clone for KeyBinding {
|
||||
impl KeyBinding {
|
||||
/// Construct a new keybinding from the given data. Panics on parse error.
|
||||
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
|
||||
let keyboard_mapper = TestKeyboardMapper::new();
|
||||
let context_predicate = if let Some(context) = context {
|
||||
Some(KeyBindingContextPredicate::parse(context).unwrap().into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self::load(
|
||||
keystrokes,
|
||||
Box::new(action),
|
||||
context_predicate,
|
||||
None,
|
||||
&keyboard_mapper,
|
||||
)
|
||||
.unwrap()
|
||||
Self::load(keystrokes, Box::new(action), context_predicate, None).unwrap()
|
||||
}
|
||||
|
||||
/// Load a keybinding from the given raw data.
|
||||
@@ -50,11 +39,10 @@ impl KeyBinding {
|
||||
action: Box<dyn Action>,
|
||||
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||
key_equivalents: Option<&HashMap<char, char>>,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) -> std::result::Result<Self, InvalidKeystrokeError> {
|
||||
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
|
||||
.split_whitespace()
|
||||
.map(|source| Keystroke::parse(source, keyboard_mapper))
|
||||
.map(Keystroke::parse)
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
|
||||
if let Some(equivalents) = key_equivalents {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
mod app_menu;
|
||||
mod keyboard;
|
||||
mod keycode;
|
||||
mod keystroke;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
@@ -67,7 +66,6 @@ use uuid::Uuid;
|
||||
|
||||
pub use app_menu::*;
|
||||
pub use keyboard::*;
|
||||
pub use keycode::*;
|
||||
pub use keystroke::*;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
@@ -196,6 +194,7 @@ pub(crate) trait Platform: 'static {
|
||||
|
||||
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
|
||||
|
||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
||||
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||
@@ -215,6 +214,7 @@ pub(crate) trait Platform: 'static {
|
||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
|
||||
|
||||
fn compositor_name(&self) -> &'static str {
|
||||
""
|
||||
@@ -235,10 +235,6 @@ pub(crate) trait Platform: 'static {
|
||||
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
|
||||
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
|
||||
fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
|
||||
|
||||
fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper>;
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
|
||||
}
|
||||
|
||||
/// A handle to a platform's display, e.g. a monitor or laptop screen.
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{Modifiers, ScanCode};
|
||||
|
||||
/// A trait for platform-specific keyboard layouts
|
||||
pub trait PlatformKeyboardLayout {
|
||||
/// Get the keyboard layout ID, which should be unique to the layout
|
||||
@@ -9,109 +5,3 @@ pub trait PlatformKeyboardLayout {
|
||||
/// Get the keyboard layout display name
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
|
||||
/// TODO:
|
||||
pub trait PlatformKeyboardMapper {
|
||||
/// TODO:
|
||||
fn scan_code_to_key(&self, scan_code: ScanCode, modifiers: &mut Modifiers) -> Result<String>;
|
||||
}
|
||||
|
||||
/// TODO:
|
||||
pub struct TestKeyboardMapper {
|
||||
#[cfg(target_os = "windows")]
|
||||
mapper: super::WindowsKeyboardMapper,
|
||||
#[cfg(target_os = "macos")]
|
||||
mapper: super::MacKeyboardMapper,
|
||||
#[cfg(target_os = "linux")]
|
||||
mapper: super::LinuxKeyboardMapper,
|
||||
}
|
||||
|
||||
impl PlatformKeyboardMapper for TestKeyboardMapper {
|
||||
fn scan_code_to_key(&self, scan_code: ScanCode, modifiers: &mut Modifiers) -> Result<String> {
|
||||
self.mapper.scan_code_to_key(scan_code, modifiers)
|
||||
}
|
||||
}
|
||||
|
||||
impl TestKeyboardMapper {
|
||||
/// TODO:
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
#[cfg(target_os = "windows")]
|
||||
mapper: super::WindowsKeyboardMapper::new(),
|
||||
#[cfg(target_os = "macos")]
|
||||
mapper: super::MacKeyboardMapper::new(),
|
||||
#[cfg(target_os = "linux")]
|
||||
mapper: super::LinuxKeyboardMapper::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A dummy keyboard mapper that does not support any key mappings
|
||||
pub struct EmptyKeyboardMapper;
|
||||
|
||||
impl PlatformKeyboardMapper for EmptyKeyboardMapper {
|
||||
fn scan_code_to_key(&self, _scan_code: ScanCode, _modifiers: &mut Modifiers) -> Result<String> {
|
||||
anyhow::bail!("EmptyKeyboardMapper does not support scan codes")
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn is_letter_key(key: &str) -> bool {
|
||||
matches!(
|
||||
key,
|
||||
"a" | "b"
|
||||
| "c"
|
||||
| "d"
|
||||
| "e"
|
||||
| "f"
|
||||
| "g"
|
||||
| "h"
|
||||
| "i"
|
||||
| "j"
|
||||
| "k"
|
||||
| "l"
|
||||
| "m"
|
||||
| "n"
|
||||
| "o"
|
||||
| "p"
|
||||
| "q"
|
||||
| "r"
|
||||
| "s"
|
||||
| "t"
|
||||
| "u"
|
||||
| "v"
|
||||
| "w"
|
||||
| "x"
|
||||
| "y"
|
||||
| "z"
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::{Modifiers, ScanCode};
|
||||
|
||||
use super::{PlatformKeyboardMapper, TestKeyboardMapper};
|
||||
|
||||
#[test]
|
||||
fn test_scan_code_to_key() {
|
||||
let mapper = TestKeyboardMapper::new();
|
||||
for scan_code in ScanCode::iter() {
|
||||
let mut modifiers = Modifiers::default();
|
||||
let key = mapper.scan_code_to_key(scan_code, &mut modifiers).unwrap();
|
||||
assert_eq!(key, scan_code.to_key(false));
|
||||
assert_eq!(modifiers, Modifiers::default());
|
||||
|
||||
let mut modifiers = Modifiers::shift();
|
||||
let shifted_key = mapper.scan_code_to_key(scan_code, &mut modifiers).unwrap();
|
||||
assert_eq!(shifted_key, scan_code.to_key(true));
|
||||
if shifted_key != key {
|
||||
assert_eq!(modifiers, Modifiers::default());
|
||||
} else {
|
||||
assert_eq!(modifiers, Modifiers::shift());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,590 +0,0 @@
|
||||
use strum::EnumIter;
|
||||
|
||||
/// Scan codes for the keyboard, which are used to identify keys in a keyboard layout-independent way.
|
||||
/// Currently, we only support a limited set of scan codes here:
|
||||
/// https://code.visualstudio.com/docs/configure/keybindings#_keyboard-layoutindependent-bindings
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
|
||||
pub enum ScanCode {
|
||||
/// F1 key
|
||||
F1,
|
||||
/// F1 key
|
||||
F2,
|
||||
/// F1 key
|
||||
F3,
|
||||
/// F1 key
|
||||
F4,
|
||||
/// F1 key
|
||||
F5,
|
||||
/// F1 key
|
||||
F6,
|
||||
/// F1 key
|
||||
F7,
|
||||
/// F1 key
|
||||
F8,
|
||||
/// F1 key
|
||||
F9,
|
||||
/// F1 key
|
||||
F10,
|
||||
/// F1 key
|
||||
F11,
|
||||
/// F1 key
|
||||
F12,
|
||||
/// F1 key
|
||||
F13,
|
||||
/// F1 key
|
||||
F14,
|
||||
/// F1 key
|
||||
F15,
|
||||
/// F1 key
|
||||
F16,
|
||||
/// F1 key
|
||||
F17,
|
||||
/// F1 key
|
||||
F18,
|
||||
/// F1 key
|
||||
F19,
|
||||
/// F20 key
|
||||
F20,
|
||||
/// F20 key
|
||||
F21,
|
||||
/// F20 key
|
||||
F22,
|
||||
/// F20 key
|
||||
F23,
|
||||
/// F20 key
|
||||
F24,
|
||||
/// A key on the main keyboard.
|
||||
A,
|
||||
/// B key on the main keyboard.
|
||||
B,
|
||||
/// C key on the main keyboard.
|
||||
C,
|
||||
/// D key on the main keyboard.
|
||||
D,
|
||||
/// E key on the main keyboard.
|
||||
E,
|
||||
/// F key on the main keyboard.
|
||||
F,
|
||||
/// G key on the main keyboard.
|
||||
G,
|
||||
/// H key on the main keyboard.
|
||||
H,
|
||||
/// I key on the main keyboard.
|
||||
I,
|
||||
/// J key on the main keyboard.
|
||||
J,
|
||||
/// K key on the main keyboard.
|
||||
K,
|
||||
/// L key on the main keyboard.
|
||||
L,
|
||||
/// M key on the main keyboard.
|
||||
M,
|
||||
/// N key on the main keyboard.
|
||||
N,
|
||||
/// O key on the main keyboard.
|
||||
O,
|
||||
/// P key on the main keyboard.
|
||||
P,
|
||||
/// Q key on the main keyboard.
|
||||
Q,
|
||||
/// R key on the main keyboard.
|
||||
R,
|
||||
/// S key on the main keyboard.
|
||||
S,
|
||||
/// T key on the main keyboard.
|
||||
T,
|
||||
/// U key on the main keyboard.
|
||||
U,
|
||||
/// V key on the main keyboard.
|
||||
V,
|
||||
/// W key on the main keyboard.
|
||||
W,
|
||||
/// X key on the main keyboard.
|
||||
X,
|
||||
/// Y key on the main keyboard.
|
||||
Y,
|
||||
/// Z key on the main keyboard.
|
||||
Z,
|
||||
/// 0 key on the main keyboard.
|
||||
Digit0,
|
||||
/// 1 key on the main keyboard.
|
||||
Digit1,
|
||||
/// 2 key on the main keyboard.
|
||||
Digit2,
|
||||
/// 3 key on the main keyboard.
|
||||
Digit3,
|
||||
/// 4 key on the main keyboard.
|
||||
Digit4,
|
||||
/// 5 key on the main keyboard.
|
||||
Digit5,
|
||||
/// 6 key on the main keyboard.
|
||||
Digit6,
|
||||
/// 7 key on the main keyboard.
|
||||
Digit7,
|
||||
/// 8 key on the main keyboard.
|
||||
Digit8,
|
||||
/// 9 key on the main keyboard.
|
||||
Digit9,
|
||||
|
||||
/// Backquote key on the main keyboard: `
|
||||
Backquote,
|
||||
/// Minus key on the main keyboard: -
|
||||
Minus,
|
||||
/// Equal key on the main keyboard: =
|
||||
Equal,
|
||||
/// BracketLeft key on the main keyboard: [
|
||||
BracketLeft,
|
||||
/// BracketRight key on the main keyboard: ]
|
||||
BracketRight,
|
||||
/// Backslash key on the main keyboard: \
|
||||
Backslash,
|
||||
/// Semicolon key on the main keyboard: ;
|
||||
Semicolon,
|
||||
/// Quote key on the main keyboard: '
|
||||
Quote,
|
||||
/// Comma key on the main keyboard: ,
|
||||
Comma,
|
||||
/// Period key on the main keyboard: .
|
||||
Period,
|
||||
/// Slash key on the main keyboard: /
|
||||
Slash,
|
||||
|
||||
/// Left arrow key
|
||||
Left,
|
||||
/// Up arrow key
|
||||
Up,
|
||||
/// Right arrow key
|
||||
Right,
|
||||
/// Down arrow key
|
||||
Down,
|
||||
/// PAGE UP key
|
||||
PageUp,
|
||||
/// PAGE DOWN key
|
||||
PageDown,
|
||||
/// END key
|
||||
End,
|
||||
/// HOME key
|
||||
Home,
|
||||
|
||||
/// TAB key
|
||||
Tab,
|
||||
/// ENTER key, also known as RETURN key
|
||||
/// This does not distinguish between the main Enter key and the numeric keypad Enter key.
|
||||
Enter,
|
||||
/// ESCAPE key
|
||||
Escape,
|
||||
/// SPACE key
|
||||
Space,
|
||||
/// BACKSPACE key
|
||||
Backspace,
|
||||
/// DELETE key
|
||||
Delete,
|
||||
|
||||
// Pause, not supported yet
|
||||
// CapsLock, not supported yet
|
||||
/// INSERT key
|
||||
Insert,
|
||||
// The following keys are not supported yet:
|
||||
// Numpad0,
|
||||
// Numpad1,
|
||||
// Numpad2,
|
||||
// Numpad3,
|
||||
// Numpad4,
|
||||
// Numpad5,
|
||||
// Numpad6,
|
||||
// Numpad7,
|
||||
// Numpad8,
|
||||
// Numpad9,
|
||||
// NumpadMultiply,
|
||||
// NumpadAdd,
|
||||
// NumpadComma,
|
||||
// NumpadSubtract,
|
||||
// NumpadDecimal,
|
||||
// NumpadDivide,
|
||||
}
|
||||
|
||||
impl ScanCode {
|
||||
/// Parse a scan code from a string.
|
||||
pub fn parse(source: &str) -> Option<Self> {
|
||||
match source {
|
||||
"[f1]" => Some(Self::F1),
|
||||
"[f2]" => Some(Self::F2),
|
||||
"[f3]" => Some(Self::F3),
|
||||
"[f4]" => Some(Self::F4),
|
||||
"[f5]" => Some(Self::F5),
|
||||
"[f6]" => Some(Self::F6),
|
||||
"[f7]" => Some(Self::F7),
|
||||
"[f8]" => Some(Self::F8),
|
||||
"[f9]" => Some(Self::F9),
|
||||
"[f10]" => Some(Self::F10),
|
||||
"[f11]" => Some(Self::F11),
|
||||
"[f12]" => Some(Self::F12),
|
||||
"[f13]" => Some(Self::F13),
|
||||
"[f14]" => Some(Self::F14),
|
||||
"[f15]" => Some(Self::F15),
|
||||
"[f16]" => Some(Self::F16),
|
||||
"[f17]" => Some(Self::F17),
|
||||
"[f18]" => Some(Self::F18),
|
||||
"[f19]" => Some(Self::F19),
|
||||
"[f20]" => Some(Self::F20),
|
||||
"[f21]" => Some(Self::F21),
|
||||
"[f22]" => Some(Self::F22),
|
||||
"[f23]" => Some(Self::F23),
|
||||
"[f24]" => Some(Self::F24),
|
||||
"[a]" | "[keya]" => Some(Self::A),
|
||||
"[b]" | "[keyb]" => Some(Self::B),
|
||||
"[c]" | "[keyc]" => Some(Self::C),
|
||||
"[d]" | "[keyd]" => Some(Self::D),
|
||||
"[e]" | "[keye]" => Some(Self::E),
|
||||
"[f]" | "[keyf]" => Some(Self::F),
|
||||
"[g]" | "[keyg]" => Some(Self::G),
|
||||
"[h]" | "[keyh]" => Some(Self::H),
|
||||
"[i]" | "[keyi]" => Some(Self::I),
|
||||
"[j]" | "[keyj]" => Some(Self::J),
|
||||
"[k]" | "[keyk]" => Some(Self::K),
|
||||
"[l]" | "[keyl]" => Some(Self::L),
|
||||
"[m]" | "[keym]" => Some(Self::M),
|
||||
"[n]" | "[keyn]" => Some(Self::N),
|
||||
"[o]" | "[keyo]" => Some(Self::O),
|
||||
"[p]" | "[keyp]" => Some(Self::P),
|
||||
"[q]" | "[keyq]" => Some(Self::Q),
|
||||
"[r]" | "[keyr]" => Some(Self::R),
|
||||
"[s]" | "[keys]" => Some(Self::S),
|
||||
"[t]" | "[keyt]" => Some(Self::T),
|
||||
"[u]" | "[keyu]" => Some(Self::U),
|
||||
"[v]" | "[keyv]" => Some(Self::V),
|
||||
"[w]" | "[keyw]" => Some(Self::W),
|
||||
"[x]" | "[keyx]" => Some(Self::X),
|
||||
"[y]" | "[keyy]" => Some(Self::Y),
|
||||
"[z]" | "[keyz]" => Some(Self::Z),
|
||||
"[0]" | "[digit0]" => Some(Self::Digit0),
|
||||
"[1]" | "[digit1]" => Some(Self::Digit1),
|
||||
"[2]" | "[digit2]" => Some(Self::Digit2),
|
||||
"[3]" | "[digit3]" => Some(Self::Digit3),
|
||||
"[4]" | "[digit4]" => Some(Self::Digit4),
|
||||
"[5]" | "[digit5]" => Some(Self::Digit5),
|
||||
"[6]" | "[digit6]" => Some(Self::Digit6),
|
||||
"[7]" | "[digit7]" => Some(Self::Digit7),
|
||||
"[8]" | "[digit8]" => Some(Self::Digit8),
|
||||
"[9]" | "[digit9]" => Some(Self::Digit9),
|
||||
|
||||
"[backquote]" => Some(Self::Backquote),
|
||||
"[minus]" => Some(Self::Minus),
|
||||
"[equal]" => Some(Self::Equal),
|
||||
"[bracketleft]" => Some(Self::BracketLeft),
|
||||
"[bracketright]" => Some(Self::BracketRight),
|
||||
"[backslash]" => Some(Self::Backslash),
|
||||
"[semicolon]" => Some(Self::Semicolon),
|
||||
"[quote]" => Some(Self::Quote),
|
||||
"[comma]" => Some(Self::Comma),
|
||||
"[period]" => Some(Self::Period),
|
||||
"[slash]" => Some(Self::Slash),
|
||||
|
||||
"[left]" | "[arrowleft]" => Some(Self::Left),
|
||||
"[up]" | "[arrowup]" => Some(Self::Up),
|
||||
"[right]" | "[arrowright]" => Some(Self::Right),
|
||||
"[down]" | "[arrowdown]" => Some(Self::Down),
|
||||
"[pageup]" => Some(Self::PageUp),
|
||||
"[pagedown]" => Some(Self::PageDown),
|
||||
"[end]" => Some(Self::End),
|
||||
"[home]" => Some(Self::Home),
|
||||
|
||||
"[tab]" => Some(Self::Tab),
|
||||
"[enter]" => Some(Self::Enter),
|
||||
"[escape]" => Some(Self::Escape),
|
||||
"[space]" => Some(Self::Space),
|
||||
"[backspace]" => Some(Self::Backspace),
|
||||
"[delete]" => Some(Self::Delete),
|
||||
|
||||
// "[pause]" => Some(Self::Pause),
|
||||
// "[capslock]" => Some(Self::CapsLock),
|
||||
"[insert]" => Some(Self::Insert),
|
||||
|
||||
// "[numpad0]" => Some(Self::Numpad0),
|
||||
// "[numpad1]" => Some(Self::Numpad1),
|
||||
// "[numpad2]" => Some(Self::Numpad2),
|
||||
// "[numpad3]" => Some(Self::Numpad3),
|
||||
// "[numpad4]" => Some(Self::Numpad4),
|
||||
// "[numpad5]" => Some(Self::Numpad5),
|
||||
// "[numpad6]" => Some(Self::Numpad6),
|
||||
// "[numpad7]" => Some(Self::Numpad7),
|
||||
// "[numpad8]" => Some(Self::Numpad8),
|
||||
// "[numpad9]" => Some(Self::Numpad9),
|
||||
// "[numpadmultiply]" => Some(Self::NumpadMultiply),
|
||||
// "[numpadadd]" => Some(Self::NumpadAdd),
|
||||
// "[numpadcomma]" => Some(Self::NumpadComma),
|
||||
// "[numpadsubtract]" => Some(Self::NumpadSubtract),
|
||||
// "[numpaddecimal]" => Some(Self::NumpadDecimal),
|
||||
// "[numpaddivide]" => Some(Self::NumpadDivide),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the scan code to its key face for immutable keys.
|
||||
pub fn try_to_key(&self) -> Option<String> {
|
||||
Some(
|
||||
match self {
|
||||
ScanCode::F1 => "f1",
|
||||
ScanCode::F2 => "f2",
|
||||
ScanCode::F3 => "f3",
|
||||
ScanCode::F4 => "f4",
|
||||
ScanCode::F5 => "f5",
|
||||
ScanCode::F6 => "f6",
|
||||
ScanCode::F7 => "f7",
|
||||
ScanCode::F8 => "f8",
|
||||
ScanCode::F9 => "f9",
|
||||
ScanCode::F10 => "f10",
|
||||
ScanCode::F11 => "f11",
|
||||
ScanCode::F12 => "f12",
|
||||
ScanCode::F13 => "f13",
|
||||
ScanCode::F14 => "f14",
|
||||
ScanCode::F15 => "f15",
|
||||
ScanCode::F16 => "f16",
|
||||
ScanCode::F17 => "f17",
|
||||
ScanCode::F18 => "f18",
|
||||
ScanCode::F19 => "f19",
|
||||
ScanCode::F20 => "f20",
|
||||
ScanCode::F21 => "f21",
|
||||
ScanCode::F22 => "f22",
|
||||
ScanCode::F23 => "f23",
|
||||
ScanCode::F24 => "f24",
|
||||
ScanCode::Left => "left",
|
||||
ScanCode::Up => "up",
|
||||
ScanCode::Right => "right",
|
||||
ScanCode::Down => "down",
|
||||
ScanCode::PageUp => "pageup",
|
||||
ScanCode::PageDown => "pagedown",
|
||||
ScanCode::End => "end",
|
||||
ScanCode::Home => "home",
|
||||
ScanCode::Tab => "tab",
|
||||
ScanCode::Enter => "enter",
|
||||
ScanCode::Escape => "escape",
|
||||
ScanCode::Space => "space",
|
||||
ScanCode::Backspace => "backspace",
|
||||
ScanCode::Delete => "delete",
|
||||
ScanCode::Insert => "insert",
|
||||
_ => return None,
|
||||
}
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
/// This function is used to convert the scan code to its key face on US keyboard layout.
|
||||
/// Only used for tests.
|
||||
pub fn to_key(&self, shift: bool) -> &str {
|
||||
match self {
|
||||
ScanCode::F1 => "f1",
|
||||
ScanCode::F2 => "f2",
|
||||
ScanCode::F3 => "f3",
|
||||
ScanCode::F4 => "f4",
|
||||
ScanCode::F5 => "f5",
|
||||
ScanCode::F6 => "f6",
|
||||
ScanCode::F7 => "f7",
|
||||
ScanCode::F8 => "f8",
|
||||
ScanCode::F9 => "f9",
|
||||
ScanCode::F10 => "f10",
|
||||
ScanCode::F11 => "f11",
|
||||
ScanCode::F12 => "f12",
|
||||
ScanCode::F13 => "f13",
|
||||
ScanCode::F14 => "f14",
|
||||
ScanCode::F15 => "f15",
|
||||
ScanCode::F16 => "f16",
|
||||
ScanCode::F17 => "f17",
|
||||
ScanCode::F18 => "f18",
|
||||
ScanCode::F19 => "f19",
|
||||
ScanCode::F20 => "f20",
|
||||
ScanCode::F21 => "f21",
|
||||
ScanCode::F22 => "f22",
|
||||
ScanCode::F23 => "f23",
|
||||
ScanCode::F24 => "f24",
|
||||
ScanCode::A => "a",
|
||||
ScanCode::B => "b",
|
||||
ScanCode::C => "c",
|
||||
ScanCode::D => "d",
|
||||
ScanCode::E => "e",
|
||||
ScanCode::F => "f",
|
||||
ScanCode::G => "g",
|
||||
ScanCode::H => "h",
|
||||
ScanCode::I => "i",
|
||||
ScanCode::J => "j",
|
||||
ScanCode::K => "k",
|
||||
ScanCode::L => "l",
|
||||
ScanCode::M => "m",
|
||||
ScanCode::N => "n",
|
||||
ScanCode::O => "o",
|
||||
ScanCode::P => "p",
|
||||
ScanCode::Q => "q",
|
||||
ScanCode::R => "r",
|
||||
ScanCode::S => "s",
|
||||
ScanCode::T => "t",
|
||||
ScanCode::U => "u",
|
||||
ScanCode::V => "v",
|
||||
ScanCode::W => "w",
|
||||
ScanCode::X => "x",
|
||||
ScanCode::Y => "y",
|
||||
ScanCode::Z => "z",
|
||||
ScanCode::Digit0 => {
|
||||
if shift {
|
||||
")"
|
||||
} else {
|
||||
"0"
|
||||
}
|
||||
}
|
||||
ScanCode::Digit1 => {
|
||||
if shift {
|
||||
"!"
|
||||
} else {
|
||||
"1"
|
||||
}
|
||||
}
|
||||
ScanCode::Digit2 => {
|
||||
if shift {
|
||||
"@"
|
||||
} else {
|
||||
"2"
|
||||
}
|
||||
}
|
||||
ScanCode::Digit3 => {
|
||||
if shift {
|
||||
"#"
|
||||
} else {
|
||||
"3"
|
||||
}
|
||||
}
|
||||
ScanCode::Digit4 => {
|
||||
if shift {
|
||||
"$"
|
||||
} else {
|
||||
"4"
|
||||
}
|
||||
}
|
||||
ScanCode::Digit5 => {
|
||||
if shift {
|
||||
"%"
|
||||
} else {
|
||||
"5"
|
||||
}
|
||||
}
|
||||
ScanCode::Digit6 => {
|
||||
if shift {
|
||||
"^"
|
||||
} else {
|
||||
"6"
|
||||
}
|
||||
}
|
||||
ScanCode::Digit7 => {
|
||||
if shift {
|
||||
"&"
|
||||
} else {
|
||||
"7"
|
||||
}
|
||||
}
|
||||
ScanCode::Digit8 => {
|
||||
if shift {
|
||||
"*"
|
||||
} else {
|
||||
"8"
|
||||
}
|
||||
}
|
||||
ScanCode::Digit9 => {
|
||||
if shift {
|
||||
"("
|
||||
} else {
|
||||
"9"
|
||||
}
|
||||
}
|
||||
ScanCode::Backquote => {
|
||||
if shift {
|
||||
"~"
|
||||
} else {
|
||||
"`"
|
||||
}
|
||||
}
|
||||
ScanCode::Minus => {
|
||||
if shift {
|
||||
"_"
|
||||
} else {
|
||||
"-"
|
||||
}
|
||||
}
|
||||
ScanCode::Equal => {
|
||||
if shift {
|
||||
"+"
|
||||
} else {
|
||||
"="
|
||||
}
|
||||
}
|
||||
ScanCode::BracketLeft => {
|
||||
if shift {
|
||||
"{"
|
||||
} else {
|
||||
"["
|
||||
}
|
||||
}
|
||||
ScanCode::BracketRight => {
|
||||
if shift {
|
||||
"}"
|
||||
} else {
|
||||
"]"
|
||||
}
|
||||
}
|
||||
ScanCode::Backslash => {
|
||||
if shift {
|
||||
"|"
|
||||
} else {
|
||||
"\\"
|
||||
}
|
||||
}
|
||||
ScanCode::Semicolon => {
|
||||
if shift {
|
||||
":"
|
||||
} else {
|
||||
";"
|
||||
}
|
||||
}
|
||||
ScanCode::Quote => {
|
||||
if shift {
|
||||
"\""
|
||||
} else {
|
||||
"'"
|
||||
}
|
||||
}
|
||||
ScanCode::Comma => {
|
||||
if shift {
|
||||
"<"
|
||||
} else {
|
||||
","
|
||||
}
|
||||
}
|
||||
ScanCode::Period => {
|
||||
if shift {
|
||||
">"
|
||||
} else {
|
||||
"."
|
||||
}
|
||||
}
|
||||
ScanCode::Slash => {
|
||||
if shift {
|
||||
"?"
|
||||
} else {
|
||||
"/"
|
||||
}
|
||||
}
|
||||
ScanCode::Left => "left",
|
||||
ScanCode::Up => "up",
|
||||
ScanCode::Right => "right",
|
||||
ScanCode::Down => "down",
|
||||
ScanCode::PageUp => "pageup",
|
||||
ScanCode::PageDown => "pagedown",
|
||||
ScanCode::End => "end",
|
||||
ScanCode::Home => "home",
|
||||
ScanCode::Tab => "tab",
|
||||
ScanCode::Enter => "enter",
|
||||
ScanCode::Escape => "escape",
|
||||
ScanCode::Space => "space",
|
||||
ScanCode::Backspace => "backspace",
|
||||
ScanCode::Delete => "delete",
|
||||
ScanCode::Insert => "insert",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
use anyhow::Context;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::{Display, Write},
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::{PlatformKeyboardMapper, ScanCode};
|
||||
|
||||
/// A keystroke and associated metadata generated by the platform
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
||||
@@ -97,10 +93,7 @@ impl Keystroke {
|
||||
/// key_char syntax is only used for generating test events,
|
||||
/// secondary means "cmd" on macOS and "ctrl" on other platforms
|
||||
/// when matching a key with an key_char set will be matched without it.
|
||||
pub fn parse(
|
||||
source: &str,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) -> std::result::Result<Self, InvalidKeystrokeError> {
|
||||
pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
|
||||
let mut modifiers = Modifiers::none();
|
||||
let mut key = None;
|
||||
let mut key_char = None;
|
||||
@@ -191,24 +184,9 @@ impl Keystroke {
|
||||
}
|
||||
});
|
||||
|
||||
// Create error once for reuse
|
||||
let error = || InvalidKeystrokeError {
|
||||
let key = key.ok_or_else(|| InvalidKeystrokeError {
|
||||
keystroke: source.to_owned(),
|
||||
};
|
||||
|
||||
let key = {
|
||||
let key = key.ok_or_else(error)?;
|
||||
if key.starts_with('[') && key.ends_with(']') {
|
||||
let scan_code = ScanCode::parse(&key).ok_or_else(error)?;
|
||||
keyboard_mapper
|
||||
.scan_code_to_key(scan_code, &mut modifiers)
|
||||
.context("Failed to convert scan code to key")
|
||||
.log_err()
|
||||
.ok_or_else(error)?
|
||||
} else {
|
||||
key
|
||||
}
|
||||
};
|
||||
})?;
|
||||
|
||||
Ok(Keystroke {
|
||||
modifiers,
|
||||
|
||||
@@ -1,20 +1,4 @@
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
use std::sync::LazyLock;
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
use collections::HashMap;
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
use x11rb::{protocol::xkb::ConnectionExt, xcb_ffi::XCBConnection};
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
use xkbcommon::xkb::{
|
||||
Keycode,
|
||||
x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION},
|
||||
};
|
||||
|
||||
use crate::{Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper, ScanCode};
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
use crate::is_letter_key;
|
||||
use crate::PlatformKeyboardLayout;
|
||||
|
||||
pub(crate) struct LinuxKeyboardLayout {
|
||||
id: String,
|
||||
@@ -35,257 +19,3 @@ impl LinuxKeyboardLayout {
|
||||
Self { id }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
pub(crate) struct LinuxKeyboardMapper {
|
||||
code_to_key: HashMap<Keycode, String>,
|
||||
code_to_shifted_key: HashMap<Keycode, String>,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
impl PlatformKeyboardMapper for LinuxKeyboardMapper {
|
||||
fn scan_code_to_key(
|
||||
&self,
|
||||
scan_code: ScanCode,
|
||||
modifiers: &mut Modifiers,
|
||||
) -> anyhow::Result<String> {
|
||||
if let Some(key) = scan_code.try_to_key() {
|
||||
return Ok(key);
|
||||
}
|
||||
let native_scan_code = get_scan_code(scan_code)
|
||||
.map(Keycode::new)
|
||||
.ok_or_else(|| anyhow::anyhow!("Unsupported scan code: {:?}", scan_code))?;
|
||||
let key = self.code_to_key.get(&native_scan_code).ok_or_else(|| {
|
||||
anyhow::anyhow!("Key not found for scan code: {:?}", native_scan_code)
|
||||
})?;
|
||||
if modifiers.shift && !is_letter_key(key) {
|
||||
if let Some(key) = self.code_to_shifted_key.get(&native_scan_code) {
|
||||
modifiers.shift = false;
|
||||
return Ok(key.clone());
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"Shifted key not found for scan code: {:?}",
|
||||
native_scan_code
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Ok(key.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
static XCB_CONNECTION: LazyLock<XCBConnection> =
|
||||
LazyLock::new(|| XCBConnection::connect(None).unwrap().0);
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
impl LinuxKeyboardMapper {
|
||||
pub(crate) fn new() -> Self {
|
||||
let _ = XCB_CONNECTION
|
||||
.xkb_use_extension(XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
let xkb_context = xkbcommon::xkb::Context::new(xkbcommon::xkb::CONTEXT_NO_FLAGS);
|
||||
let xkb_device_id = xkbcommon::xkb::x11::get_core_keyboard_device_id(&*XCB_CONNECTION);
|
||||
let xkb_state = {
|
||||
let xkb_keymap = xkbcommon::xkb::x11::keymap_new_from_device(
|
||||
&xkb_context,
|
||||
&*XCB_CONNECTION,
|
||||
xkb_device_id,
|
||||
xkbcommon::xkb::KEYMAP_COMPILE_NO_FLAGS,
|
||||
);
|
||||
xkbcommon::xkb::x11::state_new_from_device(&xkb_keymap, &*XCB_CONNECTION, xkb_device_id)
|
||||
};
|
||||
let mut code_to_key = HashMap::default();
|
||||
let mut code_to_shifted_key = HashMap::default();
|
||||
|
||||
let keymap = xkb_state.get_keymap();
|
||||
let mut shifted_state = xkbcommon::xkb::State::new(&keymap);
|
||||
|
||||
let shift_mod = keymap.mod_get_index(xkbcommon::xkb::MOD_NAME_SHIFT);
|
||||
let shift_mask = 1 << shift_mod;
|
||||
shifted_state.update_mask(shift_mask, 0, 0, 0, 0, 0);
|
||||
|
||||
for &scan_code in TYPEABLE_CODES {
|
||||
let keycode = Keycode::new(scan_code);
|
||||
let key = xkb_state.key_get_utf8(keycode);
|
||||
if !is_letter_key(&key) {
|
||||
let shifted_key = shifted_state.key_get_utf8(keycode);
|
||||
code_to_shifted_key.insert(keycode, shifted_key);
|
||||
}
|
||||
code_to_key.insert(keycode, key);
|
||||
}
|
||||
|
||||
Self {
|
||||
code_to_key,
|
||||
code_to_shifted_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All typeable scan codes for the standard US keyboard layout, ANSI104
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
const TYPEABLE_CODES: &[u32] = &[
|
||||
0x0026, // a
|
||||
0x0038, // b
|
||||
0x0036, // c
|
||||
0x0028, // d
|
||||
0x001a, // e
|
||||
0x0029, // f
|
||||
0x002a, // g
|
||||
0x002b, // h
|
||||
0x001f, // i
|
||||
0x002c, // j
|
||||
0x002d, // k
|
||||
0x002e, // l
|
||||
0x003a, // m
|
||||
0x0039, // n
|
||||
0x0020, // o
|
||||
0x0021, // p
|
||||
0x0018, // q
|
||||
0x001b, // r
|
||||
0x0027, // s
|
||||
0x001c, // t
|
||||
0x001e, // u
|
||||
0x0037, // v
|
||||
0x0019, // w
|
||||
0x0035, // x
|
||||
0x001d, // y
|
||||
0x0034, // z
|
||||
0x0013, // Digit 0
|
||||
0x000a, // Digit 1
|
||||
0x000b, // Digit 2
|
||||
0x000c, // Digit 3
|
||||
0x000d, // Digit 4
|
||||
0x000e, // Digit 5
|
||||
0x000f, // Digit 6
|
||||
0x0010, // Digit 7
|
||||
0x0011, // Digit 8
|
||||
0x0012, // Digit 9
|
||||
0x0031, // ` Backquote
|
||||
0x0014, // - Minus
|
||||
0x0015, // = Equal
|
||||
0x0022, // [ Left bracket
|
||||
0x0023, // ] Right bracket
|
||||
0x0033, // \ Backslash
|
||||
0x002f, // ; Semicolon
|
||||
0x0030, // ' Quote
|
||||
0x003b, // , Comma
|
||||
0x003c, // . Period
|
||||
0x003d, // / Slash
|
||||
];
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
fn get_scan_code(scan_code: ScanCode) -> Option<u32> {
|
||||
// https://github.com/microsoft/node-native-keymap/blob/main/deps/chromium/dom_code_data.inc
|
||||
Some(match scan_code {
|
||||
ScanCode::F1 => 0x0043,
|
||||
ScanCode::F2 => 0x0044,
|
||||
ScanCode::F3 => 0x0045,
|
||||
ScanCode::F4 => 0x0046,
|
||||
ScanCode::F5 => 0x0047,
|
||||
ScanCode::F6 => 0x0048,
|
||||
ScanCode::F7 => 0x0049,
|
||||
ScanCode::F8 => 0x004a,
|
||||
ScanCode::F9 => 0x004b,
|
||||
ScanCode::F10 => 0x004c,
|
||||
ScanCode::F11 => 0x005f,
|
||||
ScanCode::F12 => 0x0060,
|
||||
ScanCode::F13 => 0x00bf,
|
||||
ScanCode::F14 => 0x00c0,
|
||||
ScanCode::F15 => 0x00c1,
|
||||
ScanCode::F16 => 0x00c2,
|
||||
ScanCode::F17 => 0x00c3,
|
||||
ScanCode::F18 => 0x00c4,
|
||||
ScanCode::F19 => 0x00c5,
|
||||
ScanCode::F20 => 0x00c6,
|
||||
ScanCode::F21 => 0x00c7,
|
||||
ScanCode::F22 => 0x00c8,
|
||||
ScanCode::F23 => 0x00c9,
|
||||
ScanCode::F24 => 0x00ca,
|
||||
ScanCode::A => 0x0026,
|
||||
ScanCode::B => 0x0038,
|
||||
ScanCode::C => 0x0036,
|
||||
ScanCode::D => 0x0028,
|
||||
ScanCode::E => 0x001a,
|
||||
ScanCode::F => 0x0029,
|
||||
ScanCode::G => 0x002a,
|
||||
ScanCode::H => 0x002b,
|
||||
ScanCode::I => 0x001f,
|
||||
ScanCode::J => 0x002c,
|
||||
ScanCode::K => 0x002d,
|
||||
ScanCode::L => 0x002e,
|
||||
ScanCode::M => 0x003a,
|
||||
ScanCode::N => 0x0039,
|
||||
ScanCode::O => 0x0020,
|
||||
ScanCode::P => 0x0021,
|
||||
ScanCode::Q => 0x0018,
|
||||
ScanCode::R => 0x001b,
|
||||
ScanCode::S => 0x0027,
|
||||
ScanCode::T => 0x001c,
|
||||
ScanCode::U => 0x001e,
|
||||
ScanCode::V => 0x0037,
|
||||
ScanCode::W => 0x0019,
|
||||
ScanCode::X => 0x0035,
|
||||
ScanCode::Y => 0x001d,
|
||||
ScanCode::Z => 0x0034,
|
||||
ScanCode::Digit0 => 0x0013,
|
||||
ScanCode::Digit1 => 0x000a,
|
||||
ScanCode::Digit2 => 0x000b,
|
||||
ScanCode::Digit3 => 0x000c,
|
||||
ScanCode::Digit4 => 0x000d,
|
||||
ScanCode::Digit5 => 0x000e,
|
||||
ScanCode::Digit6 => 0x000f,
|
||||
ScanCode::Digit7 => 0x0010,
|
||||
ScanCode::Digit8 => 0x0011,
|
||||
ScanCode::Digit9 => 0x0012,
|
||||
ScanCode::Backquote => 0x0031,
|
||||
ScanCode::Minus => 0x0014,
|
||||
ScanCode::Equal => 0x0015,
|
||||
ScanCode::BracketLeft => 0x0022,
|
||||
ScanCode::BracketRight => 0x0023,
|
||||
ScanCode::Backslash => 0x0033,
|
||||
ScanCode::Semicolon => 0x002f,
|
||||
ScanCode::Quote => 0x0030,
|
||||
ScanCode::Comma => 0x003b,
|
||||
ScanCode::Period => 0x003c,
|
||||
ScanCode::Slash => 0x003d,
|
||||
ScanCode::Left => 0x0071,
|
||||
ScanCode::Up => 0x006f,
|
||||
ScanCode::Right => 0x0072,
|
||||
ScanCode::Down => 0x0074,
|
||||
ScanCode::PageUp => 0x0070,
|
||||
ScanCode::PageDown => 0x0075,
|
||||
ScanCode::End => 0x0073,
|
||||
ScanCode::Home => 0x006e,
|
||||
ScanCode::Tab => 0x0017,
|
||||
ScanCode::Enter => 0x0024,
|
||||
ScanCode::Escape => 0x0009,
|
||||
ScanCode::Space => 0x0041,
|
||||
ScanCode::Backspace => 0x0016,
|
||||
ScanCode::Delete => 0x0077,
|
||||
ScanCode::Insert => 0x0076,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "wayland", feature = "x11")))]
|
||||
pub(crate) struct LinuxKeyboardMapper;
|
||||
|
||||
#[cfg(not(any(feature = "wayland", feature = "x11")))]
|
||||
impl PlatformKeyboardMapper for LinuxKeyboardMapper {
|
||||
fn scan_code_to_key(
|
||||
&self,
|
||||
_scan_code: ScanCode,
|
||||
_modifiers: &mut Modifiers,
|
||||
) -> anyhow::Result<String> {
|
||||
Err(anyhow::anyhow!("LinuxKeyboardMapper not supported"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "wayland", feature = "x11")))]
|
||||
impl LinuxKeyboardMapper {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,8 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
|
||||
use crate::{
|
||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
|
||||
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
|
||||
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||
PlatformTextSystem, PlatformWindow, Point, Result, ScreenCaptureSource, Task, WindowAppearance,
|
||||
WindowParams, px,
|
||||
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow,
|
||||
Point, Result, ScreenCaptureSource, Task, WindowAppearance, WindowParams, px,
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
@@ -139,10 +138,6 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
self.with_common(|common| common.text_system.clone())
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper> {
|
||||
Box::new(super::LinuxKeyboardMapper::new())
|
||||
}
|
||||
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
|
||||
self.keyboard_layout()
|
||||
}
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
use crate::{
|
||||
CMD_MOD, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
|
||||
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NO_MOD, NavigationDirection,
|
||||
OPTION_MOD, Pixels, PlatformInput, SHIFT_MOD, ScrollDelta, ScrollWheelEvent, TouchPhase,
|
||||
always_use_command_layout, chars_for_modified_key, platform::mac::NSStringExt, point, px,
|
||||
KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
|
||||
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
|
||||
PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase,
|
||||
platform::mac::{
|
||||
LMGetKbdType, NSStringExt, TISCopyCurrentKeyboardLayoutInputSource,
|
||||
TISGetInputSourceProperty, UCKeyTranslate, kTISPropertyUnicodeKeyLayoutData,
|
||||
},
|
||||
point, px,
|
||||
};
|
||||
use cocoa::{
|
||||
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
|
||||
base::{YES, id},
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use core_foundation::data::{CFDataGetBytePtr, CFDataRef};
|
||||
use core_graphics::event::CGKeyCode;
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
use std::{borrow::Cow, ffi::c_void};
|
||||
|
||||
const BACKSPACE_KEY: u16 = 0x7f;
|
||||
const SPACE_KEY: u16 = b' ' as u16;
|
||||
@@ -445,3 +452,80 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn always_use_command_layout() -> bool {
|
||||
if chars_for_modified_key(0, NO_MOD).is_ascii() {
|
||||
return false;
|
||||
}
|
||||
|
||||
chars_for_modified_key(0, CMD_MOD).is_ascii()
|
||||
}
|
||||
|
||||
const NO_MOD: u32 = 0;
|
||||
const CMD_MOD: u32 = 1;
|
||||
const SHIFT_MOD: u32 = 2;
|
||||
const OPTION_MOD: u32 = 8;
|
||||
|
||||
fn chars_for_modified_key(code: CGKeyCode, modifiers: u32) -> String {
|
||||
// Values from: https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h#L126
|
||||
// shifted >> 8 for UCKeyTranslate
|
||||
const CG_SPACE_KEY: u16 = 49;
|
||||
// https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/Headers/UnicodeUtilities.h#L278
|
||||
#[allow(non_upper_case_globals)]
|
||||
const kUCKeyActionDown: u16 = 0;
|
||||
#[allow(non_upper_case_globals)]
|
||||
const kUCKeyTranslateNoDeadKeysMask: u32 = 0;
|
||||
|
||||
let keyboard_type = unsafe { LMGetKbdType() as u32 };
|
||||
const BUFFER_SIZE: usize = 4;
|
||||
let mut dead_key_state = 0;
|
||||
let mut buffer: [u16; BUFFER_SIZE] = [0; BUFFER_SIZE];
|
||||
let mut buffer_size: usize = 0;
|
||||
|
||||
let keyboard = unsafe { TISCopyCurrentKeyboardLayoutInputSource() };
|
||||
if keyboard.is_null() {
|
||||
return "".to_string();
|
||||
}
|
||||
let layout_data = unsafe {
|
||||
TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData as *const c_void)
|
||||
as CFDataRef
|
||||
};
|
||||
if layout_data.is_null() {
|
||||
unsafe {
|
||||
let _: () = msg_send![keyboard, release];
|
||||
}
|
||||
return "".to_string();
|
||||
}
|
||||
let keyboard_layout = unsafe { CFDataGetBytePtr(layout_data) };
|
||||
|
||||
unsafe {
|
||||
UCKeyTranslate(
|
||||
keyboard_layout as *const c_void,
|
||||
code,
|
||||
kUCKeyActionDown,
|
||||
modifiers,
|
||||
keyboard_type,
|
||||
kUCKeyTranslateNoDeadKeysMask,
|
||||
&mut dead_key_state,
|
||||
BUFFER_SIZE,
|
||||
&mut buffer_size as *mut usize,
|
||||
&mut buffer as *mut u16,
|
||||
);
|
||||
if dead_key_state != 0 {
|
||||
UCKeyTranslate(
|
||||
keyboard_layout as *const c_void,
|
||||
CG_SPACE_KEY,
|
||||
kUCKeyActionDown,
|
||||
modifiers,
|
||||
keyboard_type,
|
||||
kUCKeyTranslateNoDeadKeysMask,
|
||||
&mut dead_key_state,
|
||||
BUFFER_SIZE,
|
||||
&mut buffer_size as *mut usize,
|
||||
&mut buffer as *mut u16,
|
||||
);
|
||||
}
|
||||
let _: () = msg_send![keyboard, release];
|
||||
}
|
||||
String::from_utf16(&buffer[..buffer_size]).unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
use std::ffi::{CStr, c_void};
|
||||
|
||||
use collections::HashMap;
|
||||
use core_foundation::data::{CFDataGetBytePtr, CFDataRef};
|
||||
use core_graphics::event::CGKeyCode;
|
||||
use objc::{msg_send, runtime::Object, sel, sel_impl};
|
||||
|
||||
use crate::{
|
||||
Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper, ScanCode, is_letter_key,
|
||||
platform::mac::{LMGetKbdType, UCKeyTranslate, kTISPropertyUnicodeKeyLayoutData},
|
||||
};
|
||||
use crate::PlatformKeyboardLayout;
|
||||
|
||||
use super::{
|
||||
TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, kTISPropertyInputSourceID,
|
||||
@@ -53,300 +47,3 @@ impl MacKeyboardLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct MacKeyboardMapper {
|
||||
code_to_key: HashMap<u16, String>,
|
||||
code_to_shifted_key: HashMap<u16, String>,
|
||||
}
|
||||
|
||||
impl MacKeyboardMapper {
|
||||
pub(crate) fn new() -> Self {
|
||||
let mut code_to_key = HashMap::default();
|
||||
let mut code_to_shifted_key = HashMap::default();
|
||||
|
||||
let always_use_cmd_layout = always_use_command_layout();
|
||||
for &scan_code in TYPEABLE_CODES.iter() {
|
||||
let (key, shifted_key) = generate_key_pairs(scan_code, always_use_cmd_layout);
|
||||
if !is_letter_key(&key) {
|
||||
code_to_shifted_key.insert(scan_code, shifted_key);
|
||||
}
|
||||
code_to_key.insert(scan_code, key);
|
||||
}
|
||||
|
||||
Self {
|
||||
code_to_key,
|
||||
code_to_shifted_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformKeyboardMapper for MacKeyboardMapper {
|
||||
fn scan_code_to_key(
|
||||
&self,
|
||||
scan_code: ScanCode,
|
||||
modifiers: &mut Modifiers,
|
||||
) -> anyhow::Result<String> {
|
||||
if let Some(key) = scan_code.try_to_key() {
|
||||
return Ok(key);
|
||||
}
|
||||
let native_scan_code = get_scan_code(scan_code)
|
||||
.ok_or_else(|| anyhow::anyhow!("Unsupported scan code: {:?}", scan_code))?;
|
||||
let key = self.code_to_key.get(&native_scan_code).ok_or_else(|| {
|
||||
anyhow::anyhow!("Key not found for scan code: {:?}", native_scan_code)
|
||||
})?;
|
||||
if modifiers.shift && !is_letter_key(key) {
|
||||
if let Some(key) = self.code_to_shifted_key.get(&native_scan_code) {
|
||||
modifiers.shift = false;
|
||||
return Ok(key.clone());
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"Shifted key not found for scan code: {:?}",
|
||||
native_scan_code
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Ok(key.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const NO_MOD: u32 = 0;
|
||||
pub(crate) const CMD_MOD: u32 = 1;
|
||||
pub(crate) const SHIFT_MOD: u32 = 2;
|
||||
pub(crate) const OPTION_MOD: u32 = 8;
|
||||
|
||||
pub(crate) fn chars_for_modified_key(code: CGKeyCode, modifiers: u32) -> String {
|
||||
// Values from: https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h#L126
|
||||
// shifted >> 8 for UCKeyTranslate
|
||||
const CG_SPACE_KEY: u16 = 49;
|
||||
// https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/Headers/UnicodeUtilities.h#L278
|
||||
#[allow(non_upper_case_globals)]
|
||||
const kUCKeyActionDown: u16 = 0;
|
||||
#[allow(non_upper_case_globals)]
|
||||
const kUCKeyTranslateNoDeadKeysMask: u32 = 0;
|
||||
|
||||
let keyboard_type = unsafe { LMGetKbdType() as u32 };
|
||||
const BUFFER_SIZE: usize = 4;
|
||||
let mut dead_key_state = 0;
|
||||
let mut buffer: [u16; BUFFER_SIZE] = [0; BUFFER_SIZE];
|
||||
let mut buffer_size: usize = 0;
|
||||
|
||||
let keyboard = unsafe { TISCopyCurrentKeyboardLayoutInputSource() };
|
||||
if keyboard.is_null() {
|
||||
return "".to_string();
|
||||
}
|
||||
let layout_data = unsafe {
|
||||
TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData as *const c_void)
|
||||
as CFDataRef
|
||||
};
|
||||
if layout_data.is_null() {
|
||||
unsafe {
|
||||
let _: () = msg_send![keyboard, release];
|
||||
}
|
||||
return "".to_string();
|
||||
}
|
||||
let keyboard_layout = unsafe { CFDataGetBytePtr(layout_data) };
|
||||
|
||||
unsafe {
|
||||
UCKeyTranslate(
|
||||
keyboard_layout as *const c_void,
|
||||
code,
|
||||
kUCKeyActionDown,
|
||||
modifiers,
|
||||
keyboard_type,
|
||||
kUCKeyTranslateNoDeadKeysMask,
|
||||
&mut dead_key_state,
|
||||
BUFFER_SIZE,
|
||||
&mut buffer_size as *mut usize,
|
||||
&mut buffer as *mut u16,
|
||||
);
|
||||
if dead_key_state != 0 {
|
||||
UCKeyTranslate(
|
||||
keyboard_layout as *const c_void,
|
||||
CG_SPACE_KEY,
|
||||
kUCKeyActionDown,
|
||||
modifiers,
|
||||
keyboard_type,
|
||||
kUCKeyTranslateNoDeadKeysMask,
|
||||
&mut dead_key_state,
|
||||
BUFFER_SIZE,
|
||||
&mut buffer_size as *mut usize,
|
||||
&mut buffer as *mut u16,
|
||||
);
|
||||
}
|
||||
let _: () = msg_send![keyboard, release];
|
||||
}
|
||||
String::from_utf16(&buffer[..buffer_size]).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn always_use_command_layout() -> bool {
|
||||
if chars_for_modified_key(0, NO_MOD).is_ascii() {
|
||||
return false;
|
||||
}
|
||||
|
||||
chars_for_modified_key(0, CMD_MOD).is_ascii()
|
||||
}
|
||||
|
||||
fn generate_key_pairs(scan_code: u16, always_use_cmd_layout: bool) -> (String, String) {
|
||||
let mut chars_ignoring_modifiers = chars_for_modified_key(scan_code, NO_MOD);
|
||||
let mut chars_with_shift = chars_for_modified_key(scan_code, SHIFT_MOD);
|
||||
|
||||
// Handle Dvorak+QWERTY / Russian / Armenian
|
||||
if always_use_cmd_layout {
|
||||
let chars_with_cmd = chars_for_modified_key(scan_code, CMD_MOD);
|
||||
let chars_with_both = chars_for_modified_key(scan_code, CMD_MOD | SHIFT_MOD);
|
||||
|
||||
// We don't do this in the case that the shifted command key generates
|
||||
// the same character as the unshifted command key (Norwegian, e.g.)
|
||||
if chars_with_both != chars_with_cmd {
|
||||
chars_with_shift = chars_with_both;
|
||||
|
||||
// Handle edge-case where cmd-shift-s reports cmd-s instead of
|
||||
// cmd-shift-s (Ukrainian, etc.)
|
||||
} else if chars_with_cmd.to_ascii_uppercase() != chars_with_cmd {
|
||||
chars_with_shift = chars_with_cmd.to_ascii_uppercase();
|
||||
}
|
||||
chars_ignoring_modifiers = chars_with_cmd;
|
||||
}
|
||||
(chars_ignoring_modifiers, chars_with_shift)
|
||||
}
|
||||
|
||||
// All typeable scan codes for the standard US keyboard layout, ANSI104
|
||||
const TYPEABLE_CODES: &[u16] = &[
|
||||
0x0000, // a
|
||||
0x000b, // b
|
||||
0x0008, // c
|
||||
0x0002, // d
|
||||
0x000e, // e
|
||||
0x0003, // f
|
||||
0x0005, // g
|
||||
0x0004, // h
|
||||
0x0022, // i
|
||||
0x0026, // j
|
||||
0x0028, // k
|
||||
0x0025, // l
|
||||
0x002e, // m
|
||||
0x002d, // n
|
||||
0x001f, // o
|
||||
0x0023, // p
|
||||
0x000c, // q
|
||||
0x000f, // r
|
||||
0x0001, // s
|
||||
0x0011, // t
|
||||
0x0020, // u
|
||||
0x0009, // v
|
||||
0x000d, // w
|
||||
0x0007, // x
|
||||
0x0010, // y
|
||||
0x0006, // z
|
||||
0x001d, // Digit 0
|
||||
0x0012, // Digit 1
|
||||
0x0013, // Digit 2
|
||||
0x0014, // Digit 3
|
||||
0x0015, // Digit 4
|
||||
0x0017, // Digit 5
|
||||
0x0016, // Digit 6
|
||||
0x001a, // Digit 7
|
||||
0x001c, // Digit 8
|
||||
0x0019, // Digit 9
|
||||
0x0032, // ` Tilde
|
||||
0x001b, // - Minus
|
||||
0x0018, // = Equal
|
||||
0x0021, // [ Left bracket
|
||||
0x001e, // ] Right bracket
|
||||
0x002a, // \ Backslash
|
||||
0x0029, // ; Semicolon
|
||||
0x0027, // ' Quote
|
||||
0x002b, // , Comma
|
||||
0x002f, // . Period
|
||||
0x002c, // / Slash
|
||||
];
|
||||
|
||||
fn get_scan_code(scan_code: ScanCode) -> Option<u16> {
|
||||
// https://github.com/microsoft/node-native-keymap/blob/main/deps/chromium/dom_code_data.inc
|
||||
Some(match scan_code {
|
||||
ScanCode::F1 => 0x007a,
|
||||
ScanCode::F2 => 0x0078,
|
||||
ScanCode::F3 => 0x0063,
|
||||
ScanCode::F4 => 0x0076,
|
||||
ScanCode::F5 => 0x0060,
|
||||
ScanCode::F6 => 0x0061,
|
||||
ScanCode::F7 => 0x0062,
|
||||
ScanCode::F8 => 0x0064,
|
||||
ScanCode::F9 => 0x0065,
|
||||
ScanCode::F10 => 0x006d,
|
||||
ScanCode::F11 => 0x0067,
|
||||
ScanCode::F12 => 0x006f,
|
||||
ScanCode::F13 => 0x0069,
|
||||
ScanCode::F14 => 0x006b,
|
||||
ScanCode::F15 => 0x0071,
|
||||
ScanCode::F16 => 0x006a,
|
||||
ScanCode::F17 => 0x0040,
|
||||
ScanCode::F18 => 0x004f,
|
||||
ScanCode::F19 => 0x0050,
|
||||
ScanCode::F20 => 0x005a,
|
||||
ScanCode::F21 | ScanCode::F22 | ScanCode::F23 | ScanCode::F24 => return None,
|
||||
ScanCode::A => 0x0000,
|
||||
ScanCode::B => 0x000b,
|
||||
ScanCode::C => 0x0008,
|
||||
ScanCode::D => 0x0002,
|
||||
ScanCode::E => 0x000e,
|
||||
ScanCode::F => 0x0003,
|
||||
ScanCode::G => 0x0005,
|
||||
ScanCode::H => 0x0004,
|
||||
ScanCode::I => 0x0022,
|
||||
ScanCode::J => 0x0026,
|
||||
ScanCode::K => 0x0028,
|
||||
ScanCode::L => 0x0025,
|
||||
ScanCode::M => 0x002e,
|
||||
ScanCode::N => 0x002d,
|
||||
ScanCode::O => 0x001f,
|
||||
ScanCode::P => 0x0023,
|
||||
ScanCode::Q => 0x000c,
|
||||
ScanCode::R => 0x000f,
|
||||
ScanCode::S => 0x0001,
|
||||
ScanCode::T => 0x0011,
|
||||
ScanCode::U => 0x0020,
|
||||
ScanCode::V => 0x0009,
|
||||
ScanCode::W => 0x000d,
|
||||
ScanCode::X => 0x0007,
|
||||
ScanCode::Y => 0x0010,
|
||||
ScanCode::Z => 0x0006,
|
||||
ScanCode::Digit0 => 0x001d,
|
||||
ScanCode::Digit1 => 0x0012,
|
||||
ScanCode::Digit2 => 0x0013,
|
||||
ScanCode::Digit3 => 0x0014,
|
||||
ScanCode::Digit4 => 0x0015,
|
||||
ScanCode::Digit5 => 0x0017,
|
||||
ScanCode::Digit6 => 0x0016,
|
||||
ScanCode::Digit7 => 0x001a,
|
||||
ScanCode::Digit8 => 0x001c,
|
||||
ScanCode::Digit9 => 0x0019,
|
||||
ScanCode::Backquote => 0x0032,
|
||||
ScanCode::Minus => 0x001b,
|
||||
ScanCode::Equal => 0x0018,
|
||||
ScanCode::BracketLeft => 0x0021,
|
||||
ScanCode::BracketRight => 0x001e,
|
||||
ScanCode::Backslash => 0x002a,
|
||||
ScanCode::Semicolon => 0x0029,
|
||||
ScanCode::Quote => 0x0027,
|
||||
ScanCode::Comma => 0x002b,
|
||||
ScanCode::Period => 0x002f,
|
||||
ScanCode::Slash => 0x002c,
|
||||
ScanCode::Left => 0x007b,
|
||||
ScanCode::Up => 0x007e,
|
||||
ScanCode::Right => 0x007c,
|
||||
ScanCode::Down => 0x007d,
|
||||
ScanCode::PageUp => 0x0074,
|
||||
ScanCode::PageDown => 0x0079,
|
||||
ScanCode::End => 0x0077,
|
||||
ScanCode::Home => 0x0073,
|
||||
ScanCode::Tab => 0x0030,
|
||||
ScanCode::Enter => 0x0024,
|
||||
ScanCode::Escape => 0x0035,
|
||||
ScanCode::Space => 0x0031,
|
||||
ScanCode::Backspace => 0x0033,
|
||||
ScanCode::Delete => 0x0075,
|
||||
ScanCode::Insert => 0x0072,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,10 +7,9 @@ use super::{
|
||||
use crate::{
|
||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
|
||||
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
|
||||
MacDisplay, MacKeyboardMapper, MacWindow, Menu, MenuItem, PathPromptOptions, Platform,
|
||||
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
|
||||
PlatformWindow, Result, ScreenCaptureSource, SemanticVersion, Task, WindowAppearance,
|
||||
WindowParams, hash,
|
||||
MacDisplay, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
|
||||
PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, ScreenCaptureSource,
|
||||
SemanticVersion, Task, WindowAppearance, WindowParams, hash,
|
||||
};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use block::ConcreteBlock;
|
||||
@@ -847,10 +846,6 @@ impl Platform for MacPlatform {
|
||||
self.0.lock().validate_menu_command = Some(callback);
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper> {
|
||||
Box::new(MacKeyboardMapper::new())
|
||||
}
|
||||
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
|
||||
Box::new(MacKeyboardLayout::new())
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::{
|
||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
|
||||
PlatformKeyboardMapper, PlatformTextSystem, PromptButton, ScreenCaptureFrame,
|
||||
ScreenCaptureSource, ScreenCaptureStream, Size, Task, TestDisplay, TestKeyboardMapper,
|
||||
TestWindow, WindowAppearance, WindowParams, size,
|
||||
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
||||
Size, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::VecDeque;
|
||||
@@ -224,10 +223,6 @@ impl Platform for TestPlatform {
|
||||
self.text_system.clone()
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper> {
|
||||
Box::new(TestKeyboardMapper::new())
|
||||
}
|
||||
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
|
||||
Box::new(TestKeyboardLayout)
|
||||
}
|
||||
|
||||
@@ -702,7 +702,7 @@ fn handle_ime_composition_inner(
|
||||
} else {
|
||||
if lparam & GCS_COMPSTR.0 > 0 {
|
||||
let comp_string = parse_ime_composition_string(ctx, GCS_COMPSTR)?;
|
||||
let caret_pos = (!comp_string.is_empty() && lparam & GCS_CURSORPOS.0 > 0).then(|| {
|
||||
let caret_pos = (lparam & GCS_CURSORPOS.0 > 0).then(|| {
|
||||
let pos = retrieve_composition_cursor_position(ctx);
|
||||
pos..pos
|
||||
});
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::Result;
|
||||
use windows::Win32::UI::{
|
||||
Input::KeyboardAndMouse::{
|
||||
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VSC_TO_VK, MapVirtualKeyW, ToUnicode,
|
||||
VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1,
|
||||
VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7,
|
||||
VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
|
||||
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, VIRTUAL_KEY, VK_0,
|
||||
VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, VK_CONTROL, VK_MENU,
|
||||
VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_102,
|
||||
VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
|
||||
},
|
||||
WindowsAndMessaging::KL_NAMELENGTH,
|
||||
};
|
||||
use windows_core::HSTRING;
|
||||
|
||||
use crate::{Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper, ScanCode};
|
||||
use crate::{Modifiers, PlatformKeyboardLayout};
|
||||
|
||||
pub(crate) struct WindowsKeyboardLayout {
|
||||
id: String,
|
||||
@@ -48,29 +48,6 @@ impl WindowsKeyboardLayout {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct WindowsKeyboardMapper;
|
||||
|
||||
impl PlatformKeyboardMapper for WindowsKeyboardMapper {
|
||||
fn scan_code_to_key(&self, scan_code: ScanCode, modifiers: &mut Modifiers) -> Result<String> {
|
||||
if let Some(key) = scan_code.try_to_key() {
|
||||
return Ok(key);
|
||||
}
|
||||
let (win_scan_code, vkey) = get_virtual_key_from_scan_code(scan_code)?;
|
||||
get_keystroke_key(vkey, win_scan_code, modifiers).with_context(|| {
|
||||
format!(
|
||||
"Failed to get key from scan code: {:?}, vkey: {:?}",
|
||||
scan_code, vkey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowsKeyboardMapper {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_keystroke_key(
|
||||
vkey: VIRTUAL_KEY,
|
||||
scan_code: u32,
|
||||
@@ -105,15 +82,15 @@ fn need_to_convert_to_shifted_key(vkey: VIRTUAL_KEY) -> bool {
|
||||
| VK_OEM_MINUS
|
||||
| VK_OEM_PLUS
|
||||
| VK_OEM_4
|
||||
| VK_OEM_6
|
||||
| VK_OEM_5
|
||||
| VK_OEM_6
|
||||
| VK_OEM_1
|
||||
| VK_OEM_7
|
||||
| VK_OEM_COMMA
|
||||
| VK_OEM_PERIOD
|
||||
| VK_OEM_2
|
||||
| VK_OEM_102
|
||||
| VK_OEM_8 // Same as VK_OEM_2
|
||||
| VK_OEM_8
|
||||
| VK_ABNT_C1
|
||||
| VK_0
|
||||
| VK_1
|
||||
@@ -161,66 +138,3 @@ pub(crate) fn generate_key_char(
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn get_virtual_key_from_scan_code(gpui_scan_code: ScanCode) -> Result<(u32, VIRTUAL_KEY)> {
|
||||
// https://github.com/microsoft/node-native-keymap/blob/main/deps/chromium/dom_code_data.inc
|
||||
let scan_code = match gpui_scan_code {
|
||||
ScanCode::A => 0x001e,
|
||||
ScanCode::B => 0x0030,
|
||||
ScanCode::C => 0x002e,
|
||||
ScanCode::D => 0x0020,
|
||||
ScanCode::E => 0x0012,
|
||||
ScanCode::F => 0x0021,
|
||||
ScanCode::G => 0x0022,
|
||||
ScanCode::H => 0x0023,
|
||||
ScanCode::I => 0x0017,
|
||||
ScanCode::J => 0x0024,
|
||||
ScanCode::K => 0x0025,
|
||||
ScanCode::L => 0x0026,
|
||||
ScanCode::M => 0x0032,
|
||||
ScanCode::N => 0x0031,
|
||||
ScanCode::O => 0x0018,
|
||||
ScanCode::P => 0x0019,
|
||||
ScanCode::Q => 0x0010,
|
||||
ScanCode::R => 0x0013,
|
||||
ScanCode::S => 0x001f,
|
||||
ScanCode::T => 0x0014,
|
||||
ScanCode::U => 0x0016,
|
||||
ScanCode::V => 0x002f,
|
||||
ScanCode::W => 0x0011,
|
||||
ScanCode::X => 0x002d,
|
||||
ScanCode::Y => 0x0015,
|
||||
ScanCode::Z => 0x002c,
|
||||
ScanCode::Digit0 => 0x000b,
|
||||
ScanCode::Digit1 => 0x0002,
|
||||
ScanCode::Digit2 => 0x0003,
|
||||
ScanCode::Digit3 => 0x0004,
|
||||
ScanCode::Digit4 => 0x0005,
|
||||
ScanCode::Digit5 => 0x0006,
|
||||
ScanCode::Digit6 => 0x0007,
|
||||
ScanCode::Digit7 => 0x0008,
|
||||
ScanCode::Digit8 => 0x0009,
|
||||
ScanCode::Digit9 => 0x000a,
|
||||
ScanCode::Backquote => 0x0029,
|
||||
ScanCode::Minus => 0x000c,
|
||||
ScanCode::Equal => 0x000d,
|
||||
ScanCode::BracketLeft => 0x001a,
|
||||
ScanCode::BracketRight => 0x001b,
|
||||
ScanCode::Backslash => 0x002b,
|
||||
ScanCode::Semicolon => 0x0027,
|
||||
ScanCode::Quote => 0x0028,
|
||||
ScanCode::Comma => 0x0033,
|
||||
ScanCode::Period => 0x0034,
|
||||
ScanCode::Slash => 0x0035,
|
||||
_ => anyhow::bail!("Unsupported scan code: {:?}", gpui_scan_code),
|
||||
};
|
||||
let virtual_key = unsafe { MapVirtualKeyW(scan_code, MAPVK_VSC_TO_VK) };
|
||||
if virtual_key == 0 {
|
||||
anyhow::bail!(
|
||||
"Failed to get virtual key from scan code: {:?}, {}",
|
||||
gpui_scan_code,
|
||||
scan_code
|
||||
);
|
||||
}
|
||||
Ok((scan_code, VIRTUAL_KEY(virtual_key as u16)))
|
||||
}
|
||||
|
||||
@@ -310,10 +310,6 @@ impl Platform for WindowsPlatform {
|
||||
self.text_system.clone()
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper> {
|
||||
Box::new(WindowsKeyboardMapper::new())
|
||||
}
|
||||
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
|
||||
Box::new(
|
||||
WindowsKeyboardLayout::new()
|
||||
|
||||
@@ -765,8 +765,6 @@ pub enum ShowWhitespaceSetting {
|
||||
/// - It is adjacent to an edge (start or end)
|
||||
/// - It is adjacent to a whitespace (left or right)
|
||||
Boundary,
|
||||
/// Draw whitespaces only after non-whitespace characters.
|
||||
Trailing,
|
||||
}
|
||||
|
||||
/// Controls which formatter should be used when formatting code.
|
||||
@@ -1454,8 +1452,7 @@ impl settings::Settings for AllLanguageSettings {
|
||||
vscode.bool_setting("editor.inlineSuggest.enabled", &mut d.show_edit_predictions);
|
||||
vscode.enum_setting("editor.renderWhitespace", &mut d.show_whitespaces, |s| {
|
||||
Some(match s {
|
||||
"boundary" => ShowWhitespaceSetting::Boundary,
|
||||
"trailing" => ShowWhitespaceSetting::Trailing,
|
||||
"boundary" | "trailing" => ShowWhitespaceSetting::Boundary,
|
||||
"selection" => ShowWhitespaceSetting::Selection,
|
||||
"all" => ShowWhitespaceSetting::All,
|
||||
_ => ShowWhitespaceSetting::None,
|
||||
|
||||
@@ -185,7 +185,6 @@ impl LanguageModel for FakeLanguageModel {
|
||||
'static,
|
||||
Result<
|
||||
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
|
||||
@@ -22,7 +22,6 @@ use std::fmt;
|
||||
use std::ops::{Add, Sub};
|
||||
use std::str::FromStr as _;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use util::serde::is_default;
|
||||
use zed_llm_client::{
|
||||
@@ -75,8 +74,6 @@ pub enum LanguageModelCompletionEvent {
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum LanguageModelCompletionError {
|
||||
#[error("rate limit exceeded, retry after {0:?}")]
|
||||
RateLimit(Duration),
|
||||
#[error("received bad input JSON")]
|
||||
BadInputJson {
|
||||
id: LanguageModelToolUseId,
|
||||
@@ -273,7 +270,6 @@ pub trait LanguageModel: Send + Sync {
|
||||
'static,
|
||||
Result<
|
||||
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
>;
|
||||
|
||||
@@ -281,7 +277,7 @@ pub trait LanguageModel: Send + Sync {
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AsyncApp,
|
||||
) -> BoxFuture<'static, Result<LanguageModelTextStream, LanguageModelCompletionError>> {
|
||||
) -> BoxFuture<'static, Result<LanguageModelTextStream>> {
|
||||
let future = self.stream_completion(request, cx);
|
||||
|
||||
async move {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use futures::Stream;
|
||||
use smol::lock::{Semaphore, SemaphoreGuardArc};
|
||||
use std::{
|
||||
@@ -7,8 +8,6 @@ use std::{
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use crate::LanguageModelCompletionError;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RateLimiter {
|
||||
semaphore: Arc<Semaphore>,
|
||||
@@ -37,12 +36,9 @@ impl RateLimiter {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run<'a, Fut, T>(
|
||||
&self,
|
||||
future: Fut,
|
||||
) -> impl 'a + Future<Output = Result<T, LanguageModelCompletionError>>
|
||||
pub fn run<'a, Fut, T>(&self, future: Fut) -> impl 'a + Future<Output = Result<T>>
|
||||
where
|
||||
Fut: 'a + Future<Output = Result<T, LanguageModelCompletionError>>,
|
||||
Fut: 'a + Future<Output = Result<T>>,
|
||||
{
|
||||
let guard = self.semaphore.acquire_arc();
|
||||
async move {
|
||||
@@ -56,12 +52,9 @@ impl RateLimiter {
|
||||
pub fn stream<'a, Fut, T>(
|
||||
&self,
|
||||
future: Fut,
|
||||
) -> impl 'a
|
||||
+ Future<
|
||||
Output = Result<impl Stream<Item = T::Item> + use<Fut, T>, LanguageModelCompletionError>,
|
||||
>
|
||||
) -> impl 'a + Future<Output = Result<impl Stream<Item = T::Item> + use<Fut, T>>>
|
||||
where
|
||||
Fut: 'a + Future<Output = Result<T, LanguageModelCompletionError>>,
|
||||
Fut: 'a + Future<Output = Result<T>>,
|
||||
T: Stream,
|
||||
{
|
||||
let guard = self.semaphore.acquire_arc();
|
||||
|
||||
@@ -387,34 +387,22 @@ impl AnthropicModel {
|
||||
&self,
|
||||
request: anthropic::Request,
|
||||
cx: &AsyncApp,
|
||||
) -> BoxFuture<
|
||||
'static,
|
||||
Result<
|
||||
BoxStream<'static, Result<anthropic::Event, AnthropicError>>,
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<anthropic::Event, AnthropicError>>>>
|
||||
{
|
||||
let http_client = self.http_client.clone();
|
||||
|
||||
let Ok((api_key, api_url)) = cx.read_entity(&self.state, |state, cx| {
|
||||
let settings = &AllLanguageModelSettings::get_global(cx).anthropic;
|
||||
(state.api_key.clone(), settings.api_url.clone())
|
||||
}) else {
|
||||
return futures::future::ready(Err(anyhow!("App state dropped").into())).boxed();
|
||||
return futures::future::ready(Err(anyhow!("App state dropped"))).boxed();
|
||||
};
|
||||
|
||||
async move {
|
||||
let api_key = api_key.context("Missing Anthropic API Key")?;
|
||||
let request =
|
||||
anthropic::stream_completion(http_client.as_ref(), &api_url, &api_key, request);
|
||||
request.await.map_err(|err| match err {
|
||||
AnthropicError::RateLimit(duration) => {
|
||||
LanguageModelCompletionError::RateLimit(duration)
|
||||
}
|
||||
err @ (AnthropicError::ApiError(..) | AnthropicError::Other(..)) => {
|
||||
LanguageModelCompletionError::Other(anthropic_err_to_anyhow(err))
|
||||
}
|
||||
})
|
||||
request.await.context("failed to stream completion")
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
@@ -485,7 +473,6 @@ impl LanguageModel for AnthropicModel {
|
||||
'static,
|
||||
Result<
|
||||
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let request = into_anthropic(
|
||||
@@ -497,7 +484,12 @@ impl LanguageModel for AnthropicModel {
|
||||
);
|
||||
let request = self.stream_completion(request, cx);
|
||||
let future = self.request_limiter.stream(async move {
|
||||
let response = request.await?;
|
||||
let response = request
|
||||
.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))
|
||||
});
|
||||
async move { Ok(future.await?.boxed()) }.boxed()
|
||||
|
||||
@@ -527,7 +527,6 @@ impl LanguageModel for BedrockModel {
|
||||
'static,
|
||||
Result<
|
||||
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let Ok(region) = cx.read_entity(&self.state, |state, _cx| {
|
||||
@@ -540,13 +539,16 @@ impl LanguageModel for BedrockModel {
|
||||
.or(settings_region)
|
||||
.unwrap_or(String::from("us-east-1"))
|
||||
}) else {
|
||||
return async move { Err(anyhow::anyhow!("App State Dropped").into()) }.boxed();
|
||||
return async move {
|
||||
anyhow::bail!("App State Dropped");
|
||||
}
|
||||
.boxed();
|
||||
};
|
||||
|
||||
let model_id = match self.model.cross_region_inference_id(®ion) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return async move { Err(e.into()) }.boxed();
|
||||
return async move { Err(e) }.boxed();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -558,7 +560,7 @@ impl LanguageModel for BedrockModel {
|
||||
self.model.mode(),
|
||||
) {
|
||||
Ok(request) => request,
|
||||
Err(err) => return futures::future::ready(Err(err.into())).boxed(),
|
||||
Err(err) => return futures::future::ready(Err(err)).boxed(),
|
||||
};
|
||||
|
||||
let owned_handle = self.handler.clone();
|
||||
|
||||
@@ -807,7 +807,6 @@ impl LanguageModel for CloudLanguageModel {
|
||||
'static,
|
||||
Result<
|
||||
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let thread_id = request.thread_id.clone();
|
||||
@@ -849,8 +848,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
mode,
|
||||
provider: zed_llm_client::LanguageModelProvider::Anthropic,
|
||||
model: request.model.clone(),
|
||||
provider_request: serde_json::to_value(&request)
|
||||
.map_err(|e| anyhow!(e))?,
|
||||
provider_request: serde_json::to_value(&request)?,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -886,7 +884,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
let client = self.client.clone();
|
||||
let model = match open_ai::Model::from_id(&self.model.id.0) {
|
||||
Ok(model) => model,
|
||||
Err(err) => return async move { Err(anyhow!(err).into()) }.boxed(),
|
||||
Err(err) => return async move { Err(anyhow!(err)) }.boxed(),
|
||||
};
|
||||
let request = into_open_ai(request, &model, None);
|
||||
let llm_api_token = self.llm_api_token.clone();
|
||||
@@ -907,8 +905,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
mode,
|
||||
provider: zed_llm_client::LanguageModelProvider::OpenAi,
|
||||
model: request.model.clone(),
|
||||
provider_request: serde_json::to_value(&request)
|
||||
.map_err(|e| anyhow!(e))?,
|
||||
provider_request: serde_json::to_value(&request)?,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -947,8 +944,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
mode,
|
||||
provider: zed_llm_client::LanguageModelProvider::Google,
|
||||
model: request.model.model_id.clone(),
|
||||
provider_request: serde_json::to_value(&request)
|
||||
.map_err(|e| anyhow!(e))?,
|
||||
provider_request: serde_json::to_value(&request)?,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -265,15 +265,13 @@ impl LanguageModel for CopilotChatLanguageModel {
|
||||
'static,
|
||||
Result<
|
||||
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
if let Some(message) = request.messages.last() {
|
||||
if message.contents_empty() {
|
||||
const EMPTY_PROMPT_MSG: &str =
|
||||
"Empty prompts aren't allowed. Please provide a non-empty prompt.";
|
||||
return futures::future::ready(Err(anyhow::anyhow!(EMPTY_PROMPT_MSG).into()))
|
||||
.boxed();
|
||||
return futures::future::ready(Err(anyhow::anyhow!(EMPTY_PROMPT_MSG))).boxed();
|
||||
}
|
||||
|
||||
// Copilot Chat has a restriction that the final message must be from the user.
|
||||
@@ -281,13 +279,13 @@ impl LanguageModel for CopilotChatLanguageModel {
|
||||
// and provide a more helpful error message.
|
||||
if !matches!(message.role, Role::User) {
|
||||
const USER_ROLE_MSG: &str = "The final message must be from the user. To provide a system prompt, you must provide the system prompt followed by a user prompt.";
|
||||
return futures::future::ready(Err(anyhow::anyhow!(USER_ROLE_MSG).into())).boxed();
|
||||
return futures::future::ready(Err(anyhow::anyhow!(USER_ROLE_MSG))).boxed();
|
||||
}
|
||||
}
|
||||
|
||||
let copilot_request = match into_copilot_chat(&self.model, request) {
|
||||
Ok(request) => request,
|
||||
Err(err) => return futures::future::ready(Err(err.into())).boxed(),
|
||||
Err(err) => return futures::future::ready(Err(err)).boxed(),
|
||||
};
|
||||
let is_streaming = copilot_request.stream;
|
||||
|
||||
|
||||
@@ -348,7 +348,6 @@ impl LanguageModel for DeepSeekLanguageModel {
|
||||
'static,
|
||||
Result<
|
||||
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let request = into_deepseek(request, &self.model, self.max_output_tokens());
|
||||
|
||||
@@ -409,7 +409,6 @@ impl LanguageModel for GoogleLanguageModel {
|
||||
'static,
|
||||
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
||||
>,
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let request = into_google(
|
||||
|
||||
@@ -420,7 +420,6 @@ impl LanguageModel for LmStudioLanguageModel {
|
||||
'static,
|
||||
Result<
|
||||
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let request = self.to_lmstudio_request(request);
|
||||
|
||||
@@ -364,7 +364,6 @@ impl LanguageModel for MistralLanguageModel {
|
||||
'static,
|
||||
Result<
|
||||
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let request = into_mistral(
|
||||
|
||||
@@ -406,7 +406,6 @@ impl LanguageModel for OllamaLanguageModel {
|
||||
'static,
|
||||
Result<
|
||||
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let request = self.to_ollama_request(request);
|
||||
@@ -416,7 +415,7 @@ impl LanguageModel for OllamaLanguageModel {
|
||||
let settings = &AllLanguageModelSettings::get_global(cx).ollama;
|
||||
settings.api_url.clone()
|
||||
}) else {
|
||||
return futures::future::ready(Err(anyhow!("App state dropped").into())).boxed();
|
||||
return futures::future::ready(Err(anyhow!("App state dropped"))).boxed();
|
||||
};
|
||||
|
||||
let future = self.request_limiter.stream(async move {
|
||||
|
||||
@@ -339,7 +339,6 @@ impl LanguageModel for OpenAiLanguageModel {
|
||||
'static,
|
||||
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
||||
>,
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let request = into_open_ai(request, &self.model, self.max_output_tokens());
|
||||
|
||||
@@ -367,7 +367,6 @@ impl LanguageModel for OpenRouterLanguageModel {
|
||||
'static,
|
||||
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
||||
>,
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let request = into_open_router(request, &self.model, self.max_output_tokens());
|
||||
|
||||
@@ -82,6 +82,7 @@ text.workspace = true
|
||||
toml.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
which.workspace = true
|
||||
worktree.workspace = true
|
||||
zlog.workspace = true
|
||||
|
||||
@@ -1,88 +1,17 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use collections::FxHashMap;
|
||||
use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
|
||||
use gpui::SharedString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use task::{DebugScenario, SpawnInTerminal, TaskTemplate};
|
||||
use std::path::PathBuf;
|
||||
use task::{
|
||||
BuildTaskDefinition, DebugScenario, RevealStrategy, RevealTarget, Shell, SpawnInTerminal,
|
||||
TaskTemplate,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) struct GoLocator;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DelveLaunchRequest {
|
||||
request: String,
|
||||
mode: String,
|
||||
program: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cwd: Option<String>,
|
||||
args: Vec<String>,
|
||||
build_flags: Vec<String>,
|
||||
env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
fn is_debug_flag(arg: &str) -> Option<bool> {
|
||||
let mut part = if let Some(suffix) = arg.strip_prefix("test.") {
|
||||
suffix
|
||||
} else {
|
||||
arg
|
||||
};
|
||||
let mut might_have_arg = true;
|
||||
if let Some(idx) = part.find('=') {
|
||||
might_have_arg = false;
|
||||
part = &part[..idx];
|
||||
}
|
||||
match part {
|
||||
"benchmem" | "failfast" | "fullpath" | "fuzzworker" | "json" | "short" | "v"
|
||||
| "paniconexit0" => Some(false),
|
||||
"bench"
|
||||
| "benchtime"
|
||||
| "blockprofile"
|
||||
| "blockprofilerate"
|
||||
| "count"
|
||||
| "coverprofile"
|
||||
| "cpu"
|
||||
| "cpuprofile"
|
||||
| "fuzz"
|
||||
| "fuzzcachedir"
|
||||
| "fuzzminimizetime"
|
||||
| "fuzztime"
|
||||
| "gocoverdir"
|
||||
| "list"
|
||||
| "memprofile"
|
||||
| "memprofilerate"
|
||||
| "mutexprofile"
|
||||
| "mutexprofilefraction"
|
||||
| "outputdir"
|
||||
| "parallel"
|
||||
| "run"
|
||||
| "shuffle"
|
||||
| "skip"
|
||||
| "testlogfile"
|
||||
| "timeout"
|
||||
| "trace" => Some(might_have_arg),
|
||||
_ if arg.starts_with("test.") => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_build_flag(mut arg: &str) -> Option<bool> {
|
||||
let mut might_have_arg = true;
|
||||
if let Some(idx) = arg.find('=') {
|
||||
might_have_arg = false;
|
||||
arg = &arg[..idx];
|
||||
}
|
||||
match arg {
|
||||
"a" | "n" | "race" | "msan" | "asan" | "cover" | "work" | "x" | "v" | "buildvcs"
|
||||
| "json" | "linkshared" | "modcacherw" | "trimpath" => Some(false),
|
||||
|
||||
"p" | "covermode" | "coverpkg" | "asmflags" | "buildmode" | "compiler" | "gccgoflags"
|
||||
| "gcflags" | "installsuffix" | "ldflags" | "mod" | "modfile" | "overlay" | "pgo"
|
||||
| "pkgdir" | "tags" | "toolexec" => Some(might_have_arg),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DapLocator for GoLocator {
|
||||
fn name(&self) -> SharedString {
|
||||
@@ -103,121 +32,78 @@ impl DapLocator for GoLocator {
|
||||
|
||||
match go_action.as_str() {
|
||||
"test" => {
|
||||
let mut program = ".".to_string();
|
||||
let mut args = Vec::default();
|
||||
let mut build_flags = Vec::default();
|
||||
let binary_path = format!("__debug_{}", Uuid::new_v4().simple());
|
||||
|
||||
let mut all_args_are_test = false;
|
||||
let mut next_arg_is_test = false;
|
||||
let mut next_arg_is_build = false;
|
||||
let mut seen_pkg = false;
|
||||
let mut seen_v = false;
|
||||
|
||||
for arg in build_config.args.iter().skip(1) {
|
||||
if all_args_are_test || next_arg_is_test {
|
||||
// HACK: tasks assume that they are run in a shell context,
|
||||
// so the -run regex has escaped specials. Delve correctly
|
||||
// handles escaping, so we undo that here.
|
||||
if arg.starts_with("\\^") && arg.ends_with("\\$") {
|
||||
let mut arg = arg[1..arg.len() - 2].to_string();
|
||||
arg.push('$');
|
||||
args.push(arg);
|
||||
} else {
|
||||
args.push(arg.clone());
|
||||
}
|
||||
next_arg_is_test = false;
|
||||
} else if next_arg_is_build {
|
||||
build_flags.push(arg.clone());
|
||||
next_arg_is_build = false;
|
||||
} else if arg.starts_with('-') {
|
||||
let flag = arg.trim_start_matches('-');
|
||||
if flag == "args" {
|
||||
all_args_are_test = true;
|
||||
} else if let Some(has_arg) = is_debug_flag(flag) {
|
||||
if flag == "v" || flag == "test.v" {
|
||||
seen_v = true;
|
||||
}
|
||||
if flag.starts_with("test.") {
|
||||
args.push(arg.clone());
|
||||
} else {
|
||||
args.push(format!("-test.{flag}"))
|
||||
}
|
||||
next_arg_is_test = has_arg;
|
||||
} else if let Some(has_arg) = is_build_flag(flag) {
|
||||
build_flags.push(arg.clone());
|
||||
next_arg_is_build = has_arg;
|
||||
}
|
||||
} else if !seen_pkg {
|
||||
program = arg.clone();
|
||||
seen_pkg = true;
|
||||
} else {
|
||||
args.push(arg.clone());
|
||||
}
|
||||
}
|
||||
if !seen_v {
|
||||
args.push("-test.v".to_string());
|
||||
}
|
||||
|
||||
let config: serde_json::Value = serde_json::to_value(DelveLaunchRequest {
|
||||
request: "launch".to_string(),
|
||||
mode: "test".to_string(),
|
||||
program,
|
||||
args: args,
|
||||
build_flags,
|
||||
cwd: build_config.cwd.clone(),
|
||||
let build_task = TaskTemplate {
|
||||
label: "go test debug".into(),
|
||||
command: "go".into(),
|
||||
args: vec![
|
||||
"test".into(),
|
||||
"-c".into(),
|
||||
"-gcflags \"all=-N -l\"".into(),
|
||||
"-o".into(),
|
||||
binary_path,
|
||||
],
|
||||
env: build_config.env.clone(),
|
||||
})
|
||||
.unwrap();
|
||||
cwd: build_config.cwd.clone(),
|
||||
use_new_terminal: false,
|
||||
allow_concurrent_runs: false,
|
||||
reveal: RevealStrategy::Always,
|
||||
reveal_target: RevealTarget::Dock,
|
||||
hide: task::HideStrategy::Never,
|
||||
shell: Shell::System,
|
||||
tags: vec![],
|
||||
show_summary: true,
|
||||
show_command: true,
|
||||
};
|
||||
|
||||
Some(DebugScenario {
|
||||
label: resolved_label.to_string().into(),
|
||||
adapter: adapter.0,
|
||||
build: None,
|
||||
config: config,
|
||||
build: Some(BuildTaskDefinition::Template {
|
||||
task_template: build_task,
|
||||
locator_name: Some(self.name()),
|
||||
}),
|
||||
config: serde_json::Value::Null,
|
||||
tcp_connection: None,
|
||||
})
|
||||
}
|
||||
"run" => {
|
||||
let mut next_arg_is_build = false;
|
||||
let mut seen_pkg = false;
|
||||
let program = build_config
|
||||
.args
|
||||
.get(1)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| ".".to_string());
|
||||
|
||||
let mut program = ".".to_string();
|
||||
let mut args = Vec::default();
|
||||
let mut build_flags = Vec::default();
|
||||
|
||||
for arg in build_config.args.iter().skip(1) {
|
||||
if seen_pkg {
|
||||
args.push(arg.clone())
|
||||
} else if next_arg_is_build {
|
||||
build_flags.push(arg.clone());
|
||||
next_arg_is_build = false;
|
||||
} else if arg.starts_with("-") {
|
||||
if let Some(has_arg) = is_build_flag(arg.trim_start_matches("-")) {
|
||||
next_arg_is_build = has_arg;
|
||||
}
|
||||
build_flags.push(arg.clone())
|
||||
} else {
|
||||
program = arg.to_string();
|
||||
seen_pkg = true;
|
||||
}
|
||||
}
|
||||
|
||||
let config: serde_json::Value = serde_json::to_value(DelveLaunchRequest {
|
||||
cwd: build_config.cwd.clone(),
|
||||
let build_task = TaskTemplate {
|
||||
label: "go build debug".into(),
|
||||
command: "go".into(),
|
||||
args: vec![
|
||||
"build".into(),
|
||||
"-gcflags \"all=-N -l\"".into(),
|
||||
program.clone(),
|
||||
],
|
||||
env: build_config.env.clone(),
|
||||
request: "launch".to_string(),
|
||||
mode: "debug".to_string(),
|
||||
program,
|
||||
args: args,
|
||||
build_flags,
|
||||
})
|
||||
.unwrap();
|
||||
cwd: build_config.cwd.clone(),
|
||||
use_new_terminal: false,
|
||||
allow_concurrent_runs: false,
|
||||
reveal: RevealStrategy::Always,
|
||||
reveal_target: RevealTarget::Dock,
|
||||
hide: task::HideStrategy::Never,
|
||||
shell: Shell::System,
|
||||
tags: vec![],
|
||||
show_summary: true,
|
||||
show_command: true,
|
||||
};
|
||||
|
||||
Some(DebugScenario {
|
||||
label: resolved_label.to_string().into(),
|
||||
adapter: adapter.0,
|
||||
build: None,
|
||||
config,
|
||||
build: Some(BuildTaskDefinition::Template {
|
||||
task_template: build_task,
|
||||
locator_name: Some(self.name()),
|
||||
}),
|
||||
config: serde_json::Value::Null,
|
||||
tcp_connection: None,
|
||||
})
|
||||
}
|
||||
@@ -225,15 +111,113 @@ impl DapLocator for GoLocator {
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(&self, _build_config: SpawnInTerminal) -> Result<DebugRequest> {
|
||||
unreachable!()
|
||||
async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest> {
|
||||
if build_config.args.is_empty() {
|
||||
return Err(anyhow::anyhow!("Invalid Go command"));
|
||||
}
|
||||
|
||||
let go_action = &build_config.args[0];
|
||||
let cwd = build_config
|
||||
.cwd
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| ".".to_string());
|
||||
|
||||
let mut env = FxHashMap::default();
|
||||
for (key, value) in &build_config.env {
|
||||
env.insert(key.clone(), value.clone());
|
||||
}
|
||||
|
||||
match go_action.as_str() {
|
||||
"test" => {
|
||||
let binary_arg = build_config
|
||||
.args
|
||||
.get(4)
|
||||
.ok_or_else(|| anyhow::anyhow!("can't locate debug binary"))?;
|
||||
|
||||
let program = PathBuf::from(&cwd)
|
||||
.join(binary_arg)
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
|
||||
Ok(DebugRequest::Launch(task::LaunchRequest {
|
||||
program,
|
||||
cwd: Some(PathBuf::from(&cwd)),
|
||||
args: vec!["-test.v".into(), "-test.run=${ZED_SYMBOL}".into()],
|
||||
env,
|
||||
}))
|
||||
}
|
||||
"build" => {
|
||||
let package = build_config
|
||||
.args
|
||||
.get(2)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| ".".to_string());
|
||||
|
||||
Ok(DebugRequest::Launch(task::LaunchRequest {
|
||||
program: package,
|
||||
cwd: Some(PathBuf::from(&cwd)),
|
||||
args: vec![],
|
||||
env,
|
||||
}))
|
||||
}
|
||||
_ => Err(anyhow::anyhow!("Unsupported Go command: {}", go_action)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskTemplate};
|
||||
use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskId, TaskTemplate};
|
||||
|
||||
#[test]
|
||||
fn test_create_scenario_for_go_run() {
|
||||
let locator = GoLocator;
|
||||
let task = TaskTemplate {
|
||||
label: "go run main.go".into(),
|
||||
command: "go".into(),
|
||||
args: vec!["run".into(), "main.go".into()],
|
||||
env: Default::default(),
|
||||
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
|
||||
use_new_terminal: false,
|
||||
allow_concurrent_runs: false,
|
||||
reveal: RevealStrategy::Always,
|
||||
reveal_target: RevealTarget::Dock,
|
||||
hide: HideStrategy::Never,
|
||||
shell: Shell::System,
|
||||
tags: vec![],
|
||||
show_summary: true,
|
||||
show_command: true,
|
||||
};
|
||||
|
||||
let scenario =
|
||||
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
|
||||
|
||||
assert!(scenario.is_some());
|
||||
let scenario = scenario.unwrap();
|
||||
assert_eq!(scenario.adapter, "Delve");
|
||||
assert_eq!(scenario.label, "test label");
|
||||
assert!(scenario.build.is_some());
|
||||
|
||||
if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
|
||||
assert_eq!(task_template.command, "go");
|
||||
assert!(task_template.args.contains(&"build".into()));
|
||||
assert!(
|
||||
task_template
|
||||
.args
|
||||
.contains(&"-gcflags \"all=-N -l\"".into())
|
||||
);
|
||||
assert!(task_template.args.contains(&"main.go".into()));
|
||||
} else {
|
||||
panic!("Expected BuildTaskDefinition::Template");
|
||||
}
|
||||
|
||||
assert!(
|
||||
scenario.config.is_null(),
|
||||
"Initial config should be null to ensure it's invalid"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_scenario_for_go_build() {
|
||||
@@ -292,106 +276,99 @@ mod tests {
|
||||
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
|
||||
assert!(scenario.is_none());
|
||||
}
|
||||
#[test]
|
||||
fn test_go_locator_run() {
|
||||
let locator = GoLocator;
|
||||
let delve = DebugAdapterName("Delve".into());
|
||||
|
||||
#[test]
|
||||
fn test_create_scenario_for_go_test() {
|
||||
let locator = GoLocator;
|
||||
let task = TaskTemplate {
|
||||
label: "go run with flags".into(),
|
||||
label: "go test".into(),
|
||||
command: "go".into(),
|
||||
args: vec![
|
||||
"run".to_string(),
|
||||
"-race".to_string(),
|
||||
"-ldflags".to_string(),
|
||||
"-X main.version=1.0".to_string(),
|
||||
"./cmd/myapp".to_string(),
|
||||
"--config".to_string(),
|
||||
"production.yaml".to_string(),
|
||||
"--verbose".to_string(),
|
||||
],
|
||||
env: {
|
||||
let mut env = HashMap::default();
|
||||
env.insert("GO_ENV".to_string(), "production".to_string());
|
||||
env
|
||||
},
|
||||
cwd: Some("/project/root".into()),
|
||||
..Default::default()
|
||||
args: vec!["test".into(), ".".into()],
|
||||
env: Default::default(),
|
||||
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
|
||||
use_new_terminal: false,
|
||||
allow_concurrent_runs: false,
|
||||
reveal: RevealStrategy::Always,
|
||||
reveal_target: RevealTarget::Dock,
|
||||
hide: HideStrategy::Never,
|
||||
shell: Shell::System,
|
||||
tags: vec![],
|
||||
show_summary: true,
|
||||
show_command: true,
|
||||
};
|
||||
|
||||
let scenario = locator
|
||||
.create_scenario(&task, "test run label", delve)
|
||||
.unwrap();
|
||||
let scenario =
|
||||
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
|
||||
|
||||
let config: DelveLaunchRequest = serde_json::from_value(scenario.config).unwrap();
|
||||
assert!(scenario.is_some());
|
||||
let scenario = scenario.unwrap();
|
||||
assert_eq!(scenario.adapter, "Delve");
|
||||
assert_eq!(scenario.label, "test label");
|
||||
assert!(scenario.build.is_some());
|
||||
|
||||
assert_eq!(
|
||||
config,
|
||||
DelveLaunchRequest {
|
||||
request: "launch".to_string(),
|
||||
mode: "debug".to_string(),
|
||||
program: "./cmd/myapp".to_string(),
|
||||
build_flags: vec![
|
||||
"-race".to_string(),
|
||||
"-ldflags".to_string(),
|
||||
"-X main.version=1.0".to_string()
|
||||
],
|
||||
args: vec![
|
||||
"--config".to_string(),
|
||||
"production.yaml".to_string(),
|
||||
"--verbose".to_string(),
|
||||
],
|
||||
env: {
|
||||
let mut env = HashMap::default();
|
||||
env.insert("GO_ENV".to_string(), "production".to_string());
|
||||
env
|
||||
},
|
||||
cwd: Some("/project/root".to_string()),
|
||||
}
|
||||
if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
|
||||
assert_eq!(task_template.command, "go");
|
||||
assert!(task_template.args.contains(&"test".into()));
|
||||
assert!(task_template.args.contains(&"-c".into()));
|
||||
assert!(
|
||||
task_template
|
||||
.args
|
||||
.contains(&"-gcflags \"all=-N -l\"".into())
|
||||
);
|
||||
assert!(task_template.args.contains(&"-o".into()));
|
||||
assert!(
|
||||
task_template
|
||||
.args
|
||||
.iter()
|
||||
.any(|arg| arg.starts_with("__debug_"))
|
||||
);
|
||||
} else {
|
||||
panic!("Expected BuildTaskDefinition::Template");
|
||||
}
|
||||
|
||||
assert!(
|
||||
scenario.config.is_null(),
|
||||
"Initial config should be null to ensure it's invalid"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_go_locator_test() {
|
||||
fn test_create_scenario_for_go_test_with_cwd_binary() {
|
||||
let locator = GoLocator;
|
||||
let delve = DebugAdapterName("Delve".into());
|
||||
|
||||
// Test with tags and run flag
|
||||
let task_with_tags = TaskTemplate {
|
||||
label: "test".into(),
|
||||
let task = TaskTemplate {
|
||||
label: "go test".into(),
|
||||
command: "go".into(),
|
||||
args: vec![
|
||||
"test".to_string(),
|
||||
"-tags".to_string(),
|
||||
"integration,unit".to_string(),
|
||||
"-run".to_string(),
|
||||
"Foo".to_string(),
|
||||
".".to_string(),
|
||||
],
|
||||
..Default::default()
|
||||
args: vec!["test".into(), ".".into()],
|
||||
env: Default::default(),
|
||||
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
|
||||
use_new_terminal: false,
|
||||
allow_concurrent_runs: false,
|
||||
reveal: RevealStrategy::Always,
|
||||
reveal_target: RevealTarget::Dock,
|
||||
hide: HideStrategy::Never,
|
||||
shell: Shell::System,
|
||||
tags: vec![],
|
||||
show_summary: true,
|
||||
show_command: true,
|
||||
};
|
||||
let result = locator
|
||||
.create_scenario(&task_with_tags, "", delve.clone())
|
||||
.unwrap();
|
||||
|
||||
let config: DelveLaunchRequest = serde_json::from_value(result.config).unwrap();
|
||||
let scenario =
|
||||
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
|
||||
|
||||
assert_eq!(
|
||||
config,
|
||||
DelveLaunchRequest {
|
||||
request: "launch".to_string(),
|
||||
mode: "test".to_string(),
|
||||
program: ".".to_string(),
|
||||
build_flags: vec!["-tags".to_string(), "integration,unit".to_string(),],
|
||||
args: vec![
|
||||
"-test.run".to_string(),
|
||||
"Foo".to_string(),
|
||||
"-test.v".to_string()
|
||||
],
|
||||
env: HashMap::default(),
|
||||
cwd: None,
|
||||
}
|
||||
);
|
||||
assert!(scenario.is_some());
|
||||
let scenario = scenario.unwrap();
|
||||
|
||||
if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
|
||||
assert!(
|
||||
task_template
|
||||
.args
|
||||
.iter()
|
||||
.any(|arg| arg.starts_with("__debug_"))
|
||||
);
|
||||
} else {
|
||||
panic!("Expected BuildTaskDefinition::Template");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -418,4 +395,42 @@ mod tests {
|
||||
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
|
||||
assert!(scenario.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_go_test_missing_binary_path() {
|
||||
let locator = GoLocator;
|
||||
let build_config = SpawnInTerminal {
|
||||
id: TaskId("test_task".to_string()),
|
||||
full_label: "go test".to_string(),
|
||||
label: "go test".to_string(),
|
||||
command: "go".into(),
|
||||
args: vec![
|
||||
"test".into(),
|
||||
"-c".into(),
|
||||
"-gcflags \"all=-N -l\"".into(),
|
||||
"-o".into(),
|
||||
], // Missing the binary path (arg 4)
|
||||
command_label: "go test -c -gcflags \"all=-N -l\" -o".to_string(),
|
||||
env: Default::default(),
|
||||
cwd: Some(PathBuf::from("/test/path")),
|
||||
use_new_terminal: false,
|
||||
allow_concurrent_runs: false,
|
||||
reveal: RevealStrategy::Always,
|
||||
reveal_target: RevealTarget::Dock,
|
||||
hide: HideStrategy::Never,
|
||||
shell: Shell::System,
|
||||
show_summary: true,
|
||||
show_command: true,
|
||||
show_rerun: true,
|
||||
};
|
||||
|
||||
let result = futures::executor::block_on(locator.run(build_config));
|
||||
assert!(result.is_err());
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("can't locate debug binary")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ async fn load_shell_environment(
|
||||
use util::shell_env;
|
||||
|
||||
let dir_ = dir.to_owned();
|
||||
let mut envs = match smol::unblock(move || shell_env::capture(&dir_)).await {
|
||||
let mut envs = match smol::unblock(move || shell_env::capture(Some(dir_))).await {
|
||||
Ok(envs) => envs,
|
||||
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())?;
|
||||
Ok(Self {
|
||||
previous_result_id: lsp_store
|
||||
.update(&mut cx, |lsp_store, cx| lsp_store.result_id(buffer_id, cx))?,
|
||||
.update(&mut cx, |lsp_store, _| lsp_store.result_id(buffer_id))?,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ pub struct LocalLspStore {
|
||||
_subscription: gpui::Subscription,
|
||||
lsp_tree: Entity<LanguageServerTree>,
|
||||
registered_buffers: HashMap<BufferId, usize>,
|
||||
buffer_pull_diagnostics_result_ids: HashMap<PathBuf, Option<String>>,
|
||||
buffer_pull_diagnostics_result_ids: HashMap<BufferId, Option<String>>,
|
||||
}
|
||||
|
||||
impl LocalLspStore {
|
||||
@@ -2295,11 +2295,8 @@ impl LocalLspStore {
|
||||
|
||||
let set = DiagnosticSet::new(sanitized_diagnostics, &snapshot);
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
if let Some(abs_path) = File::from_dyn(buffer.file()).map(|f| f.abs_path(cx)) {
|
||||
self.buffer_pull_diagnostics_result_ids
|
||||
.insert(abs_path, result_id);
|
||||
}
|
||||
|
||||
self.buffer_pull_diagnostics_result_ids
|
||||
.insert(buffer.remote_id(), result_id);
|
||||
buffer.update_diagnostics(server_id, set, cx)
|
||||
});
|
||||
|
||||
@@ -3795,16 +3792,8 @@ impl LspStore {
|
||||
}
|
||||
}
|
||||
BufferStoreEvent::BufferDropped(buffer_id) => {
|
||||
let abs_path = self
|
||||
.buffer_store
|
||||
.read(cx)
|
||||
.get(*buffer_id)
|
||||
.and_then(|b| File::from_dyn(b.read(cx).file()))
|
||||
.map(|f| f.abs_path(cx));
|
||||
if let Some(local) = self.as_local_mut() {
|
||||
if let Some(abs_path) = abs_path {
|
||||
local.buffer_pull_diagnostics_result_ids.remove(&abs_path);
|
||||
}
|
||||
local.buffer_pull_diagnostics_result_ids.remove(buffer_id);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -5756,7 +5745,7 @@ impl LspStore {
|
||||
) -> Task<Result<Vec<LspPullDiagnostics>>> {
|
||||
let buffer = buffer_handle.read(cx);
|
||||
let buffer_id = buffer.remote_id();
|
||||
let result_id = self.result_id(buffer_id, cx);
|
||||
let result_id = self.result_id(buffer_id);
|
||||
|
||||
if let Some((client, upstream_project_id)) = self.upstream_client() {
|
||||
let request_task = client.request(proto::MultiLspQuery {
|
||||
@@ -9715,28 +9704,22 @@ impl LspStore {
|
||||
}
|
||||
}
|
||||
|
||||
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))?;
|
||||
pub fn result_id(&self, buffer_id: BufferId) -> Option<String> {
|
||||
self.as_local()?
|
||||
.buffer_pull_diagnostics_result_ids
|
||||
.get(&abs_path)
|
||||
.get(&buffer_id)
|
||||
.cloned()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub fn all_result_ids(&self) -> HashMap<PathBuf, String> {
|
||||
pub fn all_result_ids(&self) -> HashMap<BufferId, String> {
|
||||
let Some(local) = self.as_local() else {
|
||||
return HashMap::default();
|
||||
};
|
||||
local
|
||||
.buffer_pull_diagnostics_result_ids
|
||||
.iter()
|
||||
.filter_map(|(file_path, result_id)| Some((file_path.clone(), result_id.clone()?)))
|
||||
.filter_map(|(buffer_id, result_id)| Some((*buffer_id, result_id.clone()?)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -9819,11 +9802,17 @@ fn lsp_workspace_diagnostics_refresh(
|
||||
.await;
|
||||
attempts += 1;
|
||||
|
||||
let Ok(previous_result_ids) = lsp_store.update(cx, |lsp_store, _| {
|
||||
let Ok(previous_result_ids) = lsp_store.update(cx, |lsp_store, cx| {
|
||||
lsp_store
|
||||
.all_result_ids()
|
||||
.into_iter()
|
||||
.filter_map(|(abs_path, result_id)| {
|
||||
.filter_map(|(buffer_id, result_id)| {
|
||||
let buffer = lsp_store
|
||||
.buffer_store()
|
||||
.read(cx)
|
||||
.get_existing(buffer_id)
|
||||
.ok()?;
|
||||
let abs_path = buffer.read(cx).file()?.as_local()?.abs_path(cx);
|
||||
let uri = file_path_to_lsp_url(&abs_path).ok()?;
|
||||
Some(lsp::PreviousResultId {
|
||||
uri,
|
||||
|
||||
@@ -258,7 +258,7 @@ impl Render for TerminalOutput {
|
||||
cell: ic.cell.clone(),
|
||||
});
|
||||
let (cells, rects) =
|
||||
TerminalElement::layout_grid(grid, 0, &text_style, text_system, None, window, cx);
|
||||
TerminalElement::layout_grid(grid, &text_style, text_system, None, window, cx);
|
||||
|
||||
// lines are 0-indexed, so we must add 1 to get the number of lines
|
||||
let text_line_height = text_style.line_height_in_pixels(window.rem_size());
|
||||
|
||||
@@ -387,13 +387,7 @@ impl KeymapFile {
|
||||
},
|
||||
};
|
||||
|
||||
let key_binding = match KeyBinding::load(
|
||||
keystrokes,
|
||||
action,
|
||||
context,
|
||||
key_equivalents,
|
||||
cx.keyboard_mapper(),
|
||||
) {
|
||||
let key_binding = match KeyBinding::load(keystrokes, action, context, key_equivalents) {
|
||||
Ok(key_binding) => key_binding,
|
||||
Err(InvalidKeystrokeError { keystroke }) => {
|
||||
return Err(format!(
|
||||
|
||||
@@ -530,21 +530,6 @@ impl EnvVariableReplacer {
|
||||
fn new(variables: HashMap<VsCodeEnvVariable, ZedEnvVariable>) -> Self {
|
||||
Self { variables }
|
||||
}
|
||||
|
||||
fn replace_value(&self, input: serde_json::Value) -> serde_json::Value {
|
||||
match input {
|
||||
serde_json::Value::String(s) => serde_json::Value::String(self.replace(&s)),
|
||||
serde_json::Value::Array(arr) => {
|
||||
serde_json::Value::Array(arr.into_iter().map(|v| self.replace_value(v)).collect())
|
||||
}
|
||||
serde_json::Value::Object(obj) => serde_json::Value::Object(
|
||||
obj.into_iter()
|
||||
.map(|(k, v)| (self.replace(&k), self.replace_value(v)))
|
||||
.collect(),
|
||||
),
|
||||
_ => input,
|
||||
}
|
||||
}
|
||||
// Replaces occurrences of VsCode-specific environment variables with Zed equivalents.
|
||||
fn replace(&self, input: &str) -> String {
|
||||
shellexpand::env_with_context_no_errors(&input, |var: &str| {
|
||||
|
||||
@@ -53,7 +53,7 @@ impl VsCodeDebugTaskDefinition {
|
||||
host: None,
|
||||
timeout: None,
|
||||
}),
|
||||
config: replacer.replace_value(self.other_attributes),
|
||||
config: self.other_attributes,
|
||||
};
|
||||
Ok(definition)
|
||||
}
|
||||
@@ -75,7 +75,7 @@ impl TryFrom<VsCodeDebugTaskFile> for DebugTaskFile {
|
||||
"workspaceFolder".to_owned(),
|
||||
VariableName::WorktreeRoot.to_string(),
|
||||
),
|
||||
("file".to_owned(), VariableName::Filename.to_string()), // TODO other interesting variables?
|
||||
// TODO other interesting variables?
|
||||
]));
|
||||
let templates = file
|
||||
.configurations
|
||||
@@ -94,7 +94,6 @@ fn task_type_to_adapter_name(task_type: &str) -> SharedString {
|
||||
"php" => "PHP",
|
||||
"cppdbg" | "lldb" => "CodeLLDB",
|
||||
"debugpy" => "Debugpy",
|
||||
"rdbg" => "Ruby",
|
||||
_ => task_type,
|
||||
}
|
||||
.to_owned()
|
||||
|
||||
@@ -270,15 +270,10 @@ mod test {
|
||||
fn test_scroll_keys() {
|
||||
//These keys should be handled by the scrolling element directly
|
||||
//Need to signify this by returning 'None'
|
||||
let shift_key = |key: &str| Keystroke {
|
||||
modifiers: Modifiers::shift(),
|
||||
key: key.to_owned(),
|
||||
key_char: None,
|
||||
};
|
||||
let shift_pageup = shift_key("pageup");
|
||||
let shift_pagedown = shift_key("pagedown");
|
||||
let shift_home = shift_key("home");
|
||||
let shift_end = shift_key("end");
|
||||
let shift_pageup = Keystroke::parse("shift-pageup").unwrap();
|
||||
let shift_pagedown = Keystroke::parse("shift-pagedown").unwrap();
|
||||
let shift_home = Keystroke::parse("shift-home").unwrap();
|
||||
let shift_end = Keystroke::parse("shift-end").unwrap();
|
||||
|
||||
let none = TermMode::NONE;
|
||||
assert_eq!(to_esc_str(&shift_pageup, &none, false), None);
|
||||
@@ -304,13 +299,8 @@ mod test {
|
||||
Some("\x1b[1;2F".into())
|
||||
);
|
||||
|
||||
let normal_key = |key: &str| Keystroke {
|
||||
modifiers: crate::Modifiers::none(),
|
||||
key: key.to_owned(),
|
||||
key_char: None,
|
||||
};
|
||||
let pageup = normal_key("pageup");
|
||||
let pagedown = normal_key("pagedown");
|
||||
let pageup = Keystroke::parse("pageup").unwrap();
|
||||
let pagedown = Keystroke::parse("pagedown").unwrap();
|
||||
let any = TermMode::ANY;
|
||||
|
||||
assert_eq!(to_esc_str(&pageup, &any, false), Some("\x1b[5~".into()));
|
||||
@@ -338,15 +328,10 @@ mod test {
|
||||
let app_cursor = TermMode::APP_CURSOR;
|
||||
let none = TermMode::NONE;
|
||||
|
||||
let generate_keystroke = |key: &str| Keystroke {
|
||||
modifiers: Modifiers::none(),
|
||||
key: key.to_owned(),
|
||||
key_char: None,
|
||||
};
|
||||
let up = generate_keystroke("up");
|
||||
let down = generate_keystroke("down");
|
||||
let left = generate_keystroke("left");
|
||||
let right = generate_keystroke("right");
|
||||
let up = Keystroke::parse("up").unwrap();
|
||||
let down = Keystroke::parse("down").unwrap();
|
||||
let left = Keystroke::parse("left").unwrap();
|
||||
let right = Keystroke::parse("right").unwrap();
|
||||
|
||||
assert_eq!(to_esc_str(&up, &none, false), Some("\x1b[A".into()));
|
||||
assert_eq!(to_esc_str(&down, &none, false), Some("\x1b[B".into()));
|
||||
@@ -371,20 +356,12 @@ mod test {
|
||||
for (lower, upper) in letters_lower.zip(letters_upper) {
|
||||
assert_eq!(
|
||||
to_esc_str(
|
||||
&Keystroke {
|
||||
modifiers: Modifiers::control_shift(),
|
||||
key: lower.to_string(),
|
||||
key_char: None,
|
||||
},
|
||||
&Keystroke::parse(&format!("ctrl-shift-{}", lower)).unwrap(),
|
||||
&mode,
|
||||
false
|
||||
),
|
||||
to_esc_str(
|
||||
&Keystroke {
|
||||
modifiers: Modifiers::control(),
|
||||
key: upper.to_string(),
|
||||
key_char: None,
|
||||
},
|
||||
&Keystroke::parse(&format!("ctrl-{}", upper)).unwrap(),
|
||||
&mode,
|
||||
false
|
||||
),
|
||||
@@ -401,11 +378,7 @@ mod test {
|
||||
for character in ascii_printable {
|
||||
assert_eq!(
|
||||
to_esc_str(
|
||||
&Keystroke {
|
||||
modifiers: Modifiers::alt(),
|
||||
key: character.to_string(),
|
||||
key_char: None,
|
||||
},
|
||||
&Keystroke::parse(&format!("alt-{}", character)).unwrap(),
|
||||
&TermMode::NONE,
|
||||
true
|
||||
)
|
||||
@@ -423,11 +396,7 @@ mod test {
|
||||
for key in gpui_keys {
|
||||
assert_ne!(
|
||||
to_esc_str(
|
||||
&Keystroke {
|
||||
modifiers: Modifiers::alt(),
|
||||
key: key.to_string(),
|
||||
key_char: None,
|
||||
},
|
||||
&Keystroke::parse(&format!("alt-{}", key)).unwrap(),
|
||||
&TermMode::NONE,
|
||||
true
|
||||
)
|
||||
@@ -450,78 +419,15 @@ mod test {
|
||||
// 8 | Shift + Alt + Control
|
||||
// ---------+---------------------------
|
||||
// from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys
|
||||
assert_eq!(
|
||||
2,
|
||||
modifier_code(&Keystroke {
|
||||
modifiers: Modifiers::shift(),
|
||||
key: "a".into(),
|
||||
key_char: None
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
3,
|
||||
modifier_code(&Keystroke {
|
||||
modifiers: Modifiers::alt(),
|
||||
key: "a".into(),
|
||||
key_char: None
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
4,
|
||||
modifier_code(&Keystroke {
|
||||
modifiers: Modifiers {
|
||||
shift: true,
|
||||
alt: true,
|
||||
..Default::default()
|
||||
},
|
||||
key: "a".into(),
|
||||
key_char: None
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
5,
|
||||
modifier_code(&Keystroke {
|
||||
modifiers: Modifiers::control(),
|
||||
key: "a".into(),
|
||||
key_char: None
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
6,
|
||||
modifier_code(&Keystroke {
|
||||
modifiers: Modifiers {
|
||||
shift: true,
|
||||
control: true,
|
||||
..Default::default()
|
||||
},
|
||||
key: "a".into(),
|
||||
key_char: None
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
7,
|
||||
modifier_code(&Keystroke {
|
||||
modifiers: Modifiers {
|
||||
alt: true,
|
||||
control: true,
|
||||
..Default::default()
|
||||
},
|
||||
key: "a".into(),
|
||||
key_char: None
|
||||
})
|
||||
);
|
||||
assert_eq!(2, modifier_code(&Keystroke::parse("shift-a").unwrap()));
|
||||
assert_eq!(3, modifier_code(&Keystroke::parse("alt-a").unwrap()));
|
||||
assert_eq!(4, modifier_code(&Keystroke::parse("shift-alt-a").unwrap()));
|
||||
assert_eq!(5, modifier_code(&Keystroke::parse("ctrl-a").unwrap()));
|
||||
assert_eq!(6, modifier_code(&Keystroke::parse("shift-ctrl-a").unwrap()));
|
||||
assert_eq!(7, modifier_code(&Keystroke::parse("alt-ctrl-a").unwrap()));
|
||||
assert_eq!(
|
||||
8,
|
||||
modifier_code(&Keystroke {
|
||||
modifiers: Modifiers {
|
||||
shift: true,
|
||||
control: true,
|
||||
alt: true,
|
||||
..Default::default()
|
||||
},
|
||||
key: "a".into(),
|
||||
key_char: None
|
||||
})
|
||||
modifier_code(&Keystroke::parse("shift-ctrl-alt-a").unwrap())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use editor::{CursorLayout, HighlightedRange, HighlightedRangeLine};
|
||||
use gpui::{
|
||||
AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, DispatchPhase, Element,
|
||||
ElementId, Entity, FocusHandle, Font, FontStyle, FontWeight, GlobalElementId, HighlightStyle,
|
||||
Hitbox, Hsla, InputHandler, InteractiveElement, Interactivity, IntoElement, LayoutId, Length,
|
||||
Hitbox, Hsla, InputHandler, InteractiveElement, Interactivity, IntoElement, LayoutId,
|
||||
ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, Point, ShapedLine,
|
||||
StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, TextStyle, UTF16Selection,
|
||||
UnderlineStyle, WeakEntity, WhiteSpace, Window, WindowTextSystem, div, fill, point, px,
|
||||
@@ -32,7 +32,7 @@ use workspace::Workspace;
|
||||
use std::mem;
|
||||
use std::{fmt::Debug, ops::RangeInclusive, rc::Rc};
|
||||
|
||||
use crate::{BlockContext, BlockProperties, ContentMode, TerminalMode, TerminalView};
|
||||
use crate::{BlockContext, BlockProperties, TerminalMode, TerminalView};
|
||||
|
||||
/// The information generated during layout that is necessary for painting.
|
||||
pub struct LayoutState {
|
||||
@@ -49,7 +49,6 @@ pub struct LayoutState {
|
||||
gutter: Pixels,
|
||||
block_below_cursor_element: Option<AnyElement>,
|
||||
base_text_style: TextStyle,
|
||||
content_mode: ContentMode,
|
||||
}
|
||||
|
||||
/// Helper struct for converting data between Alacritty's cursor points, and displayed cursor points.
|
||||
@@ -203,7 +202,6 @@ impl TerminalElement {
|
||||
|
||||
pub fn layout_grid(
|
||||
grid: impl Iterator<Item = IndexedCell>,
|
||||
start_line_offset: i32,
|
||||
text_style: &TextStyle,
|
||||
// terminal_theme: &TerminalStyle,
|
||||
text_system: &WindowTextSystem,
|
||||
@@ -220,8 +218,6 @@ impl TerminalElement {
|
||||
|
||||
let linegroups = grid.into_iter().chunk_by(|i| i.point.line);
|
||||
for (line_index, (_, line)) in linegroups.into_iter().enumerate() {
|
||||
let alac_line = start_line_offset + line_index as i32;
|
||||
|
||||
for cell in line {
|
||||
let mut fg = cell.fg;
|
||||
let mut bg = cell.bg;
|
||||
@@ -249,7 +245,7 @@ impl TerminalElement {
|
||||
|| {
|
||||
Some(LayoutRect::new(
|
||||
AlacPoint::new(
|
||||
alac_line,
|
||||
line_index as i32,
|
||||
cell.point.column.0 as i32,
|
||||
),
|
||||
1,
|
||||
@@ -264,7 +260,10 @@ impl TerminalElement {
|
||||
rects.push(cur_rect.take().unwrap());
|
||||
}
|
||||
cur_rect = Some(LayoutRect::new(
|
||||
AlacPoint::new(alac_line, cell.point.column.0 as i32),
|
||||
AlacPoint::new(
|
||||
line_index as i32,
|
||||
cell.point.column.0 as i32,
|
||||
),
|
||||
1,
|
||||
convert_color(&bg, theme),
|
||||
));
|
||||
@@ -273,7 +272,7 @@ impl TerminalElement {
|
||||
None => {
|
||||
cur_alac_color = Some(bg);
|
||||
cur_rect = Some(LayoutRect::new(
|
||||
AlacPoint::new(alac_line, cell.point.column.0 as i32),
|
||||
AlacPoint::new(line_index as i32, cell.point.column.0 as i32),
|
||||
1,
|
||||
convert_color(&bg, theme),
|
||||
));
|
||||
@@ -296,7 +295,7 @@ impl TerminalElement {
|
||||
);
|
||||
|
||||
cells.push(LayoutCell::new(
|
||||
AlacPoint::new(alac_line, cell.point.column.0 as i32),
|
||||
AlacPoint::new(line_index as i32, cell.point.column.0 as i32),
|
||||
layout_cell,
|
||||
))
|
||||
};
|
||||
@@ -431,13 +430,7 @@ impl TerminalElement {
|
||||
}
|
||||
}
|
||||
|
||||
fn register_mouse_listeners(
|
||||
&mut self,
|
||||
mode: TermMode,
|
||||
hitbox: &Hitbox,
|
||||
content_mode: &ContentMode,
|
||||
window: &mut Window,
|
||||
) {
|
||||
fn register_mouse_listeners(&mut self, mode: TermMode, hitbox: &Hitbox, window: &mut Window) {
|
||||
let focus = self.focus.clone();
|
||||
let terminal = self.terminal.clone();
|
||||
let terminal_view = self.terminal_view.clone();
|
||||
@@ -519,18 +512,14 @@ impl TerminalElement {
|
||||
),
|
||||
);
|
||||
|
||||
if content_mode.is_scrollable() {
|
||||
if !matches!(self.mode, TerminalMode::Embedded { .. }) {
|
||||
self.interactivity.on_scroll_wheel({
|
||||
let terminal_view = self.terminal_view.downgrade();
|
||||
move |e, window, cx| {
|
||||
move |e, _window, cx| {
|
||||
terminal_view
|
||||
.update(cx, |terminal_view, cx| {
|
||||
if matches!(terminal_view.mode, TerminalMode::Standalone)
|
||||
|| terminal_view.focus_handle.is_focused(window)
|
||||
{
|
||||
terminal_view.scroll_wheel(e, cx);
|
||||
cx.notify();
|
||||
}
|
||||
terminal_view.scroll_wheel(e, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -616,32 +605,6 @@ impl Element for TerminalElement {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let height: Length = match self.terminal_view.read(cx).content_mode(window, cx) {
|
||||
ContentMode::Inline {
|
||||
displayed_lines,
|
||||
total_lines: _,
|
||||
} => {
|
||||
let rem_size = window.rem_size();
|
||||
let line_height = window.text_style().font_size.to_pixels(rem_size)
|
||||
* TerminalSettings::get_global(cx)
|
||||
.line_height
|
||||
.value()
|
||||
.to_pixels(rem_size)
|
||||
.0;
|
||||
(displayed_lines * line_height).into()
|
||||
}
|
||||
ContentMode::Scrollable => {
|
||||
if let TerminalMode::Embedded { .. } = &self.mode {
|
||||
let term = self.terminal.read(cx);
|
||||
if !term.scrolled_to_top() && !term.scrolled_to_bottom() && self.focused {
|
||||
self.interactivity.occlude_mouse();
|
||||
}
|
||||
}
|
||||
|
||||
relative(1.).into()
|
||||
}
|
||||
};
|
||||
|
||||
let layout_id = self.interactivity.request_layout(
|
||||
global_id,
|
||||
inspector_id,
|
||||
@@ -649,7 +612,29 @@ impl Element for TerminalElement {
|
||||
cx,
|
||||
|mut style, window, cx| {
|
||||
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)
|
||||
},
|
||||
@@ -708,7 +693,7 @@ impl Element for TerminalElement {
|
||||
TerminalMode::Embedded { .. } => {
|
||||
window.text_style().font_size.to_pixels(window.rem_size())
|
||||
}
|
||||
TerminalMode::Standalone => terminal_settings
|
||||
TerminalMode::Scrollable => terminal_settings
|
||||
.font_size
|
||||
.map_or(buffer_font_size, |size| theme::adjusted_font_size(size, cx)),
|
||||
};
|
||||
@@ -748,7 +733,7 @@ impl Element for TerminalElement {
|
||||
let player_color = theme.players().local();
|
||||
let match_color = theme.colors().search_match_background;
|
||||
let gutter;
|
||||
let (dimensions, line_height_px) = {
|
||||
let dimensions = {
|
||||
let rem_size = window.rem_size();
|
||||
let font_pixels = text_style.font_size.to_pixels(rem_size);
|
||||
// TODO: line_height should be an f32 not an AbsoluteLength.
|
||||
@@ -774,10 +759,7 @@ impl Element for TerminalElement {
|
||||
let mut origin = bounds.origin;
|
||||
origin.x += gutter;
|
||||
|
||||
(
|
||||
TerminalBounds::new(line_height, cell_width, Bounds { origin, size }),
|
||||
line_height,
|
||||
)
|
||||
TerminalBounds::new(line_height, cell_width, Bounds { origin, size })
|
||||
};
|
||||
|
||||
let search_matches = self.terminal.read(cx).matches.clone();
|
||||
@@ -845,42 +827,16 @@ impl Element for TerminalElement {
|
||||
|
||||
// then have that representation be converted to the appropriate highlight data structure
|
||||
|
||||
let content_mode = self.terminal_view.read(cx).content_mode(window, cx);
|
||||
let (cells, rects) = match content_mode {
|
||||
ContentMode::Scrollable => TerminalElement::layout_grid(
|
||||
cells.iter().cloned(),
|
||||
0,
|
||||
&text_style,
|
||||
window.text_system(),
|
||||
last_hovered_word
|
||||
.as_ref()
|
||||
.map(|last_hovered_word| (link_style, &last_hovered_word.word_match)),
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
ContentMode::Inline { .. } => {
|
||||
let intersection = window.content_mask().bounds.intersect(&bounds);
|
||||
let start_row = (intersection.top() - bounds.top()) / line_height_px;
|
||||
let end_row = start_row + intersection.size.height / line_height_px;
|
||||
let line_range = (start_row as i32)..=(end_row as i32);
|
||||
|
||||
TerminalElement::layout_grid(
|
||||
cells
|
||||
.iter()
|
||||
.skip_while(|i| &i.point.line < line_range.start())
|
||||
.take_while(|i| &i.point.line <= line_range.end())
|
||||
.cloned(),
|
||||
*line_range.start(),
|
||||
&text_style,
|
||||
window.text_system(),
|
||||
last_hovered_word.as_ref().map(|last_hovered_word| {
|
||||
(link_style, &last_hovered_word.word_match)
|
||||
}),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
};
|
||||
let (cells, rects) = TerminalElement::layout_grid(
|
||||
cells.iter().cloned(),
|
||||
&text_style,
|
||||
window.text_system(),
|
||||
last_hovered_word
|
||||
.as_ref()
|
||||
.map(|last_hovered_word| (link_style, &last_hovered_word.word_match)),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
// Layout cursor. Rectangle is used for IME, so we should lay it out even
|
||||
// if we don't end up showing it.
|
||||
@@ -976,7 +932,6 @@ impl Element for TerminalElement {
|
||||
gutter,
|
||||
block_below_cursor_element,
|
||||
base_text_style: text_style,
|
||||
content_mode,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -1014,12 +969,7 @@ impl Element for TerminalElement {
|
||||
workspace: self.workspace.clone(),
|
||||
};
|
||||
|
||||
self.register_mouse_listeners(
|
||||
layout.mode,
|
||||
&layout.hitbox,
|
||||
&layout.content_mode,
|
||||
window,
|
||||
);
|
||||
self.register_mouse_listeners(layout.mode, &layout.hitbox, window);
|
||||
if window.modifiers().secondary()
|
||||
&& bounds.contains(&window.mouse_position())
|
||||
&& 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 gpui::{
|
||||
AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
|
||||
KeyDownEvent, Keystroke, Modifiers, MouseButton, MouseDownEvent, Pixels, Render,
|
||||
ScrollWheelEvent, Stateful, Styled, Subscription, Task, WeakEntity, actions, anchored,
|
||||
deferred, div, impl_actions,
|
||||
KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render, ScrollWheelEvent,
|
||||
Stateful, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred, div,
|
||||
impl_actions,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use persistence::TERMINAL_DB;
|
||||
@@ -140,37 +140,12 @@ pub struct TerminalView {
|
||||
#[derive(Default, Clone)]
|
||||
pub enum TerminalMode {
|
||||
#[default]
|
||||
Standalone,
|
||||
Embedded {
|
||||
max_lines_when_unfocused: Option<usize>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ContentMode {
|
||||
Scrollable,
|
||||
Inline {
|
||||
displayed_lines: usize,
|
||||
total_lines: usize,
|
||||
Embedded {
|
||||
max_lines: Option<usize>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ContentMode {
|
||||
pub fn is_limited(&self) -> bool {
|
||||
match self {
|
||||
ContentMode::Scrollable => false,
|
||||
ContentMode::Inline {
|
||||
displayed_lines,
|
||||
total_lines,
|
||||
} => displayed_lines < total_lines,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_scrollable(&self) -> bool {
|
||||
matches!(self, ContentMode::Scrollable)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct HoverTarget {
|
||||
tooltip: String,
|
||||
@@ -248,7 +223,7 @@ impl TerminalView {
|
||||
blink_epoch: 0,
|
||||
hover: None,
|
||||
hover_tooltip_update: Task::ready(()),
|
||||
mode: TerminalMode::Standalone,
|
||||
mode: TerminalMode::Scrollable,
|
||||
workspace_id,
|
||||
show_breadcrumbs: TerminalSettings::get_global(cx).toolbar.breadcrumbs,
|
||||
block_below_cursor: None,
|
||||
@@ -270,46 +245,16 @@ impl TerminalView {
|
||||
}
|
||||
|
||||
/// Enable 'embedded' mode where the terminal displays the full content with an optional limit of lines.
|
||||
pub fn set_embedded_mode(
|
||||
&mut self,
|
||||
max_lines_when_unfocused: Option<usize>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.mode = TerminalMode::Embedded {
|
||||
max_lines_when_unfocused,
|
||||
};
|
||||
pub fn set_embedded_mode(&mut self, max_lines: Option<usize>, cx: &mut Context<Self>) {
|
||||
self.mode = TerminalMode::Embedded { max_lines };
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
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 {
|
||||
pub fn is_content_limited(&self, window: &Window) -> bool {
|
||||
match &self.mode {
|
||||
TerminalMode::Standalone => ContentMode::Scrollable,
|
||||
TerminalMode::Embedded {
|
||||
max_lines_when_unfocused,
|
||||
} => {
|
||||
let total_lines = self.terminal.read(cx).total_lines();
|
||||
|
||||
if total_lines > Self::MAX_EMBEDDED_LINES {
|
||||
ContentMode::Scrollable
|
||||
} else {
|
||||
let mut displayed_lines = total_lines;
|
||||
|
||||
if !self.focus_handle.is_focused(window) {
|
||||
if let Some(max_lines) = max_lines_when_unfocused {
|
||||
displayed_lines = displayed_lines.min(*max_lines)
|
||||
}
|
||||
}
|
||||
|
||||
ContentMode::Inline {
|
||||
displayed_lines,
|
||||
total_lines,
|
||||
}
|
||||
}
|
||||
TerminalMode::Scrollable => false,
|
||||
TerminalMode::Embedded { max_lines } => {
|
||||
!self.focus_handle.is_focused(window) && max_lines.is_some()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -450,15 +395,7 @@ impl TerminalView {
|
||||
{
|
||||
self.terminal.update(cx, |term, cx| {
|
||||
term.try_keystroke(
|
||||
&Keystroke {
|
||||
modifiers: Modifiers {
|
||||
control: true,
|
||||
platform: true,
|
||||
..Default::default()
|
||||
},
|
||||
key: "space".to_owned(),
|
||||
key_char: None,
|
||||
},
|
||||
&Keystroke::parse("ctrl-cmd-space").unwrap(),
|
||||
TerminalSettings::get_global(cx).option_as_meta,
|
||||
)
|
||||
});
|
||||
@@ -734,7 +671,7 @@ impl TerminalView {
|
||||
}
|
||||
|
||||
fn send_keystroke(&mut self, text: &SendKeystroke, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(keystroke) = Keystroke::parse(&text.0, cx.keyboard_mapper()).log_err() {
|
||||
if let Some(keystroke) = Keystroke::parse(&text.0).log_err() {
|
||||
self.clear_bell(cx);
|
||||
self.terminal.update(cx, |term, cx| {
|
||||
let processed =
|
||||
@@ -903,10 +840,10 @@ impl TerminalView {
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_scrollbar(&self, window: &Window, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
|
||||
fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
|
||||
if !Self::should_show_scrollbar(cx)
|
||||
|| !(self.show_scrollbar || self.scrollbar_state.is_dragging())
|
||||
|| !self.content_mode(window, cx).is_scrollable()
|
||||
|| matches!(self.mode, TerminalMode::Embedded { .. })
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -1556,7 +1493,7 @@ impl Render for TerminalView {
|
||||
self.block_below_cursor.clone(),
|
||||
self.mode.clone(),
|
||||
))
|
||||
.when_some(self.render_scrollbar(window, cx), |div, scrollbar| {
|
||||
.when_some(self.render_scrollbar(cx), |div, scrollbar| {
|
||||
div.child(scrollbar)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -148,8 +148,11 @@ impl ThemeColors {
|
||||
version_control_renamed: MODIFIED_COLOR,
|
||||
version_control_conflict: orange().light().step_12(),
|
||||
version_control_ignored: gray().light().step_12(),
|
||||
version_control_conflict_marker_ours: green().light().step_10().alpha(0.5),
|
||||
version_control_conflict_marker_theirs: blue().light().step_10().alpha(0.5),
|
||||
version_control_conflict_ours_background: green().light().step_10().alpha(0.5),
|
||||
version_control_conflict_theirs_background: blue().light().step_10().alpha(0.5),
|
||||
version_control_conflict_ours_marker_background: green().light().step_10().alpha(0.7),
|
||||
version_control_conflict_theirs_marker_background: blue().light().step_10().alpha(0.7),
|
||||
version_control_conflict_divider_background: Hsla::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,8 +273,11 @@ impl ThemeColors {
|
||||
version_control_renamed: MODIFIED_COLOR,
|
||||
version_control_conflict: orange().dark().step_12(),
|
||||
version_control_ignored: gray().dark().step_12(),
|
||||
version_control_conflict_marker_ours: green().dark().step_10().alpha(0.5),
|
||||
version_control_conflict_marker_theirs: blue().dark().step_10().alpha(0.5),
|
||||
version_control_conflict_ours_background: green().dark().step_10().alpha(0.5),
|
||||
version_control_conflict_theirs_background: blue().dark().step_10().alpha(0.5),
|
||||
version_control_conflict_ours_marker_background: green().dark().step_10().alpha(0.7),
|
||||
version_control_conflict_theirs_marker_background: blue().dark().step_10().alpha(0.7),
|
||||
version_control_conflict_divider_background: Hsla::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,8 +211,23 @@ pub(crate) fn zed_default_dark() -> Theme {
|
||||
version_control_renamed: MODIFIED_COLOR,
|
||||
version_control_conflict: crate::orange().light().step_12(),
|
||||
version_control_ignored: crate::gray().light().step_12(),
|
||||
version_control_conflict_marker_ours: crate::green().light().step_12().alpha(0.5),
|
||||
version_control_conflict_marker_theirs: crate::blue().light().step_12().alpha(0.5),
|
||||
version_control_conflict_ours_background: crate::green()
|
||||
.light()
|
||||
.step_12()
|
||||
.alpha(0.5),
|
||||
version_control_conflict_theirs_background: crate::blue()
|
||||
.light()
|
||||
.step_12()
|
||||
.alpha(0.5),
|
||||
version_control_conflict_ours_marker_background: crate::green()
|
||||
.light()
|
||||
.step_12()
|
||||
.alpha(0.7),
|
||||
version_control_conflict_theirs_marker_background: crate::blue()
|
||||
.light()
|
||||
.step_12()
|
||||
.alpha(0.7),
|
||||
version_control_conflict_divider_background: Hsla::default(),
|
||||
},
|
||||
status: StatusColors {
|
||||
conflict: yellow,
|
||||
|
||||
@@ -620,20 +620,24 @@ pub struct ThemeColorsContent {
|
||||
pub version_control_ignored: Option<String>,
|
||||
|
||||
/// Background color for row highlights of "ours" regions in merge conflicts.
|
||||
#[serde(rename = "version_control.conflict_marker.ours")]
|
||||
pub version_control_conflict_marker_ours: Option<String>,
|
||||
|
||||
/// Background color for row highlights of "theirs" regions in merge conflicts.
|
||||
#[serde(rename = "version_control.conflict_marker.theirs")]
|
||||
pub version_control_conflict_marker_theirs: Option<String>,
|
||||
|
||||
/// Deprecated in favor of `version_control_conflict_marker_ours`.
|
||||
#[deprecated]
|
||||
#[serde(rename = "version_control.conflict.ours_background")]
|
||||
pub version_control_conflict_ours_background: Option<String>,
|
||||
|
||||
/// Deprecated in favor of `version_control_conflict_marker_theirs`.
|
||||
#[deprecated]
|
||||
/// Background color for row highlights of "theirs" regions in merge conflicts.
|
||||
#[serde(rename = "version_control.conflict.theirs_background")]
|
||||
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 {
|
||||
@@ -1114,17 +1118,25 @@ impl ThemeColorsContent {
|
||||
.and_then(|color| try_parse_color(color).ok())
|
||||
// Fall back to `conflict`, for backwards compatibility.
|
||||
.or(status_colors.ignored),
|
||||
#[allow(deprecated)]
|
||||
version_control_conflict_marker_ours: self
|
||||
.version_control_conflict_marker_ours
|
||||
version_control_conflict_ours_background: self
|
||||
.version_control_conflict_ours_background
|
||||
.as_ref()
|
||||
.or(self.version_control_conflict_ours_background.as_ref())
|
||||
.and_then(|color| try_parse_color(color).ok()),
|
||||
#[allow(deprecated)]
|
||||
version_control_conflict_marker_theirs: self
|
||||
.version_control_conflict_marker_theirs
|
||||
version_control_conflict_theirs_background: self
|
||||
.version_control_conflict_theirs_background
|
||||
.as_ref()
|
||||
.and_then(|color| try_parse_color(color).ok()),
|
||||
version_control_conflict_ours_marker_background: self
|
||||
.version_control_conflict_ours_marker_background
|
||||
.as_ref()
|
||||
.and_then(|color| try_parse_color(color).ok()),
|
||||
version_control_conflict_theirs_marker_background: self
|
||||
.version_control_conflict_theirs_marker_background
|
||||
.as_ref()
|
||||
.and_then(|color| try_parse_color(color).ok()),
|
||||
version_control_conflict_divider_background: self
|
||||
.version_control_conflict_divider_background
|
||||
.as_ref()
|
||||
.or(self.version_control_conflict_theirs_background.as_ref())
|
||||
.and_then(|color| try_parse_color(color).ok()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,9 +273,12 @@ pub struct ThemeColors {
|
||||
pub version_control_ignored: Hsla,
|
||||
|
||||
/// Represents the "ours" region of a merge conflict.
|
||||
pub version_control_conflict_marker_ours: Hsla,
|
||||
pub version_control_conflict_ours_background: Hsla,
|
||||
/// Represents the "theirs" region of a merge conflict.
|
||||
pub version_control_conflict_marker_theirs: Hsla,
|
||||
pub version_control_conflict_theirs_background: Hsla,
|
||||
pub version_control_conflict_ours_marker_background: Hsla,
|
||||
pub version_control_conflict_theirs_marker_background: Hsla,
|
||||
pub version_control_conflict_divider_background: Hsla,
|
||||
}
|
||||
|
||||
#[derive(EnumIter, Debug, Clone, Copy, AsRefStr)]
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use gpui::{Action, Hsla, MouseButton, prelude::*, svg};
|
||||
use gpui::{Action, MouseButton, prelude::*};
|
||||
|
||||
use ui::prelude::*;
|
||||
|
||||
use crate::window_controls::{WindowControl, WindowControlType};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct LinuxWindowControls {
|
||||
close_window_action: Box<dyn Action>,
|
||||
@@ -43,166 +46,3 @@ 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,6 +3,7 @@ mod collab;
|
||||
mod onboarding_banner;
|
||||
mod platforms;
|
||||
mod title_bar_settings;
|
||||
mod window_controls;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
mod stories;
|
||||
|
||||
165
crates/title_bar/src/window_controls.rs
Normal file
165
crates/title_bar/src/window_controls.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
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,61 +533,78 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_text_for_keystroke() {
|
||||
let cmd_keystroke = |key: &str| Keystroke {
|
||||
modifiers: Modifiers::command(),
|
||||
key: key.to_owned(),
|
||||
key_char: None,
|
||||
};
|
||||
assert_eq!(
|
||||
keystroke_text(&cmd_keystroke("c"), PlatformStyle::Mac, false),
|
||||
keystroke_text(
|
||||
&Keystroke::parse("cmd-c").unwrap(),
|
||||
PlatformStyle::Mac,
|
||||
false
|
||||
),
|
||||
"Command-C".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
keystroke_text(&cmd_keystroke("c"), PlatformStyle::Linux, false),
|
||||
keystroke_text(
|
||||
&Keystroke::parse("cmd-c").unwrap(),
|
||||
PlatformStyle::Linux,
|
||||
false
|
||||
),
|
||||
"Super-C".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
keystroke_text(&cmd_keystroke("c"), PlatformStyle::Windows, false),
|
||||
keystroke_text(
|
||||
&Keystroke::parse("cmd-c").unwrap(),
|
||||
PlatformStyle::Windows,
|
||||
false
|
||||
),
|
||||
"Win-C".to_string()
|
||||
);
|
||||
|
||||
let ctrl_alt_keystroke = |key: &str| Keystroke {
|
||||
modifiers: Modifiers {
|
||||
control: true,
|
||||
alt: true,
|
||||
..Default::default()
|
||||
},
|
||||
key: key.to_owned(),
|
||||
key_char: None,
|
||||
};
|
||||
assert_eq!(
|
||||
keystroke_text(&ctrl_alt_keystroke("delete"), PlatformStyle::Mac, false),
|
||||
keystroke_text(
|
||||
&Keystroke::parse("ctrl-alt-delete").unwrap(),
|
||||
PlatformStyle::Mac,
|
||||
false
|
||||
),
|
||||
"Control-Option-Delete".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
keystroke_text(&ctrl_alt_keystroke("delete"), PlatformStyle::Linux, false),
|
||||
keystroke_text(
|
||||
&Keystroke::parse("ctrl-alt-delete").unwrap(),
|
||||
PlatformStyle::Linux,
|
||||
false
|
||||
),
|
||||
"Ctrl-Alt-Delete".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
keystroke_text(&ctrl_alt_keystroke("delete"), PlatformStyle::Windows, false),
|
||||
keystroke_text(
|
||||
&Keystroke::parse("ctrl-alt-delete").unwrap(),
|
||||
PlatformStyle::Windows,
|
||||
false
|
||||
),
|
||||
"Ctrl-Alt-Delete".to_string()
|
||||
);
|
||||
|
||||
let shift_keystroke = |key: &str| Keystroke {
|
||||
modifiers: Modifiers::shift(),
|
||||
key: key.to_owned(),
|
||||
key_char: None,
|
||||
};
|
||||
assert_eq!(
|
||||
keystroke_text(&shift_keystroke("pageup"), PlatformStyle::Mac, false),
|
||||
keystroke_text(
|
||||
&Keystroke::parse("shift-pageup").unwrap(),
|
||||
PlatformStyle::Mac,
|
||||
false
|
||||
),
|
||||
"Shift-PageUp".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
keystroke_text(&shift_keystroke("pageup"), PlatformStyle::Linux, false,),
|
||||
keystroke_text(
|
||||
&Keystroke::parse("shift-pageup").unwrap(),
|
||||
PlatformStyle::Linux,
|
||||
false,
|
||||
),
|
||||
"Shift-PageUp".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
keystroke_text(&shift_keystroke("pageup"), PlatformStyle::Windows, false),
|
||||
keystroke_text(
|
||||
&Keystroke::parse("shift-pageup").unwrap(),
|
||||
PlatformStyle::Windows,
|
||||
false
|
||||
),
|
||||
"Shift-PageUp".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ walkdir.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
command-fds = "0.3.1"
|
||||
libc.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
#![cfg_attr(not(unix), allow(unused))]
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use std::borrow::Cow;
|
||||
use std::ffi::OsStr;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
/// Capture all environment variables from the login shell.
|
||||
#[cfg(unix)]
|
||||
pub fn capture(directory: &std::path::Path) -> Result<collections::HashMap<String, String>> {
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::Stdio;
|
||||
|
||||
let shell_path = std::env::var("SHELL").map(std::path::PathBuf::from)?;
|
||||
let shell_name = shell_path.file_name().and_then(std::ffi::OsStr::to_str);
|
||||
|
||||
let mut command = std::process::Command::new(&shell_path);
|
||||
command.stdin(Stdio::null());
|
||||
command.stdout(Stdio::piped());
|
||||
command.stderr(Stdio::piped());
|
||||
pub fn capture(change_dir: Option<impl AsRef<Path>>) -> Result<HashMap<String, String>> {
|
||||
let shell_path = std::env::var("SHELL").map(PathBuf::from)?;
|
||||
let shell_name = shell_path.file_name().and_then(OsStr::to_str);
|
||||
|
||||
let mut command_string = String::new();
|
||||
|
||||
@@ -23,7 +18,10 @@ pub fn capture(directory: &std::path::Path) -> Result<collections::HashMap<Strin
|
||||
// the project directory to get the env in there as if the user
|
||||
// `cd`'d into it. We do that because tools like direnv, asdf, ...
|
||||
// hook into `cd` and only set up the env after that.
|
||||
command_string.push_str(&format!("cd '{}';", directory.display()));
|
||||
if let Some(dir) = change_dir {
|
||||
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
|
||||
// trigger the behavior of direnv, etc.
|
||||
@@ -32,26 +30,26 @@ pub fn capture(directory: &std::path::Path) -> Result<collections::HashMap<Strin
|
||||
_ => "",
|
||||
});
|
||||
|
||||
// In some shells, file descriptors greater than 2 cannot be used in interactive mode,
|
||||
// so file descriptor 0 is used instead.
|
||||
const ENV_OUTPUT_FD: std::os::fd::RawFd = 0;
|
||||
command_string.push_str(&format!("sh -c 'export -p >&{ENV_OUTPUT_FD}';"));
|
||||
let mut env_output_file = NamedTempFile::new()?;
|
||||
command_string.push_str(&format!(
|
||||
"sh -c 'export -p' > '{}';",
|
||||
env_output_file.path().to_string_lossy(),
|
||||
));
|
||||
|
||||
let mut command = Command::new(&shell_path);
|
||||
|
||||
// For csh/tcsh, the login shell option is set by passing `-` as
|
||||
// the 0th argument instead of using `-l`.
|
||||
if let Some("tcsh" | "csh") = shell_name {
|
||||
command.arg0("-");
|
||||
#[cfg(unix)]
|
||||
std::os::unix::process::CommandExt::arg0(&mut command, "-");
|
||||
} else {
|
||||
command.arg("-l");
|
||||
}
|
||||
|
||||
command.args(["-i", "-c", &command_string]);
|
||||
|
||||
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);
|
||||
|
||||
let process_output = super::set_pre_exec_to_start_new_session(&mut command).output()?;
|
||||
anyhow::ensure!(
|
||||
process_output.status.success(),
|
||||
"login shell exited with {}. stdout: {:?}, stderr: {:?}",
|
||||
@@ -60,36 +58,15 @@ pub fn capture(directory: &std::path::Path) -> Result<collections::HashMap<Strin
|
||||
String::from_utf8_lossy(&process_output.stderr),
|
||||
);
|
||||
|
||||
let mut env_output = String::new();
|
||||
env_output_file.read_to_string(&mut env_output)?;
|
||||
|
||||
parse(&env_output)
|
||||
.filter_map(|entry| match entry {
|
||||
Ok((name, value)) => Some(Ok((name.into(), value?.into()))),
|
||||
Err(err) => Some(Err(err)),
|
||||
})
|
||||
.collect::<Result<_>>()
|
||||
}
|
||||
|
||||
#[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()?))
|
||||
.collect::<Result<HashMap<String, String>>>()
|
||||
}
|
||||
|
||||
/// Parse the result of calling `sh -c 'export -p'`.
|
||||
@@ -177,17 +154,6 @@ fn parse_literal_double_quoted(input: &str) -> Option<(String, &str)> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn test_spawn_and_read_fd() -> anyhow::Result<()> {
|
||||
let mut command = std::process::Command::new("sh");
|
||||
super::super::set_pre_exec_to_start_new_session(&mut command);
|
||||
command.args(["-lic", "printf 'abc%.0s' $(seq 1 65536) >&0"]);
|
||||
let (bytes, _) = spawn_and_read_fd(command, 0)?;
|
||||
assert_eq!(bytes.len(), 65536 * 3);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse() {
|
||||
let input = indoc::indoc! {r#"
|
||||
|
||||
@@ -315,7 +315,7 @@ pub fn load_login_shell_environment() -> Result<()> {
|
||||
// into shell's `cd` command (and hooks) to manipulate env.
|
||||
// We do this so that we get the env a user would have when spawning a shell
|
||||
// in home directory.
|
||||
for (name, value) in shell_env::capture(paths::home_dir())? {
|
||||
for (name, value) in shell_env::capture(Some(paths::home_dir()))? {
|
||||
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>) {
|
||||
if let Some(Operator::Literal { prefix }) = self.active_operator() {
|
||||
if let Some(prefix) = prefix {
|
||||
if let Some(keystroke) = Keystroke::parse(&action.0, cx.keyboard_mapper()).ok() {
|
||||
if let Some(keystroke) = Keystroke::parse(&action.0).ok() {
|
||||
window.defer(cx, |window, cx| {
|
||||
window.dispatch_keystroke(keystroke, cx);
|
||||
});
|
||||
|
||||
@@ -222,15 +222,7 @@ impl NeovimBackedTestContext {
|
||||
pub async fn simulate_shared_keystrokes(&mut self, keystroke_texts: &str) {
|
||||
for keystroke_text in keystroke_texts.split(' ') {
|
||||
self.recent_keystrokes.push(keystroke_text.to_string());
|
||||
#[cfg(not(feature = "neovim"))]
|
||||
self.neovim.send_keystroke(keystroke_text).await;
|
||||
#[cfg(feature = "neovim")]
|
||||
{
|
||||
let keyboard_mapper = self.cx.keyboard_mapper();
|
||||
self.neovim
|
||||
.send_keystroke(keystroke_text, keyboard_mapper.as_ref())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
self.simulate_keystrokes(keystroke_texts);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use async_compat::Compat;
|
||||
#[cfg(feature = "neovim")]
|
||||
use async_trait::async_trait;
|
||||
#[cfg(feature = "neovim")]
|
||||
use gpui::{Keystroke, PlatformKeyboardMapper};
|
||||
use gpui::Keystroke;
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
use language::Point;
|
||||
@@ -110,12 +110,8 @@ impl NeovimConnection {
|
||||
|
||||
// Sends a keystroke to the neovim process.
|
||||
#[cfg(feature = "neovim")]
|
||||
pub async fn send_keystroke(
|
||||
&mut self,
|
||||
keystroke_text: &str,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) {
|
||||
let mut keystroke = Keystroke::parse(keystroke_text, keyboard_mapper).unwrap();
|
||||
pub async fn send_keystroke(&mut self, keystroke_text: &str) {
|
||||
let mut keystroke = Keystroke::parse(keystroke_text).unwrap();
|
||||
|
||||
if keystroke.key == "<" {
|
||||
keystroke.key = "lt".to_string()
|
||||
|
||||
@@ -197,7 +197,6 @@ actions!(
|
||||
SwapItemRight,
|
||||
TogglePreviewTab,
|
||||
TogglePinTab,
|
||||
UnpinAllTabs,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -2105,20 +2104,6 @@ impl Pane {
|
||||
}
|
||||
}
|
||||
|
||||
fn unpin_all_tabs(&mut self, _: &UnpinAllTabs, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let pinned_item_ids = self.pinned_item_ids().into_iter().rev();
|
||||
|
||||
for pinned_item_id in pinned_item_ids {
|
||||
if let Some(ix) = self.index_for_item_id(pinned_item_id) {
|
||||
self.unpin_tab_at(ix, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.change_tab_pin_state(ix, PinOperation::Pin, window, cx);
|
||||
}
|
||||
@@ -3147,7 +3132,7 @@ impl Pane {
|
||||
self.display_nav_history_buttons = display;
|
||||
}
|
||||
|
||||
fn pinned_item_ids(&self) -> Vec<EntityId> {
|
||||
fn pinned_item_ids(&self) -> HashSet<EntityId> {
|
||||
self.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -3161,7 +3146,7 @@ impl Pane {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn clean_item_ids(&self, cx: &mut Context<Pane>) -> Vec<EntityId> {
|
||||
fn clean_item_ids(&self, cx: &mut Context<Pane>) -> HashSet<EntityId> {
|
||||
self.items()
|
||||
.filter_map(|item| {
|
||||
if !item.is_dirty(cx) {
|
||||
@@ -3173,7 +3158,7 @@ impl Pane {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn to_the_side_item_ids(&self, item_id: EntityId, side: Side) -> Vec<EntityId> {
|
||||
fn to_the_side_item_ids(&self, item_id: EntityId, side: Side) -> HashSet<EntityId> {
|
||||
match side {
|
||||
Side::Left => self
|
||||
.items()
|
||||
@@ -3374,9 +3359,6 @@ impl Render for Pane {
|
||||
.on_action(cx.listener(|pane, action, window, cx| {
|
||||
pane.toggle_pin_tab(action, window, cx);
|
||||
}))
|
||||
.on_action(cx.listener(|pane, action, window, cx| {
|
||||
pane.unpin_all_tabs(action, window, cx);
|
||||
}))
|
||||
.when(PreviewTabsSettings::get_global(cx).enabled, |this| {
|
||||
this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
|
||||
if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
|
||||
@@ -4190,78 +4172,6 @@ mod tests {
|
||||
assert_item_labels(&pane, ["B*", "A", "C"], cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_unpin_all_tabs(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
// Unpin all, in an empty pane
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
|
||||
});
|
||||
|
||||
assert_item_labels(&pane, [], cx);
|
||||
|
||||
let item_a = add_labeled_item(&pane, "A", false, cx);
|
||||
let item_b = add_labeled_item(&pane, "B", false, cx);
|
||||
let item_c = add_labeled_item(&pane, "C", false, cx);
|
||||
assert_item_labels(&pane, ["A", "B", "C*"], cx);
|
||||
|
||||
// Unpin all, when no tabs are pinned
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
|
||||
});
|
||||
|
||||
assert_item_labels(&pane, ["A", "B", "C*"], cx);
|
||||
|
||||
// Pin inactive tabs only
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
|
||||
pane.pin_tab_at(ix, window, cx);
|
||||
|
||||
let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
|
||||
pane.pin_tab_at(ix, window, cx);
|
||||
});
|
||||
assert_item_labels(&pane, ["A!", "B!", "C*"], cx);
|
||||
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
|
||||
});
|
||||
|
||||
assert_item_labels(&pane, ["A", "B", "C*"], cx);
|
||||
|
||||
// Pin all tabs
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
let ix = pane.index_for_item_id(item_a.item_id()).unwrap();
|
||||
pane.pin_tab_at(ix, window, cx);
|
||||
|
||||
let ix = pane.index_for_item_id(item_b.item_id()).unwrap();
|
||||
pane.pin_tab_at(ix, window, cx);
|
||||
|
||||
let ix = pane.index_for_item_id(item_c.item_id()).unwrap();
|
||||
pane.pin_tab_at(ix, window, cx);
|
||||
});
|
||||
assert_item_labels(&pane, ["A!", "B!", "C*!"], cx);
|
||||
|
||||
// Activate middle tab
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.activate_item(1, false, false, window, cx);
|
||||
});
|
||||
assert_item_labels(&pane, ["A!", "B*!", "C!"], cx);
|
||||
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.unpin_all_tabs(&UnpinAllTabs, window, cx);
|
||||
});
|
||||
|
||||
// Order has not changed
|
||||
assert_item_labels(&pane, ["A", "B*", "C"], cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_pinning_active_tab_without_position_change_maintains_focus(
|
||||
cx: &mut TestAppContext,
|
||||
|
||||
@@ -2159,11 +2159,10 @@ impl Workspace {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
let keyboard_mapper = cx.keyboard_mapper();
|
||||
let mut keystrokes: Vec<Keystroke> = action
|
||||
.0
|
||||
.split(' ')
|
||||
.flat_map(|k| Keystroke::parse(k, keyboard_mapper).log_err())
|
||||
.flat_map(|k| Keystroke::parse(k).log_err())
|
||||
.collect();
|
||||
keystrokes.reverse();
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ enable = false
|
||||
"/assistant/context-servers.html" = "/docs/ai/mcp.html"
|
||||
"/assistant/model-context-protocol.html" = "/docs/ai/mcp.html"
|
||||
"/model-improvement.html" = "/docs/ai/ai-improvement.html"
|
||||
"/extensions/context-servers.html" = "/docs/extensions/mcp-extensions.html"
|
||||
|
||||
|
||||
# Our custom preprocessor for expanding commands like `{#kb action::ActionName}`,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user