import type { IFileSystem , IFileSystemStats } from '@file-services/types' ; import { Stylable , safeParse } from '@stylable/core' ; import type { HoverSettings } from 'vscode-css-languageservice' ; import type { ColorPresentationParams } from 'vscode-languageserver-protocol' ; import { Range , TextDocument } from 'vscode-languageserver-textdocument' ; import { Color , ColorInformation , ColorPresentation , Command , CompletionItem , Diagnostic , Hover , Location , ParameterInformation , Position , SignatureHelp , TextEdit , WorkspaceEdit , } from 'vscode-languageserver-types' ; import { URI } from 'vscode-uri' ; import { ProviderPosition , ProviderRange } from './completion-providers' ; import { CssService } from './css-service' ; import { dedupeRefs } from './dedupe-refs' ; import { createDiagnosis } from './diagnosis' ; import { getColorPresentation , resolveDocumentColors } from './feature/color-provider' ; import { format , lspFormattingOptionsToJsBeautifyOptions } from './feature/formatting' ; import { Provider } from './provider' ; import { getRefs , getRenameRefs } from './provider' ; import { typescriptSupport } from './typescript-support' ; import type { ExtendedTsLanguageService } from './types' ; import type { CSSBeautifyOptions } from 'js-beautify' ; import type { FormattingOptions } from 'vscode-languageserver' ; export interface StylableLanguageServiceOptions { fs : IFileSystem ; stylable : Stylable ; } export class StylableLanguageService { public cssService : CssService ; protected fs : IFileSystem ; protected provider : Provider ; protected stylable : Stylable ; protected tsLanguageService : ExtendedTsLanguageService ; constructor ( { fs , stylable } ) { this . fs = fs ; this . stylable = stylable ; this . tsLanguageService = typescriptSupport ( this . fs ) ; this . provider = new Provider ( this . stylable , this . tsLanguageService ) ; this . cssService = new CssService ( this . fs ) ; } public getStylable ( ) { return this . stylable ; } public getFs ( ) { return this . fs ; } public onCompletion ( filePath , offset ) { const stylableFile = this . readStylableFile ( filePath ) ; if ( stylableFile && stylableFile . stat . isFile ( ) ) { const document = TextDocument . create ( URI . file ( filePath ) . toString ( ) , 'stylable' , stylableFile . stat . mtime . getTime ( ) , stylableFile . content ) ; const position = document . positionAt ( offset ) ; return this . getCompletions ( document , filePath , position ) ; } else { return [ ] ; } } public onDefinition ( filePath , offset ) { const stylableFile = this . readStylableFile ( filePath ) ; if ( stylableFile && stylableFile . stat . isFile ( ) ) { const doc = TextDocument . create ( URI . file ( filePath ) . toString ( ) , 'stylable' , stylableFile . stat . mtime . getTime ( ) , stylableFile . content ) ; const res = this . provider . getDefinitionLocation ( stylableFile . content , doc . positionAt ( offset ) , filePath , this . fs ) ; return res . map ( ( loc ) => Location . create ( URI . file ( loc . uri ) . toString ( ) , loc . range ) ) ; } return [ ] ; } public onHover ( filePath , offset , settings ? ) { const stylableFile = this . readStylableFile ( filePath ) ; if ( stylableFile && stylableFile . stat . isFile ( ) ) { const doc = TextDocument . create ( URI . file ( filePath ) . toString ( ) , 'stylable' , stylableFile . stat . mtime . getTime ( ) , stylableFile . content ) ; return this . cssService . doHover ( doc , doc . positionAt ( offset ) , settings ) ; } return null ; } public onReferences ( filePath , offset ) { const stylableFile = this . readStylableFile ( filePath ) ; if ( stylableFile && stylableFile . stat . isFile ( ) ) { const doc = TextDocument . create ( URI . file ( filePath ) . toString ( ) , 'stylable' , stylableFile . stat . mtime . getTime ( ) , stylableFile . content ) ; const position = doc . positionAt ( offset ) ; const refs = this . getRefs ( filePath , position ) ; if ( refs . length ) { return dedupeRefs ( refs ) ; } else { return dedupeRefs ( this . cssService . findReferences ( doc , position ) ) ; } } return [ ] ; } public onDocumentColor ( filePath ) { const stylableFile = this . readStylableFile ( filePath ) ; if ( stylableFile && stylableFile . stat . isFile ( ) ) { const doc = TextDocument . create ( URI . file ( filePath ) . toString ( ) , 'stylable' , stylableFile . stat . mtime . getTime ( ) , stylableFile . content ) ; return resolveDocumentColors ( this . stylable , this . cssService , doc , this . fs ) ; } return [ ] ; } public onColorPresentation ( filePath , offset , color ) { const stylableFile = this . readStylableFile ( filePath ) ; if ( stylableFile && stylableFile . stat . isFile ( ) ) { const doc = TextDocument . create ( URI . file ( filePath ) . toString ( ) , 'stylable' , stylableFile . stat . mtime . getTime ( ) , stylableFile . content ) ; const range = { start : doc . positionAt ( offset . start ) , end : doc . positionAt ( offset . end ) , } ; return getColorPresentation ( this . cssService , doc , { color , range , textDocument : doc } ) ; } return [ ] ; } public onRenameRequest ( filePath , offset , newName ) { const stylableFile = this . readStylableFile ( filePath ) ; const edit = { changes : { } } ; if ( stylableFile && stylableFile . stat . isFile ( ) ) { const doc = TextDocument . create ( URI . file ( filePath ) . toString ( ) , 'stylable' , stylableFile . stat . mtime . getTime ( ) , stylableFile . content ) ; getRenameRefs ( filePath , doc . positionAt ( offset ) , this . fs , this . stylable ) . forEach ( ( ref ) => { if ( edit . changes ! [ ref . uri ] ) { edit . changes ! [ ref . uri ] . push ( { range : ref . range , newText : newName } ) ; } else { edit . changes ! [ ref . uri ] = [ { range : ref . range , newText : newName } ] ; } } ) ; } return edit ; } public onSignatureHelp ( filePath , offset ) { const stylableFile = this . readStylableFile ( filePath ) ; if ( stylableFile && stylableFile . stat . isFile ( ) ) { const doc = TextDocument . create ( URI . file ( filePath ) . toString ( ) , 'stylable' , stylableFile . stat . mtime . getTime ( ) , stylableFile . content ) ; const sig = this . provider . getSignatureHelp ( stylableFile . content , doc . positionAt ( offset ) , filePath , this . fs , ParameterInformation ) ; return sig ; } return null ; } public onDocumentFormatting ( filePath , options ) { const srcText = this . fs . readFileSync ( filePath , 'utf8' ) ; return this . getDocumentFormatting ( TextDocument . create ( URI . file ( filePath ) . toString ( ) , 'stylable' , 1 , srcText ) , { start : 0 , end : srcText . length } , lspFormattingOptionsToJsBeautifyOptions ( options ) ) ; } public onDocumentRangeFormatting ( filePath , offset , options ) { const srcText = this . fs . readFileSync ( filePath , 'utf8' ) ; return this . getDocumentFormatting ( TextDocument . create ( URI . file ( filePath ) . toString ( ) , 'stylable' , 1 , srcText ) , offset , lspFormattingOptionsToJsBeautifyOptions ( options ) ) ; } public getDocumentFormatting ( doc , offset , options ) { return format ( doc , offset , options ) ; } public provideCompletionItemsFromSrc ( src , pos , fileName ) { return this . provider . provideCompletionItemsFromSrc ( src , pos , fileName , this . fs ) ; } public getCompletions ( document , filePath , position ) { const content = document . getText ( ) ; const stCompletions = this . provider . provideCompletionItemsFromSrc ( content , { line : position . line , character : position . character , } , filePath , this . fs ) ; const ast = safeParse ( content , { from : filePath } ) ; const cleanDocument = this . cssService . createSanitizedDocument ( ast , filePath , document . version ) ; const groupedCompletions = new Map < string , CompletionItem > ( ) ; for ( const stComp of stCompletions ) { const lspCompletion = CompletionItem . create ( stComp . label ) ; if ( ! groupedCompletions . has ( lspCompletion . label ) ) { groupedCompletions . set ( lspCompletion . label , lspCompletion ) ; } else { continue ; } const textEdit = TextEdit . replace ( stComp . range ? stComp . range : new ProviderRange ( new ProviderPosition ( position . line , Math . max ( position . character - 1 , 0 ) ) , position ) , typeof stComp . insertText === 'string' ? stComp . insertText : stComp . insertText . source ) ; lspCompletion . insertTextFormat = 2 ; lspCompletion . detail = stComp . detail ; lspCompletion . textEdit = textEdit ; lspCompletion . sortText = 'a' ; lspCompletion . filterText = typeof stComp . insertText === 'string' ? stComp . insertText : stComp . insertText . source ; if ( stComp . additionalCompletions ) { lspCompletion . command = Command . create ( 'additional' , 'editor.action.triggerSuggest' ) ; } else if ( stComp . triggerSignature ) { lspCompletion . command = Command . create ( 'additional' , 'editor.action.triggerParameterHints' ) ; } } const cssCompletions = this . cssService . getCompletions ( cleanDocument , position ) ; for ( const cssComp of cssCompletions ) { const label = cssComp . label ; if ( ! groupedCompletions . has ( label ) ) { cssComp . sortText = cssComp . sortText || 'z' ; groupedCompletions . set ( label , cssComp ) ; } } return [ ... groupedCompletions . values ( ) ] ; } public getDefinitionLocation ( src , position , filePath ) { const defs = this . provider . getDefinitionLocation ( src , position , URI . file ( filePath ) . fsPath , this . fs ) ; return defs . map ( ( loc ) => Location . create ( URI . file ( loc . uri ) . fsPath , loc . range ) ) ; } public getSignatureHelp ( src , pos , filePath , paramInfo ) { return this . provider . getSignatureHelp ( src , pos , filePath , this . fs , paramInfo ) ; } public getRefs ( filePath , position ) { return getRefs ( filePath , position , this . fs , this . stylable ) ; } public resolveDocumentColors ( document ) { return resolveDocumentColors ( this . stylable , this . cssService , document , this . fs ) ; } public getColorPresentation ( document , params ) { return getColorPresentation ( this . cssService , document , params ) ; } public diagnose ( filePath ) { const stylableFile = this . readStylableFile ( filePath ) ; if ( stylableFile && stylableFile . stat . isFile ( ) ) { return createDiagnosis ( stylableFile . content , stylableFile . stat . mtime . getTime ( ) , filePath , this . stylable , this . cssService ) ; } return [ ] ; } private readStylableFile ( filePath , includeUntitled = true ) { const supportedFile = filePath . endsWith ( '.st.css' ) || ( includeUntitled && filePath . startsWith ( 'untitled:' ) ) ; if ( ! supportedFile ) { return null ; } try { const stat = this . fs . statSync ( filePath ) ; if ( stat . isFile ( ) ) { const content = this . fs . readFileSync ( filePath , 'utf8' ) ; return { content , stat , } ; } } catch { } return null ; } } interface StylableFile { stat : IFileSystemStats ; content : string ; }