aboutsummaryrefslogtreecommitdiff
path: root/modules/textadept
diff options
context:
space:
mode:
Diffstat (limited to 'modules/textadept')
-rw-r--r--modules/textadept/command_entry.lua47
-rw-r--r--modules/textadept/find.lua361
-rw-r--r--modules/textadept/init.lua6
-rw-r--r--modules/textadept/key_commands.lua652
-rw-r--r--modules/textadept/menu.lua518
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 }
+}