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