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

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.
## 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/**/*"
]
}