Skip to content

Commit

Permalink
Merge pull request #7 from jonsmithers/indentation-refactor
Browse files Browse the repository at this point in the history
Refactor indentation
  • Loading branch information
jonsmithers committed Aug 23, 2018
2 parents bb52e00 + 2373afa commit dd511a4
Show file tree
Hide file tree
Showing 2 changed files with 260 additions and 66 deletions.
282 changes: 217 additions & 65 deletions after/indent/javascript.vim
Original file line number Diff line number Diff line change
Expand Up @@ -172,99 +172,251 @@ fu! VHTL_countMatches(string, pattern)
endwhile
endfu

function! s:SynAt(l,c) " from $VIMRUNTIME/indent/javascript.vim
let byte = line2byte(a:l) + a:c - 1
let pos = index(s:synid_cache[0], byte)
if pos == -1
let s:synid_cache[:] += [[byte], [synIDattr(synID(a:l, a:c, 0), 'name')]]
endif
return s:synid_cache[1][pos]
endfunction

if exists('g:VHTL_debugging')
set debug=msg " show errors in indentexpr
fu! SynAt(l,c)
return s:SynAt(a:l,a:c)
endfu
endif
fu! VHTL_debug(str)
if exists('g:VHTL_debugging')
echom a:str
endif
endfu

let s:StateClass={}
fu! s:StateClass.new()
let l:instance = copy(self)
let l:instance.currLine = v:lnum
let l:instance.prevLine = prevnonblank(v:lnum - 1)
let l:instance.currSynstack = VHTL_SynSOL(l:instance.currLine)
let l:instance.prevSynstack = VHTL_SynEOL(l:instance.prevLine)
return l:instance
endfu

fu! s:StateClass.startsWithTemplateClose() dict
return (getline(self.currSynstack)) =~# '^\s*`'
endfu

fu! s:StateClass.closedJsExpression() dict
return VHTL_closesJsExpression(getline(self.prevLine))
endfu
fu! s:StateClass.closesJsExpression() dict
return VHTL_closesJsExpression(getline(self.currLine))
endfu
fu! s:StateClass.openedJsExpression() dict
return (VHTL_getBracketDepthChange(getline(self.prevLine)) > 0)
endfu
fu! s:StateClass.opensLitHtmlTemplate() dict
return VHTL_opensTemplate(getline(self.currLine))
endfu
fu! s:StateClass.openedLitHtmlTemplate() dict
return VHTL_opensTemplate(getline(self.prevLine))
endfu
fu! s:StateClass.closesLitHtmlTemplate() dict
return VHTL_closesTemplate(getline(self.currLine))
endfu
fu! s:StateClass.closedLitHtmlTemplate() dict
return VHTL_closesTemplate(getline(self.prevLine))
endfu

fu! s:StateClass.isInsideLitHtml() dict
return VHTL_isSynstackInsideLitHtml(self.currSynstack)
endfu
fu! s:StateClass.wasInsideLitHtml() dict
return VHTL_isSynstackInsideLitHtml(self.prevSynstack)
endfu
fu! s:StateClass.isInsideJsx() dict
return IsSynstackInsideJsx(self.currSynstack)
endfu

fu! s:StateClass.wasHtml() dict
return get(self.prevSynstack, -1) =~# '^html'
endfu
fu! s:StateClass.isHtml() dict
return get(self.currSynstack, -1) =~# '^html'
endfu
fu! s:StateClass.isLitHtmlRegionCloser() dict
return get(self.currSynstack, -1) ==# 'litHtmlRegion' && getline(self.currLine) =~# '^\s*`'
endfu
fu! s:StateClass.wasJs() dict
return get(self.prevSynstack, -1) =~# '^js'
endfu
fu! s:StateClass.isJsTemplateBrace() dict
return get(self.currSynstack, -1) ==# 'jsTemplateBraces'
endfu
fu! s:StateClass.wasJsTemplateBrace() dict
return get(self.prevSynstack, -1) ==# 'jsTemplateBraces'
endfu
fu! s:StateClass.isJs() dict
return get(self.currSynstack, -1) =~# '^js'
endfu
fu! s:StateClass.wasCss() dict
return get(self.prevSynstack, -1) =~# '^css'
endfu
fu! s:StateClass.isCss() dict
return get(self.currSynstack, -1) =~# '^css'
endfu

fu! s:StateClass.toStr() dict
return '{line ' . self.currLine . '}'
endfu

fu! s:SkipFuncJsTemplateBraces()
" let l:char = getline(line('.'))[col('.')-1]
let l:syntax = s:SynAt(line('.'), col('.'))
if (l:syntax != 'jsTemplateBraces')
return 1
endif
endfu

fu! s:SkipFuncLitHtmlRegion()
" let l:char = getline(line('.'))[col('.')-1]
let l:syntax = s:SynAt(line('.'), col('.'))
if (l:syntax != 'litHtmlRegion')
return 1
endif
endfu

