Repository: stateful/vscode-runme
Pull request #761 (2023-08-07 18:43:45)

"Paste into terminal" as additional option in codelenses
As per the user's request, please see the issue below.

Solves https://github.com/stateful/runme/issues/296.

sourishkrout: review please @christian-bromann 
sourishkrout: @degrammer could do quick review please?

Patch:
diff --git a/src/extension/commands/index.ts b/src/extension/commands/index.ts
index 537e103b..f8093d88 100644
--- a/src/extension/commands/index.ts
+++ b/src/extension/commands/index.ts
@@ -23,7 +23,6 @@ import {
   getActionsOpenViewInEditor,
   getBinaryPath,
   getCLIUseIntegratedRunme,
-  getTLSDir,
   getTLSEnabled,
   isNotebookTerminalEnabledForCell,
 } from '../../utils/configuration'
@@ -31,11 +30,11 @@ import { Kernel } from '../kernel'
 import {
   getAnnotations,
   getNotebookCategories,
+  getRunnerSessionEnvs,
   getTerminalByCell,
   openFileAsRunmeNotebook,
 } from '../utils'
 import RunmeServer from '../server/runmeServer'
-import { GrpcRunnerEnvironment } from '../runner'
 import { NotebookToolbarCommand } from '../../types'
 import getLogger from '../logger'
 import { RecommendExtensionMessage } from '../messaging'
@@ -178,21 +177,14 @@ export function runCLICommand(
       `--index=${index}`,
     ]
 
-    const envs: Record<string, string> = {}
+    let envs: Record<string, string> = {}
 
     if (grpcRunner) {
-      envs['RUNME_SERVER_ADDR'] = server.address()
-
-      if (getTLSEnabled()) {
-        envs['RUNME_TLS_DIR'] = getTLSDir(extensionBaseUri)
-      } else {
+      if (!getTLSEnabled()) {
         args.push('--insecure')
       }
 
-      const runnerEnv = kernel.getRunnerEnvironment()
-      if (runnerEnv && runnerEnv instanceof GrpcRunnerEnvironment) {
-        envs['RUNME_SESSION'] = runnerEnv.getSessionId()
-      }
+      envs = getRunnerSessionEnvs(extensionBaseUri, kernel, server)
     }
 
     const annotations = getAnnotations(cell.metadata)
diff --git a/src/extension/extension.ts b/src/extension/extension.ts
index ab56fafb..5aabe052 100644
--- a/src/extension/extension.ts
+++ b/src/extension/extension.ts
@@ -100,11 +100,13 @@ export class RunmeExtension {
     const runCLI = runCLICommand(context.extensionUri, !!grpcRunner, server, kernel)
 
     const codeLensProvider = new RunmeCodeLensProvider(
+      context.extensionUri,
       serializer,
       runCLI,
       winCodeLensRunSurvey,
       runner,
       kernel,
+      server,
     )
 
     registerExtensionEnvironmentVariables(context)
diff --git a/src/extension/provider/codelens.ts b/src/extension/provider/codelens.ts
index 11c2b93d..64fdb51f 100644
--- a/src/extension/provider/codelens.ts
+++ b/src/extension/provider/codelens.ts
@@ -11,28 +11,31 @@ import {
   languages,
   commands,
   workspace,
+  window,
   tasks,
   EndOfLine,
   Position,
   NotebookData,
   NotebookCellData,
+  Uri,
 } from 'vscode'
 
 import { SerializerBase } from '../serializer'
 import type { runCLICommand } from '../commands'
 import { IRunner } from '../runner'
 import { Kernel } from '../kernel'
-import { getAnnotations } from '../utils'
+import { getAnnotations, getRunnerSessionEnvs } from '../utils'
 import { Serializer } from '../../types'
 import { getCodeLensEnabled } from '../../utils/configuration'
 import { RunmeExtension } from '../extension'
 import type { SurveyWinCodeLensRun } from '../survey'
+import RunmeServer from '../server/runmeServer'
 
 import { RunmeTaskProvider } from './runmeTask'
 
 export const ActionCommand = 'runme.codelens.action' as const
 
-const ActionTypes = ['run', 'open'] as const satisfies readonly string[]
+const ActionTypes = ['run', 'open', 'paste'] as const satisfies readonly string[]
 
 type ActionType = (typeof ActionTypes)[number]
 
@@ -54,11 +57,13 @@ export class RunmeCodeLensProvider implements CodeLensProvider, Disposable {
   public readonly onDidChangeCodeLenses: Event<void> = this._onDidChangeCodeLenses.event
 
   constructor(
+    protected extensionBaseUri: Uri,
     protected serializer: SerializerBase,
     protected runCLI: ReturnType<typeof runCLICommand>,
     protected surveyWinCodeLensRun: SurveyWinCodeLensRun,
     protected runner?: IRunner,
     protected kernel?: Kernel,
+    protected server?: RunmeServer,
   ) {
     this.register(
       languages.registerCodeLensProvider(
@@ -135,6 +140,16 @@ export class RunmeCodeLensProvider implements CodeLensProvider, Disposable {
               })
             }
             break
+
+          case 'paste':
+            {
+              return new CodeLens(range, {
+                title: '$(clippy) Paste into Terminal',
+                command: ActionCommand,
+                arguments: args,
+              })
+            }
+            break
         }
         /* c8 ignore stop */
       })
@@ -206,6 +221,24 @@ export class RunmeCodeLensProvider implements CodeLensProvider, Disposable {
           await tasks.executeTask(task)
         }
         break
+
+      case 'paste':
+        {
+          const value = cell.value
+          let activeTerminal = window.activeTerminal
+
+          if (!activeTerminal) {
+            let envs: Record<string, string> = {}
+            if (this.runner && this.kernel && this.server) {
+              envs = getRunnerSessionEnvs(this.extensionBaseUri, this.kernel, this.server)
+            }
+            activeTerminal = window.createTerminal({ env: envs })
+          }
+
+          activeTerminal.show(false)
+          activeTerminal.sendText(value, false)
+        }
+        break
     }
   }
 }
