1use crate::*;
2use anyhow::{Context as _, Result, anyhow};
3use collections::HashMap;
4use fs::Fs;
5use gpui::Rgba;
6use paths::{cursor_settings_file_paths, vscode_settings_file_paths};
7use serde::Deserialize;
8use serde_json::{Map, Value};
9use std::{
10 num::{NonZeroU32, NonZeroUsize},
11 path::{Path, PathBuf},
12 sync::Arc,
13};
14
15#[derive(Clone, Copy, PartialEq, Eq, Debug)]
16pub enum VsCodeSettingsSource {
17 VsCode,
18 Cursor,
19}
20
21impl std::fmt::Display for VsCodeSettingsSource {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 match self {
24 VsCodeSettingsSource::VsCode => write!(f, "VS Code"),
25 VsCodeSettingsSource::Cursor => write!(f, "Cursor"),
26 }
27 }
28}
29
30pub struct VsCodeSettings {
31 pub source: VsCodeSettingsSource,
32 pub path: Arc<Path>,
33 content: Map<String, Value>,
34}
35
36impl VsCodeSettings {
37 #[cfg(any(test, feature = "test-support"))]
38 pub fn from_str(content: &str, source: VsCodeSettingsSource) -> Result<Self> {
39 Ok(Self {
40 source,
41 path: Path::new("/example-path/Code/User/settings.json").into(),
42 content: serde_json_lenient::from_str(content)?,
43 })
44 }
45
46 pub async fn load_user_settings(source: VsCodeSettingsSource, fs: Arc<dyn Fs>) -> Result<Self> {
47 let candidate_paths = match source {
48 VsCodeSettingsSource::VsCode => vscode_settings_file_paths(),
49 VsCodeSettingsSource::Cursor => cursor_settings_file_paths(),
50 };
51 let mut path = None;
52 for candidate_path in candidate_paths.iter() {
53 if fs.is_file(candidate_path).await {
54 path = Some(candidate_path.clone());
55 }
56 }
57 let Some(path) = path else {
58 return Err(anyhow!(
59 "No settings file found, expected to find it in one of the following paths:\n{}",
60 candidate_paths
61 .into_iter()
62 .map(|path| path.to_string_lossy().into_owned())
63 .collect::<Vec<_>>()
64 .join("\n")
65 ));
66 };
67 let content = fs.load(&path).await.with_context(|| {
68 format!(
69 "Error loading {} settings file from {}",
70 source,
71 path.display()
72 )
73 })?;
74 let content = serde_json_lenient::from_str(&content).with_context(|| {
75 format!(
76 "Error parsing {} settings file from {}",
77 source,
78 path.display()
79 )
80 })?;
81 Ok(Self {
82 source,
83 path: path.into(),
84 content,
85 })
86 }
87
88 fn read_value(&self, setting: &str) -> Option<&Value> {
89 self.content.get(setting)
90 }
91
92 fn read_str(&self, setting: &str) -> Option<&str> {
93 self.read_value(setting).and_then(|v| v.as_str())
94 }
95
96 fn read_string(&self, setting: &str) -> Option<String> {
97 self.read_value(setting)
98 .and_then(|v| v.as_str())
99 .map(|s| s.to_owned())
100 }
101
102 fn read_bool(&self, setting: &str) -> Option<bool> {
103 self.read_value(setting).and_then(|v| v.as_bool())
104 }
105
106 fn read_f32(&self, setting: &str) -> Option<f32> {
107 self.read_value(setting)
108 .and_then(|v| v.as_f64())
109 .map(|v| v as f32)
110 }
111
112 fn read_u64(&self, setting: &str) -> Option<u64> {
113 self.read_value(setting).and_then(|v| v.as_u64())
114 }
115
116 fn read_usize(&self, setting: &str) -> Option<usize> {
117 self.read_value(setting)
118 .and_then(|v| v.as_u64())
119 .and_then(|v| v.try_into().ok())
120 }
121
122 fn read_u32(&self, setting: &str) -> Option<u32> {
123 self.read_value(setting)
124 .and_then(|v| v.as_u64())
125 .and_then(|v| v.try_into().ok())
126 }
127
128 fn read_enum<T>(&self, key: &str, f: impl FnOnce(&str) -> Option<T>) -> Option<T> {
129 self.content.get(key).and_then(Value::as_str).and_then(f)
130 }
131
132 fn read_fonts(&self, key: &str) -> (Option<FontFamilyName>, Option<Vec<FontFamilyName>>) {
133 let Some(css_name) = self.content.get(key).and_then(Value::as_str) else {
134 return (None, None);
135 };
136
137 let mut name_buffer = String::new();
138 let mut quote_char: Option<char> = None;
139 let mut fonts = Vec::new();
140 let mut add_font = |buffer: &mut String| {
141 let trimmed = buffer.trim();
142 if !trimmed.is_empty() {
143 fonts.push(trimmed.to_string().into());
144 }
145
146 buffer.clear();
147 };
148
149 for ch in css_name.chars() {
150 match (ch, quote_char) {
151 ('"' | '\'', None) => {
152 quote_char = Some(ch);
153 }
154 (_, Some(q)) if ch == q => {
155 quote_char = None;
156 }
157 (',', None) => {
158 add_font(&mut name_buffer);
159 }
160 _ => {
161 name_buffer.push(ch);
162 }
163 }
164 }
165
166 add_font(&mut name_buffer);
167 if fonts.is_empty() {
168 return (None, None);
169 }
170 (Some(fonts.remove(0)), skip_default(fonts))
171 }
172
173 pub fn settings_content(&self) -> SettingsContent {
174 SettingsContent {
175 agent: self.agent_settings_content(),
176 agent_servers: None,
177 audio: None,
178 auto_update: None,
179 base_keymap: Some(BaseKeymapContent::VSCode),
180 calls: None,
181 collaboration_panel: None,
182 debugger: None,
183 diagnostics: None,
184 disable_ai: None,
185 editor: self.editor_settings_content(),
186 extension: ExtensionSettingsContent::default(),
187 file_finder: None,
188 git: self.git_settings_content(),
189 git_panel: self.git_panel_settings_content(),
190 global_lsp_settings: skip_default(GlobalLspSettingsContent {
191 semantic_token_rules: self.semantic_token_rules(),
192 ..GlobalLspSettingsContent::default()
193 }),
194 helix_mode: None,
195 image_viewer: None,
196 journal: None,
197 language_models: None,
198 line_indicator_format: None,
199 log: None,
200 message_editor: None,
201 node: self.node_binary_settings(),
202 notification_panel: None,
203 outline_panel: self.outline_panel_settings_content(),
204 preview_tabs: self.preview_tabs_settings_content(),
205 project: self.project_settings_content(),
206 project_panel: self.project_panel_settings_content(),
207 proxy: self.read_string("http.proxy"),
208 remote: RemoteSettingsContent::default(),
209 repl: None,
210 server_url: None,
211 session: None,
212 status_bar: self.status_bar_settings_content(),
213 tab_bar: self.tab_bar_settings_content(),
214 tabs: self.item_settings_content(),
215 telemetry: self.telemetry_settings_content(),
216 terminal: self.terminal_settings_content(),
217 theme: Box::new(self.theme_settings_content()),
218 title_bar: None,
219 vim: None,
220 vim_mode: None,
221 workspace: self.workspace_settings_content(),
222 which_key: None,
223 }
224 }
225
226 fn agent_settings_content(&self) -> Option<AgentSettingsContent> {
227 let enabled = self.read_bool("chat.agent.enabled");
228 skip_default(AgentSettingsContent {
229 enabled: enabled,
230 button: enabled,
231 ..Default::default()
232 })
233 }
234
235 fn editor_settings_content(&self) -> EditorSettingsContent {
236 EditorSettingsContent {
237 auto_signature_help: self.read_bool("editor.parameterHints.enabled"),
238 autoscroll_on_clicks: None,
239 cursor_blink: self.read_enum("editor.cursorBlinking", |s| match s {
240 "blink" | "phase" | "expand" | "smooth" => Some(true),
241 "solid" => Some(false),
242 _ => None,
243 }),
244 cursor_shape: self.read_enum("editor.cursorStyle", |s| match s {
245 "block" => Some(CursorShape::Block),
246 "block-outline" => Some(CursorShape::Hollow),
247 "line" | "line-thin" => Some(CursorShape::Bar),
248 "underline" | "underline-thin" => Some(CursorShape::Underline),
249 _ => None,
250 }),
251 current_line_highlight: self.read_enum("editor.renderLineHighlight", |s| match s {
252 "gutter" => Some(CurrentLineHighlight::Gutter),
253 "line" => Some(CurrentLineHighlight::Line),
254 "all" => Some(CurrentLineHighlight::All),
255 _ => None,
256 }),
257 diagnostics_max_severity: None,
258 double_click_in_multibuffer: None,
259 drag_and_drop_selection: None,
260 excerpt_context_lines: None,
261 expand_excerpt_lines: None,
262 fast_scroll_sensitivity: self.read_f32("editor.fastScrollSensitivity"),
263 sticky_scroll: self.sticky_scroll_content(),
264 go_to_definition_fallback: None,
265 gutter: self.gutter_content(),
266 hide_mouse: None,
267 horizontal_scroll_margin: None,
268 hover_popover_delay: self.read_u64("editor.hover.delay").map(Into::into),
269 hover_popover_enabled: self.read_bool("editor.hover.enabled"),
270 inline_code_actions: None,
271 jupyter: None,
272 lsp_document_colors: None,
273 lsp_highlight_debounce: None,
274 middle_click_paste: None,
275 minimap: self.minimap_content(),
276 minimum_contrast_for_highlights: None,
277 multi_cursor_modifier: self.read_enum("editor.multiCursorModifier", |s| match s {
278 "ctrlCmd" => Some(MultiCursorModifier::CmdOrCtrl),
279 "alt" => Some(MultiCursorModifier::Alt),
280 _ => None,
281 }),
282 redact_private_values: None,
283 relative_line_numbers: self.read_enum("editor.lineNumbers", |s| match s {
284 "relative" => Some(RelativeLineNumbers::Enabled),
285 _ => None,
286 }),
287 rounded_selection: self.read_bool("editor.roundedSelection"),
288 scroll_beyond_last_line: None,
289 scroll_sensitivity: self.read_f32("editor.mouseWheelScrollSensitivity"),
290 scrollbar: self.scrollbar_content(),
291 search: self.search_content(),
292 search_wrap: None,
293 seed_search_query_from_cursor: self.read_enum(
294 "editor.find.seedSearchStringFromSelection",
295 |s| match s {
296 "always" => Some(SeedQuerySetting::Always),
297 "selection" => Some(SeedQuerySetting::Selection),
298 "never" => Some(SeedQuerySetting::Never),
299 _ => None,
300 },
301 ),
302 selection_highlight: self.read_bool("editor.selectionHighlight"),
303 show_signature_help_after_edits: self.read_bool("editor.parameterHints.enabled"),
304 snippet_sort_order: None,
305 toolbar: None,
306 use_smartcase_search: self.read_bool("search.smartCase"),
307 vertical_scroll_margin: self.read_f32("editor.cursorSurroundingLines"),
308 completion_menu_scrollbar: None,
309 completion_detail_alignment: None,
310 }
311 }
312
313 fn sticky_scroll_content(&self) -> Option<StickyScrollContent> {
314 skip_default(StickyScrollContent {
315 enabled: self.read_bool("editor.stickyScroll.enabled"),
316 })
317 }
318
319 fn gutter_content(&self) -> Option<GutterContent> {
320 skip_default(GutterContent {
321 line_numbers: self.read_enum("editor.lineNumbers", |s| match s {
322 "on" | "relative" => Some(true),
323 "off" => Some(false),
324 _ => None,
325 }),
326 min_line_number_digits: None,
327 runnables: None,
328 breakpoints: None,
329 folds: self.read_enum("editor.showFoldingControls", |s| match s {
330 "always" | "mouseover" => Some(true),
331 "never" => Some(false),
332 _ => None,
333 }),
334 })
335 }
336
337 fn scrollbar_content(&self) -> Option<ScrollbarContent> {
338 let scrollbar_axes = skip_default(ScrollbarAxesContent {
339 horizontal: self.read_enum("editor.scrollbar.horizontal", |s| match s {
340 "auto" | "visible" => Some(true),
341 "hidden" => Some(false),
342 _ => None,
343 }),
344 vertical: self.read_enum("editor.scrollbar.vertical", |s| match s {
345 "auto" | "visible" => Some(true),
346 "hidden" => Some(false),
347 _ => None,
348 }),
349 })?;
350
351 Some(ScrollbarContent {
352 axes: Some(scrollbar_axes),
353 ..Default::default()
354 })
355 }
356
357 fn search_content(&self) -> Option<SearchSettingsContent> {
358 skip_default(SearchSettingsContent {
359 include_ignored: self.read_bool("search.useIgnoreFiles"),
360 ..Default::default()
361 })
362 }
363
364 fn semantic_token_rules(&self) -> Option<SemanticTokenRules> {
365 let customizations = self
366 .read_value("editor.semanticTokenColorCustomizations")?
367 .as_object()?;
368
369 skip_default(SemanticTokenRules {
370 rules: customizations
371 .get("rules")
372 .and_then(|v| {
373 Some(
374 v.as_object()?
375 .iter()
376 .filter_map(|(k, v)| {
377 let v = v.as_object()?;
378
379 let mut underline = v
380 .get("underline")
381 .and_then(|b| b.as_bool())
382 .unwrap_or(false);
383 let strikethrough = v
384 .get("strikethrough")
385 .and_then(|b| b.as_bool())
386 .unwrap_or(false);
387 let mut font_weight =
388 v.get("bold").and_then(|b| b.as_bool()).map(|b| {
389 if b {
390 SemanticTokenFontWeight::Bold
391 } else {
392 SemanticTokenFontWeight::Normal
393 }
394 });
395 let mut font_style =
396 v.get("italic").and_then(|b| b.as_bool()).map(|b| {
397 if b {
398 SemanticTokenFontStyle::Italic
399 } else {
400 SemanticTokenFontStyle::Normal
401 }
402 });
403
404 match v.get("fontStyle").and_then(|s| s.as_str()).unwrap_or("") {
405 "bold" => {
406 font_style = Some(SemanticTokenFontStyle::Normal);
407 font_weight = Some(SemanticTokenFontWeight::Bold);
408 }
409 "italic" => {
410 font_style = Some(SemanticTokenFontStyle::Italic);
411 font_weight = Some(SemanticTokenFontWeight::Normal);
412 }
413 "underline" => {
414 underline = true;
415 }
416 "bold italic" | "italic bold" => {
417 font_style = Some(SemanticTokenFontStyle::Italic);
418 font_weight = Some(SemanticTokenFontWeight::Bold);
419 }
420 "normal" => {
421 font_style = Some(SemanticTokenFontStyle::Normal);
422 font_weight = Some(SemanticTokenFontWeight::Normal);
423 }
424 _ => {}
425 }
426
427 let foreground = v
428 .get("foreground")
429 .and_then(|v| Rgba::try_from(v.as_str()?).ok())
430 .map(|s| s.to_owned());
431 let background = v
432 .get("background")
433 .and_then(|v| Rgba::try_from(v.as_str()?).ok())
434 .map(|s| s.to_owned());
435
436 Some(SemanticTokenRule {
437 token_type: Some(k.clone()),
438 token_modifiers: vec![],
439 style: vec![],
440 underline: if underline {
441 Some(SemanticTokenColorOverride::InheritForeground(true))
442 } else {
443 None
444 },
445 strikethrough: if strikethrough {
446 Some(SemanticTokenColorOverride::InheritForeground(true))
447 } else {
448 None
449 },
450 foreground_color: foreground,
451 background_color: background,
452 font_weight,
453 font_style,
454 })
455 })
456 .collect(),
457 )
458 })
459 .unwrap_or_default(),
460 })
461 }
462
463 fn minimap_content(&self) -> Option<MinimapContent> {
464 let minimap_enabled = self.read_bool("editor.minimap.enabled");
465 let autohide = self.read_bool("editor.minimap.autohide");
466 let show = match (minimap_enabled, autohide) {
467 (Some(true), Some(false)) => Some(ShowMinimap::Always),
468 (Some(true), _) => Some(ShowMinimap::Auto),
469 (Some(false), _) => Some(ShowMinimap::Never),
470 _ => None,
471 };
472
473 skip_default(MinimapContent {
474 show,
475 thumb: self.read_enum("editor.minimap.showSlider", |s| match s {
476 "always" => Some(MinimapThumb::Always),
477 "mouseover" => Some(MinimapThumb::Hover),
478 _ => None,
479 }),
480 max_width_columns: self
481 .read_u32("editor.minimap.maxColumn")
482 .and_then(|v| NonZeroU32::new(v)),
483 ..Default::default()
484 })
485 }
486
487 fn git_panel_settings_content(&self) -> Option<GitPanelSettingsContent> {
488 skip_default(GitPanelSettingsContent {
489 button: self.read_bool("git.enabled"),
490 fallback_branch_name: self.read_string("git.defaultBranchName"),
491 ..Default::default()
492 })
493 }
494
495 fn project_settings_content(&self) -> ProjectSettingsContent {
496 ProjectSettingsContent {
497 all_languages: AllLanguageSettingsContent {
498 edit_predictions: self.edit_predictions_settings_content(),
499 defaults: self.default_language_settings_content(),
500 languages: Default::default(),
501 file_types: self.file_types(),
502 },
503 worktree: self.worktree_settings_content(),
504 lsp: Default::default(),
505 terminal: None,
506 dap: Default::default(),
507 context_servers: self.context_servers(),
508 context_server_timeout: None,
509 load_direnv: None,
510 slash_commands: None,
511 git_hosting_providers: None,
512 }
513 }
514
515 fn default_language_settings_content(&self) -> LanguageSettingsContent {
516 LanguageSettingsContent {
517 allow_rewrap: None,
518 always_treat_brackets_as_autoclosed: None,
519 auto_indent: None,
520 auto_indent_on_paste: self.read_bool("editor.formatOnPaste"),
521 code_actions_on_format: None,
522 completions: skip_default(CompletionSettingsContent {
523 words: self.read_bool("editor.suggest.showWords").map(|b| {
524 if b {
525 WordsCompletionMode::Enabled
526 } else {
527 WordsCompletionMode::Disabled
528 }
529 }),
530 ..Default::default()
531 }),
532 debuggers: None,
533 edit_predictions_disabled_in: None,
534 enable_language_server: None,
535 ensure_final_newline_on_save: self.read_bool("files.insertFinalNewline"),
536 extend_comment_on_newline: None,
537 extend_list_on_newline: None,
538 indent_list_on_tab: None,
539 format_on_save: self.read_bool("editor.guides.formatOnSave").map(|b| {
540 if b {
541 FormatOnSave::On
542 } else {
543 FormatOnSave::Off
544 }
545 }),
546 formatter: None,
547 hard_tabs: self.read_bool("editor.insertSpaces").map(|v| !v),
548 indent_guides: skip_default(IndentGuideSettingsContent {
549 enabled: self.read_bool("editor.guides.indentation"),
550 ..Default::default()
551 }),
552 inlay_hints: None,
553 jsx_tag_auto_close: None,
554 language_servers: None,
555 semantic_tokens: self
556 .read_bool("editor.semanticHighlighting.enabled")
557 .map(|enabled| {
558 if enabled {
559 SemanticTokens::Full
560 } else {
561 SemanticTokens::Off
562 }
563 }),
564 document_folding_ranges: None,
565 linked_edits: self.read_bool("editor.linkedEditing"),
566 preferred_line_length: self.read_u32("editor.wordWrapColumn"),
567 prettier: None,
568 remove_trailing_whitespace_on_save: self.read_bool("editor.trimAutoWhitespace"),
569 show_completion_documentation: None,
570 colorize_brackets: self.read_bool("editor.bracketPairColorization.enabled"),
571 show_completions_on_input: self.read_bool("editor.suggestOnTriggerCharacters"),
572 show_edit_predictions: self.read_bool("editor.inlineSuggest.enabled"),
573 show_whitespaces: self.read_enum("editor.renderWhitespace", |s| {
574 Some(match s {
575 "boundary" => ShowWhitespaceSetting::Boundary,
576 "trailing" => ShowWhitespaceSetting::Trailing,
577 "selection" => ShowWhitespaceSetting::Selection,
578 "all" => ShowWhitespaceSetting::All,
579 _ => ShowWhitespaceSetting::None,
580 })
581 }),
582 show_wrap_guides: None,
583 soft_wrap: self.read_enum("editor.wordWrap", |s| match s {
584 "on" => Some(SoftWrap::EditorWidth),
585 "wordWrapColumn" => Some(SoftWrap::PreferLine),
586 "bounded" => Some(SoftWrap::Bounded),
587 "off" => Some(SoftWrap::None),
588 _ => None,
589 }),
590 tab_size: self
591 .read_u32("editor.tabSize")
592 .and_then(|n| NonZeroU32::new(n)),
593 tasks: None,
594 use_auto_surround: self.read_enum("editor.autoSurround", |s| match s {
595 "languageDefined" | "quotes" | "brackets" => Some(true),
596 "never" => Some(false),
597 _ => None,
598 }),
599 use_autoclose: None,
600 use_on_type_format: self.read_bool("editor.formatOnType"),
601 whitespace_map: None,
602 wrap_guides: self
603 .read_value("editor.rulers")
604 .and_then(|v| v.as_array())
605 .map(|v| {
606 v.iter()
607 .flat_map(|n| n.as_u64().map(|n| n as usize))
608 .collect()
609 }),
610 word_diff_enabled: None,
611 }
612 }
613
614 fn file_types(&self) -> Option<HashMap<Arc<str>, ExtendingVec<String>>> {
615 // vscodes file association map is inverted from ours, so we flip the mapping before merging
616 let mut associations: HashMap<Arc<str>, ExtendingVec<String>> = HashMap::default();
617 let map = self.read_value("files.associations")?.as_object()?;
618 for (k, v) in map {
619 let Some(v) = v.as_str() else { continue };
620 associations.entry(v.into()).or_default().0.push(k.clone());
621 }
622 skip_default(associations)
623 }
624
625 fn edit_predictions_settings_content(&self) -> Option<EditPredictionSettingsContent> {
626 let disabled_globs = self
627 .read_value("cursor.general.globalCursorIgnoreList")?
628 .as_array()?;
629
630 skip_default(EditPredictionSettingsContent {
631 disabled_globs: skip_default(
632 disabled_globs
633 .iter()
634 .filter_map(|glob| glob.as_str())
635 .map(|s| s.to_string())
636 .collect(),
637 ),
638 ..Default::default()
639 })
640 }
641
642 fn outline_panel_settings_content(&self) -> Option<OutlinePanelSettingsContent> {
643 skip_default(OutlinePanelSettingsContent {
644 file_icons: self.read_bool("outline.icons"),
645 folder_icons: self.read_bool("outline.icons"),
646 git_status: self.read_bool("git.decorations.enabled"),
647 ..Default::default()
648 })
649 }
650
651 fn node_binary_settings(&self) -> Option<NodeBinarySettings> {
652 // this just sets the binary name instead of a full path so it relies on path lookup
653 // resolving to the one you want
654 skip_default(NodeBinarySettings {
655 npm_path: self.read_enum("npm.packageManager", |s| match s {
656 v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
657 _ => None,
658 }),
659 ..Default::default()
660 })
661 }
662
663 fn git_settings_content(&self) -> Option<GitSettings> {
664 let inline_blame = self.read_bool("git.blame.editorDecoration.enabled")?;
665 skip_default(GitSettings {
666 inline_blame: Some(InlineBlameSettings {
667 enabled: Some(inline_blame),
668 ..Default::default()
669 }),
670 ..Default::default()
671 })
672 }
673
674 fn context_servers(&self) -> HashMap<Arc<str>, ContextServerSettingsContent> {
675 #[derive(Deserialize)]
676 struct VsCodeContextServerCommand {
677 command: PathBuf,
678 args: Option<Vec<String>>,
679 env: Option<HashMap<String, String>>,
680 // note: we don't support envFile and type
681 }
682 let Some(mcp) = self.read_value("mcp").and_then(|v| v.as_object()) else {
683 return Default::default();
684 };
685 mcp.iter()
686 .filter_map(|(k, v)| {
687 Some((
688 k.clone().into(),
689 ContextServerSettingsContent::Stdio {
690 enabled: true,
691 remote: false,
692 command: serde_json::from_value::<VsCodeContextServerCommand>(v.clone())
693 .ok()
694 .map(|cmd| ContextServerCommand {
695 path: cmd.command,
696 args: cmd.args.unwrap_or_default(),
697 env: cmd.env,
698 timeout: None,
699 })?,
700 },
701 ))
702 })
703 .collect()
704 }
705
706 fn item_settings_content(&self) -> Option<ItemSettingsContent> {
707 skip_default(ItemSettingsContent {
708 git_status: self.read_bool("git.decorations.enabled"),
709 close_position: self.read_enum("workbench.editor.tabActionLocation", |s| match s {
710 "right" => Some(ClosePosition::Right),
711 "left" => Some(ClosePosition::Left),
712 _ => None,
713 }),
714 file_icons: self.read_bool("workbench.editor.showIcons"),
715 activate_on_close: self
716 .read_bool("workbench.editor.focusRecentEditorAfterClose")
717 .map(|b| {
718 if b {
719 ActivateOnClose::History
720 } else {
721 ActivateOnClose::LeftNeighbour
722 }
723 }),
724 show_diagnostics: None,
725 show_close_button: self
726 .read_bool("workbench.editor.tabActionCloseVisibility")
727 .map(|b| {
728 if b {
729 ShowCloseButton::Always
730 } else {
731 ShowCloseButton::Hidden
732 }
733 }),
734 })
735 }
736
737 fn preview_tabs_settings_content(&self) -> Option<PreviewTabsSettingsContent> {
738 skip_default(PreviewTabsSettingsContent {
739 enabled: self.read_bool("workbench.editor.enablePreview"),
740 enable_preview_from_project_panel: None,
741 enable_preview_from_file_finder: self
742 .read_bool("workbench.editor.enablePreviewFromQuickOpen"),
743 enable_preview_from_multibuffer: None,
744 enable_preview_multibuffer_from_code_navigation: None,
745 enable_preview_file_from_code_navigation: None,
746 enable_keep_preview_on_code_navigation: self
747 .read_bool("workbench.editor.enablePreviewFromCodeNavigation"),
748 })
749 }
750
751 fn tab_bar_settings_content(&self) -> Option<TabBarSettingsContent> {
752 skip_default(TabBarSettingsContent {
753 show: self.read_enum("workbench.editor.showTabs", |s| match s {
754 "multiple" => Some(true),
755 "single" | "none" => Some(false),
756 _ => None,
757 }),
758 show_nav_history_buttons: None,
759 show_tab_bar_buttons: self
760 .read_str("workbench.editor.editorActionsLocation")
761 .and_then(|str| if str == "hidden" { Some(false) } else { None }),
762 show_pinned_tabs_in_separate_row: None,
763 })
764 }
765
766 fn status_bar_settings_content(&self) -> Option<StatusBarSettingsContent> {
767 skip_default(StatusBarSettingsContent {
768 show: self.read_bool("workbench.statusBar.visible"),
769 active_language_button: None,
770 cursor_position_button: None,
771 line_endings_button: None,
772 active_encoding_button: None,
773 })
774 }
775
776 fn project_panel_settings_content(&self) -> Option<ProjectPanelSettingsContent> {
777 let mut project_panel_settings = ProjectPanelSettingsContent {
778 auto_fold_dirs: self.read_bool("explorer.compactFolders"),
779 auto_reveal_entries: self.read_bool("explorer.autoReveal"),
780 bold_folder_labels: None,
781 button: None,
782 default_width: None,
783 dock: None,
784 drag_and_drop: None,
785 entry_spacing: None,
786 file_icons: None,
787 folder_icons: None,
788 git_status: self.read_bool("git.decorations.enabled"),
789 hide_gitignore: self.read_bool("explorer.excludeGitIgnore"),
790 hide_hidden: None,
791 hide_root: None,
792 indent_guides: None,
793 indent_size: None,
794 scrollbar: None,
795 show_diagnostics: self
796 .read_bool("problems.decorations.enabled")
797 .and_then(|b| if b { Some(ShowDiagnostics::Off) } else { None }),
798 sort_mode: None,
799 starts_open: None,
800 sticky_scroll: None,
801 auto_open: None,
802 };
803
804 if let (Some(false), Some(false)) = (
805 self.read_bool("explorer.decorations.badges"),
806 self.read_bool("explorer.decorations.colors"),
807 ) {
808 project_panel_settings.git_status = Some(false);
809 project_panel_settings.show_diagnostics = Some(ShowDiagnostics::Off);
810 }
811
812 skip_default(project_panel_settings)
813 }
814
815 fn telemetry_settings_content(&self) -> Option<TelemetrySettingsContent> {
816 self.read_enum("telemetry.telemetryLevel", |level| {
817 let (metrics, diagnostics) = match level {
818 "all" => (true, true),
819 "error" | "crash" => (false, true),
820 "off" => (false, false),
821 _ => return None,
822 };
823 Some(TelemetrySettingsContent {
824 metrics: Some(metrics),
825 diagnostics: Some(diagnostics),
826 })
827 })
828 }
829
830 fn terminal_settings_content(&self) -> Option<TerminalSettingsContent> {
831 let (font_family, font_fallbacks) = self.read_fonts("terminal.integrated.fontFamily");
832 skip_default(TerminalSettingsContent {
833 alternate_scroll: None,
834 blinking: self
835 .read_bool("terminal.integrated.cursorBlinking")
836 .map(|b| {
837 if b {
838 TerminalBlink::On
839 } else {
840 TerminalBlink::Off
841 }
842 }),
843 button: None,
844 copy_on_select: self.read_bool("terminal.integrated.copyOnSelection"),
845 cursor_shape: self.read_enum("terminal.integrated.cursorStyle", |s| match s {
846 "block" => Some(CursorShapeContent::Block),
847 "line" => Some(CursorShapeContent::Bar),
848 "underline" => Some(CursorShapeContent::Underline),
849 _ => None,
850 }),
851 default_height: None,
852 default_width: None,
853 dock: None,
854 font_fallbacks,
855 font_family,
856 font_features: None,
857 font_size: self
858 .read_f32("terminal.integrated.fontSize")
859 .map(FontSize::from),
860 font_weight: None,
861 keep_selection_on_copy: None,
862 line_height: self
863 .read_f32("terminal.integrated.lineHeight")
864 .map(|lh| TerminalLineHeight::Custom(lh)),
865 max_scroll_history_lines: self.read_usize("terminal.integrated.scrollback"),
866 minimum_contrast: None,
867 option_as_meta: self.read_bool("terminal.integrated.macOptionIsMeta"),
868 project: self.project_terminal_settings_content(),
869 scrollbar: None,
870 scroll_multiplier: None,
871 toolbar: None,
872 })
873 }
874
875 fn project_terminal_settings_content(&self) -> ProjectTerminalSettingsContent {
876 #[cfg(target_os = "windows")]
877 let platform = "windows";
878 #[cfg(target_os = "linux")]
879 let platform = "linux";
880 #[cfg(target_os = "macos")]
881 let platform = "osx";
882 #[cfg(target_os = "freebsd")]
883 let platform = "freebsd";
884 let env = self
885 .read_value(&format!("terminal.integrated.env.{platform}"))
886 .and_then(|v| v.as_object())
887 .map(|v| {
888 v.iter()
889 .map(|(k, v)| (k.clone(), v.to_string()))
890 // zed does not support substitutions, so this can break env vars
891 .filter(|(_, v)| !v.contains('$'))
892 .collect()
893 });
894
895 ProjectTerminalSettingsContent {
896 // TODO: handle arguments
897 shell: self
898 .read_string(&format!("terminal.integrated.{platform}Exec"))
899 .map(|s| Shell::Program(s)),
900 working_directory: None,
901 env,
902 detect_venv: None,
903 path_hyperlink_regexes: None,
904 path_hyperlink_timeout_ms: None,
905 }
906 }
907
908 fn theme_settings_content(&self) -> ThemeSettingsContent {
909 let (buffer_font_family, buffer_font_fallbacks) = self.read_fonts("editor.fontFamily");
910 ThemeSettingsContent {
911 ui_font_size: None,
912 ui_font_family: None,
913 ui_font_fallbacks: None,
914 ui_font_features: None,
915 ui_font_weight: None,
916 buffer_font_family,
917 buffer_font_fallbacks,
918 buffer_font_size: self.read_f32("editor.fontSize").map(FontSize::from),
919 buffer_font_weight: self.read_f32("editor.fontWeight").map(FontWeightContent),
920 buffer_line_height: None,
921 buffer_font_features: None,
922 agent_ui_font_size: None,
923 agent_buffer_font_size: None,
924 theme: None,
925 icon_theme: None,
926 ui_density: None,
927 unnecessary_code_fade: None,
928 experimental_theme_overrides: None,
929 theme_overrides: Default::default(),
930 }
931 }
932
933 fn workspace_settings_content(&self) -> WorkspaceSettingsContent {
934 WorkspaceSettingsContent {
935 active_pane_modifiers: self.active_pane_modifiers(),
936 text_rendering_mode: None,
937 autosave: self.read_enum("files.autoSave", |s| match s {
938 "off" => Some(AutosaveSetting::Off),
939 "afterDelay" => Some(AutosaveSetting::AfterDelay {
940 milliseconds: self
941 .read_value("files.autoSaveDelay")
942 .and_then(|v| v.as_u64())
943 .unwrap_or(1000)
944 .into(),
945 }),
946 "onFocusChange" => Some(AutosaveSetting::OnFocusChange),
947 "onWindowChange" => Some(AutosaveSetting::OnWindowChange),
948 _ => None,
949 }),
950 bottom_dock_layout: None,
951 centered_layout: None,
952 close_on_file_delete: None,
953 command_aliases: Default::default(),
954 confirm_quit: self.read_enum("window.confirmBeforeClose", |s| match s {
955 "always" | "keyboardOnly" => Some(true),
956 "never" => Some(false),
957 _ => None,
958 }),
959 drop_target_size: None,
960 // workbench.editor.limit contains "enabled", "value", and "perEditorGroup"
961 // our semantics match if those are set to true, some N, and true respectively.
962 // we'll ignore "perEditorGroup" for now since we only support a global max
963 max_tabs: if self.read_bool("workbench.editor.limit.enabled") == Some(true) {
964 self.read_usize("workbench.editor.limit.value")
965 .and_then(|n| NonZeroUsize::new(n))
966 } else {
967 None
968 },
969 on_last_window_closed: None,
970 pane_split_direction_horizontal: None,
971 pane_split_direction_vertical: None,
972 resize_all_panels_in_dock: None,
973 restore_on_file_reopen: self.read_bool("workbench.editor.restoreViewState"),
974 restore_on_startup: None,
975 window_decorations: None,
976 show_call_status_icon: None,
977 use_system_path_prompts: self.read_bool("files.simpleDialog.enable"),
978 use_system_prompts: None,
979 use_system_window_tabs: self.read_bool("window.nativeTabs"),
980 when_closing_with_no_tabs: self.read_bool("window.closeWhenEmpty").map(|b| {
981 if b {
982 CloseWindowWhenNoItems::CloseWindow
983 } else {
984 CloseWindowWhenNoItems::KeepWindowOpen
985 }
986 }),
987 zoomed_padding: None,
988 }
989 }
990
991 fn active_pane_modifiers(&self) -> Option<ActivePaneModifiers> {
992 if self.read_bool("accessibility.dimUnfocused.enabled") == Some(true)
993 && let Some(opacity) = self.read_f32("accessibility.dimUnfocused.opacity")
994 {
995 Some(ActivePaneModifiers {
996 border_size: None,
997 inactive_opacity: Some(InactiveOpacity(opacity)),
998 })
999 } else {
1000 None
1001 }
1002 }
1003
1004 fn worktree_settings_content(&self) -> WorktreeSettingsContent {
1005 WorktreeSettingsContent {
1006 project_name: None,
1007 prevent_sharing_in_public_channels: false,
1008 file_scan_exclusions: self
1009 .read_value("files.watcherExclude")
1010 .and_then(|v| v.as_array())
1011 .map(|v| {
1012 v.iter()
1013 .filter_map(|n| n.as_str().map(str::to_owned))
1014 .collect::<Vec<_>>()
1015 })
1016 .filter(|r| !r.is_empty()),
1017 file_scan_inclusions: self
1018 .read_value("files.watcherInclude")
1019 .and_then(|v| v.as_array())
1020 .map(|v| {
1021 v.iter()
1022 .filter_map(|n| n.as_str().map(str::to_owned))
1023 .collect::<Vec<_>>()
1024 })
1025 .filter(|r| !r.is_empty()),
1026 private_files: None,
1027 hidden_files: None,
1028 read_only_files: self
1029 .read_value("files.readonlyExclude")
1030 .and_then(|v| v.as_object())
1031 .map(|v| {
1032 v.iter()
1033 .filter_map(|(k, v)| {
1034 if v.as_bool().unwrap_or(false) {
1035 Some(k.to_owned())
1036 } else {
1037 None
1038 }
1039 })
1040 .collect::<Vec<_>>()
1041 })
1042 .filter(|r| !r.is_empty()),
1043 }
1044 }
1045}
1046
1047fn skip_default<T: Default + PartialEq>(value: T) -> Option<T> {
1048 if value == T::default() {
1049 None
1050 } else {
1051 Some(value)
1052 }
1053}