Repository: stateful/vscode-runme
Pull request #1125 (2024-02-16 21:13:59)

Named cells UX in notebooks
Make it more clear that cells can have names: https://docs.runme.dev/configuration/cell-level#unnamed-vs-named-cells

Highlight that named cells are helpful to sift through lots of "cells".

While at it, fix https://github.com/stateful/runme/issues/490 and bring it into foldout.

https://github.com/stateful/vscode-runme/assets/250527/709c0810-22ce-4e80-95e8-d94832c08ae4



sourishkrout: Highlight that named cells are helpful to sift through lots of "cells".

https://github.com/stateful/vscode-runme/assets/250527/709c0810-22ce-4e80-95e8-d94832c08ae4


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=stateful_vscode-runme&pullRequest=1125) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=stateful_vscode-runme&pullRequest=1125&resolved=false&inNewCodePeriod=true)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=stateful_vscode-runme&pullRequest=1125&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [89.7% Coverage on New Code](https://sonarcloud.io/component_measures?id=stateful_vscode-runme&pullRequest=1125&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=stateful_vscode-runme&pullRequest=1125&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=stateful_vscode-runme&pullRequest=1125)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=stateful_vscode-runme&pullRequest=1125) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=stateful_vscode-runme&pullRequest=1125&resolved=false&inNewCodePeriod=true)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=stateful_vscode-runme&pullRequest=1125&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [89.8% Coverage on New Code](https://sonarcloud.io/component_measures?id=stateful_vscode-runme&pullRequest=1125&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=stateful_vscode-runme&pullRequest=1125&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=stateful_vscode-runme&pullRequest=1125)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=stateful_vscode-runme&pullRequest=1125) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=stateful_vscode-runme&pullRequest=1125&resolved=false&inNewCodePeriod=true)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=stateful_vscode-runme&pullRequest=1125&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [86.9% Coverage on New Code](https://sonarcloud.io/component_measures?id=stateful_vscode-runme&pullRequest=1125&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=stateful_vscode-runme&pullRequest=1125&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=stateful_vscode-runme&pullRequest=1125)



Patch:
diff --git a/package-lock.json b/package-lock.json
index 46893558..89e67923 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "runme",
-  "version": "3.0.1-edge.1",
+  "version": "3.0.1",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "runme",
-      "version": "3.0.1-edge.1",
+      "version": "3.0.1",
       "hasInstallScript": true,
       "license": "Apache-2.0",
       "dependencies": {
diff --git a/package.json b/package.json
index e6bff71c..a83673f6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "runme",
-  "version": "3.0.1-edge.1",
+  "version": "3.0.1",
   "publisher": "stateful",
   "displayName": "Runme Infra Notebooks",
   "description": "Execute your runbooks, docs, and READMEs.",
diff --git a/src/client/components/configuration/annotations.ts b/src/client/components/configuration/annotations.ts
index 7703ef1e..b1a6614b 100644
--- a/src/client/components/configuration/annotations.ts
+++ b/src/client/components/configuration/annotations.ts
@@ -32,7 +32,6 @@ type Target = {
 
 interface configDetails {
   description: string
-  docs: string
 }
 
 interface DropdownListOption {
@@ -47,43 +46,36 @@ export class Annotations extends LitElement {
   readonly #details: { [id: string]: configDetails } = {
     background: {
       description: 'Run the cell as background process.',
-      docs: 'https://docs.runme.dev/getting-started/vs-code#background-processes',
     },
     interactive: {
       description: 'Run cell inside terminal to allow for interactive input.',
-      docs: 'https://docs.runme.dev/configuration/cell-level#interactive-vs-non-interactive-cells',
     },
     closeTerminalOnSuccess: {
       description: 'Hide terminal after cell successful execution.',
-      docs: 'https://docs.runme.dev/configuration/cell-level#terminal-visibility-post-execution',
     },
     promptEnv: {
       description: 'Prompt user input for exported environment variables.',
-      docs: 'https://docs.runme.dev/configuration/cell-level#set-environment-variables',
     },
     mimeType: {
       description: "Cell's output content MIME type.",
-      docs: 'https://docs.runme.dev/configuration/reference#supported-mime-types',
     },
     name: {
-      description: "Cell's canonical name for easy referencing in the CLI.",
-      docs: 'https://docs.runme.dev/configuration/cell-level#unnamed-vs-named-cells',
+      description: "Cell's canonical name for easy referencing.",
+    },
+    cwd: {
+      description: 'Optionally run the cell in different working directory (cwd).',
     },
     interpreter: {
       description: 'Inserted into shebang (aka #!) line',
-      docs: 'https://docs.runme.dev/configuration/shebang',
     },
     category: {
       description: 'Execute this code cell within a category. (no comma or spaces allowed)',
-      docs: 'https://docs.runme.dev/configuration/cell-level#run-all-cells-by-category',
     },
     excludeFromRunAll: {
       description: 'Prevent executing this cell during the "Run All" operation.',
-      docs: 'https://docs.runme.dev/configuration/cell-level#exclude-cell-from-run-all',
     },
     terminalRows: {
       description: 'Number of rows to display in the notebook terminal.',
-      docs: 'https://docs.runme.dev/configuration/cell-level#terminal-row',
     },
   }
 
@@ -162,7 +154,7 @@ export class Annotations extends LitElement {
   }
 
   renderCheckbox(id: string, isChecked: boolean, isReadOnly: boolean) {
-    const details = this.#details?.[id] as any
+    const details = this.#details?.[id]
 
     return html`<vscode-checkbox
       id="${id}"
@@ -233,18 +225,16 @@ export class Annotations extends LitElement {
     return html`<p class="error-item current-value-error">Received value: ${value}</p>`
   }
 
-  renderDocsLink(link: string) {
+  renderDocsLink(id: string) {
+    const link = `https://docs.runme.dev/r/extension/${id}`
     return html`<vscode-link href="${link}">(docs ${ExternalLinkIcon})</vscode-link>`
   }
 
   renderCheckboxTabEntry(id: AnnotationsKey) {
     const value = this.annotations?.[id]
-    const details = this.#details?.[id]
 
     return html`<div>
-      <div class="themeText" style="font-weight:600">
-        ${id} ${this.renderDocsLink(details.docs)}
-      </div>
+      <div class="themeText" style="font-weight:600">${id} ${this.renderDocsLink(id)}</div>
       <div style="padding-top:4px">${this.renderCheckbox(id, value as boolean, false)}</div>
     </div> `
   }
@@ -254,9 +244,7 @@ export class Annotations extends LitElement {
     const details = this.#details?.[id]
 
     return html`<div>
-      <div class="themeText" style="font-weight:600">
-        ${id} ${this.renderDocsLink(details.docs)}
-      </div>
+      <div class="themeText" style="font-weight:600">${id} ${this.renderDocsLink(id)}</div>
       <div style="padding-top:4px">
         ${this.renderDropdownList(id, details.description, options, value?.toString())}
       </div>
@@ -264,8 +252,8 @@ export class Annotations extends LitElement {
   }
 
   renderTextFieldTabEntry(id: AnnotationsKey) {
-    const value = this.annotations?.[id]
-    const details = this.#details?.[id]
+    let value = this.annotations?.[id]
+    let nameGenerated = this.annotations?.['runme.dev/nameGenerated']
 
     const errors: string[] = this.validationErrors?.errors
       ? this.validationErrors.errors[id as keyof CellAnnotations] || []
@@ -274,11 +262,15 @@ export class Annotations extends LitElement {
       ? this.validationErrors?.originalAnnotations[id as keyof CellAnnotations]
       : value
 
+    let placeHolder = ''
+    if (id === 'name' && nameGenerated && value === this.annotations?.['runme.dev/name']) {
+      placeHolder = value as string
+      value = ''
+    }
+
     return html`<div>
-        <div style="font-weight:600" class="themeText">
-          ${id} ${this.renderDocsLink(details.docs)}
-        </div>
-        <div style="padding-top:4px">${this.renderTextField(id, value as string)}</div>
+        <div style="font-weight:600" class="themeText">${id} ${this.renderDocsLink(id)}</div>
+        <div style="padding-top:4px">${this.renderTextField(id, value as string, placeHolder)}</div>
       </div>
 
       ${when(errors.length, () => this.renderErrors(errors))}
@@ -406,17 +398,17 @@ export class Annotations extends LitElement {
                 { text: 'No', value: 'false' },
               ])}
             </div>
+            <div class="box">${this.renderTextFieldTabEntry('cwd')}</div>
             <div class="box">${this.renderCheckboxTabEntry('interactive')}</div>
-            <div class="box">${this.renderCheckboxTabEntry('background')}</div>
           </div>
         </vscode-panel-view>
         <vscode-panel-view id="view-2">
           <div class="grid">
-            <div class="box">${this.renderTextFieldTabEntry('mimeType')}</div>
+            <div class="box">${this.renderCheckboxTabEntry('background')}</div>
             <div class="box">${this.renderTextFieldTabEntry('interpreter')}</div>
+            <div class="box">${this.renderTextFieldTabEntry('mimeType')}</div>
             <div class="box">${this.renderTextFieldTabEntry('terminalRows')}</div>
             <div class="box">${this.renderCategoryTabEntry('category')}</div>
-            <div class="box">${this.renderCheckboxTabEntry('closeTerminalOnSuccess')}</div>
             <div class="box">${this.renderCheckboxTabEntry('excludeFromRunAll')}</div>
           </div>
         </vscode-panel-view>
diff --git a/src/extension/extension.ts b/src/extension/extension.ts
index 17f68991..4dd447d7 100644
--- a/src/extension/extension.ts
+++ b/src/extension/extension.ts
@@ -68,6 +68,7 @@ import Panel from './panels/panel'
 import { createDemoFileRunnerForActiveNotebook, createDemoFileRunnerWatcher } from './handler/utils'
 import { CloudAuthProvider } from './provider/cloudAuth'
 import { StatefulAuthProvider } from './provider/statefulAuth'
+import { NamedProvider } from './provider/named'
 
 export class RunmeExtension {
   async initialize(context: ExtensionContext) {
@@ -205,6 +206,7 @@ export class RunmeExtension {
         Kernel.type,
         new AnnotationsProvider(kernel),
       ),
+      notebooks.registerNotebookCellStatusBarItemProvider(Kernel.type, new NamedProvider()),
 
       stopBackgroundTaskProvider,
 
diff --git a/src/extension/provider/named.ts b/src/extension/provider/named.ts
new file mode 100644
index 00000000..d9f97956
--- /dev/null
+++ b/src/extension/provider/named.ts
@@ -0,0 +1,43 @@
+import {
+  NotebookCell,
+  NotebookCellStatusBarItemProvider,
+  NotebookCellStatusBarItem,
+  NotebookCellStatusBarAlignment,
+  NotebookCellKind,
+} from 'vscode'
+
+import { getAnnotations } from '../utils'
+
+export class NamedProvider implements NotebookCellStatusBarItemProvider {
+  async provideCellStatusBarItems(
+    cell: NotebookCell,
+  ): Promise<NotebookCellStatusBarItem | undefined> {
+    if (cell.kind !== NotebookCellKind.Code) {
+      return
+    }
+
+    const annotations = getAnnotations(cell)
+
+    let item: NotebookCellStatusBarItem
+    const text = '$(add) Add Name'
+    item = new NotebookCellStatusBarItem(text, NotebookCellStatusBarAlignment.Left)
+    item.text = text
+    item.tooltip = 'Add name to important cells'
+
+    if (
+      annotations['runme.dev/nameGenerated'] !== true ||
+      annotations.name !== annotations['runme.dev/name']
+    ) {
+      item.tooltip = 'Be careful changing the name of an important cell'
+      item.text = `$(file-symlink-file) ${annotations.name}`
+    }
+
+    item.command = {
+      title: 'Configure cell behavior',
+      command: 'runme.toggleCellAnnotations',
+      arguments: [cell],
+    }
+
+    return item
+  }
+}
diff --git a/src/schema.ts b/src/schema.ts
index f879c94c..17116b85 100644
--- a/src/schema.ts
+++ b/src/schema.ts
@@ -48,6 +48,8 @@ const boolify = (defaultValue: boolean, invalidTypeError: string = 'expected a b
 
 export const AnnotationSchema = {
   'runme.dev/id': z.string().optional(),
+  'runme.dev/name': z.string().optional(),
+  'runme.dev/nameGenerated': boolify(true).optional(),
   id: z.string().optional(),
   background: boolify(false),
   interactive: boolify(true),
@@ -79,7 +81,7 @@ export const AnnotationSchema = {
     }, 'mime type specification invalid format')
     .default('text/plain'),
   interpreter: z.string().optional().default(''),
-  cwd: z.string().nonempty().optional(),
+  cwd: z.string().optional().default(''),
   category: z.string().default(''),
 }
 


Test patch:
diff --git a/tests/extension/extension.test.ts b/tests/extension/extension.test.ts
index 79695073..fa18ce98 100644
--- a/tests/extension/extension.test.ts
+++ b/tests/extension/extension.test.ts
@@ -97,7 +97,7 @@ test('initializes all providers', async () => {
   }
   const ext = new RunmeExtension()
   await ext.initialize(context)
-  expect(notebooks.registerNotebookCellStatusBarItemProvider).toBeCalledTimes(6)
+  expect(notebooks.registerNotebookCellStatusBarItemProvider).toBeCalledTimes(7)
   expect(workspace.registerNotebookSerializer).toBeCalledTimes(1)
   expect(commands.registerCommand).toBeCalledTimes(32)
   expect(window.registerTreeDataProvider).toBeCalledTimes(1)
diff --git a/tests/extension/provider/named.test.ts b/tests/extension/provider/named.test.ts
new file mode 100644
index 00000000..5f05c4a0
--- /dev/null
+++ b/tests/extension/provider/named.test.ts
@@ -0,0 +1,56 @@
+import { vi, suite, test, expect, beforeEach } from 'vitest'
+
+import { getAnnotations } from '../../../src/extension/utils'
+import { NamedProvider } from '../../../src/extension/provider/named'
+
+vi.mock('vscode')
+
+vi.mock('../../../src/extension/utils', () => ({
+  getAnnotations: vi.fn(),
+}))
+
+suite('NamedProvider', () => {
+  beforeEach(() => {
+    vi.mocked(getAnnotations).mockClear()
+  })
+
+  test('ignore markdown cells', async () => {
+    const p = new NamedProvider()
+    const item = await p.provideCellStatusBarItems({ kind: 1 } as any)
+    expect(item).toBeUndefined()
+  })
+
+  test('suggest to Add Name if name is generated and unchanged', async () => {
+    vi.mocked(getAnnotations).mockReturnValueOnce({
+      name: 'echo-hello',
+      'runme.dev/name': 'echo-hello',
+      'runme.dev/nameGenerated': true,
+    } as any)
+    const p = new NamedProvider()
+    const item = await p.provideCellStatusBarItems({ kind: 2 } as any)
+    // eslint-disable-next-line quotes
+    expect(item?.text).toMatchInlineSnapshot(`"$(add) Add Name"`)
+    expect(item?.tooltip).toMatchInlineSnapshot(
+      // eslint-disable-next-line quotes
+      `"Add name to important cells"`,
+    )
+    expect(getAnnotations).toBeCalledTimes(1)
+  })
+
+  test('offer changing name if name is not generated', async () => {
+    vi.mocked(getAnnotations).mockReturnValueOnce({
+      name: 'say-hello',
+      'runme.dev/name': 'echo-hello',
+      'runme.dev/nameGenerated': true,
+    } as any)
+    const p = new NamedProvider()
+    const item = await p.provideCellStatusBarItems({ kind: 2 } as any)
+    // eslint-disable-next-line quotes
+    expect(item?.text).toMatchInlineSnapshot(`"$(file-symlink-file) say-hello"`)
+    expect(item?.tooltip).toMatchInlineSnapshot(
+      // eslint-disable-next-line quotes
+      `"Be careful changing the name of an important cell"`,
+    )
+    expect(getAnnotations).toBeCalledTimes(1)
+  })
+})
diff --git a/tests/extension/utils.test.ts b/tests/extension/utils.test.ts
index fbb036f5..0bcbf094 100644
--- a/tests/extension/utils.test.ts
+++ b/tests/extension/utils.test.ts
@@ -230,6 +230,7 @@ suite('#getAnnotations', () => {
     expect(d).toStrictEqual(<CellAnnotations>{
       background: false,
       closeTerminalOnSuccess: true,
+      cwd: '',
       interactive: true,
       mimeType: 'text/plain',
       name: 'command-123',
@@ -254,6 +255,7 @@ suite('#getAnnotations', () => {
       id: undefined,
       background: false,
       closeTerminalOnSuccess: true,
+      cwd: '',
       interactive: true,
       mimeType: 'text/plain',
       name: 'echo-hello',
@@ -261,6 +263,7 @@ suite('#getAnnotations', () => {
       excludeFromRunAll: false,
       category: '',
       interpreter: '',
+      'runme.dev/name': 'echo-hello',
     })
   })
 })