snippets.lua 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. VERSION = "0.1.1"
  2. local curFileType = ""
  3. local snippets = {}
  4. local currentSnippet = nil
  5. local Location = {}
  6. Location.__index = Location
  7. local Snippet = {}
  8. Snippet.__index = Snippet
  9. function Location.new(idx, ph, snip)
  10. local self = setmetatable({}, Location)
  11. self.idx = idx
  12. self.ph = ph
  13. self.snippet = snip
  14. return self
  15. end
  16. -- offset of the location relative to the snippet start
  17. function Location.offset(self)
  18. local add = 0
  19. for i = 1, #self.snippet.locations do
  20. local loc = self.snippet.locations[i]
  21. if loc == self then
  22. break
  23. end
  24. local val = loc.ph.value
  25. if val then
  26. add = add + val:len()
  27. end
  28. end
  29. return self.idx+add
  30. end
  31. function Location.startPos(self)
  32. local loc = self.snippet.startPos
  33. return loc:Move(self:offset(), self.snippet.view.buf)
  34. end
  35. -- returns the length of the location (but at least 1)
  36. function Location.len(self)
  37. local len = 0
  38. if self.ph.value then
  39. len = self.ph.value:len()
  40. end
  41. if len <= 0 then
  42. len = 1
  43. end
  44. return len
  45. end
  46. function Location.endPos(self)
  47. local start = self:startPos()
  48. return start:Move(self:len(), self.snippet.view.buf)
  49. end
  50. -- check if the given loc is within the location
  51. function Location.isWithin(self, loc)
  52. return loc:GreaterEqual(self:startPos()) and loc:LessEqual(self:endPos())
  53. end
  54. function Location.focus(self)
  55. local view = self.snippet.view
  56. local startP = self:startPos():Move(-1, view.Buf)
  57. local endP = self:endPos():Move(-1, view.Buf)
  58. while view.Cursor:LessThan(startP) do
  59. view.Cursor:Right()
  60. end
  61. while view.Cursor:GreaterThan(endP) do
  62. view.Cursor:Left()
  63. end
  64. if self.ph.value:len() > 0 then
  65. view.Cursor:SetSelectionStart(startP)
  66. view.Cursor:SetSelectionEnd(endP)
  67. else
  68. view.Cursor:ResetSelection()
  69. end
  70. end
  71. function Location.handleInput(self, ev)
  72. if ev.EventType == 1 then
  73. -- TextInput
  74. if ev.Text == "\n" then
  75. Accept()
  76. return false
  77. else
  78. local offset = 1
  79. local sp = self:startPos()
  80. while sp:LessEqual(-ev.Start) do
  81. sp = sp:Move(1, self.snippet.view.Buf)
  82. offset = offset + 1
  83. end
  84. self.snippet:remove()
  85. local v = self.ph.value
  86. if v == nil then
  87. v = ""
  88. end
  89. self.ph.value = v:sub(0, offset-1) .. ev.Text .. v:sub(offset)
  90. self.snippet:insert()
  91. return true
  92. end
  93. elseif ev.EventType == -1 then
  94. -- TextRemove
  95. local offset = 1
  96. local sp = self:startPos()
  97. while sp:LessEqual(-ev.Start) do
  98. sp = sp:Move(1, self.snippet.view.Buf)
  99. offset = offset + 1
  100. end
  101. if ev.Start.Y ~= ev.End.Y then
  102. return false
  103. end
  104. self.snippet:remove()
  105. local v = self.ph.value
  106. if v == nil then
  107. v = ""
  108. end
  109. local len = ev.End.X - ev.Start.X
  110. self.ph.value = v:sub(0, offset-1) .. v:sub(offset+len)
  111. self.snippet:insert()
  112. return true
  113. end
  114. return false
  115. end
  116. function Snippet.new()
  117. local self = setmetatable({}, Snippet)
  118. self.code = ""
  119. return self
  120. end
  121. function Snippet.AddCodeLine(self, line)
  122. if self.code ~= "" then
  123. self.code = self.code .. "\n"
  124. end
  125. self.code = self.code .. line
  126. end
  127. function Snippet.Prepare(self)
  128. if not self.placeholders then
  129. self.placeholders = {}
  130. self.locations = {}
  131. local count = 0
  132. local pattern = "${(%d+):?([^}]*)}"
  133. while true do
  134. local num, value = self.code:match(pattern)
  135. if not num then
  136. break
  137. end
  138. count = count+1
  139. num = tonumber(num)
  140. local idx = self.code:find(pattern)
  141. self.code = self.code:gsub(pattern, "", 1)
  142. local p = self.placeholders[num]
  143. if not p then
  144. p = {num = num}
  145. self.placeholders[#self.placeholders+1] = p
  146. end
  147. self.locations[#self.locations+1] = Location.new(idx, p, self)
  148. if value then
  149. p.value = value
  150. end
  151. end
  152. end
  153. end
  154. function Snippet.clone(self)
  155. local result = Snippet.new()
  156. result:AddCodeLine(self.code)
  157. result:Prepare()
  158. return result
  159. end
  160. function Snippet.str(self)
  161. local res = self.code
  162. for i = #self.locations, 1, -1 do
  163. local loc = self.locations[i]
  164. res = res:sub(0, loc.idx-1) .. loc.ph.value .. res:sub(loc.idx)
  165. end
  166. return res
  167. end
  168. function Snippet.findLocation(self, loc)
  169. for i = 1, #self.locations do
  170. if self.locations[i]:isWithin(loc) then
  171. return self.locations[i]
  172. end
  173. end
  174. return nil
  175. end
  176. function Snippet.remove(self)
  177. local endPos = self.startPos:Move(self:str():len(), self.view.Buf)
  178. self.modText = true
  179. self.view.Cursor:SetSelectionStart(self.startPos)
  180. self.view.Cursor:SetSelectionEnd(endPos)
  181. self.view.Cursor:DeleteSelection()
  182. self.view.Cursor:ResetSelection()
  183. self.modText = false
  184. end
  185. function Snippet.insert(self)
  186. self.modText = true
  187. self.view.Buf:insert(self.startPos, self:str())
  188. self.modText = false
  189. end
  190. function Snippet.focusNext(self)
  191. if self.focused == nil then
  192. self.focused = 0
  193. else
  194. self.focused = (self.focused + 1) % #self.placeholders
  195. end
  196. local ph = self.placeholders[self.focused+1]
  197. for i = 1, #self.locations do
  198. if self.locations[i].ph == ph then
  199. self.locations[i]:focus()
  200. return
  201. end
  202. end
  203. end
  204. local function CursorWord(v)
  205. local c = v.Cursor
  206. local x = c.X-1 -- start one rune before the cursor
  207. local result = ""
  208. while x >= 0 do
  209. local r = RuneStr(c:RuneUnder(x))
  210. if IsWordChar(r) then
  211. result = r .. result
  212. else
  213. break
  214. end
  215. x = x-1
  216. end
  217. return result
  218. end
  219. local function ReadSnippets(filetype)
  220. local snippets = {}
  221. local allSnippetFiles = ListRuntimeFiles("snippets")
  222. local exists = false
  223. for i = 1, #allSnippetFiles do
  224. if allSnippetFiles[i] == filetype then
  225. exists = true
  226. break
  227. end
  228. end
  229. if not exists then
  230. messenger:Error("No snippets file for \""..filetype.."\"")
  231. return snippets
  232. end
  233. local snippetFile = ReadRuntimeFile("snippets", filetype)
  234. local curSnip = nil
  235. local lineNo = 0
  236. for line in string.gmatch(snippetFile, "(.-)\r?\n") do
  237. lineNo = lineNo + 1
  238. if string.match(line,"^#") then
  239. -- comment
  240. elseif line:match("^snippet") then
  241. curSnip = Snippet.new()
  242. for snipName in line:gmatch("%s(%a+)") do
  243. snippets[snipName] = curSnip
  244. end
  245. else
  246. local codeLine = line:match("^\t(.*)$")
  247. if codeLine ~= nil then
  248. curSnip:AddCodeLine(codeLine)
  249. elseif line ~= "" then
  250. messenger:Error("Invalid snippets file (Line #"..tostring(lineNo)..")")
  251. end
  252. end
  253. end
  254. return snippets
  255. end
  256. local function EnsureSnippets()
  257. local filetype = GetOption("filetype")
  258. if curFileType ~= filetype then
  259. snippets = ReadSnippets(filetype)
  260. curFileType = filetype
  261. end
  262. end
  263. function onBeforeTextEvent(ev)
  264. if currentSnippet ~= nil and currentSnippet.view == CurView() then
  265. if currentSnippet.modText then
  266. -- text event from the snippet. simply ignore it...
  267. return true
  268. end
  269. local locStart = currentSnippet:findLocation(ev.Start:Move(1, CurView().Buf))
  270. local locEnd = currentSnippet:findLocation(ev.End)
  271. if locStart ~= nil and ((locStart == locEnd) or (ev.End.Y==0 and ev.End.X==0)) then
  272. if locStart:handleInput(ev) then
  273. CurView().Cursor:Goto(-ev.C)
  274. return false
  275. end
  276. end
  277. Accept()
  278. end
  279. return true
  280. end
  281. function Insert(name)
  282. local v = CurView()
  283. local c = v.Cursor
  284. local buf = v.Buf
  285. local xy = Loc(c.X, c.Y)
  286. local noArg = false
  287. if not name then
  288. name = CursorWord(v)
  289. noArg = true
  290. end
  291. EnsureSnippets()
  292. local curSn = snippets[name]
  293. if curSn then
  294. currentSnippet = curSn:clone()
  295. currentSnippet.view = v
  296. if noArg then
  297. currentSnippet.startPos = xy:Move(-name:len(), buf)
  298. currentSnippet.modText = true
  299. c:SetSelectionStart(currentSnippet.startPos)
  300. c:SetSelectionEnd(xy)
  301. c:DeleteSelection()
  302. c:ResetSelection()
  303. currentSnippet.modText = false
  304. else
  305. currentSnippet.startPos = xy
  306. end
  307. currentSnippet:insert()
  308. if #currentSnippet.placeholders == 0 then
  309. local pos = currentSnippet.startPos:Move(currentSnippet:str():len(), v.Buf)
  310. while v.Cursor:LessThan(pos) do
  311. v.Cursor:Right()
  312. end
  313. while v.Cursor:GreaterThan(pos) do
  314. v.Cursor:Left()
  315. end
  316. else
  317. currentSnippet:focusNext()
  318. end
  319. else
  320. messenger:Message("Unknown snippet \""..name.."\"")
  321. end
  322. end
  323. function Next()
  324. if currentSnippet then
  325. currentSnippet:focusNext()
  326. end
  327. end
  328. function Accept()
  329. currentSnippet = nil
  330. end
  331. function Cancel()
  332. if currentSnippet then
  333. currentSnippet:remove()
  334. Accept()
  335. end
  336. end
  337. local function StartsWith(String,Start)
  338. String = String:upper()
  339. Start = Start:upper()
  340. return string.sub(String,1,string.len(Start))==Start
  341. end
  342. function findSnippet(input)
  343. local result = {}
  344. EnsureSnippets()
  345. for name,v in pairs(snippets) do
  346. if StartsWith(name, input) then
  347. table.insert(result, name)
  348. end
  349. end
  350. return result
  351. end
  352. -- Insert a snippet
  353. MakeCommand("snippetinsert", "snippets.Insert", MakeCompletion("snippets.findSnippet"), 0)
  354. -- Mark next placeholder
  355. MakeCommand("snippetnext", "snippets.Next", 0)
  356. -- Cancel current snippet (removes the text)
  357. MakeCommand("snippetcancel", "snippets.Cancel", 0)
  358. -- Acceptes snipped editing
  359. MakeCommand("snippetaccept", "snippets.Accept", 0)
  360. AddRuntimeFile("snippets", "help", "help/snippets.md")
  361. AddRuntimeFilesFromDirectory("snippets", "snippets", "snippets", "*.snippets")
  362. BindKey("Alt-w", "snippets.Next")
  363. BindKey("Alt-a", "snippets.Accept")
  364. BindKey("Alt-s", "snippets.Insert")
  365. BindKey("Alt-d", "snippets.Cancel")