From 9466ea59c5035fefdacaf554b8fcbb503a536844 Mon Sep 17 00:00:00 2001 From: PylanceBot Date: Wed, 8 Mar 2023 17:57:45 +0000 Subject: [PATCH] pull-pylance-with-pyright-1.1.298 --- .../pyright-internal/src/analyzer/program.ts | 5 +- .../src/commands/dumpFileDebugInfoCommand.ts | 112 ++++ .../src/common/extensibility.ts | 27 +- .../src/languageServerBase.ts | 4 +- .../src/languageService/completionProvider.ts | 489 ++++++++++------- .../languageService/renameModuleProvider.ts | 10 +- .../src/languageService/tooltipUtils.ts | 3 +- .../src/tests/completions.test.ts | 507 ++++++++++++++++++ ...ns.dictionary.keys.expression.fourslash.ts | 16 +- ...ictionary.keys.stringLiterals.fourslash.ts | 9 +- .../src/tests/hoverProvider.test.ts | 2 +- .../src/tests/moveSymbol.misc.test.ts | 35 ++ 12 files changed, 1006 insertions(+), 213 deletions(-) diff --git a/packages/pyright-internal/src/analyzer/program.ts b/packages/pyright-internal/src/analyzer/program.ts index 77351d0929c0..fb5713dbfa21 100644 --- a/packages/pyright-internal/src/analyzer/program.ts +++ b/packages/pyright-internal/src/analyzer/program.ts @@ -2078,7 +2078,10 @@ export class Program { } // If this isn't a name node, there are no references to be found. - if (node.nodeType !== ParseNodeType.Name || !RenameModuleProvider.canMoveSymbol(this._evaluator!, node)) { + if ( + node.nodeType !== ParseNodeType.Name || + !RenameModuleProvider.canMoveSymbol(this._configOptions, this._evaluator!, node) + ) { return undefined; } diff --git a/packages/pyright-internal/src/commands/dumpFileDebugInfoCommand.ts b/packages/pyright-internal/src/commands/dumpFileDebugInfoCommand.ts index 6b1c4ed2d13e..5d9a64a94a3a 100644 --- a/packages/pyright-internal/src/commands/dumpFileDebugInfoCommand.ts +++ b/packages/pyright-internal/src/commands/dumpFileDebugInfoCommand.ts @@ -41,6 +41,7 @@ import { BinaryOperationNode, BreakNode, CallNode, + CaseNode, ClassNode, ConstantNode, ContinueNode, @@ -71,6 +72,7 @@ import { ListComprehensionIfNode, ListComprehensionNode, ListNode, + MatchNode, MemberAccessNode, ModuleNameNode, ModuleNode, @@ -82,6 +84,16 @@ import { ParseNode, ParseNodeType, PassNode, + PatternAsNode, + PatternCaptureNode, + PatternClassArgumentNode, + PatternClassNode, + PatternLiteralNode, + PatternMappingExpandEntryNode, + PatternMappingKeyEntryNode, + PatternMappingNode, + PatternSequenceNode, + PatternValueNode, RaiseNode, ReturnNode, SetNode, @@ -93,7 +105,11 @@ import { TernaryNode, TryNode, TupleNode, + TypeAliasNode, TypeAnnotationNode, + TypeParameterCategory, + TypeParameterListNode, + TypeParameterNode, UnaryOperationNode, UnpackNode, WhileNode, @@ -911,6 +927,94 @@ class TreeDumper extends ParseTreeWalker { this._log(`${this._getPrefix(node)}`); return true; } + + override visitCase(node: CaseNode): boolean { + this._log(`${this._getPrefix(node)} isIrrefutable: ${node.isIrrefutable}`); + return true; + } + + override visitMatch(node: MatchNode): boolean { + this._log(`${this._getPrefix(node)}`); + return true; + } + + override visitPatternAs(node: PatternAsNode): boolean { + this._log(`${this._getPrefix(node)}`); + return true; + } + + override visitPatternCapture(node: PatternCaptureNode): boolean { + this._log(`${this._getPrefix(node)} isStar:${node.isStar} isWildcard:${node.isWildcard}`); + return true; + } + + override visitPatternClass(node: PatternClassNode): boolean { + this._log(`${this._getPrefix(node)}`); + return true; + } + + override visitPatternClassArgument(node: PatternClassArgumentNode): boolean { + this._log(`${this._getPrefix(node)}`); + return true; + } + + override visitPatternLiteral(node: PatternLiteralNode): boolean { + this._log(`${this._getPrefix(node)}`); + return true; + } + + override visitPatternMapping(node: PatternMappingNode): boolean { + this._log(`${this._getPrefix(node)}`); + return true; + } + + override visitPatternMappingExpandEntry(node: PatternMappingExpandEntryNode): boolean { + this._log(`${this._getPrefix(node)}`); + return true; + } + + override visitPatternMappingKeyEntry(node: PatternMappingKeyEntryNode): boolean { + this._log(`${this._getPrefix(node)}`); + return true; + } + + override visitPatternSequence(node: PatternSequenceNode): boolean { + this._log(`${this._getPrefix(node)} starEntryIndex: ${node.starEntryIndex}`); + return true; + } + + override visitPatternValue(node: PatternValueNode): boolean { + this._log(`${this._getPrefix(node)}`); + return true; + } + + override visitTypeAlias(node: TypeAliasNode): boolean { + this._log(`${this._getPrefix(node)}`); + return true; + } + + override visitTypeParameter(node: TypeParameterNode): boolean { + this._log( + `${this._getPrefix(node)} typeParamCategory:${getTypeParameterCategoryString(node.typeParamCategory)}` + ); + return true; + } + + override visitTypeParameterList(node: TypeParameterListNode): boolean { + this._log(`${this._getPrefix(node)}`); + return true; + } +} + +function getTypeParameterCategoryString(type: TypeParameterCategory) { + switch (type) { + case TypeParameterCategory.TypeVar: + return 'TypeVar'; + case TypeParameterCategory.TypeVarTuple: + return 'TypeVarTuple'; + case TypeParameterCategory.ParamSpec: + return 'ParamSpec'; + } } function getParameterCategoryString(type: ParameterCategory) { @@ -961,6 +1065,14 @@ function getErrorExpressionCategoryString(type: ErrorExpressionCategory) { return 'MissingListCloseBracket'; case ErrorExpressionCategory.MissingFunctionParameterList: return 'MissingFunctionParameterList'; + case ErrorExpressionCategory.MissingPattern: + return 'MissingPattern'; + case ErrorExpressionCategory.MissingPatternSubject: + return 'MissingPatternSubject'; + case ErrorExpressionCategory.MissingDictValue: + return 'MissingDictValue'; + case ErrorExpressionCategory.MaxDepthExceeded: + return 'MaxDepthExceeded'; default: return `Unknown Value!! (${type})`; } diff --git a/packages/pyright-internal/src/common/extensibility.ts b/packages/pyright-internal/src/common/extensibility.ts index e5d759040dac..d3a1dc6bfb7d 100644 --- a/packages/pyright-internal/src/common/extensibility.ts +++ b/packages/pyright-internal/src/common/extensibility.ts @@ -166,10 +166,35 @@ export namespace Extensions { languageServiceExtensions = languageServiceExtensions.filter((s) => s.owner !== languageServer); } + function getBestProgram(filePath: string): ProgramView { + // Find the best program to use for this file. + const programs = [...new Set(programExtensions.map((s) => s.view))]; + let bestProgram: ProgramView | undefined; + programs.forEach((program) => { + // If the file is tracked by this program, use it. + if (program.owns(filePath)) { + if (!bestProgram || filePath.startsWith(program.rootPath)) { + bestProgram = program; + } + } + }); + + // If we didn't find a program that tracks the file, use the first one that claims ownership. + if (bestProgram === undefined) { + if (programs.length === 1) { + bestProgram = programs[0]; + } else { + bestProgram = programs.find((p) => p.getBoundSourceFileInfo(filePath)) || programs[0]; + } + } + return bestProgram; + } + export function getProgramExtensions(nodeOrFilePath: ParseNode | string) { const filePath = typeof nodeOrFilePath === 'string' ? nodeOrFilePath.toString() : getFileInfo(nodeOrFilePath).filePath; - return programExtensions.filter((s) => s.view?.owns(filePath)) as ProgramExtension[]; + const bestProgram = getBestProgram(filePath); + return programExtensions.filter((s) => s.view === bestProgram) as ProgramExtension[]; } export function getLanguageServiceExtensions() { diff --git a/packages/pyright-internal/src/languageServerBase.ts b/packages/pyright-internal/src/languageServerBase.ts index d0d18cce7c34..ec86c70aea8a 100644 --- a/packages/pyright-internal/src/languageServerBase.ts +++ b/packages/pyright-internal/src/languageServerBase.ts @@ -736,7 +736,9 @@ export abstract class LanguageServerBase implements LanguageServerInterface { documentHighlightProvider: { workDoneProgress: true }, renameProvider: { prepareProvider: true, workDoneProgress: true }, completionProvider: { - triggerCharacters: this.client.hasVisualStudioExtensionsCapability ? ['.', '[', '@'] : ['.', '['], + triggerCharacters: this.client.hasVisualStudioExtensionsCapability + ? ['.', '[', '@', '"', "'"] + : ['.', '[', '"', "'"], resolveProvider: true, workDoneProgress: true, completionItem: { diff --git a/packages/pyright-internal/src/languageService/completionProvider.ts b/packages/pyright-internal/src/languageService/completionProvider.ts index ef21ac2cf30f..0e34ea8f9eff 100644 --- a/packages/pyright-internal/src/languageService/completionProvider.ts +++ b/packages/pyright-internal/src/languageService/completionProvider.ts @@ -109,7 +109,7 @@ import { TypeAnnotationNode, } from '../parser/parseNodes'; import { ParseResults } from '../parser/parser'; -import { StringToken, StringTokenFlags, Token, TokenType } from '../parser/tokenizerTypes'; +import { OperatorType, StringToken, StringTokenFlags, Token, TokenType } from '../parser/tokenizerTypes'; import { AbbreviationInfo, AutoImporter, AutoImportResult, ImportFormat, ModuleSymbolMap } from './autoImporter'; import { CompletionDetail, @@ -300,6 +300,14 @@ interface RecentCompletionInfo { autoImportText: string; } +interface QuoteInfo { + priorWord: string; + priorText: string; + filterText: string | undefined; + stringValue: string | undefined; + quoteCharacter: string; +} + export const autoImportDetail = 'Auto-import'; export const dictionaryKeyDetail = 'Dictionary key'; @@ -431,7 +439,7 @@ export class CompletionProvider { throwIfCancellationRequested(this._cancellationToken); if (curNode.nodeType === ParseNodeType.String) { - return this._getLiteralCompletions(curNode, priorWord, priorText, postText); + return this._getLiteralCompletions(curNode, offset, priorWord, priorText, postText); } if (curNode.nodeType === ParseNodeType.StringList || curNode.nodeType === ParseNodeType.FormatString) { @@ -443,7 +451,7 @@ export class CompletionProvider { } if (curNode.nodeType === ParseNodeType.Error) { - return this._getExpressionErrorCompletions(curNode, priorWord, priorText, postText); + return this._getExpressionErrorCompletions(curNode, offset, priorWord, priorText, postText); } if (curNode.nodeType === ParseNodeType.MemberAccess) { @@ -452,7 +460,16 @@ export class CompletionProvider { if (curNode.nodeType === ParseNodeType.Dictionary) { const completionMap = new CompletionMap(); - if (this._addTypedDictKeys(curNode, /* stringNode */ undefined, priorText, postText, completionMap)) { + if ( + this._tryAddTypedDictKeysFromDictionary( + curNode, + /* stringNode */ undefined, + priorWord, + priorText, + postText, + completionMap + ) + ) { return { completionMap }; } } @@ -467,9 +484,10 @@ export class CompletionProvider { if (dictionaryNode.trailingCommaToken && dictionaryNode.trailingCommaToken.start < offset) { const completionMap = new CompletionMap(); if ( - this._addTypedDictKeys( + this._tryAddTypedDictKeysFromDictionary( dictionaryNode, /* stringNode */ undefined, + priorWord, priorText, postText, completionMap @@ -822,6 +840,7 @@ export class CompletionProvider { private _getExpressionErrorCompletions( node: ErrorNode, + offset: number, priorWord: string, priorText: string, postText: string @@ -898,8 +917,9 @@ export class CompletionProvider { return this._getExpressionCompletions(node, priorWord, priorText, postText); } + case ErrorExpressionCategory.MissingPattern: case ErrorExpressionCategory.MissingIndexOrSlice: { - let completionResults = this._getLiteralCompletions(node, priorWord, priorText, postText); + let completionResults = this._getLiteralCompletions(node, offset, priorWord, priorText, postText); if (!completionResults) { completionResults = this._getExpressionCompletions(node, priorWord, priorText, postText); @@ -1626,52 +1646,7 @@ export class CompletionProvider { } // Add literal values if appropriate. - if (parseNode.nodeType === ParseNodeType.Error) { - if ( - parseNode.category === ErrorExpressionCategory.MissingIndexOrSlice && - parseNode.parent?.nodeType === ParseNodeType.Index - ) { - this._tryAddTypedDictStringLiteral( - parseNode.parent, - /* priorText */ undefined, - /* postText */ undefined, - completionMap - ); - } else if (parseNode.category === ErrorExpressionCategory.MissingExpression) { - if (parseNode.parent && parseNode.parent.nodeType === ParseNodeType.Assignment) { - const declaredTypeOfTarget = this._evaluator.getExpectedType(parseNode)?.type; - if (declaredTypeOfTarget) { - this._addLiteralValuesForTargetType( - declaredTypeOfTarget, - priorText, - priorWord, - postText, - completionMap - ); - } - } - } - } - - if (isIndexArgument) { - // Completion for dict key (ex, dict_variable[]) - const indexNode = parseNode.parent!.parent! as IndexNode; - - this._getIndexerKeys(indexNode, parseNode).forEach((key) => { - if (completionMap.has(key)) { - // Don't add key if it already exists in the completion. - // ex) key = "dictKey" - // dict[key] = 1 - // print(dict[])) - return; - } - - this._addNameToCompletions(key, CompletionItemKind.Constant, priorWord, completionMap, { - sortText: this._makeSortText(SortCategory.LiteralValue, key), - itemDetail: dictionaryKeyDetail, - }); - }); - } + this._tryAddLiterals(parseNode, priorWord, priorText, postText, completionMap); return completionResults; } @@ -1728,15 +1703,15 @@ export class CompletionProvider { } // Add literals that apply to this parameter. - this._addLiteralValuesForArgument(signatureInfo, priorText, priorWord, postText, completionMap); + this._addLiteralValuesForArgument(signatureInfo, priorWord, priorText, postText, completionMap); } } } private _addLiteralValuesForArgument( signatureInfo: CallSignatureInfo, - priorText: string, priorWord: string, + priorText: string, postText: string, completionMap: CompletionMap ) { @@ -1753,19 +1728,19 @@ export class CompletionProvider { } const paramType = type.details.parameters[paramIndex].type; - this._addLiteralValuesForTargetType(paramType, priorText, priorWord, postText, completionMap); + this._addLiteralValuesForTargetType(paramType, priorWord, priorText, postText, completionMap); return undefined; }); } private _addLiteralValuesForTargetType( type: Type, - priorText: string, priorWord: string, + priorText: string, postText: string, completionMap: CompletionMap ) { - const quoteValue = this._getQuoteInfo(priorText); + const quoteValue = this._getQuoteInfo(priorWord, priorText); this._getSubTypesWithLiteralValues(type).forEach((v) => { if (ClassType.isBuiltIn(v, 'str')) { const value = printLiteralValue(v, quoteValue.quoteCharacter); @@ -1776,9 +1751,8 @@ export class CompletionProvider { } else { this._addStringLiteralToCompletions( value.substr(1, value.length - 2), - quoteValue.stringValue, + quoteValue, postText, - quoteValue.quoteCharacter, completionMap ); } @@ -1983,135 +1957,257 @@ export class CompletionProvider { private _getLiteralCompletions( parseNode: StringNode | ErrorNode, + offset: number, priorWord: string, priorText: string, postText: string ): CompletionResults | undefined { - let parentNode: ParseNode | undefined = parseNode.parent; + if (this._options.triggerCharacter === '"' || this._options.triggerCharacter === "'") { + if (parseNode.start !== offset - 1) { + // If completion is triggered by typing " or ', it must be the one that starts a string + // literal. In another word, it can't be something inside of another string or comment + return undefined; + } + } - if (!parentNode) { + const completionMap = new CompletionMap(); + if (!this._tryAddLiterals(parseNode, priorWord, priorText, postText, completionMap)) { return undefined; } - const completionMap = new CompletionMap(); + return { completionMap }; + } + + private _tryAddLiterals( + parseNode: ParseNode, + priorWord: string, + priorText: string, + postText: string, + completionMap: CompletionMap + ): boolean { + const parentAndChild = getParentSkippingStringList(parseNode); + if (!parentAndChild) { + return false; + } // See if the type evaluator can determine the expected type for this node. - if (isExpressionNode(parentNode)) { - const expectedTypeResult = this._evaluator.getExpectedType(parentNode); + // ex) a: Literal["str"] = /* here */ + const nodeForExpectedType = + parentAndChild.parent.nodeType === ParseNodeType.Assignment + ? parentAndChild.parent.rightExpression === parentAndChild.child + ? parentAndChild.child + : undefined + : isExpressionNode(parentAndChild.child) + ? parentAndChild.child + : undefined; + + if (nodeForExpectedType) { + const expectedTypeResult = this._evaluator.getExpectedType(nodeForExpectedType); if (expectedTypeResult && isLiteralTypeOrUnion(expectedTypeResult.type)) { this._addLiteralValuesForTargetType( expectedTypeResult.type, - priorText, priorWord, + priorText, postText, completionMap ); - return { completionMap }; + return true; } + } - if (parseNode.nodeType === ParseNodeType.String && parseNode.parent?.parent) { - const stringParent = parseNode.parent.parent; + // ex) a: TypedDictType = { "/* here */" } or a: TypedDictType = { A/* here */ } + const nodeForKey = parentAndChild.parent; + if (nodeForKey) { + // If the dictionary is not yet filled in, it will appear as though it's + // a set initially. + let dictOrSet: DictionaryNode | SetNode | undefined; - // If the dictionary is not yet filled in, it will appear as though it's - // a set initially. - let dictOrSet: DictionaryNode | SetNode | undefined; + if ( + nodeForKey.nodeType === ParseNodeType.DictionaryKeyEntry && + nodeForKey.keyExpression === parentAndChild.child && + nodeForKey.parent?.nodeType === ParseNodeType.Dictionary + ) { + dictOrSet = nodeForKey.parent; + } else if (nodeForKey?.nodeType === ParseNodeType.Set) { + dictOrSet = nodeForKey; + } + if (dictOrSet) { if ( - stringParent.nodeType === ParseNodeType.DictionaryKeyEntry && - stringParent.keyExpression === parseNode.parent && - stringParent.parent?.nodeType === ParseNodeType.Dictionary + this._tryAddTypedDictKeysFromDictionary( + dictOrSet, + parseNode.nodeType === ParseNodeType.String ? parseNode : undefined, + priorWord, + priorText, + postText, + completionMap + ) ) { - dictOrSet = stringParent.parent; - } else if (stringParent?.nodeType === ParseNodeType.Set) { - dictOrSet = stringParent; - } - - if (dictOrSet) { - if (this._addTypedDictKeys(dictOrSet, parseNode, priorText, postText, completionMap)) { - return { completionMap }; - } + return true; } } } - if (parentNode.nodeType !== ParseNodeType.Argument) { - if (parentNode.nodeType !== ParseNodeType.StringList || parentNode.strings.length > 1) { - return undefined; + // a: DictType = { .... } + // a[/* here */] or a['/* here */'] or a[variable/*here*/] + const argument = parentAndChild.parent; + if (argument.nodeType === ParseNodeType.Argument && argument.parent?.nodeType === ParseNodeType.Index) { + const priorTextInString = parseNode.nodeType === ParseNodeType.String ? priorText : ''; + if ( + this._tryAddTypedDictKeysFromIndexer( + argument.parent, + priorWord, + priorTextInString, + postText, + completionMap + ) + ) { + return true; } - parentNode = parentNode.parent; - if (!parentNode) { - return undefined; - } - } + const quoteInfo = this._getQuoteInfo(priorWord, priorTextInString); + const keys = this._getIndexerKeys(argument.parent, parseNode); - if (parentNode.nodeType === ParseNodeType.Argument && parentNode.parent?.nodeType === ParseNodeType.Index) { - const priorTextInString = parseNode.nodeType === ParseNodeType.String ? priorText : ''; - if (!this._tryAddTypedDictStringLiteral(parentNode.parent, priorTextInString, postText, completionMap)) { - const keys = this._getIndexerKeys(parentNode.parent, parseNode); - const quoteValue = this._getQuoteInfo(priorTextInString); + let keyFound = false; + for (const key of keys) { + if (completionMap.has(key)) { + // Don't add key if it already exists in the completion. + // ex) key = "dictKey" + // dict[key] = 1 + // print(dict[])) + continue; + } - for (const key of keys) { - const stringLiteral = /^["|'].*["|']$/.test(key); - if (parseNode.nodeType === ParseNodeType.String && !stringLiteral) { - continue; - } + const stringLiteral = /^["|'].*["|']$/.test(key); + if (parseNode.nodeType === ParseNodeType.String && !stringLiteral) { + continue; + } - if (stringLiteral) { - const keyWithoutQuote = key.substr(1, key.length - 2); + keyFound = true; + if (stringLiteral) { + const keyWithoutQuote = key.substr(1, key.length - 2); - this._addStringLiteralToCompletions( - keyWithoutQuote, - quoteValue.stringValue, - postText, - quoteValue.quoteCharacter, - completionMap, - dictionaryKeyDetail - ); - } else { - this._addNameToCompletions(key, CompletionItemKind.Constant, priorWord, completionMap, { - sortText: this._makeSortText(SortCategory.LiteralValue, key), - itemDetail: dictionaryKeyDetail, - }); - } + this._addStringLiteralToCompletions( + keyWithoutQuote, + quoteInfo, + postText, + completionMap, + dictionaryKeyDetail + ); + } else { + this._addNameToCompletions(key, CompletionItemKind.Constant, priorWord, completionMap, { + sortText: this._makeSortText(SortCategory.LiteralValue, key), + itemDetail: dictionaryKeyDetail, + }); } + } - if (completionMap.size === 0) { - return undefined; - } + if (keyFound) { + return true; } - } else { - debug.assert(parseNode.nodeType === ParseNodeType.String); + } + + // if c == "/* here */" + const comparison = parentAndChild.parent; + const supportedOperators = [OperatorType.Assign, OperatorType.Equals, OperatorType.NotEquals]; + if (comparison.nodeType === ParseNodeType.BinaryOperation && supportedOperators.includes(comparison.operator)) { + const type = this._evaluator.getType(comparison.leftExpression); + if (type && isLiteralTypeOrUnion(type)) { + this._addLiteralValuesForTargetType(type, priorWord, priorText, postText, completionMap); + return true; + } + } + + // if c := "/* here */" + const assignmentExpression = parentAndChild.parent; + if ( + assignmentExpression.nodeType === ParseNodeType.AssignmentExpression && + assignmentExpression.rightExpression === parentAndChild.child + ) { + const type = this._evaluator.getType(assignmentExpression.name); + if (type && isLiteralTypeOrUnion(type)) { + this._addLiteralValuesForTargetType(type, priorWord, priorText, postText, completionMap); + return true; + } + } + + // For now, we only support simple cases. no complex pattern matching. + // match c: + // case /* here */ + const caseNode = parentAndChild.parent; + if ( + caseNode.nodeType === ParseNodeType.Case && + caseNode.pattern.nodeType === ParseNodeType.Error && + caseNode.pattern.category === ErrorExpressionCategory.MissingPattern && + caseNode.suite === parentAndChild.child && + caseNode.parent?.nodeType === ParseNodeType.Match + ) { + const type = this._evaluator.getType(caseNode.parent.subjectExpression); + if (type && isLiteralTypeOrUnion(type)) { + this._addLiteralValuesForTargetType(type, priorWord, priorText, postText, completionMap); + return true; + } + } + // match c: + // case "/* here */" + // case Sym/*here*/ + const patternLiteral = parentAndChild.parent; + if ( + (patternLiteral.nodeType === ParseNodeType.PatternLiteral || + patternLiteral.nodeType === ParseNodeType.PatternCapture) && + patternLiteral.parent?.nodeType === ParseNodeType.PatternAs && + patternLiteral.parent.parent?.nodeType === ParseNodeType.Case && + patternLiteral.parent.parent.parent?.nodeType === ParseNodeType.Match + ) { + const type = this._evaluator.getType(patternLiteral.parent.parent.parent.subjectExpression); + if (type && isLiteralTypeOrUnion(type)) { + this._addLiteralValuesForTargetType(type, priorWord, priorText, postText, completionMap); + return true; + } + } + + if (parseNode.nodeType === ParseNodeType.String) { const offset = convertPositionToOffset(this._position, this._parseResults.tokenizerOutput.lines)!; - const atArgument = parentNode.start < offset && offset < TextRange.getEnd(parseNode); + const atArgument = parseNode.parent!.start < offset && offset < TextRange.getEnd(parseNode); this._addCallArgumentCompletions(parseNode, priorWord, priorText, postText, atArgument, completionMap); + return true; } - return { completionMap }; + return false; + + function getParentSkippingStringList(node: ParseNode): { parent: ParseNode; child: ParseNode } | undefined { + if (!node.parent) { + return undefined; + } + + if (node.nodeType !== ParseNodeType.String) { + return { parent: node.parent, child: node }; + } + + if (!node.parent.parent) { + return undefined; + } + + if (node.parent?.nodeType !== ParseNodeType.StringList || node.parent.strings.length > 1) { + return undefined; + } + + return { parent: node.parent.parent, child: node.parent }; + } } - private _addTypedDictKeys( - dictionaryNode: DictionaryNode | SetNode, - stringNode: StringNode | undefined, + private _tryAddTypedDictKeys( + type: Type, + existingKeys: string[], + priorWord: string, priorText: string, postText: string, completionMap: CompletionMap ) { - const expectedTypeResult = this._evaluator.getExpectedType(dictionaryNode); - if (!expectedTypeResult) { - return false; - } - - // If the expected type result is associated with a node above the - // dictionaryNode in the parse tree, there are no typed dict keys to add. - if (ParseTreeUtils.getNodeDepth(expectedTypeResult.node) < ParseTreeUtils.getNodeDepth(dictionaryNode)) { - return false; - } - let typedDicts: ClassType[] = []; - doForEachSubtype(expectedTypeResult.type, (subtype) => { + doForEachSubtype(type, (subtype) => { if (isClassInstance(subtype) && ClassType.isTypedDictClass(subtype)) { typedDicts.push(subtype); } @@ -2121,15 +2217,10 @@ export class CompletionProvider { return false; } - const keys = this._getDictExpressionStringKeys( - dictionaryNode, - stringNode ? new Set([stringNode.parent?.id]) : undefined - ); + typedDicts = this._tryNarrowTypedDicts(typedDicts, existingKeys); - typedDicts = this._tryNarrowTypedDicts(typedDicts, keys); - - const quoteValue = this._getQuoteInfo(priorText); - const excludes = new Set(keys); + const quoteInfo = this._getQuoteInfo(priorWord, priorText); + const excludes = new Set(existingKeys); typedDicts.forEach((typedDict) => { getTypedDictMembersForClass(this._evaluator, typedDict, /* allowNarrowed */ true).forEach((_, key) => { @@ -2140,21 +2231,40 @@ export class CompletionProvider { excludes.add(key); - this._addStringLiteralToCompletions( - key, - quoteValue ? quoteValue.stringValue : undefined, - postText, - quoteValue - ? quoteValue.quoteCharacter - : this._parseResults.tokenizerOutput.predominantSingleQuoteCharacter, - completionMap - ); + this._addStringLiteralToCompletions(key, quoteInfo, postText, completionMap); }); }); return true; } + private _tryAddTypedDictKeysFromDictionary( + dictionaryNode: DictionaryNode | SetNode, + stringNode: StringNode | undefined, + priorWord: string, + priorText: string, + postText: string, + completionMap: CompletionMap + ) { + const expectedTypeResult = this._evaluator.getExpectedType(dictionaryNode); + if (!expectedTypeResult) { + return false; + } + + // If the expected type result is associated with a node above the + // dictionaryNode in the parse tree, there are no typed dict keys to add. + if (ParseTreeUtils.getNodeDepth(expectedTypeResult.node) < ParseTreeUtils.getNodeDepth(dictionaryNode)) { + return false; + } + + const keys = this._getDictExpressionStringKeys( + dictionaryNode, + stringNode ? new Set([stringNode.parent?.id]) : undefined + ); + + return this._tryAddTypedDictKeys(expectedTypeResult.type, keys, priorWord, priorText, postText, completionMap); + } + private _tryNarrowTypedDicts(types: ClassType[], keys: string[]): ClassType[] { const newTypes = types.flatMap((type) => { const entries = getTypedDictMembersForClass(this._evaluator, type, /* allowNarrowed */ true); @@ -2178,10 +2288,8 @@ export class CompletionProvider { // Find out quotation and string prefix to use for string literals // completion under current context. - private _getQuoteInfo(priorText: string | undefined): { - stringValue: string | undefined; - quoteCharacter: string; - } { + private _getQuoteInfo(priorWord: string, priorText: string): QuoteInfo { + let filterText = priorWord; let stringValue = undefined; let quoteCharacter = this._parseResults.tokenizerOutput.predominantSingleQuoteCharacter; @@ -2189,7 +2297,7 @@ export class CompletionProvider { // ex) typedDict[ |<= here // use default quotation char without any string prefix. if (!this._insideStringLiteral) { - return { stringValue, quoteCharacter }; + return { priorWord, priorText, filterText, stringValue, quoteCharacter }; } const singleQuote = "'"; @@ -2223,13 +2331,18 @@ export class CompletionProvider { quoteCharacter = this._insideStringLiteral.flags & StringTokenFlags.SingleQuote ? doubleQuote : singleQuote; } - return { stringValue, quoteCharacter }; + if (stringValue) { + filterText = stringValue; + } + + return { priorWord, priorText, filterText, stringValue, quoteCharacter }; } - private _tryAddTypedDictStringLiteral( - indexNode: IndexNode | undefined, - priorText: string | undefined, - postText: string | undefined, + private _tryAddTypedDictKeysFromIndexer( + indexNode: IndexNode, + priorWord: string, + priorText: string, + postText: string, completionMap: CompletionMap ) { if (!indexNode) { @@ -2241,46 +2354,18 @@ export class CompletionProvider { return false; } - let foundTypedDict = false; - - doForEachSubtype(baseType, (subtype) => { - if (!isClassInstance(subtype)) { - return; - } - - if (!ClassType.isTypedDictClass(subtype)) { - return; - } - - const entries = getTypedDictMembersForClass(this._evaluator, subtype, /* allowNarrowed */ true); - const quoteValue = this._getQuoteInfo(priorText); - - entries.forEach((_, key) => { - this._addStringLiteralToCompletions( - key, - quoteValue.stringValue, - postText, - quoteValue.quoteCharacter, - completionMap - ); - }); - - foundTypedDict = true; - }); - - return foundTypedDict; + return this._tryAddTypedDictKeys(baseType, [], priorWord, priorText, postText, completionMap); } private _addStringLiteralToCompletions( value: string, - priorString: string | undefined, + quoteInfo: QuoteInfo, postText: string | undefined, - quoteCharacter: string, completionMap: CompletionMap, detail?: string ) { - if (StringUtils.isPatternInSymbol(priorString || '', value)) { - const valueWithQuotes = `${quoteCharacter}${value}${quoteCharacter}`; + if (StringUtils.isPatternInSymbol(quoteInfo.filterText || '', value)) { + const valueWithQuotes = `${quoteInfo.quoteCharacter}${value}${quoteInfo.quoteCharacter}`; if (completionMap.has(valueWithQuotes)) { return; } @@ -2290,15 +2375,17 @@ export class CompletionProvider { completionItem.kind = CompletionItemKind.Constant; completionItem.sortText = this._makeSortText(SortCategory.LiteralValue, valueWithQuotes); let rangeStartCol = this._position.character; - if (priorString !== undefined) { - rangeStartCol -= priorString.length + 1; + if (quoteInfo.stringValue !== undefined) { + rangeStartCol -= quoteInfo.stringValue.length + 1; + } else if (quoteInfo.priorWord) { + rangeStartCol -= quoteInfo.priorWord.length; } // If the text after the insertion point is the closing quote, // replace it. let rangeEndCol = this._position.character; if (postText !== undefined) { - if (postText.startsWith(quoteCharacter)) { + if (postText.startsWith(quoteInfo.quoteCharacter)) { rangeEndCol++; } } diff --git a/packages/pyright-internal/src/languageService/renameModuleProvider.ts b/packages/pyright-internal/src/languageService/renameModuleProvider.ts index 913e9a498aab..4a98064bbdae 100644 --- a/packages/pyright-internal/src/languageService/renameModuleProvider.ts +++ b/packages/pyright-internal/src/languageService/renameModuleProvider.ts @@ -47,7 +47,7 @@ import { isPrivateName } from '../analyzer/symbolNameUtils'; import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes'; import { TypeCategory } from '../analyzer/types'; import { getOrAdd } from '../common/collectionUtils'; -import { ConfigOptions } from '../common/configOptions'; +import { ConfigOptions, matchFileSpecs } from '../common/configOptions'; import { assert, assertNever } from '../common/debug'; import { FileEditAction } from '../common/editAction'; import { FileSystem } from '../common/fileSystem'; @@ -164,7 +164,13 @@ export class RenameModuleProvider { ); } - static canMoveSymbol(evaluator: TypeEvaluator, node: NameNode): boolean { + static canMoveSymbol(configOptions: ConfigOptions, evaluator: TypeEvaluator, node: NameNode): boolean { + const filePath = getFileInfo(node)?.filePath; + if (!filePath || !matchFileSpecs(configOptions, filePath, /* isFile */ true)) { + // We only support moving symbols from a user file. + return false; + } + if (isPrivateName(node.value)) { return false; } diff --git a/packages/pyright-internal/src/languageService/tooltipUtils.ts b/packages/pyright-internal/src/languageService/tooltipUtils.ts index 73405330c8d5..5b7a92500f9a 100644 --- a/packages/pyright-internal/src/languageService/tooltipUtils.ts +++ b/packages/pyright-internal/src/languageService/tooltipUtils.ts @@ -115,7 +115,8 @@ export function getFunctionTooltip( const funcParts = evaluator.printFunctionParts(type); const paramSignature = formatSignature(funcParts, indentStr, functionSignatureDisplay); const sep = isProperty ? ': ' : ''; - return `${labelFormatted}def ${functionName}${sep}${paramSignature} -> ${funcParts[1]}`; + const defKeyword = isProperty ? '' : 'def '; + return `${labelFormatted}${defKeyword}${functionName}${sep}${paramSignature} -> ${funcParts[1]}`; } export function getConstructorTooltip( diff --git a/packages/pyright-internal/src/tests/completions.test.ts b/packages/pyright-internal/src/tests/completions.test.ts index 28b121c56ee2..a6392cd2af4c 100644 --- a/packages/pyright-internal/src/tests/completions.test.ts +++ b/packages/pyright-internal/src/tests/completions.test.ts @@ -4,8 +4,12 @@ * completions tests. */ +import assert from 'assert'; +import { CancellationToken } from 'vscode-languageserver'; import { CompletionItemKind, MarkupKind } from 'vscode-languageserver-types'; +import { ImportFormat } from '../languageService/autoImporter'; +import { CompletionOptions } from '../languageService/completionProvider'; import { parseAndGetTestState } from './harness/fourslash/testState'; test('completion import statement tooltip', async () => { @@ -358,3 +362,506 @@ test('completion from import statement tooltip - implicit module', async () => { }, }); }); + +test('include literals in expression completion', async () => { + const code = ` +// @filename: test.py +//// from typing import TypedDict +//// +//// class TestType(TypedDict): +//// A: str +//// B: int +//// +//// var: TestType = {} +//// +//// var[[|A/*marker*/|]] + `; + + const state = parseAndGetTestState(code).state; + const marker = state.getMarkerByName('marker'); + state.openFile(marker.fileName); + + await state.verifyCompletion('included', MarkupKind.Markdown, { + marker: { + completions: [ + { + kind: CompletionItemKind.Constant, + label: "'A'", + textEdit: { range: state.getPositionRange('marker'), newText: "'A'" }, + }, + ], + }, + }); +}); + +test('include literals in set key', async () => { + const code = ` +// @filename: test.py +//// from typing import TypedDict +//// +//// class TestType(TypedDict): +//// A: str +//// B: int +//// +//// var: TestType = { [|A/*marker*/|] } + `; + + const state = parseAndGetTestState(code).state; + const marker = state.getMarkerByName('marker'); + state.openFile(marker.fileName); + + await state.verifyCompletion('included', MarkupKind.Markdown, { + marker: { + completions: [ + { + kind: CompletionItemKind.Constant, + label: "'A'", + textEdit: { range: state.getPositionRange('marker'), newText: "'A'" }, + }, + ], + }, + }); +}); + +test('include literals in dict key', async () => { + const code = ` +// @filename: test.py +//// from typing import TypedDict +//// +//// class TestType(TypedDict): +//// A: str +//// B: int +//// +//// var: TestType = { [|A/*marker*/|] : "hello" } + `; + + const state = parseAndGetTestState(code).state; + const marker = state.getMarkerByName('marker'); + state.openFile(marker.fileName); + + await state.verifyCompletion('included', MarkupKind.Markdown, { + marker: { + completions: [ + { + kind: CompletionItemKind.Constant, + label: '"A"', + textEdit: { range: state.getPositionRange('marker'), newText: '"A"' }, + }, + ], + }, + }); +}); + +test('literals support for binary operators - equals', async () => { + const code = ` +// @filename: test.py +//// from typing import Literal +//// +//// Currency = Literal["USD", "EUR"] +//// +//// def foo(c: Currency): +//// if c == [|"/*marker*/"|] + `; + + const state = parseAndGetTestState(code).state; + const marker = state.getMarkerByName('marker'); + state.openFile(marker.fileName); + + await state.verifyCompletion('included', MarkupKind.Markdown, { + marker: { + completions: [ + { + kind: CompletionItemKind.Constant, + label: '"USD"', + textEdit: { range: state.getPositionRange('marker'), newText: '"USD"' }, + }, + { + kind: CompletionItemKind.Constant, + label: '"EUR"', + textEdit: { range: state.getPositionRange('marker'), newText: '"EUR"' }, + }, + ], + }, + }); +}); + +test('literals support for binary operators - not equals', async () => { + const code = ` +// @filename: test.py +//// from typing import Literal +//// +//// Currency = Literal["USD", "EUR"] +//// +//// def foo(c: Currency): +//// if c != [|"/*marker*/"|] + `; + + const state = parseAndGetTestState(code).state; + const marker = state.getMarkerByName('marker'); + state.openFile(marker.fileName); + + await state.verifyCompletion('included', MarkupKind.Markdown, { + marker: { + completions: [ + { + kind: CompletionItemKind.Constant, + label: '"USD"', + textEdit: { range: state.getPositionRange('marker'), newText: '"USD"' }, + }, + { + kind: CompletionItemKind.Constant, + label: '"EUR"', + textEdit: { range: state.getPositionRange('marker'), newText: '"EUR"' }, + }, + ], + }, + }); +}); + +test('literals support for binary operators without string node', async () => { + const code = ` +// @filename: test.py +//// from typing import Literal +//// +//// Currency = Literal["USD", "EUR"] +//// +//// def foo(c: Currency): +//// if c != [|/*marker*/|] + `; + + const state = parseAndGetTestState(code).state; + const marker = state.getMarkerByName('marker'); + state.openFile(marker.fileName); + + await state.verifyCompletion('included', MarkupKind.Markdown, { + marker: { + completions: [ + { + kind: CompletionItemKind.Constant, + label: '"USD"', + }, + { + kind: CompletionItemKind.Constant, + label: '"EUR"', + }, + ], + }, + }); +}); + +test('literals support for binary operators with prior word', async () => { + const code = ` +// @filename: test.py +//// from typing import Literal +//// +//// Currency = Literal["USD", "EUR"] +//// +//// def foo(c: Currency): +//// if c != [|US/*marker*/|] + `; + + const state = parseAndGetTestState(code).state; + const marker = state.getMarkerByName('marker'); + state.openFile(marker.fileName); + + await state.verifyCompletion('included', MarkupKind.Markdown, { + marker: { + completions: [ + { + kind: CompletionItemKind.Constant, + label: '"USD"', + }, + ], + }, + }); +}); + +test('literals support for binary operators - assignment expression', async () => { + const code = ` +// @filename: test.py +//// from typing import Literal +//// +//// Currency = Literal["USD", "EUR"] +//// +//// def foo(c: Currency): +//// if c := [|"/*marker*/"|] + `; + + const state = parseAndGetTestState(code).state; + const marker = state.getMarkerByName('marker'); + state.openFile(marker.fileName); + + await state.verifyCompletion('included', MarkupKind.Markdown, { + marker: { + completions: [ + { + kind: CompletionItemKind.Constant, + label: '"USD"', + textEdit: { range: state.getPositionRange('marker'), newText: '"USD"' }, + }, + { + kind: CompletionItemKind.Constant, + label: '"EUR"', + textEdit: { range: state.getPositionRange('marker'), newText: '"EUR"' }, + }, + ], + }, + }); +}); + +test('literals support for call', async () => { + const code = ` +// @filename: test.py +//// from typing import Literal +//// +//// Currency = Literal["USD", "EUR"] +//// +//// def foo(c: Currency) -> Currency: +//// return c +//// +//// if foo([|"/*marker1*/"|]) == [|"/*marker2*/"|] + `; + + const state = parseAndGetTestState(code).state; + const marker1 = state.getMarkerByName('marker1'); + state.openFile(marker1.fileName); + + await state.verifyCompletion('included', MarkupKind.Markdown, { + marker1: { + completions: [ + { + kind: CompletionItemKind.Constant, + label: '"USD"', + textEdit: { range: state.getPositionRange('marker1'), newText: '"USD"' }, + }, + { + kind: CompletionItemKind.Constant, + label: '"EUR"', + textEdit: { range: state.getPositionRange('marker1'), newText: '"EUR"' }, + }, + ], + }, + marker2: { + completions: [ + { + kind: CompletionItemKind.Constant, + label: '"USD"', + textEdit: { range: state.getPositionRange('marker2'), newText: '"USD"' }, + }, + { + kind: CompletionItemKind.Constant, + label: '"EUR"', + textEdit: { range: state.getPositionRange('marker2'), newText: '"EUR"' }, + }, + ], + }, + }); +}); + +test('list with literal types', async () => { + const code = ` +// @filename: test.py +//// from typing import Literal +//// +//// Currency = Literal["USD", "EUR"] +//// +//// a: list[Currency] = [[|"/*marker*/"|]] + `; + + const state = parseAndGetTestState(code).state; + const marker = state.getMarkerByName('marker'); + state.openFile(marker.fileName); + + await state.verifyCompletion('included', MarkupKind.Markdown, { + marker: { + completions: [ + { + kind: CompletionItemKind.Constant, + label: '"USD"', + textEdit: { range: state.getPositionRange('marker'), newText: '"USD"' }, + }, + { + kind: CompletionItemKind.Constant, + label: '"EUR"', + textEdit: { range: state.getPositionRange('marker'), newText: '"EUR"' }, + }, + ], + }, + }); +}); + +test('literals support for match - error case', async () => { + const code = ` +// @filename: test.py +//// from typing import Literal +//// +//// Currency = Literal["USD", "EUR"] +//// +//// def foo(c: Currency): +//// match c: +//// case [|/*marker*/|] + `; + + const state = parseAndGetTestState(code).state; + const marker = state.getMarkerByName('marker'); + state.openFile(marker.fileName); + + await state.verifyCompletion('included', MarkupKind.Markdown, { + marker: { + completions: [ + { + kind: CompletionItemKind.Constant, + label: '"USD"', + }, + { + kind: CompletionItemKind.Constant, + label: '"EUR"', + }, + ], + }, + }); +}); + +test('literals support for match - simple case', async () => { + const code = ` +// @filename: test.py +//// from typing import Literal +//// +//// Currency = Literal["USD", "EUR"] +//// +//// def foo(c: Currency): +//// match c: +//// case [|"/*marker*/"|] + `; + + const state = parseAndGetTestState(code).state; + const marker = state.getMarkerByName('marker'); + state.openFile(marker.fileName); + + await state.verifyCompletion('included', MarkupKind.Markdown, { + marker: { + completions: [ + { + kind: CompletionItemKind.Constant, + label: '"USD"', + textEdit: { range: state.getPositionRange('marker'), newText: '"USD"' }, + }, + { + kind: CompletionItemKind.Constant, + label: '"EUR"', + textEdit: { range: state.getPositionRange('marker'), newText: '"EUR"' }, + }, + ], + }, + }); +}); + +test('literals support for match - simple case without string', async () => { + const code = ` +// @filename: test.py +//// from typing import Literal +//// +//// Currency = Literal["USD", "EUR"] +//// +//// def foo(c: Currency): +//// match c: +//// case [|US/*marker*/|] + `; + + const state = parseAndGetTestState(code).state; + const marker = state.getMarkerByName('marker'); + state.openFile(marker.fileName); + + await state.verifyCompletion('included', MarkupKind.Markdown, { + marker: { + completions: [ + { + kind: CompletionItemKind.Constant, + label: '"USD"', + }, + ], + }, + }); +}); + +test('completion quote trigger', async () => { + const code = ` +// @filename: test.py +//// from typing import Literal +//// +//// Currency = Literal["USD", "EUR"] +//// +//// def foo(c: Currency): +//// match c: +//// case [|"/*marker*/"|] + `; + + const state = parseAndGetTestState(code).state; + const marker = state.getMarkerByName('marker'); + const filePath = marker.fileName; + const position = state.convertOffsetToPosition(filePath, marker.position); + + const options: CompletionOptions = { + format: 'markdown', + snippet: false, + lazyEdit: false, + autoImport: false, + extraCommitChars: false, + importFormat: ImportFormat.Absolute, + includeUserSymbolsInAutoImport: false, + triggerCharacter: '"', + }; + + const result = await state.workspace.serviceInstance.getCompletionsForPosition( + filePath, + position, + state.workspace.path, + options, + undefined, + CancellationToken.None + ); + + assert(result); + const item = result.completionList.items.find((a) => a.label === '"USD"'); + assert(item); +}); + +test('completion quote trigger - middle', async () => { + const code = ` +// @filename: test.py +//// from typing import Literal +//// +//// Currency = Literal["Quote'Middle"] +//// +//// def foo(c: Currency): +//// match c: +//// case [|"Quote'/*marker*/"|] + `; + + const state = parseAndGetTestState(code).state; + const marker = state.getMarkerByName('marker'); + const filePath = marker.fileName; + const position = state.convertOffsetToPosition(filePath, marker.position); + + const options: CompletionOptions = { + format: 'markdown', + snippet: false, + lazyEdit: false, + autoImport: false, + extraCommitChars: false, + importFormat: ImportFormat.Absolute, + includeUserSymbolsInAutoImport: false, + triggerCharacter: "'", + }; + + const result = await state.workspace.serviceInstance.getCompletionsForPosition( + filePath, + position, + state.workspace.path, + options, + undefined, + CancellationToken.None + ); + + assert.strictEqual(result?.completionList.items.length, 0); +}); diff --git a/packages/pyright-internal/src/tests/fourslash/completions.dictionary.keys.expression.fourslash.ts b/packages/pyright-internal/src/tests/fourslash/completions.dictionary.keys.expression.fourslash.ts index abc8f0dd08f8..a145ca86a31c 100644 --- a/packages/pyright-internal/src/tests/fourslash/completions.dictionary.keys.expression.fourslash.ts +++ b/packages/pyright-internal/src/tests/fourslash/completions.dictionary.keys.expression.fourslash.ts @@ -18,7 +18,7 @@ // @filename: dict_expression_partial_expression.py //// d = { "key" : 1 } //// d["key2"] = 1 -//// d[key/*marker4*/] +//// d[[|key/*marker4*/|]] // @filename: dict_expression_complex_key.py //// class C: @@ -52,8 +52,18 @@ }, marker4: { completions: [ - { label: '"key"', kind: Consts.CompletionItemKind.Constant, detail: 'Dictionary key' }, - { label: '"key2"', kind: Consts.CompletionItemKind.Constant, detail: 'Dictionary key' }, + { + label: '"key"', + kind: Consts.CompletionItemKind.Constant, + textEdit: { range: helper.getPositionRange('marker4'), newText: '"key"' }, + detail: 'Dictionary key', + }, + { + label: '"key2"', + kind: Consts.CompletionItemKind.Constant, + textEdit: { range: helper.getPositionRange('marker4'), newText: '"key2"' }, + detail: 'Dictionary key', + }, ], }, marker5: { diff --git a/packages/pyright-internal/src/tests/fourslash/completions.dictionary.keys.stringLiterals.fourslash.ts b/packages/pyright-internal/src/tests/fourslash/completions.dictionary.keys.stringLiterals.fourslash.ts index 671e3f9a3c07..2850832324a5 100644 --- a/packages/pyright-internal/src/tests/fourslash/completions.dictionary.keys.stringLiterals.fourslash.ts +++ b/packages/pyright-internal/src/tests/fourslash/completions.dictionary.keys.stringLiterals.fourslash.ts @@ -25,7 +25,7 @@ // @filename: dict_key_name_conflicts.py //// keyString = "key" //// d = dict(keyString=1) -//// d[keyStr[|/*marker6*/|]] +//// d[[|keyStr/*marker6*/|]] // @filename: dict_key_mixed_literals.py //// d = { "key": 1, 1 + 2: 1 } @@ -94,7 +94,12 @@ marker6: { completions: [ { label: 'keyString', kind: Consts.CompletionItemKind.Variable }, - { label: '"keyString"', kind: Consts.CompletionItemKind.Constant, detail: 'Dictionary key' }, + { + label: '"keyString"', + kind: Consts.CompletionItemKind.Constant, + textEdit: { range: helper.getPositionRange('marker6'), newText: '"keyString"' }, + detail: 'Dictionary key', + }, ], }, marker7: { diff --git a/packages/pyright-internal/src/tests/hoverProvider.test.ts b/packages/pyright-internal/src/tests/hoverProvider.test.ts index 30b170bbd662..e5fbecf05b74 100644 --- a/packages/pyright-internal/src/tests/hoverProvider.test.ts +++ b/packages/pyright-internal/src/tests/hoverProvider.test.ts @@ -294,6 +294,6 @@ test('import tooltip - check duplicate property', async () => { state.openFile(marker.fileName); state.verifyHover('markdown', { - marker: '```python\n(property) def test: (self: Self@Test) -> bool\n```\n---\nTest DocString.\n\nReturns\n-------\nbool \n    Lorem Ipsum', + marker: '```python\n(property) test: (self: Self@Test) -> bool\n```\n---\nTest DocString.\n\nReturns\n-------\nbool \n    Lorem Ipsum', }); }); diff --git a/packages/pyright-internal/src/tests/moveSymbol.misc.test.ts b/packages/pyright-internal/src/tests/moveSymbol.misc.test.ts index 66a87bd00a61..e5623454c5e7 100644 --- a/packages/pyright-internal/src/tests/moveSymbol.misc.test.ts +++ b/packages/pyright-internal/src/tests/moveSymbol.misc.test.ts @@ -157,6 +157,41 @@ test('augmented assignment 2', () => { testNoMoveFromCode(code); }); +test('symbol must be from user files', () => { + const code = ` +// @filename: pyrightconfig.json +//// { +//// "useLibraryCodeForTypes": true +//// } + +// @filename: used.py +//// /*used*/ +//// import lib +//// lib.a + +// @filename: lib.py +// @library: true +//// a = 1 +//// [|/*marker*/a|] += 1 + +// @filename: moved.py +// @library: true +//// [|/*dest*/|] + `; + + const state = parseAndGetTestState(code).state; + while (state.workspace.serviceInstance.test_program.analyze()); + + const actions = state.program.moveSymbolAtPosition( + state.getMarkerByName('marker').fileName, + state.getMarkerByName('dest').fileName, + state.getPositionRange('marker').start, + { importFormat: ImportFormat.Absolute }, + CancellationToken.None + ); + assert(!actions); +}); + function testNoMoveFromCode(code: string) { const state = parseAndGetTestState(code).state;