Skip to content

Commit

Permalink
feat: make use of loose Svelte parser and provide better intellisense (
Browse files Browse the repository at this point in the history
…#2631)

This adds support for the Svelte parser with its new loose mode (sveltejs/svelte#14691) 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.
  • Loading branch information
dummdidumm authored Dec 17, 2024
1 parent 6e77f92 commit ac10174
Show file tree
Hide file tree
Showing 21 changed files with 249 additions and 43 deletions.
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

0 comments on commit ac10174

Please sign in to comment.