mirror of
https://github.com/microsoft/pyright.git
synced 2024-08-17 03:40:44 +03:00
Extensibility updates in threading and fourslash, add completion context (#995)
This commit is contained in:
parent
e71ad7d23a
commit
9e231b1292
@ -4,7 +4,7 @@ Pyright offers flexible configuration options specified in a JSON-formatted text
|
||||
|
||||
Relative paths specified within the config file are relative to the config file’s location. Paths with shell variables (including `~`) are not supported.
|
||||
|
||||
## Master Pyright Config Options
|
||||
## Main Pyright Config Options
|
||||
|
||||
**include** [array of paths, optional]: Paths of directories or files that should be included. If no paths are specified, pyright defaults to the directory that contains the config file. Paths may contain wildcard characters ** (a directory or multiple levels of directories), * (a sequence of zero or more characters), or ? (a single character). If no include paths are specified, the root path for the workspace is assumed.
|
||||
|
||||
|
@ -264,3 +264,12 @@ export class BackgroundAnalysisProgram {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type BackgroundAnalysisProgramFactory = (
|
||||
console: ConsoleInterface,
|
||||
configOptions: ConfigOptions,
|
||||
importResolver: ImportResolver,
|
||||
extension?: LanguageServiceExtension,
|
||||
backgroundAnalysis?: BackgroundAnalysisBase,
|
||||
maxAnalysisTime?: MaxAnalysisTime
|
||||
) => BackgroundAnalysisProgram;
|
||||
|
@ -8,13 +8,7 @@
|
||||
* and all of their recursive imports.
|
||||
*/
|
||||
|
||||
import {
|
||||
CancellationToken,
|
||||
CompletionItem,
|
||||
CompletionList,
|
||||
DocumentSymbol,
|
||||
SymbolInformation,
|
||||
} from 'vscode-languageserver';
|
||||
import { CancellationToken, CompletionItem, DocumentSymbol, SymbolInformation } from 'vscode-languageserver';
|
||||
import {
|
||||
CallHierarchyIncomingCall,
|
||||
CallHierarchyItem,
|
||||
@ -51,6 +45,7 @@ import {
|
||||
ModuleSymbolMap,
|
||||
} from '../languageService/autoImporter';
|
||||
import { CallHierarchyProvider } from '../languageService/callHierarchyProvider';
|
||||
import { CompletionResults } from '../languageService/completionProvider';
|
||||
import { IndexResults } from '../languageService/documentSymbolProvider';
|
||||
import { HoverResults } from '../languageService/hoverProvider';
|
||||
import { ReferencesResult } from '../languageService/referencesProvider';
|
||||
@ -149,6 +144,10 @@ export class Program {
|
||||
this._createNewEvaluator();
|
||||
}
|
||||
|
||||
get evaluator(): TypeEvaluator | undefined {
|
||||
return this._evaluator;
|
||||
}
|
||||
|
||||
setConfigOptions(configOptions: ConfigOptions) {
|
||||
this._configOptions = configOptions;
|
||||
|
||||
@ -1251,13 +1250,13 @@ export class Program {
|
||||
workspacePath: string,
|
||||
libraryMap: Map<string, IndexResults> | undefined,
|
||||
token: CancellationToken
|
||||
): Promise<CompletionList | undefined> {
|
||||
): Promise<CompletionResults | undefined> {
|
||||
const sourceFileInfo = this._sourceFileMap.get(filePath);
|
||||
if (!sourceFileInfo) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let completionList = this._runEvaluatorWithCancellationToken(token, () => {
|
||||
const completionResult = this._runEvaluatorWithCancellationToken(token, () => {
|
||||
this._bindFile(sourceFileInfo);
|
||||
|
||||
const execEnv = this._configOptions.findExecEnvironment(filePath);
|
||||
@ -1275,8 +1274,8 @@ export class Program {
|
||||
);
|
||||
});
|
||||
|
||||
if (!completionList || !this._extension?.completionListExtension) {
|
||||
return completionList;
|
||||
if (!completionResult?.completionList || !this._extension?.completionListExtension) {
|
||||
return completionResult;
|
||||
}
|
||||
|
||||
const pr = sourceFileInfo.sourceFile.getParseResults();
|
||||
@ -1284,8 +1283,8 @@ export class Program {
|
||||
if (pr?.parseTree && content) {
|
||||
const offset = convertPositionToOffset(position, pr.tokenizerOutput.lines);
|
||||
if (offset !== undefined) {
|
||||
completionList = await this._extension.completionListExtension.updateCompletionList(
|
||||
completionList,
|
||||
completionResult.completionList = await this._extension.completionListExtension.updateCompletionList(
|
||||
completionResult.completionList,
|
||||
pr.parseTree,
|
||||
content,
|
||||
offset,
|
||||
@ -1295,7 +1294,7 @@ export class Program {
|
||||
}
|
||||
}
|
||||
|
||||
return completionList;
|
||||
return completionResult;
|
||||
}
|
||||
|
||||
resolveCompletionItem(
|
||||
|
@ -12,7 +12,6 @@ import {
|
||||
AbstractCancellationTokenSource,
|
||||
CancellationToken,
|
||||
CompletionItem,
|
||||
CompletionList,
|
||||
DocumentSymbol,
|
||||
SymbolInformation,
|
||||
} from 'vscode-languageserver';
|
||||
@ -46,11 +45,12 @@ import {
|
||||
} from '../common/pathUtils';
|
||||
import { DocumentRange, Position, Range } from '../common/textRange';
|
||||
import { timingStats } from '../common/timing';
|
||||
import { CompletionResults } from '../languageService/completionProvider';
|
||||
import { IndexResults } from '../languageService/documentSymbolProvider';
|
||||
import { HoverResults } from '../languageService/hoverProvider';
|
||||
import { SignatureHelpResults } from '../languageService/signatureHelpProvider';
|
||||
import { AnalysisCompleteCallback } from './analysis';
|
||||
import { BackgroundAnalysisProgram } from './backgroundAnalysisProgram';
|
||||
import { BackgroundAnalysisProgram, BackgroundAnalysisProgramFactory } from './backgroundAnalysisProgram';
|
||||
import { ImportedModuleDescriptor, ImportResolver, ImportResolverFactory } from './importResolver';
|
||||
import { MaxAnalysisTime } from './program';
|
||||
import { findPythonSearchPaths, getPythonPathFromPythonInterpreter } from './pythonPathUtils';
|
||||
@ -83,6 +83,7 @@ export class AnalyzerService {
|
||||
private _backgroundAnalysisProgram: BackgroundAnalysisProgram;
|
||||
private _backgroundAnalysisCancellationSource: AbstractCancellationTokenSource | undefined;
|
||||
private _maxAnalysisTimeInForeground?: MaxAnalysisTime;
|
||||
private _backgroundAnalysisProgramFactory?: BackgroundAnalysisProgramFactory;
|
||||
private _disposed = false;
|
||||
|
||||
constructor(
|
||||
@ -93,7 +94,8 @@ export class AnalyzerService {
|
||||
configOptions?: ConfigOptions,
|
||||
extension?: LanguageServiceExtension,
|
||||
backgroundAnalysis?: BackgroundAnalysisBase,
|
||||
maxAnalysisTime?: MaxAnalysisTime
|
||||
maxAnalysisTime?: MaxAnalysisTime,
|
||||
backgroundAnalysisProgramFactory?: BackgroundAnalysisProgramFactory
|
||||
) {
|
||||
this._instanceName = instanceName;
|
||||
this._console = console || new StandardConsole();
|
||||
@ -101,17 +103,29 @@ export class AnalyzerService {
|
||||
this._extension = extension;
|
||||
this._importResolverFactory = importResolverFactory || AnalyzerService.createImportResolver;
|
||||
this._maxAnalysisTimeInForeground = maxAnalysisTime;
|
||||
this._backgroundAnalysisProgramFactory = backgroundAnalysisProgramFactory;
|
||||
|
||||
configOptions = configOptions ?? new ConfigOptions(process.cwd());
|
||||
const importResolver = this._importResolverFactory(fs, configOptions);
|
||||
this._backgroundAnalysisProgram = new BackgroundAnalysisProgram(
|
||||
this._console,
|
||||
configOptions,
|
||||
importResolver,
|
||||
this._extension,
|
||||
backgroundAnalysis,
|
||||
this._maxAnalysisTimeInForeground
|
||||
);
|
||||
|
||||
this._backgroundAnalysisProgram =
|
||||
backgroundAnalysisProgramFactory !== undefined
|
||||
? backgroundAnalysisProgramFactory(
|
||||
this._console,
|
||||
configOptions,
|
||||
importResolver,
|
||||
this._extension,
|
||||
backgroundAnalysis,
|
||||
this._maxAnalysisTimeInForeground
|
||||
)
|
||||
: new BackgroundAnalysisProgram(
|
||||
this._console,
|
||||
configOptions,
|
||||
importResolver,
|
||||
this._extension,
|
||||
backgroundAnalysis,
|
||||
this._maxAnalysisTimeInForeground
|
||||
);
|
||||
}
|
||||
|
||||
clone(instanceName: string, backgroundAnalysis?: BackgroundAnalysisBase): AnalyzerService {
|
||||
@ -123,7 +137,8 @@ export class AnalyzerService {
|
||||
this._backgroundAnalysisProgram.configOptions,
|
||||
this._extension,
|
||||
backgroundAnalysis,
|
||||
this._maxAnalysisTimeInForeground
|
||||
this._maxAnalysisTimeInForeground,
|
||||
this._backgroundAnalysisProgramFactory
|
||||
);
|
||||
}
|
||||
|
||||
@ -137,6 +152,10 @@ export class AnalyzerService {
|
||||
this._clearLibraryReanalysisTimer();
|
||||
}
|
||||
|
||||
get backgroundAnalysisProgram(): BackgroundAnalysisProgram {
|
||||
return this._backgroundAnalysisProgram;
|
||||
}
|
||||
|
||||
static createImportResolver(fs: FileSystem, options: ConfigOptions): ImportResolver {
|
||||
return new ImportResolver(fs, options);
|
||||
}
|
||||
@ -263,7 +282,7 @@ export class AnalyzerService {
|
||||
position: Position,
|
||||
workspacePath: string,
|
||||
token: CancellationToken
|
||||
): Promise<CompletionList | undefined> {
|
||||
): Promise<CompletionResults | undefined> {
|
||||
return this._program.getCompletionsForPosition(
|
||||
filePath,
|
||||
position,
|
||||
|
@ -10,7 +10,6 @@
|
||||
import {
|
||||
CancellationToken,
|
||||
CompletionItem,
|
||||
CompletionList,
|
||||
DocumentHighlight,
|
||||
DocumentSymbol,
|
||||
SymbolInformation,
|
||||
@ -33,6 +32,7 @@ import { DocumentRange, getEmptyRange, Position, TextRange } from '../common/tex
|
||||
import { TextRangeCollection } from '../common/textRangeCollection';
|
||||
import { timingStats } from '../common/timing';
|
||||
import { ModuleSymbolMap } from '../languageService/autoImporter';
|
||||
import { CompletionResults } from '../languageService/completionProvider';
|
||||
import { CompletionItemData, CompletionProvider } from '../languageService/completionProvider';
|
||||
import { DefinitionProvider } from '../languageService/definitionProvider';
|
||||
import { DocumentHighlightProvider } from '../languageService/documentHighlightProvider';
|
||||
@ -777,7 +777,7 @@ export class SourceFile {
|
||||
libraryMap: Map<string, IndexResults> | undefined,
|
||||
moduleSymbolsCallback: () => ModuleSymbolMap,
|
||||
token: CancellationToken
|
||||
): CompletionList | undefined {
|
||||
): CompletionResults | undefined {
|
||||
// If we have no completed analysis job, there's nothing to do.
|
||||
if (!this._parseResults) {
|
||||
return undefined;
|
||||
|
@ -6,7 +6,7 @@
|
||||
* run analyzer from background thread
|
||||
*/
|
||||
|
||||
import { CancellationToken } from 'vscode-languageserver';
|
||||
import { CancellationToken } from 'vscode-languageserver/node';
|
||||
import { MessageChannel, MessagePort, parentPort, threadId, Worker, workerData } from 'worker_threads';
|
||||
|
||||
import { AnalysisCompleteCallback, AnalysisResults, analyzeProgram, nullCallback } from './analyzer/analysis';
|
||||
@ -79,34 +79,42 @@ export class BackgroundAnalysisBase {
|
||||
}
|
||||
|
||||
setConfigOptions(configOptions: ConfigOptions) {
|
||||
this._enqueueRequest({ requestType: 'setConfigOptions', data: configOptions });
|
||||
this.enqueueRequest({ requestType: 'setConfigOptions', data: configOptions });
|
||||
}
|
||||
|
||||
setTrackedFiles(filePaths: string[]) {
|
||||
this._enqueueRequest({ requestType: 'setTrackedFiles', data: filePaths });
|
||||
this.enqueueRequest({ requestType: 'setTrackedFiles', data: filePaths });
|
||||
}
|
||||
|
||||
setAllowedThirdPartyImports(importNames: string[]) {
|
||||
this._enqueueRequest({ requestType: 'setAllowedThirdPartyImports', data: importNames });
|
||||
this.enqueueRequest({ requestType: 'setAllowedThirdPartyImports', data: importNames });
|
||||
}
|
||||
|
||||
setFileOpened(filePath: string, version: number | null, contents: string) {
|
||||
this._enqueueRequest({ requestType: 'setFileOpened', data: { filePath, version, contents } });
|
||||
this.enqueueRequest({ requestType: 'setFileOpened', data: { filePath, version, contents } });
|
||||
}
|
||||
|
||||
setFileClosed(filePath: string) {
|
||||
this._enqueueRequest({ requestType: 'setFileClosed', data: filePath });
|
||||
this.enqueueRequest({ requestType: 'setFileClosed', data: filePath });
|
||||
}
|
||||
|
||||
markAllFilesDirty(evenIfContentsAreSame: boolean) {
|
||||
this._enqueueRequest({ requestType: 'markAllFilesDirty', data: evenIfContentsAreSame });
|
||||
this.enqueueRequest({ requestType: 'markAllFilesDirty', data: evenIfContentsAreSame });
|
||||
}
|
||||
|
||||
markFilesDirty(filePaths: string[], evenIfContentsAreSame: boolean) {
|
||||
this._enqueueRequest({ requestType: 'markFilesDirty', data: { filePaths, evenIfContentsAreSame } });
|
||||
this.enqueueRequest({ requestType: 'markFilesDirty', data: { filePaths, evenIfContentsAreSame } });
|
||||
}
|
||||
|
||||
startAnalysis(indices: Indices | undefined, token: CancellationToken) {
|
||||
this._startOrResumeAnalysis('analyze', indices, token);
|
||||
}
|
||||
|
||||
private _startOrResumeAnalysis(
|
||||
requestType: 'analyze' | 'resumeAnalysis',
|
||||
indices: Indices | undefined,
|
||||
token: CancellationToken
|
||||
) {
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
|
||||
// Handle response from background thread to main thread.
|
||||
@ -117,6 +125,17 @@ export class BackgroundAnalysisBase {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'analysisPaused': {
|
||||
disposeCancellationToken(token);
|
||||
port2.close();
|
||||
port1.close();
|
||||
|
||||
// Analysis request has completed, but there is more to
|
||||
// analyze, so queue another message to resume later.
|
||||
this._startOrResumeAnalysis('resumeAnalysis', indices, token);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'indexResult': {
|
||||
const { path, indexResults } = msg.data;
|
||||
indices?.setWorkspaceIndex(path, indexResults);
|
||||
@ -136,7 +155,7 @@ export class BackgroundAnalysisBase {
|
||||
});
|
||||
|
||||
const cancellationId = getCancellationTokenId(token);
|
||||
this._enqueueRequest({ requestType: 'analyze', data: cancellationId, port: port2 });
|
||||
this.enqueueRequest({ requestType, data: cancellationId, port: port2 });
|
||||
}
|
||||
|
||||
startIndexing(configOptions: ConfigOptions, indices: Indices) {
|
||||
@ -158,7 +177,7 @@ export class BackgroundAnalysisBase {
|
||||
const waiter = getBackgroundWaiter<Diagnostic[]>(port1);
|
||||
|
||||
const cancellationId = getCancellationTokenId(token);
|
||||
this._enqueueRequest({
|
||||
this.enqueueRequest({
|
||||
requestType: 'getDiagnosticsForRange',
|
||||
data: { filePath, range, cancellationId },
|
||||
port: port2,
|
||||
@ -184,7 +203,7 @@ export class BackgroundAnalysisBase {
|
||||
const waiter = getBackgroundWaiter(port1);
|
||||
|
||||
const cancellationId = getCancellationTokenId(token);
|
||||
this._enqueueRequest({
|
||||
this.enqueueRequest({
|
||||
requestType: 'writeTypeStub',
|
||||
data: { targetImportPath, targetIsSingleFile, stubPath, cancellationId },
|
||||
port: port2,
|
||||
@ -197,14 +216,14 @@ export class BackgroundAnalysisBase {
|
||||
}
|
||||
|
||||
invalidateAndForceReanalysis() {
|
||||
this._enqueueRequest({ requestType: 'invalidateAndForceReanalysis', data: null });
|
||||
this.enqueueRequest({ requestType: 'invalidateAndForceReanalysis', data: null });
|
||||
}
|
||||
|
||||
restart() {
|
||||
this._enqueueRequest({ requestType: 'restart', data: null });
|
||||
this.enqueueRequest({ requestType: 'restart', data: null });
|
||||
}
|
||||
|
||||
private _enqueueRequest(request: AnalysisRequest) {
|
||||
protected enqueueRequest(request: AnalysisRequest) {
|
||||
if (this._worker) {
|
||||
this._worker.postMessage(request, request.port ? [request.port] : undefined);
|
||||
}
|
||||
@ -218,7 +237,11 @@ export class BackgroundAnalysisBase {
|
||||
export class BackgroundAnalysisRunnerBase extends BackgroundThreadBase {
|
||||
private _configOptions: ConfigOptions;
|
||||
private _importResolver: ImportResolver;
|
||||
protected program: Program;
|
||||
private _program: Program;
|
||||
|
||||
get program(): Program {
|
||||
return this._program;
|
||||
}
|
||||
|
||||
protected constructor(private _extension?: LanguageServiceExtension) {
|
||||
super(workerData as InitializationData);
|
||||
@ -229,7 +252,7 @@ export class BackgroundAnalysisRunnerBase extends BackgroundThreadBase {
|
||||
|
||||
this._configOptions = new ConfigOptions(data.rootDirectory);
|
||||
this._importResolver = this.createImportResolver(this.fs, this._configOptions);
|
||||
this.program = new Program(
|
||||
this._program = new Program(
|
||||
this._importResolver,
|
||||
this._configOptions,
|
||||
this.getConsole(),
|
||||
@ -242,139 +265,7 @@ export class BackgroundAnalysisRunnerBase extends BackgroundThreadBase {
|
||||
this.log(LogLevel.Info, `Background analysis(${threadId}) started`);
|
||||
|
||||
// Get requests from main thread.
|
||||
parentPort?.on('message', (msg: AnalysisRequest) => {
|
||||
switch (msg.requestType) {
|
||||
case 'analyze': {
|
||||
const port = msg.port!;
|
||||
const token = getCancellationTokenFromId(msg.data);
|
||||
|
||||
// Report files to analyze first.
|
||||
const filesLeftToAnalyze = this.program.getFilesToAnalyzeCount();
|
||||
|
||||
this._onAnalysisCompletion(port, {
|
||||
diagnostics: [],
|
||||
filesInProgram: this.program.getFileCount(),
|
||||
filesRequiringAnalysis: filesLeftToAnalyze,
|
||||
checkingOnlyOpenFiles: this.program.isCheckingOnlyOpenFiles(),
|
||||
fatalErrorOccurred: false,
|
||||
configParseErrorOccurred: false,
|
||||
elapsedTime: 0,
|
||||
});
|
||||
|
||||
// Report results at the interval of the max analysis time.
|
||||
const maxTime = { openFilesTimeInMs: 50, noOpenFilesTimeInMs: 200 };
|
||||
let moreToAnalyze = true;
|
||||
|
||||
while (moreToAnalyze) {
|
||||
moreToAnalyze = analyzeProgram(
|
||||
this.program,
|
||||
maxTime,
|
||||
this._configOptions,
|
||||
(result) => this._onAnalysisCompletion(port, result),
|
||||
this.getConsole(),
|
||||
token
|
||||
);
|
||||
}
|
||||
|
||||
this.processIndexing(port, token);
|
||||
|
||||
this._analysisDone(port, msg.data);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'getDiagnosticsForRange': {
|
||||
run(() => {
|
||||
const { filePath, range, cancellationId } = msg.data;
|
||||
const token = getCancellationTokenFromId(cancellationId);
|
||||
throwIfCancellationRequested(token);
|
||||
|
||||
return this.program.getDiagnosticsForRange(filePath, range);
|
||||
}, msg.port!);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'writeTypeStub': {
|
||||
run(() => {
|
||||
const { targetImportPath, targetIsSingleFile, stubPath, cancellationId } = msg.data;
|
||||
const token = getCancellationTokenFromId(cancellationId);
|
||||
|
||||
analyzeProgram(
|
||||
this.program,
|
||||
undefined,
|
||||
this._configOptions,
|
||||
nullCallback,
|
||||
this.getConsole(),
|
||||
token
|
||||
);
|
||||
this.program.writeTypeStub(targetImportPath, targetIsSingleFile, stubPath, token);
|
||||
}, msg.port!);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'setConfigOptions': {
|
||||
this._configOptions = createConfigOptionsFrom(msg.data);
|
||||
this._importResolver = this.createImportResolver(this.fs, this._configOptions);
|
||||
this.program.setConfigOptions(this._configOptions);
|
||||
this.program.setImportResolver(this._importResolver);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'setTrackedFiles': {
|
||||
const diagnostics = this.program.setTrackedFiles(msg.data);
|
||||
this._reportDiagnostics(diagnostics, this.program.getFilesToAnalyzeCount(), 0);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'setAllowedThirdPartyImports': {
|
||||
this.program.setAllowedThirdPartyImports(msg.data);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'setFileOpened': {
|
||||
const { filePath, version, contents } = msg.data;
|
||||
this.program.setFileOpened(filePath, version, contents);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'setFileClosed': {
|
||||
const diagnostics = this.program.setFileClosed(msg.data);
|
||||
this._reportDiagnostics(diagnostics, this.program.getFilesToAnalyzeCount(), 0);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'markAllFilesDirty': {
|
||||
this.program.markAllFilesDirty(msg.data);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'markFilesDirty': {
|
||||
const { filePaths, evenIfContentsAreSame } = msg.data;
|
||||
this.program.markFilesDirty(filePaths, evenIfContentsAreSame);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'invalidateAndForceReanalysis': {
|
||||
// Make sure the import resolver doesn't have invalid
|
||||
// cached entries.
|
||||
this._importResolver.invalidateCache();
|
||||
|
||||
// Mark all files with one or more errors dirty.
|
||||
this.program.markAllFilesDirty(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'restart': {
|
||||
// recycle import resolver
|
||||
this._importResolver = this.createImportResolver(this.fs, this._configOptions);
|
||||
this.program.setImportResolver(this._importResolver);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
debug.fail(`${msg.requestType} is not expected`);
|
||||
}
|
||||
}
|
||||
});
|
||||
parentPort?.on('message', (msg: AnalysisRequest) => this.onMessage(msg));
|
||||
|
||||
parentPort?.on('error', (msg) => debug.fail(`failed ${msg}`));
|
||||
parentPort?.on('exit', (c) => {
|
||||
@ -384,6 +275,155 @@ export class BackgroundAnalysisRunnerBase extends BackgroundThreadBase {
|
||||
});
|
||||
}
|
||||
|
||||
protected onMessage(msg: AnalysisRequest) {
|
||||
this.log(LogLevel.Info, `Background analysis message: ${msg.requestType}`);
|
||||
switch (msg.requestType) {
|
||||
case 'analyze': {
|
||||
const port = msg.port!;
|
||||
const token = getCancellationTokenFromId(msg.data);
|
||||
|
||||
// Report files to analyze first.
|
||||
const filesLeftToAnalyze = this.program.getFilesToAnalyzeCount();
|
||||
|
||||
this._onAnalysisCompletion(port, {
|
||||
diagnostics: [],
|
||||
filesInProgram: this.program.getFileCount(),
|
||||
filesRequiringAnalysis: filesLeftToAnalyze,
|
||||
checkingOnlyOpenFiles: this.program.isCheckingOnlyOpenFiles(),
|
||||
fatalErrorOccurred: false,
|
||||
configParseErrorOccurred: false,
|
||||
elapsedTime: 0,
|
||||
});
|
||||
|
||||
this._analyzeOneChunk(port, token, msg);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'resumeAnalysis': {
|
||||
const port = msg.port!;
|
||||
const token = getCancellationTokenFromId(msg.data);
|
||||
|
||||
this._analyzeOneChunk(port, token, msg);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'getDiagnosticsForRange': {
|
||||
run(() => {
|
||||
const { filePath, range, cancellationId } = msg.data;
|
||||
const token = getCancellationTokenFromId(cancellationId);
|
||||
throwIfCancellationRequested(token);
|
||||
|
||||
return this.program.getDiagnosticsForRange(filePath, range);
|
||||
}, msg.port!);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'writeTypeStub': {
|
||||
run(() => {
|
||||
const { targetImportPath, targetIsSingleFile, stubPath, cancellationId } = msg.data;
|
||||
const token = getCancellationTokenFromId(cancellationId);
|
||||
|
||||
analyzeProgram(
|
||||
this.program,
|
||||
undefined,
|
||||
this._configOptions,
|
||||
nullCallback,
|
||||
this.getConsole(),
|
||||
token
|
||||
);
|
||||
this.program.writeTypeStub(targetImportPath, targetIsSingleFile, stubPath, token);
|
||||
}, msg.port!);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'setConfigOptions': {
|
||||
this._configOptions = createConfigOptionsFrom(msg.data);
|
||||
this._importResolver = this.createImportResolver(this.fs, this._configOptions);
|
||||
this.program.setConfigOptions(this._configOptions);
|
||||
this.program.setImportResolver(this._importResolver);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'setTrackedFiles': {
|
||||
const diagnostics = this.program.setTrackedFiles(msg.data);
|
||||
this._reportDiagnostics(diagnostics, this.program.getFilesToAnalyzeCount(), 0);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'setAllowedThirdPartyImports': {
|
||||
this.program.setAllowedThirdPartyImports(msg.data);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'setFileOpened': {
|
||||
const { filePath, version, contents } = msg.data;
|
||||
this.program.setFileOpened(filePath, version, contents);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'setFileClosed': {
|
||||
const diagnostics = this.program.setFileClosed(msg.data);
|
||||
this._reportDiagnostics(diagnostics, this.program.getFilesToAnalyzeCount(), 0);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'markAllFilesDirty': {
|
||||
this.program.markAllFilesDirty(msg.data);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'markFilesDirty': {
|
||||
const { filePaths, evenIfContentsAreSame } = msg.data;
|
||||
this.program.markFilesDirty(filePaths, evenIfContentsAreSame);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'invalidateAndForceReanalysis': {
|
||||
// Make sure the import resolver doesn't have invalid
|
||||
// cached entries.
|
||||
this._importResolver.invalidateCache();
|
||||
|
||||
// Mark all files with one or more errors dirty.
|
||||
this.program.markAllFilesDirty(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'restart': {
|
||||
// recycle import resolver
|
||||
this._importResolver = this.createImportResolver(this.fs, this._configOptions);
|
||||
this.program.setImportResolver(this._importResolver);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
debug.fail(`${msg.requestType} is not expected`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _analyzeOneChunk(port: MessagePort, token: CancellationToken, msg: AnalysisRequest) {
|
||||
// Report results at the interval of the max analysis time.
|
||||
const maxTime = { openFilesTimeInMs: 50, noOpenFilesTimeInMs: 200 };
|
||||
const moreToAnalyze = analyzeProgram(
|
||||
this.program,
|
||||
maxTime,
|
||||
this._configOptions,
|
||||
(result) => this._onAnalysisCompletion(port, result),
|
||||
this.getConsole(),
|
||||
token
|
||||
);
|
||||
|
||||
if (moreToAnalyze) {
|
||||
// There's more to analyze after we exceeded max time,
|
||||
// so report that we are paused. The foreground thread will
|
||||
// then queue up a message to resume the analysis.
|
||||
this._analysisPaused(port, msg.data);
|
||||
} else {
|
||||
this.processIndexing(port, token);
|
||||
this._analysisDone(port, msg.data);
|
||||
}
|
||||
}
|
||||
|
||||
protected createImportResolver(fs: FileSystem, options: ConfigOptions): ImportResolver {
|
||||
return new ImportResolver(fs, options);
|
||||
}
|
||||
@ -414,6 +454,10 @@ export class BackgroundAnalysisRunnerBase extends BackgroundThreadBase {
|
||||
port.postMessage({ requestType: 'analysisResult', data: result });
|
||||
}
|
||||
|
||||
private _analysisPaused(port: MessagePort, cancellationId: string) {
|
||||
port.postMessage({ requestType: 'analysisPaused', data: cancellationId });
|
||||
}
|
||||
|
||||
private _analysisDone(port: MessagePort, cancellationId: string) {
|
||||
port.postMessage({ requestType: 'analysisDone', data: cancellationId });
|
||||
}
|
||||
@ -461,9 +505,10 @@ export interface InitializationData {
|
||||
runner?: string;
|
||||
}
|
||||
|
||||
interface AnalysisRequest {
|
||||
export interface AnalysisRequest {
|
||||
requestType:
|
||||
| 'analyze'
|
||||
| 'resumeAnalysis'
|
||||
| 'setConfigOptions'
|
||||
| 'setTrackedFiles'
|
||||
| 'setAllowedThirdPartyImports'
|
||||
@ -474,13 +519,14 @@ interface AnalysisRequest {
|
||||
| 'invalidateAndForceReanalysis'
|
||||
| 'restart'
|
||||
| 'getDiagnosticsForRange'
|
||||
| 'writeTypeStub';
|
||||
| 'writeTypeStub'
|
||||
| 'getSemanticTokens';
|
||||
|
||||
data: any;
|
||||
port?: MessagePort;
|
||||
}
|
||||
|
||||
interface AnalysisResponse {
|
||||
requestType: 'log' | 'analysisResult' | 'indexResult' | 'analysisDone';
|
||||
requestType: 'log' | 'analysisResult' | 'analysisPaused' | 'indexResult' | 'analysisDone';
|
||||
data: any;
|
||||
}
|
||||
|
@ -44,9 +44,11 @@ import {
|
||||
WatchKind,
|
||||
WorkDoneProgressReporter,
|
||||
WorkspaceEdit,
|
||||
WorkspaceFolder,
|
||||
} from 'vscode-languageserver/node';
|
||||
|
||||
import { AnalysisResults } from './analyzer/analysis';
|
||||
import { BackgroundAnalysisProgram } from './analyzer/backgroundAnalysisProgram';
|
||||
import { ImportResolver } from './analyzer/importResolver';
|
||||
import { MaxAnalysisTime } from './analyzer/program';
|
||||
import { AnalyzerService, configFileNames } from './analyzer/service';
|
||||
@ -76,7 +78,7 @@ import { ProgressReporter, ProgressReportTracker } from './common/progressReport
|
||||
import { convertWorkspaceEdits } from './common/textEditUtils';
|
||||
import { Position } from './common/textRange';
|
||||
import { AnalyzerServiceExecutor } from './languageService/analyzerServiceExecutor';
|
||||
import { CompletionItemData } from './languageService/completionProvider';
|
||||
import { CompletionItemData, CompletionResults } from './languageService/completionProvider';
|
||||
import { convertHoverResults } from './languageService/hoverProvider';
|
||||
import { Localizer } from './localization/localize';
|
||||
import { WorkspaceMap } from './workspaceMap';
|
||||
@ -215,7 +217,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
|
||||
this._workspaceMap = new WorkspaceMap(this);
|
||||
|
||||
// Set up callbacks.
|
||||
this._setupConnection(_serverOptions.supportedCommands ?? [], _serverOptions.supportedCodeActions ?? []);
|
||||
this.setupConnection(_serverOptions.supportedCommands ?? [], _serverOptions.supportedCodeActions ?? []);
|
||||
|
||||
this._progressReporter = new ProgressReportTracker(
|
||||
this._serverOptions.progressReporterFactory
|
||||
@ -287,6 +289,24 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
|
||||
return new ImportResolver(fs, options);
|
||||
}
|
||||
|
||||
protected createBackgroundAnalysisProgram(
|
||||
console: ConsoleInterface,
|
||||
configOptions: ConfigOptions,
|
||||
importResolver: ImportResolver,
|
||||
extension?: LanguageServiceExtension,
|
||||
backgroundAnalysis?: BackgroundAnalysisBase,
|
||||
maxAnalysisTime?: MaxAnalysisTime
|
||||
): BackgroundAnalysisProgram {
|
||||
return new BackgroundAnalysisProgram(
|
||||
console,
|
||||
configOptions,
|
||||
importResolver,
|
||||
extension,
|
||||
backgroundAnalysis,
|
||||
maxAnalysisTime
|
||||
);
|
||||
}
|
||||
|
||||
protected setExtension(extension: any): void {
|
||||
this._serverOptions.extension = extension;
|
||||
}
|
||||
@ -308,7 +328,8 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
|
||||
undefined,
|
||||
this._serverOptions.extension,
|
||||
this.createBackgroundAnalysis(),
|
||||
this._serverOptions.maxAnalysisTimeInForeground
|
||||
this._serverOptions.maxAnalysisTimeInForeground,
|
||||
this.createBackgroundAnalysisProgram.bind(this)
|
||||
);
|
||||
|
||||
service.setCompletionCallback((results) => this.onAnalysisCompletedHandler(results));
|
||||
@ -380,7 +401,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
|
||||
return fileWatcher;
|
||||
}
|
||||
|
||||
private _setupConnection(supportedCommands: string[], supportedCodeActions: string[]): void {
|
||||
protected setupConnection(supportedCommands: string[], supportedCodeActions: string[]): void {
|
||||
// After the server has started the client sends an initialize request. The server receives
|
||||
// in the passed params the rootPath of the workspace plus the client capabilities.
|
||||
this._connection.onInitialize((params) => this.initialize(params, supportedCommands, supportedCodeActions));
|
||||
@ -598,6 +619,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
|
||||
});
|
||||
|
||||
this._connection.onCompletion((params, token) => this.onCompletion(params, token));
|
||||
|
||||
this._connection.onCompletionResolve(async (params, token) => {
|
||||
// Cancellation bugs in vscode and LSP:
|
||||
// https://github.com/microsoft/vscode-languageserver-node/issues/615
|
||||
@ -762,15 +784,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
|
||||
|
||||
event.added.forEach(async (workspace) => {
|
||||
const rootPath = convertUriToPath(workspace.uri);
|
||||
const newWorkspace: WorkspaceServiceInstance = {
|
||||
workspaceName: workspace.name,
|
||||
rootPath,
|
||||
rootUri: workspace.uri,
|
||||
serviceInstance: this.createAnalyzerService(workspace.name),
|
||||
disableLanguageServices: false,
|
||||
disableOrganizeImports: false,
|
||||
isInitialized: createDeferred<boolean>(),
|
||||
};
|
||||
const newWorkspace = this.createWorkspaceServiceInstance(workspace, rootPath);
|
||||
this._workspaceMap.set(rootPath, newWorkspace);
|
||||
await this.updateSettingsForWorkspace(newWorkspace);
|
||||
});
|
||||
@ -833,6 +847,16 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
|
||||
});
|
||||
}
|
||||
|
||||
protected getWorkspaceCompletionsForPosition(
|
||||
workspace: WorkspaceServiceInstance,
|
||||
filePath: string,
|
||||
position: Position,
|
||||
workspacePath: string,
|
||||
token: CancellationToken
|
||||
): Promise<CompletionResults | undefined> {
|
||||
return workspace.serviceInstance.getCompletionsForPosition(filePath, position, workspacePath, token);
|
||||
}
|
||||
|
||||
updateSettingsForAllWorkspaces(): void {
|
||||
this._workspaceMap.forEach((workspace) => {
|
||||
this.updateSettingsForWorkspace(workspace).ignoreErrors();
|
||||
@ -864,26 +888,10 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
|
||||
if (params.workspaceFolders) {
|
||||
params.workspaceFolders.forEach((folder) => {
|
||||
const path = convertUriToPath(folder.uri);
|
||||
this._workspaceMap.set(path, {
|
||||
workspaceName: folder.name,
|
||||
rootPath: path,
|
||||
rootUri: folder.uri,
|
||||
serviceInstance: this.createAnalyzerService(folder.name),
|
||||
disableLanguageServices: false,
|
||||
disableOrganizeImports: false,
|
||||
isInitialized: createDeferred<boolean>(),
|
||||
});
|
||||
this._workspaceMap.set(path, this.createWorkspaceServiceInstance(folder, path));
|
||||
});
|
||||
} else if (params.rootPath) {
|
||||
this._workspaceMap.set(params.rootPath, {
|
||||
workspaceName: '',
|
||||
rootPath: params.rootPath,
|
||||
rootUri: '',
|
||||
serviceInstance: this.createAnalyzerService(params.rootPath),
|
||||
disableLanguageServices: false,
|
||||
disableOrganizeImports: false,
|
||||
isInitialized: createDeferred<boolean>(),
|
||||
});
|
||||
this._workspaceMap.set(params.rootPath, this.createWorkspaceServiceInstance(undefined, params.rootPath));
|
||||
}
|
||||
|
||||
const result: InitializeResult = {
|
||||
@ -922,6 +930,21 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
|
||||
return result;
|
||||
}
|
||||
|
||||
protected createWorkspaceServiceInstance(
|
||||
workspace: WorkspaceFolder | undefined,
|
||||
rootPath: string
|
||||
): WorkspaceServiceInstance {
|
||||
return {
|
||||
workspaceName: workspace?.name ?? '',
|
||||
rootPath,
|
||||
rootUri: workspace?.uri ?? '',
|
||||
serviceInstance: this.createAnalyzerService(workspace?.name ?? rootPath),
|
||||
disableLanguageServices: false,
|
||||
disableOrganizeImports: false,
|
||||
isInitialized: createDeferred<boolean>(),
|
||||
};
|
||||
}
|
||||
|
||||
protected onAnalysisCompletedHandler(results: AnalysisResults): void {
|
||||
// Send the computed diagnostics to the client.
|
||||
results.diagnostics.forEach((fileDiag) => {
|
||||
@ -956,8 +979,11 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
|
||||
}
|
||||
}
|
||||
|
||||
async updateSettingsForWorkspace(workspace: WorkspaceServiceInstance): Promise<void> {
|
||||
const serverSettings = await this.getSettings(workspace);
|
||||
async updateSettingsForWorkspace(
|
||||
workspace: WorkspaceServiceInstance,
|
||||
serverSettings?: ServerSettings
|
||||
): Promise<void> {
|
||||
serverSettings = serverSettings ?? (await this.getSettings(workspace));
|
||||
|
||||
// Set logging level first.
|
||||
(this.console as ConsoleWithLogLevel).level = serverSettings.logLevel ?? LogLevel.Info;
|
||||
@ -1007,18 +1033,19 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
const completions = await workspace.serviceInstance.getCompletionsForPosition(
|
||||
const completions = await this.getWorkspaceCompletionsForPosition(
|
||||
workspace,
|
||||
filePath,
|
||||
position,
|
||||
workspace.rootPath,
|
||||
token
|
||||
);
|
||||
|
||||
if (completions) {
|
||||
completions.isIncomplete = completionIncomplete;
|
||||
if (completions && completions.completionList) {
|
||||
completions.completionList.isIncomplete = completionIncomplete;
|
||||
}
|
||||
|
||||
return completions;
|
||||
return completions?.completionList;
|
||||
}
|
||||
|
||||
protected convertLogLevel(logLevel?: string): LogLevel {
|
||||
|
@ -44,6 +44,8 @@ import {
|
||||
isNone,
|
||||
isObject,
|
||||
isTypeVar,
|
||||
isUnbound,
|
||||
isUnknown,
|
||||
ObjectType,
|
||||
Type,
|
||||
TypeBase,
|
||||
@ -51,6 +53,7 @@ import {
|
||||
} from '../analyzer/types';
|
||||
import {
|
||||
doForSubtypes,
|
||||
getDeclaringModulesForType,
|
||||
getMembersForClass,
|
||||
getMembersForModule,
|
||||
isProperty,
|
||||
@ -174,6 +177,18 @@ export interface CompletionItemData {
|
||||
symbolId?: number;
|
||||
}
|
||||
|
||||
// ModuleContext attempts to gather info for unknown types
|
||||
export interface ModuleContext {
|
||||
lastKnownModule?: string;
|
||||
lastKnownMemberName?: string;
|
||||
unknownMemberName?: string;
|
||||
}
|
||||
|
||||
export interface CompletionResults {
|
||||
completionList: CompletionList | undefined;
|
||||
moduleContext?: ModuleContext;
|
||||
}
|
||||
|
||||
interface RecentCompletionInfo {
|
||||
label: string;
|
||||
autoImportText: string;
|
||||
@ -209,7 +224,7 @@ export class CompletionProvider {
|
||||
private _cancellationToken: CancellationToken
|
||||
) {}
|
||||
|
||||
getCompletionsForPosition(): CompletionList | undefined {
|
||||
getCompletionsForPosition(): CompletionResults | undefined {
|
||||
const offset = convertPositionToOffset(this._position, this._parseResults.tokenizerOutput.lines);
|
||||
if (offset === undefined) {
|
||||
return undefined;
|
||||
@ -412,7 +427,7 @@ export class CompletionProvider {
|
||||
priorWord: string,
|
||||
priorText: string,
|
||||
postText: string
|
||||
): CompletionList | undefined {
|
||||
): CompletionResults | undefined {
|
||||
// Is the error due to a missing member access name? If so,
|
||||
// we can evaluate the left side of the member access expression
|
||||
// to determine its type and offer suggestions based on it.
|
||||
@ -451,15 +466,15 @@ export class CompletionProvider {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _createSingleKeywordCompletionList(keyword: string): CompletionList {
|
||||
private _createSingleKeywordCompletionList(keyword: string): CompletionResults {
|
||||
const completionItem = CompletionItem.create(keyword);
|
||||
completionItem.kind = CompletionItemKind.Keyword;
|
||||
completionItem.sortText = this._makeSortText(SortCategory.LikelyKeyword, keyword);
|
||||
|
||||
return CompletionList.create([completionItem]);
|
||||
const completionList = CompletionList.create([completionItem]);
|
||||
return { completionList };
|
||||
}
|
||||
|
||||
private _getMethodOverrideCompletions(partialName: NameNode): CompletionList | undefined {
|
||||
private _getMethodOverrideCompletions(partialName: NameNode): CompletionResults | undefined {
|
||||
const enclosingClass = ParseTreeUtils.getEnclosingClass(partialName, true);
|
||||
if (!enclosingClass) {
|
||||
return undefined;
|
||||
@ -498,7 +513,7 @@ export class CompletionProvider {
|
||||
}
|
||||
});
|
||||
|
||||
return completionList;
|
||||
return { completionList };
|
||||
}
|
||||
|
||||
private _printMethodSignature(node: FunctionNode): string {
|
||||
@ -536,10 +551,14 @@ export class CompletionProvider {
|
||||
return methodSignature;
|
||||
}
|
||||
|
||||
private _getMemberAccessCompletions(leftExprNode: ExpressionNode, priorWord: string): CompletionList | undefined {
|
||||
private _getMemberAccessCompletions(
|
||||
leftExprNode: ExpressionNode,
|
||||
priorWord: string
|
||||
): CompletionResults | undefined {
|
||||
const leftType = this._evaluator.getType(leftExprNode);
|
||||
const symbolTable = new Map<string, Symbol>();
|
||||
const completionList = CompletionList.create();
|
||||
let lastKnownModule: ModuleContext | undefined;
|
||||
|
||||
if (leftType) {
|
||||
doForSubtypes(leftType, (subtype) => {
|
||||
@ -575,11 +594,65 @@ export class CompletionProvider {
|
||||
const objectThrough: ObjectType | undefined = leftType && isObject(leftType) ? leftType : undefined;
|
||||
this._addSymbolsForSymbolTable(symbolTable, (_) => true, priorWord, objectThrough, completionList);
|
||||
|
||||
// const moduleNamesForType = getDeclaringModulesForType(leftType);
|
||||
// TODO - log modules to telemetry.
|
||||
// If we dont know this type, look for a module we should stub
|
||||
if (!leftType || isUnknown(leftType) || isUnbound(leftType)) {
|
||||
lastKnownModule = this._getLastKnownModule(leftExprNode, leftType);
|
||||
}
|
||||
}
|
||||
|
||||
return completionList;
|
||||
return { completionList, moduleContext: lastKnownModule };
|
||||
}
|
||||
|
||||
private _getLastKnownModule(leftExprNode: ExpressionNode, leftType: Type | undefined): ModuleContext | undefined {
|
||||
let curNode: ExpressionNode | undefined = leftExprNode;
|
||||
let curType: Type | undefined = leftType;
|
||||
let unknownMemberName: string | undefined =
|
||||
leftExprNode.nodeType === ParseNodeType.MemberAccess ? leftExprNode?.memberName.value : undefined;
|
||||
|
||||
// Walk left of the expression scope till we find a known type. A.B.Unknown.<-- return B.
|
||||
while (curNode) {
|
||||
if (curNode.nodeType === ParseNodeType.Call || curNode.nodeType === ParseNodeType.MemberAccess) {
|
||||
// Move left
|
||||
curNode = curNode.leftExpression;
|
||||
|
||||
// First time in the loop remember the name of the unknown type.
|
||||
if (unknownMemberName === undefined) {
|
||||
unknownMemberName =
|
||||
curNode.nodeType === ParseNodeType.MemberAccess ? curNode?.memberName.value ?? '' : '';
|
||||
}
|
||||
} else {
|
||||
curNode = undefined;
|
||||
}
|
||||
|
||||
if (curNode) {
|
||||
curType = this._evaluator.getType(curNode);
|
||||
|
||||
// Breakout if we found a known type.
|
||||
if (curType !== undefined && !isUnknown(curType) && !isUnbound(curType)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const context: ModuleContext = {};
|
||||
if (curType && !isUnknown(curType) && !isUnbound(curType) && curNode) {
|
||||
const moduleNamesForType = getDeclaringModulesForType(curType);
|
||||
|
||||
// For union types we only care about non 'typing' modules.
|
||||
context.lastKnownModule = moduleNamesForType.find((n) => n !== 'typing');
|
||||
|
||||
if (curNode.nodeType === ParseNodeType.MemberAccess) {
|
||||
context.lastKnownMemberName = curNode.memberName.value;
|
||||
} else if (curNode.nodeType === ParseNodeType.Name && isClass(curType)) {
|
||||
context.lastKnownMemberName = curType.details.name;
|
||||
} else if (curNode.nodeType === ParseNodeType.Name && isObject(curType)) {
|
||||
context.lastKnownMemberName = curType.classType.details.name;
|
||||
}
|
||||
|
||||
context.unknownMemberName = unknownMemberName;
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private _getStatementCompletions(
|
||||
@ -587,7 +660,7 @@ export class CompletionProvider {
|
||||
priorWord: string,
|
||||
priorText: string,
|
||||
postText: string
|
||||
): CompletionList | undefined {
|
||||
): CompletionResults | undefined {
|
||||
// For now, use the same logic for expressions and statements.
|
||||
return this._getExpressionCompletions(parseNode, priorWord, priorText, postText);
|
||||
}
|
||||
@ -597,7 +670,7 @@ export class CompletionProvider {
|
||||
priorWord: string,
|
||||
priorText: string,
|
||||
postText: string
|
||||
): CompletionList | undefined {
|
||||
): CompletionResults | undefined {
|
||||
// If the user typed a "." as part of a number, don't present
|
||||
// any completion options.
|
||||
if (parseNode.nodeType === ParseNodeType.Number) {
|
||||
@ -643,7 +716,7 @@ export class CompletionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
return completionList;
|
||||
return { completionList };
|
||||
}
|
||||
|
||||
private _addCallArgumentCompletions(
|
||||
@ -732,7 +805,7 @@ export class CompletionProvider {
|
||||
priorWord: string,
|
||||
priorText: string,
|
||||
postText: string
|
||||
): CompletionList | undefined {
|
||||
): CompletionResults | undefined {
|
||||
let parentNode: ParseNode | undefined = parseNode.parent;
|
||||
if (!parentNode || parentNode.nodeType !== ParseNodeType.StringList || parentNode.strings.length > 1) {
|
||||
return undefined;
|
||||
@ -784,7 +857,7 @@ export class CompletionProvider {
|
||||
this._addCallArgumentCompletions(parseNode, priorWord, priorText, postText, completionList);
|
||||
}
|
||||
|
||||
return completionList;
|
||||
return { completionList };
|
||||
}
|
||||
|
||||
// Given a string of text that precedes the current insertion point,
|
||||
@ -926,7 +999,10 @@ export class CompletionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private _getImportFromCompletions(importFromNode: ImportFromNode, priorWord: string): CompletionList | undefined {
|
||||
private _getImportFromCompletions(
|
||||
importFromNode: ImportFromNode,
|
||||
priorWord: string
|
||||
): CompletionResults | undefined {
|
||||
// Don't attempt to provide completions for "from X import *".
|
||||
if (importFromNode.isWildcardImport) {
|
||||
return undefined;
|
||||
@ -965,7 +1041,7 @@ export class CompletionProvider {
|
||||
}
|
||||
});
|
||||
|
||||
return completionList;
|
||||
return { completionList };
|
||||
}
|
||||
|
||||
private _findMatchingKeywords(keywordList: string[], partialMatch: string): string[] {
|
||||
@ -1443,7 +1519,7 @@ export class CompletionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private _getImportModuleCompletions(node: ModuleNameNode): CompletionList {
|
||||
private _getImportModuleCompletions(node: ModuleNameNode): CompletionResults {
|
||||
const execEnvironment = this._configOptions.findExecEnvironment(this._filePath);
|
||||
const moduleDescriptor: ImportedModuleDescriptor = {
|
||||
leadingDots: node.leadingDots,
|
||||
@ -1483,6 +1559,6 @@ export class CompletionProvider {
|
||||
completionItem.sortText = this._makeSortText(SortCategory.ImportModuleName, completionName);
|
||||
});
|
||||
|
||||
return completionList;
|
||||
return { completionList };
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @filename: test.py
|
||||
//// class Model:
|
||||
//// pass
|
||||
////
|
||||
//// def some_func1(a: Model):
|
||||
//// x = a.unknownName.[|/*marker1*/|]
|
||||
//// pass
|
||||
|
||||
// @ts-ignore
|
||||
await helper.verifyCompletion('included', {
|
||||
marker1: {
|
||||
completions: [],
|
||||
moduleContext: { lastKnownModule: 'test', lastKnownMemberName: 'Model', unknownMemberName: 'unknownName' },
|
||||
},
|
||||
});
|
@ -0,0 +1,57 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @filename: test.py
|
||||
//// class Model:
|
||||
//// @staticmethod
|
||||
//// def foo( value ):
|
||||
//// return value
|
||||
////
|
||||
////
|
||||
//// x = Model.foo(unknownValue).[|/*marker1*/|]
|
||||
//// pass
|
||||
////
|
||||
//// y = Model.unknownMember.[|/*marker2*/|]
|
||||
//// pass
|
||||
////
|
||||
//// def some_func1(a: Model):
|
||||
//// x = a.unknownMember.[|/*marker3*/|]
|
||||
//// pass
|
||||
////
|
||||
//// Model.unknownValue.[|/*marker4*/|]
|
||||
////
|
||||
//// UnkownModel.unknownValue.[|/*marker5*/|]
|
||||
|
||||
// @ts-ignore
|
||||
await helper.verifyCompletion('included', {
|
||||
// tests: _getLastKnownModule(): if (curNode.nodeType === ParseNodeType.MemberAccess && curNode.memberName)
|
||||
marker1: {
|
||||
completions: [],
|
||||
moduleContext: { lastKnownModule: 'test', lastKnownMemberName: 'foo', unknownMemberName: 'foo' },
|
||||
},
|
||||
// tests: _getLastKnownModule(): else if (curNode.nodeType === ParseNodeType.Name && isClass(curType))
|
||||
marker2: {
|
||||
completions: [],
|
||||
moduleContext: {
|
||||
lastKnownModule: 'test',
|
||||
lastKnownMemberName: 'Model',
|
||||
unknownMemberName: 'unknownMember',
|
||||
},
|
||||
},
|
||||
// tests: _getLastKnownModule(): else if (curNode.nodeType === ParseNodeType.Name && isObject(curType))
|
||||
marker3: {
|
||||
completions: [],
|
||||
moduleContext: {
|
||||
lastKnownModule: 'test',
|
||||
lastKnownMemberName: 'Model',
|
||||
unknownMemberName: 'unknownMember',
|
||||
},
|
||||
},
|
||||
marker4: {
|
||||
completions: [],
|
||||
moduleContext: { lastKnownModule: 'test', lastKnownMemberName: 'Model', unknownMemberName: 'unknownValue' },
|
||||
},
|
||||
marker5: {
|
||||
completions: [],
|
||||
moduleContext: {},
|
||||
},
|
||||
});
|
@ -0,0 +1,30 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @filename: mspythonconfig.json
|
||||
//// {
|
||||
//// "useLibraryCodeForTypes": true
|
||||
//// }
|
||||
|
||||
// @filename: test.py
|
||||
//// import testnumpy
|
||||
//// obj = testnumpy.random.randint("foo").[|/*marker1*/|]
|
||||
|
||||
// @filename: testnumpy/__init__.py
|
||||
// @library: true
|
||||
//// from . import random
|
||||
|
||||
// @filename: testnumpy/random/__init__.py
|
||||
// @library: true
|
||||
//// __all__ = ['randint']
|
||||
|
||||
// @ts-ignore
|
||||
await helper.verifyCompletion('included', {
|
||||
marker1: {
|
||||
completions: [],
|
||||
moduleContext: {
|
||||
lastKnownModule: 'testnumpy',
|
||||
lastKnownMemberName: 'random',
|
||||
unknownMemberName: 'randint',
|
||||
},
|
||||
},
|
||||
});
|
@ -154,6 +154,11 @@ declare namespace _ {
|
||||
map: {
|
||||
[marker: string]: {
|
||||
completions: FourSlashCompletionItem[];
|
||||
moduleContext?: {
|
||||
lastKnownModule?: string;
|
||||
lastKnownMemberName?: string;
|
||||
unknownMemberName?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
): Promise<void>;
|
||||
|
@ -11,9 +11,17 @@ import * as ts from 'typescript';
|
||||
import { combinePaths } from '../../../common/pathUtils';
|
||||
import * as host from '../host';
|
||||
import { parseTestData } from './fourSlashParser';
|
||||
import { FourSlashData } from './fourSlashTypes';
|
||||
import { HostSpecificFeatures, TestState } from './testState';
|
||||
import { Consts } from './testState.Consts';
|
||||
|
||||
export type TestStateFactory = (
|
||||
basePath: string,
|
||||
testData: FourSlashData,
|
||||
mountPaths?: Map<string, string>,
|
||||
hostSpecificFeatures?: HostSpecificFeatures
|
||||
) => TestState;
|
||||
|
||||
/**
|
||||
* run given fourslash test file
|
||||
*
|
||||
@ -25,10 +33,11 @@ export function runFourSlashTest(
|
||||
fileName: string,
|
||||
cb?: jest.DoneCallback,
|
||||
mountPaths?: Map<string, string>,
|
||||
hostSpecificFeatures?: HostSpecificFeatures
|
||||
hostSpecificFeatures?: HostSpecificFeatures,
|
||||
testStateFactory?: TestStateFactory
|
||||
) {
|
||||
const content = host.HOST.readFile(fileName)!;
|
||||
runFourSlashTestContent(basePath, fileName, content, cb, mountPaths, hostSpecificFeatures);
|
||||
runFourSlashTestContent(basePath, fileName, content, cb, mountPaths, hostSpecificFeatures, testStateFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -45,7 +54,8 @@ export function runFourSlashTestContent(
|
||||
content: string,
|
||||
cb?: jest.DoneCallback,
|
||||
mountPaths?: Map<string, string>,
|
||||
hostSpecificFeatures?: HostSpecificFeatures
|
||||
hostSpecificFeatures?: HostSpecificFeatures,
|
||||
testStateFactory?: TestStateFactory
|
||||
) {
|
||||
// give file paths an absolute path for the virtual file system
|
||||
const absoluteBasePath = combinePaths('/', basePath);
|
||||
@ -53,7 +63,10 @@ export function runFourSlashTestContent(
|
||||
|
||||
// parse out the files and their metadata
|
||||
const testData = parseTestData(absoluteBasePath, content, absoluteFileName);
|
||||
const state = new TestState(absoluteBasePath, testData, mountPaths, hostSpecificFeatures);
|
||||
const state =
|
||||
testStateFactory !== undefined
|
||||
? testStateFactory(absoluteBasePath, testData, mountPaths, hostSpecificFeatures)
|
||||
: new TestState(absoluteBasePath, testData, mountPaths, hostSpecificFeatures);
|
||||
const output = ts.transpileModule(content, {
|
||||
reportDiagnostics: true,
|
||||
compilerOptions: { target: ts.ScriptTarget.ES2015 },
|
||||
|
@ -253,7 +253,7 @@ export class TestState {
|
||||
const marker = this.getMarkerByName(markerString);
|
||||
const ranges = this.getRanges().filter((r) => r.marker === marker);
|
||||
if (ranges.length !== 1) {
|
||||
this._raiseError(`no matching range for ${markerString}`);
|
||||
this.raiseError(`no matching range for ${markerString}`);
|
||||
}
|
||||
|
||||
const range = ranges[0];
|
||||
@ -261,7 +261,7 @@ export class TestState {
|
||||
}
|
||||
|
||||
convertPositionRange(range: Range) {
|
||||
return this._convertOffsetsToRange(range.fileName, range.pos, range.end);
|
||||
return this.convertOffsetsToRange(range.fileName, range.pos, range.end);
|
||||
}
|
||||
|
||||
goToPosition(positionOrLineAndColumn: number | Position) {
|
||||
@ -327,7 +327,7 @@ export class TestState {
|
||||
if (this.testData.rangesByText) {
|
||||
return this.testData.rangesByText;
|
||||
}
|
||||
const result = this._createMultiMap<Range>(this.getRanges(), (r) => this._rangeText(r));
|
||||
const result = this.createMultiMap<Range>(this.getRanges(), (r) => this._rangeText(r));
|
||||
this.testData.rangesByText = result;
|
||||
|
||||
return result;
|
||||
@ -462,7 +462,7 @@ export class TestState {
|
||||
|
||||
// organize things per file
|
||||
const resultPerFile = this._getDiagnosticsPerFile();
|
||||
const rangePerFile = this._createMultiMap<Range>(this.getRanges(), (r) => r.fileName);
|
||||
const rangePerFile = this.createMultiMap<Range>(this.getRanges(), (r) => r.fileName);
|
||||
|
||||
if (!hasDiagnostics(resultPerFile) && rangePerFile.size === 0) {
|
||||
// no errors and no error is expected. we are done
|
||||
@ -470,7 +470,7 @@ export class TestState {
|
||||
}
|
||||
|
||||
for (const [file, ranges] of rangePerFile.entries()) {
|
||||
const rangesPerCategory = this._createMultiMap<Range>(ranges, (r) => {
|
||||
const rangesPerCategory = this.createMultiMap<Range>(ranges, (r) => {
|
||||
if (map) {
|
||||
const name = this.getMarkerName(r.marker!);
|
||||
return map[name].category;
|
||||
@ -503,10 +503,10 @@ export class TestState {
|
||||
? result.warnings
|
||||
: category === 'information'
|
||||
? result.information
|
||||
: this._raiseError(`unexpected category ${category}`);
|
||||
: this.raiseError(`unexpected category ${category}`);
|
||||
|
||||
if (expected.length !== actual.length) {
|
||||
this._raiseError(
|
||||
this.raiseError(
|
||||
`contains unexpected result - expected: ${stringify(expected)}, actual: ${stringify(actual)}`
|
||||
);
|
||||
}
|
||||
@ -522,7 +522,7 @@ export class TestState {
|
||||
});
|
||||
|
||||
if (matches.length === 0) {
|
||||
this._raiseError(`doesn't contain expected range: ${stringify(range)}`);
|
||||
this.raiseError(`doesn't contain expected range: ${stringify(range)}`);
|
||||
}
|
||||
|
||||
// if map is provided, check message as well
|
||||
@ -531,7 +531,7 @@ export class TestState {
|
||||
const message = map[name].message;
|
||||
|
||||
if (matches.filter((d) => message === d.message).length !== 1) {
|
||||
this._raiseError(
|
||||
this.raiseError(
|
||||
`message doesn't match: ${message} of ${name} - ${stringify(
|
||||
range
|
||||
)}, actual: ${stringify(matches)}`
|
||||
@ -543,7 +543,7 @@ export class TestState {
|
||||
}
|
||||
|
||||
if (hasDiagnostics(resultPerFile)) {
|
||||
this._raiseError(`these diagnostics were unexpected: ${stringify(resultPerFile)}`);
|
||||
this.raiseError(`these diagnostics were unexpected: ${stringify(resultPerFile)}`);
|
||||
}
|
||||
|
||||
function hasDiagnostics(
|
||||
@ -586,7 +586,7 @@ export class TestState {
|
||||
const codeActions = await this._getCodeActions(range);
|
||||
if (verifyCodeActionCount) {
|
||||
if (codeActions.length !== map[name].codeActions.length) {
|
||||
this._raiseError(
|
||||
this.raiseError(
|
||||
`doesn't contain expected result: ${stringify(map[name])}, actual: ${stringify(codeActions)}`
|
||||
);
|
||||
}
|
||||
@ -616,7 +616,7 @@ export class TestState {
|
||||
});
|
||||
|
||||
if (matches.length !== 1) {
|
||||
this._raiseError(
|
||||
this.raiseError(
|
||||
`doesn't contain expected result: ${stringify(expected)}, actual: ${stringify(codeActions)}`
|
||||
);
|
||||
}
|
||||
@ -655,7 +655,7 @@ export class TestState {
|
||||
const expectedText: string = Object.values(files)[0];
|
||||
|
||||
if (actualText !== expectedText) {
|
||||
this._raiseError(
|
||||
this.raiseError(
|
||||
`doesn't contain expected result: ${stringify(expectedText)}, actual: ${stringify(actualText)}`
|
||||
);
|
||||
}
|
||||
@ -681,7 +681,7 @@ export class TestState {
|
||||
const codeActions = await this._getCodeActions(range);
|
||||
if (verifyCodeActionCount) {
|
||||
if (codeActions.length !== Object.keys(map).length) {
|
||||
this._raiseError(
|
||||
this.raiseError(
|
||||
`doesn't contain expected result: ${stringify(map[name])}, actual: ${stringify(codeActions)}`
|
||||
);
|
||||
}
|
||||
@ -689,7 +689,7 @@ export class TestState {
|
||||
|
||||
const matches = codeActions.filter((c) => c.title === map[name].title);
|
||||
if (matches.length === 0) {
|
||||
this._raiseError(
|
||||
this.raiseError(
|
||||
`doesn't contain expected result: ${stringify(map[name])}, actual: ${stringify(codeActions)}`
|
||||
);
|
||||
}
|
||||
@ -713,7 +713,7 @@ export class TestState {
|
||||
(e) => rangesAreEqual(e.range, edit.range) && e.newText === edit.newText
|
||||
).length !== 1
|
||||
) {
|
||||
this._raiseError(
|
||||
this.raiseError(
|
||||
`doesn't contain expected result: ${stringify(map[name])}, actual: ${stringify(
|
||||
edits
|
||||
)}`
|
||||
@ -739,7 +739,7 @@ export class TestState {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rangePos = this._convertOffsetsToRange(range.fileName, range.pos, range.end);
|
||||
const rangePos = this.convertOffsetsToRange(range.fileName, range.pos, range.end);
|
||||
|
||||
const actual = convertHoverResults(
|
||||
this.program.getHoverForPosition(range.fileName, rangePos.start, CancellationToken.None)
|
||||
@ -807,6 +807,11 @@ export class TestState {
|
||||
map: {
|
||||
[marker: string]: {
|
||||
completions: _.FourSlashCompletionItem[];
|
||||
moduleContext?: {
|
||||
lastKnownModule?: string;
|
||||
lastKnownMemberName?: string;
|
||||
unknownMemberName?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
): Promise<void> {
|
||||
@ -820,7 +825,7 @@ export class TestState {
|
||||
|
||||
const filePath = marker.fileName;
|
||||
const expectedCompletions = map[markerName].completions;
|
||||
const completionPosition = this._convertOffsetToPosition(filePath, marker.position);
|
||||
const completionPosition = this.convertOffsetToPosition(filePath, marker.position);
|
||||
|
||||
const result = await this.workspace.serviceInstance.getCompletionsForPosition(
|
||||
filePath,
|
||||
@ -829,31 +834,33 @@ export class TestState {
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
if (result) {
|
||||
if (result?.completionList) {
|
||||
if (verifyMode === 'exact') {
|
||||
if (result.items.length !== expectedCompletions.length) {
|
||||
if (result.completionList.items.length !== expectedCompletions.length) {
|
||||
assert.fail(
|
||||
`Expected ${expectedCompletions.length} items but received ${
|
||||
result.items.length
|
||||
}. Actual completions:\n${stringify(result.items.map((r) => r.label))}`
|
||||
result.completionList.items.length
|
||||
}. Actual completions:\n${stringify(result.completionList.items.map((r) => r.label))}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < expectedCompletions.length; i++) {
|
||||
const expected = expectedCompletions[i];
|
||||
const actualIndex = result.items.findIndex((a) => a.label === expected.label);
|
||||
const actualIndex = result.completionList.items.findIndex((a) => a.label === expected.label);
|
||||
if (actualIndex >= 0) {
|
||||
if (verifyMode === 'excluded') {
|
||||
// we're not supposed to find the completions passed to the test
|
||||
assert.fail(
|
||||
`Completion item with label "${
|
||||
expected.label
|
||||
}" unexpected. Actual completions:\n${stringify(result.items.map((r) => r.label))}`
|
||||
}" unexpected. Actual completions:\n${stringify(
|
||||
result.completionList.items.map((r) => r.label)
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const actual: CompletionItem = result.items[actualIndex];
|
||||
const actual: CompletionItem = result.completionList.items[actualIndex];
|
||||
assert.equal(actual.label, expected.label);
|
||||
if (expectedCompletions[i].documentation !== undefined) {
|
||||
if (actual.documentation === undefined) {
|
||||
@ -870,28 +877,51 @@ export class TestState {
|
||||
}
|
||||
}
|
||||
|
||||
result.items.splice(actualIndex, 1);
|
||||
result.completionList.items.splice(actualIndex, 1);
|
||||
} else {
|
||||
if (verifyMode === 'included' || verifyMode === 'exact') {
|
||||
// we're supposed to find all items passed to the test
|
||||
assert.fail(
|
||||
`Completion item with label "${
|
||||
expected.label
|
||||
}" expected. Actual completions:\n${stringify(result.items.map((r) => r.label))}`
|
||||
}" expected. Actual completions:\n${stringify(
|
||||
result.completionList.items.map((r) => r.label)
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (verifyMode === 'exact') {
|
||||
if (result.items.length !== 0) {
|
||||
if (result.completionList.items.length !== 0) {
|
||||
// we removed every item we found, there should not be any remaining
|
||||
assert.fail(`Completion items unexpected: ${stringify(result.items.map((r) => r.label))}`);
|
||||
assert.fail(
|
||||
`Completion items unexpected: ${stringify(result.completionList.items.map((r) => r.label))}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert.fail('Failed to get completions');
|
||||
}
|
||||
|
||||
if (map[markerName].moduleContext !== undefined && result?.moduleContext !== undefined) {
|
||||
const expectedModule = map[markerName].moduleContext?.lastKnownModule;
|
||||
const expectedType = map[markerName].moduleContext?.lastKnownMemberName;
|
||||
const expectedName = map[markerName].moduleContext?.unknownMemberName;
|
||||
if (
|
||||
result?.moduleContext?.lastKnownModule !== expectedModule ||
|
||||
result?.moduleContext?.lastKnownMemberName !== expectedType ||
|
||||
result?.moduleContext?.unknownMemberName !== expectedName
|
||||
) {
|
||||
assert.fail(
|
||||
`Expected completion results moduleContext with \n lastKnownModule: "${expectedModule}"\n lastKnownMemberName: "${expectedType}"\n unknownMemberName: "${expectedName}"\n Actual moduleContext:\n lastKnownModule: "${
|
||||
result.moduleContext?.lastKnownModule ?? ''
|
||||
}"\n lastKnownMemberName: "${
|
||||
result.moduleContext?.lastKnownMemberName ?? ''
|
||||
}\n unknownMemberName: "${result.moduleContext?.unknownMemberName ?? ''}" `
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -917,7 +947,7 @@ export class TestState {
|
||||
}
|
||||
|
||||
const expected = map[name];
|
||||
const position = this._convertOffsetToPosition(fileName, marker.position);
|
||||
const position = this.convertOffsetToPosition(fileName, marker.position);
|
||||
|
||||
const actual = this.program.getSignatureHelpForPosition(fileName, position, CancellationToken.None);
|
||||
|
||||
@ -973,7 +1003,7 @@ export class TestState {
|
||||
|
||||
const expected = map[name].references;
|
||||
|
||||
const position = this._convertOffsetToPosition(fileName, marker.position);
|
||||
const position = this.convertOffsetToPosition(fileName, marker.position);
|
||||
const actual = this.program.getReferencesForPosition(fileName, position, true, CancellationToken.None);
|
||||
|
||||
assert.equal(actual?.length ?? 0, expected.length);
|
||||
@ -1011,7 +1041,7 @@ export class TestState {
|
||||
|
||||
const expected = map[name].references;
|
||||
|
||||
const position = this._convertOffsetToPosition(fileName, marker.position);
|
||||
const position = this.convertOffsetToPosition(fileName, marker.position);
|
||||
const actual = this.program.getDocumentHighlight(fileName, position, CancellationToken.None);
|
||||
|
||||
assert.equal(actual?.length ?? 0, expected.length);
|
||||
@ -1044,7 +1074,7 @@ export class TestState {
|
||||
|
||||
const expected = map[name].definitions;
|
||||
|
||||
const position = this._convertOffsetToPosition(fileName, marker.position);
|
||||
const position = this.convertOffsetToPosition(fileName, marker.position);
|
||||
const actual = this.program.getDefinitionsForPosition(fileName, position, CancellationToken.None);
|
||||
|
||||
assert.equal(actual?.length ?? 0, expected.length);
|
||||
@ -1073,7 +1103,7 @@ export class TestState {
|
||||
|
||||
const expected = map[name];
|
||||
|
||||
const position = this._convertOffsetToPosition(fileName, marker.position);
|
||||
const position = this.convertOffsetToPosition(fileName, marker.position);
|
||||
const actual = this.program.renameSymbolAtPosition(
|
||||
fileName,
|
||||
position,
|
||||
@ -1144,13 +1174,13 @@ export class TestState {
|
||||
return convertPositionToOffset(position, lines)!;
|
||||
}
|
||||
|
||||
private _convertOffsetToPosition(fileName: string, offset: number): Position {
|
||||
protected convertOffsetToPosition(fileName: string, offset: number): Position {
|
||||
const lines = this._getTextRangeCollection(fileName);
|
||||
|
||||
return convertOffsetToPosition(offset, lines);
|
||||
}
|
||||
|
||||
private _convertOffsetsToRange(fileName: string, startOffset: number, endOffset: number): PositionRange {
|
||||
protected convertOffsetsToRange(fileName: string, startOffset: number, endOffset: number): PositionRange {
|
||||
const lines = this._getTextRangeCollection(fileName);
|
||||
|
||||
return {
|
||||
@ -1175,7 +1205,7 @@ export class TestState {
|
||||
return tokenizer.tokenize(fileContents).lines;
|
||||
}
|
||||
|
||||
private _raiseError(message: string): never {
|
||||
protected raiseError(message: string): never {
|
||||
throw new Error(this._messageAtLastKnownMarker(message));
|
||||
}
|
||||
|
||||
@ -1211,7 +1241,7 @@ export class TestState {
|
||||
return text.replace(/\s/g, '');
|
||||
}
|
||||
|
||||
private _createMultiMap<T>(values?: T[], getKey?: (t: T) => string): MultiMap<T> {
|
||||
protected createMultiMap<T>(values?: T[], getKey?: (t: T) => string): MultiMap<T> {
|
||||
const map = new Map<string, T[]>() as MultiMap<T>;
|
||||
map.add = multiMapAdd;
|
||||
map.remove = multiMapRemove;
|
||||
@ -1256,7 +1286,7 @@ export class TestState {
|
||||
private _getOnlyRange() {
|
||||
const ranges = this.getRanges();
|
||||
if (ranges.length !== 1) {
|
||||
this._raiseError('Exactly one range should be specified in the test file.');
|
||||
this.raiseError('Exactly one range should be specified in the test file.');
|
||||
}
|
||||
|
||||
return ranges[0];
|
||||
@ -1272,7 +1302,7 @@ export class TestState {
|
||||
private _verifyTextMatches(actualText: string, includeWhitespace: boolean, expectedText: string) {
|
||||
const removeWhitespace = (s: string): string => (includeWhitespace ? s : this._removeWhitespace(s));
|
||||
if (removeWhitespace(actualText) !== removeWhitespace(expectedText)) {
|
||||
this._raiseError(
|
||||
this.raiseError(
|
||||
`Actual range text doesn't match expected text.\n${this._showTextDiff(expectedText, actualText)}`
|
||||
);
|
||||
}
|
||||
@ -1316,7 +1346,7 @@ export class TestState {
|
||||
// Get the text of the entire line the caret is currently at
|
||||
private _getCurrentLineContent() {
|
||||
return this._getLineContent(
|
||||
this._convertOffsetToPosition(this.activeFile.fileName, this.currentCaretPosition).line
|
||||
this.convertOffsetToPosition(this.activeFile.fileName, this.currentCaretPosition).line
|
||||
);
|
||||
}
|
||||
|
||||
@ -1366,7 +1396,7 @@ export class TestState {
|
||||
}
|
||||
|
||||
private _getLineColStringAtPosition(position: number, file: FourSlashFile = this.activeFile) {
|
||||
const pos = this._convertOffsetToPosition(file.fileName, position);
|
||||
const pos = this.convertOffsetToPosition(file.fileName, position);
|
||||
return `line ${pos.line + 1}, col ${pos.character}`;
|
||||
}
|
||||
|
||||
@ -1426,7 +1456,7 @@ export class TestState {
|
||||
};
|
||||
return [filePath, value] as [string, typeof value];
|
||||
} else {
|
||||
this._raiseError(`Source file not found for ${this._files[index]}`);
|
||||
this.raiseError(`Source file not found for ${this._files[index]}`);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1472,8 +1502,8 @@ export class TestState {
|
||||
private _getCodeActions(range: Range) {
|
||||
const file = range.fileName;
|
||||
const textRange = {
|
||||
start: this._convertOffsetToPosition(file, range.pos),
|
||||
end: this._convertOffsetToPosition(file, range.end),
|
||||
start: this.convertOffsetToPosition(file, range.pos),
|
||||
end: this.convertOffsetToPosition(file, range.end),
|
||||
};
|
||||
|
||||
return this._hostSpecificFeatures.getCodeActionsForPosition(
|
||||
@ -1494,7 +1524,7 @@ export class TestState {
|
||||
|
||||
const actual = this.fs.readFileSync(normalizedFilePath, 'utf8');
|
||||
if (actual !== expected) {
|
||||
this._raiseError(
|
||||
this.raiseError(
|
||||
`doesn't contain expected result: ${stringify(expected)}, actual: ${stringify(actual)}`
|
||||
);
|
||||
}
|
||||
|
@ -82,7 +82,7 @@
|
||||
},
|
||||
"python.analysis.stubPath": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"default": "typings",
|
||||
"description": "Path to directory containing custom type stub files.",
|
||||
"scope": "resource"
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user