Help us shape the future of Go! Take the 2026 Go Developer Survey

script

package
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 20, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

README

script

Editor commands written in Lua, for any program with a golib Editor (github.com/yongjohnlee80/golib/tui/widget or golib/gui/widget).

A script registers verbs of the editor's command line: its : prompt (VIM Commands, with Tab completion) and EditorCore.RunCommand. The script edits through the editor's core functions, and each command's edits undo as one step. AutoDoc uses this package for its own commands (table, mermaid) and for the user's commands in ~/.config/autodoc/lua/. Another program imports the same package.

import "github.com/yongjohnlee80/autodoc/script"

eng := script.New(editor.Core(),
	script.WithGlobal("myapp"),      // scripts call myapp.command{…}; "editor" by default
	script.WithNotify(statusLine),   // where myapp.notify(…) goes
	script.WithTimeout(time.Second)) // a runaway script is stopped (2 s by default)
defer eng.Close()

// a host function scripts can call: myapp.workspace()
eng.SetFunction("workspace", func(L *lua.LState) int { L.Push(lua.LString(ws)); return 1 })
// a table of them: myapp.git.branch()
git := eng.L.NewTable()
eng.L.SetField(git, "branch", eng.L.NewFunction(branch))
eng.L.SetField(eng.Global(), "git", git)

errs := eng.LoadDir(os.DirFS(configDir), "lua") // every lua/*.lua, in name order

Everything runs on the program's UI loop, as everything that touches the editor does.

A script

