Adding support for exporting/opening tours

This commit is contained in:
Jonathan Carter 2020-03-26 15:30:15 -07:00
Родитель 2266006e08
Коммит 576979279c
11 изменённых файлов: 295 добавлений и 29 удалений

4
.vscode/tours/statusbar.json поставляемый
Просмотреть файл

@ -9,11 +9,11 @@
"selection": {
"start": {
"line": 32,
"character": 5
"character": 3
},
"end": {
"line": 36,
"character": 49
"character": 40
}
}
},

Просмотреть файл

@ -1,4 +1,10 @@
## v0.0.13 (03/23/202)
## v0.0.14 (03/?/2020)
- Added the `Export Tour` command to the `CodeTour` tree, which allows exporting a recorded tour that embeds the file contents needed to play it back
- Added the ability to open a code tour file, either via the `CodeTour: Open Tour File...` command or by clicking the `Open Tour File...` button in the title bar of the `CodeTour` view
- Added support for tour steps to omit a line number, which results in the step description being displayed at the bottom of the associated file
## v0.0.13 (03/23/2020)
- Exposed an experimental API for other extensions to record/playback tours. For an example, see the [GistPad](https://aka.ms/gistpad) extension, which now allows you to create tours associated with interactive web playgrounds

Просмотреть файл

@ -23,6 +23,10 @@ Otherwise, you can manually start a tour via any of the following methods:
If the current workspace only has a single code tour, then any of the above actions will automatically start that tour. Otherwise, you'll be presented with a list of tours to select from.
### Opening a tour
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.
## Navigating a tour
Once you've started a tour, the comment UI will guide you, and includes navigation actions that allow you to perform the following:
@ -127,6 +131,12 @@ Behind the scenes, the tour will be written as a JSON file to the `.vscode/tours
- `.vscode/codetour.json`
- `.vscode/tour.json`
### Exporting Tours
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.
### Tour Schema
Within the tour file, you need to specify the following required properties:

Просмотреть файл

@ -79,6 +79,10 @@
"category": "CodeTour",
"icon": "$(debug-stop)"
},
{
"command": "codetour.exportTour",
"title": "Export Tour..."
},
{
"command": "codetour.moveTourStepBack",
"title": "Move Up",
@ -94,6 +98,12 @@
"title": "Next",
"icon": "$(arrow-right)"
},
{
"command": "codetour.openTourFile",
"title": "Open Tour File...",
"icon": "$(folder-opened)",
"category": "CodeTour"
},
{
"command": "codetour.previousTourStep",
"title": "Previous",
@ -176,6 +186,10 @@
"command": "codetour.editTourStep",
"when": "false"
},
{
"command": "codetour.exportTour",
"when": "false"
},
{
"command": "codetour.nextTourStep",
"when": "false"
@ -248,10 +262,15 @@
}
],
"view/title": [
{
"command": "codetour.openTourFile",
"when": "view == codetour.tours",
"group": "navigation@1"
},
{
"command": "codetour.recordTour",
"when": "view == codetour.tours",
"group": "navigation"
"group": "navigation@2"
}
],
"view/item/context": [
@ -305,6 +324,11 @@
"when": "viewItem =~ /^codetour.tour(.recording)?(.active)?$/",
"group": "edit@2"
},
{
"command": "codetour.exportTour",
"when": "viewItem =~ /^codetour.tour(.active)?$/",
"group": "export@1"
},
{
"command": "codetour.moveTourStepBack",
"when": "viewItem =~ /^codetour.tourStep.hasPrevious/ && codetour:recording",

Просмотреть файл

@ -14,6 +14,8 @@ import { CodeTourNode, CodeTourStepNode } from "./tree/nodes";
import { runInAction, comparer } from "mobx";
import { api, RefType } from "./git";
import * as path from "path";
import { getStepFileUri } from "./utils";
import { workspace } from "vscode";
interface CodeTourQuickPickItem extends vscode.QuickPickItem {
tour: CodeTour;
}
@ -565,4 +567,85 @@ export function registerCommands() {
return response.ref;
}
}
vscode.commands.registerCommand(
`${EXTENSION_NAME}.openTourFile`,
async () => {
const uri = await vscode.window.showOpenDialog({
filters: {
Tours: ["json"]
},
canSelectFolders: false,
canSelectMany: false,
openLabel: "Open Tour"
});
if (!uri) {
return;
}
try {
const contents = await vscode.workspace.fs.readFile(uri[0]);
const tour = JSON.parse(contents.toString());
tour.id = uri[0].toString();
startCodeTour(tour);
} catch {
vscode.window.showErrorMessage(
"This file doesn't appear to be a valid tour. Please inspect its contents and try again."
);
}
}
);
vscode.commands.registerCommand(
`${EXTENSION_NAME}.exportTour`,
async (node: CodeTourNode) => {
const uri = await vscode.window.showSaveDialog({
filters: {
Tours: ["json"]
},
saveLabel: "Export Tour"
});
if (!uri) {
return;
}
const tour = node.tour;
const newTour = {
...tour
};
newTour.steps = await Promise.all(
newTour.steps.map(async step => {
if (step.contents && step.uri) {
return step;
}
const workspaceRoot = workspace.workspaceFolders
? workspace.workspaceFolders[0].uri.toString()
: "";
const stepFileUri = await getStepFileUri(
step,
workspaceRoot,
node.tour.ref
);
const stepFileContents = await vscode.workspace.fs.readFile(
stepFileUri
);
step.contents = stepFileContents.toString();
return step;
})
);
delete newTour.id;
delete newTour.ref;
const contents = JSON.stringify(newTour, null, 2);
vscode.workspace.fs.writeFile(uri, new Buffer(contents));
}
);
}

Просмотреть файл

@ -1,2 +1,3 @@
export const EXTENSION_NAME = "codetour";
export const VSCODE_DIRECTORY = ".vscode";
export const FS_SCHEME = EXTENSION_NAME;

Просмотреть файл

@ -7,6 +7,7 @@ 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,
@ -28,19 +29,21 @@ async function promptForTour(
}
export async function activate(context: vscode.ExtensionContext) {
registerCommands();
if (vscode.workspace.workspaceFolders) {
const workspaceRoot = vscode.workspace.workspaceFolders[0].uri.toString();
await discoverTours(workspaceRoot);
registerCommands();
registerTreeProvider(context.extensionPath);
registerStatusBar();
promptForTour(workspaceRoot, context.globalState);
initializeGitApi();
}
registerFileSystemProvider();
registerStatusBar();
return {
startTour: startCodeTour,
endCurrentTour: endCurrentCodeTour

115
src/fileSystem/index.ts Normal file
Просмотреть файл

@ -0,0 +1,115 @@
import * as path from "path";
import {
Disposable,
Event,
EventEmitter,
FileChangeEvent,
FileStat,
FileSystemError,
FileSystemProvider,
FileType,
Uri,
workspace
} from "vscode";
import { store, CodeTour, CodeTourStep } from "../store";
import { FS_SCHEME } from "../constants";
export class CodeTourFileSystemProvider implements FileSystemProvider {
private count = 0;
private _onDidChangeFile = new EventEmitter<FileChangeEvent[]>();
public readonly onDidChangeFile: Event<FileChangeEvent[]> = this
._onDidChangeFile.event;
getCurrentTourStep(): [CodeTour, CodeTourStep] {
const tour = store.activeTour?.tour!;
return [tour, tour?.steps[store.activeTour!.step]!];
}
updateTour(tour: CodeTour) {
const tourUri = Uri.parse(tour.id);
const newTour = {
...tour
};
delete newTour.id;
const contents = JSON.stringify(newTour, null, 2);
workspace.fs.writeFile(tourUri, new Buffer(contents));
}
async readFile(uri: Uri): Promise<Uint8Array> {
const [, { contents }] = this.getCurrentTourStep();
return new Buffer(contents!);
}
async writeFile(
uri: Uri,
content: Uint8Array,
options: { create: boolean; overwrite: boolean }
): Promise<void> {
const [tour, step] = this.getCurrentTourStep();
step.contents = content.toString();
this.updateTour(tour);
}
async stat(uri: Uri): Promise<FileStat> {
return {
type: FileType.File,
ctime: 0,
mtime: ++this.count,
size: 100
};
}
async rename(
oldUri: Uri,
newUri: Uri,
options: { overwrite: boolean }
): Promise<void> {
const [tour, step] = this.getCurrentTourStep();
step.file = path.basename(newUri.toString());
this.updateTour(tour);
}
async copy?(
source: Uri,
destination: Uri,
options: { overwrite: boolean }
): Promise<void> {
throw FileSystemError.NoPermissions(
"CodeTour doesn't support copying files"
);
}
createDirectory(uri: Uri): void {
throw FileSystemError.NoPermissions(
"CodeTour doesn't support directories."
);
}
async delete(uri: Uri, options: { recursive: boolean }): Promise<void> {
throw FileSystemError.NoPermissions(
"CodeTour doesn't support deleting files"
);
}
async readDirectory(uri: Uri): Promise<[string, FileType][]> {
throw FileSystemError.NoPermissions("CodeTour doesnt support directories.");
}
watch(
uri: Uri,
options: { recursive: boolean; excludes: string[] }
): Disposable {
throw FileSystemError.NoPermissions(
"CodeTour doesn't support watching files."
);
}
}
export function registerFileSystemProvider() {
workspace.registerFileSystemProvider(
FS_SCHEME,
new CodeTourFileSystemProvider()
);
}

Просмотреть файл

@ -17,9 +17,9 @@ import {
Selection
} from "vscode";
import { CodeTour, store } from ".";
import { EXTENSION_NAME } from "../constants";
import { EXTENSION_NAME, FS_SCHEME } from "../constants";
import { reaction } from "mobx";
import { api } from "../git";
import { getStepFileUri } from "../utils";
const CAN_EDIT_TOUR_KEY = `${EXTENSION_NAME}:canEditTour`;
const IN_TOUR_KEY = `${EXTENSION_NAME}:inTour`;
@ -81,7 +81,7 @@ 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 - 1;
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}`;
@ -90,28 +90,12 @@ async function renderCurrentStep() {
}
const workspaceRoot = store.activeTour!.workspaceRoot
? store.activeTour!.workspaceRoot
? store.activeTour!.workspaceRoot.toString()
: workspace.workspaceFolders
? workspace.workspaceFolders[0].uri.toString()
: "";
let uri = step.uri
? Uri.parse(step.uri)
: Uri.parse(`${workspaceRoot}/${step.file}`);
if (currentTour.ref && currentTour.ref !== "HEAD") {
const repo = api.getRepository(uri);
if (
repo &&
repo.state.HEAD &&
repo.state.HEAD.name !== currentTour.ref &&
repo.state.HEAD.commit !== currentTour.ref
) {
uri = await api.toGitUri(uri, currentTour.ref);
}
}
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!)
@ -209,6 +193,12 @@ export async function endCurrentCodeTour() {
store.activeTour = null;
commands.executeCommand("setContext", IN_TOUR_KEY, false);
window.visibleTextEditors.forEach(editor => {
if (editor.document.uri.scheme === FS_SCHEME) {
editor.hide();
}
});
}
export function moveCurrentCodeTourBackward() {

Просмотреть файл

@ -13,8 +13,9 @@ export interface CodeTourStep {
description: string;
file?: string;
uri?: string;
line: number;
line?: number;
selection?: { start: CodeTourStepPosition; end: CodeTourStepPosition };
contents?: string;
}
export interface CodeTour {

33
src/utils.ts Normal file
Просмотреть файл

@ -0,0 +1,33 @@
import { CodeTourStep } from "./store";
import { Uri } from "vscode";
import { FS_SCHEME } from "./constants";
import { api } from "./git";
export async function getStepFileUri(
step: CodeTourStep,
workspaceRoot: string,
ref?: string
): Promise<Uri> {
let uri;
if (step.contents) {
uri = Uri.parse(`${FS_SCHEME}://current/${step.file}`);
} else {
uri = step.uri
? Uri.parse(step.uri)
: Uri.parse(`${workspaceRoot}/${step.file}`);
if (ref && ref !== "HEAD") {
const repo = api.getRepository(uri);
if (
repo &&
repo.state.HEAD &&
repo.state.HEAD.name !== ref &&
repo.state.HEAD.commit !== ref
) {
uri = await api.toGitUri(uri, ref);
}
}
}
return uri;
}