import path , { dirname } from 'path' ; import type * as postcss from 'postcss' ; import postcssValueParser from 'postcss-value-parser' ; import type ts from 'typescript' ; import { ClassSymbol , CSSResolve , CSSVarSymbol , ElementSymbol , evalDeclarationValue , MappedStates , nativePseudoClasses , nativePseudoElements , ResolvedElement , SRule , Stylable , StylableMeta , systemValidators , valueMapping , VarSymbol , } from '@stylable/core' ; import type { IFileSystem } from '@file-services/types' ; import { classCompletion , codeMixinCompletion , Completion , cssMixinCompletion , extendCompletion , globalCompletion , importDirectives , importInternalDirective , namedCompletion , pseudoElementCompletion , rulesetDirectives , rulesetInternalDirective , stateCompletion , stateEnumCompletion , stateTypeCompletion , topLevelDirective , topLevelDirectives , valueCompletion , valueDirective , } from './completion-types' ; import { resolveStateTypeOrValidator } from './feature/pseudo-class' ; import { extractJsModifierReturnType , extractTsSignature , getExistingNames , getNamedValues , isDirective , isInValue , } from './provider' ; import type { ExtendedTsLanguageService } from './types' ; import { getAtRuleByPosition , isComment , isDeclaration } from './utils/postcss-ast-utils' ; import type { CursorPosition , SelectorChunk } from './utils/selector-analyzer' ; const { hasOwnProperty } = Object . prototype ; export interface ProviderOptions { meta : StylableMeta ; fs : IFileSystem ; stylable : Stylable ; src : string ; tsLangService : ExtendedTsLanguageService ; resolvedElements : ResolvedElement [ ] [ ] ; resolvedRoot : ResolvedElement ; parentSelector : SRule | null ; astAtCursor : postcss . AnyNode ; lineChunkAtCursor : string ; lastSelectoid : string ; fullLineText : string ; position : ProviderPosition ; resolved : CSSResolve [ ] ; currentSelector : string ; target : CursorPosition ; isMediaQuery : boolean ; fakes : postcss . Rule [ ] ; } export interface CompletionProvider { provide ( options ) : Completion [ ] ; } export class ProviderPosition { constructor ( public line , public character ) { } } export class ProviderRange { constructor ( public start , public end ) { } } export class ProviderLocation { constructor ( public uri , public range ) { } } const cssPseudoClasses = [ 'active' , 'any' , 'checked' , 'default' , 'dir()' , 'disabled' , 'empty' , 'enabled' , 'first' , 'first-child' , 'first-of-type' , 'fullscreen' , 'focus' , 'hover' , 'indeterminate' , 'in-range' , 'invalid' , 'lang()' , 'last-child' , 'last-of-type' , 'left' , 'link' , 'not()' , 'nth-child()' , 'nth-last-child()' , 'nth-last-of-type()' , 'nth-of-type()' , 'only-child' , 'only-of-type' , 'optional' , 'out-of-range' , 'read-only' , 'read-write' , 'required' , 'right' , 'root' , 'scope' , 'target' , 'valid' , 'visited' , ] ; export function createRange ( startLine , startPos , endline , endPos ) { return new ProviderRange ( new ProviderPosition ( startLine , startPos ) , new ProviderPosition ( endline , endPos ) ) ; } function createDirectiveRange ( position , fullLineText , lineChunkAtCursor ) { return new ProviderRange ( new ProviderPosition ( position . line , Math . max ( 0 , position . character - ( topLevelDirectives . customSelector . startsWith ( fullLineText ) ? fullLineText . length : lineChunkAtCursor . length ) ) ) , position ) ; } const importDeclarations = [ 'default' , 'named' , 'from' ] ; const simpleRulesetDeclarations = [ 'extends' , 'states' , 'mixin' , ] ; const topLevelDeclarations = [ 'root' , 'namespace' , 'vars' , 'import' , 'customSelector' , 'stScope' , 'stImport' , 'stGlobalCustomProperty' , ] ; export const ImportInternalDirectivesProvider = { provide ( { parentSelector , isMediaQuery , fullLineText , position , lineChunkAtCursor , } ) { if ( parentSelector && parentSelector . selector === ':import' && ! isMediaQuery ) { const res = [ ] ; importDeclarations . forEach ( ( name ) => { if ( parentSelector . nodes . every ( ( n ) => ( isDeclaration ( n ) && importDirectives [ name ] !== n . prop ) || isComment ( n ) ) && importDirectives [ name ] . startsWith ( fullLineText . trim ( ) ) ) { res . push ( importInternalDirective ( name , createDirectiveRange ( position , fullLineText , lineChunkAtCursor ) ) ) ; } } ) ; return res ; } else { return [ ] ; } } , } ; export const RulesetInternalDirectivesProvider = { provide ( { parentSelector , isMediaQuery , fullLineText , position , lineChunkAtCursor , } ) { const res = [ ] ; if ( parentSelector && ! ( parentSelector . selector === ':import' || parentSelector . selector === ':vars' ) ) { if ( parentSelector . nodes . every ( ( n ) => ( isDeclaration ( n ) && rulesetDirectives . mixin !== n . prop ) || isComment ( n ) ) && rulesetDirectives . mixin . startsWith ( fullLineText . trim ( ) ) ) { res . push ( rulesetInternalDirective ( 'mixin' , createDirectiveRange ( position , fullLineText , lineChunkAtCursor ) ) ) ; } if ( this . isSimpleSelector ( parentSelector . selector ) && ! isMediaQuery ) { simpleRulesetDeclarations . filter ( ( d ) => d !== 'mixin' ) . forEach ( ( name ) => { if ( parentSelector . nodes . every ( ( n ) => ( isDeclaration ( n ) && rulesetDirectives [ name ] !== n . prop ) || isComment ( n ) ) && rulesetDirectives [ name ] . startsWith ( fullLineText . trim ( ) ) ) { res . push ( rulesetInternalDirective ( name , createDirectiveRange ( position , fullLineText , lineChunkAtCursor ) ) ) ; } } ) ; } return res ; } else { return [ ] ; } } , isSimpleSelector ( sel ) { return ! ! /^\s*\.?[\w-]*$/ . test ( sel ) ; } , } ; export const TopLevelDirectiveProvider = { provide ( { parentSelector , isMediaQuery , fullLineText , position , lineChunkAtCursor , meta , astAtCursor , } ) { if ( ! parentSelector ) { if ( ! isMediaQuery && ! ( astAtCursor . type === 'atrule' && astAtCursor . name === topLevelDirectives . stScope . slice ( 1 ) ) ) { return topLevelDeclarations . filter ( ( d ) => ! meta . ast . source ! . input . css . includes ( topLevelDirectives . namespace ) || d !== 'namespace' ) . filter ( ( d ) => topLevelDirectives [ d ] . startsWith ( fullLineText . trim ( ) ) ) . map ( ( d ) => topLevelDirective ( d , createDirectiveRange ( position , fullLineText , lineChunkAtCursor ) ) ) ; } else { return [ topLevelDirective ( 'root' , createDirectiveRange ( position , fullLineText , lineChunkAtCursor ) ) , ] ; } } else { return [ ] ; } } , } ; export const ValueDirectiveProvider = { provide ( { parentSelector , fullLineText , position } ) { if ( parentSelector && ! isDirective ( fullLineText ) && ! this . isInsideValueDirective ( fullLineText , position . character ) && fullLineText . includes ( ':' ) ) { const parsed = postcssValueParser ( fullLineText . slice ( fullLineText . indexOf ( ':' ) + 1 ) ) . nodes ; const node = parsed [ parsed . length - 1 ] ; if ( node && ( node . type === 'div' || node . type === 'space' || ( node . type === 'function' && ! node . unclosed ) || ( node . type === 'word' && 'value()' . startsWith ( node . value ) ) ) ) { return [ valueDirective ( new ProviderRange ( new ProviderPosition ( position . line , ~ fullLineText . indexOf ( ',' ) ? fullLineText . lastIndexOf ( ',' ) + 1 : fullLineText . indexOf ( ':' ) + 1 ) , position ) ) , ] ; } else { return [ ] ; } } else { return [ ] ; } } , isInsideValueDirective ( wholeLine , pos ) { if ( ! wholeLine . includes ( 'value(' ) ) { return false ; } const line = wholeLine . slice ( 0 , pos ) . slice ( wholeLine . lastIndexOf ( 'value(' ) ) ; let stack = 0 ; for ( let i = 0 ; i <= line . length ; i ++ ) { if ( line [ i ] === '(' ) { stack += 1 ; } else if ( line [ i ] === ')' ) { stack -= 1 ; } } return stack > 0 ; } , } ; export const GlobalCompletionProvider = { provide ( { parentSelector , fullLineText , position , lineChunkAtCursor , astAtCursor , } ) { if ( ! parentSelector && ! lineChunkAtCursor . endsWith ( '::' ) && ! isBetweenChars ( fullLineText , position , '(' , ')' ) ) { if ( astAtCursor . type === 'root' ) { if ( getAtRuleByPosition ( astAtCursor , position , 'st-import' ) ) { return [ ] ; } } let offset = 0 ; if ( fullLineText . lastIndexOf ( ':' ) !== - 1 ) { if ( ':global()' . startsWith ( lineChunkAtCursor . slice ( lineChunkAtCursor . lastIndexOf ( ':' ) ) ) ) { offset = lineChunkAtCursor . slice ( lineChunkAtCursor . lastIndexOf ( ':' ) ) . length ; } } return [ globalCompletion ( new ProviderRange ( new ProviderPosition ( position . line , position . character - offset ) , position ) ) , ] ; } else { return [ ] ; } } , } ; export const SelectorCompletionProvider = { provide ( { parentSelector , fullLineText , position , lineChunkAtCursor , meta , fakes , stylable , } ) { if ( ! parentSelector && ( lineChunkAtCursor === ':' || ! lineChunkAtCursor . endsWith ( ':' ) ) ) { const comps = [ ] ; comps . push ( ... Object . keys ( meta . getAllClasses ( ) ) . filter ( ( c ) => c !== 'root' && fakes . findIndex ( ( f ) => f . selector === '.' + c ) === - 1 ) . map ( ( c ) => classCompletion ( c , createDirectiveRange ( position , fullLineText , lineChunkAtCursor ) ) ) ) ; comps . push ( ... Object . keys ( meta . customSelectors ) . map ( ( c ) => classCompletion ( c , createDirectiveRange ( position , fullLineText , lineChunkAtCursor ) , true ) ) ) ; const moreComps = meta . getImportStatements ( ) . filter ( ( imp ) => imp . request . endsWith ( 'st.css' ) ) . reduce ( ( acc , imp ) => { if ( acc . every ( ( comp ) => comp . label !== imp . defaultExport ) ) { acc . push ( classCompletion ( imp . defaultExport , createDirectiveRange ( position , fullLineText , lineChunkAtCursor ) , true ) ) ; } Object . keys ( imp . named ) . forEach ( ( exp ) => { const res = stylable . resolver . resolve ( meta . getSymbol ( exp ) ) ; if ( res && res . _kind === 'css' && res . symbol && ( res . symbol . _kind === 'class' || res . symbol . _kind === 'element' ) && acc . every ( ( comp ) => comp . label . replace ( '.' , '' ) !== imp . named [ exp ] ) ) { acc . push ( classCompletion ( imp . named [ exp ] , createDirectiveRange ( position , fullLineText , lineChunkAtCursor ) ) ) ; } } ) ; return acc ; } , comps ) ; return moreComps . filter ( ( c ) => c . label . startsWith ( lineChunkAtCursor ) ) ; } else { return [ ] ; } } , } ; export const ExtendCompletionProvider = { provide ( { lineChunkAtCursor , position , meta , stylable } ) { if ( lineChunkAtCursor . startsWith ( valueMapping . extends ) ) { const value = lineChunkAtCursor . slice ( ( valueMapping . extends + ':' ) . length ) ; const spaces = value . search ( /\S|$/ ) ; const str = value . slice ( spaces ) ; const comps = [ [ ] ] ; comps . push ( ... Object . keys ( meta . getAllClasses ( ) ) . filter ( ( s ) => s . startsWith ( str ) ) . map ( ( s ) => [ s , 'Local file' ] ) ) ; meta . getImportStatements ( ) . forEach ( ( i ) => { if ( i . defaultExport && i . defaultExport . startsWith ( str ) && i . from . endsWith ( 'st.css' ) ) { comps . push ( [ i . defaultExport , i . request ] ) ; } } ) ; meta . getImportStatements ( ) . forEach ( ( i ) => comps . push ( ... Object . keys ( i . named ) . filter ( ( s ) => { const res = stylable . resolver . resolve ( meta . getSymbol ( s ) ) ; return ( res && res . _kind === 'css' && ( res . symbol . _kind === 'class' || res . symbol . _kind === 'element' ) ) ; } ) . filter ( ( s ) => s . startsWith ( str ) ) . map ( ( s ) => [ s , i . request ] ) ) ) ; return comps . slice ( 1 ) . map ( ( c ) => extendCompletion ( c [ 0 ] , c [ 1 ] , new ProviderRange ( new ProviderPosition ( position . line , position . character - str . length ) , position ) ) ) ; } else { return [ ] ; } } , } ; export const CssMixinCompletionProvider = { provide ( { lineChunkAtCursor , meta , position , fullLineText } ) { if ( lineChunkAtCursor . startsWith ( valueMapping . mixin + ':' ) ) { const { names , lastName } = getExistingNames ( fullLineText , position ) ; const symbols = meta . getAllSymbols ( ) ; return Object . keys ( symbols ) . filter ( ( ms ) => { const importSymbol = symbols [ ms ] ; return ( ( importSymbol . _kind === 'import' && importSymbol . import . request . endsWith ( '.st.css' ) ) || importSymbol . _kind === 'class' ) ; } ) . filter ( ( ms ) => ms . startsWith ( lastName ) ) . filter ( ( ms ) => ! names . includes ( ms ) ) . map ( ( ms ) => { const importSymbol = symbols [ ms ] ; return cssMixinCompletion ( ms , new ProviderRange ( new ProviderPosition ( position . line , position . character - lastName . length ) , position ) , importSymbol . _kind === 'import' ? importSymbol . import . request : 'Local file' ) ; } ) ; } else { return [ ] ; } } , } ; export const CodeMixinCompletionProvider = { provide ( { parentSelector , meta , fullLineText , lineChunkAtCursor , position , fs , tsLangService , stylable , } ) { if ( meta . getImportStatements ( ) . some ( ( imp ) => imp . request . endsWith ( '.ts' ) || imp . request . endsWith ( '.js' ) ) && ! fullLineText . trim ( ) . startsWith ( valueMapping . from ) && parentSelector && lineChunkAtCursor . startsWith ( valueMapping . mixin + ':' ) ) { if ( fullLineText . lastIndexOf ( '(' ) > fullLineText . lastIndexOf ( ')' ) ) { return [ ] ; } const { lastName } = getExistingNames ( fullLineText , position ) ; const symbols = meta . getAllSymbols ( ) ; return Object . keys ( symbols ) . filter ( ( ms ) => symbols [ ms ] . _kind === 'import' ) . filter ( ( ms ) => ms . startsWith ( lastName ) ) . filter ( ( ms ) => { const res = stylable . resolver . resolve ( symbols [ ms ] ) ; return res && res . _kind === 'js' ; } ) . filter ( ( ms ) => isMixin ( ms , meta , fs , tsLangService ) ) . map ( ( ms ) => createCodeMixinCompletion ( ms , lastName , position , meta ) ) ; } else { return [ ] ; } } , } ; export const FormatterCompletionProvider = { provide ( { meta , fullLineText , parentSelector , lineChunkAtCursor , position , fs , tsLangService , stylable , } ) { if ( meta . getImportStatements ( ) . some ( ( imp ) => imp . request . endsWith ( '.ts' ) || imp . request . endsWith ( '.js' ) ) && ! fullLineText . trim ( ) . startsWith ( valueMapping . from ) && ! fullLineText . trim ( ) . startsWith ( valueMapping . extends ) && ! fullLineText . trim ( ) . startsWith ( valueMapping . named ) && parentSelector && ~ fullLineText . indexOf ( ':' ) && fullLineText . indexOf ( ':' ) < position . character && ! lineChunkAtCursor . startsWith ( valueMapping . mixin + ':' ) ) { const { lastName } = getExistingNames ( fullLineText , position ) ; const symbols = meta . getAllSymbols ( ) ; return ( Object . keys ( symbols ) . filter ( ( ms ) => symbols [ ms ] . _kind === 'import' ) . filter ( ( ms ) => ms . startsWith ( lastName ) ) . filter ( ( ms ) => { const res = stylable . resolver . resolve ( symbols [ ms ] ) ; return res && res . _kind === 'js' ; } ) . filter ( ( ms ) => ! isMixin ( ms , meta , fs , tsLangService ) ) . map ( ( ms ) => createCodeMixinCompletion ( ms , lastName , position , meta ) ) ) ; } else { return [ ] ; } } , } ; export const NamedCompletionProvider = { provide ( { parentSelector , astAtCursor , stylable , meta , position , fullLineText , src , } ) { const { isNamedValueLine , namedValues } = getNamedValues ( src , position . line ) ; if ( isNamedValueLine ) { let importName = '' ; if ( parentSelector && parentSelector . selector === ':import' && ( astAtCursor as postcss . Rule ) . nodes && ( astAtCursor as postcss . Rule ) . nodes . length ) { importName = ( ( astAtCursor as postcss . Rule ) . nodes . find ( ( n ) => ( n as postcss . Declaration ) . prop === valueMapping . from ) as postcss . Declaration ) . value . replace ( /'|"/g , '' ) ; } else if ( astAtCursor . type === 'atrule' && astAtCursor . name === topLevelDirectives . stImport . slice ( 1 ) ) { const parsed = postcssValueParser ( astAtCursor . params ) ; parsed . walk ( ( n ) => { if ( n . type === 'string' ) { importName = n . value ; } } ) ; } else { return [ ] ; } const comps = [ [ ] ] ; if ( importName . endsWith ( '.st.css' ) ) { const resolvedImport = this . resolveImport ( importName , stylable , meta ) ; if ( resolvedImport ) { const { lastName } = getExistingNames ( fullLineText , position ) ; getNamedCSSImports ( stylable , comps , resolvedImport , lastName , namedValues , meta ) ; return comps . slice ( 1 ) . map ( ( c ) => namedCompletion ( c [ 0 ] , new ProviderRange ( new ProviderPosition ( position . line , position . character - lastName . length ) , new ProviderPosition ( position . line , position . character ) ) , c [ 1 ] , c [ 2 ] ) ) ; } } else if ( importName . endsWith ( '.js' ) ) { let req ; try { req = ( stylable as any ) . requireModule ( path . join ( path . dirname ( meta . source ) , importName ) ) ; } catch ( e ) { return [ ] ; } const { lastName } = getExistingNames ( fullLineText , position ) ; Object . keys ( req ) . forEach ( ( k ) => { if ( typeof req [ k ] === 'function' && k . startsWith ( lastName ) ) { comps . push ( [ k , importName , 'Mixin' ] ) ; } } ) ; return comps . slice ( 1 ) . map ( ( c ) => namedCompletion ( c [ 0 ] , new ProviderRange ( new ProviderPosition ( position . line , position . character - lastName . length ) , new ProviderPosition ( position . line , position . character ) ) , c [ 1 ] , c [ 2 ] ) ) ; } } return [ ] ; } , resolveImport ( importName , stylable , meta ) { return maybeResolveImport ( importName , stylable , meta ) ; } , } ; function maybeResolveImport ( importName , stylable , meta ) { let resolvedImport = null ; if ( importName && importName . endsWith ( '.st.css' ) ) { try { const imported = meta . getImportStatements ( ) . find ( ( i ) => i . request === importName ) ! ; resolvedImport = stylable . fileProcessor . process ( stylable . resolvePath ( imported . context , imported . request ) ) ; } catch { } } return resolvedImport ; } export const StImportNamedCompletionProvider = { provide ( { astAtCursor , stylable , meta , position , fullLineText , src , } ) { const { isNamedValueLine , namedValues } = getNamedValues ( src , position . line ) ; let importName = '' ; if ( astAtCursor . type === 'root' ) { const atRule = getAtRuleByPosition ( astAtCursor , position , 'st-import' ) ; const comps = [ [ ] ] ; if ( isNamedValueLine && atRule ) { const parsed = postcssValueParser ( atRule . params ) ; parsed . walk ( ( n ) => { if ( n . type === 'string' ) { importName = n . value ; } } ) ; if ( importName . endsWith ( '.st.css' ) ) { const resolvedImport = this . resolveImport ( importName , stylable , meta ) ; if ( resolvedImport ) { const { lastName } = getExistingNames ( fullLineText , position ) ; getNamedCSSImports ( stylable , comps , resolvedImport , lastName , namedValues , meta ) ; return comps . slice ( 1 ) . map ( ( c ) => namedCompletion ( c [ 0 ] , new ProviderRange ( new ProviderPosition ( position . line , position . character - lastName . length ) , new ProviderPosition ( position . line , position . character ) ) , c [ 1 ] , c [ 2 ] ) ) ; } } else if ( importName . endsWith ( '.js' ) ) { let req ; try { req = ( stylable as any ) . requireModule ( path . join ( path . dirname ( meta . source ) , importName ) ) ; } catch ( e ) { return [ ] ; } const { lastName } = getExistingNames ( fullLineText , position ) ; Object . keys ( req ) . forEach ( ( k ) => { if ( typeof req [ k ] === 'function' && k . startsWith ( lastName ) ) { comps . push ( [ k , importName , 'Mixin' ] ) ; } } ) ; return comps . slice ( 1 ) . map ( ( c ) => namedCompletion ( c [ 0 ] , new ProviderRange ( new ProviderPosition ( position . line , position . character - lastName . length ) , new ProviderPosition ( position . line , position . character ) ) , c [ 1 ] , c [ 2 ] ) ) ; } } } return [ ] ; } , resolveImport ( importName , stylable , meta ) { return maybeResolveImport ( importName , stylable , meta ) ; } , } ; export const PseudoElementCompletionProvider = { provide ( { parentSelector , resolved , resolvedElements , resolvedRoot , lastSelectoid , lineChunkAtCursor , meta , position , fullLineText , } ) { let comps = [ ] ; if ( ! parentSelector && resolved . length > 0 && ! isBetweenChars ( fullLineText , position , '(' , ')' ) ) { const [ firstResolvedElement ] = resolvedElements ; const secondLastNode = firstResolvedElement [ firstResolvedElement . length - 2 ] ; let lastNode = firstResolvedElement [ firstResolvedElement . length - 1 ] || resolvedRoot ; if ( secondLastNode && lastNode . type === 'pseudo-element' && nativePseudoElements . includes ( lastNode . name ) ) { lastNode = secondLastNode ; } const states = lastNode . resolved . reduce ( ( acc , cur ) => { if ( cur . symbol . _kind === 'class' ) { const symbolStates = cur . symbol [ valueMapping . states ] ; if ( symbolStates ) { acc = acc . concat ( Object . keys ( symbolStates ) ) ; } } return acc ; } , cssPseudoClasses ) ; let filter = lastNode . resolved . length ? states . includes ( lastSelectoid . replace ( ':' , '' ) ) ? '' : lastSelectoid . replace ( ':' , '' ) : lastNode . name ; const scope = filter ? secondLastNode ?. type === 'pseudo-element' && nativePseudoElements . includes ( secondLastNode . name ) ? firstResolvedElement [ firstResolvedElement . length - 3 ] : secondLastNode : lastNode ; const colons = lineChunkAtCursor . match ( /:*$/ ) ! [ 0 ] . length ; scope ?. resolved . forEach ( ( res ) => { if ( ! ( res . symbol as ClassSymbol ) [ valueMapping . root ] ) { return ; } comps = comps . concat ( Object . keys ( res . meta . getAllClasses ( ) ) . concat ( Object . keys ( res . meta . customSelectors ) . map ( ( s ) => s . slice ( ':--' . length ) ) ) . filter ( ( e ) => e . startsWith ( filter ) && e !== 'root' ) . map ( ( c ) => { let relPath = path . relative ( path . dirname ( meta . source ) , res . meta . source ) ; if ( ! relPath . startsWith ( '.' ) ) { relPath = './' + relPath ; } return pseudoElementCompletion ( c , relPath , new ProviderRange ( new ProviderPosition ( position . line , position . character - ( filter ? filter . length + 2 : colons ) ) , new ProviderPosition ( position . line , position . character ) ) ) ; } ) ) ; } ) ; let otherScope ; const chunksSplitByPseudo = lineChunkAtCursor . split ( '::' ) ; if ( ! filter && chunksSplitByPseudo . length > 1 && chunksSplitByPseudo [ chunksSplitByPseudo . length - 1 ] === scope . name ) { otherScope = resolvedElements [ 0 ] [ resolvedElements [ 0 ] . length - 2 ] ; filter = scope . name ; } if ( otherScope ) { otherScope . resolved . forEach ( ( res ) => { if ( ! ( res . symbol as ClassSymbol ) [ valueMapping . root ] ) { return ; } comps = comps . concat ( Object . keys ( res . meta . getAllClasses ( ) ) . concat ( Object . keys ( res . meta . customSelectors ) . map ( ( s ) => s . slice ( ':--' . length ) ) ) . filter ( ( e ) => e . startsWith ( filter ) && e !== 'root' ) . map ( ( c ) => { let relPath = path . relative ( path . dirname ( meta . source ) , res . meta . source ) ; if ( ! relPath . startsWith ( '.' ) ) { relPath = './' + relPath ; } return pseudoElementCompletion ( c , relPath , new ProviderRange ( new ProviderPosition ( position . line , position . character - ( filter ? filter . length + 2 : colons ) ) , new ProviderPosition ( position . line , position . character ) ) ) ; } ) ) ; } ) ; } } return comps ; } , } ; function getNamedCSSImports ( stylable , comps , resolvedImport , lastName , namedValues , meta ) { const namedSet = new Set ( namedValues ) ; for ( const [ symbolName , symbol ] of Object . entries ( resolvedImport . getAllSymbols ( ) ) ) { if ( symbol . _kind === 'import' ) { if ( symbol . name . startsWith ( '--' ) ) { const importedVar = stylable . createResolver ( { } ) . deepResolve ( symbol ) ; if ( importedVar && importedVar . _kind === 'css' && importedVar . symbol && importedVar . symbol . _kind === 'cssVar' ) { addCompletion ( symbolName , importedVar . meta , importedVar . symbol ) ; } } } else { addCompletion ( symbolName , meta , symbol ) ; } } function addCompletion ( symbolName , meta , symbol ) { if ( symbolName . slice ( 0 , - 1 ) . startsWith ( lastName ) && ! namedSet . has ( symbolName ) ) { comps . push ( [ symbolName , path . relative ( meta . source , resolvedImport . source ) . slice ( 1 ) . replace ( /\\/g , '/' ) , createCompletionDetail ( symbol ) , ] ) ; } } } function createCompletionDetail ( symbol ) { switch ( symbol . _kind ) { case 'class' : return 'Stylable class' ; case 'cssVar' : return `${ symbol . global ? 'Global ' : '' }${ symbol . name }` ; case 'var' : return symbol . text ; case 'element' : return 'Stylable element' ; } } function isNodeRule ( node ) { return node . type === 'rule' ; } function isPositionInDecl ( position , decl ) { const srcStart = decl . source && decl . source . start ; const srcEnd = decl . source && decl . source . end ; if ( srcStart && srcEnd ) { const srcStartLine = srcStart . line - 1 ; const srcEndLine = srcEnd . line - 1 ; const srcStartChar = srcStart . column - 1 ; const srcEndChar = srcEnd . column - 1 ; if ( srcStartLine < position . line && srcEndLine > position . line ) { return true ; } else if ( srcStartLine === position . line && srcEndLine === position . line ) { if ( srcStartChar <= position . character && srcEndChar >= position . character ) { return true ; } } } return false ; } export const StateTypeCompletionProvider = { provide ( { astAtCursor , fullLineText , position } ) { const acc = [ ] ; if ( isNodeRule ( astAtCursor ) ) { const declNodes = astAtCursor . nodes ; if ( declNodes ) { const stateDeclInPos = declNodes . find ( ( decl ) => { if ( decl . type === 'decl' && decl . prop === valueMapping . states && isPositionInDecl ( position , decl ) ) { return true ; } return false ; } ) ; if ( stateDeclInPos ) { const toSuggest = resolveStateTypeOrValidator ( position , fullLineText ) ; const types = Object . keys ( systemValidators ) ; const input = getStateDefinitionInput ( fullLineText , position ) ; if ( typeof toSuggest === 'string' ) { const validators = systemValidators [ toSuggest ] . subValidators ; if ( validators ) { const validatorNames = Object . keys ( validators ) ; let relevantValidators = validatorNames . filter ( ( t ) => t . startsWith ( input ) ) ; relevantValidators = relevantValidators . length ? relevantValidators : validatorNames ; relevantValidators . forEach ( ( validator ) => { acc . push ( stateTypeCompletion ( validator , `Stylable pseudo-class ${ toSuggest } validators` , new ProviderRange ( new ProviderPosition ( position . line , position . character - input . length ) , position ) ) ) ; } ) ; } } else if ( typeof toSuggest === 'boolean' && toSuggest ) { let relevantTypes = types . filter ( ( t ) => t . startsWith ( input ) ) ; relevantTypes = relevantTypes . length ? relevantTypes : types ; relevantTypes . forEach ( ( type ) => { acc . push ( stateTypeCompletion ( type , 'Stylable pseudo-class types' , new ProviderRange ( new ProviderPosition ( position . line , position . character - input . length ) , position ) ) ) ; } ) ; } } } } return acc ; } , } ; export const StateSelectorCompletionProvider = { provide ( { parentSelector , lineChunkAtCursor , resolvedElements , resolvedRoot , target , lastSelectoid , meta , position , fullLineText , } ) { if ( ! parentSelector && ! lineChunkAtCursor . endsWith ( '::' ) && ! isBetweenChars ( fullLineText , position , '(' , ')' ) ) { let lastNode = resolvedElements [ 0 ] [ resolvedElements [ 0 ] . length - 1 ] || resolvedRoot ; if ( lastNode ?. type === 'pseudo-element' && nativePseudoElements . includes ( lastNode . name ) ) { lastNode = resolvedElements [ 0 ] [ resolvedElements [ 0 ] . length - 2 ] ; } const chunk = Array . isArray ( target . focusChunk ) ? target . focusChunk [ target . focusChunk . length - 1 ] : target . focusChunk ; const chunkyStates = chunk && ( chunk as SelectorChunk ) . states ? ( chunk as SelectorChunk ) . states : [ ] ; const allStates = collectStates ( lastNode ) ; const newStates = lastNode . resolved . reduce ( ( acc , cur ) => { let relPath = path . relative ( path . dirname ( meta . source ) , cur . meta . source ) ; if ( ! relPath . startsWith ( '.' ) ) { relPath = './' + relPath ; } const symbol = cur . symbol ; if ( symbol . _kind === 'class' ) { const symbolStates = symbol [ valueMapping . states ] ; if ( symbolStates ) { Object . keys ( symbolStates ) . forEach ( ( k ) => { if ( ! acc [ k ] && ( k . slice ( 0 , - 1 ) . startsWith ( lastSelectoid . replace ( ':' , '' ) ) || nativePseudoClasses . includes ( lastSelectoid . replace ( ':' , '' ) ) || hasOwnProperty . call ( allStates , lastSelectoid . replace ( ':' , '' ) ) ) && chunkyStates . every ( ( cs ) => cs !== k ) ) { const symbolStates = symbol [ valueMapping . states ] ; const stateDef = symbolStates && symbolStates [ k ] ; const stateType = stateDef && typeof stateDef === 'object' ? stateDef . type : null ; acc [ k ] = { path : meta . source === cur . meta . source ? 'Local file' : relPath , hasParam : ! ! stateDef , type : stateType , } ; } } ) ; } } return acc ; } , { } as { [ k ] : { path : string ; hasParam : boolean ; type : string | null ; } ; } ) ; const states = Object . keys ( newStates ) . map ( ( k ) => { return { name : k , state : newStates [ k ] } ; } ) ; if ( states . length === 0 ) { return [ ] ; } const lastState = lastSelectoid . replace ( ':' , '' ) ; const realState = hasOwnProperty . call ( allStates , lastState ) || nativePseudoClasses . includes ( lastState ) ; return states . reduce ( ( acc , st ) => { acc . push ( stateCompletion ( st . name , st . state . path , new ProviderRange ( new ProviderPosition ( position . line , lastState ? realState ? position . character - ( lineChunkAtCursor . endsWith ( ':' ) ? 1 : 0 ) : position . character - ( lastState . length + 1 ) - ( lineChunkAtCursor . endsWith ( ':' ) ? 1 : 0 ) : position . character - ( lineChunkAtCursor . endsWith ( ':' ) ? 1 : 0 ) ) , position ) , st . state . type , st . state . hasParam ) ) ; return acc ; } , [ ] ) ; } else { return [ ] ; } } , } ; export const StateEnumCompletionProvider = { provide ( { meta , astAtCursor , fullLineText , lineChunkAtCursor , position , lastSelectoid , resolvedElements , resolvedRoot , } ) { let acc = [ ] ; const ast = astAtCursor ; if ( ! lineChunkAtCursor . endsWith ( '::' ) && ( ast . type === 'root' || ast . type === 'atrule' ) ) { if ( lastSelectoid . startsWith ( ':' ) ) { const stateName = lastSelectoid . slice ( 1 ) ; const lastNode = resolvedElements [ 0 ] [ resolvedElements [ 0 ] . length - 1 ] || resolvedRoot ; const resolvedStates = collectStates ( lastNode ) ; if ( Object . keys ( resolvedStates ) . length ) { const resolvedStateNode = lastNode . resolved . find ( ( node ) => { const states = node . symbol [ valueMapping . states ] ; return states && states [ stateName ] && states [ stateName ] . type === 'enum' ; } ) ; if ( resolvedStateNode ) { const resolvedState = resolvedStateNode . symbol [ valueMapping . states ] ! [ stateName ] ; let existingInput = fullLineText . slice ( 0 , position . character ) ; existingInput = existingInput . slice ( existingInput . lastIndexOf ( '(' ) + 1 ) ; if ( typeof resolvedState !== 'string' && resolvedState ?. arguments . every ( ( opt ) => typeof opt === 'string' ) ) { const options = resolvedState . arguments as string [ ] ; let filteredOptions = options . filter ( ( opt ) => opt . startsWith ( existingInput ) ) ; filteredOptions = filteredOptions . length ? filteredOptions : options ; let from = 'Local file' ; if ( meta . source !== resolvedStateNode . meta . source ) { from = path . relative ( path . dirname ( meta . source ) , resolvedStateNode . meta . source ) ; if ( ! from . startsWith ( '.' ) ) { from = './' + from ; } } acc = filteredOptions . map ( ( opt ) => stateEnumCompletion ( opt , from , new ProviderRange ( new ProviderPosition ( position . line , position . character - existingInput . length ) , position ) ) ) ; } } } } } return acc ; } , } ; export const ValueCompletionProvider = { provide ( { fullLineText , position , meta , stylable } ) { if ( isInValue ( fullLineText , position ) ) { const inner = fullLineText . slice ( 0 , fullLineText . indexOf ( ')' , position . character ) + 1 ) . slice ( fullLineText . slice ( 0 , fullLineText . indexOf ( ')' , position . character ) + 1 ) . lastIndexOf ( '(' ) ) . replace ( '(' , '' ) . replace ( ')' , '' ) . trim ( ) ; const comps = [ ] ; meta . vars . forEach ( ( v ) => { if ( v . name . startsWith ( inner ) ) { const value = evalDeclarationValue ( stylable . resolver , v . text , meta , v . node ) ; comps . push ( valueCompletion ( v . name , 'Local variable' , value , new ProviderRange ( new ProviderPosition ( position . line , position . character - inner . length ) , position ) ) ) ; } } ) ; const importVars = [ ] ; meta . getImportStatements ( ) . forEach ( ( imp ) => { try { const resolvedPath = stylable . resolvePath ( dirname ( meta . source ) , imp . request ) ; stylable . fileProcessor . process ( resolvedPath ) . vars . forEach ( ( v ) => importVars . push ( { name : v . name , value : v . text , from : imp . request , node : v . node , } ) ) ; } catch ( e ) { } } ) ; importVars . forEach ( ( v ) => { if ( v . name . startsWith ( inner ) && meta . getImportStatements ( ) . some ( ( imp ) => Object . keys ( imp . named ) . some ( ( key ) => key === v . name ) ) ) { const value = evalDeclarationValue ( stylable . resolver , v . value , meta , v . node ) ; comps . push ( valueCompletion ( v . name , v . from , value , new ProviderRange ( new ProviderPosition ( position . line , position . character - inner . length ) , position ) ) ) ; } } ) ; return comps ; } else { return [ ] ; } } , } ; function collectStates ( lastNode ) { return lastNode . resolved . reduce < MappedStates > ( ( acc , cur ) => { const symbol = cur . symbol ; if ( symbol . _kind === 'class' ) { const symbolStates = symbol [ valueMapping . states ] ; if ( symbolStates ) { Object . keys ( symbolStates ) . forEach ( ( k ) => { const symbolStates = symbol [ valueMapping . states ] ; if ( symbolStates && symbolStates [ k ] !== undefined ) { acc [ k ] = symbolStates [ k ] ; } } ) ; } } return acc ; } , { } ) ; } function isBetweenChars ( text , position , char1 , char2 ) { const posChar = position . character ; const textToPosition = text . slice ( 0 , posChar ) ; const textfromPosition = text . slice ( posChar ) ; const openingParenthesisIndex = textToPosition . lastIndexOf ( char1 ) ; const closingParenthesisIndex = textfromPosition . indexOf ( char2 ) + posChar ; return posChar > openingParenthesisIndex && posChar <= closingParenthesisIndex ; } function getStateDefinitionInput ( fullLineText , position ) { let input = fullLineText . slice ( 0 , position . character ) ; const lastParenthesis = input . lastIndexOf ( '(' ) ; input = input . slice ( lastParenthesis + 1 ) ; return input ; } function createCodeMixinCompletion ( name , lastName , position , meta ) { const importSymbol = meta . getSymbol ( name ) ; if ( ! importSymbol || importSymbol . _kind !== 'import' ) { throw new Error ( 'expected import symbol' ) ; } return codeMixinCompletion ( name , new ProviderRange ( new ProviderPosition ( position . line , position . character - lastName . length ) , position ) , importSymbol . import . request ) ; } function isMixin ( name , meta , fs , tsLangService ) { const importSymbol = meta . getSymbol ( name ) ! ; if ( importSymbol . _kind === 'import' ) { if ( importSymbol . import . request . endsWith ( '.ts' ) ) { const sig = extractTsSignature ( importSymbol . import . from , name , importSymbol . type === 'default' , tsLangService ) ; if ( ! sig || ! sig . declaration ) { return false ; } const rtype = sig . declaration . type ? ( sig . declaration . type as ts . TypeReferenceNode ) . getText ( ) : '' ; return /(\w+.)?object/ . test ( rtype . trim ( ) ) ; } if ( importSymbol . import . request . endsWith ( '.js' ) ) { return ( extractJsModifierReturnType ( name , fs . readFileSync ( importSymbol . import . from , 'utf8' ) ) === 'object' ) ; } } return false ; }