Repository: stateful/vscode-runme
Pull request #123 (2023-01-13 17:09:43)

Runme notebooks launcher
First iteration for having a markdown filter tree as part of runme extension.
TODO:
- [ ] Make folder exclusion configurable (e.g exclude node_modules folder)
- [x] Open markdown files as runme notebooks
- [ ] Make tree navigation more user friendly, reflecting the original folders structure from the workspace
- [ ] Allow right click over specific folders to filter markdown files

Fixes [https://github.com/stateful/vscode-runme/issues/120](https://github.com/stateful/vscode-runme/issues/120)


https://user-images.githubusercontent.com/4001529/212779676-b732bca5-bcf5-412f-83eb-df948edb544e.mp4





sourishkrout: Looks like a great start 👍
sourishkrout: > * [ ]  Make folder exclusion configurable (e.g exclude node_modules folder)
It'd be great if that was a simple toggle like the timeline panel offers
![image](https://user-images.githubusercontent.com/250527/212379515-5514620e-ecd6-4369-b4a0-479b38452c34.png)

> * [ ]  Open markdown files as runme notebooks
Yep, that's a must have

> * [ ]  Make tree navigation more user friendly, reflecting the original folders structure from the workspace
Let's definitely experiment with that. Currently the UX is a little rocky if entries aren't uncollapsed by default.

> * [ ]  Allow right click over specific folders to filter markdown files
I'd put this in the nice to have category.

Outside of the todos, I'd like to do some editing on panel name etc closer to the finish line.

Patch:
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 82dbc094..3ea98984 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -46,6 +46,17 @@
         "${workspaceFolder}/out/test/**/*.js"
       ],
       "preLaunchTask": "${defaultBuildTask}"
+    },
+    {
+      "type": "node",
+      "request": "launch",
+      "name": "Debug Current Test File",
+      "autoAttachChildProcesses": true,
+      "skipFiles": ["<node_internals>/**", "**/node_modules/**"],
+      "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
+      "args": ["run", "${relativeFile}"],
+      "smartStep": true,
+      "console": "integratedTerminal"
     }
   ]
 }
