import * as path from 'path' ; import * as fs from 'fs' ; import { URI } from 'vscode-uri' ; import { TextDocument , CompletionList , CompletionItemKind , CompletionItem , TextEdit , Range , Position } from 'vscode-languageserver-types' ; import { WorkspaceFolder } from 'vscode-languageserver' ; import { ICompletionParticipant } from 'vscode-css-languageservice' ; import { startsWith , endsWith } from './utils/strings' ; export function getPathCompletionParticipant ( document , workspaceFolders , result ) { return { onCssURILiteralValue : ( { position , range , uriValue } ) => { const fullValue = stripQuotes ( uriValue ) ; if ( ! shouldDoPathCompletion ( uriValue , workspaceFolders ) ) { if ( fullValue === '.' || fullValue === '..' ) { result . isIncomplete = true ; } return ; } let suggestions = providePathSuggestions ( uriValue , position , range , document , workspaceFolders ) ; result . items = [ ... suggestions , ... result . items ] ; } , onCssImportPath : ( { position , range , pathValue } ) => { const fullValue = stripQuotes ( pathValue ) ; if ( ! shouldDoPathCompletion ( pathValue , workspaceFolders ) ) { if ( fullValue === '.' || fullValue === '..' ) { result . isIncomplete = true ; } return ; } let suggestions = providePathSuggestions ( pathValue , position , range , document , workspaceFolders ) ; if ( document . languageId === 'scss' ) { suggestions . forEach ( s => { if ( startsWith ( s . label , '_' ) && endsWith ( s . label , '.scss' ) ) { if ( s . textEdit ) { s . textEdit . newText = s . label . slice ( 1 , - 5 ) ; } else { s . label = s . label . slice ( 1 , - 5 ) ; } } } ) ; } result . items = [ ... suggestions , ... result . items ] ; } } ; } function providePathSuggestions ( pathValue , position , range , document , workspaceFolders ) { const fullValue = stripQuotes ( pathValue ) ; const isValueQuoted = startsWith ( pathValue , `'` ) || startsWith ( pathValue , `"` ) ; const valueBeforeCursor = isValueQuoted ? fullValue . slice ( 0 , position . character - ( range . start . character + 1 ) ) : fullValue . slice ( 0 , position . character - range . start . character ) ; const workspaceRoot = resolveWorkspaceRoot ( document , workspaceFolders ) ; const currentDocFsPath = URI . parse ( document . uri ) . fsPath ; const paths = providePaths ( valueBeforeCursor , currentDocFsPath , workspaceRoot ) . filter ( p => { return path . resolve ( currentDocFsPath , '../' , p ) !== currentDocFsPath ; } ) . filter ( p => { return p [ 0 ] !== '.' ; } ) ; const fullValueRange = isValueQuoted ? shiftRange ( range , 1 , - 1 ) : range ; const replaceRange = pathToReplaceRange ( valueBeforeCursor , fullValue , fullValueRange ) ; const suggestions = paths . map ( p => pathToSuggestion ( p , replaceRange ) ) ; return suggestions ; } function shouldDoPathCompletion ( pathValue , workspaceFolders ) { const fullValue = stripQuotes ( pathValue ) ; if ( fullValue === '.' || fullValue === '..' ) { return false ; } if ( ! workspaceFolders || workspaceFolders . length === 0 ) { return false ; } return true ; } function stripQuotes ( fullValue ) { if ( startsWith ( fullValue , `'` ) || startsWith ( fullValue , `"` ) ) { return fullValue . slice ( 1 , - 1 ) ; } else { return fullValue ; } } /**
 * Get a list of path suggestions. Folder suggestions are suffixed with a slash.
 */ function providePaths ( valueBeforeCursor , activeDocFsPath , root ? ) { const lastIndexOfSlash = valueBeforeCursor . lastIndexOf ( '/' ) ; const valueBeforeLastSlash = valueBeforeCursor . slice ( 0 , lastIndexOfSlash + 1 ) ; const startsWithSlash = startsWith ( valueBeforeCursor , '/' ) ; let parentDir ; if ( startsWithSlash ) { if ( ! root ) { return [ ] ; } parentDir = path . resolve ( root , '.' + valueBeforeLastSlash ) ; } else { parentDir = path . resolve ( activeDocFsPath , '..' , valueBeforeLastSlash ) ; } try { return fs . readdirSync ( parentDir ) . map ( f => { return isDir ( path . resolve ( parentDir , f ) ) ? f + '/' : f ; } ) ; } catch ( e ) { return [ ] ; } } const isDir = ( p ) => { try { return fs . statSync ( p ) . isDirectory ( ) ; } catch ( e ) { return false ; } } ; function pathToReplaceRange ( valueBeforeCursor , fullValue , fullValueRange ) { let replaceRange ; const lastIndexOfSlash = valueBeforeCursor . lastIndexOf ( '/' ) ; if ( lastIndexOfSlash === - 1 ) { replaceRange = fullValueRange ; } else { const valueAfterLastSlash = fullValue . slice ( lastIndexOfSlash + 1 ) ; const startPos = shiftPosition ( fullValueRange . end , - valueAfterLastSlash . length ) ; const whitespaceIndex = valueAfterLastSlash . indexOf ( ' ' ) ; let endPos ; if ( whitespaceIndex !== - 1 ) { endPos = shiftPosition ( startPos , whitespaceIndex ) ; } else { endPos = fullValueRange . end ; } replaceRange = Range . create ( startPos , endPos ) ; } return replaceRange ; } function pathToSuggestion ( p , replaceRange ) { const isDir = p [ p . length - 1 ] === '/' ; if ( isDir ) { return { label : escapePath ( p ) , kind : CompletionItemKind . Folder , textEdit : TextEdit . replace ( replaceRange , escapePath ( p ) ) , command : { title : 'Suggest' , command : 'editor.action.triggerSuggest' } } ; } else { return { label : escapePath ( p ) , kind : CompletionItemKind . File , textEdit : TextEdit . replace ( replaceRange , escapePath ( p ) ) } ; } } function escapePath ( p ) { return p . replace ( /(\s|\(|\)|,|"|')/g , '\\$1' ) ; } function resolveWorkspaceRoot ( activeDoc , workspaceFolders ) { for ( const folder of workspaceFolders ) { if ( startsWith ( activeDoc . uri , folder . uri ) ) { return path . resolve ( URI . parse ( folder . uri ) . fsPath ) ; } } return undefined ; } function shiftPosition ( pos , offset ) { return Position . create ( pos . line , pos . character + offset ) ; } function shiftRange ( range , startOffset , endOffset ) { const start = shiftPosition ( range . start , startOffset ) ; const end = shiftPosition ( range . end , endOffset ) ; return Range . create ( start , end ) ; }