Skip to content

Commit 436007b

Browse files
authored
feat: autocompletion (#384)
Fixes part of #291 - {#each}, {#if}, {#await}, {#key} - @html, @debug, @const - data-sveltekit- attributes - on: directives - bind: directives autocompletion of import source has not yet been implemented.
1 parent a9c544d commit 436007b

File tree

3 files changed

+715
-4
lines changed

3 files changed

+715
-4
lines changed

src/routes/tutorial/[slug]/Editor.svelte

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import { afterNavigate, beforeNavigate } from '$app/navigation';
1818
import { files, selected_file, selected_name, update_file } from './state.js';
1919
import { warnings } from './adapter.js';
20+
import { autocomplete_for_svelte } from './autocompletion.js';
2021
import './codemirror.css';
2122
2223
/** @type {HTMLDivElement} */
@@ -127,16 +128,16 @@
127128
let lang;
128129
129130
if (file.name.endsWith('.js') || file.name.endsWith('.json')) {
130-
lang = javascript();
131+
lang = [javascript()];
131132
} else if (file.name.endsWith('.html')) {
132-
lang = html();
133+
lang = [html()];
133134
} else if (file.name.endsWith('.svelte')) {
134-
lang = svelte();
135+
lang = [svelte(), ...autocomplete_for_svelte()];
135136
}
136137
137138
state = EditorState.create({
138139
doc: file.contents,
139-
extensions: lang ? [...extensions, lang] : extensions
140+
extensions: lang ? [...extensions, ...lang] : extensions
140141
});
141142
142143
editor_states.set(file.name, state);
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import { svelteLanguage } from '@replit/codemirror-lang-svelte';
2+
import { javascriptLanguage } from '@codemirror/lang-javascript';
3+
import { syntaxTree } from '@codemirror/language';
4+
import { snippetCompletion } from '@codemirror/autocomplete';
5+
import {
6+
addAttributes,
7+
svelteAttributes,
8+
svelteTags,
9+
sveltekitAttributes,
10+
svelteEvents
11+
} from './autocompletionDataProvider.js';
12+
13+
const logic_block_snippets = [
14+
snippetCompletion('#if ${}}\n\n{/if', { label: '#if', type: 'keyword' }),
15+
snippetCompletion('#each ${} as }\n\n{/each', {
16+
label: '#each',
17+
type: 'keyword'
18+
}),
19+
snippetCompletion('#await ${} then }\n\n{/await', {
20+
label: '#await then',
21+
type: 'keyword'
22+
}),
23+
snippetCompletion('#await ${}}\n\n{:then }\n\n{/await', {
24+
label: '#await :then',
25+
type: 'keyword'
26+
}),
27+
snippetCompletion('#key ${}}\n\n{/key', { label: '#key', type: 'keyword' })
28+
];
29+
30+
const special_tag_snippets = [
31+
snippetCompletion('@html ${}', { label: '@html', type: 'keyword' }),
32+
snippetCompletion('@debug ${}', { label: '@debug', type: 'keyword' }),
33+
snippetCompletion('@const ${}', { label: '@const', type: 'keyword' })
34+
];
35+
36+
/**
37+
* @param {import('@codemirror/autocomplete').CompletionContext} context
38+
* @param {import("@lezer/common").SyntaxNode} node
39+
*/
40+
function completion_for_block(context, node) {
41+
const prefix = context.state.doc.sliceString(node.from, node.from + 1);
42+
43+
const from = node.from;
44+
const to = context.pos;
45+
46+
const type = 'keyword';
47+
48+
if (prefix === '/') {
49+
/** @param {string} label */
50+
const completion = (label) => ({ from, to, options: [{ label, type }], validFor: /^\/\w*$/ });
51+
52+
const parent = node.parent;
53+
const block = node.parent?.parent;
54+
55+
if (parent?.name === 'EachBlockClose' || block?.name === 'EachBlock') {
56+
return completion('/each');
57+
} else if (parent?.name === 'IfBlockClose' || block?.name === 'IfBlock') {
58+
return completion('/if');
59+
} else if (parent?.name === 'AwaitBlockClose' || block?.name === 'AwaitBlock') {
60+
return completion('/await');
61+
} else if (parent?.name === 'KeyBlockClose' || block?.name === 'KeyBlock') {
62+
return completion('/key');
63+
}
64+
} else if (prefix === ':') {
65+
/** @param {import('@codemirror/autocomplete').Completion[]} options */
66+
const completion = (options) => ({ from, to, options, validFor: /^\:\w*$/ });
67+
68+
const parent = node.parent;
69+
const block = node.parent?.parent;
70+
71+
if (parent?.name === 'ElseBlock' || block?.name === 'IfBlock') {
72+
return completion([
73+
{ label: ':else', type },
74+
{ label: ':else if ', type }
75+
]);
76+
} else if (parent?.name === 'ThenBlock' || block?.name === 'AwaitBlock') {
77+
return completion([
78+
{ label: ':then', type },
79+
{ label: ':catch', type }
80+
]);
81+
}
82+
} else if (prefix === '#') {
83+
return { from, to, options: logic_block_snippets, validFor: /^#(\w)*$/ };
84+
} else if (prefix === '@') {
85+
return { from, to, options: special_tag_snippets, validFor: /^@(\w)*$/ };
86+
}
87+
88+
return null;
89+
}
90+
91+
const options_for_svelte_events = svelteEvents.map((event) =>
92+
snippetCompletion(event.name + '={${}}', {
93+
label: event.name,
94+
info: event.description,
95+
type: 'keyword'
96+
})
97+
);
98+
99+
const options_for_sveltekit_attributes = sveltekitAttributes.map((attr) => ({
100+
label: attr.name,
101+
info: attr.description,
102+
type: 'keyword'
103+
}));
104+
105+
const options_for_sveltekit_attr_values = sveltekitAttributes.reduce((prev, cur) => {
106+
prev[cur.name] = cur.values.map((value) => ({ label: value.name, type: 'keyword' }));
107+
return prev;
108+
}, /** @type {Record<string, import('@codemirror/autocomplete').Completion[]>} */ ({}));
109+
110+
/**
111+
* @param {{ name: string, description?: string}[]} attributes
112+
*/
113+
function snippet_for_attribute(attributes) {
114+
return attributes.map((attr) =>
115+
snippetCompletion(attr.name + '={${}}', {
116+
label: attr.name,
117+
info: attr.description,
118+
type: 'keyword'
119+
})
120+
);
121+
}
122+
123+
const options_for_svelte_attributes = snippet_for_attribute(svelteAttributes);
124+
125+
const options_for_svelte_tags = svelteTags.reduce((tags, tag) => {
126+
tags[tag.name] = snippet_for_attribute(tag.attributes);
127+
return tags;
128+
}, /** @type {Record<string, import('@codemirror/autocomplete').Completion[]>} */ ({}));
129+
130+
/**
131+
* @param {import('@codemirror/autocomplete').CompletionContext} context
132+
* @param {import("@lezer/common").SyntaxNode} node
133+
*/
134+
function completion_for_attributes(context, node) {
135+
/** @param {import('@codemirror/autocomplete').Completion[]} options */
136+
const completion = (options) => {
137+
return { from: node.from, to: context.pos, options, validFor: /^\w*$/ };
138+
};
139+
140+
const global_options = [
141+
...options_for_svelte_events,
142+
...options_for_svelte_attributes,
143+
...options_for_sveltekit_attributes
144+
];
145+
146+
if (node.parent?.parent?.firstChild?.nextSibling?.name === 'SvelteElementName') {
147+
const tag_node = node.parent.parent.firstChild.nextSibling;
148+
const tag = context.state.doc.sliceString(tag_node.from, tag_node.to);
149+
return completion([...global_options, ...options_for_svelte_tags[tag]]);
150+
} else if (node.parent?.parent?.firstChild?.nextSibling?.name === 'TagName') {
151+
const tag_node = node.parent.parent.firstChild.nextSibling;
152+
const tag = context.state.doc.sliceString(tag_node.from, tag_node.to);
153+
if (addAttributes[tag]) {
154+
const completions_attributes = snippet_for_attribute(addAttributes[tag]);
155+
return completion([...global_options, ...completions_attributes]);
156+
}
157+
}
158+
159+
return completion(global_options);
160+
}
161+
162+
/**
163+
* @param {import('@codemirror/autocomplete').CompletionContext} context
164+
* @param {import("@lezer/common").SyntaxNode} node
165+
* @param {string} attr
166+
*/
167+
function completion_for_sveltekit_attr_values(context, node, attr) {
168+
const options = options_for_sveltekit_attr_values[attr];
169+
if (options) {
170+
return {
171+
from: node.name === 'AttributeValueContent' ? node.from : node.from + 1,
172+
to: context.pos,
173+
options,
174+
validFor: /^\w*$/
175+
};
176+
}
177+
178+
return null;
179+
}
180+
181+
/**
182+
* @param {import('@codemirror/autocomplete').CompletionContext} context
183+
* @returns {import('@codemirror/autocomplete').CompletionResult | null}
184+
*/
185+
function completion_for_markup(context) {
186+
const node_before = syntaxTree(context.state).resolveInner(context.pos, -1);
187+
188+
if (node_before.name === 'BlockPrefix') {
189+
return completion_for_block(context, node_before);
190+
} else if (node_before.prevSibling?.name === 'BlockPrefix') {
191+
return completion_for_block(context, node_before.prevSibling);
192+
} else if (node_before.name === 'AttributeName') {
193+
return completion_for_attributes(context, node_before);
194+
} else if (
195+
node_before.name === 'DirectiveOn' ||
196+
node_before.name === 'DirectiveBind' ||
197+
node_before.name === 'DirectiveTarget'
198+
) {
199+
if (node_before.parent) {
200+
return completion_for_attributes(context, node_before.parent);
201+
}
202+
} else if (node_before.parent?.name === 'AttributeValue') {
203+
if (node_before.parent?.parent?.firstChild) {
204+
const attr_name_node = node_before.parent.parent.firstChild;
205+
const attr_name = context.state.doc.sliceString(attr_name_node.from, attr_name_node.to);
206+
207+
if (attr_name.startsWith('data-sveltekit-')) {
208+
return completion_for_sveltekit_attr_values(context, node_before, attr_name);
209+
}
210+
}
211+
}
212+
213+
return null;
214+
}
215+
216+
/**
217+
* @param {import('@codemirror/autocomplete').CompletionContext} context
218+
* @returns {import('@codemirror/autocomplete').CompletionResult | null}
219+
*/
220+
function completion_for_javascript(context) {
221+
// TODO autocompletion for import source
222+
223+
return null;
224+
}
225+
226+
export function autocomplete_for_svelte() {
227+
return [
228+
svelteLanguage.data.of({
229+
autocomplete: completion_for_markup
230+
}),
231+
javascriptLanguage.data.of({
232+
autocomplete: completion_for_javascript
233+
})
234+
];
235+
}

0 commit comments

Comments
 (0)