├── .npmrc
├── .gitignore
├── resources
├── tutorialkit-icon.png
├── tutorialkit-screenshot.png
└── icons
│ ├── dark
│ ├── chapter.svg
│ └── lesson.svg
│ └── light
│ ├── chapter.svg
│ └── lesson.svg
├── .vscode-test.mjs
├── src
├── commands
│ ├── tutorialkit.refresh.ts
│ ├── tutorialkit.load-tutorial.ts
│ ├── tutorialkit.goto.ts
│ ├── tutorialkit.select-tutorial.ts
│ ├── tutorialkit.initialize.ts
│ ├── index.ts
│ └── tutorialkit.add.ts
├── extension.ts
├── models
│ └── Lesson.ts
├── utils
│ ├── getIcon.ts
│ └── isTutorialKit.ts
├── test
│ └── extension.test.ts
└── views
│ └── lessonsTree.ts
├── .vscodeignore
├── .vscode
├── extensions.json
├── settings.json
├── launch.json
└── tasks.json
├── CHANGELOG.md
├── tsconfig.json
├── .eslintrc.json
├── LICENSE
├── esbuild.js
├── README.md
├── vsc-extension-quickstart.md
└── package.json
/.npmrc:
--------------------------------------------------------------------------------
1 | enable-pre-post-scripts = true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | out
2 | dist
3 | node_modules
4 | .vscode-test/
5 | *.vsix
6 |
--------------------------------------------------------------------------------
/resources/tutorialkit-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackblitz/tutorialkit-extension/main/resources/tutorialkit-icon.png
--------------------------------------------------------------------------------
/resources/tutorialkit-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackblitz/tutorialkit-extension/main/resources/tutorialkit-screenshot.png
--------------------------------------------------------------------------------
/.vscode-test.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from '@vscode/test-cli';
2 |
3 | export default defineConfig({
4 | files: 'out/test/**/*.test.js',
5 | });
6 |
--------------------------------------------------------------------------------
/src/commands/tutorialkit.refresh.ts:
--------------------------------------------------------------------------------
1 | import { getLessonsTreeDataProvider } from '../views/lessonsTree';
2 |
3 | export default () => {
4 | getLessonsTreeDataProvider().refresh();
5 | };
6 |
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .vscode/**
2 | .vscode-test/**
3 | src/**
4 | .gitignore
5 | .yarnrc
6 | vsc-extension-quickstart.md
7 | **/tsconfig.json
8 | **/.eslintrc.json
9 | **/*.map
10 | **/*.ts
11 | **/.vscode-test.*
12 | node_modules
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See http://go.microsoft.com/fwlink/?LinkId=827846
3 | // for the documentation about the extensions.json format
4 | "recommendations": [
5 | "dbaeumer.vscode-eslint",
6 | "ms-vscode.extension-test-runner",
7 | "connor4312.esbuild-problem-matchers"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/src/extension.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { useCommands } from './commands';
3 | import { useLessonTree } from './views/lessonsTree';
4 |
5 | export let extContext: vscode.ExtensionContext;
6 |
7 | export function activate(context: vscode.ExtensionContext) {
8 | extContext = context;
9 |
10 | useCommands();
11 | useLessonTree();
12 | }
13 |
14 | export function deactivate() {}
15 |
--------------------------------------------------------------------------------
/src/models/Lesson.ts:
--------------------------------------------------------------------------------
1 | export class Lesson {
2 | constructor(
3 | public name: string,
4 | public readonly path: string,
5 | public readonly children: Lesson[] = [],
6 | public metadata?: {
7 | _path: string;
8 | title: string;
9 | type: LessonType;
10 | description?: string;
11 | },
12 | ) {}
13 | }
14 |
15 | export type LessonType = 'lesson' | 'chapter' | 'part';
16 |
--------------------------------------------------------------------------------
/resources/icons/dark/chapter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/icons/light/chapter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | // Place your settings in this file to overwrite default and user settings.
2 | {
3 | "files.exclude": {
4 | "out": false // set this to true to hide the "out" folder with the compiled JS files
5 | },
6 | "search.exclude": {
7 | "out": true // set this to false to include "out" folder in search results
8 | },
9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts
10 | "typescript.tsc.autoDetect": "off"
11 | }
12 |
--------------------------------------------------------------------------------
/resources/icons/light/lesson.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/utils/getIcon.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 |
3 | export function getIcon(
4 | context: vscode.ExtensionContext,
5 | filename: string,
6 | ): { light: string | vscode.Uri; dark: string | vscode.Uri } {
7 | return {
8 | light: vscode.Uri.file(
9 | context.asAbsolutePath(`/resources/icons/light/${filename}`),
10 | ),
11 | dark: vscode.Uri.file(
12 | context.asAbsolutePath(`/resources/icons/dark/${filename}`),
13 | ),
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/resources/icons/dark/lesson.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to the "tutorialkit" extension will be documented in this file.
4 |
5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
6 |
7 | ## [0.0.11]
8 |
9 | - Remove the error toast appearing when loading the extension with no workspace present
10 |
11 | ## [0.0.10]
12 |
13 | - Relax the vscode engine requirement to allow versions as low as 1.80.0
14 | - Update the extension icon
15 |
16 | ## [0.0.9]
17 |
18 | - Initial release
--------------------------------------------------------------------------------
/src/test/extension.test.ts:
--------------------------------------------------------------------------------
1 | import * as assert from 'assert';
2 |
3 | // You can import and use all API from the 'vscode' module
4 | // as well as import your extension to test it
5 | import * as vscode from 'vscode';
6 | // import * as myExtension from '../../extension';
7 |
8 | suite('Extension Test Suite', () => {
9 | vscode.window.showInformationMessage('Start all tests.');
10 |
11 | test('Sample test', () => {
12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5));
13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0));
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "Node16",
4 | "target": "ES2022",
5 | "outDir": "out",
6 | "lib": [
7 | "ES2022"
8 | ],
9 | "sourceMap": true,
10 | "rootDir": "src",
11 | "strict": true /* enable all strict type-checking options */
12 | /* Additional Checks */
13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it inside a new window
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | {
6 | "version": "0.2.0",
7 | "configurations": [
8 | {
9 | "name": "Run Extension",
10 | "type": "extensionHost",
11 | "request": "launch",
12 | "args": [
13 | "--extensionDevelopmentPath=${workspaceFolder}"
14 | ],
15 | "outFiles": [
16 | "${workspaceFolder}/dist/**/*.js"
17 | ],
18 | "preLaunchTask": "${defaultBuildTask}"
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/src/commands/tutorialkit.load-tutorial.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import {
3 | LessonsTreeDataProvider,
4 | getLessonsTreeDataProvider,
5 | setLessonsTreeDataProvider,
6 | } from '../views/lessonsTree';
7 | import { extContext } from '../extension';
8 |
9 | export async function loadTutorial(uri: vscode.Uri) {
10 | setLessonsTreeDataProvider(new LessonsTreeDataProvider(uri, extContext));
11 | extContext.subscriptions.push(
12 | vscode.window.createTreeView('tutorialkit-lessons-tree', {
13 | treeDataProvider: getLessonsTreeDataProvider(),
14 | canSelectMany: true,
15 | }),
16 | );
17 | vscode.commands.executeCommand('setContext', 'tutorialkit:tree', true);
18 | }
19 |
--------------------------------------------------------------------------------
/src/commands/tutorialkit.goto.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { Lesson } from '../models/Lesson';
3 |
4 | export default (path: string, meta: Lesson['metadata'], openFile = false) => {
5 | vscode.commands
6 | .executeCommand('revealInExplorer', vscode.Uri.file(path))
7 | .then(() => {
8 | if (openFile) {
9 | vscode.workspace.openTextDocument(meta!._path).then((document) => {
10 | vscode.window.showTextDocument(document);
11 | });
12 | }
13 | })
14 | .then(() => {
15 | setTimeout(
16 | () => {
17 | vscode.commands.executeCommand('tutorialkit-lessons-tree.focus');
18 | },
19 | meta?.type === 'lesson' ? 30 : 0,
20 | );
21 | });
22 | };
23 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "parserOptions": {
5 | "ecmaVersion": 6,
6 | "sourceType": "module"
7 | },
8 | "plugins": [
9 | "@typescript-eslint"
10 | ],
11 | "rules": {
12 | "@typescript-eslint/naming-convention": [
13 | "warn",
14 | {
15 | "selector": "import",
16 | "format": [ "camelCase", "PascalCase" ]
17 | }
18 | ],
19 | "@typescript-eslint/semi": "warn",
20 | "curly": "warn",
21 | "eqeqeq": "warn",
22 | "no-throw-literal": "warn",
23 | "semi": "off"
24 | },
25 | "ignorePatterns": [
26 | "out",
27 | "dist",
28 | "**/*.d.ts"
29 | ]
30 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "watch",
6 | "dependsOn": ["npm: watch:tsc", "npm: watch:esbuild"],
7 | "presentation": {
8 | "reveal": "never"
9 | },
10 | "group": {
11 | "kind": "build",
12 | "isDefault": true
13 | }
14 | },
15 | {
16 | "type": "npm",
17 | "script": "watch:esbuild",
18 | "group": "build",
19 | "problemMatcher": "$esbuild-watch",
20 | "isBackground": true,
21 | "label": "npm: watch:esbuild",
22 | "presentation": {
23 | "group": "watch",
24 | "reveal": "never"
25 | }
26 | },
27 | {
28 | "type": "npm",
29 | "script": "watch:tsc",
30 | "group": "build",
31 | "problemMatcher": "$tsc-watch",
32 | "isBackground": true,
33 | "label": "npm: watch:tsc",
34 | "presentation": {
35 | "group": "watch",
36 | "reveal": "never"
37 | }
38 | }
39 | ]
40 | }
--------------------------------------------------------------------------------
/src/commands/tutorialkit.select-tutorial.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import isTutorialKitWorkspace from '../utils/isTutorialKit';
3 | import { cmd } from '.';
4 |
5 | export async function selectTutorial() {
6 | const tutorialWorkpaces = (vscode.workspace.workspaceFolders || []).filter(
7 | isTutorialKitWorkspace,
8 | );
9 | const selectedWorkspace =
10 | tutorialWorkpaces.length === 1
11 | ? tutorialWorkpaces[0]
12 | : await vscode.window
13 | .showQuickPick(
14 | tutorialWorkpaces.map((workspace) => workspace.name),
15 | {
16 | placeHolder: 'Select a workspace',
17 | },
18 | )
19 | .then((selected) =>
20 | tutorialWorkpaces.find((workspace) => workspace.name === selected),
21 | );
22 |
23 | if (selectedWorkspace) {
24 | cmd.loadTutorial(selectedWorkspace.uri);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/isTutorialKit.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import * as path from 'path';
3 | import * as fs from 'fs';
4 |
5 | /**
6 | * Check if the workspace is a TutorialKit workspace
7 | * by looking for a TutorialKit dependency in the package.json file.
8 | * @param folder The workspace folder to check.
9 | * @returns True if the workspace is a TutorialKit workspace, false otherwise.
10 | **/
11 | export default function isTutorialKitWorkspace(
12 | folder: vscode.WorkspaceFolder,
13 | ): boolean {
14 | const packageJsonPath = path.join(folder.uri.fsPath, 'package.json');
15 | const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
16 | const packageJson = JSON.parse(packageJsonContent);
17 | const tutorialkitDependency =
18 | packageJson.dependencies?.['@tutorialkit/astro'] ||
19 | packageJson.devDependencies?.['@tutorialkit/astro'];
20 |
21 | return !!tutorialkitDependency;
22 | }
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 StackBlitz, Inc
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/commands/tutorialkit.initialize.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import isTutorialKitWorkspace from '../utils/isTutorialKit';
3 | import { cmd } from '.';
4 |
5 | export async function initialize(toastIfEmpty = false) {
6 | const tutorialWorkpaces = (vscode.workspace.workspaceFolders || []).filter(
7 | isTutorialKitWorkspace,
8 | );
9 |
10 | if (tutorialWorkpaces.length === 0) {
11 | if (toastIfEmpty) {
12 | vscode.window.showInformationMessage(
13 | 'No TutorialKit project found in the current workspace. Make sure there is a "@tutorialkit/astro" dependency or devDependency in your package.json file.',
14 | );
15 | vscode.commands.executeCommand('setContext', 'tutorialkit:tree', false);
16 | }
17 | } else if (tutorialWorkpaces.length === 1) {
18 | cmd.loadTutorial(tutorialWorkpaces[0].uri);
19 | } else if (tutorialWorkpaces.length > 1) {
20 | vscode.commands.executeCommand(
21 | 'setContext',
22 | 'tutorialkit:multiple-tutorials',
23 | true,
24 | );
25 | vscode.commands.executeCommand('setContext', 'tutorialkit:tree', false);
26 | }
27 |
28 | vscode.commands.executeCommand('setContext', 'tutorialkit:initialized', true);
29 | }
30 |
--------------------------------------------------------------------------------
/esbuild.js:
--------------------------------------------------------------------------------
1 | const esbuild = require('esbuild');
2 |
3 | const production = process.argv.includes('--production');
4 | const watch = process.argv.includes('--watch');
5 |
6 | async function main() {
7 | const ctx = await esbuild.context({
8 | entryPoints: ['src/extension.ts'],
9 | bundle: true,
10 | format: 'cjs',
11 | minify: production,
12 | sourcemap: !production,
13 | sourcesContent: false,
14 | platform: 'node',
15 | outfile: 'dist/extension.js',
16 | external: ['vscode'],
17 | logLevel: 'silent',
18 | plugins: [
19 | /* add to the end of plugins array */
20 | esbuildProblemMatcherPlugin
21 | ]
22 | });
23 | if (watch) {
24 | await ctx.watch();
25 | } else {
26 | await ctx.rebuild();
27 | await ctx.dispose();
28 | }
29 | }
30 |
31 | /**
32 | * @type {import('esbuild').Plugin}
33 | */
34 | const esbuildProblemMatcherPlugin = {
35 | name: 'esbuild-problem-matcher',
36 |
37 | setup(build) {
38 | build.onStart(() => {
39 | console.log('[watch] build started');
40 | });
41 | build.onEnd(result => {
42 | result.errors.forEach(({ text, location }) => {
43 | console.error(`✘ [ERROR] ${text}`);
44 | console.error(` ${location.file}:${location.line}:${location.column}:`);
45 | });
46 | console.log('[watch] build finished');
47 | });
48 | }
49 | };
50 |
51 | main().catch(e => {
52 | console.error(e);
53 | process.exit(1);
54 | });
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # The source this project is now moved to [the main tutorialkit repository](https://github.com/stackblitz/tutorialkit). This repo is archived.
2 |
3 | # TutorialKit Code Extension
4 |
5 | **Status**: Preview
6 |
7 | ## Features
8 |
9 | - Viewing lessons, chapters and parts in the side panel
10 | - Creating new lessons and chapters
11 |
12 |
13 |
14 | ## Installation
15 |
16 | 1. Open VS Code
17 | 2. Select _Extensions_ in the left vertical toolbar
18 | 3. Search for `tutorialkit`
19 | 4. Click on _Install_
20 |
21 | ## Description
22 |
23 | When designing the content architecture across folders and files, we aimed to make it as plain and clear as possible. However, even the best-thought-out file system organization doesn’t translate perfectly to the more abstract structure of lessons and chapters in a tutorial.
24 |
25 | We wanted to make navigating between the different parts of your courseware or creating new lessons even easier than traversing a file system.
26 |
27 | For that reason, we’ve decided to build a companion Visual Studio Code extension for tutorial authors. The extension provides a visual representation of your tutorial structure and allows you to create new chapters and lessons. When you navigate through the displayed structure, the extension automatically focuses on the corresponding folders and files in your file explorer, giving you a quick way to jump to the content you want to edit.
28 |
--------------------------------------------------------------------------------
/src/commands/index.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import tutorialkitGoto from './tutorialkit.goto';
3 | import tutorialkitRefresh from './tutorialkit.refresh';
4 | import { addChapter, addLesson } from './tutorialkit.add';
5 | import { selectTutorial } from './tutorialkit.select-tutorial';
6 | import { loadTutorial } from './tutorialkit.load-tutorial';
7 | import { initialize } from './tutorialkit.initialize';
8 |
9 | // No need to use these consts outsite of this file
10 | // – use `cmd[name].command` instead
11 | const CMD = {
12 | INITIALIZE: 'tutorialkit.initialize',
13 | SELECT_TUTORIAL: 'tutorialkit.select-tutorial',
14 | LOAD_TUTORIAL: 'tutorialkit.load-tutorial',
15 | GOTO: 'tutorialkit.goto',
16 | ADD_LESSON: 'tutorialkit.add-lesson',
17 | ADD_CHAPTER: 'tutorialkit.add-chapter',
18 | REFRESH: 'tutorialkit.refresh',
19 | } as const;
20 |
21 | // Register all commands in Code IDE
22 | export function useCommands() {
23 | vscode.commands.registerCommand(CMD.INITIALIZE, initialize);
24 | vscode.commands.registerCommand(CMD.SELECT_TUTORIAL, selectTutorial);
25 | vscode.commands.registerCommand(CMD.LOAD_TUTORIAL, loadTutorial);
26 | vscode.commands.registerCommand(CMD.GOTO, tutorialkitGoto);
27 | vscode.commands.registerCommand(CMD.ADD_LESSON, addLesson);
28 | vscode.commands.registerCommand(CMD.ADD_CHAPTER, addChapter);
29 | vscode.commands.registerCommand(CMD.REFRESH, tutorialkitRefresh);
30 | }
31 |
32 | // Create typesafe commands
33 | export const cmd = {
34 | initialize: createExecutor(CMD.INITIALIZE),
35 | selectTutorial: createExecutor(CMD.SELECT_TUTORIAL),
36 | loadTutorial: createExecutor(CMD.LOAD_TUTORIAL),
37 | goto: createExecutor(CMD.GOTO),
38 | addLesson: createExecutor(CMD.ADD_LESSON),
39 | addChapter: createExecutor(CMD.ADD_CHAPTER),
40 | refresh: createExecutor(CMD.REFRESH),
41 | };
42 |
43 | function createExecutor any>(name: string) {
44 | function executor(...args: Parameters) {
45 | return vscode.commands.executeCommand(name, ...args);
46 | }
47 | executor.command = name;
48 | return executor;
49 | }
50 |
--------------------------------------------------------------------------------
/src/commands/tutorialkit.add.ts:
--------------------------------------------------------------------------------
1 | import { cmd } from '.';
2 | import { Lesson, LessonType } from '../models/Lesson';
3 | import * as vscode from 'vscode';
4 |
5 | let kebabCase: (string: string) => string;
6 | let capitalize: (string: string) => string;
7 |
8 | (async () => {
9 | const module = await import('case-anything');
10 | kebabCase = module.kebabCase;
11 | capitalize = module.capitalCase;
12 | })();
13 |
14 | export async function addLesson(parent: Lesson) {
15 | const lessonNumber = parent.children.length + 1;
16 |
17 | const lessonName = await getUnitName('lesson', lessonNumber);
18 |
19 | const lessonFolderPath = await createUnitFolder(
20 | parent.path,
21 | lessonNumber,
22 | lessonName,
23 | 'lesson',
24 | );
25 |
26 | await vscode.workspace.fs.createDirectory(
27 | vscode.Uri.file(`${lessonFolderPath}/_files`),
28 | );
29 |
30 | await cmd.refresh();
31 |
32 | return navigateToUnit(lessonFolderPath, 'lesson', lessonName);
33 | }
34 |
35 | export async function addChapter(parent: Lesson) {
36 | const chapterNumber = parent.children.length + 1;
37 |
38 | const chapterName = await getUnitName('chapter', chapterNumber);
39 |
40 | const chapterFolderPath = await createUnitFolder(
41 | parent.path,
42 | chapterNumber,
43 | chapterName,
44 | 'chapter',
45 | );
46 |
47 | await navigateToUnit(chapterFolderPath, 'chapter', chapterName);
48 |
49 | await cmd.refresh();
50 | }
51 |
52 | async function getUnitName(unitType: LessonType, unitNumber: number) {
53 | const unitName = await vscode.window.showInputBox({
54 | prompt: `Enter the name of the new ${unitType}`,
55 | value: `${capitalize(unitType)} ${unitNumber}`,
56 | });
57 |
58 | // Break if no name provided
59 | if (!unitName) {
60 | throw new Error(`No ${unitType} name provided`);
61 | }
62 |
63 | return unitName;
64 | }
65 |
66 | async function createUnitFolder(
67 | parentPath: string,
68 | unitNumber: number,
69 | unitName: string,
70 | unitType: LessonType,
71 | ) {
72 | const unitFolderPath = `${parentPath}/${unitNumber}-${kebabCase(unitName)}`;
73 | const metaFile = unitType === 'lesson' ? 'content.mdx' : 'meta.md';
74 |
75 | await vscode.workspace.fs.writeFile(
76 | vscode.Uri.file(`${unitFolderPath}/${metaFile}`),
77 | new TextEncoder().encode(
78 | `---\ntitle: ${unitName}\ntype: ${unitType}\n---\n`,
79 | ),
80 | );
81 |
82 | return unitFolderPath;
83 | }
84 |
85 | async function navigateToUnit(
86 | path: string,
87 | unitType: LessonType,
88 | title: string,
89 | ) {
90 | const metaFile = unitType === 'lesson' ? 'content.mdx' : 'meta.md';
91 | return cmd.goto(
92 | path,
93 | {
94 | _path: `${path}/${metaFile}`,
95 | type: unitType,
96 | title,
97 | },
98 | true,
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/vsc-extension-quickstart.md:
--------------------------------------------------------------------------------
1 | # Welcome to your VS Code Extension
2 |
3 | ## What's in the folder
4 |
5 | * This folder contains all of the files necessary for your extension.
6 | * `package.json` - this is the manifest file in which you declare your extension and command.
7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin.
8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command.
9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`.
10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`.
11 |
12 | ## Get up and running straight away
13 |
14 | * Press `F5` to open a new window with your extension loaded.
15 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`.
16 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension.
17 | * Find output from your extension in the debug console.
18 |
19 | ## Make changes
20 |
21 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`.
22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.
23 |
24 | ## Explore the API
25 |
26 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`.
27 |
28 | ## Run tests
29 |
30 | * Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner)
31 | * Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered.
32 | * Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A`
33 | * See the output of the test result in the Test Results view.
34 | * Make changes to `src/test/extension.test.ts` or create new test files inside the `test` folder.
35 | * The provided test runner will only consider files matching the name pattern `**.test.ts`.
36 | * You can create folders inside the `test` folder to structure your tests any way you want.
37 |
38 | ## Go further
39 |
40 | * [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns.
41 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension).
42 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace.
43 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration).
44 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tutorialkit",
3 | "displayName": "TutorialKit",
4 | "description": "TutorialKit support in VS Code",
5 | "icon": "resources/tutorialkit-icon.png",
6 | "publisher": "StackBlitz",
7 | "version": "0.0.11",
8 | "engines": {
9 | "vscode": "^1.80.0"
10 | },
11 | "repository": {
12 | "url": "https://github.com/stackblitz/tutorialkit"
13 | },
14 | "categories": [
15 | "Other"
16 | ],
17 | "activationEvents": [
18 | "onStartupFinished"
19 | ],
20 | "main": "./dist/extension.js",
21 | "contributes": {
22 | "commands": [
23 | {
24 | "command": "tutorialkit.select-tutorial",
25 | "title": "Select Tutorial",
26 | "icon": "$(book)"
27 | },
28 | {
29 | "command": "tutorialkit.add-lesson",
30 | "title": "Add Lesson"
31 | },
32 | {
33 | "command": "tutorialkit.add-chapter",
34 | "title": "Add Chapter"
35 | },
36 | {
37 | "command": "tutorialkit.add-part",
38 | "title": "Add Part"
39 | },
40 | {
41 | "command": "tutorialkit.refresh",
42 | "title": "Refresh Lessons",
43 | "icon": "$(refresh)"
44 | }
45 | ],
46 | "viewsWelcome": [
47 | {
48 | "view": "tutorialkit-lessons-tree",
49 | "id": "tutorialkit-splashscreen",
50 | "contents": "",
51 | "title": "Tutorial",
52 | "description": "TutorialKit",
53 | "when": "!tutorialkit:initialized"
54 | },
55 | {
56 | "view": "tutorialkit-lessons-tree",
57 | "id": "tutorialkit-splashscreen",
58 | "contents": "Looks like there is no TutorialKit project in this workspace.\n[Rescan workspace](command:tutorialkit.initialize?true)",
59 | "title": "Tutorial",
60 | "description": "TutorialKit",
61 | "when": "tutorialkit:initialized && !tutorialkit:multiple-tutorials"
62 | },
63 | {
64 | "view": "tutorialkit-lessons-tree",
65 | "id": "tutorialkit-multiple-tutorials",
66 | "contents": "Welcome to TutorialKit!\nLooks like there is more than one tutorial in your workspace.\n[Select a tutorial](command:tutorialkit.select-tutorial)",
67 | "title": "Tutorial",
68 | "description": "TutorialKit",
69 | "when": "tutorialkit:multiple-tutorials"
70 | }
71 | ],
72 | "views": {
73 | "explorer": [
74 | {
75 | "id": "tutorialkit-lessons-tree",
76 | "name": "Tutorial",
77 | "visibility": "visible",
78 | "initialSize": 3
79 | }
80 | ]
81 | },
82 | "menus": {
83 | "view/title": [
84 | {
85 | "command": "tutorialkit.select-tutorial",
86 | "when": "view == tutorialkit-lessons-tree && tutorialkit:multiple-tutorials",
87 | "group": "navigation"
88 | },
89 | {
90 | "command": "tutorialkit.refresh",
91 | "when": "view == tutorialkit-lessons-tree && tutorialkit:tree",
92 | "group": "navigation"
93 | }
94 | ],
95 | "view/item/context": [
96 | {
97 | "command": "tutorialkit.add-lesson",
98 | "when": "view == tutorialkit-lessons-tree && viewItem == chapter"
99 | },
100 | {
101 | "command": "tutorialkit.add-chapter",
102 | "when": "view == tutorialkit-lessons-tree && viewItem == part"
103 | }
104 | ]
105 | }
106 | },
107 | "scripts": {
108 | "__esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node",
109 | "__dev": "npm run esbuild-base -- --sourcemap --watch",
110 | "__vscode:prepublish": "npm run esbuild-base -- --minify",
111 | "__build": "vsce package",
112 | "compile": "npm run check-types && node esbuild.js",
113 | "check-types": "tsc --noEmit",
114 | "watch": "npm-run-all -p watch:*",
115 | "watch:esbuild": "node esbuild.js --watch",
116 | "watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
117 | "vscode:prepublish": "npm run package",
118 | "package": "npm run check-types && node esbuild.js --production"
119 | },
120 | "devDependencies": {
121 | "@types/mocha": "^10.0.6",
122 | "@types/node": "18.x",
123 | "@types/vscode": "^1.80.0",
124 | "@typescript-eslint/eslint-plugin": "^7.11.0",
125 | "@typescript-eslint/parser": "^7.11.0",
126 | "@vscode/test-cli": "^0.0.9",
127 | "@vscode/test-electron": "^2.4.0",
128 | "esbuild": "^0.21.5",
129 | "eslint": "^8.57.0",
130 | "typescript": "^5.4.5"
131 | },
132 | "dependencies": {
133 | "case-anything": "^3.1.0",
134 | "gray-matter": "^4.0.3",
135 | "npm-run-all": "^4.1.5"
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/views/lessonsTree.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import grayMatter from 'gray-matter';
3 | import * as path from 'path';
4 | import * as vscode from 'vscode';
5 | import { cmd } from '../commands';
6 | import { Lesson } from '../models/Lesson';
7 | import { getIcon } from '../utils/getIcon';
8 | import isTutorialKitWorkspace from '../utils/isTutorialKit';
9 |
10 | const metadataFiles = ['meta.md', 'meta.mdx', 'content.md', 'content.mdx'];
11 | export const tutorialMimeType = 'application/tutorialkit.unit';
12 |
13 | let lessonsTreeDataProvider: LessonsTreeDataProvider;
14 | export function getLessonsTreeDataProvider() {
15 | return lessonsTreeDataProvider;
16 | }
17 | export function setLessonsTreeDataProvider(provider: LessonsTreeDataProvider) {
18 | lessonsTreeDataProvider = provider;
19 | }
20 |
21 | export class LessonsTreeDataProvider
22 | implements vscode.TreeDataProvider
23 | {
24 | private lessons: Lesson[] = [];
25 | private isTutorialKitWorkspace = false;
26 |
27 | constructor(
28 | private readonly workspaceRoot: vscode.Uri,
29 | private context: vscode.ExtensionContext,
30 | ) {
31 | this.loadLessons();
32 | }
33 |
34 | private loadLessons(): void {
35 | try {
36 | const tutorialFolderPath = vscode.Uri.joinPath(
37 | this.workspaceRoot,
38 | 'src',
39 | 'content',
40 | 'tutorial',
41 | ).fsPath;
42 | this.isTutorialKitWorkspace = true;
43 | this.lessons = this.loadLessonsFromFolder(tutorialFolderPath);
44 | } catch {
45 | this.isTutorialKitWorkspace = false;
46 | }
47 | }
48 |
49 | private loadLessonsFromFolder(folderPath: string): Lesson[] {
50 | const lessons: Lesson[] = [];
51 | const files = fs.readdirSync(folderPath);
52 |
53 | for (const file of files) {
54 | const filePath = path.join(folderPath, file);
55 | const stats = fs.statSync(filePath);
56 |
57 | if (stats.isDirectory()) {
58 | const lessonName = path.basename(filePath);
59 | const subLessons = this.loadLessonsFromFolder(filePath);
60 | const lesson = new Lesson(lessonName, filePath, subLessons);
61 |
62 | // Check if the folder directly includes one of the metadata files
63 | const folderFiles = fs.readdirSync(filePath);
64 | const metadataFile = folderFiles.find((folderFile) =>
65 | metadataFiles.includes(folderFile),
66 | );
67 | if (metadataFile) {
68 | const metadataFilePath = path.join(filePath, metadataFile);
69 | const metadataFileContent = fs.readFileSync(metadataFilePath, 'utf8');
70 | const parsedContent = grayMatter(metadataFileContent);
71 | lesson.name = parsedContent.data.title;
72 | lesson.metadata = {
73 | _path: metadataFilePath,
74 | ...(parsedContent.data as any),
75 | };
76 | lessons.push(lesson);
77 | }
78 | }
79 | }
80 |
81 | return lessons;
82 | }
83 |
84 | private _onDidChangeTreeData: vscode.EventEmitter =
85 | new vscode.EventEmitter();
86 | readonly onDidChangeTreeData: vscode.Event =
87 | this._onDidChangeTreeData.event;
88 |
89 | refresh(): void {
90 | this.loadLessons();
91 | this._onDidChangeTreeData.fire(undefined);
92 | }
93 |
94 | getTreeItem(lesson: Lesson): vscode.TreeItem {
95 | const treeItem = new vscode.TreeItem(lesson.name);
96 | treeItem.collapsibleState =
97 | lesson.children.length > 0
98 | ? vscode.TreeItemCollapsibleState.Collapsed
99 | : vscode.TreeItemCollapsibleState.None;
100 |
101 | treeItem.contextValue = lesson.metadata?.type;
102 |
103 | const shouldOpenFile = lesson.metadata?.type === 'lesson';
104 | treeItem.command = {
105 | command: cmd.goto.command,
106 | title: 'Go to the lesson',
107 | arguments: [lesson.path, lesson.metadata, shouldOpenFile],
108 | };
109 |
110 | treeItem.iconPath =
111 | lesson.metadata?.type === 'lesson'
112 | ? getIcon(this.context, 'lesson.svg')
113 | : getIcon(this.context, 'chapter.svg');
114 |
115 | return treeItem;
116 | }
117 |
118 | getChildren(element?: Lesson): Lesson[] {
119 | if (element) {
120 | return element.children;
121 | }
122 | return this.lessons;
123 | }
124 | }
125 |
126 | export async function useLessonTree() {
127 | // vscode.workspace.onDidChangeWorkspaceFolders((event) => {
128 | // event.added.forEach((folder) => {
129 | // if (isTutorialKitWorkspace(folder)) {
130 | // }
131 | // });
132 | // });
133 |
134 | // vscode.commands.executeCommand('setContext', 'tutorialkit:tree', true);
135 |
136 | cmd.initialize();
137 |
138 | // const tutorialWorkpaces = (vscode.workspace.workspaceFolders || []).filter(
139 | // isTutorialKitWorkspace,
140 | // );
141 | // const selectedWorkspace =
142 | // tutorialWorkpaces.length === 1
143 | // ? tutorialWorkpaces[0]
144 | // : await vscode.window
145 | // .showQuickPick(
146 | // tutorialWorkpaces.map((workspace) => workspace.name),
147 | // {
148 | // placeHolder: 'Select a workspace',
149 | // },
150 | // )
151 | // .then((selected) =>
152 | // tutorialWorkpaces.find((workspace) => workspace.name === selected),
153 | // );
154 |
155 | // if (selectedWorkspace) {
156 | // setLessonsTreeDataProvider(
157 | // new LessonsTreeDataProvider(selectedWorkspace.uri, context),
158 | // );
159 | // context.subscriptions.push(
160 | // vscode.window.createTreeView('tutorialkit-lessons-tree', {
161 | // treeDataProvider: getLessonsTreeDataProvider(),
162 | // canSelectMany: true,
163 | // }),
164 | // );
165 | // }
166 | }
167 |
--------------------------------------------------------------------------------