From b73d807e56a287a0a426cd902861908d4266fea6 Mon Sep 17 00:00:00 2001 From: "jyc.dev" Date: Fri, 18 Jul 2025 20:22:22 +0200 Subject: [PATCH 1/3] fix: Better detection version (#2801) * step 1 of version * start managing things * adding semver to test * let's not redo semver! * better fallback * not only this ;) * Update version.ts --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- packages/svelte-vscode/package.json | 2 + packages/svelte-vscode/src/sveltekit/utils.ts | 30 +++++++---- packages/svelte-vscode/src/version.ts | 51 +++++++++++++++++++ packages/svelte-vscode/test/version.spec.ts | 41 +++++++++++++++ pnpm-lock.yaml | 20 +++++++- 5 files changed, 132 insertions(+), 12 deletions(-) create mode 100644 packages/svelte-vscode/src/version.ts create mode 100644 packages/svelte-vscode/test/version.spec.ts diff --git a/packages/svelte-vscode/package.json b/packages/svelte-vscode/package.json index 574715f96..f97cf561f 100644 --- a/packages/svelte-vscode/package.json +++ b/packages/svelte-vscode/package.json @@ -741,8 +741,10 @@ "devDependencies": { "@types/lodash": "^4.14.116", "@types/node": "^18.0.0", + "@types/semver": "^7.7.0", "@types/vscode": "^1.67", "js-yaml": "^3.14.0", + "semver": "^7.7.2", "tslib": "^2.4.0", "typescript": "^5.8.2", "vitest": "^3.2.4", diff --git a/packages/svelte-vscode/src/sveltekit/utils.ts b/packages/svelte-vscode/src/sveltekit/utils.ts index 6ca33f796..cce8e2de9 100644 --- a/packages/svelte-vscode/src/sveltekit/utils.ts +++ b/packages/svelte-vscode/src/sveltekit/utils.ts @@ -2,6 +2,7 @@ import { TextDecoder } from 'util'; import * as path from 'path'; import { Uri, workspace } from 'vscode'; import type { GenerateConfig } from './generateFiles/types'; +import { atLeast } from '../version'; export async function fileExists(file: string) { try { @@ -31,10 +32,20 @@ export async function checkProjectKind(path: string): Promise= jsconfig.length); let withSatisfies = false; @@ -44,7 +55,12 @@ export async function checkProjectKind(path: string): Promise= (minor ?? 0)) || - Number(majorVersion) > major - ); -} - export async function getVersionFromPackageJson(packageName: string): Promise { const packageJsonList = await workspace.findFiles('**/package.json', '**/node_modules/**'); diff --git a/packages/svelte-vscode/src/version.ts b/packages/svelte-vscode/src/version.ts new file mode 100644 index 000000000..7db8b318d --- /dev/null +++ b/packages/svelte-vscode/src/version.ts @@ -0,0 +1,51 @@ +import { lt, coerce, gte } from 'semver'; + +/** + * @example + * const supported = atLeast({ + * packageName: 'node', + * versionMin: '18.3', + * versionToCheck: process.versions.node + * fallback: true // optional + * }); + */ +export function atLeast(o: { + packageName: string; + versionMin: string; + versionToCheck: string; + fallback: boolean; +}): boolean; +export function atLeast(o: { + packageName: string; + versionMin: string; + versionToCheck: string; + fallback?: undefined; +}): boolean | undefined; + +// Implementation +export function atLeast(o: { + packageName: string; + versionMin: string; + versionToCheck: string; + fallback?: boolean; +}): boolean | undefined { + const { packageName, versionMin, versionToCheck, fallback } = o; + if (versionToCheck === undefined || versionToCheck === '') return fallback; + + if ( + versionToCheck.includes('latest') || + versionToCheck.includes('catalog:') || + versionToCheck.includes('http') + ) { + console.warn(`Version '${versionToCheck}' for '${packageName}' is not supported`); + return fallback; + } + try { + const vMin = coerce(versionMin); + const vToCheck = coerce(versionToCheck); + if (vMin && vToCheck) { + return gte(vToCheck, vMin); + } + } catch (error) {} + return fallback; +} diff --git a/packages/svelte-vscode/test/version.spec.ts b/packages/svelte-vscode/test/version.spec.ts new file mode 100644 index 000000000..f22b9c721 --- /dev/null +++ b/packages/svelte-vscode/test/version.spec.ts @@ -0,0 +1,41 @@ +import { expect, describe, it } from 'vitest'; +import { atLeast } from '../src/version'; + +describe('atLeast', () => { + const combinationsAtLeast = [ + { min: '5', version: '>=5', supported: true }, + { min: '5', version: '>=5.0.0', supported: true }, + { min: '5', version: '5.0.0', supported: true }, + { min: '5', version: '5', supported: true }, + { min: '5', version: '4', supported: false }, + { min: '5', version: '4.9', supported: false }, + { min: '5', version: '', supported: undefined }, + { min: '5', version: 'catalog:', supported: undefined }, + { min: '5', version: 'latest', supported: undefined }, + { min: '5', version: 'latest', fallback: true, supported: true }, + { min: '5', version: 'latest', fallback: false, supported: false } + ]; + it.each(combinationsAtLeast)( + '(min $min, $version, $fallback) => $supported', + ({ min, version, supported, fallback }) => { + if (fallback !== undefined) { + expect( + atLeast({ + packageName: 'myPkg', + versionMin: min, + versionToCheck: version, + fallback + }) + ).toEqual(supported); + } else { + expect( + atLeast({ + packageName: 'myPkg', + versionMin: min, + versionToCheck: version + }) + ).toEqual(supported); + } + } + ); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29ea244d6..38c0cb205 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -207,12 +207,18 @@ importers: '@types/node': specifier: ^18.0.0 version: 18.19.46 + '@types/semver': + specifier: ^7.7.0 + version: 7.7.0 '@types/vscode': specifier: ^1.67 version: 1.78.0 js-yaml: specifier: ^3.14.0 version: 3.14.1 + semver: + specifier: ^7.7.2 + version: 7.7.2 tslib: specifier: ^2.4.0 version: 2.5.2 @@ -771,6 +777,9 @@ packages: '@types/sade@1.7.4': resolution: {integrity: sha512-6ys13kmtlY0aIOz4KtMdeBD9BHs6vSE3aRcj4vAZqXjypT2el8WZt6799CMjElVgh1cbOH/t3vrpQ4IpwytcPA==} + '@types/semver@7.7.0': + resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} + '@types/sinon@7.5.2': resolution: {integrity: sha512-T+m89VdXj/eidZyejvmoP9jivXgBDdkOSBVQjU9kF349NEx10QdPNGxHeZUaj1IlJ32/ewdyXJjnJxyxJroYwg==} @@ -1561,6 +1570,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + serialize-javascript@6.0.0: resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} @@ -2227,6 +2241,8 @@ snapshots: dependencies: '@types/mri': 1.1.1 + '@types/semver@7.7.0': {} + '@types/sinon@7.5.2': {} '@types/unist@2.0.6': {} @@ -3037,6 +3053,8 @@ snapshots: dependencies: lru-cache: 6.0.0 + semver@7.7.2: {} + serialize-javascript@6.0.0: dependencies: randombytes: 2.1.0 @@ -3290,7 +3308,7 @@ snapshots: vscode-languageclient@9.0.1: dependencies: minimatch: 5.1.6 - semver: 7.5.1 + semver: 7.7.2 vscode-languageserver-protocol: 3.17.5 vscode-languageserver-protocol@3.17.2: From 23db5a4bc858ce9c75fc8eb50cf42445b04175c9 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei-Da" <36730922+jasonlyu123@users.noreply.github.com> Date: Fri, 25 Jul 2025 14:28:24 +0800 Subject: [PATCH 2/3] fix: handle object literal in MustacheTag (#2805) --- packages/svelte2tsx/src/htmlxtojsx_v2/nodes/MustacheTag.ts | 7 +++++++ .../samples/mustache-tag-object-literal/expectedv2.js | 5 +++++ .../samples/mustache-tag-object-literal/input.svelte | 5 +++++ 3 files changed, 17 insertions(+) create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/mustache-tag-object-literal/expectedv2.js create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/mustache-tag-object-literal/input.svelte diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/MustacheTag.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/MustacheTag.ts index 9e888b90c..ab132e67e 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/MustacheTag.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/MustacheTag.ts @@ -10,6 +10,13 @@ export function handleMustacheTag(str: MagicString, node: BaseNode, parent: Base // handled inside Attribute.ts / StyleDirective.ts return; } + const text = str.original.slice(node.start + 1, node.end - 1); + if (text.trimStart().startsWith('{')) { + // possibly an object literal, wrapping it in parentheses so it's treated as an expression + str.overwrite(node.start, node.start + 1, ';(', { contentOnly: true }); + str.overwrite(node.end - 1, node.end, ');', { contentOnly: true }); + return; + } str.overwrite(node.start, node.start + 1, '', { contentOnly: true }); str.overwrite(node.end - 1, node.end, ';', { contentOnly: true }); } diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/mustache-tag-object-literal/expectedv2.js b/packages/svelte2tsx/test/htmlx2jsx/samples/mustache-tag-object-literal/expectedv2.js new file mode 100644 index 000000000..ce8c12ef9 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/mustache-tag-object-literal/expectedv2.js @@ -0,0 +1,5 @@ +;({ + toString() { return "Hello World" } +}); + +;({ a: '' }['a']); \ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/mustache-tag-object-literal/input.svelte b/packages/svelte2tsx/test/htmlx2jsx/samples/mustache-tag-object-literal/input.svelte new file mode 100644 index 000000000..0d001f271 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/mustache-tag-object-literal/input.svelte @@ -0,0 +1,5 @@ +{{ + toString() { return "Hello World" } +}} + +{{ a: '' }['a']} \ No newline at end of file From d84b178edf08cfc95e9cb21a446914e2cb5bddbd Mon Sep 17 00:00:00 2001 From: "Lyu, Wei-Da" <36730922+jasonlyu123@users.noreply.github.com> Date: Sun, 3 Aug 2025 14:30:05 +0800 Subject: [PATCH 3/3] fix: prevent organize imports from removing snippet (#2809) * wip * test, fix leftover indent --- .../features/CodeActionsProvider.ts | 78 +++++++- .../typescript/features/CompletionProvider.ts | 19 +- .../src/plugins/typescript/utils.ts | 7 + .../features/CodeActionsProvider.test.ts | 178 +++++++++++++++++- .../organize-import-all-remove.svelte | 7 + .../organize-imports-snippet.svelte | 9 + 6 files changed, 277 insertions(+), 21 deletions(-) create mode 100644 packages/language-server/test/plugins/typescript/testfiles/code-actions/organize-import-all-remove.svelte create mode 100644 packages/language-server/test/plugins/typescript/testfiles/code-actions/organize-imports-snippet.svelte diff --git a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts index 244ae4e12..6887e2514 100644 --- a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts @@ -29,6 +29,7 @@ import { flatten, getIndent, isNotNullOrUndefined, + isPositionEqual, memoize, modifyLines, normalizePath, @@ -42,6 +43,7 @@ import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; import { LanguageServiceContainer } from '../service'; import { changeSvelteComponentName, + cloneRange, convertRange, isInScript, toGeneratedSvelteComponentName @@ -469,6 +471,10 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { }) ); + for (const change of documentChanges) { + this.checkIndentLeftover(change, document); + } + return [ CodeAction.create( skipDestructiveCodeActions ? 'Sort Imports' : 'Organize Imports', @@ -521,21 +527,20 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { snapshot: DocumentSnapshot, range: Range ) { + if (!(snapshot instanceof SvelteDocumentSnapshot)) { + return range; + } // Handle svelte2tsx wrong import mapping: // The character after the last import maps to the start of the script // TODO find a way to fix this in svelte2tsx and then remove this if ( (range.end.line === 0 && range.end.character === 1) || - range.end.line < range.start.line + range.end.line < range.start.line || + (isInScript(range.start, snapshot) && !isInScript(range.end, snapshot)) ) { edit.span.length -= 1; range = mapRangeToOriginal(snapshot, convertRange(snapshot, edit.span)); - if (!(snapshot instanceof SvelteDocumentSnapshot)) { - range.end.character += 1; - return range; - } - const line = getLineAtPosition(range.end, snapshot.getOriginalText()); // remove-import code action will removes the // line break generated by svelte2tsx, @@ -554,6 +559,67 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { return range; } + private checkIndentLeftover(change: TextDocumentEdit, document: Document) { + if (!change.edits.length) { + return; + } + const orderedByStart = change.edits.sort((a, b) => { + if (a.range.start.line !== b.range.start.line) { + return a.range.start.line - b.range.start.line; + } + return a.range.start.character - b.range.start.character; + }); + + let current: TextEdit | undefined; + let groups: TextEdit[] = []; + for (let i = 0; i < orderedByStart.length; i++) { + const edit = orderedByStart[i]; + if (!current) { + current = { range: cloneRange(edit.range), newText: edit.newText }; + continue; + } + if (isPositionEqual(current.range.end, edit.range.start)) { + current.range.end = edit.range.end; + current.newText += edit.newText; + } else { + groups.push(current); + current = { range: cloneRange(edit.range), newText: edit.newText }; + } + } + if (current) { + groups.push(current); + } + + for (const edit of groups) { + if (edit.newText) { + continue; + } + const range = edit.range; + const lineContentBeforeRemove = document.getText({ + start: { line: range.start.line, character: 0 }, + end: range.start + }); + + const onlyIndentLeft = !lineContentBeforeRemove.trim(); + if (!onlyIndentLeft) { + continue; + } + + const lineContentAfterRemove = document.getText({ + start: range.end, + end: { line: range.end.line, character: Number.MAX_VALUE } + }); + const emptyAfterRemove = + !lineContentAfterRemove.trim() || lineContentAfterRemove.startsWith(''); + if (emptyAfterRemove) { + change.edits.push({ + range: { start: { line: range.start.line, character: 0 }, end: range.start }, + newText: '' + }); + } + } + } + private async applyQuickfix( document: Document, range: Range, diff --git a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts index bc1764d87..b19f48967 100644 --- a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts @@ -38,7 +38,8 @@ import { convertRange, isInScript, isGeneratedSvelteComponentName, - scriptElementKindToCompletionItemKind + scriptElementKindToCompletionItemKind, + cloneRange } from '../utils'; import { getJsDocTemplateCompletion } from './getJsDocTemplateCompletion'; import { @@ -595,7 +596,7 @@ export class CompletionsProviderImpl implements CompletionsProvider ({ label: name, kind: CompletionItemKind.Property, - textEdit: TextEdit.replace(this.cloneRange(replacementRange), name), + textEdit: TextEdit.replace(cloneRange(replacementRange), name), commitCharacters: [] })); } @@ -641,18 +642,11 @@ export class CompletionsProviderImpl implements CompletionsProvider{ uri: pathToUrl(filePath), - text: harmonizeNewLines(ts.sys.readFile(filePath) || '') + text: ts.sys.readFile(filePath) || '' }); return { provider, document, docManager, lsAndTsDocResolver }; } @@ -2086,6 +2086,116 @@ describe('CodeActionsProvider', function () { ]); }); + it('organize imports without leftover indentation', async () => { + const { provider, document } = setup('organize-import-all-remove.svelte'); + + const codeActions = await provider.getCodeActions( + document, + Range.create(Position.create(1, 4), Position.create(1, 5)), + { + diagnostics: [], + only: [CodeActionKind.SourceOrganizeImports] + } + ); + + assert.deepStrictEqual(codeActions, [ + { + title: 'Organize Imports', + edit: { + documentChanges: [ + { + textDocument: { + uri: getUri('organize-import-all-remove.svelte'), + version: null + }, + edits: [ + { + range: { + start: { + line: 1, + character: 4 + }, + end: { + line: 2, + character: 4 + } + }, + newText: '' + }, + { + range: { + start: { + line: 2, + character: 4 + }, + end: { + line: 3, + character: 0 + } + }, + newText: '' + }, + { + range: { + start: { + line: 4, + character: 4 + }, + end: { + line: 5, + character: 4 + } + }, + newText: '' + }, + { + range: { + start: { + line: 5, + character: 4 + }, + end: { + line: 6, + character: 0 + } + }, + newText: '' + }, + { + range: { + start: { + line: 1, + character: 0 + }, + end: { + line: 1, + character: 4 + } + }, + newText: '' + }, + { + range: { + start: { + line: 4, + character: 0 + }, + end: { + line: 4, + character: 4 + } + }, + newText: '' + } + ] + } + ] + }, + kind: 'source.organizeImports' + } + ]); + }); + it('should do extract into function refactor', async () => { const { provider, document } = setup('codeactions.svelte'); @@ -2310,4 +2420,70 @@ describe('CodeActionsProvider', function () { assert.deepStrictEqual(codeActions, []); }); + + if (!isSvelte5Plus) { + return; + } + + it('organizes imports with top-level snippets', async () => { + const { provider, document } = setup('organize-imports-snippet.svelte'); + + const codeActions = await provider.getCodeActions( + document, + Range.create(Position.create(4, 15), Position.create(4, 15)), + { + diagnostics: [], + only: [CodeActionKind.SourceOrganizeImports] + } + ); + + (codeActions[0]?.edit?.documentChanges?.[0])?.edits.forEach( + (edit) => (edit.newText = harmonizeNewLines(edit.newText)) + ); + + assert.deepStrictEqual(codeActions, [ + { + edit: { + documentChanges: [ + { + edits: [ + { + newText: '', + range: { + end: { + character: 0, + line: 5 + }, + start: { + character: 4, + line: 4 + } + } + }, + { + newText: '', + range: { + end: { + character: 4, + line: 4 + }, + start: { + character: 0, + line: 4 + } + } + } + ], + textDocument: { + uri: getUri('organize-imports-snippet.svelte'), + version: null + } + } + ] + }, + kind: 'source.organizeImports', + title: 'Organize Imports' + } + ]); + }); }); diff --git a/packages/language-server/test/plugins/typescript/testfiles/code-actions/organize-import-all-remove.svelte b/packages/language-server/test/plugins/typescript/testfiles/code-actions/organize-import-all-remove.svelte new file mode 100644 index 000000000..92836210a --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/code-actions/organize-import-all-remove.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/language-server/test/plugins/typescript/testfiles/code-actions/organize-imports-snippet.svelte b/packages/language-server/test/plugins/typescript/testfiles/code-actions/organize-imports-snippet.svelte new file mode 100644 index 000000000..4d9159cac --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/code-actions/organize-imports-snippet.svelte @@ -0,0 +1,9 @@ + + + + +{#snippet baz()} +{/snippet} \ No newline at end of file