Improve completion context detection, respect sig help doc kind, helpers (#1160)

This commit is contained in:
Jake Bailey 2020-11-11 13:42:26 -08:00 committed by GitHub
parent cc15c141bd
commit 54be42d245
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 947 additions and 70 deletions

View File

@ -2963,3 +2963,17 @@ export class YieldFinder extends ParseTreeWalker {
return false;
}
}
export class ReturnFinder extends ParseTreeWalker {
private _containsReturn = false;
checkContainsReturn(node: ParseNode) {
this.walk(node);
return this._containsReturn;
}
visitReturn(node: ReturnNode): boolean {
this._containsReturn = true;
return false;
}
}

View File

@ -550,7 +550,11 @@ export function getEnclosingClassOrFunction(node: ParseNode): FunctionNode | Cla
return undefined;
}
export function getEnclosingSuiteOrModule(node: ParseNode): SuiteNode | ModuleNode | undefined {
export function getEnclosingSuiteOrModule(
node: ParseNode,
stopAtFunction = false,
stopAtLambda = true
): SuiteNode | ModuleNode | undefined {
let curNode = node.parent;
while (curNode) {
if (curNode.nodeType === ParseNodeType.Suite) {
@ -562,7 +566,15 @@ export function getEnclosingSuiteOrModule(node: ParseNode): SuiteNode | ModuleNo
}
if (curNode.nodeType === ParseNodeType.Lambda) {
return undefined;
if (stopAtLambda) {
return undefined;
}
}
if (curNode.nodeType === ParseNodeType.Function) {
if (stopAtFunction) {
return undefined;
}
}
curNode = curNode.parent;

View File

@ -1330,6 +1330,7 @@ export class Program {
getSignatureHelpForPosition(
filePath: string,
position: Position,
format: MarkupKind,
token: CancellationToken
): SignatureHelpResults | undefined {
return this._runEvaluatorWithCancellationToken(token, () => {
@ -1344,6 +1345,7 @@ export class Program {
position,
this._lookUpImport,
this._evaluator!,
format,
token
);
});

View File

@ -13,7 +13,6 @@ import {
CancellationToken,
CompletionItem,
DocumentSymbol,
SymbolInformation,
} from 'vscode-languageserver';
import { TextDocumentContentChangeEvent } from 'vscode-languageserver-textdocument';
import {
@ -57,6 +56,7 @@ import { BackgroundAnalysisProgram, BackgroundAnalysisProgramFactory } from './b
import { ImportedModuleDescriptor, ImportResolver, ImportResolverFactory } from './importResolver';
import { MaxAnalysisTime } from './program';
import { findPythonSearchPaths, getPythonPathFromPythonInterpreter } from './pythonPathUtils';
import { TypeEvaluator } from './typeEvaluator';
export const configFileNames = ['pyrightconfig.json', 'mspythonconfig.json'];
@ -285,9 +285,10 @@ export class AnalyzerService {
getSignatureHelpForPosition(
filePath: string,
position: Position,
format: MarkupKind,
token: CancellationToken
): SignatureHelpResults | undefined {
return this._program.getSignatureHelpForPosition(filePath, position, token);
return this._program.getSignatureHelpForPosition(filePath, position, format, token);
}
getCompletionsForPosition(
@ -309,6 +310,10 @@ export class AnalyzerService {
);
}
getEvaluator(): TypeEvaluator | undefined {
return this._program.evaluator;
}
resolveCompletionItem(
filePath: string,
completionItem: CompletionItem,

View File

@ -771,6 +771,7 @@ export class SourceFile {
position: Position,
importLookup: ImportLookup,
evaluator: TypeEvaluator,
format: MarkupKind,
token: CancellationToken
): SignatureHelpResults | undefined {
// If we have no completed analysis job, there's nothing to do.
@ -778,7 +779,13 @@ export class SourceFile {
return undefined;
}
return SignatureHelpProvider.getSignatureHelpForPosition(this._parseResults, position, evaluator, token);
return SignatureHelpProvider.getSignatureHelpForPosition(
this._parseResults,
position,
evaluator,
format,
token
);
}
getCompletionsForPosition(

View File

@ -7,10 +7,9 @@
* Collection of functions that operate on Symbol objects.
*/
import { ParseNodeType } from '../parser/parseNodes';
import { Declaration, DeclarationType } from './declaration';
import { isFinalVariableDeclaration } from './declarationUtils';
import { Symbol, SymbolTable } from './symbol';
import { Symbol } from './symbol';
export function getLastTypedDeclaredForSymbol(symbol: Symbol): Declaration | undefined {
const typedDecls = symbol.getTypedDeclarations();

View File

@ -173,6 +173,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
protected _hasHierarchicalDocumentSymbolCapability = false;
protected _hoverContentFormat: MarkupKind = MarkupKind.PlainText;
protected _completionDocFormat: MarkupKind = MarkupKind.PlainText;
protected _signatureDocFormat: MarkupKind = MarkupKind.PlainText;
protected _supportsUnnecessaryDiagnosticTag = false;
protected _defaultClientConfig: any;
@ -589,6 +590,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
const signatureHelpResults = workspace.serviceInstance.getSignatureHelpForPosition(
filePath,
position,
this._signatureDocFormat,
token
);
if (!signatureHelpResults) {
@ -606,7 +608,8 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
);
}
const sigInfo = SignatureInformation.create(sig.label, sig.documentation, ...paramInfo);
const sigInfo = SignatureInformation.create(sig.label, undefined, ...paramInfo);
sigInfo.documentation = sig.documentation;
sigInfo.activeParameter = sig.activeParameter;
return sigInfo;
});
@ -934,6 +937,9 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
this._completionDocFormat = this._getCompatibleMarkupKind(
capabilities.textDocument?.completion?.completionItem?.documentationFormat
);
this._signatureDocFormat = this._getCompatibleMarkupKind(
capabilities.textDocument?.signatureHelp?.signatureInformation?.documentationFormat
);
const supportedDiagnosticTags = capabilities.textDocument?.publishDiagnostics?.tagSupport?.valueSet || [];
this._supportsUnnecessaryDiagnosticTag = supportedDiagnosticTags.some(
(tag) => tag === DiagnosticTag.Unnecessary

View File

@ -72,6 +72,7 @@ import { comparePositions, Position } from '../common/textRange';
import { TextRange } from '../common/textRange';
import { TextRangeCollection } from '../common/textRangeCollection';
import {
DecoratorNode,
ErrorExpressionCategory,
ErrorNode,
ExpressionNode,
@ -356,26 +357,12 @@ export class CompletionProvider {
}
if (curNode.nodeType === ParseNodeType.Name) {
// Are we within a "from X import Y as Z" statement and
// more specifically within the "Y"?
if (curNode.parent && curNode.parent.nodeType === ParseNodeType.ModuleName) {
return this._getImportModuleCompletions(curNode.parent);
} else if (curNode.parent && curNode.parent.nodeType === ParseNodeType.ImportFromAs) {
const parentNode = curNode.parent.parent;
if (parentNode && parentNode.nodeType === ParseNodeType.ImportFrom) {
if (curNode.parent.name === curNode) {
return this._getImportFromCompletions(parentNode, priorWord);
} else {
return this._getImportFromCompletions(parentNode, '');
}
}
} else if (
curNode.parent &&
curNode.parent.nodeType === ParseNodeType.MemberAccess &&
curNode === curNode.parent.memberName
) {
return this._getMemberAccessCompletions(curNode.parent.leftExpression, priorWord);
// This condition is little different than others since it does its own
// tree walk up to find context and let outer tree walk up to proceed if it can't find
// one to show completion.
const result = this._tryGetNameCompletions(curNode, offset, priorWord);
if (result || result === undefined) {
return result;
}
}
@ -387,10 +374,47 @@ export class CompletionProvider {
return this._getExpressionCompletions(curNode, priorWord, priorText, postText);
}
if (curNode.nodeType === ParseNodeType.Suite || curNode.nodeType === ParseNodeType.Module) {
if (curNode.nodeType === ParseNodeType.Suite) {
if (
curNode.parent &&
curNode.parent.nodeType === ParseNodeType.Except &&
!curNode.parent.name &&
curNode.parent.typeExpression &&
TextRange.getEnd(curNode.parent.typeExpression) < offset &&
offset <= curNode.parent.exceptSuite.start
) {
// except Exception as [<empty>]
return undefined;
}
if (
curNode.parent &&
curNode.parent.nodeType === ParseNodeType.Class &&
(!curNode.parent.name || !curNode.parent.name.value) &&
curNode.parent.arguments.length === 0 &&
offset <= curNode.parent.suite.start
) {
// class [<empty>]
return undefined;
}
return this._getStatementCompletions(curNode, priorWord, priorText, postText);
}
if (curNode.nodeType === ParseNodeType.Module) {
return this._getStatementCompletions(curNode, priorWord, priorText, postText);
}
if (
curNode.nodeType === ParseNodeType.Parameter &&
curNode.length === 0 &&
curNode.parent &&
curNode.parent.nodeType === ParseNodeType.Lambda
) {
// lambda [<empty>] or lambda x, [<empty>]
return undefined;
}
if (!curNode.parent) {
break;
}
@ -446,6 +470,96 @@ export class CompletionProvider {
}
}
private _tryGetNameCompletions(curNode: NameNode, offset: number, priorWord: string) {
if (!curNode.parent) {
return false;
}
if (curNode.parent.nodeType === ParseNodeType.ImportAs && curNode.parent.alias === curNode) {
// Are we within a "import Y as [Z]"?
return undefined;
}
if (curNode.parent.nodeType === ParseNodeType.ModuleName) {
// Are we within a "import Y as [<empty>]"?
if (
curNode.parent.parent &&
curNode.parent.parent.nodeType === ParseNodeType.ImportAs &&
!curNode.parent.parent.alias &&
TextRange.getEnd(curNode.parent.parent) < offset
) {
return undefined;
}
// Are we within a "from X import Y as Z" statement and
// more specifically within the "Y"?
return this._getImportModuleCompletions(curNode.parent);
}
if (curNode.parent.nodeType === ParseNodeType.ImportFromAs) {
if (curNode.parent.alias === curNode) {
// Are we within a "from X import Y as [Z]"?
return undefined;
}
const parentNode = curNode.parent.parent;
if (parentNode && parentNode.nodeType === ParseNodeType.ImportFrom) {
// Are we within a "from X import Y as [<empty>]"?
if (!curNode.parent.alias && TextRange.getEnd(curNode.parent) < offset) {
return undefined;
}
if (curNode.parent.name === curNode) {
return this._getImportFromCompletions(parentNode, priorWord);
}
return this._getImportFromCompletions(parentNode, '');
}
return false;
}
if (curNode.parent.nodeType === ParseNodeType.MemberAccess && curNode === curNode.parent.memberName) {
return this._getMemberAccessCompletions(curNode.parent.leftExpression, priorWord);
}
if (curNode.parent.nodeType === ParseNodeType.Except && curNode === curNode.parent.name) {
return undefined;
}
if (curNode.parent.nodeType === ParseNodeType.Function && curNode === curNode.parent.name) {
if (curNode.parent.decorators?.some((d) => this._isOverload(d))) {
return this._getMethodOverloadsCompletions(curNode);
}
return undefined;
}
if (curNode.parent.nodeType === ParseNodeType.Parameter && curNode === curNode.parent.name) {
return undefined;
}
if (curNode.parent.nodeType === ParseNodeType.Class && curNode === curNode.parent.name) {
return undefined;
}
if (
curNode.parent.nodeType === ParseNodeType.For &&
TextRange.contains(curNode.parent.targetExpression, curNode.start)
) {
return undefined;
}
if (
curNode.parent.nodeType === ParseNodeType.ListComprehensionFor &&
TextRange.contains(curNode.parent.targetExpression, curNode.start)
) {
return undefined;
}
return false;
}
private _isWithinComment(offset: number): boolean {
const token = getTokenAfter(offset, this._parseResults.tokenizerOutput.tokens);
if (!token) {
@ -523,6 +637,10 @@ export class CompletionProvider {
case ErrorExpressionCategory.MissingFunctionParameterList: {
if (node.child && node.child.nodeType === ParseNodeType.Name) {
if (node.decorators?.some((d) => this._isOverload(d))) {
return this._getMethodOverloadsCompletions(node.child);
}
// Determine if the partial name is a method that's overriding
// a method in a base class.
return this._getMethodOverrideCompletions(node.child);
@ -534,6 +652,10 @@ export class CompletionProvider {
return undefined;
}
private _isOverload(d: DecoratorNode): unknown {
return d.expression.nodeType === ParseNodeType.Name && d.expression.value === 'overload';
}
private _createSingleKeywordCompletionList(keyword: string): CompletionResults {
const completionItem = CompletionItem.create(keyword);
completionItem.kind = CompletionItemKind.Keyword;
@ -542,6 +664,75 @@ export class CompletionProvider {
return { completionList };
}
private _getMethodOverloadsCompletions(partialName: NameNode): CompletionResults | undefined {
const symbolTable = getSymbolTable(this._evaluator, partialName);
if (!symbolTable) {
return undefined;
}
const completionList = CompletionList.create();
const enclosingFunc = ParseTreeUtils.getEnclosingFunction(partialName);
symbolTable.forEach((symbol, name) => {
const decl = getLastTypedDeclaredForSymbol(symbol);
if (!decl || decl.type !== DeclarationType.Function) {
return;
}
if (!decl.node.decorators.some((d) => this._isOverload(d))) {
// Only consider ones that have overload decorator.
return;
}
const decls = symbol.getDeclarations();
if (decls.length === 1 && decls.some((d) => d.node === enclosingFunc)) {
// Don't show itself.
return;
}
const isSimilar = StringUtils.computeCompletionSimilarity(partialName.value, name) > similarityLimit;
if (isSimilar) {
const range: Range = {
start: { line: this._position.line, character: this._position.character - partialName.length },
end: { line: this._position.line, character: this._position.character },
};
const textEdit = TextEdit.replace(range, decl.node.name.value);
this._addSymbol(name, symbol, partialName.value, completionList, { edits: { textEdit } });
}
});
return { completionList };
function getSymbolTable(evaluator: TypeEvaluator, partialName: NameNode) {
const enclosingClass = ParseTreeUtils.getEnclosingClass(partialName, false);
if (enclosingClass) {
const classResults = evaluator.getTypeOfClass(enclosingClass);
if (!classResults) {
return undefined;
}
const symbolTable = new Map<string, Symbol>();
for (const mroClass of classResults.classType.details.mro) {
if (isClass(mroClass)) {
getMembersForClass(mroClass, symbolTable, false);
}
}
return symbolTable;
}
// For function overload, we only care about top level functions
const moduleNode = ParseTreeUtils.getEnclosingModule(partialName);
if (moduleNode) {
const moduleScope = AnalyzerNodeInfo.getScope(moduleNode);
return moduleScope?.symbolTable;
}
return undefined;
}
}
private _getMethodOverrideCompletions(partialName: NameNode): CompletionResults | undefined {
const enclosingClass = ParseTreeUtils.getEnclosingClass(partialName, true);
if (!enclosingClass) {
@ -928,7 +1119,13 @@ export class CompletionProvider {
this._addLiteralValuesForTargetType(declaredTypeOfTarget, priorText, postText, completionList);
}
} else {
this._addCallArgumentCompletions(parseNode, priorWord, priorText, postText, completionList);
// Make sure we are not inside of the string literal.
debug.assert(parseNode.nodeType === ParseNodeType.String);
const offset = convertPositionToOffset(this._position, this._parseResults.tokenizerOutput.lines)!;
if (offset <= parentNode.start || TextRange.getEnd(parseNode) <= offset) {
this._addCallArgumentCompletions(parseNode, priorWord, priorText, postText, completionList);
}
}
return { completionList };

View File

@ -54,7 +54,7 @@ export class ReferencesResult {
}
}
class FindReferencesTreeWalker extends ParseTreeWalker {
export class FindReferencesTreeWalker extends ParseTreeWalker {
private readonly _locationsFound: DocumentRange[] = [];
constructor(
@ -68,8 +68,8 @@ class FindReferencesTreeWalker extends ParseTreeWalker {
super();
}
findReferences() {
this.walk(this._parseResults.parseTree);
findReferences(rootNode = this._parseResults.parseTree) {
this.walk(rootNode);
return this._locationsFound;
}

View File

@ -9,8 +9,9 @@
* arguments for the call.
*/
import { CancellationToken } from 'vscode-languageserver';
import { CancellationToken, MarkupContent, MarkupKind } from 'vscode-languageserver';
import { convertDocStringToMarkdown, convertDocStringToPlainText } from '../analyzer/docStringConversion';
import { extractParameterDocumentation } from '../analyzer/docStringUtils';
import * as ParseTreeUtils from '../analyzer/parseTreeUtils';
import { CallSignature, TypeEvaluator } from '../analyzer/typeEvaluator';
@ -28,7 +29,7 @@ export interface ParamInfo {
export interface SignatureInfo {
label: string;
documentation?: string;
documentation?: MarkupContent;
parameters?: ParamInfo[];
activeParameter?: number;
}
@ -43,6 +44,7 @@ export class SignatureHelpProvider {
parseResults: ParseResults,
position: Position,
evaluator: TypeEvaluator,
format: MarkupKind,
token: CancellationToken
): SignatureHelpResults | undefined {
throwIfCancellationRequested(token);
@ -80,7 +82,7 @@ export class SignatureHelpProvider {
return undefined;
}
const signatures = callSignatureInfo.signatures.map((sig) => this._makeSignature(sig, evaluator));
const signatures = callSignatureInfo.signatures.map((sig) => this._makeSignature(sig, evaluator, format));
const callHasParameters = !!callSignatureInfo.callNode.arguments?.length;
return {
@ -89,7 +91,11 @@ export class SignatureHelpProvider {
};
}
private static _makeSignature(signature: CallSignature, evaluator: TypeEvaluator): SignatureInfo {
private static _makeSignature(
signature: CallSignature,
evaluator: TypeEvaluator,
format: MarkupKind
): SignatureInfo {
const functionType = signature.type;
const stringParts = evaluator.printFunctionParts(functionType);
const parameters: ParamInfo[] = [];
@ -124,10 +130,23 @@ export class SignatureHelpProvider {
const sigInfo: SignatureInfo = {
label,
parameters,
documentation: functionDocString,
activeParameter,
};
if (functionDocString) {
if (format === MarkupKind.Markdown) {
sigInfo.documentation = {
kind: MarkupKind.Markdown,
value: convertDocStringToMarkdown(functionDocString),
};
} else {
sigInfo.documentation = {
kind: MarkupKind.PlainText,
value: convertDocStringToPlainText(functionDocString),
};
}
}
return sigInfo;
}
}

View File

@ -701,10 +701,16 @@ export interface ErrorNode extends ParseNodeBase {
readonly nodeType: ParseNodeType.Error;
readonly category: ErrorExpressionCategory;
readonly child?: ExpressionNode;
readonly decorators?: DecoratorNode[];
}
export namespace ErrorNode {
export function create(initialRange: TextRange, category: ErrorExpressionCategory, child?: ExpressionNode) {
export function create(
initialRange: TextRange,
category: ErrorExpressionCategory,
child?: ExpressionNode,
decorators?: DecoratorNode[]
) {
const node: ErrorNode = {
start: initialRange.start,
length: initialRange.length,
@ -712,6 +718,7 @@ export namespace ErrorNode {
id: _nextNodeId++,
category,
child,
decorators,
};
if (child) {
@ -719,6 +726,16 @@ export namespace ErrorNode {
extendRange(node, child);
}
if (decorators) {
decorators.forEach((decorator) => {
decorator.parent = node;
});
if (decorators.length > 0) {
extendRange(node, decorators[0]);
}
}
return node;
}
}

View File

@ -720,7 +720,12 @@ export class Parser {
const nameToken = this._getTokenIfIdentifier();
if (!nameToken) {
this._addError(Localizer.Diagnostic.expectedFunctionName(), defToken);
return ErrorNode.create(defToken, ErrorExpressionCategory.MissingFunctionParameterList);
return ErrorNode.create(
defToken,
ErrorExpressionCategory.MissingFunctionParameterList,
undefined,
decorators
);
}
if (!this._consumeTokenIfType(TokenType.OpenParenthesis)) {
@ -728,7 +733,8 @@ export class Parser {
return ErrorNode.create(
nameToken,
ErrorExpressionCategory.MissingFunctionParameterList,
NameNode.create(nameToken)
NameNode.create(nameToken),
decorators
);
}

View File

@ -0,0 +1,35 @@
/// <reference path="fourslash.ts" />
// @filename: test.py
//// class [|/*marker1*/|]
////
// @filename: test1.py
//// class c[|/*marker2*/|]
////
// @filename: test2.py
//// class c1[|/*marker3*/|]():
//// pass
////
// @filename: test3.py
//// class c1([|/*marker4*/|]):
//// pass
////
{
helper.openFiles(helper.getMarkers().map((m) => m.fileName));
// @ts-ignore
await helper.verifyCompletion('exact', 'markdown', {
marker1: { completions: [] },
marker2: { completions: [] },
marker3: { completions: [] },
});
// @ts-ignore
await helper.verifyCompletion('included', 'markdown', {
marker4: { completions: [{ label: 'Exception', kind: Consts.CompletionItemKind.Class }] },
});
}

View File

@ -0,0 +1,18 @@
/// <reference path="fourslash.ts" />
// @filename: test.py
//// try:
//// pass
//// except ZeroDivisionError as d[|/*marker1*/|]:
//// pass
////
//// try:
//// pass
//// except ZeroDivisionError as [|/*marker2*/|]:
//// pass
// @ts-ignore
await helper.verifyCompletion('exact', 'markdown', {
marker1: { completions: [] },
marker2: { completions: [] },
});

View File

@ -0,0 +1,30 @@
/// <reference path="fourslash.ts" />
// @filename: test1.py
//// for [|/*marker1*/|]
////
// @filename: test2.py
//// for c[|/*marker2*/|]
////
// @filename: test3.py
//// for c1[|/*marker3*/|] in [1, 2]:
//// pass
////
// @filename: test4.py
//// [c for c[|/*marker4*/|] in [1, 2]]
////
{
helper.openFiles(helper.getMarkers().map((m) => m.fileName));
// @ts-ignore
await helper.verifyCompletion('exact', 'markdown', {
marker1: { completions: [{ label: 'in', kind: Consts.CompletionItemKind.Keyword }] },
marker2: { completions: [] },
marker3: { completions: [] },
marker4: { completions: [] },
});
}

View File

@ -0,0 +1,15 @@
/// <reference path="fourslash.ts" />
// @filename: test.py
//// import os as o[|/*marker1*/|]
//// import os as [|/*marker2*/|]
//// from os import path as p[|/*marker3*/|]
//// from os import path as [|/*marker4*/|]
// @ts-ignore
await helper.verifyCompletion('exact', 'markdown', {
marker1: { completions: [] },
marker2: { completions: [] },
marker3: { completions: [] },
marker4: { completions: [] },
});

View File

@ -0,0 +1,45 @@
/// <reference path="fourslash.ts" />
// @filename: test.py
//// lambda [|/*marker1*/|]
// @filename: test1.py
//// lambda x[|/*marker2*/|]
// @filename: test2.py
//// lambda x[|/*marker3*/|]:
// @filename: test3.py
//// lambda x, [|/*marker4*/|]
// @filename: test4.py
//// lambda x, [|/*marker5*/|]:
// @filename: test5.py
//// lambda x, y[|/*marker6*/|]
// @filename: test6.py
//// lambda x, y[|/*marker7*/|]:
// @filename: test7.py
//// lambda x: [|/*marker8*/|]
{
helper.openFiles(helper.getMarkers().map((m) => m.fileName));
// @ts-ignore
await helper.verifyCompletion('exact', 'markdown', {
marker1: { completions: [] },
marker2: { completions: [] },
marker3: { completions: [] },
marker4: { completions: [] },
marker5: { completions: [] },
marker6: { completions: [] },
marker7: { completions: [] },
});
// @ts-ignore
await helper.verifyCompletion('included', 'markdown', {
marker8: { completions: [{ label: 'str', kind: Consts.CompletionItemKind.Class }] },
});
}

View File

@ -0,0 +1,71 @@
/// <reference path="fourslash.ts" />
// @filename: test.py
//// def [|/*marker1*/|]
////
//// def d[|/*marker2*/|]
////
//// def d1[|/*marker3*/|]():
//// pass
////
//// async def [|/*marker4*/|]
////
//// async def a[|/*marker5*/|]
////
//// async def a1[|/*marker6*/|]():
//// pass
////
//// def method(p[|/*marker7*/|]):
//// pass
//// def method(p:[|/*marker8*/|]):
//// pass
////
//// def method(p, p2[|/*marker9*/|]):
//// pass
//// def method(p, p2:[|/*marker10*/|]):
//// pass
// @filename: test1.py
//// class A:
//// def a1[|/*marker11*/|]
////
//// def a2[|/*marker12*/|]():
//// pass
////
//// def method(p[|/*marker13*/|]):
//// pass
//// def method(p:[|/*marker14*/|]):
//// pass
////
//// def method(p, p2[|/*marker15*/|]):
//// pass
//// def method(p, p2:[|/*marker16*/|]):
//// pass
{
helper.openFiles(helper.getMarkers().map((m) => m.fileName));
// @ts-ignore
await helper.verifyCompletion('exact', 'markdown', {
marker1: { completions: [] },
marker2: { completions: [] },
marker3: { completions: [] },
marker4: { completions: [] },
marker5: { completions: [] },
marker6: { completions: [] },
marker7: { completions: [] },
marker9: { completions: [] },
marker11: { completions: [] },
marker12: { completions: [] },
marker13: { completions: [] },
marker15: { completions: [] },
});
// @ts-ignore
await helper.verifyCompletion('included', 'markdown', {
marker8: { completions: [{ label: 'str', kind: Consts.CompletionItemKind.Class }] },
marker10: { completions: [{ label: 'str', kind: Consts.CompletionItemKind.Class }] },
marker14: { completions: [{ label: 'str', kind: Consts.CompletionItemKind.Class }] },
marker16: { completions: [{ label: 'str', kind: Consts.CompletionItemKind.Class }] },
});
}

View File

@ -0,0 +1,115 @@
/// <reference path="fourslash.ts" />
// @filename: test1.py
//// from typing import overload
////
//// class A:
//// @overload
//// def met[|/*marker1*/|]
// @filename: test2.py
//// from typing import overload
////
//// class A:
//// @overload
//// def met[|/*marker2*/|]()
// @filename: test3.py
//// from typing import overload
////
//// class A:
//// @overload
//// def met[|/*marker3*/|]():
//// pass
// @filename: test4.py
//// from typing import overload
////
//// class A:
//// @overload
//// def method(self):
//// pass
//// @overload
//// def met[|/*marker4*/|]
// @filename: test5.py
//// from typing import overload
////
//// class A:
//// @overload
//// def method(self):
//// pass
//// @overload
//// def method2(self):
//// pass
//// @overload
//// def met[|/*marker5*/|]
// @filename: test6.py
//// from typing import overload
////
//// class A:
//// @overload
//// def method(self):
//// pass
//// @overload
//// def method2(self):
//// pass
//// @overload
//// def diff[|/*marker6*/|]
// @filename: test7.py
//// from typing import overload
////
//// class A:
//// @overload
//// def method(self):
//// pass
//// @overload
//// def method2(self):
//// pass
////
//// class B(A):
//// @overload
//// def method3(self):
//// pass
//// @overload
//// def met[|/*marker7*/|]
// @filename: test8.py
//// from typing import overload
////
//// class A:
//// @overload
//// def method(self):
//// pass
//// @overload
//// def method[|/*marker8*/|](self, a):
//// pass
{
helper.openFiles(helper.getMarkers().map((m) => m.fileName));
// @ts-ignore
await helper.verifyCompletion('exact', 'markdown', {
marker1: { completions: [] },
marker2: { completions: [] },
marker3: { completions: [] },
marker4: { completions: [{ label: 'method', kind: Consts.CompletionItemKind.Method }] },
marker5: {
completions: [
{ label: 'method', kind: Consts.CompletionItemKind.Method },
{ label: 'method2', kind: Consts.CompletionItemKind.Method },
],
},
marker6: { completions: [] },
marker7: {
completions: [
{ label: 'method', kind: Consts.CompletionItemKind.Method },
{ label: 'method2', kind: Consts.CompletionItemKind.Method },
{ label: 'method3', kind: Consts.CompletionItemKind.Method },
],
},
marker8: { completions: [{ label: 'method', kind: Consts.CompletionItemKind.Method }] },
});
}

View File

@ -0,0 +1,106 @@
/// <reference path="fourslash.ts" />
// @filename: test1.py
//// from typing import overload
////
//// @overload
//// def met[|/*marker1*/|]
// @filename: test2.py
//// from typing import overload
////
//// @overload
//// def met[|/*marker2*/|]()
// @filename: test3.py
//// from typing import overload
////
//// @overload
//// def met[|/*marker3*/|]():
//// pass
// @filename: test4.py
//// from typing import overload
////
//// @overload
//// def method(self):
//// pass
//// @overload
//// def met[|/*marker4*/|]
// @filename: test5.py
//// from typing import overload
////
//// @overload
//// def method(self):
//// pass
//// @overload
//// def method2(self):
//// pass
//// @overload
//// def met[|/*marker5*/|]
// @filename: test6.py
//// from typing import overload
////
//// @overload
//// def method(self):
//// pass
//// @overload
//// def method2(self):
//// pass
//// @overload
//// def diff[|/*marker6*/|]
// @filename: test7.py
//// from typing import overload
////
//// @overload
//// def method(self):
//// pass
//// @overload
//// def method2(self):
//// pass
// @filename: test7-1.py
//// from typing import overload
//// from test7 import method, method2
////
//// @overload
//// def method3(self):
//// pass
//// @overload
//// def met[|/*marker7*/|]
// @filename: test8.py
//// from typing import overload
////
//// @overload
//// def method(self):
//// pass
//// @overload
//// def method[|/*marker8*/|](self, a):
//// pass
{
helper.openFiles(helper.getMarkers().map((m) => m.fileName));
// @ts-ignore
await helper.verifyCompletion('exact', 'markdown', {
marker1: { completions: [] },
marker2: { completions: [] },
marker3: { completions: [] },
marker4: { completions: [{ label: 'method', kind: Consts.CompletionItemKind.Function }] },
marker5: {
completions: [
{ label: 'method', kind: Consts.CompletionItemKind.Function },
{ label: 'method2', kind: Consts.CompletionItemKind.Function },
],
},
marker6: { completions: [] },
marker7: {
completions: [{ label: 'method3', kind: Consts.CompletionItemKind.Function }],
},
marker8: { completions: [{ label: 'method', kind: Consts.CompletionItemKind.Function }] },
});
}

View File

@ -12,6 +12,7 @@
//// '''another function docs'''
//// pass
//// some_fun[|/*marker3*/|]
//// unknownVariable.[|/*marker4*/|]
// @ts-ignore
await helper.verifyCompletion('exact', 'markdown', {
@ -31,4 +32,5 @@ await helper.verifyCompletion('exact', 'markdown', {
},
],
},
marker4: { completions: [] },
});

View File

@ -0,0 +1,21 @@
/// <reference path="fourslash.ts" />
// @filename: test.py
//// msg = "gekki"
////
//// a = f"{[|/*marker1*/|]}"
//// b = f"{msg.c[|/*marker2*/|]}"
//// b = f"{msg.[|/*marker3*/|]}"
// @ts-ignore
await helper.verifyCompletion('included', 'markdown', {
marker1: {
completions: [{ label: 'msg', kind: Consts.CompletionItemKind.Variable }],
},
marker2: {
completions: [{ label: 'count', kind: Consts.CompletionItemKind.Method }],
},
marker3: {
completions: [{ label: 'capitalize', kind: Consts.CompletionItemKind.Method }],
},
});

View File

@ -0,0 +1,23 @@
/// <reference path="fourslash.ts" />
// @filename: test.py
//// def Method(a, b, c):
//// pass
////
//// Method([|/*marker1*/|]"[|/*marker2*/|]hello[|/*marker3*/|]"[|/*marker4*/|])
// @ts-ignore
await helper.verifyCompletion('included', 'markdown', {
marker1: {
completions: [{ label: 'a=', kind: Consts.CompletionItemKind.Variable }],
},
marker4: {
completions: [{ label: 'b=', kind: Consts.CompletionItemKind.Variable }],
},
});
// @ts-ignore
await helper.verifyCompletion('exact', 'markdown', {
marker2: { completions: [] },
marker3: { completions: [] },
});

View File

@ -0,0 +1,37 @@
/// <reference path="fourslash.ts" />
// @filename: test.py
//// import os
//// from typing import Literal, TypedDict, Union
////
//// def method(a, b, c):
//// pass
////
//// method("os.[|/*marker1*/|]")
////
//// class Movie(TypedDict):
//// name: str
//// age: int
////
//// m = Movie(name="hello", age=10)
//// m["[|/*marker2*/|]"]
////
//// a: Union[Literal["hello"], Literal["hallo"]]
//// a = "[|/*marker3*/|]"
// @ts-ignore
await helper.verifyCompletion('exact', 'markdown', {
marker1: { completions: [] },
marker2: {
completions: [
{ label: '"name"', kind: Consts.CompletionItemKind.Text },
{ label: '"age"', kind: Consts.CompletionItemKind.Text },
],
},
marker3: {
completions: [
{ label: '"hello"', kind: Consts.CompletionItemKind.Text },
{ label: '"hallo"', kind: Consts.CompletionItemKind.Text },
],
},
});

View File

@ -136,6 +136,8 @@ declare namespace _ {
importName: string;
}
type MarkupKind = 'markdown' | 'plaintext';
interface Fourslash {
getDocumentHighlightKind(m?: Marker): DocumentHighlightKind | undefined;
@ -166,7 +168,8 @@ declare namespace _ {
moveCaretRight(count: number): void;
openFile(indexOrName: number | string, content?: string): void;
openFile(indexOrName: number | string): void;
openFiles(indexOrNames: (number | string)[]): void;
verifyDiagnostics(map?: { [marker: string]: { category: string; message: string } }): void;
verifyCodeActions(
@ -185,7 +188,7 @@ declare namespace _ {
verifyHover(kind: string, map: { [marker: string]: string }): void;
verifyCompletion(
verifyMode: FourSlashCompletionVerificationMode,
docFormat: string,
docFormat: MarkupKind,
map: {
[marker: string]: {
completions: FourSlashCompletionItem[];
@ -198,17 +201,21 @@ declare namespace _ {
},
abbrMap?: { [abbr: string]: AbbreviationInfo }
): Promise<void>;
verifySignature(map: {
[marker: string]: {
noSig?: boolean;
signatures?: {
label: string;
parameters: string[];
}[];
activeParameters?: (number | undefined)[];
callHasParameters?: boolean;
};
}): void;
verifySignature(
docFormat: MarkupKind,
map: {
[marker: string]: {
noSig?: boolean;
signatures?: {
label: string;
parameters: string[];
documentation?: string;
}[];
activeParameters?: (number | undefined)[];
callHasParameters?: boolean;
};
}
): void;
verifyFindAllReferences(map: {
[marker: string]: {
references: DocumentRange[];

View File

@ -44,7 +44,7 @@
},
];
helper.verifySignature({
helper.verifySignature('plaintext', {
init: {
signatures: xInitSignatures,
activeParameters: [0],

View File

@ -0,0 +1,43 @@
/// <reference path="fourslash.ts" />
// @filename: docstrings.py
//// from typing import overload
////
//// def repeat(a: str, b: int) -> str:
//// """Repeat the string ``a`` ``b`` times.
////
//// >>> repeat('foo', 3)
//// 'foofoofoo'
//// """
////
//// return a * b
////
//// repeat([|/*marker1*/|])
{
helper.verifySignature('plaintext', {
marker1: {
signatures: [
{
label: '(a: str, b: int) -> str',
parameters: ['a: str', 'b: int'],
documentation: "Repeat the string ``a`` ``b`` times.\n\n>>> repeat('foo', 3)\n'foofoofoo'",
},
],
activeParameters: [0],
},
});
helper.verifySignature('markdown', {
marker1: {
signatures: [
{
label: '(a: str, b: int) -> str',
parameters: ['a: str', 'b: int'],
documentation: "Repeat the string `a` `b` times.\n\n```\n>>> repeat('foo', 3)\n'foofoofoo'\n```",
},
],
activeParameters: [0],
},
});
}

View File

@ -38,7 +38,7 @@
},
];
helper.verifySignature({
helper.verifySignature('plaintext', {
o1: {
signatures: overloadedSignatures,
activeParameters: [undefined, 0, 0],

View File

@ -21,7 +21,7 @@
},
];
helper.verifySignature({
helper.verifySignature('plaintext', {
s1: {
signatures: simpleSignatures,
activeParameters: [0],

View File

@ -355,7 +355,7 @@ export class TestState {
}
// Opens a file given its 0-based index or fileName
openFile(indexOrName: number | string, content?: string): void {
openFile(indexOrName: number | string): void {
const fileToOpen: FourSlashFile = this._findFile(indexOrName);
fileToOpen.fileName = normalizeSlashes(fileToOpen.fileName);
this.activeFile = fileToOpen;
@ -363,6 +363,12 @@ export class TestState {
this.program.setFileOpened(this.activeFile.fileName, 1, [{ text: fileToOpen.content }]);
}
openFiles(indexOrNames: (number | string)[]): void {
for (const indexOrName of indexOrNames) {
this.openFile(indexOrName);
}
}
printCurrentFileState(showWhitespace: boolean, makeCaretVisible: boolean) {
for (const file of this.testData.files) {
const active = this.activeFile === file;
@ -663,6 +669,7 @@ export class TestState {
);
}
}
return commandResult;
}
async verifyInvokeCodeAction(
@ -936,17 +943,21 @@ export class TestState {
}
}
verifySignature(map: {
[marker: string]: {
noSig?: boolean;
signatures?: {
label: string;
parameters: string[];
}[];
activeParameters?: (number | undefined)[];
callHasParameters?: boolean;
};
}): void {
verifySignature(
docFormat: MarkupKind,
map: {
[marker: string]: {
noSig?: boolean;
signatures?: {
label: string;
parameters: string[];
documentation?: string;
}[];
activeParameters?: (number | undefined)[];
callHasParameters?: boolean;
};
}
): void {
this._analyze();
for (const marker of this.getMarkers()) {
@ -960,7 +971,12 @@ export class TestState {
const expected = map[name];
const position = this.convertOffsetToPosition(fileName, marker.position);
const actual = this.program.getSignatureHelpForPosition(fileName, position, CancellationToken.None);
const actual = this.program.getSignatureHelpForPosition(
fileName,
position,
docFormat,
CancellationToken.None
);
if (expected.noSig) {
assert.equal(actual, undefined);
@ -984,6 +1000,15 @@ export class TestState {
});
assert.deepEqual(actualParameters, expectedSig.parameters);
if (expectedSig.documentation === undefined) {
assert.equal(sig.documentation, undefined);
} else {
assert.deepEqual(sig.documentation, {
kind: docFormat,
value: expectedSig.documentation,
});
}
});
assert.deepEqual(