From 19e0723466ea309f0726f142917b4f890716b707 Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Mon, 30 Mar 2020 22:02:11 -0700 Subject: [PATCH] Updating tour tree --- CHANGELOG.md | 3 +- package.json | 26 +++--- src/commands.ts | 18 ++-- src/extension.ts | 39 +++----- src/fileSystem/index.ts | 21 +++-- src/store/actions.ts | 196 ++++------------------------------------ src/store/provider.ts | 54 ++++++----- src/tree/index.ts | 13 +-- 8 files changed, 96 insertions(+), 274 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9903845..63b1ef3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,11 @@ ## v0.0.16 (03/30/2020) -- Fixed some bugs with the `CodeTour` tree +- Updated the `CodeTour` tree to display the currently active tour, regardless how it was started (e.g. you open a tour file). ## v0.0.15 (03/29/2020) - Updated the `CodeTour` tree to only display if the currently open workspace has any tours, or if the user is currently taking a tour. That way, it isn't obtrusive to users that aren't currently using it. - Updated the `CodeTour: Refresh Tours` command to only show up when the currently opened workspace has any tours. -- Updated the `CodeTour` tree to display the currently active tour, regardless how it was started. ## v0.0.14 (03/26/2020) diff --git a/package.json b/package.json index 9a00c83..970a3bd 100644 --- a/package.json +++ b/package.json @@ -51,19 +51,13 @@ "command": "codetour.changeTourTitle", "title": "Change Title" }, - { - "command": "codetour.deleteTour", - "title": "Delete Tour" - }, { "command": "codetour.deleteTourStep", "title": "Delete Step" }, { - "command": "codetour.editTour", - "title": "Edit Tour", - "category": "CodeTour", - "icon": "$(edit)" + "command": "codetour.deleteTour", + "title": "Delete Tour" }, { "command": "codetour.editTourAtStep", @@ -73,6 +67,12 @@ "command": "codetour.editTourStep", "title": "Edit Step" }, + { + "command": "codetour.editTour", + "title": "Edit Tour", + "category": "CodeTour", + "icon": "$(edit)" + }, { "command": "codetour.endTour", "title": "End Tour", @@ -83,16 +83,16 @@ "command": "codetour.exportTour", "title": "Export Tour..." }, - { - "command": "codetour.moveTourStepBack", - "title": "Move Up", - "icon": "$(arrow-up)" - }, { "command": "codetour.moveTourStepForward", "title": "Move Down", "icon": "$(arrow-down)" }, + { + "command": "codetour.moveTourStepBack", + "title": "Move Up", + "icon": "$(arrow-up)" + }, { "command": "codetour.nextTourStep", "title": "Next", diff --git a/src/commands.ts b/src/commands.ts index 2aec894..e70c611 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -5,9 +5,7 @@ import { endCurrentCodeTour, moveCurrentCodeTourBackward, moveCurrentCodeTourForward, - startCodeTour, - resumeCurrentCodeTour, - CodeTourComment + startCodeTour } from "./store/actions"; import { discoverTours } from "./store/provider"; import { CodeTourNode, CodeTourStepNode } from "./tree/nodes"; @@ -16,6 +14,7 @@ import { api, RefType } from "./git"; import * as path from "path"; import { getStepFileUri } from "./utils"; import { workspace } from "vscode"; +import { focusPlayer, CodeTourComment } from "./player"; interface CodeTourQuickPickItem extends vscode.QuickPickItem { tour: CodeTour; } @@ -33,7 +32,7 @@ export function registerCommands() { return startCodeTour(targetTour, stepNumber, workspaceRoot); } - let items: CodeTourQuickPickItem[] = store.tours.map(tour => ({ + const items: CodeTourQuickPickItem[] = store.tours.map(tour => ({ label: tour.title!, tour: tour, detail: tour.description @@ -78,10 +77,7 @@ export function registerCommands() { } ); - vscode.commands.registerCommand( - `${EXTENSION_NAME}.resumeTour`, - resumeCurrentCodeTour - ); + vscode.commands.registerCommand(`${EXTENSION_NAME}.resumeTour`, focusPlayer); function getTourFileUri(title: string) { const file = title @@ -225,6 +221,7 @@ export function registerCommands() { } } } + vscode.commands.registerCommand( `${EXTENSION_NAME}.addTourStep`, (reply: vscode.CommentReply) => { @@ -460,10 +457,7 @@ export function registerCommands() { "Delete Tour" ) ) { - if ( - store.activeTour && - node.tour.title === store.activeTour.tour.title - ) { + if (store.activeTour && node.tour.id === store.activeTour.tour.id) { await endCurrentCodeTour(); } diff --git a/src/extension.ts b/src/extension.ts index 45fe54a..54b2c95 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,46 +1,35 @@ import * as vscode from "vscode"; import { registerCommands } from "./commands"; -import { EXTENSION_NAME } from "./constants"; +import { registerFileSystemProvider } from "./fileSystem"; +import { initializeGitApi } from "./git"; import { registerStatusBar } from "./status"; -import { store } from "./store"; +import { + endCurrentCodeTour, + promptForTour, + startCodeTour +} from "./store/actions"; import { discoverTours } from "./store/provider"; import { registerTreeProvider } from "./tree"; -import { initializeGitApi } from "./git"; -import { startCodeTour, endCurrentCodeTour } from "./store/actions"; -import { registerFileSystemProvider } from "./fileSystem"; - -async function promptForTour( - workspaceRoot: string, - globalState: vscode.Memento -) { - const key = `${EXTENSION_NAME}:${workspaceRoot}`; - if (store.hasTours && !globalState.get(key)) { - globalState.update(key, true); - - if ( - await vscode.window.showInformationMessage( - "This workspace has guided tours you can take to get familiar with the codebase.", - "Start CodeTour" - ) - ) { - vscode.commands.executeCommand(`${EXTENSION_NAME}.startTour`); - } - } -} export async function activate(context: vscode.ExtensionContext) { registerCommands(); + // If the user has a workspace open, then attempt to discover + // the tours contained within it and optionally prompt the user. if (vscode.workspace.workspaceFolders) { const workspaceRoot = vscode.workspace.workspaceFolders[0].uri.toString(); await discoverTours(workspaceRoot); - registerTreeProvider(context.extensionPath); promptForTour(workspaceRoot, context.globalState); initializeGitApi(); } + // Regardless if the user has a workspace open, + // we still need to register the following items + // in order to support opening tour files and/or + // enabling other extensions to start a tour. + registerTreeProvider(context.extensionPath); registerFileSystemProvider(); registerStatusBar(); diff --git a/src/fileSystem/index.ts b/src/fileSystem/index.ts index 5f80afa..e4d13eb 100644 --- a/src/fileSystem/index.ts +++ b/src/fileSystem/index.ts @@ -11,19 +11,15 @@ import { Uri, workspace } from "vscode"; -import { store, CodeTour, CodeTourStep } from "../store"; import { FS_SCHEME } from "../constants"; +import { CodeTour, CodeTourStep, store } from "../store"; export class CodeTourFileSystemProvider implements FileSystemProvider { private count = 0; - private _onDidChangeFile = new EventEmitter(); - public readonly onDidChangeFile: Event = this - ._onDidChangeFile.event; - getCurrentTourStep(): [CodeTour, CodeTourStep] { - const tour = store.activeTour?.tour!; - return [tour, tour?.steps[store.activeTour!.step]!]; + const tour = store.activeTour!.tour; + return [tour, tour.steps[store.activeTour!.step]]; } updateTour(tour: CodeTour) { @@ -72,13 +68,19 @@ export class CodeTourFileSystemProvider implements FileSystemProvider { this.updateTour(tour); } + // Unimplemented members + + private _onDidChangeFile = new EventEmitter(); + public readonly onDidChangeFile: Event = this + ._onDidChangeFile.event; + async copy?( source: Uri, destination: Uri, options: { overwrite: boolean } ): Promise { throw FileSystemError.NoPermissions( - "CodeTour doesn't support copying files" + "CodeTour doesn't support copying files." ); } @@ -90,9 +92,10 @@ export class CodeTourFileSystemProvider implements FileSystemProvider { async delete(uri: Uri, options: { recursive: boolean }): Promise { throw FileSystemError.NoPermissions( - "CodeTour doesn't support deleting files" + "CodeTour doesn't support deleting files." ); } + async readDirectory(uri: Uri): Promise<[string, FileType][]> { throw FileSystemError.NoPermissions("CodeTour doesnt support directories."); } diff --git a/src/store/actions.ts b/src/store/actions.ts index 3065850..7316c75 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -1,137 +1,11 @@ -import { - commands, - Comment, - CommentAuthorInformation, - CommentMode, - comments, - CommentThread, - CommentThreadCollapsibleState, - MarkdownString, - Range, - TextEditorRevealType, - Uri, - window, - workspace, - TextDocument, - CommentController, - Selection -} from "vscode"; +import { commands, Memento, Uri, window } from "vscode"; import { CodeTour, store } from "."; import { EXTENSION_NAME, FS_SCHEME } from "../constants"; -import { reaction } from "mobx"; -import { getStepFileUri } from "../utils"; +import { startPlayer, stopPlayer } from "../player"; const CAN_EDIT_TOUR_KEY = `${EXTENSION_NAME}:canEditTour`; const IN_TOUR_KEY = `${EXTENSION_NAME}:inTour`; -const CONTROLLER_ID = "codetour"; -const CONTROLLER_LABEL = "CodeTour"; -const CONTROLLER_ICON = Uri.parse( - "https://cdn.jsdelivr.net/gh/vsls-contrib/code-tour/images/icon.png" -); - -let id = 0; -export class CodeTourComment implements Comment { - public id: string = (++id).toString(); - public contextValue: string = ""; - public mode: CommentMode = CommentMode.Preview; - public author: CommentAuthorInformation = { - name: CONTROLLER_LABEL, - iconPath: CONTROLLER_ICON - }; - - constructor( - public body: string | MarkdownString, - public label: string = "", - public parent: CommentThread - ) {} -} - -let controller: CommentController; - -async function showDocument(uri: Uri, range: Range, selection?: Selection) { - const document = - window.visibleTextEditors.find( - editor => editor.document.uri.toString() === uri.toString() - ) || (await window.showTextDocument(uri, { preserveFocus: true })); - - // TODO: Figure out how to force focus when navigating - // to documents which are already open. - - if (selection) { - document.selection = selection; - } - - document.revealRange(range, TextEditorRevealType.InCenter); -} - -async function renderCurrentStep() { - if (store.activeTour!.thread) { - store.activeTour!.thread.dispose(); - } - - const currentTour = store.activeTour!.tour; - const currentStep = store.activeTour!.step; - - const step = currentTour!.steps[currentStep]; - - if (!step) { - return; - } - - // Adjust the line number, to allow the user to specify - // them in 1-based format, not 0-based - const line = step.line ? step.line - 1 : 2000; - const range = new Range(line, 0, line, 0); - let label = `Step #${currentStep + 1} of ${currentTour!.steps.length}`; - - if (currentTour.title) { - label += ` (${currentTour.title})`; - } - - const workspaceRoot = store.activeTour!.workspaceRoot - ? store.activeTour!.workspaceRoot.toString() - : workspace.workspaceFolders - ? workspace.workspaceFolders[0].uri.toString() - : ""; - - const uri = await getStepFileUri(step, workspaceRoot, currentTour.ref); - store.activeTour!.thread = controller.createCommentThread(uri, range, []); - store.activeTour!.thread.comments = [ - new CodeTourComment(step.description, label, store.activeTour!.thread!) - ]; - - const contextValues = []; - if (currentStep > 0) { - contextValues.push("hasPrevious"); - } - - if (currentStep < currentTour.steps.length - 1) { - contextValues.push("hasNext"); - } - - store.activeTour!.thread.contextValue = contextValues.join("."); - store.activeTour!.thread.collapsibleState = - CommentThreadCollapsibleState.Expanded; - - let selection; - if (step.selection) { - // Adjust the 1-based positions - // to the 0-based positions that - // VS Code's editor uses. - selection = new Selection( - step.selection.start.line - 1, - step.selection.start.character - 1, - step.selection.end.line - 1, - step.selection.end.character - 1 - ); - } else { - selection = new Selection(range.start, range.end); - } - - showDocument(uri, range, selection); -} - export function startCodeTour( tour: CodeTour, stepNumber?: number, @@ -139,26 +13,7 @@ export function startCodeTour( startInEditMode: boolean = false, canEditTour: boolean = true ) { - if (controller) { - controller.dispose(); - } - - controller = comments.createCommentController( - CONTROLLER_ID, - CONTROLLER_LABEL - ); - - // TODO: Correctly limit the commenting ranges - // to files within the workspace root - controller.commentingRangeProvider = { - provideCommentingRanges: (document: TextDocument) => { - if (store.isRecording) { - return [new Range(0, 0, document.lineCount, 0)]; - } else { - return null; - } - } - }; + startPlayer(); store.activeTour = { tour, @@ -182,14 +37,7 @@ export async function endCurrentCodeTour() { commands.executeCommand("setContext", "codetour:recording", false); } - if (store.activeTour?.thread) { - store.activeTour!.thread.dispose(); - store.activeTour!.thread = null; - } - - if (controller) { - controller.dispose(); - } + stopPlayer(); store.activeTour = null; commands.executeCommand("setContext", IN_TOUR_KEY, false); @@ -209,27 +57,21 @@ export function moveCurrentCodeTourForward() { store.activeTour!.step++; } -export function resumeCurrentCodeTour() { - showDocument(store.activeTour!.thread!.uri, store.activeTour!.thread!.range); -} +export async function promptForTour( + workspaceRoot: string, + globalState: Memento +) { + const key = `${EXTENSION_NAME}:${workspaceRoot}`; + if (store.hasTours && !globalState.get(key)) { + globalState.update(key, true); -reaction( - () => [ - store.activeTour - ? [ - store.activeTour.step, - store.activeTour.tour.title, - store.activeTour.tour.steps.map(step => [ - step.title, - step.description, - step.line - ]) - ] - : null - ], - () => { - if (store.activeTour) { - renderCurrentStep(); + if ( + await window.showInformationMessage( + "This workspace has guided tours you can take to get familiar with the codebase.", + "Start CodeTour" + ) + ) { + commands.executeCommand(`${EXTENSION_NAME}.startTour`); } } -); +} diff --git a/src/store/provider.ts b/src/store/provider.ts index 6587d4c..085cee6 100644 --- a/src/store/provider.ts +++ b/src/store/provider.ts @@ -1,10 +1,8 @@ +import { comparer, runInAction, set } from "mobx"; import * as vscode from "vscode"; -import { CodeTour } from "."; -import { store } from "."; -import { VSCODE_DIRECTORY, EXTENSION_NAME } from "../constants"; +import { CodeTour, store } from "."; +import { EXTENSION_NAME, VSCODE_DIRECTORY } from "../constants"; import { endCurrentCodeTour } from "./actions"; -import { set, runInAction } from "mobx"; -import { comparer } from "mobx"; const MAIN_TOUR_FILES = [ `${EXTENSION_NAME}.json`, @@ -17,11 +15,11 @@ const SUB_TOUR_DIRECTORY = `${VSCODE_DIRECTORY}/tours`; const HAS_TOURS_KEY = `${EXTENSION_NAME}:hasTours`; export async function discoverTours(workspaceRoot: string): Promise { - const mainTour = await discoverMainTour(workspaceRoot); + const mainTours = await discoverMainTours(workspaceRoot); const tours = await discoverSubTours(workspaceRoot); - if (mainTour) { - tours.push(mainTour); + if (mainTours) { + tours.push(...mainTours); } runInAction(() => { @@ -39,7 +37,7 @@ export async function discoverTours(workspaceRoot: string): Promise { } else { // The user deleted the tour // file that's associated with - // the active tour + // the active tour, so end it endCurrentCodeTour(); } } @@ -48,22 +46,22 @@ export async function discoverTours(workspaceRoot: string): Promise { vscode.commands.executeCommand("setContext", HAS_TOURS_KEY, store.hasTours); } -async function discoverMainTour( - workspaceRoot: string -): Promise { - for (const tourFile of MAIN_TOUR_FILES) { - try { - const uri = vscode.Uri.parse(`${workspaceRoot}/${tourFile}`); - const mainTourContent = ( - await vscode.workspace.fs.readFile(uri) - ).toString(); - const tour = JSON.parse(mainTourContent); - tour.id = uri.toString(); - return tour; - } catch {} - } +async function discoverMainTours(workspaceRoot: string): Promise { + const tours = await Promise.all( + MAIN_TOUR_FILES.map(async tourFile => { + try { + const uri = vscode.Uri.parse(`${workspaceRoot}/${tourFile}`); + const mainTourContent = ( + await vscode.workspace.fs.readFile(uri) + ).toString(); + const tour = JSON.parse(mainTourContent); + tour.id = uri.toString(); + return tour; + } catch {} + }) + ); - return null; + return tours.filter(tour => tour); } async function discoverSubTours(workspaceRoot: string): Promise { @@ -89,15 +87,15 @@ async function discoverSubTours(workspaceRoot: string): Promise { } } -const watcher = vscode.workspace.createFileSystemWatcher( - "**/.vscode/tours/*.json" -); - function updateTours() { const workspaceRoot = vscode.workspace.workspaceFolders![0].uri.toString(); discoverTours(workspaceRoot); } +const watcher = vscode.workspace.createFileSystemWatcher( + "**/.vscode/tours/*.json" +); + watcher.onDidChange(updateTours); watcher.onDidCreate(updateTours); watcher.onDidDelete(updateTours); diff --git a/src/tree/index.ts b/src/tree/index.ts index ce72a1d..e244488 100644 --- a/src/tree/index.ts +++ b/src/tree/index.ts @@ -7,9 +7,9 @@ import { TreeItem, window } from "vscode"; +import { EXTENSION_NAME } from "../constants"; import { store } from "../store"; import { CodeTourNode, CodeTourStepNode, RecordTourNode } from "./nodes"; -import { EXTENSION_NAME } from "../constants"; class CodeTourTreeProvider implements TreeDataProvider, Disposable { private _disposables: Disposable[] = []; @@ -48,9 +48,9 @@ class CodeTourTreeProvider implements TreeDataProvider, Disposable { if (!store.hasTours && !store.activeTour) { return [new RecordTourNode()]; } else { - const tours = store.tours.map(tour => { - return new CodeTourNode(tour, this.extensionPath); - }); + const tours = store.tours.map( + tour => new CodeTourNode(tour, this.extensionPath) + ); if ( store.activeTour && @@ -107,10 +107,7 @@ export function registerTreeProvider(extensionPath: string) { () => { if (store.activeTour) { treeView.reveal( - new CodeTourStepNode(store.activeTour.tour, store.activeTour!.step), - { - focus: true - } + new CodeTourStepNode(store.activeTour.tour, store.activeTour!.step) ); } else { // TODO: Once VS Code supports it, we want