diff options
Diffstat (limited to 'modules')
-rw-r--r-- | modules/textadept/command_entry.lua | 47 | ||||
-rw-r--r-- | modules/textadept/find.lua | 361 | ||||
-rw-r--r-- | modules/textadept/init.lua | 6 | ||||
-rw-r--r-- | modules/textadept/key_commands.lua | 652 | ||||
-rw-r--r-- | modules/textadept/menu.lua | 518 |
5 files changed, 1584 insertions, 0 deletions
diff --git a/modules/textadept/command_entry.lua b/modules/textadept/command_entry.lua new file mode 100644 index 00000000..296244f4 --- /dev/null +++ b/modules/textadept/command_entry.lua @@ -0,0 +1,47 @@ +-- Copyright 2007-2010 Mitchell mitchell<att>caladbolg.net. See LICENSE. + +local locale = _G.locale + +events.connect('command_entry_command', + function(command) -- execute a Lua command + local f, err = loadstring(command) + if err then error(err) end + gui.command_entry.focus() -- toggle focus to hide + f() + end) + +events.connect('command_entry_keypress', + function(code) + local ce = gui.command_entry + local KEYSYMS = keys.KEYSYMS + if KEYSYMS[code] == 'esc' then + ce.focus() -- toggle focus to hide + return true + elseif KEYSYMS[code] == '\t' then + local substring = ce.entry_text:match('[%w_.:]+$') or '' + local path, o, prefix = substring:match('^([%w_.:]-)([.:]?)([%w_]*)$') + local ret, tbl = pcall(loadstring('return ('..path..')')) + if not ret then tbl = getfenv(0) end + if type(tbl) ~= 'table' then return end + local cmpls = {} + for k in pairs(tbl) do + if type(k) == 'string' and k:find('^'..prefix) then + cmpls[#cmpls + 1] = k + end + end + if path == 'buffer' then + if o == ':' then + for f in pairs(_SCINTILLA.functions) do + if f:find('^'..prefix) then cmpls[#cmpls + 1] = f end + end + else + for p in pairs(_SCINTILLA.properties) do + if p:find('^'..prefix) then cmpls[#cmpls + 1] = p end + end + end + end + table.sort(cmpls) + ce.show_completions(cmpls) + return true + end + end) diff --git a/modules/textadept/find.lua b/modules/textadept/find.lua new file mode 100644 index 00000000..eafef5ed --- /dev/null +++ b/modules/textadept/find.lua @@ -0,0 +1,361 @@ +-- Copyright 2007-2010 Mitchell mitchell<att>caladbolg.net. See LICENSE. + +local locale = _G.locale +local events = _G.events +local find = gui.find + +local lfs = require 'lfs' + +local MARK_FIND = 0 +local MARK_FIND_COLOR = 0x4D9999 +local previous_view + +-- Text escape sequences with their associated characters. +local escapes = { + ['\\a'] = '\a', ['\\b'] = '\b', ['\\f'] = '\f', ['\\n'] = '\n', + ['\\r'] = '\r', ['\\t'] = '\t', ['\\v'] = '\v', ['\\\\'] = '\\' +} + +-- Finds and selects text in the current buffer. +-- @param text The text to find. +-- @param next Flag indicating whether or not the search direction is forward. +-- @param flags Search flags. This is a number mask of 4 flags: match case (2), +-- whole word (4), Lua pattern (8), and in files (16) joined with binary OR. +-- If nil, this is determined based on the checkboxes in the find box. +-- @param nowrap Flag indicating whether or not the search won't wrap. +-- @param wrapped Utility flag indicating whether or not the search has wrapped +-- for displaying useful statusbar information. This flag is used and set +-- internally, and should not be set otherwise. +-- @return position of the found text or -1 +local function find_(text, next, flags, nowrap, wrapped) + if #text == 0 then return end + local buffer = buffer + local first_visible_line = buffer.first_visible_line -- for 'no results found' + + local increment + if buffer.current_pos == buffer.anchor then + increment = 0 + elseif not wrapped then + increment = next and 1 or -1 + end + + if not flags then + local find, c = find, _SCINTILLA.constants + flags = 0 + if find.match_case then flags = flags + c.SCFIND_MATCHCASE end + if find.whole_word then flags = flags + c.SCFIND_WHOLEWORD end + if find.lua then flags = flags + 8 end + if find.in_files then flags = flags + 16 end + end + + local result + find.captures = nil + + if flags < 8 then + buffer:goto_pos(buffer[next and 'current_pos' or 'anchor'] + increment) + buffer:search_anchor() + if next then + result = buffer:search_next(flags, text) + else + result = buffer:search_prev(flags, text) + end + if result ~= -1 then buffer:scroll_caret() end + + elseif flags < 16 then -- lua pattern search (forward search only) + text = text:gsub('\\[abfnrtv\\]', escapes) + local buffer_text = buffer:get_text(buffer.length) + local results = { buffer_text:find(text, buffer.anchor + increment) } + if #results > 0 then + result = results[1] + find.captures = { unpack(results, 3) } + buffer:set_sel(results[2], result - 1) + else + result = -1 + end + + else -- find in files + local utf8_dir = + gui.dialog('fileselect', + '--title', locale.FIND_IN_FILES_TITLE, + '--select-only-directories', + '--with-directory', + (buffer.filename or ''):match('^.+[/\\]') or '', + '--no-newline') + if #utf8_dir > 0 then + if not find.lua then text = text:gsub('([().*+?^$%%[%]-])', '%%%1') end + if not find.match_case then text = text:lower() end + if find.whole_word then text = '[^%W_]'..text..'[^%W_]' end + local match_case = find.match_case + local whole_word = find.whole_word + local format = string.format + local matches = { 'Find: '..text } + function search_file(file) + local line_num = 1 + for line in io.lines(file) do + local optimized_line = line + if not match_case then optimized_line = line:lower() end + if whole_word then optimized_line = ' '..line..' ' end + if string.find(optimized_line, text) then + file = file:iconv('UTF-8', _CHARSET) + matches[#matches + 1] = format('%s:%s:%s', file, line_num, line) + end + line_num = line_num + 1 + end + end + function search_dir(directory) + for file in lfs.dir(directory) do + if not file:find('^%.%.?$') then -- ignore . and .. + local path = directory..'/'..file + local type = lfs.attributes(path).mode + if type == 'directory' then + search_dir(path) + elseif type == 'file' then + search_file(path) + end + end + end + end + local dir = utf8_dir:iconv(_CHARSET, 'UTF-8') + search_dir(dir) + if #matches == 1 then matches[2] = locale.FIND_NO_RESULTS end + matches[#matches + 1] = '' + if buffer._type ~= locale.FIND_FILES_FOUND_BUFFER then + previous_view = view + end + gui._print(locale.FIND_FILES_FOUND_BUFFER, table.concat(matches, '\n')) + end + return + end + + if result == -1 and not nowrap and not wrapped then -- wrap the search + local anchor, pos = buffer.anchor, buffer.current_pos + if next or flags >= 8 then + buffer:goto_pos(0) + else + buffer:goto_pos(buffer.length) + end + gui.statusbar_text = locale.FIND_SEARCH_WRAPPED + result = find_(text, next, flags, true, true) + if result == -1 then + gui.statusbar_text = locale.FIND_NO_RESULTS + buffer:line_scroll(0, first_visible_line) + buffer:goto_pos(anchor) + end + return result + elseif result ~= -1 and not wrapped then + gui.statusbar_text = '' + end + + return result +end +events.connect('find', find_) + +-- Finds and selects text incrementally in the current buffer from a start +-- point. +-- Flags other than SCFIND_MATCHCASE are ignored. +-- @param text The text to find. +local function find_incremental(text) + local c = _SCINTILLA.constants + local flags = find.match_case and c.SCFIND_MATCHCASE or 0 + --if find.lua then flags = flags + 8 end + buffer:goto_pos(find.incremental_start or 0) + find_(text, true, flags) +end + +-- LuaDoc is in core/.find.lua. +function find.find_incremental() + find.incremental = true + find.incremental_start = buffer.current_pos + gui.command_entry.entry_text = '' + gui.command_entry.focus() +end + +events.connect('command_entry_keypress', + function(code) + if find.incremental then + if code == 0xff1b then -- escape + find.incremental = nil + elseif code < 256 or code == 0xff08 then -- character or backspace + local text = gui.command_entry.entry_text + if code == 0xff08 then + find_incremental(text:sub(1, -2)) + else + find_incremental(text..string.char(code)) + end + end + end + end, 1) -- place before command_entry.lua's handler (if necessary) + +events.connect('command_entry_command', + function(text) -- 'find next' for incremental search + if find.incremental then + find.incremental_start = buffer.current_pos + 1 + find_incremental(text) + return true + end + end, 1) -- place before command_entry.lua's handler (if necessary) + +-- Replaces found text. +-- 'find_' is called first, to select any found text. The selected text is then +-- replaced by the specified replacement text. +-- This function ignores 'Find in Files'. +-- @param rtext The text to replace found text with. It can contain both Lua +-- capture items (%n where 1 <= n <= 9) for Lua pattern searches and %() +-- sequences for embedding Lua code for any search. +-- @see find +local function replace(rtext) + if #buffer:get_sel_text() == 0 then return end + if find.in_files then find.in_files = false end + local buffer = buffer + buffer:target_from_selection() + rtext = rtext:gsub('%%%%', '\\037') -- escape '%%' + if find.captures then + for i, v in ipairs(find.captures) do + v = v:gsub('%%', '%%%%') -- escape '%' for gsub + rtext = rtext:gsub('%%'..i, v) + end + end + local ret, rtext = pcall(rtext.gsub, rtext, '%%(%b())', + function(code) + local ret, val = pcall(loadstring('return '..code)) + if not ret then + gui.dialog('ok-msgbox', + '--title', locale.FIND_ERROR_DIALOG_TITLE, + '--text', locale.FIND_ERROR_DIALOG_TEXT, + '--informative-text', val:gsub('"', '\\"'), + '--no-cancel') + error() + end + return val + end) + if ret then + rtext = rtext:gsub('\\037', '%%') -- unescape '%' + buffer:replace_target(rtext:gsub('\\[abfnrtv\\]', escapes)) + buffer:goto_pos(buffer.target_end) -- 'find' text after this replacement + else + -- Since find is called after replace returns, have it 'find' the current + -- text again, rather than the next occurance so the user can fix the error. + buffer:goto_pos(buffer.current_pos) + end +end +events.connect('replace', replace) + +-- Replaces all found text. +-- If any text is selected, all found text in that selection is replaced. +-- This function ignores 'Find in Files'. +-- @param ftext The text to find. +-- @param rtext The text to replace found text with. +-- @param flags The number mask identical to the one in 'find'. +-- @see find +local function replace_all(ftext, rtext, flags) + if #ftext == 0 then return end + if find.in_files then find.in_files = false end + local buffer = buffer + buffer:begin_undo_action() + local count = 0 + if #buffer:get_sel_text() == 0 then + buffer:goto_pos(0) + while(find_(ftext, true, flags, true) ~= -1) do + replace(rtext) + count = count + 1 + end + else + local anchor, current_pos = buffer.anchor, buffer.current_pos + local s, e = anchor, current_pos + if s > e then s, e = e, s end + buffer:insert_text(e, '\n') + local end_marker = + buffer:marker_add(buffer:line_from_position(e + 1), MARK_FIND) + buffer:goto_pos(s) + local pos = find_(ftext, true, flags, true) + while pos ~= -1 and + pos < buffer:position_from_line( + buffer:marker_line_from_handle(end_marker)) do + replace(rtext) + count = count + 1 + pos = find_(ftext, true, flags, true) + end + e = buffer:position_from_line(buffer:marker_line_from_handle(end_marker)) + buffer:goto_pos(e) + buffer:delete_back() -- delete '\n' added + if s == current_pos then anchor = e - 1 else current_pos = e - 1 end + buffer:set_sel(anchor, current_pos) + buffer:marker_delete_handle(end_marker) + end + gui.statusbar_text = + string.format(locale.FIND_REPLACEMENTS_MADE, tostring(count)) + buffer:end_undo_action() +end +events.connect('replace_all', replace_all) + +-- When the user double-clicks a found file, go to the line in the file the text +-- was found at. +-- @param pos The position of the caret. +-- @param line_num The line double-clicked. +local function goto_file(pos, line_num) + if buffer._type == locale.FIND_FILES_FOUND_BUFFER then + line = buffer:get_line(line_num) + local file, file_line_num = line:match('^(.+):(%d+):.+$') + if file and file_line_num then + buffer:marker_delete_all(MARK_FIND) + buffer:marker_set_back(MARK_FIND, MARK_FIND_COLOR) + buffer:marker_add(line_num, MARK_FIND) + buffer:goto_pos(buffer.current_pos) + if #_VIEWS == 1 then + _, previous_view = view:split(false) -- horizontal + else + local clicked_view = view + if previous_view then previous_view:focus() end + if buffer._type == locale.FIND_FILES_FOUND_BUFFER then + -- there are at least two find in files views; find one of those views + -- that the file was not selected from and focus it + for _, v in ipairs(_VIEWS) do + if v ~= clicked_view then + previous_view = v + v:focus() + break + end + end + end + end + io.open_file(file) + buffer:ensure_visible_enforce_policy(file_line_num - 1) + buffer:goto_line(file_line_num - 1) + end + end +end +events.connect('double_click', goto_file) + +-- LuaDoc is in core/.find.lua. +function find.goto_file_in_list(next) + local orig_view = view + for _, buffer in ipairs(_BUFFERS) do + if buffer._type == locale.FIND_FILES_FOUND_BUFFER then + for _, view in ipairs(_VIEWS) do + if view.doc_pointer == buffer.doc_pointer then + view:focus() + local orig_line = buffer:line_from_position(buffer.current_pos) + local line = orig_line + while true do + line = line + (next and 1 or -1) + if line > buffer.line_count - 1 then line = 0 end + if line < 0 then line = buffer.line_count - 1 end + if line == orig_line then -- prevent infinite loops + orig_view:focus() + return + end + if buffer:get_line(line):match('^(.+):(%d+):.+$') then + buffer:goto_line(line) + goto_file(buffer.current_pos, line) + return + end + end + end + end + end + end +end + +if buffer then buffer:marker_set_back(MARK_FIND, MARK_FIND_COLOR) end +events.connect('view_new', + function() buffer:marker_set_back(MARK_FIND, MARK_FIND_COLOR) end) diff --git a/modules/textadept/init.lua b/modules/textadept/init.lua index bd4245c9..d2c038f3 100644 --- a/modules/textadept/init.lua +++ b/modules/textadept/init.lua @@ -6,8 +6,14 @@ module('_m.textadept', package.seeall) require 'textadept.bookmarks' +require 'textadept.command_entry' require 'textadept.editing' +require 'textadept.find' require 'textadept.mime_types' require 'textadept.run' require 'textadept.session' require 'textadept.snippets' + +-- These need to be loaded last. +require 'textadept.menu' +require 'textadept.key_commands' diff --git a/modules/textadept/key_commands.lua b/modules/textadept/key_commands.lua new file mode 100644 index 00000000..cdfda538 --- /dev/null +++ b/modules/textadept/key_commands.lua @@ -0,0 +1,652 @@ +-- Copyright 2007-2010 Mitchell mitchell<att>caladbolg.net. See LICENSE. + +local locale = _G.locale +local events = _G.events + +--- +-- Manages and defines key commands in Textadept. +-- This set of key commands is pretty standard among other text editors. +module('_m.textadept.keys', package.seeall) + +-- Markdown: +-- ## Overview +-- +-- Key commands are defined in the global table `keys`. Each key-value pair in +-- `keys` consists of either: +-- +-- * A string representing a key command and an associated action table. +-- * A string language name and its associated `keys`-like table. +-- * A string style name and its associated `keys`-like table. +-- * A string representing a key command and its associated `keys`-like table. +-- (This is a keychain sequence.) +-- +-- A key command string is built from a combination of the `CTRL`, `SHIFT`, +-- `ALT`, and `ADD` constants as well as the pressed key itself. The value of +-- `ADD` is inserted between each of `CTRL`, `SHIFT`, `ALT`, and the key. +-- For example: +-- +-- -- keys.lua: +-- CTRL = 'Ctrl' +-- SHIFT = 'Shift' +-- ALT = 'Alt' +-- ADD = '+' +-- -- pressing control, shift, alt and 'a' yields: 'Ctrl+Shift+Alt+A' +-- +-- For key values less than 255, Lua's [`string.char()`][string_char] is used to +-- determine the key's string representation. Otherwise, the +-- [`KEYSYMS`][keysyms] lookup table is used. +-- +-- [string_char]: http://www.lua.org/manual/5.1/manual.html#pdf-string.char +-- [keysyms]: ../modules/keys.html#KEYSYMS +-- +-- An action table is a table consisting of either: +-- +-- * A Lua function followed by a list of arguments to pass to that function. +-- * A string representing a [buffer][buffer] or [view][view] function followed +-- by its respective `'buffer'` or `'view'` string and then any arguments to +-- pass to the resulting function. +-- +-- `buffer.`_`function`_ by itself cannot be used because at the time of +-- evaluation, `buffer.`_`function`_ would apply only to the current +-- buffer, not for all buffers. By using this string reference system, the +-- correct `buffer.`_`function`_ will be evaluated every time. The same +-- applies to `view`. +-- +-- [buffer]: ../modules/buffer.html +-- [view]: ../modules/view.html +-- +-- 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`. +-- +-- Key commands can be chained like in Emacs using keychain sequences. By +-- default, the `Esc` key cancels the current keychain, but it can be redefined +-- by setting the `keys.clear_sequence` field. Naturally, the clear sequence +-- cannot be chained. +-- +-- ## Settings +-- +-- * `SCOPES_ENABLED`: Flag indicating whether scopes/styles can be used for key +-- commands. +-- * `CTRL`: The string representing the Control key. +-- * `SHIFT`: The string representing the Shift key. +-- * `ALT`: The string representing the Alt key (the Apple key on Mac OSX). +-- * `ADD`: The string representing used to join together a sequence of Control, +-- Shift, or Alt modifier keys. +-- +-- ## Key Command Precedence +-- +-- When searching for a key command to execute in the `keys` table, key commands +-- in the current style have priority, followed by the ones in the current lexer, +-- and finally the ones in the global table. +-- +-- ## Example +-- +-- keys = { +-- ['ctrl+f'] = { 'char_right', 'buffer' }, +-- ['ctrl+b'] = { 'char_left', 'buffer' }, +-- lua = { +-- ['ctrl+c'] = { 'add_text', 'buffer', '-- ' }, +-- whitespace = { +-- ['ctrl+f'] = { function() print('whitespace') end } +-- } +-- } +-- } +-- +-- The first two key commands are global and call `buffer:char_right()` and +-- `buffer:char_left()` respectively. The last two commands apply only in the +-- Lua lexer with the very last one only being available in Lua's `whitespace` +-- style. If `ctrl+f` is pressed when the current style is `whitespace` in the +-- `lua` lexer, the global key command with the same shortcut is overriden and +-- `whitespace` is printed to standard out. +-- +-- ## Problems +-- +-- All Lua functions must be defined BEFORE they are reference in key commands. +-- Therefore, any module containing key commands should be loaded after all +-- other modules, whose functions are being referenced, have been loaded. +-- +-- ## Configuration +-- +-- It is not recommended to edit Textadept's `core/ext/key_commands.lua`. You +-- can either override or add to default key commands in your +-- `~/.textadept/key_commands.lua` or `require` a separate module in your +-- `~/.textadept/init.lua` instead of `ext/key_commands`. + +-- Windows and Linux key commands are listed in the first block. +-- Mac OSX key commands are listed in the second block. + +-- settings +local SCOPES_ENABLED = true +local ADD = '' +local CTRL = 'c'..ADD +local SHIFT = 's'..ADD +local ALT = 'a'..ADD +-- end settings + +local keys = _M +local b, v = 'buffer', 'view' +local gui = gui + +-- CTRL = 'c' +-- SHIFT = 's' +-- ALT = 'a' +-- ADD = '' +-- Control, Shift, Alt, and 'a' = 'caA' +-- Control, Shift, Alt, and '\t' = 'csa\t' + +if not MAC then + -- Windows and Linux key commands. + + --[[ + C: D H I J K M U + A: A B C D E F G H J K L M N P R S T U V W X Y Z + CS: A B C D G H I J K L M N O Q T U V X Y Z + SA: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z + CA: A B C D E F G H J K L M N O Q R S T U V W X Y Z + CSA: A B C D E F G H J K L M N O P Q R S T U V W X Y Z + ]]-- + + keys.clear_sequence = 'esc' + + keys.ct = {} -- Textadept command chain + + -- File + local m_session = _m.textadept.session + keys.cn = { new_buffer } + keys.co = { io.open_file } + -- TODO: { 'reload', b } + keys.cs = { 'save', b } + keys.cS = { 'save_as', b } + keys.cw = { 'close', b } + keys.cW = { io.close_all } + -- TODO: { m_session.load } after prompting with open dialog + -- TODO: { m_session.save } after prompting with save dialog + keys.aq = { quit } + + -- Edit + local m_editing = _m.textadept.editing + keys.cz = { 'undo', b } + keys.cy = { 'redo', b } + keys.cx = { 'cut', b } + keys.cc = { 'copy', b } + keys.cv = { 'paste', b } + -- Delete is delete. + keys.ca = { 'select_all', b } + keys.ce = { m_editing.match_brace } + keys.cE = { m_editing.match_brace, 'select' } + keys['c\n'] = { m_editing.autocomplete_word, '%w_' } + keys['c\n\r'] = { m_editing.autocomplete_word, '%w_' } -- win32 + keys.cq = { m_editing.block_comment } + -- TODO: { m_editing.current_word, 'delete' } + -- TODO: { m_editing.transpose_chars } + -- TODO: { m_editing.squeeze } + -- TODO: { m_editing.convert_indentation } + -- TODO: { m_editing.smart_cutcopy } + -- TODO: { m_editing.smart_cutcopy, 'copy' } + keys.ac = { -- enClose in... + t = { m_editing.enclose, 'tag' }, + T = { m_editing.enclose, 'single_tag' }, + ['"'] = { m_editing.enclose, 'dbl_quotes' }, + ["'"] = { m_editing.enclose, 'sng_quotes' }, + ['('] = { m_editing.enclose, 'parens' }, + ['['] = { m_editing.enclose, 'brackets' }, + ['{'] = { m_editing.enclose, 'braces' }, + c = { m_editing.enclose, 'chars' }, + } + keys.as = { -- select in... + t = { m_editing.select_enclosed, 'tags' }, + ['"'] = { m_editing.select_enclosed, 'dbl_quotes' }, + ["'"] = { m_editing.select_enclosed, 'sng_quotes' }, + ['('] = { m_editing.select_enclosed, 'parens' }, + ['['] = { m_editing.select_enclosed, 'brackets' }, + ['{'] = { m_editing.select_enclosed, 'braces' }, + w = { m_editing.current_word, 'select' }, + l = { m_editing.select_line }, + p = { m_editing.select_paragraph }, + b = { m_editing.select_indented_block }, + s = { m_editing.select_scope }, + g = { m_editing.grow_selection, 1 }, + } + + -- Search + keys.cf = { gui.find.focus } -- find/replace + keys['f3'] = { gui.find.find_next } + -- Find Next is an when find pane is focused. + -- Find Prev is ap when find pane is focused. + -- Replace is ar when find pane is focused. + keys.cF = { gui.find.find_incremental } + -- Find in Files is ai when find pane is focused. + -- TODO: { gui.find.goto_file_in_list, true } + -- TODO: { gui.find.goto_file_in_list, false } + keys.cg = { m_editing.goto_line } + + -- Tools + keys['f2'] = { gui.command_entry.focus } + -- Run + local m_run = _m.textadept.run + keys.cr = { m_run.run } + keys.cR = { m_run.compile } + -- Snippets + local m_snippets = _m.textadept.snippets + keys['\t'] = { m_snippets.insert } + keys['s\t'] = { m_snippets.prev } + keys.cai = { m_snippets.cancel_current } + keys.caI = { m_snippets.list } + keys.ai = { m_snippets.show_style } + + -- Buffers + keys.cb = { gui.switch_buffer } + keys['c\t'] = { 'goto_buffer', v, 1, false } + keys['cs\t'] = { 'goto_buffer', v, -1, false } + local function toggle_setting(setting) + local state = buffer[setting] + if type(state) == 'boolean' then + buffer[setting] = not state + elseif type(state) == 'number' then + buffer[setting] = buffer[setting] == 0 and 1 or 0 + end + events.emit('update_ui') -- for updating statusbar + end + keys.ct.v = { + e = { toggle_setting, 'view_eol' }, + w = { toggle_setting, 'wrap_mode' }, + i = { toggle_setting, 'indentation_guides' }, + ['\t'] = { toggle_setting, 'use_tabs' }, + [' '] = { toggle_setting, 'view_ws' }, + } + keys.cl = { _m.textadept.mime_types.select_lexer } + keys['f5'] = { 'colourise', b, 0, -1 } + + -- Views + keys.cav = { + n = { gui.goto_view, 1, false }, + p = { gui.goto_view, -1, false }, + S = { 'split', v }, -- vertical + s = { 'split', v, false }, -- horizontal + w = { function() view:unsplit() return true end }, + W = { function() while view:unsplit() do end end }, + -- TODO: { function() view.size = view.size + 10 end } + -- TODO: { function() view.size = view.size - 10 end } + } + keys.c0 = { function() buffer.zoom = 0 end } + + -- Miscellaneous not in standard menu. + -- Recent files. + local RECENT_FILES = 1 + events.connect('user_list_selection', + function(type, text) + if type == RECENT_FILES then io.open_file(text) end + end) + keys.ao = { + function() + local buffer = buffer + local files = {} + for _, filename in ipairs(io.recent_files) do + table.insert(files, 1, filename) + end + local sep = buffer.auto_c_separator + buffer.auto_c_separator = ('|'):byte() + buffer:user_list_show(RECENT_FILES, table.concat(files, '|')) + buffer.auto_c_separator = sep + end + } + +else + -- Mac OSX key commands + + --[[ + C: J M U W X Z + A: D E H J K L U Y + CS: C D G H I J K L M O Q S T U V W X Y Z + SA: A B C D H I J K L M N O Q R T U V X Y + CA: A C E J K L M N O Q R S T U V W X Y Z + CSA: A C D E H J K L M N O P Q R S T U V W X Y Z + ]]-- + + keys.clear_sequence = 'aesc' + + keys.at = {} -- Textadept command chain + + -- File + local m_session = _m.textadept.session + keys.an = { new_buffer } + keys.ao = { io.open_file } + -- TODO: { 'reload', b } + keys.as = { 'save', b } + keys.aS = { 'save_as', b } + keys.aw = { 'close', b } + keys.aW = { io.close_all } + -- TODO: { m_session.load } after prompting with open dialog + -- TODO: { m_session.save } after prompting with save dialog + keys.aq = { quit } + + -- Edit + local m_editing = _m.textadept.editing + keys.az = { 'undo', b } + keys.aZ = { 'redo', b } + keys.ax = { 'cut', b } + keys.ac = { 'copy', b } + keys.av = { 'paste', b } + -- Delete is delete. + keys.aa = { 'select_all', b } + keys.cm = { m_editing.match_brace } + keys.aE = { m_editing.match_brace, 'select' } + keys.esc = { m_editing.autocomplete_word, '%w_' } + keys.cq = { m_editing.block_comment } + -- TODO: { m_editing.current_word, 'delete' } + keys.ct = { m_editing.transpose_chars } + -- TODO: { m_editing.squeeze } + -- TODO: { m_editing.convert_indentation } + keys.ck = { m_editing.smart_cutcopy } + -- TODO: { m_editing.smart_cutcopy, 'copy' } + keys.cy = { 'paste', b } + keys.cc = { -- enClose in... + t = { m_editing.enclose, 'tag' }, + T = { m_editing.enclose, 'single_tag' }, + ['"'] = { m_editing.enclose, 'dbl_quotes' }, + ["'"] = { m_editing.enclose, 'sng_quotes' }, + ['('] = { m_editing.enclose, 'parens' }, + ['['] = { m_editing.enclose, 'brackets' }, + ['{'] = { m_editing.enclose, 'braces' }, + c = { m_editing.enclose, 'chars' }, + } + keys.cs = { -- select in... + e = { m_editing.select_enclosed }, + t = { m_editing.select_enclosed, 'tags' }, + ['"'] = { m_editing.select_enclosed, 'dbl_quotes' }, + ["'"] = { m_editing.select_enclosed, 'sng_quotes' }, + ['('] = { m_editing.select_enclosed, 'parens' }, + ['['] = { m_editing.select_enclosed, 'brackets' }, + ['{'] = { m_editing.select_enclosed, 'braces' }, + w = { m_editing.current_word, 'select' }, + l = { m_editing.select_line }, + p = { m_editing.select_paragraph }, + b = { m_editing.select_indented_block }, + s = { m_editing.select_scope }, + g = { m_editing.grow_selection, 1 }, + } + + -- Search + keys.af = { gui.find.focus } -- find/replace + keys.ag = { gui.find.find_next } + keys.aG = { gui.find.find_prev } + keys.ar = { gui.find.replace } + keys.ai = { gui.find.find_incremental } + keys.aF = { + function() + gui.find.in_files = true + gui.find.focus() + end + } + keys.cag = { gui.find.goto_file_in_list, true } + keys.caG = { gui.find.goto_file_in_list, false } + keys.cg = { m_editing.goto_line } + + -- Tools + keys['f2'] = { gui.command_entry.focus } + -- Run + local m_run = _m.textadept.run + keys.cr = { m_run.run } + keys.cR = { m_run.compile } + -- Snippets + local m_snippets = _m.textadept.snippets + keys['\t'] = { m_snippets.insert } + keys['s\t'] = { m_snippets.prev } + keys.cai = { m_snippets.cancel_current } + keys.caI = { m_snippets.list } + keys.ci = { m_snippets.show_style } + + -- Buffers + keys.ab = { gui.switch_buffer } + keys['c\t'] = { 'goto_buffer', v, 1, false } + keys['cs\t'] = { 'goto_buffer', v, -1, false } + local function toggle_setting(setting) + local state = buffer[setting] + if type(state) == 'boolean' then + buffer[setting] = not state + elseif type(state) == 'number' then + buffer[setting] = buffer[setting] == 0 and 1 or 0 + end + events.emit('update_ui') -- for updating statusbar + end + keys.at.v = { + e = { toggle_setting, 'view_eol' }, + w = { toggle_setting, 'wrap_mode' }, + i = { toggle_setting, 'indentation_guides' }, + ['\t'] = { toggle_setting, 'use_tabs' }, + [' '] = { toggle_setting, 'view_ws' }, + } + keys.cl = { _m.textadept.mime_types.select_lexer } + keys['f5'] = { 'colourise', b, 0, -1 } + + -- Views + keys.cv = { + n = { gui.goto_view, 1, false }, + p = { gui.goto_view, -1, false }, + S = { 'split', v }, -- vertical + s = { 'split', v, false }, -- horizontal + w = { function() view:unsplit() return true end }, + W = { function() while view:unsplit() do end end }, + -- TODO: { function() view.size = view.size + 10 end } + -- TODO: { function() view.size = view.size - 10 end } + } + keys.c0 = { function() buffer.zoom = 0 end } + + -- Miscellaneous not in standard menu. + -- Recent files. + local RECENT_FILES = 1 + events.connect('user_list_selection', + function(type, text) + if type == RECENT_FILES then io.open_file(text) end + end) + keys.co = { + function() + local buffer = buffer + local files = {} + for _, filename in ipairs(io.recent_files) do + table.insert(files, 1, filename) + end + local sep = buffer.auto_c_separator + buffer.auto_c_separator = ('|'):byte() + buffer:user_list_show(RECENT_FILES, table.concat(files, '|')) + buffer.auto_c_separator = sep + end + } + + -- Movement/selection commands + keys.cf = { 'char_right', b } + keys.cF = { 'char_right_extend', b } + keys.caf = { 'word_right', b } + keys.caF = { 'word_right_extend', b } + keys.cb = { 'char_left', b } + keys.cB = { 'char_left_extend', b } + keys.cab = { 'word_left', b } + keys.caB = { 'word_left_extend', b } + keys.cn = { 'line_down', b } + keys.cN = { 'line_down_extend', b } + keys.cp = { 'line_up', b } + keys.cP = { 'line_up_extend', b } + keys.ca = { 'vc_home', b } + keys.cA = { 'home_extend', b } + keys.ce = { 'line_end', b } + keys.cE = { 'line_end_extend', b } + keys.ch = { 'delete_back', b } + keys.cah = { 'del_word_left', b } + keys.cd = { 'clear', b } + keys.cad = { 'del_word_right', b } +end + +user_dofile('key_commands.lua') -- load user key commands + +-- Do not edit below this line. + +-- optimize for speed +local string = _G.string +local string_char = string.char +local string_format = string.format +local pcall = _G.pcall +local ipairs = _G.ipairs +local next = _G.next +local type = _G.type +local unpack = _G.unpack +local MAC = _G.MAC + +--- +-- Lookup table for key values higher than 255. +-- If a key value given to 'keypress' is higher than 255, this table is used to +-- return a string representation of the key if it exists. +-- @class table +-- @name KEYSYMS +KEYSYMS = { -- from <gdk/gdkkeysyms.h> + [65056] = '\t', -- backtab; will be 'shift'ed + [65288] = '\b', + [65289] = '\t', + [65293] = '\n', + [65307] = 'esc', + [65535] = 'del', + [65360] = 'home', + [65361] = 'left', + [65362] = 'up', + [65363] = 'right', + [65364] = 'down', + [65365] = 'pup', + [65366] = 'pdown', + [65367] = 'end', + [65379] = 'ins', + [65470] = 'f1', [65471] = 'f2', [65472] = 'f3', [65473] = 'f4', + [65474] = 'f5', [65475] = 'f6', [65476] = 'f7', [65477] = 'f8', + [65478] = 'f9', [65479] = 'f10', [65480] = 'f11', [65481] = 'f12', +} + +-- The current key sequence. +local keychain = {} + +-- Clears the current key sequence. +local function clear_key_sequence() + keychain = {} + gui.statusbar_text = '' +end + +-- Helper function that gets commands associated with the current keychain from +-- 'keys'. +-- If the current item in the keychain is part of a chain, throw an error value +-- of -1. This way, pcall will return false and -1, where the -1 can easily and +-- efficiently be checked rather than using a string error message. +local function try_get_cmd(active_table) + for _, key_seq in ipairs(keychain) do active_table = active_table[key_seq] end + if #active_table == 0 and next(active_table) then + gui.statusbar_text = locale.KEYCHAIN..table.concat(keychain, ' ') + error(-1, 0) + else + local func = active_table[1] + if type(func) == 'function' then + return func, { unpack(active_table, 2) } + elseif type(func) == 'string' then + local object = active_table[2] + if object == 'buffer' then + return buffer[func], { buffer, unpack(active_table, 3) } + elseif object == 'view' then + return view[func], { view, unpack(active_table, 3) } + end + else + error(locale.KEYS_UNKNOWN_COMMAND..tostring(func)) + end + end +end + +-- Tries to get a key command based on the lexer and current scope. +local function try_get_cmd1(keys, lexer, scope) + return try_get_cmd(keys[lexer][scope]) +end + +-- Tries to get a key command based on the lexer. +local function try_get_cmd2(keys, lexer) + return try_get_cmd(keys[lexer]) +end + +-- Tries to get a global key command. +local function try_get_cmd3(keys) + return try_get_cmd(keys) +end + +-- Handles Textadept keypresses. +-- It is called every time a key is pressed, and based on lexer and scope, +-- executes a command. The command is looked up in the global 'keys' key +-- command table. +-- @return whatever the executed command returns, true by default. A true +-- return value will tell Textadept not to handle the key afterwords. +local function keypress(code, shift, control, alt) + local buffer = buffer + local key + --print(code, string.char(code)) + if code < 256 then + key = string_char(code) + shift = false -- for printable characters, key is upper case + if MAC and not shift and not control and not alt then + local ch = string_char(code) + -- work around native GTK-OSX's handling of Alt key + if ch:find('[%p%d]') and #keychain == 0 then + if buffer.anchor ~= buffer.current_pos then buffer:delete_back() end + buffer:add_text(ch) + events.emit('char_added', code) + return true + end + end + else + if not KEYSYMS[code] then return end + key = KEYSYMS[code] + end + control = control and CTRL or '' + shift = shift and SHIFT or '' + alt = alt and ALT or '' + local key_seq = string_format('%s%s%s%s', control, shift, alt, key) + + if #keychain > 0 and key_seq == keys.clear_sequence then + clear_key_sequence() + return true + end + + local lexer = buffer:get_lexer_language() + keychain[#keychain + 1] = key_seq + local ret, func, args + if SCOPES_ENABLED then + local style = buffer.style_at[buffer.current_pos] + local scope = buffer:get_style_name(style) + --print(key_seq, 'Lexer: '..lexer, 'Scope: '..scope) + ret, func, args = pcall(try_get_cmd1, keys, lexer, scope) + end + if not ret and func ~= -1 then + ret, func, args = pcall(try_get_cmd2, keys, lexer) + end + if not ret and func ~= -1 then + ret, func, args = pcall(try_get_cmd3, keys) + end + + if ret then + clear_key_sequence() + if type(func) == 'function' then + local ret, retval = pcall(func, unpack(args)) + if ret then + if type(retval) == 'boolean' then return retval end + else + error(retval) + end + end + return true + else + -- Clear key sequence because it's not part of a chain. + -- (try_get_cmd throws error number -1.) + if func ~= -1 then + local size = #keychain - 1 + clear_key_sequence() + if size > 0 then -- previously in a chain + gui.statusbar_text = locale.KEYS_INVALID + return true + end + else + return true + end + end +end +events.connect('keypress', keypress, 1) diff --git a/modules/textadept/menu.lua b/modules/textadept/menu.lua new file mode 100644 index 00000000..de1a3a59 --- /dev/null +++ b/modules/textadept/menu.lua @@ -0,0 +1,518 @@ +-- Copyright 2007-2010 Mitchell mitchell<att>caladbolg.net. See LICENSE. + +local locale = _G.locale +local events = _G.events + +--- +-- Provides dynamic menus for Textadept. +-- This module, like ext/key_commands, should be 'require'ed last. +module('_m.textadept.menu', package.seeall) + +local gui = gui +local l = locale +local gtkmenu = gui.gtkmenu + +local SEPARATOR = 'separator' +local ID = { + SEPARATOR = 0, + -- File + NEW = 101, + OPEN = 102, + RELOAD = 103, + SAVE = 104, + SAVEAS = 105, + CLOSE = 106, + CLOSE_ALL = 107, + LOAD_SESSION = 108, + SAVE_SESSION = 109, + QUIT = 110, + -- Edit + UNDO = 201, + REDO = 202, + CUT = 203, + COPY = 204, + PASTE = 205, + DELETE = 206, + SELECT_ALL = 207, + MATCH_BRACE = 208, + SELECT_TO_BRACE = 209, + COMPLETE_WORD = 210, + DELETE_WORD = 211, + TRANSPOSE_CHARACTERS = 212, + SQUEEZE = 213, + JOIN_LINES = 245, + CONVERT_INDENTATION = 216, + ENCLOSE_IN_HTML_TAGS = 224, + ENCLOSE_IN_HTML_SINGLE_TAG = 225, + ENCLOSE_IN_DOUBLE_QUOTES = 226, + ENCLOSE_IN_SINGLE_QUOTES = 227, + ENCLOSE_IN_PARENTHESES = 228, + ENCLOSE_IN_BRACKETS = 229, + ENCLOSE_IN_BRACES = 230, + ENCLOSE_IN_CHARACTER_SEQUENCE = 231, + GROW_SELECTION = 232, + SELECT_IN_HTML_TAG = 234, + SELECT_IN_DOUBLE_QUOTE = 235, + SELECT_IN_SINGLE_QUOTE = 236, + SELECT_IN_PARENTHESIS = 237, + SELECT_IN_BRACKET = 238, + SELECT_IN_BRACE = 239, + SELECT_IN_WORD = 240, + SELECT_IN_LINE = 241, + SELECT_IN_PARAGRAPH = 242, + SELECT_IN_INDENTED_BLOCK = 243, + SELECT_IN_SCOPE = 244, + -- Tools + FIND = 301, + FIND_NEXT = 302, + FIND_PREV = 303, + FIND_AND_REPLACE = 304, + REPLACE = 305, + REPLACE_ALL = 306, + FIND_IN_FILES = 308, + FIND_INCREMENTAL = 311, + GOTO_NEXT_FILE_FOUND = 309, + GOTO_PREV_FILE_FOUND = 310, + GOTO_LINE = 307, + FOCUS_COMMAND_ENTRY = 401, + RUN = 420, + COMPILE = 421, + INSERT_SNIPPET = 402, + PREVIOUS_SNIPPET_PLACEHOLDER = 403, + CANCEL_SNIPPET = 404, + LIST_SNIPPETS = 405, + SHOW_SCOPE = 406, + ADD_MULTIPLE_LINE = 407, + ADD_MULTIPLE_LINES = 408, + REMOVE_MULTIPLE_LINE = 409, + REMOVE_MULTIPLE_LINES = 410, + UPDATE_MULTIPLE_LINES = 411, + FINISH_MULTIPLE_LINES = 412, + TOGGLE_BOOKMARK = 416, + CLEAR_BOOKMARKS = 417, + GOTO_NEXT_BOOKMARK = 418, + GOTO_PREV_BOOKMARK = 419, + -- Buffer + NEXT_BUFFER = 501, + PREV_BUFFER = 502, + TOGGLE_VIEW_EOL = 503, + TOGGLE_WRAP_MODE = 504, + TOGGLE_SHOW_INDENT_GUIDES = 505, + TOGGLE_USE_TABS = 506, + TOGGLE_VIEW_WHITESPACE = 507, + EOL_MODE_CRLF = 509, + EOL_MODE_CR = 510, + EOL_MODE_LF = 511, + ENCODING_UTF8 = 512, + ENCODING_ASCII = 513, + ENCODING_ISO88591 = 514, + ENCODING_MACROMAN = 515, + ENCODING_UTF16 = 516, + REFRESH_SYNTAX_HIGHLIGHTING = 508, + SWITCH_BUFFER = 517, + -- View + NEXT_VIEW = 601, + PREV_VIEW = 602, + SPLIT_VIEW_VERTICAL = 603, + SPLIT_VIEW_HORIZONTAL = 604, + UNSPLIT_VIEW = 605, + UNSPLIT_ALL_VIEWS = 606, + GROW_VIEW = 607, + SHRINK_VIEW = 608, + -- Lexers (will be generated dynamically) + LEXER_START = 801, + -- Help + MANUAL = 901, + LUADOC = 902, + ABOUT = 903, +} + + +local menubar = { + gtkmenu { + title = l.MENU_FILE_TITLE, + { l.MENU_FILE_NEW, ID.NEW }, + { l.MENU_FILE_OPEN, ID.OPEN }, + { l.MENU_FILE_RELOAD, ID.RELOAD }, + { l.MENU_FILE_SAVE, ID.SAVE }, + { l.MENU_FILE_SAVEAS, ID.SAVEAS }, + { SEPARATOR, ID.SEPARATOR }, + { l.MENU_FILE_CLOSE, ID.CLOSE }, + { l.MENU_FILE_CLOSE_ALL, ID.CLOSE_ALL }, + { SEPARATOR, ID.SEPARATOR }, + { l.MENU_FILE_LOAD_SESSION, ID.LOAD_SESSION }, + { l.MENU_FILE_SAVE_SESSION, ID.SAVE_SESSION }, + { SEPARATOR, ID.SEPARATOR }, + { l.MENU_FILE_QUIT, ID.QUIT }, + }, + gtkmenu { + title = l.MENU_EDIT_TITLE, + { l.MENU_EDIT_UNDO, ID.UNDO }, + { l.MENU_EDIT_REDO, ID.REDO }, + { SEPARATOR, ID.SEPARATOR }, + { l.MENU_EDIT_CUT, ID.CUT }, + { l.MENU_EDIT_COPY, ID.COPY }, + { l.MENU_EDIT_PASTE, ID.PASTE }, + { l.MENU_EDIT_DELETE, ID.DELETE }, + { l.MENU_EDIT_SELECT_ALL, ID.SELECT_ALL }, + { SEPARATOR, ID.SEPARATOR }, + { l.MENU_EDIT_MATCH_BRACE, ID.MATCH_BRACE }, + { l.MENU_EDIT_SELECT_TO_BRACE, ID.SELECT_TO_BRACE }, + { l.MENU_EDIT_COMPLETE_WORD, ID.COMPLETE_WORD }, + { l.MENU_EDIT_DELETE_WORD, ID.DELETE_WORD }, + { l.MENU_EDIT_TRANSPOSE_CHARACTERS, ID.TRANSPOSE_CHARACTERS }, + { l.MENU_EDIT_SQUEEZE, ID.SQUEEZE }, + { l.MENU_EDIT_JOIN_LINES, ID.JOIN_LINES }, + { l.MENU_EDIT_CONVERT_INDENTATION, ID.CONVERT_INDENTATION }, + { title = l.MENU_EDIT_SEL_TITLE, + { title = l.MENU_EDIT_SEL_ENC_TITLE, + { l.MENU_EDIT_SEL_ENC_HTML_TAGS, ID.ENCLOSE_IN_HTML_TAGS }, + { l.MENU_EDIT_SEL_ENC_HTML_SINGLE_TAG, ID.ENCLOSE_IN_HTML_SINGLE_TAG }, + { l.MENU_EDIT_SEL_ENC_DOUBLE_QUOTES, ID.ENCLOSE_IN_DOUBLE_QUOTES }, + { l.MENU_EDIT_SEL_ENC_SINGLE_QUOTES, ID.ENCLOSE_IN_SINGLE_QUOTES }, + { l.MENU_EDIT_SEL_ENC_PARENTHESES, ID.ENCLOSE_IN_PARENTHESES }, + { l.MENU_EDIT_SEL_ENC_BRACKETS, ID.ENCLOSE_IN_BRACKETS }, + { l.MENU_EDIT_SEL_ENC_BRACES, ID.ENCLOSE_IN_BRACES }, + { l.MENU_EDIT_SEL_ENC_CHAR_SEQ, ID.ENCLOSE_IN_CHARACTER_SEQUENCE }, + }, + { l.MENU_EDIT_SEL_GROW, ID.GROW_SELECTION }, + }, + { title = l.MENU_EDIT_SEL_IN_TITLE, + { l.MENU_EDIT_SEL_IN_HTML_TAG, ID.SELECT_IN_HTML_TAG }, + { l.MENU_EDIT_SEL_IN_DOUBLE_QUOTE, ID.SELECT_IN_DOUBLE_QUOTE }, + { l.MENU_EDIT_SEL_IN_SINGLE_QUOTE, ID.SELECT_IN_SINGLE_QUOTE }, + { l.MENU_EDIT_SEL_IN_PARENTHESIS, ID.SELECT_IN_PARENTHESIS }, + { l.MENU_EDIT_SEL_IN_BRACKET, ID.SELECT_IN_BRACKET }, + { l.MENU_EDIT_SEL_IN_BRACE, ID.SELECT_IN_BRACE }, + { l.MENU_EDIT_SEL_IN_WORD, ID.SELECT_IN_WORD }, + { l.MENU_EDIT_SEL_IN_LINE, ID.SELECT_IN_LINE }, + { l.MENU_EDIT_SEL_IN_PARAGRAPH, ID.SELECT_IN_PARAGRAPH }, + { l.MENU_EDIT_SEL_IN_INDENTED_BLOCK, ID.SELECT_IN_INDENTED_BLOCK }, + { l.MENU_EDIT_SEL_IN_SCOPE, ID.SELECT_IN_SCOPE }, + }, + }, + gtkmenu { + title = l.MENU_TOOLS_TITLE, + { title = l.MENU_TOOLS_SEARCH_TITLE, + { l.MENU_TOOLS_SEARCH_FIND, ID.FIND }, + { l.MENU_TOOLS_SEARCH_FIND_NEXT, ID.FIND_NEXT }, + { l.MENU_TOOLS_SEARCH_FIND_PREV, ID.FIND_PREV }, + { l.MENU_TOOLS_SEARCH_FIND_AND_REPLACE, ID.FIND_AND_REPLACE }, + { l.MENU_TOOLS_SEARCH_REPLACE, ID.REPLACE }, + { l.MENU_TOOLS_SEARCH_REPLACE_ALL, ID.REPLACE_ALL }, + { l.MENU_TOOLS_SEARCH_FIND_INCREMENTAL, ID.FIND_INCREMENTAL }, + { SEPARATOR, ID.SEPARATOR }, + { l.MENU_TOOLS_SEARCH_FIND_IN_FILES, ID.FIND_IN_FILES }, + { l.MENU_TOOLS_SEARCH_GOTO_NEXT_FILE_FOUND, ID.GOTO_NEXT_FILE_FOUND }, + { l.MENU_TOOLS_SEARCH_GOTO_PREV_FILE_FOUND, ID.GOTO_PREV_FILE_FOUND }, + { SEPARATOR, ID.SEPARATOR }, + { l.MENU_TOOLS_SEARCH_GOTO_LINE, ID.GOTO_LINE }, + }, + { l.MENU_TOOLS_FOCUS_COMMAND_ENTRY, ID.FOCUS_COMMAND_ENTRY }, + { SEPARATOR, ID.SEPARATOR }, + { l.MENU_TOOLS_RUN, ID.RUN }, + { l.MENU_TOOLS_COMPILE, ID.COMPILE }, + { SEPARATOR, ID.SEPARATOR }, + { title = l.MENU_TOOLS_SNIPPETS_TITLE, + { l.MENU_TOOLS_SNIPPETS_INSERT, ID.INSERT_SNIPPET }, + { l.MENU_TOOLS_SNIPPETS_PREV_PLACE, ID.PREVIOUS_SNIPPET_PLACEHOLDER }, + { l.MENU_TOOLS_SNIPPETS_CANCEL, ID.CANCEL_SNIPPET }, + { l.MENU_TOOLS_SNIPPETS_LIST, ID.LIST_SNIPPETS }, + { l.MENU_TOOLS_SNIPPETS_SHOW_SCOPE, ID.SHOW_SCOPE }, + }, + { title = l.MENU_TOOLS_BM_TITLE, + { l.MENU_TOOLS_BM_TOGGLE, ID.TOGGLE_BOOKMARK }, + { l.MENU_TOOLS_BM_CLEAR_ALL, ID.CLEAR_BOOKMARKS }, + { l.MENU_TOOLS_BM_NEXT, ID.GOTO_NEXT_BOOKMARK }, + { l.MENU_TOOLS_BM_PREV, ID.GOTO_PREV_BOOKMARK }, + }, + }, + gtkmenu { + title = l.MENU_BUF_TITLE, + { l.MENU_BUF_NEXT, ID.NEXT_BUFFER }, + { l.MENU_BUF_PREV, ID.PREV_BUFFER }, + { l.MENU_BUF_SWITCH, ID.SWITCH_BUFFER }, + { SEPARATOR, ID.SEPARATOR }, + { l.MENU_BUF_TOGGLE_VIEW_EOL, ID.TOGGLE_VIEW_EOL }, + { l.MENU_BUF_TOGGLE_WRAP, ID.TOGGLE_WRAP_MODE }, + { l.MENU_BUF_TOGGLE_INDENT_GUIDES, ID.TOGGLE_SHOW_INDENT_GUIDES }, + { l.MENU_BUF_TOGGLE_TABS, ID.TOGGLE_USE_TABS }, + { l.MENU_BUF_TOGGLE_VIEW_WHITESPACE, ID.TOGGLE_VIEW_WHITESPACE }, + { SEPARATOR, ID.SEPARATOR }, + { title = l.MENU_BUF_EOL_MODE_TITLE, + { l.MENU_BUF_EOL_MODE_CRLF, ID.EOL_MODE_CRLF }, + { l.MENU_BUF_EOL_MODE_CR, ID.EOL_MODE_CR }, + { l.MENU_BUF_EOL_MODE_LF, ID.EOL_MODE_LF }, + }, + { title = l.MENU_BUF_ENCODING_TITLE, + { l.MENU_BUF_ENCODING_UTF8, ID.ENCODING_UTF8 }, + { l.MENU_BUF_ENCODING_ASCII, ID.ENCODING_ASCII }, + { l.MENU_BUF_ENCODING_ISO88591, ID.ENCODING_ISO88591 }, + { l.MENU_BUF_ENCODING_MACROMAN, ID.ENCODING_MACROMAN }, + { l.MENU_BUF_ENCODING_UTF16, ID.ENCODING_UTF16 }, + }, + { SEPARATOR, ID.SEPARATOR }, + { l.MENU_BUF_REFRESH, ID.REFRESH_SYNTAX_HIGHLIGHTING }, + }, + gtkmenu { + title = l.MENU_VIEW_TITLE, + { l.MENU_VIEW_NEXT, ID.NEXT_VIEW }, + { l.MENU_VIEW_PREV, ID.PREV_VIEW }, + { SEPARATOR, ID.SEPARATOR }, + { l.MENU_VIEW_SPLIT_VERTICAL, ID.SPLIT_VIEW_VERTICAL }, + { l.MENU_VIEW_SPLIT_HORIZONTAL, ID.SPLIT_VIEW_HORIZONTAL }, + { l.MENU_VIEW_UNSPLIT, ID.UNSPLIT_VIEW }, + { l.MENU_VIEW_UNSPLIT_ALL, ID.UNSPLIT_ALL_VIEWS }, + { SEPARATOR, ID.SEPARATOR }, + { l.MENU_VIEW_GROW, ID.GROW_VIEW }, + { l.MENU_VIEW_SHRINK, ID.SHRINK_VIEW }, + }, + -- Lexer menu inserted here + gtkmenu { + title = l.MENU_HELP_TITLE, + { l.MENU_HELP_MANUAL, ID.MANUAL }, + { l.MENU_HELP_LUADOC, ID.LUADOC }, + { SEPARATOR, ID.SEPARATOR }, + { l.MENU_HELP_ABOUT, ID.ABOUT }, + }, +} +local lexer_menu = { title = l.MENU_LEX_TITLE } +for _, lexer in ipairs(_m.textadept.mime_types.lexers) do + lexer_menu[#lexer_menu + 1] = { lexer, ID.LEXER_START + #lexer_menu } +end +table.insert(menubar, #menubar, gtkmenu(lexer_menu)) -- before 'Help' +gui.menubar = menubar + +local b, v = 'buffer', 'view' +local m_snippets = _m.textadept.snippets +local m_editing = _m.textadept.editing +local m_bookmarks = _m.textadept.bookmarks +local m_run = _m.textadept.run + +local function set_encoding(encoding) + buffer:set_encoding(encoding) + events.emit('update_ui') -- for updating statusbar +end +local function toggle_setting(setting) + local state = buffer[setting] + if type(state) == 'boolean' then + buffer[setting] = not state + elseif type(state) == 'number' then + buffer[setting] = buffer[setting] == 0 and 1 or 0 + end + events.emit('update_ui') -- for updating statusbar +end +local function set_eol_mode(mode) + buffer.eol_mode = mode + buffer:convert_eo_ls(mode) + events.emit('update_ui') -- for updating statusbar +end +local function set_lexer(lexer) + buffer:set_lexer(lexer) + buffer:colourise(0, -1) + events.emit('update_ui') -- for updating statusbar +end +local function open_webpage(url) + local cmd + if WIN32 then + cmd = string.format('start "" "%s"', url) + local p = io.popen(cmd) + if not p then error(l.MENU_BROWSER_ERROR..url) end + else + cmd = string.format(MAC and 'open "file://%s"' or 'xdg-open "%s" &', url) + if os.execute(cmd) ~= 0 then error(l.MENU_BROWSER_ERROR..url) end + end +end + +local actions = { + -- File + [ID.NEW] = { new_buffer }, + [ID.OPEN] = { io.open_file }, + [ID.RELOAD] = { 'reload', b }, + [ID.SAVE] = { 'save', b }, + [ID.SAVEAS] = { 'save_as', b }, + [ID.CLOSE] = { 'close', b }, + [ID.CLOSE_ALL] = { io.close_all }, + [ID.LOAD_SESSION] = { + function() + local utf8_filename = + gui.dialog('fileselect', + '--title', l.MENU_LOAD_SESSION_TITLE, + '--with-directory', + (_SESSIONFILE or ''):match('.+[/\\]') or '', + '--with-file', + (_SESSIONFILE or ''):match('[^/\\]+$') or '', + '--no-newline') + if #utf8_filename > 0 then + _m.textadept.session.load(utf8_filename:iconv(_CHARSET, 'UTF-8')) + end + end + }, + [ID.SAVE_SESSION] = { + function() + local utf8_filename = + gui.dialog('filesave', + '--title', l.MENU_SAVE_SESSION_TITLE, + '--with-directory', + (_SESSIONFILE or ''):match('.+[/\\]') or '', + '--with-file', + (_SESSIONFILE or ''):match('[^/\\]+$') or '', + '--no-newline') + if #utf8_filename > 0 then + _m.textadept.session.save(utf8_filename:iconv(_CHARSET, 'UTF-8')) + end + end + }, + [ID.QUIT] = { quit }, + -- Edit + [ID.UNDO] = { 'undo', b }, + [ID.REDO] = { 'redo', b }, + [ID.CUT] = { 'cut', b }, + [ID.COPY] = { 'copy', b }, + [ID.PASTE] = { 'paste', b }, + [ID.DELETE] = { 'clear', b }, + [ID.SELECT_ALL] = { 'select_all', b }, + [ID.MATCH_BRACE] = { m_editing.match_brace }, + [ID.SELECT_TO_BRACE] = { m_editing.match_brace, 'select' }, + [ID.COMPLETE_WORD] = { m_editing.autocomplete_word, '%w_' }, + [ID.DELETE_WORD] = { m_editing.current_word, 'delete' }, + [ID.TRANSPOSE_CHARACTERS] = { m_editing.transpose_chars }, + [ID.SQUEEZE] = { m_editing.squeeze }, + [ID.JOIN_LINES] = { m_editing.join_lines }, + [ID.CONVERT_INDENTATION] = { m_editing.convert_indentation }, + -- Edit -> Selection -> Enclose in... + [ID.ENCLOSE_IN_HTML_TAGS] = { m_editing.enclose, 'tag' }, + [ID.ENCLOSE_IN_HTML_SINGLE_TAG] = { m_editing.enclose, 'single_tag' }, + [ID.ENCLOSE_IN_DOUBLE_QUOTES] = { m_editing.enclose, 'dbl_quotes' }, + [ID.ENCLOSE_IN_SINGLE_QUOTES] = { m_editing.enclose, 'sng_quotes' }, + [ID.ENCLOSE_IN_PARENTHESES] = { m_editing.enclose, 'parens' }, + [ID.ENCLOSE_IN_BRACKETS] = { m_editing.enclose, 'brackets' }, + [ID.ENCLOSE_IN_BRACES] = { m_editing.enclose, 'braces' }, + [ID.ENCLOSE_IN_CHARACTER_SEQUENCE] = { m_editing.enclose, 'chars' }, + -- Edit -> Selection + [ID.GROW_SELECTION] = { m_editing.grow_selection, 1 }, + -- Edit -> Select In... + [ID.SELECT_IN_HTML_TAG] = { m_editing.select_enclosed, 'tags' }, + [ID.SELECT_IN_DOUBLE_QUOTE] = { m_editing.select_enclosed, 'dbl_quotes' }, + [ID.SELECT_IN_SINGLE_QUOTE] = { m_editing.select_enclosed, 'sng_quotes' }, + [ID.SELECT_IN_PARENTHESIS] = { m_editing.select_enclosed, 'parens' }, + [ID.SELECT_IN_BRACKET] = { m_editing.select_enclosed, 'brackets' }, + [ID.SELECT_IN_BRACE] = { m_editing.select_enclosed, 'braces' }, + [ID.SELECT_IN_WORD] = { m_editing.current_word, 'select' }, + [ID.SELECT_IN_LINE] = { m_editing.select_line }, + [ID.SELECT_IN_PARAGRAPH] = { m_editing.select_paragraph }, + [ID.SELECT_IN_INDENTED_BLOCK] = { m_editing.select_indented_block }, + [ID.SELECT_IN_SCOPE] = { m_editing.select_scope }, + -- Tools + [ID.FIND] = { gui.find.focus }, + [ID.FIND_NEXT] = { gui.find.call_find_next }, + [ID.FIND_PREV] = { gui.find.call_find_prev }, + [ID.FIND_AND_REPLACE] = { gui.find.focus }, + [ID.REPLACE] = { gui.find.call_replace }, + [ID.REPLACE_ALL] = { gui.find.call_replace_all }, + [ID.FIND_INCREMENTAL] = { gui.find.find_incremental }, + [ID.FIND_IN_FILES] = { + function() + gui.find.in_files = true + gui.find.focus() + end + }, + [ID.GOTO_NEXT_FILE_FOUND] = { gui.find.goto_file_in_list, true }, + [ID.GOTO_PREV_FILE_FOUND] = { gui.find.goto_file_in_list, false }, + [ID.GOTO_LINE] = { m_editing.goto_line }, + [ID.FOCUS_COMMAND_ENTRY] = { gui.command_entry.focus }, + [ID.RUN] = { m_run.run }, + [ID.COMPILE] = { m_run.compile }, + -- Tools -> Snippets + [ID.INSERT_SNIPPET] = { m_snippets.insert }, + [ID.PREVIOUS_SNIPPET_PLACEHOLDER] = { m_snippets.prev }, + [ID.CANCEL_SNIPPET] = { m_snippets.cancel_current }, + [ID.LIST_SNIPPETS] = { m_snippets.list }, + [ID.SHOW_SCOPE] = { m_snippets.show_style }, + -- Tools -> Bookmark + [ID.TOGGLE_BOOKMARK] = { m_bookmarks.toggle }, + [ID.CLEAR_BOOKMARKS] = { m_bookmarks.clear }, + [ID.GOTO_NEXT_BOOKMARK] = { m_bookmarks.goto_next }, + [ID.GOTO_PREV_BOOKMARK] = { m_bookmarks.goto_prev }, + -- Buffer + [ID.NEXT_BUFFER] = { 'goto_buffer', v, 1, false }, + [ID.PREV_BUFFER] = { 'goto_buffer', v, -1, false }, + [ID.TOGGLE_VIEW_EOL] = { toggle_setting, 'view_eol' }, + [ID.TOGGLE_WRAP_MODE] = { toggle_setting, 'wrap_mode' }, + [ID.TOGGLE_SHOW_INDENT_GUIDES] = { toggle_setting, 'indentation_guides' }, + [ID.TOGGLE_USE_TABS] = { toggle_setting, 'use_tabs' }, + [ID.TOGGLE_VIEW_WHITESPACE] = { toggle_setting, 'view_ws' }, + [ID.EOL_MODE_CRLF] = { set_eol_mode, 0 }, + [ID.EOL_MODE_CR] = { set_eol_mode, 1 }, + [ID.EOL_MODE_LF] = { set_eol_mode, 2 }, + [ID.ENCODING_UTF8] = { set_encoding, 'UTF-8' }, + [ID.ENCODING_ASCII] = { set_encoding, 'ASCII' }, + [ID.ENCODING_ISO88591] = { set_encoding, 'ISO-8859-1' }, + [ID.ENCODING_MACROMAN] = { set_encoding, 'MacRoman' }, + [ID.ENCODING_UTF16] = { set_encoding, 'UTF-16LE' }, + [ID.REFRESH_SYNTAX_HIGHLIGHTING] = { 'colourise', b, 0, -1 }, + [ID.SWITCH_BUFFER] = { gui.switch_buffer }, + -- View + [ID.NEXT_VIEW] = { gui.goto_view, 1, false }, + [ID.PREV_VIEW] = { gui.goto_view, -1, false }, + [ID.SPLIT_VIEW_VERTICAL] = { 'split', v }, + [ID.SPLIT_VIEW_HORIZONTAL] = { 'split', v, false }, + [ID.UNSPLIT_VIEW] = { function() view:unsplit() end }, + [ID.UNSPLIT_ALL_VIEWS] = { function() while view:unsplit() do end end }, + [ID.GROW_VIEW] = { + function() if view.size then view.size = view.size + 10 end end + }, + [ID.SHRINK_VIEW] = { + function() if view.size then view.size = view.size - 10 end end + }, + -- Help + [ID.MANUAL] = { open_webpage, _HOME..'/doc/manual/1_Introduction.html' }, + [ID.LUADOC] = { open_webpage, _HOME..'/doc/index.html' }, + [ID.ABOUT] = { + gui.dialog, 'ok-msgbox', '--title', 'Textadept', '--informative-text', + _RELEASE, '--no-cancel' + }, +} + +-- Most of this handling code comes from keys.lua. +events.connect('menu_clicked', + function(menu_id) + local active_table = actions[menu_id] + if menu_id >= ID.LEXER_START and menu_id < ID.LEXER_START + 99 then + active_table = + { set_lexer, lexer_menu[menu_id - ID.LEXER_START + 1][1] } + end + local f, args + if active_table and #active_table > 0 then + local func = active_table[1] + if type(func) == 'function' then + f, args = func, { unpack(active_table, 2) } + elseif type(func) == 'string' then + local object = active_table[2] + if object == 'buffer' then + f, args = buffer[func], { buffer, unpack(active_table, 3) } + elseif object == 'view' then + f, args = view[func], { view, unpack(active_table, 3) } + end + end + if f and args then + local ret, retval = pcall(f, unpack(args)) + if not ret then error(retval) end + else + error(l.MENU_UNKNOWN_COMMAND..tostring(func)) + end + end + end) + +-- Right-click context menu. +gui.context_menu = gtkmenu { + { l.MENU_EDIT_UNDO, ID.UNDO }, + { l.MENU_EDIT_REDO, ID.REDO }, + { SEPARATOR, ID.SEPARATOR }, + { l.MENU_EDIT_CUT, ID.CUT }, + { l.MENU_EDIT_COPY, ID.COPY }, + { l.MENU_EDIT_PASTE, ID.PASTE }, + { l.MENU_EDIT_DELETE, ID.DELETE }, + { SEPARATOR, ID.SEPARATOR }, + { l.MENU_EDIT_SELECT_ALL, ID.SELECT_ALL } +} |