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:
1 parent
c229a096ca
commit
d05754ac19
59 files changed
+10820
-71
No files matched your search
@@ -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)),
|
||||
)
|
||||
}
|
||||
@@ -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))
|
||||
}))
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}))),
|
||||
)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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))
|
||||
}))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
|
||||
Reference in new issue
Block a user