Skip to content

Commit 3b2c776

Browse files
authored
feat(extension): support config-based ordering (#223)
1 parent 0b86ebd commit 3b2c776

15 files changed

+535
-133
lines changed

extensions/vscode/package.json

+19-4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@
1919
],
2020
"main": "./dist/extension.js",
2121
"contributes": {
22+
"keybindings": [
23+
{
24+
"command": "tutorialkit.delete",
25+
"key": "Shift+Backspace",
26+
"when": "focusedView == tutorialkit-lessons-tree"
27+
}
28+
],
2229
"commands": [
2330
{
2431
"command": "tutorialkit.select-tutorial",
@@ -37,6 +44,10 @@
3744
"command": "tutorialkit.add-part",
3845
"title": "Add Part"
3946
},
47+
{
48+
"command": "tutorialkit.delete",
49+
"title": "Delete"
50+
},
4051
{
4152
"command": "tutorialkit.refresh",
4253
"title": "Refresh Lessons",
@@ -100,6 +111,14 @@
100111
{
101112
"command": "tutorialkit.add-chapter",
102113
"when": "view == tutorialkit-lessons-tree && viewItem == part"
114+
},
115+
{
116+
"command": "tutorialkit.add-part",
117+
"when": "view == tutorialkit-lessons-tree && viewItem == tutorial"
118+
},
119+
{
120+
"command": "tutorialkit.delete",
121+
"when": "view == tutorialkit-lessons-tree && (viewItem == chapter || viewItem == part || viewItem == lesson)"
103122
}
104123
]
105124
},
@@ -119,10 +138,6 @@
119138
]
120139
},
121140
"scripts": {
122-
"__esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node",
123-
"__dev": "pnpm run esbuild-base -- --sourcemap --watch",
124-
"__vscode:prepublish": "pnpm run esbuild-base -- --minify",
125-
"__build": "vsce package",
126141
"dev": "node scripts/build.mjs --watch",
127142
"build": "pnpm run check-types && node scripts/build.mjs",
128143
"check-types": "tsc --noEmit",

extensions/vscode/src/commands/index.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import * as vscode from 'vscode';
2+
import { addChapter, addLesson, addPart } from './tutorialkit.add';
3+
import { deleteNode } from './tutorialkit.delete';
24
import tutorialkitGoto from './tutorialkit.goto';
5+
import { initialize } from './tutorialkit.initialize';
6+
import { loadTutorial } from './tutorialkit.load-tutorial';
37
import tutorialkitRefresh from './tutorialkit.refresh';
4-
import { addChapter, addLesson } from './tutorialkit.add';
58
import { selectTutorial } from './tutorialkit.select-tutorial';
6-
import { loadTutorial } from './tutorialkit.load-tutorial';
7-
import { initialize } from './tutorialkit.initialize';
89

910
// no need to use these consts outside of this file, use `cmd[name].command` instead
1011
const CMD = {
@@ -14,6 +15,8 @@ const CMD = {
1415
GOTO: 'tutorialkit.goto',
1516
ADD_LESSON: 'tutorialkit.add-lesson',
1617
ADD_CHAPTER: 'tutorialkit.add-chapter',
18+
ADD_PART: 'tutorialkit.add-part',
19+
DELETE: 'tutorialkit.delete',
1720
REFRESH: 'tutorialkit.refresh',
1821
} as const;
1922

@@ -25,6 +28,8 @@ export function useCommands() {
2528
vscode.commands.registerCommand(CMD.GOTO, tutorialkitGoto);
2629
vscode.commands.registerCommand(CMD.ADD_LESSON, addLesson);
2730
vscode.commands.registerCommand(CMD.ADD_CHAPTER, addChapter);
31+
vscode.commands.registerCommand(CMD.ADD_PART, addPart);
32+
vscode.commands.registerCommand(CMD.DELETE, deleteNode);
2833
vscode.commands.registerCommand(CMD.REFRESH, tutorialkitRefresh);
2934
}
3035

@@ -34,7 +39,9 @@ export const cmd = {
3439
selectTutorial: createExecutor<typeof selectTutorial>(CMD.SELECT_TUTORIAL),
3540
loadTutorial: createExecutor<typeof loadTutorial>(CMD.LOAD_TUTORIAL),
3641
goto: createExecutor<typeof tutorialkitGoto>(CMD.GOTO),
42+
delete: createExecutor<typeof deleteNode>(CMD.DELETE),
3743
addLesson: createExecutor<typeof addLesson>(CMD.ADD_LESSON),
44+
addPart: createExecutor<typeof addPart>(CMD.ADD_PART),
3845
addChapter: createExecutor<typeof addChapter>(CMD.ADD_CHAPTER),
3946
refresh: createExecutor<typeof tutorialkitRefresh>(CMD.REFRESH),
4047
};
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { cmd } from '.';
2-
import { Lesson, LessonType } from '../models/Lesson';
2+
import { Node, NodeType } from '../models/Node';
33
import * as vscode from 'vscode';
4+
import { FILES_FOLDER, SOLUTION_FOLDER } from '../models/tree/constants';
5+
import { updateNodeMetadataInVFS } from '../models/tree/update';
46

57
let kebabCase: (string: string) => string;
68
let capitalize: (string: string) => string;
@@ -11,34 +13,34 @@ let capitalize: (string: string) => string;
1113
capitalize = module.capitalCase;
1214
})();
1315

