project_settings.rs

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