Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions extensions/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@
],
"main": "./dist/extension.js",
"contributes": {
"keybindings": [
{
"command": "tutorialkit.delete",
"key": "Shift+Backspace",
"when": "focusedView == tutorialkit-lessons-tree"
}
],
"commands": [
{
"command": "tutorialkit.select-tutorial",
Expand All @@ -37,6 +44,10 @@
"command": "tutorialkit.add-part",
"title": "Add Part"
},
{
"command": "tutorialkit.delete",
"title": "Delete"
},
{
"command": "tutorialkit.refresh",
"title": "Refresh Lessons",
Expand Down Expand Up @@ -100,6 +111,14 @@
{
"command": "tutorialkit.add-chapter",
"when": "view == tutorialkit-lessons-tree && viewItem == part"
},
{
"command": "tutorialkit.add-part",
"when": "view == tutorialkit-lessons-tree && viewItem == tutorial"
},
{
"command": "tutorialkit.delete",
"when": "view == tutorialkit-lessons-tree && (viewItem == chapter || viewItem == part || viewItem == lesson)"
}
]
},
Expand All @@ -119,10 +138,6 @@
]
},
"scripts": {
"__esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node",
"__dev": "pnpm run esbuild-base -- --sourcemap --watch",
"__vscode:prepublish": "pnpm run esbuild-base -- --minify",
"__build": "vsce package",
Comment on lines -122 to -125
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed those because I don't think we should use them? @sulco what do you think?

"dev": "node scripts/build.mjs --watch",
"build": "pnpm run check-types && node scripts/build.mjs",
"check-types": "tsc --noEmit",
Expand Down
13 changes: 10 additions & 3 deletions extensions/vscode/src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as vscode from 'vscode';
import { addChapter, addLesson, addPart } from './tutorialkit.add';
import { deleteUnit } from './tutorialkit.delete';
import tutorialkitGoto from './tutorialkit.goto';
import { initialize } from './tutorialkit.initialize';
import { loadTutorial } from './tutorialkit.load-tutorial';
import tutorialkitRefresh from './tutorialkit.refresh';
import { addChapter, addLesson } from './tutorialkit.add';
import { selectTutorial } from './tutorialkit.select-tutorial';
import { loadTutorial } from './tutorialkit.load-tutorial';
import { initialize } from './tutorialkit.initialize';

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

Expand All @@ -25,6 +28,8 @@ export function useCommands() {
vscode.commands.registerCommand(CMD.GOTO, tutorialkitGoto);
vscode.commands.registerCommand(CMD.ADD_LESSON, addLesson);
vscode.commands.registerCommand(CMD.ADD_CHAPTER, addChapter);
vscode.commands.registerCommand(CMD.ADD_PART, addPart);
vscode.commands.registerCommand(CMD.DELETE, deleteUnit);
vscode.commands.registerCommand(CMD.REFRESH, tutorialkitRefresh);
}

Expand All @@ -34,7 +39,9 @@ export const cmd = {
selectTutorial: createExecutor<typeof selectTutorial>(CMD.SELECT_TUTORIAL),
loadTutorial: createExecutor<typeof loadTutorial>(CMD.LOAD_TUTORIAL),
goto: createExecutor<typeof tutorialkitGoto>(CMD.GOTO),
delete: createExecutor<typeof deleteUnit>(CMD.DELETE),
addLesson: createExecutor<typeof addLesson>(CMD.ADD_LESSON),
addPart: createExecutor<typeof addPart>(CMD.ADD_PART),
addChapter: createExecutor<typeof addChapter>(CMD.ADD_CHAPTER),
refresh: createExecutor<typeof tutorialkitRefresh>(CMD.REFRESH),
};
Expand Down
60 changes: 34 additions & 26 deletions extensions/vscode/src/commands/tutorialkit.add.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { cmd } from '.';
import { Lesson, LessonType } from '../models/Lesson';
import { Node, NodeType } from '../models/Node';
import * as vscode from 'vscode';
import { FILES_FOLDER, SOLUTION_FOLDER } from '../models/tree/constants';
import { updateNodeMetadataInVFS } from '../models/tree/update';

let kebabCase: (string: string) => string;
let capitalize: (string: string) => string;
Expand All @@ -11,34 +13,34 @@ let capitalize: (string: string) => string;
capitalize = module.capitalCase;
})();

