import { TextDocument , CompletionItemKind , CompletionItem , TextEdit , Range , Position } from 'vscode-languageserver-types' ; import { WorkspaceFolder } from 'vscode-languageserver' ; import * as path from 'path' ; import * as fs from 'fs' ; import { URI } from 'vscode-uri' ; import { ICompletionParticipant } from 'vscode-html-languageservice' ; import { startsWith } from '../utils/strings' ; import { contains } from '../utils/arrays' ; export function getPathCompletionParticipant ( document , workspaceFolders , result ) { return { onHtmlAttributeValue : ( { tag , attribute , value : valueBeforeCursor , range } ) => { const fullValue = stripQuotes ( document . getText ( range ) ) ; if ( shouldDoPathCompletion ( tag , attribute , fullValue ) ) { if ( workspaceFolders . length === 0 ) { return ; } const workspaceRoot = resolveWorkspaceRoot ( document , workspaceFolders ) ; const paths = providePaths ( valueBeforeCursor , URI . parse ( document . uri ) . fsPath , workspaceRoot ) ; result . push ( ... paths . map ( p => pathToSuggestion ( p , valueBeforeCursor , fullValue , range ) ) ) ; } } } ; } function stripQuotes ( fullValue ) { if ( startsWith ( fullValue , `'` ) || startsWith ( fullValue , `"` ) ) { return fullValue . slice ( 1 , - 1 ) ; } else { return fullValue ; } } function shouldDoPathCompletion ( tag , attr , value ) { if ( startsWith ( value , 'http' ) || startsWith ( value , 'https' ) || startsWith ( value , '//' ) ) { return false ; } if ( PATH_TAG_AND_ATTR [ tag ] ) { if ( typeof PATH_TAG_AND_ATTR [ tag ] === 'string' ) { return PATH_TAG_AND_ATTR [ tag ] === attr ; } else { return contains ( < string [ ] > PATH_TAG_AND_ATTR [ tag ] , attr ) ; } } return false ; } /**
 * 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 { const paths = fs . readdirSync ( parentDir ) . map ( f => { return isDir ( path . resolve ( parentDir , f ) ) ? f + '/' : f ; } ) ; return paths . filter ( p => p [ 0 ] !== '.' ) ; } catch ( e ) { return [ ] ; } } function isDir ( p ) { try { return fs . statSync ( p ) . isDirectory ( ) ; } catch ( e ) { return false ; } } function pathToSuggestion ( p , valueBeforeCursor , fullValue , range ) { const isDir = p [ p . length - 1 ] === '/' ; let replaceRange ; const lastIndexOfSlash = valueBeforeCursor . lastIndexOf ( '/' ) ; if ( lastIndexOfSlash === - 1 ) { replaceRange = shiftRange ( range , 1 , - 1 ) ; } else { const valueAfterLastSlash = fullValue . slice ( lastIndexOfSlash + 1 ) ; const startPos = shiftPosition ( range . end , - 1 - valueAfterLastSlash . length ) ; const whitespaceIndex = valueAfterLastSlash . indexOf ( ' ' ) ; let endPos ; if ( whitespaceIndex !== - 1 ) { endPos = shiftPosition ( startPos , whitespaceIndex ) ; } else { endPos = shiftPosition ( range . end , - 1 ) ; } replaceRange = Range . create ( startPos , endPos ) ; } if ( isDir ) { return { label : p , kind : CompletionItemKind . Folder , textEdit : TextEdit . replace ( replaceRange , p ) , command : { title : 'Suggest' , command : 'editor.action.triggerSuggest' } } ; } else { return { label : p , kind : CompletionItemKind . File , textEdit : TextEdit . replace ( replaceRange , p ) } ; } } 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 ) ; } const PATH_TAG_AND_ATTR = { a : 'href' , area : 'href' , body : 'background' , del : 'cite' , form : 'action' , frame : [ 'src' , 'longdesc' ] , img : [ 'src' , 'longdesc' ] , ins : 'cite' , link : 'href' , object : 'data' , q : 'cite' , script : 'src' , audio : 'src' , button : 'formaction' , command : 'icon' , embed : 'src' , html : 'manifest' , input : [ 'src' , 'formaction' ] , source : 'src' , track : 'src' , video : [ 'src' , 'poster' ] } ;