Files

780 lines
18 KiB
Go

package scripteditor
import (
"strconv"
"unicode/utf8"
"aego/core/mathx"
"aego/editor/core/code"
"aego/editor/core/code/highlight"
"aego/platform/input"
"aego/render/text"
"aego/script/diag"
"aego/script/langsvc"
"aego/ui/core"
)
const (
tabWidth = code.TabWidth
revealMargin = 3
viewKeep = 512
viewSlack = 64
hintPad = 8
)
type lineHint struct {
at int
label string
w float32
}
type lineView struct {
valid bool
hash uint64
in highlight.State
font *text.Font
semVer uint64
hintVer uint64
indent int
disp []rune
idx []int
base []highlight.Span
levels []int
spans []highlight.Span
texts []string
xs []float32
hints []lineHint
}
func (v *lineView) x(f *text.Font, i int) float32 {
x := f.XRunes(v.disp, i)
for _, h := range v.hints {
if h.at <= i {
x += h.w
}
}
return x
}
func (v *lineView) index(f *text.Font, x float32) int {
for i, r := range v.disp {
if x < v.x(f, i)+f.Advance(r)/2 {
return i
}
}
return len(v.disp)
}
func semKind(k langsvc.TokenKind) highlight.Kind {
switch k {
case langsvc.TokNamespace:
return highlight.Namespace
case langsvc.TokType, langsvc.TokStruct, langsvc.TokEnum:
return highlight.Type
case langsvc.TokScript:
return highlight.Script
case langsvc.TokEnumMember:
return highlight.EnumMember
case langsvc.TokFunction:
return highlight.Function
case langsvc.TokMethod:
return highlight.Method
case langsvc.TokParameter:
return highlight.Param
case langsvc.TokField:
return highlight.Field
case langsvc.TokProperty:
return highlight.Property
case langsvc.TokConstant:
return highlight.Constant
case langsvc.TokBuiltin:
return highlight.Builtin
case langsvc.TokAttribute:
return highlight.Attribute
case langsvc.TokSignal:
return highlight.Signal
case langsvc.TokCoroutine:
return highlight.Coroutine
}
return highlight.Variable
}
func palette(th *core.Theme) [highlight.KindCount]mathx.Color {
return highlight.Palette(&th.Code)
}
func hashBytes(b []byte) uint64 {
h := uint64(14695981039346656037)
for _, c := range b {
h ^= uint64(c)
h *= 1099511628211
}
return h
}
func expand(raw []byte, disp []rune, idx []int) ([]rune, []int) {
for i := 0; i < len(raw); {
r, size := utf8.DecodeRune(raw[i:])
for k := 0; k < size; k++ {
idx = append(idx, len(disp))
}
if r == '\t' {
for n := tabWidth - len(disp)%tabWidth; n > 0; n-- {
disp = append(disp, ' ')
}
} else {
disp = append(disp, r)
}
i += size
}
return disp, append(idx, len(disp))
}
func indentOf(disp []rune) int {
n := 0
for n < len(disp) && disp[n] == ' ' {
n++
}
if n == len(disp) {
return 0
}
return n
}
func byteAt(idx []int, d int) int {
for b, v := range idx {
if v >= d {
return b
}
}
return len(idx) - 1
}
func (p *Panel) view(l int, font *text.Font) *lineView {
c := p.doc.Code
p.line = c.LineBytes(l, p.line[:0])
h := hashBytes(p.line)
in := c.LineState(l)
v := p.views[l]
if v == nil {
v = &lineView{}
p.views[l] = v
}
if !v.valid || v.hash != h || v.in != in {
v.disp, v.idx = expand(p.line, v.disp[:0], v.idx[:0])
v.base, _ = highlight.Line(p.line, in, v.base[:0])
v.levels = highlight.BracketLevels(v.base, p.line, in.Depth(), v.levels[:0])
v.indent = indentOf(v.disp)
v.hash, v.in, v.valid, v.font = h, in, true, nil
}
if v.semVer != p.semVer || v.font != font || v.hintVer != p.inlayVer {
v.hints = v.hints[:0]
for _, ih := range p.inlays[l] {
if ih.off < 0 || ih.off >= len(v.idx) {
continue
}
v.hints = append(v.hints, lineHint{at: v.idx[ih.off], label: ih.label, w: font.Measure(ih.label).X + hintPad})
}
v.spans = append(v.spans[:0], v.base...)
highlight.Overlay(v.spans, c.LineStart(l), p.sem)
v.texts, v.xs = v.texts[:0], v.xs[:0]
for _, s := range v.spans {
da, db := v.idx[s.Start], v.idx[s.End]
v.texts = append(v.texts, string(v.disp[da:db]))
v.xs = append(v.xs, v.x(font, da))
}
v.semVer, v.font, v.hintVer = p.semVer, font, p.inlayVer
}
return v
}
func (p *Panel) prune(first, last int) {
if len(p.views) <= viewKeep {
return
}
for l := range p.views {
if l < first-viewSlack || l > last+viewSlack {
delete(p.views, l)
}
}
}
func (p *Panel) caretX(font *text.Font) (int, float32) {
c := p.doc.Code
l := c.Line(c.Cursor())
v := p.view(l, font)
return l, v.x(font, v.idx[c.Cursor()-c.LineStart(l)])
}
func (p *Panel) revealCaret(font *text.Font, area, txt mathx.Rect) {
l, x := p.caretX(font)
lh := p.lineH
y := float32(l) * lh
margin := min(lh*revealMargin, max(0, (area.H-lh)/2))
switch {
case y < p.scrollY+margin:
p.scrollY = y - margin
case y+lh > p.scrollY+area.H-margin:
p.scrollY = y + lh - area.H + margin
}
switch {
case x < p.scrollX:
p.scrollX = max(0, x-40)
case x > p.scrollX+txt.W-40:
p.scrollX = x - txt.W + 80
}
}
func (p *Panel) hit(ui *core.Context, txt mathx.Rect) int {
c := p.doc.Code
m := ui.Mouse()
l := min(max(int((m.Y-txt.Y+p.scrollY)/p.lineH), 0), c.Lines()-1)
font := ui.Font()
v := p.view(l, font)
d := v.index(font, m.X-(txt.X+p.pad-p.scrollX))
return c.LineStart(l) + byteAt(v.idx, d)
}
func (p *Panel) trackHover(ui *core.Context, txt mathx.Rect) {
if p.dragging || p.comp.open {
return
}
off := p.hit(ui, txt)
if off != p.hover.off {
p.hover = hoverState{off: off, since: ui.Time}
return
}
if p.hover.done || ui.Time-p.hover.since < hoverDelay {
return
}
p.hover.done = true
if d, ok := p.diagAt(off); ok {
p.hover.dg, p.hover.hasDiag = d, true
}
file, ok := p.file()
if !ok || p.snap == nil {
return
}
if old, ok := p.doc.Code.Unmap(p.snapRev, off); ok {
p.hover.info, p.hover.ok = p.host.lang.HoverIn(p.snap, file, uint32(old))
if p.hover.ok {
p.hover.builtin = p.builtinAt(file, old)
}
}
}
func (p *Panel) mouse(ui *core.Context, area, txt mathx.Rect) {
c := p.doc.Code
for i := range p.lens {
if p.lens[i].rect.W > 0 && ui.Clicked(core.IDSeq(p.fid, lensBase+i), p.lens[i].rect) {
p.host.selectAttached(p.lens[i].full)
}
}
over := ui.MouseOver(p.fid, area)
if over {
h := ui.ScrollX(40)
v := ui.ScrollY(p.lineH * 3)
if ui.Mods().Has(input.ModShift) {
h, v = h-v, 0
}
if h != 0 || v != 0 {
p.scrollX += h
p.scrollY -= v
p.hover = hoverState{off: -1}
}
}
if ui.Pressed(p.fid, txt) {
ui.SetFocus(p.fid)
off := p.hit(ui, txt)
shift := ui.Mods().Has(input.ModShift)
p.closePopups()
p.blinkAt = ui.Time
switch {
case ui.Primary() && !shift:
c.SetCursor(off, false)
p.definition(ui)
case ui.DoubleClicked(p.fid):
c.SelectWord(off)
default:
c.SetCursor(off, shift)
p.dragging = true
}
}
if p.dragging {
if ui.MouseDown(input.MouseLeft) {
c.SetCursor(p.hit(ui, txt), true)
} else {
p.dragging = false
}
}
if !over || !txt.Contains(ui.Mouse()) {
p.hover = hoverState{off: -1}
return
}
for i := range p.lens {
if p.lens[i].rect.Contains(ui.Mouse()) {
ui.SetCursorShape(core.CursorPointer)
return
}
}
ui.SetCursorShape(core.CursorText)
if ui.Primary() {
ui.SetCursorShape(core.CursorPointer)
}
p.trackHover(ui, txt)
}
func (p *Panel) keys(ui *core.Context) {
c := p.doc.Code
ro := p.doc.ReadOnly
mods := ui.Mods()
shift := mods.Has(input.ModShift)
alt := mods.Has(input.ModAlt)
word := mods&ui.WordMod() != 0
primary := ui.Primary()
before := c.Rev()
moved := false
edit := func(fn func()) {
if !ro {
fn()
}
}
if p.comp.open && p.compKeys(ui) {
p.blinkAt = ui.Time
return
}
if p.peek.open && (ui.KeyPressed(input.KeyEnter) || ui.KeyPressed(input.KeyKPEnter)) {
p.openPeek()
return
}
if ui.KeyPressed(input.KeyEscape) {
switch {
case p.peek.open:
p.peek.open = false
case p.sig.open:
p.sig.open = false
case p.comp.open || p.note != "":
p.closePopups()
default:
p.find.open = false
}
}
if p.command(ui, primary, shift, alt) {
p.reveal = true
p.blinkAt = ui.Time
p.hover = hoverState{off: -1}
return
}
if t := ui.TextInput(); len(t) > 0 && !primary && !ro {
s := string(t)
if !p.autoPair(c, s) {
c.Type(s)
}
p.typed(ui, s)
}
move := func(k input.Scancode, d code.Dir, w bool) {
if ui.KeyRepeated(k) {
c.Move(d, w, shift)
moved = true
}
}
move(input.KeyLeft, code.Left, word)
move(input.KeyRight, code.Right, word)
move(input.KeyUp, code.Up, false)
move(input.KeyDown, code.Down, false)
if ui.KeyPressed(input.KeyHome) {
if primary {
c.Move(code.DocStart, false, shift)
} else {
c.Move(code.LineStart, false, shift)
}
moved = true
}
if ui.KeyPressed(input.KeyEnd) {
if primary {
c.Move(code.DocEnd, false, shift)
} else {
c.Move(code.LineEnd, false, shift)
}
moved = true
}
if ui.KeyRepeated(input.KeyPageUp) {
c.MoveLines(-p.viewLines, shift)
moved = true
}
if ui.KeyRepeated(input.KeyPageDown) {
c.MoveLines(p.viewLines, shift)
moved = true
}
if ui.KeyRepeated(input.KeyBackspace) {
edit(func() {
if word || !p.deletePair(c) {
c.Delete(code.Left, word)
}
p.refilter()
})
}
if ui.KeyRepeated(input.KeyDelete) {
edit(func() { c.Delete(code.Right, word) })
}
if ui.KeyRepeated(input.KeyEnter) || ui.KeyRepeated(input.KeyKPEnter) {
edit(c.Newline)
}
if ui.KeyRepeated(input.KeyTab) {
edit(func() {
lo, hi := c.Sel.Range()
switch {
case shift:
c.Unindent()
case c.Line(lo) != c.Line(hi):
c.Indent()
default:
c.Type("\t")
}
})
}
if primary {
switch {
case ui.KeyPressed(input.KeyA):
c.SelectAll()
case ui.KeyPressed(input.KeyC):
if s := c.Selected(); s != "" && ui.Clipboard != nil {
ui.Clipboard.SetText(s)
}
case ui.KeyPressed(input.KeyX):
if s := c.Selected(); s != "" && ui.Clipboard != nil {
ui.Clipboard.SetText(s)
edit(func() { c.Cut() })
}
case ui.KeyPressed(input.KeyV):
if ui.Clipboard != nil {
edit(func() { c.Paste(ui.Clipboard.Text()) })
}
case ui.KeyRepeated(input.KeyZ):
edit(func() {
if shift {
c.Redo()
} else {
c.Undo()
}
})
case ui.KeyRepeated(input.KeyY):
edit(func() { c.Redo() })
case ui.KeyPressed(input.KeyS):
edit(func() { p.save() })
case ui.KeyPressed(input.KeyF):
p.openFind(false)
case ui.KeyPressed(input.KeyH):
p.openFind(!ro)
case ui.KeyPressed(input.KeySlash):
edit(c.ToggleComment)
case ui.KeyPressed(input.KeyF4):
p.host.requestClose(p)
case ui.KeyPressed(input.KeySpace):
edit(func() { p.requestCompletion(ui) })
case !shift && ui.KeyPressed(input.KeyP):
if !p.querySignature() {
p.flash(ui.Time, "No call at the cursor")
}
case ui.KeyPressed(input.KeyQ):
p.quickDoc(ui)
}
}
if moved || c.Rev() != before {
p.reveal = true
p.blinkAt = ui.Time
p.hover = hoverState{off: -1}
if p.noteAt <= ui.Time+noteTime {
p.note = ""
} else {
p.note, p.noteAt = "", 0
}
if moved && p.comp.open {
p.refilter()
}
}
}
func wave(ui *core.Context, xa, xb, base float32, col mathx.Color) {
if xb-xa < 6 {
xb = xa + 6
}
for x := xa; x < xb; x += 4 {
ui.Rect(mathx.Rect{X: x, Y: base, W: 2, H: 1.5}, col)
ui.Rect(mathx.Rect{X: x + 2, Y: base + 1, W: 2, H: 1.5}, col)
}
}
func (p *Panel) underlines(ui *core.Context, font *text.Font, v *lineView, x0, y float32, ls, le int) {
th := &ui.Theme
for _, d := range p.diags {
s, e := int(d.Primary.Start), int(d.Primary.End)
if e <= s {
e = s + 1
}
if e < ls || s > le {
continue
}
a, b := max(s, ls)-ls, min(e, le)-ls
col := th.Error
if d.Severity == diag.Warning {
col = th.Warn
}
wave(ui, x0+v.x(font, v.idx[a]), x0+v.x(font, v.idx[b]), y+p.lineH-3, col)
}
}
func (p *Panel) drawBrackets(ui *core.Context, font *text.Font, v *lineView, x0, y float32, ls, le int) {
if p.brState == code.BracketNone {
return
}
ct := &ui.Theme.Code
col, at, n := ct.BracketMatch, [2]int{p.brA, p.brB}, 2
if p.brState == code.BracketUnmatched {
col, n = ct.BracketMismatch, 1
}
for _, a := range at[:n] {
if a < ls || a >= le {
continue
}
xa := x0 + v.x(font, v.idx[a-ls])
xb := x0 + v.x(font, v.idx[a-ls+1])
ui.List().RectOutline(mathx.Rect{X: xa, Y: y + 1, W: xb - xa, H: p.lineH - 2}, col, 1)
}
}
func (p *Panel) drawSpans(ui *core.Context, font *text.Font, v *lineView, x0, y, ty float32, ls int, pal *[highlight.KindCount]mathx.Color) {
ct := &ui.Theme.Code
c := p.doc.Code
for i, s := range v.spans {
t := v.texts[i]
if t == "" {
continue
}
col := pal[s.Kind]
if s.Kind == highlight.Bracket {
switch {
case c.Unmatched(ls + s.Start):
col = ct.BracketMismatch
wave(ui, x0+v.xs[i], x0+v.x(font, v.idx[s.End]), y+p.lineH-3, col)
case p.host.Rainbow && v.levels[i] >= 0:
col = ct.Rainbow[v.levels[i]%len(ct.Rainbow)]
}
}
ui.Text(t, mathx.Vec2{X: x0 + v.xs[i], Y: ty}, col)
}
}
func (p *Panel) drawText(ui *core.Context, area mathx.Rect) {
th := &ui.Theme
ct := &th.Code
ui.PushFont(ui.MonoFont())
defer ui.PopFont()
font := ui.Font()
if font == nil || area.H <= 0 {
return
}
c := p.doc.Code
pal := palette(th)
p.lineH = ui.LineHeight() + 3
lh := p.lineH
count := c.Lines()
charW := font.Advance('0')
markerW := ct.Gutter.MarkerW * charW
numEnd := area.X + markerW + float32(len(strconv.Itoa(count))+1)*charW
gw := numEnd - area.X + ct.Gutter.NumberPad*charW
p.pad = ct.Gutter.CodePad * charW
mw := float32(0)
if p.host.Minimap && !p.inZen() && area.W-gw > mapMinW {
mw = mapWidth
}
txt := mathx.Rect{X: area.X + gw, Y: area.Y, W: max(0, area.W-gw-mw), H: area.H}
p.viewLines = max(1, int(area.H/lh)-1)
p.mouse(ui, area, txt)
focused := ui.Focus() == p.fid
if focused {
p.keys(ui)
}
p.remap()
p.updateMarks()
p.updateInlays()
p.refreshSignature()
if p.reveal {
p.revealCaret(font, area, txt)
p.reveal = false
}
p.scrollY = mathx.Clamp(p.scrollY, 0, max(0, float32(count)*lh-area.H+lh))
p.scrollX = max(0, p.scrollX)
p.occurrences(ui.Time)
p.brackets()
p.updateBlock()
p.updateLens()
for i := range p.lens {
p.lens[i].rect = mathx.Rect{}
}
ui.Rect(area, ct.Background)
ui.Rect(mathx.Rect{X: area.X, Y: area.Y, W: gw, H: area.H}, ct.GutterBg)
ui.PushClip(area)
first := int(p.scrollY / lh)
last := min(count, first+int(area.H/lh)+2)
p.buildGuides(first, last)
lo, hi := c.Sel.Range()
caretLine := c.Line(c.Cursor())
x0 := txt.X + p.pad - p.scrollX
spaceW := font.Advance(' ')
if ct.GuideColumn > 0 {
gx := x0 + float32(ct.GuideColumn)*spaceW
if gx >= txt.X && gx < txt.X+txt.W {
ui.Rect(mathx.Rect{X: gx, Y: area.Y, W: 1, H: area.H}, ct.Guide)
}
}
for l := first; l < last; l++ {
v := p.view(l, font)
y := area.Y + float32(l)*lh - p.scrollY
ls, le := c.LineStart(l), c.LineEnd(l)
ty := y + (lh-ui.LineHeight())/2
ui.PushClip(txt)
if p.inBlock(l) {
ui.Rect(mathx.Rect{X: txt.X, Y: y, W: txt.W, H: lh}, ct.CurrentBlock)
}
if l == caretLine && focused {
ui.Rect(mathx.Rect{X: txt.X, Y: y, W: txt.W, H: lh}, ct.CurrentLine)
}
if l == p.flashLine {
if t := ui.Time - p.flashAt; t >= 0 && t < flashTime {
col := ct.Occurrence
col.A = float32(1 - t/flashTime)
ui.Rect(mathx.Rect{X: txt.X, Y: y, W: txt.W, H: lh}, col)
}
}
p.drawGuides(ui, v, x0, y, spaceW, l)
p.drawOcc(ui, font, v, x0, y, ls, le)
if hi > lo && hi >= ls && lo <= le {
a, b := max(lo, ls)-ls, min(hi, le)-ls
xa := x0 + v.x(font, v.idx[a])
xb := x0 + v.x(font, v.idx[b])
if hi > le {
xb += spaceW
}
ui.Rect(mathx.Rect{X: xa, Y: y, W: xb - xa, H: lh}, th.Selection)
}
for _, h := range v.hints {
hx := x0 + v.x(font, h.at) - h.w
ui.Rect(mathx.Rect{X: hx + 1, Y: y + 2, W: h.w - 3, H: lh - 4}, th.Badge)
ui.Text(h.label, mathx.Vec2{X: hx + hintPad/2, Y: ty}, th.Unit)
}
p.drawSpans(ui, font, v, x0, y, ty, ls, &pal)
p.drawBrackets(ui, font, v, x0, y, ls, le)
for i := range p.lens {
if p.lens[i].line != l {
continue
}
label := lensLabel(p.host.counts[p.lens[i].full])
lx := x0 + v.x(font, len(v.disp)) + 3*spaceW
lr := mathx.Rect{X: lx, Y: y, W: ui.TextSize(label).X, H: lh}
p.lens[i].rect = lr
col := ct.Comment
if lr.Contains(ui.Mouse()) {
col = th.Accent
}
ui.Text(label, mathx.Vec2{X: lx, Y: ty}, col)
}
p.underlines(ui, font, v, x0, y, ls, le)
ui.PopClip()
num := strconv.Itoa(l + 1)
nc := ct.LineNo
if l == caretLine {
nc = ct.LineNoActive
}
ui.Text(num, mathx.Vec2{X: numEnd - ui.TextSize(num).X, Y: ty}, nc)
if sev, ok := p.marks[l]; ok {
col := th.Error
if sev == diag.Warning {
col = th.Warn
}
ui.Rect(mathx.Rect{X: area.X + max(0, (markerW-3)/2), Y: y + 3, W: 3, H: lh - 6}, col)
}
}
if mw > 0 {
p.mini.update(p, th)
p.mini.draw(p, ui, mathx.Rect{X: area.X + area.W - mw, Y: area.Y, W: mw, H: area.H}, first)
}
l, cx := p.caretX(font)
cy := area.Y + float32(l)*lh - p.scrollY
p.caret = mathx.Vec2{X: x0 + cx, Y: cy + lh}
if focused {
if int((ui.Time-p.blinkAt)*2)%2 == 0 {
ui.Rect(mathx.Rect{X: x0 + cx, Y: cy + 2, W: 2, H: lh - 4}, th.Text)
}
ui.WantTextInput(mathx.Rect{X: x0 + cx, Y: cy, W: 1, H: lh}, 0)
}
ui.PopClip()
p.prune(first, last)
}
var openPairs = map[byte]byte{'(': ')', '[': ']', '{': '}', '"': '"'}
func isCloser(b byte) bool {
return b == ')' || b == ']' || b == '}'
}
// autoPair pairs, types over or wraps brackets and quotes; false means insert s normally.
func (p *Panel) autoPair(c *code.Doc, s string) bool {
if len(s) != 1 {
return false
}
ch := s[0]
lo, hi := c.Sel.Range()
var prev, next byte
if lo > 0 {
if b := c.Slice(lo-1, 1, nil); len(b) == 1 {
prev = b[0]
}
}
if hi < c.Len() {
if b := c.Slice(hi, 1, nil); len(b) == 1 {
next = b[0]
}
}
if lo == hi && next == ch && (isCloser(ch) || ch == '"') {
c.SetCursor(hi+1, false)
return true
}
cl, ok := openPairs[ch]
if !ok {
return false
}
if hi > lo {
sel := string(c.Slice(lo, hi-lo, nil))
c.Replace(lo, hi-lo, string(ch)+sel+string(cl))
c.Select(lo+1, hi+1)
return true
}
if ch == '"' {
if identByte(prev) || identByte(next) || next == ch {
return false
}
}
if identByte(next) {
return false
}
c.Replace(lo, 0, string(ch)+string(cl))
c.SetCursor(lo+1, false)
return true
}
// deletePair removes both halves of an empty pair on Backspace.
func (p *Panel) deletePair(c *code.Doc) bool {
lo, hi := c.Sel.Range()
if lo != hi || lo == 0 || hi >= c.Len() {
return false
}
prev := c.Slice(lo-1, 1, nil)
next := c.Slice(hi, 1, nil)
if len(prev) == 1 && len(next) == 1 && openPairs[prev[0]] == next[0] {
c.Replace(lo-1, 2, "")
return true
}
return false
}