1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
|
-- Copyright 2007-2008 Mitchell mitchell<att>caladbolg.net. See LICENSE.
---
-- Provides Lua-centric snippets for Textadept.
-- Snippets are basically pieces of text inserted into a document, but can
-- execute code, contain placeholders a user can enter in dynamic text for, and
-- make transformations on that text. This is much more powerful than standard
-- text templating.
-- There are several option variables used:
-- MARK_SNIPPET: The integer mark used to identify the line that marks the
-- end of a snippet.
-- MARK_SNIPPET_COLOR: The Scintilla color used for the line
-- that marks the end of the snippet.
--
module('_m.textadept.lsnippets', package.seeall)
-- Usage:
-- Snippets are defined in the global table 'snippets'. Keys in that table are
-- snippet trigger words, and values are the snippet's text to insert. The
-- exceptions are language names and style names. Language names have table
-- values of either snippets or style keys with table values of snippets.
-- See /lexers/lexer.lua for some default style names. Each lexer's 'add_style'
-- function adds additional styles, the string argument being the style's name.
-- For example:
-- snippets = {
-- file = '%(buffer.filename)',
-- lua = {
-- f = 'function %1(name)(%2(args))\n %0\nend',
-- string = { [string-specific snippets here] }
-- }
-- }
-- Style and lexer insensitive snippets should be placed in the lexer and
-- snippets tables respectively.
--
-- When searching for a snippet to expand in the snippets table, snippets in the
-- current style have priority, then the ones in the current lexer, and finally
-- the ones in the global table.
--
-- As mentioned, snippets are key-value pairs, the key being the trigger word
-- and the value being the snippet text: ['trigger'] = 'text'.
-- Snippet text however can contain more than just text.
--
-- Insert-time Lua and shell code: %(lua_code), `shell_code`
-- The code is executed the moment the snippet is inserted. For Lua code, the
-- result of the code execution is inserted, so print statements are useless.
-- All global variables and a 'selected_text' variable are available.
--
-- Tab stops/Mirrors: %num
-- These are visited in numeric order with %0 being the final position of the
-- caret, the end of the snippet if not specified. If there is a placeholder
-- (described below) with the specified num, its text is mirrored here.
--
-- Placeholders: %num(text)
-- These are also visited in numeric order, having precedence over tab stops,
-- and inserting the specified text. If no placeholder is available, the tab
-- stop is visited instead. The specified text can contain Lua code executed
-- at run-time: #(lua_code).
--
-- Transformations: %num(pattern|replacement)
-- These act like mirrors, but transform the text that would be inserted using
-- a given Lua pattern and replacement. The replacement can contain Lua code
-- executed at run-time: #(lua_code), as well as the standard Lua capture
-- sequences: %n where 1 <= n <= 9.
-- See the Lua documentation for using patterns and replacements.
--
-- To escape any of the special characters '%', '`', ')', '|', or '#', prepend
-- the standard Lua escape character '%'. Note:
-- * Only '`' needs to be escaped in shell code.
-- * '|'s after the first in transformations do not need to be escaped.
-- * Only unmatched ')'s need to be escaped. Nested ()s are ignored.
local MARK_SNIPPET = 4
local MARK_SNIPPET_COLOR = 0x4D9999
---
-- Global container that holds all snippet definitions.
-- @class table
-- @name snippets
_G.snippets = {}
_G.snippets.file = "%(buffer.filename)"
_G.snippets.path = "%((buffer.filename or ''):match('^.+/'))"
_G.snippets.tab = "%%%1(1)(%2(default))"
_G.snippets.key = "['%1'] = { %2(func)%3(, %4(arg)) }"
---
-- [Local table] The current snippet.
-- @class table
-- @name snippet
local snippet = {}
---
-- [Local table] The stack of currently running snippets.
-- @class table
-- @name snippet_stack
local snippet_stack = {}
-- Local functions.
local snippet_info, run_lua_code, handle_escapes, unhandle_escapes, unescape
---
-- Begins expansion of a snippet.
-- The text inserted has escape sequences handled.
-- @param s_text Optional snippet to expand. If none is specified, the snippet
-- is determined from the trigger word (left of the caret), lexer, and style.
function insert(s_text)
local buffer = buffer
local caret = buffer.current_pos
local lexer, style, start, s_name
if not s_text then
lexer = buffer:get_lexer_language()
style = buffer:get_style_name( buffer.style_at[caret] )
buffer:word_left_extend()
start = buffer.current_pos
s_name = buffer:get_sel_text()
end
if s_name then
local function try_get_snippet(...)
local table = _G.snippets
for _, idx in ipairs{...} do table = table[idx] end
return type(table) == 'string' and table or error()
end
local ret
ret, s_text = pcall(try_get_snippet, lexer, style, s_name)
if not ret then ret, s_text = pcall(try_get_snippet, lexer, s_name) end
if not ret then ret, s_text = pcall(try_get_snippet, s_name) end
if not ret then buffer:goto_pos(caret) end -- restore caret
end
if s_text then
buffer:begin_undo_action()
s_text = handle_escapes(s_text)
-- Execute Lua and shell code.
s_text = s_text:gsub('%%(%b())', run_lua_code)
s_text = s_text:gsub('`([^`]+)`',
function(code)
local p = io.popen(code)
local out = p:read('*all'):sub(1, -2)
p:close()
return out
end)
-- Initialize the new snippet. If one is running, push it onto the stack.
if snippet.index then snippet_stack[#snippet_stack + 1] = snippet end
snippet = {}
snippet.snapshots = {}
snippet.start_pos = start or caret
snippet.prev_sel_text = buffer:get_sel_text()
snippet.index, snippet.max_index = 0, 0
for i in s_text:gmatch('%%(%d+)') do
i = tonumber(i)
if i > snippet.max_index then snippet.max_index = i end
end
-- Insert the snippet and set a mark defining the end of it.
buffer:replace_sel(s_text) buffer:add_text('\n')
local line = buffer:line_from_position(buffer.current_pos)
snippet.end_marker = buffer:marker_add(line, MARK_SNIPPET)
buffer:marker_set_back(MARK_SNIPPET, MARK_SNIPPET_COLOR)
-- Indent all lines inserted.
buffer.current_pos = snippet.start_pos
local count = 0 for _ in s_text:gmatch('\n') do count = count + 1 end
if count > 0 then
local ref_line = buffer:line_from_position(start)
local isize, ibase = buffer.indent, buffer.line_indentation[ref_line]
local inum = ibase / isize -- number of indents needed to match
for i = 1, count do
local linei = buffer.line_indentation[ref_line + i]
buffer.line_indentation[ref_line + i] = linei + isize * inum
end
end
buffer:end_undo_action()
end
next()
end
---
-- If previously at a placeholder or tab stop, attempts to mirror and/or
-- transform the entered text at all appropriate mirrors before moving on to
-- the next placeholder or tab stop.
function next()
if not snippet.index then return end
local buffer = buffer
local s_start, s_end, s_text = snippet_info()
if not s_text then cancel_current() return end
local index = snippet.index
snippet.snapshots[index] = s_text
if index > 0 then
buffer:begin_undo_action()
local caret = math.max(buffer.anchor, buffer.current_pos)
local ph_text = buffer:text_range(snippet.ph_pos, caret)
-- Transform mirror.
s_text = s_text:gsub('%%'..index..'(%b())',
function(mirror)
local pattern, replacement = mirror:match('^%(([^|]+)|(.+)%)$')
if not pattern and not replacement then return ph_text end
return ph_text:gsub( unhandle_escapes(pattern),
function(...)
local arg = {...}
local repl = replacement:gsub('%%(%d)',
function(i) return arg[ tonumber(i) ] or '' end)
return repl:gsub('#(%b())', run_lua_code)
end )
end)
-- Regular mirror.
s_text = s_text:gsub('%%'..index, ph_text)
buffer:set_sel(s_start, s_end) buffer:replace_sel(s_text)
s_start, s_end = snippet_info()
buffer:end_undo_action()
end
buffer:begin_undo_action()
index = index + 1
if index <= snippet.max_index then
local s, e, next_item = s_text:find('%%'..index..'(%b())')
if next_item and not next_item:find('|') then -- placeholder
s, e = buffer:find('%'..index..next_item, 0, s_start)
next_item = next_item:gsub('#(%b())', run_lua_code)
next_item = unhandle_escapes( next_item:sub(2, -2) )
buffer:set_sel(s, e) buffer:replace_sel(next_item)
buffer:set_sel(s, s + #next_item)
else -- use the first mirror as a placeholder
s, e = buffer:find('%'..index..'[^(]', 2097152, s_start) -- regexp
if not s then snippet.index = index + 1 return next() end
buffer:set_sel(s, e - 1) buffer:replace_sel('')
end
snippet.ph_pos = s
snippet.index = index
else
s_text = unescape( unhandle_escapes( s_text:gsub('%%0', '%%__caret') ) )
buffer:set_sel(s_start, s_end) buffer:replace_sel(s_text)
s_start, s_end = snippet_info()
if s_end then buffer:goto_pos(s_end + 1) buffer:delete_back() end
local s, e = buffer:find('%__caret', 4, s_start)
if s and s <= s_end then buffer:set_sel(s, e) buffer:replace_sel('') end
buffer:marker_delete_handle(snippet.end_marker)
snippet = #snippet_stack > 0 and table.remove(snippet_stack) or {}
end
buffer:end_undo_action()
end
---
-- Goes back to the previous placeholder or tab stop, reverting changes made to
-- subsequent ones.
function prev()
if not snippet.index then return end
local buffer = buffer
local index = snippet.index
if index > 1 then
local s_start, s_end = snippet_info()
local s_text = snippet.snapshots[index - 2]
buffer:set_sel(s_start, s_end) buffer:replace_sel(s_text)
snippet.index = index - 2
next()
end
end
---
-- Cancels the active snippet, reverting to the state before its activation,
-- and restores the previous running snippet (if any).
function cancel_current()
if not snippet.index then return end
local buffer = buffer
local s_start, s_end = snippet_info()
buffer:begin_undo_action()
if s_start and s_end then
buffer:set_sel(s_start, s_end) buffer:replace_sel('')
s_start, s_end = snippet_info()
buffer:goto_pos(s_end + 1) buffer:delete_back()
end
if snippet.prev_sel_text then buffer:add_text(snippet.prev_sel_text) end
buffer:end_undo_action()
buffer:marker_delete_handle(snippet.end_marker)
snippet = #snippet_stack > 0 and table.remove(snippet_stack) or {}
end
---
-- Lists available snippets in an autocompletion list.
-- Global snippets and snippets in the current lexer and style are used.
function list()
local buffer = buffer
local list, list_str = {}, ''
local function add_snippets(snippets)
for s_name in pairs(snippets) do list[#list + 1] = s_name end
end
local snippets = _G.snippets
add_snippets(snippets)
local lexer = buffer:get_lexer_language()
local style = buffer:get_style_name( buffer.style_at[buffer.current_pos] )
if snippets[lexer] and type( snippets[lexer] ) == 'table' then
add_snippets( snippets[lexer] )
if snippets[lexer][style] then add_snippets( snippets[lexer][style] ) end
end
table.sort(list)
local sep = string.char(buffer.auto_c_separator)
for _, v in ipairs(list) do list_str = list_str..v..sep end
list_str = list_str:sub(1, -2)
local caret = buffer.current_pos
buffer:auto_c_show(caret - buffer:word_start_position(caret, true), list_str)
end
---
-- Shows the style at the current caret position in a call tip.
function show_style()
local buffer = buffer
local lexer = buffer:get_lexer_language()
local style_num = buffer.style_at[buffer.current_pos]
local style = buffer:get_style_name(style_num)
local text = 'Lexer: '..lexer..'\nStyle: '..style..' ('..style_num..')'
buffer:call_tip_show(buffer.current_pos, text)
end
---
-- [Local function] Gets the start position, end position, and text of the
-- currently running snippet.
-- @return start pos, end pos, and snippet text.
snippet_info = function()
local buffer = buffer
local s = snippet.start_pos
local e = buffer:position_from_line(
buffer:marker_line_from_handle(snippet.end_marker) ) - 1
if e >= s then return s, e, buffer:text_range(s, e) end
end
---
-- [Local function] Runs the given Lua code.
run_lua_code = function(code)
code = unhandle_escapes(code)
local env = setmetatable(
{ selected_text = buffer:get_sel_text() }, { __index = _G } )
local _, val = pcall( setfenv( loadstring('return '..code), env ) )
return val or ''
end
---
-- [Local function] Replaces escaped characters with their octal equivalents in
-- a given string.
-- '%%' is the escape character used.
handle_escapes = function(s)
return s:gsub('%%([%%`%)|#])',
function(char) return ("\\%03d"):format( char:byte() ) end)
end
---
-- [Local function] Replaces octal characters with their escaped equivalents in
-- a given string.
unhandle_escapes = function(s)
return s:gsub('\\(%d%d%d)',
function(value) return '%'..string.char(value) end)
end
---
-- [Local function] Replaces escaped characters with the actual characters in a
-- given string.
-- This is used when escape sequences are no longer needed.
unescape = function(s) return s:gsub('%%([%%`%)|#])', '%1') end
|