keybindings.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import { openSearchPanel } from '@codemirror/search'
  2. import {
  3. Compartment,
  4. EditorSelection,
  5. Prec,
  6. TransactionSpec,
  7. } from '@codemirror/state'
  8. import { EmacsHandler } from '@replit/codemirror-emacs'
  9. import { CodeMirror } from '@replit/codemirror-vim'
  10. import { foldCode, toggleFold, unfoldCode } from '@codemirror/language'
  11. const hasNonEmptySelection = (cm: CodeMirror): boolean => {
  12. const selections = cm.getSelections()
  13. return selections.some(selection => selection.length)
  14. }
  15. let customisedVim = false
  16. const customiseVimOnce = (Vim: any, CodeMirror: any) => {
  17. if (customisedVim) {
  18. return
  19. }
  20. // Allow copy via Ctrl-C in insert mode
  21. Vim.unmap('<C-c>', 'insert')
  22. Vim.defineAction(
  23. 'insertModeCtrlC',
  24. (cm: CodeMirror, actionArgs: object, state: any) => {
  25. if (hasNonEmptySelection(cm)) {
  26. navigator.clipboard.writeText(cm.getSelection())
  27. cm.setSelection(cm.getCursor(), cm.getCursor())
  28. } else {
  29. Vim.exitInsertMode(cm)
  30. }
  31. }
  32. )
  33. // Overwrite the moveByCharacters command with a decoration-aware version
  34. Vim.defineMotion(
  35. 'moveByCharacters',
  36. function (
  37. cm: CodeMirror,
  38. head: { line: number; ch: number },
  39. motionArgs: Record<string, unknown>
  40. ) {
  41. const { cm6: view } = cm
  42. const repeat = Math.min(Number(motionArgs.repeat), view.state.doc.length)
  43. const forward = Boolean(motionArgs.forward)
  44. // head.line is 0-indexed
  45. const startLine = view.state.doc.line(head.line + 1)
  46. let cursor = EditorSelection.cursor(startLine.from + head.ch)
  47. for (let i = 0; i < repeat; ++i) {
  48. cursor = view.moveByChar(cursor, forward)
  49. }
  50. const finishLine = view.state.doc.lineAt(cursor.head)
  51. return new CodeMirror.Pos(
  52. finishLine.number - 1,
  53. cursor.head - finishLine.from
  54. )
  55. }
  56. )
  57. Vim.mapCommand('<C-c>', 'action', 'insertModeCtrlC', undefined, {
  58. context: 'insert',
  59. })
  60. // Code folding commands
  61. Vim.defineAction('toggleFold', function (cm: CodeMirror) {
  62. toggleFold(cm.cm6)
  63. })
  64. Vim.mapCommand('za', 'action', 'toggleFold')
  65. Vim.defineAction('foldCode', function (cm: CodeMirror) {
  66. foldCode(cm.cm6)
  67. })
  68. Vim.mapCommand('zc', 'action', 'foldCode')
  69. Vim.defineAction('unfoldCode', function (cm: CodeMirror) {
  70. unfoldCode(cm.cm6)
  71. })
  72. Vim.mapCommand('zo', 'action', 'unfoldCode')
  73. // Make the Vim 'write' command start a compile
  74. CodeMirror.commands.save = () => {
  75. window.dispatchEvent(new Event('pdf:recompile'))
  76. }
  77. customisedVim = true
  78. }
  79. // Used to ensure that only one listener is active
  80. let emacsSearchCloseListener: (() => void) | undefined
  81. let customisedEmacs = false
  82. const customiseEmacsOnce = () => {
  83. if (customisedEmacs) {
  84. return
  85. }
  86. customisedEmacs = true
  87. const jumpToLastMark = (handler: EmacsHandler) => {
  88. const mark = handler.popEmacsMark()
  89. if (!mark || !mark.length) {
  90. return
  91. }
  92. let selection = null
  93. if (mark.length >= 2) {
  94. selection = EditorSelection.range(mark[0], mark[1])
  95. } else {
  96. selection = EditorSelection.cursor(mark[0])
  97. }
  98. handler.view.dispatch({ selection, scrollIntoView: true })
  99. }
  100. EmacsHandler.addCommands({
  101. openSearch(handler: EmacsHandler) {
  102. const mark = handler.view.state.selection.main
  103. handler.pushEmacsMark([mark.anchor, mark.head])
  104. openSearchPanel(handler.view)
  105. if (emacsSearchCloseListener) {
  106. document.removeEventListener(
  107. 'cm:emacs-close-search-panel',
  108. emacsSearchCloseListener
  109. )
  110. }
  111. emacsSearchCloseListener = () => {
  112. jumpToLastMark(handler)
  113. }
  114. document.addEventListener(
  115. 'cm:emacs-close-search-panel',
  116. emacsSearchCloseListener
  117. )
  118. },
  119. })
  120. EmacsHandler.bindKey('C-s', 'openSearch')
  121. EmacsHandler.bindKey('C-r', 'openSearch')
  122. }
  123. const options = [
  124. {
  125. name: 'default',
  126. load: async () => {
  127. // TODO: load default keybindings?
  128. return []
  129. },
  130. },
  131. {
  132. name: 'vim',
  133. load: () =>
  134. import('@replit/codemirror-vim').then(m => {
  135. customiseVimOnce(m.Vim, m.CodeMirror)
  136. return m.vim()
  137. }),
  138. },
  139. {
  140. name: 'emacs',
  141. load: () =>
  142. import('@replit/codemirror-emacs').then(m => {
  143. customiseEmacsOnce()
  144. return m.emacs()
  145. }),
  146. },
  147. ]
  148. const keybindingsConf = new Compartment()
  149. export const keybindings = () => {
  150. return keybindingsConf.of(Prec.highest([]))
  151. }
  152. export const setKeybindings = async (
  153. selectedKeybindings = 'default'
  154. ): Promise<TransactionSpec> => {
  155. const selectedOption = options.find(
  156. option => option.name === selectedKeybindings
  157. )
  158. if (!selectedOption) {
  159. throw new Error(`No key bindings found with name ${selectedKeybindings}`)
  160. }
  161. const support = await selectedOption.load()
  162. return {
  163. // NOTE: use Prec.highest as this keybinding must be above the default keymap(s)
  164. effects: keybindingsConf.reconfigure(Prec.highest(support)),
  165. }
  166. }