@@ -137,7 +137,6 @@ namespace ts.Completions.PathCompletions {
137137 if ( directories ) {
138138 for ( const directory of directories ) {
139139 const directoryName = getBaseFileName ( normalizePath ( directory ) ) ;
140-
141140 result . push ( nameAndKind ( directoryName , ScriptElementKind . directory ) ) ;
142141 }
143142 }
@@ -177,19 +176,33 @@ namespace ts.Completions.PathCompletions {
177176 }
178177 }
179178
180- if ( compilerOptions . moduleResolution === ModuleResolutionKind . NodeJs ) {
181- forEachAncestorDirectory ( scriptPath , ancestor => {
182- const nodeModules = combinePaths ( ancestor , "node_modules" ) ;
183- if ( host . directoryExists ( nodeModules ) ) {
184- getCompletionEntriesForDirectoryFragment ( fragment , nodeModules , fileExtensions , /*includeExtensions*/ false , host , /*exclude*/ undefined , result ) ;
185- }
186- } ) ;
179+ const fragmentDirectory = containsSlash ( fragment ) ? getDirectoryPath ( fragment ) : undefined ;
180+ for ( const ambientName of getAmbientModuleCompletions ( fragment , fragmentDirectory , typeChecker ) ) {
181+ result . push ( nameAndKind ( ambientName , ScriptElementKind . externalModuleName ) ) ;
187182 }
188183
189- getCompletionEntriesFromTypings ( host , compilerOptions , scriptPath , result ) ;
190-
191- for ( const moduleName of enumeratePotentialNonRelativeModules ( fragment , scriptPath , compilerOptions , typeChecker , host ) ) {
192- result . push ( nameAndKind ( moduleName , ScriptElementKind . externalModuleName ) ) ;
184+ if ( getEmitModuleResolutionKind ( compilerOptions ) === ModuleResolutionKind . NodeJs ) {
185+ // If looking for a global package name, don't just include everything in `node_modules` because that includes dependencies' own dependencies.
186+ // (But do if we didn't find anything, e.g. 'package.json' missing.)
187+ let foundGlobal = false ;
188+ if ( fragmentDirectory === undefined ) {
189+ const oldLength = result . length ;
190+ getCompletionEntriesFromTypings ( host , compilerOptions , scriptPath , result ) ;
191+ for ( const moduleName of getNamesFromVisibleNodeModules ( fragmentDirectory , scriptPath , host ) ) {
192+ if ( ! result . some ( entry => entry . name === moduleName ) ) {
193+ result . push ( nameAndKind ( moduleName , ScriptElementKind . externalModuleName ) ) ;
194+ }
195+ }
196+ foundGlobal = result . length !== oldLength ;
197+ }
198+ if ( ! foundGlobal ) {
199+ forEachAncestorDirectory ( scriptPath , ancestor => {
200+ const nodeModules = combinePaths ( ancestor , "node_modules" ) ;
201+ if ( host . directoryExists ( nodeModules ) ) {
202+ getCompletionEntriesForDirectoryFragment ( fragment , nodeModules , fileExtensions , /*includeExtensions*/ false , host , /*exclude*/ undefined , result ) ;
203+ }
204+ } ) ;
205+ }
193206 }
194207
195208 return result ;
@@ -228,7 +241,7 @@ namespace ts.Completions.PathCompletions {
228241 const normalizedPrefixDirectory = getDirectoryPath ( normalizedPrefix ) ;
229242 const normalizedPrefixBase = getBaseFileName ( normalizedPrefix ) ;
230243
231- const fragmentHasPath = stringContains ( fragment , directorySeparator ) ;
244+ const fragmentHasPath = containsSlash ( fragment ) ;
232245
233246 // Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call
234247 const expandedPrefixDirectory = fragmentHasPath ? combinePaths ( normalizedPrefixDirectory , normalizedPrefixBase + getDirectoryPath ( fragment ) ) : normalizedPrefixDirectory ;
@@ -262,45 +275,31 @@ namespace ts.Completions.PathCompletions {
262275 return path [ 0 ] === directorySeparator ? path . slice ( 1 ) : path ;
263276 }
264277
265- function enumeratePotentialNonRelativeModules ( fragment : string , scriptPath : string , options : CompilerOptions , typeChecker : TypeChecker , host : LanguageServiceHost ) : string [ ] {
266- // Check If this is a nested module
267- const isNestedModule = stringContains ( fragment , directorySeparator ) ;
268- const moduleNameFragment = isNestedModule ? fragment . substr ( 0 , fragment . lastIndexOf ( directorySeparator ) ) : undefined ;
269-
278+ function getAmbientModuleCompletions ( fragment : string , fragmentDirectory : string | undefined , checker : TypeChecker ) : ReadonlyArray < string > {
270279 // Get modules that the type checker picked up
271- const ambientModules = map ( typeChecker . getAmbientModules ( ) , sym => stripQuotes ( sym . name ) ) ;
272- let nonRelativeModuleNames = filter ( ambientModules , moduleName => startsWith ( moduleName , fragment ) ) ;
280+ const ambientModules = checker . getAmbientModules ( ) . map ( sym => stripQuotes ( sym . name ) ) ;
281+ const nonRelativeModuleNames = ambientModules . filter ( moduleName => startsWith ( moduleName , fragment ) ) ;
273282
274283 // Nested modules of the form "module-name/sub" need to be adjusted to only return the string
275284 // after the last '/' that appears in the fragment because that's where the replacement span
276285 // starts
277- if ( isNestedModule ) {
278- const moduleNameWithSeperator = ensureTrailingDirectorySeparator ( moduleNameFragment ) ;
279- nonRelativeModuleNames = map ( nonRelativeModuleNames , nonRelativeModuleName => {
280- return removePrefix ( nonRelativeModuleName , moduleNameWithSeperator ) ;
281- } ) ;
286+ if ( fragmentDirectory !== undefined ) {
287+ const moduleNameWithSeperator = ensureTrailingDirectorySeparator ( fragmentDirectory ) ;
288+ return nonRelativeModuleNames . map ( nonRelativeModuleName => removePrefix ( nonRelativeModuleName , moduleNameWithSeperator ) ) ;
282289 }
290+ return nonRelativeModuleNames ;
291+ }
283292
284-
285- if ( ! options . moduleResolution || options . moduleResolution === ModuleResolutionKind . NodeJs ) {
286- for ( const visibleModule of enumerateNodeModulesVisibleToScript ( host , scriptPath ) ) {
287- if ( ! isNestedModule ) {
288- nonRelativeModuleNames . push ( visibleModule . moduleName ) ;
289- }
290- else if ( startsWith ( visibleModule . moduleName , moduleNameFragment ) ) {
291- const nestedFiles = tryReadDirectory ( host , visibleModule . moduleDir , supportedTypeScriptExtensions , /*exclude*/ undefined , /*include*/ [ "./*" ] ) ;
292- if ( nestedFiles ) {
293- for ( let f of nestedFiles ) {
294- f = normalizePath ( f ) ;
295- const nestedModule = removeFileExtension ( getBaseFileName ( f ) ) ;
296- nonRelativeModuleNames . push ( nestedModule ) ;
297- }
298- }
299- }
293+ function getNamesFromVisibleNodeModules ( fragmentDirectory : string | undefined , scriptPath : string , host : LanguageServiceHost ) : string [ ] {
294+ return flatMap ( enumerateNodeModulesVisibleToScript ( host , scriptPath ) , visibleModule => {
295+ if ( fragmentDirectory === undefined ) {
296+ return visibleModule . moduleName ;
300297 }
301- }
302-
303- return deduplicate ( nonRelativeModuleNames , equateStringsCaseSensitive , compareStringsCaseSensitive ) ;
298+ else if ( startsWith ( visibleModule . moduleName , fragmentDirectory ) ) {
299+ const nestedFiles = tryReadDirectory ( host , visibleModule . moduleDir , supportedTypeScriptExtensions , /*exclude*/ undefined , /*include*/ [ "./*" ] ) ;
300+ return map ( nestedFiles , f => removeFileExtension ( getBaseFileName ( normalizePath ( f ) ) ) ) ;
301+ }
302+ } ) ;
304303 }
305304
306305 export function getTripleSlashReferenceCompletion ( sourceFile : SourceFile , position : number , compilerOptions : CompilerOptions , host : LanguageServiceHost ) : ReadonlyArray < PathCompletion > | undefined {
@@ -522,4 +521,8 @@ namespace ts.Completions.PathCompletions {
522521 catch { /*ignore*/ }
523522 return undefined ;
524523 }
524+
525+ function containsSlash ( fragment : string ) {
526+ return stringContains ( fragment , directorySeparator ) ;
527+ }
525528}
0 commit comments