Compare commits
51 Commits
scan-code
...
fix-launch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8308caf16c | ||
|
|
93632fbaf4 | ||
|
|
efa5f4c96a | ||
|
|
0d1d8a1437 | ||
|
|
6c4728f00f | ||
|
|
a3cc063107 | ||
|
|
7d5a5d0984 | ||
|
|
4c3ada5753 | ||
|
|
b3a8816c0e | ||
|
|
6d9bcdb2af | ||
|
|
0852912fd6 | ||
|
|
47ac01842b | ||
|
|
5b22994d9f | ||
|
|
6c0ea88f5b | ||
|
|
fc4ca346be | ||
|
|
e9570eefbf | ||
|
|
72de3143c8 | ||
|
|
ad206a6a97 | ||
|
|
1e1bc7c373 | ||
|
|
84eca53319 | ||
|
|
b4e558ce3d | ||
|
|
00a8101016 | ||
|
|
444f797827 | ||
|
|
7a14987c02 | ||
|
|
5eb68f0ea4 | ||
|
|
9c513223c4 | ||
|
|
0c0933d1c0 | ||
|
|
a4c5a2d4d3 | ||
|
|
311e136e30 | ||
|
|
4f5433a180 | ||
|
|
295db79c47 | ||
|
|
71d5c57119 | ||
|
|
dd17fd3d5a | ||
|
|
e4f8c4fb4c | ||
|
|
e62e9facf0 | ||
|
|
3f419b32f8 | ||
|
|
5270844b42 | ||
|
|
f567bb52ff | ||
|
|
c55630889a | ||
|
|
e0ca4270b4 | ||
|
|
02dfaf7799 | ||
|
|
c9972ca532 | ||
|
|
9334e152b4 | ||
|
|
9c47c52de5 | ||
|
|
286b97c0de | ||
|
|
415d482395 | ||
|
|
a9d0eee2a9 | ||
|
|
e4e3409952 | ||
|
|
46f98b6001 | ||
|
|
c1a4a24bce | ||
|
|
eb5f59577d |
2
.github/actions/build_docs/action.yml
vendored
2
.github/actions/build_docs/action.yml
vendored
@@ -22,7 +22,7 @@ runs:
|
||||
- name: Check for broken links
|
||||
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
|
||||
with:
|
||||
args: --no-progress './docs/src/**/*'
|
||||
args: --no-progress --exclude '^http' './docs/src/**/*'
|
||||
fail: true
|
||||
|
||||
- name: Build book
|
||||
|
||||
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
|
||||
|
||||
3
.github/workflows/ci.yml
vendored
3
.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: |
|
||||
@@ -803,6 +801,7 @@ jobs:
|
||||
name: Build with Nix
|
||||
uses: ./.github/workflows/nix.yml
|
||||
if: github.repository_owner == 'zed-industries' && contains(github.event.pull_request.labels.*.name, 'run-nix')
|
||||
secrets: inherit
|
||||
with:
|
||||
flake-output: debug
|
||||
# excludes the final package to only cache dependencies
|
||||
|
||||
1
.github/workflows/eval.yml
vendored
1
.github/workflows/eval.yml
vendored
@@ -30,6 +30,7 @@ jobs:
|
||||
noop:
|
||||
name: No-op
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
steps:
|
||||
- name: No-op
|
||||
run: echo "Nothing to do"
|
||||
|
||||
1
.github/workflows/release_nightly.yml
vendored
1
.github/workflows/release_nightly.yml
vendored
@@ -214,6 +214,7 @@ jobs:
|
||||
bundle-nix:
|
||||
name: Build and cache Nix package
|
||||
needs: tests
|
||||
secrets: inherit
|
||||
uses: ./.github/workflows/nix.yml
|
||||
|
||||
update-nightly-tag:
|
||||
|
||||
1
.github/workflows/unit_evals.yml
vendored
1
.github/workflows/unit_evals.yml
vendored
@@ -19,6 +19,7 @@ env:
|
||||
|
||||
jobs:
|
||||
unit_evals:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
timeout-minutes: 60
|
||||
name: Run unit evals
|
||||
runs-on:
|
||||
|
||||
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -4027,6 +4027,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"http_client",
|
||||
"language",
|
||||
"libc",
|
||||
"log",
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
@@ -4050,7 +4051,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "dap-types"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/zed-industries/dap-types?rev=68516de327fa1be15214133a0a2e52a12982ce75#68516de327fa1be15214133a0a2e52a12982ce75"
|
||||
source = "git+https://github.com/zed-industries/dap-types?rev=b40956a7f4d1939da67429d941389ee306a3a308#b40956a7f4d1939da67429d941389ee306a3a308"
|
||||
dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -4695,7 +4696,6 @@ dependencies = [
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"convert_case 0.8.0",
|
||||
"ctor",
|
||||
"dap",
|
||||
@@ -9003,7 +9003,6 @@ dependencies = [
|
||||
"tree-sitter-yaml",
|
||||
"unindent",
|
||||
"util",
|
||||
"which 6.0.3",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -435,7 +435,7 @@ core-foundation-sys = "0.8.6"
|
||||
core-video = { version = "0.4.3", features = ["metal"] }
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
ctor = "0.4.0"
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "68516de327fa1be15214133a0a2e52a12982ce75" }
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b40956a7f4d1939da67429d941389ee306a3a308" }
|
||||
dashmap = "6.0"
|
||||
derive_more = "0.99.17"
|
||||
dirs = "4.0"
|
||||
@@ -698,6 +698,8 @@ codegen-units = 16
|
||||
[profile.dev.package]
|
||||
taffy = { opt-level = 3 }
|
||||
cranelift-codegen = { opt-level = 3 }
|
||||
cranelift-codegen-meta = { opt-level = 3 }
|
||||
cranelift-codegen-shared = { opt-level = 3 }
|
||||
resvg = { opt-level = 3 }
|
||||
rustybuzz = { opt-level = 3 }
|
||||
ttf-parser = { opt-level = 3 }
|
||||
|
||||
1
assets/icons/circle_help.svg
Normal file
1
assets/icons/circle_help.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-help-icon lucide-circle-help"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>
|
||||
|
After Width: | Height: | Size: 348 B |
@@ -395,6 +395,8 @@
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePreviousItem",
|
||||
"insert": "vim::InsertBefore",
|
||||
".": "vim::Repeat",
|
||||
"alt-.": "vim::RepeatFind",
|
||||
// tree-sitter related commands
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
"] x": "editor::SelectSmallerSyntaxNode",
|
||||
@@ -421,6 +423,7 @@
|
||||
|
||||
"x": "editor::SelectLine",
|
||||
"shift-x": "editor::SelectLine",
|
||||
"%": "editor::SelectAll",
|
||||
// Window mode
|
||||
"space w h": "workspace::ActivatePaneLeft",
|
||||
"space w l": "workspace::ActivatePaneRight",
|
||||
@@ -450,7 +453,8 @@
|
||||
"ctrl-c": "editor::ToggleComments",
|
||||
"d": "vim::HelixDelete",
|
||||
"c": "vim::Substitute",
|
||||
"shift-c": "editor::AddSelectionBelow"
|
||||
"shift-c": "editor::AddSelectionBelow",
|
||||
"alt-shift-c": "editor::AddSelectionAbove"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -445,7 +445,9 @@
|
||||
// Whether to show breakpoints in the gutter.
|
||||
"breakpoints": true,
|
||||
// Whether to show fold buttons in the gutter.
|
||||
"folds": true
|
||||
"folds": true,
|
||||
// Minimum number of characters to reserve space for in the gutter.
|
||||
"min_line_number_digits": 4
|
||||
},
|
||||
"indent_guides": {
|
||||
// Whether to show indent guides in the editor.
|
||||
@@ -1478,7 +1480,8 @@
|
||||
"Go": {
|
||||
"code_actions_on_format": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
},
|
||||
"debuggers": ["Delve"]
|
||||
},
|
||||
"GraphQL": {
|
||||
"prettier": {
|
||||
@@ -1543,9 +1546,15 @@
|
||||
"Plain Text": {
|
||||
"allow_rewrap": "anywhere"
|
||||
},
|
||||
"Python": {
|
||||
"debuggers": ["Debugpy"]
|
||||
},
|
||||
"Ruby": {
|
||||
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."]
|
||||
},
|
||||
"Rust": {
|
||||
"debuggers": ["CodeLLDB"]
|
||||
},
|
||||
"SCSS": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Static tasks configuration.
|
||||
// Project tasks configuration. See https://zed.dev/docs/tasks for documentation.
|
||||
//
|
||||
// Example:
|
||||
[
|
||||
|
||||
@@ -1605,6 +1605,7 @@ impl ActiveThread {
|
||||
|
||||
this.thread.update(cx, |thread, cx| {
|
||||
thread.advance_prompt_id();
|
||||
thread.cancel_last_completion(Some(window.window_handle()), cx);
|
||||
thread.send_to_model(
|
||||
model.model,
|
||||
CompletionIntent::UserPrompt,
|
||||
@@ -3706,7 +3707,7 @@ mod tests {
|
||||
use util::path;
|
||||
use workspace::CollaboratorId;
|
||||
|
||||
use crate::{ContextLoadResult, thread_store};
|
||||
use crate::{ContextLoadResult, thread::MessageSegment, thread_store};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -3840,6 +3841,114 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_editing_message_cancels_previous_completion(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
|
||||
let (cx, active_thread, _, thread, model) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
cx.update(|_, cx| {
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.set_default_model(
|
||||
Some(ConfiguredModel {
|
||||
provider: Arc::new(FakeLanguageModelProvider),
|
||||
model: model.clone(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Track thread events to verify cancellation
|
||||
let cancellation_events = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let new_request_events = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
|
||||
let _subscription = cx.update(|_, cx| {
|
||||
let cancellation_events = cancellation_events.clone();
|
||||
let new_request_events = new_request_events.clone();
|
||||
cx.subscribe(
|
||||
&thread,
|
||||
move |_thread, event: &ThreadEvent, _cx| match event {
|
||||
ThreadEvent::CompletionCanceled => {
|
||||
cancellation_events.lock().unwrap().push(());
|
||||
}
|
||||
ThreadEvent::NewRequest => {
|
||||
new_request_events.lock().unwrap().push(());
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
// Insert a user message and start streaming a response
|
||||
let message = thread.update(cx, |thread, cx| {
|
||||
let message_id = thread.insert_user_message(
|
||||
"Hello, how are you?",
|
||||
ContextLoadResult::default(),
|
||||
None,
|
||||
vec![],
|
||||
cx,
|
||||
);
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(
|
||||
model.clone(),
|
||||
CompletionIntent::UserPrompt,
|
||||
cx.active_window(),
|
||||
cx,
|
||||
);
|
||||
thread.message(message_id).cloned().unwrap()
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// Verify that a completion is in progress
|
||||
assert!(cx.read(|cx| thread.read(cx).is_generating()));
|
||||
assert_eq!(new_request_events.lock().unwrap().len(), 1);
|
||||
|
||||
// Edit the message while the completion is still running
|
||||
active_thread.update_in(cx, |active_thread, window, cx| {
|
||||
active_thread.start_editing_message(
|
||||
message.id,
|
||||
message.segments.as_slice(),
|
||||
message.creases.as_slice(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let editor = active_thread
|
||||
.editing_message
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.1
|
||||
.editor
|
||||
.clone();
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_text("What is the weather like?", window, cx);
|
||||
});
|
||||
active_thread.confirm_editing_message(&Default::default(), window, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// Verify that the previous completion was cancelled
|
||||
assert_eq!(cancellation_events.lock().unwrap().len(), 1);
|
||||
|
||||
// Verify that a new request was started after cancellation
|
||||
assert_eq!(new_request_events.lock().unwrap().len(), 2);
|
||||
|
||||
// Verify that the edited message contains the new text
|
||||
let edited_message =
|
||||
thread.update(cx, |thread, _| thread.message(message.id).cloned().unwrap());
|
||||
match &edited_message.segments[0] {
|
||||
MessageSegment::Text(text) => {
|
||||
assert_eq!(text, "What is the weather like?");
|
||||
}
|
||||
_ => panic!("Expected text segment"),
|
||||
}
|
||||
}
|
||||
|
||||
fn init_test_settings(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
|
||||
@@ -91,12 +91,13 @@ impl AgentModelSelector {
|
||||
|
||||
impl Render for AgentModelSelector {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let model = self.selector.read(cx).delegate.active_model(cx);
|
||||
let model_name = model
|
||||
.map(|model| model.model.name().0)
|
||||
.unwrap_or_else(|| SharedString::from("No model selected"));
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
self.selector.clone(),
|
||||
Button::new("active-model", model_name)
|
||||
|
||||
@@ -71,6 +71,10 @@ fn show_configure_mcp_modal(
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, Workspace>,
|
||||
) {
|
||||
if !window.is_window_active() {
|
||||
return;
|
||||
}
|
||||
|
||||
let context_server_store = workspace.project().read(cx).context_server_store();
|
||||
let repository: Option<SharedString> = manifest.repository.as_ref().map(|s| s.clone().into());
|
||||
|
||||
|
||||
@@ -38,8 +38,7 @@ use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||
use text::{OffsetRangeExt, ToPoint as _};
|
||||
use ui::prelude::*;
|
||||
use util::RangeExt;
|
||||
use util::ResultExt;
|
||||
use util::{RangeExt, ResultExt, maybe};
|
||||
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
|
||||
use zed_actions::agent::OpenConfiguration;
|
||||
|
||||
@@ -1171,27 +1170,31 @@ impl InlineAssistant {
|
||||
selections.select_anchor_ranges([position..position])
|
||||
});
|
||||
|
||||
let mut scroll_target_top;
|
||||
let mut scroll_target_bottom;
|
||||
let mut scroll_target_range = None;
|
||||
if let Some(decorations) = assist.decorations.as_ref() {
|
||||
scroll_target_top = editor
|
||||
.row_for_block(decorations.prompt_block_id, cx)
|
||||
.unwrap()
|
||||
.0 as f32;
|
||||
scroll_target_bottom = editor
|
||||
.row_for_block(decorations.end_block_id, cx)
|
||||
.unwrap()
|
||||
.0 as f32;
|
||||
} else {
|
||||
scroll_target_range = maybe!({
|
||||
let top = editor.row_for_block(decorations.prompt_block_id, cx)?.0 as f32;
|
||||
let bottom = editor.row_for_block(decorations.end_block_id, cx)?.0 as f32;
|
||||
Some((top, bottom))
|
||||
});
|
||||
if scroll_target_range.is_none() {
|
||||
log::error!("bug: failed to find blocks for scrolling to inline assist");
|
||||
}
|
||||
}
|
||||
let scroll_target_range = scroll_target_range.unwrap_or_else(|| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let start_row = assist
|
||||
.range
|
||||
.start
|
||||
.to_display_point(&snapshot.display_snapshot)
|
||||
.row();
|
||||
scroll_target_top = start_row.0 as f32;
|
||||
scroll_target_bottom = scroll_target_top + 1.;
|
||||
}
|
||||
let top = start_row.0 as f32;
|
||||
let bottom = top + 1.0;
|
||||
(top, bottom)
|
||||
});
|
||||
let mut scroll_target_top = scroll_target_range.0;
|
||||
let mut scroll_target_bottom = scroll_target_range.1;
|
||||
|
||||
scroll_target_top -= editor.vertical_scroll_margin() as f32;
|
||||
scroll_target_bottom += editor.vertical_scroll_margin() as f32;
|
||||
|
||||
|
||||
@@ -722,6 +722,7 @@ impl MessageEditor {
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.flex_wrap()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -731,6 +732,7 @@ impl MessageEditor {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.flex_wrap()
|
||||
.when(!incompatible_tools.is_empty(), |this| {
|
||||
this.child(
|
||||
IconButton::new(
|
||||
|
||||
@@ -594,10 +594,11 @@ impl Render for ThreadHistory {
|
||||
view.pr_5()
|
||||
.child(
|
||||
uniform_list(
|
||||
cx.entity().clone(),
|
||||
"thread-history",
|
||||
self.list_item_count(),
|
||||
Self::list_items,
|
||||
cx.processor(|this, range: Range<usize>, window, cx| {
|
||||
this.list_items(range, window, cx)
|
||||
}),
|
||||
)
|
||||
.p_1()
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
|
||||
@@ -671,7 +671,7 @@ async fn test_remote_server_debugger(
|
||||
});
|
||||
|
||||
session.update(cx_a, |session, _| {
|
||||
assert_eq!(session.binary().command, "ssh");
|
||||
assert_eq!(session.binary().command.as_deref(), Some("ssh"));
|
||||
});
|
||||
|
||||
let shutdown_session = workspace.update(cx_a, |workspace, cx| {
|
||||
|
||||
@@ -51,6 +51,9 @@ telemetry.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
libc.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
async-pipe.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -181,7 +181,7 @@ impl DebugTaskDefinition {
|
||||
/// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct DebugAdapterBinary {
|
||||
pub command: String,
|
||||
pub command: Option<String>,
|
||||
pub arguments: Vec<String>,
|
||||
pub envs: HashMap<String, String>,
|
||||
pub cwd: Option<PathBuf>,
|
||||
@@ -369,6 +369,10 @@ pub trait DebugAdapter: 'static + Send + Sync {
|
||||
}
|
||||
|
||||
async fn dap_schema(&self) -> serde_json::Value;
|
||||
|
||||
fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -433,7 +437,7 @@ impl DebugAdapter for FakeAdapter {
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
Ok(DebugAdapterBinary {
|
||||
command: "command".into(),
|
||||
command: Some("command".into()),
|
||||
arguments: vec![],
|
||||
connection: None,
|
||||
envs: HashMap::default(),
|
||||
|
||||
@@ -297,7 +297,7 @@ mod tests {
|
||||
let client = DebugAdapterClient::start(
|
||||
crate::client::SessionId(1),
|
||||
DebugAdapterBinary {
|
||||
command: "command".into(),
|
||||
command: Some("command".into()),
|
||||
arguments: Default::default(),
|
||||
envs: Default::default(),
|
||||
connection: None,
|
||||
@@ -367,7 +367,7 @@ mod tests {
|
||||
let client = DebugAdapterClient::start(
|
||||
crate::client::SessionId(1),
|
||||
DebugAdapterBinary {
|
||||
command: "command".into(),
|
||||
command: Some("command".into()),
|
||||
arguments: Default::default(),
|
||||
envs: Default::default(),
|
||||
connection: None,
|
||||
@@ -420,7 +420,7 @@ mod tests {
|
||||
let client = DebugAdapterClient::start(
|
||||
crate::client::SessionId(1),
|
||||
DebugAdapterBinary {
|
||||
command: "command".into(),
|
||||
command: Some("command".into()),
|
||||
arguments: Default::default(),
|
||||
envs: Default::default(),
|
||||
connection: None,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use dap_types::{
|
||||
ErrorResponse,
|
||||
messages::{Message, Response},
|
||||
@@ -12,7 +12,6 @@ use smol::{
|
||||
io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader},
|
||||
lock::Mutex,
|
||||
net::{TcpListener, TcpStream},
|
||||
process::Child,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
@@ -22,7 +21,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use task::TcpArgumentsTemplate;
|
||||
use util::{ConnectionResult, ResultExt as _};
|
||||
use util::ConnectionResult;
|
||||
|
||||
use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings};
|
||||
|
||||
@@ -86,10 +85,12 @@ impl Transport {
|
||||
TcpTransport::start(binary, cx)
|
||||
.await
|
||||
.map(|(transports, tcp)| (transports, Self::Tcp(tcp)))
|
||||
.context("Tried to connect to a debug adapter via TCP transport layer")
|
||||
} else {
|
||||
StdioTransport::start(binary, cx)
|
||||
.await
|
||||
.map(|(transports, stdio)| (transports, Self::Stdio(stdio)))
|
||||
.context("Tried to connect to a debug adapter via stdin/stdout transport layer")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +103,7 @@ impl Transport {
|
||||
}
|
||||
}
|
||||
|
||||
async fn kill(&self) -> Result<()> {
|
||||
async fn kill(&self) {
|
||||
match self {
|
||||
Transport::Stdio(stdio_transport) => stdio_transport.kill().await,
|
||||
Transport::Tcp(tcp_transport) => tcp_transport.kill().await,
|
||||
@@ -191,7 +192,7 @@ impl TransportDelegate {
|
||||
match Self::handle_output(
|
||||
params.output,
|
||||
client_tx,
|
||||
pending_requests,
|
||||
pending_requests.clone(),
|
||||
output_log_handler,
|
||||
)
|
||||
.await
|
||||
@@ -199,6 +200,12 @@ impl TransportDelegate {
|
||||
Ok(()) => {}
|
||||
Err(e) => log::error!("Error handling debugger output: {e}"),
|
||||
}
|
||||
let mut pending_requests = pending_requests.lock().await;
|
||||
pending_requests.drain().for_each(|(_, request)| {
|
||||
request
|
||||
.send(Err(anyhow!("debugger shutdown unexpectedly")))
|
||||
.ok();
|
||||
});
|
||||
}));
|
||||
|
||||
if let Some(stderr) = params.stderr.take() {
|
||||
@@ -531,7 +538,7 @@ impl TransportDelegate {
|
||||
current_requests.clear();
|
||||
pending_requests.clear();
|
||||
|
||||
let _ = self.transport.kill().await.log_err();
|
||||
self.transport.kill().await;
|
||||
|
||||
drop(current_requests);
|
||||
drop(pending_requests);
|
||||
@@ -562,7 +569,7 @@ pub struct TcpTransport {
|
||||
pub port: u16,
|
||||
pub host: Ipv4Addr,
|
||||
pub timeout: u64,
|
||||
process: Mutex<Child>,
|
||||
process: Option<Mutex<Child>>,
|
||||
}
|
||||
|
||||
impl TcpTransport {
|
||||
@@ -591,26 +598,23 @@ impl TcpTransport {
|
||||
let host = connection_args.host;
|
||||
let port = connection_args.port;
|
||||
|
||||
let mut command = util::command::new_std_command(&binary.command);
|
||||
util::set_pre_exec_to_start_new_session(&mut command);
|
||||
let mut command = smol::process::Command::from(command);
|
||||
let mut process = if let Some(command) = &binary.command {
|
||||
let mut command = util::command::new_std_command(&command);
|
||||
|
||||
if let Some(cwd) = &binary.cwd {
|
||||
command.current_dir(cwd);
|
||||
}
|
||||
if let Some(cwd) = &binary.cwd {
|
||||
command.current_dir(cwd);
|
||||
}
|
||||
|
||||
command.args(&binary.arguments);
|
||||
command.envs(&binary.envs);
|
||||
command.args(&binary.arguments);
|
||||
command.envs(&binary.envs);
|
||||
|
||||
command
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
let mut process = command
|
||||
.spawn()
|
||||
.with_context(|| "failed to start debug adapter.")?;
|
||||
Some(
|
||||
Child::spawn(command, Stdio::null())
|
||||
.with_context(|| "failed to start debug adapter.")?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let address = SocketAddrV4::new(host, port);
|
||||
|
||||
@@ -628,15 +632,18 @@ impl TcpTransport {
|
||||
match TcpStream::connect(address).await {
|
||||
Ok(stream) => return Ok((process, stream.split())),
|
||||
Err(_) => {
|
||||
if let Ok(Some(_)) = process.try_status() {
|
||||
let output = process.output().await?;
|
||||
let output = if output.stderr.is_empty() {
|
||||
String::from_utf8_lossy(&output.stdout).to_string()
|
||||
} else {
|
||||
String::from_utf8_lossy(&output.stderr).to_string()
|
||||
};
|
||||
anyhow::bail!("{output}\nerror: process exited before debugger attached.");
|
||||
if let Some(p) = &mut process {
|
||||
if let Ok(Some(_)) = p.try_status() {
|
||||
let output = process.take().unwrap().into_inner().output().await?;
|
||||
let output = if output.stderr.is_empty() {
|
||||
String::from_utf8_lossy(&output.stdout).to_string()
|
||||
} else {
|
||||
String::from_utf8_lossy(&output.stderr).to_string()
|
||||
};
|
||||
anyhow::bail!("{output}\nerror: process exited before debugger attached.");
|
||||
}
|
||||
}
|
||||
|
||||
cx.background_executor().timer(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
@@ -649,13 +656,13 @@ impl TcpTransport {
|
||||
host,
|
||||
port
|
||||
);
|
||||
let stdout = process.stdout.take();
|
||||
let stderr = process.stderr.take();
|
||||
let stdout = process.as_mut().and_then(|p| p.stdout.take());
|
||||
let stderr = process.as_mut().and_then(|p| p.stderr.take());
|
||||
|
||||
let this = Self {
|
||||
port,
|
||||
host,
|
||||
process: Mutex::new(process),
|
||||
process: process.map(Mutex::new),
|
||||
timeout,
|
||||
};
|
||||
|
||||
@@ -673,10 +680,19 @@ impl TcpTransport {
|
||||
true
|
||||
}
|
||||
|
||||
async fn kill(&self) -> Result<()> {
|
||||
self.process.lock().await.kill()?;
|
||||
async fn kill(&self) {
|
||||
if let Some(process) = &self.process {
|
||||
let mut process = process.lock().await;
|
||||
Child::kill(&mut process);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
impl Drop for TcpTransport {
|
||||
fn drop(&mut self) {
|
||||
if let Some(mut p) = self.process.take() {
|
||||
p.get_mut().kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,9 +703,12 @@ pub struct StdioTransport {
|
||||
impl StdioTransport {
|
||||
#[allow(dead_code, reason = "This is used in non test builds of Zed")]
|
||||
async fn start(binary: &DebugAdapterBinary, _: AsyncApp) -> Result<(TransportPipe, Self)> {
|
||||
let mut command = util::command::new_std_command(&binary.command);
|
||||
util::set_pre_exec_to_start_new_session(&mut command);
|
||||
let mut command = smol::process::Command::from(command);
|
||||
let Some(binary_command) = &binary.command else {
|
||||
bail!(
|
||||
"When using the `stdio` transport, the path to a debug adapter binary must be set by Zed."
|
||||
);
|
||||
};
|
||||
let mut command = util::command::new_std_command(&binary_command);
|
||||
|
||||
if let Some(cwd) = &binary.cwd {
|
||||
command.current_dir(cwd);
|
||||
@@ -698,16 +717,10 @@ impl StdioTransport {
|
||||
command.args(&binary.arguments);
|
||||
command.envs(&binary.envs);
|
||||
|
||||
command
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
let mut process = command.spawn().with_context(|| {
|
||||
let mut process = Child::spawn(command, Stdio::piped()).with_context(|| {
|
||||
format!(
|
||||
"failed to spawn command `{} {}`.",
|
||||
binary.command,
|
||||
binary_command,
|
||||
binary.arguments.join(" ")
|
||||
)
|
||||
})?;
|
||||
@@ -722,7 +735,7 @@ impl StdioTransport {
|
||||
if stderr.is_none() {
|
||||
bail!(
|
||||
"Failed to connect to stderr for debug adapter command {}",
|
||||
&binary.command
|
||||
&binary_command
|
||||
);
|
||||
}
|
||||
|
||||
@@ -745,9 +758,15 @@ impl StdioTransport {
|
||||
false
|
||||
}
|
||||
|
||||
async fn kill(&self) -> Result<()> {
|
||||
self.process.lock().await.kill()?;
|
||||
Ok(())
|
||||
async fn kill(&self) {
|
||||
let mut process = self.process.lock().await;
|
||||
Child::kill(&mut process);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for StdioTransport {
|
||||
fn drop(&mut self) {
|
||||
self.process.get_mut().kill();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -921,7 +940,66 @@ impl FakeTransport {
|
||||
false
|
||||
}
|
||||
|
||||
async fn kill(&self) -> Result<()> {
|
||||
Ok(())
|
||||
async fn kill(&self) {}
|
||||
}
|
||||
|
||||
struct Child {
|
||||
process: smol::process::Child,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Child {
|
||||
type Target = smol::process::Child;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.process
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for Child {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.process
|
||||
}
|
||||
}
|
||||
|
||||
impl Child {
|
||||
fn into_inner(self) -> smol::process::Child {
|
||||
self.process
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn spawn(mut command: std::process::Command, stdin: Stdio) -> Result<Self> {
|
||||
util::set_pre_exec_to_start_new_session(&mut command);
|
||||
let process = smol::process::Command::from(command)
|
||||
.stdin(stdin)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
Ok(Self { process })
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn spawn(command: std::process::Command, stdin: Stdio) -> Result<Self> {
|
||||
// TODO(windows): create a job object and add the child process handle to it,
|
||||
// see https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects
|
||||
let process = smol::process::Command::from(command)
|
||||
.stdin(stdin)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
Ok(Self { process })
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn kill(&mut self) {
|
||||
let pid = self.process.id();
|
||||
unsafe {
|
||||
libc::killpg(pid as i32, libc::SIGKILL);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn kill(&mut self) {
|
||||
// TODO(windows): terminate the job object in kill
|
||||
let _ = self.process.kill();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,18 +21,21 @@ impl CodeLldbDebugAdapter {
|
||||
|
||||
fn request_args(
|
||||
&self,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
task_definition: &DebugTaskDefinition,
|
||||
) -> Result<dap::StartDebuggingRequestArguments> {
|
||||
// CodeLLDB uses `name` for a terminal label.
|
||||
let mut configuration = task_definition.config.clone();
|
||||
|
||||
configuration
|
||||
let obj = configuration
|
||||
.as_object_mut()
|
||||
.context("CodeLLDB is not a valid json object")?
|
||||
.insert(
|
||||
"name".into(),
|
||||
Value::String(String::from(task_definition.label.as_ref())),
|
||||
);
|
||||
.context("CodeLLDB is not a valid json object")?;
|
||||
|
||||
obj.entry("name")
|
||||
.or_insert(Value::String(String::from(task_definition.label.as_ref())));
|
||||
|
||||
obj.entry("cwd")
|
||||
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
|
||||
|
||||
let request = self.request_kind(&configuration)?;
|
||||
|
||||
@@ -359,13 +362,13 @@ impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
};
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: command.unwrap(),
|
||||
command: Some(command.unwrap()),
|
||||
cwd: Some(delegate.worktree_root_path().to_path_buf()),
|
||||
arguments: vec![
|
||||
"--settings".into(),
|
||||
json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
|
||||
],
|
||||
request_args: self.request_args(&config)?,
|
||||
request_args: self.request_args(delegate, &config)?,
|
||||
envs: HashMap::default(),
|
||||
connection: None,
|
||||
})
|
||||
|
||||
@@ -177,18 +177,23 @@ impl DebugAdapter for GdbDebugAdapter {
|
||||
|
||||
let gdb_path = user_setting_path.unwrap_or(gdb_path?);
|
||||
|
||||
let request_args = StartDebuggingRequestArguments {
|
||||
request: self.request_kind(&config.config)?,
|
||||
configuration: config.config.clone(),
|
||||
};
|
||||
let mut configuration = config.config.clone();
|
||||
if let Some(configuration) = configuration.as_object_mut() {
|
||||
configuration
|
||||
.entry("cwd")
|
||||
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
|
||||
}
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: gdb_path,
|
||||
command: Some(gdb_path),
|
||||
arguments: vec!["-i=dap".into()],
|
||||
envs: HashMap::default(),
|
||||
cwd: Some(delegate.worktree_root_path().to_path_buf()),
|
||||
connection: None,
|
||||
request_args,
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
request: self.request_kind(&config.config)?,
|
||||
configuration,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,14 +462,21 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
]
|
||||
};
|
||||
|
||||
let mut configuration = task_definition.config.clone();
|
||||
if let Some(configuration) = configuration.as_object_mut() {
|
||||
configuration
|
||||
.entry("cwd")
|
||||
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
|
||||
}
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: minidelve_path.to_string_lossy().into_owned(),
|
||||
command: Some(minidelve_path.to_string_lossy().into_owned()),
|
||||
arguments,
|
||||
cwd: Some(cwd),
|
||||
envs: HashMap::default(),
|
||||
connection: None,
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
configuration: task_definition.config.clone(),
|
||||
configuration,
|
||||
request: self.request_kind(&task_definition.config)?,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ use adapters::latest_github_release;
|
||||
use anyhow::Context as _;
|
||||
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
|
||||
use gpui::AsyncApp;
|
||||
use serde_json::Value;
|
||||
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
|
||||
use task::DebugRequest;
|
||||
use util::ResultExt;
|
||||
@@ -68,13 +69,24 @@ impl JsDebugAdapter {
|
||||
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
|
||||
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
|
||||
|
||||
let mut configuration = task_definition.config.clone();
|
||||
if let Some(configuration) = configuration.as_object_mut() {
|
||||
configuration
|
||||
.entry("cwd")
|
||||
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
|
||||
|
||||
configuration.entry("type").and_modify(normalize_task_type);
|
||||
}
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: delegate
|
||||
.node_runtime()
|
||||
.binary_path()
|
||||
.await?
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
command: Some(
|
||||
delegate
|
||||
.node_runtime()
|
||||
.binary_path()
|
||||
.await?
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
),
|
||||
arguments: vec![
|
||||
adapter_path
|
||||
.join(Self::ADAPTER_PATH)
|
||||
@@ -91,7 +103,7 @@ impl JsDebugAdapter {
|
||||
timeout,
|
||||
}),
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
configuration: task_definition.config.clone(),
|
||||
configuration,
|
||||
request: self.request_kind(&task_definition.config)?,
|
||||
},
|
||||
})
|
||||
@@ -171,7 +183,7 @@ impl DebugAdapter for JsDebugAdapter {
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["pwa-node", "node", "chrome", "pwa-chrome", "edge", "pwa-edge"],
|
||||
"enum": ["pwa-node", "node", "chrome", "pwa-chrome", "msedge", "pwa-msedge"],
|
||||
"description": "The type of debug session",
|
||||
"default": "pwa-node"
|
||||
},
|
||||
@@ -431,4 +443,25 @@ impl DebugAdapter for JsDebugAdapter {
|
||||
self.get_installed_binary(delegate, &config, user_installed_path, cx)
|
||||
.await
|
||||
}
|
||||
|
||||
fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
|
||||
let label = args.configuration.get("name")?.as_str()?;
|
||||
Some(label.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_task_type(task_type: &mut Value) {
|
||||
let Some(task_type_str) = task_type.as_str() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let new_name = match task_type_str {
|
||||
"node" | "pwa-node" => "pwa-node",
|
||||
"chrome" | "pwa-chrome" => "pwa-chrome",
|
||||
"edge" | "msedge" | "pwa-edge" | "pwa-msedge" => "pwa-msedge",
|
||||
_ => task_type_str,
|
||||
}
|
||||
.to_owned();
|
||||
|
||||
*task_type = Value::String(new_name);
|
||||
}
|
||||
|
||||
@@ -71,13 +71,21 @@ impl PhpDebugAdapter {
|
||||
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
|
||||
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
|
||||
|
||||
let mut configuration = task_definition.config.clone();
|
||||
if let Some(obj) = configuration.as_object_mut() {
|
||||
obj.entry("cwd")
|
||||
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
|
||||
}
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: delegate
|
||||
.node_runtime()
|
||||
.binary_path()
|
||||
.await?
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
command: Some(
|
||||
delegate
|
||||
.node_runtime()
|
||||
.binary_path()
|
||||
.await?
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
),
|
||||
arguments: vec![
|
||||
adapter_path
|
||||
.join(Self::ADAPTER_PATH)
|
||||
@@ -93,7 +101,7 @@ impl PhpDebugAdapter {
|
||||
cwd: Some(delegate.worktree_root_path().to_path_buf()),
|
||||
envs: HashMap::default(),
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
configuration: task_definition.config.clone(),
|
||||
configuration,
|
||||
request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)?,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -83,6 +83,7 @@ impl PythonDebugAdapter {
|
||||
|
||||
fn request_args(
|
||||
&self,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
task_definition: &DebugTaskDefinition,
|
||||
) -> Result<StartDebuggingRequestArguments> {
|
||||
let request = self.request_kind(&task_definition.config)?;
|
||||
@@ -95,6 +96,11 @@ impl PythonDebugAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(obj) = configuration.as_object_mut() {
|
||||
obj.entry("cwd")
|
||||
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
|
||||
}
|
||||
|
||||
Ok(StartDebuggingRequestArguments {
|
||||
configuration,
|
||||
request,
|
||||
@@ -187,7 +193,7 @@ impl PythonDebugAdapter {
|
||||
);
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: python_command,
|
||||
command: Some(python_command),
|
||||
arguments,
|
||||
connection: Some(adapters::TcpArguments {
|
||||
host,
|
||||
@@ -196,7 +202,7 @@ impl PythonDebugAdapter {
|
||||
}),
|
||||
cwd: Some(delegate.worktree_root_path().to_path_buf()),
|
||||
envs: HashMap::default(),
|
||||
request_args: self.request_args(config)?,
|
||||
request_args: self.request_args(delegate, config)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,8 +174,15 @@ impl DebugAdapter for RubyDebugAdapter {
|
||||
|
||||
arguments.extend(ruby_config.args);
|
||||
|
||||
let mut configuration = definition.config.clone();
|
||||
if let Some(configuration) = configuration.as_object_mut() {
|
||||
configuration
|
||||
.entry("cwd")
|
||||
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
|
||||
}
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: rdbg_path.to_string_lossy().to_string(),
|
||||
command: Some(rdbg_path.to_string_lossy().to_string()),
|
||||
arguments,
|
||||
connection: Some(dap::adapters::TcpArguments {
|
||||
host,
|
||||
@@ -190,7 +197,7 @@ impl DebugAdapter for RubyDebugAdapter {
|
||||
envs: ruby_config.env.into_iter().collect(),
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
request: self.request_kind(&definition.config)?,
|
||||
configuration: definition.config.clone(),
|
||||
configuration,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,19 +4,17 @@ use crate::session::running::RunningState;
|
||||
use crate::{
|
||||
ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
|
||||
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, NewProcessModal,
|
||||
NewProcessMode, Pause, Restart, ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop,
|
||||
ToggleExpandItem, ToggleIgnoreBreakpoints, ToggleSessionPicker, ToggleThreadPicker,
|
||||
persistence, spawn_task_or_modal,
|
||||
NewProcessMode, Pause, Restart, StepInto, StepOut, StepOver, Stop, ToggleExpandItem,
|
||||
ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use dap::StartDebuggingRequestArguments;
|
||||
use dap::adapters::DebugAdapterName;
|
||||
use dap::debugger_settings::DebugPanelDockPosition;
|
||||
use dap::{
|
||||
ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
|
||||
client::SessionId, debugger_settings::DebuggerSettings,
|
||||
};
|
||||
use dap::{DapRegistry, StartDebuggingRequestArguments};
|
||||
use gpui::{
|
||||
Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter,
|
||||
FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity,
|
||||
@@ -29,7 +27,6 @@ use project::{Fs, WorktreeId};
|
||||
use project::{Project, debugger::session::ThreadStatus};
|
||||
use rpc::proto::{self};
|
||||
use settings::Settings;
|
||||
use std::any::TypeId;
|
||||
use std::sync::Arc;
|
||||
use task::{DebugScenario, TaskContext};
|
||||
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
@@ -140,82 +137,6 @@ impl DebugPanel {
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
}
|
||||
|
||||
pub(crate) fn filter_action_types(&self, cx: &mut App) {
|
||||
let (has_active_session, supports_restart, support_step_back, status) = self
|
||||
.active_session()
|
||||
.map(|item| {
|
||||
let running = item.read(cx).running_state().clone();
|
||||
let caps = running.read(cx).capabilities(cx);
|
||||
(
|
||||
!running.read(cx).session().read(cx).is_terminated(),
|
||||
caps.supports_restart_request.unwrap_or_default(),
|
||||
caps.supports_step_back.unwrap_or_default(),
|
||||
running.read(cx).thread_status(cx),
|
||||
)
|
||||
})
|
||||
.unwrap_or((false, false, false, None));
|
||||
|
||||
let filter = CommandPaletteFilter::global_mut(cx);
|
||||
let debugger_action_types = [
|
||||
TypeId::of::<Detach>(),
|
||||
TypeId::of::<Stop>(),
|
||||
TypeId::of::<ToggleIgnoreBreakpoints>(),
|
||||
];
|
||||
|
||||
let running_action_types = [TypeId::of::<Pause>()];
|
||||
|
||||
let stopped_action_type = [
|
||||
TypeId::of::<Continue>(),
|
||||
TypeId::of::<StepOver>(),
|
||||
TypeId::of::<StepInto>(),
|
||||
TypeId::of::<StepOut>(),
|
||||
TypeId::of::<ShowStackTrace>(),
|
||||
TypeId::of::<editor::actions::DebuggerRunToCursor>(),
|
||||
TypeId::of::<editor::actions::DebuggerEvaluateSelectedText>(),
|
||||
];
|
||||
|
||||
let step_back_action_type = [TypeId::of::<StepBack>()];
|
||||
let restart_action_type = [TypeId::of::<Restart>()];
|
||||
|
||||
if has_active_session {
|
||||
filter.show_action_types(debugger_action_types.iter());
|
||||
|
||||
if supports_restart {
|
||||
filter.show_action_types(restart_action_type.iter());
|
||||
} else {
|
||||
filter.hide_action_types(&restart_action_type);
|
||||
}
|
||||
|
||||
if support_step_back {
|
||||
filter.show_action_types(step_back_action_type.iter());
|
||||
} else {
|
||||
filter.hide_action_types(&step_back_action_type);
|
||||
}
|
||||
|
||||
match status {
|
||||
Some(ThreadStatus::Running) => {
|
||||
filter.show_action_types(running_action_types.iter());
|
||||
filter.hide_action_types(&stopped_action_type);
|
||||
}
|
||||
Some(ThreadStatus::Stopped) => {
|
||||
filter.show_action_types(stopped_action_type.iter());
|
||||
filter.hide_action_types(&running_action_types);
|
||||
}
|
||||
_ => {
|
||||
filter.hide_action_types(&running_action_types);
|
||||
filter.hide_action_types(&stopped_action_type);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// show only the `debug: start`
|
||||
filter.hide_action_types(&debugger_action_types);
|
||||
filter.hide_action_types(&step_back_action_type);
|
||||
filter.hide_action_types(&restart_action_type);
|
||||
filter.hide_action_types(&running_action_types);
|
||||
filter.hide_action_types(&stopped_action_type);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
@@ -233,17 +154,6 @@ impl DebugPanel {
|
||||
)
|
||||
});
|
||||
|
||||
cx.observe_new::<DebugPanel>(|debug_panel, _, cx| {
|
||||
Self::filter_action_types(debug_panel, cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.observe(&debug_panel, |_, debug_panel, cx| {
|
||||
debug_panel.update(cx, |debug_panel, cx| {
|
||||
Self::filter_action_types(debug_panel, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
workspace.set_debugger_provider(DebuggerProvider(debug_panel.clone()));
|
||||
|
||||
debug_panel
|
||||
@@ -445,10 +355,7 @@ impl DebugPanel {
|
||||
};
|
||||
|
||||
let dap_store_handle = self.project.read(cx).dap_store().clone();
|
||||
let mut label = parent_session.read(cx).label().clone();
|
||||
if !label.ends_with("(child)") {
|
||||
label = format!("{label} (child)").into();
|
||||
}
|
||||
let label = self.label_for_child_session(&parent_session, request, cx);
|
||||
let adapter = parent_session.read(cx).adapter().clone();
|
||||
let mut binary = parent_session.read(cx).binary().clone();
|
||||
binary.request_args = request.clone();
|
||||
@@ -608,6 +515,12 @@ impl DebugPanel {
|
||||
}
|
||||
})
|
||||
};
|
||||
let documentation_button = || {
|
||||
IconButton::new("debug-open-documentation", IconName::CircleHelp)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(move |_, _, cx| cx.open_url("https://zed.dev/docs/debugger"))
|
||||
.tooltip(Tooltip::text("Open Documentation"))
|
||||
};
|
||||
|
||||
Some(
|
||||
div.border_b_1()
|
||||
@@ -629,6 +542,8 @@ impl DebugPanel {
|
||||
project::debugger::session::ThreadStatus::Exited,
|
||||
);
|
||||
let capabilities = running_state.read(cx).capabilities(cx);
|
||||
let supports_detach =
|
||||
running_state.read(cx).session().read(cx).is_attached();
|
||||
this.map(|this| {
|
||||
if thread_status == ThreadStatus::Running {
|
||||
this.child(
|
||||
@@ -817,33 +732,48 @@ impl DebugPanel {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-disconnect", IconName::DebugDetach)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(window.listener_for(
|
||||
&running_state,
|
||||
|this, _, _, cx| {
|
||||
this.detach_client(cx);
|
||||
},
|
||||
))
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Detach",
|
||||
&Detach,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
.when(
|
||||
supports_detach,
|
||||
|div| {
|
||||
div.child(
|
||||
IconButton::new(
|
||||
"debug-disconnect",
|
||||
IconName::DebugDetach,
|
||||
)
|
||||
.disabled(
|
||||
thread_status != ThreadStatus::Stopped
|
||||
&& thread_status != ThreadStatus::Running,
|
||||
)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(window.listener_for(
|
||||
&running_state,
|
||||
|this, _, _, cx| {
|
||||
this.detach_client(cx);
|
||||
},
|
||||
))
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Detach",
|
||||
&Detach,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
.justify_around()
|
||||
.when(is_side, |this| this.child(new_session_button())),
|
||||
.when(is_side, |this| {
|
||||
this.child(new_session_button())
|
||||
.child(documentation_button())
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -884,7 +814,10 @@ impl DebugPanel {
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.when(!is_side, |this| this.child(new_session_button())),
|
||||
.when(!is_side, |this| {
|
||||
this.child(new_session_button())
|
||||
.child(documentation_button())
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -1039,6 +972,25 @@ impl DebugPanel {
|
||||
cx.emit(PanelEvent::ZoomIn);
|
||||
}
|
||||
}
|
||||
|
||||
fn label_for_child_session(
|
||||
&self,
|
||||
parent_session: &Entity<Session>,
|
||||
request: &StartDebuggingRequestArguments,
|
||||
cx: &mut Context<'_, Self>,
|
||||
) -> SharedString {
|
||||
let adapter = parent_session.read(cx).adapter();
|
||||
if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) {
|
||||
if let Some(label) = adapter.label_for_child_session(request) {
|
||||
return label.into();
|
||||
}
|
||||
}
|
||||
let mut label = parent_session.read(cx).label().clone();
|
||||
if !label.ends_with("(child)") {
|
||||
label = format!("{label} (child)").into();
|
||||
}
|
||||
label
|
||||
}
|
||||
}
|
||||
|
||||
async fn register_session_inner(
|
||||
@@ -1066,6 +1018,11 @@ async fn register_session_inner(
|
||||
.ok();
|
||||
let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
|
||||
let debug_session = this.update_in(cx, |this, window, cx| {
|
||||
let parent_session = this
|
||||
.sessions
|
||||
.iter()
|
||||
.find(|p| Some(p.read(cx).session_id(cx)) == session.read(cx).parent_id(cx))
|
||||
.cloned();
|
||||
this.sessions.retain(|session| {
|
||||
!session
|
||||
.read(cx)
|
||||
@@ -1079,8 +1036,8 @@ async fn register_session_inner(
|
||||
let debug_session = DebugSession::running(
|
||||
this.project.clone(),
|
||||
this.workspace.clone(),
|
||||
parent_session.map(|p| p.read(cx).running_state().read(cx).debug_terminal.clone()),
|
||||
session,
|
||||
cx.weak_entity(),
|
||||
serialized_layout,
|
||||
this.position(window, cx).axis(),
|
||||
window,
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
use std::any::TypeId;
|
||||
|
||||
use dap::debugger_settings::DebuggerSettings;
|
||||
use debugger_panel::{DebugPanel, ToggleFocus};
|
||||
use editor::Editor;
|
||||
use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt};
|
||||
use gpui::{App, EntityInputHandler, actions};
|
||||
use gpui::{App, DispatchPhase, EntityInputHandler, actions};
|
||||
use new_process_modal::{NewProcessModal, NewProcessMode};
|
||||
use project::debugger::{self, breakpoint_store::SourceBreakpoint};
|
||||
use project::debugger::{self, breakpoint_store::SourceBreakpoint, session::ThreadStatus};
|
||||
use session::DebugSession;
|
||||
use settings::Settings;
|
||||
use stack_trace_view::StackTraceView;
|
||||
use tasks_ui::{Spawn, TaskOverrides};
|
||||
use ui::{FluentBuilder, InteractiveElement};
|
||||
use util::maybe;
|
||||
use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
|
||||
|
||||
@@ -68,148 +71,6 @@ pub fn init(cx: &mut App) {
|
||||
.register_action(|workspace, _: &ToggleFocus, window, cx| {
|
||||
workspace.toggle_panel_focus::<DebugPanel>(window, cx);
|
||||
})
|
||||
.register_action(|workspace, _: &Pause, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
if let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
{
|
||||
active_item.update(cx, |item, cx| item.pause_thread(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &Restart, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
if let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
{
|
||||
active_item.update(cx, |item, cx| item.restart_session(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &Continue, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
if let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
{
|
||||
active_item.update(cx, |item, cx| item.continue_thread(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &StepInto, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
if let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
{
|
||||
active_item.update(cx, |item, cx| item.step_in(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &StepOver, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
if let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
{
|
||||
active_item.update(cx, |item, cx| item.step_over(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &StepOut, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
|
||||
panel
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
}) {
|
||||
active_item.update(cx, |item, cx| item.step_out(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &StepBack, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
if let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
{
|
||||
active_item.update(cx, |item, cx| item.step_back(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &Stop, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
if let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
{
|
||||
cx.defer(move |cx| {
|
||||
active_item.update(cx, |item, cx| item.stop_thread(cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &ToggleIgnoreBreakpoints, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
if let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
{
|
||||
active_item.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(
|
||||
|workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |store, cx| {
|
||||
store.shutdown_sessions(cx).detach();
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
.register_action(
|
||||
|workspace: &mut Workspace, _: &ShowStackTrace, window, cx| {
|
||||
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx) {
|
||||
let is_active = workspace
|
||||
.active_item(cx)
|
||||
.is_some_and(|item| item.item_id() == existing.item_id());
|
||||
workspace.activate_item(&existing, true, !is_active, window, cx);
|
||||
} else {
|
||||
let Some(active_session) = debug_panel.read(cx).active_session() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = workspace.project();
|
||||
|
||||
let stack_trace_view = active_session.update(cx, |session, cx| {
|
||||
session.stack_trace_view(project, window, cx).clone()
|
||||
});
|
||||
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(stack_trace_view),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
|
||||
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
|
||||
})
|
||||
@@ -223,90 +84,255 @@ pub fn init(cx: &mut App) {
|
||||
debug_panel.rerun_last_session(workspace, window, cx);
|
||||
})
|
||||
},
|
||||
);
|
||||
)
|
||||
.register_action(
|
||||
|workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |store, cx| {
|
||||
store.shutdown_sessions(cx).detach();
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
.register_action_renderer(|div, workspace, _, cx| {
|
||||
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
||||
return div;
|
||||
};
|
||||
let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
else {
|
||||
return div;
|
||||
};
|
||||
let running_state = active_item.read(cx);
|
||||
if running_state.session().read(cx).is_terminated() {
|
||||
return div;
|
||||
}
|
||||
|
||||
let caps = running_state.capabilities(cx);
|
||||
let supports_restart = caps.supports_restart_request.unwrap_or_default();
|
||||
let supports_step_back = caps.supports_step_back.unwrap_or_default();
|
||||
let supports_detach = running_state.session().read(cx).is_attached();
|
||||
let status = running_state.thread_status(cx);
|
||||
|
||||
let active_item = active_item.downgrade();
|
||||
div.when(status == Some(ThreadStatus::Running), |div| {
|
||||
let active_item = active_item.clone();
|
||||
div.on_action(move |_: &Pause, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.pause_thread(cx))
|
||||
.ok();
|
||||
})
|
||||
})
|
||||
.when(status == Some(ThreadStatus::Stopped), |div| {
|
||||
div.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &StepInto, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.step_in(cx)).ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &StepOver, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.step_over(cx)).ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &StepOut, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.step_out(cx)).ok();
|
||||
}
|
||||
})
|
||||
.when(supports_step_back, |div| {
|
||||
let active_item = active_item.clone();
|
||||
div.on_action(move |_: &StepBack, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.step_back(cx)).ok();
|
||||
})
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &Continue, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.continue_thread(cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.on_action(cx.listener(
|
||||
|workspace, _: &ShowStackTrace, window, cx| {
|
||||
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx)
|
||||
{
|
||||
let is_active = workspace
|
||||
.active_item(cx)
|
||||
.is_some_and(|item| item.item_id() == existing.item_id());
|
||||
workspace
|
||||
.activate_item(&existing, true, !is_active, window, cx);
|
||||
} else {
|
||||
let Some(active_session) =
|
||||
debug_panel.read(cx).active_session()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = workspace.project();
|
||||
|
||||
let stack_trace_view =
|
||||
active_session.update(cx, |session, cx| {
|
||||
session.stack_trace_view(project, window, cx).clone()
|
||||
});
|
||||
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(stack_trace_view),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
},
|
||||
))
|
||||
})
|
||||
.when(supports_detach, |div| {
|
||||
let active_item = active_item.clone();
|
||||
div.on_action(move |_: &Detach, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.detach_client(cx))
|
||||
.ok();
|
||||
})
|
||||
})
|
||||
.when(supports_restart, |div| {
|
||||
let active_item = active_item.clone();
|
||||
div.on_action(move |_: &Restart, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.restart_session(cx))
|
||||
.ok();
|
||||
})
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &Stop, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.stop_thread(cx)).ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &ToggleIgnoreBreakpoints, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
});
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.observe_new({
|
||||
move |editor: &mut Editor, _, cx| {
|
||||
move |editor: &mut Editor, _, _| {
|
||||
editor
|
||||
.register_action(cx.listener(
|
||||
move |editor, _: &editor::actions::DebuggerRunToCursor, _, cx| {
|
||||
maybe!({
|
||||
let debug_panel =
|
||||
editor.workspace()?.read(cx).panel::<DebugPanel>(cx)?;
|
||||
let cursor_point: language::Point = editor.selections.newest(cx).head();
|
||||
let active_session = debug_panel.read(cx).active_session()?;
|
||||
.register_action_renderer(move |editor, window, cx| {
|
||||
let Some(workspace) = editor.workspace() else {
|
||||
return;
|
||||
};
|
||||
let Some(debug_panel) = workspace.read(cx).panel::<DebugPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
let Some(active_session) = debug_panel
|
||||
.clone()
|
||||
.update(cx, |panel, _| panel.active_session())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let editor = cx.entity().downgrade();
|
||||
window.on_action(TypeId::of::<editor::actions::RunToCursor>(), {
|
||||
let editor = editor.clone();
|
||||
let active_session = active_session.clone();
|
||||
move |_, phase, _, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
maybe!({
|
||||
let (buffer, position, _) = editor
|
||||
.update(cx, |editor, cx| {
|
||||
let cursor_point: language::Point =
|
||||
editor.selections.newest(cx).head();
|
||||
|
||||
let (buffer, position, _) = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.point_to_buffer_point(cursor_point, cx)?;
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.point_to_buffer_point(cursor_point, cx)
|
||||
})
|
||||
.ok()??;
|
||||
|
||||
let path =
|
||||
let path =
|
||||
debugger::breakpoint_store::BreakpointStore::abs_path_from_buffer(
|
||||
&buffer, cx,
|
||||
)?;
|
||||
|
||||
let source_breakpoint = SourceBreakpoint {
|
||||
row: position.row,
|
||||
path,
|
||||
message: None,
|
||||
condition: None,
|
||||
hit_condition: None,
|
||||
state: debugger::breakpoint_store::BreakpointState::Enabled,
|
||||
};
|
||||
let source_breakpoint = SourceBreakpoint {
|
||||
row: position.row,
|
||||
path,
|
||||
message: None,
|
||||
condition: None,
|
||||
hit_condition: None,
|
||||
state: debugger::breakpoint_store::BreakpointState::Enabled,
|
||||
};
|
||||
|
||||
active_session.update(cx, |session, cx| {
|
||||
session.running_state().update(cx, |state, cx| {
|
||||
if let Some(thread_id) = state.selected_thread_id() {
|
||||
state.session().update(cx, |session, cx| {
|
||||
session.run_to_position(
|
||||
source_breakpoint,
|
||||
thread_id,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Some(())
|
||||
});
|
||||
},
|
||||
))
|
||||
.detach();
|
||||
|
||||
editor
|
||||
.register_action(cx.listener(
|
||||
move |editor, _: &editor::actions::DebuggerEvaluateSelectedText, window, cx| {
|
||||
maybe!({
|
||||
let debug_panel =
|
||||
editor.workspace()?.read(cx).panel::<DebugPanel>(cx)?;
|
||||
let active_session = debug_panel.read(cx).active_session()?;
|
||||
|
||||
let text = editor.text_for_range(
|
||||
editor.selections.newest(cx).range(),
|
||||
&mut None,
|
||||
window,
|
||||
cx,
|
||||
)?;
|
||||
|
||||
active_session.update(cx, |session, cx| {
|
||||
session.running_state().update(cx, |state, cx| {
|
||||
let stack_id = state.selected_stack_frame_id(cx);
|
||||
|
||||
state.session().update(cx, |session, cx| {
|
||||
session.evaluate(text, None, stack_id, None, cx).detach();
|
||||
active_session.update(cx, |session, cx| {
|
||||
session.running_state().update(cx, |state, cx| {
|
||||
if let Some(thread_id) = state.selected_thread_id() {
|
||||
state.session().update(cx, |session, cx| {
|
||||
session.run_to_position(
|
||||
source_breakpoint,
|
||||
thread_id,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Some(())
|
||||
});
|
||||
},
|
||||
))
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.on_action(
|
||||
TypeId::of::<editor::actions::EvaluateSelectedText>(),
|
||||
move |_, _, window, cx| {
|
||||
maybe!({
|
||||
let text = editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.text_for_range(
|
||||
editor.selections.newest(cx).range(),
|
||||
&mut None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok()??;
|
||||
|
||||
active_session.update(cx, |session, cx| {
|
||||
session.running_state().update(cx, |state, cx| {
|
||||
let stack_id = state.selected_stack_frame_id(cx);
|
||||
|
||||
state.session().update(cx, |session, cx| {
|
||||
session
|
||||
.evaluate(text, None, stack_id, None, cx)
|
||||
.detach();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Some(())
|
||||
});
|
||||
},
|
||||
);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use collections::FxHashMap;
|
||||
use collections::{FxHashMap, HashMap};
|
||||
use language::LanguageRegistry;
|
||||
use paths::local_debug_file_relative_path;
|
||||
use std::{
|
||||
@@ -15,9 +15,9 @@ use dap::{
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle,
|
||||
InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, Subscription,
|
||||
TextStyle, UnderlineStyle, WeakEntity,
|
||||
Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
HighlightStyle, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText,
|
||||
Subscription, TextStyle, UnderlineStyle, WeakEntity,
|
||||
};
|
||||
use itertools::Itertools as _;
|
||||
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
|
||||
@@ -28,10 +28,10 @@ use theme::ThemeSettings;
|
||||
use ui::{
|
||||
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
|
||||
ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize,
|
||||
IconWithIndicator, Indicator, InteractiveElement, IntoElement, Label, LabelCommon as _,
|
||||
ListItem, ListItemSpacing, ParentElement, RenderOnce, SharedString, Styled, StyledExt,
|
||||
StyledTypography, ToggleButton, ToggleState, Toggleable, Tooltip, Window, div, h_flex, px,
|
||||
relative, rems, v_flex,
|
||||
IconWithIndicator, Indicator, InteractiveElement, IntoElement, KeyBinding, Label,
|
||||
LabelCommon as _, LabelSize, ListItem, ListItemSpacing, ParentElement, RenderOnce,
|
||||
SharedString, Styled, StyledExt, StyledTypography, ToggleButton, ToggleState, Toggleable,
|
||||
Tooltip, Window, div, h_flex, px, relative, rems, v_flex,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::{ModalView, Workspace, pane};
|
||||
@@ -50,7 +50,7 @@ pub(super) struct NewProcessModal {
|
||||
mode: NewProcessMode,
|
||||
debug_picker: Entity<Picker<DebugDelegate>>,
|
||||
attach_mode: Entity<AttachMode>,
|
||||
launch_mode: Entity<ConfigureMode>,
|
||||
configure_mode: Entity<ConfigureMode>,
|
||||
task_mode: TaskMode,
|
||||
debugger: Option<DebugAdapterName>,
|
||||
// save_scenario_state: Option<SaveScenarioState>,
|
||||
@@ -194,11 +194,12 @@ impl NewProcessModal {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let (used_tasks, current_resolved_tasks) =
|
||||
task_inventory.update(cx, |task_inventory, cx| {
|
||||
let (used_tasks, current_resolved_tasks) = task_inventory
|
||||
.update(cx, |task_inventory, cx| {
|
||||
task_inventory
|
||||
.used_and_current_resolved_tasks(&task_contexts, cx)
|
||||
})?;
|
||||
.used_and_current_resolved_tasks(task_contexts.clone(), cx)
|
||||
})?
|
||||
.await;
|
||||
|
||||
debug_picker
|
||||
.update_in(cx, |picker, window, cx| {
|
||||
@@ -253,7 +254,7 @@ impl NewProcessModal {
|
||||
Self {
|
||||
debug_picker,
|
||||
attach_mode,
|
||||
launch_mode: configure_mode,
|
||||
configure_mode,
|
||||
task_mode,
|
||||
debugger: None,
|
||||
mode,
|
||||
@@ -283,7 +284,7 @@ impl NewProcessModal {
|
||||
NewProcessMode::Attach => self.attach_mode.update(cx, |this, cx| {
|
||||
this.clone().render(window, cx).into_any_element()
|
||||
}),
|
||||
NewProcessMode::Launch => self.launch_mode.update(cx, |this, cx| {
|
||||
NewProcessMode::Launch => self.configure_mode.update(cx, |this, cx| {
|
||||
this.clone().render(dap_menu, window, cx).into_any_element()
|
||||
}),
|
||||
NewProcessMode::Debug => v_flex()
|
||||
@@ -297,7 +298,7 @@ impl NewProcessModal {
|
||||
match self.mode {
|
||||
NewProcessMode::Task => self.task_mode.task_modal.focus_handle(cx),
|
||||
NewProcessMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx),
|
||||
NewProcessMode::Launch => self.launch_mode.read(cx).program.focus_handle(cx),
|
||||
NewProcessMode::Launch => self.configure_mode.read(cx).program.focus_handle(cx),
|
||||
NewProcessMode::Debug => self.debug_picker.focus_handle(cx),
|
||||
}
|
||||
}
|
||||
@@ -305,7 +306,7 @@ impl NewProcessModal {
|
||||
fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> {
|
||||
let request = match self.mode {
|
||||
NewProcessMode::Launch => Some(DebugRequest::Launch(
|
||||
self.launch_mode.read(cx).debug_request(cx),
|
||||
self.configure_mode.read(cx).debug_request(cx),
|
||||
)),
|
||||
NewProcessMode::Attach => Some(DebugRequest::Attach(
|
||||
self.attach_mode.read(cx).debug_request(),
|
||||
@@ -315,7 +316,7 @@ impl NewProcessModal {
|
||||
let label = suggested_label(&request, debugger);
|
||||
|
||||
let stop_on_entry = if let NewProcessMode::Launch = &self.mode {
|
||||
Some(self.launch_mode.read(cx).stop_on_entry.selected())
|
||||
Some(self.configure_mode.read(cx).stop_on_entry.selected())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -831,7 +832,7 @@ impl Render for NewProcessModal {
|
||||
.disabled(
|
||||
self.debugger.is_none()
|
||||
|| self
|
||||
.launch_mode
|
||||
.configure_mode
|
||||
.read(cx)
|
||||
.program
|
||||
.read(cx)
|
||||
@@ -1202,7 +1203,7 @@ impl PickerDelegate for DebugDelegate {
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
|
||||
"".into()
|
||||
"Find a debug task, or debug a command.".into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
@@ -1265,6 +1266,96 @@ impl PickerDelegate for DebugDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm_input(
|
||||
&mut self,
|
||||
_secondary: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
let text = self.prompt.clone();
|
||||
let (task_context, worktree_id) = self
|
||||
.task_contexts
|
||||
.as_ref()
|
||||
.and_then(|task_contexts| {
|
||||
Some((
|
||||
task_contexts.active_context().cloned()?,
|
||||
task_contexts.worktree(),
|
||||
))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut args = shlex::split(&text).into_iter().flatten().peekable();
|
||||
let mut env = HashMap::default();
|
||||
while args.peek().is_some_and(|arg| arg.contains('=')) {
|
||||
let arg = args.next().unwrap();
|
||||
let (lhs, rhs) = arg.split_once('=').unwrap();
|
||||
env.insert(lhs.to_string(), rhs.to_string());
|
||||
}
|
||||
|
||||
let program = if let Some(program) = args.next() {
|
||||
program
|
||||
} else {
|
||||
env = HashMap::default();
|
||||
text
|
||||
};
|
||||
|
||||
let args = args.collect::<Vec<_>>();
|
||||
let task = task::TaskTemplate {
|
||||
label: "one-off".to_owned(),
|
||||
env,
|
||||
command: program,
|
||||
args,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let Some(location) = self
|
||||
.task_contexts
|
||||
.as_ref()
|
||||
.and_then(|cx| cx.location().cloned())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let file = location.buffer.read(cx).file();
|
||||
let language = location.buffer.read(cx).language();
|
||||
let language_name = language.as_ref().map(|l| l.name());
|
||||
let Some(adapter): Option<DebugAdapterName> =
|
||||
language::language_settings::language_settings(language_name, file, cx)
|
||||
.debuggers
|
||||
.first()
|
||||
.map(SharedString::from)
|
||||
.map(Into::into)
|
||||
.or_else(|| {
|
||||
language.and_then(|l| {
|
||||
l.config()
|
||||
.debuggers
|
||||
.first()
|
||||
.map(SharedString::from)
|
||||
.map(Into::into)
|
||||
})
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(debug_scenario) = cx
|
||||
.global::<DapRegistry>()
|
||||
.locators()
|
||||
.iter()
|
||||
.find_map(|locator| locator.1.create_scenario(&task, "one-off", adapter.clone()))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
|
||||
|
||||
self.debug_panel
|
||||
.update(cx, |panel, cx| {
|
||||
panel.start_session(debug_scenario, task_context, None, worktree_id, window, cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
|
||||
let debug_scenario = self
|
||||
.matches
|
||||
@@ -1300,6 +1391,60 @@ impl PickerDelegate for DebugDelegate {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn render_footer(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<ui::AnyElement> {
|
||||
let current_modifiers = window.modifiers();
|
||||
let footer = h_flex()
|
||||
.w_full()
|
||||
.h_8()
|
||||
.p_2()
|
||||
.justify_between()
|
||||
.rounded_b_sm()
|
||||
.bg(cx.theme().colors().ghost_element_selected)
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
// TODO: add button to open selected task in debug.json
|
||||
h_flex().into_any_element(),
|
||||
)
|
||||
.map(|this| {
|
||||
if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() {
|
||||
let action = picker::ConfirmInput {
|
||||
secondary: current_modifiers.secondary(),
|
||||
}
|
||||
.boxed_clone();
|
||||
this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| {
|
||||
Button::new("launch-custom", "Launch Custom")
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(keybind)
|
||||
.on_click(move |_, window, cx| {
|
||||
window.dispatch_action(action.boxed_clone(), cx)
|
||||
})
|
||||
}))
|
||||
} else {
|
||||
this.children(KeyBinding::for_action(&menu::Confirm, window, cx).map(
|
||||
|keybind| {
|
||||
let is_recent_selected =
|
||||
self.divider_index >= Some(self.selected_index);
|
||||
let run_entry_label =
|
||||
if is_recent_selected { "Rerun" } else { "Spawn" };
|
||||
|
||||
Button::new("spawn", run_entry_label)
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(keybind)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(menu::Confirm.boxed_clone(), cx);
|
||||
})
|
||||
},
|
||||
))
|
||||
}
|
||||
});
|
||||
Some(footer.into_any_element())
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pub mod running;
|
||||
|
||||
use crate::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout};
|
||||
use crate::{StackTraceView, persistence::SerializedLayout, session::running::DebugTerminal};
|
||||
use dap::client::SessionId;
|
||||
use gpui::{
|
||||
App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
|
||||
@@ -22,7 +22,6 @@ pub struct DebugSession {
|
||||
running_state: Entity<RunningState>,
|
||||
label: OnceLock<SharedString>,
|
||||
stack_trace_view: OnceCell<Entity<StackTraceView>>,
|
||||
_debug_panel: WeakEntity<DebugPanel>,
|
||||
_worktree_store: WeakEntity<WorktreeStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_subscriptions: [Subscription; 1],
|
||||
@@ -38,8 +37,8 @@ impl DebugSession {
|
||||
pub(crate) fn running(
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
parent_terminal: Option<Entity<DebugTerminal>>,
|
||||
session: Entity<Session>,
|
||||
_debug_panel: WeakEntity<DebugPanel>,
|
||||
serialized_layout: Option<SerializedLayout>,
|
||||
dock_axis: Axis,
|
||||
window: &mut Window,
|
||||
@@ -50,6 +49,7 @@ impl DebugSession {
|
||||
session.clone(),
|
||||
project.clone(),
|
||||
workspace.clone(),
|
||||
parent_terminal,
|
||||
serialized_layout,
|
||||
dock_axis,
|
||||
window,
|
||||
@@ -64,7 +64,6 @@ impl DebugSession {
|
||||
remote_id: None,
|
||||
running_state,
|
||||
label: OnceLock::new(),
|
||||
_debug_panel,
|
||||
stack_trace_view: OnceCell::new(),
|
||||
_worktree_store: project.read(cx).worktree_store().downgrade(),
|
||||
workspace,
|
||||
|
||||
@@ -605,6 +605,7 @@ impl RunningState {
|
||||
session: Entity<Session>,
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
parent_terminal: Option<Entity<DebugTerminal>>,
|
||||
serialized_pane_layout: Option<SerializedLayout>,
|
||||
dock_axis: Axis,
|
||||
window: &mut Window,
|
||||
@@ -617,7 +618,8 @@ impl RunningState {
|
||||
StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
|
||||
});
|
||||
|
||||
let debug_terminal = cx.new(|cx| DebugTerminal::empty(window, cx));
|
||||
let debug_terminal =
|
||||
parent_terminal.unwrap_or_else(|| cx.new(|cx| DebugTerminal::empty(window, cx)));
|
||||
|
||||
let variable_list =
|
||||
cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
|
||||
@@ -816,20 +818,20 @@ impl RunningState {
|
||||
|
||||
let request_type = dap_registry
|
||||
.adapter(&adapter)
|
||||
.ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))
|
||||
.with_context(|| format!("{}: is not a valid adapter name", &adapter))
|
||||
.and_then(|adapter| adapter.request_kind(&config));
|
||||
|
||||
let config_is_valid = request_type.is_ok();
|
||||
|
||||
let build_output = if let Some(build) = build {
|
||||
let (task, locator_name) = match build {
|
||||
let (task_template, locator_name) = match build {
|
||||
BuildTaskDefinition::Template {
|
||||
task_template,
|
||||
locator_name,
|
||||
} => (task_template, locator_name),
|
||||
BuildTaskDefinition::ByName(ref label) => {
|
||||
let Some(task) = task_store.update(cx, |this, cx| {
|
||||
this.task_inventory().and_then(|inventory| {
|
||||
let task = task_store.update(cx, |this, cx| {
|
||||
this.task_inventory().map(|inventory| {
|
||||
inventory.read(cx).task_template_by_label(
|
||||
buffer,
|
||||
worktree_id,
|
||||
@@ -837,14 +839,15 @@ impl RunningState {
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
else {
|
||||
anyhow::bail!("Couldn't find task template for {:?}", build)
|
||||
};
|
||||
})?;
|
||||
let task = match task {
|
||||
Some(task) => task.await,
|
||||
None => None,
|
||||
}.with_context(|| format!("Couldn't find task template for {build:?}"))?;
|
||||
(task, None)
|
||||
}
|
||||
};
|
||||
let Some(task) = task.resolve_task("debug-build-task", &task_context) else {
|
||||
let Some(task) = task_template.resolve_task("debug-build-task", &task_context) else {
|
||||
anyhow::bail!("Could not resolve task variables within a debug scenario");
|
||||
};
|
||||
|
||||
@@ -929,15 +932,13 @@ impl RunningState {
|
||||
};
|
||||
|
||||
if config_is_valid {
|
||||
// Ok(DebugTaskDefinition {
|
||||
// label,
|
||||
// adapter: DebugAdapterName(adapter),
|
||||
// config,
|
||||
// tcp_connection,
|
||||
// })
|
||||
} else if let Some((task, locator_name)) = build_output {
|
||||
let locator_name =
|
||||
locator_name.context("Could not find a valid locator for a build task")?;
|
||||
locator_name.with_context(|| {
|
||||
format!("Could not find a valid locator for a build task and configure is invalid with error: {}", request_type.err()
|
||||
.map(|err| err.to_string())
|
||||
.unwrap_or_default())
|
||||
})?;
|
||||
let request = dap_store
|
||||
.update(cx, |this, cx| {
|
||||
this.run_debug_locator(&locator_name, task, cx)
|
||||
@@ -953,7 +954,7 @@ impl RunningState {
|
||||
|
||||
let scenario = dap_registry
|
||||
.adapter(&adapter)
|
||||
.ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))
|
||||
.with_context(|| anyhow!("{}: is not a valid adapter name", &adapter))
|
||||
.map(|adapter| adapter.config_from_zed_format(zed_config))??;
|
||||
config = scenario.config;
|
||||
Self::substitute_variables_in_config(&mut config, &task_context);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
@@ -277,10 +278,9 @@ impl BreakpointList {
|
||||
let selected_ix = self.selected_ix;
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
uniform_list(
|
||||
cx.entity(),
|
||||
"breakpoint-list",
|
||||
self.breakpoints.len(),
|
||||
move |this, range, window, cx| {
|
||||
cx.processor(move |this, range: Range<usize>, window, cx| {
|
||||
range
|
||||
.clone()
|
||||
.zip(&mut this.breakpoints[range])
|
||||
@@ -291,7 +291,7 @@ impl BreakpointList {
|
||||
.into_any_element()
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
}),
|
||||
)
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.flex_grow()
|
||||
|
||||
@@ -8,7 +8,7 @@ use project::{
|
||||
ProjectItem as _, ProjectPath,
|
||||
debugger::session::{Session, SessionEvent},
|
||||
};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use std::{ops::Range, path::Path, sync::Arc};
|
||||
use ui::{Scrollbar, ScrollbarState, prelude::*};
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -281,10 +281,11 @@ impl ModuleList {
|
||||
|
||||
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
uniform_list(
|
||||
cx.entity(),
|
||||
"module-list",
|
||||
self.entries.len(),
|
||||
|this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(),
|
||||
cx.processor(|this, range: Range<usize>, _window, cx| {
|
||||
range.map(|ix| this.render_entry(ix, cx)).collect()
|
||||
}),
|
||||
)
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.size_full()
|
||||
|
||||
@@ -183,6 +183,7 @@ impl StackFrameList {
|
||||
let mut entries = Vec::new();
|
||||
let mut collapsed_entries = Vec::new();
|
||||
let mut first_stack_frame = None;
|
||||
let mut first_not_subtle_frame = None;
|
||||
|
||||
let stack_frames = self.stack_frames(cx);
|
||||
for stack_frame in &stack_frames {
|
||||
@@ -197,6 +198,11 @@ impl StackFrameList {
|
||||
}
|
||||
|
||||
first_stack_frame.get_or_insert(entries.len());
|
||||
if stack_frame.dap.presentation_hint
|
||||
!= Some(dap::StackFramePresentationHint::Subtle)
|
||||
{
|
||||
first_not_subtle_frame.get_or_insert(entries.len());
|
||||
}
|
||||
entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
|
||||
}
|
||||
}
|
||||
@@ -209,7 +215,10 @@ impl StackFrameList {
|
||||
|
||||
std::mem::swap(&mut self.entries, &mut entries);
|
||||
|
||||
if let Some(ix) = first_stack_frame.filter(|_| open_first_stack_frame) {
|
||||
if let Some(ix) = first_not_subtle_frame
|
||||
.or(first_stack_frame)
|
||||
.filter(|_| open_first_stack_frame)
|
||||
{
|
||||
self.select_ix(Some(ix), cx);
|
||||
self.activate_selected_entry(window, cx);
|
||||
} else if let Some(old_selected_frame_id) = old_selected_frame_id {
|
||||
|
||||
@@ -980,10 +980,11 @@ impl Render for VariableList {
|
||||
.on_action(cx.listener(Self::edit_variable))
|
||||
.child(
|
||||
uniform_list(
|
||||
cx.entity().clone(),
|
||||
"variable-list",
|
||||
self.entries.len(),
|
||||
move |this, range, window, cx| this.render_entries(range, window, cx),
|
||||
cx.processor(move |this, range: Range<usize>, window, cx| {
|
||||
this.render_entries(range, window, cx)
|
||||
}),
|
||||
)
|
||||
.track_scroll(self.list_handle.clone())
|
||||
.gap_1_5()
|
||||
|
||||
@@ -35,7 +35,6 @@ assets.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
convert_case.workspace = true
|
||||
dap.workspace = true
|
||||
db.workspace = true
|
||||
|
||||
@@ -243,6 +243,8 @@ impl_actions!(
|
||||
]
|
||||
);
|
||||
|
||||
actions!(debugger, [RunToCursor, EvaluateSelectedText]);
|
||||
|
||||
actions!(
|
||||
editor,
|
||||
[
|
||||
@@ -426,8 +428,6 @@ actions!(
|
||||
DisableBreakpoint,
|
||||
EnableBreakpoint,
|
||||
EditLogBreakpoint,
|
||||
DebuggerRunToCursor,
|
||||
DebuggerEvaluateSelectedText,
|
||||
ToggleAutoSignatureHelp,
|
||||
ToggleGitBlameInline,
|
||||
OpenGitBlameCommit,
|
||||
|
||||
@@ -722,10 +722,9 @@ impl CompletionsMenu {
|
||||
let last_rendered_range = self.last_rendered_range.clone();
|
||||
let style = style.clone();
|
||||
let list = uniform_list(
|
||||
cx.entity().clone(),
|
||||
"completions",
|
||||
self.entries.borrow().len(),
|
||||
move |_editor, range, _window, cx| {
|
||||
cx.processor(move |_editor, range: Range<usize>, _window, cx| {
|
||||
last_rendered_range.borrow_mut().replace(range.clone());
|
||||
let start_ix = range.start;
|
||||
let completions_guard = completions.borrow_mut();
|
||||
@@ -837,7 +836,7 @@ impl CompletionsMenu {
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
}),
|
||||
)
|
||||
.occlude()
|
||||
.max_h(max_height_in_lines as f32 * window.line_height())
|
||||
@@ -1452,10 +1451,9 @@ impl CodeActionsMenu {
|
||||
let actions = self.actions.clone();
|
||||
let selected_item = self.selected_item;
|
||||
let list = uniform_list(
|
||||
cx.entity().clone(),
|
||||
"code_actions_menu",
|
||||
self.actions.len(),
|
||||
move |_this, range, _, cx| {
|
||||
cx.processor(move |_this, range: Range<usize>, _, cx| {
|
||||
actions
|
||||
.iter()
|
||||
.skip(range.start)
|
||||
@@ -1518,7 +1516,7 @@ impl CodeActionsMenu {
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
}),
|
||||
)
|
||||
.occlude()
|
||||
.max_h(max_height_in_lines as f32 * window.line_height())
|
||||
|
||||
@@ -240,7 +240,6 @@ pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration:
|
||||
|
||||
pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction";
|
||||
pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict";
|
||||
pub(crate) const MIN_LINE_NUMBER_DIGITS: u32 = 4;
|
||||
pub(crate) const MINIMAP_FONT_SIZE: AbsoluteLength = AbsoluteLength::Pixels(px(2.));
|
||||
|
||||
pub type RenderDiffHunkControlsFn = Arc<
|
||||
@@ -918,6 +917,7 @@ enum SelectionDragState {
|
||||
Dragging {
|
||||
selection: Selection<Anchor>,
|
||||
drop_cursor: Selection<Anchor>,
|
||||
hide_drop_cursor: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1053,8 +1053,9 @@ pub struct Editor {
|
||||
style: Option<EditorStyle>,
|
||||
text_style_refinement: Option<TextStyleRefinement>,
|
||||
next_editor_action_id: EditorActionId,
|
||||
editor_actions:
|
||||
Rc<RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&mut Window, &mut Context<Self>)>>>>,
|
||||
editor_actions: Rc<
|
||||
RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&Editor, &mut Window, &mut Context<Self>)>>>,
|
||||
>,
|
||||
use_autoclose: bool,
|
||||
use_auto_surround: bool,
|
||||
auto_replace_emoji_shortcode: bool,
|
||||
@@ -5138,10 +5139,13 @@ impl Editor {
|
||||
.as_ref()
|
||||
.map_or(true, |provider| provider.filter_completions());
|
||||
|
||||
// When `is_incomplete` is false, can filter completions instead of re-querying when the
|
||||
// current query is a suffix of the initial query.
|
||||
if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() {
|
||||
if !menu.is_incomplete && filter_completions {
|
||||
if filter_completions {
|
||||
menu.filter(query.clone(), provider.clone(), window, cx);
|
||||
}
|
||||
// When `is_incomplete` is false, no need to re-query completions when the current query
|
||||
// is a suffix of the initial query.
|
||||
if !menu.is_incomplete {
|
||||
// If the new query is a suffix of the old query (typing more characters) and
|
||||
// the previous result was complete, the existing completions can be filtered.
|
||||
//
|
||||
@@ -5159,7 +5163,6 @@ impl Editor {
|
||||
menu.initial_position.to_offset(&snapshot) == position.to_offset(&snapshot)
|
||||
};
|
||||
if position_matches {
|
||||
menu.filter(query.clone(), provider.clone(), window, cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -7538,8 +7541,7 @@ impl Editor {
|
||||
"Set Breakpoint"
|
||||
};
|
||||
|
||||
let run_to_cursor = command_palette_hooks::CommandPaletteFilter::try_global(cx)
|
||||
.map_or(false, |filter| !filter.is_hidden(&DebuggerRunToCursor));
|
||||
let run_to_cursor = window.is_action_available(&RunToCursor, cx);
|
||||
|
||||
let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state {
|
||||
BreakpointState::Enabled => Some("Disable"),
|
||||
@@ -7563,7 +7565,7 @@ impl Editor {
|
||||
})
|
||||
.ok();
|
||||
|
||||
window.dispatch_action(Box::new(DebuggerRunToCursor), cx);
|
||||
window.dispatch_action(Box::new(RunToCursor), cx);
|
||||
})
|
||||
.separator()
|
||||
})
|
||||
@@ -14035,7 +14037,8 @@ impl Editor {
|
||||
prefer_lsp && !lsp_tasks_by_rows.is_empty(),
|
||||
new_rows,
|
||||
cx.clone(),
|
||||
);
|
||||
)
|
||||
.await;
|
||||
editor
|
||||
.update(cx, |editor, _| {
|
||||
editor.clear_tasks();
|
||||
@@ -14065,35 +14068,40 @@ impl Editor {
|
||||
snapshot: DisplaySnapshot,
|
||||
prefer_lsp: bool,
|
||||
runnable_ranges: Vec<RunnableRange>,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Vec<((BufferId, BufferRow), RunnableTasks)> {
|
||||
runnable_ranges
|
||||
.into_iter()
|
||||
.filter_map(|mut runnable| {
|
||||
let mut tasks = cx
|
||||
cx: AsyncWindowContext,
|
||||
) -> Task<Vec<((BufferId, BufferRow), RunnableTasks)>> {
|
||||
cx.spawn(async move |cx| {
|
||||
let mut runnable_rows = Vec::with_capacity(runnable_ranges.len());
|
||||
for mut runnable in runnable_ranges {
|
||||
let Some(tasks) = cx
|
||||
.update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx))
|
||||
.ok()?;
|
||||
.ok()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let mut tasks = tasks.await;
|
||||
|
||||
if prefer_lsp {
|
||||
tasks.retain(|(task_kind, _)| {
|
||||
!matches!(task_kind, TaskSourceKind::Language { .. })
|
||||
});
|
||||
}
|
||||
if tasks.is_empty() {
|
||||
return None;
|
||||
continue;
|
||||
}
|
||||
|
||||
let point = runnable.run_range.start.to_point(&snapshot.buffer_snapshot);
|
||||
|
||||
let row = snapshot
|
||||
let Some(row) = snapshot
|
||||
.buffer_snapshot
|
||||
.buffer_line_for_row(MultiBufferRow(point.row))?
|
||||
.1
|
||||
.start
|
||||
.row;
|
||||
.buffer_line_for_row(MultiBufferRow(point.row))
|
||||
.map(|(_, range)| range.start.row)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let context_range =
|
||||
BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end);
|
||||
Some((
|
||||
runnable_rows.push((
|
||||
(runnable.buffer_id, row),
|
||||
RunnableTasks {
|
||||
templates: tasks,
|
||||
@@ -14104,16 +14112,17 @@ impl Editor {
|
||||
column: point.column,
|
||||
extra_variables: runnable.extra_captures,
|
||||
},
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
));
|
||||
}
|
||||
runnable_rows
|
||||
})
|
||||
}
|
||||
|
||||
fn templates_with_tags(
|
||||
project: &Entity<Project>,
|
||||
runnable: &mut Runnable,
|
||||
cx: &mut App,
|
||||
) -> Vec<(TaskSourceKind, TaskTemplate)> {
|
||||
) -> Task<Vec<(TaskSourceKind, TaskTemplate)>> {
|
||||
let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| {
|
||||
let (worktree_id, file) = project
|
||||
.buffer_for_id(runnable.buffer, cx)
|
||||
@@ -14128,39 +14137,40 @@ impl Editor {
|
||||
)
|
||||
});
|
||||
|
||||
let mut templates_with_tags = mem::take(&mut runnable.tags)
|
||||
.into_iter()
|
||||
.flat_map(|RunnableTag(tag)| {
|
||||
inventory
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.flat_map(|inventory| {
|
||||
inventory.read(cx).list_tasks(
|
||||
file.clone(),
|
||||
Some(runnable.language.clone()),
|
||||
worktree_id,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.filter(move |(_, template)| {
|
||||
template.tags.iter().any(|source_tag| source_tag == &tag)
|
||||
})
|
||||
})
|
||||
.sorted_by_key(|(kind, _)| kind.to_owned())
|
||||
.collect::<Vec<_>>();
|
||||
if let Some((leading_tag_source, _)) = templates_with_tags.first() {
|
||||
// Strongest source wins; if we have worktree tag binding, prefer that to
|
||||
// global and language bindings;
|
||||
// if we have a global binding, prefer that to language binding.
|
||||
let first_mismatch = templates_with_tags
|
||||
.iter()
|
||||
.position(|(tag_source, _)| tag_source != leading_tag_source);
|
||||
if let Some(index) = first_mismatch {
|
||||
templates_with_tags.truncate(index);
|
||||
let tags = mem::take(&mut runnable.tags);
|
||||
let language = runnable.language.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let mut templates_with_tags = Vec::new();
|
||||
if let Some(inventory) = inventory {
|
||||
for RunnableTag(tag) in tags {
|
||||
let Ok(new_tasks) = inventory.update(cx, |inventory, cx| {
|
||||
inventory.list_tasks(file.clone(), Some(language.clone()), worktree_id, cx)
|
||||
}) else {
|
||||
return templates_with_tags;
|
||||
};
|
||||
templates_with_tags.extend(new_tasks.await.into_iter().filter(
|
||||
move |(_, template)| {
|
||||
template.tags.iter().any(|source_tag| source_tag == &tag)
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
templates_with_tags.sort_by_key(|(kind, _)| kind.to_owned());
|
||||
|
||||
templates_with_tags
|
||||
if let Some((leading_tag_source, _)) = templates_with_tags.first() {
|
||||
// Strongest source wins; if we have worktree tag binding, prefer that to
|
||||
// global and language bindings;
|
||||
// if we have a global binding, prefer that to language binding.
|
||||
let first_mismatch = templates_with_tags
|
||||
.iter()
|
||||
.position(|(tag_source, _)| tag_source != leading_tag_source);
|
||||
if let Some(index) = first_mismatch {
|
||||
templates_with_tags.truncate(index);
|
||||
}
|
||||
}
|
||||
|
||||
templates_with_tags
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_to_enclosing_bracket(
|
||||
@@ -16076,13 +16086,16 @@ impl Editor {
|
||||
window: &Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<()> {
|
||||
let project = self.project.as_ref()?.downgrade();
|
||||
if !self.mode().is_full() {
|
||||
return None;
|
||||
}
|
||||
let pull_diagnostics_settings = ProjectSettings::get_global(cx)
|
||||
.diagnostics
|
||||
.lsp_pull_diagnostics;
|
||||
if !pull_diagnostics_settings.enabled {
|
||||
return None;
|
||||
}
|
||||
let project = self.project.as_ref()?.downgrade();
|
||||
let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms);
|
||||
let mut buffers = self.buffer.read(cx).all_buffers();
|
||||
if let Some(buffer_id) = buffer_id {
|
||||
@@ -18133,16 +18146,9 @@ impl Editor {
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.iter()
|
||||
.map(|selection| {
|
||||
let range = if selection.reversed {
|
||||
selection.end.text_anchor..selection.start.text_anchor
|
||||
} else {
|
||||
selection.start.text_anchor..selection.end.text_anchor
|
||||
};
|
||||
Location {
|
||||
buffer: buffer.clone(),
|
||||
range,
|
||||
}
|
||||
.map(|range| Location {
|
||||
buffer: buffer.clone(),
|
||||
range: range.start.text_anchor..range.end.text_anchor,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -19735,7 +19741,7 @@ impl Editor {
|
||||
.flatten()
|
||||
.filter_map(|keystroke| {
|
||||
if keystroke.modifiers.is_subset_of(&Modifiers::shift()) {
|
||||
Some(keystroke.key_char.clone().unwrap_or(keystroke.key.clone()))
|
||||
keystroke.key_char.clone()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -19812,6 +19818,21 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_action_renderer(
|
||||
&mut self,
|
||||
listener: impl Fn(&Editor, &mut Window, &mut Context<Editor>) + 'static,
|
||||
) -> Subscription {
|
||||
let id = self.next_editor_action_id.post_inc();
|
||||
self.editor_actions
|
||||
.borrow_mut()
|
||||
.insert(id, Box::new(listener));
|
||||
|
||||
let editor_actions = self.editor_actions.clone();
|
||||
Subscription::new(move || {
|
||||
editor_actions.borrow_mut().remove(&id);
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register_action<A: Action>(
|
||||
&mut self,
|
||||
listener: impl Fn(&A, &mut Window, &mut App) + 'static,
|
||||
@@ -19820,7 +19841,7 @@ impl Editor {
|
||||
let listener = Arc::new(listener);
|
||||
self.editor_actions.borrow_mut().insert(
|
||||
id,
|
||||
Box::new(move |window, _| {
|
||||
Box::new(move |_, window, _| {
|
||||
let listener = listener.clone();
|
||||
window.on_action(TypeId::of::<A>(), move |action, phase, window, cx| {
|
||||
let action = action.downcast_ref().unwrap();
|
||||
@@ -20052,8 +20073,13 @@ fn process_completion_for_edit(
|
||||
let buffer = buffer.read(cx);
|
||||
let buffer_snapshot = buffer.snapshot();
|
||||
let (snippet, new_text) = if completion.is_snippet() {
|
||||
// Workaround for typescript language server issues so that methods don't expand within
|
||||
// strings and functions with type expressions. The previous point is used because the query
|
||||
// for function identifier doesn't match when the cursor is immediately after. See PR #30312
|
||||
let mut snippet_source = completion.new_text.clone();
|
||||
if let Some(scope) = buffer_snapshot.language_scope_at(cursor_position) {
|
||||
let mut previous_point = text::ToPoint::to_point(cursor_position, buffer);
|
||||
previous_point.column = previous_point.column.saturating_sub(1);
|
||||
if let Some(scope) = buffer_snapshot.language_scope_at(previous_point) {
|
||||
if scope.prefers_label_for_snippet_in_completion() {
|
||||
if let Some(label) = completion.label() {
|
||||
if matches!(
|
||||
@@ -21531,7 +21557,8 @@ impl EditorSnapshot {
|
||||
.unwrap_or(gutter_settings.line_numbers);
|
||||
let line_gutter_width = if show_line_numbers {
|
||||
// Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines.
|
||||
let min_width_for_number_on_gutter = em_advance * MIN_LINE_NUMBER_DIGITS as f32;
|
||||
let min_width_for_number_on_gutter =
|
||||
em_advance * gutter_settings.min_line_number_digits as f32;
|
||||
max_line_number_width.max(min_width_for_number_on_gutter)
|
||||
} else {
|
||||
0.0.into()
|
||||
|
||||
@@ -150,6 +150,7 @@ impl Minimap {
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct Gutter {
|
||||
pub min_line_number_digits: usize,
|
||||
pub line_numbers: bool,
|
||||
pub runnables: bool,
|
||||
pub breakpoints: bool,
|
||||
@@ -609,6 +610,10 @@ pub struct GutterContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub line_numbers: Option<bool>,
|
||||
/// Minimum number of characters to reserve space for in the gutter.
|
||||
///
|
||||
/// Default: 4
|
||||
pub min_line_number_digits: Option<usize>,
|
||||
/// Whether to show runnable buttons in the gutter.
|
||||
///
|
||||
/// Default: true
|
||||
|
||||
@@ -6,10 +6,10 @@ use crate::{
|
||||
Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT,
|
||||
FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
|
||||
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp,
|
||||
MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
||||
OpenExcerpts, PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt,
|
||||
SelectPhase, SelectedTextHighlight, Selection, SelectionDragState, SoftWrap,
|
||||
StickyHeaderExcerpt, ToPoint, ToggleFold,
|
||||
MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown,
|
||||
PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase,
|
||||
SelectedTextHighlight, Selection, SelectionDragState, SoftWrap, StickyHeaderExcerpt, ToPoint,
|
||||
ToggleFold,
|
||||
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
|
||||
display_map::{
|
||||
Block, BlockContext, BlockStyle, DisplaySnapshot, EditorMargins, FoldId, HighlightedChunk,
|
||||
@@ -187,7 +187,7 @@ impl EditorElement {
|
||||
let editor = &self.editor;
|
||||
editor.update(cx, |editor, cx| {
|
||||
for action in editor.editor_actions.borrow().values() {
|
||||
(action)(window, cx)
|
||||
(action)(editor, window, cx)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -856,8 +856,11 @@ impl EditorElement {
|
||||
SelectionDragState::Dragging { ref selection, .. } => {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let selection_display = selection.map(|anchor| anchor.to_display_point(&snapshot));
|
||||
if !point_for_position.intersects_selection(&selection_display) {
|
||||
let is_cut = !event.modifiers.control;
|
||||
if !point_for_position.intersects_selection(&selection_display)
|
||||
&& text_hitbox.is_hovered(window)
|
||||
{
|
||||
let is_cut = !(cfg!(target_os = "macos") && event.modifiers.alt
|
||||
|| cfg!(not(target_os = "macos")) && event.modifiers.control);
|
||||
editor.move_selection_on_drop(
|
||||
&selection.clone(),
|
||||
point_for_position.previous_valid,
|
||||
@@ -865,10 +868,11 @@ impl EditorElement {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.selection_drag_state = SelectionDragState::None;
|
||||
cx.stop_propagation();
|
||||
return;
|
||||
}
|
||||
editor.selection_drag_state = SelectionDragState::None;
|
||||
cx.stop_propagation();
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -941,7 +945,8 @@ impl EditorElement {
|
||||
return;
|
||||
}
|
||||
|
||||
let text_bounds = position_map.text_hitbox.bounds;
|
||||
let text_hitbox = &position_map.text_hitbox;
|
||||
let text_bounds = text_hitbox.bounds;
|
||||
let point_for_position = position_map.point_for_position(event.position);
|
||||
|
||||
let mut scroll_delta = gpui::Point::<f32>::default();
|
||||
@@ -982,10 +987,12 @@ impl EditorElement {
|
||||
match editor.selection_drag_state {
|
||||
SelectionDragState::Dragging {
|
||||
ref mut drop_cursor,
|
||||
ref mut hide_drop_cursor,
|
||||
..
|
||||
} => {
|
||||
drop_cursor.start = drop_anchor;
|
||||
drop_cursor.end = drop_anchor;
|
||||
*hide_drop_cursor = !text_hitbox.is_hovered(window);
|
||||
}
|
||||
SelectionDragState::ReadyToDrag { ref selection, .. } => {
|
||||
let drop_cursor = Selection {
|
||||
@@ -998,6 +1005,7 @@ impl EditorElement {
|
||||
editor.selection_drag_state = SelectionDragState::Dragging {
|
||||
selection: selection.clone(),
|
||||
drop_cursor,
|
||||
hide_drop_cursor: false,
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
@@ -1251,16 +1259,18 @@ impl EditorElement {
|
||||
if let SelectionDragState::Dragging {
|
||||
ref selection,
|
||||
ref drop_cursor,
|
||||
ref hide_drop_cursor,
|
||||
} = editor.selection_drag_state
|
||||
{
|
||||
if drop_cursor
|
||||
.start
|
||||
.cmp(&selection.start, &snapshot.buffer_snapshot)
|
||||
.eq(&Ordering::Less)
|
||||
|| drop_cursor
|
||||
.end
|
||||
.cmp(&selection.end, &snapshot.buffer_snapshot)
|
||||
.eq(&Ordering::Greater)
|
||||
if !hide_drop_cursor
|
||||
&& (drop_cursor
|
||||
.start
|
||||
.cmp(&selection.start, &snapshot.buffer_snapshot)
|
||||
.eq(&Ordering::Less)
|
||||
|| drop_cursor
|
||||
.end
|
||||
.cmp(&selection.end, &snapshot.buffer_snapshot)
|
||||
.eq(&Ordering::Greater))
|
||||
{
|
||||
let drag_cursor_layout = SelectionLayout::new(
|
||||
drop_cursor.clone(),
|
||||
@@ -2816,7 +2826,8 @@ impl EditorElement {
|
||||
let available_width = gutter_dimensions.left_padding - git_gutter_width;
|
||||
|
||||
let editor = self.editor.clone();
|
||||
let is_wide = max_line_number_length >= MIN_LINE_NUMBER_DIGITS
|
||||
let is_wide = max_line_number_length
|
||||
>= EditorSettings::get_global(cx).gutter.min_line_number_digits as u32
|
||||
&& row_info
|
||||
.buffer_row
|
||||
.is_some_and(|row| (row + 1).ilog10() + 1 == max_line_number_length)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DebuggerEvaluateSelectedText, DisplayPoint,
|
||||
DisplaySnapshot, Editor, FindAllReferences, GoToDeclaration, GoToDefinition,
|
||||
GoToImplementation, GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode,
|
||||
SelectionExt, ToDisplayPoint, ToggleCodeActions,
|
||||
Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DisplayPoint, DisplaySnapshot, Editor,
|
||||
EvaluateSelectedText, FindAllReferences, GoToDeclaration, GoToDefinition, GoToImplementation,
|
||||
GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, SelectionExt,
|
||||
ToDisplayPoint, ToggleCodeActions,
|
||||
actions::{Format, FormatSelections},
|
||||
selections_collection::SelectionsCollection,
|
||||
};
|
||||
@@ -199,17 +199,14 @@ pub fn deploy_context_menu(
|
||||
.is_some()
|
||||
});
|
||||
|
||||
let evaluate_selection = command_palette_hooks::CommandPaletteFilter::try_global(cx)
|
||||
.map_or(false, |filter| {
|
||||
!filter.is_hidden(&DebuggerEvaluateSelectedText)
|
||||
});
|
||||
let evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx);
|
||||
|
||||
ui::ContextMenu::build(window, cx, |menu, _window, _cx| {
|
||||
let builder = menu
|
||||
.on_blur_subscription(Subscription::new(|| {}))
|
||||
.when(evaluate_selection && has_selections, |builder| {
|
||||
builder
|
||||
.action("Evaluate Selection", Box::new(DebuggerEvaluateSelectedText))
|
||||
.action("Evaluate Selection", Box::new(EvaluateSelectedText))
|
||||
.separator()
|
||||
})
|
||||
.action("Go to Definition", Box::new(GoToDefinition))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ interface dap {
|
||||
}
|
||||
|
||||
record debug-adapter-binary {
|
||||
command: string,
|
||||
command: option<string>,
|
||||
arguments: list<string>,
|
||||
envs: env-vars,
|
||||
cwd: option<string>,
|
||||
|
||||
@@ -1682,12 +1682,15 @@ impl ExtensionStore {
|
||||
|
||||
pub fn register_ssh_client(&mut self, client: Entity<SshRemoteClient>, cx: &mut Context<Self>) {
|
||||
let connection_options = client.read(cx).connection_options();
|
||||
if self.ssh_clients.contains_key(&connection_options.ssh_url()) {
|
||||
return;
|
||||
let ssh_url = connection_options.ssh_url();
|
||||
|
||||
if let Some(existing_client) = self.ssh_clients.get(&ssh_url) {
|
||||
if existing_client.upgrade().is_some() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.ssh_clients
|
||||
.insert(connection_options.ssh_url(), client.downgrade());
|
||||
self.ssh_clients.insert(ssh_url, client.downgrade());
|
||||
self.ssh_registered_tx.unbounded_send(()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,13 +554,15 @@ impl ExtensionsPage {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.child(
|
||||
Button::new(
|
||||
SharedString::from(format!("rebuild-{}", extension.id)),
|
||||
"Rebuild",
|
||||
)
|
||||
.color(Color::Accent)
|
||||
.disabled(matches!(status, ExtensionStatus::Upgrading))
|
||||
.on_click({
|
||||
let extension_id = extension.id.clone();
|
||||
move |_, _, cx| {
|
||||
@@ -568,12 +570,12 @@ impl ExtensionsPage {
|
||||
store.rebuild_dev_extension(extension_id.clone(), cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
.color(Color::Accent)
|
||||
.disabled(matches!(status, ExtensionStatus::Upgrading)),
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new(SharedString::from(extension.id.clone()), "Uninstall")
|
||||
.color(Color::Accent)
|
||||
.disabled(matches!(status, ExtensionStatus::Removing))
|
||||
.on_click({
|
||||
let extension_id = extension.id.clone();
|
||||
move |_, _, cx| {
|
||||
@@ -581,9 +583,7 @@ impl ExtensionsPage {
|
||||
store.uninstall_extension(extension_id.clone(), cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
.color(Color::Accent)
|
||||
.disabled(matches!(status, ExtensionStatus::Removing)),
|
||||
}),
|
||||
)
|
||||
.when(can_configure, |this| {
|
||||
this.child(
|
||||
@@ -591,8 +591,8 @@ impl ExtensionsPage {
|
||||
SharedString::from(format!("configure-{}", extension.id)),
|
||||
"Configure",
|
||||
)
|
||||
|
||||
|
||||
.color(Color::Accent)
|
||||
.disabled(matches!(status, ExtensionStatus::Installing))
|
||||
.on_click({
|
||||
let manifest = Arc::new(extension.clone());
|
||||
move |_, _, cx| {
|
||||
@@ -609,9 +609,7 @@ impl ExtensionsPage {
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.color(Color::Accent)
|
||||
.disabled(matches!(status, ExtensionStatus::Installing)),
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
@@ -1479,18 +1477,12 @@ impl Render for ExtensionsPage {
|
||||
return this.py_4().child(self.render_empty_state(cx));
|
||||
}
|
||||
|
||||
let extensions_page = cx.entity().clone();
|
||||
let scroll_handle = self.list.clone();
|
||||
this.child(
|
||||
uniform_list(
|
||||
extensions_page,
|
||||
"entries",
|
||||
count,
|
||||
Self::render_extensions,
|
||||
)
|
||||
.flex_grow()
|
||||
.pb_4()
|
||||
.track_scroll(scroll_handle),
|
||||
uniform_list("entries", count, cx.processor(Self::render_extensions))
|
||||
.flex_grow()
|
||||
.pb_4()
|
||||
.track_scroll(scroll_handle),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
|
||||
@@ -55,6 +55,7 @@ use project::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use std::future::Future;
|
||||
use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{collections::HashSet, sync::Arc, time::Duration, usize};
|
||||
use strum::{IntoEnumIterator, VariantNames};
|
||||
@@ -3710,8 +3711,10 @@ impl GitPanel {
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
uniform_list(cx.entity().clone(), "entries", entry_count, {
|
||||
move |this, range, window, cx| {
|
||||
uniform_list(
|
||||
"entries",
|
||||
entry_count,
|
||||
cx.processor(move |this, range: Range<usize>, window, cx| {
|
||||
let mut items = Vec::with_capacity(range.end - range.start);
|
||||
|
||||
for ix in range {
|
||||
@@ -3739,8 +3742,8 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
items
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
.when(
|
||||
!self.horizontal_scrollbar.show_track
|
||||
&& self.horizontal_scrollbar.show_scrollbar,
|
||||
|
||||
@@ -378,8 +378,6 @@ impl DataTable {
|
||||
|
||||
impl Render for DataTable {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let entity = cx.entity();
|
||||
|
||||
div()
|
||||
.font_family(".SystemUIFont")
|
||||
.bg(gpui::white())
|
||||
@@ -431,8 +429,10 @@ impl Render for DataTable {
|
||||
.relative()
|
||||
.size_full()
|
||||
.child(
|
||||
uniform_list(entity, "items", self.quotes.len(), {
|
||||
move |this, range, _, _| {
|
||||
uniform_list(
|
||||
"items",
|
||||
self.quotes.len(),
|
||||
cx.processor(move |this, range: Range<usize>, _, _| {
|
||||
this.visible_range = range.clone();
|
||||
let mut items = Vec::with_capacity(range.end - range.start);
|
||||
for i in range {
|
||||
@@ -441,8 +441,8 @@ impl Render for DataTable {
|
||||
}
|
||||
}
|
||||
items
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
.size_full()
|
||||
.track_scroll(self.scroll_handle.clone()),
|
||||
)
|
||||
|
||||
@@ -9,10 +9,9 @@ impl Render for UniformListExample {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().size_full().bg(rgb(0xffffff)).child(
|
||||
uniform_list(
|
||||
cx.entity().clone(),
|
||||
"entries",
|
||||
50,
|
||||
|_this, range, _window, _cx| {
|
||||
cx.processor(|_this, range, _window, _cx| {
|
||||
let mut items = Vec::new();
|
||||
for ix in range {
|
||||
let item = ix + 1;
|
||||
@@ -29,7 +28,7 @@ impl Render for UniformListExample {
|
||||
);
|
||||
}
|
||||
items
|
||||
},
|
||||
}),
|
||||
)
|
||||
.h_full(),
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -225,6 +225,18 @@ impl<'a, T: 'static> Context<'a, T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience method for producing view state in a closure.
|
||||
/// See `listener` for more details.
|
||||
pub fn processor<E, R>(
|
||||
&self,
|
||||
f: impl Fn(&mut T, E, &mut Window, &mut Context<T>) -> R + 'static,
|
||||
) -> impl Fn(E, &mut Window, &mut App) -> R + 'static {
|
||||
let view = self.entity();
|
||||
move |e: E, window: &mut Window, cx: &mut App| {
|
||||
view.update(cx, |view, cx| f(view, e, window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
/// Run something using this entity and cx, when the returned struct is dropped
|
||||
pub fn on_drop(
|
||||
&self,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
//! elements with uniform height.
|
||||
|
||||
use crate::{
|
||||
AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, Element, ElementId, Entity,
|
||||
GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement,
|
||||
IsZero, LayoutId, ListSizingBehavior, Overflow, Pixels, Render, ScrollHandle, Size,
|
||||
StyleRefinement, Styled, Window, point, size,
|
||||
AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, GlobalElementId,
|
||||
Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId,
|
||||
ListSizingBehavior, Overflow, Pixels, ScrollHandle, Size, StyleRefinement, Styled, Window,
|
||||
point, size,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||
@@ -19,28 +19,23 @@ use super::ListHorizontalSizingBehavior;
|
||||
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
|
||||
/// uniform_list will only render the visible subset of items.
|
||||
#[track_caller]
|
||||
pub fn uniform_list<I, R, V>(
|
||||
view: Entity<V>,
|
||||
id: I,
|
||||
pub fn uniform_list<R>(
|
||||
id: impl Into<ElementId>,
|
||||
item_count: usize,
|
||||
f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>,
|
||||
f: impl 'static + Fn(Range<usize>, &mut Window, &mut App) -> Vec<R>,
|
||||
) -> UniformList
|
||||
where
|
||||
I: Into<ElementId>,
|
||||
R: IntoElement,
|
||||
V: Render,
|
||||
{
|
||||
let id = id.into();
|
||||
let mut base_style = StyleRefinement::default();
|
||||
base_style.overflow.y = Some(Overflow::Scroll);
|
||||
|
||||
let render_range = move |range, window: &mut Window, cx: &mut App| {
|
||||
view.update(cx, |this, cx| {
|
||||
f(this, range, window, cx)
|
||||
.into_iter()
|
||||
.map(|component| component.into_any_element())
|
||||
.collect()
|
||||
})
|
||||
let render_range = move |range: Range<usize>, window: &mut Window, cx: &mut App| {
|
||||
f(range, window, cx)
|
||||
.into_iter()
|
||||
.map(|component| component.into_any_element())
|
||||
.collect()
|
||||
};
|
||||
|
||||
UniformList {
|
||||
|
||||
@@ -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"))]
|
||||
@@ -144,7 +142,11 @@ pub fn guess_compositor() -> &'static str {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn current_platform(_headless: bool) -> Rc<dyn Platform> {
|
||||
Rc::new(WindowsPlatform::new())
|
||||
Rc::new(
|
||||
WindowsPlatform::new()
|
||||
.inspect_err(|err| show_error("Error: Zed failed to launch", err.to_string()))
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) trait Platform: 'static {
|
||||
@@ -196,6 +198,7 @@ pub(crate) trait Platform: 'static {
|
||||
|
||||
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
|
||||
|
||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
||||
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||
@@ -215,6 +218,7 @@ pub(crate) trait Platform: 'static {
|
||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
|
||||
|
||||
fn compositor_name(&self) -> &'static str {
|
||||
""
|
||||
@@ -235,10 +239,6 @@ pub(crate) trait Platform: 'static {
|
||||
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
|
||||
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
|
||||
fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
|
||||
|
||||
fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper>;
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
|
||||
}
|
||||
|
||||
/// A handle to a platform's display, e.g. a monitor or laptop screen.
|
||||
|
||||
@@ -342,7 +342,7 @@ impl BladeRenderer {
|
||||
let surface = context
|
||||
.gpu
|
||||
.create_surface_configured(window, surface_config)
|
||||
.unwrap();
|
||||
.map_err(|err| anyhow::anyhow!("Failed to create surface: {err:?}"))?;
|
||||
|
||||
let command_encoder = context.gpu.create_command_encoder(gpu::CommandEncoderDesc {
|
||||
name: "main",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -888,6 +888,32 @@ fn handle_hit_test_msg(
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
if let Some(mut callback) = lock.callbacks.hit_test_window_control.take() {
|
||||
drop(lock);
|
||||
let area = callback();
|
||||
state_ptr
|
||||
.state
|
||||
.borrow_mut()
|
||||
.callbacks
|
||||
.hit_test_window_control = Some(callback);
|
||||
if let Some(area) = area {
|
||||
return match area {
|
||||
WindowControlArea::Drag => Some(HTCAPTION as _),
|
||||
WindowControlArea::Close => Some(HTCLOSE as _),
|
||||
WindowControlArea::Max => Some(HTMAXBUTTON as _),
|
||||
WindowControlArea::Min => Some(HTMINBUTTON as _),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
drop(lock);
|
||||
}
|
||||
|
||||
if !state_ptr.hide_title_bar {
|
||||
// If the OS draws the title bar, we don't need to handle hit test messages.
|
||||
return None;
|
||||
}
|
||||
|
||||
// default handler for resize areas
|
||||
let hit = unsafe { DefWindowProcW(handle, msg, wparam, lparam) };
|
||||
if matches!(
|
||||
@@ -922,25 +948,6 @@ fn handle_hit_test_msg(
|
||||
return Some(HTTOP as _);
|
||||
}
|
||||
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
if let Some(mut callback) = lock.callbacks.hit_test_window_control.take() {
|
||||
drop(lock);
|
||||
let area = callback();
|
||||
state_ptr
|
||||
.state
|
||||
.borrow_mut()
|
||||
.callbacks
|
||||
.hit_test_window_control = Some(callback);
|
||||
if let Some(area) = area {
|
||||
return match area {
|
||||
WindowControlArea::Drag => Some(HTCAPTION as _),
|
||||
WindowControlArea::Close => Some(HTCLOSE as _),
|
||||
WindowControlArea::Max => Some(HTMAXBUTTON as _),
|
||||
WindowControlArea::Min => Some(HTMINBUTTON as _),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Some(HTCLIENT as _)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
@@ -81,9 +81,9 @@ impl WindowsPlatformState {
|
||||
}
|
||||
|
||||
impl WindowsPlatform {
|
||||
pub(crate) fn new() -> Self {
|
||||
pub(crate) fn new() -> Result<Self> {
|
||||
unsafe {
|
||||
OleInitialize(None).expect("unable to initialize Windows OLE");
|
||||
OleInitialize(None).context("unable to initialize Windows OLE")?;
|
||||
}
|
||||
let (main_sender, main_receiver) = flume::unbounded::<Runnable>();
|
||||
let main_thread_id_win32 = unsafe { GetCurrentThreadId() };
|
||||
@@ -97,19 +97,19 @@ impl WindowsPlatform {
|
||||
let foreground_executor = ForegroundExecutor::new(dispatcher);
|
||||
let bitmap_factory = ManuallyDrop::new(unsafe {
|
||||
CoCreateInstance(&CLSID_WICImagingFactory, None, CLSCTX_INPROC_SERVER)
|
||||
.expect("Error creating bitmap factory.")
|
||||
.context("Error creating bitmap factory.")?
|
||||
});
|
||||
let text_system = Arc::new(
|
||||
DirectWriteTextSystem::new(&bitmap_factory)
|
||||
.expect("Error creating DirectWriteTextSystem"),
|
||||
.context("Error creating DirectWriteTextSystem")?,
|
||||
);
|
||||
let icon = load_icon().unwrap_or_default();
|
||||
let state = RefCell::new(WindowsPlatformState::new());
|
||||
let raw_window_handles = RwLock::new(SmallVec::new());
|
||||
let gpu_context = BladeContext::new().expect("Unable to init GPU context");
|
||||
let windows_version = WindowsVersion::new().expect("Error retrieve windows version");
|
||||
let gpu_context = BladeContext::new().context("Unable to init GPU context")?;
|
||||
let windows_version = WindowsVersion::new().context("Error retrieve windows version")?;
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
state,
|
||||
raw_window_handles,
|
||||
gpu_context,
|
||||
@@ -122,7 +122,7 @@ impl WindowsPlatform {
|
||||
bitmap_factory,
|
||||
validation_number,
|
||||
main_thread_id_win32,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn redraw_all(&self) {
|
||||
@@ -310,10 +310,6 @@ impl Platform for WindowsPlatform {
|
||||
self.text_system.clone()
|
||||
}
|
||||
|
||||
fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper> {
|
||||
Box::new(WindowsKeyboardMapper::new())
|
||||
}
|
||||
|
||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
|
||||
Box::new(
|
||||
WindowsKeyboardLayout::new()
|
||||
|
||||
@@ -8,7 +8,7 @@ use windows::{
|
||||
},
|
||||
Wdk::System::SystemServices::RtlGetVersion,
|
||||
Win32::{Foundation::*, Graphics::Dwm::*, UI::WindowsAndMessaging::*},
|
||||
core::BOOL,
|
||||
core::{BOOL, HSTRING},
|
||||
};
|
||||
|
||||
use crate::*;
|
||||
@@ -186,3 +186,14 @@ pub(crate) fn system_appearance() -> Result<WindowAppearance> {
|
||||
fn is_color_light(color: &Color) -> bool {
|
||||
((5 * color.G as u32) + (2 * color.R as u32) + color.B as u32) > (8 * 128)
|
||||
}
|
||||
|
||||
pub(crate) fn show_error(title: &str, content: String) {
|
||||
let _ = unsafe {
|
||||
MessageBoxW(
|
||||
None,
|
||||
&HSTRING::from(content),
|
||||
&HSTRING::from(title),
|
||||
MB_ICONERROR | MB_SYSTEMMODAL,
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1258,7 +1258,7 @@ mod windows_renderer {
|
||||
use std::num::NonZeroIsize;
|
||||
use windows::Win32::{Foundation::HWND, UI::WindowsAndMessaging::GWLP_HINSTANCE};
|
||||
|
||||
use crate::get_window_long;
|
||||
use crate::{get_window_long, show_error};
|
||||
|
||||
pub(super) fn init(
|
||||
context: &BladeContext,
|
||||
@@ -1270,7 +1270,12 @@ mod windows_renderer {
|
||||
size: Default::default(),
|
||||
transparent,
|
||||
};
|
||||
BladeRenderer::new(context, &raw, config)
|
||||
BladeRenderer::new(context, &raw, config).inspect_err(|err| {
|
||||
show_error(
|
||||
"Error: Zed failed to initialize BladeRenderer",
|
||||
err.to_string(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
struct RawWindow {
|
||||
|
||||
@@ -60,6 +60,7 @@ pub enum IconName {
|
||||
ChevronUpDown,
|
||||
Circle,
|
||||
CircleOff,
|
||||
CircleHelp,
|
||||
Clipboard,
|
||||
Close,
|
||||
Cloud,
|
||||
|
||||
@@ -39,7 +39,7 @@ use util::{ResultExt, maybe, post_inc};
|
||||
#[derive(
|
||||
Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
|
||||
)]
|
||||
pub struct LanguageName(SharedString);
|
||||
pub struct LanguageName(pub SharedString);
|
||||
|
||||
impl LanguageName {
|
||||
pub fn new(s: &str) -> Self {
|
||||
@@ -1000,6 +1000,7 @@ impl LanguageRegistry {
|
||||
txs.push(tx);
|
||||
}
|
||||
AvailableGrammar::Unloaded(wasm_path) => {
|
||||
log::trace!("start loading grammar {name:?}");
|
||||
let this = self.clone();
|
||||
let wasm_path = wasm_path.clone();
|
||||
*grammar = AvailableGrammar::Loading(wasm_path.clone(), vec![tx]);
|
||||
@@ -1025,6 +1026,7 @@ impl LanguageRegistry {
|
||||
Err(error) => AvailableGrammar::LoadFailed(error.clone()),
|
||||
};
|
||||
|
||||
log::trace!("finish loading grammar {name:?}");
|
||||
let old_value = this.state.write().grammars.insert(name, value);
|
||||
if let Some(AvailableGrammar::Loading(_, txs)) = old_value {
|
||||
for tx in txs {
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::{
|
||||
use anyhow::Context as _;
|
||||
use collections::HashMap;
|
||||
use futures::FutureExt;
|
||||
use gpui::SharedString;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::{self, Ordering, Reverse},
|
||||
@@ -183,6 +184,13 @@ enum ParseStepLanguage {
|
||||
}
|
||||
|
||||
impl ParseStepLanguage {
|
||||
fn name(&self) -> SharedString {
|
||||
match self {
|
||||
ParseStepLanguage::Loaded { language } => language.name().0,
|
||||
ParseStepLanguage::Pending { name } => name.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<LanguageId> {
|
||||
match self {
|
||||
ParseStepLanguage::Loaded { language } => Some(language.id),
|
||||
@@ -415,7 +423,9 @@ impl SyntaxSnapshot {
|
||||
.and_then(|language| language.ok())
|
||||
.is_some()
|
||||
{
|
||||
resolved_injection_ranges.push(layer.range.to_offset(text));
|
||||
let range = layer.range.to_offset(text);
|
||||
log::trace!("reparse range {range:?} for language {language_name:?}");
|
||||
resolved_injection_ranges.push(range);
|
||||
}
|
||||
|
||||
cursor.next(text);
|
||||
@@ -442,7 +452,10 @@ impl SyntaxSnapshot {
|
||||
invalidated_ranges: Vec<Range<usize>>,
|
||||
registry: Option<&Arc<LanguageRegistry>>,
|
||||
) {
|
||||
log::trace!("reparse. invalidated ranges:{:?}", invalidated_ranges);
|
||||
log::trace!(
|
||||
"reparse. invalidated ranges:{:?}",
|
||||
LogOffsetRanges(&invalidated_ranges, text),
|
||||
);
|
||||
|
||||
let max_depth = self.layers.summary().max_depth;
|
||||
let mut cursor = self.layers.cursor::<SyntaxLayerSummary>(text);
|
||||
@@ -470,6 +483,13 @@ impl SyntaxSnapshot {
|
||||
loop {
|
||||
let step = queue.pop();
|
||||
let position = if let Some(step) = &step {
|
||||
log::trace!(
|
||||
"parse step depth:{}, range:{:?}, language:{} ({:?})",
|
||||
step.depth,
|
||||
LogAnchorRange(&step.range, text),
|
||||
step.language.name(),
|
||||
step.language.id(),
|
||||
);
|
||||
SyntaxLayerPosition {
|
||||
depth: step.depth,
|
||||
range: step.range.clone(),
|
||||
@@ -568,13 +588,13 @@ impl SyntaxSnapshot {
|
||||
.to_ts_point();
|
||||
}
|
||||
|
||||
if let Some((SyntaxLayerContent::Parsed { tree: old_tree, .. }, layer_start)) =
|
||||
old_layer.map(|layer| (&layer.content, layer.range.start))
|
||||
if let Some((SyntaxLayerContent::Parsed { tree: old_tree, .. }, layer_range)) =
|
||||
old_layer.map(|layer| (&layer.content, layer.range.clone()))
|
||||
{
|
||||
log::trace!(
|
||||
"existing layer. language:{}, start:{:?}, ranges:{:?}",
|
||||
"existing layer. language:{}, range:{:?}, included_ranges:{:?}",
|
||||
language.name(),
|
||||
LogPoint(layer_start.to_point(text)),
|
||||
LogAnchorRange(&layer_range, text),
|
||||
LogIncludedRanges(&old_tree.included_ranges())
|
||||
);
|
||||
|
||||
@@ -613,7 +633,7 @@ impl SyntaxSnapshot {
|
||||
}
|
||||
|
||||
log::trace!(
|
||||
"update layer. language:{}, start:{:?}, included_ranges:{:?}",
|
||||
"update layer. language:{}, range:{:?}, included_ranges:{:?}",
|
||||
language.name(),
|
||||
LogAnchorRange(&step.range, text),
|
||||
LogIncludedRanges(&included_ranges),
|
||||
@@ -761,28 +781,36 @@ impl SyntaxSnapshot {
|
||||
#[cfg(debug_assertions)]
|
||||
fn check_invariants(&self, text: &BufferSnapshot) {
|
||||
let mut max_depth = 0;
|
||||
let mut prev_range: Option<Range<Anchor>> = None;
|
||||
let mut prev_layer: Option<(Range<Anchor>, Option<LanguageId>)> = None;
|
||||
for layer in self.layers.iter() {
|
||||
match Ord::cmp(&layer.depth, &max_depth) {
|
||||
Ordering::Less => {
|
||||
panic!("layers out of order")
|
||||
}
|
||||
Ordering::Equal => {
|
||||
if let Some(prev_range) = prev_range {
|
||||
if let Some((prev_range, prev_language_id)) = prev_layer {
|
||||
match layer.range.start.cmp(&prev_range.start, text) {
|
||||
Ordering::Less => panic!("layers out of order"),
|
||||
Ordering::Equal => {
|
||||
assert!(layer.range.end.cmp(&prev_range.end, text).is_ge())
|
||||
}
|
||||
Ordering::Equal => match layer.range.end.cmp(&prev_range.end, text) {
|
||||
Ordering::Less => panic!("layers out of order"),
|
||||
Ordering::Equal => {
|
||||
if layer.content.language_id() < prev_language_id {
|
||||
panic!("layers out of order")
|
||||
}
|
||||
}
|
||||
Ordering::Greater => {}
|
||||
},
|
||||
Ordering::Greater => {}
|
||||
}
|
||||
}
|
||||
prev_layer = Some((layer.range.clone(), layer.content.language_id()));
|
||||
}
|
||||
Ordering::Greater => {
|
||||
prev_layer = None;
|
||||
}
|
||||
Ordering::Greater => {}
|
||||
}
|
||||
|
||||
max_depth = layer.depth;
|
||||
prev_range = Some(layer.range.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1642,7 +1670,7 @@ impl Ord for ParseStep {
|
||||
Ord::cmp(&other.depth, &self.depth)
|
||||
.then_with(|| Ord::cmp(&range_b.start, &range_a.start))
|
||||
.then_with(|| Ord::cmp(&range_a.end, &range_b.end))
|
||||
.then_with(|| self.language.id().cmp(&other.language.id()))
|
||||
.then_with(|| other.language.id().cmp(&self.language.id()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1888,6 +1916,7 @@ impl ToTreeSitterPoint for Point {
|
||||
struct LogIncludedRanges<'a>(&'a [tree_sitter::Range]);
|
||||
struct LogPoint(Point);
|
||||
struct LogAnchorRange<'a>(&'a Range<Anchor>, &'a text::BufferSnapshot);
|
||||
struct LogOffsetRanges<'a>(&'a [Range<usize>], &'a text::BufferSnapshot);
|
||||
struct LogChangedRegions<'a>(&'a ChangeRegionSet, &'a text::BufferSnapshot);
|
||||
|
||||
impl fmt::Debug for LogIncludedRanges<'_> {
|
||||
@@ -1909,6 +1938,16 @@ impl fmt::Debug for LogAnchorRange<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for LogOffsetRanges<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_list()
|
||||
.entries(self.0.iter().map(|range| {
|
||||
LogPoint(range.start.to_point(self.1))..LogPoint(range.end.to_point(self.1))
|
||||
}))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for LogChangedRegions<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_list()
|
||||
|
||||
@@ -788,15 +788,99 @@ fn test_empty_combined_injections_inside_injections(cx: &mut App) {
|
||||
"(template...",
|
||||
// Markdown inline content
|
||||
"(inline)",
|
||||
// HTML within the ERB
|
||||
"(document (text))",
|
||||
// The ruby syntax tree should be empty, since there are
|
||||
// no interpolations in the ERB template.
|
||||
"(program)",
|
||||
// HTML within the ERB
|
||||
"(document (text))",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_syntax_map_languages_loading_with_erb(cx: &mut App) {
|
||||
let text = r#"
|
||||
<body>
|
||||
<% if @one %>
|
||||
<div class=one>
|
||||
<% else %>
|
||||
<div class=two>
|
||||
<% end %>
|
||||
</div>
|
||||
</body>
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), text);
|
||||
|
||||
let mut syntax_map = SyntaxMap::new(&buffer);
|
||||
syntax_map.set_language_registry(registry.clone());
|
||||
|
||||
let language = Arc::new(erb_lang());
|
||||
|
||||
log::info!("parsing");
|
||||
registry.add(language.clone());
|
||||
syntax_map.reparse(language.clone(), &buffer);
|
||||
|
||||
log::info!("loading html");
|
||||
registry.add(Arc::new(html_lang()));
|
||||
syntax_map.reparse(language.clone(), &buffer);
|
||||
|
||||
log::info!("loading ruby");
|
||||
registry.add(Arc::new(ruby_lang()));
|
||||
syntax_map.reparse(language.clone(), &buffer);
|
||||
|
||||
assert_capture_ranges(
|
||||
&syntax_map,
|
||||
&buffer,
|
||||
&["tag", "ivar"],
|
||||
"
|
||||
<«body»>
|
||||
<% if «@one» %>
|
||||
<«div» class=one>
|
||||
<% else %>
|
||||
<«div» class=two>
|
||||
<% end %>
|
||||
</«div»>
|
||||
</«body»>
|
||||
",
|
||||
);
|
||||
|
||||
let text = r#"
|
||||
<body>
|
||||
<% if @one«_hundred» %>
|
||||
<div class=one>
|
||||
<% else %>
|
||||
<div class=two>
|
||||
<% end %>
|
||||
</div>
|
||||
</body>
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
log::info!("editing");
|
||||
buffer.edit_via_marked_text(&text);
|
||||
syntax_map.interpolate(&buffer);
|
||||
syntax_map.reparse(language.clone(), &buffer);
|
||||
|
||||
assert_capture_ranges(
|
||||
&syntax_map,
|
||||
&buffer,
|
||||
&["tag", "ivar"],
|
||||
"
|
||||
<«body»>
|
||||
<% if «@one_hundred» %>
|
||||
<«div» class=one>
|
||||
<% else %>
|
||||
<«div» class=two>
|
||||
<% end %>
|
||||
</«div»>
|
||||
</«body»>
|
||||
",
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 50)]
|
||||
fn test_random_syntax_map_edits_rust_macros(rng: StdRng, cx: &mut App) {
|
||||
let text = r#"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{ops::Range, path::PathBuf, sync::Arc};
|
||||
|
||||
use crate::{LanguageToolchainStore, Location, Runnable};
|
||||
use crate::{File, LanguageToolchainStore, Location, Runnable};
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
@@ -39,10 +39,11 @@ pub trait ContextProvider: Send + Sync {
|
||||
/// Provides all tasks, associated with the current language.
|
||||
fn associated_tasks(
|
||||
&self,
|
||||
_: Option<Arc<dyn crate::File>>,
|
||||
_cx: &App,
|
||||
) -> Option<TaskTemplates> {
|
||||
None
|
||||
_: Arc<dyn Fs>,
|
||||
_: Option<Arc<dyn File>>,
|
||||
_: &App,
|
||||
) -> Task<Option<TaskTemplates>> {
|
||||
Task::ready(None)
|
||||
}
|
||||
|
||||
/// A language server name, that can return tasks using LSP (ext) for this language.
|
||||
|
||||
@@ -43,6 +43,8 @@ pub struct AvailableModel {
|
||||
pub max_tokens: usize,
|
||||
pub max_output_tokens: Option<u32>,
|
||||
pub max_completion_tokens: Option<u32>,
|
||||
pub supports_tools: Option<bool>,
|
||||
pub supports_images: Option<bool>,
|
||||
}
|
||||
|
||||
pub struct OpenRouterLanguageModelProvider {
|
||||
@@ -227,7 +229,8 @@ impl LanguageModelProvider for OpenRouterLanguageModelProvider {
|
||||
name: model.name.clone(),
|
||||
display_name: model.display_name.clone(),
|
||||
max_tokens: model.max_tokens,
|
||||
supports_tools: Some(false),
|
||||
supports_tools: model.supports_tools,
|
||||
supports_images: model.supports_images,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -345,7 +348,7 @@ impl LanguageModel for OpenRouterLanguageModel {
|
||||
}
|
||||
|
||||
fn supports_images(&self) -> bool {
|
||||
false
|
||||
self.model.supports_images.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn count_tokens(
|
||||
@@ -386,20 +389,26 @@ pub fn into_open_router(
|
||||
max_output_tokens: Option<u32>,
|
||||
) -> open_router::Request {
|
||||
let mut messages = Vec::new();
|
||||
for req_message in request.messages {
|
||||
for content in req_message.content {
|
||||
for message in request.messages {
|
||||
for content in message.content {
|
||||
match content {
|
||||
MessageContent::Text(text) | MessageContent::Thinking { text, .. } => messages
|
||||
.push(match req_message.role {
|
||||
Role::User => open_router::RequestMessage::User { content: text },
|
||||
Role::Assistant => open_router::RequestMessage::Assistant {
|
||||
content: Some(text),
|
||||
tool_calls: Vec::new(),
|
||||
},
|
||||
Role::System => open_router::RequestMessage::System { content: text },
|
||||
}),
|
||||
MessageContent::Text(text) | MessageContent::Thinking { text, .. } => {
|
||||
add_message_content_part(
|
||||
open_router::MessagePart::Text { text: text },
|
||||
message.role,
|
||||
&mut messages,
|
||||
)
|
||||
}
|
||||
MessageContent::RedactedThinking(_) => {}
|
||||
MessageContent::Image(_) => {}
|
||||
MessageContent::Image(image) => {
|
||||
add_message_content_part(
|
||||
open_router::MessagePart::Image {
|
||||
image_url: image.to_base64_url(),
|
||||
},
|
||||
message.role,
|
||||
&mut messages,
|
||||
);
|
||||
}
|
||||
MessageContent::ToolUse(tool_use) => {
|
||||
let tool_call = open_router::ToolCall {
|
||||
id: tool_use.id.to_string(),
|
||||
@@ -425,16 +434,20 @@ pub fn into_open_router(
|
||||
}
|
||||
MessageContent::ToolResult(tool_result) => {
|
||||
let content = match &tool_result.content {
|
||||
LanguageModelToolResultContent::Text(text) => {
|
||||
text.to_string()
|
||||
LanguageModelToolResultContent::Text(text) => {
|
||||
vec![open_router::MessagePart::Text {
|
||||
text: text.to_string(),
|
||||
}]
|
||||
}
|
||||
LanguageModelToolResultContent::Image(_) => {
|
||||
"[Tool responded with an image, but Zed doesn't support these in Open AI models yet]".to_string()
|
||||
LanguageModelToolResultContent::Image(image) => {
|
||||
vec![open_router::MessagePart::Image {
|
||||
image_url: image.to_base64_url(),
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
messages.push(open_router::RequestMessage::Tool {
|
||||
content: content,
|
||||
content: content.into(),
|
||||
tool_call_id: tool_result.tool_use_id.to_string(),
|
||||
});
|
||||
}
|
||||
@@ -473,6 +486,42 @@ pub fn into_open_router(
|
||||
}
|
||||
}
|
||||
|
||||
fn add_message_content_part(
|
||||
new_part: open_router::MessagePart,
|
||||
role: Role,
|
||||
messages: &mut Vec<open_router::RequestMessage>,
|
||||
) {
|
||||
match (role, messages.last_mut()) {
|
||||
(Role::User, Some(open_router::RequestMessage::User { content }))
|
||||
| (Role::System, Some(open_router::RequestMessage::System { content })) => {
|
||||
content.push_part(new_part);
|
||||
}
|
||||
(
|
||||
Role::Assistant,
|
||||
Some(open_router::RequestMessage::Assistant {
|
||||
content: Some(content),
|
||||
..
|
||||
}),
|
||||
) => {
|
||||
content.push_part(new_part);
|
||||
}
|
||||
_ => {
|
||||
messages.push(match role {
|
||||
Role::User => open_router::RequestMessage::User {
|
||||
content: open_router::MessageContent::from(vec![new_part]),
|
||||
},
|
||||
Role::Assistant => open_router::RequestMessage::Assistant {
|
||||
content: Some(open_router::MessageContent::from(vec![new_part])),
|
||||
tool_calls: Vec::new(),
|
||||
},
|
||||
Role::System => open_router::RequestMessage::System {
|
||||
content: open_router::MessageContent::from(vec![new_part]),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OpenRouterEventMapper {
|
||||
tool_calls_by_index: HashMap<usize, RawToolCall>,
|
||||
}
|
||||
|
||||
@@ -303,10 +303,9 @@ impl Render for SyntaxTreeView {
|
||||
{
|
||||
let layer = layer.clone();
|
||||
rendered = rendered.child(uniform_list(
|
||||
cx.entity().clone(),
|
||||
"SyntaxTreeView",
|
||||
layer.node().descendant_count(),
|
||||
move |this, range, _, cx| {
|
||||
cx.processor(move |this, range: Range<usize>, _, cx| {
|
||||
let mut items = Vec::new();
|
||||
let mut cursor = layer.node().walk();
|
||||
let mut descendant_ix = range.start;
|
||||
@@ -377,7 +376,7 @@ impl Render for SyntaxTreeView {
|
||||
}
|
||||
}
|
||||
items
|
||||
},
|
||||
}),
|
||||
)
|
||||
.size_full()
|
||||
.track_scroll(self.list_scroll_handle.clone())
|
||||
|
||||
@@ -88,7 +88,6 @@ tree-sitter-rust = { workspace = true, optional = true }
|
||||
tree-sitter-typescript = { workspace = true, optional = true }
|
||||
tree-sitter-yaml = { workspace = true, optional = true }
|
||||
util.workspace = true
|
||||
which.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -510,9 +510,10 @@ impl ContextProvider for GoContextProvider {
|
||||
|
||||
fn associated_tasks(
|
||||
&self,
|
||||
_: Option<Arc<dyn language::File>>,
|
||||
_: Arc<dyn Fs>,
|
||||
_: Option<Arc<dyn File>>,
|
||||
_: &App,
|
||||
) -> Option<TaskTemplates> {
|
||||
) -> Task<Option<TaskTemplates>> {
|
||||
let package_cwd = if GO_PACKAGE_TASK_VARIABLE.template_value() == "." {
|
||||
None
|
||||
} else {
|
||||
@@ -520,7 +521,7 @@ impl ContextProvider for GoContextProvider {
|
||||
};
|
||||
let module_cwd = Some(GO_MODULE_ROOT_TASK_VARIABLE.template_value());
|
||||
|
||||
Some(TaskTemplates(vec![
|
||||
Task::ready(Some(TaskTemplates(vec![
|
||||
TaskTemplate {
|
||||
label: format!(
|
||||
"go test {} -run {}",
|
||||
@@ -631,7 +632,7 @@ impl ContextProvider for GoContextProvider {
|
||||
cwd: module_cwd.clone(),
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
]))
|
||||
])))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,10 @@
|
||||
] @context
|
||||
(#any-of? @_name "it" "test" "describe" "context" "suite")
|
||||
arguments: (
|
||||
arguments . (string (string_fragment) @name)
|
||||
arguments . [
|
||||
(string (string_fragment) @name)
|
||||
(identifier) @name
|
||||
]
|
||||
)
|
||||
)
|
||||
) @item
|
||||
@@ -92,7 +95,10 @@
|
||||
(#eq? @_property "each")
|
||||
)
|
||||
arguments: (
|
||||
arguments . (string (string_fragment) @name)
|
||||
arguments . [
|
||||
(string (string_fragment) @name)
|
||||
(identifier) @name
|
||||
]
|
||||
)
|
||||
)
|
||||
) @item
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
]
|
||||
(#any-of? @_name "it" "test" "describe" "context" "suite")
|
||||
arguments: (
|
||||
arguments . (string (string_fragment) @run)
|
||||
arguments . [
|
||||
(string (string_fragment) @run)
|
||||
(identifier) @run
|
||||
]
|
||||
)
|
||||
) @_js-test
|
||||
|
||||
@@ -32,7 +35,10 @@
|
||||
(#eq? @_property "each")
|
||||
)
|
||||
arguments: (
|
||||
arguments . (string (string_fragment) @run)
|
||||
arguments . [
|
||||
(string (string_fragment) @run)
|
||||
(identifier) @run
|
||||
]
|
||||
)
|
||||
) @_js-test
|
||||
|
||||
|
||||
@@ -481,9 +481,10 @@ impl ContextProvider for PythonContextProvider {
|
||||
|
||||
fn associated_tasks(
|
||||
&self,
|
||||
_: Arc<dyn Fs>,
|
||||
file: Option<Arc<dyn language::File>>,
|
||||
cx: &App,
|
||||
) -> Option<TaskTemplates> {
|
||||
) -> Task<Option<TaskTemplates>> {
|
||||
let test_runner = selected_test_runner(file.as_ref(), cx);
|
||||
|
||||
let mut tasks = vec![
|
||||
@@ -587,7 +588,7 @@ impl ContextProvider for PythonContextProvider {
|
||||
}
|
||||
});
|
||||
|
||||
Some(TaskTemplates(tasks))
|
||||
Task::ready(Some(TaskTemplates(tasks)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use http_client::github::AssetKind;
|
||||
use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
|
||||
pub use language::*;
|
||||
use lsp::{InitializeParams, LanguageServerBinary};
|
||||
use project::Fs;
|
||||
use project::lsp_store::rust_analyzer_ext::CARGO_DIAGNOSTICS_SOURCE_NAME;
|
||||
use project::project_settings::ProjectSettings;
|
||||
use regex::Regex;
|
||||
@@ -628,9 +629,10 @@ impl ContextProvider for RustContextProvider {
|
||||
|
||||
fn associated_tasks(
|
||||
&self,
|
||||
_: Arc<dyn Fs>,
|
||||
file: Option<Arc<dyn language::File>>,
|
||||
cx: &App,
|
||||
) -> Option<TaskTemplates> {
|
||||
) -> Task<Option<TaskTemplates>> {
|
||||
const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
|
||||
const CUSTOM_TARGET_DIR: &str = "RUST_TARGET_DIR";
|
||||
|
||||
@@ -798,7 +800,7 @@ impl ContextProvider for RustContextProvider {
|
||||
.collect();
|
||||
}
|
||||
|
||||
Some(TaskTemplates(task_templates))
|
||||
Task::ready(Some(TaskTemplates(task_templates)))
|
||||
}
|
||||
|
||||
fn lsp_task_source(&self) -> Option<LanguageServerName> {
|
||||
|
||||
@@ -38,5 +38,5 @@ completion_query_characters = ["-", "."]
|
||||
opt_into_language_servers = ["tailwindcss-language-server"]
|
||||
prefer_label_for_snippet = true
|
||||
|
||||
[overrides.call_expression]
|
||||
[overrides.function_name_before_type_arguments]
|
||||
prefer_label_for_snippet = true
|
||||
|
||||
@@ -14,4 +14,6 @@
|
||||
(jsx_expression)
|
||||
] @default
|
||||
|
||||
(_ value: (call_expression) @call_expression)
|
||||
(_ value: (call_expression
|
||||
function: (identifier) @function_name_before_type_arguments
|
||||
type_arguments: (type_arguments)))
|
||||
|
||||
@@ -4,10 +4,12 @@ use async_tar::Archive;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Local};
|
||||
use collections::HashMap;
|
||||
use futures::future::join_all;
|
||||
use gpui::{App, AppContext, AsyncApp, Task};
|
||||
use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
|
||||
use language::{
|
||||
ContextLocation, ContextProvider, File, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
|
||||
ContextLocation, ContextProvider, File, LanguageToolchainStore, LocalFile, LspAdapter,
|
||||
LspAdapterDelegate,
|
||||
};
|
||||
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
|
||||
use node_runtime::NodeRuntime;
|
||||
@@ -17,11 +19,12 @@ use smol::{fs, io::BufReader, lock::RwLock, stream::StreamExt};
|
||||
use std::{
|
||||
any::Any,
|
||||
borrow::Cow,
|
||||
collections::BTreeSet,
|
||||
ffi::OsString,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
|
||||
use task::{TaskTemplate, TaskTemplates, VariableName};
|
||||
use util::archive::extract_zip;
|
||||
use util::merge_json_value_into;
|
||||
use util::{ResultExt, fs::remove_matching, maybe};
|
||||
@@ -32,23 +35,12 @@ pub(crate) struct TypeScriptContextProvider {
|
||||
|
||||
const TYPESCRIPT_RUNNER_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_RUNNER"));
|
||||
const TYPESCRIPT_JEST_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST"));
|
||||
|
||||
const TYPESCRIPT_JEST_TEST_NAME_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST_TEST_NAME"));
|
||||
const TYPESCRIPT_MOCHA_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_MOCHA"));
|
||||
|
||||
const TYPESCRIPT_VITEST_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST"));
|
||||
const TYPESCRIPT_VITEST_TEST_NAME_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST_TEST_NAME"));
|
||||
const TYPESCRIPT_JASMINE_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JASMINE"));
|
||||
const TYPESCRIPT_BUILD_SCRIPT_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_BUILD_SCRIPT"));
|
||||
const TYPESCRIPT_TEST_SCRIPT_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_TEST_SCRIPT"));
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct PackageJsonContents(Arc<RwLock<HashMap<PathBuf, PackageJson>>>);
|
||||
@@ -58,32 +50,21 @@ struct PackageJson {
|
||||
data: PackageJsonData,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
#[derive(Clone, Default)]
|
||||
struct PackageJsonData {
|
||||
jest: bool,
|
||||
mocha: bool,
|
||||
vitest: bool,
|
||||
jasmine: bool,
|
||||
build_script: bool,
|
||||
test_script: bool,
|
||||
runner: Runner,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
enum Runner {
|
||||
#[default]
|
||||
Npm,
|
||||
Npx,
|
||||
Pnpm,
|
||||
scripts: BTreeSet<String>,
|
||||
package_manager: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl PackageJsonData {
|
||||
fn new(package_json: HashMap<String, Value>) -> Self {
|
||||
let mut build_script = false;
|
||||
let mut test_script = false;
|
||||
if let Some(serde_json::Value::Object(scripts)) = package_json.get("scripts") {
|
||||
build_script |= scripts.contains_key("build");
|
||||
test_script |= scripts.contains_key("test");
|
||||
let mut scripts = BTreeSet::new();
|
||||
if let Some(serde_json::Value::Object(package_json_scripts)) = package_json.get("scripts") {
|
||||
scripts.extend(package_json_scripts.keys().cloned());
|
||||
}
|
||||
|
||||
let mut jest = false;
|
||||
@@ -104,249 +85,351 @@ impl PackageJsonData {
|
||||
jasmine |= dev_dependencies.contains_key("jasmine");
|
||||
}
|
||||
|
||||
let mut runner = Runner::Npm;
|
||||
if which::which("pnpm").is_ok() {
|
||||
runner = Runner::Pnpm;
|
||||
} else if which::which("npx").is_ok() {
|
||||
runner = Runner::Npx;
|
||||
}
|
||||
let package_manager = package_json
|
||||
.get("packageManager")
|
||||
.and_then(|value| value.as_str())
|
||||
.and_then(|value| {
|
||||
if value.starts_with("pnpm") {
|
||||
Some("pnpm")
|
||||
} else if value.starts_with("yarn") {
|
||||
Some("yarn")
|
||||
} else if value.starts_with("npm") {
|
||||
Some("npm")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
jest,
|
||||
mocha,
|
||||
vitest,
|
||||
jasmine,
|
||||
build_script,
|
||||
test_script,
|
||||
runner,
|
||||
scripts,
|
||||
package_manager,
|
||||
}
|
||||
}
|
||||
|
||||
fn fill_variables(&self, variables: &mut TaskVariables) {
|
||||
let runner = match self.runner {
|
||||
Runner::Npm => "npm",
|
||||
Runner::Npx => "npx",
|
||||
Runner::Pnpm => "pnpm",
|
||||
};
|
||||
variables.insert(TYPESCRIPT_RUNNER_VARIABLE, runner.to_owned());
|
||||
fn merge(&mut self, other: Self) {
|
||||
self.jest |= other.jest;
|
||||
self.mocha |= other.mocha;
|
||||
self.vitest |= other.vitest;
|
||||
self.jasmine |= other.jasmine;
|
||||
self.scripts.extend(other.scripts);
|
||||
}
|
||||
|
||||
fn fill_task_templates(&self, task_templates: &mut TaskTemplates) {
|
||||
if self.jest {
|
||||
variables.insert(TYPESCRIPT_JEST_TASK_VARIABLE, "jest".to_owned());
|
||||
}
|
||||
if self.mocha {
|
||||
variables.insert(TYPESCRIPT_MOCHA_TASK_VARIABLE, "mocha".to_owned());
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: "jest file test".to_owned(),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
"jest".to_owned(),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!("jest test {}", VariableName::Symbol.template_value()),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
"jest".to_owned(),
|
||||
"--testNamePattern".to_owned(),
|
||||
format!(
|
||||
"\"{}\"",
|
||||
TYPESCRIPT_JEST_TEST_NAME_VARIABLE.template_value()
|
||||
),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
tags: vec![
|
||||
"ts-test".to_owned(),
|
||||
"js-test".to_owned(),
|
||||
"tsx-test".to_owned(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
}
|
||||
|
||||
if self.vitest {
|
||||
variables.insert(TYPESCRIPT_VITEST_TASK_VARIABLE, "vitest".to_owned());
|
||||
}
|
||||
if self.jasmine {
|
||||
variables.insert(TYPESCRIPT_JASMINE_TASK_VARIABLE, "jasmine".to_owned());
|
||||
}
|
||||
if self.build_script {
|
||||
variables.insert(TYPESCRIPT_BUILD_SCRIPT_TASK_VARIABLE, "build".to_owned());
|
||||
}
|
||||
if self.test_script {
|
||||
variables.insert(TYPESCRIPT_TEST_SCRIPT_TASK_VARIABLE, "test".to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeScriptContextProvider {
|
||||
pub fn new() -> Self {
|
||||
TypeScriptContextProvider {
|
||||
last_package_json: PackageJsonContents::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextProvider for TypeScriptContextProvider {
|
||||
fn associated_tasks(&self, _: Option<Arc<dyn File>>, _: &App) -> Option<TaskTemplates> {
|
||||
let mut task_templates = TaskTemplates(Vec::new());
|
||||
|
||||
// Jest tasks
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"{} file test",
|
||||
TYPESCRIPT_JEST_TASK_VARIABLE.template_value()
|
||||
),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
TYPESCRIPT_JEST_TASK_VARIABLE.template_value(),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"{} test {}",
|
||||
TYPESCRIPT_JEST_TASK_VARIABLE.template_value(),
|
||||
VariableName::Symbol.template_value(),
|
||||
),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
TYPESCRIPT_JEST_TASK_VARIABLE.template_value(),
|
||||
"--testNamePattern".to_owned(),
|
||||
format!(
|
||||
"\"{}\"",
|
||||
TYPESCRIPT_JEST_TEST_NAME_VARIABLE.template_value()
|
||||
),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
tags: vec![
|
||||
"ts-test".to_owned(),
|
||||
"js-test".to_owned(),
|
||||
"tsx-test".to_owned(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
|
||||
// Vitest tasks
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"{} file test",
|
||||
TYPESCRIPT_VITEST_TASK_VARIABLE.template_value()
|
||||
),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
TYPESCRIPT_VITEST_TASK_VARIABLE.template_value(),
|
||||
"run".to_owned(),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"{} test {}",
|
||||
TYPESCRIPT_VITEST_TASK_VARIABLE.template_value(),
|
||||
VariableName::Symbol.template_value(),
|
||||
),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
TYPESCRIPT_VITEST_TASK_VARIABLE.template_value(),
|
||||
"run".to_owned(),
|
||||
"--testNamePattern".to_owned(),
|
||||
format!("\"{}\"", TYPESCRIPT_VITEST_TASK_VARIABLE.template_value()),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
tags: vec![
|
||||
"ts-test".to_owned(),
|
||||
"js-test".to_owned(),
|
||||
"tsx-test".to_owned(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
|
||||
// Mocha tasks
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"{} file test",
|
||||
TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value()
|
||||
),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value(),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"{} test {}",
|
||||
TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value(),
|
||||
VariableName::Symbol.template_value(),
|
||||
),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
TYPESCRIPT_MOCHA_TASK_VARIABLE.template_value(),
|
||||
"--grep".to_owned(),
|
||||
format!("\"{}\"", VariableName::Symbol.template_value()),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
tags: vec![
|
||||
"ts-test".to_owned(),
|
||||
"js-test".to_owned(),
|
||||
"tsx-test".to_owned(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
|
||||
// Jasmine tasks
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"{} file test",
|
||||
TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value()
|
||||
),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value(),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"{} test {}",
|
||||
TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value(),
|
||||
VariableName::Symbol.template_value(),
|
||||
),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
TYPESCRIPT_JASMINE_TASK_VARIABLE.template_value(),
|
||||
format!("--filter={}", VariableName::Symbol.template_value()),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
tags: vec![
|
||||
"ts-test".to_owned(),
|
||||
"js-test".to_owned(),
|
||||
"tsx-test".to_owned(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
|
||||
for package_json_script in [
|
||||
TYPESCRIPT_TEST_SCRIPT_TASK_VARIABLE,
|
||||
TYPESCRIPT_BUILD_SCRIPT_TASK_VARIABLE,
|
||||
] {
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!("{} file test", "vitest".to_owned()),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
"vitest".to_owned(),
|
||||
"run".to_owned(),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"package.json script {}",
|
||||
package_json_script.template_value()
|
||||
"{} test {}",
|
||||
"vitest".to_owned(),
|
||||
VariableName::Symbol.template_value(),
|
||||
),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
"vitest".to_owned(),
|
||||
"run".to_owned(),
|
||||
"--testNamePattern".to_owned(),
|
||||
format!("\"{}\"", "vitest".to_owned()),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
tags: vec![
|
||||
"ts-test".to_owned(),
|
||||
"js-test".to_owned(),
|
||||
"tsx-test".to_owned(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
}
|
||||
|
||||
if self.mocha {
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!("{} file test", "mocha".to_owned()),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
"mocha".to_owned(),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"{} test {}",
|
||||
"mocha".to_owned(),
|
||||
VariableName::Symbol.template_value(),
|
||||
),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
"mocha".to_owned(),
|
||||
"--grep".to_owned(),
|
||||
format!("\"{}\"", VariableName::Symbol.template_value()),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
tags: vec![
|
||||
"ts-test".to_owned(),
|
||||
"js-test".to_owned(),
|
||||
"tsx-test".to_owned(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
}
|
||||
|
||||
if self.jasmine {
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!("{} file test", "jasmine".to_owned()),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
"jasmine".to_owned(),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"{} test {}",
|
||||
"jasmine".to_owned(),
|
||||
VariableName::Symbol.template_value(),
|
||||
),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
"jasmine".to_owned(),
|
||||
format!("--filter={}", VariableName::Symbol.template_value()),
|
||||
VariableName::RelativeFile.template_value(),
|
||||
],
|
||||
tags: vec![
|
||||
"ts-test".to_owned(),
|
||||
"js-test".to_owned(),
|
||||
"tsx-test".to_owned(),
|
||||
],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
}
|
||||
|
||||
for script in &self.scripts {
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!("package.json > {script}",),
|
||||
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
|
||||
args: vec![
|
||||
"--prefix".to_owned(),
|
||||
VariableName::WorktreeRoot.template_value(),
|
||||
"run".to_owned(),
|
||||
package_json_script.template_value(),
|
||||
script.to_owned(),
|
||||
],
|
||||
tags: vec!["package-script".into()],
|
||||
cwd: Some(VariableName::WorktreeRoot.template_value()),
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"execute selection {}",
|
||||
VariableName::SelectedText.template_value()
|
||||
),
|
||||
command: "node".to_owned(),
|
||||
args: vec![
|
||||
"-e".to_owned(),
|
||||
format!("\"{}\"", VariableName::SelectedText.template_value()),
|
||||
],
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
impl TypeScriptContextProvider {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
last_package_json: PackageJsonContents::default(),
|
||||
}
|
||||
}
|
||||
|
||||
Some(task_templates)
|
||||
fn combined_package_json_data(
|
||||
&self,
|
||||
fs: Arc<dyn Fs>,
|
||||
worktree_root: &Path,
|
||||
file_abs_path: &Path,
|
||||
cx: &App,
|
||||
) -> Task<anyhow::Result<PackageJsonData>> {
|
||||
let Some(file_relative_path) = file_abs_path.strip_prefix(&worktree_root).ok() else {
|
||||
log::debug!("No package json data for off-worktree files");
|
||||
return Task::ready(Ok(PackageJsonData::default()));
|
||||
};
|
||||
let new_json_data = file_relative_path
|
||||
.ancestors()
|
||||
.map(|path| worktree_root.join(path))
|
||||
.map(|parent_path| {
|
||||
self.package_json_data(&parent_path, self.last_package_json.clone(), fs.clone(), cx)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let mut package_json_data = PackageJsonData::default();
|
||||
for new_data in join_all(new_json_data).await.into_iter().flatten() {
|
||||
package_json_data.merge(new_data);
|
||||
}
|
||||
Ok(package_json_data)
|
||||
})
|
||||
}
|
||||
|
||||
fn package_json_data(
|
||||
&self,
|
||||
directory_path: &Path,
|
||||
existing_package_json: PackageJsonContents,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) -> Task<anyhow::Result<PackageJsonData>> {
|
||||
let package_json_path = directory_path.join("package.json");
|
||||
let metadata_check_fs = fs.clone();
|
||||
cx.background_spawn(async move {
|
||||
let metadata = metadata_check_fs
|
||||
.metadata(&package_json_path)
|
||||
.await
|
||||
.with_context(|| format!("getting metadata for {package_json_path:?}"))?
|
||||
.with_context(|| format!("missing FS metadata for {package_json_path:?}"))?;
|
||||
let mtime = DateTime::<Local>::from(metadata.mtime.timestamp_for_user());
|
||||
let existing_data = {
|
||||
let contents = existing_package_json.0.read().await;
|
||||
contents
|
||||
.get(&package_json_path)
|
||||
.filter(|package_json| package_json.mtime == mtime)
|
||||
.map(|package_json| package_json.data.clone())
|
||||
};
|
||||
match existing_data {
|
||||
Some(existing_data) => Ok(existing_data),
|
||||
None => {
|
||||
let package_json_string =
|
||||
fs.load(&package_json_path).await.with_context(|| {
|
||||
format!("loading package.json from {package_json_path:?}")
|
||||
})?;
|
||||
let package_json: HashMap<String, serde_json::Value> =
|
||||
serde_json::from_str(&package_json_string).with_context(|| {
|
||||
format!("parsing package.json from {package_json_path:?}")
|
||||
})?;
|
||||
let new_data = PackageJsonData::new(package_json);
|
||||
{
|
||||
let mut contents = existing_package_json.0.write().await;
|
||||
contents.insert(
|
||||
package_json_path,
|
||||
PackageJson {
|
||||
mtime,
|
||||
data: new_data.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(new_data)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn detect_package_manager(
|
||||
&self,
|
||||
worktree_root: PathBuf,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) -> Task<&'static str> {
|
||||
let last_package_json = self.last_package_json.clone();
|
||||
let package_json_data =
|
||||
self.package_json_data(&worktree_root, last_package_json, fs.clone(), cx);
|
||||
cx.background_spawn(async move {
|
||||
if let Ok(package_json_data) = package_json_data.await {
|
||||
if let Some(package_manager) = package_json_data.package_manager {
|
||||
return package_manager;
|
||||
}
|
||||
}
|
||||
if fs.is_file(&worktree_root.join("pnpm-lock.yaml")).await {
|
||||
return "pnpm";
|
||||
}
|
||||
if fs.is_file(&worktree_root.join("yarn.lock")).await {
|
||||
return "yarn";
|
||||
}
|
||||
"npm"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextProvider for TypeScriptContextProvider {
|
||||
fn associated_tasks(
|
||||
&self,
|
||||
fs: Arc<dyn Fs>,
|
||||
file: Option<Arc<dyn File>>,
|
||||
cx: &App,
|
||||
) -> Task<Option<TaskTemplates>> {
|
||||
let Some(file) = project::File::from_dyn(file.as_ref()).cloned() else {
|
||||
return Task::ready(None);
|
||||
};
|
||||
let Some(worktree_root) = file.worktree.read(cx).root_dir() else {
|
||||
return Task::ready(None);
|
||||
};
|
||||
let file_abs_path = file.abs_path(cx);
|
||||
let package_json_data =
|
||||
self.combined_package_json_data(fs.clone(), &worktree_root, &file_abs_path, cx);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let mut task_templates = TaskTemplates(Vec::new());
|
||||
task_templates.0.push(TaskTemplate {
|
||||
label: format!(
|
||||
"execute selection {}",
|
||||
VariableName::SelectedText.template_value()
|
||||
),
|
||||
command: "node".to_owned(),
|
||||
args: vec![
|
||||
"-e".to_owned(),
|
||||
format!("\"{}\"", VariableName::SelectedText.template_value()),
|
||||
],
|
||||
..TaskTemplate::default()
|
||||
});
|
||||
|
||||
match package_json_data.await {
|
||||
Ok(package_json) => {
|
||||
package_json.fill_task_templates(&mut task_templates);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to read package.json for worktree {file_abs_path:?}: {e:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some(task_templates)
|
||||
})
|
||||
}
|
||||
|
||||
fn build_context(
|
||||
@@ -370,73 +453,19 @@ impl ContextProvider for TypeScriptContextProvider {
|
||||
);
|
||||
}
|
||||
|
||||
let Some((fs, worktree_root)) = location.fs.zip(location.worktree_root) else {
|
||||
return Task::ready(Ok(vars));
|
||||
};
|
||||
|
||||
let package_json_contents = self.last_package_json.clone();
|
||||
let task = location
|
||||
.worktree_root
|
||||
.zip(location.fs)
|
||||
.map(|(worktree_root, fs)| self.detect_package_manager(worktree_root, fs, cx));
|
||||
cx.background_spawn(async move {
|
||||
let variables = package_json_variables(fs, worktree_root, package_json_contents)
|
||||
.await
|
||||
.context("package.json context retrieval")
|
||||
.log_err()
|
||||
.unwrap_or_else(task::TaskVariables::default);
|
||||
|
||||
vars.extend(variables);
|
||||
|
||||
if let Some(task) = task {
|
||||
vars.insert(TYPESCRIPT_RUNNER_VARIABLE, task.await.to_owned());
|
||||
}
|
||||
Ok(vars)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn package_json_variables(
|
||||
fs: Arc<dyn Fs>,
|
||||
worktree_root: PathBuf,
|
||||
package_json_contents: PackageJsonContents,
|
||||
) -> anyhow::Result<task::TaskVariables> {
|
||||
let package_json_path = worktree_root.join("package.json");
|
||||
let metadata = fs
|
||||
.metadata(&package_json_path)
|
||||
.await
|
||||
.with_context(|| format!("getting metadata for {package_json_path:?}"))?
|
||||
.with_context(|| format!("missing FS metadata for {package_json_path:?}"))?;
|
||||
let mtime = DateTime::<Local>::from(metadata.mtime.timestamp_for_user());
|
||||
let existing_data = {
|
||||
let contents = package_json_contents.0.read().await;
|
||||
contents
|
||||
.get(&package_json_path)
|
||||
.filter(|package_json| package_json.mtime == mtime)
|
||||
.map(|package_json| package_json.data)
|
||||
};
|
||||
|
||||
let mut variables = TaskVariables::default();
|
||||
if let Some(existing_data) = existing_data {
|
||||
existing_data.fill_variables(&mut variables);
|
||||
} else {
|
||||
let package_json_string = fs
|
||||
.load(&package_json_path)
|
||||
.await
|
||||
.with_context(|| format!("loading package.json from {package_json_path:?}"))?;
|
||||
let package_json: HashMap<String, serde_json::Value> =
|
||||
serde_json::from_str(&package_json_string)
|
||||
.with_context(|| format!("parsing package.json from {package_json_path:?}"))?;
|
||||
let new_data = PackageJsonData::new(package_json);
|
||||
new_data.fill_variables(&mut variables);
|
||||
{
|
||||
let mut contents = package_json_contents.0.write().await;
|
||||
contents.insert(
|
||||
package_json_path,
|
||||
PackageJson {
|
||||
mtime,
|
||||
data: new_data,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(variables)
|
||||
}
|
||||
|
||||
fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
||||
vec![server_path.into(), "--stdio".into()]
|
||||
}
|
||||
|
||||
@@ -25,5 +25,5 @@ documentation = { start = "/**", end = "*/", prefix = "* ", tab_size = 1 }
|
||||
completion_query_characters = ["."]
|
||||
prefer_label_for_snippet = true
|
||||
|
||||
[overrides.call_expression]
|
||||
[overrides.function_name_before_type_arguments]
|
||||
prefer_label_for_snippet = true
|
||||
|
||||
@@ -83,7 +83,10 @@
|
||||
] @context
|
||||
(#any-of? @_name "it" "test" "describe" "context" "suite")
|
||||
arguments: (
|
||||
arguments . (string (string_fragment) @name)
|
||||
arguments . [
|
||||
(string (string_fragment) @name)
|
||||
(identifier) @name
|
||||
]
|
||||
)
|
||||
)
|
||||
) @item
|
||||
@@ -100,7 +103,10 @@
|
||||
(#any-of? @_property "each")
|
||||
)
|
||||
arguments: (
|
||||
arguments . (string (string_fragment) @name)
|
||||
arguments . [
|
||||
(string (string_fragment) @name)
|
||||
(identifier) @name
|
||||
]
|
||||
)
|
||||
)
|
||||
) @item
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
(comment) @comment.inclusive
|
||||
(string) @string
|
||||
|
||||
(_ value: (call_expression) @call_expression)
|
||||
(_ value: (call_expression
|
||||
function: (identifier) @function_name_before_type_arguments
|
||||
type_arguments: (type_arguments)))
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
]
|
||||
(#any-of? @_name "it" "test" "describe" "context" "suite")
|
||||
arguments: (
|
||||
arguments . (string (string_fragment) @run)
|
||||
arguments . [
|
||||
(string (string_fragment) @run)
|
||||
(identifier) @run
|
||||
]
|
||||
)
|
||||
) @_js-test
|
||||
|
||||
@@ -32,7 +35,10 @@
|
||||
(#any-of? @_property "each")
|
||||
)
|
||||
arguments: (
|
||||
arguments . (string (string_fragment) @run)
|
||||
arguments . [
|
||||
(string (string_fragment) @run)
|
||||
(identifier) @run
|
||||
]
|
||||
)
|
||||
) @_js-test
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ pub struct Model {
|
||||
pub display_name: Option<String>,
|
||||
pub max_tokens: usize,
|
||||
pub supports_tools: Option<bool>,
|
||||
pub supports_images: Option<bool>,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
@@ -61,6 +62,7 @@ impl Model {
|
||||
Some("Auto Router"),
|
||||
Some(2000000),
|
||||
Some(true),
|
||||
Some(false),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -73,12 +75,14 @@ impl Model {
|
||||
display_name: Option<&str>,
|
||||
max_tokens: Option<usize>,
|
||||
supports_tools: Option<bool>,
|
||||
supports_images: Option<bool>,
|
||||
) -> Self {
|
||||
Self {
|
||||
name: name.to_owned(),
|
||||
display_name: display_name.map(|s| s.to_owned()),
|
||||
max_tokens: max_tokens.unwrap_or(2000000),
|
||||
supports_tools,
|
||||
supports_images,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,22 +158,118 @@ pub struct FunctionDefinition {
|
||||
#[serde(tag = "role", rename_all = "lowercase")]
|
||||
pub enum RequestMessage {
|
||||
Assistant {
|
||||
content: Option<String>,
|
||||
content: Option<MessageContent>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
tool_calls: Vec<ToolCall>,
|
||||
},
|
||||
User {
|
||||
content: String,
|
||||
content: MessageContent,
|
||||
},
|
||||
System {
|
||||
content: String,
|
||||
content: MessageContent,
|
||||
},
|
||||
Tool {
|
||||
content: String,
|
||||
content: MessageContent,
|
||||
tool_call_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum MessageContent {
|
||||
Plain(String),
|
||||
Multipart(Vec<MessagePart>),
|
||||
}
|
||||
|
||||
impl MessageContent {
|
||||
pub fn empty() -> Self {
|
||||
Self::Plain(String::new())
|
||||
}
|
||||
|
||||
pub fn push_part(&mut self, part: MessagePart) {
|
||||
match self {
|
||||
Self::Plain(text) if text.is_empty() => {
|
||||
*self = Self::Multipart(vec![part]);
|
||||
}
|
||||
Self::Plain(text) => {
|
||||
let text_part = MessagePart::Text {
|
||||
text: std::mem::take(text),
|
||||
};
|
||||
*self = Self::Multipart(vec![text_part, part]);
|
||||
}
|
||||
Self::Multipart(parts) => parts.push(part),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<MessagePart>> for MessageContent {
|
||||
fn from(parts: Vec<MessagePart>) -> Self {
|
||||
if parts.len() == 1 {
|
||||
if let MessagePart::Text { text } = &parts[0] {
|
||||
return Self::Plain(text.clone());
|
||||
}
|
||||
}
|
||||
Self::Multipart(parts)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for MessageContent {
|
||||
fn from(text: String) -> Self {
|
||||
Self::Plain(text)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for MessageContent {
|
||||
fn from(text: &str) -> Self {
|
||||
Self::Plain(text.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageContent {
|
||||
pub fn as_text(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Plain(text) => Some(text),
|
||||
Self::Multipart(parts) if parts.len() == 1 => {
|
||||
if let MessagePart::Text { text } = &parts[0] {
|
||||
Some(text)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_text(&self) -> String {
|
||||
match self {
|
||||
Self::Plain(text) => text.clone(),
|
||||
Self::Multipart(parts) => parts
|
||||
.iter()
|
||||
.filter_map(|part| {
|
||||
if let MessagePart::Text { text } = part {
|
||||
Some(text.as_str())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum MessagePart {
|
||||
Text {
|
||||
text: String,
|
||||
},
|
||||
#[serde(rename = "image_url")]
|
||||
Image {
|
||||
image_url: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
pub struct ToolCall {
|
||||
pub id: String,
|
||||
@@ -266,6 +366,14 @@ pub struct ModelEntry {
|
||||
pub context_length: Option<usize>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub supported_parameters: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub architecture: Option<ModelArchitecture>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
|
||||
pub struct ModelArchitecture {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub input_modalities: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn complete(
|
||||
@@ -470,6 +578,13 @@ pub async fn list_models(client: &dyn HttpClient, api_url: &str) -> Result<Vec<M
|
||||
),
|
||||
max_tokens: entry.context_length.unwrap_or(2000000),
|
||||
supports_tools: Some(entry.supported_parameters.contains(&"tools".to_string())),
|
||||
supports_images: Some(
|
||||
entry
|
||||
.architecture
|
||||
.as_ref()
|
||||
.map(|arch| arch.input_modalities.contains(&"image".to_string()))
|
||||
.unwrap_or(false),
|
||||
),
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -4497,8 +4497,10 @@ impl OutlinePanel {
|
||||
let multi_buffer_snapshot = self
|
||||
.active_editor()
|
||||
.map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx));
|
||||
uniform_list(cx.entity().clone(), "entries", items_len, {
|
||||
move |outline_panel, range, window, cx| {
|
||||
uniform_list(
|
||||
"entries",
|
||||
items_len,
|
||||
cx.processor(move |outline_panel, range: Range<usize>, window, cx| {
|
||||
let entries = outline_panel.cached_entries.get(range);
|
||||
entries
|
||||
.map(|entries| entries.to_vec())
|
||||
@@ -4555,8 +4557,8 @@ impl OutlinePanel {
|
||||
),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
.with_sizing_behavior(ListSizingBehavior::Infer)
|
||||
.with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
|
||||
.with_width_from_item(self.max_width_item_index)
|
||||
|
||||
@@ -17,7 +17,7 @@ use gpui::{
|
||||
use head::Head;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use ui::{
|
||||
Color, Divider, Label, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, prelude::*, v_flex,
|
||||
};
|
||||
@@ -760,14 +760,13 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
|
||||
match &self.element_container {
|
||||
ElementContainer::UniformList(scroll_handle) => uniform_list(
|
||||
cx.entity().clone(),
|
||||
"candidates",
|
||||
self.delegate.match_count(),
|
||||
move |picker, visible_range, window, cx| {
|
||||
cx.processor(move |picker, visible_range: Range<usize>, window, cx| {
|
||||
visible_range
|
||||
.map(|ix| picker.render_element(window, cx, ix))
|
||||
.collect()
|
||||
},
|
||||
}),
|
||||
)
|
||||
.with_sizing_behavior(sizing_behavior)
|
||||
.when_some(self.widest_item, |el, widest_item| {
|
||||
|
||||
@@ -665,6 +665,7 @@ impl BreakpointStore {
|
||||
.as_ref()
|
||||
.is_some_and(|active_position| active_position == &position)
|
||||
{
|
||||
cx.emit(BreakpointStoreEvent::SetDebugLine);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user