fu! s:getCloseWordsLeftToRight(lineNum)
let l:line = getline(a:lineNum)

" The following regex converts a line to purely a list of closing words.
" Pretty cool but not useful
" echo split(getline(62), '.\{-}\ze\(}\|`\|<\/\w\+>\)')

let l:anyCloserWord = '}\|`\|<\/\w\+>'


let l:index = 0
let l:closeWords = []
while v:true
let [l:term, l:index, l:trash] = matchstrpos(l:line, l:anyCloserWord, l:index)
if (l:index == -1)
break
else
let l:col = l:index + 1
call add(l:closeWords, [l:term, l:col])
endif
let l:index += 1
endwhile
return l:closeWords
endfu

fu! s:StateClass.getIndentDelta() dict
let l:closeWords = s:getCloseWordsLeftToRight(self.currLine)
if len(l:closeWords) == 0
return 0
endif
let [l:closeWord, l:col] = l:closeWords[0]
let l:syntax = s:SynAt(self.currLine, l:col)
if (l:syntax == 'htmlEndTag')
call VHTL_debug('indent_delta: html end tag')
return - &shiftwidth
endif
if (l:syntax == 'litHtmlRegion' && 'html`' != strpart(getline(self.currLine), l:col-5, len('html`')))
call VHTL_debug('indent_delta: end of litHtmlRegion')
return - &shiftwidth
endif
return 0
endfu

" html tag, html template, or js expression on previous line
fu! s:StateClass.getIndentOfLastClose() dict

let l:closeWords = s:getCloseWordsLeftToRight(self.prevLine)

if (len(l:closeWords) == 0)
return -1
endif

for l:item in reverse(l:closeWords)
let [l:closeWord, l:col] = l:item
let l:syntax = s:SynAt(self.prevLine, l:col)
call cursor(self.prevLine, l:col) " sets start point for searchpair()
redraw
if ("}" == l:closeWord && l:syntax == 'jsTemplateBraces')
call searchpair('{', '', '}', 'b', 's:SkipFuncJsTemplateBraces()')
call VHTL_debug('js brace base indent')
elseif ("`" == l:closeWord && l:syntax == 'litHtmlRegion')
call searchpair('html`', '', '\(html\)\@<!`', 'b', 's:SkipFuncLitHtmlRegion()')
call VHTL_debug('lit html region base indent ')
elseif (l:syntax == 'htmlEndTag')
let l:openWord = substitute(substitute(l:closeWord, '/', '', ''), '>', '', '')
call searchpair(l:openWord, '', l:closeWord, 'b')
call VHTL_debug('html tag region base indent ')
else
call VHTL_debug("UNRECOGNIZED CLOSER SYNTAX: '" . l:syntax . "'")
endif
return indent(line('.')) " cursor was moved by searchpair()
endfor
endfu

" com! MyTest exec "call s:StateClass.new().getIndentOfLastClose()"

" Dispatch to indent method for js/html (use custom rules for transitions
" between syntaxes)
fu! ComputeLitHtmlIndent()
let s:synid_cache = [[],[]]

let l:state = s:StateClass.new()

" get most recent non-empty line
let l:prev_lnum = prevnonblank(v:lnum - 1)

let l:currLineSynstack = VHTL_SynSOL(v:lnum)
let l:prevLineSynstack = VHTL_SynEOL(l:prev_lnum)

if (!VHTL_isSynstackInsideLitHtml(l:currLineSynstack) && !VHTL_isSynstackInsideLitHtml(l:prevLineSynstack))
call VHTL_debug('outside of litHtmlRegion')
if (!l:state.isInsideLitHtml() && !l:state.wasInsideLitHtml())
call VHTL_debug('outside of litHtmlRegion: ' . b:litHtmlOriginalIndentExpression)

if (exists('b:hi_indent') && has_key(b:hi_indent, 'blocklnr'))
call remove(b:hi_indent, 'blocklnr')
" This avoids a really weird behavior when indenting first line inside
" style tag and then indenting any normal javascript outside of
" lit-html region. 'blocklnr' is assigned to line number of <style>,
" which is then assigned to 'nest' inside vim-javascript's indent code.
endif
return eval(b:litHtmlOriginalIndentExpression)
endif


let l:wasCss = (IsSynstackCss(l:prevLineSynstack))

" We add an extra dedent for closing } brackets, as long as the matching {
" opener is not on the same line as an opening html`.
"
" This algorithm does not always work and must be rewritten (hopefully to
" something simpler)
let l:adjustForClosingBracket = 0
" if (!l:wasCss && VHTL_closesJsExpression(getline(l:prev_lnum)))
" :exec 'normal! ' . l:prev_lnum . 'G0[{'
" let l:lineWithOpenBracket = getline(line('.'))
" if (!VHTL_opensTemplate(l:lineWithOpenBracket))
" call VHTL_debug('adjusting for close bracket')
" let l:adjustForClosingBracket = - &shiftwidth
" endif
" endif

