Compare commits

..

22 Commits

Author SHA1 Message Date
Junkui Zhang
2eac6a9222 fix linux ci 2025-06-11 18:16:59 +08:00
Junkui Zhang
7c3cffdc52 clippy 2025-06-11 17:32:18 +08:00
Junkui Zhang
5a3186b659 fix neovim 2025-06-11 16:37:37 +08:00
Junkui Zhang
caf54844de remove international keycodes 2025-06-11 16:33:11 +08:00
Junkui Zhang
745ebe2313 fix all tests 2025-06-11 16:16:31 +08:00
Junkui Zhang
5c95e942e6 fix 2025-06-10 23:35:53 +08:00
Junkui Zhang
f979f24bfa try fix linux 2025-06-10 23:32:00 +08:00
Junkui Zhang
411b9abb9e clippy 2025-06-10 23:13:43 +08:00
Junkui Zhang
81d4d48ef2 add missing codes 2025-06-10 23:11:21 +08:00
Junkui Zhang
cd9284761a fix macOS 2025-06-10 23:01:45 +08:00
Junkui Zhang
34f9eef879 try fix macOS 2025-06-10 22:14:46 +08:00
Junkui Zhang
23cf6bf268 init macOS 2025-06-10 21:16:23 +08:00
Junkui Zhang
c97e477eb1 fix test platform 2025-06-10 20:58:39 +08:00
Junkui Zhang
16804a81cc checkpoint 2025-06-10 18:27:35 +08:00
Junkui Zhang
8bf39bf768 fix 2025-06-10 18:16:21 +08:00
Junkui Zhang
75922e8fcd fix 2025-06-10 17:58:05 +08:00
Junkui Zhang
2eb83364ae support parsing scan code 2025-06-10 17:58:05 +08:00
Junkui Zhang
5d22585ef5 add shifted key tests 2025-06-10 17:58:04 +08:00
Junkui Zhang
71303fa18b add tests 2025-06-10 17:58:04 +08:00
Junkui Zhang
5753b978a0 impl for windows 2025-06-10 17:58:04 +08:00
Junkui Zhang
9cf2490ed7 add PlatformKeyboardMapper 2025-06-10 17:58:04 +08:00
Junkui Zhang
28ea3ea529 add ScanCode 2025-06-10 17:58:04 +08:00
138 changed files with 3288 additions and 2912 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

5
Cargo.lock generated
View File

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

View File

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

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 348 B

View File

