From 873c2d1c83fe143f09c2f32b74ac1e4f84d989de Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 25 Jan 2023 00:15:19 +0300 Subject: [PATCH 1/2] feat(completion): Include file extensions that should have included. List of extensions is derived from module augmentations, but can be extended with setting for unchecked contexts. fix: specify all possible values for `removeCodeFixes.codefixes`! --- README.MD | 7 +- src/configurationType.ts | 94 ++++++++++++++++++++++++- typescript/src/completionsAtPosition.ts | 58 +++++++++++++-- typescript/src/decorateProxy.ts | 3 +- 4 files changed, 154 insertions(+), 8 deletions(-) diff --git a/README.MD b/README.MD index 90279f9d..06ecc6e5 100644 --- a/README.MD +++ b/README.MD @@ -47,7 +47,7 @@ You can force enable this by using `Enable Strict Emmet in JSX` command. #### Optional Emmet Features -- cleanup input & textarea suggestions +- cleanup suggestions (can be enabled `jsxEmmet.modernize`) - override `.` snippet Is not supported in the web for now. @@ -157,6 +157,11 @@ Patches `toString()` insert function snippet on number types to remove tabStop. Try to restore [original](https://github.com/microsoft/TypeScript/issues/49012) properties sorting in some places such as object destructure & dot property access. +### File Extension Suggestions + +We extend completion list with extensions from module augmentation (e.g. `.css` files if you have `declare module '*.css'`). +But for unchecked contexts list of extensions can be extended with `tsEssentialPlugins.additionalIncludeExtensions` setting. + ### Switch Exclude Covered Cases (*enabled by default*) diff --git a/src/configurationType.ts b/src/configurationType.ts index 8baad3a9..ddc9878b 100644 --- a/src/configurationType.ts +++ b/src/configurationType.ts @@ -191,11 +191,19 @@ export type Configuration = { * @default true * */ 'removeCodeFixes.enable': boolean + /** + * Additional file extension to include in completions (suggestions) + * + * **For unchecked files only**, for checked files use module augmentation. + * Example: `["css"]` or `["*"]` that will include literally every file extension + * @default [] + */ + additionalIncludeExtensions: string[] /** * @default ["fixMissingFunctionDeclaration"] * @uniqueItems true * */ - 'removeCodeFixes.codefixes': ('fixMissingMember' | 'fixMissingProperties' | 'fixMissingAttributes' | 'fixMissingFunctionDeclaration')[] + 'removeCodeFixes.codefixes': FixId[] /** * Use full-blown emmet in jsx/tsx files! * Requires `jsxPseudoEmmet.enabled` to be disabled and `emmet.excludeLanguages` to have `javascriptreact` and `typescriptreact` @@ -444,3 +452,87 @@ export type Configuration = { */ displayAdditionalInfoInCompletions: boolean } + +// scrapped using search editor. config: caseInsesetive, context lines: 0, regex: const fix\w+ = "[^ ]+" +type FixId = + | 'addConvertToUnknownForNonOverlappingTypes' + | 'addMissingAsync' + | 'addMissingAwait' + | 'addMissingConst' + | 'addMissingDeclareProperty' + | 'addMissingInvocationForDecorator' + | 'addNameToNamelessParameter' + | 'annotateWithTypeFromJSDoc' + | 'fixConvertConstToLet' + | 'convertFunctionToEs6Class' + | 'convertLiteralTypeToMappedType' + | 'convertToAsyncFunction' + | 'fixConvertToMappedObjectType' + | 'convertToTypeOnlyExport' + | 'convertToTypeOnlyImport' + | 'correctQualifiedNameToIndexedAccessType' + | 'disableJsDiagnostics' + | 'disableJsDiagnostics' + | 'addMissingConstraint' + | 'fixMissingMember' + | 'fixMissingProperties' + | 'fixMissingAttributes' + | 'fixMissingFunctionDeclaration' + | 'addMissingNewOperator' + | 'fixAddModuleReferTypeMissingTypeof' + | 'addVoidToPromise' + | 'addVoidToPromise' + | 'fixAwaitInSyncFunction' + | 'fixCannotFindModule' + | 'installTypesPackage' + | 'fixClassDoesntImplementInheritedAbstractMember' + | 'fixClassIncorrectlyImplementsInterface' + | 'classSuperMustPrecedeThisAccess' + | 'constructorForDerivedNeedSuperCall' + | 'enableExperimentalDecorators' + | 'fixEnableJsxFlag' + | 'fixExpectedComma' + | 'extendsInterfaceBecomesImplements' + | 'forgottenThisPropertyAccess' + | 'fixImplicitThis' + | 'fixImportNonExportedMember' + | 'fixIncorrectNamedTupleSyntax' + | 'invalidImportSyntax' + | 'fixInvalidJsxCharacters_expression' + | 'fixInvalidJsxCharacters_htmlEntity' + | 'fixJSDocTypes_plain' + | 'fixJSDocTypes_nullable' + | 'fixMissingCallParentheses' + | 'fixNaNEquality' + | 'fixNoPropertyAccessFromIndexSignature' + | 'fixOverrideModifier' + | 'fixAddOverrideModifier' + | 'fixRemoveOverrideModifier' + | 'fixPropertyAssignment' + | 'fixPropertyOverrideAccessor' + | 'fixReturnTypeInAsyncFunction' + | 'fixSpelling' + | 'strictClassInitialization' + | 'addMissingPropertyDefiniteAssignmentAssertions' + | 'addMissingPropertyUndefinedType' + | 'addMissingPropertyInitializer' + | 'fixUnreachableCode' + | 'fixUnreferenceableDecoratorMetadata' + | 'unusedIdentifier' + | 'unusedIdentifier_prefix' + | 'unusedIdentifier_delete' + | 'unusedIdentifier_deleteImports' + | 'unusedIdentifier_infer' + | 'fixUnusedLabel' + | 'inferFromUsage' + | 'removeAccidentalCallParentheses' + | 'removeUnnecessaryAwait' + | 'requireInTs' + | 'returnValueCorrect' + | 'fixAddReturnStatement' + | 'fixRemoveBracesFromArrowFunctionBody' + | 'fixWrapTheBlockWithParen' + | 'splitTypeOnlyImport' + | 'useBigintLiteral' + | 'useDefaultImport' + | 'wrapJsxInFragment' diff --git a/typescript/src/completionsAtPosition.ts b/typescript/src/completionsAtPosition.ts index 0a3e899f..ebc86c88 100644 --- a/typescript/src/completionsAtPosition.ts +++ b/typescript/src/completionsAtPosition.ts @@ -20,7 +20,7 @@ import defaultHelpers from './completions/defaultHelpers' import objectLiteralCompletions from './completions/objectLiteralCompletions' import filterJsxElements from './completions/filterJsxComponents' import markOrRemoveGlobalCompletions from './completions/markOrRemoveGlobalLibCompletions' -import { oneOf } from '@zardoy/utils' +import { compact, oneOf } from '@zardoy/utils' import filterWIthIgnoreAutoImports from './completions/ignoreAutoImports' import escapeStringRegexp from 'escape-string-regexp' import addSourceDefinition from './completions/addSourceDefinition' @@ -38,7 +38,7 @@ export const getCompletionsAtPosition = ( languageService: ts.LanguageService, scriptSnapshot: ts.IScriptSnapshot, formatOptions: ts.FormatCodeSettings | undefined, - additionalData: { scriptKind: ts.ScriptKind }, + additionalData: { scriptKind: ts.ScriptKind; compilerOptions: ts.CompilerOptions }, ): | { completions: ts.CompletionInfo @@ -52,7 +52,18 @@ export const getCompletionsAtPosition = ( const sourceFile = program?.getSourceFile(fileName) if (!program || !sourceFile) return if (!scriptSnapshot || isInBannedPosition(position, scriptSnapshot, sourceFile)) return - let prior = languageService.getCompletionsAtPosition(fileName, position, options, formatOptions) + const exactNode = findChildContainingExactPosition(sourceFile, position) + const isCheckedFile = + !tsFull.isSourceFileJS(sourceFile as any) || !!tsFull.isCheckJsEnabledForFile(sourceFile as any, additionalData.compilerOptions as any) + const unpatch = patchBuiltinMethods(c, languageService, isCheckedFile) + const getPrior = () => { + try { + return languageService.getCompletionsAtPosition(fileName, position, options, formatOptions) + } finally { + unpatch() + } + } + let prior = getPrior() const ensurePrior = () => { if (!prior) prior = { entries: [], isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false } return true @@ -62,7 +73,6 @@ export const getCompletionsAtPosition = ( /** node that is one character behind * useful as in most cases we work with node that is behind the cursor */ const leftNode = findChildContainingPosition(ts, sourceFile, position - 1) - const exactNode = findChildContainingExactPosition(sourceFile, position) if (node) { // #region Fake emmet if ( @@ -327,4 +337,42 @@ const arrayMoveItemToFrom = (array: T[], originalItem: ArrayPredicate, ite return originalItemIndex } -const patchText = (input: string, start: number, end: number, newText: string) => input.slice(0, start) + newText + input.slice(end) +const patchBuiltinMethods = (c: GetConfig, languageService: ts.LanguageService, isCheckedFile: boolean) => { + let addFileExtensions: string[] | undefined + const getAddFileExtensions = () => { + const typeChecker = languageService.getProgram()!.getTypeChecker()! + const ambientModules = typeChecker.getAmbientModules() + /** file extensions from ambient modules declarations e.g. *.css */ + const fileExtensions = compact( + ambientModules.map(module => { + const name = module.name.slice(1, -1) + if (!name.startsWith('*.') || name.includes('/')) return + return name.slice(1) + }), + ) + if (!isCheckedFile) fileExtensions.push(...c('additionalIncludeExtensions').map(ext => (ext === '*' ? '' : ext))) + return fileExtensions + } + // Its known that fuzzy completion don't work within import completions + // TODO! when file name without with half-ending is typed it doesn't these completions! (seems ts bug, but probably can be fixed here) + // e.g. /styles.css import './styles.c|' - no completions + const oldGetSupportedExtensions = tsFull.getSupportedExtensions + //@ts-expect-error monkey patch + tsFull.getSupportedExtensions = (options, extraFileExtensions) => { + addFileExtensions ??= getAddFileExtensions() + // though I extensions could be just inlined as is + return oldGetSupportedExtensions( + options, + extraFileExtensions?.length + ? extraFileExtensions + : addFileExtensions.map(ext => ({ + extension: ext, + isMixedContent: true, + scriptKind: ts.ScriptKind.Deferred, + })), + ) + } + return () => { + tsFull.getSupportedExtensions = oldGetSupportedExtensions + } +} diff --git a/typescript/src/decorateProxy.ts b/typescript/src/decorateProxy.ts index bb3b6586..7b301926 100644 --- a/typescript/src/decorateProxy.ts +++ b/typescript/src/decorateProxy.ts @@ -64,7 +64,8 @@ export const decorateLanguageService = ( const scriptKind = languageServiceHost.getScriptKind!(fileName) // have no idea in which cases its possible, but we can't work without it if (!scriptSnapshot) return - const result = getCompletionsAtPosition(fileName, position, options, c, languageService, scriptSnapshot, formatOptions, { scriptKind }) + const compilerOptions = languageServiceHost.getCompilationSettings() + const result = getCompletionsAtPosition(fileName, position, options, c, languageService, scriptSnapshot, formatOptions, { scriptKind, compilerOptions }) if (!result) return prevCompletionsMap = result.prevCompletionsMap prevCompletionsAdittionalData = result.prevCompletionsAdittionalData From a102a76786a0292aa8a9a46fb6cc43595e8871a7 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 25 Jan 2023 00:21:12 +0300 Subject: [PATCH 2/2] fix ci --- typescript/test/completions.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/test/completions.spec.ts b/typescript/test/completions.spec.ts index b1c0330c..aa0e984b 100644 --- a/typescript/test/completions.spec.ts +++ b/typescript/test/completions.spec.ts @@ -84,7 +84,7 @@ const getCompletionsAtPosition = (pos: number, { fileName = entrypoint, shouldHa languageService, ts.ScriptSnapshot.fromString(files[entrypoint]), undefined, - { scriptKind: ts.ScriptKind.TSX }, + { scriptKind: ts.ScriptKind.TSX, compilerOptions: {} }, ) if (shouldHave) expect(result).not.toBeUndefined() if (!result) return