summaryrefslogtreecommitdiffstats
path: root/runtime/indent/jsonc.vim
blob: bf8e501dd5c6a666128d3ae161fa7d1a9c7bd887 (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
" Vim indent file
" Language:         JSONC (JSON with Comments)
" Original Author:  Izhak Jakov <izhak724@gmail.com>
" Acknowledgement:  Based off of vim-json maintained by Eli Parra <eli@elzr.com>
"                   https://github.com/elzr/vim-json
" Last Change:      2021-07-01

" 0. Initialization {{{1
" =================

" Only load this indent file when no other was loaded.
if exists("b:did_indent")
  finish
endif
let b:did_indent = 1

setlocal nosmartindent

" Now, set up our indentation expression and keys that trigger it.
setlocal indentexpr=GetJSONCIndent()
setlocal indentkeys=0{,0},0),0[,0],!^F,o,O,e

" Only define the function once.
if exists("*GetJSONCIndent")
  finish
endif

let s:cpo_save = &cpo
set cpo&vim

" 1. Variables {{{1
" ============

let s:line_term = '\s*\%(\%(\/\/\).*\)\=$'
" Regex that defines blocks.
let s:block_regex = '\%({\)\s*\%(|\%([*@]\=\h\w*,\=\s*\)\%(,\s*[*@]\=\h\w*\)*|\)\=' . s:line_term

" 2. Auxiliary Functions {{{1
" ======================

" Check if the character at lnum:col is inside a string.
function s:IsInString(lnum, col)
  return synIDattr(synID(a:lnum, a:col, 1), 'name') == 'jsonString'
endfunction

" Find line above 'lnum' that isn't empty, or in a string.
function s:PrevNonBlankNonString(lnum)
  let lnum = prevnonblank(a:lnum)
  while lnum > 0
    " If the line isn't empty or in a string, end search.
    let line = getline(lnum)
    if !(s:IsInString(lnum, 1) && s:IsInString(lnum, strlen(line)))
      break
    endif
    let lnum = prevnonblank(lnum - 1)
  endwhile
  return lnum
endfunction

" Check if line 'lnum' has more opening brackets than closing ones.
function s:LineHasOpeningBrackets(lnum)
  let open_0 = 0
  let open_2 = 0
  let open_4 = 0
  let line = getline(a:lnum)
  let pos = match(line, '[][(){}]', 0)
  while pos != -1
    let idx = stridx('(){}[]', line[pos])
    if idx % 2 == 0
      let open_{idx} = open_{idx} + 1
    else
      let open_{idx - 1} = open_{idx - 1} - 1
    endif
    let pos = match(line, '[][(){}]', pos + 1)
  endwhile
  return (open_0 > 0) . (open_2 > 0) . (open_4 > 0)
endfunction

function s:Match(lnum, regex)
  let col = match(getline(a:lnum), a:regex) + 1
  return col > 0 && !s:IsInString(a:lnum, col) ? col : 0
endfunction

" 3. GetJSONCIndent Function {{{1
" =========================

function GetJSONCIndent()
  if !exists("s:inside_comment")
    let s:inside_comment = 0
  endif

  " 3.1. Setup {{{2
  " ----------

  " Set up variables for restoring position in file.  Could use v:lnum here.
  let vcol = col('.')

  " 3.2. Work on the current line {{{2
  " -----------------------------


  " Get the current line.
  let line = getline(v:lnum)
  let ind = -1
  if s:inside_comment == 0
    " TODO iterate through all the matches in a line
    let col = matchend(line, '\/\*')
    if col > 0 && !s:IsInString(v:lnum, col)
      let s:inside_comment = 1
    endif
  endif
  " If we're in the middle of a comment
  if s:inside_comment == 1
    let col = matchend(line, '\*\/')
    if col > 0 && !s:IsInString(v:lnum, col)
      let s:inside_comment = 0
    endif
    return ind
  endif
  if line =~ '^\s*//'
    return ind
  endif

  " If we got a closing bracket on an empty line, find its match and indent
  " according to it.
  let col = matchend(line, '^\s*[]}]')

  if col > 0 && !s:IsInString(v:lnum, col)
    call cursor(v:lnum, col)
    let bs = strpart('{}[]', stridx('}]', line[col - 1]) * 2, 2)

    let pairstart = escape(bs[0], '[')
    let pairend = escape(bs[1], ']')
    let pairline = searchpair(pairstart, '', pairend, 'bW')

    if pairline > 0 
      let ind = indent(pairline)
    else
      let ind = virtcol('.') - 1
    endif

    return ind
  endif

  " If we are in a multi-line string, don't do anything to it.
  if s:IsInString(v:lnum, matchend(line, '^\s*') + 1)
    return indent('.')
  endif

  " 3.3. Work on the previous line. {{{2
  " -------------------------------

  let lnum = prevnonblank(v:lnum - 1)

  if lnum == 0
    return 0
  endif

  " Set up variables for current line.
  let line = getline(lnum)
  let ind = indent(lnum)

  " If the previous line ended with a block opening, add a level of indent.
  " if s:Match(lnum, s:block_regex)
  " return indent(lnum) + shiftwidth()
  " endif

  " If the previous line contained an opening bracket, and we are still in it,
  " add indent depending on the bracket type.
  if line =~ '[[({]'
    let counts = s:LineHasOpeningBrackets(lnum)
    if counts[0] == '1' || counts[1] == '1' || counts[2] == '1'
      return ind + shiftwidth()
    else
      call cursor(v:lnum, vcol)
    end
  endif

  " }}}2

  return ind
endfunction

" }}}1

let &cpo = s:cpo_save
unlet s:cpo_save

" vim:set sw=2 sts=2 ts=8 noet: