Просмотр исходного кода

[cm6] Change Emacs commands to visual-line-mode (#12523)

* [cm6] Change Emacs commands to visual-line-mode
* [cm6] Change line deletion commands to visual line mode

GitOrigin-RevId: 7a4f3d66bec611de410b6c1fbafbfe33b974e37b
Mathias Jakobsen 3 лет назад
Родитель
Сommit
4b2cc907e2

+ 7 - 7
package-lock.json

@@ -8189,8 +8189,8 @@
     },
     "node_modules/@replit/codemirror-emacs": {
       "version": "6.0.0",
-      "resolved": "git+ssh://git@github.com/overleaf/codemirror-emacs.git#3f235870d4fa83069df855d6588569865c31e220",
-      "integrity": "sha512-Fqnj4HO25LRqlvgS2g/FEP+w2LpmjAN+y+nED2eLk6ezaE8VpFc7DJyd0Or7FqwqonJv1ARMJUDOKQVTkKcEjA==",
+      "resolved": "git+ssh://git@github.com/overleaf/codemirror-emacs.git#cea6eaefe2301bf07e7dec54f028537c3fdc4982",
+      "integrity": "sha512-1dW1RZX6yaZ31N2KqQ7XgYAy44yhXOf3LBZjpoODoVnJzEX5b003mejygoVCrHr6GpjBeInAx7ggx2wRWXiLXA==",
       "license": "MIT",
       "peerDependencies": {
         "@codemirror/autocomplete": "^6.0.2",
@@ -35131,7 +35131,7 @@
         "@pollyjs/core": "^4.2.1",
         "@pollyjs/persister-fs": "^4.2.1",
         "@reach/tabs": "^0.15.0",
-        "@replit/codemirror-emacs": "overleaf/codemirror-emacs#3f235870d4fa83069df855d6588569865c31e220",
+        "@replit/codemirror-emacs": "overleaf/codemirror-emacs#cea6eaefe2301bf07e7dec54f028537c3fdc4982",
         "@replit/codemirror-indentation-markers": "overleaf/codemirror-indentation-markers#1b1f93c0bcd04293aea6986aa2275185b2c56803",
         "@replit/codemirror-vim": "overleaf/codemirror-vim#77876ac3390f5a6642e04348c0afbe9f0878ec25",
         "@sentry/browser": "^7.8.1",
@@ -44810,7 +44810,7 @@
         "@pollyjs/core": "^4.2.1",
         "@pollyjs/persister-fs": "^4.2.1",
         "@reach/tabs": "^0.15.0",
-        "@replit/codemirror-emacs": "overleaf/codemirror-emacs#3f235870d4fa83069df855d6588569865c31e220",
+        "@replit/codemirror-emacs": "overleaf/codemirror-emacs#cea6eaefe2301bf07e7dec54f028537c3fdc4982",
         "@replit/codemirror-indentation-markers": "overleaf/codemirror-indentation-markers#1b1f93c0bcd04293aea6986aa2275185b2c56803",
         "@replit/codemirror-vim": "overleaf/codemirror-vim#77876ac3390f5a6642e04348c0afbe9f0878ec25",
         "@sentry/browser": "^7.8.1",
@@ -46990,9 +46990,9 @@
       "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg=="
     },
     "@replit/codemirror-emacs": {
-      "version": "git+ssh://git@github.com/overleaf/codemirror-emacs.git#3f235870d4fa83069df855d6588569865c31e220",
-      "integrity": "sha512-Fqnj4HO25LRqlvgS2g/FEP+w2LpmjAN+y+nED2eLk6ezaE8VpFc7DJyd0Or7FqwqonJv1ARMJUDOKQVTkKcEjA==",
-      "from": "@replit/codemirror-emacs@overleaf/codemirror-emacs#3f235870d4fa83069df855d6588569865c31e220",
+      "version": "git+ssh://git@github.com/overleaf/codemirror-emacs.git#cea6eaefe2301bf07e7dec54f028537c3fdc4982",
+      "integrity": "sha512-1dW1RZX6yaZ31N2KqQ7XgYAy44yhXOf3LBZjpoODoVnJzEX5b003mejygoVCrHr6GpjBeInAx7ggx2wRWXiLXA==",
+      "from": "@replit/codemirror-emacs@overleaf/codemirror-emacs#cea6eaefe2301bf07e7dec54f028537c3fdc4982",
       "requires": {}
     },
     "@replit/codemirror-indentation-markers": {

+ 15 - 1
services/web/frontend/js/features/source-editor/extensions/index.ts

@@ -62,6 +62,12 @@ const ignoredDefaultKeybindings = new Set([
   'Mod-Alt-\\',
 ])
 
+const ignoredDefaultMacKeybindings = new Set([
+  // We replace these with our custom visual-line versions
+  'Mod-Backspace',
+  'Mod-Delete',
+])
+
 const moduleExtensions: Array<() => Extension> = importOverleafModules(
   'sourceEditorExtensions'
 ).map((item: { import: { extension: Extension } }) => item.import.extension)
@@ -92,7 +98,15 @@ export const createExtensions = (options: Record<string, any>): Extension[] => [
     ...defaultKeymap.filter(
       // We only filter on keys, so if the keybinding doesn't have a key,
       // allow it
-      item => !item.key || !ignoredDefaultKeybindings.has(item.key)
+      item => {
+        if (item.key && ignoredDefaultKeybindings.has(item.key)) {
+          return false
+        }
+        if (item.mac && ignoredDefaultMacKeybindings.has(item.mac)) {
+          return false
+        }
+        return true
+      }
     ),
     ...historyKeymap,
     ...lintKeymap,

+ 19 - 0
services/web/frontend/js/features/source-editor/extensions/keybindings.ts

@@ -8,6 +8,13 @@ import {
 import { EmacsHandler } from '@replit/codemirror-emacs'
 import { CodeMirror } from '@replit/codemirror-vim'
 import { foldCode, toggleFold, unfoldCode } from '@codemirror/language'
+import {
+  cursorToBeginningOfVisualLine,
+  cursorToEndOfVisualLine,
+  selectRestOfVisualLine,
+  selectToBeginningOfVisualLine,
+  selectToEndOfVisualLine,
+} from './visual-line-selection'
 
 const hasNonEmptySelection = (cm: CodeMirror): boolean => {
   const selections = cm.getSelections()
@@ -132,6 +139,18 @@ const customiseEmacsOnce = () => {
   })
   EmacsHandler.bindKey('C-s', 'openSearch')
   EmacsHandler.bindKey('C-r', 'openSearch')
+  EmacsHandler.bindKey('C-a', {
+    command: 'goOrSelect',
+    args: [cursorToBeginningOfVisualLine, selectToBeginningOfVisualLine],
+  })
+  EmacsHandler.bindKey('C-e', {
+    command: 'goOrSelect',
+    args: [cursorToEndOfVisualLine, selectToEndOfVisualLine],
+  })
+  EmacsHandler.bindKey('C-k', {
+    command: 'killLine',
+    args: selectRestOfVisualLine,
+  })
 }
 
 const options = [

+ 12 - 0
services/web/frontend/js/features/source-editor/extensions/shortcuts.ts

@@ -19,6 +19,10 @@ import { changeCase, duplicateSelection } from '../commands/ranges'
 import { selectOccurrence } from '../commands/select'
 import { cloneSelectionVertically } from '../commands/cursor'
 import { dispatchEditorEvent } from './changes/change-manager'
+import {
+  deleteToVisualLineEnd,
+  deleteToVisualLineStart,
+} from './visual-line-selection'
 
 export const shortcuts = () => {
   const toggleReviewPanel = () => {
@@ -168,6 +172,14 @@ export const shortcuts = () => {
       run: cursorSyntaxRight,
       shift: selectSyntaxRight,
     },
+    {
+      mac: 'Mod-Backspace',
+      run: deleteToVisualLineStart,
+    },
+    {
+      mac: 'Mod-Delete',
+      run: deleteToVisualLineEnd,
+    },
   ]
 
   return Prec.high(keymap.of(keyBindings))

+ 160 - 0
services/web/frontend/js/features/source-editor/extensions/visual-line-selection.ts

@@ -0,0 +1,160 @@
+import {
+  SelectionRange,
+  EditorSelection,
+  EditorState,
+  Transaction,
+} from '@codemirror/state'
+import { Command, EditorView } from '@codemirror/view'
+
+const getNextLineBoundary = (
+  selection: SelectionRange,
+  forward: boolean,
+  view: EditorView,
+  includeWrappingCharacter = false
+) => {
+  const newSelection = view.moveToLineBoundary(
+    EditorSelection.cursor(
+      selection.head,
+      1,
+      selection.bidiLevel || undefined,
+      selection.goalColumn
+    ),
+    forward
+  )
+  // Adjust to be "before" the simulated line break
+  let offset = 0
+  if (
+    forward &&
+    !includeWrappingCharacter &&
+    view.lineBlockAt(selection.head).to !== newSelection.head
+  ) {
+    offset = 1
+  }
+  return EditorSelection.cursor(
+    newSelection.head - offset,
+    selection.assoc,
+    selection.bidiLevel || undefined,
+    newSelection.goalColumn
+  )
+}
+
+const changeSelection = (
+  view: EditorView,
+  how: (selection: SelectionRange) => SelectionRange,
+  extend = false
+) => {
+  view.dispatch({
+    selection: EditorSelection.create(
+      view.state.selection.ranges.map(start => {
+        const newSelection = how(start)
+        const anchor = extend ? start.anchor : newSelection.head
+        return EditorSelection.range(
+          anchor,
+          newSelection.head,
+          newSelection.goalColumn,
+          newSelection.bidiLevel || undefined
+        )
+      }),
+      view.state.selection.mainIndex
+    ),
+    scrollIntoView: true,
+    userEvent: 'select',
+  })
+}
+
+export const cursorToEndOfVisualLine = (view: EditorView) =>
+  changeSelection(view, range => getNextLineBoundary(range, true, view), false)
+
+export const selectToEndOfVisualLine = (view: EditorView) =>
+  changeSelection(view, range => getNextLineBoundary(range, true, view), true)
+
+export const selectRestOfVisualLine = (view: EditorView) =>
+  changeSelection(
+    view,
+    range => getNextLineBoundary(range, true, view, true),
+    true
+  )
+
+export const cursorToBeginningOfVisualLine = (view: EditorView) =>
+  changeSelection(view, range => getNextLineBoundary(range, false, view), false)
+
+export const selectToBeginningOfVisualLine = (view: EditorView) =>
+  changeSelection(view, range => getNextLineBoundary(range, false, view), true)
+
+export const deleteToVisualLineEnd: Command = view =>
+  deleteBy(view, pos => {
+    const lineEnd = getNextLineBoundary(
+      EditorSelection.cursor(pos),
+      true,
+      view,
+      true
+    ).to
+    return pos < lineEnd ? lineEnd : Math.min(view.state.doc.length, pos + 1)
+  })
+
+export const deleteToVisualLineStart: Command = view =>
+  deleteBy(view, pos => {
+    const lineStart = getNextLineBoundary(
+      EditorSelection.cursor(pos),
+      false,
+      view
+    ).to
+    return pos > lineStart ? lineStart : Math.max(0, pos - 1)
+  })
+
+/* eslint-disable */
+/**
+ * The following definitions are from CodeMirror 6, licensed under the MIT license:
+ * https://github.com/codemirror/commands/blob/main/src/commands.ts
+ */
+type CommandTarget = { state: EditorState; dispatch: (tr: Transaction) => void }
+
+function deleteBy(target: CommandTarget, by: (start: number) => number) {
+  if (target.state.readOnly) return false
+  let event = 'delete.selection',
+    { state } = target
+  let changes = state.changeByRange(range => {
+    let { from, to } = range
+    if (from == to) {
+      let towards = by(from)
+      if (towards < from) {
+        event = 'delete.backward'
+        towards = skipAtomic(target, towards, false)
+      } else if (towards > from) {
+        event = 'delete.forward'
+        towards = skipAtomic(target, towards, true)
+      }
+      from = Math.min(from, towards)
+      to = Math.max(to, towards)
+    } else {
+      from = skipAtomic(target, from, false)
+      to = skipAtomic(target, to, true)
+    }
+    return from == to
+      ? { range }
+      : { changes: { from, to }, range: EditorSelection.cursor(from) }
+  })
+  if (changes.changes.empty) return false
+  target.dispatch(
+    state.update(changes, {
+      scrollIntoView: true,
+      userEvent: event,
+      effects:
+        event == 'delete.selection'
+          ? EditorView.announce.of(state.phrase('Selection deleted'))
+          : undefined,
+    })
+  )
+  return true
+}
+
+function skipAtomic(target: CommandTarget, pos: number, forward: boolean) {
+  if (target instanceof EditorView)
+    for (let ranges of target.state
+      .facet(EditorView.atomicRanges)
+      .map(f => f(target)))
+      ranges.between(pos, pos, (from, to) => {
+        if (from < pos && to > pos) pos = forward ? to : from
+      })
+  return pos
+}

+ 1 - 1
services/web/package.json

@@ -111,7 +111,7 @@
     "@pollyjs/core": "^4.2.1",
     "@pollyjs/persister-fs": "^4.2.1",
     "@reach/tabs": "^0.15.0",
-    "@replit/codemirror-emacs": "overleaf/codemirror-emacs#3f235870d4fa83069df855d6588569865c31e220",
+    "@replit/codemirror-emacs": "overleaf/codemirror-emacs#cea6eaefe2301bf07e7dec54f028537c3fdc4982",
     "@replit/codemirror-indentation-markers": "overleaf/codemirror-indentation-markers#1b1f93c0bcd04293aea6986aa2275185b2c56803",
     "@replit/codemirror-vim": "overleaf/codemirror-vim#77876ac3390f5a6642e04348c0afbe9f0878ec25",
     "@sentry/browser": "^7.8.1",