1use anyhow::Context as _;
2use collections::HashMap;
3use context_server::ContextServerCommand;
4use dap::adapters::DebugAdapterName;
5use fs::Fs;
6use futures::StreamExt as _;
7use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Subscription, Task};
8use lsp::LanguageServerName;
9use paths::{
10 EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_file_relative_path,
11 local_tasks_file_relative_path, local_vscode_launch_file_relative_path,
12 local_vscode_tasks_file_relative_path, task_file_name,
13};
14use rpc::{
15 AnyProtoClient, TypedEnvelope,
16 proto::{self, REMOTE_SERVER_PROJECT_ID},
17};
18use schemars::JsonSchema;
19use serde::{Deserialize, Serialize};
20pub use settings::DirenvSettings;
21pub use settings::LspSettings;
22use settings::{
23 DapSettingsContent, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation,
24 SettingsStore, parse_json_with_comments, watch_config_file,
25};
26use std::{path::PathBuf, sync::Arc, time::Duration};
27use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile};
28use util::{ResultExt, rel_path::RelPath, serde::default_true};
29use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
30
31use crate::{
32 task_store::{TaskSettingsLocation, TaskStore},
33 worktree_store::{WorktreeStore, WorktreeStoreEvent},
34};
35
36#[derive(Debug, Clone)]
37pub struct ProjectSettings {
38 /// Configuration for language servers.
39 ///
40 /// The following settings can be overridden for specific language servers:
41 /// - initialization_options
42 ///
43 /// To override settings for a language, add an entry for that language server's
44 /// name to the lsp value.
45 /// Default: null
46 // todo(settings-follow-up)
47 // We should change to use a non content type (settings::LspSettings is a content type)
48 // Note: Will either require merging with defaults, which also requires deciding where the defaults come from,
49 // or case by case deciding which fields are optional and which are actually required.
50 pub lsp: HashMap<LanguageServerName, settings::LspSettings>,
51
52 /// Common language server settings.
53 pub global_lsp_settings: GlobalLspSettings,
54
55 /// Configuration for Debugger-related features
56 pub dap: HashMap<DebugAdapterName, DapSettings>,
57
58 /// Settings for context servers used for AI-related features.
59 pub context_servers: HashMap<Arc<str>, ContextServerSettings>,
60
61 /// Configuration for Diagnostics-related features.
62 pub diagnostics: DiagnosticsSettings,
63
64 /// Configuration for Git-related features
65 pub git: GitSettings,
66
67 /// Configuration for Node-related features
68 pub node: NodeBinarySettings,
69
70 /// Configuration for how direnv configuration should be loaded
71 pub load_direnv: DirenvSettings,
72
73 /// Configuration for session-related features
74 pub session: SessionSettings,
75}
76
77#[derive(Copy, Clone, Debug)]
78pub struct SessionSettings {
79 /// Whether or not to restore unsaved buffers on restart.
80 ///
81 /// If this is true, user won't be prompted whether to save/discard
82 /// dirty files when closing the application.
83 ///
84 /// Default: true
85 pub restore_unsaved_buffers: bool,
86}
87
88#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
89pub struct NodeBinarySettings {
90 /// The path to the Node binary.
91 pub path: Option<String>,
92 /// The path to the npm binary Zed should use (defaults to `.path/../npm`).
93 pub npm_path: Option<String>,
94 /// If enabled, Zed will download its own copy of Node.
95 pub ignore_system_version: bool,
96}
97
98impl From<settings::NodeBinarySettings> for NodeBinarySettings {
99 fn from(settings: settings::NodeBinarySettings) -> Self {
100 Self {
101 path: settings.path,
102 npm_path: settings.npm_path,
103 ignore_system_version: settings.ignore_system_version.unwrap_or(false),
104 }
105 }
106}
107
108/// Common language server settings.
109#[derive(Debug, Clone, PartialEq)]
110pub struct GlobalLspSettings {
111 /// Whether to show the LSP servers button in the status bar.
112 ///
113 /// Default: `true`
114 pub button: bool,
115}
116
117#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
118#[serde(tag = "source", rename_all = "snake_case")]
119pub enum ContextServerSettings {
120 Custom {
121 /// Whether the context server is enabled.
122 #[serde(default = "default_true")]
123 enabled: bool,
124
125 #[serde(flatten)]
126 command: ContextServerCommand,
127 },
128 Extension {
129 /// Whether the context server is enabled.
130 #[serde(default = "default_true")]
131 enabled: bool,
132 /// The settings for this context server specified by the extension.
133 ///
134 /// Consult the documentation for the context server to see what settings
135 /// are supported.
136 settings: serde_json::Value,
137 },
138}
139
140impl From<settings::ContextServerSettingsContent> for ContextServerSettings {
141 fn from(value: settings::ContextServerSettingsContent) -> Self {
142 match value {
143 settings::ContextServerSettingsContent::Custom { enabled, command } => {
144 ContextServerSettings::Custom { enabled, command }
145 }
146 settings::ContextServerSettingsContent::Extension { enabled, settings } => {
147 ContextServerSettings::Extension { enabled, settings }
148 }
149 }
150 }
151}
152impl Into<settings::ContextServerSettingsContent> for ContextServerSettings {
153 fn into(self) -> settings::ContextServerSettingsContent {
154 match self {
155 ContextServerSettings::Custom { enabled, command } => {
156 settings::ContextServerSettingsContent::Custom { enabled, command }
157 }
158 ContextServerSettings::Extension { enabled, settings } => {
159 settings::ContextServerSettingsContent::Extension { enabled, settings }
160 }
161 }
162 }
163}
164
165impl ContextServerSettings {
166 pub fn default_extension() -> Self {
167 Self::Extension {
168 enabled: true,
169 settings: serde_json::json!({}),
170 }
171 }
172
173 pub fn enabled(&self) -> bool {
174 match self {
175 ContextServerSettings::Custom { enabled, .. } => *enabled,
176 ContextServerSettings::Extension { enabled, .. } => *enabled,
177 }
178 }
179
180 pub fn set_enabled(&mut self, enabled: bool) {
181 match self {
182 ContextServerSettings::Custom { enabled: e, .. } => *e = enabled,
183 ContextServerSettings::Extension { enabled: e, .. } => *e = enabled,
184 }
185 }
186}
187
188#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
189pub enum DiagnosticSeverity {
190 // No diagnostics are shown.
191 Off,
192 Error,
193 Warning,
194 Info,
195 Hint,
196}
197
198impl DiagnosticSeverity {
199 pub fn into_lsp(self) -> Option<lsp::DiagnosticSeverity> {
200 match self {
201 DiagnosticSeverity::Off => None,
202 DiagnosticSeverity::Error => Some(lsp::DiagnosticSeverity::ERROR),
203 DiagnosticSeverity::Warning => Some(lsp::DiagnosticSeverity::WARNING),
204 DiagnosticSeverity::Info => Some(lsp::DiagnosticSeverity::INFORMATION),
205 DiagnosticSeverity::Hint => Some(lsp::DiagnosticSeverity::HINT),
206 }
207 }
208}
209
210impl From<settings::DiagnosticSeverityContent> for DiagnosticSeverity {
211 fn from(severity: settings::DiagnosticSeverityContent) -> Self {
212 match severity {
213 settings::DiagnosticSeverityContent::Off => DiagnosticSeverity::Off,
214 settings::DiagnosticSeverityContent::Error => DiagnosticSeverity::Error,
215 settings::DiagnosticSeverityContent::Warning => DiagnosticSeverity::Warning,
216 settings::DiagnosticSeverityContent::Info => DiagnosticSeverity::Info,
217 settings::DiagnosticSeverityContent::Hint => DiagnosticSeverity::Hint,
218 }
219 }
220}
221
222/// Determines the severity of the diagnostic that should be moved to.
223#[derive(PartialEq, PartialOrd, Clone, Copy, Debug, Eq, Deserialize, JsonSchema)]
224#[serde(rename_all = "snake_case")]
225pub enum GoToDiagnosticSeverity {
226 /// Errors
227 Error = 3,
228 /// Warnings
229 Warning = 2,
230 /// Information
231 Information = 1,
232 /// Hints
233 Hint = 0,
234}
235
236impl From<lsp::DiagnosticSeverity> for GoToDiagnosticSeverity {
237 fn from(severity: lsp::DiagnosticSeverity) -> Self {
238 match severity {
239 lsp::DiagnosticSeverity::ERROR => Self::Error,
240 lsp::DiagnosticSeverity::WARNING => Self::Warning,
241 lsp::DiagnosticSeverity::INFORMATION => Self::Information,
242 lsp::DiagnosticSeverity::HINT => Self::Hint,
243 _ => Self::Error,
244 }
245 }
246}
247
248impl GoToDiagnosticSeverity {
249 pub fn min() -> Self {
250 Self::Hint
251 }
252
253 pub fn max() -> Self {
254 Self::Error
255 }
256}
257
258/// Allows filtering diagnostics that should be moved to.
259#[derive(PartialEq, Clone, Copy, Debug, Deserialize, JsonSchema)]
260#[serde(untagged)]
261pub enum GoToDiagnosticSeverityFilter {
262 /// Move to diagnostics of a specific severity.
263 Only(GoToDiagnosticSeverity),
264
265 /// Specify a range of severities to include.
266 Range {
267 /// Minimum severity to move to. Defaults no "error".
268 #[serde(default = "GoToDiagnosticSeverity::min")]
269 min: GoToDiagnosticSeverity,
270 /// Maximum severity to move to. Defaults to "hint".
271 #[serde(default = "GoToDiagnosticSeverity::max")]
272 max: GoToDiagnosticSeverity,
273 },
274}
275
276impl Default for GoToDiagnosticSeverityFilter {
277 fn default() -> Self {
278 Self::Range {
279 min: GoToDiagnosticSeverity::min(),
280 max: GoToDiagnosticSeverity::max(),
281 }
282 }
283}
284
285impl GoToDiagnosticSeverityFilter {
286 pub fn matches(&self, severity: lsp::DiagnosticSeverity) -> bool {
287 let severity: GoToDiagnosticSeverity = severity.into();
288 match self {
289 Self::Only(target) => *target == severity,
290 Self::Range { min, max } => severity >= *min && severity <= *max,
291 }
292 }
293}
294
295#[derive(Copy, Clone, Debug)]
296pub struct GitSettings {
297 /// Whether or not to show the git gutter.
298 ///
299 /// Default: tracked_files
300 pub git_gutter: settings::GitGutterSetting,
301 /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
302 ///
303 /// Default: null
304 pub gutter_debounce: Option<u64>,
305 /// Whether or not to show git blame data inline in
306 /// the currently focused line.
307 ///
308 /// Default: on
309 pub inline_blame: InlineBlameSettings,
310 /// Which information to show in the branch picker.
311 ///
312 /// Default: on
313 pub branch_picker: BranchPickerSettings,
314 /// How hunks are displayed visually in the editor.
315 ///
316 /// Default: staged_hollow
317 pub hunk_style: settings::GitHunkStyleSetting,
318}
319
320#[derive(Clone, Copy, Debug)]
321pub struct InlineBlameSettings {
322 /// Whether or not to show git blame data inline in
323 /// the currently focused line.
324 ///
325 /// Default: true
326 pub enabled: bool,
327 /// Whether to only show the inline blame information
328 /// after a delay once the cursor stops moving.
329 ///
330 /// Default: 0
331 pub delay_ms: std::time::Duration,
332 /// The amount of padding between the end of the source line and the start
333 /// of the inline blame in units of columns.
334 ///
335 /// Default: 7
336 pub padding: u32,
337 /// The minimum column number to show the inline blame information at
338 ///
339 /// Default: 0
340 pub min_column: u32,
341 /// Whether to show commit summary as part of the inline blame.
342 ///
343 /// Default: false
344 pub show_commit_summary: bool,
345}
346
347impl GitSettings {
348 pub fn inline_blame_delay(&self) -> Option<Duration> {
349 if self.inline_blame.delay_ms.as_millis() > 0 {
350 Some(self.inline_blame.delay_ms)
351 } else {
352 None
353 }
354 }
355}
356
357#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
358#[serde(rename_all = "snake_case")]
359pub struct BranchPickerSettings {
360 /// Whether to show author name as part of the commit information.
361 ///
362 /// Default: false
363 #[serde(default)]
364 pub show_author_name: bool,
365}
366
367impl Default for BranchPickerSettings {
368 fn default() -> Self {
369 Self {
370 show_author_name: true,
371 }
372 }
373}
374
375#[derive(Clone, Debug)]
376pub struct DiagnosticsSettings {
377 /// Whether to show the project diagnostics button in the status bar.
378 pub button: bool,
379
380 /// Whether or not to include warning diagnostics.
381 pub include_warnings: bool,
382
383 /// Settings for using LSP pull diagnostics mechanism in Zed.
384 pub lsp_pull_diagnostics: LspPullDiagnosticsSettings,
385
386 /// Settings for showing inline diagnostics.
387 pub inline: InlineDiagnosticsSettings,
388}
389
390#[derive(Clone, Copy, Debug, PartialEq, Eq)]
391pub struct InlineDiagnosticsSettings {
392 /// Whether or not to show inline diagnostics
393 ///
394 /// Default: false
395 pub enabled: bool,
396 /// Whether to only show the inline diagnostics after a delay after the
397 /// last editor event.
398 ///
399 /// Default: 150
400 pub update_debounce_ms: u64,
401 /// The amount of padding between the end of the source line and the start
402 /// of the inline diagnostic in units of columns.
403 ///
404 /// Default: 4
405 pub padding: u32,
406 /// The minimum column to display inline diagnostics. This setting can be
407 /// used to horizontally align inline diagnostics at some position. Lines
408 /// longer than this value will still push diagnostics further to the right.
409 ///
410 /// Default: 0
411 pub min_column: u32,
412
413 pub max_severity: Option<DiagnosticSeverity>,
414}
415
416#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
417pub struct LspPullDiagnosticsSettings {
418 /// Whether to pull for diagnostics or not.
419 ///
420 /// Default: true
421 pub enabled: bool,
422 /// Minimum time to wait before pulling diagnostics from the language server(s).
423 /// 0 turns the debounce off.
424 ///
425 /// Default: 50
426 pub debounce_ms: u64,
427}
428
429impl Settings for ProjectSettings {
430 fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
431 let project = &content.project.clone();
432 let diagnostics = content.diagnostics.as_ref().unwrap();
433 let lsp_pull_diagnostics = diagnostics.lsp_pull_diagnostics.as_ref().unwrap();
434 let inline_diagnostics = diagnostics.inline.as_ref().unwrap();
435
436 let git = content.git.as_ref().unwrap();
437 let git_settings = GitSettings {
438 git_gutter: git.git_gutter.unwrap(),
439 gutter_debounce: git.gutter_debounce,
440 inline_blame: {
441 let inline = git.inline_blame.unwrap();
442 InlineBlameSettings {
443 enabled: inline.enabled.unwrap(),
444 delay_ms: std::time::Duration::from_millis(inline.delay_ms.unwrap()),
445 padding: inline.padding.unwrap(),
446 min_column: inline.min_column.unwrap(),
447 show_commit_summary: inline.show_commit_summary.unwrap(),
448 }
449 },
450 branch_picker: {
451 let branch_picker = git.branch_picker.unwrap();
452 BranchPickerSettings {
453 show_author_name: branch_picker.show_author_name.unwrap(),
454 }
455 },
456 hunk_style: git.hunk_style.unwrap(),
457 };
458 Self {
459 context_servers: project
460 .context_servers
461 .clone()
462 .into_iter()
463 .map(|(key, value)| (key, value.into()))
464 .collect(),
465 lsp: project
466 .lsp
467 .clone()
468 .into_iter()
469 .map(|(key, value)| (LanguageServerName(key.into()), value))
470 .collect(),
471 global_lsp_settings: GlobalLspSettings {
472 button: content
473 .global_lsp_settings
474 .as_ref()
475 .unwrap()
476 .button
477 .unwrap(),
478 },
479 dap: project
480 .dap
481 .clone()
482 .into_iter()
483 .map(|(key, value)| (DebugAdapterName(key.into()), DapSettings::from(value)))
484 .collect(),
485 diagnostics: DiagnosticsSettings {
486 button: diagnostics.button.unwrap(),
487 include_warnings: diagnostics.include_warnings.unwrap(),
488 lsp_pull_diagnostics: LspPullDiagnosticsSettings {
489 enabled: lsp_pull_diagnostics.enabled.unwrap(),
490 debounce_ms: lsp_pull_diagnostics.debounce_ms.unwrap(),
491 },
492 inline: InlineDiagnosticsSettings {
493 enabled: inline_diagnostics.enabled.unwrap(),
494 update_debounce_ms: inline_diagnostics.update_debounce_ms.unwrap(),
495 padding: inline_diagnostics.padding.unwrap(),
496 min_column: inline_diagnostics.min_column.unwrap(),
497 max_severity: inline_diagnostics.max_severity.map(Into::into),
498 },
499 },
500 git: git_settings,
501 node: content.node.clone().unwrap().into(),
502 load_direnv: project.load_direnv.clone().unwrap(),
503 session: SessionSettings {
504 restore_unsaved_buffers: content.session.unwrap().restore_unsaved_buffers.unwrap(),
505 },
506 }
507 }
508
509 fn import_from_vscode(
510 vscode: &settings::VsCodeSettings,
511 current: &mut settings::SettingsContent,
512 ) {
513 // this just sets the binary name instead of a full path so it relies on path lookup
514 // resolving to the one you want
515 let npm_path = vscode.read_enum("npm.packageManager", |s| match s {
516 v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
517 _ => None,
518 });
519 if npm_path.is_some() {
520 current.node.get_or_insert_default().npm_path = npm_path;
521 }
522
523 if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") {
524 current
525 .git
526 .get_or_insert_default()
527 .inline_blame
528 .get_or_insert_default()
529 .enabled = Some(b);
530 }
531
532 #[derive(Deserialize)]
533 struct VsCodeContextServerCommand {
534 command: PathBuf,
535 args: Option<Vec<String>>,
536 env: Option<HashMap<String, String>>,
537 // note: we don't support envFile and type
538 }
539 if let Some(mcp) = vscode.read_value("mcp").and_then(|v| v.as_object()) {
540 current
541 .project
542 .context_servers
543 .extend(mcp.iter().filter_map(|(k, v)| {
544 Some((
545 k.clone().into(),
546 settings::ContextServerSettingsContent::Custom {
547 enabled: true,
548 command: serde_json::from_value::<VsCodeContextServerCommand>(
549 v.clone(),
550 )
551 .ok()
552 .map(|cmd| {
553 settings::ContextServerCommand {
554 path: cmd.command,
555 args: cmd.args.unwrap_or_default(),
556 env: cmd.env,
557 timeout: None,
558 }
559 })?,
560 },
561 ))
562 }));
563 }
564
565 // TODO: translate lsp settings for rust-analyzer and other popular ones to old.lsp
566 }
567}
568
569pub enum SettingsObserverMode {
570 Local(Arc<dyn Fs>),
571 Remote,
572}
573
574#[derive(Clone, Debug, PartialEq)]
575pub enum SettingsObserverEvent {
576 LocalSettingsUpdated(Result<PathBuf, InvalidSettingsError>),
577 LocalTasksUpdated(Result<PathBuf, InvalidSettingsError>),
578 LocalDebugScenariosUpdated(Result<PathBuf, InvalidSettingsError>),
579}
580
581impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
582
583pub struct SettingsObserver {
584 mode: SettingsObserverMode,
585 downstream_client: Option<AnyProtoClient>,
586 worktree_store: Entity<WorktreeStore>,
587 project_id: u64,
588 task_store: Entity<TaskStore>,
589 _user_settings_watcher: Option<Subscription>,
590 _global_task_config_watcher: Task<()>,
591 _global_debug_config_watcher: Task<()>,
592}
593
594/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
595/// (or the equivalent protobuf messages from upstream) and updates local settings
596/// and sends notifications downstream.
597/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
598/// upstream.
599impl SettingsObserver {
600 pub fn init(client: &AnyProtoClient) {
601 client.add_entity_message_handler(Self::handle_update_worktree_settings);
602 client.add_entity_message_handler(Self::handle_update_user_settings);
603 }
604
605 pub fn new_local(
606 fs: Arc<dyn Fs>,
607 worktree_store: Entity<WorktreeStore>,
608 task_store: Entity<TaskStore>,
609 cx: &mut Context<Self>,
610 ) -> Self {
611 cx.subscribe(&worktree_store, Self::on_worktree_store_event)
612 .detach();
613
614 Self {
615 worktree_store,
616 task_store,
617 mode: SettingsObserverMode::Local(fs.clone()),
618 downstream_client: None,
619 _user_settings_watcher: None,
620 project_id: REMOTE_SERVER_PROJECT_ID,
621 _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
622 fs.clone(),
623 paths::tasks_file().clone(),
624 cx,
625 ),
626 _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
627 fs.clone(),
628 paths::debug_scenarios_file().clone(),
629 cx,
630 ),
631 }
632 }
633
634 pub fn new_remote(
635 fs: Arc<dyn Fs>,
636 worktree_store: Entity<WorktreeStore>,
637 task_store: Entity<TaskStore>,
638 upstream_client: Option<AnyProtoClient>,
639 cx: &mut Context<Self>,
640 ) -> Self {
641 let mut user_settings_watcher = None;
642 if cx.try_global::<SettingsStore>().is_some() {
643 if let Some(upstream_client) = upstream_client {
644 let mut user_settings = None;
645 user_settings_watcher = Some(cx.observe_global::<SettingsStore>(move |_, cx| {
646 if let Some(new_settings) = cx.global::<SettingsStore>().raw_user_settings() {
647 if Some(new_settings) != user_settings.as_ref() {
648 if let Some(new_settings_string) =
649 serde_json::to_string(new_settings).ok()
650 {
651 user_settings = Some(new_settings.clone());
652 upstream_client
653 .send(proto::UpdateUserSettings {
654 project_id: REMOTE_SERVER_PROJECT_ID,
655 contents: new_settings_string,
656 })
657 .log_err();
658 }
659 }
660 }
661 }));
662 }
663 };
664
665 Self {
666 worktree_store,
667 task_store,
668 mode: SettingsObserverMode::Remote,
669 downstream_client: None,
670 project_id: REMOTE_SERVER_PROJECT_ID,
671 _user_settings_watcher: user_settings_watcher,
672 _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
673 fs.clone(),
674 paths::tasks_file().clone(),
675 cx,
676 ),
677 _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
678 fs.clone(),
679 paths::debug_scenarios_file().clone(),
680 cx,
681 ),
682 }
683 }
684
685 pub fn shared(
686 &mut self,
687 project_id: u64,
688 downstream_client: AnyProtoClient,
689 cx: &mut Context<Self>,
690 ) {
691 self.project_id = project_id;
692 self.downstream_client = Some(downstream_client.clone());
693
694 let store = cx.global::<SettingsStore>();
695 for worktree in self.worktree_store.read(cx).worktrees() {
696 let worktree_id = worktree.read(cx).id().to_proto();
697 for (path, content) in store.local_settings(worktree.read(cx).id()) {
698 let content = serde_json::to_string(&content).unwrap();
699 downstream_client
700 .send(proto::UpdateWorktreeSettings {
701 project_id,
702 worktree_id,
703 path: path.to_proto(),
704 content: Some(content),
705 kind: Some(
706 local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
707 ),
708 })
709 .log_err();
710 }
711 for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
712 downstream_client
713 .send(proto::UpdateWorktreeSettings {
714 project_id,
715 worktree_id,
716 path: path.to_proto(),
717 content: Some(content),
718 kind: Some(
719 local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
720 ),
721 })
722 .log_err();
723 }
724 }
725 }
726
727 pub fn unshared(&mut self, _: &mut Context<Self>) {
728 self.downstream_client = None;
729 }
730
731 async fn handle_update_worktree_settings(
732 this: Entity<Self>,
733 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
734 mut cx: AsyncApp,
735 ) -> anyhow::Result<()> {
736 let kind = match envelope.payload.kind {
737 Some(kind) => proto::LocalSettingsKind::from_i32(kind)
738 .with_context(|| format!("unknown kind {kind}"))?,
739 None => proto::LocalSettingsKind::Settings,
740 };
741 let path = RelPath::from_proto(&envelope.payload.path)?;
742 this.update(&mut cx, |this, cx| {
743 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
744 let Some(worktree) = this
745 .worktree_store
746 .read(cx)
747 .worktree_for_id(worktree_id, cx)
748 else {
749 return;
750 };
751
752 this.update_settings(
753 worktree,
754 [(
755 path,
756 local_settings_kind_from_proto(kind),
757 envelope.payload.content,
758 )],
759 cx,
760 );
761 })?;
762 Ok(())
763 }
764
765 async fn handle_update_user_settings(
766 _: Entity<Self>,
767 envelope: TypedEnvelope<proto::UpdateUserSettings>,
768 cx: AsyncApp,
769 ) -> anyhow::Result<()> {
770 cx.update_global(|settings_store: &mut SettingsStore, cx| {
771 settings_store
772 .set_user_settings(&envelope.payload.contents, cx)
773 .context("setting new user settings")?;
774 anyhow::Ok(())
775 })??;
776 Ok(())
777 }
778
779 fn on_worktree_store_event(
780 &mut self,
781 _: Entity<WorktreeStore>,
782 event: &WorktreeStoreEvent,
783 cx: &mut Context<Self>,
784 ) {
785 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
786 cx.subscribe(worktree, |this, worktree, event, cx| {
787 if let worktree::Event::UpdatedEntries(changes) = event {
788 this.update_local_worktree_settings(&worktree, changes, cx)
789 }
790 })
791 .detach()
792 }
793 }
794
795 fn update_local_worktree_settings(
796 &mut self,
797 worktree: &Entity<Worktree>,
798 changes: &UpdatedEntriesSet,
799 cx: &mut Context<Self>,
800 ) {
801 let SettingsObserverMode::Local(fs) = &self.mode else {
802 return;
803 };
804
805 let mut settings_contents = Vec::new();
806 for (path, _, change) in changes.iter() {
807 let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
808 let settings_dir = path
809 .ancestors()
810 .nth(local_settings_file_relative_path().components().count())
811 .unwrap()
812 .into();
813 (settings_dir, LocalSettingsKind::Settings)
814 } else if path.ends_with(local_tasks_file_relative_path()) {
815 let settings_dir = path
816 .ancestors()
817 .nth(
818 local_tasks_file_relative_path()
819 .components()
820 .count()
821 .saturating_sub(1),
822 )
823 .unwrap()
824 .into();
825 (settings_dir, LocalSettingsKind::Tasks)
826 } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
827 let settings_dir = path
828 .ancestors()
829 .nth(
830 local_vscode_tasks_file_relative_path()
831 .components()
832 .count()
833 .saturating_sub(1),
834 )
835 .unwrap()
836 .into();
837 (settings_dir, LocalSettingsKind::Tasks)
838 } else if path.ends_with(local_debug_file_relative_path()) {
839 let settings_dir = path
840 .ancestors()
841 .nth(
842 local_debug_file_relative_path()
843 .components()
844 .count()
845 .saturating_sub(1),
846 )
847 .unwrap()
848 .into();
849 (settings_dir, LocalSettingsKind::Debug)
850 } else if path.ends_with(local_vscode_launch_file_relative_path()) {
851 let settings_dir = path
852 .ancestors()
853 .nth(
854 local_vscode_tasks_file_relative_path()
855 .components()
856 .count()
857 .saturating_sub(1),
858 )
859 .unwrap()
860 .into();
861 (settings_dir, LocalSettingsKind::Debug)
862 } else if path.ends_with(RelPath::unix(EDITORCONFIG_NAME).unwrap()) {
863 let Some(settings_dir) = path.parent().map(Arc::from) else {
864 continue;
865 };
866 (settings_dir, LocalSettingsKind::Editorconfig)
867 } else {
868 continue;
869 };
870
871 let removed = change == &PathChange::Removed;
872 let fs = fs.clone();
873 let abs_path = worktree.read(cx).absolutize(path);
874 settings_contents.push(async move {
875 (
876 settings_dir,
877 kind,
878 if removed {
879 None
880 } else {
881 Some(
882 async move {
883 let content = fs.load(&abs_path).await?;
884 if abs_path.ends_with(local_vscode_tasks_file_relative_path().as_std_path()) {
885 let vscode_tasks =
886 parse_json_with_comments::<VsCodeTaskFile>(&content)
887 .with_context(|| {
888 format!("parsing VSCode tasks, file {abs_path:?}")
889 })?;
890 let zed_tasks = TaskTemplates::try_from(vscode_tasks)
891 .with_context(|| {
892 format!(
893 "converting VSCode tasks into Zed ones, file {abs_path:?}"
894 )
895 })?;
896 serde_json::to_string(&zed_tasks).with_context(|| {
897 format!(
898 "serializing Zed tasks into JSON, file {abs_path:?}"
899 )
900 })
901 } else if abs_path.ends_with(local_vscode_launch_file_relative_path().as_std_path()) {
902 let vscode_tasks =
903 parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
904 .with_context(|| {
905 format!("parsing VSCode debug tasks, file {abs_path:?}")
906 })?;
907 let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
908 .with_context(|| {
909 format!(
910 "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
911 )
912 })?;
913 serde_json::to_string(&zed_tasks).with_context(|| {
914 format!(
915 "serializing Zed tasks into JSON, file {abs_path:?}"
916 )
917 })
918 } else {
919 Ok(content)
920 }
921 }
922 .await,
923 )
924 },
925 )
926 });
927 }
928
929 if settings_contents.is_empty() {
930 return;
931 }
932
933 let worktree = worktree.clone();
934 cx.spawn(async move |this, cx| {
935 let settings_contents: Vec<(Arc<RelPath>, _, _)> =
936 futures::future::join_all(settings_contents).await;
937 cx.update(|cx| {
938 this.update(cx, |this, cx| {
939 this.update_settings(
940 worktree,
941 settings_contents.into_iter().map(|(path, kind, content)| {
942 (path, kind, content.and_then(|c| c.log_err()))
943 }),
944 cx,
945 )
946 })
947 })
948 })
949 .detach();
950 }
951
952 fn update_settings(
953 &mut self,
954 worktree: Entity<Worktree>,
955 settings_contents: impl IntoIterator<Item = (Arc<RelPath>, LocalSettingsKind, Option<String>)>,
956 cx: &mut Context<Self>,
957 ) {
958 let worktree_id = worktree.read(cx).id();
959 let remote_worktree_id = worktree.read(cx).id();
960 let task_store = self.task_store.clone();
961
962 for (directory, kind, file_content) in settings_contents {
963 match kind {
964 LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
965 .update_global::<SettingsStore, _>(|store, cx| {
966 let result = store.set_local_settings(
967 worktree_id,
968 directory.clone(),
969 kind,
970 file_content.as_deref(),
971 cx,
972 );
973
974 match result {
975 Err(InvalidSettingsError::LocalSettings { path, message }) => {
976 log::error!("Failed to set local settings in {path:?}: {message}");
977 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
978 InvalidSettingsError::LocalSettings { path, message },
979 )));
980 }
981 Err(e) => {
982 log::error!("Failed to set local settings: {e}");
983 }
984 Ok(()) => {
985 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(directory
986 .as_std_path()
987 .join(local_settings_file_relative_path().as_std_path()))));
988 }
989 }
990 }),
991 LocalSettingsKind::Tasks => {
992 let result = task_store.update(cx, |task_store, cx| {
993 task_store.update_user_tasks(
994 TaskSettingsLocation::Worktree(SettingsLocation {
995 worktree_id,
996 path: directory.as_ref(),
997 }),
998 file_content.as_deref(),
999 cx,
1000 )
1001 });
1002
1003 match result {
1004 Err(InvalidSettingsError::Tasks { path, message }) => {
1005 log::error!("Failed to set local tasks in {path:?}: {message:?}");
1006 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1007 InvalidSettingsError::Tasks { path, message },
1008 )));
1009 }
1010 Err(e) => {
1011 log::error!("Failed to set local tasks: {e}");
1012 }
1013 Ok(()) => {
1014 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(directory
1015 .as_std_path()
1016 .join(task_file_name()))));
1017 }
1018 }
1019 }
1020 LocalSettingsKind::Debug => {
1021 let result = task_store.update(cx, |task_store, cx| {
1022 task_store.update_user_debug_scenarios(
1023 TaskSettingsLocation::Worktree(SettingsLocation {
1024 worktree_id,
1025 path: directory.as_ref(),
1026 }),
1027 file_content.as_deref(),
1028 cx,
1029 )
1030 });
1031
1032 match result {
1033 Err(InvalidSettingsError::Debug { path, message }) => {
1034 log::error!(
1035 "Failed to set local debug scenarios in {path:?}: {message:?}"
1036 );
1037 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1038 InvalidSettingsError::Debug { path, message },
1039 )));
1040 }
1041 Err(e) => {
1042 log::error!("Failed to set local tasks: {e}");
1043 }
1044 Ok(()) => {
1045 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(directory
1046 .as_std_path()
1047 .join(task_file_name()))));
1048 }
1049 }
1050 }
1051 };
1052
1053 if let Some(downstream_client) = &self.downstream_client {
1054 downstream_client
1055 .send(proto::UpdateWorktreeSettings {
1056 project_id: self.project_id,
1057 worktree_id: remote_worktree_id.to_proto(),
1058 path: directory.to_proto(),
1059 content: file_content.clone(),
1060 kind: Some(local_settings_kind_to_proto(kind).into()),
1061 })
1062 .log_err();
1063 }
1064 }
1065 }
1066
1067 fn subscribe_to_global_task_file_changes(
1068 fs: Arc<dyn Fs>,
1069 file_path: PathBuf,
1070 cx: &mut Context<Self>,
1071 ) -> Task<()> {
1072 let mut user_tasks_file_rx =
1073 watch_config_file(cx.background_executor(), fs, file_path.clone());
1074 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1075 let weak_entry = cx.weak_entity();
1076 cx.spawn(async move |settings_observer, cx| {
1077 let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1078 settings_observer.task_store.clone()
1079 }) else {
1080 return;
1081 };
1082 if let Some(user_tasks_content) = user_tasks_content {
1083 let Ok(()) = task_store.update(cx, |task_store, cx| {
1084 task_store
1085 .update_user_tasks(
1086 TaskSettingsLocation::Global(&file_path),
1087 Some(&user_tasks_content),
1088 cx,
1089 )
1090 .log_err();
1091 }) else {
1092 return;
1093 };
1094 }
1095 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1096 let Ok(result) = task_store.update(cx, |task_store, cx| {
1097 task_store.update_user_tasks(
1098 TaskSettingsLocation::Global(&file_path),
1099 Some(&user_tasks_content),
1100 cx,
1101 )
1102 }) else {
1103 break;
1104 };
1105
1106 weak_entry
1107 .update(cx, |_, cx| match result {
1108 Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1109 file_path.clone()
1110 ))),
1111 Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1112 InvalidSettingsError::Tasks {
1113 path: file_path.clone(),
1114 message: err.to_string(),
1115 },
1116 ))),
1117 })
1118 .ok();
1119 }
1120 })
1121 }
1122 fn subscribe_to_global_debug_scenarios_changes(
1123 fs: Arc<dyn Fs>,
1124 file_path: PathBuf,
1125 cx: &mut Context<Self>,
1126 ) -> Task<()> {
1127 let mut user_tasks_file_rx =
1128 watch_config_file(cx.background_executor(), fs, file_path.clone());
1129 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1130 let weak_entry = cx.weak_entity();
1131 cx.spawn(async move |settings_observer, cx| {
1132 let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1133 settings_observer.task_store.clone()
1134 }) else {
1135 return;
1136 };
1137 if let Some(user_tasks_content) = user_tasks_content {
1138 let Ok(()) = task_store.update(cx, |task_store, cx| {
1139 task_store
1140 .update_user_debug_scenarios(
1141 TaskSettingsLocation::Global(&file_path),
1142 Some(&user_tasks_content),
1143 cx,
1144 )
1145 .log_err();
1146 }) else {
1147 return;
1148 };
1149 }
1150 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1151 let Ok(result) = task_store.update(cx, |task_store, cx| {
1152 task_store.update_user_debug_scenarios(
1153 TaskSettingsLocation::Global(&file_path),
1154 Some(&user_tasks_content),
1155 cx,
1156 )
1157 }) else {
1158 break;
1159 };
1160
1161 weak_entry
1162 .update(cx, |_, cx| match result {
1163 Ok(()) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(Ok(
1164 file_path.clone(),
1165 ))),
1166 Err(err) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(
1167 Err(InvalidSettingsError::Tasks {
1168 path: file_path.clone(),
1169 message: err.to_string(),
1170 }),
1171 )),
1172 })
1173 .ok();
1174 }
1175 })
1176 }
1177}
1178
1179pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
1180 match kind {
1181 proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
1182 proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
1183 proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
1184 proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
1185 }
1186}
1187
1188pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
1189 match kind {
1190 LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
1191 LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
1192 LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
1193 LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
1194 }
1195}
1196
1197#[derive(Debug, Clone)]
1198pub struct DapSettings {
1199 pub binary: DapBinary,
1200 pub args: Vec<String>,
1201}
1202
1203impl From<DapSettingsContent> for DapSettings {
1204 fn from(content: DapSettingsContent) -> Self {
1205 DapSettings {
1206 binary: content
1207 .binary
1208 .map_or_else(|| DapBinary::Default, |binary| DapBinary::Custom(binary)),
1209 args: content.args.unwrap_or_default(),
1210 }
1211 }
1212}
1213
1214#[derive(Debug, Clone)]
1215pub enum DapBinary {
1216 Default,
1217 Custom(String),
1218}