myapp.command{
  name = "upper", aliases = {"up"}, args = "[line]", help = "upper-case a line",
  run = function(ed, args, line)
    local n = tonumber(args[1]) or ed:cursor()
    ed:replace(n, 1, n, #ed:line(n) + 1, ed:line(n):upper())
    myapp.notify("upper-cased line " .. n)
  end,
}
  • args is the words after the verb, and line is the rest of the line as typed.
  • A command with the name of one already registered replaces it: the editor's own, or an earlier script's.
  • name, args and help are what Tab completion shows.

The editor object

Positions are 1-based: line 1 is the first line, and column 1 is the first character (a grapheme cluster). A range runs from its start up to, not including, its end.

Method Does
ed:value() / ed:set_value(text) the whole text; replace it, as one edit
ed:line_count(), ed:line(n), ed:lines() read lines
ed:cursor() / ed:set_cursor(line, col) where the cursor is; move it
ed:insert(text) / ed:insert_at(line, col, text) insert; returns the line and column after it
ed:replace(l1, c1, l2, c2, text), ed:delete(l1, c1, l2, c2) edit a range
ed:text(l1, c1, l2, c2) a range's text
ed:selection() / ed:select(l1, c1, l2, c2) / ed:unselect() the selection's range, or nil; select a range; end the selection
ed:find(term, {ignore_case=, whole_word=, backward=, wrap=, at_cursor=}) the next match from the cursor, or nil
ed:find_all(term, opts) / ed:replace_all(term, text, opts) every match, as {l1, c1, l2, c2}; replace them all as one edit, and say how many
ed:mode(), ed:read_only(), ed:modified() "normal", "insert", "visual"…; whether edits are refused; whether the text is unsaved
ed:run(line) run a command line, as the prompt does

run gets a fourth argument, cmd: cmd.name is the verb as typed (an alias, say), and cmd.bang whether it ended in !. EditorDocs() documents every method, a row each, for a host's own reference; AutoDoc's docs/lua/reference.md is generated from it.

Characters

#s and string.sub count bytes; the editor's columns are characters (grapheme clusters). The global table's text counts as the editor does (TextDocs):

Function Does
myapp.text.columns(s) how many characters s has
myapp.text.sub(s, i, j) characters i to j, as string.sub takes positions
myapp.text.chars(s) s's characters, a list

An error is said by its value: a table raised by its __tostring, its message field, or its fields; never with Lua's stack trace, and a script past its deadline as ran past <timeout>.

Host functions that wait

A host function must not wait on the UI loop: a call to a server would freeze the program while it answers. It takes a callback instead, does its work on a goroutine, and hands the answer back on the loop with Callback, which runs the Lua function as a command runs: under the deadline, with the editor object reachable again and its edits one undo step, and its error returned.

eng.SetFunction("fetch", func(L *lua.LState) int {
	arg, err := script.ToGo(L.Get(1)) // a Lua value as Go: nil, bool, int64, float64, string, []any, map[string]any
	if err != nil {
		L.ArgError(1, err.Error())
	}
	cb := L.CheckFunction(2)
	go func() {
		answer := fetch(arg)
		post(func() { // back on the UI loop
			v, _ := script.ToLua(eng.L, answer)
			if err := eng.Callback("fetch's callback", cb, v); err != nil {
				statusLine(err.Error()) // nobody waits on a callback's error: show it
			}
		})
	}()
	return 0
})

The editor may hold another document by the time the answer comes; a callback reads where things are then, not where they were when it was asked.

Values cross as msgpack carries them:

Lua Go
nil, a boolean, a string nil, bool, string ([]byte reaches Lua as a string)
an integral number / any other int64 / float64
a table with keys 1..n []any
any other table map[string]any, number and boolean keys as strings

An empty table is a map (the empty options a call takes most often); a table with a hole ({1, nil, 3}) is a map too. A function, a coroutine, the editor object or a table holding itself cannot cross (ErrNotCarried). Going to Lua, every Go number is a number, any slice a list, any map a table, a pointer what it points to, and a fmt.Stringer its string.

Safety

  • Scripts get Lua's base, table, string and math libraries. They get no os, no io, and no dofile, load, require or modules.
  • An error, whether the script's own or the deadline's, becomes the command's error. The editor shows it in its prompt; it never crashes the program.
  • A script file that fails to load is one error from LoadDir, and the other files still load.

The editor functions underneath are documented in golib's tui/widget/EDITOR_CORE.md.

Documentation

Overview

Package script runs editor commands written in Lua: each script registers verbs of a golib editor's command line (its ':' prompt and RunCommand), and edits through the editor's core functions. AutoDoc loads its own scripts and the user's with it; any program with a golib Editor can do the same.

eng := script.New(editor.Core(), script.WithGlobal("autodoc"), script.WithNotify(say))
defer eng.Close()
errs := eng.LoadDir(os.DirFS(configDir), "lua") // every lua/*.lua, in name order

A script registers a command through the global table (named by WithGlobal; "editor" by default). Its run function gets the editor, the command's words and the rest of its line:

editor.command{
  name = "upper", aliases = {"up"}, args = "[line]", help = "upper-case a line",
  run = function(ed, args, line)
    local n = tonumber(args[1]) or ed:cursor()
    ed:replace(n, 1, n, #ed:line(n) + 1, ed:line(n):upper())
  end,
}

A command with the name of one already registered (the editor's own, an earlier script's) replaces it. A host with a command catalog of its own takes the commands instead (SetRegistrar): the table's id names one there (lua.<name> when it has none) and title is how its lists call it, and the host decides what the name replaces. Positions are 1-based, as Lua's and the command line's line numbers are: line 1 is the first, column 1 the first character (a grapheme cluster). A range runs from its start up to, not including, its end, as the editor's do. The editor object:

ed:value()                    the whole text
ed:set_value(text)            replace the whole text, as one edit
ed:line_count()               how many lines
ed:line(n)                    line n's text
ed:lines()                    every line, a table
ed:cursor()                   line, column
ed:set_cursor(line, col)
ed:insert(text)               at the cursor; returns the line and column after it
ed:insert_at(line, col, text) the same, at a position
ed:replace(l1, c1, l2, c2, text)
ed:delete(l1, c1, l2, c2)
ed:text(l1, c1, l2, c2)       a range's text
ed:selection()                l1, c1, l2, c2, or nil with none
ed:select(l1, c1, l2, c2)     select a range; ed:unselect() ends the selection
ed:find(term, opts)           the next match from the cursor: l1, c1, l2, c2, or nil;
                              opts: { ignore_case, whole_word, backward, wrap, at_cursor }
ed:find_all(term, opts)       every match, a list of {l1, c1, l2, c2}
ed:replace_all(term, text, opts)  every match replaced, as one edit; how many
ed:mode()                     "normal", "insert", "replace", "visual", "visual_line", "visual_block"
ed:read_only(), ed:modified() whether edits are refused; whether the text is unsaved
ed:run(line)                  run a command line, as the prompt does

EditorDocs documents each, as a reference lists them. run gets a fourth argument, cmd: cmd.name is the verb as typed (an alias, say), and cmd.bang whether it ended in '!'. The global table's notify(message) says something where the host shows messages (WithNotify), and its text table counts a string in characters, as the editor's columns do (TextDocs): text.columns(s), text.sub(s, i, j), text.chars(s). A host adds functions of its own to the global table with SetFunction, and tables of them through Global.

A host function that must wait (a call to a server) never waits on the loop: it takes a Lua function, does its work elsewhere, and hands the answer back on the loop with Callback, which runs the function as a command runs (the deadline, the editor, one undo step). ToGo and ToLua carry the values across, shaped as msgpack carries them. To reload its scripts a host makes a new engine and closes the old one (never while Running); an answer coming back for a function of the old one is Callback's ErrStale, and dropped.

A command's edits are one undo step (the editor runs every command in one). A script runs on the UI loop, as everything that touches the editor does, with a deadline (WithTimeout); an error, a script's or the deadline's, is the command's, shown in the prompt and never a crash; it is said by the error's value (a table's message, or its fields), without Lua's stack trace. Scripts get Lua's base, table, string and math libraries: no os, io, files or modules.

Concurrency Model

  • Ownership: loop-goroutine-owned, as the editor core it registers into. Load, LoadDir, SetFunction and every command run on the program's UI loop.

Index

Constants

View Source
const DefaultTimeout = 2 * time.Second

DefaultTimeout is how long one command's script, or one file's loading, may run.

View Source
const Unclassified = "\x00unclassified"

Unclassified is a Gate's answer for a call it cannot classify (a daemon verb whose effect the daemon does not say): refused whatever the script was granted, since no grant can be known to cover it.

Variables

View Source
var ErrIncomplete = errors.New("script: the chunk is not finished")

ErrIncomplete is a chunk that stops before its end: the shell asks for more.

View Source
var ErrNotCarried = errors.New("script: a value that cannot cross between Lua and Go")

ErrNotCarried is a value that cannot cross between Lua and Go: a function, a coroutine, a host's userdata, a table nested past maxDepth, a Go channel.

View Source
var ErrStale = errors.New("script: a callback of a replaced engine")

ErrStale is Callback's answer for a function another engine made: an engine a reload replaced, whose host function's answer came after. The function is not run.

Functions

func ModeName added in v0.1.49

func ModeName(m widget.EditorMode) string

ModeName is m as ed:mode() says it: "normal", "insert", "visual_line", ….

func ToGo added in v0.1.38

func ToGo(v lua.LValue) (any, error)

ToGo is v as Go sees it (the table above).

func ToLua added in v0.1.38

func ToLua(L *lua.LState, v any) (lua.LValue, error)

ToLua is v as a script sees it (the table above): every integer and float a number, a slice or array a list, a map a table, a pointer what it points to. A fmt.Stringer that is none of those is its string; anything else is ErrNotCarried.

Types

type BufferInfo added in v0.1.51

type BufferInfo struct {
	N         int
	Path      string // "" for the untitled draft
	Workspace string // "" for a file outside every workspace, and for the draft
	Kind      string
	Modified  bool
	Active    bool
}

BufferInfo is one buffer as buffers() lists it.

type BufferSource added in v0.1.51

type BufferSource interface {
	// Buffers are the open buffers, in opening order.
	Buffers() []BufferInfo
	// Editor is buffer n's editor: the shown one, or a headless one; ok is false with no buffer n.
	Editor(n int) (*widget.EditorCore, bool)
	// Edited tells the host buffer n's text changed through a handle: dirty, its change event.
	Edited(n int)
}

BufferSource is what a host gives the engine for its buffers (WithBuffers).

type CommandInfo added in v0.1.49

type CommandInfo struct {
	ID      string   // the host's id for it: "lua.upper", "file.save"
	Name    string   // its ':' name; "" for one ':' does not reach
	Aliases []string // its other ':' names
	Args    string   // what Tab shows after the name
	Help    string   // one line
	Title   string   // what the host's lists call it
	Source  string   // who declared it: "lua", "plugin", "editor", …
}

CommandInfo is one command as the global table's commands() lists it. ID, Title and Source are a host catalog's (SetCommandList); without one they are empty, and commands() lists the editor's registry.

type Doc added in v0.1.47

type Doc struct {
	Call    string // "ed:line(n)"
	Returns string // "string"
	Notes   string // "Line n's text, …"
}

Doc is one function of the script API as its reference lists it: how it is called, what it returns, and what to know (Markdown, one row of a table). EditorDocs and TextDocs are the engine's own tables, so the reference made from them lists what the engine has.

func EditorDocs added in v0.1.47

func EditorDocs() []Doc

EditorDocs is the editor object's methods as the reference lists them, in its order.

func GlobalDocs added in v0.1.49

func GlobalDocs(global string) []Doc

GlobalDocs is the global table's own functions as the reference lists them, beside command and notify, the table named global: now, date, commands and on.

func TextDocs added in v0.1.47

func TextDocs(global string) []Doc

TextDocs is the global table's text functions as the reference lists them, the table named global ("autodoc" for "autodoc.text.columns(s)").

type Engine

type Engine struct {
	L *lua.LState
	// contains filtered or unexported fields
}

Engine is one Lua state registering into one editor's commands.

func New

func New(core *widget.EditorCore, opts ...Option) *Engine

New is an engine registering into core's commands.

func (*Engine) Callback added in v0.1.38

func (e *Engine) Callback(name string, fn *lua.LFunction, args ...lua.LValue) error

Callback calls fn with args later, on the UI loop: when the answer a host function went to fetch has come (the host's own call to a server, say). It runs as a command does: under the deadline, its error (the script's, or the deadline's) returned and never a crash, the editor object reaching the editor again and fn's edits one undo step. The editor may hold another document by then than the command that asked had; a callback reads where things are now. name names fn in the error ("search.query's callback").

func (*Engine) Caller added in v0.1.49

func (e *Engine) Caller() (script string, grants Grants, ok bool)

Caller is the gated script calling the host function running now, through its proxy: its name and grants, for a host function that keeps something of the script's (a key) and checks its grant later, when it runs. ok is false for an ungated caller. A host function gated with a Gate that answers "" is one that checks later: the call goes through, with Caller set.

func (*Engine) Close

func (e *Engine) Close()

Close releases the Lua state.

func (*Engine) CloseShell added in v0.1.55

func (e *Engine) CloseShell(sh *Shell)

CloseShell forgets sh: a callback made in it is stale from now on.

func (*Engine) Emit added in v0.1.49

func (e *Engine) Emit(event string, info map[string]any) []error

Emit runs event's handlers, each as fn(ed, info), info as Lua values (ToLua). It returns their errors, each naming the event; a handler that fails stays.

func (*Engine) Eval added in v0.1.55

func (e *Engine) Eval(sh *Shell, chunk string) ([]string, error)

Eval runs chunk in sh: as an expression first (return chunk), else as statements. Its values come back as text, one per value; an error is the engine's, or ErrIncomplete.

func (*Engine) Global added in v0.1.38

func (e *Engine) Global() *lua.LTable

Global is the global table scripts register through: a host adds its own fields to it, tables of functions among them (AutoDoc's autodoc.search.query). Only the loop touches it.

func (*Engine) Handles added in v0.1.49

func (e *Engine) Handles(event string) bool

Handles reports whether a script listens for event: a host may skip the work of an emit nobody hears.

func (*Engine) Load

func (e *Engine) Load(name string, src []byte) error

Load runs one script; name is how its errors name it.

func (*Engine) LoadDir

func (e *Engine) LoadDir(fsys fs.FS, dir string) []error

LoadDir runs every *.lua file in dir of fsys, in name order. A file that fails is one error and the others still load; a dir that does not exist is no scripts.

func (*Engine) LoadGated added in v0.1.49

func (e *Engine) LoadGated(script, name string, src []byte, grants Grants) error

LoadGated runs one gated script, src, as script (the name its grants are kept under); name is how its errors name it.

func (*Engine) LoadPackage added in v0.1.49

func (e *Engine) LoadPackage(script string, fsys fs.FS, grants Grants) error

LoadPackage runs the package in fsys (its folder: init.lua at the top) as script, gated by grants.

func (*Engine) NewShell added in v0.1.55

func (e *Engine) NewShell(out func(string)) *Shell

NewShell is a Shell whose print writes to out and whose ed is the page.

func (*Engine) Running added in v0.1.49

func (e *Engine) Running() bool

Running reports whether a script is running now: a file loading, a command, a completion or a callback. A host does not close the engine (a reload) from inside one.

func (*Engine) SetCommandList added in v0.1.49

func (e *Engine) SetCommandList(fn func() []CommandInfo)

SetCommandList makes fn what commands() lists: a host's catalog of commands, in place of the editor's registry. nil puts the registry back.

func (*Engine) SetDomainGate added in v0.1.49

func (e *Engine) SetDomainGate(domain string, g Gate)

SetDomainGate gates every function of the global table's table domain: autodoc.clipboard, or a daemon domain, autodoc.search.

func (*Engine) SetFunction

func (e *Engine) SetFunction(name string, fn lua.LGFunction)

SetFunction adds fn to the global table as name: a host's own function for its scripts (the workspace's files, say). It runs on the UI loop, like the commands that call it.

func (*Engine) SetGate added in v0.1.49

func (e *Engine) SetGate(name string, g Gate)

SetGate gates the global table's function name. Functions SetFunction adds are ungated until gated here.

func (*Engine) SetRegistrar added in v0.1.40

func (e *Engine) SetRegistrar(fn func(Registration) error)

SetRegistrar hands every command{…} to fn in place of the editor's registry: a host's own catalog of commands, which registers it into the editor as it sees fit. fn's error is raised in the script, as a refusal of the command. nil puts the editor's registry back.

type Gate added in v0.1.49

type Gate func(name string, L *lua.LState) string

Gate names the capability a call of a gated function needs, "" for none, Unclassified for one it cannot say. name is the function's key in its table ("call", "read"); L holds the call's arguments, from 1, for a gate that depends on them (autodoc.call: its verb).

type Grants added in v0.1.49

type Grants interface {
	// Granted reports whether script may use capability now.
	Granted(script, capability string) bool
	// Request is the script's requires{…}: the capabilities it declared, for the host to ask about.
	Request(script string, capabilities []string)
}

Grants is the host's record of what the user let each gated script do.

type Notice added in v0.1.59

type Notice struct {
	Text    string
	ID      string
	Ongoing bool
}

Notice is a notification with an id, as notify(message, {id = …, ongoing = …}) gives it: with Ongoing, a toast updated in place under ID; without, that toast ended.

type Option

type Option func(*config)

Option configures an Engine under construction.

func WithBuffers added in v0.1.51

func WithBuffers(s BufferSource) Option

WithBuffers gives scripts the host's buffers: buffers() and buffer(n). Without it, neither exists.

func WithClock added in v0.1.49

func WithClock(now func() time.Time, loc *time.Location) Option

WithClock is where now() and date() read the time, and the zone date() writes local time in (time.Now and time.Local by default): a test's fixed time, say.

func WithGlobal

func WithGlobal(name string) Option

WithGlobal names the global table scripts register through ("editor" by default): AutoDoc's is "autodoc".

func WithNotices added in v0.1.59

func WithNotices(fn func(Notice) error) Option

WithNotices is where notify(message, opts) goes when opts is given. fn's error is raised in the script, as a bad argument is: the host checks the notice. Unset, the message goes to WithNotify's function and the options are ignored.

func WithNotify

func WithNotify(fn func(string)) Option

WithNotify is where the global table's notify(message) goes: a status line, a toast. Unset, notify does nothing.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout is how long one command's script may run before it is stopped (DefaultTimeout).

type Registration added in v0.1.40

type Registration struct {
	ID      string // the table's id; "lua.<name>" when it has none
	Title   string // the table's title; its help, or its name, when it has none
	File    string // the file whose loading declared it; "" for one declared later (a callback)
	Command widget.Command
}

Registration is one command{…} as a host's registrar takes it: its id and title, the file that declared it, and the command. The command's Run takes the core it is given, or the engine's when it is nil: a host running it from elsewhere than the prompt (a key) passes its own.

type Shell added in v0.1.55

type Shell struct {
	// contains filtered or unexported fields
}

Shell is one REPL's environment in an engine.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL