[
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nquote_type = single\n"
  },
  {
    "path": ".eslintignore",
    "content": "/*\n!/src\n"
  },
  {
    "path": ".eslintrc.cjs",
    "content": "module.exports = {\n  extends: [require.resolve('@gera2ld/plaid/eslint')],\n  rules: {},\n};\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Publish to npmjs\n\non:\n  push:\n    tags:\n      - v*\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: pnpm/action-setup@v4\n        name: Install pnpm\n        with:\n          version: 9\n          run_install: false\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 22\n          cache: 'pnpm'\n          registry-url: 'https://registry.npmjs.org'\n\n      - run: pnpm i && pnpm publish --no-git-checks\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n*.log\n/.idea\n/dist\n/.nyc_output\n/coverage\n/types\n"
  },
  {
    "path": ".husky/pre-push",
    "content": "npm run lint\n"
  },
  {
    "path": ".npmrc",
    "content": "shamefully-hoist = true\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Gerald\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# coc-markmap\n\n![NPM](https://img.shields.io/npm/v/coc-markmap.svg)\n\nVisualize your Markdown as mindmaps with [markmap](https://markmap.js.org/).\n\nThis is an extension for [coc.nvim](https://github.com/neoclide/coc.nvim).\n\nIf you prefer a CLI version, see [markmap-cli](https://markmap.js.org/docs/packages--markmap-cli).\n\nNote: _coc-markmap_ uses _markmap-cli_ under the hood, and supports more features by connecting the Markmap with the current buffer, such as highlighting the node under cursor.\n\n<img src=\"https://user-images.githubusercontent.com/3139113/72221499-52476a80-3596-11ea-8d15-c57fdfe04ce0.png\" alt=\"markdown\" width=\"300\"> <img src=\"https://user-images.githubusercontent.com/3139113/72221508-7014cf80-3596-11ea-9b59-b8a97bba8e1c.png\" alt=\"mindmap\" width=\"300\">\n\n## Installation\n\nFirst, make sure [coc.nvim](https://github.com/neoclide/coc.nvim) is started.\n\nThen install with the Vim command:\n\n```\n:CocInstall coc-markmap\n```\n\n## Usage\n\nYou can run the commands below **in a buffer of Markdown file**.\n\n### Generating a markmap HTML\n\n```viml\n:CocCommand markmap.create\n```\n\nInline all assets to work offline:\n\n```viml\n:CocCommand markmap.create --offline\n```\n\n**This command will create an HTML file rendering the markmap and can be easily shared.**\n\nThe HTML file will have the same basename as the Markdown file and will be opened in your default browser. If there is a selection, it will be used instead of the file content.\n\n### Watching mode\n\n```viml\n:CocCommand markmap.watch\n```\n\n**This command will start a development server, watch the current buffer and track your cursor.**\n\nThe markmap will update once the markdown file changes, and the node under cursor will always be visible in the viewport on cursor move.\n\n```viml\n:CocCommand markmap.unwatch\n```\n\n**The command will unwatch the current buffer.**\n\n## Configurations\n\n### CocConfig\n\nYou can change some global configurations for this extension in `coc-settings.json`.\n\nFirst open the settings file with `:CocConfig`.\n\n### Key mappings\n\nThere is no default key mapping, but you can easily add your own:\n\n```viml\n\" Create markmap from the whole file\nnmap <Leader>m <Plug>(coc-markmap-create)\n```\n\n### Commands\n\nIt is also possible to add a command to create markmaps.\n\n```viml\ncommand! -range=% Markmap CocCommand markmap.create <line1> <line2>\n```\n\nNow you have the `:Markmap` command to create a Markmap, either from the whole file or selected lines.\n"
  },
  {
    "path": "babel.config.cjs",
    "content": "module.exports = {\n  presets: ['@babel/preset-env', '@babel/preset-typescript'],\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"coc-markmap\",\n  \"version\": \"0.8.0\",\n  \"description\": \"Visualize your Markdown as mindmaps with Markmap\",\n  \"author\": \"Gerald <gera2ld@live.com>\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"prepare\": \"husky install\",\n    \"dev\": \"rollup -cw\",\n    \"clean\": \"del-cli dist\",\n    \"prepublishOnly\": \"run-s build\",\n    \"ci\": \"run-s lint\",\n    \"build:js\": \"rollup -c\",\n    \"build\": \"run-s ci clean build:js\",\n    \"lint\": \"eslint --ext .ts . && prettier -c src\",\n    \"lint:fix\": \"eslint --ext .ts . --fix && prettier -c src -w\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  },\n  \"main\": \"dist/index.js\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"devDependencies\": {\n    \"@gera2ld/plaid\": \"~2.7.0\",\n    \"@gera2ld/plaid-rollup\": \"~2.7.0\",\n    \"@types/node\": \"^20.11.17\",\n    \"coc.nvim\": \"0.0.83-next.9\",\n    \"del-cli\": \"^6.0.0\",\n    \"es-toolkit\": \"^1.31.0\",\n    \"husky\": \"^9.1.7\"\n  },\n  \"dependencies\": {\n    \"markmap-cli\": \"0.18.7\",\n    \"open\": \"^10.1.0\"\n  },\n  \"engines\": {\n    \"coc\": \">=0.0.80\",\n    \"node\": \">=18\"\n  },\n  \"keywords\": [\n    \"coc.nvim\",\n    \"markmap\"\n  ],\n  \"activationEvents\": [\n    \"onLanguage:markdown\"\n  ],\n  \"contributes\": {\n    \"configuration\": {\n      \"title\": \"coc-markmap\",\n      \"properties\": {}\n    }\n  },\n  \"repository\": \"git@github.com:gera2ld/coc-markmap.git\",\n  \"browserslist\": [\n    \"node >= 18\"\n  ]\n}\n"
  },
  {
    "path": "rollup.config.mjs",
    "content": "import { defineExternal, definePlugins } from '@gera2ld/plaid-rollup';\nimport { defineConfig } from 'rollup';\nimport pkg from './package.json' with { type: 'json' };\n\nexport default defineConfig([\n  {\n    input: './src/index.ts',\n    plugins: definePlugins({\n      esm: true,\n    }),\n    external: defineExternal(['coc.nvim', ...Object.keys(pkg.dependencies)]),\n    output: {\n      format: 'cjs',\n      dir: 'dist',\n    },\n  },\n  {\n    input: './src/bridge.ts',\n    plugins: definePlugins({\n      esm: true,\n    }),\n    external: defineExternal(['coc.nvim', ...Object.keys(pkg.dependencies)]),\n    output: {\n      format: 'es',\n      dir: 'dist',\n    },\n  },\n]);\n"
  },
  {
    "path": "src/bridge.ts",
    "content": "import { createHash } from 'crypto';\nimport {\n  MarkmapDevServer,\n  config,\n  createMarkmap,\n  develop,\n  fetchAssets,\n} from 'markmap-cli';\nimport open from 'open';\n\nlet devServer: MarkmapDevServer | undefined;\n\ntype MaybePromise<T> = T | Promise<T>;\n\nconst handlers: Record<string, (data: unknown) => MaybePromise<unknown>> = {\n  initialize(options: { assetsDir: string }) {\n    config.assetsDir = options.assetsDir;\n  },\n  async createMarkmap(options: Record<string, unknown>) {\n    await fetchAssets();\n    await createMarkmap({\n      open: true,\n      toolbar: true,\n      offline: false,\n      ...options,\n    });\n  },\n  async startServer(options?: Record<string, unknown>) {\n    if (!devServer) {\n      await fetchAssets();\n      devServer = await develop({\n        toolbar: true,\n        offline: true,\n        ...options,\n      });\n    }\n    return (\n      devServer.serverInfo && {\n        port: devServer.serverInfo.address.port,\n      }\n    );\n  },\n  addProvider(filePath: string) {\n    const key = createHash('sha256')\n      .update(filePath, 'utf8')\n      .digest('hex')\n      .slice(0, 7);\n    const provider = invariant(devServer).addProvider({ key });\n    return provider.key;\n  },\n  delProvider(key: string) {\n    invariant(devServer).delProvider(key);\n  },\n  setContent(data: { key: string; content: string }) {\n    const provider = invariant(devServer?.providers[data.key]);\n    provider.setContent(data.content);\n  },\n  setCursor(data: { key: string; line: number }) {\n    const provider = invariant(devServer?.providers[data.key]);\n    provider.setCursor(data.line);\n  },\n  stopServer() {\n    if (!devServer) return;\n    devServer.shutdown();\n    devServer = undefined;\n  },\n  openUrl(url: string) {\n    open(url);\n  },\n};\n\nprocess.on(\n  'message',\n  async ({ id, cmd, data }: { id: number; cmd: string; data: unknown }) => {\n    const handler = handlers[cmd];\n    let result: unknown;\n    let error: string | undefined;\n    try {\n      result = await handler?.(data);\n    } catch (err) {\n      error = `${err}`;\n    }\n    process.send?.({\n      id,\n      cmd: '_setResult',\n      data: { result, error },\n    });\n  },\n);\n\nfunction invariant<T>(input: T | undefined, message?: string): T {\n  if (!input) throw new Error(message || 'input is required');\n  return input;\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "import {\n  Disposable,\n  ExtensionContext,\n  Logger,\n  commands,\n  events,\n  window,\n  workspace,\n} from 'coc.nvim';\nimport { spawn } from 'node:child_process';\nimport { basename, extname, resolve } from 'node:path';\n// Note: only CJS is supported by coc.nvim, so we must bundle it\nimport { debounce } from 'es-toolkit';\n\nclass CocMarkmapBridge {\n  private _child = spawn(process.execPath, [resolve(__dirname, 'bridge.js')], {\n    cwd: __dirname,\n    stdio: ['inherit', 'inherit', 'inherit', 'ipc'],\n  });\n\n  serverInfo: { port: number } | undefined;\n\n  id = 0;\n\n  private _callbacks: Record<\n    number,\n    (data: { result: unknown; error?: string }) => void\n  > = {};\n\n  private _connectedBuffers: Record<number, string> = {};\n\n  private _disposables: Disposable[] = [];\n\n  constructor(private logger: Logger) {\n    this._child.on(\n      'message',\n      (message: {\n        id: number;\n        cmd: string;\n        data: { result: unknown; error?: string };\n      }) => {\n        this._callbacks[message.id]?.(message.data);\n        delete this._callbacks[message.id];\n      },\n    );\n    this._disposables.push(Disposable.create(() => this.stopServer()));\n    this._disposables.push(events.on('TextChanged', this.handleTextChange));\n    this._disposables.push(events.on('TextChangedI', this.handleTextChange));\n    this._disposables.push(events.on('CursorMoved', this.handleCursorChange));\n    this._disposables.push(events.on('CursorMovedI', this.handleCursorChange));\n  }\n\n  private _send<T>(cmd: string, data?: unknown): Promise<T> {\n    this.id += 1;\n    this._child.send({ id: this.id, cmd, data });\n    return new Promise((resolve, reject) => {\n      this._callbacks[this.id] = (data) => {\n        if (data.error) reject(data.error);\n        else resolve(data.result as T);\n      };\n    });\n  }\n\n  initialize(assetsDir: string) {\n    return this._send('initialize', { assetsDir });\n  }\n\n  destroy() {\n    this._child.kill();\n  }\n\n  isServerStarted() {\n    return !!this.serverInfo;\n  }\n\n  async startServer() {\n    this.serverInfo = await this._send('startServer');\n  }\n\n  async stopServer() {\n    if (!this.serverInfo) return;\n    await this._send('stopServer');\n    this.serverInfo = undefined;\n  }\n\n  async setContent(key: string, content: string) {\n    await this._send('setContent', { key, content });\n  }\n\n  async setCursor(key: string, line: number) {\n    await this._send('setCursor', { key, line });\n  }\n\n  async connectBuffer() {\n    await this.startServer();\n    const { nvim } = workspace;\n    const buffer = await nvim.buffer;\n    const filePath = (await nvim.eval('expand(\"%:p\")')) as string;\n    const filename = basename(filePath);\n    const key =\n      this._connectedBuffers[buffer.id] ||\n      (await this._send<string>('addProvider', filePath));\n    this._connectedBuffers[buffer.id] = key;\n    this.handleTextChange(buffer.id);\n    const url = `http://localhost:${this.serverInfo?.port}/?key=${key}&filename=${encodeURIComponent(filename)}`;\n    window.showInformationMessage(\n      `Buffer ${buffer.id}: Markmap is served at ${url}`,\n    );\n    this._send('openUrl', url);\n  }\n\n  async disconnectBuffer() {\n    const { nvim } = workspace;\n    const buffer = await nvim.buffer;\n    const key = this._connectedBuffers[buffer.id];\n    if (key) {\n      await this._send('delProvider', key);\n      delete this._connectedBuffers[buffer.id];\n      window.showInformationMessage(`Buffer ${buffer.id}: Markmap is disposed`);\n    }\n  }\n\n  async createMarkmap(options?: Record<string, unknown>) {\n    const { nvim } = workspace;\n    const filePath = (await nvim.eval('expand(\"%:p\")')) as string;\n    const name = basename(filePath, extname(filePath));\n    const output = resolve(`${name}.html`);\n    const doc = await workspace.document;\n    const content = doc.textDocument.getText();\n    const createOptions = {\n      content,\n      output,\n      ...options,\n    };\n    await this._send('createMarkmap', createOptions);\n  }\n\n  private _bufferIds = new Set<number>();\n\n  private _updateContents = debounce(async () => {\n    const { nvim } = workspace;\n    const buffers = await nvim.buffers;\n    const matchedBuffers = buffers.filter((buffer) =>\n      this._bufferIds.has(buffer.id),\n    );\n    this._bufferIds.clear();\n    for (const buffer of matchedBuffers) {\n      const key = this._connectedBuffers[buffer.id];\n      if (!key) continue;\n      const lines = await buffer.getLines();\n      await this.setContent(key, lines.join('\\n'));\n    }\n    this.logger.info('Content updated');\n  }, 500);\n\n  handleTextChange = (bufferId: number) => {\n    if (!this._connectedBuffers[bufferId]) return;\n    this.logger.info(`Buffer ${bufferId}: text change`);\n    this._bufferIds.add(bufferId);\n    this._updateContents();\n  };\n\n  handleCursorChange = debounce(async () => {\n    const { nvim } = workspace;\n    const buffer = await nvim.buffer;\n    const key = this._connectedBuffers[buffer.id];\n    if (!key) return;\n    this.logger.info('Cursor change:', events.cursor.lnum);\n    await this._send('setCursor', { key, line: events.cursor.lnum - 1 });\n  }, 300);\n}\n\nexport function activate(context: ExtensionContext) {\n  // const config = workspace.getConfiguration('markmap');\n  const { logger, storagePath } = context;\n  const loading = (async () => {\n    logger.info('Initialize bridge...');\n    const bridge = new CocMarkmapBridge(logger);\n    await bridge.initialize(storagePath);\n    logger.info('Bridge loaded');\n    return bridge;\n  })();\n\n  context.subscriptions.push(\n    workspace.registerKeymap(\n      ['n'],\n      'markmap-create',\n      async () => {\n        const bridge = await loading;\n        await bridge.createMarkmap();\n      },\n      { sync: false },\n    ),\n  );\n\n  context.subscriptions.push(\n    commands.registerCommand('markmap.create', async (...args: string[]) => {\n      const options = {\n        offline: args.includes('--offline'),\n      };\n      const bridge = await loading;\n      await bridge.createMarkmap(options);\n    }),\n  );\n\n  context.subscriptions.push(\n    commands.registerCommand('markmap.watch', async () => {\n      const bridge = await loading;\n      await bridge.connectBuffer();\n    }),\n  );\n\n  context.subscriptions.push(\n    commands.registerCommand('markmap.unwatch', async () => {\n      const bridge = await loading;\n      await bridge.disconnectBuffer();\n    }),\n  );\n\n  context.subscriptions.push(\n    commands.registerCommand('markmap.stop', async () => {\n      const bridge = await loading;\n      await bridge.stopServer();\n    }),\n  );\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"Node\",\n    \"outDir\": \"dist\",\n    \"allowSyntheticDefaultImports\": true,\n    \"esModuleInterop\": true,\n    \"strictNullChecks\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\n    \"src/**/*\"\n  ]\n}\n"
  }
]