1-- Copyright 2007-2021 Mitchell. See LICENSE.
2
3local ui = ui
4
5--[[ This comment is for LuaDoc.
6---
7-- Utilities for interacting with Textadept's user interface.
8-- @field title (string, Write-only)
9--   The title text of Textadept's window.
10-- @field context_menu (userdata)
11--   The buffer's context menu, a [`ui.menu()`]().
12--   This is a low-level field. You probably want to use the higher-level
13--   [`textadept.menu.context_menu`]().
14-- @field tab_context_menu (userdata)
15--   The context menu for the buffer's tab, a [`ui.menu()`]().
16--   This is a low-level field. You probably want to use the higher-level
17--   [`textadept.menu.tab_context_menu`]().
18-- @field clipboard_text (string)
19--   The text on the clipboard.
20-- @field statusbar_text (string, Write-only)
21--   The text displayed in the statusbar.
22-- @field buffer_statusbar_text (string, Write-only)
23--   The text displayed in the buffer statusbar.
24-- @field maximized (bool)
25--   Whether or not Textadept's window is maximized.
26-- @field tabs (bool)
27--   Whether or not to display the tab bar when multiple buffers are open.
28--   The default value is `true`.
29-- @field silent_print (bool)
30--   Whether or not to print messages to buffers silently.
31--   This is not guaranteed to be a constant value, as Textadept may change it
32--   for the editor's own purposes. This flag should be used only in conjunction
33--   with a group of [`ui.print()`]() and [`ui._print()`]() function calls.
34--   The default value is `false`, and focuses buffers when messages are printed
35--   to them.
36module('ui')]]
37
38ui.silent_print = false
39
40-- Helper function for jumping to another view to print to, or creating a new
41-- view to print to (the latter depending on settings).
42local function prepare_view()
43  if #_VIEWS > 1 then ui.goto_view(1) elseif not ui.tabs then view:split() end
44end
45
46-- Helper function for printing messages to buffers.
47-- @see ui._print
48local function _print(buffer_type, ...)
49  local buffer
50  for _, buf in ipairs(_BUFFERS) do
51    if buf._type == buffer_type then buffer = buf break end
52  end
53  if not buffer then
54    prepare_view()
55    buffer = _G.buffer.new()
56    buffer._type = buffer_type
57  elseif not ui.silent_print then
58    for _, view in ipairs(_VIEWS) do
59      if view.buffer._type == buffer_type then
60        ui.goto_view(view)
61        goto view_found
62      end
63    end
64    prepare_view()
65    view:goto_buffer(buffer)
66    ::view_found::
67  end
68  local args, n = {...}, select('#', ...)
69  for i = 1, n do args[i] = tostring(args[i]) end
70  buffer:append_text(table.concat(args, '\t'))
71  buffer:append_text('\n')
72  buffer:goto_pos(buffer.length + 1)
73  buffer:set_save_point()
74end
75---
76-- Prints the given string messages to the buffer of string type *buffer_type*.
77-- Opens a new buffer for printing messages to if necessary. If the message
78-- buffer is already open in a view, the message is printed to that view.
79-- Otherwise the view is split (unless `ui.tabs` is `true`) and the message
80-- buffer is displayed before being printed to.
81-- @param buffer_type String type of message buffer.
82-- @param ... Message strings.
83-- @usage ui._print(_L['[Message Buffer]'], message)
84-- @name _print
85function ui._print(buffer_type, ...)
86  _print(assert_type(buffer_type, 'string', 1), ...)
87end
88
89---
90-- Prints the given string messages to the message buffer.
91-- Opens a new buffer if one has not already been opened for printing messages.
92-- @param ... Message strings.
93-- @name print
94function ui.print(...) ui._print(_L['[Message Buffer]'], ...) end
95
96-- Returns 0xBBGGRR colors transformed into "#RRGGBB" for the colorselect
97-- dialog.
98-- @param value Number color to transform.
99-- @return string or nil if the transform failed
100local function torgb(value)
101  local bbggrr = string.format('%06X', value)
102  local b, g, r = bbggrr:match('^(%x%x)(%x%x)(%x%x)$')
103  return r and g and b and string.format('#%s%s%s', r, g, b) or nil
104end
105
106-- Documentation is in core/.ui.dialogs.luadoc.
107ui.dialogs = setmetatable({}, {__index = function(_, k)
108  -- Wrapper for `ui.dialog(k)`, transforming the given table of arguments into
109  -- a set of command line arguments and transforming the resulting standard
110  -- output into Lua objects.
111  -- @param options Table of key-value command line options for gtdialog.
112  -- @param f Work function for progressbar dialogs.
113  -- @return Lua objects depending on the dialog kind
114  return function(options, f)
115    if not options.button1 then options.button1 = _L['OK'] end
116    if k == 'filteredlist' and not options.width then
117      options.width = ui.size[1] - 2 * (CURSES and 1 or 100)
118    end
119    -- Transform key-value pairs into command line arguments.
120    local args = {}
121    for option, value in pairs(options) do
122      if assert_type(value, 'string/number/table/boolean', option) then
123        args[#args + 1] = '--' .. option:gsub('_', '-')
124        if type(value) == 'table' then
125          for i, val in ipairs(value) do
126            local narg = string.format('%s[%d]', option, i)
127            assert_type(val, 'string/number', narg)
128            if option == 'palette' and type(val) == 'number' then
129              value[i] = torgb(val) -- nil return is okay
130            elseif option == 'select' and assert_type(val, 'number', narg) then
131              value[i] = val - 1 -- convert from 1-based index to 0-based index
132            end
133          end
134        elseif option == 'color' and type(value) == 'number' then
135          value = torgb(value)
136        elseif option == 'select' and assert_type(value, 'number', option) then
137          value = value - 1 -- convert from 1-based index to 0-based index
138        end
139        if type(value) ~= 'boolean' then args[#args + 1] = value end
140      end
141    end
142    if k == 'progressbar' then
143      args[#args + 1] = assert_type(f, 'function', 2)
144    end
145    -- Call gtdialog, stripping any trailing newline in the output.
146    local result = ui.dialog(
147      k:gsub('_', '-'), table.unpack(args)):match('^(.-)\n?$')
148    -- Depending on the dialog type, transform the result into Lua objects.
149    if k == 'fileselect' or k == 'filesave' then
150      if result == '' then return nil end
151      if WIN32 and not CURSES then result = result:iconv(_CHARSET, 'UTF-8') end
152      if k == 'filesave' or not options.select_multiple then return result end
153      local filenames = {}
154      for filename in result:gmatch('[^\n]+') do
155        filenames[#filenames + 1] = filename
156      end
157      return filenames
158    elseif k == 'filteredlist' or k == 'optionselect' or
159           k:find('input') and result:match('^[^\n]+\n?(.*)$'):find('\n') then
160      local button, value = result:match('^([^\n]+)\n?(.*)$')
161      if not options.string_output then button = tonumber(button) end
162      if k == 'optionselect' then
163        options.select_multiple = true
164      elseif k:find('input') then
165        options.string_output, options.select_multiple = true, true
166      end
167      local items, patt = {}, not k:find('input') and '[^\n]+' or '([^\n]*)\n'
168      for item in (value .. '\n'):gmatch(patt) do
169        items[#items + 1] = options.string_output and item or tonumber(item) + 1
170      end
171      return button, options.select_multiple and items or items[1]
172    elseif k == 'colorselect' then
173      if options.string_output then return result ~= '' and result or nil end
174      local r, g, b = result:match('^#(%x%x)(%x%x)(%x%x)$')
175      local bgr = r and g and b and string.format('0x%s%s%s', b, g, r) or nil
176      return tonumber(bgr)
177    elseif k == 'fontselect' or k == 'progressbar' then
178      return result ~= '' and result or nil
179    elseif not options.string_output then
180      local i, value = result:match('^(%-?%d+)\n?(.*)$')
181      i = tonumber(i)
182      if k:find('dropdown') then
183        value = i > 0 and tonumber(value) + 1 or nil
184      end
185      return i, value
186    end
187    return result:match('([^\n]+)\n?(.*)$')
188  end
189end})
190
191local buffers_zorder = {}
192
193-- Adds new buffers to the z-order list.
194events.connect(events.BUFFER_NEW, function()
195  if buffer ~= ui.command_entry then table.insert(buffers_zorder, 1, buffer) end
196end)
197
198-- Updates the z-order list.
199local function update_zorder()
200  local i = 1
201  while i <= #buffers_zorder do
202    if buffers_zorder[i] == buffer or not _BUFFERS[buffers_zorder[i]] then
203      table.remove(buffers_zorder, i)
204    else
205      i = i + 1
206    end
207  end
208  table.insert(buffers_zorder, 1, buffer)
209end
210events.connect(events.BUFFER_AFTER_SWITCH, update_zorder)
211events.connect(events.VIEW_AFTER_SWITCH, update_zorder)
212events.connect(events.BUFFER_DELETED, update_zorder)
213
214-- Saves and restores buffer zorder data during a reset.
215events.connect(
216  events.RESET_BEFORE, function(persist) persist.ui_zorder = buffers_zorder end)
217events.connect(
218  events.RESET_AFTER, function(persist) buffers_zorder = persist.ui_zorder end)
219
220---
221-- Prompts the user to select a buffer to switch to.
222-- Buffers are listed in the order they were opened unless `zorder` is `true`,
223-- in which case buffers are listed by their z-order (most recently viewed to
224-- least recently viewed).
225-- @param zorder Flag that indicates whether or not to list buffers by their
226--   z-order. The default value is `false`.
227-- @name switch_buffer
228function ui.switch_buffer(zorder)
229  local buffers = not zorder and _BUFFERS or buffers_zorder
230  local columns, utf8_list = {_L['Name'], _L['Filename']}, {}
231  for i = not zorder and 1 or 2, #buffers do
232    local buffer = buffers[i]
233    local filename = buffer.filename or buffer._type or _L['Untitled']
234    if buffer.filename then filename = filename:iconv('UTF-8', _CHARSET) end
235    local basename = buffer.filename and filename:match('[^/\\]+$') or filename
236    utf8_list[#utf8_list + 1] = (buffer.modify and '*' or '') .. basename
237    utf8_list[#utf8_list + 1] = filename
238  end
239  local button, i = ui.dialogs.filteredlist{
240    title = _L['Switch Buffers'], columns = columns, items = utf8_list
241  }
242  if button ~= 1 or not i then return end
243  view:goto_buffer(buffers[not zorder and i or i + 1])
244end
245
246---
247-- Switches to the existing view whose buffer's filename is *filename*.
248-- If no view was found and *split* is `true`, splits the current view in order
249-- to show the requested file. If *split* is `false`, shifts to the next or
250-- *preferred_view* view in order to show the requested file. If *sloppy* is
251-- `true`, requires only the basename of *filename* to match a buffer's
252-- `filename`. If the requested file was not found, it is opened in the desired
253-- view.
254-- @param filename The filename of the buffer to go to.
255-- @param split Optional flag that indicates whether or not to open the buffer
256--   in a split view if there is only one view. The default value is `false`.
257-- @param preferred_view Optional view to open the desired buffer in if the
258--   buffer is not visible in any other view.
259-- @param sloppy Optional flag that indicates whether or not to not match
260--   *filename* to `buffer.filename` exactly. When `true`, matches *filename* to
261--   only the last part of `buffer.filename` This is useful for run and compile
262--   commands which output relative filenames and paths instead of full ones and
263--   it is likely that the file in question is already open. The default value
264--   is `false`.
265-- @name goto_file
266function ui.goto_file(filename, split, preferred_view, sloppy)
267  assert_type(filename, 'string', 1)
268  local patt = string.format( -- TODO: escape filename properly
269    '%s%s$', not sloppy and '^' or '',
270    not sloppy and filename or filename:match('[^/\\]+$'))
271  if WIN32 then
272    patt = patt:gsub('%a', function(letter)
273      return string.format('[%s%s]', letter:upper(), letter:lower())
274    end)
275  end
276  if #_VIEWS == 1 and split and not (view.buffer.filename or ''):find(patt) then
277    view:split()
278  else
279    local other_view = _VIEWS[preferred_view]
280    for _, view in ipairs(_VIEWS) do
281      local filename = view.buffer.filename or ''
282      if filename:find(patt) then ui.goto_view(view) return end
283      if not other_view and view ~= _G.view then other_view = view end
284    end
285    if other_view then ui.goto_view(other_view) end
286  end
287  for _, buf in ipairs(_BUFFERS) do
288    if (buf.filename or ''):find(patt) then view:goto_buffer(buf) return end
289  end
290  io.open_file(filename)
291end
292
293-- Ensure title, statusbar, etc. are updated for new views.
294events.connect(events.VIEW_NEW, function() events.emit(events.UPDATE_UI, 3) end)
295
296-- Switches between buffers when a tab is clicked.
297events.connect(
298  events.TAB_CLICKED, function(index) view:goto_buffer(_BUFFERS[index]) end)
299
300-- Sets the title of the Textadept window to the buffer's filename.
301local function set_title()
302  local filename = buffer.filename or buffer._type or _L['Untitled']
303  if buffer.filename then
304    filename = select(2, pcall(string.iconv, filename, 'UTF-8', _CHARSET))
305  end
306  local basename = buffer.filename and filename:match('[^/\\]+$') or filename
307  ui.title = string.format(
308    '%s %s Textadept (%s)', basename, buffer.modify and '*' or '-', filename)
309  buffer.tab_label = basename .. (buffer.modify and '*' or '')
310end
311
312-- Changes Textadept title to show the buffer as being "clean" or "dirty".
313events.connect(events.SAVE_POINT_REACHED, set_title)
314events.connect(events.SAVE_POINT_LEFT, set_title)
315
316-- Open uri(s).
317events.connect(events.URI_DROPPED, function(utf8_uris)
318  for utf8_path in utf8_uris:gmatch('file://([^\r\n]+)') do
319    local path = utf8_path:gsub('%%(%x%x)', function(hex)
320      return string.char(tonumber(hex, 16))
321    end):iconv(_CHARSET, 'UTF-8')
322    -- In WIN32, ignore a leading '/', but not '//' (network path).
323    if WIN32 and not path:match('^//') then path = path:sub(2, -1) end
324    local mode = lfs.attributes(path, 'mode')
325    if mode and mode ~= 'directory' then io.open_file(path) end
326  end
327  ui.goto_view(view) -- work around any view focus synchronization issues
328end)
329events.connect(events.APPLEEVENT_ODOC, function(uri)
330  return events.emit(events.URI_DROPPED, 'file://' .. uri)
331end)
332
333-- Sets buffer statusbar text.
334events.connect(events.UPDATE_UI, function(updated)
335  if updated & 3 == 0 then return end -- ignore scrolling
336  local text = not CURSES and '%s %d/%d    %s %d    %s    %s    %s    %s' or
337    '%s %d/%d  %s %d  %s  %s  %s  %s'
338  local pos = buffer.current_pos
339  local line, max = buffer:line_from_position(pos), buffer.line_count
340  local col = buffer.column[pos]
341  local lang = buffer:get_lexer()
342  local eol = buffer.eol_mode == buffer.EOL_CRLF and _L['CRLF'] or _L['LF']
343  local tabs = string.format(
344    '%s %d', buffer.use_tabs and _L['Tabs:'] or _L['Spaces:'], buffer.tab_width)
345  local encoding = buffer.encoding or ''
346  ui.buffer_statusbar_text = string.format(
347    text, _L['Line:'], line, max, _L['Col:'], col, lang, eol, tabs, encoding)
348end)
349
350-- Save buffer properties.
351local function save_buffer_state()
352  -- Save view state.
353  buffer._anchor, buffer._current_pos = buffer.anchor, buffer.current_pos
354  local n = buffer.main_selection
355  buffer._anchor_virtual_space = buffer.selection_n_anchor_virtual_space[n]
356  buffer._caret_virtual_space = buffer.selection_n_caret_virtual_space[n]
357  buffer._top_line = view:doc_line_from_visible(view.first_visible_line)
358  buffer._x_offset = view.x_offset
359  -- Save fold state.
360  local folds, i = {}, view:contracted_fold_next(1)
361  while i >= 1 do folds[#folds + 1], i = i, view:contracted_fold_next(i + 1) end
362  buffer._folds = folds
363end
364events.connect(events.BUFFER_BEFORE_SWITCH, save_buffer_state)
365events.connect(events.FILE_BEFORE_RELOAD, save_buffer_state)
366
367-- Restore buffer properties.
368local function restore_buffer_state()
369  if not buffer._folds then return end
370  -- Restore fold state.
371  for _, line in ipairs(buffer._folds) do view:toggle_fold(line) end
372  -- Restore view state.
373  buffer:set_sel(buffer._anchor, buffer._current_pos)
374  buffer.selection_n_anchor_virtual_space[1] = buffer._anchor_virtual_space
375  buffer.selection_n_caret_virtual_space[1] = buffer._caret_virtual_space
376  buffer:choose_caret_x()
377  local _top_line, top_line = buffer._top_line, view.first_visible_line
378  view:line_scroll(0, view:visible_from_doc_line(_top_line) - top_line)
379  view.x_offset = buffer._x_offset or 0
380end
381events.connect(events.BUFFER_AFTER_SWITCH, restore_buffer_state)
382events.connect(events.FILE_AFTER_RELOAD, restore_buffer_state)
383
384-- Updates titlebar and statusbar.
385local function update_bars()
386  set_title()
387  events.emit(events.UPDATE_UI, 3)
388end
389events.connect(events.BUFFER_NEW, update_bars)
390events.connect(events.BUFFER_AFTER_SWITCH, update_bars)
391events.connect(events.VIEW_AFTER_SWITCH, update_bars)
392
393-- Save view state.
394local function save_view_state()
395  buffer._view_ws, buffer._wrap_mode = view.view_ws, view.wrap_mode
396  buffer._margin_type_n, buffer._margin_width_n = {}, {}
397  for i = 1, view.margins do
398    buffer._margin_type_n[i] = view.margin_type_n[i]
399    buffer._margin_width_n[i] = view.margin_width_n[i]
400  end
401end
402events.connect(events.BUFFER_BEFORE_SWITCH, save_view_state)
403events.connect(events.VIEW_BEFORE_SWITCH, save_view_state)
404
405-- Restore view state.
406local function restore_view_state()
407  if not buffer._margin_type_n then return end
408  view.view_ws, view.wrap_mode = buffer._view_ws, buffer._wrap_mode
409  for i = 1, view.margins do
410    view.margin_type_n[i] = buffer._margin_type_n[i]
411    view.margin_width_n[i] = buffer._margin_width_n[i]
412  end
413end
414events.connect(events.BUFFER_AFTER_SWITCH, restore_view_state)
415events.connect(events.VIEW_AFTER_SWITCH, restore_view_state)
416
417events.connect(
418  events.RESET_AFTER, function() ui.statusbar_text = _L['Lua reset'] end)
419
420-- Prompts for confirmation if any buffers are modified.
421events.connect(events.QUIT, function()
422  local utf8_list = {}
423  for _, buffer in ipairs(_BUFFERS) do
424    if not buffer.modify then goto continue end
425    local filename = buffer.filename or buffer._type or _L['Untitled']
426    if buffer.filename then filename = filename:iconv('UTF-8', _CHARSET) end
427    utf8_list[#utf8_list + 1] = filename
428    ::continue::
429  end
430  if #utf8_list == 0 then return end
431  local button = ui.dialogs.msgbox{
432    title = _L['Quit without saving?'],
433    text = _L['The following buffers are unsaved:'],
434    informative_text = table.concat(utf8_list, '\n'),
435    icon = 'gtk-dialog-question', button1 = _L['Cancel'],
436    button2 = _L['Quit without saving'],
437    width = CURSES and ui.size[1] - 2 or nil
438  }
439  if button ~= 2 then return true end -- prevent quit
440end)
441
442-- Keeps track of, and switches back to the previous buffer after buffer close.
443events.connect(
444  events.BUFFER_BEFORE_SWITCH, function() view._prev_buffer = buffer end)
445events.connect(events.BUFFER_DELETED, function()
446  if _BUFFERS[view._prev_buffer] and buffer ~= view._prev_buffer then
447    restore_view_state() -- events.BUFFER_AFTER_SWITCH is not emitted in time
448    view:goto_buffer(view._prev_buffer)
449  end
450end)
451
452-- Properly handle clipboard text between views in curses, enables and disables
453-- mouse mode, and focuses and resizes views based on mouse events.
454if CURSES then
455  events.connect(events.VIEW_BEFORE_SWITCH, function()
456    ui._clipboard_text = ui.clipboard_text
457  end)
458  events.connect(events.VIEW_AFTER_SWITCH, function()
459    ui.clipboard_text = ui._clipboard_text
460  end)
461
462  if not WIN32 then
463    local function enable_mouse() io.stdout:write("\x1b[?1002h"):flush() end
464    local function disable_mouse() io.stdout:write("\x1b[?1002l"):flush() end
465    enable_mouse()
466    events.connect(events.SUSPEND, disable_mouse)
467    events.connect(events.RESUME, enable_mouse)
468    events.connect(events.QUIT, disable_mouse)
469  end
470
471  -- Retrieves the view or split at the given terminal coordinates.
472  -- @param view View or split to test for coordinates within.
473  -- @param y The y terminal coordinate.
474  -- @param x The x terminal coordinate.
475  local function get_view(view, y, x)
476    if not view[1] and not view[2] then return view end
477    local vertical, size = view.vertical, view.size
478    if vertical and x < size or not vertical and y < size then
479      return get_view(view[1], y, x)
480    elseif vertical and x > size or not vertical and y > size then
481      -- Zero y or x relative to the other view based on split orientation.
482      return get_view(
483        view[2], vertical and y or y - size - 1, vertical and x - size - 1 or x)
484    else
485      return view -- in-between views; return the split itself
486    end
487  end
488
489  local resize
490  events.connect(events.MOUSE, function(event, button, y, x)
491    if event == view.MOUSE_RELEASE or button ~= 1 then return end
492    if event == view.MOUSE_PRESS then
493      local view = get_view(ui.get_split_table(), y - 1, x) -- title is at y = 1
494      if not view[1] and not view[2] then
495        ui.goto_view(view)
496        resize = nil
497      else
498        resize = function(y2, x2)
499          local i = getmetatable(view[1]) == getmetatable(_G.view) and 1 or 2
500          view[i].size = view.size + (view.vertical and x2 - x or y2 - y)
501        end
502      end
503    elseif resize then
504      resize(y, x)
505    end
506    return resize ~= nil -- false resends mouse event to current view
507  end)
508end
509
510events.connect(events.INITIALIZED, function()
511  local lua_error = (not WIN32 and '^/' or '^%a:[/\\]') .. '.-%.lua:%d+:'
512  -- Print internal Lua error messages as they are reported.
513  -- Attempt to mimic the Lua interpreter's error message format so tools that
514  -- look for it can recognize these errors too.
515  events.connect(events.ERROR, function(text)
516    if text and text:find(lua_error) then text = 'lua: ' .. text end
517    ui.print(text)
518  end)
519end)
520
521--[[ The tables below were defined in C.
522
523---
524-- A table of menus defining a menubar. (Write-only).
525-- This is a low-level field. You probably want to use the higher-level
526-- `textadept.menu.menubar`.
527-- @see textadept.menu.menubar
528-- @class table
529-- @name menubar
530local menubar
531
532---
533-- A table containing the width and height pixel values of Textadept's window.
534-- @class table
535-- @name size
536local size
537
538The functions below are Lua C functions.
539
540---
541-- Low-level function for prompting the user with a [gtdialog][] of kind *kind*
542-- with the given string and table arguments, returning a formatted string of
543-- the dialog's output.
544-- You probably want to use the higher-level functions in the [`ui.dialogs`]()
545-- module.
546-- Table arguments containing strings are allowed and expanded in place. This is
547-- useful for filtered list dialogs with many items.
548--
549-- [gtdialog]: https://orbitalquark.github.io/gtdialog/manual.html
550-- @param kind The kind of gtdialog.
551-- @param ... Parameters to the gtdialog.
552-- @return string gtdialog result.
553-- @class function
554-- @name dialog
555local dialog
556
557---
558-- Returns a split table that contains Textadept's current split view structure.
559-- This is primarily used in session saving.
560-- @return table of split views. Each split view entry is a table with 4
561--   fields: `1`, `2`, `vertical`, and `size`. `1` and `2` have values of either
562--   nested split view entries or the views themselves; `vertical` is a flag
563--   that indicates if the split is vertical or not; and `size` is the integer
564--   position of the split resizer.
565-- @class function
566-- @name get_split_table
567local get_split_table
568
569---
570-- Shifts to view *view* or the view *view* number of views relative to the
571-- current one.
572-- Emits `VIEW_BEFORE_SWITCH` and `VIEW_AFTER_SWITCH` events.
573-- @param view A view or relative view number (typically 1 or -1).
574-- @see _G._VIEWS
575-- @see events.VIEW_BEFORE_SWITCH
576-- @see events.VIEW_AFTER_SWITCH
577-- @class function
578-- @name goto_view
579local goto_view
580
581---
582-- Low-level function for creating a menu from table *menu_table* and returning
583-- the userdata.
584-- You probably want to use the higher-level `textadept.menu.menubar`,
585-- `textadept.menu.context_menu`, or `textadept.menu.tab_context_menu` tables.
586-- Emits a `MENU_CLICKED` event when a menu item is selected.
587-- @param menu_table A table defining the menu. It is an ordered list of tables
588--   with a string menu item, integer menu ID, and optional GDK keycode and
589--   modifier mask. The latter two are used to display key shortcuts in the
590--   menu. '_' characters are treated as a menu mnemonics. If the menu item is
591--   empty, a menu separator item is created. Submenus are just nested
592--   menu-structure tables. Their title text is defined with a `title` key.
593-- @usage ui.menu{ {'_New', 1}, {'_Open', 2}, {''}, {'_Quit', 4} }
594-- @usage ui.menu{ {'_New', 1, string.byte('n'), 4} } -- 'Ctrl+N'
595-- @see events.MENU_CLICKED
596-- @see textadept.menu.menubar
597-- @see textadept.menu.context_menu
598-- @see textadept.menu.tab_context_menu
599-- @class function
600-- @name menu
601local menu
602
603---
604-- Processes pending GTK events, including reading from spawned processes.
605-- This function is primarily used in unit tests.
606-- @class function
607-- @name update
608local update
609]]
610