keybindings.ts 5.4 KB

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