Repository: stateful/vscode-runme
Pull request #775 (2023-08-14 21:22:40)

(bugfix): recognise dot entries in `.gitignore`
This patch ensures that any gitignore entries with a starting dot are recognised.

__Behavior Before:__
![Screenshot 2023-08-14 at 14 29 19](https://github.com/stateful/vscode-runme/assets/731337/9a046cd2-aafd-4325-b8bf-24be3eca6f30)

__Behavior After:__
![Screenshot 2023-08-14 at 14 28 13](https://github.com/stateful/vscode-runme/assets/731337/5aa8584e-b59d-41bd-9c31-69aa88bfea1c)



Patch:
diff --git a/src/client/components/terminal/index.ts b/src/client/components/terminal/index.ts
index be51de11..631257e9 100644
--- a/src/client/components/terminal/index.ts
+++ b/src/client/components/terminal/index.ts
@@ -8,7 +8,7 @@ import { WebLinksAddon } from 'xterm-addon-web-links'
 import { when } from 'lit/directives/when.js'
 
 import { FitAddon, type ITerminalDimensions } from '../../fitAddon'
-import { ClientMessages, RENDERERS, OutputType } from '../../../constants'
+import { ClientMessages, RENDERERS, OutputType, WebViews } from '../../../constants'
 import { closeOutput, getContext } from '../../utils'
 import { onClientMessage, postClientMessage } from '../../../utils/messaging'
 import { stripANSI } from '../../../utils/ansi'
@@ -432,6 +432,13 @@ export class TerminalView extends LitElement {
                 this.shareUrl = htmlUrl
                 this.shareText = this.shareEnabledText
                 this.isShareReady = true
+                // Dispatch tangle update event
+                return postClientMessage(ctx, ClientMessages.tangleEvent, {
+                  webviewId: WebViews.RunmeCloud,
+                  data: {
+                    cellId: id,
+                  },
+                })
               }
               if (data.updateCellExecution) {
                 this.isUpdatedReady = true
diff --git a/src/constants.ts b/src/constants.ts
index c5a46360..6e944017 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -46,6 +46,7 @@ export enum ClientMessages {
   githubWorkflowDispatch = 'github:workflowDispatch',
   githubWorkflowDeploy = 'github:workflowDeploy',
   githubWorkflowStatusUpdate = 'github:workflowStatusUpdate',
+  tangleEvent = 'tangle:event',
 }
 
 // [pretty print, languageId, destination]
@@ -1056,3 +1057,7 @@ export const EXTENSION_NAME = 'stateful.runme'
 export enum TELEMETRY_EVENTS {
   RecommendExtension = 'runme.recommendExtension',
 }
+
+export enum WebViews {
+  RunmeCloud = 'runme.cloud',
+}
diff --git a/src/extension/extension.ts b/src/extension/extension.ts
index 5aabe052..792299e0 100644
--- a/src/extension/extension.ts
+++ b/src/extension/extension.ts
@@ -122,8 +122,8 @@ export class RunmeExtension {
       kernel,
       serializer,
       server,
-      ...this.registerPanels(context),
       treeViewer,
+      ...this.registerPanels(kernel, context),
       ...surveys,
       workspace.registerNotebookSerializer(Kernel.type, serializer, {
         transientOutputs: true,
@@ -205,13 +205,15 @@ export class RunmeExtension {
     await bootFile()
   }
 
-  protected registerPanels(context: ExtensionContext): Disposable[] {
+  protected registerPanels(kernel: Kernel, context: ExtensionContext): Disposable[] {
     const id: string = 'runme.cloud' as const
+    const cloudPanel = new Panel(context, id)
     const channel = new Channel<SyncSchema>('app')
-    const p = new Panel(context, id)
-    const bus$ = channel.register([p.webview])
-    p.registerBus(bus$)
-    return [window.registerWebviewViewProvider(id, p), p]
+    const bus$ = channel.register([cloudPanel.webview])
+    cloudPanel.registerBus(bus$)
+    const webviewProvider = window.registerWebviewViewProvider(id, cloudPanel)
+    kernel.registerWebview(id, cloudPanel, webviewProvider)
+    return [webviewProvider, cloudPanel]
   }
 
   static registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any) {
diff --git a/src/extension/kernel.ts b/src/extension/kernel.ts
index 943426f1..b059ad13 100644
--- a/src/extension/kernel.ts
+++ b/src/extension/kernel.ts
@@ -57,6 +57,8 @@ import handleGitHubMessage from './messages/github'
 import { getNotebookCategories } from './utils'
 import { handleCloudApiMessage } from './messages/cloudApiRequest'
 import { SurveyShebangComingSoon } from './survey'
+import PanelManager from './panels/panelManager'
+import Panel from './panels/panel'
 
 enum ConfirmationItems {
   Yes = 'Yes',
@@ -88,6 +90,7 @@ export class Kernel implements Disposable {
   protected cellManager = new NotebookCellManager(this.#controller)
   protected activeTerminals: ActiveTerminal[] = []
   protected category?: string
+  protected panelManager: PanelManager
 
   constructor(protected context: ExtensionContext) {
     const config = workspace.getConfiguration('runme.experiments')
@@ -116,6 +119,7 @@ export class Kernel implements Disposable {
     })
 
     this.messaging.postMessage({ from: 'kernel' })
+    this.panelManager = new PanelManager(context)
     this.#disposables.push(
       this.#shebangComingSoon,
       this.messaging.onDidReceiveMessage(this.#handleRendererMessage.bind(this)),
@@ -123,6 +127,7 @@ export class Kernel implements Disposable {
       workspace.onDidSaveNotebookDocument(this.#handleSaveNotebook.bind(this)),
       window.onDidChangeActiveColorTheme(this.#handleActiveColorThemeMessage.bind(this)),
       window.onDidChangeActiveNotebookEditor(this.#handleActiveNotebook.bind(this)),
+      this.panelManager,
     )
   }
 
@@ -379,6 +384,13 @@ export class Kernel implements Disposable {
     } else if (message.type === ClientMessages.openExternalLink) {
       TelemetryReporter.sendRawTelemetryEvent(message.output.telemetryEvent)
       return env.openExternal(Uri.parse(message.output.link))
+    } else if (message.type === ClientMessages.tangleEvent) {
+      const webviewPanel = this.panelManager.getPanel(message.output.webviewId)
+      if (webviewPanel) {
+        return webviewPanel.getBus()?.emit('onSave', {
+          cellId: message.output.data.cellId,
+        })
+      }
     } else if (message.type.startsWith('terminal:')) {
       return
     }
@@ -620,4 +632,8 @@ export class Kernel implements Disposable {
       return getTerminalRunmeId(t) === runmeId
     }) as RunmeTerminal | undefined
   }
+
+  registerWebview(webviewId: string, panel: Panel, disposableWebViewProvider: Disposable): void {
+    this.panelManager.addPanel(webviewId, panel, disposableWebViewProvider)
+  }
 }
diff --git a/src/extension/panels/panel.ts b/src/extension/panels/panel.ts
index ec8445eb..f3e09918 100644
--- a/src/extension/panels/panel.ts
+++ b/src/extension/panels/panel.ts
@@ -5,7 +5,7 @@ import { Observable, Subscription } from 'rxjs'
 
 import { fetchStaticHtml, getAuthSession } from '../utils'
 import { IAppToken, RunmeService } from '../services/runme'
-import { SyncSchemaBus } from '../../types'
+import { type SyncSchemaBus } from '../../types'
 import { getRunmeAppUrl } from '../../utils/configuration'
 
 export type DefaultUx = 'panels'
@@ -53,6 +53,7 @@ class PanelBase extends TelemetryViewProvider implements Disposable {
 export default class Panel extends PanelBase implements WebviewViewProvider {
   public readonly webview = new Subject<Webview>()
   protected readonly staticHtml
+  private bus$?: SyncSchemaBus
 
   constructor(
     protected readonly context: ExtensionContext,
@@ -111,8 +112,13 @@ export default class Panel extends PanelBase implements WebviewViewProvider {
     }
   }
 
+  public getBus() {
+    return this.bus$
+  }
+
   public registerBus(bus$: Observable<SyncSchemaBus>) {
     bus$.subscribe((bus) => {
+      this.bus$ = bus
       const subs: Subscription[] = [
         bus.on('onCommand', (cmdEvent) => {
           if (cmdEvent?.name !== 'signIn') {
diff --git a/src/extension/panels/panelManager.ts b/src/extension/panels/panelManager.ts
new file mode 100644
index 00000000..76479d50
--- /dev/null
+++ b/src/extension/panels/panelManager.ts
@@ -0,0 +1,30 @@
+import { Disposable, ExtensionContext } from 'vscode'
+
+import Panel from './panel'
+
+interface WebviewPanel {
+  panel: Panel
+  disposableWebViewProvider: Disposable
+}
+
+export default class PanelManager implements Disposable {
+  private panels: Map<string, WebviewPanel> = new Map()
+
+  constructor(readonly context: ExtensionContext) {}
+
+  dispose() {
+    this.panels.forEach((webviewPanel) => {
+      webviewPanel.panel.dispose()
+      webviewPanel.disposableWebViewProvider.dispose()
+    })
+  }
+
+  public addPanel(webviewId: string, panel: Panel, disposableWebViewProvider: Disposable): void {
+    this.panels.set(webviewId, { panel, disposableWebViewProvider })
+  }
+
+  public getPanel(webviewId: string) {
+    const webviewPanel = this.panels.get(webviewId)
+    return webviewPanel?.panel
+  }
+}
diff --git a/src/extension/provider/launcher.ts b/src/extension/provider/launcher.ts
index bd3dd281..06bd7199 100644
--- a/src/extension/provider/launcher.ts
+++ b/src/extension/provider/launcher.ts
@@ -161,9 +161,8 @@ export class RunmeLauncherProvider implements TreeDataProvider<RunmeFile>, Dispo
 
     if (this.workspaceRoot) {
       const gitIgnoreUri = Uri.parse(join(this.workspaceRoot, '.gitignore'))
-      const hasGitDirectory = (await getPathType(gitIgnoreUri)) === FileType.File
-
-      if (hasGitDirectory) {
+      const hasGitignoreFile = (await getPathType(gitIgnoreUri)) === FileType.File
+      if (hasGitignoreFile) {
         try {
           const ignoreList = await workspace.openTextDocument(gitIgnoreUri)
           const patterns = mapGitIgnoreToGlobFolders(ignoreList.getText().split('\n'))
diff --git a/src/extension/utils.ts b/src/extension/utils.ts
index 67c9bfb9..7e9ccb70 100644
--- a/src/extension/utils.ts
+++ b/src/extension/utils.ts
@@ -310,16 +310,16 @@ export function mapGitIgnoreToGlobFolders(gitignoreContents: string[]): Array<st
         }
         const hasExtension = path.extname(entry)
         const slashPlacement = entry.charAt(entry.length - 1)
-        if (slashPlacement === '/') {
+        if (firstChar === '.' || slashPlacement === '/') {
           return `**/${entry}**`
         }
-        if (hasExtension || ['.', '*', '#'].includes(firstChar)) {
+        if (hasExtension || ['*', '#'].includes(firstChar)) {
           return
         }
         return `**/${entry}/**`
       }
     })
-    .filter((entry: string | undefined) => entry)
+    .filter(Boolean)
 
   return [...new Set(entries)]
 }
diff --git a/src/types.ts b/src/types.ts
index dfa365ba..6820e3aa 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -25,6 +25,9 @@ export interface SyncSchema {
     name: string
   }
   onAppToken?: IAppToken
+  onSave?: {
+    cellId: string
+  }
 }
 
 export type SyncSchemaBus = Bus<SyncSchema>
@@ -252,6 +255,10 @@ export interface ClientMessagePayload {
   [ClientMessages.onCopyTextToClipboard]: {
     uuid: string
   }
+  [ClientMessages.tangleEvent]: {
+    data: any
+    webviewId: string
+  }
 }
 
 export interface OutputItemsPayload {


Test patch:
diff --git a/tests/extension/utils.test.ts b/tests/extension/utils.test.ts
index 9e91e3f6..7a2a78b5 100644
--- a/tests/extension/utils.test.ts
+++ b/tests/extension/utils.test.ts
@@ -239,7 +239,9 @@ suite('mapGitIgnoreToGlobFolders', () => {
       '**/node_modules/**',
       '**/.vscode-test/**',
       '**/wasm/**',
+      '**/.DS_Store**',
       '**/coverage/**',
+      '**/.wdio-vscode-service**',
       '**/tests/e2e/logs/**',
       '**/tests/e2e/screenshots/**',
       '**/coverage/config/**',