Skip to content

Commit c3dbda2

Browse files
committed
feat: metadata autocompletion
1 parent aff6bbe commit c3dbda2

File tree

8 files changed

+530
-55
lines changed

8 files changed

+530
-55
lines changed

extensions/vscode/build.mjs

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,60 @@
1+
import { chapterSchema, lessonSchema, partSchema, tutorialSchema } from '@tutorialkit/types';
2+
import { watch } from 'chokidar';
13
import * as esbuild from 'esbuild';
2-
import fs from 'node:fs';
34
import { execa } from 'execa';
5+
import fs from 'node:fs';
6+
import { createRequire } from 'node:module';
7+
import { zodToJsonSchema } from 'zod-to-json-schema';
48

9+
const require = createRequire(import.meta.url);
510
const production = process.argv.includes('--production');
6-
const watch = process.argv.includes('--watch');
11+
const isWatch = process.argv.includes('--watch');
712

813
async function main() {
914
const ctx = await esbuild.context({
10-
entryPoints: ['src/extension.ts'],
15+
entryPoints: {
16+
extension: 'src/extension.ts',
17+
server: './src/language-server/index.ts',
18+
},
1119
bundle: true,
1220
format: 'cjs',
1321
minify: production,
1422
sourcemap: !production,
1523
sourcesContent: false,
24+
tsconfig: './tsconfig.json',
1625
platform: 'node',
17-
outfile: 'dist/extension.js',
26+
outdir: 'dist',
27+
define: { 'process.env.NODE_ENV': production ? '"production"' : '"development"' },
1828
external: ['vscode'],
19-
logLevel: 'silent',
20-
plugins: [
21-
/* add to the end of plugins array */
22-
esbuildProblemMatcherPlugin,
23-
],
29+
plugins: [esbuildUMD2ESMPlugin],
2430
});
2531

26-
if (watch) {
32+
if (isWatch) {
33+
const buildMetadataSchemaDebounced = debounce(buildMetadataSchema);
34+
35+
watch(join(require.resolve('@tutorialkit/types'), 'dist'), {
36+
followSymlinks: false,
37+
}).on('all', (eventName, path) => {
38+
if (eventName !== 'change' && eventName !== 'add' && eventName !== 'unlink') {
39+
return;
40+
}
41+
42+
buildMetadataSchemaDebounced();
43+
});
44+
2745
await Promise.all([
2846
ctx.watch(),
29-
execa('tsc', ['--noEmit', '--watch', '--project', 'tsconfig.json'], { stdio: 'inherit', preferLocal: true }),
47+
execa('tsc', ['--noEmit', '--watch', '--preserveWatchOutput', '--project', 'tsconfig.json'], {
48+
stdio: 'inherit',
49+
preferLocal: true,
50+
}),
3051
]);
3152
} else {
3253
await ctx.rebuild();
3354
await ctx.dispose();
3455

56+
buildMetadataSchema();
57+
3558
if (production) {
3659
// rename name in package json to match extension name on store:
3760
const pkgJSON = JSON.parse(fs.readFileSync('./package.json', { encoding: 'utf8' }));
@@ -43,22 +66,24 @@ async function main() {
4366
}
4467
}
4568

69+
function buildMetadataSchema() {
70+
const schema = tutorialSchema.strict().or(partSchema.strict()).or(chapterSchema.strict()).or(lessonSchema.strict());
71+
72+
fs.mkdirSync('./dist', { recursive: true });
73+
fs.writeFileSync('./dist/schema.json', JSON.stringify(zodToJsonSchema(schema), undefined, 2), 'utf-8');
74+
}
75+
4676
/**
4777
* @type {import('esbuild').Plugin}
4878
*/
49-
const esbuildProblemMatcherPlugin = {
50-
name: 'esbuild-problem-matcher',
51-
79+
const esbuildUMD2ESMPlugin = {
80+
name: 'umd2esm',
5281
setup(build) {
53-
build.onStart(() => {
54-
console.log('[watch] build started');
55-
});
56-
build.onEnd((result) => {
57-
result.errors.forEach(({ text, location }) => {
58-
console.error(`✘ [ERROR] ${text}`);
59-
console.error(` ${location.file}:${location.line}:${location.column}:`);
60-
});
61-
console.log('[watch] build finished');
82+
build.onResolve({ filter: /^(vscode-.*-languageservice|jsonc-parser)/ }, (args) => {
83+
const pathUmdMay = require.resolve(args.path, { paths: [args.resolveDir] });
84+
const pathEsm = pathUmdMay.replace('/umd/', '/esm/').replace('\\umd\\', '\\esm\\');
85+
86+
return { path: pathEsm };
6287
});
6388
},
6489
};
@@ -67,3 +92,20 @@ main().catch((e) => {
6792
console.error(e);
6893
process.exit(1);
6994
});
95+
96+
/**
97+
* Debounce the provided function.
98+
*
99+
* @param {Function} fn Function to debounce
100+
* @param {number} duration Duration of the debounce
101+
* @returns {Function} Debounced function
102+
*/
103+
function debounce(fn, duration) {
104+
let timeoutId = 0;
105+
106+
return function () {
107+
clearTimeout(timeoutId);
108+
109+
timeoutId = setTimeout(fn.bind(this), duration, ...arguments);
110+
};
111+
}

extensions/vscode/package.json

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,21 @@
102102
"when": "view == tutorialkit-lessons-tree && viewItem == part"
103103
}
104104
]
105-
}
105+
},
106+
"languages": [
107+
{
108+
"id": "markdown",
109+
"extensions": [
110+
".md"
111+
]
112+
},
113+
{
114+
"id": "mdx",
115+
"extensions": [
116+
".mdx"
117+
]
118+
}
119+
]
106120
},
107121
"scripts": {
108122
"__esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node",
@@ -116,17 +130,27 @@
116130
"package": "pnpm run check-types && node build.mjs --production"
117131
},
118132
"devDependencies": {
119-
"@types/mocha": "^10.0.6",
133+
"@tutorialkit/types": "workspace:*",
120134
"@types/node": "18.x",
121135
"@types/vscode": "^1.80.0",
122136
"@typescript-eslint/eslint-plugin": "^7.11.0",
123137
"@typescript-eslint/parser": "^7.11.0",
138+
"chokidar": "3.6.0",
124139
"esbuild": "^0.21.5",
125140
"execa": "^9.2.0",
126-
"typescript": "^5.4.5"
141+
"typescript": "^5.4.5",
142+
"zod-to-json-schema": "3.23.1"
127143
},
128144
"dependencies": {
145+
"@volar/language-core": "2.3.4",
146+
"@volar/language-server": "2.3.4",
147+
"@volar/language-service": "2.3.4",
148+
"@volar/vscode": "2.3.4",
129149
"case-anything": "^3.1.0",
130-
"gray-matter": "^4.0.3"
150+
"gray-matter": "^4.0.3",
151+
"volar-service-yaml": "volar-2.3",
152+
"vscode-languageclient": "^9.0.1",
153+
"vscode-uri": "^3.0.8",
154+
"yaml-language-server": "1.15.0"
131155
}
132156
}

