Module abbreviation infrastructure, remove unused mappings on completion resolve, other (#1135)

Rollup of:

- Verify `package-lock.json` state in checks to avoid lerna modifying things.
- Add infrastructure for module abbreviations in completions.
- Removed unused auto-import mapping from `completion/resolve` handler.
- Add `isInImport` to completion data for checking the completion context.
- Allow completion verification in fourslash tests to be overridden.
This commit is contained in:
Jake Bailey 2020-11-03 16:44:28 -08:00 committed by GitHub
parent 4634309d2a
commit f071b877b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 406 additions and 267 deletions

45
build/checkLockIndent.js Normal file
View File

@ -0,0 +1,45 @@
/* eslint-disable @typescript-eslint/no-var-requires */
//@ts-check
// Lerna doesn't do a good job preserving the indention in lock files.
// Check that the lock files are still indented correctly, otherwise
// the change will cause problems with merging and the updateDeps script.
const detectIndent = require('detect-indent');
const fsExtra = require('fs-extra');
const util = require('util');
const glob = util.promisify(require('glob'));
async function findPackageLocks() {
const lernaFile = await fsExtra.readFile('lerna.json', 'utf-8');
/** @type {{ packages: string[] }} */
const lernaConfig = JSON.parse(lernaFile);
const matches = await Promise.all(lernaConfig.packages.map((pattern) => glob(pattern + '/package-lock.json')));
return ['package-lock.json'].concat(...matches);
}
async function main() {
const locks = await findPackageLocks();
let ok = true;
for (const filepath of locks) {
const input = await fsExtra.readFile(filepath, 'utf-8');
const indent = detectIndent(input);
if (indent.indent !== ' ') {
ok = false;
console.error(`${filepath} has invalid indent "${indent.indent}"`);
}
}
if (!ok) {
console.error('Lerna may have modified package-lock.json during bootstrap.');
console.error('You may need to revert any package-lock changes and rerun install:all.');
process.exit(1);
}
}
main();

20
package-lock.json generated
View File

@ -4121,9 +4121,9 @@
"dev": true "dev": true
}, },
"detect-indent": { "detect-indent": {
"version": "5.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.0.0.tgz",
"integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=", "integrity": "sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA==",
"dev": true "dev": true
}, },
"dezalgo": { "dezalgo": {
@ -10413,6 +10413,12 @@
"write-file-atomic": "^2.4.2" "write-file-atomic": "^2.4.2"
}, },
"dependencies": { "dependencies": {
"detect-indent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz",
"integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=",
"dev": true
},
"make-dir": { "make-dir": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
@ -10459,6 +10465,14 @@
"pify": "^3.0.0", "pify": "^3.0.0",
"sort-keys": "^2.0.0", "sort-keys": "^2.0.0",
"write-file-atomic": "^2.0.0" "write-file-atomic": "^2.0.0"
},
"dependencies": {
"detect-indent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz",
"integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=",
"dev": true
}
} }
} }
} }

View File

