init.lua 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. -- Copyright 2007-2017 Mitchell mitchell.att.foicica.com. See LICENSE.
  2. local M = {}
  3. --[[ This comment is for LuaDoc.
  4. ---
  5. -- The ruby module.
  6. -- It provides utilities for editing Ruby code.
  7. --
  8. -- ## Key Bindings
  9. --
  10. -- + `Shift+Enter` (`⇧↩` | `S-Enter`)
  11. -- Try to autocomplete an `if`, `while`, `for`, etc. control structure with
  12. -- `end`.
  13. module('_M.ruby')]]
  14. -- Sets default buffer properties for Ruby files.
  15. events.connect(events.LEXER_LOADED, function(lang)
  16. if lang == 'ruby' then
  17. buffer.word_chars =
  18. 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_?!'
  19. end
  20. end)
  21. -- Autocompletion and documentation.
  22. ---
  23. -- List of "fake" ctags files to use for autocompletion.
  24. -- In addition to the normal ctags kinds for Ruby, the kind 'C' is recognized as
  25. -- a constant and 'a' as an attribute.
  26. -- @class table
  27. -- @name tags
  28. M.tags = {_HOME..'/modules/ruby/tags', _USERHOME..'/modules/ruby/tags'}
  29. ---
  30. -- Map of expression patterns to their types.
  31. -- Expressions are expected to match after the '=' sign of a statement.
  32. -- @class table
  33. -- @name expr_types
  34. M.expr_types = {
  35. ['^[\'"]'] = 'String',
  36. ['^%['] = 'Array',
  37. ['^{'] = 'Hash',
  38. ['^/'] = 'Regexp',
  39. ['^:'] = 'Symbol',
  40. ['^%d+%f[^%d%.]'] = 'Integer',
  41. ['^%d+%.%d+'] = 'Float',
  42. ['^%d+%.%.%.?%d+'] = 'Range'
  43. }
  44. local XPM = textadept.editing.XPM_IMAGES
  45. local xpms = {
  46. c = XPM.CLASS, f = XPM.METHOD, m = XPM.STRUCT, F = XPM.SLOT, C = XPM.VARIABLE,
  47. a = XPM.VARIABLE
  48. }
  49. textadept.editing.autocompleters.ruby = function()
  50. local list = {}
  51. -- Retrieve the symbol behind the caret.
  52. local line, pos = buffer:get_cur_line()
  53. local symbol, op, part = line:sub(1, pos):match('([%w_%.]-)([%.:]*)([%w_]*)$')
  54. if symbol == '' and part == '' then return nil end -- nothing to complete
  55. if op ~= '' and op ~= '.' and op ~= '::' then return nil end
  56. -- Attempt to identify the symbol type.
  57. -- TODO: identify literals like "'foo'." and "[1, 2, 3].".
  58. local buffer = buffer
  59. local assignment = '%f[%w_]'..symbol:gsub('(%p)', '%%%1')..'%s*=%s*(.*)$'
  60. for i = buffer:line_from_position(buffer.current_pos) - 1, 0, -1 do
  61. local expr = buffer:get_line(i):match(assignment)
  62. if expr then
  63. for patt, type in pairs(M.expr_types) do
  64. if expr:find(patt) then symbol = type break end
  65. end
  66. if expr:find('^[%w_:]+%.new') then
  67. symbol = expr:match('^([%w_:]+).new') -- e.g. a = Foo.new
  68. break
  69. end
  70. end
  71. end
  72. -- Search through ctags for completions for that symbol.
  73. local name_patt = '^'..part
  74. local symbol_patt = '%f[%w]'..symbol..'%f[^%w_]'
  75. local sep = string.char(buffer.auto_c_type_separator)
  76. for i = 1, #M.tags do
  77. if lfs.attributes(M.tags[i]) then
  78. for line in io.lines(M.tags[i]) do
  79. local name = line:match('^%S+')
  80. if name:find(name_patt) and not list[name] then
  81. local fields = line:match(';"\t(.*)$')
  82. local k, class = fields:sub(1, 1), fields:match('class:(%S+)') or ''
  83. if class:find(symbol_patt) and (op ~= ':' or k == 'f') then
  84. list[#list + 1] = ("%s%s%d"):format(name, sep, xpms[k])
  85. list[name] = true
  86. end
  87. end
  88. end
  89. end
  90. end
  91. return #part, list
  92. end
  93. textadept.editing.api_files.ruby = {
  94. _HOME..'/modules/ruby/api', _USERHOME..'/modules/ruby/api'
  95. }
  96. -- Commands.
  97. ---
  98. -- Patterns for auto `end` completion for control structures.
  99. -- @class table
  100. -- @name control_structure_patterns
  101. -- @see try_to_autocomplete_end
  102. local control_structure_patterns = {
  103. '^%s*begin', '^%s*case', '^%s*class', '^%s*def', '^%s*for', '^%s*if',
  104. '^%s*module', '^%s*unless', '^%s*until', '^%s*while', 'do%s*|?.-|?%s*$'
  105. }
  106. ---
  107. -- Tries to autocomplete Ruby's `end` keyword for control structures like `if`,
  108. -- `while`, `for`, etc.
  109. -- @see control_structure_patterns
  110. -- @name try_to_autocomplete_end
  111. function M.try_to_autocomplete_end()
  112. local line_num = buffer:line_from_position(buffer.current_pos)
  113. local line = buffer:get_line(line_num)
  114. local line_indentation = buffer.line_indentation
  115. for _, patt in ipairs(control_structure_patterns) do
  116. if line:find(patt) then
  117. local indent = line_indentation[line_num]
  118. buffer:begin_undo_action()
  119. buffer:new_line()
  120. buffer:new_line()
  121. buffer:add_text('end')
  122. line_indentation[line_num + 1] = indent + buffer.tab_width
  123. buffer:line_up()
  124. buffer:line_end()
  125. buffer:end_undo_action()
  126. return true
  127. end
  128. end
  129. return false
  130. end
  131. -- Contains newline sequences for buffer.eol_mode.
  132. -- This table is used by toggle_block().
  133. -- @class table
  134. -- @name newlines
  135. local newlines = {[0] = '\r\n', '\r', '\n'}
  136. ---
  137. -- Toggles between `{ ... }` and `do ... end` Ruby blocks.
  138. -- If the caret is inside a `{ ... }` single-line block, that block is converted
  139. -- to a multiple-line `do .. end` block. If the caret is on a line that contains
  140. -- single-line `do ... end` block, that block is converted to a single-line
  141. -- `{ ... }` block. If the caret is inside a multiple-line `do ... end` block,
  142. -- that block is converted to a single-line `{ ... }` block with all newlines
  143. -- replaced by a space. Indentation is important. The `do` and `end` keywords
  144. -- must be on lines with the same level of indentation to toggle correctly.
  145. -- @name toggle_block
  146. function M.toggle_block()
  147. local buffer = buffer
  148. local pos = buffer.current_pos
  149. local line = buffer:line_from_position(pos)
  150. local e = buffer.line_end_position[line]
  151. local line_indentation = buffer.line_indentation
  152. -- Try to toggle from { ... } to do ... end.
  153. local char_at = buffer.char_at
  154. local p = pos
  155. while p < e do
  156. if char_at[p] == 125 then -- '}'
  157. local s = buffer:brace_match(p)
  158. if s >= 0 then
  159. local block = buffer:text_range(s + 1, p)
  160. local hash = false
  161. local s2, e2 = block:find('%b{}')
  162. if not s2 and not e2 then s2, e2 = #block, #block end
  163. local part1, part2 = block:sub(1, s2), block:sub(e2 + 1)
  164. hash = part1:find('=>') or part1:find('[%w_]:') or
  165. part2:find('=>') or part2:find('[%w_]:')
  166. if not hash then
  167. local newline = newlines[buffer.eol_mode]
  168. local block, r = block:gsub('^(%s*|[^|]*|)', '%1'..newline)
  169. if r == 0 then block = newline..block end
  170. buffer:begin_undo_action()
  171. buffer:set_target_range(s, p + 1)
  172. buffer:replace_target('do'..block..newline..'end')
  173. local indent = line_indentation[line]
  174. line_indentation[line + 1] = indent + buffer.tab_width
  175. line_indentation[line + 2] = indent
  176. buffer:end_undo_action()
  177. return
  178. end
  179. end
  180. end
  181. p = p + 1
  182. end
  183. -- Try to toggle from do ... end to { ... }.
  184. local block, r = buffer:get_cur_line():gsub('do([^%w_]+.-)end$', '{%1}')
  185. if r > 0 then
  186. -- Single-line do ... end block.
  187. buffer:begin_undo_action()
  188. buffer:set_target_range(buffer:position_from_line(line), e)
  189. buffer:replace_target(block)
  190. buffer:goto_pos(pos - 1)
  191. buffer:end_undo_action()
  192. return
  193. end
  194. local do_patt, end_patt = 'do%s*|?[^|]*|?%s*$', '^%s*end'
  195. local s = line
  196. while s >= 0 and not buffer:get_line(s):find(do_patt) do s = s - 1 end
  197. if s < 0 then return end -- no block start found
  198. local indent = line_indentation[s]
  199. e = s + 1
  200. while e < buffer.line_count and (not buffer:get_line(e):find(end_patt) or
  201. line_indentation[e] ~= indent) do
  202. e = e + 1
  203. end
  204. if e >= buffer.line_count then return end -- no block end found
  205. local s2 = buffer:position_from_line(s) + buffer:get_line(s):find(do_patt) - 1
  206. local _, e2 = buffer:get_line(e):find(end_patt)
  207. e2 = buffer:position_from_line(e) + e2
  208. if e2 < pos then return end -- the caret is outside the block found
  209. block = buffer:text_range(s2, e2):match('^do(.+)end$')
  210. block = block:gsub('[\r\n]+', ' '):gsub(' +', ' ')
  211. buffer:begin_undo_action()
  212. buffer:set_target_range(s2, e2)
  213. buffer:replace_target('{'..block..'}')
  214. buffer:end_undo_action()
  215. end
  216. ---
  217. -- Container for Ruby-specific key bindings.
  218. -- @class table
  219. -- @name _G.keys.ruby
  220. keys.ruby = {
  221. ['s\n'] = M.try_to_autocomplete_end,
  222. ['c{'] = M.toggle_block,
  223. }
  224. -- Snippets.
  225. if type(snippets) == 'table' then
  226. ---
  227. -- Container for Ruby-specific snippets.
  228. -- @class table
  229. -- @name _G.snippets.ruby
  230. snippets.ruby = {
  231. rb = '#!%[which ruby]',
  232. forin = 'for %1(element) in %2(collection)\n\t%1.%0\nend',
  233. ife = 'if %1(condition)\n\t%2\nelse\n\t%3\nend',
  234. ['if'] = 'if %1(condition)\n\t%0\nend',
  235. case = 'case %1(object)\nwhen %2(condition)\n\t%0\nend',
  236. Dir = 'Dir.glob(%1(pattern)) do |%2(file)|\n\t%0\nend',
  237. File = 'File.foreach(%1(\'path/to/file\')) do |%2(line)|\n\t%0\nend',
  238. am = 'alias_method :%1(new_name), :%2(old_name)',
  239. all = 'all? { |%1(e)| %0 }',
  240. any = 'any? { |%1(e)| %0 }',
  241. app = 'if __FILE__ == $PROGRAM_NAME\n\t%0\nend',
  242. as = 'assert(%1(test), \'%2(Failure message.)\')',
  243. ase = 'assert_equal(%1(expected), %2(actual))',
  244. asid = 'assert_in_delta(%1(expected_float), %2(actual_float), %3(2 ** -20))',
  245. asio = 'assert_instance_of(%1(ExpectedClass), %2(actual_instance))',
  246. asko = 'assert_kind_of(%1(ExpectedKind), %2(actual_instance))',
  247. asm = 'assert_match(/%1(expected_pattern)/, %2(actual_string))',
  248. asn = 'assert_nil(%1(instance))',
  249. asnm = 'assert_no_match(/%1(unexpected_pattern)/, %2(actual_string))',
  250. asne = 'assert_not_equal(%1(unexpected), %2(actual))',
  251. asnn = 'assert_not_nil(%1(instance))',
  252. asns = 'assert_not_same(%1(unexpected), %2(actual))',
  253. asnr = 'assert_nothing_raised(%1(Exception)) { %0 }',
  254. asnt = 'assert_nothing_thrown { %0 }',
  255. aso = 'assert_operator(%1(left), :%2(operator), %3(right))',
  256. asr = 'assert_raise(%1(Exception)) { %0 }',
  257. asrt = 'assert_respond_to(%1(object), :%2(method))',
  258. assa = 'assert_same(%1(expected), %2(actual))',
  259. asse = 'assert_send([%1(object), :%2(message), %3(args)])',
  260. ast = 'assert_throws(:%1(expected)) { %0 }',
  261. rw = 'attr_accessor :%1(attr_names)',
  262. r = 'attr_reader :%1(attr_names)',
  263. w = 'attr_writer :%1(attr_names)',
  264. cla = 'class %1(ClassName)\n\t%0\nend',
  265. cl = 'classify { |%1(e)| %0 }',
  266. col = 'collect { |%1(e)| %0 }',
  267. collect = 'collect { |%1(element)| %1.%0 }',
  268. def = 'def %1(method_name)\n\t%0\nend',
  269. mm = 'def method_missing(meth, *args, &block)\n\t%0\nend',
  270. defs = 'def self.%1(class_method_name)\n\t%0\nend',
  271. deft = 'def test_%1(case_name)\n\t%0\nend',
  272. deli = 'delete_if { |%1(e)| %0 }',
  273. det = 'detect { |%1(e)| %0 }',
  274. ['do'] = 'do\n\t%0\nend',
  275. doo = 'do |%1(object)|\n\t%0\nend',
  276. each = 'each { |%1(e)| %0 }',
  277. eab = 'each_byte { |%1(byte)| %0 }',
  278. eac = 'each_char { |%1(chr)| %0 }',
  279. eaco = 'each_cons(%1(2)) { |%2(group)| %0 }',
  280. eai = 'each_index { |%1(i)| %0 }',
  281. eak = 'each_key { |%1(key)| %0 }',
  282. eal = 'each_line%1 { |%2(line)| %0 }',
  283. eap = 'each_pair { |%1(name), %2(val)| %0 }',
  284. eas = 'each_slice(%1(2)) { |%2(group)| %0 }',
  285. eav = 'each_value { |%1(val)| %0 }',
  286. eawi = 'each_with_index { |%1(e), %2(i)| %0 }',
  287. fin = 'find { |%1(e)| %0 }',
  288. fina = 'find_all { |%1(e)| %0 }',
  289. flao = 'inject(Array.new) { |%1(arr), %2(a)| %1.push(*%2) }',
  290. grep = 'grep(%1(pattern)) { |%2(match)| %0 }',
  291. gsu = 'gsub(/%1(pattern)/) { |%2(match)| %0 }',
  292. [':'] = ':%1(key) => \'%2(value)\',',
  293. is = '=> ',
  294. inj = 'inject(%1(init)) { |%2(mem), %3(var)| %0 }',
  295. lam = 'lambda { |%1(args)| %0 }',
  296. map = 'map { |%1(e)| %0 }',
  297. mapwi = 'enum_with_index.map { |%1(e), %2(i)| %0 }',
  298. max = 'max { |a, b| %0 }',
  299. min = 'min { |a, b| %0 }',
  300. mod = 'module %1(ModuleName)\n\t%0\nend',
  301. par = 'partition { |%1(e)| %0 }',
  302. ran = 'sort_by { rand }',
  303. rej = 'reject { |%1(e)| %0 }',
  304. req = 'require \'%0\'',
  305. rea = 'reverse_each { |%1(e)| %0 }',
  306. sca = 'scan(/%1(pattern)/) { |%2(match)| %0 }',
  307. sel = 'select { |%1(e)| %0 }',
  308. sor = 'sort { |a, b| %0 }',
  309. sorb = 'sort_by { |%1(e)| %0 }',
  310. ste = 'step(%1(2)) { |%2(n)| %0 }',
  311. sub = 'sub(/%1(pattern)/) { |%2(match)| %0 }',
  312. tim = 'times { %1(n) %0 }',
  313. uni = 'ARGF.each_line%1 do |%2(line)|\n\t%0\nend',
  314. unless = 'unless %1(condition)\n\t%0\nend',
  315. upt = 'upto(%1(2)) { |%2(n)| %0 }',
  316. dow = 'downto(%1(2)) { |%2(n)| %0 }',
  317. when = 'when %1(condition)\n\t',
  318. zip = 'zip(%1(enums)) { |%2(row)| %0 }',
  319. tc = [[
  320. require 'test/unit'
  321. require '%1(library_file_name)'
  322. class Test%2(NameOfTestCases) < Test::Unit::TestCase
  323. def test_%3(case_name)
  324. %0
  325. end
  326. end]],
  327. }
  328. end
  329. return M