import { ChildProcess , spawn } from 'child_process' ; import { BasicList , ListContext , ListTask , Location , Neovim , Range , Uri , workspace } from 'coc.nvim' ; import { EventEmitter } from 'events' ; import fs from 'fs' ; import minimatch from 'minimatch' ; import path from 'path' ; import readline from 'readline' ; import { executable } from './util' ; class Task extends EventEmitter implements ListTask { private processes : ChildProcess [ ] = [ ] ; public start ( cmd , args , cwds , patterns ) { let remain = cwds . length ; let config = workspace . getConfiguration ( 'list.source.files' ) ; let filterByName = config . get < boolean > ( 'filterByName' , false ) ; for ( let cwd of cwds ) { let process = spawn ( cmd , args , { cwd } ) ; this . processes . push ( process ) ; process . on ( 'error' , e => { this . emit ( 'error' , e . message ) ; } ) ; const rl = readline . createInterface ( process . stdout ) ; const range = Range . create ( 0 , 0 , 0 , 0 ) ; let hasPattern = patterns . length > 0 ; process . stderr . on ( 'data' , chunk => { console . error ( chunk . toString ( 'utf8' ) ) ; } ) ; rl . on ( 'line' , line => { let file = line ; if ( file . indexOf ( cwd ) < 0 ) { file = path . join ( cwd , line ) ; } if ( hasPattern && patterns . some ( p => minimatch ( file , p ) ) ) return ; let location = Location . create ( Uri . file ( file ) . toString ( ) , range ) ; if ( ! filterByName ) { this . emit ( 'data' , { label : line , sortText : file , location } ) ; } else { let name = path . basename ( file ) ; this . emit ( 'data' , { label : `${ name }\t${ line }` , sortText : file , filterText : name , location } ) ; } } ) ; rl . on ( 'close' , ( ) => { remain = remain - 1 ; if ( remain == 0 ) { this . emit ( 'end' ) ; } } ) ; } } public dispose ( ) { for ( let process of this . processes ) { if ( ! process . killed ) { process . kill ( ) ; } } } } export default class FilesList extends BasicList { public readonly name = 'files' ; public readonly defaultAction = 'open' ; public description = 'Search files by rg or ag' ; public readonly detail = `Install ripgrep in your $PATH to have best experience.
Files is searched from current cwd by default.
Provide directory names as arguments to search other directories.
Use 'list.source.files.command' configuration for custom search command.
Use 'list.source.files.args' configuration for custom command arguments.
Note that rg ignore hidden files by default.` ; public options = [ { name : '-F, -folder' , description : 'Search files from current workspace folder instead of cwd.' } , { name : '-W, -workspace' , description : 'Search files from all workspace folders instead of cwd.' } ] ; constructor ( nvim ) { super ( nvim ) ; this . addLocationActions ( ) ; } private getArgs ( args , defaultArgs ) { return args . length ? args : defaultArgs ; } public getCommand ( ) { let config = workspace . getConfiguration ( 'list.source.files' ) ; let cmd = config . get < string > ( 'command' , '' ) ; let args = config . get < string [ ] > ( 'args' , [ ] ) ; if ( ! cmd ) { if ( executable ( 'rg' ) ) { return { cmd : 'rg' , args : this . getArgs ( args , [ '--color' , 'never' , '--files' ] ) } ; } else if ( executable ( 'ag' ) ) { return { cmd : 'ag' , args : this . getArgs ( args , [ '-f' , '-g' , '.' , '--nocolor' ] ) } ; } else if ( process . platform == 'win32' ) { return { cmd : 'dir' , args : this . getArgs ( args , [ '/a-D' , '/S' , '/B' ] ) } ; } else if ( executable ( 'find' ) ) { return { cmd : 'find' , args : this . getArgs ( args , [ '.' , '-type' , 'f' ] ) } ; } else { throw new Error ( 'Unable to find command for files list.' ) ; return null ; } } else { return { cmd , args } ; } } public async loadItems ( context ) { let { nvim } = this ; let { window , args } = context ; let options = this . parseArguments ( args ) ; let res = this . getCommand ( ) ; if ( ! res ) return null ; let used = res . args . concat ( [ '-F' , '-folder' , '-W' , '-workspace' ] ) ; let extraArgs = args . filter ( s => used . indexOf ( s ) == - 1 ) ; let cwds ; let dirArgs = [ ] ; let searchArgs = [ ] ; if ( options . folder ) { cwds = [ workspace . rootPath ] ; } else if ( options . workspace ) { cwds = workspace . workspaceFolders . map ( f => Uri . parse ( f . uri ) . fsPath ) ; } else { if ( extraArgs . length > 0 ) { for ( let i = 0 ; i < extraArgs . length ; i ++ ) { let d = await nvim . call ( 'expand' , extraArgs [ i ] ) ; try { if ( fs . lstatSync ( d ) . isDirectory ( ) ) { dirArgs . push ( d ) ; } else { searchArgs . push ( d ) ; } } catch ( e ) { searchArgs . push ( d ) ; } } } if ( dirArgs . length > 0 ) { cwds = dirArgs ; } else { let valid = await window . valid ; if ( valid ) { cwds = [ await nvim . call ( 'getcwd' , window . id ) ] ; } else { cwds = [ await nvim . call ( 'getcwd' ) ] ; } } } let task = new Task ( ) ; let excludePatterns = this . getConfig ( ) . get < string [ ] > ( 'excludePatterns' , [ ] ) ; task . start ( res . cmd , res . args . concat ( searchArgs ) , cwds , excludePatterns ) ; return task ; } public doHighlight ( ) { let config = workspace . getConfiguration ( 'list.source.files' ) ; let filterByName = config . get < boolean > ( 'filterByName' , false ) ; if ( filterByName ) { let { nvim } = this ; nvim . pauseNotification ( ) ; nvim . command ( 'syntax match CocFilesName /\\v^[^\\t]+/ contained containedin=CocFilesLine' , true ) ; nvim . command ( 'syntax match CocFilesFile /\\t.*$/ contained containedin=CocFilesLine' , true ) ; nvim . command ( 'highlight default link CocFilesName Identifier' , true ) ; nvim . command ( 'highlight default link CocFilesFile Comment' , true ) ; nvim . resumeNotification ( false , true ) ; } } }