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