index.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. // Ref: https://github.com/replit/codemirror-vim/blob/35e3a99fb0225eb7590c6dec02406df804e05422/src/index.ts
  2. /* eslint-disable */
  3. // @ts-nocheck
  4. import { initVim } from "./vim";
  5. import { CodeMirror } from "./cm_adapter";
  6. import { BlockCursorPlugin, hideNativeSelection } from "./block-cursor";
  7. import {
  8. Extension,
  9. StateField,
  10. StateEffect,
  11. RangeSetBuilder,
  12. } from "@codemirror/state";
  13. import {
  14. ViewPlugin,
  15. PluginValue,
  16. ViewUpdate,
  17. Decoration,
  18. EditorView,
  19. showPanel,
  20. Panel,
  21. } from "@codemirror/view";
  22. import { setSearchQuery } from "@codemirror/search";
  23. var FIREFOX_LINUX = typeof navigator != "undefined"
  24. && /linux/i.test(navigator.platform)
  25. && / Gecko\/\d+/.exec(navigator.userAgent);
  26. const Vim = initVim(CodeMirror);
  27. const HighlightMargin = 250;
  28. const vimStyle = EditorView.baseTheme({
  29. ".cm-vimMode .cm-cursorLayer:not(.cm-vimCursorLayer)": {
  30. display: "none",
  31. },
  32. ".cm-vim-panel": {
  33. padding: "0px 10px",
  34. fontFamily: "monospace",
  35. minHeight: "1.3em",
  36. },
  37. ".cm-vim-panel input": {
  38. border: "none",
  39. outline: "none",
  40. backgroundColor: "inherit",
  41. },
  42. "&light .cm-searchMatch": { backgroundColor: "#ffff0054" },
  43. "&dark .cm-searchMatch": { backgroundColor: "#00ffff8a" },
  44. });
  45. type EditorViewExtended = EditorView & { cm: CodeMirror };
  46. const vimPlugin = ViewPlugin.fromClass(
  47. class implements PluginValue {
  48. private dom: HTMLElement;
  49. private statusButton: HTMLElement;
  50. public view: EditorViewExtended;
  51. public cm: CodeMirror;
  52. public status = "";
  53. blockCursor: BlockCursorPlugin;
  54. constructor(view: EditorView) {
  55. this.view = view as EditorViewExtended;
  56. const cm = (this.cm = new CodeMirror(view));
  57. Vim.enterVimMode(this.cm);
  58. this.view.cm = this.cm;
  59. this.cm.state.vimPlugin = this;
  60. this.blockCursor = new BlockCursorPlugin(view, cm);
  61. this.updateClass();
  62. this.cm.on("vim-command-done", () => {
  63. if (cm.state.vim) cm.state.vim.status = "";
  64. this.blockCursor.scheduleRedraw();
  65. this.updateStatus();
  66. });
  67. this.cm.on("vim-mode-change", (e: any) => {
  68. if (!cm.state.vim) return;
  69. cm.state.vim.mode = e.mode;
  70. if (e.subMode) {
  71. cm.state.vim.mode += " block";
  72. }
  73. cm.state.vim.status = "";
  74. this.blockCursor.scheduleRedraw();
  75. this.updateClass();
  76. this.updateStatus();
  77. });
  78. this.cm.on("dialog", () => {
  79. if (this.cm.state.statusbar) {
  80. this.updateStatus();
  81. } else {
  82. view.dispatch({
  83. effects: showVimPanel.of(!!this.cm.state.dialog),
  84. });
  85. }
  86. });
  87. this.dom = document.createElement("span");
  88. this.dom.style.cssText = "position: absolute; right: 10px; top: 1px";
  89. this.statusButton = document.createElement("span");
  90. this.statusButton.onclick = (e) => {
  91. Vim.handleKey(this.cm, "<Esc>", "user");
  92. this.cm.focus();
  93. };
  94. this.statusButton.style.cssText = "cursor: pointer";
  95. }
  96. update(update: ViewUpdate) {
  97. if ((update.viewportChanged || update.docChanged) && this.query) {
  98. this.highlight(this.query);
  99. }
  100. if (update.docChanged) {
  101. this.cm.onChange(update);
  102. }
  103. if (update.selectionSet) {
  104. this.cm.onSelectionChange();
  105. }
  106. if (update.viewportChanged) {
  107. // scroll
  108. }
  109. if (this.cm.curOp && !this.cm.curOp.isVimOp) {
  110. this.cm.onBeforeEndOperation();
  111. }
  112. if (update.transactions) {
  113. for (let tr of update.transactions)
  114. for (let effect of tr.effects) {
  115. if (effect.is(setSearchQuery)) {
  116. let forVim = (effect.value as any)?.forVim;
  117. if (!forVim) {
  118. this.highlight(null);
  119. } else {
  120. let query = (effect.value as any).create();
  121. this.highlight(query);
  122. }
  123. }
  124. }
  125. }
  126. this.blockCursor.update(update);
  127. }
  128. updateClass() {
  129. const state = this.cm.state;
  130. if (!state.vim || (state.vim.insertMode && !state.overwrite))
  131. this.view.scrollDOM.classList.remove("cm-vimMode");
  132. else this.view.scrollDOM.classList.add("cm-vimMode");
  133. }
  134. updateStatus() {
  135. let dom = this.cm.state.statusbar;
  136. let vim = this.cm.state.vim;
  137. if (!dom || !vim) return;
  138. let dialog = this.cm.state.dialog;
  139. if (dialog) {
  140. if (dialog.parentElement != dom) {
  141. dom.textContent = "";
  142. dom.appendChild(dialog);
  143. }
  144. } else {
  145. dom.textContent = ""
  146. var status = (vim.mode || "normal").toUpperCase();
  147. if (vim.insertModeReturn) status += "(C-O)"
  148. this.statusButton.textContent = `--${status}--`;
  149. dom.appendChild(this.statusButton);
  150. }
  151. this.dom.textContent = vim.status;
  152. dom.appendChild(this.dom);
  153. }
  154. destroy() {
  155. Vim.leaveVimMode(this.cm);
  156. this.updateClass();
  157. this.blockCursor.destroy();
  158. delete (this.view as any).cm;
  159. }
  160. highlight(query: any) {
  161. this.query = query;
  162. if (!query) return (this.decorations = Decoration.none);
  163. let { view } = this;
  164. let builder = new RangeSetBuilder<Decoration>();
  165. for (
  166. let i = 0, ranges = view.visibleRanges, l = ranges.length;
  167. i < l;
  168. i++
  169. ) {
  170. let { from, to } = ranges[i];
  171. while (i < l - 1 && to > ranges[i + 1].from - 2 * HighlightMargin)
  172. to = ranges[++i].to;
  173. query.highlight(
  174. view.state,
  175. from,
  176. to,
  177. (from: number, to: number) => {
  178. builder.add(from, to, matchMark);
  179. }
  180. );
  181. }
  182. return (this.decorations = builder.finish());
  183. }
  184. query = null;
  185. decorations = Decoration.none;
  186. waitForCopy = false;
  187. handleKey(e: KeyboardEvent, view: EditorView) {
  188. const cm = this.cm;
  189. let vim = cm.state.vim;
  190. if (!vim) return;
  191. const key = Vim.vimKeyFromEvent(e, vim);
  192. if (!key) return;
  193. // clear search highlight
  194. if (
  195. key == "<Esc>" &&
  196. !vim.insertMode &&
  197. !vim.visualMode &&
  198. this.query /* && !cm.inMultiSelectMode*/
  199. ) {
  200. const searchState = vim.searchState_
  201. if (searchState) {
  202. cm.removeOverlay(searchState.getOverlay())
  203. searchState.setOverlay(null);
  204. }
  205. }
  206. let isCopy = key === "<C-c>" && !CodeMirror.isMac;
  207. if (isCopy && cm.somethingSelected()) {
  208. this.waitForCopy = true;
  209. return true;
  210. }
  211. vim.status = (vim.status || "") + key;
  212. let result = Vim.multiSelectHandleKey(cm, key, "user");
  213. vim = Vim.maybeInitVimState_(cm); // the object can change if there is an exception in handleKey
  214. // insert mode
  215. if (!result && vim.insertMode && cm.state.overwrite) {
  216. if (e.key && e.key.length == 1 && !/\n/.test(e.key)) {
  217. result = true;
  218. cm.overWriteSelection(e.key);
  219. } else if (e.key == "Backspace") {
  220. result = true;
  221. CodeMirror.commands.cursorCharLeft(cm);
  222. }
  223. }
  224. if (result) {
  225. CodeMirror.signal(this.cm, 'vim-keypress', key);
  226. e.preventDefault();
  227. e.stopPropagation();
  228. this.blockCursor.scheduleRedraw();
  229. }
  230. this.updateStatus();
  231. return !!result;
  232. }
  233. lastKeydown = ''
  234. useNextTextInput = false
  235. compositionText = ''
  236. },
  237. {
  238. eventHandlers: {
  239. copy: function(e: ClipboardEvent, view: EditorView) {
  240. if (!this.waitForCopy) return;
  241. this.waitForCopy = false;
  242. Promise.resolve().then(() => {
  243. var cm = this.cm;
  244. var vim = cm.state.vim;
  245. if (!vim) return;
  246. if (vim.insertMode) {
  247. cm.setSelection(cm.getCursor(), cm.getCursor());
  248. } else {
  249. cm.operation(() => {
  250. if (cm.curOp) cm.curOp.isVimOp = true;
  251. Vim.handleKey(cm, '<Esc>', 'user');
  252. });
  253. }
  254. });
  255. },
  256. compositionstart: function(e: Event, view: EditorView) {
  257. this.useNextTextInput = true;
  258. },
  259. keypress: function(e: KeyboardEvent, view: EditorView) {
  260. if (this.lastKeydown == "Dead")
  261. this.handleKey(e, view);
  262. },
  263. keydown: function(e: KeyboardEvent, view: EditorView) {
  264. this.lastKeydown = e.key;
  265. if (
  266. this.lastKeydown == "Unidentified"
  267. || this.lastKeydown == "Process"
  268. || this.lastKeydown == "Dead"
  269. ) {
  270. this.useNextTextInput = true;
  271. } else {
  272. this.useNextTextInput = false;
  273. this.handleKey(e, view);
  274. }
  275. },
  276. },
  277. provide: () => {
  278. return [
  279. EditorView.inputHandler.of((view, from, to, text) => {
  280. var cm = getCM(view);
  281. if (!cm) return false;
  282. var vim = cm.state?.vim;
  283. var vimPlugin = cm.state.vimPlugin;
  284. if (vim && !vim.insertMode && !cm.curOp?.isVimOp) {
  285. if (text === "\0\0") {
  286. return true;
  287. }
  288. if (text.length == 1 && vimPlugin.useNextTextInput) {
  289. if (vim.expectLiteralNext && view.composing) {
  290. vimPlugin.compositionText = text;
  291. return false
  292. }
  293. if (vimPlugin.compositionText) {
  294. var toRemove = vimPlugin.compositionText;
  295. vimPlugin.compositionText = '';
  296. var head = view.state.selection.main.head
  297. var textInDoc = view.state.sliceDoc(head - toRemove.length, head);
  298. if (toRemove === textInDoc) {
  299. var pos = cm.getCursor();
  300. cm.replaceRange('', cm.posFromIndex(head - toRemove.length), pos);
  301. }
  302. }
  303. vimPlugin.handleKey({
  304. key: text,
  305. preventDefault: ()=>{},
  306. stopPropagation: ()=>{}
  307. });
  308. forceEndComposition(view);
  309. return true;
  310. }
  311. }
  312. return false;
  313. })
  314. ]
  315. },
  316. decorations: (v) => v.decorations,
  317. }
  318. );
  319. /**
  320. * removes contenteditable element and adds it back to end
  321. * IME composition in normal mode
  322. * this method works on all browsers except for Firefox on Linux
  323. * where we need to reset textContent of editor
  324. * (which doesn't work on other browsers)
  325. */
  326. function forceEndComposition(view: EditorView) {
  327. var parent = view.scrollDOM.parentElement;
  328. if (!parent) return;
  329. if (FIREFOX_LINUX) {
  330. view.contentDOM.textContent = "\0\0";
  331. view.contentDOM.dispatchEvent(new CustomEvent("compositionend"));
  332. return;
  333. }
  334. var sibling = view.scrollDOM.nextSibling;
  335. var selection = window.getSelection();
  336. var savedSelection = selection && {
  337. anchorNode: selection.anchorNode,
  338. anchorOffset: selection.anchorOffset,
  339. focusNode: selection.focusNode,
  340. focusOffset: selection.focusOffset
  341. };
  342. view.scrollDOM.remove();
  343. parent.insertBefore(view.scrollDOM, sibling);
  344. try {
  345. if (savedSelection && selection) {
  346. selection.setPosition(savedSelection.anchorNode, savedSelection.anchorOffset);
  347. if (savedSelection.focusNode) {
  348. selection.extend(savedSelection.focusNode, savedSelection.focusOffset);
  349. }
  350. }
  351. } catch(e) {
  352. console.error(e);
  353. }
  354. view.focus();
  355. view.contentDOM.dispatchEvent(new CustomEvent("compositionend"));
  356. }
  357. const matchMark = Decoration.mark({ class: "cm-searchMatch" });
  358. const showVimPanel = StateEffect.define<boolean>();
  359. const vimPanelState = StateField.define<boolean>({
  360. create: () => false,
  361. update(value, tr) {
  362. for (let e of tr.effects) if (e.is(showVimPanel)) value = e.value;
  363. return value;
  364. },
  365. provide: (f) => {
  366. return showPanel.from(f, (on) => (on ? createVimPanel : null));
  367. },
  368. });
  369. function createVimPanel(view: EditorView) {
  370. let dom = document.createElement("div");
  371. dom.className = "cm-vim-panel";
  372. let cm = (view as EditorViewExtended).cm;
  373. if (cm.state.dialog) {
  374. dom.appendChild(cm.state.dialog);
  375. }
  376. return { top: false, dom };
  377. }
  378. function statusPanel(view: EditorView): Panel {
  379. let dom = document.createElement("div");
  380. dom.className = "cm-vim-panel";
  381. let cm = (view as EditorViewExtended).cm;
  382. cm.state.statusbar = dom;
  383. cm.state.vimPlugin.updateStatus();
  384. return { dom };
  385. }
  386. export function vim(options: { status?: boolean } = {}): Extension {
  387. return [
  388. vimStyle,
  389. vimPlugin,
  390. hideNativeSelection,
  391. options.status ? showPanel.of(statusPanel) : vimPanelState,
  392. ];
  393. }
  394. export { CodeMirror, Vim };
  395. export function getCM(view: EditorView): CodeMirror | null {
  396. return (view as EditorViewExtended).cm || null;
  397. }