diff --git a/src/extension/utils.ts b/src/extension/utils.ts
index 8ca1f4b7..67c9bfb9 100644
--- a/src/extension/utils.ts
+++ b/src/extension/utils.ts
@@ -32,11 +32,15 @@ import {
   getEnvLoadWorkspaceFiles,
   getEnvWorkspaceFileOrder,
   getPortNumber,
+  getTLSDir,
+  getTLSEnabled,
 } from '../utils/configuration'
 
 import getLogger from './logger'
 import { Kernel } from './kernel'
 import { ENV_STORE, DEFAULT_ENV, BOOTFILE } from './constants'
+import { GrpcRunnerEnvironment } from './runner'
+import RunmeServer from './server/runmeServer'
 
 declare var globalThis: any
 
@@ -542,3 +546,18 @@ export function getAuthSession(createIfNone: boolean = true) {
 export function fetchStaticHtml(appUrl: string) {
   return fetch(appUrl)
 }
+
+export function getRunnerSessionEnvs(extensionBaseUri: Uri, kernel: Kernel, server: RunmeServer) {
+  const envs: Record<string, string> = {}
+  envs['RUNME_SERVER_ADDR'] = server.address()
+
+  if (getTLSEnabled()) {
+    envs['RUNME_TLS_DIR'] = getTLSDir(extensionBaseUri)
+  }
+
+  const runnerEnv = kernel.getRunnerEnvironment()
+  if (runnerEnv && runnerEnv instanceof GrpcRunnerEnvironment) {
+    envs['RUNME_SESSION'] = runnerEnv.getSessionId()
+  }
+  return envs
+}


Test patch:
diff --git a/tests/e2e/helpers/index.ts b/tests/e2e/helpers/index.ts
index 795a3d33..6e006678 100644
--- a/tests/e2e/helpers/index.ts
+++ b/tests/e2e/helpers/index.ts
@@ -21,6 +21,12 @@ export async function getTerminalText(workbench: Workbench) {
   return text
 }
 
+export async function killAllTerminals(workbench: Workbench) {
+  const bottomBar = workbench.getBottomBar()
+  await bottomBar.openTerminalView()
+  await workbench.executeCommand('Terminal kill all')
+}
+
 export async function tryExecuteCommand(workbench: Workbench, command: string) {
   const cmds = await workbench.openCommandPrompt()
 
diff --git a/tests/e2e/specs/codelense.e2e.ts b/tests/e2e/specs/codelense.e2e.ts
index 711cae84..eb9fbd99 100644
--- a/tests/e2e/specs/codelense.e2e.ts
+++ b/tests/e2e/specs/codelense.e2e.ts
@@ -1,4 +1,4 @@
-import { getTerminalText } from '../helpers/index.js'
+import { getTerminalText, killAllTerminals } from '../helpers/index.js'
 
 describe('Runme Codelense Support', async () => {
   it('should allow to run a cell', async () => {
@@ -8,7 +8,24 @@ describe('Runme Codelense Support', async () => {
 
     const workbench = await browser.getWorkbench()
     const text = await getTerminalText(workbench)
-    expect(text).toContain('Hello World')
+    expect(text).toContain('Hello World!\n ')
+    await killAllTerminals(workbench)
+  })
+
+  it('should allow to paste into terminal', async () => {
+    await browser.waitUntil(
+      () => $$('.codelens-decoration').then((elems) => {
+        return elems.length > 1
+      }),
+      { timeoutMsg: 'Codelens not found', timeout: 30 * 1000 })
+    const lense = (await $$('.codelens-decoration a'))[2]
+    await lense.click()
+    await browser.pause(1000)
+
+    const workbench = await browser.getWorkbench()
+    const text = await getTerminalText(workbench)
+    expect(text).toContain('echo "Hello World!"')
+    await killAllTerminals(workbench)
   })
 
   it('should allow to open file in notebook', async () => {
diff --git a/tests/extension/runner.test.ts b/tests/extension/runner.test.ts
index a18ca3e8..0e6aa3ce 100644
--- a/tests/extension/runner.test.ts
+++ b/tests/extension/runner.test.ts
@@ -4,6 +4,7 @@ import { vi, suite, test, expect, beforeEach } from 'vitest'
 import { type GrpcTransport } from '@protobuf-ts/grpc-transport'
 import { EventEmitter, NotebookCellKind, Position, tasks, commands, EndOfLine, window } from 'vscode'
 import { RpcError } from '@protobuf-ts/runtime-rpc'
+import { URI } from 'vscode-uri'
 
 import {
   GrpcRunner,
@@ -652,7 +653,7 @@ suite('RunmeCodeLensProvider', () => {
 
     expect(serializer.deserializeNotebook).toBeCalledTimes(1)
 
-    expect(lenses).toHaveLength(4)
+    expect(lenses).toHaveLength(6)
 
     lenses.forEach((lens, i) => {
       switch (i) {
@@ -666,7 +667,7 @@ suite('RunmeCodeLensProvider', () => {
           expect(lens.range.end.character).toStrictEqual(0)
         } break
 
-        case 2: {
+        case 3: {
           const line = block2Start.split('\n').length - 2
 
           expect(lens.range.start.line).toStrictEqual(line)
@@ -776,6 +777,7 @@ function createCodeLensProvider(
   runner?: IRunner
 ) {
   return new RunmeCodeLensProvider(
+    URI.parse('file:///foo/bar'),
     serializer,
     vi.fn(),
     new SurveyWinCodeLensRun({} as any),