Repository: stateful/vscode-runme
Pull request #138 (2023-01-23 19:12:17)

Add .gitignore files/folder to exclude list from the launcher

sourishkrout: The `.gitignore` seems to work well now. However, I'm still getting absolute paths:

![image](https://user-images.githubusercontent.com/250527/214129272-ba458df5-6c97-49ff-a1dd-a10f1037ac80.png)

It'd expect folder entries to be trimmed all the way up to `.../stateful.com`.

Patch:
diff --git a/__mocks__/gitignore.mock b/__mocks__/gitignore.mock
new file mode 100644
index 00000000..2ccdc45f
--- /dev/null
+++ b/__mocks__/gitignore.mock
@@ -0,0 +1,17 @@
+out
+node_modules
+.vscode-test/
+*.vsix
+wasm
+.DS_Store
+coverage
+.wdio-vscode-service
+examples/fresh/deno.lock
+tests/e2e/logs
+tests/e2e/screenshots
+#Comment
+\#README
+!coverage/config
+abc/**
+a/**/b
+hello.*
diff --git a/__mocks__/vscode.ts b/__mocks__/vscode.ts
index 91ab0c16..a3675f4b 100644
--- a/__mocks__/vscode.ts
+++ b/__mocks__/vscode.ts
@@ -1,3 +1,5 @@
+import { readFileSync } from 'node:fs'
+import { join } from 'node:path'
 import { vi } from 'vitest'
 
 export const notebooks = {
@@ -14,16 +16,20 @@ export const notebooks = {
 
 export const Uri = {
   joinPath: vi.fn().mockReturnValue('/foo/bar'),
-  parse: vi.fn()
+  parse: vi.fn(),
+  file: vi.fn().mockReturnValue('')
 }
 
 export const workspace = {
   getConfiguration: vi.fn().mockReturnValue(new Map()),
   openNotebookDocument: vi.fn().mockReturnValue({ uri: 'new notebook uri' }),
-  openTextDocument: vi.fn(),
+  openTextDocument: vi.fn().mockReturnValue({
+    getText: vi.fn().mockReturnValue(readFileSync(join(__dirname, 'gitignore.mock'), 'utf8'))
+  }),
   registerNotebookSerializer: vi.fn(),
   fs: {
     readFile: vi.fn().mockResolvedValue(Buffer.from('some wasm file')),
+    stat: vi.fn().mockResolvedValue(1)
   },
   workspaceFolders: [
     {
@@ -99,3 +105,61 @@ export enum TreeItemCollapsibleState {
   Collapsed = 1,
   Expanded = 2
 }
+
+export enum FileType {
+  /**
+   * The file type is unknown.
+   */
+  Unknown = 0,
+  /**
+   * A regular file.
+   */
+  File = 1,
+  /**
+   * A directory.
+   */
+  Directory = 2,
+  /**
+   * A symbolic link to a file.
+   */
+  SymbolicLink = 64
+}
+
+export enum NotebookCellStatusBarAlignment {
+  Left = 1,
+  Right = 2
+}
+
+export class NotebookCellStatusBarItem {
+  text: string;
+
+  alignment: NotebookCellStatusBarAlignment;
+
+  tooltip?: string;
+
+  priority?: number;
+
+  constructor(text: string, alignment: NotebookCellStatusBarAlignment) {}
+}
+
+
+
+export class NotebookCellOutputItem {
+
+  static json(value: any, mime?: string): NotebookCellOutputItem {
+    return {
+      mime: 'text/plain',
+      data: new Uint8Array()
+    }
+  }
+
+  mime: string;
+  data: Uint8Array;
+  constructor(data: Uint8Array, mime: string) {}
+}
+
+export class NotebookCellOutput {
+  items: NotebookCellOutputItem[];
+  metadata?: { [key: string]: any };
+  constructor(items: NotebookCellOutputItem[], metadata?: { [key: string]: any }) {}
+}
diff --git a/package-lock.json b/package-lock.json
index 4010a1db..4188ba74 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14981,9 +14981,9 @@
       }
     },
     "node_modules/ua-parser-js": {
-      "version": "1.0.32",
-      "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.32.tgz",
-      "integrity": "sha512-dXVsz3M4j+5tTiovFVyVqssXBu5HM47//YSOeZ9fQkdDKkfzv2v3PP1jmH6FUyPW+yCSn7aBVK1fGGKNhowdDA==",
+      "version": "1.0.33",
+      "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz",
+      "integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==",
       "dev": true,
       "funding": [
         {
@@ -27663,9 +27663,9 @@
       "dev": true
     },
     "ua-parser-js": {
-      "version": "1.0.32",
-      "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.32.tgz",
-      "integrity": "sha512-dXVsz3M4j+5tTiovFVyVqssXBu5HM47//YSOeZ9fQkdDKkfzv2v3PP1jmH6FUyPW+yCSn7aBVK1fGGKNhowdDA==",
+      "version": "1.0.33",
+      "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz",
+      "integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==",
       "dev": true
     },
     "uc.micro": {
diff --git a/src/extension/provider/annotations.ts b/src/extension/provider/annotations.ts
index 1bd4052d..457bc3a2 100644
--- a/src/extension/provider/annotations.ts
+++ b/src/extension/provider/annotations.ts
@@ -23,7 +23,7 @@ export class AnnotationsProvider implements NotebookCellStatusBarItemProvider {
     )
   }
 
-  protected async toggleCellAnnotations(cell: NotebookCell): Promise<void> {
+  public async toggleCellAnnotations(cell: NotebookCell): Promise<void> {
     const annotationsExists = cell.outputs.find((o) =>
       o.items.find((oi) => oi.mime === OutputType.annotations)
     )
diff --git a/src/extension/provider/launcher.ts b/src/extension/provider/launcher.ts
index 0fa34ceb..4e8a76cf 100644
--- a/src/extension/provider/launcher.ts
+++ b/src/extension/provider/launcher.ts
@@ -10,8 +10,12 @@ import {
   Event,
   Uri,
   commands,
+  FileType,
 } from 'vscode'
 
+import { Kernel } from '../kernel'
+import { mapGitIgnoreToGlobFolders, getPathType } from '../utils'
+
 interface IRunmeFileProps {
   tooltip: string
   lightIcon: string
@@ -93,12 +97,25 @@ export class RunmeLauncherProvider implements TreeDataProvider<RunmeFile> {
 
   async getRunmeFilesFromWorkspace(onComplete: (value: RunmeFile[]) => void): Promise<void> {
     const runmeFileCollection: RunmeFile[] = []
-    const files = await workspace.findFiles('**/*.md', '**/node_modules/**')
+    let excludePatterns
+
+    if (this.workspaceRoot) {
+      const gitIgnoreUri = Uri.parse(join(this.workspaceRoot, '.gitignore'))
+      const hasGitDirectory = (await getPathType(gitIgnoreUri)) === FileType.File
+
+      if (hasGitDirectory) {
+        const ignoreList = await workspace.openTextDocument(gitIgnoreUri)
+        const patterns = mapGitIgnoreToGlobFolders(ignoreList.getText().split('\n'))
+        excludePatterns = patterns.join(',')
+      }
+    }
+    const files = await workspace.findFiles('**/*.md', `{${excludePatterns}}`)
+    const rootFolder = basename(this.workspaceRoot || '')
 
     for (const { path } of files) {
       const info = basename(path)
       const folderPath = dirname(path)
-      const folderName = dirname(path).replace(resolve(this.workspaceRoot || '', '..'), '')
+      const folderName = dirname(path).replace(resolve(this.workspaceRoot || '', '..'), '') || rootFolder
       if (!this.filesTree.has(folderName)) {
         this.filesTree.set(folderName, { files: [info], folderPath })
       } else {
@@ -109,7 +126,7 @@ export class RunmeLauncherProvider implements TreeDataProvider<RunmeFile> {
 
     for (const folder of this.filesTree.keys()) {
       runmeFileCollection.push(
-        new RunmeFile(folder || basename(this.workspaceRoot || ''), {
+        new RunmeFile(folder || rootFolder, {
           collapsibleState: TreeItemCollapsibleState.Collapsed,
           tooltip: 'Click to open runme files from folder',
           lightIcon: 'folder.svg',
@@ -118,11 +135,11 @@ export class RunmeLauncherProvider implements TreeDataProvider<RunmeFile> {
         })
       )
     }
-    onComplete(runmeFileCollection)
+    onComplete(runmeFileCollection.sort((a: RunmeFile,b: RunmeFile) => a.label.length > b.label.length ? 1 : -1 ))
   }
 
   public static async openFile({ file, folderPath }: { file: string, folderPath: string }) {
     const doc = Uri.file(`${folderPath}/${file}`)
-    await commands.executeCommand('vscode.openWith', doc, 'runme')
+    await commands.executeCommand('vscode.openWith', doc, Kernel.type)
   }
 }
diff --git a/src/extension/utils.ts b/src/extension/utils.ts
index 83c25eca..9f8ccb58 100644
--- a/src/extension/utils.ts
+++ b/src/extension/utils.ts
@@ -42,34 +42,26 @@ export function getAnnotations(raw: unknown): NotebookCellAnnotations {
     closeTerminalOnSuccess:
       typeof metadata.closeTerminalOnSuccess === 'string'
         ? TRUTHY_VALUES.includes(metadata.closeTerminalOnSuccess)
-        : config.get<boolean>(
-            'closeTerminalOnSuccess',
-            METADATA_DEFAULTS.closeTerminalOnSuccess
-          ),
-    mimeType:
-      typeof metadata.mimeType === 'string'
-        ? metadata.mimeType
-        : METADATA_DEFAULTS.mimeType,
-    name:
-      metadata['runme.dev/name'] ||
-      `Cell #${Math.random().toString().slice(2)}`,
+        : config.get<boolean>('closeTerminalOnSuccess', METADATA_DEFAULTS.closeTerminalOnSuccess),
+    mimeType: typeof metadata.mimeType === 'string' ? metadata.mimeType : METADATA_DEFAULTS.mimeType,
+    name: metadata['runme.dev/name'] || `Cell #${Math.random().toString().slice(2)}`,
     'runme.dev/uuid': metadata['runme.dev/uuid'],
   }
 }
 
-export function getTerminalByCell (cell: vscode.NotebookCell) {
+export function getTerminalByCell(cell: vscode.NotebookCell) {
   return vscode.window.terminals.find((t) => {
     const taskEnv = (t.creationOptions as vscode.TerminalOptions).env || {}
     return taskEnv.RUNME_ID === `${cell.document.fileName}:${cell.index}`
   })
 }
 
-export function resetEnv () {
+export function resetEnv() {
   [...ENV_STORE.keys()].forEach((key) => ENV_STORE.delete(key))
   Object.entries(DEFAULT_ENV).map(([key, val]) => ENV_STORE.set(key, val))
 }
 
-export function getKey (runningCell: vscode.TextDocument): keyof typeof executor {
+export function getKey(runningCell: vscode.TextDocument): keyof typeof executor {
   const text = runningCell.getText()
   if (text.indexOf('deployctl deploy') > -1) {
     return 'deno'
@@ -85,9 +77,13 @@ export function getKey (runningCell: vscode.TextDocument): keyof typeof executor
  * which need to be executed in sequence
  */
 export function getCmdShellSeq(cellText: string, os: string): string {
-  const trimmed = cellText.trimStart()
-    .split('\\\n').map(l => l.trim()).join(' ')
-    .split('\n').map(l => {
+  const trimmed = cellText
+    .trimStart()
+    .split('\\\n')
+    .map((l) => l.trim())
+    .join(' ')
+    .split('\n')
+    .map((l) => {
       const hashPos = l.indexOf('#')
       if (hashPos > -1) {
         return l.substring(0, hashPos).trim()
@@ -100,12 +96,13 @@ export function getCmdShellSeq(cellText: string, os: string): string {
         return stripped
       }
     })
-    .filter(l => {
+    .filter((l) => {
       const hasPrefix = (l.match(HASH_PREFIX_REGEXP) || []).length > 0
       return l !== '' && !hasPrefix
-    }).join('; ')
+    })
+    .join('; ')
 
-  if (['darwin'].find(entry => entry === os)) {
+  if (['darwin'].find((entry) => entry === os)) {
     return `set -e -o pipefail; ${trimmed}`
   } else if (os.toLocaleLowerCase().startsWith('win')) {
     return trimmed
@@ -124,7 +121,7 @@ export function normalizeLanguage(l?: string) {
   }
 }
 
-export async function verifyCheckedInFile (filePath: string) {
+export async function verifyCheckedInFile(filePath: string) {
   const fileDir = path.dirname(filePath)
   const workspaceFolder = vscode.workspace.workspaceFolders?.find((ws) => fileDir.includes(ws.uri.fsPath))
 
@@ -140,14 +137,16 @@ export async function verifyCheckedInFile (filePath: string) {
     return false
   }
 
-  const isCheckedIn = await util.promisify(cp.exec)(
-    `git ls-files --error-unmatch ${filePath}`,
-    { cwd: workspaceFolder.uri.fsPath }
-  ).then(() => true, () => false)
+  const isCheckedIn = await util
+    .promisify(cp.exec)(`git ls-files --error-unmatch ${filePath}`, { cwd: workspaceFolder.uri.fsPath })
+    .then(
+      () => true,
+      () => false
+    )
   return isCheckedIn
 }
 
-export async function canEditFile (
+export async function canEditFile(
   notebook: NotebookDocument,
   // for testing purposes only
   verifyCheckedInFileFn = verifyCheckedInFile
@@ -172,7 +171,8 @@ export async function canEditFile (
     /**
      * the user works on a checked in file
      */
-    !currentDocumentPath || (await verifyCheckedInFileFn(currentDocumentPath))
+    !currentDocumentPath ||
+    (await verifyCheckedInFileFn(currentDocumentPath))
   ) {
     return true
   }
@@ -196,5 +196,41 @@ export async function initWasm(wasmUri: Uri) {
 
 export function getDefaultWorkspace(): string | undefined {
   return workspace.workspaceFolders && workspace.workspaceFolders.length > 0
-  ? workspace.workspaceFolders[0].uri.fsPath : undefined
+    ? workspace.workspaceFolders[0].uri.fsPath
+    : undefined
+}
+
+export async function getPathType(uri: vscode.Uri): Promise<vscode.FileType> {
+  return workspace.fs.stat(uri).then(
+    (stat) => stat.type,
+    () => FileType.Unknown
+  )
+}
+
+
+export function mapGitIgnoreToGlobFolders(gitignoreContents: string[]): Array<string | undefined> {
+  const entries = gitignoreContents
+    .filter((entry: string) => entry)
+    .map((entry: string) => entry.replace(/\s/g, ''))
+    .map((entry: string) => {
+      if (entry) {
+        let firstChar = entry.charAt(0)
+        if (firstChar === '!' || firstChar === '/') {
+          entry = entry.substring(1, entry.length)
+          firstChar = entry.charAt(0)
+        }
+        const hasExtension = path.extname(entry)
+        const slashPlacement = entry.charAt(entry.length - 1)
+        if (slashPlacement === '/') {
+          return `**/${entry}**`
+        }
+        if (hasExtension || ['.', '*', '#'].includes(firstChar)) {
+          return
+        }
+        return `**/${entry}/**`
+      }
+    }).filter((entry: string | undefined) => entry)
+
+  return [...new Set(entries)]
 }
+


Test patch:
diff --git a/tests/extension/provider/annotations.test.ts b/tests/extension/provider/annotations.test.ts
new file mode 100644
index 00000000..ad3c20a1
--- /dev/null
+++ b/tests/extension/provider/annotations.test.ts
@@ -0,0 +1,137 @@
+import { vi, describe, it, expect } from 'vitest'
+import { commands, NotebookCellKind } from 'vscode'
+
+import { AnnotationsProvider } from '../../../src/extension/provider/annotations'
+import { Kernel } from '../../../src/extension/kernel'
+import { OutputType } from '../../../src/constants'
+
+
+vi.mock('vscode')
+
+vi.mock('../../../src/extension/utils', () => ({
+  getAnnotations: vi.fn().mockReturnValue({
+    'type': 'stateful.runme/annotations',
+    'output': {
+      'annotations':
+      {
+        'background': false,
+        'interactive': true,
+        'closeTerminalOnSuccess': true,
+        'mimeType': 'text/plain',
+        'name': 'npm-install',
+        'runme.dev/uuid': '849448b2-3c41-4323-920e-3098e71302ce'
+      }
+    }
+  }),
+}))
+
+describe('Runme Annotations', () => {
+  const kernel = new Kernel({} as any)
+  it('should register a command when initializing', () => {
+    new AnnotationsProvider(kernel)
+    expect(commands.registerCommand).toBeCalledTimes(1)
+    expect(commands.registerCommand).toBeCalledWith('runme.toggleCellAnnotations', expect.anything())
+  })
+
+
+  describe('provideCellStatusBarItems', () => {
+
+    it('should not create a status bar item for non-code elements', async () => {
+      const annotationsProvider = new AnnotationsProvider(kernel)
+      const cell = {
+        metadata: {
+          background: 'true'
+        },
+        executionSummary: {
+          success: false
+        },
+        kind: NotebookCellKind.Markup
+      }
+      const statusBarItems = await annotationsProvider.provideCellStatusBarItems(cell as any)
+      expect(statusBarItems).toBe(undefined)
+    })
+
+    it('should create a status bar item for code elements', async () => {
+      const annotationsProvider = new AnnotationsProvider(kernel)
+      const cell = {
+        metadata: {
+          background: 'true'
+        },
+        executionSummary: {
+          success: false
+        },
+        kind: NotebookCellKind.Code
+      }
+
+      const expectedItem = {
+        command: {
+          title: 'Configure cell behavior',
+          command: 'runme.toggleCellAnnotations',
+          arguments: [cell],
+        },
+        tooltip: 'Click to configure cell behavior'
+      }
+
+      const statusBarItem = await annotationsProvider.provideCellStatusBarItems(cell as any)
+      expect(statusBarItem).toEqual(expectedItem)
+    })
+  })
+
+  describe('toggleCellAnnotations', () => {
+    it('should clear the ouput when the annotation is already rendered', async () => {
+      const annotationsProvider = new AnnotationsProvider(kernel)
+      const cell = {
+        metadata: {
+          background: 'true'
+        },
+        executionSummary: {
+          success: false
+        },
+        kind: NotebookCellKind.Markup,
+        outputs: [{
+          items: [
+            { id: '', items: [], metadata: {}, mime: OutputType.annotations }
+          ]
+        }]
+      }
+
+      const clearOutput = vi.fn()
+      const end = vi.fn()
+      kernel.createCellExecution = vi.fn().mockResolvedValue({
+        start: vi.fn(),
+        end,
+        clearOutput,
+      })
+      await annotationsProvider.toggleCellAnnotations(cell as any)
+      expect(kernel.createCellExecution).toBeCalledTimes(1)
+      expect(clearOutput).toBeCalledTimes(1)
+      expect(end).toBeCalledTimes(1)
+    })
+
+    it('should replace the output when the annotation is not rendered', async () => {
+      const annotationsProvider = new AnnotationsProvider(kernel)
+      const cell = {
+        metadata: {
+          background: 'true'
+        },
+        executionSummary: {
+          success: false
+        },
+        kind: NotebookCellKind.Markup,
+        outputs: []
+      }
+
+      const replaceOutput = vi.fn()
+      const end = vi.fn()
+      kernel.createCellExecution = vi.fn().mockResolvedValue({
+        start: vi.fn(),
+        end,
+        replaceOutput,
+      })
+      await annotationsProvider.toggleCellAnnotations(cell as any)
+      expect(kernel.createCellExecution).toBeCalledTimes(1)
+      expect(replaceOutput).toBeCalledTimes(1)
+      expect(end).toBeCalledTimes(1)
+    })
+  })
+})
diff --git a/tests/extension/provider/launcher.test.ts b/tests/extension/provider/launcher.test.ts
index 0b5405d8..1f5ef389 100644
--- a/tests/extension/provider/launcher.test.ts
+++ b/tests/extension/provider/launcher.test.ts
@@ -1,11 +1,31 @@
 import { vi, describe, it, expect } from 'vitest'
+import {  commands } from 'vscode'
 
 import { RunmeFile, RunmeLauncherProvider } from '../../../src/extension/provider/launcher'
 import { getDefaultWorkspace } from '../../../src/extension/utils'
-import { TreeItemCollapsibleState } from '../../../__mocks__/vscode'
+
 
 vi.mock('vscode')
 
+vi.mock('../../../src/extension/utils', () => ({
+  getPathType: vi.fn().mockResolvedValue(1),
+  getDefaultWorkspace: vi.fn().mockReturnValue('runme/workspace/src'),
+  mapGitIgnoreToGlobFolders: vi.fn().mockReturnValue([
+    '**/modules/**',
+    '**/out/**',
+    '**/node_modules/**',
+    '**/.vscode-test/**',
+    '**/wasm/**',
+    '**/coverage/**',
+    '**/tests/e2e/logs/**',
+    '**/tests/e2e/screenshots/**',
+    '**/coverage/config/**',
+    '**/abc/**/**',
+    '**/a/**/b/**',
+    '**/jspm_packages/**'
+  ])
+}))
+
 describe('Runme Notebooks', () => {
   it('returns an empty tree for empty workspace', async () => {
     const launchProvider = new RunmeLauncherProvider()
@@ -22,7 +42,7 @@ describe('Runme Notebooks', () => {
 
     it('should return the items for the selected folder', async () => {
       const element = new RunmeFile('runme/workspace/src', {
-        collapsibleState: TreeItemCollapsibleState.None,
+        collapsibleState: 0,
         tooltip: 'Click to open runme file',
         lightIcon: 'icon.gif',
         darkIcon: 'icon.gif',
@@ -35,6 +55,13 @@ describe('Runme Notebooks', () => {
       })
       const treeItems = await launchProvider.getChildren(element)
       expect(treeItems.length).toStrictEqual(3)
+      expect(launchProvider.getTreeItem(element)).toStrictEqual(element)
+    })
+
+    it('should open a file using runme renderer', () => {
+      RunmeLauncherProvider.openFile({ file: 'README.md', folderPath: 'runme/workspace/src' })
+      expect(commands.executeCommand).toBeCalledTimes(1)
+      expect(commands.executeCommand).toBeCalledWith('vscode.openWith', expect.any(String), 'runme')
     })
   })
 })
diff --git a/tests/extension/utilts.test.ts b/tests/extension/utilts.test.ts
index e4dd0c9f..557a9145 100644
--- a/tests/extension/utilts.test.ts
+++ b/tests/extension/utilts.test.ts
@@ -9,6 +9,7 @@ import {
   normalizeLanguage,
   canEditFile,
   getAnnotations,
+  mapGitIgnoreToGlobFolders,
 } from '../../src/extension/utils'
 import { ENV_STORE, DEFAULT_ENV } from '../../src/extension/constants'
 
@@ -178,3 +179,58 @@ suite('canEditFile', () => {
     expect(await canEditFile(notebookMock, verifyCheckedInFile)).toBe(true)
   })
 })
+
+suite('mapGitIgnoreToGlobFolders', () => {
+  test('map properly to glob patterns folders', () => {
+    const gitIgnoreContents = `
+    # Logs
+    report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+    yarn-error.log
+    modules/
+    out
+    node_modules
+    /node_modules
+    .vscode-test/
+    *.vsix
+    wasm
+    .DS_Store
+    coverage
+    .wdio-vscode-service
+    examples/fresh/deno.lock
+    tests/e2e/logs
+    tests/e2e/screenshots
+    #Comment
+    \#README
+    !coverage/config
+    abc/**
+    a/**/b
+    hello.*
+    jspm_packages/
+    `
+
+    const expectedGlobPatterns = [
+      '**/modules/**',
+      '**/out/**',
+      '**/node_modules/**',
+      '**/.vscode-test/**',
+      '**/wasm/**',
+      '**/coverage/**',
+      '**/tests/e2e/logs/**',
+      '**/tests/e2e/screenshots/**',
+      '**/coverage/config/**',
+      '**/abc/**/**',
+      '**/a/**/b/**',
+      '**/jspm_packages/**'
+    ]
+
+    const globPatterns = mapGitIgnoreToGlobFolders(gitIgnoreContents.split('\n'))
+    expect(globPatterns).toStrictEqual(expectedGlobPatterns)
+  })
+
+  test('should handle empty gitignore file properly', () => {
+    const gitIgnoreContents = ''
+    const expectedGlobPatterns = []
+    const globPatterns = mapGitIgnoreToGlobFolders(gitIgnoreContents.split('\n'))
+    expect(globPatterns).toStrictEqual(expectedGlobPatterns)
+  })
+})