@@ -395,8 +395,6 @@
"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",
@@ -423,7 +421,6 @@
"x": "editor::SelectLine",
"shift-x": "editor::SelectLine",
"%": "editor::SelectAll",
// Window mode
"space w h": "workspace::ActivatePaneLeft",
"space w l": "workspace::ActivatePaneRight",
@@ -453,8 +450,7 @@
"ctrl-c": "editor::ToggleComments",
"d": "vim::HelixDelete",
"c": "vim::Substitute",
"shift-c": "editor::AddSelectionBelow",
"alt-shift-c": "editor::AddSelectionAbove"
"shift-c": "editor::AddSelectionBelow"
}
},
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -38,7 +38,8 @@ use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use text::{OffsetRangeExt, ToPoint as _};
use ui::prelude::*;
use util::{RangeExt, ResultExt, maybe};
use util::RangeExt;
use util::ResultExt;
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
use zed_actions::agent::OpenConfiguration;
@@ -1170,31 +1171,27 @@ impl InlineAssistant {
selections.select_anchor_ranges([position..position])
});
let mut scroll_target_range = None;
let mut scroll_target_top;
let mut scroll_target_bottom;
if let Some(decorations) = assist.decorations.as_ref() {
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(|| {
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 {
let snapshot = editor.snapshot(window, cx);
let start_row = assist
.range
.start
.to_display_point(&snapshot.display_snapshot)
.row();
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 = start_row.0 as f32;
scroll_target_bottom = scroll_target_top + 1.;
}
scroll_target_top -= editor.vertical_scroll_margin() as f32;
scroll_target_bottom += editor.vertical_scroll_margin() as f32;

View File

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

View File

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

View File

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

View File

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

View File

@@ -181,7 +181,7 @@ impl DebugTaskDefinition {
/// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session.
#[derive(Debug, Clone, PartialEq)]
pub struct DebugAdapterBinary {
pub command: Option<String>,
pub command: String,
pub arguments: Vec<String>,
pub envs: HashMap<String, String>,
pub cwd: Option<PathBuf>,
@@ -369,10 +369,6 @@ 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"))]
@@ -437,7 +433,7 @@ impl DebugAdapter for FakeAdapter {
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
Ok(DebugAdapterBinary {
command: Some("command".into()),
command: "command".into(),
arguments: vec![],
connection: None,
envs: HashMap::default(),

View File

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

View File

@@ -1,4 +1,4 @@
use anyhow::{Context as _, Result, anyhow, bail};
use anyhow::{Context as _, Result, bail};
use dap_types::{
ErrorResponse,
messages::{Message, Response},
@@ -12,6 +12,7 @@ use smol::{
io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader},
lock::Mutex,
net::{TcpListener, TcpStream},
process::Child,
};
use std::{
collections::HashMap,
@@ -21,7 +22,7 @@ use std::{
time::Duration,
};
use task::TcpArgumentsTemplate;
use util::ConnectionResult;
use util::{ConnectionResult, ResultExt as _};
use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings};
@@ -85,12 +86,10 @@ 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")
}
}
@@ -103,7 +102,7 @@ impl Transport {
}
}
async fn kill(&self) {
async fn kill(&self) -> Result<()> {
match self {
Transport::Stdio(stdio_transport) => stdio_transport.kill().await,
Transport::Tcp(tcp_transport) => tcp_transport.kill().await,
@@ -192,7 +191,7 @@ impl TransportDelegate {
match Self::handle_output(
params.output,
client_tx,
pending_requests.clone(),
pending_requests,
output_log_handler,
)
.await
@@ -200,12 +199,6 @@ 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() {
@@ -538,7 +531,7 @@ impl TransportDelegate {
current_requests.clear();
pending_requests.clear();
self.transport.kill().await;
let _ = self.transport.kill().await.log_err();
drop(current_requests);
drop(pending_requests);
@@ -569,7 +562,7 @@ pub struct TcpTransport {
pub port: u16,
pub host: Ipv4Addr,
pub timeout: u64,
process: Option<Mutex<Child>>,
process: Mutex<Child>,
}
impl TcpTransport {
@@ -598,23 +591,26 @@ impl TcpTransport {
let host = connection_args.host;
let port = connection_args.port;
let mut process = if let Some(command) = &binary.command {
let mut command = util::command::new_std_command(&command);
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);
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);
Some(
Child::spawn(command, Stdio::null())
.with_context(|| "failed to start debug adapter.")?,
)
} else {
None
};
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.")?;
let address = SocketAddrV4::new(host, port);
@@ -632,18 +628,15 @@ impl TcpTransport {
match TcpStream::connect(address).await {
Ok(stream) => return Ok((process, stream.split())),
Err(_) => {
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.");
}
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.");
}
cx.background_executor().timer(Duration::from_millis(100)).await;
}
}
@@ -656,13 +649,13 @@ impl TcpTransport {
host,
port
);
let stdout = process.as_mut().and_then(|p| p.stdout.take());
let stderr = process.as_mut().and_then(|p| p.stderr.take());
let stdout = process.stdout.take();
let stderr = process.stderr.take();
let this = Self {
port,
host,
process: process.map(Mutex::new),
process: Mutex::new(process),
timeout,
};
@@ -680,19 +673,10 @@ impl TcpTransport {
true
}
async fn kill(&self) {
if let Some(process) = &self.process {
let mut process = process.lock().await;
Child::kill(&mut process);
}
}
}
async fn kill(&self) -> Result<()> {
self.process.lock().await.kill()?;
impl Drop for TcpTransport {
fn drop(&mut self) {
if let Some(mut p) = self.process.take() {
p.get_mut().kill();
}
Ok(())
}
}
@@ -703,12 +687,9 @@ 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 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);
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);
if let Some(cwd) = &binary.cwd {
command.current_dir(cwd);
@@ -717,10 +698,16 @@ impl StdioTransport {
command.args(&binary.arguments);
command.envs(&binary.envs);
let mut process = Child::spawn(command, Stdio::piped()).with_context(|| {
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true);
let mut process = command.spawn().with_context(|| {
format!(
"failed to spawn command `{} {}`.",
binary_command,
binary.command,
binary.arguments.join(" ")
)
})?;
@@ -735,7 +722,7 @@ impl StdioTransport {
if stderr.is_none() {
bail!(
"Failed to connect to stderr for debug adapter command {}",
&binary_command
&binary.command
);
}
@@ -758,15 +745,9 @@ impl StdioTransport {
false
}
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();
async fn kill(&self) -> Result<()> {
self.process.lock().await.kill()?;
Ok(())
}
}
@@ -940,66 +921,7 @@ impl FakeTransport {
false
}
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();
async fn kill(&self) -> Result<()> {
Ok(())
}
}

View File

@@ -21,21 +21,18 @@ 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();
let obj = configuration
configuration
.as_object_mut()
.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());
.context("CodeLLDB is not a valid json object")?
.insert(
"name".into(),
Value::String(String::from(task_definition.label.as_ref())),
);
let request = self.request_kind(&configuration)?;
@@ -362,13 +359,13 @@ impl DebugAdapter for CodeLldbDebugAdapter {
};
Ok(DebugAdapterBinary {
command: Some(command.unwrap()),
command: 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(delegate, &config)?,
request_args: self.request_args(&config)?,
envs: HashMap::default(),
connection: None,
})

View File

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

View File

@@ -462,21 +462,14 @@ 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: Some(minidelve_path.to_string_lossy().into_owned()),
command: minidelve_path.to_string_lossy().into_owned(),
arguments,
cwd: Some(cwd),
envs: HashMap::default(),
connection: None,
request_args: StartDebuggingRequestArguments {
configuration,
configuration: task_definition.config.clone(),
request: self.request_kind(&task_definition.config)?,
},
})

View File

@@ -2,7 +2,6 @@ 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;
@@ -69,24 +68,13 @@ 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: Some(
delegate
.node_runtime()
.binary_path()
.await?
.to_string_lossy()
.into_owned(),
),
command: delegate
.node_runtime()
.binary_path()
.await?
.to_string_lossy()
.into_owned(),
arguments: vec![
adapter_path
.join(Self::ADAPTER_PATH)
@@ -103,7 +91,7 @@ impl JsDebugAdapter {
timeout,
}),
request_args: StartDebuggingRequestArguments {
configuration,
configuration: task_definition.config.clone(),
request: self.request_kind(&task_definition.config)?,
},
})
@@ -183,7 +171,7 @@ impl DebugAdapter for JsDebugAdapter {
"properties": {
"type": {
"type": "string",
"enum": ["pwa-node", "node", "chrome", "pwa-chrome", "msedge", "pwa-msedge"],
"enum": ["pwa-node", "node", "chrome", "pwa-chrome", "edge", "pwa-edge"],
"description": "The type of debug session",
"default": "pwa-node"
},
@@ -443,25 +431,4 @@ impl DebugAdapter for JsDebugAdapter {
self.get_installed_binary(delegate, &config, user_installed_path, cx)
.await
}
fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
let label = args.configuration.get("name")?.as_str()?;
Some(label.to_owned())
}
}
fn normalize_task_type(task_type: &mut Value) {
let Some(task_type_str) = task_type.as_str() else {
return;
};
let new_name = match task_type_str {
"node" | "pwa-node" => "pwa-node",
"chrome" | "pwa-chrome" => "pwa-chrome",
"edge" | "msedge" | "pwa-edge" | "pwa-msedge" => "pwa-msedge",
_ => task_type_str,
}
.to_owned();
*task_type = Value::String(new_name);
}

View File

@@ -71,21 +71,13 @@ 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: Some(
delegate
.node_runtime()
.binary_path()
.await?
.to_string_lossy()
.into_owned(),
),
command: delegate
.node_runtime()
.binary_path()
.await?
.to_string_lossy()
.into_owned(),
arguments: vec![
adapter_path
.join(Self::ADAPTER_PATH)
@@ -101,7 +93,7 @@ impl PhpDebugAdapter {
cwd: Some(delegate.worktree_root_path().to_path_buf()),
envs: HashMap::default(),
request_args: StartDebuggingRequestArguments {
configuration,
configuration: task_definition.config.clone(),
request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)?,
},
})

View File

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

View File

@@ -174,15 +174,8 @@ 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: Some(rdbg_path.to_string_lossy().to_string()),
command: rdbg_path.to_string_lossy().to_string(),
arguments,
connection: Some(dap::adapters::TcpArguments {
host,
@@ -197,7 +190,7 @@ impl DebugAdapter for RubyDebugAdapter {
envs: ruby_config.env.into_iter().collect(),
request_args: StartDebuggingRequestArguments {
request: self.request_kind(&definition.config)?,
configuration,
configuration: definition.config.clone(),
},
})
}

View File

@@ -4,17 +4,19 @@ use crate::session::running::RunningState;
use crate::{
ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, NewProcessModal,
NewProcessMode, Pause, Restart, StepInto, StepOut, StepOver, Stop, ToggleExpandItem,
ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
NewProcessMode, Pause, Restart, ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop,
ToggleExpandItem, ToggleIgnoreBreakpoints, 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,
@@ -27,6 +29,7 @@ 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::*};
@@ -137,6 +140,82 @@ 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,
@@ -154,6 +233,17 @@ 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
@@ -355,7 +445,10 @@ impl DebugPanel {
};
let dap_store_handle = self.project.read(cx).dap_store().clone();
let label = self.label_for_child_session(&parent_session, request, cx);
let mut label = parent_session.read(cx).label().clone();
if !label.ends_with("(child)") {
label = format!("{label} (child)").into();
}
let adapter = parent_session.read(cx).adapter().clone();
let mut binary = parent_session.read(cx).binary().clone();
binary.request_args = request.clone();
@@ -515,12 +608,6 @@ 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()
@@ -542,8 +629,6 @@ 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(
@@ -732,48 +817,33 @@ impl DebugPanel {
}
}),
)
.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,
)
}
}),
)
},
.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,
)
}
}),
)
},
),
)
.justify_around()
.when(is_side, |this| {
this.child(new_session_button())
.child(documentation_button())
}),
.when(is_side, |this| this.child(new_session_button())),
)
.child(
h_flex()
@@ -814,10 +884,7 @@ impl DebugPanel {
window,
cx,
))
.when(!is_side, |this| {
this.child(new_session_button())
.child(documentation_button())
}),
.when(!is_side, |this| this.child(new_session_button())),
),
),
)
@@ -972,25 +1039,6 @@ 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(
@@ -1018,11 +1066,6 @@ 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)
@@ -1036,8 +1079,8 @@ async fn register_session_inner(
let debug_session = DebugSession::running(
this.project.clone(),
this.workspace.clone(),
parent_session.map(|p| p.read(cx).running_state().read(cx).debug_terminal.clone()),
session,
cx.weak_entity(),
serialized_layout,
this.position(window, cx).axis(),
window,

View File

@@ -1,17 +1,14 @@
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, DispatchPhase, EntityInputHandler, actions};
use gpui::{App, EntityInputHandler, actions};
use new_process_modal::{NewProcessModal, NewProcessMode};
use project::debugger::{self, breakpoint_store::SourceBreakpoint, session::ThreadStatus};
use project::debugger::{self, breakpoint_store::SourceBreakpoint};
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};
@@ -71,6 +68,148 @@ 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);
})
@@ -84,255 +223,90 @@ 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, _, _| {
move |editor: &mut Editor, _, cx| {
editor
.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();
.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()?;
editor
.buffer()
.read(cx)
.point_to_buffer_point(cursor_point, cx)
})
.ok()??;
let (buffer, position, _) = editor
.buffer()
.read(cx)
.point_to_buffer_point(cursor_point, cx)?;
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,
};
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(())
});
}
});
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);
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
.evaluate(text, None, stack_id, None, cx)
.detach();
});
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();
});
});
Some(())
});
},
);
})
Some(())
});
},
))
.detach();
}
})

