T17 Editor: CodeEditor with gutter, folds, minimap, rulers, guides, brackets, whitespace, cursors, multi-cursor, sticky scroll, inlay hints, lenses, decorations, ghost text, inline chat and diff (T17a); completions, signatures, hover, code actions, rename, references, outline, go to symbol, call and type hierarchy (T17b); find and replace, search panel and filters (T17c); problems, output, debug console, extensions, settings with JSON, keybindings, theme previews, welcome, recent projects, switcher, task runner (T17d); status items (T17e)
Shared: generic undo History, OverflowMenu icon and tooltip, StatusBarItem leading, six Lucide icons.
This commit is contained in:
1 parent
a894e5f973
commit
5545ce6458
59 files changed
+10820
-71
No files matched your search
@@ -0,0 +1,412 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
App, ElementId, Entity, FontWeight, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||
SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
buttons::{ButtonVariant, IconButton, SegmentedControl},
|
||||
forms::{Choice, JsonInput, NumberInput, Select, Switch, TextInput},
|
||||
primitives::IconName,
|
||||
theme::{ActiveTheme, ControlSize, Radius, TextSize},
|
||||
};
|
||||
|
||||
/// A setting's value and the form it takes.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum SettingValue {
|
||||
Bool(bool),
|
||||
Number {
|
||||
value: f64,
|
||||
min: f64,
|
||||
max: f64,
|
||||
step: f64,
|
||||
},
|
||||
Choice {
|
||||
value: SharedString,
|
||||
choices: Vec<SharedString>,
|
||||
},
|
||||
}
|
||||
|
||||
impl SettingValue {
|
||||
fn json(&self) -> serde_json::Value {
|
||||
match self {
|
||||
SettingValue::Bool(on) => (*on).into(),
|
||||
SettingValue::Number { value, .. } => serde_json::Number::from_f64(*value)
|
||||
.map(serde_json::Value::Number)
|
||||
.expect("a setting's number is finite"),
|
||||
SettingValue::Choice { value, .. } => value.to_string().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One setting: its dotted key, title, what it does, its value and the value it starts from.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Setting {
|
||||
pub key: SharedString,
|
||||
pub title: SharedString,
|
||||
pub description: SharedString,
|
||||
pub value: SettingValue,
|
||||
pub default: SettingValue,
|
||||
}
|
||||
|
||||
impl Setting {
|
||||
pub fn modified(&self) -> bool {
|
||||
self.value != self.default
|
||||
}
|
||||
}
|
||||
|
||||
/// The settings changed from their defaults, as a settings file holds them.
|
||||
pub fn settings_json(groups: &[(SharedString, Vec<Setting>)]) -> String {
|
||||
let map: serde_json::Map<String, serde_json::Value> = groups
|
||||
.iter()
|
||||
.flat_map(|(_, settings)| settings)
|
||||
.filter(|setting| setting.modified())
|
||||
.map(|setting| (setting.key.to_string(), setting.value.json()))
|
||||
.collect();
|
||||
serde_json::to_string_pretty(&serde_json::Value::Object(map)).expect("settings serialize")
|
||||
}
|
||||
|
||||
type OnChange = Rc<dyn Fn(&SharedString, SettingValue, &mut Window, &mut App)>;
|
||||
type OnIndex = Rc<dyn Fn(usize, &mut Window, &mut App)>;
|
||||
type OnFlag = Rc<dyn Fn(bool, &mut Window, &mut App)>;
|
||||
|
||||
/// Settings two ways: as rows of controls by section, each changed one marked with a way back, or as the JSON a settings file holds.
|
||||
#[derive(IntoElement)]
|
||||
pub struct SettingsEditor {
|
||||
id: ElementId,
|
||||
groups: Vec<(SharedString, Vec<Setting>)>,
|
||||
section: usize,
|
||||
json: Option<Entity<TextInput>>,
|
||||
on_change: Option<OnChange>,
|
||||
on_section: Option<OnIndex>,
|
||||
on_json: Option<OnFlag>,
|
||||
}
|
||||
|
||||
impl SettingsEditor {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
groups: impl IntoIterator<Item = (impl Into<SharedString>, Vec<Setting>)>,
|
||||
) -> Self {
|
||||
let groups: Vec<(SharedString, Vec<Setting>)> = groups
|
||||
.into_iter()
|
||||
.map(|(title, settings)| (title.into(), settings))
|
||||
.collect();
|
||||
assert!(!groups.is_empty(), "settings come in at least one section");
|
||||
Self {
|
||||
id: id.into(),
|
||||
groups,
|
||||
section: 0,
|
||||
json: None,
|
||||
on_change: None,
|
||||
on_section: None,
|
||||
on_json: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn section(mut self, section: usize) -> Self {
|
||||
self.section = section;
|
||||
self
|
||||
}
|
||||
|
||||
/// Shows the JSON, held in `field`; seed it with `settings_json`.
|
||||
pub fn json(mut self, field: &Entity<TextInput>) -> Self {
|
||||
self.json = Some(field.clone());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_change(
|
||||
mut self,
|
||||
handler: impl Fn(&SharedString, SettingValue, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_change = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_section(mut self, handler: impl Fn(usize, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_section = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
/// Gets whether the JSON view is wanted.
|
||||
pub fn on_json(mut self, handler: impl Fn(bool, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_json = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for SettingsEditor {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let mode = SegmentedControl::new(
|
||||
(self.id.clone(), "mode"),
|
||||
if self.json.is_some() { "json" } else { "ui" },
|
||||
)
|
||||
.size(ControlSize::Sm)
|
||||
.segment("ui", "Settings", None)
|
||||
.segment("json", "JSON", None);
|
||||
let mode = match self.on_json.clone() {
|
||||
Some(on_json) => {
|
||||
mode.on_change(move |key, window, cx| on_json(key.as_ref() == "json", window, cx))
|
||||
}
|
||||
None => mode,
|
||||
};
|
||||
let header = div().flex().items_center().justify_between().child(
|
||||
div()
|
||||
.text_size(theme.text_size(TextSize::Md))
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.child("Settings"),
|
||||
);
|
||||
let header = header.child(mode);
|
||||
if let Some(field) = self.json {
|
||||
return div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(header)
|
||||
.child(JsonInput::new(&field))
|
||||
.into_any_element();
|
||||
}
|
||||
let section = self.section.min(self.groups.len() - 1);
|
||||
let nav = self
|
||||
.groups
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, (title, settings))| {
|
||||
let pick = self.on_section.clone();
|
||||
let changed = settings.iter().filter(|setting| setting.modified()).count();
|
||||
div()
|
||||
.id((self.id.clone(), format!("section-{ix}")))
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded(theme.radius(Radius::Sm))
|
||||
.cursor_pointer()
|
||||
.when(ix == section, |row| {
|
||||
row.bg(colors.active).text_color(colors.fg)
|
||||
})
|
||||
.when(ix != section, |row| {
|
||||
row.text_color(colors.fg_muted)
|
||||
.hover(|row| row.bg(colors.hover))
|
||||
})
|
||||
.when_some(pick, |row, pick| {
|
||||
row.on_click(move |_, window, cx| pick(ix, window, cx))
|
||||
})
|
||||
.child(title.clone())
|
||||
.when(changed > 0, |row| {
|
||||
row.child(
|
||||
div()
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.text_color(colors.accent)
|
||||
.child(changed.to_string()),
|
||||
)
|
||||
})
|
||||
});
|
||||
let rows = self.groups[section]
|
||||
.1
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, setting)| {
|
||||
let id = ElementId::from((self.id.clone(), format!("{section}-{ix}")));
|
||||
let (change, key) = (self.on_change.clone(), setting.key.clone());
|
||||
let tell = move |value: SettingValue, window: &mut Window, cx: &mut App| {
|
||||
log::info!("settings: {key} = {value:?}");
|
||||
if let Some(change) = &change {
|
||||
change(&key, value, window, cx);
|
||||
}
|
||||
};
|
||||
let tell = Rc::new(tell);
|
||||
let control = match &setting.value {
|
||||
SettingValue::Bool(on) => {
|
||||
let tell = tell.clone();
|
||||
Switch::new(id.clone(), *on)
|
||||
.on_change(move |on, window, cx| {
|
||||
tell(SettingValue::Bool(on), window, cx)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
SettingValue::Number {
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
} => {
|
||||
let (tell, min, max, step) = (tell.clone(), *min, *max, *step);
|
||||
div()
|
||||
.w(theme.label_width())
|
||||
.child(
|
||||
NumberInput::new(id.clone(), *value)
|
||||
.range(min, max)
|
||||
.step(step)
|
||||
.size(ControlSize::Sm)
|
||||
.on_change(move |value, window, cx| {
|
||||
tell(
|
||||
SettingValue::Number {
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
SettingValue::Choice { value, choices } => {
|
||||
let (tell, all) = (tell.clone(), choices.clone());
|
||||
div()
|
||||
.w(theme.label_width() * 1.5)
|
||||
.child(
|
||||
Select::new(
|
||||
id.clone(),
|
||||
choices
|
||||
.iter()
|
||||
.map(|choice| Choice::new(choice.clone(), choice.clone())),
|
||||
)
|
||||
.selected(value.clone())
|
||||
.size(ControlSize::Sm)
|
||||
.on_change(
|
||||
move |picked, window, cx| {
|
||||
let value = SettingValue::Choice {
|
||||
value: picked.clone(),
|
||||
choices: all.clone(),
|
||||
};
|
||||
tell(value, window, cx)
|
||||
},
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
};
|
||||
let reset = setting.modified().then(|| {
|
||||
let (tell, default) = (tell.clone(), setting.default.clone());
|
||||
IconButton::new(
|
||||
(self.id.clone(), format!("reset-{section}-{ix}")),
|
||||
IconName::RotateCcw,
|
||||
)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.tooltip("Reset to default")
|
||||
.on_click(move |_, window, cx| tell(default.clone(), window, cx))
|
||||
});
|
||||
div()
|
||||
.flex()
|
||||
.items_start()
|
||||
.gap_3()
|
||||
.py_3()
|
||||
.pl_3()
|
||||
.border_l_2()
|
||||
.border_color(if setting.modified() {
|
||||
colors.accent
|
||||
} else {
|
||||
gpui::transparent_black()
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_baseline()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.text_color(colors.fg)
|
||||
.child(setting.title.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.font_family(theme.mono_family.clone())
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.text_color(colors.fg_subtle)
|
||||
.child(setting.key.clone()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_color(colors.fg_muted)
|
||||
.child(setting.description.clone()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.children(reset)
|
||||
.child(control),
|
||||
)
|
||||
});
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.child(header)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_6()
|
||||
.child(
|
||||
div()
|
||||
.w(theme.label_width() * 1.25)
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_0p5()
|
||||
.children(nav),
|
||||
)
|
||||
.child(div().flex_1().min_w_0().flex().flex_col().children(rows)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn json_holds_only_what_changed() {
|
||||
let groups = vec![(
|
||||
SharedString::from("Editor"),
|
||||
vec![
|
||||
Setting {
|
||||
key: "editor.fontSize".into(),
|
||||
title: "Font size".into(),
|
||||
description: "".into(),
|
||||
value: SettingValue::Number {
|
||||
value: 15.0,
|
||||
min: 8.0,
|
||||
max: 32.0,
|
||||
step: 1.0,
|
||||
},
|
||||
default: SettingValue::Number {
|
||||
value: 14.0,
|
||||
min: 8.0,
|
||||
max: 32.0,
|
||||
step: 1.0,
|
||||
},
|
||||
},
|
||||
Setting {
|
||||
key: "editor.minimap".into(),
|
||||
title: "Minimap".into(),
|
||||
description: "".into(),
|
||||
value: SettingValue::Bool(true),
|
||||
default: SettingValue::Bool(true),
|
||||
},
|
||||
],
|
||||
)];
|
||||
let json: serde_json::Value = serde_json::from_str(&settings_json(&groups)).expect("json");
|
||||
assert_eq!(json, serde_json::json!({ "editor.fontSize": 15.0 }));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user