Last active
June 2, 2021 14:49
-
-
Save ColinTimBarndt/72ef9256b6bcf0f87285e3209226ac82 to your computer and use it in GitHub Desktop.
A very dirty CodeMirror 6 hack to manually update the selection when a key is pressed
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Based on [@codemirror/rectangular-selection](https://github.com/codemirror/rectangular-selection) | |
* This is a clean implementation made possible by [this commit] | |
* [this commit]: https://github.com/codemirror/view/commit/6ec0d746ff3a33e3f73870fa7527779ec7af9904 | |
* @license MIT | |
*/ | |
import { Extension, EditorSelection, EditorState, SelectionRange, Facet, Compartment } from "@codemirror/state"; | |
import { EditorView, MouseSelectionStyle, PluginValue, ViewPlugin, ViewUpdate } from "@codemirror/view"; | |
import { countColumn, findColumn } from "@codemirror/text"; | |
type Pos = { line: number, col: number, off: number }; | |
// Don't compute precise column positions for line offsets above this | |
// (since it could get expensive). Assume offset==column for them. | |
const MaxOff = 2000; | |
const altPressed = Facet.define<boolean, boolean>({ | |
combine: value => value.some(v => v) | |
}); | |
const comp = new Compartment(); | |
function rectangleFor(state: EditorState, a: Pos, b: Pos): SelectionRange[] { | |
const reversed = a.line > b.line; | |
const startLine = reversed ? b.line : a.line; | |
const endLine = reversed ? a.line : b.line; | |
const ranges = []; | |
let i; | |
if (a.off > MaxOff || b.off > MaxOff || a.col < 0 || b.col < 0) { | |
const startOff = Math.min(a.off, b.off); | |
const endOff = Math.max(a.off, b.off); | |
for (i = startLine; i <= endLine; i++) { | |
const line = state.doc.line(i); | |
if (line.length <= endOff) | |
ranges.push( | |
reversed | |
? EditorSelection.range(line.to + endOff, line.from + startOff) | |
: EditorSelection.range(line.from + startOff, line.to + endOff) | |
); | |
} | |
} else { | |
const startCol = Math.min(a.col, b.col); | |
const endCol = Math.max(a.col, b.col); | |
for (i = startLine; i <= endLine; i++) { | |
const line = state.doc.line(i); | |
const str = line.length > MaxOff ? line.text.slice(0, 2 * endCol) : line.text; | |
const start = findColumn(str, 0, startCol, state.tabSize) | |
const end = findColumn(str, 0, endCol, state.tabSize); | |
if (!start.leftOver) | |
ranges.push( | |
reversed | |
? EditorSelection.range(line.from + end.offset, line.from + start.offset) | |
: EditorSelection.range(line.from + start.offset, line.from + end.offset) | |
); | |
} | |
} | |
return ranges; | |
} | |
function rangeFor(state: EditorState, a: Pos, b: Pos): SelectionRange[] { | |
const offA = state.doc.line(a.line).from + a.off; | |
const offB = state.doc.line(b.line).from + b.off; | |
return [ | |
EditorSelection.range(offA, offB) | |
]; | |
} | |
function absoluteColumn(view: EditorView, x: number): number { | |
let ref = view.coordsAtPos(view.viewport.from); | |
return ref ? Math.round(Math.abs((ref.left - x) / view.defaultCharacterWidth)) : -1; | |
} | |
function getPos(view: EditorView, event: MouseEvent): Pos | null { | |
let offset = view.posAtCoords({ x: event.clientX, y: event.clientY }); | |
if (offset == null) return null; | |
let line = view.state.doc.lineAt(offset), off = offset - line.from; | |
let col = off > MaxOff ? -1 | |
: off == line.length ? absoluteColumn(view, event.clientX) | |
: countColumn(line.text.slice(0, offset - line.from), 0, view.state.tabSize); | |
return { line: line.number, col, off }; | |
} | |
function rectangleSelectionStyle(view: EditorView, event: MouseEvent): MouseSelectionStyle | null { | |
let start = getPos(view, event)!; | |
let startSel = view.state.selection; | |
let cur: Pos | null; | |
if (!start) return null; | |
return { | |
update(update) { | |
if (update.docChanged) { | |
const newStart = update.changes.mapPos(update.startState.doc.line(start.line).from); | |
const newLine = update.state.doc.lineAt(newStart); | |
start = { line: newLine.number, col: start.col, off: Math.min(start.off, newLine.length) }; | |
startSel = startSel.map(update.changes); | |
return true; | |
} else if (update.startState.facet(altPressed) !== update.state.facet(altPressed)) { | |
return true; | |
} | |
return false; | |
}, | |
get(event, _extend, multiple) { | |
const rectangular = view.state.facet(altPressed); | |
cur = getPos(view, event); | |
if (!cur) return startSel; | |
const ranges = (rectangular ? rectangleFor : rangeFor)(view.state, start, cur); | |
if (!ranges.length) return startSel; | |
if (multiple) return EditorSelection.create(ranges.concat(startSel.ranges)); | |
else return EditorSelection.create(ranges); | |
} | |
} as MouseSelectionStyle | |
} | |
export function rectangularSelection(): Extension { | |
return [ | |
EditorView.mouseSelectionStyle.of((view, event) => | |
rectangleSelectionStyle(view, event) | |
), | |
listenAltKey(), | |
comp.of(altPressed.of(false)) | |
]; | |
} | |
function listenAltKey(): Extension { | |
return ViewPlugin.fromClass(AltKeyListener, { | |
eventHandlers: { | |
keyup(event, view) { | |
if (event.key === "Alt") { | |
event.preventDefault(); | |
this.altPressed = false; | |
view.dispatch( | |
view.state.update({ | |
effects: comp.reconfigure(altPressed.of(false)), | |
}) | |
); | |
} | |
}, | |
keydown(event, view) { | |
if (event.key === "Alt" && !this.altPressed) { | |
this.altPressed = true; | |
view.dispatch( | |
view.state.update({ | |
effects: comp.reconfigure(altPressed.of(true)), | |
}) | |
); | |
} | |
} | |
} | |
}); | |
} | |
class AltKeyListener implements PluginValue { | |
altPressed: boolean = false; | |
constructor(private view: EditorView) { } | |
update(_: ViewUpdate) { } | |
destroy() { } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Based on [@codemirror/rectangular-selection](https://github.com/codemirror/rectangular-selection) | |
* @license MIT | |
*/ | |
import { Extension, EditorSelection, EditorState, SelectionRange, Facet, Compartment } from "@codemirror/state"; | |
import { EditorView, MouseSelectionStyle, PluginValue, ViewPlugin, ViewUpdate } from "@codemirror/view"; | |
import { countColumn, findColumn } from "@codemirror/text"; | |
type Pos = { line: number, col: number, off: number }; | |
// Don't compute precise column positions for line offsets above this | |
// (since it could get expensive). Assume offset==column for them. | |
const MaxOff = 2000; | |
const altPressed = Facet.define<boolean, boolean>({ | |
combine: value => value.some(v => v) | |
}); | |
const comp = new Compartment(); | |
function rectangleFor(state: EditorState, a: Pos, b: Pos): SelectionRange[] { | |
const reversed = a.line > b.line; | |
const startLine = reversed ? b.line : a.line; | |
const endLine = reversed ? a.line : b.line; | |
const ranges = []; | |
let i; | |
if (a.off > MaxOff || b.off > MaxOff || a.col < 0 || b.col < 0) { | |
const startOff = Math.min(a.off, b.off); | |
const endOff = Math.max(a.off, b.off); | |
for (i = startLine; i <= endLine; i++) { | |
const line = state.doc.line(i); | |
if (line.length <= endOff) | |
ranges.push( | |
reversed | |
? EditorSelection.range(line.to + endOff, line.from + startOff) | |
: EditorSelection.range(line.from + startOff, line.to + endOff) | |
); | |
} | |
} else { | |
const startCol = Math.min(a.col, b.col); | |
const endCol = Math.max(a.col, b.col); | |
for (i = startLine; i <= endLine; i++) { | |
const line = state.doc.line(i); | |
const str = line.length > MaxOff ? line.text.slice(0, 2 * endCol) : line.text; | |
const start = findColumn(str, 0, startCol, state.tabSize) | |
const end = findColumn(str, 0, endCol, state.tabSize); | |
if (!start.leftOver) | |
ranges.push( | |
reversed | |
? EditorSelection.range(line.from + end.offset, line.from + start.offset) | |
: EditorSelection.range(line.from + start.offset, line.from + end.offset) | |
); | |
} | |
} | |
return ranges; | |
} | |
function rangeFor(state: EditorState, a: Pos, b: Pos): SelectionRange[] { | |
const offA = state.doc.line(a.line).from + a.off; | |
const offB = state.doc.line(b.line).from + b.off; | |
return [ | |
EditorSelection.range(offA, offB) | |
]; | |
} | |
function absoluteColumn(view: EditorView, x: number): number { | |
let ref = view.coordsAtPos(view.viewport.from); | |
return ref ? Math.round(Math.abs((ref.left - x) / view.defaultCharacterWidth)) : -1; | |
} | |
function getPos(view: EditorView, event: MouseEvent): Pos | null { | |
let offset = view.posAtCoords({ x: event.clientX, y: event.clientY }); | |
if (offset == null) return null; | |
let line = view.state.doc.lineAt(offset), off = offset - line.from; | |
let col = off > MaxOff ? -1 | |
: off == line.length ? absoluteColumn(view, event.clientX) | |
: countColumn(line.text.slice(0, offset - line.from), 0, view.state.tabSize); | |
return { line: line.number, col, off }; | |
} | |
function rectangleSelectionStyle(view: EditorView, event: MouseEvent): MouseSelectionStyle | null { | |
let start = getPos(view, event)!; | |
let startSel = view.state.selection; | |
let cur: Pos | null; | |
if (!start) return null; | |
return { | |
update(update) { | |
if (update.docChanged) { | |
const newStart = update.changes.mapPos(update.startState.doc.line(start.line).from); | |
const newLine = update.state.doc.lineAt(newStart); | |
start = { line: newLine.number, col: start.col, off: Math.min(start.off, newLine.length) }; | |
startSel = startSel.map(update.changes); | |
} | |
}, | |
get(event, _extend, multiple) { | |
const rectangular = view.state.facet(altPressed); | |
cur = getPos(view, event); | |
if (!cur) return startSel; | |
const ranges = (rectangular ? rectangleFor : rangeFor)(view.state, start, cur); | |
if (!ranges.length) return startSel; | |
if (multiple) return EditorSelection.create(ranges.concat(startSel.ranges)); | |
else return EditorSelection.create(ranges); | |
} | |
} as MouseSelectionStyle | |
} | |
export function rectangularSelection(): Extension { | |
return [ | |
EditorView.mouseSelectionStyle.of((view, event) => | |
rectangleSelectionStyle(view, event) | |
), | |
listenAltKey(), | |
comp.of(altPressed.of(false)) | |
]; | |
} | |
function listenAltKey(): Extension { | |
return ViewPlugin.fromClass(AltKeyListener, { | |
eventHandlers: { | |
keyup(event, view) { | |
if (event.key === "Alt") { | |
event.preventDefault(); | |
view.dispatch( | |
view.state.update({ | |
effects: comp.reconfigure(altPressed.of(false)), | |
}) | |
); | |
this.updateSelection(); | |
} | |
}, | |
keydown(event, view) { | |
if (event.key === "Alt") { | |
view.dispatch( | |
view.state.update({ | |
effects: comp.reconfigure(altPressed.of(true)), | |
}) | |
); | |
this.updateSelection(); | |
} | |
}, | |
mousemove(event, _view) { | |
this.lastMouseMove = event; | |
} | |
} | |
}); | |
} | |
class AltKeyListener implements PluginValue { | |
lastMouseMove: MouseEvent | null = null; | |
constructor(private view: EditorView) { } | |
update(_: ViewUpdate) { } | |
destroy() { } | |
/** | |
* HACK to force-update the selection | |
*/ | |
updateSelection() { | |
let inputState: any = (this.view as any).inputState; | |
let selection: any = inputState.mouseSelection; | |
if (selection != null && this.lastMouseMove != null) { | |
const { lastMouseMove: event } = this; | |
if (event.buttons == 0) return; | |
// Run after the keyboard event is over | |
setTimeout(() => selection.select(event), 0); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment