From f27b508e4becc473afcec3733c82528358753457 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 2 Jul 2025 23:31:32 +0300 Subject: [PATCH 001/239] Improve workspace bindings (#33765) * Add a "close item"-like binding to close the active dock, if present Now, cmd/ctrl-w can be used close the focused dock before the Zed window * Add defaults to MoveItem* actions to make it appear in the command palette Release Notes: - N/A --- assets/keymaps/default-linux.json | 4 +++- assets/keymaps/default-macos.json | 3 ++- crates/workspace/src/pane.rs | 3 +++ crates/workspace/src/workspace.rs | 22 ++++++++++++++++++++-- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index f0ed829c1ec9237eec78386ab88b151ab59ad8f5..6f5094582880dcac7da0f564f0ac9a87287df1ae 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -605,7 +605,9 @@ // "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }] // or by tag: // "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }], - "f5": "debugger::Rerun" + "f5": "debugger::Rerun", + "ctrl-f4": "workspace::CloseActiveDock", + "ctrl-w": "workspace::CloseActiveDock" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index cd986e12e9c94de78b503f26a3bb89b9307446d5..71b7a3978960d0e41dcea2a1157704c1f58ba96a 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -659,7 +659,8 @@ "cmd-k shift-up": "workspace::SwapPaneUp", "cmd-k shift-down": "workspace::SwapPaneDown", "cmd-shift-x": "zed::Extensions", - "f5": "debugger::Rerun" + "f5": "debugger::Rerun", + "cmd-w": "workspace::CloseActiveDock" } }, { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index cb2dd99f5e69842d64c62ff78911799aa323c14a..b002d3ebe73d011fcc077fe72c21b64f0658dadb 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -103,6 +103,7 @@ pub struct ActivateItem(pub usize); #[action(namespace = pane)] #[serde(deny_unknown_fields)] pub struct CloseActiveItem { + #[serde(default)] pub save_intent: Option, #[serde(default)] pub close_pinned: bool, @@ -112,6 +113,7 @@ pub struct CloseActiveItem { #[action(namespace = pane)] #[serde(deny_unknown_fields)] pub struct CloseInactiveItems { + #[serde(default)] pub save_intent: Option, #[serde(default)] pub close_pinned: bool, @@ -121,6 +123,7 @@ pub struct CloseInactiveItems { #[action(namespace = pane)] #[serde(deny_unknown_fields)] pub struct CloseAllItems { + #[serde(default)] pub save_intent: Option, #[serde(default)] pub close_pinned: bool, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d31a7831ee4bfbfe9adf2aa4fa34889ce0aafea3..95e0a4aa686e68c05923b3e6937c593bfb19cf8d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -224,6 +224,7 @@ pub struct ActivatePane(pub usize); #[action(namespace = workspace)] #[serde(deny_unknown_fields)] pub struct MoveItemToPane { + #[serde(default = "default_1")] pub destination: usize, #[serde(default = "default_true")] pub focus: bool, @@ -231,10 +232,15 @@ pub struct MoveItemToPane { pub clone: bool, } +fn default_1() -> usize { + 1 +} + #[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)] #[action(namespace = workspace)] #[serde(deny_unknown_fields)] pub struct MoveItemToPaneInDirection { + #[serde(default = "default_right")] pub direction: SplitDirection, #[serde(default = "default_true")] pub focus: bool, @@ -242,10 +248,15 @@ pub struct MoveItemToPaneInDirection { pub clone: bool, } +fn default_right() -> SplitDirection { + SplitDirection::Right +} + #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = workspace)] #[serde(deny_unknown_fields)] pub struct SaveAll { + #[serde(default)] pub save_intent: Option, } @@ -253,6 +264,7 @@ pub struct SaveAll { #[action(namespace = workspace)] #[serde(deny_unknown_fields)] pub struct Save { + #[serde(default)] pub save_intent: Option, } @@ -260,6 +272,7 @@ pub struct Save { #[action(namespace = workspace)] #[serde(deny_unknown_fields)] pub struct CloseAllItemsAndPanes { + #[serde(default)] pub save_intent: Option, } @@ -267,6 +280,7 @@ pub struct CloseAllItemsAndPanes { #[action(namespace = workspace)] #[serde(deny_unknown_fields)] pub struct CloseInactiveTabsAndPanes { + #[serde(default)] pub save_intent: Option, } @@ -2806,12 +2820,14 @@ impl Workspace { }) } - fn close_active_dock(&mut self, window: &mut Window, cx: &mut Context) { + fn close_active_dock(&mut self, window: &mut Window, cx: &mut Context) -> bool { if let Some(dock) = self.active_dock(window, cx) { dock.update(cx, |dock, cx| { dock.set_open(false, window, cx); }); + return true; } + false } pub fn close_all_docks(&mut self, window: &mut Window, cx: &mut Context) { @@ -5450,7 +5466,9 @@ impl Workspace { )) .on_action(cx.listener( |workspace: &mut Workspace, _: &CloseActiveDock, window, cx| { - workspace.close_active_dock(window, cx); + if !workspace.close_active_dock(window, cx) { + cx.propagate(); + } }, )) .on_action( From 0553dc0d4910e4e1559e0624d6d8f224146b6b7b Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Wed, 2 Jul 2025 22:32:07 +0200 Subject: [PATCH 002/239] agent: Fix issue with duplicated tool names from MCP servers (#33811) Closes #33792 Follow up to #33237 - Turns out my fix for this was not correct Release Notes: - agent: Fixed an issue where tools would not work when two MCP servers provided a tool with the same name --- crates/agent/src/agent_profile.rs | 18 +- crates/agent/src/thread.rs | 233 +---------- crates/agent/src/thread_store.rs | 24 +- crates/agent_ui/src/tool_compatibility.rs | 4 +- crates/assistant_tool/Cargo.toml | 1 + crates/assistant_tool/src/tool_working_set.rs | 377 +++++++++++++++++- 6 files changed, 385 insertions(+), 272 deletions(-) diff --git a/crates/agent/src/agent_profile.rs b/crates/agent/src/agent_profile.rs index 2c3b457dc2eef593085bb63ccb42fd70082163b3..a89857e71a6b8ed0f4e7a397be2bcd1bce4b1d7a 100644 --- a/crates/agent/src/agent_profile.rs +++ b/crates/agent/src/agent_profile.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use agent_settings::{AgentProfileId, AgentProfileSettings, AgentSettings}; -use assistant_tool::{Tool, ToolSource, ToolWorkingSet}; +use assistant_tool::{Tool, ToolSource, ToolWorkingSet, UniqueToolName}; use collections::IndexMap; use convert_case::{Case, Casing}; use fs::Fs; @@ -72,7 +72,7 @@ impl AgentProfile { &self.id } - pub fn enabled_tools(&self, cx: &App) -> Vec> { + pub fn enabled_tools(&self, cx: &App) -> Vec<(UniqueToolName, Arc)> { let Some(settings) = AgentSettings::get_global(cx).profiles.get(&self.id) else { return Vec::new(); }; @@ -81,7 +81,7 @@ impl AgentProfile { .read(cx) .tools(cx) .into_iter() - .filter(|tool| Self::is_enabled(settings, tool.source(), tool.name())) + .filter(|(_, tool)| Self::is_enabled(settings, tool.source(), tool.name())) .collect() } @@ -137,7 +137,7 @@ mod tests { let mut enabled_tools = cx .read(|cx| profile.enabled_tools(cx)) .into_iter() - .map(|tool| tool.name()) + .map(|(_, tool)| tool.name()) .collect::>(); enabled_tools.sort(); @@ -174,7 +174,7 @@ mod tests { let mut enabled_tools = cx .read(|cx| profile.enabled_tools(cx)) .into_iter() - .map(|tool| tool.name()) + .map(|(_, tool)| tool.name()) .collect::>(); enabled_tools.sort(); @@ -207,7 +207,7 @@ mod tests { let mut enabled_tools = cx .read(|cx| profile.enabled_tools(cx)) .into_iter() - .map(|tool| tool.name()) + .map(|(_, tool)| tool.name()) .collect::>(); enabled_tools.sort(); @@ -267,10 +267,10 @@ mod tests { } fn default_tool_set(cx: &mut TestAppContext) -> Entity { - cx.new(|_| { + cx.new(|cx| { let mut tool_set = ToolWorkingSet::default(); - tool_set.insert(Arc::new(FakeTool::new("enabled_mcp_tool", "mcp"))); - tool_set.insert(Arc::new(FakeTool::new("disabled_mcp_tool", "mcp"))); + tool_set.insert(Arc::new(FakeTool::new("enabled_mcp_tool", "mcp")), cx); + tool_set.insert(Arc::new(FakeTool::new("disabled_mcp_tool", "mcp")), cx); tool_set }) } diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 028dabbd912ab1e58273bea6302d77baf4e635b8..815b9e86ea8a7c4c0879e81028c4ee42e3a84ca8 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -13,7 +13,7 @@ use anyhow::{Result, anyhow}; use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet}; use chrono::{DateTime, Utc}; use client::{ModelRequestUsage, RequestUsage}; -use collections::{HashMap, HashSet}; +use collections::HashMap; use feature_flags::{self, FeatureFlagAppExt}; use futures::{FutureExt, StreamExt as _, future::Shared}; use git::repository::DiffType; @@ -960,13 +960,14 @@ impl Thread { model: Arc, ) -> Vec { if model.supports_tools() { - resolve_tool_name_conflicts(self.profile.enabled_tools(cx).as_slice()) + self.profile + .enabled_tools(cx) .into_iter() .filter_map(|(name, tool)| { // Skip tools that cannot be supported let input_schema = tool.input_schema(model.tool_input_format()).ok()?; Some(LanguageModelRequestTool { - name, + name: name.into(), description: tool.description(), input_schema, }) @@ -2386,7 +2387,7 @@ impl Thread { let tool_list = available_tools .iter() - .map(|tool| format!("- {}: {}", tool.name(), tool.description())) + .map(|(name, tool)| format!("- {}: {}", name, tool.description())) .collect::>() .join("\n"); @@ -2606,7 +2607,7 @@ impl Thread { .profile .enabled_tools(cx) .iter() - .map(|tool| tool.name()) + .map(|(name, _)| name.clone().into()) .collect(); self.message_feedback.insert(message_id, feedback); @@ -3144,85 +3145,6 @@ struct PendingCompletion { _task: Task<()>, } -/// Resolves tool name conflicts by ensuring all tool names are unique. -/// -/// When multiple tools have the same name, this function applies the following rules: -/// 1. Native tools always keep their original name -/// 2. Context server tools get prefixed with their server ID and an underscore -/// 3. All tool names are truncated to MAX_TOOL_NAME_LENGTH (64 characters) -/// 4. If conflicts still exist after prefixing, the conflicting tools are filtered out -/// -/// Note: This function assumes that built-in tools occur before MCP tools in the tools list. -fn resolve_tool_name_conflicts(tools: &[Arc]) -> Vec<(String, Arc)> { - fn resolve_tool_name(tool: &Arc) -> String { - let mut tool_name = tool.name(); - tool_name.truncate(MAX_TOOL_NAME_LENGTH); - tool_name - } - - const MAX_TOOL_NAME_LENGTH: usize = 64; - - let mut duplicated_tool_names = HashSet::default(); - let mut seen_tool_names = HashSet::default(); - for tool in tools { - let tool_name = resolve_tool_name(tool); - if seen_tool_names.contains(&tool_name) { - debug_assert!( - tool.source() != assistant_tool::ToolSource::Native, - "There are two built-in tools with the same name: {}", - tool_name - ); - duplicated_tool_names.insert(tool_name); - } else { - seen_tool_names.insert(tool_name); - } - } - - if duplicated_tool_names.is_empty() { - return tools - .into_iter() - .map(|tool| (resolve_tool_name(tool), tool.clone())) - .collect(); - } - - tools - .into_iter() - .filter_map(|tool| { - let mut tool_name = resolve_tool_name(tool); - if !duplicated_tool_names.contains(&tool_name) { - return Some((tool_name, tool.clone())); - } - match tool.source() { - assistant_tool::ToolSource::Native => { - // Built-in tools always keep their original name - Some((tool_name, tool.clone())) - } - assistant_tool::ToolSource::ContextServer { id } => { - // Context server tools are prefixed with the context server ID, and truncated if necessary - tool_name.insert(0, '_'); - if tool_name.len() + id.len() > MAX_TOOL_NAME_LENGTH { - let len = MAX_TOOL_NAME_LENGTH - tool_name.len(); - let mut id = id.to_string(); - id.truncate(len); - tool_name.insert_str(0, &id); - } else { - tool_name.insert_str(0, &id); - } - - tool_name.truncate(MAX_TOOL_NAME_LENGTH); - - if seen_tool_names.contains(&tool_name) { - log::error!("Cannot resolve tool name conflict for tool {}", tool.name()); - None - } else { - Some((tool_name, tool.clone())) - } - } - } - }) - .collect() -} - #[cfg(test)] mod tests { use super::*; @@ -3238,7 +3160,6 @@ mod tests { use futures::future::BoxFuture; use futures::stream::BoxStream; use gpui::TestAppContext; - use icons::IconName; use language_model::fake_provider::{FakeLanguageModel, FakeLanguageModelProvider}; use language_model::{ LanguageModelCompletionError, LanguageModelName, LanguageModelProviderId, @@ -3883,148 +3804,6 @@ fn main() {{ }); } - #[gpui::test] - fn test_resolve_tool_name_conflicts() { - use assistant_tool::{Tool, ToolSource}; - - assert_resolve_tool_name_conflicts( - vec![ - TestTool::new("tool1", ToolSource::Native), - TestTool::new("tool2", ToolSource::Native), - TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }), - ], - vec!["tool1", "tool2", "tool3"], - ); - - assert_resolve_tool_name_conflicts( - vec![ - TestTool::new("tool1", ToolSource::Native), - TestTool::new("tool2", ToolSource::Native), - TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }), - TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }), - ], - vec!["tool1", "tool2", "mcp-1_tool3", "mcp-2_tool3"], - ); - - assert_resolve_tool_name_conflicts( - vec![ - TestTool::new("tool1", ToolSource::Native), - TestTool::new("tool2", ToolSource::Native), - TestTool::new("tool3", ToolSource::Native), - TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }), - TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }), - ], - vec!["tool1", "tool2", "tool3", "mcp-1_tool3", "mcp-2_tool3"], - ); - - // Test that tool with very long name is always truncated - assert_resolve_tool_name_conflicts( - vec![TestTool::new( - "tool-with-more-then-64-characters-blah-blah-blah-blah-blah-blah-blah-blah", - ToolSource::Native, - )], - vec!["tool-with-more-then-64-characters-blah-blah-blah-blah-blah-blah-"], - ); - - // Test deduplication of tools with very long names, in this case the mcp server name should be truncated - assert_resolve_tool_name_conflicts( - vec![ - TestTool::new("tool-with-very-very-very-long-name", ToolSource::Native), - TestTool::new( - "tool-with-very-very-very-long-name", - ToolSource::ContextServer { - id: "mcp-with-very-very-very-long-name".into(), - }, - ), - ], - vec![ - "tool-with-very-very-very-long-name", - "mcp-with-very-very-very-long-_tool-with-very-very-very-long-name", - ], - ); - - fn assert_resolve_tool_name_conflicts( - tools: Vec, - expected: Vec>, - ) { - let tools: Vec> = tools - .into_iter() - .map(|t| Arc::new(t) as Arc) - .collect(); - let tools = resolve_tool_name_conflicts(&tools); - assert_eq!(tools.len(), expected.len()); - for (i, expected_name) in expected.into_iter().enumerate() { - let expected_name = expected_name.into(); - let actual_name = &tools[i].0; - assert_eq!( - actual_name, &expected_name, - "Expected '{}' got '{}' at index {}", - expected_name, actual_name, i - ); - } - } - - struct TestTool { - name: String, - source: ToolSource, - } - - impl TestTool { - fn new(name: impl Into, source: ToolSource) -> Self { - Self { - name: name.into(), - source, - } - } - } - - impl Tool for TestTool { - fn name(&self) -> String { - self.name.clone() - } - - fn icon(&self) -> IconName { - IconName::Ai - } - - fn may_perform_edits(&self) -> bool { - false - } - - fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool { - true - } - - fn source(&self) -> ToolSource { - self.source.clone() - } - - fn description(&self) -> String { - "Test tool".to_string() - } - - fn ui_text(&self, _input: &serde_json::Value) -> String { - "Test tool".to_string() - } - - fn run( - self: Arc, - _input: serde_json::Value, - _request: Arc, - _project: Entity, - _action_log: Entity, - _model: Arc, - _window: Option, - _cx: &mut App, - ) -> assistant_tool::ToolResult { - assistant_tool::ToolResult { - output: Task::ready(Err(anyhow::anyhow!("No content"))), - card: None, - } - } - } - } - // Helper to create a model that returns errors enum TestError { Overloaded, diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index 516151e9ff90dd6dc4a3e4b3dd5eff37522db7f2..0347156cd4df0d8b5d953def949739cab1135025 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -6,7 +6,7 @@ use crate::{ }; use agent_settings::{AgentProfileId, CompletionMode}; use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{ToolId, ToolWorkingSet}; +use assistant_tool::{Tool, ToolId, ToolWorkingSet}; use chrono::{DateTime, Utc}; use collections::HashMap; use context_server::ContextServerId; @@ -537,8 +537,8 @@ impl ThreadStore { } ContextServerStatus::Stopped | ContextServerStatus::Error(_) => { if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) { - tool_working_set.update(cx, |tool_working_set, _| { - tool_working_set.remove(&tool_ids); + tool_working_set.update(cx, |tool_working_set, cx| { + tool_working_set.remove(&tool_ids, cx); }); } } @@ -569,19 +569,17 @@ impl ThreadStore { .log_err() { let tool_ids = tool_working_set - .update(cx, |tool_working_set, _| { - response - .tools - .into_iter() - .map(|tool| { - log::info!("registering context server tool: {:?}", tool.name); - tool_working_set.insert(Arc::new(ContextServerTool::new( + .update(cx, |tool_working_set, cx| { + tool_working_set.extend( + response.tools.into_iter().map(|tool| { + Arc::new(ContextServerTool::new( context_server_store.clone(), server.id(), tool, - ))) - }) - .collect::>() + )) as Arc + }), + cx, + ) }) .log_err(); diff --git a/crates/agent_ui/src/tool_compatibility.rs b/crates/agent_ui/src/tool_compatibility.rs index 936612e556cb782cc1fbee3cbfa5ff3b95607679..d4e1da5bb0a532c8307364582349378d98c51a26 100644 --- a/crates/agent_ui/src/tool_compatibility.rs +++ b/crates/agent_ui/src/tool_compatibility.rs @@ -42,8 +42,8 @@ impl IncompatibleToolsState { .profile() .enabled_tools(cx) .iter() - .filter(|tool| tool.input_schema(model.tool_input_format()).is_err()) - .cloned() + .filter(|(_, tool)| tool.input_schema(model.tool_input_format()).is_err()) + .map(|(_, tool)| tool.clone()) .collect() }) } diff --git a/crates/assistant_tool/Cargo.toml b/crates/assistant_tool/Cargo.toml index a8df1131c67e4dcf4716d24be55a16e94e30e7c7..5a54e86eac15c2846e7e72ee45b47ab014cd69e6 100644 --- a/crates/assistant_tool/Cargo.toml +++ b/crates/assistant_tool/Cargo.toml @@ -22,6 +22,7 @@ gpui.workspace = true icons.workspace = true language.workspace = true language_model.workspace = true +log.workspace = true parking_lot.workspace = true project.workspace = true regex.workspace = true diff --git a/crates/assistant_tool/src/tool_working_set.rs b/crates/assistant_tool/src/tool_working_set.rs index c72c52ba7a668ca31c91242872d7ef0c4834fb17..9a6ec49914eea3cd22f014ce2a5c014d1dca1220 100644 --- a/crates/assistant_tool/src/tool_working_set.rs +++ b/crates/assistant_tool/src/tool_working_set.rs @@ -1,18 +1,52 @@ -use std::sync::Arc; - -use collections::{HashMap, IndexMap}; -use gpui::App; +use std::{borrow::Borrow, sync::Arc}; use crate::{Tool, ToolRegistry, ToolSource}; +use collections::{HashMap, HashSet, IndexMap}; +use gpui::{App, SharedString}; +use util::debug_panic; #[derive(Copy, Clone, PartialEq, Eq, Hash, Default)] pub struct ToolId(usize); +/// A unique identifier for a tool within a working set. +#[derive(Clone, PartialEq, Eq, Hash, Default)] +pub struct UniqueToolName(SharedString); + +impl Borrow for UniqueToolName { + fn borrow(&self) -> &str { + &self.0 + } +} + +impl From for UniqueToolName { + fn from(value: String) -> Self { + UniqueToolName(SharedString::new(value)) + } +} + +impl Into for UniqueToolName { + fn into(self) -> String { + self.0.into() + } +} + +impl std::fmt::Debug for UniqueToolName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl std::fmt::Display for UniqueToolName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.as_ref()) + } +} + /// A working set of tools for use in one instance of the Assistant Panel. #[derive(Default)] pub struct ToolWorkingSet { context_server_tools_by_id: HashMap>, - context_server_tools_by_name: HashMap>, + context_server_tools_by_name: HashMap>, next_tool_id: ToolId, } @@ -24,16 +58,20 @@ impl ToolWorkingSet { .or_else(|| ToolRegistry::global(cx).tool(name)) } - pub fn tools(&self, cx: &App) -> Vec> { - let mut tools = ToolRegistry::global(cx).tools(); - tools.extend(self.context_server_tools_by_id.values().cloned()); + pub fn tools(&self, cx: &App) -> Vec<(UniqueToolName, Arc)> { + let mut tools = ToolRegistry::global(cx) + .tools() + .into_iter() + .map(|tool| (UniqueToolName(tool.name().into()), tool)) + .collect::>(); + tools.extend(self.context_server_tools_by_name.clone()); tools } pub fn tools_by_source(&self, cx: &App) -> IndexMap>> { let mut tools_by_source = IndexMap::default(); - for tool in self.tools(cx) { + for (_, tool) in self.tools(cx) { tools_by_source .entry(tool.source()) .or_insert_with(Vec::new) @@ -49,27 +87,324 @@ impl ToolWorkingSet { tools_by_source } - pub fn insert(&mut self, tool: Arc) -> ToolId { + pub fn insert(&mut self, tool: Arc, cx: &App) -> ToolId { + let tool_id = self.register_tool(tool); + self.tools_changed(cx); + tool_id + } + + pub fn extend(&mut self, tools: impl Iterator>, cx: &App) -> Vec { + let ids = tools.map(|tool| self.register_tool(tool)).collect(); + self.tools_changed(cx); + ids + } + + pub fn remove(&mut self, tool_ids_to_remove: &[ToolId], cx: &App) { + self.context_server_tools_by_id + .retain(|id, _| !tool_ids_to_remove.contains(id)); + self.tools_changed(cx); + } + + fn register_tool(&mut self, tool: Arc) -> ToolId { let tool_id = self.next_tool_id; self.next_tool_id.0 += 1; self.context_server_tools_by_id .insert(tool_id, tool.clone()); - self.tools_changed(); tool_id } - pub fn remove(&mut self, tool_ids_to_remove: &[ToolId]) { - self.context_server_tools_by_id - .retain(|id, _| !tool_ids_to_remove.contains(id)); - self.tools_changed(); + fn tools_changed(&mut self, cx: &App) { + self.context_server_tools_by_name = resolve_context_server_tool_name_conflicts( + &self + .context_server_tools_by_id + .values() + .cloned() + .collect::>(), + &ToolRegistry::global(cx).tools(), + ); + } +} + +fn resolve_context_server_tool_name_conflicts( + context_server_tools: &[Arc], + native_tools: &[Arc], +) -> HashMap> { + fn resolve_tool_name(tool: &Arc) -> String { + let mut tool_name = tool.name(); + tool_name.truncate(MAX_TOOL_NAME_LENGTH); + tool_name } - fn tools_changed(&mut self) { - self.context_server_tools_by_name.clear(); - self.context_server_tools_by_name.extend( - self.context_server_tools_by_id - .values() - .map(|tool| (tool.name(), tool.clone())), + const MAX_TOOL_NAME_LENGTH: usize = 64; + + let mut duplicated_tool_names = HashSet::default(); + let mut seen_tool_names = HashSet::default(); + seen_tool_names.extend(native_tools.iter().map(|tool| tool.name())); + for tool in context_server_tools { + let tool_name = resolve_tool_name(tool); + if seen_tool_names.contains(&tool_name) { + debug_assert!( + tool.source() != ToolSource::Native, + "Expected MCP tool but got a native tool: {}", + tool_name + ); + duplicated_tool_names.insert(tool_name); + } else { + seen_tool_names.insert(tool_name); + } + } + + if duplicated_tool_names.is_empty() { + return context_server_tools + .into_iter() + .map(|tool| (resolve_tool_name(tool).into(), tool.clone())) + .collect(); + } + + context_server_tools + .into_iter() + .filter_map(|tool| { + let mut tool_name = resolve_tool_name(tool); + if !duplicated_tool_names.contains(&tool_name) { + return Some((tool_name.into(), tool.clone())); + } + match tool.source() { + ToolSource::Native => { + debug_panic!("Expected MCP tool but got a native tool: {}", tool_name); + // Built-in tools always keep their original name + Some((tool_name.into(), tool.clone())) + } + ToolSource::ContextServer { id } => { + // Context server tools are prefixed with the context server ID, and truncated if necessary + tool_name.insert(0, '_'); + if tool_name.len() + id.len() > MAX_TOOL_NAME_LENGTH { + let len = MAX_TOOL_NAME_LENGTH - tool_name.len(); + let mut id = id.to_string(); + id.truncate(len); + tool_name.insert_str(0, &id); + } else { + tool_name.insert_str(0, &id); + } + + tool_name.truncate(MAX_TOOL_NAME_LENGTH); + + if seen_tool_names.contains(&tool_name) { + log::error!("Cannot resolve tool name conflict for tool {}", tool.name()); + None + } else { + Some((tool_name.into(), tool.clone())) + } + } + } + }) + .collect() +} +#[cfg(test)] +mod tests { + use gpui::{AnyWindowHandle, Entity, Task, TestAppContext}; + use language_model::{LanguageModel, LanguageModelRequest}; + use project::Project; + + use crate::{ActionLog, ToolResult}; + + use super::*; + + #[gpui::test] + fn test_unique_tool_names(cx: &mut TestAppContext) { + fn assert_tool( + tool_working_set: &ToolWorkingSet, + unique_name: &str, + expected_name: &str, + expected_source: ToolSource, + cx: &App, + ) { + let tool = tool_working_set.tool(unique_name, cx).unwrap(); + assert_eq!(tool.name(), expected_name); + assert_eq!(tool.source(), expected_source); + } + + let tool_registry = cx.update(ToolRegistry::default_global); + tool_registry.register_tool(TestTool::new("tool1", ToolSource::Native)); + tool_registry.register_tool(TestTool::new("tool2", ToolSource::Native)); + + let mut tool_working_set = ToolWorkingSet::default(); + cx.update(|cx| { + tool_working_set.extend( + vec![ + Arc::new(TestTool::new( + "tool2", + ToolSource::ContextServer { id: "mcp-1".into() }, + )) as Arc, + Arc::new(TestTool::new( + "tool2", + ToolSource::ContextServer { id: "mcp-2".into() }, + )) as Arc, + ] + .into_iter(), + cx, + ); + }); + + cx.update(|cx| { + assert_tool(&tool_working_set, "tool1", "tool1", ToolSource::Native, cx); + assert_tool(&tool_working_set, "tool2", "tool2", ToolSource::Native, cx); + assert_tool( + &tool_working_set, + "mcp-1_tool2", + "tool2", + ToolSource::ContextServer { id: "mcp-1".into() }, + cx, + ); + assert_tool( + &tool_working_set, + "mcp-2_tool2", + "tool2", + ToolSource::ContextServer { id: "mcp-2".into() }, + cx, + ); + }) + } + + #[gpui::test] + fn test_resolve_context_server_tool_name_conflicts() { + assert_resolve_context_server_tool_name_conflicts( + vec![ + TestTool::new("tool1", ToolSource::Native), + TestTool::new("tool2", ToolSource::Native), + ], + vec![TestTool::new( + "tool3", + ToolSource::ContextServer { id: "mcp-1".into() }, + )], + vec!["tool3"], ); + + assert_resolve_context_server_tool_name_conflicts( + vec![ + TestTool::new("tool1", ToolSource::Native), + TestTool::new("tool2", ToolSource::Native), + ], + vec![ + TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }), + TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }), + ], + vec!["mcp-1_tool3", "mcp-2_tool3"], + ); + + assert_resolve_context_server_tool_name_conflicts( + vec![ + TestTool::new("tool1", ToolSource::Native), + TestTool::new("tool2", ToolSource::Native), + TestTool::new("tool3", ToolSource::Native), + ], + vec![ + TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }), + TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }), + ], + vec!["mcp-1_tool3", "mcp-2_tool3"], + ); + + // Test deduplication of tools with very long names, in this case the mcp server name should be truncated + assert_resolve_context_server_tool_name_conflicts( + vec![TestTool::new( + "tool-with-very-very-very-long-name", + ToolSource::Native, + )], + vec![TestTool::new( + "tool-with-very-very-very-long-name", + ToolSource::ContextServer { + id: "mcp-with-very-very-very-long-name".into(), + }, + )], + vec!["mcp-with-very-very-very-long-_tool-with-very-very-very-long-name"], + ); + + fn assert_resolve_context_server_tool_name_conflicts( + builtin_tools: Vec, + context_server_tools: Vec, + expected: Vec<&'static str>, + ) { + let context_server_tools: Vec> = context_server_tools + .into_iter() + .map(|t| Arc::new(t) as Arc) + .collect(); + let builtin_tools: Vec> = builtin_tools + .into_iter() + .map(|t| Arc::new(t) as Arc) + .collect(); + let tools = + resolve_context_server_tool_name_conflicts(&context_server_tools, &builtin_tools); + assert_eq!(tools.len(), expected.len()); + for (i, (name, _)) in tools.into_iter().enumerate() { + assert_eq!( + name.0.as_ref(), + expected[i], + "Expected '{}' got '{}' at index {}", + expected[i], + name, + i + ); + } + } + } + + struct TestTool { + name: String, + source: ToolSource, + } + + impl TestTool { + fn new(name: impl Into, source: ToolSource) -> Self { + Self { + name: name.into(), + source, + } + } + } + + impl Tool for TestTool { + fn name(&self) -> String { + self.name.clone() + } + + fn icon(&self) -> icons::IconName { + icons::IconName::Ai + } + + fn may_perform_edits(&self) -> bool { + false + } + + fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool { + true + } + + fn source(&self) -> ToolSource { + self.source.clone() + } + + fn description(&self) -> String { + "Test tool".to_string() + } + + fn ui_text(&self, _input: &serde_json::Value) -> String { + "Test tool".to_string() + } + + fn run( + self: Arc, + _input: serde_json::Value, + _request: Arc, + _project: Entity, + _action_log: Entity, + _model: Arc, + _window: Option, + _cx: &mut App, + ) -> ToolResult { + ToolResult { + output: Task::ready(Err(anyhow::anyhow!("No content"))), + card: None, + } + } } } From 610f4605d8bace594ba0176443b6dff2b0b813c9 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 2 Jul 2025 16:36:25 -0400 Subject: [PATCH 003/239] Switch to `ctrl-f11` for `debugger::StepInto` on macOS (#33799) Plain `f11` is a system keybinding. We already use `ctrl-f11` for this on Linux. Release Notes: - debugger: Switched the macOS keybinding for `debugger::StepInto` from `f11` to `ctrl-f11`. --- assets/keymaps/default-macos.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 71b7a3978960d0e41dcea2a1157704c1f58ba96a..cbc90c05e6fcd2a9c4b697efca6f3241125f47bc 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -8,7 +8,7 @@ "shift-cmd-f5": "debugger::RerunSession", "f6": "debugger::Pause", "f7": "debugger::StepOver", - "f11": "debugger::StepInto", + "ctrl-f11": "debugger::StepInto", "shift-f11": "debugger::StepOut", "home": "menu::SelectFirst", "shift-pageup": "menu::SelectFirst", From e224da852250d11a77fb6e757d3db306de91ed9c Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 2 Jul 2025 16:37:36 -0400 Subject: [PATCH 004/239] Disambiguate package.json tasks by parent directory as needed (#33798) Closes #33701, cc @afgomez Release Notes: - Added the parent directory to the label as needed to disambiguate tasks from package.json --- crates/languages/src/json.rs | 6 ++-- crates/languages/src/typescript.rs | 57 ++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index bd950f34f5bda478ea6c6a0a3f0e85b25dd54e49..6f51cdadbaae62c1f806b614d9624061cd324c62 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -8,7 +8,8 @@ use futures::StreamExt; use gpui::{App, AsyncApp, Task}; use http_client::github::{GitHubLspBinaryVersion, latest_github_release}; use language::{ - ContextProvider, LanguageRegistry, LanguageToolchainStore, LspAdapter, LspAdapterDelegate, + ContextProvider, LanguageRegistry, LanguageToolchainStore, LocalFile as _, LspAdapter, + LspAdapterDelegate, }; use lsp::{LanguageServerBinary, LanguageServerName}; use node_runtime::NodeRuntime; @@ -65,13 +66,14 @@ impl ContextProvider for JsonTaskProvider { .ok()? .await .ok()?; + let path = cx.update(|cx| file.abs_path(cx)).ok()?.as_path().into(); let task_templates = if is_package_json { let package_json = serde_json_lenient::from_str::< HashMap, >(&contents.text) .ok()?; - let package_json = PackageJsonData::new(file.path.clone(), package_json); + let package_json = PackageJsonData::new(path, package_json); let command = package_json.package_manager.unwrap_or("npm").to_owned(); package_json .scripts diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index 4a9626c8b82b49abd54344a53c5ed5177f94393c..32c45dfa886358124e4331420c2bcc3c8a349514 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -221,15 +221,30 @@ impl PackageJsonData { }); } + let script_name_counts: HashMap<_, usize> = + self.scripts + .iter() + .fold(HashMap::default(), |mut acc, (_, script)| { + *acc.entry(script).or_default() += 1; + acc + }); for (path, script) in &self.scripts { + let label = if script_name_counts.get(script).copied().unwrap_or_default() > 1 + && let Some(parent) = path.parent().and_then(|parent| parent.file_name()) + { + let parent = parent.to_string_lossy(); + format!("{parent}/package.json > {script}") + } else { + format!("package.json > {script}") + }; task_templates.0.push(TaskTemplate { - label: format!("package.json > {script}",), + label, command: TYPESCRIPT_RUNNER_VARIABLE.template_value(), args: vec!["run".to_owned(), script.to_owned()], tags: vec!["package-script".into()], cwd: Some( path.parent() - .unwrap_or(Path::new("")) + .unwrap_or(Path::new("/")) .to_string_lossy() .to_string(), ), @@ -1014,6 +1029,7 @@ mod tests { use language::language_settings; use project::{FakeFs, Project}; use serde_json::json; + use task::TaskTemplates; use unindent::Unindent; use util::path; @@ -1135,5 +1151,42 @@ mod tests { package_manager: None, } ); + + let mut task_templates = TaskTemplates::default(); + package_json_data.fill_task_templates(&mut task_templates); + let task_templates = task_templates + .0 + .into_iter() + .map(|template| (template.label, template.cwd)) + .collect::>(); + pretty_assertions::assert_eq!( + task_templates, + [ + ( + "vitest file test".into(), + Some("$ZED_CUSTOM_TYPESCRIPT_VITEST_PACKAGE_PATH".into()), + ), + ( + "vitest test $ZED_SYMBOL".into(), + Some("$ZED_CUSTOM_TYPESCRIPT_VITEST_PACKAGE_PATH".into()), + ), + ( + "mocha file test".into(), + Some("$ZED_CUSTOM_TYPESCRIPT_MOCHA_PACKAGE_PATH".into()), + ), + ( + "mocha test $ZED_SYMBOL".into(), + Some("$ZED_CUSTOM_TYPESCRIPT_MOCHA_PACKAGE_PATH".into()), + ), + ( + "root/package.json > test".into(), + Some(path!("/root").into()) + ), + ( + "sub/package.json > test".into(), + Some(path!("/root/sub").into()) + ), + ] + ); } } From 82fac9da82f4fc7c665ea24308b0e3d4ed6dd347 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 2 Jul 2025 16:37:52 -0400 Subject: [PATCH 005/239] debugger: Always use runtimeExecutable for node-terminal scenarios (#33794) cc @afgomez Release Notes: - debugger: Fixed `node-terminal` debug configurations not working with some commands. --- crates/dap_adapters/src/javascript.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index fd48c599591680e6f75178996efa77087e4784fb..23a378cdf9a2cc4779e9aed44538f04483a3dc56 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -79,9 +79,9 @@ impl JsDebugAdapter { let command = configuration.get("command")?.as_str()?.to_owned(); let mut args = shlex::split(&command)?.into_iter(); let program = args.next()?; - configuration.insert("program".to_owned(), program.into()); + configuration.insert("runtimeExecutable".to_owned(), program.into()); configuration.insert( - "args".to_owned(), + "runtimeArgs".to_owned(), args.map(Value::from).collect::>().into(), ); configuration.insert("console".to_owned(), "externalTerminal".into()); From fbc42567324eedde343e557b732d68cfd545e4e2 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 2 Jul 2025 22:55:42 +0200 Subject: [PATCH 006/239] debugger: Fix update inline values on settings change (#33808) Release Notes: - Debugger: fixed that inline values would not update (hide/show) on settings change --- crates/editor/src/editor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 69b9158c31a7279b2222dd150e2fbbd4f8268224..fb3d76d3440fa874194d82089064563f3d7e9a69 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -19842,6 +19842,7 @@ impl Editor { self.tasks_update_task = Some(self.refresh_runnables(window, cx)); self.update_edit_prediction_settings(cx); self.refresh_inline_completion(true, false, window, cx); + self.refresh_inline_values(cx); self.refresh_inlay_hints( InlayHintRefreshReason::SettingsChange(inlay_hint_settings( self.selections.newest_anchor().head(), From 77c4530e128caded51ef66e3931cee11435dc5c4 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 2 Jul 2025 19:48:49 -0300 Subject: [PATCH 007/239] Add refinements to the keymap UI (#33816) This includes mostly polishing up the keystroke editing modal, and some other bits like making the keystroke rendering function more composable. Release Notes: - Added refinements to the keymap UI design. --------- Co-authored-by: Ben Kunkle Co-authored-by: Ben Kunkle --- crates/settings_ui/src/keybindings.rs | 215 ++++++++++-------- crates/ui/src/components/keybinding.rs | 124 ++++++---- crates/ui/src/components/keybinding_hint.rs | 2 +- .../ui/src/components/stories/keybinding.rs | 53 +++-- 4 files changed, 232 insertions(+), 162 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 480614bcabfd45c47cdf910efcec3a9599b85928..e725d2907fdd773cd57a266aa1f8100fd715c2f5 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -8,8 +8,8 @@ use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ AppContext as _, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, - FontWeight, Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, StyledText, - Subscription, WeakEntity, actions, div, transparent_black, + Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, StyledText, Subscription, + WeakEntity, actions, div, transparent_black, }; use language::{Language, LanguageConfig}; use settings::KeybindSource; @@ -18,7 +18,7 @@ use util::ResultExt; use ui::{ ActiveTheme as _, App, BorrowAppContext, ContextMenu, ParentElement as _, Render, SharedString, - Styled as _, Window, prelude::*, right_click_menu, + Styled as _, Tooltip, Window, prelude::*, right_click_menu, }; use workspace::{Item, ModalView, SerializableItem, Workspace, register_serializable_item}; @@ -145,7 +145,7 @@ impl KeymapEditor { let filter_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Filter action names...", cx); + editor.set_placeholder_text("Filter action names…", cx); editor }); @@ -248,7 +248,7 @@ impl KeymapEditor { let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx); let ui_key_binding = Some( - ui::KeyBinding::new(key_binding.clone(), cx) + ui::KeyBinding::new_from_gpui(key_binding.clone(), cx) .vim_mode(source == Some(settings::KeybindSource::Vim)), ); @@ -571,7 +571,7 @@ impl Render for KeymapEditor { let row_count = self.matches.len(); let theme = cx.theme(); - div() + v_flex() .key_context(self.dispatch_context(window, cx)) .on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_previous)) @@ -586,9 +586,9 @@ impl Render for KeymapEditor { .bg(theme.colors().editor_background) .id("keymap-editor") .track_focus(&self.focus_handle) + .pt_4() .px_4() - .v_flex() - .pb_4() + .gap_4() .child( h_flex() .key_context({ @@ -596,12 +596,13 @@ impl Render for KeymapEditor { context.add("BufferSearchBar"); context }) - .w_full() - .h_12() - .px_4() - .my_4() - .border_2() + .h_8() + .pl_2() + .pr_1() + .py_1() + .border_1() .border_color(theme.colors().border) + .rounded_lg() .child(self.filter_editor.clone()), ) .child( @@ -742,7 +743,7 @@ impl RenderOnce for SyntaxHighlightedText { struct KeybindingEditorModal { editing_keybind: ProcessedKeybinding, - keybind_editor: Entity, + keybind_editor: Entity, fs: Arc, error: Option, } @@ -764,7 +765,7 @@ impl KeybindingEditorModal { _window: &mut Window, cx: &mut App, ) -> Self { - let keybind_editor = cx.new(KeybindInput::new); + let keybind_editor = cx.new(KeystrokeInput::new); Self { editing_keybind, fs, @@ -777,84 +778,65 @@ impl KeybindingEditorModal { impl Render for KeybindingEditorModal { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let theme = cx.theme().colors(); + return v_flex() - .gap_4() .w(rems(36.)) + .elevation_3(cx) .child( v_flex() - .items_center() - .text_center() - .bg(theme.background) - .border_color(theme.border) - .border_2() + .pt_2() .px_4() - .py_2() + .pb_4() + .gap_2() + .child(Label::new("Input desired keystroke, then hit save")) + .child(self.keybind_editor.clone()), + ) + .child( + h_flex() + .p_2() .w_full() + .gap_1() + .justify_end() + .border_t_1() + .border_color(cx.theme().colors().border_variant) .child( - div() - .text_lg() - .font_weight(FontWeight::BOLD) - .child("Input desired keybinding, then hit save"), + Button::new("cancel", "Cancel") + .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), ) .child( - h_flex() - .w_full() - .child(self.keybind_editor.clone()) - .child( - IconButton::new("backspace-btn", ui::IconName::Backspace).on_click( - cx.listener(|this, _event, _window, cx| { - this.keybind_editor.update(cx, |editor, cx| { - editor.keystrokes.pop(); + Button::new("save-btn", "Save Keybinding").on_click(cx.listener( + |this, _event, _window, cx| { + let existing_keybind = this.editing_keybind.clone(); + let fs = this.fs.clone(); + let new_keystrokes = this + .keybind_editor + .read_with(cx, |editor, _| editor.keystrokes.clone()); + if new_keystrokes.is_empty() { + this.error = Some("Keystrokes cannot be empty".to_string()); + cx.notify(); + return; + } + let tab_size = + cx.global::().json_tab_size(); + cx.spawn(async move |this, cx| { + if let Err(err) = save_keybinding_update( + existing_keybind, + &new_keystrokes, + &fs, + tab_size, + ) + .await + { + this.update(cx, |this, cx| { + this.error = Some(err.to_string()); cx.notify(); }) - }), - ), - ) - .child(IconButton::new("clear-btn", ui::IconName::Eraser).on_click( - cx.listener(|this, _event, _window, cx| { - this.keybind_editor.update(cx, |editor, cx| { - editor.keystrokes.clear(); - cx.notify(); - }) - }), - )), - ) - .child( - h_flex().w_full().items_center().justify_center().child( - Button::new("save-btn", "Save") - .label_size(LabelSize::Large) - .on_click(cx.listener(|this, _event, _window, cx| { - let existing_keybind = this.editing_keybind.clone(); - let fs = this.fs.clone(); - let new_keystrokes = this - .keybind_editor - .read_with(cx, |editor, _| editor.keystrokes.clone()); - if new_keystrokes.is_empty() { - this.error = Some("Keystrokes cannot be empty".to_string()); - cx.notify(); - return; + .log_err(); } - let tab_size = - cx.global::().json_tab_size(); - cx.spawn(async move |this, cx| { - if let Err(err) = save_keybinding_update( - existing_keybind, - &new_keystrokes, - &fs, - tab_size, - ) - .await - { - this.update(cx, |this, cx| { - this.error = Some(err.to_string()); - cx.notify(); - }) - .log_err(); - } - }) - .detach(); - })), - ), + }) + .detach(); + }, + )), ), ) .when_some(self.error.clone(), |this, error| { @@ -879,11 +861,13 @@ async fn save_keybinding_update( let keymap_contents = settings::KeymapFile::load_keymap_file(fs) .await .context("Failed to load keymap file")?; + let existing_keystrokes = existing .ui_key_binding .as_ref() - .map(|keybinding| keybinding.key_binding.keystrokes()) + .map(|keybinding| keybinding.keystrokes.as_slice()) .unwrap_or_default(); + let context = existing .context .as_ref() @@ -927,12 +911,12 @@ async fn save_keybinding_update( Ok(()) } -struct KeybindInput { +struct KeystrokeInput { keystrokes: Vec, focus_handle: FocusHandle, } -impl KeybindInput { +impl KeystrokeInput { fn new(cx: &mut Context) -> Self { let focus_handle = cx.focus_handle(); Self { @@ -1007,16 +991,18 @@ impl KeybindInput { } } -impl Focusable for KeybindInput { +impl Focusable for KeystrokeInput { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() } } -impl Render for KeybindInput { +impl Render for KeystrokeInput { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let colors = cx.theme().colors(); - return div() + + return h_flex() + .id("keybinding_input") .track_focus(&self.focus_handle) .on_modifiers_changed(cx.listener(Self::on_modifiers_changed)) .on_key_down(cx.listener(Self::on_key_down)) @@ -1025,16 +1011,55 @@ impl Render for KeybindInput { style.border_color = Some(colors.border_focused); style }) - .h_12() + .py_2() + .px_3() + .gap_2() + .min_h_8() .w_full() + .justify_between() .bg(colors.editor_background) - .border_2() - .border_color(colors.border) - .p_4() - .flex_row() - .text_center() - .justify_center() - .child(ui::text_for_keystrokes(&self.keystrokes, cx)); + .border_1() + .rounded_md() + .flex_1() + .overflow_hidden() + .child( + h_flex() + .w_full() + .min_w_0() + .justify_center() + .flex_wrap() + .gap(ui::DynamicSpacing::Base04.rems(cx)) + .children(self.keystrokes.iter().map(|keystroke| { + h_flex().children(ui::render_keystroke( + keystroke, + None, + Some(rems(0.875).into()), + ui::PlatformStyle::platform(), + false, + )) + })), + ) + .child( + h_flex() + .gap_0p5() + .flex_none() + .child( + IconButton::new("backspace-btn", IconName::Delete) + .tooltip(Tooltip::text("Delete Keystroke")) + .on_click(cx.listener(|this, _event, _window, cx| { + this.keystrokes.pop(); + cx.notify(); + })), + ) + .child( + IconButton::new("clear-btn", IconName::Eraser) + .tooltip(Tooltip::text("Clear Keystrokes")) + .on_click(cx.listener(|this, _event, _window, cx| { + this.keystrokes.clear(); + cx.notify(); + })), + ), + ); } } diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index 6da3d03ea1869d1bff0558b510838544c419be1a..1d91492f26c7e9e93a761a1d9d46b06300ba3614 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -13,7 +13,7 @@ pub struct KeyBinding { /// More than one keystroke produces a chord. /// /// This should always contain at least one keystroke. - pub key_binding: gpui::KeyBinding, + pub keystrokes: Vec, /// The [`PlatformStyle`] to use when displaying this keybinding. platform_style: PlatformStyle, @@ -37,7 +37,7 @@ impl KeyBinding { return Self::for_action_in(action, &focused, window, cx); } let key_binding = window.highest_precedence_binding_for_action(action)?; - Some(Self::new(key_binding, cx)) + Some(Self::new_from_gpui(key_binding, cx)) } /// Like `for_action`, but lets you specify the context from which keybindings are matched. @@ -48,7 +48,7 @@ impl KeyBinding { cx: &App, ) -> Option { let key_binding = window.highest_precedence_binding_for_action_in(action, focus)?; - Some(Self::new(key_binding, cx)) + Some(Self::new_from_gpui(key_binding, cx)) } pub fn set_vim_mode(cx: &mut App, enabled: bool) { @@ -59,9 +59,9 @@ impl KeyBinding { cx.try_global::().is_some_and(|g| g.0) } - pub fn new(key_binding: gpui::KeyBinding, cx: &App) -> Self { + pub fn new(keystrokes: Vec, cx: &App) -> Self { Self { - key_binding, + keystrokes, platform_style: PlatformStyle::platform(), size: None, vim_mode: KeyBinding::is_vim_mode(cx), @@ -69,6 +69,10 @@ impl KeyBinding { } } + pub fn new_from_gpui(key_binding: gpui::KeyBinding, cx: &App) -> Self { + Self::new(key_binding.keystrokes().to_vec(), cx) + } + /// Sets the [`PlatformStyle`] for this [`KeyBinding`]. pub fn platform_style(mut self, platform_style: PlatformStyle) -> Self { self.platform_style = platform_style; @@ -92,15 +96,20 @@ impl KeyBinding { self.vim_mode = enabled; self } +} - fn render_key(&self, keystroke: &Keystroke, color: Option) -> AnyElement { - let key_icon = icon_for_key(keystroke, self.platform_style); - match key_icon { - Some(icon) => KeyIcon::new(icon, color).size(self.size).into_any_element(), - None => { - let key = util::capitalize(&keystroke.key); - Key::new(&key, color).size(self.size).into_any_element() - } +fn render_key( + keystroke: &Keystroke, + color: Option, + platform_style: PlatformStyle, + size: impl Into>, +) -> AnyElement { + let key_icon = icon_for_key(keystroke, platform_style); + match key_icon { + Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(), + None => { + let key = util::capitalize(&keystroke.key); + Key::new(&key, color).size(size).into_any_element() } } } @@ -108,17 +117,12 @@ impl KeyBinding { impl RenderOnce for KeyBinding { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let color = self.disabled.then_some(Color::Disabled); - let use_text = self.vim_mode - || matches!( - self.platform_style, - PlatformStyle::Linux | PlatformStyle::Windows - ); + h_flex() .debug_selector(|| { format!( "KEY_BINDING-{}", - self.key_binding - .keystrokes() + self.keystrokes .iter() .map(|k| k.key.to_string()) .collect::>() @@ -127,35 +131,56 @@ impl RenderOnce for KeyBinding { }) .gap(DynamicSpacing::Base04.rems(cx)) .flex_none() - .children(self.key_binding.keystrokes().iter().map(|keystroke| { + .children(self.keystrokes.iter().map(|keystroke| { h_flex() .flex_none() .py_0p5() .rounded_xs() .text_color(cx.theme().colors().text_muted) - .when(use_text, |el| { - el.child( - Key::new( - keystroke_text(&keystroke, self.platform_style, self.vim_mode), - color, - ) - .size(self.size), - ) - }) - .when(!use_text, |el| { - el.children(render_modifiers( - &keystroke.modifiers, - self.platform_style, - color, - self.size, - true, - )) - .map(|el| el.child(self.render_key(&keystroke, color))) - }) + .children(render_keystroke( + keystroke, + color, + self.size, + self.platform_style, + self.vim_mode, + )) })) } } +pub fn render_keystroke( + keystroke: &Keystroke, + color: Option, + size: impl Into>, + platform_style: PlatformStyle, + vim_mode: bool, +) -> Vec { + let use_text = vim_mode + || matches!( + platform_style, + PlatformStyle::Linux | PlatformStyle::Windows + ); + let size = size.into(); + + if use_text { + let element = Key::new(keystroke_text(&keystroke, platform_style, vim_mode), color) + .size(size) + .into_any_element(); + vec![element] + } else { + let mut elements = Vec::new(); + elements.extend(render_modifiers( + &keystroke.modifiers, + platform_style, + color, + size, + true, + )); + elements.push(render_key(&keystroke, color, platform_style, size)); + elements + } +} + fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option { match keystroke.key.as_str() { "left" => Some(IconName::ArrowLeft), @@ -466,7 +491,7 @@ impl Component for KeyBinding { vec![ single_example( "Default", - KeyBinding::new( + KeyBinding::new_from_gpui( gpui::KeyBinding::new("ctrl-s", gpui::NoAction, None), cx, ) @@ -474,7 +499,7 @@ impl Component for KeyBinding { ), single_example( "Mac Style", - KeyBinding::new( + KeyBinding::new_from_gpui( gpui::KeyBinding::new("cmd-s", gpui::NoAction, None), cx, ) @@ -483,7 +508,7 @@ impl Component for KeyBinding { ), single_example( "Windows Style", - KeyBinding::new( + KeyBinding::new_from_gpui( gpui::KeyBinding::new("ctrl-s", gpui::NoAction, None), cx, ) @@ -496,9 +521,12 @@ impl Component for KeyBinding { "Vim Mode", vec![single_example( "Vim Mode Enabled", - KeyBinding::new(gpui::KeyBinding::new("dd", gpui::NoAction, None), cx) - .vim_mode(true) - .into_any_element(), + KeyBinding::new_from_gpui( + gpui::KeyBinding::new("dd", gpui::NoAction, None), + cx, + ) + .vim_mode(true) + .into_any_element(), )], ), example_group_with_title( @@ -506,7 +534,7 @@ impl Component for KeyBinding { vec![ single_example( "Multiple Keys", - KeyBinding::new( + KeyBinding::new_from_gpui( gpui::KeyBinding::new("ctrl-k ctrl-b", gpui::NoAction, None), cx, ) @@ -514,7 +542,7 @@ impl Component for KeyBinding { ), single_example( "With Shift", - KeyBinding::new( + KeyBinding::new_from_gpui( gpui::KeyBinding::new("shift-cmd-p", gpui::NoAction, None), cx, ) diff --git a/crates/ui/src/components/keybinding_hint.rs b/crates/ui/src/components/keybinding_hint.rs index 4c8c89363612d0abef45eaf4c4c3e92ee67f54c0..d6dc094d415bec9991b83dfc50a865a838c1bdf4 100644 --- a/crates/ui/src/components/keybinding_hint.rs +++ b/crates/ui/src/components/keybinding_hint.rs @@ -216,7 +216,7 @@ impl Component for KeybindingHint { fn preview(window: &mut Window, cx: &mut App) -> Option { let enter_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None); let enter = KeyBinding::for_action(&menu::Confirm, window, cx) - .unwrap_or(KeyBinding::new(enter_fallback, cx)); + .unwrap_or(KeyBinding::new_from_gpui(enter_fallback, cx)); let bg_color = cx.theme().colors().surface_background; diff --git a/crates/ui/src/components/stories/keybinding.rs b/crates/ui/src/components/stories/keybinding.rs index 1b47870468e9b19262cf890daf58e173f8755ebd..594f70b6ab0fbafc5e997785c44c494b71320d72 100644 --- a/crates/ui/src/components/stories/keybinding.rs +++ b/crates/ui/src/components/stories/keybinding.rs @@ -18,16 +18,16 @@ impl Render for KeybindingStory { Story::container(cx) .child(Story::title_for::(cx)) .child(Story::label("Single Key", cx)) - .child(KeyBinding::new(binding("Z"), cx)) + .child(KeyBinding::new_from_gpui(binding("Z"), cx)) .child(Story::label("Single Key with Modifier", cx)) .child( div() .flex() .gap_3() - .child(KeyBinding::new(binding("ctrl-c"), cx)) - .child(KeyBinding::new(binding("alt-c"), cx)) - .child(KeyBinding::new(binding("cmd-c"), cx)) - .child(KeyBinding::new(binding("shift-c"), cx)), + .child(KeyBinding::new_from_gpui(binding("ctrl-c"), cx)) + .child(KeyBinding::new_from_gpui(binding("alt-c"), cx)) + .child(KeyBinding::new_from_gpui(binding("cmd-c"), cx)) + .child(KeyBinding::new_from_gpui(binding("shift-c"), cx)), ) .child(Story::label("Single Key with Modifier (Permuted)", cx)) .child( @@ -41,42 +41,59 @@ impl Render for KeybindingStory { .gap_4() .py_3() .children(chunk.map(|permutation| { - KeyBinding::new(binding(&(permutation.join("-") + "-x")), cx) + KeyBinding::new_from_gpui( + binding(&(permutation.join("-") + "-x")), + cx, + ) })) }), ), ) .child(Story::label("Single Key with All Modifiers", cx)) - .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z"), cx)) + .child(KeyBinding::new_from_gpui( + binding("ctrl-alt-cmd-shift-z"), + cx, + )) .child(Story::label("Chord", cx)) - .child(KeyBinding::new(binding("a z"), cx)) + .child(KeyBinding::new_from_gpui(binding("a z"), cx)) .child(Story::label("Chord with Modifier", cx)) - .child(KeyBinding::new(binding("ctrl-a shift-z"), cx)) - .child(KeyBinding::new(binding("fn-s"), cx)) + .child(KeyBinding::new_from_gpui(binding("ctrl-a shift-z"), cx)) + .child(KeyBinding::new_from_gpui(binding("fn-s"), cx)) .child(Story::label("Single Key with All Modifiers (Linux)", cx)) .child( - KeyBinding::new(binding("ctrl-alt-cmd-shift-z"), cx) + KeyBinding::new_from_gpui(binding("ctrl-alt-cmd-shift-z"), cx) .platform_style(PlatformStyle::Linux), ) .child(Story::label("Chord (Linux)", cx)) - .child(KeyBinding::new(binding("a z"), cx).platform_style(PlatformStyle::Linux)) + .child( + KeyBinding::new_from_gpui(binding("a z"), cx).platform_style(PlatformStyle::Linux), + ) .child(Story::label("Chord with Modifier (Linux)", cx)) .child( - KeyBinding::new(binding("ctrl-a shift-z"), cx).platform_style(PlatformStyle::Linux), + KeyBinding::new_from_gpui(binding("ctrl-a shift-z"), cx) + .platform_style(PlatformStyle::Linux), + ) + .child( + KeyBinding::new_from_gpui(binding("fn-s"), cx).platform_style(PlatformStyle::Linux), ) - .child(KeyBinding::new(binding("fn-s"), cx).platform_style(PlatformStyle::Linux)) .child(Story::label("Single Key with All Modifiers (Windows)", cx)) .child( - KeyBinding::new(binding("ctrl-alt-cmd-shift-z"), cx) + KeyBinding::new_from_gpui(binding("ctrl-alt-cmd-shift-z"), cx) .platform_style(PlatformStyle::Windows), ) .child(Story::label("Chord (Windows)", cx)) - .child(KeyBinding::new(binding("a z"), cx).platform_style(PlatformStyle::Windows)) + .child( + KeyBinding::new_from_gpui(binding("a z"), cx) + .platform_style(PlatformStyle::Windows), + ) .child(Story::label("Chord with Modifier (Windows)", cx)) .child( - KeyBinding::new(binding("ctrl-a shift-z"), cx) + KeyBinding::new_from_gpui(binding("ctrl-a shift-z"), cx) + .platform_style(PlatformStyle::Windows), + ) + .child( + KeyBinding::new_from_gpui(binding("fn-s"), cx) .platform_style(PlatformStyle::Windows), ) - .child(KeyBinding::new(binding("fn-s"), cx).platform_style(PlatformStyle::Windows)) } } From 32d058d95e72e3c5b8bd0fb880514786cc873763 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Wed, 2 Jul 2025 18:21:39 -0600 Subject: [PATCH 008/239] Fix remote server (ssh) crash when editing json (#33818) Closes #33807 Release Notes: - (Preview Only) Fixes a remote server (ssh) crash when editing json files --------- Co-authored-by: Cole --- crates/language/src/language_settings.rs | 5 ++- crates/settings/src/settings_json.rs | 39 +++++++++--------------- crates/theme/src/settings.rs | 21 ++++++------- 3 files changed, 27 insertions(+), 38 deletions(-) diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index ff3a7ffcb488e68aa1ef32fbae10cfd12d73def0..1caa6eceec0ebc3b6ff0ea3cb1ee33d5e2ec6e86 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -321,7 +321,7 @@ inventory::submit! { let language_settings_content_ref = generator .subschema_for::() .to_value(); - let schema = json_schema!({ + replace_subschema::(generator, || json_schema!({ "type": "object", "properties": params .language_names @@ -333,8 +333,7 @@ inventory::submit! { ) }) .collect::>() - }); - replace_subschema::(generator, schema) + })) } } } diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs index ebf32c2948ce7d4451ff35cd72fba7ba9c52d368..d78043a3354ffd8cf25be0f95c27df5cf1f8f8a8 100644 --- a/crates/settings/src/settings_json.rs +++ b/crates/settings/src/settings_json.rs @@ -23,35 +23,26 @@ inventory::collect!(ParameterizedJsonSchema); const DEFS_PATH: &str = "#/$defs/"; -/// Replaces the JSON schema definition for some type, and returns a reference to it. +/// Replaces the JSON schema definition for some type if it is in use (in the definitions list), and +/// returns a reference to it. +/// +/// This asserts that JsonSchema::schema_name() + "2" does not exist because this indicates that +/// there are multiple types that use this name, and unfortunately schemars APIs do not support +/// resolving this ambiguity - see https://github.com/GREsau/schemars/issues/449 +/// +/// This takes a closure for `schema` because some settings types are not available on the remote +/// server, and so will crash when attempting to access e.g. GlobalThemeRegistry. pub fn replace_subschema( generator: &mut schemars::SchemaGenerator, - schema: schemars::Schema, + schema: impl Fn() -> schemars::Schema, ) -> schemars::Schema { - // The key in definitions may not match T::schema_name() if multiple types have the same name. - // This is a workaround for there being no straightforward way to get the key used for a type - - // see https://github.com/GREsau/schemars/issues/449 - let ref_schema = generator.subschema_for::(); - if let Some(serde_json::Value::String(definition_pointer)) = ref_schema.get("$ref") { - if let Some(definition_name) = definition_pointer.strip_prefix(DEFS_PATH) { - generator - .definitions_mut() - .insert(definition_name.to_string(), schema.to_value()); - return ref_schema; - } else { - log::error!( - "bug: expected `$ref` field to start with {DEFS_PATH}, \ - got {definition_pointer}" - ); - } - } else { - log::error!("bug: expected `$ref` field in result of `subschema_for`"); - } // fallback on just using the schema name, which could collide. let schema_name = T::schema_name(); - generator - .definitions_mut() - .insert(schema_name.to_string(), schema.to_value()); + let definitions = generator.definitions_mut(); + assert!(!definitions.contains_key(&format!("{schema_name}2"))); + if definitions.contains_key(schema_name.as_ref()) { + definitions.insert(schema_name.to_string(), schema().to_value()); + } Schema::new_ref(format!("{DEFS_PATH}{schema_name}")) } diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 42012e080ca82f7fef487916e144ec79a30f9d84..ca59eba76672a6036533bd620dfca0fb69ab44f5 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -978,11 +978,10 @@ pub struct ThemeName(pub Arc); inventory::submit! { ParameterizedJsonSchema { add_and_get_ref: |generator, _params, cx| { - let schema = json_schema!({ + replace_subschema::(generator, || json_schema!({ "type": "string", "enum": ThemeRegistry::global(cx).list_names(), - }); - replace_subschema::(generator, schema) + })) } } } @@ -996,15 +995,14 @@ pub struct IconThemeName(pub Arc); inventory::submit! { ParameterizedJsonSchema { add_and_get_ref: |generator, _params, cx| { - let schema = json_schema!({ + replace_subschema::(generator, || json_schema!({ "type": "string", "enum": ThemeRegistry::global(cx) .list_icon_themes() .into_iter() .map(|icon_theme| icon_theme.name) .collect::>(), - }); - replace_subschema::(generator, schema) + })) } } } @@ -1018,11 +1016,12 @@ pub struct FontFamilyName(pub Arc); inventory::submit! { ParameterizedJsonSchema { add_and_get_ref: |generator, params, _cx| { - let schema = json_schema!({ - "type": "string", - "enum": params.font_names, - }); - replace_subschema::(generator, schema) + replace_subschema::(generator, || { + json_schema!({ + "type": "string", + "enum": params.font_names, + }) + }) } } } From def8bab5a82dbdaa91a58c7661ab8c079b680fba Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 2 Jul 2025 21:04:27 -0400 Subject: [PATCH 009/239] Fix script/symbolicate for Linux panic reports (#33822) Release Notes: - N/A --- crates/remote_server/src/unix.rs | 2 +- script/symbolicate | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 48b4e483b4e2c64a275715b77c56d3fc0737709a..84ce08ff25bfab3e7d6e97768549105e048164e1 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -164,7 +164,7 @@ fn init_panic_hook() { }), app_version: format!("remote-server-{version}"), app_commit_sha: option_env!("ZED_COMMIT_SHA").map(|sha| sha.into()), - release_channel: release_channel.display_name().into(), + release_channel: release_channel.dev_name().into(), target: env!("TARGET").to_owned().into(), os_name: telemetry::os_name(), os_version: Some(telemetry::os_version()), diff --git a/script/symbolicate b/script/symbolicate index 743b5872ab5a39cd054c303ab7470b4e30d2ce8e..5e818626fa952900e7c6d9bec59dd9edf281dc61 100755 --- a/script/symbolicate +++ b/script/symbolicate @@ -11,24 +11,29 @@ fi input_file=$1; if [[ "$input_file" == *.json ]]; then - version=$(cat $input_file | jq -r .app_version) - channel=$(cat $input_file | jq -r .release_channel) - target_triple=$(cat $input_file | jq -r .target) + version=$(cat $input_file | jq -r .panic.app_version) + channel=$(cat $input_file | jq -r .panic.release_channel) + target_triple=$(cat $input_file | jq -r .panic.target) - which llvm-symbolizer rustfilt >dev/null || echo Need to install llvm-symbolizer and rustfilt + which llvm-symbolizer rustfilt >/dev/null || (echo Need to install llvm-symbolizer and rustfilt && exit 1) echo $channel; mkdir -p target/dsyms/$channel - dsym="$channel/zed-$version-$target_triple.dbg" + if [[ "$version" == "remote-server-"* ]]; then + version="${version#remote-server-}" + dsym="$channel/remote_server-$version-$target_triple.dbg" + else + dsym="$channel/zed-$version-$target_triple.dbg" + fi if [[ ! -f target/dsyms/$dsym ]]; then echo "Downloading $dsym..." curl -o target/dsyms/$dsym.gz "https://zed-debug-symbols.nyc3.digitaloceanspaces.com/$dsym.gz" gunzip target/dsyms/$dsym.gz fi - cat $input_file | jq -r .backtrace[] | sed s'/.*+//' | llvm-symbolizer --no-demangle --obj=target/dsyms/$dsym | rustfilt + cat $input_file | jq -r .panic.backtrace[] | sed s'/.*+//' | llvm-symbolizer --no-demangle --obj=target/dsyms/$dsym | rustfilt else # ips file From 6cd4dbdea11bd124241fd6de106535ac8e73df55 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Wed, 2 Jul 2025 20:14:33 -0500 Subject: [PATCH 010/239] gpui: Store action documentation (#33809) Closes #ISSUE Adds a new `documentation` method to actions, that is extracted from doc comments when using the `actions!` or derive macros. Additionally, this PR adds doc comments to as many action definitions in Zed as possible. Release Notes: - N/A *or* Added/Fixed/Improved ... --- .../src/activity_indicator.rs | 8 +- crates/agent_ui/src/agent_ui.rs | 35 +++ .../agent_ui/src/language_model_selector.rs | 1 + crates/agent_ui/src/text_thread_editor.rs | 8 + crates/auto_update/src/auto_update.rs | 12 +- crates/auto_update_ui/src/auto_update_ui.rs | 8 +- crates/client/src/client.rs | 12 +- crates/collab_ui/src/channel_view.rs | 8 +- crates/collab_ui/src/chat_panel.rs | 8 +- crates/collab_ui/src/collab_panel.rs | 10 + .../src/collab_panel/channel_modal.rs | 4 + crates/collab_ui/src/notification_panel.rs | 8 +- crates/copilot/src/copilot.rs | 6 + crates/debugger_tools/src/dap_log.rs | 8 +- crates/debugger_ui/src/debugger_ui.rs | 33 ++- .../src/session/running/breakpoint_list.rs | 7 +- .../src/session/running/console.rs | 8 +- .../src/session/running/variable_list.rs | 7 + crates/diagnostics/src/diagnostics.rs | 9 +- crates/editor/src/actions.rs | 249 +++++++++++++++++- crates/extension_host/src/extension_host.rs | 8 +- crates/extensions_ui/src/extensions_ui.rs | 8 +- crates/feedback/src/feedback.rs | 4 + crates/file_finder/src/file_finder.rs | 9 +- crates/git/src/git.rs | 26 ++ crates/git_ui/src/git_panel.rs | 6 + crates/git_ui/src/git_ui.rs | 8 +- crates/git_ui/src/project_diff.rs | 10 +- crates/gpui/src/action.rs | 11 + crates/gpui_macros/src/derive_action.rs | 28 ++ crates/gpui_macros/src/register_action.rs | 1 + .../src/inline_completion_button.rs | 8 +- crates/install_cli/src/install_cli.rs | 10 +- crates/journal/src/journal.rs | 8 +- .../src/language_selector.rs | 8 +- crates/language_tools/src/key_context_view.rs | 8 +- crates/language_tools/src/lsp_log.rs | 8 +- crates/language_tools/src/lsp_tool.rs | 8 +- crates/language_tools/src/syntax_tree_view.rs | 8 +- crates/markdown/src/markdown.rs | 10 +- .../markdown_preview/src/markdown_preview.rs | 5 + crates/menu/src/menu.rs | 8 + crates/outline_panel/src/outline_panel.rs | 13 +- crates/panel/src/panel.rs | 10 +- crates/picker/src/picker.rs | 8 +- crates/project/src/context_server_store.rs | 8 +- crates/project_panel/src/project_panel.rs | 29 ++ crates/repl/src/notebook/notebook_ui.rs | 7 + crates/repl/src/repl_sessions_ui.rs | 8 + crates/rules_library/src/rules_library.rs | 11 +- crates/search/src/buffer_search.rs | 13 +- crates/search/src/project_search.rs | 11 +- crates/search/src/search.rs | 16 ++ crates/settings_ui/src/keybindings.rs | 20 +- crates/settings_ui/src/settings_ui.rs | 10 +- crates/snippets_ui/src/snippets_ui.rs | 10 +- crates/supermaven/src/supermaven.rs | 8 +- crates/svg_preview/src/svg_preview.rs | 9 +- crates/tab_switcher/src/tab_switcher.rs | 11 +- crates/terminal/src/terminal.rs | 18 ++ crates/terminal_view/src/terminal_panel.rs | 8 +- crates/terminal_view/src/terminal_view.rs | 10 +- crates/theme_selector/src/theme_selector.rs | 8 +- crates/title_bar/src/application_menu.rs | 10 +- crates/title_bar/src/collab.rs | 12 +- crates/title_bar/src/title_bar.rs | 12 +- .../src/toolchain_selector.rs | 8 +- crates/vim/src/change_list.rs | 10 +- crates/vim/src/command.rs | 21 +- crates/vim/src/helix.rs | 8 +- crates/vim/src/indent.rs | 12 +- crates/vim/src/insert.rs | 10 +- crates/vim/src/motion.rs | 55 ++++ crates/vim/src/normal.rs | 27 ++ crates/vim/src/normal/increment.rs | 2 + crates/vim/src/normal/paste.rs | 1 + crates/vim/src/normal/repeat.rs | 14 +- crates/vim/src/normal/scroll.rs | 8 + crates/vim/src/normal/search.rs | 17 +- crates/vim/src/normal/substitute.rs | 10 +- crates/vim/src/object.rs | 23 ++ crates/vim/src/replace.rs | 10 +- crates/vim/src/rewrap.rs | 8 +- crates/vim/src/vim.rs | 52 +++- crates/vim/src/visual.rs | 18 ++ crates/welcome/src/base_keymap_picker.rs | 8 +- crates/welcome/src/welcome.rs | 8 +- crates/workspace/src/pane.rs | 29 ++ crates/workspace/src/theme_preview.rs | 8 +- crates/workspace/src/workspace.rs | 72 ++++- crates/zed/src/main.rs | 2 + crates/zed/src/zed.rs | 14 + crates/zed_actions/src/lib.rs | 129 ++++++++- crates/zeta/src/init.rs | 10 +- crates/zeta/src/rate_completion_modal.rs | 6 + crates/zeta/src/zeta.rs | 8 +- 96 files changed, 1467 insertions(+), 78 deletions(-) diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index b3287e8222ccdd1f4f4ca92ff4fd4559b9fcc3f6..b07c5418218c5045ae8018c9be9ec6fd07446544 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -31,7 +31,13 @@ use workspace::{StatusItemView, Workspace, item::ItemHandle}; const GIT_OPERATION_DELAY: Duration = Duration::from_millis(0); -actions!(activity_indicator, [ShowErrorMessage]); +actions!( + activity_indicator, + [ + /// Displays error messages from language servers in the status bar. + ShowErrorMessage + ] +); pub enum Event { ShowStatus { diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index b5ab5a147e3077a3465d93c3c38b1b7cae970da5..e488cf5a1e3a19e8007d7a29ec66971f829df003 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -54,42 +54,76 @@ pub use ui::preview::{all_agent_previews, get_agent_preview}; actions!( agent, [ + /// Creates a new text-based conversation thread. NewTextThread, + /// Toggles the context picker interface for adding files, symbols, or other context. ToggleContextPicker, + /// Toggles the navigation menu for switching between threads and views. ToggleNavigationMenu, + /// Toggles the options menu for agent settings and preferences. ToggleOptionsMenu, + /// Deletes the recently opened thread from history. DeleteRecentlyOpenThread, + /// Toggles the profile selector for switching between agent profiles. ToggleProfileSelector, + /// Removes all added context from the current conversation. RemoveAllContext, + /// Expands the message editor to full size. ExpandMessageEditor, + /// Opens the conversation history view. OpenHistory, + /// Adds a context server to the configuration. AddContextServer, + /// Removes the currently selected thread. RemoveSelectedThread, + /// Starts a chat conversation with the agent. Chat, + /// Starts a chat conversation with follow-up enabled. ChatWithFollow, + /// Cycles to the next inline assist suggestion. CycleNextInlineAssist, + /// Cycles to the previous inline assist suggestion. CyclePreviousInlineAssist, + /// Moves focus up in the interface. FocusUp, + /// Moves focus down in the interface. FocusDown, + /// Moves focus left in the interface. FocusLeft, + /// Moves focus right in the interface. FocusRight, + /// Removes the currently focused context item. RemoveFocusedContext, + /// Accepts the suggested context item. AcceptSuggestedContext, + /// Opens the active thread as a markdown file. OpenActiveThreadAsMarkdown, + /// Opens the agent diff view to review changes. OpenAgentDiff, + /// Keeps the current suggestion or change. Keep, + /// Rejects the current suggestion or change. Reject, + /// Rejects all suggestions or changes. RejectAll, + /// Keeps all suggestions or changes. KeepAll, + /// Follows the agent's suggestions. Follow, + /// Resets the trial upsell notification. ResetTrialUpsell, + /// Resets the trial end upsell notification. ResetTrialEndUpsell, + /// Continues the current thread. ContinueThread, + /// Continues the thread with burn mode enabled. ContinueWithBurnMode, + /// Toggles burn mode for faster responses. ToggleBurnMode, ] ); +/// Creates a new conversation thread, optionally based on an existing thread. #[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)] #[action(namespace = agent)] #[serde(deny_unknown_fields)] @@ -98,6 +132,7 @@ pub struct NewThread { from_thread_id: Option, } +/// Opens the profile management interface for configuring agent tools and settings. #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = agent)] #[serde(deny_unknown_fields)] diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index 55c0974fc1d2bdbd65e0b6d746abf7f4ef10654d..ff18a95f3f8b84eb0876a099cb664aa0908bed8f 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -18,6 +18,7 @@ use ui::{ListItem, ListItemSpacing, prelude::*}; actions!( agent, [ + /// Toggles the language model selector dropdown. #[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])] ToggleModelSelector ] diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index d11deb790820ba18a7437ac50ed3d5b2e8d4c9c0..465b3b4e58a3737032c0758da5f6af6dce092c2c 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -85,16 +85,24 @@ use assistant_context::{ actions!( assistant, [ + /// Sends the current message to the assistant. Assist, + /// Confirms and executes the entered slash command. ConfirmCommand, + /// Copies code from the assistant's response to the clipboard. CopyCode, + /// Cycles between user and assistant message roles. CycleMessageRole, + /// Inserts the selected text into the active editor. InsertIntoEditor, + /// Quotes the current selection in the assistant conversation. QuoteSelection, + /// Splits the conversation at the current cursor position. Split, ] ); +/// Inserts files that were dragged and dropped into the assistant conversation. #[derive(PartialEq, Clone, Action)] #[action(namespace = assistant, no_json, no_register)] pub enum InsertDraggedFiles { diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 26eb36118a1a946afca0a2f334371b423479ae45..70039060cab4104c432a1654f5d9339c9364c821 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -28,7 +28,17 @@ use workspace::Workspace; const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification"; const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60); -actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes,]); +actions!( + auto_update, + [ + /// Checks for available updates. + Check, + /// Dismisses the update error message. + DismissErrorMessage, + /// Opens the release notes for the current version. + ViewReleaseNotes, + ] +); #[derive(Serialize)] struct UpdateRequestBody { diff --git a/crates/auto_update_ui/src/auto_update_ui.rs b/crates/auto_update_ui/src/auto_update_ui.rs index afb135bc974f56d04db93e2a902fe48a64ab8ea7..5d79fb5db8704e995c296e2ba432a7414edafc97 100644 --- a/crates/auto_update_ui/src/auto_update_ui.rs +++ b/crates/auto_update_ui/src/auto_update_ui.rs @@ -12,7 +12,13 @@ use workspace::Workspace; use workspace::notifications::simple_message_notification::MessageNotification; use workspace::notifications::{NotificationId, show_app_notification}; -actions!(auto_update, [ViewReleaseNotesLocally]); +actions!( + auto_update, + [ + /// Opens release notes in the browser for the current version. + ViewReleaseNotesLocally + ] +); pub fn init(cx: &mut App) { notify_if_app_was_updated(cx); diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 86612bd15b12750f22fdedbb3475c7df6e6cfc99..c4211f72c819cfed5c0ee2f555356aa970968bc5 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -81,7 +81,17 @@ pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(500); pub const MAX_RECONNECTION_DELAY: Duration = Duration::from_secs(10); pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(20); -actions!(client, [SignIn, SignOut, Reconnect]); +actions!( + client, + [ + /// Signs in to Zed account. + SignIn, + /// Signs out of Zed account. + SignOut, + /// Reconnects to the collaboration server. + Reconnect + ] +); #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct ClientSettingsContent { diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index c872f99aa10ee160ed499621d9aceb2aa7c06a05..b86d72d92faede8c52e40a8e209fde5bf1ea9f0b 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -30,7 +30,13 @@ use workspace::{ }; use workspace::{item::Dedup, notifications::NotificationId}; -actions!(collab, [CopyLink]); +actions!( + collab, + [ + /// Copies a link to the current position in the channel buffer. + CopyLink + ] +); pub fn init(cx: &mut App) { workspace::FollowableViewRegistry::register::(cx) diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 54c45a9fec39569d06d7dc45140affe4f6c27d5b..3e2d813f1ba6474dc9e089d1fde2d48b26c7a31a 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -71,7 +71,13 @@ struct SerializedChatPanel { width: Option, } -actions!(chat_panel, [ToggleFocus]); +actions!( + chat_panel, + [ + /// Toggles focus on the chat panel. + ToggleFocus + ] +); impl ChatPanel { pub fn new( diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 6501d3a56649ec3cb2ef15099829d601fbbfadd4..ec23e2c3f536dc38db05f448f0d239d243a15756 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -44,15 +44,25 @@ use workspace::{ actions!( collab_panel, [ + /// Toggles focus on the collaboration panel. ToggleFocus, + /// Removes the selected channel or contact. Remove, + /// Opens the context menu for the selected item. Secondary, + /// Collapses the selected channel in the tree view. CollapseSelectedChannel, + /// Expands the selected channel in the tree view. ExpandSelectedChannel, + /// Starts moving a channel to a new location. StartMoveChannel, + /// Moves the selected item to the current location. MoveSelected, + /// Inserts a space character in the filter input. InsertSpace, + /// Moves the selected channel up in the list. MoveChannelUp, + /// Moves the selected channel down in the list. MoveChannelDown, ] ); diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 9af7baa160a1cfea3f2894f9dc1f0cf5522c2740..c0d3130ee997e3fe2ffffc4b228de9e512f18340 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -17,9 +17,13 @@ use workspace::{ModalView, notifications::DetachAndPromptErr}; actions!( channel_modal, [ + /// Selects the next control in the channel modal. SelectNextControl, + /// Toggles between invite members and manage members mode. ToggleMode, + /// Toggles admin status for the selected member. ToggleMemberAdmin, + /// Removes the selected member from the channel. RemoveMember ] ); diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 5e5e8164f9202fa978660e50a9375ad828ace92a..fba8f66c2d19153a0288148b02e593ee37078fb0 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -74,7 +74,13 @@ pub struct NotificationPresenter { pub can_navigate: bool, } -actions!(notification_panel, [ToggleFocus]); +actions!( + notification_panel, + [ + /// Toggles focus on the notification panel. + ToggleFocus + ] +); pub fn init(cx: &mut App) { cx.observe_new(|workspace: &mut Workspace, _, _| { diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 51f0984a1f55313dbfa4f834d3aa35933b4baba7..e4370d2e67cef9c5c4db68123edfb7dca5d7fa00 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -46,11 +46,17 @@ pub use crate::sign_in::{CopilotCodeVerification, initiate_sign_in, reinstall_an actions!( copilot, [ + /// Requests a code completion suggestion from Copilot. Suggest, + /// Cycles to the next Copilot suggestion. NextSuggestion, + /// Cycles to the previous Copilot suggestion. PreviousSuggestion, + /// Reinstalls the Copilot language server. Reinstall, + /// Signs in to GitHub Copilot. SignIn, + /// Signs out of GitHub Copilot. SignOut ] ); diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 532107f63302e9e057d2f90c28a2b32bcd0622d7..f2f193cad451772146f6fd39e13a75f29f13292b 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -918,7 +918,13 @@ impl Render for DapLogView { } } -actions!(dev, [OpenDebugAdapterLogs]); +actions!( + dev, + [ + /// Opens the debug adapter protocol logs viewer. + OpenDebugAdapterLogs + ] +); pub fn init(cx: &mut App) { let log_store = cx.new(|cx| LogStore::new(cx)); diff --git a/crates/debugger_ui/src/debugger_ui.rs b/crates/debugger_ui/src/debugger_ui.rs index 71b3ce1a31a9722d384379f6535617bc0c94f56a..2056232e9bd6912bbd1b4b7da7b51b769a47e63a 100644 --- a/crates/debugger_ui/src/debugger_ui.rs +++ b/crates/debugger_ui/src/debugger_ui.rs @@ -32,36 +32,67 @@ pub mod tests; actions!( debugger, [ + /// Starts a new debugging session. Start, + /// Continues execution until the next breakpoint. Continue, + /// Detaches the debugger from the running process. Detach, + /// Pauses the currently running program. Pause, + /// Restarts the current debugging session. Restart, + /// Reruns the current debugging session with the same configuration. RerunSession, + /// Steps into the next function call. StepInto, + /// Steps over the current line. StepOver, + /// Steps out of the current function. StepOut, + /// Steps back to the previous statement. StepBack, + /// Stops the debugging session. Stop, + /// Toggles whether to ignore all breakpoints. ToggleIgnoreBreakpoints, + /// Clears all breakpoints in the project. ClearAllBreakpoints, + /// Focuses on the debugger console panel. FocusConsole, + /// Focuses on the variables panel. FocusVariables, + /// Focuses on the breakpoint list panel. FocusBreakpointList, + /// Focuses on the call stack frames panel. FocusFrames, + /// Focuses on the loaded modules panel. FocusModules, + /// Focuses on the loaded sources panel. FocusLoadedSources, + /// Focuses on the terminal panel. FocusTerminal, + /// Shows the stack trace for the current thread. ShowStackTrace, + /// Toggles the thread picker dropdown. ToggleThreadPicker, + /// Toggles the session picker dropdown. ToggleSessionPicker, + /// Reruns the last debugging session. #[action(deprecated_aliases = ["debugger::RerunLastSession"])] Rerun, + /// Toggles expansion of the selected item in the debugger UI. ToggleExpandItem, ] ); -actions!(dev, [CopyDebugAdapterArguments]); +actions!( + dev, + [ + /// Copies debug adapter launch arguments to clipboard. + CopyDebugAdapterArguments + ] +); pub fn init(cx: &mut App) { DebuggerSettings::register(cx); diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index 5576435a0875ae298a7a7f5fb9d509a6a7ea16f1..2ec20c9877ab38642fa12aa6cc2a61256dd6ab26 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -33,7 +33,12 @@ use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint}; actions!( debugger, - [PreviousBreakpointProperty, NextBreakpointProperty] + [ + /// Navigates to the previous breakpoint property in the list. + PreviousBreakpointProperty, + /// Navigates to the next breakpoint property in the list. + NextBreakpointProperty + ] ); #[derive(Clone, Copy, PartialEq)] pub(crate) enum SelectedBreakpointKind { diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index aaac63640188b2b277d1ff8bfb9b75b114f5554b..c247f93ca13ce651a556b66d21b5c67520987398 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -23,7 +23,13 @@ use std::{cell::RefCell, ops::Range, rc::Rc, usize}; use theme::{Theme, ThemeSettings}; use ui::{ContextMenu, Divider, PopoverMenu, SplitButton, Tooltip, prelude::*}; -actions!(console, [WatchExpression]); +actions!( + console, + [ + /// Adds an expression to the watch list. + WatchExpression + ] +); pub struct Console { console: Entity, diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index c58ac865f9c5ed23e3b8129666ca7006408a34bc..bdb095bde3e4295bf96cff7d02012e4a4ea9d5bd 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -18,12 +18,19 @@ use util::debug_panic; actions!( variable_list, [ + /// Expands the selected variable entry to show its children. ExpandSelectedEntry, + /// Collapses the selected variable entry to hide its children. CollapseSelectedEntry, + /// Copies the variable name to the clipboard. CopyVariableName, + /// Copies the variable value to the clipboard. CopyVariableValue, + /// Edits the value of the selected variable. EditVariable, + /// Adds the selected variable to the watch list. AddWatch, + /// Removes the selected variable from the watch list. RemoveWatch, ] ); diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 8b49c536245a2509cb73254eca8de6d1be1cfd75..1daa9025b64f2a783409ba5ebe10214ed55c362b 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -48,7 +48,14 @@ use workspace::{ actions!( diagnostics, - [Deploy, ToggleWarnings, ToggleDiagnosticsRefresh] + [ + /// Opens the project diagnostics view. + Deploy, + /// Toggles the display of warning-level diagnostics. + ToggleWarnings, + /// Toggles automatic refresh of diagnostics. + ToggleDiagnosticsRefresh + ] ); #[derive(Default)] diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index b6e784590875c07d5d88382d1306995c40b83939..def2a616a8aa0f29e330b85c75cb8ae2d285542a 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -4,6 +4,7 @@ use gpui::{Action, actions}; use schemars::JsonSchema; use util::serde::default_true; +/// Selects the next occurrence of the current selection. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -12,6 +13,7 @@ pub struct SelectNext { pub replace_newest: bool, } +/// Selects the previous occurrence of the current selection. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -20,6 +22,7 @@ pub struct SelectPrevious { pub replace_newest: bool, } +/// Moves the cursor to the beginning of the current line. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -30,6 +33,7 @@ pub struct MoveToBeginningOfLine { pub stop_at_indent: bool, } +/// Selects from the cursor to the beginning of the current line. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -40,6 +44,7 @@ pub struct SelectToBeginningOfLine { pub stop_at_indent: bool, } +/// Deletes from the cursor to the beginning of the current line. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -48,6 +53,7 @@ pub struct DeleteToBeginningOfLine { pub(super) stop_at_indent: bool, } +/// Moves the cursor up by one page. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -56,6 +62,7 @@ pub struct MovePageUp { pub(super) center_cursor: bool, } +/// Moves the cursor down by one page. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -64,6 +71,7 @@ pub struct MovePageDown { pub(super) center_cursor: bool, } +/// Moves the cursor to the end of the current line. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -72,6 +80,7 @@ pub struct MoveToEndOfLine { pub stop_at_soft_wraps: bool, } +/// Selects from the cursor to the end of the current line. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -80,6 +89,7 @@ pub struct SelectToEndOfLine { pub(super) stop_at_soft_wraps: bool, } +/// Toggles the display of available code actions at the cursor position. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -101,6 +111,7 @@ pub enum CodeActionSource { QuickActionBar, } +/// Confirms and accepts the currently selected completion suggestion. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -109,6 +120,7 @@ pub struct ConfirmCompletion { pub item_ix: Option, } +/// Composes multiple completion suggestions into a single completion. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -117,6 +129,7 @@ pub struct ComposeCompletion { pub item_ix: Option, } +/// Confirms and applies the currently selected code action. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -125,6 +138,7 @@ pub struct ConfirmCodeAction { pub item_ix: Option, } +/// Toggles comment markers for the selected lines. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -135,6 +149,7 @@ pub struct ToggleComments { pub ignore_indent: bool, } +/// Moves the cursor up by a specified number of lines. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -143,6 +158,7 @@ pub struct MoveUpByLines { pub(super) lines: u32, } +/// Moves the cursor down by a specified number of lines. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -151,6 +167,7 @@ pub struct MoveDownByLines { pub(super) lines: u32, } +/// Extends selection up by a specified number of lines. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -159,6 +176,7 @@ pub struct SelectUpByLines { pub(super) lines: u32, } +/// Extends selection down by a specified number of lines. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -167,6 +185,7 @@ pub struct SelectDownByLines { pub(super) lines: u32, } +/// Expands all excerpts in the editor. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -175,6 +194,7 @@ pub struct ExpandExcerpts { pub(super) lines: u32, } +/// Expands excerpts above the current position. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -183,6 +203,7 @@ pub struct ExpandExcerptsUp { pub(super) lines: u32, } +/// Expands excerpts below the current position. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -191,6 +212,7 @@ pub struct ExpandExcerptsDown { pub(super) lines: u32, } +/// Shows code completion suggestions at the cursor position. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -199,10 +221,12 @@ pub struct ShowCompletions { pub(super) trigger: Option, } +/// Handles text input in the editor. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] pub struct HandleInput(pub String); +/// Deletes from the cursor to the end of the next word. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -211,6 +235,7 @@ pub struct DeleteToNextWordEnd { pub ignore_newlines: bool, } +/// Deletes from the cursor to the start of the previous word. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -219,10 +244,12 @@ pub struct DeleteToPreviousWordStart { pub ignore_newlines: bool, } +/// Folds all code blocks at the specified indentation level. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] pub struct FoldAtLevel(pub u32); +/// Spawns the nearest available task from the current cursor position. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] #[serde(deny_unknown_fields)] @@ -238,11 +265,20 @@ pub enum UuidVersion { V7, } -actions!(debugger, [RunToCursor, EvaluateSelectedText]); +actions!( + debugger, + [ + /// Runs program execution to the current cursor position. + RunToCursor, + /// Evaluates the selected text in the debugger context. + EvaluateSelectedText + ] +); actions!( go_to_line, [ + /// Toggles the go to line dialog. #[action(name = "Toggle")] ToggleGoToLine ] @@ -251,219 +287,430 @@ actions!( actions!( editor, [ + /// Accepts the full edit prediction. AcceptEditPrediction, + /// Accepts a partial Copilot suggestion. AcceptPartialCopilotSuggestion, + /// Accepts a partial edit prediction. AcceptPartialEditPrediction, + /// Adds a cursor above the current selection. AddSelectionAbove, + /// Adds a cursor below the current selection. AddSelectionBelow, + /// Applies all diff hunks in the editor. ApplyAllDiffHunks, + /// Applies the diff hunk at the current position. ApplyDiffHunk, + /// Deletes the character before the cursor. Backspace, + /// Cancels the current operation. Cancel, + /// Cancels the running flycheck operation. CancelFlycheck, + /// Cancels pending language server work. CancelLanguageServerWork, + /// Clears flycheck results. ClearFlycheck, + /// Confirms the rename operation. ConfirmRename, + /// Confirms completion by inserting at cursor. ConfirmCompletionInsert, + /// Confirms completion by replacing existing text. ConfirmCompletionReplace, + /// Navigates to the first item in the context menu. ContextMenuFirst, + /// Navigates to the last item in the context menu. ContextMenuLast, + /// Navigates to the next item in the context menu. ContextMenuNext, + /// Navigates to the previous item in the context menu. ContextMenuPrevious, + /// Converts indentation from tabs to spaces. ConvertIndentationToSpaces, + /// Converts indentation from spaces to tabs. ConvertIndentationToTabs, + /// Converts selected text to kebab-case. ConvertToKebabCase, + /// Converts selected text to lowerCamelCase. ConvertToLowerCamelCase, + /// Converts selected text to lowercase. ConvertToLowerCase, + /// Toggles the case of selected text. ConvertToOppositeCase, + /// Converts selected text to snake_case. ConvertToSnakeCase, + /// Converts selected text to Title Case. ConvertToTitleCase, + /// Converts selected text to UpperCamelCase. ConvertToUpperCamelCase, + /// Converts selected text to UPPERCASE. ConvertToUpperCase, + /// Applies ROT13 cipher to selected text. ConvertToRot13, + /// Applies ROT47 cipher to selected text. ConvertToRot47, + /// Copies selected text to the clipboard. Copy, + /// Copies selected text to the clipboard with leading/trailing whitespace trimmed. CopyAndTrim, + /// Copies the current file location to the clipboard. CopyFileLocation, + /// Copies the highlighted text as JSON. CopyHighlightJson, + /// Copies the current file name to the clipboard. CopyFileName, + /// Copies the file name without extension to the clipboard. CopyFileNameWithoutExtension, + /// Copies a permalink to the current line. CopyPermalinkToLine, + /// Cuts selected text to the clipboard. Cut, + /// Cuts from cursor to end of line. CutToEndOfLine, + /// Deletes the character after the cursor. Delete, + /// Deletes the current line. DeleteLine, + /// Deletes from cursor to end of line. DeleteToEndOfLine, + /// Deletes to the end of the next subword. DeleteToNextSubwordEnd, + /// Deletes to the start of the previous subword. DeleteToPreviousSubwordStart, + /// Displays names of all active cursors. DisplayCursorNames, + /// Duplicates the current line below. DuplicateLineDown, + /// Duplicates the current line above. DuplicateLineUp, + /// Duplicates the current selection. DuplicateSelection, + /// Expands all diff hunks in the editor. #[action(deprecated_aliases = ["editor::ExpandAllHunkDiffs"])] ExpandAllDiffHunks, + /// Expands macros recursively at cursor position. ExpandMacroRecursively, + /// Finds all references to the symbol at cursor. FindAllReferences, + /// Finds the next match in the search. FindNextMatch, + /// Finds the previous match in the search. FindPreviousMatch, + /// Folds the current code block. Fold, + /// Folds all foldable regions in the editor. FoldAll, + /// Folds all function bodies in the editor. FoldFunctionBodies, + /// Folds the current code block and all its children. FoldRecursive, + /// Folds the selected ranges. FoldSelectedRanges, + /// Toggles folding at the current position. ToggleFold, + /// Toggles recursive folding at the current position. ToggleFoldRecursive, + /// Formats the entire document. Format, + /// Formats only the selected text. FormatSelections, + /// Goes to the declaration of the symbol at cursor. GoToDeclaration, + /// Goes to declaration in a split pane. GoToDeclarationSplit, + /// Goes to the definition of the symbol at cursor. GoToDefinition, + /// Goes to definition in a split pane. GoToDefinitionSplit, + /// Goes to the next diagnostic in the file. GoToDiagnostic, + /// Goes to the next diff hunk. GoToHunk, + /// Goes to the previous diff hunk. GoToPreviousHunk, + /// Goes to the implementation of the symbol at cursor. GoToImplementation, + /// Goes to implementation in a split pane. GoToImplementationSplit, + /// Goes to the next change in the file. GoToNextChange, + /// Goes to the parent module of the current file. GoToParentModule, + /// Goes to the previous change in the file. GoToPreviousChange, + /// Goes to the previous diagnostic in the file. GoToPreviousDiagnostic, + /// Goes to the type definition of the symbol at cursor. GoToTypeDefinition, + /// Goes to type definition in a split pane. GoToTypeDefinitionSplit, + /// Scrolls down by half a page. HalfPageDown, + /// Scrolls up by half a page. HalfPageUp, + /// Shows hover information for the symbol at cursor. Hover, + /// Increases indentation of selected lines. Indent, + /// Inserts a UUID v4 at cursor position. InsertUuidV4, + /// Inserts a UUID v7 at cursor position. InsertUuidV7, + /// Joins the current line with the next line. JoinLines, + /// Cuts to kill ring (Emacs-style). KillRingCut, + /// Yanks from kill ring (Emacs-style). KillRingYank, + /// Moves cursor down one line. LineDown, + /// Moves cursor up one line. LineUp, + /// Moves cursor down. MoveDown, + /// Moves cursor left. MoveLeft, + /// Moves the current line down. MoveLineDown, + /// Moves the current line up. MoveLineUp, + /// Moves cursor right. MoveRight, + /// Moves cursor to the beginning of the document. MoveToBeginning, + /// Moves cursor to the enclosing bracket. MoveToEnclosingBracket, + /// Moves cursor to the end of the document. MoveToEnd, + /// Moves cursor to the end of the paragraph. MoveToEndOfParagraph, + /// Moves cursor to the end of the next subword. MoveToNextSubwordEnd, + /// Moves cursor to the end of the next word. MoveToNextWordEnd, + /// Moves cursor to the start of the previous subword. MoveToPreviousSubwordStart, + /// Moves cursor to the start of the previous word. MoveToPreviousWordStart, + /// Moves cursor to the start of the paragraph. MoveToStartOfParagraph, + /// Moves cursor to the start of the current excerpt. MoveToStartOfExcerpt, + /// Moves cursor to the start of the next excerpt. MoveToStartOfNextExcerpt, + /// Moves cursor to the end of the current excerpt. MoveToEndOfExcerpt, + /// Moves cursor to the end of the previous excerpt. MoveToEndOfPreviousExcerpt, + /// Moves cursor up. MoveUp, + /// Inserts a new line and moves cursor to it. Newline, + /// Inserts a new line above the current line. NewlineAbove, + /// Inserts a new line below the current line. NewlineBelow, + /// Navigates to the next edit prediction. NextEditPrediction, + /// Scrolls to the next screen. NextScreen, + /// Opens the context menu at cursor position. OpenContextMenu, + /// Opens excerpts from the current file. OpenExcerpts, + /// Opens excerpts in a split pane. OpenExcerptsSplit, + /// Opens the proposed changes editor. OpenProposedChangesEditor, + /// Opens documentation for the symbol at cursor. OpenDocs, + /// Opens a permalink to the current line. OpenPermalinkToLine, + /// Opens the file whose name is selected in the editor. #[action(deprecated_aliases = ["editor::OpenFile"])] OpenSelectedFilename, + /// Opens all selections in a multibuffer. OpenSelectionsInMultibuffer, + /// Opens the URL at cursor position. OpenUrl, + /// Organizes import statements. OrganizeImports, + /// Decreases indentation of selected lines. Outdent, + /// Automatically adjusts indentation based on context. AutoIndent, + /// Scrolls down by one page. PageDown, + /// Scrolls up by one page. PageUp, + /// Pastes from clipboard. Paste, + /// Navigates to the previous edit prediction. PreviousEditPrediction, + /// Redoes the last undone edit. Redo, + /// Redoes the last selection change. RedoSelection, + /// Renames the symbol at cursor. Rename, + /// Restarts the language server for the current file. RestartLanguageServer, + /// Reveals the current file in the system file manager. RevealInFileManager, + /// Reverses the order of selected lines. ReverseLines, + /// Reloads the file from disk. ReloadFile, + /// Rewraps text to fit within the preferred line length. Rewrap, + /// Runs flycheck diagnostics. RunFlycheck, + /// Scrolls the cursor to the bottom of the viewport. ScrollCursorBottom, + /// Scrolls the cursor to the center of the viewport. ScrollCursorCenter, + /// Cycles cursor position between center, top, and bottom. ScrollCursorCenterTopBottom, + /// Scrolls the cursor to the top of the viewport. ScrollCursorTop, + /// Selects all text in the editor. SelectAll, + /// Selects all matches of the current selection. SelectAllMatches, + /// Selects to the start of the current excerpt. SelectToStartOfExcerpt, + /// Selects to the start of the next excerpt. SelectToStartOfNextExcerpt, + /// Selects to the end of the current excerpt. SelectToEndOfExcerpt, + /// Selects to the end of the previous excerpt. SelectToEndOfPreviousExcerpt, + /// Extends selection down. SelectDown, + /// Selects the enclosing symbol. SelectEnclosingSymbol, + /// Selects the next larger syntax node. SelectLargerSyntaxNode, + /// Extends selection left. SelectLeft, + /// Selects the current line. SelectLine, + /// Extends selection down by one page. SelectPageDown, + /// Extends selection up by one page. SelectPageUp, + /// Extends selection right. SelectRight, + /// Selects the next smaller syntax node. SelectSmallerSyntaxNode, + /// Selects to the beginning of the document. SelectToBeginning, + /// Selects to the end of the document. SelectToEnd, + /// Selects to the end of the paragraph. SelectToEndOfParagraph, + /// Selects to the end of the next subword. SelectToNextSubwordEnd, + /// Selects to the end of the next word. SelectToNextWordEnd, + /// Selects to the start of the previous subword. SelectToPreviousSubwordStart, + /// Selects to the start of the previous word. SelectToPreviousWordStart, + /// Selects to the start of the paragraph. SelectToStartOfParagraph, + /// Extends selection up. SelectUp, + /// Shows the system character palette. ShowCharacterPalette, + /// Shows edit prediction at cursor. ShowEditPrediction, + /// Shows signature help for the current function. ShowSignatureHelp, + /// Shows word completions. ShowWordCompletions, + /// Randomly shuffles selected lines. ShuffleLines, + /// Navigates to the next signature in the signature help popup. SignatureHelpNext, + /// Navigates to the previous signature in the signature help popup. SignatureHelpPrevious, + /// Sorts selected lines case-insensitively. SortLinesCaseInsensitive, + /// Sorts selected lines case-sensitively. SortLinesCaseSensitive, + /// Splits selection into individual lines. SplitSelectionIntoLines, + /// Stops the language server for the current file. StopLanguageServer, + /// Switches between source and header files. SwitchSourceHeader, + /// Inserts a tab character or indents. Tab, + /// Removes a tab character or outdents. Backtab, + /// Toggles a breakpoint at the current line. ToggleBreakpoint, + /// Toggles the case of selected text. ToggleCase, + /// Disables the breakpoint at the current line. DisableBreakpoint, + /// Enables the breakpoint at the current line. EnableBreakpoint, + /// Edits the log message for a breakpoint. EditLogBreakpoint, + /// Toggles automatic signature help. ToggleAutoSignatureHelp, + /// Toggles inline git blame display. ToggleGitBlameInline, + /// Opens the git commit for the blame at cursor. OpenGitBlameCommit, + /// Toggles the diagnostics panel. ToggleDiagnostics, + /// Toggles indent guides display. ToggleIndentGuides, + /// Toggles inlay hints display. ToggleInlayHints, + /// Toggles inline values display. ToggleInlineValues, + /// Toggles inline diagnostics display. ToggleInlineDiagnostics, + /// Toggles edit prediction feature. ToggleEditPrediction, + /// Toggles line numbers display. ToggleLineNumbers, + /// Toggles the minimap display. ToggleMinimap, + /// Swaps the start and end of the current selection. SwapSelectionEnds, + /// Sets a mark at the current position. SetMark, + /// Toggles relative line numbers display. ToggleRelativeLineNumbers, + /// Toggles diff display for selected hunks. #[action(deprecated_aliases = ["editor::ToggleHunkDiff"])] ToggleSelectedDiffHunks, + /// Toggles the selection menu. ToggleSelectionMenu, + /// Toggles soft wrap mode. ToggleSoftWrap, + /// Toggles the tab bar display. ToggleTabBar, + /// Transposes characters around cursor. Transpose, + /// Undoes the last edit. Undo, + /// Undoes the last selection change. UndoSelection, + /// Unfolds all folded regions. UnfoldAll, + /// Unfolds lines at cursor. UnfoldLines, + /// Unfolds recursively at cursor. UnfoldRecursive, + /// Removes duplicate lines (case-insensitive). UniqueLinesCaseInsensitive, + /// Removes duplicate lines (case-sensitive). UniqueLinesCaseSensitive, ] ); diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 97d8e23f0d4feb92fcb1d07144e87d86379b194b..8d3a218a03069cf79ec87799d566595e0b0dd3ce 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -178,7 +178,13 @@ pub struct ExtensionIndexLanguageEntry { pub grammar: Option>, } -actions!(zed, [ReloadExtensions]); +actions!( + zed, + [ + /// Reloads all installed extensions. + ReloadExtensions + ] +); pub fn init( extension_host_proxy: Arc, diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index e4c2ba4c091585721c6797622bc32c2c6fe041e9..48cb41a006560b17b2812939f41f36cf0bee9aee 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -38,7 +38,13 @@ use crate::extension_version_selector::{ ExtensionVersionSelector, ExtensionVersionSelectorDelegate, }; -actions!(zed, [InstallDevExtension]); +actions!( + zed, + [ + /// Installs an extension from a local directory for development. + InstallDevExtension + ] +); pub fn init(cx: &mut App) { cx.observe_new(move |workspace: &mut Workspace, window, cx| { diff --git a/crates/feedback/src/feedback.rs b/crates/feedback/src/feedback.rs index 67ba0dc278d8263bbf111b9d513a291892cd1c67..40c2707d34c9f5ab50bdb51c8b82183be2106285 100644 --- a/crates/feedback/src/feedback.rs +++ b/crates/feedback/src/feedback.rs @@ -11,9 +11,13 @@ pub mod system_specs; actions!( zed, [ + /// Copies system specifications to the clipboard for bug reports. CopySystemSpecsIntoClipboard, + /// Opens email client to send feedback to Zed support. EmailZed, + /// Opens the Zed repository on GitHub. OpenZedRepo, + /// Opens the feature request form. RequestFeature, ] ); diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 5096be673342f2cfa365e8806be330bfc3bd26cf..a4d61dd56f0b3503b09698aa633cf47bf12389e4 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -47,7 +47,14 @@ use workspace::{ actions!( file_finder, - [SelectPrevious, ToggleFilterMenu, ToggleSplitMenu] + [ + /// Selects the previous item in the file finder. + SelectPrevious, + /// Toggles the file filter menu. + ToggleFilterMenu, + /// Toggles the split direction menu. + ToggleSplitMenu + ] ); impl ModalView for FileFinder { diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index d64d35b789415b2dda821693ee7e9d19193a5de4..92cf58b2adafc692d8407982247d82f03d57fd78 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -31,38 +31,64 @@ actions!( git, [ // per-hunk + /// Toggles the staged state of the hunk at cursor. ToggleStaged, + /// Stages the current hunk and moves to the next one. StageAndNext, + /// Unstages the current hunk and moves to the next one. UnstageAndNext, + /// Restores the selected hunks to their original state. #[action(deprecated_aliases = ["editor::RevertSelectedHunks"])] Restore, // per-file + /// Shows git blame information for the current file. #[action(deprecated_aliases = ["editor::ToggleGitBlame"])] Blame, + /// Stages the current file. StageFile, + /// Unstages the current file. UnstageFile, // repo-wide + /// Stages all changes in the repository. StageAll, + /// Unstages all changes in the repository. UnstageAll, + /// Restores all tracked files to their last committed state. RestoreTrackedFiles, + /// Moves all untracked files to trash. TrashUntrackedFiles, + /// Undoes the last commit, keeping changes in the working directory. Uncommit, + /// Pushes commits to the remote repository. Push, + /// Pushes commits to a specific remote branch. PushTo, + /// Force pushes commits to the remote repository. ForcePush, + /// Pulls changes from the remote repository. Pull, + /// Fetches changes from the remote repository. Fetch, + /// Fetches changes from a specific remote. FetchFrom, + /// Creates a new commit with staged changes. Commit, + /// Amends the last commit with staged changes. Amend, + /// Cancels the current git operation. Cancel, + /// Expands the commit message editor. ExpandCommitEditor, + /// Generates a commit message using AI. GenerateCommitMessage, + /// Initializes a new git repository. Init, + /// Opens all modified files in the editor. OpenModifiedFiles, ] ); +/// Restores a file to its last committed state, discarding local changes. #[derive(Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)] #[action(namespace = git, deprecated_aliases = ["editor::RevertFile"])] #[serde(deny_unknown_fields)] diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 86a67fcc59d6ea47395885c424561163411c975a..e26a47ff8f28fe38b7a0f2f480f534608d69c0f8 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -77,11 +77,17 @@ use zed_llm_client::CompletionIntent; actions!( git_panel, [ + /// Closes the git panel. Close, + /// Toggles focus on the git panel. ToggleFocus, + /// Opens the git panel menu. OpenMenu, + /// Focuses on the commit message editor. FocusEditor, + /// Focuses on the changes list. FocusChanges, + /// Toggles automatic co-author suggestions. ToggleFillCoAuthors, ] ); diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 1653902bbd0e63b30ce928e9d94d77f5adbc2987..a9ccaf716074783b2bf3a5e4d969c0702320557e 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -31,7 +31,13 @@ pub mod project_diff; pub(crate) mod remote_output; pub mod repository_selector; -actions!(git, [ResetOnboarding]); +actions!( + git, + [ + /// Resets the git onboarding state to show the tutorial again. + ResetOnboarding + ] +); pub fn init(cx: &mut App) { GitPanelSettings::register(cx); diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index f858bea94c288efc5dd24c3c17c63bc4b3c63aa2..d6a4e27286af1bb38dcd1acc488bce9da1813a42 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -41,7 +41,15 @@ use workspace::{ searchable::SearchableItemHandle, }; -actions!(git, [Diff, Add]); +actions!( + git, + [ + /// Shows the diff between the working directory and the index. + Diff, + /// Adds files to the git staging area. + Add + ] +); pub struct ProjectDiff { project: Entity, diff --git a/crates/gpui/src/action.rs b/crates/gpui/src/action.rs index 7885497034c1a9b0e3404503d36e9f4fdc24b276..9e979a31ff1940facfd4bca10652c40ee0fcd6c3 100644 --- a/crates/gpui/src/action.rs +++ b/crates/gpui/src/action.rs @@ -150,6 +150,15 @@ pub trait Action: Any + Send { { None } + + /// The documentation for this action, if any. When using the derive macro for actions + /// this will be automatically generated from the doc comments on the action struct. + fn documentation() -> Option<&'static str> + where + Self: Sized, + { + None + } } impl std::fmt::Debug for dyn Action { @@ -254,6 +263,7 @@ pub struct MacroActionData { pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option, pub deprecated_aliases: &'static [&'static str], pub deprecation_message: Option<&'static str>, + pub documentation: Option<&'static str>, } inventory::collect!(MacroActionBuilder); @@ -276,6 +286,7 @@ impl ActionRegistry { json_schema: A::action_json_schema, deprecated_aliases: A::deprecated_aliases(), deprecation_message: A::deprecation_message(), + documentation: A::documentation(), }); } diff --git a/crates/gpui_macros/src/derive_action.rs b/crates/gpui_macros/src/derive_action.rs index c32baba6cbacdb809623c43aef7ec362b963d178..9c7f97371d86eecc29dc16902ba9e392d53b8660 100644 --- a/crates/gpui_macros/src/derive_action.rs +++ b/crates/gpui_macros/src/derive_action.rs @@ -14,6 +14,7 @@ pub(crate) fn derive_action(input: TokenStream) -> TokenStream { let mut no_register = false; let mut namespace = None; let mut deprecated = None; + let mut doc_str: Option = None; for attr in &input.attrs { if attr.path().is_ident("action") { @@ -74,6 +75,22 @@ pub(crate) fn derive_action(input: TokenStream) -> TokenStream { Ok(()) }) .unwrap_or_else(|e| panic!("in #[action] attribute: {}", e)); + } else if attr.path().is_ident("doc") { + use syn::{Expr::Lit, ExprLit, Lit::Str, Meta, MetaNameValue}; + if let Meta::NameValue(MetaNameValue { + value: + Lit(ExprLit { + lit: Str(ref lit_str), + .. + }), + .. + }) = attr.meta + { + let doc = lit_str.value(); + let doc_str = doc_str.get_or_insert_default(); + doc_str.push_str(doc.trim()); + doc_str.push('\n'); + } } } @@ -122,6 +139,13 @@ pub(crate) fn derive_action(input: TokenStream) -> TokenStream { quote! { None } }; + let documentation_fn_body = if let Some(doc) = doc_str { + let doc = doc.trim(); + quote! { Some(#doc) } + } else { + quote! { None } + }; + let registration = if no_register { quote! {} } else { @@ -171,6 +195,10 @@ pub(crate) fn derive_action(input: TokenStream) -> TokenStream { fn deprecation_message() -> Option<&'static str> { #deprecation_fn_body } + + fn documentation() -> Option<&'static str> { + #documentation_fn_body + } } }) } diff --git a/crates/gpui_macros/src/register_action.rs b/crates/gpui_macros/src/register_action.rs index d1910b82b2a7714849fc8d380dfc8b1b4e6b0d05..ca36ce318699348e33dc86bac99ec90fea26444d 100644 --- a/crates/gpui_macros/src/register_action.rs +++ b/crates/gpui_macros/src/register_action.rs @@ -34,6 +34,7 @@ pub(crate) fn generate_register_action(type_name: &Ident) -> TokenStream2 { json_schema: <#type_name as gpui::Action>::action_json_schema, deprecated_aliases: <#type_name as gpui::Action>::deprecated_aliases(), deprecation_message: <#type_name as gpui::Action>::deprecation_message(), + documentation: <#type_name as gpui::Action>::documentation(), } } diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index cf1e808f602803c971f2e7c604947ca5b7aee589..f8123d676a001427b8b0350d53cdd7ab8b1041ab 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -37,7 +37,13 @@ use zed_actions::OpenBrowser; use zed_llm_client::UsageLimit; use zeta::RateCompletions; -actions!(edit_prediction, [ToggleMenu]); +actions!( + edit_prediction, + [ + /// Toggles the inline completion menu. + ToggleMenu + ] +); const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot"; diff --git a/crates/install_cli/src/install_cli.rs b/crates/install_cli/src/install_cli.rs index 99f4a4e3f7e78769f0abee21dd7cd9b8acac97d0..12c094448b8362c8d638ac62da5838544b4fcc6d 100644 --- a/crates/install_cli/src/install_cli.rs +++ b/crates/install_cli/src/install_cli.rs @@ -8,7 +8,15 @@ use util::ResultExt; use workspace::notifications::{DetachAndPromptErr, NotificationId}; use workspace::{Toast, Workspace}; -actions!(cli, [Install, RegisterZedScheme]); +actions!( + cli, + [ + /// Installs the Zed CLI tool to the system PATH. + Install, + /// Registers the zed:// URL scheme handler. + RegisterZedScheme + ] +); async fn install_script(cx: &AsyncApp) -> Result { let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))??; diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 08bdb8e04f620518ef7955361979f28d83353718..0335a746cd23eb2654dac7f8960a649aa3c269ff 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -13,7 +13,13 @@ use std::{ }; use workspace::{AppState, OpenVisible, Workspace}; -actions!(journal, [NewJournalEntry]); +actions!( + journal, + [ + /// Creates a new journal entry for today. + NewJournalEntry + ] +); /// Settings specific to journaling #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs index 2a4b6de65560e146cf62ac337403ab19ca841cc4..4c034305537e51e752fcef90eeeb7668f1bb50b7 100644 --- a/crates/language_selector/src/language_selector.rs +++ b/crates/language_selector/src/language_selector.rs @@ -19,7 +19,13 @@ use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*}; use util::ResultExt; use workspace::{ModalView, Workspace}; -actions!(language_selector, [Toggle]); +actions!( + language_selector, + [ + /// Toggles the language selector modal. + Toggle + ] +); pub fn init(cx: &mut App) { cx.observe_new(LanguageSelector::register).detach(); diff --git a/crates/language_tools/src/key_context_view.rs b/crates/language_tools/src/key_context_view.rs index 4c7f80de02c2f087c73c374e029409f09745fa7f..c933872d8c513c21c2095b6b32d7a316fcb7f92f 100644 --- a/crates/language_tools/src/key_context_view.rs +++ b/crates/language_tools/src/key_context_view.rs @@ -13,7 +13,13 @@ use ui::{ }; use workspace::{Item, SplitDirection, Workspace}; -actions!(dev, [OpenKeyContextView]); +actions!( + dev, + [ + /// Opens the key context view for debugging keybindings. + OpenKeyContextView + ] +); pub fn init(cx: &mut App) { cx.observe_new(|workspace: &mut Workspace, _, _| { diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index a3827218c3b76c3b373492ca2092128b27462c40..9d2badd561396f3adddfac7f4d9982d1d7400e6f 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -204,7 +204,13 @@ pub(crate) struct LogMenuItem { pub server_kind: LanguageServerKind, } -actions!(dev, [OpenLanguageServerLogs]); +actions!( + dev, + [ + /// Opens the language server protocol logs viewer. + OpenLanguageServerLogs + ] +); pub(super) struct GlobalLogStore(pub WeakEntity); diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_tool.rs index 899aaf0679689c344b3fe6dcac15d76d40009b5c..24a53ae2529b23b45e2478227109506839c12a89 100644 --- a/crates/language_tools/src/lsp_tool.rs +++ b/crates/language_tools/src/lsp_tool.rs @@ -19,7 +19,13 @@ use workspace::{StatusItemView, Workspace}; use crate::lsp_log::GlobalLogStore; -actions!(lsp_tool, [ToggleMenu]); +actions!( + lsp_tool, + [ + /// Toggles the language server tool menu. + ToggleMenu + ] +); pub struct LspTool { state: Entity, diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 6f74e76e261b7b5f33463fe7932c7eaf0fa2a9fe..eadba2c1d2f4c96c4f0ad2646c2e9957bbae3bdc 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -15,7 +15,13 @@ use workspace::{ item::{Item, ItemHandle}, }; -actions!(dev, [OpenSyntaxTreeView]); +actions!( + dev, + [ + /// Opens the syntax tree view for the current file. + OpenSyntaxTreeView + ] +); pub fn init(cx: &mut App) { cx.observe_new(|workspace: &mut Workspace, _, _| { diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 27859f6e082b5e666e5baae24760336bb0b94fb3..dba4bc64b191b1e189cd114b2f66cd408c2feece 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -141,7 +141,15 @@ pub type CodeBlockRenderFn = Arc< pub type CodeBlockTransformFn = Arc, CodeBlockMetadata, &mut Window, &App) -> AnyDiv>; -actions!(markdown, [Copy, CopyAsMarkdown]); +actions!( + markdown, + [ + /// Copies the selected text to the clipboard. + Copy, + /// Copies the selected text as markdown to the clipboard. + CopyAsMarkdown + ] +); impl Markdown { pub fn new( diff --git a/crates/markdown_preview/src/markdown_preview.rs b/crates/markdown_preview/src/markdown_preview.rs index afc5b964b2a37aff1906382f0826b3a7490268af..91c0005097d778d4b60f7a8a721ed898f0059ed1 100644 --- a/crates/markdown_preview/src/markdown_preview.rs +++ b/crates/markdown_preview/src/markdown_preview.rs @@ -9,10 +9,15 @@ pub mod markdown_renderer; actions!( markdown, [ + /// Scrolls up by one page in the markdown preview. MovePageUp, + /// Scrolls down by one page in the markdown preview. MovePageDown, + /// Opens a markdown preview for the current file. OpenPreview, + /// Opens a markdown preview in a split pane. OpenPreviewToTheSide, + /// Opens a following markdown preview that syncs with the editor. OpenFollowingPreview ] ); diff --git a/crates/menu/src/menu.rs b/crates/menu/src/menu.rs index 10eeeff8ca40b447811db335056da19f904f752b..9a1937d100210cb975ab0630be9fd15078561e0b 100644 --- a/crates/menu/src/menu.rs +++ b/crates/menu/src/menu.rs @@ -12,13 +12,21 @@ pub fn init() {} actions!( menu, [ + /// Cancels the current menu operation. Cancel, + /// Confirms the selected menu item. Confirm, + /// Performs secondary confirmation action. SecondaryConfirm, + /// Selects the previous item in the menu. SelectPrevious, + /// Selects the next item in the menu. SelectNext, + /// Selects the first item in the menu. SelectFirst, + /// Selects the last item in the menu. SelectLast, + /// Restarts the menu from the beginning. Restart, EndSlot, ] diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 0be05d458908e3d7b1317ea205664a349eb6ef5f..05352e24def8a4aefd399d6ce764b6afbfedbaf1 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -65,17 +65,28 @@ use worktree::{Entry, ProjectEntryId, WorktreeId}; actions!( outline_panel, [ + /// Collapses all entries in the outline tree. CollapseAllEntries, + /// Collapses the currently selected entry. CollapseSelectedEntry, + /// Expands all entries in the outline tree. ExpandAllEntries, + /// Expands the currently selected entry. ExpandSelectedEntry, + /// Folds the selected directory. FoldDirectory, + /// Opens the selected entry in the editor. OpenSelectedEntry, + /// Reveals the selected item in the system file manager. RevealInFileManager, + /// Selects the parent of the current entry. SelectParent, + /// Toggles the pin status of the active editor. ToggleActiveEditorPin, - ToggleFocus, + /// Unfolds the selected directory. UnfoldDirectory, + /// Toggles focus on the outline panel. + ToggleFocus, ] ); diff --git a/crates/panel/src/panel.rs b/crates/panel/src/panel.rs index 58edb1e81d14481a838235785a42500b416aee98..a09034cc1756f5adfa7bd0d38b35a2e63b51e901 100644 --- a/crates/panel/src/panel.rs +++ b/crates/panel/src/panel.rs @@ -5,7 +5,15 @@ use settings::Settings; use theme::ThemeSettings; use ui::{Tab, prelude::*}; -actions!(panel, [NextPanelTab, PreviousPanelTab]); +actions!( + panel, + [ + /// Navigates to the next tab in the panel. + NextPanelTab, + /// Navigates to the previous tab in the panel. + PreviousPanelTab + ] +); pub trait PanelHeader: workspace::Panel { fn header_height(&self, cx: &mut App) -> Pixels { diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 4a122ac7316ed1a7552eda41ef223c62bc3ba910..692bdd5bd7a49a3d293603358c1c4d8a2061c42a 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -34,7 +34,13 @@ pub enum Direction { Down, } -actions!(picker, [ConfirmCompletion]); +actions!( + picker, + [ + /// Confirms the selected completion in the picker. + ConfirmCompletion + ] +); /// ConfirmInput is an alternative editor action which - instead of selecting active picker entry - treats pickers editor input literally, /// performing some kind of action on it. diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index 6f93238cc9b6f8d6fad08e25baa819eae4ef9b4b..d13cb37aa8b4bb97afb4ddbe454bfd9ee7b68b9e 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -21,7 +21,13 @@ pub fn init(cx: &mut App) { extension::init(cx); } -actions!(context_server, [Restart]); +actions!( + context_server, + [ + /// Restarts the context server. + Restart + ] +); #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ContextServerStatus { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 657cccf98a1007660e1563cc16585d7658e95b7c..614f8ccf81967941c099ada140fcd20bc6cc94c6 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -181,6 +181,7 @@ struct EntryDetails { canonical_path: Option>, } +/// Permanently deletes the selected file or directory. #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = project_panel)] #[serde(deny_unknown_fields)] @@ -189,6 +190,7 @@ struct Delete { pub skip_prompt: bool, } +/// Moves the selected file or directory to the system trash. #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = project_panel)] #[serde(deny_unknown_fields)] @@ -200,32 +202,59 @@ struct Trash { actions!( project_panel, [ + /// Expands the selected entry in the project tree. ExpandSelectedEntry, + /// Collapses the selected entry in the project tree. CollapseSelectedEntry, + /// Collapses all entries in the project tree. CollapseAllEntries, + /// Creates a new directory. NewDirectory, + /// Creates a new file. NewFile, + /// Copies the selected file or directory. Copy, + /// Duplicates the selected file or directory. Duplicate, + /// Reveals the selected item in the system file manager. RevealInFileManager, + /// Removes the selected folder from the project. RemoveFromProject, + /// Opens the selected file with the system's default application. OpenWithSystem, + /// Cuts the selected file or directory. Cut, + /// Pastes the previously cut or copied item. Paste, + /// Renames the selected file or directory. Rename, + /// Opens the selected file in the editor. Open, + /// Opens the selected file in a permanent tab. OpenPermanent, + /// Toggles focus on the project panel. ToggleFocus, + /// Toggles visibility of git-ignored files. ToggleHideGitIgnore, + /// Starts a new search in the selected directory. NewSearchInDirectory, + /// Unfolds the selected directory. UnfoldDirectory, + /// Folds the selected directory. FoldDirectory, + /// Selects the parent directory. SelectParent, + /// Selects the next entry with git changes. SelectNextGitEntry, + /// Selects the previous entry with git changes. SelectPrevGitEntry, + /// Selects the next entry with diagnostics. SelectNextDiagnostic, + /// Selects the previous entry with diagnostics. SelectPrevDiagnostic, + /// Selects the next directory. SelectNextDirectory, + /// Selects the previous directory. SelectPrevDirectory, ] ); diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index 9091feed635af741e9b2c916955f6259f2f2473f..d14f458fa9d4fcaf8b6cdd50bf276c36fa2ef0b6 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -28,12 +28,19 @@ use nbformat::v4::Metadata as NotebookMetadata; actions!( notebook, [ + /// Opens a Jupyter notebook file. OpenNotebook, + /// Runs all cells in the notebook. RunAll, + /// Clears all cell outputs. ClearOutputs, + /// Moves the current cell up. MoveCellUp, + /// Moves the current cell down. MoveCellDown, + /// Adds a new markdown cell. AddMarkdownBlock, + /// Adds a new code cell. AddCodeBlock, ] ); diff --git a/crates/repl/src/repl_sessions_ui.rs b/crates/repl/src/repl_sessions_ui.rs index df7ce574abadc6d6709fe5bb572e60b39041b8b8..2f4c1f86fc5d9d4baaa005e745d70161327473ca 100644 --- a/crates/repl/src/repl_sessions_ui.rs +++ b/crates/repl/src/repl_sessions_ui.rs @@ -16,13 +16,21 @@ use crate::repl_store::ReplStore; actions!( repl, [ + /// Runs the current cell and advances to the next one. Run, + /// Runs the current cell without advancing. RunInPlace, + /// Clears all outputs in the REPL. ClearOutputs, + /// Opens the REPL sessions panel. Sessions, + /// Interrupts the currently running kernel. Interrupt, + /// Shuts down the current kernel. Shutdown, + /// Restarts the current kernel. Restart, + /// Refreshes the list of available kernelspecs. RefreshKernelspecs ] ); diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index 5e249162d3286e777ba28f8c645f8e2918bc9acf..66f589bfd39cbb941cbc7ff693f13b87c8d06c83 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -37,7 +37,16 @@ pub fn init(cx: &mut App) { actions!( rules_library, - [NewRule, DeleteRule, DuplicateRule, ToggleDefaultRule] + [ + /// Creates a new rule in the rules library. + NewRule, + /// Deletes the selected rule. + DeleteRule, + /// Duplicates the selected rule. + DuplicateRule, + /// Toggles whether the selected rule is a default rule. + ToggleDefaultRule + ] ); const BUILT_IN_TOOLTIP_TEXT: &'static str = concat!( diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 28d61c135772410c8fcf57c2dc501fae24933d99..35c8fcd23098e4e5e3314263d56c73112ce0a768 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -46,6 +46,7 @@ use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults}; const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50; +/// Opens the buffer search interface with the specified configuration. #[derive(PartialEq, Clone, Deserialize, JsonSchema, Action)] #[action(namespace = buffer_search)] #[serde(deny_unknown_fields)] @@ -58,7 +59,17 @@ pub struct Deploy { pub selection_search_enabled: bool, } -actions!(buffer_search, [DeployReplace, Dismiss, FocusEditor]); +actions!( + buffer_search, + [ + /// Deploys the search and replace interface. + DeployReplace, + /// Dismisses the search bar. + Dismiss, + /// Focuses back on the editor. + FocusEditor + ] +); impl Deploy { pub fn find() -> Self { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index dd440e0639c5d235ef39398a98f8014c30ad06c2..57ca5e56b9447f8552abac55c6d79a5f6e8326a1 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -47,7 +47,16 @@ use workspace::{ actions!( project_search, - [SearchInNew, ToggleFocus, NextField, ToggleFilters] + [ + /// Searches in a new project search tab. + SearchInNew, + /// Toggles focus between the search bar and the search results. + ToggleFocus, + /// Moves to the next input field. + NextField, + /// Toggles the search filters panel. + ToggleFilters + ] ); #[derive(Default)] diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 0af3949071f8b5645e606f2322787cfa06fc2cfd..5f57bfb4b1c11c22e34f22fd029d917673a45522 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -23,19 +23,35 @@ pub fn init(cx: &mut App) { actions!( search, [ + /// Focuses on the search input field. FocusSearch, + /// Toggles whole word matching. ToggleWholeWord, + /// Toggles case-sensitive search. ToggleCaseSensitive, + /// Toggles searching in ignored files. ToggleIncludeIgnored, + /// Toggles regular expression mode. ToggleRegex, + /// Toggles the replace interface. ToggleReplace, + /// Toggles searching within selection only. ToggleSelection, + /// Selects the next search match. SelectNextMatch, + /// Selects the previous search match. SelectPreviousMatch, + /// Selects all search matches. SelectAllMatches, + /// Cycles through search modes. + CycleMode, + /// Navigates to the next query in search history. NextHistoryQuery, + /// Navigates to the previous query in search history. PreviousHistoryQuery, + /// Replaces all matches. ReplaceAll, + /// Replaces the next match. ReplaceNext, ] ); diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index e725d2907fdd773cd57a266aa1f8100fd715c2f5..85bb8431458befcc88f7a79796446698247f8615 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -28,10 +28,26 @@ use crate::{ ui_components::table::{Table, TableInteractionState}, }; -actions!(zed, [OpenKeymapEditor]); +actions!( + zed, + [ + /// Opens the keymap editor. + OpenKeymapEditor + ] +); const KEYMAP_EDITOR_NAMESPACE: &'static str = "keymap_editor"; -actions!(keymap_editor, [EditBinding, CopyAction, CopyContext]); +actions!( + keymap_editor, + [ + /// Edits the selected key binding. + EditBinding, + /// Copies the action name to clipboard. + CopyAction, + /// Copies the context predicate to clipboard. + CopyContext + ] +); pub fn init(cx: &mut App) { let keymap_event_channel = KeymapEventChannel::new(); diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 28ffb1ab4a3ce6b300c16df545daeacba66887a0..2f0abb478933c215048b64b5fa7399981f7beebe 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -29,6 +29,7 @@ impl FeatureFlag for SettingsUiFeatureFlag { const NAME: &'static str = "settings-ui"; } +/// Imports settings from Visual Studio Code. #[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)] #[action(namespace = zed)] #[serde(deny_unknown_fields)] @@ -37,6 +38,7 @@ pub struct ImportVsCodeSettings { pub skip_prompt: bool, } +/// Imports settings from Cursor editor. #[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)] #[action(namespace = zed)] #[serde(deny_unknown_fields)] @@ -44,7 +46,13 @@ pub struct ImportCursorSettings { #[serde(default)] pub skip_prompt: bool, } -actions!(zed, [OpenSettingsEditor]); +actions!( + zed, + [ + /// Opens the settings editor. + OpenSettingsEditor + ] +); pub fn init(cx: &mut App) { cx.on_action(|_: &OpenSettingsEditor, cx| { diff --git a/crates/snippets_ui/src/snippets_ui.rs b/crates/snippets_ui/src/snippets_ui.rs index ecd1143c36ed11c65037323a2b94d28074afd15d..1cc16c55761508c11470b35715b8085447032114 100644 --- a/crates/snippets_ui/src/snippets_ui.rs +++ b/crates/snippets_ui/src/snippets_ui.rs @@ -54,7 +54,15 @@ impl From for ScopeName { } } -actions!(snippets, [ConfigureSnippets, OpenFolder]); +actions!( + snippets, + [ + /// Opens the snippets configuration file. + ConfigureSnippets, + /// Opens the snippets folder in the file manager. + OpenFolder + ] +); pub fn init(cx: &mut App) { cx.observe_new(register).detach(); diff --git a/crates/supermaven/src/supermaven.rs b/crates/supermaven/src/supermaven.rs index 410cc94c88424703d230aab549b4fd5e691714eb..ab500fb79d0584f07dd12a9b25b97c0a4393c01b 100644 --- a/crates/supermaven/src/supermaven.rs +++ b/crates/supermaven/src/supermaven.rs @@ -25,7 +25,13 @@ use std::{path::PathBuf, process::Stdio, sync::Arc}; use ui::prelude::*; use util::ResultExt; -actions!(supermaven, [SignOut]); +actions!( + supermaven, + [ + /// Signs out of Supermaven. + SignOut + ] +); pub fn init(client: Arc, cx: &mut App) { let supermaven = cx.new(|_| Supermaven::Starting); diff --git a/crates/svg_preview/src/svg_preview.rs b/crates/svg_preview/src/svg_preview.rs index cbee76be834b6db23860c2a67e8e8030c81a01b7..ca1891394d693b5a9817fcc43f462a4f611b94f2 100644 --- a/crates/svg_preview/src/svg_preview.rs +++ b/crates/svg_preview/src/svg_preview.rs @@ -5,7 +5,14 @@ pub mod svg_preview_view; actions!( svg, - [OpenPreview, OpenPreviewToTheSide, OpenFollowingPreview] + [ + /// Opens an SVG preview for the current file. + OpenPreview, + /// Opens an SVG preview in a split pane. + OpenPreviewToTheSide, + /// Opens a following SVG preview that syncs with the editor. + OpenFollowingPreview + ] ); pub fn init(cx: &mut App) { diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index f2fa7b8b699d69e1c915200b7a9ed8855e4e68f7..12af124ec78eb4c2b1bf6915131024d34ee64c93 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -25,6 +25,7 @@ use workspace::{ const PANEL_WIDTH_REMS: f32 = 28.; +/// Toggles the tab switcher interface. #[derive(PartialEq, Clone, Deserialize, JsonSchema, Default, Action)] #[action(namespace = tab_switcher)] #[serde(deny_unknown_fields)] @@ -32,7 +33,15 @@ pub struct Toggle { #[serde(default)] pub select_last: bool, } -actions!(tab_switcher, [CloseSelectedItem, ToggleAll]); +actions!( + tab_switcher, + [ + /// Closes the selected item in the tab switcher. + CloseSelectedItem, + /// Toggles between showing all tabs or just the current pane's tabs. + ToggleAll + ] +); pub struct TabSwitcher { picker: Entity>, diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index e187d2811ffb8dca79d6bbc5106952d384bb7aec..b1f86bf95ea971c67482983975bd727d213e7527 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -73,18 +73,36 @@ use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str}; actions!( terminal, [ + /// Clears the terminal screen. Clear, + /// Copies selected text to the clipboard. Copy, + /// Pastes from the clipboard. Paste, + /// Shows the character palette for special characters. ShowCharacterPalette, + /// Searches for text in the terminal. SearchTest, + /// Scrolls up by one line. ScrollLineUp, + /// Scrolls down by one line. ScrollLineDown, + /// Scrolls up by one page. ScrollPageUp, + /// Scrolls down by one page. ScrollPageDown, + /// Scrolls up by half a page. + ScrollHalfPageUp, + /// Scrolls down by half a page. + ScrollHalfPageDown, + /// Scrolls to the top of the terminal buffer. ScrollToTop, + /// Scrolls to the bottom of the terminal buffer. ScrollToBottom, + /// Toggles vi mode in the terminal. ToggleViMode, + /// Selects all text in the terminal. + SelectAll, ] ); diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index dc9313a38f9f588ae2d35cbd19f15148fa628996..8c55fed2a60127db4dd6fd0845f219507a8f4f78 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -46,7 +46,13 @@ use zed_actions::assistant::InlineAssist; const TERMINAL_PANEL_KEY: &str = "TerminalPanel"; -actions!(terminal_panel, [ToggleFocus]); +actions!( + terminal_panel, + [ + /// Toggles focus on the terminal panel. + ToggleFocus + ] +); pub fn init(cx: &mut App) { cx.observe_new( diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 23202ef69166820896047b983fb79f770a4e7676..4c1b60154faeedaed777fb21ebf967d9a8961e87 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -70,15 +70,23 @@ const GIT_DIFF_PATH_PREFIXES: &[&str] = &["a", "b"]; #[derive(Clone, Debug, PartialEq)] pub struct ScrollTerminal(pub i32); +/// Sends the specified text directly to the terminal. #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = terminal)] pub struct SendText(String); +/// Sends a keystroke sequence to the terminal. #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = terminal)] pub struct SendKeystroke(String); -actions!(terminal, [RerunTask]); +actions!( + terminal, + [ + /// Reruns the last executed task in the terminal. + RerunTask + ] +); pub fn init(cx: &mut App) { assistant_slash_command::init(cx); diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index e7a3f32909c6e5725e10e8ddc6168e35dc63f193..09d9877df874f192365a7bd595a62ee3cb108846 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -17,7 +17,13 @@ use zed_actions::{ExtensionCategoryFilter, Extensions}; use crate::icon_theme_selector::{IconThemeSelector, IconThemeSelectorDelegate}; -actions!(theme_selector, [Reload]); +actions!( + theme_selector, + [ + /// Reloads all themes from disk. + Reload + ] +); pub fn init(cx: &mut App) { cx.on_action(|action: &zed_actions::theme_selector::Toggle, cx| { diff --git a/crates/title_bar/src/application_menu.rs b/crates/title_bar/src/application_menu.rs index 58efa4ee3e3bd657e7c645f51861fa7ba524f63a..a7d99cf757eb9cb765597c6767c42e5d99c22061 100644 --- a/crates/title_bar/src/application_menu.rs +++ b/crates/title_bar/src/application_menu.rs @@ -12,7 +12,15 @@ use smallvec::SmallVec; use ui::{ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*}; #[cfg(not(target_os = "macos"))] -actions!(app_menu, [ActivateMenuRight, ActivateMenuLeft]); +actions!( + app_menu, + [ + /// Navigates to the menu item on the right. + ActivateMenuRight, + /// Navigates to the menu item on the left. + ActivateMenuLeft + ] +); #[cfg(not(target_os = "macos"))] #[derive(Clone, Deserialize, JsonSchema, PartialEq, Default, Action)] diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index dbef8e02bf3677a2857fd836d4bc1f8c62466337..b2a37a4f1c11c00139abe5c555f7ef254cc69f4c 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -11,7 +11,17 @@ use workspace::notifications::DetachAndPromptErr; use crate::TitleBar; -actions!(collab, [ToggleScreenSharing, ToggleMute, ToggleDeafen]); +actions!( + collab, + [ + /// Toggles screen sharing on or off. + ToggleScreenSharing, + /// Toggles microphone mute. + ToggleMute, + /// Toggles deafen mode (mute both microphone and speakers). + ToggleDeafen + ] +); fn toggle_screen_sharing(_: &ToggleScreenSharing, window: &mut Window, cx: &mut App) { let call = ActiveCall::global(cx).read(cx); diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 53d13972266db6caf17308fe1dda1fcbf63c8265..f2006f639d97703c43d08cf376ee8824e7cd425e 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -47,7 +47,17 @@ const MAX_PROJECT_NAME_LENGTH: usize = 40; const MAX_BRANCH_NAME_LENGTH: usize = 40; const MAX_SHORT_SHA_LENGTH: usize = 8; -actions!(collab, [ToggleUserMenu, ToggleProjectMenu, SwitchBranch]); +actions!( + collab, + [ + /// Toggles the user menu dropdown. + ToggleUserMenu, + /// Toggles the project menu dropdown. + ToggleProjectMenu, + /// Switches to a different git branch. + SwitchBranch + ] +); pub fn init(cx: &mut App) { TitleBarSettings::register(cx); diff --git a/crates/toolchain_selector/src/toolchain_selector.rs b/crates/toolchain_selector/src/toolchain_selector.rs index 0bb4de4f430bdafedf852c44cf9c8f7fe4f16000..21d95a66dea3e8cf8c999142baea352bd139a54e 100644 --- a/crates/toolchain_selector/src/toolchain_selector.rs +++ b/crates/toolchain_selector/src/toolchain_selector.rs @@ -15,7 +15,13 @@ use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*}; use util::ResultExt; use workspace::{ModalView, Workspace}; -actions!(toolchain, [Select]); +actions!( + toolchain, + [ + /// Selects a toolchain for the current project. + Select + ] +); pub fn init(cx: &mut App) { cx.observe_new(ToolchainSelector::register).detach(); diff --git a/crates/vim/src/change_list.rs b/crates/vim/src/change_list.rs index 25da3e09b8f6115273176cdb74e10e52aaeb951c..a59083f7aba55b0d459e56e5b8611e730a2fc404 100644 --- a/crates/vim/src/change_list.rs +++ b/crates/vim/src/change_list.rs @@ -3,7 +3,15 @@ use gpui::{Context, Window, actions}; use crate::{Vim, state::Mode}; -actions!(vim, [ChangeListOlder, ChangeListNewer]); +actions!( + vim, + [ + /// Navigates to an older position in the change list. + ChangeListOlder, + /// Navigates to a newer position in the change list. + ChangeListNewer + ] +); pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &ChangeListOlder, window, cx| { diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 83df86d0e887f9802e664db79cb8259d83495d1a..729e1a7b3c008c957f3f018f79bdcccf78a8b698 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -44,18 +44,21 @@ use crate::{ visual::VisualDeleteLine, }; +/// Goes to the specified line number in the editor. #[derive(Clone, Debug, PartialEq, Action)] #[action(namespace = vim, no_json, no_register)] pub struct GoToLine { range: CommandRange, } +/// Yanks (copies) text based on the specified range. #[derive(Clone, Debug, PartialEq, Action)] #[action(namespace = vim, no_json, no_register)] pub struct YankCommand { range: CommandRange, } +/// Executes a command with the specified range. #[derive(Clone, Debug, PartialEq, Action)] #[action(namespace = vim, no_json, no_register)] pub struct WithRange { @@ -64,6 +67,7 @@ pub struct WithRange { action: WrappedAction, } +/// Executes a command with the specified count. #[derive(Clone, Debug, PartialEq, Action)] #[action(namespace = vim, no_json, no_register)] pub struct WithCount { @@ -155,12 +159,14 @@ impl VimOption { } } +/// Sets vim options and configuration values. #[derive(Clone, PartialEq, Action)] #[action(namespace = vim, no_json, no_register)] pub struct VimSet { options: Vec, } +/// Saves the current file with optional save intent. #[derive(Clone, PartialEq, Action)] #[action(namespace = vim, no_json, no_register)] struct VimSave { @@ -168,6 +174,7 @@ struct VimSave { pub filename: String, } +/// Deletes the specified marks from the editor. #[derive(Clone, PartialEq, Action)] #[action(namespace = vim, no_json, no_register)] enum DeleteMarks { @@ -177,8 +184,18 @@ enum DeleteMarks { actions!( vim, - [VisualCommand, CountCommand, ShellCommand, ArgumentRequired] + [ + /// Executes a command in visual mode. + VisualCommand, + /// Executes a command with a count prefix. + CountCommand, + /// Executes a shell command. + ShellCommand, + /// Indicates that an argument is required for the command. + ArgumentRequired + ] ); +/// Opens the specified file for editing. #[derive(Clone, PartialEq, Action)] #[action(namespace = vim, no_json, no_register)] struct VimEdit { @@ -1282,6 +1299,7 @@ fn generate_positions(string: &str, query: &str) -> Vec { positions } +/// Applies a command to all lines matching a pattern. #[derive(Debug, PartialEq, Clone, Action)] #[action(namespace = vim, no_json, no_register)] pub(crate) struct OnMatchingLines { @@ -1480,6 +1498,7 @@ impl OnMatchingLines { } } +/// Executes a shell command and returns the output. #[derive(Clone, Debug, PartialEq, Action)] #[action(namespace = vim, no_json, no_register)] pub struct ShellExec { diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 42890d7a06093690c3b75175fabba9db58a0be71..e271c06a5e6942cac33e10783f123dcf3d963098 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -6,7 +6,13 @@ use text::SelectionGoal; use crate::{Vim, motion::Motion, state::Mode}; -actions!(vim, [HelixNormalAfter]); +actions!( + vim, + [ + /// Switches to normal mode after the cursor (Helix-style). + HelixNormalAfter + ] +); pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, Vim::helix_normal_after); diff --git a/crates/vim/src/indent.rs b/crates/vim/src/indent.rs index b10fff8b5d1b71a2c69edd3efe878dbb913fd17e..75b1857a5b953efa28497cf92193baad10959e3a 100644 --- a/crates/vim/src/indent.rs +++ b/crates/vim/src/indent.rs @@ -13,7 +13,17 @@ pub(crate) enum IndentDirection { Auto, } -actions!(vim, [Indent, Outdent, AutoIndent]); +actions!( + vim, + [ + /// Increases indentation of selected lines. + Indent, + /// Decreases indentation of selected lines. + Outdent, + /// Automatically adjusts indentation based on syntax. + AutoIndent + ] +); pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &Indent, window, cx| { diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index 7b38bed2be087085bf66e632c027af7aa858e6f3..89c60adee7f7c2a92b9f5c7d671cbcfac7045843 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -5,7 +5,15 @@ use language::SelectionGoal; use settings::Settings; use vim_mode_setting::HelixModeSetting; -actions!(vim, [NormalBefore, TemporaryNormal]); +actions!( + vim, + [ + /// Switches to normal mode with cursor positioned before the current character. + NormalBefore, + /// Temporarily switches to normal mode for one command. + TemporaryNormal + ] +); pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, Vim::normal_before); diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 2a6e5196bc01da9f8e6f3b6e12a9e0690757580f..a50b238cc5c6591f163f2fb89ef0a2cdf145d23c 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -176,6 +176,7 @@ enum IndentType { Same, } +/// Moves to the start of the next word. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -184,6 +185,7 @@ struct NextWordStart { ignore_punctuation: bool, } +/// Moves to the end of the next word. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -192,6 +194,7 @@ struct NextWordEnd { ignore_punctuation: bool, } +/// Moves to the start of the previous word. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -200,6 +203,7 @@ struct PreviousWordStart { ignore_punctuation: bool, } +/// Moves to the end of the previous word. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -208,6 +212,7 @@ struct PreviousWordEnd { ignore_punctuation: bool, } +/// Moves to the start of the next subword. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -216,6 +221,7 @@ pub(crate) struct NextSubwordStart { pub(crate) ignore_punctuation: bool, } +/// Moves to the end of the next subword. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -224,6 +230,7 @@ pub(crate) struct NextSubwordEnd { pub(crate) ignore_punctuation: bool, } +/// Moves to the start of the previous subword. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -232,6 +239,7 @@ pub(crate) struct PreviousSubwordStart { pub(crate) ignore_punctuation: bool, } +/// Moves to the end of the previous subword. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -240,6 +248,7 @@ pub(crate) struct PreviousSubwordEnd { pub(crate) ignore_punctuation: bool, } +/// Moves cursor up by the specified number of lines. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -248,6 +257,7 @@ pub(crate) struct Up { pub(crate) display_lines: bool, } +/// Moves cursor down by the specified number of lines. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -256,6 +266,7 @@ pub(crate) struct Down { pub(crate) display_lines: bool, } +/// Moves to the first non-whitespace character on the current line. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -264,6 +275,7 @@ struct FirstNonWhitespace { display_lines: bool, } +/// Moves to the end of the current line. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -272,6 +284,7 @@ struct EndOfLine { display_lines: bool, } +/// Moves to the start of the current line. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -280,6 +293,7 @@ pub struct StartOfLine { pub(crate) display_lines: bool, } +/// Moves to the middle of the current line. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -288,6 +302,7 @@ struct MiddleOfLine { display_lines: bool, } +/// Finds the next unmatched bracket or delimiter. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -296,6 +311,7 @@ struct UnmatchedForward { char: char, } +/// Finds the previous unmatched bracket or delimiter. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -307,46 +323,85 @@ struct UnmatchedBackward { actions!( vim, [ + /// Moves cursor left one character. Left, + /// Moves cursor left one character, wrapping to previous line. #[action(deprecated_aliases = ["vim::Backspace"])] WrappingLeft, + /// Moves cursor right one character. Right, + /// Moves cursor right one character, wrapping to next line. #[action(deprecated_aliases = ["vim::Space"])] WrappingRight, + /// Selects the current line. CurrentLine, + /// Moves to the start of the next sentence. SentenceForward, + /// Moves to the start of the previous sentence. SentenceBackward, + /// Moves to the start of the paragraph. StartOfParagraph, + /// Moves to the end of the paragraph. EndOfParagraph, + /// Moves to the start of the document. StartOfDocument, + /// Moves to the end of the document. EndOfDocument, + /// Moves to the matching bracket or delimiter. Matching, + /// Goes to a percentage position in the file. GoToPercentage, + /// Moves to the start of the next line. NextLineStart, + /// Moves to the start of the previous line. PreviousLineStart, + /// Moves to the start of a line downward. StartOfLineDownward, + /// Moves to the end of a line downward. EndOfLineDownward, + /// Goes to a specific column number. GoToColumn, + /// Repeats the last character find. RepeatFind, + /// Repeats the last character find in reverse. RepeatFindReversed, + /// Moves to the top of the window. WindowTop, + /// Moves to the middle of the window. WindowMiddle, + /// Moves to the bottom of the window. WindowBottom, + /// Moves to the start of the next section. NextSectionStart, + /// Moves to the end of the next section. NextSectionEnd, + /// Moves to the start of the previous section. PreviousSectionStart, + /// Moves to the end of the previous section. PreviousSectionEnd, + /// Moves to the start of the next method. NextMethodStart, + /// Moves to the end of the next method. NextMethodEnd, + /// Moves to the start of the previous method. PreviousMethodStart, + /// Moves to the end of the previous method. PreviousMethodEnd, + /// Moves to the next comment. NextComment, + /// Moves to the previous comment. PreviousComment, + /// Moves to the previous line with lesser indentation. PreviousLesserIndent, + /// Moves to the previous line with greater indentation. PreviousGreaterIndent, + /// Moves to the previous line with the same indentation. PreviousSameIndent, + /// Moves to the next line with lesser indentation. NextLesserIndent, + /// Moves to the next line with greater indentation. NextGreaterIndent, + /// Moves to the next line with the same indentation. NextSameIndent, ] ); diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index f25467aec454e92dbc77dde2fccecd0ccbf46986..f772c446fe3fcd791f75e830ffb68b98799dcb46 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -36,32 +36,59 @@ use multi_buffer::MultiBufferRow; actions!( vim, [ + /// Inserts text after the cursor. InsertAfter, + /// Inserts text before the cursor. InsertBefore, + /// Inserts at the first non-whitespace character. InsertFirstNonWhitespace, + /// Inserts at the end of the line. InsertEndOfLine, + /// Inserts a new line above the current line. InsertLineAbove, + /// Inserts a new line below the current line. InsertLineBelow, + /// Inserts an empty line above without entering insert mode. InsertEmptyLineAbove, + /// Inserts an empty line below without entering insert mode. InsertEmptyLineBelow, + /// Inserts at the previous insert position. InsertAtPrevious, + /// Joins the current line with the next line. JoinLines, + /// Joins lines without adding whitespace. JoinLinesNoWhitespace, + /// Deletes character to the left. DeleteLeft, + /// Deletes character to the right. DeleteRight, + /// Deletes using Helix-style behavior. HelixDelete, + /// Changes from cursor to end of line. ChangeToEndOfLine, + /// Deletes from cursor to end of line. DeleteToEndOfLine, + /// Yanks (copies) the selected text. Yank, + /// Yanks the entire line. YankLine, + /// Toggles the case of selected text. ChangeCase, + /// Converts selected text to uppercase. ConvertToUpperCase, + /// Converts selected text to lowercase. ConvertToLowerCase, + /// Applies ROT13 cipher to selected text. ConvertToRot13, + /// Applies ROT47 cipher to selected text. ConvertToRot47, + /// Toggles comments for selected lines. ToggleComments, + /// Shows the current location in the file. ShowLocation, + /// Undoes the last change. Undo, + /// Redoes the last undone change. Redo, ] ); diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index 09e6e85a5ccd057111dddca9e1bc76ebfacc1b63..51f6e4a0f9b980b8a3a45d13f7e2ce0d0bc19f2d 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -9,6 +9,7 @@ use crate::{Vim, state::Mode}; const BOOLEAN_PAIRS: &[(&str, &str)] = &[("true", "false"), ("yes", "no"), ("on", "off")]; +/// Increments the number under the cursor or toggles boolean values. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -17,6 +18,7 @@ struct Increment { step: bool, } +/// Decrements the number under the cursor or toggles boolean values. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 86a5392b87bfe3ede9bc518591c95df00d87f9a1..07712fbedd418026cb816b29eb7e5477e0baaf04 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -14,6 +14,7 @@ use crate::{ state::{Mode, Register}, }; +/// Pastes text from the specified register at the cursor position. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index 8799a8b635fa53ee576ce2ff47b3e540eaa87059..5cc37629905138ab7b1e651fbf3a1de3047a60a3 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -11,7 +11,19 @@ use editor::Editor; use gpui::{Action, App, Context, Window, actions}; use workspace::Workspace; -actions!(vim, [Repeat, EndRepeat, ToggleRecord, ReplayLastRecording]); +actions!( + vim, + [ + /// Repeats the last change. + Repeat, + /// Ends the repeat recording. + EndRepeat, + /// Toggles macro recording. + ToggleRecord, + /// Replays the last recorded macro. + ReplayLastRecording + ] +); fn should_replay(action: &dyn Action) -> bool { // skip so that we don't leave the character palette open diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index f227f982cbe522c61122e27e3ba3ae3413dbf3ca..150334376b0e6a6f26bd2e8afb63243e9c67dd2e 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -11,13 +11,21 @@ use settings::Settings; actions!( vim, [ + /// Scrolls up by one line. LineUp, + /// Scrolls down by one line. LineDown, + /// Scrolls right by one column. ColumnRight, + /// Scrolls left by one column. ColumnLeft, + /// Scrolls up by half a page. ScrollUp, + /// Scrolls down by half a page. ScrollDown, + /// Scrolls up by one page. PageUp, + /// Scrolls down by one page. PageDown ] ); diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 645779883341e264fd72f41d889981f2275186a0..182e60e56c20a4377a9b531ae919293769af7dba 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -16,6 +16,7 @@ use crate::{ state::{Mode, SearchState}, }; +/// Moves to the next search match. #[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -28,6 +29,7 @@ pub(crate) struct MoveToNext { regex: bool, } +/// Moves to the previous search match. #[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -40,6 +42,7 @@ pub(crate) struct MoveToPrevious { regex: bool, } +/// Initiates a search operation with the specified parameters. #[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -50,6 +53,7 @@ pub(crate) struct Search { regex: bool, } +/// Executes a find command to search for patterns in the buffer. #[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -58,6 +62,7 @@ pub struct FindCommand { pub backwards: bool, } +/// Executes a search and replace command within the specified range. #[derive(Clone, Debug, PartialEq, Action)] #[action(namespace = vim, no_json, no_register)] pub struct ReplaceCommand { @@ -73,7 +78,17 @@ pub(crate) struct Replacement { is_case_sensitive: bool, } -actions!(vim, [SearchSubmit, MoveToNextMatch, MoveToPreviousMatch]); +actions!( + vim, + [ + /// Submits the current search query. + SearchSubmit, + /// Moves to the next search match. + MoveToNextMatch, + /// Moves to the previous search match. + MoveToPreviousMatch + ] +); pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, Vim::move_to_next); diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index 96df61e528d3df3a480b978c78154d8c0c3a0150..a9752f288791618086f02c875f1671032d1f1d9f 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -7,7 +7,15 @@ use crate::{ motion::{Motion, MotionKind}, }; -actions!(vim, [Substitute, SubstituteLine]); +actions!( + vim, + [ + /// Substitutes characters in the current selection. + Substitute, + /// Substitutes the entire line. + SubstituteLine + ] +); pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &Substitute, window, cx| { diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 2cec4e254ae3ac49a934be9a1b80842ae4cd3f1b..63139d7e94cf1a38764a3d692b88b7bb1c235b31 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -46,6 +46,7 @@ pub enum Object { EntireFile, } +/// Selects a word text object. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -54,6 +55,7 @@ struct Word { ignore_punctuation: bool, } +/// Selects a subword text object. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -61,6 +63,7 @@ struct Subword { #[serde(default)] ignore_punctuation: bool, } +/// Selects text at the same indentation level. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] #[action(namespace = vim)] #[serde(deny_unknown_fields)] @@ -258,25 +261,45 @@ fn find_mini_brackets( actions!( vim, [ + /// Selects a sentence text object. Sentence, + /// Selects a paragraph text object. Paragraph, + /// Selects text within single quotes. Quotes, + /// Selects text within backticks. BackQuotes, + /// Selects text within the nearest quotes (single or double). MiniQuotes, + /// Selects text within any type of quotes. AnyQuotes, + /// Selects text within double quotes. DoubleQuotes, + /// Selects text within vertical bars (pipes). VerticalBars, + /// Selects text within parentheses. Parentheses, + /// Selects text within the nearest brackets. MiniBrackets, + /// Selects text within any type of brackets. AnyBrackets, + /// Selects text within square brackets. SquareBrackets, + /// Selects text within curly brackets. CurlyBrackets, + /// Selects text within angle brackets. AngleBrackets, + /// Selects a function argument. Argument, + /// Selects an HTML/XML tag. Tag, + /// Selects a method or function. Method, + /// Selects a class definition. Class, + /// Selects a comment block. Comment, + /// Selects the entire file. EntireFile ] ); diff --git a/crates/vim/src/replace.rs b/crates/vim/src/replace.rs index 15753e829003f829cddb93faa85b84104c7d92c8..aa857ef73e32a26cc8553b9e7ecf4371fba8cb3b 100644 --- a/crates/vim/src/replace.rs +++ b/crates/vim/src/replace.rs @@ -13,7 +13,15 @@ use language::{Point, SelectionGoal}; use std::ops::Range; use std::sync::Arc; -actions!(vim, [ToggleReplace, UndoReplace]); +actions!( + vim, + [ + /// Toggles replace mode. + ToggleReplace, + /// Undoes the last replacement. + UndoReplace + ] +); pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &ToggleReplace, window, cx| { diff --git a/crates/vim/src/rewrap.rs b/crates/vim/src/rewrap.rs index c1d157accbc0463a79a094a084a86748a122c552..4cd9449bfa80e46f671bfed3b428338c8675d329 100644 --- a/crates/vim/src/rewrap.rs +++ b/crates/vim/src/rewrap.rs @@ -4,7 +4,13 @@ use editor::{Bias, Editor, RewrapOptions, SelectionEffects, display_map::ToDispl use gpui::{Context, Window, actions}; use language::SelectionGoal; -actions!(vim, [Rewrap]); +actions!( + vim, + [ + /// Rewraps the selected text to fit within the line width. + Rewrap + ] +); pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &Rewrap, window, cx| { diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 2c2d60004e7aae6771906ff718c73b1dc0539723..9229f145d92f99725662d892da9f8eef3a980238 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -134,55 +134,105 @@ struct PushLiteral { actions!( vim, [ + /// Switches to normal mode. SwitchToNormalMode, + /// Switches to insert mode. SwitchToInsertMode, + /// Switches to replace mode. SwitchToReplaceMode, + /// Switches to visual mode. SwitchToVisualMode, + /// Switches to visual line mode. SwitchToVisualLineMode, + /// Switches to visual block mode. SwitchToVisualBlockMode, + /// Switches to Helix-style normal mode. SwitchToHelixNormalMode, + /// Clears any pending operators. ClearOperators, + /// Clears the exchange register. ClearExchange, + /// Inserts a tab character. Tab, + /// Inserts a newline. Enter, + /// Selects inner text object. InnerObject, + /// Maximizes the current pane. MaximizePane, + /// Opens the default keymap file. OpenDefaultKeymap, + /// Resets all pane sizes to default. ResetPaneSizes, + /// Resizes the pane to the right. ResizePaneRight, + /// Resizes the pane to the left. ResizePaneLeft, + /// Resizes the pane upward. ResizePaneUp, + /// Resizes the pane downward. ResizePaneDown, + /// Starts a change operation. PushChange, + /// Starts a delete operation. PushDelete, + /// Exchanges text regions. Exchange, + /// Starts a yank operation. PushYank, + /// Starts a replace operation. PushReplace, + /// Deletes surrounding characters. PushDeleteSurrounds, + /// Sets a mark at the current position. PushMark, + /// Toggles the marks view. ToggleMarksView, + /// Starts a forced motion. PushForcedMotion, + /// Starts an indent operation. PushIndent, + /// Starts an outdent operation. PushOutdent, + /// Starts an auto-indent operation. PushAutoIndent, + /// Starts a rewrap operation. PushRewrap, + /// Starts a shell command operation. PushShellCommand, + /// Converts to lowercase. PushLowercase, + /// Converts to uppercase. PushUppercase, + /// Toggles case. PushOppositeCase, + /// Applies ROT13 encoding. PushRot13, + /// Applies ROT47 encoding. PushRot47, + /// Toggles the registers view. ToggleRegistersView, + /// Selects a register. PushRegister, + /// Starts recording to a register. PushRecordRegister, + /// Replays a register. PushReplayRegister, + /// Replaces with register contents. PushReplaceWithRegister, + /// Toggles comments. PushToggleComments, ] ); // in the workspace namespace so it's not filtered out when vim is disabled. -actions!(workspace, [ToggleVimMode,]); +actions!( + workspace, + [ + /// Toggles Vim mode on or off. + ToggleVimMode, + ] +); /// Initializes the `vim` crate. pub fn init(cx: &mut App) { diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index c3da5d21438b0734b3e537411ddf3c8d37e53508..ca8734ba8b3c420366da8cc3c19af331930c543e 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -23,23 +23,41 @@ use crate::{ actions!( vim, [ + /// Toggles visual mode. ToggleVisual, + /// Toggles visual line mode. ToggleVisualLine, + /// Toggles visual block mode. ToggleVisualBlock, + /// Deletes the visual selection. VisualDelete, + /// Deletes entire lines in visual selection. VisualDeleteLine, + /// Yanks (copies) the visual selection. VisualYank, + /// Yanks entire lines in visual selection. VisualYankLine, + /// Moves cursor to the other end of the selection. OtherEnd, + /// Moves cursor to the other end of the selection (row-aware). OtherEndRowAware, + /// Selects the next occurrence of the current selection. SelectNext, + /// Selects the previous occurrence of the current selection. SelectPrevious, + /// Selects the next match of the current selection. SelectNextMatch, + /// Selects the previous match of the current selection. SelectPreviousMatch, + /// Selects the next smaller syntax node. SelectSmallerSyntaxNode, + /// Selects the next larger syntax node. SelectLargerSyntaxNode, + /// Restores the previous visual selection. RestoreVisualSelection, + /// Inserts at the end of each line in visual selection. VisualInsertEndOfLine, + /// Inserts at the first non-whitespace character of each line. VisualInsertFirstNonWhiteSpace, ] ); diff --git a/crates/welcome/src/base_keymap_picker.rs b/crates/welcome/src/base_keymap_picker.rs index 06cda8638a0717445e744ebd6915e11ba88e41cb..d5a6ae96da1345f4cefe6ac722e040cc82192f26 100644 --- a/crates/welcome/src/base_keymap_picker.rs +++ b/crates/welcome/src/base_keymap_picker.rs @@ -12,7 +12,13 @@ use ui::{ListItem, ListItemSpacing, prelude::*}; use util::ResultExt; use workspace::{ModalView, Workspace, ui::HighlightedLabel}; -actions!(welcome, [ToggleBaseKeymapSelector]); +actions!( + welcome, + [ + /// Toggles the base keymap selector modal. + ToggleBaseKeymapSelector + ] +); pub fn init(cx: &mut App) { cx.observe_new(|workspace: &mut Workspace, _window, _cx| { diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 31b5cb4325d4367fbf16a4d73c704d3873aa4e0b..74d7323d8cea4f0b427c1a0cb3ac1291d838baee 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -25,7 +25,13 @@ mod base_keymap_setting; mod multibuffer_hint; mod welcome_ui; -actions!(welcome, [ResetHints]); +actions!( + welcome, + [ + /// Resets the welcome screen hints to their initial state. + ResetHints + ] +); pub const FIRST_OPEN: &str = "first_open"; pub const DOCS_URL: &str = "https://zed.dev/docs/"; diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index b002d3ebe73d011fcc077fe72c21b64f0658dadb..fccf0ef8c26459a7e7dd0befae5b1c8f8423b91f 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -95,10 +95,12 @@ pub enum SaveIntent { Skip, } +/// Activates a specific item in the pane by its index. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] #[action(namespace = pane)] pub struct ActivateItem(pub usize); +/// Closes the currently active item in the pane. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] #[action(namespace = pane)] #[serde(deny_unknown_fields)] @@ -109,6 +111,7 @@ pub struct CloseActiveItem { pub close_pinned: bool, } +/// Closes all inactive items in the pane. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] #[action(namespace = pane)] #[serde(deny_unknown_fields)] @@ -119,6 +122,7 @@ pub struct CloseInactiveItems { pub close_pinned: bool, } +/// Closes all items in the pane. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] #[action(namespace = pane)] #[serde(deny_unknown_fields)] @@ -129,6 +133,7 @@ pub struct CloseAllItems { pub close_pinned: bool, } +/// Closes all items that have no unsaved changes. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] #[action(namespace = pane)] #[serde(deny_unknown_fields)] @@ -137,6 +142,7 @@ pub struct CloseCleanItems { pub close_pinned: bool, } +/// Closes all items to the right of the current item. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] #[action(namespace = pane)] #[serde(deny_unknown_fields)] @@ -145,6 +151,7 @@ pub struct CloseItemsToTheRight { pub close_pinned: bool, } +/// Closes all items to the left of the current item. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] #[action(namespace = pane)] #[serde(deny_unknown_fields)] @@ -153,6 +160,7 @@ pub struct CloseItemsToTheLeft { pub close_pinned: bool, } +/// Reveals the current item in the project panel. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] #[action(namespace = pane)] #[serde(deny_unknown_fields)] @@ -161,6 +169,7 @@ pub struct RevealInProjectPanel { pub entry_id: Option, } +/// Opens the search interface with the specified configuration. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] #[action(namespace = pane)] #[serde(deny_unknown_fields)] @@ -176,25 +185,45 @@ pub struct DeploySearch { actions!( pane, [ + /// Activates the previous item in the pane. ActivatePreviousItem, + /// Activates the next item in the pane. ActivateNextItem, + /// Activates the last item in the pane. ActivateLastItem, + /// Switches to the alternate file. AlternateFile, + /// Navigates back in history. GoBack, + /// Navigates forward in history. GoForward, + /// Joins this pane into the next pane. JoinIntoNext, + /// Joins all panes into one. JoinAll, + /// Reopens the most recently closed item. ReopenClosedItem, + /// Splits the pane to the left. SplitLeft, + /// Splits the pane upward. SplitUp, + /// Splits the pane to the right. SplitRight, + /// Splits the pane downward. SplitDown, + /// Splits the pane horizontally. SplitHorizontal, + /// Splits the pane vertically. SplitVertical, + /// Swaps the current item with the one to the left. SwapItemLeft, + /// Swaps the current item with the one to the right. SwapItemRight, + /// Toggles preview mode for the current tab. TogglePreviewTab, + /// Toggles pin status for the current tab. TogglePinTab, + /// Unpins all tabs in the pane. UnpinAllTabs, ] ); diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs index f9aee26cddca7013bfa2fd1c6fb7c27dcb20e17d..03164e0a647e01d8805b1186ccddf72acd96da99 100644 --- a/crates/workspace/src/theme_preview.rs +++ b/crates/workspace/src/theme_preview.rs @@ -11,7 +11,13 @@ use ui::{ use crate::{Item, Workspace}; -actions!(dev, [OpenThemePreview]); +actions!( + dev, + [ + /// Opens the theme preview window. + OpenThemePreview + ] +); pub fn init(cx: &mut App) { cx.observe_new(|workspace: &mut Workspace, _, _| { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 95e0a4aa686e68c05923b3e6937c593bfb19cf8d..91d0a20178718fb9118f4386af4bc3944ea9546f 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -169,44 +169,83 @@ pub trait DebuggerProvider { actions!( workspace, [ + /// Activates the next pane in the workspace. ActivateNextPane, + /// Activates the previous pane in the workspace. ActivatePreviousPane, + /// Switches to the next window. ActivateNextWindow, + /// Switches to the previous window. ActivatePreviousWindow, + /// Adds a folder to the current project. AddFolderToProject, + /// Clears all notifications. ClearAllNotifications, + /// Closes the active dock. CloseActiveDock, + /// Closes all docks. CloseAllDocks, + /// Closes the current window. CloseWindow, + /// Opens the feedback dialog. Feedback, + /// Follows the next collaborator in the session. FollowNextCollaborator, + /// Moves the focused panel to the next position. MoveFocusedPanelToNextPosition, + /// Opens a new terminal in the center. NewCenterTerminal, + /// Creates a new file. NewFile, + /// Creates a new file in a vertical split. NewFileSplitVertical, + /// Creates a new file in a horizontal split. NewFileSplitHorizontal, + /// Opens a new search. NewSearch, + /// Opens a new terminal. NewTerminal, + /// Opens a new window. NewWindow, + /// Opens a file or directory. Open, + /// Opens multiple files. OpenFiles, + /// Opens the current location in terminal. OpenInTerminal, + /// Opens the component preview. OpenComponentPreview, + /// Reloads the active item. ReloadActiveItem, + /// Resets the active dock to its default size. ResetActiveDockSize, + /// Resets all open docks to their default sizes. ResetOpenDocksSize, + /// Saves the current file with a new name. SaveAs, + /// Saves without formatting. SaveWithoutFormat, + /// Shuts down all debug adapters. ShutdownDebugAdapters, + /// Suppresses the current notification. SuppressNotification, + /// Toggles the bottom dock. ToggleBottomDock, + /// Toggles centered layout mode. ToggleCenteredLayout, + /// Toggles the left dock. ToggleLeftDock, + /// Toggles the right dock. ToggleRightDock, + /// Toggles zoom on the active pane. ToggleZoom, + /// Stops following a collaborator. Unfollow, + /// Shows the welcome screen. Welcome, + /// Restores the banner. RestoreBanner, + /// Toggles expansion of the selected item. ToggleExpandItem, ] ); @@ -216,10 +255,12 @@ pub struct OpenPaths { pub paths: Vec, } +/// Activates a specific pane by its index. #[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)] #[action(namespace = workspace)] pub struct ActivatePane(pub usize); +/// Moves an item to a specific pane by index. #[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)] #[action(namespace = workspace)] #[serde(deny_unknown_fields)] @@ -236,6 +277,7 @@ fn default_1() -> usize { 1 } +/// Moves an item to a pane in the specified direction. #[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)] #[action(namespace = workspace)] #[serde(deny_unknown_fields)] @@ -252,6 +294,7 @@ fn default_right() -> SplitDirection { SplitDirection::Right } +/// Saves all open files in the workspace. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = workspace)] #[serde(deny_unknown_fields)] @@ -260,6 +303,7 @@ pub struct SaveAll { pub save_intent: Option, } +/// Saves the current file with the specified options. #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = workspace)] #[serde(deny_unknown_fields)] @@ -268,6 +312,7 @@ pub struct Save { pub save_intent: Option, } +/// Closes all items and panes in the workspace. #[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)] #[action(namespace = workspace)] #[serde(deny_unknown_fields)] @@ -276,6 +321,7 @@ pub struct CloseAllItemsAndPanes { pub save_intent: Option, } +/// Closes all inactive tabs and panes in the workspace. #[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)] #[action(namespace = workspace)] #[serde(deny_unknown_fields)] @@ -284,10 +330,12 @@ pub struct CloseInactiveTabsAndPanes { pub save_intent: Option, } +/// Sends a sequence of keystrokes to the active element. #[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)] #[action(namespace = workspace)] pub struct SendKeystrokes(pub String); +/// Reloads the active item or workspace. #[derive(Clone, Deserialize, PartialEq, Default, JsonSchema, Action)] #[action(namespace = workspace)] #[serde(deny_unknown_fields)] @@ -298,11 +346,13 @@ pub struct Reload { actions!( project_symbols, [ + /// Toggles the project symbols search. #[action(name = "Toggle")] ToggleProjectSymbols ] ); +/// Toggles the file finder interface. #[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)] #[action(namespace = file_finder, name = "Toggle")] #[serde(deny_unknown_fields)] @@ -354,13 +404,21 @@ pub struct DecreaseOpenDocksSize { actions!( workspace, [ + /// Activates the pane to the left. ActivatePaneLeft, + /// Activates the pane to the right. ActivatePaneRight, + /// Activates the pane above. ActivatePaneUp, + /// Activates the pane below. ActivatePaneDown, + /// Swaps the current pane with the one to the left. SwapPaneLeft, + /// Swaps the current pane with the one to the right. SwapPaneRight, + /// Swaps the current pane with the one above. SwapPaneUp, + /// Swaps the current pane with the one below. SwapPaneDown, ] ); @@ -416,6 +474,7 @@ impl PartialEq for Toast { } } +/// Opens a new terminal with the specified working directory. #[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema, Action)] #[action(namespace = workspace)] #[serde(deny_unknown_fields)] @@ -6677,14 +6736,25 @@ actions!( /// can be copied via "Copy link to section" in the context menu of the channel notes /// buffer. These URLs look like `https://zed.dev/channel/channel-name-CHANNEL_ID/notes`. OpenChannelNotes, + /// Mutes your microphone. Mute, + /// Deafens yourself (mute both microphone and speakers). Deafen, + /// Leaves the current call. LeaveCall, + /// Shares the current project with collaborators. ShareProject, + /// Shares your screen with collaborators. ScreenShare ] ); -actions!(zed, [OpenLog]); +actions!( + zed, + [ + /// Opens the Zed log file. + OpenLog + ] +); async fn join_channel_internal( channel_id: ChannelId, diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 144df2d50e3f6c45b2b71cdb028c83347691bab7..89d9c2edf127ff4b75f3100be3b8600dbaa89c06 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -1368,6 +1368,7 @@ fn dump_all_gpui_actions() { name: &'static str, human_name: String, aliases: &'static [&'static str], + documentation: Option<&'static str>, } let mut actions = gpui::generate_list_of_all_registered_actions() .into_iter() @@ -1375,6 +1376,7 @@ fn dump_all_gpui_actions() { name: action.name, human_name: command_palette::humanize_action_name(action.name), aliases: action.deprecated_aliases, + documentation: action.documentation, }) .collect::>(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 944e6b26af3ed4295fb1f4b72763698153481103..10fdcf34a6a1de867668163f15fd3dfe0434f09c 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -78,19 +78,33 @@ use zed_actions::{ actions!( zed, [ + /// Opens the element inspector for debugging UI. DebugElements, + /// Hides the application window. Hide, + /// Hides all other application windows. HideOthers, + /// Minimizes the current window. Minimize, + /// Opens the default settings file. OpenDefaultSettings, + /// Opens project-specific settings. OpenProjectSettings, + /// Opens the project tasks configuration. OpenProjectTasks, + /// Opens the tasks panel. OpenTasks, + /// Opens debug tasks configuration. OpenDebugTasks, + /// Resets the application database. ResetDatabase, + /// Shows all hidden windows. ShowAll, + /// Toggles fullscreen mode. ToggleFullScreen, + /// Zooms the window. Zoom, + /// Triggers a test panic for debugging. TestPanic, ] ); diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index fa852084d60b1e4eff913b6560a86bd0bf701cbd..ffe232ad7bd7e63c6ebfe8af2d1a1c4f37029e85 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize}; // https://github.com/mmastrac/rust-ctor/issues/280 pub fn init() {} +/// Opens a URL in the system's default web browser. #[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] #[action(namespace = zed)] #[serde(deny_unknown_fields)] @@ -18,6 +19,7 @@ pub struct OpenBrowser { pub url: String, } +/// Opens a zed:// URL within the application. #[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] #[action(namespace = zed)] #[serde(deny_unknown_fields)] @@ -28,15 +30,25 @@ pub struct OpenZedUrl { actions!( zed, [ + /// Opens the settings editor. OpenSettings, + /// Opens the default keymap file. OpenDefaultKeymap, + /// Opens account settings. OpenAccountSettings, + /// Opens server settings. OpenServerSettings, + /// Quits the application. Quit, + /// Opens the user keymap file. OpenKeymap, + /// Shows information about Zed. About, + /// Opens the documentation website. OpenDocs, + /// Views open source licenses. OpenLicenses, + /// Opens the telemetry log. OpenTelemetryLog, ] ); @@ -56,6 +68,7 @@ pub enum ExtensionCategoryFilter { DebugAdapters, } +/// Opens the extensions management interface. #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = zed)] #[serde(deny_unknown_fields)] @@ -65,6 +78,7 @@ pub struct Extensions { pub category_filter: Option, } +/// Decreases the font size in the editor buffer. #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = zed)] #[serde(deny_unknown_fields)] @@ -73,6 +87,7 @@ pub struct DecreaseBufferFontSize { pub persist: bool, } +/// Increases the font size in the editor buffer. #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = zed)] #[serde(deny_unknown_fields)] @@ -81,6 +96,7 @@ pub struct IncreaseBufferFontSize { pub persist: bool, } +/// Resets the buffer font size to the default value. #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = zed)] #[serde(deny_unknown_fields)] @@ -89,6 +105,7 @@ pub struct ResetBufferFontSize { pub persist: bool, } +/// Decreases the font size of the user interface. #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = zed)] #[serde(deny_unknown_fields)] @@ -97,6 +114,7 @@ pub struct DecreaseUiFontSize { pub persist: bool, } +/// Increases the font size of the user interface. #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = zed)] #[serde(deny_unknown_fields)] @@ -105,6 +123,7 @@ pub struct IncreaseUiFontSize { pub persist: bool, } +/// Resets the UI font size to the default value. #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = zed)] #[serde(deny_unknown_fields)] @@ -116,7 +135,13 @@ pub struct ResetUiFontSize { pub mod dev { use gpui::actions; - actions!(dev, [ToggleInspector]); + actions!( + dev, + [ + /// Toggles the developer inspector for debugging UI elements. + ToggleInspector + ] + ); } pub mod workspace { @@ -139,9 +164,13 @@ pub mod git { actions!( git, [ + /// Checks out a different git branch. CheckoutBranch, + /// Switches to a different git branch. Switch, + /// Selects a different repository. SelectRepo, + /// Opens the git branch selector. #[action(deprecated_aliases = ["branches::OpenRecent"])] Branch ] @@ -151,25 +180,51 @@ pub mod git { pub mod jj { use gpui::actions; - actions!(jj, [BookmarkList]); + actions!( + jj, + [ + /// Opens the Jujutsu bookmark list. + BookmarkList + ] + ); } pub mod toast { use gpui::actions; - actions!(toast, [RunAction]); + actions!( + toast, + [ + /// Runs the action associated with a toast notification. + RunAction + ] + ); } pub mod command_palette { use gpui::actions; - actions!(command_palette, [Toggle]); + actions!( + command_palette, + [ + /// Toggles the command palette. + Toggle + ] + ); } pub mod feedback { use gpui::actions; - actions!(feedback, [FileBugReport, GiveFeedback]); + actions!( + feedback, + [ + /// Opens the bug report form. + FileBugReport, + /// Opens the feedback form. + GiveFeedback + ] + ); } pub mod theme_selector { @@ -177,6 +232,7 @@ pub mod theme_selector { use schemars::JsonSchema; use serde::Deserialize; + /// Toggles the theme selector interface. #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = theme_selector)] #[serde(deny_unknown_fields)] @@ -191,6 +247,7 @@ pub mod icon_theme_selector { use schemars::JsonSchema; use serde::Deserialize; + /// Toggles the icon theme selector interface. #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = icon_theme_selector)] #[serde(deny_unknown_fields)] @@ -205,7 +262,14 @@ pub mod agent { actions!( agent, - [OpenConfiguration, OpenOnboardingModal, ResetOnboarding] + [ + /// Opens the agent configuration panel. + OpenConfiguration, + /// Opens the agent onboarding modal. + OpenOnboardingModal, + /// Resets the agent onboarding state. + ResetOnboarding + ] ); } @@ -223,8 +287,15 @@ pub mod assistant { ] ); - actions!(assistant, [ShowConfiguration]); + actions!( + assistant, + [ + /// Shows the assistant configuration panel. + ShowConfiguration + ] + ); + /// Opens the rules library for managing agent rules and prompts. #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = agent, deprecated_aliases = ["assistant::OpenRulesLibrary", "assistant::DeployPromptLibrary"])] #[serde(deny_unknown_fields)] @@ -233,6 +304,7 @@ pub mod assistant { pub prompt_to_select: Option, } + /// Deploys the assistant interface with the specified configuration. #[derive(Clone, Default, Deserialize, PartialEq, JsonSchema, Action)] #[action(namespace = assistant)] #[serde(deny_unknown_fields)] @@ -244,9 +316,18 @@ pub mod assistant { pub mod debugger { use gpui::actions; - actions!(debugger, [OpenOnboardingModal, ResetOnboarding]); + actions!( + debugger, + [ + /// Opens the debugger onboarding modal. + OpenOnboardingModal, + /// Resets the debugger onboarding state. + ResetOnboarding + ] + ); } +/// Opens the recent projects interface. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = projects)] #[serde(deny_unknown_fields)] @@ -255,6 +336,7 @@ pub struct OpenRecent { pub create_new_window: bool, } +/// Creates a project from a selected template. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = projects)] #[serde(deny_unknown_fields)] @@ -276,7 +358,7 @@ pub enum RevealTarget { Dock, } -/// Spawn a task with name or open tasks modal. +/// Spawns a task with name or opens tasks modal. #[derive(Debug, PartialEq, Clone, Deserialize, JsonSchema, Action)] #[action(namespace = task)] #[serde(untagged)] @@ -309,7 +391,7 @@ impl Spawn { } } -/// Rerun the last task. +/// Reruns the last task. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = task)] #[serde(deny_unknown_fields)] @@ -350,15 +432,36 @@ pub mod outline { pub static TOGGLE_OUTLINE: OnceLock = OnceLock::new(); } -actions!(zed_predict_onboarding, [OpenZedPredictOnboarding]); -actions!(git_onboarding, [OpenGitIntegrationOnboarding]); +actions!( + zed_predict_onboarding, + [ + /// Opens the Zed Predict onboarding modal. + OpenZedPredictOnboarding + ] +); +actions!( + git_onboarding, + [ + /// Opens the git integration onboarding modal. + OpenGitIntegrationOnboarding + ] +); -actions!(debug_panel, [ToggleFocus]); +actions!( + debug_panel, + [ + /// Toggles focus on the debug panel. + ToggleFocus + ] +); actions!( debugger, [ + /// Toggles the enabled state of a breakpoint. ToggleEnableBreakpoint, + /// Removes a breakpoint. UnsetBreakpoint, + /// Opens the project debug tasks configuration. OpenProjectDebugTasks, ] ); diff --git a/crates/zeta/src/init.rs b/crates/zeta/src/init.rs index e63ac4ec3d7841dd827456823c0d50e788a6b8d2..6411e423a4d2e0b0f8b9e8b6e2e745a11e7864e6 100644 --- a/crates/zeta/src/init.rs +++ b/crates/zeta/src/init.rs @@ -10,7 +10,15 @@ use workspace::Workspace; use crate::{RateCompletionModal, onboarding_modal::ZedPredictModal}; -actions!(edit_prediction, [ResetOnboarding, RateCompletions]); +actions!( + edit_prediction, + [ + /// Resets the edit prediction onboarding state. + ResetOnboarding, + /// Opens the rate completions modal. + RateCompletions + ] +); pub fn init(cx: &mut App) { cx.observe_new(move |workspace: &mut Workspace, _, _cx| { diff --git a/crates/zeta/src/rate_completion_modal.rs b/crates/zeta/src/rate_completion_modal.rs index 811b838ebc13e9a48279838e5c7725ff7c0bd7f7..5a873fb8de70a42c8a8d0289a14e019f6ef3d0e5 100644 --- a/crates/zeta/src/rate_completion_modal.rs +++ b/crates/zeta/src/rate_completion_modal.rs @@ -9,11 +9,17 @@ use workspace::{ModalView, Workspace}; actions!( zeta, [ + /// Rates the active completion with a thumbs up. ThumbsUpActiveCompletion, + /// Rates the active completion with a thumbs down. ThumbsDownActiveCompletion, + /// Navigates to the next edit in the completion history. NextEdit, + /// Navigates to the previous edit in the completion history. PreviousEdit, + /// Focuses on the completions list. FocusCompletions, + /// Previews the selected completion. PreviewCompletion, ] ); diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 4d643c9db08da49144d26f28a416524fd5a3ceab..87cd1e604c3fd422c2ea9c218cbed755e72925cf 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -72,7 +72,13 @@ const MAX_EVENT_TOKENS: usize = 500; /// Maximum number of events to track. const MAX_EVENT_COUNT: usize = 16; -actions!(edit_prediction, [ClearHistory]); +actions!( + edit_prediction, + [ + /// Clears the edit prediction history. + ClearHistory + ] +); #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)] pub struct InlineCompletionId(Uuid); From 2a8121fbfc50feb6d6e2b20649b5261b1f339997 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 2 Jul 2025 21:36:12 -0600 Subject: [PATCH 011/239] Clean up project repositories / repository statuses too (#33803) Co-authored-by: Cole Miller Closes #ISSUE Release Notes: - N/A --------- Co-authored-by: Cole Miller --- .../20221109000000_test_schema.sql | 4 +- ...cascading_delete_to_repository_entries.sql | 25 +++++++++++ crates/collab/src/db/queries/servers.rs | 44 +++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 crates/collab/migrations/20250702185129_add_cascading_delete_to_repository_entries.sql diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 63c8ab8ef4cdff60cd1f1e6c05d878f8cd4d6fb4..91cf4d7af055cd18f884731d243f7ab04c0e45b2 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -107,7 +107,7 @@ CREATE INDEX "index_worktree_entries_on_project_id" ON "worktree_entries" ("proj CREATE INDEX "index_worktree_entries_on_project_id_and_worktree_id" ON "worktree_entries" ("project_id", "worktree_id"); CREATE TABLE "project_repositories" ( - "project_id" INTEGER NOT NULL, + "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, "abs_path" VARCHAR, "id" INTEGER NOT NULL, "entry_ids" VARCHAR, @@ -124,7 +124,7 @@ CREATE TABLE "project_repositories" ( CREATE INDEX "index_project_repositories_on_project_id" ON "project_repositories" ("project_id"); CREATE TABLE "project_repository_statuses" ( - "project_id" INTEGER NOT NULL, + "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, "repository_id" INTEGER NOT NULL, "repo_path" VARCHAR NOT NULL, "status" INT8 NOT NULL, diff --git a/crates/collab/migrations/20250702185129_add_cascading_delete_to_repository_entries.sql b/crates/collab/migrations/20250702185129_add_cascading_delete_to_repository_entries.sql new file mode 100644 index 0000000000000000000000000000000000000000..6d898c481199f4770ab7df5ce66c08e2fdf42423 --- /dev/null +++ b/crates/collab/migrations/20250702185129_add_cascading_delete_to_repository_entries.sql @@ -0,0 +1,25 @@ +DELETE FROM project_repositories +WHERE project_id NOT IN (SELECT id FROM projects); + +ALTER TABLE project_repositories + ADD CONSTRAINT fk_project_repositories_project_id + FOREIGN KEY (project_id) + REFERENCES projects (id) + ON DELETE CASCADE + NOT VALID; + +ALTER TABLE project_repositories + VALIDATE CONSTRAINT fk_project_repositories_project_id; + +DELETE FROM project_repository_statuses +WHERE project_id NOT IN (SELECT id FROM projects); + +ALTER TABLE project_repository_statuses + ADD CONSTRAINT fk_project_repository_statuses_project_id + FOREIGN KEY (project_id) + REFERENCES projects (id) + ON DELETE CASCADE + NOT VALID; + +ALTER TABLE project_repository_statuses + VALIDATE CONSTRAINT fk_project_repository_statuses_project_id; diff --git a/crates/collab/src/db/queries/servers.rs b/crates/collab/src/db/queries/servers.rs index 73deaaffb68f2c50bb38d2d08fa71782e4600123..da6ff77cf0c5405834939e346ba1ea613199d430 100644 --- a/crates/collab/src/db/queries/servers.rs +++ b/crates/collab/src/db/queries/servers.rs @@ -142,6 +142,50 @@ impl Database { } } + loop { + let delete_query = Query::delete() + .from_table(project_repository_statuses::Entity) + .and_where( + Expr::tuple([Expr::col(( + project_repository_statuses::Entity, + project_repository_statuses::Column::ProjectId, + )) + .into()]) + .in_subquery( + Query::select() + .columns([( + project_repository_statuses::Entity, + project_repository_statuses::Column::ProjectId, + )]) + .from(project_repository_statuses::Entity) + .inner_join( + project::Entity, + Expr::col((project::Entity, project::Column::Id)).equals(( + project_repository_statuses::Entity, + project_repository_statuses::Column::ProjectId, + )), + ) + .and_where(project::Column::HostConnectionServerId.ne(server_id)) + .limit(10000) + .to_owned(), + ), + ) + .to_owned(); + + let statement = Statement::from_sql_and_values( + tx.get_database_backend(), + delete_query + .to_string(sea_orm::sea_query::PostgresQueryBuilder) + .as_str(), + vec![], + ); + + let result = tx.execute(statement).await?; + if result.rows_affected() == 0 { + break; + } + } + Ok(()) }) .await From 5c88e9c66b0618707da21063254c8a8332110d43 Mon Sep 17 00:00:00 2001 From: chico ferreira <36338391+chicoferreira@users.noreply.github.com> Date: Thu, 3 Jul 2025 07:37:27 +0100 Subject: [PATCH 012/239] terminal: Expose `selection` in context and add `keep_selection_on_copy` setting (#33491) Closes #21262 Introduces a new setting `keep_selection_on_copy`, which controls whether the current text selection is preserved after copying in the terminal. The default behavior remains the same (`true`), but setting it to `false` will clear the selection after the copy operation, matching VSCode's behavior. Additionally, the terminal context now exposes a `selection` flag whenever text is selected. This allows users to match VSCode and other terminal's smart copy behavior. Release Notes: - Expose `selection` to terminal context when there is text selected in the terminal - Add `keep_selection_on_copy` terminal setting. Can be set to false to clear the text selection when copying text. **VSCode Behavior Example:** **settings.json:** ```json "terminal": { "keep_selection_on_copy": false }, ``` **keymap.json:** ```json { "context": "Terminal && selection", "bindings": { "ctrl-c": "terminal::Copy" } } ``` --- assets/settings/default.json | 2 ++ crates/terminal/src/terminal.rs | 8 +++++++- crates/terminal/src/terminal_settings.rs | 5 +++++ crates/terminal_view/src/terminal_view.rs | 5 +++++ docs/src/configuring-zed.md | 21 +++++++++++++++++++++ 5 files changed, 40 insertions(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index f2effd7fbd278431945a5d6f4b8292cb98cade06..56fd9353ccf9fdbcd1b24871f40a7bc2d234b7a2 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1292,6 +1292,8 @@ // Whether or not selecting text in the terminal will automatically // copy to the system clipboard. "copy_on_select": false, + // Whether to keep the text selection after copying it to the clipboard + "keep_selection_on_copy": false, // Whether to show the terminal button in the status bar "button": true, // Any key-value pairs added to this list will be added to the terminal's diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index b1f86bf95ea971c67482983975bd727d213e7527..a096ef8901a6be491ca90be2040a0cbcbc7f0f30 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -898,7 +898,13 @@ impl Terminal { InternalEvent::Copy => { if let Some(txt) = term.selection_to_string() { - cx.write_to_clipboard(ClipboardItem::new_string(txt)) + cx.write_to_clipboard(ClipboardItem::new_string(txt)); + + let settings = TerminalSettings::get_global(cx); + + if !settings.keep_selection_on_copy { + self.events.push_back(InternalEvent::SetSelection(None)); + } } } InternalEvent::ScrollToAlacPoint(point) => { diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index d588d3680bbefbc522245a5ca709c2ef99de83f8..f1b729987a61dc7d36e6c8aed00e646351cff57c 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -40,6 +40,7 @@ pub struct TerminalSettings { pub alternate_scroll: AlternateScroll, pub option_as_meta: bool, pub copy_on_select: bool, + pub keep_selection_on_copy: bool, pub button: bool, pub dock: TerminalDockPosition, pub default_width: Pixels, @@ -193,6 +194,10 @@ pub struct TerminalSettingsContent { /// /// Default: false pub copy_on_select: Option, + /// Whether to keep the text selection after copying it to the clipboard. + /// + /// Default: false + pub keep_selection_on_copy: Option, /// Whether to show the terminal button in the status bar. /// /// Default: true diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 4c1b60154faeedaed777fb21ebf967d9a8961e87..be167d820db70defb8cf4e93bd6d227092d72000 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -823,6 +823,11 @@ impl TerminalView { }; dispatch_context.set("mouse_format", format); }; + + if self.terminal.read(cx).last_content.selection.is_some() { + dispatch_context.add("selection"); + } + dispatch_context } diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 25c4bd98e83bd1a6dca259c6fe69139efbc83ce6..8bba431554f5a83c97135772a8329cb822c3ccd4 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -2564,6 +2564,7 @@ List of `integer` column numbers "alternate_scroll": "off", "blinking": "terminal_controlled", "copy_on_select": false, + "keep_selection_on_copy": false, "dock": "bottom", "default_width": 640, "default_height": 320, @@ -2688,6 +2689,26 @@ List of `integer` column numbers } ``` +### Terminal: Keep Selection On Copy + +- Description: Whether or not to keep the selection in the terminal after copying text. +- Setting: `keep_selection_on_copy` +- Default: `false` + +**Options** + +`boolean` values + +**Example** + +```json +{ + "terminal": { + "keep_selection_on_copy": true + } +} +``` + ### Terminal: Env - Description: Any key-value pairs added to this object will be added to the terminal's environment. Keys must be unique, use `:` to separate multiple values in a single variable From 06ddc74917e5905d1ab02f7f38d8c52f9b2e575e Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Thu, 3 Jul 2025 10:44:17 +0200 Subject: [PATCH 013/239] editor: Reapply fix for wrap guide positioning (#33776) Reapplies the fix from #33514 which was removed in #33554. Wrap guides are currently drifting again due to this on main. Slightly changed the approach here so that we now actually only save the wrap guides in the `EditorLayout` that will actually be painted. Also ensures that we paint indent guides that were previously hidden behind the vertical scrollbar once it auto-hides. I wanted to add tests for this, however, I am rather sure this depends on the work/fixes in #33590 and thus I'd prefer to add these later so we can have this fix in the next release. Release Notes: - N/A --- crates/editor/src/element.rs | 108 +++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 50 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a4b2ceb5de5132ffda00f2be444205c77aac57a8..2aea0ef956b77805eb65c512717101242f220a19 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2444,7 +2444,7 @@ impl EditorElement { .git .inline_blame .and_then(|settings| settings.min_column) - .map(|col| self.column_pixels(col as usize, window, cx)) + .map(|col| self.column_pixels(col as usize, window)) .unwrap_or(px(0.)); let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels; @@ -2629,7 +2629,7 @@ impl EditorElement { .enumerate() .filter_map(|(i, indent_guide)| { let single_indent_width = - self.column_pixels(indent_guide.tab_size as usize, window, cx); + self.column_pixels(indent_guide.tab_size as usize, window); let total_width = single_indent_width * indent_guide.depth as f32; let start_x = content_origin.x + total_width - scroll_pixel_position.x; if start_x >= text_origin.x { @@ -2657,6 +2657,39 @@ impl EditorElement { ) } + fn layout_wrap_guides( + &self, + em_advance: Pixels, + scroll_position: gpui::Point, + content_origin: gpui::Point, + scrollbar_layout: Option<&EditorScrollbars>, + vertical_scrollbar_width: Pixels, + hitbox: &Hitbox, + window: &Window, + cx: &App, + ) -> SmallVec<[(Pixels, bool); 2]> { + let scroll_left = scroll_position.x * em_advance; + let content_origin = content_origin.x; + let horizontal_offset = content_origin - scroll_left; + let vertical_scrollbar_width = scrollbar_layout + .and_then(|layout| layout.visible.then_some(vertical_scrollbar_width)) + .unwrap_or_default(); + + self.editor + .read(cx) + .wrap_guides(cx) + .into_iter() + .flat_map(|(guide, active)| { + let wrap_position = self.column_pixels(guide, window); + let wrap_guide_x = wrap_position + horizontal_offset; + let display_wrap_guide = wrap_guide_x >= content_origin + && wrap_guide_x <= hitbox.bounds.right() - vertical_scrollbar_width; + + display_wrap_guide.then_some((wrap_guide_x, active)) + }) + .collect() + } + fn calculate_indent_guide_bounds( row_range: Range, line_height: Pixels, @@ -5240,26 +5273,7 @@ impl EditorElement { paint_highlight(range.start, range.end, color, edges); } - let scroll_left = - layout.position_map.snapshot.scroll_position().x * layout.position_map.em_width; - - for (wrap_position, active) in layout.wrap_guides.iter() { - let x = (layout.position_map.text_hitbox.origin.x - + *wrap_position - + layout.position_map.em_width / 2.) - - scroll_left; - - let show_scrollbars = layout - .scrollbars_layout - .as_ref() - .map_or(false, |layout| layout.visible); - - if x < layout.position_map.text_hitbox.origin.x - || (show_scrollbars && x > self.scrollbar_left(&layout.hitbox.bounds)) - { - continue; - } - + for (guide_x, active) in layout.wrap_guides.iter() { let color = if *active { cx.theme().colors().editor_active_wrap_guide } else { @@ -5267,7 +5281,7 @@ impl EditorElement { }; window.paint_quad(fill( Bounds { - origin: point(x, layout.position_map.text_hitbox.origin.y), + origin: point(*guide_x, layout.position_map.text_hitbox.origin.y), size: size(px(1.), layout.position_map.text_hitbox.size.height), }, color, @@ -6678,7 +6692,7 @@ impl EditorElement { let position_map: &PositionMap = &position_map; let line_height = position_map.line_height; - let max_glyph_width = position_map.em_width; + let max_glyph_advance = position_map.em_advance; let (delta, axis) = match delta { gpui::ScrollDelta::Pixels(mut pixels) => { //Trackpad @@ -6689,15 +6703,15 @@ impl EditorElement { gpui::ScrollDelta::Lines(lines) => { //Not trackpad let pixels = - point(lines.x * max_glyph_width, lines.y * line_height); + point(lines.x * max_glyph_advance, lines.y * line_height); (pixels, None) } }; let current_scroll_position = position_map.snapshot.scroll_position(); - let x = (current_scroll_position.x * max_glyph_width + let x = (current_scroll_position.x * max_glyph_advance - (delta.x * scroll_sensitivity)) - / max_glyph_width; + / max_glyph_advance; let y = (current_scroll_position.y * line_height - (delta.y * scroll_sensitivity)) / line_height; @@ -6858,11 +6872,7 @@ impl EditorElement { }); } - fn scrollbar_left(&self, bounds: &Bounds) -> Pixels { - bounds.top_right().x - self.style.scrollbar_width - } - - fn column_pixels(&self, column: usize, window: &mut Window, _: &mut App) -> Pixels { + fn column_pixels(&self, column: usize, window: &Window) -> Pixels { let style = &self.style; let font_size = style.text.font_size.to_pixels(window.rem_size()); let layout = window.text_system().shape_line( @@ -6881,14 +6891,9 @@ impl EditorElement { layout.width } - fn max_line_number_width( - &self, - snapshot: &EditorSnapshot, - window: &mut Window, - cx: &mut App, - ) -> Pixels { + fn max_line_number_width(&self, snapshot: &EditorSnapshot, window: &mut Window) -> Pixels { let digit_count = snapshot.widest_line_number().ilog10() + 1; - self.column_pixels(digit_count as usize, window, cx) + self.column_pixels(digit_count as usize, window) } fn shape_line_number( @@ -7789,7 +7794,7 @@ impl Element for EditorElement { } => { let editor_handle = cx.entity().clone(); let max_line_number_width = - self.max_line_number_width(&editor.snapshot(window, cx), window, cx); + self.max_line_number_width(&editor.snapshot(window, cx), window); window.request_measured_layout( Style::default(), move |known_dimensions, available_space, window, cx| { @@ -7879,7 +7884,7 @@ impl Element for EditorElement { .gutter_dimensions( font_id, font_size, - self.max_line_number_width(&snapshot, window, cx), + self.max_line_number_width(&snapshot, window), cx, ) .or_else(|| { @@ -7954,14 +7959,6 @@ impl Element for EditorElement { } }); - let wrap_guides = self - .editor - .read(cx) - .wrap_guides(cx) - .iter() - .map(|(guide, active)| (self.column_pixels(*guide, window, cx), *active)) - .collect::>(); - let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); let gutter_hitbox = window.insert_hitbox( gutter_bounds(bounds, gutter_dimensions), @@ -8593,7 +8590,7 @@ impl Element for EditorElement { start_row, editor_content_width, scroll_width, - em_width, + em_advance, &line_layouts, cx, ) @@ -8797,6 +8794,17 @@ impl Element for EditorElement { self.prepaint_expand_toggles(&mut expand_toggles, window, cx) }); + let wrap_guides = self.layout_wrap_guides( + em_advance, + scroll_position, + content_origin, + scrollbars_layout.as_ref(), + vertical_scrollbar_width, + &hitbox, + window, + cx, + ); + let minimap = window.with_element_namespace("minimap", |window| { self.layout_minimap( &snapshot, From 48c85550764114c23700069c7175559fa0dbfb0c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 3 Jul 2025 12:15:50 +0300 Subject: [PATCH 014/239] Show more info in the UI and logs (#33841) Addresses the `The binding is not displayed, though:` part from https://github.com/zed-industries/zed/discussions/29498#discussioncomment-13649543 Release Notes: - N/A --- crates/task/src/vscode_format.rs | 5 ++++- crates/workspace/src/pane.rs | 4 +--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/task/src/vscode_format.rs b/crates/task/src/vscode_format.rs index 8f04b48127c3e6f0c298cccec3e810a22939f2b6..9078a73fbb1d2bf747af4bee25c364f6c08862f6 100644 --- a/crates/task/src/vscode_format.rs +++ b/crates/task/src/vscode_format.rs @@ -47,7 +47,10 @@ impl VsCodeTaskDefinition { replacer: &EnvVariableReplacer, ) -> anyhow::Result> { if self.other_attributes.contains_key("dependsOn") { - log::warn!("Skipping deserializing of a task with the unsupported `dependsOn` key"); + log::warn!( + "Skipping deserializing of a task `{}` with the unsupported `dependsOn` key", + self.label + ); return Ok(None); } // `type` might not be set in e.g. tasks that use `dependsOn`; we still want to deserialize the whole object though (hence command is an Option), diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index fccf0ef8c26459a7e7dd0befae5b1c8f8423b91f..56db7fa57009b739909bcfa40c4b0a28967f776b 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2735,9 +2735,7 @@ impl Pane { .when(visible_in_project_panel, |menu| { menu.entry( "Reveal In Project Panel", - Some(Box::new(RevealInProjectPanel { - entry_id: Some(entry_id), - })), + Some(Box::new(RevealInProjectPanel::default())), window.handler_for(&pane, move |pane, _, cx| { pane.project .update(cx, |_, cx| { From 968587a7456b89ed9cefc1be91b1ebf97ec6158e Mon Sep 17 00:00:00 2001 From: marius851000 Date: Thu, 3 Jul 2025 11:26:00 +0200 Subject: [PATCH 015/239] worspace: Add partial window bound fix when switching between CSD and SSD on Wayland (#31335) Partial fix for #31330 It fix the problem that the inset stay on after switching to SSD, but it still have the problem that after that first redraw, it have the wrong size. Just resizing it even once work. I guess the relevant code to fix that would be ``handle_toplevel_decoration_event`` of ``crates/gpui/src/platform/linux/wayland/window.rs``, but trying to call resize here does not seems to work correctly (might be just wrong argument), and I would like to take a break on that for now. Release Notes: - N/A (better wait for that to be completely fixed before adding it in the changelog) --------- Co-authored-by: Michael Sloan --- crates/workspace/src/workspace.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 91d0a20178718fb9118f4386af4bc3944ea9546f..141cd36efd94689d2f45ec303bcb2851f174aec3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -7561,6 +7561,7 @@ fn parse_pixel_size_env_var(value: &str) -> Option> { Some(size(px(width as f32), px(height as f32))) } +/// Add client-side decorations (rounded corners, shadows, resize handling) when appropriate. pub fn client_side_decorations( element: impl IntoElement, window: &mut Window, @@ -7569,8 +7570,9 @@ pub fn client_side_decorations( const BORDER_SIZE: Pixels = px(1.0); let decorations = window.window_decorations(); - if matches!(decorations, Decorations::Client { .. }) { - window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW); + match decorations { + Decorations::Client { .. } => window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW), + Decorations::Server { .. } => window.set_client_inset(px(0.0)), } struct GlobalResizeEdge(ResizeEdge); From cdb7564d8955282267b0be549baa7837c44a2327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Thu, 3 Jul 2025 17:27:27 +0800 Subject: [PATCH 016/239] windows: More precise handling of `WM_SETTINGCHANGE` and appearance updates (#33829) This PR adds more fine-grained handling of the `WM_SETTINGCHANGE` message. Plus, we now only trigger the `appearance_changed` callback when the actual window appearance has changed, rather than calling it every time. Release Notes: - N/A --- crates/gpui/src/platform/windows/events.rs | 73 ++++++++++--------- .../src/platform/windows/system_settings.rs | 22 +++++- crates/gpui/src/platform/windows/util.rs | 4 +- crates/gpui/src/platform/windows/window.rs | 36 +++++++-- 4 files changed, 89 insertions(+), 46 deletions(-) diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index d7205580cdc133fccbf97f1287651521ff7bb06b..a43fdc097f24a5c30544817f60c992829dbc831a 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -93,7 +93,7 @@ pub(crate) fn handle_msg( WM_IME_STARTCOMPOSITION => handle_ime_position(handle, state_ptr), WM_IME_COMPOSITION => handle_ime_composition(handle, lparam, state_ptr), WM_SETCURSOR => handle_set_cursor(handle, lparam, state_ptr), - WM_SETTINGCHANGE => handle_system_settings_changed(handle, lparam, state_ptr), + WM_SETTINGCHANGE => handle_system_settings_changed(handle, wparam, lparam, state_ptr), WM_INPUTLANGCHANGE => handle_input_language_changed(lparam, state_ptr), WM_GPUI_CURSOR_STYLE_CHANGED => handle_cursor_changed(lparam, state_ptr), _ => None, @@ -1152,37 +1152,23 @@ fn handle_set_cursor( fn handle_system_settings_changed( handle: HWND, + wparam: WPARAM, lparam: LPARAM, state_ptr: Rc, ) -> Option { - let mut lock = state_ptr.state.borrow_mut(); - let display = lock.display; - // system settings - lock.system_settings.update(display); - // mouse double click - lock.click_state.system_update(); - // window border offset - lock.border_offset.update(handle).log_err(); - drop(lock); - - // lParam is a pointer to a string that indicates the area containing the system parameter - // that was changed. - let parameter = PCWSTR::from_raw(lparam.0 as _); - if unsafe { !parameter.is_null() && !parameter.is_empty() } { - if let Some(parameter_string) = unsafe { parameter.to_string() }.log_err() { - log::info!("System settings changed: {}", parameter_string); - match parameter_string.as_str() { - "ImmersiveColorSet" => { - handle_system_theme_changed(handle, state_ptr); - } - _ => {} - } - } - } - + if wparam.0 != 0 { + let mut lock = state_ptr.state.borrow_mut(); + let display = lock.display; + lock.system_settings.update(display, wparam.0); + lock.click_state.system_update(wparam.0); + lock.border_offset.update(handle).log_err(); + } else { + handle_system_theme_changed(handle, lparam, state_ptr)?; + }; // Force to trigger WM_NCCALCSIZE event to ensure that we handle auto hide // taskbar correctly. notify_frame_changed(handle); + Some(0) } @@ -1199,17 +1185,34 @@ fn handle_system_command(wparam: WPARAM, state_ptr: Rc) - fn handle_system_theme_changed( handle: HWND, + lparam: LPARAM, state_ptr: Rc, ) -> Option { - let mut callback = state_ptr - .state - .borrow_mut() - .callbacks - .appearance_changed - .take()?; - callback(); - state_ptr.state.borrow_mut().callbacks.appearance_changed = Some(callback); - configure_dwm_dark_mode(handle); + // lParam is a pointer to a string that indicates the area containing the system parameter + // that was changed. + let parameter = PCWSTR::from_raw(lparam.0 as _); + if unsafe { !parameter.is_null() && !parameter.is_empty() } { + if let Some(parameter_string) = unsafe { parameter.to_string() }.log_err() { + log::info!("System settings changed: {}", parameter_string); + match parameter_string.as_str() { + "ImmersiveColorSet" => { + let new_appearance = system_appearance() + .context("unable to get system appearance when handling ImmersiveColorSet") + .log_err()?; + let mut lock = state_ptr.state.borrow_mut(); + if new_appearance != lock.appearance { + lock.appearance = new_appearance; + let mut callback = lock.callbacks.appearance_changed.take()?; + drop(lock); + callback(); + state_ptr.state.borrow_mut().callbacks.appearance_changed = Some(callback); + configure_dwm_dark_mode(handle, new_appearance); + } + } + _ => {} + } + } + } Some(0) } diff --git a/crates/gpui/src/platform/windows/system_settings.rs b/crates/gpui/src/platform/windows/system_settings.rs index d8e3513b3c33da556a9258810cf86d8da9fe43a3..b2bd289cd00979541f0176a4ccea6a52143b9ddd 100644 --- a/crates/gpui/src/platform/windows/system_settings.rs +++ b/crates/gpui/src/platform/windows/system_settings.rs @@ -32,14 +32,32 @@ pub(crate) struct MouseWheelSettings { impl WindowsSystemSettings { pub(crate) fn new(display: WindowsDisplay) -> Self { let mut settings = Self::default(); - settings.update(display); + settings.init(display); settings } - pub(crate) fn update(&mut self, display: WindowsDisplay) { + fn init(&mut self, display: WindowsDisplay) { self.mouse_wheel_settings.update(); self.auto_hide_taskbar_position = AutoHideTaskbarPosition::new(display).log_err().flatten(); } + + pub(crate) fn update(&mut self, display: WindowsDisplay, wparam: usize) { + match wparam { + // SPI_SETWORKAREA + 47 => self.update_taskbar_position(display), + // SPI_GETWHEELSCROLLLINES, SPI_GETWHEELSCROLLCHARS + 104 | 108 => self.update_mouse_wheel_settings(), + _ => {} + } + } + + fn update_mouse_wheel_settings(&mut self) { + self.mouse_wheel_settings.update(); + } + + fn update_taskbar_position(&mut self, display: WindowsDisplay) { + self.auto_hide_taskbar_position = AutoHideTaskbarPosition::new(display).log_err().flatten(); + } } impl MouseWheelSettings { diff --git a/crates/gpui/src/platform/windows/util.rs b/crates/gpui/src/platform/windows/util.rs index bf9e390ba893d6f885f7a576f13d4e229b036d03..5fb8febe3b9b0b71e5d63ff4070f81b95c3dabf7 100644 --- a/crates/gpui/src/platform/windows/util.rs +++ b/crates/gpui/src/platform/windows/util.rs @@ -144,8 +144,8 @@ pub(crate) fn load_cursor(style: CursorStyle) -> Option { } /// This function is used to configure the dark mode for the window built-in title bar. -pub(crate) fn configure_dwm_dark_mode(hwnd: HWND) { - let dark_mode_enabled: BOOL = match system_appearance().log_err().unwrap_or_default() { +pub(crate) fn configure_dwm_dark_mode(hwnd: HWND, appearance: WindowAppearance) { + let dark_mode_enabled: BOOL = match appearance { WindowAppearance::Dark | WindowAppearance::VibrantDark => true.into(), WindowAppearance::Light | WindowAppearance::VibrantLight => false.into(), }; diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index 27c843932bb2a38f37f3b01b354a7eed7f8354fe..5c7dd07c4857c8d99dd8dcde294942cd33bf0573 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -37,6 +37,7 @@ pub struct WindowsWindowState { pub min_size: Option>, pub fullscreen_restore_bounds: Bounds, pub border_offset: WindowBorderOffset, + pub appearance: WindowAppearance, pub scale_factor: f32, pub restore_from_minimized: Option>, @@ -84,6 +85,7 @@ impl WindowsWindowState { display: WindowsDisplay, gpu_context: &BladeContext, min_size: Option>, + appearance: WindowAppearance, ) -> Result { let scale_factor = { let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32; @@ -118,6 +120,7 @@ impl WindowsWindowState { logical_size, fullscreen_restore_bounds, border_offset, + appearance, scale_factor, restore_from_minimized, min_size, @@ -206,6 +209,7 @@ impl WindowsWindowStatePtr { context.display, context.gpu_context, context.min_size, + context.appearance, )?); Ok(Rc::new_cyclic(|this| Self { @@ -338,6 +342,7 @@ struct WindowCreateContext<'a> { main_receiver: flume::Receiver, gpu_context: &'a BladeContext, main_thread_id_win32: u32, + appearance: WindowAppearance, } impl WindowsWindow { @@ -387,6 +392,7 @@ impl WindowsWindow { } else { WindowsDisplay::primary_monitor().unwrap() }; + let appearance = system_appearance().unwrap_or_default(); let mut context = WindowCreateContext { inner: None, handle, @@ -403,6 +409,7 @@ impl WindowsWindow { main_receiver, gpu_context, main_thread_id_win32, + appearance, }; let lpparam = Some(&context as *const _ as *const _); let creation_result = unsafe { @@ -426,7 +433,7 @@ impl WindowsWindow { let state_ptr = context.inner.take().unwrap()?; let hwnd = creation_result?; register_drag_drop(state_ptr.clone())?; - configure_dwm_dark_mode(hwnd); + configure_dwm_dark_mode(hwnd, appearance); state_ptr.state.borrow_mut().border_offset.update(hwnd)?; let placement = retrieve_window_placement( hwnd, @@ -543,7 +550,7 @@ impl PlatformWindow for WindowsWindow { } fn appearance(&self) -> WindowAppearance { - system_appearance().log_err().unwrap_or_default() + self.0.state.borrow().appearance } fn display(&self) -> Option> { @@ -951,7 +958,7 @@ impl IDropTarget_Impl for WindowsDragDropHandler_Impl { } } -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub(crate) struct ClickState { button: MouseButton, last_click: Instant, @@ -993,10 +1000,25 @@ impl ClickState { self.current_count } - pub fn system_update(&mut self) { - self.double_click_spatial_tolerance_width = unsafe { GetSystemMetrics(SM_CXDOUBLECLK) }; - self.double_click_spatial_tolerance_height = unsafe { GetSystemMetrics(SM_CYDOUBLECLK) }; - self.double_click_interval = Duration::from_millis(unsafe { GetDoubleClickTime() } as u64); + pub fn system_update(&mut self, wparam: usize) { + match wparam { + // SPI_SETDOUBLECLKWIDTH + 29 => { + self.double_click_spatial_tolerance_width = + unsafe { GetSystemMetrics(SM_CXDOUBLECLK) } + } + // SPI_SETDOUBLECLKHEIGHT + 30 => { + self.double_click_spatial_tolerance_height = + unsafe { GetSystemMetrics(SM_CYDOUBLECLK) } + } + // SPI_SETDOUBLECLICKTIME + 32 => { + self.double_click_interval = + Duration::from_millis(unsafe { GetDoubleClickTime() } as u64) + } + _ => {} + } } #[inline] From a6ee4a18c41b18e73fccb6b1d3a41e361a713715 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 3 Jul 2025 15:17:54 +0300 Subject: [PATCH 017/239] Fix remote binary bundling (#33845) Follow-up of https://github.com/zed-industries/zed/pull/32937 Fixes remote server bundling: https://github.com/zed-industries/zed/actions/runs/16043840539/job/45271137215#step:6:2079 Excludes `screen-capture` feature from Zed's default, use it only in the components that need it. Release Notes: - N/A --- Cargo.toml | 1 - crates/call/Cargo.toml | 2 +- crates/collab/Cargo.toml | 1 + crates/livekit_client/Cargo.toml | 2 +- crates/title_bar/Cargo.toml | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1d9cf31c1498df933c2e8134c97d6b96f0ded628..82cbb533971316de0a7478a611d03fca55d1be12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -276,7 +276,6 @@ go_to_line = { path = "crates/go_to_line" } google_ai = { path = "crates/google_ai" } gpui = { path = "crates/gpui", default-features = false, features = [ "http_client", - "screen-capture", ] } gpui_macros = { path = "crates/gpui_macros" } gpui_tokio = { path = "crates/gpui_tokio" } diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index 75c7a6638ab1443fd192751cb4b02cef3d9c1841..30e2943af3fcb9e8d5141568b2602a8db9a69a6c 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -29,7 +29,7 @@ client.workspace = true collections.workspace = true fs.workspace = true futures.workspace = true -gpui.workspace = true +gpui = { workspace = true, features = ["screen-capture"] } language.workspace = true log.workspace = true postage.workspace = true diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 74eff5ec4e6016b4918c4881c79c7b2dc1687411..55c15cac5ac84c9d166c54a127dd18b2237b9bd9 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -35,6 +35,7 @@ dashmap.workspace = true derive_more.workspace = true envy = "0.4.2" futures.workspace = true +gpui = { workspace = true, features = ["screen-capture"] } hex.workspace = true http_client.workspace = true jsonwebtoken.workspace = true diff --git a/crates/livekit_client/Cargo.toml b/crates/livekit_client/Cargo.toml index b4518d6c166ba1c3de027874bfdc5ea8caef0245..319dc76d4821752a4941817335f4fae9f394b757 100644 --- a/crates/livekit_client/Cargo.toml +++ b/crates/livekit_client/Cargo.toml @@ -25,7 +25,7 @@ async-trait.workspace = true collections.workspace = true cpal.workspace = true futures.workspace = true -gpui = { workspace = true, features = ["x11", "wayland"] } +gpui = { workspace = true, features = ["screen-capture", "x11", "wayland"] } gpui_tokio.workspace = true http_client_tls.workspace = true image.workspace = true diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index 5bd5821fa23b54a1bd3ee115d991248a35af9d3b..123d0468ac86d6d37d428e73b4fd8de37dce429c 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -32,7 +32,7 @@ call.workspace = true chrono.workspace = true client.workspace = true db.workspace = true -gpui.workspace = true +gpui = { workspace = true, features = ["screen-capture"] } notifications.workspace = true project.workspace = true remote.workspace = true From 2bf2c5c580992c562c4e12b375a27ba10d07b2b5 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 3 Jul 2025 09:39:03 -0300 Subject: [PATCH 018/239] Add more refinements to the keymap edit UI (#33847) Quick follow-up to https://github.com/zed-industries/zed/pull/33816, tidying it up some things a bit more. Release Notes: - N/A --- crates/settings_ui/src/keybindings.rs | 89 ++++++++++--------- crates/settings_ui/src/ui_components/table.rs | 27 +++--- 2 files changed, 59 insertions(+), 57 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 85bb8431458befcc88f7a79796446698247f8615..2b814c43b8476c0d798309a896a0d7c497783243 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -588,6 +588,8 @@ impl Render for KeymapEditor { let theme = cx.theme(); v_flex() + .id("keymap-editor") + .track_focus(&self.focus_handle) .key_context(self.dispatch_context(window, cx)) .on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_previous)) @@ -599,12 +601,9 @@ impl Render for KeymapEditor { .on_action(cx.listener(Self::copy_action_to_clipboard)) .on_action(cx.listener(Self::copy_context_to_clipboard)) .size_full() + .p_2() + .gap_1() .bg(theme.colors().editor_background) - .id("keymap-editor") - .track_focus(&self.focus_handle) - .pt_4() - .px_4() - .gap_4() .child( h_flex() .key_context({ @@ -796,15 +795,20 @@ impl Render for KeybindingEditorModal { let theme = cx.theme().colors(); return v_flex() - .w(rems(36.)) + .w(rems(34.)) .elevation_3(cx) .child( v_flex() - .pt_2() - .px_4() - .pb_4() + .p_3() .gap_2() - .child(Label::new("Input desired keystroke, then hit save")) + .child( + v_flex().child(Label::new("Edit Keystroke")).child( + Label::new( + "Input the desired keystroke for the selected action and hit save.", + ) + .color(Color::Muted), + ), + ) .child(self.keybind_editor.clone()), ) .child( @@ -819,41 +823,38 @@ impl Render for KeybindingEditorModal { Button::new("cancel", "Cancel") .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), ) - .child( - Button::new("save-btn", "Save Keybinding").on_click(cx.listener( - |this, _event, _window, cx| { - let existing_keybind = this.editing_keybind.clone(); - let fs = this.fs.clone(); - let new_keystrokes = this - .keybind_editor - .read_with(cx, |editor, _| editor.keystrokes.clone()); - if new_keystrokes.is_empty() { - this.error = Some("Keystrokes cannot be empty".to_string()); - cx.notify(); - return; + .child(Button::new("save-btn", "Save").on_click(cx.listener( + |this, _event, _window, cx| { + let existing_keybind = this.editing_keybind.clone(); + let fs = this.fs.clone(); + let new_keystrokes = this + .keybind_editor + .read_with(cx, |editor, _| editor.keystrokes.clone()); + if new_keystrokes.is_empty() { + this.error = Some("Keystrokes cannot be empty".to_string()); + cx.notify(); + return; + } + let tab_size = cx.global::().json_tab_size(); + cx.spawn(async move |this, cx| { + if let Err(err) = save_keybinding_update( + existing_keybind, + &new_keystrokes, + &fs, + tab_size, + ) + .await + { + this.update(cx, |this, cx| { + this.error = Some(err.to_string()); + cx.notify(); + }) + .log_err(); } - let tab_size = - cx.global::().json_tab_size(); - cx.spawn(async move |this, cx| { - if let Err(err) = save_keybinding_update( - existing_keybind, - &new_keystrokes, - &fs, - tab_size, - ) - .await - { - this.update(cx, |this, cx| { - this.error = Some(err.to_string()); - cx.notify(); - }) - .log_err(); - } - }) - .detach(); - }, - )), - ), + }) + .detach(); + }, + ))), ) .when_some(self.error.clone(), |this, error| { this.child( diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index bce131e48187185a7fe3ebb49d3f6295e703e8c8..0ab00fdcf7410bda4b5afb385c7aa07ecb48eac7 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/src/ui_components/table.rs @@ -2,9 +2,9 @@ use std::{ops::Range, rc::Rc, time::Duration}; use editor::{EditorSettings, ShowScrollbar, scroll::ScrollbarAutoHide}; use gpui::{ - AppContext, Axis, Context, Entity, FocusHandle, FontWeight, Length, - ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, Task, UniformListScrollHandle, - WeakEntity, transparent_black, uniform_list, + AppContext, Axis, Context, Entity, FocusHandle, Length, ListHorizontalSizingBehavior, + ListSizingBehavior, MouseButton, Task, UniformListScrollHandle, WeakEntity, transparent_black, + uniform_list, }; use settings::Settings as _; use ui::{ @@ -12,7 +12,8 @@ use ui::{ ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator, InteractiveElement as _, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce, Scrollbar, ScrollbarState, StatefulInteractiveElement as _, Styled, StyledExt as _, - StyledTypography, Window, div, example_group_with_title, h_flex, px, single_example, v_flex, + StyledTypography, Tooltip, Window, div, example_group_with_title, h_flex, px, single_example, + v_flex, }; struct UniformListData { @@ -471,11 +472,10 @@ pub fn render_row( .map_or([None; COLS], |widths| widths.map(Some)); let row = div().w_full().child( - div() + h_flex() + .id("table_row") + .tooltip(Tooltip::text("Hit enter to edit")) .w_full() - .flex() - .flex_row() - .items_center() .justify_between() .px_1p5() .py_1() @@ -518,11 +518,12 @@ pub fn render_header( .p_2() .border_b_1() .border_color(cx.theme().colors().border) - .children(headers.into_iter().zip(column_widths).map(|(h, width)| { - base_cell_style(width, cx) - .font_weight(FontWeight::SEMIBOLD) - .child(h) - })) + .children( + headers + .into_iter() + .zip(column_widths) + .map(|(h, width)| base_cell_style(width, cx).child(h)), + ) } #[derive(Clone)] From f34a7abf17b21db480f9c7e2007b5dbb97ae37e5 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 3 Jul 2025 20:50:26 +0800 Subject: [PATCH 019/239] gpui: Add `shadow_xs`, `shadow_2xs` and fix shadow values to match Tailwind CSS (#33361) Release Notes: - N/A --- https://tailwindcss.com/docs/box-shadow | name | value | | -- | -- | | shadow-2xs | box-shadow: var(--shadow-2xs); /* 0 1px rgb(0 0 0 / 0.05) */ | | shadow-xs | box-shadow: var(--shadow-xs); /* 0 1px 2px 0 rgb(0 0 0 / 0.05) */ | | shadow-sm | box-shadow: var(--shadow-sm); /* 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1) */ | | shadow-md | box-shadow: var(--shadow-md); /* 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1) */ | | shadow-lg | box-shadow: var(--shadow-lg); /* 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1) */ | | shadow-xl | box-shadow: var(--shadow-xl); /* 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1) */ | | shadow-2xl | box-shadow: var(--shadow-2xl); /* 0 25px 50px -12px rgb(0 0 0 / 0.25) */ | ## Before SCR-20250625-nnxn ## After SCR-20250625-nnrt --- crates/component/src/component_layout.rs | 2 +- crates/editor/src/editor.rs | 4 +-- crates/editor/src/element.rs | 2 +- crates/gpui/examples/shadow.rs | 4 +++ crates/gpui_macros/src/styles.rs | 42 ++++++++++++++++++++++-- crates/repl/src/notebook/cell.rs | 2 +- 6 files changed, 49 insertions(+), 7 deletions(-) diff --git a/crates/component/src/component_layout.rs b/crates/component/src/component_layout.rs index 9090c49cf9fea1ebe8e742e0b08c462dea3a2ae6..b749ea20eab8b347b83bf34e35c33ec4ef5c614f 100644 --- a/crates/component/src/component_layout.rs +++ b/crates/component/src/component_layout.rs @@ -61,7 +61,7 @@ impl RenderOnce for ComponentExample { 12.0, 12.0, )) - .shadow_sm() + .shadow_xs() .child(self.element), ) .into_any_element() diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index fb3d76d3440fa874194d82089064563f3d7e9a69..47223aa59a86eef27494f28dccae144c14a59f85 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8715,7 +8715,7 @@ impl Editor { h_flex() .bg(cx.theme().colors().editor_background) .border(BORDER_WIDTH) - .shadow_sm() + .shadow_xs() .border_color(cx.theme().colors().border) .rounded_l_lg() .when(line_count > 1, |el| el.rounded_br_lg()) @@ -8915,7 +8915,7 @@ impl Editor { .border_1() .bg(Self::edit_prediction_line_popover_bg_color(cx)) .border_color(Self::edit_prediction_callout_popover_border_color(cx)) - .shadow_sm() + .shadow_xs() .when(!has_keybind, |el| { let status_colors = cx.theme().status(); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2aea0ef956b77805eb65c512717101242f220a19..3fa8697c193f80e2f974c57b74947e32a689a506 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1875,7 +1875,7 @@ impl EditorElement { let mut minimap = div() .size_full() - .shadow_sm() + .shadow_xs() .px(PADDING_OFFSET) .child(minimap_editor) .into_any_element(); diff --git a/crates/gpui/examples/shadow.rs b/crates/gpui/examples/shadow.rs index c42b0f55f0d9ea2aa8d72ff3835c08d6b5be6b3d..352e29c042af688abf554a611f6218a468280b9e 100644 --- a/crates/gpui/examples/shadow.rs +++ b/crates/gpui/examples/shadow.rs @@ -156,6 +156,10 @@ impl Render for Shadow { .w_full() .children(vec![ example("None", Shadow::base()), + // 2Xsmall shadow + example("2X Small", Shadow::base().shadow_2xs()), + // Xsmall shadow + example("Extra Small", Shadow::base().shadow_xs()), // Small shadow example("Small", Shadow::base().shadow_sm()), // Medium shadow diff --git a/crates/gpui_macros/src/styles.rs b/crates/gpui_macros/src/styles.rs index 4e3dda9ed2491a6fdbe2bc6ae4d4481532c4acc5..36d46cfb515d4e2782296edd132f06449c6281f5 100644 --- a/crates/gpui_macros/src/styles.rs +++ b/crates/gpui_macros/src/styles.rs @@ -407,7 +407,22 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream { /// Sets the box shadow of the element. /// [Docs](https://tailwindcss.com/docs/box-shadow) - #visibility fn shadow_sm(mut self) -> Self { + #visibility fn shadow_2xs(mut self) -> Self { + use gpui::{BoxShadow, hsla, point, px}; + use std::vec; + + self.style().box_shadow = Some(vec![BoxShadow { + color: hsla(0., 0., 0., 0.05), + offset: point(px(0.), px(1.)), + blur_radius: px(0.), + spread_radius: px(0.), + }]); + self + } + + /// Sets the box shadow of the element. + /// [Docs](https://tailwindcss.com/docs/box-shadow) + #visibility fn shadow_xs(mut self) -> Self { use gpui::{BoxShadow, hsla, point, px}; use std::vec; @@ -420,6 +435,29 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream { self } + /// Sets the box shadow of the element. + /// [Docs](https://tailwindcss.com/docs/box-shadow) + #visibility fn shadow_sm(mut self) -> Self { + use gpui::{BoxShadow, hsla, point, px}; + use std::vec; + + self.style().box_shadow = Some(vec![ + BoxShadow { + color: hsla(0., 0., 0., 0.1), + offset: point(px(0.), px(1.)), + blur_radius: px(3.), + spread_radius: px(0.), + }, + BoxShadow { + color: hsla(0., 0., 0., 0.1), + offset: point(px(0.), px(1.)), + blur_radius: px(2.), + spread_radius: px(-1.), + } + ]); + self + } + /// Sets the box shadow of the element. /// [Docs](https://tailwindcss.com/docs/box-shadow) #visibility fn shadow_md(mut self) -> Self { @@ -428,7 +466,7 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream { self.style().box_shadow = Some(vec![ BoxShadow { - color: hsla(0.5, 0., 0., 0.1), + color: hsla(0., 0., 0., 0.1), offset: point(px(0.), px(4.)), blur_radius: px(6.), spread_radius: px(-1.), diff --git a/crates/repl/src/notebook/cell.rs b/crates/repl/src/notebook/cell.rs index 7bfb2ed69cf7998d560260a989ca7e635d661266..2ed68c17d13dec4236ad4416e7f950a7f61dfb8f 100644 --- a/crates/repl/src/notebook/cell.rs +++ b/crates/repl/src/notebook/cell.rs @@ -656,7 +656,7 @@ impl Render for CodeCell { // .bg(cx.theme().colors().editor_background) // .border(px(1.)) // .border_color(cx.theme().colors().border) - // .shadow_sm() + // .shadow_xs() .children(content) }, ))), From 42aca4189a06c9a3b8de0001026ac23f0e8482af Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 3 Jul 2025 10:42:15 -0300 Subject: [PATCH 020/239] agent: Improve provider section spacing in settings view (#33850) Some design polish here as a follow-up to making the provider accordion header entirely clickable. Release Notes: - agent: Improved design in the provider section by refining spacing. --- crates/agent_ui/src/agent_configuration.rs | 60 ++++++++++++++-------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 4aa9c3fc38b2555e2675d668aa534d9c0125da7e..d91aa5fb2200629703f2f789f293686eb4c3ad73 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -26,8 +26,8 @@ use project::{ }; use settings::{Settings, update_settings_file}; use ui::{ - ContextMenu, Disclosure, ElevationIndex, Indicator, PopoverMenu, Scrollbar, ScrollbarState, - Switch, SwitchColor, Tooltip, prelude::*, + ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu, + Scrollbar, ScrollbarState, Switch, SwitchColor, Tooltip, prelude::*, }; use util::ResultExt as _; use workspace::Workspace; @@ -172,19 +172,29 @@ impl AgentConfiguration { .unwrap_or(false); v_flex() - .py_2() - .gap_1p5() - .border_t_1() - .border_color(cx.theme().colors().border.opacity(0.6)) + .when(is_expanded, |this| this.mb_2()) + .child( + div() + .opacity(0.6) + .px_2() + .child(Divider::horizontal().color(DividerColor::Border)), + ) .child( h_flex() + .map(|this| { + if is_expanded { + this.mt_2().mb_1() + } else { + this.my_2() + } + }) .w_full() - .gap_1() .justify_between() .child( h_flex() .id(provider_id_string.clone()) .cursor_pointer() + .px_2() .py_0p5() .w_full() .justify_between() @@ -247,12 +257,16 @@ impl AgentConfiguration { ) }), ) - .when(is_expanded, |parent| match configuration_view { - Some(configuration_view) => parent.child(configuration_view), - None => parent.child(Label::new(format!( - "No configuration view for {provider_name}", - ))), - }) + .child( + div() + .px_2() + .when(is_expanded, |parent| match configuration_view { + Some(configuration_view) => parent.child(configuration_view), + None => parent.child(Label::new(format!( + "No configuration view for {provider_name}", + ))), + }), + ) } fn render_provider_configuration_section( @@ -262,12 +276,11 @@ impl AgentConfiguration { let providers = LanguageModelRegistry::read_global(cx).providers(); v_flex() - .p(DynamicSpacing::Base16.rems(cx)) - .pr(DynamicSpacing::Base20.rems(cx)) - .border_b_1() - .border_color(cx.theme().colors().border) .child( v_flex() + .p(DynamicSpacing::Base16.rems(cx)) + .pr(DynamicSpacing::Base20.rems(cx)) + .pb_0() .mb_2p5() .gap_0p5() .child(Headline::new("LLM Providers")) @@ -276,10 +289,15 @@ impl AgentConfiguration { .color(Color::Muted), ), ) - .children( - providers - .into_iter() - .map(|provider| self.render_provider_configuration_block(&provider, cx)), + .child( + div() + .pl(DynamicSpacing::Base08.rems(cx)) + .pr(DynamicSpacing::Base20.rems(cx)) + .children( + providers.into_iter().map(|provider| { + self.render_provider_configuration_block(&provider, cx) + }), + ), ) } From b46e961991a251d1e7c914b77245a2ce761258e0 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Thu, 3 Jul 2025 19:17:26 +0530 Subject: [PATCH 021/239] agent_ui: Clear error message callout when message is edited or added in same thread (#33768) Currently if error occurs in the agent thread and user updates the same messages or sends a new message the error callout is still present and is not cleared. In this PR we are clearing the last error in case user sends a new messages or resend the previous message after editing. Before: https://github.com/user-attachments/assets/44994004-4cf0-45bc-8b69-88546f037372 After: https://github.com/user-attachments/assets/993a2a63-8295-47d3-bbda-a2669dee2d5f Release Notes: - Fix bug in agent panel error callout not getting removed when a message is edited or new a message is send. --- crates/agent_ui/src/active_thread.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index fa6d3144b519aa823171a9e86971d5ed96cf7b06..3937f2d15d17afa75edbe7bf0ce25752c6e30291 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -1026,6 +1026,7 @@ impl ActiveThread { } } ThreadEvent::MessageAdded(message_id) => { + self.clear_last_error(); if let Some(rendered_message) = self.thread.update(cx, |thread, cx| { thread.message(*message_id).map(|message| { RenderedMessage::from_segments( @@ -1042,6 +1043,7 @@ impl ActiveThread { cx.notify(); } ThreadEvent::MessageEdited(message_id) => { + self.clear_last_error(); if let Some(index) = self.messages.iter().position(|id| id == message_id) { if let Some(rendered_message) = self.thread.update(cx, |thread, cx| { thread.message(*message_id).map(|message| { From 12ab53b8045bde2c1eb5a24d16f32643befcfb3d Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Thu, 3 Jul 2025 09:22:28 -0500 Subject: [PATCH 022/239] Fix documentation of view release notes actions (#33851) Follow up for: #33809 Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/auto_update/src/auto_update.rs | 2 +- crates/auto_update_ui/src/auto_update_ui.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 70039060cab4104c432a1654f5d9339c9364c821..1123d3f8e2a5cf5e2354a4963b96d71134fdc791 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -35,7 +35,7 @@ actions!( Check, /// Dismisses the update error message. DismissErrorMessage, - /// Opens the release notes for the current version. + /// Opens the release notes for the current version in a browser. ViewReleaseNotes, ] ); diff --git a/crates/auto_update_ui/src/auto_update_ui.rs b/crates/auto_update_ui/src/auto_update_ui.rs index 5d79fb5db8704e995c296e2ba432a7414edafc97..63baef1f7d178045a2a2b5c976ede9ad75adb646 100644 --- a/crates/auto_update_ui/src/auto_update_ui.rs +++ b/crates/auto_update_ui/src/auto_update_ui.rs @@ -15,7 +15,7 @@ use workspace::notifications::{NotificationId, show_app_notification}; actions!( auto_update, [ - /// Opens release notes in the browser for the current version. + /// Opens the release notes for the current version in a new tab. ViewReleaseNotesLocally ] ); From cc0d8a411eb4f3ecee94abf6fe3cd2b2bbbafef2 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Thu, 3 Jul 2025 17:27:49 +0300 Subject: [PATCH 023/239] Add Danger check for changes to eval fixtures (#33852) Also, revert unintentional changes to fixtures. Changes to test fixtures are intentional and necessary. Release Notes: - N/A --- .../disable_cursor_blinking/before.rs | 208 ++---------------- script/danger/dangerfile.ts | 21 ++ 2 files changed, 41 insertions(+), 188 deletions(-) diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs b/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs index a070738b600f041cbd6b3cc8ad1e8a6462b1d85a..607daa8ce3a129e0f4bc53a00d1a62f479da3932 100644 --- a/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs +++ b/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs @@ -9132,7 +9132,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.manipulate_immutable_lines(window, cx, |lines| lines.sort()) + self.manipulate_lines(window, cx, |lines| lines.sort()) } pub fn sort_lines_case_insensitive( @@ -9141,7 +9141,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.manipulate_immutable_lines(window, cx, |lines| { + self.manipulate_lines(window, cx, |lines| { lines.sort_by_key(|line| line.to_lowercase()) }) } @@ -9152,7 +9152,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.manipulate_immutable_lines(window, cx, |lines| { + self.manipulate_lines(window, cx, |lines| { let mut seen = HashSet::default(); lines.retain(|line| seen.insert(line.to_lowercase())); }) @@ -9164,7 +9164,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.manipulate_immutable_lines(window, cx, |lines| { + self.manipulate_lines(window, cx, |lines| { let mut seen = HashSet::default(); lines.retain(|line| seen.insert(*line)); }) @@ -9606,20 +9606,20 @@ impl Editor { } pub fn reverse_lines(&mut self, _: &ReverseLines, window: &mut Window, cx: &mut Context) { - self.manipulate_immutable_lines(window, cx, |lines| lines.reverse()) + self.manipulate_lines(window, cx, |lines| lines.reverse()) } pub fn shuffle_lines(&mut self, _: &ShuffleLines, window: &mut Window, cx: &mut Context) { - self.manipulate_immutable_lines(window, cx, |lines| lines.shuffle(&mut thread_rng())) + self.manipulate_lines(window, cx, |lines| lines.shuffle(&mut thread_rng())) } - fn manipulate_lines( + fn manipulate_lines( &mut self, window: &mut Window, cx: &mut Context, - mut manipulate: M, + mut callback: Fn, ) where - M: FnMut(&str) -> LineManipulationResult, + Fn: FnMut(&mut Vec<&str>), { self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); @@ -9652,14 +9652,18 @@ impl Editor { .text_for_range(start_point..end_point) .collect::(); - let LineManipulationResult { new_text, line_count_before, line_count_after} = manipulate(&text); + let mut lines = text.split('\n').collect_vec(); - edits.push((start_point..end_point, new_text)); + let lines_before = lines.len(); + callback(&mut lines); + let lines_after = lines.len(); + + edits.push((start_point..end_point, lines.join("\n"))); // Selections must change based on added and removed line count let start_row = MultiBufferRow(start_point.row + added_lines as u32 - removed_lines as u32); - let end_row = MultiBufferRow(start_row.0 + line_count_after.saturating_sub(1) as u32); + let end_row = MultiBufferRow(start_row.0 + lines_after.saturating_sub(1) as u32); new_selections.push(Selection { id: selection.id, start: start_row, @@ -9668,10 +9672,10 @@ impl Editor { reversed: selection.reversed, }); - if line_count_after > line_count_before { - added_lines += line_count_after - line_count_before; - } else if line_count_before > line_count_after { - removed_lines += line_count_before - line_count_after; + if lines_after > lines_before { + added_lines += lines_after - lines_before; + } else if lines_before > lines_after { + removed_lines += lines_before - lines_after; } } @@ -9716,171 +9720,6 @@ impl Editor { }) } - fn manipulate_immutable_lines( - &mut self, - window: &mut Window, - cx: &mut Context, - mut callback: Fn, - ) where - Fn: FnMut(&mut Vec<&str>), - { - self.manipulate_lines(window, cx, |text| { - let mut lines: Vec<&str> = text.split('\n').collect(); - let line_count_before = lines.len(); - - callback(&mut lines); - - LineManipulationResult { - new_text: lines.join("\n"), - line_count_before, - line_count_after: lines.len(), - } - }); - } - - fn manipulate_mutable_lines( - &mut self, - window: &mut Window, - cx: &mut Context, - mut callback: Fn, - ) where - Fn: FnMut(&mut Vec>), - { - self.manipulate_lines(window, cx, |text| { - let mut lines: Vec> = text.split('\n').map(Cow::from).collect(); - let line_count_before = lines.len(); - - callback(&mut lines); - - LineManipulationResult { - new_text: lines.join("\n"), - line_count_before, - line_count_after: lines.len(), - } - }); - } - - pub fn convert_indentation_to_spaces( - &mut self, - _: &ConvertIndentationToSpaces, - window: &mut Window, - cx: &mut Context, - ) { - let settings = self.buffer.read(cx).language_settings(cx); - let tab_size = settings.tab_size.get() as usize; - - self.manipulate_mutable_lines(window, cx, |lines| { - // Allocates a reasonably sized scratch buffer once for the whole loop - let mut reindented_line = String::with_capacity(MAX_LINE_LEN); - // Avoids recomputing spaces that could be inserted many times - let space_cache: Vec> = (1..=tab_size) - .map(|n| IndentSize::spaces(n as u32).chars().collect()) - .collect(); - - for line in lines.iter_mut().filter(|line| !line.is_empty()) { - let mut chars = line.as_ref().chars(); - let mut col = 0; - let mut changed = false; - - while let Some(ch) = chars.next() { - match ch { - ' ' => { - reindented_line.push(' '); - col += 1; - } - '\t' => { - // \t are converted to spaces depending on the current column - let spaces_len = tab_size - (col % tab_size); - reindented_line.extend(&space_cache[spaces_len - 1]); - col += spaces_len; - changed = true; - } - _ => { - // If we dont append before break, the character is consumed - reindented_line.push(ch); - break; - } - } - } - - if !changed { - reindented_line.clear(); - continue; - } - // Append the rest of the line and replace old reference with new one - reindented_line.extend(chars); - *line = Cow::Owned(reindented_line.clone()); - reindented_line.clear(); - } - }); - } - - pub fn convert_indentation_to_tabs( - &mut self, - _: &ConvertIndentationToTabs, - window: &mut Window, - cx: &mut Context, - ) { - let settings = self.buffer.read(cx).language_settings(cx); - let tab_size = settings.tab_size.get() as usize; - - self.manipulate_mutable_lines(window, cx, |lines| { - // Allocates a reasonably sized buffer once for the whole loop - let mut reindented_line = String::with_capacity(MAX_LINE_LEN); - // Avoids recomputing spaces that could be inserted many times - let space_cache: Vec> = (1..=tab_size) - .map(|n| IndentSize::spaces(n as u32).chars().collect()) - .collect(); - - for line in lines.iter_mut().filter(|line| !line.is_empty()) { - let mut chars = line.chars(); - let mut spaces_count = 0; - let mut first_non_indent_char = None; - let mut changed = false; - - while let Some(ch) = chars.next() { - match ch { - ' ' => { - // Keep track of spaces. Append \t when we reach tab_size - spaces_count += 1; - changed = true; - if spaces_count == tab_size { - reindented_line.push('\t'); - spaces_count = 0; - } - } - '\t' => { - reindented_line.push('\t'); - spaces_count = 0; - } - _ => { - // Dont append it yet, we might have remaining spaces - first_non_indent_char = Some(ch); - break; - } - } - } - - if !changed { - reindented_line.clear(); - continue; - } - // Remaining spaces that didn't make a full tab stop - if spaces_count > 0 { - reindented_line.extend(&space_cache[spaces_count - 1]); - } - // If we consume an extra character that was not indentation, add it back - if let Some(extra_char) = first_non_indent_char { - reindented_line.push(extra_char); - } - // Append the rest of the line and replace old reference with new one - reindented_line.extend(chars); - *line = Cow::Owned(reindented_line.clone()); - reindented_line.clear(); - } - }); - } - pub fn convert_to_upper_case( &mut self, _: &ConvertToUpperCase, @@ -21318,13 +21157,6 @@ pub struct LineHighlight { pub type_id: Option, } -struct LineManipulationResult { - pub new_text: String, - pub line_count_before: usize, - pub line_count_after: usize, -} - - fn render_diff_hunk_controls( row: u32, status: &DiffHunkStatus, diff --git a/script/danger/dangerfile.ts b/script/danger/dangerfile.ts index 3f9c80586ee30b33c9539f2665614f23122248a0..6ed4a27fedb0bea7882ad4bcdd1016929bdd40e3 100644 --- a/script/danger/dangerfile.ts +++ b/script/danger/dangerfile.ts @@ -94,3 +94,24 @@ for (const promptPath of modifiedPrompts) { ); } } + +const FIXTURE_CHANGE_ATTESTATION = "Changes to test fixtures are intentional and necessary."; + +const FIXTURES_PATHS = ["crates/assistant_tools/src/edit_agent/evals/fixtures"]; + +const modifiedFixtures = danger.git.modified_files.filter((file) => + FIXTURES_PATHS.some((fixturePath) => file.includes(fixturePath)), +); + +if (modifiedFixtures.length > 0) { + if (!body.includes(FIXTURE_CHANGE_ATTESTATION)) { + const modifiedFixturesStr = modifiedFixtures.map((path) => "`" + path + "`").join(", "); + fail( + [ + `This PR modifies eval or test fixtures (${modifiedFixturesStr}), which are typically expected to remain unchanged.`, + "If these changes are intentional and required, please add the following attestation to your PR description: ", + `"${FIXTURE_CHANGE_ATTESTATION}"`, + ].join("\n\n"), + ); + } +} From 34322ef1cd0871dbe17ca80f8261ed0e2c83f7df Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 3 Jul 2025 12:05:29 -0300 Subject: [PATCH 024/239] agent: Fix bug that prevented MCP servers to appear in the settings view (#33857) Closes https://github.com/zed-industries/zed/issues/33827 After #33644 was merged, we would not start MCP servers coming from extensions correctly anymore. The optimization uncovered a bug in the implementation of `ContextServerDescriptorRegistry`, because we never called `cx.notify()` when adding/removing context servers. `ContextServerStore` listens for these events, and before #33644 this was just working because of aace condition. Release Notes: - agent: Fixed bug that prevented MCP servers to appear in the settings view. Co-authored-by: Bennet Bo Fenner --- crates/project/src/context_server_store.rs | 4 ++-- crates/project/src/context_server_store/extension.rs | 7 ++++--- crates/project/src/context_server_store/registry.rs | 11 +++++++++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index d13cb37aa8b4bb97afb4ddbe454bfd9ee7b68b9e..d2541e1b31fe6a798edc92bb070a17b631f61f98 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -818,9 +818,9 @@ mod tests { .await; let executor = cx.executor(); - let registry = cx.new(|_| { + let registry = cx.new(|cx| { let mut registry = ContextServerDescriptorRegistry::new(); - registry.register_context_server_descriptor(SERVER_1_ID.into(), fake_descriptor_1); + registry.register_context_server_descriptor(SERVER_1_ID.into(), fake_descriptor_1, cx); registry }); let store = cx.new(|cx| { diff --git a/crates/project/src/context_server_store/extension.rs b/crates/project/src/context_server_store/extension.rs index 825ee0b6789c86133ddac9bee64a6c8350f18575..1eaecd987dd51158fc2f505c1ae9b0c8fcc076a3 100644 --- a/crates/project/src/context_server_store/extension.rs +++ b/crates/project/src/context_server_store/extension.rs @@ -103,19 +103,20 @@ struct ContextServerDescriptorRegistryProxy { impl ExtensionContextServerProxy for ContextServerDescriptorRegistryProxy { fn register_context_server(&self, extension: Arc, id: Arc, cx: &mut App) { self.context_server_factory_registry - .update(cx, |registry, _| { + .update(cx, |registry, cx| { registry.register_context_server_descriptor( id.clone(), Arc::new(ContextServerDescriptor { id, extension }) as Arc, + cx, ) }); } fn unregister_context_server(&self, server_id: Arc, cx: &mut App) { self.context_server_factory_registry - .update(cx, |registry, _| { - registry.unregister_context_server_descriptor_by_id(&server_id) + .update(cx, |registry, cx| { + registry.unregister_context_server_descriptor_by_id(&server_id, cx) }); } } diff --git a/crates/project/src/context_server_store/registry.rs b/crates/project/src/context_server_store/registry.rs index 972ec6642d8e884bda4f75da2a9889194767fdfd..b705fcadee58e294ef14bb83e1cbb9ae0dfe2764 100644 --- a/crates/project/src/context_server_store/registry.rs +++ b/crates/project/src/context_server_store/registry.rs @@ -4,7 +4,7 @@ use anyhow::Result; use collections::HashMap; use context_server::ContextServerCommand; use extension::ContextServerConfiguration; -use gpui::{App, AppContext as _, AsyncApp, Entity, Global, Task}; +use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Global, Task}; use crate::worktree_store::WorktreeStore; @@ -66,12 +66,19 @@ impl ContextServerDescriptorRegistry { &mut self, id: Arc, descriptor: Arc, + cx: &mut Context, ) { self.context_servers.insert(id, descriptor); + cx.notify(); } /// Unregisters the [`ContextServerDescriptor`] for the server with the given ID. - pub fn unregister_context_server_descriptor_by_id(&mut self, server_id: &str) { + pub fn unregister_context_server_descriptor_by_id( + &mut self, + server_id: &str, + cx: &mut Context, + ) { self.context_servers.remove(server_id); + cx.notify(); } } From 4e6b7ee3ea951a34e4bf2961e7bce40da7abdd63 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Thu, 3 Jul 2025 11:35:20 -0500 Subject: [PATCH 025/239] keymap_ui: Hover tooltip for action documentation (#33862) Closes #ISSUE Show the documentation for an action when hovered. As a bonus, also show the humanized command palette name! Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/gpui/src/action.rs | 9 ++++++ crates/gpui/src/app.rs | 7 ++++- crates/settings_ui/src/keybindings.rs | 31 +++++++++++++++++-- crates/settings_ui/src/ui_components/table.rs | 4 +-- 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/crates/gpui/src/action.rs b/crates/gpui/src/action.rs index 9e979a31ff1940facfd4bca10652c40ee0fcd6c3..e099bfec28c3e3f15267348694f60c961df6f086 100644 --- a/crates/gpui/src/action.rs +++ b/crates/gpui/src/action.rs @@ -225,6 +225,7 @@ pub(crate) struct ActionRegistry { all_names: Vec<&'static str>, // So we can return a static slice. deprecated_aliases: HashMap<&'static str, &'static str>, // deprecated name -> preferred name deprecation_messages: HashMap<&'static str, &'static str>, // action name -> deprecation message + documentation: HashMap<&'static str, &'static str>, // action name -> documentation } impl Default for ActionRegistry { @@ -232,6 +233,7 @@ impl Default for ActionRegistry { let mut this = ActionRegistry { by_name: Default::default(), names_by_type_id: Default::default(), + documentation: Default::default(), all_names: Default::default(), deprecated_aliases: Default::default(), deprecation_messages: Default::default(), @@ -327,6 +329,9 @@ impl ActionRegistry { if let Some(deprecation_msg) = action.deprecation_message { self.deprecation_messages.insert(name, deprecation_msg); } + if let Some(documentation) = action.documentation { + self.documentation.insert(name, documentation); + } } /// Construct an action based on its name and optional JSON parameters sourced from the keymap. @@ -388,6 +393,10 @@ impl ActionRegistry { pub fn deprecation_messages(&self) -> &HashMap<&'static str, &'static str> { &self.deprecation_messages } + + pub fn documentation(&self) -> &HashMap<&'static str, &'static str> { + &self.documentation + } } /// Generate a list of all the registered actions. diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 2ecc3dadaa294783a23cb523b7beb79af3506200..ef462ae084bd66ee2e851772e5ab659906aa446a 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1403,11 +1403,16 @@ impl App { self.actions.deprecated_aliases() } - /// Get a list of all action deprecation messages. + /// Get a map from an action name to the deprecation messages. pub fn action_deprecation_messages(&self) -> &HashMap<&'static str, &'static str> { self.actions.deprecation_messages() } + /// Get a map from an action name to the documentation. + pub fn action_documentation(&self) -> &HashMap<&'static str, &'static str> { + self.actions.documentation() + } + /// Register a callback to be invoked when the application is about to quit. /// It is not possible to cancel the quit event at this point. pub fn on_app_quit( diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 2b814c43b8476c0d798309a896a0d7c497783243..184a2cac373caee52fd06bc0cc9d2d7b15d91d22 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -254,7 +254,9 @@ impl KeymapEditor { let key_bindings_ptr = cx.key_bindings(); let lock = key_bindings_ptr.borrow(); let key_bindings = lock.bindings(); - let mut unmapped_action_names = HashSet::from_iter(cx.all_action_names()); + let mut unmapped_action_names = + HashSet::from_iter(cx.all_action_names().into_iter().copied()); + let action_documentation = cx.action_documentation(); let mut processed_bindings = Vec::new(); let mut string_match_candidates = Vec::new(); @@ -280,6 +282,7 @@ impl KeymapEditor { let action_input = key_binding .action_input() .map(|input| SyntaxHighlightedText::new(input, json_language.clone())); + let action_docs = action_documentation.get(action_name).copied(); let index = processed_bindings.len(); let string_match_candidate = StringMatchCandidate::new(index, &action_name); @@ -288,6 +291,7 @@ impl KeymapEditor { ui_key_binding, action: action_name.into(), action_input, + action_docs, context: Some(context), source, }); @@ -301,8 +305,9 @@ impl KeymapEditor { processed_bindings.push(ProcessedKeybinding { keystroke_text: empty.clone(), ui_key_binding: None, - action: (*action_name).into(), + action: action_name.into(), action_input: None, + action_docs: action_documentation.get(action_name).copied(), context: None, source: None, }); @@ -537,6 +542,7 @@ struct ProcessedKeybinding { ui_key_binding: Option, action: SharedString, action_input: Option, + action_docs: Option<&'static str>, context: Option, source: Option<(KeybindSource, SharedString)>, } @@ -635,7 +641,26 @@ impl Render for KeymapEditor { let candidate_id = this.matches.get(index)?.candidate_id; let binding = &this.keybindings[candidate_id]; - let action = binding.action.clone().into_any_element(); + let action = div() + .child(binding.action.clone()) + .id(("keymap action", index)) + .tooltip({ + let action_name = binding.action.clone(); + let action_docs = binding.action_docs; + move |_, cx| { + let action_tooltip = Tooltip::new( + command_palette::humanize_action_name( + &action_name, + ), + ); + let action_tooltip = match action_docs { + Some(docs) => action_tooltip.meta(docs), + None => action_tooltip, + }; + cx.new(|_| action_tooltip).into() + } + }) + .into_any_element(); let keystrokes = binding.ui_key_binding.clone().map_or( binding.keystroke_text.clone().into_any_element(), IntoElement::into_any_element, diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index 0ab00fdcf7410bda4b5afb385c7aa07ecb48eac7..c3b70d7d4f166ff3b34cd2b52146e4dc7408badc 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/src/ui_components/table.rs @@ -12,8 +12,7 @@ use ui::{ ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator, InteractiveElement as _, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce, Scrollbar, ScrollbarState, StatefulInteractiveElement as _, Styled, StyledExt as _, - StyledTypography, Tooltip, Window, div, example_group_with_title, h_flex, px, single_example, - v_flex, + StyledTypography, Window, div, example_group_with_title, h_flex, px, single_example, v_flex, }; struct UniformListData { @@ -474,7 +473,6 @@ pub fn render_row( let row = div().w_full().child( h_flex() .id("table_row") - .tooltip(Tooltip::text("Hit enter to edit")) .w_full() .justify_between() .px_1p5() From fcdd99e244892ab87377b8c22bfb8c8fd9db9f5a Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Thu, 3 Jul 2025 12:26:35 -0500 Subject: [PATCH 026/239] keymap_ui: Syntax highlight context (#33864) Closes #ISSUE Uses Rust for syntax highlighting of context in the keymap editor. Future pass will improve color choice to make colors less abrasive Release Notes: - N/A *or* Added/Fixed/Improved ... --- Cargo.lock | 1 + crates/settings_ui/Cargo.toml | 1 + crates/settings_ui/src/keybindings.rs | 56 +++++++++++++++++++++++---- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d22f1e6795983e9aadf2695f03c84899e31a2b3c..48928bcea0ff0fe5b9b9da256f265588e81f32ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14590,6 +14590,7 @@ dependencies = [ "settings", "theme", "tree-sitter-json", + "tree-sitter-rust", "ui", "util", "workspace", diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 6db6d78cd61111edf09829cffd47fdc956c46efc..7af240bd7419610ab7267439bee993ddfb194c5f 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -34,6 +34,7 @@ serde.workspace = true settings.workspace = true theme.workspace = true tree-sitter-json.workspace = true +tree-sitter-rust.workspace = true ui.workspace = true util.workspace = true workspace-hack.workspace = true diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 184a2cac373caee52fd06bc0cc9d2d7b15d91d22..1f5f4b1b7e18c7720227d6c04d7f8680e469c94b 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -249,6 +249,7 @@ impl KeymapEditor { fn process_bindings( json_language: Arc, + rust_language: Arc, cx: &mut App, ) -> (Vec, Vec) { let key_bindings_ptr = cx.key_bindings(); @@ -272,7 +273,9 @@ impl KeymapEditor { let context = key_binding .predicate() - .map(|predicate| KeybindContextString::Local(predicate.to_string().into())) + .map(|predicate| { + KeybindContextString::Local(predicate.to_string().into(), rust_language.clone()) + }) .unwrap_or(KeybindContextString::Global); let source = source.map(|source| (source, source.name().into())); @@ -320,11 +323,12 @@ impl KeymapEditor { fn update_keybindings(&mut self, cx: &mut Context) { let workspace = self.workspace.clone(); cx.spawn(async move |this, cx| { - let json_language = Self::load_json_language(workspace, cx).await; + let json_language = Self::load_json_language(workspace.clone(), cx).await; + let rust_language = Self::load_rust_language(workspace.clone(), cx).await; let query = this.update(cx, |this, cx| { let (key_bindings, string_match_candidates) = - Self::process_bindings(json_language.clone(), cx); + Self::process_bindings(json_language, rust_language, cx); this.keybindings = key_bindings; this.string_match_candidates = Arc::new(string_match_candidates); this.matches = this @@ -375,6 +379,35 @@ impl KeymapEditor { }); } + async fn load_rust_language( + workspace: WeakEntity, + cx: &mut AsyncApp, + ) -> Arc { + let rust_language_task = workspace + .read_with(cx, |workspace, cx| { + workspace + .project() + .read(cx) + .languages() + .language_for_name("Rust") + }) + .context("Failed to load Rust language") + .log_err(); + let rust_language = match rust_language_task { + Some(task) => task.await.context("Failed to load Rust language").log_err(), + None => None, + }; + return rust_language.unwrap_or_else(|| { + Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + )) + }); + } + fn dispatch_context(&self, _window: &Window, _cx: &Context) -> KeyContext { let mut dispatch_context = KeyContext::new_with_defaults(); dispatch_context.add("KeymapEditor"); @@ -550,7 +583,7 @@ struct ProcessedKeybinding { #[derive(Clone, Debug, IntoElement)] enum KeybindContextString { Global, - Local(SharedString), + Local(SharedString, Arc), } impl KeybindContextString { @@ -559,14 +592,14 @@ impl KeybindContextString { pub fn local(&self) -> Option<&SharedString> { match self { KeybindContextString::Global => None, - KeybindContextString::Local(name) => Some(name), + KeybindContextString::Local(name, _) => Some(name), } } pub fn local_str(&self) -> Option<&str> { match self { KeybindContextString::Global => None, - KeybindContextString::Local(name) => Some(name), + KeybindContextString::Local(name, _) => Some(name), } } } @@ -574,8 +607,15 @@ impl KeybindContextString { impl RenderOnce for KeybindContextString { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { match self { - KeybindContextString::Global => KeybindContextString::GLOBAL.clone(), - KeybindContextString::Local(name) => name, + KeybindContextString::Global => StyledText::new(KeybindContextString::GLOBAL.clone()) + .with_highlights([( + 0..KeybindContextString::GLOBAL.len(), + gpui::HighlightStyle::color(_cx.theme().colors().text_muted), + )]) + .into_any_element(), + KeybindContextString::Local(name, language) => { + SyntaxHighlightedText::new(name, language).into_any_element() + } } } } From fcd9da6cef6f09b40e920fa5a5e4be0a73fa1000 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 3 Jul 2025 16:48:51 -0400 Subject: [PATCH 027/239] Fix panic on inlay split (#33676) Closes #33641 Release Notes: - Fixed panic when trying to split on multibyte UTF-8 sequences. --- crates/editor/src/display_map/inlay_map.rs | 278 ++++++++++++++++++++- 1 file changed, 269 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 49b5ce1d26916de0ec79ab80ec21f1bcf8b335e3..f7a696860a1c85d6955fe9e6f5aa00c0fa32a156 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -296,12 +296,25 @@ impl<'a> Iterator for InlayChunks<'a> { *chunk = self.buffer_chunks.next().unwrap(); } - let (prefix, suffix) = chunk.text.split_at( - chunk - .text - .len() - .min(self.transforms.end(&()).0.0 - self.output_offset.0), - ); + let desired_bytes = self.transforms.end(&()).0.0 - self.output_offset.0; + + // If we're already at the transform boundary, skip to the next transform + if desired_bytes == 0 { + self.inlay_chunks = None; + self.transforms.next(&()); + return self.next(); + } + + // Determine split index handling edge cases + let split_index = if desired_bytes >= chunk.text.len() { + chunk.text.len() + } else if chunk.text.is_char_boundary(desired_bytes) { + desired_bytes + } else { + find_next_utf8_boundary(chunk.text, desired_bytes) + }; + + let (prefix, suffix) = chunk.text.split_at(split_index); chunk.text = suffix; self.output_offset.0 += prefix.len(); @@ -391,8 +404,24 @@ impl<'a> Iterator for InlayChunks<'a> { let inlay_chunk = self .inlay_chunk .get_or_insert_with(|| inlay_chunks.next().unwrap()); - let (chunk, remainder) = - inlay_chunk.split_at(inlay_chunk.len().min(next_inlay_highlight_endpoint)); + + // Determine split index handling edge cases + let split_index = if next_inlay_highlight_endpoint >= inlay_chunk.len() { + inlay_chunk.len() + } else if next_inlay_highlight_endpoint == 0 { + // Need to take at least one character to make progress + inlay_chunk + .chars() + .next() + .map(|c| c.len_utf8()) + .unwrap_or(1) + } else if inlay_chunk.is_char_boundary(next_inlay_highlight_endpoint) { + next_inlay_highlight_endpoint + } else { + find_next_utf8_boundary(inlay_chunk, next_inlay_highlight_endpoint) + }; + + let (chunk, remainder) = inlay_chunk.split_at(split_index); *inlay_chunk = remainder; if inlay_chunk.is_empty() { self.inlay_chunk = None; @@ -412,7 +441,7 @@ impl<'a> Iterator for InlayChunks<'a> { } }; - if self.output_offset == self.transforms.end(&()).0 { + if self.output_offset >= self.transforms.end(&()).0 { self.inlay_chunks = None; self.transforms.next(&()); } @@ -1143,6 +1172,31 @@ fn push_isomorphic(sum_tree: &mut SumTree, summary: TextSummary) { } } +/// Given a byte index that is NOT a UTF-8 boundary, find the next one. +/// Assumes: 0 < byte_index < text.len() and !text.is_char_boundary(byte_index) +#[inline(always)] +fn find_next_utf8_boundary(text: &str, byte_index: usize) -> usize { + let bytes = text.as_bytes(); + let mut idx = byte_index + 1; + + // Scan forward until we find a boundary + while idx < text.len() { + if is_utf8_char_boundary(bytes[idx]) { + return idx; + } + idx += 1; + } + + // Hit the end, return the full length + text.len() +} + +// Private helper function taken from Rust's core::num module (which is both Apache2 and MIT licensed) +const fn is_utf8_char_boundary(byte: u8) -> bool { + // This is bit magic equivalent to: b < 128 || b >= 192 + (byte as i8) >= -0x40 +} + #[cfg(test)] mod tests { use super::*; @@ -1882,4 +1936,210 @@ mod tests { cx.set_global(store); theme::init(theme::LoadThemes::JustBase, cx); } + + /// Helper to create test highlights for an inlay + fn create_inlay_highlights( + inlay_id: InlayId, + highlight_range: Range, + position: Anchor, + ) -> TreeMap> { + let mut inlay_highlights = TreeMap::default(); + let mut type_highlights = TreeMap::default(); + type_highlights.insert( + inlay_id, + ( + HighlightStyle::default(), + InlayHighlight { + inlay: inlay_id, + range: highlight_range, + inlay_position: position, + }, + ), + ); + inlay_highlights.insert(TypeId::of::<()>(), type_highlights); + inlay_highlights + } + + #[gpui::test] + fn test_inlay_utf8_boundary_panic_fix(cx: &mut App) { + init_test(cx); + + // This test verifies that we handle UTF-8 character boundaries correctly + // when splitting inlay text for highlighting. Previously, this would panic + // when trying to split at byte 13, which is in the middle of the '…' character. + // + // See https://github.com/zed-industries/zed/issues/33641 + let buffer = MultiBuffer::build_simple("fn main() {}\n", cx); + let (mut inlay_map, _) = InlayMap::new(buffer.read(cx).snapshot(cx)); + + // Create an inlay with text that contains a multi-byte character + // The string "SortingDirec…" contains an ellipsis character '…' which is 3 bytes (E2 80 A6) + let inlay_text = "SortingDirec…"; + let position = buffer.read(cx).snapshot(cx).anchor_before(Point::new(0, 5)); + + let inlay = Inlay { + id: InlayId::Hint(0), + position, + text: text::Rope::from(inlay_text), + color: None, + }; + + let (inlay_snapshot, _) = inlay_map.splice(&[], vec![inlay]); + + // Create highlights that request a split at byte 13, which is in the middle + // of the '…' character (bytes 12..15). We include the full character. + let inlay_highlights = create_inlay_highlights(InlayId::Hint(0), 0..13, position); + + let highlights = crate::display_map::Highlights { + text_highlights: None, + inlay_highlights: Some(&inlay_highlights), + styles: crate::display_map::HighlightStyles::default(), + }; + + // Collect chunks - this previously would panic + let chunks: Vec<_> = inlay_snapshot + .chunks( + InlayOffset(0)..InlayOffset(inlay_snapshot.len().0), + false, + highlights, + ) + .collect(); + + // Verify the chunks are correct + let full_text: String = chunks.iter().map(|c| c.chunk.text).collect(); + assert_eq!(full_text, "fn maSortingDirec…in() {}\n"); + + // Verify the highlighted portion includes the complete ellipsis character + let highlighted_chunks: Vec<_> = chunks + .iter() + .filter(|c| c.chunk.highlight_style.is_some() && c.chunk.is_inlay) + .collect(); + + assert_eq!(highlighted_chunks.len(), 1); + assert_eq!(highlighted_chunks[0].chunk.text, "SortingDirec…"); + } + + #[gpui::test] + fn test_inlay_utf8_boundaries(cx: &mut App) { + init_test(cx); + + struct TestCase { + inlay_text: &'static str, + highlight_range: Range, + expected_highlighted: &'static str, + description: &'static str, + } + + let test_cases = vec![ + TestCase { + inlay_text: "Hello👋World", + highlight_range: 0..7, + expected_highlighted: "Hello👋", + description: "Emoji boundary - rounds up to include full emoji", + }, + TestCase { + inlay_text: "Test→End", + highlight_range: 0..5, + expected_highlighted: "Test→", + description: "Arrow boundary - rounds up to include full arrow", + }, + TestCase { + inlay_text: "café", + highlight_range: 0..4, + expected_highlighted: "café", + description: "Accented char boundary - rounds up to include full é", + }, + TestCase { + inlay_text: "🎨🎭🎪", + highlight_range: 0..5, + expected_highlighted: "🎨🎭", + description: "Multiple emojis - partial highlight", + }, + TestCase { + inlay_text: "普通话", + highlight_range: 0..4, + expected_highlighted: "普通", + description: "Chinese characters - partial highlight", + }, + TestCase { + inlay_text: "Hello", + highlight_range: 0..2, + expected_highlighted: "He", + description: "ASCII only - no adjustment needed", + }, + TestCase { + inlay_text: "👋", + highlight_range: 0..1, + expected_highlighted: "👋", + description: "Single emoji - partial byte range includes whole char", + }, + TestCase { + inlay_text: "Test", + highlight_range: 0..0, + expected_highlighted: "", + description: "Empty range", + }, + TestCase { + inlay_text: "🎨ABC", + highlight_range: 2..5, + expected_highlighted: "A", + description: "Range starting mid-emoji skips the emoji", + }, + ]; + + for test_case in test_cases { + let buffer = MultiBuffer::build_simple("test", cx); + let (mut inlay_map, _) = InlayMap::new(buffer.read(cx).snapshot(cx)); + let position = buffer.read(cx).snapshot(cx).anchor_before(Point::new(0, 2)); + + let inlay = Inlay { + id: InlayId::Hint(0), + position, + text: text::Rope::from(test_case.inlay_text), + color: None, + }; + + let (inlay_snapshot, _) = inlay_map.splice(&[], vec![inlay]); + let inlay_highlights = create_inlay_highlights( + InlayId::Hint(0), + test_case.highlight_range.clone(), + position, + ); + + let highlights = crate::display_map::Highlights { + text_highlights: None, + inlay_highlights: Some(&inlay_highlights), + styles: crate::display_map::HighlightStyles::default(), + }; + + let chunks: Vec<_> = inlay_snapshot + .chunks( + InlayOffset(0)..InlayOffset(inlay_snapshot.len().0), + false, + highlights, + ) + .collect(); + + // Verify we got chunks and they total to the expected text + let full_text: String = chunks.iter().map(|c| c.chunk.text).collect(); + assert_eq!( + full_text, + format!("te{}st", test_case.inlay_text), + "Full text mismatch for case: {}", + test_case.description + ); + + // Verify that the highlighted portion matches expectations + let highlighted_text: String = chunks + .iter() + .filter(|c| c.chunk.highlight_style.is_some() && c.chunk.is_inlay) + .map(|c| c.chunk.text) + .collect(); + assert_eq!( + highlighted_text, test_case.expected_highlighted, + "Highlighted text mismatch for case: {} (text: '{}', range: {:?})", + test_case.description, test_case.inlay_text, test_case.highlight_range + ); + } + } } From 7a2593e5206fd8fe15be34cc8c195d3aa8ba97d1 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Thu, 3 Jul 2025 15:04:33 -0600 Subject: [PATCH 028/239] Fix JSON Schema definitions path used for debug task (#33873) Regression in #33678 Release Notes: - (Preview Only) Fixed invalid json schema for `debug.json`. --- crates/task/src/debug_format.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index f95fcf56b649122b6c0243db8699e2400dc98f82..2d24098bbb9644feccbae5f9e2f12a3b180b5d18 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -301,7 +301,12 @@ impl DebugTaskFile { .get_mut("properties") .and_then(|value| value.as_object_mut()) { - properties.remove("label"); + if properties.remove("label").is_none() { + debug_panic!( + "Generated TaskTemplate json schema did not have expected 'label' field. \ + Schema of 2nd alternative is: {template_object:?}" + ); + } } if let Some(arr) = template_object @@ -311,13 +316,13 @@ impl DebugTaskFile { arr.retain(|v| v.as_str() != Some("label")); } } else { - debug_panic!("Task Template schema in debug scenario's needs to be updated"); + debug_panic!( + "Generated TaskTemplate json schema did not match expectations. \ + Schema is: {build_task_value:?}" + ); } - let task_definitions = build_task_value - .get("definitions") - .cloned() - .unwrap_or_default(); + let task_definitions = build_task_value.get("$defs").cloned().unwrap_or_default(); let adapter_conditions = schemas .0 @@ -375,7 +380,7 @@ impl DebugTaskFile { }, "allOf": adapter_conditions }, - "definitions": task_definitions + "$defs": task_definitions }) } } From 0ebf7f54bb4fe0a04809850b65a63651b47fa041 Mon Sep 17 00:00:00 2001 From: Taras Martyniuk Date: Fri, 4 Jul 2025 01:25:11 +0300 Subject: [PATCH 029/239] Fix Shift+Enter to send newline instead of carriage return in terminal (#33859) Closes #33858 Changes Shift+Enter in the built-in terminal to send line feed (`\x0a`) instead of carriage return (`\x0d`), enabling multi-line input in Claude Code and other terminal applications. Release Notes: - Fixed the issue where Claude Code and other multi-line terminal applications couldn't use Shift+Enter for newlines. --- crates/terminal/src/mappings/keys.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/terminal/src/mappings/keys.rs b/crates/terminal/src/mappings/keys.rs index a9139ae601396e97718864c40819db26651696a7..b003bf82ad368cd9938788b26a037895677f2caa 100644 --- a/crates/terminal/src/mappings/keys.rs +++ b/crates/terminal/src/mappings/keys.rs @@ -56,7 +56,7 @@ pub fn to_esc_str( ("tab", AlacModifiers::None) => Some("\x09"), ("escape", AlacModifiers::None) => Some("\x1b"), ("enter", AlacModifiers::None) => Some("\x0d"), - ("enter", AlacModifiers::Shift) => Some("\x0d"), + ("enter", AlacModifiers::Shift) => Some("\x0a"), ("enter", AlacModifiers::Alt) => Some("\x1b\x0d"), ("backspace", AlacModifiers::None) => Some("\x7f"), //Interesting escape codes @@ -406,6 +406,22 @@ mod test { } } + #[test] + fn test_shift_enter_newline() { + let shift_enter = Keystroke::parse("shift-enter").unwrap(); + let regular_enter = Keystroke::parse("enter").unwrap(); + let mode = TermMode::NONE; + + // Shift-enter should send line feed (newline) + assert_eq!(to_esc_str(&shift_enter, &mode, false), Some("\x0a".into())); + + // Regular enter should still send carriage return + assert_eq!( + to_esc_str(®ular_enter, &mode, false), + Some("\x0d".into()) + ); + } + #[test] fn test_modifier_code_calc() { // Code Modifiers From 91bfe6f968d1bc45bfd00943195211216491ee1a Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 4 Jul 2025 01:12:12 +0200 Subject: [PATCH 030/239] debugger: Improve performance with large # of output (#33874) Closes #33820 Release Notes: - Improved performance of debug console when there are lots of output events. --------- Co-authored-by: Cole Miller --- .../src/session/running/console.rs | 433 +++++++++--------- crates/debugger_ui/src/tests/console.rs | 8 +- crates/project/src/debugger/session.rs | 15 +- 3 files changed, 234 insertions(+), 222 deletions(-) diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index c247f93ca13ce651a556b66d21b5c67520987398..fca8df14cdb3bd84a1b8946df883acb429085d6a 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -16,12 +16,14 @@ use language::{Buffer, CodeLabel, ToOffset}; use menu::Confirm; use project::{ Completion, CompletionResponse, - debugger::session::{CompletionsQuery, OutputToken, Session, SessionEvent}, + debugger::session::{CompletionsQuery, OutputToken, Session}, }; use settings::Settings; +use std::fmt::Write; use std::{cell::RefCell, ops::Range, rc::Rc, usize}; use theme::{Theme, ThemeSettings}; use ui::{ContextMenu, Divider, PopoverMenu, SplitButton, Tooltip, prelude::*}; +use util::ResultExt; actions!( console, @@ -39,7 +41,7 @@ pub struct Console { variable_list: Entity, stack_frame_list: Entity, last_token: OutputToken, - update_output_task: Task<()>, + update_output_task: Option>, focus_handle: FocusHandle, } @@ -89,11 +91,6 @@ impl Console { let _subscriptions = vec![ cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events), - cx.subscribe_in(&session, window, |this, _, event, window, cx| { - if let SessionEvent::ConsoleOutput = event { - this.update_output(window, cx) - } - }), cx.on_focus(&focus_handle, window, |console, window, cx| { if console.is_running(cx) { console.query_bar.focus_handle(cx).focus(window); @@ -108,7 +105,7 @@ impl Console { variable_list, _subscriptions, stack_frame_list, - update_output_task: Task::ready(()), + update_output_task: None, last_token: OutputToken(0), focus_handle, } @@ -139,202 +136,116 @@ impl Console { self.session.read(cx).has_new_output(self.last_token) } - pub fn add_messages<'a>( + fn add_messages( &mut self, - events: impl Iterator, + events: Vec, window: &mut Window, cx: &mut App, - ) { - self.console.update(cx, |console, cx| { - console.set_read_only(false); - - for event in events { - let to_insert = format!("{}\n", event.output.trim_end()); - - let mut ansi_handler = ConsoleHandler::default(); - let mut ansi_processor = ansi::Processor::::default(); - - let len = console.buffer().read(cx).len(cx); - ansi_processor.advance(&mut ansi_handler, to_insert.as_bytes()); - let output = std::mem::take(&mut ansi_handler.output); - let mut spans = std::mem::take(&mut ansi_handler.spans); - let mut background_spans = std::mem::take(&mut ansi_handler.background_spans); - if ansi_handler.current_range_start < output.len() { - spans.push(( - ansi_handler.current_range_start..output.len(), - ansi_handler.current_color, - )); - } - if ansi_handler.current_background_range_start < output.len() { - background_spans.push(( - ansi_handler.current_background_range_start..output.len(), - ansi_handler.current_background_color, - )); - } - console.move_to_end(&editor::actions::MoveToEnd, window, cx); - console.insert(&output, window, cx); - let buffer = console.buffer().read(cx).snapshot(cx); - - struct ConsoleAnsiHighlight; - - for (range, color) in spans { - let Some(color) = color else { continue }; - let start_offset = len + range.start; - let range = start_offset..len + range.end; - let range = buffer.anchor_after(range.start)..buffer.anchor_before(range.end); - let style = HighlightStyle { - color: Some(terminal_view::terminal_element::convert_color( - &color, - cx.theme(), - )), - ..Default::default() - }; - console.highlight_text_key::( - start_offset, - vec![range], - style, - cx, - ); - } - - for (range, color) in background_spans { - let Some(color) = color else { continue }; - let start_offset = len + range.start; - let range = start_offset..len + range.end; - let range = buffer.anchor_after(range.start)..buffer.anchor_before(range.end); - - let color_fetcher: fn(&Theme) -> Hsla = match color { - // Named and theme defined colors - ansi::Color::Named(n) => match n { - ansi::NamedColor::Black => |theme| theme.colors().terminal_ansi_black, - ansi::NamedColor::Red => |theme| theme.colors().terminal_ansi_red, - ansi::NamedColor::Green => |theme| theme.colors().terminal_ansi_green, - ansi::NamedColor::Yellow => |theme| theme.colors().terminal_ansi_yellow, - ansi::NamedColor::Blue => |theme| theme.colors().terminal_ansi_blue, - ansi::NamedColor::Magenta => { - |theme| theme.colors().terminal_ansi_magenta - } - ansi::NamedColor::Cyan => |theme| theme.colors().terminal_ansi_cyan, - ansi::NamedColor::White => |theme| theme.colors().terminal_ansi_white, - ansi::NamedColor::BrightBlack => { - |theme| theme.colors().terminal_ansi_bright_black - } - ansi::NamedColor::BrightRed => { - |theme| theme.colors().terminal_ansi_bright_red - } - ansi::NamedColor::BrightGreen => { - |theme| theme.colors().terminal_ansi_bright_green - } - ansi::NamedColor::BrightYellow => { - |theme| theme.colors().terminal_ansi_bright_yellow - } - ansi::NamedColor::BrightBlue => { - |theme| theme.colors().terminal_ansi_bright_blue - } - ansi::NamedColor::BrightMagenta => { - |theme| theme.colors().terminal_ansi_bright_magenta + ) -> Task> { + self.console.update(cx, |_, cx| { + cx.spawn_in(window, async move |console, cx| { + let mut len = console.update(cx, |this, cx| this.buffer().read(cx).len(cx))?; + let (output, spans, background_spans) = cx + .background_spawn(async move { + let mut all_spans = Vec::new(); + let mut all_background_spans = Vec::new(); + let mut to_insert = String::new(); + let mut scratch = String::new(); + + for event in &events { + scratch.clear(); + let mut ansi_handler = ConsoleHandler::default(); + let mut ansi_processor = + ansi::Processor::::default(); + + let trimmed_output = event.output.trim_end(); + let _ = writeln!(&mut scratch, "{trimmed_output}"); + ansi_processor.advance(&mut ansi_handler, scratch.as_bytes()); + let output = std::mem::take(&mut ansi_handler.output); + to_insert.extend(output.chars()); + let mut spans = std::mem::take(&mut ansi_handler.spans); + let mut background_spans = + std::mem::take(&mut ansi_handler.background_spans); + if ansi_handler.current_range_start < output.len() { + spans.push(( + ansi_handler.current_range_start..output.len(), + ansi_handler.current_color, + )); } - ansi::NamedColor::BrightCyan => { - |theme| theme.colors().terminal_ansi_bright_cyan + if ansi_handler.current_background_range_start < output.len() { + background_spans.push(( + ansi_handler.current_background_range_start..output.len(), + ansi_handler.current_background_color, + )); } - ansi::NamedColor::BrightWhite => { - |theme| theme.colors().terminal_ansi_bright_white - } - ansi::NamedColor::Foreground => { - |theme| theme.colors().terminal_foreground - } - ansi::NamedColor::Background => { - |theme| theme.colors().terminal_background - } - ansi::NamedColor::Cursor => |theme| theme.players().local().cursor, - ansi::NamedColor::DimBlack => { - |theme| theme.colors().terminal_ansi_dim_black - } - ansi::NamedColor::DimRed => { - |theme| theme.colors().terminal_ansi_dim_red - } - ansi::NamedColor::DimGreen => { - |theme| theme.colors().terminal_ansi_dim_green - } - ansi::NamedColor::DimYellow => { - |theme| theme.colors().terminal_ansi_dim_yellow - } - ansi::NamedColor::DimBlue => { - |theme| theme.colors().terminal_ansi_dim_blue - } - ansi::NamedColor::DimMagenta => { - |theme| theme.colors().terminal_ansi_dim_magenta - } - ansi::NamedColor::DimCyan => { - |theme| theme.colors().terminal_ansi_dim_cyan - } - ansi::NamedColor::DimWhite => { - |theme| theme.colors().terminal_ansi_dim_white - } - ansi::NamedColor::BrightForeground => { - |theme| theme.colors().terminal_bright_foreground - } - ansi::NamedColor::DimForeground => { - |theme| theme.colors().terminal_dim_foreground + + for (range, _) in spans.iter_mut() { + let start_offset = len + range.start; + *range = start_offset..len + range.end; } - }, - // 'True' colors - ansi::Color::Spec(_) => |theme| theme.colors().editor_background, - // 8 bit, indexed colors - ansi::Color::Indexed(i) => { - match i { - // 0-15 are the same as the named colors above - 0 => |theme| theme.colors().terminal_ansi_black, - 1 => |theme| theme.colors().terminal_ansi_red, - 2 => |theme| theme.colors().terminal_ansi_green, - 3 => |theme| theme.colors().terminal_ansi_yellow, - 4 => |theme| theme.colors().terminal_ansi_blue, - 5 => |theme| theme.colors().terminal_ansi_magenta, - 6 => |theme| theme.colors().terminal_ansi_cyan, - 7 => |theme| theme.colors().terminal_ansi_white, - 8 => |theme| theme.colors().terminal_ansi_bright_black, - 9 => |theme| theme.colors().terminal_ansi_bright_red, - 10 => |theme| theme.colors().terminal_ansi_bright_green, - 11 => |theme| theme.colors().terminal_ansi_bright_yellow, - 12 => |theme| theme.colors().terminal_ansi_bright_blue, - 13 => |theme| theme.colors().terminal_ansi_bright_magenta, - 14 => |theme| theme.colors().terminal_ansi_bright_cyan, - 15 => |theme| theme.colors().terminal_ansi_bright_white, - // 16-231 are a 6x6x6 RGB color cube, mapped to 0-255 using steps defined by XTerm. - // See: https://github.com/xterm-x11/xterm-snapshots/blob/master/256colres.pl - // 16..=231 => { - // let (r, g, b) = rgb_for_index(index as u8); - // rgba_color( - // if r == 0 { 0 } else { r * 40 + 55 }, - // if g == 0 { 0 } else { g * 40 + 55 }, - // if b == 0 { 0 } else { b * 40 + 55 }, - // ) - // } - // 232-255 are a 24-step grayscale ramp from (8, 8, 8) to (238, 238, 238). - // 232..=255 => { - // let i = index as u8 - 232; // Align index to 0..24 - // let value = i * 10 + 8; - // rgba_color(value, value, value) - // } - // For compatibility with the alacritty::Colors interface - // See: https://github.com/alacritty/alacritty/blob/master/alacritty_terminal/src/term/color.rs - _ => |_| gpui::black(), + + for (range, _) in background_spans.iter_mut() { + let start_offset = len + range.start; + *range = start_offset..len + range.end; } + + len += output.len(); + + all_spans.extend(spans); + all_background_spans.extend(background_spans); } - }; - - console.highlight_background_key::( - start_offset, - &[range], - color_fetcher, - cx, - ); - } - } + (to_insert, all_spans, all_background_spans) + }) + .await; + console.update_in(cx, |console, window, cx| { + console.set_read_only(false); + console.move_to_end(&editor::actions::MoveToEnd, window, cx); + console.insert(&output, window, cx); + console.set_read_only(true); + + struct ConsoleAnsiHighlight; + + let buffer = console.buffer().read(cx).snapshot(cx); + + for (range, color) in spans { + let Some(color) = color else { continue }; + let start_offset = range.start; + let range = + buffer.anchor_after(range.start)..buffer.anchor_before(range.end); + let style = HighlightStyle { + color: Some(terminal_view::terminal_element::convert_color( + &color, + cx.theme(), + )), + ..Default::default() + }; + console.highlight_text_key::( + start_offset, + vec![range], + style, + cx, + ); + } - console.set_read_only(true); - cx.notify(); - }); + for (range, color) in background_spans { + let Some(color) = color else { continue }; + let start_offset = range.start; + let range = + buffer.anchor_after(range.start)..buffer.anchor_before(range.end); + console.highlight_background_key::( + start_offset, + &[range], + color_fetcher(color), + cx, + ); + } + + cx.notify(); + })?; + + Ok(()) + }) + }) } pub fn watch_expression( @@ -464,31 +375,50 @@ impl Console { EditorElement::new(&self.query_bar, Self::editor_style(&self.query_bar, cx)) } - fn update_output(&mut self, window: &mut Window, cx: &mut Context) { + pub(crate) fn update_output(&mut self, window: &mut Window, cx: &mut Context) { + if self.update_output_task.is_some() { + return; + } let session = self.session.clone(); let token = self.last_token; - - self.update_output_task = cx.spawn_in(window, async move |this, cx| { - _ = session.update_in(cx, move |session, window, cx| { - let (output, last_processed_token) = session.output(token); - - _ = this.update(cx, |this, cx| { - if last_processed_token == this.last_token { - return; - } - this.add_messages(output, window, cx); - - this.last_token = last_processed_token; + self.update_output_task = Some(cx.spawn_in(window, async move |this, cx| { + let Some((last_processed_token, task)) = session + .update_in(cx, |session, window, cx| { + let (output, last_processed_token) = session.output(token); + + this.update(cx, |this, cx| { + if last_processed_token == this.last_token { + return None; + } + Some(( + last_processed_token, + this.add_messages(output.cloned().collect(), window, cx), + )) + }) + .ok() + .flatten() + }) + .ok() + .flatten() + else { + _ = this.update(cx, |this, _| { + this.update_output_task.take(); }); + return; + }; + _ = task.await.log_err(); + _ = this.update(cx, |this, _| { + this.last_token = last_processed_token; + this.update_output_task.take(); }); - }); + })); } } impl Render for Console { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let query_focus_handle = self.query_bar.focus_handle(cx); - + self.update_output(window, cx); v_flex() .track_focus(&self.focus_handle) .key_context("DebugConsole") @@ -851,3 +781,84 @@ impl ansi::Handler for ConsoleHandler { } } } + +fn color_fetcher(color: ansi::Color) -> fn(&Theme) -> Hsla { + let color_fetcher: fn(&Theme) -> Hsla = match color { + // Named and theme defined colors + ansi::Color::Named(n) => match n { + ansi::NamedColor::Black => |theme| theme.colors().terminal_ansi_black, + ansi::NamedColor::Red => |theme| theme.colors().terminal_ansi_red, + ansi::NamedColor::Green => |theme| theme.colors().terminal_ansi_green, + ansi::NamedColor::Yellow => |theme| theme.colors().terminal_ansi_yellow, + ansi::NamedColor::Blue => |theme| theme.colors().terminal_ansi_blue, + ansi::NamedColor::Magenta => |theme| theme.colors().terminal_ansi_magenta, + ansi::NamedColor::Cyan => |theme| theme.colors().terminal_ansi_cyan, + ansi::NamedColor::White => |theme| theme.colors().terminal_ansi_white, + ansi::NamedColor::BrightBlack => |theme| theme.colors().terminal_ansi_bright_black, + ansi::NamedColor::BrightRed => |theme| theme.colors().terminal_ansi_bright_red, + ansi::NamedColor::BrightGreen => |theme| theme.colors().terminal_ansi_bright_green, + ansi::NamedColor::BrightYellow => |theme| theme.colors().terminal_ansi_bright_yellow, + ansi::NamedColor::BrightBlue => |theme| theme.colors().terminal_ansi_bright_blue, + ansi::NamedColor::BrightMagenta => |theme| theme.colors().terminal_ansi_bright_magenta, + ansi::NamedColor::BrightCyan => |theme| theme.colors().terminal_ansi_bright_cyan, + ansi::NamedColor::BrightWhite => |theme| theme.colors().terminal_ansi_bright_white, + ansi::NamedColor::Foreground => |theme| theme.colors().terminal_foreground, + ansi::NamedColor::Background => |theme| theme.colors().terminal_background, + ansi::NamedColor::Cursor => |theme| theme.players().local().cursor, + ansi::NamedColor::DimBlack => |theme| theme.colors().terminal_ansi_dim_black, + ansi::NamedColor::DimRed => |theme| theme.colors().terminal_ansi_dim_red, + ansi::NamedColor::DimGreen => |theme| theme.colors().terminal_ansi_dim_green, + ansi::NamedColor::DimYellow => |theme| theme.colors().terminal_ansi_dim_yellow, + ansi::NamedColor::DimBlue => |theme| theme.colors().terminal_ansi_dim_blue, + ansi::NamedColor::DimMagenta => |theme| theme.colors().terminal_ansi_dim_magenta, + ansi::NamedColor::DimCyan => |theme| theme.colors().terminal_ansi_dim_cyan, + ansi::NamedColor::DimWhite => |theme| theme.colors().terminal_ansi_dim_white, + ansi::NamedColor::BrightForeground => |theme| theme.colors().terminal_bright_foreground, + ansi::NamedColor::DimForeground => |theme| theme.colors().terminal_dim_foreground, + }, + // 'True' colors + ansi::Color::Spec(_) => |theme| theme.colors().editor_background, + // 8 bit, indexed colors + ansi::Color::Indexed(i) => { + match i { + // 0-15 are the same as the named colors above + 0 => |theme| theme.colors().terminal_ansi_black, + 1 => |theme| theme.colors().terminal_ansi_red, + 2 => |theme| theme.colors().terminal_ansi_green, + 3 => |theme| theme.colors().terminal_ansi_yellow, + 4 => |theme| theme.colors().terminal_ansi_blue, + 5 => |theme| theme.colors().terminal_ansi_magenta, + 6 => |theme| theme.colors().terminal_ansi_cyan, + 7 => |theme| theme.colors().terminal_ansi_white, + 8 => |theme| theme.colors().terminal_ansi_bright_black, + 9 => |theme| theme.colors().terminal_ansi_bright_red, + 10 => |theme| theme.colors().terminal_ansi_bright_green, + 11 => |theme| theme.colors().terminal_ansi_bright_yellow, + 12 => |theme| theme.colors().terminal_ansi_bright_blue, + 13 => |theme| theme.colors().terminal_ansi_bright_magenta, + 14 => |theme| theme.colors().terminal_ansi_bright_cyan, + 15 => |theme| theme.colors().terminal_ansi_bright_white, + // 16-231 are a 6x6x6 RGB color cube, mapped to 0-255 using steps defined by XTerm. + // See: https://github.com/xterm-x11/xterm-snapshots/blob/master/256colres.pl + // 16..=231 => { + // let (r, g, b) = rgb_for_index(index as u8); + // rgba_color( + // if r == 0 { 0 } else { r * 40 + 55 }, + // if g == 0 { 0 } else { g * 40 + 55 }, + // if b == 0 { 0 } else { b * 40 + 55 }, + // ) + // } + // 232-255 are a 24-step grayscale ramp from (8, 8, 8) to (238, 238, 238). + // 232..=255 => { + // let i = index as u8 - 232; // Align index to 0..24 + // let value = i * 10 + 8; + // rgba_color(value, value, value) + // } + // For compatibility with the alacritty::Colors interface + // See: https://github.com/alacritty/alacritty/blob/master/alacritty_terminal/src/term/color.rs + _ => |_| gpui::black(), + } + } + }; + color_fetcher +} diff --git a/crates/debugger_ui/src/tests/console.rs b/crates/debugger_ui/src/tests/console.rs index cae2ff3501e5313aa93b3a124a544238d41fc953..fad483b0f4af19826f9da0d32659c8ac83712f1f 100644 --- a/crates/debugger_ui/src/tests/console.rs +++ b/crates/debugger_ui/src/tests/console.rs @@ -232,7 +232,6 @@ async fn test_escape_code_processing(executor: BackgroundExecutor, cx: &mut Test location_reference: None, })) .await; - // [crates/debugger_ui/src/session/running/console.rs:147:9] &to_insert = "Could not read source map for file:///Users/cole/roles-at/node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/typescript.js: ENOENT: no such file or directory, open '/Users/cole/roles-at/node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/typescript.js.map'\n" client .fake_event(dap::messages::Events::Output(dap::OutputEvent { category: None, @@ -260,7 +259,6 @@ async fn test_escape_code_processing(executor: BackgroundExecutor, cx: &mut Test })) .await; - // introduce some background highlight client .fake_event(dap::messages::Events::Output(dap::OutputEvent { category: None, @@ -274,7 +272,6 @@ async fn test_escape_code_processing(executor: BackgroundExecutor, cx: &mut Test location_reference: None, })) .await; - // another random line client .fake_event(dap::messages::Events::Output(dap::OutputEvent { category: None, @@ -294,6 +291,11 @@ async fn test_escape_code_processing(executor: BackgroundExecutor, cx: &mut Test let _running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| { cx.focus_self(window); + item.running_state().update(cx, |this, cx| { + this.console() + .update(cx, |this, cx| this.update_output(window, cx)); + }); + item.running_state().clone() }); diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index b76200aee6e923e81558e2c9e834c6d481f16ce6..f04aadf2df03837077226935d699204d2292935f 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -1016,7 +1016,7 @@ impl Session { cx.spawn(async move |this, cx| { while let Some(output) = rx.next().await { - this.update(cx, |this, cx| { + this.update(cx, |this, _| { let event = dap::OutputEvent { category: None, output, @@ -1028,7 +1028,7 @@ impl Session { data: None, location_reference: None, }; - this.push_output(event, cx); + this.push_output(event); })?; } anyhow::Ok(()) @@ -1458,7 +1458,7 @@ impl Session { return; } - self.push_output(event, cx); + self.push_output(event); cx.notify(); } Events::Breakpoint(event) => self.breakpoint_store.update(cx, |store, _| { @@ -1645,10 +1645,9 @@ impl Session { }); } - fn push_output(&mut self, event: OutputEvent, cx: &mut Context) { + fn push_output(&mut self, event: OutputEvent) { self.output.push_back(event); self.output_token.0 += 1; - cx.emit(SessionEvent::ConsoleOutput); } pub fn any_stopped_thread(&self) -> bool { @@ -2352,7 +2351,7 @@ impl Session { data: None, location_reference: None, }; - self.push_output(event, cx); + self.push_output(event); let request = self.mode.request_dap(EvaluateCommand { expression, context, @@ -2375,7 +2374,7 @@ impl Session { data: None, location_reference: None, }; - this.push_output(event, cx); + this.push_output(event); } Err(e) => { let event = dap::OutputEvent { @@ -2389,7 +2388,7 @@ impl Session { data: None, location_reference: None, }; - this.push_output(event, cx); + this.push_output(event); } }; cx.notify(); From 03ca2f4d2be45e44f663540bb927646268aff9b4 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 3 Jul 2025 19:57:57 -0400 Subject: [PATCH 031/239] Fix yaml comment indent (#33882) Closes #33761 The problem was that in the indentation regex we were treating lines that had `:` in them as requiring an indent on the next line, even if that `:` was inside a comment. Release Notes: - Fixed YAML indentation for lines containing comments with `:` in them --- Cargo.lock | 1 + crates/editor/Cargo.toml | 1 + crates/editor/src/editor_tests.rs | 64 +++++++++++++++++++++++++++ crates/languages/src/yaml/config.toml | 2 +- 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 48928bcea0ff0fe5b9b9da256f265588e81f32ad..d7e645131d982ee10f25e5e84e558d54c0ab0b76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4830,6 +4830,7 @@ dependencies = [ "tree-sitter-python", "tree-sitter-rust", "tree-sitter-typescript", + "tree-sitter-yaml", "ui", "unicode-script", "unicode-segmentation", diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index bea83b1df826a3dac1cf4afe14a0dd7b417b972b..4d6939567eb8150883a4eb5e4e9e5b0949a421a0 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -109,6 +109,7 @@ theme = { workspace = true, features = ["test-support"] } tree-sitter-html.workspace = true tree-sitter-rust.workspace = true tree-sitter-typescript.workspace = true +tree-sitter-yaml.workspace = true unindent.workspace = true util = { workspace = true, features = ["test-support"] } workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 8615ff2a97de676b593e46d15811ad43f1a93706..ade9a9322bcdbe38ad33fe9611820c43e2ea5809 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -3468,6 +3468,70 @@ async fn test_indent_outdent(cx: &mut TestAppContext) { "}); } +#[gpui::test] +async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) { + // This is a regression test for issue #33761 + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into()); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx)); + + cx.set_state( + r#"ˇ# ingress: +ˇ# api: +ˇ# enabled: false +ˇ# pathType: Prefix +ˇ# console: +ˇ# enabled: false +ˇ# pathType: Prefix +"#, + ); + + // Press tab to indent all lines + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); + + cx.assert_editor_state( + r#" ˇ# ingress: + ˇ# api: + ˇ# enabled: false + ˇ# pathType: Prefix + ˇ# console: + ˇ# enabled: false + ˇ# pathType: Prefix +"#, + ); +} + +#[gpui::test] +async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) { + // This is a test to make sure our fix for issue #33761 didn't break anything + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into()); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx)); + + cx.set_state( + r#"ˇingress: +ˇ api: +ˇ enabled: false +ˇ pathType: Prefix +"#, + ); + + // Press tab to indent all lines + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); + + cx.assert_editor_state( + r#"ˇingress: + ˇapi: + ˇenabled: false + ˇpathType: Prefix +"#, + ); +} + #[gpui::test] async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) { init_test(cx, |settings| { diff --git a/crates/languages/src/yaml/config.toml b/crates/languages/src/yaml/config.toml index cf3d9e1181a3844987afd7b017a8c905b3bb5691..4dfb890c5481c3814722b9d143c17d7d8399b478 100644 --- a/crates/languages/src/yaml/config.toml +++ b/crates/languages/src/yaml/config.toml @@ -12,6 +12,6 @@ brackets = [ auto_indent_on_paste = false auto_indent_using_last_non_empty_line = false -increase_indent_pattern = ":\\s*[|>]?\\s*$" +increase_indent_pattern = "^[^#]*:\\s*[|>]?\\s*$" prettier_parser_name = "yaml" tab_size = 2 From 38544e514abcfb193ca50faf25cd07e78a503798 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 3 Jul 2025 20:15:02 -0400 Subject: [PATCH 032/239] debugger: Use debugpy's suggested names for child sessions (#33885) Just like vscode-js-debug, debugpy uses the `name` key in StartDebuggingRequestArguments for this: https://github.com/microsoft/debugpy/blob/0d65353cc6e519292296bf567bdc6dfa5bcd4ffc/src/debugpy/adapter/clients.py#L753 Release Notes: - debugger: Made the names of Python subprocesses in the session list more helpful. --- crates/dap_adapters/src/python.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 43d1246d0c8ff1e2580d50b37f02020dc6804c61..dc3d15e124578e183ba5ed09b80aee7d6dda54c8 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -660,6 +660,15 @@ impl DebugAdapter for PythonDebugAdapter { self.get_installed_binary(delegate, &config, None, user_args, toolchain, false) .await } + + fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option { + let label = args + .configuration + .get("name")? + .as_str() + .filter(|label| !label.is_empty())?; + Some(label.to_owned()) + } } async fn fetch_latest_adapter_version_from_github( From ed7552d3e3f4e3a07ea8805c3a88085507994aed Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Thu, 3 Jul 2025 18:57:43 -0600 Subject: [PATCH 033/239] Default `#[schemars(deny_unknown_fields)] for json-language-server schemas (#33883) Followup to #33678, doing the same thing for all JSON Schema files provided to json-language-server Release Notes: * Added warnings for unknown fields when editing `tasks.json` / `snippets.json`. --- Cargo.lock | 1 + crates/language/src/language_settings.rs | 2 +- crates/languages/src/json.rs | 1 + crates/settings/src/keymap_file.rs | 4 ++ crates/settings/src/settings_json.rs | 58 ----------------------- crates/settings/src/settings_store.rs | 47 +++++++++---------- crates/snippet_provider/src/format.rs | 2 + crates/task/src/adapter_schema.rs | 48 +------------------ crates/task/src/debug_format.rs | 59 ++++++++++++++++-------- crates/task/src/task_template.rs | 2 + crates/theme/src/settings.rs | 3 +- crates/util/Cargo.toml | 1 + crates/util/src/schemars.rs | 58 +++++++++++++++++++++++ crates/util/src/util.rs | 1 + 14 files changed, 137 insertions(+), 150 deletions(-) create mode 100644 crates/util/src/schemars.rs diff --git a/Cargo.lock b/Cargo.lock index d7e645131d982ee10f25e5e84e558d54c0ab0b76..baed77a49fd7c35af5ede122c854746308fde162 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17348,6 +17348,7 @@ dependencies = [ "rand 0.8.5", "regex", "rust-embed", + "schemars", "serde", "serde_json", "serde_json_lenient", diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 1caa6eceec0ebc3b6ff0ea3cb1ee33d5e2ec6e86..9b0abb15379916453eeeeb35860e859a7f721458 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -18,10 +18,10 @@ use serde::{ use settings::{ ParameterizedJsonSchema, Settings, SettingsLocation, SettingsSources, SettingsStore, - replace_subschema, }; use shellexpand; use std::{borrow::Cow, num::NonZeroU32, path::Path, slice, sync::Arc}; +use util::schemars::replace_subschema; use util::serde::default_true; /// Initializes the language settings. diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 6f51cdadbaae62c1f806b614d9624061cd324c62..7a3300eb010d9da30111023e660ef56a2070ea9e 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -272,6 +272,7 @@ impl JsonLspAdapter { #[cfg(debug_assertions)] fn generate_inspector_style_schema() -> serde_json_lenient::Value { let schema = schemars::generate::SchemaSettings::draft2019_09() + .with_transform(util::schemars::DefaultDenyUnknownFields) .into_generator() .root_schema_for::(); diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 0548a69b55303e0bdf0dd3ecfda0f7892bb94026..4c4ceee49bcd0a90ac43329e6ecd6211a423ae65 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -427,6 +427,10 @@ impl KeymapFile { } pub fn generate_json_schema_for_registered_actions(cx: &mut App) -> Value { + // instead of using DefaultDenyUnknownFields, actions typically use + // `#[serde(deny_unknown_fields)]` so that these cases are reported as parse failures. This + // is because the rest of the keymap will still load in these cases, whereas other settings + // files would not. let mut generator = schemars::generate::SchemaSettings::draft2019_09().into_generator(); let action_schemas = cx.action_schemas(&mut generator); diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs index d78043a3354ffd8cf25be0f95c27df5cf1f8f8a8..f569a187699b764bbac43cca8c3799ab043c373b 100644 --- a/crates/settings/src/settings_json.rs +++ b/crates/settings/src/settings_json.rs @@ -1,6 +1,5 @@ use anyhow::Result; use gpui::App; -use schemars::{JsonSchema, Schema, transform::transform_subschemas}; use serde::{Serialize, de::DeserializeOwned}; use serde_json::Value; use std::{ops::Range, sync::LazyLock}; @@ -21,63 +20,6 @@ pub struct ParameterizedJsonSchema { inventory::collect!(ParameterizedJsonSchema); -const DEFS_PATH: &str = "#/$defs/"; - -/// Replaces the JSON schema definition for some type if it is in use (in the definitions list), and -/// returns a reference to it. -/// -/// This asserts that JsonSchema::schema_name() + "2" does not exist because this indicates that -/// there are multiple types that use this name, and unfortunately schemars APIs do not support -/// resolving this ambiguity - see https://github.com/GREsau/schemars/issues/449 -/// -/// This takes a closure for `schema` because some settings types are not available on the remote -/// server, and so will crash when attempting to access e.g. GlobalThemeRegistry. -pub fn replace_subschema( - generator: &mut schemars::SchemaGenerator, - schema: impl Fn() -> schemars::Schema, -) -> schemars::Schema { - // fallback on just using the schema name, which could collide. - let schema_name = T::schema_name(); - let definitions = generator.definitions_mut(); - assert!(!definitions.contains_key(&format!("{schema_name}2"))); - if definitions.contains_key(schema_name.as_ref()) { - definitions.insert(schema_name.to_string(), schema().to_value()); - } - Schema::new_ref(format!("{DEFS_PATH}{schema_name}")) -} - -/// Adds a new JSON schema definition and returns a reference to it. **Panics** if the name is -/// already in use. -pub fn add_new_subschema( - generator: &mut schemars::SchemaGenerator, - name: &str, - schema: Value, -) -> Schema { - let old_definition = generator.definitions_mut().insert(name.to_string(), schema); - assert_eq!(old_definition, None); - schemars::Schema::new_ref(format!("{DEFS_PATH}{name}")) -} - -/// Defaults `additionalProperties` to `true`, as if `#[schemars(deny_unknown_fields)]` was on every -/// struct. Skips structs that have `additionalProperties` set (such as if #[serde(flatten)] is used -/// on a map). -#[derive(Clone)] -pub struct DefaultDenyUnknownFields; - -impl schemars::transform::Transform for DefaultDenyUnknownFields { - fn transform(&mut self, schema: &mut schemars::Schema) { - if let Some(object) = schema.as_object_mut() { - if object.contains_key("properties") - && !object.contains_key("additionalProperties") - && !object.contains_key("unevaluatedProperties") - { - object.insert("additionalProperties".to_string(), false.into()); - } - } - transform_subschemas(self, schema); - } -} - pub fn update_value_in_json_text<'a>( text: &mut String, key_path: &mut Vec<&'a str>, diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 0ba516ad7d6d108b431cb3371e936fb82c7e2318..0d23385a682fbf8fc3b8eec97c98748b5664d480 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -6,7 +6,7 @@ use futures::{FutureExt, StreamExt, channel::mpsc, future::LocalBoxFuture}; use gpui::{App, AsyncApp, BorrowAppContext, Global, Task, UpdateGlobal}; use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name}; -use schemars::{JsonSchema, json_schema}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use serde_json::{Value, json}; use smallvec::SmallVec; @@ -18,14 +18,16 @@ use std::{ str::{self, FromStr}, sync::Arc, }; - -use util::{ResultExt as _, merge_non_null_json_value_into}; +use util::{ + ResultExt as _, merge_non_null_json_value_into, + schemars::{DefaultDenyUnknownFields, add_new_subschema}, +}; pub type EditorconfigProperties = ec4rs::Properties; use crate::{ - DefaultDenyUnknownFields, ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings, - WorktreeId, add_new_subschema, parse_json_with_comments, update_value_in_json_text, + ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings, WorktreeId, + parse_json_with_comments, update_value_in_json_text, }; /// A value that can be defined as a user setting. @@ -1019,19 +1021,19 @@ impl SettingsStore { .unwrap() .remove("additionalProperties"); - let mut root_schema = if let Some(meta_schema) = generator.settings().meta_schema.as_ref() { - json_schema!({ "$schema": meta_schema.to_string() }) - } else { - json_schema!({}) - }; - - // "unevaluatedProperties: false" to report unknown fields. - root_schema.insert("unevaluatedProperties".to_string(), false.into()); - - // Settings file contents matches ZedSettings + overrides for each release stage. - root_schema.insert( - "allOf".to_string(), - json!([ + let meta_schema = generator + .settings() + .meta_schema + .as_ref() + .expect("meta_schema should be present in schemars settings") + .to_string(); + + json!({ + "$schema": meta_schema, + "title": "Zed Settings", + "unevaluatedProperties": false, + // ZedSettings + settings overrides for each release stage + "allOf": [ zed_settings_ref, { "properties": { @@ -1041,12 +1043,9 @@ impl SettingsStore { "preview": zed_release_stage_settings_ref, } } - ]), - ); - - root_schema.insert("$defs".to_string(), definitions.into()); - - root_schema.to_value() + ], + "$defs": definitions, + }) } fn recompute_values( diff --git a/crates/snippet_provider/src/format.rs b/crates/snippet_provider/src/format.rs index 0d06cbbc887bdce83514e9b14516539c86b8ee42..1a390aa2e17dfed69cf3b298b0d1f8dfe4e2cd1d 100644 --- a/crates/snippet_provider/src/format.rs +++ b/crates/snippet_provider/src/format.rs @@ -3,6 +3,7 @@ use schemars::{JsonSchema, json_schema}; use serde::Deserialize; use serde_json_lenient::Value; use std::borrow::Cow; +use util::schemars::DefaultDenyUnknownFields; #[derive(Deserialize)] pub struct VsSnippetsFile { @@ -13,6 +14,7 @@ pub struct VsSnippetsFile { impl VsSnippetsFile { pub fn generate_json_schema() -> Value { let schema = schemars::generate::SchemaSettings::draft2019_09() + .with_transform(DefaultDenyUnknownFields) .into_generator() .root_schema_for::(); diff --git a/crates/task/src/adapter_schema.rs b/crates/task/src/adapter_schema.rs index 111f555ca521a4c4630a002cc5b35dc7b404218d..2c58bc0eabcdcb2aeef8eee6b32828a334634229 100644 --- a/crates/task/src/adapter_schema.rs +++ b/crates/task/src/adapter_schema.rs @@ -1,10 +1,8 @@ -use anyhow::Result; use gpui::SharedString; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use serde_json::json; -/// Represents a schema for a specific adapter +/// JSON schema for a specific adapter #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct AdapterSchema { /// The adapter name identifier @@ -16,47 +14,3 @@ pub struct AdapterSchema { #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(transparent)] pub struct AdapterSchemas(pub Vec); - -impl AdapterSchemas { - pub fn generate_json_schema(&self) -> Result { - let adapter_conditions = self - .0 - .iter() - .map(|adapter_schema| { - let adapter_name = adapter_schema.adapter.to_string(); - json!({ - "if": { - "properties": { - "adapter": { "const": adapter_name } - } - }, - "then": adapter_schema.schema - }) - }) - .collect::>(); - - let schema = serde_json_lenient::json!({ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Debug Adapter Configurations", - "description": "Configuration for debug adapters. Schema changes based on the selected adapter.", - "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" - }, - }, - "allOf": adapter_conditions - } - }); - - Ok(serde_json_lenient::to_value(schema)?) - } -} diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 2d24098bbb9644feccbae5f9e2f12a3b180b5d18..f20f55975e7a1d178b899b7cc89d19676d7f27cf 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -6,7 +6,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::net::Ipv4Addr; use std::path::PathBuf; -use util::debug_panic; +use util::{debug_panic, schemars::add_new_subschema}; use crate::{TaskTemplate, adapter_schema::AdapterSchemas}; @@ -286,11 +286,10 @@ pub struct DebugScenario { pub struct DebugTaskFile(pub Vec); impl DebugTaskFile { - pub fn generate_json_schema(schemas: &AdapterSchemas) -> serde_json_lenient::Value { + pub fn generate_json_schema(schemas: &AdapterSchemas) -> serde_json::Value { let mut generator = schemars::generate::SchemaSettings::draft2019_09().into_generator(); - let build_task_schema = generator.root_schema_for::(); - let mut build_task_value = - serde_json_lenient::to_value(&build_task_schema).unwrap_or_default(); + + let mut build_task_value = BuildTaskDefinition::json_schema(&mut generator).to_value(); if let Some(template_object) = build_task_value .get_mut("anyOf") @@ -322,32 +321,54 @@ impl DebugTaskFile { ); } - let task_definitions = build_task_value.get("$defs").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 - }) + add_new_subschema( + &mut generator, + &format!("{adapter_name}DebugSettings"), + serde_json::json!({ + "if": { + "properties": { + "adapter": { "const": adapter_name } + } + }, + "then": adapter_schema.schema + }), + ) }) .collect::>(); - serde_json_lenient::json!({ - "$schema": "http://json-schema.org/draft-07/schema#", + let build_task_definition_ref = add_new_subschema( + &mut generator, + BuildTaskDefinition::schema_name().as_ref(), + build_task_value, + ); + + let meta_schema = generator + .settings() + .meta_schema + .as_ref() + .expect("meta_schema should be present in schemars settings") + .to_string(); + + serde_json::json!({ + "$schema": meta_schema, "title": "Debug Configurations", "description": "Configuration for debug scenarios", "type": "array", "items": { "type": "object", "required": ["adapter", "label"], + // TODO: Uncommenting this will cause json-language-server to provide warnings for + // unrecognized properties. It should be enabled if/when there's an adapter JSON + // schema that's comprehensive. In order to not get warnings for the other schemas, + // `additionalProperties` or `unevaluatedProperties` (to handle "allOf" etc style + // schema combinations) could be set to `true` for that schema. + // + // "unevaluatedProperties": false, "properties": { "adapter": { "type": "string", @@ -357,7 +378,7 @@ impl DebugTaskFile { "type": "string", "description": "The name of the debug configuration" }, - "build": build_task_value, + "build": build_task_definition_ref, "tcp_connection": { "type": "object", "description": "Optional TCP connection information for connecting to an already running debug adapter", @@ -380,7 +401,7 @@ impl DebugTaskFile { }, "allOf": adapter_conditions }, - "$defs": task_definitions + "$defs": generator.take_definitions(true), }) } } diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 65424eeed4612b3e0f35be509f782b5947e7198f..cc36b28e4beebc230cd635b894c5288cfcd3ada4 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -4,6 +4,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::path::PathBuf; +use util::schemars::DefaultDenyUnknownFields; use util::serde::default_true; use util::{ResultExt, truncate_and_remove_front}; @@ -116,6 +117,7 @@ impl TaskTemplates { /// Generates JSON schema of Tasks JSON template format. pub fn generate_json_schema() -> serde_json_lenient::Value { let schema = schemars::generate::SchemaSettings::draft2019_09() + .with_transform(DefaultDenyUnknownFields) .into_generator() .root_schema_for::(); diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index ca59eba76672a6036533bd620dfca0fb69ab44f5..1c4c90a475ca3fa155d4d7169f3f72d37193a747 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -12,9 +12,10 @@ use gpui::{ use refineable::Refineable; use schemars::{JsonSchema, json_schema}; use serde::{Deserialize, Serialize}; -use settings::{ParameterizedJsonSchema, Settings, SettingsSources, replace_subschema}; +use settings::{ParameterizedJsonSchema, Settings, SettingsSources}; use std::sync::Arc; use util::ResultExt as _; +use util::schemars::replace_subschema; const MIN_FONT_SIZE: Pixels = px(6.0); const MIN_LINE_HEIGHT: f32 = 1.0; diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 6a874fd3293c55ebc3ae29158314bf2806ab92fa..825d6471b2d0585803d2f5567eadef11ffdc9d1b 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -30,6 +30,7 @@ log.workspace = true rand = { workspace = true, optional = true } regex.workspace = true rust-embed.workspace = true +schemars.workspace = true serde.workspace = true serde_json.workspace = true serde_json_lenient.workspace = true diff --git a/crates/util/src/schemars.rs b/crates/util/src/schemars.rs new file mode 100644 index 0000000000000000000000000000000000000000..4d8ab530dd6beb3cf3c448256ff4bde89f9de8f7 --- /dev/null +++ b/crates/util/src/schemars.rs @@ -0,0 +1,58 @@ +use schemars::{JsonSchema, transform::transform_subschemas}; + +const DEFS_PATH: &str = "#/$defs/"; + +/// Replaces the JSON schema definition for some type if it is in use (in the definitions list), and +/// returns a reference to it. +/// +/// This asserts that JsonSchema::schema_name() + "2" does not exist because this indicates that +/// there are multiple types that use this name, and unfortunately schemars APIs do not support +/// resolving this ambiguity - see https://github.com/GREsau/schemars/issues/449 +/// +/// This takes a closure for `schema` because some settings types are not available on the remote +/// server, and so will crash when attempting to access e.g. GlobalThemeRegistry. +pub fn replace_subschema( + generator: &mut schemars::SchemaGenerator, + schema: impl Fn() -> schemars::Schema, +) -> schemars::Schema { + // fallback on just using the schema name, which could collide. + let schema_name = T::schema_name(); + let definitions = generator.definitions_mut(); + assert!(!definitions.contains_key(&format!("{schema_name}2"))); + if definitions.contains_key(schema_name.as_ref()) { + definitions.insert(schema_name.to_string(), schema().to_value()); + } + schemars::Schema::new_ref(format!("{DEFS_PATH}{schema_name}")) +} + +/// Adds a new JSON schema definition and returns a reference to it. **Panics** if the name is +/// already in use. +pub fn add_new_subschema( + generator: &mut schemars::SchemaGenerator, + name: &str, + schema: serde_json::Value, +) -> schemars::Schema { + let old_definition = generator.definitions_mut().insert(name.to_string(), schema); + assert_eq!(old_definition, None); + schemars::Schema::new_ref(format!("{DEFS_PATH}{name}")) +} + +/// Defaults `additionalProperties` to `true`, as if `#[schemars(deny_unknown_fields)]` was on every +/// struct. Skips structs that have `additionalProperties` set (such as if #[serde(flatten)] is used +/// on a map). +#[derive(Clone)] +pub struct DefaultDenyUnknownFields; + +impl schemars::transform::Transform for DefaultDenyUnknownFields { + fn transform(&mut self, schema: &mut schemars::Schema) { + if let Some(object) = schema.as_object_mut() { + if object.contains_key("properties") + && !object.contains_key("additionalProperties") + && !object.contains_key("unevaluatedProperties") + { + object.insert("additionalProperties".to_string(), false.into()); + } + } + transform_subschemas(self, schema); + } +} diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index eb07d3e5e51c17dabdb2b488c6e97cf0b1995a98..86bee7ffd14c1782f806ebfe4dbe0537b675a5bc 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -5,6 +5,7 @@ pub mod fs; pub mod markdown; pub mod paths; pub mod redact; +pub mod schemars; pub mod serde; pub mod shell_env; pub mod size; From 5253702200f4fb491b6608ac0a0c6df89b224b0f Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 4 Jul 2025 01:49:11 -0300 Subject: [PATCH 034/239] Move the `LoadingLabel` component to the UI crate (#33893) So we can use it in other places and don't require them to depend on the `agent_ui`. This PR also renames it from `AnimatedLabel` to `LoadingLabel`. Release Notes: - N/A --- crates/agent_ui/src/active_thread.rs | 10 ++++------ crates/agent_ui/src/ui.rs | 2 -- crates/ui/src/components/label.rs | 2 ++ .../src/components/label/loading_label.rs} | 14 +++++++------- crates/ui/src/prelude.rs | 2 +- 5 files changed, 14 insertions(+), 16 deletions(-) rename crates/{agent_ui/src/ui/animated_label.rs => ui/src/components/label/loading_label.rs} (94%) diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 3937f2d15d17afa75edbe7bf0ce25752c6e30291..a4553fc9011b3f0bee51d08853200fac0a2950ee 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -1,9 +1,7 @@ use crate::context_picker::{ContextPicker, MentionLink}; use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; use crate::message_editor::{extract_message_creases, insert_message_creases}; -use crate::ui::{ - AddedContext, AgentNotification, AgentNotificationEvent, AnimatedLabel, ContextPill, -}; +use crate::ui::{AddedContext, AgentNotification, AgentNotificationEvent, ContextPill}; use crate::{AgentPanel, ModelUsageContext}; use agent::{ ContextStore, LastRestoreCheckpoint, MessageCrease, MessageId, MessageSegment, TextThreadStore, @@ -1820,7 +1818,7 @@ impl ActiveThread { .my_3() .mx_5() .when(is_generating_stale || message.is_hidden, |this| { - this.child(AnimatedLabel::new("").size(LabelSize::Small)) + this.child(LoadingLabel::new("").size(LabelSize::Small)) }) }); @@ -2586,7 +2584,7 @@ impl ActiveThread { .size(IconSize::XSmall) .color(Color::Muted), ) - .child(AnimatedLabel::new("Thinking").size(LabelSize::Small)), + .child(LoadingLabel::new("Thinking").size(LabelSize::Small)), ) .child( h_flex() @@ -3155,7 +3153,7 @@ impl ActiveThread { .border_color(self.tool_card_border_color(cx)) .rounded_b_lg() .child( - AnimatedLabel::new("Waiting for Confirmation").size(LabelSize::Small) + LoadingLabel::new("Waiting for Confirmation").size(LabelSize::Small) ) .child( h_flex() diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index c076d113b8946c8bc9d85dd89672f3417f4bc15a..43cd0f5e8937d860ce0f453d40ece8d230f7d16d 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -1,5 +1,4 @@ mod agent_notification; -mod animated_label; mod burn_mode_tooltip; mod context_pill; mod onboarding_modal; @@ -7,7 +6,6 @@ pub mod preview; mod upsell; pub use agent_notification::*; -pub use animated_label::*; pub use burn_mode_tooltip::*; pub use context_pill::*; pub use onboarding_modal::*; diff --git a/crates/ui/src/components/label.rs b/crates/ui/src/components/label.rs index bda97be6490d20309941e9de980f4a0fb2350bb1..8c9ea6242472ca3fa9bde88f0dc1e4271063f933 100644 --- a/crates/ui/src/components/label.rs +++ b/crates/ui/src/components/label.rs @@ -1,7 +1,9 @@ mod highlighted_label; mod label; mod label_like; +mod loading_label; pub use highlighted_label::*; pub use label::*; pub use label_like::*; +pub use loading_label::*; diff --git a/crates/agent_ui/src/ui/animated_label.rs b/crates/ui/src/components/label/loading_label.rs similarity index 94% rename from crates/agent_ui/src/ui/animated_label.rs rename to crates/ui/src/components/label/loading_label.rs index c2b4107730da59033cb81fc6ebf5117a9526b825..2a1e7059794d2ebd61399e5f7bdb85a8a8ac28b3 100644 --- a/crates/agent_ui/src/ui/animated_label.rs +++ b/crates/ui/src/components/label/loading_label.rs @@ -1,24 +1,24 @@ +use crate::prelude::*; use gpui::{Animation, AnimationExt, FontWeight, pulsating_between}; use std::time::Duration; -use ui::prelude::*; #[derive(IntoElement)] -pub struct AnimatedLabel { +pub struct LoadingLabel { base: Label, text: SharedString, } -impl AnimatedLabel { +impl LoadingLabel { pub fn new(text: impl Into) -> Self { let text = text.into(); - AnimatedLabel { + LoadingLabel { base: Label::new(text.clone()), text, } } } -impl LabelCommon for AnimatedLabel { +impl LabelCommon for LoadingLabel { fn size(mut self, size: LabelSize) -> Self { self.base = self.base.size(size); self @@ -80,14 +80,14 @@ impl LabelCommon for AnimatedLabel { } } -impl RenderOnce for AnimatedLabel { +impl RenderOnce for LoadingLabel { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { let text = self.text.clone(); self.base .color(Color::Muted) .with_animations( - "animated-label", + "loading_label", vec![ Animation::new(Duration::from_secs(1)), Animation::new(Duration::from_secs(1)).repeat(), diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index 5152751b7bebcd22cbd9a68b04c106e03a401685..80f8f863f8e60d7f2ab65b1680bc0491729c28be 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -25,7 +25,7 @@ pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton}; pub use crate::{ButtonCommon, Color}; pub use crate::{Headline, HeadlineSize}; pub use crate::{Icon, IconName, IconPosition, IconSize}; -pub use crate::{Label, LabelCommon, LabelSize, LineHeightStyle}; +pub use crate::{Label, LabelCommon, LabelSize, LineHeightStyle, LoadingLabel}; pub use crate::{h_container, h_flex, v_container, v_flex}; pub use crate::{ h_group, h_group_lg, h_group_sm, h_group_xl, v_group, v_group_lg, v_group_sm, v_group_xl, From 8ebea17a9c5b091e6182e81faa7a562dff3c7fb0 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 4 Jul 2025 14:25:08 +0200 Subject: [PATCH 035/239] debugger: Add history to console's query bar (#33914) Closes #[33457](https://github.com/zed-industries/zed/discussions/33457) Release Notes: - debugger: Added query history to the console --- .../src/session/running/console.rs | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index fca8df14cdb3bd84a1b8946df883acb429085d6a..b75586020b7c2b10d96f11ad3f97f2dd5b1f2d35 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -13,10 +13,11 @@ use gpui::{ Render, Subscription, Task, TextStyle, WeakEntity, actions, }; use language::{Buffer, CodeLabel, ToOffset}; -use menu::Confirm; +use menu::{Confirm, SelectNext, SelectPrevious}; use project::{ Completion, CompletionResponse, debugger::session::{CompletionsQuery, OutputToken, Session}, + search_history::{SearchHistory, SearchHistoryCursor}, }; use settings::Settings; use std::fmt::Write; @@ -43,6 +44,8 @@ pub struct Console { last_token: OutputToken, update_output_task: Option>, focus_handle: FocusHandle, + history: SearchHistory, + cursor: SearchHistoryCursor, } impl Console { @@ -108,6 +111,11 @@ impl Console { update_output_task: None, last_token: OutputToken(0), focus_handle, + history: SearchHistory::new( + None, + project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains, + ), + cursor: Default::default(), } } @@ -262,7 +270,8 @@ impl Console { expression }); - + self.history.add(&mut self.cursor, expression.clone()); + self.cursor.reset(); self.session.update(cx, |session, cx| { session .evaluate( @@ -282,7 +291,28 @@ impl Console { }); } - pub fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context) { + fn previous_query(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context) { + let prev = self.history.previous(&mut self.cursor); + if let Some(prev) = prev { + self.query_bar.update(cx, |editor, cx| { + editor.set_text(prev, window, cx); + }); + } + } + + fn next_query(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context) { + let next = self.history.next(&mut self.cursor); + let query = next.unwrap_or_else(|| { + self.cursor.reset(); + "" + }); + + self.query_bar.update(cx, |editor, cx| { + editor.set_text(query, window, cx); + }); + } + + fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context) { let expression = self.query_bar.update(cx, |editor, cx| { let expression = editor.text(cx); cx.defer_in(window, |editor, window, cx| { @@ -292,6 +322,8 @@ impl Console { expression }); + self.history.add(&mut self.cursor, expression.clone()); + self.cursor.reset(); self.session.update(cx, |session, cx| { session .evaluate( @@ -429,6 +461,8 @@ impl Render for Console { .when(self.is_running(cx), |this| { this.child(Divider::horizontal()).child( h_flex() + .on_action(cx.listener(Self::previous_query)) + .on_action(cx.listener(Self::next_query)) .gap_1() .bg(cx.theme().colors().editor_background) .child(self.render_query_bar(cx)) From 3d7e012e09f569e731e0a425dda5dc365e222b45 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Fri, 4 Jul 2025 17:11:38 +0200 Subject: [PATCH 036/239] gemini: Fix issue with builtin tool schemas (#33917) Closes #33894 After #33635 Gemini Integration was broken because we now produce `const` fields for enums, which are not supported. Changing this to `openapi3` fixes the issue. Release Notes: - Fixed an issue where Gemini Models would not work because of incompatible tool schemas --- crates/assistant_tools/src/schema.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/assistant_tools/src/schema.rs b/crates/assistant_tools/src/schema.rs index 888e11de4e83df853d5d1c252d30cecf84c701a2..10a8bf0acd99131d2c0a80411072f312c9a42f50 100644 --- a/crates/assistant_tools/src/schema.rs +++ b/crates/assistant_tools/src/schema.rs @@ -25,9 +25,7 @@ fn schema_to_json( fn root_schema_for(format: LanguageModelToolSchemaFormat) -> Schema { let mut generator = match format { LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(), - // TODO: Gemini docs mention using a subset of OpenAPI 3, so this may benefit from using - // `SchemaSettings::openapi3()`. - LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::draft07() + LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3() .with(|settings| { settings.meta_schema = None; settings.inline_subschemas = true; From 59cdea00c5d6f9073a17a4fc4d38c6742b55b570 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Fri, 4 Jul 2025 18:27:11 +0200 Subject: [PATCH 037/239] agent: Fix context server restart when settings unchanged (#33920) Closes #33891 Release Notes: - agent: Fix an issue where configuring an MCP server would not restart the underlying server correctly --- .../configure_context_server_modal.rs | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs index 299f3cee34b1c7635c3c0a8f46a52cc730993b01..ba0021c33ca32c50351387ab290bf33ce604b2e4 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs @@ -379,6 +379,14 @@ impl ConfigureContextServerModal { }; self.state = State::Waiting; + + let existing_server = self.context_server_store.read(cx).get_running_server(&id); + if existing_server.is_some() { + self.context_server_store.update(cx, |store, cx| { + store.stop_server(&id, cx).log_err(); + }); + } + let wait_for_context_server_task = wait_for_context_server(&self.context_server_store, id.clone(), cx); cx.spawn({ @@ -399,13 +407,21 @@ impl ConfigureContextServerModal { }) .detach(); - // When we write the settings to the file, the context server will be restarted. - workspace.update(cx, |workspace, cx| { - let fs = workspace.app_state().fs.clone(); - update_settings_file::(fs.clone(), cx, |project_settings, _| { - project_settings.context_servers.insert(id.0, settings); + let settings_changed = + ProjectSettings::get_global(cx).context_servers.get(&id.0) != Some(&settings); + + if settings_changed { + // When we write the settings to the file, the context server will be restarted. + workspace.update(cx, |workspace, cx| { + let fs = workspace.app_state().fs.clone(); + update_settings_file::(fs.clone(), cx, |project_settings, _| { + project_settings.context_servers.insert(id.0, settings); + }); }); - }); + } else if let Some(existing_server) = existing_server { + self.context_server_store + .update(cx, |store, cx| store.start_server(existing_server, cx)); + } } fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context) { From 8fecacfbaa7d388cff16b723338d64cc98fdb93c Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Fri, 4 Jul 2025 18:30:21 +0200 Subject: [PATCH 038/239] settings: Remove version keys from default settings (#33921) Follow up to #33372 Release Notes: - N/A --- assets/settings/default.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 56fd9353ccf9fdbcd1b24871f40a7bc2d234b7a2..9d858b42a8867b9968f6e6d8113e5d0c7fe357ff 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -746,8 +746,6 @@ "default_width": 380 }, "agent": { - // Version of this setting. - "version": "2", // Whether the agent is enabled. "enabled": true, /// What completion mode to start new threads in, if available. Can be 'normal' or 'burn'. @@ -1658,7 +1656,6 @@ // Different settings for specific language models. "language_models": { "anthropic": { - "version": "1", "api_url": "https://api.anthropic.com" }, "google": { @@ -1668,7 +1665,6 @@ "api_url": "http://localhost:11434" }, "openai": { - "version": "1", "api_url": "https://api.openai.com/v1" }, "open_router": { From 543a7b123ae1084076a95fa0572b95b7fe6d0864 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 4 Jul 2025 13:33:10 -0400 Subject: [PATCH 039/239] debugger: Fix errors in JavaScript DAP schema (#33884) `program` isn't required, and in fact our built-in `JavaScript debug terminal` configuration doesn't have it. Also add `node-terminal` to the list of allowed types. Co-authored-by: Michael Release Notes: - N/A --- crates/dap_adapters/src/javascript.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 23a378cdf9a2cc4779e9aed44538f04483a3dc56..d261d3b8b6e88c3b8069935caa9e3fd2b9d2d836 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -245,7 +245,7 @@ impl DebugAdapter for JsDebugAdapter { "properties": { "type": { "type": "string", - "enum": ["pwa-node", "node", "chrome", "pwa-chrome", "msedge", "pwa-msedge"], + "enum": ["pwa-node", "node", "chrome", "pwa-chrome", "msedge", "pwa-msedge", "node-terminal"], "description": "The type of debug session", "default": "pwa-node" }, @@ -379,10 +379,6 @@ impl DebugAdapter for JsDebugAdapter { } } }, - "oneOf": [ - { "required": ["program"] }, - { "required": ["url"] } - ] } ] }, From 75928f4859eb89046f54d3621cbcebef0fe6cf05 Mon Sep 17 00:00:00 2001 From: Ryan Hawkins Date: Fri, 4 Jul 2025 15:26:09 -0600 Subject: [PATCH 040/239] Sync extension debuggers to remote host (#33876) Closes #33835 Release Notes: - Fixed debugger extensions not working in remote projects. --- crates/extension/src/extension_builder.rs | 11 +++---- crates/extension/src/extension_manifest.rs | 33 +++++++++++++++++++++ crates/extension_host/src/extension_host.rs | 17 +++++++++++ crates/extension_host/src/headless_host.rs | 27 ++++++++++++++--- 4 files changed, 77 insertions(+), 11 deletions(-) diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs index 7a3897eea78fde5ad791d7766c3dca146dc3c760..621ba9250c12f8edd4ab49bbdef13bc976a239dd 100644 --- a/crates/extension/src/extension_builder.rs +++ b/crates/extension/src/extension_builder.rs @@ -1,5 +1,6 @@ use crate::{ - ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, parse_wasm_extension_version, + ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, build_debug_adapter_schema_path, + parse_wasm_extension_version, }; use anyhow::{Context as _, Result, bail}; use async_compression::futures::bufread::GzipDecoder; @@ -99,12 +100,8 @@ impl ExtensionBuilder { } for (debug_adapter_name, meta) in &mut extension_manifest.debug_adapters { - let debug_adapter_relative_schema_path = - meta.schema_path.clone().unwrap_or_else(|| { - Path::new("debug_adapter_schemas") - .join(Path::new(debug_adapter_name.as_ref()).with_extension("json")) - }); - let debug_adapter_schema_path = extension_dir.join(debug_adapter_relative_schema_path); + let debug_adapter_schema_path = + extension_dir.join(build_debug_adapter_schema_path(debug_adapter_name, meta)); let debug_adapter_schema = fs::read_to_string(&debug_adapter_schema_path) .with_context(|| { diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index 9439f0c290899d77b0989bf1d1fc21217af65c14..4e3f8a3dc214e7b6f8970c72562b85838a1660aa 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -132,6 +132,16 @@ impl ExtensionManifest { } } +pub fn build_debug_adapter_schema_path( + adapter_name: &Arc, + meta: &DebugAdapterManifestEntry, +) -> PathBuf { + meta.schema_path.clone().unwrap_or_else(|| { + Path::new("debug_adapter_schemas") + .join(Path::new(adapter_name.as_ref()).with_extension("json")) + }) +} + /// A capability for an extension. #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] #[serde(tag = "kind")] @@ -320,6 +330,29 @@ mod tests { } } + #[test] + fn test_build_adapter_schema_path_with_schema_path() { + let adapter_name = Arc::from("my_adapter"); + let entry = DebugAdapterManifestEntry { + schema_path: Some(PathBuf::from("foo/bar")), + }; + + let path = build_debug_adapter_schema_path(&adapter_name, &entry); + assert_eq!(path, PathBuf::from("foo/bar")); + } + + #[test] + fn test_build_adapter_schema_path_without_schema_path() { + let adapter_name = Arc::from("my_adapter"); + let entry = DebugAdapterManifestEntry { schema_path: None }; + + let path = build_debug_adapter_schema_path(&adapter_name, &entry); + assert_eq!( + path, + PathBuf::from("debug_adapter_schemas").join("my_adapter.json") + ); + } + #[test] fn test_allow_exact_match() { let manifest = ExtensionManifest { diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 8d3a218a03069cf79ec87799d566595e0b0dd3ce..eb6fb52eb82acec5b628aac05fd9131568a6c919 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1639,6 +1639,23 @@ impl ExtensionStore { } } + for (adapter_name, meta) in loaded_extension.manifest.debug_adapters.iter() { + let schema_path = &extension::build_debug_adapter_schema_path(adapter_name, meta); + + if fs.is_file(&src_dir.join(schema_path)).await { + match schema_path.parent() { + Some(parent) => fs.create_dir(&tmp_dir.join(parent)).await?, + None => {} + } + fs.copy_file( + &src_dir.join(schema_path), + &tmp_dir.join(schema_path), + fs::CopyOptions::default(), + ) + .await? + } + } + Ok(()) }) } diff --git a/crates/extension_host/src/headless_host.rs b/crates/extension_host/src/headless_host.rs index 31626c50d8c6a82282b1855141986358dde2710a..ad3931ce838043c7644fd3e0c3d0eb249db1dd9b 100644 --- a/crates/extension_host/src/headless_host.rs +++ b/crates/extension_host/src/headless_host.rs @@ -4,8 +4,8 @@ use anyhow::{Context as _, Result}; use client::{TypedEnvelope, proto}; use collections::{HashMap, HashSet}; use extension::{ - Extension, ExtensionHostProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy, - ExtensionManifest, + Extension, ExtensionDebugAdapterProviderProxy, ExtensionHostProxy, ExtensionLanguageProxy, + ExtensionLanguageServerProxy, ExtensionManifest, }; use fs::{Fs, RemoveOptions, RenameOptions}; use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task, WeakEntity}; @@ -169,8 +169,9 @@ impl HeadlessExtensionStore { return Ok(()); } - let wasm_extension: Arc = - Arc::new(WasmExtension::load(extension_dir, &manifest, wasm_host.clone(), &cx).await?); + let wasm_extension: Arc = Arc::new( + WasmExtension::load(extension_dir.clone(), &manifest, wasm_host.clone(), &cx).await?, + ); for (language_server_id, language_server_config) in &manifest.language_servers { for language in language_server_config.languages() { @@ -186,6 +187,24 @@ impl HeadlessExtensionStore { ); })?; } + for (debug_adapter, meta) in &manifest.debug_adapters { + let schema_path = extension::build_debug_adapter_schema_path(debug_adapter, meta); + + this.update(cx, |this, _cx| { + this.proxy.register_debug_adapter( + wasm_extension.clone(), + debug_adapter.clone(), + &extension_dir.join(schema_path), + ); + })?; + } + + for debug_adapter in manifest.debug_locators.keys() { + this.update(cx, |this, _cx| { + this.proxy + .register_debug_locator(wasm_extension.clone(), debug_adapter.clone()); + })?; + } } Ok(()) From 31ec7ef2ec75b51cb72265ec701ab9b3f5dad8c6 Mon Sep 17 00:00:00 2001 From: xdBronch <51252236+xdBronch@users.noreply.github.com> Date: Fri, 4 Jul 2025 18:26:20 -0400 Subject: [PATCH 041/239] Debugger: check for `supports_single_thread_execution_requests` in continue (#33937) i found this to break my ability to continue with an lldb fork i use Release Notes: - N/A --- crates/project/src/debugger/session.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index f04aadf2df03837077226935d699204d2292935f..bd52c0f6fa6f7c77baaa7fa052cd7499b44f0858 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -1935,12 +1935,14 @@ impl Session { } pub fn continue_thread(&mut self, thread_id: ThreadId, cx: &mut Context) { + let supports_single_thread_execution_requests = + self.capabilities.supports_single_thread_execution_requests; self.thread_states.continue_thread(thread_id); self.request( ContinueCommand { args: ContinueArguments { thread_id: thread_id.0, - single_thread: Some(true), + single_thread: supports_single_thread_execution_requests, }, }, Self::on_step_response::(thread_id), From d3da0a809ec317981645603ea85d86ed3030b287 Mon Sep 17 00:00:00 2001 From: Jacob Duba Date: Fri, 4 Jul 2025 18:50:09 -0500 Subject: [PATCH 042/239] Update documentation for tailwindcss language server now that HTML/ERB is it's own file. (#33684) Hello, Recently my tailwind auto completion broke in ERB files. I noticed that HTML/ERB is it's own file type now. It used to be ERB. This broke the previous tailwindcss ERB configuration. I made the attached change to my configuration and it works now. --- docs/src/languages/ruby.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/languages/ruby.md b/docs/src/languages/ruby.md index 67904e35f18f704197ecf7d6d607649b7133d009..f8df187e8a2cb439078b110075da950551b93f2a 100644 --- a/docs/src/languages/ruby.md +++ b/docs/src/languages/ruby.md @@ -256,7 +256,7 @@ In order to do that, you need to configure the language server so that it knows "tailwindcss-language-server": { "settings": { "includeLanguages": { - "erb": "html", + "html/erb": "html", "ruby": "html" }, "experimental": { From 0555bbd0ec1d70fca33c8d0b78fdfb1406b1aa09 Mon Sep 17 00:00:00 2001 From: Vitaly Slobodin Date: Sat, 5 Jul 2025 01:50:51 +0200 Subject: [PATCH 043/239] ruby: Document how to use `erb-formatter` for ERB files (#33872) Hi! This is a small pull request that adds a new section about configuring the `erb-formatter` for formatting ERB files. Thanks. Release Notes: - N/A --- docs/src/languages/ruby.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/src/languages/ruby.md b/docs/src/languages/ruby.md index f8df187e8a2cb439078b110075da950551b93f2a..b7856b2cd07ab15bb1b5fd402908c162a543f86d 100644 --- a/docs/src/languages/ruby.md +++ b/docs/src/languages/ruby.md @@ -379,3 +379,22 @@ The Ruby extension provides a debug adapter for debugging Ruby code. Zed's name } ] ``` + +## Formatters + +### `erb-formatter` + +To format ERB templates, you can use the `erb-formatter` formatter. This formatter uses the [`erb-formatter`](https://rubygems.org/gems/erb-formatter) gem to format ERB templates. + +```jsonc +{ + "HTML/ERB": { + "formatter": { + "external": { + "command": "erb-formatter", + "arguments": ["--stdin-filename", "{buffer_path}"], + }, + }, + }, +} +``` From 4ad47fc9cc43478d3ab02d1f27f3a1c30226d528 Mon Sep 17 00:00:00 2001 From: Vitaly Slobodin Date: Sat, 5 Jul 2025 01:51:25 +0200 Subject: [PATCH 044/239] emmet: Enable in `HTML/ERB` files (#33865) Closes [#133](https://github.com/zed-extensions/ruby/issues/133) This PR enables the Emmet LS in `HTML/ERB` files that we added in the https://github.com/zed-extensions/ruby/pull/113. Let me know if I picked the right approach here. Thanks! Release Notes: - N/A --- extensions/emmet/extension.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/emmet/extension.toml b/extensions/emmet/extension.toml index 02198a2c822fe41c46e981f2867e051787993169..99aa80a2d4330b7957e77f47f7c29b89decc1d32 100644 --- a/extensions/emmet/extension.toml +++ b/extensions/emmet/extension.toml @@ -9,12 +9,13 @@ repository = "https://github.com/zed-industries/zed" [language_servers.emmet-language-server] name = "Emmet Language Server" language = "HTML" -languages = ["HTML", "PHP", "ERB", "JavaScript", "TSX", "CSS", "HEEX", "Elixir"] +languages = ["HTML", "PHP", "ERB", "HTML/ERB", "JavaScript", "TSX", "CSS", "HEEX", "Elixir"] [language_servers.emmet-language-server.language_ids] "HTML" = "html" "PHP" = "php" "ERB" = "eruby" +"HTML/ERB" = "eruby" "JavaScript" = "javascriptreact" "TSX" = "typescriptreact" "CSS" = "css" From 69fd23e947edebfaa5704eee800d935645755759 Mon Sep 17 00:00:00 2001 From: abhimanyu maurya <47671638+Aerma7309@users.noreply.github.com> Date: Sat, 5 Jul 2025 05:24:43 +0530 Subject: [PATCH 045/239] Fix path parsing for goto syntax provided by Haskell language server (#33697) path parsing for multiline errors provided by haskell language server where not working correctly image while its being parsed correctly in vscode image Release Notes: - Fixed path parsing paths in the format of the Haskell language server --- crates/util/src/paths.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 47ea662d7de5b5d367dc854ee03c07faf02f5fca..2e02f051d15fc8a20d3181b3a37cbad0b9745651 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -170,6 +170,12 @@ impl> From for SanitizedPath { pub const FILE_ROW_COLUMN_DELIMITER: char = ':'; const ROW_COL_CAPTURE_REGEX: &str = r"(?xs) + ([^\(]+)\:(?: + \((\d+)[,:](\d+)\) # filename:(row,column), filename:(row:column) + | + \((\d+)\)() # filename:(row) + ) + | ([^\(]+)(?: \((\d+)[,:](\d+)\) # filename(row,column), filename(row:column) | @@ -674,6 +680,15 @@ mod tests { column: None } ); + + assert_eq!( + PathWithPosition::parse_str("Types.hs:(617,9)-(670,28):"), + PathWithPosition { + path: PathBuf::from("Types.hs"), + row: Some(617), + column: Some(9), + } + ); } #[test] From 44d1f512f87cd6cbb7f3bcd88cab92b8cbfbf4d2 Mon Sep 17 00:00:00 2001 From: Alvaro Parker <64918109+AlvaroParker@users.noreply.github.com> Date: Fri, 4 Jul 2025 20:27:21 -0400 Subject: [PATCH 046/239] Use /usr/bin/env to run bash restart script (#33936) Closes #33935 Release Notes: - Use `/usr/bin/env` to launch the bash restart script --- crates/gpui/src/platform/linux/platform.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 01a587e72a65c4466719cffe762ea90a76f1449c..af53899b437c244fd06d43b7963920c9596b94a0 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -200,8 +200,8 @@ impl Platform for P { app_path = app_path.display() ); - // execute the script using /bin/bash - let restart_process = Command::new("/bin/bash") + let restart_process = Command::new("/usr/bin/env") + .arg("bash") .arg("-c") .arg(script) .process_group(0) From 76fe33245fcff14013760255223d4b1cb92573c1 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Sat, 5 Jul 2025 05:57:37 +0530 Subject: [PATCH 047/239] project_panel: Fix indent guide collapse on secondary click for multiple worktrees (#33939) Release Notes: - Fixed issue where `cmd`/`ctrl` click on indent guide would not collapse directory in case of multiple projects. --- crates/project_panel/src/project_panel.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 614f8ccf81967941c099ada140fcd20bc6cc94c6..ded6e0e3f48f0650f97f265a1b5aed1b9b1b443a 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -3303,12 +3303,13 @@ impl ProjectPanel { fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef<'_>)> { let mut offset = 0; for (worktree_id, visible_worktree_entries, _) in &self.visible_entries { - if visible_worktree_entries.len() > offset + index { + let current_len = visible_worktree_entries.len(); + if index < offset + current_len { return visible_worktree_entries - .get(index) + .get(index - offset) .map(|entry| (*worktree_id, entry.to_ref())); } - offset += visible_worktree_entries.len(); + offset += current_len; } None } From 66e45818af4c93c64bf0707de1103cb23ecb34e6 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 5 Jul 2025 16:20:41 +0200 Subject: [PATCH 048/239] debugger: Improve debug console autocompletions (#33868) Partially fixes: https://github.com/zed-industries/zed/discussions/33777#discussioncomment-13646294 ### Improves debug console autocompletion behavior This PR fixes a regression in completion trigger support for the debug console, as we only looked if a completion trigger, was in the beginning of the search text, but we also had to check if the current text is a word so we also show completions for variables/input that doesn't start with any of the completion triggers. We now also leverage DAP provided information to sort completion items more effectively. This results in improved prioritization, showing variable completions above classes and global scope types. I also added for completion the documentation field, that directly comes from the DAP server. NOTE: I haven't found an adapter that returns this, but it needs to have. **Before** Screenshot 2025-07-03 at 21 00 19 **After** Screenshot 2025-07-03 at 20 59 38 Release Notes: - Debugger: Improve autocompletion sorting for debug console - Debugger: Fix autocompletion menu now shown when you type - Debugger: Fix completion item showing up twice for some adapters --- .../src/session/running/console.rs | 88 ++++++++++++++----- crates/editor/src/code_context_menus.rs | 9 +- crates/project/src/lsp_store.rs | 17 +++- crates/project/src/project.rs | 4 + crates/proto/proto/lsp.proto | 2 + 5 files changed, 93 insertions(+), 27 deletions(-) diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index b75586020b7c2b10d96f11ad3f97f2dd5b1f2d35..9375c8820b0eb335f1d36534f219f339ec587df1 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -5,7 +5,7 @@ use super::{ use alacritty_terminal::vte::ansi; use anyhow::Result; use collections::HashMap; -use dap::OutputEvent; +use dap::{CompletionItem, CompletionItemType, OutputEvent}; use editor::{Bias, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId}; use fuzzy::StringMatchCandidate; use gpui::{ @@ -17,6 +17,7 @@ use menu::{Confirm, SelectNext, SelectPrevious}; use project::{ Completion, CompletionResponse, debugger::session::{CompletionsQuery, OutputToken, Session}, + lsp_store::CompletionDocumentation, search_history::{SearchHistory, SearchHistoryCursor}, }; use settings::Settings; @@ -555,15 +556,27 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider { buffer: &Entity, position: language::Anchor, text: &str, - _trigger_in_words: bool, + trigger_in_words: bool, menu_is_open: bool, cx: &mut Context, ) -> bool { + let mut chars = text.chars(); + let char = if let Some(char) = chars.next() { + char + } else { + return false; + }; + let snapshot = buffer.read(cx).snapshot(); if !menu_is_open && !snapshot.settings_at(position, cx).show_completions_on_input { return false; } + let classifier = snapshot.char_classifier_at(position).for_completion(true); + if trigger_in_words && classifier.is_word(char) { + return true; + } + self.0 .read_with(cx, |console, cx| { console @@ -596,21 +609,28 @@ impl ConsoleQueryBarCompletionProvider { variable_list.completion_variables(cx) }) { if let Some(evaluate_name) = &variable.evaluate_name { - variables.insert(evaluate_name.clone(), variable.value.clone()); + if variables + .insert(evaluate_name.clone(), variable.value.clone()) + .is_none() + { + string_matches.push(StringMatchCandidate { + id: 0, + string: evaluate_name.clone(), + char_bag: evaluate_name.chars().collect(), + }); + } + } + + if variables + .insert(variable.name.clone(), variable.value.clone()) + .is_none() + { string_matches.push(StringMatchCandidate { id: 0, - string: evaluate_name.clone(), - char_bag: evaluate_name.chars().collect(), + string: variable.name.clone(), + char_bag: variable.name.chars().collect(), }); } - - variables.insert(variable.name.clone(), variable.value.clone()); - - string_matches.push(StringMatchCandidate { - id: 0, - string: variable.name.clone(), - char_bag: variable.name.chars().collect(), - }); } (variables, string_matches) @@ -656,11 +676,13 @@ impl ConsoleQueryBarCompletionProvider { new_text: string_match.string.clone(), label: CodeLabel { filter_range: 0..string_match.string.len(), - text: format!("{} {}", string_match.string, variable_value), + text: string_match.string.clone(), runs: Vec::new(), }, icon_path: None, - documentation: None, + documentation: Some(CompletionDocumentation::MultiLineMarkdown( + variable_value.into(), + )), confirm: None, source: project::CompletionSource::Custom, insert_text_mode: None, @@ -675,6 +697,32 @@ impl ConsoleQueryBarCompletionProvider { }) } + const fn completion_type_score(completion_type: CompletionItemType) -> usize { + match completion_type { + CompletionItemType::Field | CompletionItemType::Property => 0, + CompletionItemType::Variable | CompletionItemType::Value => 1, + CompletionItemType::Method + | CompletionItemType::Function + | CompletionItemType::Constructor => 2, + CompletionItemType::Class + | CompletionItemType::Interface + | CompletionItemType::Module => 3, + _ => 4, + } + } + + fn completion_item_sort_text(completion_item: &CompletionItem) -> String { + completion_item.sort_text.clone().unwrap_or_else(|| { + format!( + "{:03}_{}", + Self::completion_type_score( + completion_item.type_.unwrap_or(CompletionItemType::Text) + ), + completion_item.label.to_ascii_lowercase() + ) + }) + } + fn client_completions( &self, console: &Entity, @@ -699,6 +747,7 @@ impl ConsoleQueryBarCompletionProvider { let completions = completions .into_iter() .map(|completion| { + let sort_text = Self::completion_item_sort_text(&completion); let new_text = completion .text .as_ref() @@ -731,12 +780,11 @@ impl ConsoleQueryBarCompletionProvider { runs: Vec::new(), }, icon_path: None, - documentation: None, + documentation: completion.detail.map(|detail| { + CompletionDocumentation::MultiLineMarkdown(detail.into()) + }), confirm: None, - source: project::CompletionSource::BufferWord { - word_range: buffer_position..language::Anchor::MAX, - resolved: false, - }, + source: project::CompletionSource::Dap { sort_text }, insert_text_mode: None, } }) diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 291c03422def426054457c04ab8c9e4e710112a7..8fbae8d6052d89299b10f3cd0c971af79abd3c90 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -1083,11 +1083,10 @@ impl CompletionsMenu { if lsp_completion.kind == Some(CompletionItemKind::SNIPPET) ); - let sort_text = if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source - { - lsp_completion.sort_text.as_deref() - } else { - None + let sort_text = match &completion.source { + CompletionSource::Lsp { lsp_completion, .. } => lsp_completion.sort_text.as_deref(), + CompletionSource::Dap { sort_text } => Some(sort_text.as_str()), + _ => None, }; let (sort_kind, sort_label) = completion.sort_key(); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index f2b04b9b210712107a2836c8445160a5c31bb5b0..8a14e02e0b40946ed8e81b72e6cea5eb2a6c56ef 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -6043,7 +6043,9 @@ impl LspStore { ); server.request::(*lsp_completion.clone()) } - CompletionSource::BufferWord { .. } | CompletionSource::Custom => { + CompletionSource::BufferWord { .. } + | CompletionSource::Dap { .. } + | CompletionSource::Custom => { return Ok(()); } } @@ -6195,7 +6197,9 @@ impl LspStore { } serde_json::to_string(lsp_completion).unwrap().into_bytes() } - CompletionSource::Custom | CompletionSource::BufferWord { .. } => { + CompletionSource::Custom + | CompletionSource::Dap { .. } + | CompletionSource::BufferWord { .. } => { return Ok(()); } } @@ -11081,6 +11085,10 @@ impl LspStore { serialized_completion.source = proto::completion::Source::Custom as i32; serialized_completion.resolved = true; } + CompletionSource::Dap { sort_text } => { + serialized_completion.source = proto::completion::Source::Dap as i32; + serialized_completion.sort_text = Some(sort_text.clone()); + } } serialized_completion @@ -11135,6 +11143,11 @@ impl LspStore { resolved: completion.resolved, } } + Some(proto::completion::Source::Dap) => CompletionSource::Dap { + sort_text: completion + .sort_text + .context("expected sort text to exist")?, + }, _ => anyhow::bail!("Unexpected completion source {}", completion.source), }, }) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8a41a75d682e918a4ae698b6fe13ea8c2f9b9816..c7a1f057615c0e75414389935dbbabab9bc7155d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -456,6 +456,10 @@ pub enum CompletionSource { /// Whether this completion has been resolved, to ensure it happens once per completion. resolved: bool, }, + Dap { + /// The sort text for this completion. + sort_text: String, + }, Custom, BufferWord { word_range: Range, diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index c0eadd5e699f4b89fff5c84169b928598bc706ea..e3c2f69c0b7587580a393b343eff1c4cd932fd72 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -222,11 +222,13 @@ message Completion { optional Anchor buffer_word_end = 10; Anchor old_insert_start = 11; Anchor old_insert_end = 12; + optional string sort_text = 13; enum Source { Lsp = 0; Custom = 1; BufferWord = 2; + Dap = 3; } } From 01295aa687d726343e3b58ac4f0b3ae3b0d123eb Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sun, 6 Jul 2025 01:48:55 +0200 Subject: [PATCH 049/239] debugger: Fix the JavaScript debug terminal scenario (#33924) There were a couple of things preventing this from working: - our hack to stop the node REPL from appearing broke in recent versions of the JS DAP that started passing `--experimental-network-inspection` by default - we had lost the ability to create a debug terminal without specifying a program This PR fixes those issues. We also fixed environment variables from the **runInTerminal** request not getting passed to the spawned program. Release Notes: - Debugger: Fix RunInTerminal not working for JavaScript debugger. --------- Co-authored-by: Cole Miller --- crates/assistant_tools/src/terminal_tool.rs | 2 +- crates/dap_adapters/src/javascript.rs | 13 ++- crates/debugger_ui/src/session/running.rs | 87 +++++++++++-------- crates/extension_host/src/wasm_host/wit.rs | 2 +- .../src/wasm_host/wit/since_v0_6_0.rs | 12 +-- crates/project/src/debugger/locators/cargo.rs | 2 +- crates/project/src/project_tests.rs | 2 +- crates/project/src/terminals.rs | 19 ++-- crates/proto/proto/debugger.proto | 2 +- crates/task/src/shell_builder.rs | 26 +++--- crates/task/src/task.rs | 2 +- crates/task/src/task_template.rs | 8 +- crates/terminal_view/src/terminal_panel.rs | 2 +- crates/vim/src/command.rs | 2 +- 14 files changed, 108 insertions(+), 73 deletions(-) diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index 2c582a531069eb9a81340af7eb07731e8df8a96e..9a3eac907cbdd6848df32eeed9481058bc368840 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -218,7 +218,7 @@ impl Tool for TerminalTool { .update(cx, |project, cx| { project.create_terminal( TerminalKind::Task(task::SpawnInTerminal { - command: program, + command: Some(program), args, cwd, env, diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index d261d3b8b6e88c3b8069935caa9e3fd2b9d2d836..76c1d1fb7bb3b2b3a534293957b43919a079a888 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -1,9 +1,10 @@ use adapters::latest_github_release; use anyhow::Context as _; +use collections::HashMap; use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; use gpui::AsyncApp; use serde_json::Value; -use std::{collections::HashMap, path::PathBuf, sync::OnceLock}; +use std::{path::PathBuf, sync::OnceLock}; use task::DebugRequest; use util::{ResultExt, maybe}; @@ -70,6 +71,8 @@ impl JsDebugAdapter { let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default(); let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?; + let mut envs = HashMap::default(); + let mut configuration = task_definition.config.clone(); if let Some(configuration) = configuration.as_object_mut() { maybe!({ @@ -110,6 +113,12 @@ impl JsDebugAdapter { } } + if let Some(env) = configuration.get("env").cloned() { + if let Ok(env) = serde_json::from_value(env) { + envs = env; + } + } + configuration .entry("cwd") .or_insert(delegate.worktree_root_path().to_string_lossy().into()); @@ -158,7 +167,7 @@ impl JsDebugAdapter { ), arguments, cwd: Some(delegate.worktree_root_path().to_path_buf()), - envs: HashMap::default(), + envs, connection: Some(adapters::TcpArguments { host, port, diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index b9f373daa4b6afc96e63817d64b686840a2d0738..af8c14aef77d0886071dfd899d8de5adff0d3ed6 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -973,7 +973,7 @@ impl RunningState { let task_with_shell = SpawnInTerminal { command_label, - command, + command: Some(command), args, ..task.resolved.clone() }; @@ -1085,19 +1085,6 @@ impl RunningState { .map(PathBuf::from) .or_else(|| session.binary().unwrap().cwd.clone()); - let mut args = request.args.clone(); - - // Handle special case for NodeJS debug adapter - // If only the Node binary path is provided, we set the command to None - // This prevents the NodeJS REPL from appearing, which is not the desired behavior - // The expected usage is for users to provide their own Node command, e.g., `node test.js` - // This allows the NodeJS debug client to attach correctly - let command = if args.len() > 1 { - Some(args.remove(0)) - } else { - None - }; - let mut envs: HashMap = self.session.read(cx).task_context().project_env.clone(); if let Some(Value::Object(env)) = &request.env { @@ -1111,32 +1098,58 @@ impl RunningState { } } - let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone(); - let kind = if let Some(command) = command { - let title = request.title.clone().unwrap_or(command.clone()); - TerminalKind::Task(task::SpawnInTerminal { - id: task::TaskId("debug".to_string()), - full_label: title.clone(), - label: title.clone(), - command: command.clone(), - args, - command_label: title.clone(), - cwd, - env: envs, - use_new_terminal: true, - allow_concurrent_runs: true, - reveal: task::RevealStrategy::NoFocus, - reveal_target: task::RevealTarget::Dock, - hide: task::HideStrategy::Never, - shell, - show_summary: false, - show_command: false, - show_rerun: false, - }) + let mut args = request.args.clone(); + let command = if envs.contains_key("VSCODE_INSPECTOR_OPTIONS") { + // Handle special case for NodeJS debug adapter + // If the Node binary path is provided (possibly with arguments like --experimental-network-inspection), + // we set the command to None + // This prevents the NodeJS REPL from appearing, which is not the desired behavior + // The expected usage is for users to provide their own Node command, e.g., `node test.js` + // This allows the NodeJS debug client to attach correctly + if args + .iter() + .filter(|arg| !arg.starts_with("--")) + .collect::>() + .len() + > 1 + { + Some(args.remove(0)) + } else { + None + } + } else if args.len() > 0 { + Some(args.remove(0)) } else { - TerminalKind::Shell(cwd.map(|c| c.to_path_buf())) + None }; + let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone(); + let title = request + .title + .clone() + .filter(|title| !title.is_empty()) + .or_else(|| command.clone()) + .unwrap_or_else(|| "Debug terminal".to_string()); + let kind = TerminalKind::Task(task::SpawnInTerminal { + id: task::TaskId("debug".to_string()), + full_label: title.clone(), + label: title.clone(), + command: command.clone(), + args, + command_label: title.clone(), + cwd, + env: envs, + use_new_terminal: true, + allow_concurrent_runs: true, + reveal: task::RevealStrategy::NoFocus, + reveal_target: task::RevealTarget::Dock, + hide: task::HideStrategy::Never, + shell, + show_summary: false, + show_command: false, + show_rerun: false, + }); + let workspace = self.workspace.clone(); let weak_project = project.downgrade(); diff --git a/crates/extension_host/src/wasm_host/wit.rs b/crates/extension_host/src/wasm_host/wit.rs index b2b7726a1566dd202f25f52c3ceb8023ff216371..1f1fa49bd535ad19f4981eeed9fcdca1ba9421a9 100644 --- a/crates/extension_host/src/wasm_host/wit.rs +++ b/crates/extension_host/src/wasm_host/wit.rs @@ -999,7 +999,7 @@ impl Extension { ) -> Result> { match self { Extension::V0_6_0(ext) => { - let build_config_template = resolved_build_task.into(); + let build_config_template = resolved_build_task.try_into()?; let dap_request = ext .call_run_dap_locator(store, &locator_name, &build_config_template) .await? diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs index f8f9ae1977687296790a562711c286e2fce026e4..ced2ea9c677022e95f106ac6ba0543303fe5a372 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs @@ -299,15 +299,17 @@ impl From for DebugScenario { } } -impl From for ResolvedTask { - fn from(value: SpawnInTerminal) -> Self { - Self { +impl TryFrom for ResolvedTask { + type Error = anyhow::Error; + + fn try_from(value: SpawnInTerminal) -> Result { + Ok(Self { label: value.label, - command: value.command, + command: value.command.context("missing command")?, args: value.args, env: value.env.into_iter().collect(), cwd: value.cwd.map(|s| s.to_string_lossy().into_owned()), - } + }) } } diff --git a/crates/project/src/debugger/locators/cargo.rs b/crates/project/src/debugger/locators/cargo.rs index bad7dfe9f8947810346c06493d47d5f0b4c89c22..7d70371380192c99e1ace9676b02088f86ed9e5f 100644 --- a/crates/project/src/debugger/locators/cargo.rs +++ b/crates/project/src/debugger/locators/cargo.rs @@ -119,7 +119,7 @@ impl DapLocator for CargoLocator { .context("Couldn't get cwd from debug config which is needed for locators")?; let builder = ShellBuilder::new(true, &build_config.shell).non_interactive(); let (program, args) = builder.build( - "cargo".into(), + Some("cargo".into()), &build_config .args .iter() diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index b5eb62c4eb75e2165deaab9044d7b96f4c5e1f57..779cf95add9ad5547e13d85d87c0dcc3935ab326 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -568,7 +568,7 @@ async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) { .into_iter() .map(|(source_kind, task)| { let resolved = task.resolved; - (source_kind, resolved.command) + (source_kind, resolved.command.unwrap()) }) .collect::>(), vec![( diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index b4e1093293b6275b9da68075425dd3b75b5bb335..b067396881d3b1bc0c20d8b0f21cb5ea80b675f9 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -149,7 +149,7 @@ impl Project { let settings = self.terminal_settings(&path, cx).clone(); let builder = ShellBuilder::new(ssh_details.is_none(), &settings.shell).non_interactive(); - let (command, args) = builder.build(command, &Vec::new()); + let (command, args) = builder.build(Some(command), &Vec::new()); let mut env = self .environment @@ -297,7 +297,10 @@ impl Project { .or_insert_with(|| "xterm-256color".to_string()); let (program, args) = wrap_for_ssh( &ssh_command, - Some((&spawn_task.command, &spawn_task.args)), + spawn_task + .command + .as_ref() + .map(|command| (command, &spawn_task.args)), path.as_deref(), env, python_venv_directory.as_deref(), @@ -317,14 +320,16 @@ impl Project { add_environment_path(&mut env, &venv_path.join("bin")).log_err(); } - ( - task_state, + let shell = if let Some(program) = spawn_task.command { Shell::WithArguments { - program: spawn_task.command, + program, args: spawn_task.args, title_override: None, - }, - ) + } + } else { + Shell::System + }; + (task_state, shell) } } } diff --git a/crates/proto/proto/debugger.proto b/crates/proto/proto/debugger.proto index 3979265accaa07040373174a4e7984d181a1da33..09abd4bf1c1aa73e89d77c55ade1bce21f0027d4 100644 --- a/crates/proto/proto/debugger.proto +++ b/crates/proto/proto/debugger.proto @@ -535,7 +535,7 @@ message DebugScenario { message SpawnInTerminal { string label = 1; - string command = 2; + optional string command = 2; repeated string args = 3; map env = 4; optional string cwd = 5; diff --git a/crates/task/src/shell_builder.rs b/crates/task/src/shell_builder.rs index c75aa059f0e4571093ce97fec31860af3c8c4652..544663713933dd967f71c9330268f46688b11d93 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -149,17 +149,23 @@ 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, Vec) { - let combined_command = task_args - .into_iter() - .fold(task_command, |mut command, arg| { - command.push(' '); - command.push_str(&self.kind.to_shell_variable(arg)); - command - }); + pub fn build( + mut self, + task_command: Option, + task_args: &Vec, + ) -> (String, Vec) { + if let Some(task_command) = task_command { + let combined_command = task_args + .into_iter() + .fold(task_command, |mut command, arg| { + command.push(' '); + command.push_str(&self.kind.to_shell_variable(arg)); + command + }); - self.args - .extend(self.kind.args_for_shell(self.interactive, combined_command)); + self.args + .extend(self.kind.args_for_shell(self.interactive, combined_command)); + } (self.program, self.args) } diff --git a/crates/task/src/task.rs b/crates/task/src/task.rs index bbae4850c16d5e7b7caacde245dc36429c18ed8e..aae28ab874544f683bf48f873d4a9a80a529a32b 100644 --- a/crates/task/src/task.rs +++ b/crates/task/src/task.rs @@ -46,7 +46,7 @@ pub struct SpawnInTerminal { /// Human readable name of the terminal tab. pub label: String, /// Executable command to spawn. - pub command: String, + pub command: Option, /// Arguments to the command, potentially unsubstituted, /// to let the shell that spawns the command to do the substitution, if needed. pub args: Vec, diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index cc36b28e4beebc230cd635b894c5288cfcd3ada4..ae5054ac556b4ad82f5c9243005592593b033006 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -255,7 +255,7 @@ impl TaskTemplate { command_label }, ), - command, + command: Some(command), args: self.args.clone(), env, use_new_terminal: self.use_new_terminal, @@ -635,7 +635,7 @@ mod tests { "Human-readable label should have long substitutions trimmed" ); assert_eq!( - spawn_in_terminal.command, + spawn_in_terminal.command.clone().unwrap(), format!("echo test_file {long_value}"), "Command should be substituted with variables and those should not be shortened" ); @@ -652,7 +652,7 @@ mod tests { spawn_in_terminal.command_label, format!( "{} arg1 test_selected_text arg2 5678 arg3 {long_value}", - spawn_in_terminal.command + spawn_in_terminal.command.clone().unwrap() ), "Command label args should be substituted with variables and those should not be shortened" ); @@ -711,7 +711,7 @@ mod tests { assert_substituted_variables(&resolved_task, Vec::new()); let resolved = resolved_task.resolved; assert_eq!(resolved.label, task.label); - assert_eq!(resolved.command, task.command); + assert_eq!(resolved.command, Some(task.command)); assert_eq!(resolved.args, task.args); } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 8c55fed2a60127db4dd6fd0845f219507a8f4f78..f6eee3065ca974449315ab2ac519de1acb5da11e 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -505,7 +505,7 @@ impl TerminalPanel { let task = SpawnInTerminal { command_label, - command, + command: Some(command), args, ..task.clone() }; diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 729e1a7b3c008c957f3f018f79bdcccf78a8b698..b24ca75e8bc1f922a86b011c9dcfc27a92b57e47 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1688,7 +1688,7 @@ impl ShellExec { id: TaskId("vim".to_string()), full_label: command.clone(), label: command.clone(), - command: command.clone(), + command: Some(command.clone()), args: Vec::new(), command_label: command.clone(), cwd, From 2246b01c4b6504966542bf92a49805fd6bf46772 Mon Sep 17 00:00:00 2001 From: feeiyu <158308373+feeiyu@users.noreply.github.com> Date: Sun, 6 Jul 2025 20:52:16 +0800 Subject: [PATCH 050/239] Allow remote loading for DAP-only extensions (#33981) Closes #ISSUE ![image](https://github.com/user-attachments/assets/8e1766e4-5d89-4263-875d-ad0dff5c55c2) Release Notes: - Allow remote loading for DAP-only extensions --- .../src/extension_locator_adapter.rs | 6 ++- crates/extension/src/extension_manifest.rs | 6 +++ crates/extension_host/src/extension_host.rs | 2 +- crates/extension_host/src/headless_host.rs | 40 ++++++++++--------- 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/crates/debug_adapter_extension/src/extension_locator_adapter.rs b/crates/debug_adapter_extension/src/extension_locator_adapter.rs index 54c03b1eafa1cda8495c29f419f1588c570d78c3..55094ea7de02385ad3a5a75ea8ac0042c50a8600 100644 --- a/crates/debug_adapter_extension/src/extension_locator_adapter.rs +++ b/crates/debug_adapter_extension/src/extension_locator_adapter.rs @@ -44,7 +44,9 @@ impl DapLocator for ExtensionLocatorAdapter { .flatten() } - async fn run(&self, _build_config: SpawnInTerminal) -> Result { - Err(anyhow::anyhow!("Not implemented")) + async fn run(&self, build_config: SpawnInTerminal) -> Result { + self.extension + .run_dap_locator(self.locator_name.as_ref().to_owned(), build_config) + .await } } diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index 4e3f8a3dc214e7b6f8970c72562b85838a1660aa..0a14923c0c1a4ccfb153d9fa7f602d36805799fe 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -130,6 +130,12 @@ impl ExtensionManifest { Ok(()) } + + pub fn allow_remote_load(&self) -> bool { + !self.language_servers.is_empty() + || !self.debug_adapters.is_empty() + || !self.debug_locators.is_empty() + } } pub fn build_debug_adapter_schema_path( diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index eb6fb52eb82acec5b628aac05fd9131568a6c919..7c58fac1e0d363a4536fd0c7ea0035609c90330e 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1670,7 +1670,7 @@ impl ExtensionStore { .extensions .iter() .filter_map(|(id, entry)| { - if entry.manifest.language_servers.is_empty() { + if !entry.manifest.allow_remote_load() { return None; } Some(proto::Extension { diff --git a/crates/extension_host/src/headless_host.rs b/crates/extension_host/src/headless_host.rs index ad3931ce838043c7644fd3e0c3d0eb249db1dd9b..8feaec89de5c0c607bffe87c3be9b4700169e190 100644 --- a/crates/extension_host/src/headless_host.rs +++ b/crates/extension_host/src/headless_host.rs @@ -125,7 +125,7 @@ impl HeadlessExtensionStore { let manifest = Arc::new(ExtensionManifest::load(fs.clone(), &extension_dir).await?); - debug_assert!(!manifest.languages.is_empty() || !manifest.language_servers.is_empty()); + debug_assert!(!manifest.languages.is_empty() || manifest.allow_remote_load()); if manifest.version.as_ref() != extension.version.as_str() { anyhow::bail!( @@ -165,7 +165,7 @@ impl HeadlessExtensionStore { })?; } - if manifest.language_servers.is_empty() { + if !manifest.allow_remote_load() { return Ok(()); } @@ -187,24 +187,28 @@ impl HeadlessExtensionStore { ); })?; } - for (debug_adapter, meta) in &manifest.debug_adapters { - let schema_path = extension::build_debug_adapter_schema_path(debug_adapter, meta); + log::info!("Loaded language server: {}", language_server_id); + } - this.update(cx, |this, _cx| { - this.proxy.register_debug_adapter( - wasm_extension.clone(), - debug_adapter.clone(), - &extension_dir.join(schema_path), - ); - })?; - } + for (debug_adapter, meta) in &manifest.debug_adapters { + let schema_path = extension::build_debug_adapter_schema_path(debug_adapter, meta); - for debug_adapter in manifest.debug_locators.keys() { - this.update(cx, |this, _cx| { - this.proxy - .register_debug_locator(wasm_extension.clone(), debug_adapter.clone()); - })?; - } + this.update(cx, |this, _cx| { + this.proxy.register_debug_adapter( + wasm_extension.clone(), + debug_adapter.clone(), + &extension_dir.join(schema_path), + ); + })?; + log::info!("Loaded debug adapter: {}", debug_adapter); + } + + for debug_locator in manifest.debug_locators.keys() { + this.update(cx, |this, _cx| { + this.proxy + .register_debug_locator(wasm_extension.clone(), debug_locator.clone()); + })?; + log::info!("Loaded debug locator: {}", debug_locator); } Ok(()) From 6efc5ecefe98487b7ede1f35536b7710b831c251 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Mon, 7 Jul 2025 08:32:42 +0530 Subject: [PATCH 051/239] project_panel: Add Sticky Scroll (#33994) Closes #7243 - Adds `top_slot_items` to `uniform_list` component to offset list items. - Adds `ToPosition` scroll strategy to `uniform_list` to scroll list to specified index. - Adds `sticky_items` component which can be used along with `uniform_list` to add sticky functionality to any view that implements uniform list. https://github.com/user-attachments/assets/eb508fa4-167e-4595-911b-52651537284c Release Notes: - Added sticky scroll to the project panel, which keeps parent directories visible while scrolling. This feature is enabled by default. To disable it, toggle `sticky_scroll` in settings. --- assets/settings/default.json | 2 + crates/gpui/src/elements/uniform_list.rs | 83 +- crates/project_panel/src/project_panel.rs | 779 +++++++++++------- .../src/project_panel_settings.rs | 5 + crates/ui/src/components.rs | 2 + crates/ui/src/components/sticky_items.rs | 150 ++++ 6 files changed, 738 insertions(+), 283 deletions(-) create mode 100644 crates/ui/src/components/sticky_items.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index 9d858b42a8867b9968f6e6d8113e5d0c7fe357ff..985e322cac2a2c4b6b807aeff24caeb68beacf89 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -617,6 +617,8 @@ // 3. Mark files with errors and warnings: // "all" "show_diagnostics": "all", + // Whether to stick parent directories at top of the project panel. + "sticky_scroll": true, // Settings related to indent guides in the project panel. "indent_guides": { // When to show indent guides in the project panel. diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index c85f71eae8954e8afd72812e313bfd1ebfbea2c1..f32ecfc20cb0d1a488705f9e48e596f9a05ef98c 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -7,8 +7,8 @@ use crate::{ AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId, - ListSizingBehavior, Overflow, Pixels, ScrollHandle, Size, StyleRefinement, Styled, Window, - point, size, + ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size, StyleRefinement, Styled, + Window, point, size, }; use smallvec::SmallVec; use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; @@ -42,6 +42,7 @@ where item_count, item_to_measure_index: 0, render_items: Box::new(render_range), + top_slot: None, decorations: Vec::new(), interactivity: Interactivity { element_id: Some(id), @@ -61,6 +62,7 @@ pub struct UniformList { render_items: Box< dyn for<'a> Fn(Range, &'a mut Window, &'a mut App) -> SmallVec<[AnyElement; 64]>, >, + top_slot: Option>, decorations: Vec>, interactivity: Interactivity, scroll_handle: Option, @@ -71,6 +73,7 @@ pub struct UniformList { /// Frame state used by the [UniformList]. pub struct UniformListFrameState { items: SmallVec<[AnyElement; 32]>, + top_slot_items: SmallVec<[AnyElement; 8]>, decorations: SmallVec<[AnyElement; 1]>, } @@ -88,6 +91,8 @@ pub enum ScrollStrategy { /// May not be possible if there's not enough list items above the item scrolled to: /// in this case, the element will be placed at the closest possible position. Center, + /// Scrolls the element to be at the given item index from the top of the viewport. + ToPosition(usize), } #[derive(Clone, Debug, Default)] @@ -212,6 +217,7 @@ impl Element for UniformList { UniformListFrameState { items: SmallVec::new(), decorations: SmallVec::new(), + top_slot_items: SmallVec::new(), }, ) } @@ -345,6 +351,15 @@ impl Element for UniformList { } } } + ScrollStrategy::ToPosition(sticky_index) => { + let target_y_in_viewport = item_height * sticky_index; + let target_scroll_top = item_top - target_y_in_viewport; + let max_scroll_top = + (content_height - list_height).max(Pixels::ZERO); + let new_scroll_top = + target_scroll_top.clamp(Pixels::ZERO, max_scroll_top); + updated_scroll_offset.y = -new_scroll_top; + } } scroll_offset = *updated_scroll_offset } @@ -354,7 +369,17 @@ impl Element for UniformList { let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height) / item_height) .ceil() as usize; - let visible_range = first_visible_element_ix + let initial_range = first_visible_element_ix + ..cmp::min(last_visible_element_ix, self.item_count); + + let mut top_slot_elements = if let Some(ref mut top_slot) = self.top_slot { + top_slot.compute(initial_range, window, cx) + } else { + SmallVec::new() + }; + let top_slot_offset = top_slot_elements.len(); + + let visible_range = (top_slot_offset + first_visible_element_ix) ..cmp::min(last_visible_element_ix, self.item_count); let items = if y_flipped { @@ -393,6 +418,20 @@ impl Element for UniformList { frame_state.items.push(item); } + if let Some(ref top_slot) = self.top_slot { + top_slot.prepaint( + &mut top_slot_elements, + padded_bounds, + item_height, + scroll_offset, + padding, + can_scroll_horizontally, + window, + cx, + ); + } + frame_state.top_slot_items = top_slot_elements; + let bounds = Bounds::new( padded_bounds.origin + point( @@ -454,6 +493,9 @@ impl Element for UniformList { for decoration in &mut request_layout.decorations { decoration.paint(window, cx); } + if let Some(ref top_slot) = self.top_slot { + top_slot.paint(&mut request_layout.top_slot_items, window, cx); + } }, ) } @@ -483,6 +525,35 @@ pub trait UniformListDecoration { ) -> AnyElement; } +/// A trait for implementing top slots in a [`UniformList`]. +/// Top slots are elements that appear at the top of the list and can adjust +/// the visible range of list items. +pub trait UniformListTopSlot { + /// Returns elements to render at the top slot for the given visible range. + fn compute( + &mut self, + visible_range: Range, + window: &mut Window, + cx: &mut App, + ) -> SmallVec<[AnyElement; 8]>; + + /// Layout and prepaint the top slot elements. + fn prepaint( + &self, + elements: &mut SmallVec<[AnyElement; 8]>, + bounds: Bounds, + item_height: Pixels, + scroll_offset: Point, + padding: crate::Edges, + can_scroll_horizontally: bool, + window: &mut Window, + cx: &mut App, + ); + + /// Paint the top slot elements. + fn paint(&self, elements: &mut SmallVec<[AnyElement; 8]>, window: &mut Window, cx: &mut App); +} + impl UniformList { /// Selects a specific list item for measurement. pub fn with_width_from_item(mut self, item_index: Option) -> Self { @@ -521,6 +592,12 @@ impl UniformList { self } + /// Sets a top slot for the list. + pub fn with_top_slot(mut self, top_slot: impl UniformListTopSlot + 'static) -> Self { + self.top_slot = Some(Box::new(top_slot)); + self + } + fn measure_item( &self, list_width: Option, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index ded6e0e3f48f0650f97f265a1b5aed1b9b1b443a..ca791869d9db9a70090583f21b06b8099e9d74f1 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -56,7 +56,7 @@ use theme::ThemeSettings; use ui::{ Color, ContextMenu, DecoratedIcon, Icon, IconDecoration, IconDecorationKind, IndentGuideColors, IndentGuideLayout, KeyBinding, Label, LabelSize, ListItem, ListItemSpacing, Scrollbar, - ScrollbarState, Tooltip, prelude::*, v_flex, + ScrollbarState, StickyCandidate, Tooltip, prelude::*, v_flex, }; use util::{ResultExt, TakeUntilExt, TryFutureExt, maybe, paths::compare_paths}; use workspace::{ @@ -173,6 +173,7 @@ struct EntryDetails { is_editing: bool, is_processing: bool, is_cut: bool, + sticky: Option, filename_text_color: Color, diagnostic_severity: Option, git_status: GitSummary, @@ -181,6 +182,11 @@ struct EntryDetails { canonical_path: Option>, } +#[derive(Debug, PartialEq, Eq, Clone)] +struct StickyDetails { + sticky_index: usize, +} + /// Permanently deletes the selected file or directory. #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = project_panel)] @@ -3366,22 +3372,13 @@ impl ProjectPanel { } let end_ix = range.end.min(ix + visible_worktree_entries.len()); - let (git_status_setting, show_file_icons, show_folder_icons) = { + let git_status_setting = { let settings = ProjectPanelSettings::get_global(cx); - ( - settings.git_status, - settings.file_icons, - settings.folder_icons, - ) + settings.git_status }; if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) { let snapshot = worktree.read(cx).snapshot(); let root_name = OsStr::new(snapshot.root_name()); - let expanded_entry_ids = self - .expanded_dir_ids - .get(&snapshot.id()) - .map(Vec::as_slice) - .unwrap_or(&[]); let entry_range = range.start.saturating_sub(ix)..end_ix - ix; let entries = entries_paths.get_or_init(|| { @@ -3394,80 +3391,17 @@ impl ProjectPanel { let status = git_status_setting .then_some(entry.git_summary) .unwrap_or_default(); - let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok(); - let icon = match entry.kind { - EntryKind::File => { - if show_file_icons { - FileIcons::get_icon(&entry.path, cx) - } else { - None - } - } - _ => { - if show_folder_icons { - FileIcons::get_folder_icon(is_expanded, cx) - } else { - FileIcons::get_chevron_icon(is_expanded, cx) - } - } - }; - - let (depth, difference) = - ProjectPanel::calculate_depth_and_difference(&entry, entries); - - let filename = match difference { - diff if diff > 1 => entry - .path - .iter() - .skip(entry.path.components().count() - diff) - .collect::() - .to_str() - .unwrap_or_default() - .to_string(), - _ => entry - .path - .file_name() - .map(|name| name.to_string_lossy().into_owned()) - .unwrap_or_else(|| root_name.to_string_lossy().to_string()), - }; - let selection = SelectedEntry { - worktree_id: snapshot.id(), - entry_id: entry.id, - }; - let is_marked = self.marked_entries.contains(&selection); - - let diagnostic_severity = self - .diagnostics - .get(&(*worktree_id, entry.path.to_path_buf())) - .cloned(); - - let filename_text_color = - entry_git_aware_label_color(status, entry.is_ignored, is_marked); - - let mut details = EntryDetails { - filename, - icon, - path: entry.path.clone(), - depth, - kind: entry.kind, - is_ignored: entry.is_ignored, - is_expanded, - is_selected: self.selection == Some(selection), - is_marked, - is_editing: false, - is_processing: false, - is_cut: self - .clipboard - .as_ref() - .map_or(false, |e| e.is_cut() && e.items().contains(&selection)), - filename_text_color, - diagnostic_severity, - git_status: status, - is_private: entry.is_private, - worktree_id: *worktree_id, - canonical_path: entry.canonical_path.clone(), - }; + let mut details = self.details_for_entry( + entry, + *worktree_id, + root_name, + entries, + status, + None, + window, + cx, + ); if let Some(edit_state) = &self.edit_state { let is_edited_entry = if edit_state.is_new_entry() { @@ -3879,6 +3813,8 @@ impl ProjectPanel { const GROUP_NAME: &str = "project_entry"; let kind = details.kind; + let is_sticky = details.sticky.is_some(); + let sticky_index = details.sticky.as_ref().map(|this| this.sticky_index); let settings = ProjectPanelSettings::get_global(cx); let show_editor = details.is_editing && !details.is_processing; @@ -4002,141 +3938,144 @@ impl ProjectPanel { .border_r_2() .border_color(border_color) .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color)) - .on_drag_move::(cx.listener( - move |this, event: &DragMoveEvent, _, cx| { - let is_current_target = this.drag_target_entry.as_ref() - .map(|entry| entry.entry_id) == Some(entry_id); - - if !event.bounds.contains(&event.event.position) { - // Entry responsible for setting drag target is also responsible to - // clear it up after drag is out of bounds + .when(!is_sticky, |this| { + this + .when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over)) + .on_drag_move::(cx.listener( + move |this, event: &DragMoveEvent, _, cx| { + let is_current_target = this.drag_target_entry.as_ref() + .map(|entry| entry.entry_id) == Some(entry_id); + + if !event.bounds.contains(&event.event.position) { + // Entry responsible for setting drag target is also responsible to + // clear it up after drag is out of bounds + if is_current_target { + this.drag_target_entry = None; + } + return; + } + if is_current_target { - this.drag_target_entry = None; + return; } - return; - } - if is_current_target { - return; - } + let Some((entry_id, highlight_entry_id)) = maybe!({ + let target_worktree = this.project.read(cx).worktree_for_id(selection.worktree_id, cx)?.read(cx); + let target_entry = target_worktree.entry_for_path(&path_for_external_paths)?; + let highlight_entry_id = this.highlight_entry_for_external_drag(target_entry, target_worktree); + Some((target_entry.id, highlight_entry_id)) + }) else { + return; + }; - let Some((entry_id, highlight_entry_id)) = maybe!({ - let target_worktree = this.project.read(cx).worktree_for_id(selection.worktree_id, cx)?.read(cx); - let target_entry = target_worktree.entry_for_path(&path_for_external_paths)?; - let highlight_entry_id = this.highlight_entry_for_external_drag(target_entry, target_worktree); - Some((target_entry.id, highlight_entry_id)) - }) else { - return; - }; + this.drag_target_entry = Some(DragTargetEntry { + entry_id, + highlight_entry_id, + }); + this.marked_entries.clear(); + }, + )) + .on_drop(cx.listener( + move |this, external_paths: &ExternalPaths, window, cx| { + this.drag_target_entry = None; + this.hover_scroll_task.take(); + this.drop_external_files(external_paths.paths(), entry_id, window, cx); + cx.stop_propagation(); + }, + )) + .on_drag_move::(cx.listener( + move |this, event: &DragMoveEvent, window, cx| { + let is_current_target = this.drag_target_entry.as_ref() + .map(|entry| entry.entry_id) == Some(entry_id); + + if !event.bounds.contains(&event.event.position) { + // Entry responsible for setting drag target is also responsible to + // clear it up after drag is out of bounds + if is_current_target { + this.drag_target_entry = None; + } + return; + } - this.drag_target_entry = Some(DragTargetEntry { - entry_id, - highlight_entry_id, - }); - this.marked_entries.clear(); - }, - )) - .on_drop(cx.listener( - move |this, external_paths: &ExternalPaths, window, cx| { - this.drag_target_entry = None; - this.hover_scroll_task.take(); - this.drop_external_files(external_paths.paths(), entry_id, window, cx); - cx.stop_propagation(); - }, - )) - .on_drag_move::(cx.listener( - move |this, event: &DragMoveEvent, window, cx| { - let is_current_target = this.drag_target_entry.as_ref() - .map(|entry| entry.entry_id) == Some(entry_id); - - if !event.bounds.contains(&event.event.position) { - // Entry responsible for setting drag target is also responsible to - // clear it up after drag is out of bounds if is_current_target { - this.drag_target_entry = None; + return; } - return; - } - if is_current_target { - return; - } - - let drag_state = event.drag(cx); - let Some((entry_id, highlight_entry_id)) = maybe!({ - let target_worktree = this.project.read(cx).worktree_for_id(selection.worktree_id, cx)?.read(cx); - let target_entry = target_worktree.entry_for_path(&path_for_dragged_selection)?; - let highlight_entry_id = this.highlight_entry_for_selection_drag(target_entry, target_worktree, drag_state, cx); - Some((target_entry.id, highlight_entry_id)) - }) else { - return; - }; + let drag_state = event.drag(cx); + let Some((entry_id, highlight_entry_id)) = maybe!({ + let target_worktree = this.project.read(cx).worktree_for_id(selection.worktree_id, cx)?.read(cx); + let target_entry = target_worktree.entry_for_path(&path_for_dragged_selection)?; + let highlight_entry_id = this.highlight_entry_for_selection_drag(target_entry, target_worktree, drag_state, cx); + Some((target_entry.id, highlight_entry_id)) + }) else { + return; + }; - this.drag_target_entry = Some(DragTargetEntry { - entry_id, - highlight_entry_id, - }); - if drag_state.items().count() == 1 { - this.marked_entries.clear(); - this.marked_entries.insert(drag_state.active_selection); - } - this.hover_expand_task.take(); + this.drag_target_entry = Some(DragTargetEntry { + entry_id, + highlight_entry_id, + }); + if drag_state.items().count() == 1 { + this.marked_entries.clear(); + this.marked_entries.insert(drag_state.active_selection); + } + this.hover_expand_task.take(); - if !kind.is_dir() - || this - .expanded_dir_ids - .get(&details.worktree_id) - .map_or(false, |ids| ids.binary_search(&entry_id).is_ok()) - { - return; - } + if !kind.is_dir() + || this + .expanded_dir_ids + .get(&details.worktree_id) + .map_or(false, |ids| ids.binary_search(&entry_id).is_ok()) + { + return; + } - let bounds = event.bounds; - this.hover_expand_task = - Some(cx.spawn_in(window, async move |this, cx| { - cx.background_executor() - .timer(Duration::from_millis(500)) - .await; - this.update_in(cx, |this, window, cx| { - this.hover_expand_task.take(); - if this.drag_target_entry.as_ref().map(|entry| entry.entry_id) == Some(entry_id) - && bounds.contains(&window.mouse_position()) - { - this.expand_entry(worktree_id, entry_id, cx); - this.update_visible_entries( - Some((worktree_id, entry_id)), - cx, - ); - cx.notify(); - } - }) - .ok(); - })); - }, - )) - .on_drag( - dragged_selection, - move |selection, click_offset, _window, cx| { - cx.new(|_| DraggedProjectEntryView { - details: details.clone(), - click_offset, - selection: selection.active_selection, - selections: selection.marked_selections.clone(), - }) - }, - ) - .when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over)) - .on_drop( - cx.listener(move |this, selections: &DraggedSelection, window, cx| { - this.drag_target_entry = None; - this.hover_scroll_task.take(); - this.hover_expand_task.take(); - if folded_directory_drag_target.is_some() { - return; - } - this.drag_onto(selections, entry_id, kind.is_file(), window, cx); - }), - ) + let bounds = event.bounds; + this.hover_expand_task = + Some(cx.spawn_in(window, async move |this, cx| { + cx.background_executor() + .timer(Duration::from_millis(500)) + .await; + this.update_in(cx, |this, window, cx| { + this.hover_expand_task.take(); + if this.drag_target_entry.as_ref().map(|entry| entry.entry_id) == Some(entry_id) + && bounds.contains(&window.mouse_position()) + { + this.expand_entry(worktree_id, entry_id, cx); + this.update_visible_entries( + Some((worktree_id, entry_id)), + cx, + ); + cx.notify(); + } + }) + .ok(); + })); + }, + )) + .on_drag( + dragged_selection, + move |selection, click_offset, _window, cx| { + cx.new(|_| DraggedProjectEntryView { + details: details.clone(), + click_offset, + selection: selection.active_selection, + selections: selection.marked_selections.clone(), + }) + }, + ) + .on_drop( + cx.listener(move |this, selections: &DraggedSelection, window, cx| { + this.drag_target_entry = None; + this.hover_scroll_task.take(); + this.hover_expand_task.take(); + if folded_directory_drag_target.is_some() { + return; + } + this.drag_onto(selections, entry_id, kind.is_file(), window, cx); + }), + ) + }) .on_mouse_down( MouseButton::Left, cx.listener(move |this, _, _, cx| { @@ -4168,7 +4107,7 @@ impl ProjectPanel { current_selection.zip(target_selection) { let range_start = source_index.min(target_index); - let range_end = source_index.max(target_index) + 1; // Make the range inclusive. + let range_end = source_index.max(target_index) + 1; let mut new_selections = BTreeSet::new(); this.for_each_visible_entry( range_start..range_end, @@ -4214,6 +4153,16 @@ impl ProjectPanel { let allow_preview = preview_tabs_enabled && click_count == 1; this.open_entry(entry_id, focus_opened_item, allow_preview, cx); } + + if is_sticky { + if let Some((_, _, index)) = this.index_for_entry(entry_id, worktree_id) { + let strategy = sticky_index + .map(ScrollStrategy::ToPosition) + .unwrap_or(ScrollStrategy::Top); + this.scroll_handle.scroll_to_item(index, strategy); + cx.notify(); + } + } }), ) .child( @@ -4328,51 +4277,99 @@ impl ProjectPanel { let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - delimiter_target_index).cloned(); this = this.child( div() - .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| { - this.hover_scroll_task.take(); - this.drag_target_entry = None; - this.folded_directory_drag_target = None; - if let Some(target_entry_id) = target_entry_id { - this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx); - } - })) + .when(!is_sticky, |div| { + div + .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| { + this.hover_scroll_task.take(); + this.drag_target_entry = None; + this.folded_directory_drag_target = None; + if let Some(target_entry_id) = target_entry_id { + this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx); + } + })) + .on_drag_move(cx.listener( + move |this, event: &DragMoveEvent, _, _| { + if event.bounds.contains(&event.event.position) { + this.folded_directory_drag_target = Some( + FoldedDirectoryDragTarget { + entry_id, + index: delimiter_target_index, + is_delimiter_target: true, + } + ); + } else { + let is_current_target = this.folded_directory_drag_target + .map_or(false, |target| + target.entry_id == entry_id && + target.index == delimiter_target_index && + target.is_delimiter_target + ); + if is_current_target { + this.folded_directory_drag_target = None; + } + } + + }, + )) + }) + .child( + Label::new(DELIMITER.clone()) + .single_line() + .color(filename_text_color) + ) + ); + } + let id = SharedString::from(format!( + "project_panel_path_component_{}_{index}", + entry_id.to_usize() + )); + let label = div() + .id(id) + .when(!is_sticky,| div| { + div + .when(index != components_len - 1, |div|{ + let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned(); + div .on_drag_move(cx.listener( move |this, event: &DragMoveEvent, _, _| { - if event.bounds.contains(&event.event.position) { + if event.bounds.contains(&event.event.position) { this.folded_directory_drag_target = Some( FoldedDirectoryDragTarget { entry_id, - index: delimiter_target_index, - is_delimiter_target: true, + index, + is_delimiter_target: false, } ); } else { let is_current_target = this.folded_directory_drag_target + .as_ref() .map_or(false, |target| target.entry_id == entry_id && - target.index == delimiter_target_index && - target.is_delimiter_target + target.index == index && + !target.is_delimiter_target ); if is_current_target { this.folded_directory_drag_target = None; } } - }, )) - .child( - Label::new(DELIMITER.clone()) - .single_line() - .color(filename_text_color) - ) - ); - } - let id = SharedString::from(format!( - "project_panel_path_component_{}_{index}", - entry_id.to_usize() - )); - let label = div() - .id(id) + .on_drop(cx.listener(move |this, selections: &DraggedSelection, window,cx| { + this.hover_scroll_task.take(); + this.drag_target_entry = None; + this.folded_directory_drag_target = None; + if let Some(target_entry_id) = target_entry_id { + this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx); + } + })) + .when(folded_directory_drag_target.map_or(false, |target| + target.entry_id == entry_id && + target.index == index + ), |this| { + this.bg(item_colors.drag_over) + }) + }) + }) .on_click(cx.listener(move |this, _, _, cx| { if index != active_index { if let Some(folds) = @@ -4384,48 +4381,6 @@ impl ProjectPanel { } } })) - .when(index != components_len - 1, |div|{ - let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned(); - div - .on_drag_move(cx.listener( - move |this, event: &DragMoveEvent, _, _| { - if event.bounds.contains(&event.event.position) { - this.folded_directory_drag_target = Some( - FoldedDirectoryDragTarget { - entry_id, - index, - is_delimiter_target: false, - } - ); - } else { - let is_current_target = this.folded_directory_drag_target - .as_ref() - .map_or(false, |target| - target.entry_id == entry_id && - target.index == index && - !target.is_delimiter_target - ); - if is_current_target { - this.folded_directory_drag_target = None; - } - } - }, - )) - .on_drop(cx.listener(move |this, selections: &DraggedSelection, window,cx| { - this.hover_scroll_task.take(); - this.drag_target_entry = None; - this.folded_directory_drag_target = None; - if let Some(target_entry_id) = target_entry_id { - this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx); - } - })) - .when(folded_directory_drag_target.map_or(false, |target| - target.entry_id == entry_id && - target.index == index - ), |this| { - this.bg(item_colors.drag_over) - }) - }) .child( Label::new(component) .single_line() @@ -4497,6 +4452,108 @@ impl ProjectPanel { ) } + fn details_for_entry( + &self, + entry: &Entry, + worktree_id: WorktreeId, + root_name: &OsStr, + entries_paths: &HashSet>, + git_status: GitSummary, + sticky: Option, + _window: &mut Window, + cx: &mut Context, + ) -> EntryDetails { + let (show_file_icons, show_folder_icons) = { + let settings = ProjectPanelSettings::get_global(cx); + (settings.file_icons, settings.folder_icons) + }; + + let expanded_entry_ids = self + .expanded_dir_ids + .get(&worktree_id) + .map(Vec::as_slice) + .unwrap_or(&[]); + let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok(); + + let icon = match entry.kind { + EntryKind::File => { + if show_file_icons { + FileIcons::get_icon(&entry.path, cx) + } else { + None + } + } + _ => { + if show_folder_icons { + FileIcons::get_folder_icon(is_expanded, cx) + } else { + FileIcons::get_chevron_icon(is_expanded, cx) + } + } + }; + + let (depth, difference) = + ProjectPanel::calculate_depth_and_difference(&entry, entries_paths); + + let filename = match difference { + diff if diff > 1 => entry + .path + .iter() + .skip(entry.path.components().count() - diff) + .collect::() + .to_str() + .unwrap_or_default() + .to_string(), + _ => entry + .path + .file_name() + .map(|name| name.to_string_lossy().into_owned()) + .unwrap_or_else(|| root_name.to_string_lossy().to_string()), + }; + + let selection = SelectedEntry { + worktree_id, + entry_id: entry.id, + }; + let is_marked = self.marked_entries.contains(&selection); + let is_selected = self.selection == Some(selection); + + let diagnostic_severity = self + .diagnostics + .get(&(worktree_id, entry.path.to_path_buf())) + .cloned(); + + let filename_text_color = + entry_git_aware_label_color(git_status, entry.is_ignored, is_marked); + + let is_cut = self + .clipboard + .as_ref() + .map_or(false, |e| e.is_cut() && e.items().contains(&selection)); + + EntryDetails { + filename, + icon, + path: entry.path.clone(), + depth, + kind: entry.kind, + is_ignored: entry.is_ignored, + is_expanded, + is_selected, + is_marked, + is_editing: false, + is_processing: false, + is_cut, + sticky, + filename_text_color, + diagnostic_severity, + git_status, + is_private: entry.is_private, + worktree_id, + canonical_path: entry.canonical_path.clone(), + } + } + fn render_vertical_scrollbar(&self, cx: &mut Context) -> Option> { if !Self::should_show_scrollbar(cx) || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging()) @@ -4751,6 +4808,156 @@ impl ProjectPanel { } None } + + fn candidate_entries_in_range_for_sticky( + &self, + range: Range, + _window: &mut Window, + _cx: &mut Context, + ) -> Vec { + let mut result = Vec::new(); + let mut current_offset = 0; + + for (_, visible_worktree_entries, entries_paths) in &self.visible_entries { + let worktree_len = visible_worktree_entries.len(); + let worktree_end_offset = current_offset + worktree_len; + + if current_offset >= range.end { + break; + } + + if worktree_end_offset > range.start { + let local_start = range.start.saturating_sub(current_offset); + let local_end = range.end.saturating_sub(current_offset).min(worktree_len); + + let paths = entries_paths.get_or_init(|| { + visible_worktree_entries + .iter() + .map(|e| e.path.clone()) + .collect() + }); + + let entries_from_this_worktree = visible_worktree_entries[local_start..local_end] + .iter() + .enumerate() + .map(|(i, entry)| { + let (depth, _) = Self::calculate_depth_and_difference(&entry.entry, paths); + StickyProjectPanelCandidate { + index: current_offset + local_start + i, + depth, + } + }); + + result.extend(entries_from_this_worktree); + } + + current_offset = worktree_end_offset; + } + + result + } + + fn render_sticky_entries( + &self, + child: StickyProjectPanelCandidate, + window: &mut Window, + cx: &mut Context, + ) -> SmallVec<[AnyElement; 8]> { + let project = self.project.read(cx); + + let Some((worktree_id, entry_ref)) = self.entry_at_index(child.index) else { + return SmallVec::new(); + }; + + let Some((_, visible_worktree_entries, entries_paths)) = self + .visible_entries + .iter() + .find(|(id, _, _)| *id == worktree_id) + else { + return SmallVec::new(); + }; + + let Some(worktree) = project.worktree_for_id(worktree_id, cx) else { + return SmallVec::new(); + }; + let worktree = worktree.read(cx).snapshot(); + + let paths = entries_paths.get_or_init(|| { + visible_worktree_entries + .iter() + .map(|e| e.path.clone()) + .collect() + }); + + let mut sticky_parents = Vec::new(); + let mut current_path = entry_ref.path.clone(); + + 'outer: loop { + if let Some(parent_path) = current_path.parent() { + for ancestor_path in parent_path.ancestors() { + if paths.contains(ancestor_path) { + if let Some(parent_entry) = worktree.entry_for_path(ancestor_path) { + sticky_parents.push(parent_entry.clone()); + current_path = parent_entry.path.clone(); + continue 'outer; + } + } + } + } + break 'outer; + } + + sticky_parents.reverse(); + + let git_status_enabled = ProjectPanelSettings::get_global(cx).git_status; + let root_name = OsStr::new(worktree.root_name()); + + let git_summaries_by_id = if git_status_enabled { + visible_worktree_entries + .iter() + .map(|e| (e.id, e.git_summary)) + .collect::>() + } else { + Default::default() + }; + + sticky_parents + .iter() + .enumerate() + .map(|(index, entry)| { + let git_status = git_summaries_by_id + .get(&entry.id) + .copied() + .unwrap_or_default(); + let sticky_details = Some(StickyDetails { + sticky_index: index, + }); + let details = self.details_for_entry( + entry, + worktree_id, + root_name, + paths, + git_status, + sticky_details, + window, + cx, + ); + self.render_entry(entry.id, details, window, cx).into_any() + }) + .collect() + } +} + +#[derive(Clone)] +struct StickyProjectPanelCandidate { + index: usize, + depth: usize, +} + +impl StickyCandidate for StickyProjectPanelCandidate { + fn depth(&self) -> usize { + self.depth + } } fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize { @@ -4769,6 +4976,7 @@ impl Render for ProjectPanel { let indent_size = ProjectPanelSettings::get_global(cx).indent_size; let show_indent_guides = ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always; + let show_sticky_scroll = ProjectPanelSettings::get_global(cx).sticky_scroll; let is_local = project.is_local(); if has_worktree { @@ -4963,6 +5171,17 @@ impl Render for ProjectPanel { items }) }) + .when(show_sticky_scroll, |list| { + list.with_top_slot(ui::sticky_items( + cx.entity().clone(), + |this, range, window, cx| { + this.candidate_entries_in_range_for_sticky(range, window, cx) + }, + |this, marker_entry, window, cx| { + this.render_sticky_entries(marker_entry, window, cx) + }, + )) + }) .when(show_indent_guides, |list| { list.with_decoration( ui::indent_guides( @@ -5079,7 +5298,7 @@ impl Render for ProjectPanel { .anchor(gpui::Corner::TopLeft) .child(menu.clone()), ) - .with_priority(1) + .with_priority(3) })) } else { v_flex() diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index 31f4a21b0973c430bbddff168bafc3c40c69aa3c..9057480972a07b25ad30917a03ccf871b0bb6e3f 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -40,6 +40,7 @@ pub struct ProjectPanelSettings { pub git_status: bool, pub indent_size: f32, pub indent_guides: IndentGuidesSettings, + pub sticky_scroll: bool, pub auto_reveal_entries: bool, pub auto_fold_dirs: bool, pub scrollbar: ScrollbarSettings, @@ -150,6 +151,10 @@ pub struct ProjectPanelSettingsContent { /// /// Default: false pub hide_root: Option, + /// Whether to stick parent directories at top of the project panel. + /// + /// Default: true + pub sticky_scroll: Option, } impl Settings for ProjectPanelSettings { diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 237403d4ba053646108e88546242df1f07cdc8ab..88676e8a2bbe383538e91499a71ca908b2057203 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -30,6 +30,7 @@ mod scrollbar; mod settings_container; mod settings_group; mod stack; +mod sticky_items; mod tab; mod tab_bar; mod toggle; @@ -70,6 +71,7 @@ pub use scrollbar::*; pub use settings_container::*; pub use settings_group::*; pub use stack::*; +pub use sticky_items::*; pub use tab::*; pub use tab_bar::*; pub use toggle::*; diff --git a/crates/ui/src/components/sticky_items.rs b/crates/ui/src/components/sticky_items.rs new file mode 100644 index 0000000000000000000000000000000000000000..e5ef0cdf27daae5ccbd9fc1eeceac631e8ce757b --- /dev/null +++ b/crates/ui/src/components/sticky_items.rs @@ -0,0 +1,150 @@ +use std::ops::Range; + +use gpui::{ + AnyElement, App, AvailableSpace, Bounds, Context, Entity, Pixels, Render, UniformListTopSlot, + Window, point, size, +}; +use smallvec::SmallVec; + +pub trait StickyCandidate { + fn depth(&self) -> usize; +} + +pub struct StickyItems { + compute_fn: Box, &mut Window, &mut App) -> Vec>, + render_fn: Box SmallVec<[AnyElement; 8]>>, + last_item_is_drifting: bool, + anchor_index: Option, +} + +pub fn sticky_items( + entity: Entity, + compute_fn: impl Fn(&mut V, Range, &mut Window, &mut Context) -> Vec + 'static, + render_fn: impl Fn(&mut V, T, &mut Window, &mut Context) -> SmallVec<[AnyElement; 8]> + 'static, +) -> StickyItems +where + V: Render, + T: StickyCandidate + Clone + 'static, +{ + let entity_compute = entity.clone(); + let entity_render = entity.clone(); + + let compute_fn = Box::new( + move |range: Range, window: &mut Window, cx: &mut App| -> Vec { + entity_compute.update(cx, |view, cx| compute_fn(view, range, window, cx)) + }, + ); + let render_fn = Box::new( + move |entry: T, window: &mut Window, cx: &mut App| -> SmallVec<[AnyElement; 8]> { + entity_render.update(cx, |view, cx| render_fn(view, entry, window, cx)) + }, + ); + StickyItems { + compute_fn, + render_fn, + last_item_is_drifting: false, + anchor_index: None, + } +} + +impl UniformListTopSlot for StickyItems +where + T: StickyCandidate + Clone + 'static, +{ + fn compute( + &mut self, + visible_range: Range, + window: &mut Window, + cx: &mut App, + ) -> SmallVec<[AnyElement; 8]> { + let entries = (self.compute_fn)(visible_range.clone(), window, cx); + + let mut anchor_entry = None; + + let mut iter = entries.iter().enumerate().peekable(); + while let Some((ix, current_entry)) = iter.next() { + let current_depth = current_entry.depth(); + let index_in_range = ix; + + if current_depth < index_in_range { + anchor_entry = Some(current_entry.clone()); + break; + } + + if let Some(&(_next_ix, next_entry)) = iter.peek() { + let next_depth = next_entry.depth(); + + if next_depth < current_depth && next_depth < index_in_range { + self.last_item_is_drifting = true; + self.anchor_index = Some(visible_range.start + ix); + anchor_entry = Some(current_entry.clone()); + break; + } + } + } + + if let Some(anchor_entry) = anchor_entry { + (self.render_fn)(anchor_entry, window, cx) + } else { + SmallVec::new() + } + } + + fn prepaint( + &self, + items: &mut SmallVec<[AnyElement; 8]>, + bounds: Bounds, + item_height: Pixels, + scroll_offset: gpui::Point, + padding: gpui::Edges, + can_scroll_horizontally: bool, + window: &mut Window, + cx: &mut App, + ) { + let items_count = items.len(); + + for (ix, item) in items.iter_mut().enumerate() { + let mut item_y_offset = None; + if ix == items_count - 1 && self.last_item_is_drifting { + if let Some(anchor_index) = self.anchor_index { + let scroll_top = -scroll_offset.y; + let anchor_top = item_height * anchor_index; + let sticky_area_height = item_height * items_count; + item_y_offset = + Some((anchor_top - scroll_top - sticky_area_height).min(Pixels::ZERO)); + }; + } + + let sticky_origin = bounds.origin + + point( + if can_scroll_horizontally { + scroll_offset.x + padding.left + } else { + scroll_offset.x + }, + item_height * ix + padding.top + item_y_offset.unwrap_or(Pixels::ZERO), + ); + + let available_width = if can_scroll_horizontally { + bounds.size.width + scroll_offset.x.abs() + } else { + bounds.size.width + }; + + let available_space = size( + AvailableSpace::Definite(available_width), + AvailableSpace::Definite(item_height), + ); + + item.layout_as_root(available_space, window, cx); + item.prepaint_at(sticky_origin, window, cx); + } + } + + fn paint(&self, items: &mut SmallVec<[AnyElement; 8]>, window: &mut Window, cx: &mut App) { + // reverse so that last item is bottom most among sticky items + for item in items.iter_mut().rev() { + item.paint(window, cx); + } + } +} From 6b456ede49d8d95acc4d257bdaaf5ca6190b50db Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Mon, 7 Jul 2025 11:45:54 +0530 Subject: [PATCH 052/239] languages: Fix string override to match just `string_fragment` part of `template_string` (#33997) Closes #33703 `template_string` consists of `template_substitution` and `string_fragment` chunks. `template_substitution` should not be considered a string. ```ts const variable = `this is a string_fragment but ${this.is.template_substitution}`; ``` Release Notes: - Fixed auto-complete not showing on typing `.` character in template literal string in JavaScript and TypeScript files. --- crates/languages/src/javascript/overrides.scm | 7 +++---- crates/languages/src/tsx/overrides.scm | 7 +++---- crates/languages/src/typescript/overrides.scm | 3 +++ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/languages/src/javascript/overrides.scm b/crates/languages/src/javascript/overrides.scm index d93c8b5aea27b1fced6d021c68403348a97bb9e9..6dbbc88ef924c2cac65aaf9ff7e7dba87b99a359 100644 --- a/crates/languages/src/javascript/overrides.scm +++ b/crates/languages/src/javascript/overrides.scm @@ -1,9 +1,8 @@ (comment) @comment.inclusive -[ - (string) - (template_string) -] @string +(string) @string + +(template_string (string_fragment) @string) (jsx_element) @element diff --git a/crates/languages/src/tsx/overrides.scm b/crates/languages/src/tsx/overrides.scm index b26d010ce34b58cac34e516075c8c010525ed5fe..f5a51af33fee340762d6b689e78d2e94e9c84901 100644 --- a/crates/languages/src/tsx/overrides.scm +++ b/crates/languages/src/tsx/overrides.scm @@ -1,9 +1,8 @@ (comment) @comment.inclusive -[ - (string) - (template_string) -] @string +(string) @string + +(template_string (string_fragment) @string) (jsx_element) @element diff --git a/crates/languages/src/typescript/overrides.scm b/crates/languages/src/typescript/overrides.scm index 17ad7be339ccb2e670ebcf225b1ab9d2b9af40ae..8f437a1424af06aa4855aac67511926181977936 100644 --- a/crates/languages/src/typescript/overrides.scm +++ b/crates/languages/src/typescript/overrides.scm @@ -1,6 +1,9 @@ (comment) @comment.inclusive + (string) @string +(template_string (string_fragment) @string) + (_ value: (call_expression function: (identifier) @function_name_before_type_arguments type_arguments: (type_arguments))) From 83562fca77ff176e2519453ff924e5d6fc987b1c Mon Sep 17 00:00:00 2001 From: Liam <33645555+lj3954@users.noreply.github.com> Date: Mon, 7 Jul 2025 08:24:17 +0000 Subject: [PATCH 053/239] copilot: Indicate whether a request is initiated by an agent to Copilot API (#33895) Per [GitHub's documentation for VSCode's agent mode](https://docs.github.com/en/copilot/how-tos/chat/asking-github-copilot-questions-in-your-ide#agent-mode), a premium request is charged per user-submitted prompt. rather than per individual request the agent makes to an LLM. This PR matches Zed's functionality to VSCode's, accurately indicating to GitHub's API whether a given request is initiated by the user or by an agent, allowing a user to be metered only for prompts they send. See also: #31068 Release Notes: - Improve Copilot premium request tracking --- crates/copilot/src/copilot_chat.rs | 16 ++++++++++++++-- .../src/provider/copilot_chat.rs | 17 ++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/crates/copilot/src/copilot_chat.rs b/crates/copilot/src/copilot_chat.rs index b1fa1565f30ed79fdff763964708fe01c62d023f..4c91b4fedb790ab3500273ff21aba767cacd28e0 100644 --- a/crates/copilot/src/copilot_chat.rs +++ b/crates/copilot/src/copilot_chat.rs @@ -528,6 +528,7 @@ impl CopilotChat { pub async fn stream_completion( request: Request, + is_user_initiated: bool, mut cx: AsyncApp, ) -> Result>> { let this = cx @@ -562,7 +563,14 @@ impl CopilotChat { }; let api_url = configuration.api_url_from_endpoint(&token.api_endpoint); - stream_completion(client.clone(), token.api_key, api_url.into(), request).await + stream_completion( + client.clone(), + token.api_key, + api_url.into(), + request, + is_user_initiated, + ) + .await } pub fn set_configuration( @@ -697,6 +705,7 @@ async fn stream_completion( api_key: String, completion_url: Arc, request: Request, + is_user_initiated: bool, ) -> Result>> { let is_vision_request = request.messages.iter().any(|message| match message { ChatMessage::User { content } @@ -707,6 +716,8 @@ async fn stream_completion( _ => false, }); + let request_initiator = if is_user_initiated { "user" } else { "agent" }; + let mut request_builder = HttpRequest::builder() .method(Method::POST) .uri(completion_url.as_ref()) @@ -719,7 +730,8 @@ async fn stream_completion( ) .header("Authorization", format!("Bearer {}", api_key)) .header("Content-Type", "application/json") - .header("Copilot-Integration-Id", "vscode-chat"); + .header("Copilot-Integration-Id", "vscode-chat") + .header("X-Initiator", request_initiator); if is_vision_request { request_builder = diff --git a/crates/language_models/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs index 5411fbc63c10d84d96e2d85bf77c453bf66b5411..d9a84f1eb74465a0d5e72591d450802d5708cb20 100644 --- a/crates/language_models/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -30,6 +30,7 @@ use settings::SettingsStore; use std::time::Duration; use ui::prelude::*; use util::debug_panic; +use zed_llm_client::CompletionIntent; use super::anthropic::count_anthropic_tokens; use super::google::count_google_tokens; @@ -268,6 +269,19 @@ impl LanguageModel for CopilotChatLanguageModel { LanguageModelCompletionError, >, > { + let is_user_initiated = request.intent.is_none_or(|intent| match intent { + CompletionIntent::UserPrompt + | CompletionIntent::ThreadContextSummarization + | CompletionIntent::InlineAssist + | CompletionIntent::TerminalInlineAssist + | CompletionIntent::GenerateGitCommitMessage => true, + + CompletionIntent::ToolResults + | CompletionIntent::ThreadSummarization + | CompletionIntent::CreateFile + | CompletionIntent::EditFile => false, + }); + let copilot_request = match into_copilot_chat(&self.model, request) { Ok(request) => request, Err(err) => return futures::future::ready(Err(err.into())).boxed(), @@ -276,7 +290,8 @@ impl LanguageModel for CopilotChatLanguageModel { let request_limiter = self.request_limiter.clone(); let future = cx.spawn(async move |cx| { - let request = CopilotChat::stream_completion(copilot_request, cx.clone()); + let request = + CopilotChat::stream_completion(copilot_request, is_user_initiated, cx.clone()); request_limiter .stream(async move { let response = request.await?; From 30a441b7149d38d1cdf0a070c360b165b191c1f5 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Mon, 7 Jul 2025 15:32:33 +0530 Subject: [PATCH 054/239] agent_ui: Fix disabled context servers not showing in agent setting (#33856) Previously if I set enabled: false for one the context servers in settings.json it will not show up in the settings in agent panel when I start zed. But if I enabled it from settings it properly showed up. We were filtering the configuration to only get the enabled context servers from settings.json. This PR adds fetching all of them. Release Notes: - agent: Show context servers which are disabled in settings in agent panel settings. --- crates/agent_ui/src/agent_configuration.rs | 2 +- crates/project/src/context_server_store.rs | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index d91aa5fb2200629703f2f789f293686eb4c3ad73..8bfdd507611112b2930fd07270667050796533e3 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -436,7 +436,7 @@ impl AgentConfiguration { window: &mut Window, cx: &mut Context, ) -> impl IntoElement { - let context_server_ids = self.context_server_store.read(cx).all_server_ids().clone(); + let context_server_ids = self.context_server_store.read(cx).configured_server_ids(); v_flex() .p(DynamicSpacing::Base16.rems(cx)) diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index d2541e1b31fe6a798edc92bb070a17b631f61f98..fd31e638d4bf7774af83d430dca232d1ade74f01 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -171,6 +171,15 @@ impl ContextServerStore { ) } + /// Returns all configured context server ids, regardless of enabled state. + pub fn configured_server_ids(&self) -> Vec { + self.context_server_settings + .keys() + .cloned() + .map(ContextServerId) + .collect() + } + #[cfg(any(test, feature = "test-support"))] pub fn test( registry: Entity, From 018dbfba0948a3df6f23fe932eeb35b05c9807fb Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 7 Jul 2025 12:53:59 +0200 Subject: [PATCH 055/239] agent: Show line numbers of symbols when using `@symbol` (#34004) Closes #ISSUE Release Notes: - N/A --- crates/agent_ui/src/context_picker/completion_provider.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent_ui/src/context_picker/completion_provider.rs b/crates/agent_ui/src/context_picker/completion_provider.rs index ab91ded2c8e45ffd8c840f9dacaa413137dacd51..b377e40b193d090a61b88232098fd45645a2ab4f 100644 --- a/crates/agent_ui/src/context_picker/completion_provider.rs +++ b/crates/agent_ui/src/context_picker/completion_provider.rs @@ -686,6 +686,7 @@ impl ContextPickerCompletionProvider { let mut label = CodeLabel::plain(symbol.name.clone(), None); label.push_str(" ", None); label.push_str(&file_name, comment_id); + label.push_str(&format!(" L{}", symbol.range.start.0.row + 1), comment_id); let new_text = format!("{} ", MentionLink::for_symbol(&symbol.name, &full_path)); let new_text_len = new_text.len(); From c99e42a3d6d57b9f6155a17e99ef9ca5aa5e694d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Mon, 7 Jul 2025 20:21:48 +0800 Subject: [PATCH 056/239] windows: Properly handle surrogates (#34006) Closes #33791 Surrogate pairs are now handled correctly, so input from tools like `WinCompose` is properly received. Release Notes: - N/A --- crates/gpui/src/platform/windows/events.rs | 41 ++++++++++++++++++---- crates/gpui/src/platform/windows/window.rs | 3 ++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index a43fdc097f24a5c30544817f60c992829dbc831a..8b8964b2dfa827784ad5bfc2281835d64f4d7fbc 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -466,12 +466,7 @@ fn handle_keyup_msg( } fn handle_char_msg(wparam: WPARAM, state_ptr: Rc) -> Option { - let Some(input) = char::from_u32(wparam.0 as u32) - .filter(|c| !c.is_control()) - .map(String::from) - else { - return Some(1); - }; + let input = parse_char_message(wparam, &state_ptr)?; with_input_handler(&state_ptr, |input_handler| { input_handler.replace_text_in_range(None, &input); }); @@ -1228,6 +1223,36 @@ fn handle_input_language_changed( Some(0) } +#[inline] +fn parse_char_message(wparam: WPARAM, state_ptr: &Rc) -> Option { + let code_point = wparam.loword(); + let mut lock = state_ptr.state.borrow_mut(); + // https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-3/#G2630 + match code_point { + 0xD800..=0xDBFF => { + // High surrogate, wait for low surrogate + lock.pending_surrogate = Some(code_point); + None + } + 0xDC00..=0xDFFF => { + if let Some(high_surrogate) = lock.pending_surrogate.take() { + // Low surrogate, combine with pending high surrogate + String::from_utf16(&[high_surrogate, code_point]).ok() + } else { + // Invalid low surrogate without a preceding high surrogate + log::warn!( + "Received low surrogate without a preceding high surrogate: {code_point:x}" + ); + None + } + } + _ => { + lock.pending_surrogate = None; + String::from_utf16(&[code_point]).ok() + } + } +} + #[inline] fn translate_message(handle: HWND, wparam: WPARAM, lparam: LPARAM) { let msg = MSG { @@ -1270,6 +1295,10 @@ where capslock: current_capslock(), })) } + VK_PACKET => { + translate_message(handle, wparam, lparam); + None + } VK_CAPITAL => { let capslock = current_capslock(); if state diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index 5c7dd07c4857c8d99dd8dcde294942cd33bf0573..5703a82815eb0679ca3668a13c08f3e9affa3696 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -43,6 +43,7 @@ pub struct WindowsWindowState { pub callbacks: Callbacks, pub input_handler: Option, + pub pending_surrogate: Option, pub last_reported_modifiers: Option, pub last_reported_capslock: Option, pub system_key_handled: bool, @@ -105,6 +106,7 @@ impl WindowsWindowState { let renderer = windows_renderer::init(gpu_context, hwnd, transparent)?; let callbacks = Callbacks::default(); let input_handler = None; + let pending_surrogate = None; let last_reported_modifiers = None; let last_reported_capslock = None; let system_key_handled = false; @@ -126,6 +128,7 @@ impl WindowsWindowState { min_size, callbacks, input_handler, + pending_surrogate, last_reported_modifiers, last_reported_capslock, system_key_handled, From 955580dae6f8e9faf8a64d145f287d3710c08c11 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 7 Jul 2025 09:51:30 -0400 Subject: [PATCH 057/239] Adjust Go outline query for method definition to avoid pesky whitespace (#33971) Closes #33951 There's an adjustment that kicks in to extend `name_ranges` when we capture more than one `@name` for an outline `@item`. That was happening here because we captured both the parameter name for the method receiver and the name of the method as `@name`. It seems like only the second one should have that annotation. Release Notes: - Fixed extraneous leading space in `$ZED_SYMBOL` when used with Go methods. --- crates/languages/src/go/outline.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/languages/src/go/outline.scm b/crates/languages/src/go/outline.scm index 0e4d6a52f3b5d48538c30d658f98ec3692b966c6..e37ae7e5723027af3227f947898b301c0305a4f5 100644 --- a/crates/languages/src/go/outline.scm +++ b/crates/languages/src/go/outline.scm @@ -25,7 +25,7 @@ receiver: (parameter_list "(" @context (parameter_declaration - name: (_) @name + name: (_) @context type: (_) @context) ")" @context) name: (field_identifier) @name From 82aee6bcf70f4d1039f748e6922545c79a534a18 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 7 Jul 2025 17:28:18 +0300 Subject: [PATCH 058/239] Another lsp tool UI migration (#34009) https://github.com/user-attachments/assets/54182f0d-43e9-4482-89b9-94db5ddaabf8 Release Notes: - N/A --- Cargo.lock | 1 - crates/agent_ui/src/context_picker.rs | 2 + .../src/inline_completion_button.rs | 4 - crates/language_tools/Cargo.toml | 1 - crates/language_tools/src/lsp_tool.rs | 967 ++++++++---------- crates/ui/src/components/context_menu.rs | 25 +- crates/ui/src/components/popover_menu.rs | 18 + 7 files changed, 484 insertions(+), 534 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index baed77a49fd7c35af5ede122c854746308fde162..921eea00f8147f4715934ed0c0ab2015945cd4ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9023,7 +9023,6 @@ dependencies = [ "itertools 0.14.0", "language", "lsp", - "picker", "project", "release_channel", "serde_json", diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index f303f34a52856a068f1d2da33cf1f0a4fb5813a5..73fc0b36ce33853abd7b8689ef251855e5aca6ac 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -426,6 +426,7 @@ impl ContextPicker { this.add_recent_file(project_path.clone(), window, cx); }) }, + None, ) } RecentEntry::Thread(thread) => { @@ -443,6 +444,7 @@ impl ContextPicker { .detach_and_log_err(cx); }) }, + None, ) } } diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index f8123d676a001427b8b0350d53cdd7ab8b1041ab..7e6b77b93deafbb971980d8b2d19f33f2fa348b4 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -835,10 +835,6 @@ impl InlineCompletionButton { cx.notify(); } - - pub fn toggle_menu(&mut self, window: &mut Window, cx: &mut Context) { - self.popover_menu_handle.toggle(window, cx); - } } impl StatusItemView for InlineCompletionButton { diff --git a/crates/language_tools/Cargo.toml b/crates/language_tools/Cargo.toml index ffdc939809145b319d5421adf5b8a923604e74fe..45af7518d589166e26788203c919d2267b544756 100644 --- a/crates/language_tools/Cargo.toml +++ b/crates/language_tools/Cargo.toml @@ -24,7 +24,6 @@ gpui.workspace = true itertools.workspace = true language.workspace = true lsp.workspace = true -picker.workspace = true project.workspace = true serde_json.workspace = true settings.workspace = true diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_tool.rs index 24a53ae2529b23b45e2478227109506839c12a89..6cd2f83184d946fdbf133f5d7d17d188d294ee13 100644 --- a/crates/language_tools/src/lsp_tool.rs +++ b/crates/language_tools/src/lsp_tool.rs @@ -1,19 +1,18 @@ -use std::{collections::hash_map, path::PathBuf, sync::Arc, time::Duration}; +use std::{collections::hash_map, path::PathBuf, rc::Rc, time::Duration}; use client::proto; use collections::{HashMap, HashSet}; use editor::{Editor, EditorEvent}; use feature_flags::FeatureFlagAppExt as _; -use gpui::{ - Corner, DismissEvent, Entity, Focusable as _, MouseButton, Subscription, Task, WeakEntity, - actions, -}; +use gpui::{Corner, Entity, Subscription, Task, WeakEntity, actions}; use language::{BinaryStatus, BufferId, LocalFile, ServerHealth}; use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector}; -use picker::{Picker, PickerDelegate, popover_menu::PickerPopoverMenu}; use project::{LspStore, LspStoreEvent, project_settings::ProjectSettings}; use settings::{Settings as _, SettingsStore}; -use ui::{Context, Indicator, PopoverMenuHandle, Tooltip, Window, prelude::*}; +use ui::{ + Context, ContextMenu, ContextMenuEntry, ContextMenuItem, DocumentationAside, DocumentationSide, + Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, Window, prelude::*, +}; use workspace::{StatusItemView, Workspace}; @@ -28,33 +27,38 @@ actions!( ); pub struct LspTool { - state: Entity, - popover_menu_handle: PopoverMenuHandle>, - lsp_picker: Option>>, + server_state: Entity, + popover_menu_handle: PopoverMenuHandle, + lsp_menu: Option>, + lsp_menu_refresh: Task<()>, _subscriptions: Vec, } -struct PickerState { +#[derive(Debug)] +struct LanguageServerState { + items: Vec, + other_servers_start_index: Option, workspace: WeakEntity, lsp_store: WeakEntity, active_editor: Option, language_servers: LanguageServers, } -#[derive(Debug)] -pub struct LspPickerDelegate { - state: Entity, - selected_index: usize, - items: Vec, - other_servers_start_index: Option, -} - struct ActiveEditor { editor: WeakEntity, _editor_subscription: Subscription, editor_buffers: HashSet, } +impl std::fmt::Debug for ActiveEditor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ActiveEditor") + .field("editor", &self.editor) + .field("editor_buffers", &self.editor_buffers) + .finish_non_exhaustive() + } +} + #[derive(Debug, Default, Clone)] struct LanguageServers { health_statuses: HashMap, @@ -104,192 +108,154 @@ impl LanguageServerHealthStatus { } } -impl LspPickerDelegate { - fn regenerate_items(&mut self, cx: &mut Context>) { - self.state.update(cx, |state, cx| { - let editor_buffers = state - .active_editor - .as_ref() - .map(|active_editor| active_editor.editor_buffers.clone()) - .unwrap_or_default(); - let editor_buffer_paths = editor_buffers - .iter() - .filter_map(|buffer_id| { - let buffer_path = state - .lsp_store - .update(cx, |lsp_store, cx| { - Some( - project::File::from_dyn( - lsp_store - .buffer_store() +impl LanguageServerState { + fn fill_menu(&self, mut menu: ContextMenu, cx: &mut Context) -> ContextMenu { + let lsp_logs = cx + .try_global::() + .and_then(|lsp_logs| lsp_logs.0.upgrade()); + let lsp_store = self.lsp_store.upgrade(); + let Some((lsp_logs, lsp_store)) = lsp_logs.zip(lsp_store) else { + return menu; + }; + + for (i, item) in self.items.iter().enumerate() { + if let LspItem::ToggleServersButton { restart } = item { + let label = if *restart { + "Restart All Servers" + } else { + "Stop All Servers" + }; + let restart = *restart; + let button = ContextMenuEntry::new(label).handler({ + let state = cx.entity(); + move |_, cx| { + let lsp_store = state.read(cx).lsp_store.clone(); + lsp_store + .update(cx, |lsp_store, cx| { + if restart { + let Some(workspace) = state.read(cx).workspace.upgrade() else { + return; + }; + let project = workspace.read(cx).project().clone(); + let buffer_store = project.read(cx).buffer_store().clone(); + let worktree_store = project.read(cx).worktree_store(); + + let buffers = state .read(cx) - .get(*buffer_id)? + .language_servers + .servers_per_buffer_abs_path + .keys() + .filter_map(|abs_path| { + worktree_store.read(cx).find_worktree(abs_path, cx) + }) + .filter_map(|(worktree, relative_path)| { + let entry = + worktree.read(cx).entry_for_path(&relative_path)?; + project.read(cx).path_for_entry(entry.id, cx) + }) + .filter_map(|project_path| { + buffer_store.read(cx).get_by_path(&project_path) + }) + .collect(); + let selectors = state .read(cx) - .file(), - )? - .abs_path(cx), - ) - }) - .ok()??; - Some(buffer_path) - }) - .collect::>(); - - let mut servers_with_health_checks = HashSet::default(); - let mut server_ids_with_health_checks = HashSet::default(); - let mut buffer_servers = - Vec::with_capacity(state.language_servers.health_statuses.len()); - let mut other_servers = - Vec::with_capacity(state.language_servers.health_statuses.len()); - let buffer_server_ids = editor_buffer_paths - .iter() - .filter_map(|buffer_path| { - state - .language_servers - .servers_per_buffer_abs_path - .get(buffer_path) - }) - .flatten() - .fold(HashMap::default(), |mut acc, (server_id, name)| { - match acc.entry(*server_id) { - hash_map::Entry::Occupied(mut o) => { - let old_name: &mut Option<&LanguageServerName> = o.get_mut(); - if old_name.is_none() { - *old_name = name.as_ref(); - } - } - hash_map::Entry::Vacant(v) => { - v.insert(name.as_ref()); - } + .items + .iter() + // Do not try to use IDs as we have stopped all servers already, when allowing to restart them all + .flat_map(|item| match item { + LspItem::ToggleServersButton { .. } => None, + LspItem::WithHealthCheck(_, status, ..) => Some( + LanguageServerSelector::Name(status.name.clone()), + ), + LspItem::WithBinaryStatus(_, server_name, ..) => Some( + LanguageServerSelector::Name(server_name.clone()), + ), + }) + .collect(); + lsp_store.restart_language_servers_for_buffers( + buffers, selectors, cx, + ); + } else { + lsp_store.stop_all_language_servers(cx); + } + }) + .ok(); } - acc }); - for (server_id, server_state) in &state.language_servers.health_statuses { - let binary_status = state - .language_servers - .binary_statuses - .get(&server_state.name); - servers_with_health_checks.insert(&server_state.name); - server_ids_with_health_checks.insert(*server_id); - if buffer_server_ids.contains_key(server_id) { - buffer_servers.push(ServerData::WithHealthCheck( - *server_id, - server_state, - binary_status, - )); - } else { - other_servers.push(ServerData::WithHealthCheck( - *server_id, - server_state, - binary_status, - )); - } - } - - let mut can_stop_all = false; - let mut can_restart_all = true; + menu = menu.separator().item(button); + continue; + }; + let Some(server_info) = item.server_info() else { + continue; + }; + let workspace = self.workspace.clone(); + let server_selector = server_info.server_selector(); + // TODO currently, Zed remote does not work well with the LSP logs + // https://github.com/zed-industries/zed/issues/28557 + let has_logs = lsp_store.read(cx).as_local().is_some() + && lsp_logs.read(cx).has_server_logs(&server_selector); + let status_color = server_info + .binary_status + .and_then(|binary_status| match binary_status.status { + BinaryStatus::None => None, + BinaryStatus::CheckingForUpdate + | BinaryStatus::Downloading + | BinaryStatus::Starting => Some(Color::Modified), + BinaryStatus::Stopping => Some(Color::Disabled), + BinaryStatus::Stopped => Some(Color::Disabled), + BinaryStatus::Failed { .. } => Some(Color::Error), + }) + .or_else(|| { + Some(match server_info.health? { + ServerHealth::Ok => Color::Success, + ServerHealth::Warning => Color::Warning, + ServerHealth::Error => Color::Error, + }) + }) + .unwrap_or(Color::Success); - for (server_name, status) in state - .language_servers - .binary_statuses - .iter() - .filter(|(name, _)| !servers_with_health_checks.contains(name)) + if self + .other_servers_start_index + .is_some_and(|index| index == i) { - match status.status { - BinaryStatus::None => { - can_restart_all = false; - can_stop_all = true; - } - BinaryStatus::CheckingForUpdate => { - can_restart_all = false; - } - BinaryStatus::Downloading => { - can_restart_all = false; - } - BinaryStatus::Starting => { - can_restart_all = false; - } - BinaryStatus::Stopping => { - can_restart_all = false; - } - BinaryStatus::Stopped => {} - BinaryStatus::Failed { .. } => {} - } - - let matching_server_id = state - .language_servers - .servers_per_buffer_abs_path - .iter() - .filter(|(path, _)| editor_buffer_paths.contains(path)) - .flat_map(|(_, server_associations)| server_associations.iter()) - .find_map(|(id, name)| { - if name.as_ref() == Some(server_name) { - Some(*id) - } else { - None - } - }); - if let Some(server_id) = matching_server_id { - buffer_servers.push(ServerData::WithBinaryStatus( - Some(server_id), - server_name, - status, - )); - } else { - other_servers.push(ServerData::WithBinaryStatus(None, server_name, status)); - } + menu = menu.separator(); } - - buffer_servers.sort_by_key(|data| data.name().clone()); - other_servers.sort_by_key(|data| data.name().clone()); - - let mut other_servers_start_index = None; - let mut new_lsp_items = - Vec::with_capacity(buffer_servers.len() + other_servers.len() + 1); - new_lsp_items.extend(buffer_servers.into_iter().map(ServerData::into_lsp_item)); - if !new_lsp_items.is_empty() { - other_servers_start_index = Some(new_lsp_items.len()); - } - new_lsp_items.extend(other_servers.into_iter().map(ServerData::into_lsp_item)); - if !new_lsp_items.is_empty() { - if can_stop_all { - new_lsp_items.push(LspItem::ToggleServersButton { restart: false }); - } else if can_restart_all { - new_lsp_items.push(LspItem::ToggleServersButton { restart: true }); - } - } - - self.items = new_lsp_items; - self.other_servers_start_index = other_servers_start_index; - }); - } - - fn server_info(&self, ix: usize) -> Option { - match self.items.get(ix)? { - LspItem::ToggleServersButton { .. } => None, - LspItem::WithHealthCheck( - language_server_id, - language_server_health_status, - language_server_binary_status, - ) => Some(ServerInfo { - name: language_server_health_status.name.clone(), - id: Some(*language_server_id), - health: language_server_health_status.health(), - binary_status: language_server_binary_status.clone(), - message: language_server_health_status.message(), - }), - LspItem::WithBinaryStatus( - server_id, - language_server_name, - language_server_binary_status, - ) => Some(ServerInfo { - name: language_server_name.clone(), - id: *server_id, - health: None, - binary_status: Some(language_server_binary_status.clone()), - message: language_server_binary_status.message.clone(), - }), + menu = menu.item(ContextMenuItem::custom_entry( + move |_, _| { + h_flex() + .gap_1() + .w_full() + .child(Indicator::dot().color(status_color)) + .child(Label::new(server_info.name.0.clone())) + .when(!has_logs, |div| div.cursor_default()) + .into_any_element() + }, + { + let lsp_logs = lsp_logs.clone(); + move |window, cx| { + if !has_logs { + cx.propagate(); + return; + } + lsp_logs.update(cx, |lsp_logs, cx| { + lsp_logs.open_server_trace( + workspace.clone(), + server_selector.clone(), + window, + cx, + ); + }); + } + }, + server_info.message.map(|server_message| { + DocumentationAside::new( + DocumentationSide::Right, + Rc::new(move |_| Label::new(server_message.clone()).into_any_element()), + ) + }), + )); } + menu } } @@ -375,6 +341,36 @@ enum LspItem { }, } +impl LspItem { + fn server_info(&self) -> Option { + match self { + LspItem::ToggleServersButton { .. } => None, + LspItem::WithHealthCheck( + language_server_id, + language_server_health_status, + language_server_binary_status, + ) => Some(ServerInfo { + name: language_server_health_status.name.clone(), + id: Some(*language_server_id), + health: language_server_health_status.health(), + binary_status: language_server_binary_status.clone(), + message: language_server_health_status.message(), + }), + LspItem::WithBinaryStatus( + server_id, + language_server_name, + language_server_binary_status, + ) => Some(ServerInfo { + name: language_server_name.clone(), + id: *server_id, + health: None, + binary_status: Some(language_server_binary_status.clone()), + message: language_server_binary_status.message.clone(), + }), + } + } +} + impl ServerData<'_> { fn name(&self) -> &LanguageServerName { match self { @@ -395,267 +391,21 @@ impl ServerData<'_> { } } -impl PickerDelegate for LspPickerDelegate { - type ListItem = AnyElement; - - fn match_count(&self) -> usize { - self.items.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context>) { - self.selected_index = ix; - cx.notify(); - } - - fn update_matches( - &mut self, - _: String, - _: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - cx.spawn(async move |lsp_picker, cx| { - cx.background_executor() - .timer(Duration::from_millis(30)) - .await; - lsp_picker - .update(cx, |lsp_picker, cx| { - lsp_picker.delegate.regenerate_items(cx); - }) - .ok(); - }) - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - Arc::default() - } - - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { - if let Some(LspItem::ToggleServersButton { restart }) = self.items.get(self.selected_index) - { - let lsp_store = self.state.read(cx).lsp_store.clone(); - lsp_store - .update(cx, |lsp_store, cx| { - if *restart { - let Some(workspace) = self.state.read(cx).workspace.upgrade() else { - return; - }; - let project = workspace.read(cx).project().clone(); - let buffer_store = project.read(cx).buffer_store().clone(); - let worktree_store = project.read(cx).worktree_store(); - - let buffers = self - .state - .read(cx) - .language_servers - .servers_per_buffer_abs_path - .keys() - .filter_map(|abs_path| { - worktree_store.read(cx).find_worktree(abs_path, cx) - }) - .filter_map(|(worktree, relative_path)| { - let entry = worktree.read(cx).entry_for_path(&relative_path)?; - project.read(cx).path_for_entry(entry.id, cx) - }) - .filter_map(|project_path| { - buffer_store.read(cx).get_by_path(&project_path) - }) - .collect(); - let selectors = self - .items - .iter() - // Do not try to use IDs as we have stopped all servers already, when allowing to restart them all - .flat_map(|item| match item { - LspItem::ToggleServersButton { .. } => None, - LspItem::WithHealthCheck(_, status, ..) => { - Some(LanguageServerSelector::Name(status.name.clone())) - } - LspItem::WithBinaryStatus(_, server_name, ..) => { - Some(LanguageServerSelector::Name(server_name.clone())) - } - }) - .collect(); - lsp_store.restart_language_servers_for_buffers(buffers, selectors, cx); - } else { - lsp_store.stop_all_language_servers(cx); - } - }) - .ok(); - } - - let Some(server_selector) = self - .server_info(self.selected_index) - .map(|info| info.server_selector()) - else { - return; - }; - let lsp_logs = cx.global::().0.clone(); - let lsp_store = self.state.read(cx).lsp_store.clone(); - let workspace = self.state.read(cx).workspace.clone(); - lsp_logs - .update(cx, |lsp_logs, cx| { - let has_logs = lsp_store - .update(cx, |lsp_store, _| { - lsp_store.as_local().is_some() && lsp_logs.has_server_logs(&server_selector) - }) - .unwrap_or(false); - if has_logs { - lsp_logs.open_server_trace(workspace, server_selector, window, cx); - } - }) - .ok(); - } - - fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { - cx.emit(DismissEvent); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - let rendered_match = h_flex().px_1().gap_1(); - let rendered_match_contents = h_flex() - .id(("lsp-item", ix)) - .w_full() - .px_2() - .gap_2() - .when(selected, |server_entry| { - server_entry.bg(cx.theme().colors().element_hover) - }) - .hover(|s| s.bg(cx.theme().colors().element_hover)); - - if let Some(LspItem::ToggleServersButton { restart }) = self.items.get(ix) { - let label = Label::new(if *restart { - "Restart All Servers" - } else { - "Stop All Servers" - }); - return Some( - rendered_match - .child(rendered_match_contents.child(label)) - .into_any_element(), - ); - } - - let server_info = self.server_info(ix)?; - let workspace = self.state.read(cx).workspace.clone(); - let lsp_logs = cx.global::().0.upgrade()?; - let lsp_store = self.state.read(cx).lsp_store.upgrade()?; - let server_selector = server_info.server_selector(); - - // TODO currently, Zed remote does not work well with the LSP logs - // https://github.com/zed-industries/zed/issues/28557 - let has_logs = lsp_store.read(cx).as_local().is_some() - && lsp_logs.read(cx).has_server_logs(&server_selector); - - let status_color = server_info - .binary_status - .and_then(|binary_status| match binary_status.status { - BinaryStatus::None => None, - BinaryStatus::CheckingForUpdate - | BinaryStatus::Downloading - | BinaryStatus::Starting => Some(Color::Modified), - BinaryStatus::Stopping => Some(Color::Disabled), - BinaryStatus::Stopped => Some(Color::Disabled), - BinaryStatus::Failed { .. } => Some(Color::Error), - }) - .or_else(|| { - Some(match server_info.health? { - ServerHealth::Ok => Color::Success, - ServerHealth::Warning => Color::Warning, - ServerHealth::Error => Color::Error, - }) - }) - .unwrap_or(Color::Success); - - Some( - rendered_match - .child( - rendered_match_contents - .child(Indicator::dot().color(status_color)) - .child(Label::new(server_info.name.0.clone())) - .when_some( - server_info.message.clone(), - |server_entry, server_message| { - server_entry.tooltip(Tooltip::text(server_message.clone())) - }, - ), - ) - .when_else( - has_logs, - |server_entry| { - server_entry.on_mouse_down(MouseButton::Left, { - let workspace = workspace.clone(); - let lsp_logs = lsp_logs.downgrade(); - let server_selector = server_selector.clone(); - move |_, window, cx| { - lsp_logs - .update(cx, |lsp_logs, cx| { - lsp_logs.open_server_trace( - workspace.clone(), - server_selector.clone(), - window, - cx, - ); - }) - .ok(); - } - }) - }, - |div| div.cursor_default(), - ) - .into_any_element(), - ) - } - - fn render_editor( - &self, - editor: &Entity, - _: &mut Window, - cx: &mut Context>, - ) -> Div { - div().child(div().track_focus(&editor.focus_handle(cx))) - } - - fn separators_after_indices(&self) -> Vec { - if self.items.is_empty() { - return Vec::new(); - } - let mut indices = vec![self.items.len().saturating_sub(2)]; - if let Some(other_servers_start_index) = self.other_servers_start_index { - if other_servers_start_index > 0 { - indices.insert(0, other_servers_start_index - 1); - indices.dedup(); - } - } - indices - } -} - impl LspTool { pub fn new( workspace: &Workspace, - popover_menu_handle: PopoverMenuHandle>, + popover_menu_handle: PopoverMenuHandle, window: &mut Window, cx: &mut Context, ) -> Self { let settings_subscription = cx.observe_global_in::(window, move |lsp_tool, window, cx| { if ProjectSettings::get_global(cx).global_lsp_settings.button { - if lsp_tool.lsp_picker.is_none() { - lsp_tool.lsp_picker = - Some(Self::new_lsp_picker(lsp_tool.state.clone(), window, cx)); - cx.notify(); + if lsp_tool.lsp_menu.is_none() { + lsp_tool.refresh_lsp_menu(true, window, cx); return; } - } else if lsp_tool.lsp_picker.take().is_some() { + } else if lsp_tool.lsp_menu.take().is_some() { cx.notify(); } }); @@ -666,17 +416,20 @@ impl LspTool { lsp_tool.on_lsp_store_event(e, window, cx) }); - let state = cx.new(|_| PickerState { + let state = cx.new(|_| LanguageServerState { workspace: workspace.weak_handle(), + items: Vec::new(), + other_servers_start_index: None, lsp_store: lsp_store.downgrade(), active_editor: None, language_servers: LanguageServers::default(), }); Self { - state, + server_state: state, popover_menu_handle, - lsp_picker: None, + lsp_menu: None, + lsp_menu_refresh: Task::ready(()), _subscriptions: vec![settings_subscription, lsp_store_subscription], } } @@ -687,7 +440,7 @@ impl LspTool { window: &mut Window, cx: &mut Context, ) { - let Some(lsp_picker) = self.lsp_picker.clone() else { + if self.lsp_menu.is_none() { return; }; let mut updated = false; @@ -720,7 +473,7 @@ impl LspTool { BinaryStatus::Failed { error } } }; - self.state.update(cx, |state, _| { + self.server_state.update(cx, |state, _| { state.language_servers.update_binary_status( binary_status, status_update.message.as_deref(), @@ -737,7 +490,7 @@ impl LspTool { proto::ServerHealth::Warning => ServerHealth::Warning, proto::ServerHealth::Error => ServerHealth::Error, }; - self.state.update(cx, |state, _| { + self.server_state.update(cx, |state, _| { state.language_servers.update_server_health( *language_server_id, health, @@ -756,7 +509,7 @@ impl LspTool { message: proto::update_language_server::Variant::RegisteredForBuffer(update), .. } => { - self.state.update(cx, |state, _| { + self.server_state.update(cx, |state, _| { state .language_servers .servers_per_buffer_abs_path @@ -770,27 +523,203 @@ impl LspTool { }; if updated { - lsp_picker.update(cx, |lsp_picker, cx| { - lsp_picker.refresh(window, cx); - }); + self.refresh_lsp_menu(false, window, cx); } } - fn new_lsp_picker( - state: Entity, + fn regenerate_items(&mut self, cx: &mut App) { + self.server_state.update(cx, |state, cx| { + let editor_buffers = state + .active_editor + .as_ref() + .map(|active_editor| active_editor.editor_buffers.clone()) + .unwrap_or_default(); + let editor_buffer_paths = editor_buffers + .iter() + .filter_map(|buffer_id| { + let buffer_path = state + .lsp_store + .update(cx, |lsp_store, cx| { + Some( + project::File::from_dyn( + lsp_store + .buffer_store() + .read(cx) + .get(*buffer_id)? + .read(cx) + .file(), + )? + .abs_path(cx), + ) + }) + .ok()??; + Some(buffer_path) + }) + .collect::>(); + + let mut servers_with_health_checks = HashSet::default(); + let mut server_ids_with_health_checks = HashSet::default(); + let mut buffer_servers = + Vec::with_capacity(state.language_servers.health_statuses.len()); + let mut other_servers = + Vec::with_capacity(state.language_servers.health_statuses.len()); + let buffer_server_ids = editor_buffer_paths + .iter() + .filter_map(|buffer_path| { + state + .language_servers + .servers_per_buffer_abs_path + .get(buffer_path) + }) + .flatten() + .fold(HashMap::default(), |mut acc, (server_id, name)| { + match acc.entry(*server_id) { + hash_map::Entry::Occupied(mut o) => { + let old_name: &mut Option<&LanguageServerName> = o.get_mut(); + if old_name.is_none() { + *old_name = name.as_ref(); + } + } + hash_map::Entry::Vacant(v) => { + v.insert(name.as_ref()); + } + } + acc + }); + for (server_id, server_state) in &state.language_servers.health_statuses { + let binary_status = state + .language_servers + .binary_statuses + .get(&server_state.name); + servers_with_health_checks.insert(&server_state.name); + server_ids_with_health_checks.insert(*server_id); + if buffer_server_ids.contains_key(server_id) { + buffer_servers.push(ServerData::WithHealthCheck( + *server_id, + server_state, + binary_status, + )); + } else { + other_servers.push(ServerData::WithHealthCheck( + *server_id, + server_state, + binary_status, + )); + } + } + + let mut can_stop_all = !state.language_servers.health_statuses.is_empty(); + let mut can_restart_all = state.language_servers.health_statuses.is_empty(); + for (server_name, status) in state + .language_servers + .binary_statuses + .iter() + .filter(|(name, _)| !servers_with_health_checks.contains(name)) + { + match status.status { + BinaryStatus::None => { + can_restart_all = false; + can_stop_all |= true; + } + BinaryStatus::CheckingForUpdate => { + can_restart_all = false; + can_stop_all = false; + } + BinaryStatus::Downloading => { + can_restart_all = false; + can_stop_all = false; + } + BinaryStatus::Starting => { + can_restart_all = false; + can_stop_all = false; + } + BinaryStatus::Stopping => { + can_restart_all = false; + can_stop_all = false; + } + BinaryStatus::Stopped => {} + BinaryStatus::Failed { .. } => {} + } + + let matching_server_id = state + .language_servers + .servers_per_buffer_abs_path + .iter() + .filter(|(path, _)| editor_buffer_paths.contains(path)) + .flat_map(|(_, server_associations)| server_associations.iter()) + .find_map(|(id, name)| { + if name.as_ref() == Some(server_name) { + Some(*id) + } else { + None + } + }); + if let Some(server_id) = matching_server_id { + buffer_servers.push(ServerData::WithBinaryStatus( + Some(server_id), + server_name, + status, + )); + } else { + other_servers.push(ServerData::WithBinaryStatus(None, server_name, status)); + } + } + + buffer_servers.sort_by_key(|data| data.name().clone()); + other_servers.sort_by_key(|data| data.name().clone()); + + let mut other_servers_start_index = None; + let mut new_lsp_items = + Vec::with_capacity(buffer_servers.len() + other_servers.len() + 1); + new_lsp_items.extend(buffer_servers.into_iter().map(ServerData::into_lsp_item)); + if !new_lsp_items.is_empty() { + other_servers_start_index = Some(new_lsp_items.len()); + } + new_lsp_items.extend(other_servers.into_iter().map(ServerData::into_lsp_item)); + if !new_lsp_items.is_empty() { + if can_stop_all { + new_lsp_items.push(LspItem::ToggleServersButton { restart: false }); + } else if can_restart_all { + new_lsp_items.push(LspItem::ToggleServersButton { restart: true }); + } + } + + state.items = new_lsp_items; + state.other_servers_start_index = other_servers_start_index; + }); + } + + fn refresh_lsp_menu( + &mut self, + create_if_empty: bool, window: &mut Window, cx: &mut Context, - ) -> Entity> { - cx.new(|cx| { - let mut delegate = LspPickerDelegate { - selected_index: 0, - other_servers_start_index: None, - items: Vec::new(), - state, - }; - delegate.regenerate_items(cx); - Picker::list(delegate, window, cx) - }) + ) { + if create_if_empty || self.lsp_menu.is_some() { + let state = self.server_state.clone(); + self.lsp_menu_refresh = cx.spawn_in(window, async move |lsp_tool, cx| { + cx.background_executor() + .timer(Duration::from_millis(30)) + .await; + lsp_tool + .update_in(cx, |lsp_tool, window, cx| { + lsp_tool.regenerate_items(cx); + let menu = ContextMenu::build(window, cx, |menu, _, cx| { + state.update(cx, |state, cx| state.fill_menu(menu, cx)) + }); + lsp_tool.lsp_menu = Some(menu.clone()); + // TODO kb will this work? + // what about the selections? + lsp_tool.popover_menu_handle.refresh_menu( + window, + cx, + Rc::new(move |_, _| Some(menu.clone())), + ); + cx.notify(); + }) + .ok(); + }); + } } } @@ -805,7 +734,7 @@ impl StatusItemView for LspTool { if let Some(editor) = active_pane_item.and_then(|item| item.downcast::()) { if Some(&editor) != self - .state + .server_state .read(cx) .active_editor .as_ref() @@ -819,25 +748,24 @@ impl StatusItemView for LspTool { window, |lsp_tool, _, e: &EditorEvent, window, cx| match e { EditorEvent::ExcerptsAdded { buffer, .. } => { - lsp_tool.state.update(cx, |state, cx| { + let updated = lsp_tool.server_state.update(cx, |state, cx| { if let Some(active_editor) = state.active_editor.as_mut() { let buffer_id = buffer.read(cx).remote_id(); - if active_editor.editor_buffers.insert(buffer_id) { - if let Some(picker) = &lsp_tool.lsp_picker { - picker.update(cx, |picker, cx| { - picker.refresh(window, cx) - }); - } - } + active_editor.editor_buffers.insert(buffer_id) + } else { + false } }); + if updated { + lsp_tool.refresh_lsp_menu(false, window, cx); + } } EditorEvent::ExcerptsRemoved { removed_buffer_ids, .. } => { - lsp_tool.state.update(cx, |state, cx| { + let removed = lsp_tool.server_state.update(cx, |state, _| { + let mut removed = false; if let Some(active_editor) = state.active_editor.as_mut() { - let mut removed = false; for id in removed_buffer_ids { active_editor.editor_buffers.retain(|buffer_id| { let retain = buffer_id != id; @@ -845,68 +773,53 @@ impl StatusItemView for LspTool { retain }); } - if removed { - if let Some(picker) = &lsp_tool.lsp_picker { - picker.update(cx, |picker, cx| { - picker.refresh(window, cx) - }); - } - } } + removed }); + if removed { + lsp_tool.refresh_lsp_menu(false, window, cx); + } } _ => {} }, ); - self.state.update(cx, |state, _| { + self.server_state.update(cx, |state, _| { state.active_editor = Some(ActiveEditor { editor: editor.downgrade(), _editor_subscription, editor_buffers, }); }); - - let lsp_picker = Self::new_lsp_picker(self.state.clone(), window, cx); - self.lsp_picker = Some(lsp_picker.clone()); - lsp_picker.update(cx, |lsp_picker, cx| lsp_picker.refresh(window, cx)); + self.refresh_lsp_menu(true, window, cx); } - } else if self.state.read(cx).active_editor.is_some() { - self.state.update(cx, |state, _| { + } else if self.server_state.read(cx).active_editor.is_some() { + self.server_state.update(cx, |state, _| { state.active_editor = None; }); - if let Some(lsp_picker) = self.lsp_picker.as_ref() { - lsp_picker.update(cx, |lsp_picker, cx| { - lsp_picker.refresh(window, cx); - }); - }; + self.refresh_lsp_menu(false, window, cx); } - } else if self.state.read(cx).active_editor.is_some() { - self.state.update(cx, |state, _| { + } else if self.server_state.read(cx).active_editor.is_some() { + self.server_state.update(cx, |state, _| { state.active_editor = None; }); - if let Some(lsp_picker) = self.lsp_picker.as_ref() { - lsp_picker.update(cx, |lsp_picker, cx| { - lsp_picker.refresh(window, cx); - }); - } + self.refresh_lsp_menu(false, window, cx); } } } impl Render for LspTool { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { - if !cx.is_staff() || self.state.read(cx).language_servers.is_empty() { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl ui::IntoElement { + if !cx.is_staff() + || self.server_state.read(cx).language_servers.is_empty() + || self.lsp_menu.is_none() + { return div(); } - let Some(lsp_picker) = self.lsp_picker.clone() else { - return div(); - }; - let mut has_errors = false; let mut has_warnings = false; let mut has_other_notifications = false; - let state = self.state.read(cx); + let state = self.server_state.read(cx); for server in state.language_servers.health_statuses.values() { if let Some(binary_status) = &state.language_servers.binary_statuses.get(&server.name) { has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. }); @@ -933,19 +846,21 @@ impl Render for LspTool { None }; + let lsp_tool = cx.entity().clone(); div().child( - PickerPopoverMenu::new( - lsp_picker.clone(), - IconButton::new("zed-lsp-tool-button", IconName::BoltFilledAlt) - .when_some(indicator, IconButton::indicator) - .icon_size(IconSize::Small) - .indicator_border_color(Some(cx.theme().colors().status_bar_background)), - move |window, cx| Tooltip::for_action("Language Servers", &ToggleMenu, window, cx), - Corner::BottomLeft, - cx, - ) - .with_handle(self.popover_menu_handle.clone()) - .render(window, cx), + PopoverMenu::new("lsp-tool") + .menu(move |_, cx| lsp_tool.read(cx).lsp_menu.clone()) + .anchor(Corner::BottomLeft) + .with_handle(self.popover_menu_handle.clone()) + .trigger_with_tooltip( + IconButton::new("zed-lsp-tool-button", IconName::BoltFilledAlt) + .when_some(indicator, IconButton::indicator) + .icon_size(IconSize::Small) + .indicator_border_color(Some(cx.theme().colors().status_bar_background)), + move |window, cx| { + Tooltip::for_action("Language Servers", &ToggleMenu, window, cx) + }, + ), ) } } diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index d7080f21f4f374777fab03104dad339d250f2d2a..075cf7a7d7a881fc308b0d2a7dcee3c9bdabcd57 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -24,6 +24,7 @@ pub enum ContextMenuItem { entry_render: Box AnyElement>, handler: Rc, &mut Window, &mut App)>, selectable: bool, + documentation_aside: Option, }, } @@ -31,11 +32,13 @@ impl ContextMenuItem { pub fn custom_entry( entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static, handler: impl Fn(&mut Window, &mut App) + 'static, + documentation_aside: Option, ) -> Self { Self::CustomEntry { entry_render: Box::new(entry_render), handler: Rc::new(move |_, window, cx| handler(window, cx)), selectable: true, + documentation_aside, } } } @@ -170,6 +173,12 @@ pub struct DocumentationAside { render: Rc AnyElement>, } +impl DocumentationAside { + pub fn new(side: DocumentationSide, render: Rc AnyElement>) -> Self { + Self { side, render } + } +} + impl Focusable for ContextMenu { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() @@ -456,6 +465,7 @@ impl ContextMenu { entry_render: Box::new(entry_render), handler: Rc::new(|_, _, _| {}), selectable: false, + documentation_aside: None, }); self } @@ -469,6 +479,7 @@ impl ContextMenu { entry_render: Box::new(entry_render), handler: Rc::new(move |_, window, cx| handler(window, cx)), selectable: true, + documentation_aside: None, }); self } @@ -705,10 +716,19 @@ impl ContextMenu { let item = self.items.get(ix)?; if item.is_selectable() { self.selected_index = Some(ix); - if let ContextMenuItem::Entry(entry) = item { - if let Some(callback) = &entry.documentation_aside { + match item { + ContextMenuItem::Entry(entry) => { + if let Some(callback) = &entry.documentation_aside { + self.documentation_aside = Some((ix, callback.clone())); + } + } + ContextMenuItem::CustomEntry { + documentation_aside: Some(callback), + .. + } => { self.documentation_aside = Some((ix, callback.clone())); } + _ => (), } } Some(ix) @@ -806,6 +826,7 @@ impl ContextMenu { entry_render, handler, selectable, + .. } => { let handler = handler.clone(); let menu = cx.entity().downgrade(); diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index 077c18f69e5476d8d47a85f5dd9664b3284c6681..55ce0218c75d4450067a5c09c2ea523f7d86ca3c 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -105,6 +105,24 @@ impl PopoverMenuHandle { .map_or(false, |model| model.focus_handle(cx).is_focused(window)) }) } + + pub fn refresh_menu( + &self, + window: &mut Window, + cx: &mut App, + new_menu_builder: Rc Option>>, + ) { + let show_menu = if let Some(state) = self.0.borrow_mut().as_mut() { + state.menu_builder = new_menu_builder; + state.menu.borrow().is_some() + } else { + false + }; + + if show_menu { + self.show(window, cx); + } + } } pub struct PopoverMenu { From f785853239ab2344cf1677b89cdf039ec3b6877c Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Mon, 7 Jul 2025 10:55:37 -0400 Subject: [PATCH 059/239] ssh: Fix incorrect handling of ssh paths that exist locally (#33743) - Closes: https://github.com/zed-industries/zed/issues/33733 I also tested that remote canonicalization of symlink directories still works. (e.g. `zed ssh://hostname/~/foo` where `foo -> foobar` will open `~/foobar` on the remote). I believe this has been broken since 2024-10-11 from https://github.com/zed-industries/zed/pull/19057. CC: @SomeoneToIgnore. I guess I'm the only person silly enough to run `zed ssh://hostname/tmp`. Release Notes: - ssh: Fixed an issue where Zed incorrectly canonicalized paths locally prior to connecting to the ssh remote. --- crates/zed/src/main.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 89d9c2edf127ff4b75f3100be3b8600dbaa89c06..e04e9c38c15b7ed1bc95bffc0d702b013150b3a5 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -727,11 +727,10 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut if let Some(connection_options) = request.ssh_connection { cx.spawn(async move |mut cx| { - let paths_with_position = - derive_paths_with_position(app_state.fs.as_ref(), request.open_paths).await; + let paths: Vec = request.open_paths.into_iter().map(PathBuf::from).collect(); open_ssh_project( connection_options, - paths_with_position.into_iter().map(|p| p.path).collect(), + paths, app_state, workspace::OpenOptions::default(), &mut cx, From 861ca05fb91f6bfbdd0e09fa832d0d129ce9b407 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Mon, 7 Jul 2025 10:56:38 -0400 Subject: [PATCH 060/239] Support loading environment from plan9 `rc` shell (#33599) Closes: https://github.com/zed-industries/zed/issues/33511 Add support for loading environment from Plan9 shell Document esoteric shell behavior. Remove two useless tests. Follow-up to: - https://github.com/zed-industries/zed/pull/32702 - https://github.com/zed-industries/zed/pull/32637 Release Notes: - Add support for loading environment variables from Plan9 `rc` shell. --- crates/util/src/shell_env.rs | 12 ++++++---- crates/util/src/util.rs | 46 ------------------------------------ 2 files changed, 7 insertions(+), 51 deletions(-) diff --git a/crates/util/src/shell_env.rs b/crates/util/src/shell_env.rs index 9e42ebe500ba372806731b799b15afd4b44429b4..21f6096f19fa0c89bf4516b122878be04361ddcd 100644 --- a/crates/util/src/shell_env.rs +++ b/crates/util/src/shell_env.rs @@ -16,8 +16,13 @@ pub fn capture(directory: &std::path::Path) -> Result format!(">[1={}]", ENV_OUTPUT_FD), // `[1=0]` + _ => format!(">&{}", ENV_OUTPUT_FD), // `>&0` + }; command.stdin(Stdio::null()); command.stdout(Stdio::piped()); command.stderr(Stdio::piped()); @@ -38,10 +43,7 @@ pub fn capture(directory: &std::path::Path) -> Result&{}\";", - zed_path, ENV_OUTPUT_FD - )); + command_string.push_str(&format!("{} --printenv {}", zed_path, redir)); command.args(["-i", "-c", &command_string]); super::set_pre_exec_to_start_new_session(&mut command); diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 86bee7ffd14c1782f806ebfe4dbe0537b675a5bc..932b519b18d4c555a2ee6189eef5744b0f85829e 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -1097,52 +1097,6 @@ mod tests { assert_eq!(vec, &[1000, 101, 21, 19, 17, 13, 9, 8]); } - #[test] - fn test_get_shell_safe_zed_path_with_spaces() { - // Test that shlex::try_quote handles paths with spaces correctly - let path_with_spaces = "/Applications/Zed Nightly.app/Contents/MacOS/zed"; - let quoted = shlex::try_quote(path_with_spaces).unwrap(); - - // The quoted path should be properly escaped for shell use - assert!(quoted.contains(path_with_spaces)); - - // When used in a shell command, it should not be split at spaces - let command = format!("sh -c '{} --printenv'", quoted); - println!("Command would be: {}", command); - - // Test that shlex can parse it back correctly - let parsed = shlex::split(&format!("{} --printenv", quoted)).unwrap(); - assert_eq!(parsed.len(), 2); - assert_eq!(parsed[0], path_with_spaces); - assert_eq!(parsed[1], "--printenv"); - } - - #[test] - fn test_shell_command_construction_with_quoted_path() { - // Test the specific pattern used in shell_env.rs to ensure proper quoting - let path_with_spaces = "/Applications/Zed Nightly.app/Contents/MacOS/zed"; - let quoted_path = shlex::try_quote(path_with_spaces).unwrap(); - - // This should be: '/Applications/Zed Nightly.app/Contents/MacOS/zed' - assert_eq!( - quoted_path, - "'/Applications/Zed Nightly.app/Contents/MacOS/zed'" - ); - - // Test the command construction pattern from shell_env.rs - // The fixed version should use double quotes around the entire sh -c argument - let env_fd = 0; - let command = format!("sh -c \"{} --printenv >&{}\";", quoted_path, env_fd); - - // This should produce: sh -c "'/Applications/Zed Nightly.app/Contents/MacOS/zed' --printenv >&0"; - let expected = - "sh -c \"'/Applications/Zed Nightly.app/Contents/MacOS/zed' --printenv >&0\";"; - assert_eq!(command, expected); - - // The command should not contain the problematic double single-quote pattern - assert!(!command.contains("''")); - } - #[test] fn test_truncate_to_bottom_n_sorted_by() { let mut vec: Vec = vec![5, 2, 3, 4, 1]; From 966e75b610970af43643f4a5d309aa18c2434064 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Mon, 7 Jul 2025 18:34:14 +0300 Subject: [PATCH 061/239] tools: Ensure `properties` always exists in JSON Schema (#34015) OpenAI API requires `properties` to be present, even if it's empty. Release Notes: - N/A --- crates/assistant_tool/src/tool_schema.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/assistant_tool/src/tool_schema.rs b/crates/assistant_tool/src/tool_schema.rs index 001b16ac87f02d3783d606ec3bc8d69a0cefd5a0..7b48f93ba6d23bcc1a6e2cf051737efaf69fa595 100644 --- a/crates/assistant_tool/src/tool_schema.rs +++ b/crates/assistant_tool/src/tool_schema.rs @@ -25,10 +25,15 @@ fn preprocess_json_schema(json: &mut Value) -> Result<()> { // `additionalProperties` defaults to `false` unless explicitly specified. // This prevents models from hallucinating tool parameters. if let Value::Object(obj) = json { - if let Some(Value::String(type_str)) = obj.get("type") { - if type_str == "object" && !obj.contains_key("additionalProperties") { + if matches!(obj.get("type"), Some(Value::String(s)) if s == "object") { + if !obj.contains_key("additionalProperties") { obj.insert("additionalProperties".to_string(), Value::Bool(false)); } + + // OpenAI API requires non-missing `properties` + if !obj.contains_key("properties") { + obj.insert("properties".to_string(), Value::Object(Default::default())); + } } } Ok(()) From 6cb382c49f91bab5cb6e5467f8b35655bf4c386b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:40:14 +0200 Subject: [PATCH 062/239] debugger: Make exception breakpoints persistent (#34014) Closes #33053 Release Notes: - Exception breakpoint state is now persisted across debugging sessions. --- .../src/session/running/breakpoint_list.rs | 117 ++++++++++++++---- crates/project/src/debugger/dap_store.rs | 117 ++++++++---------- crates/project/src/debugger/session.rs | 60 +++++---- 3 files changed, 178 insertions(+), 116 deletions(-) diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index 2ec20c9877ab38642fa12aa6cc2a61256dd6ab26..78c87db2e6f2a1f9d54368b875d1e86b3ac5789f 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -5,7 +5,8 @@ use std::{ time::Duration, }; -use dap::{Capabilities, ExceptionBreakpointsFilter}; +use dap::{Capabilities, ExceptionBreakpointsFilter, adapters::DebugAdapterName}; +use db::kvp::KEY_VALUE_STORE; use editor::Editor; use gpui::{ Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, @@ -16,6 +17,7 @@ use project::{ Project, debugger::{ breakpoint_store::{BreakpointEditAction, BreakpointStore, SourceBreakpoint}, + dap_store::{DapStore, PersistedAdapterOptions}, session::Session, }, worktree_store::WorktreeStore, @@ -48,6 +50,7 @@ pub(crate) enum SelectedBreakpointKind { pub(crate) struct BreakpointList { workspace: WeakEntity, breakpoint_store: Entity, + dap_store: Entity, worktree_store: Entity, scrollbar_state: ScrollbarState, breakpoints: Vec, @@ -59,6 +62,7 @@ pub(crate) struct BreakpointList { selected_ix: Option, input: Entity, strip_mode: Option, + serialize_exception_breakpoints_task: Option>>, } impl Focusable for BreakpointList { @@ -85,24 +89,34 @@ impl BreakpointList { let project = project.read(cx); let breakpoint_store = project.breakpoint_store(); let worktree_store = project.worktree_store(); + let dap_store = project.dap_store(); let focus_handle = cx.focus_handle(); let scroll_handle = UniformListScrollHandle::new(); let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); - cx.new(|cx| Self { - breakpoint_store, - worktree_store, - scrollbar_state, - breakpoints: Default::default(), - hide_scrollbar_task: None, - show_scrollbar: false, - workspace, - session, - focus_handle, - scroll_handle, - selected_ix: None, - input: cx.new(|cx| Editor::single_line(window, cx)), - strip_mode: None, + let adapter_name = session.as_ref().map(|session| session.read(cx).adapter()); + cx.new(|cx| { + let this = Self { + breakpoint_store, + dap_store, + worktree_store, + scrollbar_state, + breakpoints: Default::default(), + hide_scrollbar_task: None, + show_scrollbar: false, + workspace, + session, + focus_handle, + scroll_handle, + selected_ix: None, + input: cx.new(|cx| Editor::single_line(window, cx)), + strip_mode: None, + serialize_exception_breakpoints_task: None, + }; + if let Some(name) = adapter_name { + _ = this.deserialize_exception_breakpoints(name, cx); + } + this }) } @@ -404,12 +418,8 @@ impl BreakpointList { self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx); } BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => { - if let Some(session) = &self.session { - let id = exception_breakpoint.id.clone(); - session.update(cx, |session, cx| { - session.toggle_exception_breakpoint(&id, cx); - }); - } + let id = exception_breakpoint.id.clone(); + self.toggle_exception_breakpoint(&id, cx); } } cx.notify(); @@ -480,6 +490,64 @@ impl BreakpointList { cx.notify(); } + fn toggle_exception_breakpoint(&mut self, id: &str, cx: &mut Context) { + if let Some(session) = &self.session { + session.update(cx, |this, cx| { + this.toggle_exception_breakpoint(&id, cx); + }); + cx.notify(); + const EXCEPTION_SERIALIZATION_INTERVAL: Duration = Duration::from_secs(1); + self.serialize_exception_breakpoints_task = Some(cx.spawn(async move |this, cx| { + cx.background_executor() + .timer(EXCEPTION_SERIALIZATION_INTERVAL) + .await; + this.update(cx, |this, cx| this.serialize_exception_breakpoints(cx))? + .await?; + Ok(()) + })); + } + } + + fn kvp_key(adapter_name: &str) -> String { + format!("debug_adapter_`{adapter_name}`_persistence") + } + fn serialize_exception_breakpoints( + &mut self, + cx: &mut Context, + ) -> Task> { + if let Some(session) = self.session.as_ref() { + let key = { + let session = session.read(cx); + let name = session.adapter().0; + Self::kvp_key(&name) + }; + let settings = self.dap_store.update(cx, |this, cx| { + this.sync_adapter_options(session, cx); + }); + let value = serde_json::to_string(&settings); + + cx.background_executor() + .spawn(async move { KEY_VALUE_STORE.write_kvp(key, value?).await }) + } else { + return Task::ready(Result::Ok(())); + } + } + + fn deserialize_exception_breakpoints( + &self, + adapter_name: DebugAdapterName, + cx: &mut Context, + ) -> anyhow::Result<()> { + let Some(val) = KEY_VALUE_STORE.read_kvp(&Self::kvp_key(&adapter_name))? else { + return Ok(()); + }; + let value: PersistedAdapterOptions = serde_json::from_str(&val)?; + self.dap_store + .update(cx, |this, _| this.set_adapter_options(adapter_name, value)); + + Ok(()) + } + fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context) { const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| { @@ -988,12 +1056,7 @@ impl ExceptionBreakpoint { let list = list.clone(); move |_, _, cx| { list.update(cx, |this, cx| { - if let Some(session) = &this.session { - session.update(cx, |this, cx| { - this.toggle_exception_breakpoint(&id, cx); - }); - cx.notify(); - } + this.toggle_exception_breakpoint(&id, cx); }) .ok(); } diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index be4964bbee2688c0025900c552eec3fbbc9af492..29555d0179a41448131aecad8ebea610f2321c1d 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -14,15 +14,13 @@ use anyhow::{Context as _, Result, anyhow}; use async_trait::async_trait; use collections::HashMap; use dap::{ - Capabilities, CompletionItem, CompletionsArguments, DapRegistry, DebugRequest, - EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, Source, StackFrameId, + Capabilities, DapRegistry, DebugRequest, EvaluateArgumentsContext, StackFrameId, adapters::{ DapDelegate, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments, }, client::SessionId, inline_value::VariableLookupKind, messages::Message, - requests::{Completions, Evaluate}, }; use fs::Fs; use futures::{ @@ -40,6 +38,7 @@ use rpc::{ AnyProtoClient, TypedEnvelope, proto::{self}, }; +use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsLocation, WorktreeId}; use std::{ borrow::Borrow, @@ -93,10 +92,23 @@ pub struct DapStore { worktree_store: Entity, sessions: BTreeMap>, next_session_id: u32, + adapter_options: BTreeMap>, } impl EventEmitter for DapStore {} +#[derive(Clone, Serialize, Deserialize)] +pub struct PersistedExceptionBreakpoint { + pub enabled: bool, +} + +/// Represents best-effort serialization of adapter state during last session (e.g. watches) +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct PersistedAdapterOptions { + /// Which exception breakpoints were enabled during the last session with this adapter? + pub exception_breakpoints: BTreeMap, +} + impl DapStore { pub fn init(client: &AnyProtoClient, cx: &mut App) { static ADD_LOCATORS: Once = Once::new(); @@ -173,6 +185,7 @@ impl DapStore { breakpoint_store, worktree_store, sessions: Default::default(), + adapter_options: Default::default(), } } @@ -520,65 +533,6 @@ impl DapStore { )) } - pub fn evaluate( - &self, - session_id: &SessionId, - stack_frame_id: u64, - expression: String, - context: EvaluateArgumentsContext, - source: Option, - cx: &mut Context, - ) -> Task> { - let Some(client) = self - .session_by_id(session_id) - .and_then(|client| client.read(cx).adapter_client()) - else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id))); - }; - - cx.background_executor().spawn(async move { - client - .request::(EvaluateArguments { - expression: expression.clone(), - frame_id: Some(stack_frame_id), - context: Some(context), - format: None, - line: None, - column: None, - source, - }) - .await - }) - } - - pub fn completions( - &self, - session_id: &SessionId, - stack_frame_id: u64, - text: String, - completion_column: u64, - cx: &mut Context, - ) -> Task>> { - let Some(client) = self - .session_by_id(session_id) - .and_then(|client| client.read(cx).adapter_client()) - else { - return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id))); - }; - - cx.background_executor().spawn(async move { - Ok(client - .request::(CompletionsArguments { - frame_id: Some(stack_frame_id), - line: None, - text, - column: completion_column, - }) - .await? - .targets) - }) - } - pub fn resolve_inline_value_locations( &self, session: Entity, @@ -853,6 +807,45 @@ impl DapStore { }) }) } + + pub fn sync_adapter_options( + &mut self, + session: &Entity, + cx: &App, + ) -> Arc { + let session = session.read(cx); + let adapter = session.adapter(); + let exceptions = session.exception_breakpoints(); + let exception_breakpoints = exceptions + .map(|(exception, enabled)| { + ( + exception.filter.clone(), + PersistedExceptionBreakpoint { enabled: *enabled }, + ) + }) + .collect(); + let options = Arc::new(PersistedAdapterOptions { + exception_breakpoints, + }); + self.adapter_options.insert(adapter, options.clone()); + options + } + + pub fn set_adapter_options( + &mut self, + adapter: DebugAdapterName, + options: PersistedAdapterOptions, + ) { + self.adapter_options.insert(adapter, Arc::new(options)); + } + + pub fn adapter_options(&self, name: &str) -> Option> { + self.adapter_options.get(name).cloned() + } + + pub fn all_adapter_options(&self) -> &BTreeMap> { + &self.adapter_options + } } #[derive(Clone)] diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index bd52c0f6fa6f7c77baaa7fa052cd7499b44f0858..9ab83610f02cdeb0661062d04dc1b3d6fa3013be 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -409,17 +409,6 @@ impl RunningMode { }; let configuration_done_supported = ConfigurationDone::is_supported(capabilities); - let exception_filters = capabilities - .exception_breakpoint_filters - .as_ref() - .map(|exception_filters| { - exception_filters - .iter() - .filter(|filter| filter.default == Some(true)) - .cloned() - .collect::>() - }) - .unwrap_or_default(); // From spec (on initialization sequence): // client sends a setExceptionBreakpoints request if one or more exceptionBreakpointFilters have been defined (or if supportsConfigurationDoneRequest is not true) // @@ -434,10 +423,20 @@ impl RunningMode { .unwrap_or_default(); let this = self.clone(); let worktree = self.worktree().clone(); + let mut filters = capabilities + .exception_breakpoint_filters + .clone() + .unwrap_or_default(); let configuration_sequence = cx.spawn({ - async move |_, cx| { - let breakpoint_store = - dap_store.read_with(cx, |dap_store, _| dap_store.breakpoint_store().clone())?; + async move |session, cx| { + let adapter_name = session.read_with(cx, |this, _| this.adapter())?; + let (breakpoint_store, adapter_defaults) = + dap_store.read_with(cx, |dap_store, _| { + ( + dap_store.breakpoint_store().clone(), + dap_store.adapter_options(&adapter_name), + ) + })?; initialized_rx.await?; let errors_by_path = cx .update(|cx| this.send_source_breakpoints(false, &breakpoint_store, cx))? @@ -471,7 +470,25 @@ impl RunningMode { })?; if should_send_exception_breakpoints { - this.send_exception_breakpoints(exception_filters, supports_exception_filters) + _ = session.update(cx, |this, _| { + filters.retain(|filter| { + let is_enabled = if let Some(defaults) = adapter_defaults.as_ref() { + defaults + .exception_breakpoints + .get(&filter.filter) + .map(|options| options.enabled) + .unwrap_or_else(|| filter.default.unwrap_or_default()) + } else { + filter.default.unwrap_or_default() + }; + this.exception_breakpoints + .entry(filter.filter.clone()) + .or_insert_with(|| (filter.clone(), is_enabled)); + is_enabled + }); + }); + + this.send_exception_breakpoints(filters, supports_exception_filters) .await .ok(); } @@ -1233,18 +1250,7 @@ impl Session { Ok(capabilities) => { this.update(cx, |session, cx| { session.capabilities = capabilities; - let filters = session - .capabilities - .exception_breakpoint_filters - .clone() - .unwrap_or_default(); - for filter in filters { - let default = filter.default.unwrap_or_default(); - session - .exception_breakpoints - .entry(filter.filter.clone()) - .or_insert_with(|| (filter, default)); - } + cx.emit(SessionEvent::CapabilitiesLoaded); })?; return Ok(()); From c35af6c2e2225c055d106b394c346eea02de8996 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Mon, 7 Jul 2025 11:51:45 -0400 Subject: [PATCH 063/239] Fix panic with Helix mode changing case (#34016) Closes #33750 Release Notes: - Fixed a panic when trying to change case in Helix mode --- crates/vim/src/normal/convert.rs | 41 ++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/crates/vim/src/normal/convert.rs b/crates/vim/src/normal/convert.rs index 25b425e847d67eb5bc3d58b1d0a2201581a1e03f..cf9498bec9d7ee796bd2d4d6830085eda9970ea5 100644 --- a/crates/vim/src/normal/convert.rs +++ b/crates/vim/src/normal/convert.rs @@ -212,7 +212,19 @@ impl Vim { } } - Mode::HelixNormal => {} + Mode::HelixNormal => { + if selection.is_empty() { + // Handle empty selection by operating on the whole word + let (word_range, _) = snapshot.surrounding_word(selection.start, false); + let word_start = snapshot.offset_to_point(word_range.start); + let word_end = snapshot.offset_to_point(word_range.end); + ranges.push(word_start..word_end); + cursor_positions.push(selection.start..selection.start); + } else { + ranges.push(selection.start..selection.end); + cursor_positions.push(selection.start..selection.end); + } + } Mode::Insert | Mode::Normal | Mode::Replace => { let start = selection.start; let mut end = start; @@ -245,12 +257,16 @@ impl Vim { }) }); }); - self.switch_mode(Mode::Normal, true, window, cx) + if self.mode != Mode::HelixNormal { + self.switch_mode(Mode::Normal, true, window, cx) + } } } #[cfg(test)] mod test { + use crate::test::VimTestContext; + use crate::{state::Mode, test::NeovimBackedTestContext}; #[gpui::test] @@ -419,4 +435,25 @@ mod test { .await .assert_eq("ˇnopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM"); } + + #[gpui::test] + async fn test_change_case_helix_mode(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // Explicit selection + cx.set_state("«hello worldˇ»", Mode::HelixNormal); + cx.simulate_keystrokes("~"); + cx.assert_state("«HELLO WORLDˇ»", Mode::HelixNormal); + + // Cursor-only (empty) selection + cx.set_state("The ˇquick brown", Mode::HelixNormal); + cx.simulate_keystrokes("~"); + cx.assert_state("The ˇQUICK brown", Mode::HelixNormal); + + // With `e` motion (which extends selection to end of word in Helix) + cx.set_state("The ˇquick brown fox", Mode::HelixNormal); + cx.simulate_keystrokes("e"); + cx.simulate_keystrokes("~"); + cx.assert_state("The «QUICKˇ» brown fox", Mode::HelixNormal); + } } From ddf3d992650d94f4986d8babd2fbf3ef51422e64 Mon Sep 17 00:00:00 2001 From: Hilmar Wiegand Date: Mon, 7 Jul 2025 18:18:55 +0200 Subject: [PATCH 064/239] Add g-w rewrap keybind for vim visual mode (#33853) There are both `g q` and `g w` keybinds for rewrapping in normal mode, but `g w` is missing in visual mode. This PR adds that keybind. Release Notes: - Add `g w` rewrap keybind for vim visual mode --- assets/keymaps/vim.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 639f1cefade18ee46771d22e08eda4a24f8696c0..ba3012cc54f7cc0af464357b6fb05c041130262d 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -327,6 +327,7 @@ "g shift-r": ["vim::Paste", { "preserve_clipboard": true }], "g c": "vim::ToggleComments", "g q": "vim::Rewrap", + "g w": "vim::Rewrap", "g ?": "vim::ConvertToRot13", // "g ?": "vim::ConvertToRot47", "\"": "vim::PushRegister", From de9053c7ca276adba5d1244fe9805bf331a3c646 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 7 Jul 2025 11:44:19 -0500 Subject: [PATCH 065/239] keymap_ui: Add ability to edit context (#34019) Closes #ISSUE Adds a context input to the keybind edit modal. Also fixes some bugs in the keymap update function to handle context changes gracefully. The current keybind update strategy implemented in this PR is * when the context doesn't change, just update the binding in place * when the context changes, but the binding is the only binding in the keymap section, update the binding _and_ context in place * when the context changes, and the binding is _not_ the only binding in the keymap section, remove the existing binding and create a new section with the update context and binding so as to avoid impacting other bindings Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/gpui/src/platform/keystroke.rs | 2 +- crates/settings/src/keymap_file.rs | 187 +++++++++++++++++++++++--- crates/settings_ui/src/keybindings.rs | 163 ++++++++++++++++------ 3 files changed, 288 insertions(+), 64 deletions(-) diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index 18adc1af10073001b82e0a72f8e372fafe4395b0..40387a820230cfc0f73f90643c082619ceaa595a 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -55,7 +55,7 @@ impl Keystroke { /// /// This method assumes that `self` was typed and `target' is in the keymap, and checks /// both possibilities for self against the target. - pub(crate) fn should_match(&self, target: &Keystroke) -> bool { + pub fn should_match(&self, target: &Keystroke) -> bool { #[cfg(not(target_os = "windows"))] if let Some(key_char) = self .key_char diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 4c4ceee49bcd0a90ac43329e6ecd6211a423ae65..ca54b6a877361af15a634ec7ce3c247ffeaff49f 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -604,7 +604,7 @@ impl KeymapFile { // if trying to replace a keybinding that is not user-defined, treat it as an add operation match operation { KeybindUpdateOperation::Replace { - target_source, + target_keybind_source: target_source, source, .. } if target_source != KeybindSource::User => { @@ -643,7 +643,12 @@ impl KeymapFile { else { continue; }; - if keystrokes != target.keystrokes { + if keystrokes.len() != target.keystrokes.len() + || !keystrokes + .iter() + .zip(target.keystrokes) + .all(|(a, b)| a.should_match(b)) + { continue; } if action.0 != target_action_value { @@ -655,18 +660,75 @@ impl KeymapFile { } if let Some(index) = found_index { - let (replace_range, replace_value) = replace_top_level_array_value_in_json_text( - &keymap_contents, - &["bindings", &target.keystrokes_unparsed()], - Some(&source_action_value), - Some(&source.keystrokes_unparsed()), - index, - tab_size, - ) - .context("Failed to replace keybinding")?; - keymap_contents.replace_range(replace_range, &replace_value); - - return Ok(keymap_contents); + if target.context == source.context { + // if we are only changing the keybinding (common case) + // not the context, etc. Then just update the binding in place + + let (replace_range, replace_value) = + replace_top_level_array_value_in_json_text( + &keymap_contents, + &["bindings", &target.keystrokes_unparsed()], + Some(&source_action_value), + Some(&source.keystrokes_unparsed()), + index, + tab_size, + ) + .context("Failed to replace keybinding")?; + keymap_contents.replace_range(replace_range, &replace_value); + + return Ok(keymap_contents); + } else if keymap.0[index] + .bindings + .as_ref() + .map_or(true, |bindings| bindings.len() == 1) + { + // if we are replacing the only binding in the section, + // just update the section in place, updating the context + // and the binding + + let (replace_range, replace_value) = + replace_top_level_array_value_in_json_text( + &keymap_contents, + &["bindings", &target.keystrokes_unparsed()], + Some(&source_action_value), + Some(&source.keystrokes_unparsed()), + index, + tab_size, + ) + .context("Failed to replace keybinding")?; + keymap_contents.replace_range(replace_range, &replace_value); + + let (replace_range, replace_value) = + replace_top_level_array_value_in_json_text( + &keymap_contents, + &["context"], + source.context.map(Into::into).as_ref(), + None, + index, + tab_size, + ) + .context("Failed to replace keybinding")?; + keymap_contents.replace_range(replace_range, &replace_value); + return Ok(keymap_contents); + } else { + // if we are replacing one of multiple bindings in a section + // with a context change, remove the existing binding from the + // section, then treat this operation as an add operation of the + // new binding with the updated context. + + let (replace_range, replace_value) = + replace_top_level_array_value_in_json_text( + &keymap_contents, + &["bindings", &target.keystrokes_unparsed()], + None, + None, + index, + tab_size, + ) + .context("Failed to replace keybinding")?; + keymap_contents.replace_range(replace_range, &replace_value); + operation = KeybindUpdateOperation::Add(source); + } } else { log::warn!( "Failed to find keybinding to update `{:?} -> {}` creating new binding for `{:?} -> {}` instead", @@ -712,7 +774,7 @@ pub enum KeybindUpdateOperation<'a> { source: KeybindUpdateTarget<'a>, /// Describes the keybind to remove target: KeybindUpdateTarget<'a>, - target_source: KeybindSource, + target_keybind_source: KeybindSource, }, Add(KeybindUpdateTarget<'a>), } @@ -1001,7 +1063,7 @@ mod tests { use_key_equivalents: false, input: Some(r#"{"foo": "bar"}"#), }, - target_source: KeybindSource::Base, + target_keybind_source: KeybindSource::Base, }, r#"[ { @@ -1027,14 +1089,14 @@ mod tests { r#"[ { "bindings": { - "ctrl-a": "zed::SomeAction" + "a": "zed::SomeAction" } } ]"# .unindent(), KeybindUpdateOperation::Replace { target: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-a"), + keystrokes: &parse_keystrokes("a"), action_name: "zed::SomeAction", context: None, use_key_equivalents: false, @@ -1047,7 +1109,7 @@ mod tests { use_key_equivalents: false, input: Some(r#"{"foo": "bar"}"#), }, - target_source: KeybindSource::User, + target_keybind_source: KeybindSource::User, }, r#"[ { @@ -1088,7 +1150,7 @@ mod tests { use_key_equivalents: false, input: None, }, - target_source: KeybindSource::User, + target_keybind_source: KeybindSource::User, }, r#"[ { @@ -1131,7 +1193,7 @@ mod tests { use_key_equivalents: false, input: Some(r#"{"foo": "bar"}"#), }, - target_source: KeybindSource::User, + target_keybind_source: KeybindSource::User, }, r#"[ { @@ -1149,5 +1211,88 @@ mod tests { ]"# .unindent(), ); + + check_keymap_update( + r#"[ + { + "context": "SomeContext", + "bindings": { + "a": "foo::bar", + "b": "baz::qux", + } + } + ]"# + .unindent(), + KeybindUpdateOperation::Replace { + target: KeybindUpdateTarget { + keystrokes: &parse_keystrokes("a"), + action_name: "foo::bar", + context: Some("SomeContext"), + use_key_equivalents: false, + input: None, + }, + source: KeybindUpdateTarget { + keystrokes: &parse_keystrokes("c"), + action_name: "foo::baz", + context: Some("SomeOtherContext"), + use_key_equivalents: false, + input: None, + }, + target_keybind_source: KeybindSource::User, + }, + r#"[ + { + "context": "SomeContext", + "bindings": { + "b": "baz::qux", + } + }, + { + "context": "SomeOtherContext", + "bindings": { + "c": "foo::baz" + } + } + ]"# + .unindent(), + ); + + check_keymap_update( + r#"[ + { + "context": "SomeContext", + "bindings": { + "a": "foo::bar", + } + } + ]"# + .unindent(), + KeybindUpdateOperation::Replace { + target: KeybindUpdateTarget { + keystrokes: &parse_keystrokes("a"), + action_name: "foo::bar", + context: Some("SomeContext"), + use_key_equivalents: false, + input: None, + }, + source: KeybindUpdateTarget { + keystrokes: &parse_keystrokes("c"), + action_name: "foo::baz", + context: Some("SomeOtherContext"), + use_key_equivalents: false, + input: None, + }, + target_keybind_source: KeybindSource::User, + }, + r#"[ + { + "context": "SomeOtherContext", + "bindings": { + "c": "foo::baz", + } + } + ]"# + .unindent(), + ); } } diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 1f5f4b1b7e18c7720227d6c04d7f8680e469c94b..34d4b8585256d12b62b725d360679225b7360a82 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -1,4 +1,7 @@ -use std::{ops::Range, sync::Arc}; +use std::{ + ops::{Not, Range}, + sync::Arc, +}; use anyhow::{Context as _, anyhow}; use collections::HashSet; @@ -824,6 +827,7 @@ impl RenderOnce for SyntaxHighlightedText { struct KeybindingEditorModal { editing_keybind: ProcessedKeybinding, keybind_editor: Entity, + context_editor: Entity, fs: Arc, error: Option, } @@ -842,17 +846,86 @@ impl KeybindingEditorModal { pub fn new( editing_keybind: ProcessedKeybinding, fs: Arc, - _window: &mut Window, + window: &mut Window, cx: &mut App, ) -> Self { let keybind_editor = cx.new(KeystrokeInput::new); + let context_editor = cx.new(|cx| { + let mut editor = Editor::single_line(window, cx); + if let Some(context) = editing_keybind + .context + .as_ref() + .and_then(KeybindContextString::local) + { + editor.set_text(context.clone(), window, cx); + } else { + editor.set_placeholder_text("Keybinding context", cx); + } + + editor + }); Self { editing_keybind, fs, keybind_editor, + context_editor, error: None, } } + + fn save(&mut self, cx: &mut Context) { + let existing_keybind = self.editing_keybind.clone(); + let fs = self.fs.clone(); + let new_keystrokes = self + .keybind_editor + .read_with(cx, |editor, _| editor.keystrokes().to_vec()); + if new_keystrokes.is_empty() { + self.error = Some("Keystrokes cannot be empty".to_string()); + cx.notify(); + return; + } + let tab_size = cx.global::().json_tab_size(); + let new_context = self + .context_editor + .read_with(cx, |editor, cx| editor.text(cx)); + let new_context = new_context.is_empty().not().then_some(new_context); + let new_context_err = new_context.as_deref().and_then(|context| { + gpui::KeyBindingContextPredicate::parse(context) + .context("Failed to parse key context") + .err() + }); + if let Some(err) = new_context_err { + // TODO: store and display as separate error + // TODO: also, should be validating on keystroke + self.error = Some(err.to_string()); + cx.notify(); + return; + } + + cx.spawn(async move |this, cx| { + if let Err(err) = save_keybinding_update( + existing_keybind, + &new_keystrokes, + new_context.as_deref(), + &fs, + tab_size, + ) + .await + { + this.update(cx, |this, cx| { + this.error = Some(err.to_string()); + cx.notify(); + }) + .log_err(); + } else { + this.update(cx, |_this, cx| { + cx.emit(DismissEvent); + }) + .ok(); + } + }) + .detach(); + } } impl Render for KeybindingEditorModal { @@ -868,14 +941,35 @@ impl Render for KeybindingEditorModal { .gap_2() .child( v_flex().child(Label::new("Edit Keystroke")).child( - Label::new( - "Input the desired keystroke for the selected action and hit save.", - ) - .color(Color::Muted), + Label::new("Input the desired keystroke for the selected action.") + .color(Color::Muted), ), ) .child(self.keybind_editor.clone()), ) + .child( + v_flex() + .p_3() + .gap_3() + .child( + v_flex().child(Label::new("Edit Keystroke")).child( + Label::new("Input the desired keystroke for the selected action.") + .color(Color::Muted), + ), + ) + .child( + div() + .w_full() + .border_color(cx.theme().colors().border_variant) + .border_1() + .py_2() + .px_3() + .min_h_8() + .rounded_md() + .bg(theme.editor_background) + .child(self.context_editor.clone()), + ), + ) .child( h_flex() .p_2() @@ -888,38 +982,11 @@ impl Render for KeybindingEditorModal { Button::new("cancel", "Cancel") .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), ) - .child(Button::new("save-btn", "Save").on_click(cx.listener( - |this, _event, _window, cx| { - let existing_keybind = this.editing_keybind.clone(); - let fs = this.fs.clone(); - let new_keystrokes = this - .keybind_editor - .read_with(cx, |editor, _| editor.keystrokes.clone()); - if new_keystrokes.is_empty() { - this.error = Some("Keystrokes cannot be empty".to_string()); - cx.notify(); - return; - } - let tab_size = cx.global::().json_tab_size(); - cx.spawn(async move |this, cx| { - if let Err(err) = save_keybinding_update( - existing_keybind, - &new_keystrokes, - &fs, - tab_size, - ) - .await - { - this.update(cx, |this, cx| { - this.error = Some(err.to_string()); - cx.notify(); - }) - .log_err(); - } - }) - .detach(); - }, - ))), + .child( + Button::new("save-btn", "Save").on_click( + cx.listener(|this, _event, _window, cx| Self::save(this, cx)), + ), + ), ) .when_some(self.error.clone(), |this, error| { this.child( @@ -937,6 +1004,7 @@ impl Render for KeybindingEditorModal { async fn save_keybinding_update( existing: ProcessedKeybinding, new_keystrokes: &[Keystroke], + new_context: Option<&str>, fs: &Arc, tab_size: usize, ) -> anyhow::Result<()> { @@ -950,7 +1018,7 @@ async fn save_keybinding_update( .map(|keybinding| keybinding.keystrokes.as_slice()) .unwrap_or_default(); - let context = existing + let existing_context = existing .context .as_ref() .and_then(KeybindContextString::local_str); @@ -963,18 +1031,18 @@ async fn save_keybinding_update( let operation = if existing.ui_key_binding.is_some() { settings::KeybindUpdateOperation::Replace { target: settings::KeybindUpdateTarget { - context, + context: existing_context, keystrokes: existing_keystrokes, action_name: &existing.action, use_key_equivalents: false, input, }, - target_source: existing + target_keybind_source: existing .source .map(|(source, _name)| source) .unwrap_or(KeybindSource::User), source: settings::KeybindUpdateTarget { - context, + context: new_context, keystrokes: new_keystrokes, action_name: &existing.action, use_key_equivalents: false, @@ -1071,6 +1139,17 @@ impl KeystrokeInput { cx.stop_propagation(); cx.notify(); } + + fn keystrokes(&self) -> &[Keystroke] { + if self + .keystrokes + .last() + .map_or(false, |last| last.key.is_empty()) + { + return &self.keystrokes[..self.keystrokes.len() - 1]; + } + return &self.keystrokes; + } } impl Focusable for KeystrokeInput { From d87603dd60d0cefe87da782a0a906b75c7fd5ab4 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Mon, 7 Jul 2025 19:48:18 +0300 Subject: [PATCH 066/239] agent: Send stale file notifications using the project_notifications tool (#34005) This commit introduces the `project_notifications` tool, which proactively pushes notifications to the agent. Unlike other tools, `Thread` automatically invokes this tool on every turn, even when the LLM doesn't ask for it. When notifications are available, the tool use and results are inserted into the thread, simulating an LLM tool call. As with other tools, users can disable `project_notifications` in Profiles if they do not want them. Currently, the tool only notifies users about stale files: that is, files that have been edited by the user while the agent is also working on them. In the future, notifications may be expanded to include compiler diagnostics, long-running processes, and more. Release Notes: - Added `project_notifications` tool --- assets/settings/default.json | 2 + .../src/prompts/stale_files_prompt_header.txt | 3 + crates/agent/src/thread.rs | 222 +++++++++++++++++- crates/assistant_tools/src/assistant_tools.rs | 3 + .../src/project_notifications_tool.rs | 193 +++++++++++++++ .../project_notifications_tool/description.md | 3 + .../prompt_header.txt | 3 + .../src/examples/file_change_notification.rs | 2 +- 8 files changed, 427 insertions(+), 4 deletions(-) create mode 100644 crates/agent/src/prompts/stale_files_prompt_header.txt create mode 100644 crates/assistant_tools/src/project_notifications_tool.rs create mode 100644 crates/assistant_tools/src/project_notifications_tool/description.md create mode 100644 crates/assistant_tools/src/project_notifications_tool/prompt_header.txt diff --git a/assets/settings/default.json b/assets/settings/default.json index 985e322cac2a2c4b6b807aeff24caeb68beacf89..48cdd665e1745fdcacb15b6317ade6ec2dd4480b 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -810,6 +810,7 @@ "edit_file": true, "fetch": true, "list_directory": true, + "project_notifications": true, "move_path": true, "now": true, "find_path": true, @@ -829,6 +830,7 @@ "diagnostics": true, "fetch": true, "list_directory": true, + "project_notifications": true, "now": true, "find_path": true, "read_file": true, diff --git a/crates/agent/src/prompts/stale_files_prompt_header.txt b/crates/agent/src/prompts/stale_files_prompt_header.txt new file mode 100644 index 0000000000000000000000000000000000000000..f743e239c883c7456f7bdc6e089185c6b994cb44 --- /dev/null +++ b/crates/agent/src/prompts/stale_files_prompt_header.txt @@ -0,0 +1,3 @@ +[The following is an auto-generated notification; do not reply] + +These files have changed since the last read: diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 815b9e86ea8a7c4c0879e81028c4ee42e3a84ca8..50d2a4d77383e4336bed6ba3fd6fc4a9e3e64ac1 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -25,8 +25,8 @@ use language_model::{ ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent, - LanguageModelToolUseId, MessageContent, ModelRequestLimitReachedError, PaymentRequiredError, - Role, SelectedModel, StopReason, TokenUsage, + LanguageModelToolUse, LanguageModelToolUseId, MessageContent, ModelRequestLimitReachedError, + PaymentRequiredError, Role, SelectedModel, StopReason, TokenUsage, }; use postage::stream::Stream as _; use project::{ @@ -45,7 +45,7 @@ use std::{ time::{Duration, Instant}, }; use thiserror::Error; -use util::{ResultExt as _, post_inc}; +use util::{ResultExt as _, debug_panic, post_inc}; use uuid::Uuid; use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit}; @@ -1248,6 +1248,8 @@ impl Thread { self.remaining_turns -= 1; + self.flush_notifications(model.clone(), intent, cx); + let request = self.to_completion_request(model.clone(), intent, cx); self.stream_completion(request, model, intent, window, cx); @@ -1481,6 +1483,110 @@ impl Thread { request } + /// Insert auto-generated notifications (if any) to the thread + fn flush_notifications( + &mut self, + model: Arc, + intent: CompletionIntent, + cx: &mut Context, + ) { + match intent { + CompletionIntent::UserPrompt | CompletionIntent::ToolResults => { + if let Some(pending_tool_use) = self.attach_tracked_files_state(model, cx) { + cx.emit(ThreadEvent::ToolFinished { + tool_use_id: pending_tool_use.id.clone(), + pending_tool_use: Some(pending_tool_use), + }); + } + } + CompletionIntent::ThreadSummarization + | CompletionIntent::ThreadContextSummarization + | CompletionIntent::CreateFile + | CompletionIntent::EditFile + | CompletionIntent::InlineAssist + | CompletionIntent::TerminalInlineAssist + | CompletionIntent::GenerateGitCommitMessage => {} + }; + } + + fn attach_tracked_files_state( + &mut self, + model: Arc, + cx: &mut App, + ) -> Option { + let action_log = self.action_log.read(cx); + + action_log.stale_buffers(cx).next()?; + + // Represent notification as a simulated `project_notifications` tool call + let tool_name = Arc::from("project_notifications"); + let Some(tool) = self.tools.read(cx).tool(&tool_name, cx) else { + debug_panic!("`project_notifications` tool not found"); + return None; + }; + + if !self.profile.is_tool_enabled(tool.source(), tool.name(), cx) { + return None; + } + + let input = serde_json::json!({}); + let request = Arc::new(LanguageModelRequest::default()); // unused + let window = None; + let tool_result = tool.run( + input, + request, + self.project.clone(), + self.action_log.clone(), + model.clone(), + window, + cx, + ); + + let tool_use_id = + LanguageModelToolUseId::from(format!("project_notifications_{}", self.messages.len())); + + let tool_use = LanguageModelToolUse { + id: tool_use_id.clone(), + name: tool_name.clone(), + raw_input: "{}".to_string(), + input: serde_json::json!({}), + is_input_complete: true, + }; + + let tool_output = cx.background_executor().block(tool_result.output); + + // Attach a project_notification tool call to the latest existing + // Assistant message. We cannot create a new Assistant message + // because thinking models require a `thinking` block that we + // cannot mock. We cannot send a notification as a normal + // (non-tool-use) User message because this distracts Agent + // too much. + let tool_message_id = self + .messages + .iter() + .enumerate() + .rfind(|(_, message)| message.role == Role::Assistant) + .map(|(_, message)| message.id)?; + + let tool_use_metadata = ToolUseMetadata { + model: model.clone(), + thread_id: self.id.clone(), + prompt_id: self.last_prompt_id.clone(), + }; + + self.tool_use + .request_tool_use(tool_message_id, tool_use, tool_use_metadata.clone(), cx); + + let pending_tool_use = self.tool_use.insert_tool_output( + tool_use_id.clone(), + tool_name, + tool_output, + self.configured_model.as_ref(), + ); + + pending_tool_use + } + pub fn stream_completion( &mut self, request: LanguageModelRequest, @@ -3156,10 +3262,13 @@ mod tests { const TEST_RATE_LIMIT_RETRY_SECS: u64 = 30; use agent_settings::{AgentProfileId, AgentSettings, LanguageModelParameters}; use assistant_tool::ToolRegistry; + use assistant_tools; use futures::StreamExt; use futures::future::BoxFuture; use futures::stream::BoxStream; use gpui::TestAppContext; + use http_client; + use indoc::indoc; use language_model::fake_provider::{FakeLanguageModel, FakeLanguageModelProvider}; use language_model::{ LanguageModelCompletionError, LanguageModelName, LanguageModelProviderId, @@ -3487,6 +3596,105 @@ fn main() {{ ); } + #[gpui::test] + async fn test_stale_buffer_notification(cx: &mut TestAppContext) { + init_test_settings(cx); + + let project = create_test_project( + cx, + json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}), + ) + .await; + + let (_workspace, _thread_store, thread, context_store, model) = + setup_test_environment(cx, project.clone()).await; + + // Add a buffer to the context. This will be a tracked buffer + let buffer = add_file_to_context(&project, &context_store, "test/code.rs", cx) + .await + .unwrap(); + + let context = context_store + .read_with(cx, |store, _| store.context().next().cloned()) + .unwrap(); + let loaded_context = cx + .update(|cx| load_context(vec![context], &project, &None, cx)) + .await; + + // Insert user message and assistant response + thread.update(cx, |thread, cx| { + thread.insert_user_message("Explain this code", loaded_context, None, Vec::new(), cx); + thread.insert_assistant_message( + vec![MessageSegment::Text("This code prints 42.".into())], + cx, + ); + }); + + // We shouldn't have a stale buffer notification yet + let notification = thread.read_with(cx, |thread, _| { + find_tool_use(thread, "project_notifications") + }); + assert!( + notification.is_none(), + "Should not have stale buffer notification before buffer is modified" + ); + + // Modify the buffer + buffer.update(cx, |buffer, cx| { + buffer.edit( + [(1..1, "\n println!(\"Added a new line\");\n")], + None, + cx, + ); + }); + + // Insert another user message + thread.update(cx, |thread, cx| { + thread.insert_user_message( + "What does the code do now?", + ContextLoadResult::default(), + None, + Vec::new(), + cx, + ) + }); + + // Check for the stale buffer warning + thread.update(cx, |thread, cx| { + thread.flush_notifications(model.clone(), CompletionIntent::UserPrompt, cx) + }); + + let Some(notification_result) = thread.read_with(cx, |thread, _cx| { + find_tool_use(thread, "project_notifications") + }) else { + panic!("Should have a `project_notifications` tool use"); + }; + + let Some(notification_content) = notification_result.content.to_str() else { + panic!("`project_notifications` should return text"); + }; + + let expected_content = indoc! {"[The following is an auto-generated notification; do not reply] + + These files have changed since the last read: + - code.rs + "}; + assert_eq!(notification_content, expected_content); + } + + fn find_tool_use(thread: &Thread, tool_name: &str) -> Option { + thread + .messages() + .filter_map(|message| { + thread + .tool_results_for_message(message.id) + .into_iter() + .find(|result| result.tool_name == tool_name.into()) + }) + .next() + .cloned() + } + #[gpui::test] async fn test_storing_profile_setting_per_thread(cx: &mut TestAppContext) { init_test_settings(cx); @@ -5052,6 +5260,14 @@ fn main() {{ language_model::init_settings(cx); ThemeSettings::register(cx); ToolRegistry::default_global(cx); + assistant_tool::init(cx); + + let http_client = Arc::new(http_client::HttpClientWithUrl::new( + http_client::FakeHttpClient::with_200_response(), + "http://localhost".to_string(), + None, + )); + assistant_tools::init(http_client, cx); }); } diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs index 83312a07b625404085694194b92ee7c732a67998..eef792f526fb684e83752241194d293064a9f4f7 100644 --- a/crates/assistant_tools/src/assistant_tools.rs +++ b/crates/assistant_tools/src/assistant_tools.rs @@ -11,6 +11,7 @@ mod list_directory_tool; mod move_path_tool; mod now_tool; mod open_tool; +mod project_notifications_tool; mod read_file_tool; mod schema; mod templates; @@ -45,6 +46,7 @@ pub use edit_file_tool::{EditFileMode, EditFileToolInput}; pub use find_path_tool::FindPathToolInput; pub use grep_tool::{GrepTool, GrepToolInput}; pub use open_tool::OpenTool; +pub use project_notifications_tool::ProjectNotificationsTool; pub use read_file_tool::{ReadFileTool, ReadFileToolInput}; pub use terminal_tool::TerminalTool; @@ -61,6 +63,7 @@ pub fn init(http_client: Arc, cx: &mut App) { registry.register_tool(ListDirectoryTool); registry.register_tool(NowTool); registry.register_tool(OpenTool); + registry.register_tool(ProjectNotificationsTool); registry.register_tool(FindPathTool); registry.register_tool(ReadFileTool); registry.register_tool(GrepTool); diff --git a/crates/assistant_tools/src/project_notifications_tool.rs b/crates/assistant_tools/src/project_notifications_tool.rs new file mode 100644 index 0000000000000000000000000000000000000000..552ebb3d53e7453dbcaa4f363bd6e6ae5d2709b6 --- /dev/null +++ b/crates/assistant_tools/src/project_notifications_tool.rs @@ -0,0 +1,193 @@ +use crate::schema::json_schema_for; +use anyhow::Result; +use assistant_tool::{ActionLog, Tool, ToolResult}; +use gpui::{AnyWindowHandle, App, Entity, Task}; +use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; +use project::Project; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::fmt::Write as _; +use std::sync::Arc; +use ui::IconName; + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct ProjectUpdatesToolInput {} + +pub struct ProjectNotificationsTool; + +impl Tool for ProjectNotificationsTool { + fn name(&self) -> String { + "project_notifications".to_string() + } + + fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { + false + } + fn may_perform_edits(&self) -> bool { + false + } + fn description(&self) -> String { + include_str!("./project_notifications_tool/description.md").to_string() + } + + fn icon(&self) -> IconName { + IconName::Envelope + } + + fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { + json_schema_for::(format) + } + + fn ui_text(&self, _input: &serde_json::Value) -> String { + "Check project notifications".into() + } + + fn run( + self: Arc, + _input: serde_json::Value, + _request: Arc, + _project: Entity, + action_log: Entity, + _model: Arc, + _window: Option, + cx: &mut App, + ) -> ToolResult { + let mut stale_files = String::new(); + + let action_log = action_log.read(cx); + + for stale_file in action_log.stale_buffers(cx) { + if let Some(file) = stale_file.read(cx).file() { + writeln!(&mut stale_files, "- {}", file.path().display()).ok(); + } + } + + let response = if stale_files.is_empty() { + "No new notifications".to_string() + } else { + // NOTE: Changes to this prompt require a symmetric update in the LLM Worker + const HEADER: &str = include_str!("./project_notifications_tool/prompt_header.txt"); + format!("{HEADER}{stale_files}").replace("\r\n", "\n") + }; + + Task::ready(Ok(response.into())).into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use assistant_tool::ToolResultContent; + use gpui::{AppContext, TestAppContext}; + use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModelProvider}; + use project::{FakeFs, Project}; + use serde_json::json; + use settings::SettingsStore; + use std::sync::Arc; + use util::path; + + #[gpui::test] + async fn test_stale_buffer_notification(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/test"), + json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}), + ) + .await; + + let project = Project::test(fs, [path!("/test").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + + let buffer_path = project + .read_with(cx, |project, cx| { + project.find_project_path("test/code.rs", cx) + }) + .unwrap(); + + let buffer = project + .update(cx, |project, cx| { + project.open_buffer(buffer_path.clone(), cx) + }) + .await + .unwrap(); + + // Start tracking the buffer + action_log.update(cx, |log, cx| { + log.buffer_read(buffer.clone(), cx); + }); + + // Run the tool before any changes + let tool = Arc::new(ProjectNotificationsTool); + let provider = Arc::new(FakeLanguageModelProvider); + let model: Arc = Arc::new(provider.test_model()); + let request = Arc::new(LanguageModelRequest::default()); + let tool_input = json!({}); + + let result = cx.update(|cx| { + tool.clone().run( + tool_input.clone(), + request.clone(), + project.clone(), + action_log.clone(), + model.clone(), + None, + cx, + ) + }); + + let response = result.output.await.unwrap(); + let response_text = match &response.content { + ToolResultContent::Text(text) => text.clone(), + _ => panic!("Expected text response"), + }; + assert_eq!( + response_text.as_str(), + "No new notifications", + "Tool should return 'No new notifications' when no stale buffers" + ); + + // Modify the buffer (makes it stale) + buffer.update(cx, |buffer, cx| { + buffer.edit([(1..1, "\nChange!\n")], None, cx); + }); + + // Run the tool again + let result = cx.update(|cx| { + tool.run( + tool_input.clone(), + request.clone(), + project.clone(), + action_log, + model.clone(), + None, + cx, + ) + }); + + // This time the buffer is stale, so the tool should return a notification + let response = result.output.await.unwrap(); + let response_text = match &response.content { + ToolResultContent::Text(text) => text.clone(), + _ => panic!("Expected text response"), + }; + + let expected_content = "[The following is an auto-generated notification; do not reply]\n\nThese files have changed since the last read:\n- code.rs\n"; + assert_eq!( + response_text.as_str(), + expected_content, + "Tool should return the stale buffer notification" + ); + } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + Project::init_settings(cx); + assistant_tool::init(cx); + }); + } +} diff --git a/crates/assistant_tools/src/project_notifications_tool/description.md b/crates/assistant_tools/src/project_notifications_tool/description.md new file mode 100644 index 0000000000000000000000000000000000000000..24ff678f5e7fd728b94ad4ebce06f2a1dcc6a658 --- /dev/null +++ b/crates/assistant_tools/src/project_notifications_tool/description.md @@ -0,0 +1,3 @@ +This tool reports which files have been modified by the user since the agent last accessed them. + +It serves as a notification mechanism to inform the agent of recent changes. No immediate action is required in response to these updates. diff --git a/crates/assistant_tools/src/project_notifications_tool/prompt_header.txt b/crates/assistant_tools/src/project_notifications_tool/prompt_header.txt new file mode 100644 index 0000000000000000000000000000000000000000..f743e239c883c7456f7bdc6e089185c6b994cb44 --- /dev/null +++ b/crates/assistant_tools/src/project_notifications_tool/prompt_header.txt @@ -0,0 +1,3 @@ +[The following is an auto-generated notification; do not reply] + +These files have changed since the last read: diff --git a/crates/eval/src/examples/file_change_notification.rs b/crates/eval/src/examples/file_change_notification.rs index 0e4f770a6757a216061e28efb227a339f1094084..7879ad6f2ebb782bd4a5620f0fdf562c9aad1360 100644 --- a/crates/eval/src/examples/file_change_notification.rs +++ b/crates/eval/src/examples/file_change_notification.rs @@ -14,7 +14,7 @@ impl Example for FileChangeNotificationExample { url: "https://github.com/octocat/hello-world".to_string(), revision: "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d".to_string(), language_server: None, - max_assertions: Some(1), + max_assertions: None, profile_id: AgentProfileId::default(), existing_thread_json: None, max_turns: Some(3), From 8cc3b094d23afaa5cad9326831e2cc461a586296 Mon Sep 17 00:00:00 2001 From: Alex Povel Date: Mon, 7 Jul 2025 19:02:35 +0200 Subject: [PATCH 067/239] editor: Add action to sort lines by length (#33622) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change introduces a new `Action` implementation to sort lines by their `char` length. It reuses the same calculation as used for getting the caret column position, i.e. `TextSummary`. The motivation is to e.g. handle source code where this sort of order matters ([example](https://github.com/alexpovel/srgn/blob/fdf537c3d3e4c18ebf3426bb34759400552a82c3/tests/readme.rs#L529-L535)). Tested manually via `cargo build && ./target/debug/zed .`: the new action shows up in the command palette, and testing it on `.mailmap` entries turns those from ```text Agus Zubiaga Agus Zubiaga Alex Viscreanu Alex Viscreanu Alexander Mankuta Alexander Mankuta amtoaer amtoaer Andrei Zvonimir Crnković Andrei Zvonimir Crnković Angelk90 Angelk90 <20476002+Angelk90@users.noreply.github.com> Antonio Scandurra Antonio Scandurra Ben Kunkle Ben Kunkle Bennet Bo Fenner Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> Bennet Bo Fenner Boris Cherny Boris Cherny Brian Tan Chris Hayes Christian Bergschneider Christian Bergschneider Conrad Irwin Conrad Irwin Dairon Medina Danilo Leal Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Edwin Aronsson <75266237+4teapo@users.noreply.github.com> Elvis Pranskevichus Elvis Pranskevichus Evren Sen Evren Sen <146845123+evrensen467@users.noreply.github.com> Evren Sen <146845123+evrsen@users.noreply.github.com> Fernando Tagawa Fernando Tagawa Finn Evers Finn Evers <75036051+MrSubidubi@users.noreply.github.com> Finn Evers Gowtham K <73059450+dovakin0007@users.noreply.github.com> Greg Morenz Greg Morenz Ihnat Aŭtuška Ivan Žužak Ivan Žužak Joseph T. Lyons Joseph T. Lyons Julia Julia <30666851+ForLoveOfCats@users.noreply.github.com> Kaylee Simmons Kaylee Simmons Kaylee Simmons Kaylee Simmons Kirill Bulatov Kirill Bulatov Kyle Caverly Kyle Caverly Lilith Iris Lilith Iris <83819417+Irilith@users.noreply.github.com> LoganDark LoganDark LoganDark Marko Kungla Marko Kungla Marshall Bowers Marshall Bowers Marshall Bowers Matt Fellenz Matt Fellenz Max Brunsfeld Max Brunsfeld Max Linke Max Linke Michael Sloan Michael Sloan Michael Sloan Mikayla Maki Mikayla Maki Mikayla Maki Morgan Krey Muhammad Talal Anwar Muhammad Talal Anwar Nate Butler Nate Butler Nathan Sobo Nathan Sobo Nathan Sobo Nigel Jose Nigel Jose Peter Tripp Peter Tripp Petros Amoiridis Petros Amoiridis Piotr Osiewicz Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Pocæus Pocæus Rashid Almheiri Rashid Almheiri <69181766+huwaireb@users.noreply.github.com> Richard Feldman Richard Feldman Robert Clover Robert Clover Roy Williams Roy Williams Sebastijan Kelnerič Sebastijan Kelnerič Sergey Onufrienko Shish Shish Smit Barmase <0xtimsb@gmail.com> Smit Barmase <0xtimsb@gmail.com> Thomas Thomas Thomas Thomas Heartman Thomas Heartman Thomas Mickley-Doyle Thomas Mickley-Doyle Thorben Kröger Thorben Kröger Thorsten Ball Thorsten Ball Thorsten Ball Tristan Hume Tristan Hume Uladzislau Kaminski Uladzislau Kaminski Vitaly Slobodin Vitaly Slobodin Will Bradley Will Bradley WindSoilder 张小白 <364772080@qq.com> ```` into ```text 张小白 <364772080@qq.com> Ben Kunkle Finn Evers Agus Zubiaga amtoaer Peter Tripp Pocæus Danilo Leal Matt Fellenz Morgan Krey Nathan Sobo Robert Clover Conrad Irwin Ivan Žužak Mikayla Maki Piotr Osiewicz Shish Evren Sen Kirill Bulatov Michael Sloan Angelk90 Max Linke Smit Barmase <0xtimsb@gmail.com> Thorben Kröger Antonio Scandurra Bennet Bo Fenner Brian Tan Julia Nigel Jose Petros Amoiridis Rashid Almheiri Thomas Boris Cherny Nate Butler Thorsten Ball Tristan Hume Richard Feldman Greg Morenz Kaylee Simmons Lilith Iris Marshall Bowers Muhammad Talal Anwar Kyle Caverly Marko Kungla WindSoilder Alexander Mankuta Chris Hayes Max Brunsfeld Dairon Medina Elvis Pranskevichus Agus Zubiaga Alex Viscreanu Ihnat Aŭtuška Joseph T. Lyons Uladzislau Kaminski Will Bradley LoganDark Roy Williams Sergey Onufrienko Andrei Zvonimir Crnković Fernando Tagawa Vitaly Slobodin Nathan Sobo Thomas Mickley-Doyle Ben Kunkle Smit Barmase <0xtimsb@gmail.com> Finn Evers Robert Clover amtoaer Nate Butler Thomas Heartman Kaylee Simmons Peter Tripp Petros Amoiridis Pocæus Antonio Scandurra Bennet Bo Fenner Matt Fellenz Michael Sloan Nathan Sobo Sebastijan Kelnerič Shish Kaylee Simmons Kyle Caverly Max Brunsfeld Michael Sloan Ivan Žužak Richard Feldman Thorsten Ball Conrad Irwin Kirill Bulatov Marshall Bowers Will Bradley Christian Bergschneider Elvis Pranskevichus Greg Morenz Thorsten Ball Edwin Aronsson <75266237+4teapo@users.noreply.github.com> Gowtham K <73059450+dovakin0007@users.noreply.github.com> Marko Kungla Mikayla Maki Mikayla Maki Tristan Hume Thomas Boris Cherny Kaylee Simmons Thomas Muhammad Talal Anwar Roy Williams Marshall Bowers Thorben Kröger Andrei Zvonimir Crnković Thomas Mickley-Doyle Alexander Mankuta LoganDark Max Linke Alex Viscreanu Finn Evers <75036051+MrSubidubi@users.noreply.github.com> Nigel Jose Uladzislau Kaminski LoganDark Thomas Heartman Christian Bergschneider Evren Sen <146845123+evrsen@users.noreply.github.com> Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Vitaly Slobodin Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Angelk90 <20476002+Angelk90@users.noreply.github.com> Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> Rashid Almheiri <69181766+huwaireb@users.noreply.github.com> Evren Sen <146845123+evrensen467@users.noreply.github.com> Joseph T. Lyons Lilith Iris <83819417+Irilith@users.noreply.github.com> Fernando Tagawa Julia <30666851+ForLoveOfCats@users.noreply.github.com> Sebastijan Kelnerič ``` which looks good. There's a bit of Unicode in there -- though no grapheme clusters. Column number calculations do not seem to handle grapheme clusters either (?) so I thought this is OK. Open questions are: - should this be added to vim mode as well? - is `TextSummary` the way to go here? Is it perhaps too expensive? (it seems fine -- manually counting `char`s seems more brittle -- this way it will stay in sync with column number calculations) --- Team, I realize you [ask for a discussion to be opened first](https://github.com/zed-industries/zed/blob/86161aa427b9e8b18486272ca436c344224e8ba4/CONTRIBUTING.md#L32), so apologies for not doing that! It turned out hacking on Zed was much easier than expected (it's really nice!), and this change is small, adding a variation to an existing feature. Hope that's fine. Release Notes: - Added feature to sort lines by their length --------- Co-authored-by: Conrad Irwin --- crates/editor/src/actions.rs | 2 ++ crates/editor/src/editor.rs | 11 +++++++++++ crates/editor/src/editor_tests.rs | 23 +++++++++++++++++++++++ crates/editor/src/element.rs | 1 + 4 files changed, 37 insertions(+) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index def2a616a8aa0f29e330b85c75cb8ae2d285542a..70ec8ea00f52dccbab9e2a3ad4856599a8a94acf 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -635,6 +635,8 @@ actions!( SignatureHelpNext, /// Navigates to the previous signature in the signature help popup. SignatureHelpPrevious, + /// Sorts selected lines by length. + SortLinesByLength, /// Sorts selected lines case-insensitively. SortLinesCaseInsensitive, /// Sorts selected lines case-sensitively. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 47223aa59a86eef27494f28dccae144c14a59f85..6d529287a778e1b56994f80084dfb52f33d1e893 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -10204,6 +10204,17 @@ impl Editor { self.manipulate_immutable_lines(window, cx, |lines| lines.sort()) } + pub fn sort_lines_by_length( + &mut self, + _: &SortLinesByLength, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_immutable_lines(window, cx, |lines| { + lines.sort_by_key(|&line| line.chars().count()) + }) + } + pub fn sort_lines_case_insensitive( &mut self, _: &SortLinesCaseInsensitive, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index ade9a9322bcdbe38ad33fe9611820c43e2ea5809..05280de02b630eba7c35c4cbacc8d4173cf753a5 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -4075,6 +4075,29 @@ async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppC Zˇ» "}); + // Test sort_lines_by_length() + // + // Demonstrates: + // - ∞ is 3 bytes UTF-8, but sorted by its char count (1) + // - sort is stable + cx.set_state(indoc! {" + «123 + æ + 12 + ∞ + 1 + æˇ» + "}); + cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx)); + cx.assert_editor_state(indoc! {" + «æ + ∞ + 1 + æ + 12 + 123ˇ» + "}); + // Test reverse_lines() cx.set_state(indoc! {" «5 diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 3fa8697c193f80e2f974c57b74947e32a689a506..49f4fc52ac725bcef2707f2bf508a1fae811f434 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -225,6 +225,7 @@ impl EditorElement { register_action(editor, window, Editor::autoindent); register_action(editor, window, Editor::delete_line); register_action(editor, window, Editor::join_lines); + register_action(editor, window, Editor::sort_lines_by_length); register_action(editor, window, Editor::sort_lines_case_sensitive); register_action(editor, window, Editor::sort_lines_case_insensitive); register_action(editor, window, Editor::reverse_lines); From 38febed02d9341d9dc812c0895dc1144733ef94c Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Mon, 7 Jul 2025 22:41:29 +0530 Subject: [PATCH 068/239] languages: Fix detents case line after typing `:` in Python (#34017) Closes #34002 `decrease_indent_patterns` should only contain mapping which are at same indent level with each other, which is not true for `match` and `case` mapping. Caused in https://github.com/zed-industries/zed/pull/33370 Release Notes: - N/A --- crates/editor/src/editor_tests.rs | 13 +++++++++++++ crates/languages/src/python/config.toml | 1 - crates/languages/src/python/indents.scm | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 05280de02b630eba7c35c4cbacc8d4173cf753a5..aea84de9b022d742080ca9187a6a835092da67af 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -22348,6 +22348,19 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) { def f() -> list[str]: aˇ "}); + + // test does not outdent on typing : after case keyword + cx.set_state(indoc! {" + match 1: + caseˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input(":", window, cx); + }); + cx.assert_editor_state(indoc! {" + match 1: + case:ˇ + "}); } #[gpui::test] diff --git a/crates/languages/src/python/config.toml b/crates/languages/src/python/config.toml index 6d83d3f3dec6ba44e87e1d361fb5e61198767874..8728dfeaf138a97a7d9d7e9e2e3ca4b6b6db1820 100644 --- a/crates/languages/src/python/config.toml +++ b/crates/languages/src/python/config.toml @@ -34,5 +34,4 @@ decrease_indent_patterns = [ { pattern = "^\\s*else\\b.*:", valid_after = ["if", "elif", "for", "while", "except"] }, { pattern = "^\\s*except\\b.*:", valid_after = ["try", "except"] }, { pattern = "^\\s*finally\\b.*:", valid_after = ["try", "except", "else"] }, - { pattern = "^\\s*case\\b.*:", valid_after = ["match", "case"] } ] diff --git a/crates/languages/src/python/indents.scm b/crates/languages/src/python/indents.scm index 617aa706d3177c368f334c409989a27d09655b1e..3d4c1cc9c4260d4e925cc373662ae5ca3b82e124 100644 --- a/crates/languages/src/python/indents.scm +++ b/crates/languages/src/python/indents.scm @@ -14,4 +14,4 @@ (else_clause) @start.else (except_clause) @start.except (finally_clause) @start.finally -(case_pattern) @start.case +(case_clause) @start.case From 2a6ef006f4ea35ee0e611aa26640aed8b655022a Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Mon, 7 Jul 2025 13:56:53 -0400 Subject: [PATCH 069/239] Make `script/generate-license` fail on WARN too (#34008) Current main shows this on `script/generate-licenses`: ``` [WARN] failed to validate all files specified in clarification for crate ring 0.17.14: checksum mismatch, expected '76b39f9b371688eac9d8323f96ee80b3aef5ecbc2217f25377bd4e4a615296a9' ``` Ring fixed it's licenses ambiguity upstream. This warning was identifying that their license text (multiple licenses concatenated) had changed (sha mismatch) and thus our license clarification was invalid. Tested the script to confirm this [now fails](https://github.com/zed-industries/zed/actions/runs/16118890720/job/45479355992?pr=34008) under CI and then removed the ring clarification because it is no longer required and now passes. Release Notes: - N/A --- .github/workflows/ci.yml | 2 +- script/generate-licenses | 11 ++++++++++- script/licenses/zed-licenses.toml | 6 ------ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39036ef5649e699ffda1636f304629fce6184371..84c7a9682819408c95fb8dfaa1f424bd3f847a9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: else echo "run_docs=false" >> $GITHUB_OUTPUT fi - if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep '^Cargo.lock') ]]; then + if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -P '^(Cargo.lock|script/.*licenses)') ]]; then echo "run_license=true" >> $GITHUB_OUTPUT else echo "run_license=false" >> $GITHUB_OUTPUT diff --git a/script/generate-licenses b/script/generate-licenses index 9fcb2bd5133e10007f3335abd59fa7c4da2e3176..7ae0f1c3f60b93eb500ffa5e127a96c82d954b72 100755 --- a/script/generate-licenses +++ b/script/generate-licenses @@ -6,6 +6,15 @@ CARGO_ABOUT_VERSION="0.7" OUTPUT_FILE="${1:-$(pwd)/assets/licenses.md}" TEMPLATE_FILE="script/licenses/template.md.hbs" +fail_on_stderr() { + local tmpfile=$(mktemp) + "$@" 2> >(tee "$tmpfile" >&2) + local rc=$? + [ -s "$tmpfile" ] && rc=1 + rm "$tmpfile" + return $rc +} + echo -n "" >"$OUTPUT_FILE" { @@ -28,7 +37,7 @@ fi echo "Generating cargo licenses" if [ -z "${ALLOW_MISSING_LICENSES-}" ]; then FAIL_FLAG=--fail; else FAIL_FLAG=""; fi set -x -cargo about generate \ +fail_on_stderr cargo about generate \ $FAIL_FLAG \ -c script/licenses/zed-licenses.toml \ "$TEMPLATE_FILE" >>"$OUTPUT_FILE" diff --git a/script/licenses/zed-licenses.toml b/script/licenses/zed-licenses.toml index 4df1f5989ab3aa0c70b887d0a9cfc07719f3e7c3..9d13087ece08404e2cae1a44733e382521b8fdd0 100644 --- a/script/licenses/zed-licenses.toml +++ b/script/licenses/zed-licenses.toml @@ -177,9 +177,3 @@ license = "MIT" [[pet-windows-store.clarify.files]] path = '../../LICENSE' checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[ring.clarify] -license = "ISC AND OpenSSL" -[[ring.clarify.files]] -path = 'LICENSE' -checksum = '76b39f9b371688eac9d8323f96ee80b3aef5ecbc2217f25377bd4e4a615296a9' From e0c860c42a3669f7daabb7d902c6ad76a19d3da9 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 7 Jul 2025 14:28:59 -0400 Subject: [PATCH 070/239] debugger: Fix issues with restarting sessions (#33932) Restarting sessions was broken in #33273 when we moved away from calling `kill` in the shutdown sequence. This PR re-adds that `kill` call so that old debug adapter processes will be cleaned up when sessions are restarted within Zed. This doesn't re-introduce the issue that motivated the original changes to the shutdown sequence, because we still send Disconnect/Terminate to debug adapters when quitting Zed without killing the process directly. We also now remove manually-restarted sessions eagerly from the session list. Closes #33916 Release Notes: - debugger: Fixed not being able to restart sessions for Debugpy and other adapters that communicate over TCP. - debugger: Fixed debug adapter processes not being cleaned up. --------- Co-authored-by: Remco Smits --- crates/dap/src/client.rs | 8 ++- crates/dap/src/transport.rs | 65 ++++++++++---------- crates/debugger_ui/src/debugger_panel.rs | 18 +++--- crates/debugger_ui/src/tests/attach_modal.rs | 10 +-- crates/project/src/debugger/session.rs | 43 +++++++++---- 5 files changed, 82 insertions(+), 62 deletions(-) diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index 4515e2a1d723f0701b53723e472bd8c5013ffa65..ff082e3b765b0baac294cf310a50b54534ae9bd1 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -2,7 +2,7 @@ use crate::{ adapters::DebugAdapterBinary, transport::{IoKind, LogKind, TransportDelegate}, }; -use anyhow::Result; +use anyhow::{Context as _, Result}; use dap_types::{ messages::{Message, Response}, requests::Request, @@ -108,7 +108,11 @@ impl DebugAdapterClient { arguments: Some(serialized_arguments), }; self.transport_delegate - .add_pending_request(sequence_id, callback_tx); + .pending_requests + .lock() + .as_mut() + .context("client is closed")? + .insert(sequence_id, callback_tx); log::debug!( "Client {} send `{}` request with sequence_id: {}", diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 9576608ab0aa6267ecfed248106c7ca1c5d60654..14370f66e458309e3551a769bd79d29529a0cf3d 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -49,7 +49,6 @@ pub enum IoKind { StdErr, } -type Requests = Arc>>>>; type LogHandlers = Arc>>; pub trait Transport: Send + Sync { @@ -93,18 +92,14 @@ async fn start( pub(crate) struct TransportDelegate { log_handlers: LogHandlers, - pub(crate) pending_requests: Requests, + // TODO this should really be some kind of associative channel + pub(crate) pending_requests: + Arc>>>>>, pub(crate) transport: Mutex>, pub(crate) server_tx: smol::lock::Mutex>>, tasks: Mutex>>, } -impl Drop for TransportDelegate { - fn drop(&mut self) { - self.transport.lock().kill() - } -} - impl TransportDelegate { pub(crate) async fn start(binary: &DebugAdapterBinary, cx: &mut AsyncApp) -> Result { let log_handlers: LogHandlers = Default::default(); @@ -113,7 +108,7 @@ impl TransportDelegate { transport: Mutex::new(transport), log_handlers, server_tx: Default::default(), - pending_requests: Default::default(), + pending_requests: Arc::new(Mutex::new(Some(HashMap::default()))), tasks: Default::default(), }) } @@ -154,16 +149,26 @@ impl TransportDelegate { .await { Ok(()) => { - pending_requests.lock().drain().for_each(|(_, request)| { - request - .send(Err(anyhow!("debugger shutdown unexpectedly"))) - .ok(); - }); + pending_requests + .lock() + .take() + .into_iter() + .flatten() + .for_each(|(_, request)| { + request + .send(Err(anyhow!("debugger shutdown unexpectedly"))) + .ok(); + }); } Err(e) => { - pending_requests.lock().drain().for_each(|(_, request)| { - request.send(Err(e.cloned())).ok(); - }); + pending_requests + .lock() + .take() + .into_iter() + .flatten() + .for_each(|(_, request)| { + request.send(Err(e.cloned())).ok(); + }); } } })); @@ -188,15 +193,6 @@ impl TransportDelegate { self.transport.lock().tcp_arguments() } - pub(crate) fn add_pending_request( - &self, - sequence_id: u64, - request: oneshot::Sender>, - ) { - let mut pending_requests = self.pending_requests.lock(); - pending_requests.insert(sequence_id, request); - } - pub(crate) async fn send_message(&self, message: Message) -> Result<()> { if let Some(server_tx) = self.server_tx.lock().await.as_ref() { server_tx.send(message).await.context("sending message") @@ -290,7 +286,7 @@ impl TransportDelegate { async fn recv_from_server( server_stdout: Stdout, mut message_handler: DapMessageHandler, - pending_requests: Requests, + pending_requests: Arc>>>>>, log_handlers: Option, ) -> Result<()> where @@ -300,16 +296,21 @@ impl TransportDelegate { let mut reader = BufReader::new(server_stdout); let result = loop { - match Self::receive_server_message(&mut reader, &mut recv_buffer, log_handlers.as_ref()) - .await - { + let result = + Self::receive_server_message(&mut reader, &mut recv_buffer, log_handlers.as_ref()) + .await; + match result { ConnectionResult::Timeout => anyhow::bail!("Timed out when connecting to debugger"), ConnectionResult::ConnectionReset => { log::info!("Debugger closed the connection"); - return Ok(()); + break Ok(()); } ConnectionResult::Result(Ok(Message::Response(res))) => { - let tx = pending_requests.lock().remove(&res.request_seq); + let tx = pending_requests + .lock() + .as_mut() + .context("client is closed")? + .remove(&res.request_seq); if let Some(tx) = tx { if let Err(e) = tx.send(Self::process_response(res)) { log::trace!("Did not send response `{:?}` for a cancelled", e); diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index d03e8c5225f04fae6b12d220a78a6806ebeaf6aa..37df989c0b2dc8f5900dd74bd98cdc419d181620 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -33,7 +33,7 @@ use std::sync::{Arc, LazyLock}; use task::{DebugScenario, TaskContext}; use tree_sitter::{Query, StreamingIterator as _}; use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*}; -use util::maybe; +use util::{ResultExt, maybe}; use workspace::SplitDirection; use workspace::{ Pane, Workspace, @@ -363,11 +363,17 @@ impl DebugPanel { let label = curr_session.read(cx).label().clone(); let adapter = curr_session.read(cx).adapter().clone(); let binary = curr_session.read(cx).binary().cloned().unwrap(); - let task = curr_session.update(cx, |session, cx| session.shutdown(cx)); let task_context = curr_session.read(cx).task_context().clone(); + let curr_session_id = curr_session.read(cx).session_id(); + self.sessions + .retain(|session| session.read(cx).session_id(cx) != curr_session_id); + let task = dap_store_handle.update(cx, |dap_store, cx| { + dap_store.shutdown_session(curr_session_id, cx) + }); + cx.spawn_in(window, async move |this, cx| { - task.await; + task.await.log_err(); let (session, task) = dap_store_handle.update(cx, |dap_store, cx| { let session = dap_store.new_session(label, adapter, task_context, None, cx); @@ -1298,9 +1304,7 @@ impl Panel for DebugPanel { impl Render for DebugPanel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let has_sessions = self.sessions.len() > 0; let this = cx.weak_entity(); - debug_assert_eq!(has_sessions, self.active_session.is_some()); if self .active_session @@ -1487,8 +1491,8 @@ impl Render for DebugPanel { })) }) .map(|this| { - if has_sessions { - this.children(self.active_session.clone()) + if let Some(active_session) = self.active_session.clone() { + this.child(active_session) } else { let docked_to_bottom = self.position(window, cx) == DockPosition::Bottom; let welcome_experience = v_flex() diff --git a/crates/debugger_ui/src/tests/attach_modal.rs b/crates/debugger_ui/src/tests/attach_modal.rs index 139591530583b010c97ed8d2126a4ea210efe4c7..906a7a0d4bd76f0451d6b5d5cfa5beff0136c613 100644 --- a/crates/debugger_ui/src/tests/attach_modal.rs +++ b/crates/debugger_ui/src/tests/attach_modal.rs @@ -27,7 +27,7 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); - let session = start_debug_session_with( + let _session = start_debug_session_with( &workspace, cx, DebugTaskDefinition { @@ -59,14 +59,6 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te assert!(workspace.active_modal::(cx).is_none()); }) .unwrap(); - - let shutdown_session = project.update(cx, |project, cx| { - project.dap_store().update(cx, |dap_store, cx| { - dap_store.shutdown_session(session.read(cx).session_id(), cx) - }) - }); - - shutdown_session.await.unwrap(); } #[gpui::test] diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 9ab83610f02cdeb0661062d04dc1b3d6fa3013be..3190254af8545f1a48088460c4ef8d3bbd38bb41 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -677,6 +677,7 @@ pub struct Session { ignore_breakpoints: bool, exception_breakpoints: BTreeMap, background_tasks: Vec>, + restart_task: Option>, task_context: TaskContext, } @@ -838,6 +839,7 @@ impl Session { loaded_sources: Vec::default(), threads: IndexMap::default(), background_tasks: Vec::default(), + restart_task: None, locations: Default::default(), is_session_terminated: false, ignore_breakpoints: false, @@ -1870,18 +1872,30 @@ impl Session { } pub fn restart(&mut self, args: Option, cx: &mut Context) { - if self.capabilities.supports_restart_request.unwrap_or(false) && !self.is_terminated() { - self.request( - RestartCommand { - raw: args.unwrap_or(Value::Null), - }, - Self::fallback_to_manual_restart, - cx, - ) - .detach(); - } else { - cx.emit(SessionStateEvent::Restart); + if self.restart_task.is_some() || self.as_running().is_none() { + return; } + + let supports_dap_restart = + self.capabilities.supports_restart_request.unwrap_or(false) && !self.is_terminated(); + + self.restart_task = Some(cx.spawn(async move |this, cx| { + let _ = this.update(cx, |session, cx| { + if supports_dap_restart { + session + .request( + RestartCommand { + raw: args.unwrap_or(Value::Null), + }, + Self::fallback_to_manual_restart, + cx, + ) + .detach(); + } else { + cx.emit(SessionStateEvent::Restart); + } + }); + })); } pub fn shutdown(&mut self, cx: &mut Context) -> Task<()> { @@ -1919,8 +1933,13 @@ impl Session { cx.emit(SessionStateEvent::Shutdown); - cx.spawn(async move |_, _| { + cx.spawn(async move |this, cx| { task.await; + let _ = this.update(cx, |this, _| { + if let Some(adapter_client) = this.adapter_client() { + adapter_client.kill(); + } + }); }) } From d549993c73def3854c860e1e2939d91bee270d02 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Mon, 7 Jul 2025 22:30:01 +0300 Subject: [PATCH 071/239] tools: Send stale file notifications only once (#34026) Previously, we sent notifications repeatedly until the agent read a file, which was often inefficient. With this change, we now send a notification only once (unless the files are modified again, in which case we'll send another notification). Release Notes: - N/A --- crates/agent/src/thread.rs | 55 ++++++++++++++----- crates/assistant_tool/src/action_log.rs | 40 +++++++++++++- .../src/project_notifications_tool.rs | 41 ++++++++++++-- 3 files changed, 117 insertions(+), 19 deletions(-) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 50d2a4d77383e4336bed6ba3fd6fc4a9e3e64ac1..72417cfe999cd3e34f1b7ff8921bd8cb6e577895 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -1516,7 +1516,7 @@ impl Thread { ) -> Option { let action_log = self.action_log.read(cx); - action_log.stale_buffers(cx).next()?; + action_log.unnotified_stale_buffers(cx).next()?; // Represent notification as a simulated `project_notifications` tool call let tool_name = Arc::from("project_notifications"); @@ -3631,11 +3631,11 @@ fn main() {{ }); // We shouldn't have a stale buffer notification yet - let notification = thread.read_with(cx, |thread, _| { - find_tool_use(thread, "project_notifications") + let notifications = thread.read_with(cx, |thread, _| { + find_tool_uses(thread, "project_notifications") }); assert!( - notification.is_none(), + notifications.is_empty(), "Should not have stale buffer notification before buffer is modified" ); @@ -3664,13 +3664,15 @@ fn main() {{ thread.flush_notifications(model.clone(), CompletionIntent::UserPrompt, cx) }); - let Some(notification_result) = thread.read_with(cx, |thread, _cx| { - find_tool_use(thread, "project_notifications") - }) else { + let notifications = thread.read_with(cx, |thread, _cx| { + find_tool_uses(thread, "project_notifications") + }); + + let [notification] = notifications.as_slice() else { panic!("Should have a `project_notifications` tool use"); }; - let Some(notification_content) = notification_result.content.to_str() else { + let Some(notification_content) = notification.content.to_str() else { panic!("`project_notifications` should return text"); }; @@ -3680,19 +3682,46 @@ fn main() {{ - code.rs "}; assert_eq!(notification_content, expected_content); + + // Insert another user message and flush notifications again + thread.update(cx, |thread, cx| { + thread.insert_user_message( + "Can you tell me more?", + ContextLoadResult::default(), + None, + Vec::new(), + cx, + ) + }); + + thread.update(cx, |thread, cx| { + thread.flush_notifications(model.clone(), CompletionIntent::UserPrompt, cx) + }); + + // There should be no new notifications (we already flushed one) + let notifications = thread.read_with(cx, |thread, _cx| { + find_tool_uses(thread, "project_notifications") + }); + + assert_eq!( + notifications.len(), + 1, + "Should still have only one notification after second flush - no duplicates" + ); } - fn find_tool_use(thread: &Thread, tool_name: &str) -> Option { + fn find_tool_uses(thread: &Thread, tool_name: &str) -> Vec { thread .messages() - .filter_map(|message| { + .flat_map(|message| { thread .tool_results_for_message(message.id) .into_iter() - .find(|result| result.tool_name == tool_name.into()) + .filter(|result| result.tool_name == tool_name.into()) + .cloned() + .collect::>() }) - .next() - .cloned() + .collect() } #[gpui::test] diff --git a/crates/assistant_tool/src/action_log.rs b/crates/assistant_tool/src/action_log.rs index 0877f18060d91f5e031477cd2590fad85dc22ecb..2071a1f444b547197fabc252fddb1f9bd165ae67 100644 --- a/crates/assistant_tool/src/action_log.rs +++ b/crates/assistant_tool/src/action_log.rs @@ -1,5 +1,6 @@ use anyhow::{Context as _, Result}; use buffer_diff::BufferDiff; +use clock; use collections::BTreeMap; use futures::{FutureExt, StreamExt, channel::mpsc}; use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity}; @@ -17,6 +18,8 @@ pub struct ActionLog { edited_since_project_diagnostics_check: bool, /// The project this action log is associated with project: Entity, + /// Tracks which buffer versions have already been notified as changed externally + notified_versions: BTreeMap, clock::Global>, } impl ActionLog { @@ -26,6 +29,7 @@ impl ActionLog { tracked_buffers: BTreeMap::default(), edited_since_project_diagnostics_check: false, project, + notified_versions: BTreeMap::default(), } } @@ -51,6 +55,7 @@ impl ActionLog { ) -> &mut TrackedBuffer { let status = if is_created { if let Some(tracked) = self.tracked_buffers.remove(&buffer) { + self.notified_versions.remove(&buffer); match tracked.status { TrackedBufferStatus::Created { existing_file_content, @@ -106,7 +111,7 @@ impl ActionLog { TrackedBuffer { buffer: buffer.clone(), diff_base, - unreviewed_edits: unreviewed_edits, + unreviewed_edits, snapshot: text_snapshot.clone(), status, version: buffer.read(cx).version(), @@ -165,6 +170,7 @@ impl ActionLog { // If the buffer had been edited by a tool, but it got // deleted externally, we want to stop tracking it. self.tracked_buffers.remove(&buffer); + self.notified_versions.remove(&buffer); } cx.notify(); } @@ -178,6 +184,7 @@ impl ActionLog { // resurrected externally, we want to clear the edits we // were tracking and reset the buffer's state. self.tracked_buffers.remove(&buffer); + self.notified_versions.remove(&buffer); self.track_buffer_internal(buffer, false, cx); } cx.notify(); @@ -483,6 +490,7 @@ impl ActionLog { match tracked_buffer.status { TrackedBufferStatus::Created { .. } => { self.tracked_buffers.remove(&buffer); + self.notified_versions.remove(&buffer); cx.notify(); } TrackedBufferStatus::Modified => { @@ -508,6 +516,7 @@ impl ActionLog { match tracked_buffer.status { TrackedBufferStatus::Deleted => { self.tracked_buffers.remove(&buffer); + self.notified_versions.remove(&buffer); cx.notify(); } _ => { @@ -616,6 +625,7 @@ impl ActionLog { }; self.tracked_buffers.remove(&buffer); + self.notified_versions.remove(&buffer); cx.notify(); task } @@ -629,6 +639,7 @@ impl ActionLog { // Clear all tracked edits for this buffer and start over as if we just read it. self.tracked_buffers.remove(&buffer); + self.notified_versions.remove(&buffer); self.buffer_read(buffer.clone(), cx); cx.notify(); save @@ -713,6 +724,33 @@ impl ActionLog { .collect() } + /// Returns stale buffers that haven't been notified yet + pub fn unnotified_stale_buffers<'a>( + &'a self, + cx: &'a App, + ) -> impl Iterator> { + self.stale_buffers(cx).filter(|buffer| { + let buffer_entity = buffer.read(cx); + self.notified_versions + .get(buffer) + .map_or(true, |notified_version| { + *notified_version != buffer_entity.version + }) + }) + } + + /// Marks the given buffers as notified at their current versions + pub fn mark_buffers_as_notified( + &mut self, + buffers: impl IntoIterator>, + cx: &App, + ) { + for buffer in buffers { + let version = buffer.read(cx).version.clone(); + self.notified_versions.insert(buffer, version); + } + } + /// Iterate over buffers changed since last read or edited by the model pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator> { self.tracked_buffers diff --git a/crates/assistant_tools/src/project_notifications_tool.rs b/crates/assistant_tools/src/project_notifications_tool.rs index 552ebb3d53e7453dbcaa4f363bd6e6ae5d2709b6..01dcbba4ac54c34358de9f851aaca6e6db5201e1 100644 --- a/crates/assistant_tools/src/project_notifications_tool.rs +++ b/crates/assistant_tools/src/project_notifications_tool.rs @@ -53,15 +53,21 @@ impl Tool for ProjectNotificationsTool { cx: &mut App, ) -> ToolResult { let mut stale_files = String::new(); + let mut notified_buffers = Vec::new(); - let action_log = action_log.read(cx); - - for stale_file in action_log.stale_buffers(cx) { + for stale_file in action_log.read(cx).unnotified_stale_buffers(cx) { if let Some(file) = stale_file.read(cx).file() { writeln!(&mut stale_files, "- {}", file.path().display()).ok(); + notified_buffers.push(stale_file.clone()); } } + if !notified_buffers.is_empty() { + action_log.update(cx, |log, cx| { + log.mark_buffers_as_notified(notified_buffers, cx); + }); + } + let response = if stale_files.is_empty() { "No new notifications".to_string() } else { @@ -155,11 +161,11 @@ mod tests { // Run the tool again let result = cx.update(|cx| { - tool.run( + tool.clone().run( tool_input.clone(), request.clone(), project.clone(), - action_log, + action_log.clone(), model.clone(), None, cx, @@ -179,6 +185,31 @@ mod tests { expected_content, "Tool should return the stale buffer notification" ); + + // Run the tool once more without any changes - should get no new notifications + let result = cx.update(|cx| { + tool.run( + tool_input.clone(), + request.clone(), + project.clone(), + action_log, + model.clone(), + None, + cx, + ) + }); + + let response = result.output.await.unwrap(); + let response_text = match &response.content { + ToolResultContent::Text(text) => text.clone(), + _ => panic!("Expected text response"), + }; + + assert_eq!( + response_text.as_str(), + "No new notifications", + "Tool should return 'No new notifications' when running again without changes" + ); } fn init_test(cx: &mut TestAppContext) { From a9107dfaebe4cf5da85408d04bb735a1eb63d4e6 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Mon, 7 Jul 2025 14:04:21 -0700 Subject: [PATCH 072/239] Edit debug tasks (#32908) Release Notes: - Added the ability to edit LSP provided debug tasks --------- Co-authored-by: Conrad Irwin --- .zed/debug.json | 10 +- Cargo.lock | 2 + crates/debugger_ui/Cargo.toml | 2 + crates/debugger_ui/src/attach_modal.rs | 40 +- crates/debugger_ui/src/debugger_panel.rs | 235 ++++++++-- crates/debugger_ui/src/new_process_modal.rs | 408 +++++++----------- .../src/tests/new_process_modal.rs | 232 +++++----- crates/editor/src/element.rs | 1 + 8 files changed, 515 insertions(+), 415 deletions(-) diff --git a/.zed/debug.json b/.zed/debug.json index 49b8f1a7a697303c383332f4ed704c844df22132..6f4e936c80f966a5882d3dc2cbc6d53d03e877c8 100644 --- a/.zed/debug.json +++ b/.zed/debug.json @@ -5,9 +5,7 @@ "build": { "label": "Build Zed", "command": "cargo", - "args": [ - "build" - ] + "args": ["build"] } }, { @@ -16,9 +14,7 @@ "build": { "label": "Build Zed", "command": "cargo", - "args": [ - "build" - ] + "args": ["build"] } - }, + } ] diff --git a/Cargo.lock b/Cargo.lock index 921eea00f8147f4715934ed0c0ab2015945cd4ab..57b97cb853246c6b2747e383494ea29501332541 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4324,6 +4324,7 @@ dependencies = [ "futures 0.3.31", "fuzzy", "gpui", + "indoc", "itertools 0.14.0", "language", "log", @@ -4344,6 +4345,7 @@ dependencies = [ "tasks_ui", "telemetry", "terminal_view", + "text", "theme", "tree-sitter", "tree-sitter-go", diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index ba71e50a0830c7fbab60aa75ba14bb63d58bac07..fc543a47f9b4a406fd50f894d0d123920bcc0df5 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -40,6 +40,7 @@ file_icons.workspace = true futures.workspace = true fuzzy.workspace = true gpui.workspace = true +indoc.workspace = true itertools.workspace = true language.workspace = true log.workspace = true @@ -60,6 +61,7 @@ task.workspace = true tasks_ui.workspace = true telemetry.workspace = true terminal_view.workspace = true +text.workspace = true theme.workspace = true tree-sitter.workspace = true tree-sitter-json.workspace = true diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index aa4ca9e868a508ca646b005bb08944d282d7bc37..662a98c82075cd6e936988959c855eadb5138092 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -206,7 +206,7 @@ impl PickerDelegate for AttachModalDelegate { }) } - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { + fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) { let candidate = self .matches .get(self.selected_index()) @@ -229,30 +229,44 @@ impl PickerDelegate for AttachModalDelegate { } } + let workspace = self.workspace.clone(); + let Some(panel) = workspace + .update(cx, |workspace, cx| workspace.panel::(cx)) + .ok() + .flatten() + else { + return; + }; + + if secondary { + // let Some(id) = worktree_id else { return }; + // cx.spawn_in(window, async move |_, cx| { + // panel + // .update_in(cx, |debug_panel, window, cx| { + // debug_panel.save_scenario(&debug_scenario, id, window, cx) + // })? + // .await?; + // anyhow::Ok(()) + // }) + // .detach_and_log_err(cx); + } let Some(adapter) = cx.read_global::(|registry, _| { registry.adapter(&self.definition.adapter) }) else { return; }; - let workspace = self.workspace.clone(); let definition = self.definition.clone(); cx.spawn_in(window, async move |this, cx| { let Ok(scenario) = adapter.config_from_zed_format(definition).await else { return; }; - let panel = workspace - .update(cx, |workspace, cx| workspace.panel::(cx)) - .ok() - .flatten(); - if let Some(panel) = panel { - panel - .update_in(cx, |panel, window, cx| { - panel.start_session(scenario, Default::default(), None, None, window, cx); - }) - .ok(); - } + panel + .update_in(cx, |panel, window, cx| { + panel.start_session(scenario, Default::default(), None, None, window, cx); + }) + .ok(); this.update(cx, |_, cx| { cx.emit(DismissEvent); }) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 37df989c0b2dc8f5900dd74bd98cdc419d181620..988f6f4019ef5e681b2e9b73f041435c569366a2 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -16,16 +16,18 @@ use dap::{ client::SessionId, debugger_settings::DebuggerSettings, }; use dap::{DapRegistry, StartDebuggingRequestArguments}; +use editor::Editor; use gpui::{ Action, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity, anchored, deferred, }; +use text::ToPoint as _; use itertools::Itertools as _; use language::Buffer; use project::debugger::session::{Session, SessionStateEvent}; -use project::{DebugScenarioContext, Fs, ProjectPath, WorktreeId}; +use project::{DebugScenarioContext, Fs, ProjectPath, TaskSourceKind, WorktreeId}; use project::{Project, debugger::session::ThreadStatus}; use rpc::proto::{self}; use settings::Settings; @@ -35,8 +37,9 @@ use tree_sitter::{Query, StreamingIterator as _}; use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*}; use util::{ResultExt, maybe}; use workspace::SplitDirection; +use workspace::item::SaveOptions; use workspace::{ - Pane, Workspace, + Item, Pane, Workspace, dock::{DockPosition, Panel, PanelEvent}, }; use zed_actions::ToggleFocus; @@ -988,13 +991,90 @@ impl DebugPanel { cx.notify(); } + pub(crate) fn go_to_scenario_definition( + &self, + kind: TaskSourceKind, + scenario: DebugScenario, + worktree_id: WorktreeId, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + let Some(workspace) = self.workspace.upgrade() else { + return Task::ready(Ok(())); + }; + let project_path = match kind { + TaskSourceKind::AbsPath { abs_path, .. } => { + let Some(project_path) = workspace + .read(cx) + .project() + .read(cx) + .project_path_for_absolute_path(&abs_path, cx) + else { + return Task::ready(Err(anyhow!("no abs path"))); + }; + + project_path + } + TaskSourceKind::Worktree { + id, + directory_in_worktree: dir, + .. + } => { + let relative_path = if dir.ends_with(".vscode") { + dir.join("launch.json") + } else { + dir.join("debug.json") + }; + ProjectPath { + worktree_id: id, + path: Arc::from(relative_path), + } + } + _ => return self.save_scenario(scenario, worktree_id, window, cx), + }; + + let editor = workspace.update(cx, |workspace, cx| { + workspace.open_path(project_path, None, true, window, cx) + }); + cx.spawn_in(window, async move |_, cx| { + let editor = editor.await?; + let editor = cx + .update(|_, cx| editor.act_as::(cx))? + .context("expected editor")?; + + // unfortunately debug tasks don't have an easy way to globally + // identify them. to jump to the one that you just created or an + // old one that you're choosing to edit we use a heuristic of searching for a line with `label: ` from the end rather than the start so we bias towards more renctly + editor.update_in(cx, |editor, window, cx| { + let row = editor.text(cx).lines().enumerate().find_map(|(row, text)| { + if text.contains(scenario.label.as_ref()) && text.contains("\"label\": ") { + Some(row) + } else { + None + } + }); + if let Some(row) = row { + editor.go_to_singleton_buffer_point( + text::Point::new(row as u32, 4), + window, + cx, + ); + } + })?; + + Ok(()) + }) + } + pub(crate) fn save_scenario( &self, - scenario: &DebugScenario, + scenario: DebugScenario, worktree_id: WorktreeId, window: &mut Window, - cx: &mut App, - ) -> Task> { + cx: &mut Context, + ) -> Task> { + let this = cx.weak_entity(); + let project = self.project.clone(); self.workspace .update(cx, |workspace, cx| { let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else { @@ -1027,47 +1107,7 @@ impl DebugPanel { ) .await?; } - - let mut content = fs.load(path).await?; - let new_scenario = serde_json_lenient::to_string_pretty(&serialized_scenario)? - .lines() - .map(|l| format!(" {l}")) - .join("\n"); - - static ARRAY_QUERY: LazyLock = LazyLock::new(|| { - Query::new( - &tree_sitter_json::LANGUAGE.into(), - "(document (array (object) @object))", // TODO: use "." anchor to only match last object - ) - .expect("Failed to create ARRAY_QUERY") - }); - - let mut parser = tree_sitter::Parser::new(); - parser - .set_language(&tree_sitter_json::LANGUAGE.into()) - .unwrap(); - let mut cursor = tree_sitter::QueryCursor::new(); - let syntax_tree = parser.parse(&content, None).unwrap(); - let mut matches = - cursor.matches(&ARRAY_QUERY, syntax_tree.root_node(), content.as_bytes()); - - // we don't have `.last()` since it's a lending iterator, so loop over - // the whole thing to find the last one - let mut last_offset = None; - while let Some(mat) = matches.next() { - if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end) { - last_offset = Some(pos) - } - } - - if let Some(pos) = last_offset { - content.insert_str(pos, &new_scenario); - content.insert_str(pos, ",\n"); - } - - fs.write(path, content.as_bytes()).await?; - - workspace.update(cx, |workspace, cx| { + let project_path = workspace.update(cx, |workspace, cx| { workspace .project() .read(cx) @@ -1075,12 +1115,113 @@ impl DebugPanel { .context( "Couldn't get project path for .zed/debug.json in active worktree", ) - })? + })??; + + let editor = this + .update_in(cx, |this, window, cx| { + this.workspace.update(cx, |workspace, cx| { + workspace.open_path(project_path, None, true, window, cx) + }) + })?? + .await?; + let editor = cx + .update(|_, cx| editor.act_as::(cx))? + .context("expected editor")?; + + let new_scenario = serde_json_lenient::to_string_pretty(&serialized_scenario)? + .lines() + .map(|l| format!(" {l}")) + .join("\n"); + + editor + .update_in(cx, |editor, window, cx| { + Self::insert_task_into_editor(editor, new_scenario, project, window, cx) + })?? + .await }) }) .unwrap_or_else(|err| Task::ready(Err(err))) } + pub fn insert_task_into_editor( + editor: &mut Editor, + new_scenario: String, + project: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Result>> { + static LAST_ITEM_QUERY: LazyLock = LazyLock::new(|| { + Query::new( + &tree_sitter_json::LANGUAGE.into(), + "(document (array (object) @object))", // TODO: use "." anchor to only match last object + ) + .expect("Failed to create LAST_ITEM_QUERY") + }); + static EMPTY_ARRAY_QUERY: LazyLock = LazyLock::new(|| { + Query::new( + &tree_sitter_json::LANGUAGE.into(), + "(document (array) @array)", + ) + .expect("Failed to create EMPTY_ARRAY_QUERY") + }); + + let content = editor.text(cx); + let mut parser = tree_sitter::Parser::new(); + parser.set_language(&tree_sitter_json::LANGUAGE.into())?; + let mut cursor = tree_sitter::QueryCursor::new(); + let syntax_tree = parser + .parse(&content, None) + .context("could not parse debug.json")?; + let mut matches = cursor.matches( + &LAST_ITEM_QUERY, + syntax_tree.root_node(), + content.as_bytes(), + ); + + let mut last_offset = None; + while let Some(mat) = matches.next() { + if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end) { + last_offset = Some(pos) + } + } + let mut edits = Vec::new(); + let mut cursor_position = 0; + + if let Some(pos) = last_offset { + edits.push((pos..pos, format!(",\n{new_scenario}"))); + cursor_position = pos + ",\n ".len(); + } else { + let mut matches = cursor.matches( + &EMPTY_ARRAY_QUERY, + syntax_tree.root_node(), + content.as_bytes(), + ); + + if let Some(mat) = matches.next() { + if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end - 1) { + edits.push((pos..pos, format!("\n{new_scenario}\n"))); + cursor_position = pos + "\n ".len(); + } + } else { + edits.push((0..0, format!("[\n{}\n]", new_scenario))); + cursor_position = "[\n ".len(); + } + } + editor.transact(window, cx, |editor, window, cx| { + editor.edit(edits, cx); + let snapshot = editor + .buffer() + .read(cx) + .as_singleton() + .unwrap() + .read(cx) + .snapshot(); + let point = cursor_position.to_point(&snapshot); + editor.go_to_singleton_buffer_point(point, window, cx); + }); + Ok(editor.save(SaveOptions::default(), project, window, cx)) + } + pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context) { self.thread_picker_menu_handle.toggle(window, cx); } diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index e857e336775cfc42fd073ba199f855a967656e12..6d7fa244a2e2bfaaaa82f1321d446627e2b0c343 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -1,12 +1,10 @@ -use anyhow::bail; +use anyhow::{Context as _, bail}; use collections::{FxHashMap, HashMap}; use language::LanguageRegistry; -use paths::local_debug_file_relative_path; use std::{ borrow::Cow, path::{Path, PathBuf}, sync::Arc, - time::Duration, usize, }; use tasks_ui::{TaskOverrides, TasksModal}; @@ -18,35 +16,27 @@ use editor::{Editor, EditorElement, EditorStyle}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, - HighlightStyle, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, - Subscription, Task, TextStyle, UnderlineStyle, WeakEntity, + KeyContext, Render, Subscription, Task, TextStyle, WeakEntity, }; use itertools::Itertools as _; use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; -use project::{ - DebugScenarioContext, ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore, -}; -use settings::{Settings, initial_local_debug_tasks_content}; +use project::{DebugScenarioContext, TaskContexts, TaskSourceKind, task_store::TaskStore}; +use settings::Settings; use task::{DebugScenario, RevealTarget, ZedDebugConfig}; use theme::ThemeSettings; use ui::{ - ActiveTheme, CheckboxWithLabel, Clickable, Context, ContextMenu, Disableable, DropdownMenu, - FluentBuilder, IconWithIndicator, Indicator, IntoElement, KeyBinding, ListItem, - ListItemSpacing, ParentElement, StyledExt, ToggleButton, ToggleState, Toggleable, Tooltip, - Window, div, prelude::*, px, relative, rems, + ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context, + ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize, + IconWithIndicator, Indicator, InteractiveElement, IntoElement, KeyBinding, Label, + LabelCommon as _, LabelSize, ListItem, ListItemSpacing, ParentElement, RenderOnce, + SharedString, Styled, StyledExt, ToggleButton, ToggleState, Toggleable, Tooltip, Window, div, + h_flex, relative, rems, v_flex, }; use util::ResultExt; -use workspace::{ModalView, Workspace, pane}; +use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr, pane}; use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel}; -#[allow(unused)] -enum SaveScenarioState { - Saving, - Saved((ProjectPath, SharedString)), - Failed(SharedString), -} - pub(super) struct NewProcessModal { workspace: WeakEntity, debug_panel: WeakEntity, @@ -56,7 +46,6 @@ pub(super) struct NewProcessModal { configure_mode: Entity, task_mode: TaskMode, debugger: Option, - save_scenario_state: Option, _subscriptions: [Subscription; 3], } @@ -268,7 +257,6 @@ impl NewProcessModal { mode, debug_panel: debug_panel.downgrade(), workspace: workspace_handle, - save_scenario_state: None, _subscriptions, } }); @@ -420,63 +408,29 @@ impl NewProcessModal { self.debug_picker.read(cx).delegate.task_contexts.clone() } - fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context) { - let task_contents = self.task_contexts(cx); + pub fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context) { + let task_contexts = self.task_contexts(cx); let Some(adapter) = self.debugger.as_ref() else { return; }; let scenario = self.debug_scenario(&adapter, cx); - - self.save_scenario_state = Some(SaveScenarioState::Saving); - cx.spawn_in(window, async move |this, cx| { - let Some((scenario, worktree_id)) = scenario - .await - .zip(task_contents.and_then(|tcx| tcx.worktree())) - else { - this.update(cx, |this, _| { - this.save_scenario_state = Some(SaveScenarioState::Failed( - "Couldn't get scenario or task contents".into(), - )) - }) - .ok(); - return; - }; - - let Some(save_scenario) = this - .update_in(cx, |this, window, cx| { - this.debug_panel - .update(cx, |panel, cx| { - panel.save_scenario(&scenario, worktree_id, window, cx) - }) - .ok() + let scenario = scenario.await.context("no scenario to save")?; + let worktree_id = task_contexts + .context("no task contexts")? + .worktree() + .context("no active worktree")?; + this.update_in(cx, |this, window, cx| { + this.debug_panel.update(cx, |panel, cx| { + panel.save_scenario(scenario, worktree_id, window, cx) }) - .ok() - .flatten() - else { - return; - }; - let res = save_scenario.await; - - this.update(cx, |this, _| match res { - Ok(saved_file) => { - this.save_scenario_state = Some(SaveScenarioState::Saved(( - saved_file, - scenario.label.clone(), - ))) - } - Err(error) => { - this.save_scenario_state = - Some(SaveScenarioState::Failed(error.to_string().into())) - } + })?? + .await?; + this.update_in(cx, |_, _, cx| { + cx.emit(DismissEvent); }) - .ok(); - - cx.background_executor().timer(Duration::from_secs(3)).await; - this.update(cx, |this, _| this.save_scenario_state.take()) - .ok(); }) - .detach(); + .detach_and_prompt_err("Failed to edit debug.json", window, cx, |_, _, _| None); } fn adapter_drop_down_menu( @@ -544,70 +498,6 @@ impl NewProcessModal { }), ) } - - fn open_debug_json(&self, window: &mut Window, cx: &mut Context) { - let this = cx.entity(); - window - .spawn(cx, async move |cx| { - let worktree_id = this.update(cx, |this, cx| { - let tcx = this.task_contexts(cx); - tcx?.worktree() - })?; - - let Some(worktree_id) = worktree_id else { - let _ = cx.prompt( - PromptLevel::Critical, - "Cannot open debug.json", - Some("You must have at least one project open"), - &[PromptButton::ok("Ok")], - ); - return Ok(()); - }; - - let editor = this - .update_in(cx, |this, window, cx| { - this.workspace.update(cx, |workspace, cx| { - workspace.open_path( - ProjectPath { - worktree_id, - path: local_debug_file_relative_path().into(), - }, - None, - true, - window, - cx, - ) - }) - })?? - .await?; - - cx.update(|_window, cx| { - if let Some(editor) = editor.act_as::(cx) { - editor.update(cx, |editor, cx| { - editor.buffer().update(cx, |buffer, cx| { - if let Some(singleton) = buffer.as_singleton() { - singleton.update(cx, |buffer, cx| { - if buffer.is_empty() { - buffer.edit( - [(0..0, initial_local_debug_tasks_content())], - None, - cx, - ); - } - }) - } - }) - }); - } - }) - .ok(); - - this.update(cx, |_, cx| cx.emit(DismissEvent)).ok(); - - anyhow::Ok(()) - }) - .detach(); - } } static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger"); @@ -812,39 +702,21 @@ impl Render for NewProcessModal { NewProcessMode::Launch => el.child( container .child( - h_flex() - .text_ui_sm(cx) - .text_color(Color::Muted.color(cx)) - .child( - InteractiveText::new( - "open-debug-json", - StyledText::new( - "Open .zed/debug.json for advanced configuration.", - ) - .with_highlights([( - 5..20, - HighlightStyle { - underline: Some(UnderlineStyle { - thickness: px(1.0), - color: None, - wavy: false, - }), - ..Default::default() - }, - )]), - ) - .on_click( - vec![5..20], - { - let this = cx.entity(); - move |_, window, cx| { - this.update(cx, |this, cx| { - this.open_debug_json(window, cx); - }) - } - }, + h_flex().child( + Button::new("edit-custom-debug", "Edit in 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), ), - ), + ), ) .child( Button::new("debugger-spawn", "Start") @@ -862,29 +734,48 @@ impl Render for NewProcessModal { ), ), ), - NewProcessMode::Attach => el.child( + NewProcessMode::Attach => el.child({ + let disabled = self.debugger.is_none() + || self + .attach_mode + .read(cx) + .attach_picker + .read(cx) + .picker + .read(cx) + .delegate + .match_count() + == 0; + let secondary_action = menu::SecondaryConfirm.boxed_clone(); container - .child(div().child(self.adapter_drop_down_menu(window, cx))) + .child(div().children( + KeyBinding::for_action(&*secondary_action, window, cx).map( + |keybind| { + Button::new("edit-attach-task", "Edit in debug.json") + .label_size(LabelSize::Small) + .key_binding(keybind) + .on_click(move |_, window, cx| { + window.dispatch_action( + secondary_action.boxed_clone(), + cx, + ) + }) + .disabled(disabled) + }, + ), + )) .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, + h_flex() + .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(disabled), ), - ), - ), + ) + }), NewProcessMode::Debug => el, NewProcessMode::Task => el, } @@ -1048,25 +939,6 @@ impl ConfigureMode { ) .checkbox_position(ui::IconPosition::End), ) - .child( - CheckboxWithLabel::new( - "debugger-save-to-debug-json", - Label::new("Save to debug.json") - .size(LabelSize::Small) - .color(Color::Muted), - self.save_to_debug_json, - { - let this = cx.weak_entity(); - move |state, _, cx| { - this.update(cx, |this, _| { - this.save_to_debug_json = *state; - }) - .ok(); - } - }, - ) - .checkbox_position(ui::IconPosition::End), - ) } } @@ -1329,12 +1201,7 @@ impl PickerDelegate for DebugDelegate { } } - fn confirm_input( - &mut self, - _secondary: bool, - window: &mut Window, - cx: &mut Context>, - ) { + fn confirm_input(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { let text = self.prompt.clone(); let (task_context, worktree_id) = self .task_contexts @@ -1364,7 +1231,7 @@ impl PickerDelegate for DebugDelegate { let args = args.collect::>(); let task = task::TaskTemplate { - label: "one-off".to_owned(), + label: "one-off".to_owned(), // TODO: rename using command as label env, command: program, args, @@ -1405,7 +1272,11 @@ impl PickerDelegate for DebugDelegate { .background_spawn(async move { for locator in locators { if let Some(scenario) = - locator.1.create_scenario(&task, "one-off", &adapter).await + // TODO: use a more informative label than "one-off" + locator + .1 + .create_scenario(&task, &task.label, &adapter) + .await { return Some(scenario); } @@ -1439,13 +1310,18 @@ impl PickerDelegate for DebugDelegate { .detach(); } - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { + fn confirm( + &mut self, + secondary: bool, + window: &mut Window, + cx: &mut Context>, + ) { let debug_scenario = self .matches .get(self.selected_index()) .and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned()); - let Some((_, debug_scenario, context)) = debug_scenario else { + let Some((kind, debug_scenario, context)) = debug_scenario else { return; }; @@ -1463,24 +1339,38 @@ impl PickerDelegate for DebugDelegate { }); let DebugScenarioContext { task_context, - active_buffer, + active_buffer: _, worktree_id, } = context; - let active_buffer = active_buffer.and_then(|buffer| buffer.upgrade()); - - send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx); - self.debug_panel - .update(cx, |panel, cx| { - panel.start_session( - debug_scenario, - task_context, - active_buffer, - worktree_id, - window, - cx, - ); + + if secondary { + let Some(kind) = kind else { return }; + let Some(id) = worktree_id else { return }; + let debug_panel = self.debug_panel.clone(); + cx.spawn_in(window, async move |_, cx| { + debug_panel + .update_in(cx, |debug_panel, window, cx| { + debug_panel.go_to_scenario_definition(kind, debug_scenario, id, window, cx) + })? + .await?; + anyhow::Ok(()) }) - .ok(); + .detach(); + } else { + send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx); + self.debug_panel + .update(cx, |panel, cx| { + panel.start_session( + debug_scenario, + task_context, + None, + worktree_id, + window, + cx, + ); + }) + .ok(); + } cx.emit(DismissEvent); } @@ -1498,19 +1388,23 @@ impl PickerDelegate for DebugDelegate { let footer = h_flex() .w_full() .p_1p5() - .justify_end() + .justify_between() .border_t_1() .border_color(cx.theme().colors().border_variant) - // .child( - // // TODO: add button to open selected task in debug.json - // h_flex().into_any_element(), - // ) + .children({ + let action = menu::SecondaryConfirm.boxed_clone(); + KeyBinding::for_action(&*action, window, cx).map(|keybind| { + Button::new("edit-debug-task", "Edit in debug.json") + .label_size(LabelSize::Small) + .key_binding(keybind) + .on_click(move |_, window, cx| { + window.dispatch_action(action.boxed_clone(), cx) + }) + }) + }) .map(|this| { if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() { - let action = picker::ConfirmInput { - secondary: current_modifiers.secondary(), - } - .boxed_clone(); + let action = picker::ConfirmInput { secondary: false }.boxed_clone(); this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| { Button::new("launch-custom", "Launch Custom") .key_binding(keybind) @@ -1607,3 +1501,35 @@ pub(crate) fn resolve_path(path: &mut String) { ); }; } + +#[cfg(test)] +impl NewProcessModal { + pub(crate) fn set_configure( + &mut self, + program: impl AsRef, + cwd: impl AsRef, + stop_on_entry: bool, + window: &mut Window, + cx: &mut Context, + ) { + self.mode = NewProcessMode::Launch; + self.debugger = Some(dap::adapters::DebugAdapterName("fake-adapter".into())); + + 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); + }); + + configure.cwd.update(cx, |editor, cx| { + editor.clear(window, cx); + editor.set_text(cwd.as_ref(), window, cx); + }); + + configure.stop_on_entry = match stop_on_entry { + true => ToggleState::Selected, + _ => ToggleState::Unselected, + } + }) + } +} diff --git a/crates/debugger_ui/src/tests/new_process_modal.rs b/crates/debugger_ui/src/tests/new_process_modal.rs index 81c5f7b5983bf351079ec4244e3e7b10170a28ca..a4616eaa3b640cd256b997b543852f772a9c3570 100644 --- a/crates/debugger_ui/src/tests/new_process_modal.rs +++ b/crates/debugger_ui/src/tests/new_process_modal.rs @@ -1,13 +1,15 @@ use dap::DapRegistry; +use editor::Editor; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; -use project::{FakeFs, Project}; +use project::{FakeFs, Fs as _, Project}; use serde_json::json; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use task::{DebugRequest, DebugScenario, LaunchRequest, TaskContext, VariableName, ZedDebugConfig}; +use text::Point; use util::path; -// use crate::new_process_modal::NewProcessMode; +use crate::NewProcessMode; use crate::tests::{init_test, init_test_workspace}; #[gpui::test] @@ -159,111 +161,127 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths( } } -// #[gpui::test] -// async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(executor.clone()); -// fs.insert_tree( -// path!("/project"), -// json!({ -// "main.rs": "fn main() {}" -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; -// let workspace = init_test_workspace(&project, cx).await; -// let cx = &mut VisualTestContext::from_window(*workspace, cx); - -// workspace -// .update(cx, |workspace, window, cx| { -// crate::new_process_modal::NewProcessModal::show( -// workspace, -// window, -// NewProcessMode::Debug, -// None, -// cx, -// ); -// }) -// .unwrap(); - -// cx.run_until_parked(); - -// let modal = workspace -// .update(cx, |workspace, _, cx| { -// workspace.active_modal::(cx) -// }) -// .unwrap() -// .expect("Modal should be active"); - -// modal.update_in(cx, |modal, window, cx| { -// modal.set_configure("/project/main", "/project", false, window, cx); -// modal.save_scenario(window, cx); -// }); - -// cx.executor().run_until_parked(); - -// let debug_json_content = fs -// .load(path!("/project/.zed/debug.json").as_ref()) -// .await -// .expect("debug.json should exist"); - -// let expected_content = vec![ -// "[", -// " {", -// r#" "adapter": "fake-adapter","#, -// r#" "label": "main (fake-adapter)","#, -// r#" "request": "launch","#, -// r#" "program": "/project/main","#, -// r#" "cwd": "/project","#, -// r#" "args": [],"#, -// r#" "env": {}"#, -// " }", -// "]", -// ]; - -// let actual_lines: Vec<&str> = debug_json_content.lines().collect(); -// pretty_assertions::assert_eq!(expected_content, actual_lines); - -// modal.update_in(cx, |modal, window, cx| { -// modal.set_configure("/project/other", "/project", true, window, cx); -// modal.save_scenario(window, cx); -// }); - -// cx.executor().run_until_parked(); - -// let debug_json_content = fs -// .load(path!("/project/.zed/debug.json").as_ref()) -// .await -// .expect("debug.json should exist after second save"); - -// let expected_content = vec![ -// "[", -// " {", -// r#" "adapter": "fake-adapter","#, -// r#" "label": "main (fake-adapter)","#, -// r#" "request": "launch","#, -// r#" "program": "/project/main","#, -// r#" "cwd": "/project","#, -// r#" "args": [],"#, -// r#" "env": {}"#, -// " },", -// " {", -// r#" "adapter": "fake-adapter","#, -// r#" "label": "other (fake-adapter)","#, -// r#" "request": "launch","#, -// r#" "program": "/project/other","#, -// r#" "cwd": "/project","#, -// r#" "args": [],"#, -// r#" "env": {}"#, -// " }", -// "]", -// ]; - -// let actual_lines: Vec<&str> = debug_json_content.lines().collect(); -// pretty_assertions::assert_eq!(expected_content, actual_lines); -// } +#[gpui::test] +async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + fs.insert_tree( + path!("/project"), + json!({ + "main.rs": "fn main() {}" + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + workspace + .update(cx, |workspace, window, cx| { + crate::new_process_modal::NewProcessModal::show( + workspace, + window, + NewProcessMode::Debug, + None, + cx, + ); + }) + .unwrap(); + + cx.run_until_parked(); + + let modal = workspace + .update(cx, |workspace, _, cx| { + workspace.active_modal::(cx) + }) + .unwrap() + .expect("Modal should be active"); + + modal.update_in(cx, |modal, window, cx| { + modal.set_configure("/project/main", "/project", false, window, cx); + modal.save_debug_scenario(window, cx); + }); + + cx.executor().run_until_parked(); + + let editor = workspace + .update(cx, |workspace, _window, cx| { + workspace.active_item_as::(cx).unwrap() + }) + .unwrap(); + + let debug_json_content = fs + .load(path!("/project/.zed/debug.json").as_ref()) + .await + .expect("debug.json should exist") + .lines() + .filter(|line| !line.starts_with("//")) + .collect::>() + .join("\n"); + + let expected_content = indoc::indoc! {r#" + [ + { + "adapter": "fake-adapter", + "label": "main (fake-adapter)", + "request": "launch", + "program": "/project/main", + "cwd": "/project", + "args": [], + "env": {} + } + ]"#}; + + pretty_assertions::assert_eq!(expected_content, debug_json_content); + + editor.update(cx, |editor, cx| { + assert_eq!( + editor.selections.newest::(cx).head(), + Point::new(5, 2) + ) + }); + + modal.update_in(cx, |modal, window, cx| { + modal.set_configure("/project/other", "/project", true, window, cx); + modal.save_debug_scenario(window, cx); + }); + + cx.executor().run_until_parked(); + + let expected_content = indoc::indoc! {r#" + [ + { + "adapter": "fake-adapter", + "label": "main (fake-adapter)", + "request": "launch", + "program": "/project/main", + "cwd": "/project", + "args": [], + "env": {} + }, + { + "adapter": "fake-adapter", + "label": "other (fake-adapter)", + "request": "launch", + "program": "/project/other", + "cwd": "/project", + "args": [], + "env": {} + } + ]"#}; + + let debug_json_content = fs + .load(path!("/project/.zed/debug.json").as_ref()) + .await + .expect("debug.json should exist") + .lines() + .filter(|line| !line.starts_with("//")) + .collect::>() + .join("\n"); + pretty_assertions::assert_eq!(expected_content, debug_json_content); +} #[gpui::test] async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 49f4fc52ac725bcef2707f2bf508a1fae811f434..d5c8eae99c2993163dbbde191524bad48965c7f6 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2829,6 +2829,7 @@ impl EditorElement { ) -> Vec { self.editor.update(cx, |editor, cx| { let active_task_indicator_row = + // TODO: add edit button on the right side of each row in the context menu if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu { deployed_from, actions, From 66a1c356bfc8510a2528d34c9df825d5db540e7a Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 7 Jul 2025 23:13:24 +0200 Subject: [PATCH 073/239] agent: Fix max token count mismatch when not using burn mode (#34025) Closes #31854 Release Notes: - agent: Fixed an issue where the maximum token count would be displayed incorrectly when burn mode was not being used. --- Cargo.lock | 4 +-- Cargo.toml | 2 +- crates/agent/src/thread.rs | 32 +++++++++++++++----- crates/agent/src/tool_use.rs | 12 ++++++-- crates/agent_ui/src/text_thread_editor.rs | 6 ++-- crates/language_model/src/language_model.rs | 18 ++++++++++- crates/language_models/src/provider/cloud.rs | 7 +++++ 7 files changed, 64 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 57b97cb853246c6b2747e383494ea29501332541..a19397bdf909226095943735d3afd34ac80723e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20145,9 +20145,9 @@ dependencies = [ [[package]] name = "zed_llm_client" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c740e29260b8797ad252c202ea09a255b3cbc13f30faaf92fb6b2490336106e0" +checksum = "6607f74dee2a18a9ce0f091844944a0e59881359ab62e0768fb0618f55d4c1dc" dependencies = [ "anyhow", "serde", diff --git a/Cargo.toml b/Cargo.toml index 82cbb533971316de0a7478a611d03fca55d1be12..8dd7892329e70098985225eb4493366389b1aca3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -625,7 +625,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.5" +zed_llm_client = "= 0.8.6" zstd = "0.11" [workspace.dependencies.async-stripe] diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 72417cfe999cd3e34f1b7ff8921bd8cb6e577895..1f2654dac5bf31481f79f00b03d9376f00bf6f03 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -23,10 +23,11 @@ use gpui::{ }; use language_model::{ ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, - LanguageModelId, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, - LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent, - LanguageModelToolUse, LanguageModelToolUseId, MessageContent, ModelRequestLimitReachedError, - PaymentRequiredError, Role, SelectedModel, StopReason, TokenUsage, + LanguageModelExt as _, LanguageModelId, LanguageModelRegistry, LanguageModelRequest, + LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult, + LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, MessageContent, + ModelRequestLimitReachedError, PaymentRequiredError, Role, SelectedModel, StopReason, + TokenUsage, }; use postage::stream::Stream as _; use project::{ @@ -1582,6 +1583,7 @@ impl Thread { tool_name, tool_output, self.configured_model.as_ref(), + self.completion_mode, ); pending_tool_use @@ -1610,6 +1612,10 @@ impl Thread { prompt_id: prompt_id.clone(), }; + let completion_mode = request + .mode + .unwrap_or(zed_llm_client::CompletionMode::Normal); + self.last_received_chunk_at = Some(Instant::now()); let task = cx.spawn(async move |thread, cx| { @@ -1959,7 +1965,11 @@ impl Thread { .unwrap_or(0) // We know the context window was exceeded in practice, so if our estimate was // lower than max tokens, the estimate was wrong; return that we exceeded by 1. - .max(model.max_token_count().saturating_add(1)) + .max( + model + .max_token_count_for_mode(completion_mode) + .saturating_add(1), + ) }); thread.exceeded_window_error = Some(ExceededWindowError { model_id: model.id(), @@ -2507,6 +2517,7 @@ impl Thread { hallucinated_tool_name, Err(anyhow!("Missing tool call: {error_message}")), self.configured_model.as_ref(), + self.completion_mode, ); cx.emit(ThreadEvent::MissingToolUse { @@ -2533,6 +2544,7 @@ impl Thread { tool_name, Err(anyhow!("Error parsing input JSON: {error}")), self.configured_model.as_ref(), + self.completion_mode, ); let ui_text = if let Some(pending_tool_use) = &pending_tool_use { pending_tool_use.ui_text.clone() @@ -2608,6 +2620,7 @@ impl Thread { tool_name, output, thread.configured_model.as_ref(), + thread.completion_mode, ); thread.tool_finished(tool_use_id, pending_tool_use, false, window, cx); }) @@ -3084,7 +3097,9 @@ impl Thread { return TotalTokenUsage::default(); }; - let max = model.model.max_token_count(); + let max = model + .model + .max_token_count_for_mode(self.completion_mode().into()); let index = self .messages @@ -3111,7 +3126,9 @@ impl Thread { pub fn total_token_usage(&self) -> Option { let model = self.configured_model.as_ref()?; - let max = model.model.max_token_count(); + let max = model + .model + .max_token_count_for_mode(self.completion_mode().into()); if let Some(exceeded_error) = &self.exceeded_window_error { if model.model.id() == exceeded_error.model_id { @@ -3177,6 +3194,7 @@ impl Thread { tool_name, err, self.configured_model.as_ref(), + self.completion_mode, ); self.tool_finished(tool_use_id.clone(), None, true, window, cx); } diff --git a/crates/agent/src/tool_use.rs b/crates/agent/src/tool_use.rs index 76de3d20223fcd1c22631029d2040c9109d9ac0d..74c719b4e6cf4ad0743a833f8b1c9fcc9da8b929 100644 --- a/crates/agent/src/tool_use.rs +++ b/crates/agent/src/tool_use.rs @@ -2,6 +2,7 @@ use crate::{ thread::{MessageId, PromptId, ThreadId}, thread_store::SerializedMessage, }; +use agent_settings::CompletionMode; use anyhow::Result; use assistant_tool::{ AnyToolCard, Tool, ToolResultContent, ToolResultOutput, ToolUseStatus, ToolWorkingSet, @@ -11,8 +12,9 @@ use futures::{FutureExt as _, future::Shared}; use gpui::{App, Entity, SharedString, Task, Window}; use icons::IconName; use language_model::{ - ConfiguredModel, LanguageModel, LanguageModelRequest, LanguageModelToolResult, - LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, Role, + ConfiguredModel, LanguageModel, LanguageModelExt, LanguageModelRequest, + LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolUse, + LanguageModelToolUseId, Role, }; use project::Project; use std::sync::Arc; @@ -400,6 +402,7 @@ impl ToolUseState { tool_name: Arc, output: Result, configured_model: Option<&ConfiguredModel>, + completion_mode: CompletionMode, ) -> Option { let metadata = self.tool_use_metadata_by_id.remove(&tool_use_id); @@ -426,7 +429,10 @@ impl ToolUseState { // Protect from overly large output let tool_output_limit = configured_model - .map(|model| model.model.max_token_count() as usize * BYTES_PER_TOKEN_ESTIMATE) + .map(|model| { + model.model.max_token_count_for_mode(completion_mode.into()) as usize + * BYTES_PER_TOKEN_ESTIMATE + }) .unwrap_or(usize::MAX); let content = match tool_result { diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 465b3b4e58a3737032c0758da5f6af6dce092c2c..de7606dbfb333e524c81435432050cfba1b71831 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -38,8 +38,8 @@ use language::{ language_settings::{SoftWrap, all_language_settings}, }; use language_model::{ - ConfigurationError, LanguageModelImage, LanguageModelProviderTosView, LanguageModelRegistry, - Role, + ConfigurationError, LanguageModelExt, LanguageModelImage, LanguageModelProviderTosView, + LanguageModelRegistry, Role, }; use multi_buffer::MultiBufferRow; use picker::{Picker, popover_menu::PickerPopoverMenu}; @@ -3063,7 +3063,7 @@ fn token_state(context: &Entity, cx: &App) -> Option u64; + /// Returns the maximum token count for this model in burn mode (If `supports_burn_mode` is `false` this returns `None`) + fn max_token_count_in_burn_mode(&self) -> Option { + None + } fn max_output_tokens(&self) -> Option { None } @@ -557,6 +561,18 @@ pub trait LanguageModel: Send + Sync { } } +pub trait LanguageModelExt: LanguageModel { + fn max_token_count_for_mode(&self, mode: CompletionMode) -> u64 { + match mode { + CompletionMode::Normal => self.max_token_count(), + CompletionMode::Max => self + .max_token_count_in_burn_mode() + .unwrap_or_else(|| self.max_token_count()), + } + } +} +impl LanguageModelExt for dyn LanguageModel {} + pub trait LanguageModelTool: 'static + DeserializeOwned + JsonSchema { fn name() -> String; fn description() -> String; diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 505caa2e42b27f21e07cda9dc55252dfdde403b1..1cd673710c47a745f9a7afc02ac37eb285eb0a68 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -730,6 +730,13 @@ impl LanguageModel for CloudLanguageModel { self.model.max_token_count as u64 } + fn max_token_count_in_burn_mode(&self) -> Option { + self.model + .max_token_count_in_max_mode + .filter(|_| self.model.supports_max_mode) + .map(|max_token_count| max_token_count as u64) + } + fn cache_configuration(&self) -> Option { match &self.model.provider { zed_llm_client::LanguageModelProvider::Anthropic => { From 877ef5e1b1fcfeabe8cdd203239d2738a0991f19 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 7 Jul 2025 16:54:51 -0500 Subject: [PATCH 074/239] keymap_ui: Add auto-complete for context in keybind editor (#34031) Closes #ISSUE Implements a very basic completion provider that is attached to the context editor in the keybind editing modal. The context identifiers used for completions are scraped from the default, vim, and base keymaps on demand. Release Notes: - N/A *or* Added/Fixed/Improved ... --- Cargo.lock | 1 - .../src/base_keymap_setting.rs | 4 +- crates/settings/src/keymap_file.rs | 2 +- crates/settings/src/settings.rs | 3 + crates/settings_ui/src/keybindings.rs | 152 +++++++++++++++++- crates/welcome/Cargo.toml | 1 - crates/welcome/src/base_keymap_picker.rs | 3 +- crates/welcome/src/welcome.rs | 4 - crates/zed/src/main.rs | 4 +- crates/zed/src/zed.rs | 9 +- 10 files changed, 163 insertions(+), 20 deletions(-) rename crates/{welcome => settings}/src/base_keymap_setting.rs (96%) diff --git a/Cargo.lock b/Cargo.lock index a19397bdf909226095943735d3afd34ac80723e8..58e482ee39bc5d89870671b77a1bfdd5a4762e9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18359,7 +18359,6 @@ dependencies = [ "language", "picker", "project", - "schemars", "serde", "settings", "telemetry", diff --git a/crates/welcome/src/base_keymap_setting.rs b/crates/settings/src/base_keymap_setting.rs similarity index 96% rename from crates/welcome/src/base_keymap_setting.rs rename to crates/settings/src/base_keymap_setting.rs index b841b69f9d25c5b84d46b0b192f8dffd9929fe02..6916d98ae336629df1ebd2ba58be4f5491b8bd82 100644 --- a/crates/welcome/src/base_keymap_setting.rs +++ b/crates/settings/src/base_keymap_setting.rs @@ -1,8 +1,8 @@ use std::fmt::{Display, Formatter}; +use crate::{Settings, SettingsSources, VsCodeSettings}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; /// Base key bindings scheme. Base keymaps can be overridden with user keymaps. /// @@ -114,7 +114,7 @@ impl Settings for BaseKeymap { sources.default.ok_or_else(Self::missing_default) } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + fn import_from_vscode(_vscode: &VsCodeSettings, current: &mut Self::FileContent) { *current = Some(BaseKeymap::VSCode); } } diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index ca54b6a877361af15a634ec7ce3c247ffeaff49f..b91739ca87c72c13c1cfe283b80c8ef260c31f6a 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -63,7 +63,7 @@ pub struct KeymapSection { /// current file extension are also supported - see [the /// documentation](https://zed.dev/docs/key-bindings#contexts) for more details. #[serde(default)] - context: String, + pub context: String, /// This option enables specifying keys based on their position on a QWERTY keyboard, by using /// position-equivalent mappings for some non-QWERTY keyboards. This is currently only supported /// on macOS. See the documentation for more details. diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index f690a2ea936c6516b6d4a60701a7cce89fa50cb2..4e6bd94d92bc09009b2775b7b43f9962341c5f94 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -1,3 +1,4 @@ +mod base_keymap_setting; mod editable_setting_control; mod key_equivalents; mod keymap_file; @@ -11,6 +12,7 @@ use rust_embed::RustEmbed; use std::{borrow::Cow, fmt, str}; use util::asset_str; +pub use base_keymap_setting::*; pub use editable_setting_control::*; pub use key_equivalents::*; pub use keymap_file::{ @@ -71,6 +73,7 @@ pub fn init(cx: &mut App) { .set_default_settings(&default_settings(), cx) .unwrap(); cx.set_global(settings); + BaseKeymap::register(cx); } pub fn default_settings() -> Cow<'static, str> { diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 34d4b8585256d12b62b725d360679225b7360a82..2dd693c798a7a4c91eb89d9d404cc2becf143197 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -5,7 +5,7 @@ use std::{ use anyhow::{Context as _, anyhow}; use collections::HashSet; -use editor::{Editor, EditorEvent}; +use editor::{CompletionProvider, Editor, EditorEvent}; use feature_flags::FeatureFlagViewExt; use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; @@ -14,8 +14,8 @@ use gpui::{ Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, StyledText, Subscription, WeakEntity, actions, div, transparent_black, }; -use language::{Language, LanguageConfig}; -use settings::KeybindSource; +use language::{Language, LanguageConfig, ToOffset as _}; +use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets}; use util::ResultExt; @@ -850,8 +850,10 @@ impl KeybindingEditorModal { cx: &mut App, ) -> Self { let keybind_editor = cx.new(KeystrokeInput::new); + let context_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); + if let Some(context) = editing_keybind .context .as_ref() @@ -862,6 +864,21 @@ impl KeybindingEditorModal { editor.set_placeholder_text("Keybinding context", cx); } + cx.spawn(async |editor, cx| { + let contexts = cx + .background_spawn(async { collect_contexts_from_assets() }) + .await; + + editor + .update(cx, |editor, _cx| { + editor.set_completion_provider(Some(std::rc::Rc::new( + KeyContextCompletionProvider { contexts }, + ))); + }) + .context("Failed to load completions for keybinding context") + }) + .detach_and_log_err(cx); + editor }); Self { @@ -1001,6 +1018,69 @@ impl Render for KeybindingEditorModal { } } +struct KeyContextCompletionProvider { + contexts: Vec, +} + +impl CompletionProvider for KeyContextCompletionProvider { + fn completions( + &self, + _excerpt_id: editor::ExcerptId, + buffer: &Entity, + buffer_position: language::Anchor, + _trigger: editor::CompletionContext, + _window: &mut Window, + cx: &mut Context, + ) -> gpui::Task>> { + let buffer = buffer.read(cx); + let mut count_back = 0; + for char in buffer.reversed_chars_at(buffer_position) { + if char.is_ascii_alphanumeric() || char == '_' { + count_back += 1; + } else { + break; + } + } + let start_anchor = buffer.anchor_before( + buffer_position + .to_offset(&buffer) + .saturating_sub(count_back), + ); + let replace_range = start_anchor..buffer_position; + gpui::Task::ready(Ok(vec![project::CompletionResponse { + completions: self + .contexts + .iter() + .map(|context| project::Completion { + replace_range: replace_range.clone(), + label: language::CodeLabel::plain(context.to_string(), None), + new_text: context.to_string(), + documentation: None, + source: project::CompletionSource::Custom, + icon_path: None, + insert_text_mode: None, + confirm: None, + }) + .collect(), + is_incomplete: false, + }])) + } + + fn is_completion_trigger( + &self, + _buffer: &Entity, + _position: language::Anchor, + text: &str, + _trigger_in_words: bool, + _menu_is_open: bool, + _cx: &mut Context, + ) -> bool { + text.chars().last().map_or(false, |last_char| { + last_char.is_ascii_alphanumeric() || last_char == '_' + }) + } +} + async fn save_keybinding_update( existing: ProcessedKeybinding, new_keystrokes: &[Keystroke], @@ -1254,6 +1334,72 @@ fn build_keybind_context_menu( }) } +fn collect_contexts_from_assets() -> Vec { + let mut keymap_assets = vec![ + util::asset_str::(settings::DEFAULT_KEYMAP_PATH), + util::asset_str::(settings::VIM_KEYMAP_PATH), + ]; + keymap_assets.extend( + BaseKeymap::OPTIONS + .iter() + .filter_map(|(_, base_keymap)| base_keymap.asset_path()) + .map(util::asset_str::), + ); + + let mut contexts = HashSet::default(); + + for keymap_asset in keymap_assets { + let Ok(keymap) = KeymapFile::parse(&keymap_asset) else { + continue; + }; + + for section in keymap.sections() { + let context_expr = §ion.context; + let mut queue = Vec::new(); + let Ok(root_context) = gpui::KeyBindingContextPredicate::parse(context_expr) else { + continue; + }; + + queue.push(root_context); + while let Some(context) = queue.pop() { + match context { + gpui::KeyBindingContextPredicate::Identifier(ident) => { + contexts.insert(ident); + } + gpui::KeyBindingContextPredicate::Equal(ident_a, ident_b) => { + contexts.insert(ident_a); + contexts.insert(ident_b); + } + gpui::KeyBindingContextPredicate::NotEqual(ident_a, ident_b) => { + contexts.insert(ident_a); + contexts.insert(ident_b); + } + gpui::KeyBindingContextPredicate::Child(ctx_a, ctx_b) => { + queue.push(*ctx_a); + queue.push(*ctx_b); + } + gpui::KeyBindingContextPredicate::Not(ctx) => { + queue.push(*ctx); + } + gpui::KeyBindingContextPredicate::And(ctx_a, ctx_b) => { + queue.push(*ctx_a); + queue.push(*ctx_b); + } + gpui::KeyBindingContextPredicate::Or(ctx_a, ctx_b) => { + queue.push(*ctx_a); + queue.push(*ctx_b); + } + } + } + } + } + + let mut contexts = contexts.into_iter().collect::>(); + contexts.sort(); + + return contexts; +} + impl SerializableItem for KeymapEditor { fn serialized_item_kind() -> &'static str { "KeymapEditor" diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index 6d4896016c7106527ecfe3c7a89da65c1126dc19..769dd8d6aa1591b2bd9c62dcb8c9ad9e48ad457b 100644 --- a/crates/welcome/Cargo.toml +++ b/crates/welcome/Cargo.toml @@ -26,7 +26,6 @@ install_cli.workspace = true language.workspace = true picker.workspace = true project.workspace = true -schemars.workspace = true serde.workspace = true settings.workspace = true telemetry.workspace = true diff --git a/crates/welcome/src/base_keymap_picker.rs b/crates/welcome/src/base_keymap_picker.rs index d5a6ae96da1345f4cefe6ac722e040cc82192f26..92317ca7113aee06025d2490656e869c7b4a5b0a 100644 --- a/crates/welcome/src/base_keymap_picker.rs +++ b/crates/welcome/src/base_keymap_picker.rs @@ -1,4 +1,3 @@ -use super::base_keymap_setting::BaseKeymap; use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; use gpui::{ App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, Task, WeakEntity, Window, @@ -6,7 +5,7 @@ use gpui::{ }; use picker::{Picker, PickerDelegate}; use project::Fs; -use settings::{Settings, update_settings_file}; +use settings::{BaseKeymap, Settings, update_settings_file}; use std::sync::Arc; use ui::{ListItem, ListItemSpacing, prelude::*}; use util::ResultExt; diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 74d7323d8cea4f0b427c1a0cb3ac1291d838baee..ea4ac13de7f41f4b46da6b465f1e22076a5bae7b 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -17,11 +17,9 @@ use workspace::{ open_new, }; -pub use base_keymap_setting::BaseKeymap; pub use multibuffer_hint::*; mod base_keymap_picker; -mod base_keymap_setting; mod multibuffer_hint; mod welcome_ui; @@ -37,8 +35,6 @@ pub const FIRST_OPEN: &str = "first_open"; pub const DOCS_URL: &str = "https://zed.dev/docs/"; pub fn init(cx: &mut App) { - BaseKeymap::register(cx); - cx.observe_new(|workspace: &mut Workspace, _, _cx| { workspace.register_action(|workspace, _: &Welcome, window, cx| { let welcome_page = WelcomePage::new(workspace, cx); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e04e9c38c15b7ed1bc95bffc0d702b013150b3a5..3c46c486a8abce4db926aa650bd349d92eaecd9c 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -29,7 +29,7 @@ use project::project_settings::ProjectSettings; use recent_projects::{SshSettings, open_ssh_project}; use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use session::{AppSession, Session}; -use settings::{Settings, SettingsStore, watch_config_file}; +use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file}; use std::{ env, io::{self, IsTerminal}, @@ -43,7 +43,7 @@ use theme::{ }; use util::{ConnectionResult, ResultExt, TryFutureExt, maybe}; use uuid::Uuid; -use welcome::{BaseKeymap, FIRST_OPEN, show_welcome_view}; +use welcome::{FIRST_OPEN, show_welcome_view}; use workspace::{ AppState, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, WorkspaceStore, notifications::NotificationId, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 10fdcf34a6a1de867668163f15fd3dfe0434f09c..dc094a6c12fb1ba11642cc988f5d06d2cce01078 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -48,9 +48,10 @@ use release_channel::{AppCommitSha, ReleaseChannel}; use rope::Rope; use search::project_search::ProjectSearchBar; use settings::{ - DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeybindSource, KeymapFile, KeymapFileLoadResult, - Settings, SettingsStore, VIM_KEYMAP_PATH, initial_local_debug_tasks_content, - initial_project_settings_content, initial_tasks_content, update_settings_file, + BaseKeymap, DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeybindSource, KeymapFile, + KeymapFileLoadResult, Settings, SettingsStore, VIM_KEYMAP_PATH, + initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content, + update_settings_file, }; use std::path::PathBuf; use std::sync::atomic::{self, AtomicBool}; @@ -62,7 +63,7 @@ use util::markdown::MarkdownString; use util::{ResultExt, asset_str}; use uuid::Uuid; use vim_mode_setting::VimModeSetting; -use welcome::{BaseKeymap, DOCS_URL, MultibufferHint}; +use welcome::{DOCS_URL, MultibufferHint}; use workspace::notifications::{NotificationId, dismiss_app_notification, show_app_notification}; use workspace::{ AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings, From 9b7632d5f6621e2fd188b1b2b04b386a9e7b7746 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Mon, 7 Jul 2025 18:39:11 -0400 Subject: [PATCH 075/239] Automatically adjust ANSI color contrast (#34033) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #33253 in a way that doesn't regress #32175 - namely, automatically adjusts the contrast between the foreground and background text in the terminal such that it's above a certain threshold. The threshold is configurable in settings, and can be set to 0 to turn off this feature and use exactly the colors the theme specifies even if they are illegible. ## One Light Theme Before Screenshot 2025-07-07 at 6 00 47 PM (Last row is highlighted because otherwise the text is unreadable; the foreground and background are the same color.) ## One Light Theme After (This is with the new default contrast adjustment setting.) Screenshot 2025-07-07 at 6 22 02 PM This approach was inspired by @mitchellh's use of automatic contrast adjustment in [Ghostty](https://ghostty.org/) - thanks, Mitchell! The main difference is that we're using APCA's formula instead of WCAG for [these reasons](https://khan-tw.medium.com/wcag2-are-you-still-using-it-ui-contrast-visibility-standard-readability-contrast-f34eb73e89ee). Release Notes: - Added automatic dynamic contrast adjustment for terminal foreground and background colors --- assets/settings/default.json | 17 +- crates/repl/src/outputs/plain.rs | 14 +- crates/terminal/src/terminal_settings.rs | 29 +- crates/terminal_view/src/color_contrast.rs | 474 +++++++++++++++++++ crates/terminal_view/src/terminal_element.rs | 142 +++++- crates/terminal_view/src/terminal_view.rs | 1 + 6 files changed, 669 insertions(+), 8 deletions(-) create mode 100644 crates/terminal_view/src/color_contrast.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index 48cdd665e1745fdcacb15b6317ade6ec2dd4480b..924b97008482e7f739b642b1730beaa9a60e6e22 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1352,7 +1352,7 @@ // 5. Never show the scrollbar: // "never" "show": null - } + }, // Set the terminal's font size. If this option is not included, // the terminal will default to matching the buffer's font size. // "font_size": 15, @@ -1369,6 +1369,21 @@ // Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling. // Existing terminals will not pick up this change until they are recreated. // "max_scroll_history_lines": 10000, + // The minimum APCA perceptual contrast between foreground and background colors. + // APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x, + // especially for dark mode. Values range from 0 to 106. + // + // Based on APCA Readability Criterion (ARC) Bronze Simple Mode: + // https://readtech.org/ARC/tests/bronze-simple-mode/ + // - 0: No contrast adjustment + // - 45: Minimum for large fluent text (36px+) + // - 60: Minimum for other content text + // - 75: Minimum for body text + // - 90: Preferred for body text + // + // Most terminal themes have APCA values of 40-70. + // A value of 45 preserves colorful themes while ensuring legibility. + "minimum_contrast": 45 }, "code_actions_on_format": {}, // Settings related to running tasks. diff --git a/crates/repl/src/outputs/plain.rs b/crates/repl/src/outputs/plain.rs index 237c86d8cce3fdddf9cf6d7631dd42daf55bce1d..515bc654f00732888c6ad709d4bce9596c1b5cbb 100644 --- a/crates/repl/src/outputs/plain.rs +++ b/crates/repl/src/outputs/plain.rs @@ -25,6 +25,7 @@ use alacritty_terminal::{ use gpui::{Bounds, ClipboardItem, Entity, FontStyle, TextStyle, WhiteSpace, canvas, size}; use language::Buffer; use settings::Settings as _; +use terminal::terminal_settings::TerminalSettings; use terminal_view::terminal_element::TerminalElement; use theme::ThemeSettings; use ui::{IntoElement, prelude::*}; @@ -257,8 +258,17 @@ impl Render for TerminalOutput { point: ic.point, cell: ic.cell.clone(), }); - let (cells, rects) = - TerminalElement::layout_grid(grid, 0, &text_style, text_system, None, window, cx); + let minimum_contrast = TerminalSettings::get_global(cx).minimum_contrast; + let (cells, rects) = TerminalElement::layout_grid( + grid, + 0, + &text_style, + text_system, + None, + minimum_contrast, + window, + cx, + ); // lines are 0-indexed, so we must add 1 to get the number of lines let text_line_height = text_style.line_height_in_pixels(window.rem_size()); diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index f1b729987a61dc7d36e6c8aed00e646351cff57c..31c32dbdca22a73fddda7cd9334c6cde76a99c8b 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -49,6 +49,7 @@ pub struct TerminalSettings { pub max_scroll_history_lines: Option, pub toolbar: Toolbar, pub scrollbar: ScrollbarSettings, + pub minimum_contrast: f32, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -229,6 +230,21 @@ pub struct TerminalSettingsContent { pub toolbar: Option, /// Scrollbar-related settings pub scrollbar: Option, + /// The minimum APCA perceptual contrast between foreground and background colors. + /// + /// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x, + /// especially for dark mode. Values range from 0 to 106. + /// + /// Based on APCA Readability Criterion (ARC) Bronze Simple Mode: + /// https://readtech.org/ARC/tests/bronze-simple-mode/ + /// - 0: No contrast adjustment + /// - 45: Minimum for large fluent text (36px+) + /// - 60: Minimum for other content text + /// - 75: Minimum for body text + /// - 90: Preferred for body text + /// + /// Default: 0 (no adjustment) + pub minimum_contrast: Option, } impl settings::Settings for TerminalSettings { @@ -237,7 +253,18 @@ impl settings::Settings for TerminalSettings { type FileContent = TerminalSettingsContent; fn load(sources: SettingsSources, _: &mut App) -> anyhow::Result { - sources.json_merge() + let settings: Self = sources.json_merge()?; + + // Validate minimum_contrast for APCA + if settings.minimum_contrast < 0.0 || settings.minimum_contrast > 106.0 { + anyhow::bail!( + "terminal.minimum_contrast must be between 0 and 106, but got {}. \ + APCA values: 0 = no adjustment, 75 = recommended for body text, 106 = maximum contrast.", + settings.minimum_contrast + ); + } + + Ok(settings) } fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { diff --git a/crates/terminal_view/src/color_contrast.rs b/crates/terminal_view/src/color_contrast.rs new file mode 100644 index 0000000000000000000000000000000000000000..fe4a881cea2f2dcc0ad05198bfd0bdd5f0aa9f2a --- /dev/null +++ b/crates/terminal_view/src/color_contrast.rs @@ -0,0 +1,474 @@ +use gpui::Hsla; + +/// APCA (Accessible Perceptual Contrast Algorithm) constants +/// Based on APCA 0.0.98G-4g W3 compatible constants +/// https://github.com/Myndex/apca-w3 +struct APCAConstants { + // Main TRC exponent for monitor perception + main_trc: f32, + + // sRGB coefficients + s_rco: f32, + s_gco: f32, + s_bco: f32, + + // G-4g constants for use with 2.4 exponent + norm_bg: f32, + norm_txt: f32, + rev_txt: f32, + rev_bg: f32, + + // G-4g Clamps and Scalers + blk_thrs: f32, + blk_clmp: f32, + scale_bow: f32, + scale_wob: f32, + lo_bow_offset: f32, + lo_wob_offset: f32, + delta_y_min: f32, + lo_clip: f32, +} + +impl Default for APCAConstants { + fn default() -> Self { + Self { + main_trc: 2.4, + s_rco: 0.2126729, + s_gco: 0.7151522, + s_bco: 0.0721750, + norm_bg: 0.56, + norm_txt: 0.57, + rev_txt: 0.62, + rev_bg: 0.65, + blk_thrs: 0.022, + blk_clmp: 1.414, + scale_bow: 1.14, + scale_wob: 1.14, + lo_bow_offset: 0.027, + lo_wob_offset: 0.027, + delta_y_min: 0.0005, + lo_clip: 0.1, + } + } +} + +/// Calculates the perceptual lightness contrast using APCA. +/// Returns a value between approximately -108 and 106. +/// Negative values indicate light text on dark background. +/// Positive values indicate dark text on light background. +/// +/// The APCA algorithm is more perceptually accurate than WCAG 2.x, +/// especially for dark mode interfaces. Key improvements include: +/// - Better accuracy for dark backgrounds +/// - Polarity-aware (direction matters) +/// - Perceptually uniform across the range +/// +/// Common APCA Lc thresholds per ARC Bronze Simple Mode: +/// https://readtech.org/ARC/tests/bronze-simple-mode/ +/// - Lc 45: Minimum for large fluent text (36px+) +/// - Lc 60: Minimum for other content text +/// - Lc 75: Minimum for body text +/// - Lc 90: Preferred for body text +/// +/// Most terminal themes use colors with APCA values of 40-70. +/// +/// https://github.com/Myndex/apca-w3 +pub fn apca_contrast(text_color: Hsla, background_color: Hsla) -> f32 { + let constants = APCAConstants::default(); + + let text_y = srgb_to_y(text_color, &constants); + let bg_y = srgb_to_y(background_color, &constants); + + // Apply soft clamp to near-black colors + let text_y_clamped = if text_y > constants.blk_thrs { + text_y + } else { + text_y + (constants.blk_thrs - text_y).powf(constants.blk_clmp) + }; + + let bg_y_clamped = if bg_y > constants.blk_thrs { + bg_y + } else { + bg_y + (constants.blk_thrs - bg_y).powf(constants.blk_clmp) + }; + + // Return 0 for extremely low delta Y + if (bg_y_clamped - text_y_clamped).abs() < constants.delta_y_min { + return 0.0; + } + + let sapc; + let output_contrast; + + if bg_y_clamped > text_y_clamped { + // Normal polarity: dark text on light background + sapc = (bg_y_clamped.powf(constants.norm_bg) - text_y_clamped.powf(constants.norm_txt)) + * constants.scale_bow; + + // Low contrast smooth rollout to prevent polarity reversal + output_contrast = if sapc < constants.lo_clip { + 0.0 + } else { + sapc - constants.lo_bow_offset + }; + } else { + // Reverse polarity: light text on dark background + sapc = (bg_y_clamped.powf(constants.rev_bg) - text_y_clamped.powf(constants.rev_txt)) + * constants.scale_wob; + + output_contrast = if sapc > -constants.lo_clip { + 0.0 + } else { + sapc + constants.lo_wob_offset + }; + } + + // Return Lc (lightness contrast) scaled to percentage + output_contrast * 100.0 +} + +/// Converts sRGB color to Y (luminance) for APCA calculation +fn srgb_to_y(color: Hsla, constants: &APCAConstants) -> f32 { + let rgba = color.to_rgb(); + + // Linearize and apply coefficients + let r_linear = (rgba.r).powf(constants.main_trc); + let g_linear = (rgba.g).powf(constants.main_trc); + let b_linear = (rgba.b).powf(constants.main_trc); + + constants.s_rco * r_linear + constants.s_gco * g_linear + constants.s_bco * b_linear +} + +/// Adjusts the foreground color to meet the minimum APCA contrast against the background. +/// The minimum_apca_contrast should be an absolute value (e.g., 75 for Lc 75). +/// +/// This implementation gradually adjusts the lightness while preserving the hue and +/// saturation as much as possible, only falling back to black/white when necessary. +pub fn ensure_minimum_contrast( + foreground: Hsla, + background: Hsla, + minimum_apca_contrast: f32, +) -> Hsla { + if minimum_apca_contrast <= 0.0 { + return foreground; + } + + let current_contrast = apca_contrast(foreground, background).abs(); + + if current_contrast >= minimum_apca_contrast { + return foreground; + } + + // First, try to adjust lightness while preserving hue and saturation + let adjusted = adjust_lightness_for_contrast(foreground, background, minimum_apca_contrast); + + let adjusted_contrast = apca_contrast(adjusted, background).abs(); + if adjusted_contrast >= minimum_apca_contrast { + return adjusted; + } + + // If that's not enough, gradually reduce saturation while adjusting lightness + let desaturated = + adjust_lightness_and_saturation_for_contrast(foreground, background, minimum_apca_contrast); + + let desaturated_contrast = apca_contrast(desaturated, background).abs(); + if desaturated_contrast >= minimum_apca_contrast { + return desaturated; + } + + // Last resort: use black or white + let black = Hsla { + h: 0.0, + s: 0.0, + l: 0.0, + a: foreground.a, + }; + + let white = Hsla { + h: 0.0, + s: 0.0, + l: 1.0, + a: foreground.a, + }; + + let black_contrast = apca_contrast(black, background).abs(); + let white_contrast = apca_contrast(white, background).abs(); + + if white_contrast > black_contrast { + white + } else { + black + } +} + +/// Adjusts only the lightness to meet the minimum contrast, preserving hue and saturation +fn adjust_lightness_for_contrast( + foreground: Hsla, + background: Hsla, + minimum_apca_contrast: f32, +) -> Hsla { + // Determine if we need to go lighter or darker + let bg_luminance = srgb_to_y(background, &APCAConstants::default()); + let should_go_darker = bg_luminance > 0.5; + + // Binary search for the optimal lightness + let mut low = if should_go_darker { 0.0 } else { foreground.l }; + let mut high = if should_go_darker { foreground.l } else { 1.0 }; + let mut best_l = foreground.l; + + for _ in 0..20 { + let mid = (low + high) / 2.0; + let test_color = Hsla { + h: foreground.h, + s: foreground.s, + l: mid, + a: foreground.a, + }; + + let contrast = apca_contrast(test_color, background).abs(); + + if contrast >= minimum_apca_contrast { + best_l = mid; + // Try to get closer to the minimum + if should_go_darker { + low = mid; + } else { + high = mid; + } + } else { + if should_go_darker { + high = mid; + } else { + low = mid; + } + } + + // If we're close enough to the target, stop + if (contrast - minimum_apca_contrast).abs() < 1.0 { + best_l = mid; + break; + } + } + + Hsla { + h: foreground.h, + s: foreground.s, + l: best_l, + a: foreground.a, + } +} + +/// Adjusts both lightness and saturation to meet the minimum contrast +fn adjust_lightness_and_saturation_for_contrast( + foreground: Hsla, + background: Hsla, + minimum_apca_contrast: f32, +) -> Hsla { + // Try different saturation levels + let saturation_steps = [1.0, 0.8, 0.6, 0.4, 0.2, 0.0]; + + for &sat_multiplier in &saturation_steps { + let test_color = Hsla { + h: foreground.h, + s: foreground.s * sat_multiplier, + l: foreground.l, + a: foreground.a, + }; + + let adjusted = adjust_lightness_for_contrast(test_color, background, minimum_apca_contrast); + let contrast = apca_contrast(adjusted, background).abs(); + + if contrast >= minimum_apca_contrast { + return adjusted; + } + } + + // If we get here, even grayscale didn't work, so return the grayscale attempt + Hsla { + h: foreground.h, + s: 0.0, + l: foreground.l, + a: foreground.a, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla { + Hsla { h, s, l, a } + } + + fn hsla_from_hex(hex: u32) -> Hsla { + let r = ((hex >> 16) & 0xFF) as f32 / 255.0; + let g = ((hex >> 8) & 0xFF) as f32 / 255.0; + let b = (hex & 0xFF) as f32 / 255.0; + + let max = r.max(g).max(b); + let min = r.min(g).min(b); + let l = (max + min) / 2.0; + + if max == min { + // Achromatic + Hsla { + h: 0.0, + s: 0.0, + l, + a: 1.0, + } + } else { + let d = max - min; + let s = if l > 0.5 { + d / (2.0 - max - min) + } else { + d / (max + min) + }; + + let h = if max == r { + (g - b) / d + if g < b { 6.0 } else { 0.0 } + } else if max == g { + (b - r) / d + 2.0 + } else { + (r - g) / d + 4.0 + } / 6.0; + + Hsla { h, s, l, a: 1.0 } + } + } + + #[test] + fn test_apca_contrast() { + // Test black text on white background (should be positive) + let black = hsla(0.0, 0.0, 0.0, 1.0); + let white = hsla(0.0, 0.0, 1.0, 1.0); + let contrast = apca_contrast(black, white); + assert!( + contrast > 100.0, + "Black on white should have high positive contrast, got {}", + contrast + ); + + // Test white text on black background (should be negative) + let contrast_reversed = apca_contrast(white, black); + assert!( + contrast_reversed < -100.0, + "White on black should have high negative contrast, got {}", + contrast_reversed + ); + + // Same color should have zero contrast + let gray = hsla(0.0, 0.0, 0.5, 1.0); + let contrast_same = apca_contrast(gray, gray); + assert!( + contrast_same.abs() < 1.0, + "Same color should have near-zero contrast, got {}", + contrast_same + ); + + // APCA is NOT commutative - polarity matters + assert!( + (contrast + contrast_reversed).abs() > 1.0, + "APCA should not be commutative" + ); + } + + #[test] + fn test_srgb_to_y() { + let constants = APCAConstants::default(); + + // Test known Y values + let black = hsla(0.0, 0.0, 0.0, 1.0); + let y_black = srgb_to_y(black, &constants); + assert!( + y_black.abs() < 0.001, + "Black should have Y near 0, got {}", + y_black + ); + + let white = hsla(0.0, 0.0, 1.0, 1.0); + let y_white = srgb_to_y(white, &constants); + assert!( + (y_white - 1.0).abs() < 0.001, + "White should have Y near 1, got {}", + y_white + ); + } + + #[test] + fn test_ensure_minimum_contrast() { + let white_bg = hsla(0.0, 0.0, 1.0, 1.0); + let light_gray = hsla(0.0, 0.0, 0.9, 1.0); + + // Light gray on white has poor contrast + let initial_contrast = apca_contrast(light_gray, white_bg).abs(); + assert!( + initial_contrast < 15.0, + "Initial contrast should be low, got {}", + initial_contrast + ); + + // Should be adjusted to black for better contrast (using APCA Lc 45 as minimum) + let adjusted = ensure_minimum_contrast(light_gray, white_bg, 45.0); + assert_eq!(adjusted.l, 0.0); // Should be black + assert_eq!(adjusted.a, light_gray.a); // Alpha preserved + + // Test with dark background + let black_bg = hsla(0.0, 0.0, 0.0, 1.0); + let dark_gray = hsla(0.0, 0.0, 0.1, 1.0); + + // Dark gray on black has poor contrast + let initial_contrast = apca_contrast(dark_gray, black_bg).abs(); + assert!( + initial_contrast < 15.0, + "Initial contrast should be low, got {}", + initial_contrast + ); + + // Should be adjusted to white for better contrast + let adjusted = ensure_minimum_contrast(dark_gray, black_bg, 45.0); + assert_eq!(adjusted.l, 1.0); // Should be white + + // Test when contrast is already sufficient + let black = hsla(0.0, 0.0, 0.0, 1.0); + let adjusted = ensure_minimum_contrast(black, white_bg, 45.0); + assert_eq!(adjusted, black); // Should remain unchanged + } + + #[test] + fn test_one_light_theme_exact_colors() { + // Test with exact colors from One Light theme + // terminal.background and terminal.ansi.white are both #fafafaff + let fafafa = hsla_from_hex(0xfafafa); + + // They should be identical + let bg = fafafa; + let fg = fafafa; + + // Contrast should be 0 (no contrast) + let contrast = apca_contrast(fg, bg); + assert!( + contrast.abs() < 1.0, + "Same color should have near-zero APCA contrast, got {}", + contrast + ); + + // With minimum APCA contrast of 15 (very low, but detectable), it should adjust + let adjusted = ensure_minimum_contrast(fg, bg, 15.0); + // The new algorithm preserves colors, so we just need to check contrast + let new_contrast = apca_contrast(adjusted, bg).abs(); + assert!( + new_contrast >= 15.0, + "Adjusted contrast {} should be >= 15.0", + new_contrast + ); + + // The adjusted color should have sufficient contrast + let new_contrast = apca_contrast(adjusted, bg).abs(); + assert!( + new_contrast >= 15.0, + "Adjusted APCA contrast {} should be >= 15.0", + new_contrast + ); + } +} diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 3439a5b7f882eb089b6e104d1b150cd40b5fdacf..f34dc8f00943ccb1d6c41e95d9c990b1968107aa 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1,3 +1,4 @@ +use crate::color_contrast; use editor::{CursorLayout, HighlightedRange, HighlightedRangeLine}; use gpui::{ AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, DispatchPhase, Element, @@ -204,9 +205,9 @@ impl TerminalElement { grid: impl Iterator, start_line_offset: i32, text_style: &TextStyle, - // terminal_theme: &TerminalStyle, text_system: &WindowTextSystem, hyperlink: Option<(HighlightStyle, &RangeInclusive)>, + minimum_contrast: f32, window: &Window, cx: &App, ) -> (Vec, Vec) { @@ -285,8 +286,15 @@ impl TerminalElement { { if !is_blank(&cell) { let cell_text = cell.c.to_string(); - let cell_style = - TerminalElement::cell_style(&cell, fg, theme, text_style, hyperlink); + let cell_style = TerminalElement::cell_style( + &cell, + fg, + bg, + theme, + text_style, + hyperlink, + minimum_contrast, + ); let layout_cell = text_system.shape_line( cell_text.into(), @@ -341,13 +349,17 @@ impl TerminalElement { fn cell_style( indexed: &IndexedCell, fg: terminal::alacritty_terminal::vte::ansi::Color, - // bg: terminal::alacritty_terminal::ansi::Color, + bg: terminal::alacritty_terminal::vte::ansi::Color, colors: &Theme, text_style: &TextStyle, hyperlink: Option<(HighlightStyle, &RangeInclusive)>, + minimum_contrast: f32, ) -> TextRun { let flags = indexed.cell.flags; let mut fg = convert_color(&fg, colors); + let bg = convert_color(&bg, colors); + + fg = color_contrast::ensure_minimum_contrast(fg, bg, minimum_contrast); // Ghostty uses (175/255) as the multiplier (~0.69), Alacritty uses 0.66, Kitty // uses 0.75. We're using 0.7 because it's pretty well in the middle of that. @@ -680,6 +692,7 @@ impl Element for TerminalElement { let buffer_font_size = settings.buffer_font_size(cx); let terminal_settings = TerminalSettings::get_global(cx); + let minimum_contrast = terminal_settings.minimum_contrast; let font_family = terminal_settings.font_family.as_ref().map_or_else( || settings.buffer_font.family.clone(), @@ -853,6 +866,7 @@ impl Element for TerminalElement { last_hovered_word .as_ref() .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)), + minimum_contrast, window, cx, ), @@ -874,6 +888,7 @@ impl Element for TerminalElement { last_hovered_word.as_ref().map(|last_hovered_word| { (link_style, &last_hovered_word.word_match) }), + minimum_contrast, window, cx, ) @@ -1390,3 +1405,122 @@ pub fn convert_color(fg: &terminal::alacritty_terminal::vte::ansi::Color, theme: } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_contrast_adjustment_logic() { + // Test the core contrast adjustment logic without needing full app context + + // Test case 1: Light colors (poor contrast) + let white_fg = gpui::Hsla { + h: 0.0, + s: 0.0, + l: 1.0, + a: 1.0, + }; + let light_gray_bg = gpui::Hsla { + h: 0.0, + s: 0.0, + l: 0.95, + a: 1.0, + }; + + // Should have poor contrast + let actual_contrast = color_contrast::apca_contrast(white_fg, light_gray_bg).abs(); + assert!( + actual_contrast < 30.0, + "White on light gray should have poor APCA contrast: {}", + actual_contrast + ); + + // After adjustment with minimum APCA contrast of 45, should be darker + let adjusted = color_contrast::ensure_minimum_contrast(white_fg, light_gray_bg, 45.0); + assert!( + adjusted.l < white_fg.l, + "Adjusted color should be darker than original" + ); + let adjusted_contrast = color_contrast::apca_contrast(adjusted, light_gray_bg).abs(); + assert!(adjusted_contrast >= 45.0, "Should meet minimum contrast"); + + // Test case 2: Dark colors (poor contrast) + let black_fg = gpui::Hsla { + h: 0.0, + s: 0.0, + l: 0.0, + a: 1.0, + }; + let dark_gray_bg = gpui::Hsla { + h: 0.0, + s: 0.0, + l: 0.05, + a: 1.0, + }; + + // Should have poor contrast + let actual_contrast = color_contrast::apca_contrast(black_fg, dark_gray_bg).abs(); + assert!( + actual_contrast < 30.0, + "Black on dark gray should have poor APCA contrast: {}", + actual_contrast + ); + + // After adjustment with minimum APCA contrast of 45, should be lighter + let adjusted = color_contrast::ensure_minimum_contrast(black_fg, dark_gray_bg, 45.0); + assert!( + adjusted.l > black_fg.l, + "Adjusted color should be lighter than original" + ); + let adjusted_contrast = color_contrast::apca_contrast(adjusted, dark_gray_bg).abs(); + assert!(adjusted_contrast >= 45.0, "Should meet minimum contrast"); + + // Test case 3: Already good contrast + let good_contrast = color_contrast::ensure_minimum_contrast(black_fg, white_fg, 45.0); + assert_eq!( + good_contrast, black_fg, + "Good contrast should not be adjusted" + ); + } + + #[test] + fn test_white_on_white_contrast_issue() { + // This test reproduces the exact issue from the bug report + // where white ANSI text on white background should be adjusted + + // Simulate One Light theme colors + let white_fg = gpui::Hsla { + h: 0.0, + s: 0.0, + l: 0.98, // #fafafaff is approximately 98% lightness + a: 1.0, + }; + let white_bg = gpui::Hsla { + h: 0.0, + s: 0.0, + l: 0.98, // Same as foreground - this is the problem! + a: 1.0, + }; + + // With minimum contrast of 0.0, no adjustment should happen + let no_adjust = color_contrast::ensure_minimum_contrast(white_fg, white_bg, 0.0); + assert_eq!(no_adjust, white_fg, "No adjustment with min_contrast 0.0"); + + // With minimum APCA contrast of 15, it should adjust to a darker color + let adjusted = color_contrast::ensure_minimum_contrast(white_fg, white_bg, 15.0); + assert!( + adjusted.l < white_fg.l, + "White on white should become darker, got l={}", + adjusted.l + ); + + // Verify the contrast is now acceptable + let new_contrast = color_contrast::apca_contrast(adjusted, white_bg).abs(); + assert!( + new_contrast >= 15.0, + "Adjusted APCA contrast {} should be >= 15.0", + new_contrast + ); + } +} diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index be167d820db70defb8cf4e93bd6d227092d72000..76ec9dcb2591a3d0f5483507f25a7c036464e93d 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1,3 +1,4 @@ +mod color_contrast; mod persistence; pub mod terminal_element; pub mod terminal_panel; From c0dc758f24fd6c1a5fb13698d4227941b3e480f3 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 8 Jul 2025 01:05:37 +0200 Subject: [PATCH 076/239] debugger: Rely on LocalDapCommand in more places (#34035) - **debugger: Move cacheable property onto LocalDapCommand** - **debugger/session: Relax method bounds to use LocalDapCommand** Release Notes: - N/A --- crates/project/src/debugger/dap_command.rs | 20 ++++++++-------- crates/project/src/debugger/session.rs | 28 +++++++++++----------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index 735444b3f31f64b016972f9c6545d58ea94cb63c..411bacd3ba1557b7392eb0e981df1a4297772b31 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -17,6 +17,8 @@ use util::ResultExt; pub trait LocalDapCommand: 'static + Send + Sync + std::fmt::Debug { type Response: 'static + Send + std::fmt::Debug; type DapRequest: 'static + Send + dap::requests::Request; + /// Is this request idempotent? Is it safe to cache the response for as long as the execution environment is unchanged? + const CACHEABLE: bool = false; fn is_supported(_capabilities: &Capabilities) -> bool { true @@ -33,7 +35,6 @@ pub trait LocalDapCommand: 'static + Send + Sync + std::fmt::Debug { pub trait DapCommand: LocalDapCommand { type ProtoRequest: 'static + Send; type ProtoResponse: 'static + Send; - const CACHEABLE: bool = false; #[allow(dead_code)] fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId; @@ -823,6 +824,7 @@ pub struct VariablesCommand { impl LocalDapCommand for VariablesCommand { type Response = Vec; type DapRequest = dap::requests::Variables; + const CACHEABLE: bool = true; fn to_dap(&self) -> ::Arguments { dap::VariablesArguments { @@ -845,7 +847,6 @@ impl LocalDapCommand for VariablesCommand { impl DapCommand for VariablesCommand { type ProtoRequest = proto::VariablesRequest; type ProtoResponse = proto::DapVariables; - const CACHEABLE: bool = true; fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { SessionId::from_proto(request.client_id) @@ -1041,6 +1042,7 @@ pub(crate) struct ModulesCommand; impl LocalDapCommand for ModulesCommand { type Response = Vec; type DapRequest = dap::requests::Modules; + const CACHEABLE: bool = true; fn is_supported(capabilities: &Capabilities) -> bool { capabilities.supports_modules_request.unwrap_or_default() @@ -1064,7 +1066,6 @@ impl LocalDapCommand for ModulesCommand { impl DapCommand for ModulesCommand { type ProtoRequest = proto::DapModulesRequest; type ProtoResponse = proto::DapModulesResponse; - const CACHEABLE: bool = true; fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { SessionId::from_proto(request.client_id) @@ -1113,6 +1114,7 @@ pub(crate) struct LoadedSourcesCommand; impl LocalDapCommand for LoadedSourcesCommand { type Response = Vec; type DapRequest = dap::requests::LoadedSources; + const CACHEABLE: bool = true; fn is_supported(capabilities: &Capabilities) -> bool { capabilities @@ -1134,7 +1136,6 @@ impl LocalDapCommand for LoadedSourcesCommand { impl DapCommand for LoadedSourcesCommand { type ProtoRequest = proto::DapLoadedSourcesRequest; type ProtoResponse = proto::DapLoadedSourcesResponse; - const CACHEABLE: bool = true; fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { SessionId::from_proto(request.client_id) @@ -1187,6 +1188,7 @@ pub(crate) struct StackTraceCommand { impl LocalDapCommand for StackTraceCommand { type Response = Vec; type DapRequest = dap::requests::StackTrace; + const CACHEABLE: bool = true; fn to_dap(&self) -> ::Arguments { dap::StackTraceArguments { @@ -1208,7 +1210,6 @@ impl LocalDapCommand for StackTraceCommand { impl DapCommand for StackTraceCommand { type ProtoRequest = proto::DapStackTraceRequest; type ProtoResponse = proto::DapStackTraceResponse; - const CACHEABLE: bool = true; fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { proto::DapStackTraceRequest { @@ -1258,6 +1259,7 @@ pub(crate) struct ScopesCommand { impl LocalDapCommand for ScopesCommand { type Response = Vec; type DapRequest = dap::requests::Scopes; + const CACHEABLE: bool = true; fn to_dap(&self) -> ::Arguments { dap::ScopesArguments { @@ -1276,7 +1278,6 @@ impl LocalDapCommand for ScopesCommand { impl DapCommand for ScopesCommand { type ProtoRequest = proto::DapScopesRequest; type ProtoResponse = proto::DapScopesResponse; - const CACHEABLE: bool = true; fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { proto::DapScopesRequest { @@ -1313,6 +1314,7 @@ impl DapCommand for ScopesCommand { impl LocalDapCommand for super::session::CompletionsQuery { type Response = dap::CompletionsResponse; type DapRequest = dap::requests::Completions; + const CACHEABLE: bool = true; fn to_dap(&self) -> ::Arguments { dap::CompletionsArguments { @@ -1340,7 +1342,6 @@ impl LocalDapCommand for super::session::CompletionsQuery { impl DapCommand for super::session::CompletionsQuery { type ProtoRequest = proto::DapCompletionRequest; type ProtoResponse = proto::DapCompletionResponse; - const CACHEABLE: bool = true; fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { proto::DapCompletionRequest { @@ -1477,6 +1478,7 @@ pub(crate) struct ThreadsCommand; impl LocalDapCommand for ThreadsCommand { type Response = Vec; type DapRequest = dap::requests::Threads; + const CACHEABLE: bool = true; fn to_dap(&self) -> ::Arguments { dap::ThreadsArgument {} @@ -1493,7 +1495,6 @@ impl LocalDapCommand for ThreadsCommand { impl DapCommand for ThreadsCommand { type ProtoRequest = proto::DapThreadsRequest; type ProtoResponse = proto::DapThreadsResponse; - const CACHEABLE: bool = true; fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { proto::DapThreadsRequest { @@ -1712,6 +1713,7 @@ pub(super) struct LocationsCommand { impl LocalDapCommand for LocationsCommand { type Response = dap::LocationsResponse; type DapRequest = dap::requests::Locations; + const CACHEABLE: bool = true; fn to_dap(&self) -> ::Arguments { dap::LocationsArguments { @@ -1731,8 +1733,6 @@ impl DapCommand for LocationsCommand { type ProtoRequest = proto::DapLocationsRequest; type ProtoResponse = proto::DapLocationsResponse; - const CACHEABLE: bool = true; - fn client_id_from_proto(message: &Self::ProtoRequest) -> SessionId { SessionId::from_proto(message.session_id) } diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 3190254af8545f1a48088460c4ef8d3bbd38bb41..59c35da4cac4328dc109b8463ef02868b4885d63 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -4,12 +4,12 @@ use super::breakpoint_store::{ BreakpointStore, BreakpointStoreEvent, BreakpointUpdatedReason, SourceBreakpoint, }; use super::dap_command::{ - self, Attach, ConfigurationDone, ContinueCommand, DapCommand, DisconnectCommand, - EvaluateCommand, Initialize, Launch, LoadedSourcesCommand, LocalDapCommand, LocationsCommand, - ModulesCommand, NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand, - ScopesCommand, SetExceptionBreakpoints, SetVariableValueCommand, StackTraceCommand, - StepBackCommand, StepCommand, StepInCommand, StepOutCommand, TerminateCommand, - TerminateThreadsCommand, ThreadsCommand, VariablesCommand, + self, Attach, ConfigurationDone, ContinueCommand, DisconnectCommand, EvaluateCommand, + Initialize, Launch, LoadedSourcesCommand, LocalDapCommand, LocationsCommand, ModulesCommand, + NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand, ScopesCommand, + SetExceptionBreakpoints, SetVariableValueCommand, StackTraceCommand, StepBackCommand, + StepCommand, StepInCommand, StepOutCommand, TerminateCommand, TerminateThreadsCommand, + ThreadsCommand, VariablesCommand, }; use super::dap_store::DapStore; use anyhow::{Context as _, Result, anyhow}; @@ -555,7 +555,7 @@ impl RunningMode { } impl Mode { - pub(super) fn request_dap(&self, request: R) -> Task> + pub(super) fn request_dap(&self, request: R) -> Task> where ::Response: 'static, ::Arguments: 'static + Send, @@ -689,7 +689,7 @@ trait CacheableCommand: Any + Send + Sync { impl CacheableCommand for T where - T: DapCommand + PartialEq + Eq + Hash, + T: LocalDapCommand + PartialEq + Eq + Hash, { fn dyn_eq(&self, rhs: &dyn CacheableCommand) -> bool { (rhs as &dyn Any) @@ -708,7 +708,7 @@ where pub(crate) struct RequestSlot(Arc); -impl From for RequestSlot { +impl From for RequestSlot { fn from(request: T) -> Self { Self(Arc::new(request)) } @@ -1534,7 +1534,7 @@ impl Session { } /// Ensure that there's a request in flight for the given command, and if not, send it. Use this to run requests that are idempotent. - fn fetch( + fn fetch( &mut self, request: T, process_result: impl FnOnce(&mut Self, Result, &mut Context) + 'static, @@ -1585,7 +1585,7 @@ impl Session { } } - fn request_inner( + fn request_inner( capabilities: &Capabilities, mode: &Mode, request: T, @@ -1621,7 +1621,7 @@ impl Session { }) } - fn request( + fn request( &self, request: T, process_result: impl FnOnce( @@ -1635,7 +1635,7 @@ impl Session { Self::request_inner(&self.capabilities, &self.mode, request, process_result, cx) } - fn invalidate_command_type(&mut self) { + fn invalidate_command_type(&mut self) { self.requests.remove(&std::any::TypeId::of::()); } @@ -1816,7 +1816,7 @@ impl Session { Some(()) } - fn on_step_response( + fn on_step_response( thread_id: ThreadId, ) -> impl FnOnce(&mut Self, Result, &mut Context) -> Option + 'static { From 4693f1675903d5c4b820aace164f61bbb3c9090f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 8 Jul 2025 01:36:32 +0200 Subject: [PATCH 077/239] debugger: Remove PHP debug adapter (#34020) This commit removes the PHP debug adapter in favor of a new version (0.3.0) of PHP extension. The name of a debug adapter has been changed from "PHP" to "Xdebug", which makes this a breaking change in user-configured scenarios Release Notes: - debugger: PHP debug adapter is no longer shipped in core Zed editor; it is now available in PHP extension (starting with version 0.3.0). The adapter has been renamed from `PHP` to `Xdebug`, which might break your user-defined debug scenarios. --- assets/settings/initial_debug_tasks.json | 7 - crates/dap_adapters/src/dap_adapters.rs | 3 - crates/dap_adapters/src/php.rs | 368 ------------------ .../src/tests/new_process_modal.rs | 1 - crates/task/src/vscode_debug_format.rs | 2 +- 5 files changed, 1 insertion(+), 380 deletions(-) delete mode 100644 crates/dap_adapters/src/php.rs diff --git a/assets/settings/initial_debug_tasks.json b/assets/settings/initial_debug_tasks.json index 75e42b2d1b43b6fa87eeb28ba74529b2b265e0ac..78fc1fc5f02a03bc83c93a4cf5cc7c517fd301c7 100644 --- a/assets/settings/initial_debug_tasks.json +++ b/assets/settings/initial_debug_tasks.json @@ -3,13 +3,6 @@ // For more documentation on how to configure debug tasks, // see: https://zed.dev/docs/debugger [ - { - "label": "Debug active PHP file", - "adapter": "PHP", - "program": "$ZED_FILE", - "request": "launch", - "cwd": "$ZED_WORKTREE_ROOT" - }, { "label": "Debug active Python file", "adapter": "Debugpy", diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index c254302e7144b53500fd2a3b84be06e8ec30c2a0..a147861f8dc965c7924a70d884004d594d59a949 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -2,7 +2,6 @@ mod codelldb; mod gdb; mod go; mod javascript; -mod php; mod python; use std::sync::Arc; @@ -22,7 +21,6 @@ use gdb::GdbDebugAdapter; use go::GoDebugAdapter; use gpui::{App, BorrowAppContext}; use javascript::JsDebugAdapter; -use php::PhpDebugAdapter; use python::PythonDebugAdapter; use serde_json::json; use task::{DebugScenario, ZedDebugConfig}; @@ -31,7 +29,6 @@ pub fn init(cx: &mut App) { cx.update_default_global(|registry: &mut DapRegistry, _cx| { registry.add_adapter(Arc::from(CodeLldbDebugAdapter::default())); registry.add_adapter(Arc::from(PythonDebugAdapter::default())); - registry.add_adapter(Arc::from(PhpDebugAdapter::default())); registry.add_adapter(Arc::from(JsDebugAdapter::default())); registry.add_adapter(Arc::from(GoDebugAdapter::default())); registry.add_adapter(Arc::from(GdbDebugAdapter)); diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs deleted file mode 100644 index 7d7dee00c900dcfa44fc4bf99e164d0f2454c817..0000000000000000000000000000000000000000 --- a/crates/dap_adapters/src/php.rs +++ /dev/null @@ -1,368 +0,0 @@ -use adapters::latest_github_release; -use anyhow::Context as _; -use anyhow::bail; -use dap::StartDebuggingRequestArguments; -use dap::StartDebuggingRequestArgumentsRequest; -use dap::adapters::{DebugTaskDefinition, TcpArguments}; -use gpui::{AsyncApp, SharedString}; -use language::LanguageName; -use std::{collections::HashMap, path::PathBuf, sync::OnceLock}; -use util::ResultExt; - -use crate::*; - -#[derive(Default)] -pub(crate) struct PhpDebugAdapter { - checked: OnceLock<()>, -} - -impl PhpDebugAdapter { - const ADAPTER_NAME: &'static str = "PHP"; - const ADAPTER_PACKAGE_NAME: &'static str = "vscode-php-debug"; - const ADAPTER_PATH: &'static str = "extension/out/phpDebug.js"; - - async fn fetch_latest_adapter_version( - &self, - delegate: &Arc, - ) -> Result { - let release = latest_github_release( - &format!("{}/{}", "xdebug", Self::ADAPTER_PACKAGE_NAME), - true, - false, - delegate.http_client(), - ) - .await?; - - let asset_name = format!("php-debug-{}.vsix", release.tag_name.replace("v", "")); - - Ok(AdapterVersion { - tag_name: release.tag_name, - url: release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .with_context(|| format!("no asset found matching {asset_name:?}"))? - .browser_download_url - .clone(), - }) - } - - async fn get_installed_binary( - &self, - delegate: &Arc, - task_definition: &DebugTaskDefinition, - user_installed_path: Option, - user_args: Option>, - _: &mut AsyncApp, - ) -> Result { - let adapter_path = if let Some(user_installed_path) = user_installed_path { - user_installed_path - } else { - let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref()); - - let file_name_prefix = format!("{}_", self.name()); - - util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { - file_name.starts_with(&file_name_prefix) - }) - .await - .context("Couldn't find PHP dap directory")? - }; - - let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default(); - let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?; - - let mut configuration = task_definition.config.clone(); - if let Some(obj) = configuration.as_object_mut() { - obj.entry("cwd") - .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into()); - } - - let arguments = if let Some(mut args) = user_args { - args.insert( - 0, - adapter_path - .join(Self::ADAPTER_PATH) - .to_string_lossy() - .to_string(), - ); - args - } else { - vec![ - adapter_path - .join(Self::ADAPTER_PATH) - .to_string_lossy() - .to_string(), - format!("--server={}", port), - ] - }; - - Ok(DebugAdapterBinary { - command: Some( - delegate - .node_runtime() - .binary_path() - .await? - .to_string_lossy() - .into_owned(), - ), - arguments, - connection: Some(TcpArguments { - port, - host, - timeout, - }), - cwd: Some(delegate.worktree_root_path().to_path_buf()), - envs: HashMap::default(), - request_args: StartDebuggingRequestArguments { - configuration, - request: ::request_kind(self, &task_definition.config) - .await?, - }, - }) - } -} - -#[async_trait(?Send)] -impl DebugAdapter for PhpDebugAdapter { - fn dap_schema(&self) -> serde_json::Value { - json!({ - "properties": { - "request": { - "type": "string", - "enum": ["launch"], - "description": "The request type for the PHP debug adapter, always \"launch\"", - "default": "launch" - }, - "hostname": { - "type": "string", - "description": "The address to bind to when listening for Xdebug (default: all IPv6 connections if available, else all IPv4 connections) or Unix Domain socket (prefix with unix://) or Windows Pipe (\\\\?\\pipe\\name) - cannot be combined with port" - }, - "port": { - "type": "integer", - "description": "The port on which to listen for Xdebug (default: 9003). If port is set to 0 a random port is chosen by the system and a placeholder ${port} is replaced with the chosen port in env and runtimeArgs.", - "default": 9003 - }, - "program": { - "type": "string", - "description": "The PHP script to debug (typically a path to a file)", - "default": "${file}" - }, - "cwd": { - "type": "string", - "description": "Working directory for the debugged program" - }, - "args": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Command line arguments to pass to the program" - }, - "env": { - "type": "object", - "description": "Environment variables to pass to the program", - "additionalProperties": { - "type": "string" - } - }, - "stopOnEntry": { - "type": "boolean", - "description": "Whether to break at the beginning of the script", - "default": false - }, - "pathMappings": { - "type": "object", - "description": "A mapping of server paths to local paths.", - }, - "log": { - "type": "boolean", - "description": "Whether to log all communication between editor and the adapter to the debug console", - "default": false - }, - "ignore": { - "type": "array", - "description": "An array of glob patterns that errors should be ignored from (for example **/vendor/**/*.php)", - "items": { - "type": "string" - } - }, - "ignoreExceptions": { - "type": "array", - "description": "An array of exception class names that should be ignored (for example BaseException, \\NS1\\Exception, \\*\\Exception or \\**\\Exception*)", - "items": { - "type": "string" - } - }, - "skipFiles": { - "type": "array", - "description": "An array of glob patterns to skip when debugging. Star patterns and negations are allowed.", - "items": { - "type": "string" - } - }, - "skipEntryPaths": { - "type": "array", - "description": "An array of glob patterns to immediately detach from and ignore for debugging if the entry script matches", - "items": { - "type": "string" - } - }, - "maxConnections": { - "type": "integer", - "description": "Accept only this number of parallel debugging sessions. Additional connections will be dropped.", - "default": 1 - }, - "proxy": { - "type": "object", - "description": "DBGp Proxy settings", - "properties": { - "enable": { - "type": "boolean", - "description": "To enable proxy registration", - "default": false - }, - "host": { - "type": "string", - "description": "The address of the proxy. Supports host name, IP address, or Unix domain socket.", - "default": "127.0.0.1" - }, - "port": { - "type": "integer", - "description": "The port where the adapter will register with the proxy", - "default": 9001 - }, - "key": { - "type": "string", - "description": "A unique key that allows the proxy to match requests to your editor", - "default": "vsc" - }, - "timeout": { - "type": "integer", - "description": "The number of milliseconds to wait before giving up on the connection to proxy", - "default": 3000 - }, - "allowMultipleSessions": { - "type": "boolean", - "description": "If the proxy should forward multiple sessions/connections at the same time or not", - "default": true - } - } - }, - "xdebugSettings": { - "type": "object", - "description": "Allows you to override Xdebug's remote debugging settings to fine tune Xdebug to your needs", - "properties": { - "max_children": { - "type": "integer", - "description": "Max number of array or object children to initially retrieve" - }, - "max_data": { - "type": "integer", - "description": "Max amount of variable data to initially retrieve" - }, - "max_depth": { - "type": "integer", - "description": "Maximum depth that the debugger engine may return when sending arrays, hashes or object structures to the IDE" - }, - "show_hidden": { - "type": "integer", - "description": "Whether to show detailed internal information on properties (e.g. private members of classes). Zero means hidden members are not shown.", - "enum": [0, 1] - }, - "breakpoint_include_return_value": { - "type": "boolean", - "description": "Determines whether to enable an additional \"return from function\" debugging step, allowing inspection of the return value when a function call returns" - } - } - }, - "xdebugCloudToken": { - "type": "string", - "description": "Instead of listening locally, open a connection and register with Xdebug Cloud and accept debugging sessions on that connection" - }, - "stream": { - "type": "object", - "description": "Allows to influence DBGp streams. Xdebug only supports stdout", - "properties": { - "stdout": { - "type": "integer", - "description": "Redirect stdout stream: 0 (disable), 1 (copy), 2 (redirect)", - "enum": [0, 1, 2], - "default": 0 - } - } - } - }, - "required": ["request", "program"] - }) - } - - fn name(&self) -> DebugAdapterName { - DebugAdapterName(Self::ADAPTER_NAME.into()) - } - - fn adapter_language_name(&self) -> Option { - Some(SharedString::new_static("PHP").into()) - } - - async fn request_kind( - &self, - _: &serde_json::Value, - ) -> Result { - Ok(StartDebuggingRequestArgumentsRequest::Launch) - } - - async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result { - let obj = match &zed_scenario.request { - dap::DebugRequest::Attach(_) => { - bail!("Php adapter doesn't support attaching") - } - dap::DebugRequest::Launch(launch_config) => json!({ - "program": launch_config.program, - "cwd": launch_config.cwd, - "args": launch_config.args, - "env": launch_config.env_json(), - "stopOnEntry": zed_scenario.stop_on_entry.unwrap_or_default(), - }), - }; - - Ok(DebugScenario { - adapter: zed_scenario.adapter, - label: zed_scenario.label, - build: None, - config: obj, - tcp_connection: None, - }) - } - - async fn get_binary( - &self, - delegate: &Arc, - task_definition: &DebugTaskDefinition, - user_installed_path: Option, - user_args: Option>, - cx: &mut AsyncApp, - ) -> Result { - if self.checked.set(()).is_ok() { - delegate.output_to_console(format!("Checking latest version of {}...", self.name())); - if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() { - adapters::download_adapter_from_github( - self.name(), - version, - adapters::DownloadedFileType::Vsix, - delegate.as_ref(), - ) - .await?; - } - } - - self.get_installed_binary( - delegate, - &task_definition, - user_installed_path, - user_args, - cx, - ) - .await - } -} diff --git a/crates/debugger_ui/src/tests/new_process_modal.rs b/crates/debugger_ui/src/tests/new_process_modal.rs index a4616eaa3b640cd256b997b543852f772a9c3570..0805060bf4413a16d4b7242d133e635bdf4d7cd4 100644 --- a/crates/debugger_ui/src/tests/new_process_modal.rs +++ b/crates/debugger_ui/src/tests/new_process_modal.rs @@ -290,7 +290,6 @@ async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppConte let mut expected_adapters = vec![ "CodeLLDB", "Debugpy", - "PHP", "JavaScript", "Delve", "GDB", diff --git a/crates/task/src/vscode_debug_format.rs b/crates/task/src/vscode_debug_format.rs index a74401a2c66ea8080cbf5abbe29c211064e256d1..29907166860d7404b33a7bb1a676d037534f09da 100644 --- a/crates/task/src/vscode_debug_format.rs +++ b/crates/task/src/vscode_debug_format.rs @@ -90,7 +90,7 @@ fn task_type_to_adapter_name(task_type: &str) -> String { "pwa-node" | "node" | "node-terminal" | "chrome" | "pwa-chrome" | "edge" | "pwa-edge" | "msedge" | "pwa-msedge" => "JavaScript", "go" => "Delve", - "php" => "PHP", + "php" => "Xdebug", "cppdbg" | "lldb" => "CodeLLDB", "debugpy" => "Debugpy", "rdbg" => "rdbg", From 211d6205b904247cbf10e58b93f924767a3cec58 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 8 Jul 2025 01:18:52 -0300 Subject: [PATCH 078/239] project panel: Add a shadow in the last sticky item (#34042) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to https://github.com/zed-industries/zed/pull/33994. This PR adds a subtle shadow—built from an absolute-positioned div, due to layering of items—to the last sticky item in the project panel when that setting is turned on. This helps understand the block of items that is currently sticky. Would love to add indent guides to the items that are sticky as a next step. Release Notes: - project panel: When `sticky_scroll` is true, the last item will now have a subtle shadow to help visualizing the block of items that are currently sticky. --- crates/project_panel/src/project_panel.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index ca791869d9db9a70090583f21b06b8099e9d74f1..a5861250a41fba178c6959726548d9283d900cad 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -23,7 +23,8 @@ use gpui::{ ListSizingBehavior, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, ScrollStrategy, Stateful, Styled, Subscription, Task, UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, - div, point, px, size, transparent_white, uniform_list, + div, hsla, linear_color_stop, linear_gradient, point, px, size, transparent_white, + uniform_list, }; use indexmap::IndexMap; use language::DiagnosticSeverity; @@ -185,6 +186,7 @@ struct EntryDetails { #[derive(Debug, PartialEq, Eq, Clone)] struct StickyDetails { sticky_index: usize, + is_last: bool, } /// Permanently deletes the selected file or directory. @@ -3928,8 +3930,24 @@ impl ProjectPanel { } }; + let last_sticky_item = details.sticky.as_ref().map_or(false, |item| item.is_last); + let shadow_color_top = hsla(0.0, 0.0, 0.0, 0.15); + let shadow_color_bottom = hsla(0.0, 0.0, 0.0, 0.); + let sticky_shadow = div() + .absolute() + .left_0() + .bottom_neg_1p5() + .h_1p5() + .w_full() + .bg(linear_gradient( + 0., + linear_color_stop(shadow_color_top, 1.), + linear_color_stop(shadow_color_bottom, 0.), + )); + div() .id(entry_id.to_proto() as usize) + .relative() .group(GROUP_NAME) .cursor_pointer() .rounded_none() @@ -3938,6 +3956,7 @@ impl ProjectPanel { .border_r_2() .border_color(border_color) .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color)) + .when(is_sticky && last_sticky_item, |this| this.child(sticky_shadow)) .when(!is_sticky, |this| { this .when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over)) @@ -4931,6 +4950,7 @@ impl ProjectPanel { .unwrap_or_default(); let sticky_details = Some(StickyDetails { sticky_index: index, + is_last: index == sticky_parents.len() - 1, }); let details = self.details_for_entry( entry, From 02d0e725a8883adfea25f83e88f59cd250937f9c Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 8 Jul 2025 01:19:00 -0300 Subject: [PATCH 079/239] agent: Allow clicking on the whole edit file row to trigger review (#34041) Just a small quality-of-life type of PR that makes clicking anywhere until the "Review" button trigger the action that that button triggers (i.e., opens the review multibuffer). Release Notes: - agent: Added the ability to click the whole file row in the edits bar to trigger the review multibuffer. --- crates/agent_ui/src/message_editor.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 70d2b6e06619e36df37aa6280c60d38ccfb4fbaa..38065b828acb2e3a86d5b04161a1b8d0e89ccf40 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -1160,7 +1160,7 @@ impl MessageEditor { }) .child( h_flex() - .id("file-name") + .id(("file-name", index)) .pr_8() .gap_1p5() .max_w_full() @@ -1171,9 +1171,16 @@ impl MessageEditor { .gap_0p5() .children(file_name) .children(file_path), - ), // TODO: Implement line diff - // .child(Label::new("+").color(Color::Created)) - // .child(Label::new("-").color(Color::Deleted)), + ) + .on_click({ + let buffer = buffer.clone(); + cx.listener(move |this, _, window, cx| { + this.handle_file_click(buffer.clone(), window, cx); + }) + }), // TODO: Implement line diff + // .child(Label::new("+").color(Color::Created)) + // .child(Label::new("-").color(Color::Deleted)), + // ) .child( h_flex() From f1db3b4e1d639b3aacbe472ddeb4471d844d4e04 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 8 Jul 2025 01:19:09 -0300 Subject: [PATCH 080/239] agent: Add setting to control edit card expanded state (#34040) This PR adds the `expand_edit_card` setting, which controls whether edit cards in the agent panel are expanded, thus showing or not the full diff of a given file's AI-driven change. I personally prefer to have these cards collapsed by default as I am mostly reviewing diffs using either the review multibuffer or the diffs within the file's buffer itself. Didn't want to change the default behavior as that was intentionally chosen, so here we are! :) Open to feedback about the setting name; I've iterated between a few options and don't necessarily feel like the current one is the best. Release Notes: - agent: Added a setting to control whether edit cards are expanded in the agent panel, thus showing or hiding the full diff of a file's changes. --- assets/settings/default.json | 6 ++++- crates/agent_settings/src/agent_settings.rs | 6 +++++ crates/assistant_tools/src/edit_file_tool.rs | 26 ++++++++++++++++++-- docs/src/ai/configuration.md | 16 ++++++++++++ 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 924b97008482e7f739b642b1730beaa9a60e6e22..203c90f8ff46410366fee07bde8c0e04cec4a290 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -857,7 +857,11 @@ // its response, or needs user input. // Default: false - "play_sound_when_agent_done": false + "play_sound_when_agent_done": false, + /// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff. + /// + /// Default: true + "expand_edit_card": true }, // The settings for slash commands. "slash_commands": { diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index f3087765de072f2043fb7f87fd8369a2eab39d25..30cd2552efb1db06d3d990913a743429f4a95864 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -67,6 +67,7 @@ pub struct AgentSettings { pub model_parameters: Vec, pub preferred_completion_mode: CompletionMode, pub enable_feedback: bool, + pub expand_edit_card: bool, } impl AgentSettings { @@ -291,6 +292,10 @@ pub struct AgentSettingsContent { /// /// Default: true enable_feedback: Option, + /// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff. + /// + /// Default: true + expand_edit_card: Option, } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)] @@ -441,6 +446,7 @@ impl Settings for AgentSettings { value.preferred_completion_mode, ); merge(&mut settings.enable_feedback, value.enable_feedback); + merge(&mut settings.expand_edit_card, value.expand_edit_card); settings .model_parameters diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 8c7728b4b72c9aa52c717e58fbdd63591dd88f0f..baf62c11f26cbefd0fcaecc6c99b0ebb71e42c93 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -4,6 +4,7 @@ use crate::{ schema::json_schema_for, ui::{COLLAPSED_LINES, ToolOutputPreview}, }; +use agent_settings; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::{ ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, @@ -14,7 +15,7 @@ use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey}; use futures::StreamExt; use gpui::{ Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task, - TextStyleRefinement, WeakEntity, pulsating_between, px, + TextStyleRefinement, Transformation, WeakEntity, percentage, pulsating_between, px, }; use indoc::formatdoc; use language::{ @@ -515,7 +516,9 @@ pub struct EditFileToolCard { impl EditFileToolCard { pub fn new(path: PathBuf, project: Entity, window: &mut Window, cx: &mut App) -> Self { + let expand_edit_card = agent_settings::AgentSettings::get_global(cx).expand_edit_card; let multibuffer = cx.new(|_| MultiBuffer::without_headers(Capability::ReadOnly)); + let editor = cx.new(|cx| { let mut editor = Editor::new( EditorMode::Full { @@ -556,7 +559,7 @@ impl EditFileToolCard { diff_task: None, preview_expanded: true, error_expanded: None, - full_height_expanded: true, + full_height_expanded: expand_edit_card, total_lines: None, } } @@ -755,6 +758,13 @@ impl ToolCard for EditFileToolCard { _ => None, }; + let running_or_pending = match status { + ToolUseStatus::Running | ToolUseStatus::Pending => Some(()), + _ => None, + }; + + let should_show_loading = running_or_pending.is_some() && !self.full_height_expanded; + let path_label_button = h_flex() .id(("edit-tool-path-label-button", self.editor.entity_id())) .w_full() @@ -863,6 +873,18 @@ impl ToolCard for EditFileToolCard { header.bg(codeblock_header_bg) }) .child(path_label_button) + .when(should_show_loading, |header| { + header.pr_1p5().child( + Icon::new(IconName::ArrowCircle) + .size(IconSize::XSmall) + .color(Color::Info) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + ), + ) + }) .when_some(error_message, |header, error_message| { header.child( h_flex() diff --git a/docs/src/ai/configuration.md b/docs/src/ai/configuration.md index 5c49cde598a71a3592bf96c2660dc4b31dfa8c30..e65433afe5420aa4dc2abf18d87cb7a3b991e0c6 100644 --- a/docs/src/ai/configuration.md +++ b/docs/src/ai/configuration.md @@ -646,3 +646,19 @@ You can choose between `thread` (the default) and `text_thread`: } } ``` + +### Edit Card + +Use the `expand_edit_card` setting to control whether edit cards show the full diff in the Agent Panel. +It is set to `true` by default, but if set to false, the card's height is capped to a certain number of lines, requiring a click to be expanded. + +```json +{ + "agent": { + "expand_edit_card": "false" + } +} +``` + +This setting is currently only available in Preview. +It should be up in Stable by the next release. From 1f3575ad6eb87f6253943bbe73c67c228534ffae Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 8 Jul 2025 14:22:24 +0530 Subject: [PATCH 081/239] project_panel: Only show sticky item shadow when list is scrolled (#34050) Follow up: https://github.com/zed-industries/zed/pull/34042 - Removes `top_slot_items` from `uniform_list` in favor of using existing `decorations` - Add condition to only show shadow for sticky item when list is scrolled and scrollable Release Notes: - N/A --- crates/gpui/src/elements/uniform_list.rs | 49 ++---- crates/project_panel/src/project_panel.rs | 123 +++++++------- crates/ui/src/components/indent_guides.rs | 1 + crates/ui/src/components/sticky_items.rs | 187 +++++++++++++--------- 4 files changed, 181 insertions(+), 179 deletions(-) diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index f32ecfc20cb0d1a488705f9e48e596f9a05ef98c..342490b882b65843549d1261679a6e4dbf204e66 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -42,7 +42,6 @@ where item_count, item_to_measure_index: 0, render_items: Box::new(render_range), - top_slot: None, decorations: Vec::new(), interactivity: Interactivity { element_id: Some(id), @@ -62,7 +61,6 @@ pub struct UniformList { render_items: Box< dyn for<'a> Fn(Range, &'a mut Window, &'a mut App) -> SmallVec<[AnyElement; 64]>, >, - top_slot: Option>, decorations: Vec>, interactivity: Interactivity, scroll_handle: Option, @@ -73,7 +71,6 @@ pub struct UniformList { /// Frame state used by the [UniformList]. pub struct UniformListFrameState { items: SmallVec<[AnyElement; 32]>, - top_slot_items: SmallVec<[AnyElement; 8]>, decorations: SmallVec<[AnyElement; 1]>, } @@ -145,6 +142,15 @@ impl UniformListScrollHandle { .map(|(ix, _)| ix) .unwrap_or_else(|| this.base_handle.logical_scroll_top().0) } + + /// Checks if the list can be scrolled vertically. + pub fn is_scrollable(&self) -> bool { + if let Some(size) = self.0.borrow().last_item_size { + size.contents.height > size.item.height + } else { + false + } + } } impl Styled for UniformList { @@ -217,7 +223,6 @@ impl Element for UniformList { UniformListFrameState { items: SmallVec::new(), decorations: SmallVec::new(), - top_slot_items: SmallVec::new(), }, ) } @@ -369,17 +374,8 @@ impl Element for UniformList { let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height) / item_height) .ceil() as usize; - let initial_range = first_visible_element_ix - ..cmp::min(last_visible_element_ix, self.item_count); - let mut top_slot_elements = if let Some(ref mut top_slot) = self.top_slot { - top_slot.compute(initial_range, window, cx) - } else { - SmallVec::new() - }; - let top_slot_offset = top_slot_elements.len(); - - let visible_range = (top_slot_offset + first_visible_element_ix) + let visible_range = first_visible_element_ix ..cmp::min(last_visible_element_ix, self.item_count); let items = if y_flipped { @@ -418,20 +414,6 @@ impl Element for UniformList { frame_state.items.push(item); } - if let Some(ref top_slot) = self.top_slot { - top_slot.prepaint( - &mut top_slot_elements, - padded_bounds, - item_height, - scroll_offset, - padding, - can_scroll_horizontally, - window, - cx, - ); - } - frame_state.top_slot_items = top_slot_elements; - let bounds = Bounds::new( padded_bounds.origin + point( @@ -448,6 +430,7 @@ impl Element for UniformList { let mut decoration = decoration.as_ref().compute( visible_range.clone(), bounds, + scroll_offset, item_height, self.item_count, window, @@ -493,9 +476,6 @@ impl Element for UniformList { for decoration in &mut request_layout.decorations { decoration.paint(window, cx); } - if let Some(ref top_slot) = self.top_slot { - top_slot.paint(&mut request_layout.top_slot_items, window, cx); - } }, ) } @@ -518,6 +498,7 @@ pub trait UniformListDecoration { &self, visible_range: Range, bounds: Bounds, + scroll_offset: Point, item_height: Pixels, item_count: usize, window: &mut Window, @@ -592,12 +573,6 @@ impl UniformList { self } - /// Sets a top slot for the list. - pub fn with_top_slot(mut self, top_slot: impl UniformListTopSlot + 'static) -> Self { - self.top_slot = Some(Box::new(top_slot)); - self - } - fn measure_item( &self, list_width: Option, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index a5861250a41fba178c6959726548d9283d900cad..bd0d5e3919b02875077d251626fc759c2f2f6d38 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -56,8 +56,8 @@ use std::{ use theme::ThemeSettings; use ui::{ Color, ContextMenu, DecoratedIcon, Icon, IconDecoration, IconDecorationKind, IndentGuideColors, - IndentGuideLayout, KeyBinding, Label, LabelSize, ListItem, ListItemSpacing, Scrollbar, - ScrollbarState, StickyCandidate, Tooltip, prelude::*, v_flex, + IndentGuideLayout, KeyBinding, Label, LabelSize, ListItem, ListItemSpacing, ScrollableHandle, + Scrollbar, ScrollbarState, StickyCandidate, Tooltip, prelude::*, v_flex, }; use util::{ResultExt, TakeUntilExt, TryFutureExt, maybe, paths::compare_paths}; use workspace::{ @@ -3327,7 +3327,13 @@ impl ProjectPanel { range: Range, window: &mut Window, cx: &mut Context, - mut callback: impl FnMut(&Entry, &HashSet>, &mut Window, &mut Context), + mut callback: impl FnMut( + &Entry, + usize, + &HashSet>, + &mut Window, + &mut Context, + ), ) { let mut ix = 0; for (_, visible_worktree_entries, entries_paths) in &self.visible_entries { @@ -3348,8 +3354,10 @@ impl ProjectPanel { .map(|e| (e.path.clone())) .collect() }); - for entry in visible_worktree_entries[entry_range].iter() { - callback(&entry, entries, window, cx); + let base_index = ix + entry_range.start; + for (i, entry) in visible_worktree_entries[entry_range].iter().enumerate() { + let global_index = base_index + i; + callback(&entry, global_index, entries, window, cx); } ix = end_ix; } @@ -3930,7 +3938,15 @@ impl ProjectPanel { } }; - let last_sticky_item = details.sticky.as_ref().map_or(false, |item| item.is_last); + let show_sticky_shadow = details.sticky.as_ref().map_or(false, |item| { + if item.is_last { + let is_scrollable = self.scroll_handle.is_scrollable(); + let is_scrolled = self.scroll_handle.offset().y < px(0.); + is_scrollable && is_scrolled + } else { + false + } + }); let shadow_color_top = hsla(0.0, 0.0, 0.0, 0.15); let shadow_color_bottom = hsla(0.0, 0.0, 0.0, 0.); let sticky_shadow = div() @@ -3956,7 +3972,7 @@ impl ProjectPanel { .border_r_2() .border_color(border_color) .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color)) - .when(is_sticky && last_sticky_item, |this| this.child(sticky_shadow)) + .when(show_sticky_shadow, |this| this.child(sticky_shadow)) .when(!is_sticky, |this| { this .when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over)) @@ -4828,54 +4844,6 @@ impl ProjectPanel { None } - fn candidate_entries_in_range_for_sticky( - &self, - range: Range, - _window: &mut Window, - _cx: &mut Context, - ) -> Vec { - let mut result = Vec::new(); - let mut current_offset = 0; - - for (_, visible_worktree_entries, entries_paths) in &self.visible_entries { - let worktree_len = visible_worktree_entries.len(); - let worktree_end_offset = current_offset + worktree_len; - - if current_offset >= range.end { - break; - } - - if worktree_end_offset > range.start { - let local_start = range.start.saturating_sub(current_offset); - let local_end = range.end.saturating_sub(current_offset).min(worktree_len); - - let paths = entries_paths.get_or_init(|| { - visible_worktree_entries - .iter() - .map(|e| e.path.clone()) - .collect() - }); - - let entries_from_this_worktree = visible_worktree_entries[local_start..local_end] - .iter() - .enumerate() - .map(|(i, entry)| { - let (depth, _) = Self::calculate_depth_and_difference(&entry.entry, paths); - StickyProjectPanelCandidate { - index: current_offset + local_start + i, - depth, - } - }); - - result.extend(entries_from_this_worktree); - } - - current_offset = worktree_end_offset; - } - - result - } - fn render_sticky_entries( &self, child: StickyProjectPanelCandidate, @@ -4926,6 +4894,10 @@ impl ProjectPanel { break 'outer; } + if sticky_parents.is_empty() { + return SmallVec::new(); + } + sticky_parents.reverse(); let git_status_enabled = ProjectPanelSettings::get_global(cx).git_status; @@ -4940,6 +4912,8 @@ impl ProjectPanel { Default::default() }; + // already checked if non empty above + let last_item_index = sticky_parents.len() - 1; sticky_parents .iter() .enumerate() @@ -4950,7 +4924,7 @@ impl ProjectPanel { .unwrap_or_default(); let sticky_details = Some(StickyDetails { sticky_index: index, - is_last: index == sticky_parents.len() - 1, + is_last: index == last_item_index, }); let details = self.details_for_entry( entry, @@ -5191,17 +5165,6 @@ impl Render for ProjectPanel { items }) }) - .when(show_sticky_scroll, |list| { - list.with_top_slot(ui::sticky_items( - cx.entity().clone(), - |this, range, window, cx| { - this.candidate_entries_in_range_for_sticky(range, window, cx) - }, - |this, marker_entry, window, cx| { - this.render_sticky_entries(marker_entry, window, cx) - }, - )) - }) .when(show_indent_guides, |list| { list.with_decoration( ui::indent_guides( @@ -5215,7 +5178,7 @@ impl Render for ProjectPanel { range, window, cx, - |entry, entries, _, _| { + |entry, _, entries, _, _| { let (depth, _) = Self::calculate_depth_and_difference( entry, entries, ); @@ -5301,6 +5264,30 @@ impl Render for ProjectPanel { ), ) }) + .when(show_sticky_scroll, |list| { + list.with_decoration(ui::sticky_items( + cx.entity().clone(), + |this, range, window, cx| { + let mut items = SmallVec::with_capacity(range.end - range.start); + this.iter_visible_entries( + range, + window, + cx, + |entry, index, entries, _, _| { + let (depth, _) = + Self::calculate_depth_and_difference(entry, entries); + let candidate = + StickyProjectPanelCandidate { index, depth }; + items.push(candidate); + }, + ); + items + }, + |this, marker_entry, window, cx| { + this.render_sticky_entries(marker_entry, window, cx) + }, + )) + }) .size_full() .with_sizing_behavior(ListSizingBehavior::Infer) .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained) diff --git a/crates/ui/src/components/indent_guides.rs b/crates/ui/src/components/indent_guides.rs index 01b3e2cf74f090173f6cffd173d59ae3003664ca..6d4db984f938454c4f72f06ae63490b78e014e85 100644 --- a/crates/ui/src/components/indent_guides.rs +++ b/crates/ui/src/components/indent_guides.rs @@ -147,6 +147,7 @@ mod uniform_list { &self, visible_range: Range, bounds: Bounds, + _scroll_offset: Point, item_height: Pixels, item_count: usize, window: &mut Window, diff --git a/crates/ui/src/components/sticky_items.rs b/crates/ui/src/components/sticky_items.rs index e5ef0cdf27daae5ccbd9fc1eeceac631e8ce757b..e98e3023d291aee6df3ea83a75af898de3359738 100644 --- a/crates/ui/src/components/sticky_items.rs +++ b/crates/ui/src/components/sticky_items.rs @@ -1,7 +1,8 @@ -use std::ops::Range; +use std::{ops::Range, rc::Rc}; use gpui::{ - AnyElement, App, AvailableSpace, Bounds, Context, Entity, Pixels, Render, UniformListTopSlot, + AnyElement, App, AvailableSpace, Bounds, Context, Element, ElementId, Entity, GlobalElementId, + InspectorElementId, IntoElement, LayoutId, Pixels, Point, Render, Style, UniformListDecoration, Window, point, size, }; use smallvec::SmallVec; @@ -10,16 +11,16 @@ pub trait StickyCandidate { fn depth(&self) -> usize; } +#[derive(Clone)] pub struct StickyItems { - compute_fn: Box, &mut Window, &mut App) -> Vec>, - render_fn: Box SmallVec<[AnyElement; 8]>>, - last_item_is_drifting: bool, - anchor_index: Option, + compute_fn: Rc, &mut Window, &mut App) -> SmallVec<[T; 8]>>, + render_fn: Rc SmallVec<[AnyElement; 8]>>, } pub fn sticky_items( entity: Entity, - compute_fn: impl Fn(&mut V, Range, &mut Window, &mut Context) -> Vec + 'static, + compute_fn: impl Fn(&mut V, Range, &mut Window, &mut Context) -> SmallVec<[T; 8]> + + 'static, render_fn: impl Fn(&mut V, T, &mut Window, &mut Context) -> SmallVec<[AnyElement; 8]> + 'static, ) -> StickyItems where @@ -29,37 +30,106 @@ where let entity_compute = entity.clone(); let entity_render = entity.clone(); - let compute_fn = Box::new( - move |range: Range, window: &mut Window, cx: &mut App| -> Vec { + let compute_fn = Rc::new( + move |range: Range, window: &mut Window, cx: &mut App| -> SmallVec<[T; 8]> { entity_compute.update(cx, |view, cx| compute_fn(view, range, window, cx)) }, ); - let render_fn = Box::new( + let render_fn = Rc::new( move |entry: T, window: &mut Window, cx: &mut App| -> SmallVec<[AnyElement; 8]> { entity_render.update(cx, |view, cx| render_fn(view, entry, window, cx)) }, ); + StickyItems { compute_fn, render_fn, - last_item_is_drifting: false, - anchor_index: None, } } -impl UniformListTopSlot for StickyItems +struct StickyItemsElement { + elements: SmallVec<[AnyElement; 8]>, +} + +impl IntoElement for StickyItemsElement { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for StickyItemsElement { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + (window.request_layout(Style::default(), [], cx), ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _window: &mut Window, + _cx: &mut App, + ) -> Self::PrepaintState { + () + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + // reverse so that last item is bottom most among sticky items + for item in self.elements.iter_mut().rev() { + item.paint(window, cx); + } + } +} + +impl UniformListDecoration for StickyItems where T: StickyCandidate + Clone + 'static, { fn compute( - &mut self, + &self, visible_range: Range, + bounds: Bounds, + scroll_offset: Point, + item_height: Pixels, + _item_count: usize, window: &mut Window, cx: &mut App, - ) -> SmallVec<[AnyElement; 8]> { + ) -> AnyElement { let entries = (self.compute_fn)(visible_range.clone(), window, cx); + let mut elements = SmallVec::new(); let mut anchor_entry = None; + let mut last_item_is_drifting = false; + let mut anchor_index = None; let mut iter = entries.iter().enumerate().peekable(); while let Some((ix, current_entry)) = iter.next() { @@ -75,8 +145,8 @@ where let next_depth = next_entry.depth(); if next_depth < current_depth && next_depth < index_in_range { - self.last_item_is_drifting = true; - self.anchor_index = Some(visible_range.start + ix); + last_item_is_drifting = true; + anchor_index = Some(visible_range.start + ix); anchor_entry = Some(current_entry.clone()); break; } @@ -84,67 +154,36 @@ where } if let Some(anchor_entry) = anchor_entry { - (self.render_fn)(anchor_entry, window, cx) - } else { - SmallVec::new() - } - } + elements = (self.render_fn)(anchor_entry, window, cx); + let items_count = elements.len(); + + for (ix, element) in elements.iter_mut().enumerate() { + let mut item_y_offset = None; + if ix == items_count - 1 && last_item_is_drifting { + if let Some(anchor_index) = anchor_index { + let scroll_top = -scroll_offset.y; + let anchor_top = item_height * anchor_index; + let sticky_area_height = item_height * items_count; + item_y_offset = + Some((anchor_top - scroll_top - sticky_area_height).min(Pixels::ZERO)); + }; + } - fn prepaint( - &self, - items: &mut SmallVec<[AnyElement; 8]>, - bounds: Bounds, - item_height: Pixels, - scroll_offset: gpui::Point, - padding: gpui::Edges, - can_scroll_horizontally: bool, - window: &mut Window, - cx: &mut App, - ) { - let items_count = items.len(); - - for (ix, item) in items.iter_mut().enumerate() { - let mut item_y_offset = None; - if ix == items_count - 1 && self.last_item_is_drifting { - if let Some(anchor_index) = self.anchor_index { - let scroll_top = -scroll_offset.y; - let anchor_top = item_height * anchor_index; - let sticky_area_height = item_height * items_count; - item_y_offset = - Some((anchor_top - scroll_top - sticky_area_height).min(Pixels::ZERO)); - }; - } + let sticky_origin = bounds.origin + + point( + -scroll_offset.x, + -scroll_offset.y + item_height * ix + item_y_offset.unwrap_or(Pixels::ZERO), + ); - let sticky_origin = bounds.origin - + point( - if can_scroll_horizontally { - scroll_offset.x + padding.left - } else { - scroll_offset.x - }, - item_height * ix + padding.top + item_y_offset.unwrap_or(Pixels::ZERO), + let available_space = size( + AvailableSpace::Definite(bounds.size.width), + AvailableSpace::Definite(item_height), ); - - let available_width = if can_scroll_horizontally { - bounds.size.width + scroll_offset.x.abs() - } else { - bounds.size.width - }; - - let available_space = size( - AvailableSpace::Definite(available_width), - AvailableSpace::Definite(item_height), - ); - - item.layout_as_root(available_space, window, cx); - item.prepaint_at(sticky_origin, window, cx); + element.layout_as_root(available_space, window, cx); + element.prepaint_at(sticky_origin, window, cx); + } } - } - fn paint(&self, items: &mut SmallVec<[AnyElement; 8]>, window: &mut Window, cx: &mut App) { - // reverse so that last item is bottom most among sticky items - for item in items.iter_mut().rev() { - item.paint(window, cx); - } + StickyItemsElement { elements }.into_any_element() } } From 5e15c05a9de28e1ba132227351deebdbb281d13a Mon Sep 17 00:00:00 2001 From: curiouslad <31123517+curiouslad@users.noreply.github.com> Date: Tue, 8 Jul 2025 15:14:22 +0200 Subject: [PATCH 082/239] editor: Fix diagnostic popovers not being scrollable (#33581) Closes #32673 Release Notes: - Fixed diagnostic popovers not being scrollable --- crates/editor/src/hover_popover.rs | 86 +++++++++++++++++++++++------- 1 file changed, 67 insertions(+), 19 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index cae47895356c4fbd6ffc94779952475ce6f18dd6..bda229e34669482549182b2c7abbe2c3efb9a751 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -381,10 +381,14 @@ fn show_hover( .anchor_after(local_diagnostic.range.end), }; + let scroll_handle = ScrollHandle::new(); + Some(DiagnosticPopover { local_diagnostic, markdown, border_color, + scrollbar_state: ScrollbarState::new(scroll_handle.clone()), + scroll_handle, background_color, keyboard_grace: Rc::new(RefCell::new(ignore_timeout)), anchor, @@ -955,6 +959,8 @@ pub struct DiagnosticPopover { pub keyboard_grace: Rc>, pub anchor: Anchor, _subscription: Subscription, + pub scroll_handle: ScrollHandle, + pub scrollbar_state: ScrollbarState, } impl DiagnosticPopover { @@ -968,10 +974,7 @@ impl DiagnosticPopover { let this = cx.entity().downgrade(); div() .id("diagnostic") - .block() - .max_h(max_size.height) - .overflow_y_scroll() - .max_w(max_size.width) + .occlude() .elevation_2_borderless(cx) // Don't draw the background color if the theme // allows transparent surfaces. @@ -992,27 +995,72 @@ impl DiagnosticPopover { div() .py_1() .px_2() - .child( - MarkdownElement::new( - self.markdown.clone(), - diagnostics_markdown_style(window, cx), - ) - .on_url_click(move |link, window, cx| { - if let Some(renderer) = GlobalDiagnosticRenderer::global(cx) { - this.update(cx, |this, cx| { - renderer.as_ref().open_link(this, link, window, cx); - }) - .ok(); - } - }), - ) .bg(self.background_color) .border_1() .border_color(self.border_color) - .rounded_lg(), + .rounded_lg() + .child( + div() + .id("diagnostic-content-container") + .overflow_y_scroll() + .max_w(max_size.width) + .max_h(max_size.height) + .track_scroll(&self.scroll_handle) + .child( + MarkdownElement::new( + self.markdown.clone(), + diagnostics_markdown_style(window, cx), + ) + .on_url_click( + move |link, window, cx| { + if let Some(renderer) = GlobalDiagnosticRenderer::global(cx) + { + this.update(cx, |this, cx| { + renderer.as_ref().open_link(this, link, window, cx); + }) + .ok(); + } + }, + ), + ), + ) + .child(self.render_vertical_scrollbar(cx)), ) .into_any_element() } + + fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ + div() + .occlude() + .id("diagnostic-popover-vertical-scroll") + .on_mouse_move(cx.listener(|_, _, _, cx| { + cx.notify(); + cx.stop_propagation() + })) + .on_hover(|_, _, cx| { + cx.stop_propagation(); + }) + .on_any_mouse_down(|_, _, cx| { + cx.stop_propagation(); + }) + .on_mouse_up( + MouseButton::Left, + cx.listener(|_, _, _, cx| { + cx.stop_propagation(); + }), + ) + .on_scroll_wheel(cx.listener(|_, _, _, cx| { + cx.notify(); + })) + .h_full() + .absolute() + .right_1() + .top_1() + .bottom_0() + .w(px(12.)) + .cursor_default() + .children(Scrollbar::vertical(self.scrollbar_state.clone())) + } } #[cfg(test)] From 3327f90e0f1ffa431e6598a968b6d88d8e5675dc Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 8 Jul 2025 10:43:35 -0300 Subject: [PATCH 083/239] agent: Add setting to control terminal card expanded state (#34061) Similar to https://github.com/zed-industries/zed/pull/34040, this PR allows to control via settings whether the terminal card in the agent panel should be expanded. It is set to true by default. Release Notes: - agent: Added a setting to control whether terminal cards are expanded in the agent panel, thus showing or hiding the full command output. --- assets/settings/default.json | 6 +- crates/agent_settings/src/agent_settings.rs | 9 +++ crates/assistant_tools/src/terminal_tool.rs | 80 +++++++++++++-------- docs/src/ai/configuration.md | 16 +++++ 4 files changed, 79 insertions(+), 32 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 203c90f8ff46410366fee07bde8c0e04cec4a290..9693a474e746a10c39d6462d2b7f67707d105ecf 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -861,7 +861,11 @@ /// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff. /// /// Default: true - "expand_edit_card": true + "expand_edit_card": true, + /// Whether to have terminal cards in the agent panel expanded, showing the whole command output. + /// + /// Default: true + "expand_terminal_card": true }, // The settings for slash commands. "slash_commands": { diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 30cd2552efb1db06d3d990913a743429f4a95864..131cd2dc3f3e4e8967c03cbf1e808ebdeee306cf 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -68,6 +68,7 @@ pub struct AgentSettings { pub preferred_completion_mode: CompletionMode, pub enable_feedback: bool, pub expand_edit_card: bool, + pub expand_terminal_card: bool, } impl AgentSettings { @@ -296,6 +297,10 @@ pub struct AgentSettingsContent { /// /// Default: true expand_edit_card: Option, + /// Whether to have terminal cards in the agent panel expanded, showing the whole command output. + /// + /// Default: true + expand_terminal_card: Option, } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)] @@ -447,6 +452,10 @@ impl Settings for AgentSettings { ); merge(&mut settings.enable_feedback, value.enable_feedback); merge(&mut settings.expand_edit_card, value.expand_edit_card); + merge( + &mut settings.expand_terminal_card, + value.expand_terminal_card, + ); settings .model_parameters diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index 9a3eac907cbdd6848df32eeed9481058bc368840..6641873182b311c5a28cb975b7327adcb30a16df 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -2,12 +2,13 @@ use crate::{ schema::json_schema_for, ui::{COLLAPSED_LINES, ToolOutputPreview}, }; +use agent_settings; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus}; use futures::{FutureExt as _, future::Shared}; use gpui::{ - AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, TextStyleRefinement, - WeakEntity, Window, + Animation, AnimationExt, AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, + TextStyleRefinement, Transformation, WeakEntity, Window, percentage, }; use language::LineEnding; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; @@ -247,6 +248,7 @@ impl Tool for TerminalTool { command_markdown.clone(), working_dir.clone(), cx.entity_id(), + cx, ) }); @@ -441,7 +443,10 @@ impl TerminalToolCard { input_command: Entity, working_dir: Option, entity_id: EntityId, + cx: &mut Context, ) -> Self { + let expand_terminal_card = + agent_settings::AgentSettings::get_global(cx).expand_terminal_card; Self { input_command, working_dir, @@ -453,7 +458,7 @@ impl TerminalToolCard { finished_with_empty_output: false, original_content_len: 0, content_line_count: 0, - preview_expanded: true, + preview_expanded: expand_terminal_card, start_instant: Instant::now(), elapsed_time: None, } @@ -518,6 +523,46 @@ impl ToolCard for TerminalToolCard { .color(Color::Muted), ), ) + .when(!self.command_finished, |header| { + header.child( + Icon::new(IconName::ArrowCircle) + .size(IconSize::XSmall) + .color(Color::Info) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + ), + ) + }) + .when(tool_failed || command_failed, |header| { + header.child( + div() + .id(("terminal-tool-error-code-indicator", self.entity_id)) + .child( + Icon::new(IconName::Close) + .size(IconSize::Small) + .color(Color::Error), + ) + .when(command_failed && self.exit_status.is_some(), |this| { + this.tooltip(Tooltip::text(format!( + "Exited with code {}", + self.exit_status + .and_then(|status| status.code()) + .unwrap_or(-1), + ))) + }) + .when( + !command_failed && tool_failed && status.error().is_some(), + |this| { + this.tooltip(Tooltip::text(format!( + "Error: {}", + status.error().unwrap(), + ))) + }, + ), + ) + }) .when(self.was_content_truncated, |header| { let tooltip = if self.content_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES { "Output exceeded terminal max lines and was \ @@ -555,34 +600,6 @@ impl ToolCard for TerminalToolCard { .size(LabelSize::Small), ) }) - .when(tool_failed || command_failed, |header| { - header.child( - div() - .id(("terminal-tool-error-code-indicator", self.entity_id)) - .child( - Icon::new(IconName::Close) - .size(IconSize::Small) - .color(Color::Error), - ) - .when(command_failed && self.exit_status.is_some(), |this| { - this.tooltip(Tooltip::text(format!( - "Exited with code {}", - self.exit_status - .and_then(|status| status.code()) - .unwrap_or(-1), - ))) - }) - .when( - !command_failed && tool_failed && status.error().is_some(), - |this| { - this.tooltip(Tooltip::text(format!( - "Error: {}", - status.error().unwrap(), - ))) - }, - ), - ) - }) .when(!self.finished_with_empty_output, |header| { header.child( Disclosure::new( @@ -634,6 +651,7 @@ impl ToolCard for TerminalToolCard { div() .pt_2() .border_t_1() + .when(tool_failed || command_failed, |card| card.border_dashed()) .border_color(border_color) .bg(cx.theme().colors().editor_background) .rounded_b_md() diff --git a/docs/src/ai/configuration.md b/docs/src/ai/configuration.md index e65433afe5420aa4dc2abf18d87cb7a3b991e0c6..907e318d05af8295219e556ff26022032cbc6405 100644 --- a/docs/src/ai/configuration.md +++ b/docs/src/ai/configuration.md @@ -662,3 +662,19 @@ It is set to `true` by default, but if set to false, the card's height is capped This setting is currently only available in Preview. It should be up in Stable by the next release. + +### Terminal Card + +Use the `expand_terminal_card` setting to control whether terminal cards show the command output in the Agent Panel. +It is set to `true` by default, but if set to false, the card will be fully collapsed even while the command is running, requiring a click to be expanded. + +```json +{ + "agent": { + "expand_terminal_card": "false" + } +} +``` + +This setting is currently only available in Preview. +It should be up in Stable by the next release. From 60e9ab8f9381b7c2bc1f82b2f84ba346c537b386 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 8 Jul 2025 10:08:17 -0400 Subject: [PATCH 084/239] Delete access tokens on user delete (#34036) Trying to delete a user record from our admin panel throws the following error: `update or delete on table "users" violates foreign key constraint "access_tokens_user_id_fkey" on table "access_tokens" Detail: Key (id)=(....) is still referenced from table "access_tokens".` We need to add a cascade delete to the `access_tokens` table. Release Notes: - N/A --- .../20221109000000_test_schema.sql | 2 +- ...d_access_tokens_cascade_delete_on_user.sql | 3 ++ crates/collab/src/db/tests/user_tests.rs | 50 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 crates/collab/migrations/20250707182700_add_access_tokens_cascade_delete_on_user.sql diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 91cf4d7af055cd18f884731d243f7ab04c0e45b2..ca840493ad5772b2eaa054f86c8c927fea5d13b9 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -26,7 +26,7 @@ CREATE UNIQUE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id" CREATE TABLE "access_tokens" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "user_id" INTEGER REFERENCES users (id), + "user_id" INTEGER REFERENCES users (id) ON DELETE CASCADE, "impersonated_user_id" INTEGER REFERENCES users (id), "hash" VARCHAR(128) ); diff --git a/crates/collab/migrations/20250707182700_add_access_tokens_cascade_delete_on_user.sql b/crates/collab/migrations/20250707182700_add_access_tokens_cascade_delete_on_user.sql new file mode 100644 index 0000000000000000000000000000000000000000..ae0ffe24f6322196358225ff4159df9d1cfa6298 --- /dev/null +++ b/crates/collab/migrations/20250707182700_add_access_tokens_cascade_delete_on_user.sql @@ -0,0 +1,3 @@ +ALTER TABLE access_tokens DROP CONSTRAINT access_tokens_user_id_fkey; +ALTER TABLE access_tokens ADD CONSTRAINT access_tokens_user_id_fkey + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; diff --git a/crates/collab/src/db/tests/user_tests.rs b/crates/collab/src/db/tests/user_tests.rs index bb2dac1f77e38fae95160fa7899217bcb981ed43..dd61da55ca001a0424aaeafb0411f8a7de343795 100644 --- a/crates/collab/src/db/tests/user_tests.rs +++ b/crates/collab/src/db/tests/user_tests.rs @@ -44,3 +44,53 @@ async fn test_accepted_tos(db: &Arc) { let user = db.get_user_by_id(user_id).await.unwrap().unwrap(); assert!(user.accepted_tos_at.is_none()); } + +test_both_dbs!( + test_destroy_user_cascade_deletes_access_tokens, + test_destroy_user_cascade_deletes_access_tokens_postgres, + test_destroy_user_cascade_deletes_access_tokens_sqlite +); + +async fn test_destroy_user_cascade_deletes_access_tokens(db: &Arc) { + let user_id = db + .create_user( + "user1@example.com", + Some("user1"), + false, + NewUserParams { + github_login: "user1".to_string(), + github_user_id: 12345, + }, + ) + .await + .unwrap() + .user_id; + + let user = db.get_user_by_id(user_id).await.unwrap(); + assert!(user.is_some()); + + let token_1_id = db + .create_access_token(user_id, None, "token-1", 10) + .await + .unwrap(); + + let token_2_id = db + .create_access_token(user_id, None, "token-2", 10) + .await + .unwrap(); + + let token_1 = db.get_access_token(token_1_id).await; + let token_2 = db.get_access_token(token_2_id).await; + assert!(token_1.is_ok()); + assert!(token_2.is_ok()); + + db.destroy_user(user_id).await.unwrap(); + + let user = db.get_user_by_id(user_id).await.unwrap(); + assert!(user.is_none()); + + let token_1 = db.get_access_token(token_1_id).await; + let token_2 = db.get_access_token(token_2_id).await; + assert!(token_1.is_err()); + assert!(token_2.is_err()); +} From 8bd739d86956f629e6d28284e9321126b7efb144 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:23:36 -0300 Subject: [PATCH 085/239] git panel: Add some design refinements (#34064) Things like borders, border colors, which icons are being used, button sizes, and spacing. There is more to do here: polish that we're using a bunch of divs for spacing, arbitrary pixel values for tokens we have in the system, etc. This is just a quick pass! Release Notes: - git panel: Polished the panel spacing, border colors, and icons. --- crates/git_ui/src/git_panel.rs | 139 +++++++++++++++++---------------- crates/panel/src/panel.rs | 4 +- 2 files changed, 73 insertions(+), 70 deletions(-) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index e26a47ff8f28fe38b7a0f2f480f534608d69c0f8..84ce97a982652369036996261ae0d45e58d8d0ae 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -2844,7 +2844,7 @@ impl GitPanel { PopoverMenu::new(id.into()) .trigger( - IconButton::new("overflow-menu-trigger", IconName::EllipsisVertical) + IconButton::new("overflow-menu-trigger", IconName::Ellipsis) .icon_size(IconSize::Small) .icon_color(Color::Muted), ) @@ -2965,15 +2965,20 @@ impl GitPanel { &self, id: impl Into, keybinding_target: Option, + cx: &mut Context, ) -> impl IntoElement { PopoverMenu::new(id.into()) .trigger( ui::ButtonLike::new_rounded_right("commit-split-button-right") .layer(ui::ElevationIndex::ModalSurface) - .size(ui::ButtonSize::None) + .size(ButtonSize::None) .child( - div() + h_flex() .px_1() + .h_full() + .justify_center() + .border_l_1() + .border_color(cx.theme().colors().border) .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)), ), ) @@ -3066,6 +3071,7 @@ impl GitPanel { Some( self.panel_header_container(window, cx) .px_2() + .justify_between() .child( panel_button(change_string) .color(Color::Muted) @@ -3080,23 +3086,25 @@ impl GitPanel { }) }), ) - .child(div().flex_grow()) // spacer - .child(self.render_overflow_menu("overflow_menu")) - .child(div().w_2()) // another spacer .child( - panel_filled_button(text) - .tooltip(Tooltip::for_action_title_in( - tooltip, - action.as_ref(), - &self.focus_handle, - )) - .disabled(self.entry_count == 0) - .on_click(move |_, _, cx| { - let action = action.boxed_clone(); - cx.defer(move |cx| { - cx.dispatch_action(action.as_ref()); - }) - }), + h_flex() + .gap_1() + .child(self.render_overflow_menu("overflow_menu")) + .child( + panel_filled_button(text) + .tooltip(Tooltip::for_action_title_in( + tooltip, + action.as_ref(), + &self.focus_handle, + )) + .disabled(self.entry_count == 0) + .on_click(move |_, _, cx| { + let action = action.boxed_clone(); + cx.defer(move |cx| { + cx.dispatch_action(action.as_ref()); + }) + }), + ), ), ) } @@ -3174,7 +3182,7 @@ impl GitPanel { .w_full() .h(max_height + footer_size) .border_t_1() - .border_color(cx.theme().colors().border_variant) + .border_color(cx.theme().colors().border) .cursor_text() .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| { window.focus(&this.commit_editor.focus_handle(cx)); @@ -3259,6 +3267,7 @@ impl GitPanel { let (can_commit, tooltip) = self.configure_commit_button(cx); let title = self.commit_button_title(); let commit_tooltip_focus_handle = self.commit_editor.focus_handle(cx); + div() .id("commit-wrapper") .on_hover(cx.listener(move |this, hovered, _, cx| { @@ -3371,6 +3380,7 @@ impl GitPanel { self.render_git_commit_menu( ElementId::Name(format!("split-button-right-{}", title).into()), Some(commit_tooltip_focus_handle.clone()), + cx, ) .into_any_element(), )) @@ -3415,8 +3425,8 @@ impl GitPanel { fn render_pending_amend(&self, cx: &mut Context) -> impl IntoElement { div() - .py_2() - .px(px(8.)) + .p_2() + .border_t_1() .border_color(cx.theme().colors().border) .child( Label::new( @@ -3431,22 +3441,21 @@ impl GitPanel { let branch = active_repository.read(cx).branch.as_ref()?; let commit = branch.most_recent_commit.as_ref()?.clone(); let workspace = self.workspace.clone(); - let this = cx.entity(); + Some( h_flex() - .items_center() - .py_2() - .px(px(8.)) - .border_color(cx.theme().colors().border) + .py_1p5() + .px_2() .gap_1p5() + .justify_between() + .border_t_1() + .border_color(cx.theme().colors().border.opacity(0.8)) .child( div() .flex_grow() .overflow_hidden() - .items_center() .max_w(relative(0.85)) - .h_full() .child( Label::new(commit.subject.clone()) .size(LabelSize::Small) @@ -3480,12 +3489,11 @@ impl GitPanel { } }), ) - .child(div().flex_1()) .when(commit.has_parent, |this| { let has_unstaged = self.has_unstaged_changes(); this.child( panel_icon_button("undo", IconName::Undo) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .icon_color(Color::Muted) .tooltip(move |window, cx| { Tooltip::with_meta( @@ -3507,43 +3515,38 @@ impl GitPanel { } fn render_empty_state(&self, cx: &mut Context) -> impl IntoElement { - h_flex() - .h_full() - .flex_grow() - .justify_center() - .items_center() - .child( - v_flex() - .gap_2() - .child(h_flex().w_full().justify_around().child( - if self.active_repository.is_some() { - "No changes to commit" - } else { - "No Git repositories" - }, - )) - .children({ - let worktree_count = self.project.read(cx).visible_worktrees(cx).count(); - (worktree_count > 0 && self.active_repository.is_none()).then(|| { - h_flex().w_full().justify_around().child( - panel_filled_button("Initialize Repository") - .tooltip(Tooltip::for_action_title_in( - "git init", - &git::Init, - &self.focus_handle, - )) - .on_click(move |_, _, cx| { - cx.defer(move |cx| { - cx.dispatch_action(&git::Init); - }) - }), - ) - }) + h_flex().h_full().flex_grow().justify_center().child( + v_flex() + .gap_2() + .child(h_flex().w_full().justify_around().child( + if self.active_repository.is_some() { + "No changes to commit" + } else { + "No Git repositories" + }, + )) + .children({ + let worktree_count = self.project.read(cx).visible_worktrees(cx).count(); + (worktree_count > 0 && self.active_repository.is_none()).then(|| { + h_flex().w_full().justify_around().child( + panel_filled_button("Initialize Repository") + .tooltip(Tooltip::for_action_title_in( + "git init", + &git::Init, + &self.focus_handle, + )) + .on_click(move |_, _, cx| { + cx.defer(move |cx| { + cx.dispatch_action(&git::Init); + }) + }), + ) }) - .text_ui_sm(cx) - .mx_auto() - .text_color(Color::Placeholder.color(cx)), - ) + }) + .text_ui_sm(cx) + .mx_auto() + .text_color(Color::Placeholder.color(cx)), + ) } fn render_vertical_scrollbar( @@ -4621,7 +4624,7 @@ impl RenderOnce for PanelRepoFooter { }) .trigger_with_tooltip( repo_selector_trigger.disabled(single_repo).truncate(true), - Tooltip::text("Switch active repository"), + Tooltip::text("Switch Active Repository"), ) .anchor(Corner::BottomLeft) .into_any_element(); diff --git a/crates/panel/src/panel.rs b/crates/panel/src/panel.rs index a09034cc1756f5adfa7bd0d38b35a2e63b51e901..658a51167ba7da3f02c49ab77b50e72dabbbae57 100644 --- a/crates/panel/src/panel.rs +++ b/crates/panel/src/panel.rs @@ -67,10 +67,10 @@ pub fn panel_filled_button(label: impl Into) -> ui::Button { pub fn panel_icon_button(id: impl Into, icon: IconName) -> ui::IconButton { let id = ElementId::Name(id.into()); - ui::IconButton::new(id, icon) + + IconButton::new(id, icon) // TODO: Change this once we use on_surface_bg in button_like .layer(ui::ElevationIndex::ModalSurface) - .size(ui::ButtonSize::Compact) } pub fn panel_filled_icon_button(id: impl Into, icon: IconName) -> ui::IconButton { From 0ca0914ccae465e951591f2958d7d5eea67410fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Tue, 8 Jul 2025 22:34:57 +0800 Subject: [PATCH 086/239] windows: Add support for SSH (#29145) Closes #19892 This PR builds on top of #20587 and improves upon it. Release Notes: - N/A --------- Co-authored-by: Kirill Bulatov --- Cargo.lock | 21 + Cargo.toml | 3 + crates/askpass/Cargo.toml | 2 + crates/askpass/src/askpass.rs | 156 ++++--- crates/extension_host/src/extension_host.rs | 10 +- crates/extension_host/src/headless_host.rs | 7 +- crates/file_finder/src/open_path_prompt.rs | 194 ++++++-- .../file_finder/src/open_path_prompt_tests.rs | 56 ++- crates/net/Cargo.toml | 25 ++ crates/net/LICENSE-GPL | 1 + crates/net/src/async_net.rs | 69 +++ crates/net/src/listener.rs | 45 ++ crates/net/src/net.rs | 107 +++++ crates/net/src/socket.rs | 59 +++ crates/net/src/stream.rs | 60 +++ crates/net/src/util.rs | 76 ++++ crates/project/src/debugger/dap_store.rs | 20 +- crates/project/src/project.rs | 22 +- crates/project/src/terminals.rs | 96 ++-- crates/project/src/worktree_store.rs | 29 +- crates/proto/Cargo.toml | 1 + crates/proto/src/typed_envelope.rs | 147 ++++-- crates/recent_projects/src/remote_servers.rs | 49 +- crates/remote/src/ssh_session.rs | 422 ++++++++++++------ crates/util/src/paths.rs | 92 ++++ crates/zed/src/main.rs | 20 +- 26 files changed, 1435 insertions(+), 354 deletions(-) create mode 100644 crates/net/Cargo.toml create mode 120000 crates/net/LICENSE-GPL create mode 100644 crates/net/src/async_net.rs create mode 100644 crates/net/src/listener.rs create mode 100644 crates/net/src/net.rs create mode 100644 crates/net/src/socket.rs create mode 100644 crates/net/src/stream.rs create mode 100644 crates/net/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index 58e482ee39bc5d89870671b77a1bfdd5a4762e9b..302c1cc6eaffbf99fc649ebe44a3ed491f976d0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -538,6 +538,8 @@ dependencies = [ "anyhow", "futures 0.3.31", "gpui", + "net", + "parking_lot", "smol", "tempfile", "util", @@ -10231,6 +10233,18 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "net" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-io", + "smol", + "tempfile", + "windows 0.61.1", + "workspace-hack", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -12535,6 +12549,7 @@ dependencies = [ "prost 0.9.0", "prost-build 0.9.0", "serde", + "typed-path", "workspace-hack", ] @@ -17036,6 +17051,12 @@ dependencies = [ "utf-8", ] +[[package]] +name = "typed-path" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c462d18470a2857aa657d338af5fa67170bb48bcc80a296710ce3b0802a32566" + [[package]] name = "typeid" version = "1.0.3" diff --git a/Cargo.toml b/Cargo.toml index 8dd7892329e70098985225eb4493366389b1aca3..32f520c6024b113ed210f9ab4374a0212f3fca4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,6 +99,7 @@ members = [ "crates/migrator", "crates/mistral", "crates/multi_buffer", + "crates/net", "crates/node_runtime", "crates/notifications", "crates/ollama", @@ -311,6 +312,7 @@ menu = { path = "crates/menu" } migrator = { path = "crates/migrator" } mistral = { path = "crates/mistral" } multi_buffer = { path = "crates/multi_buffer" } +net = { path = "crates/net" } node_runtime = { path = "crates/node_runtime" } notifications = { path = "crates/notifications" } ollama = { path = "crates/ollama" } @@ -660,6 +662,7 @@ features = [ "Win32_Graphics_Gdi", "Win32_Graphics_Imaging", "Win32_Graphics_Imaging_D2D", + "Win32_Networking_WinSock", "Win32_Security", "Win32_Security_Credentials", "Win32_Storage_FileSystem", diff --git a/crates/askpass/Cargo.toml b/crates/askpass/Cargo.toml index d64ee9f7c35d4dfe54ab8d36c9142a0a82f9b3ca..0527399af8b6f45ef18650ee5c286c0b51a83608 100644 --- a/crates/askpass/Cargo.toml +++ b/crates/askpass/Cargo.toml @@ -15,6 +15,8 @@ path = "src/askpass.rs" anyhow.workspace = true futures.workspace = true gpui.workspace = true +net.workspace = true +parking_lot.workspace = true smol.workspace = true tempfile.workspace = true util.workspace = true diff --git a/crates/askpass/src/askpass.rs b/crates/askpass/src/askpass.rs index 519c08aa26bb880a2e4c663c3d42c08cc2912001..f085a2be72d04d7c1d16f855230011639853ddf2 100644 --- a/crates/askpass/src/askpass.rs +++ b/crates/askpass/src/askpass.rs @@ -1,21 +1,14 @@ -use std::path::{Path, PathBuf}; -use std::time::Duration; +use std::{ffi::OsStr, time::Duration}; -#[cfg(unix)] -use anyhow::Context as _; +use anyhow::{Context as _, Result}; use futures::channel::{mpsc, oneshot}; -#[cfg(unix)] -use futures::{AsyncBufReadExt as _, io::BufReader}; -#[cfg(unix)] -use futures::{AsyncWriteExt as _, FutureExt as _, select_biased}; -use futures::{SinkExt, StreamExt}; +use futures::{ + AsyncBufReadExt as _, AsyncWriteExt as _, FutureExt as _, SinkExt, StreamExt, io::BufReader, + select_biased, +}; use gpui::{AsyncApp, BackgroundExecutor, Task}; -#[cfg(unix)] use smol::fs; -#[cfg(unix)] -use smol::net::unix::UnixListener; -#[cfg(unix)] -use util::{ResultExt as _, fs::make_file_executable, get_shell_safe_zed_path}; +use util::ResultExt as _; #[derive(PartialEq, Eq)] pub enum AskPassResult { @@ -42,41 +35,56 @@ impl AskPassDelegate { Self { tx, _task: task } } - pub async fn ask_password(&mut self, prompt: String) -> anyhow::Result { + pub async fn ask_password(&mut self, prompt: String) -> Result { let (tx, rx) = oneshot::channel(); self.tx.send((prompt, tx)).await?; Ok(rx.await?) } } -#[cfg(unix)] pub struct AskPassSession { - script_path: PathBuf, + #[cfg(not(target_os = "windows"))] + script_path: std::path::PathBuf, + #[cfg(target_os = "windows")] + askpass_helper: String, + #[cfg(target_os = "windows")] + secret: std::sync::Arc>, _askpass_task: Task<()>, askpass_opened_rx: Option>, askpass_kill_master_rx: Option>, } -#[cfg(unix)] +#[cfg(not(target_os = "windows"))] +const ASKPASS_SCRIPT_NAME: &str = "askpass.sh"; +#[cfg(target_os = "windows")] +const ASKPASS_SCRIPT_NAME: &str = "askpass.ps1"; + impl AskPassSession { /// This will create a new AskPassSession. /// You must retain this session until the master process exits. #[must_use] - pub async fn new( - executor: &BackgroundExecutor, - mut delegate: AskPassDelegate, - ) -> anyhow::Result { + pub async fn new(executor: &BackgroundExecutor, mut delegate: AskPassDelegate) -> Result { + use net::async_net::UnixListener; + use util::fs::make_file_executable; + + #[cfg(target_os = "windows")] + let secret = std::sync::Arc::new(parking_lot::Mutex::new(String::new())); let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?; let askpass_socket = temp_dir.path().join("askpass.sock"); - let askpass_script_path = temp_dir.path().join("askpass.sh"); + let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME); let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>(); - let listener = - UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?; - let zed_path = get_shell_safe_zed_path()?; + let listener = UnixListener::bind(&askpass_socket).context("creating askpass socket")?; + #[cfg(not(target_os = "windows"))] + let zed_path = util::get_shell_safe_zed_path()?; + #[cfg(target_os = "windows")] + let zed_path = std::env::current_exe() + .context("finding current executable path for use in askpass")?; let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>(); let mut kill_tx = Some(askpass_kill_master_tx); + #[cfg(target_os = "windows")] + let askpass_secret = secret.clone(); let askpass_task = executor.spawn(async move { let mut askpass_opened_tx = Some(askpass_opened_tx); @@ -93,10 +101,14 @@ impl AskPassSession { if let Some(password) = delegate .ask_password(prompt.to_string()) .await - .context("failed to get askpass password") + .context("getting askpass password") .log_err() { stream.write_all(password.as_bytes()).await.log_err(); + #[cfg(target_os = "windows")] + { + *askpass_secret.lock() = password; + } } else { if let Some(kill_tx) = kill_tx.take() { kill_tx.send(()).log_err(); @@ -112,34 +124,49 @@ impl AskPassSession { }); // Create an askpass script that communicates back to this process. - let askpass_script = format!( - "{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n", - zed_exe = zed_path, - askpass_socket = askpass_socket.display(), - print_args = "printf '%s\\0' \"$@\"", - shebang = "#!/bin/sh", - ); - fs::write(&askpass_script_path, askpass_script).await?; + let askpass_script = generate_askpass_script(&zed_path, &askpass_socket); + fs::write(&askpass_script_path, askpass_script) + .await + .with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?; make_file_executable(&askpass_script_path).await?; + #[cfg(target_os = "windows")] + let askpass_helper = format!( + "powershell.exe -ExecutionPolicy Bypass -File {}", + askpass_script_path.display() + ); Ok(Self { + #[cfg(not(target_os = "windows"))] script_path: askpass_script_path, + + #[cfg(target_os = "windows")] + secret, + #[cfg(target_os = "windows")] + askpass_helper, + _askpass_task: askpass_task, askpass_kill_master_rx: Some(askpass_kill_master_rx), askpass_opened_rx: Some(askpass_opened_rx), }) } - pub fn script_path(&self) -> &Path { + #[cfg(not(target_os = "windows"))] + pub fn script_path(&self) -> impl AsRef { &self.script_path } + #[cfg(target_os = "windows")] + pub fn script_path(&self) -> impl AsRef { + &self.askpass_helper + } + // This will run the askpass task forever, resolving as many authentication requests as needed. // The caller is responsible for examining the result of their own commands and cancelling this // future when this is no longer needed. Note that this can only be called once, but due to the // drop order this takes an &mut, so you can `drop()` it after you're done with the master process. pub async fn run(&mut self) -> AskPassResult { - let connection_timeout = Duration::from_secs(10); + // This is the default timeout setting used by VSCode. + let connection_timeout = Duration::from_secs(17); let askpass_opened_rx = self.askpass_opened_rx.take().expect("Only call run once"); let askpass_kill_master_rx = self .askpass_kill_master_rx @@ -158,14 +185,19 @@ impl AskPassSession { } } } + + /// This will return the password that was last set by the askpass script. + #[cfg(target_os = "windows")] + pub fn get_password(&self) -> String { + self.secret.lock().clone() + } } /// The main function for when Zed is running in netcat mode for use in askpass. /// Called from both the remote server binary and the zed binary in their respective main functions. -#[cfg(unix)] pub fn main(socket: &str) { + use net::UnixStream; use std::io::{self, Read, Write}; - use std::os::unix::net::UnixStream; use std::process::exit; let mut stream = match UnixStream::connect(socket) { @@ -182,6 +214,10 @@ pub fn main(socket: &str) { exit(1); } + #[cfg(target_os = "windows")] + while buffer.last().map_or(false, |&b| b == b'\n' || b == b'\r') { + buffer.pop(); + } if buffer.last() != Some(&b'\0') { buffer.push(b'\0'); } @@ -202,28 +238,28 @@ pub fn main(socket: &str) { exit(1); } } -#[cfg(not(unix))] -pub fn main(_socket: &str) {} -#[cfg(not(unix))] -pub struct AskPassSession { - path: PathBuf, +#[inline] +#[cfg(not(target_os = "windows"))] +fn generate_askpass_script(zed_path: &str, askpass_socket: &std::path::Path) -> String { + format!( + "{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n", + zed_exe = zed_path, + askpass_socket = askpass_socket.display(), + print_args = "printf '%s\\0' \"$@\"", + shebang = "#!/bin/sh", + ) } -#[cfg(not(unix))] -impl AskPassSession { - pub async fn new(_: &BackgroundExecutor, _: AskPassDelegate) -> anyhow::Result { - Ok(Self { - path: PathBuf::new(), - }) - } - - pub fn script_path(&self) -> &Path { - &self.path - } - - pub async fn run(&mut self) -> AskPassResult { - futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(20))).await; - AskPassResult::Timedout - } +#[inline] +#[cfg(target_os = "windows")] +fn generate_askpass_script(zed_path: &std::path::Path, askpass_socket: &std::path::Path) -> String { + format!( + r#" + $ErrorActionPreference = 'Stop'; + ($args -join [char]0) | & "{zed_exe}" --askpass={askpass_socket} 2> $null + "#, + zed_exe = zed_path.display(), + askpass_socket = askpass_socket.display(), + ) } diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 7c58fac1e0d363a4536fd0c7ea0035609c90330e..dd753199a88aef1366d900432ba9869f9a32942e 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -54,7 +54,7 @@ use std::{ time::{Duration, Instant}, }; use url::Url; -use util::ResultExt; +use util::{ResultExt, paths::RemotePathBuf}; use wasm_host::{ WasmExtension, WasmHost, wit::{is_supported_wasm_api_version, wasm_api_version_range}, @@ -1689,6 +1689,7 @@ impl ExtensionStore { .request(proto::SyncExtensions { extensions }) })? .await?; + let path_style = client.read_with(cx, |client, _| client.path_style())?; for missing_extension in response.missing_extensions.into_iter() { let tmp_dir = tempfile::tempdir()?; @@ -1701,7 +1702,10 @@ impl ExtensionStore { ) })? .await?; - let dest_dir = PathBuf::from(&response.tmp_dir).join(missing_extension.clone().id); + let dest_dir = RemotePathBuf::new( + PathBuf::from(&response.tmp_dir).join(missing_extension.clone().id), + path_style, + ); log::info!("Uploading extension {}", missing_extension.clone().id); client @@ -1718,7 +1722,7 @@ impl ExtensionStore { client .update(cx, |client, _cx| { client.proto_client().request(proto::InstallExtension { - tmp_dir: dest_dir.to_string_lossy().to_string(), + tmp_dir: dest_dir.to_proto(), extension: Some(missing_extension), }) })? diff --git a/crates/extension_host/src/headless_host.rs b/crates/extension_host/src/headless_host.rs index 8feaec89de5c0c607bffe87c3be9b4700169e190..1dfcf56e5dbe9a43fdc443b9a6a8eb0782dc02cc 100644 --- a/crates/extension_host/src/headless_host.rs +++ b/crates/extension_host/src/headless_host.rs @@ -1,7 +1,10 @@ use std::{path::PathBuf, sync::Arc}; use anyhow::{Context as _, Result}; -use client::{TypedEnvelope, proto}; +use client::{ + TypedEnvelope, + proto::{self, FromProto}, +}; use collections::{HashMap, HashSet}; use extension::{ Extension, ExtensionDebugAdapterProviderProxy, ExtensionHostProxy, ExtensionLanguageProxy, @@ -328,7 +331,7 @@ impl HeadlessExtensionStore { version: extension.version, dev: extension.dev, }, - PathBuf::from(envelope.payload.tmp_dir), + PathBuf::from_proto(envelope.payload.tmp_dir), cx, ) })? diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index d7428f00686a2f8405cb1980112101145333b67f..68ba7a78b52fee42588b732d7a6a3c582a80061f 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -15,16 +15,14 @@ use std::{ }; use ui::{Context, LabelLike, ListItem, Window}; use ui::{HighlightedLabel, ListItemSpacing, prelude::*}; -use util::{maybe, paths::compare_paths}; +use util::{ + maybe, + paths::{PathStyle, compare_paths}, +}; use workspace::Workspace; pub(crate) struct OpenPathPrompt; -#[cfg(target_os = "windows")] -const PROMPT_ROOT: &str = "C:\\"; -#[cfg(not(target_os = "windows"))] -const PROMPT_ROOT: &str = "/"; - #[derive(Debug)] pub struct OpenPathDelegate { tx: Option>>>, @@ -34,6 +32,8 @@ pub struct OpenPathDelegate { string_matches: Vec, cancel_flag: Arc, should_dismiss: bool, + prompt_root: String, + path_style: PathStyle, replace_prompt: Task<()>, } @@ -42,6 +42,7 @@ impl OpenPathDelegate { tx: oneshot::Sender>>, lister: DirectoryLister, creating_path: bool, + path_style: PathStyle, ) -> Self { Self { tx: Some(tx), @@ -53,6 +54,11 @@ impl OpenPathDelegate { string_matches: Vec::new(), cancel_flag: Arc::new(AtomicBool::new(false)), should_dismiss: true, + prompt_root: match path_style { + PathStyle::Posix => "/".to_string(), + PathStyle::Windows => "C:\\".to_string(), + }, + path_style, replace_prompt: Task::ready(()), } } @@ -185,7 +191,8 @@ impl OpenPathPrompt { cx: &mut Context, ) { workspace.toggle_modal(window, cx, |window, cx| { - let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path); + let delegate = + OpenPathDelegate::new(tx, lister.clone(), creating_path, PathStyle::current()); let picker = Picker::uniform_list(delegate, window, cx).width(rems(34.)); let query = lister.default_query(cx); picker.set_query(query, window, cx); @@ -226,18 +233,7 @@ impl PickerDelegate for OpenPathDelegate { cx: &mut Context>, ) -> Task<()> { let lister = &self.lister; - let last_item = Path::new(&query) - .file_name() - .unwrap_or_default() - .to_string_lossy(); - let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) { - (dir.to_string(), last_item.into_owned()) - } else { - (query, String::new()) - }; - if dir == "" { - dir = PROMPT_ROOT.to_string(); - } + let (dir, suffix) = get_dir_and_suffix(query, self.path_style); let query = match &self.directory_state { DirectoryState::List { parent_path, .. } => { @@ -266,6 +262,7 @@ impl PickerDelegate for OpenPathDelegate { self.cancel_flag = Arc::new(AtomicBool::new(false)); let cancel_flag = self.cancel_flag.clone(); + let parent_path_is_root = self.prompt_root == dir; cx.spawn_in(window, async move |this, cx| { if let Some(query) = query { let paths = query.await; @@ -279,7 +276,7 @@ impl PickerDelegate for OpenPathDelegate { DirectoryState::None { create: false } | DirectoryState::List { .. } => match paths { Ok(paths) => DirectoryState::List { - entries: path_candidates(&dir, paths), + entries: path_candidates(parent_path_is_root, paths), parent_path: dir.clone(), error: None, }, @@ -292,7 +289,7 @@ impl PickerDelegate for OpenPathDelegate { DirectoryState::None { create: true } | DirectoryState::Create { .. } => match paths { Ok(paths) => { - let mut entries = path_candidates(&dir, paths); + let mut entries = path_candidates(parent_path_is_root, paths); let mut exists = false; let mut is_dir = false; let mut new_id = None; @@ -488,6 +485,7 @@ impl PickerDelegate for OpenPathDelegate { _: &mut Context>, ) -> Option { let candidate = self.get_entry(self.selected_index)?; + let path_style = self.path_style; Some( maybe!({ match &self.directory_state { @@ -496,7 +494,7 @@ impl PickerDelegate for OpenPathDelegate { parent_path, candidate.path.string, if candidate.is_dir { - MAIN_SEPARATOR_STR + path_style.separator() } else { "" } @@ -506,7 +504,7 @@ impl PickerDelegate for OpenPathDelegate { parent_path, candidate.path.string, if candidate.is_dir { - MAIN_SEPARATOR_STR + path_style.separator() } else { "" } @@ -527,8 +525,8 @@ impl PickerDelegate for OpenPathDelegate { DirectoryState::None { .. } => return, DirectoryState::List { parent_path, .. } => { let confirmed_path = - if parent_path == PROMPT_ROOT && candidate.path.string.is_empty() { - PathBuf::from(PROMPT_ROOT) + if parent_path == &self.prompt_root && candidate.path.string.is_empty() { + PathBuf::from(&self.prompt_root) } else { Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref()) .join(&candidate.path.string) @@ -548,8 +546,8 @@ impl PickerDelegate for OpenPathDelegate { return; } let prompted_path = - if parent_path == PROMPT_ROOT && user_input.file.string.is_empty() { - PathBuf::from(PROMPT_ROOT) + if parent_path == &self.prompt_root && user_input.file.string.is_empty() { + PathBuf::from(&self.prompt_root) } else { Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref()) .join(&user_input.file.string) @@ -652,8 +650,8 @@ impl PickerDelegate for OpenPathDelegate { .inset(true) .toggle_state(selected) .child(HighlightedLabel::new( - if parent_path == PROMPT_ROOT { - format!("{}{}", PROMPT_ROOT, candidate.path.string) + if parent_path == &self.prompt_root { + format!("{}{}", self.prompt_root, candidate.path.string) } else { candidate.path.string.clone() }, @@ -665,10 +663,10 @@ impl PickerDelegate for OpenPathDelegate { user_input, .. } => { - let (label, delta) = if parent_path == PROMPT_ROOT { + let (label, delta) = if parent_path == &self.prompt_root { ( - format!("{}{}", PROMPT_ROOT, candidate.path.string), - PROMPT_ROOT.len(), + format!("{}{}", self.prompt_root, candidate.path.string), + self.prompt_root.len(), ) } else { (candidate.path.string.clone(), 0) @@ -751,8 +749,11 @@ impl PickerDelegate for OpenPathDelegate { } } -fn path_candidates(parent_path: &String, mut children: Vec) -> Vec { - if *parent_path == PROMPT_ROOT { +fn path_candidates( + parent_path_is_root: bool, + mut children: Vec, +) -> Vec { + if parent_path_is_root { children.push(DirectoryItem { is_dir: true, path: PathBuf::default(), @@ -769,3 +770,128 @@ fn path_candidates(parent_path: &String, mut children: Vec) -> Ve }) .collect() } + +#[cfg(target_os = "windows")] +fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) { + let last_item = Path::new(&query) + .file_name() + .unwrap_or_default() + .to_string_lossy(); + let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) { + (dir.to_string(), last_item.into_owned()) + } else { + (query.to_string(), String::new()) + }; + match path_style { + PathStyle::Posix => { + if dir.is_empty() { + dir = "/".to_string(); + } + } + PathStyle::Windows => { + if dir.len() < 3 { + dir = "C:\\".to_string(); + } + } + } + (dir, suffix) +} + +#[cfg(not(target_os = "windows"))] +fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) { + match path_style { + PathStyle::Posix => { + let (mut dir, suffix) = if let Some(index) = query.rfind('/') { + (query[..index].to_string(), query[index + 1..].to_string()) + } else { + (query, String::new()) + }; + if !dir.ends_with('/') { + dir.push('/'); + } + (dir, suffix) + } + PathStyle::Windows => { + let (mut dir, suffix) = if let Some(index) = query.rfind('\\') { + (query[..index].to_string(), query[index + 1..].to_string()) + } else { + (query, String::new()) + }; + if dir.len() < 3 { + dir = "C:\\".to_string(); + } + if !dir.ends_with('\\') { + dir.push('\\'); + } + (dir, suffix) + } + } +} + +#[cfg(test)] +mod tests { + use util::paths::PathStyle; + + use crate::open_path_prompt::get_dir_and_suffix; + + #[test] + fn test_get_dir_and_suffix_with_windows_style() { + let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Windows); + assert_eq!(dir, "C:\\"); + assert_eq!(suffix, ""); + + let (dir, suffix) = get_dir_and_suffix("C:".into(), PathStyle::Windows); + assert_eq!(dir, "C:\\"); + assert_eq!(suffix, ""); + + let (dir, suffix) = get_dir_and_suffix("C:\\".into(), PathStyle::Windows); + assert_eq!(dir, "C:\\"); + assert_eq!(suffix, ""); + + let (dir, suffix) = get_dir_and_suffix("C:\\Use".into(), PathStyle::Windows); + assert_eq!(dir, "C:\\"); + assert_eq!(suffix, "Use"); + + let (dir, suffix) = + get_dir_and_suffix("C:\\Users\\Junkui\\Docum".into(), PathStyle::Windows); + assert_eq!(dir, "C:\\Users\\Junkui\\"); + assert_eq!(suffix, "Docum"); + + let (dir, suffix) = + get_dir_and_suffix("C:\\Users\\Junkui\\Documents".into(), PathStyle::Windows); + assert_eq!(dir, "C:\\Users\\Junkui\\"); + assert_eq!(suffix, "Documents"); + + let (dir, suffix) = + get_dir_and_suffix("C:\\Users\\Junkui\\Documents\\".into(), PathStyle::Windows); + assert_eq!(dir, "C:\\Users\\Junkui\\Documents\\"); + assert_eq!(suffix, ""); + } + + #[test] + fn test_get_dir_and_suffix_with_posix_style() { + let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Posix); + assert_eq!(dir, "/"); + assert_eq!(suffix, ""); + + let (dir, suffix) = get_dir_and_suffix("/".into(), PathStyle::Posix); + assert_eq!(dir, "/"); + assert_eq!(suffix, ""); + + let (dir, suffix) = get_dir_and_suffix("/Use".into(), PathStyle::Posix); + assert_eq!(dir, "/"); + assert_eq!(suffix, "Use"); + + let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Posix); + assert_eq!(dir, "/Users/Junkui/"); + assert_eq!(suffix, "Docum"); + + let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Posix); + assert_eq!(dir, "/Users/Junkui/"); + assert_eq!(suffix, "Documents"); + + let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Posix); + assert_eq!(dir, "/Users/Junkui/Documents/"); + assert_eq!(suffix, ""); + } +} diff --git a/crates/file_finder/src/open_path_prompt_tests.rs b/crates/file_finder/src/open_path_prompt_tests.rs index 0acf2a517dc4cf8153e1b60d9261e233e9334fcf..a69ac6992dc280fd6537b16087302c2fbb9f8f4c 100644 --- a/crates/file_finder/src/open_path_prompt_tests.rs +++ b/crates/file_finder/src/open_path_prompt_tests.rs @@ -5,7 +5,7 @@ use picker::{Picker, PickerDelegate}; use project::Project; use serde_json::json; use ui::rems; -use util::path; +use util::{path, paths::PathStyle}; use workspace::{AppState, Workspace}; use crate::OpenPathDelegate; @@ -37,7 +37,7 @@ async fn test_open_path_prompt(cx: &mut TestAppContext) { let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (picker, cx) = build_open_path_prompt(project, false, cx); + let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx); let query = path!("/root"); insert_query(query, &picker, cx).await; @@ -111,7 +111,7 @@ async fn test_open_path_prompt_completion(cx: &mut TestAppContext) { let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (picker, cx) = build_open_path_prompt(project, false, cx); + let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx); // Confirm completion for the query "/root", since it's a directory, it should add a trailing slash. let query = path!("/root"); @@ -186,7 +186,7 @@ async fn test_open_path_prompt_completion(cx: &mut TestAppContext) { } #[gpui::test] -#[cfg(target_os = "windows")] +#[cfg_attr(not(target_os = "windows"), ignore)] async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) { let app_state = init_test(cx); app_state @@ -204,7 +204,7 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) { let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (picker, cx) = build_open_path_prompt(project, false, cx); + let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx); // Support both forward and backward slashes. let query = "C:/root/"; @@ -251,6 +251,47 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) { ); } +#[gpui::test] +#[cfg_attr(not(target_os = "windows"), ignore)] +async fn test_open_path_prompt_on_windows_with_remote(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": "A", + "dir1": {}, + "dir2": {} + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + + let (picker, cx) = build_open_path_prompt(project, false, PathStyle::Posix, cx); + + let query = "/root/"; + insert_query(query, &picker, cx).await; + assert_eq!( + collect_match_candidates(&picker, cx), + vec!["a", "dir1", "dir2"] + ); + assert_eq!(confirm_completion(query, 0, &picker, cx), "/root/a"); + + // Confirm completion for the query "/root/d", selecting the second candidate "dir2", since it's a directory, it should add a trailing slash. + let query = "/root/d"; + insert_query(query, &picker, cx).await; + assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]); + assert_eq!(confirm_completion(query, 1, &picker, cx), "/root/dir2/"); + + let query = "/root/d"; + insert_query(query, &picker, cx).await; + assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]); + assert_eq!(confirm_completion(query, 0, &picker, cx), "/root/dir1/"); +} + #[gpui::test] async fn test_new_path_prompt(cx: &mut TestAppContext) { let app_state = init_test(cx); @@ -278,7 +319,7 @@ async fn test_new_path_prompt(cx: &mut TestAppContext) { let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (picker, cx) = build_open_path_prompt(project, true, cx); + let (picker, cx) = build_open_path_prompt(project, true, PathStyle::current(), cx); insert_query(path!("/root"), &picker, cx).await; assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]); @@ -315,11 +356,12 @@ fn init_test(cx: &mut TestAppContext) -> Arc { fn build_open_path_prompt( project: Entity, creating_path: bool, + path_style: PathStyle, cx: &mut TestAppContext, ) -> (Entity>, &mut VisualTestContext) { let (tx, _) = futures::channel::oneshot::channel(); let lister = project::DirectoryLister::Project(project.clone()); - let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path); + let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path, path_style); let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); ( diff --git a/crates/net/Cargo.toml b/crates/net/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..fc08bc89f53550fe926ce4a00ac68ce4b0502409 --- /dev/null +++ b/crates/net/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "net" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/net.rs" +doctest = false + +[dependencies] +smol.workspace = true +workspace-hack.workspace = true + +[target.'cfg(target_os = "windows")'.dependencies] +anyhow.workspace = true +async-io = "2.4" +windows.workspace = true + +[dev-dependencies] +tempfile.workspace = true diff --git a/crates/net/LICENSE-GPL b/crates/net/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/net/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/net/src/async_net.rs b/crates/net/src/async_net.rs new file mode 100644 index 0000000000000000000000000000000000000000..6a47902bd8dbe20dd9cc34e3e33038db1132a1ce --- /dev/null +++ b/crates/net/src/async_net.rs @@ -0,0 +1,69 @@ +#[cfg(not(target_os = "windows"))] +pub use smol::net::unix::{UnixListener, UnixStream}; + +#[cfg(target_os = "windows")] +pub use windows::{UnixListener, UnixStream}; + +#[cfg(target_os = "windows")] +pub mod windows { + use std::{ + io::Result, + path::Path, + pin::Pin, + task::{Context, Poll}, + }; + + use smol::{ + Async, + io::{AsyncRead, AsyncWrite}, + }; + + pub struct UnixListener(Async); + + impl UnixListener { + pub fn bind>(path: P) -> Result { + Ok(UnixListener(Async::new(crate::UnixListener::bind(path)?)?)) + } + + pub async fn accept(&self) -> Result<(UnixStream, ())> { + let (sock, _) = self.0.read_with(|listener| listener.accept()).await?; + Ok((UnixStream(Async::new(sock)?), ())) + } + } + + pub struct UnixStream(Async); + + impl UnixStream { + pub async fn connect>(path: P) -> Result { + Ok(UnixStream(Async::new(crate::UnixStream::connect(path)?)?)) + } + } + + impl AsyncRead for UnixStream { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + Pin::new(&mut self.0).poll_read(cx, buf) + } + } + + impl AsyncWrite for UnixStream { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + Pin::new(&mut self.0).poll_write(cx, buf) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.0).poll_flush(cx) + } + + fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.0).poll_close(cx) + } + } +} diff --git a/crates/net/src/listener.rs b/crates/net/src/listener.rs new file mode 100644 index 0000000000000000000000000000000000000000..4774bb850ba0d5f8c44769b64e96faafe3245302 --- /dev/null +++ b/crates/net/src/listener.rs @@ -0,0 +1,45 @@ +use std::{ + io::Result, + os::windows::io::{AsSocket, BorrowedSocket}, + path::Path, +}; + +use windows::Win32::Networking::WinSock::{SOCKADDR_UN, SOMAXCONN, bind, listen}; + +use crate::{ + socket::UnixSocket, + stream::UnixStream, + util::{init, map_ret, sockaddr_un}, +}; + +pub struct UnixListener(UnixSocket); + +impl UnixListener { + pub fn bind>(path: P) -> Result { + init(); + let socket = UnixSocket::new()?; + let (addr, len) = sockaddr_un(path)?; + unsafe { + map_ret(bind( + socket.as_raw(), + &addr as *const _ as *const _, + len as i32, + ))?; + map_ret(listen(socket.as_raw(), SOMAXCONN as _))?; + } + Ok(Self(socket)) + } + + pub fn accept(&self) -> Result<(UnixStream, ())> { + let mut storage = SOCKADDR_UN::default(); + let mut len = std::mem::size_of_val(&storage) as i32; + let raw = self.0.accept(&mut storage as *mut _ as *mut _, &mut len)?; + Ok((UnixStream::new(raw), ())) + } +} + +impl AsSocket for UnixListener { + fn as_socket(&self) -> BorrowedSocket<'_> { + unsafe { BorrowedSocket::borrow_raw(self.0.as_raw().0 as _) } + } +} diff --git a/crates/net/src/net.rs b/crates/net/src/net.rs new file mode 100644 index 0000000000000000000000000000000000000000..4fa76ffcb8a0fad2e0f6533e689f2279c947cfff --- /dev/null +++ b/crates/net/src/net.rs @@ -0,0 +1,107 @@ +pub mod async_net; +#[cfg(target_os = "windows")] +pub mod listener; +#[cfg(target_os = "windows")] +pub mod socket; +#[cfg(target_os = "windows")] +pub mod stream; +#[cfg(target_os = "windows")] +mod util; + +#[cfg(target_os = "windows")] +pub use listener::*; +#[cfg(target_os = "windows")] +pub use socket::*; +#[cfg(not(target_os = "windows"))] +pub use std::os::unix::net::{UnixListener, UnixStream}; +#[cfg(target_os = "windows")] +pub use stream::*; + +#[cfg(test)] +mod tests { + use std::io::{Read, Write}; + + use smol::io::{AsyncReadExt, AsyncWriteExt}; + + const SERVER_MESSAGE: &str = "Connection closed"; + const CLIENT_MESSAGE: &str = "Hello, server!"; + const BUFFER_SIZE: usize = 32; + + #[test] + fn test_windows_listener() -> std::io::Result<()> { + use crate::{UnixListener, UnixStream}; + + let temp = tempfile::tempdir()?; + let socket = temp.path().join("socket.sock"); + let listener = UnixListener::bind(&socket)?; + + // Server + let server = std::thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + + // Read data from the client + let mut buffer = [0; BUFFER_SIZE]; + let bytes_read = stream.read(&mut buffer).unwrap(); + let string = String::from_utf8_lossy(&buffer[..bytes_read]); + assert_eq!(string, CLIENT_MESSAGE); + + // Send a message back to the client + stream.write_all(SERVER_MESSAGE.as_bytes()).unwrap(); + }); + + // Client + let mut client = UnixStream::connect(&socket)?; + + // Send data to the server + client.write_all(CLIENT_MESSAGE.as_bytes())?; + let mut buffer = [0; BUFFER_SIZE]; + + // Read the response from the server + let bytes_read = client.read(&mut buffer)?; + let string = String::from_utf8_lossy(&buffer[..bytes_read]); + assert_eq!(string, SERVER_MESSAGE); + client.flush()?; + + server.join().unwrap(); + Ok(()) + } + + #[test] + fn test_unix_listener() -> std::io::Result<()> { + use crate::async_net::{UnixListener, UnixStream}; + + smol::block_on(async { + let temp = tempfile::tempdir()?; + let socket = temp.path().join("socket.sock"); + let listener = UnixListener::bind(&socket)?; + + // Server + let server = smol::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + + // Read data from the client + let mut buffer = [0; BUFFER_SIZE]; + let bytes_read = stream.read(&mut buffer).await.unwrap(); + let string = String::from_utf8_lossy(&buffer[..bytes_read]); + assert_eq!(string, CLIENT_MESSAGE); + + // Send a message back to the client + stream.write_all(SERVER_MESSAGE.as_bytes()).await.unwrap(); + }); + + // Client + let mut client = UnixStream::connect(&socket).await?; + client.write_all(CLIENT_MESSAGE.as_bytes()).await?; + + // Read the response from the server + let mut buffer = [0; BUFFER_SIZE]; + let bytes_read = client.read(&mut buffer).await?; + let string = String::from_utf8_lossy(&buffer[..bytes_read]); + assert_eq!(string, "Connection closed"); + client.flush().await?; + + server.await; + Ok(()) + }) + } +} diff --git a/crates/net/src/socket.rs b/crates/net/src/socket.rs new file mode 100644 index 0000000000000000000000000000000000000000..6a1fa3d4c4309572fa4f3e2918f48ef1b91bd1b5 --- /dev/null +++ b/crates/net/src/socket.rs @@ -0,0 +1,59 @@ +use std::io::{Error, ErrorKind, Result}; + +use windows::Win32::{ + Foundation::{HANDLE, HANDLE_FLAG_INHERIT, HANDLE_FLAGS, SetHandleInformation}, + Networking::WinSock::{ + AF_UNIX, SEND_RECV_FLAGS, SOCK_STREAM, SOCKADDR, SOCKET, WSA_FLAG_OVERLAPPED, + WSAEWOULDBLOCK, WSASocketW, accept, closesocket, recv, send, + }, +}; + +use crate::util::map_ret; + +pub struct UnixSocket(SOCKET); + +impl UnixSocket { + pub fn new() -> Result { + unsafe { + let raw = WSASocketW(AF_UNIX as _, SOCK_STREAM.0, 0, None, 0, WSA_FLAG_OVERLAPPED)?; + SetHandleInformation( + HANDLE(raw.0 as _), + HANDLE_FLAG_INHERIT.0, + HANDLE_FLAGS::default(), + )?; + Ok(Self(raw)) + } + } + + pub(crate) fn as_raw(&self) -> SOCKET { + self.0 + } + + pub fn accept(&self, storage: *mut SOCKADDR, len: &mut i32) -> Result { + match unsafe { accept(self.0, Some(storage), Some(len)) } { + Ok(sock) => Ok(Self(sock)), + Err(err) => { + let wsa_err = unsafe { windows::Win32::Networking::WinSock::WSAGetLastError().0 }; + if wsa_err == WSAEWOULDBLOCK.0 { + Err(Error::new(ErrorKind::WouldBlock, "accept would block")) + } else { + Err(err.into()) + } + } + } + } + + pub(crate) fn recv(&self, buf: &mut [u8]) -> Result { + map_ret(unsafe { recv(self.0, buf, SEND_RECV_FLAGS::default()) }) + } + + pub(crate) fn send(&self, buf: &[u8]) -> Result { + map_ret(unsafe { send(self.0, buf, SEND_RECV_FLAGS::default()) }) + } +} + +impl Drop for UnixSocket { + fn drop(&mut self) { + unsafe { closesocket(self.0) }; + } +} diff --git a/crates/net/src/stream.rs b/crates/net/src/stream.rs new file mode 100644 index 0000000000000000000000000000000000000000..d8b6852fcf3ecada2eab4711be12861f3d92fd74 --- /dev/null +++ b/crates/net/src/stream.rs @@ -0,0 +1,60 @@ +use std::{ + io::{Read, Result, Write}, + os::windows::io::{AsSocket, BorrowedSocket}, + path::Path, +}; + +use async_io::IoSafe; +use windows::Win32::Networking::WinSock::connect; + +use crate::{ + socket::UnixSocket, + util::{init, map_ret, sockaddr_un}, +}; + +pub struct UnixStream(UnixSocket); + +unsafe impl IoSafe for UnixStream {} + +impl UnixStream { + pub fn new(socket: UnixSocket) -> Self { + Self(socket) + } + + pub fn connect>(path: P) -> Result { + init(); + unsafe { + let inner = UnixSocket::new()?; + let (addr, len) = sockaddr_un(path)?; + + map_ret(connect( + inner.as_raw(), + &addr as *const _ as *const _, + len as i32, + ))?; + Ok(Self(inner)) + } + } +} + +impl Read for UnixStream { + fn read(&mut self, buf: &mut [u8]) -> Result { + self.0.recv(buf) + } +} + +impl Write for UnixStream { + fn write(&mut self, buf: &[u8]) -> Result { + self.0.send(buf) + } + + fn flush(&mut self) -> Result<()> { + Ok(()) + } +} + +impl AsSocket for UnixStream { + fn as_socket(&self) -> BorrowedSocket<'_> { + unsafe { BorrowedSocket::borrow_raw(self.0.as_raw().0 as _) } + } +} diff --git a/crates/net/src/util.rs b/crates/net/src/util.rs new file mode 100644 index 0000000000000000000000000000000000000000..f454c099c7e3b84e54e84bb8b8eaf49b66754570 --- /dev/null +++ b/crates/net/src/util.rs @@ -0,0 +1,76 @@ +use std::{ + io::{Error, ErrorKind, Result}, + path::Path, + sync::Once, +}; + +use windows::Win32::Networking::WinSock::{ + ADDRESS_FAMILY, AF_UNIX, SOCKADDR_UN, SOCKET_ERROR, WSAGetLastError, WSAStartup, +}; + +pub(crate) fn init() { + static ONCE: Once = Once::new(); + + ONCE.call_once(|| unsafe { + let mut wsa_data = std::mem::zeroed(); + let result = WSAStartup(0x202, &mut wsa_data); + if result != 0 { + panic!("WSAStartup failed: {}", result); + } + }); +} + +// https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/ +pub(crate) fn sockaddr_un>(path: P) -> Result<(SOCKADDR_UN, usize)> { + let mut addr = SOCKADDR_UN::default(); + addr.sun_family = ADDRESS_FAMILY(AF_UNIX); + + let bytes = path + .as_ref() + .to_str() + .map(|s| s.as_bytes()) + .ok_or(ErrorKind::InvalidInput)?; + + if bytes.contains(&0) { + return Err(Error::new( + ErrorKind::InvalidInput, + "paths may not contain interior null bytes", + )); + } + if bytes.len() >= addr.sun_path.len() { + return Err(Error::new( + ErrorKind::InvalidInput, + "path must be shorter than SUN_LEN", + )); + } + + unsafe { + std::ptr::copy_nonoverlapping( + bytes.as_ptr(), + addr.sun_path.as_mut_ptr().cast(), + bytes.len(), + ); + } + + let mut len = sun_path_offset(&addr) + bytes.len(); + match bytes.first() { + Some(&0) | None => {} + Some(_) => len += 1, + } + Ok((addr, len)) +} + +pub(crate) fn map_ret(ret: i32) -> Result { + if ret == SOCKET_ERROR { + Err(Error::from_raw_os_error(unsafe { WSAGetLastError().0 })) + } else { + Ok(ret as usize) + } +} + +fn sun_path_offset(addr: &SOCKADDR_UN) -> usize { + // Work with an actual instance of the type since using a null pointer is UB + let base = addr as *const _ as usize; + let path = &addr.sun_path as *const _ as usize; + path - base +} diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 29555d0179a41448131aecad8ebea610f2321c1d..19e64adb2d6e412e25f49170109af1596273ea34 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -33,7 +33,7 @@ use http_client::HttpClient; use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind}; use node_runtime::NodeRuntime; -use remote::SshRemoteClient; +use remote::{SshRemoteClient, ssh_session::SshArgs}; use rpc::{ AnyProtoClient, TypedEnvelope, proto::{self}, @@ -253,11 +253,16 @@ impl DapStore { cx.spawn(async move |_, cx| { let response = request.await?; let binary = DebugAdapterBinary::from_proto(response)?; - let mut ssh_command = ssh_client.read_with(cx, |ssh, _| { - anyhow::Ok(SshCommand { - arguments: ssh.ssh_args().context("SSH arguments not found")?, - }) - })??; + let (mut ssh_command, envs, path_style) = + ssh_client.read_with(cx, |ssh, _| { + let (SshArgs { arguments, envs }, path_style) = + ssh.ssh_info().context("SSH arguments not found")?; + anyhow::Ok(( + SshCommand { arguments }, + envs.unwrap_or_default(), + path_style, + )) + })??; let mut connection = None; if let Some(c) = binary.connection { @@ -282,12 +287,13 @@ impl DapStore { binary.cwd.as_deref(), binary.envs, None, + path_style, ); Ok(DebugAdapterBinary { command: Some(program), arguments: args, - envs: HashMap::default(), + envs, cwd: None, connection, request_args: binary.request_args, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c7a1f057615c0e75414389935dbbabab9bc7155d..8e1026421e984b77655d451a2b80f1fe1299ebfb 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -117,7 +117,7 @@ use text::{Anchor, BufferId, Point}; use toolchain_store::EmptyToolchainStore; use util::{ ResultExt as _, - paths::{SanitizedPath, compare_paths}, + paths::{PathStyle, RemotePathBuf, SanitizedPath, compare_paths}, }; use worktree::{CreatedEntry, Snapshot, Traversal}; pub use worktree::{ @@ -1159,9 +1159,11 @@ impl Project { let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx); - let ssh_proto = ssh.read(cx).proto_client(); - let worktree_store = - cx.new(|_| WorktreeStore::remote(false, ssh_proto.clone(), SSH_PROJECT_ID)); + let (ssh_proto, path_style) = + ssh.read_with(cx, |ssh, _| (ssh.proto_client(), ssh.path_style())); + let worktree_store = cx.new(|_| { + WorktreeStore::remote(false, ssh_proto.clone(), SSH_PROJECT_ID, path_style) + }); cx.subscribe(&worktree_store, Self::on_worktree_store_event) .detach(); @@ -1410,8 +1412,15 @@ impl Project { let remote_id = response.payload.project_id; let role = response.payload.role(); + // todo(zjk) + // Set the proper path style based on the remote let worktree_store = cx.new(|_| { - WorktreeStore::remote(true, client.clone().into(), response.payload.project_id) + WorktreeStore::remote( + true, + client.clone().into(), + response.payload.project_id, + PathStyle::Posix, + ) })?; let buffer_store = cx.new(|cx| { BufferStore::remote(worktree_store.clone(), client.clone().into(), remote_id, cx) @@ -4039,7 +4048,8 @@ impl Project { }) }) } else if let Some(ssh_client) = self.ssh_client.as_ref() { - let request_path = Path::new(path); + let path_style = ssh_client.read(cx).path_style(); + let request_path = RemotePathBuf::from_str(path, path_style); let request = ssh_client .read(cx) .proto_client() diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index b067396881d3b1bc0c20d8b0f21cb5ea80b675f9..385fdf9082baaf86bcdb547841cc98d161c5c508 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -4,6 +4,7 @@ use collections::HashMap; use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, Task, WeakEntity}; use itertools::Itertools; use language::LanguageName; +use remote::ssh_session::SshArgs; use settings::{Settings, SettingsLocation}; use smol::channel::bounded; use std::{ @@ -17,7 +18,10 @@ use terminal::{ TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::{self, TerminalSettings, VenvSettings}, }; -use util::ResultExt; +use util::{ + ResultExt, + paths::{PathStyle, RemotePathBuf}, +}; pub struct Terminals { pub(crate) local_handles: Vec>, @@ -47,6 +51,13 @@ impl SshCommand { } } +pub struct SshDetails { + pub host: String, + pub ssh_command: SshCommand, + pub envs: Option>, + pub path_style: PathStyle, +} + impl Project { pub fn active_project_directory(&self, cx: &App) -> Option> { let worktree = self @@ -68,14 +79,16 @@ impl Project { } } - pub fn ssh_details(&self, cx: &App) -> Option<(String, SshCommand)> { + pub fn ssh_details(&self, cx: &App) -> Option { if let Some(ssh_client) = &self.ssh_client { let ssh_client = ssh_client.read(cx); - if let Some(args) = ssh_client.ssh_args() { - return Some(( - ssh_client.connection_options().host.clone(), - SshCommand { arguments: args }, - )); + if let Some((SshArgs { arguments, envs }, path_style)) = ssh_client.ssh_info() { + return Some(SshDetails { + host: ssh_client.connection_options().host.clone(), + ssh_command: SshCommand { arguments }, + envs, + path_style, + }); } } @@ -158,17 +171,26 @@ impl Project { .unwrap_or_default(); env.extend(settings.env.clone()); - match &self.ssh_details(cx) { - Some((_, ssh_command)) => { + match self.ssh_details(cx) { + Some(SshDetails { + ssh_command, + envs, + path_style, + .. + }) => { let (command, args) = wrap_for_ssh( - ssh_command, + &ssh_command, Some((&command, &args)), path.as_deref(), env, None, + path_style, ); let mut command = std::process::Command::new(command); command.args(args); + if let Some(envs) = envs { + command.envs(envs); + } command } None => { @@ -202,6 +224,7 @@ impl Project { } }; let ssh_details = this.ssh_details(cx); + let is_ssh_terminal = ssh_details.is_some(); let mut settings_location = None; if let Some(path) = path.as_ref() { @@ -226,11 +249,7 @@ impl Project { // precedence. env.extend(settings.env.clone()); - let local_path = if ssh_details.is_none() { - path.clone() - } else { - None - }; + let local_path = if is_ssh_terminal { None } else { path.clone() }; let mut python_venv_activate_command = None; @@ -241,8 +260,13 @@ impl Project { this.python_activate_command(python_venv_directory, &settings.detect_venv); } - match &ssh_details { - Some((host, ssh_command)) => { + match ssh_details { + Some(SshDetails { + host, + ssh_command, + envs, + path_style, + }) => { log::debug!("Connecting to a remote server: {ssh_command:?}"); // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed @@ -252,9 +276,18 @@ impl Project { env.entry("TERM".to_string()) .or_insert_with(|| "xterm-256color".to_string()); - let (program, args) = - wrap_for_ssh(&ssh_command, None, path.as_deref(), env, None); + let (program, args) = wrap_for_ssh( + &ssh_command, + None, + path.as_deref(), + env, + None, + path_style, + ); env = HashMap::default(); + if let Some(envs) = envs { + env.extend(envs); + } ( Option::::None, Shell::WithArguments { @@ -290,8 +323,13 @@ impl Project { ); } - match &ssh_details { - Some((host, ssh_command)) => { + match ssh_details { + Some(SshDetails { + host, + ssh_command, + envs, + path_style, + }) => { log::debug!("Connecting to a remote server: {ssh_command:?}"); env.entry("TERM".to_string()) .or_insert_with(|| "xterm-256color".to_string()); @@ -304,8 +342,12 @@ impl Project { path.as_deref(), env, python_venv_directory.as_deref(), + path_style, ); env = HashMap::default(); + if let Some(envs) = envs { + env.extend(envs); + } ( task_state, Shell::WithArguments { @@ -343,7 +385,7 @@ impl Project { settings.cursor_shape.unwrap_or_default(), settings.alternate_scroll, settings.max_scroll_history_lines, - ssh_details.is_some(), + is_ssh_terminal, window, completion_tx, cx, @@ -533,6 +575,7 @@ pub fn wrap_for_ssh( path: Option<&Path>, env: HashMap, venv_directory: Option<&Path>, + path_style: PathStyle, ) -> (String, Vec) { let to_run = if let Some((command, args)) = command { // DEFAULT_REMOTE_SHELL is '"${SHELL:-sh}"' so must not be escaped @@ -555,24 +598,25 @@ pub fn wrap_for_ssh( } if let Some(venv_directory) = venv_directory { if let Ok(str) = shlex::try_quote(venv_directory.to_string_lossy().as_ref()) { - env_changes.push_str(&format!("PATH={}:$PATH ", str)); + let path = RemotePathBuf::new(PathBuf::from(str.to_string()), path_style).to_string(); + env_changes.push_str(&format!("PATH={}:$PATH ", path)); } } let commands = if let Some(path) = path { - let path_string = path.to_string_lossy().to_string(); + let path = RemotePathBuf::new(path.to_path_buf(), path_style).to_string(); // shlex will wrap the command in single quotes (''), disabling ~ expansion, // replace ith with something that works let tilde_prefix = "~/"; if path.starts_with(tilde_prefix) { - let trimmed_path = path_string + let trimmed_path = path .trim_start_matches("/") .trim_start_matches("~") .trim_start_matches("/"); format!("cd \"$HOME/{trimmed_path}\"; {env_changes} {to_run}") } else { - format!("cd {path:?}; {env_changes} {to_run}") + format!("cd {path}; {env_changes} {to_run}") } } else { format!("cd; {env_changes} {to_run}") diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index 48ef3bda6f9e051868f1fd968dc52751af4ccd00..16e42e90cbbe654b0c2a22eb9d7cbee7ad902abd 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -25,7 +25,10 @@ use smol::{ stream::StreamExt, }; use text::ReplicaId; -use util::{ResultExt, paths::SanitizedPath}; +use util::{ + ResultExt, + paths::{PathStyle, RemotePathBuf, SanitizedPath}, +}; use worktree::{ Entry, ProjectEntryId, UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings, @@ -46,6 +49,7 @@ enum WorktreeStoreState { Remote { upstream_client: AnyProtoClient, upstream_project_id: u64, + path_style: PathStyle, }, } @@ -100,6 +104,7 @@ impl WorktreeStore { retain_worktrees: bool, upstream_client: AnyProtoClient, upstream_project_id: u64, + path_style: PathStyle, ) -> Self { Self { next_entry_id: Default::default(), @@ -111,6 +116,7 @@ impl WorktreeStore { state: WorktreeStoreState::Remote { upstream_client, upstream_project_id, + path_style, }, } } @@ -214,17 +220,16 @@ impl WorktreeStore { if !self.loading_worktrees.contains_key(&abs_path) { let task = match &self.state { WorktreeStoreState::Remote { - upstream_client, .. + upstream_client, + path_style, + .. } => { if upstream_client.is_via_collab() { Task::ready(Err(Arc::new(anyhow!("cannot create worktrees via collab")))) } else { - self.create_ssh_worktree( - upstream_client.clone(), - abs_path.clone(), - visible, - cx, - ) + let abs_path = + RemotePathBuf::new(abs_path.as_path().to_path_buf(), *path_style); + self.create_ssh_worktree(upstream_client.clone(), abs_path, visible, cx) } } WorktreeStoreState::Local { fs } => { @@ -250,11 +255,12 @@ impl WorktreeStore { fn create_ssh_worktree( &mut self, client: AnyProtoClient, - abs_path: impl Into, + abs_path: RemotePathBuf, visible: bool, cx: &mut Context, ) -> Task, Arc>> { - let mut abs_path = Into::::into(abs_path).to_string(); + let path_style = abs_path.path_style(); + let mut abs_path = abs_path.to_string(); // If we start with `/~` that means the ssh path was something like `ssh://user@host/~/home-dir-folder/` // in which case want to strip the leading the `/`. // On the host-side, the `~` will get expanded. @@ -265,10 +271,11 @@ impl WorktreeStore { if abs_path.is_empty() { abs_path = "~/".to_string(); } + cx.spawn(async move |this, cx| { let this = this.upgrade().context("Dropped worktree store")?; - let path = Path::new(abs_path.as_str()); + let path = RemotePathBuf::new(abs_path.into(), path_style); let response = client .request(proto::AddWorktree { project_id: SSH_PROJECT_ID, diff --git a/crates/proto/Cargo.toml b/crates/proto/Cargo.toml index 52554636009a10cb5e6c0a6e02ae7fcd046b104e..6cae4394bdb9fa13ab31b42f7ee4031ef5449d4b 100644 --- a/crates/proto/Cargo.toml +++ b/crates/proto/Cargo.toml @@ -27,3 +27,4 @@ prost-build.workspace = true [dev-dependencies] collections = { workspace = true, features = ["test-support"] } +typed-path = "0.11" diff --git a/crates/proto/src/typed_envelope.rs b/crates/proto/src/typed_envelope.rs index a4d0a9bf858e5f1a03a17b5aa1f29aa9d58bc2d0..381a6379dc95b9f96025315c357fecfe5b8fc937 100644 --- a/crates/proto/src/typed_envelope.rs +++ b/crates/proto/src/typed_envelope.rs @@ -127,51 +127,46 @@ pub trait ToProto { fn to_proto(self) -> String; } -impl FromProto for PathBuf { +#[inline] +fn from_proto_path(proto: String) -> PathBuf { #[cfg(target_os = "windows")] - fn from_proto(proto: String) -> Self { - proto.split("/").collect() - } + let proto = proto.replace('/', "\\"); + + PathBuf::from(proto) +} + +#[inline] +fn to_proto_path(path: &Path) -> String { + #[cfg(target_os = "windows")] + let proto = path.to_string_lossy().replace('\\', "/"); #[cfg(not(target_os = "windows"))] + let proto = path.to_string_lossy().to_string(); + + proto +} + +impl FromProto for PathBuf { fn from_proto(proto: String) -> Self { - PathBuf::from(proto) + from_proto_path(proto) } } impl FromProto for Arc { fn from_proto(proto: String) -> Self { - PathBuf::from_proto(proto).into() + from_proto_path(proto).into() } } impl ToProto for PathBuf { - #[cfg(target_os = "windows")] - fn to_proto(self) -> String { - self.components() - .map(|comp| comp.as_os_str().to_string_lossy().to_string()) - .collect::>() - .join("/") - } - - #[cfg(not(target_os = "windows"))] fn to_proto(self) -> String { - self.to_string_lossy().to_string() + to_proto_path(&self) } } impl ToProto for &Path { - #[cfg(target_os = "windows")] fn to_proto(self) -> String { - self.components() - .map(|comp| comp.as_os_str().to_string_lossy().to_string()) - .collect::>() - .join("/") - } - - #[cfg(not(target_os = "windows"))] - fn to_proto(self) -> String { - self.to_string_lossy().to_string() + to_proto_path(self) } } @@ -214,3 +209,103 @@ impl TypedEnvelope { } } } + +#[cfg(test)] +mod tests { + use typed_path::{UnixPath, UnixPathBuf, WindowsPath, WindowsPathBuf}; + + fn windows_path_from_proto(proto: String) -> WindowsPathBuf { + let proto = proto.replace('/', "\\"); + WindowsPathBuf::from(proto) + } + + fn unix_path_from_proto(proto: String) -> UnixPathBuf { + UnixPathBuf::from(proto) + } + + fn windows_path_to_proto(path: &WindowsPath) -> String { + path.to_string_lossy().replace('\\', "/") + } + + fn unix_path_to_proto(path: &UnixPath) -> String { + path.to_string_lossy().to_string() + } + + #[test] + fn test_path_proto_interop() { + const WINDOWS_PATHS: &[&str] = &[ + "C:\\Users\\User\\Documents\\file.txt", + "C:/Program Files/App/app.exe", + "projects\\zed\\crates\\proto\\src\\typed_envelope.rs", + "projects/my project/src/main.rs", + ]; + const UNIX_PATHS: &[&str] = &[ + "/home/user/documents/file.txt", + "/usr/local/bin/my app/app", + "projects/zed/crates/proto/src/typed_envelope.rs", + "projects/my project/src/main.rs", + ]; + + // Windows path to proto and back + for &windows_path_str in WINDOWS_PATHS { + let windows_path = WindowsPathBuf::from(windows_path_str); + let proto = windows_path_to_proto(&windows_path); + let recovered_path = windows_path_from_proto(proto); + assert_eq!(windows_path, recovered_path); + assert_eq!( + recovered_path.to_string_lossy(), + windows_path_str.replace('/', "\\") + ); + } + // Unix path to proto and back + for &unix_path_str in UNIX_PATHS { + let unix_path = UnixPathBuf::from(unix_path_str); + let proto = unix_path_to_proto(&unix_path); + let recovered_path = unix_path_from_proto(proto); + assert_eq!(unix_path, recovered_path); + assert_eq!(recovered_path.to_string_lossy(), unix_path_str); + } + // Windows host, Unix client, host sends Windows path to client + for &windows_path_str in WINDOWS_PATHS { + let windows_host_path = WindowsPathBuf::from(windows_path_str); + let proto = windows_path_to_proto(&windows_host_path); + let unix_client_received_path = unix_path_from_proto(proto); + let proto = unix_path_to_proto(&unix_client_received_path); + let windows_host_recovered_path = windows_path_from_proto(proto); + assert_eq!(windows_host_path, windows_host_recovered_path); + assert_eq!( + windows_host_recovered_path.to_string_lossy(), + windows_path_str.replace('/', "\\") + ); + } + // Unix host, Windows client, host sends Unix path to client + for &unix_path_str in UNIX_PATHS { + let unix_host_path = UnixPathBuf::from(unix_path_str); + let proto = unix_path_to_proto(&unix_host_path); + let windows_client_received_path = windows_path_from_proto(proto); + let proto = windows_path_to_proto(&windows_client_received_path); + let unix_host_recovered_path = unix_path_from_proto(proto); + assert_eq!(unix_host_path, unix_host_recovered_path); + assert_eq!(unix_host_recovered_path.to_string_lossy(), unix_path_str); + } + } + + // todo(zjk) + #[test] + fn test_unsolved_case() { + // Unix host, Windows client + // The Windows client receives a Unix path with backslashes in it, then + // sends it back to the host. + // This currently fails. + let unix_path = UnixPathBuf::from("/home/user/projects/my\\project/src/main.rs"); + let proto = unix_path_to_proto(&unix_path); + let windows_client_received_path = windows_path_from_proto(proto); + let proto = windows_path_to_proto(&windows_client_received_path); + let unix_host_recovered_path = unix_path_from_proto(proto); + assert_ne!(unix_path, unix_host_recovered_path); + assert_eq!( + unix_host_recovered_path.to_string_lossy(), + "/home/user/projects/my/project/src/main.rs" + ); + } +} diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 134f728680b15f4babdcf20d8ce5c23cb8ef0720..aa5103e62ba4d28f150287d5716fa83a3b17260d 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -28,9 +28,8 @@ use paths::user_ssh_config_file; use picker::Picker; use project::Fs; use project::Project; -use remote::SshConnectionOptions; -use remote::SshRemoteClient; use remote::ssh_session::ConnectionIdentifier; +use remote::{SshConnectionOptions, SshRemoteClient}; use settings::Settings; use settings::SettingsStore; use settings::update_settings_file; @@ -42,7 +41,10 @@ use ui::{ IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Scrollbar, ScrollbarState, Section, Tooltip, prelude::*, }; -use util::ResultExt; +use util::{ + ResultExt, + paths::{PathStyle, RemotePathBuf}, +}; use workspace::OpenOptions; use workspace::Toast; use workspace::notifications::NotificationId; @@ -142,20 +144,21 @@ impl ProjectPicker { ix: usize, connection: SshConnectionOptions, project: Entity, - home_dir: PathBuf, + home_dir: RemotePathBuf, + path_style: PathStyle, workspace: WeakEntity, window: &mut Window, cx: &mut Context, ) -> Entity { let (tx, rx) = oneshot::channel(); let lister = project::DirectoryLister::Project(project.clone()); - let delegate = file_finder::OpenPathDelegate::new(tx, lister, false); + let delegate = file_finder::OpenPathDelegate::new(tx, lister, false, path_style); let picker = cx.new(|cx| { let picker = Picker::uniform_list(delegate, window, cx) .width(rems(34.)) .modal(false); - picker.set_query(home_dir.to_string_lossy().to_string(), window, cx); + picker.set_query(home_dir.to_string(), window, cx); picker }); let connection_string = connection.connection_string().into(); @@ -422,7 +425,8 @@ impl RemoteServerProjects { ix: usize, connection_options: remote::SshConnectionOptions, project: Entity, - home_dir: PathBuf, + home_dir: RemotePathBuf, + path_style: PathStyle, window: &mut Window, cx: &mut Context, workspace: WeakEntity, @@ -435,6 +439,7 @@ impl RemoteServerProjects { connection_options, project, home_dir, + path_style, workspace, window, cx, @@ -589,15 +594,18 @@ impl RemoteServerProjects { }); }; - let project = cx.update(|_, cx| { - project::Project::ssh( - session, - app_state.client.clone(), - app_state.node_runtime.clone(), - app_state.user_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - cx, + let (path_style, project) = cx.update(|_, cx| { + ( + session.read(cx).path_style(), + project::Project::ssh( + session, + app_state.client.clone(), + app_state.node_runtime.clone(), + app_state.user_store.clone(), + app_state.languages.clone(), + app_state.fs.clone(), + cx, + ), ) })?; @@ -605,7 +613,13 @@ impl RemoteServerProjects { .read_with(cx, |project, cx| project.resolve_abs_path("~", cx))? .await .and_then(|path| path.into_abs_path()) - .unwrap_or(PathBuf::from("/")); + .map(|path| RemotePathBuf::new(path, path_style)) + .unwrap_or_else(|| match path_style { + PathStyle::Posix => RemotePathBuf::from_str("/", PathStyle::Posix), + PathStyle::Windows => { + RemotePathBuf::from_str("C:\\", PathStyle::Windows) + } + }); workspace .update_in(cx, |workspace, window, cx| { @@ -617,6 +631,7 @@ impl RemoteServerProjects { connection_options, project, home_dir, + path_style, window, cx, weak, diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index e01f4cfb0462baef01656755ebdd1abdcdd56d2c..2653a19bd9aa888c701903aa14f846d6a5b0d446 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -49,7 +49,10 @@ use std::{ time::{Duration, Instant}, }; use tempfile::TempDir; -use util::ResultExt; +use util::{ + ResultExt, + paths::{PathStyle, RemotePathBuf}, +}; #[derive( Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, @@ -59,7 +62,10 @@ pub struct SshProjectId(pub u64); #[derive(Clone)] pub struct SshSocket { connection_options: SshConnectionOptions, + #[cfg(not(target_os = "windows"))] socket_path: PathBuf, + #[cfg(target_os = "windows")] + envs: HashMap, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)] @@ -85,6 +91,11 @@ pub struct SshConnectionOptions { pub upload_binary_over_ssh: bool, } +pub struct SshArgs { + pub arguments: Vec, + pub envs: Option>, +} + #[macro_export] macro_rules! shell_script { ($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{ @@ -338,6 +349,28 @@ pub trait SshClientDelegate: Send + Sync { } impl SshSocket { + #[cfg(not(target_os = "windows"))] + fn new(options: SshConnectionOptions, socket_path: PathBuf) -> Result { + Ok(Self { + connection_options: options, + socket_path, + }) + } + + #[cfg(target_os = "windows")] + fn new(options: SshConnectionOptions, temp_dir: &TempDir, secret: String) -> Result { + let askpass_script = temp_dir.path().join("askpass.bat"); + std::fs::write(&askpass_script, "@ECHO OFF\necho %ZED_SSH_ASKPASS%")?; + let mut envs = HashMap::default(); + envs.insert("SSH_ASKPASS_REQUIRE".into(), "force".into()); + envs.insert("SSH_ASKPASS".into(), askpass_script.display().to_string()); + envs.insert("ZED_SSH_ASKPASS".into(), secret); + Ok(Self { + connection_options: options, + envs, + }) + } + // :WARNING: ssh unquotes arguments when executing on the remote :WARNING: // e.g. $ ssh host sh -c 'ls -l' is equivalent to $ ssh host sh -c ls -l // and passes -l as an argument to sh, not to ls. @@ -375,6 +408,7 @@ impl SshSocket { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } + #[cfg(not(target_os = "windows"))] fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command { command .stdin(Stdio::piped()) @@ -384,14 +418,68 @@ impl SshSocket { .arg(format!("ControlPath={}", self.socket_path.display())) } - fn ssh_args(&self) -> Vec { - vec![ - "-o".to_string(), - "ControlMaster=no".to_string(), - "-o".to_string(), - format!("ControlPath={}", self.socket_path.display()), - self.connection_options.ssh_url(), - ] + #[cfg(target_os = "windows")] + fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command { + command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .envs(self.envs.clone()) + } + + // On Windows, we need to use `SSH_ASKPASS` to provide the password to ssh. + // On Linux, we use the `ControlPath` option to create a socket file that ssh can use to + #[cfg(not(target_os = "windows"))] + fn ssh_args(&self) -> SshArgs { + SshArgs { + arguments: vec![ + "-o".to_string(), + "ControlMaster=no".to_string(), + "-o".to_string(), + format!("ControlPath={}", self.socket_path.display()), + self.connection_options.ssh_url(), + ], + envs: None, + } + } + + #[cfg(target_os = "windows")] + fn ssh_args(&self) -> SshArgs { + SshArgs { + arguments: vec![self.connection_options.ssh_url()], + envs: Some(self.envs.clone()), + } + } + + async fn platform(&self) -> Result { + let uname = self.run_command("sh", &["-c", "uname -sm"]).await?; + let Some((os, arch)) = uname.split_once(" ") else { + anyhow::bail!("unknown uname: {uname:?}") + }; + + let os = match os.trim() { + "Darwin" => "macos", + "Linux" => "linux", + _ => anyhow::bail!( + "Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development" + ), + }; + // exclude armv5,6,7 as they are 32-bit. + let arch = if arch.starts_with("armv8") + || arch.starts_with("armv9") + || arch.starts_with("arm64") + || arch.starts_with("aarch64") + { + "aarch64" + } else if arch.starts_with("x86") { + "x86_64" + } else { + anyhow::bail!( + "Prebuilt remote servers are not yet available for {arch:?}. See https://zed.dev/docs/remote-development" + ) + }; + + Ok(SshPlatform { os, arch }) } } @@ -560,6 +648,7 @@ pub struct SshRemoteClient { client: Arc, unique_identifier: String, connection_options: SshConnectionOptions, + path_style: PathStyle, state: Arc>>, } @@ -620,22 +709,25 @@ impl SshRemoteClient { let client = cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "client"))?; - let this = cx.new(|_| Self { - client: client.clone(), - unique_identifier: unique_identifier.clone(), - connection_options: connection_options.clone(), - state: Arc::new(Mutex::new(Some(State::Connecting))), - })?; let ssh_connection = cx .update(|cx| { cx.update_default_global(|pool: &mut ConnectionPool, cx| { - pool.connect(connection_options, &delegate, cx) + pool.connect(connection_options.clone(), &delegate, cx) }) })? .await .map_err(|e| e.cloned())?; + let path_style = ssh_connection.path_style(); + let this = cx.new(|_| Self { + client: client.clone(), + unique_identifier: unique_identifier.clone(), + connection_options, + path_style, + state: Arc::new(Mutex::new(Some(State::Connecting))), + })?; + let io_task = ssh_connection.start_proxy( unique_identifier, false, @@ -1065,18 +1157,18 @@ impl SshRemoteClient { self.client.subscribe_to_entity(remote_id, entity); } - pub fn ssh_args(&self) -> Option> { + pub fn ssh_info(&self) -> Option<(SshArgs, PathStyle)> { self.state .lock() .as_ref() .and_then(|state| state.ssh_connection()) - .map(|ssh_connection| ssh_connection.ssh_args()) + .map(|ssh_connection| (ssh_connection.ssh_args(), ssh_connection.path_style())) } pub fn upload_directory( &self, src_path: PathBuf, - dest_path: PathBuf, + dest_path: RemotePathBuf, cx: &App, ) -> Task> { let state = self.state.lock(); @@ -1110,6 +1202,10 @@ impl SshRemoteClient { self.connection_state() == ConnectionState::Disconnected } + pub fn path_style(&self) -> PathStyle { + self.path_style + } + #[cfg(any(test, feature = "test-support"))] pub fn simulate_disconnect(&self, client_cx: &mut App) -> Task<()> { let opts = self.connection_options(); @@ -1288,12 +1384,19 @@ trait RemoteConnection: Send + Sync { delegate: Arc, cx: &mut AsyncApp, ) -> Task>; - fn upload_directory(&self, src_path: PathBuf, dest_path: PathBuf, cx: &App) - -> Task>; + fn upload_directory( + &self, + src_path: PathBuf, + dest_path: RemotePathBuf, + cx: &App, + ) -> Task>; async fn kill(&self) -> Result<()>; fn has_been_killed(&self) -> bool; - fn ssh_args(&self) -> Vec; + /// On Windows, we need to use `SSH_ASKPASS` to provide the password to ssh. + /// On Linux, we use the `ControlPath` option to create a socket file that ssh can use to + fn ssh_args(&self) -> SshArgs; fn connection_options(&self) -> SshConnectionOptions; + fn path_style(&self) -> PathStyle; #[cfg(any(test, feature = "test-support"))] fn simulate_disconnect(&self, _: &AsyncApp) {} @@ -1302,7 +1405,9 @@ trait RemoteConnection: Send + Sync { struct SshRemoteConnection { socket: SshSocket, master_process: Mutex>, - remote_binary_path: Option, + remote_binary_path: Option, + ssh_platform: SshPlatform, + ssh_path_style: PathStyle, _temp_dir: TempDir, } @@ -1321,7 +1426,7 @@ impl RemoteConnection for SshRemoteConnection { self.master_process.lock().is_none() } - fn ssh_args(&self) -> Vec { + fn ssh_args(&self) -> SshArgs { self.socket.ssh_args() } @@ -1332,7 +1437,7 @@ impl RemoteConnection for SshRemoteConnection { fn upload_directory( &self, src_path: PathBuf, - dest_path: PathBuf, + dest_path: RemotePathBuf, cx: &App, ) -> Task> { let mut command = util::command::new_smol_command("scp"); @@ -1352,7 +1457,7 @@ impl RemoteConnection for SshRemoteConnection { .arg(format!( "{}:{}", self.socket.connection_options.scp_url(), - dest_path.display() + dest_path.to_string() )) .output(); @@ -1363,7 +1468,7 @@ impl RemoteConnection for SshRemoteConnection { output.status.success(), "failed to upload directory {} -> {}: {}", src_path.display(), - dest_path.display(), + dest_path.to_string(), String::from_utf8_lossy(&output.stderr) ); @@ -1389,7 +1494,7 @@ impl RemoteConnection for SshRemoteConnection { let mut start_proxy_command = shell_script!( "exec {binary_path} proxy --identifier {identifier}", - binary_path = &remote_binary_path.to_string_lossy(), + binary_path = &remote_binary_path.to_string(), identifier = &unique_identifier, ); @@ -1432,19 +1537,13 @@ impl RemoteConnection for SshRemoteConnection { &cx, ) } -} -impl SshRemoteConnection { - #[cfg(not(unix))] - async fn new( - _connection_options: SshConnectionOptions, - _delegate: Arc, - _cx: &mut AsyncApp, - ) -> Result { - anyhow::bail!("ssh is not supported on this platform"); + fn path_style(&self) -> PathStyle { + self.ssh_path_style } +} - #[cfg(unix)] +impl SshRemoteConnection { async fn new( connection_options: SshConnectionOptions, delegate: Arc, @@ -1470,27 +1569,38 @@ impl SshRemoteConnection { // Start the master SSH process, which does not do anything except for establish // the connection and keep it open, allowing other ssh commands to reuse it // via a control socket. + #[cfg(not(target_os = "windows"))] let socket_path = temp_dir.path().join("ssh.sock"); - let mut master_process = process::Command::new("ssh") - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .env("SSH_ASKPASS_REQUIRE", "force") - .env("SSH_ASKPASS", &askpass.script_path()) - .args(connection_options.additional_args()) - .args([ + let mut master_process = { + #[cfg(not(target_os = "windows"))] + let args = [ "-N", "-o", "ControlPersist=no", "-o", "ControlMaster=yes", "-o", - ]) - .arg(format!("ControlPath={}", socket_path.display())) - .arg(&url) - .kill_on_drop(true) - .spawn()?; + ]; + // On Windows, `ControlMaster` and `ControlPath` are not supported: + // https://github.com/PowerShell/Win32-OpenSSH/issues/405 + // https://github.com/PowerShell/Win32-OpenSSH/wiki/Project-Scope + #[cfg(target_os = "windows")] + let args = ["-N"]; + let mut master_process = process::Command::new("ssh"); + master_process + .kill_on_drop(true) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .env("SSH_ASKPASS_REQUIRE", "force") + .env("SSH_ASKPASS", askpass.script_path()) + .args(connection_options.additional_args()) + .args(args); + #[cfg(not(target_os = "windows"))] + master_process.arg(format!("ControlPath={}", socket_path.display())); + master_process.arg(&url).spawn()? + }; // Wait for this ssh process to close its stdout, indicating that authentication // has completed. let mut stdout = master_process.stdout.take().unwrap(); @@ -1529,11 +1639,16 @@ impl SshRemoteConnection { anyhow::bail!(error_message); } + #[cfg(not(target_os = "windows"))] + let socket = SshSocket::new(connection_options, socket_path)?; + #[cfg(target_os = "windows")] + let socket = SshSocket::new(connection_options, &temp_dir, askpass.get_password())?; drop(askpass); - let socket = SshSocket { - connection_options, - socket_path, + let ssh_platform = socket.platform().await?; + let ssh_path_style = match ssh_platform.os { + "windows" => PathStyle::Windows, + _ => PathStyle::Posix, }; let mut this = Self { @@ -1541,6 +1656,8 @@ impl SshRemoteConnection { master_process: Mutex::new(Some(master_process)), _temp_dir: temp_dir, remote_binary_path: None, + ssh_path_style, + ssh_platform, }; let (release_channel, version, commit) = cx.update(|cx| { @@ -1558,37 +1675,6 @@ impl SshRemoteConnection { Ok(this) } - async fn platform(&self) -> Result { - let uname = self.socket.run_command("sh", &["-c", "uname -sm"]).await?; - let Some((os, arch)) = uname.split_once(" ") else { - anyhow::bail!("unknown uname: {uname:?}") - }; - - let os = match os.trim() { - "Darwin" => "macos", - "Linux" => "linux", - _ => anyhow::bail!( - "Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development" - ), - }; - // exclude armv5,6,7 as they are 32-bit. - let arch = if arch.starts_with("armv8") - || arch.starts_with("armv9") - || arch.starts_with("arm64") - || arch.starts_with("aarch64") - { - "aarch64" - } else if arch.starts_with("x86") { - "x86_64" - } else { - anyhow::bail!( - "Prebuilt remote servers are not yet available for {arch:?}. See https://zed.dev/docs/remote-development" - ) - }; - - Ok(SshPlatform { os, arch }) - } - fn multiplex( mut ssh_proxy_process: Child, incoming_tx: UnboundedSender, @@ -1699,11 +1785,10 @@ impl SshRemoteConnection { version: SemanticVersion, commit: Option, cx: &mut AsyncApp, - ) -> Result { + ) -> Result { let version_str = match release_channel { ReleaseChannel::Nightly => { let commit = commit.map(|s| s.full()).unwrap_or_default(); - format!("{}-{}", version, commit) } ReleaseChannel::Dev => "build".to_string(), @@ -1714,19 +1799,23 @@ impl SshRemoteConnection { release_channel.dev_name(), version_str ); - let dst_path = paths::remote_server_dir_relative().join(binary_name); + let dst_path = RemotePathBuf::new( + paths::remote_server_dir_relative().join(binary_name), + self.ssh_path_style, + ); let build_remote_server = std::env::var("ZED_BUILD_REMOTE_SERVER").ok(); #[cfg(debug_assertions)] if let Some(build_remote_server) = build_remote_server { - let src_path = self - .build_local(build_remote_server, self.platform().await?, delegate, cx) - .await?; - let tmp_path = paths::remote_server_dir_relative().join(format!( - "download-{}-{}", - std::process::id(), - src_path.file_name().unwrap().to_string_lossy() - )); + let src_path = self.build_local(build_remote_server, delegate, cx).await?; + let tmp_path = RemotePathBuf::new( + paths::remote_server_dir_relative().join(format!( + "download-{}-{}", + std::process::id(), + src_path.file_name().unwrap().to_string_lossy() + )), + self.ssh_path_style, + ); self.upload_local_server_binary(&src_path, &tmp_path, delegate, cx) .await?; self.extract_server_binary(&dst_path, &tmp_path, delegate, cx) @@ -1736,7 +1825,7 @@ impl SshRemoteConnection { if self .socket - .run_command(&dst_path.to_string_lossy(), &["version"]) + .run_command(&dst_path.to_string(), &["version"]) .await .is_ok() { @@ -1754,16 +1843,17 @@ impl SshRemoteConnection { _ => Ok(Some(AppVersion::global(cx))), })??; - let platform = self.platform().await?; - - let tmp_path_gz = PathBuf::from(format!( - "{}-download-{}.gz", - dst_path.to_string_lossy(), - std::process::id() - )); + let tmp_path_gz = RemotePathBuf::new( + PathBuf::from(format!( + "{}-download-{}.gz", + dst_path.to_string(), + std::process::id() + )), + self.ssh_path_style, + ); if !self.socket.connection_options.upload_binary_over_ssh { if let Some((url, body)) = delegate - .get_download_params(platform, release_channel, wanted_version, cx) + .get_download_params(self.ssh_platform, release_channel, wanted_version, cx) .await? { match self @@ -1786,7 +1876,7 @@ impl SshRemoteConnection { } let src_path = delegate - .download_server_binary_locally(platform, release_channel, wanted_version, cx) + .download_server_binary_locally(self.ssh_platform, release_channel, wanted_version, cx) .await?; self.upload_local_server_binary(&src_path, &tmp_path_gz, delegate, cx) .await?; @@ -1799,7 +1889,7 @@ impl SshRemoteConnection { &self, url: &str, body: &str, - tmp_path_gz: &Path, + tmp_path_gz: &RemotePathBuf, delegate: &Arc, cx: &mut AsyncApp, ) -> Result<()> { @@ -1809,10 +1899,7 @@ impl SshRemoteConnection { "sh", &[ "-c", - &shell_script!( - "mkdir -p {parent}", - parent = parent.to_string_lossy().as_ref() - ), + &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), ], ) .await?; @@ -1835,7 +1922,7 @@ impl SshRemoteConnection { &body, &url, "-o", - &tmp_path_gz.to_string_lossy(), + &tmp_path_gz.to_string(), ], ) .await @@ -1857,7 +1944,7 @@ impl SshRemoteConnection { &body, &url, "-O", - &tmp_path_gz.to_string_lossy(), + &tmp_path_gz.to_string(), ], ) .await @@ -1880,7 +1967,7 @@ impl SshRemoteConnection { async fn upload_local_server_binary( &self, src_path: &Path, - tmp_path_gz: &Path, + tmp_path_gz: &RemotePathBuf, delegate: &Arc, cx: &mut AsyncApp, ) -> Result<()> { @@ -1890,10 +1977,7 @@ impl SshRemoteConnection { "sh", &[ "-c", - &shell_script!( - "mkdir -p {parent}", - parent = parent.to_string_lossy().as_ref() - ), + &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), ], ) .await?; @@ -1918,33 +2002,33 @@ impl SshRemoteConnection { async fn extract_server_binary( &self, - dst_path: &Path, - tmp_path: &Path, + dst_path: &RemotePathBuf, + tmp_path: &RemotePathBuf, delegate: &Arc, cx: &mut AsyncApp, ) -> Result<()> { delegate.set_status(Some("Extracting remote development server"), cx); let server_mode = 0o755; - let orig_tmp_path = tmp_path.to_string_lossy(); + let orig_tmp_path = tmp_path.to_string(); let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") { shell_script!( "gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}", server_mode = &format!("{:o}", server_mode), - dst_path = &dst_path.to_string_lossy() + dst_path = &dst_path.to_string(), ) } else { shell_script!( "chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}", server_mode = &format!("{:o}", server_mode), - dst_path = &dst_path.to_string_lossy() + dst_path = &dst_path.to_string() ) }; self.socket.run_command("sh", &["-c", &script]).await?; Ok(()) } - async fn upload_file(&self, src_path: &Path, dest_path: &Path) -> Result<()> { + async fn upload_file(&self, src_path: &Path, dest_path: &RemotePathBuf) -> Result<()> { log::debug!("uploading file {:?} to {:?}", src_path, dest_path); let mut command = util::command::new_smol_command("scp"); let output = self @@ -1961,7 +2045,7 @@ impl SshRemoteConnection { .arg(format!( "{}:{}", self.socket.connection_options.scp_url(), - dest_path.display() + dest_path.to_string() )) .output() .await?; @@ -1970,7 +2054,7 @@ impl SshRemoteConnection { output.status.success(), "failed to upload file {} -> {}: {}", src_path.display(), - dest_path.display(), + dest_path.to_string(), String::from_utf8_lossy(&output.stderr) ); Ok(()) @@ -1980,7 +2064,6 @@ impl SshRemoteConnection { async fn build_local( &self, build_remote_server: String, - platform: SshPlatform, delegate: &Arc, cx: &mut AsyncApp, ) -> Result { @@ -1999,7 +2082,9 @@ impl SshRemoteConnection { Ok(()) } - if platform.arch == std::env::consts::ARCH && platform.os == std::env::consts::OS { + if self.ssh_platform.arch == std::env::consts::ARCH + && self.ssh_platform.os == std::env::consts::OS + { delegate.set_status(Some("Building remote server binary from source"), cx); log::info!("building remote server binary from source"); run_cmd(Command::new("cargo").args([ @@ -2025,12 +2110,15 @@ impl SshRemoteConnection { let path = std::env::current_dir()?.join("target/remote_server/debug/remote_server.gz"); return Ok(path); } - let Some(triple) = platform.triple() else { - anyhow::bail!("can't cross compile for: {:?}", platform); + let Some(triple) = self.ssh_platform.triple() else { + anyhow::bail!("can't cross compile for: {:?}", self.ssh_platform); }; smol::fs::create_dir_all("target/remote_server").await?; if build_remote_server.contains("cross") { + #[cfg(target_os = "windows")] + use util::paths::SanitizedPath; + delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx); log::info!("installing cross"); run_cmd(Command::new("cargo").args([ @@ -2049,6 +2137,13 @@ impl SshRemoteConnection { cx, ); log::info!("building remote server binary from source for {}", &triple); + + // On Windows, the binding needs to be set to the canonical path + #[cfg(target_os = "windows")] + let src = + SanitizedPath::from(smol::fs::canonicalize("./target").await?).to_glob_string(); + #[cfg(not(target_os = "windows"))] + let src = "./target"; run_cmd( Command::new("cross") .args([ @@ -2064,7 +2159,7 @@ impl SshRemoteConnection { ]) .env( "CROSS_CONTAINER_OPTS", - "--mount type=bind,src=./target,dst=/app/target", + format!("--mount type=bind,src={src},dst=/app/target"), ), ) .await?; @@ -2074,9 +2169,18 @@ impl SshRemoteConnection { .await; if which.is_err() { - anyhow::bail!( - "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" - ) + #[cfg(not(target_os = "windows"))] + { + anyhow::bail!( + "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" + ) + } + #[cfg(target_os = "windows")] + { + anyhow::bail!( + "zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" + ) + } } delegate.set_status(Some("Adding rustup target for cross-compilation"), cx); @@ -2112,12 +2216,31 @@ impl SshRemoteConnection { if !build_remote_server.contains("nocompress") { delegate.set_status(Some("Compressing binary"), cx); - run_cmd(Command::new("gzip").args([ - "-9", - "-f", - &format!("target/remote_server/{}/debug/remote_server", triple), - ])) - .await?; + #[cfg(not(target_os = "windows"))] + { + run_cmd(Command::new("gzip").args([ + "-9", + "-f", + &format!("target/remote_server/{}/debug/remote_server", triple), + ])) + .await?; + } + #[cfg(target_os = "windows")] + { + // On Windows, we use 7z to compress the binary + let seven_zip = which::which("7z.exe").context("7z.exe not found on $PATH, install it (e.g. with `winget install -e --id 7zip.7zip`) or, if you don't want this behaviour, set $env:ZED_BUILD_REMOTE_SERVER=\"nocompress\"")?; + let gz_path = format!("target/remote_server/{}/debug/remote_server.gz", triple); + if smol::fs::metadata(&gz_path).await.is_ok() { + smol::fs::remove_file(&gz_path).await?; + } + run_cmd(Command::new(seven_zip).args([ + "a", + "-tgzip", + &gz_path, + &format!("target/remote_server/{}/debug/remote_server", triple), + ])) + .await?; + } path = std::env::current_dir()?.join(format!( "target/remote_server/{triple}/debug/remote_server.gz" @@ -2450,9 +2573,11 @@ mod fake { use gpui::{App, AppContext as _, AsyncApp, SemanticVersion, Task, TestAppContext}; use release_channel::ReleaseChannel; use rpc::proto::Envelope; + use util::paths::{PathStyle, RemotePathBuf}; use super::{ - ChannelClient, RemoteConnection, SshClientDelegate, SshConnectionOptions, SshPlatform, + ChannelClient, RemoteConnection, SshArgs, SshClientDelegate, SshConnectionOptions, + SshPlatform, }; pub(super) struct FakeRemoteConnection { @@ -2488,13 +2613,17 @@ mod fake { false } - fn ssh_args(&self) -> Vec { - Vec::new() + fn ssh_args(&self) -> SshArgs { + SshArgs { + arguments: Vec::new(), + envs: None, + } } + fn upload_directory( &self, _src_path: PathBuf, - _dest_path: PathBuf, + _dest_path: RemotePathBuf, _cx: &App, ) -> Task> { unreachable!() @@ -2513,7 +2642,6 @@ mod fake { fn start_proxy( &self, - _unique_identifier: String, _reconnect: bool, mut client_incoming_tx: mpsc::UnboundedSender, @@ -2551,6 +2679,10 @@ mod fake { } }) } + + fn path_style(&self) -> PathStyle { + PathStyle::current() + } } pub(super) struct Delegate; diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 2e02f051d15fc8a20d3181b3a37cbad0b9745651..585f2b08aa8da11874eacc4371721949f1e5e8d6 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -166,6 +166,98 @@ impl> From for SanitizedPath { } } +#[derive(Debug, Clone, Copy)] +pub enum PathStyle { + Posix, + Windows, +} + +impl PathStyle { + #[cfg(target_os = "windows")] + pub const fn current() -> Self { + PathStyle::Windows + } + + #[cfg(not(target_os = "windows"))] + pub const fn current() -> Self { + PathStyle::Posix + } + + #[inline] + pub fn separator(&self) -> &str { + match self { + PathStyle::Posix => "/", + PathStyle::Windows => "\\", + } + } +} + +#[derive(Debug, Clone)] +pub struct RemotePathBuf { + inner: PathBuf, + style: PathStyle, + string: String, // Cached string representation +} + +impl RemotePathBuf { + pub fn new(path: PathBuf, style: PathStyle) -> Self { + #[cfg(target_os = "windows")] + let string = match style { + PathStyle::Posix => path.to_string_lossy().replace('\\', "/"), + PathStyle::Windows => path.to_string_lossy().into(), + }; + #[cfg(not(target_os = "windows"))] + let string = match style { + PathStyle::Posix => path.to_string_lossy().to_string(), + PathStyle::Windows => path.to_string_lossy().replace('/', "\\"), + }; + Self { + inner: path, + style, + string, + } + } + + pub fn from_str(path: &str, style: PathStyle) -> Self { + let path_buf = PathBuf::from(path); + Self::new(path_buf, style) + } + + pub fn to_string(&self) -> String { + self.string.clone() + } + + #[cfg(target_os = "windows")] + pub fn to_proto(self) -> String { + match self.path_style() { + PathStyle::Posix => self.to_string(), + PathStyle::Windows => self.inner.to_string_lossy().replace('\\', "/"), + } + } + + #[cfg(not(target_os = "windows"))] + pub fn to_proto(self) -> String { + match self.path_style() { + PathStyle::Posix => self.inner.to_string_lossy().to_string(), + PathStyle::Windows => self.to_string(), + } + } + + pub fn as_path(&self) -> &Path { + &self.inner + } + + pub fn path_style(&self) -> PathStyle { + self.style + } + + pub fn parent(&self) -> Option { + self.inner + .parent() + .map(|p| RemotePathBuf::new(p.to_path_buf(), self.style)) + } +} + /// A delimiter to use in `path_query:row_number:column_number` strings parsing. pub const FILE_ROW_COLUMN_DELIMITER: char = ':'; diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 3c46c486a8abce4db926aa650bd349d92eaecd9c..b5efea10e243ada4ebdf62985fe63489800d06be 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -167,16 +167,6 @@ pub fn main() { #[cfg(unix)] util::prevent_root_execution(); - // Check if there is a pending installer - // If there is, run the installer and exit - // And we don't want to run the installer if we are not the first instance - #[cfg(target_os = "windows")] - let is_first_instance = crate::zed::windows_only_instance::is_first_instance(); - #[cfg(target_os = "windows")] - if is_first_instance && auto_update::check_pending_installation() { - return; - } - let args = Args::parse(); // `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass @@ -191,6 +181,16 @@ pub fn main() { return; } + // Check if there is a pending installer + // If there is, run the installer and exit + // And we don't want to run the installer if we are not the first instance + #[cfg(target_os = "windows")] + let is_first_instance = crate::zed::windows_only_instance::is_first_instance(); + #[cfg(target_os = "windows")] + if is_first_instance && auto_update::check_pending_installation() { + return; + } + if args.dump_all_actions { dump_all_gpui_actions(); return; From bcac748c2bc4aaa25bda987ce1c42e17f2c9a18d Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Tue, 8 Jul 2025 10:57:37 -0400 Subject: [PATCH 087/239] Add support for Nushell in shell builder (#33806) We also swap out env variables before sending them to shells now in the task system. This fixed issues Fish and Nushell had where an empty argument could be sent into a command when no argument should be sent. This only happened from task's generated by Zed. Closes #31297 #31240 Release Notes: - Fix bug where spawning a Zed generated task or debug session with Fish or Nushell failed --- crates/task/src/shell_builder.rs | 121 ++++++++++++++++++++++++++++++- crates/task/src/task_template.rs | 10 +-- 2 files changed, 123 insertions(+), 8 deletions(-) diff --git a/crates/task/src/shell_builder.rs b/crates/task/src/shell_builder.rs index 544663713933dd967f71c9330268f46688b11d93..b8c49d4230f384982b74e7a3055b8504a8671711 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -5,6 +5,7 @@ enum ShellKind { #[default] Posix, Powershell, + Nushell, Cmd, } @@ -18,6 +19,8 @@ impl ShellKind { ShellKind::Powershell } else if program == "cmd" || program.ends_with("cmd.exe") { ShellKind::Cmd + } else if program == "nu" { + ShellKind::Nushell } else { // Someother shell detected, the user might install and use a // unix-like shell. @@ -30,6 +33,7 @@ impl ShellKind { Self::Powershell => Self::to_powershell_variable(input), Self::Cmd => Self::to_cmd_variable(input), Self::Posix => input.to_owned(), + Self::Nushell => Self::to_nushell_variable(input), } } @@ -70,11 +74,86 @@ impl ShellKind { } } + fn to_nushell_variable(input: &str) -> String { + let mut result = String::new(); + let mut source = input; + let mut is_start = true; + + loop { + match source.chars().next() { + None => return result, + Some('$') => { + source = Self::parse_nushell_var(&source[1..], &mut result, is_start); + is_start = false; + } + Some(_) => { + is_start = false; + let chunk_end = source.find('$').unwrap_or(source.len()); + let (chunk, rest) = source.split_at(chunk_end); + result.push_str(chunk); + source = rest; + } + } + } + } + + fn parse_nushell_var<'a>(source: &'a str, text: &mut String, is_start: bool) -> &'a str { + if source.starts_with("env.") { + text.push('$'); + return source; + } + + match source.chars().next() { + Some('{') => { + let source = &source[1..]; + if let Some(end) = source.find('}') { + let var_name = &source[..end]; + if !var_name.is_empty() { + if !is_start { + text.push_str("("); + } + text.push_str("$env."); + text.push_str(var_name); + if !is_start { + text.push_str(")"); + } + &source[end + 1..] + } else { + text.push_str("${}"); + &source[end + 1..] + } + } else { + text.push_str("${"); + source + } + } + Some(c) if c.is_alphabetic() || c == '_' => { + let end = source + .find(|c: char| !c.is_alphanumeric() && c != '_') + .unwrap_or(source.len()); + let var_name = &source[..end]; + if !is_start { + text.push_str("("); + } + text.push_str("$env."); + text.push_str(var_name); + if !is_start { + text.push_str(")"); + } + &source[end..] + } + _ => { + text.push('$'); + source + } + } + } + fn args_for_shell(&self, interactive: bool, combined_command: String) -> Vec { match self { ShellKind::Powershell => vec!["-C".to_owned(), combined_command], ShellKind::Cmd => vec!["/C".to_owned(), combined_command], - ShellKind::Posix => interactive + ShellKind::Posix | ShellKind::Nushell => interactive .then(|| "-i".to_owned()) .into_iter() .chain(["-c".to_owned(), combined_command]) @@ -142,9 +221,12 @@ impl ShellBuilder { ShellKind::Cmd => { format!("{} /C '{}'", self.program, command_label) } - ShellKind::Posix => { + ShellKind::Posix | ShellKind::Nushell => { let interactivity = self.interactive.then_some("-i ").unwrap_or_default(); - format!("{} {interactivity}-c '{}'", self.program, command_label) + format!( + "{} {interactivity}-c '$\"{}\"'", + self.program, command_label + ) } } } @@ -170,3 +252,36 @@ impl ShellBuilder { (self.program, self.args) } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_nu_shell_variable_substitution() { + let shell = Shell::Program("nu".to_owned()); + let shell_builder = ShellBuilder::new(true, &shell); + + let (program, args) = shell_builder.build( + Some("echo".into()), + &vec![ + "${hello}".to_string(), + "$world".to_string(), + "nothing".to_string(), + "--$something".to_string(), + "$".to_string(), + "${test".to_string(), + ], + ); + + assert_eq!(program, "nu"); + assert_eq!( + args, + vec![ + "-i", + "-c", + "echo $env.hello $env.world nothing --($env.something) $ ${test" + ] + ); + } +} diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index ae5054ac556b4ad82f5c9243005592593b033006..24e11d771546dc3f4c15310af2f37e1c6ac4d824 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -256,7 +256,7 @@ impl TaskTemplate { }, ), command: Some(command), - args: self.args.clone(), + args: args_with_substitutions, env, use_new_terminal: self.use_new_terminal, allow_concurrent_runs: self.allow_concurrent_runs, @@ -642,11 +642,11 @@ mod tests { assert_eq!( spawn_in_terminal.args, &[ - "arg1 $ZED_SELECTED_TEXT", - "arg2 $ZED_COLUMN", - "arg3 $ZED_SYMBOL", + "arg1 test_selected_text", + "arg2 5678", + "arg3 010101010101010101010101010101010101010101010101010101010101", ], - "Args should not be substituted with variables" + "Args should be substituted with variables" ); assert_eq!( spawn_in_terminal.command_label, From 925464cfc6cb26f2576816f8f02bbf7044e9f41b Mon Sep 17 00:00:00 2001 From: Alisina Bahadori Date: Tue, 8 Jul 2025 11:05:01 -0400 Subject: [PATCH 088/239] Improve terminal rendering performance (#33345) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #18263 Improvements: • **Batch text rendering** - Combine adjacent cells with identical styling into single text runs to reduce draw calls • **Throttle hyperlink searches** - Limit hyperlink detection to every 100ms or when mouse moves >5px to reduce CPU usage • **Pre-allocate collections** - Use `Vec::with_capacity()` for cells, runs, and regions to minimize reallocations • **Optimize background regions** - Merge adjacent background rectangles to reduce number of draw operations • **Cache selection text** - Only compute terminal selection string when selection exists Release Notes: - Improved terminal rendering performance. --------- Co-authored-by: Conrad Irwin --- crates/editor/src/display_map.rs | 2 +- crates/editor/src/element.rs | 29 +- crates/gpui/examples/input.rs | 2 +- crates/gpui/src/text_system.rs | 10 +- crates/gpui/src/text_system/line_layout.rs | 42 +- crates/repl/src/outputs/plain.rs | 24 +- crates/repl/src/outputs/table.rs | 6 +- crates/terminal/src/terminal.rs | 63 +- crates/terminal_view/src/terminal_element.rs | 608 +++++++++++++++---- 9 files changed, 609 insertions(+), 177 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 3352d21ef878835987e0227a926dfb61c893a182..aa2408d6d9b616f2b1436d9bc66f42bd87506d19 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1066,7 +1066,7 @@ impl DisplaySnapshot { } let font_size = editor_style.text.font_size.to_pixels(*rem_size); - text_system.layout_line(&line, font_size, &runs) + text_system.layout_line(&line, font_size, &runs, None) } pub fn x_for_display_point( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index d5c8eae99c2993163dbbde191524bad48965c7f6..4c53d7f28a7d183af449308e259e8a6a3f694198 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1611,6 +1611,7 @@ impl EditorElement { strikethrough: None, underline: None, }], + None, ) }) } else { @@ -3263,10 +3264,12 @@ impl EditorElement { underline: None, strikethrough: None, }; - let line = - window - .text_system() - .shape_line(line.to_string().into(), font_size, &[run]); + let line = window.text_system().shape_line( + line.to_string().into(), + font_size, + &[run], + None, + ); LineWithInvisibles { width: line.width, len: line.len, @@ -6888,6 +6891,7 @@ impl EditorElement { underline: None, strikethrough: None, }], + None, ); layout.width @@ -6916,6 +6920,7 @@ impl EditorElement { text, self.style.text.font_size.to_pixels(window.rem_size()), &[run], + None, ) } @@ -7184,10 +7189,12 @@ impl LineWithInvisibles { }]) { if let Some(replacement) = highlighted_chunk.replacement { if !line.is_empty() { - let shaped_line = - window - .text_system() - .shape_line(line.clone().into(), font_size, &styles); + let shaped_line = window.text_system().shape_line( + line.clone().into(), + font_size, + &styles, + None, + ); width += shaped_line.width; len += shaped_line.len; fragments.push(LineFragment::Text(shaped_line)); @@ -7207,6 +7214,7 @@ impl LineWithInvisibles { chunk, font_size, &[text_style.to_run(highlighted_chunk.text.len())], + None, ); AvailableSpace::Definite(shaped_line.width) } else { @@ -7251,7 +7259,7 @@ impl LineWithInvisibles { }; let line_layout = window .text_system() - .shape_line(x, font_size, &[run]) + .shape_line(x, font_size, &[run], None) .with_len(highlighted_chunk.text.len()); width += line_layout.width; @@ -7266,6 +7274,7 @@ impl LineWithInvisibles { line.clone().into(), font_size, &styles, + None, ); width += shaped_line.width; len += shaped_line.len; @@ -8831,6 +8840,7 @@ impl Element for EditorElement { underline: None, strikethrough: None, }], + None ); let space_invisible = window.text_system().shape_line( "•".into(), @@ -8843,6 +8853,7 @@ impl Element for EditorElement { underline: None, strikethrough: None, }], + None ); let mode = snapshot.mode.clone(); diff --git a/crates/gpui/examples/input.rs b/crates/gpui/examples/input.rs index 430d59acb8e8255099eeac0e13d09d7f828c2cc9..52a5b08b967927ef709dffc1e21c4075e4cdc5df 100644 --- a/crates/gpui/examples/input.rs +++ b/crates/gpui/examples/input.rs @@ -487,7 +487,7 @@ impl Element for TextElement { let font_size = style.font_size.to_pixels(window.rem_size()); let line = window .text_system() - .shape_line(display_text, font_size, &runs); + .shape_line(display_text, font_size, &runs, None); let cursor_pos = line.x_for_index(cursor); let (selection, cursor) = if selected_range.is_empty() { diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index b2af9140c6b9bd7bdf79b4885fab00185728d429..ed1307c6cdb797d85b5a48b0d5ef033f1ebddc36 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -357,6 +357,7 @@ impl WindowTextSystem { text: SharedString, font_size: Pixels, runs: &[TextRun], + force_width: Option, ) -> ShapedLine { debug_assert!( text.find('\n').is_none(), @@ -384,7 +385,7 @@ impl WindowTextSystem { }); } - let layout = self.layout_line(&text, font_size, runs); + let layout = self.layout_line(&text, font_size, runs, force_width); ShapedLine { layout, @@ -524,6 +525,7 @@ impl WindowTextSystem { text: Text, font_size: Pixels, runs: &[TextRun], + force_width: Option, ) -> Arc where Text: AsRef, @@ -544,9 +546,9 @@ impl WindowTextSystem { }); } - let layout = self - .line_layout_cache - .layout_line(text, font_size, &font_runs); + let layout = + self.line_layout_cache + .layout_line_internal(text, font_size, &font_runs, force_width); font_runs.clear(); self.font_runs_pool.lock().push(font_runs); diff --git a/crates/gpui/src/text_system/line_layout.rs b/crates/gpui/src/text_system/line_layout.rs index 5a72080e4809663679483b41b70cf84a69cc5a06..9c2dd7f0871e5b67bd15d3a419c1c03496e2afaa 100644 --- a/crates/gpui/src/text_system/line_layout.rs +++ b/crates/gpui/src/text_system/line_layout.rs @@ -482,6 +482,7 @@ impl LineLayoutCache { font_size, runs, wrap_width, + force_width: None, } as &dyn AsCacheKeyRef; let current_frame = self.current_frame.upgradable_read(); @@ -516,6 +517,7 @@ impl LineLayoutCache { font_size, runs: SmallVec::from(runs), wrap_width, + force_width: None, }); let mut current_frame = self.current_frame.write(); @@ -534,6 +536,20 @@ impl LineLayoutCache { font_size: Pixels, runs: &[FontRun], ) -> Arc + where + Text: AsRef, + SharedString: From, + { + self.layout_line_internal(text, font_size, runs, None) + } + + pub fn layout_line_internal( + &self, + text: Text, + font_size: Pixels, + runs: &[FontRun], + force_width: Option, + ) -> Arc where Text: AsRef, SharedString: From, @@ -543,6 +559,7 @@ impl LineLayoutCache { font_size, runs, wrap_width: None, + force_width, } as &dyn AsCacheKeyRef; let current_frame = self.current_frame.upgradable_read(); @@ -557,16 +574,30 @@ impl LineLayoutCache { layout } else { let text = SharedString::from(text); - let layout = Arc::new( - self.platform_text_system - .layout_line(&text, font_size, runs), - ); + let mut layout = self + .platform_text_system + .layout_line(&text, font_size, runs); + + if let Some(force_width) = force_width { + let mut glyph_pos = 0; + for run in layout.runs.iter_mut() { + for glyph in run.glyphs.iter_mut() { + if (glyph.position.x - glyph_pos * force_width).abs() > px(1.) { + glyph.position.x = glyph_pos * force_width; + } + glyph_pos += 1; + } + } + } + let key = Arc::new(CacheKey { text, font_size, runs: SmallVec::from(runs), wrap_width: None, + force_width, }); + let layout = Arc::new(layout); current_frame.lines.insert(key.clone(), layout.clone()); current_frame.used_lines.push(key); layout @@ -591,6 +622,7 @@ struct CacheKey { font_size: Pixels, runs: SmallVec<[FontRun; 1]>, wrap_width: Option, + force_width: Option, } #[derive(Copy, Clone, PartialEq, Eq, Hash)] @@ -599,6 +631,7 @@ struct CacheKeyRef<'a> { font_size: Pixels, runs: &'a [FontRun], wrap_width: Option, + force_width: Option, } impl PartialEq for (dyn AsCacheKeyRef + '_) { @@ -622,6 +655,7 @@ impl AsCacheKeyRef for CacheKey { font_size: self.font_size, runs: self.runs.as_slice(), wrap_width: self.wrap_width, + force_width: self.force_width, } } } diff --git a/crates/repl/src/outputs/plain.rs b/crates/repl/src/outputs/plain.rs index 515bc654f00732888c6ad709d4bce9596c1b5cbb..74c7bfa3c3a33da450f07f879b5681e9bdc1fdca 100644 --- a/crates/repl/src/outputs/plain.rs +++ b/crates/repl/src/outputs/plain.rs @@ -259,20 +259,17 @@ impl Render for TerminalOutput { cell: ic.cell.clone(), }); let minimum_contrast = TerminalSettings::get_global(cx).minimum_contrast; - let (cells, rects) = TerminalElement::layout_grid( - grid, - 0, - &text_style, - text_system, - None, - minimum_contrast, - window, - cx, - ); + let (rects, batched_text_runs) = + TerminalElement::layout_grid(grid, 0, &text_style, None, minimum_contrast, cx); // lines are 0-indexed, so we must add 1 to get the number of lines let text_line_height = text_style.line_height_in_pixels(window.rem_size()); - let num_lines = cells.iter().map(|c| c.point.line).max().unwrap_or(0) + 1; + let num_lines = batched_text_runs + .iter() + .map(|b| b.start_point.line) + .max() + .unwrap_or(0) + + 1; let height = num_lines as f32 * text_line_height; let font_pixels = text_style.font_size.to_pixels(window.rem_size()); @@ -300,15 +297,14 @@ impl Render for TerminalOutput { ); } - for cell in cells { - cell.paint( + for batch in batched_text_runs { + batch.paint( bounds.origin, &terminal::TerminalBounds { cell_width, line_height: text_line_height, bounds, }, - bounds, window, cx, ); diff --git a/crates/repl/src/outputs/table.rs b/crates/repl/src/outputs/table.rs index 0606b421aa02232e34d5559c51d01b3955a38bbf..c94e8c26a9ff02fde0b73bf8ee09006e50fbc087 100644 --- a/crates/repl/src/outputs/table.rs +++ b/crates/repl/src/outputs/table.rs @@ -106,7 +106,9 @@ impl TableView { for field in table.schema.fields.iter() { runs[0].len = field.name.len(); - let mut width = text_system.layout_line(&field.name, font_size, &runs).width; + let mut width = text_system + .layout_line(&field.name, font_size, &runs, None) + .width; let Some(data) = table.data.as_ref() else { widths.push(width); @@ -118,7 +120,7 @@ impl TableView { runs[0].len = content.len(); let cell_width = window .text_system() - .layout_line(&content, font_size, &runs) + .layout_line(&content, font_size, &runs, None) .width; width = width.max(cell_width) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index a096ef8901a6be491ca90be2040a0cbcbc7f0f30..8172b564852e9b32aed20f467b5940616ca54686 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -58,7 +58,7 @@ use std::{ path::PathBuf, process::ExitStatus, sync::Arc, - time::Duration, + time::{Duration, Instant}, }; use thiserror::Error; @@ -501,6 +501,8 @@ impl TerminalBuilder { vi_mode_enabled: false, is_ssh_terminal, python_venv_directory, + last_mouse_move_time: Instant::now(), + last_hyperlink_search_position: None, }; Ok(TerminalBuilder { @@ -659,6 +661,8 @@ pub struct Terminal { task: Option, vi_mode_enabled: bool, is_ssh_terminal: bool, + last_mouse_move_time: Instant, + last_hyperlink_search_position: Option>, } pub struct TaskState { @@ -1307,24 +1311,27 @@ impl Terminal { fn make_content(term: &Term, last_content: &TerminalContent) -> TerminalContent { let content = term.renderable_content(); + + // Pre-allocate with estimated size to reduce reallocations + let estimated_size = content.display_iter.size_hint().0; + let mut cells = Vec::with_capacity(estimated_size); + + cells.extend(content.display_iter.map(|ic| IndexedCell { + point: ic.point, + cell: ic.cell.clone(), + })); + + let selection_text = if content.selection.is_some() { + term.selection_to_string() + } else { + None + }; + TerminalContent { - cells: content - .display_iter - //TODO: Add this once there's a way to retain empty lines - // .filter(|ic| { - // !ic.flags.contains(Flags::HIDDEN) - // && !(ic.bg == Named(NamedColor::Background) - // && ic.c == ' ' - // && !ic.flags.contains(Flags::INVERSE)) - // }) - .map(|ic| IndexedCell { - point: ic.point, - cell: ic.cell.clone(), - }) - .collect::>(), + cells, mode: content.mode, display_offset: content.display_offset, - selection_text: term.selection_to_string(), + selection_text, selection: content.selection, cursor: content.cursor, cursor_char: term.grid()[content.cursor.point].c, @@ -1457,10 +1464,26 @@ impl Terminal { if self.selection_phase == SelectionPhase::Selecting { self.last_content.last_hovered_word = None; } else if self.last_content.terminal_bounds.bounds.contains(&position) { - self.events.push_back(InternalEvent::FindHyperlink( - position - self.last_content.terminal_bounds.bounds.origin, - false, - )); + // Throttle hyperlink searches to avoid excessive processing + let now = Instant::now(); + let should_search = if let Some(last_pos) = self.last_hyperlink_search_position { + // Only search if mouse moved significantly or enough time passed + let distance_moved = + ((position.x - last_pos.x).abs() + (position.y - last_pos.y).abs()) > px(5.0); + let time_elapsed = now.duration_since(self.last_mouse_move_time).as_millis() > 100; + distance_moved || time_elapsed + } else { + true + }; + + if should_search { + self.last_mouse_move_time = now; + self.last_hyperlink_search_position = Some(position); + self.events.push_back(InternalEvent::FindHyperlink( + position - self.last_content.terminal_bounds.bounds.origin, + false, + )); + } } else { self.last_content.last_hovered_word = None; } diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index f34dc8f00943ccb1d6c41e95d9c990b1968107aa..c34d8926440287ca684d7c93527516c41e2869df 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1,17 +1,18 @@ use crate::color_contrast; use editor::{CursorLayout, HighlightedRange, HighlightedRangeLine}; use gpui::{ - AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, DispatchPhase, Element, - ElementId, Entity, FocusHandle, Font, FontStyle, FontWeight, GlobalElementId, HighlightStyle, - Hitbox, Hsla, InputHandler, InteractiveElement, Interactivity, IntoElement, LayoutId, Length, - ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, Point, ShapedLine, - StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, TextStyle, UTF16Selection, - UnderlineStyle, WeakEntity, WhiteSpace, Window, WindowTextSystem, div, fill, point, px, - relative, size, + AbsoluteLength, AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, DispatchPhase, + Element, ElementId, Entity, FocusHandle, Font, FontFeatures, FontStyle, FontWeight, + GlobalElementId, HighlightStyle, Hitbox, Hsla, InputHandler, InteractiveElement, Interactivity, + IntoElement, LayoutId, Length, ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, + Point, ShapedLine, StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, TextStyle, + UTF16Selection, UnderlineStyle, WeakEntity, WhiteSpace, Window, div, fill, point, px, relative, + size, }; use itertools::Itertools; use language::CursorShape; use settings::Settings; +use std::time::Instant; use terminal::{ IndexedCell, Terminal, TerminalBounds, TerminalContent, alacritty_terminal::{ @@ -38,7 +39,7 @@ use crate::{BlockContext, BlockProperties, ContentMode, TerminalMode, TerminalVi /// The information generated during layout that is necessary for painting. pub struct LayoutState { hitbox: Hitbox, - cells: Vec, + batched_text_runs: Vec, rects: Vec, relative_highlighted_ranges: Vec<(RangeInclusive, Hsla)>, cursor: Option, @@ -76,37 +77,69 @@ impl DisplayCursor { } } -#[derive(Debug, Default)] -pub struct LayoutCell { - pub point: AlacPoint, - text: gpui::ShapedLine, +/// A batched text run that combines multiple adjacent cells with the same style +#[derive(Debug)] +pub struct BatchedTextRun { + pub start_point: AlacPoint, + pub text: String, + pub cell_count: usize, + pub style: TextRun, + pub font_size: AbsoluteLength, } -impl LayoutCell { - fn new(point: AlacPoint, text: gpui::ShapedLine) -> LayoutCell { - LayoutCell { point, text } +impl BatchedTextRun { + fn new_from_char( + start_point: AlacPoint, + c: char, + style: TextRun, + font_size: AbsoluteLength, + ) -> Self { + let mut text = String::with_capacity(100); // Pre-allocate for typical line length + text.push(c); + BatchedTextRun { + start_point, + text, + cell_count: 1, + style, + font_size, + } + } + + fn can_append(&self, other_style: &TextRun) -> bool { + self.style.font == other_style.font + && self.style.color == other_style.color + && self.style.background_color == other_style.background_color + && self.style.underline == other_style.underline + && self.style.strikethrough == other_style.strikethrough + } + + fn append_char(&mut self, c: char) { + self.text.push(c); + self.cell_count += 1; + self.style.len += c.len_utf8(); } pub fn paint( &self, origin: Point, dimensions: &TerminalBounds, - _visible_bounds: Bounds, window: &mut Window, cx: &mut App, ) { - let pos = { - let point = self.point; + let pos = Point::new( + (origin.x + self.start_point.column as f32 * dimensions.cell_width).floor(), + origin.y + self.start_point.line as f32 * dimensions.line_height, + ); - Point::new( - (origin.x + point.column as f32 * dimensions.cell_width).floor(), - origin.y + point.line as f32 * dimensions.line_height, + let _ = window + .text_system() + .shape_line( + self.text.clone().into(), + self.font_size.to_pixels(window.rem_size()), + &[self.style.clone()], + Some(dimensions.cell_width), ) - }; - - self.text - .paint(pos, dimensions.line_height, window, cx) - .ok(); + .paint(pos, dimensions.line_height, window, cx); } } @@ -126,14 +159,6 @@ impl LayoutRect { } } - fn extend(&self) -> Self { - LayoutRect { - point: self.point, - num_of_cells: self.num_of_cells + 1, - color: self.color, - } - } - pub fn paint(&self, origin: Point, dimensions: &TerminalBounds, window: &mut Window) { let position = { let alac_point = self.point; @@ -152,6 +177,87 @@ impl LayoutRect { } } +/// Represents a rectangular region with a specific background color +#[derive(Debug, Clone)] +struct BackgroundRegion { + start_line: i32, + start_col: i32, + end_line: i32, + end_col: i32, + color: Hsla, +} + +impl BackgroundRegion { + fn new(line: i32, col: i32, color: Hsla) -> Self { + BackgroundRegion { + start_line: line, + start_col: col, + end_line: line, + end_col: col, + color, + } + } + + /// Check if this region can be merged with another region + fn can_merge_with(&self, other: &BackgroundRegion) -> bool { + if self.color != other.color { + return false; + } + + // Check if regions are adjacent horizontally + if self.start_line == other.start_line && self.end_line == other.end_line { + return self.end_col + 1 == other.start_col || other.end_col + 1 == self.start_col; + } + + // Check if regions are adjacent vertically with same column span + if self.start_col == other.start_col && self.end_col == other.end_col { + return self.end_line + 1 == other.start_line || other.end_line + 1 == self.start_line; + } + + false + } + + /// Merge this region with another region + fn merge_with(&mut self, other: &BackgroundRegion) { + self.start_line = self.start_line.min(other.start_line); + self.start_col = self.start_col.min(other.start_col); + self.end_line = self.end_line.max(other.end_line); + self.end_col = self.end_col.max(other.end_col); + } +} + +/// Merge background regions to minimize the number of rectangles +fn merge_background_regions(regions: Vec) -> Vec { + if regions.is_empty() { + return regions; + } + + let mut merged = regions; + let mut changed = true; + + // Keep merging until no more merges are possible + while changed { + changed = false; + let mut i = 0; + + while i < merged.len() { + let mut j = i + 1; + while j < merged.len() { + if merged[i].can_merge_with(&merged[j]) { + let other = merged.remove(j); + merged[i].merge_with(&other); + changed = true; + } else { + j += 1; + } + } + i += 1; + } + } + + merged +} + /// The GPUI element that paints the terminal. /// We need to keep a reference to the model for mouse events, do we need it for any other terminal stuff, or can we move that to connection? pub struct TerminalElement { @@ -205,23 +311,37 @@ impl TerminalElement { grid: impl Iterator, start_line_offset: i32, text_style: &TextStyle, - text_system: &WindowTextSystem, hyperlink: Option<(HighlightStyle, &RangeInclusive)>, minimum_contrast: f32, - window: &Window, cx: &App, - ) -> (Vec, Vec) { + ) -> (Vec, Vec) { + let start_time = Instant::now(); let theme = cx.theme(); - let mut cells = vec![]; - let mut rects = vec![]; - let mut cur_rect: Option = None; - let mut cur_alac_color = None; + // Pre-allocate with estimated capacity to reduce reallocations + let estimated_cells = grid.size_hint().0; + let estimated_runs = estimated_cells / 10; // Estimate ~10 cells per run + let estimated_regions = estimated_cells / 20; // Estimate ~20 cells per background region + + let mut batched_runs = Vec::with_capacity(estimated_runs); + let mut cell_count = 0; + + // Collect background regions for efficient merging + let mut background_regions: Vec = Vec::with_capacity(estimated_regions); + let mut current_batch: Option = None; + // First pass: collect all cells and their backgrounds let linegroups = grid.into_iter().chunk_by(|i| i.point.line); for (line_index, (_, line)) in linegroups.into_iter().enumerate() { let alac_line = start_line_offset + line_index as i32; + // Flush any existing batch at line boundaries + if let Some(batch) = current_batch.take() { + batched_runs.push(batch); + } + + let mut previous_cell_had_extras = false; + for cell in line { let mut fg = cell.fg; let mut bg = cell.bg; @@ -229,63 +349,43 @@ impl TerminalElement { mem::swap(&mut fg, &mut bg); } - //Expand background rect range - { - if matches!(bg, Named(NamedColor::Background)) { - //Continue to next cell, resetting variables if necessary - cur_alac_color = None; - if let Some(rect) = cur_rect { - rects.push(rect); - cur_rect = None + // Collect background regions (skip default background) + if !matches!(bg, Named(NamedColor::Background)) { + let color = convert_color(&bg, theme); + let col = cell.point.column.0 as i32; + + // Try to extend the last region if it's on the same line with the same color + if let Some(last_region) = background_regions.last_mut() { + if last_region.color == color + && last_region.start_line == alac_line + && last_region.end_line == alac_line + && last_region.end_col + 1 == col + { + last_region.end_col = col; + } else { + background_regions.push(BackgroundRegion::new(alac_line, col, color)); } } else { - match cur_alac_color { - Some(cur_color) => { - if bg == cur_color { - // `cur_rect` can be None if it was moved to the `rects` vec after wrapping around - // from one line to the next. The variables are all set correctly but there is no current - // rect, so we create one if necessary. - cur_rect = cur_rect.map_or_else( - || { - Some(LayoutRect::new( - AlacPoint::new( - alac_line, - cell.point.column.0 as i32, - ), - 1, - convert_color(&bg, theme), - )) - }, - |rect| Some(rect.extend()), - ); - } else { - cur_alac_color = Some(bg); - if cur_rect.is_some() { - rects.push(cur_rect.take().unwrap()); - } - cur_rect = Some(LayoutRect::new( - AlacPoint::new(alac_line, cell.point.column.0 as i32), - 1, - convert_color(&bg, theme), - )); - } - } - None => { - cur_alac_color = Some(bg); - cur_rect = Some(LayoutRect::new( - AlacPoint::new(alac_line, cell.point.column.0 as i32), - 1, - convert_color(&bg, theme), - )); - } - } + background_regions.push(BackgroundRegion::new(alac_line, col, color)); } } + // Skip wide character spacers - they're just placeholders for the second cell of wide characters + if cell.flags.contains(Flags::WIDE_CHAR_SPACER) { + continue; + } + + // Skip spaces that follow cells with extras (emoji variation sequences) + if cell.c == ' ' && previous_cell_had_extras { + previous_cell_had_extras = false; + continue; + } + // Update tracking for next iteration + previous_cell_had_extras = cell.extra.is_some(); //Layout current cell text { if !is_blank(&cell) { - let cell_text = cell.c.to_string(); + cell_count += 1; let cell_style = TerminalElement::cell_style( &cell, fg, @@ -296,25 +396,74 @@ impl TerminalElement { minimum_contrast, ); - let layout_cell = text_system.shape_line( - cell_text.into(), - text_style.font_size.to_pixels(window.rem_size()), - &[cell_style], - ); + let cell_point = AlacPoint::new(alac_line, cell.point.column.0 as i32); - cells.push(LayoutCell::new( - AlacPoint::new(alac_line, cell.point.column.0 as i32), - layout_cell, - )) + // Try to batch with existing run + if let Some(ref mut batch) = current_batch { + if batch.can_append(&cell_style) + && batch.start_point.line == cell_point.line + && batch.start_point.column + batch.cell_count as i32 + == cell_point.column + { + batch.append_char(cell.c); + } else { + // Flush current batch and start new one + let old_batch = current_batch.take().unwrap(); + batched_runs.push(old_batch); + current_batch = Some(BatchedTextRun::new_from_char( + cell_point, + cell.c, + cell_style, + text_style.font_size, + )); + } + } else { + // Start new batch + current_batch = Some(BatchedTextRun::new_from_char( + cell_point, + cell.c, + cell_style, + text_style.font_size, + )); + } }; } } + } + + // Flush any remaining batch + if let Some(batch) = current_batch { + batched_runs.push(batch); + } - if cur_rect.is_some() { - rects.push(cur_rect.take().unwrap()); + // Second pass: merge background regions and convert to layout rects + let region_count = background_regions.len(); + let merged_regions = merge_background_regions(background_regions); + let mut rects = Vec::with_capacity(merged_regions.len() * 2); // Estimate 2 rects per merged region + + // Convert merged regions to layout rects + // Since LayoutRect only supports single-line rectangles, we need to split multi-line regions + for region in merged_regions { + for line in region.start_line..=region.end_line { + rects.push(LayoutRect::new( + AlacPoint::new(line, region.start_col), + (region.end_col - region.start_col + 1) as usize, + region.color, + )); } } - (cells, rects) + + let layout_time = start_time.elapsed(); + log::debug!( + "Terminal layout_grid: {} cells processed, {} batched runs created, {} rects (from {} merged regions), layout took {:?}", + cell_count, + batched_runs.len(), + rects.len(), + region_count, + layout_time + ); + + (rects, batched_runs) } /// Computes the cursor position and expected block width, may return a zero width if x_for_index returns @@ -708,7 +857,7 @@ impl Element for TerminalElement { let font_features = terminal_settings .font_features .as_ref() - .unwrap_or(&settings.buffer_font.features) + .unwrap_or(&FontFeatures::disable_ligatures()) .clone(); let font_weight = terminal_settings.font_weight.unwrap_or_default(); @@ -857,19 +1006,22 @@ impl Element for TerminalElement { // then have that representation be converted to the appropriate highlight data structure let content_mode = self.terminal_view.read(cx).content_mode(window, cx); - let (cells, rects) = match content_mode { - ContentMode::Scrollable => TerminalElement::layout_grid( - cells.iter().cloned(), - 0, - &text_style, - window.text_system(), - last_hovered_word - .as_ref() - .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)), - minimum_contrast, - window, - cx, - ), + let (rects, batched_text_runs) = match content_mode { + ContentMode::Scrollable => { + // In scrollable mode, the terminal already provides cells + // that are correctly positioned for the current viewport + // based on its display_offset. We don't need additional filtering. + TerminalElement::layout_grid( + cells.iter().cloned(), + 0, + &text_style, + last_hovered_word.as_ref().map(|last_hovered_word| { + (link_style, &last_hovered_word.word_match) + }), + minimum_contrast, + cx, + ) + } ContentMode::Inline { .. } => { let intersection = window.content_mask().bounds.intersect(&bounds); let start_row = (intersection.top() - bounds.top()) / line_height_px; @@ -884,12 +1036,10 @@ impl Element for TerminalElement { .cloned(), *line_range.start(), &text_style, - window.text_system(), last_hovered_word.as_ref().map(|last_hovered_word| { (link_style, &last_hovered_word.word_match) }), minimum_contrast, - window, cx, ) } @@ -915,6 +1065,7 @@ impl Element for TerminalElement { underline: Default::default(), strikethrough: None, }], + None, ) }; @@ -977,7 +1128,7 @@ impl Element for TerminalElement { LayoutState { hitbox, - cells, + batched_text_runs, cursor, background_color, dimensions, @@ -1005,6 +1156,7 @@ impl Element for TerminalElement { window: &mut Window, cx: &mut App, ) { + let paint_start = Instant::now(); window.with_content_mask(Some(ContentMask { bounds }), |window| { let scroll_top = self.terminal_view.read(cx).scroll_top; @@ -1089,9 +1241,12 @@ impl Element for TerminalElement { } } - for cell in &layout.cells { - cell.paint(origin, &layout.dimensions, bounds, window, cx); + // Paint batched text runs instead of individual cells + let text_paint_start = Instant::now(); + for batch in &layout.batched_text_runs { + batch.paint(origin, &layout.dimensions, window, cx); } + let text_paint_time = text_paint_start.elapsed(); if let Some(text_to_mark) = &marked_text_cloned { if !text_to_mark.is_empty() { @@ -1115,6 +1270,7 @@ impl Element for TerminalElement { underline: ime_style.underline, strikethrough: None, }], + None ); shaped_line .paint(ime_position, layout.dimensions.line_height, window, cx) @@ -1136,6 +1292,14 @@ impl Element for TerminalElement { if let Some(mut element) = hyperlink_tooltip { element.paint(window, cx); } + let total_paint_time = paint_start.elapsed(); + log::debug!( + "Terminal paint: {} text runs, {} rects, text paint took {:?}, total paint took {:?}", + layout.batched_text_runs.len(), + layout.rects.len(), + text_paint_time, + total_paint_time + ); }, ); }); @@ -1290,7 +1454,7 @@ pub fn is_blank(cell: &IndexedCell) -> bool { return false; } - true + return true; } fn to_highlighted_range_lines( @@ -1409,6 +1573,7 @@ pub fn convert_color(fg: &terminal::alacritty_terminal::vte::ansi::Color, theme: #[cfg(test)] mod tests { use super::*; + use gpui::{AbsoluteLength, Hsla, font}; #[test] fn test_contrast_adjustment_logic() { @@ -1523,4 +1688,203 @@ mod tests { new_contrast ); } + + #[test] + fn test_batched_text_run_can_append() { + let style1 = TextRun { + len: 1, + font: font("Helvetica"), + color: Hsla::red(), + background_color: None, + underline: None, + strikethrough: None, + }; + + let style2 = TextRun { + len: 1, + font: font("Helvetica"), + color: Hsla::red(), + background_color: None, + underline: None, + strikethrough: None, + }; + + let style3 = TextRun { + len: 1, + font: font("Helvetica"), + color: Hsla::blue(), // Different color + background_color: None, + underline: None, + strikethrough: None, + }; + + let font_size = AbsoluteLength::Pixels(px(12.0)); + let batch = + BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'a', style1.clone(), font_size); + + // Should be able to append same style + assert!(batch.can_append(&style2)); + + // Should not be able to append different style + assert!(!batch.can_append(&style3)); + } + + #[test] + fn test_batched_text_run_append() { + let style = TextRun { + len: 1, + font: font("Helvetica"), + color: Hsla::red(), + background_color: None, + underline: None, + strikethrough: None, + }; + + let font_size = AbsoluteLength::Pixels(px(12.0)); + let mut batch = BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'a', style, font_size); + + assert_eq!(batch.text, "a"); + assert_eq!(batch.cell_count, 1); + assert_eq!(batch.style.len, 1); + + batch.append_char('b'); + + assert_eq!(batch.text, "ab"); + assert_eq!(batch.cell_count, 2); + assert_eq!(batch.style.len, 2); + + batch.append_char('c'); + + assert_eq!(batch.text, "abc"); + assert_eq!(batch.cell_count, 3); + assert_eq!(batch.style.len, 3); + } + + #[test] + fn test_batched_text_run_append_char() { + let style = TextRun { + len: 1, + font: font("Helvetica"), + color: Hsla::red(), + background_color: None, + underline: None, + strikethrough: None, + }; + + let font_size = AbsoluteLength::Pixels(px(12.0)); + let mut batch = BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'x', style, font_size); + + assert_eq!(batch.text, "x"); + assert_eq!(batch.cell_count, 1); + assert_eq!(batch.style.len, 1); + + batch.append_char('y'); + + assert_eq!(batch.text, "xy"); + assert_eq!(batch.cell_count, 2); + assert_eq!(batch.style.len, 2); + + // Test with multi-byte character + batch.append_char('😀'); + + assert_eq!(batch.text, "xy😀"); + assert_eq!(batch.cell_count, 3); + assert_eq!(batch.style.len, 6); // 1 + 1 + 4 bytes for emoji + } + + #[test] + fn test_background_region_can_merge() { + let color1 = Hsla::red(); + let color2 = Hsla::blue(); + + // Test horizontal merging + let mut region1 = BackgroundRegion::new(0, 0, color1); + region1.end_col = 5; + let region2 = BackgroundRegion::new(0, 6, color1); + assert!(region1.can_merge_with(®ion2)); + + // Test vertical merging with same column span + let mut region3 = BackgroundRegion::new(0, 0, color1); + region3.end_col = 5; + let mut region4 = BackgroundRegion::new(1, 0, color1); + region4.end_col = 5; + assert!(region3.can_merge_with(®ion4)); + + // Test cannot merge different colors + let region5 = BackgroundRegion::new(0, 0, color1); + let region6 = BackgroundRegion::new(0, 1, color2); + assert!(!region5.can_merge_with(®ion6)); + + // Test cannot merge non-adjacent regions + let region7 = BackgroundRegion::new(0, 0, color1); + let region8 = BackgroundRegion::new(0, 2, color1); + assert!(!region7.can_merge_with(®ion8)); + + // Test cannot merge vertical regions with different column spans + let mut region9 = BackgroundRegion::new(0, 0, color1); + region9.end_col = 5; + let mut region10 = BackgroundRegion::new(1, 0, color1); + region10.end_col = 6; + assert!(!region9.can_merge_with(®ion10)); + } + + #[test] + fn test_background_region_merge() { + let color = Hsla::red(); + + // Test horizontal merge + let mut region1 = BackgroundRegion::new(0, 0, color); + region1.end_col = 5; + let mut region2 = BackgroundRegion::new(0, 6, color); + region2.end_col = 10; + region1.merge_with(®ion2); + assert_eq!(region1.start_col, 0); + assert_eq!(region1.end_col, 10); + assert_eq!(region1.start_line, 0); + assert_eq!(region1.end_line, 0); + + // Test vertical merge + let mut region3 = BackgroundRegion::new(0, 0, color); + region3.end_col = 5; + let mut region4 = BackgroundRegion::new(1, 0, color); + region4.end_col = 5; + region3.merge_with(®ion4); + assert_eq!(region3.start_col, 0); + assert_eq!(region3.end_col, 5); + assert_eq!(region3.start_line, 0); + assert_eq!(region3.end_line, 1); + } + + #[test] + fn test_merge_background_regions() { + let color = Hsla::red(); + + // Test merging multiple adjacent regions + let regions = vec![ + BackgroundRegion::new(0, 0, color), + BackgroundRegion::new(0, 1, color), + BackgroundRegion::new(0, 2, color), + BackgroundRegion::new(1, 0, color), + BackgroundRegion::new(1, 1, color), + BackgroundRegion::new(1, 2, color), + ]; + + let merged = merge_background_regions(regions); + assert_eq!(merged.len(), 1); + assert_eq!(merged[0].start_line, 0); + assert_eq!(merged[0].end_line, 1); + assert_eq!(merged[0].start_col, 0); + assert_eq!(merged[0].end_col, 2); + + // Test with non-mergeable regions + let color2 = Hsla::blue(); + let regions2 = vec![ + BackgroundRegion::new(0, 0, color), + BackgroundRegion::new(0, 2, color), // Gap at column 1 + BackgroundRegion::new(1, 0, color2), // Different color + ]; + + let merged2 = merge_background_regions(regions2); + assert_eq!(merged2.len(), 3); + } } From 263080c4c4b8de4ea902b0180cc21c8968d61713 Mon Sep 17 00:00:00 2001 From: Gwen Lg <105106246+gwen-lg@users.noreply.github.com> Date: Tue, 8 Jul 2025 18:31:20 +0200 Subject: [PATCH 089/239] Allow local build of remote_server dev to be deployed to different linux than local (#33395) setup local build of `remote_server` to not depend of the local linux libraries by : - enable `vendored-libgit2` feature of git2 - setup target triple to `unknown-linux-musl` (mirror bundle-linux script) - add flag ` -C target-feature=+crt-static` in `RUSTFLAGS` env var (mirror bundle-linux script) Bonus: Add an option to setup mold as linker of local build. Closes #33341 Release Notes: - N/A --- Cargo.lock | 1 + crates/remote/src/ssh_session.rs | 269 ++++++++++++++++--------------- crates/remote_server/Cargo.toml | 5 +- 3 files changed, 143 insertions(+), 132 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 302c1cc6eaffbf99fc649ebe44a3ed491f976d0d..ce6bdffefb88811c200fa4f5add712caa505dee0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13213,6 +13213,7 @@ dependencies = [ "fs", "futures 0.3.31", "git", + "git2", "git_hosting_providers", "gpui", "gpui_tokio", diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 2653a19bd9aa888c701903aa14f846d6a5b0d446..ff51cfda710f84e3137f182070309fc0720aaf61 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -314,20 +314,6 @@ pub struct SshPlatform { pub arch: &'static str, } -impl SshPlatform { - pub fn triple(&self) -> Option { - Some(format!( - "{}-{}", - self.arch, - match self.os { - "linux" => "unknown-linux-gnu", - "macos" => "apple-darwin", - _ => return None, - } - )) - } -} - pub trait SshClientDelegate: Send + Sync { fn ask_password(&self, prompt: String, tx: oneshot::Sender, cx: &mut AsyncApp); fn get_download_params( @@ -2068,6 +2054,7 @@ impl SshRemoteConnection { cx: &mut AsyncApp, ) -> Result { use smol::process::{Command, Stdio}; + use std::env::VarError; async fn run_cmd(command: &mut Command) -> Result<()> { let output = command @@ -2082,70 +2069,37 @@ impl SshRemoteConnection { Ok(()) } + let triple = format!( + "{}-{}", + self.ssh_platform.arch, + match self.ssh_platform.os { + "linux" => "unknown-linux-musl", + "macos" => "apple-darwin", + _ => anyhow::bail!("can't cross compile for: {:?}", self.ssh_platform), + } + ); + let mut rust_flags = match std::env::var("RUSTFLAGS") { + Ok(val) => val, + Err(VarError::NotPresent) => String::new(), + Err(e) => { + log::error!("Failed to get env var `RUSTFLAGS` value: {e}"); + String::new() + } + }; + if self.ssh_platform.os == "linux" { + rust_flags.push_str(" -C target-feature=+crt-static"); + } + if build_remote_server.contains("mold") { + rust_flags.push_str(" -C link-arg=-fuse-ld=mold"); + } + if self.ssh_platform.arch == std::env::consts::ARCH && self.ssh_platform.os == std::env::consts::OS { delegate.set_status(Some("Building remote server binary from source"), cx); log::info!("building remote server binary from source"); - run_cmd(Command::new("cargo").args([ - "build", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - ])) - .await?; - - delegate.set_status(Some("Compressing binary"), cx); - - run_cmd(Command::new("gzip").args([ - "-9", - "-f", - "target/remote_server/debug/remote_server", - ])) - .await?; - - let path = std::env::current_dir()?.join("target/remote_server/debug/remote_server.gz"); - return Ok(path); - } - let Some(triple) = self.ssh_platform.triple() else { - anyhow::bail!("can't cross compile for: {:?}", self.ssh_platform); - }; - smol::fs::create_dir_all("target/remote_server").await?; - - if build_remote_server.contains("cross") { - #[cfg(target_os = "windows")] - use util::paths::SanitizedPath; - - delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx); - log::info!("installing cross"); - run_cmd(Command::new("cargo").args([ - "install", - "cross", - "--git", - "https://github.com/cross-rs/cross", - ])) - .await?; - - delegate.set_status( - Some(&format!( - "Building remote server binary from source for {} with Docker", - &triple - )), - cx, - ); - log::info!("building remote server binary from source for {}", &triple); - - // On Windows, the binding needs to be set to the canonical path - #[cfg(target_os = "windows")] - let src = - SanitizedPath::from(smol::fs::canonicalize("./target").await?).to_glob_string(); - #[cfg(not(target_os = "windows"))] - let src = "./target"; run_cmd( - Command::new("cross") + Command::new("cargo") .args([ "build", "--package", @@ -2157,73 +2111,126 @@ impl SshRemoteConnection { "--target", &triple, ]) - .env( - "CROSS_CONTAINER_OPTS", - format!("--mount type=bind,src={src},dst=/app/target"), - ), + .env("RUSTFLAGS", &rust_flags), ) .await?; } else { - let which = cx - .background_spawn(async move { which::which("zig") }) - .await; + if build_remote_server.contains("cross") { + #[cfg(target_os = "windows")] + use util::paths::SanitizedPath; + + delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx); + log::info!("installing cross"); + run_cmd(Command::new("cargo").args([ + "install", + "cross", + "--git", + "https://github.com/cross-rs/cross", + ])) + .await?; - if which.is_err() { - #[cfg(not(target_os = "windows"))] - { - anyhow::bail!( - "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" - ) - } + delegate.set_status( + Some(&format!( + "Building remote server binary from source for {} with Docker", + &triple + )), + cx, + ); + log::info!("building remote server binary from source for {}", &triple); + + // On Windows, the binding needs to be set to the canonical path #[cfg(target_os = "windows")] - { - anyhow::bail!( - "zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" - ) + let src = + SanitizedPath::from(smol::fs::canonicalize("./target").await?).to_glob_string(); + #[cfg(not(target_os = "windows"))] + let src = "./target"; + run_cmd( + Command::new("cross") + .args([ + "build", + "--package", + "remote_server", + "--features", + "debug-embed", + "--target-dir", + "target/remote_server", + "--target", + &triple, + ]) + .env( + "CROSS_CONTAINER_OPTS", + format!("--mount type=bind,src={src},dst=/app/target"), + ) + .env("RUSTFLAGS", &rust_flags), + ) + .await?; + } else { + let which = cx + .background_spawn(async move { which::which("zig") }) + .await; + + if which.is_err() { + #[cfg(not(target_os = "windows"))] + { + anyhow::bail!( + "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" + ) + } + #[cfg(target_os = "windows")] + { + anyhow::bail!( + "zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup) or pass ZED_BUILD_REMOTE_SERVER=cross to use cross" + ) + } } - } - delegate.set_status(Some("Adding rustup target for cross-compilation"), cx); - log::info!("adding rustup target"); - run_cmd(Command::new("rustup").args(["target", "add"]).arg(&triple)).await?; + delegate.set_status(Some("Adding rustup target for cross-compilation"), cx); + log::info!("adding rustup target"); + run_cmd(Command::new("rustup").args(["target", "add"]).arg(&triple)).await?; - delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx); - log::info!("installing cargo-zigbuild"); - run_cmd(Command::new("cargo").args(["install", "--locked", "cargo-zigbuild"])).await?; + delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx); + log::info!("installing cargo-zigbuild"); + run_cmd(Command::new("cargo").args(["install", "--locked", "cargo-zigbuild"])) + .await?; - delegate.set_status( - Some(&format!( - "Building remote binary from source for {triple} with Zig" - )), - cx, - ); - log::info!("building remote binary from source for {triple} with Zig"); - run_cmd(Command::new("cargo").args([ - "zigbuild", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - "--target", - &triple, - ])) - .await?; + delegate.set_status( + Some(&format!( + "Building remote binary from source for {triple} with Zig" + )), + cx, + ); + log::info!("building remote binary from source for {triple} with Zig"); + run_cmd( + Command::new("cargo") + .args([ + "zigbuild", + "--package", + "remote_server", + "--features", + "debug-embed", + "--target-dir", + "target/remote_server", + "--target", + &triple, + ]) + .env("RUSTFLAGS", &rust_flags), + ) + .await?; + } }; + let bin_path = Path::new("target") + .join("remote_server") + .join(&triple) + .join("debug") + .join("remote_server"); - let mut path = format!("target/remote_server/{triple}/debug/remote_server").into(); - if !build_remote_server.contains("nocompress") { + let path = if !build_remote_server.contains("nocompress") { delegate.set_status(Some("Compressing binary"), cx); #[cfg(not(target_os = "windows"))] { - run_cmd(Command::new("gzip").args([ - "-9", - "-f", - &format!("target/remote_server/{}/debug/remote_server", triple), - ])) - .await?; + run_cmd(Command::new("gzip").args(["-9", "-f", &bin_path.to_string_lossy()])) + .await?; } #[cfg(target_os = "windows")] { @@ -2237,17 +2244,19 @@ impl SshRemoteConnection { "a", "-tgzip", &gz_path, - &format!("target/remote_server/{}/debug/remote_server", triple), + &bin_path.to_string_lossy(), ])) .await?; } - path = std::env::current_dir()?.join(format!( - "target/remote_server/{triple}/debug/remote_server.gz" - )); - } + let mut archive_path = bin_path; + archive_path.set_extension("gz"); + std::env::current_dir()?.join(archive_path) + } else { + bin_path + }; - return Ok(path); + Ok(path) } } diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index b780f57ab4463befb17bc4bb3846e32a1c070e57..443c47919f14b1fe4d19daeae762d711961d1a17 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -37,6 +37,7 @@ fs.workspace = true futures.workspace = true git.workspace = true git_hosting_providers.workspace = true +git2 = { workspace = true, features = ["vendored-libgit2"] } gpui.workspace = true gpui_tokio.workspace = true http_client.workspace = true @@ -85,7 +86,7 @@ node_runtime = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] } remote = { workspace = true, features = ["test-support"] } language_model = { workspace = true, features = ["test-support"] } -lsp = { workspace = true, features=["test-support"] } +lsp = { workspace = true, features = ["test-support"] } unindent.workspace = true serde_json.workspace = true zlog.workspace = true @@ -95,4 +96,4 @@ cargo_toml.workspace = true toml.workspace = true [package.metadata.cargo-machete] -ignored = ["rust-embed", "paths"] +ignored = ["git2", "rust-embed", "paths"] From 684e14e55be0af000bffcc2e2064b0c7db341f4b Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 8 Jul 2025 13:40:00 -0400 Subject: [PATCH 090/239] Move Perplexity extension to dedicated repository (#34070) Move Perplexity extension to: https://github.com/zed-extensions/perplexity Release Notes: - N/A --- .config/hakari.toml | 1 - Cargo.lock | 8 -- Cargo.toml | 1 - extensions/perplexity/Cargo.toml | 17 --- extensions/perplexity/LICENSE-APACHE | 1 - extensions/perplexity/README.md | 43 ------- extensions/perplexity/extension.toml | 12 -- extensions/perplexity/src/perplexity.rs | 158 ------------------------ 8 files changed, 241 deletions(-) delete mode 100644 extensions/perplexity/Cargo.toml delete mode 120000 extensions/perplexity/LICENSE-APACHE delete mode 100644 extensions/perplexity/README.md delete mode 100644 extensions/perplexity/extension.toml delete mode 100644 extensions/perplexity/src/perplexity.rs diff --git a/.config/hakari.toml b/.config/hakari.toml index bd742b33cdf5553346688c93580d3f5b0410216c..982542ca397e072d83af67608ea31a3415360a8e 100644 --- a/.config/hakari.toml +++ b/.config/hakari.toml @@ -33,7 +33,6 @@ workspace-members = [ "zed_emmet", "zed_glsl", "zed_html", - "perplexity", "zed_proto", "zed_ruff", "slash_commands_example", diff --git a/Cargo.lock b/Cargo.lock index ce6bdffefb88811c200fa4f5add712caa505dee0..bb3e4024bafb4cc432d535c74f464c975e8510e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11350,14 +11350,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "perplexity" -version = "0.1.0" -dependencies = [ - "serde", - "zed_extension_api 0.6.0", -] - [[package]] name = "pest" version = "2.8.0" diff --git a/Cargo.toml b/Cargo.toml index 32f520c6024b113ed210f9ab4374a0212f3fca4e..f22bff1f863e3cfc151952ea59889d35c955c4db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -189,7 +189,6 @@ members = [ "extensions/emmet", "extensions/glsl", "extensions/html", - "extensions/perplexity", "extensions/proto", "extensions/ruff", "extensions/slash-commands-example", diff --git a/extensions/perplexity/Cargo.toml b/extensions/perplexity/Cargo.toml deleted file mode 100644 index 476f4d414db7015d15e1a20f4cfc7062407d6b8e..0000000000000000000000000000000000000000 --- a/extensions/perplexity/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "perplexity" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "Apache-2.0" - -[lib] -path = "src/perplexity.rs" -crate-type = ["cdylib"] - -[lints] -workspace = true - -[dependencies] -serde = "1" -zed_extension_api = { path = "../../crates/extension_api" } diff --git a/extensions/perplexity/LICENSE-APACHE b/extensions/perplexity/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3affae83854be02a0afdec3b7a9ec4d..0000000000000000000000000000000000000000 --- a/extensions/perplexity/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/perplexity/README.md b/extensions/perplexity/README.md deleted file mode 100644 index 337c24325b260b39137acc52eb336154698cb076..0000000000000000000000000000000000000000 --- a/extensions/perplexity/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# Zed Perplexity Extension - -This example extension adds the `/perplexity` [slash command](https://zed.dev/docs/assistant/commands) to the Zed AI assistant. - -## Usage - -Open the AI Assistant panel (`cmd-r` or `ctrl-r`) and enter: - -``` -/perplexity What's the weather in Boulder, CO tomorrow evening? -``` - -## Development Setup - -1. Install the Rust toolchain and clone the zed repo: - - ``` - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - - mkdir -p ~/code - cd ~/code - git clone https://github.com/zed-industries/zed - ``` - -1. Open Zed -1. Open Zed Extensions (`cmd-shift-x` / `ctrl-shift-x`) -1. Click "Install Dev Extension" -1. Navigate to the "extensions/perplexity" folder inside the zed git repo. -1. Ensure your `PERPLEXITY_API_KEY` environment variable is set (instructions below) - - ```sh - env | grep PERPLEXITY_API_KEY - ``` - -1. Quit and relaunch Zed - -## PERPLEXITY_API_KEY - -This extension requires a Perplexity API key to be available via the `PERPLEXITY_API_KEY` environment variable. - -To obtain a Perplexity.ai API token, login to your Perplexity.ai account and go [Settings->API](https://www.perplexity.ai/settings/api) and under "API Keys" click "Generate". This will require you to have [Perplexity Pro](https://www.perplexity.ai/pro) or to buy API credits. By default the extension uses `llama-3.1-sonar-small-128k-online`, currently cheapest model available which is roughly half a penny per request + a penny per 50,000 tokens. So most requests will cost less than $0.01 USD. - -Take your API key and add it to your environment by adding `export PERPLEXITY_API_KEY="pplx-0123456789abcdef..."` to your `~/.zshrc` or `~/.bashrc`. Reload close and reopen your terminal session. Check with `env |grep PERPLEXITY_API_KEY`. diff --git a/extensions/perplexity/extension.toml b/extensions/perplexity/extension.toml deleted file mode 100644 index 474d9ee981aefc169ecd2670b9fa7686aa5b8d26..0000000000000000000000000000000000000000 --- a/extensions/perplexity/extension.toml +++ /dev/null @@ -1,12 +0,0 @@ -id = "perplexity" -name = "Perplexity" -version = "0.1.0" -description = "Ask questions to Perplexity AI directly from Zed" -authors = ["Zed Industries "] -repository = "https://github.com/zed-industries/zed" -schema_version = 1 - -[slash_commands.perplexity] -description = "Ask a question to Perplexity AI" -requires_argument = true -tooltip_text = "Ask Perplexity" diff --git a/extensions/perplexity/src/perplexity.rs b/extensions/perplexity/src/perplexity.rs deleted file mode 100644 index 95b829c11238a08ecd10679222d882612e611fd8..0000000000000000000000000000000000000000 --- a/extensions/perplexity/src/perplexity.rs +++ /dev/null @@ -1,158 +0,0 @@ -use zed::{ - http_client::HttpMethod, - http_client::HttpRequest, - serde_json::{self, json}, -}; -use zed_extension_api::{self as zed, Result, http_client::RedirectPolicy}; - -struct Perplexity; - -impl zed::Extension for Perplexity { - fn new() -> Self { - Self - } - - fn run_slash_command( - &self, - command: zed::SlashCommand, - argument: Vec, - worktree: Option<&zed::Worktree>, - ) -> zed::Result { - // Check if the command is 'perplexity' - if command.name != "perplexity" { - return Err("Invalid command. Expected 'perplexity'.".into()); - } - - let worktree = worktree.ok_or("Worktree is required")?; - // Join arguments with space as the query - let query = argument.join(" "); - if query.is_empty() { - return Ok(zed::SlashCommandOutput { - text: "Error: Query not provided. Please enter a question or topic.".to_string(), - sections: vec![], - }); - } - - // Get the API key from the environment - let env_vars = worktree.shell_env(); - let api_key = env_vars - .iter() - .find(|(key, _)| key == "PERPLEXITY_API_KEY") - .map(|(_, value)| value.clone()) - .ok_or("PERPLEXITY_API_KEY not found in environment")?; - - // Prepare the request - let request = HttpRequest { - method: HttpMethod::Post, - url: "https://api.perplexity.ai/chat/completions".to_string(), - headers: vec![ - ("Authorization".to_string(), format!("Bearer {}", api_key)), - ("Content-Type".to_string(), "application/json".to_string()), - ], - body: Some( - serde_json::to_vec(&json!({ - "model": "llama-3.1-sonar-small-128k-online", - "messages": [{"role": "user", "content": query}], - "stream": true, - })) - .unwrap(), - ), - redirect_policy: RedirectPolicy::FollowAll, - }; - - // Make the HTTP request - match zed::http_client::fetch_stream(&request) { - Ok(stream) => { - let mut full_content = String::new(); - let mut buffer = String::new(); - while let Ok(Some(chunk)) = stream.next_chunk() { - buffer.push_str(&String::from_utf8_lossy(&chunk)); - for line in buffer.lines() { - if let Some(json) = line.strip_prefix("data: ") { - if let Ok(event) = serde_json::from_str::(json) { - if let Some(choice) = event.choices.first() { - full_content.push_str(&choice.delta.content); - } - } - } - } - buffer.clear(); - } - Ok(zed::SlashCommandOutput { - text: full_content, - sections: vec![], - }) - } - Err(e) => Ok(zed::SlashCommandOutput { - text: format!("API request failed. Error: {}. API Key: {}", e, api_key), - sections: vec![], - }), - } - } - - fn complete_slash_command_argument( - &self, - _command: zed::SlashCommand, - query: Vec, - ) -> zed::Result> { - let suggestions = vec!["How do I develop a Zed extension?"]; - let query = query.join(" ").to_lowercase(); - - Ok(suggestions - .into_iter() - .filter(|suggestion| suggestion.to_lowercase().contains(&query)) - .map(|suggestion| zed::SlashCommandArgumentCompletion { - label: suggestion.to_string(), - new_text: suggestion.to_string(), - run_command: true, - }) - .collect()) - } - - fn language_server_command( - &mut self, - _language_server_id: &zed_extension_api::LanguageServerId, - _worktree: &zed_extension_api::Worktree, - ) -> Result { - Err("Not implemented".into()) - } -} - -#[derive(serde::Deserialize)] -struct StreamEvent { - id: String, - model: String, - created: u64, - usage: Usage, - object: String, - choices: Vec, -} - -#[derive(serde::Deserialize)] -struct Usage { - prompt_tokens: u32, - completion_tokens: u32, - total_tokens: u32, -} - -#[derive(serde::Deserialize)] -struct Choice { - index: u32, - finish_reason: Option, - message: Message, - delta: Delta, -} - -#[derive(serde::Deserialize)] -struct Message { - role: String, - content: String, -} - -#[derive(serde::Deserialize)] -struct Delta { - role: String, - content: String, -} - -zed::register_extension!(Perplexity); From 11ddecb995c5f2368ca6fb07e440afa7d80a45d9 Mon Sep 17 00:00:00 2001 From: fantacell Date: Tue, 8 Jul 2025 19:47:39 +0200 Subject: [PATCH 091/239] helix: Change keymap (#33925) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Might close #33838 for now Keymaps that work both in vim and helix, but only in normal mode, not the more general `VimControl` context are written separately. This makes the file shorter by combining them and also adds one more keymap. Release Notes: - N/A --- assets/keymaps/vim.json | 56 +++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 36 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index ba3012cc54f7cc0af464357b6fb05c041130262d..d90813e3d6f59192e866fabb13451b8ce8e6114f 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -218,35 +218,18 @@ "context": "vim_mode == normal", "bindings": { "ctrl-[": "editor::Cancel", - "escape": "editor::Cancel", ":": "command_palette::Toggle", "c": "vim::PushChange", "shift-c": "vim::ChangeToEndOfLine", "d": "vim::PushDelete", "delete": "vim::DeleteRight", - "shift-d": "vim::DeleteToEndOfLine", - "shift-j": "vim::JoinLines", "g shift-j": "vim::JoinLinesNoWhitespace", "y": "vim::PushYank", - "shift-y": "vim::YankLine", - "i": "vim::InsertBefore", - "shift-i": "vim::InsertFirstNonWhitespace", - "a": "vim::InsertAfter", - "shift-a": "vim::InsertEndOfLine", "x": "vim::DeleteRight", "shift-x": "vim::DeleteLeft", - "o": "vim::InsertLineBelow", - "shift-o": "vim::InsertLineAbove", - "~": "vim::ChangeCase", "ctrl-a": "vim::Increment", "ctrl-x": "vim::Decrement", - "p": "vim::Paste", - "shift-p": ["vim::Paste", { "before": true }], - "u": "vim::Undo", "ctrl-r": "vim::Redo", - "r": "vim::PushReplace", - "s": "vim::Substitute", - "shift-s": "vim::SubstituteLine", ">": "vim::PushIndent", "<": "vim::PushOutdent", "=": "vim::PushAutoIndent", @@ -256,11 +239,8 @@ "g ~": "vim::PushOppositeCase", "g ?": "vim::PushRot13", // "g ?": "vim::PushRot47", - "\"": "vim::PushRegister", "g w": "vim::PushRewrap", "g q": "vim::PushRewrap", - "ctrl-pagedown": "pane::ActivateNextItem", - "ctrl-pageup": "pane::ActivatePreviousItem", "insert": "vim::InsertBefore", // tree-sitter related commands "[ x": "vim::SelectLargerSyntaxNode", @@ -364,18 +344,11 @@ } }, { - "context": "vim_mode == helix_normal && !menu", + "context": "(vim_mode == normal || vim_mode == helix_normal) && !menu", "bindings": { "escape": "editor::Cancel", - "ctrl-[": "editor::Cancel", - ":": "command_palette::Toggle", - "left": "vim::WrappingLeft", - "right": "vim::WrappingRight", - "h": "vim::WrappingLeft", - "l": "vim::WrappingRight", "shift-d": "vim::DeleteToEndOfLine", "shift-j": "vim::JoinLines", - "y": "editor::Copy", "shift-y": "vim::YankLine", "i": "vim::InsertBefore", "shift-i": "vim::InsertFirstNonWhitespace", @@ -389,27 +362,39 @@ "p": "vim::Paste", "shift-p": ["vim::Paste", { "before": true }], "u": "vim::Undo", + "r": "vim::PushReplace", + "s": "vim::Substitute", + "shift-s": "vim::SubstituteLine", + "\"": "vim::PushRegister", + "ctrl-pagedown": "pane::ActivateNextItem", + "ctrl-pageup": "pane::ActivatePreviousItem" + } + }, + { + "context": "vim_mode == helix_normal && !menu", + "bindings": { + "ctrl-[": "editor::Cancel", + ":": "command_palette::Toggle", + "left": "vim::WrappingLeft", + "right": "vim::WrappingRight", + "h": "vim::WrappingLeft", + "l": "vim::WrappingRight", + "y": "editor::Copy", + "alt-;": "vim::OtherEnd", "ctrl-r": "vim::Redo", "f": ["vim::PushFindForward", { "before": false, "multiline": true }], "t": ["vim::PushFindForward", { "before": true, "multiline": true }], "shift-f": ["vim::PushFindBackward", { "after": false, "multiline": true }], "shift-t": ["vim::PushFindBackward", { "after": true, "multiline": true }], - "r": "vim::PushReplace", - "s": "vim::Substitute", - "shift-s": "vim::SubstituteLine", ">": "vim::Indent", "<": "vim::Outdent", "=": "vim::AutoIndent", "g u": "vim::PushLowercase", "g shift-u": "vim::PushUppercase", "g ~": "vim::PushOppositeCase", - "\"": "vim::PushRegister", "g q": "vim::PushRewrap", "g w": "vim::PushRewrap", - "ctrl-pagedown": "pane::ActivateNextItem", - "ctrl-pageup": "pane::ActivatePreviousItem", "insert": "vim::InsertBefore", - ".": "vim::Repeat", "alt-.": "vim::RepeatFind", // tree-sitter related commands "[ x": "editor::SelectLargerSyntaxNode", @@ -429,7 +414,6 @@ "g h": "vim::StartOfLine", "g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s" "g e": "vim::EndOfDocument", - "g y": "editor::GoToTypeDefinition", "g r": "editor::FindAllReferences", // zed specific "g t": "vim::WindowTop", "g c": "vim::WindowMiddle", From 9a3720edd32d1d43348cc0d2daaede5734b35991 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 8 Jul 2025 13:49:54 -0400 Subject: [PATCH 092/239] Disable word completions by default for plaintext/markdown (#34065) This disables word based completions in Plain Text and Markdown buffers by default. Word-based completion when typing natural language can be quite disruptive, in particular at the end of a line (e.g. markdown style lists) where `enter` will accept word completions rather than insert a newline (see screenshot). I think the default, empty buffer experience in Zed should be closer to a zed-mode experience -- just an editor getting out of your way to let you type and not having to mash escape/cmd-z repeatedly to undo a over-aggressive completion. Screenshot 2025-07-08 at 11 57 26 - Context: https://github.com/zed-industries/zed/issues/4957#issuecomment-3049513501 - Follow-up to: https://github.com/zed-industries/zed/pull/26410 Re-enable the existing behavior with: ```json "languages": { "Plain Text": { "completions": { "words": "fallback" } }, "Markdown": { "completions": { "words": "fallback" } }, }, ``` Or disable Word based completions everywhere with: ```json "completions": { "words": "fallback" }, ``` Release Notes: - Disable word-completions by default in Plain Text and Markdown Buffers --- assets/settings/default.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/assets/settings/default.json b/assets/settings/default.json index 9693a474e746a10c39d6462d2b7f67707d105ecf..dc1040e1d0ab97520c198f5427fe6da0c8fbfd46 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1603,6 +1603,9 @@ "use_on_type_format": false, "allow_rewrap": "anywhere", "soft_wrap": "editor_width", + "completions": { + "words": "disabled" + }, "prettier": { "allowed": true } @@ -1616,6 +1619,9 @@ } }, "Plain Text": { + "completions": { + "words": "disabled" + }, "allow_rewrap": "anywhere" }, "Python": { From 01bdef130b03c6e2f48386d86173eaeac378f3db Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Tue, 8 Jul 2025 11:05:11 -0700 Subject: [PATCH 093/239] nix: Fix generate-licenses failure (#34072) We should maybe add `generate-licenses` to the sensitivity list for running nix in CI given that nix uses a workaround for it. Release Notes: - N/A --- script/generate-licenses | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/generate-licenses b/script/generate-licenses index 7ae0f1c3f60b93eb500ffa5e127a96c82d954b72..771ce2363d0e46856393d0aea3d72d484519353a 100755 --- a/script/generate-licenses +++ b/script/generate-licenses @@ -36,8 +36,9 @@ fi echo "Generating cargo licenses" if [ -z "${ALLOW_MISSING_LICENSES-}" ]; then FAIL_FLAG=--fail; else FAIL_FLAG=""; fi +if [ -z "${ALLOW_MISSING_LICENSES-}" ]; then WRAPPER=fail_on_stderr; else WRAPPER=""; fi set -x -fail_on_stderr cargo about generate \ +$WRAPPER cargo about generate \ $FAIL_FLAG \ -c script/licenses/zed-licenses.toml \ "$TEMPLATE_FILE" >>"$OUTPUT_FILE" From 1220049089cb35c5b7ea89b1231b4049c164718a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 8 Jul 2025 14:44:51 -0400 Subject: [PATCH 094/239] Add feature flag to use `cloud.zed.dev` instead of `llm.zed.dev` (#34076) This PR adds a new `zed-cloud` feature flag that can be used to send traffic to `cloud.zed.dev` instead of `llm.zed.dev`. This is just so Zed staff can test the new infrastructure. When we're ready for prime-time we'll reroute traffic on the server. Release Notes: - N/A --- Cargo.lock | 2 ++ crates/feature_flags/src/feature_flags.rs | 11 ++++++++ crates/http_client/src/http_client.rs | 15 ++++++++-- crates/language_models/Cargo.toml | 1 + crates/language_models/src/provider/cloud.rs | 29 ++++++++++++++++---- crates/web_search_providers/Cargo.toml | 1 + crates/web_search_providers/src/cloud.rs | 13 +++++++-- crates/zeta/src/zeta.rs | 10 +++++-- 8 files changed, 70 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb3e4024bafb4cc432d535c74f464c975e8510e0..07a445eefebc9454217bf70a52d45b65afdea255 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8955,6 +8955,7 @@ dependencies = [ "credentials_provider", "deepseek", "editor", + "feature_flags", "fs", "futures 0.3.31", "google_ai", @@ -18296,6 +18297,7 @@ version = "0.1.0" dependencies = [ "anyhow", "client", + "feature_flags", "futures 0.3.31", "gpui", "http_client", diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index 6c0cb763ef9e8b43b9caf787ded7251c986d816c..2c2bbfe30db1b5ece060a4a21ce30c3e067f167a 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -92,6 +92,17 @@ impl FeatureFlag for JjUiFeatureFlag { const NAME: &'static str = "jj-ui"; } +pub struct ZedCloudFeatureFlag {} + +impl FeatureFlag for ZedCloudFeatureFlag { + const NAME: &'static str = "zed-cloud"; + + fn enabled_for_staff() -> bool { + // Require individual opt-in, for now. + false + } +} + pub trait FeatureFlagViewExt { fn observe_flag(&mut self, window: &Window, callback: F) -> Subscription where diff --git a/crates/http_client/src/http_client.rs b/crates/http_client/src/http_client.rs index 288dec9a31b913c2dc173af76b73e2ae78ec7d2b..c60a56002f5234f1085c65e25cfc0f1549fdbdb1 100644 --- a/crates/http_client/src/http_client.rs +++ b/crates/http_client/src/http_client.rs @@ -226,10 +226,21 @@ impl HttpClientWithUrl { } /// Builds a Zed LLM URL using the given path. - pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result { + pub fn build_zed_llm_url( + &self, + path: &str, + query: &[(&str, &str)], + use_cloud: bool, + ) -> Result { let base_url = self.base_url(); let base_api_url = match base_url.as_ref() { - "https://zed.dev" => "https://llm.zed.dev", + "https://zed.dev" => { + if use_cloud { + "https://cloud.zed.dev" + } else { + "https://llm.zed.dev" + } + } "https://staging.zed.dev" => "https://llm-staging.zed.dev", "http://localhost:3000" => "http://localhost:8787", other => other, diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index 0f248edd574819aee9ac1311ed23de30be48b21e..514443ddec57cff2efa94261f7c94dce25f609cd 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -28,6 +28,7 @@ credentials_provider.workspace = true copilot.workspace = true deepseek = { workspace = true, features = ["schemars"] } editor.workspace = true +feature_flags.workspace = true fs.workspace = true futures.workspace = true google_ai = { workspace = true, features = ["schemars"] } diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 1cd673710c47a745f9a7afc02ac37eb285eb0a68..9b7fee228aa6139859cdb4b54b013223684b8048 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -2,6 +2,7 @@ use anthropic::AnthropicModelMode; use anyhow::{Context as _, Result, anyhow}; use chrono::{DateTime, Utc}; use client::{Client, ModelRequestUsage, UserStore, zed_urls}; +use feature_flags::{FeatureFlagAppExt as _, ZedCloudFeatureFlag}; use futures::{ AsyncBufReadExt, FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream, }; @@ -136,6 +137,7 @@ impl State { cx: &mut Context, ) -> Self { let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx); + let use_cloud = cx.has_flag::(); Self { client: client.clone(), @@ -163,7 +165,7 @@ impl State { .await; } - let response = Self::fetch_models(client, llm_api_token).await?; + let response = Self::fetch_models(client, llm_api_token, use_cloud).await?; cx.update(|cx| { this.update(cx, |this, cx| { let mut models = Vec::new(); @@ -265,13 +267,18 @@ impl State { async fn fetch_models( client: Arc, llm_api_token: LlmApiToken, + use_cloud: bool, ) -> Result { let http_client = &client.http_client(); let token = llm_api_token.acquire(&client).await?; let request = http_client::Request::builder() .method(Method::GET) - .uri(http_client.build_zed_llm_url("/models", &[])?.as_ref()) + .uri( + http_client + .build_zed_llm_url("/models", &[], use_cloud)? + .as_ref(), + ) .header("Authorization", format!("Bearer {token}")) .body(AsyncBody::empty())?; let mut response = http_client @@ -535,6 +542,7 @@ impl CloudLanguageModel { llm_api_token: LlmApiToken, app_version: Option, body: CompletionBody, + use_cloud: bool, ) -> Result { let http_client = &client.http_client(); @@ -542,9 +550,11 @@ impl CloudLanguageModel { let mut refreshed_token = false; loop { - let request_builder = http_client::Request::builder() - .method(Method::POST) - .uri(http_client.build_zed_llm_url("/completions", &[])?.as_ref()); + let request_builder = http_client::Request::builder().method(Method::POST).uri( + http_client + .build_zed_llm_url("/completions", &[], use_cloud)? + .as_ref(), + ); let request_builder = if let Some(app_version) = app_version { request_builder.header(ZED_VERSION_HEADER_NAME, app_version.to_string()) } else { @@ -771,6 +781,7 @@ impl LanguageModel for CloudLanguageModel { let model_id = self.model.id.to_string(); let generate_content_request = into_google(request, model_id.clone(), GoogleModelMode::Default); + let use_cloud = cx.has_flag::(); async move { let http_client = &client.http_client(); let token = llm_api_token.acquire(&client).await?; @@ -786,7 +797,7 @@ impl LanguageModel for CloudLanguageModel { .method(Method::POST) .uri( http_client - .build_zed_llm_url("/count_tokens", &[])? + .build_zed_llm_url("/count_tokens", &[], use_cloud)? .as_ref(), ) .header("Content-Type", "application/json") @@ -835,6 +846,9 @@ impl LanguageModel for CloudLanguageModel { let intent = request.intent; let mode = request.mode; let app_version = cx.update(|cx| AppVersion::global(cx)).ok(); + let use_cloud = cx + .update(|cx| cx.has_flag::()) + .unwrap_or(false); match self.model.provider { zed_llm_client::LanguageModelProvider::Anthropic => { let request = into_anthropic( @@ -872,6 +886,7 @@ impl LanguageModel for CloudLanguageModel { provider_request: serde_json::to_value(&request) .map_err(|e| anyhow!(e))?, }, + use_cloud, ) .await .map_err(|err| match err.downcast::() { @@ -924,6 +939,7 @@ impl LanguageModel for CloudLanguageModel { provider_request: serde_json::to_value(&request) .map_err(|e| anyhow!(e))?, }, + use_cloud, ) .await?; @@ -964,6 +980,7 @@ impl LanguageModel for CloudLanguageModel { provider_request: serde_json::to_value(&request) .map_err(|e| anyhow!(e))?, }, + use_cloud, ) .await?; diff --git a/crates/web_search_providers/Cargo.toml b/crates/web_search_providers/Cargo.toml index 2e052796c48601565e5e6870f9848f8dcd9354b1..208cb63593f0647970c1b576f1733740ee99196c 100644 --- a/crates/web_search_providers/Cargo.toml +++ b/crates/web_search_providers/Cargo.toml @@ -14,6 +14,7 @@ path = "src/web_search_providers.rs" [dependencies] anyhow.workspace = true client.workspace = true +feature_flags.workspace = true futures.workspace = true gpui.workspace = true http_client.workspace = true diff --git a/crates/web_search_providers/src/cloud.rs b/crates/web_search_providers/src/cloud.rs index adf79b0ff68c4d569dbf7cd40951c7c6c9761583..79ccf97e47aacdeaa1da0cc2f063b3937e4f955f 100644 --- a/crates/web_search_providers/src/cloud.rs +++ b/crates/web_search_providers/src/cloud.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use anyhow::{Context as _, Result}; use client::Client; +use feature_flags::{FeatureFlagAppExt as _, ZedCloudFeatureFlag}; use futures::AsyncReadExt as _; use gpui::{App, AppContext, Context, Entity, Subscription, Task}; use http_client::{HttpClient, Method}; @@ -62,7 +63,10 @@ impl WebSearchProvider for CloudWebSearchProvider { let client = state.client.clone(); let llm_api_token = state.llm_api_token.clone(); let body = WebSearchBody { query }; - cx.background_spawn(async move { perform_web_search(client, llm_api_token, body).await }) + let use_cloud = cx.has_flag::(); + cx.background_spawn(async move { + perform_web_search(client, llm_api_token, body, use_cloud).await + }) } } @@ -70,6 +74,7 @@ async fn perform_web_search( client: Arc, llm_api_token: LlmApiToken, body: WebSearchBody, + use_cloud: bool, ) -> Result { const MAX_RETRIES: usize = 3; @@ -86,7 +91,11 @@ async fn perform_web_search( let request = http_client::Request::builder() .method(Method::POST) - .uri(http_client.build_zed_llm_url("/web_search", &[])?.as_ref()) + .uri( + http_client + .build_zed_llm_url("/web_search", &[], use_cloud)? + .as_ref(), + ) .header("Content-Type", "application/json") .header("Authorization", format!("Bearer {token}")) .body(serde_json::to_string(&body)?.into())?; diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 87cd1e604c3fd422c2ea9c218cbed755e72925cf..12d3d4bfbc7aae1c5b2ed2d36c7a89dd1f526723 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -8,6 +8,7 @@ mod rate_completion_modal; pub(crate) use completion_diff_element::*; use db::kvp::KEY_VALUE_STORE; +use feature_flags::{FeatureFlagAppExt as _, ZedCloudFeatureFlag}; pub use init::*; use inline_completion::DataCollectionState; use license_detection::LICENSE_FILES_TO_CHECK; @@ -390,6 +391,7 @@ impl Zeta { let client = self.client.clone(); let llm_token = self.llm_token.clone(); let app_version = AppVersion::global(cx); + let use_cloud = cx.has_flag::(); let buffer = buffer.clone(); @@ -480,6 +482,7 @@ impl Zeta { llm_token, app_version, body, + use_cloud, }) .await; let (response, usage) = match response { @@ -745,6 +748,7 @@ and then another llm_token, app_version, body, + use_cloud, .. } = params; @@ -760,7 +764,7 @@ and then another } else { request_builder.uri( http_client - .build_zed_llm_url("/predict_edits/v2", &[])? + .build_zed_llm_url("/predict_edits/v2", &[], use_cloud)? .as_ref(), ) }; @@ -820,6 +824,7 @@ and then another let client = self.client.clone(); let llm_token = self.llm_token.clone(); let app_version = AppVersion::global(cx); + let use_cloud = cx.has_flag::(); cx.spawn(async move |this, cx| { let http_client = client.http_client(); let mut response = llm_token_retry(&llm_token, &client, |token| { @@ -830,7 +835,7 @@ and then another } else { request_builder.uri( http_client - .build_zed_llm_url("/predict_edits/accept", &[])? + .build_zed_llm_url("/predict_edits/accept", &[], use_cloud)? .as_ref(), ) }; @@ -1126,6 +1131,7 @@ struct PerformPredictEditsParams { pub llm_token: LlmApiToken, pub app_version: SemanticVersion, pub body: PredictEditsBody, + pub use_cloud: bool, } #[derive(Error, Debug)] From 6b7c30d7ad19313a09de107d7c5ae0da4f7efcea Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 8 Jul 2025 14:39:55 -0500 Subject: [PATCH 095/239] keymap_ui: Editor for action input in modal (#34080) Closes #ISSUE Adds a very simple editor for editing action input to the edit keybind modal. No auto-complete yet. Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/settings/src/keymap_file.rs | 8 +- crates/settings_ui/src/keybindings.rs | 206 +++++++++++++++++--------- 2 files changed, 140 insertions(+), 74 deletions(-) diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index b91739ca87c72c13c1cfe283b80c8ef260c31f6a..19bc58ea2342dae25be0bebfcab600771594989c 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -426,12 +426,18 @@ impl KeymapFile { } } + /// Creates a JSON schema generator, suitable for generating json schemas + /// for actions + pub fn action_schema_generator() -> schemars::SchemaGenerator { + schemars::generate::SchemaSettings::draft2019_09().into_generator() + } + pub fn generate_json_schema_for_registered_actions(cx: &mut App) -> Value { // instead of using DefaultDenyUnknownFields, actions typically use // `#[serde(deny_unknown_fields)]` so that these cases are reported as parse failures. This // is because the rest of the keymap will still load in these cases, whereas other settings // files would not. - let mut generator = schemars::generate::SchemaSettings::draft2019_09().into_generator(); + let mut generator = Self::action_schema_generator(); let action_schemas = cx.action_schemas(&mut generator); let deprecations = cx.deprecated_actions_to_preferred_actions(); diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 2dd693c798a7a4c91eb89d9d404cc2becf143197..951dd77541b8c20d6ef40f321a799e0ac7c091f7 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -4,7 +4,7 @@ use std::{ }; use anyhow::{Context as _, anyhow}; -use collections::HashSet; +use collections::{HashMap, HashSet}; use editor::{CompletionProvider, Editor, EditorEvent}; use feature_flags::FeatureFlagViewExt; use fs::Fs; @@ -240,7 +240,7 @@ impl KeymapEditor { Some(Default) => 3, None => 4, }; - return (source_precedence, keybind.action.as_ref()); + return (source_precedence, keybind.action_name.as_ref()); }); } this.selected_index.take(); @@ -261,6 +261,12 @@ impl KeymapEditor { let mut unmapped_action_names = HashSet::from_iter(cx.all_action_names().into_iter().copied()); let action_documentation = cx.action_documentation(); + let mut generator = KeymapFile::action_schema_generator(); + let action_schema = HashMap::from_iter( + cx.action_schemas(&mut generator) + .into_iter() + .filter_map(|(name, schema)| schema.map(|schema| (name, schema))), + ); let mut processed_bindings = Vec::new(); let mut string_match_candidates = Vec::new(); @@ -295,9 +301,10 @@ impl KeymapEditor { processed_bindings.push(ProcessedKeybinding { keystroke_text: keystroke_text.into(), ui_key_binding, - action: action_name.into(), + action_name: action_name.into(), action_input, action_docs, + action_schema: action_schema.get(action_name).cloned(), context: Some(context), source, }); @@ -311,9 +318,10 @@ impl KeymapEditor { processed_bindings.push(ProcessedKeybinding { keystroke_text: empty.clone(), ui_key_binding: None, - action: action_name.into(), + action_name: action_name.into(), action_input: None, action_docs: action_documentation.get(action_name).copied(), + action_schema: action_schema.get(action_name).cloned(), context: None, source: None, }); @@ -326,8 +334,8 @@ impl KeymapEditor { fn update_keybindings(&mut self, cx: &mut Context) { let workspace = self.workspace.clone(); cx.spawn(async move |this, cx| { - let json_language = Self::load_json_language(workspace.clone(), cx).await; - let rust_language = Self::load_rust_language(workspace.clone(), cx).await; + let json_language = load_json_language(workspace.clone(), cx).await; + let rust_language = load_rust_language(workspace.clone(), cx).await; let query = this.update(cx, |this, cx| { let (key_bindings, string_match_candidates) = @@ -353,64 +361,6 @@ impl KeymapEditor { .detach_and_log_err(cx); } - async fn load_json_language( - workspace: WeakEntity, - cx: &mut AsyncApp, - ) -> Arc { - let json_language_task = workspace - .read_with(cx, |workspace, cx| { - workspace - .project() - .read(cx) - .languages() - .language_for_name("JSON") - }) - .context("Failed to load JSON language") - .log_err(); - let json_language = match json_language_task { - Some(task) => task.await.context("Failed to load JSON language").log_err(), - None => None, - }; - return json_language.unwrap_or_else(|| { - Arc::new(Language::new( - LanguageConfig { - name: "JSON".into(), - ..Default::default() - }, - Some(tree_sitter_json::LANGUAGE.into()), - )) - }); - } - - async fn load_rust_language( - workspace: WeakEntity, - cx: &mut AsyncApp, - ) -> Arc { - let rust_language_task = workspace - .read_with(cx, |workspace, cx| { - workspace - .project() - .read(cx) - .languages() - .language_for_name("Rust") - }) - .context("Failed to load Rust language") - .log_err(); - let rust_language = match rust_language_task { - Some(task) => task.await.context("Failed to load Rust language").log_err(), - None => None, - }; - return rust_language.unwrap_or_else(|| { - Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - ..Default::default() - }, - Some(tree_sitter_rust::LANGUAGE.into()), - )) - }); - } - fn dispatch_context(&self, _window: &Window, _cx: &Context) -> KeyContext { let mut dispatch_context = KeyContext::new_with_defaults(); dispatch_context.add("KeymapEditor"); @@ -526,8 +476,10 @@ impl KeymapEditor { self.workspace .update(cx, |workspace, cx| { let fs = workspace.app_state().fs.clone(); + let workspace_weak = cx.weak_entity(); workspace.toggle_modal(window, cx, |window, cx| { - let modal = KeybindingEditorModal::new(keybind.clone(), fs, window, cx); + let modal = + KeybindingEditorModal::new(keybind.clone(), workspace_weak, fs, window, cx); window.focus(&modal.focus_handle(cx)); modal }); @@ -564,7 +516,7 @@ impl KeymapEditor { ) { let action = self .selected_binding() - .map(|binding| binding.action.to_string()); + .map(|binding| binding.action_name.to_string()); let Some(action) = action else { return; }; @@ -576,9 +528,10 @@ impl KeymapEditor { struct ProcessedKeybinding { keystroke_text: SharedString, ui_key_binding: Option, - action: SharedString, + action_name: SharedString, action_input: Option, action_docs: Option<&'static str>, + action_schema: Option, context: Option, source: Option<(KeybindSource, SharedString)>, } @@ -685,10 +638,10 @@ impl Render for KeymapEditor { let binding = &this.keybindings[candidate_id]; let action = div() - .child(binding.action.clone()) + .child(binding.action_name.clone()) .id(("keymap action", index)) .tooltip({ - let action_name = binding.action.clone(); + let action_name = binding.action_name.clone(); let action_docs = binding.action_docs; move |_, cx| { let action_tooltip = Tooltip::new( @@ -828,6 +781,7 @@ struct KeybindingEditorModal { editing_keybind: ProcessedKeybinding, keybind_editor: Entity, context_editor: Entity, + input_editor: Option>, fs: Arc, error: Option, } @@ -845,6 +799,7 @@ impl Focusable for KeybindingEditorModal { impl KeybindingEditorModal { pub fn new( editing_keybind: ProcessedKeybinding, + workspace: WeakEntity, fs: Arc, window: &mut Window, cx: &mut App, @@ -881,11 +836,39 @@ impl KeybindingEditorModal { editor }); + + let input_editor = editing_keybind.action_schema.clone().map(|_schema| { + cx.new(|cx| { + let mut editor = Editor::auto_height_unbounded(1, window, cx); + if let Some(input) = editing_keybind.action_input.clone() { + editor.set_text(input.text, window, cx); + } else { + // TODO: default value from schema? + editor.set_placeholder_text("Action input", cx); + } + cx.spawn(async |editor, cx| { + let json_language = load_json_language(workspace, cx).await; + editor + .update(cx, |editor, cx| { + if let Some(buffer) = editor.buffer().read(cx).as_singleton() { + buffer.update(cx, |buffer, cx| { + buffer.set_language(Some(json_language), cx) + }); + } + }) + .context("Failed to load JSON language for editing keybinding action input") + }) + .detach_and_log_err(cx); + editor + }) + }); + Self { editing_keybind, fs, keybind_editor, context_editor, + input_editor, error: None, } } @@ -964,13 +947,38 @@ impl Render for KeybindingEditorModal { ) .child(self.keybind_editor.clone()), ) + .when_some(self.input_editor.clone(), |this, editor| { + this.child( + v_flex() + .p_3() + .gap_3() + .child( + v_flex().child(Label::new("Edit Input")).child( + Label::new("Input the desired input to the binding.") + .color(Color::Muted), + ), + ) + .child( + div() + .w_full() + .border_color(cx.theme().colors().border_variant) + .border_1() + .py_2() + .px_3() + .min_h_8() + .rounded_md() + .bg(theme.editor_background) + .child(editor), + ), + ) + }) .child( v_flex() .p_3() .gap_3() .child( - v_flex().child(Label::new("Edit Keystroke")).child( - Label::new("Input the desired keystroke for the selected action.") + v_flex().child(Label::new("Edit Context")).child( + Label::new("Input the desired context for the binding.") .color(Color::Muted), ), ) @@ -1081,6 +1089,58 @@ impl CompletionProvider for KeyContextCompletionProvider { } } +async fn load_json_language(workspace: WeakEntity, cx: &mut AsyncApp) -> Arc { + let json_language_task = workspace + .read_with(cx, |workspace, cx| { + workspace + .project() + .read(cx) + .languages() + .language_for_name("JSON") + }) + .context("Failed to load JSON language") + .log_err(); + let json_language = match json_language_task { + Some(task) => task.await.context("Failed to load JSON language").log_err(), + None => None, + }; + return json_language.unwrap_or_else(|| { + Arc::new(Language::new( + LanguageConfig { + name: "JSON".into(), + ..Default::default() + }, + Some(tree_sitter_json::LANGUAGE.into()), + )) + }); +} + +async fn load_rust_language(workspace: WeakEntity, cx: &mut AsyncApp) -> Arc { + let rust_language_task = workspace + .read_with(cx, |workspace, cx| { + workspace + .project() + .read(cx) + .languages() + .language_for_name("Rust") + }) + .context("Failed to load Rust language") + .log_err(); + let rust_language = match rust_language_task { + Some(task) => task.await.context("Failed to load Rust language").log_err(), + None => None, + }; + return rust_language.unwrap_or_else(|| { + Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + )) + }); +} + async fn save_keybinding_update( existing: ProcessedKeybinding, new_keystrokes: &[Keystroke], @@ -1113,7 +1173,7 @@ async fn save_keybinding_update( target: settings::KeybindUpdateTarget { context: existing_context, keystrokes: existing_keystrokes, - action_name: &existing.action, + action_name: &existing.action_name, use_key_equivalents: false, input, }, @@ -1124,7 +1184,7 @@ async fn save_keybinding_update( source: settings::KeybindUpdateTarget { context: new_context, keystrokes: new_keystrokes, - action_name: &existing.action, + action_name: &existing.action_name, use_key_equivalents: false, input, }, From 139af027377a0436a253cdc226c1aec5fed1d5af Mon Sep 17 00:00:00 2001 From: Dino Date: Tue, 8 Jul 2025 21:48:48 +0100 Subject: [PATCH 096/239] vim: Fix and improve horizontal scrolling (#33590) This Pull Request introduces various changes to the editor's horizontal scrolling, mostly focused on vim mode's horizontal scroll motions (`z l`, `z h`, `z shift-l`, `z shift-h`). In order to make it easier to review, the logical changes have been split into different sections. ## Cursor Position Update Changes introduced on https://github.com/zed-industries/zed/pull/32558 added both `z l` and `z h` to vim mode but it only scrolled the editor's content, without changing the cursor position. This doesn't reflect the actual behavior of those motions in vim, so these two commits tackled that, ensuring that the cursor position is updated, only when the cursor is on the left or right edges of the editor: - https://github.com/zed-industries/zed/commit/ea3b866a763ba0bcfc12999ee1741c6528c895b7 - https://github.com/zed-industries/zed/commit/805f41a913c6e86ef8be550d363a3cc2caeccbe9 ## Horizontal Autoscroll Fix After introducing the cursor position update to both `z l` and `z h` it was noted that there was a bug with using `z l`, followed by `0` and then `z l` again, as on the second use `z l` the cursor would not be updated. This would only happen on the first line in the editor, and it was concluded that it was because the `editor::scroll::autoscroll::Editor.autoscroll_horizontally` method was directly updating the scroll manager's anchor offset, instead of using the `editor::scroll::Editor.set_scroll_position_internal` method, like is being done by the vertical autoscroll (`editor::scroll::autoscroll::Editor.autoscroll_vertically`). This wouldn't update the scroll manager's anchor, which would still think it was at `(0, 1)` so the cursor position would not be updated. The changes in [this commit](https://github.com/zed-industries/zed/commit/3957f02e189018ef559cd28516f0b872026b0ce1) updated the horizontal autoscrolling method to also leverage `set_scroll_position_internal`. ## Visible Column Count & Page Width Scroll Amount The changes in https://github.com/zed-industries/zed/commit/d83652c3ae1d0356fd7dca7b8922a0116de39ff0 add a `visible_column_count` field to `editor::scroll::ScrollManager` struct, which allowed the introduction of the `ScrollAmount::PageWidth` enum. With these changes, two new actions are introduced, `vim::normal::scroll::HalfPageRight` and `vim::normal::scroll::HalfPageLeft` (in https://github.com/zed-industries/zed/commit/7f344304d56337654a34b1b461a1dc69defd2e4e), which move the editor half page to the right and half page to the left, as well as the cursor position, which have also been mapped to `z shift-l` and `z shift-h`, respectively. Closes #17219 Release Notes: - Improved `z l` and `z h` to actually move the cursor position, similar to vim's behavior - Added `z shift-l` and `z shift-h` to scroll half of the page width's to the right or to the left, respectively --------- Co-authored-by: Conrad Irwin --- assets/keymaps/vim.json | 2 + crates/editor/src/element.rs | 3 + crates/editor/src/scroll.rs | 82 ++++++++++++-- crates/editor/src/scroll/autoscroll.rs | 21 ++-- crates/editor/src/scroll/scroll_amount.rs | 7 +- crates/vim/src/normal/scroll.rs | 105 ++++++++++++++++-- .../vim/test_data/test_horizontal_scroll.json | 16 +++ 7 files changed, 209 insertions(+), 27 deletions(-) create mode 100644 crates/vim/test_data/test_horizontal_scroll.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index d90813e3d6f59192e866fabb13451b8ce8e6114f..4b48b26ef48536632942c03bd84000e089e01f62 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -189,6 +189,8 @@ "z shift-r": "editor::UnfoldAll", "z l": "vim::ColumnRight", "z h": "vim::ColumnLeft", + "z shift-l": "vim::HalfPageRight", + "z shift-h": "vim::HalfPageLeft", "shift-z shift-q": ["pane::CloseActiveItem", { "save_intent": "skip" }], "shift-z shift-z": ["pane::CloseActiveItem", { "save_intent": "save_all" }], // Count support diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 4c53d7f28a7d183af449308e259e8a6a3f694198..a4463383359fedd5e199b0f65eabaa80f005838d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -7944,6 +7944,7 @@ impl Element for EditorElement { editor.last_bounds = Some(bounds); editor.gutter_dimensions = gutter_dimensions; editor.set_visible_line_count(bounds.size.height / line_height, window, cx); + editor.set_visible_column_count(editor_content_width / em_advance); if matches!( editor.mode, @@ -8449,6 +8450,7 @@ impl Element for EditorElement { scroll_width, em_advance, &line_layouts, + window, cx, ) } else { @@ -8603,6 +8605,7 @@ impl Element for EditorElement { scroll_width, em_advance, &line_layouts, + window, cx, ) } else { diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 0642b2b20ebfb7213f74ab6980889a7e07218415..b3007d3091d79074b99b3fe6f2d7b00003f72015 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -13,6 +13,7 @@ use crate::{ pub use autoscroll::{Autoscroll, AutoscrollStrategy}; use core::fmt::Debug; use gpui::{App, Axis, Context, Global, Pixels, Task, Window, point, px}; +use language::language_settings::{AllLanguageSettings, SoftWrap}; use language::{Bias, Point}; pub use scroll_amount::ScrollAmount; use settings::Settings; @@ -151,12 +152,16 @@ pub struct ScrollManager { pub(crate) vertical_scroll_margin: f32, anchor: ScrollAnchor, ongoing: OngoingScroll, + /// The second element indicates whether the autoscroll request is local + /// (true) or remote (false). Local requests are initiated by user actions, + /// while remote requests come from external sources. autoscroll_request: Option<(Autoscroll, bool)>, last_autoscroll: Option<(gpui::Point, f32, f32, AutoscrollStrategy)>, show_scrollbars: bool, hide_scrollbar_task: Option>, active_scrollbar: Option, visible_line_count: Option, + visible_column_count: Option, forbid_vertical_scroll: bool, minimap_thumb_state: Option, } @@ -173,6 +178,7 @@ impl ScrollManager { active_scrollbar: None, last_autoscroll: None, visible_line_count: None, + visible_column_count: None, forbid_vertical_scroll: false, minimap_thumb_state: None, } @@ -210,7 +216,7 @@ impl ScrollManager { window: &mut Window, cx: &mut Context, ) { - let (new_anchor, top_row) = if scroll_position.y <= 0. { + let (new_anchor, top_row) = if scroll_position.y <= 0. && scroll_position.x <= 0. { ( ScrollAnchor { anchor: Anchor::min(), @@ -218,6 +224,22 @@ impl ScrollManager { }, 0, ) + } else if scroll_position.y <= 0. { + let buffer_point = map + .clip_point( + DisplayPoint::new(DisplayRow(0), scroll_position.x as u32), + Bias::Left, + ) + .to_point(map); + let anchor = map.buffer_snapshot.anchor_at(buffer_point, Bias::Right); + + ( + ScrollAnchor { + anchor: anchor, + offset: scroll_position.max(&gpui::Point::default()), + }, + 0, + ) } else { let scroll_top = scroll_position.y; let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line { @@ -242,8 +264,13 @@ impl ScrollManager { } }; - let scroll_top_buffer_point = - DisplayPoint::new(DisplayRow(scroll_top as u32), 0).to_point(map); + let scroll_top_row = DisplayRow(scroll_top as u32); + let scroll_top_buffer_point = map + .clip_point( + DisplayPoint::new(scroll_top_row, scroll_position.x as u32), + Bias::Left, + ) + .to_point(map); let top_anchor = map .buffer_snapshot .anchor_at(scroll_top_buffer_point, Bias::Right); @@ -476,6 +503,10 @@ impl Editor { .map(|line_count| line_count as u32 - 1) } + pub fn visible_column_count(&self) -> Option { + self.scroll_manager.visible_column_count + } + pub(crate) fn set_visible_line_count( &mut self, lines: f32, @@ -497,6 +528,10 @@ impl Editor { } } + pub(crate) fn set_visible_column_count(&mut self, columns: f32) { + self.scroll_manager.visible_column_count = Some(columns); + } + pub fn apply_scroll_delta( &mut self, scroll_delta: gpui::Point, @@ -675,25 +710,48 @@ impl Editor { let Some(visible_line_count) = self.visible_line_count() else { return; }; + let Some(mut visible_column_count) = self.visible_column_count() else { + return; + }; + + // If the user has a preferred line length, and has the editor + // configured to wrap at the preferred line length, or bounded to it, + // use that value over the visible column count. This was mostly done so + // that tests could actually be written for vim's `z l`, `z h`, `z + // shift-l` and `z shift-h` commands, as there wasn't a good way to + // configure the editor to only display a certain number of columns. If + // that ever happens, this could probably be removed. + let settings = AllLanguageSettings::get_global(cx); + if matches!( + settings.defaults.soft_wrap, + SoftWrap::PreferredLineLength | SoftWrap::Bounded + ) { + if (settings.defaults.preferred_line_length as f32) < visible_column_count { + visible_column_count = settings.defaults.preferred_line_length as f32; + } + } // If the scroll position is currently at the left edge of the document // (x == 0.0) and the intent is to scroll right, the gutter's margin // should first be added to the current position, otherwise the cursor // will end at the column position minus the margin, which looks off. - if current_position.x == 0.0 && amount.columns() > 0. { + if current_position.x == 0.0 && amount.columns(visible_column_count) > 0. { if let Some(last_position_map) = &self.last_position_map { current_position.x += self.gutter_dimensions.margin / last_position_map.em_advance; } } - let new_position = - current_position + point(amount.columns(), amount.lines(visible_line_count)); + let new_position = current_position + + point( + amount.columns(visible_column_count), + amount.lines(visible_line_count), + ); self.set_scroll_position(new_position, window, cx); } /// Returns an ordering. The newest selection is: /// Ordering::Equal => on screen - /// Ordering::Less => above the screen - /// Ordering::Greater => below the screen + /// Ordering::Less => above or to the left of the screen + /// Ordering::Greater => below or to the right of the screen pub fn newest_selection_on_screen(&self, cx: &mut App) -> Ordering { let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let newest_head = self @@ -711,8 +769,12 @@ impl Editor { return Ordering::Less; } - if let Some(visible_lines) = self.visible_line_count() { - if newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32) { + if let (Some(visible_lines), Some(visible_columns)) = + (self.visible_line_count(), self.visible_column_count()) + { + if newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32) + && newest_head.column() <= screen_top.column() + visible_columns as u32 + { return Ordering::Equal; } } diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index 55998aa2fd4081f1679ea603c0065fca08910b02..340277633a2c63131997f9eca76316ccf6c3ad39 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -274,12 +274,14 @@ impl Editor { start_row: DisplayRow, viewport_width: Pixels, scroll_width: Pixels, - max_glyph_width: Pixels, + em_advance: Pixels, layouts: &[LineWithInvisibles], + window: &mut Window, cx: &mut Context, ) -> bool { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let selections = self.selections.all::(cx); + let mut scroll_position = self.scroll_manager.scroll_position(&display_map); let mut target_left; let mut target_right; @@ -295,16 +297,17 @@ impl Editor { if head.row() >= start_row && head.row() < DisplayRow(start_row.0 + layouts.len() as u32) { - let start_column = head.column().saturating_sub(3); - let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3); + let start_column = head.column(); + let end_column = cmp::min(display_map.line_len(head.row()), head.column()); target_left = target_left.min( layouts[head.row().minus(start_row) as usize] - .x_for_index(start_column as usize), + .x_for_index(start_column as usize) + + self.gutter_dimensions.margin, ); target_right = target_right.max( layouts[head.row().minus(start_row) as usize] .x_for_index(end_column as usize) - + max_glyph_width, + + em_advance, ); } } @@ -319,14 +322,16 @@ impl Editor { return false; } - let scroll_left = self.scroll_manager.anchor.offset.x * max_glyph_width; + let scroll_left = self.scroll_manager.anchor.offset.x * em_advance; let scroll_right = scroll_left + viewport_width; if target_left < scroll_left { - self.scroll_manager.anchor.offset.x = target_left / max_glyph_width; + scroll_position.x = target_left / em_advance; + self.set_scroll_position_internal(scroll_position, true, true, window, cx); true } else if target_right > scroll_right { - self.scroll_manager.anchor.offset.x = (target_right - viewport_width) / max_glyph_width; + scroll_position.x = (target_right - viewport_width) / em_advance; + self.set_scroll_position_internal(scroll_position, true, true, window, cx); true } else { false diff --git a/crates/editor/src/scroll/scroll_amount.rs b/crates/editor/src/scroll/scroll_amount.rs index bc9d4757f1d6b30192c5888e5a3d576ea34fec25..b2af4f8e4fbce899c6aee317402ee1365cee8600 100644 --- a/crates/editor/src/scroll/scroll_amount.rs +++ b/crates/editor/src/scroll/scroll_amount.rs @@ -23,6 +23,8 @@ pub enum ScrollAmount { Page(f32), // Scroll N columns (positive is towards the right of the document) Column(f32), + // Scroll N page width (positive is towards the right of the document) + PageWidth(f32), } impl ScrollAmount { @@ -37,14 +39,16 @@ impl ScrollAmount { (visible_line_count * count).trunc() } Self::Column(_count) => 0.0, + Self::PageWidth(_count) => 0.0, } } - pub fn columns(&self) -> f32 { + pub fn columns(&self, visible_column_count: f32) -> f32 { match self { Self::Line(_count) => 0.0, Self::Page(_count) => 0.0, Self::Column(count) => *count, + Self::PageWidth(count) => (visible_column_count * count).trunc(), } } @@ -58,6 +62,7 @@ impl ScrollAmount { // so I'm leaving this at 0.0 for now to try and make it clear that // this should not have an impact on that? ScrollAmount::Column(_) => px(0.0), + ScrollAmount::PageWidth(_) => px(0.0), } } diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index 150334376b0e6a6f26bd2e8afb63243e9c67dd2e..47b9fe92fd92eee83c01a5ca0041e67d912d7b4f 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -7,6 +7,7 @@ use editor::{ use gpui::{Context, Window, actions}; use language::Bias; use settings::Settings; +use text::SelectionGoal; actions!( vim, @@ -26,7 +27,11 @@ actions!( /// Scrolls up by one page. PageUp, /// Scrolls down by one page. - PageDown + PageDown, + /// Scrolls right by half a page's width. + HalfPageRight, + /// Scrolls left by half a page's width. + HalfPageLeft, ] ); @@ -51,6 +56,16 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &PageUp, window, cx| { vim.scroll(false, window, cx, |c| ScrollAmount::Page(-c.unwrap_or(1.))) }); + Vim::action(editor, cx, |vim, _: &HalfPageRight, window, cx| { + vim.scroll(false, window, cx, |c| { + ScrollAmount::PageWidth(c.unwrap_or(0.5)) + }) + }); + Vim::action(editor, cx, |vim, _: &HalfPageLeft, window, cx| { + vim.scroll(false, window, cx, |c| { + ScrollAmount::PageWidth(-c.unwrap_or(0.5)) + }) + }); Vim::action(editor, cx, |vim, _: &ScrollDown, window, cx| { vim.scroll(true, window, cx, |c| { if let Some(c) = c { @@ -123,6 +138,10 @@ fn scroll_editor( return; }; + let Some(visible_column_count) = editor.visible_column_count() else { + return; + }; + let top_anchor = editor.scroll_manager.anchor().anchor; let vertical_scroll_margin = EditorSettings::get_global(cx).vertical_scroll_margin; @@ -132,8 +151,14 @@ fn scroll_editor( cx, |s| { s.move_with(|map, selection| { + // TODO: Improve the logic and function calls below to be dependent on + // the `amount`. If the amount is vertical, we don't care about + // columns, while if it's horizontal, we don't care about rows, + // so we don't need to calculate both and deal with logic for + // both. let mut head = selection.head(); let top = top_anchor.to_display_point(map); + let max_point = map.max_point(); let starting_column = head.column(); let vertical_scroll_margin = @@ -163,9 +188,8 @@ fn scroll_editor( (visible_line_count as u32).saturating_sub(1 + vertical_scroll_margin), ); // scroll off the end. - let max_row = if top.row().0 + visible_line_count as u32 >= map.max_point().row().0 - { - map.max_point().row() + let max_row = if top.row().0 + visible_line_count as u32 >= max_point.row().0 { + max_point.row() } else { DisplayRow( (top.row().0 + visible_line_count as u32) @@ -185,13 +209,52 @@ fn scroll_editor( } else { head.row() }; - let new_head = - map.clip_point(DisplayPoint::new(new_row, starting_column), Bias::Left); + + // The minimum column position that the cursor position can be + // at is either the scroll manager's anchor column, which is the + // left-most column in the visible area, or the scroll manager's + // old anchor column, in case the cursor position is being + // preserved. This is necessary for motions like `ctrl-d` in + // case there's not enough content to scroll half page down, in + // which case the scroll manager's anchor column will be the + // maximum column for the current line, so the minimum column + // would end up being the same as the maximum column. + let min_column = match preserve_cursor_position { + true => old_top_anchor.to_display_point(map).column(), + false => top.column(), + }; + + // As for the maximum column position, that should be either the + // right-most column in the visible area, which we can easily + // calculate by adding the visible column count to the minimum + // column position, or the right-most column in the current + // line, seeing as the cursor might be in a short line, in which + // case we don't want to go past its last column. + let max_row_column = map.line_len(new_row); + let max_column = match min_column + visible_column_count as u32 { + max_column if max_column >= max_row_column => max_row_column, + max_column => max_column, + }; + + // Ensure that the cursor's column stays within the visible + // area, otherwise clip it at either the left or right edge of + // the visible area. + let new_column = match (min_column, max_column) { + (min_column, _) if starting_column < min_column => min_column, + (_, max_column) if starting_column > max_column => max_column, + _ => starting_column, + }; + + let new_head = map.clip_point(DisplayPoint::new(new_row, new_column), Bias::Left); + let goal = match amount { + ScrollAmount::Column(_) | ScrollAmount::PageWidth(_) => SelectionGoal::None, + _ => selection.goal, + }; if selection.is_empty() { - selection.collapse_to(new_head, selection.goal) + selection.collapse_to(new_head, goal) } else { - selection.set_head(new_head, selection.goal) + selection.set_head(new_head, goal) }; }) }, @@ -472,4 +535,30 @@ mod test { cx.simulate_shared_keystrokes("ctrl-o").await; cx.shared_state().await.assert_matches(); } + + #[gpui::test] + async fn test_horizontal_scroll(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_scroll_height(20).await; + cx.set_shared_wrap(12).await; + cx.set_neovim_option("nowrap").await; + + let content = "ˇ01234567890123456789"; + cx.set_shared_state(&content).await; + + cx.simulate_shared_keystrokes("z shift-l").await; + cx.shared_state().await.assert_eq("012345ˇ67890123456789"); + + // At this point, `z h` should not move the cursor as it should still be + // visible within the 12 column width. + cx.simulate_shared_keystrokes("z h").await; + cx.shared_state().await.assert_eq("012345ˇ67890123456789"); + + let content = "ˇ01234567890123456789"; + cx.set_shared_state(&content).await; + + cx.simulate_shared_keystrokes("z l").await; + cx.shared_state().await.assert_eq("0ˇ1234567890123456789"); + } } diff --git a/crates/vim/test_data/test_horizontal_scroll.json b/crates/vim/test_data/test_horizontal_scroll.json new file mode 100644 index 0000000000000000000000000000000000000000..c6cbac8be5498281f1aba6698401fba57441e924 --- /dev/null +++ b/crates/vim/test_data/test_horizontal_scroll.json @@ -0,0 +1,16 @@ +{"SetOption":{"value":"scrolloff=3"}} +{"SetOption":{"value":"lines=22"}} +{"SetOption":{"value":"wrap"}} +{"SetOption":{"value":"columns=12"}} +{"SetOption":{"value":"nowrap"}} +{"Put":{"state":"ˇ01234567890123456789"}} +{"Key":"z"} +{"Key":"shift-l"} +{"Get":{"state":"012345ˇ67890123456789","mode":"Normal"}} +{"Key":"z"} +{"Key":"h"} +{"Get":{"state":"012345ˇ67890123456789","mode":"Normal"}} +{"Put":{"state":"ˇ01234567890123456789"}} +{"Key":"z"} +{"Key":"l"} +{"Get":{"state":"0ˇ1234567890123456789","mode":"Normal"}} From ad8b823555d6d2642a07d3256bc4c210de4b40b5 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 8 Jul 2025 19:51:24 -0300 Subject: [PATCH 097/239] Improve the LSP popover menu design (#34081) - Add a slightly different bolt icon SVG so it sits better when with an indicator - Attempt to clarify what happens when clicking any of the menu items - Add descriptions to the tooltips to clarify what each indicator color means - Add section titles to clarify in which category each menu item is sitting on Release Notes: - N/A --- assets/icons/bolt_filled_alt.svg | 2 +- crates/language_tools/src/lsp_tool.rs | 65 ++++++++++++++++++++++----- 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/assets/icons/bolt_filled_alt.svg b/assets/icons/bolt_filled_alt.svg index 3c8938736279684981b03d168b11272d4e196d24..141e1c5f577bbd9bdc661de6629f863bfc760de9 100644 --- a/assets/icons/bolt_filled_alt.svg +++ b/assets/icons/bolt_filled_alt.svg @@ -1,3 +1,3 @@ - + diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_tool.rs index 6cd2f83184d946fdbf133f5d7d17d188d294ee13..81cc38d33f6c7f19b304a4b52043329d448fd45a 100644 --- a/crates/language_tools/src/lsp_tool.rs +++ b/crates/language_tools/src/lsp_tool.rs @@ -185,15 +185,18 @@ impl LanguageServerState { menu = menu.separator().item(button); continue; }; + let Some(server_info) = item.server_info() else { continue; }; + let workspace = self.workspace.clone(); let server_selector = server_info.server_selector(); // TODO currently, Zed remote does not work well with the LSP logs // https://github.com/zed-industries/zed/issues/28557 let has_logs = lsp_store.read(cx).as_local().is_some() && lsp_logs.read(cx).has_server_logs(&server_selector); + let status_color = server_info .binary_status .and_then(|binary_status| match binary_status.status { @@ -218,16 +221,40 @@ impl LanguageServerState { .other_servers_start_index .is_some_and(|index| index == i) { - menu = menu.separator(); + menu = menu.separator().header("Other Buffers"); + } + + if i == 0 && self.other_servers_start_index.is_some() { + menu = menu.header("Current Buffer"); } + menu = menu.item(ContextMenuItem::custom_entry( move |_, _| { h_flex() - .gap_1() + .group("menu_item") .w_full() - .child(Indicator::dot().color(status_color)) - .child(Label::new(server_info.name.0.clone())) - .when(!has_logs, |div| div.cursor_default()) + .gap_2() + .justify_between() + .child( + h_flex() + .gap_2() + .child(Indicator::dot().color(status_color)) + .child(Label::new(server_info.name.0.clone())), + ) + .child( + h_flex() + .visible_on_hover("menu_item") + .child( + Label::new("View Logs") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child( + Icon::new(IconName::ChevronRight) + .size(IconSize::Small) + .color(Color::Muted), + ), + ) .into_any_element() }, { @@ -836,17 +863,27 @@ impl Render for LspTool { } } - let indicator = if has_errors { - Some(Indicator::dot().color(Color::Error)) + let (indicator, description) = if has_errors { + ( + Some(Indicator::dot().color(Color::Error)), + "Server with errors", + ) } else if has_warnings { - Some(Indicator::dot().color(Color::Warning)) + ( + Some(Indicator::dot().color(Color::Warning)), + "Server with warnings", + ) } else if has_other_notifications { - Some(Indicator::dot().color(Color::Modified)) + ( + Some(Indicator::dot().color(Color::Modified)), + "Server with notifications", + ) } else { - None + (None, "All Servers Operational") }; let lsp_tool = cx.entity().clone(); + div().child( PopoverMenu::new("lsp-tool") .menu(move |_, cx| lsp_tool.read(cx).lsp_menu.clone()) @@ -858,7 +895,13 @@ impl Render for LspTool { .icon_size(IconSize::Small) .indicator_border_color(Some(cx.theme().colors().status_bar_background)), move |window, cx| { - Tooltip::for_action("Language Servers", &ToggleMenu, window, cx) + Tooltip::with_meta( + "Language Servers", + Some(&ToggleMenu), + description, + window, + cx, + ) }, ), ) From 3a247ee94760541906488b4145148964fce709a3 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 9 Jul 2025 05:28:25 +0530 Subject: [PATCH 098/239] project panel: Add indent guides for sticky items (#34092) - Adds new trait `StickyItemsDecoration` in `sticky_items` which is implemented by `IndentGuides` from `indent_guides`. image Release Notes: - N/A --- crates/gpui/src/elements/uniform_list.rs | 29 -- crates/outline_panel/src/outline_panel.rs | 91 ++-- crates/project_panel/src/project_panel.rs | 153 +++--- crates/storybook/src/stories/indent_guides.rs | 38 +- crates/ui/src/components/indent_guides.rs | 451 ++++++++++-------- crates/ui/src/components/sticky_items.rs | 214 +++++++-- 6 files changed, 578 insertions(+), 398 deletions(-) diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 342490b882b65843549d1261679a6e4dbf204e66..52e2015c20f9983e78c126cc920ed115eef0fd7a 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -506,35 +506,6 @@ pub trait UniformListDecoration { ) -> AnyElement; } -/// A trait for implementing top slots in a [`UniformList`]. -/// Top slots are elements that appear at the top of the list and can adjust -/// the visible range of list items. -pub trait UniformListTopSlot { - /// Returns elements to render at the top slot for the given visible range. - fn compute( - &mut self, - visible_range: Range, - window: &mut Window, - cx: &mut App, - ) -> SmallVec<[AnyElement; 8]>; - - /// Layout and prepaint the top slot elements. - fn prepaint( - &self, - elements: &mut SmallVec<[AnyElement; 8]>, - bounds: Bounds, - item_height: Pixels, - scroll_offset: Point, - padding: crate::Edges, - can_scroll_horizontally: bool, - window: &mut Window, - cx: &mut App, - ); - - /// Paint the top slot elements. - fn paint(&self, elements: &mut SmallVec<[AnyElement; 8]>, window: &mut Window, cx: &mut App); -} - impl UniformList { /// Selects a specific list item for measurement. pub fn with_width_from_item(mut self, item_index: Option) -> Self { diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 05352e24def8a4aefd399d6ce764b6afbfedbaf1..12dcab9e8702a98dbcecd8549ce40fe86fa45e0f 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -4584,53 +4584,52 @@ impl OutlinePanel { .track_scroll(self.scroll_handle.clone()) .when(show_indent_guides, |list| { list.with_decoration( - ui::indent_guides( - cx.entity().clone(), - px(indent_size), - IndentGuideColors::panel(cx), - |outline_panel, range, _, _| { - let entries = outline_panel.cached_entries.get(range); - if let Some(entries) = entries { - entries.into_iter().map(|item| item.depth).collect() - } else { - smallvec::SmallVec::new() - } - }, - ) - .with_render_fn( - cx.entity().clone(), - move |outline_panel, params, _, _| { - const LEFT_OFFSET: Pixels = px(14.); - - let indent_size = params.indent_size; - let item_height = params.item_height; - let active_indent_guide_ix = find_active_indent_guide_ix( - outline_panel, - ¶ms.indent_guides, - ); + ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx)) + .with_compute_indents_fn( + cx.entity().clone(), + |outline_panel, range, _, _| { + let entries = outline_panel.cached_entries.get(range); + if let Some(entries) = entries { + entries.into_iter().map(|item| item.depth).collect() + } else { + smallvec::SmallVec::new() + } + }, + ) + .with_render_fn( + cx.entity().clone(), + move |outline_panel, params, _, _| { + const LEFT_OFFSET: Pixels = px(14.); + + let indent_size = params.indent_size; + let item_height = params.item_height; + let active_indent_guide_ix = find_active_indent_guide_ix( + outline_panel, + ¶ms.indent_guides, + ); - params - .indent_guides - .into_iter() - .enumerate() - .map(|(ix, layout)| { - let bounds = Bounds::new( - point( - layout.offset.x * indent_size + LEFT_OFFSET, - layout.offset.y * item_height, - ), - size(px(1.), layout.length * item_height), - ); - ui::RenderedIndentGuide { - bounds, - layout, - is_active: active_indent_guide_ix == Some(ix), - hitbox: None, - } - }) - .collect() - }, - ), + params + .indent_guides + .into_iter() + .enumerate() + .map(|(ix, layout)| { + let bounds = Bounds::new( + point( + layout.offset.x * indent_size + LEFT_OFFSET, + layout.offset.y * item_height, + ), + size(px(1.), layout.length * item_height), + ); + ui::RenderedIndentGuide { + bounds, + layout, + is_active: active_indent_guide_ix == Some(ix), + hitbox: None, + } + }) + .collect() + }, + ), ) }) }; diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index bd0d5e3919b02875077d251626fc759c2f2f6d38..0ec9bac33f89c81527e520322555d6d1071273b4 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -3947,7 +3947,7 @@ impl ProjectPanel { false } }); - let shadow_color_top = hsla(0.0, 0.0, 0.0, 0.15); + let shadow_color_top = hsla(0.0, 0.0, 0.0, 0.1); let shadow_color_bottom = hsla(0.0, 0.0, 0.0, 0.); let sticky_shadow = div() .absolute() @@ -4176,6 +4176,16 @@ impl ProjectPanel { } } else if kind.is_dir() { this.marked_entries.clear(); + if is_sticky { + if let Some((_, _, index)) = this.index_for_entry(entry_id, worktree_id) { + let strategy = sticky_index + .map(ScrollStrategy::ToPosition) + .unwrap_or(ScrollStrategy::Top); + this.scroll_handle.scroll_to_item(index, strategy); + cx.notify(); + return; + } + } if event.modifiers().alt { this.toggle_expand_all(entry_id, window, cx); } else { @@ -4188,16 +4198,6 @@ impl ProjectPanel { let allow_preview = preview_tabs_enabled && click_count == 1; this.open_entry(entry_id, focus_opened_item, allow_preview, cx); } - - if is_sticky { - if let Some((_, _, index)) = this.index_for_entry(entry_id, worktree_id) { - let strategy = sticky_index - .map(ScrollStrategy::ToPosition) - .unwrap_or(ScrollStrategy::Top); - this.scroll_handle.scroll_to_item(index, strategy); - cx.notify(); - } - } }), ) .child( @@ -5167,52 +5167,51 @@ impl Render for ProjectPanel { }) .when(show_indent_guides, |list| { list.with_decoration( - ui::indent_guides( - cx.entity().clone(), - px(indent_size), - IndentGuideColors::panel(cx), - |this, range, window, cx| { - let mut items = - SmallVec::with_capacity(range.end - range.start); - this.iter_visible_entries( - range, - window, - cx, - |entry, _, entries, _, _| { - let (depth, _) = Self::calculate_depth_and_difference( - entry, entries, - ); - items.push(depth); - }, - ); - items - }, - ) - .on_click(cx.listener( - |this, active_indent_guide: &IndentGuideLayout, window, cx| { - if window.modifiers().secondary() { - let ix = active_indent_guide.offset.y; - let Some((target_entry, worktree)) = maybe!({ - let (worktree_id, entry) = this.entry_at_index(ix)?; - let worktree = this - .project - .read(cx) - .worktree_for_id(worktree_id, cx)?; - let target_entry = worktree - .read(cx) - .entry_for_path(&entry.path.parent()?)?; - Some((target_entry, worktree)) - }) else { - return; - }; - - this.collapse_entry(target_entry.clone(), worktree, cx); - } - }, - )) - .with_render_fn( - cx.entity().clone(), - move |this, params, _, cx| { + ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx)) + .with_compute_indents_fn( + cx.entity().clone(), + |this, range, window, cx| { + let mut items = + SmallVec::with_capacity(range.end - range.start); + this.iter_visible_entries( + range, + window, + cx, + |entry, _, entries, _, _| { + let (depth, _) = + Self::calculate_depth_and_difference( + entry, entries, + ); + items.push(depth); + }, + ); + items + }, + ) + .on_click(cx.listener( + |this, active_indent_guide: &IndentGuideLayout, window, cx| { + if window.modifiers().secondary() { + let ix = active_indent_guide.offset.y; + let Some((target_entry, worktree)) = maybe!({ + let (worktree_id, entry) = + this.entry_at_index(ix)?; + let worktree = this + .project + .read(cx) + .worktree_for_id(worktree_id, cx)?; + let target_entry = worktree + .read(cx) + .entry_for_path(&entry.path.parent()?)?; + Some((target_entry, worktree)) + }) else { + return; + }; + + this.collapse_entry(target_entry.clone(), worktree, cx); + } + }, + )) + .with_render_fn(cx.entity().clone(), move |this, params, _, cx| { const LEFT_OFFSET: Pixels = px(14.); const PADDING_Y: Pixels = px(4.); const HITBOX_OVERDRAW: Pixels = px(3.); @@ -5260,12 +5259,11 @@ impl Render for ProjectPanel { } }) .collect() - }, - ), + }), ) }) .when(show_sticky_scroll, |list| { - list.with_decoration(ui::sticky_items( + let sticky_items = ui::sticky_items( cx.entity().clone(), |this, range, window, cx| { let mut items = SmallVec::with_capacity(range.end - range.start); @@ -5286,7 +5284,40 @@ impl Render for ProjectPanel { |this, marker_entry, window, cx| { this.render_sticky_entries(marker_entry, window, cx) }, - )) + ); + list.with_decoration(if show_indent_guides { + sticky_items.with_decoration( + ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx)) + .with_render_fn(cx.entity().clone(), move |_, params, _, _| { + const LEFT_OFFSET: Pixels = px(14.); + + let indent_size = params.indent_size; + let item_height = params.item_height; + + params + .indent_guides + .into_iter() + .map(|layout| { + let bounds = Bounds::new( + point( + layout.offset.x * indent_size + LEFT_OFFSET, + layout.offset.y * item_height, + ), + size(px(1.), layout.length * item_height), + ); + ui::RenderedIndentGuide { + bounds, + layout, + is_active: false, + hitbox: None, + } + }) + .collect() + }), + ) + } else { + sticky_items + }) }) .size_full() .with_sizing_behavior(ListSizingBehavior::Infer) diff --git a/crates/storybook/src/stories/indent_guides.rs b/crates/storybook/src/stories/indent_guides.rs index e83c9ed3837b49c4c701d4434ca1533fef83a5d7..e4f9669b1fd9172205511a487d20895443305d2e 100644 --- a/crates/storybook/src/stories/indent_guides.rs +++ b/crates/storybook/src/stories/indent_guides.rs @@ -55,23 +55,27 @@ impl Render for IndentGuidesStory { }), ) .with_sizing_behavior(gpui::ListSizingBehavior::Infer) - .with_decoration(ui::indent_guides( - cx.entity().clone(), - px(16.), - ui::IndentGuideColors { - default: Color::Info.color(cx), - hover: Color::Accent.color(cx), - active: Color::Accent.color(cx), - }, - |this, range, _cx, _context| { - this.depths - .iter() - .skip(range.start) - .take(range.end - range.start) - .cloned() - .collect() - }, - )), + .with_decoration( + ui::indent_guides( + px(16.), + ui::IndentGuideColors { + default: Color::Info.color(cx), + hover: Color::Accent.color(cx), + active: Color::Accent.color(cx), + }, + ) + .with_compute_indents_fn( + cx.entity().clone(), + |this, range, _cx, _context| { + this.depths + .iter() + .skip(range.start) + .take(range.end - range.start) + .cloned() + .collect() + }, + ), + ), ), ) } diff --git a/crates/ui/src/components/indent_guides.rs b/crates/ui/src/components/indent_guides.rs index 6d4db984f938454c4f72f06ae63490b78e014e85..e3dc1f35fa8a8509d8a03eab3b6fea37f7df42e7 100644 --- a/crates/ui/src/components/indent_guides.rs +++ b/crates/ui/src/components/indent_guides.rs @@ -1,8 +1,7 @@ use std::{cmp::Ordering, ops::Range, rc::Rc}; -use gpui::{ - AnyElement, App, Bounds, Entity, Hsla, Point, UniformListDecoration, fill, point, size, -}; +use gpui::{AnyElement, App, Bounds, Entity, Hsla, Point, fill, point, size}; +use gpui::{DispatchPhase, Hitbox, HitboxBehavior, MouseButton, MouseDownEvent, MouseMoveEvent}; use smallvec::SmallVec; use crate::prelude::*; @@ -32,7 +31,8 @@ impl IndentGuideColors { pub struct IndentGuides { colors: IndentGuideColors, indent_size: Pixels, - compute_indents_fn: Box, &mut Window, &mut App) -> SmallVec<[usize; 64]>>, + compute_indents_fn: + Option, &mut Window, &mut App) -> SmallVec<[usize; 64]>>>, render_fn: Option< Box< dyn Fn( @@ -45,25 +45,11 @@ pub struct IndentGuides { on_click: Option>, } -pub fn indent_guides( - entity: Entity, - indent_size: Pixels, - colors: IndentGuideColors, - compute_indents_fn: impl Fn( - &mut V, - Range, - &mut Window, - &mut Context, - ) -> SmallVec<[usize; 64]> - + 'static, -) -> IndentGuides { - let compute_indents_fn = Box::new(move |range, window: &mut Window, cx: &mut App| { - entity.update(cx, |this, cx| compute_indents_fn(this, range, window, cx)) - }); +pub fn indent_guides(indent_size: Pixels, colors: IndentGuideColors) -> IndentGuides { IndentGuides { colors, indent_size, - compute_indents_fn, + compute_indents_fn: None, render_fn: None, on_click: None, } @@ -79,6 +65,25 @@ impl IndentGuides { self } + /// Sets the function that computes indents for uniform list decoration. + pub fn with_compute_indents_fn( + mut self, + entity: Entity, + compute_indents_fn: impl Fn( + &mut V, + Range, + &mut Window, + &mut Context, + ) -> SmallVec<[usize; 64]> + + 'static, + ) -> Self { + let compute_indents_fn = Box::new(move |range, window: &mut Window, cx: &mut App| { + entity.update(cx, |this, cx| compute_indents_fn(this, range, window, cx)) + }); + self.compute_indents_fn = Some(compute_indents_fn); + self + } + /// Sets a custom callback that will be called when the indent guides need to be rendered. pub fn with_render_fn( mut self, @@ -97,6 +102,53 @@ impl IndentGuides { self.render_fn = Some(Box::new(render_fn)); self } + + fn render_from_layout( + &self, + indent_guides: SmallVec<[IndentGuideLayout; 12]>, + bounds: Bounds, + item_height: Pixels, + window: &mut Window, + cx: &mut App, + ) -> AnyElement { + let mut indent_guides = if let Some(ref custom_render) = self.render_fn { + let params = RenderIndentGuideParams { + indent_guides, + indent_size: self.indent_size, + item_height, + }; + custom_render(params, window, cx) + } else { + indent_guides + .into_iter() + .map(|layout| RenderedIndentGuide { + bounds: Bounds::new( + point( + layout.offset.x * self.indent_size, + layout.offset.y * item_height, + ), + size(px(1.), layout.length * item_height), + ), + layout, + is_active: false, + hitbox: None, + }) + .collect() + }; + for guide in &mut indent_guides { + guide.bounds.origin += bounds.origin; + if let Some(hitbox) = guide.hitbox.as_mut() { + hitbox.origin += bounds.origin; + } + } + + let indent_guides = IndentGuidesElement { + indent_guides: Rc::new(indent_guides), + colors: self.colors.clone(), + on_hovered_indent_guide_click: self.on_click.clone(), + }; + indent_guides.into_any_element() + } } /// Parameters for rendering indent guides. @@ -136,9 +188,7 @@ pub struct IndentGuideLayout { /// Implements the necessary functionality for rendering indent guides inside a uniform list. mod uniform_list { - use gpui::{ - DispatchPhase, Hitbox, HitboxBehavior, MouseButton, MouseDownEvent, MouseMoveEvent, - }; + use gpui::UniformListDecoration; use super::*; @@ -161,227 +211,212 @@ mod uniform_list { if includes_trailing_indent { visible_range.end += 1; } - let visible_entries = &(self.compute_indents_fn)(visible_range.clone(), window, cx); + let Some(ref compute_indents_fn) = self.compute_indents_fn else { + panic!("compute_indents_fn is required for UniformListDecoration"); + }; + let visible_entries = &compute_indents_fn(visible_range.clone(), window, cx); let indent_guides = compute_indent_guides( &visible_entries, visible_range.start, includes_trailing_indent, ); - let mut indent_guides = if let Some(ref custom_render) = self.render_fn { - let params = RenderIndentGuideParams { - indent_guides, - indent_size: self.indent_size, - item_height, - }; - custom_render(params, window, cx) - } else { - indent_guides - .into_iter() - .map(|layout| RenderedIndentGuide { - bounds: Bounds::new( - point( - layout.offset.x * self.indent_size, - layout.offset.y * item_height, - ), - size(px(1.), layout.length * item_height), - ), - layout, - is_active: false, - hitbox: None, - }) - .collect() - }; - for guide in &mut indent_guides { - guide.bounds.origin += bounds.origin; - if let Some(hitbox) = guide.hitbox.as_mut() { - hitbox.origin += bounds.origin; - } - } - - let indent_guides = IndentGuidesElement { - indent_guides: Rc::new(indent_guides), - colors: self.colors.clone(), - on_hovered_indent_guide_click: self.on_click.clone(), - }; - indent_guides.into_any_element() + self.render_from_layout(indent_guides, bounds, item_height, window, cx) } } +} - struct IndentGuidesElement { - colors: IndentGuideColors, - indent_guides: Rc>, - on_hovered_indent_guide_click: - Option>, - } +/// Implements the necessary functionality for rendering indent guides inside a sticky items. +mod sticky_items { + use crate::StickyItemsDecoration; - enum IndentGuidesElementPrepaintState { - Static, - Interactive { - hitboxes: Rc>, - on_hovered_indent_guide_click: Rc, - }, + use super::*; + + impl StickyItemsDecoration for IndentGuides { + fn compute( + &self, + indents: &SmallVec<[usize; 8]>, + bounds: Bounds, + _scroll_offset: Point, + item_height: Pixels, + window: &mut Window, + cx: &mut App, + ) -> AnyElement { + let indent_guides = compute_indent_guides(&indents, 0, false); + self.render_from_layout(indent_guides, bounds, item_height, window, cx) + } } +} - impl Element for IndentGuidesElement { - type RequestLayoutState = (); - type PrepaintState = IndentGuidesElementPrepaintState; +struct IndentGuidesElement { + colors: IndentGuideColors, + indent_guides: Rc>, + on_hovered_indent_guide_click: Option>, +} - fn id(&self) -> Option { - None - } +enum IndentGuidesElementPrepaintState { + Static, + Interactive { + hitboxes: Rc>, + on_hovered_indent_guide_click: Rc, + }, +} - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } +impl Element for IndentGuidesElement { + type RequestLayoutState = (); + type PrepaintState = IndentGuidesElementPrepaintState; - fn request_layout( - &mut self, - _id: Option<&gpui::GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (gpui::LayoutId, Self::RequestLayoutState) { - (window.request_layout(gpui::Style::default(), [], cx), ()) - } + fn id(&self) -> Option { + None + } - fn prepaint( - &mut self, - _id: Option<&gpui::GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - _bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - _cx: &mut App, - ) -> Self::PrepaintState { - if let Some(on_hovered_indent_guide_click) = self.on_hovered_indent_guide_click.clone() - { - let hitboxes = self - .indent_guides - .as_ref() - .iter() - .map(|guide| { - window.insert_hitbox( - guide.hitbox.unwrap_or(guide.bounds), - HitboxBehavior::Normal, - ) - }) - .collect(); - Self::PrepaintState::Interactive { - hitboxes: Rc::new(hitboxes), - on_hovered_indent_guide_click, - } - } else { - Self::PrepaintState::Static + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&gpui::GlobalElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (gpui::LayoutId, Self::RequestLayoutState) { + (window.request_layout(gpui::Style::default(), [], cx), ()) + } + + fn prepaint( + &mut self, + _id: Option<&gpui::GlobalElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + _cx: &mut App, + ) -> Self::PrepaintState { + if let Some(on_hovered_indent_guide_click) = self.on_hovered_indent_guide_click.clone() { + let hitboxes = self + .indent_guides + .as_ref() + .iter() + .map(|guide| { + window + .insert_hitbox(guide.hitbox.unwrap_or(guide.bounds), HitboxBehavior::Normal) + }) + .collect(); + Self::PrepaintState::Interactive { + hitboxes: Rc::new(hitboxes), + on_hovered_indent_guide_click, } + } else { + Self::PrepaintState::Static } + } - fn paint( - &mut self, - _id: Option<&gpui::GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - _bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - prepaint: &mut Self::PrepaintState, - window: &mut Window, - _cx: &mut App, - ) { - let current_view = window.current_view(); - - match prepaint { - IndentGuidesElementPrepaintState::Static => { - for indent_guide in self.indent_guides.as_ref() { - let fill_color = if indent_guide.is_active { - self.colors.active - } else { - self.colors.default - }; - - window.paint_quad(fill(indent_guide.bounds, fill_color)); - } + fn paint( + &mut self, + _id: Option<&gpui::GlobalElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + prepaint: &mut Self::PrepaintState, + window: &mut Window, + _cx: &mut App, + ) { + let current_view = window.current_view(); + + match prepaint { + IndentGuidesElementPrepaintState::Static => { + for indent_guide in self.indent_guides.as_ref() { + let fill_color = if indent_guide.is_active { + self.colors.active + } else { + self.colors.default + }; + + window.paint_quad(fill(indent_guide.bounds, fill_color)); } - IndentGuidesElementPrepaintState::Interactive { - hitboxes, - on_hovered_indent_guide_click, - } => { - window.on_mouse_event({ - let hitboxes = hitboxes.clone(); - let indent_guides = self.indent_guides.clone(); - let on_hovered_indent_guide_click = on_hovered_indent_guide_click.clone(); - move |event: &MouseDownEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble && event.button == MouseButton::Left { - let mut active_hitbox_ix = None; - for (i, hitbox) in hitboxes.iter().enumerate() { - if hitbox.is_hovered(window) { - active_hitbox_ix = Some(i); - break; - } + } + IndentGuidesElementPrepaintState::Interactive { + hitboxes, + on_hovered_indent_guide_click, + } => { + window.on_mouse_event({ + let hitboxes = hitboxes.clone(); + let indent_guides = self.indent_guides.clone(); + let on_hovered_indent_guide_click = on_hovered_indent_guide_click.clone(); + move |event: &MouseDownEvent, phase, window, cx| { + if phase == DispatchPhase::Bubble && event.button == MouseButton::Left { + let mut active_hitbox_ix = None; + for (i, hitbox) in hitboxes.iter().enumerate() { + if hitbox.is_hovered(window) { + active_hitbox_ix = Some(i); + break; } + } - let Some(active_hitbox_ix) = active_hitbox_ix else { - return; - }; + let Some(active_hitbox_ix) = active_hitbox_ix else { + return; + }; - let active_indent_guide = &indent_guides[active_hitbox_ix].layout; - on_hovered_indent_guide_click(active_indent_guide, window, cx); + let active_indent_guide = &indent_guides[active_hitbox_ix].layout; + on_hovered_indent_guide_click(active_indent_guide, window, cx); - cx.stop_propagation(); - window.prevent_default(); - } + cx.stop_propagation(); + window.prevent_default(); } - }); - let mut hovered_hitbox_id = None; - for (i, hitbox) in hitboxes.iter().enumerate() { - window.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox); - let indent_guide = &self.indent_guides[i]; - let fill_color = if hitbox.is_hovered(window) { - hovered_hitbox_id = Some(hitbox.id); - self.colors.hover - } else if indent_guide.is_active { - self.colors.active - } else { - self.colors.default - }; - - window.paint_quad(fill(indent_guide.bounds, fill_color)); } + }); + let mut hovered_hitbox_id = None; + for (i, hitbox) in hitboxes.iter().enumerate() { + window.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox); + let indent_guide = &self.indent_guides[i]; + let fill_color = if hitbox.is_hovered(window) { + hovered_hitbox_id = Some(hitbox.id); + self.colors.hover + } else if indent_guide.is_active { + self.colors.active + } else { + self.colors.default + }; + + window.paint_quad(fill(indent_guide.bounds, fill_color)); + } - window.on_mouse_event({ - let prev_hovered_hitbox_id = hovered_hitbox_id; - let hitboxes = hitboxes.clone(); - move |_: &MouseMoveEvent, phase, window, cx| { - let mut hovered_hitbox_id = None; - for hitbox in hitboxes.as_ref() { - if hitbox.is_hovered(window) { - hovered_hitbox_id = Some(hitbox.id); - break; - } + window.on_mouse_event({ + let prev_hovered_hitbox_id = hovered_hitbox_id; + let hitboxes = hitboxes.clone(); + move |_: &MouseMoveEvent, phase, window, cx| { + let mut hovered_hitbox_id = None; + for hitbox in hitboxes.as_ref() { + if hitbox.is_hovered(window) { + hovered_hitbox_id = Some(hitbox.id); + break; } - if phase == DispatchPhase::Capture { - // If the hovered hitbox has changed, we need to re-paint the indent guides. - match (prev_hovered_hitbox_id, hovered_hitbox_id) { - (Some(prev_id), Some(id)) => { - if prev_id != id { - cx.notify(current_view) - } + } + if phase == DispatchPhase::Capture { + // If the hovered hitbox has changed, we need to re-paint the indent guides. + match (prev_hovered_hitbox_id, hovered_hitbox_id) { + (Some(prev_id), Some(id)) => { + if prev_id != id { + cx.notify(current_view) } - (None, Some(_)) => cx.notify(current_view), - (Some(_), None) => cx.notify(current_view), - (None, None) => {} } + (None, Some(_)) => cx.notify(current_view), + (Some(_), None) => cx.notify(current_view), + (None, None) => {} } } - }); - } + } + }); } } } +} - impl IntoElement for IndentGuidesElement { - type Element = Self; +impl IntoElement for IndentGuidesElement { + type Element = Self; - fn into_element(self) -> Self::Element { - self - } + fn into_element(self) -> Self::Element { + self } } diff --git a/crates/ui/src/components/sticky_items.rs b/crates/ui/src/components/sticky_items.rs index e98e3023d291aee6df3ea83a75af898de3359738..da6c14ff0974716ba5fb8423a8ade07349cea36f 100644 --- a/crates/ui/src/components/sticky_items.rs +++ b/crates/ui/src/components/sticky_items.rs @@ -3,7 +3,7 @@ use std::{ops::Range, rc::Rc}; use gpui::{ AnyElement, App, AvailableSpace, Bounds, Context, Element, ElementId, Entity, GlobalElementId, InspectorElementId, IntoElement, LayoutId, Pixels, Point, Render, Style, UniformListDecoration, - Window, point, size, + Window, point, px, size, }; use smallvec::SmallVec; @@ -11,10 +11,10 @@ pub trait StickyCandidate { fn depth(&self) -> usize; } -#[derive(Clone)] pub struct StickyItems { compute_fn: Rc, &mut Window, &mut App) -> SmallVec<[T; 8]>>, render_fn: Rc SmallVec<[AnyElement; 8]>>, + decorations: Vec>, } pub fn sticky_items( @@ -44,11 +44,26 @@ where StickyItems { compute_fn, render_fn, + decorations: Vec::new(), + } +} + +impl StickyItems +where + T: StickyCandidate + Clone + 'static, +{ + /// Adds a decoration element to the sticky items. + pub fn with_decoration(mut self, decoration: impl StickyItemsDecoration + 'static) -> Self { + self.decorations.push(Box::new(decoration)); + self } } struct StickyItemsElement { - elements: SmallVec<[AnyElement; 8]>, + drifting_element: Option, + drifting_decoration: Option, + rest_elements: SmallVec<[AnyElement; 8]>, + rest_decorations: SmallVec<[AnyElement; 1]>, } impl IntoElement for StickyItemsElement { @@ -103,8 +118,16 @@ impl Element for StickyItemsElement { window: &mut Window, cx: &mut App, ) { - // reverse so that last item is bottom most among sticky items - for item in self.elements.iter_mut().rev() { + if let Some(ref mut drifting_element) = self.drifting_element { + drifting_element.paint(window, cx); + } + if let Some(ref mut drifting_decoration) = self.drifting_decoration { + drifting_decoration.paint(window, cx); + } + for item in self.rest_elements.iter_mut().rev() { + item.paint(window, cx); + } + for item in self.rest_decorations.iter_mut() { item.paint(window, cx); } } @@ -125,11 +148,14 @@ where cx: &mut App, ) -> AnyElement { let entries = (self.compute_fn)(visible_range.clone(), window, cx); - let mut elements = SmallVec::new(); - let mut anchor_entry = None; + struct StickyAnchor { + entry: T, + index: usize, + } + + let mut sticky_anchor = None; let mut last_item_is_drifting = false; - let mut anchor_index = None; let mut iter = entries.iter().enumerate().peekable(); while let Some((ix, current_entry)) = iter.next() { @@ -137,7 +163,10 @@ where let index_in_range = ix; if current_depth < index_in_range { - anchor_entry = Some(current_entry.clone()); + sticky_anchor = Some(StickyAnchor { + entry: current_entry.clone(), + index: visible_range.start + ix, + }); break; } @@ -146,44 +175,155 @@ where if next_depth < current_depth && next_depth < index_in_range { last_item_is_drifting = true; - anchor_index = Some(visible_range.start + ix); - anchor_entry = Some(current_entry.clone()); + sticky_anchor = Some(StickyAnchor { + entry: current_entry.clone(), + index: visible_range.start + ix, + }); break; } } } - if let Some(anchor_entry) = anchor_entry { - elements = (self.render_fn)(anchor_entry, window, cx); - let items_count = elements.len(); - - for (ix, element) in elements.iter_mut().enumerate() { - let mut item_y_offset = None; - if ix == items_count - 1 && last_item_is_drifting { - if let Some(anchor_index) = anchor_index { - let scroll_top = -scroll_offset.y; - let anchor_top = item_height * anchor_index; - let sticky_area_height = item_height * items_count; - item_y_offset = - Some((anchor_top - scroll_top - sticky_area_height).min(Pixels::ZERO)); - }; - } + let Some(sticky_anchor) = sticky_anchor else { + return StickyItemsElement { + drifting_element: None, + drifting_decoration: None, + rest_elements: SmallVec::new(), + rest_decorations: SmallVec::new(), + } + .into_any_element(); + }; + + let anchor_depth = sticky_anchor.entry.depth(); + let mut elements = (self.render_fn)(sticky_anchor.entry, window, cx); + let items_count = elements.len(); + + let indents: SmallVec<[usize; 8]> = { + elements + .iter() + .enumerate() + .map(|(ix, _)| anchor_depth.saturating_sub(items_count.saturating_sub(ix))) + .collect() + }; + + let mut last_decoration_element = None; + let mut rest_decoration_elements = SmallVec::new(); + + let available_space = size( + AvailableSpace::Definite(bounds.size.width), + AvailableSpace::Definite(bounds.size.height), + ); + + let drifting_y_offset = if last_item_is_drifting { + let scroll_top = -scroll_offset.y; + let anchor_top = item_height * sticky_anchor.index; + let sticky_area_height = item_height * items_count; + (anchor_top - scroll_top - sticky_area_height).min(Pixels::ZERO) + } else { + Pixels::ZERO + }; + + let (drifting_indent, rest_indents) = if last_item_is_drifting && !indents.is_empty() { + let last = indents[indents.len() - 1]; + let rest: SmallVec<[usize; 8]> = indents[..indents.len() - 1].iter().copied().collect(); + (Some(last), rest) + } else { + (None, indents) + }; - let sticky_origin = bounds.origin - + point( - -scroll_offset.x, - -scroll_offset.y + item_height * ix + item_y_offset.unwrap_or(Pixels::ZERO), - ); + for decoration in &self.decorations { + if let Some(drifting_indent) = drifting_indent { + let drifting_indent_vec: SmallVec<[usize; 8]> = + [drifting_indent].into_iter().collect(); + let sticky_origin = bounds.origin - scroll_offset + + point(px(0.), item_height * rest_indents.len() + drifting_y_offset); + let decoration_bounds = Bounds::new(sticky_origin, bounds.size); - let available_space = size( - AvailableSpace::Definite(bounds.size.width), - AvailableSpace::Definite(item_height), + let mut drifting_dec = decoration.as_ref().compute( + &drifting_indent_vec, + decoration_bounds, + scroll_offset, + item_height, + window, + cx, ); - element.layout_as_root(available_space, window, cx); - element.prepaint_at(sticky_origin, window, cx); + drifting_dec.layout_as_root(available_space, window, cx); + drifting_dec.prepaint_at(sticky_origin, window, cx); + last_decoration_element = Some(drifting_dec); } + + if !rest_indents.is_empty() { + let decoration_bounds = Bounds::new(bounds.origin - scroll_offset, bounds.size); + let mut rest_dec = decoration.as_ref().compute( + &rest_indents, + decoration_bounds, + scroll_offset, + item_height, + window, + cx, + ); + rest_dec.layout_as_root(available_space, window, cx); + rest_dec.prepaint_at(bounds.origin, window, cx); + rest_decoration_elements.push(rest_dec); + } + } + + let (mut drifting_element, mut rest_elements) = + if last_item_is_drifting && !elements.is_empty() { + let last = elements.pop().unwrap(); + (Some(last), elements) + } else { + (None, elements) + }; + + for (ix, element) in rest_elements.iter_mut().enumerate() { + let sticky_origin = bounds.origin - scroll_offset + point(px(0.), item_height * ix); + let element_available_space = size( + AvailableSpace::Definite(bounds.size.width), + AvailableSpace::Definite(item_height), + ); + + element.layout_as_root(element_available_space, window, cx); + element.prepaint_at(sticky_origin, window, cx); + } + + if let Some(ref mut drifting_element) = drifting_element { + let sticky_origin = bounds.origin - scroll_offset + + point( + px(0.), + item_height * rest_elements.len() + drifting_y_offset, + ); + let element_available_space = size( + AvailableSpace::Definite(bounds.size.width), + AvailableSpace::Definite(item_height), + ); + + drifting_element.layout_as_root(element_available_space, window, cx); + drifting_element.prepaint_at(sticky_origin, window, cx); } - StickyItemsElement { elements }.into_any_element() + StickyItemsElement { + drifting_element, + drifting_decoration: last_decoration_element, + rest_elements, + rest_decorations: rest_decoration_elements, + } + .into_any_element() } } + +/// A decoration for a [`StickyItems`]. This can be used for various things, +/// such as rendering indent guides, or other visual effects. +pub trait StickyItemsDecoration { + /// Compute the decoration element, given the visible range of list items, + /// the bounds of the list, and the height of each item. + fn compute( + &self, + indents: &SmallVec<[usize; 8]>, + bounds: Bounds, + scroll_offset: Point, + item_height: Pixels, + window: &mut Window, + cx: &mut App, + ) -> AnyElement; +} From df57754baf64b118972ee3c42f68a0acc23989ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Wed, 9 Jul 2025 08:57:03 +0800 Subject: [PATCH 099/239] windows: Publish nightly (#24800) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The installer, uninstaller, and the Zed binary files are all signed using Microsoft’s newly launched Trusted Signing service. For demonstration purposes, I have used my own account for the signing process. For more information about Trusted Signing, you can refer to the following links: - [Microsoft Security Blog: Trusted Signing is in Public Preview](https://techcommunity.microsoft.com/blog/microsoft-security-blog/trusted-signing-is-in-public-preview/4103457) - [Overview of Azure Trusted Signing](https://learn.microsoft.com/en-us/azure/trusted-signing/overview) **TODO:** - [x] `InnoSetup` script to setup an installer - [x] Signing process - [x] `Open with Zed` in right click context menu (by using sparse package) - [x] Integrate with `cli` - [x] Implement `cli` (#25412) - [x] Pack `cli.exe` into installer - [x] Implement auto updating (#25734) - [x] Pack autoupdater helper into installer - [x] Implement dock menus - [x] Add `Recent Documents` entries (#26369) - [x] Make `zed.exe` aware of sigle instance (#25412) - [x] Properly handle dock menu events (#26010) - [x] Handle `zed://***` uri **Materials needed:** - [ ] Icons - [ ] App icon for all channels (#9571) - [ ] Associated file icons, at minimum a default icon ([example](https://github.com/microsoft/vscode/tree/main/resources/win32)) - [ ] Logos for installer wizard - [ ] Icons for appx - [x] Code signing - [x] Secrets: AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, ACCOUNT_NAME, CERT_PROFILE_NAME - [x] Other constants: ENDPOINT, Identity Signature (i.e. `CN=Junkui Zhang, O=Junkui Zhang, L=Wuhan, S=Hubei, C=CN`) ![屏幕截图 2025-02-13 205132](https://github.com/user-attachments/assets/925ec5b2-c8f4-4f0e-8666-26e30278eb3d) https://github.com/user-attachments/assets/4f1092b4-90fc-4a47-a868-8f2f1a5d8ad8 Release Notes: - N/A --------- Co-authored-by: Kate Co-authored-by: localcc Co-authored-by: Peter Tripp Co-authored-by: Max Brunsfeld --- .cargo/config.toml | 2 + .../install_trusted_signing/action.yml | 64 + .github/workflows/ci.yml | 75 +- .github/workflows/release_nightly.yml | 71 + Cargo.lock | 10 + Cargo.toml | 3 + crates/auto_update/src/auto_update.rs | 2 +- crates/auto_update_helper/app-icon.ico | Bin 0 -> 590611 bytes crates/cli/src/main.rs | 7 + .../AppxManifest-Nightly.xml | 78 + .../AppxManifest-Preview.xml | 78 + .../AppxManifest.xml | 79 + crates/explorer_command_injector/Cargo.toml | 28 + crates/explorer_command_injector/LICENSE-GPL | 1 + .../src/explorer_command_injector.rs | 201 +++ crates/gpui/Cargo.toml | 2 +- crates/zed/build.rs | 12 +- crates/zed/resources/windows/app-icon-dev.ico | Bin 0 -> 156580 bytes .../resources/windows/app-icon-nightly.ico | Bin 0 -> 159619 bytes .../resources/windows/app-icon-preview.ico | Bin 0 -> 155339 bytes .../windows/messages/Default.zh-cn.isl | 403 +++++ crates/zed/resources/windows/messages/en.isl | 15 + .../zed/resources/windows/messages/zh-cn.isl | 9 + crates/zed/resources/windows/sign.ps1 | 53 + crates/zed/resources/windows/zed.iss | 1412 +++++++++++++++++ script/bundle-windows.ps1 | 263 +++ script/clear-target-dir-if-larger-than.ps1 | 2 +- script/determine-release-channel.ps1 | 37 + script/get-crate-version.ps1 | 16 + script/lib/blob-store.ps1 | 68 + script/lib/workspace.ps1 | 6 + script/upload-nightly.ps1 | 60 + typos.toml | 2 + 33 files changed, 3040 insertions(+), 19 deletions(-) create mode 100644 .github/actions/install_trusted_signing/action.yml create mode 100644 crates/explorer_command_injector/AppxManifest-Nightly.xml create mode 100644 crates/explorer_command_injector/AppxManifest-Preview.xml create mode 100644 crates/explorer_command_injector/AppxManifest.xml create mode 100644 crates/explorer_command_injector/Cargo.toml create mode 120000 crates/explorer_command_injector/LICENSE-GPL create mode 100644 crates/explorer_command_injector/src/explorer_command_injector.rs create mode 100644 crates/zed/resources/windows/app-icon-dev.ico create mode 100644 crates/zed/resources/windows/app-icon-nightly.ico create mode 100644 crates/zed/resources/windows/app-icon-preview.ico create mode 100644 crates/zed/resources/windows/messages/Default.zh-cn.isl create mode 100644 crates/zed/resources/windows/messages/en.isl create mode 100644 crates/zed/resources/windows/messages/zh-cn.isl create mode 100644 crates/zed/resources/windows/sign.ps1 create mode 100644 crates/zed/resources/windows/zed.iss create mode 100644 script/bundle-windows.ps1 create mode 100644 script/determine-release-channel.ps1 create mode 100644 script/get-crate-version.ps1 create mode 100644 script/lib/blob-store.ps1 create mode 100644 script/lib/workspace.ps1 create mode 100644 script/upload-nightly.ps1 diff --git a/.cargo/config.toml b/.cargo/config.toml index 717c5e18c8d294bacf65207bc6b8ecb7dba1b152..8db58d238003c29df6dbc9fa733c6d5521340103 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -19,6 +19,8 @@ rustflags = [ "windows_slim_errors", # This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes "-C", "target-feature=+crt-static", # This fixes the linking issue when compiling livekit on Windows + "-C", + "link-arg=-fuse-ld=lld", ] [env] diff --git a/.github/actions/install_trusted_signing/action.yml b/.github/actions/install_trusted_signing/action.yml new file mode 100644 index 0000000000000000000000000000000000000000..a99ff08eb1eb1f1b92cdea2c374a62b2384b2237 --- /dev/null +++ b/.github/actions/install_trusted_signing/action.yml @@ -0,0 +1,64 @@ +name: "Trusted Signing on Windows" +description: "Install trusted signing on Windows." + +# Modified from https://github.com/Azure/trusted-signing-action +runs: + using: "composite" + steps: + - name: Set variables + id: set-variables + shell: "pwsh" + run: | + $defaultPath = $env:PSModulePath -split ';' | Select-Object -First 1 + "PSMODULEPATH=$defaultPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + + "TRUSTED_SIGNING_MODULE_VERSION=0.5.3" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "BUILD_TOOLS_NUGET_VERSION=10.0.22621.3233" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "TRUSTED_SIGNING_NUGET_VERSION=1.0.53" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + "DOTNET_SIGNCLI_NUGET_VERSION=0.9.1-beta.24469.1" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + + - name: Cache TrustedSigning PowerShell module + id: cache-module + uses: actions/cache@v4 + env: + cache-name: cache-module + with: + path: ${{ steps.set-variables.outputs.PSMODULEPATH }}\TrustedSigning\${{ steps.set-variables.outputs.TRUSTED_SIGNING_MODULE_VERSION }} + key: TrustedSigning-${{ steps.set-variables.outputs.TRUSTED_SIGNING_MODULE_VERSION }} + if: ${{ inputs.cache-dependencies == 'true' }} + + - name: Cache Microsoft.Windows.SDK.BuildTools NuGet package + id: cache-buildtools + uses: actions/cache@v4 + env: + cache-name: cache-buildtools + with: + path: ~\AppData\Local\TrustedSigning\Microsoft.Windows.SDK.BuildTools\Microsoft.Windows.SDK.BuildTools.${{ steps.set-variables.outputs.BUILD_TOOLS_NUGET_VERSION }} + key: Microsoft.Windows.SDK.BuildTools-${{ steps.set-variables.outputs.BUILD_TOOLS_NUGET_VERSION }} + if: ${{ inputs.cache-dependencies == 'true' }} + + - name: Cache Microsoft.Trusted.Signing.Client NuGet package + id: cache-tsclient + uses: actions/cache@v4 + env: + cache-name: cache-tsclient + with: + path: ~\AppData\Local\TrustedSigning\Microsoft.Trusted.Signing.Client\Microsoft.Trusted.Signing.Client.${{ steps.set-variables.outputs.TRUSTED_SIGNING_NUGET_VERSION }} + key: Microsoft.Trusted.Signing.Client-${{ steps.set-variables.outputs.TRUSTED_SIGNING_NUGET_VERSION }} + if: ${{ inputs.cache-dependencies == 'true' }} + + - name: Cache SignCli NuGet package + id: cache-signcli + uses: actions/cache@v4 + env: + cache-name: cache-signcli + with: + path: ~\AppData\Local\TrustedSigning\sign\sign.${{ steps.set-variables.outputs.DOTNET_SIGNCLI_NUGET_VERSION }} + key: SignCli-${{ steps.set-variables.outputs.DOTNET_SIGNCLI_NUGET_VERSION }} + if: ${{ inputs.cache-dependencies == 'true' }} + + - name: Install Trusted Signing module + shell: "pwsh" + run: | + Install-Module -Name TrustedSigning -RequiredVersion ${{ steps.set-variables.outputs.TRUSTED_SIGNING_MODULE_VERSION }} -Force -Repository PSGallery + if: ${{ inputs.cache-dependencies != 'true' || steps.cache-module.outputs.cache-hit != 'true' }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84c7a9682819408c95fb8dfaa1f424bd3f847a9a..25a1ed86702c92a7e77d5a9214d13afd15b1e591 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -411,11 +411,10 @@ jobs: with: clean: false - - name: Setup Cargo and Rustup + - name: Configure CI run: | - mkdir -p ${{ env.CARGO_HOME }} -ErrorAction Ignore - cp ./.cargo/ci-config.toml ${{ env.CARGO_HOME }}/config.toml - .\script\install-rustup.ps1 + New-Item -ItemType Directory -Path "./../.cargo" -Force + Copy-Item -Path "./.cargo/ci-config.toml" -Destination "./../.cargo/config.toml" - name: cargo clippy run: | @@ -430,18 +429,9 @@ jobs: - name: Limit target directory size run: ./script/clear-target-dir-if-larger-than.ps1 250 - # - name: Check dev drive space - # working-directory: ${{ env.ZED_WORKSPACE }} - # # `setup-dev-driver.ps1` creates a 100GB drive, with CI taking up ~45GB of the drive. - # run: ./script/exit-ci-if-dev-drive-is-full.ps1 95 - - # Since the Windows runners are stateful, so we need to remove the config file to prevent potential bug. - name: Clean CI config file if: always() - run: | - if (Test-Path "${{ env.CARGO_HOME }}/config.toml") { - Remove-Item -Path "${{ env.CARGO_HOME }}/config.toml" -Force - } + run: Remove-Item -Recurse -Path "./../.cargo" -Force -ErrorAction SilentlyContinue tests_pass: name: Tests Pass @@ -763,12 +753,67 @@ jobs: # excludes the final package to only cache dependencies cachix-filter: "-zed-editor-[0-9.]*-nightly" + bundle-windows-x64: + timeout-minutes: 120 + name: Create a Windows installer + runs-on: [self-hosted, Windows, X64] + if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }} + needs: [windows_tests] + env: + AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }} + ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }} + CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }} + ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }} + DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} + DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} + FILE_DIGEST: SHA256 + TIMESTAMP_DIGEST: SHA256 + TIMESTAMP_SERVER: "http://timestamp.acs.microsoft.com" + steps: + - name: Checkout repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + clean: false + + - name: Determine version and release channel + working-directory: ${{ env.ZED_WORKSPACE }} + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + run: | + # This exports RELEASE_CHANNEL into env (GITHUB_ENV) + script/determine-release-channel.ps1 + + - name: Install trusted signing + uses: ./.github/actions/install_trusted_signing + + - name: Build Zed installer + working-directory: ${{ env.ZED_WORKSPACE }} + run: script/bundle-windows.ps1 + + - name: Upload installer (x86_64) to Workflow - zed (run-bundling) + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + if: contains(github.event.pull_request.labels.*.name, 'run-bundling') + with: + name: ZedEditorUserSetup-x64-${{ github.event.pull_request.head.sha || github.sha }}.exe + path: ${{ env.SETUP_PATH }} + + - name: Upload Artifacts to release + uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1 + if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) && env.RELEASE_CHANNEL == 'preview' }} # upload only preview + with: + draft: true + prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }} + files: ${{ env.SETUP_PATH }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + auto-release-preview: name: Auto release preview if: | startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') - needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, freebsd] + needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, bundle-windows-x64, freebsd] runs-on: - self-hosted - bundle diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index d9287cb0826815a4b60c72389b950156da43df99..df9f6ef40fc9faf87c5be82e3f95288012cd4221 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -51,6 +51,32 @@ jobs: - name: Run tests uses: ./.github/actions/run_tests + windows-tests: + timeout-minutes: 60 + name: Run tests on Windows + if: github.repository_owner == 'zed-industries' + runs-on: [self-hosted, Windows, X64] + steps: + - name: Checkout repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + clean: false + + - name: Configure CI + run: | + New-Item -ItemType Directory -Path "./../.cargo" -Force + Copy-Item -Path "./.cargo/ci-config.toml" -Destination "./../.cargo/config.toml" + + - name: Run tests + uses: ./.github/actions/run_tests_windows + + - name: Limit target directory size + run: ./script/clear-target-dir-if-larger-than.ps1 1024 + + - name: Clean CI config file + if: always() + run: Remove-Item -Recurse -Path "./../.cargo" -Force -ErrorAction SilentlyContinue + bundle-mac: timeout-minutes: 60 name: Create a macOS bundle @@ -213,10 +239,54 @@ jobs: bundle-nix: name: Build and cache Nix package + if: false needs: tests secrets: inherit uses: ./.github/workflows/nix.yml + bundle-windows-x64: + timeout-minutes: 60 + name: Create a Windows installer + if: github.repository_owner == 'zed-industries' + runs-on: [self-hosted, Windows, X64] + needs: windows-tests + env: + AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }} + ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }} + CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }} + ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }} + DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} + DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} + FILE_DIGEST: SHA256 + TIMESTAMP_DIGEST: SHA256 + TIMESTAMP_SERVER: "http://timestamp.acs.microsoft.com" + steps: + - name: Checkout repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + clean: false + + - name: Set release channel to nightly + working-directory: ${{ env.ZED_WORKSPACE }} + run: | + $ErrorActionPreference = "Stop" + $version = git rev-parse --short HEAD + Write-Host "Publishing version: $version on release channel nightly" + "nightly" | Set-Content -Path "crates/zed/RELEASE_CHANNEL" + + - name: Install trusted signing + uses: ./.github/actions/install_trusted_signing + + - name: Build Zed installer + working-directory: ${{ env.ZED_WORKSPACE }} + run: script/bundle-windows.ps1 + + - name: Upload Zed Nightly + working-directory: ${{ env.ZED_WORKSPACE }} + run: script/upload-nightly.ps1 windows + update-nightly-tag: name: Update nightly tag if: github.repository_owner == 'zed-industries' @@ -225,6 +295,7 @@ jobs: - bundle-mac - bundle-linux-x86 - bundle-linux-arm + - bundle-windows-x64 steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/Cargo.lock b/Cargo.lock index 07a445eefebc9454217bf70a52d45b65afdea255..38bb7819caa125928f82dec5a57167c63d344813 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5191,6 +5191,16 @@ dependencies = [ "libc", ] +[[package]] +name = "explorer_command_injector" +version = "0.1.0" +dependencies = [ + "windows 0.61.1", + "windows-core 0.61.0", + "windows-registry 0.5.1", + "workspace-hack", +] + [[package]] name = "exr" version = "1.73.0" diff --git a/Cargo.toml b/Cargo.toml index f22bff1f863e3cfc151952ea59889d35c955c4db..a4d8b3cb95c37787fae1ed15c7c68cf7d63632a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ members = [ "crates/diagnostics", "crates/docs_preprocessor", "crates/editor", + "crates/explorer_command_injector", "crates/eval", "crates/extension", "crates/extension_api", @@ -625,6 +626,8 @@ wasmtime = { version = "29", default-features = false, features = [ ] } wasmtime-wasi = "29" which = "6.0.0" +windows-core = "0.61" +wit-component = "0.221" workspace-hack = "0.1.0" zed_llm_client = "= 0.8.6" zstd = "0.11" diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 1123d3f8e2a5cf5e2354a4963b96d71134fdc791..d62a9cdbe330964759fa5362689349c28cd2b713 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -638,7 +638,7 @@ impl AutoUpdater { let filename = match OS { "macos" => anyhow::Ok("Zed.dmg"), "linux" => Ok("zed.tar.gz"), - "windows" => Ok("ZedUpdateInstaller.exe"), + "windows" => Ok("zed_editor_installer.exe"), unsupported_os => anyhow::bail!("not supported: {unsupported_os}"), }?; diff --git a/crates/auto_update_helper/app-icon.ico b/crates/auto_update_helper/app-icon.ico index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..321e90fcfa15d8f84c2619b4d12af892ea5cda66 100644 GIT binary patch literal 590611 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

vDky&Z7IH+P+BTHvtq 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 TiR= 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 T2@fmu2~d((m#cjFI^=% anyhow::Result { } fn main() -> Result<()> { + #[cfg(all(not(debug_assertions), target_os = "windows"))] + unsafe { + use ::windows::Win32::System::Console::{ATTACH_PARENT_PROCESS, AttachConsole}; + + let _ = AttachConsole(ATTACH_PARENT_PROCESS); + } + #[cfg(unix)] util::prevent_root_execution(); diff --git a/crates/explorer_command_injector/AppxManifest-Nightly.xml b/crates/explorer_command_injector/AppxManifest-Nightly.xml new file mode 100644 index 0000000000000000000000000000000000000000..31d2d8f782c94c0d68c04f53c07976b80e9bed10 --- /dev/null +++ b/crates/explorer_command_injector/AppxManifest-Nightly.xml @@ -0,0 +1,78 @@ + + + + + + Zed Editor Nightly + Zed Industries + + resources\logo_150x150.png + true + disabled + disabled + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/explorer_command_injector/AppxManifest-Preview.xml b/crates/explorer_command_injector/AppxManifest-Preview.xml new file mode 100644 index 0000000000000000000000000000000000000000..44c967f5e20a99eb85fa8d3d8750608fd39fd0e3 --- /dev/null +++ b/crates/explorer_command_injector/AppxManifest-Preview.xml @@ -0,0 +1,78 @@ + + + + + + Zed Editor Preview + Zed Industries + + resources\logo_150x150.png + true + disabled + disabled + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/explorer_command_injector/AppxManifest.xml b/crates/explorer_command_injector/AppxManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..0f0e2c68cfc7ba07a53644ce438913486771a95a --- /dev/null +++ b/crates/explorer_command_injector/AppxManifest.xml @@ -0,0 +1,79 @@ + + + + + + Zed Editor + + Zed Industries + + resources\logo_150x150.png + true + disabled + disabled + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/explorer_command_injector/Cargo.toml b/crates/explorer_command_injector/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..e929ba6fc824d6fa7a9b2f995828d8081cf2c2a0 --- /dev/null +++ b/crates/explorer_command_injector/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "explorer_command_injector" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +crate-type = ["cdylib"] +path = "src/explorer_command_injector.rs" +doctest = false + +[features] +default = ["nightly"] +stable = [] +preview = [] +nightly = [] + +[target.'cfg(target_os = "windows")'.dependencies] +windows.workspace = true +windows-core.workspace = true +windows-registry = "0.5" + +[dependencies] +workspace-hack.workspace = true diff --git a/crates/explorer_command_injector/LICENSE-GPL b/crates/explorer_command_injector/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/explorer_command_injector/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/explorer_command_injector/src/explorer_command_injector.rs b/crates/explorer_command_injector/src/explorer_command_injector.rs new file mode 100644 index 0000000000000000000000000000000000000000..57454bc3a87e6574d43bba05ff353c6cffc31601 --- /dev/null +++ b/crates/explorer_command_injector/src/explorer_command_injector.rs @@ -0,0 +1,201 @@ +#![cfg(target_os = "windows")] + +use std::{os::windows::ffi::OsStringExt, path::PathBuf}; + +use windows::{ + Win32::{ + Foundation::{ + CLASS_E_CLASSNOTAVAILABLE, E_FAIL, E_INVALIDARG, E_NOTIMPL, ERROR_INSUFFICIENT_BUFFER, + GetLastError, HINSTANCE, MAX_PATH, + }, + Globalization::u_strlen, + System::{ + Com::{IBindCtx, IClassFactory, IClassFactory_Impl}, + LibraryLoader::GetModuleFileNameW, + SystemServices::DLL_PROCESS_ATTACH, + }, + UI::Shell::{ + ECF_DEFAULT, ECS_ENABLED, IEnumExplorerCommand, IExplorerCommand, + IExplorerCommand_Impl, IShellItemArray, SHStrDupW, SIGDN_FILESYSPATH, + }, + }, + core::{BOOL, GUID, HRESULT, HSTRING, Interface, Ref, Result, implement}, +}; + +static mut DLL_INSTANCE: HINSTANCE = HINSTANCE(std::ptr::null_mut()); + +#[unsafe(no_mangle)] +extern "system" fn DllMain( + hinstdll: HINSTANCE, + fdwreason: u32, + _lpvreserved: *mut core::ffi::c_void, +) -> bool { + if fdwreason == DLL_PROCESS_ATTACH { + unsafe { DLL_INSTANCE = hinstdll }; + } + + true +} + +#[implement(IExplorerCommand)] +struct ExplorerCommandInjector; + +#[allow(non_snake_case)] +impl IExplorerCommand_Impl for ExplorerCommandInjector_Impl { + fn GetTitle(&self, _: Ref) -> Result { + let command_description = + retrieve_command_description().unwrap_or(HSTRING::from("Open with Zed")); + unsafe { SHStrDupW(&command_description) } + } + + fn GetIcon(&self, _: Ref) -> Result { + let Some(zed_exe) = get_zed_exe_path() else { + return Err(E_FAIL.into()); + }; + unsafe { SHStrDupW(&HSTRING::from(zed_exe)) } + } + + fn GetToolTip(&self, _: Ref) -> Result { + Err(E_NOTIMPL.into()) + } + + fn GetCanonicalName(&self) -> Result { + Ok(GUID::zeroed()) + } + + fn GetState(&self, _: Ref, _: BOOL) -> Result { + Ok(ECS_ENABLED.0 as _) + } + + fn Invoke(&self, psiitemarray: Ref, _: Ref) -> Result<()> { + let items = psiitemarray.ok()?; + let Some(zed_exe) = get_zed_exe_path() else { + return Ok(()); + }; + + let count = unsafe { items.GetCount()? }; + for idx in 0..count { + let item = unsafe { items.GetItemAt(idx)? }; + let item_path = unsafe { item.GetDisplayName(SIGDN_FILESYSPATH)?.to_string()? }; + std::process::Command::new(&zed_exe) + .arg(&item_path) + .spawn() + .map_err(|_| E_INVALIDARG)?; + } + + Ok(()) + } + + fn GetFlags(&self) -> Result { + Ok(ECF_DEFAULT.0 as _) + } + + fn EnumSubCommands(&self) -> Result { + Err(E_NOTIMPL.into()) + } +} + +#[implement(IClassFactory)] +struct ExplorerCommandInjectorFactory; + +impl IClassFactory_Impl for ExplorerCommandInjectorFactory_Impl { + fn CreateInstance( + &self, + punkouter: Ref, + riid: *const windows_core::GUID, + ppvobject: *mut *mut core::ffi::c_void, + ) -> Result<()> { + unsafe { + *ppvobject = std::ptr::null_mut(); + } + if punkouter.is_none() { + let factory: IExplorerCommand = ExplorerCommandInjector {}.into(); + let ret = unsafe { factory.query(riid, ppvobject).ok() }; + if ret.is_ok() { + unsafe { + *ppvobject = factory.into_raw(); + } + } + ret + } else { + Err(E_INVALIDARG.into()) + } + } + + fn LockServer(&self, _: BOOL) -> Result<()> { + Ok(()) + } +} + +#[cfg(all(feature = "stable", not(feature = "preview"), not(feature = "nightly")))] +const MODULE_ID: GUID = GUID::from_u128(0x6a1f6b13_3b82_48a1_9e06_7bb0a6d0bffd); +#[cfg(all(feature = "preview", not(feature = "stable"), not(feature = "nightly")))] +const MODULE_ID: GUID = GUID::from_u128(0xaf8e85ea_fb20_4db2_93cf_56513c1ec697); +#[cfg(all(feature = "nightly", not(feature = "stable"), not(feature = "preview")))] +const MODULE_ID: GUID = GUID::from_u128(0x266f2cfe_1653_42af_b55c_fe3590c83871); + +// Make cargo clippy happy +#[cfg(all(feature = "nightly", feature = "stable", feature = "preview"))] +const MODULE_ID: GUID = GUID::from_u128(0x685f4d49_6718_4c55_b271_ebb5c6a48d6f); + +#[unsafe(no_mangle)] +extern "system" fn DllGetClassObject( + class_id: *const GUID, + iid: *const GUID, + out: *mut *mut std::ffi::c_void, +) -> HRESULT { + unsafe { + *out = std::ptr::null_mut(); + } + let class_id = unsafe { *class_id }; + if class_id == MODULE_ID { + let instance: IClassFactory = ExplorerCommandInjectorFactory {}.into(); + let ret = unsafe { instance.query(iid, out) }; + if ret.is_ok() { + unsafe { + *out = instance.into_raw(); + } + } + ret + } else { + CLASS_E_CLASSNOTAVAILABLE + } +} + +fn get_zed_install_folder() -> Option { + let mut buf = vec![0u16; MAX_PATH as usize]; + unsafe { GetModuleFileNameW(Some(DLL_INSTANCE.into()), &mut buf) }; + + while unsafe { GetLastError() } == ERROR_INSUFFICIENT_BUFFER { + buf = vec![0u16; buf.len() * 2]; + unsafe { GetModuleFileNameW(Some(DLL_INSTANCE.into()), &mut buf) }; + } + let len = unsafe { u_strlen(buf.as_ptr()) }; + let path: PathBuf = std::ffi::OsString::from_wide(&buf[..len as usize]) + .into_string() + .ok()? + .into(); + Some(path.parent()?.parent()?.to_path_buf()) +} + +#[inline] +fn get_zed_exe_path() -> Option { + get_zed_install_folder().map(|path| path.join("Zed.exe").to_string_lossy().to_string()) +} + +#[inline] +fn retrieve_command_description() -> Result { + #[cfg(all(feature = "stable", not(feature = "preview"), not(feature = "nightly")))] + const REG_PATH: &str = "Software\\Classes\\ZedEditorContextMenu"; + #[cfg(all(feature = "preview", not(feature = "stable"), not(feature = "nightly")))] + const REG_PATH: &str = "Software\\Classes\\ZedEditorPreviewContextMenu"; + #[cfg(all(feature = "nightly", not(feature = "stable"), not(feature = "preview")))] + const REG_PATH: &str = "Software\\Classes\\ZedEditorNightlyContextMenu"; + + // Make cargo clippy happy + #[cfg(all(feature = "nightly", feature = "stable", feature = "preview"))] + const REG_PATH: &str = "Software\\Classes\\ZedEditorClippyContextMenu"; + + let key = windows_registry::CURRENT_USER.open(REG_PATH)?; + key.get_hstring("Title") +} diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 1ab591e9d7f306ced32a8bed8f647f6d7f979e1f..b7cf6dd3888ae725025c71092d9a96827625fddd 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -220,7 +220,7 @@ blade-macros.workspace = true flume = "0.11" rand.workspace = true windows.workspace = true -windows-core = "0.61" +windows-core.workspace = true windows-numerics = "0.2" windows-registry = "0.5" diff --git a/crates/zed/build.rs b/crates/zed/build.rs index 531de3ceb9235965e2f0e8099e3aec64c873835f..0cfb3eba9fbecff024697bfffb542ae1b8b8d829 100644 --- a/crates/zed/build.rs +++ b/crates/zed/build.rs @@ -50,7 +50,17 @@ fn main() { println!("cargo:rustc-link-arg=/stack:{}", 8 * 1024 * 1024); } - let icon = std::path::Path::new("resources/windows/app-icon.ico"); + let release_channel = option_env!("RELEASE_CHANNEL").unwrap_or("nightly"); + + let icon = match release_channel { + "stable" => "resources/windows/app-icon.ico", + "preview" => "resources/windows/app-icon-preview.ico", + "nightly" => "resources/windows/app-icon-nightly.ico", + _ => "resources/windows/app-icon-dev.ico", + }; + let icon = std::path::Path::new(icon); + + println!("cargo:rerun-if-env-changed=RELEASE_CHANNEL"); println!("cargo:rerun-if-changed={}", icon.display()); let mut res = winresource::WindowsResource::new(); diff --git a/crates/zed/resources/windows/app-icon-dev.ico b/crates/zed/resources/windows/app-icon-dev.ico new file mode 100644 index 0000000000000000000000000000000000000000..de92b6dd3c4a34164a72dec5f2427589e97536a6 GIT binary patch literal 156580 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�zKe&w^@9jUhQ~$mF58Djy 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

  • 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

    gCA#eP(B6B@~0n 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

    $<}oKwgS_qR^+!y<6afPlw*@# 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 LYrwpGhwuI$0rBT` literal 0 HcmV?d00001 diff --git a/crates/zed/resources/windows/messages/Default.zh-cn.isl b/crates/zed/resources/windows/messages/Default.zh-cn.isl new file mode 100644 index 0000000000000000000000000000000000000000..d900c7d448cd61e5e02ef107c38ca75d286a430c --- /dev/null +++ b/crates/zed/resources/windows/messages/Default.zh-cn.isl @@ -0,0 +1,403 @@ +; *** Inno Setup version 6.4.0+ Chinese Simplified messages *** +; +; To download user-contributed translations of this file, go to: +; https://jrsoftware.org/files/istrans/ +; +; Note: When translating this text, do not add periods (.) to the end of +; messages that didn't have them already, because on those messages Inno +; Setup adds the periods automatically (appending a period would result in +; two periods being displayed). +; +; Maintained by Zhenghan Yang +; Email: 847320916@QQ.com +; Translation based on network resource +; The latest Translation is on https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation +; + +[LangOptions] +; The following three entries are very important. Be sure to read and +; understand the '[LangOptions] section' topic in the help file. +LanguageName=简体中文 +; If Language Name display incorrect, uncomment next line +; LanguageName=<7B80><4F53><4E2D><6587> +; About LanguageID, to reference link: +; https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c +LanguageID=$0804 +; About CodePage, to reference link: +; https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers +LanguageCodePage=936 +; If the language you are translating to requires special font faces or +; sizes, uncomment any of the following entries and change them accordingly. +;DialogFontName= +;DialogFontSize=8 +;WelcomeFontName=Verdana +;WelcomeFontSize=12 +;TitleFontName=Arial +;TitleFontSize=29 +;CopyrightFontName=Arial +;CopyrightFontSize=8 + +[Messages] + +; *** 应用程序标题 +SetupAppTitle=安装 +SetupWindowTitle=安装 - %1 +UninstallAppTitle=卸载 +UninstallAppFullTitle=%1 卸载 + +; *** Misc. common +InformationTitle=信息 +ConfirmTitle=确认 +ErrorTitle=错误 + +; *** SetupLdr messages +SetupLdrStartupMessage=现在将安装 %1。您想要继续吗? +LdrCannotCreateTemp=无法创建临时文件。安装程序已中止 +LdrCannotExecTemp=无法执行临时目录中的文件。安装程序已中止 +HelpTextNote= + +; *** 启动错误消息 +LastErrorMessage=%1。%n%n错误 %2: %3 +SetupFileMissing=安装目录中缺少文件 %1。请修正这个问题或者获取程序的新副本。 +SetupFileCorrupt=安装文件已损坏。请获取程序的新副本。 +SetupFileCorruptOrWrongVer=安装文件已损坏,或是与这个安装程序的版本不兼容。请修正这个问题或获取新的程序副本。 +InvalidParameter=无效的命令行参数:%n%n%1 +SetupAlreadyRunning=安装程序正在运行。 +WindowsVersionNotSupported=此程序不支持当前计算机运行的 Windows 版本。 +WindowsServicePackRequired=此程序需要 %1 服务包 %2 或更高版本。 +NotOnThisPlatform=此程序不能在 %1 上运行。 +OnlyOnThisPlatform=此程序只能在 %1 上运行。 +OnlyOnTheseArchitectures=此程序只能安装到为下列处理器架构设计的 Windows 版本中:%n%n%1 +WinVersionTooLowError=此程序需要 %1 版本 %2 或更高。 +WinVersionTooHighError=此程序不能安装于 %1 版本 %2 或更高。 +AdminPrivilegesRequired=在安装此程序时您必须以管理员身份登录。 +PowerUserPrivilegesRequired=在安装此程序时您必须以管理员身份或有权限的用户组身份登录。 +SetupAppRunningError=安装程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。 +UninstallAppRunningError=卸载程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。 + +; *** 启动问题 +PrivilegesRequiredOverrideTitle=选择安装程序模式 +PrivilegesRequiredOverrideInstruction=选择安装模式 +PrivilegesRequiredOverrideText1=%1 可以为所有用户安装(需要管理员权限),或仅为您安装。 +PrivilegesRequiredOverrideText2=%1 只能为您安装,或为所有用户安装(需要管理员权限)。 +PrivilegesRequiredOverrideAllUsers=为所有用户安装(&A) +PrivilegesRequiredOverrideAllUsersRecommended=为所有用户安装(&A) (建议选项) +PrivilegesRequiredOverrideCurrentUser=只为我安装(&M) +PrivilegesRequiredOverrideCurrentUserRecommended=只为我安装(&M) (建议选项) + +; *** 其他错误 +ErrorCreatingDir=安装程序无法创建目录“%1” +ErrorTooManyFilesInDir=无法在目录“%1”中创建文件,因为里面包含太多文件 + +; *** 安装程序公共消息 +ExitSetupTitle=退出安装程序 +ExitSetupMessage=安装程序尚未完成。如果现在退出,将不会安装该程序。%n%n您之后可以再次运行安装程序完成安装。%n%n现在退出安装程序吗? +AboutSetupMenuItem=关于安装程序(&A)... +AboutSetupTitle=关于安装程序 +AboutSetupMessage=%1 版本 %2%n%3%n%n%1 主页:%n%4 +AboutSetupNote= +TranslatorNote=简体中文翻译由Kira(847320916@qq.com)维护。项目地址:https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation + +; *** 按钮 +ButtonBack=< 上一步(&B) +ButtonNext=下一步(&N) > +ButtonInstall=安装(&I) +ButtonOK=确定 +ButtonCancel=取消 +ButtonYes=是(&Y) +ButtonYesToAll=全是(&A) +ButtonNo=否(&N) +ButtonNoToAll=全否(&O) +ButtonFinish=完成(&F) +ButtonBrowse=浏览(&B)... +ButtonWizardBrowse=浏览(&R)... +ButtonNewFolder=新建文件夹(&M) + +; *** “选择语言”对话框消息 +SelectLanguageTitle=选择安装语言 +SelectLanguageLabel=选择安装时使用的语言。 + +; *** 公共向导文字 +ClickNext=点击“下一步”继续,或点击“取消”退出安装程序。 +BeveledLabel= +BrowseDialogTitle=浏览文件夹 +BrowseDialogLabel=在下面的列表中选择一个文件夹,然后点击“确定”。 +NewFolderName=新建文件夹 + +; *** “欢迎”向导页 +WelcomeLabel1=欢迎使用 [name] 安装向导 +WelcomeLabel2=现在将安装 [name/ver] 到您的电脑中。%n%n建议您在继续安装前关闭所有其他应用程序。 + +; *** “密码”向导页 +WizardPassword=密码 +PasswordLabel1=这个安装程序有密码保护。 +PasswordLabel3=请输入密码,然后点击“下一步”继续。密码区分大小写。 +PasswordEditLabel=密码(&P): +IncorrectPassword=您输入的密码不正确,请重新输入。 + +; *** “许可协议”向导页 +WizardLicense=许可协议 +LicenseLabel=请在继续安装前阅读以下重要信息。 +LicenseLabel3=请仔细阅读下列许可协议。在继续安装前您必须同意这些协议条款。 +LicenseAccepted=我同意此协议(&A) +LicenseNotAccepted=我不同意此协议(&D) + +; *** “信息”向导页 +WizardInfoBefore=信息 +InfoBeforeLabel=请在继续安装前阅读以下重要信息。 +InfoBeforeClickLabel=准备好继续安装后,点击“下一步”。 +WizardInfoAfter=信息 +InfoAfterLabel=请在继续安装前阅读以下重要信息。 +InfoAfterClickLabel=准备好继续安装后,点击“下一步”。 + +; *** “用户信息”向导页 +WizardUserInfo=用户信息 +UserInfoDesc=请输入您的信息。 +UserInfoName=用户名(&U): +UserInfoOrg=组织(&O): +UserInfoSerial=序列号(&S): +UserInfoNameRequired=您必须输入用户名。 + +; *** “选择目标目录”向导页 +WizardSelectDir=选择目标位置 +SelectDirDesc=您想将 [name] 安装在哪里? +SelectDirLabel3=安装程序将安装 [name] 到下面的文件夹中。 +SelectDirBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。 +DiskSpaceGBLabel=至少需要有 [gb] GB 的可用磁盘空间。 +DiskSpaceMBLabel=至少需要有 [mb] MB 的可用磁盘空间。 +CannotInstallToNetworkDrive=安装程序无法安装到一个网络驱动器。 +CannotInstallToUNCPath=安装程序无法安装到一个 UNC 路径。 +InvalidPath=您必须输入一个带驱动器卷标的完整路径,例如:%n%nC:\APP%n%n或UNC路径:%n%n\\server\share +InvalidDrive=您选定的驱动器或 UNC 共享不存在或不能访问。请选择其他位置。 +DiskSpaceWarningTitle=磁盘空间不足 +DiskSpaceWarning=安装程序至少需要 %1 KB 的可用空间才能安装,但选定驱动器只有 %2 KB 的可用空间。%n%n您一定要继续吗? +DirNameTooLong=文件夹名称或路径太长。 +InvalidDirName=文件夹名称无效。 +BadDirName32=文件夹名称不能包含下列任何字符:%n%n%1 +DirExistsTitle=文件夹已存在 +DirExists=文件夹:%n%n%1%n%n已经存在。您一定要安装到这个文件夹中吗? +DirDoesntExistTitle=文件夹不存在 +DirDoesntExist=文件夹:%n%n%1%n%n不存在。您想要创建此文件夹吗? + +; *** “选择组件”向导页 +WizardSelectComponents=选择组件 +SelectComponentsDesc=您想安装哪些程序组件? +SelectComponentsLabel2=选中您想安装的组件;取消您不想安装的组件。然后点击“下一步”继续。 +FullInstallation=完全安装 +; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language) +CompactInstallation=简洁安装 +CustomInstallation=自定义安装 +NoUninstallWarningTitle=组件已存在 +NoUninstallWarning=安装程序检测到下列组件已安装在您的电脑中:%n%n%1%n%n取消选中这些组件不会卸载它们。%n%n确定要继续吗? +ComponentSize1=%1 KB +ComponentSize2=%1 MB +ComponentsDiskSpaceGBLabel=当前选择的组件需要至少 [gb] GB 的磁盘空间。 +ComponentsDiskSpaceMBLabel=当前选择的组件需要至少 [mb] MB 的磁盘空间。 + +; *** “选择附加任务”向导页 +WizardSelectTasks=选择附加任务 +SelectTasksDesc=您想要安装程序执行哪些附加任务? +SelectTasksLabel2=选择您想要安装程序在安装 [name] 时执行的附加任务,然后点击“下一步”。 + +; *** “选择开始菜单文件夹”向导页 +WizardSelectProgramGroup=选择开始菜单文件夹 +SelectStartMenuFolderDesc=安装程序应该在哪里放置程序的快捷方式? +SelectStartMenuFolderLabel3=安装程序将在下列“开始”菜单文件夹中创建程序的快捷方式。 +SelectStartMenuFolderBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。 +MustEnterGroupName=您必须输入一个文件夹名。 +GroupNameTooLong=文件夹名或路径太长。 +InvalidGroupName=无效的文件夹名字。 +BadGroupName=文件夹名不能包含下列任何字符:%n%n%1 +NoProgramGroupCheck2=不创建开始菜单文件夹(&D) + +; *** “准备安装”向导页 +WizardReady=准备安装 +ReadyLabel1=安装程序准备就绪,现在可以开始安装 [name] 到您的电脑。 +ReadyLabel2a=点击“安装”继续此安装程序。如果您想重新考虑或修改任何设置,点击“上一步”。 +ReadyLabel2b=点击“安装”继续此安装程序。 +ReadyMemoUserInfo=用户信息: +ReadyMemoDir=目标位置: +ReadyMemoType=安装类型: +ReadyMemoComponents=已选择组件: +ReadyMemoGroup=开始菜单文件夹: +ReadyMemoTasks=附加任务: + +; *** TExtractionWizardPage wizard page and Extract7ZipArchive +ExtractionLabel=正在提取附加文件... +ButtonStopExtraction=停止提取(&S) +StopExtraction=您确定要停止提取吗? +ErrorExtractionAborted=提取已中止 +ErrorExtractionFailed=提取失败:%1 + +; *** TDownloadWizardPage wizard page and DownloadTemporaryFile +DownloadingLabel=正在下载附加文件... +ButtonStopDownload=停止下载(&S) +StopDownload=您确定要停止下载吗? +ErrorDownloadAborted=下载已中止 +ErrorDownloadFailed=下载失败:%1 %2 +ErrorDownloadSizeFailed=获取下载大小失败:%1 %2 +ErrorFileHash1=校验文件哈希失败:%1 +ErrorFileHash2=无效的文件哈希:预期 %1,实际 %2 +ErrorProgress=无效的进度:%1 / %2 +ErrorFileSize=文件大小错误:预期 %1,实际 %2 + +; *** “正在准备安装”向导页 +WizardPreparing=正在准备安装 +PreparingDesc=安装程序正在准备安装 [name] 到您的电脑。 +PreviousInstallNotCompleted=先前的程序安装或卸载未完成,您需要重启您的电脑以完成。%n%n在重启电脑后,再次运行安装程序以完成 [name] 的安装。 +CannotContinue=安装程序不能继续。请点击“取消”退出。 +ApplicationsFound=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。 +ApplicationsFound2=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。安装完成后,安装程序将尝试重新启动这些应用程序。 +CloseApplications=自动关闭应用程序(&A) +DontCloseApplications=不要关闭应用程序(&D) +ErrorCloseApplications=安装程序无法自动关闭所有应用程序。建议您在继续之前,关闭所有在使用需要由安装程序更新的文件的应用程序。 +PrepareToInstallNeedsRestart=安装程序必须重启您的计算机。计算机重启后,请再次运行安装程序以完成 [name] 的安装。%n%n是否立即重新启动? + +; *** “正在安装”向导页 +WizardInstalling=正在安装 +InstallingLabel=安装程序正在安装 [name] 到您的电脑,请稍候。 + +; *** “安装完成”向导页 +FinishedHeadingLabel=[name] 安装完成 +FinishedLabelNoIcons=安装程序已在您的电脑中安装了 [name]。 +FinishedLabel=安装程序已在您的电脑中安装了 [name]。您可以通过已安装的快捷方式运行此应用程序。 +ClickFinish=点击“完成”退出安装程序。 +FinishedRestartLabel=为完成 [name] 的安装,安装程序必须重新启动您的电脑。要立即重启吗? +FinishedRestartMessage=为完成 [name] 的安装,安装程序必须重新启动您的电脑。%n%n要立即重启吗? +ShowReadmeCheck=是,我想查阅自述文件 +YesRadio=是,立即重启电脑(&Y) +NoRadio=否,稍后重启电脑(&N) +; used for example as 'Run MyProg.exe' +RunEntryExec=运行 %1 +; used for example as 'View Readme.txt' +RunEntryShellExec=查阅 %1 + +; *** “安装程序需要下一张磁盘”提示 +ChangeDiskTitle=安装程序需要下一张磁盘 +SelectDiskLabel2=请插入磁盘 %1 并点击“确定”。%n%n如果这个磁盘中的文件可以在下列文件夹之外的文件夹中找到,请输入正确的路径或点击“浏览”。 +PathLabel=路径(&P): +FileNotInDir2=“%2”中找不到文件“%1”。请插入正确的磁盘或选择其他文件夹。 +SelectDirectoryLabel=请指定下一张磁盘的位置。 + +; *** 安装状态消息 +SetupAborted=安装程序未完成安装。%n%n请修正这个问题并重新运行安装程序。 +AbortRetryIgnoreSelectAction=选择操作 +AbortRetryIgnoreRetry=重试(&T) +AbortRetryIgnoreIgnore=忽略错误并继续(&I) +AbortRetryIgnoreCancel=关闭安装程序 + +; *** 安装状态消息 +StatusClosingApplications=正在关闭应用程序... +StatusCreateDirs=正在创建目录... +StatusExtractFiles=正在解压缩文件... +StatusCreateIcons=正在创建快捷方式... +StatusCreateIniEntries=正在创建 INI 条目... +StatusCreateRegistryEntries=正在创建注册表条目... +StatusRegisterFiles=正在注册文件... +StatusSavingUninstall=正在保存卸载信息... +StatusRunProgram=正在完成安装... +StatusRestartingApplications=正在重启应用程序... +StatusRollback=正在撤销更改... + +; *** 其他错误 +ErrorInternal2=内部错误:%1 +ErrorFunctionFailedNoCode=%1 失败 +ErrorFunctionFailed=%1 失败;错误代码 %2 +ErrorFunctionFailedWithMessage=%1 失败;错误代码 %2.%n%3 +ErrorExecutingProgram=无法执行文件:%n%1 + +; *** 注册表错误 +ErrorRegOpenKey=打开注册表项时出错:%n%1\%2 +ErrorRegCreateKey=创建注册表项时出错:%n%1\%2 +ErrorRegWriteKey=写入注册表项时出错:%n%1\%2 + +; *** INI 错误 +ErrorIniEntry=在文件“%1”中创建 INI 条目时出错。 + +; *** 文件复制错误 +FileAbortRetryIgnoreSkipNotRecommended=跳过此文件(&S) (不推荐) +FileAbortRetryIgnoreIgnoreNotRecommended=忽略错误并继续(&I) (不推荐) +SourceIsCorrupted=源文件已损坏 +SourceDoesntExist=源文件“%1”不存在 +ExistingFileReadOnly2=无法替换现有文件,它是只读的。 +ExistingFileReadOnlyRetry=移除只读属性并重试(&R) +ExistingFileReadOnlyKeepExisting=保留现有文件(&K) +ErrorReadingExistingDest=尝试读取现有文件时出错: +FileExistsSelectAction=选择操作 +FileExists2=文件已经存在。 +FileExistsOverwriteExisting=覆盖已存在的文件(&O) +FileExistsKeepExisting=保留现有的文件(&K) +FileExistsOverwriteOrKeepAll=为所有冲突文件执行此操作(&D) +ExistingFileNewerSelectAction=选择操作 +ExistingFileNewer2=现有的文件比安装程序将要安装的文件还要新。 +ExistingFileNewerOverwriteExisting=覆盖已存在的文件(&O) +ExistingFileNewerKeepExisting=保留现有的文件(&K) (推荐) +ExistingFileNewerOverwriteOrKeepAll=为所有冲突文件执行此操作(&D) +ErrorChangingAttr=尝试更改下列现有文件的属性时出错: +ErrorCreatingTemp=尝试在目标目录创建文件时出错: +ErrorReadingSource=尝试读取下列源文件时出错: +ErrorCopying=尝试复制下列文件时出错: +ErrorReplacingExistingFile=尝试替换现有文件时出错: +ErrorRestartReplace=重启并替换失败: +ErrorRenamingTemp=尝试重命名下列目标目录中的一个文件时出错: +ErrorRegisterServer=无法注册 DLL/OCX:%1 +ErrorRegSvr32Failed=RegSvr32 失败;退出代码 %1 +ErrorRegisterTypeLib=无法注册类库:%1 + +; *** 卸载显示名字标记 +; used for example as 'My Program (32-bit)' +UninstallDisplayNameMark=%1 (%2) +; used for example as 'My Program (32-bit, All users)' +UninstallDisplayNameMarks=%1 (%2, %3) +UninstallDisplayNameMark32Bit=32 位 +UninstallDisplayNameMark64Bit=64 位 +UninstallDisplayNameMarkAllUsers=所有用户 +UninstallDisplayNameMarkCurrentUser=当前用户 + +; *** 安装后错误 +ErrorOpeningReadme=尝试打开自述文件时出错。 +ErrorRestartingComputer=安装程序无法重启电脑,请手动重启。 + +; *** 卸载消息 +UninstallNotFound=文件“%1”不存在。无法卸载。 +UninstallOpenError=文件“%1”不能被打开。无法卸载。 +UninstallUnsupportedVer=此版本的卸载程序无法识别卸载日志文件“%1”的格式。无法卸载 +UninstallUnknownEntry=卸载日志中遇到一个未知条目 (%1) +ConfirmUninstall=您确认要完全移除 %1 及其所有组件吗? +UninstallOnlyOnWin64=仅允许在 64 位 Windows 中卸载此程序。 +OnlyAdminCanUninstall=仅使用管理员权限的用户能完成此卸载。 +UninstallStatusLabel=正在从您的电脑中移除 %1,请稍候。 +UninstalledAll=已顺利从您的电脑中移除 %1。 +UninstalledMost=%1 卸载完成。%n%n有部分内容未能被删除,但您可以手动删除它们。 +UninstalledAndNeedsRestart=为完成 %1 的卸载,需要重启您的电脑。%n%n立即重启电脑吗? +UninstallDataCorrupted=文件“%1”已损坏。无法卸载 + +; *** 卸载状态消息 +ConfirmDeleteSharedFileTitle=删除共享的文件吗? +ConfirmDeleteSharedFile2=系统表示下列共享的文件已不有其他程序使用。您希望卸载程序删除这些共享的文件吗?%n%n如果删除这些文件,但仍有程序在使用这些文件,则这些程序可能出现异常。如果您不能确定,请选择“否”,在系统中保留这些文件以免引发问题。 +SharedFileNameLabel=文件名: +SharedFileLocationLabel=位置: +WizardUninstalling=卸载状态 +StatusUninstalling=正在卸载 %1... + +; *** Shutdown block reasons +ShutdownBlockReasonInstallingApp=正在安装 %1。 +ShutdownBlockReasonUninstallingApp=正在卸载 %1。 + +; The custom messages below aren't used by Setup itself, but if you make +; use of them in your scripts, you'll want to translate them. + +[CustomMessages] + +NameAndVersion=%1 版本 %2 +AdditionalIcons=附加快捷方式: +CreateDesktopIcon=创建桌面快捷方式(&D) +CreateQuickLaunchIcon=创建快速启动栏快捷方式(&Q) +ProgramOnTheWeb=%1 网站 +UninstallProgram=卸载 %1 +LaunchProgram=运行 %1 +AssocFileExtension=将 %2 文件扩展名与 %1 建立关联(&A) +AssocingFileExtension=正在将 %2 文件扩展名与 %1 建立关联... +AutoStartProgramGroupDescription=启动: +AutoStartProgram=自动启动 %1 +AddonHostProgramNotFound=您选择的文件夹中无法找到 %1。%n%n您要继续吗? diff --git a/crates/zed/resources/windows/messages/en.isl b/crates/zed/resources/windows/messages/en.isl new file mode 100644 index 0000000000000000000000000000000000000000..2e82bea4fff303e9199a8c49fdb50d9761096f8e --- /dev/null +++ b/crates/zed/resources/windows/messages/en.isl @@ -0,0 +1,15 @@ +[Messages] +FinishedLabel=Setup has finished installing [name] on your computer. The application may be launched by selecting the installed shortcuts. +ConfirmUninstall=Are you sure you want to completely remove %1 and all of its components? + +[CustomMessages] +AdditionalIcons=Additional icons: +CreateDesktopIcon=Create a &desktop icon +AddContextMenuFiles=Add "Open with %1" action to Windows Explorer file context menu +AddContextMenuFolders=Add "Open with %1" action to Windows Explorer directory context menu +AssociateWithFiles=Register %1 as an editor for supported file types +AddToPath=Add to PATH (requires shell restart) +RunAfter=Run %1 after installation +Other=Other: +SourceFile=%1 Source File +OpenWithContextMenu=Open w&ith %1 diff --git a/crates/zed/resources/windows/messages/zh-cn.isl b/crates/zed/resources/windows/messages/zh-cn.isl new file mode 100644 index 0000000000000000000000000000000000000000..50c03ccaaf8fac85c8ea8a59d2fe1d850f1d17d7 --- /dev/null +++ b/crates/zed/resources/windows/messages/zh-cn.isl @@ -0,0 +1,9 @@ +[CustomMessages] +AddContextMenuFiles=将“通过 %1 打开”操作添加到 Windows 资源管理器文件上下文菜单 +AddContextMenuFolders=将“通过 %1 打开”操作添加到 Windows 资源管理器目录上下文菜单 +AssociateWithFiles=将 %1 注册为受支持的文件类型的编辑器 +AddToPath=添加到 PATH (重启后生效) +RunAfter=安装后运行 %1 +Other=其他: +SourceFile=%1 源文件 +OpenWithContextMenu=通过 %1 打开 diff --git a/crates/zed/resources/windows/sign.ps1 b/crates/zed/resources/windows/sign.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..d00b33c0fc48cc7c30ebf00a03e8091dcc540663 --- /dev/null +++ b/crates/zed/resources/windows/sign.ps1 @@ -0,0 +1,53 @@ +param ( + [Parameter(Mandatory = $true)] + [string]$filePath +) + +$params = @{} + +$endpoint = $ENV:ENDPOINT +if ([string]::IsNullOrWhiteSpace($endpoint)) { + throw "The 'ENDPOINT' env is required." +} +$params["Endpoint"] = $endpoint + +$trustedSigningAccountName = $ENV:ACCOUNT_NAME +if ([string]::IsNullOrWhiteSpace($trustedSigningAccountName)) { + throw "The 'ACCOUNT_NAME' env is required." +} +$params["CodeSigningAccountName"] = $trustedSigningAccountName + +$certificateProfileName = $ENV:CERT_PROFILE_NAME +if ([string]::IsNullOrWhiteSpace($certificateProfileName)) { + throw "The 'CERT_PROFILE_NAME' env is required." +} +$params["CertificateProfileName"] = $certificateProfileName + +$fileDigest = $ENV:FILE_DIGEST +if ([string]::IsNullOrWhiteSpace($fileDigest)) { + throw "The 'FILE_DIGEST' env is required." +} +$params["FileDigest"] = $fileDigest + +$timeStampDigest = $ENV:TIMESTAMP_DIGEST +if ([string]::IsNullOrWhiteSpace($timeStampDigest)) { + throw "The 'TIMESTAMP_DIGEST' env is required." +} +$params["TimestampDigest"] = $timeStampDigest + +$timeStampServer = $ENV:TIMESTAMP_SERVER +if ([string]::IsNullOrWhiteSpace($timeStampServer)) { + throw "The 'TIMESTAMP_SERVER' env is required." +} +$params["TimestampRfc3161"] = $timeStampServer + +$params["Files"] = $filePath + +$trace = $ENV:TRACE +if (-Not [string]::IsNullOrWhiteSpace($trace)) { + if ([System.Convert]::ToBoolean($trace)) { + Set-PSDebug -Trace 2 + } +} + +Invoke-TrustedSigning @params diff --git a/crates/zed/resources/windows/zed.iss b/crates/zed/resources/windows/zed.iss new file mode 100644 index 0000000000000000000000000000000000000000..7c80676ca0377348439a530338843095f93b813b --- /dev/null +++ b/crates/zed/resources/windows/zed.iss @@ -0,0 +1,1412 @@ +[Setup] +AppId={#AppId} +AppName={#AppName} +AppVerName={#AppDisplayName} +AppPublisher=Zed Industries +AppPublisherURL=https://www.zed.dev/ +AppSupportURL=https://www.zed.dev/ +AppUpdatesURL=https://www.zed.dev/ +DefaultGroupName={#AppName} +AllowNoIcons=yes +OutputDir={#OutputDir} +OutputBaseFilename={#AppSetupName} +Compression=lzma +SolidCompression=yes +AppMutex={code:GetAppMutex} +SetupMutex={#AppMutex}Setup +; WizardImageFile="{#ResourcesDir}\inno-100.bmp,{#ResourcesDir}\inno-125.bmp,{#ResourcesDir}\inno-150.bmp,{#ResourcesDir}\inno-175.bmp,{#ResourcesDir}\inno-200.bmp,{#ResourcesDir}\inno-225.bmp,{#ResourcesDir}\inno-250.bmp" +; WizardSmallImageFile="{#ResourcesDir}\inno-small-100.bmp,{#ResourcesDir}\inno-small-125.bmp,{#ResourcesDir}\inno-small-150.bmp,{#ResourcesDir}\inno-small-175.bmp,{#ResourcesDir}\inno-small-200.bmp,{#ResourcesDir}\inno-small-225.bmp,{#ResourcesDir}\inno-small-250.bmp" +SetupIconFile={#ResourcesDir}\{#AppIconName}.ico +UninstallDisplayIcon={app}\{#AppExeName}.exe +ChangesEnvironment=true +ChangesAssociations=true +MinVersion=10.0.16299 +SourceDir={#SourceDir} +AppVersion={#Version} +VersionInfoVersion={#Version} +ShowLanguageDialog=auto +WizardStyle=modern + +CloseApplications=force + +SignTool=Defaultsign +DefaultDirName={autopf}\{#AppName} +PrivilegesRequired=lowest + +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl,{#ResourcesDir}\messages\en.isl"; LicenseFile: "script\terms\terms.rtf" +Name: "simplifiedChinese"; MessagesFile: "{#ResourcesDir}\messages\Default.zh-cn.isl,{#ResourcesDir}\messages\zh-cn.isl"; LicenseFile: "script\terms\terms.rtf" + +[UninstallDelete] +; Delete logs +Type: filesandordirs; Name: "{app}\tools" +Type: filesandordirs; Name: "{app}\updates" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked +Name: "addcontextmenufiles"; Description: "{cm:AddContextMenuFiles,{#AppDisplayName}}"; GroupDescription: "{cm:Other}" +Name: "addcontextmenufolders"; Description: "{cm:AddContextMenuFolders,{#AppDisplayName}}"; GroupDescription: "{cm:Other}"; Flags: unchecked; Check: not IsWindows11OrLater +Name: "associatewithfiles"; Description: "{cm:AssociateWithFiles,{#AppDisplayName}}"; GroupDescription: "{cm:Other}" +Name: "addtopath"; Description: "{cm:AddToPath}"; GroupDescription: "{cm:Other}" + +[Dirs] +Name: "{app}"; AfterInstall: DisableAppDirInheritance + +[Files] +Source: "{#ResourcesDir}\Zed.exe"; DestDir: "{code:GetInstallDir}"; Flags: ignoreversion +Source: "{#ResourcesDir}\bin\*"; DestDir: "{code:GetInstallDir}\bin"; Flags: ignoreversion +Source: "{#ResourcesDir}\tools\*"; DestDir: "{app}\tools"; Flags: ignoreversion +Source: "{#ResourcesDir}\appx\*"; DestDir: "{app}\appx"; BeforeInstall: RemoveAppxPackage; AfterInstall: AddAppxPackage; Flags: ignoreversion; Check: IsWindows11OrLater + +[Icons] +Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}.exe"; AppUserModelID: "{#AppUserId}" +Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}.exe"; Tasks: desktopicon; AppUserModelID: "{#AppUserId}" + +[Run] +Filename: "{app}\{#AppExeName}.exe"; Description: "{cm:LaunchProgram,{#AppName}}"; Flags: nowait postinstall; Check: WizardNotSilent + +[UninstallRun] +Filename: "powershell.exe"; Parameters: "Invoke-Command -ScriptBlock {{Remove-AppxPackage -Package ""{#AppxFullName}""}"; Check: IsWindows11OrLater; Flags: shellexec waituntilterminated runhidden + +[Registry] +Root: HKCU; Subkey: "Software\Classes\.ascx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.ascx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ascx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ascx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASCX}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ascx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ascx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\xml.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ascx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ascx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.asp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.asp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.asp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.asp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASP}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.asp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.asp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\html.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.asp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.asp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.aspx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.aspx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.aspx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.aspx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASPX}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.aspx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.aspx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\html.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.aspx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.aspx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.bash\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.bash\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\shell.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.bash_login\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.bash_login\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_login"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_login"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Login}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_login"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_login\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\shell.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_login\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_login\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.bash_logout\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.bash_logout\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_logout"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_logout"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Logout}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_logout"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_logout\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\shell.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_logout\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_logout\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.bash_profile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.bash_profile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_profile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_profile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_profile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_profile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\shell.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_profile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_profile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.bashrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.bashrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bashrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bashrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bashrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bashrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\shell.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bashrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bashrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.bib\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.bib\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bib"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bib"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,BibTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bib"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bib\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bib\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bib\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.bowerrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.bowerrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bowerrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bowerrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bower RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bowerrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bowerrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\bower.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bowerrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bowerrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.c++\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.c++\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.c++"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c++"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c++"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c++\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\cpp.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c++\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.c\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.c\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.c"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\c.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.cc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.cc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\cpp.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.cfg\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.cfg\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cfg"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cfg"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Configuration}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cfg"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cfg\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\config.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cfg\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cfg\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.cjs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.cjs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cjs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cjs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cjs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cjs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\javascript.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cjs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cjs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.clj\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.clj\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.clj"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clj"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Clojure}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clj"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clj\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clj\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clj\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.cljs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.cljs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cljs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ClojureScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.cljx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.cljx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cljx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CLJX}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.clojure\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.clojure\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.clojure"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clojure"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Clojure}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clojure"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clojure\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clojure\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clojure\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.cls\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.cls\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cls"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cls"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LaTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cls"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cls\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cls\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cls\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.code-workspace\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.code-workspace\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.code-workspace"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.code-workspace"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Code Workspace}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.code"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.code-workspace\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.code-workspace\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.code-workspace\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.cmake\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.cmake\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cmake"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cmake"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CMake}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cmake"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cmake\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cmake\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cmake\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.coffee\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.coffee\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.coffee"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.coffee"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CoffeeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.coffee"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.coffee\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.coffee\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.coffee\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.config\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.config\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.config"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.config"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Configuration}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.config"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.config\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\config.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.config\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.config\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.containerfile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.containerfile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.containerfile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.containerfile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Containerfile}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.containerfile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.containerfile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.containerfile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.cpp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.cpp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cpp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cpp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cpp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cpp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\cpp.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cpp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cpp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.cs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.cs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C#}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\csharp.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.cshtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.cshtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cshtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cshtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CSHTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cshtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cshtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\html.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cshtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cshtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.csproj\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.csproj\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csproj"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csproj"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C# Project}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csproj"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csproj\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\xml.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csproj\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csproj\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.css\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.css\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.css"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.css"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CSS}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.css"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.css\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\css.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.css\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.css\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.csv\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.csv\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csv"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csv"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Comma Separated Values}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csv"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csv\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csv\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csv\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.csx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.csx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\csharp.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.ctp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.ctp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ctp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ctp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CakePHP Template}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ctp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ctp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ctp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ctp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.cxx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.cxx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cxx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cxx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cxx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cxx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\cpp.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cxx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cxx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.dart\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.dart\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dart"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dart"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dart}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dart"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dart\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dart\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dart\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.diff\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.diff\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.diff"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.diff"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Diff}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.diff"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.diff\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.diff\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.diff\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.dockerfile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.dockerfile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dockerfile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dockerfile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dockerfile}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dockerfile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dockerfile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dockerfile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dockerfile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.dot\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.dot\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dot"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dot"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dot}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dot"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dot\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dot\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dot\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.dtd\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.dtd\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dtd"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dtd"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Document Type Definition}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dtd"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dtd\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\xml.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dtd\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dtd\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.editorconfig\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.editorconfig\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.editorconfig"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.editorconfig"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Editor Config}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.editorconfig"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.editorconfig\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\config.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.editorconfig\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.editorconfig\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.edn\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.edn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.edn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.edn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Extensible Data Notation}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.edn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.edn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.edn\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.edn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.erb\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.erb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.erb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.erb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Ruby}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.erb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.erb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\ruby.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.erb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.erb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.eyaml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.eyaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.eyaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Hiera Eyaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\yaml.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyaml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.eyml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.eyml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.eyml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Hiera Eyaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\yaml.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.fs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.fs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F#}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.fsi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.fsi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Signature}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.fsscript\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.fsscript\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsscript"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsscript"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsscript"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsscript\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsscript\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsscript\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.fsx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.fsx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.gemspec\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.gemspec\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gemspec"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gemspec"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Gemspec}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gemspec"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gemspec\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\ruby.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gemspec\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gemspec\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.gitattributes\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.gitattributes\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gitattributes"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Attributes}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitattributes\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\config.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitattributes\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitattributes\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.gitconfig\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.gitconfig\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gitconfig"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Config}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitconfig\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\config.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitconfig\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitconfig\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.gitignore\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.gitignore\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gitignore"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Ignore}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitignore\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\config.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitignore\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitignore\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.go\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.go\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.go"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.go"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Go}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.go"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.go\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\go.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.go\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.go\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.gradle\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.gradle\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gradle"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gradle"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Gradle}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gradle"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gradle\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gradle\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gradle\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.groovy\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.groovy\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.groovy"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.groovy"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Groovy}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.groovy"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.groovy\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.groovy\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.groovy\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.h\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.h\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.h"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\c.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.handlebars\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.handlebars\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.handlebars"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.handlebars"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Handlebars}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.handlebars"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.handlebars\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.handlebars\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.handlebars\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.hbs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.hbs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hbs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hbs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Handlebars}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hbs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hbs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hbs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hbs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.h++\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.h++\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.h++"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h++"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h++"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h++\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\cpp.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h++\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.hh\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.hh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\cpp.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hh\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.hpp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.hpp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hpp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hpp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hpp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hpp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\cpp.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hpp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hpp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.htm\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.htm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.htm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.htm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.htm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.htm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\html.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.htm\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.htm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.html\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.html\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.html"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.html"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.html"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.html\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\html.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.html\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.html\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.hxx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.hxx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hxx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hxx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hxx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hxx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\cpp.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hxx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hxx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.ini\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.ini\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ini"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ini"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,INI}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ini"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ini\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\config.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ini\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ini\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.ipynb\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.ipynb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ipynb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ipynb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Jupyter}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ipynb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ipynb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ipynb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ipynb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.jade\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.jade\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jade"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jade"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Jade}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jade"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jade\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\jade.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jade\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jade\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.jav\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.jav\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jav"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jav"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jav"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jav\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\java.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jav\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jav\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.java\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.java\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.java"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.java"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.java"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.java\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\java.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.java\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.java\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.js\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.js\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.js"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.js"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.js"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.js\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\javascript.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.js\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.js\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.jsx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.jsx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jsx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\react.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.jscsrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.jscsrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jscsrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jscsrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSCS RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jscsrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jscsrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\javascript.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jscsrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jscsrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.jshintrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.jshintrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jshintrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshintrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSHint RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshintrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshintrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\javascript.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshintrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshintrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.jshtm\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.jshtm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jshtm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshtm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript HTML Template}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshtm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshtm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\html.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshtm\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshtm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.json\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.json\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.json"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.json"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSON}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.json"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.json\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\json.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.json\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.json\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.jsp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.jsp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jsp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java Server Pages}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\html.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.less\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.less\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.less"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.less"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LESS}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.less"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.less\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\less.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.less\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.less\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.log\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.log\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.log"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.log"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Log file}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.log"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.log\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.log\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.log\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.lua\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.lua\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.lua"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.lua"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Lua}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.lua"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.lua\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.lua\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.lua\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.m\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.m\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.m"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.m"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Objective C}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.m"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.m\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.m\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.m\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.makefile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.makefile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.makefile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.makefile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Makefile}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.makefile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.makefile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.makefile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.makefile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.markdown\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.markdown\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.markdown"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.markdown"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.markdown"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.markdown\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\markdown.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.markdown\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.markdown\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.md\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.md\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.md"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.md"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.md"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.md\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\markdown.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.md\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.md\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.mdoc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.mdoc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdoc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdoc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,MDoc}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdoc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdoc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\markdown.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdoc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdoc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.mdown\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.mdown\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdown"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdown"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdown"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdown\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\markdown.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdown\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdown\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.mdtext\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.mdtext\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdtext"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtext"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtext"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtext\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\markdown.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtext\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtext\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.mdtxt\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.mdtxt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdtxt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtxt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtxt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtxt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\markdown.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtxt\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtxt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.mdwn\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.mdwn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdwn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdwn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdwn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdwn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\markdown.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdwn\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdwn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.mk\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.mk\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mk"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mk"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Makefile}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mk"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mk\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mk\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mk\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.mkd\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.mkd\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mkd"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkd"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkd"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkd\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\markdown.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkd\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkd\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.mkdn\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.mkdn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mkdn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkdn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkdn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkdn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\markdown.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkdn\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkdn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.ml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.ml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,OCaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.mli\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.mli\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mli"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mli"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,OCaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mli"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mli\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mli\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mli\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.mjs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.mjs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mjs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mjs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mjs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mjs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\javascript.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mjs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mjs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.npmignore\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.npmignore\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.npmignore"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,NPM Ignore}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.npmignore\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.npmignore\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.npmignore\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.php\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.php\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.php"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.php"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PHP}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.php"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.php\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\php.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.php\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.php\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.phtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.phtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.phtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.phtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PHP HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.phtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.phtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\html.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.phtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.phtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.pl\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.pl\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pl"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.pl6\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.pl6\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pl6"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl6"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl 6}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl6"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl6\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl6\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl6\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.plist\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.plist\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.plist"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.plist"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Properties file}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.plist"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.plist\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.plist\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.plist\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.pm\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.pm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.pm6\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.pm6\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pm6"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm6"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl 6 Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm6"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm6\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm6\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm6\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.pod\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.pod\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pod"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pod"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl POD}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pod"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pod\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pod\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pod\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.pp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.pp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.profile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.profile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.profile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.profile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.profile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.profile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\shell.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.profile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.profile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.properties\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.properties\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.properties"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.properties"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Properties}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.properties"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.properties\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.properties\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.properties\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.ps1\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.ps1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ps1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ps1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ps1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ps1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\powershell.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ps1\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ps1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.psd1\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.psd1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psd1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psd1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell Module Manifest}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psd1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psd1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\powershell.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psd1\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psd1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.psgi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.psgi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psgi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psgi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl CGI}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psgi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psgi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psgi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psgi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.psm1\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.psm1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psm1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psm1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psm1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psm1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\powershell.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psm1\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psm1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.py\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.py\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.py"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.py"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Python}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.py"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.py\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\python.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.py\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.py\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.pyi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.pyi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pyi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pyi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Python}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pyi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pyi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\python.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pyi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pyi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.r\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.r\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.r"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.r"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.r"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.r\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.r\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.r\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.rb\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.rb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Ruby}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\ruby.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.rhistory\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.rhistory\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rhistory"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rhistory"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R History}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rhistory"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rhistory\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\shell.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rhistory\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rhistory\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.rprofile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.rprofile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rprofile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rprofile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rprofile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rprofile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\shell.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rprofile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rprofile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.rs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.rs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Rust}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.rst\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.rst\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rst"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rst"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Restructured Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rst"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rst\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rst\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rst\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.rt\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.rt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Rich Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rt\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.sass\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.sass\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sass"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sass"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Sass}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sass"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sass\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\sass.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sass\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sass\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.scss\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.scss\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.scss"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.scss"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Sass}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.scss"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.scss\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\sass.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.scss\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.scss\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.sh\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.sh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SH}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\shell.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sh\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.shtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.shtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.shtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.shtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SHTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.shtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.shtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\html.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.shtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.shtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.sql\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.sql\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sql"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sql"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SQL}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sql"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sql\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\sql.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sql\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sql\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.svg\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.svg\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.svg"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.svg"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SVG}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.svg"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.svg\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.svg\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.svg\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.t\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.t\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.t"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.t"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.t"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.t\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.t\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.t\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.tex\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.tex\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.tex"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tex"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LaTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tex"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tex\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tex\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tex\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.ts\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.ts\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ts"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ts"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,TypeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ts"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ts\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\typescript.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ts\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ts\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.toml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.toml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.toml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.toml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Toml}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.toml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.toml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.toml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.toml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.tsx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.tsx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.tsx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tsx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,TypeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tsx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tsx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\react.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tsx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tsx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.txt\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.txt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.txt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.txt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.txt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.txt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.txt\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.txt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.vb\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.vb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.vb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Visual Basic}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.vue\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.vue\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.vue"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vue"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,VUE}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vue"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vue\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\vue.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vue\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vue\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.wxi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.wxi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX Include}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.wxl\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.wxl\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxl"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxl"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX Localization}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxl"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxl\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxl\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxl\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.wxs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.wxs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.xaml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.xaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,XAML}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\xml.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xaml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.xhtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.xhtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xhtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xhtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xhtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xhtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\html.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xhtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xhtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.xml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.xml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,XML}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\xml.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.yaml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.yaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.yaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Yaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\yaml.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yaml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.yml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.yml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.yml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Yaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\yaml.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\.zsh\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\.zsh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.zsh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.zsh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ZSH}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.zsh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.zsh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\shell.ico"; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.zsh\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.zsh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles + +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}SourceFile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,{#AppName}}"; Flags: uninsdeletekey +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}SourceFile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico" +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}SourceFile\shell\open"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe""" +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}SourceFile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1""" + +Root: HKCU; Subkey: "Software\Classes\Applications\{#AppExeName}.exe"; ValueType: none; ValueName: ""; Flags: uninsdeletekey +Root: HKCU; Subkey: "Software\Classes\Applications\{#AppExeName}.exe\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\default.ico" +Root: HKCU; Subkey: "Software\Classes\Applications\{#AppExeName}.exe\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe""" +Root: HKCU; Subkey: "Software\Classes\Applications\{#AppExeName}.exe\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1""" + +Root: HKCU; Subkey: "Software\Classes\{#RegValueName}ContextMenu"; ValueType: expandsz; ValueName: "Title"; ValueData: "{cm:OpenWithContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: IsWindows11OrLater +Root: HKCU; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: not IsWindows11OrLater +Root: HKCU; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#AppExeName}.exe"; Tasks: addcontextmenufiles; Check: not IsWindows11OrLater +Root: HKCU; Subkey: "Software\Classes\*\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: addcontextmenufiles; Check: not IsWindows11OrLater +Root: HKCU; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: IsWindows11OrLater +Root: HKCU; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#AppExeName}.exe"; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater +Root: HKCU; Subkey: "Software\Classes\directory\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater +Root: HKCU; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not IsWindows11OrLater +Root: HKCU; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#AppExeName}.exe"; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater +Root: HKCU; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater +Root: HKCU; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not IsWindows11OrLater +Root: HKCU; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#AppExeName}.exe"; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater +Root: HKCU; Subkey: "Software\Classes\Drive\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater + +; Environment +Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "Path"; ValueData: "{code:AddToPath|{app}\bin}"; Tasks: addtopath; Check: NeedsAddToPath(ExpandConstant('{app}\bin')) + +; URI Scheme +Root: HKCU; Subkey: "Software\Classes\zed"; ValueType: "string"; ValueData: "URL:zed Protocol"; Flags: uninsdeletekey +Root: HKCU; Subkey: "Software\Classes\zed"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "" +Root: HKCU; Subkey: "Software\Classes\zed\DefaultIcon"; ValueType: "string"; ValueData: "{app}\Zed.exe,1" +Root: HKCU; Subkey: "Software\Classes\zed\shell\open\command"; ValueType: "string"; ValueData: """{app}\Zed.exe"" ""%1""" + +[Code] +function InitializeSetup(): Boolean; +begin + Result := True; + + if not WizardSilent() and IsAdmin() then begin + MsgBox('This User Installer is not meant to be run as an Administrator.', mbError, MB_OK); + Result := False; + end; +end; + +function WizardNotSilent(): Boolean; +begin + Result := not WizardSilent(); +end; + +function IsWindows11OrLater(): Boolean; +begin + Result := (GetWindowsVersion >= $0A0055F0); +end; + +// https://stackoverflow.com/a/23838239/261019 +procedure Explode(var Dest: TArrayOfString; Text: String; Separator: String); +var + i, p: Integer; +begin + i := 0; + repeat + SetArrayLength(Dest, i+1); + p := Pos(Separator,Text); + if p > 0 then begin + Dest[i] := Copy(Text, 1, p-1); + Text := Copy(Text, p + Length(Separator), Length(Text)); + i := i + 1; + end else begin + Dest[i] := Text; + Text := ''; + end; + until Length(Text)=0; +end; + +function NeedsAddToPath(path: string): boolean; +var + OrigPath: string; +begin + if not RegQueryStringValue(HKCU, 'Environment', 'Path', OrigPath) + then begin + Result := True; + exit; + end; + Result := Pos(';' + path + ';', ';' + OrigPath + ';') = 0; +end; + +function AddToPath(path: string): string; +var + OrigPath: string; +begin + RegQueryStringValue(HKCU, 'Environment', 'Path', OrigPath) + + if (Length(OrigPath) > 0) and (OrigPath[Length(OrigPath)] = ';') then + Result := OrigPath + path + else + Result := OrigPath + ';' + path +end; + +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +var + Path: string; + InstalledPath: string; + Parts: TArrayOfString; + NewPath: string; + i: Integer; +begin + if not CurUninstallStep = usUninstall then begin + exit; + end; + if not RegQueryStringValue(HKCU, 'Environment', 'Path', Path) + then begin + exit; + end; + NewPath := ''; + InstalledPath := ExpandConstant('{app}\bin') + Explode(Parts, Path, ';'); + for i:=0 to GetArrayLength(Parts)-1 do begin + if CompareText(Parts[i], InstalledPath) <> 0 then begin + NewPath := NewPath + Parts[i]; + + if i < GetArrayLength(Parts) - 1 then begin + NewPath := NewPath + ';'; + end; + end; + end; + RegWriteExpandStringValue(HKCU, 'Environment', 'Path', NewPath); +end; + +// https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/icacls +// https://docs.microsoft.com/en-US/windows/security/identity-protection/access-control/security-identifiers +procedure DisableAppDirInheritance(); +var + ResultCode: Integer; + Permissions: string; +begin + Permissions := '/grant:r "*S-1-5-18:(OI)(CI)F" /grant:r "*S-1-5-32-544:(OI)(CI)F" /grant:r "*S-1-5-11:(OI)(CI)RX" /grant:r "*S-1-5-32-545:(OI)(CI)RX"'; + + Permissions := Permissions + Format(' /grant:r "*S-1-3-0:(OI)(CI)F" /grant:r "%s:(OI)(CI)F"', [GetUserNameString()]); + + Exec(ExpandConstant('{sys}\icacls.exe'), ExpandConstant('"{app}" /inheritancelevel:r ') + Permissions, '', SW_HIDE, ewWaitUntilTerminated, ResultCode); +end; + +procedure AddAppxPackage(); +var + AddAppxPackageResultCode: Integer; +begin + if WizardIsTaskSelected('addcontextmenufiles') then begin + ShellExec('', 'powershell.exe', '-Command ' + AddQuotes('Add-AppxPackage -Path ''' + ExpandConstant('{app}\appx\zed_explorer_command_injector.appx') + ''' -ExternalLocation ''' + ExpandConstant('{app}\appx') + ''''), '', SW_HIDE, ewWaitUntilTerminated, AddAppxPackageResultCode); + RegDeleteKeyIncludingSubkeys(HKCU, 'Software\Classes\*\shell\{#RegValueName}'); + RegDeleteKeyIncludingSubkeys(HKCU, 'Software\Classes\directory\shell\{#RegValueName}'); + RegDeleteKeyIncludingSubkeys(HKCU, 'Software\Classes\directory\background\shell\{#RegValueName}'); + RegDeleteKeyIncludingSubkeys(HKCU, 'Software\Classes\Drive\shell\{#RegValueName}'); + end; +end; + +procedure RemoveAppxPackage(); +var + RemoveAppxPackageResultCode: Integer; +begin + ShellExec('', 'powershell.exe', '-Command ' + AddQuotes('Remove-AppxPackage -Package ''{#AppxFullName}'''), '', SW_HIDE, ewWaitUntilTerminated, RemoveAppxPackageResultCode); + if not WizardIsTaskSelected('addcontextmenufiles') then begin + RegDeleteKeyIncludingSubkeys(HKCU, 'Software\Classes\{#RegValueName}ContextMenu'); + end; +end; + +function SwitchHasValue(Name: string; Value: string): Boolean; +begin + Result := CompareText(ExpandConstant('{param:' + Name + '}'), Value) = 0; +end; + +function IsUpdating(): Boolean; +begin + Result := SwitchHasValue('update', 'true') and WizardSilent(); +end; + +procedure CurStepChanged(CurStep: TSetupStep); +begin + if CurStep = ssPostInstall then + begin + if IsUpdating() then + begin + SaveStringToFile(ExpandConstant('{app}\updates\versions.txt'), '{#Version}' + #13#10, True); + end + end; +end; + +function GetAppMutex(Param: string): string; +begin + if IsUpdating() then + Result := '' + else + Result := '{#AppMutex}'; +end; + +function GetInstallDir(Param: string): string; +begin + if IsUpdating() then + Result := ExpandConstant('{app}\install') + else + Result := ExpandConstant('{app}'); +end; diff --git a/script/bundle-windows.ps1 b/script/bundle-windows.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..9b61d220cf741928c44d026a8ddcc7bc29bf95c5 --- /dev/null +++ b/script/bundle-windows.ps1 @@ -0,0 +1,263 @@ +[CmdletBinding()] +Param( + [Parameter()][Alias('i')][switch]$Install, + [Parameter()][Alias('h')][switch]$Help, + [Parameter()][string]$Name +) + +. "$PSScriptRoot/lib/blob-store.ps1" +. "$PSScriptRoot/lib/workspace.ps1" + +# https://stackoverflow.com/questions/57949031/powershell-script-stops-if-program-fails-like-bash-set-o-errexit +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true + +$buildSuccess = $false + +if ($Help) { + Write-Output "Usage: test.ps1 [-Install] [-Help]" + Write-Output "Build the installer for Windows.\n" + Write-Output "Options:" + Write-Output " -Install, -i Run the installer after building." + Write-Output " -Help, -h Show this help message." + exit 0 +} + +Push-Location -Path crates/zed +$channel = Get-Content "RELEASE_CHANNEL" +$env:ZED_RELEASE_CHANNEL = $channel +Pop-Location + +function CheckEnvironmentVariables { + $requiredVars = @( + 'ZED_WORKSPACE', 'RELEASE_VERSION', 'ZED_RELEASE_CHANNEL', + 'AZURE_TENANT_ID', 'AZURE_CLIENT_ID', 'AZURE_CLIENT_SECRET', + 'ACCOUNT_NAME', 'CERT_PROFILE_NAME', 'ENDPOINT', + 'FILE_DIGEST', 'TIMESTAMP_DIGEST', 'TIMESTAMP_SERVER' + ) + + foreach ($var in $requiredVars) { + if (-not (Test-Path "env:$var")) { + Write-Error "$var is not set" + exit 1 + } + } +} + +$innoDir = "$env:ZED_WORKSPACE\inno" + +function PrepareForBundle { + if (Test-Path "$innoDir") { + Remove-Item -Path "$innoDir" -Recurse -Force + } + New-Item -Path "$innoDir" -ItemType Directory -Force + Copy-Item -Path "$env:ZED_WORKSPACE\crates\zed\resources\windows\*" -Destination "$innoDir" -Recurse -Force + New-Item -Path "$innoDir\make_appx" -ItemType Directory -Force + New-Item -Path "$innoDir\appx" -ItemType Directory -Force + New-Item -Path "$innoDir\bin" -ItemType Directory -Force + New-Item -Path "$innoDir\tools" -ItemType Directory -Force +} + +function BuildZedAndItsFriends { + Write-Output "Building Zed and its friends, for channel: $channel" + # Build zed.exe, cli.exe and auto_update_helper.exe + cargo build --release --package zed --package cli --package auto_update_helper + Copy-Item -Path ".\target\release\zed.exe" -Destination "$innoDir\Zed.exe" -Force + Copy-Item -Path ".\target\release\cli.exe" -Destination "$innoDir\cli.exe" -Force + Copy-Item -Path ".\target\release\auto_update_helper.exe" -Destination "$innoDir\auto_update_helper.exe" -Force + # Build explorer_command_injector.dll + switch ($channel) { + "stable" { + cargo build --release --features stable --no-default-features --package explorer_command_injector + } + "preview" { + cargo build --release --features preview --no-default-features --package explorer_command_injector + } + default { + cargo build --release --package explorer_command_injector + } + } + Copy-Item -Path ".\target\release\explorer_command_injector.dll" -Destination "$innoDir\zed_explorer_command_injector.dll" -Force +} + +function ZipZedAndItsFriendsDebug { + $items = @( + ".\target\release\zed.pdb", + ".\target\release\cli.pdb", + ".\target\release\auto_update_helper.pdb", + ".\target\release\explorer_command_injector.pdb" + ) + + Compress-Archive -Path $items -DestinationPath ".\target\release\zed-$env:RELEASE_VERSION-$env:ZED_RELEASE_CHANNEL.dbg.zip" -Force +} + +function MakeAppx { + switch ($channel) { + "stable" { + $manifestFile = "$env:ZED_WORKSPACE\crates\explorer_command_injector\AppxManifest.xml" + } + "preview" { + $manifestFile = "$env:ZED_WORKSPACE\crates\explorer_command_injector\AppxManifest-Preview.xml" + } + default { + $manifestFile = "$env:ZED_WORKSPACE\crates\explorer_command_injector\AppxManifest-Nightly.xml" + } + } + Copy-Item -Path "$manifestFile" -Destination "$innoDir\make_appx\AppxManifest.xml" + # Add makeAppx.exe to Path + $sdk = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64" + $env:Path += ';' + $sdk + makeAppx.exe pack /d "$innoDir\make_appx" /p "$innoDir\zed_explorer_command_injector.appx" /nv +} + +function SignZedAndItsFriends { + $files = "$innoDir\Zed.exe,$innoDir\cli.exe,$innoDir\auto_update_helper.exe,$innoDir\zed_explorer_command_injector.dll,$innoDir\zed_explorer_command_injector.appx" + & "$innoDir\sign.ps1" $files +} + +function CollectFiles { + Move-Item -Path "$innoDir\zed_explorer_command_injector.appx" -Destination "$innoDir\appx\zed_explorer_command_injector.appx" -Force + Move-Item -Path "$innoDir\zed_explorer_command_injector.dll" -Destination "$innoDir\appx\zed_explorer_command_injector.dll" -Force + Move-Item -Path "$innoDir\cli.exe" -Destination "$innoDir\bin\zed.exe" -Force + Move-Item -Path "$innoDir\auto_update_helper.exe" -Destination "$innoDir\tools\auto_update_helper.exe" -Force +} + +function BuildInstaller { + $issFilePath = "$innoDir\zed.iss" + switch ($channel) { + "stable" { + $appId = "{{2DB0DA96-CA55-49BB-AF4F-64AF36A86712}" + $appIconName = "app-icon" + $appName = "Zed Editor" + $appDisplayName = "Zed Editor" + $appSetupName = "ZedEditorUserSetup-x64-$env:RELEASE_VERSION" + # The mutex name here should match the mutex name in crates\zed\src\zed\windows_only_instance.rs + $appMutex = "Zed-Editor-Stable-Instance-Mutex" + $appExeName = "Zed" + $regValueName = "ZedEditor" + $appUserId = "ZedIndustries.Zed" + $appShellNameShort = "Z&ed Editor" + $appAppxFullName = "ZedIndustries.Zed_1.0.0.0_neutral__japxn1gcva8rg" + } + "preview" { + $appId = "{{F70E4811-D0E2-4D88-AC99-D63752799F95}" + $appIconName = "app-icon-preview" + $appName = "Zed Editor Preview" + $appDisplayName = "Zed Editor Preview" + $appSetupName = "ZedEditorUserSetup-x64-$env:RELEASE_VERSION-preview" + # The mutex name here should match the mutex name in crates\zed\src\zed\windows_only_instance.rs + $appMutex = "Zed-Editor-Preview-Instance-Mutex" + $appExeName = "Zed" + $regValueName = "ZedEditorPreview" + $appUserId = "ZedIndustries.Zed.Preview" + $appShellNameShort = "Z&ed Editor Preview" + $appAppxFullName = "ZedIndustries.Zed.Preview_1.0.0.0_neutral__japxn1gcva8rg" + } + "nightly" { + $appId = "{{1BDB21D3-14E7-433C-843C-9C97382B2FE0}" + $appIconName = "app-icon-nightly" + $appName = "Zed Editor Nightly" + $appDisplayName = "Zed Editor Nightly" + $appSetupName = "ZedEditorUserSetup-x64-$env:RELEASE_VERSION-nightly" + # The mutex name here should match the mutex name in crates\zed\src\zed\windows_only_instance.rs + $appMutex = "Zed-Editor-Nightly-Instance-Mutex" + $appExeName = "Zed" + $regValueName = "ZedEditorNightly" + $appUserId = "ZedIndustries.Zed.Nightly" + $appShellNameShort = "Z&ed Editor Nightly" + $appAppxFullName = "ZedIndustries.Zed.Nightly_1.0.0.0_neutral__japxn1gcva8rg" + } + "dev" { + $appId = "{{8357632E-24A4-4F32-BA97-E575B4D1FE5D}" + $appIconName = "app-icon-nightly" + $appName = "Zed Editor Dev" + $appDisplayName = "Zed Editor Dev" + $appSetupName = "ZedEditorUserSetup-x64-$env:RELEASE_VERSION-dev" + # The mutex name here should match the mutex name in crates\zed\src\zed\windows_only_instance.rs + $appMutex = "Zed-Editor-Dev-Instance-Mutex" + $appExeName = "Zed" + $regValueName = "ZedEditorDev" + $appUserId = "ZedIndustries.Zed.Dev" + $appShellNameShort = "Z&ed Editor Dev" + $appAppxFullName = "ZedIndustries.Zed.Dev_1.0.0.0_neutral__japxn1gcva8rg" + } + default { + Write-Error "can't bundle installer for $channel." + exit 1 + } + } + + # Windows runner 2022 default has iscc in PATH, https://github.com/actions/runner-images/blob/main/images/windows/Windows2022-Readme.md + # Currently, we are using Windows 2022 runner. + # Windows runner 2025 doesn't have iscc in PATH for now, https://github.com/actions/runner-images/issues/11228 + # $innoSetupPath = "iscc.exe" + $innoSetupPath = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" + + $definitions = @{ + "AppId" = $appId + "AppIconName" = $appIconName + "OutputDir" = "$env:ZED_WORKSPACE\target" + "AppSetupName" = $appSetupName + "AppName" = $appName + "AppDisplayName" = $appDisplayName + "RegValueName" = $regValueName + "AppMutex" = $appMutex + "AppExeName" = $appExeName + "ResourcesDir" = "$innoDir" + "ShellNameShort" = $appShellNameShort + "AppUserId" = $appUserId + "Version" = "$env:RELEASE_VERSION" + "SourceDir" = "$env:ZED_WORKSPACE" + "AppxFullName" = $appAppxFullName + } + + $signTool = "powershell.exe -ExecutionPolicy Bypass -File $innoDir\sign.ps1 `$f" + + $defs = @() + foreach ($key in $definitions.Keys) { + $defs += "/d$key=`"$($definitions[$key])`"" + } + + $innoArgs = @($issFilePath) + $defs + "/sDefaultsign=`"$signTool`"" + + # Execute Inno Setup + Write-Host "🚀 Running Inno Setup: $innoSetupPath $innoArgs" + $process = Start-Process -FilePath $innoSetupPath -ArgumentList $innoArgs -NoNewWindow -Wait -PassThru + + if ($process.ExitCode -eq 0) { + Write-Host "✅ Inno Setup successfully compiled the installer" + Write-Output "SETUP_PATH=target/$appSetupName.exe" >> $env:GITHUB_ENV + $script:buildSuccess = $true + } + else { + Write-Host "❌ Inno Setup failed: $($process.ExitCode)" + $script:buildSuccess = $false + } +} + +ParseZedWorkspace +CheckEnvironmentVariables +PrepareForBundle +BuildZedAndItsFriends +MakeAppx +SignZedAndItsFriends +ZipZedAndItsFriendsDebug +CollectFiles +BuildInstaller + +$debugArchive = ".\target\release\zed-$env:RELEASE_VERSION-$env:ZED_RELEASE_CHANNEL.dbg.zip" +$debugStoreKey = "$env:ZED_RELEASE_CHANNEL/zed-$env:RELEASE_VERSION-$env:ZED_RELEASE_CHANNEL.dbg.zip" +UploadToBlobStorePublic -BucketName "zed-debug-symbols" -FileToUpload $debugArchive -BlobStoreKey $debugStoreKey + +if ($buildSuccess) { + Write-Output "Build successful" + if ($Install) { + Write-Output "Installing Zed..." + Start-Process -FilePath "$env:ZED_WORKSPACE/target/ZedEditorUserSetup-x64-$env:RELEASE_VERSION.exe" + } + exit 0 +} +else { + Write-Output "Build failed" + exit 1 +} diff --git a/script/clear-target-dir-if-larger-than.ps1 b/script/clear-target-dir-if-larger-than.ps1 index 8bb77997d6309a9a41791b8c6de364a649795316..c18c308624d93937d88392ef519f331afced8077 100644 --- a/script/clear-target-dir-if-larger-than.ps1 +++ b/script/clear-target-dir-if-larger-than.ps1 @@ -18,5 +18,5 @@ Write-Host "target directory size: ${current_size_gb}GB. max size: ${MAX_SIZE_IN if ($current_size_gb -gt $MAX_SIZE_IN_GB) { Write-Host "clearing target directory" - Remove-Item -Recurse -Force -Path "target\*" + Remove-Item -Recurse -Force -Path "target\*" -ErrorAction SilentlyContinue } diff --git a/script/determine-release-channel.ps1 b/script/determine-release-channel.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..eb3ad9c0059ef323117072ca7a6c713ccc7dda0f --- /dev/null +++ b/script/determine-release-channel.ps1 @@ -0,0 +1,37 @@ +$ErrorActionPreference = "Stop" + +if (-not $env:GITHUB_ACTIONS) { + Write-Error "Error: This script must be run in a GitHub Actions environment" + exit 1 +} + +if (-not $env:GITHUB_REF) { + Write-Error "Error: GITHUB_REF is not set" + exit 1 +} + +$version = & "script/get-crate-version.ps1" "zed" +$channel = Get-Content "crates/zed/RELEASE_CHANNEL" + +Write-Host "Publishing version: $version on release channel $channel" +Write-Output "RELEASE_CHANNEL=$channel" >> $env:GITHUB_ENV +Write-Output "RELEASE_VERSION=$version" >> $env:GITHUB_ENV + +$expectedTagName = "" +switch ($channel) { + "stable" { + $expectedTagName = "v$version" + } + "preview" { + $expectedTagName = "v$version-pre" + } + default { + Write-Error "can't publish a release on channel $channel" + exit 1 + } +} + +if ($env:GITHUB_REF_NAME -ne $expectedTagName) { + Write-Error "invalid release tag $($env:GITHUB_REF_NAME). expected $expectedTagName" + exit 1 +} diff --git a/script/get-crate-version.ps1 b/script/get-crate-version.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..d86c971e32515d5a409868c55a240f4a0780f8b6 --- /dev/null +++ b/script/get-crate-version.ps1 @@ -0,0 +1,16 @@ +if ($args.Length -ne 1) { + Write-Error "Usage: $($MyInvocation.MyCommand.Name) " + exit 1 +} + +$crateName = $args[0] + +$metadata = cargo metadata --no-deps --format-version=1 | ConvertFrom-Json + +$package = $metadata.packages | Where-Object { $_.name -eq $crateName } +if ($package) { + $package.version +} +else { + Write-Error "Crate '$crateName' not found." +} diff --git a/script/lib/blob-store.ps1 b/script/lib/blob-store.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..38bf1682d24ba7ec7fe6db1c7201ffa128f5ba1a --- /dev/null +++ b/script/lib/blob-store.ps1 @@ -0,0 +1,68 @@ +function UploadToBlobStoreWithACL { + param ( + [string]$BucketName, + [string]$FileToUpload, + [string]$BlobStoreKey, + [string]$ACL + ) + + # Format date to match AWS requirements + $Date = (Get-Date).ToUniversalTime().ToString("r") + # Note: Original script had a bug where it overrode the ACL parameter + # I'm keeping the same behavior for compatibility + $ACL = "public-read" + $ContentType = "application/octet-stream" + $StorageClass = "STANDARD" + + # Create string to sign (AWS S3 compatible format) + $StringToSign = "PUT`n`n${ContentType}`n${Date}`nx-amz-acl:${ACL}`nx-amz-storage-class:${StorageClass}`n/${BucketName}/${BlobStoreKey}" + + # Generate HMAC-SHA1 signature + $HMACSHA1 = New-Object System.Security.Cryptography.HMACSHA1 + $HMACSHA1.Key = [System.Text.Encoding]::UTF8.GetBytes($env:DIGITALOCEAN_SPACES_SECRET_KEY) + $Signature = [System.Convert]::ToBase64String($HMACSHA1.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($StringToSign))) + + # Upload file using Invoke-WebRequest (equivalent to curl) + $Headers = @{ + "Host" = "${BucketName}.nyc3.digitaloceanspaces.com" + "Date" = $Date + "Content-Type" = $ContentType + "x-amz-storage-class" = $StorageClass + "x-amz-acl" = $ACL + "Authorization" = "AWS ${env:DIGITALOCEAN_SPACES_ACCESS_KEY}:$Signature" + } + + $Uri = "https://${BucketName}.nyc3.digitaloceanspaces.com/${BlobStoreKey}" + + # Read file content + $FileContent = Get-Content $FileToUpload -Raw -AsByteStream + + try { + Invoke-WebRequest -Uri $Uri -Method PUT -Headers $Headers -Body $FileContent -ContentType $ContentType -Verbose + Write-Host "Successfully uploaded $FileToUpload to $Uri" -ForegroundColor Green + } + catch { + Write-Error "Failed to upload file: $_" + throw $_ + } +} + +function UploadToBlobStorePublic { + param ( + [string]$BucketName, + [string]$FileToUpload, + [string]$BlobStoreKey + ) + + UploadToBlobStoreWithACL -BucketName $BucketName -FileToUpload $FileToUpload -BlobStoreKey $BlobStoreKey -ACL "public-read" +} + +function UploadToBlobStore { + param ( + [string]$BucketName, + [string]$FileToUpload, + [string]$BlobStoreKey + ) + + UploadToBlobStoreWithACL -BucketName $BucketName -FileToUpload $FileToUpload -BlobStoreKey $BlobStoreKey -ACL "private" +} diff --git a/script/lib/workspace.ps1 b/script/lib/workspace.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..c6fdc274c120522aeee1ed3e4d524c23460d4da8 --- /dev/null +++ b/script/lib/workspace.ps1 @@ -0,0 +1,6 @@ + +function ParseZedWorkspace { + $metadata = cargo metadata --no-deps --offline | ConvertFrom-Json + $env:ZED_WORKSPACE = $metadata.workspace_root + $env:RELEASE_VERSION = $metadata.packages | Where-Object { $_.name -eq "zed" } | Select-Object -ExpandProperty version +} diff --git a/script/upload-nightly.ps1 b/script/upload-nightly.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..7fa453c806b91369851c20042c5131a30ccf77a7 --- /dev/null +++ b/script/upload-nightly.ps1 @@ -0,0 +1,60 @@ +# Based on the template in: https://docs.digitalocean.com/reference/api/spaces-api/ +$ErrorActionPreference = "Stop" +. "$PSScriptRoot\lib\blob-store.ps1" +. "$PSScriptRoot\lib\workspace.ps1" + +$allowedTargets = @("windows") + +function Test-AllowedTarget { + param ( + [string]$Target + ) + + return $allowedTargets -contains $Target +} + +# Process arguments +if ($args.Count -gt 0) { + $target = $args[0] + if (Test-AllowedTarget $target) { + # Valid target + } else { + Write-Error "Error: Target '$target' is not allowed.`nUsage: $($MyInvocation.MyCommand.Name) [$($allowedTargets -join ', ')]" + exit 1 + } +} else { + Write-Error "Error: Target is not specified.`nUsage: $($MyInvocation.MyCommand.Name) [$($allowedTargets -join ', ')]" + exit 1 +} + +ParseZedWorkspace +Write-Host "Uploading nightly for target: $target" + +$bucketName = "zed-nightly-host" + +# Get current git SHA +$sha = git rev-parse HEAD +$sha | Out-File -FilePath "target/latest-sha" -NoNewline + +# TODO: +# Upload remote server files +# $remoteServerFiles = Get-ChildItem -Path "target" -Filter "zed-remote-server-*.gz" -Recurse -File +# foreach ($file in $remoteServerFiles) { +# Upload-ToBlobStore -BucketName $bucketName -FileToUpload $file.FullName -BlobStoreKey "nightly/$($file.Name)" +# Remove-Item -Path $file.FullName +# } + +switch ($target) { + "windows" { + UploadToBlobStore -BucketName $bucketName -FileToUpload $env:SETUP_PATH -BlobStoreKey "nightly/zed_editor_installer_x86_64.exe" + UploadToBlobStore -BucketName $bucketName -FileToUpload "target/latest-sha" -BlobStoreKey "nightly/latest-sha-windows" + + Remove-Item -Path $env:SETUP_PATH -ErrorAction SilentlyContinue + Remove-Item -Path "target/latest-sha" -ErrorAction SilentlyContinue + } + + default { + Write-Error "Error: Unknown target '$target'" + exit 1 + } +} diff --git a/typos.toml b/typos.toml index 83d1f2967d4bb8872d27efa2932533defca0a708..7f1c6e04f12867f4b3b88d64d6b2cad06dd9d509 100644 --- a/typos.toml +++ b/typos.toml @@ -46,6 +46,8 @@ extend-exclude = [ "script/danger/dangerfile.ts", # Eval examples for prompts and criteria "crates/eval/src/examples/", + # File type extensions are not typos + "crates/zed/resources/windows/zed.iss", # typos-cli doesn't understand our `vˇariable` markup "crates/editor/src/hover_links.rs", # typos-cli doesn't understand `setis` is intentional test case From 8e8a772c2d40348740125b7ca851c94fdac0072b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 8 Jul 2025 21:24:43 -0600 Subject: [PATCH 100/239] vim: Add U to undo last line (#33571) Closes #14760 Still TODO: * Vim actually undoes *many* changes if they're all on the same line. Release Notes: - vim: Add `U` to return to the last changed line and undo --- assets/keymaps/vim.json | 1 + crates/editor/src/editor.rs | 42 +++- crates/vim/src/normal.rs | 216 +++++++++++++++++- crates/vim/src/vim.rs | 2 + crates/vim/test_data/test_undo_last_line.json | 14 ++ .../test_undo_last_line_newline.json | 15 ++ ...t_undo_last_line_newline_many_changes.json | 21 ++ 7 files changed, 303 insertions(+), 8 deletions(-) create mode 100644 crates/vim/test_data/test_undo_last_line.json create mode 100644 crates/vim/test_data/test_undo_last_line_newline.json create mode 100644 crates/vim/test_data/test_undo_last_line_newline_many_changes.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 4b48b26ef48536632942c03bd84000e089e01f62..571192a4791846011318238ade9aad84091bca4d 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -364,6 +364,7 @@ "p": "vim::Paste", "shift-p": ["vim::Paste", { "before": true }], "u": "vim::Undo", + "shift-u": "vim::UndoLastLine", "r": "vim::PushReplace", "s": "vim::Substitute", "shift-s": "vim::SubstituteLine", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6d529287a778e1b56994f80084dfb52f33d1e893..03e2124742e0d24cff213bbd9fff18db9c2c24b7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -865,9 +865,19 @@ pub trait Addon: 'static { } } +struct ChangeLocation { + current: Option>, + original: Vec, +} +impl ChangeLocation { + fn locations(&self) -> &[Anchor] { + self.current.as_ref().unwrap_or(&self.original) + } +} + /// A set of caret positions, registered when the editor was edited. pub struct ChangeList { - changes: Vec>, + changes: Vec, /// Currently "selected" change. position: Option, } @@ -894,20 +904,38 @@ impl ChangeList { (prev + count).min(self.changes.len() - 1) }; self.position = Some(next); - self.changes.get(next).map(|anchors| anchors.as_slice()) + self.changes.get(next).map(|change| change.locations()) } /// Adds a new change to the list, resetting the change list position. - pub fn push_to_change_list(&mut self, pop_state: bool, new_positions: Vec) { + pub fn push_to_change_list(&mut self, group: bool, new_positions: Vec) { self.position.take(); - if pop_state { - self.changes.pop(); + if let Some(last) = self.changes.last_mut() + && group + { + last.current = Some(new_positions) + } else { + self.changes.push(ChangeLocation { + original: new_positions, + current: None, + }); } - self.changes.push(new_positions.clone()); } pub fn last(&self) -> Option<&[Anchor]> { - self.changes.last().map(|anchors| anchors.as_slice()) + self.changes.last().map(|change| change.locations()) + } + + pub fn last_before_grouping(&self) -> Option<&[Anchor]> { + self.changes.last().map(|change| change.original.as_slice()) + } + + pub fn invert_last_group(&mut self) { + if let Some(last) = self.changes.last_mut() { + if let Some(current) = last.current.as_mut() { + mem::swap(&mut last.original, current); + } + } } } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index f772c446fe3fcd791f75e830ffb68b98799dcb46..baaf6bc3c448e5ef4207a659813c5a353a0be2d6 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -24,9 +24,9 @@ use crate::{ }; use collections::BTreeSet; use convert::ConvertTarget; -use editor::Bias; use editor::Editor; use editor::{Anchor, SelectionEffects}; +use editor::{Bias, ToPoint}; use editor::{display_map::ToDisplayPoint, movement}; use gpui::{Context, Window, actions}; use language::{Point, SelectionGoal}; @@ -90,6 +90,8 @@ actions!( Undo, /// Redoes the last undone change. Redo, + /// Undoes all changes to the most recently changed line. + UndoLastLine, ] ); @@ -194,6 +196,120 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { } }); }); + Vim::action(editor, cx, |vim, _: &UndoLastLine, window, cx| { + Vim::take_forced_motion(cx); + vim.update_editor(window, cx, |vim, editor, window, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let Some(last_change) = editor.change_list.last_before_grouping() else { + return; + }; + + let anchors = last_change.iter().cloned().collect::>(); + let mut last_row = None; + let ranges: Vec<_> = anchors + .iter() + .filter_map(|anchor| { + let point = anchor.to_point(&snapshot); + if last_row == Some(point.row) { + return None; + } + last_row = Some(point.row); + let line_range = Point::new(point.row, 0) + ..Point::new(point.row, snapshot.line_len(MultiBufferRow(point.row))); + Some(( + snapshot.anchor_before(line_range.start) + ..snapshot.anchor_after(line_range.end), + line_range, + )) + }) + .collect(); + + let edits = editor.buffer().update(cx, |buffer, cx| { + let current_content = ranges + .iter() + .map(|(anchors, _)| { + buffer + .snapshot(cx) + .text_for_range(anchors.clone()) + .collect::() + }) + .collect::>(); + let mut content_before_undo = current_content.clone(); + let mut undo_count = 0; + + loop { + let undone_tx = buffer.undo(cx); + undo_count += 1; + let mut content_after_undo = Vec::new(); + + let mut line_changed = false; + for ((anchors, _), text_before_undo) in + ranges.iter().zip(content_before_undo.iter()) + { + let snapshot = buffer.snapshot(cx); + let text_after_undo = + snapshot.text_for_range(anchors.clone()).collect::(); + + if &text_after_undo != text_before_undo { + line_changed = true; + } + content_after_undo.push(text_after_undo); + } + + content_before_undo = content_after_undo; + if !line_changed { + break; + } + if undone_tx == vim.undo_last_line_tx { + break; + } + } + + let edits = ranges + .into_iter() + .zip(content_before_undo.into_iter().zip(current_content)) + .filter_map(|((_, mut points), (mut old_text, new_text))| { + if new_text == old_text { + return None; + } + let common_suffix_starts_at = old_text + .char_indices() + .rev() + .zip(new_text.chars().rev()) + .find_map( + |((i, a), b)| { + if a != b { Some(i + a.len_utf8()) } else { None } + }, + ) + .unwrap_or(old_text.len()); + points.end.column -= (old_text.len() - common_suffix_starts_at) as u32; + old_text = old_text.split_at(common_suffix_starts_at).0.to_string(); + let common_prefix_len = old_text + .char_indices() + .zip(new_text.chars()) + .find_map(|((i, a), b)| if a != b { Some(i) } else { None }) + .unwrap_or(0); + points.start.column = common_prefix_len as u32; + old_text = old_text.split_at(common_prefix_len).1.to_string(); + + Some((points, old_text)) + }) + .collect::>(); + + for _ in 0..undo_count { + buffer.redo(cx); + } + edits + }); + vim.undo_last_line_tx = editor.transact(window, cx, |editor, window, cx| { + editor.change_list.invert_last_group(); + editor.edit(edits, cx); + editor.change_selections(SelectionEffects::default(), window, cx, |s| { + s.select_anchor_ranges(anchors.into_iter().map(|a| a..a)); + }) + }); + }); + }); repeat::register(editor, cx); scroll::register(editor, cx); @@ -1876,4 +1992,102 @@ mod test { cx.simulate_shared_keystrokes("ctrl-o").await; cx.shared_state().await.assert_matches(); } + + #[gpui::test] + async fn test_undo_last_line(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + ˇfn a() { } + fn a() { } + fn a() { } + "}) + .await; + // do a jump to reset vim's undo grouping + cx.simulate_shared_keystrokes("shift-g").await; + cx.shared_state().await.assert_matches(); + cx.simulate_shared_keystrokes("r a").await; + cx.shared_state().await.assert_matches(); + cx.simulate_shared_keystrokes("shift-u").await; + cx.shared_state().await.assert_matches(); + cx.simulate_shared_keystrokes("shift-u").await; + cx.shared_state().await.assert_matches(); + cx.simulate_shared_keystrokes("g g shift-u").await; + cx.shared_state().await.assert_matches(); + } + + #[gpui::test] + async fn test_undo_last_line_newline(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + ˇfn a() { } + fn a() { } + fn a() { } + "}) + .await; + // do a jump to reset vim's undo grouping + cx.simulate_shared_keystrokes("shift-g k").await; + cx.shared_state().await.assert_matches(); + cx.simulate_shared_keystrokes("o h e l l o escape").await; + cx.shared_state().await.assert_matches(); + cx.simulate_shared_keystrokes("shift-u").await; + cx.shared_state().await.assert_matches(); + cx.simulate_shared_keystrokes("shift-u").await; + } + + #[gpui::test] + async fn test_undo_last_line_newline_many_changes(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + ˇfn a() { } + fn a() { } + fn a() { } + "}) + .await; + // do a jump to reset vim's undo grouping + cx.simulate_shared_keystrokes("x shift-g k").await; + cx.shared_state().await.assert_matches(); + cx.simulate_shared_keystrokes("x f a x f { x").await; + cx.shared_state().await.assert_matches(); + cx.simulate_shared_keystrokes("shift-u").await; + cx.shared_state().await.assert_matches(); + cx.simulate_shared_keystrokes("shift-u").await; + cx.shared_state().await.assert_matches(); + cx.simulate_shared_keystrokes("shift-u").await; + cx.shared_state().await.assert_matches(); + cx.simulate_shared_keystrokes("shift-u").await; + cx.shared_state().await.assert_matches(); + } + + #[gpui::test] + async fn test_undo_last_line_multicursor(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state( + indoc! {" + ˇone two ˇone + two ˇone two + "}, + Mode::Normal, + ); + cx.simulate_keystrokes("3 r a"); + cx.assert_state( + indoc! {" + aaˇa two aaˇa + two aaˇa two + "}, + Mode::Normal, + ); + cx.simulate_keystrokes("escape escape"); + cx.simulate_keystrokes("shift-u"); + cx.set_state( + indoc! {" + onˇe two onˇe + two onˇe two + "}, + Mode::Normal, + ); + } } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 9229f145d92f99725662d892da9f8eef3a980238..95a08d7c66a49b0ca3f0f1d40ecc378276fbf131 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -375,6 +375,7 @@ pub(crate) struct Vim { pub(crate) current_tx: Option, pub(crate) current_anchor: Option>, pub(crate) undo_modes: HashMap, + pub(crate) undo_last_line_tx: Option, selected_register: Option, pub search: SearchState, @@ -422,6 +423,7 @@ impl Vim { stored_visual_mode: None, current_tx: None, + undo_last_line_tx: None, current_anchor: None, undo_modes: HashMap::default(), diff --git a/crates/vim/test_data/test_undo_last_line.json b/crates/vim/test_data/test_undo_last_line.json new file mode 100644 index 0000000000000000000000000000000000000000..a2f6fc0995a872939b8ff1d920b74e1950c14492 --- /dev/null +++ b/crates/vim/test_data/test_undo_last_line.json @@ -0,0 +1,14 @@ +{"Put":{"state":"ˇfn a() { }\nfn a() { }\nfn a() { }\n"}} +{"Key":"shift-g"} +{"Get":{"state":"fn a() { }\nfn a() { }\nfn a() { }\nˇ","mode":"Normal"}} +{"Key":"r"} +{"Key":"a"} +{"Get":{"state":"fn a() { }\nfn a() { }\nfn a() { }\nˇ","mode":"Normal"}} +{"Key":"shift-u"} +{"Get":{"state":"ˇ\nfn a() { }\nfn a() { }\n","mode":"Normal"}} +{"Key":"shift-u"} +{"Get":{"state":"ˇfn a() { }\nfn a() { }\nfn a() { }\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"g"} +{"Key":"shift-u"} +{"Get":{"state":"ˇ\nfn a() { }\nfn a() { }\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_undo_last_line_newline.json b/crates/vim/test_data/test_undo_last_line_newline.json new file mode 100644 index 0000000000000000000000000000000000000000..2b21ccef097950bb8e89db284675dfe172435e5f --- /dev/null +++ b/crates/vim/test_data/test_undo_last_line_newline.json @@ -0,0 +1,15 @@ +{"Put":{"state":"ˇfn a() { }\nfn a() { }\nfn a() { }\n"}} +{"Key":"shift-g"} +{"Key":"k"} +{"Get":{"state":"fn a() { }\nfn a() { }\nˇfn a() { }\n","mode":"Normal"}} +{"Key":"o"} +{"Key":"h"} +{"Key":"e"} +{"Key":"l"} +{"Key":"l"} +{"Key":"o"} +{"Key":"escape"} +{"Get":{"state":"fn a() { }\nfn a() { }\nfn a() { }\nhellˇo\n","mode":"Normal"}} +{"Key":"shift-u"} +{"Get":{"state":"fn a() { }\nfn a() { }\nfn a() { }\nˇ\n","mode":"Normal"}} +{"Key":"shift-u"} diff --git a/crates/vim/test_data/test_undo_last_line_newline_many_changes.json b/crates/vim/test_data/test_undo_last_line_newline_many_changes.json new file mode 100644 index 0000000000000000000000000000000000000000..6615e8d79ad9130742837c7ecb600f8f675b41d0 --- /dev/null +++ b/crates/vim/test_data/test_undo_last_line_newline_many_changes.json @@ -0,0 +1,21 @@ +{"Put":{"state":"ˇfn a() { }\nfn a() { }\nfn a() { }\n"}} +{"Key":"x"} +{"Key":"shift-g"} +{"Key":"k"} +{"Get":{"state":"n a() { }\nfn a() { }\nˇfn a() { }\n","mode":"Normal"}} +{"Key":"x"} +{"Key":"f"} +{"Key":"a"} +{"Key":"x"} +{"Key":"f"} +{"Key":"{"} +{"Key":"x"} +{"Get":{"state":"n a() { }\nfn a() { }\nn () ˇ }\n","mode":"Normal"}} +{"Key":"shift-u"} +{"Get":{"state":"n a() { }\nfn a() { }\nˇfn a() { }\n","mode":"Normal"}} +{"Key":"shift-u"} +{"Get":{"state":"n a() { }\nfn a() { }\nn () ˇ }\n","mode":"Normal"}} +{"Key":"shift-u"} +{"Get":{"state":"n a() { }\nfn a() { }\nˇfn a() { }\n","mode":"Normal"}} +{"Key":"shift-u"} +{"Get":{"state":"n a() { }\nfn a() { }\nn () ˇ }\n","mode":"Normal"}} From ecf4d5539ea1695a7408f5b8d542c0ca02e43315 Mon Sep 17 00:00:00 2001 From: Joel Courtney Date: Tue, 8 Jul 2025 20:34:20 -0700 Subject: [PATCH 101/239] helix: Stay in helix normal mode after helix delete (#34093) Currently, the HelixDelete action switches to (vim) Normal mode instead of HelixNormal mode. This adds a line to the helix delete action to stay in helix normal mode. There was already a commented-out test for this. I've uncommented it and it now passes. Release Notes: - helix: Fixed switching to vim NORMAL mode instead of HELIX_NORMAL mode after deletion --- crates/vim/src/helix.rs | 58 ++++++++++++++++++++-------------------- crates/vim/src/normal.rs | 1 + 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index e271c06a5e6942cac33e10783f123dcf3d963098..ec9b959b1220939394956e22e8936141c74fae1b 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -368,40 +368,40 @@ mod test { cx.assert_state("aa\n«ˇ »bb", Mode::HelixNormal); } - // #[gpui::test] - // async fn test_delete(cx: &mut gpui::TestAppContext) { - // let mut cx = VimTestContext::new(cx, true).await; + #[gpui::test] + async fn test_delete(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; - // // test delete a selection - // cx.set_state( - // indoc! {" - // The qu«ick ˇ»brown - // fox jumps over - // the lazy dog."}, - // Mode::HelixNormal, - // ); + // test delete a selection + cx.set_state( + indoc! {" + The qu«ick ˇ»brown + fox jumps over + the lazy dog."}, + Mode::HelixNormal, + ); - // cx.simulate_keystrokes("d"); + cx.simulate_keystrokes("d"); - // cx.assert_state( - // indoc! {" - // The quˇbrown - // fox jumps over - // the lazy dog."}, - // Mode::HelixNormal, - // ); + cx.assert_state( + indoc! {" + The quˇbrown + fox jumps over + the lazy dog."}, + Mode::HelixNormal, + ); - // // test deleting a single character - // cx.simulate_keystrokes("d"); + // test deleting a single character + cx.simulate_keystrokes("d"); - // cx.assert_state( - // indoc! {" - // The quˇrown - // fox jumps over - // the lazy dog."}, - // Mode::HelixNormal, - // ); - // } + cx.assert_state( + indoc! {" + The quˇrown + fox jumps over + the lazy dog."}, + Mode::HelixNormal, + ); + } // #[gpui::test] // async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) { diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index baaf6bc3c448e5ef4207a659813c5a353a0be2d6..6131032f4fab7b6ac7f3d2965413464317e55490 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -140,6 +140,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { }) }); vim.visual_delete(false, window, cx); + vim.switch_mode(Mode::HelixNormal, true, window, cx); }); Vim::action(editor, cx, |vim, _: &ChangeToEndOfLine, window, cx| { From acff48fc0d7a39199d713d2b11623332f1fe83d1 Mon Sep 17 00:00:00 2001 From: AidanV <84053180+AidanV@users.noreply.github.com> Date: Tue, 8 Jul 2025 22:43:43 -0700 Subject: [PATCH 102/239] vim: Add `:sp[lit] ` and `:vs[plit] ` support (#33686) Closes #32627 Release Notes: - Adds `:sp[lit] ` and `:vs[plit] ` support --- crates/vim/src/command.rs | 56 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index b24ca75e8bc1f922a86b011c9dcfc27a92b57e47..c001f55a41c9d488240cb59fcc70ba111cca988b 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -28,8 +28,8 @@ use std::{ use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId}; use ui::ActiveTheme; use util::ResultExt; -use workspace::notifications::DetachAndPromptErr; use workspace::{Item, SaveIntent, notifications::NotifyResultExt}; +use workspace::{SplitDirection, notifications::DetachAndPromptErr}; use zed_actions::{OpenDocs, RevealTarget}; use crate::{ @@ -175,6 +175,13 @@ struct VimSave { } /// Deletes the specified marks from the editor. +#[derive(Clone, PartialEq, Action)] +#[action(namespace = vim, no_json, no_register)] +struct VimSplit { + pub vertical: bool, + pub filename: String, +} + #[derive(Clone, PartialEq, Action)] #[action(namespace = vim, no_json, no_register)] enum DeleteMarks { @@ -323,6 +330,33 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { }); }); + Vim::action(editor, cx, |vim, action: &VimSplit, window, cx| { + let Some(workspace) = vim.workspace(window) else { + return; + }; + + workspace.update(cx, |workspace, cx| { + let project = workspace.project().clone(); + let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else { + return; + }; + let project_path = ProjectPath { + worktree_id: worktree.read(cx).id(), + path: Arc::from(Path::new(&action.filename)), + }; + + let direction = if action.vertical { + SplitDirection::vertical(cx) + } else { + SplitDirection::horizontal(cx) + }; + + workspace + .split_path_preview(project_path, false, Some(direction), window, cx) + .detach_and_log_err(cx); + }) + }); + Vim::action(editor, cx, |vim, action: &DeleteMarks, window, cx| { fn err(s: String, window: &mut Window, cx: &mut Context) { let _ = window.prompt( @@ -998,8 +1032,24 @@ fn generate_commands(_: &App) -> Vec { save_intent: Some(SaveIntent::Overwrite), }), VimCommand::new(("cq", "uit"), zed_actions::Quit), - VimCommand::new(("sp", "lit"), workspace::SplitHorizontal), - VimCommand::new(("vs", "plit"), workspace::SplitVertical), + VimCommand::new(("sp", "lit"), workspace::SplitHorizontal).args(|_, args| { + Some( + VimSplit { + vertical: false, + filename: args, + } + .boxed_clone(), + ) + }), + VimCommand::new(("vs", "plit"), workspace::SplitVertical).args(|_, args| { + Some( + VimSplit { + vertical: true, + filename: args, + } + .boxed_clone(), + ) + }), VimCommand::new( ("bd", "elete"), workspace::CloseActiveItem { From 6daf888fdbe5cdd402b69d1ecefa2ba5e40888f9 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 9 Jul 2025 00:05:46 -0600 Subject: [PATCH 103/239] More Tips'n'tricks (#34103) Document one way to avoid pathological cargo cache problems. Release Notes: - N/A --- docs/src/development/macos.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/src/development/macos.md b/docs/src/development/macos.md index ee6930c953184043ef78a2b61c31ef9fa21c50b0..91adf7819386b8e60306a692fed38bf142ccc26c 100644 --- a/docs/src/development/macos.md +++ b/docs/src/development/macos.md @@ -136,6 +136,21 @@ This error seems to be caused by OS resource constraints. Installing and running ## Tips & Tricks +### Avoiding continual rebuilds + +If you are finding that Zed is continually rebuilding root crates, it may be because +you are pointing your development Zed at the codebase itself. + +This causes problems because `cargo run` exports a bunch of environment +variables which are picked up by the `rust-analyzer` that runs in the development +build of Zed. These environment variables are in turn passed to `cargo check`, which +invalidates the build cache of some of the crates we depend on. + +You can easily avoid running the built binary on the checked-out Zed codebase using `cargo run +~/path/to/other/project` to ensure that you don't hit this. + +### Speeding up verification + If you are building Zed a lot, you may find that macOS continually verifies new builds which can add a few seconds to your iteration cycles. From 4ed206b37ce2b1713aa20361dfc74bda4a59b11c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 9 Jul 2025 00:14:04 -0600 Subject: [PATCH 104/239] vim: Implement /n and /c in :s (#34102) Closes #23345 Release Notes: - vim: Support /n and /c in :s// --- crates/editor/src/items.rs | 14 -- crates/project/src/search.rs | 3 + crates/search/src/buffer_search.rs | 20 ++ crates/vim/src/normal/search.rs | 273 +++++++++++++++++------ crates/vim/test_data/test_replace_g.json | 23 ++ crates/vim/test_data/test_replace_n.json | 13 ++ 6 files changed, 264 insertions(+), 82 deletions(-) create mode 100644 crates/vim/test_data/test_replace_g.json create mode 100644 crates/vim/test_data/test_replace_n.json diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index fa6bd93ab8558628670cb315e672ddf4fb3ebcab..2e4631a62b16db51476c5ce5918bdc973806381e 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1607,24 +1607,10 @@ impl SearchableItem for Editor { let text = self.buffer.read(cx); let text = text.snapshot(cx); let mut edits = vec![]; - let mut last_point: Option = None; for m in matches { - let point = m.start.to_point(&text); let text = text.text_for_range(m.clone()).collect::>(); - // Check if the row for the current match is different from the last - // match. If that's not the case and we're still replacing matches - // in the same row/line, skip this match if the `one_match_per_line` - // option is enabled. - if last_point.is_none() { - last_point = Some(point); - } else if last_point.is_some() && point.row != last_point.unwrap().row { - last_point = Some(point); - } else if query.one_match_per_line().is_some_and(|enabled| enabled) { - continue; - } - let text: Cow<_> = if text.len() == 1 { text.first().cloned().unwrap().into() } else { diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index d3585115f51a1a7dc317194f183156ac399cdf2b..44732b23cd4fb7ff8044b65e55d0ea09f0c3b5c9 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -404,6 +404,9 @@ impl SearchQuery { let start = line_offset + mat.start(); let end = line_offset + mat.end(); matches.push(start..end); + if self.one_match_per_line() == Some(true) { + break; + } } line_offset += line.len() + 1; diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 35c8fcd23098e4e5e3314263d56c73112ce0a768..c2590ec9b04df03434a9434ebbd44af9c6ebb698 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -939,6 +939,11 @@ impl BufferSearchBar { }); } + pub fn focus_replace(&mut self, window: &mut Window, cx: &mut Context) { + self.focus(&self.replacement_editor.focus_handle(cx), window, cx); + cx.notify(); + } + pub fn search( &mut self, query: &str, @@ -1092,6 +1097,21 @@ impl BufferSearchBar { } } + pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context) { + if let Some(searchable_item) = self.active_searchable_item.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + if matches.is_empty() { + return; + } + searchable_item.update_matches(matches, window, cx); + searchable_item.activate_match(0, matches, window, cx); + } + } + } + pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context) { if let Some(searchable_item) = self.active_searchable_item.as_ref() { if let Some(matches) = self diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 182e60e56c20a4377a9b531ae919293769af7dba..24f2cf751f10768fb9a7728e3180ef582f2266ef 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -71,11 +71,13 @@ pub struct ReplaceCommand { } #[derive(Clone, Debug, PartialEq)] -pub(crate) struct Replacement { +pub struct Replacement { search: String, replacement: String, - should_replace_all: bool, - is_case_sensitive: bool, + case_sensitive: Option, + flag_n: bool, + flag_g: bool, + flag_c: bool, } actions!( @@ -468,71 +470,89 @@ impl Vim { result.notify_err(workspace, cx); }) } - let vim = cx.entity().clone(); - pane.update(cx, |pane, cx| { - let mut options = SearchOptions::REGEX; + let Some(search_bar) = pane.update(cx, |pane, cx| { + pane.toolbar().read(cx).item_of_type::() + }) else { + return; + }; + let mut options = SearchOptions::REGEX; + let search = search_bar.update(cx, |search_bar, cx| { + if !search_bar.show(window, cx) { + return None; + } - let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() else { - return; + let search = if replacement.search.is_empty() { + search_bar.query(cx) + } else { + replacement.search }; - let search = search_bar.update(cx, |search_bar, cx| { - if !search_bar.show(window, cx) { - return None; - } - - if replacement.is_case_sensitive { - options.set(SearchOptions::CASE_SENSITIVE, true) - } - let search = if replacement.search.is_empty() { - search_bar.query(cx) - } else { - replacement.search - }; - if search_bar.should_use_smartcase_search(cx) { - options.set( - SearchOptions::CASE_SENSITIVE, - search_bar.is_contains_uppercase(&search), - ); - } - if !replacement.should_replace_all { - options.set(SearchOptions::ONE_MATCH_PER_LINE, true); - } + if let Some(case) = replacement.case_sensitive { + options.set(SearchOptions::CASE_SENSITIVE, case) + } else if search_bar.should_use_smartcase_search(cx) { + options.set( + SearchOptions::CASE_SENSITIVE, + search_bar.is_contains_uppercase(&search), + ); + } else { + options.set(SearchOptions::CASE_SENSITIVE, false) + } - search_bar.set_replacement(Some(&replacement.replacement), cx); - Some(search_bar.search(&search, Some(options), window, cx)) - }); - let Some(search) = search else { return }; - let search_bar = search_bar.downgrade(); - cx.spawn_in(window, async move |_, cx| { - search.await?; - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_last_match(window, cx); - search_bar.replace_all(&Default::default(), window, cx); - editor.update(cx, |editor, cx| editor.clear_search_within_ranges(cx)); - let _ = search_bar.search(&search_bar.query(cx), None, window, cx); - vim.update(cx, |vim, cx| { - vim.move_cursor( - Motion::StartOfLine { - display_lines: false, - }, - None, - window, - cx, - ) - }); + if !replacement.flag_g { + options.set(SearchOptions::ONE_MATCH_PER_LINE, true); + } - // Disable the `ONE_MATCH_PER_LINE` search option when finished, as - // this is not properly supported outside of vim mode, and - // not disabling it makes the "Replace All Matches" button - // actually replace only the first match on each line. - options.set(SearchOptions::ONE_MATCH_PER_LINE, false); - search_bar.set_search_options(options, cx); - })?; - anyhow::Ok(()) + search_bar.set_replacement(Some(&replacement.replacement), cx); + if replacement.flag_c { + search_bar.focus_replace(window, cx); + } + Some(search_bar.search(&search, Some(options), window, cx)) + }); + if replacement.flag_n { + self.move_cursor( + Motion::StartOfLine { + display_lines: false, + }, + None, + window, + cx, + ); + return; + } + let Some(search) = search else { return }; + let search_bar = search_bar.downgrade(); + cx.spawn_in(window, async move |vim, cx| { + search.await?; + search_bar.update_in(cx, |search_bar, window, cx| { + if replacement.flag_c { + search_bar.select_first_match(window, cx); + return; + } + search_bar.select_last_match(window, cx); + search_bar.replace_all(&Default::default(), window, cx); + editor.update(cx, |editor, cx| editor.clear_search_within_ranges(cx)); + let _ = search_bar.search(&search_bar.query(cx), None, window, cx); + vim.update(cx, |vim, cx| { + vim.move_cursor( + Motion::StartOfLine { + display_lines: false, + }, + None, + window, + cx, + ) + }) + .ok(); + + // Disable the `ONE_MATCH_PER_LINE` search option when finished, as + // this is not properly supported outside of vim mode, and + // not disabling it makes the "Replace All Matches" button + // actually replace only the first match on each line. + options.set(SearchOptions::ONE_MATCH_PER_LINE, false); + search_bar.set_search_options(options, cx); }) - .detach_and_log_err(cx); }) + .detach_and_log_err(cx); } } @@ -593,16 +613,19 @@ impl Replacement { let mut replacement = Replacement { search, replacement, - should_replace_all: false, - is_case_sensitive: true, + case_sensitive: None, + flag_g: false, + flag_n: false, + flag_c: false, }; for c in flags.chars() { match c { - 'g' => replacement.should_replace_all = true, - 'c' | 'n' => replacement.should_replace_all = false, - 'i' => replacement.is_case_sensitive = false, - 'I' => replacement.is_case_sensitive = true, + 'g' => replacement.flag_g = true, + 'n' => replacement.flag_n = true, + 'c' => replacement.flag_c = true, + 'i' => replacement.case_sensitive = Some(false), + 'I' => replacement.case_sensitive = Some(true), _ => {} } } @@ -913,7 +936,6 @@ mod test { }); } - // cargo test -p vim --features neovim test_replace_with_range_at_start #[gpui::test] async fn test_replace_with_range_at_start(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; @@ -979,6 +1001,121 @@ mod test { }); } + #[gpui::test] + async fn test_replace_n(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_shared_state(indoc! { + "ˇaa + bb + aa" + }) + .await; + + cx.simulate_shared_keystrokes(": s / b b / d d / n").await; + cx.simulate_shared_keystrokes("enter").await; + + cx.shared_state().await.assert_eq(indoc! { + "ˇaa + bb + aa" + }); + + let search_bar = cx.update_workspace(|workspace, _, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.toolbar() + .read(cx) + .item_of_type::() + .unwrap() + }) + }); + cx.update_entity(search_bar, |search_bar, _, cx| { + assert!(!search_bar.is_dismissed()); + assert_eq!(search_bar.query(cx), "bb".to_string()); + assert_eq!(search_bar.replacement(cx), "dd".to_string()); + }) + } + + #[gpui::test] + async fn test_replace_g(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_shared_state(indoc! { + "ˇaa aa aa aa + aa + aa" + }) + .await; + + cx.simulate_shared_keystrokes(": s / a a / b b").await; + cx.simulate_shared_keystrokes("enter").await; + cx.shared_state().await.assert_eq(indoc! { + "ˇbb aa aa aa + aa + aa" + }); + cx.simulate_shared_keystrokes(": s / a a / b b / g").await; + cx.simulate_shared_keystrokes("enter").await; + cx.shared_state().await.assert_eq(indoc! { + "ˇbb bb bb bb + aa + aa" + }); + } + + #[gpui::test] + async fn test_replace_c(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.set_state( + indoc! { + "ˇaa + aa + aa" + }, + Mode::Normal, + ); + + cx.simulate_keystrokes("v j : s / a a / d d / c"); + cx.simulate_keystrokes("enter"); + + cx.assert_state( + indoc! { + "ˇaa + aa + aa" + }, + Mode::Normal, + ); + + cx.simulate_keystrokes("enter"); + + cx.assert_state( + indoc! { + "dd + ˇaa + aa" + }, + Mode::Normal, + ); + + cx.simulate_keystrokes("enter"); + cx.assert_state( + indoc! { + "dd + ddˇ + aa" + }, + Mode::Normal, + ); + cx.simulate_keystrokes("enter"); + cx.assert_state( + indoc! { + "dd + ddˇ + aa" + }, + Mode::Normal, + ); + } + #[gpui::test] async fn test_replace_with_range(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; diff --git a/crates/vim/test_data/test_replace_g.json b/crates/vim/test_data/test_replace_g.json new file mode 100644 index 0000000000000000000000000000000000000000..583d1f89bc33adf2f13dc4dea1fa749ec53b122a --- /dev/null +++ b/crates/vim/test_data/test_replace_g.json @@ -0,0 +1,23 @@ +{"Put":{"state":"ˇaa aa aa aa\naa\naa"}} +{"Key":":"} +{"Key":"s"} +{"Key":"/"} +{"Key":"a"} +{"Key":"a"} +{"Key":"/"} +{"Key":"b"} +{"Key":"b"} +{"Key":"enter"} +{"Get":{"state":"ˇbb aa aa aa\naa\naa","mode":"Normal"}} +{"Key":":"} +{"Key":"s"} +{"Key":"/"} +{"Key":"a"} +{"Key":"a"} +{"Key":"/"} +{"Key":"b"} +{"Key":"b"} +{"Key":"/"} +{"Key":"g"} +{"Key":"enter"} +{"Get":{"state":"ˇbb bb bb bb\naa\naa","mode":"Normal"}} diff --git a/crates/vim/test_data/test_replace_n.json b/crates/vim/test_data/test_replace_n.json new file mode 100644 index 0000000000000000000000000000000000000000..a03c69e9b26b72b09494abe0e9969a4e051c452c --- /dev/null +++ b/crates/vim/test_data/test_replace_n.json @@ -0,0 +1,13 @@ +{"Put":{"state":"ˇaa\nbb\naa"}} +{"Key":":"} +{"Key":"s"} +{"Key":"/"} +{"Key":"b"} +{"Key":"b"} +{"Key":"/"} +{"Key":"d"} +{"Key":"d"} +{"Key":"/"} +{"Key":"n"} +{"Key":"enter"} +{"Get":{"state":"ˇaa\nbb\naa","mode":"Normal"}} From 1569b662ff8ede1d126b27d44966ddbf0e207cbf Mon Sep 17 00:00:00 2001 From: Daniel Sauble Date: Wed, 9 Jul 2025 00:08:23 -0700 Subject: [PATCH 105/239] editor: Change `drag_and_drop_selection` cursor on delay elapsed + Add `drag_and_drop_selection` delay setting (#33928) When [`drag_and_drop_selection` is true](https://zed.dev/docs/configuring-zed#drag-and-drop-selection), users can make a selection in the buffer and then drag and drop it to a new location. However, the editor forces users to wait 300ms after mouse down before dragging. If users try to drag before this delay has elapsed, they will create a new text selection instead, which can create the impression that drag and drop does not work. I made two changes to improve the UX of this feature: * If users do not want a delay before drag and drop is enabled, they can set the `drag_and_drop_selection.delay_ms` setting to 0. * If the user has done a mouse down on a text selection, the cursor changes to a copy affordance as soon as the configured delay has elapsed, rather than waiting for them to start dragging. This way they don't need to guess at when the delay has elapsed. The default settings for this feature are now: ``` "drag_and_drop_selection": { "enabled": true, "delay_ms": 300 } ``` Closes #33915 Before: https://github.com/user-attachments/assets/7b2f986f-9c67-4b2b-a10e-757c3e9c934b After: https://github.com/user-attachments/assets/726d0dbf-e58b-41ad-93d2-1a758640b422 Release Notes: - Migrate `drag_and_drop_selection` setting to `drag_and_drop_selection.enabled`. - Add `drag_and_drop_selection.delay_ms` setting to configure the delay that must elapse before drag and drop is allowed. - Show a ready to drag cursor affordance as soon as the delay has elapsed --------- Co-authored-by: Smit Barmase --- assets/settings/default.json | 7 +++- crates/editor/src/editor.rs | 3 -- crates/editor/src/editor_settings.rs | 28 +++++++++++--- crates/editor/src/element.rs | 25 +++++++++++-- crates/migrator/src/migrations.rs | 6 +++ .../src/migrations/m_2025_07_08/settings.rs | 37 +++++++++++++++++++ crates/migrator/src/migrator.rs | 8 ++++ docs/src/configuring-zed.md | 13 ++++--- 8 files changed, 110 insertions(+), 17 deletions(-) create mode 100644 crates/migrator/src/migrations/m_2025_07_08/settings.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index dc1040e1d0ab97520c198f5427fe6da0c8fbfd46..8c105b2c1ee425c4a0950fbaf31b0c3c67093a2d 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -228,7 +228,12 @@ // Whether to show code action button at start of buffer line. "inline_code_actions": true, // Whether to allow drag and drop text selection in buffer. - "drag_and_drop_selection": true, + "drag_and_drop_selection": { + // When true, enables drag and drop text selection in buffer. + "enabled": true, + // The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created. + "delay": 300 + }, // What to do when go to definition yields no results. // // 1. Do nothing: `none` diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 03e2124742e0d24cff213bbd9fff18db9c2c24b7..c5fe0db74cedca342aee3199d97ca27d9efd19c3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1170,7 +1170,6 @@ pub struct Editor { pub change_list: ChangeList, inline_value_cache: InlineValueCache, selection_drag_state: SelectionDragState, - drag_and_drop_selection_enabled: bool, next_color_inlay_id: usize, colors: Option, folding_newlines: Task<()>, @@ -2202,7 +2201,6 @@ impl Editor { change_list: ChangeList::new(), mode, selection_drag_state: SelectionDragState::None, - drag_and_drop_selection_enabled: EditorSettings::get_global(cx).drag_and_drop_selection, folding_newlines: Task::ready(()), }; if let Some(breakpoints) = editor.breakpoint_store.as_ref() { @@ -19899,7 +19897,6 @@ impl Editor { self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs; self.cursor_shape = editor_settings.cursor_shape.unwrap_or_default(); self.hide_mouse_mode = editor_settings.hide_mouse.unwrap_or_default(); - self.drag_and_drop_selection_enabled = editor_settings.drag_and_drop_selection; } if old_cursor_shape != self.cursor_shape { diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index d7b8bac359abe21ef3cc977518e828899a57e299..5d8379ddfb87600f7cd56d10f5684ed333589e78 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -52,7 +52,7 @@ pub struct EditorSettings { #[serde(default)] pub diagnostics_max_severity: Option, pub inline_code_actions: bool, - pub drag_and_drop_selection: bool, + pub drag_and_drop_selection: DragAndDropSelection, pub lsp_document_colors: DocumentColorsRenderMode, } @@ -275,6 +275,26 @@ pub struct ScrollbarAxes { pub vertical: bool, } +/// Whether to allow drag and drop text selection in buffer. +#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct DragAndDropSelection { + /// When true, enables drag and drop text selection in buffer. + /// + /// Default: true + #[serde(default = "default_true")] + pub enabled: bool, + + /// The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created. + /// + /// Default: 300 + #[serde(default = "default_drag_and_drop_selection_delay_ms")] + pub delay: u64, +} + +fn default_drag_and_drop_selection_delay_ms() -> u64 { + 300 +} + /// Which diagnostic indicators to show in the scrollbar. /// /// Default: all @@ -536,10 +556,8 @@ pub struct EditorSettingsContent { /// Default: true pub inline_code_actions: Option, - /// Whether to allow drag and drop text selection in buffer. - /// - /// Default: true - pub drag_and_drop_selection: Option, + /// Drag and drop related settings + pub drag_and_drop_selection: Option, /// How to render LSP `textDocument/documentColor` colors in the editor. /// diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a4463383359fedd5e199b0f65eabaa80f005838d..8a5bfb3babdc2a1f3b2594e6b97c8b7d2751e180 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -87,7 +87,6 @@ use util::{RangeExt, ResultExt, debug_panic}; use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt}; const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.; -const SELECTION_DRAG_DELAY: Duration = Duration::from_millis(300); /// Determines what kinds of highlights should be applied to a lines background. #[derive(Clone, Copy, Default)] @@ -644,7 +643,11 @@ impl EditorElement { return; } - if editor.drag_and_drop_selection_enabled && click_count == 1 { + if EditorSettings::get_global(cx) + .drag_and_drop_selection + .enabled + && click_count == 1 + { let newest_anchor = editor.selections.newest_anchor(); let snapshot = editor.snapshot(window, cx); let selection = newest_anchor.map(|anchor| anchor.to_display_point(&snapshot)); @@ -1022,7 +1025,10 @@ impl EditorElement { ref click_position, ref mouse_down_time, } => { - if mouse_down_time.elapsed() >= SELECTION_DRAG_DELAY { + let drag_and_drop_delay = Duration::from_millis( + EditorSettings::get_global(cx).drag_and_drop_selection.delay, + ); + if mouse_down_time.elapsed() >= drag_and_drop_delay { let drop_cursor = Selection { id: post_inc(&mut editor.selections.next_selection_id), start: drop_anchor, @@ -5710,6 +5716,19 @@ impl EditorElement { let editor = self.editor.read(cx); if editor.mouse_cursor_hidden { window.set_window_cursor_style(CursorStyle::None); + } else if let SelectionDragState::ReadyToDrag { + mouse_down_time, .. + } = &editor.selection_drag_state + { + let drag_and_drop_delay = Duration::from_millis( + EditorSettings::get_global(cx).drag_and_drop_selection.delay, + ); + if mouse_down_time.elapsed() >= drag_and_drop_delay { + window.set_cursor_style( + CursorStyle::DragCopy, + &layout.position_map.text_hitbox, + ); + } } else if matches!( editor.selection_drag_state, SelectionDragState::Dragging { .. } diff --git a/crates/migrator/src/migrations.rs b/crates/migrator/src/migrations.rs index 4e3839358b6e55b0cbec72f92ca12d9b2b30c168..9db597e9643866ba2fcbad00ea77d4de0c244fd1 100644 --- a/crates/migrator/src/migrations.rs +++ b/crates/migrator/src/migrations.rs @@ -93,3 +93,9 @@ pub(crate) mod m_2025_06_27 { pub(crate) use settings::SETTINGS_PATTERNS; } + +pub(crate) mod m_2025_07_08 { + mod settings; + + pub(crate) use settings::SETTINGS_PATTERNS; +} diff --git a/crates/migrator/src/migrations/m_2025_07_08/settings.rs b/crates/migrator/src/migrations/m_2025_07_08/settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..c9656491cea89dad81d19d4f8abfbd8b2c50403a --- /dev/null +++ b/crates/migrator/src/migrations/m_2025_07_08/settings.rs @@ -0,0 +1,37 @@ +use std::ops::Range; +use tree_sitter::{Query, QueryMatch}; + +use crate::MigrationPatterns; +use crate::patterns::SETTINGS_ROOT_KEY_VALUE_PATTERN; + +pub const SETTINGS_PATTERNS: MigrationPatterns = &[( + SETTINGS_ROOT_KEY_VALUE_PATTERN, + migrate_drag_and_drop_selection, +)]; + +fn migrate_drag_and_drop_selection( + contents: &str, + mat: &QueryMatch, + query: &Query, +) -> Option<(Range, String)> { + let name_ix = query.capture_index_for_name("name")?; + let name_range = mat.nodes_for_capture_index(name_ix).next()?.byte_range(); + let name = contents.get(name_range)?; + + if name != "drag_and_drop_selection" { + return None; + } + + let value_ix = query.capture_index_for_name("value")?; + let value_node = mat.nodes_for_capture_index(value_ix).next()?; + let value_range = value_node.byte_range(); + let value = contents.get(value_range.clone())?; + + match value { + "true" | "false" => { + let replacement = format!("{{\n \"enabled\": {}\n }}", value); + Some((value_range, replacement)) + } + _ => None, + } +} diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index be32b2734e66ebd621ff858a79eef322468b11ae..b425f7f1d5dc691ed1501d712ab72556412f7eb6 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -160,6 +160,10 @@ pub fn migrate_settings(text: &str) -> Result> { migrations::m_2025_06_27::SETTINGS_PATTERNS, &SETTINGS_QUERY_2025_06_27, ), + ( + migrations::m_2025_07_08::SETTINGS_PATTERNS, + &SETTINGS_QUERY_2025_07_08, + ), ]; run_migrations(text, migrations) } @@ -270,6 +274,10 @@ define_query!( SETTINGS_QUERY_2025_06_27, migrations::m_2025_06_27::SETTINGS_PATTERNS ); +define_query!( + SETTINGS_QUERY_2025_07_08, + migrations::m_2025_07_08::SETTINGS_PATTERNS +); // custom query static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock = LazyLock::new(|| { diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 8bba431554f5a83c97135772a8329cb822c3ccd4..eec9da60dd96d21652d78a8c4d0c0dfca17c207a 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1218,13 +1218,16 @@ or ### Drag And Drop Selection -- Description: Whether to allow drag and drop text selection in buffer. +- Description: Whether to allow drag and drop text selection in buffer. `delay` is the milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created. - Setting: `drag_and_drop_selection` -- Default: `true` - -**Options** +- Default: -`boolean` values +```json +"drag_and_drop_selection": { + "enabled": true, + "delay": 300 +} +``` ## Editor Toolbar From 45d200f2f8b86605c5137bf49441c74b5288cc51 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 9 Jul 2025 14:44:29 +0300 Subject: [PATCH 106/239] Wrap back around in context menu properly (#34112) When navigating back in the context menu, it was not possible to get past first element, if it was not selectable. The other way around works, hence the fix. Release Notes: - N/A --- crates/language_tools/src/lsp_tool.rs | 2 - crates/ui/Cargo.toml | 3 + crates/ui/src/components/context_menu.rs | 89 +++++++++++++++++++++--- 3 files changed, 81 insertions(+), 13 deletions(-) diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_tool.rs index 81cc38d33f6c7f19b304a4b52043329d448fd45a..9e542f582a994aeba8920ab3563899df8ed5bb94 100644 --- a/crates/language_tools/src/lsp_tool.rs +++ b/crates/language_tools/src/lsp_tool.rs @@ -735,8 +735,6 @@ impl LspTool { state.update(cx, |state, cx| state.fill_menu(menu, cx)) }); lsp_tool.lsp_menu = Some(menu.clone()); - // TODO kb will this work? - // what about the selections? lsp_tool.popover_menu_handle.refresh_menu( window, cx, diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 625bdc62f5e899912929539e89c5357ea4e7e8f6..c0472917721eaca37214cc21505c94fab54d21cd 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -34,6 +34,9 @@ workspace-hack.workspace = true [target.'cfg(windows)'.dependencies] windows.workspace = true +[dev-dependencies] +gpui = { workspace = true, features = ["test-support"] } + [features] default = [] stories = ["dep:story"] diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 075cf7a7d7a881fc308b0d2a7dcee3c9bdabcd57..873b7b9e63c87decfc321bd104170813587ba4c9 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -690,20 +690,15 @@ impl ContextMenu { cx: &mut Context, ) { if let Some(ix) = self.selected_index { - if ix == 0 { - self.handle_select_last(&SelectLast, window, cx); - } else { - for (ix, item) in self.items.iter().enumerate().take(ix).rev() { - if item.is_selectable() { - self.select_index(ix, window, cx); - cx.notify(); - break; - } + for (ix, item) in self.items.iter().enumerate().take(ix).rev() { + if item.is_selectable() { + self.select_index(ix, window, cx); + cx.notify(); + return; } } - } else { - self.handle_select_last(&SelectLast, window, cx); } + self.handle_select_last(&SelectLast, window, cx); } fn select_index( @@ -1171,3 +1166,75 @@ impl Render for ContextMenu { }))) } } + +#[cfg(test)] +mod tests { + use gpui::TestAppContext; + + use super::*; + + #[gpui::test] + fn can_navigate_back_over_headers(cx: &mut TestAppContext) { + let cx = cx.add_empty_window(); + let context_menu = cx.update(|window, cx| { + ContextMenu::build(window, cx, |menu, _, _| { + menu.header("First header") + .separator() + .entry("First entry", None, |_, _| {}) + .separator() + .separator() + .entry("Last entry", None, |_, _| {}) + }) + }); + + context_menu.update_in(cx, |context_menu, window, cx| { + assert_eq!( + None, context_menu.selected_index, + "No selection is in the menu initially" + ); + + context_menu.select_first(&SelectFirst, window, cx); + assert_eq!( + Some(2), + context_menu.selected_index, + "Should select first selectable entry, skipping the header and the separator" + ); + + context_menu.select_next(&SelectNext, window, cx); + assert_eq!( + Some(5), + context_menu.selected_index, + "Should select next selectable entry, skipping 2 separators along the way" + ); + + context_menu.select_next(&SelectNext, window, cx); + assert_eq!( + Some(2), + context_menu.selected_index, + "Should wrap around to first selectable entry" + ); + }); + + context_menu.update_in(cx, |context_menu, window, cx| { + assert_eq!( + Some(2), + context_menu.selected_index, + "Should start from the first selectable entry" + ); + + context_menu.select_previous(&SelectPrevious, window, cx); + assert_eq!( + Some(5), + context_menu.selected_index, + "Should wrap around to previous selectable entry (last)" + ); + + context_menu.select_previous(&SelectPrevious, window, cx); + assert_eq!( + Some(2), + context_menu.selected_index, + "Should go back to previous selectable entry (first)" + ); + }); + } +} From 7114a5ca99aa14016a278100f136a32dd7911521 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Wed, 9 Jul 2025 16:39:02 +0200 Subject: [PATCH 107/239] Fix panic in context server configuration (#34118) Release Notes: - Fixed a panic that could occur when configuring MCP servers --- .../src/agent_configuration/configure_context_server_modal.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs index ba0021c33ca32c50351387ab290bf33ce604b2e4..9e5f6e09c82489dd4ccdc89f188e962ceeec596d 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs @@ -740,7 +740,9 @@ fn wait_for_context_server( }); cx.spawn(async move |_cx| { - let result = rx.await.unwrap(); + let result = rx + .await + .map_err(|_| Arc::from("Context server store was dropped"))?; drop(subscription); result }) From 81cc1e8f753222be31ba3df8a912d8f8d71206b6 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 9 Jul 2025 20:26:21 +0530 Subject: [PATCH 108/239] project_panel: Improve last sticky item drifting logic (#34119) - Now instead of drifting directory along with last item of that directory, it waits till last item is completely consumed. Release Notes: - N/A --- crates/ui/src/components/sticky_items.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/ui/src/components/sticky_items.rs b/crates/ui/src/components/sticky_items.rs index da6c14ff0974716ba5fb8423a8ade07349cea36f..218f7aae3510213afeed9d80a28428ce9c0df28a 100644 --- a/crates/ui/src/components/sticky_items.rs +++ b/crates/ui/src/components/sticky_items.rs @@ -159,10 +159,9 @@ where let mut iter = entries.iter().enumerate().peekable(); while let Some((ix, current_entry)) = iter.next() { - let current_depth = current_entry.depth(); - let index_in_range = ix; + let depth = current_entry.depth(); - if current_depth < index_in_range { + if depth < ix { sticky_anchor = Some(StickyAnchor { entry: current_entry.clone(), index: visible_range.start + ix, @@ -172,9 +171,15 @@ where if let Some(&(_next_ix, next_entry)) = iter.peek() { let next_depth = next_entry.depth(); + let next_item_outdented = next_depth + 1 == depth; - if next_depth < current_depth && next_depth < index_in_range { - last_item_is_drifting = true; + let depth_same_as_index = depth == ix; + let depth_greater_than_index = depth == ix + 1; + + if next_item_outdented && (depth_same_as_index || depth_greater_than_index) { + if depth_greater_than_index { + last_item_is_drifting = true; + } sticky_anchor = Some(StickyAnchor { entry: current_entry.clone(), index: visible_range.start + ix, @@ -216,7 +221,7 @@ where let drifting_y_offset = if last_item_is_drifting { let scroll_top = -scroll_offset.y; - let anchor_top = item_height * sticky_anchor.index; + let anchor_top = item_height * (sticky_anchor.index + 1); let sticky_area_height = item_height * items_count; (anchor_top - scroll_top - sticky_area_height).min(Pixels::ZERO) } else { From a9b82e1e5740f2ee3dfe9265ddb0d430f8c07e50 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Wed, 9 Jul 2025 11:04:13 -0400 Subject: [PATCH 109/239] Bump Zed to v0.196 (#34127) Release Notes: - N/A --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38bb7819caa125928f82dec5a57167c63d344813..5c22b90526d7d82f96bd4d1c627aad0803b4d7a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19972,7 +19972,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.195.0" +version = "0.196.0" dependencies = [ "activity_indicator", "agent", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 884443e770e9191a477c1ed449b0f985c37178cc..48591d65c12a83eeb844b8d73e8216458a53bede 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition.workspace = true name = "zed" -version = "0.195.0" +version = "0.196.0" publish.workspace = true license = "GPL-3.0-or-later" authors = ["Zed Team "] From b9b42bee990dfb94a9c29ab2b4bafae19444395a Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Wed, 9 Jul 2025 18:31:58 +0300 Subject: [PATCH 110/239] evals: Fix bug that prevented multiple turns from displaying (#34128) Release Notes: - N/A --- crates/eval/src/explorer.html | 204 +++++++++------------------------- 1 file changed, 54 insertions(+), 150 deletions(-) diff --git a/crates/eval/src/explorer.html b/crates/eval/src/explorer.html index fec459716393ef50ececca0dd456eec674f15edd..04c41090d37ef975ce1f4805cde3eaaf433d100a 100644 --- a/crates/eval/src/explorer.html +++ b/crates/eval/src/explorer.html @@ -324,20 +324,8 @@

    Thread Explorer

    - - + + @@ -368,8 +352,7 @@ ← Previous
    - Thread 1 of - 1: + Thread 1 of 1: Default Thread

    8I{!Ecl96o$q7vx5mNwag!>#Vf8Jt(KF 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 literal 0 HcmV?d00001 diff --git a/crates/zed/resources/windows/app-icon-nightly.ico b/crates/zed/resources/windows/app-icon-nightly.ico new file mode 100644 index 0000000000000000000000000000000000000000..7d5f53f2eec36306e1b423fa3cee0f0642501103 GIT binary patch literal 159619 zcmd3Ng;N~Q6Ym{zxZB}}I~)+)9TMDvJA~j6AVC6$ySuyl0SAFE?h;6Fhv1R~f;*32 zz4u4F-P)Sz+S%!vuHByQ&!-mv00K|}|2u#H2;e&&5TO6shr#}@%!&>KFyI3L($fD= zmIVObfsp_Z$p4iq@d1EOMo0ks*YpXBPHt_z$l{+#`)51=A1qx)s$_tVElTSIS}@UM=iU#GqeOvA+0QC6ilh_cT1uWuy6zC=Y z)R#O;^)An!mgdnIsTX2xQG&!rNzvaUyI4Qp`JW%uZ`jfY zSFtaR2U*Kr(y%E!CHE~70HF|i7!eqIJW%q{|1rWRS^p(jd846@J#(B6HDSpIT4FMi z<_Bb7bi1O-y)E*kJ-rnQ9Ibd9n||ppXm8zm77lDDd)aH-xSGyF!`m*Hr z-MDCb8RiK9Y+lrp{i2Zy#YTtUbzgIAeqB5{A3O+r_C&sg{AYF-b_&c$VG9xo(m@ek ze_`8^Qst5ljAXeAcnMe6MZYn&{~S=@*Q0R1Xz=I}CmeX-=K{LvgkCmudF*wbKfQnH z8E&fxtc&|4S)lQ9+-iRJcOK`LSm58+*-O5RD+O}eLPXf2vQGWu5C2ZzjA!rqiDN%X z*$%WnSdUF=Z+tSBxzkg|^<&khV zIx2fF#jB#BssnzPz!IOk{f`VNiUtYhMLOf`C0QPKivHz zn!^om;^mun5T;;r?~I&%LtD8(Dk3T2sa=-RMG3$w3&&IjXz0OTr)Z zd5HY)nhHtD=xG>*n^CaVPQOzAP@HRknj|GaN3GT4Vs8@};DlNV*^D8Fpxl{}P9~w0 zK$AwHK~`^rq9zFkc@o|8bsAN_L{#3h!N6?Ybcx~LnuP}4n!6}BtoQP9pP&D^zRVnf z&bqlD(-<5yr%0ir#J$m1o27fdmfp;sqW=6Kb14U)5%P{DCWCr2z6iD=Vb=pZFkJkB zmhN(7iWGoIMPN5*cp-9K#*oqWUk8CO_xNJ0;FMLH4IF`$bW(!$3W7hk0vh{GnMu+aR^UT`lYfcQ4zzPkS;or=+RdaDOH!SJdsi@d{Kd zMWGAw9MMtG4O)IzphhChb#;)?=MDsw`zsKKq zET>SW+j>PIjDRSA0v|i*4M%;2oh&m!nAf)&~k6R!i(h zvSBxr9vG4%-2(I;_T-$%N%oxXF&T&Gyf*^ykvO%+AL3mWVTIhd=x@2x*HqjPPGT(R z1IEf_%8`OvSEPZ`rk`+|Rw;A8+zeC3Q3i>;UF`=_iLGN(Fb0}*_qJoi8p}mv-F-re zq>7}{J{A%ajl#jV_zi}{>?T+ot1$+8D%pNgaLPobzH$F!X;mv>2OgB=AYH=Pa{|qMpZnwu%)Il(y5)am>n)CwQJ%etE zbLh1+sr~Qq;q7DUQsLR}mgs>v8lGG!WA*|+UZ}G`kP=&|y42`%H!leNGI6B7@5J*j zQ-%51D$8mN+0IRKv}7&2qk?tfNrGW@{Ek&9Iawq>3}{>+bJ=N8KEs~KTZUv2zjsd| z#1_Lkv|Pv~+Q-kDL$B_HMBc9a6g5V$JG`~jzr?F;K+n{`!m{3kk$UnLgjkWjjDRA5 zs>F1W+TURQA7AZhuEEGAmn3`7TL)3haXy)?a;H{Q59>jf_xLxdjaP@Zx;t9S#;BerLA}Iw7T3xueai9Ynv{M>ff$&PSz;jn z^KZGS7v7AS@`+)T1K4Z|xqkPCiFM=jr$7|o@~9xZsSH3L1I|!h@8+CjOiQUkBt1$B zPua>fT9=GQ{ z7`-qPWN^$9b%<)%5{-S6CI)Pd`gofGyqbz5>-)8<#~s7^r0+r1@;>947Sjj_vJ+N;$X@chMK`)=t1BGk;6#!)t)f-H4ijXC$6h!bXgef; zoqXGBxb@^?sjylXX3vq^n34=sKET*TxQyMKN_9nF2g2B#@o74aS&SzWHcsDKWRT4; z)H~SMS4QI>Xl#0X(MV{XURtn;_`^*0;YXL>x{^}JO~Jo8k!P;K0{N(FWRAB)tV z3+{hY6N4q2FftuH%wD#nl4flJir?%#?^9NEIrX2r;qrwN?@MEz18~R=3pb~C5Qhn6 z?^~?dmXj>1qd4Jx)3!rozP$;j_Wwm;TX3d7C}Aps6pnzU=+sCk%RC% zO5g_k!kK z653rLR9iUxJ1l$_fr7+vhs=q$!KNy{(7=RWEHzrA2{4RTwglm0{p~N>5*n_<+vxPa zj^!VnqfZ~hsB6)`f4)L|9vNAg*)yh}j?$&jssNW#t^kEmp|?_W@X4mHR;}8m>kgSj zW)Epne$l|S&;MR_Kb(&B9g6+Cd;w&=xEa(3+hiOpjmbGUGD=0%EhKHLx)2{cuU%ou zYiPySO-%xg)$lt@@*j&Sfy)$Z-cShY6$*xs3ujvB)OOZRA*!&uPXr2N)|C&5ZLV=% zRMxLkstT>Q(B0bkT|ad%P15D5yrPU^MUEcmemGi8*t8bgx*TFnFc1`=E;6Qbtgeck zeEFl}u$21aWzXz5_+rE4>n}iJT^e4W5fclHs@55Q_P?HyP%$Ge^*Vi9Ovw(mWU16? zTct9iWHX7;r5SZwhS4^HuI`}Lw>I%%x$~}%+ILshHawI^<6cbQPTc+a0$sq12J9uI zxTu$1GqYiwX!DLoLkK1|@jOlPb8IZn6Z&B5xQM?)P@6``8+Mo1yaMN$+xoG7r9;`q z-F!%VwOe~-Nw?&HoSP2c0&z;iahxR5aOgGAiBUYSn6-tuSpzjtjD|QvcV~lQgPIsb z=D~#yCim7biyY_IO@j;qu(qt>8gyw;g@m(gth=&YN!$0aw~Oay8QAww7Iawlp2gv9>B1g77v3CS_`M-HF212GoJg9abGW#U` zWE16EJid;-xfdV(WW1P=cyn6o@;ERIo24cH+D99OvlUiwADhe({YM5*hP9?L!g*SHW!f$iO2E#dFFtK;v^1m?TFid=%#O^cU zOGz5%n!K-)zw=0ElQpP?N=!^{s+Ve@A^*IY$4J&!61{7*-V>)nVv}jlJJ}*moY59i zHij+CvuOs`d6#<7qrr*pB=-Wwi2JhtRaoUd(QJlUt9u~*dW~SCbg*BI@u-{Bz3;vd zwek{^d#l#ywwP4-mD{&SgU*^O=*L?X15&MIK0aP%u@ea4pdn1+coiwFGT0_UJwV?e zv^=}FJqZ>t47nE(RtVj5UZbO42+P=};^n%}adG6bB`QCd!XSGYq;GN&m0tD}EU+Ls zBg?&>50{;bQ^YPTTGoJ?#0Wuo)GAm+t9=;@MyQuLsrW9k zKwwq~CqyhQ@IzibD$Am;tzCS-k3}ASZC+O?m#m#rIgxDbpi;9Q4;1qT#D{M7t{9Th0A_-UZ0(E?xlJTlia zIUVIYDxFsZ)Vq$8Ps9tBH$ux@=MY_YUE)!Vu?fK@jnkUyBy^5dz-~T2MaSMzG$;!$ z7T;Ad4i#6EPELuc9KPP8cH1T@NH4RYDg0}Z9%ugrl;r^Zh>1*ynL*UiAXt3)SjDkR z3~UoGKR{TXC~in7l@j;57YTzvHXKM5uYt*r)42Zb6y$cf-`+{ZfI9SdOsrZ`sJ~i} zTE_SLA_Ib2v2egL8mI8d@rPhOmZ<7Je*BDocgH;;dEy7W42}N|f>vshH$2aLei~Z? znB7KctOvcpi(`(hdLfX0IePn2yVIVU^;y2L{wGh|+dIZg2Wt65oj7(=fR^hI6Lk#S z#;f?L|HwEPgn=M3qn`p>jq;e)WDqoCSLESidRlJPyd!=b=1h7;EBVaWA3C4384NZ) zf5RQ`K~o|pJI<`bPxthI)=Fn~UGE%)@+xcOddwRw*J>Yctz}1tvWu(>!y^5D5j4g3 zq({#jpgpx8puu|Cu6MM>w0-Lr5q7<$Zq}%L-!ap(+EY^2WA!nVc5RSk)YIt;sCIEG z`tt%@0c)h`g}O*~S1U4Xo4LF9{ryWgU8+y=3hWio_FXK4He5#`G>>ES9l=XqMufZ1b|#&zeh%5}zB%L_m3Wmc@Lz|KwbvbBiv z4}|k`jGvm!-z?vLG|Hu6q$*OoRs620PonX~iLN3vVqy`HA1s0OIZ0?>^3a**1RVDT%&P|jJ zbqBF2I`bqW`DVd$6Vp88aWajU?y+V4n(cW#m@I1I`lmDE-dK+Mw|p10>I_7KjKPmB z9V!<4$4eQw(RAtA>*DRX`teErt`_-EPh3sk$f;<%=#F_{^r7gIK` z-Uafp1BSmUPP6$-5sZ&%l)bMA_;)gC9FT*b-U*$TUllS|w={Oe?q;U<&f^XsnIV~i$8-B$J{9j3#E;XBcLE}`3Hiap?~NqX!3ry< zUuEOO&k*9ntOg@(Wjo(x$yXn+0MeY?t_0T(V=t%5&Ji`)e#}cNMTR*R|FNYJeyqyr zCzQ7y#JA;9#bD!-9;WAX9r&T#r1|GYN7~-?B-T`r-`KU`4Bx7pyCK#Sg_ZvbB{XR! ziJZz#yOqSQyyXoWHyZm0jf&ExQQFZB0&}ftw1yu?JX}uU==*WWG`05JKkMI#kAzeS zesPA`$W+9m0!A3}K)3Ie1*wf8b%MJ_jDbiN=|4YWN4)SdJeB$RRsJi8#e8+zx4Y|s zEvf(-hT_FIYL5BY0+=ScTsCJj&)$bqv_*3Z9gQSRcjjrAG@|@fq~iQt>)ggoVWe^- z@)+Xu*C;oVaX7xTG@<)ugrfql;WG2Ci(++D+5If(!X8H+qV9G@D%-}>Bg6nu(XgiG z|JYD))R1*1S1v2S3wEvB+4{5kHQ}R!_~jZ2xGlcq0e3ST!4rys4B{?I5sop{l{O}TZj@5HN@)`NduMsJq4`b%ln<#d((|hW|TW3)o#=GBuyAFfbjj``z z9TSTeJ+xYG0TX|5k+j%}H{if_0gPWFUQCD}%^7szPYp0t&b^h?Ow` ztJP|>3|=*iq&*BvrH|DiHIwA}B3wch8`BG*(tZmK*NPiC+`oYL)S52INaR`gxW(RW z6pb4xaE<1UMAF#QrPpD{6HJPdL-4GrESz38extJ?pu?9oOxDo#^$|!8;lKj0Cxw zIi$A}9Yg!2ofml{XFMnqg>3SPl{K~ZFn#B^+4GHuv9P74~k8Z+t!NM9aHYtcuUN`ZB za>LZ8FK2&hrc&Skoc4b^BICinT1Y_=igTrw#B5sV>EXJ^1<{4Kf`-SLWxP>> z`~Jn5t_2)1wM0Bfn*?je%>60JQaw9k$shQJCxN@OAmgGV^@*6m#lBDZ{Il-B9OX(E zFSqzjOXK-k%ZL`ZFGJLzq%zsJqWRV&+2|OocVX$zDoWhd_irr~=9%#(NGF=s)p5(b@egh!?(U)d zxXgwXl1U{~Un7c~go{gy4sQzzU0>W3Y3Pu^hn7}!(f4?b^AxN;_)5!Ev0-#*Vk>M|ms@#` z?bmyo!*v;EWD5CGfDzmjeq-U6$&|{9552r}ch-jT>vWNE>L%E|!PznCvNeBvO*M9m z4iOxjJB+r175TkH2QwQuW2X2xvdds$dPL{AA2=j7?9|&5v%+!%ITbcun0Jeg?IK>G zUK8~#acEncb!-^NeoCuXnp)w2*J-;y{IS8;Yn~nRY@EmcZ-P6j+l;N~-3Z41sSU{z zb+aB<1OLwf-4t!@lqTs|SRK~wY)yZ4{~HEtnQ&-N>)U3$KQbC_V>`>Wv~2>Taq?#= zHggL1lIJ{PE1+?;(-9M@eTxQS^C>FQWVAs8s-Dlt;heQum3Ahzpc<`+D=e}0e^-lJ zWX*hGis#UUei05^5#^R=#Q0+-P3E+oYBSDzR|j%2t@(mSe-E5jdn?n?BigWbpomV$ z$vq;JQ+tX9g4M3L!obM5&Nv1J%U223#vWTxjmGo#e2sk)YOs757j$Z7W{|dV;|&(E z^mcsD&V4dhsi2hlY@jR?@Dkb-<#au)CXG-;SK?#$Br5&Ur<+qPKR3V-_A3L3f{JQ9 zWMJbzscP_-wlZGIx`Akq0Nb$4+txF;0>==|%4z+9O+SZSL4Sfz>}P`p;9^s7DVg0In9uJ;%I}I*}yv}DQ4lYisrQODS5_E^O~(=@)b(-Vq2d- zW-C)4@#u<*tP#Y2{DrZdyC%;6=3wg*&0JaNSjqnLV_AOJaB!EBRq&_SJQmuKzb8Tf zm$aF+HYvsTUV=XgV-1Nbiu-OBq;Y7{Q_Ho>3Jmk~DQXsV8Z5QY6VRr_)c;|8Ogj%u zp$_u0S)lZ^o5eNrakUxI{yK!KTZjBn%jv(`PfP9o1f30CWj79JR9>ETlgF&rr3Vn4 z!vRrlwIUJ;ITLKBo)-DGxMSv)w9#ozcdhYuHB>%^(l>cO{s6znS@G=n8#ndjXx-WiMA9 z(MwXl7DxGfTqLT~x)bh8g5s$J%#ggGQam|P4xqJRIlp;B2>QURn?`8p(3ckj%#~=wdFCMk{l?EBbey_wOKrKK7*GVDF2h;FjY>Flwr;y?Gql` zyc`{=#r;76k@WRT%W=!#>50Kv(0jn< zNwups_DCj$uAT>AjaS49dp`P`_T-kMMgpP>4Wju-Vz{2PXQpQh9}uno{RFCtwTz>t zyC0zW&cmQUJj}fOuTUV8CBMjtulEU@fvxS(=M>4Bmm3pQGBUe)u^K22h0!mwrkUIy zAhn<{0(dzYS?RyDBBgA6kDJE2jX()iU-sa9YCzIg)Ij{#JU*E>(Vxl=vD!(7X^~fA zV{3niJCvmVMZcO6BT}-0{jCOk(+M@$AF&KCr-{ z?{GL+rY|2!=1$0B922U_Sz=YgD;jRGGku#iLt<(gb1t6*e6hUUw0F`rvg3ZY|8O)6 z6LTb+&WNB!F}Yj&Y;iH8kXV!SsI8)@w#oRxE-&Y@pN)`(Rc7x52&V}m(TT5||BkPb zxG9j+ALT~cg;*zjRDo~ZYbC!?ha~_?GsQR5ERwgeNFx6v!f%8ps=Gm z=ifbE)TfUdF%ZY-HJ_bdTzPK+x@vvHup)iCd0ko)(n4nd4c|`*`qt745_e6of3Ci% zQ!(P*EWA*O5`*f->&<*RT6TC2D*A9;U;Grd=T)nV@ChF?&IQSC4kXoVYDaU3x0ilq z9C6&=*3cO6&&o9`pMK*u&0IbjV?GkZa2`rBs{HQxHxER*rXBA}k<%_rK28pFr z!|zBdt(5XNnxXxLvqdTO9F>0~-f~G1_o9$RG*Vdc&G4H+lLTixTw!~hHPwK8mgk*g zl57opgDO1QT&)c?<~?FaRD%iOeUU`Q0KP6lcb$XG3?Glon>x;JPSuMHnjXN4zfi(r|AcUj@d z&r5_E;fU;AQq$dqad#{7!irMd(>Y?Q>^88qg-NI7%Z_EKzt|uoa-%f+e65DL=9&x? zUFsWVw3DytIAz|jkAACLuyhLBIcfVzGo5A&?kiHj#9(7Km21IVIX`o!5uX`Cs5YRb zWOok}9qWvb)|gE|8)ImMGgZM8? zSoSvoz=o%pn@4wh;5`Nm#`W(Z=6G5V`No_Y=DCt$(w2KJB2st*S zZc#I?&Hc|!eV+l#g_YjS5cH%cgFwlgt%$*jit%8#TeSj%ZSqZfaIdOCryI@t?j4`6 zlCJ7iq;_%rFeckC-Kc%VO|Qe$|2F zohxo-V6A=mr4Z+4p5ZvE@<zY+3dNo6_L@XyjGTjlur-htoYd7qw5e@aISeEhE{||@nS8_gG%M7Q4vOw z1GZgBN6@z|BKgL7gGkelY_>uq9myWI8Hf8=|6Nh`#Js|VVUz{P*7iuni}YVel*;;) z+u2DHtAxfM)S6Bj*pk}i530}$2lMq`I;ltfOPpxfg;S#81EKs|KT*-GVi4J(G&t5ZMp~b6JK`c}6bF z2cnVoBv612FTVc$`=e=gc_}h&rOY4`WDgwe&;IW(^J2uHpy;kXuoBbY)AS@>4X3}* z27&BEuVVk)Nwi(g3|Y1>&e$~Q+^FMo^0cXov8#J)bo__C(5iBFi4FNnv|t2(NF0zU zZpPE=*I`VPU1Hn_*v*eKKbkH0nC**CV|I87vdXzlDZY!-FRR2HbUKG0E6cuJBcA$? zS3gd0$GZ;iOurGW=?wz-gB1+Wy zvsAN>-G44P8aEX9gSMt@={N3o8Fl|-IXO@-ElZb}fsXuF`m(R-y)B$xyo$x5M?RWnxC5TD>KoliJe=>< zntMC6HWPHw&J^*^q3XYFIkc!v_G01^RT*Ps2g)vAEo=+8P~ea2_dyQHwUqaY3@bje5QSM}%C1Q(~z^xYV7Ftx%$a zR5)z$+?YgcY5R#KER-3iKNhO0`X{9EK}xGQ+^+)*f7J#Gv+JuxiYGK3at`_{3Qp~G zZ5Frwna1U#LLc`0vVGBW)mMR}H1R^o{iow}!gNO-T8zazq)F5NgQo8l%!YR@KvH$= z3v^#eX<58#fWX#ns@vV{Gy_u_Q3`T$#>+vstAOqpBfWxrkEWi9j!`-mZ2;kc8jJhh6+#4EjXbtW7+jj5#be87Z)y;Bc%}7St(W+z>|aSol_6Qr=42sCR%Z9m>Y| zZ~M3J`a)s#7Ksnuj?*N`#_KCF^f3&^T{5BG4D_TUMo^NFWW&BP;=T8gZdME;&Ltc! z`2CrokFs#5qH*r@r{Ci4P4Z(BjYAbIY_bXF8T)(OY3?b*&%Hd4^}|NUJ0o`l$_Y@V z=G-t|hKwod6ZD%*ZDMyr@MjU*IlV}CZU%jiNOz3Jj2AS|674c-@=MZ2$)x+eT=cJfV|aQC z!i?jOG;@G*gAv&bT83)J@Ar#i2AUe2)eBq0V#5AH7Y$5D;nu8avX}1$moU&Xv!d;E zNhvsTzVjG?9f5x>BINO?`?S2N78=}3sI{o7uUe}X-OsLFrf;eiF@LnlQgaEJ%Sc^u zZ0%ZR*AdS2F^!hE9{;wlF-d?&#!=%(W5>JvEi#@;OrExK@ag#;AL{B6>g;X9K2RGg zaET`=&zP!h;3G+Nm6Hu{#jJKuR5Mgl8yax0@;pR{$1l`XWyr{4^2;-QBp0F{EOp#; z#f~KOUnTi3Tm)FYNY}V8<>4SRU!H#8KLUo{;a;pp^#7TTP2H8z?Gre7h-2Ufc?Bzs z2M{wX;UZQzy9s?(`b4zzs@%vjhfs!eDYK`z4H9Uq$85Xs`L+(Ajt|_Md%ol$e^=k-@N!e=zEO$CWIW^-a5? zmZ;flgi=D830tbM=sw(1G#xJF}Pm{?+LHo71et)4W+n(&AUn2r^dd^6(3{BfSx zQ`lnrm$%JMRG~|9=wBSUu)~j-EQmW+;x?}xrcNClr>jHOM)Or9`f%jj=R-v9QI`j4 zRR6SneK9Nv(}JT+LMp_>Bn&Rh(Jyd-zmg;I)$eSLP_I%+!^t})121CY;^_3Q*Ah^J z)t^0P4$;A*Q41j^&#h$K-47#^W5N5Zm9v+^d4$v4<&I1a)r0UWzOA)&*E{oP<4J)Z z*-O{izUE~~?tBr?gR03)XS-yQ5qEWz$};vfRe5yxq->)Nj~BLY5XW5^kE+%9V!%l- z8!8YQ2c(lJ9|J{ULU}}qp_0S+?xc>8JNGzc?Pwey9X<+pqjJ5yT+M(%prhGmSIq&hz3 zLrg{m2jI)bkQRJp`-9C#{%`%`@iVlQn6Gc-^W#?TICbrv6w7rWPsVV{cQrOT#}Qmww?4PAe&{20BD)lYOyFMCX0cnDC=*pLDnB`+yiL1XFfG_C6`ra zp(tlMQGsmU6M2C-(aYdBEk>HbcctUjBy{hkqMYvvlf+_gPIQ*+aGFdD^S)pC878XO zm+L?YS9ZcyhQ<_UobrR3VCGZ!#^0EK@@U4fG{zMkPYhU7e?>rq=Nw65DT{Kb>00uB zL|TVZ3N*H+{`m9D2|B{^=0x`5Dh9PUq1xJVZU;1MSem*;mNm!FYcD|_gk6-wlaw5f zuOr*s4J-~lM3$vQX;z`plL&Jj9B}gye=S!@C2sAtKcT`uK+jXM?^4GC*MHPZXzM!y zzv@n`nS(n;**U8TZX;&nl{mW;DGz|j=Uoz_AXR3}?^N2UiaVioe>ybTOUP=W5hbum?vf^e_dP!BFtfLZ)vT#roSOJDKiMJP};Uhc!tc{wP<1uEW9j~q#7O3S1EHsJ#C+X%g{6ana z79vAD2^P!mwO5>oY zCT|y_S^k{L;W?}Yt58SI1)jeaPyKcxV;)qO_usf`^VX4^db37&So2bU*4A132B4M1 zCAD8v4&ambU#^iZ@cd>KVM&woY7rT-@k9PT+|okBw^|Ps>5OXqWDMojV8$loAI{NH zMT~%5F<+$aQN3vb(Gk^+0p3`S2ME+3GgSS97_U9(GDQ(h0wb=8%z&@x@XGT4G;^1e zA;}Vye`teBG^4)9V`jzxJ3Cwcc_gpR>nsEo#1b@t}Av4n)zT?|3gp7YknPkT*J_+o#W)X zHFH5LBj7-sKo@7mpo(aNZmj69w6ZI_+Gk%}kVg{PIKeZU#k%F^uqg9~X^a290o@{& zS^<9G6sW`vI8GXPe4XMYh0L;5w~(gV0d*E?CShh4^Sl-OIMq>+P4z+$xW@bUj(;lH3i92xFUl;$Bhxj;?2Fb2r?p@APfiNfi^qS_yr4-Jxk6Z3K>b$tEWt*Y^? z!g>LIuYtcKD*|Mdp{o~(*}aiQgjba-KWFM^LdJIF@CuRK@X~EYpQO3=%mRMKw9XXM zZrYF<8e(IVlWxa@O)y22P#6sfMM1GL$NM2!A|cfnOFSr0J`^iikJ2*UKK&9tE?9Tl zpNSmssngck`i%uK2Si#`T>1XY0nYU){IBNGpkmh_wLa8R2Q5@9gWk9{K>2op#xMkd z#xAIRuI(Q)>Jl#_z@fpb}AYHU{ zw0c&f@rrm30mdXc4`&>gM0L|!O)P2DnG%P&ff9=~@wH(&xg-SoV+WD2?*U;1b{_iY zlySfa+@_q9r-dU~`-vUvShgKF*iJh0s5~`QSMR80G*W>|1o+%*rAo1H5q~y;N^^I~ z;=VKVO1Q|7BxB4UVh3-&XDuSay7i^5TBW6$Q`}kJt!?~Hs%=#JpGJJ0Q3QE=66bEe z9GwdD-yvDhf*Moo;AJl99mSm)Z>z$hd&S2?v_hoY;f#FqyRXUw3kFlNm@0+yIzU9l zMY<>TSAY|j)(X=%7M$mebT*hl(9QKb1fi#|z2Q)`7=)p#I5)f1V?bg`{2#jaW;<%B zzAaLk#}LU!^{UOh9XP(t=v<*g>=N}2yG#Q*ypGWgrK>Y;vPXb?$^MH9akv?6f*OaZ zk+<60pN`#-x^`PzrJPi&({H{02Pi|8bm%uf&CA9$*&EymL)rT(=0@Cz`t%%?m*W-< z@)icq6O#$k&Ij`6##JRlNVfI5m{XlR`Z<*EC1dBS`rV~K`WFOuU8k*>qZVbfXMXMY zeJFwG5|O!sy|xEYBaJT$UdyS;_@7mocz^luF}d$Q4bqOc;v+AY>hCcOURNr&ig2M_ z)!&0W1aVCr#It#Fau20IpyqE@YTcQ2ja8A+S2_MScwY;lu)Z4i@78b5rg*$n!dO4} zQj4lgYYZ7!S60weE+h{d8VY4Hq(pPaV?9IsfoMC&-Mh@KYlt{S@x`UDI~@QUMp=5D zIA3l#JAYd~v@4qVs-dcY7bo$q0%?jDtw57( z?qJa{v{|IwMiIRASPK*PzbaW_VTB9&RX-SP88dXPL?WCQ zN?EWc(x~}w5M65vmrfU!NC|bYRQ%@bcw{SLMDU&HnIQO=cnf5->RcHD!@7M)HO=Jd+J*&W}7fJ>6YSkR1MSrz!>io z62M-T(NHn+jRCXX8}hdA3cHPRUB3_J=2rl>F-sMsz!}o82V$Os>q}{i+Dv8%oVV*g z@b6owpKKp7smVy(idEq&CBb zW=8VRD~i#DV{t0K#dQ>|m!n7FJCWp5i`-0O2qICUS-MPHgDG|N7ALS5_!ueK>N zf|w5S@AcoDQ1%OGDppo0gzm42jtbC=yr)a`;)Azb?HBUdGRf$LoD^L8@Qq)mfO+`@ zOg`CJXm1n$%h0dVXq@8XxaW^WbH#C>nY5gw);t_wQ!HeWLSLJQdytlJ5`Et$KF(iqg6$!C(4jnR!sor3BYSjLV zt4zVh#Dr(dZXfLKD2g*`dtoB5FUwAV$R_FJP3w)uQb+FHE;^vm%JJj|l;m!4-R~~R z94RR#e?pf){3LrhTmPBYU(=qtAQLpzzLufW>Is3iiLF|h-E#i zdveS-cUg87<*}@rLTFC&M7R()5kx$I^je(coEvzpGUw`a|Jm)s z&wMEewnd`*wDhrEkQ@tAf;VnFK~v66z-JDvRiBpaOQdWXc_cc|Pd@fFNrdPa2&0oY z8U8uDV-Mumy0VdA!264JYm^*?=;O5iL7qvg zdzg^2L51tN`u#O(hx}J_C1>uR}&@br%ZPN zTHs9iR{CdOcnzL{$=Y0@b{R{G!ujhDG^}Wek|k7PMP3g>z1`x*^I6*kOK7oHSD%RA ztq`R<(q`ho(E2fawA*}BkWuf~4DO+V9xK=bym|nl0YZ6P{m;5t6q$Sqi8@FtDL);` z>}%IxLBB@pGB(4iOb9OV$0G4nZS7t|!00&$TT_U>_2SNkxPrmLFMb3WTl4wj;Iu4BZOb5q}q z*7F0tF6`4YiT;OITCGTcQcPcG{`a5E`?qm$>6*6nsoteVPNhrb^``3Eb_=xOxW=1( zoJM^!K8as&X`i7C*DuoATB}JP-=-yM>BHwC=)J@VC{ulf04IK#%Lw!$Qc54%rk86q zC{{-Bj%+}HS?@88s`s3q94`GPRzYYi%s({EkwG-k)|I=1{@DuL7XCZJ;x`H-N+g=g z+Ue_Od_Z(1?XMjxYK1pRv1)8cLItyoMa?A&0qH;3q&ZXUSR8BpLfjOAV-E%9=!|Xe zY|T4r64v}bn}3+7*8k)M{7?Jvo~9%BR>DwOF}dgr zVlhw1CV~I5g^_rzJ-m@W6!?SXds{aHQK@u*=Cn@>39w0&2iNZKfzRGy#~kdf=hn7P z6?)0O!oyn%^)Ws_Ph);z^$E^wGwr#JGSa;gG`~-e&{dzFRLFQ(zOl$<6mDPE-3W6L z7%C<&sNuJgP~5-#gWe=He`LR!Rylri**x^0DJxn(aV)=vjtax>C)~oSw!}yHnzg+m zE&^hEmgh$YQ))aL!jEvkFwX{I`NLDC!}32-1-Om z!I?_)=ON_vW@zLb{dj@361h|TQR4@MfgzwX=YRJVbjgxud1N;wUW3;FRnk;r?D%8X z%!wOVX1_E=j@!}Qh?YiMW1tr;+UbAcsYdO-IEy`Z;nz3bVT910udo`SYami?O&_ss z3X0p+Vo!BhYwQO@+8>`Zq=Lt$1mj(4x3}uP`dExXIPr9?CY3F5-#b^|GB*?!1|Xvh zR_4@fNfwFy5b0o*j-xyZ2IvP1w2(Aib*L&n_PXR4)OE(AF@eX;^y!&gq}x zK6#nd%eW9N__h+fkAX6g25END>iFJl8Nt#vb1Y+ZnyLj!M)gps=xfX#WbSPYum(%_ znhP(KRsI#RcZQ}1h~A5+HkVgz85Svixh2B;wIB1o%$&dW`97JNg|+L{)Mk8^u$S(d z3;KbaKVylPo`d~A8OPinjdE<3&2&duSftBb_T)TKU1Hb)cYF`alDW-gGgO#ach=c0 zmoBU{gtR}7Y(w$)hW@O2QKh2P`wK15o(F&D(y(g3g?xOn9D1)nj;y_GIj$HJt;%Zu z5&LngmyeUdZ1ybOqxNAJO2`lDQPxydIvJAGuH|&}_I7#fPf0`GXl}155f#0!W%!Ci z?Qn6xkOrTp(u{A~flXDDU3bHAb-B;Y{tC%}uusvnIdbvL`QBrmbqwwlW5h!bqp2x`v#4NP7M7}ja& zmJ0BcMJWfN~BPEeExb^b8IJ$9wi_cwA&UmI@E>_U&yg6~!!iz9N!KJ}PGveuQ zU5yha1-%ojmd%;uA?8eLc5eIg%e_Xh-Phq$G-h}T>RLhi2hhfFkD=TKyF|&-YP*z2vqItUFOBopgmIGr>8a`WSVP76iHuWfuqk8bK`tdU9*D#Bpzo67`6%ZEtSnMBa z%ZT#5^UniPxj(L&7Cni8%@!$0M7b1_hl(pv$q+1-;4BB2QLjdl@lFpUN_A)N0f2k} zDu8Tsk)2bj38I%iIr#;u=~;Nc04abawoNT!Yz08khewPoJz|M#O0dQtrKC3z#m1UU zrE+pvG@(7#Sa5^(XaOQ%v>~1W#`H8T37eDd@>S@A!_XxXh9BL`fNHKv7;O8<{m>n*E`YG6Iii3CF!{Ivz zI)$udL-6{3O>Pk!=DTye`CBgo+zNI9$P^(g6^C6i-%^Je;P>hGP z;ieo+X$Z@sC0r%0L%B^%sw6Y;gOLpZ)~O9vHni+#&4npPBrL)tL>5;|%vkWVQc?lpb=zAMMG9_j?{t-!B$fQzYrE-X^~GDw`5X~N{#`Qv$A?HP*S?HE~rtBc_7}5XAs(3AXeugGD7IlIP=&YDTC3<{hX|$*_h(+-D@Bs)XfgMRsgZ9gWwm4QBDxCkgA1B z3?UhD5a(l4i73gbIR{5ZyVylc0SQ8@z4p~N(auy-s{m9rn&8Rr`WPPi_=jQYDMB~v zCkvKfEGgeS!i~3daU`U)fH9)u_shdR4M#U1(o@h?Be}{}CJ{Q-gNd)e9ffPIODIg0bAhfqs_L~Y{L9mJru{(mbHH>uy9Bq#e zasKHmuueD{!5^ZkxAZxU@XOo9;u{KB7FDFF5#wA9R|N+#^}0lkX{5c39+zG zdPiH*b!Ci#A0z?jeVqarvgL+yT=h3ggY3P$o95D%<5F|udBBs_o>{r%96WqHC_*3x zVe`^?y!GYR#2O6QZ_OccK*$V4%9-~aCZXeO8_ zAAtxdc?qj9QBDRjFx5z=0x80$IXD43k6pxMYl0gu+(d|TNkL+WKOjSq{DjbXy!uOD zFU(1!j6w0W3xk+5LRjpT2Qyb0{bAp;8Vide1(B?3uu7R2K#fOZ+n;2^x{(aJqYhVp z{p*6a5Xs5zb*8Oa{p_?15UzaNCvp9oFJXCbtWc3iq7%=wRfwyBGE91}HQ+OgYp9Y+ z06|*Lgd*bw0j61(QDsbp$W+9VDW^&G%PbaJxn3_*;jJ&fi1qVZIKH_+^an_~F@|a5qyjB)>qx(!ARO~_e`2R36Y02!NWLp6dE`@Z(n&BJC9!~wrQCacl9^EjGf0G z#%SXVm<>jo8;IS3G7nR+A zgAAiyA1Uh-0DE*tx3K^AyI^i%>opiRmX-mmG-crhh^)DnYTIgE?ZVTEB}PT3VcoP( za>ig|(t|BCFdwun*aK<-`t(``gF{EidUYOvAzJcFf6e74+iIe=+Z{N@L1uk^22B|o ziTWzD>Gbh0MifI64#tINA3+LXz@2BfX1y3cl;%@`!CcmHrl#iq&!fb_f+;7hCJ#%% zr=HeoCXs?zcb)&x1#pVkd+R1{y>zuq8=RYhnephiK8jnfzJvL#Lv5qfIhJHD&B?=B z8JVWkf>Hv47fyZlVC)u!Zy`aXRmoW7n6Pzu2X3TTh*EV$(yD#D9nzk~E79iB95kDO z3|zHV9(Gr6ly+JdHf@T%|;FrlP$+$ zC-_83uj+u*33d}Yl^g3upSph2q+v#2FPcipBPkcg)*CYRBr7dYZd2Ze*MnsO4n~0~ z`jr+z!a2q|izglJNVyyRVt}M3<-9}3RXVCuho0n@u+2Kc;uyAe;NbgGmruM8_B!&j zAo12Hb7O|Tk<7v zOkIQh5!hBh6#HkNT5w8u?U%k<`gFu0ZJbt954|P>g8|flt-#8#G>u4Z$yiWWA}SYO zaEFK#X0o8#rO;o8AQSq!Ts7?Ha7U|fgTIdt>uLa_!^E^Jr%^n*vs*s=H+7=!4Sy_O?;^{FH~D^MwO3hW5c zABm$6`b|Bh1ybBq!j~<%jS}G7>LH?{B_fq74^@$h9i^}m@{*qqG9xunY2ij^)E3T` z5R}sPC~qT%nZ()=KhPH70c`*$-Y`*#sV=rSDxxv25m%pX0XN!2H{XS|4UDT1+PR)! zr0a3fU)a1!FvHIdaPH}c5q-kJTQ}rJPqvWedr}&Q0Ann!{oWg@1?X9J9KD!@6fHsn zsu~1UtDT%s$O1?T{B%8b!Hl>(90*V}nJ3Hv>%hE+E1!J?*M9$Py!MO#3f)}6XbbE- zwuxJ>9^5?vR{zY^wal1P{<;dO^Dv}>G`e%|&3j<+tjLoB+u)tAyn)!o73Z*FS~>>Y zC^AK)*SG&w1@``~x4chq(ms3Ef3q`BT*mEJuj#*cxcZxKV*TP}Y(LuIt>6B7afhnL zmsB*iE~jL*u2_U@FiAJMXiV9tWF1D0G#`uN6s#t*Ja9hJ>_!rQ#ON1@ZNSZ!-&9~U ziJpNhqCbWm?cky39>(==yaYe@BB>5SOfzmO-`mvUE<}T1RaG(~H)d*m9~Md%*5}UF zTarGUNoSPf&Ei0j`GML1PCUc50;i=q{%}dP6;+$WauEPO-_z40DavgI%0eo*2$zyu zMHQT|edP>Z`S}+$pRq!k?<)IDdp=jgFIy?gIVky~x>H~8Dv|Hb<1Si`d z#h}@4ZEu5Kq{ANF;$4h4pTo1?`w!qo-;K>ncfUqY{3-m2d0qLoyZ`yQ5B2{Y+M9Ue z*Z)1Vhu?r3ofjotCJ|*pSzaW)?@?cdM?Xk5%Kd*3%>ea_D*9P3Xj(1{9umVst;j6G z;uhZi@*W=ljt}AWUwuRA-&8Ergn_0l7b!TapdwXMAbHhJ*#1BqgqDVOPwC^erUt7V z`>td14+03tyz-0vQZ?bVE@A_is`oxA7XLbQ0;d+S+e3TsDxUnVe}WHx z-*3W=z8j~n{|P5ka}G~^;Y%2AeOxHU=-H)3MM;?&j=sOnCMznVE(yH@(P>ZOH|q4Z zRL%hR&!Ps18e7SDm4R!&`U}D+iAu_H zYF_o#_<(Kzy%~k{Qc27(QxwvSa^ZWikc{>+L(i$JP6iB-Nj+{Xv{>W7iQ(rr6$5L5&PTPs>&p(G>+`w}`_zE^J zf8Xh9{x^6%{`vnAVSZh67*jvC9EgAzu_pi-@nlSCsJ-W2ACRX`rtahABEV5N8S;#@ zI^%Z(*?EFrGOTS>r;`*ATuqW59@v}$Qg^7LC0+raktCHlg}GYZCEb%$&w-(%1`n91 zAGk4~&Y-R2+Y^j{d@PJIQ;j52J6|B3+}UajB7@K^B)eV>G9|S&B6OlV51mL&tMU2@ zLM;?mUzm9*G9LN#6Nn)dkHP+bG_hvzfbqZtOw1aF!O#MTHRBzP8t)`OP)#Sh#dSRS zg@1~A`pD@D{x@wOHG47V5RhF>YZmiXlzDv8u+)4G$zfo6ZwCb^qw?`t$w6-9aQ@kc zG@-}nWGWQLX;U>?Erh~g%toVG&m!TcJ$->=2RlBag=p$015A?Kq*aKiHb9(BSNg2L z_hmx=K6C(Jkes~TLLE?r2f&3hq=`6rJeU}G)YKza`^PovDyrrhu^6m3G$%qfkbYMm8=OJv0vP9H9@A z1|Fn2=z|{vo_Ix-;HDrzT;ApLu9wF?CwuPfup@s)V0O6tnWymfmtWB}DDzrq1Qn-< z7&~DAsx!^;4Rwmqc;}v{)D+34m`xg_R;jiegzeXd-5KAEb!rkMPhZ z{_A^w{D7}ODG)-tT#Yxss|%I3N7?eL$du*YrNPfIIrKuAhzwRLau|$z<@2Ptgso>7Z~j^A zJpQ-tdCh`9fC*c$m4jajZI?6%R2Gf8|K=cB4l>%zmhPlB>D{qB3jln3fV$a2-~}iJ z`1Tm{z0BOxBxj;hof26j1c@Nus4+c1#jRIvf>Ht_`x9by+=4!4UH+&->}Jxw@=`$L z)TzgX)$kDR6RY2cGT?og0%QyK41DBvI&l=@N*)&}nrmGnV93A{%DB4p=FAM^BEWNW zvpGy<%g+hjyePOhN(KOw)N%9WcVS)C(~*e=GkfXhvQtrhz?p2}Di95L0>yPG0j4(t zjolnuckBB(;Wz)^7RDMFH-)RGg~=#Yj!Z zPSu#Sa*7&TORSeB`q*lOsCj}}Vyn`re&JUVBhG$Ngf+tB7dfr5jgDstrd}6vOzhN` zAoX)daYN}_sW?Yy53up@pT(u;@4fh6{l&iwTW!I*sbmtSnW!`qm7D@&#T|MQ$r~k|+aqE>^ zeN39#jpvL8(ubbxNG#U}JF9pfl3cgq>=aiv;RhK9_TV=Fx_2SaFTOr6&{Shps3jnJ zL{0_8Ejwt6y+DKrDF9c0;|&;VR9q3tRN&+sJ8vRcK-BBIwf7H86!XpE))fa`60Zsp ztC8@#vl=gWR1=44JjP=G?w91+SN@N1RSh@3sO2ERIHRqIycgT7gEa?GEU>l_yS?&Z z^2xAje}2NH*N$MSPAUPvkP}&R5UbISB>!j_34>5oc@iTVM0W2f8-P0fvK5gNxiM@# zRXg!9B=BWJ$D%b-=yXFS4Sv;FEF~G3YK*YF4eyV!^Y~xK>6e_k3gs-9M)Lb*==p}h6*$X(pD;v2)%`57oo_=DPwvNyp z77)^q1)Rm}zw*uE$dvDw_&dlLFvS+GT1Sk_6{>nqr{?0Slr6`KP9qD8VDO;9fCrQd zK*czs*eRU^2@-%Lr-?A;lp5?tu!`_Alk{y$%N6rVsE0ilAc#1X=xgce)X{~UggYeV zwHJEtDn^n0uwxO zH4#Q1IY~1zBe+JcRa_n-EN@}+@)z&B_>ZptJ=}ivAHh~+~y*9X7`RE@#yTt+Q=vzURfT!5qS=nXN#{6L8j5k!rUhDcm-@=ls!(lp=% zq=m|26#xnehAPC+V(aoIj&Dkwwy_ODw*U}i45`&c>O{#EV}Nm$Slfk#957LhN9r0J zJH*8SU}|hXx{cR=`AvkSzt4s(h9)U^Y+l&F!L<(7S`ca^ZedL$hNMs%@1+=PQj8iA zE@LWxa0@V5{!4@Ye^lhgV2`TaM7OvNWqe`G7(xO$v4@QhdYwwj4z2?n}l1 z$(I(B*%X`udT(U^;lA=9pqZXQXb-g&b^s*NIkpQJS0OGBN<-@ewXcf} zm2p6fxbnG6AY;)U&GF7x-h;6O*a};ZtYQ1gHjJ@&_2*wKbqx{Y@$YyFoESG>x`v&{ zcd+-?D=-!~^VBxh&!0!^BHsSp8|ncM)RP*Ie*3o|cGvLUH*aBnOKw);gomGh6ywda z=;i^}zWg;!0_xB$M!59pXOR3hUj2o4?s`oo+tXqPf9COXNS<--nH?c9{rXi&2bQac zCqDmCaGK%Z+7f$L-<7hH6Ly|{6zk^+oCt6I_FDi2b-%HAJT>I+VLXGwv)-PVf#pfPH@-?o1{RQ|%R15Qj%b$G~wqmr0z>ODQN6@}z z?ffRrK7APgMpDA%PdyG>6ArJvi~YBD6&Pb|KXwW07cYS0O}z56KaZ_P zzwf?_|FvKGVbs&F;QX^+P;B8?DkEcXbbTL}p8qV2J;Zxoe;vysO%URUhd%ZQn)M0V z;|1RR>Q%6BkpN6~COH4$kHXLbZ-4m(gk?^83V8IhkHA(AM>h^}@b(_IE}zHYyZhL> zyosI1&%hXmtH1tw$&F;H@Z@(r1CC4Vy?Gn^Z|-BXHO6A^7-yb-6dM)L)1n)L=3KKfx?|N6@%bKn83 z{|BQo_~ws4zf$e{x&L`%-50XUtCZu?w2TTNrl+PkrITc=_jE z7K05#niQ-#1>I7v4Fj&07_~l`NX{KRuyzdQPEAfTmS7Q< zdkF1KY(4UQc>MGK-Mtt8ul_9@y!m6W^?4B0x==dx>X%B-sDzeN2eJmn7)@fziBd+q z37wd%CQe}eI))cV;T#4^jd(}e3eB{}*{61J?JMsI(+?8T3Q7P4l~j+V{qVDLB0uq` zp2q9H{4$uW#;kX6%|uOMz2=uHNz9Ts9;c;N1tO_-)f6!v_oux8-T%0rOnwN1%K7O( z^E)4m3LvzriZQ07>MnIjA5TEtQhRtPt1AMAbj}{7*D?l_?XCzg*5KmD&f~RTdJV1` z14dJYOg{7()+xEjFH498KuFAZ{ny`6(3Me-rYgzIlZA|mkUqrX=_U^HQF90&9`8v+ zG~=x?{G3aRQcY?skMA`YRdY#e@cOU64qIC(6=`5yAH_hU8jTdm4{BqbXTlm^O*uq0 zgJ2Az2=NWyIftv!9o@Y>I2s%ngF-pIgMLx_WO$W>+l#)R*V;$-PEb!)UT@|rV zT!NDPWj)ytUO3prjXWDL$|+B=8;l3D$)tRTq)t33C0uy! zJi6l+i~apFYku;(AII`QH7<;3;vk*L~E^qmaADrGyy z4izQ{10j{QuJ8epal*ok9?9=3;CG*s|CfIl2k-m@Tyvph2kS&bxdl6 zg&ISDZRvw12OcIM<=_P4>fWt4Q}uM#9=wn<>hU_f+IchgrXj^d6hK%;7*{D}QxH`@ zKU8&TC8x3P22$3HN65n@e)n?fSIwGc;R&jXf%e=unVBOEBAc^~IRWeE$9U;0@4`4? zb3OU_r|{}8ee?cJ757$LQvCFyCO_YYZKeo|Ly!gH@&;mPG1`0vqw%F)ijY%snto>4 z)TA*}JqTG(w%`~0WyF_f=2cUR{Ms-3MLwWJPq!XL@N<3OO>sr)w*f+9WU*9&S_FX`-^{x|8y+?A1}{GHRDU#?d*-a+i<2%#+&^OPB;A5=MT|6D~~{P}V0 z?X#Ew^5Uu2Ee_?RF&5TklwOTBN&qFvX{WBWj8ta*r06W?hnk?|p`>)+RQjo*k3W$C z8_6Y1Ap*t>j6wPbKig(8N4)ZLZ^#_q2&v@iZ@z=DjQ6S7sQ0yxnrmQO={LcQusA@9 zb9BoCJo?%HE6zRr@Bgu8_`dQBKa7L7ehl^6CGla%qAHDoBTZr^W3d1Bu9VN2A;|`> z{qjq_MWd}{6YP#Odb{g`JqCV%q}QC)@;inlQb;8Xl?<&gF-t;q^hg~0`vp^c4`Mef zd5FT?RC}+e05(}|nW#5iU>1E0Ss)6{IEI`xNHV@ICORK*Pryw1%^*{s6cgmM9w_aI zj9v`i|GUH*)ouEO+HCPu*k#b0h?U+oe~%O*p8D?Rky0vvm(VgBtdOoV8_wkTtFhkf zN@J$I^tcwNnn}5e;_uG)+yG=VB&3+|#CJW36j+mODzW)C1}V%Cy4(2pUw-M2{o;T1 z7k?Q0Z~iEr`qSU3K7f+;B(dbFq$^2(U+c(J4**jY!_$nRaO@C^yqo*b4^0-LO1S=< zd{K+`!XBU+TRi&dCktqtW#=g@z_CT*pzjj{zg|)<#%j!Y40r09ISD!)mE)cR6IfaV&R+upsKS{iwy}0@3Zx`p z*-+xCv11&gNRCTvn1iXBK|3L28V<4K*Kzdvvtn`gldCN)!0F_^8_ z1xw=0;*;=&}J8!hjHDhFfGAKfYe$H6rl z%Dj~4*VqP0sn1zs5o0UgKO|iGH_e!Brc`7U>+k+Qqj5{%Jso za1)lR-kK{`O@GukfMGNsWQIOS-IKU)BK;*&OU|h>97~y$%k~&rY+hQ!{@a4!CKXEb zl3mSZcNJ0wRD0@f~^!RDn4 z#rxj4gbN=zqoAjJ;A(WQJyEL|W3hX6S0&D}F)`F6GPOAS)c@j-{oW+aj%9p4_wbAp^R5%rv4JoNF05hKgJjS&|NP60MkuZuwI!Ri6cgDWgd>3geYDl$$GY(0T-m4rH|_-jkWXN_Q!hhzwvATKIXT69Ykvq-fAt5t}j4j(X~f#Rimdm3Zidp zEzd-0^BA*R$2kA&c_|v0vHR8@9)AAe6=NR2=+`=sPJNPY_{NIAqH!anpH?W)=nFB+iPRMQ|6mHhdDu=bc zFU$XZas~D}_Dd-tEsw?B*LNBN4w$+^>QYg?SvOK%wJd0I21oB6si7w@l)oZE%|)Cb zyhLgy+Nsw`J?xi@XHA2 z9AY;YatZ&zrIB@FWHQi%Aaygi`tGIw*MIce;1@Sw>UAZsNHTE@zBk@1Kx2=-o_Xvc z9A4i=3Zx1>D(%Y9@{o)y5{@237NgBE+-QWucdvu2sxXytC*%qpGb$5udHgnZuKXaL z`27Fp-Y5Sr{4n<3{t`x;k4rFyZzVKD&0PiivOx0UmB+s1M3m2_egE#&U7UUDtlU8Y zmitRA_vZzu6{C)b&@L43(fdLqtpJGtEp`jFn0F$^(WG7xMF|eYF%2Tgl1ztU*DquS z5i0!eus>T)lmZ&-jp*vRn%M>xplw_1cXbe7}$v_w^Fi4O^2xW1^ zZXway<@c>*vZbUZn^pgf(y*Nv_`zE%J%1D}er%Wh*N`p#ZUs|GC^vH`&9se@WXQnQ zlL21QZ#4OJ^W|Nc@m3Rn8N06^!$3|I(#piR`{5$8+WqqQ4splu)Vq^dbsMqU*EUk} z71&!5vnam-L>O<5u{>Iet$#Jbjh9|5BiIx@Y&}NmX1b`O#BkdRz1ym|<#yM(c;T@Z zj{SIZ4YS*aswZGoye-fjzlE*Kf8qX%{}+E4dvARS)#M=nwv_gWNljR?8SA`&yQZqa zJ}K}Uianbg0tUOU?`r)@aHA=1zPv9RlxfrwoOR49q8r&;dg4&hsT6W6rbIMwLhO#Q z5(qPRhzM5Pds1c~8CWAhEy*ti15@sQ23V7lC5lWiE*S+91P6%4Q-PF|Fi~&I4k7%M9XLToJB3CpDHSRHLD$9N z>{DlO>*d=Zr$&)c`dc=4@5soEV4P&j^^#}@JF8zXf?v**0qNlC zS`&rW3i-v2M*tkw&aY#+cMQK+fGY7$OumJyCkSEQXEt&K5o7J_!?^NoAIICj`#KhT zhhXjyyF;9L;&0%|FZ`qXFa9^bgnIh0jLKrt1|MPr8!sg=zM9X?7zJ$ZGiGua+rur zglcgSuEt=$)Bw_j0Dnk`-9l*Xwt{QcN?xTgLOv_up?OT0LaD{_=rivDDK76w%l_bv z0rk{#)dYsDsLPClwH4xWCMiJm2=#PJ7V2mMYirb_H8pQ=h|%Nxvs0uXi^Raeji#a- z$R~9OMfzz-W0aHxlua-Ea)I@Wwxr1j8?P>wQIbLwR*g(m0T|{B8S%7BFcUbifXBWY zxYQDCy>?GIaKrbmz*f|hwgO-QSS$}(y!cb!#O(G= z7p{e|7PgZ9RhW^I?wbN5wo6R5N2sP7;6&gc#FuKkCG8I!4Aq+W-GIp9@SPc6{+X{K z`G^!d`1yO-y8IXJyZB%Gm46R=Z+;2Y=%UD%VM`_iL2R)%E^f-tY1UN(;$YoWPjIWF zJlUZ9e7|pDk}&6zPjV1|kfLNne{K_^mmOQauWPIp^a-Ok< z;Rx}AumNyN#leP|;g>U{6ye*s0Bf2T&xYXJ1={0%a7qZXL&WIO&X15%LJS^T4{yRR zLIGf63gXR1>MNK>B({QSCI~?cMXVbk#(>bZ{Wwsmz)W&sZH^@?K#PEK#@3n`Y>|?| zW1o8%oFqfeFFQQ(U6&QOk*|qSqPIdjzc*M@IORaf!WA;$OjZGDl-S)~vJjNEgcKRE z>kzvZt{Gu*u#{9^V+-!Vi3I!5?7Qr`Y?P_SiQwmlie?jnSL!Z{Y9U6C1R$h<1i9G%5yX2*&1Ca^%@!F^ zWzEG+Nl&Z~lq>K`Ja|d}RiD4u!FO|g7+tZU%H*74`)y=~>Lw9;ZylE8*Nnm%T{xLg z%P}DNR@Giqyx(L8_Ar&htG{?vdE!dHT^{vmEZpDTkjF{JVsd7L&<3O!;Foi3URuNP z&6%(i2i%6>h2vc~Pj{5@PUy7;c;%N~g{g$P%f6G79of?Q?%1Jl2pGQeSiiWAW@7@f z4!&Jr7}}W~bNeX>i}oV>(&^`Y_p5JXvA-zQkdR({Ii<*m2`X}yf|Z_sdGrQ0FMThb z`ocfF58(GVaq#v}Dz|?O7&}Pi5e@}&fHA<;-*}~%YXSlUXI+R)@nrKN4P6!A#f5@> zM%pL@^??N3NL!cGJHBMfwXps=(SZ!?;D7|b2zclBZ^6|gH56GYH5e=3tAwqx_)uE4 zs+V*i!v32_7;lYWje{*_A$=Pmh;l-R3&hwKOEBX`gBT~$P4I)t11Lkzp7fK6JH$%% z;ek;?iCF8g>8Kfh*S~QaVUZ;57l5s@!w=L(YphvtspnQRqzqKT4rwW<)M{8$2_Ynj z5k>~ea(S5nJ%j*PG2Z*yO;n>viP*mWjob2l>d_GKR3_WK`yt~bgWhP8E^(j4i^LLL zem`PWVr*X8!r3R!VzGNH0O(25RdzD=T^Vz7OrjYSA$2)O>Wo2W)=0Y_#f2QowS#yF*K zim<9j=53EPC&6Cw`?b}iztu@aVo4K-QY>MjyKn5HJ@gTf5m0v+S#QY;sTgE>|fnei#fvl);<=u4%Anl%d{rU_i^!~AI9f6_FG))T zw8z)s7w=;G(J$huKYj1T|H{w(5caSA1nl^NP^g( zpi}8WkDz)4z`&Y_;1@9D1d!tyx?>>4c@gy;-w+ZTwG_OE1CQ_J5)Y%|wCdKST zC1#`qdS9{u9;h0y{62Wt(L~H7LMPTo!LI}-a<-lxNPxS=OdU~-miHBGJ%V2x!`P$> zxUe$T%lvMk70b;w)fmyYuoV!NNi4k-p9r&Kb=7GAL^1w=fvIXOoq@EN0d)h83{z z3BSCJ*e&q%cRz*Qt8d}fE4QWUP@xN%N=#8YrM_xK!YSeHFJH&`k35VEXU2H@%Wt4+ zM(E}@u=Ds2-goi8{IfrVy*EYJi?Ez2nkxs3ttMg_KWrgXB5cSaafBgTHiZ-!?@@1_${Q4plXCzeBI#OJ~&v&(?PerCJT|lGR^ zb9~Sf!1reiNZF**cPg=s2*HbigfjI^)gY+@Pi$v$(L+Ep8N(QZZV{C39}pKZLyulg zbyJUpQ;n$TDO9*o!7t}x)g2?Ek9hJ6PvV82eihs;N?MWduOTOkF*2?R9%-5SmJA7g zxlj;Ms+cmithslkri(a*gvY+)GG6%U*I=?D%vkuv5^j8t>LIhp2s(B&#W$V- zI9H=x>?8U^gl-QzkA4x;GtcAbMnp9R9{vO=W+J8&v0GsK(T6eLpP@TCDo!+g&q+wZ z;h~QQ01?KHaOwH~{e2hz?$sYfGra_6huC#1pcQih1*}STIqJ1|3~&7EYpTu+@Lf>X zA0fg-#U(^T&!7|$(?U~xZ6R!Riz6Km_fs6BCN6C9&(-ha75r0zQ%VVl2YBeY^SJt( z?+9^?0AnmnC8RA7Nh_oyYk>kyFIpl(OaUAj%aOQTvjxB_-~V1WZ2(yIVwDJ3J1F<={>3|2cCdT(MvrySs`yCQjdN7Z z2EOHg`R*V3Hm#liH^0>{|J)B_@9K}ijV@q%?vj#&QlFNr{`j1tOe$Orv@`v43Bw@k zP}L*&_E-oqFv9a-Rta_xxv#5*P$s*#2E{CqLd0XAdmL~4+G`+dU`G~b9^b-yU%v(8 zGN7Em+=@uYn1Tz*M5KeM`p^{XMkTsFda;b>6cJMg>l$(XWg!k3a)S70mZh=Sd2AcE zUq8U|Uj8}c1t7_wjxhWZY%R9ZDMa{o zsQ?}Kp$M9inq|GC&4}5pMc)JGOt=tM!Lj_`suCm^gyl>CzHItn2sdBc74JP;$@}ox zBFpK+)xj^-iN!dKcPild!rUkz-V(F zm0b7!iR0 z#I$b4YV~bYQme#{uoU?v5w8B`n_5itx$hj}JyFs;GnVPVsNA)T-p_9fjW&j5aTsEi z|0eHOHY~D)qxTqgJOvqpX5HcX*WQtnBzn!NQxgd%i9DfO-m?V|je{`44&OOa!6PGi zFS+zt4ttl^>%Vrjw?NOvoWZLH9%H-}apubRzORe_rGNR?vG>N0pQ~ZLJmty$5gWOSSLA2eIz9UtwvJmw>1`f zS8s^gQb(eP@4P3Ai4$Oq>MfQ?tyrSv35WFMgyeMim ze9K~OmwhQx#O!8^gSQV0_e1RLWRo|p)}(A1TW7{&W}dNr{#dFc`T$k{D>`N_W*tdS z?!=6Cag3DYw64Zekp`>z2P@UrFRgmt6IRJkBv_lVqMpJ~sU;+g(6;gr<^6#O?fgK) zV?>*8U9DVjlu(hPLImUbH*SfuY)a^k6As>)sc9%7F~JxKB8{O#j6wL^_qd`-dE;XG zIqk1YelN{f+mX9`E%!3&{NK@ggy6x%ICyst``0eMe<%OX{Y@G5O)i3=U?KxB2De_@ z#qrIflC4Lac^V0(UIT2UB{Ve5RphzNcw0UjE9POWg1!?8h^eQ&dNg=Z_vxZb6mk89 z8%P|GIH4X-aqTPD!3K!xGMxv!rkTieI6tT8jYt>?37XSq985iu&sNAlNiD~Erl^qG zdQ~Shvv7GdQ}a>Tq@xCjTUW_! z-dNd%{jz*{ib1#tR=$=}k};WrHIx&F?=b}+btM9Tb~clTM?a6LYJ}zdo?;;4OkH*Q z*#_7~65rxtI`M#CFaFeD!|m681g<#`GNLOG5tM_m2Ci;&49p-@3f5ZXBZT6eKuUua zjshbD3F|0vTbdFTyH+CQ+a+987xPvrq56ektVNw==SsLD2na{|Z&Qo{A}djdY}BVSjK!66{Fi^99GwuY-GTGFpN{N#Ke&=MSf-1TxsSLgQ`ZxMn)bMTi06p z262QLatek&l~;eCM~o>l9{J2AT>HKEWX@_y3`DxFYyvR~P{kp_m`cD&eVAk0g6dk7 zjZtizL%Ue{sOAr_aE|Q~;3XdY?8A8HYe&LvWJYS+V%m8xVug7AYZ@>KQHC=Cg)vZ2 zuD*Wy?-QCSU@JM;R49j?D-#@DpTjR?KWqRJ>T>tuQgvCr0fJ%=wWaU1SDa^!f zc`Tu-89M@%4WD4p&JF>N=)3@Z##D&DD?WlyI&Svf7dg2{N(@&UY+qT!%@+?Oe*q#L z27sXAuc10LaTH~xPLrV2cP;n8$|x;ATQ~X=gV!fi7ClI|FlUVd<5h!$4^Rs*{6!?v zT`gZzE~S_eV-h8rv84qTkxc|4;Nr*5BDI3Zr>-mHqFjEZ$bI{y&Ri#d#SjofM2w8- z`E~ft_g|k<`FB)RfOEnrkBsfdrc!!VBQO)rK79@dqCgYxlB#bpQbI~e`1Il47+`+S z({p3QE+`?z>U-dOatK(fnb0ngh>BI!rROh5Scaj)a#GsmlB!)66=8~V(07RXaPdu+FroKhV)k*pwIRc%u zGC7H04i5X64M0`!AVE7dnyH0>FFb74(;qs1v!fA8{yN{~7X;j?#uM180uiu0*q4!v zhHb>)VO@o4JVx|hm2R?O>^wO^^bujXM6+%&J|q7|CJWiKP{1KujfKF0bYHMm9y&sM zB(&BTqvX${2TK3I3z`x_yTqmE&tUiJzV?@$5~A+%vYMD|8UPOD8a2ZZif}i(iE|(N zckz+$`_X$Z{-6A-*nRCs;F>caH-c-X02tG=W8Iu0g71)bFpg3NZ#}XN)|ShdWQ9r$1F#XIPpOhrB%?cPXj|+&Hb!VW*(hVenI|TqB25vt zs`?W~pPA4ul*uPJOgEEMpA=d>uzEq8I0Q=>C9cE1mlLS-Gys@N+MeF*PW5|;ZOWUV9# zKvNK-PgmCxg_V0mlD=`Y zQDJ^-rlcl@NV<}0&!smM?mX@*2bKU5;PqdTTtm{n(=}rl=kAU8a{qoQiS;>|5$3nO z1Qw;F{Pw%7eEH_{E_v zNUK(MY9AMqP`%qZhN3kIBh?XZ5u~eM+pr_e zr6VcrjLJxR5>a)}4Zv6{S%(Bo1?pLSelbJzohZ@n+5$u!O;$zu_~viF1zS4--`ogp zynT<&LnaSlepEFP#hIsn0MGu#|KYxifA{qt1z`%~CNR}Vh*&8gg@kv%c0)gp z73@SSI3j7IuKnJwu;-RD#db}f%itta&{j@2zy9#Bu2H&j_S!cJ*5##5<=*A#iXDyc z^3T5~2gYauvcQW!eGRUuWwsAVr3%YMSy-l-^!kt_Y|{K5Qj7@dh9t&RI!Pc?V#vjG zR=fp4ISp70KHvJST{*b2AW1n1R8_s7pk5P(RKi2;Kk0^^Kko-B9hmQ@TvAt`e&jWX zykYSN6mEP4-!2M>0O>#$zY^QFTn)b`=Y;1Hh;zW3)#{f(om1Z641hoy}V(3bT9i`4bC>=~BrxkWf+ujS?edR^wf2 z2WCR(L_V6;grQxiA<`7rzwr-obp0zxVJ0p;^vxgtcX9jmA4M~L2*!>J8;-Qq0c8)zP`n?Ck%lfS!6rI2G3kvk^R>vLtrRqN zT(4{JEgY*QJK3HfrtZ@DdICUb=Te!WQ_hSO04cOWv>K!hk_9AXy~{Z%AC zMG=*>ty)2yXrB`hyNJnljoB^Vw~;)M|AG)hEE`cmDk?)iUzCTAoX&ESm`>&e8rx0~ z>~wDgfI~tGjJ2~BX1iV#fpra2k3cQ0sQymu9>N&3Rsuz;UIZnVrNZ*?ZFI+1g+^)! z920CcMl(5w6r~6Cvwd{)cd_%t_v0h?Ir(4w=^w)W)t^K)z9@^IdZwCe!~j*Uj4>sh zIHRe0U9FVx3i}F}r;I=_BX%uPVANBCDiX^KyYQD8y0bwqfBBS}(3R-Xn$pKX0)-UP#JqNTrmDAtAI8 zzKv+-9Ul4YHeythjF^y;LG%es2G#nSl%<|EqeT8Lg#?Uk; z7S?XU)!QJ9^Pj>s7lBmkbVl5F!m>r^qTIK( zleVR?NQn?VBlw6IB8-}en5u#?4LDVR8-pR{uwucM5S9TCe_{)^85hT%&rD(J zse-=&Vc{kAUAYP=NJM>TqagfAae)xv+ZLfq;3UjeS4+DZmR^2!@+`&9Yx}_PZHKT7 z@|;D{#H1*KqUaN>BiNc?D+61b4^9Q(8nZI`dgjmX;O*Z%#A2_*vRlAZHJu=1;t>T=%u77LvF@Fun&*}(Q= z>-d#_@O8M691Yc|!qZ=n0MU2<)omQUGefgBMGOHKpF4xmdWWOyZ{qOX@4Ii=|Kd;m z4eY-9V|ez@e>duB!eWo{&cC{;HrT+{BO5sX;Vlp`UjDha0J6CJ=|}O(&%cUi{+&l* zY{Kr#Sm4dyzNNsd!Q{*u9{Y}q07ty_<$bh=BrHrHaOGRZ*nZ?Jx@E-I{`ob4 zfM-5`5m$ffHXi@>rvRSg)n9rKp))W}d=)?RJrBV)6%O8E+`Y3=rYwONY4qy1Mox#={?f3>y!%NRjcvPhQ8vpE!%fuEqX4 zH}Uj$UjRV3`I1nCv!d|wr*^P*euVBQ;Pqd-C0?1K!+5jCBcC~gw|?h#k!nAvBH)31 z-!K2e53L9&x>@{rSGh056k&)EIGk~>ll|wgQ7LrM}X0pyAbOKi8Qh$1GgvG9w$hzQ2V)@{zN-Vb)9nL(t2C{@( zugt(9;<+Du7%%+ft`d0|cHDrIyf5P_gn6qy`Vp3UyZGM!obR%@{x;tI+W$*bb7!YG zzR`**4FH*h81^vPej1@$FIUQPJ%JXA$1*? z3C7AVvVx2F`61flcX9TqAHc`{%0Iv7J^RLw{T1w8{c%`#7NDjlAep2*7A{jXQh`b} zsqpBxoW;vOb8Y3?ldAWg`QE>R>tA~V?Xh?k+GdQ!?(c(B3yub%^=Q@^%foN=hOm`gK`}!eL40!H~SMc>8 zdmGFVu5JneO!Ock84t@=o|oETvNgf+tz!jB<=$FLsHPKyR4fM3Lw9-N5bTb-@u@syKA>N}a!hNXr*8o*YBTQ46YbU6T0P+wCCqHUW>FoTrf z+YYX&dk()^fssa!TW9o!0*k#)5O^pOZ_9Iz;6;*bYm1{BOW4Z6I)lg&Z~x97h|~)J zBp!p-0VyK+pbr9zN1eIPUaJ_?Vz)&z9jRfdAenb{tu7c!6v`<{Uwh-FL%60EAPxlL z1lLskgd~Y2HuDmG`5w+a^Ox}4U-}>KdB48?BYzpYum2dT$wj!)ny~snAj@#o8Hq{f ztk$#$ex}a24R&9fm4`RqCkgrtICyIhDT>L5abnCGx)v$Sg%{u$?Qw*$M%|S}4==_b z4$MKXkC2IMZNY5RF*?V&k?QUfmIrfm$0Qij@iDG_^)|?e2H#Yw`Opawk<9Yz=~x(_ z0FG}TNhJ!>{DBBE4xwElbS;cAu$2XCgm>xrnbe_4EWV60IJnlLo5}N*$AL1wm1%@^ z6;h%;`$Ln9bF>{9LTJ+i_XK$$8$b%_&Z&S#d=+XgGg9Yan;KP9qn$}~6EwBw=w_?q zADJziS{50pvWuKkZoM+QQj}9K({57&IV*Teh%4`fXte-h{r;RpZRnRtNI@*$bGEDg z^wd*}IVD(IA*3KoMI8;@%hZF1{_ck0MopQ0XJe1yIBfN6sXVk~4fftVl9MdYiwB0O zd8Xbi=ICbc;_TBufakvdK8ycf{AJvF?MF~g9}?ew9?+-#0_Q&T*ATjSZ+~WM%x?Z3 z7JIKCEL$Ai0PIMp-MulX5Ture0H}f+H!1`Z_Z%hxhVyuqNdi8}S+QzUp_c*4eIx^d zg&mC%ml6cSES`*+LeXul9If)LeBXy9EI`GI?a zyf2nudE7%r(|I5x6x&n?ixypoK#ICkD}t-DkF5XBVDc5@+zn@Leb7!6w7H#b%WS>RFe((nJ+{onF6Q> zAW~1Sr36@la~0Zl25PjeiA$NClNHybqXQT-DJ!oR;`F!Fjv9pdv1}446=9KWM$*C= zvE51>L9T+I&y-G_N`*#ArUKbbm`ZMBTJ4?nZ~|L(C{o*b<`a{s)eSm-tx<|Rhgj_8 zY}2T9oR{8|AM7UZ;MIUCmdHqQ>2YF$pNUFrNI=7r<-)W6^9(OEm$eo}iR@-hlzlg-8i-%~;1Za%;QeS${*V zLkf&)JObJKZ;UA>Jo%^3$@D?J>m{nH6Ar>%UZL{{ZP%YBtT}pF%AArcerA{|p18$!X4`t3%y^G9Z{562%{$s(qN;Ik*79Z~}N z|JS?)*7T&fY=J0MZR}(+&`Z<@c56c>e2Asp%6}d^UufO@KDdHCpbdal9{LzUDRVPX zhm0wa;J#f7#g?QD<^+tlD~!)L#4akI73!+$)Edl6%b@byEXIJf2tKNRpVfc{A?P$~ z7QPLLotKtF%h2qKE52`p6cQf)&K+0-pr;}iI0EZ37$L`nrx^84N>sAwi3~W`wIJqXa9!W$t85toPhi-NZH<`-j6Qt#e zUN;-hxcr$7Ar2Xf7=SC^zJu;~f#|((u$f`3T{ZH^vT{SHCI-&cD->a)%sm0)Oj*ce zNCHj)u(jfGnNRr$Jp8c>MSvt~NIM{$nO;DoRYP{JBCC$XD{JY&8G}5ybl}16mj}Wa++8kg zC=5sl+em;6MS+meJf()5$XS{B^3CrK=bSyPwZ8u2`_|g)-1lAzY+j;A-K|JvzWeUI zXWDD6@B4kfp$|BG^90*ZbO&ZwR~2T8RpS#fcYfj!sTi+N2l*!n{Wq1{}I&0~}qB$R_7Nwi#P8&1jgP z?nFNTiHRqY@Xa`QsQ`@d@!=VT*SY{~aS z_ILmbKo-DvTR&qtW5zzvvT zt9taR&}APr+Dv5U16)(XRrP2`cFkb`arMp({g49@68AOt*lG;I%&bX#?@g~ic3X1Mn^&%;&@aS$gY*~lfGY2$_lJI@G;Zfc#{q|CI!b{)tAD+GXa5HS2_;&q>A~<%4y69u<5dBhQ zx!MW@e;lFT1(6f0YG&AG9k#0RwSV^{s_7P*^)(D_i{K@PF)gfbeC{G{ex+DzafmAL zN&PxtNGm6U+!9802%@SMLl!NYBMd@hw7a{oZd1TASIOz0L1n^YAHOIQ27Qkn`_x$& zYf#ryaNoi@C!P&M*r!_&Jpx$+@^X*JzN1P)9T6u_#?%uenQ(&kM#nnFy}x-%=+NnZ zs)Ed?Js7rWG~jelYx+J@s)4v2R?4+!p=}cps;ZKbQ)RP=5@aG}WS!M${o=I5e%T83?6}HZt8o-%1bD)* z#A5ptuDk19c-asBhi|$m$LLkO<0V3y2U+}~m-zM2sW4|y_MZw?Vx4b7LsR(WITb%K z`uSeccL5QoCsQC=u#pfK4wZ(E0+!s0oR{>yw3S1*H_rfJ9J~O5O4J}SFzObagmGde zPsRJIMIxnGFv@&^ez_2?hfm%MlZgb7 z3x}wL93C}+rm7T07N9S*0|FBJOo2M*axh4mS!ZuJXVJHkKttB#(OL|QBR5yreyT@o zTck`-6wywiQ&*Rm( z%P}SkG7$(twfIolECJ*t0vi##XTBm?c^U5^jc$wR7cGKc!c>zS&(2yRSq(oOy&{0E zn;Oi|#vJ0A-aj%)1*llHruv9n*p9+2q{L(;jn!Js8A_$bID-$I2}A?}ye>&**xF&b z<*<8d30~^&DmU=Tfd`g$76Lw!-pfJ9#Jn$qFolc67)6AnoQ-~OH;a&>iTOQD_Tt&b zny!8{(AAj&EO)xoB%Xm=bxyS~Q z0ub4zN<5~^EPh767!ZbtTVA~>7E#Jgu#JS3*0Uq1*KWY%(Dj%dxf#vo^|<-fFNAG2 z!E80sEFkm?_~mIFzw6z2*$;jAPx+A;kgZ{>8C2XxE zW2k1wQB9AbnjVFz55Y}Oz)fz#;oDB&(NFw8U~LUTRfM$agu^q|j!&?5bRq_k0WsCdFu#)5!wN9h&X&p zh2SZx{*0#nCM^gTEkKNnYJGy^FFdT#5|l%IQ_B)MuK=l7gbi(v>{Xz+fU9aD`l#wN zLpxmH#yTox#S2C;7sP5zorhf2JQrv4b5Q}%7{RSC*fmuSK}d*d)zqUgCd-NaiXXiR zU;L%VVe2H%l-3}0Lq=R>?83zqK!ZrU%vmY(kVNm#7+qVX#5HhrrJ)s~a+9Q9#?9Zp zfzuBzfx#mTz-@2X!UMl^LG%cTy=ToBBB2wG%kQ)j3X(y}$gs}HNgVVPC5L(@L@16z zN1dOExcmEW!hOH}41CwY_j?H4863Ogop|YAzcS!=>b_qS$*P*t8Annz5#YNXhi+*w zKeIr;=+%$jtL!*{DdhKBlzWWpU$TwIKJo9-ZC^w++W-fGsa^JJAeIgMj<;^%%fGS> za<%&TS()MnvH9Z23d}hS-j6~TN@=?(zTKjp%n*Xk^R;?N%ti{667YhU&glW_wfS!t`-w;-yO&=j#s`9oIl2{}zmdzG&XzT;%} z?;{RBZ-Uve3eSA4&yTymm8YWWttJhIwo_7-TA3$X<7;vQh^sw*XnWX6t&Arbb>y;? zA*EyJvIv72Kmhhm?qYVdLbvUuG$p_`k}t@G$ft~cC1qEI-e;897z0?~-rqVchTuUA zJksdK_k(;tNPj-a=&`W;Znq<5N1c|I^5HidGdT&Ol+8T&XZ_5?iP+-4kDQgwzyMWK zDK%68Eu+@t44__{V(;`4?e+k05K{@dh@-c^6EFLr-@M}EeEHYjhG)M1QMjgH$kk#n zAzFBbt1aRnlM*7z`3q^67Sl@oV>H-#Y7^T}N4R>HCC{NB^2Qruz~=Q6JoD8h!VuuB zELt)ZjIjtT69EDcLIhOQ_3+2Gb@NjeUFZ}X7Q$BSloFmTw`4Tynp!rMtl1YXhMU%~ zE~4AI{PF-Vh#z7s$HEcV zI8o!m6SAOr-c;fDqKy|Xy+!gt{1@4fQkzvqMR!nub(ipkco(ZVZ8`mUP* z)`@+W?+2JV!c{KEZlwW^B-tt*R-44-d4g?}yT5Xj$tmob8hd9t?3`MHoZ5gJ2POmW zgL?HlL;!y9a8(6kCB`d(ufg}qjjv#;8pC3)919B=gKAQ*kja=ON!m?n-1F;az|0ty zGKqQQkCqzMtz*4j4LKvPENDM(Wg-(Ie9thIg|mcy-)pR=4f^?VeEf80htUM3USCJI zy@$nlkKNO~@LrffCB>Wbr>(7tOag)*(046dT`QhppOY5gDw%?o|48W+eaNAjU_v_= z1XMsR0d|ZP^AGPuw%P7P?3~;au}Cb?SRM(Lz7H~Ch%AN~iyr;1I?F&xFbGKZhZO6k7x0jpb1)LFI>CMAc_7=)57u%UAIz(mw=sk zDvmZ?u5#$+oocp7-HCv1Z=tr}cI757>An5#9zej^hZf3OwYmKjpcMrJQ8N&g-AX#* z^_@&&th3Jv58&!*0n)g~$(!Jp!@km86@n~oG9oi475fnUvKN7n8m8LD<+-YDYPiN} z7)4m|#U}uE;&fcbI^uyi6AcVQLc$s4YZ(3cR8L@tFm#e+(=D{>;IYCX4a1Ppko~64 ztz!QqfT93nCbttS^kM~yl#vf>Ji6_(xbEBDiC4Vu*%$wr2R@8u{RC_^0~v!q`GeDV z-gg|siMzM-zEjKX0QhA<=pw=pF)X`$`rq-^>u~gr4LtVA^Rg)$v88gA&CJbY^&W?a zp&ihynXzpy4^C4_$ftF%8itu~{xaL4zz4(`NkTg$Bve2_VF)VqT3ZhTlF~kzh4Q$8 z5=f}U)@U|g`iw?&pI8=LXnO?*je^MH0vtmS06WI4W3|?byjLHiYHHL?GfJT$69rq@ z5o_Q}*F<7OhQ5biEDCHuLSKX%!lldowNwFc9@%gi=8^v_3b){+%;+m6i!@_0u+UJg z+@SLzi%Wv~&*hL%0HGaLet(QCwpCu*4%;~RKH{bCIfkJR@IFc?2_cg?T;&AGuPYem zU?(-Kv#4kCsSWLb7=e1tAOr=3j0l*5+K^=%_$#3BF$nVg;&&fWb5FI%o>VztfZ~L1KBC%egy|OqAE?)? z@JBRB3d*Pp?T`g&$$e?;-3is2!R>F_gl|2vziEVS@?&yO@IiiFa|6NOpcs35LY3g0hc?goFLU&4d;yF2EfpI1Rr5*LOrdtJf&Q1 z#}JiCD9^?B9^GDxVL4#)#&z8E$}O2Hgowkp);RH^DTXeH+Kt4-!qirkYF8Ko`j9sW zV+q4z$kU8r-pUQ2D!dp1y2TEb+h?$O)A!<4KlWSCBJ6o5PTl_z)a%#5)Kd{y0jQfw z@C!>~(MDrH>BUF)@Z=ZI;ogrtgT;2AHy?l@Av|K15|v}m4Af0C+PO1hXnUM^(Hfrl z+8jI2r1%ftnaoje#kV(%rEeqYG$wX*>fi=RjR zBw`=4OE1SV6O9yY7GZ_8Hq(cVBOJS9g3x(YmZ|w{A)ZMnHfgAb5RZh5o%q;9z}Y`( z;RlbZspK?EVir~vqK+S7L?l8WJoUv3xbB6umU|VR{K6bJfBV{4Z4|Gsp43-%1PMet@&DVmg+tiBP$fv zF%+vLDUZv9Unbzp{Y%{ajzc*4wS|fY49JZPKbQn00=BV;eZa8WNsC=M5fVW*l2i9C zHAZU4n~u@Ikg%WnY2@p+CVc5v&I)IpL^=1>4_(OK{G?;E$PB-HHZ3q^s`Jxd-c@@u zD){~cKSYFKiEj5CPQ2*tc-4=7i(__jmd8_lr(F1xZ zG9^2xDFSqC7sHsx<_E^TAKq3`5Q9Pl+6YtGv9wlxeB2v_7GyN3M!5w5qhI!kFrJ@d zW+f}SoK%Tv)0K@$PS2tDAOiYD$i|{U86A_x!MD#+P>~r8I|-xp6Umbp`W|6eVpyEV z^)G%0Uil+e2K>JCf%o9-BcH_j@uM(yiaXwV9K(|E#AnXJR23?tMiV*~#SjZ5iAHK# z2|VifkGqt$AT5cpLMEyv7B79vsy%ozc!>b(20Sbo#`N-9E2}}D1oru(&i%8kE&QSr z(3V72o!3}ts}>kOL}91KC{;wxZv>*w?bG>|%JtZG_{xWr@}BM?07i=;jzBeSgt^K< z90IJmFtJWXf)B9Hj#@*y1~&j)QR_n^A3(<<%ww92n>hH^uW0RM(P)A<1NByeZl}|% z0Y){cFmyh9zPrlgtoP^zy*0H|2BeHM(WJ=uZn9ouxz|b0ryEM}5yld#iNmlQ^0-l) zj4`_MFstDs&@Y6&XY0$qC6kTgk~6=u_7crGA`VNmJLhr3i{JjNiyr`9`M!ULSHADk zuiy5a|LJpE38V~&0(OXUEeyg83<1riL3^>oWW(demrF8`*jNc-#wLYddc-gw414hH zj)0u5QkUHZ{d~R>fu&?n*v1J1GH=)#hfXomu16a9$RcglD|CBmJEn%Ou~W@{fQE=r zCWo2YA@%{bsUWiSw6LXwX*MS6GL3u!M;}%fqVA=kobRi*1Qlc7)J91sV_+=9IRoEv zKJd!l;Rkt~$c(D4HHqr#76Dg%11Nh-BAvM>aW9QMJV%CK`jwxxm5iEvAGC+6(eFwf z8(DT}(i^FnR_K-j_Kn(lE~szg5SY=Mq2d#~65 z!&rvzyddnufaP`~{ijr3@p0soe5;@h$AHKlP}Lz|w7P1*p~v##U##X zd+kaFXg#apyYxAzSSdsd%QoADUrj#1bK(gI=nUM)f<%FA)R|s<0u7UKXY6AJ1Dka* zp5PGSY6oj2@~NAPMRParKpa zeJK;ka`hA$afoPU6^PYgNn5UdcR7M1VA#4ED+b6k%+(IAsf6B~NJEA$6M28FZUC22 z^9cwB9iy>;ON!u|%BWvnQfm=j{=Q@A=N>Uq?v>KJ=%$r!CgskI92N>g=;s|6J=|2s zJ44heQr>Lwbyv^|0w6ILl1w6;y0^vkFP;el&=4N@kGr_*`wk&!q!nO6u0l22f}u^g z<}h4+7;d@=<0iuRB9cCsfH1V^_AcVY-QR~-|IOe2Q@)kzi*KAT^=u8}8W>xFto)fw z4P$En72@C!y&HM?wuY^0*t&t6Of+||2H6_M)o6DN7Tbgv^!r@{Q_1%jD~}_OH$DbY z1>@w;rfT%HPGH;=w%I_vb{zAw58|mW{sOAWx(1BW=oF1YDF@Qe#pEdfr?0UrcQFSQuHWv;WnJ+BF0M zVxMOoWzM{Y;i_qZkV$Oi5=jeW3Vxjf2D3c}K} zk_fPtu-qPS-$yUNTA4^BoA7E<3&|2O({V#zSCX2bn}uy1#7HKE5MQrl#MKaVQ-L`w6-X|4+T*%%HrSN#`Q1$UcCBm z{>~NOvd{mUx8dYhKZZVu_xr$;bye}YPCabhkR-=GbVn5AFKZ>>~uX{4s*E%7*Biy^9CqHqPm;Nd0{F%_{+I2l9Gxt#RAq$hIp5#3&wrN#1` zRjUzY@kS4XB{1En;CrnKBBS5wG*om%-&3n(j74Zi<4>{{cH%J1dsL0g+~Evj?-61c zIV&{YsM+Izl)B3RL)d`C;^n|#>^>s^r0+ePsbDJu-xa_zky^(oyHfoR`WRu7-I%o? z6*{@b$%cWeivvD48$e?9@H3~pxQ}A00;r~R02!MQ1%VD^RKSsf&aK8A*B&` z(L1*A!0+yi(%>YgcSfnv0l_cP?QG-vmwX>y^S7R5@&D4>@yxv+#q{tIxMnRIUsSUi zF^xR+WV5xA?1XfiM8G=0O>6kjXSXwE!1uuP@VahZvhgMXmZ0Ggwi<~^WCUoH_s76A z5>cK?-z3qrwFLzsyHqoW7$cnW^Akx+9?Oh)7(oL-fY=KLB=c{ZACJg_oDh}jwFlXt;WOzLt`#xZD$YS%R8patIM|kv;yLi=)AD3z- zM?vYN0};MoV7a}G6EFVWXI=bX{N;Dx^nJgLX8nlv%vJX5_sf77%?OaeAO@*s+@u!c z32409GJ?q?Z z5R^nB9X$iq<^^wS%>>j#ovr*_x7XuEZ{GmNh+#RP-R<(`ls?3u%E_a5%<#y^m!eZ3 z!tHOGVSZNI4mVnL`%YRDN@1X2`D~RGWNHgi0J@(qMRp6q^l*h2{M9YdcUY4**^mW0 zqYZgfj3LGdul8S=0$;2usT{?fI{JX10=0}$r28bV4oSuegCrI`2Sb48W&;q!U6D*h zHnX%;Sp23(Zwrf_$rEIz{<|k6@p?>$u>{ z%lMlWlnGtP3Ab^GsHQS%bd?!#1f3EXWAN1vUBt$*8m<-}gZ4b*(T{KINRf&WXTY%B zL$|ks8(;c;c zv}~_)9NTKqs5D?mkjsU~dyQ!mE46+BJoc$=Sw2J5`MKT?3=H#rj5H4!rxexT5cA&+ zomW3f`Pmp4gYr>A7c#-jkWi|lG6Y2_2&^A%@Ph9;4By4kBQcIGWu^KMaW*CquqA;{ zWZ8U;8Q)JP#Z@R@NIx`0ndD`gLRbCrZ_6%uTt@5 zqej0NG(1Mea^wiyhs@GTkzm8pBL>2$`<7XtMJBmJOIbeBl?l8I*-Z5IH?85m-?=D% z_9G)O0e!yp2wv>p(?YH{Cg^us#3)KtBsXM3Sz<0W;Z)|oUOO8=H65v&90^Bmt#RS; z9)2l(Vz=Gn_zP-OGmGVR!1>2JB!y+YM!4~1f_k66zY~46p2|19bOs`awL^ekdQ?-X z1dcp!g2`cmZ5*EX%nm3IR)Fj75(3G^rv{w5SCU^i0Ecg>G2N1R`pK{CAr2nZR1$b^ zcvzNRL zuXx|Hq5bc`(_i{6H0y^!G{tpy%|I~NJ=0@;rq83MjS~%KMfM)*G{}h zY`S-z?y=k+q=#g};pa7yiP#35dq`eC5wJKv;Mg7VoG(7n!?z-7_DheWcQmjKq1z3( z_ze;0_#t4jVQ}cC3LwIn2U_@LuNw+*;_eB^5*B9}yQk%5=K!o9cUZr^0TJWWy-RS6 zsMad9yB%(N#S9!H){YvqJ3ZXQBJKq=8;t2816w&P&IOeQ8yE{*|KbVi^$nbPU~YsV{aV4S{RZ0GCs z8h$b0hL?yV@%GaL+UxTq;%n$Us#yj01D^cyQuy;)vUPhscnE6k#W{U8 zms$|2r>VxEh5TF!bEY#!ht>9Dt*@Pf*XQYzE<5kK5ne zpxxfX^)LB>XI=dFeDHhmjnDoTs@b|I-blkn5x_x`Vq?Lvk6}QJ9wGF~1eEVde=i`O zF!PLKL_f&S2M>;e{KFoit(Kn)Ll56|80Ia49}uHz0Ak3$+t$$E5tM}}DZ69XND%FS zvI4WNfTBvhJ$&0E2CoJh8ojI$%qd(o4iU!AK!&k#eWesz34GnS5ok7z1i9$s!#R`N zNaJh<+c-vzVy6|ip7i-kDbxamY@-z1>VI1d- z0P(Gm)34v_M-PSZu9Q)7@8K6c$QV>>6S!%W+brTdq+DSgUoCw8&;9!SpT1#b{MRqy z9}wZKIq!%$d;Ea_SIgoYmOj6MIb}sR2wCi-LXRw|Pah;DSBX31fM)72^g+tZAU$C- ztpFn>m#9pIgw(7#7;r9AjWO`O}*gn+FE0Vgi(G!5EXx3a|Xp!}$0=I|Vm2 zAj_B?I*+@)`>*1}-B&LAKmIc>!^y9H74JiIsMjDkha&4quAIa% zsg$p3C%T4|daIuS5rHgcJ-S4&D+SuBS!zM1N@@x+1lL&jHsrpU2yl%RBF+%R%bp2d zjaDd+3?)ftXk~#(#%Fqb?af(z7PG4l@X8`*#TGvbFde-(87D=dnA#FT7k{-{Ge26M{BQcNPF@==fN$eI`YORV zLe*Fdt%qhM3JWiYU%p?+!c3|zRyidVE~?3mmmysIe(v-f7UdBes01HcxU{)!=#%fPIvWf+`niv=-V>f2Xp_JW@$AiDOgQIsg z*m-&Y#Q~?jHsDQv_sWa^@lXFes_7BD;JbbpWT!dCiVNStY-6npp7RX(aq`Q}`* zUUDS0CM9|4f!B^#Se)zOhnV&KY5J1MQ8E@$ZhIVsR*al_o8-HOG#Hb`BF#8W>(Tfx zW7EQqE3fa^9W~B9)FMWYYO;YFUiv;vwvOQJgWF&)U?G_)T=Q9c{udW8ZYuq8lYmb~ z=p*bz$DxIQCZ-@#7DZxE^Hv6gf!a7Q3x+h=ta14HHJ-X>7X|_X%gxMc6|RcXM9~k* z{8V-sd8jBHqrYQV-^61pp6#PvSYhI4wwM*&bA>7W3FZXs-&L7wNUqUX6x%k)wLsN8NZv#h{#K zn)a-SOpIYHiDOiYHHX8`Ye5tdhE@$xb`ZUXaTd0kpqg&si~s8{Ar1|U<#97)pjodJ zhtb@Ff^1fjoV@ZGpqrF!l8snG?E{Y9KEYF8me{V)%T&lD9*0*#P+)|C@roZkjC+3V zyq+6zd@LSKZ4!}($}9loL~sn607iL3BsHf&gi7OwmA3fWAp+4V3osEYps`x9aPZCB zB7knUTVZqMIq5K-dSMXIWVuV&x^V_uu^4=EX}gR}iDi;aCiD^AB4k@@s*KoHiBBfH zALnMIV;R=@u60&Q%i3a?3n^r>Av$#F`=I`QB!r(XfWv?8I?b6|d76?u4^^Hbn_Eh$ z;c@=Q97obHC*8i3Qye71yqZBrreR#z`R9@9^ZfOwymfi3^mnX|LO}^wAwN!%B5 zr49Nx!fm%nh%ua`%s#gcU<)@ngj4ta2KxCq)azT>i7CxF`-ND$4RaxVxyDMXv?@p$ zc_-=sljzN^wiuRj0=L^8nvEKv9fT@gv_jBa!AQWdyVud}#4)cjq4-5GR3boS{`(jR z0J?(Nhq0tqYCyl&jdEbR#==2MifXda@~sC|GFKlKzOXuGxM_u98H$^|*hFHMT<@br zM%?q8yO6#BIhQ`1NG!{v z5AwCWAwT+Zr-z+b7)T#%v~P?7*tovIp+;IEH!+uiMMr0gd@m>6Spi%oCef&JjAJH3 z{%@>o3|W-JMX6n!@af;(Zo0uk;Z>h}15z0$3z$exY>duAoHiBRG;?g05#C2)TaJyAxsVk@m85dNmQv%AyK!w*0K7^gj|#+vA;NcKxg?}BgMLxO zCQ;Ew6oCpdt}lME0p{nU0!Xs+53R&}GZ@v(p`V9wGctsnegy%Wf?9pYR(krnv9OIn zzcVODNihm5R^X!Midmo%Isw2$oFIgB_68T940y?Vj^MFR{}tT%){k9IzlS^+Pr%a^ z3n*SmnB+_^ESzHVW-uCa-O`b;!sxL2DziCxd=gg}qzx?vBpH2Jo!dh2KSU#5Dd>?U-LssXX0gn|-iWTq)M zgQ@hSW)zm*fT;~A0Hy(a$8b{%zZf!lY%7D%N40v=9lbW_=Dq;V^z`QlL?f=!#paPp zI@b0iO(uNnC6$(x45RQZ6N%tOdH$}pLdaptC2d#=si|HQF2PqmG+_PsAK}T*y&SLq z$=^k@apFI*n<@dy=OiyuHiTGK4QazDd-$+&vd7&27t~pdK_BI&BoWy9{(}IbKwZDF z+>V$WVt@xM&Oe585C1N{{^1W|@6=OplL_2(rVJvjjACJ_7S*|^c*g=qI8`ySzIXLZ z#ASeFM%cz?>v@xS!bSO-sKC|4WwmPXQHeV-v7mEX;U+HYL^yEv|4*bbvZAnb6DL%7 zO|Hs2QEE|?sO6k_E`oust{gb`duq2uJdUNJSKWAi{UamISedzUhzLr{Wc@(mkh0|E z&<&nt4rNEF43-IY@A!D+73UqVm;sA!_zV;2{&jybb zZ@Sd)j#b>)(vk&C4fy$J%2@_y!UD0A3XTEHjsVLGISW*N-bre&tv6uEA_l-s4BX72 z+wJnbFmwUN8MxZ0ic}7sW>&*ALecIQy(S_@ls>FHghWxupX+4Pq22aqwrcog(9hZf zQ4W+CWI|=F(clhN&4E(UL(HzmxjIW@SH0i5-9g(b%2^Cq%}EA!QUPs0%CN7=DZmw2 z3o!z&F$kT`lbvYVV;eBpsL<|6wmXxeut~39IN0V9mAVA8s^4DsW1INoFYch5`^@P# z3iL6_P5knAZs3#uX1kPdNkZ?mf>-;odSWrmh0`gI9|dT_m|uWMa)T6O&LUV>{%j4b zoxoHzT;p-#MuYQ@bTAF@?SJDseER=)8rQ#M9k;z{6QBFnCvz6yWD_e9Tx8_6mX5f# zA~drK-C_^`6eFHuDhW89Zj7O&PLqKFN%R>O0e8P`6W{pa0--hNmjjyh)iPAnfU*&b zKVF1&M*Q;a$R$YNRnqYrJ-O=1sJL{~3d3Rm6QP+BUi*JKj8Faic`Ua*UirgY_~NhZ z!q&j_kiqhT8o3BJT)L}TdiHLo%j(8_Pw7FyYM?<${;i~*^j>lp%lT5Enm*@h2Hg4G zTe#-~=hU`5jq|h`92jr-sbl!$&z%L3FdqpgfvQNUxJNSxs0Ujt{=Q7g8k-XF0*tMA zP7KRlos+J?4d9C9tP$jyCBy~nWgKN3q1_835k-C3tC`inP2JrCU>ln_U)-#-aNIc_S{zJFm-j7@?8`YHvYi5-?lYC-^2>ntx z^RDb`(q8G1Wx=H2sH>3|Xz0DzbvZ`cda+|$@wt-|+E@eU#Oy=Tf(WjbgW0dtEC z2V;TBR)h9Ji!$6cdNoQ}c-;N=O?>KK&Ec00);SP0IC>lK^7n1wGyn5hSf>h575zwT zSjdaVR>BG_<=Z*RJn28Qy+kNBWh2t*s3T%Kal%)L(!P-TL(mA1s@ztyXu1W^+kG*ki z|CjzY7`G<*FEe1Laym!Ph@Mfe*{n@RQUdncN7-b&AQglkcoj5Z!l{QnTum7Im5EC( zK^eeuJEGl=u#M51Oyk89i_mbfF{GPGn1EowFv%uVQH}v4lQB7YE&8E}$Kc4%nV$_f zbzg@=w=}r$Xoqf>;cA1>1E(MChwdFRN$C*`=H+c>EvJl)x_i=k8XQV zR9}fX6UxC7Inf2ur$ry(`>3|tl#NP~PMi^?8qhvG zm6cU(@&=X8;W$LNi7jN%4C^S9YFuM*;zbi|+)znCXdje13!MDg5^wma>o7YmCspV} z)}jwxkWrJ4RS{+3LvL;l5vGz8D#i$FNvO|?E)(g7qEuBeG*$k*z6o_BK7VqG0aasj zN^sCwH>(a`J2CMt;=tnYqYpV*ccZ>S z&0*ILiPEqbS*ho%Z0s^wC?+QQ9!LwxsMi~%789&p#mK5kHmoF$vzAgK5rM8g75J*O z0Dt3+v;E-T-vr}!jsotbve;SzS-C0FINd3#8u}=2jGoHX%;u4tSV(E!pdagXjv_2N zaaRMsB=}x#c%$WLy6TD38Nccjk^`%LXbcD_7=g*OLbc{{mY-`Z`ZmI_3Y4^kp}@s_ zUtjkSSHQ+>cIl<^XtyI?@{S|ey4m5(1H$}@K4Nh`;PKDxq22MAY*p}G5G1^^Vhb2K zm$(FmomeH`=t!~;BYvP23K1~dtkEw#!XRVVdgjJyNwIf(znVsy3^k0=YOAPpqLn{-8^9Jx(YYG$Z0yP^pep7FMH=E zLND~u=*K`1Qfxs==rNtuFrnQYu)NTt+w*!_2sq_~2wsR~BZC#g2^4Cu@lzJ@5+dN2 z0e64@A)&7>13vq&PvidI*~uj=DK{Z8Ui)1SV+jqahawABp(FukPW&-(ScKoe8(Uagy69vNU?=>*iu=D^mah{35_jt%8%b zn4@D1lJQarhN3SMUq2Zc8UnhVUc+Ne2Bqs6;nvqrwKbJZ)b~K!^ z)<_C4>3fX?x*}^K2=d;i*s^`OH7oVKu|_v?Qtw5K$nLdl2{x*F#upp9kgcmSqLYBM zLGDsRQX#+sZbERCp6a#`d>C561C0gl`{)i1-)QshoWQxYV|ElOY55NI8tms@rJ)4Y zn-c)s%qm8Z0K+Jnb!XsQ+B9^-AQCIF$|w+649+u3P1v};$_uNSO0Vu3c`h+9j^EiJ z^s>>ANp8X(fAq7b;Tpo)5vTT6TGoRw^oj{Y)>wR{+}BTm4~AfGapnjI4pzaJ(!Hf zuU}VXBysJS#qQ~d-IGB;V-#ssArW^}(PuPeTJJ09Mh1;%2SEgHe0klB(fcK)3M!H2 zRu0l@DH-U}5+EkDJS_uLn=YL~Q zM~yLudhR?uz&b%=*{i~f2(Hs7%+Kplt3C7^-hTsL z@t#er9ksBHl|`&0PTvKIT~DNwC}8YjJHiiGdGExoo8f!TKf^3H{Lxz-Ui)7h#w*`@ zNEd+o-N~;naps{8#)(9jwQqjjYZ|=qr?#@L!&(JeizMC{1GZ20GUBX^*lsIU5qyx( zIBOkvg)y2uBy$s+FcE!4=^jdYfF!C*&o3Ex`qt}XGTLph; z>_nDO$HTd$BjA;;wU#y=mfwb1@vrVq(!*gvu40dWFf~;n2e@xR`j@E>S7Es z+-}yy#a6~{(&s1SwVGM@HpdR-3T?9P(C&JG7|qmyiA0pb;(sWSPjOU%W&JyYYLnX> zX5l_m6DdhuBkna}h~kydd9W8B1K-Pgk;p(XFx;#{x7}-oy$VP?!;nF}Ca=Tyf<1(R zargIZfSGajp%&XGh3_t)nwqOXp1%ODgnkil+v_(l-DK>Y9`NKntv)ued7Z;@htbbN zX6z-Of?kPJ>qi@e7_tT<^id_Xqb6Qn52Kk?SZw#QNg80b)u5gCxlf*LROs6PKLk`0 z*&M4{j7dV^Oz|!03+25r*{reL=~u`(uCk-OmRiN9jU|Rv64ZWSTf!wq&>8yzWOTBj zRTncvI-W5WrqWde(08@1;}zLChOA&3Y1K@fNA463aP1V*WSpSSWU-PWV=Un{@7=)D_jh>Wb92pE0H!l5BgQ0mPL$RQ9uNHf4mPfH!jEV@ zTy5Yc28WLl?tJqah=4DBXct2pkwAyV`3`3u3fMdAb)yl7rQNeK!XgVaTNV7$BlHoI zO^3U`dlS_Z_`(NvSBxyB-JlgUr{=L@xu=G!jX^bY=yv**_q|(+acJTxFcX%0YVKJs zy%IP~kq;zl&61XGY6Dx7e3pP{HZ=WLBMB8)4VkrL z_`f`hTVA_?wZj(69hE!>M!Oi`d(ORex8rkPx^vP4PQGr8@F>pi0;eCmpf~+EemHhn$L8T5UCwMOH@3GL1RS4j}Y`fNPy;lMqEw)`euSFK2EP@QjSw^t(nZ zAbQ53o9of5o(l@ENnhhJ223j)TghU5>Wh2WIpYzApu`g3jqkq>Z~BSDIPrpd9REc| z--+8&&g}G~9)0ts8g3$evS0>05}Ul{1+)SE-T-3Y`LC(*#-BKZmwx|x7PlBfIP=JW zozq?>1qmUyl=Bp_Vs6!wQ>Gwoz}5|7$L?y4)Q)2Ww{N|00F>Z^6q&6(zVIp(pHZ<7 zt@O<42saq!UhW4Yi*aZNjdPPjWwLH@*I!w~SO49v21@TgeK|8O6X%0<3`|H`ekk)$ ztJyxN6VZw`7E3iRN%r9=9M~{Ub5A}O$so^31;CNF6XC`fgL=)V&R!OB@(7U83}tV> zW=&?dafli^o3dSHX+>sW5G{XxEI-m$lSyM3BL26ZyMQ7|UfOauqub$@dcX0yVRpPJ+8dqqhX`+!IPc!7uhaIS1M%A-P~$X4(%d*N{}?_oW) zdIIvw4elleLn}S=oo}Av^S?5v#99SMT?EyXvmT*@pXASw0Y4sf3$8I@&Z0puX#)mf z4Bb==Ci>+0I0l>$G0r`*6tK=3@t0VZp{Tm?Sjy}}E=Xia3QUf`9aJGG_n+0)FKxh7 zaM)susHfG+V@W3Af3Bz0U*rZrt0!UASlg>!K8A>XE(>A0DGMACa5MSfONG>+0Sv(d z^-RlieG{WD?p6Om{h$=dtv4+Cy$H(@Fcx+qGv#Jxg=(zvYe_vALd0}S06z{?BpTCr zLM!mLpY&MIBv7P$K0zg=F#U<%0%7^v4Bq8FQW z=t8{j_x3XR$TpNuBHzoXHS~HzI-PM7<5R!5osBGw6QfV7-igh+16hn4w)VnFG-W3i zLC0&Z60kLu;aTWJ7+G@xIcTKa9kgZAS^BU#`xdg;D2|cTWy+gM>>|Rx`_H)1v3GP= zqZQMce7`w@bs|OP$fNTRMk%il#f0GrpIDe;5;AmyauA{d%1#MV!dH=$B@3@9Sy1|P zJ#nh5NYf++3Ha-t3qydb!UX)$*Uwh&*%N<4e~GSD3T)5$CWZ!zCR5O)D61NaH~{sg zlkn1sl%X;COInrMdL5hTW?&m56O4f|*-S?=Vy4Bt@D-4McddBI82Aq3*K)vMzY^Bk?x!F>~OzqX>wJ2>m z6px{rh-&I|a~pZ^#+q!wRn;Z~db#Y?>*A@1VX2FoXhmC5O{|RZYNKh;lG|9VR|s7| zy;i9XK~7#{CE+;6aa5R6b_j&Etp>e70 zPXYY*LKjib#9nJW^;BV33pKZzm^_wA8FX^m8|9KGbCH7WLF3Mzh(U=0Y-*zsQ;eaP zo9rduwT|hg#f6iCbf+7QNmFp05vE}vm`Yld7)Skkwzg*3Frai|HppZqA%RHADhpHD zV>KZR7|Y^(!;g1H=r1LWuv(O*3L`DB7z7_ky*_CRYA~koQsJX%9KlU(1}{T@0!BCsJjgBOpKxHMEzGV?b=$%%sxdOvw%! z@R73yy`n3zAju_i(hyeOr-TEf!$0Vv4L#$PKfDRoRKi67;H(i3K#bJz{8|$pV^bn? z9My+mR6vqWHukseD;xkMcPv`b{@xrnyr996 zn=8Z!bn}RADNeq@tJk|q2C1;fiG$oY)>;iEAqn$H*i;xxPY!7l(@DU-phq%5wPsaJ zf)&=+>hZ&?lu`Zpq?829I5RsOb9u*>Le?)mN4TleASO#wC7r3RuN)tVWuvXZ6NL ziP!3>PC8j3E74X7@lFCD;RQ$_Oz1hY-4ftP3T{yPJaeMYI0fNIl6V}7K|GHjnyVye zGgXF3S5HcD?iV2=?{~d*4Hq8sIQ2j;RB2`Gl?uF+IiczFviKe4Tt+I|2`ae#!y=Sm z-WZ1-SD$YN8Y5uSgezp8f!(L6y9O};eI>(%?$P>?^g3Q?ooQT@MrmFNJHCymrzZOX zWRzOjZIvVVL0DAA5V*u~$7ad4w|Fl=AzNf$ky&NWj92~jLkJzC-wpbG+A8&8BQB4t%&>L4 zTIU)We~CJhQO(3#A;pNL?)S%j26@k8%l{8FSpz{Sv$Yhmvk}^$h{8S__ap?0C#+^)5 zUUT3`h@hK$VRQ&E zi)gAcqYRnHdn!#rNVNlQGWO|YNVE+DW9tT|El#RB#>oOf@F$@J%h&p)&)j}j31vDJ z=tPWqqZ*x#^KhC0A6Lhe+Fsd0Pr#h22|efHlJdwZ;t2-oF zjO;oEQ;emew@+n}EIexo)yybuhw|@Ac3Png!lTzV!#E+NB;Eh%M|=3aAwQmL3?^#= zh`r!D*Ko&c*T)f3WyYJ9Wu{NbnoI#V;|vf-W02D4EwzWXCe#~FT94Ym8iUghbvmDx znX_>t=ik$2M^Vytx2PogQY-zxgfiOE&N*~c|o0hDda zLZ^9%Dhme@e8(e$Py6_sxu>A0t0Z8&|?3m-hdBZB+pK!3$>m z2Ql#=3~kJ+vl76|2&w*Dvu3mQ-z%%F+bKK%d;|{+H!8i<+FQL^m-Q=21YvVMKE?la49NO4IWDHqTy<#9bIL4#9q3DYsB6{qSf$bpl&CB^rZfT4cv*PkPb{?!=+{^}nu z@x!m3u7ZJ)8vs9oFiL-uWfv97yI9WDO9n7ge)F>&L}$R!%gq=E$|*!Z!WqVqP#R0g zjj1zOb$OwRaO%Mx&D6kmob~u@WB@ zl1;}c-+>FNaT-;`L5WRv?6p(kQu>+w$g$9oWo9n6?!3aTOi|#*8qH!Hzb<*ETntVG z;}}}t!c!j8Epb0KI&0@3FNGyn|RT|vc% zr2t>ELsrviBf7ny@?t^z*AF`kZNT1nukmIwQ(r&s;JXM@TkM@4GSDPC+N^oT5F@6CWn?zABc(PhBkGBi*F=PNSKh0B?%^7P z^%GK6?4A;9G4T~-tRFD|Sr}`vd#0DxYhX+d+dPrnI}@@tATqFaR18|WMbuHDY(kSY zgGnPN^`aX^IWWN55eHivEY61&63ukeWLkM-gMJ?1Y62|uvjLVnBO@?I#>REh68N4G z7o+U2ni|YDr486VCA|G=&7#}(SU)bkdKU$>@?KgKHx;!ZLEh&OMt0w!n`t8a()**tOS$zFt zi)<6!F9Pm-?HU-sQ}_0mtXn+&PzPehk(+BAzOhEXWIXt(xrRoHnaGJdD#TkV+<5mC zpZKM-==KB{JAOxl*_Op}H{#LH?TMhNlMnKFub2T~JpAcB^h+@g0Ss<<(G;#CY@h0J z@&TD-Y@M)J?ggB9K@B%0-1oojO2W}nj)a?EHbLwo&O9<;*E8%yPL9L3*4WsTa`?VK zTI6cOR=_PUn*annb#I4$;W1sem~RIhyS2ul=T%so^LXSlOH4K#x@EwL+Z?Wc**cb| zT0H*c4$GaOfS|z*FKkdx2=j{(Pu$a@TX+x=j@;Vd*liWK4|wogfFD@ObOvsEX#-<` z3r~B@FN$Rv7?^Dmj^0**3~=A?F9jS_=Hc^RHN|yzRQU35?xLH^=GV6Yhi|EH=mv+- zGamgyr)oP3TLI5|`2;aAzVVfa#f6wFisN@wSii2u?kSJmvmUQ}*BUS-?c*z8YB-;JdgN&^k9Zo;g;qXz5>t86ofjg&Vg46AJLd_(*dJVzY8wZ`FFEI0u7|KYNjBoJf*LpSB*@RKJiO9_RoCi32dPOZr!aOC+F?tJS8KKDx(< zEMjU|2;EK)yO_j7FE|CRA_QINL3tDmpqaTGmQs2MtSB-ImZBko_hQqXoNHrgU{bP) zD`jJb;&&@eBfTbLrS}azgDfKqrT@Xm_H?y^n|^#v$-lAPlWL<`v)NuOJ?})Q2_0ko zXpOz|L;n3CXhm27lQoBS9&$!NQUc~KI*tM^s}l~{OiUJm0n*ZtExy0p!TT7VHhl&JH1$R5`osHJgBI`l>9g0?7;x+G&VDun z;$Bd#eFFAS`1#47ryDL4Gf2fMN%0j2UPa(}{BR|(L+!b>0;?LEM~Y;z!s4=(ScUg{ zL4!hM2d{Os7h2BMMyV`&-47nd^IkE7Z^yZAl&lW1<2ocRxS=1l<|UGCjEbKua@>~K zjs;EbCEfp4!oKwNiH$ncB1<;KqFRX30+Abo@@!+WJ-B#+j$xIi(tkS{b=t~|ZcDm> z#VBv9g71hWD0{d@HKlAP7Kc?!ai;+!)znDU5rDduc|DXm+%2Tko~&DW4+`t7L6e-<2oSG{{3cYend?XHK_O&NxA*akT&yy>L^^KC@G2w>$fB)M=hfT%Vq z(V_&2tYf7kSTkCM<=OraQLl}fiWpKz2=YCUOig0s3>I2j+~b)yKn_PPp z004aGr?xJ|F(cj&gP$#e`Qyr=&MaL+mpoi7 zMKC_8q?1QBi3Alc=IU+lb;q}r!E)Q5|eACb(CKNIeEMkzJkDT>jqv4GEq-ZtALz!$S%PvwPD1n_1TKGds*g} z>^fv0zEs^wnNImcC*>3*QBt#RCEBeY`{n3a=jDay3PqZ(R*xAZv@*-L4XtWSF4x-> zgvBiF)meTTQyTrfpfQhv3%>X2SVffTqI9T$VmhJNyo|oCghjAwUH~SJ`=2^p z)eYde@dPXhFd}*UNKAweFWhZYk;Gba%9uq#@)&*K>kToPl)n?TpIftXUz&7!NVo@= zz54*X>4y)Cu|-yuF}htZAe$Bbf!LBMSk_4!;JfmqmBr12R2|H~+F^?$H&%HebL1T0 z$*OTk*#sOIZ~BR2*}P-ul(DEZ(zI~K)L&VgA&jpj$%&1lOfm^U7Yrkh=1fglbkpw% zi!iY05nlG!*0FxX!dTr9R57@M9VEIlC!^L?ojVx;`C<_kl9Gc%ujH7Z^lw{a!|1dp z48ZGta6`)SWz0@b+0Y~X8-NeM%`a+3qE(un%uHTzt{RP9xJW4C0vB;J0fX(NFmNQs zbi?2kKd_NsOY)gaZA{vbC54xD4^VhTf1+kPiA-;vZ>zT1M3M^G6%`C;HbAAqqv%#v$GnE@ z@MI`DL~Nh(84XWtK54Zyw#mpj^)#tC# zXz*E2N&OyH-IS|_&?}43SlK}Qz-VfNuD>)vDut97tsFCnORF&|U7d+EK3y~Y6pL_; zY@pp-cEW1S0x*GyK`kCe$5vl#G(sADlxNz@lv1?rNsta`_lGG5>DYKRXBhCrItS5FAu%kNFD zzgbk2-HJ!?3}nDsP4Yb~d$Tky1xC#}jSezVS#G8_K#HzUB%8vwPflkV{lpX7jzlUgKqu!# zl2CNf0x0v*8J$zpN-LLn8fNq=7XXtB7o!ogC~pGRMQ5#)&WU*R`XArI4YyTz`u<*Y z7@7f*H=sBw?5aG&=S-l=Nj+LELWM`hCSFI(KyBWL5=*eKrHfwt6;cRlD!bxPSc7Li z0#tTBeuk-1b54m`u5fJ9jZ3CL^p9f1?_F{-)74-6&q)OU0Kfiswz9)b_EAgZiXa1T zTbvN-P_Ng-k_s$0YXZFLnMDX9&y^C(qFcmlY+)RU9alcFDN}51Q4%-n(og4#AXN=o zo~6yCBy5DDKbNu+=of-P2wTnMaLuSu)zfvc3^$sGm==ZA(v>;pyg+18RO-1eflx*( zHIm-Eo*DFu0PBqC5Gr%2_MFCuE@MZEuA2yuN@|NWR%o&b58bK}Xn9qQEdJm*qxG)Y zH{H>%C9XxD{1MQrI7=9If`Qa)2FvY;W+n!m1|u*!ssrWOr1vv#c1}LOYNC>0RY=N| zLn7&nQmeTi$tIL?9+-5B5VI;X6VEjK)GuScPjcR096`y_S*ltyh#jvaUP;TKMl4bh z5?wOtmg&+v{^8Z%0G^X6zz_Z0cH-o%2nFRN#PtS1wQ0-hUy@;zJykU(_mI{QhBgS@ zSKq2s&aqd-+>Z_~>CGKl#%(W|h>aKvdv9ntC(0NzQaDLV(L81g^1FR6i??bBh8P?o z`h}F1sl<$)F3>lZvCJDXFuNbUh^8cFb;LixUpu5v}fG?mKk35SEm_l10;OZIu_?;oeHEDB8IqP zkrjfx=XGr|F)YQ6<(vgCzuQzK5ot=vB~3Dh>L^@AxbfR6AzCT6!TChjiH6sNUxU#b zAza$hq@y(`iEX;&;AUp!ba%CZZ^NxC@%A)g3eUyje{RQsMd_Dr7Gl8GI0eSf_1j_9 z-=Fl&jfB`saHp}PDLa%&BDJF8h{&lm9D&Ipnb{G4hFed}%1JvcBj)ElOeHtIxYGofN@o7S%g8AV5pVwSLok&=wdu64 zRL{CN;sR;5j1nh++GT4Q8Ff*~v|$9U2oYY(-+njbML8@Y4j;E-(cX`n-Y}2oF8T~; zN!Fj@^8=&b3nl86{r=gEU(N;|din>?Se*C5I3#IjHjY`m;fL1o%!3`KC!9DIr#4Qp z9}dMPj+7uhcC4G3T>qf3|8>oq;#?90aG0gwf3xt-{=?=p4Ea( zRec7StQ!~yxC)t9kXU)6d5Df1S>9I!5j+{EtxZkfIHoAyaA$>TDkc4N(_*N)TH^sn26DrK%yyKWZWgBGf8_HBPwuP`Rk@BD$TR2B!t~Kt;6!W$YQ?6Tfm1 zlXa`26loKxsX@PF-12P={>l$+Vlq{LtBuNKkT$_Oqhu59Z!4K*aCW+on=|1Y2^UEu z9uk`Z-215|b}z)N?n@QTVmFL_h>4z!(ZJOdn*w`j6QXAtClSnpSukzAHQ=bU#z$g*%Uz~Zzw`;3P2 z!A8$Nd;KyNqJUH)jY&eO&$6{J^U^#zE6%ip$o;4h zoj|u6vIA^VZTX&MhmRvb$;Bp=W5kp(mjJV>A@$SeoSu_KKT%YLejz6l*GEnBbh-ON-^))j%j=1-aTX|k{u{`IflxkfV8Ft^7R+N;S zqmiCGZoMwQi>fh4F{LO2`o8s|!H?>FAmhQ9ISuv9NJSP`H52l1sru0X5QvLVObJwm z;AReC=|Qz<29i|0Ubp($3zHJMVK(c+L+O_xFZ!$r0o_88pWL6Fkv7$L{iE&YbUoVV zd;|D=hxdJZvzl<3AVQ*D4)g^M{9~DTL#z7~M>&B@cULYB4>Gg@Kv5w^h$<)%#^Gqx zkXpTJBK-YkO9UXPUz;438M_2ms+}{B1R`++9z#wNAXIwTl|F%SpO!~>1afmrJ7J2y zlf`rLkpY{>EDqgR;p}6*p!PjuZPTD`43>LJRV@Ij=oz+==Qgwv({-sTtfLINLF2BC zA?0Hv>TweJYq_snjiWbJQjI9+$r1Rr?_9&qS&xUm(4v|;_+A4-t%8?2iK(;-nXbzi zH;IclFeY0f%f>zmET@rYk*l}TN~TIM%9Ma1ROGy=B<+K~>8>&=(*>sMRsm!}L~-9J zP+CoF21K(*U`#|b0g;pOX>y8AD#|cK%{M4$y#aq6z-OtDd1fKiU%=x33)uhw;C-)d z_MPnGW?(EHWRI>|H{UJHJShW3KtY&pi4AzBzZTmAN7Br^)#@T4<8qy&nOn$avLv&9He`K-6w7rWz@rax#^^ zTBaISjW8KE-&N!07t}a>y~Ww5ye!_x*gS6Wx_7UGB5?6+fbT|a#OcR+?3_-PR=hGJ zCj(3rY)p&LcVp(GtHttLtiUy2Fczs_DWpk-OVbUTN0X&C0y+(nv7K@;6f9=H&(lGx zw=^nElJaVzm4hW%XR|qI5`-BVv5=T_Dyk6xLx^LoY*_W0MKv>7M?#GMCk*~2R_9^b zlF5YVSO2*CKgI^|fiHA;->WB=obGu?wDgIgjn{$cHDab9h%-T0Z$h!8eTgZ0`XvL4 zG|N_t>CiLY^dpDx*jL-^9+N)(IB0)aQ2RiZBLA<^L#5D zb>9k-?t8|Ye`FI6eRe6mqbljDN;LjX%}QhxsidPpDuZ@;LL9FGxIuZq9i4 zOC3%>;qytE%G-N%^nkw$Ec7_0V6R*nJi`oFP(XO#3)HTrq=W9yMgHz9?8 z@(RY2t;UNC#cHBXQ|f%g7#)+6;Y*3Oa{wl5gqMBq3{UDNnutn9r%lx)={vM)vhq1s zYeuQc(&u?EAfylv(&ioW$ z{@Z#g6wsmL?51PvY7Sn~q`M8R#jgMW4{u3CK~#yV`5G$18pqLN*og=p@ScmvZ0KZw zP_OBD5y?45duUY$8dKO4Xb1<908Q-$4>F8u0{Bj%$m*$j%%^Po^qjT}X`hz3eH#&m zfT0UY4oY5l@~!NnW!MVTNZ02Z9F3}Xd!6%v-s z919=DJiUGq;U<)cQdJ{>8!3uzY$E+6l2<-qb<8NluAz;nCeltBn<&pC=?mq^S3lG4 zqPP*&Q{ALE;{@-2vi&}<% zF%>{b6MpY&Cae1ZjjT26=;K6mPYn1G39B@@#9A?`0A&DBO4`w@KF!nJL@c!~p7r^| zNfu$o5aOVMA{EdO0rf;uUVO`AuU3XQ6v#3ag5tzs4XHoA7nXgdZ%9J%w5MUm8z z%#3EkWXvPmizR7sVVW`~4pXK&lU6R14ZGrYlgduN6svTy$&MV^XF+E1E%6vyN=HtWxAr5tt(1LmB%X80~hzVml=g31Aq5j9I;A49XhS|4+UEy1HU< zI`2pLlWiOar3)MNN2E&yQpNzm6JnrD;SGhBBnhJG#^ify=y@gnd}U!Z1Vi*rFn(bL z*C;z*2Hy2Q?)*hA!RLMoaCxfmp_NZAM^Ndr8+PJtcFjDJ9fS6H{-G!bGrFsugT^Yt z4@R|a(C)-6KFR2;o>s5~Qf)mem6Nn2=~|5)r?EenZV7EQ-$?qnPA3$;<3ezftg&<~ zrKYAUMit8GPd#*+64;tBEcghS8bP8E3pYDj+Hn45%nU zMFcZhQ3ON*$vNj>AV&!z=A0wu{r#SIpVPGS&3D(j|90+ozyIo{`U-e@4|2YzV(5%?{6MAEomxyjfz8*-xu#J@Bru3?J5Yw=r?5(k_<@#ANH?(x(cMH}YDS6}tk;DKqK-@Nek zF;`!`wrI}DQ9U1elK*P)MhY5rr;yU!hX=(dFZJH9>g z%*m4`KhUb8S-a1!yQ=G|;O)13{7kncv)^f+f6b&JcOTf>b@1@Z1{@q(b$w;|j4nmb z1}{1G&X+&GQSkH9*{A({c|pO#@i&g1wP5tLM~{piHmA?thZCo7c=oX_`|sPZe)zW| zE)4$pr?!D*7wx{ocmMQW-z1#+R^G#3hNk^-QEu|rU0=Owe&@FfF57TdVf9_Nx}zra z9)8O;Yc6gv_}DK;Hn+X?)wSQ9@Q?lzPUsvHbZ`3l!Cq+>&(2TpKlkd=xnCch+^tEQ zE1S){(Ajxx&mYQG#^`%_FN{fgzHK>AEPP`8w(qisot?kv zj3#aVyzc6f3-fMWvEsXh1IK=|>FvMHe{A_rTaNzuZ}a;;K7ah??uWYW51h6vw*7(f ztc&w5ThZ75th;o}o9@`du;ad8HsAYk`3vKUFM7NCcc*nf(kmsf^YVKti_UoTv>O+U zI{%{GRj1wd#r4-bp8r&reFG0%Gx)imc1<6-w))}M$358VjtAfDQ2Cd#Cpvy~^OWJI zrKFscd1T1GiBEko^1#bi7f*Tbk87uuHCuh;!l_B;4haS~&RKDI$cg9ux#q+%i$3^r z_MTREJl!bnkyXQ9x#F^`mvrh;et&k<{3YjnxV-&!M;lK$zfY%w<(6Bgs>*P6u zryO{t?T&Mw=ojqt<*qH$=B&J<$vaJMAJg*W6MO#Ywga0-JUIHuoufW_1bf?VR-K`5)9=8Q*19*7&2>U)&=(Gv}K-ro35o{#nzV6QW=J$5}@< zy*2fs*^O`QIHU8$(NSBz8aHBB`yowkzaaO|H|CbD`mW{C@7v!nXl5yY1=7^ve;9Se zlYvnew&^wH&JXT7$#I+Bk(c*V-bt;lD~=tqr^%~DuMJp}{EunldtY&6^cg3Vc277o z^~)o}Tkf3rYU_7m?*AY&ds??CS7!C!GIUeh##0)$9phf~_|49|A0LeAx!@01oX{)x z`aWHb+&a7an_b*BMLpgaaK~Q~|1{yes$*YW@XGDmx=+90+TH8kU-tHOp-Zmna@#pw zi%;z_vGeWSp8YiC$`w;bynXnoi$0w4&8`bRZh7XqpB_KM=`wcv;bXHdZFu5$e=FYi zw>N4Y`>Eyrt~o=_KQN)?_t!P>t>`=IyoM7yv}%}l-|SNdty-2dV$=tJY%=(oq8V); zO1)z0&DZ=#MN;SIA8g+!?c@#a4ekxg+y8Wrv+AQkhrhq%vg=-Jo$>lRa|XG`9vOUJ z<*1(z&B=&;Bfb0UsiP7n|2<*TqlNtOS%a-znok-syz2$aZai`5I|uX5eym})H%@KT z`N)Q`?jHl!O^6$}^Og6P6->{)?ZW}(^JgA?p-IWPFI{)lu9;^&e(Hq<$JQU}-g^00 z1%pm(@P4;#FAh0(_s7q--TLbK({Fn5>6O7|!(Z(B*@kC^Pdf4Ei9PxE5%)ypyn4^< zQ@>fUzT%p7!xK9vU7xph%qIU|6F<(mcJSmeqn{aY&&1-_j%>bT#DlE|1UgUt>-eG% zjy>F|eCD4<3}}A1bHcRu+g)Eh&z)M_<;)9?e)R|bZur2#rC&FEbWZeY_r-}n+%X~h z=63E$C%RuZ+O{U^tg)94+OuQYhyz{!Bd>8~{F-~tIJf4L>QP^8y7^C2qjJ~ZcYn+0 zy1cVt_euRu9Fp*OkN5XAF8gM~%P-z}r1cleKO5&8@v49DrNjT@(lI;!);YLu;1kUX z_7|2OYQ64Q@Q(NQJlv#p#u7tfP(YdHA(OIe{lTopJy4c0Ub?x#NqRf{A}0A3x@kOB-e1wWfSzY(|I4`<_~U zc-_pdEgK%%Hge1z-~M&h=0R1h8a?zx)h~0qem8M&`QBGPdu`a)pAC9_;>QEG&Mm#E zls`@Qu6&34+>)|}6~n4NP4~rqpLfCI->*I4^UptQ*`vfgck;sr1`nJ6m(mL+6b$SC z!yo_lQ|?92TpoI}{Jr?nV_kY*d~Vg08~1eYa@MBcz9uhOD?~%`=8&u zsPgVccOAR()T+&AzkE2i)4VI@9R2ip|Cb!R{&8i;lbbZY^76YoJ^Sq7k$vB}<)$GW zAG^Bw`p${H@Vf6=k=7HDb->9eR#9ba(DIf1fe-#HEF$TNhnb(dny3 z59}`*I_ts(b5;!++UXzr3fG_vAaH)SkyPiS@Dx|TOi*t}zN%P#lda%J?!9`7&TbNMrmuRecx z>nE??eO0h}(c6QcsER)Ok@CNMwlI$WCGMGrpNVhz$MZLBUv^+oTG7byPp7|dcdMRL zN@joa^+Qix^oM&3_g=sM(U-bhaNghV8NYn$(%x^)TYqPH;;IKfIW=(F+~r#iUvu7L zqwe{3#B;MkNp92c-Z^_x)_|sqr?#k^J@oOHKYI7)PcHvy(&CmY2kpJ=v=1+Ihb`!E zwma}Z`=hI;zPz{j(i5KfsQQ;rNBIBP_=Sly`I~QR z_Rv!YPB{Adu;>0hrTNC`L%ZGlyl?Z1OD(frJnMuGmz~8QDb9Mhs@wP5 zie?Vj+NS$=U#FKLsQRK8F*vg=+VPo zPg(!+x*N;ieQo{tC;oB$?K5s&yWsh_=%|CO(nl{TnNl|W?=k)dO0R4%u6Victd$d| zJ^p?0lWs{#-4+ZTefx{UK77dE_VXWZo%_a3YeRoIzr#7XtB!16-n;9_ikPda#&$?~ zddnj(y|Aul$yrgSIwzib*Ifw@uI_);k~3eQu%hUuZSERpO1aHVtamc-JKhT6{n9uV3yTHSp_S-t$j%XLRZ{pyg%n?puAN;XJqe z*tO1_n;XCV-jfgg>9%)npLW@qqYHi+)52MO^s7*t17H8p_rMPgKR-G#`HQ=v$NnSp zv$egy>$3UBg?D~&=+URP{?zF5+}Z!}=76a)Hg9@=<=gYi&n#Ti^_17o{c*?E5mU>$ z{psOm2jd^zGW3$IANBrr$>;uun-ncN>9vms9I5`IV(jUK9lzfB*3g5;Qoir9^T#ef z4Zr4<@4miu)q4{^dA)q=JD-nxzklx&5(d7W|45^kp6z*3mx>Q}F8I#fR-Jam@?}q- zd+B}Og+6U}=i?_mI(Jdr$FIdV${)XV^{KyfZ+z_17AJgO{r#RbSC46w`_78jCu;+^^--~|1bp&UOhe*gWx0>7`o z?6m!0#*YZ(4zv?lT;dYe-FIIx#(t zOEmfD9U!avM|TKsQ@L-3$p_=9~ zr}N-A-gU8$GfZA8qr{|n5+x*gH+f0cUJ z&w~Rx?{Q!jzJHd}^ztc9V|&l@Mg3!1O7wNJ@}sYt zQySHEW;xhaMqTw>75+aIb@lX6)Kv(1unsl7a(bxg<7m6^gW9< zb9g@Ox_kMk9<-FjITx&yp^tIu3J?79Sc?8~6HtmUAKc~FuHM2^Z_Lx-=-D^H` zo)gpbif5Wc^@SgOJe{x0gJ0nu!9Sd;{r`lY;SJ|~5&4zf5A&f8FJik-Zx-8a#;BO? zGruDKb7QZY?J7mTJ?T!`Z3gn38xY&a?f0b5JU5Cm{Jsm8q_amw^`6~4g8#q7zAwJ2FY)S)$W04O z{%>+1_B#B!uod>w|Nnyxi2CEi2Gp(afdg@X8wf(s zE4+lQa3+ua@cI-7R4xHq!(9B32cfdZ(4c#-xZF<9;WsSSfYEou?}WytsDOi*UrH-u1E0$_F3b;<68Li3Q=n zEaO59Ya9bkF~VJCv{RYkCJYT{QejKpKXME9L0sTk(u4LrSzBRA9(!zJoZ?#bCfA7r zUVYpn^da2fo~6o*6H0~s4d6$632~y8%RbZ!ghgq!0?xz3up19uZOwmJ@^|J zA{@9W+#a6AKemnM%@*!1n7U2H8>PMHZI8ZXfnnb?qW6Zoun*%eTR{H5^M@^Vgg^QT z2WER55bne}o(&-FF}n}G0euf+FZ{WVV~+7REx`uNF#LV!QY=`Ru%2+rg;Qa>01v>M zSi$Ue47ivL=Q{SL#UAdeS2<;7N64FR5Z>3Sz0x{D-(DYZuis)frVnjcd-mO)NFT8? zF{bZuNH#}$-8-w~&*m$!Ilv^si#S7XiRxta3PcUxC zj&P5ai9cFT`4#Ss5iUxFt#G7|7~yz5_TpM>M~~;+81S}sY8$~;_Xs~y%z|hH)|_9uYM%r4HA3nA~!~HLf8jbKj?qNk5~*qyC`wa!`sE)yWnau zf;bR$^TH$l8wVoTVr`daSEsiyhXD*UB zVa#>n@J*yM-8j}fo;4LW;?x!$iMv7kK*SUC55x@A(Z9zHu#E+K|Gn$+58?tsKjMJe zSZdc`NS)$^81Oc^j=b;=V;?_^K7=XQT3_OT?iI%L9g9B4$e!yS=45%=3)@)mi>1u} z;0EJZES!i%XJgUb80KRl%<0Q;*K??k0aMup`2;Jcuee8_cVpm4lzf6>1Ji%7ziFWx zbNf;^syF500OirQE~<_FOz&&4U+Bc#y71rX2Bi1cPESntXQTg!Vt^axVFOryVaNRA z(T6xvVJB?kv4e&``QVT>E~GM~y>bEWZMV0J;iM17&ZKJZEyx`q^t z;7crRO~egwGaCX&Vs-zZRc`#im4Lc`!wYf2xWyV6&f-x7bJZy?8v`$V z53I-6ZSkUKs@W~XC17JdU3<-2Ia;VuGOFU z0`%I4KKsyZANcub6VdVD6WiVR0ZXw5aEY}O4@|<`fUBmno^3$q>DTmLeaIH*dEBqm z2WJA~w|aQ1o%EP7nk@)l8z-29Y(y;jtvcg_uF(Tuf8w26&}HGyc%=VI)t3F9#{=Ol zZ2N&dby0my*TH&`+f-Z;_VmHNj}(#5H~cxn>=(xV--y3qFa7@)Hh>uYRpG99T^vyS zMm!MKS8x;uOoTo5|Gu?gFZ>baZ$iD_creFJxOqN&2-m|AKXy^}K)xcvgM`5=4SOqR zJ(I@o^lSrM5Y|30l1+%CFLA@iv&=V0-)_QI^St;$^dXGIQ`Xc69l+JsGW~% zA7k*{w%ARavfF&WPd=Sz#oe{ayGQ*C>suFEU$I~*KcX_@0@sQGYWWel2Gh9VYpqV4 zh`F7(R=kkziwj^c{N)>(%0H-#u~PSnaQ|oQrT_Hto7;e2;UD3F(rae+l>S)!E!`LH zaM{OROu8D+x=8PZonN?uOX_1Ux_&T9pe}*ASqcUT68uDhu&14g^0Lp&U3`frACJvX z9<{|yX5VD}3upR^LwD_(HikL(nr%?3agfGqz2S>|L}0`VjML*robaWM;Z8o5=ld9& z%ES>9aYy&+nl{0SJKe+yTd@;xpE~1>?q!~`|Kd~}Sc+FZcwqG^7e~M`Cd>{0m=`_# zsk8GW`iQ0v?77(riwh`=q6|Cl;ZL7n-TS`-fArha`?|Q3kK(1RLA|M>%v^3w!#Yi zkCT0f*a6DTzKCbcL)=MvW_Q?z(1-BWyIZz|up2%&C~jE1D85iG&csNB zJ!8VQ8wZRB(s{Vz*?sc#5AMhU?G0=Fenj`B|FO6Kc5wh_X~*%u9-TL_cV$~`k0jgA zI(U8*9zC0t2ehhc@12^ni<+?{)An${3ajdO)VVn?_ zs-s->k-AuM?M{5V(lFK@?$USqVZ6fLr+euyj`_ti_c+EAM=D>SeMH=#WwxjEsV!rQ zC5XsoDq02il$* z{g0g4i3esUr5n;6Bp#02UJpG>dvb^Azv(?|97I0@w}3zE8$g0!9}owy9YN-r`RpFg z9#CiYfVzxnJKY4<#?Lw@pj&>VcAv@f{HkNUjR*7>r{@a;VJW=mBN_W0N1t(F%nf(2 zGJ7GMxi4OQC{MYryMr?+Qi(VaLw~aQ@&(!>g0+XcxF83q-Pf2|vMD(06Tm(LHt=^%5IuABa|hKXDU z(VOJEm$}LJEO&#fy~O+jYY<>P5?CAU7yPVm0`vECpW)4##H)@r@&)n2MEXUU{BRt0 zSN`8`{(y3EQ#P44#tk?aht3(+^kI2nZrDn9g(H276UGJVluCrJVhekgYz6k~KK7pM zBV>PM|E*5#xQ@qu=^mxFMaS@p?|N;<3cR)XzU%--M)B5 z1b3yelK#J442W%aOyB`~A_P7}aF^aQS35fs4~XB)7a0D;Q;MynZ_+=BIKaBgmn)_V z-nPK-PP`o+-^#V@JUW?9j2M(nFri#Fg0)x7;HMuG^#c|O*h@d0G5Zf!#D@s>e)LKB zL@{Z8os+aE%G7t0p z;#j0kxXUguHp5?>5D#P|rlX^n)MVAF&7MzWjkfIjJ^eh zy|@6s(;nSmF`%v!nYTp#KyBpz1FX4tkoGiv@GRp2>k+T@7536k;SDEDFDWU!TY&!@veL~Y&R0GOJ;-`?mzzi` z8;~)1yPJ47eqr!Z_K`2R$q#Qp-)Z;gMmOb&Ents5!R{tc-eI`QA0$7s)AP4#$9^q= z@d@Xo30vI2BO49(1oq&GPi{4=66il=#%@n<;Y=d;C)~H%+6A9@$xV2O^FhX@G;tDp zP_Xmgy~0g`3*u2cV@-Z;x7l*FOQKEU=uN!$T<<21-RuTOZM5eH9$x1rJ+;O44_?Mu z#8NkT#vZdD;z8o{-L8MwYJ39r?a3`}9BpL>{9`t|e%4KXA#V5@H{t2+a6vp+Gzwec z)D_OKejEJRW3acYD~CNBuXknM-cRpDV>sipYn7qyVZUS!WE+idn(_Ca>q?q^=i!D2?9BO9W< zy+m!SKRsX9;V~q@c|ROczxE9J)7XT2oITHbPTZYKy?9W%PahFo_c6X$ohhn}XPDfv zH7# zfBO>hOWXv-Tv}Ufx1amXo@@Q-TexVg{q&zS34NEo+S=1jyr4XewKZQXEOdYP+DASf zOstMF;TOc77}mm*ajK1<{hav$l`%fWbS4(_FLsk(*{^84|+pX>O$ zmb)I-_yfun3kY`~Hr4XF)-!e1O}%fx633IJq)VN+fxm(N-vs`=+c63AK>pLtyz%As zzC*DddMz&4`z`ca=NPqTq0)WyNqa)YWr2H%i|IqQ!1S0HPxwb{y!1V3;4(Pi>AtQl z&$Y?}tb5Xf>)jw&hzkMc9%RiDuoZsR#}978SJ;aervKoqyx~oq;@ZW_mP@Vdhh;O9AA8b9GmA{kc;5Sqz{K4Awfwx4sN2I`U=AJ!C}Ykk zbBT9`uW}Pq&wau_!kGltCmufPJ(N#lpe}BputtQno)^KNYs3Gp zOoy3UB6^oN5S*EJBJ)n-I*IkswepFBmZS5F-PEzq;}^IO@$O$rivFOl@#Y6uPwM1% z#RtPy_WTxf6p1IV7*YD}ligP;JHS02_M~8JHbJ(4dOvZi*?-!YJ%(=yW1crZ>=PgE zR2(PFXwR5sr-hy24_3lR`1;{-99%G40mp)mKW{uT?8ODiyZ zK2vSwH&W*x@M1vbu5-HtVMtya2%rZ+*3x(&yjj0Q`2wZl0P7nBi*#c5Ab1DRJ>w2* zAK=(aXlW z1mUf{0QCv%fedr;iGJem4quB$wDXA%!km4Fcwza0%QE8b`gh>J5E~Gdo94H&7+2>d z>}~8_mux-0-Fvqg#$KGb5q_X+F~Xm96!r;x>yV(e;2!y7?VS=x4M+6f@TWeAm<4NSP;KFFcUsQJdY=C)uaLlOXGtAU)Ny;9!8ghV2Vj(`5FQ;(_AF z1om3;#|grR>qtDGI2j+szD+*cxWMyFZ^Je~SjEz};xEG?tmozfG*2WxQfAj+d+$oK z2?;~M4m%Lw*?|$n0N8hn0cbDIXipihy)yAq95yi?pWqiSh!Onc%_peuhzwd%7Jv70 zr>@jn|9Qs@-gTv|bjIR2bjsdg$Q}q^Y=UtB8z3KGXPDIKe9-JadBqNX&7bzd-*g{r zmCBX}!9{5ja}Kh$NvuH-ypvf6>Av{_bSe!Wl8DYJ9#CA6eE(WE9jvSm+S|D>wk37k z^PCI3!7k}2t`IYor+)is}eiXYYKMSe;4#iIXm9%;%5^qtahmXA9Tdw2SU1J;iOe&Rc_y-!N!Y$* z_#oe}*g(3U{TydPv^T%Nx~WZ2wwt=t$DX&dA?+IjTw8oV8l-Re2=g;MFO_pQ;~>ws z{Q@}4HYrB8YuY3|4X5Cm;zPxc=8wf6VN04cZI{`2>8I%^nCd$-KjVqN3tg4o3TNh` z*ueA|+*BVx4-%N4&YBd1rV|g!7N}hUTuNZhCio>iSNsy*!d>>kgmqOLs~fhu65Vp@ zO1<&FZLx!mtQGL85oU^`z5Oeg6Uzy2alp8M-soMXom&#)DONNeh(6l;aIlZ(Y*RMY zPaLeZ4zQj9Va^$T&?$?K+IPbf0VSKzL~nEc>1yo^Y>hfzBO+oGGf@1Z?B+5gy)N>c5vd*-zD} zo%*GX&L;!+udebBUFFo3`r;4P&adRQMTWm0zFMq|Es(y*{tJJto9+9Bwe|^$^X2yp zdtGA-Xm8k~pR)V11D+p1H)$&ykjQ#?Hh{b^MTCEX@DPR%V!L^^IH>cUME1}L+^6Sj zZ*3{})Y?_bwa)SQ8pU$*;j&4Jhuz~4A=@5&JN!Qs7Qt<)R;{!1ME<#D-vNsVVKg(2(A+_i`D_Q|YIf7U{_Kzs=M z&*T~Vu=_e^m5o^7rf@c{GZ*<`Pv6jgbVK+Bq?>R+ahpW?o5Vb&zp-$x0kLF*$6s(y zn(&hA5M!QMt@3K6YJA9LAb=a>{X6@m5^WBE{p$6j_4|8q=uVNUd#{+BtdD#Jr<-pl= zR(g)kdiEdt&UNO}1L!{G#)%bf#_~hpFKk!SR<<1>m5%G3vLDQqH=hqDtex84zcxg< zLo8b>^~E3Ea?rV23Ea8V@E7*-S&E+(BMC?Cm*o=;cWk@GPS|%l_trI{cuP8I_k?v{ zHaLhaPC!3{tb?!)f_b8FhXdk<^iOMFGjgLlYW=J3Bb#4yA6RwRy?^y#_knds+~I3p zafh#c#T~X9S@(*2@9IPDkd+8+?p|@oy^C}Z>pFd z@7|2uxL}`qBWa)c``q61_qsRWk8hZ_&%K`XI_j>Oy~pi7Yq#4Cf87<_ubQ^Qy_##K z9mZ{T9oFGQY*H-a_AzGd*@Th4C081}e~sA##s7Zng5rP0dcrS&&P#&eDiQ9+kzu^! zk$s?zbX>YF4g}~+_sJ$GCcyT)$&YRfVN0CA@HMqkU;OczPDH@RAug|xy%kpS0p=&M zp^A?!CXg+_CfhkHwpQm|YHx9t>@s}tVs6fR!QAXUw$I)J!XfFuun)ii)r$iGIMEQk zU9|O0_qY80{PQpF_dovZe*42u?pNRc=zjL?5AMg`{NR4@)%WhZUw-Gl`S~&TwNJiv zU;X$S_wYwwy9Yk}%H8+=m+qeTzI1oJ`-Qvx&ClGeAAIF*`Q#h-r8hoxSAY7An*tUM zSc6#Uz4RFzbw*=cU@o@z1H*uH7(7ihNBIVDR%{?llXxF$u^;$rpOV14KEHI|^dHP= z8xS|7_rhQHVCc$FUF_jVec^Y&<5z+$s1f!F0~9;SzlOPh?Up?-PQV4l^Qp7;8W)7U z3HqLJ2k&lX?lv9-6mM~#@b+Rbd_U`!h^|=5`YBeEZ*fKK!A_gZ=M+;qHM0J884&=qK)q zLm#@!4}Rz_-1olujmE5taQAGsumL;fX?|aNj0C_^Hb0Z|AHA~{j)v#GtKCHQs;RU0 z!XMp>s9yXqoQ1iEyI~(LPkwZLC~*H;2fldn;M#iQpGZ6rl-!F9S?MJ7UsNOAR2*hv zTo4C@J-RRb#}4XyDt&*Kpx6$bm!2mm)&~Eihu|}5fHQL0`UK*2aY6BafcN@_Jvtx2 z9*7%h&+T+iCT=``(f(S@la><B@l%>zuWxB63n=JNP(e&%Ui9RpB{wogrmFGBJXSgR~BYljaG5m|> znTg!rgmE=dEID(R8^fALiDNu7mc2q_#^&RkAdc8gZK5CH9Ov0>Zeuta#ahSX*Biq> z#dOLyW$hbb-{POy?#6JY)R-|hWu8qrH*$z;OSxd~4z)joIuZjon;-{D~!!hI!b_7O=0b zF};Mhipzp<-~55%0sMjG(fN#doMoWj0qHilTTIV7CxSuR#Fy;-U0@L9+@A!$weL@c z(}}E+*#X+YJ5Sg6HpkDigssE32R-P!?oZc>-aW*>sdPDO0w&jhMT1c?r5d zYtOsxpE)Ny8SeSDmuFr9*=n9){+)h29hd#~Y(X0Fqt1fVhwztP2e3J+&(hvN&y|fg zonK2mqL@I}>^~J7Xg?enzBXjKFAj|Kq`m}TZ5&TlI{0NL`My;(7Ncuk_%}&VB72Qa zSQ@tV!aR}hc#~w?SS#hVr%!wG1@`yU-Q(H!B+Z+8t$8AOy&IN~$(Xmt`p$;q79&vJ zlyZkNn< zd3Lm})tLs@+`kM^L7J~nCyWe&H#@^&){9`I( zG9Rydgb{6&DsD^U+*o}`(%8ewk4Td6N!BjH0Wj9{>^|u^oJvC1WiPB;xT_!T)BaEP zpYI83<+s7#!(RBKFEv5153;whRQ^nQ!@B9rUN#{~yp&#taaSt6(;1yoT_=J~BI_!h zi-_p}edv2I+Yf*>F^9$fI^VZ_5o_5PeRDWhyp}y$-CsBMX(wQ78iR4uTI^Q3QH(7H zy*3}f*yC7Fl|?g_DB23wD8>=Rv!dxgB29%oW6^bt%4z4|hZh>GZxc#VGKWl#6~&;Ovd)h_XTOE_XEj@ ze66k%`R2sR#RbaMUUf+$H-_*7PF<-l{@@VSe=o&G*Od|A^JbX2jDkF|`##>DUp zp9J6SaHe$K!hLlq@c%jg?t-;=3Q+kBI%>V-KkP-%;ZJ!|eYKe*Cq&pZ(v` zS3Yucz@{-{O~59kOx$X`NddPR``>dX?|#Sq13o@V?~~w%d~^!FU-sH?2P^5T*>LKU z(c5%O+$Zdlc{icdc<}Is5UEpF>W}|wCu!($ zNgTSmCW-gxiQ1!piE#makl(gz>_Rg3O}0RBathdr1IgIbWbCeZkvDs{)u+N`*@`6A zF;O}XpCsw%kM{f-@{8zK6Z}hKM0%)iiyM(o;_rW&;BOrE8TH|S_IzK0`DZZaefE8C ze0I!z1*{LU&)fU{SME;s`}~iI+>Pw-mmm4qU4hI-&mEo}Ph67DIbbwnN`n*6?tRaF za>v{51&sex=?2`8o~BLTWp+R|TGtU>PUS31-?3#7FPdEkN5ydx;{iGykiI_xzLe?v z;biWyYrX%ZT;IW@@NIDt*X$!g)H`*h-uM&qqnlwN4jj6w20plH%te?5g{$zE%||bj zrN^Wm?!>P|ZPvHRTXpBOficfI3ILY~<6miriINuytS z!yWO$>+ZvwUw0qaeAK;f)2r@1YdK2<<9le+AHBP6|-k5ksD9c5B((}*-yL}N*T4ufe*D(U;MH0&aY(Hnwk{mY?zzQ zqPO6XB3p+I$mJ|UX)0@@cwX^$25Xc4@LIUQI)Qs8`@2-dZuL4o{%_Bl#b-o{Mcdjd2ISIu~0PG0MtS00ShwNBhq!1;R`_sE8%Xn)4MQq`9F?CCq%cd>5+ zJMI7C=*wZ>{m}ET)k2$v9{5<-6z`+Uh=Z z#SMXFI^TonZ%FLh5Xu#QYrUnXDg1p&uJ&K#Plgjc*q=|^|Gqnu`(ojp5B_EGP9cW1 zNJam~p!*NK_=bB9`rnv2C(tI7{k?2{I%l4Sx7KGgxPh1ayX<-n_9ap$Ts>^VHr;qY zd(KCs>!iMgdm=U_1zqv;9EW#`4?TbU z+$V?mo~k$zo06tJ`P-jl+WL5g!`|Qq&Q9w7dUCDu0Pp+q=kNEhmL6&z+$)_d0Ot(8 zACiux5wA&~^^H-wz8_@WPlJ2mXAoh0Z4G?}s!O@EC#a--j>h#CIi4 z7`Nhf&whKpTQQpHtT1E_rpv;RF{JWLl}VSCrlHruS!sk5hP}q7I;F(G>BN`HC(~!v zgcn1pWhCmXNVH?%;9hIob-_!YY=QUID@-S2c34X@eW*4dIIUm+V?X*Jy(6o zCa1B^vEXtQwq(5Ghn?@Zaf~e<5k_&EBlQn}|J|F8x}CB0O~qyWoZ_`iY+9=DAudca zd|5xPW$V**O&{rCYPKFMg}K>t*(S}8>kRBiDmDxKJ}x>ZWq)KmAWa#!DU|m3^G;o< zFa8;$HaZe8H_3bQ`I;214cM9PF?aZ39LNEGVJ_7Mu2Pd;`xty2ECd&o~z|>*?_z-bgA>FgY=-PFh7 z0ld$hv^7LqhkUKn8~@Q^{KWy|!KRuF)?7L#`>em$O+_-KE8vwuxiHTd<)zt-F+;H_ z^U7kb%4fmVZ1TcAjq5bpdVXB?7;dRg)=NGzNWC+7nfnO375U{e=O(n1Jq^IMIQTV) zSpPQO|DT8MMZrNIV{zDP-oSgA|Lmtd|9kBB4}bcty8|w4eCu;}?Q5U9eTe&GnO_Pz zC|&fUtAFP_Z#e$_0rm}k=9_?|gNvVW2-7<^9CdG9_o{ma@m~z%Pn7;5N$^OzF03t9 z8^<-0subR2@GZM^J{8-kziAcbX*%0iIr&tbAsFVK&6mDsfVX|;$XaAP`6Ab}d*X#q z`V%iY=}&q>d41t`GKeF=`?!qRR09w2cfv^cfxA*9hnQWuuY4B#Ry(EQLJnij9D}|v z-ptX|vli0*47HOF&^^5WQ5==QzB!F=xD^v6BGToDS@Xy*hkF_k&&e;xiwBG$^4sv% zXKn**0q5UtGUxjaHlf!)`2w|-9r)tdkM0Ly>|c223wHtfKbP2E{#|;UfIZ7#?eV2< z9MAd-x;}W*Ywq3Xc>=fw;Dw!42@l%!1N**kup?&z!atS24KRFV7ipV0WviQsE=y-+ zx6{mavj&t2bKwkrmZpJkiu67r)Md*D@T@G(oMjWF^I&cGr*WO}y&zK;2$)vsn z$Rw8Z1P+Yd{hfuKaCpAnDLQb;AzoKZFo`p~(L3I8Z$ST3cyF1h@w2u8 z#&a>}`u#W`=npr7T^8HbJ{CzX)4K*s>`5m0rojWlR#@}54B}ET_lq+skF*gNz}w9tw#}KmHAJ~nSL%y@ z7V$*ZW1eIZQ)Z6?f74m`%p4{1!4mNyQ}#@G+DN}8;y@mA75?Hv9`T*>xy(`6d-!{O zDBeovZ>r=^GtfKzt-QnjY6Lzv^2_C!v8;h?Y7F`xO{@|5%@+Rb1+~sO=u{SKok)9! zy}>_opbz^sal^+m6Tvf@{vO%>mV3_&Z@68T9&`hYAyZf($w(LClV0H47Y-zXC+}C? zENs2-m;PRdedrEGHE=7QIjCG%dw$%r*TPwn3C_}A>3J^irI1|d8xW&nwbX@9-e-3=gmS9h^Wn0GL|HJraus(3Z z!ya6-7@M#cwzBuaJ{t_N8CI6Z9J9Gs9LQ8kJ)8m~c%=5MRi<$QUr0YH)85A6EO}I& zZ-4S=uk#;a;{*Sgk;n0$#XC6Zv-Bd5_l&u$RSs(z1S^Mq%0I`1efVxr+z7&vWXAtj z&XE4ZyZcVObC-=R#D{1c`ga@70{^b)yD&~>45^6uWsRHXbfx@?B?sO1*y(uqqnNNWu@5Y6#34ga@Dfo&n@{eZc$!F3oL%NTEx!D8w z5z3mf&B=OZyOS+>c6)u{cXH`3_i1GERwoBuHQV)eO&;GMWHaYd_%2+0doDt8U9Pa=0fmj8_?Fmj#?l6*KoD&XEh*M`nXrA@8uu;7l=`)84t9 zzVe9i#ocnAk%J#e1ADD=A$yNz?1A!_a}j-Iu?D%EEft672Ig+F10RO6r|xjL&&hdi zr;|HlSH1DinIzm1>`Km4TZjWU*JNYs@|b@HxJ$P(bxy__NPn})=YX3DYgwpq!h;<0 zIkd?Jn^L$UY;#nOs4wZV$};hD8N|Wzx#_e`M)zd@A7$Sa`4zD~N!U!?6NE>x#3GU3 zEbRZu!~e7lu}b(Z_2-v!(! zKA0YpW~iKd6boe`vKz{0FfP?eGF5JMw1H>hEcb_U4F4TYE*!{(1NHrx-IwhT3;5Jz zu|~4f66Ga1%rjr}B%g;J%VnOqT<2k@l-C;STA1XK<};@p**mW3+s$Gfa#`~n^4a1P ze35OGZZ%+UFrtqAFC~tS0V}l$fUO^%M1FJ7`;%#}Jy9Od(0k(Pe4Barvv1wk+4G+U z2ZGd{NSm9{=eoZeu~R3~uhzK;eJo^si@;wT5I)uDcxTQR&Lb|&WzF+gbN1Y}H_zev zPuRQ*!LkM4E2h#uo#$tPyXh+R!W4U2i(NK(Vd-H@%5@fV%MhMwN1BiQR4S}xYYRDB z&Lz#m?q%bfa>z@RFPXIq{I@&eK<+e8>Wja0{jO6u@uPlpE%KYg9{|SP z-CsZ6Gn}N@gL$=tAHrW_#Q(V+u)k;W_h#pW@fVhb>_g;Ris8n2V1F+5p&9-!g)yoR z>2E1#g3{qK?7ObDPsTqR-fB}se>vbMT&>*j-NCAV|6Jb9 zo=R*f{VW0--TMaLXh(i__Djs@=zIvw9KI#!jBl;`69*dLCvxZP2K#Mp8RvQVq|Na6 zE&1lPEpcE8Yc5}{IH7>GF9GvTVBZNWTi}-!FGO@$;}BO0DED-nHRGNF{Dj(+!P7#X zkqOSiJc4^3Wx_wA;{}XUdM?a!!niAyXLyGm$Qrqm#L;dEV2)4m|m{>BJAUFO)ELVOq}m+4VEhE6T$C zwZPuWo-|*NeV|`GLwa-?G36+}tBd?9Sd*YQf~51D09=Utmhv|mr+`~Nc$Bg}@z{#> zKm8o3{x7Asa1PJ}`y(uCIDaf=tt-H!91axIx9~4Rw}ovXb#1W^?Xdw>vvwJ-h1jti z#?e~)1?Jrx`<*MUaBsfq$Vt17c5{j0?R~f~E<*Rq@Qb1~sGM<%7p>S2$)0Nc`KX`s>PPOIUmkOh5C^`h@A`-Z-{o(=j*x#vLb+nZ z{e16N_t%fHM#*n6ucGI`YUWP2jJ-!W>#ytf+upS6La?fa3$5Wm6@DX|`AU}!SN40- zW9{Lr46KBku$CPN!L3}LVxDDVl5eqg^hx|D z{Od}6@dq2Ht`tw-Ra0pA3%jtO$9dox;exO$X1*n18!2qXnF{7s0+*#LMcU)T)XjBI1>5I?PGetKjjgRBRxd?&q_c&Lrx;A~#YvU1eywODe1xIwRuQRm ztucM~tMk4|&+X6oa}zk14fiVOw~X}`7bLpB6r5CM+yGCr(ac%nP`p;f-qLKlp22wX zNTt7?&U<)AumnTHRQg^>pE~zpur*!7-nquZf%KEd8VO&eVxe$G9{laU8f4Ov>wUn(WXZ&hc8cy4APAKfv zhjd4=YXLYEVGqhU3lpy8%(nonvfw}yV!+Y7lZ*T+Sid6HQ!#S}ekNt=ar|e$_KDjN z-PX4j!ljIL(HWyKFV$LwaW3MSr*C<~xL1X~mav}1T3hbPq^=QmrEi^Y4)yHYcwf*6 z4hRpmuK=6V;Y9^p3eiU?sp0|gK=lQTvkJSS`IT{Im&X{i_btYLXiuR1fb2kX`Yoi~ z_6fR=I$^JN;)HBLEBGkASGgzHAN)x^Z6!rKzih$&5UEpF>We>EI}s@&FZ{J{F~7#x z^ReSvOZh_KQD&Hj^UObD--W+;qqwOV`(*VY?6p=}vm#=u0{&eH*#h+^AEEY{;CKpn zj;o{p@*`ez6r%5{SDAPqKB%mWZ`ktq+u$PN zr98DE&8Mz%v4?*#94MaeNqzAznYr63t|L|R_SL`xaKrX7AK7nVUnac@bD)&D7P0Q7 z;4ht5tW<98Jr3wv*vdbaGPgqYBY^|*mDSH5H9OsmGgSIL?tgO~8n zz~)FFBfq!5`QA7%2S2cozt5kCJ(x#cvgCs=-9`M3?tH%GpC@_!Q_HK}65__CeE+e4 z^7)kO{;7OBK7up-xA;b&?(cSe|HJ&UnV)cNM!edL*jKiug7f(j>`VnQUvqp|F=Z|A z0jBrhUPgUBYXL@XDS6p*`69KG-j^8e;sAW1PW6>~m!dV}o&s=|wyi|GXMU%k%a8HRZRA(Mn&k5Dd&s`#@QjAU zasS=FJ9fO~o<>Y5zg~gx_CvkFK{!CX2Y$rp2PKKsAA*ehS(pW!bKgau4$ zq_6P6!w~$W$BM0_<3-@1SW5OlQi{kvnvbO|w#040+UUHZRJM#cNJ=z6cv6h+NPpyq z#j{LefkywN|BCA}v6R9eAhs-~GE4Ul0!txnNh$`d5&beXjsl%}MhsMnc#E z#b^~^E)n+Pfb_SJxl~~%r2B>3Q^Ed0`kW8WCG;tKAl$_X?Tdx8FxPoN3GK?&e&Nfu zw+t;g5TeYfEA_=6+vI?Gtq>#CD9$mxhXaNq7)YEIJTS(=ZY4k5i z!3Jf}S2~gkFEe>o8l1}Ixmn~>!|n8M$YnFWOeCE#s-3Q_jC_{rNCWhD6PVZi-KKp3 zW67aS8JGy`vpB2J{5rB{6(0<1I8njgNOnNBx`Z+7{jX9JFqQ5b)?h4L#fM5_CABN0 zz6AV*yWXEFl@zF7VrIQV$e+J2RI+ftQ@ZHoTB$Go_!ozHIn2Q+owv^cyP9Izb3_~v zmXZkmiVb7~PT%U#nz`3)yZ)%#0$-{<5o34H;(fp3fim(L#0!t{ zcXyGW>|-Y1O|+!EnZBt2pK9Xt)|}TW-wdDIb{o%RoLbK_IXl)nMXhfK#$3hs6pE!g zp`WdJ=U9rqHD7(i?Z7=%=vxKfesy5oE66t|zCWLK^5^QS72kgPDZjnW_b3|IfPL?~ zrTFOrJ)ilXzxQ3YInR{6?*Ly~vNrPXHN0DD#W}ae7Gmx#8MFG-_cCp`U(b-Pwo%ez}*BJ zz&^F6$b2<634F`u;;X?-aj^7OF|hX8+V|*8q?A;)FoZrRRV-kBfOV2@j7TZ|PWvL; z8^aCRg%U8zflrC7{aE7g$WOlVWMZ!%F?f>pci8+y^jW%_MEhj;n9j46O3cnn=MmX> zNh-D=nfp>{m&J237)wObn4jLYMana&%cf0466wd`y~H)(QujB6?+l{ZL!JT0gnKje zy9IHIamw`H(^=`VbYHQ%^k1U=g6z8Nz4F4^{5@@C+oj*wb;G(4PDqLiT;ckk za(2_s^3+qVX9%kRV|x(H>;C?(cUX*DIO=(NzS@UK%^px*jm<1w#F!})2Pzqd*>~nq z%zVlvJhLsag<;Pq`Cc-wXZ{{67NrHNsA!y-qoJ z>8xFGO(k=Y{;N!U5at#0J?zaV!r5}}IRksBxJbUyMEzkKZQrLpC>JMGru~ury|~7_ zC;Wfmr}Kpz<|8gO7Z>?kS8*u>J`qt}71zzE6aF3fwn2QV5QboC0!NEkFXhXz?ZUel zsTB8g=A@K1=GRC|XvcSizd8Pj`RYp1oP9O6*9qfAU9GNK?8AJ-193p9r=QvvVcVFG za2I!4Fo(!q$NFRbrL>nGm`$XOY@}iw*}{0vhFkM(?f>-OmDs65=E7dgZ2|7BcyHGX ztjxZk_f_O|o+Am7FC#t@_GPBqtO@0cd4zX`;SLY*_44oK%wMsIaMyJi_hawvy|CU5 zmn=SL@tl(!_!rky@>>l18?mGL>8J9#HketKWT@|>C3zhId zc3V6Mk=p(hjAfH81_u91+G#JXYo*!;=ir0o+kL!GxC~1XId`rd= zBaXu!Kgl;BkMSMw_*Xu5A3NmbYg5XVf0DZK{5uchxPF{76JW{80 z5`QN)p1P_0-OyCd4JYy4(cd}8NoT&zz_}V+s=-mxf@|3V*#U7uF>`b3#R0VqQ7%6& zzbJlmVqVho5-_e(J7LVVae%xyVEA)?2tOeXC}!rY$@Bl=7(j7AeSg2gA1?5YzD9AZ z_OOw&4B2hvg}JyO(O#z--x?ydYhf9dPS`uOm&iAY1B#J!E@D!9Ee?1wk^HIlJ=&`U z*y|S&d&O`z5QW6DCktXbV~Izji3cMR4_{(Am-Lgya8{YX9>k9X=pz<;l0zKrqdt~B zrjPhB4u2j?Tm5a3{>?v0BKaWg)mIW+%w(+-H3sSuc%I(fWWf0x#-7i(Wy6JcOZjj7 zdTYfK+N{wu{534I8A z*^K5Z4_o}NJpf!F20T<>_#O1mgguud2`%2=PWvyNS(I^}p;R%O@GeJm{vrP@+Z`gt zsa)uBqYd9#8s^k#eJp09joK*=Qf$ZB;B)iss4G_- z%5@v|vmyEs-XZx8q&$qbY=A@@K(}3DeE!DG!(Ke7T5%{O%h2~fH4Wk-UqcEH17 zKip0_>Fr%uYj`hzEo_8=%B91Kvn#;9xoiM+t(ixA&IEJ@)`Gg0tg~W&wUh3P1B#K= zHiEy_m3La!M!rH^5kJ&M&k*OT6i0L2Vl98CL0fTBd+alLmNzDk1B%79uWCkW{i+Y{ zSDQ*bmvW_*qDOn|UeK{DiIWZ;iC6!-LlF zpcNRmWgpv;Hf^}}I1%B17Z04rza7?$zQmQ5v}sOTVP6$an=uCA-i-QEQk|n$3iq(^ zjX;QfgHu<23;Zh#f6jnNTP;1*R{mIP9of5bMqq2}Vd7zgbviylw#jS?ZN#TGVAp~7 z{KCB*=WKdkue_euSvnlfpTn7LTXawR#*Xye4qFle|7!Zxxa7AxQ7#;Xo%wdKx`_LP zeMH|oVCR$$$Rx7-+HrNIE1obC>sCYnjLHNtQ3wzo1D%wPJ-RwN= zN)dG2?1t>VuEhuNuPjIZJvr#`%|U(PH_U~5M2hDhXj95wUH;hiETmc=*>Blvt-0cQ zVWPEF+^h9&r8&{21{>NIOmu&zqa$)4dv}#;1B+%QuUg(*F*OS+TtAwNk^8 z`gY*ig?L}s3g^=~&ub6XZ7A3E8R7z|q#fmLk@mD{MZK3lZ1Gd>Q_p@>0?eTq+|~J(#;C%MR7S-{V1j3BdZ-p8w+e9QGuwr0;t7 zBy6OU%4^*tb|A9W6^pU2=)b4W(rwSrOSd9;SGW*sCRhdhkcCW4_- zwYL}>tXgT^z)|Nb;*RM*_WC@|YSp$4oaxM&l52L+hyk~sMEJZmXa6Vt>~kgHsxIF42iUidZ(_`7k~890b6iKz`yy@ zLs`OI`*LBg^(rHkXk8;Z>S5#gTH&EMx25JtD&MU8)JCFsDT2RjpzJ_ssrvTL%7lU1 z>0a4?rKfXlZoHtaaFPv<*aD3~Tu?jlK%#o}Cy9*lOl*YN5c<&hp2j4bth70OT0F+s zTH(vRc#Rm9YvElD{?hwD!6E6o@@Ml7M40Qk-I~L0N4`-$6R9EJ0pB8>KOLK(I`tzS zwdeX=z9E*+7v9q4>XqUKT%?WOr6_H|wQN9y2f|-@Vb3`~0tZ$c%Fx-rB*KCE;!hl8 z_=^LQHY*M%8_uke&MvfXQG6{C2gC{4fe0QJPqF^O-*AOHXJ9k6ALzt;Gqbz&C5(ih z&c+lI%g!49!jW>_EB;92qt%D>TjdgACJq@FxJT{k;;FpZ3Cg9T2sgtROwPl0%jQSQWCzXw>ki;95%v<<|Iu)?U%f3QuYAy?~18q&!vBcw_zguhy$g=%u{iY@R$EpOefsh2m|6G z<%O^8iNyG#R53H+rQZIl4LU2FwI|a)O&pS~6~4k*eW`!jn_K_XF*db{;BGq4wb^^c z^k6Sr(1x{d8P;><#RXw1J1;$#en)Wk(nDqoYS^Qwo%#_EQkK%!96VC>;q+Iwn z$HqwiL$W`VwctMSLAo9yFWlMlxy`AobuC+R9^ZD#zDs}Q&lO*vp?OlTJ+tl= z*3CITX}v-`Vo$N*2=~g)c-X40#d6Qi8%MZDoDiPkf;gaknRsAa;QDmF%M)i*SHp9Z z3UiCiX{)_`3;I_d(p}kX)r&u}>$>iM@0MNAJrd#FmcH7pIpm%W-n#Cv&eI^oO21!1h1pgrx*pnv&&iSVwbTsA;w0@@FX2Nk691K{mes2%w0y{~bA zR2-=9&-{KZ_8tdh_a$vPKNCk~?_277aWU+tX-xLswH_cs_yfZTg-FX_GB6iP_FX|zOi9G%fe$0vbNDWL_B zB8s6(5!8U7g9?h{IFKF)NiHF|1X4k;P^1Vr-5J68l$mde$^L%7wa-5H+!G?A=s4e# z^E~Ukd%wG`^KM;C8D6Jze{r9RT&G+1p$m?= zn8`PcV-qHE-O&#{pB#-2s0R`{@!P_0eLta1vl|_??wRNG{ra8gf5O*izKFIc@@=D< z2oLIrwlf*oSq9j)eJt-Omg!2Zmyy4Q1D0+ZuBkKfnc*dNq9OJy%>e6=B0R(^T_;~bu9y%pJ;rz7xqd5JA3Tz4oTCorj%j(}n1^0SjxM9$ z1^%>&CeY_?7q#wZn`z#sWxJ}|d0X}be7tR_wr$!j?lHe}+VvCqc`&yP`xq$7dhl(R zA5zcrc=VNM`{;!JSfA_h8GiqBj{DZD)eq@d_SGLTu?WF5+@6LGh~G^0fa~==?QBmKC-}5JC(Xsq_3JZv z@8i)G)7g;M&PETWb8ITt&gQ#Q-1YC?%Qh+NecnquQU|mx?dV9}>sZdg?#X}Qi{f8S zZwvm8C(AJJO4mI*PW){*54^=V^no@QFg9O&$Fn^i9WYPK#M1ne92ZHR_}JDFJ^(up zU0~nyPqtkRzh<82_-$-z`^d3upU8Y5%Lezo-f~hOVm(Tkilg-?xBZs3Z1uRLeVO+z z>K$U}w)W)t9-mB}+g>vje`G3#;hRZ4Zl0LEkA6pSQy*ue3$w9J&l6v5_PvZ-a$o%P z7y5+R*u4I&T_XQaf9N?G)>GM^k`vd3k73{QC$cY1Qa?-~vDYU|e)z?)981b+{DUw2 z*8blcWcXVSWCXs{1+714eVF!9j<;_bZ0*a8{z|qZA1v*wvX9dI%m-+H+9Q~g260Z* zfz4yvazOq9WA-x}2PexqD621L+Zk>dc9w-lN4+`IiM$RazT#kcJjF7G^Zee5`%x}? z>{wDUoyhmM+xfnA6n(zOyf*a0Z#1?ift%lBfg7pkd9PXEHOIEj%|8m9r%}I|hHcLP z|LLUD*`Gx!{?nnE=*CR;XOiCz+5V*dcualpddCn>WSpY5uaCgiV$TieFC1k$Jjh`;@5_D$#M1o9@V6VD>1$lPx%>-Ub)bZ&(9I}yCZ z)9qXz;`>(|!QqeS)At^ez{TT!FNGf<6$`NpoY@wi+&<0onDvY)$W0rZ$o-}v$4R8| zeZ@1}cI@iZB6+?TnkV*>d0wBNGaXzfZPdS0uMcdp^Jg%Q^-mZdAg*37{xdk9;r&uz z8#sgSEY1~c@2M_i`P2=_^FH=9neTm!DL9#X`doFxwoLs%=s@R-_vN^APJ+H99Dlpu z5BtZKlX5!!nSbb!`vB{JVy@jfMrAD2W$)IzGJCgt_T1wc8*ACEZR@+m()Mn3KwI{k zHnzdrW8$Qa_>JIrVSUF*jJGw<^P?V#AJq26Tpch^tkoaW97p$+x3)VE-|RIW(}qoF zVTYN`i8t4hXTJ8W{fUX`B=pJqypOROK5rV|KU2kuYxG-|)lORR@#_ix zzN7<`(Uu3>_3_eC+B2zTv#FRz*Hav!5FK=wY<>-7ivd~wj;kKugr*MEz>`C<2BSj-dmIrI~GPUwo)dLAVH zrs_mS8TQIu{~NxTR2`ZEj;4+gw>(!*Ci8Bdo7<`7y?UenR~N#*(Sd25+WJfBaePO6 zt>@1I?>QXH{6(e@p%)zYKH{xjsR!x=cK_I9Y(2w2^Zn|;%q?#({@80b>?z9=WS!&~ zh^1wK<$vH$UE4Hp5B;D%jc#c7*8PrD4>%{Q(@}R9?;;%u?&5S*5r4~q{FwegzoDGx z;R7adjd`A{?N7!h2gc}tdLUVzXL`Ul1KXh&r1qbM-hi|9H~p;ViN89L?PvLui}r3_ z94CDb<&s!L-JE^PW%V-0##G$3ZO^mp_uFp<_|AERxGL?5K?|B-}d)e^4s>(V@4^{=HF#KjlAmFZN|+!Fk%hbvyCSY+w7&>iqir ztlpRT3BL_apnpw2VEy0zZAm@w+s(STeV$?}j@qd>>F32aKQ@mxr0^f13qMNdJ@b>m zF6t5L1Z3SHFy=aOw@eoM41d~)Xm1c_ZQRtdA;Ug9Pt47e^!MWRUNBep)D!h;9{$K{ z#6Ie4;4%q(CXuI))>q5~Z_9JHvpnj0MO?-DkNK8SN3{Lp(2sVHkv}`G%f1&pKXiaH zz+**Ua*pM^)TIM2CVUUPt=Q-IN8PVG_6hc_F1(oe_M*Lmt@iDAvu$SLug&^hXZ>%c z?SAZA|Bt$RS?c#%){=J*V@qyK~Z`AzP9r_=6Qt}kwTj@QiNyKXWv@*8j_?S1%1 z{6O~Yr+($_sK?QkXM3v0q8`clQMU(!IkeU2*XNSHTYSJ%9mw=0vw8Jm7IT>N_s2ro zrS&zj_nF$bbr$X2R1$yj6KB&CXak%_AJTMi75}-&My!t`-)~9Jv+k$8&+!`aLN^|H z>1FZ~?w6ofp#!-t{4VfU2gJY3?z3(1w{Bp0uMKPOVlUaw=QqD?ZI%PJ)u;oK`k>vb z1KPEA>o;DGlX{TjV%^YsVzv){mK%_miZ$KI=VftCzNEy3V8W&V7U5bU8)ai)91XN1aXGVqe?VA4~fENt=rGKJF9v zliKzdzJPb~e0_rV(4UCE?R=K;vmo1qw0Ctt{O9p*)*HQt=Ri@$<0~>9Ko7*9_Ncer z_VXL=g?|xy@#p&t9T0DQe}?-}w9DHb8*O;nxLBhnGZ?3(zW5!V;h*{a+%~mg+t=*x z%I#iTc3*PaK2!C=`krw1$zFEZQr`@rLp$8uYC41ri5 zZ+`$J{%%XQ56`21s4o%w6VQXvwCk{)P?rcD;65ck@VmrcJt*0K5qs+eL5wxS_Cp7( z;~fjWredFsWkl}arQe@}ztb0Jzqzj$mlzj;+{GiqJ+t`?KXL1_?~|xQo+9xZ2Ng9eKt{l z;9Bk8GQhf?{@*g%G_MEfQ{2|4drX}DrgIFTb-l2A^2Gck?Oj_(2c+ZZgUIyZ<%F^! zi2kF{1N13T2e@Z$s=u?8_M9m5wfS=Fv3apiXvwwij!%%PuEX$=}zXq)s3)Y&Q%8jXSVaa+!v?|>OrOhw&7-F zx%jFBIoA2U<#~L|bKJIA^K;SwIsBz4&+hFu~3fFgiQOQoGmA z9Rnk7(dSB@IEri5&ypPzZ_8sb_Zt1TePyO%nAa7iBNOjsp6%{|C+BGQ;$Lp}+O=)B z-XnZ9`(Aev<&bG^zv3^Zw#ABZ?$>ktZD)^s+Fi7D>wPCuzYKi*8(S^6}`0^+EC$iF1bi%cL(SCrDeREMNS8pFe%Q zI#9Cva_q$)!k;I!vF`}K4_5jFeZjl%cW!I<_N9rVzCb;QGJx&O?!iNRpu8W(dZOpg zqYW|qf|zoT=qnRH_QhMfR~JHmz|H=cyid+=ee<1nlbw^s^#b2z%GwK8C8OO7>9?SZy5706`>h$dA-|gb>^?vgO z{@f$`rn5bWx}UgX^I(7f)*rvtLu}E7z&Px@h`;%gx}Z<^pZrm#Xa2q{1>d9`|9o4Y zfDW{A%@@G^S#8{L8@aC!9}Xt=o#i?p{_V(ET`(^{H+(e5w0$Y-n?payi)*Zlah>h? zejkdXsd;Y4KF8DvZCg9f?LP4LeDbXu#y0?I^u@-x8II;#mYc7CkMf&+zuBVPCr^7< z55(6rzU94!vO&_vQ+_{39q#k&w=q_vWanZ}`Crn3-2TO0o1eSwmH$cTOV4KPMA*4F zi*b&3$^P{N8=p(?_Z9Sff;}gr)-kSp)AM~Oiw8oJu;HoL`!sy{bnJBc%*`*fQwL~A zKicv0?TpQ6|KQ87w$l#K&bM6qoQGe8e$)=`?bv)qoX@o!f4-giL8RQ5>$w;A_!SUBkBbW!y$Pexe;;z%@T=r>@q{_dq*#-i@%~cKT`C@jvYg{^qrK&b+NJasG?# z=ukVDd;Sc@6HP}CrZcu|8u(A8{x@m%qc0C&ELU&aZK&@@-Hq}+k*v>0IbXDM@y~G1 zbfDb#pS53E;$MM#H(j9L8r%|aLpQS2{XEZcb54%G^<~C!#(gO3v-~pb ze;xeAow;F|4sbuQ&oM7g-wOV@AJEt5_`ehEPlo=t{pNrV<0KR6V|)i>I*?-@_~)3C z2L8n~w|(=%{=4Ci{c2;_kg52Ky*6HM|2h7-56FE);Lm%BzqV`oR`AykUj6|4IDwav)ECmF)H(+b@Cr64)<+{Sw$Of&CKLFM<6M*e`+o64)<+{Sw$Of&CKL zFM;1g0)Oc1>HZq;w;J+NZG^!Da2PSdj2Ns<|FzF+pbNz%XD>%T4gW$cFl z_V1Sgh(hd_$9{Q~B=85Q55H^w@K*g*rN2u{!eayXx!3*`T_=P;?d;kQfBQ<;e)!`u zgA9LNCIH-HH-|dI{jvwfZkGcumKUWP#_c$#_y+Ox?2`Yz|09szOy~;efMfvw{AR}g z{=fYacuNwfUAQ!CkESl0xpExh$z(xIEG-wsXqzvtw0ftw@46 zuSz-lyhlc&?Ozawt*_@2ea#85H@~ z2BdQQIZvJMuK(njU(UZW2zmql!v^FZ9@YO`?~DBAe3w#r5B}l(f}B=&G59OCe6IrU zv2Ph&8fPe;{h_jXjBv9tqU&`~&dRcyN-v~XDm za_FcwHSZjbFI*mZk)FY^vsZLr7a4J^`hqo_&p8XpJ9{OxqN94zstxGE16Aj(T3L1O z@|${{{;irW-pjC;_agpPA77JHUwq%M;GD0Zg_&O}l*yo^1L#7X`f%p5i=edb{QFXT zKw3C`S&9y%)u$~@!8ry06nR8iShS{?zdWttys9&^w3@WqYc9Ai%UekPSu0X_N|6KU z%CwqoWRVswUYmk>kvM)H+htVvMDaLzz0P0tiO_lVzLNKgGX4vf-oH;7RD*lqU*tdZ zplAp6XRkaAACT6a1@T^W=dMbX1NIP9bCz;I9uns%lfpuHFqPzmeD0sgX(9Zd&3Wgr zPs+24Jexx2axHup7OhGP=dXf)_TjPm{M9M=XWQ;yuqN%Le8`9Y6x&a0E?eKT8}3Ek zt1r1P*%$xv9sIw63VFi)#5x<68EYgFib&O5qaDDq$PiM@3I47&JN29Wq4e(KW0 z#0`I!Vhiw|$)JH}Ht_rg_;28y8y2ld>v_j|^6KDK8PvIb?((!A{_4=vTHd+VclNj` z?_8t2#Eg4{Os?oa7OOM9!L#P$_aPI`MLvb|iu)I=P766*x;Av68o5?+E<9&+5%OGo ze@|^*3f?cdKk%=)Y;XRNH)RfdSIPk$5B_NBjBk$N0#5Vbj=R+5+ zLO<4~&EOmIfVVm@mFh2oH_od=7WK%a?tJbmaj&3!n|s!Sef{O@(z;Kt582eJGw^2` zdUPS%pIny~*sqp2W*Yi{JgP5dKX}jhR`&~FUvt^oy?g+AmxK(u$zj%^ybw7qHE{w=7_&I`foC9OFEAkiSUDua{Sk!H@dVaAik!5AdzK1Q|fJ7n6SC{s$i0e)*pCEufoXE&ms&1}nWEAnmuyOv1VMpqj-*2p_4g9cLHS*lYO`nJ$r`7iP; zAG~uA+rdBgtcB{hZ#_CE?)9HSU#{PnHhl)&Fz?grp!MMoYuT^moLchW8UML%3EN5C z;+)o3vY%j&C8~A%qBU#uu~E)|x%CX|9tF!R?6K*l&all!_-f*Pn~{AB{yHPK*QLF& z3wdvZkA_Pj@NE`1?5W{O?BolN2F?xFZ%!Mo-WYyfzC$jgwaV+p&B0S0_qH6!NM0fB z@770(yn9b{MgO4AT!AdK1Glf-u!=MZx{?&C{GpFY1#QmC82GgehX0H*{?)tCKX8-( zCirdPSuMO{Q0!yl9Ba9L6J;UWPz!QvT)ZJ|y!yek3Hj*L8gF2+LR z%eA~~)CKC~g}mS&-olsgyzm*=xMfMbzNN^2w#`2N0@_f2RYwwZH7Qj2gO{X&HrIn~ z$N(L!k@SK1!v_4B -(X#iV!*VbEjehc-NCit`N*MJX_mwL|EFV*ti1@2kkn0~qz ze^yOejg3?z$7*CzKtBq|sTzH%MsC&UMK$_T>pi)59jVV&{+SFnr_IU~Kh&TckZb)_ zML)_p`csZ|kS0Obl0ua~_(;0b+D`RU|12-zm$3B)e6?j*_++ue&Klvr3EZ1`w+6Nk zN6!_qoB=Bv5s+HGgH>a=u;y=^Z|MQpWFaGa;r~mdJ|DC=L{p-K~SNhY}|1g-B7>HjH>3CDq0>!^!5`kW>uS!K`L&&;hV!qpCdeArW9)eTpVRg5 zvX(XSS2I6*IqPfO!(8ugvIhE{kNr5kgSqmzvp&x)tc&nD=2YIue9UW^LwY&$IJf`p z&(h17o7;nDTb^UT!I!q*`o%}nW}evupDnjM8n!9l%D@z!;eS&{(`Pp)IW@Cg`D?!V z!K9p8u79Yr1z*&}vs%zW)81d+nznwfGwMBkzO)s;#xuEpAKsyb=e1DZX}I-KbpF2d zdglE4JD0WcL#Neo^&K5fs1GK{Ex8hcrv%w0)cYdOd~FAw5`27uZe76q*e|gjRS)i8 z&$~48UM;u5H+t6Rj_qmh+qY7#Je0QbPQAarGi~O))-P{Mn>n}jHs14=M>?86*O`=4 zCI2ngY)r}tf8E)PAJK0&qjRC-;M~GHxAIQCd0xv^*!(s4J7m+09tQtB`ygyUTmK9g z`diHVC-}_-{}KG+GZM}@kozQVLkHkD4#1WUgy(~l2Rhf2=N`cG&u6{)TUcMe2ePhJ z--~)KKCNGPIBllh*L)K)ftn!YVmYH<47$0qqvi7tC*@Shf9q!+O1jat;D0SVs#o}x zKKPebcmJ)HOJNm(F&2|n>U<_7=TzddRAjGy2m z?q=1iAwjPb-Yr4L6L>ig-VQ;p58?gC|HbxnBV|EP_76qw zrxIWBQs&7Ys@`Mwz0m=m*PCIVR{zpKYWb!#!n^Y*az9~g+5pDO*omD zyvtaB;|TT-=lws-@kPk37dqDGu3c$=bf^z<=<~%#(t&LE?#E)gKQb7^^9DeT$Vk2R_o(<|EAJ@s|7XNx z?SZZ&_|wl6_2A>I^%i9va=DAT&TROs$8L`z7Wh?LiQ$ZnH-7!G^j*Yip3nNCwYP%l zojU{bz#0AqfBQQbkG!M&_y5YybP)I}7kK8qcBF&u+SM`eu3bqvRq{XRj_pY~4Y_Mq z=MbJvUOEC?26OM>=-O!FP!B<;M&A2mI`r$i(!tz+1n)PLcWy-n33vtn+CSR|U^D(c zhz~Cyvuf~2u!);kd(z(x$^hLNi|-pj{OcjapFRrxKabc!eeT?qwtfR&$-0Vv%9`xW z_{st3#&F^?58*uq^FAXuf9O3=rh|ExA@_ba9mMgG#O)qJo^lw>dy&^M=y zpZSg={~sWZ#3A5+6z_K->tv`0*RgJ+znfX}EkSo$c-BDf--ayS&-wk??#FXxz~eiK zMck||lQ#%lz%m`E{JGztZ)Ds0f!3>`|(aF9g6;q0siokK=wL;Ac2CaSXBD+u?s8{EmYEk&wK~+fZZ1bN+DjeHhz=$qRdAdj$EzxDWVs z48QlgNjX*WKjK?YB;_>f+fR0~pALt|QQUhZer6Q%9L00n*dNRLxUFnP5OZGJ9e{ii z{Gq??@PELaJJQ48$KRjv9diV>QU#rd4qU?ek{4n7=TI*=pEy)^^L^y+Holb(M^6TF z-%;4Jv8>0V1C!8y`P1Hq1z%65!|_4JKOe?`=lpwc?Y&!#k=*;X1N5lK*BJX2(k8$4DeV#q?9^M0)4_)!S^g#TizwPjUAiNi_8-McjC)8n11^fA| z9kf7VouK*DjTf+P)Cc)qoQ0p?MLodZ=Q+QZ_Rsy>xUW9JxNB|b!)Wrx;2+>Ao*O)} zJ*v2G{z#8QqgR0w{6k4O?ZyAn?MTQpY@ymZlkTBj{6T5=at`&7~tx|7`DfvZ)1=- zv9NB$FY!0-;aGJM z`TG2^%C{&3^>HY9hhi%!-*H?2>E~$=%A$IFMo;`g)wdo`tJ$u?Z%PGxM=f%#!FFoV zF{uXXMSc(Lq44Z>FXVU_GV4iRuNBDcubxb+Dd(gE@Vg0extuz% zzprzzKJe_ildMBTd`Q-##+I^}gv6j@J7QJxJz-3BW0s6_oAt?!pGjT^d5P3kq)PtV zRy~!J)0h=M=tLIj7_mm*v_IoZj$iftbOO8{1J7fTyLjZfXg#qS-nOIr{*=Ks{J{2q zem&iZT}wNP+dKaG7qP#cbce_IUfu=8IqYxZ+NH0&7WeV*1Jn~nBVS{$8z0v=tAWQx z;)CN8#^4jjBZo18H+srBZABWh_L&m@xg6e%|FNr{?nE9%{(g{-gXeMRn`=8Bxth4I zyvtF>1|>Bfdz(6mECx_kx8M(-;G3rO!yex1Z_l1R>3>1L zj6-a^nu)Xzj8z|@F_74g;ka=N#;*He2m47mRq{V}HSteN^i%?0oo(h}M(+zNpGvDZ z?{Mv2JD@x-U>|kpT|K(g!u|TgUw?G24cjy3qA`|?x0%JjWuLX`jKOy_esH|{iEND1 zC`EiO>|w%&=Q^-~q@4ERe+~S1r_E#GSzN_epK~PqPbB7_>peNTjAHRXv*=1D8Kku8AQ`>#dY_hyHCc5#CCy*MS zHp813lh|XvoyE7!V&b~33?l9=@8G`V8p)Dt3%Y)`Yn4x<+_xNM?V(r$QJ!2o_E_Fu zoTVA8MVrT@Hs-t(GKe%g-?ej%m*aYy#?i10fLG!zAxDl8!*hc%gUhLse`CPo?@Pq{ zuQuj)#C+7Zz`J%Aan`}Sw4OG3S*&h&j|R z*63ztv*fWYzUFH~u^&9Mwz~07(Q{+d8jDz*T_aQcT~~DmaVX{A_`|Lv8*zq_q3i9t zex_@w=Ih{_8cS9kAYSki;|G`1KKajM!mfRKzSkT3Pn*=YxIYDci4#j4;Fn{~X4ZE~ z+u4paJi*8Hag7l(i?wjup-CIDTk5)#NvCqHaW#$IIg?{EA0-w8Jb9imI$0~p_yG8z>oz?k#I+wb$F;|aACr+Xr%{4$zAkX-!#ub>&d93@yI_}7W>wWJ@ z^c%6>|AH)jYF8ZHO8$+7*cA`=*<{q(rwZ!6{F_EO1MbE>HU6pV{IgDO#6$>tMmDZ1 zYJ3dV&840Kj@WXm z_2gL_Gh!YdC+{3%{WI1$<~mZ=$qFB;A7%Ybw`+_aNj!@Z?VJB@@gO4xn6cfA0c8xh zJoc0E9@bC#{751 z!89fY*k6>#K+0krVNdJf72J;vJr`&Fx@%V>bJEx*e#{i!Wh#9(`MT!D6*AtOsXQ91 zXa;SZ5x0na-&;Pjn02mS>pr}}x8yNx%k}7U85m>igdN1oCeO92U2ELbb!%Ol+VhDW zbqVVjm*PhulZ3i^CHoNz5PnN>AmKM+LKp{38CV`$$1pWMsP7s3F`10RGm$u7Q;4xK8=j}5>&9-$V{hea@GAq$N#m~^ zPg`=nKDWHPezWWKx+bsd@*{(;7)oVv^eP3Q{YM-mWst{0qRvDt%){h4vuk`I_MO?g zyk=#tzRxv}C1crG4u`#Cds!Z~i(QD}4EV9UpQ8QZ+g#V&JoUvjhmBn%_L&~&FO5^6 zOvF>ZvKSMg{~R;cow6`~cElRtIj%{pk9g0{zdhXbXpJ4B9CBHpKb7nU{=t74@5X|V zD6iV&S>7%8w0+|u%46^f-?}lY#zTQ&(;a7 z&uP1+v+!?zYl@|DmW;ci9E`Q19?T>Da>UbtU*l+4E*^(WSYv(cUU5{4a@bc4wemPP zIq&eD#XouY*`85e!?WeBJjuI$dx|`fvKD1*TPAAv;+-AC2G9lh&Qs6PKK0eXUy*P9 zcV4G;{c*9)Z8`LsYt(Hi$^rOQ=UHbmcsKUMAMN__5w16k9>n^Sxg5}$(1E?cFJ|QY zmw3)zo$REw79TF0XFq>+xs& zXKS&m#`q!*yOZ~8b9OlE@63VvKV802THdc*#^Fi?4$y&B?%yYqfGyHF6dlU7f)x_Ia0)Gqe57ohUtS?;_SD+}1 zkOAAonc5fh-ST7I|04g?W6H4aTHhD`Vy#ct0CpRmyAk{)U>R$-#^qeCKHe@5-XYYmx8RFY=$QYnt(x@y$A?Z^plCO}`QU*tq;l>b?Ay#}UZ+e=EG_ z=c@NzcAu~DF5l(b`^G=8uHZjk)77=QbDpvN(srf~Z-D4e+j(RcG+wln;FIR zQL2lJCa`3}70Qa06*7^P(&DlZi}SR&ti+7{X$WdBIplK6(@J@kqHEIWn9q0aZch0eX6Lf5-g{1RJ};d)iu-;(^ZAO;kGa;)DVOqf1(PIR;fPTD^z4 zO?#MEyNCJ2dyw%Hz0O%NkGX@{vz;&P+)*z=gP$2>XK zh=7>u%6utgkrtd2>^xTIjxd+XIjs5I-k7t!sqdHg2Iabc) zs9|oCbGi!5Q>Z4*=1#78t-3f@G@m!fyC*^F>>2mYbzY#n)$!aW?%90NsEd@klfIfu*ndogd3@uAFHoL8E&=iD{) zvy8UHTqNGz`6*4z3+c@my57#cVJvkEV@Vqwv(KD?m@~+jyn6Scm_x-FYv&L+kE)R| z*o_=#ZbZzVs$u+Q4fm<}^tvrEUyONE&Yf~jREaKJ{cq06j(2b_B;%Bu;HQPLzs-zs zcOHOq5}kus!#mXQeg)FPCz(gc*mmc@I7iR9X}(Vb<0Ku+@0=#*_c(8h`97~Rhlx2$ zMFJ1z$7SSPoZ_6M8nAZ0l=BjtgW~ggGGFCj?tKu?I*8*3!FLbFp1XapW2(tNly~U` z)pCtc{x`pj#eE`f`3h zuIo=aknw#3Z_CD-AIjLrn|S`8gZVhdG7e#klfRdK_V4Lx;>&-S{JxBj`Vjr+<9Ux+ zjP+<@oNjNfcf4v}#&!2&ET!i#c6T?FFn%{^;Fq^0IXNbi@rmhR(qW998p`-b$3^yH z9O=!>w>gD&$&IhP8sqe8;m2PC_nb|?v}31FqA!0u*iVJ}q9X&i*0F*E8Ou1Bu@}X0 zl)D*UndCI&&Yej{yAsBV^32E5VO%$iF7^}l^p7m~YKi&af&Zcf=kx};i zA223^aX9!^`{Bp){9(woFLJnz^fkCLIcmgy-SpjVHAq*gkH@GuT&de~M!l?7Np3zmViq>YKN3dhOG%+c)hP z1IMJ%-o!g(eacyXGyN6hM_)aCmEKn#kAm!*HBP6oG~4ZSrCr*-MEfpj+bH#&GgjdB zqssb>%lk&@3*O?`lIRoWe)dn;woMyXw29j`J&8U|V?f$2Zd_d3z4Mrk#9~WlZ?&K1 z#pu7!`tfPc=N)VlpUAsS-2CE}yub2j+SBZtivFo}^v|q)cCP(#_DP!Bmz0fvu#KH( z(|?lnHD>*6_9sO@6YpnVgZ*ml#r^~P73?>oZ8YmcWIOr}H@z^|zOuaUFzXxX);IK; z`YYKF5aT;|4^!n~Uq-3ligM3;+JE8m>^m`Lyyw}!V?UPt-S&OkcSF1PYqo>Y?w@c@ z^xaK>?>t7k?b8x{F|Uec^qnGS@r&4-^2xTCM4yA$l4pO4?d$d{*f*W^kNJH1ooGL& z{mgdsc(!e>_8sMYM)B<8bL|tcjo7w9+5+d&uDyjgaIdR}6760^tnJrC&(H_+vNnF( z_G!b1*F0`6?b)x>ufK)%`?=AUS?sqUKjQJ(f0grG{%qT2;WKN8MMp~Q(6rUj*O0Y6 z7u%cZSEAiHYj3vQ(Y_E#K8?|2o0;v0IaR(#^aGXnrcVss=@07m?DFT^-dkz|E_=4P zVjrbvS3ci1>^JJa=-wt@Y>$q1*zWD_$iIts-XYo`-_rAqQ`!}WIO0*$b{-D9_(xO2 z3|8OlPt!y7D@k(5+ezNjd;5&?XWC9-v%~iG_A<$J0La_s@W3HeTC4FbOx`QC8- zhdRh_D%KAe8EahdjZ}NV>QntT^V`U8-ClkN@h#Sa@2(!C2a_Jg_lw_p&LfYt6|Pu+ zir+eoi`OTOpWbjvw(byf`Kixeh5z^rzUtijpGrSUU1dIH?0cxERr%e*953tde7h`Y zx-pwW$~@u)t<3jpWq$Ob-2Zs$cU!0rF2YZ~pZdz5P-i-U`dWecFRta{+*#`0^O>)i z44}@{|7rnc{{sB)0{ZSw!T+E7uAM&~sh`&m>W}klD}8(T n^YG=F56^vf^nXxZn0g-@o?kg6Njl%b+P4S%dgQrY#l6AZ9RkIQ(*nia3KVw@PH~4)TvIf7a9)1jdw;}> zZ8rI`yLU2o?wK=Z000EQ8^C`z1OOG_-75f~2Y(+L`oH^3s0aW@_-9g5|F{1U07!X@ z2%w_+-~FGs06?b_A^;b@{qOg7$N&J)2oVsYsjh&9PKpjc6qb^rtoDE3{`W*hfp1Ja ze%ioK0Z@{a{^a-fq}#uXUEdJ$99-#;WZMOqB2oM;mK=s`m4{q9i(>oKeRsOD1622@oGg!wOokc{ur8^yvN|1H`NBIG@5=KG16_W^vU&fRXm?t z_q`I{TMKqrHq&ihYRub)J%za4djEgAADDaGzE=f*@*W2#jR3JJ#D3@cZj3@CU;S@S zS4Z;11KSR=9RJh<-wqR_gd>oVk^N*@ZE*&=xM1t<55^Gus;Oz{=~2%rJ6UT_$rbg* zM@ASX{!T}&gbyfU8k;6SmVQ}xC#R+P_3IZGF{XimfrXFH56@}wbm!F;ZgOm;`P7>m zZvg=T0}~S~8=I&Qum*V3R*ZrG89_xwCBpLeY_WoH$deG@)F<1fany>ZaQS%Fq4`gz zFK1j_9Gz0X9p7j80asQGmzI_cOik(4bVPEo;Xj?2oD7z!1@UCM{hOi2CKu|xSvH>* zhU5$!o4*Dyq@Ya)9q`__xIPLjEid2yUG5p)rF`|Hxc{xt(AK6%9QF0jVcs+B1f%FR zo8z`D+l2-d@e5VKtR1c#o_!V%F(z?>e7rm@{5Y-w^Vzs~K4)vAazv(s6BC$@j*e^Y zBOikHSxMiqNQ3sN@-G1BgeU#PIg`dowJUzb8ipP?Vw7$Mxnour&1USc6 zx(Vea$F}DqB?b~>N8jH!1SSWMSn-Tv<%4ktxUBd{xrs3^j|uVS_^g4z@DOl5$c2k| z596N9`*|gpB57nT7`Fc3(75dKewgiuhK->rOBo+rfX zBM$#MAw%`K%?*2Y%^@aO@%;$MvI~i_% z_?L_td9t!_bNh5ox(V%_Zx6t)Z%63h-)!-9fcSs@X>LB9G%1#Rk^|camXw2q(343NB(MVaEJL_QV#;)^b4=Q#sF%xprz4jL z%78mO>E!$M75}*cIaf>|?I5!%emHk5GD5XYO8W|SAu3rnmY$v-31$>#lnlczwPkleQ%|LI)Wv=++fRr-6EVaM6*jmJv3&~{+n;&?KGf76_Lug#7TNcjbB2Q4H+`C~^QY9bx z$z3wc6+^HjfPxtXq{GBRlg6^o!i_?>y25A@HKjvE4F`M+b_5?*kD z+H?-Q=m&?~1F{`-b5ygq;h_dZO?Xj3mYYgGll&P_a|+Xv6*5&o#PVWO^)p3~tM=$`sw@d-E}yqfL^jM}!)U}y&k1vWjGMR*AXaFp=a|^o*Esv$F0ld3yCJyx=3UtbYuNA@DfOKtu(cU4RgabqJ=Jd^ z`U1uDl$$k}R23JqT3)sjuV5Ezrw{##_rsJpguKPo{IjKHSbMv0KNnkMvL*3lQ@Qx* zFNC}0pvkGJhPt}m8q?1I)(uErAP13&8v=X^%=%5Z@mU6E%p@V<+b2hv$|s!uIwU^^ zT7LiNu;s{#S4K${I4!-4-6V?3M}*oCmmeR@wK&-i0|#;>=+Q9J|sdY%9J zz#;338ALlAWM#{I%21~Dsh_%vN`8B zO~s+=okMxzo%XG(a8r0bp0;`}oBHlACcK(nzS+D+*}AC(iE9zg9(utsM#-$5+sPm9 zv$kFAL<&*);*~PqPN|udVU#QKe6RbxKI{n-1c>?DVhXmCD5|1abi0-~x!AvIWiSzg zpcMrU9$tvqFX>%GG%_3=v|E}!YzszwHbj+POd3xnZ8PS}P}dHLVe+W% z0RgmQM9{-L(a^!1gSJvGF`OImOc9>P@}JTq1s!2-_>0~%z@Ng@4%k&nXN)8pMAtZ* z&9{#ltIN->xEcL*#Len!es&h0x9-pkOv9G6?gpd%hdq5832zMJqu`;+^Cj;?L>L_6 zNT2mQy1*BlLobn&KlS18pa>7DwV>zYY0}Wz*7xA9u8DQu&B_0u-DnCU+LutKSOfFT zt=_(Ha)}nzO+J!VA3W11=b$alvoDO|WK-)ZGsp>rtKJw)p-ax7X3_P>He1Jq5EguK zyl)s|tFuUXF&mhv52J>j*^Q=cFBs#m*n=;t9CIDOxGSb5Udr;=oZJoNux#R>ekJQtweELn?43~_aI=Qv?KAn8p8-0hI%*;`h^vVY{ zN7No>tlsR6Ylee^131(ym_MxR>6xkrj+iKo*DaB`{ToDy4nXkOhh6m5n%J$kvQ8l( z<+g}xKn7~FG+gdrwX=2iyNZ4x{w{}^+)h$^KH9>Qz-_)H{eOIx#Q7WTWM^v^NW!oZ zd6NE|0UIfWbp zSraMs>U$ysBcm~Kxb?pleTC!MakJZoMwG2jwQ-z18DuVPVG3e9cqf0GS5FbM4N2dmA#)u11R2ltm>7fJ&aTMx2`Gpxd=>L{w()A+6qN>=XKN zxbuEd(r$xgJG*77By|i?8;g>oPw+*)$l_Iym|K%4%uu98V;Y-)9roAYua&$U{nzb+ z+#HiL0qgx`>Wa^I>Nl-A!dTf717mMfwn0nfIj?a6qN>_o-}YW0N6~WR-%|x9y>PxK zBk*DKTB#3?iH!6&s$L@OJ+uYg+QJ=f(oAr}j}{KXPgWWkqnle+oe2$0kKNA)QCz|N zk;c$y%HX#q=vwHx@A)(K=&X#Sq-uMBFW-HtR^yn%wQ4GiuKY+p+z0aTCP>} ze+iTJA67A)%bdT9T|-3*x)e7^R1HHoF7ESNDE`?__ z@XW_fWH77p?{LaR#NtcDCTJY=HU!1&F58XS%a-b2=!xjLK>w}A(=qgJpZQLTXQPs9 z9{&L-&W=++BEf}JH-lzCmN!iE#^`Kq_ifNwb-INmU?EE6a%Mf{Z2%ZtI4&P*b?xKX z=g+AoK$SpsPkJ7RDE6K5b+nvk6+0t8*K#^fd}3yB~vd&wn~_*uj(XZ(3VTH}&@9svIcS>#Y+oro7js8jBPUAt*Oj-4>2 z)>ZplJ5mc<(BAHp7(gHfvWUU?m!oTong`$!3%S!22k955rPYeFwC}m+^CQGBg$agp z3|xWs_D_tQ>OCLvwTh*)==eyn-~n=&_^ji&hW7|92M;MZ?M^*UZ1fwg0zCBrJi?&1 zaBMG>NRkLtzsBFVP;d=%OdJ^Z<>Ezjmh#)|^Cf7`%O<)6qnWLI<}KHv6-4)}4o#T% zE+uOz5KL^{_`pjwy}XMMpOejjljd9aZr6i!SK>7A3lja|;Wp8W?!XtTr;WXS&-^M{ zVv?DEh4A?%6R5!OJ-(f;p*wZq!B#L0N75TBHId3v`kX*^76uM|2e*)V`G;c z^vwP8fb+`QrDxyu`Ryr!$&*pc7o?F-#Y>)Aa@|9<<|8<1o1UIb{0!PGpsFVqqNE?w zjM>4y7ugjg;|oH%SJTcR;zO^4&&y8kzuaMF%xA!7VT{3i&{+qt47AKXVv+&9{P45e zTzgI9DZ#f2vX@4{=SepBXtol>krObn2y?DBFJAJEeoV68^~P^T{=zs#?W(_3+Vu>R z7&!{ybX_$wHMJI_Xl!fyO`mSkuO_&D4T^qy4xe#(>0NvDKUt2CkN-=D;JDv&?>F9A zJ4_-38V=)nnA@ZQze&L6W5f-*&!;S3<#=Wk{Vd+?2I7UaqQDvt(R5WHc<%C59+zBo zQdtYB)8=he-v@xYmNo27@o5$*e`LExH)9cx5$KMl^eJtb-phAZv;MN82CNuVWT=Qf z8e=_9{T{(`(q`b5H_Spap!nk=_1aSrOP0R3@v+kqOpC!eM&Y@w0HKf)wE&h3iR(1a zII>G7{^BY6DntKXiXJrc-i6)TkO3Lpw`c3)?U20jv-QsQhoh?3X?{TE*oee!z1X;G{C`}*8W{+wCRe1++Y8hVFWjv0CBK9x2v+fQe?cgx=^9gOuB zyVDJW+^f{qBOHXl{O%GJ3x3h&BRS=J^T|_o(a5!TFYwo`! zn*75rm)i~B3&)@4CD3=lKpiAu#-po)u4=>uCd!HBW8EEV@O0E;>YLH0n>`$80WL_- zJrPZYVc@M z1W&jj;MZHSc8H_z-{_n7rR$+~$S0ey>?WcR2J?dQmmk_iii_-e^#n|>o%M06C0VfF zw&F)6!#;7uyCF??wKk#dyOU2O0cqSV#{>n2~pE-#^fj-gZJUWXz$&5?u_Bl?n}+{MT4c$_sa_4Y#sKVl()85vnfB< z#z!a+1wk%7y7Du=3Jcx{v%OfDyA!p43NnrLY9a&9>f&eU=eS%?tRFUHY+kenf2kW! zlQVr3dC~0z5wo<4mtTsJ*I24e)@rRiBlZvM67ry%jjo)dn&D3EVdSJwi6$+|~ z@XF)EWY=a-zVy{#+-hD}{5SCUWHC4`^G5;+v^HO(r-wCK&|9JY_)a%^uN<;HN%^ml=2q+VWb{g( zW3-CCn!nzdzea>;|2TFbt!Lvwa^)1>WB5#oTK*G6#8(muRrk+S<%qW6p=pa_xIs9w5FMrW^D_l z=wB{;E*nZDY;Wq;U=-2+vi{jU)Lni%U_YVP;*-Q)`EE&2Y!12+Amx*s7ORYD-sun zjNY*xsECu9`pB>i(KD0l^Zd*v>ActOdyZGimjO`8>M7ix4t+Y|^wt^pHH4${gy*d5 zYU=gb6YyW&(Ejpp?8#CZk9ye~hSYVLRx6wIlKa~>uarFW7P+W)BBhPVy+^*praQRRV+U6 zplC?R`fPBhk?LLkMT_<$Vm-ztEuYXcdDwFfwnmpxP1}xIia3`)-M;}aOW&H#4L_&4NW1 zG=^&Ah#P!BtC41^zZulwtIyPrGYw6!Gn{ zIW1LVjp(Z4nY)!zg`}qa16Q^cRy~_w9#KYnIgR$owV8PEcbglpq{$+N%$90`I-Xw) z^JDuc-Y&i)i}TTj)n>;Ox$9~1ETXoEi7;m#HD&JLcD6F9<=jBrK%vkz6haI!NngRO zyIvXuYl7XJ+)lo;iWr7cnym%#Zyr1xok=V8YB)g6?$)DyudDnS5@8va0#OWv<+B*S z1z|jmj=H{~Rhl_uMB24Lr{CExFM#}~HS~!|+$k?RK~m%dCR;D&v(QgtnGO3c)`>6U z1pZIF62hpV>7f+eaxPC(`p3NB(?d2kwmm-Jx=XqWXLl-`Ri%94c5f>!w{mI3SPy=&(xtLwab18N7$Il&mT>(p1+e@AGw)zvm6ByVU) z&Ih_N`A*;DTciG~YZ{WKZr)W6go;Ku;l@qKDi|2PRSD@_JG+T653?bfIL6Lj+9A4V z9GFT_@1x@`zKHPZek(mu3*N)ed-zJv$4CB0(QiN!>u&G^!SSv9M^8Oeq)}gG(07}4 zC-(r-NoMV`mlPdwP;2Mdp~Ck_f`)fR>XqgVHwKluBkW1Rp^zFB0uQ6k8T+-Av0#pM z*A?r69c2T2tRvRCY;?XO#qUtXbrxW}}s%a^L?ZcPF>&M?fn(hYjyJEA!Z4;oU z@mg+*#26x*!=@}`t-VSV0Vgy>B5kJfuH%8_4;0Ps*$8ri%0GudJ|k< zO7SsXe{R3&#gmv?8eO6@(e@=uQnyc@`D34>y?2CJ*7-3d*?Aaed?CVY{0wS@ro(&3 z{k|9%ZKO(!lva@zBiNL3ijiRie_F>Al!H8Z5;a0>)3|aUVTDj=E0VxT&8lSI-8F!c zCG%1&w|+>RZOuEkhcSCCeeU^(j##?{dG?UWV3mgx7PWB zGH+p@d6~oGs#NeY|K*ay<-Q;Nxl~xhG<*I#7t{Nm-xZ?obfrJ;>A<>M-6!8{iofvv z)>wuhBh5`2#j_na<(Jdl1fnwC%*Qwq4JutiU;0 z_`KP;hZI-Fn?0ob7FRyzLZ*DCOrkvsNgv2|d7usXuEEZh#z#+)MB;M2wZ|g)+zq)J zrtJPQ74LS{4a|Fqt_s>32lgk8%CURT@)d5120&t~qd(_)8Du zeyW0S-Iwk|no4pwj&r(Q#oB(s+Ap&Omd;-*D1xM~h7N`;o36VTb<$b{ZYYrUJ%3D{ z4#%qJ*OFG*`eD5`$^~goK`U_*e3to~shTd?SUWuv>Tw1Sj90(qvRdlp&6SelpMry4hnlWMGs(%29mh0$EL)%fX%D$X~&DdnV!I!n1GJtE-FCjntVgaZDu( zG-Nu^z|71B9fncRHyvz+l+v0(@#iE$F!m_u5?k2$|C)5nP*Kc{J z?=k+1w8^Mr%4NlSYNHSro`Qt~(F*ZacXCkLy7{pc%GaOR$4Qht1{HSjEv9q6(c@YC%jMRNXcB(nLN_BzD?iK|s`E>rA3pH5){AKW0| zUmuFg-jBNeu+Gigam||m@kC0=DbCkR&f>~Tb-d>n(bSb`Kx@cplHDX&$oJWCtIoeT z!C1T~HY;`X4@U?$?Z!Rtvd7t!z=%9EXTUP4BU#i>`lgT6s5ux+a+#umzqJz>8wd07 z;8h|r0?urSH-5|%HhF34&1-SmGKY?YExt(V*s2tMKbnwgqolpN)<#TnC5^iKGy zcAM~4z7jV3F3<%TC1n!dd&pKNa^3WG?Oj1QvvOFh8e%0G%;us!y>k+(l=pQq-Y!Ia z)c3hU;2;+3GCL%`-((&?(+bO2w|3Ak%DghWxqh0Tgf;Gp8uTQ7b#fFzE+TPo5B4UL z7vY20+VLAJ5(kv_|H^9A-JuV?_)Gb(m?K!}NsJiuX)BW@DPW=arnBc*ILCLx-(TC! z#U(Ki4mqhgF(mdVdi}(vy#GEtOR%!C!mEjMMiy7CL130#aQFsBDfc6!t z*nraaWyX##7mvx6u*0xM?p+p!q{I`Rzdulmz(28w@ud>9dKR~fj}i_3xAvrefW9Fz_!`8{ z*OoK8xssmZoxV-?a~ByzVBot@brrj)@5NWjnO@Arb5FUOFLwLjTVD+3RtV-LC}sKA zIHbTU9`s8tBLIZabns|RI+hnatjm64;P7GwGt=}8f}Fpky+#YGxjAk8L@Lasj$UC$2<}hIV)_&-jL{Z2x_pe!uZ(-YIiZUZ1#45 zqBx;v-c2>Ghk%{FP9R~VqqYaSbbu>_&Kdk*j&R%6kmRv5i1r^XH8!SPgLbdx z2r#=J)AzXC8#R%z=gEY#+liGtf?e7Al}G<{xB?0%@VqI&x|4%A{!c@aHvrnXmTqY-up7xP`V8@*X``w(tl2v%9M2xV*daD&jF~9& z<3bPh{XHY}a0A;_f06R;hhh71ji@19E8y(Rp8g5;E*s2@75dd}eE|;v!MA@K4AO*%&gpI`>b+)(*MO zb)*u=!^$Nxn(U~I4W_DLW`Yehqf@hfMyjxJewN~%p0W0iQTCtHRQG;_a6ErXID2D5dX<53T#Gw z5}c6?I%aW@u1=XYCwKgx-G0y`1Zqukfbi^O-t%yXGiLrYWIl6zLyU={_pfEKXcG$Z zj0WVtK0(@d6Gi`kL7fQh2&?Zt zGMURfi~npNt{!e1g`wk|UzNAvynv7#raBjIbU@EHFvZq_2o{i@`o*s|tA#7}2Ee?K9IeSZy+q!ANAjgA&+o;-`WKQ_4HGF0=TI$HTyqDkxb!CWsqV#5WEVb&uQm?)|Y z1aNuHij&9nzu9O$Ouyfly$TYSc>P>^d!s4jVt&cbk1$U-ssp@?n0FbzL{~;BZ2uky z-;v$$MkzNi@**-T;qxBCq=%oxmk7q| zn)`$Qoh*U&e7^D_aWjOS^6BxFni8%Sx{UohJd6T^*Vbf6QN&6pq*_-edDG!C9`JoO zcsqsi#oPmyeliq|RM7ZrtHOVSE~0Y2rU~Ry`gmgU`uZTFKj-kK>vak*|LHPAexBxW zujwnm6jYmc;NskJx4Nt%VL&<_E>JJLsU)lTAEcAV z!F7+(dr>$wLJ7at=H_NGMtCfFnY0DSMsx?lctk~2h@%GG<0~&kUGK1zLhIfZQ+D2( ze05;_y0=9(di)F*<6_*mtoWA&W7q{D;#NDa+BfZKu=%u|oc?unx?`AMe zupHsP(pokNTqYGt-T`B6z{(KC+y+0-r@J0KK*WfK5hTg~y7GOwp?E#KPX~S+a1Cj~ zVfLb7DakSxTf^^%dO?borqveNA=P<_?%d@J!kGtS)nwyMkfgUhE6M}~9g6CU1KM0j z&f>T8WUWhNH9DG7Olg0*2eC|Dx1P+^$KU53k+K4D8iJ&Xo(>cqGkVYCINQ*d1T*60 zGcXxWg7sRMFi@t0j#;m(&gEvpVL^rfiR(g~St!iV%cWcuk5dM_Ce3wnfF`vpNq+VW2_PB!JoUngH|2-ebotdd91 z`dFo~vMFeYKJXXBfz7J*8rI?IcpoT07UvF|O!jXwju)MrvTY$2+^@HSOuuvviH{}< zhPV#?n4U<5d|7$8C(3+OJJ7M7ieV{a(jt(!=vo$Jorq(txCPH(bk`1KROwR_gq){*}e1K{&~;_$0x&hRm{G@^@> z6Fl&v!Anb54R3m#^B#V4ixL`r6S<5Fpp=v$^r4+zp9p32?o1!( zfG7S0!k@`jx{`F?*^tTURU&iO9NwwCv!S(YVUExq0G&NA|KwRsUJHGz7!0upaS42K zA_#O@p~+245XFE7;rC@dW{~=7kP5BTKRl-MQ+gNgpxt>Eu)NmPa29<)udWM7Fp?(! zbKaQQmtzW*noTdT=G<@`0*ErEL|q`WxtSjhDl5kr zB`>92kXjISp}`mL%0J5Pf0bdo@YqS{>Yt%*hxU`3MWbE(F(oQF`h+-I&1gaKE*v5IPSKjB<=2fH& zG*=V(ZIdF`UC*fzgsz~RoO;x-&w)DcK=cgdCFJ;iy z^Z`4b_NMgF2IiZhsPj?IB%uwBb_*ybk%^ds2)X54$xnw|7F;}H@mB>7eT#Q{&9yJg z2$w_ohc+F!kJ$rALSFwgDPTA#Nb<$sI)BLegkgtm{Uv(Mu4du;o^JI{4a=SFI|Z$)4y zq@_VsY?dRTt0FO*Bo3u1U*Z4_@5$mSyMBDLVUo8Odjkj{_>;k8S+McK(A?Sd2RqN? z(zxu__+qO1`m;6gS45wlFi-gGm-r^Zi$PgAhd|DFBcp~8C-U`tziCZnfH-a zOC&5r=z(j<+u~!;i6LH)H=@aN-PPW0@VLi{b^0Me-Z}D)kWYK`R`$q$Cc<6j+pyF!z#thKR&_O6D+8v#K0XNCAJ&oi7GSj`ZR}%c8Wzb z=|feEvr2tQ2ioj-Ai+4{vsw)AIml08&Ny7y%%#V%AVp_g@vXqn;wvYa*dPL*5>6)J zHLLb&`)-i){PO=tTMAr%tg#ltjSCr{N>AX&?V2`5BJA*d2i15Lb($Lt*&Loc5F3YXV@j|hZzr!$IeUyzFisM2VfBH4T8Tc|l{S)@5gFViQ*D=Cct zr`p{T+cUog{ve2QL`!hO1~+FLKV(x(_{{)y&Kj4J#5s^LPbkucT!>P%fxR#EN}G=UKZ1#=FAHUoRhYAAcpY#hYmB)xNf zTwD^0CcgTPvXZHC-!AkQcCWMAE^{B9I$7f;I4zYY!&DX;h3v8SD%(SH%TVVWrU#f4 zi5Akdo7*H8g-gw@zNrU>l;^RQ>+`-(dXgir!u@ux@yI}|OX0ciL!3~J;FkMkM`r!l zq0{nsjVltV04oeQl87z^A0J#q-rn9G9nW-4%?)^qO>W`sod(xXTk*JODi5>>@F2-^ zG*LyfpsQUCCb5&Od-2CB zUTUi&7WWXbT;s?>;m*+>Y|MyVyhq-h(Ci*DLCd}vt%QgQ-P=#CNU~aWKQI)h1 zV5Th0TrBU2nbE^UKHa(kqzQN9|kwA?9hgwT$6id&?^rEG2Zm(lC#ysj$7dPHlsADut z%_o-!-+|U6aA!*ZQ^~S^mGpg$|AIp7&DLi0gO`ucG{;9Rn~grh@$sF!xk~Ema6;ct znkcpJu0z3GF7Bk&b(|G?T3Tksg&mi!D@eLyb7!UgI~5#@(lI>BJjL&9$^kuIbn=M% zM{z$a#HiKqNrww8(-en7+KhS)60MA7ysc^aqb08qv~=$`pwDK`0@=cZZR+BO=Ia1i ztwFdJ&XZ*qP9y^kY;E_jEfAY}_7{nk5h^d8f<~L+8hm7$Kb_qJgC+5LIz{akV@9zG zt=`HJuHSk&N5XZj%HBU&I^-1MinVz(>zS*bW3ugP+yDF^Aq0dSj|7K{0rs22Gj7UguHPv3jhxmpuH>_XXR2ckk6p8{$HE+a< z`c=5?sPcl^n!jfDO?karQ_6K{>=<*&q&0aCN&+8`Q_lNJ%-{zxCJO7;f>Igm2YJitnvo2(Ensp&Ln*6i0KPZKs_4zehC?e5aS*Bv|GhtRo zT59EEw%&$kO`s14Z1ah+@LE`D9dQzwGybu&B53M)eeb^-aW~~;hzZlT;GQ$^%1!0U zULXM!TozPK47-0aYp*AP=2ST1E%U@qXYhe2{9pPwR|1|eb}kt9CT$n*Kf%l)#X&7Wkp%CssH*8WnurES zJ`_u4`7&E;4n5wkkKdNk8=4D$CNlFK@%J_fFyxVqBuB(#9^{6c93r~8vFGkM&*KNg zO)4=~%xGwA`*el&S#5tk=v2A2P{n?npM#MCk9X^@UU$gfYBChunBoFc-N|{B=9r~) znm@UG${f5cP%63~{H`XJvtaP_F^719!EMmV1SQa=BBv8uBzbb#JziJuN5DI%(}_HF zqVop{Nnh`icgVX!QA#IgN=9;G-#T&R)5?m;Vu6#^f9T4MJ)v)0)P*acCo7fHQL^qE zCKslTZW7*pJYu*1NL4WqAN6%O*0CX{Ck+AMNaVK+%@K%sLbAjE_Fx=}z7T)s0vwaT z{rfI>VK@;Ez#E&I{%d4`SDGLV;~cw<`Ohbr`~V{Jp#PdA_TU{6gYFam)fC=JoFin$ zqg*%GuH=ZjlD0GYPvm#{oo&)F2eve8g~=R8E_w4q6w>zn=glJ7)aS)qe*M6ZyK)Nk zEM}Ko9A5R&*Dj`$SJG)V@{IZDWZ+p+psI@XRJ+4kksEKnk? z22mdh?w*dQ%Q8aZdXvOfysu~MVxO4$=}!=w6-3YAVwk=85Zjp4rDIXzcB(s5Glu0Y zeq{3?35O+Xb$4@FBcJ;ZWK^ezZsr+OJ6T*COT!vUi!oSW7USYLBatnUkI|NrmD0;aRNrulkfR0@O?O1@WO5i z!x+5C5VR~Vo>$NgND;zBTJ&d`A6(2^>OY_%>|_MSt=9pz-VDvkuSn2RQX99k=3DHJxmYPKC;B>0#78^oYj4idrRft#W)|g+nY()Lo>u_ch@(`|MD2tIExk2fhSF65djpRY^GX_#AV5= zY+_w$F%4f3x~1OAE`Afwjje1-ytcqJ=;pO<5#PYI{AKVUhOP?f?NORXndKbK?Cm#| zG|T{63jgODVPql9fw=(DViYROfweC-GzF9|=h5`)+QMiP2*}%04eCGiLZy_c?^3@g z8D=6_VIU{v}a&Yy)U)g~dX{%G&>AS=?4-TvYZ*+Y)C(K$AK#V| zQLFGpCsE_>$QeUSgdVFDs;NtF9z8GOPOdQPn<&FlMfF14t&bg4}FonpEpAKdtKzuYB^TI(tyJM5sLNI0DrKND6emXmixys@%LWo8zXl8N9t0;CyL~ zd%B2T;?kNmrgBK8npn&nZ@#pTp=+N6-3_jaRb>bvW%;8gr)8hm zG-eYys%1eJr7nt?`eGoc^x63W`uBT6pvmwTHAHskONgQNLNXo71M{SFnDJ9oN)@fV zQl80_xDejxkRTn?j`nE7yfk&jHkdIC7MM4h_mbmRM7Hy6jJV9UOUb(Lc@MQU4O-cJ zhSs2Gs3xp~IY3X^B-sllDWK;?$C}QXtmA2MLFNa(C;2|7!+3(wRKbu;-#ETwtxiST`qAFW_S5LKVc8PzA|{*`F-% z5C`uYgGMKMCJE}Vj) z?PHM(!IP)WUj5q~Rcw^X0*r2cF><;IZ)2{hbJ7|~ZcM>+)Z2Er40^k@!|?`To~k8J zxploD6eGJRT8b?<26dk$G{L?DMF`pFws6gx&cCAwZgbO}1HoK{{2E?(Y^ zYu;$gv-#~2Sh~|ITkZ*U-Eeqi(G znU;hJ&?FUg8l@~{dG};i8O9`(eCF#?ABqnzSVX<@w8=J|m7yy=G>K5@Q$=?|BEcef z|NcGUZ&8;Ny3uxoDZh8Uwa5Sy??v6U+fkS!GARO3EK;_1wp8`+u98I69BMeZVnb%c z?EzGcckt`-j1Hoy)$%s~A{_xzcdBIItU8tT_a9&mbPGt!dn)oxt~HRyM`8oblpS-b!DckB;ztJ zfOc$oT70w8dVBX!nNdpM%vjqvvzj-=kg|l_P{VI~mxyE}%Kh`06jzvHgkI-p*go<{ zx&F|C#V<-m`3o~@=&u1|@$zqsO-Cgws)Z_8CgttA)oOL>vjA)m8@{CJbm?>&D}R4s zTNKiyk@nW$VJsh)vHxYqmN1JPD~XMy{n)?yg8 z1w)gD<7bw*kxl+CDp!Gsgr;-yO7CD$kpOSqCrvs5H!n>OGb^MyjAa%9ywvv{? zlTPPm0pVt-U{Ug~V29XF%fGo}%kfMn$V!Cw6SfjE4ZTP02UG&Nv`8ctop5e|`kZVklSuzDo zKF4dRk3a%8bRvcJFV_>Eu6nSHGh&45zZLnywz>ooP?}6#lpN{CoI*z0uMy>|3zTRl z@V{zXF?7{2u_iZtjF#Z#5!Yv#O2gtTz*Ejss~yjLP?RDxDojEwaYF7Qw`Y~~m0M{w z+VI@PrAipAN(xj_!BDZ$E+t`^oDr6r+Ev!3|4{Dsc|vs44Qo1Z4Zr(k@JqXDy*&!e zL<^!wE{foK!vxOj(X-pAdX~%jKCZcr0%mb3y)z5$JMYn>jzqQ637xG?^L*ovb(3l& z)G-%iF%4K!WmhRF!~#wfQk-pDAL0GsCH7gbR@N|H2f!R3d&lml%-mdSxJ;-+GBOHD zkB-E@@nO-!n=bwREG-Q(a)#ztnZk8U)Ik^Uq@rDF_?y&DXQ=tdNT^3=GJ}94@Vzvn|qg?R7z%_m(J8LQbFlEjAJn7 zL3AI>7HKJK7daDyZqA{Ub|Dm&0(5yeb-e2t;g}^;{g)KiM%jAzKO~(~U|n6;g=43U zZQHhO+qP}nHkzcdoiw)97>ygVaq{o?`!CMbx!P-=v({X5j`56>4UgNtC)FvwFU^0* z2Cb3d8Avfl-?FTAsve4#rO4447a5KfO1(6Mx<@PvV;VbfE@$O}RkS=?74n$&Bh(NfC^r!i6;H zYn76iq8wSoY4kpHP+6XlQzCmdekAM1Q>SCyXU(LW!m+4)k5rOfQOhHBqe6DxL)p~T zjCS^)bWHqPu6JIGEK&1DZG%>@uOLn~cZ)&8Fp&0Pu>NPfi?R9OCb7&-@!oU$u}XJZ zr_AH-vo^%D4(AsB+WX~X!b8P0jZERMBbk%euy4S<&admn&A*5yucWFXj*%qOT_azd zUv42MpNrdKnrkD36UWPjE|YeK;e*l~-n)gdsx7zXjWD@PgR@}JD-Eq z_DsJyaQ8;BM6TlNV2-8fPEW&GjuK&RO6}D?NjfhVMXnN&ik2$GBei!L<@xIm&#yOT z*#{`*Q0E-*hWC-(OSX=`sAFEAj9cKE`H_1C&cB;#=N(~tlbxS@s}XC7tgsbYNZ3@Z zEj@9KXwf7ZGxko;a*Cqj?Xf+L&GWEAhK&Z}!dBWKtG&{LdPvaLX1(S+9f*o)*UfkX zEnsGUf@aB4Rou-ErdIeG0pLfM))1wt-dTYZo+pClnmL54^H3Qd*nS~CDi#rb!=d(9KA-kkJStOV_& z1O-%v&PVB*i*_8q1sG^Wf(#N7Nf$%n$bnMC#1q}z)k*NdQeRw2nGYzY54dKk&={yO z%Qiz8O7NxJ_S=e&NW7zT*jSTHxn-}u<bZ#0bZh>6|E;^!mO}e z9lEq<$a6fsM~zAq(vUy-n*5=y zt%Y+l7(bI7DEG?~36EaBZWTJN{&?I%^VoM(X4>SkM6jn%&b%^BCLz{7InppeXzNBw8h$qTE#ZHQjFPIuAS1CH1^MqyD^v{a?~?!uuAZmfYse%r z=^yK2DX=K^D8c3*n%o)UQ_dgM!h+bTu^QZ5cD?X-pS&>ddD{ggpS@$h90femPQD^}_mVT`ZMw6*08B_b zM4K}l`Z3P;J+;)%Qo!=l63%%g=`G(OI3b+g10=VbX z(!(e7)++iJ(>%N;$W4V zbe|4CxD=Of9hT&h_1&S=3GVc?RGY`R7jn+U&tOTRh;AfGCw%iB!>v2;PTaj}`mi2o z(9?duei>FScyjgZf5hC(CsbXa`e}~O&wn*8uqlHMggN;Q?V=@(K&!0$cr?ODNG0={5D zExiE=-7jT~OHX0hzF-(fg2}^9bR|y;-8=seqa8}D{G0xrm>C+=-FyPeN6Q_LkH#j> z2xN##wbPB7aaR8=^R@rf<9qj5hf3kpKiYsw#ve=3USpC8Hz9>zaPg9cP*llT@^LK= z#~cL?@#n$>=b*4Ru@vQaZ?RqkLDZdQ$`I%MR_tZY7g!*`{SB}a|GQWp`VUcac6OG{ z3R1d2Wl8;ri)y0S$@cS4V6}aNMs|S4<7-ZVu3=Dg3`7oQu5c|+f{#Ntjw%9UJ)9l# zcMTcveRt>R?^rh)X@p={;mS3`y=bGA;xc6sd6rdReYosf{4&d`qf0tWNN}{$CCwC+a?VQ*2ud&By?Hc2n{uQz%6ZiRe{ z+n;w|7D#*c+}oGD_R#?k+vm6)x8x)Q__F9v_f*j~p0+w0=n5z7YH?dZP9;GY)?nra zPXe89aDM-3#3sly8+@M}0O!=mXheB@Bd24qpRc0|CINn0yPQE7JGON`eDTzvSmV0JK zT#d%r#({rFPVVy)^^BhU#L%RZFYffNg~*<o_7WK2NCt;0m}Y_Jvocup@D?M8=rG)8# zTF_<`X4#63phfOElm#SH-vb+%e-R~^Cg5=o3Vz)S-rr^R<)Mxxrc%t0tlYJ8bfJ;y z>AIQN9J$U$nVr$KLWbgefIPZ@yQ>&=Nrxu?US3&wPWn+2$~bo?Ghl&-OHS~kf#uI! z;1P?#^Duc|NT0X2V>NF3ikZOD_C!UAt<&{brHhOGG}!_Qo4|v~r)R(e%9e`HGm2Vx0ddBUMLpCngC81G_I$zMtGyd$C zAOVh6Z0A!sZika(>Md6>r%cV$H73Xp3R?ZYl37(=8aP%o? z1Up>xy!owEmA6xugRRb%K3om&YCLS#d> z*>8^Gdu|a2m0M5#PuZWpj@J*2Z~)QPA1AFz(2L!`BDNPg`}Cy>&j)P`=%boqKA54C2fCd}Iv_ zC(*Rx4+ZG2emAvU(r)rLJ3JBPY_(|O&OZG+&uC?y=l&ikfYm)&Le3r=Sckf3E!p#S zAo(0z_=UpTcWHys1&Rz+3VL6sXzEX9?q*1SX(}$6=6XU;EQy6S+DJp1)w^o1ljy*- z=#oaBHe$X-vddw5(%ey{l%bsSWAHY5(XL~@Zquk3ckx23jZ=cGegHc;?>GMphnSG* zTe>h7L^Um~p7RJgVEND7xS^JIk?Y511<^KaDulRr^bu1Ycghm>XO+!PWh|wEWYqZv zsSwR(&L>q&sQc__ROF~-z4nb~!n&s)OLviXowKfT^yHkxk#pu}Zg)buHIqUwOWZ_p z|2|&~S6k0l>9E>~^XD6x`Wh46Tp1h1nkzKu5MD-}F-Xk`%ijv5i1+c-_SE6%P^5FU zbKg-NJ*a3t5YNr}CD{?&pI*qho#*5kHu=Bjs()3e>w&pLXs1LP=7Ks@#eU@z@((`s zjb6Ref}5LSo$Ty|oWB_2=J2g55wfh?#4-G2-7V>faD7kg2C_5M?Je=*uUT`AMZTC!pbM3jw zHxL3k+Ie96z~(k?UY~XbX>M)?TvwfVeSrW$s327_DMvbsxRk}LXqj#3Fwb`fNCs!(Y9TIB=wamcB2(GvsSb+?QT8$lN#s5$qIQcr#z;eiCi0vaE5j?eqV^;;ZLK2 zci-&JkC*+xOUysYn)7FCS$!~@7j@q==qq^`WeS2s*TO+NDf%;PH+Vhw5_VCj(iTFX zmzwN@lVpEZ3ML|xC5bFflV;W$NZ1&G&}r07W2LTsadL3X=7DAv7FGa_%}Wn!j$9DG z&-+44rqd-m5mT!P3mIOdrP}g;N;Cs)3c6g9BQwJ{z&YVmmzz50L7?1?+|Y8fx31r}ZaAhnI$SCF z<1&g_ok+>J96t>;fKyL_)$qN`TK1|Ok5DIdD|IFnTj%kSlluC_YMkbm)G{r}U$%F0 zMi88tXCeI(=AoqVWlae!h9!RAe@=jA&U<62pgeUVYylW=L45Bb+!OW$*ANy;^gAS7 zUt1%RzjVOyifS1|{mJxjY%_pPeWEh2^%19ubE6xu!Z~sl97x7mXl_@XBVyW5i&%Ai zYz{R(_o>dRuNwXjY)aH*t=r0aR@HPUgbqRrU}V+lYCeAnv<~`FPV<*vASmxm7}>i- zmo3^2f%!Vg)3kP>UdbtIr5(u7O|sv1R+&@Ybsut|axpnP84~{33iKQ7{`@$a_8?KP zlXFqC@VG)M1^^R(Iq1euD!w2cdO|F8;lc_XO?!VtsIf+^a3@h;>XglsU^hXw1-5}QAvd_w&7LqoSUY_JRHSHQ%=rraITeAL%O;b{`rd**zwF;^gae9|%A3~= zc%(U+_kLxCt`R&Bz9}V!k5LlR&zt9%d!v7U)@3k0rsB+e#Oy1ohFXjh6U~=t4eY)7 z^0&$EdoJ~Fm~U{F+D4VJv9Q(O?m!)6sy)Fp2iMZSyA`tB{;b47*@2s^~_|t2d zu6ourR88v;3#YNgIR3XaGrWLa*uY23#6OUVJ0p!p$p@NLTVF~wn|)lXhxHBu)89WU z7zariE7oD7&vY!RI}&CdrfhIVZKg=w<9-PsS<&K&wmv9Ja`CKeCdUhxpSQS=>VfFK zq>zOJdh+{8&0i`8k5!s~0SXTbKnw!*083-zKfr>_K)}`aoYA~ING3LcUf77%11{yg z(vQW=j5o#e-zouxa@YrFPf&+D;T2G7zy4rjM_s!uy9!q7)<{ed%UFA^h`47Il^7SP zfWc?I`-LySgWEO^_Ag4jl&B)EPCKwoeV3IE8ip>W%CK#~enC^;f0FdEpPz@JrLU@5 z=B}w4(PC-cSK003HMc=yRoHt5+8K`$1y95Ic!t=UqXcs+=|ZP%VfOd*JI?pU0XOM| zmVYPU^=@uk0Pwnhvc^K>$x#3#%7(T! zpkKlRRGau9nT8(NhOZaQCVW&*=6BEhM=I{s%}I;Z=IJf0Xn6IBnJv|41}}7jkV8%d z!Qm=nL^Zw1R zLGw!$RyNK@F4dr15^WzLJMu7F+?0E0>`Jxu=t z)ARZh4&XTc(~bc=Rlr;QA02h;wNjB8Q(pQ#=$r_V|8XZx7O<>VJTJAdj0{h|oHMlAmHmDeX5XcozCFt#8rZ`d8s2t+8YE(9o?Gt9&7S`4|81)~;VxwBv^107K z;SA~0c<5IQ@x5~1X)lv8qHFeW1fT8o`X8>w=jA99RU8?Z{YsSH7Oe#lw19T(g}?*E zRRA~I9QYOB@ax6mzfbQSV6(l4H!PBe8l>{LM|q`un08_DbR6%I`l+T{L;VnfCBrGw#C4jN zrrX2A6^z1@s>UT4+nC^3fzJPCW)5G`+@^7ugX(Ep2uh6jcF#3CeP|05SbUO(sFnCP zh6JKwNpOaBtl>Y(7ttn36d6t@myUtFB!jeuFEamuV&32RfCU$P4i$WUz4!V)3Swp- zz3ss12=$t9{rv5(Telkz$4QDcX4)f`ISqwM{#j05OnkYL{R#x=BRA+e&Fw)qyv0T~ z8+VTUJ>*6AT<1kG#%jj-g6bYTpNG?G(Y{BMjYcOKRq}$BdY1=SAtO>^o34v!(&a^m z1T{T5D57fGj*6F@YKl=hT{g|I^`upj4%i&0V9bf2H|7)mRkMJU6y9%QTgL+HaKP>` z#Vg>KbkeuKG;SI@Qn<3DNI%xPeoxzA)H%#O5=ed$&L&QIs@8Y)JDShtPEyF<-S~{} zXx`@jAvle{96=O}O*iQWb>ugEl!kcWlfpm&O#^Ro&Ml{*?0b+V86h-rZj_s|>iXm7 z+XIA14a}m0rtC8zHWG6Wg1lIJ!X#t9#+EJVvjlwIoD(*njaZAjVlRd;>UB0?P%c1p z@`r=l9piyAXG**>M@F}SB^wKiTAC*w;-R>f4CBP!(`VC&O*)O}2D4s=?s@od4@>pZ zZ#bV(gCe?>E=O_0#CS~l&j!Y~0H}Y-dS^x{KXD_Mp(kacn+=gEq(`Cme64akIW3eG zSye_GAz8~9j=#=jFE^cF!Gr>F(}15QX+0pcO#eb)i>AW5z1u zcK5VN+hH>Dx1#IuL~T$gzO>OC-Do&nvxP{=UnRAg8EojJsCi5uS0$kF(8r`WOwQVV z@jU?B(T$~aG9GNeX2RchT;oQ<>V<6) z?YD*|fPi*=h@q_$QU`dEoy1&#M(14qpSldxhd?a`8EJXPW0hUiC!yHL$>Kwnkv}^A zOyZ2kdeH?u=2fF+1q$NU;6l?TDf(<2iv+jln?LE^7k??m9KMh`m|%ISldu}QB1ldp zzPRhtmZQ_eFxgF*Jh))qUGvNGO^iG!sw_6XO_OqKb++ai^y9 zDoRNF_}a9e(aAM`PyVX_s0Ua{3Rra@$B}}t35@*x^Ru(Hp{D9Ca$c?S23M!FmH9N~ zevVd?qlpWj=pU3$xcMo~Ghp+k`d3j>lN-a8s#GQ)b+Lw~Z%Ab0`PIM7=G#y8+hf`av%o^~hCpY;njH|&rE zM4RN3$pO*YoE17IGBVY11XLgd`=oa*5)nJr5HgV4I%mQ+ME(2LFWBujD-}$l`O{g> zlcEOZqAhCM<1)#kq@2t;=@B^z-K+@8zYe^fW`8nCBU&WsoL&=!M;p&$VC96wHKtiH zR5UFic;s1)Al?b;_OI`k_sdKdzAu#q78_VD z{G&WT`RGZJYb9qYXKdKgXkiew#zPiLq_Mjo1w&5pyHN|amlcgcv5^?Z4n-#T?lP^m zT%co>w}Z}TYRIQAz!UH40wBp!6-;rqP;jYB9no^#7ZsD6 zN2%nLVC>@9Qx#aA*sF0>)oQ!{^gDZY>dR2FCdRS#UZ`tBWAT+1Y7Gf`-Dd?zt8#pozb~#m_e0Px!G*7A zTUnRyx%8jVuQNI-S+fXI5*(%Mi-i?|HZQN^L9~Wsxj~s`OLHr!Ark!@NIBt=A33W% z5l2VnAw+usl)go$mempjXnUrJqV!r2$?vbR92%81Pwl}{^K^+-@NS&pu=(A{mm!PF zgr)Jw%cpkk1@!KelA@BYBGQdSdh@S&gV?~C?UtkQ=g_uFMK26VqFWdQMe-M^!PBtc z%N7hUZe>W9G+^AXjoAS^^#P7h=hHmY`A6Dt7F=L(+5kU~JevwRf-?OOlDa>n{wDuV zw2#N^JneRT=NHKP_m#+RccJyKY&<<95{AVd_L?Bd)bp2KZ zV*kn@^4Q?z*87}^5bTxhCs?9~Aq-^3s+;XPF8j_+X}m!E2q;@X3!+S=j5h1iB>t&c zCW}7JM82`J|KH;$(0^YG-1@hnctrWb>Nb5w<-Fo1q%|atF4*3Mw|Lr3q^@V4{xfq| z<~i(KE6etH{nN!orOI$0w4rKUuzc9q<%RkBgQQcJf)_zXgjqN3DvibU6b)1yF||U& zdW+WnRG@2l3h7k`<1D;w#r4Oi&jMn2B>%Kb=r#}~z<2FEcTf_`U;@bS=olCZxWNIY zyoiKS2f%)v`+A=P2cRex=2praAL}hoSJ1UH!SXl`X0f${0zyk1WqTM+{Y~ zXouwFt`oMI&6Q@+%TpV=ibDJ-sE?>4w3+t`sT_0DjI-Fj=b`mq0x9CsAE;Ny8D!5pqNl?!P zOrtyzVk1?g;iTJT5f>EG1ThO6@!oRr%( zN*ai>ZBn?IhpG@HQ^#k34WnxPFh+xkSIX02MfYaA=H|wWvNjp&%R+S~EE(OPgqC<8f6DOoY2HKqeo0CanKo;&n-$E@e4T3pdk-WgL)bg}x zxXn_^E?gL{Qv|)50;1--ilpo%N*aM0H%bx=7_7@x4<4=>=~)@UB(wbTi#p^u)tjL2 zu3bORQbNa7iumUp;rH9Y!X9)2Z}dNMxC_fEZ59$@W^j61%k#0^Msmar$)J5Q(>=4S zOx8)@KOrp9;?py$ecp<@E3DzHl|Tw)$RZfXn5Zh9FIQi*I8X_e@>N)o{30%{*0&LwGfTP z_v}X$>0P7s6e`i@3(_A(B@q{ zo$(+s(9*7{2C39IHiAp(Yx3s2aciZD@R>FnyAA&dhA!Gcz?h&0!hkbB=sJM@z%b6@ z`L{`?&cN>&0soJ$V;HGl^Llr0K6*hdp7%f{&pMLtu1S2y`<~d~c|(!;D^0S&zF0I$ znBDBRvgeb8Fn?!x2LW=s!>m36ELj5{Bp9x!sV_qlUqZX?$;p*gyC8FeC36WfjI_-p zDj$N@1q|&bZk=RF9EWluG%-8VQWI(wCxYIp6*;ErLhu?hE=m^3dY1ninQ{U}Mu|Lm z&e&%Gl^Av}$yhDcf^I$bu-PmPY|t^1ms86aZvE+j)Sjfl1`+ zKS`KMyPRx@fVpC=OP*65TB~gIPgh*eyZrf=yS*?6k%o|9%}eYGIQeSe0Zy9TEc( zH!wtopLdy$(uzJPz;Ill!qtKs)UopjBVvdc2)F^FYHn&Q`Qd{ss>P9nR)zQ~izlj_ zZmgj$ABp<*E6>p72@KZd>?+hjATj&Gk95x%``#&jqiZaCgjKp@j)Z<$2<67ja(@?W znjcn64)Q)nK@1b_i&< z$3@3Z-IkOOcXGLK>JLbFZZahAFB7*$X9@)khN}$Rn>OLRAJS#tj|2%0!v!CyJ3eJ{ zT=7Yy@2{B?OtwAFtrxOS$wP3#YL)D|TSY|EBtYVo^!XfN#BCZ$g1aY*xzG)3LhnLND2B&VKpY@tZ7ELqOIQ#R<`9D#r$4`RktEhbnc-{za({g!JO_!lpWueZ8K+)|Zhw$H`SK1cXdL)LzT zWY!g~lTAq3fGR7<&rw8HKWTp?0eQ(Ndy-o8spfGuyN7T`< znW@T4)@>N2FHu@%@cGV9qblwDSm1n1C(fE;Ss<%PDV(AZ1icE@LQN_scbA6qu2LV9u zME!0L;7>1Ay|SMH>|gUtF<6Pk;IlszNXuGOh-w&{)s%FW+uuu;3ZKyJhO0-aGPZx& zNmBhmH zgk^>}AsF!Py^#4&0*Kjgd~zq3al`l7_M4%(wW%=&-dQ6vJFrxe`(0}&zANO-m;3Jo zd*SsRkafL-^a0K676z>mmWuayLcfPoy}gTJlv_WqcKOQnpYs0EZyNVX6bsSRF5UE_ zg;qC&;e=Vzm$br}Cee(FtgG#Dxypc9g|_hmVMSnjgkQIWz_7(`vn?KoYiY7wt^2PW zC*YrqMx_LRYXG6n`LcC&qoaP+bh8ZuT!?bXN}!v1663qDj5%80s7l&(wVRe4Gmj!O zlqM`RA5nzQn!QZbAA^_bDuRoHUVgPguiov?N9^`z=M)ugZYyd5&!v?m)f>-n>khLB zuV(n}CUJBLQGTN?8fg=(?j<;p8 z40ErT#kL?Yc)m*5qe_ACU0x>WaeN*wJnwtx&8L2gfz;nT3%leXFr0NV-#P?f&;+P% zP7e7>9 zN17p>;{l6(}(`OwCHFM_~mRFR$%mK{BUfMp-38g`JB~O zu9z+?#Yi*Nxuu|q8Uib>BF`+*19hSk5J>(>OG+(}?~&ZYN^ZdaSky26!Bu~$8Fdyj zt9OthX7F>qWo*akrA;T%sNDR+NU%7|8(F&TIWh6MnR9dGE3uGUCeREns$RClT^CgG zmjh+s=YQ)#>tOsDooG}B?6*W} z52zUvkC|SOM--Q~i=JpWsoJ)zWv9o9a^_G4Sah|d`jT9$@W0F2lZ_$lX5dNdNdJZC z1Uz{LW_}NR9@2AL5C?3PUVH-0c|po(N4#1QG`xP#4G5B@e5E*)?7U*qR!jVLh6L0_ zEkDcs87h;*tKRt|A?+H|0r8z5Oykrv4hz_V43<)Gh(ffD^#V4PLC?tntczRm0$;GU z`5S#zG(|m9cyZD!r`I;&*3bq~BfJ!=gRt?c$hiMl#QY7HSRQ+{D*5FYprZb(`&?Xc zeFWZ4P%OxthJ9(&hJUk+>v4NR$kqS7Zy%%MSfv@#%>sJyBA605$Cdz<9hF(}#Txa5L{r!o5LsGFpfK;+7Icm$G7QH!p z+4SZer0)Cl3EsSwR{Ixaz&@qd%aox4n)u7$)8FT>6~un_RlOl2pg-uJ54il3%c+5} zE;*cNT(CBuRkuQ9xwv(WoLKhQO8RI_UiYje+V2ucfr_JlMBUhv^0euof_4q(rFky1 z=jUhcx90xuX$M_;bydT9Lujq`u*#>Pv3!_6dNlaH3vc^JgGpJ61e`NohejvXIvK9D z9>OS-QoYXMNsV8-UB|7F!rnkZ!F`NCu2pi9R!)*N zh;DshcA42%I&$H-xJ=@-kP_PSleuwrdeJ5|lS3D~tNOVb6GnF{(MRJWl?=W@o_5Yz zYP5beiu!*~2;^F`n^Mv62I`Y{=`8qnHgLH89>PQO3@iV6vA97TS&a(<*}-q(Ecrr3 zBfmKVXeV<FX2rJDvqLq|aDvO|pg#0aL z8pozvPft{Rq*Cn7aD1fdV~$}KU#aLxmPNkvRxc#7%%NFMU!JN!Gh_ZI@!SyZM_}9e zm;N9VW`3h2bJvaP8yp|jV6iB)y)hAt6_5tRz|aI(nYXsK03G0ee=>R!TmNlJig}>a zW+~{0$g)Li**j`eYcGm_{6Mpk7D@_>(;+rK{oH9;o|0E&TVm)50%jpbCV3LR;V@*j zKAbT&LhaI5^bja%=m9KL4F{XJY~UqDcwcWY0?0+g+;B{(Q)-cJWX8&juxIMgmTbahS!0vkRIA7wS`C!WLx$8Q zd9b?8JHWeYu7)2KZg#k|$GCs!7Ab=RP7=r@UB_N^fL~SdAB7f(H~=uSW5But^)(Sp zRWwX?ZXxL^-Gccflt8^k6_d$y%-&zyd`s4b`=naW6OkBn)d z9Z-7)d6!9hf`ixm6WQa~i)&#$zf>BcIAlV9S_n_nZepx1fykz$7myUiZ>{%1a+TT6 zGOz2h1{p~61Ki-NGPX{)(21>5y-kf7)$Aq1ux!B^ltuvQv+u3D&sBn-IKdQPl3@$% z#bE^v81MBTJMb zv$8swA%=vdyjt(%rA&C6r%jMxv?NQyFw~N&sQ6FB>Zr^MMcWBrk3=GOC=qYEH~FwR zlpN-&D;<85tJ77Y+lWi$aiw$}HHh(83bM9`5tp41JBST6lM=WsQu5Z8<+}`(8I|f? z%#RIRRFjsZ&cnJNbL9rrlxT?=;@{kqnbp^8m1=Ql!<@{_#Txe9PX{# zyz6y@_Vb68;2g&d6)nAt@Ct=0SV+#v4hw|E?^YcR>_rp|>gy9{3EDaCPUqa3dhs$f z>U&eaA0WTd`K%jVNkNW}MSv(uK+yM+HW}BCEf03J+gAv%ah$jwpzAj?GoJ7KpBBJy z=&@GOjLu~I)MLV|2aM=_KxMc(x;A2e!^z1Y{uqrUc}wywRp%(eF{`epK8>%3pv_>R z4Bka8A|HgIAIksuVPt-o%_P<6+6kcx_m?Odzt2(30&M5(W$gAV_GNYATgc7N&6BP_0hPT8=1zQNycPno+7P{3f@l^a&BG90Y0G zhhDb62vk=v;8QD-LWfimRi&7R`c_|l(b=HS=i7d&u1`3YJq=1G4G5y>8dDYav$C