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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-09-26 00:43:41 -04:00
1 parent c229a096ca
commit d05754ac19
59 files changed
+10820 -71

No files matched your search

+309
View File
@@ -0,0 +1,309 @@
use ely_gpui_component::{
buttons::{Button, SegmentedControl},
editor::{
CodeEditor, CodeLens, CursorShape, Diagnostic, DiffHunk, GhostText, GitMark, InlayHint,
LineNumbers, VimMode, VimModeIndicator,
},
navigation::{Breadcrumb, Crumb},
primitives::Severity,
theme::{ActiveTheme, Radius},
typography::Caption,
};
use gpui::{App, Div, Entity, IntoElement, ParentElement, Styled, Window, div, px};
use crate::{
probe::probe,
ui::{keep, row, section, set},
};
/// The code the editors open with.
pub(super) const SAMPLE: &str = r#"use std::collections::HashMap;
/// A palette of named colors, looked up by the component that paints.
pub struct Palette {
colors: HashMap<String, Color>,
fallback: Color,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Color {
pub red: f32,
pub green: f32,
pub blue: f32,
}
impl Palette {
pub fn new(fallback: Color) -> Self {
Self {
colors: HashMap::new(),
fallback,
}
}
pub fn insert(&mut self, name: &str, color: Color) {
self.colors.insert(name.to_string(), color);
}
/// The color for `name`, blended toward white by `lift`.
pub fn lookup(&self, name: &str, lift: f32) -> Color {
let base = self.colors.get(name).copied().unwrap_or(self.fallback);
match lift {
lift if lift <= 0.0 => base,
lift if lift >= 1.0 => Color { red: 1.0, green: 1.0, blue: 1.0 },
lift => Color {
red: base.red + (1.0 - base.red) * lift,
green: base.green + (1.0 - base.green) * lift,
blue: base.blue + (1.0 - base.blue) * lift,
},
}
}
}
fn main() {
let mut palette = Palette::new(Color { red: 0.1, green: 0.1, blue: 0.1 });
palette.insert("accent", Color { red: 0.21, green: 0.45, blue: 0.73 });
let tints: Vec<Color> = (0..5u8).map(|step| palette.lookup("accent", step as f32 / 4.0)).collect();
let count: u32 = tints.len();
let spare = 12;
println!("{count} tints of accent, the last {:?}", tints.last());
}
"#;
/// The byte where `needle` first starts in `text`.
pub(super) fn at(text: &str, needle: &str) -> usize {
text.find(needle)
.unwrap_or_else(|| panic!("{needle:?} is in the sample"))
}
/// The line `needle` first sits on in `text`.
fn line_of(text: &str, needle: &str) -> usize {
text[..at(text, needle)].matches('\n').count()
}
/// A bordered box an editor fills.
pub(super) fn framed(width: f32, height: f32, cx: &App) -> Div {
let theme = cx.theme();
div()
.w(px(width))
.h(px(height))
.rounded(theme.radius(Radius::Md))
.border_1()
.border_color(theme.colors.border)
.overflow_hidden()
}
fn main_editor(window: &mut Window, cx: &mut App) -> Entity<CodeEditor> {
window.use_keyed_state("editor-main", cx, |window, cx| {
let mut editor = CodeEditor::new(SAMPLE, window, cx)
.language("Rust")
.minimap()
.sticky_scroll()
.rainbow_brackets()
.rulers([100]);
let map = at(SAMPLE, "HashMap;");
let len = at(SAMPLE, "tints.len();");
let spare = at(SAMPLE, "spare");
let step = at(SAMPLE, "step as f32");
editor.set_diagnostics(
vec![
Diagnostic {
range: map..map + 7,
severity: Severity::Info,
message: "a BTreeMap keeps a steady order".into(),
},
Diagnostic {
range: spare..spare + 5,
severity: Severity::Warning,
message: "unused variable: `spare`".into(),
},
Diagnostic {
range: len..len + 11,
severity: Severity::Danger,
message: "mismatched types: expected `u32`, found `usize`".into(),
},
],
cx,
);
editor.set_inlay_hints(
vec![
InlayHint {
offset: at(SAMPLE, "let base") + 8,
text: ": Color".into(),
},
InlayHint {
offset: at(SAMPLE, "\"accent\", step"),
text: "name: ".into(),
},
InlayHint {
offset: step,
text: "lift: ".into(),
},
],
cx,
);
editor.set_code_lenses(
vec![CodeLens {
line: line_of(SAMPLE, " pub fn lookup"),
items: vec!["3 references".into(), "Run test".into(), "Debug".into()],
}],
cx,
);
let main = line_of(SAMPLE, "fn main");
editor.set_git_marks(
vec![
(main + 2, GitMark::Added),
(main + 3, GitMark::Modified),
(4, GitMark::Deleted),
],
cx,
);
editor.toggle_breakpoint(main + 3, cx);
editor
})
}
pub fn editor(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let editor = main_editor(window, cx);
let (line, column) = editor.read(cx).position();
let crumbs = Breadcrumb::new(
"editor-crumbs",
[
Crumb::new("src", "src"),
Crumb::new("palette", "palette.rs"),
Crumb::new("impl", "impl Palette"),
Crumb::new("lookup", "lookup"),
],
);
section(
"CodeEditor / EditorGutter / LineNumbers / FoldingControl / Minimap / Ruler / IndentGuide / BracketMatcher / RainbowBrackets / MultiCursor / SelectionHighlight / StickyScroll / Breadcrumb / InlayHints / CodeLens / InlineDecoration",
"Rust, colored as it reads. The gutter holds breakpoints, problems, numbers, folds and git marks, and the blocks around the top line stay pinned as it scrolls. Cmd-D selects the next match, Cmd-Alt-Down adds a cursor, Alt-click adds one anywhere, Cmd-/ comments, Tab indents.",
cx,
)
.child(crumbs)
.child(probe("editor-main", framed(840.0, 460.0, cx).child(editor)))
.child(Caption::new(format!("Ln {line}, Col {column}")))
}
pub fn looks(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let shape = keep("editor-shape", || CursorShape::Block, window, cx);
let numbers = keep("editor-numbers", || LineNumbers::Relative, window, cx);
let (now_shape, now_numbers) = (*shape.read(cx), *numbers.read(cx));
let editor = window.use_keyed_state("editor-looks", cx, |window, cx| {
CodeEditor::new(&SAMPLE[..at(SAMPLE, "impl Palette")], window, cx)
.language("Rust")
.show_whitespace()
});
editor.update(cx, |editor, cx| {
editor.set_cursor_shape(now_shape, cx);
editor.set_line_numbers(now_numbers, cx);
});
let (pick_shape, pick_numbers) = (shape.clone(), numbers.clone());
let shapes = SegmentedControl::new(
"editor-shapes",
match now_shape {
CursorShape::Line => "line",
CursorShape::Block => "block",
CursorShape::Underline => "underline",
},
)
.segment("line", "Line", None)
.segment("block", "Block", None)
.segment("underline", "Underline", None)
.on_change(move |key, _, cx| {
let shape = match key.as_ref() {
"line" => CursorShape::Line,
"underline" => CursorShape::Underline,
_ => CursorShape::Block,
};
set(&pick_shape, shape, cx)
});
let counting = SegmentedControl::new(
"editor-counting",
match now_numbers {
LineNumbers::Absolute => "absolute",
LineNumbers::Relative => "relative",
LineNumbers::Hidden => "hidden",
},
)
.segment("absolute", "Absolute", None)
.segment("relative", "Relative", None)
.segment("hidden", "Hidden", None)
.on_change(move |key, _, cx| {
let numbers = match key.as_ref() {
"absolute" => LineNumbers::Absolute,
"hidden" => LineNumbers::Hidden,
_ => LineNumbers::Relative,
};
set(&pick_numbers, numbers, cx)
});
section(
"WhitespaceRenderer / CursorRenderer",
"Dots for spaces, and the cursor as a line, a block or an underline; it blinks, and holds still under reduced motion. Numbers count from the top or from the cursor.",
cx,
)
.child(row().gap_4().child(shapes).child(counting))
.child(framed(840.0, 260.0, cx).child(editor))
}
pub fn changes(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let text = &SAMPLE[at(SAMPLE, "impl Palette")..at(SAMPLE, "fn main")];
let editor = window.use_keyed_state("editor-changes", cx, |window, cx| {
let mut editor = CodeEditor::new(text, window, cx).language("Rust");
editor.set_diff(
vec![DiffHunk {
line: line_of(text, " let base"),
added: 1,
removed: vec![" let base = self.colors[name];".into()],
}],
cx,
);
let end = at(text, " match lift");
editor.set_ghost_text(
Some(GhostText {
offset: end,
text: " // a lift past one stays white\n".into(),
}),
cx,
);
let cursor = end..end;
editor.select([cursor], cx);
editor
});
let readonly = window.use_keyed_state("editor-readonly", cx, |window, cx| {
CodeEditor::new(&SAMPLE[..at(SAMPLE, "impl Palette")], window, cx)
.language("Rust")
.read_only()
});
let open = editor.read(cx).chat_line().is_some();
let chat_editor = editor.clone();
let toggle = Button::new(
"editor-chat",
if open {
"Close inline chat"
} else {
"Open inline chat"
},
)
.on_click(move |_, window, cx| {
chat_editor.update(cx, |editor, cx| {
let next = editor.chat_line().is_none().then_some(3);
editor.set_chat(next, window, cx)
})
});
section(
"InlineDiff / GhostText / InlineChat / ReadOnlyBanner / VimModeIndicator",
"A changed line under the one it replaced; a suggestion in grey that Tab takes; a prompt between lines; a banner over code that cannot change here; and the mode of a modal editor.",
cx,
)
.child(row().child(toggle))
.child(framed(840.0, 300.0, cx).child(editor))
.child(framed(840.0, 180.0, cx).child(readonly))
.child(
row()
.gap_4()
.child(VimModeIndicator::new(VimMode::Normal))
.child(VimModeIndicator::new(VimMode::Insert))
.child(VimModeIndicator::new(VimMode::Visual).pending("d2"))
.child(VimModeIndicator::new(VimMode::Replace)),
)
}
+241
View File
@@ -0,0 +1,241 @@
use ely_gpui_component::{
buttons::Button,
editor::{
ActionKind, Call, CallHierarchy, CodeAction, CodeActionMenu, CompletionItem,
CompletionMenu, FileHits, GoToSymbol, Hit, HoverInfo, ReferencesPanel, RenameInput,
Signature, SignatureHelp, Symbol, SymbolKind, SymbolOutline, completions,
},
forms::TextInput,
primitives::Severity,
typography::Caption,
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use crate::ui::{keep, row, section, set};
fn items() -> Vec<CompletionItem> {
vec![
CompletionItem::new("lookup", SymbolKind::Method)
.detail("fn(&self, &str, f32) -> Color")
.docs("The color for a name, blended toward white by lift."),
CompletionItem::new("insert", SymbolKind::Method).detail("fn(&mut self, &str, Color)"),
CompletionItem::new("colors", SymbolKind::Field).detail("HashMap<String, Color>"),
CompletionItem::new("fallback", SymbolKind::Field).detail("Color"),
CompletionItem::new("len", SymbolKind::Method).detail("fn(&self) -> usize"),
CompletionItem::new("Palette", SymbolKind::Struct).detail("struct Palette"),
CompletionItem::new("loop", SymbolKind::Keyword),
]
}
pub fn hints(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let overload = keep("intel-overload", || 0usize, window, cx);
let told = keep("intel-told", || None::<SharedString>, window, cx);
let (over, said) = (*overload.read(cx), told.read(cx).clone());
let chosen = completions(&items(), "lo")
.iter()
.position(|(item, _)| item.label.as_ref() == "lookup")
.expect("lookup fits lo");
let param = |label: &str, text: &str| {
let start = label.find(text).expect("the parameter is in the label");
start..start + text.len()
};
let (long, short) = (
"fn lookup(&self, name: &str, lift: f32) -> Color",
"fn lookup(&self, name: &str) -> Color",
);
let (pick, step, tell) = (told.clone(), overload.clone(), told.clone());
let signatures = [
Signature {
label: long.into(),
params: vec![param(long, "name: &str"), param(long, "lift: f32")],
docs: Some("The color for a name, blended toward white by lift.".into()),
},
Signature {
label: short.into(),
params: vec![param(short, "name: &str")],
docs: Some("The color for a name as it is.".into()),
},
];
section(
"CompletionMenu / SignatureHelp / HoverInfo",
"Completions as you type, the typed letters marked and the chosen one's documentation beside; the call's signature with the parameter at the cursor; and what a hover over a symbol reads.",
cx,
)
.child(
row()
.items_start()
.gap_8()
.child(
CompletionMenu::new("intel-menu", items())
.typed("lo")
.selected(chosen)
.on_pick(move |item, _, cx| set(&pick, Some(format!("Picked {}.", item.label).into()), cx)),
)
.child(
div()
.flex()
.flex_col()
.gap_4()
.child(
SignatureHelp::new("intel-signature", signatures)
.at(over, 1)
.on_step(move |next, _, cx| set(&step, next, cx)),
)
.child(
HoverInfo::new("intel-hover", "pub fn lookup(&self, name: &str, lift: f32) -> Color")
.problem(Severity::Warning, "`lift` outside 0..=1 is clamped")
.docs("The color for `name`, blended toward white by `lift`.")
.docs("Unknown names read as the fallback color.")
.link("Go to definition", move |_, cx| set(&tell, Some("Went to palette.rs:23.".into()), cx))
.link("Find references", |_, _| {}),
),
),
)
.children(said.map(Caption::new))
}
fn symbols() -> Vec<Symbol> {
vec![
Symbol::new("Palette", SymbolKind::Struct, 3).children([
Symbol::new("colors", SymbolKind::Field, 4).detail("HashMap<String, Color>"),
Symbol::new("fallback", SymbolKind::Field, 5).detail("Color"),
]),
Symbol::new("Color", SymbolKind::Struct, 9),
Symbol::new("impl Palette", SymbolKind::Module, 15).children([
Symbol::new("new", SymbolKind::Method, 16).detail("fn(Color) -> Self"),
Symbol::new("insert", SymbolKind::Method, 23),
Symbol::new("lookup", SymbolKind::Method, 28).detail("fn(&self, &str, f32) -> Color"),
]),
Symbol::new("main", SymbolKind::Function, 42),
]
}
pub fn navigate(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let cursor = keep("intel-cursor", || 30usize, window, cx);
let palette = keep("intel-goto", || false, window, cx);
let incoming = keep("intel-incoming", || true, window, cx);
let rename_open = keep("intel-rename", || false, window, cx);
let field = window.use_keyed_state("intel-rename-field", cx, |window, cx| {
let mut field = TextInput::new(window, cx);
field.set_text("lookup", cx);
field
});
let (line, open, inbound, renaming) = (
*cursor.read(cx),
*palette.read(cx),
*incoming.read(cx),
*rename_open.read(cx),
);
let (pick, go, close, flip) = (
cursor.clone(),
cursor.clone(),
palette.clone(),
incoming.clone(),
);
let opener = palette.clone();
let (renamer, done, cancel) = (
rename_open.clone(),
rename_open.clone(),
rename_open.clone(),
);
let hit = |line: usize, text: &str| {
let start = text.find("lookup").expect("the line names lookup");
let name = start..start + "lookup".len();
Hit {
line,
text: text.to_string().into(),
ranges: vec![name],
}
};
let references = [
FileHits {
path: "src/palette.rs".into(),
hits: vec![hit(
27,
" pub fn lookup(&self, name: &str, lift: f32) -> Color {",
)],
},
FileHits {
path: "src/main.rs".into(),
hits: vec![
hit(
44,
" let tints: Vec<Color> = (0..5u8).map(|step| palette.lookup(\"accent\", step as f32 / 4.0)).collect();",
),
hit(51, " let text = palette.lookup(\"fg\", 0.0);"),
],
},
];
let callers = Call {
name: "lookup".into(),
kind: SymbolKind::Method,
place: "palette.rs:28".into(),
calls: vec![
Call {
name: "main".into(),
kind: SymbolKind::Function,
place: "main.rs:44".into(),
calls: Vec::new(),
},
Call {
name: "paint_theme".into(),
kind: SymbolKind::Function,
place: "theme.rs:12".into(),
calls: vec![Call {
name: "render".into(),
kind: SymbolKind::Method,
place: "app.rs:88".into(),
calls: Vec::new(),
}],
},
],
};
let fixes = [
CodeAction::new("Change type of `count` to `usize`", ActionKind::QuickFix).preferred(),
CodeAction::new("Convert with `u32::try_from`", ActionKind::QuickFix),
CodeAction::new("Extract into function", ActionKind::Refactor),
CodeAction::new("Inline variable", ActionKind::Refactor),
CodeAction::new("Organize imports", ActionKind::Source),
];
section(
"CodeActionMenu / RenameInput / PeekView / ReferencesPanel / SymbolOutline / GoToSymbol / CallHierarchy / TypeHierarchy",
"Fixes and refactors behind a lightbulb; renaming in place; where a symbol is used; a file's symbols as a tree that follows the cursor, or as a palette to jump by name; and who calls a function.",
cx,
)
.child(
row()
.gap_4()
.child(CodeActionMenu::new("intel-actions", fixes))
.child(Button::new("intel-goto-open", "Go to symbol…").on_click(move |_, _, cx| set(&opener, true, cx)))
.child(Button::new("intel-rename-open", "Rename lookup").on_click(move |_, _, cx| set(&renamer, true, cx)))
.child(Caption::new(format!("Cursor on line {}.", line + 1))),
)
.children(renaming.then(|| {
RenameInput::new("intel-rename-box", &field)
.on_rename(move |_, _, cx| set(&done, false, cx))
.on_cancel(move |_, cx| set(&cancel, false, cx))
}))
.child(
row()
.items_start()
.gap_8()
.child(
div().w(px(300.)).h(px(240.)).child(
SymbolOutline::new("intel-outline", symbols())
.cursor(line)
.on_pick(move |line, _, cx| set(&pick, line, cx)),
),
)
.child(
div().w(px(300.)).h(px(240.)).child(
CallHierarchy::new("intel-calls", callers, inbound)
.on_flip(move |incoming, _, cx| set(&flip, incoming, cx)),
),
),
)
.child(div().w(px(840.)).child(ReferencesPanel::new("intel-references", "lookup", references)))
.children(open.then(|| {
GoToSymbol::new("intel-goto-palette", symbols(), move |_, cx| set(&close, false, cx))
.on_pick(move |line, _, cx| set(&go, line, cx))
}))
}
+59
View File
@@ -0,0 +1,59 @@
mod code;
mod intel;
mod panels;
mod search;
mod settings;
mod start;
mod status;
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
use super::Page;
use crate::script::Step;
pub const PAGE: Page = Page {
number: 17,
slug: "editor",
title: "Editor",
summary: "A code editor with the gutter, folds and cursors it needs, the help a language server gives, search, the panels around it and its status row.",
render,
script: SCRIPT,
};
const SCRIPT: &[Step] = &[
Step::Rest,
Step::DownAt("editor-main", 118.0, 104.0),
Step::UpAt("editor-main", 118.0, 104.0),
Step::Key("cmd-d"),
Step::Key("cmd-d"),
Step::Wait(300),
Step::Shot("cursors"),
Step::Key("escape"),
Step::Key("cmd-/"),
Step::Wait(300),
Step::Shot("commented"),
Step::Key("cmd-z"),
Step::Wait(300),
Step::DownAt("status", 249.0, 11.0),
Step::UpAt("status", 249.0, 11.0),
Step::Wait(300),
Step::Shot("status"),
Step::Rest,
];
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
div()
.child(code::editor(window, cx))
.child(code::looks(window, cx))
.child(code::changes(window, cx))
.child(intel::hints(window, cx))
.child(intel::navigate(window, cx))
.child(search::find(window, cx))
.child(search::panel(window, cx))
.child(panels::problems_and_output(window, cx))
.child(panels::console_and_extensions(window, cx))
.child(settings::settings_and_keys(window, cx))
.child(start::start(window, cx))
.child(status::status(window, cx))
.into_any_element()
}
+259
View File
@@ -0,0 +1,259 @@
use ely_gpui_component::{
editor::{
ConsoleEntry, DebugConsole, Extension, ExtensionAction, ExtensionState, ExtensionsPanel,
OutputPanel, Problem, ProblemsPanel,
},
forms::TextInput,
primitives::Severity,
typography::Caption,
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use crate::ui::{keep, section, set};
fn problems() -> Vec<Problem> {
let problem = |path: &str, line, column, severity, message: &str, source: &str| Problem {
path: path.to_string().into(),
line,
column,
severity,
message: message.to_string().into(),
source: Some(source.to_string().into()),
};
vec![
problem(
"src/main.rs",
45,
21,
Severity::Danger,
"mismatched types: expected `u32`, found `usize`",
"rustc",
),
problem(
"src/main.rs",
46,
8,
Severity::Warning,
"unused variable: `spare`",
"rustc",
),
problem(
"src/palette.rs",
0,
23,
Severity::Info,
"a BTreeMap keeps a steady order",
"clippy",
),
problem(
"src/theme/tokens.rs",
112,
4,
Severity::Warning,
"this function has too many arguments (8/7)",
"clippy",
),
]
}
pub fn problems_and_output(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let shown = keep(
"panels-shown",
|| vec![Severity::Danger, Severity::Warning, Severity::Info],
window,
cx,
);
let channel = keep("panels-channel", || SharedString::from("Build"), window, cx);
let follow = keep("panels-follow", || true, window, cx);
let cleared = keep("panels-cleared", || false, window, cx);
let opened = keep("panels-problem", || None::<SharedString>, window, cx);
let (now_shown, now_channel, now_follow, now_cleared, said) = (
shown.read(cx).clone(),
channel.read(cx).clone(),
*follow.read(cx),
*cleared.read(cx),
opened.read(cx).clone(),
);
let lines: Vec<&str> = if now_cleared {
Vec::new()
} else if now_channel.as_ref() == "Build" {
vec![
" Compiling ely-gpui-component v0.1.0",
"warning: unused variable: `spare`",
"error[E0308]: mismatched types",
" --> src/main.rs:46:22",
"error: could not compile `palette` due to 1 previous error",
]
} else {
vec![
"[info] rust-analyzer started",
"[debug] indexing 214 files",
"[info] indexed in 1.2 s",
]
};
let (show, pick, keep_up, clear, open) = (
shown.clone(),
channel.clone(),
follow.clone(),
cleared.clone(),
opened.clone(),
);
let reset = cleared.clone();
section(
"ProblemsPanel / DiagnosticsList / OutputPanel",
"Every problem by file, worst first, with toggles by severity; and a tool's log by channel, levels tinted, following the newest line.",
cx,
)
.child(
div()
.flex()
.gap_8()
.child(
div().flex_1().child(
ProblemsPanel::new("panels-problems", problems())
.shown(now_shown)
.on_shown(move |next, _, cx| set(&show, next, cx))
.on_open(move |problem, _, cx| {
set(&open, Some(format!("Opened {}:{}.", problem.path, problem.line + 1).into()), cx)
}),
),
)
.child(
div().w(px(400.)).h(px(220.)).child(
OutputPanel::new("panels-output", ["Build", "Language server"], now_channel, lines)
.follow(now_follow)
.on_channel(move |name, _, cx| {
set(&pick, name.clone(), cx);
set(&reset, false, cx);
})
.on_follow(move |on, _, cx| set(&keep_up, on, cx))
.on_clear(move |_, cx| set(&clear, true, cx)),
),
),
)
.children(said.map(Caption::new))
}
pub fn console_and_extensions(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let field = window.use_keyed_state("panels-console-field", cx, TextInput::new);
let entries = keep(
"panels-console",
|| {
vec![
ConsoleEntry::Input("tints.len()".into()),
ConsoleEntry::Value("5".into(), "usize".into()),
ConsoleEntry::Input("palette.lookup(\"accent\", 0.5).red".into()),
ConsoleEntry::Value("0.605".into(), "f32".into()),
ConsoleEntry::Input("spare + missing".into()),
ConsoleEntry::Error("cannot find value `missing` in this scope".into()),
]
},
window,
cx,
);
let states = keep(
"panels-extensions",
|| {
vec![
ExtensionState::Installed,
ExtensionState::Available,
ExtensionState::Outdated,
ExtensionState::Disabled,
]
},
window,
cx,
);
let now_entries = entries.read(cx).clone();
let now_states = states.read(cx).clone();
let extension = |ix: usize,
id: &str,
name: &str,
publisher: &str,
description: &str,
version: &str,
installs: u64,
rating: f32| Extension {
id: id.to_string().into(),
name: name.to_string().into(),
publisher: publisher.to_string().into(),
description: description.to_string().into(),
version: version.to_string().into(),
installs,
rating,
state: now_states[ix],
};
let extensions = vec![
extension(
0,
"rust",
"Rust",
"Ely Tools",
"Syntax, completions and checks for Rust.",
"1.4.2",
2_410_000,
4.8,
),
extension(
1,
"toml",
"Even Better TOML",
"Tamasfe",
"TOML with schemas and formatting.",
"0.19.2",
830_000,
4.6,
),
extension(
2,
"lens",
"Git Lens",
"Line Labs",
"Who changed each line, and when.",
"14.1.0",
5_900_000,
4.4,
),
extension(
3,
"vim",
"Vim",
"Modal Folk",
"Modal editing with the keys you know.",
"1.27.3",
1_200_000,
4.2,
),
];
let (eval, act) = (entries.clone(), states.clone());
section(
"DebugConsole / ExtensionsPanel / PluginList",
"An expression prompt beside a paused program; and extensions with their reach, rating, and the one action each needs next.",
cx,
)
.child(
div()
.flex()
.gap_8()
.child(
div().w(px(380.)).child(DebugConsole::new("panels-console", &field, now_entries).on_eval(move |text, _, cx| {
let mut next = eval.read(cx).clone();
next.push(ConsoleEntry::Input(text.clone()));
next.push(match text.trim().parse::<i64>() {
Ok(value) => ConsoleEntry::Value(value.to_string().into(), "i64".into()),
Err(_) => ConsoleEntry::Error(format!("cannot find value `{}` in this scope", text.trim()).into()),
});
set(&eval, next, cx)
})),
)
.child(div().flex_1().child(ExtensionsPanel::new("panels-extensions", extensions).on_action(move |id, action, _, cx| {
let ix = ["rust", "toml", "lens", "vim"].iter().position(|known| *known == id.as_ref()).expect("a listed extension");
let mut next = act.read(cx).clone();
next[ix] = match action {
ExtensionAction::Install | ExtensionAction::Enable | ExtensionAction::Update => ExtensionState::Installed,
ExtensionAction::Uninstall => ExtensionState::Available,
};
set(&act, next, cx)
}))),
)
}
+241
View File
@@ -0,0 +1,241 @@
use ely_gpui_component::{
editor::{
CodeEditor, FileHits, FindOptions, FindWidget, Hit, SearchFilters, SearchPanel, find_all,
passes,
},
forms::TextInput,
theme::ActiveTheme,
typography::Caption,
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use super::code::{SAMPLE, framed};
use crate::ui::{keep, section, set};
fn field(
key: &'static str,
text: &str,
window: &mut Window,
cx: &mut App,
) -> gpui::Entity<TextInput> {
let seed = text.to_string();
window.use_keyed_state(key, cx, move |window, cx| {
let mut field = TextInput::new(window, cx);
field.set_text(seed, cx);
field
})
}
pub fn find(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let editor = window.use_keyed_state("search-editor", cx, |window, cx| {
CodeEditor::new(SAMPLE, window, cx).language("Rust")
});
let query = field("search-find", "lift", window, cx);
let replacement = field("search-replace", "blend", window, cx);
let options = keep("search-options", FindOptions::default, window, cx);
let current = keep("search-current", || 0usize, window, cx);
let replacing = keep("search-replacing", || true, window, cx);
let (now, at, showing) = (*options.read(cx), *current.read(cx), *replacing.read(cx));
let text = editor.read(cx).text().to_string();
let found = find_all(&text, query.read(cx).text(), now);
let colors = cx.theme().colors.clone();
let (widget, current_range) = match &found {
Ok(ranges) => {
let at = if ranges.is_empty() {
None
} else {
Some(at.min(ranges.len() - 1))
};
let marks: Vec<_> = ranges
.iter()
.enumerate()
.map(|(ix, range)| {
let wash = if Some(ix) == at {
colors.warning.opacity(0.45)
} else {
colors.warning.opacity(0.2)
};
(range.clone(), wash)
})
.collect();
editor.update(cx, |editor, cx| editor.set_backgrounds(marks, cx));
(
FindWidget::new("search-widget", &query, at, ranges.len()),
at.map(|ix| ranges[ix].clone()),
)
}
Err(message) => {
editor.update(cx, |editor, cx| editor.set_backgrounds(Vec::new(), cx));
(
FindWidget::new("search-widget", &query, None, 0)
.error(message.lines().last().unwrap_or("bad pattern").to_string()),
None,
)
}
};
if let Some(range) = current_range.clone() {
editor.update(cx, |editor, cx| editor.select([range], cx));
}
let total = found.as_ref().map_or(0, Vec::len);
let (set_options, step, toggle) = (options.clone(), current.clone(), replacing.clone());
let (swap, swap_editor, swap_query, swap_with) = (
current.clone(),
editor.clone(),
query.clone(),
replacement.clone(),
);
let widget = widget
.options(now)
.on_options(move |next, _, cx| set(&set_options, next, cx))
.on_step(move |forward, _, cx| {
let at = *step.read(cx);
let next = if total == 0 {
0
} else if forward {
(at + 1) % total
} else {
(at + total - 1) % total
};
set(&step, next, cx)
})
.on_toggle_replace(move |show, _, cx| set(&toggle, show, cx))
.on_replace(move |all, _, cx| {
let text = swap_editor.read(cx).text().to_string();
let Ok(ranges) = find_all(&text, swap_query.read(cx).text(), now) else {
return;
};
let with = swap_with.read(cx).text().to_string();
let chosen: Vec<_> = if all {
ranges
.into_iter()
.map(|range| (range, with.clone()))
.collect()
} else {
let at = (*swap.read(cx)).min(ranges.len().saturating_sub(1));
ranges
.into_iter()
.nth(at)
.map(|range| (range, with.clone()))
.into_iter()
.collect()
};
swap_editor.update(cx, |editor, cx| editor.edit(chosen, cx));
});
let widget = if showing {
widget.replace(&replacement)
} else {
widget
};
section(
"FindWidget / ReplaceWidget",
"Find over code by case, whole word or pattern; the current match glows among the rest, and the arrows step between them. The replace row swaps one or every match.",
cx,
)
.child(
framed(840.0, 320.0, cx)
.relative()
.child(editor)
.child(div().absolute().top_1().right_4().child(widget)),
)
}
fn results() -> Vec<FileHits> {
let hit = |line: usize, text: &str, needle: &str| {
let start = text.find(needle).expect("the needle is in the line");
let found = start..start + needle.len();
Hit {
line,
text: text.to_string().into(),
ranges: vec![found],
}
};
vec![
FileHits {
path: "src/palette.rs".into(),
hits: vec![
hit(
27,
" /// The color for `name`, blended toward white by `lift`.",
"lift",
),
hit(
28,
" pub fn lookup(&self, name: &str, lift: f32) -> Color {",
"lift",
),
hit(30, " match lift {", "lift"),
],
},
FileHits {
path: "src/theme/tokens.rs".into(),
hits: vec![hit(
112,
" pub fn surface_lift(&self, level: u8) -> f32 {",
"lift",
)],
},
FileHits {
path: "tests/palette.rs".into(),
hits: vec![hit(
8,
" assert_eq!(palette.lookup(\"accent\", 1.0), WHITE, \"a full lift is white\");",
"lift",
)],
},
]
}
pub fn panel(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let include = field("search-include", "src/**", window, cx);
let exclude = field("search-exclude", "", window, cx);
let ignore = keep("search-ignore", || true, window, cx);
let folded = keep("search-folded", Vec::<SharedString>::new, window, cx);
let opened = keep("search-opened", || None::<SharedString>, window, cx);
let (now_ignore, now_folded, said) = (
*ignore.read(cx),
folded.read(cx).clone(),
opened.read(cx).clone(),
);
let (include_text, exclude_text) = (
include.read(cx).text().to_string(),
exclude.read(cx).text().to_string(),
);
let shown: Vec<FileHits> = results()
.into_iter()
.filter(|file| passes(&file.path, &include_text, &exclude_text))
.collect();
let (flip, fold, open) = (ignore.clone(), folded.clone(), opened.clone());
section(
"SearchPanel / SearchResultItem / SearchFilters",
"Matches across files, each file folding away, each line trimmed near its match; globs choose which files count.",
cx,
)
.child(
div()
.flex()
.gap_8()
.child(
div().w(px(280.)).child(
SearchFilters::new("search-filters", &include, &exclude)
.ignore(now_ignore)
.on_ignore(move |on, _, cx| set(&flip, on, cx)),
),
)
.child(
div().flex_1().child(
SearchPanel::new("search-panel", shown)
.folded(now_folded)
.on_fold(move |path, _, cx| {
let mut now = fold.read(cx).clone();
match now.iter().position(|folded| folded == path) {
Some(ix) => drop(now.remove(ix)),
None => now.push(path.clone()),
}
set(&fold, now, cx)
})
.on_open(move |path, line, _, cx| set(&open, Some(format!("Opened {path}:{}.", line + 1).into()), cx)),
),
),
)
.children(said.map(Caption::new))
}
+208
View File
@@ -0,0 +1,208 @@
use ely_gpui_component::{
editor::{
KeySource, Keybinding, KeybindingsEditor, Setting, SettingValue, SettingsEditor,
settings_json,
},
forms::{TextInput, json_highlights},
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use crate::ui::{keep, section, set};
fn settings() -> Vec<(SharedString, Vec<Setting>)> {
let number = |value, min, max| SettingValue::Number {
value,
min,
max,
step: 1.0,
};
let choice = |value: &str, choices: &[&str]| SettingValue::Choice {
value: value.to_string().into(),
choices: choices
.iter()
.map(|choice| SharedString::from(choice.to_string()))
.collect(),
};
let setting = |key: &str, title: &str, description: &str, value: SettingValue| Setting {
key: key.to_string().into(),
title: title.to_string().into(),
description: description.to_string().into(),
default: value.clone(),
value,
};
vec![
(
"Editor".into(),
vec![
setting(
"editor.fontSize",
"Font size",
"Code text size in pixels.",
number(14.0, 8.0, 32.0),
),
setting(
"editor.minimap",
"Minimap",
"A narrow map of the file beside the code.",
SettingValue::Bool(true),
),
setting(
"editor.cursorStyle",
"Cursor style",
"How the cursor draws.",
choice("line", &["line", "block", "underline"]),
),
],
),
(
"Files".into(),
vec![
setting(
"files.autoSave",
"Auto save",
"When files save themselves.",
choice("afterDelay", &["off", "afterDelay", "onFocusChange"]),
),
setting(
"files.trimTrailingWhitespace",
"Trim trailing whitespace",
"Drops spaces at line ends on save.",
SettingValue::Bool(false),
),
],
),
]
}
pub fn settings_and_keys(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let groups = keep("panels-settings", settings, window, cx);
let section_now = keep("panels-section", || 0usize, window, cx);
let json = keep("panels-json", || false, window, cx);
let json_field = window.use_keyed_state("panels-json-field", cx, |window, cx| {
TextInput::new(window, cx)
.multi_line(6, 12)
.highlighter(json_highlights)
});
let (now_groups, now_section, now_json) = (
groups.read(cx).clone(),
*section_now.read(cx),
*json.read(cx),
);
let (change, pick, flip, seed, source) = (
groups.clone(),
section_now.clone(),
json.clone(),
json_field.clone(),
groups.clone(),
);
let recording = keep("panels-recording", || None::<SharedString>, window, cx);
let bindings = keep(
"panels-bindings",
|| {
let binding =
|command: &str, title: &str, keys: Option<&str>, when: Option<&str>, source| {
Keybinding {
command: command.to_string().into(),
title: title.to_string().into(),
keys: keys.map(|keys| keys.to_string().into()),
when: when.map(|when| when.to_string().into()),
source,
}
};
vec![
binding(
"editor.save",
"Save",
Some("secondary-s"),
None,
KeySource::Default,
),
binding(
"editor.sortLines",
"Sort lines",
Some("secondary-s"),
None,
KeySource::User,
),
binding(
"editor.comment",
"Toggle comment",
Some("secondary-/"),
Some("editorFocus"),
KeySource::Default,
),
binding(
"editor.selectNext",
"Select next match",
Some("secondary-d"),
Some("editorFocus"),
KeySource::Default,
),
binding(
"terminal.split",
"Split terminal",
Some("secondary-s"),
Some("terminalFocus"),
KeySource::User,
),
binding(
"editor.fold",
"Fold",
None,
Some("editorFocus"),
KeySource::Default,
),
]
},
window,
cx,
);
let (now_recording, now_bindings) = (recording.read(cx).clone(), bindings.read(cx).clone());
let (record, bind, done) = (recording.clone(), bindings.clone(), recording.clone());
let editor = SettingsEditor::new("panels-settings-editor", now_groups.clone())
.section(now_section)
.on_section(move |ix, _, cx| set(&pick, ix, cx))
.on_json(move |on, _, cx| {
if on {
let text = settings_json(source.read(cx));
seed.update(cx, |field, cx| field.set_text(text, cx));
}
set(&flip, on, cx)
})
.on_change(move |key, value, _, cx| {
let mut next = change.read(cx).clone();
for (_, list) in &mut next {
for setting in list.iter_mut().filter(|setting| setting.key == *key) {
setting.value = value.clone();
}
}
set(&change, next, cx)
});
let editor = if now_json {
editor.json(&json_field)
} else {
editor
};
section(
"SettingsEditor / KeybindingsEditor",
"Settings as controls by section, each change marked with a way back, or as the JSON a settings file holds; and every command's keys, clashes marked. Double-press a command to record new keys.",
cx,
)
.child(div().w(px(840.)).child(editor))
.child(
div().w(px(840.)).child(
KeybindingsEditor::new("panels-keys", now_bindings)
.recording(now_recording)
.on_record(move |command, _, cx| set(&record, Some(command.clone()), cx))
.on_bind(move |command, stroke, _, cx| {
let mut next = bind.read(cx).clone();
if let Some(binding) = next.iter_mut().find(|binding| binding.command == *command) {
binding.keys = stroke.map(|stroke| stroke.unparse().into());
binding.source = KeySource::User;
}
set(&bind, next, cx);
set(&done, None, cx)
}),
),
)
}
+170
View File
@@ -0,0 +1,170 @@
use std::time::Duration;
use ely_gpui_component::{
buttons::Button,
editor::{
Project, ProjectSwitcher, RecentProjects, Task, TaskRunner, TaskState, ThemePreview,
WelcomePage,
},
primitives::IconName,
theme::{ActiveTheme, Mode, Palette, Theme},
typography::Caption,
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use crate::ui::{keep, row, section, set};
fn projects() -> Vec<Project> {
let project = |name: &str, path: &str, branch: Option<&str>, opened: &str, pinned| Project {
name: name.to_string().into(),
path: path.to_string().into(),
branch: branch.map(|branch| branch.to_string().into()),
opened: opened.to_string().into(),
pinned,
};
vec![
project(
"ely-gpui-component",
"~/Documents/GitHub/Ely-GPUI-Components",
Some("main"),
"now",
true,
),
project(
"palette",
"~/Code/palette",
Some("feature/lift"),
"2 h ago",
false,
),
project("notes", "~/Code/notes", None, "yesterday", false),
project("zed", "~/Code/zed", Some("main"), "3 days ago", false),
]
}
pub fn start(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let switching = keep("panels-switcher", || false, window, cx);
let opened = keep("panels-opened", || None::<SharedString>, window, cx);
let show = keep("panels-welcome", || true, window, cx);
let tasks = keep(
"panels-tasks",
|| {
vec![
TaskState::Passed(Duration::from_secs(14)),
TaskState::Running,
TaskState::Failed(101),
TaskState::Idle,
]
},
window,
cx,
);
let open_task = keep("panels-task-open", || Some(2usize), window, cx);
let (now_switching, said, now_show, now_tasks, now_open) = (
*switching.read(cx),
opened.read(cx).clone(),
*show.read(cx),
tasks.read(cx).clone(),
*open_task.read(cx),
);
let mode = cx.theme().mode();
let task = |name: &str, group: &str, command: &str, state| Task {
name: name.to_string().into(),
group: group.to_string().into(),
command: command.to_string().into(),
cwd: "~/Documents/GitHub/Ely-GPUI-Components".into(),
env: vec![("RUST_LOG".into(), "info".into())],
state,
};
let task_list = vec![
task("check", "Build", "cargo check --all-targets", now_tasks[0]),
task(
"gallery",
"Run",
"cargo run --example gallery",
now_tasks[1],
),
task(
"test",
"Test",
"cargo test --lib --features test-support",
now_tasks[2],
),
task(
"clippy",
"Test",
"cargo clippy --all-targets -- -D warnings",
now_tasks[3],
),
];
let (open, pin, close, pick, start_run, stop_run, fold) = (
opened.clone(),
opened.clone(),
switching.clone(),
opened.clone(),
tasks.clone(),
tasks.clone(),
open_task.clone(),
);
let (opener, toggle) = (switching.clone(), show.clone());
let recent = RecentProjects::new("panels-recent", projects())
.on_open(move |ix, _, cx| {
set(
&open,
Some(format!("Opened {}.", projects()[ix].name).into()),
cx,
)
})
.on_pin(move |ix, _, cx| {
set(
&pin,
Some(format!("Pinned {}.", projects()[ix].name).into()),
cx,
)
});
section(
"WelcomePage / RecentProjects / ProjectSwitcher / ThemePreview / TaskRunner / RunConfiguration",
"Where an editor starts: ways in, recent projects and a switcher; each theme painted in its own colors; and tasks with how their last run went, one open to its configuration.",
cx,
)
.child(
div().w(px(840.)).child(
WelcomePage::new("panels-welcome-page", "Ely Editor", "Quiet tools for careful code.")
.action(IconName::FilePlus, "New file", Some("secondary-n"))
.action(IconName::FolderOpen, "Open folder", Some("secondary-o"))
.action(IconName::GitBranch, "Clone repository", None)
.recent(recent)
.show_at_start(now_show)
.on_show(move |on, _, cx| set(&toggle, on, cx)),
),
)
.child(row().child(Button::new("panels-switch", "Switch project…").on_click(move |_, _, cx| set(&opener, true, cx))))
.children(said.map(Caption::new))
.child(
row()
.gap_6()
.child(div().w(px(260.)).child(ThemePreview::new("panels-light", "Ely Light", Palette::light(false)).selected(mode == Mode::Light).on_pick(|_, cx| Theme::set_mode(Mode::Light, cx))))
.child(div().w(px(260.)).child(ThemePreview::new("panels-dark", "Ely Dark", Palette::dark(false)).selected(mode == Mode::Dark).on_pick(|_, cx| Theme::set_mode(Mode::Dark, cx)))),
)
.child(
div().w(px(840.)).child(
TaskRunner::new("panels-tasks", task_list)
.open(now_open)
.on_open(move |ix, _, cx| set(&fold, ix, cx))
.on_run(move |ix, _, cx| {
let mut next = start_run.read(cx).clone();
next[ix] = TaskState::Running;
set(&start_run, next, cx)
})
.on_stop(move |ix, _, cx| {
let mut next = stop_run.read(cx).clone();
next[ix] = TaskState::Idle;
set(&stop_run, next, cx)
}),
),
)
.children(now_switching.then(|| {
ProjectSwitcher::new("panels-switcher-palette", projects(), move |_, cx| set(&close, false, cx))
.on_open(move |ix, _, cx| set(&pick, Some(format!("Switched to {}.", projects()[ix].name).into()), cx))
}))
}
+86
View File
@@ -0,0 +1,86 @@
use ely_gpui_component::{
editor::{
BranchIndicator, CursorPosition, Encoding, IndentSettings, LanguageMode, LineEnding,
LspState, LspStatus, NotificationBell,
},
shell::StatusBar,
typography::Caption,
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use crate::{
probe::probe,
ui::{keep, section, set},
};
const LANGUAGES: [&str; 3] = ["Rust", "TOML", "Markdown"];
fn lsp(step: usize) -> LspState {
match step % 4 {
0 => LspState::Starting,
1 => LspState::Busy("indexing 214 files".into()),
2 => LspState::Ready,
_ => LspState::Failed("the server exited with code 101".into()),
}
}
pub fn status(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let language = keep("status-language", || 0usize, window, cx);
let crlf = keep("status-crlf", || false, window, cx);
let spaces = keep("status-spaces", || true, window, cx);
let server = keep("status-server", || 1usize, window, cx);
let unread = keep("status-unread", || 3usize, window, cx);
let said = keep("status-said", || None::<SharedString>, window, cx);
let (now_language, now_crlf, now_spaces, now_server, now_unread, told) = (
*language.read(cx),
*crlf.read(cx),
*spaces.read(cx),
*server.read(cx),
*unread.read(cx),
said.read(cx).clone(),
);
let (go, sync, encode) = (said.clone(), said.clone(), said.clone());
let bar = StatusBar::new()
.left(
BranchIndicator::new("status-branch", "feature/editor")
.dirty(true)
.sync(2, 1)
.on_click(move |_, cx| set(&sync, Some("Pulled 1 commit, pushed 2.".into()), cx)),
)
.left(
LspStatus::new("status-lsp", "rust-analyzer", lsp(now_server))
.on_click(move |_, cx| set(&server, now_server + 1, cx)),
)
.right(
CursorPosition::new("status-cursor", 48, 27)
.selected(6)
.on_click(move |_, cx| set(&go, Some("Go to line…".into()), cx)),
)
.right(
IndentSettings::new("status-indent", now_spaces, 4)
.on_click(move |_, cx| set(&spaces, !now_spaces, cx)),
)
.right(
Encoding::new("status-encoding", "UTF-8")
.on_click(move |_, cx| set(&encode, Some("Reopen with encoding…".into()), cx)),
)
.right(
LineEnding::new("status-ending", now_crlf)
.on_click(move |_, cx| set(&crlf, !now_crlf, cx)),
)
.right(
LanguageMode::new("status-language", LANGUAGES[now_language])
.on_click(move |_, cx| set(&language, (now_language + 1) % LANGUAGES.len(), cx)),
)
.right(
NotificationBell::new("status-bell", now_unread)
.on_click(move |_, cx| set(&unread, 0, cx)),
);
section(
"CursorPosition / LanguageMode / Encoding / LineEnding / IndentSettings / BranchIndicator / LspStatus / NotificationBell",
"The quiet row under the code. Each item says one thing and opens the one control that changes it: press the language, the line ending or the indent to switch them, the server to step through its states, the bell to read.",
cx,
)
.child(probe("status", div().w(px(840.)).child(bar)))
.children(told.map(Caption::new))
}
+2
View File
@@ -1,6 +1,7 @@
mod buttons;
mod charts;
mod data;
mod editor;
mod feedback;
mod finance;
mod forms;
@@ -47,6 +48,7 @@ pub const ALL: &[Page] = &[
tables::PAGE,
charts::PAGE,
finance::PAGE,
editor::PAGE,
theme::PAGE,
];