[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
This commit is contained in:
Mathias Jakobsen 2023-04-14 09:58:19 +01:00 committed by Copybot
parent e09a5f3adf
commit 4b2cc907e2
6 changed files with 214 additions and 9 deletions

14
package-lock.json generated
View file

@ -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": {

View file

@ -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,

View file

@ -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 = [

View file

@ -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))

View file

@ -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
}

View file

@ -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",