| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434 |
- // Ref: https://github.com/replit/codemirror-vim/blob/35e3a99fb0225eb7590c6dec02406df804e05422/src/index.ts
- /* eslint-disable */
- // @ts-nocheck
- import { initVim } from "./vim";
- import { CodeMirror } from "./cm_adapter";
- import { BlockCursorPlugin, hideNativeSelection } from "./block-cursor";
- import {
- Extension,
- StateField,
- StateEffect,
- RangeSetBuilder,
- } from "@codemirror/state";
- import {
- ViewPlugin,
- PluginValue,
- ViewUpdate,
- Decoration,
- EditorView,
- showPanel,
- Panel,
- } from "@codemirror/view";
- import { setSearchQuery } from "@codemirror/search";
- var FIREFOX_LINUX = typeof navigator != "undefined"
- && /linux/i.test(navigator.platform)
- && / Gecko\/\d+/.exec(navigator.userAgent);
- const Vim = initVim(CodeMirror);
- const HighlightMargin = 250;
- const vimStyle = EditorView.baseTheme({
- ".cm-vimMode .cm-cursorLayer:not(.cm-vimCursorLayer)": {
- display: "none",
- },
- ".cm-vim-panel": {
- padding: "0px 10px",
- fontFamily: "monospace",
- minHeight: "1.3em",
- },
- ".cm-vim-panel input": {
- border: "none",
- outline: "none",
- backgroundColor: "inherit",
- },
- "&light .cm-searchMatch": { backgroundColor: "#ffff0054" },
- "&dark .cm-searchMatch": { backgroundColor: "#00ffff8a" },
- });
- type EditorViewExtended = EditorView & { cm: CodeMirror };
- const vimPlugin = ViewPlugin.fromClass(
- class implements PluginValue {
- private dom: HTMLElement;
- private statusButton: HTMLElement;
- public view: EditorViewExtended;
- public cm: CodeMirror;
- public status = "";
- blockCursor: BlockCursorPlugin;
- constructor(view: EditorView) {
- this.view = view as EditorViewExtended;
- const cm = (this.cm = new CodeMirror(view));
- Vim.enterVimMode(this.cm);
- this.view.cm = this.cm;
- this.cm.state.vimPlugin = this;
- this.blockCursor = new BlockCursorPlugin(view, cm);
- this.updateClass();
- this.cm.on("vim-command-done", () => {
- if (cm.state.vim) cm.state.vim.status = "";
- this.blockCursor.scheduleRedraw();
- this.updateStatus();
- });
- this.cm.on("vim-mode-change", (e: any) => {
- if (!cm.state.vim) return;
- cm.state.vim.mode = e.mode;
- if (e.subMode) {
- cm.state.vim.mode += " block";
- }
- cm.state.vim.status = "";
- this.blockCursor.scheduleRedraw();
- this.updateClass();
- this.updateStatus();
- });
- this.cm.on("dialog", () => {
- if (this.cm.state.statusbar) {
- this.updateStatus();
- } else {
- view.dispatch({
- effects: showVimPanel.of(!!this.cm.state.dialog),
- });
- }
- });
- this.dom = document.createElement("span");
- this.dom.style.cssText = "position: absolute; right: 10px; top: 1px";
- this.statusButton = document.createElement("span");
- this.statusButton.onclick = (e) => {
- Vim.handleKey(this.cm, "<Esc>", "user");
- this.cm.focus();
- };
- this.statusButton.style.cssText = "cursor: pointer";
- }
- update(update: ViewUpdate) {
- if ((update.viewportChanged || update.docChanged) && this.query) {
- this.highlight(this.query);
- }
- if (update.docChanged) {
- this.cm.onChange(update);
- }
- if (update.selectionSet) {
- this.cm.onSelectionChange();
- }
- if (update.viewportChanged) {
- // scroll
- }
- if (this.cm.curOp && !this.cm.curOp.isVimOp) {
- this.cm.onBeforeEndOperation();
- }
- if (update.transactions) {
- for (let tr of update.transactions)
- for (let effect of tr.effects) {
- if (effect.is(setSearchQuery)) {
- let forVim = (effect.value as any)?.forVim;
- if (!forVim) {
- this.highlight(null);
- } else {
- let query = (effect.value as any).create();
- this.highlight(query);
- }
- }
- }
- }
- this.blockCursor.update(update);
- }
- updateClass() {
- const state = this.cm.state;
- if (!state.vim || (state.vim.insertMode && !state.overwrite))
- this.view.scrollDOM.classList.remove("cm-vimMode");
- else this.view.scrollDOM.classList.add("cm-vimMode");
- }
- updateStatus() {
- let dom = this.cm.state.statusbar;
- let vim = this.cm.state.vim;
- if (!dom || !vim) return;
- let dialog = this.cm.state.dialog;
- if (dialog) {
- if (dialog.parentElement != dom) {
- dom.textContent = "";
- dom.appendChild(dialog);
- }
- } else {
- dom.textContent = ""
- var status = (vim.mode || "normal").toUpperCase();
- if (vim.insertModeReturn) status += "(C-O)"
- this.statusButton.textContent = `--${status}--`;
- dom.appendChild(this.statusButton);
- }
- this.dom.textContent = vim.status;
- dom.appendChild(this.dom);
- }
- destroy() {
- Vim.leaveVimMode(this.cm);
- this.updateClass();
- this.blockCursor.destroy();
- delete (this.view as any).cm;
- }
- highlight(query: any) {
- this.query = query;
- if (!query) return (this.decorations = Decoration.none);
- let { view } = this;
- let builder = new RangeSetBuilder<Decoration>();
- for (
- let i = 0, ranges = view.visibleRanges, l = ranges.length;
- i < l;
- i++
- ) {
- let { from, to } = ranges[i];
- while (i < l - 1 && to > ranges[i + 1].from - 2 * HighlightMargin)
- to = ranges[++i].to;
- query.highlight(
- view.state,
- from,
- to,
- (from: number, to: number) => {
- builder.add(from, to, matchMark);
- }
- );
- }
- return (this.decorations = builder.finish());
- }
- query = null;
- decorations = Decoration.none;
- waitForCopy = false;
- handleKey(e: KeyboardEvent, view: EditorView) {
- const cm = this.cm;
- let vim = cm.state.vim;
- if (!vim) return;
- const key = Vim.vimKeyFromEvent(e, vim);
- if (!key) return;
- // clear search highlight
- if (
- key == "<Esc>" &&
- !vim.insertMode &&
- !vim.visualMode &&
- this.query /* && !cm.inMultiSelectMode*/
- ) {
- const searchState = vim.searchState_
- if (searchState) {
- cm.removeOverlay(searchState.getOverlay())
- searchState.setOverlay(null);
- }
- }
- let isCopy = key === "<C-c>" && !CodeMirror.isMac;
- if (isCopy && cm.somethingSelected()) {
- this.waitForCopy = true;
- return true;
- }
- vim.status = (vim.status || "") + key;
- let result = Vim.multiSelectHandleKey(cm, key, "user");
- vim = Vim.maybeInitVimState_(cm); // the object can change if there is an exception in handleKey
- // insert mode
- if (!result && vim.insertMode && cm.state.overwrite) {
- if (e.key && e.key.length == 1 && !/\n/.test(e.key)) {
- result = true;
- cm.overWriteSelection(e.key);
- } else if (e.key == "Backspace") {
- result = true;
- CodeMirror.commands.cursorCharLeft(cm);
- }
- }
- if (result) {
- CodeMirror.signal(this.cm, 'vim-keypress', key);
- e.preventDefault();
- e.stopPropagation();
- this.blockCursor.scheduleRedraw();
- }
- this.updateStatus();
- return !!result;
- }
- lastKeydown = ''
- useNextTextInput = false
- compositionText = ''
- },
- {
- eventHandlers: {
- copy: function(e: ClipboardEvent, view: EditorView) {
- if (!this.waitForCopy) return;
- this.waitForCopy = false;
- Promise.resolve().then(() => {
- var cm = this.cm;
- var vim = cm.state.vim;
- if (!vim) return;
- if (vim.insertMode) {
- cm.setSelection(cm.getCursor(), cm.getCursor());
- } else {
- cm.operation(() => {
- if (cm.curOp) cm.curOp.isVimOp = true;
- Vim.handleKey(cm, '<Esc>', 'user');
- });
- }
- });
- },
- compositionstart: function(e: Event, view: EditorView) {
- this.useNextTextInput = true;
- },
- keypress: function(e: KeyboardEvent, view: EditorView) {
- if (this.lastKeydown == "Dead")
- this.handleKey(e, view);
- },
- keydown: function(e: KeyboardEvent, view: EditorView) {
- this.lastKeydown = e.key;
- if (
- this.lastKeydown == "Unidentified"
- || this.lastKeydown == "Process"
- || this.lastKeydown == "Dead"
- ) {
- this.useNextTextInput = true;
- } else {
- this.useNextTextInput = false;
- this.handleKey(e, view);
- }
- },
- },
- provide: () => {
- return [
- EditorView.inputHandler.of((view, from, to, text) => {
- var cm = getCM(view);
- if (!cm) return false;
- var vim = cm.state?.vim;
- var vimPlugin = cm.state.vimPlugin;
- if (vim && !vim.insertMode && !cm.curOp?.isVimOp) {
- if (text === "\0\0") {
- return true;
- }
- if (text.length == 1 && vimPlugin.useNextTextInput) {
- if (vim.expectLiteralNext && view.composing) {
- vimPlugin.compositionText = text;
- return false
- }
- if (vimPlugin.compositionText) {
- var toRemove = vimPlugin.compositionText;
- vimPlugin.compositionText = '';
- var head = view.state.selection.main.head
- var textInDoc = view.state.sliceDoc(head - toRemove.length, head);
- if (toRemove === textInDoc) {
- var pos = cm.getCursor();
- cm.replaceRange('', cm.posFromIndex(head - toRemove.length), pos);
- }
- }
- vimPlugin.handleKey({
- key: text,
- preventDefault: ()=>{},
- stopPropagation: ()=>{}
- });
- forceEndComposition(view);
- return true;
- }
- }
- return false;
- })
- ]
- },
- decorations: (v) => v.decorations,
- }
- );
- /**
- * removes contenteditable element and adds it back to end
- * IME composition in normal mode
- * this method works on all browsers except for Firefox on Linux
- * where we need to reset textContent of editor
- * (which doesn't work on other browsers)
- */
- function forceEndComposition(view: EditorView) {
- var parent = view.scrollDOM.parentElement;
- if (!parent) return;
- if (FIREFOX_LINUX) {
- view.contentDOM.textContent = "\0\0";
- view.contentDOM.dispatchEvent(new CustomEvent("compositionend"));
- return;
- }
- var sibling = view.scrollDOM.nextSibling;
- var selection = window.getSelection();
- var savedSelection = selection && {
- anchorNode: selection.anchorNode,
- anchorOffset: selection.anchorOffset,
- focusNode: selection.focusNode,
- focusOffset: selection.focusOffset
- };
- view.scrollDOM.remove();
- parent.insertBefore(view.scrollDOM, sibling);
- try {
- if (savedSelection && selection) {
- selection.setPosition(savedSelection.anchorNode, savedSelection.anchorOffset);
- if (savedSelection.focusNode) {
- selection.extend(savedSelection.focusNode, savedSelection.focusOffset);
- }
- }
- } catch(e) {
- console.error(e);
- }
- view.focus();
- view.contentDOM.dispatchEvent(new CustomEvent("compositionend"));
- }
- const matchMark = Decoration.mark({ class: "cm-searchMatch" });
- const showVimPanel = StateEffect.define<boolean>();
- const vimPanelState = StateField.define<boolean>({
- create: () => false,
- update(value, tr) {
- for (let e of tr.effects) if (e.is(showVimPanel)) value = e.value;
- return value;
- },
- provide: (f) => {
- return showPanel.from(f, (on) => (on ? createVimPanel : null));
- },
- });
- function createVimPanel(view: EditorView) {
- let dom = document.createElement("div");
- dom.className = "cm-vim-panel";
- let cm = (view as EditorViewExtended).cm;
- if (cm.state.dialog) {
- dom.appendChild(cm.state.dialog);
- }
- return { top: false, dom };
- }
- function statusPanel(view: EditorView): Panel {
- let dom = document.createElement("div");
- dom.className = "cm-vim-panel";
- let cm = (view as EditorViewExtended).cm;
- cm.state.statusbar = dom;
- cm.state.vimPlugin.updateStatus();
- return { dom };
- }
- export function vim(options: { status?: boolean } = {}): Extension {
- return [
- vimStyle,
- vimPlugin,
- hideNativeSelection,
- options.status ? showPanel.of(statusPanel) : vimPanelState,
- ];
- }
- export { CodeMirror, Vim };
- export function getCM(view: EditorView): CodeMirror | null {
- return (view as EditorViewExtended).cm || null;
- }
|