diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 7933acf96..eafda89b6 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -64,8 +64,8 @@ "svelte2tsx": "workspace:~", "typescript": "^5.8.2", "typescript-auto-import-cache": "^0.3.5", - "vscode-css-languageservice": "~6.3.2", - "vscode-html-languageservice": "~5.3.2", + "vscode-css-languageservice": "~6.3.5", + "vscode-html-languageservice": "~5.4.0", "vscode-languageserver": "9.0.1", "vscode-languageserver-protocol": "3.17.5", "vscode-languageserver-types": "3.17.5", diff --git a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts index 581a46d8d..244ae4e12 100644 --- a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts @@ -1,3 +1,4 @@ +import { internalHelpers } from 'svelte2tsx'; import ts from 'typescript'; import { CancellationToken, @@ -24,6 +25,7 @@ import { } from '../../../lib/documents'; import { LSConfigManager } from '../../../ls-config'; import { + createGetCanonicalFileName, flatten, getIndent, isNotNullOrUndefined, @@ -37,6 +39,7 @@ import { import { CodeActionsProvider } from '../../interfaces'; import { DocumentSnapshot, SvelteDocumentSnapshot } from '../DocumentSnapshot'; import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; +import { LanguageServiceContainer } from '../service'; import { changeSvelteComponentName, convertRange, @@ -44,6 +47,7 @@ import { toGeneratedSvelteComponentName } from '../utils'; import { CompletionsProviderImpl } from './CompletionProvider'; +import { DiagnosticCode } from './DiagnosticsProvider'; import { findClosestContainingNode, FormatCodeBasis, @@ -53,15 +57,12 @@ import { isTextSpanInGeneratedCode, SnapshotMap } from './utils'; -import { DiagnosticCode } from './DiagnosticsProvider'; -import { createGetCanonicalFileName } from '../../../utils'; -import { LanguageServiceContainer } from '../service'; -import { internalHelpers } from 'svelte2tsx'; /** * TODO change this to protocol constant if it's part of the protocol */ export const SORT_IMPORT_CODE_ACTION_KIND = 'source.sortImports'; +export const ADD_MISSING_IMPORTS_CODE_ACTION_KIND = 'source.addMissingImports'; interface RefactorArgs { type: 'refactor'; @@ -121,6 +122,10 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { ); } + if (context.only?.[0] === ADD_MISSING_IMPORTS_CODE_ACTION_KIND) { + return await this.addMissingImports(document, cancellationToken); + } + // for source action command (all source.xxx) // vscode would show different source code action kinds to choose from if (context.only?.[0] === CodeActionKind.Source) { @@ -130,7 +135,8 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { document, cancellationToken, /**skipDestructiveCodeActions */ true - )) + )), + ...(await this.addMissingImports(document, cancellationToken)) ]; } @@ -377,7 +383,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { if (editForThisFile?.edits.length) { const [first] = editForThisFile.edits; first.newText = - getNewScriptStartTag(this.configManager.getConfig()) + + getNewScriptStartTag(this.configManager.getConfig(), formatCodeBasis.newLine) + formatCodeBasis.baseIndent + first.newText.trimStart(); @@ -1553,4 +1559,48 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { private async getLSAndTSDoc(document: Document) { return this.lsAndTsDocResolver.getLSAndTSDoc(document); } + + private async addMissingImports( + document: Document, + cancellationToken?: CancellationToken + ): Promise { + // Re-introduce LS/TSDoc resolution and diagnostic check + const { lang, tsDoc } = await this.getLSAndTSDoc(document); + if (cancellationToken?.isCancellationRequested) { + return []; + } + + // Check if there are any relevant "cannot find name" diagnostics + const diagnostics = lang.getSemanticDiagnostics(tsDoc.filePath); + const hasMissingImports = diagnostics.some( + (diag) => + (diag.code === DiagnosticCode.CANNOT_FIND_NAME || + diag.code === DiagnosticCode.CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y) && + // Ensure the diagnostic is not in generated code + !isTextSpanInGeneratedCode(tsDoc.getFullText(), { + start: diag.start ?? 0, + length: diag.length ?? 0 + }) + ); + + // Only return the action if there are potential imports to add + if (!hasMissingImports) { + return []; + } + + // If imports might be needed, create the deferred action + const codeAction = CodeAction.create( + FIX_IMPORT_FIX_DESCRIPTION, + ADD_MISSING_IMPORTS_CODE_ACTION_KIND + ); + + const data: QuickFixAllResolveInfo = { + uri: document.uri, + fixName: FIX_IMPORT_FIX_NAME, + fixId: FIX_IMPORT_FIX_ID + }; + codeAction.data = data; + + return [codeAction]; + } } diff --git a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts index 48b6db53f..bc1764d87 100644 --- a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts @@ -595,7 +595,8 @@ export class CompletionsProviderImpl implements CompletionsProvider ({ label: name, kind: CompletionItemKind.Property, - textEdit: TextEdit.replace(this.cloneRange(replacementRange), name) + textEdit: TextEdit.replace(this.cloneRange(replacementRange), name), + commitCharacters: [] })); } @@ -1131,9 +1132,17 @@ export class CompletionsProviderImpl implements CompletionsProvider${newLine}` + `${getNewScriptStartTag(config, newLine)}${newText}${newLine}` ); } diff --git a/packages/language-server/src/plugins/typescript/features/utils.ts b/packages/language-server/src/plugins/typescript/features/utils.ts index a8b0229e3..954ef03bc 100644 --- a/packages/language-server/src/plugins/typescript/features/utils.ts +++ b/packages/language-server/src/plugins/typescript/features/utils.ts @@ -429,10 +429,10 @@ export function findChildOfKind(node: ts.Node, kind: ts.SyntaxKind): ts.Node | u } } -export function getNewScriptStartTag(lsConfig: Readonly) { +export function getNewScriptStartTag(lsConfig: Readonly, newLine: string) { const lang = lsConfig.svelte.defaultScriptLanguage; const scriptLang = lang === 'none' ? '' : ` lang="${lang}"`; - return `${ts.sys.newLine}`; + return `${newLine}`; } export function checkRangeMappingWithGeneratedSemi( diff --git a/packages/language-server/src/plugins/typescript/module-loader.ts b/packages/language-server/src/plugins/typescript/module-loader.ts index 6c96ec172..a681c658a 100644 --- a/packages/language-server/src/plugins/typescript/module-loader.ts +++ b/packages/language-server/src/plugins/typescript/module-loader.ts @@ -164,8 +164,13 @@ export function createSvelteModuleLoader( >(); const impliedNodeFormatResolver = new ImpliedNodeFormatResolver(tsSystem); - const failedPathToContainingFile = new FileMap(); - const failedLocationInvalidated = new FileSet(); + const resolutionWithFailedLookup = new Set< + ts.ResolvedModuleWithFailedLookupLocations & { + files?: Set; + } + >(); + const failedLocationInvalidated = new FileSet(tsSystem.useCaseSensitiveFileNames); + const pendingFailedLocationCheck = new FileSet(tsSystem.useCaseSensitiveFileNames); return { svelteFileExists: svelteSys.svelteFileExists, @@ -179,16 +184,7 @@ export function createSvelteModuleLoader( deleteUnresolvedResolutionsFromCache: (path: string) => { svelteSys.deleteFromCache(path); moduleCache.deleteUnresolvedResolutionsFromCache(path); - - const previousTriedButFailed = failedPathToContainingFile.get(path); - - if (!previousTriedButFailed) { - return; - } - - for (const containingFile of previousTriedButFailed) { - failedLocationInvalidated.add(containingFile); - } + pendingFailedLocationCheck.add(path); tsModuleCache.clear(); typeReferenceCache.clear(); @@ -197,7 +193,8 @@ export function createSvelteModuleLoader( resolveTypeReferenceDirectiveReferences, mightHaveInvalidatedResolutions, clearPendingInvalidations, - getModuleResolutionCache: () => tsModuleCache + getModuleResolutionCache: () => tsModuleCache, + invalidateFailedLocationResolution }; function resolveModuleNames( @@ -222,11 +219,7 @@ export function createSvelteModuleLoader( options ); - resolvedModule?.failedLookupLocations?.forEach((failedLocation) => { - const failedPaths = failedPathToContainingFile.get(failedLocation) ?? new FileSet(); - failedPaths.add(containingFile); - failedPathToContainingFile.set(failedLocation, failedPaths); - }); + cacheResolutionWithFailedLookup(resolvedModule, containingFile); moduleCache.set(moduleName, containingFile, resolvedModule?.resolvedModule); return resolvedModule?.resolvedModule; @@ -333,5 +326,46 @@ export function createSvelteModuleLoader( function clearPendingInvalidations() { moduleCache.clearPendingInvalidations(); failedLocationInvalidated.clear(); + pendingFailedLocationCheck.clear(); + } + + function cacheResolutionWithFailedLookup( + resolvedModule: ts.ResolvedModuleWithFailedLookupLocations & { + files?: Set; + }, + containingFile: string + ) { + if (!resolvedModule.failedLookupLocations?.length) { + return; + } + + // The resolvedModule object will be reused in different files. A bit hacky, but TypeScript also does this. + // https://github.com/microsoft/TypeScript/blob/11e79327598db412a161616849041487673fadab/src/compiler/resolutionCache.ts#L1103 + resolvedModule.files ??= new Set(); + resolvedModule.files.add(containingFile); + resolutionWithFailedLookup.add(resolvedModule); + } + + function invalidateFailedLocationResolution() { + resolutionWithFailedLookup.forEach((resolvedModule) => { + if ( + !resolvedModule.resolvedModule || + !resolvedModule.files || + !resolvedModule.failedLookupLocations + ) { + return; + } + for (const location of resolvedModule.failedLookupLocations) { + if (pendingFailedLocationCheck.has(location)) { + moduleCache.delete(resolvedModule.resolvedModule.resolvedFileName); + resolvedModule.files?.forEach((file) => { + failedLocationInvalidated.add(file); + }); + break; + } + } + }); + + pendingFailedLocationCheck.clear(); } } diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index a45f04ecd..7ceec495e 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -89,10 +89,10 @@ declare module 'typescript' { resolutionDiagnostics?: ts.Diagnostic[]; /** * @internal - * Used to issue a diagnostic if typings for a non-relative import couldn't be found - * while respecting package.json `exports`, but were found when disabling `exports`. + * Used to issue a better diagnostic when an unresolvable module may + * have been resolvable under different module resolution settings. */ - node10Result?: string; + alternateResult?: string; } } @@ -527,10 +527,11 @@ async function createLanguageService( function invalidateModuleCache(filePaths: string[]) { for (const filePath of filePaths) { - svelteModuleLoader.deleteFromModuleCache(filePath); - svelteModuleLoader.deleteUnresolvedResolutionsFromCache(filePath); + const normalizedPath = normalizePath(filePath); + svelteModuleLoader.deleteFromModuleCache(normalizedPath); + svelteModuleLoader.deleteUnresolvedResolutionsFromCache(normalizedPath); - scheduleUpdate(filePath); + scheduleUpdate(normalizedPath); } } @@ -974,6 +975,7 @@ async function createLanguageService( return; } + svelteModuleLoader.invalidateFailedLocationResolution(); const oldProgram = project?.program; let program: ts.Program | undefined; try { @@ -981,15 +983,14 @@ async function createLanguageService( } finally { // mark as clean even if the update fails, at least we can still try again next time there is a change dirty = false; + compilerHost = undefined; + svelteModuleLoader.clearPendingInvalidations(); } - svelteModuleLoader.clearPendingInvalidations(); if (project) { project.program = program; } - compilerHost = undefined; - if (!skipSvelteInputCheck) { const svelteConfigDiagnostics = checkSvelteInput(program, projectConfig); const codes = svelteConfigDiagnostics.map((d) => d.code); diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index f1aa52ca0..af5f92a60 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -45,7 +45,10 @@ import { debounceThrottle, isNotNullOrUndefined, normalizeUri, urlToPath } from import { FallbackWatcher } from './lib/FallbackWatcher'; import { configLoader } from './lib/documents/configLoader'; import { setIsTrusted } from './importPackage'; -import { SORT_IMPORT_CODE_ACTION_KIND } from './plugins/typescript/features/CodeActionsProvider'; +import { + SORT_IMPORT_CODE_ACTION_KIND, + ADD_MISSING_IMPORTS_CODE_ACTION_KIND +} from './plugins/typescript/features/CodeActionsProvider'; import { createLanguageServices } from './plugins/css/service'; import { FileSystemProvider } from './plugins/css/FileSystemProvider'; @@ -270,6 +273,7 @@ export function startServer(options?: LSOptions) { CodeActionKind.QuickFix, CodeActionKind.SourceOrganizeImports, SORT_IMPORT_CODE_ACTION_KIND, + ADD_MISSING_IMPORTS_CODE_ACTION_KIND, ...(clientSupportApplyEditCommand ? [CodeActionKind.Refactor] : []) ].filter( clientSupportedCodeActionKinds && diff --git a/packages/language-server/test/plugins/css/CSSPlugin.test.ts b/packages/language-server/test/plugins/css/CSSPlugin.test.ts index 5e9c4bf32..9a82c53ff 100644 --- a/packages/language-server/test/plugins/css/CSSPlugin.test.ts +++ b/packages/language-server/test/plugins/css/CSSPlugin.test.ts @@ -72,8 +72,8 @@ describe('CSS Plugin', () => { kind: 'markdown', value: "Specifies the height of the content area, padding area or border area \\(depending on 'box\\-sizing'\\) of certain boxes\\.\n\n" + - '(Edge 12, Firefox 1, Safari 1, Chrome 1, IE 4, Opera 7)\n\n' + - 'Syntax: auto | <length> | <percentage> | min\\-content | max\\-content | fit\\-content | fit\\-content\\(<length\\-percentage>\\)\n\n' + + '![Baseline icon](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTAiIHZpZXdCb3g9IjAgMCA1NDAgMzAwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxzdHlsZT4KICAgIC5ncmVlbi1zaGFwZSB7CiAgICAgIGZpbGw6ICNDNEVFRDA7IC8qIExpZ2h0IG1vZGUgKi8KICAgIH0KCiAgICBAbWVkaWEgKHByZWZlcnMtY29sb3Itc2NoZW1lOiBkYXJrKSB7CiAgICAgIC5ncmVlbi1zaGFwZSB7CiAgICAgICAgZmlsbDogIzEyNTIyNTsgLyogRGFyayBtb2RlICovCiAgICAgIH0KICAgIH0KICA8L3N0eWxlPgogIDxwYXRoIGQ9Ik00MjAgMzBMMzkwIDYwTDQ4MCAxNTBMMzkwIDI0MEwzMzAgMTgwTDMwMCAyMTBMMzkwIDMwMEw1NDAgMTUwTDQyMCAzMFoiIGNsYXNzPSJncmVlbi1zaGFwZSIvPgogIDxwYXRoIGQ9Ik0xNTAgMEwzMCAxMjBMNjAgMTUwTDE1MCA2MEwyMTAgMTIwTDI0MCA5MEwxNTAgMFoiIGNsYXNzPSJncmVlbi1zaGFwZSIvPgogIDxwYXRoIGQ9Ik0zOTAgMEw0MjAgMzBMMTUwIDMwMEwwIDE1MEwzMCAxMjBMMTUwIDI0MEwzOTAgMFoiIGZpbGw9IiMxRUE0NDYiLz4KPC9zdmc+) _Widely available across major browsers (Baseline since 2015)_\n\n' + + 'Syntax: auto | <length\\-percentage \\[0,∞\\]> | min\\-content | max\\-content | fit\\-content | fit\\-content\\(<length\\-percentage \\[0,∞\\]>\\) | <calc\\-size\\(\\)> | <anchor\\-size\\(\\)>\n\n' + '[MDN Reference](https://developer.mozilla.org/docs/Web/CSS/height)' }, range: Range.create(0, 12, 0, 24) @@ -105,7 +105,8 @@ describe('CSS Plugin', () => { documentation: { kind: 'markdown', value: - 'Defines character set of the document\\.\n\n(Edge 12, Firefox 1, Safari 4, Chrome 2, IE 5, Opera 9)\n\n' + + 'Defines character set of the document\\.\n\n' + + '![Baseline icon](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTAiIHZpZXdCb3g9IjAgMCA1NDAgMzAwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxzdHlsZT4KICAgIC5ncmVlbi1zaGFwZSB7CiAgICAgIGZpbGw6ICNDNEVFRDA7IC8qIExpZ2h0IG1vZGUgKi8KICAgIH0KCiAgICBAbWVkaWEgKHByZWZlcnMtY29sb3Itc2NoZW1lOiBkYXJrKSB7CiAgICAgIC5ncmVlbi1zaGFwZSB7CiAgICAgICAgZmlsbDogIzEyNTIyNTsgLyogRGFyayBtb2RlICovCiAgICAgIH0KICAgIH0KICA8L3N0eWxlPgogIDxwYXRoIGQ9Ik00MjAgMzBMMzkwIDYwTDQ4MCAxNTBMMzkwIDI0MEwzMzAgMTgwTDMwMCAyMTBMMzkwIDMwMEw1NDAgMTUwTDQyMCAzMFoiIGNsYXNzPSJncmVlbi1zaGFwZSIvPgogIDxwYXRoIGQ9Ik0xNTAgMEwzMCAxMjBMNjAgMTUwTDE1MCA2MEwyMTAgMTIwTDI0MCA5MEwxNTAgMFoiIGNsYXNzPSJncmVlbi1zaGFwZSIvPgogIDxwYXRoIGQ9Ik0zOTAgMEw0MjAgMzBMMTUwIDMwMEwwIDE1MEwzMCAxMjBMMTUwIDI0MEwzOTAgMFoiIGZpbGw9IiMxRUE0NDYiLz4KPC9zdmc+) _Widely available across major browsers (Baseline since 2015)_\n\n' + '[MDN Reference](https://developer.mozilla.org/docs/Web/CSS/@charset)' }, textEdit: TextEdit.insert(Position.create(0, 7), '@charset'), @@ -344,6 +345,38 @@ describe('CSS Plugin', () => { }, newText: 'hwb(240 0% -25400%)' } + }, + { + label: 'lab(3880.51% 6388.69 -8701.22)', + textEdit: { + newText: 'lab(3880.51% 6388.69 -8701.22)', + range: { + end: { + character: 21, + line: 0 + }, + start: { + character: 17, + line: 0 + } + } + } + }, + { + label: 'lch(3880.51% 10794.75 306.29)', + textEdit: { + newText: 'lch(3880.51% 10794.75 306.29)', + range: { + end: { + character: 21, + line: 0 + }, + start: { + character: 17, + line: 0 + } + } + } } ]); }); diff --git a/packages/language-server/test/plugins/html/HTMLPlugin.test.ts b/packages/language-server/test/plugins/html/HTMLPlugin.test.ts index 4a45b6310..eff038a11 100644 --- a/packages/language-server/test/plugins/html/HTMLPlugin.test.ts +++ b/packages/language-server/test/plugins/html/HTMLPlugin.test.ts @@ -35,7 +35,10 @@ describe('HTML Plugin', () => { assert.deepStrictEqual(plugin.doHover(document, Position.create(0, 2)), { contents: { kind: 'markdown', - value: 'The h1 element represents a section heading.\n\n[MDN Reference](https://developer.mozilla.org/docs/Web/HTML/Element/Heading_Elements)' + value: + 'The h1 element represents a section heading.\n\n' + + '![Baseline icon](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTAiIHZpZXdCb3g9IjAgMCA1NDAgMzAwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxzdHlsZT4KICAgIC5ncmVlbi1zaGFwZSB7CiAgICAgIGZpbGw6ICNDNEVFRDA7IC8qIExpZ2h0IG1vZGUgKi8KICAgIH0KCiAgICBAbWVkaWEgKHByZWZlcnMtY29sb3Itc2NoZW1lOiBkYXJrKSB7CiAgICAgIC5ncmVlbi1zaGFwZSB7CiAgICAgICAgZmlsbDogIzEyNTIyNTsgLyogRGFyayBtb2RlICovCiAgICAgIH0KICAgIH0KICA8L3N0eWxlPgogIDxwYXRoIGQ9Ik00MjAgMzBMMzkwIDYwTDQ4MCAxNTBMMzkwIDI0MEwzMzAgMTgwTDMwMCAyMTBMMzkwIDMwMEw1NDAgMTUwTDQyMCAzMFoiIGNsYXNzPSJncmVlbi1zaGFwZSIvPgogIDxwYXRoIGQ9Ik0xNTAgMEwzMCAxMjBMNjAgMTUwTDE1MCA2MEwyMTAgMTIwTDI0MCA5MEwxNTAgMFoiIGNsYXNzPSJncmVlbi1zaGFwZSIvPgogIDxwYXRoIGQ9Ik0zOTAgMEw0MjAgMzBMMTUwIDMwMEwwIDE1MEwzMCAxMjBMMTUwIDI0MEwzOTAgMFoiIGZpbGw9IiMxRUE0NDYiLz4KPC9zdmc+) _Widely available across major browsers (Baseline since 2015)_\n\n' + + '[MDN Reference](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/Heading_Elements)' }, range: Range.create(0, 1, 0, 3) 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 e622ae1c2..a07288542 100644 --- a/packages/language-server/test/plugins/typescript/features/CodeActionsProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/CodeActionsProvider.test.ts @@ -1,5 +1,7 @@ import * as assert from 'assert'; import * as path from 'path'; +import { VERSION } from 'svelte/compiler'; +import { internalHelpers } from 'svelte2tsx'; import ts from 'typescript'; import { CancellationTokenSource, @@ -12,17 +14,16 @@ import { import { Document, DocumentManager } from '../../../../src/lib/documents'; import { LSConfigManager } from '../../../../src/ls-config'; import { + ADD_MISSING_IMPORTS_CODE_ACTION_KIND, CodeActionsProviderImpl, SORT_IMPORT_CODE_ACTION_KIND } from '../../../../src/plugins/typescript/features/CodeActionsProvider'; import { CompletionsProviderImpl } from '../../../../src/plugins/typescript/features/CompletionProvider'; +import { DiagnosticCode } from '../../../../src/plugins/typescript/features/DiagnosticsProvider'; import { LSAndTSDocResolver } from '../../../../src/plugins/typescript/LSAndTSDocResolver'; import { __resetCache } from '../../../../src/plugins/typescript/service'; import { pathToUrl } from '../../../../src/utils'; import { recursiveServiceWarmup } from '../test-utils'; -import { DiagnosticCode } from '../../../../src/plugins/typescript/features/DiagnosticsProvider'; -import { VERSION } from 'svelte/compiler'; -import { internalHelpers } from 'svelte2tsx'; const testDir = path.join(__dirname, '..'); const indent = ' '.repeat(4); @@ -2229,4 +2230,84 @@ describe('CodeActionsProvider', function () { after(() => { __resetCache(); }); + + it('provides source action for adding all missing imports', async () => { + const { provider, document } = setup('codeaction-custom-fix-all-component5.svelte'); + + const range = Range.create(Position.create(4, 1), Position.create(4, 15)); + + // Request the specific source action + const codeActions = await provider.getCodeActions(document, range, { + diagnostics: [], // Diagnostics might not be needed here if we only want the source action by kind + only: [ADD_MISSING_IMPORTS_CODE_ACTION_KIND] + }); + + assert.ok(codeActions.length > 0, 'No code actions found'); + + // Find the action by its kind + const addImportsAction = codeActions.find((action) => action.data); + + // Ensure the action was found and has data (as it's now deferred) + assert.ok(addImportsAction, 'Add missing imports action should be found'); + assert.ok( + addImportsAction.data, + 'Add missing imports action should have data for resolution' + ); + + // Resolve the action to get the edits + const resolvedAction = await provider.resolveCodeAction(document, addImportsAction); + + // Assert the edits on the resolved action + assert.ok(resolvedAction.edit, 'Resolved action should have an edit'); + (resolvedAction.edit?.documentChanges?.[0])?.edits.forEach( + (edit) => (edit.newText = harmonizeNewLines(edit.newText)) + ); + + assert.deepStrictEqual(resolvedAction.edit, { + documentChanges: [ + { + edits: [ + { + newText: + `\n${indent}import FixAllImported from \"./importing/FixAllImported.svelte\";\n` + + `${indent}import FixAllImported2 from \"./importing/FixAllImported2.svelte\";\n`, + range: { + start: { + character: 18, + line: 0 + }, + end: { + character: 18, + line: 0 + } + } + } + ], + textDocument: { + uri: getUri('codeaction-custom-fix-all-component5.svelte'), + version: null + } + } + ] + }); + + // Optional: Verify the kind and title remain correct on the resolved action + assert.strictEqual(resolvedAction.kind, ADD_MISSING_IMPORTS_CODE_ACTION_KIND); + assert.strictEqual(resolvedAction.title, 'Add all missing imports'); + }); + + it('provides source action for adding all missing imports only when imports are missing', async () => { + const { provider, document } = setup('codeaction-custom-fix-all-component6.svelte'); + + const codeActions = await provider.getCodeActions( + document, + Range.create(Position.create(1, 4), Position.create(1, 5)), + { + diagnostics: [], // No diagnostics = no missing imports + only: [ADD_MISSING_IMPORTS_CODE_ACTION_KIND] + } + ); + + assert.deepStrictEqual(codeActions, []); + }); }); 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 e06fe7278..f0f3ea933 100644 --- a/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts @@ -298,6 +298,7 @@ describe('CompletionProviderImpl', function () { assert.deepStrictEqual(item, { label: 'custom-element', kind: CompletionItemKind.Property, + commitCharacters: [], textEdit: { range: { start: { line: 0, character: 1 }, end: { line: 0, character: 2 } }, newText: 'custom-element' diff --git a/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts b/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts index dbccee6e5..b6227bfb7 100644 --- a/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts @@ -84,7 +84,7 @@ describe('DiagnosticsProvider', function () { try { const diagnostics3 = await plugin.getDiagnostics(document); - assert.deepStrictEqual(diagnostics3.length, 1); + assert.deepStrictEqual(diagnostics3.length, 0); await lsAndTsDocResolver.deleteSnapshot(newTsFilePath); } finally { unlinkSync(newTsFilePath); diff --git a/packages/language-server/test/plugins/typescript/testfiles/code-actions/codeaction-custom-fix-all-component5.svelte b/packages/language-server/test/plugins/typescript/testfiles/code-actions/codeaction-custom-fix-all-component5.svelte new file mode 100644 index 000000000..15f6d8fd7 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/code-actions/codeaction-custom-fix-all-component5.svelte @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/language-server/test/plugins/typescript/testfiles/code-actions/codeaction-custom-fix-all-component6.svelte b/packages/language-server/test/plugins/typescript/testfiles/code-actions/codeaction-custom-fix-all-component6.svelte new file mode 100644 index 000000000..6c32ebc5e --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/code-actions/codeaction-custom-fix-all-component6.svelte @@ -0,0 +1,10 @@ + + + + + diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts index b3ef9233c..bd8947c64 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts @@ -43,6 +43,7 @@ export class Element { private isSelfclosing: boolean; public tagName: string; public child?: any; + private tagNameEnd: number; // Add const $$xxx = ... only if the variable name is actually used // in order to prevent "$$xxx is defined but never used" TS hints @@ -74,7 +75,7 @@ export class Element { this.startTagStart = this.node.start; this.startTagEnd = this.computeStartTagEnd(); - const tagEnd = this.startTagStart + this.node.name.length + 1; + const tagEnd = (this.tagNameEnd = this.startTagStart + this.node.name.length + 1); // Ensure deleted characters are mapped to the attributes object so we // get autocompletion when triggering it on a whitespace. if (/\s/.test(str.original.charAt(tagEnd))) { @@ -205,7 +206,19 @@ export class Element { } if (this.isSelfclosing) { - transform(this.str, this.startTagStart, this.startTagEnd, [ + // The transformation is the whole start tag + <, ex: ' && + (transformEnd === this.tagNameEnd || transformEnd === this.tagNameEnd + 1) + ) { + transformEnd = this.startTagStart; + this.str.remove(this.startTagStart, this.startTagStart + 1); + } + + transform(this.str, this.startTagStart, transformEnd, [ // Named slot transformations go first inside a outer block scope because //
means "use the x of let:x", and without a separate // block scope this would give a "used before defined" error @@ -295,7 +308,7 @@ export class Element { default: { createElementStatement = [ `${createElement}("`, - [this.node.start + 1, this.node.start + 1 + this.node.name.length], + [this.node.start + 1, this.tagNameEnd], `"${addActions()}, {` ]; break; diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/InlineComponent.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/InlineComponent.ts index 66f6d0818..2be599802 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/InlineComponent.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/InlineComponent.ts @@ -38,6 +38,7 @@ export class InlineComponent { private startTagStart: number; private startTagEnd: number; private isSelfclosing: boolean; + private tagNameEnd: number; public child?: any; // Add const $$xxx = ... only if the variable name is actually used @@ -64,7 +65,7 @@ export class InlineComponent { this.startTagStart = this.node.start; this.startTagEnd = this.computeStartTagEnd(); - const tagEnd = this.startTagStart + this.node.name.length + 1; + const tagEnd = (this.tagNameEnd = this.startTagStart + this.node.name.length + 1); // Ensure deleted characters are mapped to the attributes object so we // get autocompletion when triggering it on a whitespace. if (/\s/.test(str.original.charAt(tagEnd))) { @@ -227,7 +228,7 @@ export class InlineComponent { if (endStart === -1) { // Can happen in loose parsing mode when there's no closing tag endStart = this.node.end; - this.startTagEnd = this.node.end - 1; + this.startTagEnd = Math.max(this.node.end - 1, this.tagNameEnd); } else { endStart += this.node.start; } @@ -238,7 +239,17 @@ export class InlineComponent { } this.endTransformation.push('}'); - transform(this.str, this.startTagStart, this.startTagEnd, [ + let transformationEnd = this.startTagEnd; + + // The transformation is the whole start tag + <, ex: , dontAddTypeDef: boolean, - omitTyped = false + onlyTyped = false ): string[] { return names .map(([key, value]) => { - if (omitTyped && value.type) return; + if (onlyTyped && !value.type) return; // Important to not use shorthand props for rename functionality return `${dontAddTypeDef && value.doc ? `\n${value.doc}` : ''}${ value.identifierText || key diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts index 7d95d1c73..dd2ebaceb 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts @@ -86,6 +86,14 @@ export class HoistableInterfaces { if (ts.isInterfaceDeclaration(node)) { this.module_types.add(node.name.text); } + + if (ts.isEnumDeclaration(node)) { + this.module_types.add(node.name.text); + } + + if (ts.isModuleDeclaration(node) && ts.isIdentifier(node.name)) { + this.module_types.add(node.name.text); + } } analyzeInstanceScriptNode(node: ts.Node) { @@ -158,6 +166,24 @@ export class HoistableInterfaces { } }); + node.heritageClauses?.forEach((clause) => { + clause.types.forEach((type) => { + if (ts.isIdentifier(type.expression)) { + const type_name = type.expression.text; + if (!generics.includes(type_name)) { + type_dependencies.add(type_name); + } + } + + this.collectTypeDependencies( + type, + type_dependencies, + value_dependencies, + generics + ); + }); + }); + if (this.module_types.has(interface_name)) { // shadowed; we can't hoist this.disallowed_types.add(interface_name); @@ -229,6 +255,14 @@ export class HoistableInterfaces { if (ts.isEnumDeclaration(node)) { this.disallowed_values.add(node.name.text); } + + // namespace declaration should not be in the instance script. + // Only adding the top-level name to the disallowed list, + // so that at least there won't a confusing error message of "can't find namespace Foo" + if (ts.isModuleDeclaration(node) && ts.isIdentifier(node.name)) { + this.disallowed_types.add(node.name.text); + this.disallowed_values.add(node.name.text); + } } analyze$propsRune( @@ -239,7 +273,7 @@ export class HoistableInterfaces { if (node.initializer.typeArguments?.length > 0 || node.type) { const generic_arg = node.initializer.typeArguments?.[0] || node.type; if (ts.isTypeReferenceNode(generic_arg)) { - const name = this.getEntityNameText(generic_arg.typeName); + const name = this.getEntityNameRoot(generic_arg.typeName); const interface_node = this.interface_map.get(name); if (interface_node) { this.props_interface.name = name; @@ -394,13 +428,13 @@ export class HoistableInterfaces { ) { const walk = (node: ts.Node) => { if (ts.isTypeReferenceNode(node)) { - const type_name = this.getEntityNameText(node.typeName); + const type_name = this.getEntityNameRoot(node.typeName); if (!generics.includes(type_name)) { type_dependencies.add(type_name); } } else if (ts.isTypeQueryNode(node)) { // Handle 'typeof' expressions: e.g., foo: typeof bar - value_dependencies.add(this.getEntityNameText(node.exprName)); + value_dependencies.add(this.getEntityNameRoot(node.exprName)); } ts.forEachChild(node, walk); @@ -410,15 +444,16 @@ export class HoistableInterfaces { } /** - * Retrieves the full text of an EntityName (handles nested names). + * Retrieves the top-level variable/namespace of an EntityName (handles nested names). + * ex: `foo.bar.baz` -> `foo` * @param entity_name The EntityName to extract text from. - * @returns The full name as a string. + * @returns The top-level name as a string. */ - private getEntityNameText(entity_name: ts.EntityName): string { + private getEntityNameRoot(entity_name: ts.EntityName): string { if (ts.isIdentifier(entity_name)) { return entity_name.text; } else { - return this.getEntityNameText(entity_name.left) + '.' + entity_name.right.text; + return this.getEntityNameRoot(entity_name.left); } } } diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/handleImportDeclaration.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/handleImportDeclaration.ts index 4f022260b..9daeb78e5 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/handleImportDeclaration.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/handleImportDeclaration.ts @@ -1,6 +1,6 @@ import MagicString from 'magic-string'; import ts from 'typescript'; -import { moveNode } from '../utils/tsAst'; +import { getTopLevelImports, moveNode } from '../utils/tsAst'; /** * move imports to top of script so they appear outside our render function @@ -25,7 +25,7 @@ export function handleFirstInstanceImport( hasModuleScript: boolean, str: MagicString ) { - const imports = tsAst.statements.filter(ts.isImportDeclaration).sort((a, b) => a.end - b.end); + const imports = getTopLevelImports(tsAst); const firstImport = imports[0]; if (!firstImport) { return; diff --git a/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts b/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts index a189c75b5..d8c11ac64 100644 --- a/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts +++ b/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts @@ -273,3 +273,7 @@ function isNewGroup(sourceFile: ts.SourceFile, topLevelImportDecl: ts.Node, scan return false; } + +export function getTopLevelImports(sourceFile: ts.SourceFile): ts.ImportDeclaration[] { + return sourceFile.statements.filter(ts.isImportDeclaration).sort((a, b) => a.end - b.end); +} diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-component-no-attr.v5/expected.error.json b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-component-no-attr.v5/expected.error.json new file mode 100644 index 000000000..05090efbb --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-component-no-attr.v5/expected.error.json @@ -0,0 +1,20 @@ +{ + "code": "expected_token", + "message": "Expected token >\nhttps://svelte.dev/e/expected_token", + "filename": "(unknown)", + "start": { + "line": 7, + "column": 2, + "character": 138 + }, + "end": { + "line": 7, + "column": 2, + "character": 138 + }, + "position": [ + 138, + 138 + ], + "frame": " 5:
\n 6: \n ^\n 8: \n 9: +
+ + +\nhttps://svelte.dev/e/expected_token", + "filename": "(unknown)", + "start": { + "line": 7, + "column": 2, + "character": 138 + }, + "end": { + "line": 7, + "column": 2, + "character": 138 + }, + "position": [ + 138, + 138 + ], + "frame": " 5:
\n 6: \n ^\n 8: \n 9: +
+ + + ; - let foo = true; -; const hoistable1/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => { + const hoistable1/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => { { svelteHTML.createElement("div", {}); } };return __sveltets_2_any(0)}; const hoistable2/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => { { svelteHTML.createElement("div", {});foo; } -};return __sveltets_2_any(0)};;function $$render() { +};return __sveltets_2_any(0)}; + let foo = true; +;;function $$render() { async () => { diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-5.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-5.v5/expectedv2.ts new file mode 100644 index 000000000..1063eae01 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-5.v5/expectedv2.ts @@ -0,0 +1,12 @@ +/// +; + import {} from 'svelte' + const foo/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => {};return __sveltets_2_any(0)}; +;;function $$render() { +async () => { + +}; +return { props: /** @type {Record} */ ({}), exports: {}, bindings: "", slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_isomorphic_component(__sveltets_2_partial(__sveltets_2_with_any_event($$render()))); +/*Ωignore_startΩ*/type Input__SvelteComponent_ = InstanceType; +/*Ωignore_endΩ*/export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-5.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-5.v5/input.svelte new file mode 100644 index 000000000..6321ca3bf --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-5.v5/input.svelte @@ -0,0 +1,5 @@ + + +{#snippet foo()}{/snippet} \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-6.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-6.v5/expectedv2.ts new file mode 100644 index 000000000..39bb66302 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-6.v5/expectedv2.ts @@ -0,0 +1,12 @@ +/// +; + const _foo/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => {};return __sveltets_2_any(0)}; + export const foo = _foo; +;;function $$render() { +async () => { + +}; +return { props: /** @type {Record} */ ({}), exports: {}, bindings: "", slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_isomorphic_component(__sveltets_2_partial(__sveltets_2_with_any_event($$render()))); +/*Ωignore_startΩ*/type Input__SvelteComponent_ = InstanceType; +/*Ωignore_endΩ*/export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-6.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-6.v5/input.svelte new file mode 100644 index 000000000..8a2e6f5ed --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-6.v5/input.svelte @@ -0,0 +1,5 @@ + + +{#snippet _foo()}{/snippet} \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-export-list-runes.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-export-list-runes.v5/expectedv2.ts index d05d48969..23428b25c 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-export-list-runes.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-export-list-runes.v5/expectedv2.ts @@ -20,7 +20,7 @@ ; async () => { { svelteHTML.createElement("svelte:options", {"runes":true,});} }; -return { props: {} as Record, exports: {Foo: Foo,bar: bar,RenamedFoo: RenameFoo,renamedbar: renamebar} as any as { name1: string,name2: string,name3: string,name4: string,renamed1: string,renamed2: string,Foo: typeof Foo,bar: typeof bar,baz: string,RenamedFoo: typeof RenameFoo,renamedbar: typeof renamebar,renamedbaz: string }, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +return { props: {} as Record, exports: {name1: name1,name2: name2,name3: name3,name4: name4,renamed1: rename1,renamed2: rename2,baz: baz,renamedbaz: renamebaz} as any as { name1: string,name2: string,name3: string,name4: string,renamed1: string,renamed2: string,Foo: typeof Foo,bar: typeof bar,baz: string,RenamedFoo: typeof RenameFoo,renamedbar: typeof renamebar,renamedbaz: string }, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} const Input__SvelteComponent_ = __sveltets_2_fn_component($$render()); type Input__SvelteComponent_ = ReturnType; export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-6.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-6.v5/expectedv2.ts new file mode 100644 index 000000000..f063d70b8 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-6.v5/expectedv2.ts @@ -0,0 +1,19 @@ +/// +;; + interface A { + type: string; + };; + + interface Props extends A { + a: string; + };function $$render() { + + + + const { }: Props = $props(); +; +async () => {}; +return { props: {} as any as Props, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_fn_component($$render()); +type Input__SvelteComponent_ = ReturnType; +export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-6.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-6.v5/input.svelte new file mode 100644 index 000000000..dec767819 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-6.v5/input.svelte @@ -0,0 +1,10 @@ + diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-10.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-10.v5/expectedv2.ts new file mode 100644 index 000000000..22f05c3f8 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-10.v5/expectedv2.ts @@ -0,0 +1,17 @@ +/// +;; +type Props = { + data: {cfg: string}; +};;function $$render() { + + +let { data }: Props = $props(); + +type A = typeof data.cfg; +type B = (typeof data)['cfg']; +; +async () => {}; +return { props: {} as any as Props, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_fn_component($$render()); +type Input__SvelteComponent_ = ReturnType; +export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-10.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-10.v5/input.svelte new file mode 100644 index 000000000..42e0fbb2a --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-10.v5/input.svelte @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-11.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-11.v5/expectedv2.ts new file mode 100644 index 000000000..ed54ba6dc --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-11.v5/expectedv2.ts @@ -0,0 +1,20 @@ +/// +;function $$render() { + + const a = 1; + +interface A { + Abc: typeof a +} + +interface Abc { + foo: A.Abc +} + +let {}: Abc = $props(); +; +async () => {}; +return { props: {} as any as Abc, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_fn_component($$render()); +type Input__SvelteComponent_ = ReturnType; +export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-11.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-11.v5/input.svelte new file mode 100644 index 000000000..c2532ebe6 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-11.v5/input.svelte @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-12.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-12.v5/expectedv2.ts new file mode 100644 index 000000000..1bcbfe8c5 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-12.v5/expectedv2.ts @@ -0,0 +1,20 @@ +/// +;function $$render() { + +const a = 1; + +namespace A { + export type Abc = typeof a +} + +interface Abc { + foo: A.Abc +} + +let {}: Abc = $props(); +; +async () => {}; +return { props: {} as any as Abc, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_fn_component($$render()); +type Input__SvelteComponent_ = ReturnType; +export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-12.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-12.v5/input.svelte new file mode 100644 index 000000000..897e08c65 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-12.v5/input.svelte @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-13.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-13.v5/expectedv2.ts new file mode 100644 index 000000000..1525d0cc2 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-13.v5/expectedv2.ts @@ -0,0 +1,20 @@ +/// +; + namespace A { + export type Abd = number + } +;;function $$render() { + +interface A { + Abc: number +} + +let {Abc}: A = $props() +; +async () => { + +}; +return { props: {} as any as A, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_fn_component($$render()); +type Input__SvelteComponent_ = ReturnType; +export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-13.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-13.v5/input.svelte new file mode 100644 index 000000000..1617746f0 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-13.v5/input.svelte @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-14.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-14.v5/expectedv2.ts new file mode 100644 index 000000000..27b661d11 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-14.v5/expectedv2.ts @@ -0,0 +1,19 @@ +/// +; + enum A { + } +;;function $$render() { + +interface A { + Abc: number +} + +let {Abc}: A = $props() +; +async () => { + +}; +return { props: {} as any as A, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_fn_component($$render()); +type Input__SvelteComponent_ = ReturnType; +export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-14.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-14.v5/input.svelte new file mode 100644 index 000000000..6d555bb69 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-14.v5/input.svelte @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-8.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-8.v5/expectedv2.ts new file mode 100644 index 000000000..60855f1fb --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-8.v5/expectedv2.ts @@ -0,0 +1,36 @@ +/// +;function $$render() { + + interface WithItems { + items: T[]; + } + + interface Props extends WithItems { + prop: T; + }; + let { prop }: Props = $props(); +; +async () => {}; +return { props: {} as any as Props, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +class __sveltets_Render { + props() { + return $$render().props; + } + events() { + return $$render().events; + } + slots() { + return $$render().slots; + } + bindings() { return __sveltets_$$bindings(''); } + exports() { return {}; } +} + +interface $$IsomorphicComponent { + new (options: import('svelte').ComponentConstructorOptions['props']>>): import('svelte').SvelteComponent['props']>, ReturnType<__sveltets_Render['events']>, ReturnType<__sveltets_Render['slots']>> & { $$bindings?: ReturnType<__sveltets_Render['bindings']> } & ReturnType<__sveltets_Render['exports']>; + (internal: unknown, props: ReturnType<__sveltets_Render['props']> & {}): ReturnType<__sveltets_Render['exports']>; + z_$$bindings?: ReturnType<__sveltets_Render['bindings']>; +} +const Input__SvelteComponent_: $$IsomorphicComponent = null as any; +/*Ωignore_startΩ*/type Input__SvelteComponent_ = InstanceType>; +/*Ωignore_endΩ*/export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-8.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-8.v5/input.svelte new file mode 100644 index 000000000..11461621f --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-8.v5/input.svelte @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-9.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-9.v5/expectedv2.ts new file mode 100644 index 000000000..39b62f5b3 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-9.v5/expectedv2.ts @@ -0,0 +1,31 @@ +/// +;function $$render() { + + interface Props extends T { + }; + let { a }: Props = $props(); +; +async () => {}; +return { props: {} as any as Props, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +class __sveltets_Render { + props() { + return $$render().props; + } + events() { + return $$render().events; + } + slots() { + return $$render().slots; + } + bindings() { return __sveltets_$$bindings(''); } + exports() { return {}; } +} + +interface $$IsomorphicComponent { + new (options: import('svelte').ComponentConstructorOptions['props']>>): import('svelte').SvelteComponent['props']>, ReturnType<__sveltets_Render['events']>, ReturnType<__sveltets_Render['slots']>> & { $$bindings?: ReturnType<__sveltets_Render['bindings']> } & ReturnType<__sveltets_Render['exports']>; + (internal: unknown, props: ReturnType<__sveltets_Render['props']> & {}): ReturnType<__sveltets_Render['exports']>; + z_$$bindings?: ReturnType<__sveltets_Render['bindings']>; +} +const Input__SvelteComponent_: $$IsomorphicComponent = null as any; +/*Ωignore_startΩ*/type Input__SvelteComponent_ = InstanceType>; +/*Ωignore_endΩ*/export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-9.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-9.v5/input.svelte new file mode 100644 index 000000000..fd8a6f6f1 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-9.v5/input.svelte @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-sveltekit-autotypes-$props-rune-unchanged.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-sveltekit-autotypes-$props-rune-unchanged.v5/expectedv2.ts index c788dc05e..b2051ddd0 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-sveltekit-autotypes-$props-rune-unchanged.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-sveltekit-autotypes-$props-rune-unchanged.v5/expectedv2.ts @@ -6,7 +6,7 @@ let { form, data }:/*Ωignore_startΩ*/$$ComponentProps/*Ωignore_endΩ*/ = $props(); ; async () => {}; -return { props: {} as any as $$ComponentProps, exports: {} as any as { snapshot: any }, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +return { props: {} as any as $$ComponentProps, exports: {snapshot: snapshot} as any as { snapshot: any }, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} const Page__SvelteComponent_ = __sveltets_2_fn_component($$render()); type Page__SvelteComponent_ = ReturnType; export default Page__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-sveltekit-autotypes-$props-rune.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-sveltekit-autotypes-$props-rune.v5/expectedv2.ts index 4a33a614f..ce2a45ef9 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-sveltekit-autotypes-$props-rune.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-sveltekit-autotypes-$props-rune.v5/expectedv2.ts @@ -5,7 +5,7 @@ let { form, data }: $$ComponentProps = $props(); ; async () => {}; -return { props: {} as any as $$ComponentProps, exports: {snapshot: snapshot} as any as { snapshot: typeof snapshot }, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +return { props: {} as any as $$ComponentProps, exports: {} as any as { snapshot: typeof snapshot }, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} const Page__SvelteComponent_ = __sveltets_2_fn_component($$render()); type Page__SvelteComponent_ = ReturnType; export default Page__SvelteComponent_; \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 905264a94..26e32643a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,11 +64,11 @@ importers: specifier: ^0.3.5 version: 0.3.5 vscode-css-languageservice: - specifier: ~6.3.2 - version: 6.3.2 + specifier: ~6.3.5 + version: 6.3.5 vscode-html-languageservice: - specifier: ~5.3.2 - version: 5.3.2 + specifier: ~5.4.0 + version: 5.4.0 vscode-languageserver: specifier: 9.0.1 version: 9.0.1 @@ -80,7 +80,7 @@ importers: version: 3.17.5 vscode-uri: specifier: ~3.0.0 - version: 3.0.8 + version: 3.1.0 devDependencies: '@types/estree': specifier: ^0.0.42 @@ -181,7 +181,7 @@ importers: version: 3.17.2 vscode-uri: specifier: ~3.0.0 - version: 3.0.8 + version: 3.1.0 packages/svelte-vscode: dependencies: @@ -1302,11 +1302,11 @@ packages: vfile-message@3.1.4: resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==} - vscode-css-languageservice@6.3.2: - resolution: {integrity: sha512-GEpPxrUTAeXWdZWHev1OJU9lz2Q2/PPBxQ2TIRmLGvQiH3WZbqaNoute0n0ewxlgtjzTW3AKZT+NHySk5Rf4Eg==} + vscode-css-languageservice@6.3.5: + resolution: {integrity: sha512-ehEIMXYPYEz/5Svi2raL9OKLpBt5dSAdoCFoLpo0TVFKrVpDemyuQwS3c3D552z/qQCg3pMp8oOLMObY6M3ajQ==} - vscode-html-languageservice@5.3.2: - resolution: {integrity: sha512-3MgFQqVG+iQVNG7QI/slaoL7lJpne0nssX082kjUF1yn/YJa8BWCLeCJjM0YpTlp8A7JT1+J22mk4qSPx3NjSQ==} + vscode-html-languageservice@5.4.0: + resolution: {integrity: sha512-9/cbc90BSYCghmHI7/VbWettHZdC7WYpz2g5gBK6UDUI1MkZbM773Q12uAYJx9jzAiNHPpyo6KzcwmcnugncAQ==} vscode-jsonrpc@8.0.2: resolution: {integrity: sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ==} @@ -1359,8 +1359,8 @@ packages: vscode-uri@2.1.2: resolution: {integrity: sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==} - vscode-uri@3.0.8: - resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} @@ -2372,19 +2372,19 @@ snapshots: '@types/unist': 2.0.6 unist-util-stringify-position: 3.0.3 - vscode-css-languageservice@6.3.2: + vscode-css-languageservice@6.3.5: dependencies: '@vscode/l10n': 0.0.18 vscode-languageserver-textdocument: 1.0.12 vscode-languageserver-types: 3.17.5 - vscode-uri: 3.0.8 + vscode-uri: 3.1.0 - vscode-html-languageservice@5.3.2: + vscode-html-languageservice@5.4.0: dependencies: '@vscode/l10n': 0.0.18 vscode-languageserver-textdocument: 1.0.12 vscode-languageserver-types: 3.17.5 - vscode-uri: 3.0.8 + vscode-uri: 3.1.0 vscode-jsonrpc@8.0.2: {} @@ -2437,7 +2437,7 @@ snapshots: vscode-uri@2.1.2: {} - vscode-uri@3.0.8: {} + vscode-uri@3.1.0: {} which@2.0.2: dependencies: