Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Change Log

## 0.3.0

- Added single-key shortcuts when the preview is focused: <kbd>F</kbd> fit window size, <kbd>R</kbd> toggle recursively, <kbd>+</kbd>/<kbd>-</kbd> zoom in/out, <kbd>E</kbd>/<kbd>C</kbd> expand/collapse all, <kbd>T</kbd> toggle the highlighted node, <kbd>?</kbd> show shortcut help
- Click anywhere on a node to fold/unfold it, not only the circle (<kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+click inverts the toggle-recursively mode)
- Toolbar buttons are highlighted in sync with the shortcuts
- Fixed the `Toggle the active node` commands, which previously did nothing

## 0.1.1

- Use <kbd>Cmd</kbd>+click to toggle nodes recursively on macOS
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This extension integrates [markmap](https://markmap.js.org/) into VSCode.

- Preview markdown files as markmap
- Edit markdown files in a text editor and the markmap will update on the fly
- Click anywhere on a node (not only the circle) to fold/unfold it, <kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+click inverts the toggle-recursively mode
- Works offline

<img width="1014" alt="markmap" src="https://user-images.githubusercontent.com/3139113/97068999-5f9e8480-15ff-11eb-8222-43d26cecade5.png">
Expand All @@ -29,6 +30,22 @@ Open a markdown file. Find the markmap icon on the editor title-bar and click it

![title button](https://user-images.githubusercontent.com/3139113/110966366-25f0cf00-8390-11eb-9a16-3c4d66712f47.png)

### Keyboard shortcuts

When the markmap preview is focused, the following single-key shortcuts are available:

| Key | Action |
| --- | --- |
| `F` | Fit window size |
| `R` | Toggle recursively (toggling a node affects the whole subtree) |
| `+` / `=` | Zoom in |
| `-` | Zoom out |
| `E` | Expand all nodes |
| `C` | Collapse all nodes (keep the first level) |
| `T` | Toggle the highlighted node |
| `?` | Show/hide the shortcut help |
| `Esc` | Close the shortcut help |

## Configuration

### Custom CSS
Expand Down
55 changes: 55 additions & 0 deletions assets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,58 @@ body {
padding-left: 4px;
padding-right: 4px;
}

.markmap-help {
position: absolute;
top: 20px;
right: 20px;
z-index: 10;
padding: 12px 16px;
border: 1px solid rgb(0 0 0 / 10%);
border-radius: 8px;
background: rgb(255 255 255 / 80%);
color: #333;
font-size: 14px;
line-height: 1.6;
}

.markmap-help-title {
margin-bottom: 4px;
font-weight: 600;
}

.markmap-help ul {
margin: 0;
padding: 0;
list-style: none;
}

.markmap-help li {
display: flex;
justify-content: space-between;
gap: 32px;
}

.markmap-help kbd {
display: inline-block;
min-width: 1em;
padding: 0 6px;
border: 1px solid rgb(0 0 0 / 30%);
border-bottom-width: 2px;
border-radius: 4px;
background: #fff;
font-family: monospace;
text-align: center;
}

.markmap-dark .markmap-help {
border-color: rgb(255 255 255 / 15%);
background: rgb(0 0 0 / 70%);
color: #eee;
}

.markmap-dark .markmap-help kbd {
border-color: rgb(255 255 255 / 30%);
background: #333;
color: #eee;
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "markmap-vscode",
"version": "0.2.11",
"version": "0.3.0",
"description": "Visualize your markdown in VSCode",
"author": "Gerald <gera2ld@live.com>",
"license": "MIT",
Expand Down
172 changes: 169 additions & 3 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { IDeferred, defer, wrapFunction, type INode } from 'markmap-common';
import {
IDeferred,
defer,
walkTree,
wrapFunction,
type INode,
} from 'markmap-common';
import { Toolbar } from 'markmap-toolbar';
import {
defaultOptions,
Expand All @@ -16,7 +22,6 @@ let style: HTMLStyleElement;
let active:
| {
node: INode;
el: Element;
}
| undefined;
const activeNodeOptions: {
Expand Down Expand Up @@ -139,7 +144,19 @@ function initialize(mm: Markmap) {
vscode.postMessage({ type: 'setFocus', data: line });
},
true,
);
)
.on('click.toggleNode', (e, d) => {
// markmap-view only handles clicks on the circle of a node;
// make the whole node (text included) clickable
if (!d?.children?.length) return;
// the circle is handled by markmap-view itself, and links by the
// document-level click handler
if ((e.target as Element).closest('circle, a')) return;
// ctrl/cmd inverts the recursive mode, same as clicking the circle
const recursive =
mm.options.toggleRecursively !== !!(e.metaKey || e.ctrlKey);
mm.toggleNode(d, recursive);
});
});
}

