Compare commits
23 Commits
v0.202.8
...
v0.189.3-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eece7266dd | ||
|
|
26b5ceabe6 | ||
|
|
60bbaaded7 | ||
|
|
fc061ab139 | ||
|
|
cf6ffab260 | ||
|
|
4932a127d1 | ||
|
|
7fef60004b | ||
|
|
51c8e35b2e | ||
|
|
f556d634fc | ||
|
|
4315869c62 | ||
|
|
cab167d53d | ||
|
|
6c86825756 | ||
|
|
0e562f2176 | ||
|
|
abea2409cd | ||
|
|
a5b511edb3 | ||
|
|
be1efa5d33 | ||
|
|
ce8ff9572e | ||
|
|
5ec3dffa11 | ||
|
|
261646656e | ||
|
|
0ac8774cd4 | ||
|
|
ba5c7326e9 | ||
|
|
41f944bd9e | ||
|
|
fc306a271c |
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -559,6 +559,7 @@ dependencies = [
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4732,6 +4733,7 @@ dependencies = [
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
"ui",
|
||||
"unicode-script",
|
||||
"unicode-segmentation",
|
||||
"unindent",
|
||||
"url",
|
||||
@@ -5045,6 +5047,7 @@ dependencies = [
|
||||
"util",
|
||||
"uuid",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6147,6 +6150,7 @@ dependencies = [
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
"zed_llm_client",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
@@ -15581,6 +15585,7 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"hex",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"proto",
|
||||
@@ -16480,9 +16485,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.25.3"
|
||||
version = "0.25.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9ac5ea5e7f2f1700842ec071401010b9c59bf735295f6e9fa079c3dc035b167"
|
||||
checksum = "ac5fff5c47490dfdf473b5228039bfacad9d765d9b6939d26bf7cc064c1c7822"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
@@ -17115,8 +17120,6 @@ dependencies = [
|
||||
"tempfile",
|
||||
"tendril",
|
||||
"unicase",
|
||||
"unicode-script",
|
||||
"unicode-segmentation",
|
||||
"util_macros",
|
||||
"walkdir",
|
||||
"workspace-hack",
|
||||
@@ -19680,7 +19683,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.189.0"
|
||||
version = "0.189.3"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
@@ -19876,9 +19879,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_llm_client"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22a8b9575b215536ed8ad254ba07171e4e13bd029eda3b54cca4b184d2768050"
|
||||
checksum = "de7d9523255f4e00ee3d0918e5407bd252d798a4a8e71f6d37f23317a1588203"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
|
||||
@@ -572,7 +572,7 @@ tokio = { version = "1" }
|
||||
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
|
||||
toml = "0.8"
|
||||
tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.25.3", features = ["wasm"] }
|
||||
tree-sitter = { version = "0.25.5", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.23"
|
||||
tree-sitter-c = "0.23"
|
||||
tree-sitter-cpp = "0.23"
|
||||
@@ -617,7 +617,7 @@ wasmtime = { version = "29", default-features = false, features = [
|
||||
wasmtime-wasi = "29"
|
||||
which = "6.0.0"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.8.3"
|
||||
zed_llm_client = "0.8.4"
|
||||
zstd = "0.11"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
|
||||
@@ -1018,5 +1018,12 @@
|
||||
"bindings": {
|
||||
"enter": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "RunModal",
|
||||
"bindings": {
|
||||
"ctrl-tab": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivatePreviousItem"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1108,5 +1108,13 @@
|
||||
"bindings": {
|
||||
"enter": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "RunModal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-tab": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivatePreviousItem"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1452,9 +1452,7 @@
|
||||
"language_servers": ["erlang-ls", "!elp", "..."]
|
||||
},
|
||||
"Git Commit": {
|
||||
"allow_rewrap": "anywhere",
|
||||
"preferred_line_length": 72,
|
||||
"soft_wrap": "bounded"
|
||||
"allow_rewrap": "anywhere"
|
||||
},
|
||||
"Go": {
|
||||
"code_actions_on_format": {
|
||||
|
||||
@@ -311,6 +311,31 @@ impl ActivityIndicator {
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(session) = self
|
||||
.project
|
||||
.read(cx)
|
||||
.dap_store()
|
||||
.read(cx)
|
||||
.sessions()
|
||||
.find(|s| !s.read(cx).is_started())
|
||||
{
|
||||
return Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!("Debug: {}", session.read(cx).adapter()),
|
||||
tooltip_message: Some(session.read(cx).label().to_string()),
|
||||
on_click: None,
|
||||
});
|
||||
}
|
||||
|
||||
let current_job = self
|
||||
.project
|
||||
.read(cx)
|
||||
|
||||
@@ -55,6 +55,7 @@ use util::ResultExt as _;
|
||||
use util::markdown::MarkdownCodeBlock;
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::assistant::OpenRulesLibrary;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub struct ActiveThread {
|
||||
context_store: Entity<ContextStore>,
|
||||
@@ -1436,6 +1437,7 @@ impl ActiveThread {
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: None,
|
||||
mode: None,
|
||||
messages: vec![request_message],
|
||||
tools: vec![],
|
||||
@@ -1597,7 +1599,12 @@ impl ActiveThread {
|
||||
|
||||
this.thread.update(cx, |thread, cx| {
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(model.model, Some(window.window_handle()), cx);
|
||||
thread.send_to_model(
|
||||
model.model,
|
||||
CompletionIntent::UserPrompt,
|
||||
Some(window.window_handle()),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
this._load_edited_message_context_task = None;
|
||||
cx.notify();
|
||||
@@ -3691,7 +3698,8 @@ mod tests {
|
||||
|
||||
// Stream response to user message
|
||||
thread.update(cx, |thread, cx| {
|
||||
let request = thread.to_completion_request(model.clone(), cx);
|
||||
let request =
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx);
|
||||
thread.stream_completion(request, model, cx.active_window(), cx)
|
||||
});
|
||||
// Follow the agent
|
||||
|
||||
@@ -52,7 +52,7 @@ use workspace::{
|
||||
use zed_actions::agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding};
|
||||
use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
|
||||
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
|
||||
use zed_llm_client::UsageLimit;
|
||||
use zed_llm_client::{CompletionIntent, UsageLimit};
|
||||
|
||||
use crate::active_thread::{self, ActiveThread, ActiveThreadEvent};
|
||||
use crate::agent_configuration::{AgentConfiguration, AssistantConfigurationEvent};
|
||||
@@ -1296,7 +1296,12 @@ impl AgentPanel {
|
||||
active_thread.thread().update(cx, |thread, cx| {
|
||||
thread.insert_invisible_continue_message(cx);
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(model, Some(window.window_handle()), cx);
|
||||
thread.send_to_model(
|
||||
model,
|
||||
CompletionIntent::UserPrompt,
|
||||
Some(window.window_handle()),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -34,6 +34,7 @@ use std::{
|
||||
};
|
||||
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
|
||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub struct BufferCodegen {
|
||||
alternatives: Vec<Entity<CodegenAlternative>>,
|
||||
@@ -464,6 +465,7 @@ impl CodegenAlternative {
|
||||
LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: Some(CompletionIntent::InlineAssist),
|
||||
mode: None,
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
|
||||
@@ -42,6 +42,7 @@ use theme::ThemeSettings;
|
||||
use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use util::{ResultExt as _, maybe};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
|
||||
use crate::context_store::ContextStore;
|
||||
@@ -375,7 +376,12 @@ impl MessageEditor {
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(model, Some(window_handle), cx);
|
||||
thread.send_to_model(
|
||||
model,
|
||||
CompletionIntent::UserPrompt,
|
||||
Some(window_handle),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
@@ -1268,6 +1274,7 @@ impl MessageEditor {
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: None,
|
||||
mode: None,
|
||||
messages: vec![request_message],
|
||||
tools: vec![],
|
||||
|
||||
@@ -25,6 +25,7 @@ use terminal_view::TerminalView;
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use workspace::{Toast, Workspace, notifications::NotificationId};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -291,6 +292,7 @@ impl TerminalInlineAssistant {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
intent: Some(CompletionIntent::TerminalInlineAssist),
|
||||
messages: vec![request_message],
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
|
||||
@@ -38,7 +38,7 @@ use thiserror::Error;
|
||||
use ui::Window;
|
||||
use util::{ResultExt as _, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::CompletionRequestStatus;
|
||||
use zed_llm_client::{CompletionIntent, CompletionRequestStatus};
|
||||
|
||||
use crate::ThreadStore;
|
||||
use crate::context::{AgentContext, AgentContextHandle, ContextLoadResult, LoadedContext};
|
||||
@@ -1187,6 +1187,7 @@ impl Thread {
|
||||
pub fn send_to_model(
|
||||
&mut self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
intent: CompletionIntent,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -1196,7 +1197,7 @@ impl Thread {
|
||||
|
||||
self.remaining_turns -= 1;
|
||||
|
||||
let request = self.to_completion_request(model.clone(), cx);
|
||||
let request = self.to_completion_request(model.clone(), intent, cx);
|
||||
|
||||
self.stream_completion(request, model, window, cx);
|
||||
}
|
||||
@@ -1216,11 +1217,13 @@ impl Thread {
|
||||
pub fn to_completion_request(
|
||||
&self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
intent: CompletionIntent,
|
||||
cx: &mut Context<Self>,
|
||||
) -> LanguageModelRequest {
|
||||
let mut request = LanguageModelRequest {
|
||||
thread_id: Some(self.id.to_string()),
|
||||
prompt_id: Some(self.last_prompt_id.to_string()),
|
||||
intent: Some(intent),
|
||||
mode: None,
|
||||
messages: vec![],
|
||||
tools: Vec::new(),
|
||||
@@ -1374,12 +1377,14 @@ impl Thread {
|
||||
fn to_summarize_request(
|
||||
&self,
|
||||
model: &Arc<dyn LanguageModel>,
|
||||
intent: CompletionIntent,
|
||||
added_user_message: String,
|
||||
cx: &App,
|
||||
) -> LanguageModelRequest {
|
||||
let mut request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: Some(intent),
|
||||
mode: None,
|
||||
messages: vec![],
|
||||
tools: Vec::new(),
|
||||
@@ -1857,7 +1862,12 @@ impl Thread {
|
||||
If the conversation is about a specific subject, include it in the title. \
|
||||
Be descriptive. DO NOT speak in the first person.";
|
||||
|
||||
let request = self.to_summarize_request(&model.model, added_user_message.into(), cx);
|
||||
let request = self.to_summarize_request(
|
||||
&model.model,
|
||||
CompletionIntent::ThreadSummarization,
|
||||
added_user_message.into(),
|
||||
cx,
|
||||
);
|
||||
|
||||
self.summary = ThreadSummary::Generating;
|
||||
|
||||
@@ -1958,7 +1968,12 @@ impl Thread {
|
||||
4. Any action items or next steps if any\n\
|
||||
Format it in Markdown with headings and bullet points.";
|
||||
|
||||
let request = self.to_summarize_request(&model, added_user_message.into(), cx);
|
||||
let request = self.to_summarize_request(
|
||||
&model,
|
||||
CompletionIntent::ThreadContextSummarization,
|
||||
added_user_message.into(),
|
||||
cx,
|
||||
);
|
||||
|
||||
*self.detailed_summary_tx.borrow_mut() = DetailedSummaryState::Generating {
|
||||
message_id: last_message_id,
|
||||
@@ -2050,7 +2065,8 @@ impl Thread {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
) -> Vec<PendingToolUse> {
|
||||
self.auto_capture_telemetry(cx);
|
||||
let request = Arc::new(self.to_completion_request(model.clone(), cx));
|
||||
let request =
|
||||
Arc::new(self.to_completion_request(model.clone(), CompletionIntent::ToolResults, cx));
|
||||
let pending_tool_uses = self
|
||||
.tool_use
|
||||
.pending_tool_uses()
|
||||
@@ -2246,7 +2262,7 @@ impl Thread {
|
||||
if self.all_tools_finished() {
|
||||
if let Some(ConfiguredModel { model, .. }) = self.configured_model.as_ref() {
|
||||
if !canceled {
|
||||
self.send_to_model(model.clone(), window, cx);
|
||||
self.send_to_model(model.clone(), CompletionIntent::ToolResults, window, cx);
|
||||
}
|
||||
self.auto_capture_telemetry(cx);
|
||||
}
|
||||
@@ -2941,7 +2957,7 @@ fn main() {{
|
||||
|
||||
// Check message in request
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
|
||||
assert_eq!(request.messages.len(), 2);
|
||||
@@ -3036,7 +3052,7 @@ fn main() {{
|
||||
|
||||
// Check entire request to make sure all contexts are properly included
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
|
||||
// The request should contain all 3 messages
|
||||
@@ -3143,7 +3159,7 @@ fn main() {{
|
||||
|
||||
// Check message in request
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
|
||||
assert_eq!(request.messages.len(), 2);
|
||||
@@ -3169,7 +3185,7 @@ fn main() {{
|
||||
|
||||
// Check that both messages appear in the request
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
|
||||
assert_eq!(request.messages.len(), 3);
|
||||
@@ -3214,7 +3230,7 @@ fn main() {{
|
||||
|
||||
// Create a request and check that it doesn't have a stale buffer warning yet
|
||||
let initial_request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
|
||||
// Make sure we don't have a stale file warning yet
|
||||
@@ -3250,7 +3266,7 @@ fn main() {{
|
||||
|
||||
// Create a new request and check for the stale buffer warning
|
||||
let new_request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
|
||||
// We should have a stale file warning as the last message
|
||||
@@ -3300,7 +3316,7 @@ fn main() {{
|
||||
});
|
||||
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
assert_eq!(request.temperature, Some(0.66));
|
||||
|
||||
@@ -3320,7 +3336,7 @@ fn main() {{
|
||||
});
|
||||
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
assert_eq!(request.temperature, Some(0.66));
|
||||
|
||||
@@ -3340,7 +3356,7 @@ fn main() {{
|
||||
});
|
||||
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
assert_eq!(request.temperature, Some(0.66));
|
||||
|
||||
@@ -3360,7 +3376,7 @@ fn main() {{
|
||||
});
|
||||
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx)
|
||||
});
|
||||
assert_eq!(request.temperature, None);
|
||||
}
|
||||
@@ -3392,7 +3408,12 @@ fn main() {{
|
||||
// Send a message
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message("Hi!", ContextLoadResult::default(), None, vec![], cx);
|
||||
thread.send_to_model(model.clone(), None, cx);
|
||||
thread.send_to_model(
|
||||
model.clone(),
|
||||
CompletionIntent::ThreadSummarization,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let fake_model = model.as_fake();
|
||||
@@ -3487,7 +3508,7 @@ fn main() {{
|
||||
vec![],
|
||||
cx,
|
||||
);
|
||||
thread.send_to_model(model.clone(), None, cx);
|
||||
thread.send_to_model(model.clone(), CompletionIntent::UserPrompt, None, cx);
|
||||
});
|
||||
|
||||
let fake_model = model.as_fake();
|
||||
@@ -3525,7 +3546,12 @@ fn main() {{
|
||||
) {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message("Hi!", ContextLoadResult::default(), None, vec![], cx);
|
||||
thread.send_to_model(model.clone(), None, cx);
|
||||
thread.send_to_model(
|
||||
model.clone(),
|
||||
CompletionIntent::ThreadSummarization,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
@@ -57,6 +57,7 @@ uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -45,6 +45,7 @@ use text::{BufferSnapshot, ToPoint};
|
||||
use ui::IconName;
|
||||
use util::{ResultExt, TryFutureExt, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct ContextId(String);
|
||||
@@ -2272,6 +2273,7 @@ impl AssistantContext {
|
||||
let mut completion_request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: Some(CompletionIntent::UserPrompt),
|
||||
mode: None,
|
||||
messages: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
|
||||
@@ -155,53 +155,35 @@ impl LanguageModelSelector {
|
||||
}
|
||||
|
||||
fn all_models(cx: &App) -> GroupedModels {
|
||||
let mut recommended = Vec::new();
|
||||
let mut recommended_set = HashSet::default();
|
||||
for provider in LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
let providers = LanguageModelRegistry::global(cx).read(cx).providers();
|
||||
|
||||
let recommended = providers
|
||||
.iter()
|
||||
{
|
||||
let models = provider.recommended_models(cx);
|
||||
recommended_set.extend(models.iter().map(|model| (model.provider_id(), model.id())));
|
||||
recommended.extend(
|
||||
.flat_map(|provider| {
|
||||
provider
|
||||
.recommended_models(cx)
|
||||
.into_iter()
|
||||
.map(move |model| ModelInfo {
|
||||
model: model.clone(),
|
||||
.map(|model| ModelInfo {
|
||||
model,
|
||||
icon: provider.icon(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let other_models = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.map(|provider| {
|
||||
(
|
||||
provider.id(),
|
||||
provider
|
||||
.provided_models(cx)
|
||||
.into_iter()
|
||||
.filter_map(|model| {
|
||||
let not_included =
|
||||
!recommended_set.contains(&(model.provider_id(), model.id()));
|
||||
not_included.then(|| ModelInfo {
|
||||
model: model.clone(),
|
||||
icon: provider.icon(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<IndexMap<_, _>>();
|
||||
.collect();
|
||||
|
||||
GroupedModels {
|
||||
recommended,
|
||||
other: other_models,
|
||||
}
|
||||
let other = providers
|
||||
.iter()
|
||||
.flat_map(|provider| {
|
||||
provider
|
||||
.provided_models(cx)
|
||||
.into_iter()
|
||||
.map(|model| ModelInfo {
|
||||
model,
|
||||
icon: provider.icon(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
GroupedModels::new(other, recommended)
|
||||
}
|
||||
|
||||
pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||
@@ -326,11 +308,14 @@ struct GroupedModels {
|
||||
|
||||
impl GroupedModels {
|
||||
pub fn new(other: Vec<ModelInfo>, recommended: Vec<ModelInfo>) -> Self {
|
||||
let recommended_ids: HashSet<_> = recommended.iter().map(|info| info.model.id()).collect();
|
||||
let recommended_ids = recommended
|
||||
.iter()
|
||||
.map(|info| (info.model.provider_id(), info.model.id()))
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let mut other_by_provider: IndexMap<_, Vec<ModelInfo>> = IndexMap::default();
|
||||
for model in other {
|
||||
if recommended_ids.contains(&model.model.id()) {
|
||||
if recommended_ids.contains(&(model.model.provider_id(), model.model.id())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -917,4 +902,26 @@ mod tests {
|
||||
// Recommended models should not appear in "other"
|
||||
assert_models_eq(actual_other_models, vec!["zed/gemini", "copilot/o3"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_dont_exclude_models_from_other_providers(_cx: &mut TestAppContext) {
|
||||
let recommended_models = create_models(vec![("zed", "claude")]);
|
||||
let all_models = create_models(vec![
|
||||
("zed", "claude"), // Should be filtered out from "other"
|
||||
("zed", "gemini"),
|
||||
("copilot", "claude"), // Should not be filtered out from "other"
|
||||
]);
|
||||
|
||||
let grouped_models = GroupedModels::new(all_models, recommended_models);
|
||||
|
||||
let actual_other_models = grouped_models
|
||||
.other
|
||||
.values()
|
||||
.flatten()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Recommended models should not appear in "other"
|
||||
assert_models_eq(actual_other_models, vec!["zed/gemini", "copilot/claude"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ use std::{cmp, iter, mem, ops::Range, path::PathBuf, pin::Pin, sync::Arc, task::
|
||||
use streaming_diff::{CharOperation, StreamingDiff};
|
||||
use streaming_fuzzy_matcher::StreamingFuzzyMatcher;
|
||||
use util::debug_panic;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CreateFilePromptTemplate {
|
||||
@@ -106,7 +107,9 @@ impl EditAgent {
|
||||
edit_description,
|
||||
}
|
||||
.render(&this.templates)?;
|
||||
let new_chunks = this.request(conversation, prompt, cx).await?;
|
||||
let new_chunks = this
|
||||
.request(conversation, CompletionIntent::CreateFile, prompt, cx)
|
||||
.await?;
|
||||
|
||||
let (output, mut inner_events) = this.overwrite_with_chunks(buffer, new_chunks, cx);
|
||||
while let Some(event) = inner_events.next().await {
|
||||
@@ -213,7 +216,9 @@ impl EditAgent {
|
||||
edit_description,
|
||||
}
|
||||
.render(&this.templates)?;
|
||||
let edit_chunks = this.request(conversation, prompt, cx).await?;
|
||||
let edit_chunks = this
|
||||
.request(conversation, CompletionIntent::EditFile, prompt, cx)
|
||||
.await?;
|
||||
this.apply_edit_chunks(buffer, edit_chunks, events_tx, cx)
|
||||
.await
|
||||
});
|
||||
@@ -589,6 +594,7 @@ impl EditAgent {
|
||||
async fn request(
|
||||
&self,
|
||||
mut conversation: LanguageModelRequest,
|
||||
intent: CompletionIntent,
|
||||
prompt: String,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<BoxStream<'static, Result<String, LanguageModelCompletionError>>> {
|
||||
@@ -646,6 +652,7 @@ impl EditAgent {
|
||||
let request = LanguageModelRequest {
|
||||
thread_id: conversation.thread_id,
|
||||
prompt_id: conversation.prompt_id,
|
||||
intent: Some(intent),
|
||||
mode: conversation.mode,
|
||||
messages: conversation.messages,
|
||||
tool_choice,
|
||||
|
||||
@@ -658,9 +658,13 @@ impl StdioTransport {
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
let mut process = command
|
||||
.spawn()
|
||||
.with_context(|| "failed to spawn command.")?;
|
||||
let mut process = command.spawn().with_context(|| {
|
||||
format!(
|
||||
"failed to spawn command `{} {}`.",
|
||||
binary.command,
|
||||
binary.arguments.join(" ")
|
||||
)
|
||||
})?;
|
||||
|
||||
let stdin = process.stdin.take().context("Failed to open stdin")?;
|
||||
let stdout = process.stdout.take().context("Failed to open stdout")?;
|
||||
|
||||
@@ -37,7 +37,7 @@ pub fn init(cx: &mut App) {
|
||||
registry.add_adapter(Arc::from(PhpDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(JsDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(RubyDebugAdapter));
|
||||
registry.add_adapter(Arc::from(GoDebugAdapter));
|
||||
registry.add_adapter(Arc::from(GoDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(GdbDebugAdapter));
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
||||
@@ -1,22 +1,87 @@
|
||||
use anyhow::{Context as _, anyhow, bail};
|
||||
use dap::{
|
||||
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
|
||||
adapters::DebugTaskDefinition,
|
||||
adapters::{
|
||||
DebugTaskDefinition, DownloadedFileType, download_adapter_from_github,
|
||||
latest_github_release,
|
||||
},
|
||||
};
|
||||
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
use language::LanguageName;
|
||||
use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
|
||||
use std::{collections::HashMap, env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock};
|
||||
use util;
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub(crate) struct GoDebugAdapter;
|
||||
pub(crate) struct GoDebugAdapter {
|
||||
shim_path: OnceLock<PathBuf>,
|
||||
}
|
||||
|
||||
impl GoDebugAdapter {
|
||||
const ADAPTER_NAME: &'static str = "Delve";
|
||||
const DEFAULT_TIMEOUT_MS: u64 = 60000;
|
||||
async fn fetch_latest_adapter_version(
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
) -> Result<AdapterVersion> {
|
||||
let release = latest_github_release(
|
||||
&"zed-industries/delve-shim-dap",
|
||||
true,
|
||||
false,
|
||||
delegate.http_client(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let os = match consts::OS {
|
||||
"macos" => "apple-darwin",
|
||||
"linux" => "unknown-linux-gnu",
|
||||
"windows" => "pc-windows-msvc",
|
||||
other => bail!("Running on unsupported os: {other}"),
|
||||
};
|
||||
let suffix = if consts::OS == "windows" {
|
||||
".zip"
|
||||
} else {
|
||||
".tar.gz"
|
||||
};
|
||||
let asset_name = format!("delve-shim-dap-{}-{os}{suffix}", consts::ARCH);
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == asset_name)
|
||||
.with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
|
||||
|
||||
Ok(AdapterVersion {
|
||||
tag_name: release.tag_name,
|
||||
url: asset.browser_download_url.clone(),
|
||||
})
|
||||
}
|
||||
async fn install_shim(&self, delegate: &Arc<dyn DapDelegate>) -> anyhow::Result<PathBuf> {
|
||||
if let Some(path) = self.shim_path.get().cloned() {
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
let asset = Self::fetch_latest_adapter_version(delegate).await?;
|
||||
let ty = if consts::OS == "windows" {
|
||||
DownloadedFileType::Zip
|
||||
} else {
|
||||
DownloadedFileType::GzipTar
|
||||
};
|
||||
download_adapter_from_github(
|
||||
"delve-shim-dap".into(),
|
||||
asset.clone(),
|
||||
ty,
|
||||
delegate.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let path = paths::debug_adapters_dir()
|
||||
.join("delve-shim-dap")
|
||||
.join(format!("delve-shim-dap_{}", asset.tag_name))
|
||||
.join(format!("delve-shim-dap{}", std::env::consts::EXE_SUFFIX));
|
||||
self.shim_path.set(path.clone()).ok();
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
@@ -349,13 +414,15 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
&self,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
task_definition: &DebugTaskDefinition,
|
||||
_user_installed_path: Option<PathBuf>,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
_cx: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
|
||||
let dlv_path = adapter_path.join("dlv");
|
||||
|
||||
let delve_path = if let Some(path) = delegate.which(OsStr::new("dlv")).await {
|
||||
let delve_path = if let Some(path) = user_installed_path {
|
||||
path.to_string_lossy().to_string()
|
||||
} else if let Some(path) = delegate.which(OsStr::new("dlv")).await {
|
||||
path.to_string_lossy().to_string()
|
||||
} else if delegate.fs().is_file(&dlv_path).await {
|
||||
dlv_path.to_string_lossy().to_string()
|
||||
@@ -384,16 +451,10 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
|
||||
adapter_path.join("dlv").to_string_lossy().to_string()
|
||||
};
|
||||
let minidelve_path = self.install_shim(delegate).await?;
|
||||
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
|
||||
|
||||
let mut tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
|
||||
|
||||
if tcp_connection.timeout.is_none()
|
||||
|| tcp_connection.timeout.unwrap_or(0) < Self::DEFAULT_TIMEOUT_MS
|
||||
{
|
||||
tcp_connection.timeout = Some(Self::DEFAULT_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
|
||||
let (host, port, _) = crate::configure_tcp_connection(tcp_connection).await?;
|
||||
|
||||
let cwd = task_definition
|
||||
.config
|
||||
@@ -404,6 +465,7 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
|
||||
let arguments = if cfg!(windows) {
|
||||
vec![
|
||||
delve_path,
|
||||
"dap".into(),
|
||||
"--listen".into(),
|
||||
format!("{}:{}", host, port),
|
||||
@@ -411,6 +473,7 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
delve_path,
|
||||
"dap".into(),
|
||||
"--listen".into(),
|
||||
format!("{}:{}", host, port),
|
||||
@@ -418,15 +481,11 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
};
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: delve_path,
|
||||
command: minidelve_path.to_string_lossy().into_owned(),
|
||||
arguments,
|
||||
cwd: Some(cwd),
|
||||
envs: HashMap::default(),
|
||||
connection: Some(adapters::TcpArguments {
|
||||
host,
|
||||
port,
|
||||
timeout,
|
||||
}),
|
||||
connection: None,
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
configuration: task_definition.config.clone(),
|
||||
request: self.validate_config(&task_definition.config)?,
|
||||
|
||||
@@ -149,22 +149,8 @@ impl DebugAdapter for PhpDebugAdapter {
|
||||
"default": false
|
||||
},
|
||||
"pathMappings": {
|
||||
"type": "array",
|
||||
"description": "A list of server paths mapping to the local source paths on your machine for remote host debugging",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"serverPath": {
|
||||
"type": "string",
|
||||
"description": "Path on the server"
|
||||
},
|
||||
"localPath": {
|
||||
"type": "string",
|
||||
"description": "Corresponding path on the local machine"
|
||||
}
|
||||
},
|
||||
"required": ["serverPath", "localPath"]
|
||||
}
|
||||
"type": "object",
|
||||
"description": "A mapping of server paths to local paths.",
|
||||
},
|
||||
"log": {
|
||||
"type": "boolean",
|
||||
|
||||
@@ -660,7 +660,7 @@ impl DebugAdapter for PythonDebugAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
self.get_installed_binary(delegate, &config, None, None, false)
|
||||
self.get_installed_binary(delegate, &config, None, toolchain, false)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
|
||||
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart,
|
||||
ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
|
||||
ToggleSessionPicker, ToggleThreadPicker, persistence,
|
||||
ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
@@ -65,6 +65,7 @@ pub struct DebugPanel {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||
debug_scenario_scheduled_last: bool,
|
||||
pub(crate) thread_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
pub(crate) session_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -103,6 +104,7 @@ impl DebugPanel {
|
||||
thread_picker_menu_handle,
|
||||
session_picker_menu_handle,
|
||||
_subscriptions: [focus_subscription],
|
||||
debug_scenario_scheduled_last: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -264,6 +266,7 @@ impl DebugPanel {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.debug_scenario_scheduled_last = true;
|
||||
if let Some(inventory) = self
|
||||
.project
|
||||
.read(cx)
|
||||
@@ -432,7 +435,10 @@ impl DebugPanel {
|
||||
};
|
||||
|
||||
let dap_store_handle = self.project.read(cx).dap_store().clone();
|
||||
let label = parent_session.read(cx).label().clone();
|
||||
let mut label = parent_session.read(cx).label().clone();
|
||||
if !label.ends_with("(child)") {
|
||||
label = format!("{label} (child)").into();
|
||||
}
|
||||
let adapter = parent_session.read(cx).adapter().clone();
|
||||
let mut binary = parent_session.read(cx).binary().clone();
|
||||
binary.request_args = request.clone();
|
||||
@@ -1378,4 +1384,30 @@ impl workspace::DebuggerProvider for DebuggerProvider {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn spawn_task_or_modal(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
action: &tasks_ui::Spawn,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
spawn_task_or_modal(workspace, action, window, cx);
|
||||
}
|
||||
|
||||
fn debug_scenario_scheduled(&self, cx: &mut App) {
|
||||
self.0.update(cx, |this, _| {
|
||||
this.debug_scenario_scheduled_last = true;
|
||||
});
|
||||
}
|
||||
|
||||
fn task_scheduled(&self, cx: &mut App) {
|
||||
self.0.update(cx, |this, _| {
|
||||
this.debug_scenario_scheduled_last = false;
|
||||
})
|
||||
}
|
||||
|
||||
fn debug_scenario_scheduled_last(&self, cx: &App) -> bool {
|
||||
self.0.read(cx).debug_scenario_scheduled_last
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@ use debugger_panel::{DebugPanel, ToggleFocus};
|
||||
use editor::Editor;
|
||||
use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt};
|
||||
use gpui::{App, EntityInputHandler, actions};
|
||||
use new_session_modal::NewSessionModal;
|
||||
use new_session_modal::{NewSessionModal, NewSessionMode};
|
||||
use project::debugger::{self, breakpoint_store::SourceBreakpoint};
|
||||
use session::DebugSession;
|
||||
use settings::Settings;
|
||||
use stack_trace_view::StackTraceView;
|
||||
use tasks_ui::{Spawn, TaskOverrides};
|
||||
use util::maybe;
|
||||
use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
|
||||
|
||||
@@ -62,6 +63,7 @@ pub fn init(cx: &mut App) {
|
||||
|
||||
cx.when_flag_enabled::<DebuggerFeatureFlag>(window, |workspace, _, _| {
|
||||
workspace
|
||||
.register_action(spawn_task_or_modal)
|
||||
.register_action(|workspace, _: &ToggleFocus, window, cx| {
|
||||
workspace.toggle_panel_focus::<DebugPanel>(window, cx);
|
||||
})
|
||||
@@ -208,7 +210,7 @@ pub fn init(cx: &mut App) {
|
||||
},
|
||||
)
|
||||
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
|
||||
NewSessionModal::show(workspace, window, cx);
|
||||
NewSessionModal::show(workspace, window, NewSessionMode::Launch, None, cx);
|
||||
})
|
||||
.register_action(
|
||||
|workspace: &mut Workspace, _: &RerunLastSession, window, cx| {
|
||||
@@ -309,3 +311,48 @@ pub fn init(cx: &mut App) {
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn spawn_task_or_modal(
|
||||
workspace: &mut Workspace,
|
||||
action: &Spawn,
|
||||
window: &mut ui::Window,
|
||||
cx: &mut ui::Context<Workspace>,
|
||||
) {
|
||||
match action {
|
||||
Spawn::ByName {
|
||||
task_name,
|
||||
reveal_target,
|
||||
} => {
|
||||
let overrides = reveal_target.map(|reveal_target| TaskOverrides {
|
||||
reveal_target: Some(reveal_target),
|
||||
});
|
||||
let name = task_name.clone();
|
||||
tasks_ui::spawn_tasks_filtered(
|
||||
move |(_, task)| task.label.eq(&name),
|
||||
overrides,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
Spawn::ByTag {
|
||||
task_tag,
|
||||
reveal_target,
|
||||
} => {
|
||||
let overrides = reveal_target.map(|reveal_target| TaskOverrides {
|
||||
reveal_target: Some(reveal_target),
|
||||
});
|
||||
let tag = task_tag.clone();
|
||||
tasks_ui::spawn_tasks_filtered(
|
||||
move |(_, task)| task.tags.contains(&tag),
|
||||
overrides,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
Spawn::ViaModal { reveal_target } => {
|
||||
NewSessionModal::show(workspace, window, NewSessionMode::Task, *reveal_target, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use gpui::Entity;
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{Animation, AnimationExt as _, Entity, Transformation, percentage};
|
||||
use project::debugger::session::{ThreadId, ThreadStatus};
|
||||
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
|
||||
|
||||
@@ -23,31 +25,40 @@ impl DebugPanel {
|
||||
let sessions = self.sessions().clone();
|
||||
let weak = cx.weak_entity();
|
||||
let running_state = running_state.read(cx);
|
||||
let label = if let Some(active_session) = active_session {
|
||||
let label = if let Some(active_session) = active_session.clone() {
|
||||
active_session.read(cx).session(cx).read(cx).label()
|
||||
} else {
|
||||
SharedString::new_static("Unknown Session")
|
||||
};
|
||||
|
||||
let is_terminated = running_state.session().read(cx).is_terminated();
|
||||
let session_state_indicator = {
|
||||
if is_terminated {
|
||||
Some(Indicator::dot().color(Color::Error))
|
||||
} else {
|
||||
match running_state.thread_status(cx).unwrap_or_default() {
|
||||
project::debugger::session::ThreadStatus::Stopped => {
|
||||
Some(Indicator::dot().color(Color::Conflict))
|
||||
}
|
||||
_ => Some(Indicator::dot().color(Color::Success)),
|
||||
let is_started = active_session
|
||||
.is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
|
||||
|
||||
let session_state_indicator = if is_terminated {
|
||||
Indicator::dot().color(Color::Error).into_any_element()
|
||||
} else if !is_started {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
match running_state.thread_status(cx).unwrap_or_default() {
|
||||
ThreadStatus::Stopped => {
|
||||
Indicator::dot().color(Color::Conflict).into_any_element()
|
||||
}
|
||||
_ => Indicator::dot().color(Color::Success).into_any_element(),
|
||||
}
|
||||
};
|
||||
|
||||
let trigger = h_flex()
|
||||
.gap_2()
|
||||
.when_some(session_state_indicator, |this, indicator| {
|
||||
this.child(indicator)
|
||||
})
|
||||
.child(session_state_indicator)
|
||||
.justify_between()
|
||||
.child(
|
||||
DebugPanel::dropdown_label(label)
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::{
|
||||
time::Duration,
|
||||
usize,
|
||||
};
|
||||
use tasks_ui::{TaskOverrides, TasksModal};
|
||||
|
||||
use dap::{
|
||||
DapRegistry, DebugRequest, TelemetrySpawnLocation, adapters::DebugAdapterName, send_telemetry,
|
||||
@@ -16,12 +17,12 @@ use editor::{Anchor, Editor, EditorElement, EditorStyle, scroll::Autoscroll};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, Render, Subscription, TextStyle, Transformation, WeakEntity, percentage,
|
||||
Focusable, KeyContext, Render, Subscription, TextStyle, Transformation, WeakEntity, percentage,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
|
||||
use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore};
|
||||
use settings::Settings;
|
||||
use task::{DebugScenario, LaunchRequest, ZedDebugConfig};
|
||||
use task::{DebugScenario, LaunchRequest, RevealTarget, ZedDebugConfig};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
|
||||
@@ -47,10 +48,11 @@ pub(super) struct NewSessionModal {
|
||||
mode: NewSessionMode,
|
||||
launch_picker: Entity<Picker<DebugScenarioDelegate>>,
|
||||
attach_mode: Entity<AttachMode>,
|
||||
custom_mode: Entity<CustomMode>,
|
||||
configure_mode: Entity<ConfigureMode>,
|
||||
task_mode: TaskMode,
|
||||
debugger: Option<DebugAdapterName>,
|
||||
save_scenario_state: Option<SaveScenarioState>,
|
||||
_subscriptions: [Subscription; 2],
|
||||
_subscriptions: [Subscription; 3],
|
||||
}
|
||||
|
||||
fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString {
|
||||
@@ -75,6 +77,8 @@ impl NewSessionModal {
|
||||
pub(super) fn show(
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
mode: NewSessionMode,
|
||||
reveal_target: Option<RevealTarget>,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
||||
@@ -84,20 +88,50 @@ impl NewSessionModal {
|
||||
let languages = workspace.app_state().languages.clone();
|
||||
|
||||
cx.spawn_in(window, async move |workspace, cx| {
|
||||
let task_contexts = workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
tasks_ui::task_contexts(workspace, window, cx)
|
||||
})?
|
||||
.await;
|
||||
let task_contexts = Arc::new(task_contexts);
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
let attach_mode = AttachMode::new(None, workspace_handle.clone(), window, cx);
|
||||
|
||||
let launch_picker = cx.new(|cx| {
|
||||
Picker::uniform_list(
|
||||
DebugScenarioDelegate::new(debug_panel.downgrade(), task_store),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.modal(false)
|
||||
let mut delegate =
|
||||
DebugScenarioDelegate::new(debug_panel.downgrade(), task_store.clone());
|
||||
delegate.task_contexts_loaded(task_contexts.clone(), languages, window, cx);
|
||||
Picker::uniform_list(delegate, window, cx).modal(false)
|
||||
});
|
||||
|
||||
let configure_mode = ConfigureMode::new(None, window, cx);
|
||||
if let Some(active_cwd) = task_contexts
|
||||
.active_context()
|
||||
.and_then(|context| context.cwd.clone())
|
||||
{
|
||||
configure_mode.update(cx, |configure_mode, cx| {
|
||||
configure_mode.load(active_cwd, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
let task_overrides = Some(TaskOverrides { reveal_target });
|
||||
|
||||
let task_mode = TaskMode {
|
||||
task_modal: cx.new(|cx| {
|
||||
TasksModal::new(
|
||||
task_store.clone(),
|
||||
task_contexts,
|
||||
task_overrides,
|
||||
false,
|
||||
workspace_handle.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
};
|
||||
|
||||
let _subscriptions = [
|
||||
cx.subscribe(&launch_picker, |_, _, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
@@ -108,52 +142,18 @@ impl NewSessionModal {
|
||||
cx.emit(DismissEvent);
|
||||
},
|
||||
),
|
||||
cx.subscribe(&task_mode.task_modal, |_, _, _: &DismissEvent, cx| {
|
||||
cx.emit(DismissEvent)
|
||||
}),
|
||||
];
|
||||
|
||||
let custom_mode = CustomMode::new(None, window, cx);
|
||||
|
||||
cx.spawn_in(window, {
|
||||
let workspace_handle = workspace_handle.clone();
|
||||
async move |this, cx| {
|
||||
let task_contexts = workspace_handle
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
tasks_ui::task_contexts(workspace, window, cx)
|
||||
})?
|
||||
.await;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
if let Some(active_cwd) = task_contexts
|
||||
.active_context()
|
||||
.and_then(|context| context.cwd.clone())
|
||||
{
|
||||
this.custom_mode.update(cx, |custom, cx| {
|
||||
custom.load(active_cwd, window, cx);
|
||||
});
|
||||
|
||||
this.debugger = None;
|
||||
}
|
||||
|
||||
this.launch_picker.update(cx, |picker, cx| {
|
||||
picker.delegate.task_contexts_loaded(
|
||||
task_contexts,
|
||||
languages,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
picker.refresh(window, cx);
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
launch_picker,
|
||||
attach_mode,
|
||||
custom_mode,
|
||||
configure_mode,
|
||||
task_mode,
|
||||
debugger: None,
|
||||
mode: NewSessionMode::Launch,
|
||||
mode,
|
||||
debug_panel: debug_panel.downgrade(),
|
||||
workspace: workspace_handle,
|
||||
save_scenario_state: None,
|
||||
@@ -170,10 +170,17 @@ impl NewSessionModal {
|
||||
fn render_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
||||
let dap_menu = self.adapter_drop_down_menu(window, cx);
|
||||
match self.mode {
|
||||
NewSessionMode::Task => self
|
||||
.task_mode
|
||||
.task_modal
|
||||
.read(cx)
|
||||
.picker
|
||||
.clone()
|
||||
.into_any_element(),
|
||||
NewSessionMode::Attach => self.attach_mode.update(cx, |this, cx| {
|
||||
this.clone().render(window, cx).into_any_element()
|
||||
}),
|
||||
NewSessionMode::Custom => self.custom_mode.update(cx, |this, cx| {
|
||||
NewSessionMode::Configure => self.configure_mode.update(cx, |this, cx| {
|
||||
this.clone().render(dap_menu, window, cx).into_any_element()
|
||||
}),
|
||||
NewSessionMode::Launch => v_flex()
|
||||
@@ -185,16 +192,17 @@ impl NewSessionModal {
|
||||
|
||||
fn mode_focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
match self.mode {
|
||||
NewSessionMode::Task => self.task_mode.task_modal.focus_handle(cx),
|
||||
NewSessionMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx),
|
||||
NewSessionMode::Custom => self.custom_mode.read(cx).program.focus_handle(cx),
|
||||
NewSessionMode::Configure => self.configure_mode.read(cx).program.focus_handle(cx),
|
||||
NewSessionMode::Launch => self.launch_picker.focus_handle(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> {
|
||||
let request = match self.mode {
|
||||
NewSessionMode::Custom => Some(DebugRequest::Launch(
|
||||
self.custom_mode.read(cx).debug_request(cx),
|
||||
NewSessionMode::Configure => Some(DebugRequest::Launch(
|
||||
self.configure_mode.read(cx).debug_request(cx),
|
||||
)),
|
||||
NewSessionMode::Attach => Some(DebugRequest::Attach(
|
||||
self.attach_mode.read(cx).debug_request(),
|
||||
@@ -203,8 +211,8 @@ impl NewSessionModal {
|
||||
}?;
|
||||
let label = suggested_label(&request, debugger);
|
||||
|
||||
let stop_on_entry = if let NewSessionMode::Custom = &self.mode {
|
||||
Some(self.custom_mode.read(cx).stop_on_entry.selected())
|
||||
let stop_on_entry = if let NewSessionMode::Configure = &self.mode {
|
||||
Some(self.configure_mode.read(cx).stop_on_entry.selected())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -527,7 +535,8 @@ static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select De
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum NewSessionMode {
|
||||
Custom,
|
||||
Task,
|
||||
Configure,
|
||||
Attach,
|
||||
Launch,
|
||||
}
|
||||
@@ -535,9 +544,10 @@ pub(crate) enum NewSessionMode {
|
||||
impl std::fmt::Display for NewSessionMode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mode = match self {
|
||||
NewSessionMode::Launch => "Launch".to_owned(),
|
||||
NewSessionMode::Attach => "Attach".to_owned(),
|
||||
NewSessionMode::Custom => "Custom".to_owned(),
|
||||
NewSessionMode::Task => "Run",
|
||||
NewSessionMode::Launch => "Debug",
|
||||
NewSessionMode::Attach => "Attach",
|
||||
NewSessionMode::Configure => "Configure Debugger",
|
||||
};
|
||||
|
||||
write!(f, "{}", mode)
|
||||
@@ -597,36 +607,39 @@ impl Render for NewSessionModal {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.w(rems(34.))
|
||||
.key_context("Pane")
|
||||
.key_context({
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("Pane");
|
||||
key_context.add("RunModal");
|
||||
key_context
|
||||
})
|
||||
.elevation_3(cx)
|
||||
.bg(cx.theme().colors().elevated_surface_background)
|
||||
.on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &pane::ActivateNextItem, window, cx| {
|
||||
this.mode = match this.mode {
|
||||
NewSessionMode::Task => NewSessionMode::Launch,
|
||||
NewSessionMode::Launch => NewSessionMode::Attach,
|
||||
NewSessionMode::Attach => NewSessionMode::Configure,
|
||||
NewSessionMode::Configure => NewSessionMode::Task,
|
||||
};
|
||||
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
}))
|
||||
.on_action(
|
||||
cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| {
|
||||
this.mode = match this.mode {
|
||||
NewSessionMode::Task => NewSessionMode::Configure,
|
||||
NewSessionMode::Launch => NewSessionMode::Task,
|
||||
NewSessionMode::Attach => NewSessionMode::Launch,
|
||||
NewSessionMode::Launch => NewSessionMode::Attach,
|
||||
_ => {
|
||||
return;
|
||||
}
|
||||
NewSessionMode::Configure => NewSessionMode::Attach,
|
||||
};
|
||||
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
}),
|
||||
)
|
||||
.on_action(cx.listener(|this, _: &pane::ActivateNextItem, window, cx| {
|
||||
this.mode = match this.mode {
|
||||
NewSessionMode::Attach => NewSessionMode::Launch,
|
||||
NewSessionMode::Launch => NewSessionMode::Attach,
|
||||
_ => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
@@ -637,37 +650,73 @@ impl Render for NewSessionModal {
|
||||
.justify_start()
|
||||
.w_full()
|
||||
.child(
|
||||
ToggleButton::new("debugger-session-ui-picker-button", "Launch")
|
||||
.size(ButtonSize::Default)
|
||||
.style(ui::ButtonStyle::Subtle)
|
||||
.toggle_state(matches!(self.mode, NewSessionMode::Launch))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.mode = NewSessionMode::Launch;
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}))
|
||||
.first(),
|
||||
ToggleButton::new(
|
||||
"debugger-session-ui-tasks-button",
|
||||
NewSessionMode::Task.to_string(),
|
||||
)
|
||||
.size(ButtonSize::Default)
|
||||
.toggle_state(matches!(self.mode, NewSessionMode::Task))
|
||||
.style(ui::ButtonStyle::Subtle)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.mode = NewSessionMode::Task;
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}))
|
||||
.first(),
|
||||
)
|
||||
.child(
|
||||
ToggleButton::new("debugger-session-ui-attach-button", "Attach")
|
||||
.size(ButtonSize::Default)
|
||||
.toggle_state(matches!(self.mode, NewSessionMode::Attach))
|
||||
.style(ui::ButtonStyle::Subtle)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.mode = NewSessionMode::Attach;
|
||||
ToggleButton::new(
|
||||
"debugger-session-ui-launch-button",
|
||||
NewSessionMode::Launch.to_string(),
|
||||
)
|
||||
.size(ButtonSize::Default)
|
||||
.style(ui::ButtonStyle::Subtle)
|
||||
.toggle_state(matches!(self.mode, NewSessionMode::Launch))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.mode = NewSessionMode::Launch;
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}))
|
||||
.middle(),
|
||||
)
|
||||
.child(
|
||||
ToggleButton::new(
|
||||
"debugger-session-ui-attach-button",
|
||||
NewSessionMode::Attach.to_string(),
|
||||
)
|
||||
.size(ButtonSize::Default)
|
||||
.toggle_state(matches!(self.mode, NewSessionMode::Attach))
|
||||
.style(ui::ButtonStyle::Subtle)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.mode = NewSessionMode::Attach;
|
||||
|
||||
if let Some(debugger) = this.debugger.as_ref() {
|
||||
Self::update_attach_picker(
|
||||
&this.attach_mode,
|
||||
&debugger,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}))
|
||||
.last(),
|
||||
if let Some(debugger) = this.debugger.as_ref() {
|
||||
Self::update_attach_picker(
|
||||
&this.attach_mode,
|
||||
&debugger,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}))
|
||||
.middle(),
|
||||
)
|
||||
.child(
|
||||
ToggleButton::new(
|
||||
"debugger-session-ui-custom-button",
|
||||
NewSessionMode::Configure.to_string(),
|
||||
)
|
||||
.size(ButtonSize::Default)
|
||||
.toggle_state(matches!(self.mode, NewSessionMode::Configure))
|
||||
.style(ui::ButtonStyle::Subtle)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.mode = NewSessionMode::Configure;
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}))
|
||||
.last(),
|
||||
),
|
||||
)
|
||||
.justify_between()
|
||||
@@ -675,83 +724,83 @@ impl Render for NewSessionModal {
|
||||
.border_b_1(),
|
||||
)
|
||||
.child(v_flex().child(self.render_mode(window, cx)))
|
||||
.child(
|
||||
h_flex()
|
||||
.map(|el| {
|
||||
let container = h_flex()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.border_t_1()
|
||||
.w_full()
|
||||
.child(match self.mode {
|
||||
NewSessionMode::Attach => {
|
||||
div().child(self.adapter_drop_down_menu(window, cx))
|
||||
}
|
||||
NewSessionMode::Launch => div().child(
|
||||
Button::new("new-session-modal-custom", "Custom").on_click({
|
||||
let this = cx.weak_entity();
|
||||
move |_, window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.mode = NewSessionMode::Custom;
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
),
|
||||
NewSessionMode::Custom => h_flex()
|
||||
.w_full();
|
||||
match self.mode {
|
||||
NewSessionMode::Configure => el.child(
|
||||
container
|
||||
.child(
|
||||
Button::new("new-session-modal-back", "Save to .zed/debug.json...")
|
||||
h_flex()
|
||||
.child(
|
||||
Button::new(
|
||||
"new-session-modal-back",
|
||||
"Save to .zed/debug.json...",
|
||||
)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.save_debug_scenario(window, cx);
|
||||
}))
|
||||
.disabled(
|
||||
self.debugger.is_none()
|
||||
|| self
|
||||
.configure_mode
|
||||
.read(cx)
|
||||
.program
|
||||
.read(cx)
|
||||
.is_empty(cx)
|
||||
|| self.save_scenario_state.is_some(),
|
||||
),
|
||||
)
|
||||
.child(self.render_save_state(cx)),
|
||||
)
|
||||
.child(
|
||||
Button::new("debugger-spawn", "Start")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.save_debug_scenario(window, cx);
|
||||
this.start_new_session(window, cx)
|
||||
}))
|
||||
.disabled(
|
||||
self.debugger.is_none()
|
||||
|| self
|
||||
.custom_mode
|
||||
.configure_mode
|
||||
.read(cx)
|
||||
.program
|
||||
.read(cx)
|
||||
.is_empty(cx)
|
||||
|| self.save_scenario_state.is_some(),
|
||||
.is_empty(cx),
|
||||
),
|
||||
)
|
||||
.child(self.render_save_state(cx)),
|
||||
})
|
||||
.child(
|
||||
Button::new("debugger-spawn", "Start")
|
||||
.on_click(cx.listener(|this, _, window, cx| match &this.mode {
|
||||
NewSessionMode::Launch => {
|
||||
this.launch_picker.update(cx, |picker, cx| {
|
||||
picker.delegate.confirm(true, window, cx)
|
||||
})
|
||||
}
|
||||
_ => this.start_new_session(window, cx),
|
||||
}))
|
||||
.disabled(match self.mode {
|
||||
NewSessionMode::Launch => {
|
||||
!self.launch_picker.read(cx).delegate.matches.is_empty()
|
||||
}
|
||||
NewSessionMode::Attach => {
|
||||
self.debugger.is_none()
|
||||
|| self
|
||||
.attach_mode
|
||||
.read(cx)
|
||||
.attach_picker
|
||||
.read(cx)
|
||||
.picker
|
||||
.read(cx)
|
||||
.delegate
|
||||
.match_count()
|
||||
== 0
|
||||
}
|
||||
NewSessionMode::Custom => {
|
||||
self.debugger.is_none()
|
||||
|| self.custom_mode.read(cx).program.read(cx).is_empty(cx)
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
NewSessionMode::Attach => el.child(
|
||||
container
|
||||
.child(div().child(self.adapter_drop_down_menu(window, cx)))
|
||||
.child(
|
||||
Button::new("debugger-spawn", "Start")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.start_new_session(window, cx)
|
||||
}))
|
||||
.disabled(
|
||||
self.debugger.is_none()
|
||||
|| self
|
||||
.attach_mode
|
||||
.read(cx)
|
||||
.attach_picker
|
||||
.read(cx)
|
||||
.picker
|
||||
.read(cx)
|
||||
.delegate
|
||||
.match_count()
|
||||
== 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
NewSessionMode::Launch => el,
|
||||
NewSessionMode::Task => el,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -774,13 +823,13 @@ impl RenderOnce for AttachMode {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct CustomMode {
|
||||
pub(super) struct ConfigureMode {
|
||||
program: Entity<Editor>,
|
||||
cwd: Entity<Editor>,
|
||||
stop_on_entry: ToggleState,
|
||||
}
|
||||
|
||||
impl CustomMode {
|
||||
impl ConfigureMode {
|
||||
pub(super) fn new(
|
||||
past_launch_config: Option<LaunchRequest>,
|
||||
window: &mut Window,
|
||||
@@ -940,6 +989,11 @@ impl AttachMode {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct TaskMode {
|
||||
pub(super) task_modal: Entity<TasksModal>,
|
||||
}
|
||||
|
||||
pub(super) struct DebugScenarioDelegate {
|
||||
task_store: Entity<TaskStore>,
|
||||
candidates: Vec<(Option<TaskSourceKind>, DebugScenario)>,
|
||||
@@ -995,12 +1049,12 @@ impl DebugScenarioDelegate {
|
||||
|
||||
pub fn task_contexts_loaded(
|
||||
&mut self,
|
||||
task_contexts: TaskContexts,
|
||||
task_contexts: Arc<TaskContexts>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
self.task_contexts = Some(Arc::new(task_contexts));
|
||||
self.task_contexts = Some(task_contexts);
|
||||
|
||||
let (recent, scenarios) = self
|
||||
.task_store
|
||||
@@ -1206,7 +1260,7 @@ pub(crate) fn resolve_path(path: &mut String) {
|
||||
|
||||
#[cfg(test)]
|
||||
impl NewSessionModal {
|
||||
pub(crate) fn set_custom(
|
||||
pub(crate) fn set_configure(
|
||||
&mut self,
|
||||
program: impl AsRef<str>,
|
||||
cwd: impl AsRef<str>,
|
||||
@@ -1214,21 +1268,21 @@ impl NewSessionModal {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.mode = NewSessionMode::Custom;
|
||||
self.mode = NewSessionMode::Configure;
|
||||
self.debugger = Some(dap::adapters::DebugAdapterName("fake-adapter".into()));
|
||||
|
||||
self.custom_mode.update(cx, |custom, cx| {
|
||||
custom.program.update(cx, |editor, cx| {
|
||||
self.configure_mode.update(cx, |configure, cx| {
|
||||
configure.program.update(cx, |editor, cx| {
|
||||
editor.clear(window, cx);
|
||||
editor.set_text(program.as_ref(), window, cx);
|
||||
});
|
||||
|
||||
custom.cwd.update(cx, |editor, cx| {
|
||||
configure.cwd.update(cx, |editor, cx| {
|
||||
editor.clear(window, cx);
|
||||
editor.set_text(cwd.as_ref(), window, cx);
|
||||
});
|
||||
|
||||
custom.stop_on_entry = match stop_on_entry {
|
||||
configure.stop_on_entry = match stop_on_entry {
|
||||
true => ToggleState::Selected,
|
||||
_ => ToggleState::Unselected,
|
||||
}
|
||||
@@ -1239,28 +1293,3 @@ impl NewSessionModal {
|
||||
self.save_debug_scenario(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use paths::home_dir;
|
||||
|
||||
#[test]
|
||||
fn test_normalize_paths() {
|
||||
let sep = std::path::MAIN_SEPARATOR;
|
||||
let home = home_dir().to_string_lossy().to_string();
|
||||
let resolve_path = |path: &str| -> String {
|
||||
let mut path = path.to_string();
|
||||
super::resolve_path(&mut path);
|
||||
path
|
||||
};
|
||||
|
||||
assert_eq!(resolve_path("bin"), format!("bin"));
|
||||
assert_eq!(resolve_path(&format!("{sep}foo")), format!("{sep}foo"));
|
||||
assert_eq!(resolve_path(""), format!(""));
|
||||
assert_eq!(
|
||||
resolve_path(&format!("~{sep}blah")),
|
||||
format!("{home}{sep}blah")
|
||||
);
|
||||
assert_eq!(resolve_path("~"), home);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -874,7 +874,6 @@ impl RunningState {
|
||||
args,
|
||||
..task.resolved.clone()
|
||||
};
|
||||
|
||||
let terminal = project
|
||||
.update_in(cx, |project, window, cx| {
|
||||
project.create_terminal(
|
||||
@@ -919,6 +918,12 @@ 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")?;
|
||||
@@ -937,7 +942,7 @@ impl RunningState {
|
||||
|
||||
let scenario = dap_registry
|
||||
.adapter(&adapter)
|
||||
.context(format!("{}: is not a valid adapter name", &adapter))
|
||||
.ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))
|
||||
.map(|adapter| adapter.config_from_zed_format(zed_config))??;
|
||||
config = scenario.config;
|
||||
Self::substitute_variables_in_config(&mut config, &task_context);
|
||||
|
||||
@@ -110,7 +110,7 @@ impl Console {
|
||||
}
|
||||
|
||||
fn is_running(&self, cx: &Context<Self>) -> bool {
|
||||
self.session.read(cx).is_local()
|
||||
self.session.read(cx).is_running()
|
||||
}
|
||||
|
||||
fn handle_stack_frame_list_events(
|
||||
|
||||
@@ -250,9 +250,6 @@ impl StackFrameList {
|
||||
let Some(abs_path) = Self::abs_path_from_stack_frame(&stack_frame) else {
|
||||
return Task::ready(Err(anyhow!("Project path not found")));
|
||||
};
|
||||
if abs_path.starts_with("<node_internals>") {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
let row = stack_frame.line.saturating_sub(1) as u32;
|
||||
cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
|
||||
stack_frame_id,
|
||||
@@ -345,6 +342,7 @@ impl StackFrameList {
|
||||
s.path
|
||||
.as_deref()
|
||||
.map(|path| Arc::<Path>::from(Path::new(path)))
|
||||
.filter(|path| path.is_absolute())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use task::{DebugRequest, DebugScenario, LaunchRequest, TaskContext, VariableName, ZedDebugConfig};
|
||||
use util::path;
|
||||
|
||||
use crate::new_session_modal::NewSessionMode;
|
||||
use crate::tests::{init_test, init_test_workspace};
|
||||
|
||||
#[gpui::test]
|
||||
@@ -170,7 +171,13 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, window, cx| {
|
||||
crate::new_session_modal::NewSessionModal::show(workspace, window, cx);
|
||||
crate::new_session_modal::NewSessionModal::show(
|
||||
workspace,
|
||||
window,
|
||||
NewSessionMode::Launch,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -184,7 +191,7 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut
|
||||
.expect("Modal should be active");
|
||||
|
||||
modal.update_in(cx, |modal, window, cx| {
|
||||
modal.set_custom("/project/main", "/project", false, window, cx);
|
||||
modal.set_configure("/project/main", "/project", false, window, cx);
|
||||
modal.save_scenario(window, cx);
|
||||
});
|
||||
|
||||
@@ -213,7 +220,7 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut
|
||||
pretty_assertions::assert_eq!(expected_content, actual_lines);
|
||||
|
||||
modal.update_in(cx, |modal, window, cx| {
|
||||
modal.set_custom("/project/other", "/project", true, window, cx);
|
||||
modal.set_configure("/project/other", "/project", true, window, cx);
|
||||
modal.save_scenario(window, cx);
|
||||
});
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ tree-sitter-rust = { workspace = true, optional = true }
|
||||
tree-sitter-typescript = { workspace = true, optional = true }
|
||||
tree-sitter-python = { workspace = true, optional = true }
|
||||
unicode-segmentation.workspace = true
|
||||
unicode-script.workspace = true
|
||||
unindent = { workspace = true, optional = true }
|
||||
ui.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
@@ -201,7 +201,7 @@ use ui::{
|
||||
ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName,
|
||||
IconSize, Indicator, Key, Tooltip, h_flex, prelude::*,
|
||||
};
|
||||
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc, wrap_with_prefix};
|
||||
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc};
|
||||
use workspace::{
|
||||
CollaboratorId, Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal,
|
||||
RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast,
|
||||
@@ -19587,6 +19587,347 @@ fn update_uncommitted_diff_for_buffer(
|
||||
})
|
||||
}
|
||||
|
||||
fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize {
|
||||
let tab_size = tab_size.get() as usize;
|
||||
let mut width = offset;
|
||||
|
||||
for ch in text.chars() {
|
||||
width += if ch == '\t' {
|
||||
tab_size - (width % tab_size)
|
||||
} else {
|
||||
1
|
||||
};
|
||||
}
|
||||
|
||||
width - offset
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_string_size_with_expanded_tabs() {
|
||||
let nz = |val| NonZeroU32::new(val).unwrap();
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "", nz(4)), 0);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "hello", nz(4)), 5);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "\thello", nz(4)), 9);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "abc\tab", nz(4)), 6);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "hello\t", nz(4)), 8);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "\t\t", nz(8)), 16);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "x\t", nz(8)), 8);
|
||||
assert_eq!(char_len_with_expanded_tabs(7, "x\t", nz(8)), 9);
|
||||
}
|
||||
}
|
||||
|
||||
/// Tokenizes a string into runs of text that should stick together, or that is whitespace.
|
||||
struct WordBreakingTokenizer<'a> {
|
||||
input: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> WordBreakingTokenizer<'a> {
|
||||
fn new(input: &'a str) -> Self {
|
||||
Self { input }
|
||||
}
|
||||
}
|
||||
|
||||
fn is_char_ideographic(ch: char) -> bool {
|
||||
use unicode_script::Script::*;
|
||||
use unicode_script::UnicodeScript;
|
||||
matches!(ch.script(), Han | Tangut | Yi)
|
||||
}
|
||||
|
||||
fn is_grapheme_ideographic(text: &str) -> bool {
|
||||
text.chars().any(is_char_ideographic)
|
||||
}
|
||||
|
||||
fn is_grapheme_whitespace(text: &str) -> bool {
|
||||
text.chars().any(|x| x.is_whitespace())
|
||||
}
|
||||
|
||||
fn should_stay_with_preceding_ideograph(text: &str) -> bool {
|
||||
text.chars().next().map_or(false, |ch| {
|
||||
matches!(ch, '。' | '、' | ',' | '?' | '!' | ':' | ';' | '…')
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
|
||||
enum WordBreakToken<'a> {
|
||||
Word { token: &'a str, grapheme_len: usize },
|
||||
InlineWhitespace { token: &'a str, grapheme_len: usize },
|
||||
Newline,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for WordBreakingTokenizer<'a> {
|
||||
/// Yields a span, the count of graphemes in the token, and whether it was
|
||||
/// whitespace. Note that it also breaks at word boundaries.
|
||||
type Item = WordBreakToken<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
if self.input.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut iter = self.input.graphemes(true).peekable();
|
||||
let mut offset = 0;
|
||||
let mut grapheme_len = 0;
|
||||
if let Some(first_grapheme) = iter.next() {
|
||||
let is_newline = first_grapheme == "\n";
|
||||
let is_whitespace = is_grapheme_whitespace(first_grapheme);
|
||||
offset += first_grapheme.len();
|
||||
grapheme_len += 1;
|
||||
if is_grapheme_ideographic(first_grapheme) && !is_whitespace {
|
||||
if let Some(grapheme) = iter.peek().copied() {
|
||||
if should_stay_with_preceding_ideograph(grapheme) {
|
||||
offset += grapheme.len();
|
||||
grapheme_len += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut words = self.input[offset..].split_word_bound_indices().peekable();
|
||||
let mut next_word_bound = words.peek().copied();
|
||||
if next_word_bound.map_or(false, |(i, _)| i == 0) {
|
||||
next_word_bound = words.next();
|
||||
}
|
||||
while let Some(grapheme) = iter.peek().copied() {
|
||||
if next_word_bound.map_or(false, |(i, _)| i == offset) {
|
||||
break;
|
||||
};
|
||||
if is_grapheme_whitespace(grapheme) != is_whitespace
|
||||
|| (grapheme == "\n") != is_newline
|
||||
{
|
||||
break;
|
||||
};
|
||||
offset += grapheme.len();
|
||||
grapheme_len += 1;
|
||||
iter.next();
|
||||
}
|
||||
}
|
||||
let token = &self.input[..offset];
|
||||
self.input = &self.input[offset..];
|
||||
if token == "\n" {
|
||||
Some(WordBreakToken::Newline)
|
||||
} else if is_whitespace {
|
||||
Some(WordBreakToken::InlineWhitespace {
|
||||
token,
|
||||
grapheme_len,
|
||||
})
|
||||
} else {
|
||||
Some(WordBreakToken::Word {
|
||||
token,
|
||||
grapheme_len,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_word_breaking_tokenizer() {
|
||||
let tests: &[(&str, &[WordBreakToken<'static>])] = &[
|
||||
("", &[]),
|
||||
(" ", &[whitespace(" ", 2)]),
|
||||
("Ʒ", &[word("Ʒ", 1)]),
|
||||
("Ǽ", &[word("Ǽ", 1)]),
|
||||
("⋑", &[word("⋑", 1)]),
|
||||
("⋑⋑", &[word("⋑⋑", 2)]),
|
||||
(
|
||||
"原理,进而",
|
||||
&[word("原", 1), word("理,", 2), word("进", 1), word("而", 1)],
|
||||
),
|
||||
(
|
||||
"hello world",
|
||||
&[word("hello", 5), whitespace(" ", 1), word("world", 5)],
|
||||
),
|
||||
(
|
||||
"hello, world",
|
||||
&[word("hello,", 6), whitespace(" ", 1), word("world", 5)],
|
||||
),
|
||||
(
|
||||
" hello world",
|
||||
&[
|
||||
whitespace(" ", 2),
|
||||
word("hello", 5),
|
||||
whitespace(" ", 1),
|
||||
word("world", 5),
|
||||
],
|
||||
),
|
||||
(
|
||||
"这是什么 \n 钢笔",
|
||||
&[
|
||||
word("这", 1),
|
||||
word("是", 1),
|
||||
word("什", 1),
|
||||
word("么", 1),
|
||||
whitespace(" ", 1),
|
||||
newline(),
|
||||
whitespace(" ", 1),
|
||||
word("钢", 1),
|
||||
word("笔", 1),
|
||||
],
|
||||
),
|
||||
(" mutton", &[whitespace(" ", 1), word("mutton", 6)]),
|
||||
];
|
||||
|
||||
fn word(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> {
|
||||
WordBreakToken::Word {
|
||||
token,
|
||||
grapheme_len,
|
||||
}
|
||||
}
|
||||
|
||||
fn whitespace(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> {
|
||||
WordBreakToken::InlineWhitespace {
|
||||
token,
|
||||
grapheme_len,
|
||||
}
|
||||
}
|
||||
|
||||
fn newline() -> WordBreakToken<'static> {
|
||||
WordBreakToken::Newline
|
||||
}
|
||||
|
||||
for (input, result) in tests {
|
||||
assert_eq!(
|
||||
WordBreakingTokenizer::new(input)
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice(),
|
||||
*result,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap_with_prefix(
|
||||
line_prefix: String,
|
||||
unwrapped_text: String,
|
||||
wrap_column: usize,
|
||||
tab_size: NonZeroU32,
|
||||
preserve_existing_whitespace: bool,
|
||||
) -> String {
|
||||
let line_prefix_len = char_len_with_expanded_tabs(0, &line_prefix, tab_size);
|
||||
let mut wrapped_text = String::new();
|
||||
let mut current_line = line_prefix.clone();
|
||||
|
||||
let tokenizer = WordBreakingTokenizer::new(&unwrapped_text);
|
||||
let mut current_line_len = line_prefix_len;
|
||||
let mut in_whitespace = false;
|
||||
for token in tokenizer {
|
||||
let have_preceding_whitespace = in_whitespace;
|
||||
match token {
|
||||
WordBreakToken::Word {
|
||||
token,
|
||||
grapheme_len,
|
||||
} => {
|
||||
in_whitespace = false;
|
||||
if current_line_len + grapheme_len > wrap_column
|
||||
&& current_line_len != line_prefix_len
|
||||
{
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
}
|
||||
current_line.push_str(token);
|
||||
current_line_len += grapheme_len;
|
||||
}
|
||||
WordBreakToken::InlineWhitespace {
|
||||
mut token,
|
||||
mut grapheme_len,
|
||||
} => {
|
||||
in_whitespace = true;
|
||||
if have_preceding_whitespace && !preserve_existing_whitespace {
|
||||
continue;
|
||||
}
|
||||
if !preserve_existing_whitespace {
|
||||
token = " ";
|
||||
grapheme_len = 1;
|
||||
}
|
||||
if current_line_len + grapheme_len > wrap_column {
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
} else if current_line_len != line_prefix_len || preserve_existing_whitespace {
|
||||
current_line.push_str(token);
|
||||
current_line_len += grapheme_len;
|
||||
}
|
||||
}
|
||||
WordBreakToken::Newline => {
|
||||
in_whitespace = true;
|
||||
if preserve_existing_whitespace {
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
} else if have_preceding_whitespace {
|
||||
continue;
|
||||
} else if current_line_len + 1 > wrap_column && current_line_len != line_prefix_len
|
||||
{
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
} else if current_line_len != line_prefix_len {
|
||||
current_line.push(' ');
|
||||
current_line_len += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !current_line.is_empty() {
|
||||
wrapped_text.push_str(¤t_line);
|
||||
}
|
||||
wrapped_text
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrap_with_prefix() {
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"# ".to_string(),
|
||||
"abcdefg".to_string(),
|
||||
4,
|
||||
NonZeroU32::new(4).unwrap(),
|
||||
false,
|
||||
),
|
||||
"# abcdefg"
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"".to_string(),
|
||||
"\thello world".to_string(),
|
||||
8,
|
||||
NonZeroU32::new(4).unwrap(),
|
||||
false,
|
||||
),
|
||||
"hello\nworld"
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"// ".to_string(),
|
||||
"xx \nyy zz aa bb cc".to_string(),
|
||||
12,
|
||||
NonZeroU32::new(4).unwrap(),
|
||||
false,
|
||||
),
|
||||
"// xx yy zz\n// aa bb cc"
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
String::new(),
|
||||
"这是什么 \n 钢笔".to_string(),
|
||||
3,
|
||||
NonZeroU32::new(4).unwrap(),
|
||||
false,
|
||||
),
|
||||
"这是什\n么 钢\n笔"
|
||||
);
|
||||
}
|
||||
|
||||
pub trait CollaborationHub {
|
||||
fn collaborators<'a>(&self, cx: &'a App) -> &'a HashMap<PeerId, Collaborator>;
|
||||
fn user_participant_indices<'a>(&self, cx: &'a App) -> &'a HashMap<u64, ParticipantIndex>;
|
||||
|
||||
@@ -16769,9 +16769,9 @@ fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -
|
||||
async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
}"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
@@ -16784,10 +16784,10 @@ async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
|
||||
async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
}"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
@@ -16800,14 +16800,14 @@ async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
|
||||
async fn test_indent_guide_nested(cx: &mut TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
if a == 3 {
|
||||
let b = 2;
|
||||
} else {
|
||||
let c = 3;
|
||||
}
|
||||
}"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
if a == 3 {
|
||||
let b = 2;
|
||||
} else {
|
||||
let c = 3;
|
||||
}
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
@@ -16829,11 +16829,11 @@ async fn test_indent_guide_nested(cx: &mut TestAppContext) {
|
||||
async fn test_indent_guide_tab(cx: &mut TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
let c = 3;
|
||||
}"
|
||||
fn main() {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
let c = 3;
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
@@ -16963,6 +16963,72 @@ async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
&"
|
||||
fn main() {
|
||||
if a {
|
||||
b(
|
||||
c,
|
||||
d,
|
||||
)
|
||||
} else {
|
||||
e(
|
||||
f
|
||||
)
|
||||
}
|
||||
}"
|
||||
.unindent(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(
|
||||
0..11,
|
||||
vec![
|
||||
indent_guide(buffer_id, 1, 10, 0),
|
||||
indent_guide(buffer_id, 2, 5, 1),
|
||||
indent_guide(buffer_id, 7, 9, 1),
|
||||
indent_guide(buffer_id, 3, 4, 2),
|
||||
indent_guide(buffer_id, 8, 8, 2),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.fold_at(MultiBufferRow(2), window, cx);
|
||||
assert_eq!(
|
||||
editor.display_text(cx),
|
||||
"
|
||||
fn main() {
|
||||
if a {
|
||||
b(⋯
|
||||
)
|
||||
} else {
|
||||
e(
|
||||
f
|
||||
)
|
||||
}
|
||||
}"
|
||||
.unindent()
|
||||
);
|
||||
});
|
||||
|
||||
assert_indent_guides(
|
||||
0..11,
|
||||
vec![
|
||||
indent_guide(buffer_id, 1, 10, 0),
|
||||
indent_guide(buffer_id, 2, 5, 1),
|
||||
indent_guide(buffer_id, 7, 9, 1),
|
||||
indent_guide(buffer_id, 8, 8, 2),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
|
||||
@@ -7607,7 +7607,10 @@ impl Element for EditorElement {
|
||||
editor.gutter_dimensions = gutter_dimensions;
|
||||
editor.set_visible_line_count(bounds.size.height / line_height, window, cx);
|
||||
|
||||
if matches!(editor.mode, EditorMode::Minimap { .. }) {
|
||||
if matches!(
|
||||
editor.mode,
|
||||
EditorMode::AutoHeight { .. } | EditorMode::Minimap { .. }
|
||||
) {
|
||||
snapshot
|
||||
} else {
|
||||
let wrap_width_for = |column: u32| (column as f32 * em_advance).ceil();
|
||||
@@ -9626,7 +9629,6 @@ fn compute_auto_height_layout(
|
||||
let font_size = style.text.font_size.to_pixels(window.rem_size());
|
||||
let line_height = style.text.line_height_in_pixels(window.rem_size());
|
||||
let em_width = window.text_system().em_width(font_id, font_size).unwrap();
|
||||
let em_advance = window.text_system().em_advance(font_id, font_size).unwrap();
|
||||
|
||||
let mut snapshot = editor.snapshot(window, cx);
|
||||
let gutter_dimensions = snapshot
|
||||
@@ -9643,18 +9645,10 @@ fn compute_auto_height_layout(
|
||||
let overscroll = size(em_width, px(0.));
|
||||
|
||||
let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width;
|
||||
let content_offset = point(gutter_dimensions.margin, Pixels::ZERO);
|
||||
let editor_content_width = editor_width - content_offset.x;
|
||||
let wrap_width_for = |column: u32| (column as f32 * em_advance).ceil();
|
||||
let wrap_width = match editor.soft_wrap_mode(cx) {
|
||||
SoftWrap::GitDiff => None,
|
||||
SoftWrap::None => Some(wrap_width_for(MAX_LINE_LEN as u32 / 2)),
|
||||
SoftWrap::EditorWidth => Some(editor_content_width),
|
||||
SoftWrap::Column(column) => Some(wrap_width_for(column)),
|
||||
SoftWrap::Bounded(column) => Some(editor_content_width.min(wrap_width_for(column))),
|
||||
};
|
||||
if editor.set_wrap_width(wrap_width, cx) {
|
||||
snapshot = editor.snapshot(window, cx);
|
||||
if !matches!(editor.soft_wrap_mode(cx), SoftWrap::None) {
|
||||
if editor.set_wrap_width(Some(editor_width), cx) {
|
||||
snapshot = editor.snapshot(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
let scroll_height = (snapshot.max_point().row().next_row().0 as f32) * line_height;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::{ops::Range, time::Duration};
|
||||
use std::{cmp::Ordering, ops::Range, time::Duration};
|
||||
|
||||
use collections::HashSet;
|
||||
use gpui::{App, AppContext as _, Context, Task, Window};
|
||||
use language::language_settings::language_settings;
|
||||
use multi_buffer::{IndentGuide, MultiBufferRow};
|
||||
use multi_buffer::{IndentGuide, MultiBufferRow, ToPoint};
|
||||
use text::{LineIndent, Point};
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -154,12 +154,28 @@ pub fn indent_guides_in_range(
|
||||
snapshot: &DisplaySnapshot,
|
||||
cx: &App,
|
||||
) -> Vec<IndentGuide> {
|
||||
let start_anchor = snapshot
|
||||
let start_offset = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(Point::new(visible_buffer_range.start.0, 0));
|
||||
let end_anchor = snapshot
|
||||
.point_to_offset(Point::new(visible_buffer_range.start.0, 0));
|
||||
let end_offset = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_after(Point::new(visible_buffer_range.end.0, 0));
|
||||
.point_to_offset(Point::new(visible_buffer_range.end.0, 0));
|
||||
let start_anchor = snapshot.buffer_snapshot.anchor_before(start_offset);
|
||||
let end_anchor = snapshot.buffer_snapshot.anchor_after(end_offset);
|
||||
|
||||
let mut fold_ranges = Vec::<Range<Point>>::new();
|
||||
let mut folds = snapshot.folds_in_range(start_offset..end_offset).peekable();
|
||||
while let Some(fold) = folds.next() {
|
||||
let start = fold.range.start.to_point(&snapshot.buffer_snapshot);
|
||||
let end = fold.range.end.to_point(&snapshot.buffer_snapshot);
|
||||
if let Some(last_range) = fold_ranges.last_mut() {
|
||||
if last_range.end >= start {
|
||||
last_range.end = last_range.end.max(end);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
fold_ranges.push(start..end);
|
||||
}
|
||||
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
@@ -169,15 +185,19 @@ pub fn indent_guides_in_range(
|
||||
return false;
|
||||
}
|
||||
|
||||
let start = MultiBufferRow(indent_guide.start_row.0.saturating_sub(1));
|
||||
// Filter out indent guides that are inside a fold
|
||||
// All indent guides that are starting "offscreen" have a start value of the first visible row minus one
|
||||
// Therefore checking if a line is folded at first visible row minus one causes the other indent guides that are not related to the fold to disappear as well
|
||||
let is_folded = snapshot.is_line_folded(start);
|
||||
let line_indent = snapshot.line_indent_for_buffer_row(start);
|
||||
let contained_in_fold =
|
||||
line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level();
|
||||
!(is_folded && contained_in_fold)
|
||||
let has_containing_fold = fold_ranges
|
||||
.binary_search_by(|fold_range| {
|
||||
if fold_range.start >= Point::new(indent_guide.start_row.0, 0) {
|
||||
Ordering::Greater
|
||||
} else if fold_range.end < Point::new(indent_guide.end_row.0, 0) {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
})
|
||||
.is_ok();
|
||||
|
||||
!has_containing_fold
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -67,3 +67,4 @@ unindent.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
@@ -19,6 +19,7 @@ use collections::HashMap;
|
||||
use futures::{FutureExt as _, StreamExt, channel::mpsc, select_biased};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity};
|
||||
use language_model::{LanguageModel, Role, StopReason};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub const THREAD_EVENT_TIMEOUT: Duration = Duration::from_secs(60 * 2);
|
||||
|
||||
@@ -307,7 +308,7 @@ impl ExampleContext {
|
||||
|
||||
let message_count_before = self.app.update_entity(&self.agent_thread, |thread, cx| {
|
||||
thread.set_remaining_turns(iterations);
|
||||
thread.send_to_model(model, None, cx);
|
||||
thread.send_to_model(model, CompletionIntent::UserPrompt, None, cx);
|
||||
thread.messages().len()
|
||||
})?;
|
||||
|
||||
|
||||
@@ -576,6 +576,7 @@ impl ExampleInstance {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
intent: None,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![MessageContent::Text(to_prompt(assertion.description))],
|
||||
|
||||
@@ -1527,7 +1527,6 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("open-selection", "Open").on_click(|_, window, cx| {
|
||||
|
||||
@@ -59,6 +59,7 @@ util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
@@ -13,7 +13,6 @@ use agent_settings::AgentSettings;
|
||||
use anyhow::Context as _;
|
||||
use askpass::AskPassDelegate;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
|
||||
use editor::{
|
||||
Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar,
|
||||
scroll::ScrollbarAutoHide,
|
||||
@@ -42,6 +41,7 @@ use language_model::{
|
||||
};
|
||||
use menu::{Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
|
||||
use multi_buffer::ExcerptInfo;
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use panel::{
|
||||
PanelHeader, panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
|
||||
panel_icon_button,
|
||||
@@ -54,7 +54,6 @@ use project::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use std::future::Future;
|
||||
use std::num::NonZeroU32;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{collections::HashSet, sync::Arc, time::Duration, usize};
|
||||
use strum::{IntoEnumIterator, VariantNames};
|
||||
@@ -63,15 +62,14 @@ use ui::{
|
||||
Checkbox, ContextMenu, ElevationIndex, PopoverMenu, Scrollbar, ScrollbarState, SplitButton,
|
||||
Tooltip, prelude::*,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt, maybe, wrap_with_prefix};
|
||||
use util::{ResultExt, TryFutureExt, maybe};
|
||||
use workspace::AppState;
|
||||
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use workspace::{
|
||||
Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::DetachAndPromptErr,
|
||||
};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
actions!(
|
||||
git_panel,
|
||||
@@ -385,6 +383,7 @@ pub(crate) fn commit_message_editor(
|
||||
commit_editor.set_show_gutter(false, cx);
|
||||
commit_editor.set_show_wrap_guides(false, cx);
|
||||
commit_editor.set_show_indent_guides(false, cx);
|
||||
commit_editor.set_hard_wrap(Some(72), cx);
|
||||
let placeholder = placeholder.unwrap_or("Enter commit message".into());
|
||||
commit_editor.set_placeholder_text(placeholder, cx);
|
||||
commit_editor
|
||||
@@ -1486,22 +1485,8 @@ impl GitPanel {
|
||||
|
||||
fn custom_or_suggested_commit_message(&self, cx: &mut Context<Self>) -> Option<String> {
|
||||
let message = self.commit_editor.read(cx).text(cx);
|
||||
let width = self
|
||||
.commit_editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.language_settings(cx)
|
||||
.preferred_line_length as usize;
|
||||
|
||||
if !message.trim().is_empty() {
|
||||
let message = wrap_with_prefix(
|
||||
String::new(),
|
||||
message,
|
||||
width,
|
||||
NonZeroU32::new(8).unwrap(), // tab size doesn't matter when prefix is empty
|
||||
false,
|
||||
);
|
||||
return Some(message);
|
||||
}
|
||||
|
||||
@@ -1781,6 +1766,7 @@ impl GitPanel {
|
||||
let request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: Some(CompletionIntent::GenerateGitCommitMessage),
|
||||
mode: None,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
///
|
||||
/// The keybindings themselves are managed independently by calling cx.bind_keys().
|
||||
/// (Though mostly when developing Zed itself, you just need to add a new line to
|
||||
/// assets/keymaps/default.json).
|
||||
/// assets/keymaps/default-{platform}.json).
|
||||
///
|
||||
/// ```rust
|
||||
/// cx.bind_keys([
|
||||
|
||||
@@ -680,7 +680,7 @@ pub struct CodeLabel {
|
||||
pub filter_range: Range<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Deserialize, JsonSchema)]
|
||||
pub struct LanguageConfig {
|
||||
/// Human-readable name of the language.
|
||||
pub name: LanguageName,
|
||||
@@ -791,7 +791,7 @@ pub struct LanguageMatcher {
|
||||
}
|
||||
|
||||
/// The configuration for JSX tag auto-closing.
|
||||
#[derive(Clone, Debug, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Deserialize, JsonSchema)]
|
||||
pub struct JsxTagAutoCloseConfig {
|
||||
/// The name of the node for a opening tag
|
||||
pub open_tag_node_name: String,
|
||||
@@ -824,7 +824,7 @@ pub struct JsxTagAutoCloseConfig {
|
||||
}
|
||||
|
||||
/// The configuration for documentation block for this language.
|
||||
#[derive(Clone, Debug, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Deserialize, JsonSchema)]
|
||||
pub struct DocumentationConfig {
|
||||
/// A start tag of documentation block.
|
||||
pub start: Arc<str>,
|
||||
|
||||
@@ -12,7 +12,7 @@ use gpui::{
|
||||
use image::codecs::png::PngEncoder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::ResultExt;
|
||||
use zed_llm_client::CompletionMode;
|
||||
use zed_llm_client::{CompletionIntent, CompletionMode};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
pub struct LanguageModelImage {
|
||||
@@ -281,6 +281,7 @@ pub enum LanguageModelToolChoice {
|
||||
pub struct LanguageModelRequest {
|
||||
pub thread_id: Option<String>,
|
||||
pub prompt_id: Option<String>,
|
||||
pub intent: Option<CompletionIntent>,
|
||||
pub mode: Option<CompletionMode>,
|
||||
pub messages: Vec<LanguageModelRequestMessage>,
|
||||
pub tools: Vec<LanguageModelRequestTool>,
|
||||
|
||||
@@ -809,6 +809,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
> {
|
||||
let thread_id = request.thread_id.clone();
|
||||
let prompt_id = request.prompt_id.clone();
|
||||
let intent = request.intent;
|
||||
let mode = request.mode;
|
||||
let app_version = cx.update(|cx| AppVersion::global(cx)).ok();
|
||||
match self.model.provider {
|
||||
@@ -841,6 +842,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
CompletionBody {
|
||||
thread_id,
|
||||
prompt_id,
|
||||
intent,
|
||||
mode,
|
||||
provider: zed_llm_client::LanguageModelProvider::Anthropic,
|
||||
model: request.model.clone(),
|
||||
@@ -897,6 +899,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
CompletionBody {
|
||||
thread_id,
|
||||
prompt_id,
|
||||
intent,
|
||||
mode,
|
||||
provider: zed_llm_client::LanguageModelProvider::OpenAi,
|
||||
model: request.model.clone(),
|
||||
@@ -934,6 +937,7 @@ impl LanguageModel for CloudLanguageModel {
|
||||
CompletionBody {
|
||||
thread_id,
|
||||
prompt_id,
|
||||
intent,
|
||||
mode,
|
||||
provider: zed_llm_client::LanguageModelProvider::Google,
|
||||
model: request.model.model_id.clone(),
|
||||
|
||||
@@ -820,6 +820,7 @@ mod tests {
|
||||
tool_choice: None,
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: None,
|
||||
mode: None,
|
||||
stop: Vec::new(),
|
||||
};
|
||||
|
||||
@@ -864,6 +864,7 @@ mod tests {
|
||||
let request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: None,
|
||||
mode: None,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
|
||||
@@ -357,6 +357,9 @@ const PYTHON_TEST_TARGET_TASK_VARIABLE: VariableName =
|
||||
const PYTHON_ACTIVE_TOOLCHAIN_PATH: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN"));
|
||||
|
||||
const PYTHON_ACTIVE_TOOLCHAIN_PATH_RAW: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN_RAW"));
|
||||
|
||||
const PYTHON_MODULE_NAME_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("PYTHON_MODULE_NAME"));
|
||||
|
||||
@@ -378,21 +381,25 @@ impl ContextProvider for PythonContextProvider {
|
||||
let worktree_id = location.buffer.read(cx).file().map(|f| f.worktree_id(cx));
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let active_toolchain = if let Some(worktree_id) = worktree_id {
|
||||
let raw_toolchain = if let Some(worktree_id) = worktree_id {
|
||||
toolchains
|
||||
.active_toolchain(worktree_id, Arc::from("".as_ref()), "Python".into(), cx)
|
||||
.await
|
||||
.map_or_else(|| "python3".to_owned(), |toolchain| toolchain.path.into())
|
||||
.map_or_else(
|
||||
|| String::from("python3"),
|
||||
|toolchain| toolchain.path.to_string(),
|
||||
)
|
||||
} else {
|
||||
String::from("python3")
|
||||
};
|
||||
let active_toolchain = format!("\"{raw_toolchain}\"");
|
||||
let toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH, active_toolchain);
|
||||
|
||||
let raw_toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH_RAW, raw_toolchain);
|
||||
Ok(task::TaskVariables::from_iter(
|
||||
test_target
|
||||
.into_iter()
|
||||
.chain(module_target.into_iter())
|
||||
.chain([toolchain]),
|
||||
.chain([toolchain, raw_toolchain]),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5753,15 +5753,28 @@ impl MultiBufferSnapshot {
|
||||
let mut result = Vec::new();
|
||||
let mut indent_stack = SmallVec::<[IndentGuide; 8]>::new();
|
||||
|
||||
let mut prev_settings = None;
|
||||
while let Some((first_row, mut line_indent, buffer)) = row_indents.next() {
|
||||
if first_row > end_row {
|
||||
break;
|
||||
}
|
||||
let current_depth = indent_stack.len() as u32;
|
||||
|
||||
let settings =
|
||||
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx);
|
||||
let tab_size = settings.tab_size.get() as u32;
|
||||
// Avoid retrieving the language settings repeatedly for every buffer row.
|
||||
if let Some((prev_buffer_id, _)) = &prev_settings {
|
||||
if prev_buffer_id != &buffer.remote_id() {
|
||||
prev_settings.take();
|
||||
}
|
||||
}
|
||||
let settings = &prev_settings
|
||||
.get_or_insert_with(|| {
|
||||
(
|
||||
buffer.remote_id(),
|
||||
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx),
|
||||
)
|
||||
})
|
||||
.1;
|
||||
let tab_size = settings.tab_size.get();
|
||||
|
||||
// When encountering empty, continue until found useful line indent
|
||||
// then add to the indent stack with the depth found
|
||||
|
||||
@@ -5,7 +5,7 @@ use async_trait::async_trait;
|
||||
use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
|
||||
use gpui::SharedString;
|
||||
|
||||
use task::{DebugScenario, SpawnInTerminal, TaskTemplate};
|
||||
use task::{DebugScenario, SpawnInTerminal, TaskTemplate, VariableName};
|
||||
|
||||
pub(crate) struct PythonLocator;
|
||||
|
||||
@@ -35,6 +35,13 @@ impl DapLocator for PythonLocator {
|
||||
// We cannot debug selections.
|
||||
return None;
|
||||
}
|
||||
let command = if build_config.command
|
||||
== VariableName::Custom("PYTHON_ACTIVE_ZED_TOOLCHAIN".into()).template_value()
|
||||
{
|
||||
VariableName::Custom("PYTHON_ACTIVE_ZED_TOOLCHAIN_RAW".into()).template_value()
|
||||
} else {
|
||||
build_config.command.clone()
|
||||
};
|
||||
let module_specifier_position = build_config
|
||||
.args
|
||||
.iter()
|
||||
@@ -68,7 +75,7 @@ impl DapLocator for PythonLocator {
|
||||
}
|
||||
let mut config = serde_json::json!({
|
||||
"request": "launch",
|
||||
"python": build_config.command,
|
||||
"python": command,
|
||||
"args": args,
|
||||
"cwd": build_config.cwd.clone()
|
||||
});
|
||||
|
||||
@@ -121,16 +121,17 @@ impl From<dap::Thread> for Thread {
|
||||
|
||||
pub enum Mode {
|
||||
Building,
|
||||
Running(LocalMode),
|
||||
Running(RunningMode),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LocalMode {
|
||||
pub struct RunningMode {
|
||||
client: Arc<DebugAdapterClient>,
|
||||
binary: DebugAdapterBinary,
|
||||
tmp_breakpoint: Option<SourceBreakpoint>,
|
||||
worktree: WeakEntity<Worktree>,
|
||||
executor: BackgroundExecutor,
|
||||
is_started: bool,
|
||||
}
|
||||
|
||||
fn client_source(abs_path: &Path) -> dap::Source {
|
||||
@@ -148,7 +149,7 @@ fn client_source(abs_path: &Path) -> dap::Source {
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalMode {
|
||||
impl RunningMode {
|
||||
async fn new(
|
||||
session_id: SessionId,
|
||||
parent_session: Option<Entity<Session>>,
|
||||
@@ -181,6 +182,7 @@ impl LocalMode {
|
||||
tmp_breakpoint: None,
|
||||
binary,
|
||||
executor: cx.background_executor().clone(),
|
||||
is_started: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -373,7 +375,7 @@ impl LocalMode {
|
||||
capabilities: &Capabilities,
|
||||
initialized_rx: oneshot::Receiver<()>,
|
||||
dap_store: WeakEntity<DapStore>,
|
||||
cx: &App,
|
||||
cx: &mut Context<Session>,
|
||||
) -> Task<Result<()>> {
|
||||
let raw = self.binary.request_args.clone();
|
||||
|
||||
@@ -405,7 +407,7 @@ impl LocalMode {
|
||||
let this = self.clone();
|
||||
let worktree = self.worktree().clone();
|
||||
let configuration_sequence = cx.spawn({
|
||||
async move |cx| {
|
||||
async move |_, cx| {
|
||||
let breakpoint_store =
|
||||
dap_store.read_with(cx, |dap_store, _| dap_store.breakpoint_store().clone())?;
|
||||
initialized_rx.await?;
|
||||
@@ -453,9 +455,20 @@ impl LocalMode {
|
||||
}
|
||||
});
|
||||
|
||||
cx.background_spawn(async move {
|
||||
futures::future::try_join(launch, configuration_sequence).await?;
|
||||
Ok(())
|
||||
let task = cx.background_spawn(futures::future::try_join(launch, configuration_sequence));
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
task.await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(this) = this.as_running_mut() {
|
||||
this.is_started = true;
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -704,7 +717,7 @@ impl Session {
|
||||
cx.subscribe(&breakpoint_store, |this, store, event, cx| match event {
|
||||
BreakpointStoreEvent::BreakpointsUpdated(path, reason) => {
|
||||
if let Some(local) = (!this.ignore_breakpoints)
|
||||
.then(|| this.as_local_mut())
|
||||
.then(|| this.as_running_mut())
|
||||
.flatten()
|
||||
{
|
||||
local
|
||||
@@ -714,7 +727,7 @@ impl Session {
|
||||
}
|
||||
BreakpointStoreEvent::BreakpointsCleared(paths) => {
|
||||
if let Some(local) = (!this.ignore_breakpoints)
|
||||
.then(|| this.as_local_mut())
|
||||
.then(|| this.as_running_mut())
|
||||
.flatten()
|
||||
{
|
||||
local.unset_breakpoints_from_paths(paths, cx).detach();
|
||||
@@ -806,7 +819,7 @@ impl Session {
|
||||
let parent_session = self.parent_session.clone();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let mode = LocalMode::new(
|
||||
let mode = RunningMode::new(
|
||||
id,
|
||||
parent_session,
|
||||
worktree.downgrade(),
|
||||
@@ -906,18 +919,29 @@ impl Session {
|
||||
return tx;
|
||||
}
|
||||
|
||||
pub fn is_local(&self) -> bool {
|
||||
pub fn is_started(&self) -> bool {
|
||||
match &self.mode {
|
||||
Mode::Building => false,
|
||||
Mode::Running(running) => running.is_started,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_building(&self) -> bool {
|
||||
matches!(self.mode, Mode::Building)
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
matches!(self.mode, Mode::Running(_))
|
||||
}
|
||||
|
||||
pub fn as_local_mut(&mut self) -> Option<&mut LocalMode> {
|
||||
pub fn as_running_mut(&mut self) -> Option<&mut RunningMode> {
|
||||
match &mut self.mode {
|
||||
Mode::Running(local_mode) => Some(local_mode),
|
||||
Mode::Building => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_local(&self) -> Option<&LocalMode> {
|
||||
pub fn as_running(&self) -> Option<&RunningMode> {
|
||||
match &self.mode {
|
||||
Mode::Running(local_mode) => Some(local_mode),
|
||||
Mode::Building => None,
|
||||
@@ -1140,7 +1164,7 @@ impl Session {
|
||||
body: Option<serde_json::Value>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let Some(local_session) = self.as_local() else {
|
||||
let Some(local_session) = self.as_running() else {
|
||||
unreachable!("Cannot respond to remote client");
|
||||
};
|
||||
let client = local_session.client.clone();
|
||||
@@ -1162,7 +1186,7 @@ impl Session {
|
||||
fn handle_stopped_event(&mut self, event: StoppedEvent, cx: &mut Context<Self>) {
|
||||
// todo(debugger): Find a clean way to get around the clone
|
||||
let breakpoint_store = self.breakpoint_store.clone();
|
||||
if let Some((local, path)) = self.as_local_mut().and_then(|local| {
|
||||
if let Some((local, path)) = self.as_running_mut().and_then(|local| {
|
||||
let breakpoint = local.tmp_breakpoint.take()?;
|
||||
let path = breakpoint.path.clone();
|
||||
Some((local, path))
|
||||
@@ -1528,7 +1552,7 @@ impl Session {
|
||||
|
||||
self.ignore_breakpoints = ignore;
|
||||
|
||||
if let Some(local) = self.as_local() {
|
||||
if let Some(local) = self.as_running() {
|
||||
local.send_source_breakpoints(ignore, &self.breakpoint_store, cx)
|
||||
} else {
|
||||
// todo(debugger): We need to propagate this change to downstream sessions and send a message to upstream sessions
|
||||
@@ -1550,7 +1574,7 @@ impl Session {
|
||||
}
|
||||
|
||||
fn send_exception_breakpoints(&mut self, cx: &App) {
|
||||
if let Some(local) = self.as_local() {
|
||||
if let Some(local) = self.as_running() {
|
||||
let exception_filters = self
|
||||
.exception_breakpoints
|
||||
.values()
|
||||
|
||||
@@ -923,6 +923,7 @@ impl RulesLibrary {
|
||||
LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: None,
|
||||
mode: None,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
|
||||
@@ -560,6 +560,7 @@ impl SummaryIndex {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
intent: None,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![prompt.into()],
|
||||
|
||||
@@ -20,6 +20,7 @@ collections.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
hex.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
proto.workspace = true
|
||||
schemars.workspace = true
|
||||
@@ -29,8 +30,8 @@ serde_json_lenient.workspace = true
|
||||
sha2.workspace = true
|
||||
shellexpand.workspace = true
|
||||
util.workspace = true
|
||||
zed_actions.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::FxHashMap;
|
||||
use gpui::SharedString;
|
||||
use log as _;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::path::PathBuf;
|
||||
use util::debug_panic;
|
||||
|
||||
use crate::{TaskTemplate, adapter_schema::AdapterSchemas};
|
||||
|
||||
@@ -182,7 +184,7 @@ impl From<AttachRequest> for DebugRequest {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
|
||||
#[derive(Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum BuildTaskDefinition {
|
||||
ByName(SharedString),
|
||||
@@ -194,6 +196,47 @@ pub enum BuildTaskDefinition {
|
||||
},
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for BuildTaskDefinition {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct TemplateHelper {
|
||||
#[serde(default)]
|
||||
label: Option<String>,
|
||||
#[serde(flatten)]
|
||||
rest: serde_json::Value,
|
||||
}
|
||||
|
||||
let value = serde_json::Value::deserialize(deserializer)?;
|
||||
|
||||
if let Ok(name) = serde_json::from_value::<SharedString>(value.clone()) {
|
||||
return Ok(BuildTaskDefinition::ByName(name));
|
||||
}
|
||||
|
||||
let helper: TemplateHelper =
|
||||
serde_json::from_value(value).map_err(serde::de::Error::custom)?;
|
||||
|
||||
let mut template_value = helper.rest;
|
||||
if let serde_json::Value::Object(ref mut map) = template_value {
|
||||
map.insert(
|
||||
"label".to_string(),
|
||||
serde_json::to_value(helper.label.unwrap_or_else(|| "debug-build".to_owned()))
|
||||
.map_err(serde::de::Error::custom)?,
|
||||
);
|
||||
}
|
||||
|
||||
let task_template: TaskTemplate =
|
||||
serde_json::from_value(template_value).map_err(serde::de::Error::custom)?;
|
||||
|
||||
Ok(BuildTaskDefinition::Template {
|
||||
task_template,
|
||||
locator_name: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)]
|
||||
pub enum Request {
|
||||
Launch,
|
||||
@@ -243,9 +286,96 @@ pub struct DebugScenario {
|
||||
pub struct DebugTaskFile(pub Vec<DebugScenario>);
|
||||
|
||||
impl DebugTaskFile {
|
||||
/// Generates JSON schema of Tasks JSON template format.
|
||||
pub fn generate_json_schema(schemas: &AdapterSchemas) -> serde_json_lenient::Value {
|
||||
schemas.generate_json_schema().unwrap_or_default()
|
||||
let build_task_schema = schemars::schema_for!(BuildTaskDefinition);
|
||||
let mut build_task_value =
|
||||
serde_json_lenient::to_value(&build_task_schema).unwrap_or_default();
|
||||
|
||||
if let Some(template_object) = build_task_value
|
||||
.get_mut("anyOf")
|
||||
.and_then(|array| array.as_array_mut())
|
||||
.and_then(|array| array.get_mut(1))
|
||||
{
|
||||
if let Some(properties) = template_object
|
||||
.get_mut("properties")
|
||||
.and_then(|value| value.as_object_mut())
|
||||
{
|
||||
properties.remove("label");
|
||||
}
|
||||
|
||||
if let Some(arr) = template_object
|
||||
.get_mut("required")
|
||||
.and_then(|array| array.as_array_mut())
|
||||
{
|
||||
arr.retain(|v| v.as_str() != Some("label"));
|
||||
}
|
||||
} else {
|
||||
debug_panic!("Task Template schema in debug scenario's needs to be updated");
|
||||
}
|
||||
|
||||
let task_definitions = build_task_value
|
||||
.get("definitions")
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let adapter_conditions = schemas
|
||||
.0
|
||||
.iter()
|
||||
.map(|adapter_schema| {
|
||||
let adapter_name = adapter_schema.adapter.to_string();
|
||||
serde_json::json!({
|
||||
"if": {
|
||||
"properties": {
|
||||
"adapter": { "const": adapter_name }
|
||||
}
|
||||
},
|
||||
"then": adapter_schema.schema
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
serde_json_lenient::json!({
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Debug Configurations",
|
||||
"description": "Configuration for debug scenarios",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["adapter", "label"],
|
||||
"properties": {
|
||||
"adapter": {
|
||||
"type": "string",
|
||||
"description": "The name of the debug adapter"
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "The name of the debug configuration"
|
||||
},
|
||||
"build": build_task_value,
|
||||
"tcp_connection": {
|
||||
"type": "object",
|
||||
"description": "Optional TCP connection information for connecting to an already running debug adapter",
|
||||
"properties": {
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"description": "The port that the debug adapter is listening on (default: auto-find open port)"
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"pattern": "^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$",
|
||||
"description": "The host that the debug adapter is listening to (default: 127.0.0.1)"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "The max amount of time in milliseconds to connect to a tcp DAP before returning an error (default: 2000ms)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": adapter_conditions
|
||||
},
|
||||
"definitions": task_definitions
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,6 +384,32 @@ mod tests {
|
||||
use crate::DebugScenario;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_just_build_args() {
|
||||
let json = r#"{
|
||||
"label": "Build & debug rust",
|
||||
"adapter": "CodeLLDB",
|
||||
"build": {
|
||||
"command": "rust",
|
||||
"args": ["build"]
|
||||
}
|
||||
}"#;
|
||||
|
||||
let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
|
||||
assert!(deserialized.build.is_some());
|
||||
match deserialized.build.as_ref().unwrap() {
|
||||
crate::BuildTaskDefinition::Template { task_template, .. } => {
|
||||
assert_eq!("debug-build", task_template.label);
|
||||
assert_eq!("rust", task_template.command);
|
||||
assert_eq!(vec!["build"], task_template.args);
|
||||
}
|
||||
_ => panic!("Expected Template variant"),
|
||||
}
|
||||
assert_eq!(json!({}), deserialized.config);
|
||||
assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
|
||||
assert_eq!("Build & debug rust", deserialized.label.as_ref());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_scenario_has_none_request() {
|
||||
let json = r#"{
|
||||
@@ -307,4 +463,45 @@ mod tests {
|
||||
assert_eq!("CodeLLDB", deserialized.adapter.as_ref());
|
||||
assert_eq!("Attach to process", deserialized.label.as_ref());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_task_definition_without_label() {
|
||||
use crate::BuildTaskDefinition;
|
||||
|
||||
let json = r#""my_build_task""#;
|
||||
let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
|
||||
match deserialized {
|
||||
BuildTaskDefinition::ByName(name) => assert_eq!("my_build_task", name.as_ref()),
|
||||
_ => panic!("Expected ByName variant"),
|
||||
}
|
||||
|
||||
let json = r#"{
|
||||
"command": "cargo",
|
||||
"args": ["build", "--release"]
|
||||
}"#;
|
||||
let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
|
||||
match deserialized {
|
||||
BuildTaskDefinition::Template { task_template, .. } => {
|
||||
assert_eq!("debug-build", task_template.label);
|
||||
assert_eq!("cargo", task_template.command);
|
||||
assert_eq!(vec!["build", "--release"], task_template.args);
|
||||
}
|
||||
_ => panic!("Expected Template variant"),
|
||||
}
|
||||
|
||||
let json = r#"{
|
||||
"label": "Build Release",
|
||||
"command": "cargo",
|
||||
"args": ["build", "--release"]
|
||||
}"#;
|
||||
let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap();
|
||||
match deserialized {
|
||||
BuildTaskDefinition::Template { task_template, .. } => {
|
||||
assert_eq!("Build Release", task_template.label);
|
||||
assert_eq!("cargo", task_template.command);
|
||||
assert_eq!(vec!["build", "--release"], task_template.args);
|
||||
}
|
||||
_ => panic!("Expected Template variant"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,7 +384,6 @@ impl ShellBuilder {
|
||||
|
||||
/// Returns the program and arguments to run this task in a shell.
|
||||
pub fn build(mut self, task_command: String, task_args: &Vec<String>) -> (String, Vec<String>) {
|
||||
let task_command = format!("\"{task_command}\"");
|
||||
let combined_command = task_args
|
||||
.into_iter()
|
||||
.fold(task_command, |mut command, arg| {
|
||||
|
||||
@@ -237,18 +237,6 @@ impl TaskTemplate {
|
||||
env
|
||||
};
|
||||
|
||||
// We filter out env variables here that aren't set so we don't have extra white space in args
|
||||
let args = self
|
||||
.args
|
||||
.iter()
|
||||
.filter(|arg| {
|
||||
arg.starts_with('$')
|
||||
.then(|| env.get(&arg[1..]).is_some_and(|arg| !arg.trim().is_empty()))
|
||||
.unwrap_or(true)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
Some(ResolvedTask {
|
||||
id: id.clone(),
|
||||
substituted_variables,
|
||||
@@ -268,7 +256,7 @@ impl TaskTemplate {
|
||||
},
|
||||
),
|
||||
command,
|
||||
args,
|
||||
args: self.args.clone(),
|
||||
env,
|
||||
use_new_terminal: self.use_new_terminal,
|
||||
allow_concurrent_runs: self.allow_concurrent_runs,
|
||||
@@ -715,7 +703,6 @@ mod tests {
|
||||
label: "My task".into(),
|
||||
command: "echo".into(),
|
||||
args: vec!["$PATH".into()],
|
||||
env: HashMap::from_iter([("PATH".to_owned(), "non-empty".to_owned())]),
|
||||
..TaskTemplate::default()
|
||||
};
|
||||
let resolved_task = task
|
||||
@@ -728,32 +715,6 @@ mod tests {
|
||||
assert_eq!(resolved.args, task.args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_env_variables_excluded_from_args() {
|
||||
let task = TaskTemplate {
|
||||
label: "My task".into(),
|
||||
command: "echo".into(),
|
||||
args: vec![
|
||||
"$EMPTY_VAR".into(),
|
||||
"hello".into(),
|
||||
"$WHITESPACE_VAR".into(),
|
||||
"$UNDEFINED_VAR".into(),
|
||||
"$WORLD".into(),
|
||||
],
|
||||
env: HashMap::from_iter([
|
||||
("EMPTY_VAR".to_owned(), "".to_owned()),
|
||||
("WHITESPACE_VAR".to_owned(), " ".to_owned()),
|
||||
("WORLD".to_owned(), "non-empty".to_owned()),
|
||||
]),
|
||||
..TaskTemplate::default()
|
||||
};
|
||||
let resolved_task = task
|
||||
.resolve_task(TEST_ID_BASE, &TaskContext::default())
|
||||
.unwrap();
|
||||
let resolved = resolved_task.resolved;
|
||||
assert_eq!(resolved.args, vec!["hello", "$WORLD"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_errors_on_missing_zed_variable() {
|
||||
let task = TaskTemplate {
|
||||
|
||||
@@ -22,7 +22,7 @@ use workspace::{ModalView, Workspace};
|
||||
pub use zed_actions::{Rerun, Spawn};
|
||||
|
||||
/// A modal used to spawn new tasks.
|
||||
pub(crate) struct TasksModalDelegate {
|
||||
pub struct TasksModalDelegate {
|
||||
task_store: Entity<TaskStore>,
|
||||
candidates: Option<Vec<(TaskSourceKind, ResolvedTask)>>,
|
||||
task_overrides: Option<TaskOverrides>,
|
||||
@@ -32,21 +32,21 @@ pub(crate) struct TasksModalDelegate {
|
||||
selected_index: usize,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
prompt: String,
|
||||
task_contexts: TaskContexts,
|
||||
task_contexts: Arc<TaskContexts>,
|
||||
placeholder_text: Arc<str>,
|
||||
}
|
||||
|
||||
/// Task template amendments to do before resolving the context.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) struct TaskOverrides {
|
||||
pub struct TaskOverrides {
|
||||
/// See [`RevealTarget`].
|
||||
pub(crate) reveal_target: Option<RevealTarget>,
|
||||
pub reveal_target: Option<RevealTarget>,
|
||||
}
|
||||
|
||||
impl TasksModalDelegate {
|
||||
fn new(
|
||||
task_store: Entity<TaskStore>,
|
||||
task_contexts: TaskContexts,
|
||||
task_contexts: Arc<TaskContexts>,
|
||||
task_overrides: Option<TaskOverrides>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
) -> Self {
|
||||
@@ -122,15 +122,16 @@ impl TasksModalDelegate {
|
||||
}
|
||||
|
||||
pub struct TasksModal {
|
||||
picker: Entity<Picker<TasksModalDelegate>>,
|
||||
pub picker: Entity<Picker<TasksModalDelegate>>,
|
||||
_subscription: [Subscription; 2],
|
||||
}
|
||||
|
||||
impl TasksModal {
|
||||
pub(crate) fn new(
|
||||
pub fn new(
|
||||
task_store: Entity<TaskStore>,
|
||||
task_contexts: TaskContexts,
|
||||
task_contexts: Arc<TaskContexts>,
|
||||
task_overrides: Option<TaskOverrides>,
|
||||
is_modal: bool,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -141,6 +142,7 @@ impl TasksModal {
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.modal(is_modal)
|
||||
});
|
||||
let _subscription = [
|
||||
cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| {
|
||||
@@ -157,6 +159,20 @@ impl TasksModal {
|
||||
_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn task_contexts_loaded(
|
||||
&mut self,
|
||||
task_contexts: Arc<TaskContexts>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.delegate.task_contexts = task_contexts;
|
||||
picker.delegate.candidates = None;
|
||||
picker.refresh(window, cx);
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for TasksModal {
|
||||
@@ -547,6 +563,7 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_footer(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
use std::path::Path;
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use gpui::{App, AppContext as _, Context, Entity, Task, Window};
|
||||
use modal::TaskOverrides;
|
||||
use project::{Location, TaskContexts, TaskSourceKind, Worktree};
|
||||
use task::{RevealTarget, TaskContext, TaskId, TaskTemplate, TaskVariables, VariableName};
|
||||
use workspace::Workspace;
|
||||
|
||||
mod modal;
|
||||
|
||||
pub use modal::{Rerun, ShowAttachModal, Spawn, TasksModal};
|
||||
pub use modal::{Rerun, ShowAttachModal, Spawn, TaskOverrides, TasksModal};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(
|
||||
@@ -95,6 +94,11 @@ fn spawn_task_or_modal(
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
if let Some(provider) = workspace.debugger_provider() {
|
||||
provider.spawn_task_or_modal(workspace, action, window, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
match action {
|
||||
Spawn::ByName {
|
||||
task_name,
|
||||
@@ -143,7 +147,7 @@ pub fn toggle_modal(
|
||||
if can_open_modal {
|
||||
let task_contexts = task_contexts(workspace, window, cx);
|
||||
cx.spawn_in(window, async move |workspace, cx| {
|
||||
let task_contexts = task_contexts.await;
|
||||
let task_contexts = Arc::new(task_contexts.await);
|
||||
workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
@@ -153,6 +157,7 @@ pub fn toggle_modal(
|
||||
reveal_target.map(|target| TaskOverrides {
|
||||
reveal_target: Some(target),
|
||||
}),
|
||||
true,
|
||||
workspace_handle,
|
||||
window,
|
||||
cx,
|
||||
@@ -166,7 +171,7 @@ pub fn toggle_modal(
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_tasks_filtered<F>(
|
||||
pub fn spawn_tasks_filtered<F>(
|
||||
mut predicate: F,
|
||||
overrides: Option<TaskOverrides>,
|
||||
window: &mut Window,
|
||||
|
||||
@@ -37,8 +37,6 @@ smol.workspace = true
|
||||
take-until.workspace = true
|
||||
tempfile.workspace = true
|
||||
unicase.workspace = true
|
||||
unicode-script.workspace = true
|
||||
unicode-segmentation.workspace = true
|
||||
util_macros = { workspace = true, optional = true }
|
||||
walkdir.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -14,7 +14,6 @@ use anyhow::Result;
|
||||
use futures::Future;
|
||||
use itertools::Either;
|
||||
use regex::Regex;
|
||||
use std::num::NonZeroU32;
|
||||
use std::sync::{LazyLock, OnceLock};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
@@ -184,208 +183,29 @@ pub fn truncate_lines_to_byte_limit(s: &str, max_bytes: usize) -> &str {
|
||||
truncate_to_byte_limit(s, max_bytes)
|
||||
}
|
||||
|
||||
fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize {
|
||||
let tab_size = tab_size.get() as usize;
|
||||
let mut width = offset;
|
||||
#[test]
|
||||
fn test_truncate_lines_to_byte_limit() {
|
||||
let text = "Line 1\nLine 2\nLine 3\nLine 4";
|
||||
|
||||
for ch in text.chars() {
|
||||
width += if ch == '\t' {
|
||||
tab_size - (width % tab_size)
|
||||
} else {
|
||||
1
|
||||
};
|
||||
}
|
||||
// Limit that includes all lines
|
||||
assert_eq!(truncate_lines_to_byte_limit(text, 100), text);
|
||||
|
||||
width - offset
|
||||
}
|
||||
// Exactly the first line
|
||||
assert_eq!(truncate_lines_to_byte_limit(text, 7), "Line 1\n");
|
||||
|
||||
/// Tokenizes a string into runs of text that should stick together, or that is whitespace.
|
||||
struct WordBreakingTokenizer<'a> {
|
||||
input: &'a str,
|
||||
}
|
||||
// Limit between lines
|
||||
assert_eq!(truncate_lines_to_byte_limit(text, 13), "Line 1\n");
|
||||
assert_eq!(truncate_lines_to_byte_limit(text, 20), "Line 1\nLine 2\n");
|
||||
|
||||
impl<'a> WordBreakingTokenizer<'a> {
|
||||
fn new(input: &'a str) -> Self {
|
||||
Self { input }
|
||||
}
|
||||
}
|
||||
// Limit before first newline
|
||||
assert_eq!(truncate_lines_to_byte_limit(text, 6), "Line ");
|
||||
|
||||
fn is_char_ideographic(ch: char) -> bool {
|
||||
use unicode_script::Script::*;
|
||||
use unicode_script::UnicodeScript;
|
||||
matches!(ch.script(), Han | Tangut | Yi)
|
||||
}
|
||||
|
||||
fn is_grapheme_ideographic(text: &str) -> bool {
|
||||
text.chars().any(is_char_ideographic)
|
||||
}
|
||||
|
||||
fn is_grapheme_whitespace(text: &str) -> bool {
|
||||
text.chars().any(|x| x.is_whitespace())
|
||||
}
|
||||
|
||||
fn should_stay_with_preceding_ideograph(text: &str) -> bool {
|
||||
text.chars().next().map_or(false, |ch| {
|
||||
matches!(ch, '。' | '、' | ',' | '?' | '!' | ':' | ';' | '…')
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
|
||||
enum WordBreakToken<'a> {
|
||||
Word { token: &'a str, grapheme_len: usize },
|
||||
InlineWhitespace { token: &'a str, grapheme_len: usize },
|
||||
Newline,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for WordBreakingTokenizer<'a> {
|
||||
/// Yields a span, the count of graphemes in the token, and whether it was
|
||||
/// whitespace. Note that it also breaks at word boundaries.
|
||||
type Item = WordBreakToken<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
if self.input.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut iter = self.input.graphemes(true).peekable();
|
||||
let mut offset = 0;
|
||||
let mut grapheme_len = 0;
|
||||
if let Some(first_grapheme) = iter.next() {
|
||||
let is_newline = first_grapheme == "\n";
|
||||
let is_whitespace = is_grapheme_whitespace(first_grapheme);
|
||||
offset += first_grapheme.len();
|
||||
grapheme_len += 1;
|
||||
if is_grapheme_ideographic(first_grapheme) && !is_whitespace {
|
||||
if let Some(grapheme) = iter.peek().copied() {
|
||||
if should_stay_with_preceding_ideograph(grapheme) {
|
||||
offset += grapheme.len();
|
||||
grapheme_len += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut words = self.input[offset..].split_word_bound_indices().peekable();
|
||||
let mut next_word_bound = words.peek().copied();
|
||||
if next_word_bound.map_or(false, |(i, _)| i == 0) {
|
||||
next_word_bound = words.next();
|
||||
}
|
||||
while let Some(grapheme) = iter.peek().copied() {
|
||||
if next_word_bound.map_or(false, |(i, _)| i == offset) {
|
||||
break;
|
||||
};
|
||||
if is_grapheme_whitespace(grapheme) != is_whitespace
|
||||
|| (grapheme == "\n") != is_newline
|
||||
{
|
||||
break;
|
||||
};
|
||||
offset += grapheme.len();
|
||||
grapheme_len += 1;
|
||||
iter.next();
|
||||
}
|
||||
}
|
||||
let token = &self.input[..offset];
|
||||
self.input = &self.input[offset..];
|
||||
if token == "\n" {
|
||||
Some(WordBreakToken::Newline)
|
||||
} else if is_whitespace {
|
||||
Some(WordBreakToken::InlineWhitespace {
|
||||
token,
|
||||
grapheme_len,
|
||||
})
|
||||
} else {
|
||||
Some(WordBreakToken::Word {
|
||||
token,
|
||||
grapheme_len,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wrap_with_prefix(
|
||||
line_prefix: String,
|
||||
unwrapped_text: String,
|
||||
wrap_column: usize,
|
||||
tab_size: NonZeroU32,
|
||||
preserve_existing_whitespace: bool,
|
||||
) -> String {
|
||||
let line_prefix_len = char_len_with_expanded_tabs(0, &line_prefix, tab_size);
|
||||
let mut wrapped_text = String::new();
|
||||
let mut current_line = line_prefix.clone();
|
||||
|
||||
let tokenizer = WordBreakingTokenizer::new(&unwrapped_text);
|
||||
let mut current_line_len = line_prefix_len;
|
||||
let mut in_whitespace = false;
|
||||
for token in tokenizer {
|
||||
let have_preceding_whitespace = in_whitespace;
|
||||
match token {
|
||||
WordBreakToken::Word {
|
||||
token,
|
||||
grapheme_len,
|
||||
} => {
|
||||
in_whitespace = false;
|
||||
if current_line_len + grapheme_len > wrap_column
|
||||
&& current_line_len != line_prefix_len
|
||||
{
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
}
|
||||
current_line.push_str(token);
|
||||
current_line_len += grapheme_len;
|
||||
}
|
||||
WordBreakToken::InlineWhitespace {
|
||||
mut token,
|
||||
mut grapheme_len,
|
||||
} => {
|
||||
in_whitespace = true;
|
||||
if have_preceding_whitespace && !preserve_existing_whitespace {
|
||||
continue;
|
||||
}
|
||||
if !preserve_existing_whitespace {
|
||||
token = " ";
|
||||
grapheme_len = 1;
|
||||
}
|
||||
if current_line_len + grapheme_len > wrap_column {
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
} else if current_line_len != line_prefix_len || preserve_existing_whitespace {
|
||||
current_line.push_str(token);
|
||||
current_line_len += grapheme_len;
|
||||
}
|
||||
}
|
||||
WordBreakToken::Newline => {
|
||||
in_whitespace = true;
|
||||
if preserve_existing_whitespace {
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
} else if have_preceding_whitespace {
|
||||
continue;
|
||||
} else if current_line_len + 1 > wrap_column && current_line_len != line_prefix_len
|
||||
{
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
} else if current_line_len != line_prefix_len {
|
||||
current_line.push(' ');
|
||||
current_line_len += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !current_line.is_empty() {
|
||||
wrapped_text.push_str(¤t_line);
|
||||
}
|
||||
wrapped_text
|
||||
// Test with non-ASCII characters
|
||||
let text_utf8 = "Line 1\nLíne 2\nLine 3";
|
||||
assert_eq!(
|
||||
truncate_lines_to_byte_limit(text_utf8, 15),
|
||||
"Line 1\nLíne 2\n"
|
||||
);
|
||||
}
|
||||
|
||||
pub fn post_inc<T: From<u8> + AddAssign<T> + Copy>(value: &mut T) -> T {
|
||||
@@ -1581,163 +1401,6 @@ Line 3"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_lines_to_byte_limit() {
|
||||
let text = "Line 1\nLine 2\nLine 3\nLine 4";
|
||||
|
||||
// Limit that includes all lines
|
||||
assert_eq!(truncate_lines_to_byte_limit(text, 100), text);
|
||||
|
||||
// Exactly the first line
|
||||
assert_eq!(truncate_lines_to_byte_limit(text, 7), "Line 1\n");
|
||||
|
||||
// Limit between lines
|
||||
assert_eq!(truncate_lines_to_byte_limit(text, 13), "Line 1\n");
|
||||
assert_eq!(truncate_lines_to_byte_limit(text, 20), "Line 1\nLine 2\n");
|
||||
|
||||
// Limit before first newline
|
||||
assert_eq!(truncate_lines_to_byte_limit(text, 6), "Line ");
|
||||
|
||||
// Test with non-ASCII characters
|
||||
let text_utf8 = "Line 1\nLíne 2\nLine 3";
|
||||
assert_eq!(
|
||||
truncate_lines_to_byte_limit(text_utf8, 15),
|
||||
"Line 1\nLíne 2\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_size_with_expanded_tabs() {
|
||||
let nz = |val| NonZeroU32::new(val).unwrap();
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "", nz(4)), 0);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "hello", nz(4)), 5);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "\thello", nz(4)), 9);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "abc\tab", nz(4)), 6);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "hello\t", nz(4)), 8);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "\t\t", nz(8)), 16);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "x\t", nz(8)), 8);
|
||||
assert_eq!(char_len_with_expanded_tabs(7, "x\t", nz(8)), 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_word_breaking_tokenizer() {
|
||||
let tests: &[(&str, &[WordBreakToken<'static>])] = &[
|
||||
("", &[]),
|
||||
(" ", &[whitespace(" ", 2)]),
|
||||
("Ʒ", &[word("Ʒ", 1)]),
|
||||
("Ǽ", &[word("Ǽ", 1)]),
|
||||
("⋑", &[word("⋑", 1)]),
|
||||
("⋑⋑", &[word("⋑⋑", 2)]),
|
||||
(
|
||||
"原理,进而",
|
||||
&[word("原", 1), word("理,", 2), word("进", 1), word("而", 1)],
|
||||
),
|
||||
(
|
||||
"hello world",
|
||||
&[word("hello", 5), whitespace(" ", 1), word("world", 5)],
|
||||
),
|
||||
(
|
||||
"hello, world",
|
||||
&[word("hello,", 6), whitespace(" ", 1), word("world", 5)],
|
||||
),
|
||||
(
|
||||
" hello world",
|
||||
&[
|
||||
whitespace(" ", 2),
|
||||
word("hello", 5),
|
||||
whitespace(" ", 1),
|
||||
word("world", 5),
|
||||
],
|
||||
),
|
||||
(
|
||||
"这是什么 \n 钢笔",
|
||||
&[
|
||||
word("这", 1),
|
||||
word("是", 1),
|
||||
word("什", 1),
|
||||
word("么", 1),
|
||||
whitespace(" ", 1),
|
||||
newline(),
|
||||
whitespace(" ", 1),
|
||||
word("钢", 1),
|
||||
word("笔", 1),
|
||||
],
|
||||
),
|
||||
(" mutton", &[whitespace(" ", 1), word("mutton", 6)]),
|
||||
];
|
||||
|
||||
fn word(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> {
|
||||
WordBreakToken::Word {
|
||||
token,
|
||||
grapheme_len,
|
||||
}
|
||||
}
|
||||
|
||||
fn whitespace(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> {
|
||||
WordBreakToken::InlineWhitespace {
|
||||
token,
|
||||
grapheme_len,
|
||||
}
|
||||
}
|
||||
|
||||
fn newline() -> WordBreakToken<'static> {
|
||||
WordBreakToken::Newline
|
||||
}
|
||||
|
||||
for (input, result) in tests {
|
||||
assert_eq!(
|
||||
WordBreakingTokenizer::new(input)
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice(),
|
||||
*result,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrap_with_prefix() {
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"# ".to_string(),
|
||||
"abcdefg".to_string(),
|
||||
4,
|
||||
NonZeroU32::new(4).unwrap(),
|
||||
false,
|
||||
),
|
||||
"# abcdefg"
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"".to_string(),
|
||||
"\thello world".to_string(),
|
||||
8,
|
||||
NonZeroU32::new(4).unwrap(),
|
||||
false,
|
||||
),
|
||||
"hello\nworld"
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"// ".to_string(),
|
||||
"xx \nyy zz aa bb cc".to_string(),
|
||||
12,
|
||||
NonZeroU32::new(4).unwrap(),
|
||||
false,
|
||||
),
|
||||
"// xx yy zz\n// aa bb cc"
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
String::new(),
|
||||
"这是什么 \n 钢笔".to_string(),
|
||||
3,
|
||||
NonZeroU32::new(4).unwrap(),
|
||||
false,
|
||||
),
|
||||
"这是什\n么 钢\n笔"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_with_ranges() {
|
||||
let input = "hi";
|
||||
|
||||
@@ -56,6 +56,10 @@ impl Workspace {
|
||||
) {
|
||||
let spawn_in_terminal = resolved_task.resolved.clone();
|
||||
if !omit_history {
|
||||
if let Some(debugger_provider) = self.debugger_provider.as_ref() {
|
||||
debugger_provider.task_scheduled(cx);
|
||||
}
|
||||
|
||||
self.project().update(cx, |project, cx| {
|
||||
if let Some(task_inventory) =
|
||||
project.task_store().read(cx).task_inventory().cloned()
|
||||
|
||||
@@ -100,13 +100,13 @@ use task::{DebugScenario, SpawnInTerminal, TaskContext};
|
||||
use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
|
||||
pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||
pub use ui;
|
||||
use ui::prelude::*;
|
||||
use ui::{Window, prelude::*};
|
||||
use util::{ResultExt, TryFutureExt, paths::SanitizedPath, serde::default_true};
|
||||
use uuid::Uuid;
|
||||
pub use workspace_settings::{
|
||||
AutosaveSetting, BottomDockLayout, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings,
|
||||
};
|
||||
use zed_actions::feedback::FileBugReport;
|
||||
use zed_actions::{Spawn, feedback::FileBugReport};
|
||||
|
||||
use crate::notifications::NotificationId;
|
||||
use crate::persistence::{
|
||||
@@ -149,6 +149,18 @@ pub trait DebuggerProvider {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
);
|
||||
|
||||
fn spawn_task_or_modal(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
action: &Spawn,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
);
|
||||
|
||||
fn task_scheduled(&self, cx: &mut App);
|
||||
fn debug_scenario_scheduled(&self, cx: &mut App);
|
||||
fn debug_scenario_scheduled_last(&self, cx: &App) -> bool;
|
||||
}
|
||||
|
||||
actions!(
|
||||
@@ -947,7 +959,7 @@ pub struct Workspace {
|
||||
on_prompt_for_new_path: Option<PromptForNewPath>,
|
||||
on_prompt_for_open_path: Option<PromptForOpenPath>,
|
||||
terminal_provider: Option<Box<dyn TerminalProvider>>,
|
||||
debugger_provider: Option<Box<dyn DebuggerProvider>>,
|
||||
debugger_provider: Option<Arc<dyn DebuggerProvider>>,
|
||||
serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
|
||||
serialized_ssh_project: Option<SerializedSshProject>,
|
||||
_items_serializer: Task<Result<()>>,
|
||||
@@ -1828,7 +1840,11 @@ impl Workspace {
|
||||
}
|
||||
|
||||
pub fn set_debugger_provider(&mut self, provider: impl DebuggerProvider + 'static) {
|
||||
self.debugger_provider = Some(Box::new(provider));
|
||||
self.debugger_provider = Some(Arc::new(provider));
|
||||
}
|
||||
|
||||
pub fn debugger_provider(&self) -> Option<Arc<dyn DebuggerProvider>> {
|
||||
self.debugger_provider.clone()
|
||||
}
|
||||
|
||||
pub fn serialized_ssh_project(&self) -> Option<SerializedSshProject> {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.189.0"
|
||||
version = "0.189.3"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
preview
|
||||
@@ -133,6 +133,46 @@ impl Render for QuickActionBar {
|
||||
)
|
||||
});
|
||||
|
||||
let last_run_debug = self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, cx| {
|
||||
workspace
|
||||
.debugger_provider()
|
||||
.map(|provider| provider.debug_scenario_scheduled_last(cx))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
let run_button = if last_run_debug {
|
||||
QuickActionBarButton::new(
|
||||
"debug",
|
||||
IconName::Debug, // TODO: use debug + play icon
|
||||
false,
|
||||
Box::new(debugger_ui::Start),
|
||||
focus_handle.clone(),
|
||||
"Debug",
|
||||
move |_, window, cx| {
|
||||
window.dispatch_action(Box::new(debugger_ui::Start), cx);
|
||||
},
|
||||
)
|
||||
} else {
|
||||
let action = Box::new(tasks_ui::Spawn::ViaModal {
|
||||
reveal_target: None,
|
||||
});
|
||||
QuickActionBarButton::new(
|
||||
"run",
|
||||
IconName::Play,
|
||||
false,
|
||||
action.boxed_clone(),
|
||||
focus_handle.clone(),
|
||||
"Spawn Task",
|
||||
move |_, window, cx| {
|
||||
window.dispatch_action(action.boxed_clone(), cx);
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
let assistant_button = QuickActionBarButton::new(
|
||||
"toggle inline assistant",
|
||||
IconName::ZedAssistant,
|
||||
@@ -561,6 +601,7 @@ impl Render for QuickActionBar {
|
||||
AgentSettings::get_global(cx).enabled && AgentSettings::get_global(cx).button,
|
||||
|bar| bar.child(assistant_button),
|
||||
)
|
||||
.child(run_button)
|
||||
.children(code_actions_dropdown)
|
||||
.children(editor_selections_dropdown)
|
||||
.child(editor_settings_dropdown)
|
||||
|
||||
@@ -28,15 +28,15 @@ These adapters enable Zed to provide a consistent debugging experience across mu
|
||||
|
||||
## Getting Started
|
||||
|
||||
For basic debugging you can set up a new configuration by opening the `New Session Modal` either via the `debugger: start` (default: f4) or clicking the plus icon at the top right of the debug panel.
|
||||
For basic debugging, you can set up a new configuration by opening the `New Session Modal` either via the `debugger: start` (default: f4) or by clicking the plus icon at the top right of the debug panel.
|
||||
|
||||
For more advanced use cases you can create debug configurations by directly editing the `.zed/debug.json` file in your project root directory.
|
||||
For more advanced use cases, you can create debug configurations by directly editing the `.zed/debug.json` file in your project root directory.
|
||||
|
||||
You can then use the `New Session Modal` to select a configuration then start debugging.
|
||||
You can then use the `New Session Modal` to select a configuration and start debugging.
|
||||
|
||||
### Configuration
|
||||
|
||||
While configuration fields are debug adapter dependent, most adapters support the following fields.
|
||||
While configuration fields are debug adapter-dependent, most adapters support the following fields:
|
||||
|
||||
```json
|
||||
[
|
||||
@@ -58,22 +58,114 @@ While configuration fields are debug adapter dependent, most adapters support th
|
||||
]
|
||||
```
|
||||
|
||||
#### Task Variables
|
||||
#### Tasks
|
||||
|
||||
All configuration fields support task variables. See [Tasks](./tasks.md)
|
||||
All configuration fields support task variables. See [Tasks Variables](./tasks.md#variables)
|
||||
|
||||
Zed also allows embedding a task that is run before the debugger starts. This is useful for setting up the environment or running any necessary setup steps before the debugger starts.
|
||||
|
||||
See an example [here](#build-binary-then-debug)
|
||||
|
||||
#### Python Examples
|
||||
|
||||
##### Python Active File
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"label": "Active File",
|
||||
"adapter": "Debugpy",
|
||||
"program": "$ZED_FILE",
|
||||
"request": "launch"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
##### Flask App
|
||||
|
||||
For a common Flask Application with a file structure similar to the following:
|
||||
|
||||
- .venv/
|
||||
- app/
|
||||
- **init**.py
|
||||
- **main**.py
|
||||
- routes.py
|
||||
- templates/
|
||||
- index.html
|
||||
- static/
|
||||
- style.css
|
||||
- requirements.txt
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"label": "Python: Flask",
|
||||
"adapter": "Debugpy",
|
||||
"request": "launch",
|
||||
"module": "app",
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
"env": {
|
||||
"FLASK_APP": "app",
|
||||
"FLASK_DEBUG": "1"
|
||||
},
|
||||
"args": [
|
||||
"run",
|
||||
"--reload", // Enables Flask reloader that watches for file changes
|
||||
"--debugger" // Enables Flask debugger
|
||||
],
|
||||
"autoReload": {
|
||||
"enable": true
|
||||
},
|
||||
"jinja": true,
|
||||
"justMyCode": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Rust/C++/C
|
||||
|
||||
##### Using pre-built binary
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"label": "Debug native binary",
|
||||
"program": "$ZED_WORKTREE_ROOT/build/binary",
|
||||
"request": "launch",
|
||||
"adapter": "CodeLLDB" // GDB is available on non arm macs as well as linux
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
##### Build binary then debug
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"label": "Build & Debug native binary",
|
||||
"build": {
|
||||
"command": "cargo",
|
||||
"args": ["build"]
|
||||
},
|
||||
"program": "$ZED_WORKTREE_ROOT/target/debug/binary",
|
||||
"request": "launch",
|
||||
"adapter": "CodeLLDB" // GDB is available on non arm macs as well as linux
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Breakpoints
|
||||
|
||||
Zed currently supports these types of breakpoints
|
||||
Zed currently supports these types of breakpoints:
|
||||
|
||||
- Standard Breakpoints: Stop at the breakpoint when it's hit
|
||||
- Log Breakpoints: Output a log message instead of stopping at the breakpoint when it's hit
|
||||
- Conditional Breakpoints: Stop at the breakpoint when it's hit if the condition is met
|
||||
- Hit Breakpoints: Stop at the breakpoint when it's hit a certain number of times
|
||||
|
||||
Standard breakpoints can be toggled by left clicking on the editor gutter or using the Toggle Breakpoint action. Right clicking on a breakpoint or on a code runner symbol brings up the breakpoint context menu. This has options for toggling breakpoints and editing log breakpoints.
|
||||
Standard breakpoints can be toggled by left-clicking on the editor gutter or using the Toggle Breakpoint action. Right-clicking on a breakpoint or on a code runner symbol brings up the breakpoint context menu. This has options for toggling breakpoints and editing log breakpoints.
|
||||
|
||||
Other kinds of breakpoints can be toggled/edited by right clicking on the breakpoint icon in the gutter and selecting the desired option.
|
||||
Other kinds of breakpoints can be toggled/edited by right-clicking on the breakpoint icon in the gutter and selecting the desired option.
|
||||
|
||||
## Settings
|
||||
|
||||
@@ -81,8 +173,8 @@ Other kinds of breakpoints can be toggled/edited by right clicking on the breakp
|
||||
- `save_breakpoints`: Whether the breakpoints should be reused across Zed sessions.
|
||||
- `button`: Whether to show the debug button in the status bar.
|
||||
- `timeout`: Time in milliseconds until timeout error when connecting to a TCP debug adapter.
|
||||
- `log_dap_communications`: Whether to log messages between active debug adapters and Zed
|
||||
- `format_dap_log_messages`: Whether to format dap messages in when adding them to debug adapter logger
|
||||
- `log_dap_communications`: Whether to log messages between active debug adapters and Zed.
|
||||
- `format_dap_log_messages`: Whether to format DAP messages when adding them to the debug adapter logger.
|
||||
|
||||
### Stepping granularity
|
||||
|
||||
@@ -163,7 +255,7 @@ Other kinds of breakpoints can be toggled/edited by right clicking on the breakp
|
||||
### Timeout
|
||||
|
||||
- Description: Time in milliseconds until timeout error when connecting to a TCP debug adapter.
|
||||
- Default: 2000ms
|
||||
- Default: 2000
|
||||
- Setting: debugger.timeout
|
||||
|
||||
**Options**
|
||||
@@ -198,7 +290,7 @@ Other kinds of breakpoints can be toggled/edited by right clicking on the breakp
|
||||
|
||||
### Format Dap Log Messages
|
||||
|
||||
- Description: Whether to format dap messages in when adding them to debug adapter logger. (Used for DAP development)
|
||||
- Description: Whether to format DAP messages when adding them to the debug adapter logger. (Used for DAP development)
|
||||
- Default: false
|
||||
- Setting: debugger.format_dap_log_messages
|
||||
|
||||
@@ -218,8 +310,5 @@ Other kinds of breakpoints can be toggled/edited by right clicking on the breakp
|
||||
|
||||
The Debugger supports the following theme options:
|
||||
|
||||
/// Color used to accent some of the debugger's elements
|
||||
/// Only accents breakpoint & breakpoint related symbols right now
|
||||
|
||||
**debugger.accent**: Color used to accent breakpoint & breakpoint related symbols
|
||||
**debugger.accent**: Color used to accent breakpoint & breakpoint-related symbols
|
||||
**editor.debugger_active_line.background**: Background color of active debug line
|
||||
|
||||
Reference in New Issue
Block a user