Skip to content

Commit 4564e30

Browse files
fix(lang-service): gurantee using pipeline for completions list (#295)
* fix(lang-service): avoid duplicate references in scoped class names * test(e2e): add vineEmits test cases * fix(lang-service): gurantee using pipeline for completions list
1 parent 5cccf01 commit 4564e30

File tree

12 files changed

+588
-371
lines changed

12 files changed

+588
-371
lines changed

packages/e2e-test/src/app.vine.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const routes = [
99
{ path: '/vibe', label: 'Vibe' },
1010
{ path: '/use-defaults', label: 'Use Defaults' },
1111
{ path: '/vine-model', label: 'Vine Model' },
12+
{ path: '/vine-emits', label: 'Vine Emits' },
1213
{ path: '/vine-validators', label: 'Vine Validators' },
1314
{ path: '/todo-list', label: 'Todo List' },
1415
{ path: '/mix-with-jsx', label: 'Mix With JSX' },
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { ref } from 'vue'
2+
3+
function TestDefineEmitsByTypes() {
4+
const emits = vineEmits<{
5+
foo: [string]
6+
}>()
7+
8+
return vine`
9+
<button class="emits-defined-by-types" @click="emits('foo', '111')">
10+
emits defined by types
11+
</button>
12+
`
13+
}
14+
15+
function TestDefineEmitsByNames() {
16+
const emits = vineEmits(['bar'])
17+
18+
return vine`
19+
<button class="emits-defined-by-names" @click="emits('bar', '222')">
20+
emits defined by names
21+
</button>
22+
`
23+
}
24+
25+
export function TestVineEmitsPage() {
26+
const count = ref(0)
27+
const onEvent = (event: any) => {
28+
console.log('accept event', event)
29+
count.value++
30+
}
31+
32+
return vine`
33+
<div class="flex flex-col gap-4 p-4">
34+
<TestDefineEmitsByTypes @foo="onEvent" />
35+
<TestDefineEmitsByNames @bar="onEvent" />
36+
<div class="result">count: {{ count }}</div>
37+
</div>
38+
`
39+
}

packages/e2e-test/src/router.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { TodoList } from './fixtures/todo-list.vine'
1010
import { TestTransformAssetUrl } from './fixtures/transform-asset-url.vine'
1111
import { TestUseDefaults } from './fixtures/use-defaults.vine'
1212
import { TestVibe } from './fixtures/vibe.vine'
13+
import { TestVineEmitsPage } from './fixtures/vine-emits.vine'
1314
import { TestVineModel } from './fixtures/vine-model.vine'
1415
import { TestVinePropPage } from './fixtures/vine-prop.vine'
1516
import { TestVineSlots } from './fixtures/vine-slots.vine'
@@ -26,6 +27,7 @@ const routes = [
2627
{ path: '/vibe', component: TestVibe },
2728
{ path: '/use-defaults', component: TestUseDefaults },
2829
{ path: '/vine-model', component: TestVineModel },
30+
{ path: '/vine-emits', component: TestVineEmitsPage },
2931
{ path: '/vine-validators', component: TestVineValidatorsPage },
3032
{ path: '/todo-list', component: TodoList },
3133
{ path: '/mix-with-jsx', component: TestVineWithJsx },

packages/e2e-test/tests/basic.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,20 @@ describe('test basic functionality', async () => {
181181
},
182182
))
183183

184+
it('should support vineEmits', runTestAtPage(
185+
'/vine-emits',
186+
browserCtx,
187+
async () => {
188+
expect(await evaluator.getTextContent('.result')).toBe('count: 0')
189+
190+
await browserCtx.page?.click('.emits-defined-by-types')
191+
expect(await evaluator.getTextContent('.result')).toBe('count: 1')
192+
193+
await browserCtx.page?.click('.emits-defined-by-names')
194+
expect(await evaluator.getTextContent('.result')).toBe('count: 2')
195+
},
196+
))
197+
184198
it('should support vineSlots different use cases', runTestAtPage(
185199
'/vine-slots',
186200
browserCtx,

packages/language-server/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"typecheck": "tsgo --noEmit"
3737
},
3838
"dependencies": {
39+
"@volar/language-core": "catalog:volar",
3940
"@volar/language-server": "catalog:volar",
4041
"@volar/language-service": "catalog:volar",
4142
"@vue-vine/compiler": "workspace:*",
@@ -46,6 +47,7 @@
4647
"volar-service-emmet": "catalog:volar",
4748
"volar-service-html": "catalog:volar",
4849
"volar-service-typescript": "catalog:volar",
50+
"vscode-css-languageservice": "catalog:vscode",
4951
"vscode-languageserver-protocol": "catalog:vscode",
5052
"vscode-languageserver-textdocument": "catalog:vscode",
5153
"ws": "catalog:miscs"

packages/language-server/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import {
99
} from '@volar/language-server/node'
1010
import { createVueVineLanguagePlugin, setupGlobalTypes } from '@vue-vine/language-service'
1111
import { getDefaultCompilerOptions } from '@vue/language-core'
12-
import { create as createCssService } from 'volar-service-css'
1312
import { create as createEmmetService } from 'volar-service-emmet'
1413
import { create as createTypeScriptServices } from 'volar-service-typescript'
14+
import { createVineCssService } from './plugins/vine-css-service'
1515
import { createVineDiagnosticsPlugin } from './plugins/vine-diagnostics'
1616
import { createDocumentHighlightForward } from './plugins/vine-document-highlight'
1717
import { createVineFoldingRangesPlugin } from './plugins/vine-folding-ranges'
@@ -37,7 +37,7 @@ connection.onInitialize(async (params) => {
3737
)
3838

3939
const plugins = [
40-
createCssService(),
40+
createVineCssService(),
4141
createEmmetService(),
4242
// Vine plugins:
4343
createVineDiagnosticsPlugin(),
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import type { LanguageServicePlugin } from '@volar/language-service'
2+
import type { Provide } from 'volar-service-css'
3+
import type * as css from 'vscode-css-languageservice'
4+
import type { TextDocument } from 'vscode-languageserver-textdocument'
5+
import { isRenameEnabled } from '@volar/language-core'
6+
import { isVueVineVirtualCode } from '@vue-vine/language-service'
7+
import { create as createCssService } from 'volar-service-css'
8+
import { getVueVineVirtualCode } from '../utils'
9+
10+
export function createVineCssService(): LanguageServicePlugin {
11+
const baseCssService = createCssService({ scssDocumentSelector: ['scss', 'postcss'] })
12+
13+
return {
14+
...baseCssService,
15+
name: 'vue-vine-css-service',
16+
create(context) {
17+
const baseCssServiceInstance = baseCssService.create(context)
18+
const {
19+
'css/languageService': getCssLs,
20+
'css/stylesheet': getStylesheet,
21+
} = baseCssServiceInstance.provide as Provide
22+
23+
return {
24+
...baseCssServiceInstance,
25+
async provideDiagnostics(document, token) {
26+
let diagnostics = await baseCssServiceInstance.provideDiagnostics?.(document, token) ?? []
27+
if (document.languageId === 'postcss') {
28+
diagnostics = diagnostics.filter(diag =>
29+
diag.code !== 'css-semicolonexpected'
30+
&& diag.code !== 'css-ruleorselectorexpected'
31+
&& diag.code !== 'unknownAtRules',
32+
)
33+
}
34+
return diagnostics
35+
},
36+
37+
/**
38+
* If the position is within the virtual code and navigation is enabled,
39+
* skip the CSS navigation feature to avoid duplicate results.
40+
*/
41+
provideReferences(document, position) {
42+
if (isWithinNavigationVirtualCode(document, position)) {
43+
return
44+
}
45+
return worker(document, (stylesheet, cssLs) => {
46+
return cssLs.findReferences(document, position, stylesheet)
47+
})
48+
},
49+
provideRenameRange(document, position) {
50+
if (isWithinNavigationVirtualCode(document, position)) {
51+
return
52+
}
53+
return worker(document, (stylesheet, cssLs) => {
54+
return cssLs.prepareRename(document, position, stylesheet)
55+
})
56+
},
57+
}
58+
59+
function isWithinNavigationVirtualCode(
60+
document: TextDocument,
61+
position: css.Position,
62+
) {
63+
const { vineVirtualCode: root } = getVueVineVirtualCode(document, context)
64+
if (!isVueVineVirtualCode(root)) {
65+
return false
66+
}
67+
68+
const offset = document.offsetAt(position)
69+
for (const { sourceOffsets, lengths, data } of root.mappings) {
70+
if (!sourceOffsets.length || !isRenameEnabled(data)) {
71+
continue
72+
}
73+
74+
const start = sourceOffsets[0]
75+
const end = sourceOffsets.at(-1)! + lengths.at(-1)!
76+
77+
if (offset >= start && offset <= end) {
78+
return true
79+
}
80+
}
81+
return false
82+
}
83+
84+
function worker<T>(
85+
document: TextDocument,
86+
callback: (stylesheet: css.Stylesheet, cssLs: css.LanguageService) => T,
87+
) {
88+
const cssLs = getCssLs(document)
89+
if (!cssLs) {
90+
return
91+
}
92+
return callback(getStylesheet(document, cssLs), cssLs)
93+
}
94+
},
95+
}
96+
}

packages/language-server/src/plugins/vine-template.ts

Lines changed: 29 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ export function isTemplateDiagnosticOfVineCompName(vineDiag: VineDiagnostic, vin
5151

5252
export function createVineTemplatePlugin(): LanguageServicePlugin {
5353
let customData: IHTMLDataProvider[] = []
54-
const tagInfosMap = new Map<string, Map<string, HtmlTagInfo>>()
5554

5655
const onDidChangeCustomDataListeners = new Set<() => void>()
5756
const onDidChangeCustomData = (listener: () => void): Disposable => {
@@ -97,9 +96,6 @@ export function createVineTemplatePlugin(): LanguageServicePlugin {
9796

9897
return {
9998
...baseServiceInstance,
100-
dispose() {
101-
baseServiceInstance.dispose?.()
102-
},
10399
async provideCompletionItems(document, position, completionContext, triggerCharToken) {
104100
if (document.languageId !== 'html') {
105101
return
@@ -116,14 +112,7 @@ export function createVineTemplatePlugin(): LanguageServicePlugin {
116112
return
117113
}
118114

119-
let tagInfos: Map<string, HtmlTagInfo>
120-
if (!tagInfosMap.has(vineVirtualCode.fileName)) {
121-
tagInfos = new Map()
122-
tagInfosMap.set(vineVirtualCode.fileName, tagInfos)
123-
}
124-
else {
125-
tagInfos = tagInfosMap.get(vineVirtualCode.fileName)!
126-
}
115+
const tagInfos = new Map<string, HtmlTagInfo>()
127116

128117
// Set up HTML data providers before requesting completions
129118
const { sync } = provideHtmlData(
@@ -250,25 +239,12 @@ export function createVineTemplatePlugin(): LanguageServicePlugin {
250239
provideAttributes: (tag) => {
251240
const tagAttrs: IAttributeData[] = []
252241
let tagInfo = tagInfos.get(tag)
253-
const findAtVineCompFn = vineVirtualCode.vineMetaCtx.vineFileCtx.vineCompFns.find(
254-
(compFn) => {
255-
return compFn.fnName === tag
256-
},
257-
)
258242

259-
if (findAtVineCompFn?.propsDefinitionBy === VinePropsDefinitionBy.typeLiteral) {
260-
// If trigger on a tag that references a local component(in current file),
261-
// we recompute tagInfo
262-
tagInfo = {
263-
props: Object.keys(findAtVineCompFn.props).map(prop => hyphenateAttr(prop)),
264-
events: findAtVineCompFn.emits.map(emit => hyphenateAttr(emit)),
265-
}
266-
tagInfos.set(tag, tagInfo)
267-
}
268-
else if (!tagInfo) {
269-
// Trigger on a tag that may be:
270-
// - a native HTML element
271-
// - references a external component, that we need to fetch props from pipeline
243+
// Trigger on a tag that may be:
244+
// - a native HTML element
245+
// - references a local component that has complex props type
246+
// - references an external component
247+
if (!tagInfo) {
272248
try {
273249
getComponentPropsFromPipeline(tag, pipelineClientContext)
274250
getElementAttrsFromPipeline(tag, pipelineClientContext)
@@ -281,6 +257,21 @@ export function createVineTemplatePlugin(): LanguageServicePlugin {
281257
return tagAttrs
282258
}
283259

260+
const foundLocalVineCompFn = vineVirtualCode.vineMetaCtx.vineFileCtx.vineCompFns.find(
261+
compFn => compFn.fnName === tag,
262+
)
263+
// If trigger on a tag that references a local component(in current file),
264+
// we can use as much as possible existing information to fill `tagInfo`
265+
if (foundLocalVineCompFn) {
266+
const hasEmits = foundLocalVineCompFn.emits.length > 0
267+
if (foundLocalVineCompFn?.propsDefinitionBy === VinePropsDefinitionBy.typeLiteral) {
268+
tagInfo.props = Object.keys(foundLocalVineCompFn.props).map(prop => hyphenateAttr(prop))
269+
}
270+
if (hasEmits) {
271+
tagInfo.events = foundLocalVineCompFn.emits.map(emit => hyphenateAttr(emit))
272+
}
273+
}
274+
284275
const { props, events } = tagInfo
285276
const attributes: IAttributeData[] = []
286277

@@ -302,14 +293,18 @@ export function createVineTemplatePlugin(): LanguageServicePlugin {
302293
attributes.push({ name: hyphenateAttr(prop) })
303294
}
304295
else {
296+
const hyphenatedProp = hyphenateAttr(prop)
305297
attributes.push(
306-
{ name: prop },
307-
{ name: `:${prop}` },
308-
{ name: `!${prop}` },
309-
{ name: `v-bind:${prop}` },
298+
{ name: hyphenatedProp },
299+
{ name: `:${hyphenatedProp}` },
300+
{ name: `!${hyphenatedProp}` },
301+
{ name: `v-bind:${hyphenatedProp}` },
310302
)
311303
}
312304
}
305+
306+
// Just keep robustness for events
307+
// because this has actually not been used
313308
for (const event of events) {
314309
const name = hyphenateAttr(event)
315310

packages/language-service/typescript-plugin/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ export interface PipelineServerContext {
3939

4040
export interface PipelineLogger {
4141
enabled: boolean
42-
messages: string[]
4342
info: (...msg: any[]) => void
4443
error: (...msg: any[]) => void
4544
}

packages/language-service/typescript-plugin/utils.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,15 @@ export function createPipelineLogger({ enabled = false }: {
3232
} = {}): PipelineLogger {
3333
const logger: PipelineLogger = {
3434
enabled,
35-
messages: [] as string[],
3635
info: (...msg: string[]) => {
3736
if (!logger.enabled)
3837
return
39-
logger.messages.push(`[INFO] ${new Date().toLocaleString()}: ${msg.join(' ')}`)
38+
console.log(`[INFO] ${new Date().toLocaleString()}: `, ...msg)
4039
},
4140
error: (...msg: string[]) => {
4241
if (!logger.enabled)
4342
return
44-
logger.messages.push(`[ERROR] ${new Date().toLocaleString()}: ${msg.join(' ')}`)
43+
console.error(`[ERROR] ${new Date().toLocaleString()}: `, ...msg)
4544
},
4645
}
4746
return logger

0 commit comments

Comments
 (0)