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 match event {
796 WorktreeStoreEvent::WorktreeAdded(worktree) => cx
797 .subscribe(worktree, |this, worktree, event, cx| {
798 if let worktree::Event::UpdatedEntries(changes) = event {
799 this.update_local_worktree_settings(&worktree, changes, cx)
800 }
801 })
802 .detach(),
803 WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => {
804 cx.update_global::<SettingsStore, _>(|store, cx| {
805 store.clear_local_settings(*worktree_id, cx).log_err();
806 });
807 }
808 _ => {}
809 }
810 }
811
812 fn update_local_worktree_settings(
813 &mut self,
814 worktree: &Entity<Worktree>,
815 changes: &UpdatedEntriesSet,
816 cx: &mut Context<Self>,
817 ) {
818 let SettingsObserverMode::Local(fs) = &self.mode else {
819 return;
820 };
821
822 let mut settings_contents = Vec::new();
823 for (path, _, change) in changes.iter() {
824 let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
825 let settings_dir = path
826 .ancestors()
827 .nth(local_settings_file_relative_path().components().count())
828 .unwrap()
829 .into();
830 (settings_dir, LocalSettingsKind::Settings)
831 } else if path.ends_with(local_tasks_file_relative_path()) {
832 let settings_dir = path
833 .ancestors()
834 .nth(
835 local_tasks_file_relative_path()
836 .components()
837 .count()
838 .saturating_sub(1),
839 )
840 .unwrap()
841 .into();
842 (settings_dir, LocalSettingsKind::Tasks)
843 } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
844 let settings_dir = path
845 .ancestors()
846 .nth(
847 local_vscode_tasks_file_relative_path()
848 .components()
849 .count()
850 .saturating_sub(1),
851 )
852 .unwrap()
853 .into();
854 (settings_dir, LocalSettingsKind::Tasks)
855 } else if path.ends_with(local_debug_file_relative_path()) {
856 let settings_dir = path
857 .ancestors()
858 .nth(
859 local_debug_file_relative_path()
860 .components()
861 .count()
862 .saturating_sub(1),
863 )
864 .unwrap()
865 .into();
866 (settings_dir, LocalSettingsKind::Debug)
867 } else if path.ends_with(local_vscode_launch_file_relative_path()) {
868 let settings_dir = path
869 .ancestors()
870 .nth(
871 local_vscode_tasks_file_relative_path()
872 .components()
873 .count()
874 .saturating_sub(1),
875 )
876 .unwrap()
877 .into();
878 (settings_dir, LocalSettingsKind::Debug)
879 } else if path.ends_with(RelPath::unix(EDITORCONFIG_NAME).unwrap()) {
880 let Some(settings_dir) = path.parent().map(Arc::from) else {
881 continue;
882 };
883 (settings_dir, LocalSettingsKind::Editorconfig)
884 } else {
885 continue;
886 };
887
888 let removed = change == &PathChange::Removed;
889 let fs = fs.clone();
890 let abs_path = worktree.read(cx).absolutize(path);
891 settings_contents.push(async move {
892 (
893 settings_dir,
894 kind,
895 if removed {
896 None
897 } else {
898 Some(
899 async move {
900 let content = fs.load(&abs_path).await?;
901 if abs_path.ends_with(local_vscode_tasks_file_relative_path().as_std_path()) {
902 let vscode_tasks =
903 parse_json_with_comments::<VsCodeTaskFile>(&content)
904 .with_context(|| {
905 format!("parsing VSCode tasks, file {abs_path:?}")
906 })?;
907 let zed_tasks = TaskTemplates::try_from(vscode_tasks)
908 .with_context(|| {
909 format!(
910 "converting VSCode 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 if abs_path.ends_with(local_vscode_launch_file_relative_path().as_std_path()) {
919 let vscode_tasks =
920 parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
921 .with_context(|| {
922 format!("parsing VSCode debug tasks, file {abs_path:?}")
923 })?;
924 let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
925 .with_context(|| {
926 format!(
927 "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
928 )
929 })?;
930 serde_json::to_string(&zed_tasks).with_context(|| {
931 format!(
932 "serializing Zed tasks into JSON, file {abs_path:?}"
933 )
934 })
935 } else {
936 Ok(content)
937 }
938 }
939 .await,
940 )
941 },
942 )
943 });
944 }
945
946 if settings_contents.is_empty() {
947 return;
948 }
949
950 let worktree = worktree.clone();
951 cx.spawn(async move |this, cx| {
952 let settings_contents: Vec<(Arc<RelPath>, _, _)> =
953 futures::future::join_all(settings_contents).await;
954 cx.update(|cx| {
955 this.update(cx, |this, cx| {
956 this.update_settings(
957 worktree,
958 settings_contents.into_iter().map(|(path, kind, content)| {
959 (path, kind, content.and_then(|c| c.log_err()))
960 }),
961 cx,
962 )
963 })
964 })
965 })
966 .detach();
967 }
968
969 fn update_settings(
970 &mut self,
971 worktree: Entity<Worktree>,
972 settings_contents: impl IntoIterator<Item = (Arc<RelPath>, LocalSettingsKind, Option<String>)>,
973 cx: &mut Context<Self>,
974 ) {
975 let worktree_id = worktree.read(cx).id();
976 let remote_worktree_id = worktree.read(cx).id();
977 let task_store = self.task_store.clone();
978
979 for (directory, kind, file_content) in settings_contents {
980 match kind {
981 LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
982 .update_global::<SettingsStore, _>(|store, cx| {
983 let result = store.set_local_settings(
984 worktree_id,
985 directory.clone(),
986 kind,
987 file_content.as_deref(),
988 cx,
989 );
990
991 match result {
992 Err(InvalidSettingsError::LocalSettings { path, message }) => {
993 log::error!("Failed to set local settings in {path:?}: {message}");
994 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
995 InvalidSettingsError::LocalSettings { path, message },
996 )));
997 }
998 Err(e) => {
999 log::error!("Failed to set local settings: {e}");
1000 }
1001 Ok(()) => {
1002 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(directory
1003 .as_std_path()
1004 .join(local_settings_file_relative_path().as_std_path()))));
1005 }
1006 }
1007 }),
1008 LocalSettingsKind::Tasks => {
1009 let result = task_store.update(cx, |task_store, cx| {
1010 task_store.update_user_tasks(
1011 TaskSettingsLocation::Worktree(SettingsLocation {
1012 worktree_id,
1013 path: directory.as_ref(),
1014 }),
1015 file_content.as_deref(),
1016 cx,
1017 )
1018 });
1019
1020 match result {
1021 Err(InvalidSettingsError::Tasks { path, message }) => {
1022 log::error!("Failed to set local tasks in {path:?}: {message:?}");
1023 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1024 InvalidSettingsError::Tasks { path, message },
1025 )));
1026 }
1027 Err(e) => {
1028 log::error!("Failed to set local tasks: {e}");
1029 }
1030 Ok(()) => {
1031 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(directory
1032 .as_std_path()
1033 .join(task_file_name()))));
1034 }
1035 }
1036 }
1037 LocalSettingsKind::Debug => {
1038 let result = task_store.update(cx, |task_store, cx| {
1039 task_store.update_user_debug_scenarios(
1040 TaskSettingsLocation::Worktree(SettingsLocation {
1041 worktree_id,
1042 path: directory.as_ref(),
1043 }),
1044 file_content.as_deref(),
1045 cx,
1046 )
1047 });
1048
1049 match result {
1050 Err(InvalidSettingsError::Debug { path, message }) => {
1051 log::error!(
1052 "Failed to set local debug scenarios in {path:?}: {message:?}"
1053 );
1054 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1055 InvalidSettingsError::Debug { path, message },
1056 )));
1057 }
1058 Err(e) => {
1059 log::error!("Failed to set local tasks: {e}");
1060 }
1061 Ok(()) => {
1062 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(directory
1063 .as_std_path()
1064 .join(task_file_name()))));
1065 }
1066 }
1067 }
1068 };
1069
1070 if let Some(downstream_client) = &self.downstream_client {
1071 downstream_client
1072 .send(proto::UpdateWorktreeSettings {
1073 project_id: self.project_id,
1074 worktree_id: remote_worktree_id.to_proto(),
1075 path: directory.to_proto(),
1076 content: file_content.clone(),
1077 kind: Some(local_settings_kind_to_proto(kind).into()),
1078 })
1079 .log_err();
1080 }
1081 }
1082 }
1083
1084 fn subscribe_to_global_task_file_changes(
1085 fs: Arc<dyn Fs>,
1086 file_path: PathBuf,
1087 cx: &mut Context<Self>,
1088 ) -> Task<()> {
1089 let mut user_tasks_file_rx =
1090 watch_config_file(cx.background_executor(), fs, file_path.clone());
1091 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1092 let weak_entry = cx.weak_entity();
1093 cx.spawn(async move |settings_observer, cx| {
1094 let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1095 settings_observer.task_store.clone()
1096 }) else {
1097 return;
1098 };
1099 if let Some(user_tasks_content) = user_tasks_content {
1100 let Ok(()) = task_store.update(cx, |task_store, cx| {
1101 task_store
1102 .update_user_tasks(
1103 TaskSettingsLocation::Global(&file_path),
1104 Some(&user_tasks_content),
1105 cx,
1106 )
1107 .log_err();
1108 }) else {
1109 return;
1110 };
1111 }
1112 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1113 let Ok(result) = task_store.update(cx, |task_store, cx| {
1114 task_store.update_user_tasks(
1115 TaskSettingsLocation::Global(&file_path),
1116 Some(&user_tasks_content),
1117 cx,
1118 )
1119 }) else {
1120 break;
1121 };
1122
1123 weak_entry
1124 .update(cx, |_, cx| match result {
1125 Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1126 file_path.clone()
1127 ))),
1128 Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1129 InvalidSettingsError::Tasks {
1130 path: file_path.clone(),
1131 message: err.to_string(),
1132 },
1133 ))),
1134 })
1135 .ok();
1136 }
1137 })
1138 }
1139 fn subscribe_to_global_debug_scenarios_changes(
1140 fs: Arc<dyn Fs>,
1141 file_path: PathBuf,
1142 cx: &mut Context<Self>,
1143 ) -> Task<()> {
1144 let mut user_tasks_file_rx =
1145 watch_config_file(cx.background_executor(), fs, file_path.clone());
1146 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1147 let weak_entry = cx.weak_entity();
1148 cx.spawn(async move |settings_observer, cx| {
1149 let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1150 settings_observer.task_store.clone()
1151 }) else {
1152 return;
1153 };
1154 if let Some(user_tasks_content) = user_tasks_content {
1155 let Ok(()) = task_store.update(cx, |task_store, cx| {
1156 task_store
1157 .update_user_debug_scenarios(
1158 TaskSettingsLocation::Global(&file_path),
1159 Some(&user_tasks_content),
1160 cx,
1161 )
1162 .log_err();
1163 }) else {
1164 return;
1165 };
1166 }
1167 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1168 let Ok(result) = task_store.update(cx, |task_store, cx| {
1169 task_store.update_user_debug_scenarios(
1170 TaskSettingsLocation::Global(&file_path),
1171 Some(&user_tasks_content),
1172 cx,
1173 )
1174 }) else {
1175 break;
1176 };
1177
1178 weak_entry
1179 .update(cx, |_, cx| match result {
1180 Ok(()) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(Ok(
1181 file_path.clone(),
1182 ))),
1183 Err(err) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(
1184 Err(InvalidSettingsError::Tasks {
1185 path: file_path.clone(),
1186 message: err.to_string(),
1187 }),
1188 )),
1189 })
1190 .ok();
1191 }
1192 })
1193 }
1194}
1195
1196pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
1197 match kind {
1198 proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
1199 proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
1200 proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
1201 proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
1202 }
1203}
1204
1205pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
1206 match kind {
1207 LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
1208 LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
1209 LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
1210 LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
1211 }
1212}
1213
1214#[derive(Debug, Clone)]
1215pub struct DapSettings {
1216 pub binary: DapBinary,
1217 pub args: Vec<String>,
1218 pub env: HashMap<String, String>,
1219}
1220
1221impl From<DapSettingsContent> for DapSettings {
1222 fn from(content: DapSettingsContent) -> Self {
1223 DapSettings {
1224 binary: content
1225 .binary
1226 .map_or_else(|| DapBinary::Default, |binary| DapBinary::Custom(binary)),
1227 args: content.args.unwrap_or_default(),
1228 env: content.env.unwrap_or_default(),
1229 }
1230 }
1231}
1232
1233#[derive(Debug, Clone)]
1234pub enum DapBinary {
1235 Default,
1236 Custom(String),
1237}