mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Closes https://github.com/zed-industries/zed/issues/33587 https://github.com/user-attachments/assets/bbaea8a9-402e-485b-800e-2f4486142956 Release Notes: - Supported LSP document links (enabled by default, use `"lsp_document_links": false` to turn it off)
1116 lines
44 KiB
Rust
1116 lines
44 KiB
Rust
use crate::*;
|
|
use anyhow::{Context as _, Result, anyhow};
|
|
use collections::HashMap;
|
|
use fs::Fs;
|
|
use gpui::Rgba;
|
|
use paths::{cursor_settings_file_paths, vscode_settings_file_paths};
|
|
use serde::Deserialize;
|
|
use serde_json::{Map, Value};
|
|
use std::{
|
|
num::{NonZeroU32, NonZeroUsize},
|
|
path::{Path, PathBuf},
|
|
sync::Arc,
|
|
};
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
pub enum VsCodeSettingsSource {
|
|
VsCode,
|
|
Cursor,
|
|
}
|
|
|
|
impl std::fmt::Display for VsCodeSettingsSource {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
VsCodeSettingsSource::VsCode => write!(f, "VS Code"),
|
|
VsCodeSettingsSource::Cursor => write!(f, "Cursor"),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct VsCodeSettings {
|
|
pub source: VsCodeSettingsSource,
|
|
pub path: Arc<Path>,
|
|
content: Map<String, Value>,
|
|
}
|
|
|
|
impl VsCodeSettings {
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub fn from_str(content: &str, source: VsCodeSettingsSource) -> Result<Self> {
|
|
Ok(Self {
|
|
source,
|
|
path: Path::new("/example-path/Code/User/settings.json").into(),
|
|
content: serde_json_lenient::from_str(content)?,
|
|
})
|
|
}
|
|
|
|
pub async fn load_user_settings(source: VsCodeSettingsSource, fs: Arc<dyn Fs>) -> Result<Self> {
|
|
let candidate_paths = match source {
|
|
VsCodeSettingsSource::VsCode => vscode_settings_file_paths(),
|
|
VsCodeSettingsSource::Cursor => cursor_settings_file_paths(),
|
|
};
|
|
let mut path = None;
|
|
for candidate_path in candidate_paths.iter() {
|
|
if fs.is_file(candidate_path).await {
|
|
path = Some(candidate_path.clone());
|
|
}
|
|
}
|
|
let Some(path) = path else {
|
|
return Err(anyhow!(
|
|
"No settings file found, expected to find it in one of the following paths:\n{}",
|
|
candidate_paths
|
|
.into_iter()
|
|
.map(|path| path.to_string_lossy().into_owned())
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
));
|
|
};
|
|
let content = fs.load(&path).await.with_context(|| {
|
|
format!(
|
|
"Error loading {} settings file from {}",
|
|
source,
|
|
path.display()
|
|
)
|
|
})?;
|
|
let content = serde_json_lenient::from_str(&content).with_context(|| {
|
|
format!(
|
|
"Error parsing {} settings file from {}",
|
|
source,
|
|
path.display()
|
|
)
|
|
})?;
|
|
Ok(Self {
|
|
source,
|
|
path: path.into(),
|
|
content,
|
|
})
|
|
}
|
|
|
|
fn read_value(&self, setting: &str) -> Option<&Value> {
|
|
self.content.get(setting)
|
|
}
|
|
|
|
fn read_str(&self, setting: &str) -> Option<&str> {
|
|
self.read_value(setting).and_then(|v| v.as_str())
|
|
}
|
|
|
|
fn read_string(&self, setting: &str) -> Option<String> {
|
|
self.read_value(setting)
|
|
.and_then(|v| v.as_str())
|
|
.map(|s| s.to_owned())
|
|
}
|
|
|
|
fn read_bool(&self, setting: &str) -> Option<bool> {
|
|
self.read_value(setting).and_then(|v| v.as_bool())
|
|
}
|
|
|
|
fn read_f32(&self, setting: &str) -> Option<f32> {
|
|
self.read_value(setting)
|
|
.and_then(|v| v.as_f64())
|
|
.map(|v| v as f32)
|
|
}
|
|
|
|
fn read_u64(&self, setting: &str) -> Option<u64> {
|
|
self.read_value(setting).and_then(|v| v.as_u64())
|
|
}
|
|
|
|
fn read_usize(&self, setting: &str) -> Option<usize> {
|
|
self.read_value(setting)
|
|
.and_then(|v| v.as_u64())
|
|
.and_then(|v| v.try_into().ok())
|
|
}
|
|
|
|
fn read_u32(&self, setting: &str) -> Option<u32> {
|
|
self.read_value(setting)
|
|
.and_then(|v| v.as_u64())
|
|
.and_then(|v| v.try_into().ok())
|
|
}
|
|
|
|
fn read_enum<T>(&self, key: &str, f: impl FnOnce(&str) -> Option<T>) -> Option<T> {
|
|
self.content.get(key).and_then(Value::as_str).and_then(f)
|
|
}
|
|
|
|
fn read_fonts(&self, key: &str) -> (Option<FontFamilyName>, Option<Vec<FontFamilyName>>) {
|
|
let Some(css_name) = self.content.get(key).and_then(Value::as_str) else {
|
|
return (None, None);
|
|
};
|
|
|
|
let mut name_buffer = String::new();
|
|
let mut quote_char: Option<char> = None;
|
|
let mut fonts = Vec::new();
|
|
let mut add_font = |buffer: &mut String| {
|
|
let trimmed = buffer.trim();
|
|
if !trimmed.is_empty() {
|
|
fonts.push(trimmed.to_string().into());
|
|
}
|
|
|
|
buffer.clear();
|
|
};
|
|
|
|
for ch in css_name.chars() {
|
|
match (ch, quote_char) {
|
|
('"' | '\'', None) => {
|
|
quote_char = Some(ch);
|
|
}
|
|
(_, Some(q)) if ch == q => {
|
|
quote_char = None;
|
|
}
|
|
(',', None) => {
|
|
add_font(&mut name_buffer);
|
|
}
|
|
_ => {
|
|
name_buffer.push(ch);
|
|
}
|
|
}
|
|
}
|
|
|
|
add_font(&mut name_buffer);
|
|
if fonts.is_empty() {
|
|
return (None, None);
|
|
}
|
|
(Some(fonts.remove(0)), skip_default(fonts))
|
|
}
|
|
|
|
pub fn settings_content(&self) -> SettingsContent {
|
|
SettingsContent {
|
|
agent: self.agent_settings_content(),
|
|
agent_servers: None,
|
|
audio: None,
|
|
auto_update: None,
|
|
base_keymap: Some(BaseKeymapContent::VSCode),
|
|
calls: None,
|
|
collaboration_panel: None,
|
|
credentials_url: None,
|
|
debugger: None,
|
|
diagnostics: None,
|
|
editor: self.editor_settings_content(),
|
|
extension: ExtensionSettingsContent::default(),
|
|
file_finder: None,
|
|
git: self.git_settings_content(),
|
|
git_panel: self.git_panel_settings_content(),
|
|
global_lsp_settings: skip_default(GlobalLspSettingsContent {
|
|
semantic_token_rules: self.semantic_token_rules(),
|
|
..GlobalLspSettingsContent::default()
|
|
}),
|
|
helix_mode: None,
|
|
hide_mouse: None,
|
|
image_viewer: None,
|
|
journal: None,
|
|
language_models: None,
|
|
line_indicator_format: None,
|
|
log: None,
|
|
message_editor: None,
|
|
node: self.node_binary_settings(),
|
|
|
|
outline_panel: self.outline_panel_settings_content(),
|
|
preview_tabs: self.preview_tabs_settings_content(),
|
|
project: self.project_settings_content(),
|
|
project_panel: self.project_panel_settings_content(),
|
|
proxy: self.read_string("http.proxy"),
|
|
remote: RemoteSettingsContent::default(),
|
|
repl: None,
|
|
server_url: None,
|
|
session: None,
|
|
status_bar: self.status_bar_settings_content(),
|
|
tab_bar: self.tab_bar_settings_content(),
|
|
tabs: self.item_settings_content(),
|
|
telemetry: self.telemetry_settings_content(),
|
|
terminal: self.terminal_settings_content(),
|
|
theme: Box::new(self.theme_settings_content()),
|
|
title_bar: None,
|
|
vim: None,
|
|
vim_mode: None,
|
|
workspace: self.workspace_settings_content(),
|
|
which_key: None,
|
|
modeline_lines: None,
|
|
feature_flags: None,
|
|
instrumentation: None,
|
|
}
|
|
}
|
|
|
|
fn agent_settings_content(&self) -> Option<AgentSettingsContent> {
|
|
let enabled = self.read_bool("chat.agent.enabled");
|
|
skip_default(AgentSettingsContent {
|
|
enabled: enabled,
|
|
button: enabled,
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
fn editor_settings_content(&self) -> EditorSettingsContent {
|
|
EditorSettingsContent {
|
|
auto_signature_help: self.read_bool("editor.parameterHints.enabled"),
|
|
autoscroll_on_clicks: None,
|
|
cursor_blink: self.read_enum("editor.cursorBlinking", |s| match s {
|
|
"blink" | "phase" | "expand" | "smooth" => Some(true),
|
|
"solid" => Some(false),
|
|
_ => None,
|
|
}),
|
|
cursor_shape: self.read_enum("editor.cursorStyle", |s| match s {
|
|
"block" => Some(CursorShape::Block),
|
|
"block-outline" => Some(CursorShape::Hollow),
|
|
"line" | "line-thin" => Some(CursorShape::Bar),
|
|
"underline" | "underline-thin" => Some(CursorShape::Underline),
|
|
_ => None,
|
|
}),
|
|
current_line_highlight: self.read_enum("editor.renderLineHighlight", |s| match s {
|
|
"gutter" => Some(CurrentLineHighlight::Gutter),
|
|
"line" => Some(CurrentLineHighlight::Line),
|
|
"all" => Some(CurrentLineHighlight::All),
|
|
_ => None,
|
|
}),
|
|
diagnostics_max_severity: None,
|
|
double_click_in_multibuffer: None,
|
|
drag_and_drop_selection: None,
|
|
excerpt_context_lines: None,
|
|
expand_excerpt_lines: None,
|
|
fast_scroll_sensitivity: self.read_f32("editor.fastScrollSensitivity"),
|
|
sticky_scroll: self.sticky_scroll_content(),
|
|
go_to_definition_fallback: None,
|
|
go_to_definition_scroll_strategy: None,
|
|
gutter: self.gutter_content(),
|
|
horizontal_scroll_margin: None,
|
|
hover_popover_delay: self.read_u64("editor.hover.delay").map(Into::into),
|
|
hover_popover_enabled: self.read_bool("editor.hover.enabled"),
|
|
hover_popover_sticky: self.read_bool("editor.hover.sticky"),
|
|
hover_popover_hiding_delay: self.read_u64("editor.hover.hidingDelay").map(Into::into),
|
|
inline_code_actions: None,
|
|
code_lens: None,
|
|
jupyter: None,
|
|
lsp_document_colors: None,
|
|
lsp_document_links: self.read_bool("editor.links"),
|
|
lsp_highlight_debounce: None,
|
|
middle_click_paste: None,
|
|
minimap: self.minimap_content(),
|
|
minimum_contrast_for_highlights: None,
|
|
multi_cursor_modifier: self.read_enum("editor.multiCursorModifier", |s| match s {
|
|
"ctrlCmd" => Some(MultiCursorModifier::CmdOrCtrl),
|
|
"alt" => Some(MultiCursorModifier::Alt),
|
|
_ => None,
|
|
}),
|
|
redact_private_values: None,
|
|
relative_line_numbers: self.read_enum("editor.lineNumbers", |s| match s {
|
|
"relative" => Some(RelativeLineNumbers::Enabled),
|
|
_ => None,
|
|
}),
|
|
rounded_selection: self.read_bool("editor.roundedSelection"),
|
|
scroll_beyond_last_line: None,
|
|
mouse_wheel_zoom: self.read_bool("editor.mouseWheelZoom"),
|
|
scroll_sensitivity: self.read_f32("editor.mouseWheelScrollSensitivity"),
|
|
scrollbar: self.scrollbar_content(),
|
|
search: self.search_content(),
|
|
search_wrap: None,
|
|
seed_search_query_from_cursor: self.read_enum(
|
|
"editor.find.seedSearchStringFromSelection",
|
|
|s| match s {
|
|
"always" => Some(SeedQuerySetting::Always),
|
|
"selection" => Some(SeedQuerySetting::Selection),
|
|
"never" => Some(SeedQuerySetting::Never),
|
|
_ => None,
|
|
},
|
|
),
|
|
selection_highlight: self.read_bool("editor.selectionHighlight"),
|
|
show_signature_help_after_edits: self.read_bool("editor.parameterHints.enabled"),
|
|
snippet_sort_order: None,
|
|
toolbar: None,
|
|
use_smartcase_search: self.read_bool("search.smartCase"),
|
|
vertical_scroll_margin: self.read_f32("editor.cursorSurroundingLines"),
|
|
completion_menu_scrollbar: None,
|
|
completion_detail_alignment: None,
|
|
completion_menu_item_kind: None,
|
|
diff_view_style: None,
|
|
minimum_split_diff_width: None,
|
|
}
|
|
}
|
|
|
|
fn sticky_scroll_content(&self) -> Option<StickyScrollContent> {
|
|
skip_default(StickyScrollContent {
|
|
enabled: self.read_bool("editor.stickyScroll.enabled"),
|
|
})
|
|
}
|
|
|
|
fn gutter_content(&self) -> Option<GutterContent> {
|
|
skip_default(GutterContent {
|
|
line_numbers: self.read_enum("editor.lineNumbers", |s| match s {
|
|
"on" | "relative" => Some(true),
|
|
"off" => Some(false),
|
|
_ => None,
|
|
}),
|
|
min_line_number_digits: None,
|
|
runnables: None,
|
|
breakpoints: None,
|
|
bookmarks: None,
|
|
folds: self.read_enum("editor.showFoldingControls", |s| match s {
|
|
"always" | "mouseover" => Some(true),
|
|
"never" => Some(false),
|
|
_ => None,
|
|
}),
|
|
})
|
|
}
|
|
|
|
fn scrollbar_content(&self) -> Option<ScrollbarContent> {
|
|
let scrollbar_axes = skip_default(ScrollbarAxesContent {
|
|
horizontal: self.read_enum("editor.scrollbar.horizontal", |s| match s {
|
|
"auto" | "visible" => Some(true),
|
|
"hidden" => Some(false),
|
|
_ => None,
|
|
}),
|
|
vertical: self.read_enum("editor.scrollbar.vertical", |s| match s {
|
|
"auto" | "visible" => Some(true),
|
|
"hidden" => Some(false),
|
|
_ => None,
|
|
}),
|
|
})?;
|
|
|
|
Some(ScrollbarContent {
|
|
axes: Some(scrollbar_axes),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
fn search_content(&self) -> Option<SearchSettingsContent> {
|
|
skip_default(SearchSettingsContent {
|
|
include_ignored: self.read_bool("search.useIgnoreFiles"),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
fn semantic_token_rules(&self) -> Option<SemanticTokenRules> {
|
|
let customizations = self
|
|
.read_value("editor.semanticTokenColorCustomizations")?
|
|
.as_object()?;
|
|
|
|
skip_default(SemanticTokenRules {
|
|
rules: customizations
|
|
.get("rules")
|
|
.and_then(|v| {
|
|
Some(
|
|
v.as_object()?
|
|
.iter()
|
|
.filter_map(|(k, v)| {
|
|
let v = v.as_object()?;
|
|
|
|
let mut underline = v
|
|
.get("underline")
|
|
.and_then(|b| b.as_bool())
|
|
.unwrap_or(false);
|
|
let strikethrough = v
|
|
.get("strikethrough")
|
|
.and_then(|b| b.as_bool())
|
|
.unwrap_or(false);
|
|
let mut font_weight =
|
|
v.get("bold").and_then(|b| b.as_bool()).map(|b| {
|
|
if b {
|
|
SemanticTokenFontWeight::Bold
|
|
} else {
|
|
SemanticTokenFontWeight::Normal
|
|
}
|
|
});
|
|
let mut font_style =
|
|
v.get("italic").and_then(|b| b.as_bool()).map(|b| {
|
|
if b {
|
|
SemanticTokenFontStyle::Italic
|
|
} else {
|
|
SemanticTokenFontStyle::Normal
|
|
}
|
|
});
|
|
|
|
match v.get("fontStyle").and_then(|s| s.as_str()).unwrap_or("") {
|
|
"bold" => {
|
|
font_style = Some(SemanticTokenFontStyle::Normal);
|
|
font_weight = Some(SemanticTokenFontWeight::Bold);
|
|
}
|
|
"italic" => {
|
|
font_style = Some(SemanticTokenFontStyle::Italic);
|
|
font_weight = Some(SemanticTokenFontWeight::Normal);
|
|
}
|
|
"underline" => {
|
|
underline = true;
|
|
}
|
|
"bold italic" | "italic bold" => {
|
|
font_style = Some(SemanticTokenFontStyle::Italic);
|
|
font_weight = Some(SemanticTokenFontWeight::Bold);
|
|
}
|
|
"normal" => {
|
|
font_style = Some(SemanticTokenFontStyle::Normal);
|
|
font_weight = Some(SemanticTokenFontWeight::Normal);
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
let foreground = v
|
|
.get("foreground")
|
|
.and_then(|v| Rgba::try_from(v.as_str()?).ok())
|
|
.map(|s| s.to_owned());
|
|
let background = v
|
|
.get("background")
|
|
.and_then(|v| Rgba::try_from(v.as_str()?).ok())
|
|
.map(|s| s.to_owned());
|
|
|
|
Some(SemanticTokenRule {
|
|
token_type: Some(k.clone()),
|
|
token_modifiers: vec![],
|
|
style: vec![],
|
|
underline: if underline {
|
|
Some(SemanticTokenColorOverride::InheritForeground(true))
|
|
} else {
|
|
None
|
|
},
|
|
strikethrough: if strikethrough {
|
|
Some(SemanticTokenColorOverride::InheritForeground(true))
|
|
} else {
|
|
None
|
|
},
|
|
foreground_color: foreground,
|
|
background_color: background,
|
|
font_weight,
|
|
font_style,
|
|
})
|
|
})
|
|
.collect(),
|
|
)
|
|
})
|
|
.unwrap_or_default(),
|
|
})
|
|
}
|
|
|
|
fn minimap_content(&self) -> Option<MinimapContent> {
|
|
let minimap_enabled = self.read_bool("editor.minimap.enabled");
|
|
let autohide = self.read_bool("editor.minimap.autohide");
|
|
let show = match (minimap_enabled, autohide) {
|
|
(Some(true), Some(false)) => Some(ShowMinimap::Always),
|
|
(Some(true), _) => Some(ShowMinimap::Auto),
|
|
(Some(false), _) => Some(ShowMinimap::Never),
|
|
_ => None,
|
|
};
|
|
|
|
skip_default(MinimapContent {
|
|
show,
|
|
thumb: self.read_enum("editor.minimap.showSlider", |s| match s {
|
|
"always" => Some(MinimapThumb::Always),
|
|
"mouseover" => Some(MinimapThumb::Hover),
|
|
_ => None,
|
|
}),
|
|
max_width_columns: self
|
|
.read_u32("editor.minimap.maxColumn")
|
|
.and_then(|v| NonZeroU32::new(v)),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
fn git_panel_settings_content(&self) -> Option<GitPanelSettingsContent> {
|
|
skip_default(GitPanelSettingsContent {
|
|
button: self.read_bool("git.enabled"),
|
|
fallback_branch_name: self.read_string("git.defaultBranchName"),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
fn project_settings_content(&self) -> ProjectSettingsContent {
|
|
ProjectSettingsContent {
|
|
all_languages: AllLanguageSettingsContent {
|
|
edit_predictions: self.edit_predictions_settings_content(),
|
|
defaults: self.default_language_settings_content(),
|
|
languages: Default::default(),
|
|
file_types: self.file_types(),
|
|
},
|
|
worktree: self.worktree_settings_content(),
|
|
lsp: Default::default(),
|
|
terminal: None,
|
|
dap: Default::default(),
|
|
context_servers: self.context_servers(),
|
|
context_server_timeout: None,
|
|
load_direnv: None,
|
|
git_hosting_providers: None,
|
|
disable_ai: None,
|
|
}
|
|
}
|
|
|
|
fn default_language_settings_content(&self) -> LanguageSettingsContent {
|
|
LanguageSettingsContent {
|
|
allow_rewrap: None,
|
|
always_treat_brackets_as_autoclosed: None,
|
|
auto_indent: None,
|
|
auto_indent_on_paste: self.read_bool("editor.formatOnPaste"),
|
|
code_actions_on_format: None,
|
|
completions: skip_default(CompletionSettingsContent {
|
|
words: self.read_bool("editor.suggest.showWords").map(|b| {
|
|
if b {
|
|
WordsCompletionMode::Enabled
|
|
} else {
|
|
WordsCompletionMode::Disabled
|
|
}
|
|
}),
|
|
..Default::default()
|
|
}),
|
|
debuggers: None,
|
|
edit_predictions_disabled_in: None,
|
|
enable_language_server: None,
|
|
ensure_final_newline_on_save: self.read_bool("files.insertFinalNewline"),
|
|
line_ending: self.read_enum("files.eol", |s| match s {
|
|
"\n" => Some(LineEndingSetting::PreferLf),
|
|
"\r\n" => Some(LineEndingSetting::PreferCrlf),
|
|
"auto" => Some(LineEndingSetting::Detect),
|
|
_ => None,
|
|
}),
|
|
extend_comment_on_newline: None,
|
|
extend_list_on_newline: None,
|
|
indent_list_on_tab: None,
|
|
format_on_save: self.read_bool("editor.guides.formatOnSave").map(|b| {
|
|
if b {
|
|
FormatOnSave::On
|
|
} else {
|
|
FormatOnSave::Off
|
|
}
|
|
}),
|
|
formatter: None,
|
|
hard_tabs: self.read_bool("editor.insertSpaces").map(|v| !v),
|
|
indent_guides: skip_default(IndentGuideSettingsContent {
|
|
enabled: self.read_bool("editor.guides.indentation"),
|
|
..Default::default()
|
|
}),
|
|
inlay_hints: None,
|
|
jsx_tag_auto_close: None,
|
|
language_servers: None,
|
|
semantic_tokens: self
|
|
.read_bool("editor.semanticHighlighting.enabled")
|
|
.map(|enabled| {
|
|
if enabled {
|
|
SemanticTokens::Full
|
|
} else {
|
|
SemanticTokens::Off
|
|
}
|
|
}),
|
|
document_folding_ranges: None,
|
|
document_symbols: None,
|
|
linked_edits: self.read_bool("editor.linkedEditing"),
|
|
preferred_line_length: self.read_u32("editor.wordWrapColumn"),
|
|
prettier: None,
|
|
remove_trailing_whitespace_on_save: self.read_bool("editor.trimAutoWhitespace"),
|
|
show_completion_documentation: None,
|
|
colorize_brackets: self.read_bool("editor.bracketPairColorization.enabled"),
|
|
show_completions_on_input: self.read_bool("editor.suggestOnTriggerCharacters"),
|
|
show_edit_predictions: self.read_bool("editor.inlineSuggest.enabled"),
|
|
show_whitespaces: self.read_enum("editor.renderWhitespace", |s| {
|
|
Some(match s {
|
|
"boundary" => ShowWhitespaceSetting::Boundary,
|
|
"trailing" => ShowWhitespaceSetting::Trailing,
|
|
"selection" => ShowWhitespaceSetting::Selection,
|
|
"all" => ShowWhitespaceSetting::All,
|
|
_ => ShowWhitespaceSetting::None,
|
|
})
|
|
}),
|
|
show_wrap_guides: None,
|
|
soft_wrap: self.read_enum("editor.wordWrap", |s| match s {
|
|
"on" => Some(SoftWrap::EditorWidth),
|
|
"wordWrapColumn" => Some(SoftWrap::PreferLine),
|
|
"bounded" => Some(SoftWrap::Bounded),
|
|
"off" => Some(SoftWrap::None),
|
|
_ => None,
|
|
}),
|
|
tab_size: self
|
|
.read_u32("editor.tabSize")
|
|
.and_then(|n| NonZeroU32::new(n)),
|
|
tasks: None,
|
|
use_auto_surround: self.read_enum("editor.autoSurround", |s| match s {
|
|
"languageDefined" | "quotes" | "brackets" => Some(true),
|
|
"never" => Some(false),
|
|
_ => None,
|
|
}),
|
|
use_autoclose: None,
|
|
use_on_type_format: self.read_bool("editor.formatOnType"),
|
|
whitespace_map: None,
|
|
wrap_guides: self
|
|
.read_value("editor.rulers")
|
|
.and_then(|v| v.as_array())
|
|
.map(|v| {
|
|
v.iter()
|
|
.flat_map(|n| n.as_u64().map(|n| n as usize))
|
|
.collect()
|
|
}),
|
|
word_diff_enabled: None,
|
|
}
|
|
}
|
|
|
|
fn file_types(&self) -> Option<HashMap<Arc<str>, ExtendingVec<String>>> {
|
|
// vscodes file association map is inverted from ours, so we flip the mapping before merging
|
|
let mut associations: HashMap<Arc<str>, ExtendingVec<String>> = HashMap::default();
|
|
let map = self.read_value("files.associations")?.as_object()?;
|
|
for (k, v) in map {
|
|
let Some(v) = v.as_str() else { continue };
|
|
associations.entry(v.into()).or_default().0.push(k.clone());
|
|
}
|
|
skip_default(associations)
|
|
}
|
|
|
|
fn edit_predictions_settings_content(&self) -> Option<EditPredictionSettingsContent> {
|
|
let disabled_globs = self
|
|
.read_value("cursor.general.globalCursorIgnoreList")?
|
|
.as_array()?;
|
|
|
|
skip_default(EditPredictionSettingsContent {
|
|
disabled_globs: skip_default(
|
|
disabled_globs
|
|
.iter()
|
|
.filter_map(|glob| glob.as_str())
|
|
.map(|s| s.to_string())
|
|
.collect(),
|
|
),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
fn outline_panel_settings_content(&self) -> Option<OutlinePanelSettingsContent> {
|
|
skip_default(OutlinePanelSettingsContent {
|
|
file_icons: self.read_bool("outline.icons"),
|
|
folder_icons: self.read_bool("outline.icons"),
|
|
git_status: self.read_bool("git.decorations.enabled"),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
fn node_binary_settings(&self) -> Option<NodeBinarySettings> {
|
|
// this just sets the binary name instead of a full path so it relies on path lookup
|
|
// resolving to the one you want
|
|
skip_default(NodeBinarySettings {
|
|
npm_path: self.read_enum("npm.packageManager", |s| match s {
|
|
v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
|
|
_ => None,
|
|
}),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
fn git_settings_content(&self) -> Option<GitSettings> {
|
|
let inline_blame = self.read_bool("git.blame.editorDecoration.enabled")?;
|
|
skip_default(GitSettings {
|
|
inline_blame: Some(InlineBlameSettings {
|
|
enabled: Some(inline_blame),
|
|
..Default::default()
|
|
}),
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
fn context_servers(&self) -> HashMap<Arc<str>, ContextServerSettingsContent> {
|
|
#[derive(Deserialize)]
|
|
struct VsCodeContextServerCommand {
|
|
command: PathBuf,
|
|
args: Option<Vec<String>>,
|
|
env: Option<HashMap<String, String>>,
|
|
// note: we don't support envFile and type
|
|
}
|
|
let Some(mcp) = self.read_value("mcp").and_then(|v| v.as_object()) else {
|
|
return Default::default();
|
|
};
|
|
mcp.iter()
|
|
.filter_map(|(k, v)| {
|
|
Some((
|
|
k.clone().into(),
|
|
ContextServerSettingsContent::Stdio {
|
|
enabled: true,
|
|
remote: false,
|
|
command: serde_json::from_value::<VsCodeContextServerCommand>(v.clone())
|
|
.ok()
|
|
.map(|cmd| ContextServerCommand {
|
|
path: cmd.command,
|
|
args: cmd.args.unwrap_or_default(),
|
|
env: cmd.env,
|
|
timeout: None,
|
|
})?,
|
|
},
|
|
))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn item_settings_content(&self) -> Option<ItemSettingsContent> {
|
|
skip_default(ItemSettingsContent {
|
|
git_status: self.read_bool("git.decorations.enabled"),
|
|
close_position: self.read_enum("workbench.editor.tabActionLocation", |s| match s {
|
|
"right" => Some(ClosePosition::Right),
|
|
"left" => Some(ClosePosition::Left),
|
|
_ => None,
|
|
}),
|
|
file_icons: self.read_bool("workbench.editor.showIcons"),
|
|
activate_on_close: self
|
|
.read_bool("workbench.editor.focusRecentEditorAfterClose")
|
|
.map(|b| {
|
|
if b {
|
|
ActivateOnClose::History
|
|
} else {
|
|
ActivateOnClose::LeftNeighbour
|
|
}
|
|
}),
|
|
show_diagnostics: None,
|
|
show_close_button: self
|
|
.read_bool("workbench.editor.tabActionCloseVisibility")
|
|
.map(|b| {
|
|
if b {
|
|
ShowCloseButton::Always
|
|
} else {
|
|
ShowCloseButton::Hidden
|
|
}
|
|
}),
|
|
})
|
|
}
|
|
|
|
fn preview_tabs_settings_content(&self) -> Option<PreviewTabsSettingsContent> {
|
|
skip_default(PreviewTabsSettingsContent {
|
|
enabled: self.read_bool("workbench.editor.enablePreview"),
|
|
enable_preview_from_project_panel: None,
|
|
enable_preview_from_file_finder: self
|
|
.read_bool("workbench.editor.enablePreviewFromQuickOpen"),
|
|
enable_preview_from_multibuffer: None,
|
|
enable_preview_multibuffer_from_code_navigation: None,
|
|
enable_preview_file_from_code_navigation: None,
|
|
enable_keep_preview_on_code_navigation: self
|
|
.read_bool("workbench.editor.enablePreviewFromCodeNavigation"),
|
|
})
|
|
}
|
|
|
|
fn tab_bar_settings_content(&self) -> Option<TabBarSettingsContent> {
|
|
skip_default(TabBarSettingsContent {
|
|
show: self.read_enum("workbench.editor.showTabs", |s| match s {
|
|
"multiple" => Some(true),
|
|
"single" | "none" => Some(false),
|
|
_ => None,
|
|
}),
|
|
show_nav_history_buttons: None,
|
|
show_tab_bar_buttons: self
|
|
.read_str("workbench.editor.editorActionsLocation")
|
|
.and_then(|str| if str == "hidden" { Some(false) } else { None }),
|
|
show_pinned_tabs_in_separate_row: None,
|
|
})
|
|
}
|
|
|
|
fn status_bar_settings_content(&self) -> Option<StatusBarSettingsContent> {
|
|
skip_default(StatusBarSettingsContent {
|
|
show: self.read_bool("workbench.statusBar.visible"),
|
|
show_active_file: None,
|
|
active_language_button: None,
|
|
cursor_position_button: None,
|
|
line_endings_button: None,
|
|
active_encoding_button: None,
|
|
})
|
|
}
|
|
|
|
fn project_panel_settings_content(&self) -> Option<ProjectPanelSettingsContent> {
|
|
let mut project_panel_settings = ProjectPanelSettingsContent {
|
|
auto_fold_dirs: self.read_bool("explorer.compactFolders"),
|
|
auto_reveal_entries: self.read_bool("explorer.autoReveal"),
|
|
bold_folder_labels: None,
|
|
button: None,
|
|
default_width: None,
|
|
dock: None,
|
|
drag_and_drop: None,
|
|
entry_spacing: None,
|
|
file_icons: None,
|
|
folder_icons: None,
|
|
git_status: self.read_bool("git.decorations.enabled"),
|
|
hide_gitignore: self.read_bool("explorer.excludeGitIgnore"),
|
|
hide_hidden: None,
|
|
hide_root: None,
|
|
indent_guides: None,
|
|
indent_size: None,
|
|
scrollbar: self.read_bool("workbench.list.horizontalScrolling").map(
|
|
|horizontal_scrolling| ProjectPanelScrollbarSettingsContent {
|
|
show: None,
|
|
horizontal_scroll: Some(horizontal_scrolling),
|
|
},
|
|
),
|
|
show_diagnostics: self
|
|
.read_bool("problems.decorations.enabled")
|
|
.and_then(|b| if b { Some(ShowDiagnostics::Off) } else { None }),
|
|
sort_mode: self.read_enum("explorer.sortOrder", |s| match s {
|
|
"default" | "foldersNestsFiles" => Some(ProjectPanelSortMode::DirectoriesFirst),
|
|
"mixed" => Some(ProjectPanelSortMode::Mixed),
|
|
"filesFirst" => Some(ProjectPanelSortMode::FilesFirst),
|
|
_ => None,
|
|
}),
|
|
sort_order: self.read_enum("explorer.sortOrderLexicographicOptions", |s| match s {
|
|
"default" => Some(ProjectPanelSortOrder::Default),
|
|
"upper" => Some(ProjectPanelSortOrder::Upper),
|
|
"lower" => Some(ProjectPanelSortOrder::Lower),
|
|
"unicode" => Some(ProjectPanelSortOrder::Unicode),
|
|
_ => None,
|
|
}),
|
|
starts_open: None,
|
|
sticky_scroll: None,
|
|
auto_open: None,
|
|
diagnostic_badges: None,
|
|
git_status_indicator: None,
|
|
};
|
|
|
|
if let (Some(false), Some(false)) = (
|
|
self.read_bool("explorer.decorations.badges"),
|
|
self.read_bool("explorer.decorations.colors"),
|
|
) {
|
|
project_panel_settings.git_status = Some(false);
|
|
project_panel_settings.show_diagnostics = Some(ShowDiagnostics::Off);
|
|
}
|
|
|
|
skip_default(project_panel_settings)
|
|
}
|
|
|
|
fn telemetry_settings_content(&self) -> Option<TelemetrySettingsContent> {
|
|
self.read_enum("telemetry.telemetryLevel", |level| {
|
|
let (metrics, diagnostics) = match level {
|
|
"all" => (true, true),
|
|
"error" | "crash" => (false, true),
|
|
"off" => (false, false),
|
|
_ => return None,
|
|
};
|
|
Some(TelemetrySettingsContent {
|
|
metrics: Some(metrics),
|
|
diagnostics: Some(diagnostics),
|
|
})
|
|
})
|
|
}
|
|
|
|
fn terminal_settings_content(&self) -> Option<TerminalSettingsContent> {
|
|
let (font_family, font_fallbacks) = self.read_fonts("terminal.integrated.fontFamily");
|
|
skip_default(TerminalSettingsContent {
|
|
alternate_scroll: None,
|
|
blinking: self
|
|
.read_bool("terminal.integrated.cursorBlinking")
|
|
.map(|b| {
|
|
if b {
|
|
TerminalBlink::On
|
|
} else {
|
|
TerminalBlink::Off
|
|
}
|
|
}),
|
|
button: None,
|
|
copy_on_select: self.read_bool("terminal.integrated.copyOnSelection"),
|
|
cursor_shape: self.read_enum("terminal.integrated.cursorStyle", |s| match s {
|
|
"block" => Some(CursorShapeContent::Block),
|
|
"line" => Some(CursorShapeContent::Bar),
|
|
"underline" => Some(CursorShapeContent::Underline),
|
|
_ => None,
|
|
}),
|
|
default_height: None,
|
|
default_width: None,
|
|
dock: None,
|
|
font_fallbacks,
|
|
font_family,
|
|
font_features: None,
|
|
font_size: self
|
|
.read_f32("terminal.integrated.fontSize")
|
|
.map(FontSize::from),
|
|
font_weight: None,
|
|
keep_selection_on_copy: None,
|
|
line_height: self
|
|
.read_f32("terminal.integrated.lineHeight")
|
|
.map(|lh| TerminalLineHeight::Custom(lh)),
|
|
max_scroll_history_lines: self.read_usize("terminal.integrated.scrollback"),
|
|
bell: self
|
|
.read_value("accessibility.signals.terminalBell")
|
|
.and_then(|v| Some(v.get("sound")?.as_str()? == "on"))
|
|
.or_else(|| {
|
|
// Older deprecated setting, might as well still support it:
|
|
self.read_value("terminal.integrated.enableBell")
|
|
.map(|v| v.as_bool() == Some(true) || v.as_str() == Some("both"))
|
|
})
|
|
.map(|enabled| {
|
|
if enabled {
|
|
TerminalBell::System
|
|
} else {
|
|
TerminalBell::Off
|
|
}
|
|
}),
|
|
minimum_contrast: None,
|
|
option_as_meta: self.read_bool("terminal.integrated.macOptionIsMeta"),
|
|
project: self.project_terminal_settings_content(),
|
|
scrollbar: None,
|
|
scroll_multiplier: None,
|
|
toolbar: None,
|
|
show_count_badge: None,
|
|
flexible: None,
|
|
})
|
|
}
|
|
|
|
fn project_terminal_settings_content(&self) -> ProjectTerminalSettingsContent {
|
|
#[cfg(target_os = "windows")]
|
|
let platform = "windows";
|
|
#[cfg(target_os = "linux")]
|
|
let platform = "linux";
|
|
#[cfg(target_os = "macos")]
|
|
let platform = "osx";
|
|
#[cfg(target_os = "freebsd")]
|
|
let platform = "freebsd";
|
|
let env = self
|
|
.read_value(&format!("terminal.integrated.env.{platform}"))
|
|
.and_then(|v| v.as_object())
|
|
.map(|v| {
|
|
v.iter()
|
|
.map(|(k, v)| (k.clone(), v.to_string()))
|
|
// zed does not support substitutions, so this can break env vars
|
|
.filter(|(_, v)| !v.contains('$'))
|
|
.collect()
|
|
});
|
|
|
|
ProjectTerminalSettingsContent {
|
|
// TODO: handle arguments
|
|
shell: self
|
|
.read_string(&format!("terminal.integrated.{platform}Exec"))
|
|
.map(|s| Shell::Program(s)),
|
|
working_directory: None,
|
|
env,
|
|
detect_venv: None,
|
|
path_hyperlink_regexes: None,
|
|
path_hyperlink_timeout_ms: None,
|
|
}
|
|
}
|
|
|
|
fn theme_settings_content(&self) -> ThemeSettingsContent {
|
|
let (buffer_font_family, buffer_font_fallbacks) = self.read_fonts("editor.fontFamily");
|
|
ThemeSettingsContent {
|
|
ui_font_size: None,
|
|
ui_font_family: None,
|
|
ui_font_fallbacks: None,
|
|
ui_font_features: None,
|
|
ui_font_weight: None,
|
|
buffer_font_family,
|
|
buffer_font_fallbacks,
|
|
buffer_font_size: self.read_f32("editor.fontSize").map(FontSize::from),
|
|
buffer_font_weight: self.read_f32("editor.fontWeight").map(FontWeightContent),
|
|
buffer_line_height: None,
|
|
buffer_font_features: None,
|
|
agent_ui_font_size: None,
|
|
agent_buffer_font_size: None,
|
|
git_commit_buffer_font_size: None,
|
|
markdown_preview_font_family: None,
|
|
markdown_preview_code_font_family: None,
|
|
markdown_preview_theme: None,
|
|
theme: None,
|
|
icon_theme: None,
|
|
ui_density: None,
|
|
unnecessary_code_fade: None,
|
|
experimental_theme_overrides: None,
|
|
theme_overrides: Default::default(),
|
|
}
|
|
}
|
|
|
|
fn workspace_settings_content(&self) -> WorkspaceSettingsContent {
|
|
WorkspaceSettingsContent {
|
|
active_pane_modifiers: self.active_pane_modifiers(),
|
|
text_rendering_mode: None,
|
|
autosave: self.read_enum("files.autoSave", |s| match s {
|
|
"off" => Some(AutosaveSetting::Off),
|
|
"afterDelay" => Some(AutosaveSetting::AfterDelay {
|
|
milliseconds: self
|
|
.read_value("files.autoSaveDelay")
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(1000)
|
|
.into(),
|
|
}),
|
|
"onFocusChange" => Some(AutosaveSetting::OnFocusChange),
|
|
"onWindowChange" => Some(AutosaveSetting::OnWindowChange),
|
|
_ => None,
|
|
}),
|
|
bottom_dock_layout: None,
|
|
centered_layout: None,
|
|
cli_default_open_behavior: None,
|
|
close_on_file_delete: None,
|
|
close_panel_on_toggle: None,
|
|
command_aliases: Default::default(),
|
|
confirm_quit: self.read_enum("window.confirmBeforeClose", |s| match s {
|
|
"always" | "keyboardOnly" => Some(true),
|
|
"never" => Some(false),
|
|
_ => None,
|
|
}),
|
|
drop_target_size: None,
|
|
// workbench.editor.limit contains "enabled", "value", and "perEditorGroup"
|
|
// our semantics match if those are set to true, some N, and true respectively.
|
|
// we'll ignore "perEditorGroup" for now since we only support a global max
|
|
max_tabs: if self.read_bool("workbench.editor.limit.enabled") == Some(true) {
|
|
self.read_usize("workbench.editor.limit.value")
|
|
.and_then(|n| NonZeroUsize::new(n))
|
|
} else {
|
|
None
|
|
},
|
|
on_last_window_closed: None,
|
|
pane_split_direction_horizontal: None,
|
|
pane_split_direction_vertical: None,
|
|
resize_all_panels_in_dock: None,
|
|
restore_on_file_reopen: self.read_bool("workbench.editor.restoreViewState"),
|
|
restore_on_startup: None,
|
|
window_decorations: None,
|
|
show_call_status_icon: None,
|
|
use_system_path_prompts: self.read_bool("files.simpleDialog.enable").map(|b| !b),
|
|
use_system_prompts: None,
|
|
use_system_window_tabs: self.read_bool("window.nativeTabs"),
|
|
when_closing_with_no_tabs: self.read_bool("window.closeWhenEmpty").map(|b| {
|
|
if b {
|
|
CloseWindowWhenNoItems::CloseWindow
|
|
} else {
|
|
CloseWindowWhenNoItems::KeepWindowOpen
|
|
}
|
|
}),
|
|
zoomed_padding: None,
|
|
focus_follows_mouse: None,
|
|
}
|
|
}
|
|
|
|
fn active_pane_modifiers(&self) -> Option<ActivePaneModifiers> {
|
|
if self.read_bool("accessibility.dimUnfocused.enabled") == Some(true)
|
|
&& let Some(opacity) = self.read_f32("accessibility.dimUnfocused.opacity")
|
|
{
|
|
Some(ActivePaneModifiers {
|
|
border_size: None,
|
|
inactive_opacity: Some(InactiveOpacity(opacity)),
|
|
})
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn worktree_settings_content(&self) -> WorktreeSettingsContent {
|
|
WorktreeSettingsContent {
|
|
prevent_sharing_in_public_channels: false,
|
|
file_scan_exclusions: self
|
|
.read_value("files.watcherExclude")
|
|
.and_then(|v| v.as_array())
|
|
.map(|v| {
|
|
v.iter()
|
|
.filter_map(|n| n.as_str().map(str::to_owned))
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.filter(|r| !r.is_empty()),
|
|
file_scan_inclusions: self
|
|
.read_value("files.watcherInclude")
|
|
.and_then(|v| v.as_array())
|
|
.map(|v| {
|
|
v.iter()
|
|
.filter_map(|n| n.as_str().map(str::to_owned))
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.filter(|r| !r.is_empty()),
|
|
private_files: None,
|
|
hidden_files: None,
|
|
read_only_files: self
|
|
.read_value("files.readonlyExclude")
|
|
.and_then(|v| v.as_object())
|
|
.map(|v| {
|
|
v.iter()
|
|
.filter_map(|(k, v)| {
|
|
if v.as_bool().unwrap_or(false) {
|
|
Some(k.to_owned())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.filter(|r| !r.is_empty()),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn skip_default<T: Default + PartialEq>(value: T) -> Option<T> {
|
|
if value == T::default() {
|
|
None
|
|
} else {
|
|
Some(value)
|
|
}
|
|
}
|