diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4510efb --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,15 @@ +name: test + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Vim and Neovim + run: sudo apt-get update && sudo apt-get install -y vim neovim + - name: Run tests + run: ./test/run.sh diff --git a/.gitignore b/.gitignore index 6e9b2ec..4d4d57c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ README.html test.vue .DS_STORE +test/deps/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e8679a7..0000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -dist: focal - -language: minimal - -before_install: - - sudo add-apt-repository ppa:neovim-ppa/stable -y - - sudo apt-get update - - sudo apt-get -y install neovim - -install: - - git clone https://github.com/leafOfTree/test ../test --depth=1 - - - git clone https://github.com/digitaltoad/vim-pug ../vim-pug --depth=1 - - git clone https://github.com/kchmck/vim-coffee-script ../vim-coffee-script --depth=1 - - git clone https://github.com/leafgarland/typescript-vim ../typescript-vim --depth=1 - - git clone https://github.com/iloginow/vim-stylus.git ../vim-stylus --depth=1 - -script: - - bash ../test/start.sh vue vim-vue-plugin diff --git a/README.md b/README.md index 11ecfac..423c839 100644 --- a/README.md +++ b/README.md @@ -37,14 +37,14 @@ You could install it just like other plugins. The filetype will be set to `vue`. :PlugInstall ``` -- Or manually, clone this plugin to `path/to/this_plugin`, and add it to `rtp` in vimrc +- Or manually, clone this plugin to `path/to/this_plugin`, and add it to the start of `rtp` in vimrc. Vim 9 and Neovim come with placeholder `vue` syntax and indent files, which are used instead if this plugin is added to the end by `set rtp+=`. ```vim - set rtp+=path/to/this_plugin + set rtp^=path/to/this_plugin " If filetype is not set to 'vue', try filetype off - set rtp+=path/to/this_plugin + set rtp^=path/to/this_plugin filetype plugin indent on ```
@@ -208,6 +208,24 @@ For example, the built-in syntax `sass.vim` and `less.vim` in vim8.1 runtime and + endif ``` +## Tests + +Run tests with Vim and Neovim, whichever is installed + +```sh +./test/run.sh +./test/run.sh basic template # Only these cases +``` + +Each case is a `.vue` or `.wpy` file in [test/cases](test/cases), with an optional `.vim` file for the config and checks. A case fails if + +- indenting the whole file changes it +- with `g:test_flatten`, indenting the file after removing all indent doesn't restore it +- a check in `Check()` fails, such as syntax, `GetVueTag()`, `GetVueSyntax()` or fold level +- there is an error message + +Tests also run on GitHub Actions for each push and pull request. + ## Acknowledgments & Refs - [mxw/vim-jsx][1] diff --git a/autoload/vue.vim b/autoload/vue.vim index 869cb0c..0f08ba6 100644 --- a/autoload/vue.vim +++ b/autoload/vue.vim @@ -143,8 +143,9 @@ function! s:SyntaxListAtEnd(lnum) let plnum = prevnonblank(a:lnum) let pline = getline(plnum) - " return [] if prevnonblank is an end tag - if pline =~ '^<\/' + " return [] if prevnonblank is an end tag, e.g. for blank lines between + " blocks, but not for the end tag line itself + if plnum != a:lnum && pline =~ '^<\/' return [] endif diff --git a/syntax/patch/vue-keyword.vim b/syntax/patch/vue-keyword.vim index 8e68275..a59e8e3 100644 --- a/syntax/patch/vue-keyword.vim +++ b/syntax/patch/vue-keyword.vim @@ -35,7 +35,7 @@ function! s:GetMatchOption() endfunction " Vue keywords as option key -let s:vue_keywords = 'name parent functional delimiters comments components directives filters extends mixins inheritAttrs model props propsData data computed watch methods template render renderError inject provide beforeCreate created beforeMount mounted beforeUpdate updated activated deactivated beforeDestroy destroyed setup beforeUnmount unmounted errorCaptured renderTracked renderTriggered' +let s:vue_keywords = 'name parent functional delimiters comments components directives filters extends mixins inheritAttrs model props propsData data computed watch methods template render renderError inject provide beforeCreate created beforeMount mounted beforeUpdate updated activated deactivated beforeDestroy destroyed setup beforeUnmount unmounted errorCaptured renderTracked renderTriggered emits expose slots compilerOptions serverPrefetch' let s:indent = s:GetIndent() let s:keywords_regexp = '\v^\s{'.s:indent.'}(async )?<(' @@ -61,21 +61,26 @@ execute 'syntax match vueObjectFuncKey display /' \.s:match_option \.' nextgroup=jsFuncArgs' -" Vue3 keywords as API, https://v3.vuejs.org/api/ -let s:basic_reactive = 'reactive readonly isProxy isReactive isReadonly toRaw markRaw shallowReactive shallowReadonly' -let s:refs = 'ref unref toRef toRefs isRef customRef shallowRef triggerRef' -let s:computed_and_watch = 'computed watchEffect watchPostEffect watchSyncEffect watch' -let s:composition = 'setup onBeforeMount onMounted onBeforeUpdate onUpdated onBeforeUnmount onUnmounted onErrorCaptured onRenderTracked onRenderTriggered onActivated onDeactivated getCurrentInstance InjectionKey provide inject' -let s:global = 'createApp h defineComponent defineAsyncComponent defineCustomElement resolveComponent resolveDynamicComponent resolveDirective withDirectives createRenderer nextTick mergeProps useCssModule' -let s:vue3_keywords = join([s:basic_reactive, s:refs, s:computed_and_watch, s:composition, s:global], ' ') +" Vue3 keywords as API, https://vuejs.org/api/ +let s:basic_reactive = 'reactive readonly isProxy isReactive isReadonly isShallow toRaw markRaw shallowReactive shallowReadonly' +let s:refs = 'ref unref toRef toValue toRefs isRef customRef shallowRef triggerRef' +let s:computed_and_watch = 'computed watchEffect watchPostEffect watchSyncEffect watch onWatcherCleanup' +let s:effect_scope = 'effectScope getCurrentScope onScopeDispose' +let s:composition = 'setup onBeforeMount onMounted onBeforeUpdate onUpdated onBeforeUnmount onUnmounted onErrorCaptured onRenderTracked onRenderTriggered onActivated onDeactivated onServerPrefetch getCurrentInstance InjectionKey provide inject hasInjectionContext' +let s:helpers = 'useAttrs useSlots useModel useTemplateRef useId useCssModule useHost useShadowRoot useSSRContext' +" Compiler macros in let oneline = start.'.*'.end_tag execute 'syntax match '.name.' fold ' @@ -135,7 +132,10 @@ function! s:SetSyntax(block, syntax, has_lang) endfunction function! s:SetBlockSyntax(config_syntax) + " Parse from start, as syncing by block start doesn't restore states inside + " the block like a multi-line css rule syntax sync clear + syntax sync fromstart for [block, syntax] in items(a:config_syntax) let type = type(syntax) diff --git a/test/cases/attribute-off.vim b/test/cases/attribute-off.vim new file mode 100644 index 0000000..09c8797 --- /dev/null +++ b/test/cases/attribute-off.vim @@ -0,0 +1,14 @@ +" Without attribute and keyword highlight +let g:vim_vue_plugin_config = {'attribute': 0, 'keyword': 0} +let g:test_flatten = 1 + +function! Check() + call ExpectSyntax('@click', 'VueKey') + call ExpectSyntax('toggle', 'VueValue') + call ExpectNoSyntax('$t', 'VueInject') + call ExpectNoSyntax('1"', 'javaScriptNumber') + call ExpectSyntax('message', 'VueExpression') + call ExpectEqual(matchstr(execute('highlight VueKey'), 'links to \zs\S\+'), + \ 'htmlArg', 'VueKey link') + call ExpectNoSyntax('data', 'vueObjectFuncName') +endfunction diff --git a/test/cases/attribute-off.vue b/test/cases/attribute-off.vue new file mode 100644 index 0000000..73cf150 --- /dev/null +++ b/test/cases/attribute-off.vue @@ -0,0 +1,13 @@ + + + diff --git a/test/cases/attribute.vim b/test/cases/attribute.vim new file mode 100644 index 0000000..4806efa --- /dev/null +++ b/test/cases/attribute.vim @@ -0,0 +1,26 @@ +" Template syntax with attribute highlight +let g:vim_vue_plugin_config = {'attribute': 1} +let g:test_flatten = 1 + +function! Check() + " Directives + call ExpectSyntax(':class', 'VueKey') + call ExpectSyntax('@click', 'VueKey') + call ExpectSyntax('v-if', 'VueKey') + " Values as expressions + call ExpectSyntax('1"', 'javaScriptNumber') + call ExpectSyntax('$t', 'VueInject') + call ExpectSyntax("'title'", 'javaScriptStringS') + " Components and attributes + normal! gg + call ExpectSyntax('MyComponent', 'VueComponentName') + call ExpectSyntax('ref=', 'htmlArg') + call ExpectSyntax('data-id', 'htmlArg') + call ExpectSyntax('my-component', 'VueComponentName') + call ExpectSyntax('transition', 'VueComponentName') + " Interpolation + call ExpectSyntax('{{', 'VueBrace') + call ExpectSyntax('message', 'VueExpression') + call ExpectEqual(matchstr(execute('highlight VueKey'), 'links to \zs\S\+'), + \ 'Type', 'VueKey link') +endfunction diff --git a/test/cases/attribute.vue b/test/cases/attribute.vue new file mode 100644 index 0000000..ffd430f --- /dev/null +++ b/test/cases/attribute.vue @@ -0,0 +1,19 @@ + + + diff --git a/test/cases/basic.vim b/test/cases/basic.vim new file mode 100644 index 0000000..475c037 --- /dev/null +++ b/test/cases/basic.vim @@ -0,0 +1,19 @@ +let g:vim_vue_plugin_config = { + \'syntax': { + \ 'script': ['javascript'], + \ 'template': ['html'], + \ 'style': ['css'], + \}, + \} +let g:test_flatten = 1 +packadd matchit + +" Jump between tags with % by matchit. Use :normal for its mapping +function! Check() + for [lnum, col, expected] in [[1, 2, [3, '/template']], [2, 4, [2, '/p']]] + call cursor(lnum, col) + normal % + call ExpectEqual([line('.'), matchstr(getline('.'), '\%'.col('.').'c/\w\+')], + \ expected, printf('after %% from line %d col %d', lnum, col)) + endfor +endfunction diff --git a/test/cases/basic.vue b/test/cases/basic.vue new file mode 100644 index 0000000..1f7d4bc --- /dev/null +++ b/test/cases/basic.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/test/cases/context.vim b/test/cases/context.vim new file mode 100644 index 0000000..4257389 --- /dev/null +++ b/test/cases/context.vim @@ -0,0 +1,27 @@ +" OnChangeVueSyntax() is called when syntax under cursor changes. It's +" defined before the plugin is loaded +let g:vim_vue_plugin_config = { + \'syntax': { + \ 'template': ['html'], + \ 'script': ['javascript', 'typescript'], + \ 'style': ['css', 'scss'], + \}, + \} +let g:test_changes = [] + +function! OnChangeVueSyntax(syntax) + call add(g:test_changes, a:syntax) +endfunction + +function! Check() + for lnum in [2, 3, 6, 6, 10, 2] + call cursor(lnum, 1) + doautocmd CursorMoved + endfor + call ExpectEqual(g:test_changes, ['html', 'typescript', 'scss', 'html'], + \ 'syntax changes') + call ExpectEqual(GetVueSyntax(), 'html', 'GetVueSyntax()') + call ExpectTag(6, 'script') + call ExpectTag(10, 'style') +endfunction +let g:test_flatten = 1 diff --git a/test/cases/context.vue b/test/cases/context.vue new file mode 100644 index 0000000..08d2b42 --- /dev/null +++ b/test/cases/context.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/test/cases/example.vim b/test/cases/example.vim new file mode 100644 index 0000000..f6edbd6 --- /dev/null +++ b/test/cases/example.vim @@ -0,0 +1,40 @@ +" Uses syntax plugins in test/deps +let g:vim_vue_plugin_config = { + \'syntax': { + \ 'template': ['html', 'pug'], + \ 'script': ['javascript', 'typescript', 'coffee'], + \ 'style': ['css', 'scss', 'sass', 'less', 'stylus'], + \ 'docs': 'markdown', + \ 'i18n': ['json', 'yaml'], + \ 'route': 'json', + \}, + \'attribute': 1, + \'keyword': 1, + \'foldexpr': 1, + \'full_syntax': ['scss', 'less'], + \} + +" Block tag, syntax by lang="..." or the first in config, and block group +" with css prefix for emmet-vim +function! Check() + call ExpectBlock('div.set', 'template', 'pug', 'pugTemplateBlock') + call ExpectBlock('', 'template', 'html', 'htmlTemplateBlock') + call ExpectBlock('export default', 'script', 'javascript', 'javascriptScriptBlock') + call ExpectBlock('import Vue', 'script', 'typescript', 'typescriptScriptBlock') + call ExpectBlock('# Assignment', 'script', 'coffee', 'coffeeScriptBlock') + call ExpectBlock('p {', 'style', 'css', 'cssStyleBlock') + call ExpectBlock('$a: 100px', 'style', 'scss', 'cssScssStyleBlock') + call ExpectBlock('$w: 128px', 'style', 'sass', 'sassStyleBlock') + call ExpectBlock('@list: 10 12', 'style', 'less', 'cssLessStyleBlock') + call ExpectBlock("@import 'vendor'", 'style', 'stylus', 'cssStylusStyleBlock') + call ExpectBlock('"requiresAuth"', 'route', 'json', 'jsonRouteBlock') + call ExpectBlock('# This is', 'docs', 'markdown', 'markdownDocsBlock') + call ExpectBlock('en:', 'i18n', 'yaml', 'yamlI18nBlock') + call ExpectBlock('"en"', 'i18n', 'json', 'jsonI18nBlock') + + " Syntax from a sync point, as after jumping to the end + silent edit! + normal! G + call search('"en"', 'bW') + call ExpectEqual(GetVueSyntax(), 'json', 'syntax at the end after reload') +endfunction diff --git a/test/cases/example.vue b/test/cases/example.vue new file mode 100644 index 0000000..fce9e3e --- /dev/null +++ b/test/cases/example.vue @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + +{ + "name": "home", + "meta": { + "requiresAuth": true + } +} + + + +# This is the documentation for component. + + + +en: + hello: "Hello World!" +ja: + hello: "こんにちは、世界!" + + + +{ + "en": { + "language": "Language", + "hello": "hello, world!" + }, + "ja": { + "language": "言語", + "hello": "こんにちは、世界!" + } +} + diff --git a/test/cases/fold.vim b/test/cases/fold.vim new file mode 100644 index 0000000..524725e --- /dev/null +++ b/test/cases/fold.vim @@ -0,0 +1,16 @@ +let g:vim_vue_plugin_config = { + \'syntax': { + \ 'script': ['javascript'], + \ 'template': ['html'], + \ 'style': ['css'], + \}, + \'foldexpr': 1, + \} + +function! Check() + let levels = [1, 2, 1, 1, 1, 2, 3, 4, 4, 3, 3, 1, 1, 1, 1, 1, 2, 2, 1, 1] + for lnum in range(1, len(levels)) + call ExpectFoldlevel(lnum, levels[lnum - 1]) + endfor +endfunction +let g:test_flatten = 1 diff --git a/test/cases/fold.vue b/test/cases/fold.vue new file mode 100644 index 0000000..1f7d4bc --- /dev/null +++ b/test/cases/fold.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/test/cases/indent.vim b/test/cases/indent.vim new file mode 100644 index 0000000..5caa8f8 --- /dev/null +++ b/test/cases/indent.vim @@ -0,0 +1,10 @@ +let g:vim_vue_plugin_config = { + \'syntax': { + \ 'template': ['html'], + \ 'script': ['javascript'], + \ 'style': ['css', 'sass', 'scss'], + \ 'codepen-additional': ['javascript', 'json'], + \}, + \'initial_indent': ['script', 'style', 'codepen-additional'], + \} +let g:test_flatten = 1 diff --git a/test/cases/indent.vue b/test/cases/indent.vue new file mode 100644 index 0000000..52a4b8e --- /dev/null +++ b/test/cases/indent.vue @@ -0,0 +1,27 @@ + + + + + + + + const easings = { + linear: '', + easeInQuad: '', + } + diff --git a/test/cases/initial-indent.vim b/test/cases/initial-indent.vim new file mode 100644 index 0000000..4b50c63 --- /dev/null +++ b/test/cases/initial-indent.vim @@ -0,0 +1,10 @@ +" initial_indent as `syntax` and `tag.syntax` +let g:vim_vue_plugin_config = { + \'syntax': { + \ 'script': ['javascript'], + \ 'style': ['css', 'scss'], + \ 'i18n': ['json', 'yaml'], + \}, + \'initial_indent': ['scss', 'i18n.json'], + \} +let g:test_flatten = 1 diff --git a/test/cases/initial-indent.vue b/test/cases/initial-indent.vue new file mode 100644 index 0000000..0506fec --- /dev/null +++ b/test/cases/initial-indent.vue @@ -0,0 +1,28 @@ + + + + + + + + { + "en": { + "hello": "Hello" + } + } + + + +en: + hello: Hello + diff --git a/test/cases/keyword.vim b/test/cases/keyword.vim new file mode 100644 index 0000000..9c7b832 --- /dev/null +++ b/test/cases/keyword.vim @@ -0,0 +1,17 @@ +let g:vim_vue_plugin_config = { + \'syntax': { + \ 'template': ['html'], + \ 'script': ['javascript', 'typescript'], + \}, + \'keyword': 1, + \'initial_indent': ['script', 'style', 'codepen-additional'], + \} + +function! Check() + call ExpectSyntax('name', 'vueObjectKey') + call ExpectSyntax('data', 'vueObjectFuncKey') + call ExpectSyntax('data', 'vueObjectFuncName') + call ExpectSyntax('computed', 'vueObjectKey') + call ExpectSyntax('mounted', 'vueObjectFuncName') +endfunction +let g:test_flatten = 1 diff --git a/test/cases/keyword.vue b/test/cases/keyword.vue new file mode 100644 index 0000000..992725b --- /dev/null +++ b/test/cases/keyword.vue @@ -0,0 +1,45 @@ + + + diff --git a/test/cases/miniprogram.vim b/test/cases/miniprogram.vim new file mode 100644 index 0000000..f9398bf --- /dev/null +++ b/test/cases/miniprogram.vim @@ -0,0 +1,9 @@ +" .wpy for miniprogram, with initial indent for script and style by default +let g:test_flatten = 1 + +function! Check() + call ExpectSyntax('view', 'VueCustomTag') + call ExpectSyntax('wx:if', 'VueKey') + call ExpectSyntax('show', 'VueExpression') + call ExpectSyntax('text', 'VueCustomTag') +endfunction diff --git a/test/cases/miniprogram.wpy b/test/cases/miniprogram.wpy new file mode 100644 index 0000000..2ee8324 --- /dev/null +++ b/test/cases/miniprogram.wpy @@ -0,0 +1,21 @@ + + + + + diff --git a/test/cases/oneline.vim b/test/cases/oneline.vim new file mode 100644 index 0000000..41c1192 --- /dev/null +++ b/test/cases/oneline.vim @@ -0,0 +1,23 @@ +" One-line blocks like + +function! Check() + call ExpectSyntax('script', 'javascriptScriptBlock') + call ExpectSyntax('p {', 'cssStyleBlock') + call ExpectSyntax('color', 'cssTextProp') + call ExpectSyntax('p {', 'cssStyleBlock') + call ExpectSyntax('color', 'cssTextProp') + call ExpectSyntax('Hello', 'htmlTemplateBlock') + call CheckSync() +endfunction + +" Syntax from a sync point, as after jumping to a line not displayed yet +function! CheckSync() + silent edit! + call cursor(line('$'), 1) + call search('Hello', 'cW') + if index(SyntaxList(), 'htmlTemplateBlock') == -1 + call Fail('after syncing, Hello should be htmlTemplateBlock, but got ' + \.string(SyntaxList())) + endif +endfunction +let g:test_flatten = 1 diff --git a/test/cases/oneline.vue b/test/cases/oneline.vue new file mode 100644 index 0000000..d8db5f4 --- /dev/null +++ b/test/cases/oneline.vue @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/test/cases/random.vim b/test/cases/random.vim new file mode 100644 index 0000000..411eccc --- /dev/null +++ b/test/cases/random.vim @@ -0,0 +1,11 @@ +let g:vim_vue_plugin_config = { + \'syntax': { + \ 'template': ['html'], + \ 'script': ['javascript'], + \ 'style': [], + \}, + \'attribute': 1, + \'keyword': 1, + \'foldexpr': 1, + \} +let g:test_flatten = 1 diff --git a/test/cases/random.vue b/test/cases/random.vue new file mode 100644 index 0000000..45b12b2 --- /dev/null +++ b/test/cases/random.vue @@ -0,0 +1,151 @@ + + + + diff --git a/test/cases/script-setup.vim b/test/cases/script-setup.vim new file mode 100644 index 0000000..40dc916 --- /dev/null +++ b/test/cases/script-setup.vim @@ -0,0 +1,19 @@ +let g:vim_vue_plugin_config = { + \'syntax': { + \ 'template': ['html'], + \ 'script': ['javascript', 'typescript'], + \}, + \'keyword': 1, + \} + +function! Check() + call ExpectSyntax('withDefaults', 'vue3Keyword') + call ExpectSyntax('defineProps<', 'vue3Keyword') + call ExpectSyntax('defineEmits<', 'vue3Keyword') + call ExpectSyntax('defineModel<', 'vue3Keyword') + call ExpectSyntax('useTemplateRef', 'vue3Keyword') + call ExpectSyntax('ref', 'vue3Keyword') + call ExpectNoSyntax('value < 3', 'vue3Keyword') + call ExpectSyntax('defineExpose', 'vue3Keyword') +endfunction +let g:test_flatten = 1 diff --git a/test/cases/script-setup.vue b/test/cases/script-setup.vue new file mode 100644 index 0000000..d64cd51 --- /dev/null +++ b/test/cases/script-setup.vue @@ -0,0 +1,15 @@ + + + diff --git a/test/cases/template.vim b/test/cases/template.vim new file mode 100644 index 0000000..432af00 --- /dev/null +++ b/test/cases/template.vim @@ -0,0 +1,19 @@ +" Nested