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
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
|
-- Copyright 2007-2010 Mitchell mitchell<att>caladbolg.net. See LICENSE.
local L = _G.locale.localize
---
-- Provides Lua-style snippets for Textadept.
module('_m.textadept.snippets', package.seeall)
-- Markdown:
-- ## Overview
--
-- Snippets are basically pieces of text inserted into a document, but can
-- execute code, contain placeholders you can enter dynamic text for, and
-- perform transformations on that text. This is much more powerful than
-- standard text templating.
--
-- Snippets are defined in the global table `snippets`. Each key-value pair in
-- `snippets` consist of either:
--
-- * A string snippet trigger word and its expanded text.
-- * A string language name and its associated `snippets`-like table.
-- * A string style name and its associated `snippets`-like table.
--
-- Language names are the names of the lexer files in `lexers/` such as `cpp`
-- and `lua`. Style names are different lexer styles, most of which are in
-- `lexers/lexer.lua`; examples are `whitespace`, `comment`, and `string`.
--
-- Snippet text should contain spaces instead of tabs since Textadept
-- automatically converts spaces to tabs depending on the current settings.
--
-- ## Settings
--
-- * `MARK_SNIPPET`: The unique integer mark used to identify the line that
-- marks the end of a snippet.
-- * `MARK_SNIPPET_COLOR`: The [Scintilla color][scintilla_color] used for the
-- line that marks the end of the snippet.
--
-- [scintilla_color]: http://scintilla.org/ScintillaDoc.html#colour
--
-- ## Snippet Precedence
--
-- When searching for a snippet to expand in the `snippets` table, snippets in
-- the current style have priority, followed by the ones in the current lexer,
-- and finally the ones in the global table.
--
-- ## Snippet Syntax
--
-- A snippet to insert may contain any of the following:
--
-- #### Plain Text
--
-- Any plain text characters may be used with the exception of `%` and `.
-- These are special characters and must be "escaped" by prefixing one with a
-- `%`. As an example, `%%` inserts a single `%` in the snippet.
--
-- #### Lua and Shell Code
--
-- %(lua_code)
-- `shell_code`
--
-- The code is executed the moment the snippet is inserted.
--
-- For Lua code, the global Lua state is available as well as a `selected_text`
-- variable (containing the current selection in the buffer) for convenience.
-- Only the return value of the code execution is inserted, not standard out.
-- Therefore any `print()` statements are meaningless.
--
-- Shell code is run via Lua's [`io.popen()`][io_popen].
--
-- [io_popen]: http://www.lua.org/manual/5.1/manual.html#pdf-io.popen
--
-- #### Tab Stops and Mirrors
--
-- %num
--
-- These are visited in numeric order (1, 2, 3, etc.) with %0 being the final
-- position of the caret, or the end of the snippet if %0 is 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, but have precedence over tab stops,
-- and insert the specified `text` at the current position upon entry. `text`
-- can contain Lua code executed at run-time:
--
-- %num(#(lua_code))
--
-- The global Lua state is available as well as a `selected_text` variable
-- (containing the current selection in the buffer) for convenience.
--
-- `#`'s will have to be escaped with `%` for plain text. Any mis-matched `)`'s
-- must also be escaped, but balanced `()`'s need not be.
--
-- #### Transformations
--
-- %num(pattern|replacement)
--
-- These act like mirrors, but transform the text that would be inserted using
-- a given [Lua pattern](../manual/14_Appendix.html#lua_patterns) and
-- replacement. Like in placeholders, `replacement` can contain Lua code
-- executed at run-time as well as the standard Lua capture (`%n`) sequences.
-- Any `|`'s after the first one do not need to be escaped.
--
-- ## Example
--
-- snippets = {
-- file = '%(buffer.filename)',
-- lua = {
-- f = 'function %1(name)(%2(args))\n %0\nend',
-- string = { [string-specific snippets here] }
-- }
-- }
--
-- The first snippet is global and runs the Lua code to determine the current
-- buffer's filename and inserts it. The other snippets apply only in the `lua`
-- lexer. Any snippets in the `string` table are available only when the current
-- style is `string` in the `lua` lexer.
-- settings
MARK_SNIPPET = 4
MARK_SNIPPET_COLOR = 0x4D9999
-- end settings
file = "%(buffer.filename)"
path = "%((buffer.filename or ''):match('^.+/'))"
tab = "%%%1(1)(%2(default))"
key = "['%1'] = { %2(func)%3(, %4(arg)) }"
-- The current snippet.
local snippet = {}
-- The stack of currently running snippets.
local snippet_stack = {}
-- Replaces escaped characters with their octal equivalents in a given string.
-- @param s The string to handle escapes in.
-- @return string with escapes handled.
local function handle_escapes(s)
return s:gsub('%%([%%`%)|#])',
function(char) return ("\\%03d"):format(char:byte()) end)
end
-- Replaces octal characters with their escaped equivalents in a given string.
-- @param s The string to unhandle escapes in.
-- @return string with escapes unhandled.
local function unhandle_escapes(s)
local char = string.char
return s:gsub('\\(%d%d%d)', function(byte) return '%'..char(byte) end)
end
-- Replaces escaped characters with the actual characters in a given string.
-- This is used when escape sequences are no longer needed.
-- @param s The string to unescape escapes in.
-- @return string with escapes unescaped.
local function unescape(s) return s:gsub('%%([%%`%)|#])', '%1') end
-- Gets the start position, end position, and text of the currently running
-- snippet.
-- @return start pos, end pos, and snippet text.
local function snippet_info()
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
-- Runs the given Lua code.
-- @param code The Lua code to run.
-- @return string result from the code run.
local function run_lua_code(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
-- 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.
-- @return false if no snippet was expanded; nil otherwise
local function next_tab_stop()
if not snippet.index then return false end -- no snippet active
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 = buffer.selection_end
local ph_text = buffer:text_range(snippet.ph_pos, caret)
-- Transform mirror.
local function transform_mirror(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, 1)
end
s_text = s_text:gsub('%%'..index..'(%b())', transform_mirror)
-- Regular mirror.
s_text = s_text:gsub('()%%'..index,
function(pos)
for mirror, e in s_text:gmatch('%%%d+(%b())()') do
local s = mirror:find('|')
-- If inside transform, do not do anything.
if s and pos > s and pos < e then return nil end
end
return ph_text
end)
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
-- Find the next tab stop.
local s, e, next_item
repeat -- ignore replacement mirrors
s, e, next_item = s_text:find('%%'..index..'(%b())', e)
until not s or next_item and not next_item:find('|')
if next_item then -- placeholder
buffer.target_start, buffer.target_end = s_start, buffer.length
buffer.search_flags = 0
buffer:search_in_target('%'..index..next_item)
next_item = next_item:gsub('#(%b())', run_lua_code)
next_item = unhandle_escapes(next_item:sub(2, -2))
buffer:replace_target(next_item)
buffer:set_sel(buffer.target_start, buffer.target_start + #next_item)
snippet.ph_pos = buffer.target_start
else
repeat -- ignore placeholders
local found = true
s, e = (s_text..' '):find('%%'..index..'[^(]', e)
if not s then
snippet.index = index + 1
next_tab_stop()
return
end
for p_s, p_e in s_text:gmatch('%%%d+()%b()()') do
if s > p_s and s < p_e then
found = false
break
end
end
until found
buffer:set_sel(s_start + s - 1, s_start + e - 1)
buffer:replace_sel('') -- replace_target() doesn't place caret
snippet.ph_pos = s_start + s - 1
end
-- Place additional carets at mirrors.
local _, _, text = snippet_info()
text = text:gsub('(%%%d+%b())',
function(mirror)
-- Lua code in replacement mirrors may contain '%'
-- sequences; do not treat as mirrors
if mirror:find('|') then
return string.rep('_', #mirror)
end
end)
for s, e in text:gmatch('()%%'..index..'()[^(]') do
buffer:add_selection(s_start + s - 1, s_start + e - 1)
end
buffer.main_selection = 0 -- original placeholder/mirror
-- Done.
snippet.index = index
else
-- Finished. Find '%0' and place the caret there.
s_text = unescape(unhandle_escapes(s_text))
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 = s_text:find('%%0')
if s and e then
buffer:set_sel(s_start + s - 1, s_start + 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
---
-- 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.
-- @return false if no snippet was expanded; true otherwise.
function _insert(s_text)
local buffer = buffer
local anchor, caret = buffer.anchor, buffer.current_pos
local lexer, style, start, s_name
if not s_text then
lexer = buffer:get_lexer()
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:set_sel(anchor, caret) end -- restore caret
end
if s_text then
buffer:begin_undo_action()
s_text = handle_escapes(s_text)
-- Take into account tab settings.
if not buffer.use_tabs then
s_text = s_text:gsub('\t', string.rep(' ', buffer.tab_width))
end
-- 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:gsub('(%%%d+)%b()', '%1'):gmatch('%%(%d+)') do
-- placeholders may contain Lua code that has %n sequences that mess up
-- this calculation; the above gsub accounts for this
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
return next_tab_stop() ~= false
end
---
-- Goes back to the previous placeholder or tab stop, reverting changes made to
-- subsequent ones.
-- @return false if no snippet is active; nil otherwise
function _prev()
if not snippet.index then return false end -- no snippet active
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_tab_stop()
else
_cancel_current()
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 = {}
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()
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 caret = buffer.current_pos
buffer:auto_c_show(caret - buffer:word_start_position(caret, true),
table.concat(list, string.char(buffer.auto_c_separator)))
end
---
-- Shows the style at the current caret position in a call tip.
function _show_style()
local buffer = buffer
local lexer = buffer:get_lexer()
local style_num = buffer.style_at[buffer.current_pos]
local style = buffer:get_style_name(style_num)
local text = string.format("%s %s\n%s %s (%d)", L('Lexer'), lexer, L('Style'),
style, style_num)
buffer:call_tip_show(buffer.current_pos, text)
end
---
-- Provides access to snippets from _G.
-- @class table
-- @name _G.snippets
_G.snippets = _M
|