Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make languages & policies user-configurable #109

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 82 additions & 37 deletions indent/vue.vim
Original file line number Diff line number Diff line change
Expand Up @@ -6,59 +6,104 @@
if exists('b:did_indent')
finish
endif

function! s:get_indentexpr(language)
unlet! b:did_indent
execute 'runtime! indent/' . a:language . '.vim'
return &indentexpr
endfunction
let b:did_indent = 1

" The order is important here, tags without attributes go last.
" HTML is left out, it will be used when there is no match.
let s:languages = [
\ { 'name': 'pug', 'pairs': ['<template lang="pug"', '</template>'] },
\ { 'name': 'stylus', 'pairs': ['<style lang="stylus"', '</style>'] },
\ { 'name': 'css', 'pairs': ['<style', '</style>'] },
\ { 'name': 'coffee', 'pairs': ['<script lang="coffee"', '</script>'] },
\ { 'name': 'javascript', 'pairs': ['<script', '</script>'] },
\ ]
" HTML is included for <template> to apply policies set by
" g:vue_indent_open_close_tags and g:vue_indent_first_line. Anything
" outside pairs will also be handled by html.vim
if !exists('g:vue_indent_languages')
let g:vue_indent_languages = [
\ {'name': 'pug', 'pairs': ['<template lang="pug"', '</template>']},
\ {'name': 'html', 'pairs': ['<template', '</template>'], 'first_line': 'html'},
\ {'name': 'stylus', 'pairs': ['<style lang="stylus"', '</style>']},
\ {'name': 'css', 'pairs': ['<style', '</style>']},
\ {'name': 'coffee', 'pairs': ['<script lang="coffee"', '</script>']},
\ {'name': 'javascript', 'pairs': ['<script', '</script>']},
\ ]
endif

for s:language in s:languages
" Set 'indentexpr' if the user has an indent file installed for the language
if strlen(globpath(&rtp, 'indent/'. s:language.name .'.vim'))
let s:language.indentexpr = s:get_indentexpr(s:language.name)
if !exists('g:vue_indent_open_close_tags')
" Default of 0 forces open/close tags to the first column. Another
" reasonable option is 'html' which defers to html.vim to set the
" indentation.
let g:vue_indent_open_close_tags = 0
endif

if !exists('g:vue_indent_first_line')
" Default of -1 defers to autoindent which maintains the same indent as
" the previous line (the opening tag). During re-indentation, this also
" maintains whatever indent the user sets for the first line following
" the opening tag. Other reasonable options are 0 (force first column) or
" 'html' (defer to html.vim)
let g:vue_indent_first_line = -1
endif

" Load and return the indentexpr for language, resetting indentexpr to its
" prior value before returning.
function! s:get_indentexpr(language)
let saved_indentexpr = &indentexpr
" Default to blank to indicate that this language did not set indentexpr.
let &l:indentexpr = ''
if strlen(globpath(&rtp, 'indent/'. a:language .'.vim'))
unlet! b:did_indent
execute 'runtime! indent/' . a:language . '.vim'
let b:did_indent = 1
endif
endfor
let lang_indentexpr = &indentexpr
let &l:indentexpr = saved_indentexpr
return lang_indentexpr
endfunction

let s:html_indent = s:get_indentexpr('html')

let b:did_indent = 1

setlocal indentexpr=GetVueIndent()
setlocal indentexpr=GetVueIndent(v:lnum)

if exists('*GetVueIndent')
finish
endif

function! GetVueIndent()
for language in s:languages
let opening_tag_line = searchpair(language.pairs[0], '', language.pairs[1], 'bWr')
function! GetVueIndent(...)
let lnum = a:0 ? a:1 : getpos('.')[1]
let indent = s:get_vue_indent(lnum)
" NB: Strings compare as equal to 0, so 'html' == 0 is true, whoops.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use is# here, which also checks if they are the same instance/type.

I think I will eventually s/==/is/g the entire codebase.

return type(indent) == type('html') && indent == 'html' ?
\ eval(s:html_indent) : indent
endfunction

function! s:get_vue_indent(lnum)
for language in g:vue_indent_languages
let opening_tag_line = searchpair(language.pairs[0], '', language.pairs[1], 'bcrW')

if opening_tag_line
execute 'let indent = ' . get(language, 'indentexpr', -1)
" If we're on the open/close tag, use g:vue_indent_open_close_tags
if opening_tag_line == a:lnum
return g:vue_indent_open_close_tags
endif
let closing_tag_line = searchpair(language.pairs[0], '', language.pairs[1], 'crW')
if closing_tag_line == a:lnum
return g:vue_indent_open_close_tags
endif