extensions/vscode/src/extension.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,49 @@
1+
import * as serverProtocol from '@volar/language-server/protocol';
2+
import { createLabsInfo } from '@volar/vscode';
13
import * as vscode from 'vscode';
24
import { useCommands } from './commands';
35
import { useLessonTree } from './views/lessonsTree';
6+
import * as lsp from 'vscode-languageclient/node';
47

58
export let extContext: vscode.ExtensionContext;
69

7-
export function activate(context: vscode.ExtensionContext) {
10+
let client: lsp.BaseLanguageClient;
11+
12+
export async function activate(context: vscode.ExtensionContext) {
813
extContext = context;
914

1015
useCommands();
1116
useLessonTree();
17+
18+
const serverModule = vscode.Uri.joinPath(context.extensionUri, 'dist', 'server.js');
19+
const runOptions = { execArgv: <string[]>[] };
20+
const debugOptions = { execArgv: ['--nolazy', '--inspect=' + 6009] };
21+
const serverOptions: lsp.ServerOptions = {
22+
run: {
23+
module: serverModule.fsPath,
24+
transport: lsp.TransportKind.ipc,
25+
options: runOptions,
26+
},
27+
debug: {
28+
module: serverModule.fsPath,
29+
transport: lsp.TransportKind.ipc,
30+
options: debugOptions,
31+
},
32+
};
33+
const clientOptions: lsp.LanguageClientOptions = {
34+
documentSelector: [{ language: 'markdown' }, { language: 'mdx' }],
35+
initializationOptions: {},
36+
};
37+
client = new lsp.LanguageClient('tutorialkit-language-server', 'TutorialKit', serverOptions, clientOptions);
38+
39+
await client.start();
40+
41+
const labsInfo = createLabsInfo(serverProtocol);
42+
labsInfo.addLanguageClient(client);
43+
44+
return labsInfo.extensionExports;
1245
}
1346

1447
export function deactivate() {
15-
// do nothing
48+
return client?.stop();
1649
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { createConnection, createServer, createSimpleProject } from '@volar/language-server/node';
2+
import { create as createYamlService } from 'volar-service-yaml';
3+
import { SchemaPriority } from 'yaml-language-server';
4+
import { frontmatterPlugin } from './languagePlugin';
5+
import { readSchema } from './schema';
6+
7+
const connection = createConnection();
8+
const server = createServer(connection);
9+
10+
connection.listen();
11+
12+
// server.watchFiles(["src/content/**/*.md", "src/content/**/*.mdx"]);
13+
14+
connection.onInitialize((params) => {
15+
connection.console.debug('CONNECTED' + params.capabilities);
16+
17+
const yamlService = createYamlService({
18+
// onDidChangeLanguageSettings(listener, context) {
19+
// const watcher = watch(context., () => {
20+
// listener();
21+
// });
22+
23+
// return {
24+
// dispose() {
25+
// watcher.dispose();
26+
// },
27+
// };
28+
// },
29+
30+
getLanguageSettings(_context) {
31+
// connection.console.debug('GET LANGUAGE SERVICE');
32+
// connection.console.debug('ENV ' + JSON.stringify(context.env.workspaceFolders));
33+
34+
// const workspacePath = path.join(context.env.workspaceFolders?.[0].fsPath, );
35+
36+
const schema = readSchema();
37+
38+
return {
39+
completion: true,
40+
validate: true,
41+
hover: true,
42+
format: true,
43+
yamlVersion: '1.2',
44+
isKubernetes: false,
45+
schemas: [
46+
{
47+
uri: 'https://tutorialkit.dev/schema.json',
48+
schema,
49+
// schema: {
50+
// type: 'object',
51+
// properties: {
52+
// test: {
53+
// type: 'number',
54+
// description: 'A test property',
55+
// },
56+
// foobar: {
57+
// type: 'string',
58+
// description: 'A string property',
59+
// },
60+
// },
61+
// required: ['test', 'foobar'],
62+
// },
63+
fileMatch: [
64+
'**/*',
65+
66+
// TODO: those don't work
67+
'src/content/*.md',
68+
'src/content/**/*.md',
69+
'src/content/**/*.mdx',
70+
],
71+
priority: SchemaPriority.Settings,
72+
},
73+
],
74+
};
75+
},
76+
});
77+
78+
delete yamlService.capabilities.codeLensProvider;
79+
80+
return server.initialize(
81+
params,
82+
createSimpleProject([frontmatterPlugin(connection.console.debug.bind(connection.console.debug))]),
83+
[yamlService],
84+
);
85+
});
86+
87+
connection.onInitialized(server.initialized);
88+
89+
connection.onShutdown(server.shutdown);

0 commit comments

Comments
 (0)