Adding shell commands
This commit is contained in:
Родитель
5a7ba6b15b
Коммит
142d706bb2
|
@ -1,4 +1,11 @@
|
|||
## v0.0.19 (04/?/2020)
|
||||
## v0.0.20 (04/08/2020)
|
||||
|
||||
- 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.
|
||||
- Added the `exportTour` to the API that is exposed by this extension
|
||||
|
||||
## v0.0.19 (04/06/2020)
|
||||
|
||||
- Added support for recording and playing tours within a multi-root workspace
|
||||
- Added support for recording steps that reference files outside of the currently opened workspace. _Note: This should only be done if the file is outside of the workspace, but still within the same git repo. Otherwise, the tour wouldn't be "stable" for people who clone the repo and try to replay it._
|
||||
|
|
12
README.md
12
README.md
|
@ -125,6 +125,12 @@ If you want to edit an existing tour, simply right-click the tour in the `CodeTo
|
|||
|
||||
At any time, you can right-click a tour in the `CodeTour` tree and change it's title, description or git ref, by selecting the `Change Title`, `Change Description` or `Change Git Ref` menu items. Additionally, you can delete a tour by right-clicking it in the `CodeTour` tree and seelcting `Delete Tour`.
|
||||
|
||||
### Shell Commands
|
||||
|
||||
In order to add more interactivity to a tour, you can embed shell commands into a step (e.g. to perform a build, run tests, start an app), using the special `>>` synax, followed by the shell command you want to run (e.g. `>> npm run compile`). This will be converted into a hyperlink, that when clicked, will launch a new integrated terminal (called `CodeTour`) and will run the specified command.
|
||||
|
||||
<img width="600px" src="https://user-images.githubusercontent.com/116461/78858896-91912600-79e2-11ea-8002-196c12273ebc.gif" />
|
||||
|
||||
### Versioning tours
|
||||
|
||||
When you record a tour, you'll be asked which git "ref" to associate it with. This allows you to define how resilient you want the tour to be, as changes are made to the respective codebase.
|
||||
|
@ -142,13 +148,15 @@ At any time, you can edit the tour's ref by right-clicking it in the `CodeTour`
|
|||
|
||||
### Tour Files
|
||||
|
||||
Behind the scenes, the tour will be written as a JSON file to the `.vscode/tours` directory of the current workspace. This file is pretty simple and can be hand-edited if you'd like. Additionally, you can manually create tour files, by following the [tour schema](#tour-schema). You can then store these files to the `.vscode/tours` directory, or you can also create a tour at any of the following locations:
|
||||
Behind the scenes, the tour will be written as a JSON file to the `.vscode/tours` directory of the current workspace. This file is pretty simple and can be hand-edited if you'd like. Additionally, you can manually create tour files, by following the [tour schema](#tour-schema). You can then store these files to the `.vscode/tours` (or `.tours`) directory, or you can also create a tour at any of the following well-known locations:
|
||||
|
||||
- `codetour.json`
|
||||
- `tour.json`
|
||||
- `.vscode/codetour.json`
|
||||
- `.vscode/tour.json`
|
||||
|
||||
Within the `.vscode/tours` or `.tours` directory, you can organize your tour files into arbitrarily deep sub-directories, and the CodeTour player will properly discover them.
|
||||
|
||||
### 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.
|
||||
|
@ -233,3 +241,5 @@ In order to enable other extensions to contribute/manage their own code tours, t
|
|||
- `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<string>` - Exports a `CodeTour` instance into a fully-embedded tour file, that can then be written to some persistent storage (e.g. a GitHub Gist).
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"displayName": "CodeTour",
|
||||
"description": "VS Code extension that allows you to record and playback guided tours of codebases, directly within the editor",
|
||||
"publisher": "vsls-contrib",
|
||||
"version": "0.0.19",
|
||||
"version": "0.0.20",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vsls-contrib/codetour"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { comparer, runInAction } from "mobx";
|
||||
import { comparer, runInAction, when } from "mobx";
|
||||
import * as path from "path";
|
||||
import * as vscode from "vscode";
|
||||
import { workspace } from "vscode";
|
||||
|
@ -10,15 +10,17 @@ import {
|
|||
endCurrentCodeTour,
|
||||
moveCurrentCodeTourBackward,
|
||||
moveCurrentCodeTourForward,
|
||||
startCodeTour
|
||||
startCodeTour,
|
||||
exportTour
|
||||
} from "./store/actions";
|
||||
import { discoverTours } from "./store/provider";
|
||||
import { CodeTourNode, CodeTourStepNode } from "./tree/nodes";
|
||||
import { getActiveWorkspacePath, getStepFileUri } from "./utils";
|
||||
import { getActiveWorkspacePath } from "./utils";
|
||||
interface CodeTourQuickPickItem extends vscode.QuickPickItem {
|
||||
tour: CodeTour;
|
||||
}
|
||||
|
||||
let terminal: vscode.Terminal | null;
|
||||
export function registerCommands() {
|
||||
// This is a "private" command that's used exclusively
|
||||
// by the hover description for tour markers.
|
||||
|
@ -32,6 +34,30 @@ export function registerCommands() {
|
|||
}
|
||||
);
|
||||
|
||||
// This is a "private" command that powers the
|
||||
// ">>" shell command syntax in step comments.
|
||||
vscode.commands.registerCommand(
|
||||
`${EXTENSION_NAME}._sendTextToTerminal`,
|
||||
async (text: string) => {
|
||||
if (!terminal) {
|
||||
terminal = vscode.window.createTerminal("CodeTour");
|
||||
vscode.window.onDidCloseTerminal(term => {
|
||||
if (term.name === "CodeTour") {
|
||||
terminal = null;
|
||||
}
|
||||
});
|
||||
|
||||
when(
|
||||
() => store.activeTour === null,
|
||||
() => terminal?.dispose()
|
||||
);
|
||||
}
|
||||
|
||||
terminal.show();
|
||||
terminal.sendText(text, true);
|
||||
}
|
||||
);
|
||||
|
||||
vscode.commands.registerCommand(
|
||||
`${EXTENSION_NAME}.startTour`,
|
||||
async (
|
||||
|
@ -345,6 +371,7 @@ export function registerCommands() {
|
|||
`${EXTENSION_NAME}.editTourStep`,
|
||||
async (comment: CodeTourComment) => {
|
||||
comment.parent.comments = comment.parent.comments.map(comment => {
|
||||
(comment as CodeTourComment).decodeBody();
|
||||
comment.mode = vscode.CommentMode.Editing;
|
||||
return comment;
|
||||
});
|
||||
|
@ -666,40 +693,7 @@ export function registerCommands() {
|
|||
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);
|
||||
const contents = await exportTour(node.tour);
|
||||
vscode.workspace.fs.writeFile(uri, new Buffer(contents));
|
||||
}
|
||||
);
|
||||
|
|
|
@ -6,7 +6,8 @@ import { registerStatusBar } from "./status";
|
|||
import {
|
||||
endCurrentCodeTour,
|
||||
promptForTour,
|
||||
startCodeTour
|
||||
startCodeTour,
|
||||
exportTour
|
||||
} from "./store/actions";
|
||||
import { discoverTours } from "./store/provider";
|
||||
import { registerTreeProvider } from "./tree";
|
||||
|
@ -48,6 +49,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||
|
||||
return {
|
||||
startTour: startCodeTour,
|
||||
exportTour,
|
||||
endCurrentTour: endCurrentCodeTour
|
||||
};
|
||||
}
|
||||
|
|
|
@ -23,6 +23,10 @@ const CONTROLLER_ID = "codetour";
|
|||
const CONTROLLER_LABEL = "CodeTour";
|
||||
|
||||
let id = 0;
|
||||
|
||||
const SHELL_SCRIPT_PATTERN = /^>>\s+(.*)$/gm;
|
||||
const REVERSE_SHELL_SCRIPT_PATTERN = /^> \[([^\]]+)\]\(command:codetour\._sendTextToTerminal\?.*$/gm;
|
||||
|
||||
export class CodeTourComment implements Comment {
|
||||
public id: string = (++id).toString();
|
||||
public contextValue: string = "";
|
||||
|
@ -31,12 +35,29 @@ export class CodeTourComment implements Comment {
|
|||
name: CONTROLLER_LABEL,
|
||||
iconPath: Uri.parse(ICON_URL)
|
||||
};
|
||||
public body: MarkdownString;
|
||||
|
||||
constructor(
|
||||
public body: string | MarkdownString,
|
||||
body: string,
|
||||
public label: string = "",
|
||||
public parent: CommentThread
|
||||
) {}
|
||||
) {
|
||||
body = body.replace(SHELL_SCRIPT_PATTERN, (_, script) => {
|
||||
const args = encodeURIComponent(JSON.stringify([script]));
|
||||
return `> [${script}](command:codetour._sendTextToTerminal?${args} "Run \\"${script}\\" in a terminal")`;
|
||||
});
|
||||
|
||||
this.body = new MarkdownString(body);
|
||||
this.body.isTrusted = true;
|
||||
}
|
||||
|
||||
decodeBody() {
|
||||
const body = (this.body as MarkdownString).value.replace(
|
||||
REVERSE_SHELL_SCRIPT_PATTERN,
|
||||
(_, script) => `>> ${script}`
|
||||
);
|
||||
this.body = new MarkdownString(body);
|
||||
}
|
||||
}
|
||||
|
||||
let controller: CommentController | null;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { commands, Memento, Uri, window } from "vscode";
|
||||
import { commands, Memento, Uri, window, workspace } from "vscode";
|
||||
import { CodeTour, store } from ".";
|
||||
import { EXTENSION_NAME, FS_SCHEME } from "../constants";
|
||||
import { startPlayer, stopPlayer } from "../player";
|
||||
import { getWorkspaceKey, getWorkspaceUri } from "../utils";
|
||||
import { getWorkspaceKey, getWorkspaceUri, getStepFileUri } from "../utils";
|
||||
|
||||
const CAN_EDIT_TOUR_KEY = `${EXTENSION_NAME}:canEditTour`;
|
||||
const IN_TOUR_KEY = `${EXTENSION_NAME}:inTour`;
|
||||
|
@ -78,3 +78,35 @@ export async function promptForTour(globalState: Memento) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function exportTour(tour: CodeTour) {
|
||||
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, tour.ref);
|
||||
|
||||
const stepFileContents = await workspace.fs.readFile(stepFileUri);
|
||||
|
||||
return {
|
||||
...step,
|
||||
contents: stepFileContents.toString()
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
delete newTour.id;
|
||||
delete newTour.ref;
|
||||
|
||||
return JSON.stringify(newTour, null, 2);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,8 @@ const MAIN_TOUR_FILES = [
|
|||
`${VSCODE_DIRECTORY}/tour.json`
|
||||
];
|
||||
|
||||
const SUB_TOUR_DIRECTORY = `${VSCODE_DIRECTORY}/tours`;
|
||||
const SUB_TOUR_DIRECTORIES = [`${VSCODE_DIRECTORY}/tours`, `.tours`];
|
||||
|
||||
const HAS_TOURS_KEY = `${EXTENSION_NAME}:hasTours`;
|
||||
|
||||
export async function discoverTours(): Promise<void> {
|
||||
|
@ -73,33 +74,55 @@ async function discoverMainTours(workspaceRoot: string): Promise<CodeTour[]> {
|
|||
return tours.filter(tour => tour);
|
||||
}
|
||||
|
||||
async function discoverSubTours(workspaceRoot: string): Promise<CodeTour[]> {
|
||||
async function readTourDirectory(tourDirectory: string): Promise<CodeTour[]> {
|
||||
try {
|
||||
const tourDirectory = `${workspaceRoot}/${SUB_TOUR_DIRECTORY}`;
|
||||
const uri = vscode.Uri.parse(tourDirectory);
|
||||
const tourFiles = await vscode.workspace.fs.readDirectory(uri);
|
||||
return Promise.all(
|
||||
tourFiles
|
||||
.filter(([, type]) => type === vscode.FileType.File)
|
||||
.map(async ([file]) => {
|
||||
const tourUri = vscode.Uri.parse(`${tourDirectory}/${file}`);
|
||||
const tourContent = (
|
||||
await vscode.workspace.fs.readFile(tourUri)
|
||||
).toString();
|
||||
const tour = JSON.parse(tourContent);
|
||||
tour.id = tourUri.toString();
|
||||
return tour;
|
||||
})
|
||||
const tours = await Promise.all(
|
||||
tourFiles.map(async ([file, type]) => {
|
||||
if (type === vscode.FileType.File) {
|
||||
return readTourFile(tourDirectory, file);
|
||||
} else {
|
||||
return readTourDirectory(`${tourDirectory}/${file}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return tours.flat().filter(tour => tour);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function readTourFile(
|
||||
directory: string,
|
||||
file: string
|
||||
): Promise<CodeTour | undefined> {
|
||||
try {
|
||||
const tourUri = vscode.Uri.parse(`${directory}/${file}`);
|
||||
const tourContent = (
|
||||
await vscode.workspace.fs.readFile(tourUri)
|
||||
).toString();
|
||||
const tour = JSON.parse(tourContent);
|
||||
tour.id = tourUri.toString();
|
||||
return tour;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function discoverSubTours(workspaceRoot: string): Promise<CodeTour[]> {
|
||||
const tours = await Promise.all(
|
||||
SUB_TOUR_DIRECTORIES.map(directory =>
|
||||
readTourDirectory(`${workspaceRoot}/${directory}`)
|
||||
)
|
||||
);
|
||||
|
||||
return tours.flat();
|
||||
}
|
||||
|
||||
vscode.workspace.onDidChangeWorkspaceFolders(discoverTours);
|
||||
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(
|
||||
"**/.vscode/tours/*.json"
|
||||
"**/{.vscode/tours,.tours}/**/*.json"
|
||||
);
|
||||
|
||||
watcher.onDidChange(discoverTours);
|
||||
|
|
Загрузка…
Ссылка в новой задаче