Something went wrong. Try again.
Fork of micro-editor/micro
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335VERSION = "0.2.0"
-- Tree-sitter highlighting for the filetypes linked into the helper.-- The helper owns parsing, grammars, queries, and capture mapping; this-- plugin owns the process, request framing, and reply generation checks.---- One provider is registered per filetype. They all share one helper process-- and one request function, so a buffer switch does not restart the helper.
local syntax = import("micro/syntax")local shell = import("micro/shell")local micro = import("micro")local os = import("os")
-- Mirrors pkg/filetype/table.go for micro's tree-sitter filetypes.local PROVIDERS = { { filetype = "go", filename = [[\.go$]] }, { filetype = "python", filename = [[\.py(3|w)?$]] }, { filetype = "javascript", filename = [[(\.(m|c)?js$|\.es[5678]?$)]] }, { filetype = "typescript", filename = [[\.tsx?$]] }, { filetype = "rust", filename = [[\.rs$]] }, { filetype = "c", filename = [[(\.(c|C)$|\.(h|H)$|\.ii?$|\.(def)$)]] }, { filetype = "c++", filename = [[(\.c(c|pp|xx)$|\.h(h|pp|xx)?$|\.ii?$|\.(def)$)]], signature = [[\b(namespace|class|public|protected|private|template|constexpr|noexcept|nullptr|throw)\b]], }, { filetype = "shell", filename = [[(\.(sh|bash|ash|ebuild)$|(\.bash(rc|_aliases|_functions|_profile)|\.?profile|Pkgfile|pkgmk\.conf|rc\.conf|PKGBUILD|APKBUILD)$|bash-fc\.)]], }, { filetype = "json", filename = [[\.jsonc?$]] }, { filetype = "toml", filename = [[\.toml$]] }, { filetype = "html", filename = [[\.htm[l]?$]] }, { filetype = "css", filename = [[\.(css|scss)$]] }, { filetype = "lua", filename = [[\.lua$]] }, { filetype = "yaml", filename = [[\.ya?ml$]] }, { filetype = "nix", filename = [[\.nix$]] },}
local job = nillocal disabled = falselocal logged = falselocal providersRegistered = falselocal spawnGeneration = 0local helperPath = nil
local outputBuffer = ""local collecting = nillocal replies = {}local bufferIDs = {}local nextBufferID = 0local errorLogged = {}
local function logOnce(message) if not logged then logged = true micro.Log("treesitter: " .. message) endend
local function nextLine() local newline = outputBuffer:find("\n", 1, true) if not newline then return nil end local line = outputBuffer:sub(1, newline - 1) outputBuffer = outputBuffer:sub(newline + 1) return lineend
local function deliver(id, generation, spans, errMessage) local pending = replies[id] if pending == nil then return end if tostring(pending.generation) ~= tostring(generation) then return end replies[id] = nil if errMessage ~= nil then local filetype = pending.filetype or "" if not errorLogged[filetype] then errorLogged[filetype] = true micro.Log("treesitter: " .. filetype .. " unavailable: " .. errMessage) end -- A failed request carries no spans. Clear the previous ones so the -- buffer is never coloured from a stale generation. pcall(pending.reply, {}) return end local ok, err = pcall(pending.reply, spans) if not ok then micro.Log("treesitter: reply failed: " .. tostring(err)) endend
local function stopJob(generation) if generation ~= spawnGeneration then return end spawnGeneration = spawnGeneration + 1 if job ~= nil then shell.JobStop(job) job = nil end replies = {} collecting = nil outputBuffer = ""end
local function protocolFailure(generation, message) if generation ~= spawnGeneration then return end logOnce("helper protocol error: " .. message) disabled = true stopJob(generation)end
local process -- forward declaration
local function handleHeader(generation, line) local kind, id, gen, rest = line:match("^(%S+)\t(%S+)\t(%S+)\t(.*)$") if kind == nil then protocolFailure(generation, "malformed header: " .. line) return end if kind == "SPANS" then local count = tonumber(rest) if count == nil or count < 0 or count ~= math.floor(count) then protocolFailure(generation, "bad span count: " .. rest) return end if count == 0 then deliver(id, gen, {}) return end collecting = { id = id, generation = gen, count = count, spans = {} } elseif kind == "ERR" then deliver(id, gen, nil, rest) else protocolFailure(generation, "unknown frame: " .. kind) endend
local function handleSpanLine(generation, line) local lineNo, startByte, endByte, group = line:match("^(%-?%d+)\t(%-?%d+)\t(%-?%d+)\t(.*)$") if lineNo == nil then protocolFailure(generation, "malformed span line: " .. line) return end local spans = collecting.spans local n = #spans spans[n + 1] = tonumber(lineNo) spans[n + 2] = tonumber(startByte) spans[n + 3] = tonumber(endByte) spans[n + 4] = group if #spans >= collecting.count * 4 then local finished = collecting collecting = nil deliver(finished.id, finished.generation, finished.spans) endend
process = function(generation) if generation ~= spawnGeneration then return end while true do local line = nextLine() if line == nil then return end if collecting ~= nil then handleSpanLine(generation, line) else handleHeader(generation, line) end if generation ~= spawnGeneration then return end endend
local function bufferID(ctx) if ctx.id ~= nil then return tostring(ctx.id) end -- Older core builds may omit ctx.id. Derive a stable numeric id from the -- buffer handle so the helper can still key its parse state. local key = tostring(ctx.buf) local id = bufferIDs[key] if id == nil then nextBufferID = nextBufferID + 1 id = nextBufferID bufferIDs[key] = id end return tostring(id)end
local function request(ctx, reply) if disabled or job == nil then reply({}) return end local id = bufferID(ctx) local generation = ctx.generation or 0 local filetype = ctx.filetype or "" local text = ctx.text or "" replies[id] = { generation = generation, reply = reply, filetype = filetype } local frame = "REQ\t" .. id .. "\t" .. tostring(generation) .. "\t" .. filetype .. "\t" .. tostring(#text) .. "\n" .. text shell.JobSend(job, frame)end
local function startHelper() spawnGeneration = (spawnGeneration or 0) + 1 local generation = spawnGeneration outputBuffer = "" collecting = nil replies = {}
local function onStdout(data, args) if generation ~= spawnGeneration then return end outputBuffer = outputBuffer .. data process(generation) end
local function onStderr(data, args) if generation ~= spawnGeneration then return end local detail = tostring(data or "") if #detail > 400 then detail = detail:sub(1, 400) .. "..." end micro.Log("treesitter: " .. detail) end
local function onExit(output, args) if generation ~= spawnGeneration then return end job = nil disabled = true collecting = nil outputBuffer = "" replies = {} local detail = tostring(output or "") if #detail > 400 then detail = detail:sub(1, 400) .. "..." end detail = detail:gsub("%s+$", "") if detail ~= "" then logOnce("helper exited (" .. helperPath .. "): " .. detail) else logOnce("could not run helper " .. helperPath .. " (set MICRO_TS or install micro-treesitter)") end end
job = shell.JobSpawn(helperPath, {}, onStdout, onStderr, onExit)end
local function registerProviders() local failed = false for _, spec in ipairs(PROVIDERS) do local ok, err = pcall(syntax.register, { name = "treesitter-" .. spec.filetype, filetype = spec.filetype, filename = spec.filename, signature = spec.signature, priority = 100, request = request, }) if not ok then failed = true micro.Log("treesitter: register " .. spec.filetype .. ": " .. tostring(err)) end end if failed then disabled = true if job ~= nil then stopJob(spawnGeneration) end else providersRegistered = true endend
function init() if syntax == nil then logOnce("this micro build has no micro/syntax provider API") disabled = true return end if job ~= nil then return end
disabled = false logged = false errorLogged = {} helperPath = os.Getenv("MICRO_TS") if helperPath == nil or helperPath == "" then helperPath = "micro-treesitter" end
startHelper()
if not providersRegistered then registerProviders() endend
function deinit() spawnGeneration = spawnGeneration + 1 if job ~= nil then shell.JobStop(job) job = nil end outputBuffer = "" collecting = nil replies = {} bufferIDs = {} nextBufferID = 0 disabled = false logged = false providersRegistered = false errorLogged = {}end