Skip to content

Commit

Permalink
feat: Support opening shinylive apps locally from a vscode:// or `p…
Browse files Browse the repository at this point in the history
…ositron://` URL (#70)
  • Loading branch information
gadenbuie authored Aug 23, 2024
1 parent 062579e commit dc85119
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 8 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

- Improved feedback when waiting for a slow Shiny app to start up. ([#65](https://github.com/posit-dev/shiny-vscode/pull/65))

- The extension can now open Shinylive apps locally from `vscode://posit.shiny/shinylive?url=...` links. ([#70](https://github.com/posit-dev/shiny-vscode/pull/70))

## 1.0.0

The Shiny extension for VS Code now has a new extension ID: `Posit.shiny`! New Shiny users should install the Shiny extension from [the VS Code marketplace](https://marketplace.visualstudio.com/items?itemName=Posit.shiny) or [https://open-vsx.org/extension/posit/shiny](https://open-vsx.org/extension/posit/shiny).
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"onCommand:shiny.python.runApp",
"onCommand:shiny.python.debugApp",
"onLanguage:r",
"onCommand:shiny.r.runApp"
"onCommand:shiny.r.runApp",
"onUri"
],
"main": "./out/extension.js",
"contributes": {
Expand Down Expand Up @@ -238,4 +239,4 @@
"lz-string": "^1.5.0",
"winreg": "^1.2.5"
}
}
}
81 changes: 81 additions & 0 deletions src/extension-onUri.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as vscode from "vscode";
import { URLSearchParams } from "url";
import {
shinyliveSaveAppFromUrl,
shinyliveUrlDecode,
shinyliveUrlEncode,
} from "./shinylive";

export async function handlePositShinyUri(uri: vscode.Uri): Promise<void> {
if (!["/shinylive", "/shinylive/"].includes(uri.path)) {
console.warn(`[shiny] Unexpected URI: ${uri.toString()}`);
return;
}

const encodedUrl = new URLSearchParams(uri.query).get("url");

if (!encodedUrl) {
vscode.window.showErrorMessage(
"No URL provided in the Open from Shinylive link."
);
return;
}

const url = decodeURIComponent(encodedUrl);
const bundle = shinyliveUrlDecode(url);

if (!bundle) {
vscode.window.showErrorMessage(
"Shinylive: Failed to parse the Shinylive link. " +
"Please check the link and try again."
);
return;
}

let filesText = bundle.files
.slice(0, 3)
.map((f) => f.name)
.join(", ");
if (bundle.files.length > 3) {
filesText += `, and ${bundle.files.length - 3} more files`;
}

const reviewAction = await vscode.window.showWarningMessage(
`You are about to save a Shinylive app with ${filesText} to your workspace. Would you like to...`,
{ modal: true },
{ title: "Cancel", action: "cancel" },
{ title: "Review the app on shinylive.io", action: "review" },
{
title: `Save app ${bundle.files.length === 1 ? "file" : "files"} locally`,
action: "save",
}
);

let { action } = reviewAction || { action: "cancel" };

if (action === "cancel") {
return;
}

if (action === "review") {
bundle.mode = "editor";
const editorUrl = shinyliveUrlEncode(bundle);
vscode.env.openExternal(vscode.Uri.parse(editorUrl));

const openAfterReview = await vscode.window.showInformationMessage(
"After reviewing the Shinylive app, would you like to save it?",
{ modal: true },
"Yes"
);

if (!openAfterReview) {
return;
}
action = "save";
}

if (action === "save") {
await shinyliveSaveAppFromUrl(url);
return;
}
}
8 changes: 7 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
shinyliveSaveAppFromUrl,
shinyliveCreateFromExplorer,
} from "./shinylive";
import { handlePositShinyUri } from "./extension-onUri";

export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
Expand All @@ -23,7 +24,12 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand(
"shiny.shinylive.createFromExplorer",
shinyliveCreateFromExplorer
)
),
vscode.window.registerUriHandler({
handleUri(uri: vscode.Uri): vscode.ProviderResult<void> {
handlePositShinyUri(uri);
},
})
);

const throttledUpdateContext = new Throttler(2000, () => {
Expand Down
2 changes: 1 addition & 1 deletion src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ async function getRPathFromPositron(bin: string): Promise<string> {
return "";
}

console.log(`[shiny] runtimeMetadata: ${JSON.stringify(runtimeMetadata)}`)
console.log(`[shiny] runtimeMetadata: ${JSON.stringify(runtimeMetadata)}`);

const runtimePath = runtimeMetadata.runtimePath;
if (!runtimePath) {
Expand Down
16 changes: 12 additions & 4 deletions src/shinylive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,19 @@ async function createAndOpenShinyliveLink(
* files will be saved. The link is decoded and the files are saved into the
* directory.
*
* @param {string} [url] The Shinylive URL to save the app from. If not provided
* the user will be prompted to enter a URL.
*
* @export
* @async
*/
export async function shinyliveSaveAppFromUrl(): Promise<void> {
const url = await askUserForUrl();
export async function shinyliveSaveAppFromUrl(
url: string | undefined
): Promise<void> {
if (typeof url === "undefined") {
url = await askUserForUrl();
}

if (!url) {
return;
}
Expand Down Expand Up @@ -496,7 +504,7 @@ async function askUserForOutputLocation(
* with the language, files, and mode to encode.
* @returns {string} The encoded Shinylive URL.
*/
function shinyliveUrlEncode({ language, files, mode }: ShinyliveBundle) {
export function shinyliveUrlEncode({ language, files, mode }: ShinyliveBundle) {
const filesJson = JSON.stringify(files);
const filesLZ = lzstring.compressToEncodedURIComponent(filesJson);

Expand All @@ -523,7 +531,7 @@ function shinyliveUrlEncode({ language, files, mode }: ShinyliveBundle) {
* @returns {ShinyliveBundle | undefined} The decoded Shinylive bundle, or
* `undefined` if the URL could not be decoded.
*/
function shinyliveUrlDecode(url: string): ShinyliveBundle | undefined {
export function shinyliveUrlDecode(url: string): ShinyliveBundle | undefined {
const { hash, pathname } = new URL(url);
const { searchParams } = new URL(
"https://shinylive.io/?" + hash.substring(1)
Expand Down

0 comments on commit dc85119

Please sign in to comment.