project_settings.rs

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