Merge pull request #41 from leafOfTree/fix-sync-and-tests

Fix syntax sync and end tag, highlight <script setup> macros, add tests
This commit is contained in:
leaf authored and GitHub committed 2026-10-20 09:22:22 +08:00
commit ddf4a96bdb
40 files changed
+1263 -36

No files matched your search

+15
View File
@@ -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
+1
View File
@@ -1,3 +1,4 @@
README.html
test.vue
.DS_STORE
test/deps/
-19
View File
@@ -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
+21 -3
View File
@@ -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
```
<br />
@@ -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]
+3 -2
View File
@@ -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
+14 -9
View File
@@ -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 <script setup>
let s:script_setup = 'defineProps defineEmits defineModel defineExpose defineOptions defineSlots withDefaults'
let s:global = 'createApp createSSRApp h defineComponent defineAsyncComponent defineCustomElement resolveComponent resolveDynamicComponent resolveDirective withDirectives withModifiers createRenderer nextTick mergeProps cloneVNode isVNode'
let s:vue3_keywords = join([s:basic_reactive, s:refs, s:computed_and_watch, s:effect_scope, s:composition, s:helpers, s:script_setup, s:global], ' ')
let s:vue3_keywords_regexp = '\v<('
\.join(split(s:vue3_keywords, ' '), '|')
\.')\ze'
" Followed by ( or type arguments like defineProps<{ foo: string }>()
execute 'syntax match vue3Keyword display /'
\.s:vue3_keywords_regexp
\.'\_s*\(/'
\.'(\_s*\(|\<)/'
\.s:match_option
highlight default link vueObjectKey vueObjectKeyword
+3 -3
View File
@@ -124,9 +124,6 @@ function! s:SetSyntax(block, syntax, has_lang)
\.' end=+'.end.'+'
\.' keepend contains='.syntax_group.', vueTag'
execute 'syntax sync match vueSync groupthere '.name.' +'.start.'+'
execute 'syntax sync match vueSync groupthere NONE +'.end.'+'
" Block like <script src="...">...</script>
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)
+14
View File
@@ -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
+13
View File
@@ -0,0 +1,13 @@
<template>
<div @click="toggle(item)" :title="$t('title')" v-if="count > 1">
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
data() {
return {}
},
}
</script>
+26
View File
@@ -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
+19
View File
@@ -0,0 +1,19 @@
<template>
<div :class="{ active: isActive }" @click="toggle(item)" v-if="count > 1">
<MyComponent ref="comp" data-id="1" :title="$t('title')" />
<my-component v-model="text"></my-component>
<transition name="fade">
<p>{{ message.toUpperCase() }}</p>
</transition>
</div>
</template>
<script>
export default {
data() {
return {
count: 0,
}
},
}
</script>
+19
View File
@@ -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
+20
View File
@@ -0,0 +1,20 @@
<template>
<p>{{ greeting }}</p>
</template>
<script>
export default {
data: function () {
return {
greeting: 'Hello'
};
},
};
</script>
<style scoped>
p {
font-size: 2em;
text-align: center;
}
</style>
+27
View File
@@ -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
+11
View File
@@ -0,0 +1,11 @@
<template>
<div></div>
</template>
<script lang="ts">
const a: number = 1
</script>
<style lang="scss">
$a: 1px;
</style>
+40
View File
@@ -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></template>', '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
+160
View File
@@ -0,0 +1,160 @@
<template lang="pug">
div.set
span(id="123")
</template>
<template>
<template></template>
<div></div>
</template>
<template>
<p>{{ greeting }}</p>
</template>
<script>
export default {
data: function () {
return {
greeting: 'Hello'
};
},
};
</script>
<script lang="ts">
import Vue from 'vue'
import { User } from '@/types'
export default Vue.extend({
name: 'Home' as string,
data() {
return {
user: {} as User
}
},
computed: {
fullName(): string { // Computed Property returns a string
return `${this.user.firstName} ${this.user.lastName}`
}
},
mounted(): void { // The mounted hook returns nothing
...
}
})
</script>
<script lang="coffee">
# Assignment:
number = 42
opposite = true
# Conditions:
number = -42 if opposite
# Functions:
square = (x) -> x * x
# Arrays:
list = [1, 2, 3, 4, 5]
`
function hello() {}
console.log
a = 123;
`
# Objects:
math =
root: Math.sqrt
square: square
cube: (x) -> x * square x
# Splats:
race = (winner, runners...) ->
print winner, runners
# Existence:
alert "I knew it!" if elvis?
# Array comprehensions:
cubes = (math.cube num for num in list)
</script>
<style scoped>
p {
font-size: 2em;
text-align: center;
}
</style>
<style scoped lang="scss">
$a: 100px;
div {
background: white;
}
</style>
<style lang="sass">
$w: 128px;
.bg
$w: 128px;
background: white;
align-items: center;
canvas
width: 100vw;
</style>
<style lang="less">
@list: 10 12;
canvas {
background: white;
width: 100vh;
}
</style>
<style lang="stylus">
@import 'vendor'
body
font: 12px Helvetica, Arial, sans-serif
a.button
border-radius: 5px
</style>
<route>
{
"name": "home",
"meta": {
"requiresAuth": true
}
}
</route>
<docs>
# This is the documentation for component.
</docs>
<i18n lang="yaml">
en:
hello: "Hello World!"
ja:
hello: "こんにちは、世界!"
</i18n>
<i18n>
{
"en": {
"language": "Language",
"hello": "hello, world!"
},
"ja": {
"language": "言語",
"hello": "こんにちは、世界!"
}
}
</i18n>
+16
View File
@@ -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
+20
View File
@@ -0,0 +1,20 @@
<template>
<p>{{ greeting }}</p>
</template>
<script>
export default {
data: function () {
return {
greeting: 'Hello'
};
},
};
</script>
<style scoped>
p {
font-size: 2em;
text-align: center;
}
</style>
+10
View File
@@ -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
+27
View File
@@ -0,0 +1,27 @@
<template>
<p>{{ greeting }}</p>
</template>
<script>
export default {
data: function () {
return {
greeting: 'Hello'
};
},
};
</script>
<style scoped lang="scss">
p {
font-size: 2em;
text-align: center;
}
</style>
<codepen-additional>
const easings = {
linear: '',
easeInQuad: '',
}
</codepen-additional>
+10
View File
@@ -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
+28
View File
@@ -0,0 +1,28 @@
<script>
export default {}
</script>
<style lang="scss">
.a {
color: red;
}
</style>
<style>
.b {
color: blue;
}
</style>
<i18n>
{
"en": {
"hello": "Hello"
}
}
</i18n>
<i18n lang="yaml">
en:
hello: Hello
</i18n>
+17
View File
@@ -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
+45
View File
@@ -0,0 +1,45 @@
<script>
export default {
name: 'Home' as string,
data: function () {
return {
greeting: 'Hello'
};
},
data() {
return {
greeting: 'Hello'
};
},
computed: {
fullName() { // Computed Property returns a string
return `${this.user.firstName} ${this.user.lastName}`
}
},
mounted() { // The mounted hook returns nothing
...
}
};
</script>
<script lang="ts">
import Vue from 'vue'
import { User } from '@/types'
export default Vue.extend({
name: 'Home' as string,
data() {
return {
user: {} as User
}
},
computed: {
fullName(): string { // Computed Property returns a string
return `${this.user.firstName} ${this.user.lastName}`
}
},
mounted(): void { // The mounted hook returns nothing
...
}
})
</script>
+9
View File
@@ -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
+21
View File
@@ -0,0 +1,21 @@
<template>
<view wx:if="{{ show }}" class="greeting">
<text>{{ greeting }}</text>
</view>
</template>
<script>
export default {
data: function () {
return {
greeting: 'Hello'
};
},
};
</script>
<style scoped>
.greeting {
font-size: 2em;
}
</style>
+23
View File
@@ -0,0 +1,23 @@
" One-line blocks like <style>p { color: red; }</style>
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
+12
View File
@@ -0,0 +1,12 @@
<script src="./tmp"> </script>
<style lang="css">
p {
color: red;
}
</style>
<style>p { color: red; }</style>
<template><div>Hello</div></template>
+11
View File
@@ -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
+151
View File
@@ -0,0 +1,151 @@
<template>
<div class="editor-page">
<div class="container page">
<div class="row">
<div class="col-md-10 offset-md-1 col-xs-12">
<RwvListErrors :errors="errors" />
<form @submit.prevent="onPublish(article.slug)">
<fieldset :disabled="inProgress">
<fieldset class="form-group">
<input
type="text"
class="form-control form-control-lg"
v-model="article.title"
placeholder="Article Title"
/>
</fieldset>
<fieldset class="form-group">
<input
type="text"
class="form-control"
v-model="article.description"
placeholder="What's this article about?"
/>
</fieldset>
<fieldset class="form-group">
<textarea
class="form-control"
rows="8"
v-model="article.body"
placeholder="Write your article (in markdown)"
>
</textarea>
</fieldset>
<fieldset class="form-group">
<input
type="text"
class="form-control"
placeholder="Enter tags"
v-model="tagInput"
@keypress.enter.prevent="addTag(tagInput)"
/>
<div class="tag-list">
<span
class="tag-default tag-pill"
v-for="(tag, index) of article.tagList"
:key="tag + index"
>
<i class="ion-close-round" @click="removeTag(tag)"> </i>
{{ tag }}
</span>
</div>
</fieldset>
</fieldset>
<button
:disabled="inProgress"
class="btn btn-lg pull-xs-right btn-primary"
type="submit"
>
Publish Article
</button>
</form>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import store from "@/store";
import RwvListErrors from "@/components/ListErrors";
import {
ARTICLE_PUBLISH,
ARTICLE_EDIT,
FETCH_ARTICLE,
ARTICLE_EDIT_ADD_TAG,
ARTICLE_EDIT_REMOVE_TAG,
ARTICLE_RESET_STATE
} from "@/store/actions.type";
export default {
name: "RwvArticleEdit",
components: { RwvListErrors },
props: {
previousArticle: {
type: Object,
required: false
}
},
async beforeRouteUpdate(to, from, next) {
// Reset state if user goes from /editor/:id to /editor
// The component is not recreated so we use to hook to reset the state.
await store.dispatch(ARTICLE_RESET_STATE);
return next();
},
async beforeRouteEnter(to, from, next) {
// SO: https://github.com/vuejs/vue-router/issues/1034
// If we arrive directly to this url, we need to fetch the article
await store.dispatch(ARTICLE_RESET_STATE);
if (to.params.slug !== undefined) {
await store.dispatch(
FETCH_ARTICLE,
to.params.slug,
to.params.previousArticle
);
}
return next();
},
async beforeRouteLeave(to, from, next) {
await store.dispatch(ARTICLE_RESET_STATE);
next();
},
data() {
return {
tagInput: null,
inProgress: false,
errors: {}
};
},
computed: {
...mapGetters(["article"])
},
methods: {
onPublish(slug) {
let action = slug ? ARTICLE_EDIT : ARTICLE_PUBLISH;
this.inProgress = true;
this.$store
.dispatch(action)
.then(({ data }) => {
this.inProgress = false;
this.$router.push({
name: "article",
params: { slug: data.article.slug }
});
})
.catch(({ response }) => {
this.inProgress = false;
this.errors = response.data.errors;
});
},
removeTag(tag) {
this.$store.dispatch(ARTICLE_EDIT_REMOVE_TAG, tag);
},
addTag(tag) {
this.$store.dispatch(ARTICLE_EDIT_ADD_TAG, tag);
this.tagInput = null;
}
}
};
</script>
+19
View File
@@ -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<number>', 'vue3Keyword')
call ExpectNoSyntax('value < 3', 'vue3Keyword')
call ExpectSyntax('defineExpose', 'vue3Keyword')
endfunction
let g:test_flatten = 1
+15
View File
@@ -0,0 +1,15 @@
<template>
<div ref="el">{{ props.foo }}</div>
</template>
<script setup lang="ts">
const props = withDefaults(defineProps<{ foo?: string }>(), {
foo: 'a',
})
const emit = defineEmits<{ (e: 'change', id: number): void }>()
const model = defineModel<string>()
const el = useTemplateRef('el')
const count = ref<number>(1)
const small = count.value < 3
defineExpose({ el })
</script>
+19
View File
@@ -0,0 +1,19 @@
" Nested <template> and <docs> in <template>
let g:vim_vue_plugin_config = {
\'syntax': {
\ 'script': ['javascript'],
\ 'template': ['html'],
\ 'style': ['css'],
\ 'docs': 'markdown',
\},
\}
function! Check()
while search('/template', 'W')
call ExpectTag(line('.'), 'template')
endwhile
normal! gg
call ExpectNoSyntax('/docs', 'markdownDocsBlock')
call ExpectSyntax('/docs', 'markdownDocsBlock')
endfunction
let g:test_flatten = 1
+23
View File
@@ -0,0 +1,23 @@
<template>
<template>
<div></div>
</template>
<div></div>
<template>
<div></div>
</template>
<div></div>
<docs></docs>
<div></div>
</template>
<template>
<template></template>
<div></div>
</template>
<docs>
# Hello
</docs>
+16
View File
@@ -0,0 +1,16 @@
let g:vim_vue_plugin_config = {
\'syntax': {
\ 'template': ['html'],
\ 'script': ['javascript', 'typescript'],
\ 'style': ['css'],
\},
\'keyword': 1,
\}
function! Check()
call ExpectSyntax('v-bind', 'cssFunctionName')
call ExpectSyntax('slotted', 'cssPseudoClassId')
call ExpectSyntax('global', 'cssPseudoClassId')
call ExpectSyntax('deep', 'cssPseudoClassId')
endfunction
let g:test_flatten = 1
+8
View File
@@ -0,0 +1,8 @@
<style scoped>
button {
color: v-bind(color);
}
.a :slotted(.b) :global(.c) :deep(.d) {
/* ... */
}
</style>
Executable
+47
View File
@@ -0,0 +1,47 @@
#!/bin/sh
# Run all test cases, or the ones given as arguments, with vim and nvim if
# they are installed. Vim runs in a pseudo terminal by `script`, as syntax
# in Ex mode (-es) isn't the same as in a terminal.
# ./test/run.sh
# ./test/run.sh basic template
cd "$(dirname "$0")/.." || exit 1
# Syntax plugins used by test/cases/example.vue
mkdir -p test/deps
for repo in digitaltoad/vim-pug kchmck/vim-coffee-script \
leafgarland/typescript-vim iloginow/vim-stylus; do
dir=test/deps/${repo#*/}
[ -d "$dir" ] || git clone -q --depth 1 "https://github.com/$repo" "$dir"
done
if [ $# -gt 0 ]; then
cases=$*
else
cases=$(ls test/cases/*.vue test/cases/*.wpy 2>/dev/null | sed 's#.*/##; s#\.[a-z]*$##')
fi
out=$(mktemp)
status=0
expected=0
for name in $cases; do
if command -v vim >/dev/null; then
expected=$((expected + 1))
VUE_TEST_CASE=$name VUE_TEST_OUT=$out timeout 60 script -qec \
'stty rows 40 cols 120; vim -N -u NONE -i NONE -S test/run.vim' \
/dev/null >/dev/null || status=1
fi
if command -v nvim >/dev/null; then
expected=$((expected + 1))
VUE_TEST_CASE=$name VUE_TEST_OUT=$out timeout 60 \
nvim --headless -u NONE -i NONE -S test/run.vim || status=1
fi
done
cat "$out"
passed=$(grep -c '^ok' "$out")
failed=$(grep -c '^FAIL' "$out")
missing=$((expected - passed - failed))
echo "$passed passed, $failed failed, $missing without result"
[ $failed -eq 0 ] && [ $missing -eq 0 ] || status=1
rm -f "$out"
exit $status
+153
View File
@@ -0,0 +1,153 @@
" Run one test case, see test/run.sh
" VUE_TEST_CASE=basic vim -N -u NONE -i NONE -S test/run.vim
"
" A case is test/cases/<name>.vue (or .wpy), with optional
" test/cases/<name>.vim to set g:vim_vue_plugin_config and define Check() for
" more assertions. It fails if
" - indenting the whole file changes it
" - with g:test_flatten, indenting the file without any indent doesn't
" restore it
" - Check() calls Fail() or an Expect*() function fails
" - there are error messages
"
" Results are appended to $VUE_TEST_OUT. Exit code is 1 if the case fails.
set nocompatible
let s:root = expand('<sfile>:p:h:h')
let s:name = $VUE_TEST_CASE
let s:case = s:root.'/test/cases/'.s:name
let s:file = filereadable(s:case.'.vue') ? s:case.'.vue' : s:case.'.wpy'
let s:errors = []
" Plugin first, before Vim's placeholder vue syntax and indent files
let &runtimepath = join([s:root, s:root.'/test/runtime']
\ + glob(s:root.'/test/deps/*', 0, 1)
\ + [&runtimepath], ',')
set expandtab shiftwidth=2
" Plugin scripts aren't loaded with -u NONE, e.g. vim-stylus sets g:css_units
runtime! plugin/**/*.vim
" Helpers for Check()
function! Fail(msg)
call add(s:errors, a:msg)
endfunction
function! SyntaxList()
return map(synstack(line('.'), col('.')), { _, id -> synIDattr(id, 'name') })
endfunction
" Search pattern forward from cursor and check syntax at the match
function! ExpectSyntax(pattern, syntax)
if !search('\V'.a:pattern, 'W')
return Fail('not found: '.a:pattern)
endif
if index(SyntaxList(), a:syntax) == -1
call Fail(printf('%s at line %d should be %s, but got %s',
\ a:pattern, line('.'), a:syntax, string(SyntaxList())))
endif
endfunction
function! ExpectNoSyntax(pattern, syntax)
if !search('\V'.a:pattern, 'W')
return Fail('not found: '.a:pattern)
endif
if index(SyntaxList(), a:syntax) != -1
call Fail(printf('%s at line %d should not be %s, but got %s',
\ a:pattern, line('.'), a:syntax, string(SyntaxList())))
endif
endfunction
function! ExpectFoldlevel(lnum, level)
if foldlevel(a:lnum) != a:level
call Fail(printf('foldlevel at line %d should be %d, but got %d',
\ a:lnum, a:level, foldlevel(a:lnum)))
endif
endfunction
function! ExpectEqual(actual, expected, desc)
if a:actual !=# a:expected
call Fail(printf('%s should be %s, but got %s',
\ a:desc, string(a:expected), string(a:actual)))
endif
endfunction
" Search pattern forward from cursor and check the block there by
" GetVueTag(), GetVueSyntax() and the block syntax group at the line end
function! ExpectBlock(pattern, tag, syntax, group)
if !search('\V'.a:pattern, 'W')
return Fail('not found: '.a:pattern)
endif
let desc = printf('%s at line %d: ', a:pattern, line('.'))
call ExpectEqual(GetVueTag(), a:tag, desc.'tag')
call ExpectEqual(GetVueSyntax(), a:syntax, desc.'syntax')
let group = synIDattr(synstack(line('.'), col('$') - 1)[0], 'name')
call ExpectEqual(group, a:group, desc.'block group')
endfunction
function! ExpectTag(lnum, tag)
if GetVueTag(a:lnum) !=# a:tag
call Fail(printf('tag at line %d should be %s, but got %s',
\ a:lnum, string(a:tag), string(GetVueTag(a:lnum))))
endif
endfunction
if filereadable(s:case.'.vim')
execute 'source '.fnameescape(s:case.'.vim')
endif
filetype plugin indent on
syntax on
messages clear
execute 'silent edit '.fnameescape(s:file)
redraw
function! s:ExpectLines(lines, desc)
let actual = getline(1, '$')
if actual == a:lines
return
endif
let i = 0
while i < len(a:lines) && get(actual, i) ==# a:lines[i]
let i += 1
endwhile
call Fail(printf('%s changed line %d: expected %s, but got %s',
\ a:desc, i + 1, string(get(a:lines, i, '')), string(get(actual, i, ''))))
endfunction
" Indent
let s:lines = getline(1, '$')
silent normal! gg=G
call s:ExpectLines(s:lines, 'indent')
silent edit!
" Indent without any indent
if get(g:, 'test_flatten', 0)
silent %s/^\s\+//e
silent normal! gg=G
call s:ExpectLines(s:lines, 'indent from flat')
silent edit!
endif
" Assertions
normal! gg
if exists('*Check')
call Check()
endif
" Errors
for s:msg in split(execute('messages'), "\n")
if s:msg =~# '^E\d\+:\|^Error detected'
call Fail('message: '.s:msg)
endif
endfor
let s:editor = has('nvim') ? 'nvim' : 'vim'
call writefile(empty(s:errors)
\ ? [printf('ok %s %s', s:editor, s:name)]
\ : [printf('FAIL %s %s', s:editor, s:name)]
\ + map(copy(s:errors), '" ".v:val'),
\ $VUE_TEST_OUT, 'a')
if !empty(s:errors)
cquit
endif
qall!
+157
View File
@@ -0,0 +1,157 @@
" Vim indent file
" Language: YAML
" Maintainer: Nikolai Pavlov <zyx.vim@gmail.com>
" Last Update: Lukas Reineke
" Last Change: 2020 Jun 07
" Only load this indent file when no other was loaded.
if exists('b:did_indent')
finish
endif
let s:save_cpo = &cpo
set cpo&vim
let b:did_indent = 1
setlocal indentexpr=GetYAMLIndent(v:lnum)
setlocal indentkeys=!^F,o,O,0#,0},0],<:>,0-
setlocal nosmartindent
let b:undo_indent = 'setlocal indentexpr< indentkeys< smartindent<'
" Only define the function once.
if exists('*GetYAMLIndent')
finish
endif
function s:FindPrevLessIndentedLine(lnum, ...)
let prevlnum = prevnonblank(a:lnum-1)
let curindent = a:0 ? a:1 : indent(a:lnum)
while prevlnum
\&& indent(prevlnum) >= curindent
\&& getline(prevlnum) !~# '^\s*#'
let prevlnum = prevnonblank(prevlnum-1)
endwhile
return prevlnum
endfunction
function s:FindPrevLEIndentedLineMatchingRegex(lnum, regex)
let plilnum = s:FindPrevLessIndentedLine(a:lnum, indent(a:lnum)+1)
while plilnum && getline(plilnum) !~# a:regex
let plilnum = s:FindPrevLessIndentedLine(plilnum)
endwhile
return plilnum
endfunction
let s:mapkeyregex='\v^\s*\#@!\S@=%(\''%([^'']|\''\'')*\'''.
\ '|\"%([^"\\]|\\.)*\"'.
\ '|%(%(\:\ )@!.)*)\:%(\ |$)'
let s:liststartregex='\v^\s*%(\-%(\ |$))'
let s:c_ns_anchor_char = '\v%([\n\r\uFEFF \t,[\]{}]@!\p)'
let s:c_ns_anchor_name = s:c_ns_anchor_char.'+'
let s:c_ns_anchor_property = '\v\&'.s:c_ns_anchor_name
let s:ns_word_char = '\v[[:alnum:]_\-]'
let s:ns_tag_char = '\v%('.s:ns_word_char.'|[#/;?:@&=+$.~*''()])'
let s:c_named_tag_handle = '\v\!'.s:ns_word_char.'+\!'
let s:c_secondary_tag_handle = '\v\!\!'
let s:c_primary_tag_handle = '\v\!'
let s:c_tag_handle = '\v%('.s:c_named_tag_handle.
\ '|'.s:c_secondary_tag_handle.
\ '|'.s:c_primary_tag_handle.')'
let s:c_ns_shorthand_tag = '\v'.s:c_tag_handle . s:ns_tag_char.'+'
let s:c_non_specific_tag = '\v\!'
let s:ns_uri_char = '\v%('.s:ns_word_char.'\v|[#/;?:@&=+$,.!~*''()[\]])'
let s:c_verbatim_tag = '\v\!\<'.s:ns_uri_char.'+\>'
let s:c_ns_tag_property = '\v'.s:c_verbatim_tag.
\ '\v|'.s:c_ns_shorthand_tag.
\ '\v|'.s:c_non_specific_tag
let s:block_scalar_header = '\v[|>]%([+-]?[1-9]|[1-9]?[+-])?'
function GetYAMLIndent(lnum)
if a:lnum == 1 || !prevnonblank(a:lnum-1)
return 0
endif
let prevlnum = prevnonblank(a:lnum-1)
let previndent = indent(prevlnum)
let line = getline(a:lnum)
if line =~# '^\s*#' && getline(a:lnum-1) =~# '^\s*#'
" Comment blocks should have identical indent
return previndent
elseif line =~# '^\s*[\]}]'
" Lines containing only closing braces should have previous indent
return indent(s:FindPrevLessIndentedLine(a:lnum))
endif
" Ignore comment lines when calculating indent
while getline(prevlnum) =~# '^\s*#'
let prevlnum = prevnonblank(prevlnum-1)
if !prevlnum
return previndent
endif
endwhile
let prevline = getline(prevlnum)
let previndent = indent(prevlnum)
" Any examples below assume that shiftwidth=2
if prevline =~# '\v[{[:]$|[:-]\ [|>][+\-]?%(\s+\#.*|\s*)$'
" Mapping key:
" nested mapping: ...
"
" - {
" key: [
" list value
" ]
" }
"
" - |-
" Block scalar without indentation indicator
return previndent+shiftwidth()
elseif prevline =~# '\v[:-]\ [|>]%(\d+[+\-]?|[+\-]?\d+)%(\#.*|\s*)$'
" - |+2
" block scalar with indentation indicator
"#^^ indent+2, not indent+shiftwidth
return previndent + str2nr(matchstr(prevline,
\'\v([:-]\ [|>])@<=[+\-]?\d+%([+\-]?%(\s+\#.*|\s*)$)@='))
elseif prevline =~# '\v\"%([^"\\]|\\.)*\\$'
" "Multiline string \
" with escaped end"
let qidx = match(prevline, '\v\"%([^"\\]|\\.)*\\')
return virtcol([prevlnum, qidx+1])
elseif line =~# s:liststartregex
" List line should have indent equal to previous list line unless it was
" caught by one of the previous rules
return indent(s:FindPrevLEIndentedLineMatchingRegex(a:lnum,
\ s:liststartregex))
elseif line =~# s:mapkeyregex
" Same for line containing mapping key
let prevmapline = s:FindPrevLEIndentedLineMatchingRegex(a:lnum,
\ s:mapkeyregex)
if getline(prevmapline) =~# '^\s*- '
return indent(prevmapline) + 2
else
return indent(prevmapline)
endif
elseif prevline =~# '^\s*- '
" - List with
" multiline scalar
return previndent+2
elseif prevline =~# s:mapkeyregex . '\v\s*%(%('.s:c_ns_tag_property.
\ '\v|'.s:c_ns_anchor_property.
\ '\v|'.s:block_scalar_header.
\ '\v)%(\s+|\s*%(\#.*)?$))*'
" Mapping with: value
" that is multiline scalar
return previndent+shiftwidth()
endif
return previndent
endfunction
let &cpo = s:save_cpo