586 lines
14 KiB
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
|