diff --git a/docs/configuration/language-server-settings.md b/docs/configuration/language-server-settings.md index 0c1fafab17..8fa9f710ba 100644 --- a/docs/configuration/language-server-settings.md +++ b/docs/configuration/language-server-settings.md @@ -61,6 +61,8 @@ the following settings are exclusive to basedpyright **basedpyright.analysis.fileEnumerationTimeout** [integer]: Timeout (in seconds) for file enumeration operations. When basedpyright scans your workspace files, it can take a long time in some workspaces. This setting controls when to show a "slow enumeration" warning. Default is 10 seconds. +**basedpyright.analysis.maxLiteralStringLength** [integer]: Maximum number of characters to show for string literals in hovers, completions, etc. Defaults to `50`. + **basedpyright.analysis.autoFormatStrings** [boolean]: Whether to automatically insert an `f` in front of a string when typing a `{` inside it. Defaults to `true`. [more info](../benefits-over-pyright/pylance-features.md#automatic-conversion-to-f-string-when-typing-inside-a-string) **basedpyright.analysis.configFilePath** [path]: Path to the directory or file containing the Pyright configuration (`pyrightconfig.json` or `pyproject.toml`). If a directory is specified, basedpyright will search for the config file in that directory. This is useful for monorepo structures where the config file is in a subdirectory rather than the workspace root. For example, if your Python code is in a `backend/` subdirectory with its own `pyproject.toml`, you can set this to `${workspaceFolder}/backend` to make basedpyright use that configuration file instead of searching from the workspace root. diff --git a/packages/pyright-internal/src/analyzer/program.ts b/packages/pyright-internal/src/analyzer/program.ts index 70a2403e54..1eea5b7772 100644 --- a/packages/pyright-internal/src/analyzer/program.ts +++ b/packages/pyright-internal/src/analyzer/program.ts @@ -1752,6 +1752,7 @@ export class Program { minimumLoggingThreshold: this._configOptions.typeEvaluationTimeThreshold, evaluateUnknownImportsAsAny: !!this._configOptions.evaluateUnknownImportsAsAny, verifyTypeCacheEvaluatorFlags: !!this._configOptions.internalTestMode, + maxLiteralStringLength: this._configOptions.maxLiteralStringLength, }, this._logTracker, this._configOptions.logTypeEvaluationTime diff --git a/packages/pyright-internal/src/analyzer/service.ts b/packages/pyright-internal/src/analyzer/service.ts index d9ba1fae84..e8359444b6 100644 --- a/packages/pyright-internal/src/analyzer/service.ts +++ b/packages/pyright-internal/src/analyzer/service.ts @@ -1071,6 +1071,13 @@ export class AnalyzerService { configOptions.fileEnumerationTimeoutInSec = languageServerOptions.fileEnumerationTimeoutInSec; } + if (languageServerOptions.maxLiteralStringLength !== undefined) { + const maxLiteralStringLength = languageServerOptions.maxLiteralStringLength; + if (maxLiteralStringLength > 0) { + configOptions.maxLiteralStringLength = maxLiteralStringLength; + } + } + // Special case, the language service can also set a pythonPath. It should override any other setting. if (languageServerOptions.pythonPath) { this._console.info( diff --git a/packages/pyright-internal/src/analyzer/typeEvaluator.ts b/packages/pyright-internal/src/analyzer/typeEvaluator.ts index 3668b28cd3..4159f97a5c 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluator.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluator.ts @@ -602,6 +602,7 @@ export interface EvaluatorOptions { minimumLoggingThreshold: number; evaluateUnknownImportsAsAny: boolean; verifyTypeCacheEvaluatorFlags: boolean; + maxLiteralStringLength: number | undefined; } // Describes a "deferred class completion" that is run when a class type is @@ -28861,17 +28862,16 @@ export function createTypeEvaluator( } function printObjectTypeForClass(type: ClassType): string { - return TypePrinter.printObjectTypeForClass( - type, - evaluatorOptions.printTypeFlags, - getEffectiveReturnType, - undefined - ); + return TypePrinter.printObjectTypeForClass(type, evaluatorOptions.printTypeFlags, getEffectiveReturnType, { + maxLiteralStringLength: evaluatorOptions.maxLiteralStringLength, + }); } function printFunctionParts(type: FunctionType, extraFlags?: TypePrinter.PrintTypeFlags): [string[], string] { const flags = extraFlags ? evaluatorOptions.printTypeFlags | extraFlags : evaluatorOptions.printTypeFlags; - return TypePrinter.printFunctionParts(type, flags, getEffectiveReturnType); + return TypePrinter.printFunctionParts(type, flags, getEffectiveReturnType, { + maxLiteralStringLength: evaluatorOptions.maxLiteralStringLength, + }); } // Prints two types and determines whether they need to be output in @@ -28929,7 +28929,10 @@ export function createTypeEvaluator( if (options?.useFullyQualifiedNames) { flags |= TypePrinter.PrintTypeFlags.UseFullyQualifiedNames; } - const result = TypePrinter.printType(type, flags, getEffectiveReturnType, options?.importTracker); + const result = TypePrinter.printType(type, flags, getEffectiveReturnType, { + importTracker: options?.importTracker, + maxLiteralStringLength: options?.maxLiteralStringLength ?? evaluatorOptions.maxLiteralStringLength, + }); return result; } diff --git a/packages/pyright-internal/src/analyzer/typeEvaluatorTypes.ts b/packages/pyright-internal/src/analyzer/typeEvaluatorTypes.ts index d535d48646..35c734a252 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluatorTypes.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluatorTypes.ts @@ -533,6 +533,7 @@ export interface PrintTypeOptions { printTypeVarVariance?: boolean; omitTypeArgsIfUnknown?: boolean; importTracker?: ImportTracker; + maxLiteralStringLength?: number; } export interface DeclaredSymbolTypeInfo { diff --git a/packages/pyright-internal/src/analyzer/typePrinter.ts b/packages/pyright-internal/src/analyzer/typePrinter.ts index 7baf13178a..1b914b8fe3 100644 --- a/packages/pyright-internal/src/analyzer/typePrinter.ts +++ b/packages/pyright-internal/src/analyzer/typePrinter.ts @@ -107,31 +107,42 @@ export function printType( type: Type, printTypeFlags: PrintTypeFlags, returnTypeCallback: FunctionReturnTypeCallback, - importTracker?: ImportTracker + options?: TypePrintOptions ): string { - const uniqueNameMap = new UniqueNameMap(printTypeFlags, returnTypeCallback, importTracker?.fileUri); + const typePrintOptions = options ?? {}; + const uniqueNameMap = new UniqueNameMap( + printTypeFlags, + returnTypeCallback, + typePrintOptions.importTracker?.fileUri + ); uniqueNameMap.build(type); - return printTypeInternal(type, printTypeFlags, returnTypeCallback, uniqueNameMap, [], 0, importTracker); + return printTypeInternal(type, printTypeFlags, returnTypeCallback, uniqueNameMap, [], 0, typePrintOptions); } export function printFunctionParts( type: FunctionType, printTypeFlags: PrintTypeFlags, - returnTypeCallback: FunctionReturnTypeCallback + returnTypeCallback: FunctionReturnTypeCallback, + options?: TypePrintOptions ): [string[], string] { - const uniqueNameMap = new UniqueNameMap(printTypeFlags, returnTypeCallback); + const uniqueNameMap = new UniqueNameMap(printTypeFlags, returnTypeCallback, options?.importTracker?.fileUri); uniqueNameMap.build(type); - return printFunctionPartsInternal(type, printTypeFlags, returnTypeCallback, uniqueNameMap, [], 0); + return printFunctionPartsInternal(type, printTypeFlags, returnTypeCallback, uniqueNameMap, [], 0, options ?? {}); } export function printObjectTypeForClass( type: ClassType, printTypeFlags: PrintTypeFlags, returnTypeCallback: FunctionReturnTypeCallback, - importTracker: ImportTracker | undefined + options?: TypePrintOptions ): string { - const uniqueNameMap = new UniqueNameMap(printTypeFlags, returnTypeCallback, importTracker?.fileUri); + const typePrintOptions = options ?? {}; + const uniqueNameMap = new UniqueNameMap( + printTypeFlags, + returnTypeCallback, + typePrintOptions.importTracker?.fileUri + ); uniqueNameMap.build(type); return printObjectTypeForClassInternal( @@ -141,13 +152,30 @@ export function printObjectTypeForClass( uniqueNameMap, [], 0, - importTracker + typePrintOptions ); } -const maxLiteralStringLength = 50; +export interface TypePrintOptions { + importTracker?: ImportTracker; + maxLiteralStringLength?: number; +} + +const defaultMaxLiteralStringLength = 50; + +function getMaxLiteralStringLength(options?: TypePrintOptions) { + const maxLiteralStringLength = options?.maxLiteralStringLength; + + if (maxLiteralStringLength && maxLiteralStringLength > 0) { + return maxLiteralStringLength; + } + + return defaultMaxLiteralStringLength; +} + +export function isLiteralValueTruncated(type: ClassType, options?: TypePrintOptions): boolean { + const maxLiteralStringLength = getMaxLiteralStringLength(options); -export function isLiteralValueTruncated(type: ClassType): boolean { if (typeof type.priv.literalValue === 'string') { if (type.priv.literalValue.length > maxLiteralStringLength) { return true; @@ -157,7 +185,8 @@ export function isLiteralValueTruncated(type: ClassType): boolean { return false; } -export function printLiteralValueTruncated(type: ClassType, importTracker: ImportTracker | undefined): string { +export function printLiteralValueTruncated(type: ClassType, options?: TypePrintOptions): string { + const importTracker = options?.importTracker; if (type.shared.name === 'bytes') { return 'bytes'; } @@ -168,15 +197,17 @@ export function printLiteralValueTruncated(type: ClassType, importTracker: Impor return name; } -export function printLiteralValue(type: ClassType, quotation = "'", importTracker: ImportTracker | undefined): string { +export function printLiteralValue(type: ClassType, quotation = "'", options?: TypePrintOptions): string { const literalValue = type.priv.literalValue; if (literalValue === undefined) { return ''; } + const importTracker = options?.importTracker; let literalStr: string; if (typeof literalValue === 'string') { let effectiveLiteralValue = literalValue; + const maxLiteralStringLength = getMaxLiteralStringLength(options); // Limit the length of the string literal. if (literalValue.length > maxLiteralStringLength) { @@ -273,8 +304,9 @@ function printTypeInternal( uniqueNameMap: UniqueNameMap, recursionTypes: Type[], recursionCount: number, - importTracker: ImportTracker | undefined + options: TypePrintOptions ): string { + const importTracker = options.importTracker; if (recursionCount > maxTypeRecursionCount) { if (printTypeFlags & PrintTypeFlags.PythonSyntax) { importTracker?.addTypingImport('Any'); @@ -351,7 +383,7 @@ function printTypeInternal( uniqueNameMap, recursionTypes, recursionCount, - importTracker + options ) ); }); @@ -364,7 +396,7 @@ function printTypeInternal( uniqueNameMap, recursionTypes, recursionCount, - importTracker + options ) ); } @@ -385,7 +417,7 @@ function printTypeInternal( uniqueNameMap, recursionTypes, recursionCount, - importTracker + options ) ); }); @@ -452,7 +484,7 @@ function printTypeInternal( uniqueNameMap, recursionTypes, recursionCount, - importTracker + options ); } finally { recursionTypes.pop(); @@ -502,13 +534,16 @@ function printTypeInternal( case TypeCategory.Class: { if (TypeBase.isInstance(type)) { if (type.priv.literalValue !== undefined) { - if (isLiteralValueTruncated(type) && (printTypeFlags & PrintTypeFlags.PythonSyntax) !== 0) { - return printLiteralValueTruncated(type, importTracker); + if ( + isLiteralValueTruncated(type, options) && + (printTypeFlags & PrintTypeFlags.PythonSyntax) !== 0 + ) { + return printLiteralValueTruncated(type, options); } else if (type.priv.literalValue instanceof SentinelLiteral) { return type.priv.literalValue.className; } else { importTracker?.addTypingImport('Literal'); - return `Literal[${printLiteralValue(type, "'", importTracker)}]`; + return `Literal[${printLiteralValue(type, "'", options)}]`; } } @@ -519,19 +554,20 @@ function printTypeInternal( uniqueNameMap, recursionTypes, recursionCount, - importTracker + + options )}${getConditionalIndicator(type)}`; } else { let typeToWrap: string; if (type.priv.literalValue !== undefined) { if (isLiteralValueTruncated(type) && (printTypeFlags & PrintTypeFlags.PythonSyntax) !== 0) { - typeToWrap = printLiteralValueTruncated(type, importTracker); + typeToWrap = printLiteralValueTruncated(type, options); } else if (type.priv.literalValue instanceof SentinelLiteral) { return type.priv.literalValue.className; } else { importTracker?.addTypingImport('Literal'); - typeToWrap = `Literal[${printLiteralValue(type, "'", importTracker)}]`; + typeToWrap = `Literal[${printLiteralValue(type, "'", options)}]`; } return printWrappedType(type, typeToWrap); @@ -545,7 +581,8 @@ function printTypeInternal( uniqueNameMap, recursionTypes, recursionCount, - importTracker + + options ); return specialFormText; @@ -558,7 +595,8 @@ function printTypeInternal( uniqueNameMap, recursionTypes, recursionCount, - importTracker + + options ); return printWrappedType(type, typeToWrap); @@ -574,7 +612,8 @@ function printTypeInternal( uniqueNameMap, recursionTypes, recursionCount, - importTracker + + options ); return `type[${typeString}]`; } @@ -586,7 +625,8 @@ function printTypeInternal( uniqueNameMap, recursionTypes, recursionCount, - importTracker + + options ); } @@ -599,7 +639,8 @@ function printTypeInternal( uniqueNameMap, recursionTypes, recursionCount, - importTracker + + options ) ); @@ -627,7 +668,8 @@ function printTypeInternal( uniqueNameMap, recursionTypes, recursionCount, - importTracker + + options ); return specialFormText; @@ -647,7 +689,8 @@ function printTypeInternal( uniqueNameMap, recursionTypes, recursionCount, - importTracker + + options ); } @@ -670,7 +713,8 @@ function printTypeInternal( uniqueNameMap, recursionTypes, recursionCount, - importTracker + + options ); } return type.shared.recursiveAlias.name; @@ -687,7 +731,8 @@ function printTypeInternal( uniqueNameMap, recursionTypes, recursionCount, - importTracker + + options ); if (!isAnyOrUnknown(type.shared.boundType)) { @@ -788,8 +833,9 @@ function printUnionType( uniqueNameMap: UniqueNameMap, recursionTypes: Type[], recursionCount: number, - importTracker: ImportTracker | undefined + options: TypePrintOptions ) { + const importTracker = options.importTracker; // Allocate a set that refers to subtypes in the union by // their indices. If the index is within the set, it is already // accounted for in the output. @@ -839,7 +885,8 @@ function printUnionType( uniqueNameMap, recursionTypes, recursionCount, - importTracker + + options ) ); indicesCoveredByTypeAlias.forEach((index) => subtypeHandledSet.add(index)); @@ -861,7 +908,8 @@ function printUnionType( uniqueNameMap, recursionTypes, recursionCount, - importTracker + + options ); if (printTypeFlags & PrintTypeFlags.PEP604) { @@ -882,9 +930,9 @@ function printUnionType( if (!subtypeHandledSet.has(index)) { if (isClassInstance(subtype) && subtype.priv.literalValue !== undefined && !isSentinelLiteral(subtype)) { if (isLiteralValueTruncated(subtype) && (printTypeFlags & PrintTypeFlags.PythonSyntax) !== 0) { - subtypeStrings.add(printLiteralValueTruncated(subtype, importTracker)); + subtypeStrings.add(printLiteralValueTruncated(subtype, options)); } else { - literalObjectStrings.add(printLiteralValue(subtype, "'", importTracker)); + literalObjectStrings.add(printLiteralValue(subtype, "'", options)); } } else if ( isInstantiableClass(subtype) && @@ -892,9 +940,9 @@ function printUnionType( !isSentinelLiteral(subtype) ) { if (isLiteralValueTruncated(subtype) && (printTypeFlags & PrintTypeFlags.PythonSyntax) !== 0) { - subtypeStrings.add(`type[${printLiteralValueTruncated(subtype, importTracker)}]`); + subtypeStrings.add(`type[${printLiteralValueTruncated(subtype, options)}]`); } else { - literalClassStrings.add(printLiteralValue(subtype, "'", importTracker)); + literalClassStrings.add(printLiteralValue(subtype, "'", options)); } } else { subtypeStrings.add( @@ -905,7 +953,8 @@ function printUnionType( uniqueNameMap, recursionTypes, recursionCount, - importTracker + + options ) ); } @@ -951,8 +1000,9 @@ function printFunctionType( uniqueNameMap: UniqueNameMap, recursionTypes: Type[], recursionCount: number, - importTracker: ImportTracker | undefined + options: TypePrintOptions ) { + const importTracker = options.importTracker; if (printTypeFlags & PrintTypeFlags.PythonSyntax) { const paramSpec = FunctionType.getParamSpecFromArgsKwargs(type); const typeWithoutParamSpec = paramSpec ? FunctionType.cloneRemoveParamSpecArgsKwargs(type) : type; @@ -981,7 +1031,8 @@ function printFunctionType( uniqueNameMap, recursionTypes, recursionCount, - importTracker + + options ); } else { importTracker?.addTypingImport('Any'); @@ -1003,7 +1054,8 @@ function printFunctionType( uniqueNameMap, recursionTypes, recursionCount, - importTracker + + options ) ); } else { @@ -1037,7 +1089,9 @@ function printFunctionType( returnTypeCallback, uniqueNameMap, recursionTypes, - recursionCount + recursionCount, + + options ); const paramSignature = `(${parts[0].join(', ')})`; @@ -1066,8 +1120,9 @@ function printObjectTypeForClassInternal( uniqueNameMap: UniqueNameMap, recursionTypes: Type[], recursionCount: number, - importTracker: ImportTracker | undefined + options: TypePrintOptions ): string { + const importTracker = options.importTracker; let objName = type.priv.aliasName; if (!objName) { objName = @@ -1137,7 +1192,8 @@ function printObjectTypeForClassInternal( uniqueNameMap, recursionTypes, recursionCount, - importTracker + + options ); if (typeArg.isUnbounded) { @@ -1160,7 +1216,8 @@ function printObjectTypeForClassInternal( uniqueNameMap, recursionTypes, recursionCount, - importTracker + + options ); if (typeArg.isUnbounded) { @@ -1212,7 +1269,8 @@ function printObjectTypeForClassInternal( uniqueNameMap, recursionTypes, recursionCount, - importTracker + + options ); }) .join(', ') + @@ -1238,7 +1296,8 @@ function printFunctionPartsInternal( returnTypeCallback: FunctionReturnTypeCallback, uniqueNameMap: UniqueNameMap, recursionTypes: Type[], - recursionCount: number + recursionCount: number, + options: TypePrintOptions ): [string[], string] { const paramTypeStrings: string[] = []; let sawDefinedName = false; @@ -1273,7 +1332,8 @@ function printFunctionPartsInternal( uniqueNameMap, recursionTypes, recursionCount, - undefined + + options ); paramTypeStrings.push(paramString); }); @@ -1295,7 +1355,8 @@ function printFunctionPartsInternal( uniqueNameMap, recursionTypes, recursionCount, - undefined + + options ); paramTypeStrings.push(`${k}: ${valueTypeString}`); }); @@ -1309,7 +1370,8 @@ function printFunctionPartsInternal( uniqueNameMap, recursionTypes, recursionCount, - undefined + + options ); paramTypeStrings.push(`**kwargs: ${valueTypeString}`); } @@ -1353,7 +1415,8 @@ function printFunctionPartsInternal( uniqueNameMap, recursionTypes, recursionCount, - undefined + + options ) : ''; @@ -1439,7 +1502,8 @@ function printFunctionPartsInternal( uniqueNameMap, recursionTypes, recursionCount, - undefined + + options )}` ); } @@ -1455,7 +1519,8 @@ function printFunctionPartsInternal( uniqueNameMap, recursionTypes, recursionCount, - undefined + + options ) : ''; diff --git a/packages/pyright-internal/src/common/commandLineOptions.ts b/packages/pyright-internal/src/common/commandLineOptions.ts index f7b406af3e..f2303689a0 100644 --- a/packages/pyright-internal/src/common/commandLineOptions.ts +++ b/packages/pyright-internal/src/common/commandLineOptions.ts @@ -144,6 +144,9 @@ export class CommandLineLanguageServerOptions { // Override default timeout (in seconds) for file enumeration operations. fileEnumerationTimeoutInSec?: number; + // Maximum length of string literals when displayed. + maxLiteralStringLength?: number; + // Run ambient analysis. enableAmbientAnalysis = true; diff --git a/packages/pyright-internal/src/common/configOptions.ts b/packages/pyright-internal/src/common/configOptions.ts index fb861ea0d0..ebfaf400a5 100644 --- a/packages/pyright-internal/src/common/configOptions.ts +++ b/packages/pyright-internal/src/common/configOptions.ts @@ -1406,6 +1406,9 @@ export class ConfigOptions { // Minimum threshold for type eval logging typeEvaluationTimeThreshold = 50; + // Maximum number of characters to show for string literals in hovers/completions. + maxLiteralStringLength: number | undefined = undefined; + // Was this config initialized from JSON (pyrightconfig/pyproject)? initializedFromJson = false; @@ -1832,6 +1835,13 @@ export class ConfigOptions { } } + if (configObj.maxLiteralStringLength !== undefined) { + const val = Number(configObj.maxLiteralStringLength); + if (val > 0) { + this.maxLiteralStringLength = val; + } + } + // Read the "functionSignatureDisplay" setting. if (configObj.functionSignatureDisplay !== undefined) { if (typeof configObj.functionSignatureDisplay !== 'string') { diff --git a/packages/pyright-internal/src/common/languageServerInterface.ts b/packages/pyright-internal/src/common/languageServerInterface.ts index 6a942253a4..6b030d521b 100644 --- a/packages/pyright-internal/src/common/languageServerInterface.ts +++ b/packages/pyright-internal/src/common/languageServerInterface.ts @@ -53,6 +53,7 @@ export interface ServerSettings { useTypingExtensions?: boolean; fileEnumerationTimeoutInSec?: number | undefined; autoFormatStrings?: boolean; + maxLiteralStringLength?: number | undefined; } export interface MessageAction { diff --git a/packages/pyright-internal/src/languageServerBase.ts b/packages/pyright-internal/src/languageServerBase.ts index 9c2b31f302..0fcaca5a19 100644 --- a/packages/pyright-internal/src/languageServerBase.ts +++ b/packages/pyright-internal/src/languageServerBase.ts @@ -444,6 +444,8 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis workspace.useTypingExtensions = serverSettings.useTypingExtensions ?? false; workspace.fileEnumerationTimeoutInSec = serverSettings.fileEnumerationTimeoutInSec ?? 10; workspace.autoFormatStrings = serverSettings.autoFormatStrings ?? true; + workspace.maxLiteralStringLength = + workspace.service.getConfigOptions().maxLiteralStringLength ?? serverSettings.maxLiteralStringLength; } finally { // Don't use workspace.isInitialized directly since it might have been // reset due to pending config change event. @@ -1123,6 +1125,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis } return workspace.service.run((program) => { + workspace.inlayHints; const completions = new CompletionProvider( program, uri, @@ -1134,6 +1137,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis triggerCharacter: params?.context?.triggerCharacter, checkDeprecatedWhenResolving: this.client.completionItemResolveSupportsTags, useTypingExtensions: workspace.useTypingExtensions, + maxLiteralStringLength: workspace.maxLiteralStringLength, }, token, false @@ -1166,6 +1170,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis lazyEdit: false, checkDeprecatedWhenResolving: this.client.completionItemResolveSupportsTags, useTypingExtensions: workspace.useTypingExtensions, + maxLiteralStringLength: workspace.maxLiteralStringLength, }, token, false diff --git a/packages/pyright-internal/src/languageService/analyzerServiceExecutor.ts b/packages/pyright-internal/src/languageService/analyzerServiceExecutor.ts index f70c386209..a723d885c9 100644 --- a/packages/pyright-internal/src/languageService/analyzerServiceExecutor.ts +++ b/packages/pyright-internal/src/languageService/analyzerServiceExecutor.ts @@ -161,6 +161,10 @@ export function getEffectiveCommandLineOptions( serverSettings.fileEnumerationTimeoutInSec; } + if (serverSettings.maxLiteralStringLength !== undefined) { + commandLineOptions.languageServerSettings.maxLiteralStringLength = serverSettings.maxLiteralStringLength; + } + if (typeStubTargetImportName) { commandLineOptions.languageServerSettings.typeStubTargetImportName = typeStubTargetImportName; } diff --git a/packages/pyright-internal/src/languageService/codeActionProvider.ts b/packages/pyright-internal/src/languageService/codeActionProvider.ts index 2a389ce9da..e67a82b08b 100644 --- a/packages/pyright-internal/src/languageService/codeActionProvider.ts +++ b/packages/pyright-internal/src/languageService/codeActionProvider.ts @@ -87,6 +87,7 @@ export class CodeActionProvider { // (we don't call resolveCompletionItem) checkDeprecatedWhenResolving: true, useTypingExtensions: workspace.useTypingExtensions, + maxLiteralStringLength: workspace.maxLiteralStringLength, }, token, true diff --git a/packages/pyright-internal/src/languageService/completionProvider.ts b/packages/pyright-internal/src/languageService/completionProvider.ts index 95985be155..0c16b49f0b 100644 --- a/packages/pyright-internal/src/languageService/completionProvider.ts +++ b/packages/pyright-internal/src/languageService/completionProvider.ts @@ -254,6 +254,7 @@ export interface CompletionOptions { readonly triggerCharacter?: string; readonly checkDeprecatedWhenResolving: boolean; readonly useTypingExtensions: boolean; + readonly maxLiteralStringLength?: number; } interface RecentCompletionInfo { @@ -2222,7 +2223,7 @@ export class CompletionProvider { const quoteValue = this._getQuoteInfo(priorWord, priorText); this._getSubTypesWithLiteralValues(type).forEach((v) => { if (ClassType.isBuiltIn(v, 'str')) { - const value = printLiteralValue(v, quoteValue.quoteCharacter, undefined); + const value = printLiteralValue(v, quoteValue.quoteCharacter, this.options); if (quoteValue.stringValue === undefined) { this.addNameToCompletions(value, CompletionItemKind.Constant, priorWord, completionMap, { sortText: this._makeSortText(SortCategory.LiteralValue, v.priv.literalValue as string), @@ -2334,7 +2335,11 @@ export class CompletionProvider { } keys.push( - printLiteralValue(v, this.parseResults.tokenizerOutput.predominantSingleQuoteCharacter, undefined) + printLiteralValue( + v, + this.parseResults.tokenizerOutput.predominantSingleQuoteCharacter, + this.options + ) ); }); diff --git a/packages/pyright-internal/src/realLanguageServer.ts b/packages/pyright-internal/src/realLanguageServer.ts index e482a16b0d..134a16826e 100644 --- a/packages/pyright-internal/src/realLanguageServer.ts +++ b/packages/pyright-internal/src/realLanguageServer.ts @@ -237,6 +237,10 @@ export abstract class RealLanguageServer extends LanguageServerBase { serverSettings.fileEnumerationTimeoutInSec = pythonAnalysisSection.fileEnumerationTimeout; } + if (pythonAnalysisSection.maxLiteralStringLength !== undefined) { + serverSettings.maxLiteralStringLength = pythonAnalysisSection.maxLiteralStringLength; + } + if (pythonAnalysisSection.autoFormatStrings !== undefined) { serverSettings.autoFormatStrings = pythonAnalysisSection.autoFormatStrings; } diff --git a/packages/pyright-internal/src/tests/completions.test.ts b/packages/pyright-internal/src/tests/completions.test.ts index aa6218706c..c1de435820 100644 --- a/packages/pyright-internal/src/tests/completions.test.ts +++ b/packages/pyright-internal/src/tests/completions.test.ts @@ -1933,3 +1933,59 @@ test('import from stdlib package', async () => { false ); }); + +test('string literal length is respected', async () => { + const code = ` +// @filename: test.py +//// from typing import Literal +//// +//// A = Literal[ +//// "123456789", +//// "123456789_123456789_123456789" +//// ] +//// +//// def foo(a: A): +//// pass +//// +//// foo([|"/*marker*/"|]) + `; + + const state = parseAndGetTestState(code).state; + const marker = state.getMarkerByName('marker'); + const filePath = marker.fileName; + const uri = Uri.file(filePath, state.serviceProvider); + const position = state.convertOffsetToPosition(filePath, marker.position); + + const cases: [number, string[]][] = [ + [5, ['12345…']], + [20, ['123456789', '123456789_123456789_…']], + [100, ['123456789', '123456789_123456789_123456789']], + ]; + + for (const [length, completions] of cases) { + const options: CompletionOptions = { + format: 'markdown', + snippet: false, + lazyEdit: false, + triggerCharacter: '"', + checkDeprecatedWhenResolving: false, + useTypingExtensions: false, + maxLiteralStringLength: length, + }; + + const result = new CompletionProvider( + state.program, + uri, + position, + options, + CancellationToken.None, + false + ).getCompletions(); + + assert(result); + for (const label of completions) { + const item = result.items.find((a) => a.label === `"${label}"`); + assert(item, `Expected to find completion item with label "${label}" for maxLiteralStringLength=${length}`); + } + } +}); diff --git a/packages/pyright-internal/src/tests/harness/fourslash/testLanguageService.ts b/packages/pyright-internal/src/tests/harness/fourslash/testLanguageService.ts index 2e6b556165..7ef6d07eaf 100644 --- a/packages/pyright-internal/src/tests/harness/fourslash/testLanguageService.ts +++ b/packages/pyright-internal/src/tests/harness/fourslash/testLanguageService.ts @@ -128,6 +128,7 @@ export class TestLanguageService implements LanguageServerInterface { searchPathsToWatch: [], useTypingExtensions: false, fileEnumerationTimeoutInSec: 10, + maxLiteralStringLength: undefined, autoFormatStrings: true, }; } diff --git a/packages/pyright-internal/src/tests/harness/fourslash/testState.ts b/packages/pyright-internal/src/tests/harness/fourslash/testState.ts index e45a3f820e..cf3684b7ff 100644 --- a/packages/pyright-internal/src/tests/harness/fourslash/testState.ts +++ b/packages/pyright-internal/src/tests/harness/fourslash/testState.ts @@ -216,6 +216,7 @@ export class TestState { searchPathsToWatch: [], useTypingExtensions: false, fileEnumerationTimeoutInSec: 10, + maxLiteralStringLength: undefined, autoFormatStrings: true, }; diff --git a/packages/pyright-internal/src/tests/typePrinter.test.ts b/packages/pyright-internal/src/tests/typePrinter.test.ts index 94cb548605..aa5621d4bf 100644 --- a/packages/pyright-internal/src/tests/typePrinter.test.ts +++ b/packages/pyright-internal/src/tests/typePrinter.test.ts @@ -187,6 +187,33 @@ test('FunctionTypes', () => { assert.strictEqual(printType(funcTypeD, PrintTypeFlags.PythonSyntax, returnTypeCallback), 'Callable[P, Any]'); }); +test('LiteralStringTruncation', () => { + const strClass = ClassType.createInstantiable( + 'str', + 'builtins', + '', + Uri.empty(), + ClassTypeFlags.BuiltIn, + 0, + /* declaredMetaclass */ undefined, + /* effectiveMetaclass */ undefined + ); + const literalStringType = ClassType.cloneWithLiteral( + ClassType.cloneAsInstance(strClass), + '123456789012345678901234567890123456789012345678901234567890' + ); + + const truncated = printType(literalStringType, PrintTypeFlags.PythonSyntax, returnTypeCallback, { + maxLiteralStringLength: 10, + }); + assert.strictEqual(truncated, 'LiteralString'); + + const full = printType(literalStringType, PrintTypeFlags.PythonSyntax, returnTypeCallback, { + maxLiteralStringLength: 100, + }); + assert.strictEqual(full, "Literal['123456789012345678901234567890123456789012345678901234567890']"); +}); + describe('ParamSpec', () => { test('positional only', () => { const paramSpec = FunctionType.createSynthesizedInstance('', FunctionTypeFlags.ParamSpecValue); diff --git a/packages/pyright-internal/src/workspaceFactory.ts b/packages/pyright-internal/src/workspaceFactory.ts index 88d84563f3..e91df324a0 100644 --- a/packages/pyright-internal/src/workspaceFactory.ts +++ b/packages/pyright-internal/src/workspaceFactory.ts @@ -100,6 +100,7 @@ export interface Workspace extends WorkspaceFolder { isInitialized: InitStatus; searchPathsToWatch: Uri[]; inlayHints?: InlayHintSettings | undefined; + maxLiteralStringLength: number | undefined; useTypingExtensions: boolean; fileEnumerationTimeoutInSec: number; autoFormatStrings: boolean; @@ -304,6 +305,7 @@ export class WorkspaceFactory implements IWorkspaceFactory { searchPathsToWatch: [], useTypingExtensions: false, fileEnumerationTimeoutInSec: 10, + maxLiteralStringLength: undefined, autoFormatStrings: true, }; diff --git a/packages/vscode-pyright/package.json b/packages/vscode-pyright/package.json index 1cc04d7332..b6e86fb383 100644 --- a/packages/vscode-pyright/package.json +++ b/packages/vscode-pyright/package.json @@ -1827,6 +1827,12 @@ "description": "Timeout (in seconds) for file enumeration operations. Default is 10 seconds.", "scope": "resource" }, + "basedpyright.analysis.maxLiteralStringLength": { + "type": "integer", + "minimum": 1, + "description": "Maximum number of characters to show for string literals in hovers, completions, etc.", + "scope": "resource" + }, "basedpyright.analysis.logLevel": { "type": "string", "default": "Information", @@ -2005,4 +2011,4 @@ "webpack": "^5.97.1", "webpack-cli": "^5.1.4" } -} +} \ No newline at end of file diff --git a/packages/vscode-pyright/schemas/pyrightconfig.schema.json b/packages/vscode-pyright/schemas/pyrightconfig.schema.json index 23348336d9..865c9d0945 100644 --- a/packages/vscode-pyright/schemas/pyrightconfig.schema.json +++ b/packages/vscode-pyright/schemas/pyrightconfig.schema.json @@ -626,11 +626,13 @@ "defineConstant": { "type": "object", "title": "Identifiers that should be treated as constants", - "properties": { - }, + "properties": {}, "additionalProperties": { - "type": ["string", "boolean"], - "title": "Value of constant (boolean or string)" + "type": [ + "string", + "boolean" + ], + "title": "Value of constant (boolean or string)" } }, "typeCheckingMode": { @@ -1015,6 +1017,11 @@ "title": "Path to a baseline file that contains a list of diagnostics that should be ignored. defaults to `./.basedpyright/baseline.json`", "default": "" }, + "maxLiteralStringLength": { + "type": "integer", + "description": "Maximum length of string literals when displayed in hovers, completions, etc.", + "minimum": 1 + }, "executionEnvironments": { "type": "array", "title": "Analysis settings to use for specified subdirectories of code", @@ -1359,4 +1366,4 @@ } } } -} +} \ No newline at end of file