aboutsummaryrefslogtreecommitdiff
path: root/modules/textadept/adeptsense.lua
diff options
context:
space:
mode:
Diffstat (limited to 'modules/textadept/adeptsense.lua')
-rw-r--r--modules/textadept/adeptsense.lua468
1 files changed, 468 insertions, 0 deletions
diff --git a/modules/textadept/adeptsense.lua b/modules/textadept/adeptsense.lua
new file mode 100644
index 00000000..d792a698
--- /dev/null
+++ b/modules/textadept/adeptsense.lua
@@ -0,0 +1,468 @@
+-- Copyright 2007-2011 Mitchell mitchell<att>caladbolg.net. See LICENSE.
+
+---
+-- Language autocompletion support for the textadept module.
+module('_m.textadept.adeptsense', package.seeall)
+
+local senses = {}
+
+local f_xpm = '/* XPM */\nstatic char *function[] = {\n/* columns rows colors chars-per-pixel */\n"16 16 5 1",\n" c black",\n". c #E0BC38",\n"X c #F0DC5C",\n"o c #FCFC80",\n"O c None",\n/* pixels */\n"OOOOOOOOOOOOOOOO",\n"OOOOOOOOOOOOOOOO",\n"OOOOOOOOOOOOOOOO",\n"OOOOOOOOOO OOOO",\n"OOOOOOOOO oo OO",\n"OOOOOOOO ooooo O",\n"OOOOOOO ooooo. O",\n"OOOO O XXoo.. O",\n"OOO oo XXX... O",\n"OO ooooo XX.. OO",\n"O ooooo. X. OOO",\n"O XXoo.. O OOOO",\n"O XXX... OOOOOOO",\n"O XXX.. OOOOOOOO",\n"OO X. OOOOOOOOO",\n"OOOO OOOOOOOOOO"\n};'
+local v_xpm = '/* XPM */\nstatic char *field[] = {\n/* columns rows colors chars-per-pixel */\n"16 16 5 1",\n" c black",\n". c #8C748C",\n"X c #9C94A4",\n"o c #ACB4C0",\n"O c None",\n/* pixels */\n"OOOOOOOOOOOOOOOO",\n"OOOOOOOOOOOOOOOO",\n"OOOOOOOOOOOOOOOO",\n"OOOOOOOOOOOOOOOO",\n"OOOOOOOOOOOOOOOO",\n"OOOOOOOOOOOOOOOO",\n"OOOOOOOOO OOOOO",\n"OOOOOOOO oo OOO",\n"OOOOOOO ooooo OO",\n"OOOOOO ooooo. OO",\n"OOOOOO XXoo.. OO",\n"OOOOOO XXX... OO",\n"OOOOOO XXX.. OOO",\n"OOOOOOO X. OOOO",\n"OOOOOOOOO OOOOO",\n"OOOOOOOOOOOOOOOO"\n};'
+
+---
+-- Returns a full symbol (if any) and current symbol part (if any) behind the
+-- caret.
+-- For example: buffer.cur would return 'buffer' and 'cur'.
+-- @param sense The adeptsense returned by adeptsense.new().
+-- @return symbol or '', part or ''.
+function get_symbol(sense)
+ local line, p = buffer:get_cur_line()
+ local symbol, part =
+ line:sub(1, p):match('('..sense.syntax.symbol_chars..'+)[^%w_]+([%w_]*)$')
+ if not symbol then part = line:sub(1, p):match('([%w_]*)$') end
+ return symbol or '', part or ''
+end
+
+---
+-- Returns the class name for a given symbol.
+-- If the symbol is sense.syntax.self and a class definition using the
+-- sense.syntax.class keyword is found, that class is returned. Otherwise the
+-- buffer is searched backwards for a type declaration of the symbol according
+-- to the patterns in sense.syntax.type_declarations.
+-- @param sense The adeptsense returned by adeptsense.new().
+-- @param symbol The symbol to get the class of.
+-- @return class or nil
+-- @see syntax
+function get_class(sense, symbol)
+ local buffer = buffer
+ local self = sense.syntax.self
+ local class_def = sense.syntax.class
+ local symbol_chars = sense.syntax.symbol_chars
+ local type_declarations = sense.syntax.type_declarations
+ local class
+ for i = buffer:line_from_position(buffer.current_pos), 0, -1 do
+ local s, e
+ if symbol == self then
+ -- Determine classname from the class declaration.
+ s, e, class = buffer:get_line(i):find(class_def..'%s+([%w_]+)')
+ if class and not sense.class_list[class] then class = nil end
+ else
+ -- Search for a type declaration.
+ local line = buffer:get_line(i)
+ if line:find(symbol) then
+ for _, patt in ipairs(type_declarations) do
+ s, e, class = line:find(patt:gsub('%%_', symbol))
+ if class then break end
+ end
+ end
+ end
+ if class then
+ -- The type declaration should not be in a comment or string.
+ local pos = buffer:position_from_line(i)
+ local style = buffer:get_style_name(buffer.style_at[pos + s - 1])
+ if style ~= 'comment' and style ~= 'string' then break end
+ class = nil
+ end
+ end
+ return class
+end
+
+---
+-- Returns a list of completions for the given symbol.
+-- @param sense The adeptsense returned by adeptsense.new().
+-- @param symbol The symbol to get completions for.
+-- @param only_fields If true, returns list of only fields; defaults to false.
+-- @param only_functions If true, returns list of only functions; defaults to
+-- false.
+-- @return completion_list or nil
+function get_completions(sense, symbol, only_fields, only_functions)
+ if only_fields and only_functions or not symbol then return nil end
+ local compls = sense.completions
+ local class = compls[symbol] and symbol or sense:get_class(symbol)
+ if not compls[class] then return nil end
+ local c = {}
+ if not only_fields then
+ for _, v in ipairs(compls[class].functions) do c[#c + 1] = v end
+ end
+ if not only_functions then
+ for _, v in ipairs(compls[class].fields) do c[#c + 1] = v end
+ end
+ for _, inherited in ipairs(sense.class_list[class] or {}) do
+ if compls[inherited] then
+ if not only_fields then
+ for _, v in ipairs(compls[inherited].functions) do c[#c + 1] = v end
+ end
+ if not only_functions then
+ for _, v in ipairs(compls[inherited].fields) do c[#c + 1] = v end
+ end
+ end
+ end
+ table.sort(c)
+ return c
+end
+
+---
+-- Shows an autocompletion list for the symbol behind the caret.
+-- @param sense The adeptsense returned by adeptsense.new().
+-- @param only_fields If true, returns list of only fields; defaults to false.
+-- @param only_functions If true, returns list of only functions; defaults to
+-- false.
+-- @return true on success or false.
+-- @see get_symbol
+-- @see get_completions
+function complete(sense, only_fields, only_functions)
+ local buffer = buffer
+ local symbol, part = sense:get_symbol()
+ local completions = sense:get_completions(symbol, only_fields, only_functions)
+ if not completions then return false end
+ buffer:clear_registered_images()
+ buffer:register_image(1, v_xpm)
+ buffer:register_image(2, f_xpm)
+ buffer:auto_c_show(#part, table.concat(completions, ' '))
+ return true
+end
+
+---
+-- Sets the trigger for autocompletion.
+-- @param sense The adeptsense returned by adeptsense.new().
+-- @param c The character(s) that triggers the autocompletion. You can have up
+-- to two characters.
+-- @param only_fields If true, this trigger only completes fields. Defaults to
+-- false.
+-- @param only_functions If true, this trigger only completes functions.
+-- Defaults to false.
+-- @usage sense:add_trigger('.')
+-- @usage sense:add_trigger(':', false, true) -- only functions
+-- @usage sense:add_trigger('->')
+function add_trigger(sense, c, only_fields, only_functions)
+ if #c > 2 then return end -- TODO: warn
+ local c1, c2 = c:match('.$'):byte(), #c > 1 and c:sub(1, 1):byte()
+ local i = events.connect('char_added', function(char)
+ if char == c1 and buffer:get_lexer() == sense.lexer then
+ if c2 and buffer.char_at[buffer.current_pos - 2] ~= c2 then return end
+ sense:complete(only_fields, only_functions)
+ end
+ end)
+ sense.events[#sense.events + 1] = i
+end
+
+---
+-- Returns a list of apidocs for the given symbol.
+-- If there are multiple apidocs, the index of one to display is the value of
+-- the 'pos' key in the returned list.
+-- @param sense The adeptsense returned by adeptsense.new().
+-- @param symbol The symbol to get apidocs for.
+-- @return apidoc_list or nil
+function get_apidoc(sense, symbol)
+ if not symbol then return nil end
+ local apidocs = { pos = 1}
+ local entity, func = symbol:match('^(.-)[^%w_]*([%w_]+)$')
+ local c = func:sub(1, 1) -- for quick comparison
+ local patt = '^'..func..'%s+(.+)$'
+ for _, file in ipairs(sense.api_files) do
+ if lfs.attributes(file) then
+ for line in io.lines(file) do
+ if line:sub(1, 1) == c then apidocs[#apidocs + 1] = line:match(patt) end
+ end
+ end
+ end
+ if #apidocs == 0 then return nil end
+ -- Try to display the type-correct apidoc by getting the entity the function
+ -- is being called on and attempting to determine its type. Otherwise, fall
+ -- back to the entity itself. In order for this to work, the first line in the
+ -- apidoc must start with the entiry (e.g. Class.function).
+ local class = sense.completions[entity] or sense:get_class(entity)
+ if type(class) ~= 'string' then class = entity end -- fall back to entity
+ for i, apidoc in ipairs(apidocs) do
+ if apidoc:match('^[%w_]+') == class then
+ apidocs.pos = i
+ break
+ end
+ end
+ return apidocs
+end
+
+---
+-- Shows a calltip with API documentation for the symbol behind the caret.
+-- @param sense The adeptsense returned by adeptsense.new().
+-- @return true on success or false.
+-- @see get_symbol
+-- @see get_apidoc
+function show_apidoc(sense)
+ local symbol = sense:get_symbol()
+ local apidocs = sense:get_apidoc(symbol)
+ if not apidocs then return false end
+ for i, doc in ipairs(apidocs) do
+ doc = doc:gsub('([^\\])\\n', '%1\n'):gsub('\\\\n', '\\n')
+ if #apidocs > 1 then
+ if not doc:find('\n') then doc = doc..'\n' end
+ doc = '\001'..doc:gsub('\n', '\n\002', 1)
+ end
+ apidocs[i] = doc
+ end
+ buffer:call_tip_show(buffer.current_pos, apidocs[apidocs.pos or 1])
+ local event_id = events.connect('call_tip_click',
+ function(position) -- cycle through calltips
+ apidocs.pos = apidocs.pos + (position == 1 and -1 or 1)
+ if apidocs.pos > #apidocs then apidocs.pos = 1 end
+ if apidocs.pos < 1 then apidocs.pos = #apidocs end
+ buffer:call_tip_show(buffer.current_pos, apidocs[apidocs.pos])
+ end)
+ _G.timeout(1, function()
+ if buffer:call_tip_active() then return true end
+ events.disconnect('call_tip_click', event_id)
+ end)
+ return true
+end
+
+---
+-- Loads the given ctags file for autocompletion.
+-- It is recommended to pass '-n' to ctags in order to use line numbers instead
+-- of text patterns to locate tags. This will greatly reduce memory usage for a
+-- large number of symbols if nolocations is not true.
+-- @param sense The adeptsense returned by adeptsense.new().
+-- @param tag_file The path of the ctags file to load.
+-- @param nolocations If true, does not store the locations of the tags for use
+-- by goto_ctag(). Defaults to false.
+function load_ctags(sense, tag_file, nolocations)
+ local ctags_kinds = sense.ctags_kinds
+ local completions = sense.completions
+ local locations = sense.locations
+ local class_list = sense.class_list
+ local ctags_fmt = '^(%S+)\t([^\t]+)\t(.-);"\t(.*)$'
+ for line in io.lines(tag_file) do
+ local tag_name, file_name, ex_cmd, ext_fields = line:match(ctags_fmt)
+ if tag_name then
+ local k = ext_fields:sub(1, 1)
+ local kind = ctags_kinds[k]
+ if kind == 'functions' or kind == 'fields' then
+ -- Update completions.
+ -- If no class structure is found, the global namespace is used.
+ for _, key in ipairs{ 'class', 'interface', 'struct', '' } do
+ local class = (#key == 0) and '' or ext_fields:match(key..':(%S+)')
+ if class then
+ if not completions[class] then
+ completions[class] = { fields = {}, functions = {} }
+ end
+ local t = completions[class][kind]
+ t[#t + 1] = tag_name..(kind == 'fields' and '?1' or '?2')
+ -- Update locations.
+ if not nolocations then
+ if not locations[k] then locations[k] = {} end
+ locations[k][class..'#'..tag_name] = { file_name, ex_cmd }
+ end
+ break
+ end
+ end
+ elseif kind == 'classes' then
+ -- Update class list.
+ local inherits = ext_fields:match('inherits:(%S+)')
+ if not inherits then inherits = ext_fields:match('struct:(%S+)') end
+ if inherits then
+ class_list[tag_name] = {}
+ for class in inherits:gmatch('[^,]+') do
+ local t = class_list[tag_name]
+ t[#t + 1] = class
+ -- Even though this class inherits fields and functions from others,
+ -- an empty completions table needs to be added to it so
+ -- get_completions() does not return prematurely.
+ completions[tag_name] = { fields = {}, functions = {} }
+ end
+ end
+ -- Update completions.
+ -- Add the class to the global namespace.
+ if not completions[''] then
+ completions[''] = { fields = {}, functions = {} }
+ end
+ local t = completions[''].fields
+ t[#t + 1] = tag_name..'?1'
+ -- Update locations.
+ if not nolocations then
+ if not locations[k] then locations[k] = {} end
+ locations[k][tag_name] = { file_name, ex_cmd }
+ end
+ else
+ sense:handle_ctag(tag_name, file_name, ex_cmd, ext_fields)
+ end
+ end
+ end
+ for _, v in pairs(completions) do
+ table.sort(v.functions)
+ table.sort(v.fields)
+ end
+end
+
+---
+-- Displays a filteredlist of all known symbols of the given kind (classes,
+-- functions, fields, etc.) and jumps to the source of the selected one.
+-- @param sense The adeptsense returned by adeptsense.new().
+-- @param k The ctag character kind (e.g. 'f' for a Lua function).
+-- @param title The title for the filteredlist dialog.
+function goto_ctag(sense, k, title)
+ if not sense.locations[k] then return end -- no ctags loaded
+ local items = {}
+ local kind = sense.ctags_kinds[k]
+ for k, v in pairs(sense.locations[k]) do
+ items[#items + 1] = k:match('[^#]+$') -- symbol name
+ if kind == 'function' or kind == 'fields' then
+ items[#items + 1] = k:match('^[^#]+') -- class name
+ end
+ items[#items + 1] = v[1]..':'..v[2]
+ end
+ local columns = { 'Name', 'Location' }
+ if kind == 'function' or kind == 'field' then
+ table.insert(columns, 2, 'Class')
+ end
+ local out = gui.dialog('filteredlist',
+ '--title', title,
+ '--button1', 'gtk-ok',
+ '--button2', 'gtk-cancel',
+ '--no-newline',
+ '--string-output',
+ '--output-column', '3',
+ '--columns', columns,
+ '--items', items)
+ local response, location = out:match('([^\n]+)\n([^\n]+)$')
+ if response and response ~= 'gtk-cancel' then
+ local path, line = location:match('^(.+):(.+)$')
+ io.open_file(path)
+ if not tonumber(line) then
+ -- /^ ... $/
+ buffer.target_start, buffer.target_end = 0, buffer.length
+ buffer.search_flags = _SCINTILLA.constants.SCFIND_REGEXP
+ if buffer:search_in_target(line:sub(2, -2)) >= 0 then
+ buffer:goto_pos(buffer.target_start)
+ end
+ else
+ _m.textadept.editing.goto_line(tonumber(line))
+ end
+ end
+end
+
+---
+-- Called by load_ctags when a ctag kind is not recognized.
+-- This method should be replaced with your own that is specific to the
+-- language.
+-- @param sense The adeptsense returned by adeptsense.new().
+-- @param tag_name The tag name.
+-- @param file_name The name of the file the tag belongs to.
+-- @param ex_cmd The ex_cmd returned by ctags.
+-- @param ext_fields The ext_fields returned by ctags.
+function handle_ctag(sense, tag_name, file_name, ex_cmd, ext_fields) end
+
+---
+-- Clears an adeptsense.
+-- This is necessary for loading a new ctags file or completions from a
+-- different project.
+-- @param sense The adeptsense returned by adeptsense.new().
+function clear(sense)
+ sense.class_list = {}
+ sense.completions = {}
+ sense.locations = {}
+ sense:handle_clear()
+ collectgarbage('collect')
+end
+
+---
+-- Called when clearing an adeptsense.
+-- This function should be replaced with your own if you have any persistant
+-- objects that need to be deleted.
+-- @param sense The adeptsense returned by adeptsense.new().
+function handle_clear(sense) end
+
+---
+-- Creates a new adeptsense for the given lexer language.
+-- Only one sense can exist per language.
+-- @param lang The lexer language to create an adeptsense for.
+-- @return adeptsense.
+-- @usage local lua_sense = _m.textadept.adeptsense.new('lua')
+function new(lang)
+ local sense = senses[lang]
+ if sense then
+ sense.ctags_kinds = {}
+ sense.api_files = {}
+ for _, i in ipairs(sense.events) do events.disconnect('char_added', i) end
+ sense.events = {}
+ sense:clear()
+ end
+
+ sense = setmetatable({
+ lexer = lang,
+ events = {},
+
+---
+-- Contains a map of ctags kinds to adeptsense kinds.
+-- Recognized kinds are 'functions', 'fields', and 'classes'. Classes are quite
+-- simply containers for functions and fields so Lua modules would count as
+-- classes. Any other kinds will be passed to handle_ctag() for user-defined
+-- handling.
+-- @usage luasense.ctags_kinds = { 'f' = 'functions' }
+-- @usage csense.ctags_kinds = { 'm' = 'fields', 'f' = 'functions',
+-- c = 'classes', s = 'classes' }
+-- @usage javasense.ctags_kinds = { 'f' = 'fields', 'm' = 'functions',
+-- c = 'classes', i = 'classes' }
+-- @class table
+-- @name ctags_kinds
+-- @see handle_ctag
+ctags_kinds = {},
+
+---
+-- Contains a map of classes and a list of their inherited classes.
+-- @class table
+-- @name class_list
+class_list = {},
+
+---
+-- Contains lists of possible completions for known symbols.
+-- Each symbol key has a table value that contains a list of field completions
+-- with a `fields` key and a list of functions completions with a `functions`
+-- key. This table is normally populated by load_ctags(), but can also be set
+-- by the user.
+-- @class table
+-- @name completions
+completions = {},
+
+---
+-- Contains the locations of known symbols.
+-- This table is populated by load_ctags().
+-- @class table
+-- @name locations
+locations = {},
+
+---
+-- Contains a list of api files used by show_apidoc().
+-- Each line in the api file contains a symbol followed by a space character and
+-- then the symbol's documentation. It is recommended to put the symbol's full
+-- signature (e.g. Class.function(arg1, arg2, ...)) on the first line. Newlines
+-- are represented with '\n'. A '\' before '\n' escapes the newline.
+-- @class table
+-- @name api_files
+api_files = {},
+
+---
+-- Contains syntax-specific values for the language.
+-- @field self The language's syntax-equivalent of 'self'. Default is 'self'.
+-- @field class The language's class definition keyword. Default is 'class'.
+-- @field symbol_chars A Lua pattern of characters allowed in a symbol,
+-- including member operators. Default is '[%w_%.]'.
+-- @field type_declarations A list of Lua patterns used for determining the
+-- class of a symbol. The first capture returned must be the class name. Use
+-- '%_' to match the symbol. Defaults to '(%u[%w_%.]+)%s+%_'.
+-- @class table
+-- @name syntax
+-- @see get_class
+syntax = {
+ self = 'self',
+ class = 'class',
+ symbol_chars = '[%w_%.]',
+ type_declarations = {
+ '(%u[%w_%.]+)%s+%_', -- Foo bar
+ }
+},
+
+ super = setmetatable({}, { __index = _M })
+ }, { __index = _M })
+
+ senses[lang] = sense
+ return sense
+end