Repository: marp-team/marp-vscode (ts)

Issue:
A default file name for exporting untitled document becomes ".pdf"
### Version of Marp Tool

Marp for VS Code v1.3.0

### Operating System

Windows

### Environment

- OS version: Windows 10
- VS Code version (Marp for VS Code): VS Code 1.59.1


### How to reproduce

1. Create an untitled document
2. Enable Marp feature
3. Execute export command

### Expected behavior

A default file name is empty as same as previous versions (Marp for VS Code <= v1.2.0).

### Actual behavior

".pdf" is the default file name.

### Additional information

_No response_

Pull request:
Fix the default export name for untitled Markdown
Closes #280.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 46c9192..cf59e49 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,10 @@
 
 - [Early preview] Set up build for Web extension such as [github.dev](https://github.dev) ([#281](https://github.com/marp-team/marp-vscode/issues/281), [#283](https://github.com/marp-team/marp-vscode/pull/283))
 
+### Fixed
+
+- Fix the default export name for untitled Markdown ([#280](https://github.com/marp-team/marp-vscode/issues/280), [#285](https://github.com/marp-team/marp-vscode/pull/285))
+
 ### Changed
 
 - Upgrade Marp CLI to [v1.4.0](https://github.com/marp-team/marp-cli/releases/tag/v1.4.0) ([#282](https://github.com/marp-team/marp-vscode/pull/282))
diff --git a/src/commands/export.ts b/src/commands/export.ts
index c9e0ebf..3aea3c8 100644
--- a/src/commands/export.ts
+++ b/src/commands/export.ts
@@ -15,7 +15,7 @@ import marpCli, {
   createWorkFile,
   MarpCLIError,
 } from '../marp-cli'
-import { marpConfiguration, unlink } from '../utils'
+import { marpConfiguration, unlink, hasToString } from '../utils'
 import {
   createWorkspaceProxyServer,
   WorkspaceProxyServer,
@@ -152,8 +152,9 @@ export const doExport = async (uri: Uri, document: TextDocument) => {
         `Failure to export${(() => {
           if (e instanceof MarpCLIError) return `. ${e.message}`
           if (e instanceof Error) return `: [${e.name}] ${e.message}`
+          if (hasToString(e)) return `. ${e.toString()}`
 
-          return `. ${e.toString()}`
+          return ' by unknown error.'
         })()}`
       )
     } finally {
@@ -174,7 +175,11 @@ export const saveDialog = async (document: TextDocument) => {
   const types = [...new Set<string>([defaultType, ...baseTypes])]
 
   const saveURI = await window.showSaveDialog({
-    defaultUri: Uri.file(fsPath.slice(0, -path.extname(fsPath).length) + ext),
+    defaultUri: Uri.file(
+      (document.isUntitled
+        ? 'untitled'
+        : fsPath.slice(0, -path.extname(fsPath).length)) + ext
+    ),
     filters: types.reduce((f, t) => {
       if (baseTypes.includes(t)) f[descriptions[t]] = extensions[t]
       return f
diff --git a/src/utils.ts b/src/utils.ts
index dae1fc7..328acb1 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -64,3 +64,20 @@ export const writeFile = (target: Uri, text: string) =>
 
 export const unlink = (target: Uri) =>
   workspace.fs.delete(target, { useTrash: false })
+
+export const hasToString = (
+  target: unknown
+): target is { toString(): string } => {
+  switch (typeof target) {
+    case 'object':
+      return typeof target?.toString === 'function'
+    case 'bigint':
+    case 'boolean':
+    case 'function':
+    case 'number':
+    case 'string':
+    case 'symbol':
+      return true
+  }
+  return false
+}


Test patch:
diff --git a/src/commands/export.test.ts b/src/commands/export.test.ts
index d971e11..80304c6 100644
--- a/src/commands/export.test.ts
+++ b/src/commands/export.test.ts
@@ -117,6 +117,18 @@ describe('#saveDialog', () => {
     )
   })
 
+  it('opens save dialog with default name "untitled" if the document is untitled', async () => {
+    const document: any = { uri: { fsPath: '' }, isUntitled: true }
+
+    await exportModule.saveDialog(document)
+
+    expect(window.showSaveDialog).toHaveBeenCalledWith(
+      expect.objectContaining({
+        defaultUri: expect.objectContaining({ fsPath: 'untitled.pdf' }),
+      })
+    )
+  })
+
   it('opens save dialog with configured default type', async () => {
     setConfiguration({ 'markdown.marp.exportType': 'pptx' })
 
@@ -197,13 +209,21 @@ describe('#doExport', () => {
       expect.stringContaining('[Error] ERROR')
     )
 
-    // Unknown error
+    // Unknown error (via toString())
     marpCliSpy.mockRejectedValueOnce('UNKNOWN ERROR!')
     await exportModule.doExport(saveURI(), document)
 
     expect(window.showErrorMessage).toHaveBeenCalledWith(
       expect.stringContaining('UNKNOWN ERROR!')
     )
+
+    // WTF
+    marpCliSpy.mockRejectedValueOnce(null)
+    await exportModule.doExport(saveURI(), document)
+
+    expect(window.showErrorMessage).toHaveBeenCalledWith(
+      'Failure to export by unknown error.'
+    )
   })
 
   describe('when enabled markdown.marp.pdf.noteAnnotations', () => {
diff --git a/src/utils.test.ts b/src/utils.test.ts
index 2febc5a..7c6b998 100644
--- a/src/utils.test.ts
+++ b/src/utils.test.ts
@@ -72,4 +72,24 @@ describe('Utilities', () => {
         jest.advanceTimersByTime(timeout)
       }))
   })
+
+  describe('#hasToString', () => {
+    it('returns true if the object has a toString method', () => {
+      expect(utils.hasToString({ toString: () => 'test' })).toBe(true)
+      expect(utils.hasToString({})).toBe(true)
+      expect(utils.hasToString(Object.create(null))).toBe(false)
+    })
+
+    it('returns true if the literal has a toString method', () => {
+      expect(utils.hasToString(0)).toBe(true)
+      expect(utils.hasToString(BigInt(1))).toBe(true)
+      expect(utils.hasToString('string')).toBe(true)
+      expect(utils.hasToString(Symbol())).toBe(true)
+      expect(utils.hasToString(utils.hasToString)).toBe(true)
+      expect(utils.hasToString(false)).toBe(true)
+      expect(utils.hasToString(true)).toBe(true)
+      expect(utils.hasToString(null)).toBe(false)
+      expect(utils.hasToString(undefined)).toBe(false)
+    })
+  })
 })