diff --git a/CHANGELOG.md b/CHANGELOG.md index a20cd33..dcbc010 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## v0.0.34 (06/27/2020) + +- Updated the tour recorder, to allow you to edit the line associated with a step +- Updated the tour recorder, to allow you to add a tour step from an editor selection +- Added the ability to record a new tour that is saved to an arbitrary location on disk, as opposed to the `.tours` directory of the opened workspace. +- Added new extensibility APIs to record and playback tours for external workspaces (e.g. GistPad repo editing). +- Updated the `CodeTour` tree to always show when you're taking a tour, even if you don't have a workspace open. + ## v0.0.33 (06/18/2020) - Fixed an issue where CodeTour overrode the JSON language type @@ -75,7 +83,7 @@ - Introduced support for embedding shell commands in a tour step (e.g. `>> npm run compile`), which allows you to add more interactivity to a tour. - Added support for including VS Code `command:` links within your tour step comments (e.g. `[Start Tour](command:codetour.startTour)`), in order to automate arbitrary workbench actions. -- Tours can now be organized within sub-directories of the `.vscode/tours` drectory, and can now also be places withtin a root-level `.tours` folder. +- Tours can now be organized within sub-directories of the `.vscode/tours` directory, and can now also be places withtin a root-level `.tours` folder. - Added the `exportTour` to the API that is exposed by this extension ## v0.0.19 (04/06/2020) diff --git a/README.md b/README.md index 7cc12e9..18b3ac1 100644 --- a/README.md +++ b/README.md @@ -8,89 +8,15 @@ CodeTour is a Visual Studio Code extension, which allows you to record and playb In order to get started, install the [CodeTour extension](https://aka.ms/codetour), and then following one of the following guides, depending on whether you want to record or playback a tour: -- [Starting Tours](#starting-tours) -- [Navigating Tours](#navigating-tours) - [Recording Tours](#recording-tours) - [Exporting Tours](#exporting-tours) +- [Starting Tours](#starting-tours) +- [Navigating Tours](#navigating-tours) - [Reference](#reference) -## Starting Tours - -In order to start a tour, simply open up a codebase that has one or more tours. If this is the first time you've ever opened this codebase, you'll be presented with a toast notification asking if you'd like to take a tour of it. - - - -Otherwise, you can manually start a tour via any of the following methods: - -1. Selecting a tour (or specific step) in the [`CodeTour` view](#tree-view) in the `Explorer` activity tab - - - -1. Running the `CodeTour: Start Tour` [command](#contributed-commands), and selecting the tour you'd like to take - - - - If the current workspace only has a single code tour, then this command will automatically start that tour. Otherwise, you'll be presented with a list of tours to select from. - -### Opening Tours - -In addition to taking tours that are part of the currently open workspace, you can also open a tour file that someone else sent you and/or you created yourself. Simply run the `CodeTour: Open Tour File...` command and/or click the folder icon in the title bar of the `CodeTour` tree view. - -> Note: The `CodeTour` tree view only appears if the currently opened workspace has any tours and/or you're currently taking a tour. - -Additionally, if someone has [exported](#exporting-tours) a tour, and uploaded it to a publically accessible location, they can send you the URL, and you can open it by running the `CodeTour: Open Tour URL...` command. - -### Tour Markers - -As you explore a codebase, you might encounter a "tour marker", which displays the CodeTour icon in the file gutter. This indicates that a line of code participates in a tour for the open workspace, which makes it easier to discover tours that might be relevant to what you're currently working on. When you see a marker, simply hover over the line and click the `Start Tour` link in the hover tooltip. This will start the tour that's associated with this line of code, at the specific step. - - - -If you want to disable tour markers, you can perform one of the following actions: - -- Run the `CodeTour: Hide Tour Markers` command -- Click the "eye icon" in the title bar of the `CodeTour` tree view -- Set the `codetour.showMarkers` configuration setting to `false`. _Note that the above two actions do this for you automatically._ - - - -## Navigating Tours - -Once you've started a tour, the comment UI will guide you, and includes navigation actions that allow you to perform the following: - -- `Move Previous` - Navigate to the previous step in the current tour. This command is visible for step #2 and later. -- `Move Next` - Navigate to the next step in the current tour. This command is visible for all steps but the last one in a tour. -- `Edit Tour` - Begin editing the current tour (see [authoring](#authoring-tours) for details). Note that not all tours are editable, so depending on how you started the tour, you may or may not see this action. -- `End Tour` - End the current tour and remove the comment UI. - - - -Additionally, you can use the `ctrl+right` / `ctrl+left` (Windows/Linux) and `cmd+right` / `cmd+left` (macOS) keyboard shortcuts to move forwards and backwards in the tour. The `CodeTour` tree view and status bar is also kept in sync with your current tour/step, to help the developer easily understand where they're at in the context of the broader tour. - - - -If you navigate away from the current step and need to resume, you can do that via any of the following actions: - -- Right-clicking the active tour in the `CodeTour` tree and selecting `Resume Tour` -- Clicking the `CodeTour` status bar item -- Running the `CodeTour: Resume Tour` command in the command palette - -At any time, you can end the current code tour by means of one of the following actions: - -- Click the stop button (the red square) in the current step comment -- Click the stop button next to the active tour in the `CodeTour` tree -- Running the `CodeTour: End Tour` command in the command palette - ## Recording Tours -If you'd like to record a code tour for your codebase, you can simply click the `+` button in the `CodeTour` tree view (if it's visible) and/or run the `CodeTour: Record Tour` command. This will start the tour recorder, which allows you to begin opening files, clicking the "comment bar" for the line you want to annotate, and then adding the respective description (including markdown!). Add as many steps as you want, and then when done, simply click the stop tour action (the red square button). You can also create [directory steps](#directory-steps), or [content steps](#content-steps) to add an introductory or intermediate explainations to a tour. +If you'd like to record a code tour for your codebase, you can simply click the `+` button in the `CodeTour` tree view (if it's visible) and/or run the `CodeTour: Record Tour` command. This will start the tour recorder, which allows you to begin opening files, clicking the "comment bar" for the line you want to annotate, and then adding the respective description (including markdown!). Add as many steps as you want, and then when done, simply click the stop tour action (the red square button). You can also create [directory steps](#directory-steps), [selection steps](#text-selection), or [content steps](#content-steps) in order to add an introductory or intermediate explanations to a tour. While you're recording, the `CodeTour` [tree view](#tree-view) will display the currently recorded tour, and it's current set of steps. You can tell which tour is being recorded because it will have a microphone icon to the left of its name. @@ -119,7 +45,7 @@ By default, each step is associated with the line of code you created the commen -If you need to tweak the selection that's associated with a step, simply edit the step, reset the selection and then save it. +If you need to tweak the selection that's associated with a step, simply edit the step, reset the selection and then save it. Furthermore, if you want to create a step from a selection, simply highlight a span a code, right-click the editor and select `Add CodeTour Step`. ### Re-arranging steps @@ -261,7 +187,7 @@ For an example, refer to the `.tours/tree.tour` file of this repository. By default, when you record a tour, it is written to the currently open workspace. This makes it easy to check-in the tour and share it with the rest of the team. However, there may be times where you want to record a tour for yourself, or a tour to help explain a one-off to someone, and in those situations, you might not want to check the tour into the repo. -So support this, after you finish recording a tour, you can right-click it in the `CodeTour` tree and select `Export Tour...`. This will allow you to save the tour to a new location, and then you can delete the tour file from your repo. Furthermore, when you export a tour, the tour file itself will embed the contents of all files needed by the tour, which ensures that someone can play it back, regardless if the have the respective code available locally. This enables a powerful form of collaboration. +To support this scenario, when you start recording a new tour, you can click the `Save tour as...` button in the upper-right side of the dialog that asks for the title of the tour. This wll allow you to select the file that the new tour will be written to, so that it isn't persisted to the workspace. Furthermore, you can record a tour as usual, and then when done, you can right-click it in the `CodeTour` tree and select `Export Tour...`. This will allow you to save the tour to a new location, and then you can delete the tour file from your repo. When you export a tour, the tour file itself will embed the contents of all files needed by the tour, which ensures that someone can play it back, regardless if the have the respective code available locally. This enables a powerful form of collaboration. @@ -271,6 +197,80 @@ If you install the [GistPad](https://aka.ms/gistpad) extension, then you'll see Once a tour is exported as a gist, you can right-click the `main.tour` file in the `GistPad` tree, and select `Copy GitHub URL`. If you send that to someone, and they run the `CodeTour: Open Tour URL...` command, then they'll be able to take the exact same tour, regardless if they have the code locally available or not. +## Starting Tours + +In order to start a tour, simply open up a codebase that has one or more tours. If this is the first time you've ever opened this codebase, you'll be presented with a toast notification asking if you'd like to take a tour of it. + + + +Otherwise, you can manually start a tour via any of the following methods: + +1. Selecting a tour (or specific step) in the [`CodeTour` view](#tree-view) in the `Explorer` activity tab + + + +1. Running the `CodeTour: Start Tour` [command](#contributed-commands), and selecting the tour you'd like to take + + + + If the current workspace only has a single code tour, then this command will automatically start that tour. Otherwise, you'll be presented with a list of tours to select from. + +### Opening Tours + +In addition to taking tours that are part of the currently open workspace, you can also open a tour file that someone else sent you and/or you created yourself. Simply run the `CodeTour: Open Tour File...` command and/or click the folder icon in the title bar of the `CodeTour` tree view. + +> Note: The `CodeTour` tree view only appears if the currently opened workspace has any tours and/or you're currently taking a tour. + +Additionally, if someone has [exported](#exporting-tours) a tour, and uploaded it to a publically accessible location, they can send you the URL, and you can open it by running the `CodeTour: Open Tour URL...` command. + +### Tour Markers + +As you explore a codebase, you might encounter a "tour marker", which displays the CodeTour icon in the file gutter. This indicates that a line of code participates in a tour for the open workspace, which makes it easier to discover tours that might be relevant to what you're currently working on. When you see a marker, simply hover over the line and click the `Start Tour` link in the hover tooltip. This will start the tour that's associated with this line of code, at the specific step. + + + +If you want to disable tour markers, you can perform one of the following actions: + +- Run the `CodeTour: Hide Tour Markers` command +- Click the "eye icon" in the title bar of the `CodeTour` tree view +- Set the `codetour.showMarkers` configuration setting to `false`. _Note that the above two actions do this for you automatically._ + + + +## Navigating Tours + +Once you've started a tour, the comment UI will guide you, and includes navigation actions that allow you to perform the following: + +- `Move Previous` - Navigate to the previous step in the current tour. This command is visible for step #2 and later. +- `Move Next` - Navigate to the next step in the current tour. This command is visible for all steps but the last one in a tour. +- `Edit Tour` - Begin editing the current tour (see [authoring](#authoring-tours) for details). Note that not all tours are editable, so depending on how you started the tour, you may or may not see this action. +- `End Tour` - End the current tour and remove the comment UI. + + + +Additionally, you can use the `ctrl+right` / `ctrl+left` (Windows/Linux) and `cmd+right` / `cmd+left` (macOS) keyboard shortcuts to move forwards and backwards in the tour. The `CodeTour` tree view and status bar is also kept in sync with your current tour/step, to help the developer easily understand where they're at in the context of the broader tour. + + + +If you navigate away from the current step and need to resume, you can do that via any of the following actions: + +- Right-clicking the active tour in the `CodeTour` tree and selecting `Resume Tour` +- Clicking the `CodeTour` status bar item +- Running the `CodeTour: Resume Tour` command in the command palette + +At any time, you can end the current code tour by means of one of the following actions: + +- Click the stop button (the red square) in the current step comment +- Click the stop button next to the active tour in the `CodeTour` tree +- Running the `CodeTour: End Tour` command in the command palette + ## Reference The following sections describe the VS Code integrations that the CodeTour extension contributes (e.g. tree, status bar, settings): @@ -330,8 +330,8 @@ In addition to the available commands, the Code Tour extension also contributes In order to enable other extensions to contribute/manage their own code tours, the CodeTour extension exposes an API with the following methods: -- `startTour(tour: CodeTour, stepNumber: number, workspaceRoot: Uri, startInEditMode: boolean = false, canEditTour: boolean): void` - Starts the specified tour, at a specific step, and using a specific workspace root to resolve relative file paths. Additionally, you can specify whether the tour should be started in edit/record mode or not, as well as whether the tour should be editable. Once the tour has been started, the end-user can use the status bar, command palette, key bindings and comment UI to navigate and edit the tour, just like a "normal" tour. - - `endCurrentTour(): void` - Ends the currently running tour (if there is one). Note that this is simply a programatic way to end the tour, and the end-user can also choose to end the tour using either the command palette (running the `CodeTour: End Tour` command) or comment UI (clicking the red square, stop icon) as usual. - `exportTour(tour: CodeTour): Promise` - Exports a `CodeTour` instance into a fully-embedded tour file, that can then be written to some persistent storage (e.g. a GitHub Gist). + +- `startTour(tour: CodeTour, stepNumber: number, workspaceRoot: Uri, startInEditMode: boolean = false, canEditTour: boolean): void` - Starts the specified tour, at a specific step, and using a specific workspace root to resolve relative file paths. Additionally, you can specify whether the tour should be started in edit/record mode or not, as well as whether the tour should be editable. Once the tour has been started, the end-user can use the status bar, command palette, key bindings and comment UI to navigate and edit the tour, just like a "normal" tour. diff --git a/package.json b/package.json index 0234344..5f38a5e 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,10 @@ "command": "codetour.addDirectoryStep", "title": "Add CodeTour Step" }, + { + "command": "codetour.addSelectionStep", + "title": "Add CodeTour Step" + }, { "command": "codetour.addTourStep", "title": "Add Step to Tour", @@ -66,6 +70,10 @@ "command": "codetour.changeTourRef", "title": "Change Git Ref" }, + { + "command": "codetour.changeTourStepLine", + "title": "Change Line" + }, { "command": "codetour.changeTourStepTitle", "title": "Change Title" @@ -228,6 +236,10 @@ "command": "codetour.addDirectoryStep", "when": "false" }, + { + "command": "codetour.addSelectionStep", + "when": "false" + }, { "command": "codetour.addTourStep", "when": "false" @@ -236,6 +248,10 @@ "command": "codetour.changeTourRef", "when": "false" }, + { + "command": "codetour.changeTourStepLine", + "when": "false" + }, { "command": "codetour.changeTourStepTitle", "when": "false" @@ -336,7 +352,12 @@ { "command": "codetour.moveTourStepForward", "group": "move@2", - "when": "commentController == codetour && && codetour:canEditTour commentThread =~ /hasNext/" + "when": "commentController == codetour && codetour:canEditTour commentThread =~ /hasNext/" + }, + { + "command": "codetour.changeTourStepLine", + "group": "mutate@1", + "when": "commentController == codetour && codetour:canEditTour" }, { "command": "codetour.deleteTourStep", @@ -490,6 +511,13 @@ "command": "codetour.addDirectoryStep", "when": "codetour:recording && explorerResourceIsFolder" } + ], + "editor/context": [ + { + "command": "codetour.addSelectionStep", + "when": "codetour:recording && editorHasSelection", + "group": "codetour@1" + } ] }, "views": { @@ -497,7 +525,7 @@ { "id": "codetour.tours", "name": "CodeTour", - "when": "workspaceFolderCount != 0" + "when": "workspaceFolderCount != 0 || codetour:inTour" } ] }, diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..c677e4a --- /dev/null +++ b/src/api.ts @@ -0,0 +1,22 @@ +import { ExtensionContext } from "vscode"; +import { + endCurrentCodeTour, + exportTour, + onDidEndTour, + promptForTour, + recordTour, + selectTour, + startCodeTour +} from "./store/actions"; + +export function initializeApi(context: ExtensionContext) { + return { + endCurrentTour: endCurrentCodeTour, + exportTour, + onDidEndTour, + promptForTour: promptForTour.bind(null, context.globalState), + recordTour, + startTour: startCodeTour, + selectTour + }; +} diff --git a/src/extension.ts b/src/extension.ts index 43d2747..e3abc7f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,4 +1,5 @@ import * as vscode from "vscode"; +import { initializeApi } from "./api"; import { registerCommands } from "./commands"; import { registerFileSystemProvider } from "./fileSystem"; import { registerTextDocumentContentProvider } from "./fileSystem/documentProvider"; @@ -7,12 +8,7 @@ import { registerDecorators } from "./player/decorator"; import { registerStatusBar } from "./player/status"; import { registerCompletionProvider } from "./recorder/completionProvider"; import { store } from "./store"; -import { - endCurrentCodeTour, - exportTour, - promptForTour, - startCodeTour -} from "./store/actions"; +import { promptForTour } from "./store/actions"; import { discoverTours } from "./store/provider"; import { registerTreeProvider } from "./tree"; @@ -51,9 +47,5 @@ export async function activate(context: vscode.ExtensionContext) { registerStatusBar(); registerCompletionProvider(); - return { - startTour: startCodeTour, - exportTour, - endCurrentTour: endCurrentCodeTour - }; + return initializeApi(context); } diff --git a/src/player/commands.ts b/src/player/commands.ts index 77640f8..e7e0ceb 100644 --- a/src/player/commands.ts +++ b/src/player/commands.ts @@ -8,13 +8,11 @@ import { exportTour, moveCurrentCodeTourBackward, moveCurrentCodeTourForward, + selectTour, startCodeTour } from "../store/actions"; import { CodeTourNode } from "../tree/nodes"; import { readUriContents } from "../utils"; -interface CodeTourQuickPickItem extends vscode.QuickPickItem { - tour: CodeTour; -} let terminal: vscode.Terminal | null; export function registerPlayerCommands() { @@ -34,9 +32,17 @@ export function registerPlayerCommands() { vscode.commands.registerCommand( `${EXTENSION_NAME}.startTourByTitle`, async (title: string, stepNumber?: number) => { - const tour = store.tours.find(tour => tour.title === title); + const tours = store.activeTour?.tours || store.tours; + const tour = tours.find(tour => tour.title === title); if (tour) { - startCodeTour(tour, stepNumber && --stepNumber); + startCodeTour( + tour, + stepNumber && --stepNumber, + store.activeTour?.workspaceRoot, + undefined, + undefined, + store.activeTour?.tours + ); } } ); @@ -45,7 +51,14 @@ export function registerPlayerCommands() { vscode.commands.registerCommand( `${EXTENSION_NAME}.navigateToStep`, async (stepNumber: number) => { - startCodeTour(store.activeTour!.tour, --stepNumber); + startCodeTour( + store.activeTour!.tour, + --stepNumber, + store.activeTour?.workspaceRoot, + undefined, + undefined, + store.activeTour?.tours + ); } ); @@ -77,30 +90,22 @@ export function registerPlayerCommands() { async ( tour?: CodeTour | CodeTourNode, stepNumber?: number, - workspaceRoot?: vscode.Uri + workspaceRoot?: vscode.Uri, + tours?: CodeTour[] ) => { if (tour) { const targetTour = tour instanceof CodeTourNode ? tour.tour : tour; - return startCodeTour(targetTour, stepNumber, workspaceRoot); + return startCodeTour( + targetTour, + stepNumber, + workspaceRoot, + undefined, + undefined, + tours + ); } - const items: CodeTourQuickPickItem[] = store.tours.map(tour => ({ - label: tour.title!, - tour: tour, - detail: tour.description - })); - - if (items.length === 1) { - return startCodeTour(items[0].tour); - } - - const response = await vscode.window.showQuickPick(items, { - placeHolder: "Select the tour to start..." - }); - - if (response) { - startCodeTour(response.tour); - } + selectTour(store.tours, workspaceRoot); } ); diff --git a/src/player/index.ts b/src/player/index.ts index f13b825..b63c1d3 100644 --- a/src/player/index.ts +++ b/src/player/index.ts @@ -71,7 +71,8 @@ export class CodeTourComment implements Comment { return `[${title}](command:codetour.navigateToStep?${stepNumber} "Navigate to step #${stepNumber}")`; } - const tour = store.tours.find(tour => tour.title === tourTitle); + const tours = store.activeTour?.tours || store.tours; + const tour = tours.find(tour => tour.title === tourTitle); if (tour) { const args = [tourTitle]; @@ -166,7 +167,12 @@ async function renderCurrentStep() { // 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 line = step.line + ? step.line - 1 + : step.selection + ? step.selection.end.line - 1 + : 2000; + const range = new Range(line, 0, line, 0); let label = `Step #${currentStep + 1} of ${currentTour!.steps.length}`; diff --git a/src/recorder/commands.ts b/src/recorder/commands.ts index 9a50f92..301f4a6 100644 --- a/src/recorder/commands.ts +++ b/src/recorder/commands.ts @@ -1,11 +1,17 @@ import { action, comparer, runInAction } from "mobx"; +import * as path from "path"; import * as vscode from "vscode"; import { workspace } from "vscode"; import { EXTENSION_NAME, FS_SCHEME_CONTENT } from "../constants"; import { api, RefType } from "../git"; import { CodeTourComment } from "../player"; import { CodeTour, store } from "../store"; -import { endCurrentCodeTour, startCodeTour } from "../store/actions"; +import { + endCurrentCodeTour, + exportTour, + onDidEndTour, + startCodeTour +} from "../store/actions"; import { CodeTourNode, CodeTourStepNode } from "../tree/nodes"; import { getActiveWorkspacePath, getRelativePath } from "../utils"; @@ -16,8 +22,12 @@ export function registerRecorderCommands() { .replace(/\s/g, "-") .replace(/[^\w\d-_]/g, ""); + const prefix = workspaceRoot.path.endsWith("/") + ? workspaceRoot.path + : `${workspaceRoot.path}/`; + return workspaceRoot.with({ - path: `${workspaceRoot.path}/.tours/${file}.tour` + path: `${prefix}.tours/${file}.tour` }); } @@ -34,12 +44,18 @@ export function registerRecorderCommands() { async function writeTourFile( workspaceRoot: vscode.Uri, - title: string, + title: string | vscode.Uri, ref?: string ): Promise { - const uri = getTourFileUri(workspaceRoot, title); + const uri = + typeof title === "string" ? getTourFileUri(workspaceRoot, title) : title; - const tour = { title, steps: [] }; + const tourTitle = + typeof title === "string" + ? title + : path.basename(title.path).replace(".tour", ""); + + const tour = { title: tourTitle, steps: [] }; if (ref && ref !== "HEAD") { (tour as any).ref = ref; } @@ -53,24 +69,19 @@ export function registerRecorderCommands() { // @ts-ignore return tour as CodeTour; } + interface WorkspaceQuickPickItem extends vscode.QuickPickItem { uri: vscode.Uri; } const REENTER_TITLE_RESPONSE = "Re-enter title"; - vscode.commands.registerCommand( - `${EXTENSION_NAME}.recordTour`, - async (placeHolderTitle?: string) => { - const title = await vscode.window.showInputBox({ - prompt: "Specify the title of the tour", - value: placeHolderTitle - }); + async function recordTourInternal( + tourTitle: string | vscode.Uri, + workspaceRoot?: vscode.Uri + ) { + if (!workspaceRoot) { + workspaceRoot = workspace.workspaceFolders![0].uri; - if (!title) { - return; - } - - let workspaceRoot = workspace.workspaceFolders![0].uri; if (workspace.workspaceFolders!.length > 1) { const items: WorkspaceQuickPickItem[] = workspace.workspaceFolders!.map( ({ name, uri }) => ({ @@ -89,11 +100,14 @@ export function registerRecorderCommands() { workspaceRoot = response.uri; } + } + + if (typeof tourTitle === "string") { + const tourExists = await checkIfTourExists(workspaceRoot, tourTitle); - const tourExists = await checkIfTourExists(workspaceRoot, title); if (tourExists) { const response = await vscode.window.showErrorMessage( - `This workspace already includes a tour with the title "${title}."`, + `This workspace already includes a tour with the title "${tourTitle}."`, REENTER_TITLE_RESPONSE, "Overwrite existing tour" ); @@ -101,7 +115,8 @@ export function registerRecorderCommands() { if (response === REENTER_TITLE_RESPONSE) { return vscode.commands.executeCommand( `${EXTENSION_NAME}.recordTour`, - title + workspaceRoot, + tourTitle ); } else if (!response) { // If the end-user closes the error @@ -109,36 +124,76 @@ export function registerRecorderCommands() { return; } } + } - const ref = await promptForTourRef(workspaceRoot); - const tour = await writeTourFile(workspaceRoot, title, ref); + const ref = await promptForTourRef(workspaceRoot); + const tour = await writeTourFile(workspaceRoot, tourTitle, ref); - startCodeTour(tour); + startCodeTour(tour, 0, workspaceRoot, true); - store.isRecording = true; - await vscode.commands.executeCommand( - "setContext", - "codetour:recording", - true - ); + vscode.window.showInformationMessage( + "CodeTour recording started! Begin creating steps by opening a file, clicking the + button to the left of a line of code, and then adding the appropriate comments." + ); + } - if ( - await vscode.window.showInformationMessage( - "CodeTour recording started! Begin creating steps by opening a file, clicking the + button to the left of a line of code, and then adding the appropriate comments.", - "Cancel" - ) - ) { - const uri = vscode.Uri.parse(tour.id); - vscode.workspace.fs.delete(uri); + vscode.commands.registerCommand( + `${EXTENSION_NAME}.recordTour`, + async (workspaceRoot?: vscode.Uri, placeHolderTitle?: string) => { + const inputBox = vscode.window.createInputBox(); + inputBox.title = + "Specify the title of the tour, or save it to a specific location"; + inputBox.placeholder = placeHolderTitle; + inputBox.buttons = [ + { + iconPath: new vscode.ThemeIcon("save-as"), + tooltip: "Save tour as..." + } + ]; - endCurrentCodeTour(); - store.isRecording = false; - vscode.commands.executeCommand( - "setContext", - "codetour:recording", - false - ); - } + inputBox.onDidAccept(async () => { + inputBox.hide(); + + if (!inputBox.value) { + return; + } + + recordTourInternal(inputBox.value, workspaceRoot); + }); + + inputBox.onDidTriggerButton(async button => { + inputBox.hide(); + + const uri = await vscode.window.showSaveDialog({ + filters: { + Tours: ["tour"] + }, + saveLabel: "Save Tour" + }); + + if (!uri) { + return; + } + + const disposeEndTourHandler = onDidEndTour(async tour => { + if (tour.id === decodeURIComponent(uri.toString())) { + disposeEndTourHandler.dispose(); + + if ( + await vscode.window.showInformationMessage( + "Would you like to export this tour?", + "Export Tour" + ) + ) { + const content = await exportTour(tour); + vscode.workspace.fs.writeFile(uri, Buffer.from(content)); + } + } + }); + + recordTourInternal(uri, workspaceRoot); + }); + + inputBox.show(); } ); @@ -225,6 +280,25 @@ export function registerRecorderCommands() { }) ); + vscode.commands.registerTextEditorCommand( + `${EXTENSION_NAME}.addSelectionStep`, + action(async (editor: vscode.TextEditor) => { + const stepNumber = ++store.activeTour!.step; + const tour = store.activeTour!.tour; + + const workspaceRoot = getActiveWorkspacePath(); + const file = getRelativePath(workspaceRoot, editor.document.uri.path); + + tour.steps.splice(stepNumber, 0, { + file, + selection: getStepSelection(), + description: "" + }); + + saveTour(tour); + }) + ); + vscode.commands.registerCommand( `${EXTENSION_NAME}.addTourStep`, action((reply: vscode.CommentReply) => { @@ -481,6 +555,25 @@ export function registerRecorderCommands() { } ); + vscode.commands.registerCommand( + `${EXTENSION_NAME}.changeTourStepLine`, + async (comment: CodeTourComment) => { + const step = store.activeTour!.tour.steps[store.activeTour!.step]; + const response = await vscode.window.showInputBox({ + prompt: `Enter the new line # for this tour step (Leave blank to use the selection/document end)`, + value: step.line?.toString() || "" + }); + + if (response) { + step.line = Number(response); + } else { + delete step.line; + } + + saveTour(store.activeTour!.tour); + } + ); + vscode.commands.registerCommand( `${EXTENSION_NAME}.changeTourRef`, async (node: CodeTourNode) => { diff --git a/src/store/actions.ts b/src/store/actions.ts index 00df059..30fe0a9 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -1,4 +1,4 @@ -import { commands, Memento, Uri, window } from "vscode"; +import { commands, EventEmitter, Memento, Uri, window } from "vscode"; import { CodeTour, store } from "."; import { EXTENSION_NAME, FS_SCHEME, FS_SCHEME_CONTENT } from "../constants"; import { startPlayer, stopPlayer } from "../player"; @@ -13,12 +13,16 @@ const CAN_EDIT_TOUR_KEY = `${EXTENSION_NAME}:canEditTour`; const IN_TOUR_KEY = `${EXTENSION_NAME}:inTour`; const RECORDING_KEY = `${EXTENSION_NAME}:recording`; +const _onDidEndTour = new EventEmitter(); +export const onDidEndTour = _onDidEndTour.event; + export function startCodeTour( tour: CodeTour, stepNumber?: number, workspaceRoot?: Uri, startInEditMode: boolean = false, - canEditTour: boolean = true + canEditTour: boolean = true, + tours?: CodeTour[] ) { startPlayer(); @@ -30,7 +34,8 @@ export function startCodeTour( tour, step: stepNumber ? stepNumber : tour.steps.length ? 0 : -1, workspaceRoot, - thread: null + thread: null, + tours }; commands.executeCommand("setContext", IN_TOUR_KEY, true); @@ -42,7 +47,36 @@ export function startCodeTour( } } +export async function selectTour( + tours: CodeTour[], + workspaceRoot?: Uri +): Promise { + const items: any[] = tours.map(tour => ({ + label: tour.title!, + tour: tour, + detail: tour.description + })); + + if (items.length === 1) { + startCodeTour(items[0].tour, 0, workspaceRoot, false, true, tours); + return true; + } + + const response = await window.showQuickPick(items, { + placeHolder: "Select the tour to start..." + }); + + if (response) { + startCodeTour(response.tour, 0, workspaceRoot, false, true, tours); + return true; + } + + return false; +} + export async function endCurrentCodeTour() { + _onDidEndTour.fire(store.activeTour!.tour); + if (store.isRecording) { store.isRecording = false; commands.executeCommand("setContext", RECORDING_KEY, false); @@ -71,10 +105,13 @@ export function moveCurrentCodeTourForward() { store.activeTour!.step++; } -export async function promptForTour(globalState: Memento) { - const workspaceKey = getWorkspaceKey(); - const key = `${EXTENSION_NAME}:${workspaceKey}`; - if (store.hasTours && !globalState.get(key)) { +export async function promptForTour( + globalState: Memento, + workspaceRoot: Uri = getWorkspaceKey(), + tours: CodeTour[] = store.tours +): Promise { + const key = `${EXTENSION_NAME}:${workspaceRoot}`; + if (tours.length > 0 && !globalState.get(key)) { globalState.update(key, true); if ( @@ -83,15 +120,18 @@ export async function promptForTour(globalState: Memento) { "Start CodeTour" ) ) { - const primaryTour = store.tours.find(tour => tour.isPrimary); + const primaryTour = tours.find(tour => tour.isPrimary); if (primaryTour) { - startCodeTour(primaryTour); + startCodeTour(primaryTour, 0, workspaceRoot, false, undefined, tours); + return true; } else { - commands.executeCommand(`${EXTENSION_NAME}.startTour`); + return selectTour(tours, workspaceRoot); } } } + + return false; } export async function exportTour(tour: CodeTour) { @@ -121,3 +161,7 @@ export async function exportTour(tour: CodeTour) { return JSON.stringify(newTour, null, 2); } + +export async function recordTour(workspaceRoot: Uri) { + commands.executeCommand(`${EXTENSION_NAME}.recordTour`, workspaceRoot); +} diff --git a/src/store/index.ts b/src/store/index.ts index 03674b3..bddd9d5 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -49,6 +49,14 @@ export interface ActiveTour { // In order to resolve relative file // paths, we need to know the workspace root workspaceRoot?: Uri; + + // In order to resolve inter-tour + // links, the active tour might need + // the context of its sibling tours, if + // they're coming from somewhere other + // then the active workspace (e.g. a + // GistPad-managed repo). + tours?: CodeTour[]; } export interface Store { diff --git a/src/tree/nodes.ts b/src/tree/nodes.ts index 50169a8..59c9f46 100644 --- a/src/tree/nodes.ts +++ b/src/tree/nodes.ts @@ -81,17 +81,16 @@ export class CodeTourStepNode extends TreeItem { const step = tour.steps[stepNumber]; - const workspaceRoot = - store.activeTour && - store.activeTour.tour.id === tour.id && - store.activeTour.workspaceRoot - ? store.activeTour.workspaceRoot - : undefined; + let workspaceRoot, tours; + if (store.activeTour && store.activeTour.tour.id === tour.id) { + workspaceRoot = store.activeTour.workspaceRoot; + tours = store.activeTour.tours; + } this.command = { command: `${EXTENSION_NAME}.startTour`, title: "Start Tour", - arguments: [tour, stepNumber, workspaceRoot] + arguments: [tour, stepNumber, workspaceRoot, tours] }; let resourceUri; diff --git a/src/utils.ts b/src/utils.ts index 18aac60..a42b4c6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -76,19 +76,17 @@ export function getActiveWorkspacePath() { } export function getWorkspaceKey() { - return ( - workspace.workspaceFile || workspace.workspaceFolders![0].uri.toString() - ); + return workspace.workspaceFile || workspace.workspaceFolders![0].uri; } export function getWorkspacePath(tour: CodeTour) { return getWorkspaceUri(tour)?.toString() || ""; } -export function getWorkspaceUri(tour: CodeTour) { +export function getWorkspaceUri(tour: CodeTour): Uri | undefined { const tourUri = Uri.parse(tour.id); return ( workspace.getWorkspaceFolder(tourUri)?.uri || - workspace.workspaceFolders![0].uri + (workspace.workspaceFolders && workspace.workspaceFolders[0].uri) ); }