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