diff --git a/__mocks__/vscode.ts b/__mocks__/vscode.ts
index 8d1cb9ca..91ab0c16 100644
--- a/__mocks__/vscode.ts
+++ b/__mocks__/vscode.ts
@@ -9,7 +9,7 @@ export const notebooks = {
   createRendererMessaging: vi.fn().mockReturnValue({
     postMessage: vi.fn(),
     onDidReceiveMessage: vi.fn().mockReturnValue({ dispose: vi.fn() })
-  })
+  }),
 }
 
 export const Uri = {
@@ -23,8 +23,29 @@ export const workspace = {
   openTextDocument: vi.fn(),
   registerNotebookSerializer: vi.fn(),
   fs: {
-    readFile: vi.fn().mockResolvedValue(Buffer.from('some wasm file'))
-  }
+    readFile: vi.fn().mockResolvedValue(Buffer.from('some wasm file')),
+  },
+  workspaceFolders: [
+    {
+      uri: {
+        fsPath: '/runme/workspace',
+      },
+    },
+  ],
+  findFiles: vi.fn().mockReturnValue([
+    {
+      path: 'runme/workspace/README.md',
+    },
+    {
+      path: 'runme/workspace/src/README.md',
+    },
+    {
+      path: 'runme/workspace/src/COMMANDS.md',
+    },
+    {
+      path: 'runme/workspace/src/RUNME.md',
+    }
+  ]),
 }
 
 export const terminal = {
@@ -38,7 +59,8 @@ export const window = {
   createTerminal: vi.fn().mockReturnValue(terminal),
   showNotebookDocument: vi.fn(),
   showTextDocument: vi.fn(),
-  onDidChangeActiveNotebookEditor: vi.fn().mockReturnValue({ dispose: vi.fn() })
+  onDidChangeActiveNotebookEditor: vi.fn().mockReturnValue({ dispose: vi.fn() }),
+  registerTreeDataProvider: vi.fn()
 }
 
 export const tasks = {
@@ -50,7 +72,7 @@ export const commands = {
   executeCommand: vi.fn()
 }
 
-export enum ViewColumn  {
+export enum ViewColumn {
   Beside = 'Beside'
 }
 
@@ -67,3 +89,13 @@ export enum NotebookCellKind {
   Code = 1,
   Markup = 2
 }
+
+export const TreeItem = vi.fn()
+export const EventEmitter = vi.fn()
+export const Event = vi.fn()
+
+export enum TreeItemCollapsibleState {
+  None = 0,
+  Collapsed = 1,
+  Expanded = 2
+}
diff --git a/assets/folder.svg b/assets/folder.svg
new file mode 100644
index 00000000..13b18d18
--- /dev/null
+++ b/assets/folder.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><style type="text/css">.icon-canvas-transparent{opacity:0;fill:#F6F6F6;} .icon-vs-out{opacity:0;fill:#F6F6F6;} .icon-vs-fg{opacity:0;fill:#F0EFF1;} .icon-folder{fill:#C5C5C5;}</style><path class="icon-canvas-transparent" d="M16 16h-16v-16h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 2.5v10c0 .827-.673 1.5-1.5 1.5h-11.996c-.827 0-1.5-.673-1.5-1.5v-8c0-.827.673-1.5 1.5-1.5h2.886l1-2h8.11c.827 0 1.5.673 1.5 1.5z" id="outline"/><path class="icon-folder" d="M14.5 2h-7.492l-1 2h-3.504c-.277 0-.5.224-.5.5v8c0 .276.223.5.5.5h11.996c.275 0 .5-.224.5-.5v-10c0-.276-.225-.5-.5-.5zm-.496 2h-6.496l.5-1h5.996v1z" id="iconBg"/><path class="icon-vs-fg" d="M14 3v1h-6.5l.5-1h6z" id="iconFg"/></svg>
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index c2d95e82..32c69088 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -665,25 +665,6 @@
         "send": "^0.18.0"
       }
     },
-    "node_modules/@fastify/static/node_modules/glob": {
-      "version": "8.0.3",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz",
-      "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==",
-      "dev": true,
-      "dependencies": {
-        "fs.realpath": "^1.0.0",
-        "inflight": "^1.0.4",
-        "inherits": "2",
-        "minimatch": "^5.0.1",
-        "once": "^1.3.0"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      }
-    },
     "node_modules/@fastify/static/node_modules/readable-stream": {
       "version": "4.2.0",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.2.0.tgz",
@@ -1869,6 +1850,45 @@
       "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz",
       "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg=="
     },
+    "node_modules/@vercel/nft/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/@vercel/nft/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/@vercel/nft/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/@vercel/nft/node_modules/resolve-from": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
@@ -2161,6 +2181,26 @@
         "concat-map": "0.0.1"
       }
     },
+    "node_modules/@vscode/vsce/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "dev": true,
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/@vscode/vsce/node_modules/minimatch": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -2272,25 +2312,6 @@
         "node": "^16.13 || >=18"
       }
     },
-    "node_modules/@wdio/config/node_modules/glob": {
-      "version": "8.0.3",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz",
-      "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==",
-      "dev": true,
-      "dependencies": {
-        "fs.realpath": "^1.0.0",
-        "inflight": "^1.0.4",
-        "inherits": "2",
-        "minimatch": "^5.0.1",
-        "once": "^1.3.0"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      }
-    },
     "node_modules/@wdio/globals": {
       "version": "8.1.2",
       "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.1.2.tgz",
@@ -3100,12 +3121,54 @@
         "node": ">= 6"
       }
     },
+    "node_modules/archiver-utils/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/archiver-utils/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "dev": true,
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/archiver-utils/node_modules/isarray": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
       "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
       "dev": true
     },
+    "node_modules/archiver-utils/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/archiver-utils/node_modules/readable-stream": {
       "version": "2.3.7",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
@@ -8028,6 +8091,48 @@
         "node": ">=0.6"
       }
     },
+    "node_modules/fstream/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/fstream/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "dev": true,
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/fstream/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/fstream/node_modules/mkdirp": {
       "version": "0.5.6",
       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
