import type { IFileSystem } from '@file-services/types' ; import path from 'path' ; import type * as postcss from 'postcss' ; import { getCSSLanguageService , HoverSettings , Stylesheet } from 'vscode-css-languageservice' ; import { TextDocument } from 'vscode-languageserver-textdocument' ; import { Color , ColorInformation , ColorPresentation , CompletionItem , Diagnostic , Hover , Location , Position , Range , } from 'vscode-languageserver-types' ; import { URI } from 'vscode-uri' ; import { createMeta } from './provider' ; function readDocRange ( doc , rng ) { const lines = doc . getText ( ) . split ( '\n' ) ; return lines [ rng . start . line ] . slice ( rng . start . character , rng . end . character ) ; } function findPseudoStateStart ( line , lookFrom ) { let i = lookFrom - 1 ; let res = - 1 ; let openParens = 0 ; while ( i !== - 1 ) { if ( line [ i ] === ':' && line [ i - 1 ] !== ':' ) { res = i ; } if ( line [ i ] === '(' ) { openParens ++ ; } if ( line [ i ] === ')' ) { openParens -- ; } i -- ; } return { index : res , openParens , } ; } /**
 * the API for "normal" css language features fallback
 */ export class CssService { private inner = getCSSLanguageService ( { fileSystemProvider : this . fs . promises as any } ) ; constructor ( private fs ) { } public getCompletions ( document , position ) { const cssCompsRaw = this . inner . doComplete ( document , position , this . inner . parseStylesheet ( document ) ) ; return cssCompsRaw ? cssCompsRaw . items : [ ] ; } public createSanitizedDocument ( ast , filePath , version ) { let cleanContentAst = this . cleanValuesInMediaQuery ( ast ) ; cleanContentAst = this . cleanStScopes ( cleanContentAst ) ; return TextDocument . create ( URI . file ( filePath ) . toString ( ) , 'stylable' , version , cleanContentAst . toString ( ) ) ; } private cleanValuesInMediaQuery ( ast ) { const mq = 'media' ; const valueMatch = 'value(' ; ast . walkAtRules ( mq , ( atRule ) => { while ( atRule . params . includes ( 'value(' ) ) { const currentValueIndex = atRule . params . indexOf ( valueMatch ) ; const closingParenthesisIndex = atRule . params . indexOf ( ')' , currentValueIndex ) ; atRule . params = atRule . params . replace ( valueMatch , ' ' . repeat ( valueMatch . length ) ) ; atRule . params = atRule . params . substr ( 0 , closingParenthesisIndex ) + ' ' + atRule . params . substr ( closingParenthesisIndex + 1 ) ; } } ) ; return ast ; } private cleanStScopes ( ast ) { const stScope = 'st-scope' ; const mq = 'media' ; ast . walkAtRules ( stScope , ( atRule ) => { atRule . name = mq + ' ' . repeat ( stScope . length - mq . length ) ; if ( atRule . params . includes ( '.' ) ) { atRule . params = atRule . params . replace ( '.' , ' ' ) ; } } ) ; return ast ; } public getDiagnostics ( document ) { if ( ! document . uri . endsWith ( '.css' ) ) { return [ ] ; } const stylesheet = this . inner . parseStylesheet ( document ) ; return this . inner . doValidation ( document , stylesheet ) . filter ( ( diag ) => { if ( diag . code === 'emptyRules' ) { return false ; } const atRuleName = readDocRange ( document , diag . range ) ; if ( diag . code === 'unknownAtRules' && ( atRuleName === '@custom-selector' || atRuleName === '@st-scope' || atRuleName === '@st-import' || atRuleName === '@st-global-custom-property' ) ) { return false ; } if ( diag . code === 'css-lcurlyexpected' && readDocRange ( document , Range . create ( Position . create ( diag . range . start . line , 0 ) , diag . range . end ) ) . startsWith ( '@custom-selector' ) ) { return false ; } if ( diag . code === 'css-rparentexpected' || diag . code === 'css-identifierexpected' ) { const line = readDocRange ( document , Range . create ( Position . create ( diag . range . start . line , 0 ) , diag . range . end ) ) ; const stateStart = findPseudoStateStart ( line , diag . range . start . character ) ; if ( stateStart . index !== - 1 && stateStart . openParens > 0 ) { return false ; } } if ( diag . code === 'unknownProperties' ) { const prop = diag . message . match ( /'(.*)'/ ) ! [ 1 ] ; const uri = URI . parse ( document . uri ) ; const filePath = uri . scheme === 'file' && ! uri . authority && uri . path . charCodeAt ( 2 ) !== 58 && path . isAbsolute ( uri . path ) ? uri . path : uri . fsPath ; const src = this . fs . readFileSync ( filePath , 'utf8' ) ; const meta = createMeta ( src , filePath ) . meta ; if ( meta && Object . keys ( meta . getAllSymbols ( ) ) . some ( ( ms ) => ms === prop ) ) { return false ; } } return true ; } ) . map ( ( diag ) => { diag . source = 'css' ; return diag ; } ) ; } public doHover ( document , position , settings ? ) { const stylesheet = this . inner . parseStylesheet ( document ) ; return this . inner . doHover ( document , position , stylesheet , settings ) ; } public findReferences ( document , position ) { const stylesheet = this . inner . parseStylesheet ( document ) ; return this . inner . findReferences ( document , position , stylesheet ) ; } public getColorPresentations ( document , color , range ) { const stylesheet = this . inner . parseStylesheet ( document ) ; return this . inner . getColorPresentations ( document , stylesheet , color , range ) ; } public findColors ( document ) { const stylesheet = this . inner . parseStylesheet ( document ) ; return this . inner . findDocumentColors ( document , stylesheet ) ; } public findColor ( document ) { const colors = this . findColors ( document ) ; return colors . length ? colors [ 0 ] . color : null ; } }