sj.vim 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  1. " vim: foldmethod=marker
  2. " Cursor stack manipulation {{{1
  3. "
  4. " In order to make the pattern of saving the cursor and restoring it
  5. " afterwards easier, these functions implement a simple cursor stack. The
  6. " basic usage is:
  7. "
  8. " call sj#PushCursor()
  9. " " Do stuff that move the cursor around
  10. " call sj#PopCursor()
  11. "
  12. " function! sj#PushCursor() {{{2
  13. "
  14. " Adds the current cursor position to the cursor stack.
  15. function! sj#PushCursor()
  16. if !exists('b:cursor_position_stack')
  17. let b:cursor_position_stack = []
  18. endif
  19. call add(b:cursor_position_stack, winsaveview())
  20. endfunction
  21. " function! sj#PopCursor() {{{2
  22. "
  23. " Restores the cursor to the latest position in the cursor stack, as added
  24. " from the sj#PushCursor function. Removes the position from the stack.
  25. function! sj#PopCursor()
  26. call winrestview(remove(b:cursor_position_stack, -1))
  27. endfunction
  28. " function! sj#DropCursor() {{{2
  29. "
  30. " Discards the last saved cursor position from the cursor stack.
  31. " Note that if the cursor hasn't been saved at all, this will raise an error.
  32. function! sj#DropCursor()
  33. call remove(b:cursor_position_stack, -1)
  34. endfunction
  35. " Indenting {{{1
  36. "
  37. " Some languages don't have built-in support, and some languages have semantic
  38. " indentation. In such cases, code blocks might need to be reindented
  39. " manually.
  40. "
  41. " function! sj#SetIndent(start_lineno, end_lineno, indent) {{{2
  42. " function! sj#SetIndent(lineno, indent)
  43. "
  44. " Sets the indent of the given line numbers to "indent" amount of whitespace.
  45. "
  46. function! sj#SetIndent(...)
  47. if a:0 == 3
  48. let start_lineno = a:1
  49. let end_lineno = a:2
  50. let indent = a:3
  51. elseif a:0 == 2
  52. let start_lineno = a:1
  53. let end_lineno = a:1
  54. let indent = a:2
  55. endif
  56. let is_tabs = &l:expandtab
  57. let shift = shiftwidth()
  58. if is_tabs == 0
  59. if shift > 0
  60. let indent = indent / shift
  61. endif
  62. let whitespace = repeat('\t', indent)
  63. else
  64. let whitespace = repeat(' ', indent)
  65. endif
  66. exe start_lineno.','.end_lineno.'s/^\s*/'.whitespace
  67. " Don't leave a history entry
  68. call histdel('search', -1)
  69. let @/ = histget('search', -1)
  70. endfunction
  71. " function! sj#PeekCursor() {{{2
  72. "
  73. " Returns the last saved cursor position from the cursor stack.
  74. " Note that if the cursor hasn't been saved at all, this will raise an error.
  75. function! sj#PeekCursor()
  76. return b:cursor_position_stack[-1]
  77. endfunction
  78. " Text replacement {{{1
  79. "
  80. " Vim doesn't seem to have a whole lot of functions to aid in text replacement
  81. " within a buffer. The ":normal!" command usually works just fine, but it
  82. " could be difficult to maintain sometimes. These functions encapsulate a few
  83. " common patterns for this.
  84. " function! sj#ReplaceMotion(motion, text) {{{2
  85. "
  86. " Replace the normal mode "motion" with "text". This is mostly just a wrapper
  87. " for a normal! command with a paste, but doesn't pollute any registers.
  88. "
  89. " Examples:
  90. " call sj#ReplaceMotion('Va{', 'some text')
  91. " call sj#ReplaceMotion('V', 'replacement line')
  92. "
  93. " Note that the motion needs to include a visual mode key, like "V", "v" or
  94. " "gv"
  95. function! sj#ReplaceMotion(motion, text)
  96. " reset clipboard to avoid problems with 'unnamed' and 'autoselect'
  97. let saved_clipboard = &clipboard
  98. set clipboard=
  99. let saved_register_text = getreg('"', 1)
  100. let saved_register_type = getregtype('"')
  101. call setreg('"', a:text, 'v')
  102. exec 'silent normal! '.a:motion.'p'
  103. silent normal! gv=
  104. call setreg('"', saved_register_text, saved_register_type)
  105. let &clipboard = saved_clipboard
  106. endfunction
  107. " function! sj#ReplaceLines(start, end, text) {{{2
  108. "
  109. " Replace the area defined by the 'start' and 'end' lines with 'text'.
  110. function! sj#ReplaceLines(start, end, text)
  111. let interval = a:end - a:start
  112. if interval == 0
  113. return sj#ReplaceMotion(a:start.'GV', a:text)
  114. else
  115. return sj#ReplaceMotion(a:start.'GV'.interval.'j', a:text)
  116. endif
  117. endfunction
  118. " function! sj#ReplaceCols(start, end, text) {{{2
  119. "
  120. " Replace the area defined by the 'start' and 'end' columns on the current
  121. " line with 'text'
  122. function! sj#ReplaceCols(start, end, text)
  123. let start_position = getpos('.')
  124. let end_position = getpos('.')
  125. let start_position[2] = a:start
  126. let end_position[2] = a:end
  127. return sj#ReplaceByPosition(start_position, end_position, a:text)
  128. endfunction
  129. " function! sj#ReplaceByPosition(start, end, text) {{{2
  130. "
  131. " Replace the area defined by the 'start' and 'end' positions with 'text'. The
  132. " positions should be compatible with the results of getpos():
  133. "
  134. " [bufnum, lnum, col, off]
  135. "
  136. function! sj#ReplaceByPosition(start, end, text)
  137. let saved_z_pos = getpos("'z")
  138. try
  139. call setpos('.', a:start)
  140. call setpos("'z", a:end)
  141. return sj#ReplaceMotion('v`z', a:text)
  142. finally
  143. call setpos("'z", saved_z_pos)
  144. endtry
  145. endfunction
  146. " Text retrieval {{{1
  147. "
  148. " These functions are similar to the text replacement functions, only retrieve
  149. " the text instead.
  150. "
  151. " function! sj#GetMotion(motion) {{{2
  152. "
  153. " Execute the normal mode motion "motion" and return the text it marks.
  154. "
  155. " Note that the motion needs to include a visual mode key, like "V", "v" or
  156. " "gv"
  157. function! sj#GetMotion(motion)
  158. call sj#PushCursor()
  159. let saved_register_text = getreg('z', 1)
  160. let saved_register_type = getregtype('z')
  161. let @z = ''
  162. exec 'silent normal! '.a:motion.'"zy'
  163. let text = @z
  164. if text == ''
  165. " nothing got selected, so we might still be in visual mode
  166. exe "normal! \<esc>"
  167. endif
  168. call setreg('z', saved_register_text, saved_register_type)
  169. call sj#PopCursor()
  170. return text
  171. endfunction
  172. " function! sj#GetLines(start, end) {{{2
  173. "
  174. " Retrieve the lines from "start" to "end" and return them as a list. This is
  175. " simply a wrapper for getbufline for the moment.
  176. function! sj#GetLines(start, end)
  177. return getbufline('%', a:start, a:end)
  178. endfunction
  179. " function! sj#GetCols(start, end) {{{2
  180. "
  181. " Retrieve the text from columns "start" to "end" on the current line.
  182. function! sj#GetCols(start, end)
  183. return strpart(getline('.'), a:start - 1, a:end - a:start + 1)
  184. endfunction
  185. " function! sj#GetByPosition(start, end) {{{2
  186. "
  187. " Fetch the area defined by the 'start' and 'end' positions. The positions
  188. " should be compatible with the results of getpos():
  189. "
  190. " [bufnum, lnum, col, off]
  191. "
  192. function! sj#GetByPosition(start, end)
  193. let saved_z_pos = getpos("'z")
  194. try
  195. call setpos('.', a:start)
  196. call setpos("'z", a:end)
  197. return sj#GetMotion('v`z')
  198. finally
  199. call setpos("'z", saved_z_pos)
  200. endtry
  201. endfunction
  202. " String functions {{{1
  203. " Various string manipulation utility functions
  204. function! sj#BlankString(s)
  205. return (a:s =~ '^\s*$')
  206. endfunction
  207. " Surprisingly, Vim doesn't seem to have a "trim" function. In any case, these
  208. " should be fairly obvious.
  209. function! sj#Ltrim(s)
  210. return substitute(a:s, '^\_s\+', '', '')
  211. endfunction
  212. function! sj#Rtrim(s)
  213. return substitute(a:s, '\_s\+$', '', '')
  214. endfunction
  215. function! sj#Trim(s)
  216. return sj#Rtrim(sj#Ltrim(a:s))
  217. endfunction
  218. " Execute sj#Trim on each item of a List
  219. function! sj#TrimList(list)
  220. return map(a:list, 'sj#Trim(v:val)')
  221. endfunction
  222. " Remove blank strings from the List
  223. function! sj#RemoveBlanks(list)
  224. return filter(a:list, 'v:val !~ "^\\s*$"')
  225. endfunction
  226. " Searching for patterns {{{1
  227. "
  228. " function! sj#SearchUnderCursor(pattern, flags, skip) {{{2
  229. "
  230. " Searches for a match for the given pattern under the cursor. Returns the
  231. " result of the |search()| call if a match was found, 0 otherwise.
  232. "
  233. " Moves the cursor unless the 'n' flag is given.
  234. "
  235. " The a:flags parameter can include one of "e", "p", "s", "n", which work the
  236. " same way as the built-in |search()| call. Any other flags will be ignored.
  237. "
  238. function! sj#SearchUnderCursor(pattern, ...)
  239. let [match_start, match_end] = call('sj#SearchColsUnderCursor', [a:pattern] + a:000)
  240. if match_start > 0
  241. return match_start
  242. else
  243. return 0
  244. endif
  245. endfunction
  246. " function! sj#SearchColsUnderCursor(pattern, flags, skip) {{{2
  247. "
  248. " Searches for a match for the given pattern under the cursor. Returns the
  249. " start and (end + 1) column positions of the match. If nothing was found,
  250. " returns [0, 0].
  251. "
  252. " Moves the cursor unless the 'n' flag is given.
  253. "
  254. " Respects the skip expression if it's given.
  255. "
  256. " See sj#SearchUnderCursor for the behaviour of a:flags
  257. "
  258. function! sj#SearchColsUnderCursor(pattern, ...)
  259. if a:0 >= 1
  260. let given_flags = a:1
  261. else
  262. let given_flags = ''
  263. endif
  264. if a:0 >= 2
  265. let skip = a:2
  266. else
  267. let skip = ''
  268. endif
  269. let lnum = line('.')
  270. let col = col('.')
  271. let pattern = a:pattern
  272. let extra_flags = ''
  273. " handle any extra flags provided by the user
  274. for char in ['e', 'p', 's']
  275. if stridx(given_flags, char) >= 0
  276. let extra_flags .= char
  277. endif
  278. endfor
  279. call sj#PushCursor()
  280. " find the start of the pattern
  281. call search(pattern, 'bcW', lnum)
  282. let search_result = sj#SearchSkip(pattern, skip, 'cW'.extra_flags, lnum)
  283. if search_result <= 0
  284. call sj#PopCursor()
  285. return [0, 0]
  286. endif
  287. call sj#PushCursor()
  288. " find the end of the pattern
  289. if stridx(extra_flags, 'e') >= 0
  290. let match_end = col('.')
  291. call sj#PushCursor()
  292. call sj#SearchSkip(pattern, skip, 'cWb', lnum)
  293. let match_start = col('.')
  294. call sj#PopCursor()
  295. else
  296. let match_start = col('.')
  297. call sj#SearchSkip(pattern, skip, 'cWe', lnum)
  298. let match_end = col('.')
  299. end
  300. " set the end of the pattern to the next character, or EOL. Extra logic
  301. " is for multibyte characters.
  302. normal! l
  303. if col('.') == match_end
  304. " no movement, we must be at the end
  305. let match_end = col('$')
  306. else
  307. let match_end = col('.')
  308. endif
  309. call sj#PopCursor()
  310. if !sj#ColBetween(col, match_start, match_end)
  311. " then the cursor is not in the pattern
  312. call sj#PopCursor()
  313. return [0, 0]
  314. else
  315. " a match has been found
  316. if stridx(given_flags, 'n') >= 0
  317. call sj#PopCursor()
  318. else
  319. call sj#DropCursor()
  320. endif
  321. return [match_start, match_end]
  322. endif
  323. endfunction
  324. " function! sj#SearchSkip(pattern, skip, ...) {{{2
  325. " A partial replacement to search() that consults a skip pattern when
  326. " performing a search, just like searchpair().
  327. "
  328. " Note that it doesn't accept the "n" and "c" flags due to implementation
  329. " difficulties.
  330. function! sj#SearchSkip(pattern, skip, ...)
  331. " collect all of our arguments
  332. let pattern = a:pattern
  333. let skip = a:skip
  334. if a:0 >= 1
  335. let flags = a:1
  336. else
  337. let flags = ''
  338. endif
  339. if stridx(flags, 'n') > -1
  340. echoerr "Doesn't work with 'n' flag, was given: ".flags
  341. return
  342. endif
  343. let stopline = (a:0 >= 2) ? a:2 : 0
  344. let timeout = (a:0 >= 3) ? a:3 : 0
  345. " just delegate to search() directly if no skip expression was given
  346. if skip == ''
  347. return search(pattern, flags, stopline, timeout)
  348. endif
  349. " search for the pattern, skipping a match if necessary
  350. let skip_match = 1
  351. while skip_match
  352. let match = search(pattern, flags, stopline, timeout)
  353. " remove 'c' flag for any run after the first
  354. let flags = substitute(flags, 'c', '', 'g')
  355. if match && eval(skip)
  356. let skip_match = 1
  357. else
  358. let skip_match = 0
  359. endif
  360. endwhile
  361. return match
  362. endfunction
  363. function! sj#SkipSyntax(syntax_groups)
  364. let syntax_groups = a:syntax_groups
  365. let skip_pattern = '\%('.join(syntax_groups, '\|').'\)'
  366. return "synIDattr(synID(line('.'),col('.'),1),'name') =~ '".skip_pattern."'"
  367. endfunction
  368. " Checks if the current position of the cursor is within the given limits.
  369. "
  370. function! sj#CursorBetween(start, end)
  371. return sj#ColBetween(col('.'), a:start, a:end)
  372. endfunction
  373. " Checks if the given column is within the given limits.
  374. "
  375. function! sj#ColBetween(col, start, end)
  376. return a:start <= a:col && a:end > a:col
  377. endfunction
  378. " Regex helpers {{{1
  379. "
  380. " function! sj#ExtractRx(expr, pat, sub) {{{2
  381. "
  382. " Extract a regex match from a string. Ordinarily, substitute() would be used
  383. " for this, but it's a bit too cumbersome for extracting a particular grouped
  384. " match. Example usage:
  385. "
  386. " sj#ExtractRx('foo:bar:baz', ':\(.*\):', '\1') == 'bar'
  387. "
  388. function! sj#ExtractRx(expr, pat, sub)
  389. let rx = a:pat
  390. if stridx(a:pat, '^') != 0
  391. let rx = '^.*'.rx
  392. endif
  393. if strridx(a:pat, '$') + 1 != strlen(a:pat)
  394. let rx = rx.'.*$'
  395. endif
  396. return substitute(a:expr, rx, a:sub, '')
  397. endfunction
  398. " Compatibility {{{1
  399. "
  400. " Functionality that is present in newer versions of Vim, but needs a
  401. " compatibility layer for older ones.
  402. "
  403. " function! sj#Keeppatterns(command) {{{2
  404. "
  405. " Executes the given command, but attempts to keep search patterns as they
  406. " were.
  407. "
  408. function! sj#Keeppatterns(command)
  409. if exists(':keeppatterns')
  410. exe 'keeppatterns '.a:command
  411. else
  412. let histnr = histnr('search')
  413. exe a:command
  414. if histnr != histnr('search')
  415. call histdel('search', -1)
  416. let @/ = histget('search', -1)
  417. endif
  418. endif
  419. endfunction
  420. " Splitjoin-specific helpers {{{1
  421. " These functions are not general-purpose, but can be used all around the
  422. " plugin disregarding filetype, so they have no place in the specific autoload
  423. " files.
  424. function! sj#Align(from, to, type)
  425. if a:from >= a:to
  426. return
  427. endif
  428. if exists('g:tabular_loaded')
  429. call s:Tabularize(a:from, a:to, a:type)
  430. elseif exists('g:loaded_AlignPlugin')
  431. call s:Align(a:from, a:to, a:type)
  432. endif
  433. endfunction
  434. function! s:Tabularize(from, to, type)
  435. if a:type == 'hashrocket'
  436. let pattern = '^[^=>]*\zs=>'
  437. elseif a:type == 'css_declaration' || a:type == 'json_object'
  438. let pattern = '^[^:]*:\s*\zs\s/l0'
  439. elseif a:type == 'lua_table'
  440. let pattern = '^[^=]*\zs='
  441. elseif a:type == 'when_then'
  442. let pattern = 'then'
  443. elseif a:type == 'equals'
  444. let pattern = '='
  445. else
  446. return
  447. endif
  448. exe a:from.",".a:to."Tabularize/".pattern
  449. endfunction
  450. function! s:Align(from, to, type)
  451. if a:type == 'hashrocket'
  452. let pattern = 'l: =>'
  453. elseif a:type == 'css_declaration' || a:type == 'json_object'
  454. let pattern = 'lp0W0 :\s*\zs'
  455. elseif a:type == 'when_then'
  456. let pattern = 'l: then'
  457. elseif a:type == 'equals'
  458. let pattern = '='
  459. else
  460. return
  461. endif
  462. exe a:from.",".a:to."Align! ".pattern
  463. endfunction
  464. " Returns a pair with the column positions of the closest opening and closing
  465. " braces on the current line. The a:open and a:close parameters are the
  466. " opening and closing brace characters to look for.
  467. "
  468. " The optional parameter is the list of syntax groups to skip while searching.
  469. "
  470. " If a pair is not found on the line, returns [-1, -1]
  471. "
  472. " Examples:
  473. "
  474. " let [start, end] = sj#LocateBracesOnLine('{', '}')
  475. " let [start, end] = sj#LocateBracesOnLine('{', '}', ['rubyString'])
  476. " let [start, end] = sj#LocateBracesOnLine('[', ']')
  477. "
  478. function! sj#LocateBracesOnLine(open, close, ...)
  479. let [_bufnum, line, col, _off] = getpos('.')
  480. let search_pattern = '\V'.a:open.'\m.*\V'.a:close
  481. " bail early if there's obviously no match
  482. if getline('.') !~ search_pattern
  483. return [-1, -1]
  484. endif
  485. " optional skip parameter
  486. if a:0 > 0
  487. let skip = sj#SkipSyntax(a:1)
  488. else
  489. let skip = ''
  490. endif
  491. " try looking backwards, then forwards
  492. let found = searchpair('\V'.a:open, '', '\V'.a:close, 'cb', skip, line('.'))
  493. if found <= 0
  494. let found = sj#SearchSkip(search_pattern, skip, '', line('.'))
  495. endif
  496. if found > 0
  497. let from = col('.')
  498. normal! %
  499. let to = col('.')
  500. return [from, to]
  501. else
  502. return [-1, -1]
  503. endif
  504. endfunction
  505. " Returns a pair with the column positions of the closest opening and closing
  506. " braces on the current line, but only if the cursor is between them.
  507. "
  508. " The optional parameter is the list of syntax groups to skip while searching.
  509. "
  510. " If a pair is not found around the cursor, returns [-1, -1]
  511. "
  512. " Examples:
  513. "
  514. " let [start, end] = sj#LocateBracesAroundCursor('{', '}')
  515. " let [start, end] = sj#LocateBracesAroundCursor('{', '}', ['rubyString'])
  516. " let [start, end] = sj#LocateBracesAroundCursor('[', ']')
  517. "
  518. function! sj#LocateBracesAroundCursor(open, close, ...)
  519. let args = [a:open, a:close]
  520. if a:0 > 0
  521. call extend(args, a:000)
  522. endif
  523. call sj#PushCursor()
  524. let [start, end] = call('sj#LocateBracesOnLine', args)
  525. call sj#PopCursor()
  526. if sj#CursorBetween(start, end)
  527. return [start, end]
  528. else
  529. return [-1, -1]
  530. endif
  531. endfunction
  532. " Removes all extra whitespace on the current line. Such is often left when
  533. " joining lines that have been aligned.
  534. "
  535. " Example:
  536. "
  537. " var one = { one: "two", three: "four" };
  538. " " turns into:
  539. " var one = { one: "two", three: "four" };
  540. "
  541. function! sj#CompressWhitespaceOnLine()
  542. call sj#PushCursor()
  543. s/\S\zs \+/ /g
  544. " Don't leave a history entry
  545. call histdel('search', -1)
  546. let @/ = histget('search', -1)
  547. call sj#PopCursor()
  548. endfunction
  549. " Parses a JSON-like object and returns a list of its components
  550. " (comma-separated parts).
  551. "
  552. " Note that a:from and a:to are the start and end of the body, not the curly
  553. " braces that usually define a JSON object. This makes it possible to use the
  554. " function for parsing an argument list into separate arguments, knowing their
  555. " start and end.
  556. "
  557. " Different languages have different rules for delimiters, so it might be a
  558. " better idea to write a specific parser. See autoload/sj/argparser/js.vim for
  559. " inspiration.
  560. "
  561. function! sj#ParseJsonObjectBody(from, to)
  562. " Just use js object parser
  563. let parser = sj#argparser#js#Construct(a:from, a:to, getline('.'))
  564. call parser.Process()
  565. return parser.args
  566. endfunction