aboutsummaryrefslogtreecommitdiffhomepage
path: root/modules/textadept/snippets.lua
blob: af7819b978ff2600338517d65dcb4b0a229d6676 (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
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
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
-- Copyright 2007-2015 Mitchell mitchell.att.foicica.com. See LICENSE.

local M = {}

--[=[ This comment is for LuaDoc.
---
-- Snippets for Textadept.
--
-- ## Overview
--
-- Define snippets in the global `snippets` table in key-value pairs. Each pair
-- consists of either a string trigger word and its snippet text, or a string
-- lexer language (from the *lexers/* directory) with a table of trigger words
-- and snippet texts. When searching for a snippet to insert based on a trigger
-- word, Textadept considers snippets in the current lexer to have priority,
-- followed by the ones in the global table. This means if there are two
-- snippets with the same trigger word, Textadept inserts the one specific to
-- the current lexer, not the global one.
--
-- ## Special Sequences
--
-- ### `%`*n*`(`*text*`)`
--
-- Represents a placeholder, where *n* is an integer and *text* is default
-- placeholder text. Textadept moves the caret to placeholders in numeric order
-- each time it calls [`textadept.snippets._insert()`](), finishing at either
-- the "%0" placeholder if it exists or at the end of the snippet. Examples are
--
--     snippets['foo'] = 'foobar%1(baz)'
--     snippets['bar'] = 'start\n\t%0\nend'
--
-- ### `%`*n*
--
-- Represents a mirror, where *n* is an integer. Mirrors with the same *n* as a
-- placeholder mirror any user input in the placeholder. If no placeholder
-- exists for *n*, the first occurrence of that mirror in the snippet becomes
-- the placeholder, but with no default text. Examples are
--
--     snippets['foo'] = '%1(mirror), %1, on the wall'
--     snippets['q'] = '"%1"'
--
-- ### `%`*n*`<`*Lua code*`>`<br/>`%`*n*`[`*Shell code*`]`
--
-- Represents a transform, where *n* is an integer that has an associated
-- placeholder, *Lua code* is arbitrary Lua code, and *Shell code* is arbitrary
-- Shell code. Textadept executes the code as text is typed into placeholder
-- *n*. If the transform omits *n*, Textadept executes the transform's code the
-- moment the editor inserts the snippet.
--
-- Textadept runs Lua code in its Lua State and replaces the transform with the
-- code's return text. The code may use the temporary `text` and `selected_text`
-- global variables which contain placeholder *n*'s text and the text originally
-- selected when the snippet was inserted, respectively. An example is
--
--     snippets['attr'] = [[
--     %1(int) %2(foo) = %3;
--
--     %1 get%2<text:gsub('^.', function(c) return c:upper() end)>() {
--     	return %2;
--     }
--     void set%2<text:gsub('^.', function(c) return c:upper() end)>(%1 value) {
--     	%2 = value;
--     }
--     ]]
--
-- Textadept executes shell code using Lua's [`io.popen()`][] and replaces the
-- transform with the process' standard output (stdout). The code may use a `%`
-- character to represent placeholder *n*'s text. An example is
--
--     snippets['env'] = '$%1(HOME) = %1[echo $%]'
--
-- ### `%%`
--
-- Stands for a single '%' since '%' by itself has a special meaning in
-- snippets.
--
-- ### `\t`
--
-- A single unit of indentation based on the buffer's indentation settings
-- ([`buffer.use_tabs`]() and [`buffer.tab_width`]()).
--
-- ### `\n`
--
-- A single set of line ending delimiters based on the buffer's end of line mode
-- ([`buffer.eol_mode`]()).
--
-- [`io.popen()`]: http://www.lua.org/manual/5.3/manual.html#pdf-io.popen
--
-- @fields INDIC_PLACEHOLDER (number)
--   The snippet placeholder indicator number.
module('textadept.snippets')]=]

M.INDIC_PLACEHOLDER = _SCINTILLA.next_indic_number()

local INDIC_SNIPPET = _SCINTILLA.next_indic_number()
local INDIC_CURRENTPLACEHOLDER = _SCINTILLA.next_indic_number()

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

-- Inserts a new snippet, adds it to the snippet stack, and returns the 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)
  -- An inserted snippet.
  -- @field trigger The word that triggered this snippet.
  -- @field original_sel_text The text originally selected when this snippet was
  --   inserted.
  -- @field start_pos This snippet's start position.
  -- @field end_pos This snippet's end position. This is a metafield that is
  --   computed based on the `INDIC_SNIPPET` sentinel.
  -- @field placeholder_pos The beginning of the current placeholder in this
  --   snippet. This is used by transforms to identify text to transform. This
  --   is a metafield that is computed based on `INDIC_CURRENTPLACEHOLDER`.
  -- @field index This snippet's current placeholder index.
  -- @field max_index The number of different placeholders in this snippet.
  -- @field snapshots A record of this snippet's text over time. The snapshot
  --   for a given placeholder index contains the state of the snippet with all
  --   placeholders of that index filled in (prior to moving to the next
  --   placeholder index). Snippet state consists of a `text` string field and a
  --   `placeholders` table field.
  -- @class table
  -- @name snippet
  local snippet = setmetatable({
    trigger = trigger, original_sel_text = buffer:get_sel_text(),
    start_pos = buffer.current_pos - (trigger and #trigger or 0),
    index = 0, max_index = 0, snapshots = {},
  }, {__index = function(self, k)
    if k == 'end_pos' then
      return buffer:indicator_end(INDIC_SNIPPET, self.start_pos)
    elseif k == 'placeholder_pos' then
      -- Normally the marker is one character behind the placeholder. However
      -- it will not exist at all if the placeholder is at the beginning of the
      -- snippet. Also account for the marker being at the beginning of the
      -- snippet. (If so, pos will point to the correct position.)
      local pos = buffer:indicator_end(INDIC_CURRENTPLACEHOLDER, self.start_pos)
      if pos == 0 then pos = self.start_pos end
      return bit32.band(buffer:indicator_all_on_for(pos),
                        2^INDIC_CURRENTPLACEHOLDER) > 0 and pos + 1 or pos
    else
      return M._snippet_mt[k]
    end
  end})
  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
      -- Need integer division and LuaJIT does not have // operator.
      indent_size = math.floor(indent_size / buffer.tab_width)
    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, ({[0] = '\r\n', '\r', '\n'})[buffer.eol_mode])

  -- Parse placeholders and generate initial snapshot.
  local snapshot = {text = '', placeholders = {}}
  local P, S, R, V = lpeg.P, lpeg.S, lpeg.R, lpeg.V
  local C, Cp, Ct, Cg, Cc = lpeg.C, lpeg.Cp, lpeg.Ct, lpeg.Cg, lpeg.Cc
  local patt = P{
    V('plain_text') * V('placeholder') * Cp() + V('plain_text') * -1,
    plain_text = C(((P(1) - '%')^1 + '%%')^0),
    placeholder = Ct('%' * (V('index')^-1 * (V('angles') + V('brackets')) *
                            V('transform') +
                            V('index') * (V('parens') + V('simple')))),
    index = Cg(R('09') / tonumber, 'index'),
    parens = '(' * Cg((1 - S('()') + V('parens'))^0, 'default') * ')',
    simple = Cg(Cc(true), 'simple'), transform = Cg(Cc(true), 'transform'),
    angles = '<' * Cg((1 - S('<>') + V('angles'))^0, 'lua_code') * '>',
    brackets = '[' * Cg((1 - S('[]') + V('brackets'))^0, 'sh_code') * ']'
  }
  -- A snippet placeholder.
  -- Each placeholder is stored in a snippet snapshot.
  -- @field id This placeholder's unique ID. This field is used as an
  --   indicator's value for identification purposes.
  -- @field index This placeholder's index.
  -- @field default This placeholder's default text, if any.
  -- @field transform Whether or not this placeholder is a transform (containing
  --   either Lua or Shell code).
  -- @field lua_code The Lua code of this transform.
  -- @field sh_code The Shell code of this transform.
  -- @field position This placeholder's initial position in its snapshot. This
  --   field will not update until the next snapshot is taken. Use
  --   `snippet:each_placeholder()` to determine a placeholder's current
  --   position.
  -- @field length This placeholder's initial length in its snapshot. This field
  --   will never update. Use `buffer:indicator_end()` in conjunction with
  --   `snippet:each_placeholder()` to determine a placeholder's current length.
  -- @class table
  -- @name placeholder
  local text_part, placeholder, e = lpeg.match(patt, text)
  while placeholder do
    if placeholder.index then
      local i = placeholder.index
      if i > snippet.max_index then snippet.max_index = i end
      placeholder.id = #snapshot.placeholders + 1
      snapshot.placeholders[#snapshot.placeholders + 1] = placeholder
    end
    if text_part ~= '' then snapshot.text = snapshot.text..text_part end
    placeholder.position = #snapshot.text
    if placeholder.default then
      -- Execute any embedded code first.
      placeholder.default = placeholder.default:gsub('%%%b<>', function(s)
        return snippet:execute_code{lua_code = s:sub(3, -2)}
      end):gsub('%%%b[]', function(s)
        return snippet:execute_code{sh_code = s:sub(3, -2)}
      end)
      if placeholder.default:find('%%%d+%b()') then
        -- Parses out embedded placeholders, adding them to this snippet's
        -- snapshot.
        -- @param s The placeholder string to parse.
        -- @param start_pos The absolute position in the snippet `s` starts
        --   from. All computed positions are anchored from here.
        -- @return plain text from `s` (i.e. no placeholder markup)
        local function process_placeholders(s, start_pos)
          return s:gsub('()%%(%d+)(%b())', function(position, index, default)
            position = start_pos + position - 1
            default = process_placeholders(default:sub(2, -2),
                                           position + #index + 2) -- skip '%n('
            index = tonumber(index)
            if index > snippet.max_index then snippet.max_index = index end
            snapshot.placeholders[#snapshot.placeholders + 1] = {
              id = #snapshot.placeholders + 1,
              index = index, default = default,
              position = snippet.start_pos + position, length = #default
            }
            return default
          end)
        end
        placeholder.default = process_placeholders(placeholder.default,
                                                   placeholder.position)
      end
      snapshot.text = snapshot.text..placeholder.default
    elseif placeholder.transform and not placeholder.index then
      snapshot.text = snapshot.text..snippet:execute_code(placeholder)
    else
      snapshot.text = snapshot.text..' ' -- fill empty placeholders for display
    end
    placeholder.length = #snapshot.text - placeholder.position
    placeholder.position = snippet.start_pos + placeholder.position -- absolute
    text_part, placeholder, e = lpeg.match(patt, text, e)
  end
  if text_part ~= '' then snapshot.text = snapshot.text..text_part end
  snippet.snapshots[0] = snapshot

  -- Insert the snippet into the buffer and mark its end position.
  buffer:begin_undo_action()
  buffer:set_target_range(snippet.start_pos, buffer.selection_end)
  buffer:replace_target('  ') -- placeholder for snippet text
  buffer.indicator_current = INDIC_SNIPPET
  buffer:indicator_fill_range(snippet.start_pos + 1, 1)
  snippet:insert() -- insert into placeholder
  buffer:end_undo_action()

  return snippet
end

---
-- Inserts snippet text *text* or the snippet assigned to the trigger word
-- behind the caret.
-- Otherwise, if a snippet is active, goes to the active snippet's next
-- placeholder. Returns `false` if no action was taken.
-- @param text Optional snippet text to insert. If `nil`, attempts to insert a
--   new snippet based on the trigger, the word behind caret, and the current
--   lexer.
-- @return `false` if no action was taken; `nil` otherwise.
-- @see buffer.word_chars
-- @name _insert
function M._insert(text)
  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)
    text = type(M[lexer]) == 'table' and M[lexer][trigger] or M[trigger]
  end
  local snippet = type(text) == 'string' and new_snippet(text, trigger) or
                  snippet_stack[#snippet_stack]
  if snippet then snippet:next() else return false end
end

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

---
-- Cancels the active snippet, removing all inserted text.
-- @name _cancel_current
function M._cancel_current()
  if #snippet_stack > 0 then snippet_stack[#snippet_stack]:finish(true) end
end

---
-- Prompts the user to select a snippet to be inserted from a list of global and
-- language-specific snippets.
-- @name _select
function M._select()
  local list, t = {}, {}
  for trigger, text in pairs(snippets) do
    if type(text) == 'string' then list[#list + 1] = trigger..'\0 \0'..text end
  end
  local lexer = buffer:get_lexer(true)
  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)
  for i = 1, #list do
    t[#t + 1], t[#t + 2], t[#t + 3] = list[i]:match('^(%Z+)%z(%Z+)%z(%Z+)$')
  end
  local button, i = ui.dialogs.filteredlist{
    title = _L['Select Snippet'],
    columns = {_L['Trigger'], _L['Scope'], _L['Snippet Text']}, items = t,
    width = CURSES and ui.size[1] - 2 or nil
  }
  if button == 1 and i then M._insert(t[i * 3]) end
end

-- Metatable for a snippet object.
-- @class table
-- @name _snippet_mt
M._snippet_mt = {
  -- Inserts the current snapshot (based on `self.index`) of this snippet into
  -- the buffer and marks placeholders.
  insert = function(self)
    buffer:set_target_range(self.start_pos, self.end_pos)
    buffer:replace_target(self.snapshots[self.index].text)
    buffer.indicator_current = M.INDIC_PLACEHOLDER
    for id, placeholder in pairs(self.snapshots[self.index].placeholders) do
      buffer.indicator_value = id
      buffer:indicator_fill_range(placeholder.position, placeholder.length)
    end
  end,

  -- Jumps to the next placeholder in this snippet and adds additional carets
  -- at mirrors.
  next = function(self)
    -- Take a snapshot of the current state in order to restore it later if
    -- necessary.
    if self.index > 0 then
      local text = buffer:text_range(self.start_pos, self.end_pos)
      local placeholders = {}
      for pos, ph in self:each_placeholder() do
        -- Only the position of placeholders changes between snapshots; save it
        -- and keep all other existing properties.
        -- Note that nested placeholders will return the same placeholder id
        -- twice: once before a nested placeholder, and again after. (e.g.
        -- [foo[bar]baz] will will return the '[foo' and 'baz]' portions of the
        -- same placeholder.) Only process the first occurrence.
        if not placeholders[ph.id] then
          placeholders[ph.id] = setmetatable({position = pos}, {
            __index = self.snapshots[self.index - 1].placeholders[ph.id]
          })
        end
      end
      self.snapshots[self.index] = {text = text, placeholders = placeholders}
    end
    self.index = self.index < self.max_index and self.index + 1 or 0

    -- Find the default placeholder, which may be the first mirror.
    local ph = select(2, self:each_placeholder(self.index, 'default')()) or
               select(2, self:each_placeholder(self.index, 'simple')()) or
               self.index == 0 and {position = self.end_pos, length = 0}
    if not ph then self:next() return end -- try next placeholder

    -- Mark the position of the placeholder so transforms can identify it.
    buffer.indicator_current = INDIC_CURRENTPLACEHOLDER
    buffer:indicator_clear_range(self.placeholder_pos - 1, 1)
    if ph.position > self.start_pos and self.index > 0 then
      -- Place it directly behind the placeholder so it will be preserved.
      buffer:indicator_fill_range(ph.position - 1, 1)
    end

    buffer:begin_undo_action()

    -- Jump to the default placeholder and clear its marker.
    buffer:set_sel(ph.position, ph.position + ph.length)
    local e = buffer:indicator_end(M.INDIC_PLACEHOLDER, ph.position)
    buffer.indicator_current = M.INDIC_PLACEHOLDER
    buffer:indicator_clear_range(ph.position, e - ph.position)
    if not ph.default then buffer:replace_sel('') end -- delete filler ' '

    -- Add additional carets at mirrors and clear their markers.
    local text = ph.default or ''
    ::redo::
    for pos in self:each_placeholder(self.index, 'simple') do
      local e = buffer:indicator_end(M.INDIC_PLACEHOLDER, pos)
      buffer:indicator_clear_range(pos, e - pos)
      buffer:set_target_range(pos, pos + 1)
      buffer:replace_target(text)
      buffer:add_selection(pos, pos + #text)
      goto redo -- indicator positions have changed
    end
    buffer.main_selection = 0

    -- Update transforms.
    self:update_transforms()

    buffer:end_undo_action()

    if self.index == 0 then self:finish() end
  end,

  -- Jumps to the previous placeholder in this snippet and restores the state
  -- associated with that placeholder.
  previous = function(self)
    if self.index < 2 then self:finish(true) return end
    self.index = self.index - 2
    self:insert()
    self:next()
  end,

  -- Finishes or cancels this snippet depending on boolean *canceling*.
  -- The snippet cleans up after itself regardless.
  -- @param canceling Whether or not to cancel inserting this snippet. When
  --   `true`, the buffer is restored to its state prior to snippet expansion.
  finish = function(self, canceling)
    if canceling then
      buffer:set_sel(self.start_pos, self.end_pos)
      buffer:replace_sel(self.trigger or self.original_sel_text)
    end
    buffer:delete_range(self.end_pos, 1) -- clear initial padding space
    snippet_stack[#snippet_stack] = nil
  end,

  -- Returns a generator that returns each placeholder's position and state for
  -- all placeholders in this snippet.
  -- DO NOT modify the buffer while this generator is running. Doing so will
  -- affect the generator's state and cause errors. Re-run the generator each
  -- time a buffer edit is made (e.g. via `goto`).
  -- @param index Optional placeholder index to constrain results to.
  -- @param type Optional placeholder type to constrain results to.
  each_placeholder = function(self, index, type)
    local snapshot = self.snapshots[self.index > 0 and self.index - 1 or
                                    #self.snapshots]
    local i = self.start_pos
    return function()
      local s = buffer:indicator_end(M.INDIC_PLACEHOLDER, i)
      while s > 0 and s <= self.end_pos do
        if bit32.band(buffer:indicator_all_on_for(i),
                      2^M.INDIC_PLACEHOLDER) > 0 then
          -- This next indicator comes directly after the previous one; adjust
          -- start and end positions to compensate.
          s, i = buffer:indicator_start(M.INDIC_PLACEHOLDER, i), s
        else
          i = buffer:indicator_end(M.INDIC_PLACEHOLDER, s)
        end
        local id = buffer:indicator_value_at(M.INDIC_PLACEHOLDER, s)
        local ph = snapshot.placeholders[id]
        if (not index or ph.index == index) and (not type or ph[type]) then
          return s, ph
        end
        s = buffer:indicator_end(M.INDIC_PLACEHOLDER, i)
      end
    end
  end,

  -- Returns the result of executing Lua or Shell code, in placeholder table
  -- *placeholder*, in the context of this snippet.
  -- @param placeholder The placeholder that contains code to execute.
  execute_code = function(self, placeholder)
    local text = buffer:text_range(self.placeholder_pos, buffer.selection_end)
    if not self.index then text = '' end -- %<...> or %[...]
    if placeholder.lua_code then
      local env = {text = text, selected_text = self.original_sel_text}
      local f, result = load('return '..placeholder.lua_code, nil, 'bt',
                             setmetatable(env, {__index = _G}))
      return f and select(2, pcall(f)) or result or ''
    elseif placeholder.sh_code then
      local command = placeholder.sh_code:gsub('%f[%%]%%%f[^%%]', text)
      local p = io.popen(command)
      local result = p:read('*a'):sub(1, -2) -- chop '\n'
      p:close()
      return result
    end
  end,

  -- Updates transforms in place based on the current placeholder's text.
  update_transforms = function(self)
    buffer.indicator_current = M.INDIC_PLACEHOLDER
    local processed = {}
    ::redo::
    for s, ph in self:each_placeholder(nil, 'transform') do
      if ph.index == self.index and not processed[ph] then
        -- Execute the code and replace any existing transform text.
        local result = self:execute_code(ph)
        if result == '' then result = ' ' end -- fill for display
        local id = buffer:indicator_value_at(M.INDIC_PLACEHOLDER, s)
        buffer:set_target_range(s, buffer:indicator_end(M.INDIC_PLACEHOLDER, s))
        buffer:replace_target(result)
        buffer.indicator_value = id
        buffer:indicator_fill_range(s, #result) -- re-mark
        processed[ph] = true
        goto redo -- indicator positions have changed
      elseif ph.index < self.index or self.index == 0 then
        -- Clear obsolete transforms, deleting filler text if necessary.
        local e = buffer:indicator_end(M.INDIC_PLACEHOLDER, s)
        buffer:indicator_clear_range(s, e - s)
        if buffer:text_range(s, e) == ' ' then
          buffer:set_target_range(s, e)
          buffer:replace_target('')
          goto redo
        end
      end
      -- TODO: insert initial transform for ph.index > self.index
    end
  end,
}

-- Update snippet transforms when text is added or deleted.
events.connect(events.UPDATE_UI, function(updated)
  if #snippet_stack > 0 and bit32.band(updated, buffer.UPDATE_CONTENT) > 0 then
    snippet_stack[#snippet_stack]:update_transforms()
  end
end)

events.connect(events.VIEW_NEW, function()
  buffer.indic_style[INDIC_SNIPPET] = buffer.INDIC_HIDDEN
  buffer.indic_style[INDIC_CURRENTPLACEHOLDER] = buffer.INDIC_HIDDEN
end)

---
-- Map of snippet triggers with their snippet text, with language-specific
-- snippets tables assigned to a lexer name key.
-- This table also contains the `textadept.snippets` module.
-- @class table
-- @name _G.snippets
_G.snippets = M

return M