Repository: stateful/vscode-runme
Pull request #455 (2023-04-26 19:10:01)

"Open with Runme" explorer context menu
Introduces "Open with Runme" button for all markdown files in the explorer menu.

<img width="410" alt="image" src="https://user-images.githubusercontent.com/16108792/234678328-eecead92-1ec3-414f-9750-6454e0bfc968.png">



Patch:
diff --git a/examples/README.md b/examples/README.md
index da5bf13e..3f2c432c 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -135,7 +135,7 @@ def hello():
 ## Curl an image
 
 ```sh { interactive=false, mimeType=image/png }
-curl https://lever-client-logos.s3.us-west-2.amazonaws.com/a8ff9b1f-f313-4632-b90f-1f7ae7ee807f-1638388150933.png 2>/dev/null
+curl -s https://lever-client-logos.s3.us-west-2.amazonaws.com/a8ff9b1f-f313-4632-b90f-1f7ae7ee807f-1638388150933.png
 ```
 
 ## Terminal Dimensions
diff --git a/package-lock.json b/package-lock.json
index 5f9e0e15..7333ff68 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "runme",
-  "version": "1.2.0-edge.3",
+  "version": "1.2.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "runme",
-      "version": "1.2.0-edge.3",
+      "version": "1.2.0",
       "hasInstallScript": true,
       "license": "Apache-2.0",
       "dependencies": {
diff --git a/package.json b/package.json
index fcb39bed..2e2c51d3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "runme",
-  "version": "1.2.0-edge.3",
+  "version": "1.2.0",
   "publisher": "stateful",
   "displayName": "Runme",
   "description": "Run your README.md as an interactive notebook",
@@ -139,6 +139,10 @@
       {
         "command": "runme.codelens.action",
         "title": "Runme CodeLens Action"
+      },
+      {
+        "command": "runme.file.openInRunme",
+        "title": "Open in Runme"
       }
     ],
     "menus": {
@@ -174,6 +178,10 @@
         {
           "command": "runme.codelens.action",
           "when": "false"
+        },
+        {
+          "command": "runme.file.openInRunme",
+          "when": "false"
         }
       ],
       "editor/title": [
@@ -205,6 +213,13 @@
           "when": "view == runme.launcher && !runme.launcher.isExpanded",
           "group": "navigation"
         }
+      ],
+      "explorer/context": [
+        {
+          "command": "runme.file.openInRunme",
+          "when": "resourceLangId == markdown",
+          "group": "navigation@30"
+        }
       ]
     },
     "notebooks": [
diff --git a/src/extension/commands/index.ts b/src/extension/commands/index.ts
index 6aeda8b9..3a7b735d 100644
--- a/src/extension/commands/index.ts
+++ b/src/extension/commands/index.ts
@@ -9,7 +9,7 @@ import { v4 as uuidv4 } from 'uuid'
 
 import { getBinaryPath, getTLSDir, getTLSEnabled, isNotebookTerminalEnabledForCell } from '../../utils/configuration'
 import { Kernel } from '../kernel'
-import { getAnnotations, getTerminalByCell, replaceOutput } from '../utils'
+import { getAnnotations, getTerminalByCell, openFileAsRunmeNotebook, replaceOutput } from '../utils'
 import RunmeServer from '../server/runmeServer'
 import { GrpcRunnerEnvironment } from '../runner'
 
@@ -173,3 +173,7 @@ export async function tryIt (context: ExtensionContext) {
     return commands.executeCommand('vscode.openWith', localMarkdown, Kernel.type)
   }
 }
+
+export async function openFileInRunme (uri: Uri, selection?: Uri[]) {
+  await Promise.all((selection ?? [uri]).map(openFileAsRunmeNotebook))
+}
diff --git a/src/extension/extension.ts b/src/extension/extension.ts
index 9a2760cd..d88ee574 100644
--- a/src/extension/extension.ts
+++ b/src/extension/extension.ts
@@ -19,7 +19,8 @@ import {
   stopBackgroundTask,
   createNewRunmeNotebook,
   welcome,
-  tryIt
+  tryIt,
+  openFileInRunme
 } from './commands'
 import { WasmSerializer, GrpcSerializer } from './serializer'
 import { RunmeLauncherProvider } from './provider/launcher'
