mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
[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:
parent
e09a5f3adf
commit
4b2cc907e2
6 changed files with 214 additions and 9 deletions
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -8189,8 +8189,8 @@
|
||||||
},
|
},
|
||||||
"node_modules/@replit/codemirror-emacs": {
|
"node_modules/@replit/codemirror-emacs": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "git+ssh://git@github.com/overleaf/codemirror-emacs.git#3f235870d4fa83069df855d6588569865c31e220",
|
"resolved": "git+ssh://git@github.com/overleaf/codemirror-emacs.git#cea6eaefe2301bf07e7dec54f028537c3fdc4982",
|
||||||
"integrity": "sha512-Fqnj4HO25LRqlvgS2g/FEP+w2LpmjAN+y+nED2eLk6ezaE8VpFc7DJyd0Or7FqwqonJv1ARMJUDOKQVTkKcEjA==",
|
"integrity": "sha512-1dW1RZX6yaZ31N2KqQ7XgYAy44yhXOf3LBZjpoODoVnJzEX5b003mejygoVCrHr6GpjBeInAx7ggx2wRWXiLXA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@codemirror/autocomplete": "^6.0.2",
|
"@codemirror/autocomplete": "^6.0.2",
|
||||||
|
@ -35131,7 +35131,7 @@
|
||||||
"@pollyjs/core": "^4.2.1",
|
"@pollyjs/core": "^4.2.1",
|
||||||
"@pollyjs/persister-fs": "^4.2.1",
|
"@pollyjs/persister-fs": "^4.2.1",
|
||||||
"@reach/tabs": "^0.15.0",
|
"@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-indentation-markers": "overleaf/codemirror-indentation-markers#1b1f93c0bcd04293aea6986aa2275185b2c56803",
|
||||||
"@replit/codemirror-vim": "overleaf/codemirror-vim#77876ac3390f5a6642e04348c0afbe9f0878ec25",
|
"@replit/codemirror-vim": "overleaf/codemirror-vim#77876ac3390f5a6642e04348c0afbe9f0878ec25",
|
||||||
"@sentry/browser": "^7.8.1",
|
"@sentry/browser": "^7.8.1",
|
||||||
|
@ -44810,7 +44810,7 @@
|
||||||
"@pollyjs/core": "^4.2.1",
|
"@pollyjs/core": "^4.2.1",
|
||||||
"@pollyjs/persister-fs": "^4.2.1",
|
"@pollyjs/persister-fs": "^4.2.1",
|
||||||
"@reach/tabs": "^0.15.0",
|
"@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-indentation-markers": "overleaf/codemirror-indentation-markers#1b1f93c0bcd04293aea6986aa2275185b2c56803",
|
||||||
"@replit/codemirror-vim": "overleaf/codemirror-vim#77876ac3390f5a6642e04348c0afbe9f0878ec25",
|
"@replit/codemirror-vim": "overleaf/codemirror-vim#77876ac3390f5a6642e04348c0afbe9f0878ec25",
|
||||||
"@sentry/browser": "^7.8.1",
|
"@sentry/browser": "^7.8.1",
|
||||||
|
@ -46990,9 +46990,9 @@
|
||||||
"integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg=="
|
"integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg=="
|
||||||
},
|
},
|
||||||
"@replit/codemirror-emacs": {
|
"@replit/codemirror-emacs": {
|
||||||
"version": "git+ssh://git@github.com/overleaf/codemirror-emacs.git#3f235870d4fa83069df855d6588569865c31e220",
|
"version": "git+ssh://git@github.com/overleaf/codemirror-emacs.git#cea6eaefe2301bf07e7dec54f028537c3fdc4982",
|
||||||
"integrity": "sha512-Fqnj4HO25LRqlvgS2g/FEP+w2LpmjAN+y+nED2eLk6ezaE8VpFc7DJyd0Or7FqwqonJv1ARMJUDOKQVTkKcEjA==",
|
"integrity": "sha512-1dW1RZX6yaZ31N2KqQ7XgYAy44yhXOf3LBZjpoODoVnJzEX5b003mejygoVCrHr6GpjBeInAx7ggx2wRWXiLXA==",
|
||||||
"from": "@replit/codemirror-emacs@overleaf/codemirror-emacs#3f235870d4fa83069df855d6588569865c31e220",
|
"from": "@replit/codemirror-emacs@overleaf/codemirror-emacs#cea6eaefe2301bf07e7dec54f028537c3fdc4982",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@replit/codemirror-indentation-markers": {
|
"@replit/codemirror-indentation-markers": {
|
||||||
|
|
|
@ -62,6 +62,12 @@ const ignoredDefaultKeybindings = new Set([
|
||||||
'Mod-Alt-\\',
|
'Mod-Alt-\\',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const ignoredDefaultMacKeybindings = new Set([
|
||||||
|
// We replace these with our custom visual-line versions
|
||||||
|
'Mod-Backspace',
|
||||||
|
'Mod-Delete',
|
||||||
|
])
|
||||||
|
|
||||||
const moduleExtensions: Array<() => Extension> = importOverleafModules(
|
const moduleExtensions: Array<() => Extension> = importOverleafModules(
|
||||||
'sourceEditorExtensions'
|
'sourceEditorExtensions'
|
||||||
).map((item: { import: { extension: Extension } }) => item.import.extension)
|
).map((item: { import: { extension: Extension } }) => item.import.extension)
|
||||||
|
@ -92,7 +98,15 @@ export const createExtensions = (options: Record<string, any>): Extension[] => [
|
||||||
...defaultKeymap.filter(
|
...defaultKeymap.filter(
|
||||||
// We only filter on keys, so if the keybinding doesn't have a key,
|
// We only filter on keys, so if the keybinding doesn't have a key,
|
||||||
// allow it
|
// 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,
|
...historyKeymap,
|
||||||
...lintKeymap,
|
...lintKeymap,
|
||||||
|
|
|
@ -8,6 +8,13 @@ import {
|
||||||
import { EmacsHandler } from '@replit/codemirror-emacs'
|
import { EmacsHandler } from '@replit/codemirror-emacs'
|
||||||
import { CodeMirror } from '@replit/codemirror-vim'
|
import { CodeMirror } from '@replit/codemirror-vim'
|
||||||
import { foldCode, toggleFold, unfoldCode } from '@codemirror/language'
|
import { foldCode, toggleFold, unfoldCode } from '@codemirror/language'
|
||||||
|
import {
|
||||||
|
cursorToBeginningOfVisualLine,
|
||||||
|
cursorToEndOfVisualLine,
|
||||||
|
selectRestOfVisualLine,
|
||||||
|
selectToBeginningOfVisualLine,
|
||||||
|
selectToEndOfVisualLine,
|
||||||
|
} from './visual-line-selection'
|
||||||
|
|
||||||
const hasNonEmptySelection = (cm: CodeMirror): boolean => {
|
const hasNonEmptySelection = (cm: CodeMirror): boolean => {
|
||||||
const selections = cm.getSelections()
|
const selections = cm.getSelections()
|
||||||
|
@ -132,6 +139,18 @@ const customiseEmacsOnce = () => {
|
||||||
})
|
})
|
||||||
EmacsHandler.bindKey('C-s', 'openSearch')
|
EmacsHandler.bindKey('C-s', 'openSearch')
|
||||||
EmacsHandler.bindKey('C-r', '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 = [
|
const options = [
|
||||||
|
|
|
@ -19,6 +19,10 @@ import { changeCase, duplicateSelection } from '../commands/ranges'
|
||||||
import { selectOccurrence } from '../commands/select'
|
import { selectOccurrence } from '../commands/select'
|
||||||
import { cloneSelectionVertically } from '../commands/cursor'
|
import { cloneSelectionVertically } from '../commands/cursor'
|
||||||
import { dispatchEditorEvent } from './changes/change-manager'
|
import { dispatchEditorEvent } from './changes/change-manager'
|
||||||
|
import {
|
||||||
|
deleteToVisualLineEnd,
|
||||||
|
deleteToVisualLineStart,
|
||||||
|
} from './visual-line-selection'
|
||||||
|
|
||||||
export const shortcuts = () => {
|
export const shortcuts = () => {
|
||||||
const toggleReviewPanel = () => {
|
const toggleReviewPanel = () => {
|
||||||
|
@ -168,6 +172,14 @@ export const shortcuts = () => {
|
||||||
run: cursorSyntaxRight,
|
run: cursorSyntaxRight,
|
||||||
shift: selectSyntaxRight,
|
shift: selectSyntaxRight,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
mac: 'Mod-Backspace',
|
||||||
|
run: deleteToVisualLineStart,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mac: 'Mod-Delete',
|
||||||
|
run: deleteToVisualLineEnd,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return Prec.high(keymap.of(keyBindings))
|
return Prec.high(keymap.of(keyBindings))
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -111,7 +111,7 @@
|
||||||
"@pollyjs/core": "^4.2.1",
|
"@pollyjs/core": "^4.2.1",
|
||||||
"@pollyjs/persister-fs": "^4.2.1",
|
"@pollyjs/persister-fs": "^4.2.1",
|
||||||
"@reach/tabs": "^0.15.0",
|
"@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-indentation-markers": "overleaf/codemirror-indentation-markers#1b1f93c0bcd04293aea6986aa2275185b2c56803",
|
||||||
"@replit/codemirror-vim": "overleaf/codemirror-vim#77876ac3390f5a6642e04348c0afbe9f0878ec25",
|
"@replit/codemirror-vim": "overleaf/codemirror-vim#77876ac3390f5a6642e04348c0afbe9f0878ec25",
|
||||||
"@sentry/browser": "^7.8.1",
|
"@sentry/browser": "^7.8.1",
|
||||||
|
|
Loading…
Reference in a new issue