14-
export async function addLesson(parent: Lesson) {
15-
const lessonNumber = parent.children.length + 1;
16+
export async function addLesson(parent: Node) {
17+
const { folderPath, metaFilePath } = await createNodeFolder(parent, 'lesson');
1618

17-
const lessonName = await getUnitName('lesson', lessonNumber);
18-
19-
const lessonFolderPath = await createUnitFolder(parent.path, lessonNumber, lessonName, 'lesson');
20-
21-
await vscode.workspace.fs.createDirectory(vscode.Uri.file(`${lessonFolderPath}/_files`));
22-
await vscode.workspace.fs.createDirectory(vscode.Uri.file(`${lessonFolderPath}/_solution`));
19+
await vscode.workspace.fs.createDirectory(vscode.Uri.joinPath(folderPath, FILES_FOLDER));
20+
await vscode.workspace.fs.createDirectory(vscode.Uri.joinPath(folderPath, SOLUTION_FOLDER));
2321

2422
await cmd.refresh();
2523

26-
return navigateToUnit(lessonFolderPath, 'lesson');
24+
return cmd.goto(metaFilePath);
2725
}
2826

29-
export async function addChapter(parent: Lesson) {
30-
const chapterNumber = parent.children.length + 1;
27+
export async function addChapter(parent: Node) {
28+
const { metaFilePath } = await createNodeFolder(parent, 'chapter');
3129

32-
const chapterName = await getUnitName('chapter', chapterNumber);
30+
await cmd.refresh();
3331

34-
const chapterFolderPath = await createUnitFolder(parent.path, chapterNumber, chapterName, 'chapter');
32+
return cmd.goto(metaFilePath);
33+
}
3534

36-
await navigateToUnit(chapterFolderPath, 'chapter');
35+
export async function addPart(parent: Node) {
36+
const { metaFilePath } = await createNodeFolder(parent, 'part');
3737

3838
await cmd.refresh();
39+
40+
return cmd.goto(metaFilePath);
3941
}
4042

41-
async function getUnitName(unitType: LessonType, unitNumber: number) {
43+
async function getNodeName(unitType: NodeType, unitNumber: number) {
4244
const unitName = await vscode.window.showInputBox({
4345
prompt: `Enter the name of the new ${unitType}`,
4446
value: `${capitalize(unitType)} ${unitNumber}`,
@@ -52,20 +54,26 @@ async function getUnitName(unitType: LessonType, unitNumber: number) {
5254
return unitName;
5355
}
5456

55-
async function createUnitFolder(parentPath: string, unitNumber: number, unitName: string, unitType: LessonType) {
56-
const unitFolderPath = `${parentPath}/${unitNumber}-${kebabCase(unitName)}`;
57-
const metaFile = unitType === 'lesson' ? 'content.mdx' : 'meta.md';
57+
async function createNodeFolder(parent: Node, nodeType: NodeType) {
58+
const unitNumber = parent.children.length + 1;
59+
const unitName = await getNodeName(nodeType, unitNumber);
60+
const unitFolderPath = parent.order ? kebabCase(unitName) : `${unitNumber}-${kebabCase(unitName)}`;
5861

59-
await vscode.workspace.fs.writeFile(
60-
vscode.Uri.file(`${unitFolderPath}/${metaFile}`),
61-
new TextEncoder().encode(`---\ntype: ${unitType}\ntitle: ${unitName}\n---\n`),
62-
);
62+
const metaFile = nodeType === 'lesson' ? 'content.mdx' : 'meta.md';
63+
const metaFilePath = vscode.Uri.joinPath(parent.path, unitFolderPath, metaFile);
6364

64-
return unitFolderPath;
65-
}
65+
if (parent.order) {
66+
parent.pushChild(unitFolderPath);
67+
await updateNodeMetadataInVFS(parent);
68+
}
6669

67-
async function navigateToUnit(path: string, unitType: LessonType) {
68-
const metaFile = unitType === 'lesson' ? 'content.mdx' : 'meta.md';
70+
await vscode.workspace.fs.writeFile(
71+
metaFilePath,
72+
new TextEncoder().encode(`---\ntype: ${nodeType}\ntitle: ${unitName}\n---\n`),
73+
);
6974

70-
return cmd.goto(`${path}/${metaFile}`);
75+
return {
76+
folderPath: vscode.Uri.joinPath(parent.path, unitFolderPath),
77+
metaFilePath,
78+
};
7179
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { cmd } from '.';
2+
import * as vscode from 'vscode';
3+
import { Node } from '../models/Node';
4+
import { getLessonsTreeView } from '../global-state';
5+
import { updateNodeMetadataInVFS } from '../models/tree/update';
6+
7+
export async function deleteNode(selectedNode: Node | undefined, selectedNodes: Node[] | undefined) {
8+
let nodes: readonly Node[] = (selectedNodes ? selectedNodes : [selectedNode]).filter((node) => node !== undefined);
9+
10+
if (nodes.length === 0) {
11+
nodes = getLessonsTreeView().selection;
12+
}
13+
14+
const parents = new Set<Node>();
15+
16+
for (const node of nodes) {
17+
if (node.parent) {
18+
parents.add(node.parent);
19+
node.parent.removeChild(node);
20+
}
21+
22+
await vscode.workspace.fs.delete(node.path, { recursive: true });
23+
}
24+
25+
// remove all nodes from parents that that might have been parent of other deleted nodes
26+
for (const node of nodes) {
27+
parents.delete(node);
28+
}
29+
30+
for (const parent of parents) {
31+
await updateNodeMetadataInVFS(parent);
32+
}
33+
34+
return cmd.refresh();
35+
}

extensions/vscode/src/commands/tutorialkit.goto.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
11
import * as vscode from 'vscode';
22

3-
export default async (path: string | undefined) => {
3+
export default async (path: string | vscode.Uri | undefined) => {
44
if (!path) {
55
return;
66
}
77

8-
const document = await vscode.workspace.openTextDocument(path);
8+
/**
9+
* This cast to 'any' makes no sense because if we narrow the type of path
10+
* there are no type errors. So this code:
11+
*
12+
* ```ts
13+
* typeof path === 'string'
14+
* ? await vscode.workspace.openTextDocument(path)
15+
* : await vscode.workspace.openTextDocument(path)
16+
* ;
17+
* ```
18+
*
19+
* Type check correctly despite being identical to calling the function
20+
* without the branch.
21+
*
22+
* To avoid this TypeScript bug here we just cast to any.
23+
*/
24+
const document = await vscode.workspace.openTextDocument(path as any);
925

1026
await vscode.window.showTextDocument(document, {
1127
preserveFocus: true,
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
import * as vscode from 'vscode';
22
import { extContext } from '../extension';
3-
import { LessonsTreeDataProvider, getLessonsTreeDataProvider, setLessonsTreeDataProvider } from '../views/lessonsTree';
3+
import { LessonsTreeDataProvider } from '../views/lessonsTree';
4+
import { setLessonsTreeDataProvider, setLessonsTreeView } from '../global-state';
45

56
export async function loadTutorial(uri: vscode.Uri) {
6-
setLessonsTreeDataProvider(new LessonsTreeDataProvider(uri, extContext));
7+
const treeDataProvider = new LessonsTreeDataProvider(uri, extContext);
78

8-
extContext.subscriptions.push(
9-
vscode.window.createTreeView('tutorialkit-lessons-tree', {
10-
treeDataProvider: getLessonsTreeDataProvider(),
11-
canSelectMany: true,
12-
}),
13-
);
9+
await treeDataProvider.init();
10+
11+
const treeView = vscode.window.createTreeView('tutorialkit-lessons-tree', {
12+
treeDataProvider,
13+
canSelectMany: true,
14+
});
15+
16+
setLessonsTreeDataProvider(treeDataProvider);
17+
setLessonsTreeView(treeView);
18+
19+
extContext.subscriptions.push(treeView, treeDataProvider);
1420

1521
vscode.commands.executeCommand('setContext', 'tutorialkit:tree', true);
1622
}

extensions/vscode/src/commands/tutorialkit.refresh.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getLessonsTreeDataProvider } from '../views/lessonsTree';
1+
import { getLessonsTreeDataProvider } from '../global-state';
22

33
export default () => {
44
getLessonsTreeDataProvider().refresh();

extensions/vscode/src/global-state.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { TreeView } from 'vscode';
2+
import type { LessonsTreeDataProvider } from './views/lessonsTree';
3+
import type { Node } from './models/Node';
4+
5+
let lessonsTreeDataProvider: LessonsTreeDataProvider;
6+
let lessonsTreeView: TreeView<Node>;
7+
8+
export function getLessonsTreeDataProvider() {
9+
return lessonsTreeDataProvider;
10+
}
11+
12+
export function getLessonsTreeView() {
13+
return lessonsTreeView;
14+
}
15+
16+
export function setLessonsTreeDataProvider(provider: LessonsTreeDataProvider) {
17+
lessonsTreeDataProvider = provider;
18+
}
19+
20+
export function setLessonsTreeView(treeView: TreeView<Node>) {
21+
lessonsTreeView = treeView;
22+
}

extensions/vscode/src/models/Lesson.ts

-15
This file was deleted.

0 commit comments

Comments
 (0)