Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make use of loose Svelte parser and provide better intellisense #2631

Merged
merged 6 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionRe
}

const originalOffset = document.offsetAt(position);
const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position));
let offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position));

if (isJsDocTriggerCharacter) {
return getJsDocTemplateCompletion(tsDoc, langForSyntheticOperations, filePath, offset);
Expand Down Expand Up @@ -204,6 +204,16 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionRe
return null;
}

// Special case: completion at `<Comp.` -> 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(
Expand Down
18 changes: 16 additions & 2 deletions packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
25 changes: 19 additions & 6 deletions packages/svelte2tsx/src/htmlxtojsx_v2/nodes/EachBlock.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -65,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} -> }
Expand All @@ -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
});
}
}
}
6 changes: 3 additions & 3 deletions packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
// <div let:xx {x} /> means "use the x of let:x", and without a separate
// block scope this would give a "used before defined" error
Expand All @@ -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(),
Expand All @@ -230,7 +230,7 @@ export class Element {
.lastIndexOf(`</${this.node.name}`);
// tagEndIdx === -1 happens in situations of unclosed tags like `<p>fooo <p>anothertag</p>`
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);
}
}

Expand Down
10 changes: 7 additions & 3 deletions packages/svelte2tsx/src/htmlxtojsx_v2/nodes/IfElseBlock.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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, '}');
}
}

/**
Expand Down
23 changes: 15 additions & 8 deletions packages/svelte2tsx/src/htmlxtojsx_v2/nodes/InlineComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
// <Comp let:xx {x} /> means "use the x of let:x", and without a separate
// block scope this would give a "used before defined" error
Expand All @@ -221,17 +221,24 @@ export class InlineComponent {
...this.endTransformation
]);
} else {
const endStart =
this.str.original
.substring(this.node.start, this.node.end)
.lastIndexOf(`</${this.node.name}`) + this.node.start;
if (!this.node.name.startsWith('svelte:')) {
let endStart = this.str.original
.substring(this.node.start, this.node.end)
.lastIndexOf(`</${this.node.name}`);
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;
} else {
endStart += this.node.start;
}

if (!this.node.name.startsWith('svelte:') && endStart !== this.node.end) {
// Ensure the end tag is mapped, too. </Component> -> Component}
this.endTransformation.push([endStart + 2, endStart + this.node.name.length + 2]);
}
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,
Expand All @@ -241,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);
}
}

Expand Down
6 changes: 4 additions & 2 deletions packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Key.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 });
}
}
36 changes: 28 additions & 8 deletions packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);

Expand All @@ -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) {
Expand All @@ -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
);
}
Expand All @@ -109,7 +129,7 @@ export function handleSnippet(
afterParameters
);

transform(str, snippetBlock.start, startEnd, startEnd, transforms);
transform(str, snippetBlock.start, startEnd, transforms);
}
}

Expand Down
21 changes: 20 additions & 1 deletion packages/svelte2tsx/src/htmlxtojsx_v2/utils/node-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export function transform(
str: MagicString,
start: number,
end: number,
_xxx: number, // TODO
transformations: TransformationArray
) {
const moves: Array<[number, number]> = [];
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -243,3 +246,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
* <div>
* {#if x}
* </div>
* ```
* @param end
* @param block
* @returns
*/
export function isImplicitlyClosedBlock(end: number, block: Node) {
return end < (block.children[block.children.length - 1]?.end ?? block.expression.end);
}
16 changes: 11 additions & 5 deletions packages/svelte2tsx/src/utils/htmlxparser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,18 +114,24 @@ 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
const verbatimElements = findVerbatimElements(htmlx);
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) {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading