Skip to content

Commit ac10174

Browse files
authored
feat: make use of loose Svelte parser and provide better intellisense (#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.
1 parent 6e77f92 commit ac10174

File tree

21 files changed

+249
-43
lines changed

21 files changed

+249
-43
lines changed

packages/language-server/src/plugins/typescript/features/CompletionProvider.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionRe
158158
}
159159

160160
const originalOffset = document.offsetAt(position);
161-
const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position));
161+
let offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position));
162162

163163
if (isJsDocTriggerCharacter) {
164164
return getJsDocTemplateCompletion(tsDoc, langForSyntheticOperations, filePath, offset);
@@ -204,6 +204,16 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionRe
204204
return null;
205205
}
206206

207+
// Special case: completion at `<Comp.` -> mapped one character too short -> adjust
208+
if (
209+
!inScript &&
210+
wordInfo.word === '' &&
211+
document.getText()[originalOffset - 1] === '.' &&
212+
tsDoc.getFullText()[offset] === '.'
213+
) {
214+
offset++;
215+
}
216+
207217
const componentInfo = getComponentAtPosition(lang, document, tsDoc, position);
208218
const attributeContext = componentInfo && getAttributeContextAtPosition(document, position);
209219
const eventAndSlotLetCompletions = this.getEventAndSlotLetCompletions(

packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Attribute.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,14 @@ export function handleAttribute(
128128

129129
if (attributeValueIsOfType(attr.value, 'AttributeShorthand')) {
130130
// For the attribute shorthand, the name will be the mapped part
131-
addAttribute([[attr.value[0].start, attr.value[0].end]]);
131+
let [start, end] = [attr.value[0].start, attr.value[0].end];
132+
if (start === end) {
133+
// Loose parsing mode, we have an empty attribute value, e.g. {}
134+
// For proper intellisense we need to make this a non-empty expression.
135+
start--;
136+
str.overwrite(start, end, ' ', { contentOnly: true });
137+
}
138+
addAttribute([[start, end]]);
132139
return;
133140
} else {
134141
let name =
@@ -208,7 +215,14 @@ export function handleAttribute(
208215

209216
addAttribute(attributeName, attributeValue);
210217
} else if (attrVal.type == 'MustacheTag') {
211-
attributeValue.push(rangeWithTrailingPropertyAccess(str.original, attrVal.expression));
218+
let [start, end] = rangeWithTrailingPropertyAccess(str.original, attrVal.expression);
219+
if (start === end) {
220+
// Loose parsing mode, we have an empty attribute value, e.g. attr={}
221+
// For proper intellisense we need to make this a non-empty expression.
222+
start--;
223+
str.overwrite(start, end, ' ', { contentOnly: true });
224+
}
225+
attributeValue.push([start, end]);
212226
addAttribute(attributeName, attributeValue);
213227
}
214228
return;

packages/svelte2tsx/src/htmlxtojsx_v2/nodes/AwaitPendingCatchBlock.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,5 @@ export function handleAwait(str: MagicString, awaitBlock: BaseNode): void {
7878
transforms.push('}');
7979
}
8080
transforms.push('}');
81-
transform(str, awaitBlock.start, awaitBlock.end, awaitBlock.end, transforms);
81+
transform(str, awaitBlock.start, awaitBlock.end, transforms);
8282
}

packages/svelte2tsx/src/htmlxtojsx_v2/nodes/EachBlock.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import MagicString from 'magic-string';
22
import { BaseNode } from '../../interfaces';
3-
import { getEnd, transform, TransformationArray } from '../utils/node-utils';
3+
import {
4+
getEnd,
5+
isImplicitlyClosedBlock,
6+
transform,
7+
TransformationArray
8+
} from '../utils/node-utils';
49

510
/**
611
* Transform #each into a for-of loop
@@ -65,7 +70,7 @@ export function handleEach(str: MagicString, eachBlock: BaseNode): void {
6570
if (eachBlock.key) {
6671
transforms.push([eachBlock.key.start, eachBlock.key.end], ';');
6772
}
68-
transform(str, eachBlock.start, startEnd, startEnd, transforms);
73+
transform(str, eachBlock.start, startEnd, transforms);
6974

7075
const endEach = str.original.lastIndexOf('{', eachBlock.end - 1);
7176
// {/each} -> } or {:else} -> }
@@ -75,10 +80,18 @@ export function handleEach(str: MagicString, eachBlock: BaseNode): void {
7580
str.overwrite(elseStart, elseEnd + 1, '}' + (arrayAndItemVarTheSame ? '}' : ''), {
7681
contentOnly: true
7782
});
78-
str.remove(endEach, eachBlock.end);
83+
84+
if (!isImplicitlyClosedBlock(endEach, eachBlock)) {
85+
str.remove(endEach, eachBlock.end);
86+
}
7987
} else {
80-
str.overwrite(endEach, eachBlock.end, '}' + (arrayAndItemVarTheSame ? '}' : ''), {
81-
contentOnly: true
82-
});
88+
const closing = '}' + (arrayAndItemVarTheSame ? '}' : '');
89+
if (isImplicitlyClosedBlock(endEach, eachBlock)) {
90+
str.prependLeft(eachBlock.end, closing);
91+
} else {
92+
str.overwrite(endEach, eachBlock.end, closing, {
93+
contentOnly: true
94+
});
95+
}
8396
}
8497
}

packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ export class Element {
205205
}
206206

207207
if (this.isSelfclosing) {
208-
transform(this.str, this.startTagStart, this.startTagEnd, this.startTagEnd, [
208+
transform(this.str, this.startTagStart, this.startTagEnd, [
209209
// Named slot transformations go first inside a outer block scope because
210210
// <div let:xx {x} /> means "use the x of let:x", and without a separate
211211
// block scope this would give a "used before defined" error
@@ -217,7 +217,7 @@ export class Element {
217217
...this.endTransformation
218218
]);
219219
} else {
220-
transform(this.str, this.startTagStart, this.startTagEnd, this.startTagEnd, [
220+
transform(this.str, this.startTagStart, this.startTagEnd, [
221221
...slotLetTransformation,
222222
...this.actionsTransformation,
223223
...this.getStartTransformation(),
@@ -230,7 +230,7 @@ export class Element {
230230
.lastIndexOf(`</${this.node.name}`);
231231
// tagEndIdx === -1 happens in situations of unclosed tags like `<p>fooo <p>anothertag</p>`
232232
const endStart = tagEndIdx === -1 ? this.node.end : tagEndIdx + this.node.start;
233-
transform(this.str, endStart, this.node.end, this.node.end, this.endTransformation);
233+
transform(this.str, endStart, this.node.end, this.endTransformation);
234234
}
235235
}
236236

packages/svelte2tsx/src/htmlxtojsx_v2/nodes/IfElseBlock.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import MagicString from 'magic-string';
22
import { Node } from 'estree-walker';
3-
import { withTrailingPropertyAccess } from '../utils/node-utils';
3+
import { isImplicitlyClosedBlock, withTrailingPropertyAccess } from '../utils/node-utils';
44

55
/**
66
* Transforms #if and :else if to a regular if control block.
@@ -18,9 +18,13 @@ export function handleIf(str: MagicString, ifBlock: Node): void {
1818
const end = str.original.indexOf('}', expressionEnd);
1919
str.overwrite(expressionEnd, end + 1, '){');
2020

21-
// {/if} -> }
2221
const endif = str.original.lastIndexOf('{', ifBlock.end - 1);
23-
str.overwrite(endif, ifBlock.end, '}');
22+
if (isImplicitlyClosedBlock(endif, ifBlock)) {
23+
str.prependLeft(ifBlock.end, '}');
24+
} else {
25+
// {/if} -> }
26+
str.overwrite(endif, ifBlock.end, '}');
27+
}
2428
}
2529

2630
/**

packages/svelte2tsx/src/htmlxtojsx_v2/nodes/InlineComponent.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ export class InlineComponent {
207207

208208
if (this.isSelfclosing) {
209209
this.endTransformation.push('}');
210-
transform(this.str, this.startTagStart, this.startTagEnd, this.startTagEnd, [
210+
transform(this.str, this.startTagStart, this.startTagEnd, [
211211
// Named slot transformations go first inside a outer block scope because
212212
// <Comp let:xx {x} /> means "use the x of let:x", and without a separate
213213
// block scope this would give a "used before defined" error
@@ -221,17 +221,24 @@ export class InlineComponent {
221221
...this.endTransformation
222222
]);
223223
} else {
224-
const endStart =
225-
this.str.original
226-
.substring(this.node.start, this.node.end)
227-
.lastIndexOf(`</${this.node.name}`) + this.node.start;
228-
if (!this.node.name.startsWith('svelte:')) {
224+
let endStart = this.str.original
225+
.substring(this.node.start, this.node.end)
226+
.lastIndexOf(`</${this.node.name}`);
227+
if (endStart === -1) {
228+
// Can happen in loose parsing mode when there's no closing tag
229+
endStart = this.node.end;
230+
this.startTagEnd = this.node.end - 1;
231+
} else {
232+
endStart += this.node.start;
233+
}
234+
235+
if (!this.node.name.startsWith('svelte:') && endStart !== this.node.end) {
229236
// Ensure the end tag is mapped, too. </Component> -> Component}
230237
this.endTransformation.push([endStart + 2, endStart + this.node.name.length + 2]);
231238
}
232239
this.endTransformation.push('}');
233240

234-
transform(this.str, this.startTagStart, this.startTagEnd, this.startTagEnd, [
241+
transform(this.str, this.startTagStart, this.startTagEnd, [
235242
// See comment above why this goes first
236243
...namedSlotLetTransformation,
237244
...this.startTransformation,
@@ -241,7 +248,7 @@ export class InlineComponent {
241248
snippetPropVariablesDeclaration,
242249
...defaultSlotLetTransformation
243250
]);
244-
transform(this.str, endStart, this.node.end, this.node.end, this.endTransformation);
251+
transform(this.str, endStart, this.node.end, this.endTransformation);
245252
}
246253
}
247254

packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Key.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import MagicString from 'magic-string';
22
import { BaseNode } from '../../interfaces';
3-
import { withTrailingPropertyAccess } from '../utils/node-utils';
3+
import { isImplicitlyClosedBlock, withTrailingPropertyAccess } from '../utils/node-utils';
44

55
/**
66
* {#key expr}content{/key} ---> expr; content
@@ -14,5 +14,7 @@ export function handleKey(str: MagicString, keyBlock: BaseNode): void {
1414

1515
// {/key} ->
1616
const endKey = str.original.lastIndexOf('{', keyBlock.end - 1);
17-
str.overwrite(endKey, keyBlock.end, '', { contentOnly: true });
17+
if (!isImplicitlyClosedBlock(endKey, keyBlock)) {
18+
str.overwrite(endKey, keyBlock.end, '', { contentOnly: true });
19+
}
1820
}

packages/svelte2tsx/src/htmlxtojsx_v2/nodes/SnippetBlock.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import MagicString from 'magic-string';
22
import { BaseNode } from '../../interfaces';
3-
import { transform, TransformationArray } from '../utils/node-utils';
3+
import { isImplicitlyClosedBlock, transform, TransformationArray } from '../utils/node-utils';
44
import { InlineComponent } from './InlineComponent';
55
import { IGNORE_POSITION_COMMENT, surroundWithIgnoreComments } from '../../utils/ignore';
66
import { Element } from './Element';
@@ -38,9 +38,13 @@ export function handleSnippet(
3838
? `};return __sveltets_2_any(0)}`
3939
: `};return __sveltets_2_any(0)};`;
4040

41-
str.overwrite(endSnippet, snippetBlock.end, afterSnippet, {
42-
contentOnly: true
43-
});
41+
if (isImplicitlyClosedBlock(endSnippet, snippetBlock)) {
42+
str.prependLeft(snippetBlock.end, afterSnippet);
43+
} else {
44+
str.overwrite(endSnippet, snippetBlock.end, afterSnippet, {
45+
contentOnly: true
46+
});
47+
}
4448

4549
const lastParameter = snippetBlock.parameters?.at(-1);
4650

@@ -63,7 +67,23 @@ export function handleSnippet(
6367
const afterParameters = ` => { async ()${IGNORE_POSITION_COMMENT} => {`;
6468

6569
if (isImplicitProp) {
66-
str.overwrite(snippetBlock.start, snippetBlock.expression.start, '', { contentOnly: true });
70+
/** Can happen in loose parsing mode, e.g. code is currently `{#snippet }` */
71+
const emptyId = snippetBlock.expression.start === snippetBlock.expression.end;
72+
73+
if (emptyId) {
74+
// Give intellisense a way to map into the right position for implicit prop completion
75+
str.overwrite(snippetBlock.start, snippetBlock.expression.start - 1, '', {
76+
contentOnly: true
77+
});
78+
str.overwrite(snippetBlock.expression.start - 1, snippetBlock.expression.start, ' ', {
79+
contentOnly: true
80+
});
81+
} else {
82+
str.overwrite(snippetBlock.start, snippetBlock.expression.start, '', {
83+
contentOnly: true
84+
});
85+
}
86+
6787
const transforms: TransformationArray = ['('];
6888

6989
if (parameters) {
@@ -82,12 +102,12 @@ export function handleSnippet(
82102

83103
if (component instanceof InlineComponent) {
84104
component.addImplicitSnippetProp(
85-
[snippetBlock.expression.start, snippetBlock.expression.end],
105+
[snippetBlock.expression.start - (emptyId ? 1 : 0), snippetBlock.expression.end],
86106
transforms
87107
);
88108
} else {
89109
component.addAttribute(
90-
[[snippetBlock.expression.start, snippetBlock.expression.end]],
110+
[[snippetBlock.expression.start - (emptyId ? 1 : 0), snippetBlock.expression.end]],
91111
transforms
92112
);
93113
}
@@ -109,7 +129,7 @@ export function handleSnippet(
109129
afterParameters
110130
);
111131

112-
transform(str, snippetBlock.start, startEnd, startEnd, transforms);
132+
transform(str, snippetBlock.start, startEnd, transforms);
113133
}
114134
}
115135

packages/svelte2tsx/src/htmlxtojsx_v2/utils/node-utils.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ export function transform(
2727
str: MagicString,
2828
start: number,
2929
end: number,
30-
_xxx: number, // TODO
3130
transformations: TransformationArray
3231
) {
3332
const moves: Array<[number, number]> = [];
@@ -128,6 +127,10 @@ export function transform(
128127
}
129128

130129
for (let i = deletePos; i < moves.length; i++) {
130+
// Can happen when there's not enough space left at the end of an unfininished element/component tag.
131+
// Better to leave potentially slightly disarranged code than fail loudly
132+
if (moves[i][1] >= end && moves[i][0] <= end) break;
133+
131134
str.move(moves[i][0], moves[i][1], end);
132135
}
133136
}
@@ -243,3 +246,19 @@ export function isTypescriptNode(node: any) {
243246
node.type === 'TSNonNullExpression'
244247
);
245248
}
249+
250+
/**
251+
* Returns `true` if the given block is implicitly closed, which could be the case in loose parsing mode.
252+
* E.g.:
253+
* ```html
254+
* <div>
255+
* {#if x}
256+
* </div>
257+
* ```
258+
* @param end
259+
* @param block
260+
* @returns
261+
*/
262+
export function isImplicitlyClosedBlock(end: number, block: Node) {
263+
return end < (block.children[block.children.length - 1]?.end ?? block.expression.end);
264+
}

0 commit comments

Comments
 (0)