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