Expand Down Expand Up @@ -214,6 +231,7 @@ function findActiveNode({
}

async function highlightNode(node?: INode) {
active = node && { node };
await mm.setHighlight(node);
if (!node) return;
await mm[
Expand All @@ -222,3 +240,151 @@ async function highlightNode(node?: INode) {
bottom: 80,
});
}

/**
* Keyboard shortcuts, available when the markmap preview is focused.
* Single keys only, since they have no other function in the preview.
*/
const keyHandlers: {
[key: string]: () => void;
} = {
f: () =>
whenReady(() => {
mm.fit();
pulseToolbar('fit');
}),
r: () => toggleRecursively(),
'+': () => rescaleByKey(1.25),
'=': () => rescaleByKey(1.25),
'-': () => rescaleByKey(0.8),
e: () => setFoldAll(0),
c: () => setFoldAll(1),
t: () => {
if (active?.node) mm.toggleNode(active.node, isToggleRecursively());
},
'?': () => toggleHelp(),
};

document.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey || e.altKey) return;
const el = e.target as HTMLElement | null;
if (el?.closest?.('input, textarea, select, [contenteditable]')) return;
if (e.key === 'Escape') {
hideHelp();
return;
}
const handler = keyHandlers[e.key.toLowerCase()];
if (!handler) return;
e.preventDefault();
handler();
});

function whenReady(fn: () => void | Promise<void>) {
return loading?.promise.then(fn);
}

function isToggleRecursively() {
return mm.options.toggleRecursively;
}

/**
* Toggle `toggleRecursively` mode, same as the `recurse` toolbar button,
* and sync the button's active state so the current mode is visible.
*/
function toggleRecursively() {
const enabled = !mm.options.toggleRecursively;
mm.setOptions({ toggleRecursively: enabled });
getToolbarItemEl('recurse')?.classList.toggle('active', enabled);
}

/**
* Get the rendered DOM element of a toolbar item.
* `registry[id].content` holds a virtual node instead of the rendered
* element, so look the button up by its index in the rendered items.
*/
function getToolbarItemEl(id: string) {
const index = toolbar.items.indexOf(id);
if (index < 0) return;
return toolbar.el.querySelectorAll<HTMLElement>('.mm-toolbar-item')[index];
}

/**
* Briefly highlight a toolbar item, as visual feedback for shortcuts
* mapped to one-shot actions like `fit` and zooming.
*/
function pulseToolbar(id: string) {
const el = getToolbarItemEl(id);
if (!el) return;
el.classList.add('active');
setTimeout(() => {
el.classList.remove('active');
}, 200);
}

function rescaleByKey(ratio: number) {
whenReady(() => {
mm.rescale(ratio);
pulseToolbar(ratio > 1 ? 'zoomIn' : 'zoomOut');
});
}

async function setFoldAll(fold: number) {
await whenReady();
if (!root) return;
let isRoot = true;
walkTree(root, (node, next) => {
// never fold the root node, otherwise nothing is visible
if (!isRoot && node.children?.length) {
node.payload = { ...node.payload, fold };
}
isRoot = false;
next();
});
await mm.renderData();
await mm.fit();
}

let helpEl: HTMLDivElement | undefined;

function toggleHelp() {
if (helpEl?.isConnected) {
hideHelp();
return;
}
helpEl = document.createElement('div');
helpEl.className = 'markmap-help';
const title = document.createElement('div');
title.className = 'markmap-help-title';
title.textContent = 'Keyboard Shortcuts';
const list = document.createElement('ul');
const items: [string[], string][] = [
[['F'], 'Fit window size'],
[['R'], 'Toggle recursively'],
[['+', '='], 'Zoom in'],
[['-'], 'Zoom out'],
[['E'], 'Expand all'],
[['C'], 'Collapse all'],
[['T'], 'Toggle the highlighted node'],
[['?'], 'Show/hide this help'],
];
items.forEach(([keys, label]) => {
const li = document.createElement('li');
const kbdContainer = document.createElement('span');
keys.forEach((key, i) => {
if (i) kbdContainer.append(' / ');
const kbd = document.createElement('kbd');
kbd.textContent = key;
kbdContainer.append(kbd);
});
const text = document.createElement('span');
text.textContent = label;
li.append(kbdContainer, text);
list.append(li);
});
helpEl.append(title, list);
document.body.append(helpEl);
}

function hideHelp() {
helpEl?.remove();
}