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,447 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
App, ElementId, Hsla, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
buttons::SegmentedControl,
|
||||
lists::{Tree, TreeNode},
|
||||
navigation::{Group, Palette, Row, fuzzy, marked, query_field},
|
||||
primitives::IconName,
|
||||
theme::{ActiveTheme, ControlSize, Palette as Colors},
|
||||
};
|
||||
|
||||
/// What a symbol in code is.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SymbolKind {
|
||||
Module,
|
||||
Struct,
|
||||
Enum,
|
||||
Trait,
|
||||
Function,
|
||||
Method,
|
||||
Field,
|
||||
Variable,
|
||||
Constant,
|
||||
Keyword,
|
||||
Snippet,
|
||||
}
|
||||
|
||||
impl SymbolKind {
|
||||
pub fn icon(self) -> IconName {
|
||||
match self {
|
||||
SymbolKind::Module => IconName::Package,
|
||||
SymbolKind::Struct => IconName::Braces,
|
||||
SymbolKind::Enum => IconName::ListOrdered,
|
||||
SymbolKind::Trait => IconName::Shapes,
|
||||
SymbolKind::Function | SymbolKind::Method => IconName::SquareFunction,
|
||||
SymbolKind::Field => IconName::Tag,
|
||||
SymbolKind::Variable => IconName::Variable,
|
||||
SymbolKind::Constant => IconName::Hash,
|
||||
SymbolKind::Keyword => IconName::Key,
|
||||
SymbolKind::Snippet => IconName::Code,
|
||||
}
|
||||
}
|
||||
|
||||
/// The syntax color its name takes.
|
||||
pub fn color(self, colors: &Colors) -> Hsla {
|
||||
let syntax = &colors.syntax;
|
||||
match self {
|
||||
SymbolKind::Module | SymbolKind::Snippet => colors.fg_muted,
|
||||
SymbolKind::Struct | SymbolKind::Enum | SymbolKind::Trait => syntax.type_name,
|
||||
SymbolKind::Function | SymbolKind::Method => syntax.function,
|
||||
SymbolKind::Field => syntax.property,
|
||||
SymbolKind::Variable => syntax.variable,
|
||||
SymbolKind::Constant => syntax.constant,
|
||||
SymbolKind::Keyword => syntax.keyword,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn word(self) -> &'static str {
|
||||
match self {
|
||||
SymbolKind::Module => "module",
|
||||
SymbolKind::Struct => "struct",
|
||||
SymbolKind::Enum => "enum",
|
||||
SymbolKind::Trait => "trait",
|
||||
SymbolKind::Function => "function",
|
||||
SymbolKind::Method => "method",
|
||||
SymbolKind::Field => "field",
|
||||
SymbolKind::Variable => "variable",
|
||||
SymbolKind::Constant => "constant",
|
||||
SymbolKind::Keyword => "keyword",
|
||||
SymbolKind::Snippet => "snippet",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A symbol in a file: its name, kind, the line it starts on, a detail such as its type, and what it holds.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Symbol {
|
||||
pub name: SharedString,
|
||||
pub kind: SymbolKind,
|
||||
pub line: usize,
|
||||
pub detail: Option<SharedString>,
|
||||
pub children: Vec<Symbol>,
|
||||
}
|
||||
|
||||
impl Symbol {
|
||||
pub fn new(name: impl Into<SharedString>, kind: SymbolKind, line: usize) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
kind,
|
||||
line,
|
||||
detail: None,
|
||||
children: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detail(mut self, detail: impl Into<SharedString>) -> Self {
|
||||
self.detail = Some(detail.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn children(mut self, children: impl IntoIterator<Item = Symbol>) -> Self {
|
||||
self.children = children.into_iter().collect();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
type OnLine = Rc<dyn Fn(usize, &mut Window, &mut App)>;
|
||||
|
||||
fn key(symbol: &Symbol) -> SharedString {
|
||||
format!("{}:{}", symbol.line, symbol.name).into()
|
||||
}
|
||||
|
||||
fn node(symbol: &Symbol) -> TreeNode {
|
||||
let tree = TreeNode::new(key(symbol), symbol.name.clone()).icon(symbol.kind.icon());
|
||||
let tree = match &symbol.detail {
|
||||
Some(detail) => tree.note(detail.clone()),
|
||||
None => tree,
|
||||
};
|
||||
if symbol.children.is_empty() {
|
||||
return tree;
|
||||
}
|
||||
tree.children(symbol.children.iter().map(node))
|
||||
}
|
||||
|
||||
/// Each symbol with the path of those holding it, outermost first.
|
||||
pub(crate) fn flattened(symbols: &[Symbol]) -> Vec<(Vec<SharedString>, &Symbol)> {
|
||||
fn walk<'a>(
|
||||
symbols: &'a [Symbol],
|
||||
path: &mut Vec<SharedString>,
|
||||
out: &mut Vec<(Vec<SharedString>, &'a Symbol)>,
|
||||
) {
|
||||
for symbol in symbols {
|
||||
out.push((path.clone(), symbol));
|
||||
path.push(symbol.name.clone());
|
||||
walk(&symbol.children, path, out);
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
walk(symbols, &mut Vec::new(), &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// The innermost symbol at or above a line.
|
||||
pub(crate) fn enclosing(symbols: &[Symbol], line: usize) -> Option<&Symbol> {
|
||||
let mut best = None;
|
||||
for (_, symbol) in flattened(symbols) {
|
||||
if symbol.line <= line && best.is_none_or(|best: &Symbol| symbol.line >= best.line) {
|
||||
best = Some(symbol);
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
/// A file's symbols as a tree filling its box, the one around the cursor chosen; a press goes to its line.
|
||||
#[derive(IntoElement)]
|
||||
pub struct SymbolOutline {
|
||||
id: ElementId,
|
||||
symbols: Vec<Symbol>,
|
||||
cursor: Option<usize>,
|
||||
on_pick: Option<OnLine>,
|
||||
}
|
||||
|
||||
impl SymbolOutline {
|
||||
pub fn new(id: impl Into<ElementId>, symbols: impl IntoIterator<Item = Symbol>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
symbols: symbols.into_iter().collect(),
|
||||
cursor: None,
|
||||
on_pick: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The cursor's line, whose symbol is chosen.
|
||||
pub fn cursor(mut self, line: usize) -> Self {
|
||||
self.cursor = Some(line);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_pick(mut self, handler: impl Fn(usize, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_pick = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for SymbolOutline {
|
||||
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
||||
let open: Vec<SharedString> = flattened(&self.symbols)
|
||||
.into_iter()
|
||||
.filter(|(_, symbol)| !symbol.children.is_empty())
|
||||
.map(|(_, symbol)| key(symbol))
|
||||
.collect();
|
||||
let chosen = self
|
||||
.cursor
|
||||
.and_then(|line| enclosing(&self.symbols, line))
|
||||
.map(key);
|
||||
let lines: Vec<(SharedString, usize)> = flattened(&self.symbols)
|
||||
.into_iter()
|
||||
.map(|(_, symbol)| (key(symbol), symbol.line))
|
||||
.collect();
|
||||
let tree = Tree::new(self.id, self.symbols.iter().map(node))
|
||||
.size_full()
|
||||
.open(open)
|
||||
.selected(chosen);
|
||||
match self.on_pick {
|
||||
Some(on_pick) => tree.on_select(move |keys, window, cx| {
|
||||
let Some(picked) = keys.first() else {
|
||||
return;
|
||||
};
|
||||
let line = lines
|
||||
.iter()
|
||||
.find(|(key, _)| key == picked)
|
||||
.map(|(_, line)| *line)
|
||||
.expect("a picked symbol is in the outline");
|
||||
log::info!("symbol outline: line {line}");
|
||||
on_pick(line, window, cx);
|
||||
}),
|
||||
None => tree,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type OnClose = Rc<dyn Fn(&mut Window, &mut App)>;
|
||||
type OnFlip = Rc<dyn Fn(bool, &mut Window, &mut App)>;
|
||||
|
||||
/// A palette of a file's symbols by name, each with its kind and what holds it; `@` style jumping.
|
||||
#[derive(IntoElement)]
|
||||
pub struct GoToSymbol {
|
||||
id: ElementId,
|
||||
symbols: Vec<Symbol>,
|
||||
on_pick: Option<OnLine>,
|
||||
on_close: OnClose,
|
||||
}
|
||||
|
||||
impl GoToSymbol {
|
||||
/// Render it while open; `on_close` runs on Escape, a click outside, or a pick.
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
symbols: impl IntoIterator<Item = Symbol>,
|
||||
on_close: impl Fn(&mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
symbols: symbols.into_iter().collect(),
|
||||
on_pick: None,
|
||||
on_close: Rc::new(on_close),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_pick(mut self, handler: impl Fn(usize, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_pick = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for GoToSymbol {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let input = query_field(&self.id, "Go to a symbol", window, cx);
|
||||
let query = input.read(cx).text().trim().to_string();
|
||||
let colors = cx.theme().colors.clone();
|
||||
let mut found: Vec<(i32, Row)> = flattened(&self.symbols)
|
||||
.into_iter()
|
||||
.filter_map(|(path, symbol)| {
|
||||
let fit = if query.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(fuzzy(&query, &symbol.name)?)
|
||||
};
|
||||
let (score, hits) = fit.map_or((0, Vec::new()), |fit| (fit.score, fit.hits));
|
||||
let detail = (!path.is_empty()).then(|| {
|
||||
div()
|
||||
.text_color(colors.fg_subtle)
|
||||
.child(path.join(" › "))
|
||||
.into_any_element()
|
||||
});
|
||||
let row = Row {
|
||||
value: symbol.line.to_string().into(),
|
||||
icon: Some(symbol.kind.icon()),
|
||||
label: marked(symbol.name.clone(), hits, cx),
|
||||
detail,
|
||||
end: Some(
|
||||
div()
|
||||
.text_color(colors.fg_subtle)
|
||||
.child(symbol.kind.word())
|
||||
.into_any_element(),
|
||||
),
|
||||
};
|
||||
Some((score, row))
|
||||
})
|
||||
.collect();
|
||||
if !query.is_empty() {
|
||||
found.sort_by(|(a, _), (b, _)| b.cmp(a));
|
||||
}
|
||||
let on_pick = self.on_pick.map(|on_pick| {
|
||||
Rc::new(
|
||||
move |value: &SharedString, window: &mut Window, cx: &mut App| {
|
||||
let line: usize = value.parse().expect("a symbol row carries its line");
|
||||
on_pick(line, window, cx);
|
||||
},
|
||||
) as Rc<dyn Fn(&SharedString, &mut Window, &mut App)>
|
||||
});
|
||||
Palette {
|
||||
id: self.id,
|
||||
input,
|
||||
groups: vec![Group {
|
||||
title: None,
|
||||
rows: found.into_iter().map(|(_, row)| row).collect(),
|
||||
}],
|
||||
start: 0,
|
||||
empty: "No matching symbols".into(),
|
||||
on_pick,
|
||||
on_close: self.on_close,
|
||||
}
|
||||
.overlay(window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// A call in a hierarchy: who, of what kind, where, and the calls under it.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Call {
|
||||
pub name: SharedString,
|
||||
pub kind: SymbolKind,
|
||||
pub place: SharedString,
|
||||
pub calls: Vec<Call>,
|
||||
}
|
||||
|
||||
/// Who calls a function or whom it calls, or a type's supertypes and subtypes, as a tree filling its box that flips between the two directions.
|
||||
#[derive(IntoElement)]
|
||||
pub struct CallHierarchy {
|
||||
id: ElementId,
|
||||
root: Call,
|
||||
labels: (SharedString, SharedString),
|
||||
incoming: bool,
|
||||
on_flip: Option<OnFlip>,
|
||||
}
|
||||
|
||||
impl CallHierarchy {
|
||||
/// Calls to or from `root`; `incoming` picks which the tree shows.
|
||||
pub fn new(id: impl Into<ElementId>, root: Call, incoming: bool) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
root,
|
||||
labels: ("Incoming".into(), "Outgoing".into()),
|
||||
incoming,
|
||||
on_flip: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// For types: supertypes and subtypes instead of calls.
|
||||
pub fn types(mut self) -> Self {
|
||||
self.labels = ("Supertypes".into(), "Subtypes".into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Gets whether the incoming side is chosen.
|
||||
pub fn on_flip(mut self, handler: impl Fn(bool, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_flip = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn call_node(call: &Call, path: &str) -> TreeNode {
|
||||
let key = format!("{path}/{}", call.name);
|
||||
let node = TreeNode::new(key.clone(), call.name.clone())
|
||||
.icon(call.kind.icon())
|
||||
.note(call.place.clone());
|
||||
if call.calls.is_empty() {
|
||||
return node;
|
||||
}
|
||||
node.children(call.calls.iter().map(|inner| call_node(inner, &key)))
|
||||
}
|
||||
|
||||
fn keys(call: &Call, path: &str, out: &mut Vec<SharedString>) {
|
||||
let key = format!("{path}/{}", call.name);
|
||||
for inner in &call.calls {
|
||||
keys(inner, &key, out);
|
||||
}
|
||||
out.push(key.into());
|
||||
}
|
||||
|
||||
impl RenderOnce for CallHierarchy {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let (incoming, outgoing) = self.labels;
|
||||
let side = SegmentedControl::new(
|
||||
(self.id.clone(), "side"),
|
||||
if self.incoming { "in" } else { "out" },
|
||||
)
|
||||
.size(ControlSize::Sm)
|
||||
.segment("in", incoming, None)
|
||||
.segment("out", outgoing, None);
|
||||
let side = match self.on_flip {
|
||||
Some(on_flip) => {
|
||||
side.on_change(move |key, window, cx| on_flip(key.as_ref() == "in", window, cx))
|
||||
}
|
||||
None => side,
|
||||
};
|
||||
let mut open = Vec::new();
|
||||
keys(&self.root, "", &mut open);
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.text_size(theme.text_size(crate::theme::TextSize::Sm))
|
||||
.child(side)
|
||||
.child(
|
||||
Tree::new((self.id, "calls"), [call_node(&self.root, "")])
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.open(open),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_innermost_symbol_above_a_line_holds_it() {
|
||||
let symbols = vec![
|
||||
Symbol::new("Editor", SymbolKind::Struct, 2),
|
||||
Symbol::new("impl Editor", SymbolKind::Module, 10).children([
|
||||
Symbol::new("new", SymbolKind::Method, 11),
|
||||
Symbol::new("render", SymbolKind::Method, 20),
|
||||
]),
|
||||
];
|
||||
assert_eq!(
|
||||
enclosing(&symbols, 25).map(|symbol| &symbol.name[..]),
|
||||
Some("render")
|
||||
);
|
||||
assert_eq!(
|
||||
enclosing(&symbols, 5).map(|symbol| &symbol.name[..]),
|
||||
Some("Editor")
|
||||
);
|
||||
assert_eq!(enclosing(&symbols, 0), None);
|
||||
let paths: Vec<usize> = flattened(&symbols)
|
||||
.iter()
|
||||
.map(|(path, _)| path.len())
|
||||
.collect();
|
||||
assert_eq!(paths, [0, 0, 1, 1]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user