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