aboutsummaryrefslogtreecommitdiffhomepage
path: root/core/ext/pm/ctags_browser.lua
blob: db2ea27eefbcf67f69afd926aecc8ff2fd92f44b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
-- Copyright 2007-2010 Mitchell mitchell<att>caladbolg.net. See LICENSE.

local textadept = _G.textadept
local locale = _G.locale

---
-- CTags Browser for the Textadept project manager.
-- It is enabled with the prefix 'ctags' in the project manager entry field
-- followed by either nothing or ':' and the path to a ctags file. If no path
-- is specified, the current file is parsed via ctags and its structure shown.
module('textadept.pm.browsers.ctags', package.seeall)

if not RESETTING then textadept.pm.add_browser('ctags') end

local FILE_OUT = '/tmp/textadept_output'

---
-- The current ctags file and current directory.
-- When a ctags file is opened, current_dir is set to its dirname.
local current_file, current_dir

---
-- The table of ctags with property values.
-- Each key is the name of a ctags identifier (function, class, etc.) and the
-- value is a table containing:
--   * The GTK stock-id for the pixbuf to display next to the identifier in the
--     tree view (pixbuf key).
--   * The display text used for displaying the identifier in the tree view
--     (text key).
--   * Boolean parent value if the identifier is a container.
--   * The line number or pattern used to goto the identifier.
-- Note this table is returned by get_contents_for, but only 'pixbuf',
-- 'text' and 'parent' fields are read; all others are ignored.
-- @class table
-- @name tags
local tags

---
-- Table of associations of tag identifier types for specific languages with
-- GTK stock-id pixbufs.
-- @class table
-- @name pixbuf
local pixbuf = {
  lua = { f = 'prog-method' },
  ruby = {
    c = 'prog-class',
    f = 'prog-method',
    F = 'prog-method',
    m = 'prog-namespace'
  },
  cpp = {
    c = 'prog-class',
    e = 'prog-enum',
    f = 'prog-method',
    g = 'prog-enum',
    m = 'prog-field',
    n = 'prog-namespace',
    s = 'prog-struct'
  }
}

---
-- Table of associations of file extensions with languages.
-- @class table
-- @name language
local language = {
  lua = 'lua',
  rb = 'ruby',
  h = 'cpp', c = 'cpp', cxx = 'cpp' -- C++
}

---
-- Table used to determine if a tag kind is a container or not in a specific
-- language.
-- Top-level keys are language names from the languages table with table
-- values. These table values have tag kind keys with boolean values indicating
-- if they are containers or not.
-- @class table
-- @name container
-- @return true if the tag kind is a container.
-- @see language
local container = {
  lua = {},
  ruby = { c = true, m = true },
  cpp = { c = true, g = true, s = true }
}

---
-- Table used to determine if a construct name is a container or not in a
-- specific language.
-- Top-level keys are language names from the languages table with table
-- values. These table values have construct name keys with boolean values
-- indicating if they are containers or not.
-- @class table
-- @name container_construct
-- @return true if the construct name is a container.
-- @see language
local container_construct = {
  lua = {},
  ruby = { class = true, module = true },
  cpp = { class = true, enum = true, struct = true }
}

--- Matches 'ctags:[/absolute/path/to/ctags/file]'
function matches(entry_text)
  return entry_text:sub(1, 5) == 'ctags'
end

