aboutsummaryrefslogtreecommitdiffhomepage
path: root/modules/textadept/snippets.lua
blob: 3763ab7ca07bce8d8f4bbde68dd42a3dcd0f7e2c (plain)
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
-- Copyright 2007-2011 Mitchell mitchell<att>caladbolg.net. See LICENSE.

local L = locale.localize

---
-- Provides Lua-style snippets for Textadept.
module('_m.textadept.snippets', package.seeall)

-- Markdown:
-- ## Overview
--
-- Snippets are dynamic pieces of text inserted into a document that contain
-- placeholders for further input, mirror or transform that input, and execute
-- code.
--
-- Snippets are defined in the global table `snippets`. Each key-value pair in
-- `snippets` consists of either:
--
-- * A string snippet trigger word and its expanded text.
-- * A string lexer language name and its associated `snippets`-like table.
--
-- Language names are the names of the lexer files in `lexers/` such as `cpp`
-- and `lua`.
--
-- By default, the `Tab` key expands a snippet and tabs through placeholders
-- while `Shift+Tab` tabs backwards through them. Snippets can also be expanded
-- inside one another.
--
-- ## Snippet Precedence
--
-- When searching for a snippet to expand in the `snippets` table, snippets in
-- the current lexer have priority, followed by 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 `%` followed
-- immediately by a digit (`0`-`9`), `(`, `)`, `>`, or `]` character. These are
-- "escape sequences" for the more complicated features of snippets. If you want
-- to use `%` followed by one of the before-mentioned characters, prepend
-- another `%` to the first `%`. For example, `%%>` in the snippet inserts a
-- literal `%>` into the document.
--
-- #### Placeholders
--
-- Textadept's snippets provide a number of different placeholders. The simplest
-- ones are of the form
--
--     %num
--
-- where `num` is a number. Placeholders are visited in numeric order (1, 2, 3,
-- etc.) with the `Tab` key after the snippet is inserted and can be used to
-- enter in additional text. When no more placeholders are left, the caret is
-- placed at either the end of the snippet or the `%0` placeholder if it exists.
--
-- A placeholder can specify default text. It is of the form
--
--     %num(default text)
--
-- where, again, `num` is a number. These kinds of placeholders take precedence
-- over the simpler placeholders described above. If a snippet contains more
-- than one placeholder with the same `num`, the one containing default text is
-- visited first and the others become _mirrors_. Mirrors simply mirror the text
-- typed into the current placeholder.
--
-- The last kind of placeholder executes either Lua or Shell code.
--
--     %<lua_code>
--     %num<lua_code>
--     %[shell_code]
--     %num[shell_code]
--
-- For placeholders that omit `num`, their code is executed the moment the
-- snippet is inserted. Otherwise the code is executed as placeholders are
-- visited.
--
-- For Lua code, the global Lua state is available as well as a `selected_text`
-- variable (containing the current selection in the buffer). After execution,
-- the placeholder contains the return value of the code that was run.
--
-- Shell code is executed using Lua's [`io.popen()`][io_popen] which reads from
-- the process' standard output (STDOUT). After execution, the placeholder will
-- contain the STDOUT of the process.
--
-- [io_popen]: http://www.lua.org/manual/5.1/manual.html#pdf-io.popen
--
-- These kinds of placeholders can be used to transform mirrored text. For
-- example, `%2<([[%1]]):gsub('^.', function(c) return c:upper() end)>` will
-- capitalize a mirrored `%1` placeholder.
--
-- ##### Important Note
--
-- It is very important that any `%`, `(`, `)`, `>`, or `]` characters
-- **within** placeholders be escaped with a `%` as necessary. Otherwise,
-- unexpected results will occur. `%`s only need to be escaped if they are
-- proceeded by a digit, `(`s and `)`s only need to be escaped directly after a
-- %num sequence or inside default text placeholders **if and only if** there is
-- no matching parenthesis (thus, nested parentheses do not need to be escaped),
-- `]`s only need to be escaped inside Shell code placeholders, and `>`s only
-- need to be escaped inside Lua code placeholders.
--
-- ## Example
--
--     snippets.snippet = 'snippets.%1 = \'%0\''
--     snippets.file = '%<buffer.filename>'
--     snippets.lua = {
--       f = 'function %1(name)(%2(args))\n\t%0\nend'
--     }
--
-- The first two snippets are global. The first is quite simple to understand.
-- The second runs Lua code to determine the current buffer's filename and
-- inserts it. The last snippet expands only when editing Lua code.
--
-- It is recommended to use tab characters instead of spaces like in the last
-- example. Tabs will be converted to spaces as necessary.

