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