" If we're on the first line of the block after the opening tag,
" use either language.first_line or g:vue_indent_first_line
if opening_tag_line == prevnonblank(a:lnum - 1)
return has_key(language, 'first_line') ? language.first_line : g:vue_indent_first_line
endif

" Look up the indentexpr for this language. This is cached, even if
" falsy, so it's only loaded the first time.
if !has_key(language, 'indentexpr')
let language.indentexpr = s:get_indentexpr(language.name)
endif
if !empty(language.indentexpr)
return eval(language.indentexpr)
endif

break
endif
endfor

if exists('l:indent')
if (opening_tag_line == prevnonblank(v:lnum - 1) || opening_tag_line == v:lnum)
\ || getline(v:lnum) =~ '\v^\s*\</(script|style|template)'
return 0
endif
else
" Couldn't find language, fall back to html
execute 'let indent = ' . s:html_indent
endif

return indent
" Couldn't match a language, or language didn't provide indentexpr
return 'html'
endfunction
31 changes: 31 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@ and properly [configured](https://github.com/vuejs/eslint-plugin-vue#rocket-usag
npm i -g eslint eslint-plugin-vue
```

## Configuration

`g:vue_indent_languages` configures the list of languages and their
top-level tags for indentation. See `indent/vue.vim` for the default.

`g:vue_indent_first_line` configures the policy for indenting the first
line after an open tag. It defaults to -1 which uses the autoindent
default, i.e. the same column as the open tag. Because of the default
behavior of the autoindenter, it will preserve existing indentation when
reindenting, which means that you can use your preferred first line
indentation without needing any special configuration.

`g:vue_indent_open_close_tags` configures the policy for indenting
open/close tags. It defaults to 0 which forces open/close tags for
top-level sections in a Vue single-file component to the first column. It
is unlikely you will want to change this.

## Contributing

If your language is not getting highlighted open an issue or a PR with the fix.
Expand Down Expand Up @@ -74,6 +91,20 @@ autocmd FileType vue syntax sync fromstart
See `:h :syn-sync-first` and [this article](http://vim.wikia.com/wiki/Fix_syntax_highlighting)
for more details.

### My indentation isn't what I expect

Check `:echom string(g:vue_indent_languages)` after indenting a line to see
what `indentexpr` is being used. For example, if you have Eclim installed
then Vim will find its indenters, when you'd rather be using html.vim and
vim-javascript. The fixes for these situations are case-by-case, for
example to work around Eclim you can use:

```
let g:EclimHtmlIndentDisabled = 1
let g:EclimCssIndentDisabled = 1
let g:EclimJavascriptIndentDisabled = 1
```

### How can I use existing configuration/plugins in Vue files?

If you already have some configuration for filetypes like html, css and
Expand Down
111 changes: 111 additions & 0 deletions test/test_indent.vader
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,114 @@ Expect:
font-size: 30px
display: block
</style>

#
# Mixed
#
Given vue (Mixed with default settings):
<template>
<div>
<template v-if="loading">
Loading...
</template>
</div>
</template>

<style>
body {
background: tomato;
}
</style>

<script>
export default {
methods: {
foo() {
//
}
}
}
</script>

Do (Indent the whole buffer):
gg=G

Expect (All sections indented):
<template>
<div>
<template v-if="loading">
Loading...
</template>
</div>
</template>

<style>
body {
background: tomato;
}
</style>

<script>
export default {
methods: {
foo() {
//
}
}
}
</script>


Given vue (Mixed with custom settings):
<template>
<div>
<template v-if="loading">
Loading...
</template>
</div>
</template>

<style>
body {
background: tomato;
}
</style>

<script lang="python">
def fun():
if foo:
bar
</script>

Execute (Customize settings, indent buffer, restore settings):
let backup_globals = copy(g:)
let g:vue_indent_languages = [
\ {'name': 'html', 'pairs': ['<template', '</template>']},
\ {'name': 'css', 'pairs': ['<style', '</style>']},
\ {'name': 'python', 'pairs': ['<script lang="python"', '</script>']},
\ ]
let g:vue_indent_open_close_tags = 2
let g:vue_indent_first_line = 0
normal gg=G
call extend(g:, backup_globals, 'force')

Expect (All sections indented):
<template>
<div>
<template v-if="loading">
Loading...
</template>
</div>
</template>

<style>
body {
background: tomato;
}
</style>

<script lang="python">
def fun():
if foo:
bar
</script>