let l:wasHtml = (IsSynstackHtml(l:prevLineSynstack))
let l:isHtml = (IsSynstackHtml(l:currLineSynstack))
let l:wasCss = (IsSynstackCss(l:prevLineSynstack))
let l:isCss = (IsSynstackCss(l:currLineSynstack))
let l:wasJs = (IsSynstackJs(l:prevLineSynstack))
let l:isJs = (IsSynstackJs(l:currLineSynstack))

" If a line starts with template close, it is dedented. If a line otherwise
" contains a template close, the NEXT line is dedented. Note that template
" closers can be balanced out by template openers.
if (VHTL_startsWithTemplateEnd(v:lnum))
call VHTL_debug('closed template at start ')
let l:result = indent(l:prev_lnum) - &shiftwidth
if (VHTL_closesJsExpression(getline(l:prev_lnum)))
call VHTL_debug('closed template at start and js expression')
let l:result -= &shiftwidth
endif
return l:result
if (l:state.openedLitHtmlTemplate())
call VHTL_debug('opened tagged template literal')
return indent(l:prev_lnum) + &shiftwidth
endif
if (VHTL_opensTemplate(getline(l:prev_lnum)))
call VHTL_debug('opened template')

if (l:state.openedJsExpression())
call VHTL_debug('opened js expression')
return indent(l:prev_lnum) + &shiftwidth
elseif (VHTL_closesTemplate(getline(l:prev_lnum)) && !VHTL_startsWithTemplateEnd(l:prev_lnum))
call VHTL_debug('closed template ' . l:adjustForClosingBracket)
let l:result = indent(l:prev_lnum) - &shiftwidth + l:adjustForClosingBracket
if (VHTL_closesTag(getline(v:lnum)))
call VHTL_debug('closed template and tag ' . l:adjustForClosingBracket)
let l:result -= &shiftwidth
endif
return l:result
elseif (l:isHtml && l:wasJs && VHTL_closesJsExpression(getline(l:prev_lnum)))
let l:result = indent(l:prev_lnum) - &shiftwidth
call VHTL_debug('closes expression')
if (VHTL_closesTag(getline(v:lnum)))
let l:result -= &shiftwidth
call VHTL_debug('closes expression and tag')
endif
return l:result
endif

let l:isJsx = (IsSynstackInsideJsx(l:currLineSynstack))
if (l:wasCss || l:isCss || l:wasHtml || l:isHtml) && !l:isJsx
call VHTL_debug('html indent ' . l:adjustForClosingBracket)
return HtmlIndent() + l:adjustForClosingBracket
if (l:state.wasJsTemplateBrace() || l:state.isLitHtmlRegionCloser())
" let l:indent_basis = previous matching js or template start
" let l:indent_delta = -1 for starting with closing tag, template, or expression
let l:indent_basis = l:state.getIndentOfLastClose()
if (l:indent_basis == -1)
call VHTL_debug("default to html indent because base indent not found")
return HtmlIndent()
endif
let l:indent_delta = l:state.getIndentDelta()
call VHTL_debug('indent delta ' . l:indent_delta)
call VHTL_debug('indent basis ' . l:indent_basis)
return l:indent_basis + l:indent_delta
endif

if len(b:litHtmlOriginalIndentExpression)
call VHTL_debug('js indent ' . b:litHtmlOriginalIndentExpression)
if ((l:state.wasJs() && !l:state.wasJsTemplateBrace()) && (l:state.isJs() && !l:state.isJsTemplateBrace()))
return eval(b:litHtmlOriginalIndentExpression)
else
call VHTL_debug('cindent should never happen')
return cindent(v:lnum)
endif

call VHTL_debug('default to html indent')
return HtmlIndent()
endfu
44 changes: 43 additions & 1 deletion test/lit-html.vader
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Before:
set shiftwidth=2
set expandtab

Given javascript (xml in xml);
Given javascript (html in html);
html`
<div>
<div> </div>
Expand Down Expand Up @@ -213,6 +213,48 @@ Expect javascript:
</div>
`

Given javascript (adjacent js expressions):
html`
${console.log(null)}
${console.log('this line should have equal indentation')}
`
Do:
=G

Expect javascript:
html`
${console.log(null)}
${console.log('this line should have equal indentation')}
`

Given javascript(multiple dedents after closing js expression):
html`
<div>
${ test
? html`<p>one</p>`
: html`
<p>
two
</p>`}
</div>
<span> dedent THRICE here </span>
`;

Do:
=G

Expect javascript:
html`
<div>
${ test
? html`<p>one</p>`
: html`
<p>
two
</p>`}
</div>
<span> dedent THRICE here </span>
`;

" " This test fails because vim-javascript doesn't correctly indent ternaries inside ${ }
" Given javascript (subsequent templates):
Expand Down

0 comments on commit dd511a4

Please sign in to comment.