@@ -117,6 +118,7 @@ export class RunmeExtension {
       RunmeExtension.registerCommand('runme.openRunmeFile', RunmeLauncherProvider.openFile),
       RunmeExtension.registerCommand('runme.keybinding.m', () => { }),
       RunmeExtension.registerCommand('runme.keybinding.y', () => { }),
+      RunmeExtension.registerCommand('runme.file.openInRunme', openFileInRunme),
       tasks.registerTaskProvider(RunmeTaskProvider.id, new RunmeTaskProvider(context, serializer, runner, kernel)),
       notebooks.registerNotebookCellStatusBarItemProvider(Kernel.type, new CliProvider()),
 
diff --git a/src/extension/utils.ts b/src/extension/utils.ts
index 749822cf..24387932 100644
--- a/src/extension/utils.ts
+++ b/src/extension/utils.ts
@@ -11,6 +11,7 @@ import vscode, {
   NotebookCell,
   NotebookCellExecution,
   NotebookCellOutput,
+  commands,
 } from 'vscode'
 import { v5 as uuidv5 } from 'uuid'
 import getPort from 'get-port'
@@ -343,3 +344,7 @@ export function processEnviron(): string[] {
 export function isWindows(): boolean {
   return os.platform().startsWith('win')
 }
+
+export async function openFileAsRunmeNotebook(uri: Uri): Promise<void> {
+  return await commands.executeCommand('vscode.openWith', uri, Kernel.type)
+}


Test patch:
diff --git a/tests/extension/commands/index.test.ts b/tests/extension/commands/index.test.ts
index 1b0c4d69..e2c0b728 100644
--- a/tests/extension/commands/index.test.ts
+++ b/tests/extension/commands/index.test.ts
@@ -24,9 +24,10 @@ import {
   stopBackgroundTask,
   createNewRunmeNotebook,
   welcome,
-  tryIt
+  tryIt,
+  openFileInRunme
 } from '../../../src/extension/commands'
-import { getTerminalByCell, getAnnotations, replaceOutput } from '../../../src/extension/utils'
+import { getTerminalByCell, getAnnotations, replaceOutput, openFileAsRunmeNotebook } from '../../../src/extension/utils'
 import { getBinaryPath, isNotebookTerminalEnabledForCell } from '../../../src/utils/configuration'
 
 vi.mock('vscode', () => import(path.join(process.cwd(), '__mocks__', 'vscode')))
@@ -35,6 +36,7 @@ vi.mock('../../../src/extension/utils', () => ({
   getAnnotations: vi.fn(),
   getTerminalByCell: vi.fn(),
   replaceOutput: vi.fn(),
+  openFileAsRunmeNotebook: vi.fn(),
 }))
 vi.mock('../../../src/utils/configuration', () => ({
   getBinaryPath: vi.fn(),
@@ -175,3 +177,29 @@ test('tryIt command in failure case', async () => {
   await tryIt({ extensionPath: '/foo/bar' } as any)
   expect(commands.executeCommand).toBeCalledWith('vscode.openWith', expect.any(Object), 'runme')
 })
+
+beforeEach(() => {
+  vi.mocked(openFileAsRunmeNotebook).mockClear()
+})
+
+test('openFileInRunme command', async () => {
+  vi.mocked(openFileAsRunmeNotebook).mockClear()
+
+  const uri = { } as any
+  await openFileInRunme(uri)
+
+  expect(openFileAsRunmeNotebook).toHaveBeenCalledOnce()
+  expect(openFileAsRunmeNotebook).toBeCalledWith(uri, 0, [uri])
+})
+
+test('openFileInRunme command multiselect', async () => {
+  vi.mocked(openFileAsRunmeNotebook).mockClear()
+
+  const uri = { } as any
+  const uri2 = { } as any
+  await openFileInRunme(uri, [ uri, uri2 ])
+
+  expect(openFileAsRunmeNotebook).toBeCalledTimes(2)
+  expect(openFileAsRunmeNotebook).toBeCalledWith(uri, 0, [uri, uri2])
+  expect(openFileAsRunmeNotebook).toBeCalledWith(uri2, 1, [uri, uri2])
+})
diff --git a/tests/extension/extension.test.ts b/tests/extension/extension.test.ts
index a7cee88b..0e241f98 100644
--- a/tests/extension/extension.test.ts
+++ b/tests/extension/extension.test.ts
@@ -54,7 +54,7 @@ test('initializes all providers', async () => {
   await ext.initialize(context)
   expect(notebooks.registerNotebookCellStatusBarItemProvider).toBeCalledTimes(6)
   expect(workspace.registerNotebookSerializer).toBeCalledTimes(1)
-  expect(commands.registerCommand).toBeCalledTimes(19)
+  expect(commands.registerCommand).toBeCalledTimes(20)
   expect(window.registerTreeDataProvider).toBeCalledTimes(1)
   expect(window.registerUriHandler).toBeCalledTimes(1)
 
diff --git a/tests/extension/utils.test.ts b/tests/extension/utils.test.ts
index 16690043..50da2031 100644
--- a/tests/extension/utils.test.ts
+++ b/tests/extension/utils.test.ts
@@ -1,4 +1,4 @@
-import vscode, { workspace } from 'vscode'
+import vscode, { commands, workspace } from 'vscode'
 import { expect, vi, test, beforeEach, beforeAll, afterAll, suite } from 'vitest'
 
 import {
@@ -13,6 +13,7 @@ import {
   hashDocumentUri,
   getGrpcHost,
   prepareCmdSeq,
+  openFileAsRunmeNotebook,
 } from '../../src/extension/utils'
 import { ENV_STORE, DEFAULT_ENV } from '../../src/extension/constants'
 import { CellAnnotations } from '../../src/types'
@@ -38,6 +39,9 @@ vi.mock('vscode', () => ({
   workspace: {
     getConfiguration: vi.fn()
   },
+  commands: {
+    executeCommand: vi.fn()
+  },
 }))
 vi.mock('vscode-telemetry')
 
@@ -301,3 +305,17 @@ suite('prepareCmdSeq', () => {
     expect(prepareCmdSeq('echo 1\necho 2\n $ echo 4')).toStrictEqual(['echo 1', 'echo 2', 'echo 4'])
   })
 })
+
+suite('openFileAsRunmeNotebook', () => {
+  beforeEach(() => {
+    vi.mocked(commands.executeCommand).mockClear()
+  })
+
+  test('runs executecommand', async () => {
+    const uri = { } as any
+    await openFileAsRunmeNotebook(uri)
+
+    expect(commands.executeCommand).toBeCalledTimes(1)
+    expect(commands.executeCommand).toBeCalledWith('vscode.openWith', uri, 'runme')
+  })
+})