Skip to content

Commit

Permalink
Add a rectangular selection extension
Browse files Browse the repository at this point in the history
FEATURE: Add a new package `rectangular-selection`, which implements
rectangle selection on alt-drag.

Closes #174
  • Loading branch information
marijnh committed May 18, 2020
1 parent 6045b07 commit 46aaa00
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 2 deletions.
2 changes: 2 additions & 0 deletions demo/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {multipleSelections} from "@codemirror/next/multiple-selections"
import {search, defaultSearchKeymap} from "@codemirror/next/search"
import {autocomplete, startCompletion} from "@codemirror/next/autocomplete"
import {toggleLineComment, lineComment, lineUncomment, toggleBlockComment} from "@codemirror/next/comment"
import {rectangularSelection} from "@codemirror/next/rectangular-selection"

import {html} from "@codemirror/next/lang-html"
import {defaultHighlighter} from "@codemirror/next/highlight"
Expand Down Expand Up @@ -45,6 +46,7 @@ let state = EditorState.create({doc: `<script>
bracketMatching(),
closeBrackets,
autocomplete(),
rectangularSelection(),
keymap({
"Mod-z": undo,
"Mod-Shift-z": redo,
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,12 @@
"./highlight": "./highlight/dist/index.js",
"./stream-syntax": "./stream-syntax/dist/index.js",
"./autocomplete": "./autocomplete/dist/index.js",
"./comment": "./comment/dist/index.js",
"./rectangular-selection": "./rectangular-selection/dist/index.js",
"./lang-javascript": "./lang-javascript/dist/index.js",
"./lang-python": "./lang-python/dist/index.js",
"./lang-css": "./lang-css/dist/index.js",
"./lang-html": "./lang-html/dist/index.js",
"./comment": "./comment/dist/index.js"
"./lang-html": "./lang-html/dist/index.js"
},
"license": "(MIT OR GPL-3.0)",
"dependencies": {
Expand Down
5 changes: 5 additions & 0 deletions rectangular-selection/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"main": "dist/index.js",
"types": "src/rectangular-selection",
"type": "module"
}
77 changes: 77 additions & 0 deletions rectangular-selection/src/rectangular-selection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {Extension, EditorSelection, SelectionRange, EditorState} from "@codemirror/next/state"
import {EditorView, MouseSelectionStyle} from "@codemirror/next/view"
import {countColumn, findColumn} from "@codemirror/next/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

function rectangleFor(state: EditorState, a: Pos, b: Pos) {
let startLine = Math.min(a.line, b.line), endLine = Math.max(a.line, b.line)
let ranges = []
if (a.off > MaxOff || b.off > MaxOff || a.col < 0 || b.col < 0) {
let startOff = Math.min(a.off, b.off), endOff = Math.max(a.off, b.off)
for (let i = startLine; i <= endLine; i++) {
let line = state.doc.line(i)
if (line.length <= endOff)
ranges.push(new SelectionRange(line.start + startOff, line.end + endOff))
}
} else {
let startCol = Math.min(a.col, b.col), endCol = Math.max(a.col, b.col)
for (let i = startLine; i <= endLine; i++) {
let line = state.doc.line(i), str = line.length > MaxOff ? line.slice(0, 2 * endCol) : line.slice()
let start = findColumn(str, 0, startCol, state.tabSize), end = findColumn(str, 0, endCol, state.tabSize)
if (!start.leftOver)
ranges.push(new SelectionRange(line.start + start.offset, line.start + end.offset))
}
}
return ranges
}

function absoluteColumn(view: EditorView, x: 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) {
let offset = view.posAtCoords({x: event.clientX, y: event.clientY}) // FIXME
let line = view.state.doc.lineAt(offset), off = offset - line.start
let col = off > MaxOff ? -1
: off == line.length ? absoluteColumn(view, event.clientX)
: countColumn(line.slice(0, offset - line.start), 0, view.state.tabSize)
return {line: line.number, col, off}
}

function rectangleSelectionStyle(view: EditorView, event: MouseEvent) {
let start = getPos(view, event), startSel = view.state.selection
return {
update(update) {
if (update.docChanged) {
let newStart = update.changes.mapPos(update.prevState.doc.line(start.line).start)
let 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) {
let cur = getPos(view, event), ranges = rectangleFor(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
}

/// Create an extension that enables rectangular selections. By
/// default, it will rect to left mouse drag with the alt key held
/// down. When such a selection occurs, the text within the rectangle
/// that was dragged over will be selected, as one selection
/// [range](#state.SelectionRange) per line. You can pass a custom
/// predicate function, which takes a `mousedown` event and returns
/// true if it should be used for rectangular selection.
export function rectangularSelection(eventFilter?: (event: MouseEvent) => boolean): Extension {
let filter = eventFilter || (e => e.altKey && e.button == 0)
return EditorView.mouseSelectionStyle.of((view, event) => filter(event) ? rectangleSelectionStyle(view, event) : null)
}

0 comments on commit 46aaa00

Please sign in to comment.