T17 Editor: CodeEditor with gutter, folds, minimap, rulers, guides, brackets, whitespace, cursors, multi-cursor, sticky scroll, inlay hints, lenses, decorations, ghost text, inline chat and diff (T17a); completions, signatures, hover, code actions, rename, references, outline, go to symbol, call and type hierarchy (T17b); find and replace, search panel and filters (T17c); problems, output, debug console, extensions, settings with JSON, keybindings, theme previews, welcome, recent projects, switcher, task runner (T17d); status items (T17e)
Shared: generic undo History, OverflowMenu icon and tooltip, StatusBarItem leading, six Lucide icons.
This commit is contained in:
1 parent
a894e5f973
commit
5545ce6458
59 files changed
+10820
-71
No files matched your search
@@ -0,0 +1,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)),
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user