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:
ZacharyZhang-NY committed 2026-09-26 00:43:41 -04:00
1 parent a894e5f973
commit 5545ce6458
59 files changed
+10820 -71

No files matched your search

+447
View File
@@ -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]);
}
}