export async function addLesson(parent: Lesson) {
const lessonNumber = parent.children.length + 1;
export async function addLesson(parent: Node) {
const { folderPath, metaFilePath } = await createUnitFolder(parent, 'lesson');

const lessonName = await getUnitName('lesson', lessonNumber);

const lessonFolderPath = await createUnitFolder(parent.path, lessonNumber, lessonName, 'lesson');

await vscode.workspace.fs.createDirectory(vscode.Uri.file(`${lessonFolderPath}/_files`));
await vscode.workspace.fs.createDirectory(vscode.Uri.file(`${lessonFolderPath}/_solution`));
await vscode.workspace.fs.createDirectory(vscode.Uri.joinPath(folderPath, FILES_FOLDER));
await vscode.workspace.fs.createDirectory(vscode.Uri.joinPath(folderPath, SOLUTION_FOLDER));

await cmd.refresh();

return navigateToUnit(lessonFolderPath, 'lesson');
return cmd.goto(metaFilePath);
}

export async function addChapter(parent: Lesson) {
const chapterNumber = parent.children.length + 1;
export async function addChapter(parent: Node) {
const { metaFilePath } = await createUnitFolder(parent, 'chapter');

const chapterName = await getUnitName('chapter', chapterNumber);
await cmd.refresh();

const chapterFolderPath = await createUnitFolder(parent.path, chapterNumber, chapterName, 'chapter');
return cmd.goto(metaFilePath);
}

await navigateToUnit(chapterFolderPath, 'chapter');
export async function addPart(parent: Node) {
const { metaFilePath } = await createUnitFolder(parent, 'part');

await cmd.refresh();

return cmd.goto(metaFilePath);
}

async function getUnitName(unitType: LessonType, unitNumber: number) {
async function getUnitName(unitType: NodeType, unitNumber: number) {
const unitName = await vscode.window.showInputBox({
prompt: `Enter the name of the new ${unitType}`,
value: `${capitalize(unitType)} ${unitNumber}`,
Expand All @@ -52,20 +54,26 @@ async function getUnitName(unitType: LessonType, unitNumber: number) {
return unitName;
}

async function createUnitFolder(parentPath: string, unitNumber: number, unitName: string, unitType: LessonType) {
const unitFolderPath = `${parentPath}/${unitNumber}-${kebabCase(unitName)}`;
async function createUnitFolder(parent: Node, unitType: NodeType) {
const unitNumber = parent.children.length + 1;
const unitName = await getUnitName(unitType, unitNumber);
const unitFolderPath = parent.order ? kebabCase(unitName) : `${unitNumber}-${kebabCase(unitName)}`;

const metaFile = unitType === 'lesson' ? 'content.mdx' : 'meta.md';
const metaFilePath = vscode.Uri.joinPath(parent.path, unitFolderPath, metaFile);

if (parent.order) {
parent.pushChild(unitFolderPath);
await updateNodeMetadataInVFS(parent);
}

await vscode.workspace.fs.writeFile(
vscode.Uri.file(`${unitFolderPath}/${metaFile}`),
metaFilePath,
new TextEncoder().encode(`---\ntype: ${unitType}\ntitle: ${unitName}\n---\n`),
);

return unitFolderPath;
}

async function navigateToUnit(path: string, unitType: LessonType) {
const metaFile = unitType === 'lesson' ? 'content.mdx' : 'meta.md';

return cmd.goto(`${path}/${metaFile}`);
return {
folderPath: vscode.Uri.joinPath(parent.path, unitFolderPath),
metaFilePath,
};
}
18 changes: 18 additions & 0 deletions extensions/vscode/src/commands/tutorialkit.delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { cmd } from '.';
import * as vscode from 'vscode';
import { Node } from '../models/Node';
import { getLessonsTreeView } from '../global-state';

export async function deleteUnit(selectedNode: Node | undefined, selectedNodes: Node[] | undefined) {
let nodes: readonly Node[] = (selectedNodes ? selectedNodes : [selectedNode]).filter((node) => node !== undefined);

if (nodes.length === 0) {
nodes = getLessonsTreeView().selection;
}

for (const node of nodes) {
await vscode.workspace.fs.delete(node.path, { recursive: true });
}

return cmd.refresh();
}
19 changes: 17 additions & 2 deletions extensions/vscode/src/commands/tutorialkit.goto.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import * as vscode from 'vscode';

export default async (path: string | undefined) => {
export default async (path: string | vscode.Uri | undefined) => {
if (!path) {
return;
}

const document = await vscode.workspace.openTextDocument(path);
/**
* This cast to 'any' makes no sense because if we narrow the type of path
* there are no type errors. So this code:
*
* ```ts
* typeof path === 'string'
* ? await vscode.workspace.openTextDocument(path)
* : await vscode.workspace.openTextDocument(path)
* ;
* ```
*
* Type check correctly despite doing nothing different on each branch.
*
* To avoid this TypeScript bug here we just cast to any.
*/
const document = await vscode.workspace.openTextDocument(path as any);

await vscode.window.showTextDocument(document, {
preserveFocus: true,
Expand Down
22 changes: 14 additions & 8 deletions extensions/vscode/src/commands/tutorialkit.load-tutorial.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import * as vscode from 'vscode';
import { extContext } from '../extension';
import { LessonsTreeDataProvider, getLessonsTreeDataProvider, setLessonsTreeDataProvider } from '../views/lessonsTree';
import { LessonsTreeDataProvider } from '../views/lessonsTree';
import { setLessonsTreeDataProvider, setLessonsTreeView } from '../global-state';

export async function loadTutorial(uri: vscode.Uri) {
setLessonsTreeDataProvider(new LessonsTreeDataProvider(uri, extContext));
const treeDataProvider = new LessonsTreeDataProvider(uri, extContext);

extContext.subscriptions.push(
vscode.window.createTreeView('tutorialkit-lessons-tree', {
treeDataProvider: getLessonsTreeDataProvider(),
canSelectMany: true,
}),
);
await treeDataProvider.init();

const treeView = vscode.window.createTreeView('tutorialkit-lessons-tree', {
treeDataProvider,
canSelectMany: true,
});

setLessonsTreeDataProvider(treeDataProvider);
setLessonsTreeView(treeView);

extContext.subscriptions.push(treeView, treeDataProvider);

vscode.commands.executeCommand('setContext', 'tutorialkit:tree', true);
}
2 changes: 1 addition & 1 deletion extensions/vscode/src/commands/tutorialkit.refresh.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getLessonsTreeDataProvider } from '../views/lessonsTree';
import { getLessonsTreeDataProvider } from '../global-state';

export default () => {
getLessonsTreeDataProvider().refresh();
Expand Down
22 changes: 22 additions & 0 deletions extensions/vscode/src/global-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { TreeView } from 'vscode';
import type { LessonsTreeDataProvider } from './views/lessonsTree';
import type { Node } from './models/Node';

let lessonsTreeDataProvider: LessonsTreeDataProvider;
let lessonsTreeView: TreeView<Node>;

export function getLessonsTreeDataProvider() {
return lessonsTreeDataProvider;
}

export function getLessonsTreeView() {
return lessonsTreeView;
}

export function setLessonsTreeDataProvider(provider: LessonsTreeDataProvider) {
lessonsTreeDataProvider = provider;
}

export function setLessonsTreeView(treeView: TreeView<Node>) {
lessonsTreeView = treeView;
}
15 changes: 0 additions & 15 deletions extensions/vscode/src/models/Lesson.ts

This file was deleted.

83 changes: 83 additions & 0 deletions extensions/vscode/src/models/Node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as vscode from 'vscode';
import type { TutorialSchema, PartSchema, ChapterSchema, LessonSchema } from '@tutorialkit/types';

export class Node {
/**
* Path to the meta.md / content.md file.
*/
metadataFilePath?: vscode.Uri;

/**
* The metadata read from the metadata file.
*/
metadata?: Metadata;

/**
* The number of expected children, populated on creation.
* If an order is specified but more folder are found, they
* are also included in that count but end up at the end of
* the tree.
*/
childCount: number = 0;

/**
* The children of that node, loaded lazily.
*/
children: Node[] = [];

/**
* If specified, describe the order of the children.
* When children are loaded, this should be used to sort
* them appropriately.
*/
order?: Map<string, number>;

get type() {
return this.metadata?.type;
}

get name() {
if (this._customName) {
return this._customName;
}

if (this.metadata && this.metadata.type !== 'tutorial') {
return this.metadata.title;
}

return '<no name>';
}

constructor(
public folderName: string,
readonly path: vscode.Uri,
private _customName?: string,
) {}

pushChild(folderPath: string) {
this.childCount += 1;

if (this.order) {
this.order.set(folderPath, this.order.size);

switch (this.metadata?.type) {
case 'chapter': {
this.metadata.lessons!.push(folderPath);
break;
}
case 'tutorial': {
this.metadata.parts!.push(folderPath);
break;
}
case 'part': {
this.metadata.chapters!.push(folderPath);
break;
}
}
}
}
}

export type Metadata = PartSchema | ChapterSchema | LessonSchema | TutorialSchema;

export type NodeType = Metadata['type'];
Loading