From 6a6ffe782f769f2aca80e544b7c847ed03899cf5 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 12 Dec 2024 12:57:08 +0100 Subject: [PATCH 1/4] feat: make use of loose Svelte parser and provide better intellisense This adds support for the Svelte parser with its new loose mode and adjusts code paths to make use of it properly. As a result, intellisense should be a lot more useful in situations where code is in the middle of being typed and the Svelte file is in a broken state. --- .../src/htmlxtojsx_v2/nodes/Attribute.ts | 18 ++++++++-- .../src/htmlxtojsx_v2/nodes/EachBlock.ts | 23 ++++++++++--- .../src/htmlxtojsx_v2/nodes/IfElseBlock.ts | 10 ++++-- .../htmlxtojsx_v2/nodes/InlineComponent.ts | 17 +++++++--- .../svelte2tsx/src/htmlxtojsx_v2/nodes/Key.ts | 6 ++-- .../src/htmlxtojsx_v2/nodes/SnippetBlock.ts | 34 +++++++++++++++---- .../src/htmlxtojsx_v2/utils/node-utils.ts | 16 +++++++++ packages/svelte2tsx/src/utils/htmlxparser.ts | 16 ++++++--- .../editing-binding/expected-svelte5.js | 6 ++-- 9 files changed, 114 insertions(+), 32 deletions(-) diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Attribute.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Attribute.ts index 0ef4095cc..d68e24078 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Attribute.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Attribute.ts @@ -128,7 +128,14 @@ export function handleAttribute( if (attributeValueIsOfType(attr.value, 'AttributeShorthand')) { // For the attribute shorthand, the name will be the mapped part - addAttribute([[attr.value[0].start, attr.value[0].end]]); + let [start, end] = [attr.value[0].start, attr.value[0].end]; + if (start === end) { + // Loose parsing mode, we have an empty attribute value, e.g. {} + // For proper intellisense we need to make this a non-empty expression. + start--; + str.overwrite(start, end, ' ', { contentOnly: true }); + } + addAttribute([[start, end]]); return; } else { let name = @@ -208,7 +215,14 @@ export function handleAttribute( addAttribute(attributeName, attributeValue); } else if (attrVal.type == 'MustacheTag') { - attributeValue.push(rangeWithTrailingPropertyAccess(str.original, attrVal.expression)); + let [start, end] = rangeWithTrailingPropertyAccess(str.original, attrVal.expression); + if (start === end) { + // Loose parsing mode, we have an empty attribute value, e.g. attr={} + // For proper intellisense we need to make this a non-empty expression. + start--; + str.overwrite(start, end, ' ', { contentOnly: true }); + } + attributeValue.push([start, end]); addAttribute(attributeName, attributeValue); } return; diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/EachBlock.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/EachBlock.ts index 51dcb950e..4b394cab3 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/EachBlock.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/EachBlock.ts @@ -1,6 +1,11 @@ import MagicString from 'magic-string'; import { BaseNode } from '../../interfaces'; -import { getEnd, transform, TransformationArray } from '../utils/node-utils'; +import { + getEnd, + isImplicitlyClosedBlock, + transform, + TransformationArray +} from '../utils/node-utils'; /** * Transform #each into a for-of loop @@ -75,10 +80,18 @@ export function handleEach(str: MagicString, eachBlock: BaseNode): void { str.overwrite(elseStart, elseEnd + 1, '}' + (arrayAndItemVarTheSame ? '}' : ''), { contentOnly: true }); - str.remove(endEach, eachBlock.end); + + if (!isImplicitlyClosedBlock(endEach, eachBlock)) { + str.remove(endEach, eachBlock.end); + } } else { - str.overwrite(endEach, eachBlock.end, '}' + (arrayAndItemVarTheSame ? '}' : ''), { - contentOnly: true - }); + const closing = '}' + (arrayAndItemVarTheSame ? '}' : ''); + if (isImplicitlyClosedBlock(endEach, eachBlock)) { + str.prependLeft(eachBlock.end, closing); + } else { + str.overwrite(endEach, eachBlock.end, closing, { + contentOnly: true + }); + } } } diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/IfElseBlock.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/IfElseBlock.ts index b3380869f..658b13317 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/IfElseBlock.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/IfElseBlock.ts @@ -1,6 +1,6 @@ import MagicString from 'magic-string'; import { Node } from 'estree-walker'; -import { withTrailingPropertyAccess } from '../utils/node-utils'; +import { isImplicitlyClosedBlock, withTrailingPropertyAccess } from '../utils/node-utils'; /** * Transforms #if and :else if to a regular if control block. @@ -18,9 +18,13 @@ export function handleIf(str: MagicString, ifBlock: Node): void { const end = str.original.indexOf('}', expressionEnd); str.overwrite(expressionEnd, end + 1, '){'); - // {/if} -> } const endif = str.original.lastIndexOf('{', ifBlock.end - 1); - str.overwrite(endif, ifBlock.end, '}'); + if (isImplicitlyClosedBlock(endif, ifBlock)) { + str.prependLeft(ifBlock.end, '}'); + } else { + // {/if} -> } + str.overwrite(endif, ifBlock.end, '}'); + } } /** diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/InlineComponent.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/InlineComponent.ts index b1bab058d..f4d6ba4f6 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/InlineComponent.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/InlineComponent.ts @@ -221,11 +221,18 @@ export class InlineComponent { ...this.endTransformation ]); } else { - const endStart = - this.str.original - .substring(this.node.start, this.node.end) - .lastIndexOf(` -> Component} this.endTransformation.push([endStart + 2, endStart + this.node.name.length + 2]); } diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Key.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Key.ts index 6f0ae9ebe..edf269d2b 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Key.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Key.ts @@ -1,6 +1,6 @@ import MagicString from 'magic-string'; import { BaseNode } from '../../interfaces'; -import { withTrailingPropertyAccess } from '../utils/node-utils'; +import { isImplicitlyClosedBlock, withTrailingPropertyAccess } from '../utils/node-utils'; /** * {#key expr}content{/key} ---> expr; content @@ -14,5 +14,7 @@ export function handleKey(str: MagicString, keyBlock: BaseNode): void { // {/key} -> const endKey = str.original.lastIndexOf('{', keyBlock.end - 1); - str.overwrite(endKey, keyBlock.end, '', { contentOnly: true }); + if (!isImplicitlyClosedBlock(endKey, keyBlock)) { + str.overwrite(endKey, keyBlock.end, '', { contentOnly: true }); + } } diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts index 701e1b83a..595248216 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts @@ -1,6 +1,6 @@ import MagicString from 'magic-string'; import { BaseNode } from '../../interfaces'; -import { transform, TransformationArray } from '../utils/node-utils'; +import { isImplicitlyClosedBlock, transform, TransformationArray } from '../utils/node-utils'; import { InlineComponent } from './InlineComponent'; import { IGNORE_POSITION_COMMENT, surroundWithIgnoreComments } from '../../utils/ignore'; import { Element } from './Element'; @@ -38,9 +38,13 @@ export function handleSnippet( ? `};return __sveltets_2_any(0)}` : `};return __sveltets_2_any(0)};`; - str.overwrite(endSnippet, snippetBlock.end, afterSnippet, { - contentOnly: true - }); + if (isImplicitlyClosedBlock(endSnippet, snippetBlock)) { + str.prependLeft(snippetBlock.end, afterSnippet); + } else { + str.overwrite(endSnippet, snippetBlock.end, afterSnippet, { + contentOnly: true + }); + } const lastParameter = snippetBlock.parameters?.at(-1); @@ -63,7 +67,23 @@ export function handleSnippet( const afterParameters = ` => { async ()${IGNORE_POSITION_COMMENT} => {`; if (isImplicitProp) { - str.overwrite(snippetBlock.start, snippetBlock.expression.start, '', { contentOnly: true }); + /** Can happen in loose parsing mode, e.g. code is currently `{#snippet }` */ + const emptyId = snippetBlock.expression.start === snippetBlock.expression.end; + + if (emptyId) { + // Give intellisense a way to map into the right position for implicit prop completion + str.overwrite(snippetBlock.start, snippetBlock.expression.start - 1, '', { + contentOnly: true + }); + str.overwrite(snippetBlock.expression.start - 1, snippetBlock.expression.start, ' ', { + contentOnly: true + }); + } else { + str.overwrite(snippetBlock.start, snippetBlock.expression.start, '', { + contentOnly: true + }); + } + const transforms: TransformationArray = ['(']; if (parameters) { @@ -82,12 +102,12 @@ export function handleSnippet( if (component instanceof InlineComponent) { component.addImplicitSnippetProp( - [snippetBlock.expression.start, snippetBlock.expression.end], + [snippetBlock.expression.start - (emptyId ? 1 : 0), snippetBlock.expression.end], transforms ); } else { component.addAttribute( - [[snippetBlock.expression.start, snippetBlock.expression.end]], + [[snippetBlock.expression.start - (emptyId ? 1 : 0), snippetBlock.expression.end]], transforms ); } diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/utils/node-utils.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/utils/node-utils.ts index d3c95164a..bbbfa2502 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/utils/node-utils.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/utils/node-utils.ts @@ -243,3 +243,19 @@ export function isTypescriptNode(node: any) { node.type === 'TSNonNullExpression' ); } + +/** + * Returns `true` if the given block is implicitly closed, which could be the case in loose parsing mode. + * E.g.: + * ```html + *
+ * {#if x} + *
+ * ``` + * @param end + * @param block + * @returns + */ +export function isImplicitlyClosedBlock(end: number, block: Node) { + return end < (block.children[block.children.length - 1]?.end ?? block.expression.end); +} diff --git a/packages/svelte2tsx/src/utils/htmlxparser.ts b/packages/svelte2tsx/src/utils/htmlxparser.ts index 3757f8452..999621d70 100644 --- a/packages/svelte2tsx/src/utils/htmlxparser.ts +++ b/packages/svelte2tsx/src/utils/htmlxparser.ts @@ -114,7 +114,7 @@ function blankVerbatimContent(htmlx: string, verbatimElements: Node[]) { export function parseHtmlx( htmlx: string, parse: typeof import('svelte/compiler').parse, - options: { emitOnTemplateError?: boolean } + options: { emitOnTemplateError?: boolean; svelte5Plus: boolean } ) { //Svelte tries to parse style and script tags which doesn't play well with typescript, so we blank them out. //HTMLx spec says they should just be retained after processing as is, so this is fine @@ -122,10 +122,16 @@ export function parseHtmlx( const deconstructed = blankVerbatimContent(htmlx, verbatimElements); //extract the html content parsed as htmlx this excludes our script and style tags - const parsingCode = options.emitOnTemplateError - ? blankPossiblyErrorOperatorOrPropertyAccess(deconstructed) - : deconstructed; - const htmlxAst = parse(parsingCode).html as any; + const parsingCode = + options.emitOnTemplateError && !options.svelte5Plus + ? blankPossiblyErrorOperatorOrPropertyAccess(deconstructed) + : deconstructed; + const htmlxAst = ( + parse( + parsingCode, + options.svelte5Plus ? ({ loose: options.emitOnTemplateError } as any) : undefined + ) as any + ).html; //restore our script and style tags as nodes to maintain validity with HTMLx for (const s of verbatimElements) { diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/editing-binding/expected-svelte5.js b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-binding/expected-svelte5.js index 7bb207fea..4e136e37c 100644 --- a/packages/svelte2tsx/test/htmlx2jsx/samples/editing-binding/expected-svelte5.js +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-binding/expected-svelte5.js @@ -1,3 +1,3 @@ - { svelteHTML.createElement("input", { });obj = __sveltets_2_any(null);} - { svelteHTML.createElement("input", { "bind:value":obj.,});/*Ωignore_startΩ*/() => obj = __sveltets_2_any(null);/*Ωignore_endΩ*/} - { const $$_tupnI0C = __sveltets_2_ensureComponent(Input); const $$_tupnI0 = new $$_tupnI0C({ target: __sveltets_2_any(), props: { value:obj.,}});/*Ωignore_startΩ*/() => obj = __sveltets_2_any(null);/*Ωignore_endΩ*/$$_tupnI0.$$bindings = 'value';} \ No newline at end of file + { svelteHTML.createElement("input", { });obj. = __sveltets_2_any(null);} + { svelteHTML.createElement("input", { "bind:value":obj.,});/*Ωignore_startΩ*/() => obj. = __sveltets_2_any(null);/*Ωignore_endΩ*/} + { const $$_tupnI0C = __sveltets_2_ensureComponent(Input); const $$_tupnI0 = new $$_tupnI0C({ target: __sveltets_2_any(), props: { value:obj.,}});/*Ωignore_startΩ*/() => obj. = __sveltets_2_any(null);/*Ωignore_endΩ*/$$_tupnI0.$$bindings = 'value';} \ No newline at end of file From def666f84213b11fe69b9e73f460d3e94244dbdd Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 12 Dec 2024 15:27:22 +0100 Subject: [PATCH 2/4] tests --- .../expected.error.json | 20 +++++++++++++++++++ .../editing-empty-expression.v5/expectedv2.js | 7 +++++++ .../editing-empty-expression.v5/input.svelte | 7 +++++++ .../expected.error.json | 20 +++++++++++++++++++ .../editing-unclosed-block.v5/expectedv2.js | 12 +++++++++++ .../editing-unclosed-block.v5/input.svelte | 12 +++++++++++ .../expected.error.json | 20 +++++++++++++++++++ .../editing-unclosed-tag.v5/expectedv2.js | 6 ++++++ .../editing-unclosed-tag.v5/input.svelte | 7 +++++++ 9 files changed, 111 insertions(+) create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/editing-empty-expression.v5/expected.error.json create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/editing-empty-expression.v5/expectedv2.js create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/editing-empty-expression.v5/input.svelte create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-block.v5/expected.error.json create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-block.v5/expectedv2.js create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-block.v5/input.svelte create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-tag.v5/expected.error.json create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-tag.v5/expectedv2.js create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-tag.v5/input.svelte diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/editing-empty-expression.v5/expected.error.json b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-empty-expression.v5/expected.error.json new file mode 100644 index 000000000..f351fbbd7 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-empty-expression.v5/expected.error.json @@ -0,0 +1,20 @@ +{ + "code": "js_parse_error", + "message": "Unexpected token\nhttps://svelte.dev/e/js_parse_error", + "filename": "(unknown)", + "start": { + "line": 1, + "column": 6, + "character": 6 + }, + "end": { + "line": 1, + "column": 6, + "character": 6 + }, + "position": [ + 6, + 6 + ], + "frame": "1:
{}
\n ^\n2: \n3:
" +} \ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/editing-empty-expression.v5/expectedv2.js b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-empty-expression.v5/expectedv2.js new file mode 100644 index 000000000..cf23d1cda --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-empty-expression.v5/expectedv2.js @@ -0,0 +1,7 @@ + { svelteHTML.createElement("div", {});; } + + { svelteHTML.createElement("div", {"attr": ,}); } + { svelteHTML.createElement("div", { ,}); } + + { const $$_tnenopmoC0C = __sveltets_2_ensureComponent(Component); new $$_tnenopmoC0C({ target: __sveltets_2_any(), props: {"prop": ,}}); Component} + { const $$_tnenopmoC0C = __sveltets_2_ensureComponent(Component); new $$_tnenopmoC0C({ target: __sveltets_2_any(), props: { ,}}); Component} \ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/editing-empty-expression.v5/input.svelte b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-empty-expression.v5/input.svelte new file mode 100644 index 000000000..aabd2d50e --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-empty-expression.v5/input.svelte @@ -0,0 +1,7 @@ +
{}
+ +
+
+ + + diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-block.v5/expected.error.json b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-block.v5/expected.error.json new file mode 100644 index 000000000..73fd4ce8d --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-block.v5/expected.error.json @@ -0,0 +1,20 @@ +{ + "code": "element_invalid_closing_tag", + "message": "`` attempted to close an element that was not open\nhttps://svelte.dev/e/element_invalid_closing_tag", + "filename": "(unknown)", + "start": { + "line": 3, + "column": 0, + "character": 20 + }, + "end": { + "line": 3, + "column": 0, + "character": 20 + }, + "position": [ + 20, + 20 + ], + "frame": "1:
\n2: {#if foo}\n3:
\n ^\n4: \n5:
" +} \ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-block.v5/expectedv2.js b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-block.v5/expectedv2.js new file mode 100644 index 000000000..b93c8d8cf --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-block.v5/expectedv2.js @@ -0,0 +1,12 @@ + { svelteHTML.createElement("div", {}); + if(foo){ +} } + + { svelteHTML.createElement("div", {}); + for(let item of __sveltets_2_ensureArray(array)){ + if(i){ +}} } + + { const $$_tnenopmoC0C = __sveltets_2_ensureComponent(Component); const $$_tnenopmoC0 = new $$_tnenopmoC0C({ target: __sveltets_2_any(), props: {f:() => { async ()/*Ωignore_positionΩ*/ => { +};return __sveltets_2_any(0)},}});/*Ωignore_startΩ*/const {f} = $$_tnenopmoC0.$$prop_def;/*Ωignore_endΩ*/ + Component} \ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-block.v5/input.svelte b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-block.v5/input.svelte new file mode 100644 index 000000000..4b4130816 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-block.v5/input.svelte @@ -0,0 +1,12 @@ +
+ {#if foo} +
+ +
+ {#each array as item} + {#if i} +
+ + + {#snippet f} + diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-tag.v5/expected.error.json b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-tag.v5/expected.error.json new file mode 100644 index 000000000..d7dea39f6 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-tag.v5/expected.error.json @@ -0,0 +1,20 @@ +{ + "code": "expected_token", + "message": "Expected token >\nhttps://svelte.dev/e/expected_token", + "filename": "(unknown)", + "start": { + "line": 3, + "column": 2, + "character": 24 + }, + "end": { + "line": 3, + "column": 2, + "character": 24 + }, + "position": [ + 24, + 24 + ], + "frame": "1:
\n2: \n ^\n4: \n5:
" +} \ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-tag.v5/expectedv2.js b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-tag.v5/expectedv2.js new file mode 100644 index 000000000..5b87c6c58 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-tag.v5/expectedv2.js @@ -0,0 +1,6 @@ + { svelteHTML.createElement("div", {}); + { const $$_pmoC1C = __sveltets_2_ensureComponent(Comp); new $$_pmoC1C({ target: __sveltets_2_any(), props: { "a":b,}}); +} } + + { svelteHTML.createElement("div", {}); + { svelteHTML.createElement("span", { "a":b,});} } \ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-tag.v5/input.svelte b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-tag.v5/input.svelte new file mode 100644 index 000000000..154202db2 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-tag.v5/input.svelte @@ -0,0 +1,7 @@ +
+ + +
+ From d5a57cb0f78ad47c7131f658b1622c62f85f06f7 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 17 Dec 2024 00:11:28 +0100 Subject: [PATCH 3/4] handle case of last tag being left open better --- .../src/htmlxtojsx_v2/nodes/AwaitPendingCatchBlock.ts | 2 +- packages/svelte2tsx/src/htmlxtojsx_v2/nodes/EachBlock.ts | 2 +- packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts | 6 +++--- .../svelte2tsx/src/htmlxtojsx_v2/nodes/InlineComponent.ts | 6 +++--- packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts | 2 +- packages/svelte2tsx/src/htmlxtojsx_v2/utils/node-utils.ts | 5 ++++- 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/AwaitPendingCatchBlock.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/AwaitPendingCatchBlock.ts index 89a539e65..d322dd9c5 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/AwaitPendingCatchBlock.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/AwaitPendingCatchBlock.ts @@ -78,5 +78,5 @@ export function handleAwait(str: MagicString, awaitBlock: BaseNode): void { transforms.push('}'); } transforms.push('}'); - transform(str, awaitBlock.start, awaitBlock.end, awaitBlock.end, transforms); + transform(str, awaitBlock.start, awaitBlock.end, transforms); } diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/EachBlock.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/EachBlock.ts index 4b394cab3..d10c96bb4 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/EachBlock.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/EachBlock.ts @@ -70,7 +70,7 @@ export function handleEach(str: MagicString, eachBlock: BaseNode): void { if (eachBlock.key) { transforms.push([eachBlock.key.start, eachBlock.key.end], ';'); } - transform(str, eachBlock.start, startEnd, startEnd, transforms); + transform(str, eachBlock.start, startEnd, transforms); const endEach = str.original.lastIndexOf('{', eachBlock.end - 1); // {/each} -> } or {:else} -> } diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts index 5b0b30285..e7d1b4a55 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts @@ -205,7 +205,7 @@ export class Element { } if (this.isSelfclosing) { - transform(this.str, this.startTagStart, this.startTagEnd, this.startTagEnd, [ + transform(this.str, this.startTagStart, this.startTagEnd, [ // 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 @@ -217,7 +217,7 @@ export class Element { ...this.endTransformation ]); } else { - transform(this.str, this.startTagStart, this.startTagEnd, this.startTagEnd, [ + transform(this.str, this.startTagStart, this.startTagEnd, [ ...slotLetTransformation, ...this.actionsTransformation, ...this.getStartTransformation(), @@ -230,7 +230,7 @@ export class Element { .lastIndexOf(`fooo

anothertag

` const endStart = tagEndIdx === -1 ? this.node.end : tagEndIdx + this.node.start; - transform(this.str, endStart, this.node.end, this.node.end, this.endTransformation); + transform(this.str, endStart, this.node.end, this.endTransformation); } } diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/InlineComponent.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/InlineComponent.ts index f4d6ba4f6..66f6d0818 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/InlineComponent.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/InlineComponent.ts @@ -207,7 +207,7 @@ export class InlineComponent { if (this.isSelfclosing) { this.endTransformation.push('}'); - transform(this.str, this.startTagStart, this.startTagEnd, this.startTagEnd, [ + transform(this.str, this.startTagStart, this.startTagEnd, [ // 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 @@ -238,7 +238,7 @@ export class InlineComponent { } this.endTransformation.push('}'); - transform(this.str, this.startTagStart, this.startTagEnd, this.startTagEnd, [ + transform(this.str, this.startTagStart, this.startTagEnd, [ // See comment above why this goes first ...namedSlotLetTransformation, ...this.startTransformation, @@ -248,7 +248,7 @@ export class InlineComponent { snippetPropVariablesDeclaration, ...defaultSlotLetTransformation ]); - transform(this.str, endStart, this.node.end, this.node.end, this.endTransformation); + transform(this.str, endStart, this.node.end, this.endTransformation); } } diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts index 595248216..7dd6633a8 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts @@ -129,7 +129,7 @@ export function handleSnippet( afterParameters ); - transform(str, snippetBlock.start, startEnd, startEnd, transforms); + transform(str, snippetBlock.start, startEnd, transforms); } } diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/utils/node-utils.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/utils/node-utils.ts index bbbfa2502..0630c71b3 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/utils/node-utils.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/utils/node-utils.ts @@ -27,7 +27,6 @@ export function transform( str: MagicString, start: number, end: number, - _xxx: number, // TODO transformations: TransformationArray ) { const moves: Array<[number, number]> = []; @@ -128,6 +127,10 @@ export function transform( } for (let i = deletePos; i < moves.length; i++) { + // Can happen when there's not enough space left at the end of an unfininished element/component tag. + // Better to leave potentially slightly disarranged code than fail loudly + if (moves[i][1] >= end && moves[i][0] <= end) break; + str.move(moves[i][0], moves[i][1], end); } } From 27f51728ec92e9188cce0c6449e9704e05795a28 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 17 Dec 2024 12:03:33 +0100 Subject: [PATCH 4/4] handle ` mapped one character too short -> adjust + if ( + !inScript && + wordInfo.word === '' && + document.getText()[originalOffset - 1] === '.' && + tsDoc.getFullText()[offset] === '.' + ) { + offset++; + } + const componentInfo = getComponentAtPosition(lang, document, tsDoc, position); const attributeContext = componentInfo && getAttributeContextAtPosition(document, position); const eventAndSlotLetCompletions = this.getEventAndSlotLetCompletions(