View File

@@ -1,4 +1,4 @@
use collections::{FxHashMap, HashMap};
use collections::FxHashMap;
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::{
Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
HighlightStyle, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText,
Subscription, TextStyle, UnderlineStyle, WeakEntity,
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, 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,
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,
};
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>,
configure_mode: Entity<ConfigureMode>,
launch_mode: Entity<ConfigureMode>,
task_mode: TaskMode,
debugger: Option<DebugAdapterName>,
// save_scenario_state: Option<SaveScenarioState>,
@@ -194,12 +194,11 @@ 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.clone(), cx)
})?
.await;
.used_and_current_resolved_tasks(&task_contexts, cx)
})?;
debug_picker
.update_in(cx, |picker, window, cx| {
@@ -254,7 +253,7 @@ impl NewProcessModal {
Self {
debug_picker,
attach_mode,
configure_mode,
launch_mode: configure_mode,
task_mode,
debugger: None,
mode,
@@ -284,7 +283,7 @@ impl NewProcessModal {
NewProcessMode::Attach => self.attach_mode.update(cx, |this, cx| {
this.clone().render(window, cx).into_any_element()
}),
NewProcessMode::Launch => self.configure_mode.update(cx, |this, cx| {
NewProcessMode::Launch => self.launch_mode.update(cx, |this, cx| {
this.clone().render(dap_menu, window, cx).into_any_element()
}),
NewProcessMode::Debug => v_flex()
@@ -298,7 +297,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.configure_mode.read(cx).program.focus_handle(cx),
NewProcessMode::Launch => self.launch_mode.read(cx).program.focus_handle(cx),
NewProcessMode::Debug => self.debug_picker.focus_handle(cx),
}
}
@@ -306,7 +305,7 @@ impl NewProcessModal {
fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> {
let request = match self.mode {
NewProcessMode::Launch => Some(DebugRequest::Launch(
self.configure_mode.read(cx).debug_request(cx),
self.launch_mode.read(cx).debug_request(cx),
)),
NewProcessMode::Attach => Some(DebugRequest::Attach(
self.attach_mode.read(cx).debug_request(),
@@ -316,7 +315,7 @@ impl NewProcessModal {
let label = suggested_label(&request, debugger);
let stop_on_entry = if let NewProcessMode::Launch = &self.mode {
Some(self.configure_mode.read(cx).stop_on_entry.selected())
Some(self.launch_mode.read(cx).stop_on_entry.selected())
} else {
None
};
@@ -832,7 +831,7 @@ impl Render for NewProcessModal {
.disabled(
self.debugger.is_none()
|| self
.configure_mode
.launch_mode
.read(cx)
.program
.read(cx)
@@ -1203,7 +1202,7 @@ impl PickerDelegate for DebugDelegate {
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
"Find a debug task, or debug a command.".into()
"".into()
}
fn update_matches(
@@ -1266,96 +1265,6 @@ 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
@@ -1391,60 +1300,6 @@ impl PickerDelegate for DebugDelegate {
cx.emit(DismissEvent);
}
fn render_footer(
&self,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<ui::AnyElement> {
let current_modifiers = window.modifiers();
let footer = h_flex()
.w_full()
.h_8()
.p_2()
.justify_between()
.rounded_b_sm()
.bg(cx.theme().colors().ghost_element_selected)
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(
// TODO: add button to open selected task in debug.json
h_flex().into_any_element(),
)
.map(|this| {
if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() {
let action = picker::ConfirmInput {
secondary: current_modifiers.secondary(),
}
.boxed_clone();
this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| {
Button::new("launch-custom", "Launch Custom")
.label_size(LabelSize::Small)
.key_binding(keybind)
.on_click(move |_, window, cx| {
window.dispatch_action(action.boxed_clone(), cx)
})
}))
} else {
this.children(KeyBinding::for_action(&menu::Confirm, window, cx).map(
|keybind| {
let is_recent_selected =
self.divider_index >= Some(self.selected_index);
let run_entry_label =
if is_recent_selected { "Rerun" } else { "Spawn" };
Button::new("spawn", run_entry_label)
.label_size(LabelSize::Small)
.key_binding(keybind)
.on_click(|_, window, cx| {
window.dispatch_action(menu::Confirm.boxed_clone(), cx);
})
},
))
}
});
Some(footer.into_any_element())
}
fn render_match(
&self,
ix: usize,

View File

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

View File

@@ -605,7 +605,6 @@ 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,
@@ -618,8 +617,7 @@ impl RunningState {
StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
});
let debug_terminal =
parent_terminal.unwrap_or_else(|| cx.new(|cx| DebugTerminal::empty(window, cx)));
let debug_terminal = cx.new(|cx| DebugTerminal::empty(window, cx));
let variable_list =
cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
@@ -818,20 +816,20 @@ impl RunningState {
let request_type = dap_registry
.adapter(&adapter)
.with_context(|| format!("{}: is not a valid adapter name", &adapter))
.ok_or_else(|| anyhow!("{}: 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_template, locator_name) = match build {
let (task, locator_name) = match build {
BuildTaskDefinition::Template {
task_template,
locator_name,
} => (task_template, locator_name),
BuildTaskDefinition::ByName(ref label) => {
let task = task_store.update(cx, |this, cx| {
this.task_inventory().map(|inventory| {
let Some(task) = task_store.update(cx, |this, cx| {
this.task_inventory().and_then(|inventory| {
inventory.read(cx).task_template_by_label(
buffer,
worktree_id,
@@ -839,15 +837,14 @@ impl RunningState {
cx,
)
})
})?;
let task = match task {
Some(task) => task.await,
None => None,
}.with_context(|| format!("Couldn't find task template for {build:?}"))?;
})?
else {
anyhow::bail!("Couldn't find task template for {:?}", build)
};
(task, None)
}
};
let Some(task) = task_template.resolve_task("debug-build-task", &task_context) else {
let Some(task) = task.resolve_task("debug-build-task", &task_context) else {
anyhow::bail!("Could not resolve task variables within a debug scenario");
};
@@ -932,13 +929,15 @@ 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.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())
})?;
locator_name.context("Could not find a valid locator for a build task")?;
let request = dap_store
.update(cx, |this, cx| {
this.run_debug_locator(&locator_name, task, cx)
@@ -954,7 +953,7 @@ impl RunningState {
let scenario = dap_registry
.adapter(&adapter)
.with_context(|| anyhow!("{}: is not a valid adapter name", &adapter))
.ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))
.map(|adapter| adapter.config_from_zed_format(zed_config))??;
config = scenario.config;
Self::substitute_variables_in_config(&mut config, &task_context);

View File

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

View File

@@ -8,7 +8,7 @@ use project::{
ProjectItem as _, ProjectPath,
debugger::session::{Session, SessionEvent},
};
use std::{ops::Range, path::Path, sync::Arc};
use std::{path::Path, sync::Arc};
use ui::{Scrollbar, ScrollbarState, prelude::*};
use workspace::Workspace;
@@ -281,11 +281,10 @@ 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(),
cx.processor(|this, range: Range<usize>, _window, cx| {
range.map(|ix| this.render_entry(ix, cx)).collect()
}),
|this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(),
)
.track_scroll(self.scroll_handle.clone())
.size_full()

View File

@@ -183,7 +183,6 @@ 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 {
@@ -198,11 +197,6 @@ 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()));
}
}
@@ -215,10 +209,7 @@ impl StackFrameList {
std::mem::swap(&mut self.entries, &mut entries);
if let Some(ix) = first_not_subtle_frame
.or(first_stack_frame)
.filter(|_| open_first_stack_frame)
{
if let Some(ix) = first_stack_frame.filter(|_| open_first_stack_frame) {
self.select_ix(Some(ix), cx);
self.activate_selected_entry(window, cx);
} else if let Some(old_selected_frame_id) = old_selected_frame_id {

View File

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

View File

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

View File

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

View File

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

View File

@@ -240,6 +240,7 @@ 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<
@@ -917,7 +918,6 @@ enum SelectionDragState {
Dragging {
selection: Selection<Anchor>,
drop_cursor: Selection<Anchor>,
hide_drop_cursor: bool,
},
}
@@ -1053,9 +1053,8 @@ 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(&Editor, &mut Window, &mut Context<Self>)>>>,
>,
editor_actions:
Rc<RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&mut Window, &mut Context<Self>)>>>>,
use_autoclose: bool,
use_auto_surround: bool,
auto_replace_emoji_shortcode: bool,
@@ -5139,13 +5138,10 @@ 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 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 !menu.is_incomplete && filter_completions {
// 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.
//
@@ -5163,6 +5159,7 @@ impl Editor {
menu.initial_position.to_offset(&snapshot) == position.to_offset(&snapshot)
};
if position_matches {
menu.filter(query.clone(), provider.clone(), window, cx);
return;
}
}
@@ -7541,7 +7538,8 @@ impl Editor {
"Set Breakpoint"
};
let run_to_cursor = window.is_action_available(&RunToCursor, cx);
let run_to_cursor = command_palette_hooks::CommandPaletteFilter::try_global(cx)
.map_or(false, |filter| !filter.is_hidden(&DebuggerRunToCursor));
let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state {
BreakpointState::Enabled => Some("Disable"),
@@ -7565,7 +7563,7 @@ impl Editor {
})
.ok();
window.dispatch_action(Box::new(RunToCursor), cx);
window.dispatch_action(Box::new(DebuggerRunToCursor), cx);
})
.separator()
})
@@ -14037,8 +14035,7 @@ impl Editor {
prefer_lsp && !lsp_tasks_by_rows.is_empty(),
new_rows,
cx.clone(),
)
.await;
);
editor
.update(cx, |editor, _| {
editor.clear_tasks();
@@ -14068,40 +14065,35 @@ impl Editor {
snapshot: DisplaySnapshot,
prefer_lsp: bool,
runnable_ranges: Vec<RunnableRange>,
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
mut cx: AsyncWindowContext,
) -> Vec<((BufferId, BufferRow), RunnableTasks)> {
runnable_ranges
.into_iter()
.filter_map(|mut runnable| {
let mut tasks = cx
.update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx))
.ok()
else {
continue;
};
let mut tasks = tasks.await;
.ok()?;
if prefer_lsp {
tasks.retain(|(task_kind, _)| {
!matches!(task_kind, TaskSourceKind::Language { .. })
});
}
if tasks.is_empty() {
continue;
return None;
}
let point = runnable.run_range.start.to_point(&snapshot.buffer_snapshot);
let Some(row) = snapshot
let row = snapshot
.buffer_snapshot
.buffer_line_for_row(MultiBufferRow(point.row))
.map(|(_, range)| range.start.row)
else {
continue;
};
.buffer_line_for_row(MultiBufferRow(point.row))?
.1
.start
.row;
let context_range =
BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end);
runnable_rows.push((
Some((
(runnable.buffer_id, row),
RunnableTasks {
templates: tasks,
@@ -14112,17 +14104,16 @@ impl Editor {
column: point.column,
extra_variables: runnable.extra_captures,
},
));
}
runnable_rows
})
))
})
.collect()
}
fn templates_with_tags(
project: &Entity<Project>,
runnable: &mut Runnable,
cx: &mut App,
) -> Task<Vec<(TaskSourceKind, TaskTemplate)>> {
) -> 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)
@@ -14137,40 +14128,39 @@ impl Editor {
)
});
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)
},
));
}
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);
}
templates_with_tags.sort_by_key(|(kind, _)| kind.to_owned());
}
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
})
templates_with_tags
}
pub fn move_to_enclosing_bracket(
@@ -16086,16 +16076,13 @@ impl Editor {
window: &Window,
cx: &mut Context<Self>,
) -> Option<()> {
if !self.mode().is_full() {
return None;
}
let project = self.project.as_ref()?.downgrade();
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 {
@@ -18146,9 +18133,16 @@ impl Editor {
.selections
.disjoint_anchors()
.iter()
.map(|range| Location {
buffer: buffer.clone(),
range: range.start.text_anchor..range.end.text_anchor,
.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,
}
})
.collect::<Vec<_>>();
@@ -19741,7 +19735,7 @@ impl Editor {
.flatten()
.filter_map(|keystroke| {
if keystroke.modifiers.is_subset_of(&Modifiers::shift()) {
keystroke.key_char.clone()
Some(keystroke.key_char.clone().unwrap_or(keystroke.key.clone()))
} else {
None
}
@@ -19818,21 +19812,6 @@ 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,
@@ -19841,7 +19820,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();
@@ -20073,13 +20052,8 @@ 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();
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 let Some(scope) = buffer_snapshot.language_scope_at(cursor_position) {
if scope.prefers_label_for_snippet_in_completion() {
if let Some(label) = completion.label() {
if matches!(
@@ -21557,8 +21531,7 @@ 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 * gutter_settings.min_line_number_digits as f32;
let min_width_for_number_on_gutter = em_advance * MIN_LINE_NUMBER_DIGITS as f32;
max_line_number_width.max(min_width_for_number_on_gutter)
} else {
0.0.into()

View File

@@ -150,7 +150,6 @@ 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,
@@ -610,10 +609,6 @@ pub struct GutterContent {
///
/// Default: true
pub line_numbers: Option<bool>,
/// Minimum number of characters to reserve space for in the gutter.
///
/// Default: 4
pub min_line_number_digits: Option<usize>,
/// Whether to show runnable buttons in the gutter.
///
/// Default: true

View File

@@ -6,10 +6,10 @@ use crate::{
Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT,
FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp,
MAX_LINE_LEN, 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, 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,
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)(editor, window, cx)
(action)(window, cx)
}
});
@@ -856,11 +856,8 @@ 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)
&& text_hitbox.is_hovered(window)
{
let is_cut = !(cfg!(target_os = "macos") && event.modifiers.alt
|| cfg!(not(target_os = "macos")) && event.modifiers.control);
if !point_for_position.intersects_selection(&selection_display) {
let is_cut = !event.modifiers.control;
editor.move_selection_on_drop(
&selection.clone(),
point_for_position.previous_valid,
@@ -868,11 +865,10 @@ 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;
}
_ => {}
}
@@ -945,8 +941,7 @@ impl EditorElement {
return;
}
let text_hitbox = &position_map.text_hitbox;
let text_bounds = text_hitbox.bounds;
let text_bounds = position_map.text_hitbox.bounds;
let point_for_position = position_map.point_for_position(event.position);
let mut scroll_delta = gpui::Point::<f32>::default();
@@ -987,12 +982,10 @@ 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 {
@@ -1005,7 +998,6 @@ impl EditorElement {
editor.selection_drag_state = SelectionDragState::Dragging {
selection: selection.clone(),
drop_cursor,
hide_drop_cursor: false,
};
}
_ => {}
@@ -1259,18 +1251,16 @@ impl EditorElement {
if let SelectionDragState::Dragging {
ref selection,
ref drop_cursor,
ref hide_drop_cursor,
} = editor.selection_drag_state
{
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))
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)
{
let drag_cursor_layout = SelectionLayout::new(
drop_cursor.clone(),
@@ -2826,8 +2816,7 @@ impl EditorElement {
let available_width = gutter_dimensions.left_padding - git_gutter_width;
let editor = self.editor.clone();
let is_wide = max_line_number_length
>= EditorSettings::get_global(cx).gutter.min_line_number_digits as u32
let is_wide = max_line_number_length >= MIN_LINE_NUMBER_DIGITS
&& row_info
.buffer_row
.is_some_and(|row| (row + 1).ilog10() + 1 == max_line_number_length)

View File

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

View File

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

View File

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

View File

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

View File

@@ -554,15 +554,13 @@ impl ExtensionsPage {
)
.child(
h_flex()
.gap_1()
.gap_2()
.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| {
@@ -570,12 +568,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| {
@@ -583,7 +581,9 @@ 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,7 +609,9 @@ impl ExtensionsPage {
});
}
}
}),
})
.color(Color::Accent)
.disabled(matches!(status, ExtensionStatus::Installing)),
)
}),
),
@@ -1477,12 +1479,18 @@ 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("entries", count, cx.processor(Self::render_extensions))
.flex_grow()
.pb_4()
.track_scroll(scroll_handle),
uniform_list(
extensions_page,
"entries",
count,
Self::render_extensions,
)
.flex_grow()
.pb_4()
.track_scroll(scroll_handle),
)
.child(
div()

View File

@@ -55,7 +55,6 @@ 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};
@@ -3711,10 +3710,8 @@ impl GitPanel {
.relative()
.overflow_hidden()
.child(
uniform_list(
"entries",
entry_count,
cx.processor(move |this, range: Range<usize>, window, cx| {
uniform_list(cx.entity().clone(), "entries", entry_count, {
move |this, range, window, cx| {
let mut items = Vec::with_capacity(range.end - range.start);
for ix in range {
@@ -3742,8 +3739,8 @@ impl GitPanel {
}
items
}),
)
}
})
.when(
!self.horizontal_scrollbar.show_track
&& self.horizontal_scrollbar.show_scrollbar,

View File

@@ -378,6 +378,8 @@ 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())
@@ -429,10 +431,8 @@ impl Render for DataTable {
.relative()
.size_full()
.child(
uniform_list(
"items",
self.quotes.len(),
cx.processor(move |this, range: Range<usize>, _, _| {
uniform_list(entity, "items", self.quotes.len(), {
move |this, range, _, _| {
this.visible_range = range.clone();
let mut items = Vec::with_capacity(range.end - range.start);
for i in range {
@@ -441,8 +441,8 @@ impl Render for DataTable {
}
}
items
}),
)
}
})
.size_full()
.track_scroll(self.scroll_handle.clone()),
)

View File

@@ -9,9 +9,10 @@ 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,
cx.processor(|_this, range, _window, _cx| {
|_this, range, _window, _cx| {
let mut items = Vec::new();
for ix in range {
let item = ix + 1;
@@ -28,7 +29,7 @@ impl Render for UniformListExample {
);
}
items
}),
},
)
.h_full(),
)

View File

@@ -37,10 +37,10 @@ use crate::{
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window,
WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder,
PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle,
Reservation, ScreenCaptureSource, SharedString, SubscriberSet, Subscription, SvgRenderer, Task,
TextSystem, Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
colors::{Colors, GlobalColors},
current_platform, hash, init_app_menus,
};
@@ -262,6 +262,7 @@ pub struct App {
pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
pub(crate) focus_handles: Arc<FocusMap>,
pub(crate) keymap: Rc<RefCell<Keymap>>,
pub(crate) keyboard_mapper: Box<dyn PlatformKeyboardMapper>,
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
pub(crate) global_action_listeners:
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
@@ -308,6 +309,7 @@ impl App {
let text_system = Arc::new(TextSystem::new(platform.text_system()));
let entities = EntityMap::new();
let keyboard_mapper = platform.keyboard_mapper();
let keyboard_layout = platform.keyboard_layout();
let app = Rc::new_cyclic(|this| AppCell {
@@ -333,6 +335,7 @@ impl App {
window_handles: FxHashMap::default(),
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
keymap: Rc::new(RefCell::new(Keymap::default())),
keyboard_mapper,
keyboard_layout,
global_action_listeners: FxHashMap::default(),
pending_effects: VecDeque::new(),
@@ -369,6 +372,7 @@ impl App {
move || {
if let Some(app) = app.upgrade() {
let cx = &mut app.borrow_mut();
cx.keyboard_mapper = cx.platform.keyboard_mapper();
cx.keyboard_layout = cx.platform.keyboard_layout();
cx.keyboard_layout_observers
.clone()
@@ -413,6 +417,11 @@ impl App {
self.quitting = false;
}
/// Get the keyboard mapper of current keyboard layout
pub fn keyboard_mapper(&self) -> &dyn PlatformKeyboardMapper {
self.keyboard_mapper.as_ref()
}
/// Get the id of the current keyboard layout
pub fn keyboard_layout(&self) -> &dyn PlatformKeyboardLayout {
self.keyboard_layout.as_ref()

View File

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

View File

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

View File

@@ -5,10 +5,10 @@
//! elements with uniform height.
use crate::{
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,
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,
};
use smallvec::SmallVec;
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -19,23 +19,28 @@ 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<R>(
id: impl Into<ElementId>,
pub fn uniform_list<I, R, V>(
view: Entity<V>,
id: I,
item_count: usize,
f: impl 'static + Fn(Range<usize>, &mut Window, &mut App) -> Vec<R>,
f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> 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: Range<usize>, window: &mut Window, cx: &mut App| {
f(range, window, cx)
.into_iter()
.map(|component| component.into_any_element())
.collect()
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()
})
};
UniformList {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -888,32 +888,6 @@ 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!(
@@ -948,6 +922,25 @@ fn handle_hit_test_msg(
return Some(HTTOP as _);
}
let mut lock = state_ptr.state.borrow_mut();
if let Some(mut callback) = lock.callbacks.hit_test_window_control.take() {
drop(lock);
let area = callback();
state_ptr
.state
.borrow_mut()
.callbacks
.hit_test_window_control = Some(callback);
if let Some(area) = area {
return match area {
WindowControlArea::Drag => Some(HTCAPTION as _),
WindowControlArea::Close => Some(HTCLOSE as _),
WindowControlArea::Max => Some(HTMAXBUTTON as _),
WindowControlArea::Min => Some(HTMINBUTTON as _),
};
}
}
Some(HTCLIENT as _)
}

View File

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

View File

@@ -81,9 +81,9 @@ impl WindowsPlatformState {
}
impl WindowsPlatform {
pub(crate) fn new() -> Result<Self> {
pub(crate) fn new() -> Self {
unsafe {
OleInitialize(None).context("unable to initialize Windows OLE")?;
OleInitialize(None).expect("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)
.context("Error creating bitmap factory.")?
.expect("Error creating bitmap factory.")
});
let text_system = Arc::new(
DirectWriteTextSystem::new(&bitmap_factory)
.context("Error creating DirectWriteTextSystem")?,
.expect("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().context("Unable to init GPU context")?;
let windows_version = WindowsVersion::new().context("Error retrieve windows version")?;
let gpu_context = BladeContext::new().expect("Unable to init GPU context");
let windows_version = WindowsVersion::new().expect("Error retrieve windows version");
Ok(Self {
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,6 +310,10 @@ impl Platform for WindowsPlatform {
self.text_system.clone()
}
fn keyboard_mapper(&self) -> Box<dyn PlatformKeyboardMapper> {
Box::new(WindowsKeyboardMapper::new())
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
Box::new(
WindowsKeyboardLayout::new()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -88,6 +88,7 @@ tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-typescript = { workspace = true, optional = true }
tree-sitter-yaml = { workspace = true, optional = true }
util.workspace = true
which.workspace = true
workspace-hack.workspace = true
[dev-dependencies]

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,6 @@ 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;
@@ -629,10 +628,9 @@ impl ContextProvider for RustContextProvider {
fn associated_tasks(
&self,
_: Arc<dyn Fs>,
file: Option<Arc<dyn language::File>>,
cx: &App,
) -> Task<Option<TaskTemplates>> {
) -> Option<TaskTemplates> {
const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
const CUSTOM_TARGET_DIR: &str = "RUST_TARGET_DIR";
@@ -800,7 +798,7 @@ impl ContextProvider for RustContextProvider {
.collect();
}
Task::ready(Some(TaskTemplates(task_templates)))
Some(TaskTemplates(task_templates))
}
fn lsp_task_source(&self) -> Option<LanguageServerName> {

View File

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

View File

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

View File

@@ -4,12 +4,10 @@ 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, LocalFile, LspAdapter,
LspAdapterDelegate,
ContextLocation, ContextProvider, File, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
use node_runtime::NodeRuntime;
@@ -19,12 +17,11 @@ 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, VariableName};
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
use util::archive::extract_zip;
use util::merge_json_value_into;
use util::{ResultExt, fs::remove_matching, maybe};
@@ -35,12 +32,23 @@ 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>>>);
@@ -50,21 +58,32 @@ struct PackageJson {
data: PackageJsonData,
}
#[derive(Clone, Default)]
#[derive(Clone, Copy, Default)]
struct PackageJsonData {
jest: bool,
mocha: bool,
vitest: bool,
jasmine: bool,
scripts: BTreeSet<String>,
package_manager: Option<&'static str>,
build_script: bool,
test_script: bool,
runner: Runner,
}
#[derive(Clone, Copy, Default)]
enum Runner {
#[default]
Npm,
Npx,
Pnpm,
}
impl PackageJsonData {
fn new(package_json: HashMap<String, Value>) -> Self {
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 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 jest = false;
@@ -85,351 +104,249 @@ impl PackageJsonData {
jasmine |= dev_dependencies.contains_key("jasmine");
}
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
}
});
let mut runner = Runner::Npm;
if which::which("pnpm").is_ok() {
runner = Runner::Pnpm;
} else if which::which("npx").is_ok() {
runner = Runner::Npx;
}
Self {
jest,
mocha,
vitest,
jasmine,
scripts,
package_manager,
build_script,
test_script,
runner,
}
}
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_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 fill_task_templates(&self, task_templates: &mut TaskTemplates) {
if self.jest {
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()
});
variables.insert(TYPESCRIPT_JEST_TASK_VARIABLE, "jest".to_owned());
}
if self.vitest {
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!(
"{} 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()
});
variables.insert(TYPESCRIPT_MOCHA_TASK_VARIABLE, "mocha".to_owned());
}
if self.vitest {
variables.insert(TYPESCRIPT_VITEST_TASK_VARIABLE, "vitest".to_owned());
}
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()
});
variables.insert(TYPESCRIPT_JASMINE_TASK_VARIABLE, "jasmine".to_owned());
}
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(),
script.to_owned(),
],
tags: vec!["package-script".into()],
cwd: Some(VariableName::WorktreeRoot.template_value()),
..TaskTemplate::default()
});
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 {
Self {
TypeScriptContextProvider {
last_package_json: PackageJsonContents::default(),
}
}
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);
fn associated_tasks(&self, _: Option<Arc<dyn File>>, _: &App) -> Option<TaskTemplates> {
let mut task_templates = TaskTemplates(Vec::new());
cx.background_spawn(async move {
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!(
"execute selection {}",
VariableName::SelectedText.template_value()
"package.json script {}",
package_json_script.template_value()
),
command: "node".to_owned(),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
"-e".to_owned(),
format!("\"{}\"", VariableName::SelectedText.template_value()),
"--prefix".to_owned(),
VariableName::WorktreeRoot.template_value(),
"run".to_owned(),
package_json_script.template_value(),
],
tags: vec!["package-script".into()],
cwd: Some(VariableName::WorktreeRoot.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:#}"
);
}
}
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()
});
Some(task_templates)
})
Some(task_templates)
}
fn build_context(
@@ -453,19 +370,73 @@ impl ContextProvider for TypeScriptContextProvider {
);
}
let task = location
.worktree_root
.zip(location.fs)
.map(|(worktree_root, fs)| self.detect_package_manager(worktree_root, fs, cx));
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();
cx.background_spawn(async move {
if let Some(task) = task {
vars.insert(TYPESCRIPT_RUNNER_VARIABLE, task.await.to_owned());
}
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);
Ok(vars)
})
}
}
async fn package_json_variables(
fs: Arc<dyn Fs>,
worktree_root: PathBuf,
package_json_contents: PackageJsonContents,
) -> anyhow::Result<task::TaskVariables> {
let package_json_path = worktree_root.join("package.json");
let metadata = fs
.metadata(&package_json_path)
.await
.with_context(|| format!("getting metadata for {package_json_path:?}"))?
.with_context(|| format!("missing FS metadata for {package_json_path:?}"))?;
let mtime = DateTime::<Local>::from(metadata.mtime.timestamp_for_user());
let existing_data = {
let contents = package_json_contents.0.read().await;
contents
.get(&package_json_path)
.filter(|package_json| package_json.mtime == mtime)
.map(|package_json| package_json.data)
};
let mut variables = TaskVariables::default();
if let Some(existing_data) = existing_data {
existing_data.fill_variables(&mut variables);
} else {
let package_json_string = fs
.load(&package_json_path)
.await
.with_context(|| format!("loading package.json from {package_json_path:?}"))?;
let package_json: HashMap<String, serde_json::Value> =
serde_json::from_str(&package_json_string)
.with_context(|| format!("parsing package.json from {package_json_path:?}"))?;
let new_data = PackageJsonData::new(package_json);
new_data.fill_variables(&mut variables);
{
let mut contents = package_json_contents.0.write().await;
contents.insert(
package_json_path,
PackageJson {
mtime,
data: new_data,
},
);
}
}
Ok(variables)
}
fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -52,7 +52,6 @@ pub struct Model {
pub display_name: Option<String>,
pub max_tokens: usize,
pub supports_tools: Option<bool>,
pub supports_images: Option<bool>,
}
impl Model {
@@ -62,7 +61,6 @@ impl Model {
Some("Auto Router"),
Some(2000000),
Some(true),
Some(false),
)
}
@@ -75,14 +73,12 @@ 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,
}
}
@@ -158,118 +154,22 @@ pub struct FunctionDefinition {
#[serde(tag = "role", rename_all = "lowercase")]
pub enum RequestMessage {
Assistant {
content: Option<MessageContent>,
content: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
tool_calls: Vec<ToolCall>,
},
User {
content: MessageContent,
content: String,
},
System {
content: MessageContent,
content: String,
},
Tool {
content: MessageContent,
content: String,
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,
@@ -366,14 +266,6 @@ 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(
@@ -578,13 +470,6 @@ pub async fn list_models(client: &dyn HttpClient, api_url: &str) -> Result<Vec<M
),
max_tokens: entry.context_length.unwrap_or(2000000),
supports_tools: Some(entry.supported_parameters.contains(&"tools".to_string())),
supports_images: Some(
entry
.architecture
.as_ref()
.map(|arch| arch.input_modalities.contains(&"image".to_string()))
.unwrap_or(false),
),
})
.collect();

View File

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

View File

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

View File

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