@@ -8220,19 +8325,19 @@
       "optional": true
     },
     "node_modules/glob": {
-      "version": "7.2.3",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
-      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "version": "8.0.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz",
+      "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==",
+      "dev": true,
       "dependencies": {
         "fs.realpath": "^1.0.0",
         "inflight": "^1.0.4",
         "inherits": "2",
-        "minimatch": "^3.1.1",
-        "once": "^1.3.0",
-        "path-is-absolute": "^1.0.0"
+        "minimatch": "^5.0.1",
+        "once": "^1.3.0"
       },
       "engines": {
-        "node": "*"
+        "node": ">=12"
       },
       "funding": {
         "url": "https://github.com/sponsors/isaacs"
@@ -8255,26 +8360,6 @@
       "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
       "dev": true
     },
-    "node_modules/glob/node_modules/brace-expansion": {
-      "version": "1.1.11",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-      "dependencies": {
-        "balanced-match": "^1.0.0",
-        "concat-map": "0.0.1"
-      }
-    },
-    "node_modules/glob/node_modules/minimatch": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-      "dependencies": {
-        "brace-expansion": "^1.1.7"
-      },
-      "engines": {
-        "node": "*"
-      }
-    },
     "node_modules/global-dirs": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz",
@@ -13306,6 +13391,45 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/rimraf/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/rimraf/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/rimraf/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/rollup": {
       "version": "3.9.1",
       "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.9.1.tgz",
@@ -14385,6 +14509,26 @@
         "concat-map": "0.0.1"
       }
     },
