notes/.config/micro/plug/snippets/snippets.lua

586 lines
14 KiB
Lua

VERSION = "0.2.0"
local micro = import("micro")
local buffer = import("micro/buffer")
local config = import("micro/config")
local util = import("micro/util")
local debugflag = true
local curFileType = ""
local snippets = {}
local currentSnippet = nil
local RTSnippets = config.NewRTFiletype()
local Location = {}
Location.__index = Location
local Snippet = {}
Snippet.__index = Snippet
-- Snippets
-- --> Snippet
-- --> Location
function Location.new(idx, ph, snippet)
debug1("Location.new(idx, ph, snip) idx = " , idx)
--debugt("Location.new(idx, ph, snip) ph = ", ph)
--debugt("Location.new(idx, ph, snippet) snippet = ", snippet)
local self = setmetatable({}, Location)
self.idx = idx
self.ph = ph
self.snippet = snippet
return self
end
-- offset of the location relative to the snippet start
function Location.offset(self)
debug("Location.offset(self)")
local add = 0
for i = 1, #self.snippet.locations do
local loc = self.snippet.locations[i]
debug1("loc = ",loc)
if loc == self then
break
end
local val = loc.ph.value
micro.Log("VAL", val)
if val then
add = add + val:len()
end
end
return self.idx+add
end
function Location.startPos(self)
--debugt("Location.startPos(self) = ",self)
local loc = self.snippet.startPos
return loc:Move(self:offset(), self.snippet.view.buf)
end
-- returns the length of the location (but at least 1)
function Location.len(self)
debug("Location.len(self)")
local len = 0
if self.ph.value then
len = self.ph.value:len()
end
if len <= 0 then
len = 1
end
return len
end
function Location.endPos(self)
debug("Location.endPos(self)")
local start = self:startPos()
micro.Log("ENDPOS", self.ph.value)
return start:Move(self:len(), self.snippet.view.buf)
end
-- check if the given loc is within the location
function Location.isWithin(self, loc)
debug("Location.isWithin(self, loc)")
return loc:GreaterEqual(self:startPos()) and loc:LessEqual(self:endPos())
end
function Location.focus(self)
debug("Location.focus(self)")
local view = self.snippet.view
local startP = self:startPos():Move(-1, view.Buf)
local endP = self:endPos():Move(-1, view.Buf)
micro.Log(startP, endP)
if view.Cursor:LessThan(startP) then
while view.Cursor:LessThan(startP) do
view.Cursor:Right()
end
elseif view.Cursor:GreaterEqual(endP) then
while view.Cursor:GreaterEqual(endP) do
view.Cursor:Left()
end
end
if self.ph.value:len() > 0 then
view.Cursor:SetSelectionStart(startP)
view.Cursor:SetSelectionEnd(endP)
else
view.Cursor:ResetSelection()
end
end
function Location.handleInput(self, ev)
debug("Location.handleInput(self, ev)")
if ev.EventType == 1 then
-- TextInput
if util.String(ev.Deltas[1].Text) == "\n" then
Accept()
return false
else
local offset = 1
local sp = self:startPos()
while sp:LessEqual(-ev.Deltas[1].Start) do
sp = sp:Move(1, self.snippet.view.Buf)
offset = offset + 1
end
self.snippet:remove()
local v = self.ph.value
if v == nil then
v = ""
end
self.ph.value = v:sub(0, offset-1) .. util.String(ev.Deltas[1].Text) .. v:sub(offset)
self.snippet:insert()
return true
end
elseif ev.EventType == -1 then
-- TextRemove
local offset = 1
local sp = self:startPos()
while sp:LessEqual(-ev.Deltas[1].Start) do
sp = sp:Move(1, self.snippet.view.Buf)
offset = offset + 1
end
if ev.Deltas[1].Start.Y ~= ev.Deltas[1].End.Y then
return false
end
self.snippet:remove()
local v = self.ph.value
if v == nil then
v = ""
end
local len = ev.Deltas[1].End.X - ev.Deltas[1].Start.X
self.ph.value = v:sub(0, offset-1) .. v:sub(offset+len)
self.snippet:insert()
return true
end
return false
end
-- new snippet
function Snippet.new()
debug("Snippet.new()")
local self = setmetatable({}, Snippet)
self.code = ""
return self
end
-- add line of code to snippet
function Snippet.AddCodeLine(self, line)
--debugt("Snippet.AddCodeLine(self,line) self = " , self)
debug1("Snippet.AddCodeLine(self, line) line = " , line)
if self.code ~= "" then
self.code = self.code .. "\n"
end
self.code = self.code .. line
end
function Snippet.Prepare(self)
debug("Snippet.Prepare(self)")
if not self.placeholders then
self.placeholders = {}
self.locations = {}
local count = 0
local pattern = "${(%d+):?([^}]*)}"
while true do
local num, value = self.code:match(pattern)
if not num then
break
end
count = count+1
num = tonumber(num)
local idx = self.code:find(pattern)
self.code = self.code:gsub(pattern, "", 1)
micro.Log("IDX", idx, self.code)
local placeHolders = self.placeholders[num]
if not placeHolders then
placeHolders = {num = num}
self.placeholders[#self.placeholders+1] = placeHolders
end
self.locations[#self.locations+1] = Location.new(idx, placeHolders, self)
debug1("location total = ",#self.locations)
if value then
placeHolders.value = value
end
end
end
end
function Snippet.clone(self)
debug("Snippet.clone(self)")
local result = Snippet.new()
result:AddCodeLine(self.code)
result:Prepare()
return result
end
function Snippet.str(self)
debug("Snippet.str(self)")
local res = self.code
for i = #self.locations, 1, -1 do
local loc = self.locations[i]
res = res:sub(0, loc.idx-1) .. loc.ph.value .. res:sub(loc.idx)
end
return res
end
function Snippet.findLocation(self, loc)
debug1("Snippet.findLocation(self, loc) loc = ",loc)
for i = 1, #self.locations do
if self.locations[i]:isWithin(loc) then
return self.locations[i]
end
end
return nil
end
function Snippet.remove(self)
debug("Snippet.remove(self)")
local endPos = self.startPos:Move(self:str():len(), self.view.Buf)
self.modText = true
self.view.Cursor:SetSelectionStart(self.startPos)
self.view.Cursor:SetSelectionEnd(endPos)
self.view.Cursor:DeleteSelection()
self.view.Cursor:ResetSelection()
self.modText = false
end
function Snippet.insert(self)
debug("Snippet.insert(self)")
self.modText = true
self.view.Buf:insert(self.startPos, self:str())
self.modText = false
end
function Snippet.focusNext(self)
debug("Snippet.focusNext(self)")
if self.focused == nil then
self.focused = 0
else
self.focused = (self.focused + 1) % #self.placeholders
end
local ph = self.placeholders[self.focused+1]
for i = 1, #self.locations do
if self.locations[i].ph == ph then
self.locations[i]:focus()
return
end
end
end
local function CursorWord(bp)
debug1("CursorWord(bp)",bp)
local c = bp.Cursor
local x = c.X-1 -- start one rune before the cursor
local result = ""
while x >= 0 do
local r = util.RuneStr(c:RuneUnder(x))
if (r == " ") then -- IsWordChar(r) then
break
else
result = r .. result
end
x = x-1
end
return result
end
local function ReadSnippets(filetype)
debug1("ReadSnippets(filetype)",filetype)
local snippets = {}
local allSnippetFiles = config.ListRuntimeFiles(RTSnippets)
local exists = false
for i = 1, #allSnippetFiles do
if allSnippetFiles[i] == filetype then
exists = true
break
end
end
if not exists then
micro.InfoBar():Error("No snippets file for \""..filetype.."\"")
return snippets
end
local snippetFile = config.ReadRuntimeFile(RTSnippets, filetype)
local curSnip = nil
local lineNo = 0
for line in string.gmatch(snippetFile, "(.-)\r?\n") do
lineNo = lineNo + 1
if string.match(line,"^#") then
-- comment
elseif line:match("^snippet") then
curSnip = Snippet.new()
for snipName in line:gmatch("%s(.+)") do -- %s space .+ one or more non-empty sequence
snippets[snipName] = curSnip
end
else
local codeLine = line:match("^\t(.*)$")
if codeLine ~= nil then
curSnip:AddCodeLine(codeLine)
elseif line ~= "" then
micro.InfoBar():Error("Invalid snippets file (Line #"..tostring(lineNo)..")")
end
end
end
debugt("ReadSnippets(filetype) snippets = ",snippets)
return snippets
end
-- Check filetype and load snippets
-- Return true is snippets loaded for filetype
-- Return false if no snippets loaded
local function EnsureSnippets(bp)
debug("EnsureSnippets()")
local filetype = bp.Buf.Settings["filetype"]
if curFileType ~= filetype then
snippets = ReadSnippets(filetype)
curFileType = filetype
end
if next(snippets) == nil then
return false
end
return true
end
function onBeforeTextEvent(sb, ev)
debug1("onBeforeTextEvent(ev)",ev)
if currentSnippet ~= nil and currentSnippet.view.Buf.SharedBuffer == sb then
if currentSnippet.modText then
-- text event from the snippet. simply ignore it...
return true
end
local locStart = nil
local locEnd = nil
if ev.Deltas[1].Start ~= nil and currentSnippet ~= nil then
locStart = currentSnippet:findLocation(ev.Deltas[1].Start:Move(1, currentSnippet.view.Buf))
locEnd = currentSnippet:findLocation(ev.Deltas[1].End)
end
if locStart ~= nil and ((locStart == locEnd) or (ev.Deltas[1].End.Y==0 and ev.Deltas[1].End.X==0)) then
if locStart:handleInput(ev) then
currentSnippet.view.Cursor:Goto(-ev.C)
return false
end
end
Accept()
end
return true
end
-- Insert snippet if found.
-- Pass in the name of the snippet to be inserted by command mode
-- No name passed in then it will check the text left of the cursor
function Insert(bp, args)
local snippetName = nil
if args ~= nil and #args > 0 then
snippetName = args[1]
end
debug1("Insert(snippetName)",snippetName)
local c = bp.Cursor
local buf = bp.Buf
local xy = buffer.Loc(c.X, c.Y)
-- check if a snippet name was passed in
local noArg = false
if not snippetName then
snippetName = CursorWord(bp)
noArg = true
end
-- check filetype and load snippets
local result = EnsureSnippets(bp)
-- if no snippets return early
if (result == false) then return end
-- curSn cloned into currentSnippet if snippet found
local curSn = snippets[snippetName]
if curSn then
currentSnippet = curSn:clone()
currentSnippet.view = bp
-- remove snippet keyword from micro buffer before inserting snippet
if noArg then
currentSnippet.startPos = xy:Move(-snippetName:len(), buf)
currentSnippet.modText = true
c:SetSelectionStart(currentSnippet.startPos)
c:SetSelectionEnd(xy)
c:DeleteSelection()
c:ResetSelection()
currentSnippet.modText = false
else
-- no need to remove snippet keyword from buffer as run from command mode
currentSnippet.startPos = xy
end
-- insert snippet to micro buffer
currentSnippet:insert()
micro.InfoBar():Message("Snippet Inserted \""..snippetName.."\"")
-- Placeholders
if #currentSnippet.placeholders == 0 then
local pos = currentSnippet.startPos:Move(currentSnippet:str():len(), bp.Buf)
while bp.Cursor:LessThan(pos) do
bp.Cursor:Right()
end
while bp.Cursor:GreaterThan(pos) do
bp.Cursor:Left()
end
else
currentSnippet:focusNext()
end
else
-- Snippet not found
micro.InfoBar():Message("Unknown snippet \""..snippetName.."\"")
end
end
function Next()
debug("Next()")
if currentSnippet then
currentSnippet:focusNext()
end
end
function Accept()
debug("Accept()")
currentSnippet = nil
end
function Cancel()
debug("Cancel()")
if currentSnippet then
currentSnippet:remove()
Accept()
end
end
local function StartsWith(String,Start)
debug1("StartsWith(String,Start) String ",String)
debug1("StartsWith(String,Start) start ",start)
String = String:upper()
Start = Start:upper()
return string.sub(String,1,string.len(Start))==Start
end
-- Used for auto complete in the command prompt
function findSnippet(input)
debug1("findSnippet(input)",input)
local result = {}
-- TODO: pass bp
EnsureSnippets()
for name,v in pairs(snippets) do
if StartsWith(name, input) then
table.insert(result, name)
end
end
return result
end
-- Debug functions below
-- debug1 is for logging functionName and 1 argument passed
function debug1(functionName, argument)
if debugflag == false then return end
if argument == nil then
micro.Log("snippets-plugin -> function " .. functionName .. " = nil")
elseif argument == "" then
micro.Log("snippets-plugin -> function " .. functionName .. " = empty string")
else micro.Log("snippets-plugin -> function " .. functionName .. " = " .. tostring(argument))
end
end
-- debug is for logging functionName only
function debug(functionName)
if debugflag == false then return end
micro.Log("snippets-plugin -> function " .. functionName)
end
-- debug is for logging functionName and table
function debugt(functionName,tablepassed)
if debugflag == false then return end
micro.Log("snippets-plugin -> function " .. functionName )
tprint(tablepassed)
-- if (tablepassed == nil) then return end
-- for key,value in pairs(tablepassed) do
-- micro.Log("key - " .. tostring(key) .. "value = " .. tostring(value[1]) )
-- end
end
-- dump table
function dump(o)
if type(o) == 'table' then
local s = '{ '
for k,v in pairs(o) do
if type(k) ~= 'number' then k = '"'..k..'"' end
s = s .. '['..k..'] = ' .. dump(v) .. ','
end
return s .. '} '
else
return tostring(o)
end
end
function tprint (tbl, indent)
if not indent then indent = 0 end
for k, v in pairs(tbl) do
formatting = string.rep(" ", indent) .. k .. ": "
if type(v) == "table" then
micro.Log(formatting .. "Table ->")
tprint(v, indent+1)
elseif type(v) == nil then
micro.Log(formatting .. " nil")
else
micro.Log(formatting .. tostring(v))
end
end
end
function checkTableisEmpty(myTable)
if next(myTable) == nil then
-- myTable is empty
end
end
function tablePrint(tbl)
for index = 1, #tbl do
micro.Log(tostring(index) .. " = " .. tostring(tbl[index]))
end
end
function init()
-- Insert a snippet
config.MakeCommand("snippetinsert", Insert, config.NoComplete)
-- Mark next placeholder
config.MakeCommand("snippetnext", Next, config.NoComplete)
-- Cancel current snippet (removes the text)
config.MakeCommand("snippetcancel", Cancel, config.NoComplete)
-- Acceptes snipped editing
config.MakeCommand("snippetaccept", Accept, config.NoComplete)
config.AddRuntimeFile("snippets", config.RTHelp, "help/snippets.md")
config.AddRuntimeFilesFromDirectory("snippets", RTSnippets, "snippets", "*.snippets")
config.TryBindKey("Alt-w", "lua:snippets.Next", false)
config.TryBindKey("Alt-a", "lua:snippets.Accept", false)
config.TryBindKey("Alt-s", "lua:snippets.Insert", false)
config.TryBindKey("Alt-d", "lua:snippets.Cancel", false)
end