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 /// How hunks are displayed visually in the editor.
172 ///
173 /// Default: transparent
174 pub hunk_style: Option<GitHunkStyleSetting>,
175}
176
177impl GitSettings {
178 pub fn inline_blame_enabled(&self) -> bool {
179 #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
180 match self.inline_blame {
181 Some(InlineBlameSettings { enabled, .. }) => enabled,
182 _ => false,
183 }
184 }
185
186 pub fn inline_blame_delay(&self) -> Option<Duration> {
187 match self.inline_blame {
188 Some(InlineBlameSettings {
189 delay_ms: Some(delay_ms),
190 ..
191 }) if delay_ms > 0 => Some(Duration::from_millis(delay_ms)),
192 _ => None,
193 }
194 }
195
196 pub fn show_inline_commit_summary(&self) -> bool {
197 match self.inline_blame {
198 Some(InlineBlameSettings {
199 show_commit_summary,
200 ..
201 }) => show_commit_summary,
202 _ => false,
203 }
204 }
205}
206
207#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
208#[serde(rename_all = "snake_case")]
209pub enum GitHunkStyleSetting {
210 /// Show unstaged hunks with a transparent background
211 #[default]
212 Transparent,
213 /// Show unstaged hunks with a pattern background
214 Pattern,
215 /// Show unstaged hunks with a border background
216 Border,
217
218 /// Show staged hunks with a pattern background
219 StagedPattern,
220 /// Show staged hunks with a pattern background
221 StagedTransparent,
222 /// Show staged hunks with a pattern background
223 StagedBorder,
224}
225
226#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
227#[serde(rename_all = "snake_case")]
228pub enum GitGutterSetting {
229 /// Show git gutter in tracked files.
230 #[default]
231 TrackedFiles,
232 /// Hide git gutter
233 Hide,
234}
235
236#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
237#[serde(rename_all = "snake_case")]
238pub struct InlineBlameSettings {
239 /// Whether or not to show git blame data inline in
240 /// the currently focused line.
241 ///
242 /// Default: true
243 #[serde(default = "true_value")]
244 pub enabled: bool,
245 /// Whether to only show the inline blame information
246 /// after a delay once the cursor stops moving.
247 ///
248 /// Default: 0
249 pub delay_ms: Option<u64>,
250 /// The minimum column number to show the inline blame information at
251 ///
252 /// Default: 0
253 pub min_column: Option<u32>,
254 /// Whether to show commit summary as part of the inline blame.
255 ///
256 /// Default: false
257 #[serde(default)]
258 pub show_commit_summary: bool,
259}
260
261const fn true_value() -> bool {
262 true
263}
264
265#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
266pub struct BinarySettings {
267 pub path: Option<String>,
268 pub arguments: Option<Vec<String>>,
269 pub ignore_system_version: Option<bool>,
270}
271
272#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
273#[serde(rename_all = "snake_case")]
274pub struct LspSettings {
275 pub binary: Option<BinarySettings>,
276 pub initialization_options: Option<serde_json::Value>,
277 pub settings: Option<serde_json::Value>,
278}
279
280#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
281pub struct SessionSettings {
282 /// Whether or not to restore unsaved buffers on restart.
283 ///
284 /// If this is true, user won't be prompted whether to save/discard
285 /// dirty files when closing the application.
286 ///
287 /// Default: true
288 pub restore_unsaved_buffers: bool,
289}
290
291impl Default for SessionSettings {
292 fn default() -> Self {
293 Self {
294 restore_unsaved_buffers: true,
295 }
296 }
297}
298
299impl Settings for ProjectSettings {
300 const KEY: Option<&'static str> = None;
301
302 type FileContent = Self;
303
304 fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
305 sources.json_merge()
306 }
307}
308
309pub enum SettingsObserverMode {
310 Local(Arc<dyn Fs>),
311 Remote,
312}
313
314#[derive(Clone, Debug, PartialEq)]
315pub enum SettingsObserverEvent {
316 LocalSettingsUpdated(Result<(), InvalidSettingsError>),
317}
318
319impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
320
321pub struct SettingsObserver {
322 mode: SettingsObserverMode,
323 downstream_client: Option<AnyProtoClient>,
324 worktree_store: Entity<WorktreeStore>,
325 project_id: u64,
326 task_store: Entity<TaskStore>,
327}
328
329/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
330/// (or the equivalent protobuf messages from upstream) and updates local settings
331/// and sends notifications downstream.
332/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
333/// upstream.
334impl SettingsObserver {
335 pub fn init(client: &AnyProtoClient) {
336 client.add_entity_message_handler(Self::handle_update_worktree_settings);
337 }
338
339 pub fn new_local(
340 fs: Arc<dyn Fs>,
341 worktree_store: Entity<WorktreeStore>,
342 task_store: Entity<TaskStore>,
343 cx: &mut Context<Self>,
344 ) -> Self {
345 cx.subscribe(&worktree_store, Self::on_worktree_store_event)
346 .detach();
347
348 Self {
349 worktree_store,
350 task_store,
351 mode: SettingsObserverMode::Local(fs),
352 downstream_client: None,
353 project_id: 0,
354 }
355 }
356
357 pub fn new_remote(
358 worktree_store: Entity<WorktreeStore>,
359 task_store: Entity<TaskStore>,
360 _: &mut Context<Self>,
361 ) -> Self {
362 Self {
363 worktree_store,
364 task_store,
365 mode: SettingsObserverMode::Remote,
366 downstream_client: None,
367 project_id: 0,
368 }
369 }
370
371 pub fn shared(
372 &mut self,
373 project_id: u64,
374 downstream_client: AnyProtoClient,
375 cx: &mut Context<Self>,
376 ) {
377 self.project_id = project_id;
378 self.downstream_client = Some(downstream_client.clone());
379
380 let store = cx.global::<SettingsStore>();
381 for worktree in self.worktree_store.read(cx).worktrees() {
382 let worktree_id = worktree.read(cx).id().to_proto();
383 for (path, content) in store.local_settings(worktree.read(cx).id()) {
384 downstream_client
385 .send(proto::UpdateWorktreeSettings {
386 project_id,
387 worktree_id,
388 path: path.to_proto(),
389 content: Some(content),
390 kind: Some(
391 local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
392 ),
393 })
394 .log_err();
395 }
396 for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
397 downstream_client
398 .send(proto::UpdateWorktreeSettings {
399 project_id,
400 worktree_id,
401 path: path.to_proto(),
402 content: Some(content),
403 kind: Some(
404 local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
405 ),
406 })
407 .log_err();
408 }
409 }
410 }
411
412 pub fn unshared(&mut self, _: &mut Context<Self>) {
413 self.downstream_client = None;
414 }
415
416 async fn handle_update_worktree_settings(
417 this: Entity<Self>,
418 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
419 mut cx: AsyncApp,
420 ) -> anyhow::Result<()> {
421 let kind = match envelope.payload.kind {
422 Some(kind) => proto::LocalSettingsKind::from_i32(kind)
423 .with_context(|| format!("unknown kind {kind}"))?,
424 None => proto::LocalSettingsKind::Settings,
425 };
426 this.update(&mut cx, |this, cx| {
427 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
428 let Some(worktree) = this
429 .worktree_store
430 .read(cx)
431 .worktree_for_id(worktree_id, cx)
432 else {
433 return;
434 };
435
436 this.update_settings(
437 worktree,
438 [(
439 Arc::<Path>::from_proto(envelope.payload.path.clone()),
440 local_settings_kind_from_proto(kind),
441 envelope.payload.content,
442 )],
443 cx,
444 );
445 })?;
446 Ok(())
447 }
448
449 fn on_worktree_store_event(
450 &mut self,
451 _: Entity<WorktreeStore>,
452 event: &WorktreeStoreEvent,
453 cx: &mut Context<Self>,
454 ) {
455 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
456 cx.subscribe(worktree, |this, worktree, event, cx| {
457 if let worktree::Event::UpdatedEntries(changes) = event {
458 this.update_local_worktree_settings(&worktree, changes, cx)
459 }
460 })
461 .detach()
462 }
463 }
464
465 fn update_local_worktree_settings(
466 &mut self,
467 worktree: &Entity<Worktree>,
468 changes: &UpdatedEntriesSet,
469 cx: &mut Context<Self>,
470 ) {
471 let SettingsObserverMode::Local(fs) = &self.mode else {
472 return;
473 };
474
475 let mut settings_contents = Vec::new();
476 for (path, _, change) in changes.iter() {
477 let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
478 let settings_dir = Arc::<Path>::from(
479 path.ancestors()
480 .nth(local_settings_file_relative_path().components().count())
481 .unwrap(),
482 );
483 (settings_dir, LocalSettingsKind::Settings)
484 } else if path.ends_with(local_tasks_file_relative_path()) {
485 let settings_dir = Arc::<Path>::from(
486 path.ancestors()
487 .nth(
488 local_tasks_file_relative_path()
489 .components()
490 .count()
491 .saturating_sub(1),
492 )
493 .unwrap(),
494 );
495 (settings_dir, LocalSettingsKind::Tasks)
496 } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
497 let settings_dir = Arc::<Path>::from(
498 path.ancestors()
499 .nth(
500 local_vscode_tasks_file_relative_path()
501 .components()
502 .count()
503 .saturating_sub(1),
504 )
505 .unwrap(),
506 );
507 (settings_dir, LocalSettingsKind::Tasks)
508 } else if path.ends_with(EDITORCONFIG_NAME) {
509 let Some(settings_dir) = path.parent().map(Arc::from) else {
510 continue;
511 };
512 (settings_dir, LocalSettingsKind::Editorconfig)
513 } else {
514 continue;
515 };
516
517 let removed = change == &PathChange::Removed;
518 let fs = fs.clone();
519 let abs_path = match worktree.read(cx).absolutize(path) {
520 Ok(abs_path) => abs_path,
521 Err(e) => {
522 log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
523 continue;
524 }
525 };
526 settings_contents.push(async move {
527 (
528 settings_dir,
529 kind,
530 if removed {
531 None
532 } else {
533 Some(
534 async move {
535 let content = fs.load(&abs_path).await?;
536 if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
537 let vscode_tasks =
538 parse_json_with_comments::<VsCodeTaskFile>(&content)
539 .with_context(|| {
540 format!("parsing VSCode tasks, file {abs_path:?}")
541 })?;
542 let zed_tasks = TaskTemplates::try_from(vscode_tasks)
543 .with_context(|| {
544 format!(
545 "converting VSCode tasks into Zed ones, file {abs_path:?}"
546 )
547 })?;
548 serde_json::to_string(&zed_tasks).with_context(|| {
549 format!(
550 "serializing Zed tasks into JSON, file {abs_path:?}"
551 )
552 })
553 } else {
554 Ok(content)
555 }
556 }
557 .await,
558 )
559 },
560 )
561 });
562 }
563
564 if settings_contents.is_empty() {
565 return;
566 }
567
568 let worktree = worktree.clone();
569 cx.spawn(move |this, cx| async move {
570 let settings_contents: Vec<(Arc<Path>, _, _)> =
571 futures::future::join_all(settings_contents).await;
572 cx.update(|cx| {
573 this.update(cx, |this, cx| {
574 this.update_settings(
575 worktree,
576 settings_contents.into_iter().map(|(path, kind, content)| {
577 (path, kind, content.and_then(|c| c.log_err()))
578 }),
579 cx,
580 )
581 })
582 })
583 })
584 .detach();
585 }
586
587 fn update_settings(
588 &mut self,
589 worktree: Entity<Worktree>,
590 settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
591 cx: &mut Context<Self>,
592 ) {
593 let worktree_id = worktree.read(cx).id();
594 let remote_worktree_id = worktree.read(cx).id();
595 let task_store = self.task_store.clone();
596
597 for (directory, kind, file_content) in settings_contents {
598 match kind {
599 LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
600 .update_global::<SettingsStore, _>(|store, cx| {
601 let result = store.set_local_settings(
602 worktree_id,
603 directory.clone(),
604 kind,
605 file_content.as_deref(),
606 cx,
607 );
608
609 match result {
610 Err(InvalidSettingsError::LocalSettings { path, message }) => {
611 log::error!(
612 "Failed to set local settings in {:?}: {:?}",
613 path,
614 message
615 );
616 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
617 InvalidSettingsError::LocalSettings { path, message },
618 )));
619 }
620 Err(e) => {
621 log::error!("Failed to set local settings: {e}");
622 }
623 Ok(_) => {
624 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(())));
625 }
626 }
627 }),
628 LocalSettingsKind::Tasks => task_store.update(cx, |task_store, cx| {
629 task_store
630 .update_user_tasks(
631 Some(SettingsLocation {
632 worktree_id,
633 path: directory.as_ref(),
634 }),
635 file_content.as_deref(),
636 cx,
637 )
638 .log_err();
639 }),
640 };
641
642 if let Some(downstream_client) = &self.downstream_client {
643 downstream_client
644 .send(proto::UpdateWorktreeSettings {
645 project_id: self.project_id,
646 worktree_id: remote_worktree_id.to_proto(),
647 path: directory.to_proto(),
648 content: file_content,
649 kind: Some(local_settings_kind_to_proto(kind).into()),
650 })
651 .log_err();
652 }
653 }
654 }
655}
656
657pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
658 match kind {
659 proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
660 proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
661 proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
662 }
663}
664
665pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
666 match kind {
667 LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
668 LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
669 LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
670 }
671}