---
-- If not expanding, creates the entire tree; otherwise returns the child table
-- of the parent being expanded.
function get_contents_for(full_path, expanding)
  local ctags_file = full_path[1]:sub(7) -- ignore 'ctags:'
  local f
  if #ctags_file == 0 then
    tags = {}
    current_file = nil
    current_dir = '' -- ctags file will specify absolute paths
    os.execute('ctags -f "'..FILE_OUT..'" '..(buffer.filename or ''))
    f = io.open(FILE_OUT, 'rb')
    if not f then return {} end
  elseif not expanding then
    tags = {}
    current_file = ctags_file
    current_dir = ctags_file:match('^.+/') -- ctags file dirname
    f = io.open(ctags_file, 'rb')
    if not f then return {} end
  else
    local parent = tags
    for i = 2, #full_path do
      local identifier = full_path[i]
      if not parent[identifier] then return {} end
      parent = parent[identifier].children
    end
    return parent
  end
  for line in f:lines() do
    if line:sub(1, 2) ~= '!_' then
      -- Parse ctags line to get identifier attributes.
      local name, filepath, pattern, line_num, ext
      name, filepath, pattern, ext =
        line:match('^([^\t]+)\t([^\t]+)\t/^(.+)$/;"\t(.*)$')
      if not name then
        name, filepath, line_num, ext =
          line:match('^([^\t]+)\t([^\t]+)\t(%d+);"\t(.*)$')
      end
      -- If the ctag line is parsed correctly, create the entry.
      if name and #name > 0 then
        local entry = {}
        local file_ext = filepath:match('%.([^.]+)$')
        local lang = language[file_ext]
        if lang then
          -- Parse the extension fields for details on if this identifier is a
          -- child or parent and where to put it.
          local fields = {}
          --print(ext)
          for key, val in ext:gmatch('([^:%s]+):?(%S*)') do
            if #val == 0 and #key == 1 then -- kind
              if container[lang][key] then
                -- This identifier is a container. Place it in the toplevel of
                -- tags.
                entry.parent = true
                entry.children = {}
                if tags[name] then
                  -- If previously defined by a child, preserve the children
                  -- field.
                  entry.children = tags[name].children
                end
                tags[name] = entry
                entry.set = true
              end
              entry.pixbuf = pixbuf[lang][key]
            elseif container_construct[lang][key] then
              -- This identifier belongs to a container, so define the
              -- container if it hasn't been already and place this identifier
              -- in it. Just in case there is no ctag entry for container later
              -- on, define 'parent' and 'text'.
              if not tags[val] then
                tags[val] = { parent = true, text = val }
              end
              local parent = tags[val]
              if not parent.children then parent.children = {} end
              parent.children[name] = entry -- add to parent
              entry.set = true
            end
          end
          entry.text = name
          -- The following keys are ignored by caller.
          entry.filepath =
            filepath:sub(1, 1) == '/' and filepath or current_dir..filepath
          entry.pattern = pattern
          entry.line_num = line_num
          if not entry.set then tags[name] = entry end
        else
          print(string.format(locale.PM_BROWSER_CTAGS_BAD_EXT, file_ext))
        end
      else
        print(string.format(locale.PM_BROWSER_CTAGS_UNMATCHED, line))
      end
    end
  end
  f:close()
  return tags
end

function perform_action(selected_item)
  local item = tags
  for i = 2, #selected_item do
    local identifier = selected_item[i]
    item = item[identifier]
    if item.children then item = item.children end
  end
  if item.pattern then
    local buffer_text = buffer:get_text(buffer.length)
    local search_text = item.pattern:gsub('\\/', '/')
    local s = buffer_text:find(search_text, 1, true)
    if s then
      textadept.io.open(item.filepath)
      local line = buffer:line_from_position(s)
      buffer:ensure_visible_enforce_policy(line)
      buffer:goto_line(line)
    else
      error(
        string.format(locale.PM_BROWSER_CTAGS_NOT_FOUND, item.text))
    end
  elseif item.line_num then
    textadept.io.open(item.filepath)
    buffer:goto_line(item.line_num - 1)
  end
  view:focus()
end

function get_context_menu(selected_item)

end

function perform_menu_action(menu_id, selected_item)

end

local function update_view()
  if matches(textadept.pm.entry_text) then
    if buffer.filename then
      textadept.pm.activate()
    else
      textadept.pm.clear()
    end
  end
end
textadept.events.add_handler('file_opened', update_view)
textadept.events.add_handler('buffer_deleted', update_view)
textadept.events.add_handler('buffer_after_switch', update_view)
textadept.events.add_handler('save_point_reached', update_view)