settings_ui: Language settings UI (#39640)
Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ...
This commit is contained in:
@@ -1876,21 +1876,19 @@
|
||||
// Allows to enable/disable formatting with Prettier
|
||||
// and configure default Prettier, used when no project-level Prettier installation is found.
|
||||
"prettier": {
|
||||
// // Whether to consider prettier formatter or not when attempting to format a file.
|
||||
"allowed": false
|
||||
//
|
||||
// // Use regular Prettier json configuration.
|
||||
// // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if
|
||||
// // the project has no other Prettier installed.
|
||||
// "plugins": [],
|
||||
//
|
||||
// // Use regular Prettier json configuration.
|
||||
// // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if
|
||||
// // the project has no other Prettier installed.
|
||||
// Enables or disables formatting with Prettier for any given language.
|
||||
"allowed": false,
|
||||
// Forces Prettier integration to use a specific parser name when formatting files with the language.
|
||||
"plugins": [],
|
||||
// Default Prettier options, in the format as in package.json section for Prettier.
|
||||
// If project installs Prettier via its package.json, these options will be ignored.
|
||||
// "trailingComma": "es5",
|
||||
// "tabWidth": 4,
|
||||
// "semi": false,
|
||||
// "singleQuote": true
|
||||
// Forces Prettier integration to use a specific parser name when formatting files with the language
|
||||
// when set to a non-empty string.
|
||||
"parser": ""
|
||||
},
|
||||
// Settings for auto-closing of JSX tags.
|
||||
"jsx_tag_auto_close": {
|
||||
|
||||
@@ -533,9 +533,9 @@ impl settings::Settings for AllLanguageSettings {
|
||||
formatter: settings.formatter.unwrap(),
|
||||
prettier: PrettierSettings {
|
||||
allowed: prettier.allowed.unwrap(),
|
||||
parser: prettier.parser,
|
||||
plugins: prettier.plugins,
|
||||
options: prettier.options,
|
||||
parser: prettier.parser.filter(|parser| !parser.is_empty()),
|
||||
plugins: prettier.plugins.unwrap_or_default(),
|
||||
options: prettier.options.unwrap_or_default(),
|
||||
},
|
||||
jsx_tag_auto_close: settings.jsx_tag_auto_close.unwrap().enabled.unwrap(),
|
||||
enable_language_server: settings.enable_language_server.unwrap(),
|
||||
@@ -571,7 +571,7 @@ impl settings::Settings for AllLanguageSettings {
|
||||
code_actions_on_format: settings.code_actions_on_format.unwrap(),
|
||||
linked_edits: settings.linked_edits.unwrap(),
|
||||
tasks: LanguageTaskSettings {
|
||||
variables: tasks.variables,
|
||||
variables: tasks.variables.unwrap_or_default(),
|
||||
enabled: tasks.enabled.unwrap(),
|
||||
prefer_lsp: tasks.prefer_lsp.unwrap(),
|
||||
},
|
||||
@@ -579,7 +579,7 @@ impl settings::Settings for AllLanguageSettings {
|
||||
show_completion_documentation: settings.show_completion_documentation.unwrap(),
|
||||
completions: CompletionSettings {
|
||||
words: completions.words.unwrap(),
|
||||
words_min_length: completions.words_min_length.unwrap(),
|
||||
words_min_length: completions.words_min_length.unwrap() as usize,
|
||||
lsp: completions.lsp.unwrap(),
|
||||
lsp_fetch_timeout_ms: completions.lsp_fetch_timeout_ms.unwrap(),
|
||||
lsp_insert_mode: completions.lsp_insert_mode.unwrap(),
|
||||
|
||||
@@ -385,7 +385,19 @@ pub struct WhitespaceMapContent {
|
||||
}
|
||||
|
||||
/// The behavior of `editor::Rewrap`.
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
||||
#[derive(
|
||||
Debug,
|
||||
PartialEq,
|
||||
Clone,
|
||||
Copy,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
MergeFrom,
|
||||
strum::VariantArray,
|
||||
strum::VariantNames,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RewrapBehavior {
|
||||
/// Only rewrap within comments.
|
||||
@@ -500,7 +512,7 @@ pub struct CompletionSettingsContent {
|
||||
/// Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command.
|
||||
///
|
||||
/// Default: 3
|
||||
pub words_min_length: Option<usize>,
|
||||
pub words_min_length: Option<u32>,
|
||||
/// Whether to fetch LSP completions or not.
|
||||
///
|
||||
/// Default: true
|
||||
@@ -516,7 +528,19 @@ pub struct CompletionSettingsContent {
|
||||
pub lsp_insert_mode: Option<LspInsertMode>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
PartialEq,
|
||||
Eq,
|
||||
JsonSchema,
|
||||
MergeFrom,
|
||||
strum::VariantArray,
|
||||
strum::VariantNames,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum LspInsertMode {
|
||||
/// Replaces text before the cursor, using the `insert` range described in the LSP specification.
|
||||
@@ -532,7 +556,19 @@ pub enum LspInsertMode {
|
||||
}
|
||||
|
||||
/// Controls how document's words are completed.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
PartialEq,
|
||||
Eq,
|
||||
JsonSchema,
|
||||
MergeFrom,
|
||||
strum::VariantArray,
|
||||
strum::VariantNames,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WordsCompletionMode {
|
||||
/// Always fetch document's words for completions along with LSP completions.
|
||||
@@ -559,17 +595,29 @@ pub struct PrettierSettingsContent {
|
||||
|
||||
/// Forces Prettier integration to use specific plugins when formatting files with the language.
|
||||
/// The default Prettier will be installed with these plugins.
|
||||
#[serde(default)]
|
||||
pub plugins: HashSet<String>,
|
||||
pub plugins: Option<HashSet<String>>,
|
||||
|
||||
/// Default Prettier options, in the format as in package.json section for Prettier.
|
||||
/// If project installs Prettier via its package.json, these options will be ignored.
|
||||
#[serde(flatten)]
|
||||
pub options: HashMap<String, serde_json::Value>,
|
||||
pub options: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
/// TODO: this should just be a bool
|
||||
/// Controls the behavior of formatting files when they are saved.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
MergeFrom,
|
||||
strum::VariantArray,
|
||||
strum::VariantNames,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum FormatOnSave {
|
||||
/// Files should be formatted on save.
|
||||
@@ -745,11 +793,10 @@ pub struct IndentGuideSettingsContent {
|
||||
|
||||
/// The task settings for a particular language.
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize, JsonSchema, MergeFrom)]
|
||||
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize, JsonSchema, MergeFrom)]
|
||||
pub struct LanguageTaskSettingsContent {
|
||||
/// Extra task variables to set for a particular language.
|
||||
#[serde(default)]
|
||||
pub variables: HashMap<String, String>,
|
||||
pub variables: Option<HashMap<String, String>>,
|
||||
pub enabled: Option<bool>,
|
||||
/// Use LSP tasks over Zed language extension ones.
|
||||
/// If no LSP tasks are returned due to error/timeout or regular execution,
|
||||
@@ -768,7 +815,18 @@ pub struct LanguageToSettingsMap(pub HashMap<SharedString, LanguageSettingsConte
|
||||
|
||||
/// Determines how indent guides are colored.
|
||||
#[derive(
|
||||
Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
|
||||
Default,
|
||||
Debug,
|
||||
Copy,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
MergeFrom,
|
||||
strum::VariantArray,
|
||||
strum::VariantNames,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum IndentGuideColoring {
|
||||
@@ -783,7 +841,18 @@ pub enum IndentGuideColoring {
|
||||
|
||||
/// Determines how indent guide backgrounds are colored.
|
||||
#[derive(
|
||||
Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
|
||||
Default,
|
||||
Debug,
|
||||
Copy,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
MergeFrom,
|
||||
strum::VariantArray,
|
||||
strum::VariantNames,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum IndentGuideBackgroundColoring {
|
||||
@@ -836,4 +905,25 @@ mod test {
|
||||
let result: Result<LanguageSettingsContent, _> = serde_json::from_str(raw_auto);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prettier_options() {
|
||||
let raw_prettier = r#"{"allowed": false, "tabWidth": 4, "semi": false}"#;
|
||||
let result = serde_json::from_str::<PrettierSettingsContent>(raw_prettier)
|
||||
.expect("Failed to parse prettier options");
|
||||
assert!(
|
||||
result
|
||||
.options
|
||||
.as_ref()
|
||||
.expect("options were flattened")
|
||||
.contains_key("semi")
|
||||
);
|
||||
assert!(
|
||||
result
|
||||
.options
|
||||
.as_ref()
|
||||
.expect("options were flattened")
|
||||
.contains_key("tabWidth")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1236
-31
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,7 @@ use std::{
|
||||
num::NonZeroU32,
|
||||
ops::Range,
|
||||
rc::Rc,
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
sync::{Arc, LazyLock, RwLock, atomic::AtomicBool},
|
||||
};
|
||||
use ui::{
|
||||
ButtonLike, ContextMenu, Divider, DropdownMenu, DropdownStyle, IconButtonShape, PopoverMenu,
|
||||
@@ -309,6 +309,26 @@ fn init_renderers(cx: &mut App) {
|
||||
.add_renderer::<settings::ShowCloseButton>(|settings_field, file, _, window, cx| {
|
||||
render_dropdown(*settings_field, file, window, cx)
|
||||
})
|
||||
.add_renderer::<settings::RewrapBehavior>(|settings_field, file, _, window, cx| {
|
||||
render_dropdown(*settings_field, file, window, cx)
|
||||
})
|
||||
.add_renderer::<settings::FormatOnSave>(|settings_field, file, _, window, cx| {
|
||||
render_dropdown(*settings_field, file, window, cx)
|
||||
})
|
||||
.add_renderer::<settings::IndentGuideColoring>(|settings_field, file, _, window, cx| {
|
||||
render_dropdown(*settings_field, file, window, cx)
|
||||
})
|
||||
.add_renderer::<settings::IndentGuideBackgroundColoring>(
|
||||
|settings_field, file, _, window, cx| {
|
||||
render_dropdown(*settings_field, file, window, cx)
|
||||
},
|
||||
)
|
||||
.add_renderer::<settings::WordsCompletionMode>(|settings_field, file, _, window, cx| {
|
||||
render_dropdown(*settings_field, file, window, cx)
|
||||
})
|
||||
.add_renderer::<settings::LspInsertMode>(|settings_field, file, _, window, cx| {
|
||||
render_dropdown(*settings_field, file, window, cx)
|
||||
})
|
||||
.add_renderer::<f32>(|settings_field, file, _, window, cx| {
|
||||
render_numeric_stepper(*settings_field, file, window, cx)
|
||||
})
|
||||
@@ -353,6 +373,26 @@ pub fn open_settings_editor(cx: &mut App) -> anyhow::Result<WindowHandle<Setting
|
||||
)
|
||||
}
|
||||
|
||||
/// The current sub page path that is selected.
|
||||
/// If this is empty the selected page is rendered,
|
||||
/// otherwise the last sub page gets rendered.
|
||||
///
|
||||
/// Global so that `pick` and `pick_mut` callbacks can access it
|
||||
/// and use it to dynamically render sub pages (e.g. for language settings)
|
||||
static SUB_PAGE_STACK: LazyLock<RwLock<Vec<SubPage>>> = LazyLock::new(|| RwLock::new(Vec::new()));
|
||||
|
||||
fn sub_page_stack() -> std::sync::RwLockReadGuard<'static, Vec<SubPage>> {
|
||||
SUB_PAGE_STACK
|
||||
.read()
|
||||
.expect("SUB_PAGE_STACK is never poisoned")
|
||||
}
|
||||
|
||||
fn sub_page_stack_mut() -> std::sync::RwLockWriteGuard<'static, Vec<SubPage>> {
|
||||
SUB_PAGE_STACK
|
||||
.write()
|
||||
.expect("SUB_PAGE_STACK is never poisoned")
|
||||
}
|
||||
|
||||
pub struct SettingsWindow {
|
||||
files: Vec<SettingsUiFile>,
|
||||
current_file: SettingsUiFile,
|
||||
@@ -363,10 +403,6 @@ pub struct SettingsWindow {
|
||||
navbar_entries: Vec<NavBarEntry>,
|
||||
list_handle: UniformListScrollHandle,
|
||||
search_matches: Vec<Vec<bool>>,
|
||||
/// The current sub page path that is selected.
|
||||
/// If this is empty the selected page is rendered,
|
||||
/// otherwise the last sub page gets rendered.
|
||||
sub_page_stack: Vec<SubPage>,
|
||||
scroll_handle: ScrollHandle,
|
||||
}
|
||||
|
||||
@@ -548,7 +584,12 @@ impl PartialEq for SettingItem {
|
||||
#[derive(Clone)]
|
||||
struct SubPageLink {
|
||||
title: &'static str,
|
||||
render: Rc<dyn Fn(&mut SettingsWindow, &mut Window, &mut App) -> AnyElement>,
|
||||
render: Arc<
|
||||
dyn Fn(&mut SettingsWindow, &mut Window, &mut Context<SettingsWindow>) -> AnyElement
|
||||
+ 'static
|
||||
+ Send
|
||||
+ Sync,
|
||||
>,
|
||||
}
|
||||
|
||||
impl PartialEq for SubPageLink {
|
||||
@@ -647,7 +688,6 @@ impl SettingsWindow {
|
||||
search_bar,
|
||||
search_task: None,
|
||||
search_matches: vec![],
|
||||
sub_page_stack: vec![],
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
};
|
||||
|
||||
@@ -978,7 +1018,7 @@ impl SettingsWindow {
|
||||
let mut items = vec![];
|
||||
items.push(self.current_page().title);
|
||||
items.extend(
|
||||
self.sub_page_stack
|
||||
sub_page_stack()
|
||||
.iter()
|
||||
.flat_map(|page| [page.section_header, page.link.title]),
|
||||
);
|
||||
@@ -995,6 +1035,73 @@ impl SettingsWindow {
|
||||
.child(Label::new(last))
|
||||
}
|
||||
|
||||
fn render_page_items<'a, Items: Iterator<Item = &'a SettingsPageItem>>(
|
||||
&self,
|
||||
items: Items,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<SettingsWindow>,
|
||||
) -> impl IntoElement {
|
||||
let mut page_content = v_flex()
|
||||
.id("settings-ui-page")
|
||||
.size_full()
|
||||
.gap_4()
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&self.scroll_handle);
|
||||
|
||||
let items: Vec<_> = items.collect();
|
||||
let items_len = items.len();
|
||||
let mut section_header = None;
|
||||
|
||||
let has_active_search = !self.search_bar.read(cx).is_empty(cx);
|
||||
let has_no_results = items_len == 0 && has_active_search;
|
||||
|
||||
if has_no_results {
|
||||
let search_query = self.search_bar.read(cx).text(cx);
|
||||
page_content = page_content.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_1()
|
||||
.child(div().child("No Results"))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(format!("No settings match \"{}\"", search_query)),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
let last_non_header_index = items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.find(|(_, item)| !matches!(item, SettingsPageItem::SectionHeader(_)))
|
||||
.map(|(index, _)| index);
|
||||
|
||||
page_content =
|
||||
page_content.children(items.clone().into_iter().enumerate().map(|(index, item)| {
|
||||
let no_bottom_border = items
|
||||
.get(index + 1)
|
||||
.map(|next_item| matches!(next_item, SettingsPageItem::SectionHeader(_)))
|
||||
.unwrap_or(false);
|
||||
let is_last = Some(index) == last_non_header_index;
|
||||
|
||||
if let SettingsPageItem::SectionHeader(header) = item {
|
||||
section_header = Some(*header);
|
||||
}
|
||||
item.render(
|
||||
self.current_file.clone(),
|
||||
section_header.expect("All items rendered after a section header"),
|
||||
no_bottom_border || is_last,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
}
|
||||
page_content
|
||||
}
|
||||
|
||||
fn render_page(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
@@ -1009,70 +1116,13 @@ impl SettingsWindow {
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.vertical_scrollbar_for(self.scroll_handle.clone(), window, cx);
|
||||
|
||||
let mut page_content = v_flex()
|
||||
.id("settings-ui-page")
|
||||
.size_full()
|
||||
.gap_4()
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&self.scroll_handle);
|
||||
let page_content;
|
||||
|
||||
if self.sub_page_stack.len() == 0 {
|
||||
if sub_page_stack().len() == 0 {
|
||||
page = page.child(self.render_files(window, cx));
|
||||
|
||||
let items: Vec<_> = self.page_items().collect();
|
||||
let items_len = items.len();
|
||||
let mut section_header = None;
|
||||
|
||||
let search_query = self.search_bar.read(cx).text(cx);
|
||||
let has_active_search = !search_query.is_empty();
|
||||
let has_no_results = items_len == 0 && has_active_search;
|
||||
|
||||
if has_no_results {
|
||||
page_content = page_content.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_1()
|
||||
.child(div().child("No Results"))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(format!("No settings match \"{}\"", search_query)),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
let last_non_header_index = items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.find(|(_, item)| !matches!(item, SettingsPageItem::SectionHeader(_)))
|
||||
.map(|(index, _)| index);
|
||||
|
||||
page_content = page_content.children(items.clone().into_iter().enumerate().map(
|
||||
|(index, item)| {
|
||||
let no_bottom_border = items
|
||||
.get(index + 1)
|
||||
.map(|next_item| {
|
||||
matches!(next_item, SettingsPageItem::SectionHeader(_))
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let is_last = Some(index) == last_non_header_index;
|
||||
|
||||
if let SettingsPageItem::SectionHeader(header) = item {
|
||||
section_header = Some(*header);
|
||||
}
|
||||
item.render(
|
||||
self.current_file.clone(),
|
||||
section_header.expect("All items rendered after a section header"),
|
||||
no_bottom_border || is_last,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
))
|
||||
}
|
||||
page_content = self
|
||||
.render_page_items(self.page_items(), window, cx)
|
||||
.into_any_element();
|
||||
} else {
|
||||
page = page.child(
|
||||
h_flex()
|
||||
@@ -1089,8 +1139,8 @@ impl SettingsWindow {
|
||||
.child(self.render_sub_page_breadcrumbs()),
|
||||
);
|
||||
|
||||
let active_page_render_fn = self.sub_page_stack.last().unwrap().link.render.clone();
|
||||
page_content = page_content.child((active_page_render_fn)(self, window, cx));
|
||||
let active_page_render_fn = sub_page_stack().last().unwrap().link.render.clone();
|
||||
page_content = (active_page_render_fn)(self, window, cx);
|
||||
}
|
||||
|
||||
return page.child(page_content);
|
||||
@@ -1122,7 +1172,7 @@ impl SettingsWindow {
|
||||
section_header: &'static str,
|
||||
cx: &mut Context<SettingsWindow>,
|
||||
) {
|
||||
self.sub_page_stack.push(SubPage {
|
||||
sub_page_stack_mut().push(SubPage {
|
||||
link: sub_page_link,
|
||||
section_header,
|
||||
});
|
||||
@@ -1130,7 +1180,7 @@ impl SettingsWindow {
|
||||
}
|
||||
|
||||
fn pop_sub_page(&mut self, cx: &mut Context<SettingsWindow>) {
|
||||
self.sub_page_stack.pop();
|
||||
sub_page_stack_mut().pop();
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -1558,7 +1608,6 @@ mod test {
|
||||
list_handle: UniformListScrollHandle::default(),
|
||||
search_matches: vec![],
|
||||
search_task: None,
|
||||
sub_page_stack: vec![],
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user