+    "node_modules/test-exclude/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "dev": true,
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/test-exclude/node_modules/minimatch": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -16700,19 +16844,6 @@
         "send": "^0.18.0"
       },
       "dependencies": {
-        "glob": {
-          "version": "8.0.3",
-          "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz",
-          "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==",
-          "dev": true,
-          "requires": {
-            "fs.realpath": "^1.0.0",
-            "inflight": "^1.0.4",
-            "inherits": "2",
-            "minimatch": "^5.0.1",
-            "once": "^1.3.0"
-          }
-        },
         "readable-stream": {
           "version": "4.2.0",
           "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.2.0.tgz",
@@ -17680,6 +17811,36 @@
           "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz",
           "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg=="
         },
+        "brace-expansion": {
+          "version": "1.1.11",
+          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+          "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+          "requires": {
+            "balanced-match": "^1.0.0",
+            "concat-map": "0.0.1"
+          }
+        },
+        "glob": {
+          "version": "7.2.3",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+          "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.1.1",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "minimatch": {
+          "version": "3.1.2",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+          "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        },
         "resolve-from": {
           "version": "5.0.0",
           "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
@@ -17901,6 +18062,20 @@
             "concat-map": "0.0.1"
           }
         },
+        "glob": {
+          "version": "7.2.3",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+          "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.1.1",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
         "minimatch": {
           "version": "3.1.2",
           "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -17987,21 +18162,6 @@
         "glob": "^8.0.3",
         "import-meta-resolve": "^2.1.0",
         "read-pkg-up": "^9.1.0"
-      },
-      "dependencies": {
-        "glob": {
-          "version": "8.0.3",
-          "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz",
-          "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==",
-          "dev": true,
-          "requires": {
-            "fs.realpath": "^1.0.0",
-            "inflight": "^1.0.4",
-            "inherits": "2",
-            "minimatch": "^5.0.1",
-            "once": "^1.3.0"
-          }
-        }
       }
     },
     "@wdio/globals": {
@@ -18660,12 +18820,45 @@
         "readable-stream": "^2.0.0"
       },
       "dependencies": {
+        "brace-expansion": {
+          "version": "1.1.11",
+          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+          "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+          "dev": true,
+          "requires": {
+            "balanced-match": "^1.0.0",
+            "concat-map": "0.0.1"
+          }
+        },
+        "glob": {
+          "version": "7.2.3",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+          "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.1.1",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
         "isarray": {
           "version": "1.0.0",
           "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
           "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
           "dev": true
         },
+        "minimatch": {
+          "version": "3.1.2",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+          "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        },
         "readable-stream": {
           "version": "2.3.7",
           "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
@@ -22334,6 +22527,39 @@
         "rimraf": "2"
       },
       "dependencies": {
+        "brace-expansion": {
+          "version": "1.1.11",
+          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+          "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+          "dev": true,
+          "requires": {
+            "balanced-match": "^1.0.0",
+            "concat-map": "0.0.1"
+          }
+        },
+        "glob": {
+          "version": "7.2.3",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+          "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.1.1",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "minimatch": {
+          "version": "3.1.2",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+          "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        },
         "mkdirp": {
           "version": "0.5.6",
           "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
@@ -22476,35 +22702,16 @@
       "optional": true
     },
     "glob": {
-      "version": "7.2.3",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
-      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "version": "8.0.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz",
+      "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==",
+      "dev": true,
       "requires": {
         "fs.realpath": "^1.0.0",
         "inflight": "^1.0.4",
         "inherits": "2",
-        "minimatch": "^3.1.1",
-        "once": "^1.3.0",
-        "path-is-absolute": "^1.0.0"
-      },
-      "dependencies": {
-        "brace-expansion": {
-          "version": "1.1.11",
-          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-          "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-          "requires": {
-            "balanced-match": "^1.0.0",
-            "concat-map": "0.0.1"
-          }
-        },
-        "minimatch": {
-          "version": "3.1.2",
-          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-          "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-          "requires": {
-            "brace-expansion": "^1.1.7"
-          }
-        }
+        "minimatch": "^5.0.1",
+        "once": "^1.3.0"
       }
     },
     "glob-parent": {
@@ -26268,6 +26475,38 @@
       "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
       "requires": {
         "glob": "^7.1.3"
+      },
+      "dependencies": {
+        "brace-expansion": {
+          "version": "1.1.11",
+          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+          "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+          "requires": {
+            "balanced-match": "^1.0.0",
+            "concat-map": "0.0.1"
+          }
+        },
+        "glob": {
+          "version": "7.2.3",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+          "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.1.1",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "minimatch": {
+          "version": "3.1.2",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+          "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        }
       }
     },
     "rollup": {
@@ -27076,6 +27315,20 @@
             "concat-map": "0.0.1"
           }
         },
+        "glob": {
+          "version": "7.2.3",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+          "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.1.1",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
         "minimatch": {
           "version": "3.1.2",
           "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
diff --git a/package.json b/package.json
index 01ff640c..e48a4cae 100644
--- a/package.json
+++ b/package.json
@@ -263,6 +263,21 @@
           }
         }
       }
+    ],
+    "views": {
+      "explorer": [
+        {
+          "id": "runme.launcher",
+          "name": "Runme Notebooks",
+          "visibility": "collapsed"
+        }
+      ]
+    },
+    "viewsWelcome": [
+      {
+        "view": "runme.launcher",
+        "contents": "Open a workspace/folder to display Runme files here"
+      }
     ]
   },
   "scripts": {
@@ -274,7 +289,8 @@
     "esmify": "echo '{ \"type\": \"module\" }' >> ./out/package.json",
     "postinstall": "npm run download:wasm",
     "test": "run-s test:*",
-    "test:lint": "eslint src --ext ts",
+    "test:lint": "eslint src tests --ext ts",
+    "test:lint:fix": "eslint src tests --ext ts --fix",
     "test:unit": "vitest -c ./vitest.conf.ts",
     "test:e2e": "wdio run ./tests/e2e/wdio.conf.ts",
     "watch": "npm run build:dev -- --watch"
diff --git a/src/extension/extension.ts b/src/extension/extension.ts
index 3204cc20..3e9a1e5d 100644
--- a/src/extension/extension.ts
+++ b/src/extension/extension.ts
@@ -1,10 +1,10 @@
 
-import { workspace, notebooks, commands, ExtensionContext, tasks } from 'vscode'
+import { workspace, notebooks, commands, ExtensionContext, tasks, window } from 'vscode'
 
 import { Kernel } from './kernel'
 import { ShowTerminalProvider, BackgroundTaskProvider, StopBackgroundTaskProvider} from './provider/background'
 import { CopyProvider } from './provider/copy'
-import { resetEnv } from './utils'
+import { getDefaultWorkspace, resetEnv } from './utils'
 import { CliProvider } from './provider/cli'
 import { AnnotationsProvider } from './provider/annotations'
 import { RunmeTaskProvider } from './provider/runmeTask'
@@ -18,6 +18,7 @@ import {
   createNewRunmeNotebook
 } from './commands'
 import { Serializer } from './serializer'
+import { RunmeLauncherProvider } from './provider/launcher'
 
 
 export class RunmeExtension {
@@ -48,7 +49,9 @@ export class RunmeExtension {
       commands.registerCommand('runme.openSplitViewAsMarkdownText', openSplitViewAsMarkdownText),
       commands.registerCommand('runme.openAsRunmeNotebook', openAsRunmeNotebook),
       commands.registerCommand('runme.new', createNewRunmeNotebook),
-      tasks.registerTaskProvider(RunmeTaskProvider.id, new RunmeTaskProvider(context))
+      commands.registerCommand('runme.openRunmeFile', RunmeLauncherProvider.openFile),
+      tasks.registerTaskProvider(RunmeTaskProvider.id, new RunmeTaskProvider(context)),
+      window.registerTreeDataProvider('runme.launcher', new RunmeLauncherProvider(getDefaultWorkspace()))
     )
 
     /**
diff --git a/src/extension/provider/launcher.ts b/src/extension/provider/launcher.ts
new file mode 100644
index 00000000..d55a81c6
--- /dev/null
+++ b/src/extension/provider/launcher.ts
@@ -0,0 +1,128 @@
+import { join, basename, dirname, resolve } from 'node:path'
+
+import {
+  TreeItem,
+  EventEmitter,
+  workspace,
+  TreeItemCollapsibleState,
+  Command,
+  TreeDataProvider,
+  Event,
+  Uri,
+  commands,
+} from 'vscode'
+
+interface IRunmeFileProps {
+  tooltip: string
+  lightIcon: string
+  darkIcon: string
+  collapsibleState: TreeItemCollapsibleState
+  onSelectedCommand?: Command
+  contextValue: string
+}
+
+export class RunmeFile extends TreeItem {
+  constructor(
+    public readonly label: string,
+    { collapsibleState, tooltip, onSelectedCommand, lightIcon, darkIcon, contextValue }: IRunmeFileProps
+  ) {
+    super(label, collapsibleState)
+    const assetsPath = join(__filename, '..', '..', 'assets')
+    this.tooltip = tooltip
+    this.label = label
+    this.command = onSelectedCommand
+    this.contextValue = contextValue
+    this.iconPath = {
+      light: join(assetsPath, lightIcon),
+      dark: join(assetsPath, darkIcon),
+    }
+  }
+}
+
+export class RunmeLauncherProvider implements TreeDataProvider<RunmeFile> {
+  private filesTree: Map<string, any>
+  constructor(private workspaceRoot?: string | undefined) {
+    this.filesTree = new Map()
+  }
+
+  private _onDidChangeTreeData: EventEmitter<RunmeFile | undefined | void> = new EventEmitter<
+    RunmeFile | undefined | void
+  >()
+
+  readonly onDidChangeTreeData: Event<RunmeFile | undefined | void> = this._onDidChangeTreeData.event
+  getTreeItem(element: RunmeFile): TreeItem | Thenable<TreeItem> {
+    return element
+  }
+  getChildren(element?: RunmeFile | undefined): Thenable<RunmeFile[]> {
+    if (!this.workspaceRoot) {
+      return Promise.resolve([])
+    }
+
+    if (!element) {
+      return new Promise(async (resolve: (value: RunmeFile[]) => void) => {
+        await this.getRunmeFilesFromWorkspace(resolve)
+      })
+    }
+
+    const { files, folderPath } = this.filesTree.get(element.label)
+    const folderMarkdownItems: RunmeFile[] = []
+
+    for (const file of files) {
+      folderMarkdownItems.push(
+        new RunmeFile(file, {
+          collapsibleState: TreeItemCollapsibleState.None,
+          tooltip: 'Click to open runme file',
+          lightIcon: 'icon.gif',
+          darkIcon: 'icon.gif',
+          contextValue: 'markdown-file',
+          onSelectedCommand: {
+            arguments: [{ file, folderPath }],
+            command: 'runme.openRunmeFile',
+            title: file,
+          },
+        })
+      )
+    }
+
+    return Promise.resolve(folderMarkdownItems)
+  }
+
+  refresh(): void {
+    this._onDidChangeTreeData.fire()
+  }
+
+  async getRunmeFilesFromWorkspace(onComplete: (value: RunmeFile[]) => void): Promise<void> {
+    const runmeFileCollection: RunmeFile[] = []
+    const files = await workspace.findFiles('**/*.md', '**/node_modules/**')
+
+    for (const { path } of files) {
+      const info = basename(path)
+      const folderPath = dirname(path)
+      const folderName = dirname(path).replace(resolve(__dirname, '..'), '')
+      if (!this.filesTree.has(folderName)) {
+        this.filesTree.set(folderName, { files: [info], folderPath })
+      } else {
+        const { files } = this.filesTree.get(folderName)
+        this.filesTree.set(folderName, { files: [...files, info], folderPath })
+      }
+    }
+
+    for (const folder of this.filesTree.keys()) {
+      runmeFileCollection.push(
+        new RunmeFile(folder || basename(this.workspaceRoot || ''), {
+          collapsibleState: TreeItemCollapsibleState.Collapsed,
+          tooltip: 'Click to open runme files from folder',
+          lightIcon: 'folder.svg',
+          darkIcon: 'folder.svg',
+          contextValue: 'folder',
+        })
+      )
+    }
+    onComplete(runmeFileCollection)
+  }
+
+  public static async openFile({ file, folderPath }: { file: string, folderPath: string }) {
+    const doc = Uri.file(`${folderPath}/${file}`)
+    await commands.executeCommand('vscode.openWith', doc, 'runme')
+  }
+}
diff --git a/src/extension/utils.ts b/src/extension/utils.ts
index 27e512e0..83c25eca 100644
--- a/src/extension/utils.ts
+++ b/src/extension/utils.ts
@@ -193,3 +193,8 @@ export async function initWasm(wasmUri: Uri) {
     }
   )
 }
+
+export function getDefaultWorkspace(): string | undefined {
+  return workspace.workspaceFolders && workspace.workspaceFolders.length > 0
+  ? workspace.workspaceFolders[0].uri.fsPath : undefined
+}


Test patch:
diff --git a/tests/extension/extension.test.ts b/tests/extension/extension.test.ts
index 566e16d0..e5b3976c 100644
--- a/tests/extension/extension.test.ts
+++ b/tests/extension/extension.test.ts
@@ -1,5 +1,5 @@
 import { test, expect, vi } from 'vitest'
-import { notebooks, workspace, commands } from 'vscode'
+import { notebooks, workspace, commands, window } from 'vscode'
 
 import { RunmeExtension } from '../../src/extension/extension'
 
@@ -11,5 +11,6 @@ test('initializes all providers', async () => {
   await ext.initialize(context)
   expect(notebooks.registerNotebookCellStatusBarItemProvider).toBeCalledTimes(6)
   expect(workspace.registerNotebookSerializer).toBeCalledTimes(1)
-  expect(commands.registerCommand).toBeCalledTimes(9)
+  expect(commands.registerCommand).toBeCalledTimes(10)
+  expect(window.registerTreeDataProvider).toBeCalledTimes(1)
 })
diff --git a/tests/extension/provider/launcher.test.ts b/tests/extension/provider/launcher.test.ts
new file mode 100644
index 00000000..0b5405d8
--- /dev/null
+++ b/tests/extension/provider/launcher.test.ts
@@ -0,0 +1,40 @@
+import { vi, describe, it, expect } from 'vitest'
+
+import { RunmeFile, RunmeLauncherProvider } from '../../../src/extension/provider/launcher'
+import { getDefaultWorkspace } from '../../../src/extension/utils'
+import { TreeItemCollapsibleState } from '../../../__mocks__/vscode'
+
+vi.mock('vscode')
+
+describe('Runme Notebooks', () => {
+  it('returns an empty tree for empty workspace', async () => {
+    const launchProvider = new RunmeLauncherProvider()
+    const treeItems = await launchProvider.getChildren()
+    expect(treeItems).toStrictEqual([])
+  })
+
+  describe('when opening in a workspace', () => {
+    const launchProvider = new RunmeLauncherProvider(getDefaultWorkspace())
+    it('should return the items for the root folder', async () => {
+      const treeItems = await launchProvider.getChildren()
+      expect(treeItems.length).toStrictEqual(2)
+    })
+
+    it('should return the items for the selected folder', async () => {
+      const element = new RunmeFile('runme/workspace/src', {
+        collapsibleState: TreeItemCollapsibleState.None,
+        tooltip: 'Click to open runme file',
+        lightIcon: 'icon.gif',
+        darkIcon: 'icon.gif',
+        contextValue: 'markdown-file',
+        onSelectedCommand: {
+          arguments: [{ file: 'README.md', folderPath: 'runme/workspace/src' }],
+          command: 'runme.openRunmeFile',
+          title: 'README.md',
+        },
+      })
+      const treeItems = await launchProvider.getChildren(element)
+      expect(treeItems.length).toStrictEqual(3)
+    })
+  })
+})