343 lines
7.9 KiB
Go
343 lines
7.9 KiB
Go
package scripteditor
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"aego/core/mathx"
|
|
"aego/platform/input"
|
|
"aego/script/diag"
|
|
"aego/script/langsvc"
|
|
"aego/ui/core"
|
|
"aego/ui/draw"
|
|
)
|
|
|
|
const compRows = 10
|
|
|
|
func identByte(b byte) bool {
|
|
return b == '_' || (b >= '0' && b <= '9') || (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
|
|
}
|
|
|
|
func (p *Panel) wordBefore(off int) string {
|
|
c := p.doc.Code
|
|
ls := c.LineStart(c.Line(off))
|
|
b := c.Slice(ls, off-ls, nil)
|
|
i := len(b)
|
|
for i > 0 && identByte(b[i-1]) {
|
|
i--
|
|
}
|
|
return string(b[i:])
|
|
}
|
|
|
|
func tabStop(s string, i int) (int, string, int, bool) {
|
|
if s[i] != '$' || i+1 >= len(s) {
|
|
return 0, "", 0, false
|
|
}
|
|
j := i + 1
|
|
brace := s[j] == '{'
|
|
if brace {
|
|
j++
|
|
}
|
|
k := j
|
|
for k < len(s) && s[k] >= '0' && s[k] <= '9' {
|
|
k++
|
|
}
|
|
if k == j {
|
|
return 0, "", 0, false
|
|
}
|
|
n, _ := strconv.Atoi(s[j:k])
|
|
switch {
|
|
case !brace:
|
|
return n, "", k, true
|
|
case k < len(s) && s[k] == '}':
|
|
return n, "", k + 1, true
|
|
case k < len(s) && s[k] == ':':
|
|
if e := strings.IndexByte(s[k:], '}'); e > 0 {
|
|
return n, s[k+1 : k+e], k + e + 1, true
|
|
}
|
|
}
|
|
return 0, "", 0, false
|
|
}
|
|
|
|
func expandSnippet(s string) (string, int, int) {
|
|
var b strings.Builder
|
|
best, lo, hi, final := -1, -1, -1, -1
|
|
for i := 0; i < len(s); {
|
|
if n, text, next, ok := tabStop(s, i); ok {
|
|
at := b.Len()
|
|
b.WriteString(text)
|
|
switch {
|
|
case n == 0:
|
|
final = at
|
|
case best < 0 || n < best:
|
|
best, lo, hi = n, at, b.Len()
|
|
}
|
|
i = next
|
|
continue
|
|
}
|
|
b.WriteByte(s[i])
|
|
i++
|
|
}
|
|
out := b.String()
|
|
switch {
|
|
case lo >= 0:
|
|
return out, lo, hi
|
|
case final >= 0:
|
|
return out, final, final
|
|
}
|
|
return out, len(out), len(out)
|
|
}
|
|
|
|
func (p *Panel) typed(ui *core.Context, s string) {
|
|
last := s[len(s)-1]
|
|
switch {
|
|
case last == '(' || last == ',':
|
|
p.comp.open = false
|
|
p.querySignature()
|
|
if last == '(' && p.wordBefore(p.doc.Code.Cursor()-1) == "Connect" {
|
|
p.requestCompletion(ui)
|
|
}
|
|
case last == ')':
|
|
p.comp.open = false
|
|
if p.sig.open {
|
|
p.querySignature()
|
|
}
|
|
case last == '.' || last == '@':
|
|
p.requestCompletion(ui)
|
|
case last == '<':
|
|
switch p.wordBefore(p.doc.Code.Cursor() - 1) {
|
|
case "Get", "Has", "GetScript":
|
|
p.requestCompletion(ui)
|
|
}
|
|
case last == ' ':
|
|
p.comp.open = false
|
|
switch p.wordBefore(p.doc.Code.Cursor() - 1) {
|
|
case "start", "yield":
|
|
p.requestCompletion(ui)
|
|
}
|
|
case identByte(last):
|
|
if p.comp.open {
|
|
p.refilter()
|
|
} else if len(p.wordBefore(p.doc.Code.Cursor())) >= 2 {
|
|
p.requestCompletion(ui)
|
|
}
|
|
default:
|
|
p.comp.open = false
|
|
}
|
|
}
|
|
|
|
func (p *Panel) requestCompletion(ui *core.Context) {
|
|
gen := p.push()
|
|
file, ok := p.file()
|
|
if !ok {
|
|
return
|
|
}
|
|
c := p.doc.Code
|
|
p.host.lang.Complete(gen, file, uint32(c.Cursor()), func(cm langsvc.Completion) {
|
|
start := int(cm.Prefix.Start)
|
|
if start > p.doc.Code.Cursor() || len(cm.Items) == 0 {
|
|
return
|
|
}
|
|
p.comp = compState{open: true, start: start, items: cm.Items}
|
|
p.refilter()
|
|
})
|
|
}
|
|
|
|
func matches(label, prefix string) bool {
|
|
return len(prefix) <= len(label) && strings.EqualFold(label[:len(prefix)], prefix)
|
|
}
|
|
|
|
func (p *Panel) refilter() {
|
|
if !p.comp.open {
|
|
return
|
|
}
|
|
c := p.doc.Code
|
|
cur := c.Cursor()
|
|
if cur < p.comp.start || c.Line(cur) != c.Line(p.comp.start) {
|
|
p.comp.open = false
|
|
return
|
|
}
|
|
prefix := string(c.Slice(p.comp.start, cur-p.comp.start, nil))
|
|
if strings.ContainsFunc(prefix, func(r rune) bool { return r != '_' && !unicode.IsLetter(r) && !unicode.IsDigit(r) }) {
|
|
p.comp.open = false
|
|
return
|
|
}
|
|
p.comp.view = p.comp.view[:0]
|
|
for i, it := range p.comp.items {
|
|
if matches(it.Label, prefix) {
|
|
p.comp.view = append(p.comp.view, i)
|
|
}
|
|
}
|
|
if len(p.comp.view) == 0 {
|
|
p.comp.open = false
|
|
return
|
|
}
|
|
p.comp.sel = min(max(p.comp.sel, 0), len(p.comp.view)-1)
|
|
}
|
|
|
|
func (p *Panel) compKeys(ui *core.Context) bool {
|
|
n := len(p.comp.view)
|
|
switch {
|
|
case ui.KeyRepeated(input.KeyDown):
|
|
p.comp.sel = (p.comp.sel + 1) % n
|
|
case ui.KeyRepeated(input.KeyUp):
|
|
p.comp.sel = (p.comp.sel + n - 1) % n
|
|
case ui.KeyPressed(input.KeyEnter), ui.KeyPressed(input.KeyKPEnter), ui.KeyPressed(input.KeyTab):
|
|
p.accept(p.comp.sel)
|
|
case ui.KeyPressed(input.KeyEscape):
|
|
p.comp.open = false
|
|
default:
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (p *Panel) accept(i int) {
|
|
if i < 0 || i >= len(p.comp.view) {
|
|
return
|
|
}
|
|
it := p.comp.items[p.comp.view[i]]
|
|
c := p.doc.Code
|
|
ins, lo, hi := expandSnippet(it.Insert)
|
|
start := p.comp.start
|
|
c.Replace(start, c.Cursor()-start, ins)
|
|
if lo == hi {
|
|
c.SetCursor(start+lo, false)
|
|
} else {
|
|
c.Select(start+lo, start+hi)
|
|
}
|
|
p.comp.open = false
|
|
p.reveal = true
|
|
if strings.HasSuffix(ins, "(") {
|
|
p.querySignature()
|
|
}
|
|
}
|
|
|
|
func (p *Panel) jump(l langsvc.Location) {
|
|
if l.Path == p.doc.Path && p.snap != nil {
|
|
if s, _, ok := p.doc.Code.MapSpan(p.snapRev, int(l.Span.Start), int(l.Span.End)); ok {
|
|
p.host.OpenAt(l.Path, s)
|
|
return
|
|
}
|
|
}
|
|
p.host.OpenAt(l.Path, int(l.Span.Start))
|
|
}
|
|
|
|
func box(ui *core.Context, at mathx.Vec2, lines []string, first mathx.Color) {
|
|
th := &ui.Theme
|
|
w, lh := float32(0), ui.LineHeight()+2
|
|
for _, l := range lines {
|
|
w = max(w, ui.TextSize(l).X)
|
|
}
|
|
r := mathx.Rect{X: at.X, Y: at.Y, W: w + 2*th.Padding, H: float32(len(lines))*lh + th.Padding}
|
|
sz := ui.Size()
|
|
r.X = max(0, min(r.X, sz.X-r.W))
|
|
r.Y = max(0, min(r.Y, sz.Y-r.H))
|
|
l := ui.List()
|
|
l.PushLayer(draw.LayerTop)
|
|
l.Rect(r, th.PanelAlt)
|
|
l.RectOutline(r, th.Border, 1)
|
|
for i, s := range lines {
|
|
col := th.TextDim
|
|
if i == 0 {
|
|
col = first
|
|
}
|
|
l.Text(s, mathx.Vec2{X: r.X + th.Padding, Y: r.Y + th.Padding/2 + float32(i)*lh}, col)
|
|
}
|
|
l.PopLayer()
|
|
}
|
|
|
|
func (p *Panel) hoverLines(th *core.Theme) ([]string, mathx.Color) {
|
|
var lines []string
|
|
first := th.Text
|
|
if p.hover.hasDiag {
|
|
d := p.hover.dg
|
|
first = th.Error
|
|
if d.Severity == diag.Warning {
|
|
first = th.Warn
|
|
}
|
|
lines = append(lines, d.Code+" "+d.Message)
|
|
for _, n := range d.Notes {
|
|
lines = append(lines, "note: "+n.Message+p.noteWhere(n))
|
|
}
|
|
if h, ok := diag.Explain(d.Code); ok {
|
|
lines = append(lines, h.What, "Fix: "+h.Fix)
|
|
}
|
|
if p.hover.ok {
|
|
lines = append(lines, "")
|
|
}
|
|
}
|
|
if p.hover.ok {
|
|
lines = append(lines, p.hover.info.Signature)
|
|
if p.hover.builtin {
|
|
lines = append(lines, "engine builtin")
|
|
}
|
|
if p.hover.info.Doc != "" {
|
|
lines = append(lines, strings.Split(p.hover.info.Doc, "\n")...)
|
|
}
|
|
}
|
|
return lines, first
|
|
}
|
|
|
|
func (p *Panel) drawPopups(ui *core.Context) {
|
|
th := &ui.Theme
|
|
if p.comp.open {
|
|
p.drawCompletion(ui)
|
|
}
|
|
p.drawSignature(ui)
|
|
p.drawPeek(ui)
|
|
p.drawOutline(ui)
|
|
if p.hover.done && (p.hover.ok || p.hover.hasDiag) && !p.comp.open && !p.peek.open {
|
|
lines, first := p.hoverLines(th)
|
|
m := ui.Mouse()
|
|
box(ui, mathx.Vec2{X: m.X + 12, Y: m.Y + 18}, lines, first)
|
|
}
|
|
if p.note != "" {
|
|
if ui.Time-p.noteAt > noteTime {
|
|
p.note = ""
|
|
} else {
|
|
box(ui, p.caret, strings.Split(p.note, "\n"), th.Text)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Panel) drawCompletion(ui *core.Context) {
|
|
th := &ui.Theme
|
|
rh := ui.LineHeight() + 6
|
|
rows := min(len(p.comp.view), compRows)
|
|
r := mathx.Rect{X: p.caret.X, Y: p.caret.Y + 2, W: 420, H: float32(rows)*rh + 4}
|
|
sz := ui.Size()
|
|
r.X = max(0, min(r.X, sz.X-r.W))
|
|
if r.Y+r.H > sz.Y {
|
|
r.Y = p.caret.Y - p.lineH - r.H - 2
|
|
}
|
|
top := min(max(p.comp.sel-rows+1, 0), len(p.comp.view)-rows)
|
|
ui.BeginOverlay(r, draw.LayerTop)
|
|
ui.Rect(r, th.PanelAlt)
|
|
ui.Outline(r, th.Border)
|
|
for i := 0; i < rows; i++ {
|
|
k := top + i
|
|
it := p.comp.items[p.comp.view[k]]
|
|
rr := mathx.Rect{X: r.X + 2, Y: r.Y + 2 + float32(i)*rh, W: r.W - 4, H: rh}
|
|
if ui.Pressed(core.IDSeq(p.fid, 100+i), rr) {
|
|
p.accept(k)
|
|
ui.SetFocus(p.fid)
|
|
}
|
|
if k == p.comp.sel {
|
|
ui.Rect(rr, th.Selection)
|
|
}
|
|
ui.Text(it.Label, mathx.Vec2{X: rr.X + 6, Y: ui.TextY(rr)}, th.Text)
|
|
if it.Detail != "" {
|
|
dx := rr.X + 6 + ui.TextSize(it.Label).X + 16
|
|
ui.PushClip(mathx.Rect{X: dx, Y: rr.Y, W: max(0, rr.X+rr.W-dx-4), H: rr.H})
|
|
ui.Text(it.Detail, mathx.Vec2{X: dx, Y: ui.TextY(rr)}, th.TextDim)
|
|
ui.PopClip()
|
|
}
|
|
}
|
|
ui.EndOverlay()
|
|
if it := p.comp.items[p.comp.view[p.comp.sel]]; it.Doc != "" {
|
|
box(ui, mathx.Vec2{X: r.X, Y: r.Y + r.H + 2}, strings.Split(it.Doc, "\n"), th.TextDim)
|
|
}
|
|
}
|