Improve Markdown mode.

Replace [ ] and [x] with checkboxes in Markdown todo lists.
Add Markdown LanguageSupport extension in order to automatically add/remove new list items when pressing enter/backspace in a list in Markdown mode.
This commit is contained in:
Jonatan Heyman 2023-03-07 22:32:50 +01:00
parent b26381164a
commit 6bc3e09826
2 changed files with 114 additions and 0 deletions

View File

@ -1,6 +1,7 @@
import { Annotation, EditorState, Compartment } from "@codemirror/state" import { Annotation, EditorState, Compartment } from "@codemirror/state"
import { EditorView, keymap, drawSelection, ViewPlugin, lineNumbers } from "@codemirror/view" import { EditorView, keymap, drawSelection, ViewPlugin, lineNumbers } from "@codemirror/view"
import { indentUnit, forceParsing, foldGutter } from "@codemirror/language" import { indentUnit, forceParsing, foldGutter } from "@codemirror/language"
import { markdown } from "@codemirror/lang-markdown"
import { heynoteLight } from "./theme/light.js" import { heynoteLight } from "./theme/light.js"
import { heynoteDark } from "./theme/dark.js" import { heynoteDark } from "./theme/dark.js"
@ -14,6 +15,7 @@ import { heynoteKeymap, emacsKeymap } from "./keymap.js"
import { heynoteCopyPaste } from "./copy-paste" import { heynoteCopyPaste } from "./copy-paste"
import { languageDetection } from "./language-detection/autodetect.js" import { languageDetection } from "./language-detection/autodetect.js"
import { autoSaveContent } from "./save.js" import { autoSaveContent } from "./save.js"
import { todoCheckboxPlugin} from "./todo-checkbox.ts"
export const LANGUAGE_SELECTOR_EVENT = "openLanguageSelector" export const LANGUAGE_SELECTOR_EVENT = "openLanguageSelector"
@ -75,6 +77,9 @@ export class HeynoteEditor {
}), }),
saveFunction ? autoSaveContent(saveFunction, 2000) : [], saveFunction ? autoSaveContent(saveFunction, 2000) : [],
todoCheckboxPlugin,
markdown(),
], ],
}) })

109
src/editor/todo-checkbox.ts Normal file
View File

@ -0,0 +1,109 @@
import { EditorView, Decoration } from "@codemirror/view"
import { syntaxTree, ensureSyntaxTree } from "@codemirror/language"
import { WidgetType } from "@codemirror/view"
import { ViewUpdate, ViewPlugin, DecorationSet } from "@codemirror/view"
class CheckboxWidget extends WidgetType {
constructor(readonly checked: boolean) { super() }
eq(other: CheckboxWidget) { return other.checked == this.checked }
toDOM() {
let wrap = document.createElement("span")
wrap.setAttribute("aria-hidden", "true")
wrap.className = "cm-taskmarker-toggle"
wrap.style.position = "relative"
// Three spaces since it's the same width as [ ] and [x]
wrap.appendChild(document.createTextNode(" "))
let box = wrap.appendChild(document.createElement("input"))
box.type = "checkbox"
box.checked = this.checked
box.style.position = "absolute"
box.style.top = "-3px"
box.style.left = "0"
return wrap
}
ignoreEvent() { return false }
}
function checkboxes(view: EditorView) {
let widgets: any = []
for (let { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({
from, to,
enter: (nodeRef) => {
// make sure we only enter markdown nodes
if (nodeRef.name == "Note") {
let langNode = nodeRef.node.firstChild?.firstChild
if (langNode) {
const language = view.state.doc.sliceString(langNode.from, langNode.to)
if (!language.startsWith("markdown")) {
return false
}
}
}
if (nodeRef.name == "TaskMarker") {
// the Markdown parser creates a TaskMarker for "- [x]", but we don't want to replace it with a
// checkbox widget, unless its followed by a space
if (view.state.doc.sliceString(nodeRef.to, nodeRef.to+1) === " ") {
let isChecked = view.state.doc.sliceString(nodeRef.from, nodeRef.to).toLowerCase() === "[x]"
let deco = Decoration.replace({
widget: new CheckboxWidget(isChecked),
inclusive: false,
})
widgets.push(deco.range(nodeRef.from, nodeRef.to))
}
}
}
})
}
return Decoration.set(widgets)
}
function toggleBoolean(view: EditorView, pos: number) {
let before = view.state.doc.sliceString(pos-3, pos).toLowerCase()
let change
if (before === "[x]") {
change = { from: pos - 3, to: pos, insert: "[ ]" }
} else if (before === "[ ]") {
change = { from: pos - 3, to: pos, insert: "[x]" }
} else {
return false
}
view.dispatch({ changes: change })
return true
}
/**
* A plugin that replaces [ ] and [x] with checkboxes to task list items in Markup mode
*/
export const todoCheckboxPlugin = [
ViewPlugin.fromClass(class {
decorations: DecorationSet
constructor(view: EditorView) {
this.decorations = checkboxes(view)
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged)
this.decorations = checkboxes(update.view)
}
}, {
decorations: v => v.decorations,
eventHandlers: {
mousedown: (e, view) => {
let target = e.target as HTMLElement
if (target.nodeName == "INPUT" && target.parentElement!.classList.contains("cm-taskmarker-toggle"))
return toggleBoolean(view, view.posAtDOM(target))
}
}
}),
]