1use anyhow::Context as _;
2use collections::HashMap;
3use fs::Fs;
4use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter};
5use lsp::LanguageServerName;
6use paths::{
7 local_settings_file_relative_path, local_tasks_file_relative_path,
8 local_vscode_tasks_file_relative_path, EDITORCONFIG_NAME,
9};
10use rpc::{
11 proto::{self, FromProto, ToProto},
12 AnyProtoClient, TypedEnvelope,
13};
14use schemars::JsonSchema;
15use serde::{Deserialize, Serialize};
16use settings::{
17 parse_json_with_comments, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation,
18 SettingsSources, SettingsStore,
19};
20use std::{path::Path, sync::Arc, time::Duration};
21use task::{TaskTemplates, VsCodeTaskFile};
22use util::ResultExt;
23use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
24
25use crate::{
26 task_store::TaskStore,
27 worktree_store::{WorktreeStore, WorktreeStoreEvent},
28};
29
30#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
31pub struct ProjectSettings {
32 /// Configuration for language servers.
33 ///
34 /// The following settings can be overridden for specific language servers:
35 /// - initialization_options
36 ///
37 /// To override settings for a language, add an entry for that language server's
38 /// name to the lsp value.
39 /// Default: null
40 #[serde(default)]
41 pub lsp: HashMap<LanguageServerName, LspSettings>,
42
43 /// Configuration for Diagnostics-related features.
44 #[serde(default)]
45 pub diagnostics: DiagnosticsSettings,
46
47 /// Configuration for Git-related features
48 #[serde(default)]
49 pub git: GitSettings,
50
51 /// Configuration for Node-related features
52 #[serde(default)]
53 pub node: NodeBinarySettings,
54
55 /// Configuration for how direnv configuration should be loaded
56 #[serde(default)]
57 pub load_direnv: DirenvSettings,
58
59 /// Configuration for session-related features
60 #[serde(default)]
61 pub session: SessionSettings,
62}
63
64#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
65pub struct NodeBinarySettings {
66 /// The path to the Node binary.
67 pub path: Option<String>,
68 /// The path to the npm binary Zed should use (defaults to `.path/../npm`).
69 pub npm_path: Option<String>,
70 /// If disabled, Zed will download its own copy of Node.
71 #[serde(default)]
72 pub ignore_system_version: Option<bool>,
73}
74
75#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
76#[serde(rename_all = "snake_case")]
77pub enum DirenvSettings {
78 /// Load direnv configuration through a shell hook
79 ShellHook,
80 /// Load direnv configuration directly using `direnv export json`
81 #[default]
82 Direct,
83}
84
85#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
86pub struct DiagnosticsSettings {
87 /// Whether or not to include warning diagnostics
88 #[serde(default = "true_value")]
89 pub include_warnings: bool,
90
91 /// Settings for showing inline diagnostics
92 #[serde(default)]
93 pub inline: InlineDiagnosticsSettings,
94}
95
96#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
97pub struct InlineDiagnosticsSettings {
98 /// Whether or not to show inline diagnostics
99 ///
100 /// Default: false
101 #[serde(default)]
102 pub enabled: bool,
103 /// Whether to only show the inline diaganostics after a delay after the
104 /// last editor event.
105 ///
106 /// Default: 150
107 #[serde(default = "default_inline_diagnostics_debounce_ms")]
108 pub update_debounce_ms: u64,
109 /// The amount of padding between the end of the source line and the start
110 /// of the inline diagnostic in units of columns.
111 ///
112 /// Default: 4
113 #[serde(default = "default_inline_diagnostics_padding")]
114 pub padding: u32,
115 /// The minimum column to display inline diagnostics. This setting can be
116 /// used to horizontally align inline diagnostics at some position. Lines
117 /// longer than this value will still push diagnostics further to the right.
118 ///
119 /// Default: 0
120 #[serde(default)]
121 pub min_column: u32,
122
123 #[serde(default)]
124 pub max_severity: Option<DiagnosticSeverity>,
125}
126
127#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
128#[serde(rename_all = "snake_case")]
129pub enum DiagnosticSeverity {
130 Error,
131 Warning,
132 Info,
133 Hint,
134}
135
136impl Default for InlineDiagnosticsSettings {
137 fn default() -> Self {
138 Self {
139 enabled: false,
140 update_debounce_ms: default_inline_diagnostics_debounce_ms(),
141 padding: default_inline_diagnostics_padding(),
142 min_column: 0,
143 max_severity: None,
144 }
145 }
146}
147
148fn default_inline_diagnostics_debounce_ms() -> u64 {
149 150
150}
151
152fn default_inline_diagnostics_padding() -> u32 {
153 4
154}
155
156#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
157pub struct GitSettings {
158 /// Whether or not to show the git gutter.
159 ///
160 /// Default: tracked_files
161 pub git_gutter: Option<GitGutterSetting>,
162 /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
163 ///
164 /// Default: null
165 pub gutter_debounce: Option<u64>,
166 /// Whether or not to show git blame data inline in
167 /// the currently focused line.
168 ///
169 /// Default: on
170 pub inline_blame: Option<InlineBlameSettings>,
171}
172
173impl GitSettings {
174 pub fn inline_blame_enabled(&self) -> bool {
175 #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
176 match self.inline_blame {
177 Some(InlineBlameSettings { enabled, .. }) => enabled,
178 _ => false,
179 }
180 }
181
182 pub fn inline_blame_delay(&self) -> Option<Duration> {
183 match self.inline_blame {
184 Some(InlineBlameSettings {
185 delay_ms: Some(delay_ms),
186 ..
187 }) if delay_ms > 0 => Some(Duration::from_millis(delay_ms)),
188 _ => None,
189 }
190 }
191
192 pub fn show_inline_commit_summary(&self) -> bool {
193 match self.inline_blame {
194 Some(InlineBlameSettings {
195 show_commit_summary,
196 ..
197 }) => show_commit_summary,
198 _ => false,
199 }
200 }
201}
202
203#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
204#[serde(rename_all = "snake_case")]
205pub enum GitHunkStyleSetting {
206 /// Show unstaged hunks with a transparent background
207 #[default]
208 Transparent,
209 /// Show unstaged hunks with a pattern background
210 Pattern,
211 /// Show unstaged hunks with a border background
212 Border,
213
214 /// Show staged hunks with a pattern background
215 StagedPattern,
216 /// Show staged hunks with a pattern background
217 StagedTransparent,
218 /// Show staged hunks with a pattern background
219 StagedBorder,
220}
221
222#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
223#[serde(rename_all = "snake_case")]
224pub enum GitGutterSetting {
225 /// Show git gutter in tracked files.
226 #[default]
227 TrackedFiles,
228 /// Hide git gutter
229 Hide,
230}
231
232#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
233#[serde(rename_all = "snake_case")]
234pub struct InlineBlameSettings {
235 /// Whether or not to show git blame data inline in
236 /// the currently focused line.
237 ///
238 /// Default: true
239 #[serde(default = "true_value")]
240 pub enabled: bool,
241 /// Whether to only show the inline blame information
242 /// after a delay once the cursor stops moving.
243 ///
244 /// Default: 0
245 pub delay_ms: Option<u64>,
246 /// The minimum column number to show the inline blame information at
247 ///
248 /// Default: 0
249 pub min_column: Option<u32>,
250 /// Whether to show commit summary as part of the inline blame.
251 ///
252 /// Default: false
253 #[serde(default)]
254 pub show_commit_summary: bool,
255}
256
257const fn true_value() -> bool {
258 true
259}
260
261#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
262pub struct BinarySettings {
263 pub path: Option<String>,
264 pub arguments: Option<Vec<String>>,
265 pub ignore_system_version: Option<bool>,
266}
267
268#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
269#[serde(rename_all = "snake_case")]
270pub struct LspSettings {
271 pub binary: Option<BinarySettings>,
272 pub initialization_options: Option<serde_json::Value>,
273 pub settings: Option<serde_json::Value>,
274}
275
276#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
277pub struct SessionSettings {
278 /// Whether or not to restore unsaved buffers on restart.
279 ///
280 /// If this is true, user won't be prompted whether to save/discard
281 /// dirty files when closing the application.
282 ///
283 /// Default: true
284 pub restore_unsaved_buffers: bool,
285}
286
287impl Default for SessionSettings {
288 fn default() -> Self {
289 Self {
290 restore_unsaved_buffers: true,
291 }
292 }
293}
294
295impl Settings for ProjectSettings {
296 const KEY: Option<&'static str> = None;
297
298 type FileContent = Self;
299
300 fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
301 sources.json_merge()
302 }
303}
304
305pub enum SettingsObserverMode {
306 Local(Arc<dyn Fs>),
307 Remote,
308}
309
310#[derive(Clone, Debug, PartialEq)]
311pub enum SettingsObserverEvent {
312 LocalSettingsUpdated(Result<(), InvalidSettingsError>),
313}
314
315impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
316
317pub struct SettingsObserver {
318 mode: SettingsObserverMode,
319 downstream_client: Option<AnyProtoClient>,
320 worktree_store: Entity<WorktreeStore>,
321 project_id: u64,
322 task_store: Entity<TaskStore>,
323}
324
325/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
326/// (or the equivalent protobuf messages from upstream) and updates local settings
327/// and sends notifications downstream.
328/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
329/// upstream.
330impl SettingsObserver {
331 pub fn init(client: &AnyProtoClient) {
332 client.add_entity_message_handler(Self::handle_update_worktree_settings);
333 }
334
335 pub fn new_local(
336 fs: Arc<dyn Fs>,
337 worktree_store: Entity<WorktreeStore>,
338 task_store: Entity<TaskStore>,
339 cx: &mut Context<Self>,
340 ) -> Self {
341 cx.subscribe(&worktree_store, Self::on_worktree_store_event)
342 .detach();
343
344 Self {
345 worktree_store,
346 task_store,
347 mode: SettingsObserverMode::Local(fs),
348 downstream_client: None,
349 project_id: 0,
350 }
351 }
352
353 pub fn new_remote(
354 worktree_store: Entity<WorktreeStore>,
355 task_store: Entity<TaskStore>,
356 _: &mut Context<Self>,
357 ) -> Self {
358 Self {
359 worktree_store,
360 task_store,
361 mode: SettingsObserverMode::Remote,
362 downstream_client: None,
363 project_id: 0,
364 }
365 }
366
367 pub fn shared(
368 &mut self,
369 project_id: u64,
370 downstream_client: AnyProtoClient,
371 cx: &mut Context<Self>,
372 ) {
373 self.project_id = project_id;
374 self.downstream_client = Some(downstream_client.clone());
375
376 let store = cx.global::<SettingsStore>();
377 for worktree in self.worktree_store.read(cx).worktrees() {
378 let worktree_id = worktree.read(cx).id().to_proto();
379 for (path, content) in store.local_settings(worktree.read(cx).id()) {
380 downstream_client
381 .send(proto::UpdateWorktreeSettings {
382 project_id,
383 worktree_id,
384 path: path.to_proto(),
385 content: Some(content),
386 kind: Some(
387 local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
388 ),
389 })
390 .log_err();
391 }
392 for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
393 downstream_client
394 .send(proto::UpdateWorktreeSettings {
395 project_id,
396 worktree_id,
397 path: path.to_proto(),
398 content: Some(content),
399 kind: Some(
400 local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
401 ),
402 })
403 .log_err();
404 }
405 }
406 }
407
408 pub fn unshared(&mut self, _: &mut Context<Self>) {
409 self.downstream_client = None;
410 }
411
412 async fn handle_update_worktree_settings(
413 this: Entity<Self>,
414 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
415 mut cx: AsyncApp,
416 ) -> anyhow::Result<()> {
417 let kind = match envelope.payload.kind {
418 Some(kind) => proto::LocalSettingsKind::from_i32(kind)
419 .with_context(|| format!("unknown kind {kind}"))?,
420 None => proto::LocalSettingsKind::Settings,
421 };
422 this.update(&mut cx, |this, cx| {
423 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
424 let Some(worktree) = this
425 .worktree_store
426 .read(cx)
427 .worktree_for_id(worktree_id, cx)
428 else {
429 return;
430 };
431
432 this.update_settings(
433 worktree,
434 [(
435 Arc::<Path>::from_proto(envelope.payload.path.clone()),
436 local_settings_kind_from_proto(kind),
437 envelope.payload.content,
438 )],
439 cx,
440 );
441 })?;
442 Ok(())
443 }
444
445 fn on_worktree_store_event(
446 &mut self,
447 _: Entity<WorktreeStore>,
448 event: &WorktreeStoreEvent,
449 cx: &mut Context<Self>,
450 ) {
451 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
452 cx.subscribe(worktree, |this, worktree, event, cx| {
453 if let worktree::Event::UpdatedEntries(changes) = event {
454 this.update_local_worktree_settings(&worktree, changes, cx)
455 }
456 })
457 .detach()
458 }
459 }
460
461 fn update_local_worktree_settings(
462 &mut self,
463 worktree: &Entity<Worktree>,
464 changes: &UpdatedEntriesSet,
465 cx: &mut Context<Self>,
466 ) {
467 let SettingsObserverMode::Local(fs) = &self.mode else {
468 return;
469 };
470
471 let mut settings_contents = Vec::new();
472 for (path, _, change) in changes.iter() {
473 let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
474 let settings_dir = Arc::<Path>::from(
475 path.ancestors()
476 .nth(local_settings_file_relative_path().components().count())
477 .unwrap(),
478 );
479 (settings_dir, LocalSettingsKind::Settings)
480 } else if path.ends_with(local_tasks_file_relative_path()) {
481 let settings_dir = Arc::<Path>::from(
482 path.ancestors()
483 .nth(
484 local_tasks_file_relative_path()
485 .components()
486 .count()
487 .saturating_sub(1),
488 )
489 .unwrap(),
490 );
491 (settings_dir, LocalSettingsKind::Tasks)
492 } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
493 let settings_dir = Arc::<Path>::from(
494 path.ancestors()
495 .nth(
496 local_vscode_tasks_file_relative_path()
497 .components()
498 .count()
499 .saturating_sub(1),
500 )
501 .unwrap(),
502 );
503 (settings_dir, LocalSettingsKind::Tasks)
504 } else if path.ends_with(EDITORCONFIG_NAME) {
505 let Some(settings_dir) = path.parent().map(Arc::from) else {
506 continue;
507 };
508 (settings_dir, LocalSettingsKind::Editorconfig)
509 } else {
510 continue;
511 };
512
513 let removed = change == &PathChange::Removed;
514 let fs = fs.clone();
515 let abs_path = match worktree.read(cx).absolutize(path) {
516 Ok(abs_path) => abs_path,
517 Err(e) => {
518 log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
519 continue;
520 }
521 };
522 settings_contents.push(async move {
523 (
524 settings_dir,
525 kind,
526 if removed {
527 None
528 } else {
529 Some(
530 async move {
531 let content = fs.load(&abs_path).await?;
532 if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
533 let vscode_tasks =
534 parse_json_with_comments::<VsCodeTaskFile>(&content)
535 .with_context(|| {
536 format!("parsing VSCode tasks, file {abs_path:?}")
537 })?;
538 let zed_tasks = TaskTemplates::try_from(vscode_tasks)
539 .with_context(|| {
540 format!(
541 "converting VSCode tasks into Zed ones, file {abs_path:?}"
542 )
543 })?;
544 serde_json::to_string(&zed_tasks).with_context(|| {
545 format!(
546 "serializing Zed tasks into JSON, file {abs_path:?}"
547 )
548 })
549 } else {
550 Ok(content)
551 }
552 }
553 .await,
554 )
555 },
556 )
557 });
558 }
559
560 if settings_contents.is_empty() {
561 return;
562 }
563
564 let worktree = worktree.clone();
565 cx.spawn(move |this, cx| async move {
566 let settings_contents: Vec<(Arc<Path>, _, _)> =
567 futures::future::join_all(settings_contents).await;
568 cx.update(|cx| {
569 this.update(cx, |this, cx| {
570 this.update_settings(
571 worktree,
572 settings_contents.into_iter().map(|(path, kind, content)| {
573 (path, kind, content.and_then(|c| c.log_err()))
574 }),
575 cx,
576 )
577 })
578 })
579 })
580 .detach();
581 }
582
583 fn update_settings(
584 &mut self,
585 worktree: Entity<Worktree>,
586 settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
587 cx: &mut Context<Self>,
588 ) {
589 let worktree_id = worktree.read(cx).id();
590 let remote_worktree_id = worktree.read(cx).id();
591 let task_store = self.task_store.clone();
592
593 for (directory, kind, file_content) in settings_contents {
594 match kind {
595 LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
596 .update_global::<SettingsStore, _>(|store, cx| {
597 let result = store.set_local_settings(
598 worktree_id,
599 directory.clone(),
600 kind,
601 file_content.as_deref(),
602 cx,
603 );
604
605 match result {
606 Err(InvalidSettingsError::LocalSettings { path, message }) => {
607 log::error!(
608 "Failed to set local settings in {:?}: {:?}",
609 path,
610 message
611 );
612 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
613 InvalidSettingsError::LocalSettings { path, message },
614 )));
615 }
616 Err(e) => {
617 log::error!("Failed to set local settings: {e}");
618 }
619 Ok(_) => {
620 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(())));
621 }
622 }
623 }),
624 LocalSettingsKind::Tasks => task_store.update(cx, |task_store, cx| {
625 task_store
626 .update_user_tasks(
627 Some(SettingsLocation {
628 worktree_id,
629 path: directory.as_ref(),
630 }),
631 file_content.as_deref(),
632 cx,
633 )
634 .log_err();
635 }),
636 };
637
638 if let Some(downstream_client) = &self.downstream_client {
639 downstream_client
640 .send(proto::UpdateWorktreeSettings {
641 project_id: self.project_id,
642 worktree_id: remote_worktree_id.to_proto(),
643 path: directory.to_proto(),
644 content: file_content,
645 kind: Some(local_settings_kind_to_proto(kind).into()),
646 })
647 .log_err();
648 }
649 }
650 }
651}
652
653pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
654 match kind {
655 proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
656 proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
657 proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
658 }
659}
660
661pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
662 match kind {
663 LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
664 LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
665 LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
666 }
667}