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, 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 settings::DiagnosticSeverityContent::All => DiagnosticSeverity::Hint,
219 }
220 }
221}
222
223/// Determines the severity of the diagnostic that should be moved to.
224#[derive(PartialEq, PartialOrd, Clone, Copy, Debug, Eq, Deserialize, JsonSchema)]
225#[serde(rename_all = "snake_case")]
226pub enum GoToDiagnosticSeverity {
227 /// Errors
228 Error = 3,
229 /// Warnings
230 Warning = 2,
231 /// Information
232 Information = 1,
233 /// Hints
234 Hint = 0,
235}
236
237impl From<lsp::DiagnosticSeverity> for GoToDiagnosticSeverity {
238 fn from(severity: lsp::DiagnosticSeverity) -> Self {
239 match severity {
240 lsp::DiagnosticSeverity::ERROR => Self::Error,
241 lsp::DiagnosticSeverity::WARNING => Self::Warning,
242 lsp::DiagnosticSeverity::INFORMATION => Self::Information,
243 lsp::DiagnosticSeverity::HINT => Self::Hint,
244 _ => Self::Error,
245 }
246 }
247}
248
249impl GoToDiagnosticSeverity {
250 pub fn min() -> Self {
251 Self::Hint
252 }
253
254 pub fn max() -> Self {
255 Self::Error
256 }
257}
258
259/// Allows filtering diagnostics that should be moved to.
260#[derive(PartialEq, Clone, Copy, Debug, Deserialize, JsonSchema)]
261#[serde(untagged)]
262pub enum GoToDiagnosticSeverityFilter {
263 /// Move to diagnostics of a specific severity.
264 Only(GoToDiagnosticSeverity),
265
266 /// Specify a range of severities to include.
267 Range {
268 /// Minimum severity to move to. Defaults no "error".
269 #[serde(default = "GoToDiagnosticSeverity::min")]
270 min: GoToDiagnosticSeverity,
271 /// Maximum severity to move to. Defaults to "hint".
272 #[serde(default = "GoToDiagnosticSeverity::max")]
273 max: GoToDiagnosticSeverity,
274 },
275}
276
277impl Default for GoToDiagnosticSeverityFilter {
278 fn default() -> Self {
279 Self::Range {
280 min: GoToDiagnosticSeverity::min(),
281 max: GoToDiagnosticSeverity::max(),
282 }
283 }
284}
285
286impl GoToDiagnosticSeverityFilter {
287 pub fn matches(&self, severity: lsp::DiagnosticSeverity) -> bool {
288 let severity: GoToDiagnosticSeverity = severity.into();
289 match self {
290 Self::Only(target) => *target == severity,
291 Self::Range { min, max } => severity >= *min && severity <= *max,
292 }
293 }
294}
295
296#[derive(Copy, Clone, Debug)]
297pub struct GitSettings {
298 /// Whether or not to show the git gutter.
299 ///
300 /// Default: tracked_files
301 pub git_gutter: settings::GitGutterSetting,
302 /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
303 ///
304 /// Default: 0
305 pub gutter_debounce: u64,
306 /// Whether or not to show git blame data inline in
307 /// the currently focused line.
308 ///
309 /// Default: on
310 pub inline_blame: InlineBlameSettings,
311 /// Git blame settings.
312 pub blame: BlameSettings,
313 /// Which information to show in the branch picker.
314 ///
315 /// Default: on
316 pub branch_picker: BranchPickerSettings,
317 /// How hunks are displayed visually in the editor.
318 ///
319 /// Default: staged_hollow
320 pub hunk_style: settings::GitHunkStyleSetting,
321}
322
323#[derive(Clone, Copy, Debug)]
324pub struct InlineBlameSettings {
325 /// Whether or not to show git blame data inline in
326 /// the currently focused line.
327 ///
328 /// Default: true
329 pub enabled: bool,
330 /// Whether to only show the inline blame information
331 /// after a delay once the cursor stops moving.
332 ///
333 /// Default: 0
334 pub delay_ms: std::time::Duration,
335 /// The amount of padding between the end of the source line and the start
336 /// of the inline blame in units of columns.
337 ///
338 /// Default: 7
339 pub padding: u32,
340 /// The minimum column number to show the inline blame information at
341 ///
342 /// Default: 0
343 pub min_column: u32,
344 /// Whether to show commit summary as part of the inline blame.
345 ///
346 /// Default: false
347 pub show_commit_summary: bool,
348}
349
350#[derive(Clone, Copy, Debug)]
351pub struct BlameSettings {
352 /// Whether to show the avatar of the author of the commit.
353 ///
354 /// Default: true
355 pub show_avatar: bool,
356}
357
358impl GitSettings {
359 pub fn inline_blame_delay(&self) -> Option<Duration> {
360 if self.inline_blame.delay_ms.as_millis() > 0 {
361 Some(self.inline_blame.delay_ms)
362 } else {
363 None
364 }
365 }
366}
367
368#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
369#[serde(rename_all = "snake_case")]
370pub struct BranchPickerSettings {
371 /// Whether to show author name as part of the commit information.
372 ///
373 /// Default: false
374 #[serde(default)]
375 pub show_author_name: bool,
376}
377
378impl Default for BranchPickerSettings {
379 fn default() -> Self {
380 Self {
381 show_author_name: true,
382 }
383 }
384}
385
386#[derive(Clone, Debug)]
387pub struct DiagnosticsSettings {
388 /// Whether to show the project diagnostics button in the status bar.
389 pub button: bool,
390
391 /// Whether or not to include warning diagnostics.
392 pub include_warnings: bool,
393
394 /// Settings for using LSP pull diagnostics mechanism in Zed.
395 pub lsp_pull_diagnostics: LspPullDiagnosticsSettings,
396
397 /// Settings for showing inline diagnostics.
398 pub inline: InlineDiagnosticsSettings,
399}
400
401#[derive(Clone, Copy, Debug, PartialEq, Eq)]
402pub struct InlineDiagnosticsSettings {
403 /// Whether or not to show inline diagnostics
404 ///
405 /// Default: false
406 pub enabled: bool,
407 /// Whether to only show the inline diagnostics after a delay after the
408 /// last editor event.
409 ///
410 /// Default: 150
411 pub update_debounce_ms: u64,
412 /// The amount of padding between the end of the source line and the start
413 /// of the inline diagnostic in units of columns.
414 ///
415 /// Default: 4
416 pub padding: u32,
417 /// The minimum column to display inline diagnostics. This setting can be
418 /// used to horizontally align inline diagnostics at some position. Lines
419 /// longer than this value will still push diagnostics further to the right.
420 ///
421 /// Default: 0
422 pub min_column: u32,
423
424 pub max_severity: Option<DiagnosticSeverity>,
425}
426
427#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
428pub struct LspPullDiagnosticsSettings {
429 /// Whether to pull for diagnostics or not.
430 ///
431 /// Default: true
432 pub enabled: bool,
433 /// Minimum time to wait before pulling diagnostics from the language server(s).
434 /// 0 turns the debounce off.
435 ///
436 /// Default: 50
437 pub debounce_ms: u64,
438}
439
440impl Settings for ProjectSettings {
441 fn from_settings(content: &settings::SettingsContent) -> Self {
442 let project = &content.project.clone();
443 let diagnostics = content.diagnostics.as_ref().unwrap();
444 let lsp_pull_diagnostics = diagnostics.lsp_pull_diagnostics.as_ref().unwrap();
445 let inline_diagnostics = diagnostics.inline.as_ref().unwrap();
446
447 let git = content.git.as_ref().unwrap();
448 let git_settings = GitSettings {
449 git_gutter: git.git_gutter.unwrap(),
450 gutter_debounce: git.gutter_debounce.unwrap_or_default(),
451 inline_blame: {
452 let inline = git.inline_blame.unwrap();
453 InlineBlameSettings {
454 enabled: inline.enabled.unwrap(),
455 delay_ms: std::time::Duration::from_millis(inline.delay_ms.unwrap()),
456 padding: inline.padding.unwrap(),
457 min_column: inline.min_column.unwrap(),
458 show_commit_summary: inline.show_commit_summary.unwrap(),
459 }
460 },
461 blame: {
462 let blame = git.blame.unwrap();
463 BlameSettings {
464 show_avatar: blame.show_avatar.unwrap(),
465 }
466 },
467 branch_picker: {
468 let branch_picker = git.branch_picker.unwrap();
469 BranchPickerSettings {
470 show_author_name: branch_picker.show_author_name.unwrap(),
471 }
472 },
473 hunk_style: git.hunk_style.unwrap(),
474 };
475 Self {
476 context_servers: project
477 .context_servers
478 .clone()
479 .into_iter()
480 .map(|(key, value)| (key, value.into()))
481 .collect(),
482 lsp: project
483 .lsp
484 .clone()
485 .into_iter()
486 .map(|(key, value)| (LanguageServerName(key.into()), value))
487 .collect(),
488 global_lsp_settings: GlobalLspSettings {
489 button: content
490 .global_lsp_settings
491 .as_ref()
492 .unwrap()
493 .button
494 .unwrap(),
495 },
496 dap: project
497 .dap
498 .clone()
499 .into_iter()
500 .map(|(key, value)| (DebugAdapterName(key.into()), DapSettings::from(value)))
501 .collect(),
502 diagnostics: DiagnosticsSettings {
503 button: diagnostics.button.unwrap(),
504 include_warnings: diagnostics.include_warnings.unwrap(),
505 lsp_pull_diagnostics: LspPullDiagnosticsSettings {
506 enabled: lsp_pull_diagnostics.enabled.unwrap(),
507 debounce_ms: lsp_pull_diagnostics.debounce_ms.unwrap(),
508 },
509 inline: InlineDiagnosticsSettings {
510 enabled: inline_diagnostics.enabled.unwrap(),
511 update_debounce_ms: inline_diagnostics.update_debounce_ms.unwrap(),
512 padding: inline_diagnostics.padding.unwrap(),
513 min_column: inline_diagnostics.min_column.unwrap(),
514 max_severity: inline_diagnostics.max_severity.map(Into::into),
515 },
516 },
517 git: git_settings,
518 node: content.node.clone().unwrap().into(),
519 load_direnv: project.load_direnv.clone().unwrap(),
520 session: SessionSettings {
521 restore_unsaved_buffers: content.session.unwrap().restore_unsaved_buffers.unwrap(),
522 },
523 }
524 }
525
526 fn import_from_vscode(
527 vscode: &settings::VsCodeSettings,
528 current: &mut settings::SettingsContent,
529 ) {
530 // this just sets the binary name instead of a full path so it relies on path lookup
531 // resolving to the one you want
532 let npm_path = vscode.read_enum("npm.packageManager", |s| match s {
533 v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
534 _ => None,
535 });
536 if npm_path.is_some() {
537 current.node.get_or_insert_default().npm_path = npm_path;
538 }
539
540 if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") {
541 current
542 .git
543 .get_or_insert_default()
544 .inline_blame
545 .get_or_insert_default()
546 .enabled = Some(b);
547 }
548
549 #[derive(Deserialize)]
550 struct VsCodeContextServerCommand {
551 command: PathBuf,
552 args: Option<Vec<String>>,
553 env: Option<HashMap<String, String>>,
554 // note: we don't support envFile and type
555 }
556 if let Some(mcp) = vscode.read_value("mcp").and_then(|v| v.as_object()) {
557 current
558 .project
559 .context_servers
560 .extend(mcp.iter().filter_map(|(k, v)| {
561 Some((
562 k.clone().into(),
563 settings::ContextServerSettingsContent::Custom {
564 enabled: true,
565 command: serde_json::from_value::<VsCodeContextServerCommand>(
566 v.clone(),
567 )
568 .ok()
569 .map(|cmd| {
570 settings::ContextServerCommand {
571 path: cmd.command,
572 args: cmd.args.unwrap_or_default(),
573 env: cmd.env,
574 timeout: None,
575 }
576 })?,
577 },
578 ))
579 }));
580 }
581
582 // TODO: translate lsp settings for rust-analyzer and other popular ones to old.lsp
583 }
584}
585
586pub enum SettingsObserverMode {
587 Local(Arc<dyn Fs>),
588 Remote,
589}
590
591#[derive(Clone, Debug, PartialEq)]
592pub enum SettingsObserverEvent {
593 LocalSettingsUpdated(Result<PathBuf, InvalidSettingsError>),
594 LocalTasksUpdated(Result<PathBuf, InvalidSettingsError>),
595 LocalDebugScenariosUpdated(Result<PathBuf, InvalidSettingsError>),
596}
597
598impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
599
600pub struct SettingsObserver {
601 mode: SettingsObserverMode,
602 downstream_client: Option<AnyProtoClient>,
603 worktree_store: Entity<WorktreeStore>,
604 project_id: u64,
605 task_store: Entity<TaskStore>,
606 _user_settings_watcher: Option<Subscription>,
607 _global_task_config_watcher: Task<()>,
608 _global_debug_config_watcher: Task<()>,
609}
610
611/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
612/// (or the equivalent protobuf messages from upstream) and updates local settings
613/// and sends notifications downstream.
614/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
615/// upstream.
616impl SettingsObserver {
617 pub fn init(client: &AnyProtoClient) {
618 client.add_entity_message_handler(Self::handle_update_worktree_settings);
619 client.add_entity_message_handler(Self::handle_update_user_settings);
620 }
621
622 pub fn new_local(
623 fs: Arc<dyn Fs>,
624 worktree_store: Entity<WorktreeStore>,
625 task_store: Entity<TaskStore>,
626 cx: &mut Context<Self>,
627 ) -> Self {
628 cx.subscribe(&worktree_store, Self::on_worktree_store_event)
629 .detach();
630
631 Self {
632 worktree_store,
633 task_store,
634 mode: SettingsObserverMode::Local(fs.clone()),
635 downstream_client: None,
636 _user_settings_watcher: None,
637 project_id: REMOTE_SERVER_PROJECT_ID,
638 _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
639 fs.clone(),
640 paths::tasks_file().clone(),
641 cx,
642 ),
643 _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
644 fs.clone(),
645 paths::debug_scenarios_file().clone(),
646 cx,
647 ),
648 }
649 }
650
651 pub fn new_remote(
652 fs: Arc<dyn Fs>,
653 worktree_store: Entity<WorktreeStore>,
654 task_store: Entity<TaskStore>,
655 upstream_client: Option<AnyProtoClient>,
656 cx: &mut Context<Self>,
657 ) -> Self {
658 let mut user_settings_watcher = None;
659 if cx.try_global::<SettingsStore>().is_some() {
660 if let Some(upstream_client) = upstream_client {
661 let mut user_settings = None;
662 user_settings_watcher = Some(cx.observe_global::<SettingsStore>(move |_, cx| {
663 if let Some(new_settings) = cx.global::<SettingsStore>().raw_user_settings() {
664 if Some(new_settings) != user_settings.as_ref() {
665 if let Some(new_settings_string) =
666 serde_json::to_string(new_settings).ok()
667 {
668 user_settings = Some(new_settings.clone());
669 upstream_client
670 .send(proto::UpdateUserSettings {
671 project_id: REMOTE_SERVER_PROJECT_ID,
672 contents: new_settings_string,
673 })
674 .log_err();
675 }
676 }
677 }
678 }));
679 }
680 };
681
682 Self {
683 worktree_store,
684 task_store,
685 mode: SettingsObserverMode::Remote,
686 downstream_client: None,
687 project_id: REMOTE_SERVER_PROJECT_ID,
688 _user_settings_watcher: user_settings_watcher,
689 _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
690 fs.clone(),
691 paths::tasks_file().clone(),
692 cx,
693 ),
694 _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
695 fs.clone(),
696 paths::debug_scenarios_file().clone(),
697 cx,
698 ),
699 }
700 }
701
702 pub fn shared(
703 &mut self,
704 project_id: u64,
705 downstream_client: AnyProtoClient,
706 cx: &mut Context<Self>,
707 ) {
708 self.project_id = project_id;
709 self.downstream_client = Some(downstream_client.clone());
710
711 let store = cx.global::<SettingsStore>();
712 for worktree in self.worktree_store.read(cx).worktrees() {
713 let worktree_id = worktree.read(cx).id().to_proto();
714 for (path, content) in store.local_settings(worktree.read(cx).id()) {
715 let content = serde_json::to_string(&content).unwrap();
716 downstream_client
717 .send(proto::UpdateWorktreeSettings {
718 project_id,
719 worktree_id,
720 path: path.to_proto(),
721 content: Some(content),
722 kind: Some(
723 local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
724 ),
725 })
726 .log_err();
727 }
728 for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
729 downstream_client
730 .send(proto::UpdateWorktreeSettings {
731 project_id,
732 worktree_id,
733 path: path.to_proto(),
734 content: Some(content),
735 kind: Some(
736 local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
737 ),
738 })
739 .log_err();
740 }
741 }
742 }
743
744 pub fn unshared(&mut self, _: &mut Context<Self>) {
745 self.downstream_client = None;
746 }
747
748 async fn handle_update_worktree_settings(
749 this: Entity<Self>,
750 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
751 mut cx: AsyncApp,
752 ) -> anyhow::Result<()> {
753 let kind = match envelope.payload.kind {
754 Some(kind) => proto::LocalSettingsKind::from_i32(kind)
755 .with_context(|| format!("unknown kind {kind}"))?,
756 None => proto::LocalSettingsKind::Settings,
757 };
758 let path = RelPath::from_proto(&envelope.payload.path)?;
759 this.update(&mut cx, |this, cx| {
760 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
761 let Some(worktree) = this
762 .worktree_store
763 .read(cx)
764 .worktree_for_id(worktree_id, cx)
765 else {
766 return;
767 };
768
769 this.update_settings(
770 worktree,
771 [(
772 path,
773 local_settings_kind_from_proto(kind),
774 envelope.payload.content,
775 )],
776 cx,
777 );
778 })?;
779 Ok(())
780 }
781
782 async fn handle_update_user_settings(
783 _: Entity<Self>,
784 envelope: TypedEnvelope<proto::UpdateUserSettings>,
785 cx: AsyncApp,
786 ) -> anyhow::Result<()> {
787 cx.update_global(|settings_store: &mut SettingsStore, cx| {
788 settings_store
789 .set_user_settings(&envelope.payload.contents, cx)
790 .context("setting new user settings")?;
791 anyhow::Ok(())
792 })??;
793 Ok(())
794 }
795
796 fn on_worktree_store_event(
797 &mut self,
798 _: Entity<WorktreeStore>,
799 event: &WorktreeStoreEvent,
800 cx: &mut Context<Self>,
801 ) {
802 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
803 cx.subscribe(worktree, |this, worktree, event, cx| {
804 if let worktree::Event::UpdatedEntries(changes) = event {
805 this.update_local_worktree_settings(&worktree, changes, cx)
806 }
807 })
808 .detach()
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}