236 lines
5.9 KiB
Go
236 lines
5.9 KiB
Go
package mujica
|
|
|
|
import (
|
|
"log/slog"
|
|
"strings"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"mujicaui/internal"
|
|
"mujicaui/theme"
|
|
)
|
|
|
|
// SymbolKind is what a Symbol declares.
|
|
type SymbolKind int
|
|
|
|
const (
|
|
SymbolFunction SymbolKind = iota
|
|
SymbolMethod
|
|
SymbolType
|
|
SymbolInterface
|
|
SymbolField
|
|
SymbolVariable
|
|
SymbolConstant
|
|
SymbolModule
|
|
)
|
|
|
|
// Symbol is a declaration spanning Line to EndLine, from 1.
|
|
type Symbol struct {
|
|
Name, Detail string
|
|
Kind SymbolKind
|
|
Line, EndLine int
|
|
Children []Symbol
|
|
}
|
|
|
|
// SymbolOutlineState is a SymbolOutline's filter, open symbols and choice.
|
|
type SymbolOutlineState struct {
|
|
Query string
|
|
outline ui.OutlineState[int]
|
|
cursor int
|
|
nodes []symbolNode
|
|
roots []int
|
|
data *Symbol
|
|
n int
|
|
line int
|
|
started bool
|
|
}
|
|
|
|
type symbolNode struct {
|
|
sym *Symbol
|
|
parent int
|
|
kids []int
|
|
}
|
|
|
|
// SymbolOutlineOptions configure a SymbolOutline.
|
|
type SymbolOutlineOptions struct {
|
|
// CursorLine chooses its symbol when it changes; 0 is off.
|
|
CursorLine int
|
|
Disabled bool
|
|
}
|
|
|
|
// SymbolOutlineResult carries the symbol to jump to.
|
|
type SymbolOutlineResult struct {
|
|
Element *ui.Element
|
|
Jumped *Symbol
|
|
}
|
|
|
|
var symbolBadges = [...]string{"f", "m", "T", "I", "p", "v", "c", "M"}
|
|
|
|
// SymbolOutline shows a filterable symbol tree.
|
|
func SymbolOutline(c *ui.Context, symbols []Symbol, s *SymbolOutlineState, opts SymbolOutlineOptions) SymbolOutlineResult {
|
|
if s == nil {
|
|
panic("mujica: SymbolOutline needs a state")
|
|
}
|
|
if !s.started {
|
|
s.started, s.cursor = true, -1
|
|
}
|
|
symbolIndex(symbols, s)
|
|
k := Tokens(c)
|
|
d := Density(c)
|
|
var res SymbolOutlineResult
|
|
res.Element = codePanel(c).Disabled(opts.Disabled)
|
|
disabled := res.Element.IsDisabled()
|
|
res.Element.Children(func() {
|
|
codeHeader(c, func() {
|
|
SearchInput(c, &s.Query, SearchInputOptions{Placeholder: msg(c, "symbol_outline.filter"), Disabled: opts.Disabled}).Element.Grow(1)
|
|
})
|
|
q := strings.ToLower(strings.TrimSpace(s.Query))
|
|
keep := symbolFilter(s, q)
|
|
roots := symbolKept(s.roots, keep)
|
|
if len(roots) == 0 {
|
|
codeCaption(c, msg(c, "symbol_outline.empty")).Padding(d.Unit() * 4)
|
|
return
|
|
}
|
|
follow := opts.CursorLine != s.line
|
|
s.line = opts.CursorLine
|
|
if follow {
|
|
symbolFollow(s, opts.CursorLine, keep)
|
|
}
|
|
s.outline.List.Selected = &s.cursor
|
|
s.outline.List.Label = func(i int) string { return s.nodes[s.outline.Item(i)].sym.Name }
|
|
children := func(i int) []int {
|
|
if len(s.nodes[i].kids) == 0 {
|
|
return nil
|
|
}
|
|
if q != "" {
|
|
s.outline.Open.Add(i)
|
|
}
|
|
return symbolKept(s.nodes[i].kids, keep)
|
|
}
|
|
restore := dataSelectTheme(c, 0)
|
|
e := ui.Outline(c, &s.outline, roots, children, func(i int) {
|
|
sym := s.nodes[i].sym
|
|
if sym.Kind < SymbolFunction || sym.Kind > SymbolModule {
|
|
panic("mujica: Symbol kind out of range")
|
|
}
|
|
ui.Row(c).MinHeight(d.ControlHeight() - 2*c.Theme().Space(1)).Gap(d.Unit() * 2).AlignItems(ui.Center).Grow(1).MinWidth(0).Children(func() {
|
|
codeBadge(c, symbolBadges[sym.Kind], [...]ui.Color{k.AccentText, k.AccentText, k.Info, k.Info, k.TextMuted, k.TextMuted, k.Warning, k.Success}[sym.Kind])
|
|
ui.RichText(c, codeMatchSpans(c, sym.Name, q)...).SingleLine().MinWidth(0)
|
|
if sym.Detail != "" {
|
|
codeCaption(c, sym.Detail).MinWidth(0)
|
|
}
|
|
ui.Spacer(c)
|
|
codeSelectMark(c, s.cursor >= 0 && s.cursor < s.outline.Rows() && s.outline.Item(s.cursor) == i)
|
|
})
|
|
})
|
|
restore()
|
|
e.Grow(1).MinHeight(d.ControlHeight() * 3)
|
|
internal.Ring(e, k.Focus, theme.ControlRadius)
|
|
if follow && s.cursor >= 0 {
|
|
s.outline.List.ScrollIntoView(s.cursor)
|
|
}
|
|
if !disabled && s.cursor >= 0 && s.cursor < s.outline.Rows() && e.Submitted() {
|
|
sym := *s.nodes[s.outline.Item(s.cursor)].sym
|
|
res.Jumped = &sym
|
|
slog.Debug("mujica: symbol_outline jump", "line", sym.Line)
|
|
}
|
|
})
|
|
return res
|
|
}
|
|
|
|
// symbolIndex numbers the symbols depth first when the tree changes.
|
|
func symbolIndex(symbols []Symbol, s *SymbolOutlineState) {
|
|
var first *Symbol
|
|
if len(symbols) > 0 {
|
|
first = &symbols[0]
|
|
}
|
|
if s.data == first && s.n == len(symbols) {
|
|
return
|
|
}
|
|
s.data, s.n, s.nodes, s.roots = first, len(symbols), s.nodes[:0], s.roots[:0]
|
|
var walk func(list []Symbol, parent int) []int
|
|
walk = func(list []Symbol, parent int) []int {
|
|
var ids []int
|
|
for i := range list {
|
|
id := len(s.nodes)
|
|
s.nodes = append(s.nodes, symbolNode{sym: &list[i], parent: parent})
|
|
s.nodes[id].kids = walk(list[i].Children, id)
|
|
ids = append(ids, id)
|
|
}
|
|
return ids
|
|
}
|
|
s.roots = walk(symbols, -1)
|
|
}
|
|
|
|
// symbolFilter marks matches and ancestors; nil keeps all.
|
|
func symbolFilter(s *SymbolOutlineState, q string) []bool {
|
|
if q == "" {
|
|
return nil
|
|
}
|
|
keep := make([]bool, len(s.nodes))
|
|
for i, n := range s.nodes {
|
|
if strings.Contains(strings.ToLower(n.sym.Name), q) {
|
|
for j := i; j >= 0 && !keep[j]; j = s.nodes[j].parent {
|
|
keep[j] = true
|
|
}
|
|
}
|
|
}
|
|
return keep
|
|
}
|
|
|
|
func symbolKept(ids []int, keep []bool) []int {
|
|
if keep == nil {
|
|
return ids
|
|
}
|
|
var out []int
|
|
for _, id := range ids {
|
|
if keep[id] {
|
|
out = append(out, id)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// symbolFollow opens and chooses the innermost symbol spanning line.
|
|
func symbolFollow(s *SymbolOutlineState, line int, keep []bool) {
|
|
best := -1
|
|
for i, n := range s.nodes {
|
|
if keep != nil && !keep[i] {
|
|
continue
|
|
}
|
|
if n.sym.Line <= line && line <= max(n.sym.EndLine, n.sym.Line) {
|
|
best = i
|
|
}
|
|
}
|
|
if best < 0 {
|
|
return
|
|
}
|
|
for p := s.nodes[best].parent; p >= 0; p = s.nodes[p].parent {
|
|
s.outline.Open.Add(p)
|
|
}
|
|
s.cursor = symbolRow(s, best, keep)
|
|
slog.Debug("mujica: symbol_outline follow", "line", line)
|
|
}
|
|
|
|
// symbolRow returns the row the shown node id is at.
|
|
func symbolRow(s *SymbolOutlineState, id int, keep []bool) int {
|
|
row := 0
|
|
var walk func(ids []int) bool
|
|
walk = func(ids []int) bool {
|
|
for _, n := range symbolKept(ids, keep) {
|
|
if n == id {
|
|
return true
|
|
}
|
|
row++
|
|
if s.outline.Open.Has(n) && walk(s.nodes[n].kids) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
if walk(s.roots) {
|
|
return row
|
|
}
|
|
return -1
|
|
}
|