diff --git a/cmd/micro/initlua.go b/cmd/micro/initlua.go index 2f7b2c62..428548d0 100644 --- a/cmd/micro/initlua.go +++ b/cmd/micro/initlua.go @@ -35,6 +35,8 @@ func LuaImport(pkg string) *lua.LTable { return luaImportMicroConfig() case "micro/util": return luaImportMicroUtil() + case "micro/syntax": + return luaImportMicroSyntax() default: return ulua.Import(pkg) } @@ -82,7 +84,6 @@ func luaImportMicroConfig() *lua.LTable { ulua.L.SetField(pkg, "ReadRuntimeFile", luar.New(ulua.L, config.PluginReadRuntimeFile)) ulua.L.SetField(pkg, "NewRTFiletype", luar.New(ulua.L, config.NewRTFiletype)) ulua.L.SetField(pkg, "RTColorscheme", luar.New(ulua.L, config.RTColorscheme)) - ulua.L.SetField(pkg, "RTSyntax", luar.New(ulua.L, config.RTSyntax)) ulua.L.SetField(pkg, "RTHelp", luar.New(ulua.L, config.RTHelp)) ulua.L.SetField(pkg, "RTPlugin", luar.New(ulua.L, config.RTPlugin)) ulua.L.SetField(pkg, "RegisterCommonOption", luar.New(ulua.L, config.RegisterCommonOptionPlug)) diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index e0c409e1..70f89da3 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -422,6 +422,9 @@ func main() { action.InitCommands() timerChan = make(chan func()) + buffer.MainThread = func(f func()) { + timerChan <- f + } err = config.RunPluginFn("preinit") if err != nil { @@ -451,6 +454,9 @@ func main() { screen.TermMessage(err) } + // Providers registered in plugin init() attach to buffers opened before it. + buffer.RefreshSyntax() + err = config.InitColorscheme() if err != nil { screen.TermMessage(err) diff --git a/cmd/micro/syntax_lua.go b/cmd/micro/syntax_lua.go new file mode 100644 index 00000000..c179c774 --- /dev/null +++ b/cmd/micro/syntax_lua.go @@ -0,0 +1,229 @@ +package main + +import ( + "log" + "regexp" + "strings" + + lua "github.com/yuin/gopher-lua" + luar "layeh.com/gopher-luar" + + "github.com/micro-editor/micro/v2/internal/buffer" + "github.com/micro-editor/micro/v2/internal/config" + ulua "github.com/micro-editor/micro/v2/internal/lua" + "github.com/micro-editor/micro/v2/internal/screen" + "github.com/micro-editor/micro/v2/pkg/syntax" +) + +func luaImportMicroSyntax() *lua.LTable { + L := ulua.L + pkg := L.NewTable() + + L.SetField(pkg, "register", L.NewFunction(func(L *lua.LState) int { + t := L.CheckTable(1) + p, err := providerFromTable(L, t, ulua.Loading) + if err != nil { + screen.TermMessage("syntax.register: " + err.Error()) + return 0 + } + syntax.Register(p) + buffer.RefreshSyntax() + return 0 + })) + + L.SetField(pkg, "unregister", L.NewFunction(func(L *lua.LState) int { + name := L.CheckString(1) + full := namespaced(ulua.Loading, name) + syntax.Unregister(full) + if full != name { + syntax.Unregister(name) + } + buffer.RefreshSyntax() + return 0 + })) + + L.SetField(pkg, "invalidate", L.NewFunction(func(L *lua.LState) int { + b := bufferFromLua(L, 1) + b.InvalidateSyntax() + return 0 + })) + + L.SetField(pkg, "has_color", L.NewFunction(func(L *lua.LState) int { + name := L.CheckString(1) + L.Push(lua.LBool(hasColor(name))) + return 1 + })) + + L.SetField(pkg, "providers", L.NewFunction(func(L *lua.LState) int { + names := syntax.ProviderNames() + t := L.NewTable() + for i, name := range names { + t.RawSetInt(i+1, lua.LString(name)) + } + L.Push(t) + return 1 + })) + + return pkg +} + +func providerFromTable(L *lua.LState, t *lua.LTable, owner string) (*syntax.Provider, error) { + name := tableString(t, "name") + if name == "" { + return nil, errString("missing name") + } + request, ok := t.RawGetString("request").(*lua.LFunction) + if !ok { + return nil, errString("missing request function") + } + + p := &syntax.Provider{ + Name: namespaced(owner, name), + Owner: owner, + Priority: int(tableNumber(t, "priority")), + } + p.Match.FileType = tableString(t, "filetype") + + var err error + if p.Match.FileName, err = compileField(t, "filename"); err != nil { + return nil, err + } + if p.Match.Header, err = compileField(t, "header"); err != nil { + return nil, err + } + if p.Match.Signature, err = compileField(t, "signature"); err != nil { + return nil, err + } + + p.Request = func(req syntax.Request) { + ctx := L.NewTable() + L.SetField(ctx, "buf", luar.New(L, req.Buf)) + L.SetField(ctx, "id", lua.LNumber(req.BufID)) + L.SetField(ctx, "text", lua.LString(req.Text)) + L.SetField(ctx, "path", lua.LString(req.Path)) + L.SetField(ctx, "filetype", lua.LString(req.FileType)) + L.SetField(ctx, "first", lua.LNumber(req.First)) + L.SetField(ctx, "last", lua.LNumber(req.Last)) + L.SetField(ctx, "generation", lua.LNumber(req.Generation)) + L.SetField(ctx, "cause", lua.LString(causeName(req.Cause))) + + reply := L.NewFunction(func(L *lua.LState) int { + req.Reply(parseFlatSpans(L, L.CheckTable(1))) + return 0 + }) + + if err := L.CallByParam(lua.P{Fn: request, NRet: 0, Protect: true}, ctx, reply); err != nil { + if req.Fail != nil { + req.Fail(err) + } + log.Println("syntax provider", p.Name, "failed:", err) + if syntax.ReportError(p.Name) { + screen.TermMessage("syntax: disabling provider " + p.Name + ": " + err.Error()) + buffer.RefreshSyntax() + } + } + } + + return p, nil +} + +type stringError string + +func (e stringError) Error() string { return string(e) } + +func errString(s string) error { return stringError(s) } + +func namespaced(owner, name string) string { + if owner == "" || strings.HasPrefix(name, owner+".") { + return name + } + return owner + "." + name +} + +func tableString(t *lua.LTable, key string) string { + v := t.RawGetString(key) + if v == lua.LNil { + return "" + } + return lua.LVAsString(v) +} + +func tableNumber(t *lua.LTable, key string) float64 { + v := t.RawGetString(key) + if v == lua.LNil { + return 0 + } + return float64(lua.LVAsNumber(v)) +} + +func compileField(t *lua.LTable, key string) (*regexp.Regexp, error) { + expr := tableString(t, key) + if expr == "" { + return nil, nil + } + re, err := regexp.Compile(expr) + if err != nil { + return nil, errString(key + ": " + err.Error()) + } + return re, nil +} + +func parseFlatSpans(L *lua.LState, t *lua.LTable) []syntax.Span { + n := t.Len() + spans := make([]syntax.Span, 0, n/4) + for i := 1; i+3 <= n; i += 4 { + line := int(lua.LVAsNumber(t.RawGetInt(i))) + start := int(lua.LVAsNumber(t.RawGetInt(i + 1))) + end := int(lua.LVAsNumber(t.RawGetInt(i + 2))) + group := lua.LVAsString(t.RawGetInt(i + 3)) + spans = append(spans, syntax.Span{ + Line: line, + StartByte: start, + EndByte: end, + Group: syntax.Intern(group), + }) + } + return spans +} + +func bufferFromLua(L *lua.LState, n int) *buffer.Buffer { + if ud, ok := L.Get(n).(*lua.LUserData); ok { + if b, ok := ud.Value.(*buffer.Buffer); ok { + return b + } + } + L.ArgError(n, "buffer expected") + return nil +} + +func hasColor(name string) bool { + if name == "" { + return false + } + cur := "" + for i, part := range strings.Split(name, ".") { + if i > 0 { + cur += "." + } + cur += part + if _, ok := config.Colorscheme[cur]; ok { + return true + } + } + return false +} + +func causeName(c syntax.Cause) string { + switch c { + case syntax.CauseOpen: + return "open" + case syntax.CauseEdit: + return "edit" + case syntax.CauseManual: + return "manual" + case syntax.CauseReload: + return "reload" + default: + return "unknown" + } +} diff --git a/cmd/micro/syntax_lua_test.go b/cmd/micro/syntax_lua_test.go new file mode 100644 index 00000000..4fba68b8 --- /dev/null +++ b/cmd/micro/syntax_lua_test.go @@ -0,0 +1,63 @@ +package main + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/micro-editor/micro/v2/internal/buffer" + "github.com/micro-editor/micro/v2/pkg/syntax" +) + +func TestLuaSyntaxProviderEndToEnd(t *testing.T) { + found := false + for _, name := range syntax.ProviderNames() { + if name == "gosyntax.go-hardcoded" { + found = true + break + } + } + if !found { + t.Fatalf("demo provider not registered: %v", syntax.ProviderNames()) + } + + path := filepath.Join(t.TempDir(), "demo.go") + content := "package main\n\nfunc main() {\n\tprintln(\"hi\", 42)\n}\n" + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + + work := make(chan func(), 4) + oldMain, oldDebounce := buffer.MainThread, buffer.HighlightDebounce + buffer.MainThread = func(f func()) { work <- f } + buffer.HighlightDebounce = time.Millisecond + defer func() { + buffer.MainThread = oldMain + buffer.HighlightDebounce = oldDebounce + }() + + b, err := buffer.NewBufferFromFile(path, buffer.BTDefault) + if err != nil { + t.Fatal(err) + } + defer b.Close() + + b.InvalidateSyntax() + + deadline := time.Now().Add(2 * time.Second) + for time.Now().Before(deadline) { + select { + case f := <-work: + f() + default: + } + for _, g := range b.Match(3) { + if g != syntax.NoGroup && g.String() == "constant.string" { + return + } + } + time.Sleep(time.Millisecond) + } + t.Fatal("demo provider did not produce a Go string span") +} diff --git a/internal/action/command.go b/internal/action/command.go index cd97c522..95808548 100644 --- a/internal/action/command.go +++ b/internal/action/command.go @@ -330,7 +330,7 @@ func (h *BufPane) ToggleLogCmd(args []string) { } } -// ReloadCmd reloads all files (syntax files, colorschemes, plugins...) +// ReloadCmd reloads all files (colorschemes, plugins, settings...) func (h *BufPane) ReloadCmd(args []string) { reloadRuntime(true) } @@ -582,11 +582,8 @@ func doSetGlobalOptionNative(option string, nativeValue any) error { delete(config.VolatileSettings, option) if option == "colorscheme" { - // LoadSyntaxFiles() config.InitColorscheme() - for _, b := range buffer.OpenBuffers { - b.UpdateRules() - } + screen.Redraw() } else if option == "infobar" || option == "keymenu" || option == "tabalways" { Tabs.Resize() } else if option == "mouse" { diff --git a/internal/action/infocomplete.go b/internal/action/infocomplete.go index ff227d80..137c870c 100644 --- a/internal/action/infocomplete.go +++ b/internal/action/infocomplete.go @@ -8,7 +8,8 @@ import ( "github.com/micro-editor/micro/v2/internal/buffer" "github.com/micro-editor/micro/v2/internal/config" "github.com/micro-editor/micro/v2/internal/util" - "github.com/micro-editor/micro/v2/pkg/highlight" + "github.com/micro-editor/micro/v2/pkg/filetype" + "github.com/micro-editor/micro/v2/pkg/syntax" ) // This file is meant (for now) for autocompletion in command mode, not @@ -80,52 +81,27 @@ func colorschemeComplete(input string) (string, []string) { // filetypeComplete autocompletes filetype func filetypeComplete(input string) (string, []string) { + seen := make(map[string]bool) var suggestions []string - - // We cannot match filetypes just by names of syntax files, - // since those names may be different from the actual filetype values - // specified inside syntax files (e.g. "c++" filetype in cpp.yaml). - // So we need to parse filetype values out of those files. - for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) { - data, err := f.Data() - if err != nil { - continue - } - header, err := highlight.MakeHeaderYaml(data) - if err != nil { - continue - } - // Prevent duplicated defaults - if header.FileType == "off" || header.FileType == "unknown" { - continue + add := func(ft string) { + if ft == "" || ft == "unknown" || seen[ft] { + return } - if strings.HasPrefix(header.FileType, input) { - suggestions = append(suggestions, header.FileType) + seen[ft] = true + if strings.HasPrefix(ft, input) { + suggestions = append(suggestions, ft) } } -headerLoop: - for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) { - data, err := f.Data() - if err != nil { - continue - } - header, err := highlight.MakeHeader(data) - if err != nil { - continue - } - for _, v := range suggestions { - if v == header.FileType { - continue headerLoop - } - } - if strings.HasPrefix(header.FileType, input) { - suggestions = append(suggestions, header.FileType) - } + for _, ft := range filetype.Filetypes() { + add(ft) + } + for _, ft := range syntax.FileTypes() { + add(ft) } - if strings.HasPrefix("off", input) { suggestions = append(suggestions, "off") } + sort.Strings(suggestions) var chosen string if len(suggestions) == 1 { diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index f033ca24..705f1e80 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -8,11 +8,13 @@ import ( "fmt" "io" "io/fs" + "log" "os" "path/filepath" "strconv" "strings" "sync" + "sync/atomic" "time" luar "layeh.com/gopher-luar" @@ -21,7 +23,8 @@ import ( ulua "github.com/micro-editor/micro/v2/internal/lua" "github.com/micro-editor/micro/v2/internal/screen" "github.com/micro-editor/micro/v2/internal/util" - "github.com/micro-editor/micro/v2/pkg/highlight" + "github.com/micro-editor/micro/v2/pkg/filetype" + "github.com/micro-editor/micro/v2/pkg/syntax" dmp "github.com/sergi/go-diff/diffmatchpatch" "golang.org/x/text/encoding" "golang.org/x/text/encoding/htmlindex" @@ -31,7 +34,8 @@ import ( var ( // OpenBuffers is a list of the currently open buffers - OpenBuffers []*Buffer + OpenBuffers []*Buffer + nextBufferID uint64 // LogBuf is a reference to the log buffer which can be opened with the // `> log` command LogBuf *Buffer @@ -111,11 +115,9 @@ type SharedBuffer struct { // it changes based on how the buffer has changed HasSuggestions bool - // The Highlighter struct actually performs the highlighting - Highlighter *highlight.Highlighter - // SyntaxDef represents the syntax highlighting definition being used - // This stores the highlighting rules and filetype detection info - SyntaxDef *highlight.Def + hl *highlightSession + hlGen uint64 + hlID uint64 ModifiedThisFrame bool @@ -129,6 +131,14 @@ func (b *SharedBuffer) insert(pos Loc, value []byte) { b.setModified() inslines := bytes.Count(value, []byte{'\n'}) + b.recordEdit(syntax.Edit{ + StartLine: pos.Y, + StartCol: pos.X, + OldEndLine: pos.Y, + OldEndCol: pos.X, + NewEndLine: pos.Y + inslines, + NewEndCol: insertedEndCol(pos, value, inslines), + }) b.MarkModified(pos.Y, pos.Y+inslines) } @@ -136,9 +146,25 @@ func (b *SharedBuffer) remove(start, end Loc) []byte { b.HasSuggestions = false defer b.setModified() defer b.MarkModified(start.Y, end.Y) + b.recordEdit(syntax.Edit{ + StartLine: start.Y, + StartCol: start.X, + OldEndLine: end.Y, + OldEndCol: end.X, + NewEndLine: start.Y, + NewEndCol: start.X, + }) return b.LineArray.remove(start, end) } +func insertedEndCol(pos Loc, value []byte, newlines int) int { + if newlines == 0 { + return pos.X + syntax.CharacterCount(value) + } + last := bytes.LastIndexByte(value, '\n') + return syntax.CharacterCount(value[last+1:]) +} + func (b *SharedBuffer) setModified() { if b.Type.Scratch { return @@ -180,20 +206,18 @@ func (b *SharedBuffer) calcHash(out *[md5.Size]byte) { h.Sum((*out)[:0]) } -// MarkModified marks the buffer as modified for this frame -// and performs rehighlighting if syntax highlighting is enabled +// MarkModified marks the buffer as modified for this frame and asks the +// active highlighting provider for fresh spans after a short debounce. func (b *SharedBuffer) MarkModified(start, end int) { b.ModifiedThisFrame = true start = util.Clamp(start, 0, len(b.lines)-1) end = util.Clamp(end, 0, len(b.lines)-1) - if b.Settings["syntax"].(bool) && b.SyntaxDef != nil { - l := -1 - for i := start; i <= end; i++ { - l = util.Max(b.Highlighter.ReHighlightStates(b, i), l) - } - b.Highlighter.HighlightMatches(b, start, l) + if b.hl != nil { + b.hlGen++ + b.hl.cause = syntax.CauseEdit + b.scheduleHighlight() } for i := start; i <= end; i++ { @@ -376,6 +400,7 @@ func NewBuffer(r io.Reader, size int64, path string, btype BufType, cmd Command) hasBackup := false if !found { b.SharedBuffer = new(SharedBuffer) + b.hlID = atomic.AddUint64(&nextBufferID, 1) b.Type = btype b.AbsPath = absPath @@ -533,6 +558,7 @@ func (b *Buffer) Fini() { b.Serialize() } b.CancelBackup() + b.SharedBuffer.closeHighlight() } if b.Type == BTStdout { @@ -721,309 +747,303 @@ func (b *Buffer) Size() int { return nb } -func parseDefFromFile(f config.RuntimeFile, header *highlight.Header) *highlight.Def { - data, err := f.Data() - if err != nil { - screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error()) - return nil - } +// HighlightDebounce is how long edits settle before a provider request. +// It is a variable so tests can shorten it. +var HighlightDebounce = 25 * time.Millisecond - if header == nil { - header, err = highlight.MakeHeaderYaml(data) - if err != nil { - screen.TermMessage("Error parsing header for syntax file " + f.Name() + ": " + err.Error()) - return nil - } - } +// MainThread schedules f on the editor's main goroutine. cmd/micro replaces +// this with a send to its timer channel so Lua providers never run on a +// timer goroutine. +var MainThread = func(f func()) { f() } - file, err := highlight.ParseFile(data) - if err != nil { - screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error()) - return nil - } +type highlightSession struct { + provider *syntax.Provider + filetype string + requestGen uint64 + inFlight bool + dirty bool + closed bool + cause syntax.Cause + timer *time.Timer + edits []syntax.Edit +} - syndef, err := highlight.ParseDef(file, header) - if err != nil { - screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error()) - return nil +func (b *SharedBuffer) recordEdit(e syntax.Edit) { + if b.hl != nil && !b.hl.closed { + b.hl.edits = append(b.hl.edits, e) } - - return syndef } -// findRealRuntimeSyntaxDef finds a specific syntax definition -// in the user's custom syntax files -func findRealRuntimeSyntaxDef(name string, header *highlight.Header) *highlight.Def { - for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) { - if f.Name() == name { - syndef := parseDefFromFile(f, header) - if syndef != nil { - return syndef - } +func (b *SharedBuffer) highlightView() *Buffer { + for _, buf := range OpenBuffers { + if buf.SharedBuffer == b { + return buf } } return nil } -// findRuntimeSyntaxDef finds a specific syntax definition -// in the built-in syntax files -func findRuntimeSyntaxDef(name string, header *highlight.Header) *highlight.Def { - for _, f := range config.ListRuntimeFiles(config.RTSyntax) { - if f.Name() == name { - syndef := parseDefFromFile(f, header) - if syndef != nil { - return syndef - } +// Text returns the whole buffer as UTF-8 lines joined with a single newline. +func (b *SharedBuffer) Text() string { + var sb strings.Builder + for i := 0; i < b.LinesNum(); i++ { + if i > 0 { + sb.WriteByte('\n') } + sb.Write(b.LineBytes(i)) } - return nil + return sb.String() +} + +// UpdateRules is kept as an alias for plugins that call buf:UpdateRules(). +func (b *Buffer) UpdateRules() { + b.UpdateProvider() } -func resolveIncludes(syndef *highlight.Def) { - includes := highlight.GetIncludes(syndef) - if len(includes) == 0 { +// UpdateProvider re-detects the filetype and (re)attaches a highlighting +// provider. No provider is a supported state: filetype identity still works. +func (b *Buffer) UpdateProvider() { + if b.SharedBuffer == nil { return } + b.SharedBuffer.updateProvider() +} - var files []*highlight.File - for _, f := range config.ListRuntimeFiles(config.RTSyntax) { - data, err := f.Data() - if err != nil { - screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error()) - continue - } +func (b *SharedBuffer) updateProvider() { + if !b.Type.Syntax { + b.detachHighlight(true) + return + } - header, err := highlight.MakeHeaderYaml(data) - if err != nil { - screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error()) - continue + ft := b.Settings["filetype"].(string) + if ft == "off" { + b.detachHighlight(true) + return + } + + detectLimit := util.IntOpt(b.Settings["detectlimit"]) + if ft == "" || ft == "unknown" { + if detected := filetype.Match(b.Path, b, detectLimit); detected != "" { + ft = detected } + } - for _, i := range includes { - if header.FileType == i { - file, err := highlight.ParseFile(data) - if err != nil { - screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error()) - continue - } - files = append(files, file) - break - } + if p := syntax.Lookup(b.Path, ft, b, detectLimit); p != nil { + if p.Match.FileType != "" { + ft = p.Match.FileType } - if len(files) >= len(includes) { - break + b.Settings["filetype"] = ft + if b.Settings["syntax"].(bool) { + b.attachHighlight(p, ft) + return } + } else if ft != "" && ft != b.Settings["filetype"] { + b.Settings["filetype"] = ft } - highlight.ResolveIncludes(syndef, files) + b.detachHighlight(true) } -// UpdateRules updates the syntax rules and filetype for this buffer -// This is called when the colorscheme changes -func (b *Buffer) UpdateRules() { - if !b.Type.Syntax { +func (b *SharedBuffer) attachHighlight(p *syntax.Provider, ft string) { + if b.hl != nil && !b.hl.closed && b.hl.provider == p && b.hl.filetype == ft { return } - ft := b.Settings["filetype"].(string) - if ft == "off" { + b.detachHighlight(false) + b.hlGen++ + b.hl = &highlightSession{ + provider: p, + filetype: ft, + cause: syntax.CauseOpen, + } + b.ClearMatches() + b.scheduleHighlight() +} + +func (b *SharedBuffer) detachHighlight(clear bool) { + if h := b.hl; h != nil { + if h.timer != nil { + h.timer.Stop() + h.timer = nil + } + h.closed = true + b.hl = nil + } + b.hlGen++ + if clear { b.ClearMatches() - b.SyntaxDef = nil - return + screen.Redraw() } +} - b.SyntaxDef = nil +func (b *SharedBuffer) scheduleHighlight() { + h := b.hl + if h == nil || h.closed { + return + } + h.dirty = true + if h.timer != nil { + return + } + run := MainThread + h.timer = time.AfterFunc(HighlightDebounce, func() { + run(func() { b.highlightTick() }) + }) +} - // syntaxFileInfo is an internal helper structure - // to store properties of one single syntax file - type syntaxFileInfo struct { - header *highlight.Header - fileName string - syntaxDef *highlight.Def +func (b *SharedBuffer) highlightTick() { + h := b.hl + if h == nil || h.closed { + return + } + h.timer = nil + if !h.dirty || h.inFlight { + return + } + if !b.Settings["syntax"].(bool) || h.provider == nil { + h.dirty = false + return } - fnameMatches := []syntaxFileInfo{} - headerMatches := []syntaxFileInfo{} - syntaxFile := "" - foundDef := false - var header *highlight.Header - // search for the syntax file in the user's custom syntax files - for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) { - if f.Name() == "default" { - continue - } + view := b.highlightView() + if view == nil { + h.dirty = false + return + } - data, err := f.Data() - if err != nil { - screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error()) - continue + h.dirty = false + h.inFlight = true + h.requestGen = b.hlGen + req := syntax.Request{ + Buf: view, + BufID: b.hlID, + Path: b.Path, + FileType: h.filetype, + Text: b.Text(), + First: 0, + Last: b.LinesNum() - 1, + Generation: h.requestGen, + Cause: h.cause, + Edits: append([]syntax.Edit(nil), h.edits...), + } + h.edits = h.edits[:0] + h.cause = syntax.CauseEdit + req.Reply = func(spans []syntax.Span) { + b.ApplySpans(req.Generation, spans) + } + req.Fail = func(err error) { + if b.hl != h || h.closed || h.requestGen != req.Generation { + return } - - header, err = highlight.MakeHeaderYaml(data) + h.inFlight = false + h.dirty = false if err != nil { - screen.TermMessage("Error parsing header for syntax file " + f.Name() + ": " + err.Error()) - continue + log.Println("syntax provider", h.provider.Name, "failed:", err) } + } - matchedFileType := false - matchedFileName := false - matchedFileHeader := false - - if ft == "unknown" || ft == "" { - if header.MatchFileName(b.Path) { - matchedFileName = true - } - if len(fnameMatches) == 0 && header.MatchFileHeader(b.lines[0].data) { - matchedFileHeader = true - } - } else if header.FileType == ft { - matchedFileType = true + p := h.provider + defer func() { + if r := recover(); r != nil { + log.Println("syntax provider", p.Name, "panic:", r) + b.providerPanicked(h) } + }() + p.Request(req) +} - if matchedFileType || matchedFileName || matchedFileHeader { - file, err := highlight.ParseFile(data) - if err != nil { - screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error()) - continue - } - - syndef, err := highlight.ParseDef(file, header) - if err != nil { - screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error()) - continue - } - - if matchedFileType { - b.SyntaxDef = syndef - syntaxFile = f.Name() - foundDef = true - break - } +func (b *SharedBuffer) providerPanicked(h *highlightSession) { + if b.hl != h || h.closed { + return + } + h.inFlight = false + h.dirty = false + if syntax.ReportError(h.provider.Name) { + log.Println("syntax: disabling provider", h.provider.Name) + b.detachHighlight(true) + } +} - if matchedFileName { - fnameMatches = append(fnameMatches, syntaxFileInfo{header, f.Name(), syndef}) - } else if matchedFileHeader { - headerMatches = append(headerMatches, syntaxFileInfo{header, f.Name(), syndef}) +// ApplySpans replaces all highlighting with spans from generation gen. +// Lines not mentioned in spans are cleared. Stale generations are dropped. +func (b *SharedBuffer) ApplySpans(gen uint64, spans []syntax.Span) bool { + h := b.hl + if h == nil || h.closed { + return false + } + if gen != b.hlGen { + if gen == h.requestGen { + h.inFlight = false + if h.dirty { + b.scheduleHighlight() } } + return false } - if !foundDef { - // search for the syntax file in the built-in syntax files - for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) { - data, err := f.Data() - if err != nil { - screen.TermMessage("Error loading syntax header file " + f.Name() + ": " + err.Error()) - continue - } - - header, err = highlight.MakeHeader(data) - if err != nil { - screen.TermMessage("Error reading syntax header file", f.Name(), err) - continue - } + h.inFlight = false + h.dirty = false + syntax.ReportSuccess(h.provider.Name) - if ft == "unknown" || ft == "" { - if header.MatchFileName(b.Path) { - fnameMatches = append(fnameMatches, syntaxFileInfo{header, f.Name(), nil}) - } - if len(fnameMatches) == 0 && header.MatchFileHeader(b.lines[0].data) { - headerMatches = append(headerMatches, syntaxFileInfo{header, f.Name(), nil}) - } - } else if header.FileType == ft { - syntaxFile = f.Name() - break - } + byLine := make(map[int][]syntax.Span) + for _, s := range spans { + if s.Line < 0 || s.Line >= b.LinesNum() || s.EndByte <= s.StartByte { + continue } + byLine[s.Line] = append(byLine[s.Line], s) } - if syntaxFile == "" { - matches := fnameMatches - if len(matches) == 0 { - matches = headerMatches - } - - length := len(matches) - if length > 0 { - signatureMatch := false - if length > 1 { - // multiple matching syntax files found, try to resolve the ambiguity - // using signatures - detectlimit := util.IntOpt(b.Settings["detectlimit"]) - lineCount := len(b.lines) - limit := lineCount - if detectlimit > 0 && lineCount > detectlimit { - limit = detectlimit - } - - matchLoop: - for _, m := range matches { - if m.header.HasFileSignature() { - for i := 0; i < limit; i++ { - if m.header.MatchFileSignature(b.lines[i].data) { - syntaxFile = m.fileName - if m.syntaxDef != nil { - b.SyntaxDef = m.syntaxDef - foundDef = true - } - header = m.header - signatureMatch = true - break matchLoop - } - } - } - } - } - if length == 1 || !signatureMatch { - syntaxFile = matches[0].fileName - if matches[0].syntaxDef != nil { - b.SyntaxDef = matches[0].syntaxDef - foundDef = true - } - header = matches[0].header - } + for i := 0; i < b.LinesNum(); i++ { + if lineSpans, ok := byLine[i]; ok { + b.SetMatch(i, syntax.BuildMatch(b.LineBytes(i), lineSpans)) + } else { + b.SetMatch(i, nil) } } - if syntaxFile != "" && !foundDef { - // we found a syntax file using a syntax header file - b.SyntaxDef = findRuntimeSyntaxDef(syntaxFile, header) - } + screen.Redraw() + return true +} - if b.SyntaxDef != nil { - b.Settings["filetype"] = b.SyntaxDef.FileType - } else { - // search for the default file in the user's custom syntax files - b.SyntaxDef = findRealRuntimeSyntaxDef("default", nil) - if b.SyntaxDef == nil { - // search for the default file in the built-in syntax files - b.SyntaxDef = findRuntimeSyntaxDef("default", nil) - } +// InvalidateSyntax asks for fresh spans with a manual cause. +func (b *Buffer) InvalidateSyntax() { + s := b.SharedBuffer + if s == nil { + return } - - if b.SyntaxDef != nil { - resolveIncludes(b.SyntaxDef) + if s.hl == nil || s.hl.closed { + s.updateProvider() + return } + s.hlGen++ + s.hl.cause = syntax.CauseManual + s.scheduleHighlight() +} - if b.SyntaxDef != nil { - b.Highlighter = highlight.NewHighlighter(b.SyntaxDef) - if b.Settings["syntax"].(bool) { - go func() { - b.Highlighter.HighlightStates(b) - b.Highlighter.HighlightMatches(b, 0, b.End().Y) - screen.Redraw() - }() +// RefreshSyntax re-resolves highlighting for every open buffer, usually +// after plugins register or unregister providers. +func RefreshSyntax() { + seen := make(map[*SharedBuffer]bool) + for _, b := range OpenBuffers { + if b.SharedBuffer == nil || seen[b.SharedBuffer] { + continue } + seen[b.SharedBuffer] = true + b.UpdateProvider() } + screen.Redraw() } -// ClearMatches clears all of the syntax highlighting for the buffer -func (b *Buffer) ClearMatches() { +// ClearMatches clears all of the syntax highlighting for the buffer. +func (b *SharedBuffer) ClearMatches() { for i := range b.lines { b.SetMatch(i, nil) - b.SetState(i, nil) } } +func (b *SharedBuffer) closeHighlight() { + b.detachHighlight(true) +} + // IndentString returns this buffer's indent method (a tabstop or n spaces // depending on the settings) func (b *Buffer) IndentString(tabsize int) string { diff --git a/internal/buffer/line_array.go b/internal/buffer/line_array.go index eecb6b59..3b808c3d 100644 --- a/internal/buffer/line_array.go +++ b/internal/buffer/line_array.go @@ -7,7 +7,7 @@ import ( "sync" "github.com/micro-editor/micro/v2/internal/util" - "github.com/micro-editor/micro/v2/pkg/highlight" + "github.com/micro-editor/micro/v2/pkg/syntax" ) // Finds the byte index of the nth rune in a byte slice @@ -41,13 +41,11 @@ type searchState struct { done bool } -// A Line contains the data in bytes as well as a highlight state, match -// and a flag for whether the highlighting needs to be updated +// A Line contains the data in bytes and the display-facing highlight match. type Line struct { data []byte - state highlight.State - match highlight.LineMatch + match syntax.LineMatch lock sync.Mutex // The search states for the line, used for highlighting of search matches, @@ -148,7 +146,6 @@ func NewLineArray(size uint64, endings FileFormat, reader io.Reader) *LineArray if err == io.EOF { la.lines = Append(la.lines, Line{ data: data, - state: nil, match: nil, }) } @@ -157,7 +154,6 @@ func NewLineArray(size uint64, endings FileFormat, reader io.Reader) *LineArray } else { la.lines = Append(la.lines, Line{ data: data[:dlen-1], - state: nil, match: nil, }) } @@ -189,13 +185,11 @@ func (la *LineArray) Bytes() []byte { func (la *LineArray) newlineBelow(y int) { la.lines = append(la.lines, Line{ data: []byte{' '}, - state: nil, match: nil, }) copy(la.lines[y+2:], la.lines[y+1:]) la.lines[y+1] = Line{ data: []byte{}, - state: la.lines[y].state, match: nil, } } @@ -240,8 +234,6 @@ func (la *LineArray) joinLines(a, b int) { func (la *LineArray) split(pos Loc) { la.newlineBelow(pos.Y) la.lines[pos.Y+1].data = append(la.lines[pos.Y+1].data, la.lines[pos.Y].data[pos.X:]...) - la.lines[pos.Y+1].state = la.lines[pos.Y].state - la.lines[pos.Y].state = nil la.lines[pos.Y].match = nil la.lines[pos.Y+1].match = nil la.deleteToEnd(Loc{pos.X, pos.Y}) @@ -330,29 +322,15 @@ func (la *LineArray) LineBytes(lineN int) []byte { return la.lines[lineN].data } -// State gets the highlight state for the given line number -func (la *LineArray) State(lineN int) highlight.State { - la.lines[lineN].lock.Lock() - defer la.lines[lineN].lock.Unlock() - return la.lines[lineN].state -} - -// SetState sets the highlight state at the given line number -func (la *LineArray) SetState(lineN int, s highlight.State) { - la.lines[lineN].lock.Lock() - defer la.lines[lineN].lock.Unlock() - la.lines[lineN].state = s -} - // SetMatch sets the match at the given line number -func (la *LineArray) SetMatch(lineN int, m highlight.LineMatch) { +func (la *LineArray) SetMatch(lineN int, m syntax.LineMatch) { la.lines[lineN].lock.Lock() defer la.lines[lineN].lock.Unlock() la.lines[lineN].match = m } // Match retrieves the match for the given line number -func (la *LineArray) Match(lineN int) highlight.LineMatch { +func (la *LineArray) Match(lineN int) syntax.LineMatch { la.lines[lineN].lock.Lock() defer la.lines[lineN].lock.Unlock() return la.lines[lineN].match diff --git a/internal/buffer/settings.go b/internal/buffer/settings.go index e2178726..ea356e4d 100644 --- a/internal/buffer/settings.go +++ b/internal/buffer/settings.go @@ -93,11 +93,7 @@ func (b *Buffer) DoSetOptionNative(option string, nativeValue any) { } b.setModified() } else if option == "syntax" { - if !nativeValue.(bool) { - b.ClearMatches() - } else { - b.UpdateRules() - } + b.UpdateProvider() } else if option == "encoding" { enc, err := htmlindex.Get(b.Settings["encoding"].(string)) if err != nil { diff --git a/internal/buffer/syntax_test.go b/internal/buffer/syntax_test.go new file mode 100644 index 00000000..6edb227a --- /dev/null +++ b/internal/buffer/syntax_test.go @@ -0,0 +1,159 @@ +package buffer + +import ( + "testing" + "time" + + "github.com/micro-editor/micro/v2/pkg/syntax" +) + +func TestNoProviderKeepsWorking(t *testing.T) { + syntax.Clear() + b := NewBufferFromString("hello\nworld", "file.definitely-not-a-known-ext", BTDefault) + defer b.Close() + + if b.hl != nil { + t.Fatal("unexpected provider session") + } + b.UpdateProvider() + if b.Match(0) != nil || b.Match(1) != nil { + t.Fatal("unexpected highlight match without a provider") + } +} + +func TestApplySpansDropsStaleGeneration(t *testing.T) { + syntax.Clear() + defer syntax.Clear() + syntax.Register(&syntax.Provider{ + Name: "test-provider", + Match: syntax.Matcher{FileType: "go"}, + Request: func(syntax.Request) {}, + }) + + old := HighlightDebounce + HighlightDebounce = time.Hour + defer func() { HighlightDebounce = old }() + + b := NewBufferFromString("hello", "x.go", BTDefault) + defer b.Close() + if b.hl == nil { + t.Fatal("provider did not attach") + } + if b.hl.timer != nil { + b.hl.timer.Stop() + b.hl.timer = nil + } + + gen := b.hlGen + g := syntax.Intern("comment") + spans := []syntax.Span{{Line: 0, StartByte: 0, EndByte: 5, Group: g}} + + if b.ApplySpans(gen-1, spans) { + t.Fatal("stale generation was applied") + } + if b.Match(0) != nil { + t.Fatal("stale generation changed matches") + } + if !b.ApplySpans(gen, spans) { + t.Fatal("current generation was rejected") + } + if got := b.Match(0)[0]; got != g { + t.Fatalf("match[0] = %v, want %v", got, g) + } + + b.MarkModified(0, 0) + if b.ApplySpans(gen, spans) { + t.Fatal("reply from before an edit was applied") + } + if b.hl.timer != nil { + b.hl.timer.Stop() + b.hl.timer = nil + } +} + +func TestApplySpansClearsUnmentionedLines(t *testing.T) { + syntax.Clear() + defer syntax.Clear() + syntax.Register(&syntax.Provider{ + Name: "test-provider", + Match: syntax.Matcher{FileType: "go"}, + Request: func(syntax.Request) {}, + }) + + old := HighlightDebounce + HighlightDebounce = time.Hour + defer func() { HighlightDebounce = old }() + + b := NewBufferFromString("one\ntwo", "x.go", BTDefault) + defer b.Close() + if b.hl.timer != nil { + b.hl.timer.Stop() + b.hl.timer = nil + } + + g := syntax.Intern("comment") + if !b.ApplySpans(b.hlGen, []syntax.Span{{Line: 0, StartByte: 0, EndByte: 3, Group: g}}) { + t.Fatal("spans rejected") + } + if b.Match(0)[0] != g { + t.Fatal("line 0 not highlighted") + } + if b.Match(1) != nil { + t.Fatal("unmentioned line was not cleared") + } +} + +func TestSharedViewsShareProviderID(t *testing.T) { + syntax.Clear() + first := NewBufferFromString("x", "same.go", BTDefault) + defer first.Close() + second := NewBufferFromString("x", "same.go", BTDefault) + defer second.Close() + + if first.hlID == 0 || first.hlID != second.hlID { + t.Fatalf("ids = %d and %d", first.hlID, second.hlID) + } +} + +func TestSyntaxOffStopsProvider(t *testing.T) { + syntax.Clear() + defer syntax.Clear() + syntax.Register(&syntax.Provider{ + Name: "test-provider", + Match: syntax.Matcher{FileType: "go"}, + Request: func(syntax.Request) {}, + }) + + old := HighlightDebounce + HighlightDebounce = time.Hour + defer func() { HighlightDebounce = old }() + + b := NewBufferFromString("package main", "x.go", BTDefault) + defer b.Close() + if b.hl == nil { + t.Fatal("provider did not attach") + } + if b.hl.timer != nil { + b.hl.timer.Stop() + b.hl.timer = nil + } + + b.Settings["syntax"] = false + b.UpdateProvider() + if b.hl != nil { + t.Fatal("provider was not detached") + } + if b.FileType() != "go" { + t.Fatalf("filetype = %q, want go", b.FileType()) + } + + b.Settings["syntax"] = true + b.UpdateProvider() + if b.hl == nil { + t.Fatal("provider was not attached again") + } + if b.hl.timer != nil { + b.hl.timer.Stop() + b.hl.timer = nil + } +} diff --git a/internal/config/plugin.go b/internal/config/plugin.go index 931e773e..d59e39df 100644 --- a/internal/config/plugin.go +++ b/internal/config/plugin.go @@ -90,6 +90,9 @@ func (p *Plugin) Load() error { if v, ok := GlobalSettings[p.Name]; ok && !v.(bool) { return nil } + prev := ulua.Loading + ulua.Loading = p.Name + defer func() { ulua.Loading = prev }() for _, f := range p.Srcs { dat, err := f.Data() if err != nil { @@ -107,6 +110,10 @@ func (p *Plugin) Load() error { // Call calls a given function in this plugin func (p *Plugin) Call(fn string, args ...lua.LValue) (lua.LValue, error) { + prev := ulua.Loading + ulua.Loading = p.Name + defer func() { ulua.Loading = prev }() + plug := ulua.L.GetGlobal(p.Name) if plug == lua.LNil { log.Println("Plugin does not exist:", p.Name, "at", p.DirName, ":", p) diff --git a/internal/config/rtfiles.go b/internal/config/rtfiles.go index 2cd331ac..4b4e2f8c 100644 --- a/internal/config/rtfiles.go +++ b/internal/config/rtfiles.go @@ -8,24 +8,23 @@ import ( "regexp" "strings" + "github.com/micro-editor/micro/v2/pkg/syntax" rt "github.com/micro-editor/micro/v2/runtime" ) const ( - RTColorscheme = 0 - RTSyntax = 1 - RTHelp = 2 - RTPlugin = 3 - RTSyntaxHeader = 4 + RTColorscheme = 0 + RTHelp = 2 + RTPlugin = 3 ) -var ( - NumTypes = 5 // How many filetypes are there -) +// NumTypes preserves the numeric slots used before the syntax engine was +// removed. Slots 1 (RTSyntax) and 4 (RTSyntaxHeader) are intentionally unused. +var NumTypes = 5 type RTFiletype int -// RuntimeFile allows the program to read runtime data like colorschemes or syntax files +// RuntimeFile allows the program to read runtime data like colorschemes or help files type RuntimeFile interface { // Name returns a name of the file without paths or extensions Name() string @@ -172,13 +171,12 @@ func InitRuntimeFiles(user bool) { initRuntimeVars() add(RTColorscheme, "colorschemes", "*.micro") - add(RTSyntax, "syntax", "*.yaml") - add(RTSyntaxHeader, "syntax", "*.hdr") add(RTHelp, "help", "*.md") } // InitPlugins initializes the plugins func InitPlugins() { + syntax.Clear() Plugins = Plugins[:0] initlua := filepath.Join(ConfigDir, "init.lua") diff --git a/internal/config/rtfiles_test.go b/internal/config/rtfiles_test.go index 38c8e965..c65b47b5 100644 --- a/internal/config/rtfiles_test.go +++ b/internal/config/rtfiles_test.go @@ -12,31 +12,26 @@ func init() { func TestAddFile(t *testing.T) { AddRuntimeFile(RTPlugin, memoryFile{"foo.lua", []byte("hello world\n")}) - AddRuntimeFile(RTSyntax, memoryFile{"bar", []byte("some syntax file\n")}) - f1 := FindRuntimeFile(RTPlugin, "foo.lua") - assert.NotNil(t, f1) - assert.Equal(t, "foo.lua", f1.Name()) - data, err := f1.Data() + f := FindRuntimeFile(RTPlugin, "foo.lua") + assert.NotNil(t, f) + assert.Equal(t, "foo.lua", f.Name()) + data, err := f.Data() assert.Nil(t, err) assert.Equal(t, []byte("hello world\n"), data) - - f2 := FindRuntimeFile(RTSyntax, "bar") - assert.NotNil(t, f2) - assert.Equal(t, "bar", f2.Name()) - data, err = f2.Data() - assert.Nil(t, err) - assert.Equal(t, []byte("some syntax file\n"), data) } func TestFindFile(t *testing.T) { - f := FindRuntimeFile(RTSyntax, "go") + f := FindRuntimeFile(RTHelp, "help") assert.NotNil(t, f) - assert.Equal(t, "go", f.Name()) - data, err := f.Data() - assert.Nil(t, err) - assert.Equal(t, []byte("filetype: go"), data[:12]) + assert.Equal(t, "help", f.Name()) +} - e := FindRuntimeFile(RTSyntax, "foobar") - assert.Nil(t, e) +func TestRemovedSyntaxSlots(t *testing.T) { + if FindRuntimeFile(1, "go") != nil { + t.Fatal("syntax runtime slot should be empty") + } + if FindRuntimeFile(4, "go") != nil { + t.Fatal("syntax header runtime slot should be empty") + } } diff --git a/internal/lua/lua.go b/internal/lua/lua.go index 58347f2f..7abcd0fe 100644 --- a/internal/lua/lua.go +++ b/internal/lua/lua.go @@ -27,6 +27,10 @@ import ( var L *lua.LState +// Loading names the plugin whose code is currently being loaded or called. +// The syntax bridge uses it to namespace providers. +var Loading string + // LoadFile loads a lua file func LoadFile(module string, file string, data []byte) error { pluginDef := []byte("module(\"" + module + "\", package.seeall)") diff --git a/runtime/plugins/gosyntax/gosyntax.lua b/runtime/plugins/gosyntax/gosyntax.lua new file mode 100644 index 00000000..fa42eb1a --- /dev/null +++ b/runtime/plugins/gosyntax/gosyntax.lua @@ -0,0 +1,168 @@ +VERSION = "0.1.0" + +local syntax = import("micro/syntax") + +local groups = { + ["break"] = "statement", + ["case"] = "statement", + ["continue"] = "statement", + ["default"] = "statement", + ["else"] = "statement", + ["fallthrough"] = "statement", + ["for"] = "statement", + ["goto"] = "statement", + ["if"] = "statement", + ["range"] = "statement", + ["return"] = "statement", + ["select"] = "statement", + ["switch"] = "statement", + + ["package"] = "preproc", + ["import"] = "preproc", + ["const"] = "preproc", + ["var"] = "preproc", + ["type"] = "preproc", + ["struct"] = "preproc", + ["interface"] = "preproc", + ["func"] = "preproc", + ["map"] = "preproc", + ["chan"] = "preproc", + ["go"] = "preproc", + ["defer"] = "preproc", + + ["true"] = "constant.bool", + ["false"] = "constant.bool", + ["nil"] = "constant.bool", + ["iota"] = "constant.bool", + + ["bool"] = "type", + ["byte"] = "type", + ["complex64"] = "type", + ["complex128"] = "type", + ["error"] = "type", + ["float32"] = "type", + ["float64"] = "type", + ["int"] = "type", + ["int8"] = "type", + ["int16"] = "type", + ["int32"] = "type", + ["int64"] = "type", + ["rune"] = "type", + ["string"] = "type", + ["uint"] = "type", + ["uint8"] = "type", + ["uint16"] = "type", + ["uint32"] = "type", + ["uint64"] = "type", + ["uintptr"] = "type", +} + +local function add(out, n, line, first, last, group) + out[n + 1] = line + out[n + 2] = first + out[n + 3] = last + out[n + 4] = group + return n + 4 +end + +local function find_comment(line) + local i = 1 + while i <= #line do + local c = line:sub(i, i) + if c == '"' then + i = i + 1 + while i <= #line do + local q = line:sub(i, i) + if q == "\\" then + i = i + 2 + elseif q == '"' then + i = i + 1 + break + else + i = i + 1 + end + end + elseif c == "`" then + local e = line:find("`", i + 1, true) + if not e then + break + end + i = e + 1 + elseif c == "/" and line:sub(i + 1, i + 1) == "/" then + return i + else + i = i + 1 + end + end + return nil +end + +local function scan_quoted(code, quote, line, out, n) + local i = 1 + while true do + local first = code:find(quote, i, true) + if not first then + return n + end + local last = code:find(quote, first + 1, true) + if last then + n = add(out, n, line, first - 1, last, "constant.string") + i = last + 1 + else + n = add(out, n, line, first - 1, #code, "constant.string") + return n + end + end +end + +local function highlight_line(line, lineNo, out, n) + local comment = find_comment(line) + local code = comment and line:sub(1, comment - 1) or line + + n = scan_quoted(code, '"', lineNo, out, n) + n = scan_quoted(code, "`", lineNo, out, n) + + for first, last in code:gmatch("()%d+()") do + local before = first > 1 and code:sub(first - 1, first - 1) or "" + if not before:match("[%a_]") then + n = add(out, n, lineNo, first - 1, last - 1, "constant.number") + end + end + + for first, last, word in code:gmatch("()([%a_][%w_]*)()") do + local group = groups[word] + if group then + n = add(out, n, lineNo, first - 1, last - 1, group) + end + end + + if comment then + n = add(out, n, lineNo, comment - 1, #line, "comment") + end + return n +end + +local function request(ctx, reply) + local out, n = {}, 0 + local lineNo = 0 + for line in (ctx.text .. "\n"):gmatch("(.-)\n") do + if lineNo > ctx.last then + break + end + if lineNo >= ctx.first then + n = highlight_line(line, lineNo, out, n) + end + lineNo = lineNo + 1 + end + reply(out) +end + +function init() + syntax.register { + name = "go-hardcoded", + filetype = "go", + filename = "\\.go$", + priority = -1000, + request = request, + } +end