From c55d9e7f9159493d3b6b81ba84c3cff029e584e6 Mon Sep 17 00:00:00 2001 From: Jonathan Carter Date: Sun, 24 May 2020 11:14:19 -0700 Subject: [PATCH] Adding support for VFS with query strings --- CHANGELOG.md | 4 ++++ package.json | 9 ++------ src/notebook/index.ts | 10 ++++----- src/player/commands.ts | 15 ++++++++----- src/player/decorator.ts | 4 ++-- src/player/index.ts | 6 ++--- src/store/actions.ts | 7 ++---- src/store/index.ts | 7 +++++- src/store/provider.ts | 50 ++++++++++++++++++++++++----------------- src/tree/nodes.ts | 8 +++---- src/utils.ts | 38 +++++++++++++++---------------- tsconfig.json | 2 +- 12 files changed, 87 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f726fb2..a10022e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ - Added the following commands to the command link completion list: `Run build task`, `Run task` and `Run test task`. - Fixed a bug where command links didn't work, if the command included multiple "components" to the name (e.g. `workbench.action.tasks.build`). +- Fixed a bug where tours weren't being discovered for virtual file systems that include a query string in their workspace path. +- Fixed a bug where tours that included content-only steps couldn't be exported. +- Fixed the open/export tour commands to correctly look for `*.tour` files. +- Fixed a bug where the `CodeTour: Record Tour` command was being displayed without having any workspaces open. ## v0.0.27 (05/22/2020) diff --git a/package.json b/package.json index b40785d..231929f 100644 --- a/package.json +++ b/package.json @@ -154,11 +154,6 @@ "category": "CodeTour", "icon": "$(add)" }, - { - "command": "codetour.refreshTours", - "title": "Refresh Tours", - "category": "CodeTour" - }, { "command": "codetour.resumeTour", "title": "Resume Tour", @@ -214,8 +209,8 @@ "when": "codetour:inTour && codetour:recording" }, { - "command": "codetour.refreshTours", - "when": "codetour:hasTours" + "command": "codetour.recordTour", + "when": "workspaceFolderCount != 0" }, { "command": "codetour.resumeTour", diff --git a/src/notebook/index.ts b/src/notebook/index.ts index cd13592..96e3c3b 100644 --- a/src/notebook/index.ts +++ b/src/notebook/index.ts @@ -1,7 +1,7 @@ import * as vscode from "vscode"; -import { CodeTour } from "../store"; -import { getStepFileUri, getWorkspacePath } from "../utils"; import { EXTENSION_NAME, SMALL_ICON_URL } from "../constants"; +import { CodeTour } from "../store"; +import { getStepFileUri, getWorkspaceUri } from "../utils"; class CodeTourNotebookProvider implements vscode.NotebookProvider { async resolveNotebook(editor: vscode.NotebookEditor): Promise { @@ -11,15 +11,15 @@ class CodeTourNotebookProvider implements vscode.NotebookProvider { cellEditable: false }; - let contents = ( + let contents = new TextDecoder().decode( await vscode.workspace.fs.readFile(editor.document.uri) - ).toString(); + ); let tour = JSON.parse(contents); tour.id = editor.document.uri.toString(); let steps: any[] = []; - const workspaceRoot = getWorkspacePath(tour); + const workspaceRoot = getWorkspaceUri(tour); for (let item of tour.steps) { const uri = await getStepFileUri(item, workspaceRoot, tour.ref); diff --git a/src/player/commands.ts b/src/player/commands.ts index 479416d..79339d1 100644 --- a/src/player/commands.ts +++ b/src/player/commands.ts @@ -134,7 +134,7 @@ export function registerPlayerCommands() { async () => { const uri = await vscode.window.showOpenDialog({ filters: { - Tours: ["json"] + Tours: ["tour"] }, canSelectFolders: false, canSelectMany: false, @@ -146,9 +146,13 @@ export function registerPlayerCommands() { } try { - const contents = await vscode.workspace.fs.readFile(uri[0]); - const tour = JSON.parse(contents.toString()); + const contents = new TextDecoder().decode( + await vscode.workspace.fs.readFile(uri[0]) + ); + + const tour = JSON.parse(contents); tour.id = uri[0].toString(); + startCodeTour(tour); } catch { vscode.window.showErrorMessage( @@ -186,7 +190,7 @@ export function registerPlayerCommands() { async (node: CodeTourNode) => { const uri = await vscode.window.showSaveDialog({ filters: { - Tours: ["json"] + Tours: ["tour"] }, saveLabel: "Export Tour" }); @@ -196,7 +200,8 @@ export function registerPlayerCommands() { } const contents = await exportTour(node.tour); - vscode.workspace.fs.writeFile(uri, new Buffer(contents)); + const bytes = new TextEncoder().encode(contents); + vscode.workspace.fs.writeFile(uri, bytes); } ); diff --git a/src/player/decorator.ts b/src/player/decorator.ts index f609f2a..b944540 100644 --- a/src/player/decorator.ts +++ b/src/player/decorator.ts @@ -2,7 +2,7 @@ import { reaction } from "mobx"; import * as vscode from "vscode"; import { FS_SCHEME_CONTENT, ICON_URL } from "../constants"; import { CodeTour, CodeTourStep, store } from "../store"; -import { getStepFileUri, getWorkspacePath } from "../utils"; +import { getStepFileUri, getWorkspaceUri } from "../utils"; const DISABLED_SCHEMES = [FS_SCHEME_CONTENT, "comment"]; @@ -27,7 +27,7 @@ async function getTourSteps( const tourSteps = await Promise.all( steps.map(async ([tour, step, stepNumber]) => { - const workspaceRoot = getWorkspacePath(tour); + const workspaceRoot = getWorkspaceUri(tour); const uri = await getStepFileUri(step, workspaceRoot); if ( diff --git a/src/player/index.ts b/src/player/index.ts index 935126e..44c8f1f 100644 --- a/src/player/index.ts +++ b/src/player/index.ts @@ -18,7 +18,7 @@ import { } from "vscode"; import { SMALL_ICON_URL } from "../constants"; import { store } from "../store"; -import { getActiveWorkspacePath, getFileUri, getStepFileUri } from "../utils"; +import { getFileUri, getStepFileUri } from "../utils"; const CONTROLLER_ID = "codetour"; const CONTROLLER_LABEL = "CodeTour"; @@ -131,7 +131,7 @@ async function renderCurrentStep() { label += ` (${currentTour.title})`; } - const workspaceRoot = getActiveWorkspacePath(); + const workspaceRoot = store.activeTour?.workspaceRoot; const uri = await getStepFileUri(step, workspaceRoot, currentTour.ref); store.activeTour!.thread = controller!.createCommentThread(uri, range, []); @@ -176,7 +176,7 @@ async function renderCurrentStep() { await showDocument(uri, range, selection); if (step.directory) { - const directoryUri = getFileUri(workspaceRoot, step.directory); + const directoryUri = getFileUri(step.directory, workspaceRoot); commands.executeCommand("revealInExplorer", directoryUri); } } diff --git a/src/store/actions.ts b/src/store/actions.ts index 02e16a4..e77e437 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -96,14 +96,11 @@ export async function exportTour(tour: CodeTour) { newTour.steps = await Promise.all( newTour.steps.map(async step => { - if (step.contents && step.uri) { + if (step.contents || step.uri || !step.file) { return step; } - const workspaceRoot = workspace.workspaceFolders - ? workspace.workspaceFolders[0].uri.toString() - : ""; - + const workspaceRoot = getWorkspaceUri(tour); const stepFileUri = await getStepFileUri(step, workspaceRoot, tour.ref); const stepFileContents = await workspace.fs.readFile(stepFileUri); diff --git a/src/store/index.ts b/src/store/index.ts index 6f25068..a543b0a 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -9,12 +9,17 @@ export interface CodeTourStepPosition { export interface CodeTourStep { title?: string; description: string; + + // If any of the following are set, then only + // one of them can be, since these properties + // indicate the "type" of step. file?: string; directory?: string; + contents?: string; uri?: string; + line?: number; selection?: { start: CodeTourStepPosition; end: CodeTourStepPosition }; - contents?: string; } export interface CodeTour { diff --git a/src/store/provider.ts b/src/store/provider.ts index dff1cdf..ffb5fd1 100644 --- a/src/store/provider.ts +++ b/src/store/provider.ts @@ -5,7 +5,6 @@ import { EXTENSION_NAME, VSCODE_DIRECTORY } from "../constants"; import { endCurrentCodeTour } from "./actions"; const MAIN_TOUR_FILES = [".tour", `${VSCODE_DIRECTORY}/main.tour`]; - const SUB_TOUR_DIRECTORIES = [`${VSCODE_DIRECTORY}/tours`, `.tours`]; const HAS_TOURS_KEY = `${EXTENSION_NAME}:hasTours`; @@ -13,9 +12,8 @@ const HAS_TOURS_KEY = `${EXTENSION_NAME}:hasTours`; export async function discoverTours(): Promise { const tours = await Promise.all( vscode.workspace.workspaceFolders!.map(async workspaceFolder => { - const workspaceRoot = workspaceFolder.uri.toString(); - const mainTours = await discoverMainTours(workspaceRoot); - const tours = await discoverSubTours(workspaceRoot); + const mainTours = await discoverMainTours(workspaceFolder.uri); + const tours = await discoverSubTours(workspaceFolder.uri); if (mainTours) { tours.push(...mainTours); @@ -51,14 +49,19 @@ export async function discoverTours(): Promise { vscode.commands.executeCommand("setContext", HAS_TOURS_KEY, store.hasTours); } -async function discoverMainTours(workspaceRoot: string): Promise { +async function discoverMainTours( + workspaceUri: vscode.Uri +): Promise { const tours = await Promise.all( MAIN_TOUR_FILES.map(async tourFile => { try { - const uri = vscode.Uri.parse(`${workspaceRoot}/${tourFile}`); - const mainTourContent = ( + const uri = workspaceUri.with({ + path: `${workspaceUri.path}/${tourFile}` + }); + + const mainTourContent = new TextDecoder().decode( await vscode.workspace.fs.readFile(uri) - ).toString(); + ); const tour = JSON.parse(mainTourContent); tour.id = uri.toString(); return tour; @@ -69,20 +72,23 @@ async function discoverMainTours(workspaceRoot: string): Promise { return tours.filter(tour => tour); } -async function readTourDirectory(tourDirectory: string): Promise { +async function readTourDirectory(uri: vscode.Uri): Promise { try { - const uri = vscode.Uri.parse(tourDirectory); const tourFiles = await vscode.workspace.fs.readDirectory(uri); const tours = await Promise.all( tourFiles.map(async ([file, type]) => { + const fileUri = uri.with({ + path: `${uri.path}/${file}` + }); if (type === vscode.FileType.File) { - return readTourFile(tourDirectory, file); + return readTourFile(fileUri); } else { - return readTourDirectory(`${tourDirectory}/${file}`); + return readTourDirectory(fileUri); } }) ); + // @ts-ignore return tours.flat().filter(tour => tour); } catch { return []; @@ -90,25 +96,27 @@ async function readTourDirectory(tourDirectory: string): Promise { } async function readTourFile( - directory: string, - file: string + tourUri: vscode.Uri ): Promise { try { - const tourUri = vscode.Uri.parse(`${directory}/${file}`); - const tourContent = ( + const tourContent = new TextDecoder().decode( 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 { +async function discoverSubTours(workspaceUri: vscode.Uri): Promise { const tours = await Promise.all( - SUB_TOUR_DIRECTORIES.map(directory => - readTourDirectory(`${workspaceRoot}/${directory}`) - ) + SUB_TOUR_DIRECTORIES.map(directory => { + const uri = workspaceUri.with({ + path: `${workspaceUri.path}/${directory}` + }); + + return readTourDirectory(uri); + }) ); return tours.flat(); diff --git a/src/tree/nodes.ts b/src/tree/nodes.ts index 0afdc4f..dd5e5e7 100644 --- a/src/tree/nodes.ts +++ b/src/tree/nodes.ts @@ -2,7 +2,7 @@ import * as path from "path"; import { ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from "vscode"; import { CONTENT_URI, EXTENSION_NAME, FS_SCHEME } from "../constants"; import { CodeTour, store } from "../store"; -import { getFileUri, getWorkspacePath } from "../utils"; +import { getFileUri, getWorkspaceUri } from "../utils"; function isRecording(tour: CodeTour) { return ( @@ -101,10 +101,10 @@ export class CodeTourStepNode extends TreeItem { resourceUri = Uri.parse(`${FS_SCHEME}://current/${step.file}`); } else if (step.file || step.directory) { const resourceRoot = workspaceRoot - ? workspaceRoot.toString() - : getWorkspacePath(tour); + ? workspaceRoot + : getWorkspaceUri(tour); - resourceUri = getFileUri(resourceRoot, step.directory || step.file!); + resourceUri = getFileUri(step.directory || step.file!, resourceRoot); } else { resourceUri = CONTENT_URI; } diff --git a/src/utils.ts b/src/utils.ts index a173e76..a290e27 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,28 +4,27 @@ import { CONTENT_URI, FS_SCHEME } from "./constants"; import { api } from "./git"; import { CodeTour, CodeTourStep, store } from "./store"; -export function getFileUri(workspaceRoot: string, file: string) { - const uriPrefix = workspaceRoot.endsWith("/") - ? workspaceRoot - : `${workspaceRoot}/`; - - let uri = Uri.parse(`${uriPrefix}${file}`); - - if (file.startsWith("..")) { - // path.join/normalize will resolve relative paths (e.g. replacing - // ".." with the actual directories), but it also messes up - // non-file based schemes. So we parse the workspace root and - // then replace it's path with a "joined" version of _only_ the path. - uri = uri.with({ - path: path.normalize(uri.path) - }); +export function getFileUri(file: string, workspaceRoot?: Uri) { + if (!workspaceRoot) { + return Uri.parse(file); } - return uri; + + const rootPath = workspaceRoot.path.endsWith("/") + ? workspaceRoot.path + : `${workspaceRoot.path}/`; + + // path.join/normalize will resolve relative paths (e.g. replacing + // ".." with the actual directories), but it also messes up + // non-file based schemes. So we parse the workspace root and + // then replace it's path with a normalized version of _only_ the path. + return workspaceRoot.with({ + path: path.normalize(`${rootPath}${file}`) + }); } export async function getStepFileUri( step: CodeTourStep, - workspaceRoot: string, + workspaceRoot?: Uri, ref?: string ): Promise { let uri; @@ -34,7 +33,7 @@ export async function getStepFileUri( } else if (step.uri || step.file) { uri = step.uri ? Uri.parse(step.uri) - : getFileUri(workspaceRoot, step.file!); + : getFileUri(step.file!, workspaceRoot); if (api && ref && ref !== "HEAD") { const repo = api.getRepository(uri); @@ -72,5 +71,6 @@ export function getWorkspacePath(tour: CodeTour) { } export function getWorkspaceUri(tour: CodeTour) { - return workspace.getWorkspaceFolder(Uri.parse(tour.id))?.uri; + const tourUri = Uri.parse(tour.id); + return workspace.getWorkspaceFolder(tourUri)?.uri; } diff --git a/tsconfig.json b/tsconfig.json index 8e3f9d1..e451c7a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "module": "commonjs", "target": "es6", "outDir": "out", - "lib": ["ES2019"], + "lib": ["ES2019", "DOM"], "sourceMap": true, "rootDir": "src", "strict": true,