@ -10,13 +10,14 @@
"build:extension:dev": "cd packages/vscode-pyright && npm run webpack", "build:extension:dev": "cd packages/vscode-pyright && npm run webpack",
"build:cli:dev": "cd packages/pyright && npm run webpack", "build:cli:dev": "cd packages/pyright && npm run webpack",
"watch:extension": "cd packages/vscode-pyright && npm run webpack-dev", "watch:extension": "cd packages/vscode-pyright && npm run webpack-dev",
"check": "npm run check:syncpack && npm run check:eslint && npm run check:prettier", "check": "npm run check:syncpack && npm run check:eslint && npm run check:prettier && npm run check:lockindent",
"check:syncpack": "syncpack list-mismatches", "check:syncpack": "syncpack list-mismatches",
"fix:syncpack": "syncpack fix-mismatches --indent \" \" && npm run install:all", "fix:syncpack": "syncpack fix-mismatches --indent \" \" && npm run install:all",
"check:eslint": "eslint .", "check:eslint": "eslint .",
"fix:eslint": "eslint --fix .", "fix:eslint": "eslint --fix .",
"check:prettier": "prettier -c .", "check:prettier": "prettier -c .",
"fix:prettier": "prettier --write ." "fix:prettier": "prettier --write .",
"check:lockindent": "node ./build/checkLockIndent.js"
}, },
"devDependencies": { "devDependencies": {
"@types/fs-extra": "^9.0.2", "@types/fs-extra": "^9.0.2",
@ -25,6 +26,7 @@
"@types/yargs": "^15.0.9", "@types/yargs": "^15.0.9",
"@typescript-eslint/eslint-plugin": "^4.6.0", "@typescript-eslint/eslint-plugin": "^4.6.0",
"@typescript-eslint/parser": "^4.6.0", "@typescript-eslint/parser": "^4.6.0",
"detect-indent": "^6.0.0",
"eslint": "^7.12.0", "eslint": "^7.12.0",
"eslint-config-prettier": "^6.14.0", "eslint-config-prettier": "^6.14.0",
"eslint-plugin-simple-import-sort": "^5.0.3", "eslint-plugin-simple-import-sort": "^5.0.3",

View File

@ -45,10 +45,11 @@ import {
AutoImporter, AutoImporter,
AutoImportResult, AutoImportResult,
buildModuleSymbolsMap, buildModuleSymbolsMap,
getAutoImportCandidatesForAbbr,
ModuleSymbolMap, ModuleSymbolMap,
} from '../languageService/autoImporter'; } from '../languageService/autoImporter';
import { CallHierarchyProvider } from '../languageService/callHierarchyProvider'; import { CallHierarchyProvider } from '../languageService/callHierarchyProvider';
import { CompletionResults } from '../languageService/completionProvider'; import { AbbreviationMap, CompletionResults } from '../languageService/completionProvider';
import { IndexOptions, IndexResults, WorkspaceSymbolCallback } from '../languageService/documentSymbolProvider'; import { IndexOptions, IndexResults, WorkspaceSymbolCallback } from '../languageService/documentSymbolProvider';
import { HoverResults } from '../languageService/hoverProvider'; import { HoverResults } from '../languageService/hoverProvider';
import { ReferenceCallback, ReferencesResult } from '../languageService/referencesProvider'; import { ReferenceCallback, ReferencesResult } from '../languageService/referencesProvider';
@ -433,16 +434,16 @@ export class Program {
}); });
} }
indexWorkspace(callback: (path: string, results: IndexResults) => void, token: CancellationToken) { indexWorkspace(callback: (path: string, results: IndexResults) => void, token: CancellationToken): number {
if (!this._configOptions.indexing) { if (!this._configOptions.indexing) {
return; return 0;
} }
let count = 0;
return this._runEvaluatorWithCancellationToken(token, () => { return this._runEvaluatorWithCancellationToken(token, () => {
// Go through all workspace files to create indexing data. // Go through all workspace files to create indexing data.
// This will cause all files in the workspace to be parsed and bound. But // This will cause all files in the workspace to be parsed and bound. But
// _handleMemoryHighUsage will make sure we don't OOM // _handleMemoryHighUsage will make sure we don't OOM
let count = 0;
for (const sourceFileInfo of this._sourceFileList) { for (const sourceFileInfo of this._sourceFileList) {
if (!this._isUserCode(sourceFileInfo)) { if (!this._isUserCode(sourceFileInfo)) {
continue; continue;
@ -453,7 +454,7 @@ export class Program {
if (results) { if (results) {
if (++count > 2000) { if (++count > 2000) {
this._console.warn(`Workspace indexing has hit its upper limit: 2000 files`); this._console.warn(`Workspace indexing has hit its upper limit: 2000 files`);
return; return count;
} }
callback(sourceFileInfo.sourceFile.getFilePath(), results); callback(sourceFileInfo.sourceFile.getFilePath(), results);
@ -461,6 +462,8 @@ export class Program {
this._handleMemoryHighUsage(); this._handleMemoryHighUsage();
} }
return count;
}); });
} }
@ -955,7 +958,7 @@ export class Program {
filePath: string, filePath: string,
range: Range, range: Range,
similarityLimit: number, similarityLimit: number,
nameMap: Map<string, string> | undefined, nameMap: AbbreviationMap | undefined,
libraryMap: Map<string, IndexResults> | undefined, libraryMap: Map<string, IndexResults> | undefined,
token: CancellationToken token: CancellationToken
): AutoImportResult[] { ): AutoImportResult[] {
@ -1002,13 +1005,10 @@ export class Program {
const currentScope = getScopeForNode(currentNode); const currentScope = getScopeForNode(currentNode);
if (currentScope) { if (currentScope) {
const translatedWord = nameMap?.get(writtenWord); const info = nameMap?.get(writtenWord);
if (translatedWord) { if (info) {
// No filter is needed since we only do exact match. // No scope filter is needed since we only do exact match.
const exactMatch = 1; results.push(...getAutoImportCandidatesForAbbr(autoImporter, writtenWord, info, token));
results.push(
...autoImporter.getAutoImportCandidates(translatedWord, exactMatch, writtenWord, token)
);
} }
results.push( results.push(
@ -1354,6 +1354,7 @@ export class Program {
position: Position, position: Position,
workspacePath: string, workspacePath: string,
format: MarkupKind, format: MarkupKind,
nameMap: AbbreviationMap | undefined,
libraryMap: Map<string, IndexResults> | undefined, libraryMap: Map<string, IndexResults> | undefined,
token: CancellationToken token: CancellationToken
): Promise<CompletionResults | undefined> { ): Promise<CompletionResults | undefined> {
@ -1378,6 +1379,7 @@ export class Program {
this._evaluator!, this._evaluator!,
format, format,
this._createSourceMapper(execEnv, /* mapCompiled */ true), this._createSourceMapper(execEnv, /* mapCompiled */ true),
nameMap,
libraryMap, libraryMap,
() => this._buildModuleSymbolsMap(sourceFileInfo, !!libraryMap, token), () => this._buildModuleSymbolsMap(sourceFileInfo, !!libraryMap, token),
token token
@ -1416,7 +1418,6 @@ export class Program {
filePath: string, filePath: string,
completionItem: CompletionItem, completionItem: CompletionItem,
format: MarkupKind, format: MarkupKind,
libraryMap: Map<string, IndexResults> | undefined,
token: CancellationToken token: CancellationToken
) { ) {
return this._runEvaluatorWithCancellationToken(token, () => { return this._runEvaluatorWithCancellationToken(token, () => {
@ -1435,8 +1436,6 @@ export class Program {
this._evaluator!, this._evaluator!,
format, format,
this._createSourceMapper(execEnv, /* mapCompiled */ true), this._createSourceMapper(execEnv, /* mapCompiled */ true),
libraryMap,
() => this._buildModuleSymbolsMap(sourceFileInfo, !!libraryMap, token),
completionItem, completionItem,
token token
); );

View File

@ -47,7 +47,7 @@ import {
} from '../common/pathUtils'; } from '../common/pathUtils';
import { DocumentRange, Position, Range } from '../common/textRange'; import { DocumentRange, Position, Range } from '../common/textRange';
import { timingStats } from '../common/timing'; import { timingStats } from '../common/timing';
import { CompletionResults } from '../languageService/completionProvider'; import { AbbreviationMap, CompletionResults } from '../languageService/completionProvider';
import { IndexResults, WorkspaceSymbolCallback } from '../languageService/documentSymbolProvider'; import { IndexResults, WorkspaceSymbolCallback } from '../languageService/documentSymbolProvider';
import { HoverResults } from '../languageService/hoverProvider'; import { HoverResults } from '../languageService/hoverProvider';
import { ReferenceCallback } from '../languageService/referencesProvider'; import { ReferenceCallback } from '../languageService/referencesProvider';
@ -226,7 +226,7 @@ export class AnalyzerService {
filePath: string, filePath: string,
range: Range, range: Range,
similarityLimit: number, similarityLimit: number,
nameMap: Map<string, string> | undefined, nameMap: AbbreviationMap | undefined,
token: CancellationToken token: CancellationToken
) { ) {
return this._program.getAutoImports( return this._program.getAutoImports(
@ -295,6 +295,7 @@ export class AnalyzerService {
position: Position, position: Position,
workspacePath: string, workspacePath: string,
format: MarkupKind, format: MarkupKind,
nameMap: AbbreviationMap | undefined,
token: CancellationToken token: CancellationToken
): Promise<CompletionResults | undefined> { ): Promise<CompletionResults | undefined> {
return this._program.getCompletionsForPosition( return this._program.getCompletionsForPosition(
@ -302,6 +303,7 @@ export class AnalyzerService {
position, position,
workspacePath, workspacePath,
format, format,
nameMap,
this._backgroundAnalysisProgram.getIndexing(filePath), this._backgroundAnalysisProgram.getIndexing(filePath),
token token
); );
@ -313,13 +315,7 @@ export class AnalyzerService {
format: MarkupKind, format: MarkupKind,
token: CancellationToken token: CancellationToken
) { ) {
this._program.resolveCompletionItem( this._program.resolveCompletionItem(filePath, completionItem, format, token);
filePath,
completionItem,
format,
this._backgroundAnalysisProgram.getIndexing(filePath),
token
);
} }
performQuickAction( performQuickAction(

View File

@ -33,7 +33,7 @@ import { DocumentRange, getEmptyRange, Position, TextRange } from '../common/tex
import { TextRangeCollection } from '../common/textRangeCollection'; import { TextRangeCollection } from '../common/textRangeCollection';
import { timingStats } from '../common/timing'; import { timingStats } from '../common/timing';
import { ModuleSymbolMap } from '../languageService/autoImporter'; import { ModuleSymbolMap } from '../languageService/autoImporter';
import { CompletionResults } from '../languageService/completionProvider'; import { AbbreviationMap, CompletionResults } from '../languageService/completionProvider';
import { CompletionItemData, CompletionProvider } from '../languageService/completionProvider'; import { CompletionItemData, CompletionProvider } from '../languageService/completionProvider';
import { DefinitionProvider } from '../languageService/definitionProvider'; import { DefinitionProvider } from '../languageService/definitionProvider';
import { DocumentHighlightProvider } from '../languageService/documentHighlightProvider'; import { DocumentHighlightProvider } from '../languageService/documentHighlightProvider';
@ -790,6 +790,7 @@ export class SourceFile {
evaluator: TypeEvaluator, evaluator: TypeEvaluator,
format: MarkupKind, format: MarkupKind,
sourceMapper: SourceMapper, sourceMapper: SourceMapper,
nameMap: AbbreviationMap | undefined,
libraryMap: Map<string, IndexResults> | undefined, libraryMap: Map<string, IndexResults> | undefined,
moduleSymbolsCallback: () => ModuleSymbolMap, moduleSymbolsCallback: () => ModuleSymbolMap,
token: CancellationToken token: CancellationToken
@ -818,8 +819,11 @@ export class SourceFile {
evaluator, evaluator,
format, format,
sourceMapper, sourceMapper,
libraryMap, {
moduleSymbolsCallback, nameMap,
libraryMap,
getModuleSymbolsMap: moduleSymbolsCallback,
},
token token
); );
@ -833,8 +837,6 @@ export class SourceFile {
evaluator: TypeEvaluator, evaluator: TypeEvaluator,
format: MarkupKind, format: MarkupKind,
sourceMapper: SourceMapper, sourceMapper: SourceMapper,
libraryMap: Map<string, IndexResults> | undefined,
moduleSymbolsCallback: () => ModuleSymbolMap,
completionItem: CompletionItem, completionItem: CompletionItem,
token: CancellationToken token: CancellationToken
) { ) {
@ -856,8 +858,7 @@ export class SourceFile {
evaluator, evaluator,
format, format,
sourceMapper, sourceMapper,
libraryMap, undefined,
moduleSymbolsCallback,
token token
); );

View File

@ -49,25 +49,7 @@ export class BackgroundAnalysisBase {
this._worker = worker; this._worker = worker;
// global channel to communicate from BG channel to main thread. // global channel to communicate from BG channel to main thread.
worker.on('message', (msg: AnalysisResponse) => { worker.on('message', (msg: AnalysisResponse) => this.onMessage(msg));
switch (msg.requestType) {
case 'log': {
const logData = msg.data as LogData;
this.log(logData.level, logData.message);
break;
}
case 'analysisResult': {
// Change in diagnostics due to host such as file closed rather than
// analyzing files.
this._onAnalysisCompletion(convertAnalysisResults(msg.data));
break;
}
default:
debug.fail(`${msg.requestType} is not expected`);
}
});
// this will catch any exception thrown from background thread, // this will catch any exception thrown from background thread,
// print log and ignore exception // print log and ignore exception
@ -76,6 +58,26 @@ export class BackgroundAnalysisBase {
}); });
} }
protected onMessage(msg: AnalysisResponse) {
switch (msg.requestType) {
case 'log': {
const logData = msg.data as LogData;
this.log(logData.level, logData.message);
break;
}
case 'analysisResult': {
// Change in diagnostics due to host such as file closed rather than
// analyzing files.
this._onAnalysisCompletion(convertAnalysisResults(msg.data));
break;
}
default:
debug.fail(`${msg.requestType} is not expected`);
}
}
setCompletionCallback(callback?: AnalysisCompleteCallback) { setCompletionCallback(callback?: AnalysisCompleteCallback) {
this._onAnalysisCompletion = callback ?? nullCallback; this._onAnalysisCompletion = callback ?? nullCallback;
} }
@ -530,7 +532,7 @@ export interface AnalysisRequest {
port?: MessagePort; port?: MessagePort;
} }
interface AnalysisResponse { export interface AnalysisResponse {
requestType: 'log' | 'analysisResult' | 'analysisPaused' | 'indexResult' | 'analysisDone'; requestType: 'log' | 'telemetry' | 'analysisResult' | 'analysisPaused' | 'indexResult' | 'analysisDone';
data: any; data: any;
} }

View File

@ -127,3 +127,18 @@ export function isDebugMode() {
const argv = process.execArgv.join(); const argv = process.execArgv.join();
return argv.includes('inspect') || argv.includes('debug'); return argv.includes('inspect') || argv.includes('debug');
} }
interface Thenable<T> {
then<TResult>(
onfulfilled?: (value: T) => TResult | Thenable<TResult>,
onrejected?: (reason: any) => TResult | Thenable<TResult>
): Thenable<TResult>;
then<TResult>(
onfulfilled?: (value: T) => TResult | Thenable<TResult>,
onrejected?: (reason: any) => void
): Thenable<TResult>;
}
export function isThenable<T>(v: any): v is Thenable<T> {
return typeof v?.then === 'function';
}

View File

@ -891,6 +891,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
position, position,
workspacePath, workspacePath,
this._completionDocFormat, this._completionDocFormat,
undefined,
token token
); );
} }

View File

@ -114,6 +114,23 @@ export function buildModuleSymbolsMap(files: SourceFileInfo[], token: Cancellati
return moduleSymbolMap; return moduleSymbolMap;
} }
export interface AbbreviationInfo {
importFrom?: string;
importName: string;
}
export function getAutoImportCandidatesForAbbr(
autoImporter: AutoImporter,
abbr: string | undefined,
abbrInfo: AbbreviationInfo,
token: CancellationToken
) {
const exactMatch = 1;
return autoImporter
.getAutoImportCandidates(abbrInfo.importName, exactMatch, abbr, token)
.filter((r) => r.source === abbrInfo.importFrom && r.name === abbrInfo.importName);
}
export interface AutoImportResult { export interface AutoImportResult {
name: string; name: string;
symbol?: Symbol; symbol?: Symbol;

View File

@ -60,7 +60,6 @@ import {
getMembersForModule, getMembersForModule,
isProperty, isProperty,
makeTypeVarsConcrete, makeTypeVarsConcrete,
specializeType,
} from '../analyzer/typeUtils'; } from '../analyzer/typeUtils';
import { throwIfCancellationRequested } from '../common/cancellationUtils'; import { throwIfCancellationRequested } from '../common/cancellationUtils';
import { ConfigOptions } from '../common/configOptions'; import { ConfigOptions } from '../common/configOptions';
@ -85,7 +84,13 @@ import {
StringNode, StringNode,
} from '../parser/parseNodes'; } from '../parser/parseNodes';
import { ParseResults } from '../parser/parser'; import { ParseResults } from '../parser/parser';
import { AutoImporter, ModuleSymbolMap } from './autoImporter'; import {
AbbreviationInfo,
AutoImporter,
AutoImportResult,
getAutoImportCandidatesForAbbr,
ModuleSymbolMap,
} from './autoImporter';
import { IndexResults } from './documentSymbolProvider'; import { IndexResults } from './documentSymbolProvider';
const _keywords: string[] = [ const _keywords: string[] = [
@ -179,6 +184,7 @@ export interface CompletionItemData {
position: Position; position: Position;
autoImportText?: string; autoImportText?: string;
symbolLabel?: string; symbolLabel?: string;
isInImport?: boolean;
} }
// MemberAccessInfo attempts to gather info for unknown types // MemberAccessInfo attempts to gather info for unknown types
@ -193,11 +199,40 @@ export interface CompletionResults {
memberAccessInfo?: MemberAccessInfo; memberAccessInfo?: MemberAccessInfo;
} }
export type AbbreviationMap = Map<string, AbbreviationInfo>;
export interface AutoImportMaps {
nameMap?: AbbreviationMap;
libraryMap?: Map<string, IndexResults>;
getModuleSymbolsMap: () => ModuleSymbolMap;
}
interface RecentCompletionInfo { interface RecentCompletionInfo {
label: string; label: string;
autoImportText: string; autoImportText: string;
} }
interface Edits {
textEdit?: TextEdit;
additionalTextEdits?: TextEditAction[];
}
interface SymbolDetail {
isInImport?: boolean;
autoImportSource?: string;
autoImportAlias?: string;
objectThrough?: ObjectType;
edits?: Edits;
}
interface CompletionDetail {
isInImport?: boolean;
typeDetail?: string;
documentation?: string;
autoImportText?: string;
edits?: Edits;
}
// We'll use a somewhat-arbitrary cutoff value here to determine // We'll use a somewhat-arbitrary cutoff value here to determine
// whether it's sufficiently similar. // whether it's sufficiently similar.
const similarityLimit = 0.25; const similarityLimit = 0.25;
@ -224,8 +259,7 @@ export class CompletionProvider {
private _evaluator: TypeEvaluator, private _evaluator: TypeEvaluator,
private _format: MarkupKind, private _format: MarkupKind,
private _sourceMapper: SourceMapper, private _sourceMapper: SourceMapper,
private _libraryMap: Map<string, IndexResults> | undefined, private _autoImportMaps: AutoImportMaps | undefined,
private _moduleSymbolsCallback: () => ModuleSymbolMap,
private _cancellationToken: CancellationToken private _cancellationToken: CancellationToken
) {} ) {}
@ -513,7 +547,7 @@ export class CompletionProvider {
const methodSignature = this._printMethodSignature(decl.node) + ':'; const methodSignature = this._printMethodSignature(decl.node) + ':';
const textEdit = TextEdit.replace(range, methodSignature); const textEdit = TextEdit.replace(range, methodSignature);
this._addSymbol(name, symbol, partialName.value, completionList, undefined, textEdit); this._addSymbol(name, symbol, partialName.value, completionList, { edits: { textEdit } });
} }
} }
}); });
@ -603,7 +637,7 @@ export class CompletionProvider {
const objectThrough: ObjectType | undefined = isObject(specializedLeftType) const objectThrough: ObjectType | undefined = isObject(specializedLeftType)
? specializedLeftType ? specializedLeftType
: undefined; : undefined;
this._addSymbolsForSymbolTable(symbolTable, (_) => true, priorWord, objectThrough, completionList); this._addSymbolsForSymbolTable(symbolTable, (_) => true, priorWord, false, objectThrough, completionList);
// If we don't know this type, look for a module we should stub // If we don't know this type, look for a module we should stub
if (!leftType || isUnknown(leftType) || isUnbound(leftType)) { if (!leftType || isUnknown(leftType) || isUnbound(leftType)) {
@ -965,46 +999,49 @@ export class CompletionProvider {
} }
private _getAutoImportCompletions(priorWord: string, completionList: CompletionList) { private _getAutoImportCompletions(priorWord: string, completionList: CompletionList) {
const moduleSymbolMap = this._moduleSymbolsCallback(); if (!this._autoImportMaps) {
return;
}
const moduleSymbolMap = this._autoImportMaps.getModuleSymbolsMap();
const excludes = completionList.items.filter((i) => !i.data?.autoImport).map((i) => i.label);
const autoImporter = new AutoImporter( const autoImporter = new AutoImporter(
this._configOptions.findExecEnvironment(this._filePath), this._configOptions.findExecEnvironment(this._filePath),
this._importResolver, this._importResolver,
this._parseResults, this._parseResults,
this._position, this._position,
completionList.items.filter((i) => !i.data?.autoImport).map((i) => i.label), excludes,
moduleSymbolMap, moduleSymbolMap,
this._libraryMap this._autoImportMaps.libraryMap
); );
for (const result of autoImporter.getAutoImportCandidates( const results: AutoImportResult[] = [];
priorWord, const info = this._autoImportMaps.nameMap?.get(priorWord);
similarityLimit, if (info && priorWord.length > 1 && !excludes.some((e) => e === priorWord)) {
undefined, results.push(...getAutoImportCandidatesForAbbr(autoImporter, priorWord, info, this._cancellationToken));
this._cancellationToken }
)) {
results.push(
...autoImporter.getAutoImportCandidates(priorWord, similarityLimit, undefined, this._cancellationToken)
);
for (const result of results) {
if (result.symbol) { if (result.symbol) {
this._addSymbol( this._addSymbol(result.name, result.symbol, priorWord, completionList, {
result.name, autoImportSource: result.source,
result.symbol, autoImportAlias: result.alias,
priorWord, edits: { additionalTextEdits: result.edits },
completionList, });
result.source,
undefined,
result.edits
);
} else { } else {
this._addNameToCompletionList( this._addNameToCompletionList(
result.name, result.alias ?? result.name,
result.kind ?? CompletionItemKind.Module, result.kind ?? CompletionItemKind.Module,
priorWord, priorWord,
completionList, completionList,
undefined, {
'', autoImportText: this._getAutoImportText(result.name, result.source, result.alias),
result.source edits: { additionalTextEdits: result.edits },
? `\`\`\`\nfrom ${result.source} import ${result.name}\n\`\`\`` }
: `\`\`\`\nimport ${result.name}\n\`\`\``,
undefined,
result.edits
); );
} }
} }
@ -1040,6 +1077,7 @@ export class CompletionProvider {
return !importFromNode.imports.find((imp) => imp.name.value === name); return !importFromNode.imports.find((imp) => imp.name.value === name);
}, },
priorWord, priorWord,
true,
undefined, undefined,
completionList completionList
); );
@ -1120,7 +1158,14 @@ export class CompletionProvider {
let scope = AnalyzerNodeInfo.getScope(curNode); let scope = AnalyzerNodeInfo.getScope(curNode);
if (scope) { if (scope) {
while (scope) { while (scope) {
this._addSymbolsForSymbolTable(scope.symbolTable, () => true, priorWord, undefined, completionList); this._addSymbolsForSymbolTable(
scope.symbolTable,
() => true,
priorWord,
false,
undefined,
completionList
);
scope = scope.parent; scope = scope.parent;
} }
@ -1144,6 +1189,7 @@ export class CompletionProvider {
.some((decl) => decl.type === DeclarationType.Variable); .some((decl) => decl.type === DeclarationType.Variable);
}, },
priorWord, priorWord,
false,
undefined, undefined,
completionList completionList
); );
@ -1162,6 +1208,7 @@ export class CompletionProvider {
symbolTable: SymbolTable, symbolTable: SymbolTable,
includeSymbolCallback: (name: string) => boolean, includeSymbolCallback: (name: string) => boolean,
priorWord: string, priorWord: string,
isInImport: boolean,
objectThrough: ObjectType | undefined, objectThrough: ObjectType | undefined,
completionList: CompletionList completionList: CompletionList
) { ) {
@ -1173,16 +1220,10 @@ export class CompletionProvider {
// Don't add a symbol more than once. It may have already been // Don't add a symbol more than once. It may have already been
// added from an inner scope's symbol table. // added from an inner scope's symbol table.
if (!completionList.items.some((item) => item.label === name)) { if (!completionList.items.some((item) => item.label === name)) {
this._addSymbol( this._addSymbol(name, symbol, priorWord, completionList, {
name, objectThrough,
symbol, isInImport,
priorWord, });
completionList,
undefined,
undefined,
undefined,
objectThrough
);
} }
} }
}); });
@ -1193,10 +1234,7 @@ export class CompletionProvider {
symbol: Symbol, symbol: Symbol,
priorWord: string, priorWord: string,
completionList: CompletionList, completionList: CompletionList,
autoImportSource?: string, detail: SymbolDetail
textEdit?: TextEdit,
additionalTextEdits?: TextEditAction[],
objectThrough?: ObjectType
) { ) {
let primaryDecl = getLastTypedDeclaredForSymbol(symbol); let primaryDecl = getLastTypedDeclaredForSymbol(symbol);
if (!primaryDecl) { if (!primaryDecl) {
@ -1235,11 +1273,11 @@ export class CompletionProvider {
break; break;
case DeclarationType.Function: { case DeclarationType.Function: {
const functionType = objectThrough const functionType = detail.objectThrough
? this._evaluator.bindFunctionToClassOrObject(objectThrough, type, false) ? this._evaluator.bindFunctionToClassOrObject(detail.objectThrough, type, false)
: type; : type;
if (functionType) { if (functionType) {
if (isProperty(functionType) && objectThrough) { if (isProperty(functionType) && detail.objectThrough) {
const propertyType = const propertyType =
this._evaluator.getGetterTypeFromProperty( this._evaluator.getGetterTypeFromProperty(
functionType.classType, functionType.classType,
@ -1339,161 +1377,165 @@ export class CompletionProvider {
} }
} }
let autoImportText: string | undefined; const autoImportText = detail.autoImportSource
if (autoImportSource) { ? this._getAutoImportText(name, detail.autoImportSource, detail.autoImportAlias)
if (this._format === MarkupKind.Markdown) { : undefined;
autoImportText = `\`\`\`\nfrom ${autoImportSource} import ${name}\n\`\`\``;
} else if (this._format === MarkupKind.PlainText) {
autoImportText = `from ${autoImportSource} import ${name}`;
} else {
fail(`Unsupported markup type: ${this._format}`);
}
}
this._addNameToCompletionList( this._addNameToCompletionList(detail.autoImportAlias ?? name, itemKind, priorWord, completionList, {
name,
itemKind,
priorWord,
completionList,
undefined,
undefined,
autoImportText, autoImportText,
textEdit, isInImport: detail.isInImport,
additionalTextEdits edits: detail.edits,
); });
} else { } else {
// Does the symbol have no declaration but instead has a synthesized type? // Does the symbol have no declaration but instead has a synthesized type?
const synthesizedType = symbol.getSynthesizedType(); const synthesizedType = symbol.getSynthesizedType();
if (synthesizedType) { if (synthesizedType) {
const itemKind: CompletionItemKind = CompletionItemKind.Variable; const itemKind: CompletionItemKind = CompletionItemKind.Variable;
this._addNameToCompletionList( this._addNameToCompletionList(name, itemKind, priorWord, completionList, {
name, isInImport: detail.isInImport,
itemKind, edits: detail.edits,
priorWord, });
completionList,
undefined,
undefined,
undefined,
textEdit,
additionalTextEdits
);
} }
} }
} }
private _getAutoImportText(importName: string, importFrom?: string, importAlias?: string) {
let autoImportText: string | undefined;
if (!importFrom) {
autoImportText = `import ${importName}`;
} else {
autoImportText = `from ${importFrom} import ${importName}`;
}
if (importAlias) {
autoImportText = `${autoImportText} as ${importAlias}`;
}
if (this._format === MarkupKind.Markdown) {
return `\`\`\`\n${autoImportText}\n\`\`\``;
} else if (this._format === MarkupKind.PlainText) {
return autoImportText;
} else {
fail(`Unsupported markup type: ${this._format}`);
}
}
private _addNameToCompletionList( private _addNameToCompletionList(
name: string, name: string,
itemKind: CompletionItemKind, itemKind: CompletionItemKind,
filter: string, filter: string,
completionList: CompletionList, completionList: CompletionList,
typeDetail?: string, detail?: CompletionDetail
documentation?: string,
autoImportText?: string,
textEdit?: TextEdit,
additionalTextEdits?: TextEditAction[]
) { ) {
const similarity = StringUtils.computeCompletionSimilarity(filter, name); // Auto importer already filtered out unnecessary ones. No need to do it again.
const similarity = detail?.autoImportText ? 1 : StringUtils.computeCompletionSimilarity(filter, name);
if (similarity > similarityLimit) { if (similarity <= similarityLimit) {
const completionItem = CompletionItem.create(name); return;
completionItem.kind = itemKind;
const completionItemData: CompletionItemData = {
workspacePath: this._workspacePath,
filePath: this._filePath,
position: this._position,
};
completionItem.data = completionItemData;
if (autoImportText) {
// Force auto-import entries to the end.
completionItem.sortText = this._makeSortText(SortCategory.AutoImport, name, autoImportText);
completionItemData.autoImportText = autoImportText;
completionItem.detail = 'Auto-import';
} else if (SymbolNameUtils.isDunderName(name)) {
// Force dunder-named symbols to appear after all other symbols.
completionItem.sortText = this._makeSortText(SortCategory.DunderSymbol, name);
} else if (filter === '' && SymbolNameUtils.isPrivateOrProtectedName(name)) {
// Distinguish between normal and private symbols only if there is
// currently no filter text. Once we get a single character to filter
// upon, we'll no longer differentiate.
completionItem.sortText = this._makeSortText(SortCategory.PrivateSymbol, name);
} else {
completionItem.sortText = this._makeSortText(SortCategory.NormalSymbol, name);
}
completionItemData.symbolLabel = name;
if (this._format === MarkupKind.Markdown) {
let markdownString = '';
if (autoImportText) {
markdownString += autoImportText + '\n\n';
}
if (typeDetail) {
markdownString += '```python\n' + typeDetail + '\n```\n';
}
if (documentation) {
markdownString += '---\n';
markdownString += convertDocStringToMarkdown(documentation);
}
markdownString = markdownString.trimEnd();
if (markdownString) {
completionItem.documentation = {
kind: MarkupKind.Markdown,
value: markdownString,
};
}
} else if (this._format === MarkupKind.PlainText) {
let plainTextString = '';
if (autoImportText) {
plainTextString += autoImportText + '\n\n';
}
if (typeDetail) {
plainTextString += typeDetail + '\n';
}
if (documentation) {
plainTextString += '\n' + convertDocStringToPlainText(documentation);
}
plainTextString = plainTextString.trimEnd();
if (plainTextString) {
completionItem.documentation = {
kind: MarkupKind.PlainText,
value: plainTextString,
};
}
} else {
fail(`Unsupported markup type: ${this._format}`);
}
if (textEdit) {
completionItem.textEdit = textEdit;
}
if (additionalTextEdits) {
completionItem.additionalTextEdits = additionalTextEdits.map((te) => {
const textEdit: TextEdit = {
range: {
start: { line: te.range.start.line, character: te.range.start.character },
end: { line: te.range.end.line, character: te.range.end.character },
},
newText: te.replacementText,
};
return textEdit;
});
}
completionList.items.push(completionItem);
} }
const completionItem = CompletionItem.create(name);
completionItem.kind = itemKind;
const completionItemData: CompletionItemData = {
workspacePath: this._workspacePath,
filePath: this._filePath,
position: this._position,
};
if (detail?.isInImport) {
completionItemData.isInImport = true;
}
completionItem.data = completionItemData;
if (detail?.autoImportText) {
// Force auto-import entries to the end.
completionItem.sortText = this._makeSortText(SortCategory.AutoImport, name, detail.autoImportText);
completionItemData.autoImportText = detail.autoImportText;
completionItem.detail = 'Auto-import';
} else if (SymbolNameUtils.isDunderName(name)) {
// Force dunder-named symbols to appear after all other symbols.
completionItem.sortText = this._makeSortText(SortCategory.DunderSymbol, name);
} else if (filter === '' && SymbolNameUtils.isPrivateOrProtectedName(name)) {
// Distinguish between normal and private symbols only if there is
// currently no filter text. Once we get a single character to filter
// upon, we'll no longer differentiate.
completionItem.sortText = this._makeSortText(SortCategory.PrivateSymbol, name);
} else {
completionItem.sortText = this._makeSortText(SortCategory.NormalSymbol, name);
}
completionItemData.symbolLabel = name;
if (this._format === MarkupKind.Markdown) {
let markdownString = '';
if (detail?.autoImportText) {
markdownString += detail.autoImportText + '\n\n';
}
if (detail?.typeDetail) {
markdownString += '```python\n' + detail.typeDetail + '\n```\n';
}
if (detail?.documentation) {
markdownString += '---\n';
markdownString += convertDocStringToMarkdown(detail.documentation);
}
markdownString = markdownString.trimEnd();
if (markdownString) {
completionItem.documentation = {
kind: MarkupKind.Markdown,
value: markdownString,
};
}
} else if (this._format === MarkupKind.PlainText) {
let plainTextString = '';
if (detail?.autoImportText) {
plainTextString += detail.autoImportText + '\n\n';
}
if (detail?.typeDetail) {
plainTextString += detail.typeDetail + '\n';
}
if (detail?.documentation) {
plainTextString += '\n' + convertDocStringToPlainText(detail.documentation);
}
plainTextString = plainTextString.trimEnd();
if (plainTextString) {
completionItem.documentation = {
kind: MarkupKind.PlainText,
value: plainTextString,
};
}
} else {
fail(`Unsupported markup type: ${this._format}`);
}
if (detail?.edits?.textEdit) {
completionItem.textEdit = detail.edits.textEdit;
}
if (detail?.edits?.additionalTextEdits) {
completionItem.additionalTextEdits = detail.edits.additionalTextEdits.map((te) => {
const textEdit: TextEdit = {
range: {
start: { line: te.range.start.line, character: te.range.start.character },
end: { line: te.range.end.line, character: te.range.end.character },
},
newText: te.replacementText,
};
return textEdit;
});
}
completionList.items.push(completionItem);
} }
private _getRecentListIndex(name: string, autoImportText: string) { private _getRecentListIndex(name: string, autoImportText: string) {

View File

@ -131,6 +131,11 @@ declare namespace _ {
kind?: DocumentHighlightKind; kind?: DocumentHighlightKind;
} }
interface AbbreviationInfo {
importFrom?: string;
importName: string;
}
interface Fourslash { interface Fourslash {
getDocumentHighlightKind(m?: Marker): DocumentHighlightKind | undefined; getDocumentHighlightKind(m?: Marker): DocumentHighlightKind | undefined;
@ -190,7 +195,8 @@ declare namespace _ {
unknownMemberName?: string; unknownMemberName?: string;
}; };
}; };
} },
abbrMap?: { [abbr: string]: AbbreviationInfo }
): Promise<void>; ): Promise<void>;
verifySignature(map: { verifySignature(map: {
[marker: string]: { [marker: string]: {

View File

@ -47,6 +47,7 @@ import { getStringComparer } from '../../../common/stringUtils';
import { DocumentRange, Position, Range as PositionRange, rangesAreEqual, TextRange } from '../../../common/textRange'; import { DocumentRange, Position, Range as PositionRange, rangesAreEqual, TextRange } from '../../../common/textRange';
import { TextRangeCollection } from '../../../common/textRangeCollection'; import { TextRangeCollection } from '../../../common/textRangeCollection';
import { LanguageServerInterface, WorkspaceServiceInstance } from '../../../languageServerBase'; import { LanguageServerInterface, WorkspaceServiceInstance } from '../../../languageServerBase';
import { AbbreviationInfo } from '../../../languageService/autoImporter';
import { convertHoverResults } from '../../../languageService/hoverProvider'; import { convertHoverResults } from '../../../languageService/hoverProvider';
import { ParseResults } from '../../../parser/parser'; import { ParseResults } from '../../../parser/parser';
import { Tokenizer } from '../../../parser/tokenizer'; import { Tokenizer } from '../../../parser/tokenizer';
@ -94,6 +95,7 @@ export class TestState {
readonly fs: vfs.TestFileSystem; readonly fs: vfs.TestFileSystem;
readonly workspace: WorkspaceServiceInstance; readonly workspace: WorkspaceServiceInstance;
readonly console: ConsoleInterface; readonly console: ConsoleInterface;
readonly rawConfigJson: any | undefined;
// The current caret position in the active file // The current caret position in the active file
currentCaretPosition = 0; currentCaretPosition = 0;
@ -124,14 +126,13 @@ export class TestState {
for (const file of testData.files) { for (const file of testData.files) {
// if one of file is configuration file, set config options from the given json // if one of file is configuration file, set config options from the given json
if (this._isConfig(file, ignoreCase)) { if (this._isConfig(file, ignoreCase)) {
let configJson: any;
try { try {
configJson = JSON.parse(file.content); this.rawConfigJson = JSON.parse(file.content);
} catch (e) { } catch (e) {
throw new Error(`Failed to parse test ${file.fileName}: ${e.message}`); throw new Error(`Failed to parse test ${file.fileName}: ${e.message}`);
} }
configOptions.initializeFromJson(configJson, 'basic', nullConsole); configOptions.initializeFromJson(this.rawConfigJson, 'basic', nullConsole);
this._applyTestConfigOptions(configOptions); this._applyTestConfigOptions(configOptions);
} else { } else {
files[file.fileName] = new vfs.File(file.content, { meta: file.fileOptions, encoding: 'utf8' }); files[file.fileName] = new vfs.File(file.content, { meta: file.fileOptions, encoding: 'utf8' });
@ -817,7 +818,8 @@ export class TestState {
unknownMemberName?: string; unknownMemberName?: string;
}; };
}; };
} },
abbrMap?: { [abbr: string]: AbbreviationInfo }
): Promise<void> { ): Promise<void> {
this._analyze(); this._analyze();
@ -836,6 +838,7 @@ export class TestState {
completionPosition, completionPosition,
this.workspace.rootPath, this.workspace.rootPath,
docFormat, docFormat,
abbrMap ? new Map<string, AbbreviationInfo>(Object.entries(abbrMap)) : undefined,
CancellationToken.None CancellationToken.None
); );
@ -866,23 +869,16 @@ export class TestState {
} }
const actual: CompletionItem = result.completionList.items[actualIndex]; const actual: CompletionItem = result.completionList.items[actualIndex];
assert.equal(actual.label, expected.label); this.verifyCompletionItem(expected, actual);
assert.equal(actual.detail, expected.detail);
assert.equal(actual.kind, expected.kind); if (expected.documentation !== undefined) {
if (expectedCompletions[i].documentation !== undefined) {
if (actual.documentation === undefined) { if (actual.documentation === undefined) {
this.program.resolveCompletionItem( this.program.resolveCompletionItem(filePath, actual, docFormat, CancellationToken.None);
filePath,
actual,
docFormat,
undefined,
CancellationToken.None
);
} }
if (MarkupContent.is(actual.documentation)) { if (MarkupContent.is(actual.documentation)) {
assert.equal(actual.documentation.value, expected.documentation); assert.strictEqual(actual.documentation.value, expected.documentation);
assert.equal(actual.documentation.kind, docFormat); assert.strictEqual(actual.documentation.kind, docFormat);
} else { } else {
assert.fail( assert.fail(
`Unexpected type of contents object "${actual.documentation}", should be MarkupContent.` `Unexpected type of contents object "${actual.documentation}", should be MarkupContent.`
@ -1551,4 +1547,10 @@ export class TestState {
} }
} }
} }
protected verifyCompletionItem(expected: _.FourSlashCompletionItem, actual: CompletionItem) {
assert.strictEqual(actual.label, expected.label);
assert.strictEqual(actual.detail, expected.detail);
assert.strictEqual(actual.kind, expected.kind);
}
} }

View File

@ -37,6 +37,7 @@ import {
} from 'vscode-languageclient/node'; } from 'vscode-languageclient/node';
import { Commands } from 'pyright-internal/commands/commands'; import { Commands } from 'pyright-internal/commands/commands';
import { isThenable } from 'pyright-internal/common/core';
import { FileBasedCancellationStrategy } from './cancellationUtils'; import { FileBasedCancellationStrategy } from './cancellationUtils';
import { ProgressReporting } from './progress'; import { ProgressReporting } from './progress';
@ -279,10 +280,6 @@ async function getPythonPathFromPythonExtension(
return undefined; return undefined;
} }
function isThenable<T>(v: any): v is Thenable<T> {
return typeof v?.then === 'function';
}
function installPythonPathChangedListener( function installPythonPathChangedListener(
onDidChangeExecutionDetails: (callback: () => void) => void, onDidChangeExecutionDetails: (callback: () => void) => void,
scopeUri: Uri | undefined, scopeUri: Uri | undefined,