import * as fs from "fs" ; import * as path from "path" ; import * as vscode from "vscode" ; import { CancellationToken , Command , CompletionItem , CompletionItemKind , CompletionItemProvider , Disposable , ExtensionContext , FileSystemWatcher , Position , TextDocument , TextEdit , Uri , WorkspaceConfiguration , } from "vscode" ; import resolvePackage from "./resolve" ; export default class IntellisenseProvider implements CompletionItemProvider { /**
     * Builtin Node.js modules
     */ public static readonly builtinModules : string [ ] = getBuiltinModules ( ) ; public static readonly configPath : string = "node-module-intellisense" ; public static readonly defaultAutoStripExtensions : string [ ] = [ ".js" , ".jsx" , ".ts" , ".d.ts" , ".tsx" ] ; public static readonly languageSelector : string [ ] = [ "javascript" , "javascriptreact" , "typescript" , "typescriptreact" , "html" , "coffeescript" ] ; public static readonly triggerCharacters : string [ ] = [ "'" , "\"" , "/" ] ; private context : ExtensionContext ; private dependencies : string [ ] = [ ] ; private packageJsonFile : string = this . resolveWorkspacePath ( "package.json" ) ; private packageJsonWatcher : FileSystemWatcher ; private config : WorkspaceConfiguration ; private enableDevDependencies : boolean = true ; private enableFileModules : boolean = true ; private modulePaths : string [ ] = [ ] ; private enableBuiltinModules : boolean = true ; private autoStripExtensions : string [ ] = IntellisenseProvider . defaultAutoStripExtensions ; private readonly disposables : Disposable [ ] = [ ] ; public activate ( context ) { this . context = context ; context . subscriptions . push ( this ) ; const loadConfig = ( ) => { this . config = vscode . workspace . getConfiguration ( IntellisenseProvider . configPath ) ; this . enableBuiltinModules = this . config . get ( "scanBuiltinModules" , true ) ; this . enableDevDependencies = this . config . get ( "scanDevDependencies" , true ) ; this . enableFileModules = this . config . get ( "scanFileModules" , true ) ; this . modulePaths = this . config . get ( "modulePaths" , [ ] ) ; this . autoStripExtensions = this . config . get ( "autoStripExtensions" , IntellisenseProvider . defaultAutoStripExtensions ) ; this . autoStripExtensions . sort ( ( a , b ) => b . length - a . length ) ; } ; vscode . workspace . onDidChangeConfiguration ( ( e ) => { loadConfig ( ) ; } ) ; loadConfig ( ) ; vscode . languages . registerCompletionItemProvider ( IntellisenseProvider . languageSelector , this , ... IntellisenseProvider . triggerCharacters ) ; this . updateDependenciesFromPackageJson ( ) ; this . packageJsonWatcher = vscode . workspace . createFileSystemWatcher ( "**/package.json" ) ; this . disposables . push ( this . packageJsonWatcher ) ; const onPackageJsonFileChange = ( e ) => { if ( e . fsPath === this . packageJsonFile ) { this . updateDependenciesFromPackageJson ( ) ; } } ; this . packageJsonWatcher . onDidChange ( onPackageJsonFileChange ) ; this . packageJsonWatcher . onDidCreate ( onPackageJsonFileChange ) ; this . packageJsonWatcher . onDidDelete ( onPackageJsonFileChange ) ; } public dispose ( ) { this . disposables . forEach ( ( item ) => { try { item . dispose ( ) ; } catch ( err ) { } } ) ; } param document param position param token return public async provideCompletionItems ( document , position , token ) { const info = parseLine ( document , position ) ; if ( ! info ) { return [ ] ; } let list = [ ] ; const isShowPackageSubPath = info . isPackagePath && info . search . indexOf ( "/" ) > 0 ; const isShowPackage = info . isPackagePath || info . search === "" ; const isShowFile = info . isAbsoultePath || info . isRelativePath || info . search === "" ; const isIncludeExtname = info . type === "reference" ; if ( isShowPackageSubPath ) { let pkgDir ; try { pkgDir = await resolvePackageDirectory ( info . packageName , document . uri . fsPath ) ; const currentDir = path . resolve ( pkgDir , info . packageSubPath ) ; const files = await this . readCurrentDirectory ( currentDir , info . search , false ) ; files . forEach ( ( item ) => { item . insertText = item . label . slice ( info . search . length ) ; } ) ; list = list . concat ( files ) ; } catch ( err ) { this . debug ( "resolvePackageDirectory" , err ) ; } } else { if ( isShowPackage && this . enableBuiltinModules ) { list = IntellisenseProvider . builtinModules . map ( ( name ) => { return createCompletionItem ( name , CompletionItemKind . Module , { detail : "builtin module" } ) ; } ) ; } if ( isShowPackage ) { list = list . concat ( this . dependencies . map ( ( name ) => { return createCompletionItem ( name , CompletionItemKind . Module , { detail : "npm dependencies" } ) ; } ) ) ; } } if ( isShowFile && this . enableFileModules ) { const currentDir = path . resolve ( path . dirname ( document . uri . fsPath ) , info . search ) ; const files = await this . readCurrentDirectory ( currentDir , info . search || "./" , isIncludeExtname ) ; files . forEach ( ( item ) => { item . insertText = item . label . slice ( info . search . length ) ; } ) ; list = list . concat ( files ) ; } if ( this . modulePaths . length > 0 ) { for ( const modulePath of this . modulePaths ) { const currentDir = this . resolveWorkspacePath ( modulePath . replace ( "${workspaceRoot}" , "" ) , info . search || "" ) ; const files = await this . readCurrentDirectory ( currentDir , info . search || "" , isIncludeExtname ) ; files . forEach ( ( item ) => { item . insertText = item . label . slice ( info . search . length ) ; } ) ; list = list . concat ( files ) ; } } return list ; } param item param token return public resolveCompletionItem ? ( item , token ) { return item ; } private debug ( ... data ) { console . log ( "IntellisenseProvider debug:" , ... data ) ; } private showWarning ( msg ) { vscode . window . showWarningMessage ( `node-module-intellisense: ${ msg }` ) ; } private resolveWorkspacePath ( ... paths ) { if ( vscode . workspace . rootPath ) { return path . resolve ( vscode . workspace . rootPath , ... paths ) ; } return path . resolve ( ... paths ) ; } private async updateDependenciesFromPackageJson ( ) { const exists = await isFileExists ( this . packageJsonFile ) ; if ( ! exists ) { return ; } let data ; try { data = ( await readFileContent ( this . packageJsonFile ) ) . toString ( ) ; } catch ( err ) { return this . showWarning ( err . message ) ; } let json ; try { json = JSON . parse ( data . toString ( ) ) ; } catch ( err ) { return this . showWarning ( `parsing package.json file error: ${ err . message }` ) ; } const list = new Set < string > ( ) ; if ( json . dependencies ) { Object . keys ( json . dependencies ) . forEach ( ( name ) => list . add ( name ) ) ; } if ( this . enableDevDependencies && json . devDependencies ) { Object . keys ( json . devDependencies ) . forEach ( ( name ) => list . add ( name ) ) ; } this . dependencies = Array . from ( list . values ( ) ) ; } private async readCurrentDirectory ( dir , prefix , isIncludeExtname ) { const names = await readdir ( dir ) ; const list = [ ] ; const fileMap = new Map < string , boolean > ( ) ; const relativePathInfo = ( p ) => { if ( vscode . workspace . rootPath ) { return `relative to workspace: ${ path . relative ( vscode . workspace . rootPath , p ) }` ; } return `absolute path: ${ p }` ; } ; list . push ( createCompletionItem ( ".." , CompletionItemKind . Module , { detail : "directory" , documentation : relativePathInfo ( path . dirname ( dir ) ) , } ) ) ; for ( const name of names ) { const realPath = path . join ( dir , name ) ; const stats = await readFileStats ( realPath ) ; if ( stats . isDirectory ( ) ) { list . push ( createCompletionItem ( `${ prefix }${ name }` , CompletionItemKind . Module , { detail : "directory" , documentation : relativePathInfo ( realPath ) , } ) ) ; } else if ( stats . isFile ( ) ) { const [ strip , ext ] = parseFileExtensionName ( name , this . autoStripExtensions ) ; this . debug ( "FILE" , name , strip , ext ) ; let n = name ; if ( ! isIncludeExtname && strip ) { n = name . slice ( 0 , name . length - ext . length ) ; } if ( ! fileMap . has ( n ) ) { fileMap . set ( n , true ) ; list . push ( createCompletionItem ( `${ prefix }${ n }` , CompletionItemKind . File , { detail : "file module" , documentation : relativePathInfo ( realPath ) , } ) ) ; } } } return list ; } } /**
 * returns builtin modules
 */ function getBuiltinModules ( ) { return Object . keys ( ( process as any ) . binding ( "natives" ) ) . filter ( ( n ) => { if ( n . indexOf ( "_" ) !== - 1 ) { return false ; } if ( n . indexOf ( "/" ) !== - 1 ) { return false ; } if ( n . indexOf ( "-" ) !== - 1 ) { return false ; } return true ; } ) ; } interface ExtraCompletionInfo { label ? : string ; kind ? : CompletionItemKind ; detail ? : string ; documentation ? : string ; sortText ? : string ; filterText ? : string ; insertText ? : string ; command ? : Command ; textEdit ? : TextEdit ; additionalTextEdits ? : TextEdit ; } /**
 * create CompletionItem
 */ function createCompletionItem ( name , kind , info ) { const item = new CompletionItem ( name , kind ) ; Object . assign ( item , info ) ; return item ; } /**
 * returns true if file is exists
 */ function isFileExists ( filename ) { return new Promise ( ( resolve , reject ) => { fs . exists ( filename , resolve ) ; } ) ; } /**
 * returns file content
 */ function readFileContent ( filename ) { return new Promise ( ( resolve , reject ) => { fs . readFile ( filename , ( err , data ) => { if ( err ) { return reject ( err ) ; } resolve ( data ) ; } ) ; } ) ; } /**
 * returns file stats
 */ function readFileStats ( filename ) { return new Promise ( ( resolve , reject ) => { fs . stat ( filename , ( err , stats ) => { if ( err ) { return reject ( err ) ; } resolve ( stats ) ; } ) ; } ) ; } /**
 * returns directory files
 */ function readdir ( dir ) { return new Promise ( ( resolve , reject ) => { fs . readdir ( dir , ( err , list ) => { if ( err ) { return reject ( err ) ; } resolve ( list ) ; } ) ; } ) ; } interface IntellisenseLineInfo { line ? : string ; quotation ? : string ; quotationStart ? : number ; search ? : string ; isAbsoultePath ? : boolean ; isRelativePath ? : boolean ; isPackagePath ? : boolean ; packageName ? : string ; packageSubPath ? : string ; position ? : Position ; type ? : StatementType ; } type StatementType = "require" | "import" | "export" | "reference" | false ; /**
 * Parse current line
 */ function parseLine ( document , position ) { const info = { position , } ; const line = document . getText ( document . lineAt ( position ) . range ) ; info . type = getStatementType ( line ) ; if ( ! info . type ) { return ; } const [ i , quotation ] = getForwardQuotation ( line , position . character ) ; info . quotation = quotation ; info . quotationStart = i ; info . search = line . slice ( i + 1 , position . character ) ; if ( info . search [ 0 ] === "." ) { info . isRelativePath = true ; } else if ( info . search [ 0 ] === "/" ) { info . isAbsoultePath = true ; } else { info . isPackagePath = true ; let j = info . search . indexOf ( path . sep ) ; if ( j !== - 1 && info . search [ 0 ] === "@" ) { j = info . search . indexOf ( path . sep , j + 1 ) ; } if ( j === - 1 ) { info . packageName = info . search ; info . packageSubPath = "" ; } else { info . packageName = info . search . slice ( 0 , j ) ; info . packageSubPath = info . search . slice ( j + 1 ) ; } } return info ; } /**
 * Returns statement type
 */ function getStatementType ( line ) { line = line . trim ( ) ; if ( line . indexOf ( "import " ) === 0 ) { return "import" ; } if ( line . indexOf ( "require(" ) !== - 1 ) { return "require" ; } if ( line . indexOf ( "export " ) === 0 && line . indexOf ( " from " ) !== - 1 ) { return "export" ; } if ( line . trim ( ) . indexOf ( "/// <reference " ) === 0 ) { return "reference" ; } return false ; } /**
 * Returns forward quotation position and character
 */ function getForwardQuotation ( line , index ) { const i = line . lastIndexOf ( "\"" , index - 1 ) ; const j = line . lastIndexOf ( "'" , index - 1 ) ; if ( i > j ) { return [ i , "\"" ] ; } return [ j , "'" ] ; } /**
 * Parse File extension name
 */ function parseFileExtensionName ( filename , autoStripExtensions ) { const len = filename . length ; for ( const ext of autoStripExtensions ) { if ( filename . slice ( len - ext . length ) === ext ) { return [ true , ext ] ; } } return [ false , "" ] ; } /**
 * Returns require package directory from current path
 */ function resolvePackageDirectory ( pkgName , filename ) { return resolvePackage ( pkgName , path . dirname ( filename ) ) ; }