From 25685922b5491703776d19259ed81f6ff7aaecd6 Mon Sep 17 00:00:00 2001 From: mitchell <70453897+667e-11@users.noreply.github.com> Date: Fri, 15 Apr 2016 20:43:48 -0400 Subject: Use function notation for menu and key commands. As a result, the undocumented `textadept.keys.utils` has been removed. Those functions have been moved directly into menu definitions and their corresponding keys have been bound to those menu functions (this also shows menu key shortcuts properly). Therefore, "textadept.menu" should be loaded before "textadept.keys" now. Also, setting `textadept.menu.menubar = {}` must be done within an `events.INITIALIZED` handler. --- modules/textadept/menu.lua | 394 ++++++++++++++++++++++++++++----------------- 1 file changed, 249 insertions(+), 145 deletions(-) (limited to 'modules/textadept/menu.lua') diff --git a/modules/textadept/menu.lua b/modules/textadept/menu.lua index f2bd2d17..acec62b3 100644 --- a/modules/textadept/menu.lua +++ b/modules/textadept/menu.lua @@ -10,25 +10,54 @@ local M = {} -- place. A menu item itself is a table whose first element is a menu label and -- whose second element is a menu command to run. Submenus have `title` keys -- assigned to string text. --- If applicable, load this module last in your *~/.textadept/init.lua*, after --- [`textadept.keys`]() since it looks up defined key commands to show them in --- menus. module('textadept.menu')]] -local _L, buffer, view = _L, buffer, view -local editing, utils = textadept.editing, textadept.keys.utils +local _L = _L local SEPARATOR = {''} +-- The following buffer functions need to be constantized in order for menu +-- items to identify the key associated with the functions. +local menu_buffer_functions = { + 'undo', 'redo', 'cut', 'copy', 'paste', 'line_duplicate', 'clear', + 'select_all', 'upper_case', 'lower_case', 'move_selected_lines_up', + 'move_selected_lines_down', 'zoom_in', 'zoom_out', 'colourise' +} +for i = 1, #menu_buffer_functions do + buffer[menu_buffer_functions[i]] = buffer[menu_buffer_functions[i]] +end + +-- Commonly used functions in menu commands. +local sel_enc = textadept.editing.select_enclosed +local enc = textadept.editing.enclose +local function set_indentation(i) + buffer.tab_width = i + events.emit(events.UPDATE_UI) -- for updating statusbar +end +local function set_eol_mode(mode) + buffer.eol_mode = mode + buffer:convert_eols(mode) + events.emit(events.UPDATE_UI) -- for updating statusbar +end +local function set_encoding(encoding) + buffer:set_encoding(encoding) + events.emit(events.UPDATE_UI) -- for updating statusbar +end +local function open_page(url) + local cmd = (WIN32 and 'start ""') or (OSX and 'open') or 'xdg-open' + spawn(string.format('%s "%s"', cmd, not OSX and url or 'file://'..url)) +end + --- -- The default main menubar. -- Individual menus, submenus, and menu items can be retrieved by name in -- addition to table index number. -- @class table -- @name menubar --- @usage textadept.menubar[_L['_File']]['_New'] -- returns {'_New', buffer.new} --- @usage textadept.menubar[_L['_File']]['_New'][2] = function() ... end +-- @usage textadept.menu.menubar[_L['_File']][_L['_New']] +-- @usage textadept.menu.menubar[_L['_File']][_L['_New']][2] = function() .. end local default_menubar = { - { title = _L['_File'], + { + title = _L['_File'], {_L['_New'], buffer.new}, {_L['_Open'], io.open_file}, {_L['Open _Recent...'], io.open_recent_file}, @@ -43,9 +72,10 @@ local default_menubar = { {_L['Loa_d Session...'], textadept.session.load}, {_L['Sav_e Session...'], textadept.session.save}, SEPARATOR, - {_L['_Quit'], quit}, + {_L['_Quit'], quit} }, - { title = _L['_Edit'], + { + title = _L['_Edit'], {_L['_Undo'], buffer.undo}, {_L['_Redo'], buffer.redo}, SEPARATOR, @@ -54,161 +84,241 @@ local default_menubar = { {_L['_Paste'], buffer.paste}, {_L['Duplicate _Line'], buffer.line_duplicate}, {_L['_Delete'], buffer.clear}, - {_L['D_elete Word'], utils.delete_word}, + {_L['D_elete Word'], function() + textadept.editing.select_word() + buffer:delete_back() + end}, {_L['Select _All'], buffer.select_all}, SEPARATOR, - {_L['_Match Brace'], editing.match_brace}, - {_L['Complete _Word'], {editing.autocomplete, 'word'}}, - {_L['_Highlight Word'], editing.highlight_word}, - {_L['Toggle _Block Comment'], editing.block_comment}, - {_L['T_ranspose Characters'], editing.transpose_chars}, - {_L['_Join Lines'], editing.join_lines}, - {_L['_Filter Through'], - {ui.command_entry.enter_mode, 'filter_through', 'bash'}}, - { title = _L['_Select'], - {_L['Select to _Matching Brace'], {editing.match_brace, 'select'}}, - {_L['Select between _XML Tags'], {editing.select_enclosed, '>', '<'}}, - {_L['Select in XML _Tag'], {editing.select_enclosed, '<', '>'}}, - {_L['Select in _Single Quotes'], {editing.select_enclosed, "'", "'"}}, - {_L['Select in _Double Quotes'], {editing.select_enclosed, '"', '"'}}, - {_L['Select in _Parentheses'], {editing.select_enclosed, '(', ')'}}, - {_L['Select in _Brackets'], {editing.select_enclosed, '[', ']'}}, - {_L['Select in B_races'], {editing.select_enclosed, '{', '}'}}, - {_L['Select _Word'], editing.select_word}, - {_L['Select _Line'], editing.select_line}, - {_L['Select Para_graph'], editing.select_paragraph}, + {_L['_Match Brace'], textadept.editing.match_brace}, + {_L['Complete _Word'], function() + textadept.editing.autocomplete('word') + end}, + {_L['_Highlight Word'], textadept.editing.highlight_word}, + {_L['Toggle _Block Comment'], textadept.editing.block_comment}, + {_L['T_ranspose Characters'], textadept.editing.transpose_chars}, + {_L['_Join Lines'], textadept.editing.join_lines}, + {_L['_Filter Through'], function() + ui.command_entry.enter_mode('filter_through', 'bash') + end}, + { + title = _L['_Select'], + {_L['Select to _Matching Brace'], function() + textadept.editing.match_brace('select') + end}, + {_L['Select between _XML Tags'], function() sel_enc('>', '<') end}, + {_L['Select in XML _Tag'], function() sel_enc('<', '>') end}, + {_L['Select in _Single Quotes'], function() sel_enc("'", "'") end}, + {_L['Select in _Double Quotes'], function() sel_enc('"', '"') end}, + {_L['Select in _Parentheses'], function() sel_enc('(', ')') end}, + {_L['Select in _Brackets'], function() sel_enc('[', ']') end}, + {_L['Select in B_races'], function() sel_enc('{', '}') end}, + {_L['Select _Word'], textadept.editing.select_word}, + {_L['Select _Line'], textadept.editing.select_line}, + {_L['Select Para_graph'], textadept.editing.select_paragraph} }, - { title = _L['Selectio_n'], + { + title = _L['Selectio_n'], {_L['_Upper Case Selection'], buffer.upper_case}, {_L['_Lower Case Selection'], buffer.lower_case}, SEPARATOR, - {_L['Enclose as _XML Tags'], utils.enclose_as_xml_tags}, - {_L['Enclose as Single XML _Tag'], {editing.enclose, '<', ' />'}}, - {_L['Enclose in Single _Quotes'], {editing.enclose, "'", "'"}}, - {_L['Enclose in _Double Quotes'], {editing.enclose, '"', '"'}}, - {_L['Enclose in _Parentheses'], {editing.enclose, '(', ')'}}, - {_L['Enclose in _Brackets'], {editing.enclose, '[', ']'}}, - {_L['Enclose in B_races'], {editing.enclose, '{', '}'}}, + {_L['Enclose as _XML Tags'], function() + enc('<', '>') + local pos = buffer.current_pos + while buffer.char_at[pos - 1] ~= 60 do pos = pos - 1 end -- '<' + buffer:insert_text(-1, '') end}, + {_L['Enclose in Single _Quotes'], function() enc("'", "'") end}, + {_L['Enclose in _Double Quotes'], function() enc('"', '"') end}, + {_L['Enclose in _Parentheses'], function() enc('(', ')') end}, + {_L['Enclose in _Brackets'], function() enc('[', ']') end}, + {_L['Enclose in B_races'], function() enc('{', '}') end}, SEPARATOR, {_L['_Move Selected Lines Up'], buffer.move_selected_lines_up}, - {_L['Move Selected Lines Do_wn'], buffer.move_selected_lines_down}, - }, + {_L['Move Selected Lines Do_wn'], buffer.move_selected_lines_down} + } }, - { title = _L['_Search'], - {_L['_Find'], utils.find}, + { + title = _L['_Search'], + {_L['_Find'], function() + ui.find.in_files = false + ui.find.focus() + end}, {_L['Find _Next'], ui.find.find_next}, {_L['Find _Previous'], ui.find.find_prev}, {_L['_Replace'], ui.find.replace}, {_L['Replace _All'], ui.find.replace_all}, {_L['Find _Incremental'], ui.find.find_incremental}, SEPARATOR, - {_L['Find in Fi_les'], {utils.find, true}}, - {_L['Goto Nex_t File Found'], {ui.find.goto_file_found, false, true}}, - {_L['Goto Previou_s File Found'], {ui.find.goto_file_found, false, false}}, + {_L['Find in Fi_les'], function() + ui.find.in_files = true + ui.find.focus() + end}, + {_L['Goto Nex_t File Found'], function() + ui.find.goto_file_found(false, true) + end}, + {_L['Goto Previou_s File Found'], function() + ui.find.goto_file_found(false, false) + end}, SEPARATOR, - {_L['_Jump to'], editing.goto_line}, + {_L['_Jump to'], textadept.editing.goto_line} }, - { title = _L['_Tools'], - {_L['Command _Entry'], {ui.command_entry.enter_mode, 'lua_command', 'lua'}}, - {_L['Select Co_mmand'], utils.select_command}, + { + title = _L['_Tools'], + {_L['Command _Entry'], function() + ui.command_entry.enter_mode('lua_command', 'lua') + end}, + {_L['Select Co_mmand'], function() M.select_command() end}, SEPARATOR, {_L['_Run'], textadept.run.run}, {_L['_Compile'], textadept.run.compile}, {_L['Buil_d'], textadept.run.build}, {_L['S_top'], textadept.run.stop}, - {_L['_Next Error'], {textadept.run.goto_error, false, true}}, - {_L['_Previous Error'], {textadept.run.goto_error, false, false}}, + {_L['_Next Error'], function() textadept.run.goto_error(false, true) end}, + {_L['_Previous Error'], function() + textadept.run.goto_error(false, false) + end}, SEPARATOR, - { title = _L['_Bookmark'], + { + title = _L['_Bookmark'], {_L['_Toggle Bookmark'], textadept.bookmarks.toggle}, {_L['_Clear Bookmarks'], textadept.bookmarks.clear}, - {_L['_Next Bookmark'], {textadept.bookmarks.goto_mark, true}}, - {_L['_Previous Bookmark'], {textadept.bookmarks.goto_mark, false}}, + {_L['_Next Bookmark'], function() + textadept.bookmarks.goto_mark(true) + end}, + {_L['_Previous Bookmark'], function() + textadept.bookmarks.goto_mark(false) + end}, {_L['_Goto Bookmark...'], textadept.bookmarks.goto_mark}, }, - { title = _L['Snap_open'], - {_L['Snapopen _User Home'], {io.snapopen, _USERHOME}}, - {_L['Snapopen _Textadept Home'], {io.snapopen, _HOME}}, - {_L['Snapopen _Current Directory'], utils.snapopen_filedir}, + { + title = _L['Snap_open'], + {_L['Snapopen _User Home'], function() io.snapopen(_USERHOME) end}, + {_L['Snapopen _Textadept Home'], function() io.snapopen(_HOME) end}, + {_L['Snapopen _Current Directory'], function() + if buffer.filename then + io.snapopen(buffer.filename:match('^(.+)[/\\]')) + end + end}, {_L['Snapopen Current _Project'], io.snapopen}, }, - { title = _L['_Snippets'], + { + title = _L['_Snippets'], {_L['_Insert Snippet...'], textadept.snippets._select}, {_L['_Expand Snippet/Next Placeholder'], textadept.snippets._insert}, {_L['_Previous Snippet Placeholder'], textadept.snippets._previous}, {_L['_Cancel Snippet'], textadept.snippets._cancel_current}, }, SEPARATOR, - {_L['_Complete Symbol'], utils.autocomplete_symbol}, + {_L['_Complete Symbol'], function() + textadept.editing.autocomplete(buffer:get_lexer(true)) + end}, {_L['Show _Documentation'], textadept.editing.show_documentation}, - {_L['Show St_yle'], utils.show_style}, + {_L['Show St_yle'], function() + local char = buffer:text_range(buffer.current_pos, + buffer:position_after(buffer.current_pos)) + local bytes = string.rep(' 0x%X', #char):format(char:byte(1, #char)) + local style = buffer.style_at[buffer.current_pos] + local text = string.format("'%s' (U+%04X:%s)\n%s %s\n%s %s (%d)", char, + utf8.codepoint(char), bytes, _L['Lexer'], + buffer:get_lexer(true), _L['Style'], + buffer.style_name[style], style) + buffer:call_tip_show(buffer.current_pos, text) + end} }, - { title = _L['_Buffer'], - {_L['_Next Buffer'], {view.goto_buffer, view, 1, true}}, - {_L['_Previous Buffer'], {view.goto_buffer, view, -1, true}}, + { + title = _L['_Buffer'], + {_L['_Next Buffer'], function() view:goto_buffer(1, true) end}, + {_L['_Previous Buffer'], function() view:goto_buffer(-1, true) end}, {_L['_Switch to Buffer...'], ui.switch_buffer}, SEPARATOR, - { title = _L['_Indentation'], - {_L['Tab width: _2'], {utils.set_indentation, 2}}, - {_L['Tab width: _3'], {utils.set_indentation, 3}}, - {_L['Tab width: _4'], {utils.set_indentation, 4}}, - {_L['Tab width: _8'], {utils.set_indentation, 8}}, + { + title = _L['_Indentation'], + {_L['Tab width: _2'], function() set_indentation(2) end}, + {_L['Tab width: _3'], function() set_indentation(3) end}, + {_L['Tab width: _4'], function() set_indentation(4) end}, + {_L['Tab width: _8'], function() set_indentation(8) end}, SEPARATOR, - {_L['_Toggle Use Tabs'], {utils.toggle_property, 'use_tabs'}}, - {_L['_Convert Indentation'], editing.convert_indentation}, + {_L['_Toggle Use Tabs'], function() + buffer.use_tabs = not buffer.use_tabs + events.emit(events.UPDATE_UI) -- for updating statusbar + end}, + {_L['_Convert Indentation'], textadept.editing.convert_indentation} }, - { title = _L['_EOL Mode'], - {_L['CRLF'], {utils.set_eol_mode, buffer.EOL_CRLF}}, - {_L['CR'], {utils.set_eol_mode, buffer.EOL_CR}}, - {_L['LF'], {utils.set_eol_mode, buffer.EOL_LF}}, + { + title = _L['_EOL Mode'], + {_L['CRLF'], function() set_eol_mode(buffer.EOL_CRLF) end}, + {_L['CR'], function() set_eol_mode(buffer.EOL_CR) end}, + {_L['LF'], function() set_eol_mode(buffer.EOL_LF) end} }, - { title = _L['E_ncoding'], - {_L['_UTF-8 Encoding'], {utils.set_encoding, 'UTF-8'}}, - {_L['_ASCII Encoding'], {utils.set_encoding, 'ASCII'}}, - {_L['_ISO-8859-1 Encoding'], {utils.set_encoding, 'ISO-8859-1'}}, - {_L['_MacRoman Encoding'], {utils.set_encoding, 'MacRoman'}}, - {_L['UTF-1_6 Encoding'], {utils.set_encoding, 'UTF-16LE'}}, + { + title = _L['E_ncoding'], + {_L['_UTF-8 Encoding'], function() set_encoding('UTF-8') end}, + {_L['_ASCII Encoding'], function() set_encoding('ASCII') end}, + {_L['_ISO-8859-1 Encoding'], function() set_encoding('ISO-8859-1') end}, + {_L['_MacRoman Encoding'], function() set_encoding('MacRoman') end}, + {_L['UTF-1_6 Encoding'], function() set_encoding('UTF-16LE') end} }, SEPARATOR, - {_L['Toggle View _EOL'], {utils.toggle_property, 'view_eol'}}, - {_L['Toggle _Wrap Mode'], {utils.toggle_property, 'wrap_mode'}}, - {_L['Toggle View White_space'], {utils.toggle_property, 'view_ws'}}, + {_L['Toggle View _EOL'], function() + buffer.view_eol = not buffer.view_eol + end}, + {_L['Toggle _Wrap Mode'], function() + buffer.wrap_mode = buffer.wrap_mode == 0 and buffer.WRAP_WHITESPACE or 0 + end}, + {_L['Toggle View White_space'], function() + buffer.view_ws = buffer.view_ws == 0 and buffer.WS_VISIBLEALWAYS or 0 + end}, SEPARATOR, {_L['Select _Lexer...'], textadept.file_types.select_lexer}, - {_L['_Refresh Syntax Highlighting'], {buffer.colourise, buffer, 0, -1}}, + {_L['_Refresh Syntax Highlighting'], function() buffer:colourise(0, -1) end} }, - { title = _L['_View'], - {_L['_Next View'], {ui.goto_view, 1, true}}, - {_L['_Previous View'], {ui.goto_view, -1, true}}, + { + title = _L['_View'], + {_L['_Next View'], function() ui.goto_view(1, true) end}, + {_L['_Previous View'], function() ui.goto_view(-1, true) end}, SEPARATOR, - {_L['Split View _Horizontal'], {view.split, view}}, - {_L['Split View _Vertical'], {view.split, view, true}}, - {_L['_Unsplit View'], {view.unsplit, view}}, - {_L['Unsplit _All Views'], utils.unsplit_all}, - {_L['_Grow View'], utils.grow}, - {_L['Shrin_k View'], utils.shrink}, + {_L['Split View _Horizontal'], function() view:split() end}, + {_L['Split View _Vertical'], function() view:split(true) end}, + {_L['_Unsplit View'], function() view:unsplit() end}, + {_L['Unsplit _All Views'], function() while view:unsplit() do end end}, + {_L['_Grow View'], function() + if view.size then view.size = view.size + buffer:text_height(0) end + end}, + {_L['Shrin_k View'], function() + if view.size then view.size = view.size - buffer:text_height(0) end + end}, SEPARATOR, - {_L['Toggle Current _Fold'], utils.toggle_current_fold}, + {_L['Toggle Current _Fold'], function() + buffer:toggle_fold(buffer:line_from_position(buffer.current_pos)) + end}, SEPARATOR, - {_L['Toggle Show In_dent Guides'], - {utils.toggle_property, 'indentation_guides'}}, - {_L['Toggle _Virtual Space'], - {utils.toggle_property, 'virtual_space_options', - buffer.VS_USERACCESSIBLE}}, + {_L['Toggle Show In_dent Guides'], function() + local off = buffer.indentation_guides == 0 + buffer.indentation_guides = off and buffer.IV_LOOKBOTH or 0 + end}, + {_L['Toggle _Virtual Space'], function() + local off = buffer.virtual_space_options == 0 + buffer.virtual_space_options = off and buffer.VS_USERACCESSIBLE or 0 + end}, SEPARATOR, {_L['Zoom _In'], buffer.zoom_in}, {_L['Zoom _Out'], buffer.zoom_out}, - {_L['_Reset Zoom'], utils.reset_zoom}, + {_L['_Reset Zoom'], function() buffer.zoom = 0 end} }, - { title = _L['_Help'], - {_L['Show _Manual'], {utils.open_webpage, _HOME..'/doc/manual.html'}}, - {_L['Show _LuaDoc'], {utils.open_webpage, _HOME..'/doc/api.html'}}, + { + title = _L['_Help'], + {_L['Show _Manual'], function() open_page(_HOME..'/doc/manual.html') end}, + {_L['Show _LuaDoc'], function() open_page(_HOME..'/doc/api.html') end}, SEPARATOR, - {_L['_About'], - {ui.dialogs.msgbox, {title = 'Textadept', text = _RELEASE, - informative_text = _COPYRIGHT, - icon_file = _HOME..'/core/images/ta_64x64.png'}}}, - }, + {_L['_About'], function() + ui.dialogs.msgbox({ + title = 'Textadept', text = _RELEASE, informative_text = _COPYRIGHT, + icon_file = _HOME..'/core/images/ta_64x64.png' + }) + end} + } } --- @@ -311,27 +421,6 @@ local function read_menu_table(menu, contextmenu) return gtkmenu end --- Builds the item and commands tables for the filtered list dialog. --- @param menu The menu to read from. --- @param title The title of the menu. --- @param items The current list of items. --- @param commands The current list of commands. -local function build_command_tables(menu, title, items, commands) - for i = 1, #menu do - if menu[i].title then - build_command_tables(menu[i], menu[i].title, items, commands) - elseif menu[i][1] ~= '' then - local label, f = menu[i][1], menu[i][2] - if title then label = title..': '..label end - items[#items + 1] = label:gsub('_([^_])', '%1') - items[#items + 1] = key_shortcuts[get_id(f)] or '' - commands[#commands + 1] = f - end - end -end - -local items, commands - -- Returns a proxy table for menu table *menu* such that when a menu item is -- changed or added, *update* is called to update the menu in the UI. -- @param menu The menu or table of menus to create a proxy for. @@ -343,7 +432,7 @@ local function proxy_menu(menu, update, menubar) return setmetatable({}, { __index = function(_, k) local v - if type(k) == 'number' then + if type(k) == 'number' or k == 'title' then v = menu[k] elseif type(k) == 'string' then for i = 1, #menu do @@ -378,11 +467,10 @@ local function set_menubar(menubar) _menubar[#_menubar + 1] = ui.menu(read_menu_table(menubar[i])) end ui.menubar = _menubar - items, commands = {}, {} - build_command_tables(menubar, nil, items, commands) proxies.menubar = proxy_menu(menubar, set_menubar) end -set_menubar(default_menubar) +proxies.menubar = proxy_menu(default_menubar, function() end) -- for keys.lua +events.connect(events.INITIALIZED, function() set_menubar(default_menubar) end) -- Sets `ui.context_menu` and `ui.tab_context_menu` from menu item lists -- *buffer_menu* and *tab_menu*, respectively. @@ -408,12 +496,37 @@ local function set_contextmenus(buffer_menu, tab_menu) set_contextmenus(nil, menu) end) end -set_contextmenus() +events.connect(events.INITIALIZED, set_contextmenus) + +-- Performs the appropriate action when clicking a menu item. +events.connect(events.MENU_CLICKED, function(menu_id) + local actions = menu_id < 1000 and menu_actions or contextmenu_actions + local action = actions[menu_id < 1000 and menu_id or menu_id - 1000] + assert(type(action) == 'function' or type(action) == 'table', + _L['Unknown command:']..' '..tostring(action)) + keys.run_command(action, type(action)) +end) --- -- Prompts the user to select a menu command to run. -- @name select_command function M.select_command() + local items, commands = {}, {} + -- Builds the item and commands tables for the filtered list dialog. + -- @param menu The menu to read from. + local function build_command_tables(menu) + for i = 1, #menu do + if menu[i].title then + build_command_tables(menu[i]) + elseif menu[i][1] ~= '' then + local label = menu.title and menu.title..': '..menu[i][1] or menu[i][1] + items[#items + 1] = label:gsub('_([^_])', '%1') + items[#items + 1] = key_shortcuts[get_id(menu[i][2])] or '' + commands[#commands + 1] = menu[i][2] + end + end + end + build_command_tables(getmetatable(M.menubar).menu) local button, i = ui.dialogs.filteredlist{ title = _L['Run Command'], columns = {_L['Command'], _L['Key Command']}, items = items, width = CURSES and ui.size[1] - 2 or nil @@ -422,15 +535,6 @@ function M.select_command() keys.run_command(commands[i], type(commands[i])) end --- Performs the appropriate action when clicking a menu item. -events.connect(events.MENU_CLICKED, function(menu_id) - local actions = menu_id < 1000 and menu_actions or contextmenu_actions - local action = actions[menu_id < 1000 and menu_id or menu_id - 1000] - assert(type(action) == 'function' or type(action) == 'table', - _L['Unknown command:']..' '..tostring(action)) - keys.run_command(action, type(action)) -end) - return setmetatable(M, { __index = function(_, k) return proxies[k] or M[k] end, __newindex = function(_, k, v) -- cgit v1.2.3