@@ -19,8 +19,7 @@ import { loadModelModifiersData, loadTemplateData } from './data';
1919
2020type InternalItemId =
2121 | 'componentEvent'
22- | 'componentProp'
23- | 'specialTag' ;
22+ | 'componentProp' ;
2423
2524const specialTags = new Set ( [
2625 'slot' ,
@@ -174,24 +173,20 @@ export function create(
174173 return ;
175174 }
176175
177- // #4298: Precompute HTMLDocument before provideHtmlData to avoid parseHTMLDocument requesting component names from tsserver
178- baseServiceInstance . provideCompletionItems ?.( document , position , completionContext , token ) ;
179-
180- let sync = ( await provideHtmlData ( sourceScript . id , root ) ) . sync ;
181- let currentVersion : number | undefined ;
182- let completionList : CompletionList | null | undefined ;
183-
184- while ( currentVersion !== ( currentVersion = await sync ?.( ) ) ) {
185- completionList = await baseServiceInstance . provideCompletionItems ?.(
186- document ,
187- position ,
188- completionContext ,
189- token ,
190- ) ;
191- }
176+ const completionList = await runWithVueData (
177+ sourceScript . id ,
178+ root ,
179+ ( ) =>
180+ baseServiceInstance . provideCompletionItems ! (
181+ document ,
182+ position ,
183+ completionContext ,
184+ token ,
185+ ) ,
186+ ) ;
192187
193188 if ( completionList ) {
194- transformCompletionList ( completionList , document ) ;
189+ postProcessCompletionList ( completionList , document ) ;
195190 return completionList ;
196191 }
197192 } ,
@@ -209,30 +204,37 @@ export function create(
209204 } ,
210205 } ;
211206
207+ async function runWithVueData < T > ( sourceDocumentUri : URI , vueCode : VueVirtualCode , fn : ( ) => T ) {
208+ // #4298: Precompute HTMLDocument before provideHtmlData to avoid parseHTMLDocument requesting component names from tsserver
209+ await fn ( ) ;
210+
211+ const { sync } = await provideHtmlData ( sourceDocumentUri , vueCode ) ;
212+ let lastVersion = await sync ( ) ;
213+ let result = await fn ( ) ;
214+ while ( lastVersion !== ( lastVersion = await sync ( ) ) ) {
215+ result = await fn ( ) ;
216+ }
217+ return result ;
218+ }
219+
212220 async function provideHtmlData ( sourceDocumentUri : URI , vueCode : VueVirtualCode ) {
213221 await ( initializing ??= initialize ( ) ) ;
214222
215223 const casing = await checkCasing ( context , sourceDocumentUri ) ;
216224
217- if ( builtInData . tags ) {
218- for ( const tag of builtInData . tags ) {
219- if ( isItemKey ( tag . name ) ) {
220- continue ;
221- }
222-
223- if ( specialTags . has ( tag . name ) ) {
224- tag . name = generateItemKey ( 'specialTag' , tag . name , '' ) ;
225- }
226- else if ( casing . tag === TagNameCasing . Kebab ) {
227- tag . name = hyphenateTag ( tag . name ) ;
228- }
229- else {
230- tag . name = camelize ( capitalize ( tag . name ) ) ;
231- }
225+ for ( const tag of builtInData . tags ?? [ ] ) {
226+ if ( specialTags . has ( tag . name ) ) {
227+ continue ;
228+ }
229+ if ( casing . tag === TagNameCasing . Kebab ) {
230+ tag . name = hyphenateTag ( tag . name ) ;
231+ }
232+ else {
233+ tag . name = camelize ( capitalize ( tag . name ) ) ;
232234 }
233235 }
234236
235- const promises : Promise < void > [ ] = [ ] ;
237+ const tasks : Promise < void > [ ] = [ ] ;
236238 const tagInfos = new Map < string , {
237239 attrs : string [ ] ;
238240 propInfos : ComponentPropInfo [ ] ;
@@ -252,7 +254,8 @@ export function create(
252254 isApplicable : ( ) => true ,
253255 provideTags : ( ) => {
254256 if ( ! components ) {
255- promises . push ( ( async ( ) => {
257+ components = [ ] ;
258+ tasks . push ( ( async ( ) => {
256259 components = ( await tsPluginClient ?. getComponentNames ( vueCode . fileName ) ?? [ ] )
257260 . filter ( name =>
258261 name !== 'Transition'
@@ -264,7 +267,6 @@ export function create(
264267 lastCompletionComponentNames = new Set ( components ) ;
265268 version ++ ;
266269 } ) ( ) ) ;
267- return [ ] ;
268270 }
269271 const scriptSetupRanges = tsCodegen . get ( vueCode . sfc ) ?. getScriptSetupRanges ( ) ;
270272 const names = new Set < string > ( ) ;
@@ -299,10 +301,16 @@ export function create(
299301 return tags ;
300302 } ,
301303 provideAttributes : tag => {
302- const tagInfo = tagInfos . get ( tag ) ;
303-
304+ let tagInfo = tagInfos . get ( tag ) ;
304305 if ( ! tagInfo ) {
305- promises . push ( ( async ( ) => {
306+ tagInfo = {
307+ attrs : [ ] ,
308+ propInfos : [ ] ,
309+ events : [ ] ,
310+ directives : [ ] ,
311+ } ;
312+ tagInfos . set ( tag , tagInfo ) ;
313+ tasks . push ( ( async ( ) => {
306314 const attrs = await tsPluginClient ?. getElementAttrs ( vueCode . fileName , tag ) ?? [ ] ;
307315 const propInfos = await tsPluginClient ?. getComponentProps ( vueCode . fileName , tag ) ?? [ ] ;
308316 const events = await tsPluginClient ?. getComponentEvents ( vueCode . fileName , tag ) ?? [ ] ;
@@ -315,7 +323,6 @@ export function create(
315323 } ) ;
316324 version ++ ;
317325 } ) ( ) ) ;
318- return [ ] ;
319326 }
320327
321328 const { attrs, propInfos, events, directives } = tagInfo ;
@@ -458,63 +465,26 @@ export function create(
458465
459466 return {
460467 async sync ( ) {
461- await Promise . all ( promises ) ;
468+ await Promise . all ( tasks ) ;
462469 return version ;
463470 } ,
464471 } ;
465472 }
466473
467- function transformCompletionList ( completionList : CompletionList , document : TextDocument ) {
468- addDirectiveModifiers ( ) ;
474+ function postProcessCompletionList ( completionList : CompletionList , document : TextDocument ) {
475+ addDirectiveModifiers ( completionList , document ) ;
469476
470- function addDirectiveModifiers ( ) {
471- const replacement = getReplacement ( completionList , document ) ;
472- if ( ! replacement ?. text . includes ( '.' ) ) {
473- return ;
474- }
477+ const tagMap = new Map < string , html . CompletionItem > ( ) ;
475478
476- const [ text , ...modifiers ] = replacement . text . split ( '.' ) ;
477- const isVOn = text . startsWith ( 'v-on:' ) || text . startsWith ( '@' ) && text . length > 1 ;
478- const isVBind = text . startsWith ( 'v-bind:' ) || text . startsWith ( ':' ) && text . length > 1 ;
479- const isVModel = text . startsWith ( 'v-model:' ) || text === 'v-model' ;
480- const currentModifiers = isVOn
481- ? vOnModifiers
482- : isVBind
483- ? vBindModifiers
484- : isVModel
485- ? vModelModifiers
486- : undefined ;
487-
488- if ( ! currentModifiers ) {
489- return ;
479+ completionList . items = completionList . items . filter ( item => {
480+ const key = item . kind + '_' + item . label ;
481+ if ( ! tagMap . has ( key ) ) {
482+ tagMap . set ( key , item ) ;
483+ return true ;
490484 }
491-
492- for ( const modifier in currentModifiers ) {
493- if ( modifiers . includes ( modifier ) ) {
494- continue ;
495- }
496-
497- const description = currentModifiers [ modifier ] ;
498- const insertText = text + modifiers . slice ( 0 , - 1 ) . map ( m => '.' + m ) . join ( '' ) + '.' + modifier ;
499- const newItem : html . CompletionItem = {
500- label : modifier ,
501- filterText : insertText ,
502- documentation : {
503- kind : 'markdown' ,
504- value : description ,
505- } ,
506- textEdit : {
507- range : replacement . textEdit . range ,
508- newText : insertText ,
509- } ,
510- kind : 20 satisfies typeof CompletionItemKind . EnumMember ,
511- } ;
512-
513- completionList . items . push ( newItem ) ;
514- }
515- }
516-
517- completionList . items = completionList . items . filter ( item => ! specialTags . has ( parseLabel ( item . label ) . name ) ) ;
485+ tagMap . get ( key ) ! . documentation = item . documentation ;
486+ return false ;
487+ } ) ;
518488
519489 const htmlDocumentations = new Map < string , string > ( ) ;
520490
@@ -693,6 +663,53 @@ export function create(
693663 updateExtraCustomData ( [ ] ) ;
694664 }
695665
666+ function addDirectiveModifiers ( completionList : CompletionList , document : TextDocument ) {
667+ const replacement = getReplacement ( completionList , document ) ;
668+ if ( ! replacement ?. text . includes ( '.' ) ) {
669+ return ;
670+ }
671+
672+ const [ text , ...modifiers ] = replacement . text . split ( '.' ) ;
673+ const isVOn = text . startsWith ( 'v-on:' ) || text . startsWith ( '@' ) && text . length > 1 ;
674+ const isVBind = text . startsWith ( 'v-bind:' ) || text . startsWith ( ':' ) && text . length > 1 ;
675+ const isVModel = text . startsWith ( 'v-model:' ) || text === 'v-model' ;
676+ const currentModifiers = isVOn
677+ ? vOnModifiers
678+ : isVBind
679+ ? vBindModifiers
680+ : isVModel
681+ ? vModelModifiers
682+ : undefined ;
683+
684+ if ( ! currentModifiers ) {
685+ return ;
686+ }
687+
688+ for ( const modifier in currentModifiers ) {
689+ if ( modifiers . includes ( modifier ) ) {
690+ continue ;
691+ }
692+
693+ const description = currentModifiers [ modifier ] ;
694+ const insertText = text + modifiers . slice ( 0 , - 1 ) . map ( m => '.' + m ) . join ( '' ) + '.' + modifier ;
695+ const newItem : html . CompletionItem = {
696+ label : modifier ,
697+ filterText : insertText ,
698+ documentation : {
699+ kind : 'markdown' ,
700+ value : description ,
701+ } ,
702+ textEdit : {
703+ range : replacement . textEdit . range ,
704+ newText : insertText ,
705+ } ,
706+ kind : 20 satisfies typeof CompletionItemKind . EnumMember ,
707+ } ;
708+
709+ completionList . items . push ( newItem ) ;
710+ }
711+ }
712+
696713 async function initialize ( ) {
697714 customData = await getHtmlCustomData ( ) ;
698715 }
0 commit comments