-- The stack of currently running snippets.
local snippet_stack = {}

-- Contains newline sequences for `buffer.eol_mode`.
-- This table is used by `new_snippet()`.
-- @class table
-- @name newlines
local newlines = { [0] = '\r\n', '\r', '\n' }

local INDIC_SNIPPET = _SCINTILLA.next_indic_number()

-- Inserts a new snippet.
-- @param text The new snippet to insert.
-- @param trigger The trigger text used to expand the snippet, if any.
local function new_snippet(text, trigger)
  local buffer = buffer
  local snippet = setmetatable({
    trigger = trigger,
    original_sel_text = buffer:get_sel_text(),
    snapshots = {}
  }, { __index = _snippet_mt })
  snippet_stack[#snippet_stack + 1] = snippet

  -- Convert and match indentation.
  local lines = {}
  local indent = { [true] = '\t', [false] = (' '):rep(buffer.tab_width) }
  local use_tabs = buffer.use_tabs
  for line in (text..'\n'):gmatch('([^\r\n]*)\r?\n') do
    lines[#lines + 1] = line:gsub('^(%s*)', function(indentation)
      return indentation:gsub(indent[not use_tabs], indent[use_tabs])
    end)
  end
  if #lines > 1 then
    -- Match indentation on all lines after the first.
    local indent_size = #buffer:get_cur_line():match('^%s*')
    if not use_tabs then indent_size = indent_size / buffer.indent end
    local additional_indent = indent[use_tabs]:rep(indent_size)
    for i = 2, #lines do lines[i] = additional_indent..lines[i] end
  end
  text = table.concat(lines, newlines[buffer.eol_mode])

  -- Insert the snippet and its mark into the buffer.
  buffer:target_from_selection()
  if trigger then buffer.target_start = buffer.current_pos - #trigger end
  snippet.start_position = buffer.target_start
  buffer:replace_target(text..' ')
  buffer.indicator_current = INDIC_SNIPPET
  buffer:indicator_fill_range(buffer.target_end - 1, 1)

  snippet:execute_code('')
  return snippet
end

---
-- Inserts a snippet.
-- @param text Optional snippet text. If none is specified, the snippet text
--   is determined from the trigger and lexer.
-- @return `false` if no snippet was expanded; `true` otherwise.
function _insert(text)
  local buffer = buffer
  local trigger
  if not text then
    local lexer = buffer:get_lexer(true)
    trigger = buffer:text_range(buffer:word_start_position(buffer.current_pos),
                                buffer.current_pos)
    local snip = snippets
    text = snip[trigger]
    if type(snip) == 'table' and snip[lexer] then snip = snip[lexer] end
    text = snip[trigger] or text
  end
  local snippet = snippet_stack[#snippet_stack]
  if type(text) == 'string' then snippet = new_snippet(text, trigger) end
  if not snippet then return false end
  snippet:next()
end

---
-- Goes back to the previous placeholder, reverting any changes from the current
-- one.
-- @return `false` if no snippet is active; `nil` otherwise.
function _previous()
  if #snippet_stack == 0 then return false end
  snippet_stack[#snippet_stack]:previous()
end

---
-- Cancels the active snippet, reverting to the state before its activation, and
-- restores the previously running snippet (if any).
function _cancel_current()
  if #snippet_stack > 0 then snippet_stack[#snippet_stack]:cancel() end
end

---
-- Prompts the user to select a snippet to insert from a filtered list dialog.
-- Global snippets and snippets in the current lexer are shown.
function _select()
  local list = {}
  local type = type
  for trigger, text in pairs(snippets) do
    if type(text) == 'string' and
       trigger ~= '_NAME' and trigger ~= '_PACKAGE' then
      list[#list + 1] = trigger..'\0global\0'..text
    end
  end
  local lexer = buffer:get_lexer()
  for trigger, text in pairs(snippets[lexer] or {}) do
    if type(text) == 'string' then
      list[#list + 1] = trigger..'\0'..lexer..'\0'..text
    end
  end
  table.sort(list)
  local t = {}
  for i = 1, #list do
    t[#t + 1], t[#t + 2], t[#t + 3] = list[i]:match('^(%Z+)%z(%Z+)%z(%Z+)$')
  end
  local i = gui.filteredlist(L('Select Snippet'),
                             { L('Trigger'), L('Scope'), L('Snippet Text') },
                             t, true, '--output-column', '2')
  if i then _insert(t[(i + 1) * 3]) end
end

-- Table of escape sequences.
-- @class table
-- @name escapes
local escapes = {
  ['%%'] = '\027\027', ['\027\027'] = '%%',
  ['%('] = '\027\017', ['\027\017'] = '%(',
  ['%)'] = '\027\018', ['\027\018'] = '%)',
  ['%>'] = '\027\019', ['\027\019'] = '%>',
  ['%]'] = '\027\020', ['\027\020'] = '%]'
}

-- Metatable for a snippet object.
-- @class table
-- @name _snippet_mt
_snippet_mt = {
  -- Gets a snippet's end position in the buffer.
  -- @param snippet The snippet returned by `new_snippet()`.
  get_end_position = function(snippet)
    local e = buffer:indicator_end(INDIC_SNIPPET, snippet.start_position + 1)
    if e == 0 then e = snippet.start_position end
    return e
  end,

  -- Gets the text for a snippet.
  -- @param snippet The snippet returned by `new_snippet()`.
  get_text = function(snippet)
    local s, e = snippet.start_position, snippet:get_end_position()
    local ok, text = pcall(buffer.text_range, buffer, s, e)
    return ok and text or ''
  end,

  -- Sets the text for a snippet.
  -- This text will be displayed immediately in the buffer.
  -- @param snippet The snippet returned by `new_snippet()`.
  -- @param text The snippet's text.
  set_text = function(snippet, text)
    local buffer = buffer
    buffer.target_start = snippet.start_position
    buffer.target_end = snippet:get_end_position()
    buffer:replace_target(text)
  end,

  -- Returns the escaped form of the snippet's text.
  -- @param snippet The snippet returned by `new_snippet()`.
  -- @see escapes
  get_escaped_text = function(snippet)
    return snippet:get_text():gsub('%%[%%%(%)>%]]', escapes)
  end,

  -- Returns the unescaped form of the given text.
  -- This does the opposite of `get_escaped_text()` by default. The behaviour is
  -- slightly different when `complete` true.
  -- @param text Text to unescape.
  -- @param complete Flag indicating whether or not to also remove the extra
  --   escape character '%'. Defaults to `false`.
  unescape_text = function(text, complete)
    text = text:gsub('\027.', escapes)
    return complete and text:gsub('%%([%%%(%)>%]])', '%1') or text
  end,

  -- Executes code in the snippet for the given index.
  -- @param snippet The snippet returned by `new_snippet()`.
  -- @param index Execute code with this index.
  execute_code = function(snippet, index)
    local escaped_text = snippet:get_escaped_text()
    -- Lua code.
    escaped_text = escaped_text:gsub('%%'..index..'<([^>]*)>', function(code)
      local env = setmetatable({ selected_text = snippet.original_sel_text },
                               { __index = _G })
      local f, result = loadstring('return '..snippet.unescape_text(code, true))
      if f then f, result = pcall(setfenv(f, env)) end
      return result or ''
    end)
    -- Shell code.
    escaped_text = escaped_text:gsub('%%'..index..'%[([^%]]*)%]', function(code)
      local p = io.popen(snippet.unescape_text(code, true))
      local result = p:read('*all'):sub(1, -2) -- chop '\n'
      p:close()
      return result
    end)
    snippet:set_text(snippet.unescape_text(escaped_text))
  end,

  -- Goes to the next placeholder in a snippet.
  -- @param snippet The snippet returned by `new_snippet()`.
  next = function(snippet)
    local buffer = buffer
    -- If the snippet was just initialized, determine how many placeholders it
    -- has.
    if not snippet.index then
      snippet.index, snippet.max_index = 1, 0
      for i in snippet:get_escaped_text():gmatch('%%(%d+)') do
        i = tonumber(i)
        if i > snippet.max_index then snippet.max_index = i end
      end
    end
    local index = snippet.index
    snippet.snapshots[index] = snippet:get_text()

    if index <= snippet.max_index then
      -- Execute shell and Lua code.
      snippet:execute_code(index)
      local escaped_text = snippet:get_escaped_text()..' '

      -- Find the next placeholder.
      local s, _, placeholder, e = escaped_text:find('%%'..index..'(%b())()')
      if not s then s, _, e = escaped_text:find('%%'..index..'()[^(]') end
      if s then
        local start = snippet.start_position
        placeholder = (placeholder or ''):sub(2, -2)

        -- Place the caret at the placeholder.
        buffer.target_start, buffer.target_end = start + s - 1, start + e - 1
        buffer:replace_target(snippet.unescape_text(placeholder))
        buffer:set_sel(buffer.target_start, buffer.target_end)

        -- Add additional carets at mirrors.
        escaped_text = snippet:get_escaped_text()..' '
        offset = 0
        for s, e in escaped_text:gmatch('()%%'..index..'()[^(]') do
          buffer.target_start = start + s - 1 + offset
          buffer.target_end = start + e - 1 + offset
          buffer:replace_target(placeholder)
          buffer:add_selection(buffer.target_start, buffer.target_end)
          offset = offset + (#placeholder - (e - s))
        end
        buffer.main_selection = 0
      end
      snippet.index = index + 1
      if not s then snippet:next() end
    else
      snippet:finish()
    end
  end,

  -- Goes to the previous placeholder in a snippet.
  -- @param snippet The snippet returned by `new_snippet()`.
  previous = function(snippet)
    if snippet.index > 2 then
      snippet:set_text(snippet.snapshots[snippet.index - 2])
      snippet.index = snippet.index - 2
      snippet:next()
    else
      snippet:cancel()
    end
  end,

  -- Cancels a snippet.
  -- @param snippet The snippet returned by `new_snippet()`.
  cancel = function(snippet)
    local buffer = buffer
    buffer:set_sel(snippet.start_position, snippet:get_end_position())
    buffer:replace_sel(snippet.trigger or snippet.original_sel_text)
    buffer.indicator_current = INDIC_SNIPPET
    buffer:indicator_clear_range(snippet:get_end_position(), 1)
    snippet_stack[#snippet_stack] = nil
  end,

  -- Finishes a snippet by going to its `%0` placeholder and cleaning up.
  -- @param snippet The snippet returned by `new_snippet()`.
  finish = function(snippet)
    local buffer = buffer
    snippet:set_text(snippet.unescape_text(snippet:get_text(), true))
    local s, e = snippet:get_text():find('%%0')
    if s and e then
      buffer:set_sel(snippet.start_position + s - 1, snippet.start_position + e)
      buffer:replace_sel('')
    else
      buffer:goto_pos(snippet:get_end_position())
    end
    buffer.indicator_current = INDIC_SNIPPET
    e = snippet:get_end_position()
    buffer:indicator_clear_range(e, 1)
    buffer.target_start, buffer.target_end = e, e + 1
    buffer:replace_target('') -- clear initial padding space
    snippet_stack[#snippet_stack] = nil
  end,
}

local INDIC_HIDDEN = _SCINTILLA.constants.INDIC_HIDDEN
if buffer then buffer.indic_style[INDIC_SNIPPET] = INDIC_HIDDEN end
events.connect(events.VIEW_NEW,
               function() buffer.indic_style[INDIC_SNIPPET] = INDIC_HIDDEN end)

---
-- Provides access to snippets from `_G`.
-- @class table
-- @name _G.snippets
_G.snippets = _M