@@ -39,15 +39,12 @@ import {
39
39
FileSpec ,
40
40
deduplicateFolders ,
41
41
getFileSpec ,
42
- getFileSystemEntries ,
43
42
hasPythonExtension ,
44
43
isDirectory ,
45
44
isFile ,
46
45
makeDirectories ,
47
- tryRealpath ,
48
46
tryStat ,
49
47
} from '../common/uri/uriUtils' ;
50
- import { Localizer } from '../localization/localize' ;
51
48
import { AnalysisCompleteCallback } from './analysis' ;
52
49
import {
53
50
BackgroundAnalysisProgram ,
@@ -63,6 +60,7 @@ import {
63
60
findPyprojectTomlFile ,
64
61
findPyprojectTomlFileHereOrUp ,
65
62
} from './serviceUtils' ;
63
+ import { SourceEnumerator } from './sourceEnumerator' ;
66
64
import { IPythonMode } from './sourceFile' ;
67
65
68
66
// How long since the last user activity should we wait until running
@@ -128,6 +126,7 @@ export class AnalyzerService {
128
126
private _requireTrackedFileUpdate = true ;
129
127
private _lastUserInteractionTime = 0 ;
130
128
private _backgroundAnalysisCancellationSource : AbstractCancellationTokenSource | undefined ;
129
+ private _sourceEnumerator : SourceEnumerator | undefined ;
131
130
132
131
private _disposed = false ;
133
132
private _pendingLibraryChanges : RefreshOptions = { changesOnly : true } ;
@@ -448,7 +447,16 @@ export class AnalyzerService {
448
447
}
449
448
450
449
test_getFileNamesFromFileSpecs ( ) : Uri [ ] {
451
- return this . _getFileNamesFromFileSpecs ( ) ;
450
+ const enumerator = new SourceEnumerator (
451
+ this . _configOptions . include ,
452
+ this . _configOptions . exclude ,
453
+ ! ! this . _configOptions . autoExcludeVenv ,
454
+ this . fs ,
455
+ this . _console
456
+ ) ;
457
+
458
+ const results = enumerator . enumerate ( 0 ) ;
459
+ return this . _getTrackedFileList ( results . matches ) ;
452
460
}
453
461
454
462
test_shouldHandleSourceFileWatchChanges ( uri : Uri , isFile : boolean ) {
@@ -547,6 +555,16 @@ export class AnalyzerService {
547
555
this . _updateTrackedFileList ( /* markFilesDirtyUnconditionally */ false ) ;
548
556
}
549
557
558
+ // Continue to enumerate sources if we haven't finished doing so.
559
+ // Use the "noOpenFilesTimeInMs" limit if it's provided. Otherwise
560
+ // do all enumeration in one shot. The latter is used for the CLI
561
+ // and other environments where the user is not blocked on the operation.
562
+ const maxSourceEnumeratorTime = this . options . maxAnalysisTime ?. noOpenFilesTimeInMs ?? 0 ;
563
+ if ( ! this . enumerateSourceFiles ( maxSourceEnumeratorTime ) ) {
564
+ this . scheduleReanalysis ( /* requireTrackedFileUpdate */ false ) ;
565
+ return ;
566
+ }
567
+
550
568
// Recreate the cancellation token every time we start analysis.
551
569
this . _backgroundAnalysisCancellationSource = this . cancellationProvider . createCancellationTokenSource ( ) ;
552
570
@@ -556,6 +574,52 @@ export class AnalyzerService {
556
574
} , timeUntilNextAnalysisInMs ) ;
557
575
}
558
576
577
+ // Attempts to make progress on source file enumeration if there is an active
578
+ // source enumerator associated with the service. Returns true if complete.
579
+ protected enumerateSourceFiles ( maxSourceEnumeratorTime : number ) : boolean {
580
+ // If there is no active source enumerator, we're done.
581
+ if ( ! this . _sourceEnumerator ) {
582
+ return true ;
583
+ }
584
+
585
+ let fileMap : Map < string , Uri > ;
586
+
587
+ if ( this . _executionRootUri . isEmpty ( ) ) {
588
+ // No user files for default workspace.
589
+ fileMap = new Map < string , Uri > ( ) ;
590
+ } else {
591
+ const enumerator = this . _sourceEnumerator ;
592
+ const enumResults = timingStats . findFilesTime . timeOperation ( ( ) =>
593
+ enumerator . enumerate ( maxSourceEnumeratorTime )
594
+ ) ;
595
+
596
+ if ( ! enumResults . isComplete ) {
597
+ return false ;
598
+ }
599
+
600
+ // Update the config options to include the auto-excluded directories.
601
+ const excludes = this . options . configOptions ?. exclude ;
602
+ if ( enumResults . autoExcludedDirs && excludes ) {
603
+ enumResults . autoExcludedDirs . forEach ( ( excludedDir ) => {
604
+ if ( ! FileSpec . isInPath ( excludedDir , excludes ) ) {
605
+ excludes . push ( getFileSpec ( this . _configOptions . projectRoot , `${ excludedDir } /**` ) ) ;
606
+ }
607
+ } ) ;
608
+ this . _backgroundAnalysisProgram . setConfigOptions ( this . _configOptions ) ;
609
+ }
610
+
611
+ fileMap = enumResults . matches ;
612
+
613
+ const fileList = this . _getTrackedFileList ( fileMap ) ;
614
+ this . _backgroundAnalysisProgram . setTrackedFiles ( fileList ) ;
615
+
616
+ // Source file enumeration is complete. Proceed with analysis.
617
+ this . _sourceEnumerator = undefined ;
618
+ }
619
+
620
+ return true ;
621
+ }
622
+
559
623
protected applyConfigOptions ( host : Host ) {
560
624
// Indicate that we are about to reanalyze because of this config change.
561
625
if ( this . options . onInvalidated ) {
@@ -1267,19 +1331,10 @@ export class AnalyzerService {
1267
1331
return undefined ;
1268
1332
}
1269
1333
1270
- private _getFileNamesFromFileSpecs ( ) : Uri [ ] {
1271
- // Use a map to generate a list of unique files.
1272
- const fileMap = new Map < string , Uri > ( ) ;
1273
-
1274
- // Scan all matching files from file system.
1275
- timingStats . findFilesTime . timeOperation ( ( ) => {
1276
- const matchedFiles = this . _matchFiles ( this . _configOptions . include , this . _configOptions . exclude ) ;
1277
-
1278
- for ( const file of matchedFiles ) {
1279
- fileMap . set ( file . key , file ) ;
1280
- }
1281
- } ) ;
1282
-
1334
+ // Given a file map returned by the source enumerator, this function
1335
+ // adds any open files that match the include file spec and returns a
1336
+ // final deduped file list.
1337
+ private _getTrackedFileList ( fileMap : Map < string , Uri > ) : Uri [ ] {
1283
1338
// And scan all matching open files. We need to do this since some of files are not backed by
1284
1339
// files in file system but only exist in memory (ex, virtual workspace)
1285
1340
this . _backgroundAnalysisProgram . program
@@ -1288,7 +1343,8 @@ export class AnalyzerService {
1288
1343
. filter ( ( f ) => matchFileSpecs ( this . _program . configOptions , f ) )
1289
1344
. forEach ( ( f ) => fileMap . set ( f . key , f ) ) ;
1290
1345
1291
- return Array . from ( fileMap . values ( ) ) ;
1346
+ const fileList = Array . from ( fileMap . values ( ) ) ;
1347
+ return fileList ;
1292
1348
}
1293
1349
1294
1350
// If markFilesDirtyUnconditionally is true, we need to reparse
@@ -1362,152 +1418,22 @@ export class AnalyzerService {
1362
1418
} else {
1363
1419
this . _console . error ( `Import '${ this . _typeStubTargetImportName } ' not found` ) ;
1364
1420
}
1421
+
1422
+ this . _requireTrackedFileUpdate = false ;
1365
1423
} else if ( ! this . options . skipScanningUserFiles ) {
1366
- let fileList : Uri [ ] = [ ] ;
1367
- this . _console . log ( `Searching for source files` ) ;
1368
- fileList = this . _getFileNamesFromFileSpecs ( ) ;
1424
+ // Allocate a new source enumerator. We'll call this
1425
+ // repeatedly until all source files are found.
1426
+ this . _sourceEnumerator = new SourceEnumerator (
1427
+ this . _configOptions . include ,
1428
+ this . _configOptions . exclude ,
1429
+ ! ! this . _configOptions . autoExcludeVenv ,
1430
+ this . fs ,
1431
+ this . _console
1432
+ ) ;
1369
1433
1370
- // getFileNamesFromFileSpecs might have updated configOptions, resync options.
1371
- this . _backgroundAnalysisProgram . setConfigOptions ( this . _configOptions ) ;
1372
- this . _backgroundAnalysisProgram . setTrackedFiles ( fileList ) ;
1373
1434
this . _backgroundAnalysisProgram . markAllFilesDirty ( markFilesDirtyUnconditionally ) ;
1374
-
1375
- if ( fileList . length === 0 ) {
1376
- this . _console . info ( `No source files found.` ) ;
1377
- } else {
1378
- this . _console . info ( `Found ${ fileList . length } ` + `source ${ fileList . length === 1 ? 'file' : 'files' } ` ) ;
1379
- }
1380
- }
1381
-
1382
- this . _requireTrackedFileUpdate = false ;
1383
- }
1384
-
1385
- private _tryShowLongOperationMessageBox ( ) {
1386
- const windowService = this . serviceProvider . tryGet ( ServiceKeys . windowService ) ;
1387
- if ( ! windowService ) {
1388
- return ;
1389
- }
1390
-
1391
- const message = Localizer . Service . longOperation ( ) ;
1392
- const action = windowService . createGoToOutputAction ( ) ;
1393
- windowService . showInformationMessage ( message , action ) ;
1394
- }
1395
-
1396
- private _matchFiles ( include : FileSpec [ ] , exclude : FileSpec [ ] ) : Uri [ ] {
1397
- if ( this . _executionRootUri . isEmpty ( ) ) {
1398
- // No user files for default workspace.
1399
- return [ ] ;
1435
+ this . _requireTrackedFileUpdate = false ;
1400
1436
}
1401
-
1402
- const envMarkers = [ [ 'bin' , 'activate' ] , [ 'Scripts' , 'activate' ] , [ 'pyvenv.cfg' ] , [ 'conda-meta' ] ] ;
1403
- const results : Uri [ ] = [ ] ;
1404
- const startTime = Date . now ( ) ;
1405
- const longOperationLimitInSec = 10 ;
1406
- const nFilesToSuggestSubfolder = 50 ;
1407
-
1408
- let loggedLongOperationError = false ;
1409
- let nFilesVisited = 0 ;
1410
-
1411
- const visitDirectoryUnchecked = ( absolutePath : Uri , includeRegExp : RegExp , hasDirectoryWildcard : boolean ) => {
1412
- if ( ! loggedLongOperationError ) {
1413
- const secondsSinceStart = ( Date . now ( ) - startTime ) * 0.001 ;
1414
-
1415
- // If this is taking a long time, log an error to help the user
1416
- // diagnose and mitigate the problem.
1417
- if ( secondsSinceStart >= longOperationLimitInSec && nFilesVisited >= nFilesToSuggestSubfolder ) {
1418
- this . _console . error (
1419
- `Enumeration of workspace source files is taking longer than ${ longOperationLimitInSec } seconds.\n` +
1420
- 'This may be because:\n' +
1421
- '* You have opened your home directory or entire hard drive as a workspace\n' +
1422
- '* Your workspace contains a very large number of directories and files\n' +
1423
- '* Your workspace contains a symlink to a directory with many files\n' +
1424
- '* Your workspace is remote, and file enumeration is slow\n' +
1425
- 'To reduce this time, open a workspace directory with fewer files ' +
1426
- 'or add a pyrightconfig.json configuration file with an "exclude" section to exclude ' +
1427
- 'subdirectories from your workspace. For more details, refer to ' +
1428
- 'https://github.com/microsoft/pyright/blob/main/docs/configuration.md.'
1429
- ) ;
1430
-
1431
- // Show it in message box if it is supported.
1432
- this . _tryShowLongOperationMessageBox ( ) ;
1433
-
1434
- loggedLongOperationError = true ;
1435
- }
1436
- }
1437
-
1438
- if ( this . _configOptions . autoExcludeVenv ) {
1439
- if ( envMarkers . some ( ( f ) => this . fs . existsSync ( absolutePath . resolvePaths ( ...f ) ) ) ) {
1440
- // Save auto exclude paths in the configOptions once we found them.
1441
- if ( ! FileSpec . isInPath ( absolutePath , exclude ) ) {
1442
- exclude . push ( getFileSpec ( this . _configOptions . projectRoot , `${ absolutePath } /**` ) ) ;
1443
- }
1444
-
1445
- this . _console . info ( `Auto-excluding ${ absolutePath . toUserVisibleString ( ) } ` ) ;
1446
- return ;
1447
- }
1448
- }
1449
-
1450
- const { files, directories } = getFileSystemEntries ( this . fs , absolutePath ) ;
1451
-
1452
- for ( const filePath of files ) {
1453
- if ( FileSpec . matchIncludeFileSpec ( includeRegExp , exclude , filePath ) ) {
1454
- nFilesVisited ++ ;
1455
- results . push ( filePath ) ;
1456
- }
1457
- }
1458
-
1459
- for ( const dirPath of directories ) {
1460
- if ( dirPath . matchesRegex ( includeRegExp ) || hasDirectoryWildcard ) {
1461
- if ( ! FileSpec . isInPath ( dirPath , exclude ) ) {
1462
- visitDirectory ( dirPath , includeRegExp , hasDirectoryWildcard ) ;
1463
- }
1464
- }
1465
- }
1466
- } ;
1467
-
1468
- const seenDirs = new Set < string > ( ) ;
1469
- const visitDirectory = ( absolutePath : Uri , includeRegExp : RegExp , hasDirectoryWildcard : boolean ) => {
1470
- const realDirPath = tryRealpath ( this . fs , absolutePath ) ;
1471
- if ( ! realDirPath ) {
1472
- this . _console . warn ( `Skipping broken link "${ absolutePath } "` ) ;
1473
- return ;
1474
- }
1475
-
1476
- if ( seenDirs . has ( realDirPath . key ) ) {
1477
- this . _console . warn ( `Skipping recursive symlink "${ absolutePath } " -> "${ realDirPath } "` ) ;
1478
- return ;
1479
- }
1480
- seenDirs . add ( realDirPath . key ) ;
1481
-
1482
- try {
1483
- visitDirectoryUnchecked ( absolutePath , includeRegExp , hasDirectoryWildcard ) ;
1484
- } finally {
1485
- seenDirs . delete ( realDirPath . key ) ;
1486
- }
1487
- } ;
1488
-
1489
- include . forEach ( ( includeSpec ) => {
1490
- if ( ! FileSpec . isInPath ( includeSpec . wildcardRoot , exclude ) ) {
1491
- let foundFileSpec = false ;
1492
-
1493
- const stat = tryStat ( this . fs , includeSpec . wildcardRoot ) ;
1494
- if ( stat ?. isFile ( ) ) {
1495
- results . push ( includeSpec . wildcardRoot ) ;
1496
- foundFileSpec = true ;
1497
- } else if ( stat ?. isDirectory ( ) ) {
1498
- visitDirectory ( includeSpec . wildcardRoot , includeSpec . regExp , includeSpec . hasDirectoryWildcard ) ;
1499
- foundFileSpec = true ;
1500
- }
1501
-
1502
- if ( ! foundFileSpec ) {
1503
- this . _console . error (
1504
- `File or directory "${ includeSpec . wildcardRoot . toUserVisibleString ( ) } " does not exist.`
1505
- ) ;
1506
- }
1507
- }
1508
- } ) ;
1509
-
1510
- return results ;
1511
1437
}
1512
1438
1513
1439
private _removeSourceFileWatchers ( ) {
0 commit comments