diff --git a/package.json b/package.json index 7270c0f7d..b822ce1b4 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "lint": "prettier --check . && eslint \"packages/**/*.{ts,js}\"" }, "dependencies": { - "typescript": "^4.6.2" + "typescript": "^4.7.2" }, "devDependencies": { "@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.2.0", diff --git a/packages/language-server/src/plugins/typescript/ComponentInfoProvider.ts b/packages/language-server/src/plugins/typescript/ComponentInfoProvider.ts index f911e427e..a114f645a 100644 --- a/packages/language-server/src/plugins/typescript/ComponentInfoProvider.ts +++ b/packages/language-server/src/plugins/typescript/ComponentInfoProvider.ts @@ -1,6 +1,6 @@ import { ComponentEvents } from 'svelte2tsx'; import ts from 'typescript'; -import { isNotNullOrUndefined } from '../../utils'; +import { flatten, isNotNullOrUndefined } from '../../utils'; import { findContainingNode } from './features/utils'; export type ComponentPartInfo = ReturnType; @@ -8,6 +8,7 @@ export type ComponentPartInfo = ReturnType; export interface ComponentInfoProvider { getEvents(): ComponentPartInfo; getSlotLets(slot?: string): ComponentPartInfo; + getProps(propName: string): ts.CompletionEntry[]; } export class JsOrTsComponentInfoProvider implements ComponentInfoProvider { @@ -44,6 +45,61 @@ export class JsOrTsComponentInfoProvider implements ComponentInfoProvider { return this.mapPropertiesOfType(slotLetsType); } + getProps(propName: string): ts.CompletionEntry[] { + const props = this.getType('$$prop_def'); + if (!props) { + return []; + } + + const prop = props.getProperties().find((prop) => prop.name === propName); + if (!prop?.valueDeclaration) { + return []; + } + + const propDef = this.typeChecker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration); + + if (!propDef.isUnion()) { + return []; + } + + const types = flatten(propDef.types.map((type) => this.getStringLiteralTypes(type))); + + // adopted from https://github.com/microsoft/TypeScript/blob/0921eac6dc9eba0be6319dff10b85d60c90155ea/src/services/stringCompletions.ts#L61 + return types.map((v) => ({ + name: v.value, + kindModifiers: ts.ScriptElementKindModifier.none, + kind: ts.ScriptElementKind.string, + sortText: /**LocationPriority: */ '11' + })); + } + + /** + * adopted from https://github.com/microsoft/TypeScript/blob/0921eac6dc9eba0be6319dff10b85d60c90155ea/src/services/stringCompletions.ts#L310 + */ + private getStringLiteralTypes( + type: ts.Type | undefined, + uniques = new Set() + ): ts.StringLiteralType[] { + if (!type) { + return []; + } + + type = type.isTypeParameter() ? type.getConstraint() || type : type; + + if (type.isUnion()) { + return flatten(type.types.map((t) => this.getStringLiteralTypes(t, uniques))); + } + + if ( + type.isStringLiteral() && + !(type.flags & ts.TypeFlags.EnumLiteral) && + !uniques.has(type.value) + ) { + return [type]; + } + return []; + } + private getType(classProperty: string) { const symbol = this.classType.getProperty(classProperty); if (!symbol?.valueDeclaration) { diff --git a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts index f228a060b..386936026 100644 --- a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts +++ b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts @@ -50,7 +50,7 @@ import { } from '../interfaces'; import { CodeActionsProviderImpl } from './features/CodeActionsProvider'; import { - CompletionEntryWithIdentifer, + CompletionEntryWithIdentifier, CompletionsProviderImpl } from './features/CompletionProvider'; import { DiagnosticsProviderImpl } from './features/DiagnosticsProvider'; @@ -91,7 +91,7 @@ export class TypeScriptPlugin ImplementationProvider, TypeDefinitionProvider, OnWatchFileChanges, - CompletionsProvider, + CompletionsProvider, UpdateTsOrJsFile { __name = 'ts'; @@ -273,7 +273,7 @@ export class TypeScriptPlugin position: Position, completionContext?: CompletionContext, cancellationToken?: CancellationToken - ): Promise | null> { + ): Promise | null> { if (!this.featureEnabled('completions')) { return null; } @@ -303,9 +303,9 @@ export class TypeScriptPlugin async resolveCompletion( document: Document, - completionItem: AppCompletionItem, + completionItem: AppCompletionItem, cancellationToken?: CancellationToken - ): Promise> { + ): Promise> { return this.completionProvider.resolveCompletion( document, completionItem, diff --git a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts index 2d0fda569..5bbc6124e 100644 --- a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts @@ -145,9 +145,9 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { mapRangeToOriginal(fragment, convertRange(fragment, edit.span)) ); - return TextEdit.replace( - range, - this.fixIndentationOfImports(edit.newText, range, document) + return this.fixIndentationOfImports( + TextEdit.replace(range, edit.newText), + document ); }) ); @@ -165,11 +165,12 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { ]; } - private fixIndentationOfImports(edit: string, range: Range, document: Document): string { - // "Organize Imports" will have edits that delete all imports by return empty edits - // and one edit which contains all the organized imports. Fix indentation + private fixIndentationOfImports(edit: TextEdit, document: Document): TextEdit { + // "Organize Imports" will have edits that delete a group of imports by return empty edits + // and one edit which contains all the organized imports of the group. Fix indentation // of that one by prepending all lines with the indentation of the first line. - if (!edit || range.start.character === 0) { + const { newText, range } = edit; + if (!newText || range.start.character === 0) { return edit; } @@ -178,7 +179,21 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { if (leadingChars.trim() !== '') { return edit; } - return modifyLines(edit, (line, idx) => (idx === 0 || !line ? line : leadingChars + line)); + + const fixedNewText = modifyLines(edit.newText, (line, idx) => + idx === 0 || !line ? line : leadingChars + line + ); + + if (range.end.character > 0) { + const endLine = getLineAtPosition(range.start, document.getText()); + const isIndent = !endLine.substring(0, range.start.character).trim(); + + if (isIndent && endLine.trim()) { + range.end.character = 0; + } + } + + return TextEdit.replace(range, fixedNewText); } private checkRemoveImportCodeActionRange( diff --git a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts index 33818635f..3c67ed2fc 100644 --- a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts @@ -25,7 +25,7 @@ import { import { LSConfigManager } from '../../../ls-config'; import { flatten, getRegExpMatches, isNotNullOrUndefined, pathToUrl } from '../../../utils'; import { AppCompletionItem, AppCompletionList, CompletionsProvider } from '../../interfaces'; -import { ComponentPartInfo } from '../ComponentInfoProvider'; +import { ComponentInfoProvider, ComponentPartInfo } from '../ComponentInfoProvider'; import { SvelteDocumentSnapshot, SvelteSnapshotFragment } from '../DocumentSnapshot'; import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; import { getMarkdownDocumentation } from '../previewer'; @@ -37,9 +37,9 @@ import { scriptElementKindToCompletionItemKind } from '../utils'; import { getJsDocTemplateCompletion } from './getJsDocTemplateCompletion'; -import { getComponentAtPosition, isPartOfImportStatement } from './utils'; +import { findContainingNode, getComponentAtPosition, isPartOfImportStatement } from './utils'; -export interface CompletionEntryWithIdentifer extends ts.CompletionEntry, TextDocumentIdentifier { +export interface CompletionEntryWithIdentifier extends ts.CompletionEntry, TextDocumentIdentifier { position: Position; } @@ -48,10 +48,10 @@ type validTriggerCharacter = '.' | '"' | "'" | '`' | '/' | '@' | '<' | '#'; type LastCompletion = { key: string; position: Position; - completionList: AppCompletionList | null; + completionList: AppCompletionList | null; }; -export class CompletionsProviderImpl implements CompletionsProvider { +export class CompletionsProviderImpl implements CompletionsProvider { constructor( private readonly lsAndTsDocResolver: LSAndTSDocResolver, private readonly configManager: LSConfigManager @@ -79,7 +79,7 @@ export class CompletionsProviderImpl implements CompletionsProvider | null> { + ): Promise | null> { if (isInTag(position, document.styleInfo)) { return null; } @@ -163,11 +163,10 @@ export class CompletionsProviderImpl implements CompletionsProvider> { - const componentInfo = getComponentAtPosition(lang, doc, tsDoc, originalPosition); - if (!componentInfo) { + ): Array> { + if (componentInfo === null) { return []; } @@ -285,7 +291,7 @@ export class CompletionsProviderImpl implements CompletionsProvider - ): AppCompletionItem | null { + ): AppCompletionItem | null { const completionLabelAndInsert = this.getCompletionLabelAndInsert(fragment, comp); if (!completionLabelAndInsert) { return null; @@ -435,9 +441,9 @@ export class CompletionsProviderImpl implements CompletionsProvider, + completionItem: AppCompletionItem, cancellationToken?: CancellationToken - ): Promise> { + ): Promise> { const { data: comp } = completionItem; const { tsDoc, lang, userPreferences } = await this.lsAndTsDocResolver.getLSAndTSDoc( document @@ -635,6 +641,49 @@ export class CompletionsProviderImpl implements CompletionsProvider ({ + ...item, + replacementSpan + })); + } } const beginOfDocumentRange = Range.create(Position.create(0, 0), Position.create(0, 0)); diff --git a/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts b/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts index 4833ba3ff..b3c3b1e28 100644 --- a/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts @@ -1,7 +1,7 @@ import ts from 'typescript'; import { Location, Position, ReferenceContext } from 'vscode-languageserver'; import { Document } from '../../../lib/documents'; -import { pathToUrl } from '../../../utils'; +import { flatten, pathToUrl } from '../../../utils'; import { FindReferencesProvider } from '../../interfaces'; import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; import { convertToLocationRange, hasNonZeroRange } from '../utils'; @@ -18,7 +18,7 @@ export class FindReferencesProviderImpl implements FindReferencesProvider { const { lang, tsDoc } = await this.getLSAndTSDoc(document); const fragment = tsDoc.getFragment(); - const references = lang.getReferencesAtPosition( + const references = lang.findReferences( tsDoc.filePath, fragment.offsetAt(fragment.getGeneratedPosition(position)) ); @@ -30,7 +30,7 @@ export class FindReferencesProviderImpl implements FindReferencesProvider { docs.set(tsDoc.filePath, { fragment, snapshot: tsDoc }); const locations = await Promise.all( - references + flatten(references.map((ref) => ref.references)) .filter((ref) => context.includeDeclaration || !ref.isDefinition) .filter(notInGeneratedCode(tsDoc.getFullText())) .map(async (ref) => { diff --git a/packages/language-server/test/plugins/typescript/features/CodeActionsProvider.test.ts b/packages/language-server/test/plugins/typescript/features/CodeActionsProvider.test.ts index 4ef58d7d3..e78f48bed 100644 --- a/packages/language-server/test/plugins/typescript/features/CodeActionsProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/CodeActionsProvider.test.ts @@ -863,13 +863,10 @@ function test(useNewTransformation: boolean) { { edits: [ { - newText: - '// @ts-ignore\n' + - " import { } from './somepng.png';\n" + - " import { } from './t.png';\n", + newText: "import { } from './t.png';\n", range: { end: { - character: 4, + character: 0, line: 2 }, start: { @@ -879,7 +876,7 @@ function test(useNewTransformation: boolean) { } }, { - newText: '', + newText: "import { } from './somepng.png';\n", range: { end: { character: 0, @@ -887,7 +884,7 @@ function test(useNewTransformation: boolean) { }, start: { character: 4, - line: 2 + line: 3 } } } diff --git a/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts b/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts index 77c4e92d3..09c55fce8 100644 --- a/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts @@ -17,7 +17,7 @@ import { } from 'vscode-languageserver'; import { CompletionsProviderImpl, - CompletionEntryWithIdentifer + CompletionEntryWithIdentifier } from '../../../../src/plugins/typescript/features/CompletionProvider'; import { LSAndTSDocResolver } from '../../../../src/plugins/typescript/LSAndTSDocResolver'; import { sortBy } from 'lodash'; @@ -466,6 +466,7 @@ function test(useNewTransformation: boolean) { isSnippet: undefined, kind: 'method', kindModifiers: '', + labelDetails: undefined, name: 'b', position: { character: 49, @@ -476,7 +477,7 @@ function test(useNewTransformation: boolean) { source: undefined, sourceDisplay: undefined, uri: fileNameToAbsoluteUri(filename) - } as CompletionEntryWithIdentifer); + } as CompletionEntryWithIdentifier); }); it('resolve completion and provide documentation', async () => { diff --git a/packages/language-server/test/plugins/typescript/testfiles/code-actions/organize-imports-leading-comment.svelte b/packages/language-server/test/plugins/typescript/testfiles/code-actions/organize-imports-leading-comment.svelte index 27ae0f3e8..7dcabda8c 100644 --- a/packages/language-server/test/plugins/typescript/testfiles/code-actions/organize-imports-leading-comment.svelte +++ b/packages/language-server/test/plugins/typescript/testfiles/code-actions/organize-imports-leading-comment.svelte @@ -1,5 +1,5 @@ diff --git a/packages/svelte2tsx/package.json b/packages/svelte2tsx/package.json index 2bf94c744..0118a084a 100644 --- a/packages/svelte2tsx/package.json +++ b/packages/svelte2tsx/package.json @@ -37,7 +37,7 @@ "svelte": "~3.48.0", "tiny-glob": "^0.2.6", "tslib": "^1.10.0", - "typescript": "^4.6.2" + "typescript": "^4.7.2" }, "peerDependencies": { "svelte": "^3.24", diff --git a/yarn.lock b/yarn.lock index 307cb5616..c2cdac512 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2634,10 +2634,10 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -typescript@*, typescript@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.2.tgz#fe12d2727b708f4eef40f51598b3398baa9611d4" - integrity sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg== +typescript@*, typescript@^4.7.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.2.tgz#1f9aa2ceb9af87cca227813b4310fff0b51593c4" + integrity sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A== unist-util-stringify-position@^2.0.0: version "2.0.3"