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