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