pax_global_header00006660000000000000000000000064146276221760014527gustar00rootroot0000000000000052 comment=99bc90474ed8d3fe884a05242309e4b7276aae66 commands-6.6.0/000077500000000000000000000000001462762217600133415ustar00rootroot00000000000000commands-6.6.0/.github/000077500000000000000000000000001462762217600147015ustar00rootroot00000000000000commands-6.6.0/.github/workflows/000077500000000000000000000000001462762217600167365ustar00rootroot00000000000000commands-6.6.0/.github/workflows/dispatch.yml000066400000000000000000000006371462762217600212660ustar00rootroot00000000000000name: Trigger CI on: push jobs: build: name: Dispatch to main repo runs-on: ubuntu-latest steps: - name: Emit repository_dispatch uses: mvasigh/dispatch-action@main with: # You should create a personal access token and store it in your repository token: ${{ secrets.DISPATCH_AUTH }} repo: dev owner: codemirror event_type: push commands-6.6.0/.gitignore000066400000000000000000000001271462762217600153310ustar00rootroot00000000000000/node_modules package-lock.json /dist /test/*.js /test/*.d.ts /test/*.d.ts.map .tern-* commands-6.6.0/.npmignore000066400000000000000000000001001462762217600153270ustar00rootroot00000000000000/src /test /node_modules .tern-* rollup.config.js tsconfig.json commands-6.6.0/CHANGELOG.md000066400000000000000000000213661462762217600151620ustar00rootroot00000000000000## 6.6.0 (2024-06-04) ### New features The new `toggleTabFocusMode` and `temporarilySetTabFocusMode` commands provide control over the view's tab-focus mode. The default keymap now binds Ctrl-m (Shift-Alt-m on macOS) to `toggleTabFocusMode`. ## 6.5.0 (2024-04-19) ### New features The `insertNewlineKeepIndent` command inserts a newline along with the same indentation as the line before. ## 6.4.0 (2024-04-17) ### Bug fixes Fix an issue where `deleteLine` sometimes leaves the cursor on the wrong line. ### New features The new `deleteCharBackwardStrict` command just deletes a character, without further smart behavior around indentation. ## 6.3.3 (2023-12-28) ### Bug fixes Fix an issue causing cursor motion commands to not dispatch a transaction when the change only affects cursor associativity. ## 6.3.2 (2023-11-28) ### Bug fixes Fix a regression that caused `deleteCharBackward` to sometimes delete a large chunk of text. ## 6.3.1 (2023-11-27) ### Bug fixes When undoing, store the selection after the undone change with the redo event, so that redoing restores it. `deleteCharBackward` will no longer delete variant selector characters as separate characters. ## 6.3.0 (2023-09-29) ### Bug fixes Make it possible for `selectParentSyntax` to jump out of or into a syntax tree overlay. Make Cmd-Backspace and Cmd-Delete on macOS delete to the next line wrap point, not the start/end of the line. ### New features The new `deleteLineBoundaryForward` and `deleteLineBoundaryBackward` commands delete to the start/end of the line or the next line wrapping point. ## 6.2.5 (2023-08-26) ### Bug fixes Make `insertNewlineAndIndent` properly count indentation for tabs when copying over the previous line's indentation. The various sub-word motion commands will now use `Intl.Segmenter`, when available, to stop at CJK language word boundaries. Fix a bug in `insertNewlineAndIndent` that would delete text between brackets if it had no corresponding AST node. ## 6.2.4 (2023-05-03) ### Bug fixes The by-subword motion commands now properly treat dashes, underscores, and similar as subword separators. ## 6.2.3 (2023-04-19) ### Bug fixes Block commenting the selection no longer includes indentation on the first line. ## 6.2.2 (2023-03-10) ### Bug fixes Fix a bug where line commenting got confused when commenting a range that crossed language boundaries. ## 6.2.1 (2023-02-15) ### Bug fixes Keep cursor position stable in `cursorPageUp`/`cursorPageDown` when there are panels or other scroll margins active. Make sure `toggleComment` doesn't get thrown off by local language nesting, by fetching the language data for the start of the selection line. ## 6.2.0 (2023-01-18) ### New features The new `joinToEvent` history configuration option allows you to provide custom logic that determines whether a new transaction is added to an existing history event. ## 6.1.3 (2022-12-26) ### Bug fixes Preserve selection bidi level when extending the selection, to prevent shift-selection from getting stuck in some kinds of bidirectional text. ## 6.1.2 (2022-10-13) ### Bug fixes Fix a bug that caused deletion commands on non-empty ranges to incorrectly return false and do nothing, causing the editor to fall back to native behavior. ## 6.1.1 (2022-09-28) ### Bug fixes Make sure the selection endpoints are moved out of atomic ranges when applying a deletion command to a non-empty selection. ## 6.1.0 (2022-08-18) ### Bug fixes Prevent native behavior on Ctrl/Cmd-ArrowLeft/ArrowRight bindings, so that browsers with odd bidi behavior won't do the wrong thing at start/end of line. Cmd-ArrowLeft/Right on macOS now moves the cursor in the direction of the arrow even in right-to-left content. ### New features The new `cursorLineBoundaryLeft`/`Right` and `selectLineBoundaryLeft`/`Right` commands allow directional motion to line boundaries. ## 6.0.1 (2022-06-30) ### Bug fixes Announce to the screen reader when the selection is deleted. Also bind Ctrl-Shift-z to redo on Linux. ## 6.0.0 (2022-06-08) ### Bug fixes Fix a bug where by-page selection commands sometimes moved one line too far. ## 0.20.0 (2022-04-20) ### Breaking changes There is no longer a separate `commentKeymap`. Those bindings are now part of `defaultKeymap`. ### Bug fixes Make `cursorPageUp` and `cursorPageDown` move by window height when the editor is higher than the window. Make sure the default behavior of Home/End is prevented, since it could produce unexpected results on macOS. ### New features The exports from @codemirror/comment are now available in this package. The exports from the @codemirror/history package are now available from this package. ## 0.19.8 (2022-01-26) ### Bug fixes `deleteCharBackward` now removes extending characters one at a time, rather than deleting the entire glyph at once. Alt-v is no longer bound in `emacsStyleKeymap` and macOS's `standardKeymap`, because macOS doesn't bind it by default and it conflicts with some keyboard layouts. ## 0.19.7 (2022-01-11) ### Bug fixes Don't bind Alt-\< and Alt-> on macOS by default, since those interfere with some keyboard layouts. Make cursorPageUp/Down scroll the view to keep the cursor in place `cursorPageUp` and `cursorPageDown` now scroll the view by the amount that the cursor moved. ## 0.19.6 (2021-12-10) ### Bug fixes The standard keymap no longer overrides Shift-Delete, in order to allow the native behavior of that key to happen on platforms that support it. ## 0.19.5 (2021-09-21) ### New features Adds an `insertBlankLine` command which creates an empty line below the selection, and binds it to Mod-Enter in the default keymap. ## 0.19.4 (2021-09-13) ### Bug fixes Make commands that affect the editor's content check `state.readOnly` and return false when that is true. ## 0.19.3 (2021-09-09) ### Bug fixes Make by-line cursor motion commands move the cursor to the start/end of the document when they hit the first/last line. Fix a bug where `deleteCharForward`/`Backward` behaved incorrectly when deleting directly before or after an atomic range. ## 0.19.2 (2021-08-24) ### New features New commands `cursorSubwordForward`, `cursorSubwordBackward`, `selectSubwordForward`, and `selectSubwordBackward` which implement motion by camel case subword. ## 0.19.1 (2021-08-11) ### Bug fixes Fix incorrect versions for @lezer dependencies. ## 0.19.0 (2021-08-11) ### Breaking changes Change default binding for backspace to `deleteCharBackward`, drop `deleteCodePointBackward`/`Forward` from the library. `defaultTabBinding` was removed. ### Bug fixes Drop Alt-d, Alt-f, and Alt-b bindings from `emacsStyleKeymap` (and thus from the default macOS bindings). `deleteCharBackward` and `deleteCharForward` now take atomic ranges into account. ### New features Attach more granular user event strings to transactions. The module exports a new binding `indentWithTab` that binds tab and shift-tab to `indentMore` and `indentLess`. ## 0.18.3 (2021-06-11) ### Bug fixes `moveLineDown` will no longer incorrectly grow the selection. Line-based commands will no longer include lines where a range selection ends right at the start of the line. ## 0.18.2 (2021-05-06) ### Bug fixes Use Ctrl-l, not Alt-l, to bind `selectLine` on macOS, to avoid conflicting with special-character-insertion bindings. Make the macOS Command-ArrowLeft/Right commands behave more like their native versions. ## 0.18.1 (2021-04-08) ### Bug fixes Also bind Shift-Backspace and Shift-Delete in the default keymap (to do the same thing as the Shift-less binding). ### New features Adds a `deleteToLineStart` command. Adds bindings for Cmd-Delete and Cmd-Backspace on macOS. ## 0.18.0 (2021-03-03) ### Breaking changes Update dependencies to 0.18. ## 0.17.5 (2021-02-25) ### Bug fixes Use Alt-l for the default `selectLine` binding, because Mod-l already has an important meaning in the browser. Make `deleteGroupBackward`/`deleteGroupForward` delete groups of whitespace when bigger than a single space. Don't change lines that have the end of a range selection directly at their start in `indentLess`, `indentMore`, and `indentSelection`. ## 0.17.4 (2021-02-18) ### Bug fixes Fix a bug where `deleteToLineEnd` would delete the rest of the document when at the end of a line. ## 0.17.3 (2021-02-16) ### Bug fixes Fix an issue where `insertNewlineAndIndent` behaved strangely with the cursor between brackets that sat on different lines. ## 0.17.2 (2021-01-22) ### New features The new `insertTab` command inserts a tab when nothing is selected, and defers to `indentMore` otherwise. The package now exports a `defaultTabBinding` object that provides a recommended binding for tab (if you must bind tab). ## 0.17.1 (2021-01-06) ### New features The package now also exports a CommonJS module. ## 0.17.0 (2020-12-29) ### Breaking changes First numbered release. commands-6.6.0/LICENSE000066400000000000000000000021361462762217600143500ustar00rootroot00000000000000MIT License Copyright (C) 2018-2021 by Marijn Haverbeke and others Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. commands-6.6.0/README.md000066400000000000000000000020161462762217600146170ustar00rootroot00000000000000# @codemirror/commands [![NPM version](https://img.shields.io/npm/v/@codemirror/commands.svg)](https://www.npmjs.org/package/@codemirror/commands) [ [**WEBSITE**](https://codemirror.net/) | [**DOCS**](https://codemirror.net/docs/ref/#commands) | [**ISSUES**](https://github.com/codemirror/dev/issues) | [**FORUM**](https://discuss.codemirror.net/c/next/) | [**CHANGELOG**](https://github.com/codemirror/commands/blob/main/CHANGELOG.md) ] This package implements a collection of editing commands for the [CodeMirror](https://codemirror.net/) code editor. The [project page](https://codemirror.net/) has more information, a number of [examples](https://codemirror.net/examples/) and the [documentation](https://codemirror.net/docs/). This code is released under an [MIT license](https://github.com/codemirror/commands/tree/main/LICENSE). We aim to be an inclusive, welcoming community. To make that explicit, we have a [code of conduct](http://contributor-covenant.org/version/1/1/0/) that applies to communication around the project. commands-6.6.0/package.json000066400000000000000000000020001462762217600156170ustar00rootroot00000000000000{ "name": "@codemirror/commands", "version": "6.6.0", "description": "Collection of editing commands for the CodeMirror code editor", "scripts": { "test": "cm-runtests", "prepare": "cm-buildhelper src/commands.ts" }, "keywords": [ "editor", "code" ], "author": { "name": "Marijn Haverbeke", "email": "marijn@haverbeke.berlin", "url": "http://marijnhaverbeke.nl" }, "type": "module", "main": "dist/index.cjs", "exports": { "import": "./dist/index.js", "require": "./dist/index.cjs" }, "types": "dist/index.d.ts", "module": "dist/index.js", "sideEffects": false, "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" }, "devDependencies": { "@codemirror/buildhelper": "^1.0.0", "@codemirror/lang-javascript": "^6.0.0" }, "repository": { "type": "git", "url": "https://github.com/codemirror/commands.git" } } commands-6.6.0/src/000077500000000000000000000000001462762217600141305ustar00rootroot00000000000000commands-6.6.0/src/README.md000066400000000000000000000045151462762217600154140ustar00rootroot00000000000000This package exports a collection of generic editing commands, along with key bindings for a lot of them. ### Keymaps @standardKeymap @defaultKeymap @emacsStyleKeymap @indentWithTab ### Selection @simplifySelection #### By character @cursorCharLeft @selectCharLeft @cursorCharRight @selectCharRight @cursorCharForward @selectCharForward @cursorCharBackward @selectCharBackward #### By group @cursorGroupLeft @selectGroupLeft @cursorGroupRight @selectGroupRight @cursorGroupForward @selectGroupForward @cursorGroupBackward @selectGroupBackward @cursorSubwordForward @selectSubwordForward @cursorSubwordBackward @selectSubwordBackward #### Vertical motion @cursorLineUp @selectLineUp @cursorLineDown @selectLineDown @cursorPageUp @selectPageUp @cursorPageDown @selectPageDown #### By line boundary @cursorLineBoundaryForward @selectLineBoundaryForward @cursorLineBoundaryBackward @selectLineBoundaryBackward @cursorLineBoundaryLeft @selectLineBoundaryLeft @cursorLineBoundaryRight @selectLineBoundaryRight @cursorLineStart @selectLineStart @cursorLineEnd @selectLineEnd @selectLine #### By document boundary @cursorDocStart @selectDocStart @cursorDocEnd @selectDocEnd @selectAll #### By syntax @cursorSyntaxLeft @selectSyntaxLeft @cursorSyntaxRight @selectSyntaxRight @selectParentSyntax @cursorMatchingBracket @selectMatchingBracket ### Deletion @deleteCharBackward @deleteCharBackwardStrict @deleteCharForward @deleteGroupBackward @deleteGroupForward @deleteToLineStart @deleteToLineEnd @deleteLineBoundaryBackward @deleteLineBoundaryForward @deleteTrailingWhitespace ### Line manipulation @splitLine @moveLineUp @moveLineDown @copyLineUp @copyLineDown @deleteLine ### Indentation @indentSelection @indentMore @indentLess @insertTab ### Character Manipulation @transposeChars @insertNewline @insertNewlineAndIndent @insertNewlineKeepIndent @insertBlankLine ### Undo History @history @historyKeymap @historyField @undo @redo @undoSelection @redoSelection @undoDepth @redoDepth @isolateHistory @invertedEffects ### Commenting and Uncommenting @CommentTokens @toggleComment @toggleLineComment @lineComment @lineUncomment @toggleBlockComment @blockComment @blockUncomment @toggleBlockCommentByLine ### Tab Focus Mode @toggleTabFocusMode @temporarilySetTabFocusMode commands-6.6.0/src/commands.ts000066400000000000000000001401671462762217600163120ustar00rootroot00000000000000import {EditorState, StateCommand, EditorSelection, SelectionRange, ChangeSpec, Transaction, CharCategory, findClusterBreak, Text, Line, countColumn} from "@codemirror/state" import {EditorView, Command, Direction, KeyBinding} from "@codemirror/view" import {syntaxTree, IndentContext, getIndentUnit, indentUnit, indentString, getIndentation, matchBrackets} from "@codemirror/language" import {SyntaxNode, NodeProp} from "@lezer/common" import {toggleComment, toggleBlockComment} from "./comment" export {CommentTokens, toggleComment, toggleLineComment, lineComment, lineUncomment, toggleBlockComment, blockComment, blockUncomment, toggleBlockCommentByLine} from "./comment" export {history, historyKeymap, historyField, undo, redo, undoSelection, redoSelection, undoDepth, redoDepth, isolateHistory, invertedEffects} from "./history" function updateSel(sel: EditorSelection, by: (range: SelectionRange) => SelectionRange) { return EditorSelection.create(sel.ranges.map(by), sel.mainIndex) } function setSel(state: EditorState, selection: EditorSelection | {anchor: number, head?: number}) { return state.update({selection, scrollIntoView: true, userEvent: "select"}) } type CommandTarget = {state: EditorState, dispatch: (tr: Transaction) => void} function moveSel({state, dispatch}: CommandTarget, how: (range: SelectionRange) => SelectionRange): boolean { let selection = updateSel(state.selection, how) if (selection.eq(state.selection, true)) return false dispatch(setSel(state, selection)) return true } function rangeEnd(range: SelectionRange, forward: boolean) { return EditorSelection.cursor(forward ? range.to : range.from) } function cursorByChar(view: EditorView, forward: boolean) { return moveSel(view, range => range.empty ? view.moveByChar(range, forward) : rangeEnd(range, forward)) } function ltrAtCursor(view: EditorView) { return view.textDirectionAt(view.state.selection.main.head) == Direction.LTR } /// Move the selection one character to the left (which is backward in /// left-to-right text, forward in right-to-left text). export const cursorCharLeft: Command = view => cursorByChar(view, !ltrAtCursor(view)) /// Move the selection one character to the right. export const cursorCharRight: Command = view => cursorByChar(view, ltrAtCursor(view)) /// Move the selection one character forward. export const cursorCharForward: Command = view => cursorByChar(view, true) /// Move the selection one character backward. export const cursorCharBackward: Command = view => cursorByChar(view, false) function cursorByGroup(view: EditorView, forward: boolean) { return moveSel(view, range => range.empty ? view.moveByGroup(range, forward) : rangeEnd(range, forward)) } /// Move the selection to the left across one group of word or /// non-word (but also non-space) characters. export const cursorGroupLeft: Command = view => cursorByGroup(view, !ltrAtCursor(view)) /// Move the selection one group to the right. export const cursorGroupRight: Command = view => cursorByGroup(view, ltrAtCursor(view)) /// Move the selection one group forward. export const cursorGroupForward: Command = view => cursorByGroup(view, true) /// Move the selection one group backward. export const cursorGroupBackward: Command = view => cursorByGroup(view, false) const segmenter = typeof Intl != "undefined" && (Intl as any).Segmenter ? new ((Intl as any).Segmenter)(undefined, {granularity: "word"}) : null function moveBySubword(view: EditorView, range: SelectionRange, forward: boolean) { let categorize = view.state.charCategorizer(range.from) let cat = CharCategory.Space, pos = range.from, steps = 0 let done = false, sawUpper = false, sawLower = false let step = (next: string) => { if (done) return false pos += forward ? next.length : -next.length let nextCat = categorize(next), ahead if (nextCat == CharCategory.Word && next.charCodeAt(0) < 128 && /[\W_]/.test(next)) nextCat = -1 as any // Treat word punctuation specially if (cat == CharCategory.Space) cat = nextCat if (cat != nextCat) return false if (cat == CharCategory.Word) { if (next.toLowerCase() == next) { if (!forward && sawUpper) return false sawLower = true } else if (sawLower) { if (forward) return false done = true } else { if (sawUpper && forward && categorize(ahead = view.state.sliceDoc(pos, pos + 1)) == CharCategory.Word && ahead.toLowerCase() == ahead) return false sawUpper = true } } steps++ return true } let end = view.moveByChar(range, forward, start => { step(start) return step }) if (segmenter && cat == CharCategory.Word as any && end.from == range.from + steps * (forward ? 1 : -1)) { let from = Math.min(range.head, end.head), to = Math.max(range.head, end.head) let skipped = view.state.sliceDoc(from, to) if (skipped.length > 1 && /[\u4E00-\uffff]/.test(skipped)) { let segments = Array.from(segmenter.segment(skipped)) as {index: number}[] if (segments.length > 1) { if (forward) return EditorSelection.cursor(range.head + segments[1].index, -1) return EditorSelection.cursor(end.head + segments[segments.length - 1].index, 1) } } } return end } function cursorBySubword(view: EditorView, forward: boolean) { return moveSel(view, range => range.empty ? moveBySubword(view, range, forward) : rangeEnd(range, forward)) } /// Move the selection one group or camel-case subword forward. export const cursorSubwordForward: Command = view => cursorBySubword(view, true) /// Move the selection one group or camel-case subword backward. export const cursorSubwordBackward: Command = view => cursorBySubword(view, false) function interestingNode(state: EditorState, node: SyntaxNode, bracketProp: NodeProp) { if (node.type.prop(bracketProp)) return true let len = node.to - node.from return len && (len > 2 || /[^\s,.;:]/.test(state.sliceDoc(node.from, node.to))) || node.firstChild } function moveBySyntax(state: EditorState, start: SelectionRange, forward: boolean) { let pos = syntaxTree(state).resolveInner(start.head) let bracketProp = forward ? NodeProp.closedBy : NodeProp.openedBy // Scan forward through child nodes to see if there's an interesting // node ahead. for (let at = start.head;;) { let next = forward ? pos.childAfter(at) : pos.childBefore(at) if (!next) break if (interestingNode(state, next, bracketProp)) pos = next else at = forward ? next.to : next.from } let bracket = pos.type.prop(bracketProp), match, newPos if (bracket && (match = forward ? matchBrackets(state, pos.from, 1) : matchBrackets(state, pos.to, -1)) && match.matched) newPos = forward ? match.end!.to : match.end!.from else newPos = forward ? pos.to : pos.from return EditorSelection.cursor(newPos, forward ? -1 : 1) } /// Move the cursor over the next syntactic element to the left. export const cursorSyntaxLeft: Command = view => moveSel(view, range => moveBySyntax(view.state, range, !ltrAtCursor(view))) /// Move the cursor over the next syntactic element to the right. export const cursorSyntaxRight: Command = view => moveSel(view, range => moveBySyntax(view.state, range, ltrAtCursor(view))) function cursorByLine(view: EditorView, forward: boolean) { return moveSel(view, range => { if (!range.empty) return rangeEnd(range, forward) let moved = view.moveVertically(range, forward) return moved.head != range.head ? moved : view.moveToLineBoundary(range, forward) }) } /// Move the selection one line up. export const cursorLineUp: Command = view => cursorByLine(view, false) /// Move the selection one line down. export const cursorLineDown: Command = view => cursorByLine(view, true) function pageInfo(view: EditorView) { let selfScroll = view.scrollDOM.clientHeight < view.scrollDOM.scrollHeight - 2 let marginTop = 0, marginBottom = 0, height if (selfScroll) { for (let source of view.state.facet(EditorView.scrollMargins)) { let margins = source(view) if (margins?.top) marginTop = Math.max(margins?.top, marginTop) if (margins?.bottom) marginBottom = Math.max(margins?.bottom, marginBottom) } height = view.scrollDOM.clientHeight - marginTop - marginBottom } else { height = (view.dom.ownerDocument.defaultView || window).innerHeight } return {marginTop, marginBottom, selfScroll, height: Math.max(view.defaultLineHeight, height - 5)} } function cursorByPage(view: EditorView, forward: boolean) { let page = pageInfo(view) let {state} = view, selection = updateSel(state.selection, range => { return range.empty ? view.moveVertically(range, forward, page.height) : rangeEnd(range, forward) }) if (selection.eq(state.selection)) return false let effect if (page.selfScroll) { let startPos = view.coordsAtPos(state.selection.main.head) let scrollRect = view.scrollDOM.getBoundingClientRect() let scrollTop = scrollRect.top + page.marginTop, scrollBottom = scrollRect.bottom - page.marginBottom if (startPos && startPos.top > scrollTop && startPos.bottom < scrollBottom) effect = EditorView.scrollIntoView(selection.main.head, {y: "start", yMargin: startPos.top - scrollTop}) } view.dispatch(setSel(state, selection), {effects: effect}) return true } /// Move the selection one page up. export const cursorPageUp: Command = view => cursorByPage(view, false) /// Move the selection one page down. export const cursorPageDown: Command = view => cursorByPage(view, true) function moveByLineBoundary(view: EditorView, start: SelectionRange, forward: boolean) { let line = view.lineBlockAt(start.head), moved = view.moveToLineBoundary(start, forward) if (moved.head == start.head && moved.head != (forward ? line.to : line.from)) moved = view.moveToLineBoundary(start, forward, false) if (!forward && moved.head == line.from && line.length) { let space = /^\s*/.exec(view.state.sliceDoc(line.from, Math.min(line.from + 100, line.to)))![0].length if (space && start.head != line.from + space) moved = EditorSelection.cursor(line.from + space) } return moved } /// Move the selection to the next line wrap point, or to the end of /// the line if there isn't one left on this line. export const cursorLineBoundaryForward: Command = view => moveSel(view, range => moveByLineBoundary(view, range, true)) /// Move the selection to previous line wrap point, or failing that to /// the start of the line. If the line is indented, and the cursor /// isn't already at the end of the indentation, this will move to the /// end of the indentation instead of the start of the line. export const cursorLineBoundaryBackward: Command = view => moveSel(view, range => moveByLineBoundary(view, range, false)) /// Move the selection one line wrap point to the left. export const cursorLineBoundaryLeft: Command = view => moveSel(view, range => moveByLineBoundary(view, range, !ltrAtCursor(view))) /// Move the selection one line wrap point to the right. export const cursorLineBoundaryRight: Command = view => moveSel(view, range => moveByLineBoundary(view, range, ltrAtCursor(view))) /// Move the selection to the start of the line. export const cursorLineStart: Command = view => moveSel(view, range => EditorSelection.cursor(view.lineBlockAt(range.head).from, 1)) /// Move the selection to the end of the line. export const cursorLineEnd: Command = view => moveSel(view, range => EditorSelection.cursor(view.lineBlockAt(range.head).to, -1)) function toMatchingBracket(state: EditorState, dispatch: (tr: Transaction) => void, extend: boolean) { let found = false, selection = updateSel(state.selection, range => { let matching = matchBrackets(state, range.head, -1) || matchBrackets(state, range.head, 1) || (range.head > 0 && matchBrackets(state, range.head - 1, 1)) || (range.head < state.doc.length && matchBrackets(state, range.head + 1, -1)) if (!matching || !matching.end) return range found = true let head = matching.start.from == range.head ? matching.end.to : matching.end.from return extend ? EditorSelection.range(range.anchor, head) : EditorSelection.cursor(head) }) if (!found) return false dispatch(setSel(state, selection)) return true } /// Move the selection to the bracket matching the one it is currently /// on, if any. export const cursorMatchingBracket: StateCommand = ({state, dispatch}) => toMatchingBracket(state, dispatch, false) /// Extend the selection to the bracket matching the one the selection /// head is currently on, if any. export const selectMatchingBracket: StateCommand = ({state, dispatch}) => toMatchingBracket(state, dispatch, true) function extendSel(view: EditorView, how: (range: SelectionRange) => SelectionRange): boolean { let selection = updateSel(view.state.selection, range => { let head = how(range) return EditorSelection.range(range.anchor, head.head, head.goalColumn, head.bidiLevel || undefined) }) if (selection.eq(view.state.selection)) return false view.dispatch(setSel(view.state, selection)) return true } function selectByChar(view: EditorView, forward: boolean) { return extendSel(view, range => view.moveByChar(range, forward)) } /// Move the selection head one character to the left, while leaving /// the anchor in place. export const selectCharLeft: Command = view => selectByChar(view, !ltrAtCursor(view)) /// Move the selection head one character to the right. export const selectCharRight: Command = view => selectByChar(view, ltrAtCursor(view)) /// Move the selection head one character forward. export const selectCharForward: Command = view => selectByChar(view, true) /// Move the selection head one character backward. export const selectCharBackward: Command = view => selectByChar(view, false) function selectByGroup(view: EditorView, forward: boolean) { return extendSel(view, range => view.moveByGroup(range, forward)) } /// Move the selection head one [group](#commands.cursorGroupLeft) to /// the left. export const selectGroupLeft: Command = view => selectByGroup(view, !ltrAtCursor(view)) /// Move the selection head one group to the right. export const selectGroupRight: Command = view => selectByGroup(view, ltrAtCursor(view)) /// Move the selection head one group forward. export const selectGroupForward: Command = view => selectByGroup(view, true) /// Move the selection head one group backward. export const selectGroupBackward: Command = view => selectByGroup(view, false) function selectBySubword(view: EditorView, forward: boolean) { return extendSel(view, range => moveBySubword(view, range, forward)) } /// Move the selection head one group or camel-case subword forward. export const selectSubwordForward: Command = view => selectBySubword(view, true) /// Move the selection head one group or subword backward. export const selectSubwordBackward: Command = view => selectBySubword(view, false) /// Move the selection head over the next syntactic element to the left. export const selectSyntaxLeft: Command = view => extendSel(view, range => moveBySyntax(view.state, range, !ltrAtCursor(view))) /// Move the selection head over the next syntactic element to the right. export const selectSyntaxRight: Command = view => extendSel(view, range => moveBySyntax(view.state, range, ltrAtCursor(view))) function selectByLine(view: EditorView, forward: boolean) { return extendSel(view, range => view.moveVertically(range, forward)) } /// Move the selection head one line up. export const selectLineUp: Command = view => selectByLine(view, false) /// Move the selection head one line down. export const selectLineDown: Command = view => selectByLine(view, true) function selectByPage(view: EditorView, forward: boolean) { return extendSel(view, range => view.moveVertically(range, forward, pageInfo(view).height)) } /// Move the selection head one page up. export const selectPageUp: Command = view => selectByPage(view, false) /// Move the selection head one page down. export const selectPageDown: Command = view => selectByPage(view, true) /// Move the selection head to the next line boundary. export const selectLineBoundaryForward: Command = view => extendSel(view, range => moveByLineBoundary(view, range, true)) /// Move the selection head to the previous line boundary. export const selectLineBoundaryBackward: Command = view => extendSel(view, range => moveByLineBoundary(view, range, false)) /// Move the selection head one line boundary to the left. export const selectLineBoundaryLeft: Command = view => extendSel(view, range => moveByLineBoundary(view, range, !ltrAtCursor(view))) /// Move the selection head one line boundary to the right. export const selectLineBoundaryRight: Command = view => extendSel(view, range => moveByLineBoundary(view, range, ltrAtCursor(view))) /// Move the selection head to the start of the line. export const selectLineStart: Command = view => extendSel(view, range => EditorSelection.cursor(view.lineBlockAt(range.head).from)) /// Move the selection head to the end of the line. export const selectLineEnd: Command = view => extendSel(view, range => EditorSelection.cursor(view.lineBlockAt(range.head).to)) /// Move the selection to the start of the document. export const cursorDocStart: StateCommand = ({state, dispatch}) => { dispatch(setSel(state, {anchor: 0})) return true } /// Move the selection to the end of the document. export const cursorDocEnd: StateCommand = ({state, dispatch}) => { dispatch(setSel(state, {anchor: state.doc.length})) return true } /// Move the selection head to the start of the document. export const selectDocStart: StateCommand = ({state, dispatch}) => { dispatch(setSel(state, {anchor: state.selection.main.anchor, head: 0})) return true } /// Move the selection head to the end of the document. export const selectDocEnd: StateCommand = ({state, dispatch}) => { dispatch(setSel(state, {anchor: state.selection.main.anchor, head: state.doc.length})) return true } /// Select the entire document. export const selectAll: StateCommand = ({state, dispatch}) => { dispatch(state.update({selection: {anchor: 0, head: state.doc.length}, userEvent: "select"})) return true } /// Expand the selection to cover entire lines. export const selectLine: StateCommand = ({state, dispatch}) => { let ranges = selectedLineBlocks(state).map(({from, to}) => EditorSelection.range(from, Math.min(to + 1, state.doc.length))) dispatch(state.update({selection: EditorSelection.create(ranges), userEvent: "select"})) return true } /// Select the next syntactic construct that is larger than the /// selection. Note that this will only work insofar as the language /// [provider](#language.language) you use builds up a full /// syntax tree. export const selectParentSyntax: StateCommand = ({state, dispatch}) => { let selection = updateSel(state.selection, range => { let stack = syntaxTree(state).resolveStack(range.from, 1) for (let cur: typeof stack | null = stack; cur; cur = cur.next) { let {node} = cur if (((node.from < range.from && node.to >= range.to) || (node.to > range.to && node.from <= range.from)) && node.parent?.parent) return EditorSelection.range(node.to, node.from) } return range }) dispatch(setSel(state, selection)) return true } /// Simplify the current selection. When multiple ranges are selected, /// reduce it to its main range. Otherwise, if the selection is /// non-empty, convert it to a cursor selection. export const simplifySelection: StateCommand = ({state, dispatch}) => { let cur = state.selection, selection = null if (cur.ranges.length > 1) selection = EditorSelection.create([cur.main]) else if (!cur.main.empty) selection = EditorSelection.create([EditorSelection.cursor(cur.main.head)]) if (!selection) return false dispatch(setSel(state, selection)) return true } function deleteBy(target: CommandTarget, by: (start: SelectionRange) => 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(range) 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, from < range.head ? -1 : 1)} }) 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 } const deleteByChar = (target: CommandTarget, forward: boolean, byIndentUnit: boolean) => deleteBy(target, range => { let pos = range.from, {state} = target, line = state.doc.lineAt(pos), before, targetPos: number if (byIndentUnit && !forward && pos > line.from && pos < line.from + 200 && !/[^ \t]/.test(before = line.text.slice(0, pos - line.from))) { if (before[before.length - 1] == "\t") return pos - 1 let col = countColumn(before, state.tabSize), drop = col % getIndentUnit(state) || getIndentUnit(state) for (let i = 0; i < drop && before[before.length - 1 - i] == " "; i++) pos-- targetPos = pos } else { targetPos = findClusterBreak(line.text, pos - line.from, forward, forward) + line.from if (targetPos == pos && line.number != (forward ? state.doc.lines : 1)) targetPos += forward ? 1 : -1 else if (!forward && /[\ufe00-\ufe0f]/.test(line.text.slice(targetPos - line.from, pos - line.from))) targetPos = findClusterBreak(line.text, targetPos - line.from, false, false) + line.from } return targetPos }) /// Delete the selection, or, for cursor selections, the character or /// indentation unit before the cursor. export const deleteCharBackward: Command = view => deleteByChar(view, false, true) /// Delete the selection or the character before the cursor. Does not /// implement any extended behavior like deleting whole indentation /// units in one go. export const deleteCharBackwardStrict: Command = view => deleteByChar(view, false, false) /// Delete the selection or the character after the cursor. export const deleteCharForward: Command = view => deleteByChar(view, true, false) const deleteByGroup = (target: CommandTarget, forward: boolean) => deleteBy(target, range => { let pos = range.head, {state} = target, line = state.doc.lineAt(pos) let categorize = state.charCategorizer(pos) for (let cat: CharCategory | null = null;;) { if (pos == (forward ? line.to : line.from)) { if (pos == range.head && line.number != (forward ? state.doc.lines : 1)) pos += forward ? 1 : -1 break } let next = findClusterBreak(line.text, pos - line.from, forward) + line.from let nextChar = line.text.slice(Math.min(pos, next) - line.from, Math.max(pos, next) - line.from) let nextCat = categorize(nextChar) if (cat != null && nextCat != cat) break if (nextChar != " " || pos != range.head) cat = nextCat pos = next } return pos }) /// Delete the selection or backward until the end of the next /// [group](#view.EditorView.moveByGroup), only skipping groups of /// whitespace when they consist of a single space. export const deleteGroupBackward: StateCommand = target => deleteByGroup(target, false) /// Delete the selection or forward until the end of the next group. export const deleteGroupForward: StateCommand = target => deleteByGroup(target, true) /// Delete the selection, or, if it is a cursor selection, delete to /// the end of the line. If the cursor is directly at the end of the /// line, delete the line break after it. export const deleteToLineEnd: Command = view => deleteBy(view, range => { let lineEnd = view.lineBlockAt(range.head).to return range.head < lineEnd ? lineEnd : Math.min(view.state.doc.length, range.head + 1) }) /// Delete the selection, or, if it is a cursor selection, delete to /// the start of the line. If the cursor is directly at the start of the /// line, delete the line break before it. export const deleteToLineStart: Command = view => deleteBy(view, range => { let lineStart = view.lineBlockAt(range.head).from return range.head > lineStart ? lineStart : Math.max(0, range.head - 1) }) /// Delete the selection, or, if it is a cursor selection, delete to /// the start of the line or the next line wrap before the cursor. export const deleteLineBoundaryBackward: Command = view => deleteBy(view, range => { let lineStart = view.moveToLineBoundary(range, false).head return range.head > lineStart ? lineStart : Math.max(0, range.head - 1) }) /// Delete the selection, or, if it is a cursor selection, delete to /// the end of the line or the next line wrap after the cursor. export const deleteLineBoundaryForward: Command = view => deleteBy(view, range => { let lineStart = view.moveToLineBoundary(range, true).head return range.head < lineStart ? lineStart : Math.min(view.state.doc.length, range.head + 1) }) /// Delete all whitespace directly before a line end from the /// document. export const deleteTrailingWhitespace: StateCommand = ({state, dispatch}) => { if (state.readOnly) return false let changes = [] for (let pos = 0, prev = "", iter = state.doc.iter();;) { iter.next() if (iter.lineBreak || iter.done) { let trailing = prev.search(/\s+$/) if (trailing > -1) changes.push({from: pos - (prev.length - trailing), to: pos}) if (iter.done) break prev = "" } else { prev = iter.value } pos += iter.value.length } if (!changes.length) return false dispatch(state.update({changes, userEvent: "delete"})) return true } /// Replace each selection range with a line break, leaving the cursor /// on the line before the break. export const splitLine: StateCommand = ({state, dispatch}) => { if (state.readOnly) return false let changes = state.changeByRange(range => { return {changes: {from: range.from, to: range.to, insert: Text.of(["", ""])}, range: EditorSelection.cursor(range.from)} }) dispatch(state.update(changes, {scrollIntoView: true, userEvent: "input"})) return true } /// Flip the characters before and after the cursor(s). export const transposeChars: StateCommand = ({state, dispatch}) => { if (state.readOnly) return false let changes = state.changeByRange(range => { if (!range.empty || range.from == 0 || range.from == state.doc.length) return {range} let pos = range.from, line = state.doc.lineAt(pos) let from = pos == line.from ? pos - 1 : findClusterBreak(line.text, pos - line.from, false) + line.from let to = pos == line.to ? pos + 1 : findClusterBreak(line.text, pos - line.from, true) + line.from return {changes: {from, to, insert: state.doc.slice(pos, to).append(state.doc.slice(from, pos))}, range: EditorSelection.cursor(to)} }) if (changes.changes.empty) return false dispatch(state.update(changes, {scrollIntoView: true, userEvent: "move.character"})) return true } function selectedLineBlocks(state: EditorState) { let blocks = [], upto = -1 for (let range of state.selection.ranges) { let startLine = state.doc.lineAt(range.from), endLine = state.doc.lineAt(range.to) if (!range.empty && range.to == endLine.from) endLine = state.doc.lineAt(range.to - 1) if (upto >= startLine.number) { let prev = blocks[blocks.length - 1] prev.to = endLine.to prev.ranges.push(range) } else { blocks.push({from: startLine.from, to: endLine.to, ranges: [range]}) } upto = endLine.number + 1 } return blocks } function moveLine(state: EditorState, dispatch: (tr: Transaction) => void, forward: boolean): boolean { if (state.readOnly) return false let changes = [], ranges = [] for (let block of selectedLineBlocks(state)) { if (forward ? block.to == state.doc.length : block.from == 0) continue let nextLine = state.doc.lineAt(forward ? block.to + 1 : block.from - 1) let size = nextLine.length + 1 if (forward) { changes.push({from: block.to, to: nextLine.to}, {from: block.from, insert: nextLine.text + state.lineBreak}) for (let r of block.ranges) ranges.push(EditorSelection.range(Math.min(state.doc.length, r.anchor + size), Math.min(state.doc.length, r.head + size))) } else { changes.push({from: nextLine.from, to: block.from}, {from: block.to, insert: state.lineBreak + nextLine.text}) for (let r of block.ranges) ranges.push(EditorSelection.range(r.anchor - size, r.head - size)) } } if (!changes.length) return false dispatch(state.update({ changes, scrollIntoView: true, selection: EditorSelection.create(ranges, state.selection.mainIndex), userEvent: "move.line" })) return true } /// Move the selected lines up one line. export const moveLineUp: StateCommand = ({state, dispatch}) => moveLine(state, dispatch, false) /// Move the selected lines down one line. export const moveLineDown: StateCommand = ({state, dispatch}) => moveLine(state, dispatch, true) function copyLine(state: EditorState, dispatch: (tr: Transaction) => void, forward: boolean): boolean { if (state.readOnly) return false let changes = [] for (let block of selectedLineBlocks(state)) { if (forward) changes.push({from: block.from, insert: state.doc.slice(block.from, block.to) + state.lineBreak}) else changes.push({from: block.to, insert: state.lineBreak + state.doc.slice(block.from, block.to)}) } dispatch(state.update({changes, scrollIntoView: true, userEvent: "input.copyline"})) return true } /// Create a copy of the selected lines. Keep the selection in the top copy. export const copyLineUp: StateCommand = ({state, dispatch}) => copyLine(state, dispatch, false) /// Create a copy of the selected lines. Keep the selection in the bottom copy. export const copyLineDown: StateCommand = ({state, dispatch}) => copyLine(state, dispatch, true) /// Delete selected lines. export const deleteLine: Command = view => { if (view.state.readOnly) return false let {state} = view, changes = state.changes(selectedLineBlocks(state).map(({from, to}) => { if (from > 0) from-- else if (to < state.doc.length) to++ return {from, to} })) let selection = updateSel(state.selection, range => { let dist: number | undefined = undefined if (view.lineWrapping) { let block = view.lineBlockAt(range.head), pos = view.coordsAtPos(range.head, range.assoc || 1) if (pos) dist = (block.bottom + view.documentTop) - pos.bottom + view.defaultLineHeight / 2 } return view.moveVertically(range, true, dist) }).map(changes) view.dispatch({changes, selection, scrollIntoView: true, userEvent: "delete.line"}) return true } /// Replace the selection with a newline. export const insertNewline: StateCommand = ({state, dispatch}) => { dispatch(state.update(state.replaceSelection(state.lineBreak), {scrollIntoView: true, userEvent: "input"})) return true } /// Replace the selection with a newline and the same amount of /// indentation as the line above. export const insertNewlineKeepIndent: StateCommand = ({state, dispatch}) => { dispatch(state.update(state.changeByRange(range => { let indent = /^\s*/.exec(state.doc.lineAt(range.from).text)![0] return { changes: {from: range.from, to: range.to, insert: state.lineBreak + indent}, range: EditorSelection.cursor(range.from + indent.length + 1) } }), {scrollIntoView: true, userEvent: "input"})) return true } function isBetweenBrackets(state: EditorState, pos: number): {from: number, to: number} | null { if (/\(\)|\[\]|\{\}/.test(state.sliceDoc(pos - 1, pos + 1))) return {from: pos, to: pos} let context = syntaxTree(state).resolveInner(pos) let before = context.childBefore(pos), after = context.childAfter(pos), closedBy if (before && after && before.to <= pos && after.from >= pos && (closedBy = before.type.prop(NodeProp.closedBy)) && closedBy.indexOf(after.name) > -1 && state.doc.lineAt(before.to).from == state.doc.lineAt(after.from).from && !/\S/.test(state.sliceDoc(before.to, after.from))) return {from: before.to, to: after.from} return null } /// Replace the selection with a newline and indent the newly created /// line(s). If the current line consists only of whitespace, this /// will also delete that whitespace. When the cursor is between /// matching brackets, an additional newline will be inserted after /// the cursor. export const insertNewlineAndIndent = newlineAndIndent(false) /// Create a blank, indented line below the current line. export const insertBlankLine = newlineAndIndent(true) function newlineAndIndent(atEof: boolean): StateCommand { return ({state, dispatch}): boolean => { if (state.readOnly) return false let changes = state.changeByRange(range => { let {from, to} = range, line = state.doc.lineAt(from) let explode = !atEof && from == to && isBetweenBrackets(state, from) if (atEof) from = to = (to <= line.to ? line : state.doc.lineAt(to)).to let cx = new IndentContext(state, {simulateBreak: from, simulateDoubleBreak: !!explode}) let indent = getIndentation(cx, from) if (indent == null) indent = countColumn(/^\s*/.exec(state.doc.lineAt(from).text)![0], state.tabSize) while (to < line.to && /\s/.test(line.text[to - line.from])) to++ if (explode) ({from, to} = explode) else if (from > line.from && from < line.from + 100 && !/\S/.test(line.text.slice(0, from))) from = line.from let insert = ["", indentString(state, indent)] if (explode) insert.push(indentString(state, cx.lineIndent(line.from, -1))) return {changes: {from, to, insert: Text.of(insert)}, range: EditorSelection.cursor(from + 1 + insert[1].length)} }) dispatch(state.update(changes, {scrollIntoView: true, userEvent: "input"})) return true } } function changeBySelectedLine(state: EditorState, f: (line: Line, changes: ChangeSpec[], range: SelectionRange) => void) { let atLine = -1 return state.changeByRange(range => { let changes: ChangeSpec[] = [] for (let pos = range.from; pos <= range.to;) { let line = state.doc.lineAt(pos) if (line.number > atLine && (range.empty || range.to > line.from)) { f(line, changes, range) atLine = line.number } pos = line.to + 1 } let changeSet = state.changes(changes) return {changes, range: EditorSelection.range(changeSet.mapPos(range.anchor, 1), changeSet.mapPos(range.head, 1))} }) } /// Auto-indent the selected lines. This uses the [indentation service /// facet](#language.indentService) as source for auto-indent /// information. export const indentSelection: StateCommand = ({state, dispatch}) => { if (state.readOnly) return false let updated: {[lineStart: number]: number} = Object.create(null) let context = new IndentContext(state, {overrideIndentation: start => { let found = updated[start] return found == null ? -1 : found }}) let changes = changeBySelectedLine(state, (line, changes, range) => { let indent = getIndentation(context, line.from) if (indent == null) return if (!/\S/.test(line.text)) indent = 0 let cur = /^\s*/.exec(line.text)![0] let norm = indentString(state, indent) if (cur != norm || range.from < line.from + cur.length) { updated[line.from] = indent changes.push({from: line.from, to: line.from + cur.length, insert: norm}) } }) if (!changes.changes!.empty) dispatch(state.update(changes, {userEvent: "indent"})) return true } /// Add a [unit](#language.indentUnit) of indentation to all selected /// lines. export const indentMore: StateCommand = ({state, dispatch}) => { if (state.readOnly) return false dispatch(state.update(changeBySelectedLine(state, (line, changes) => { changes.push({from: line.from, insert: state.facet(indentUnit)}) }), {userEvent: "input.indent"})) return true } /// Remove a [unit](#language.indentUnit) of indentation from all /// selected lines. export const indentLess: StateCommand = ({state, dispatch}) => { if (state.readOnly) return false dispatch(state.update(changeBySelectedLine(state, (line, changes) => { let space = /^\s*/.exec(line.text)![0] if (!space) return let col = countColumn(space, state.tabSize), keep = 0 let insert = indentString(state, Math.max(0, col - getIndentUnit(state))) while (keep < space.length && keep < insert.length && space.charCodeAt(keep) == insert.charCodeAt(keep)) keep++ changes.push({from: line.from + keep, to: line.from + space.length, insert: insert.slice(keep)}) }), {userEvent: "delete.dedent"})) return true } /// Enables or disables /// [tab-focus mode](#view.EditorView.setTabFocusMode). While on, this /// prevents the editor's key bindings from capturing Tab or /// Shift-Tab, making it possible for the user to move focus out of /// the editor with the keyboard. export const toggleTabFocusMode: Command = view => { view.setTabFocusMode() return true } /// Temporarily enables [tab-focus /// mode](#view.EditorView.setTabFocusMode) for two seconds or until /// another key is pressed. export const temporarilySetTabFocusMode: Command = view => { view.setTabFocusMode(2000) return true } /// Insert a tab character at the cursor or, if something is selected, /// use [`indentMore`](#commands.indentMore) to indent the entire /// selection. export const insertTab: StateCommand = ({state, dispatch}) => { if (state.selection.ranges.some(r => !r.empty)) return indentMore({state, dispatch}) dispatch(state.update(state.replaceSelection("\t"), {scrollIntoView: true, userEvent: "input"})) return true } /// Array of key bindings containing the Emacs-style bindings that are /// available on macOS by default. /// /// - Ctrl-b: [`cursorCharLeft`](#commands.cursorCharLeft) ([`selectCharLeft`](#commands.selectCharLeft) with Shift) /// - Ctrl-f: [`cursorCharRight`](#commands.cursorCharRight) ([`selectCharRight`](#commands.selectCharRight) with Shift) /// - Ctrl-p: [`cursorLineUp`](#commands.cursorLineUp) ([`selectLineUp`](#commands.selectLineUp) with Shift) /// - Ctrl-n: [`cursorLineDown`](#commands.cursorLineDown) ([`selectLineDown`](#commands.selectLineDown) with Shift) /// - Ctrl-a: [`cursorLineStart`](#commands.cursorLineStart) ([`selectLineStart`](#commands.selectLineStart) with Shift) /// - Ctrl-e: [`cursorLineEnd`](#commands.cursorLineEnd) ([`selectLineEnd`](#commands.selectLineEnd) with Shift) /// - Ctrl-d: [`deleteCharForward`](#commands.deleteCharForward) /// - Ctrl-h: [`deleteCharBackward`](#commands.deleteCharBackward) /// - Ctrl-k: [`deleteToLineEnd`](#commands.deleteToLineEnd) /// - Ctrl-Alt-h: [`deleteGroupBackward`](#commands.deleteGroupBackward) /// - Ctrl-o: [`splitLine`](#commands.splitLine) /// - Ctrl-t: [`transposeChars`](#commands.transposeChars) /// - Ctrl-v: [`cursorPageDown`](#commands.cursorPageDown) /// - Alt-v: [`cursorPageUp`](#commands.cursorPageUp) export const emacsStyleKeymap: readonly KeyBinding[] = [ {key: "Ctrl-b", run: cursorCharLeft, shift: selectCharLeft, preventDefault: true}, {key: "Ctrl-f", run: cursorCharRight, shift: selectCharRight}, {key: "Ctrl-p", run: cursorLineUp, shift: selectLineUp}, {key: "Ctrl-n", run: cursorLineDown, shift: selectLineDown}, {key: "Ctrl-a", run: cursorLineStart, shift: selectLineStart}, {key: "Ctrl-e", run: cursorLineEnd, shift: selectLineEnd}, {key: "Ctrl-d", run: deleteCharForward}, {key: "Ctrl-h", run: deleteCharBackward}, {key: "Ctrl-k", run: deleteToLineEnd}, {key: "Ctrl-Alt-h", run: deleteGroupBackward}, {key: "Ctrl-o", run: splitLine}, {key: "Ctrl-t", run: transposeChars}, {key: "Ctrl-v", run: cursorPageDown}, ] /// An array of key bindings closely sticking to platform-standard or /// widely used bindings. (This includes the bindings from /// [`emacsStyleKeymap`](#commands.emacsStyleKeymap), with their `key` /// property changed to `mac`.) /// /// - ArrowLeft: [`cursorCharLeft`](#commands.cursorCharLeft) ([`selectCharLeft`](#commands.selectCharLeft) with Shift) /// - ArrowRight: [`cursorCharRight`](#commands.cursorCharRight) ([`selectCharRight`](#commands.selectCharRight) with Shift) /// - Ctrl-ArrowLeft (Alt-ArrowLeft on macOS): [`cursorGroupLeft`](#commands.cursorGroupLeft) ([`selectGroupLeft`](#commands.selectGroupLeft) with Shift) /// - Ctrl-ArrowRight (Alt-ArrowRight on macOS): [`cursorGroupRight`](#commands.cursorGroupRight) ([`selectGroupRight`](#commands.selectGroupRight) with Shift) /// - Cmd-ArrowLeft (on macOS): [`cursorLineStart`](#commands.cursorLineStart) ([`selectLineStart`](#commands.selectLineStart) with Shift) /// - Cmd-ArrowRight (on macOS): [`cursorLineEnd`](#commands.cursorLineEnd) ([`selectLineEnd`](#commands.selectLineEnd) with Shift) /// - ArrowUp: [`cursorLineUp`](#commands.cursorLineUp) ([`selectLineUp`](#commands.selectLineUp) with Shift) /// - ArrowDown: [`cursorLineDown`](#commands.cursorLineDown) ([`selectLineDown`](#commands.selectLineDown) with Shift) /// - Cmd-ArrowUp (on macOS): [`cursorDocStart`](#commands.cursorDocStart) ([`selectDocStart`](#commands.selectDocStart) with Shift) /// - Cmd-ArrowDown (on macOS): [`cursorDocEnd`](#commands.cursorDocEnd) ([`selectDocEnd`](#commands.selectDocEnd) with Shift) /// - Ctrl-ArrowUp (on macOS): [`cursorPageUp`](#commands.cursorPageUp) ([`selectPageUp`](#commands.selectPageUp) with Shift) /// - Ctrl-ArrowDown (on macOS): [`cursorPageDown`](#commands.cursorPageDown) ([`selectPageDown`](#commands.selectPageDown) with Shift) /// - PageUp: [`cursorPageUp`](#commands.cursorPageUp) ([`selectPageUp`](#commands.selectPageUp) with Shift) /// - PageDown: [`cursorPageDown`](#commands.cursorPageDown) ([`selectPageDown`](#commands.selectPageDown) with Shift) /// - Home: [`cursorLineBoundaryBackward`](#commands.cursorLineBoundaryBackward) ([`selectLineBoundaryBackward`](#commands.selectLineBoundaryBackward) with Shift) /// - End: [`cursorLineBoundaryForward`](#commands.cursorLineBoundaryForward) ([`selectLineBoundaryForward`](#commands.selectLineBoundaryForward) with Shift) /// - Ctrl-Home (Cmd-Home on macOS): [`cursorDocStart`](#commands.cursorDocStart) ([`selectDocStart`](#commands.selectDocStart) with Shift) /// - Ctrl-End (Cmd-Home on macOS): [`cursorDocEnd`](#commands.cursorDocEnd) ([`selectDocEnd`](#commands.selectDocEnd) with Shift) /// - Enter: [`insertNewlineAndIndent`](#commands.insertNewlineAndIndent) /// - Ctrl-a (Cmd-a on macOS): [`selectAll`](#commands.selectAll) /// - Backspace: [`deleteCharBackward`](#commands.deleteCharBackward) /// - Delete: [`deleteCharForward`](#commands.deleteCharForward) /// - Ctrl-Backspace (Alt-Backspace on macOS): [`deleteGroupBackward`](#commands.deleteGroupBackward) /// - Ctrl-Delete (Alt-Delete on macOS): [`deleteGroupForward`](#commands.deleteGroupForward) /// - Cmd-Backspace (macOS): [`deleteLineBoundaryBackward`](#commands.deleteLineBoundaryBackward). /// - Cmd-Delete (macOS): [`deleteLineBoundaryForward`](#commands.deleteLineBoundaryForward). export const standardKeymap: readonly KeyBinding[] = ([ {key: "ArrowLeft", run: cursorCharLeft, shift: selectCharLeft, preventDefault: true}, {key: "Mod-ArrowLeft", mac: "Alt-ArrowLeft", run: cursorGroupLeft, shift: selectGroupLeft, preventDefault: true}, {mac: "Cmd-ArrowLeft", run: cursorLineBoundaryLeft, shift: selectLineBoundaryLeft, preventDefault: true}, {key: "ArrowRight", run: cursorCharRight, shift: selectCharRight, preventDefault: true}, {key: "Mod-ArrowRight", mac: "Alt-ArrowRight", run: cursorGroupRight, shift: selectGroupRight, preventDefault: true}, {mac: "Cmd-ArrowRight", run: cursorLineBoundaryRight, shift: selectLineBoundaryRight, preventDefault: true}, {key: "ArrowUp", run: cursorLineUp, shift: selectLineUp, preventDefault: true}, {mac: "Cmd-ArrowUp", run: cursorDocStart, shift: selectDocStart}, {mac: "Ctrl-ArrowUp", run: cursorPageUp, shift: selectPageUp}, {key: "ArrowDown", run: cursorLineDown, shift: selectLineDown, preventDefault: true}, {mac: "Cmd-ArrowDown", run: cursorDocEnd, shift: selectDocEnd}, {mac: "Ctrl-ArrowDown", run: cursorPageDown, shift: selectPageDown}, {key: "PageUp", run: cursorPageUp, shift: selectPageUp}, {key: "PageDown", run: cursorPageDown, shift: selectPageDown}, {key: "Home", run: cursorLineBoundaryBackward, shift: selectLineBoundaryBackward, preventDefault: true}, {key: "Mod-Home", run: cursorDocStart, shift: selectDocStart}, {key: "End", run: cursorLineBoundaryForward, shift: selectLineBoundaryForward, preventDefault: true}, {key: "Mod-End", run: cursorDocEnd, shift: selectDocEnd}, {key: "Enter", run: insertNewlineAndIndent}, {key: "Mod-a", run: selectAll}, {key: "Backspace", run: deleteCharBackward, shift: deleteCharBackward}, {key: "Delete", run: deleteCharForward}, {key: "Mod-Backspace", mac: "Alt-Backspace", run: deleteGroupBackward}, {key: "Mod-Delete", mac: "Alt-Delete", run: deleteGroupForward}, {mac: "Mod-Backspace", run: deleteLineBoundaryBackward}, {mac: "Mod-Delete", run: deleteLineBoundaryForward} ] as KeyBinding[]).concat(emacsStyleKeymap.map(b => ({mac: b.key, run: b.run, shift: b.shift}))) /// The default keymap. Includes all bindings from /// [`standardKeymap`](#commands.standardKeymap) plus the following: /// /// - Alt-ArrowLeft (Ctrl-ArrowLeft on macOS): [`cursorSyntaxLeft`](#commands.cursorSyntaxLeft) ([`selectSyntaxLeft`](#commands.selectSyntaxLeft) with Shift) /// - Alt-ArrowRight (Ctrl-ArrowRight on macOS): [`cursorSyntaxRight`](#commands.cursorSyntaxRight) ([`selectSyntaxRight`](#commands.selectSyntaxRight) with Shift) /// - Alt-ArrowUp: [`moveLineUp`](#commands.moveLineUp) /// - Alt-ArrowDown: [`moveLineDown`](#commands.moveLineDown) /// - Shift-Alt-ArrowUp: [`copyLineUp`](#commands.copyLineUp) /// - Shift-Alt-ArrowDown: [`copyLineDown`](#commands.copyLineDown) /// - Escape: [`simplifySelection`](#commands.simplifySelection) /// - Ctrl-Enter (Cmd-Enter on macOS): [`insertBlankLine`](#commands.insertBlankLine) /// - Alt-l (Ctrl-l on macOS): [`selectLine`](#commands.selectLine) /// - Ctrl-i (Cmd-i on macOS): [`selectParentSyntax`](#commands.selectParentSyntax) /// - Ctrl-[ (Cmd-[ on macOS): [`indentLess`](#commands.indentLess) /// - Ctrl-] (Cmd-] on macOS): [`indentMore`](#commands.indentMore) /// - Ctrl-Alt-\\ (Cmd-Alt-\\ on macOS): [`indentSelection`](#commands.indentSelection) /// - Shift-Ctrl-k (Shift-Cmd-k on macOS): [`deleteLine`](#commands.deleteLine) /// - Shift-Ctrl-\\ (Shift-Cmd-\\ on macOS): [`cursorMatchingBracket`](#commands.cursorMatchingBracket) /// - Ctrl-/ (Cmd-/ on macOS): [`toggleComment`](#commands.toggleComment). /// - Shift-Alt-a: [`toggleBlockComment`](#commands.toggleBlockComment). /// - Ctrl-m (Alt-Shift-m on macOS): [`toggleTabFocusMode`](#commands.toggleTabFocusMode). export const defaultKeymap: readonly KeyBinding[] = ([ {key: "Alt-ArrowLeft", mac: "Ctrl-ArrowLeft", run: cursorSyntaxLeft, shift: selectSyntaxLeft}, {key: "Alt-ArrowRight", mac: "Ctrl-ArrowRight", run: cursorSyntaxRight, shift: selectSyntaxRight}, {key: "Alt-ArrowUp", run: moveLineUp}, {key: "Shift-Alt-ArrowUp", run: copyLineUp}, {key: "Alt-ArrowDown", run: moveLineDown}, {key: "Shift-Alt-ArrowDown", run: copyLineDown}, {key: "Escape", run: simplifySelection}, {key: "Mod-Enter", run: insertBlankLine}, {key: "Alt-l", mac: "Ctrl-l", run: selectLine}, {key: "Mod-i", run: selectParentSyntax, preventDefault: true}, {key: "Mod-[", run: indentLess}, {key: "Mod-]", run: indentMore}, {key: "Mod-Alt-\\", run: indentSelection}, {key: "Shift-Mod-k", run: deleteLine}, {key: "Shift-Mod-\\", run: cursorMatchingBracket}, {key: "Mod-/", run: toggleComment}, {key: "Alt-A", run: toggleBlockComment}, {key: "Ctrl-m", mac: "Shift-Alt-m", run: toggleTabFocusMode}, ] as readonly KeyBinding[]).concat(standardKeymap) /// A binding that binds Tab to [`indentMore`](#commands.indentMore) and /// Shift-Tab to [`indentLess`](#commands.indentLess). /// Please see the [Tab example](../../examples/tab/) before using /// this. export const indentWithTab: KeyBinding = {key: "Tab", run: indentMore, shift: indentLess} commands-6.6.0/src/comment.ts000066400000000000000000000203351462762217600161450ustar00rootroot00000000000000import {Line, EditorState, TransactionSpec, StateCommand} from "@codemirror/state" /// An object of this type can be provided as [language /// data](#state.EditorState.languageDataAt) under a `"commentTokens"` /// property to configure comment syntax for a language. export interface CommentTokens { /// The block comment syntax, if any. For example, for HTML /// you'd provide `{open: ""}`. block?: {open: string, close: string}, /// The line comment syntax. For example `"//"`. line?: string } /// Comment or uncomment the current selection. Will use line comments /// if available, otherwise falling back to block comments. export const toggleComment: StateCommand = target => { let {state} = target, line = state.doc.lineAt(state.selection.main.from), config = getConfig(target.state, line.from) return config.line ? toggleLineComment(target) : config.block ? toggleBlockCommentByLine(target) : false } const enum CommentOption { Toggle, Comment, Uncomment } function command(f: (option: CommentOption, state: EditorState) => TransactionSpec | null, option: CommentOption): StateCommand { return ({state, dispatch}) => { if (state.readOnly) return false let tr = f(option, state) if (!tr) return false dispatch(state.update(tr)) return true } } /// Comment or uncomment the current selection using line comments. /// The line comment syntax is taken from the /// [`commentTokens`](#commands.CommentTokens) [language /// data](#state.EditorState.languageDataAt). export const toggleLineComment = command(changeLineComment, CommentOption.Toggle) /// Comment the current selection using line comments. export const lineComment = command(changeLineComment, CommentOption.Comment) /// Uncomment the current selection using line comments. export const lineUncomment = command(changeLineComment, CommentOption.Uncomment) /// Comment or uncomment the current selection using block comments. /// The block comment syntax is taken from the /// [`commentTokens`](#commands.CommentTokens) [language /// data](#state.EditorState.languageDataAt). export const toggleBlockComment = command(changeBlockComment, CommentOption.Toggle) /// Comment the current selection using block comments. export const blockComment = command(changeBlockComment, CommentOption.Comment) /// Uncomment the current selection using block comments. export const blockUncomment = command(changeBlockComment, CommentOption.Uncomment) /// Comment or uncomment the lines around the current selection using /// block comments. export const toggleBlockCommentByLine = command((o, s) => changeBlockComment(o, s, selectedLineRanges(s)), CommentOption.Toggle) function getConfig(state: EditorState, pos: number) { let data = state.languageDataAt("commentTokens", pos) return data.length ? data[0] : {} } type BlockToken = {open: string, close: string} type BlockComment = { open: {pos: number, margin: number}, close: {pos: number, margin: number} } const SearchMargin = 50 /// Determines if the given range is block-commented in the given /// state. function findBlockComment( state: EditorState, {open, close}: BlockToken, from: number, to: number ): BlockComment | null { let textBefore = state.sliceDoc(from - SearchMargin, from) let textAfter = state.sliceDoc(to, to + SearchMargin) let spaceBefore = /\s*$/.exec(textBefore)![0].length, spaceAfter = /^\s*/.exec(textAfter)![0].length let beforeOff = textBefore.length - spaceBefore if (textBefore.slice(beforeOff - open.length, beforeOff) == open && textAfter.slice(spaceAfter, spaceAfter + close.length) == close) { return {open: {pos: from - spaceBefore, margin: spaceBefore && 1}, close: {pos: to + spaceAfter, margin: spaceAfter && 1}} } let startText: string, endText: string if (to - from <= 2 * SearchMargin) { startText = endText = state.sliceDoc(from, to) } else { startText = state.sliceDoc(from, from + SearchMargin) endText = state.sliceDoc(to - SearchMargin, to) } let startSpace = /^\s*/.exec(startText)![0].length, endSpace = /\s*$/.exec(endText)![0].length let endOff = endText.length - endSpace - close.length if (startText.slice(startSpace, startSpace + open.length) == open && endText.slice(endOff, endOff + close.length) == close) { return {open: {pos: from + startSpace + open.length, margin: /\s/.test(startText.charAt(startSpace + open.length)) ? 1 : 0}, close: {pos: to - endSpace - close.length, margin: /\s/.test(endText.charAt(endOff - 1)) ? 1 : 0}} } return null } function selectedLineRanges(state: EditorState) { let ranges: {from: number, to: number}[] = [] for (let r of state.selection.ranges) { let fromLine = state.doc.lineAt(r.from) let toLine = r.to <= fromLine.to ? fromLine : state.doc.lineAt(r.to) let last = ranges.length - 1 if (last >= 0 && ranges[last].to > fromLine.from) ranges[last].to = toLine.to else ranges.push({from: fromLine.from + /^\s*/.exec(fromLine.text)![0].length, to: toLine.to}) } return ranges } // Performs toggle, comment and uncomment of block comments in // languages that support them. function changeBlockComment( option: CommentOption, state: EditorState, ranges: readonly {from: number, to: number}[] = state.selection.ranges, ) { let tokens = ranges.map(r => getConfig(state, r.from).block) as {open: string, close: string}[] if (!tokens.every(c => c)) return null let comments = ranges.map((r, i) => findBlockComment(state, tokens[i], r.from, r.to)) if (option != CommentOption.Uncomment && !comments.every(c => c)) { return {changes: state.changes(ranges.map((range, i) => { if (comments[i]) return [] return [{from: range.from, insert: tokens[i].open + " "}, {from: range.to, insert: " " + tokens[i].close}] }))} } else if (option != CommentOption.Comment && comments.some(c => c)) { let changes = [] for (let i = 0, comment; i < comments.length; i++) if (comment = comments[i]) { let token = tokens[i], {open, close} = comment changes.push( {from: open.pos - token.open.length, to: open.pos + open.margin}, {from: close.pos - close.margin, to: close.pos + token.close.length} ) } return {changes} } return null } // Performs toggle, comment and uncomment of line comments. function changeLineComment( option: CommentOption, state: EditorState, ranges: readonly {from: number, to: number}[] = state.selection.ranges, ): TransactionSpec | null { let lines: {line: Line, token: string, comment: number, empty: boolean, indent: number, single: boolean}[] = [] let prevLine = -1 for (let {from, to} of ranges) { let startI = lines.length, minIndent = 1e9 let token = getConfig(state, from).line if (!token) continue for (let pos = from; pos <= to;) { let line = state.doc.lineAt(pos) if (line.from > prevLine && (from == to || to > line.from)) { prevLine = line.from let indent = /^\s*/.exec(line.text)![0].length let empty = indent == line.length let comment = line.text.slice(indent, indent + token.length) == token ? indent : -1 if (indent < line.text.length && indent < minIndent) minIndent = indent lines.push({line, comment, token, indent, empty, single: false}) } pos = line.to + 1 } if (minIndent < 1e9) for (let i = startI; i < lines.length; i++) if (lines[i].indent < lines[i].line.text.length) lines[i].indent = minIndent if (lines.length == startI + 1) lines[startI].single = true } if (option != CommentOption.Uncomment && lines.some(l => l.comment < 0 && (!l.empty || l.single))) { let changes = [] for (let {line, token, indent, empty, single} of lines) if (single || !empty) changes.push({from: line.from + indent, insert: token + " "}) let changeSet = state.changes(changes) return {changes: changeSet, selection: state.selection.map(changeSet, 1)} } else if (option != CommentOption.Comment && lines.some(l => l.comment >= 0)) { let changes = [] for (let {line, comment, token} of lines) if (comment >= 0) { let from = line.from + comment, to = from + token.length if (line.text[to - line.from] == " ") to++ changes.push({from, to}) } return {changes} } return null } commands-6.6.0/src/history.ts000066400000000000000000000376641462762217600162210ustar00rootroot00000000000000import {combineConfig, EditorState, Transaction, StateField, StateCommand, StateEffect, Facet, Annotation, Extension, ChangeSet, ChangeDesc, EditorSelection} from "@codemirror/state" import {KeyBinding, EditorView} from "@codemirror/view" const enum BranchName { Done, Undone } const fromHistory = Annotation.define<{side: BranchName, rest: Branch, selection: EditorSelection}>() /// Transaction annotation that will prevent that transaction from /// being combined with other transactions in the undo history. Given /// `"before"`, it'll prevent merging with previous transactions. With /// `"after"`, subsequent transactions won't be combined with this /// one. With `"full"`, the transaction is isolated on both sides. export const isolateHistory = Annotation.define<"before" | "after" | "full">() /// This facet provides a way to register functions that, given a /// transaction, provide a set of effects that the history should /// store when inverting the transaction. This can be used to /// integrate some kinds of effects in the history, so that they can /// be undone (and redone again). export const invertedEffects = Facet.define<(tr: Transaction) => readonly StateEffect[]>() interface HistoryConfig { /// The minimum depth (amount of events) to store. Defaults to 100. minDepth?: number /// The maximum time (in milliseconds) that adjacent events can be /// apart and still be grouped together. Defaults to 500. newGroupDelay?: number /// By default, when close enough together in time, changes are /// joined into an existing undo event if they touch any of the /// changed ranges from that event. You can pass a custom predicate /// here to influence that logic. joinToEvent?: (tr: Transaction, isAdjacent: boolean) => boolean } const historyConfig = Facet.define>({ combine(configs) { return combineConfig(configs, { minDepth: 100, newGroupDelay: 500, joinToEvent: (_t, isAdjacent) => isAdjacent, }, { minDepth: Math.max, newGroupDelay: Math.min, joinToEvent: (a, b) => (tr, adj) => a(tr, adj) || b(tr, adj) }) } }) const historyField_ = StateField.define({ create() { return HistoryState.empty }, update(state: HistoryState, tr: Transaction): HistoryState { let config = tr.state.facet(historyConfig) let fromHist = tr.annotation(fromHistory) if (fromHist) { let item = HistEvent.fromTransaction(tr, fromHist.selection), from = fromHist.side let other = from == BranchName.Done ? state.undone : state.done if (item) other = updateBranch(other, other.length, config.minDepth, item) else other = addSelection(other, tr.startState.selection) return new HistoryState(from == BranchName.Done ? fromHist.rest : other, from == BranchName.Done ? other : fromHist.rest) } let isolate = tr.annotation(isolateHistory) if (isolate == "full" || isolate == "before") state = state.isolate() if (tr.annotation(Transaction.addToHistory) === false) return !tr.changes.empty ? state.addMapping(tr.changes.desc) : state let event = HistEvent.fromTransaction(tr) let time = tr.annotation(Transaction.time)!, userEvent = tr.annotation(Transaction.userEvent) if (event) state = state.addChanges(event, time, userEvent, config, tr) else if (tr.selection) state = state.addSelection(tr.startState.selection, time, userEvent, config.newGroupDelay) if (isolate == "full" || isolate == "after") state = state.isolate() return state }, toJSON(value) { return {done: value.done.map(e => e.toJSON()), undone: value.undone.map(e => e.toJSON())} }, fromJSON(json) { return new HistoryState(json.done.map(HistEvent.fromJSON), json.undone.map(HistEvent.fromJSON)) } }) /// Create a history extension with the given configuration. export function history(config: HistoryConfig = {}): Extension { return [ historyField_, historyConfig.of(config), EditorView.domEventHandlers({ beforeinput(e, view) { let command = e.inputType == "historyUndo" ? undo : e.inputType == "historyRedo" ? redo : null if (!command) return false e.preventDefault() return command(view) } }) ] } /// The state field used to store the history data. Should probably /// only be used when you want to /// [serialize](#state.EditorState.toJSON) or /// [deserialize](#state.EditorState^fromJSON) state objects in a way /// that preserves history. export const historyField = historyField_ as StateField function cmd(side: BranchName, selection: boolean): StateCommand { return function({state, dispatch}: {state: EditorState, dispatch: (tr: Transaction) => void}) { if (!selection && state.readOnly) return false let historyState = state.field(historyField_, false) if (!historyState) return false let tr = historyState.pop(side, state, selection) if (!tr) return false dispatch(tr) return true } } /// Undo a single group of history events. Returns false if no group /// was available. export const undo = cmd(BranchName.Done, false) /// Redo a group of history events. Returns false if no group was /// available. export const redo = cmd(BranchName.Undone, false) /// Undo a change or selection change. export const undoSelection = cmd(BranchName.Done, true) /// Redo a change or selection change. export const redoSelection = cmd(BranchName.Undone, true) function depth(side: BranchName) { return function(state: EditorState): number { let histState = state.field(historyField_, false) if (!histState) return 0 let branch = side == BranchName.Done ? histState.done : histState.undone return branch.length - (branch.length && !branch[0].changes ? 1 : 0) } } /// The amount of undoable change events available in a given state. export const undoDepth = depth(BranchName.Done) /// The amount of redoable change events available in a given state. export const redoDepth = depth(BranchName.Undone) // History events store groups of changes or effects that need to be // undone/redone together. class HistEvent { constructor( // The changes in this event. Normal events hold at least one // change or effect. But it may be necessary to store selection // events before the first change, in which case a special type of // instance is created which doesn't hold any changes, with // changes == startSelection == undefined readonly changes: ChangeSet | undefined, // The effects associated with this event readonly effects: readonly StateEffect[], // Accumulated mapping (from addToHistory==false) that should be // applied to events below this one. readonly mapped: ChangeDesc | undefined, // The selection before this event readonly startSelection: EditorSelection | undefined, // Stores selection changes after this event, to be used for // selection undo/redo. readonly selectionsAfter: readonly EditorSelection[] ) {} setSelAfter(after: readonly EditorSelection[]) { return new HistEvent(this.changes, this.effects, this.mapped, this.startSelection, after) } toJSON() { return { changes: this.changes?.toJSON(), mapped: this.mapped?.toJSON(), startSelection: this.startSelection?.toJSON(), selectionsAfter: this.selectionsAfter.map(s => s.toJSON()) } } static fromJSON(json: any) { return new HistEvent( json.changes && ChangeSet.fromJSON(json.changes), [], json.mapped && ChangeDesc.fromJSON(json.mapped), json.startSelection && EditorSelection.fromJSON(json.startSelection), json.selectionsAfter.map(EditorSelection.fromJSON) ) } // This does not check `addToHistory` and such, it assumes the // transaction needs to be converted to an item. Returns null when // there are no changes or effects in the transaction. static fromTransaction(tr: Transaction, selection?: EditorSelection) { let effects: readonly StateEffect[] = none for (let invert of tr.startState.facet(invertedEffects)) { let result = invert(tr) if (result.length) effects = effects.concat(result) } if (!effects.length && tr.changes.empty) return null return new HistEvent(tr.changes.invert(tr.startState.doc), effects, undefined, selection || tr.startState.selection, none) } static selection(selections: readonly EditorSelection[]) { return new HistEvent(undefined, none, undefined, undefined, selections) } } type Branch = readonly HistEvent[] function updateBranch(branch: Branch, to: number, maxLen: number, newEvent: HistEvent) { let start = to + 1 > maxLen + 20 ? to - maxLen - 1 : 0 let newBranch = branch.slice(start, to) newBranch.push(newEvent) return newBranch } function isAdjacent(a: ChangeDesc, b: ChangeDesc): boolean { let ranges: number[] = [], isAdjacent = false a.iterChangedRanges((f, t) => ranges.push(f, t)) b.iterChangedRanges((_f, _t, f, t) => { for (let i = 0; i < ranges.length;) { let from = ranges[i++], to = ranges[i++] if (t >= from && f <= to) isAdjacent = true } }) return isAdjacent } function eqSelectionShape(a: EditorSelection, b: EditorSelection) { return a.ranges.length == b.ranges.length && a.ranges.filter((r, i) => r.empty != b.ranges[i].empty).length === 0 } function conc(a: readonly T[], b: readonly T[]) { return !a.length ? b : !b.length ? a : a.concat(b) } const none: readonly any[] = [] const MaxSelectionsPerEvent = 200 function addSelection(branch: Branch, selection: EditorSelection) { if (!branch.length) { return [HistEvent.selection([selection])] } else { let lastEvent = branch[branch.length - 1] let sels = lastEvent.selectionsAfter.slice(Math.max(0, lastEvent.selectionsAfter.length - MaxSelectionsPerEvent)) if (sels.length && sels[sels.length - 1].eq(selection)) return branch sels.push(selection) return updateBranch(branch, branch.length - 1, 1e9, lastEvent.setSelAfter(sels)) } } // Assumes the top item has one or more selectionAfter values function popSelection(branch: Branch): Branch { let last = branch[branch.length - 1] let newBranch = branch.slice() newBranch[branch.length - 1] = last.setSelAfter(last.selectionsAfter.slice(0, last.selectionsAfter.length - 1)) return newBranch } // Add a mapping to the top event in the given branch. If this maps // away all the changes and effects in that item, drop it and // propagate the mapping to the next item. function addMappingToBranch(branch: Branch, mapping: ChangeDesc) { if (!branch.length) return branch let length = branch.length, selections = none while (length) { let event = mapEvent(branch[length - 1], mapping, selections) if (event.changes && !event.changes.empty || event.effects.length) { // Event survived mapping let result = branch.slice(0, length) result[length - 1] = event return result } else { // Drop this event, since there's no changes or effects left mapping = event.mapped! length-- selections = event.selectionsAfter } } return selections.length ? [HistEvent.selection(selections)] : none } function mapEvent(event: HistEvent, mapping: ChangeDesc, extraSelections: readonly EditorSelection[]) { let selections = conc(event.selectionsAfter.length ? event.selectionsAfter.map(s => s.map(mapping)) : none, extraSelections) // Change-less events don't store mappings (they are always the last event in a branch) if (!event.changes) return HistEvent.selection(selections) let mappedChanges = event.changes.map(mapping), before = mapping.mapDesc(event.changes, true) let fullMapping = event.mapped ? event.mapped.composeDesc(before) : before return new HistEvent(mappedChanges, StateEffect.mapEffects(event.effects, mapping), fullMapping, event.startSelection!.map(before), selections) } const joinableUserEvent = /^(input\.type|delete)($|\.)/ class HistoryState { constructor(public readonly done: Branch, public readonly undone: Branch, private readonly prevTime: number = 0, private readonly prevUserEvent: string | undefined = undefined) {} isolate() { return this.prevTime ? new HistoryState(this.done, this.undone) : this } addChanges(event: HistEvent, time: number, userEvent: string | undefined, config: Required, tr: Transaction): HistoryState { let done = this.done, lastEvent = done[done.length - 1] if (lastEvent && lastEvent.changes && !lastEvent.changes.empty && event.changes && (!userEvent || joinableUserEvent.test(userEvent)) && ((!lastEvent.selectionsAfter.length && time - this.prevTime < config.newGroupDelay && config.joinToEvent(tr, isAdjacent(lastEvent.changes, event.changes))) || // For compose (but not compose.start) events, always join with previous event userEvent == "input.type.compose")) { done = updateBranch(done, done.length - 1, config.minDepth, new HistEvent(event.changes.compose(lastEvent.changes), conc(event.effects, lastEvent.effects), lastEvent.mapped, lastEvent.startSelection, none)) } else { done = updateBranch(done, done.length, config.minDepth, event) } return new HistoryState(done, none, time, userEvent) } addSelection(selection: EditorSelection, time: number, userEvent: string | undefined, newGroupDelay: number) { let last = this.done.length ? this.done[this.done.length - 1].selectionsAfter : none if (last.length > 0 && time - this.prevTime < newGroupDelay && userEvent == this.prevUserEvent && userEvent && /^select($|\.)/.test(userEvent) && eqSelectionShape(last[last.length - 1], selection)) return this return new HistoryState(addSelection(this.done, selection), this.undone, time, userEvent) } addMapping(mapping: ChangeDesc): HistoryState { return new HistoryState(addMappingToBranch(this.done, mapping), addMappingToBranch(this.undone, mapping), this.prevTime, this.prevUserEvent) } pop(side: BranchName, state: EditorState, onlySelection: boolean): Transaction | null { let branch = side == BranchName.Done ? this.done : this.undone if (branch.length == 0) return null let event = branch[branch.length - 1], selection = event.selectionsAfter[0] || state.selection if (onlySelection && event.selectionsAfter.length) { return state.update({ selection: event.selectionsAfter[event.selectionsAfter.length - 1], annotations: fromHistory.of({side, rest: popSelection(branch), selection}), userEvent: side == BranchName.Done ? "select.undo" : "select.redo", scrollIntoView: true }) } else if (!event.changes) { return null } else { let rest = branch.length == 1 ? none : branch.slice(0, branch.length - 1) if (event.mapped) rest = addMappingToBranch(rest, event.mapped!) return state.update({ changes: event.changes, selection: event.startSelection, effects: event.effects, annotations: fromHistory.of({side, rest, selection}), filter: false, userEvent: side == BranchName.Done ? "undo" : "redo", scrollIntoView: true }) } } static empty: HistoryState = new HistoryState(none, none) } /// Default key bindings for the undo history. /// /// - Mod-z: [`undo`](#commands.undo). /// - Mod-y (Mod-Shift-z on macOS) + Ctrl-Shift-z on Linux: [`redo`](#commands.redo). /// - Mod-u: [`undoSelection`](#commands.undoSelection). /// - Alt-u (Mod-Shift-u on macOS): [`redoSelection`](#commands.redoSelection). export const historyKeymap: readonly KeyBinding[] = [ {key: "Mod-z", run: undo, preventDefault: true}, {key: "Mod-y", mac: "Mod-Shift-z", run: redo, preventDefault: true}, {linux: "Ctrl-Shift-z", run: redo, preventDefault: true}, {key: "Mod-u", run: undoSelection, preventDefault: true}, {key: "Alt-u", mac: "Mod-Shift-u", run: redoSelection, preventDefault: true} ] commands-6.6.0/test/000077500000000000000000000000001462762217600143205ustar00rootroot00000000000000commands-6.6.0/test/state.ts000066400000000000000000000023101462762217600160040ustar00rootroot00000000000000import {EditorState, EditorSelection, Extension} from "@codemirror/state" export function mkState(doc: string, extensions: Extension = []) { let range = /\||<([^]*?)>/g, m let ranges = [] while (m = range.exec(doc)) { if (m[1]) { ranges.push(EditorSelection.range(m.index, m.index + m[1].length)) doc = doc.slice(0, m.index) + doc.slice(m.index + 1, m.index + 1 + m[1].length) + doc.slice(m.index + m[0].length) range.lastIndex -= 2 } else { ranges.push(EditorSelection.cursor(m.index)) doc = doc.slice(0, m.index) + doc.slice(m.index + 1) range.lastIndex-- } } return EditorState.create({ doc, selection: ranges.length ? EditorSelection.create(ranges) : undefined, extensions: [extensions, EditorState.allowMultipleSelections.of(true)] }) } export function stateStr(state: EditorState) { let doc = state.doc.toString() for (let i = state.selection.ranges.length - 1; i >= 0; i--) { let range = state.selection.ranges[i] if (range.empty) doc = doc.slice(0, range.from) + "|" + doc.slice(range.from) else doc = doc.slice(0, range.from) + "<" + doc.slice(range.from, range.to) + ">" + doc.slice(range.to) } return doc } commands-6.6.0/test/test-commands.ts000066400000000000000000000210051462762217600174440ustar00rootroot00000000000000import {EditorState, StateCommand, Extension} from "@codemirror/state" import {indentMore, indentLess, indentSelection, insertNewlineAndIndent, insertNewlineKeepIndent, deleteTrailingWhitespace, deleteGroupForward, deleteGroupBackward, moveLineUp, moveLineDown} from "@codemirror/commands" import {javascriptLanguage} from "@codemirror/lang-javascript" import {indentUnit} from "@codemirror/language" import ist from "ist" import {mkState, stateStr} from "./state.js" function cmd(state: EditorState, command: StateCommand) { command({state, dispatch(tr) { state = tr.state }}) return state } describe("commands", () => { describe("indentMore", () => { function test(from: string, to: string) { ist(stateStr(cmd(mkState(from), indentMore)), to) } it("adds indentation", () => test("one\ntwo|\nthree", "one\n two|\nthree")) it("indents all lines in a range", () => test("one\n", "one\n ")) it("doesn't double-indent a given line", () => test("on|e|\n", " on|e|\n ")) it("ignores lines if a range selection ends directly at their start", () => test("onthree", " onthree")) }) describe("indentLess", () => { function test(from: string, to: string) { ist(stateStr(cmd(mkState(from), indentLess)), to) } it("removes indentation", () => test("one\n two|\nthree", "one\ntwo|\nthree")) it("removes one unit of indentation", () => test("one\n two|\n three|", "one\n two|\n three|")) it("dedents all lines in a range", () => test("one\n ", "one\n")) it("takes tabs into account", () => test(" \tone|\n \ttwo|", " one|\n two|")) it("can split tabs", () => test("\tone|", " one|")) }) describe("indentSelection", () => { function test(from: string, to: string) { ist(stateStr(cmd(mkState(from, javascriptLanguage), indentSelection)), to) } it("auto-indents the current line", () => test("if (0)\nfoo()|", "if (0)\n foo()|")) it("moves the cursor ahead of the indentation", () => test("if (0)\n | foo()", "if (0)\n |foo()")) it("indents blocks of lines", () => test("if (0) {\n\n}", "if (0) {\n \n}")) it("includes previous indentation changes in relative indentation", () => test("<{\n{\n{\n{}\n}\n}\n}>", "<{\n {\n {\n {}\n }\n }\n}>")) }) describe("insertNewlineKeepIndent", () => { function test(from: string, to: string) { ist(stateStr(cmd(mkState(from), insertNewlineKeepIndent)), to) } it("keeps indentation", () => test(" one|", " one\n |")) it("keeps zero indentation", () => test("one|two", "one\n|two")) it("deletes the selection", () => test("if x:\n onefour", "if x:\n one\n |four")) }) describe("insertNewlineAndIndent", () => { function test(from: string, to: string) { ist(stateStr(cmd(mkState(from, javascriptLanguage), insertNewlineAndIndent)), to) } it("indents the new line", () => test("{|", "{\n |")) it("can handle multiple selections", () => test("{|\n foo()|", "{\n |\n foo()\n |")) it("isn't confused by text after the cursor", () => test("{|two", "{\n |two")) it("clears empty lines before the cursor", () => test(" |", "\n|")) it("deletes selected text", () => test("{two", "{\n |two")) it("can explode brackets", () => test("let x = [|]", "let x = [\n |\n]")) it("can explode in indented positions", () => test("{\n foo(|)", "{\n foo(\n |\n )")) it("can explode brackets with whitespace", () => test("foo( | )", "foo(\n |\n)")) it("doesn't try to explode already-exploded brackets", () => test("foo(\n |\n)", "foo(\n\n |\n)")) function testIndentationFromPrevLine(from: string, to: string, ext: Extension = []) { ist(stateStr(cmd(mkState(from, ext), insertNewlineAndIndent)), to) } it("doesn't indent when previous line lacks indentation", () => testIndentationFromPrevLine("foo|", "foo\n|")) it("indents when previous line uses two space indentation", () => testIndentationFromPrevLine(" foo|", " foo\n |")) it("indents when previous line uses four space indentation", () => testIndentationFromPrevLine(" foo|", " foo\n |")) it("indents when previous line uses tab indentation", () => testIndentationFromPrevLine("\tfoo|", "\tfoo\n\t|", indentUnit.of("\t"))) it("indents when previous line uses tab indentation and short alignment", () => testIndentationFromPrevLine("\t foo|", "\t foo\n\t |", indentUnit.of("\t"))) }) describe("deleteTrailingWhitespace", () => { function test(from: string, to: string) { ist(cmd(mkState(from), deleteTrailingWhitespace).doc.toString(), to) } it("deletes trailing whitespace", () => test("foo ", "foo")) it("checks multiple lines", () => test("one\ntwo \nthree \n ", "one\ntwo\nthree\n")) it("can handle empty lines", () => test("one \n\ntwo ", "one\n\ntwo")) }) describe("deleteGroupForward", () => { function test(from: string, to: string) { ist(stateStr(cmd(mkState(from), deleteGroupForward)), to) } it("deletes a word", () => test("one |two three", "one | three")) it("deletes a word with leading space", () => test("one| two three", "one| three")) it("deletes a group of punctuation", () => test("one|...two", "one|two")) it("deletes a group of space", () => test("one| \ttwo", "one|two")) it("deletes a newline", () => test("one|\ntwo", "one|two")) it("stops deleting at a newline", () => test("one| \n two", "one|\n two")) it("stops deleting after a newline", () => test("one|\n two", "one| two")) it("deletes up to the end of the doc", () => test("one|two", "one|")) it("does nothing at the end of the doc", () => test("one|", "one|")) }) describe("deleteGroupBackward", () => { function test(from: string, to: string) { ist(stateStr(cmd(mkState(from), deleteGroupBackward)), to) } it("deletes a word", () => test("one two| three", "one | three")) it("deletes a word with trailing space", () => test("one two |three", "one |three")) it("deletes a group of punctuation", () => test("one...|two", "one|two")) it("deletes a group of space", () => test("one \t |two", "one|two")) it("deletes a newline", () => test("one\n|two", "one|two")) it("stops deleting at a newline", () => test("one \n |two", "one \n|two")) it("stops deleting after a newline", () => test("one \n|two", "one |two")) it("deletes up to the start of the doc", () => test("one|two", "|two")) }) describe("moveLineUp", () => { function test(from: string, to: string) { ist(stateStr(cmd(mkState(from), moveLineUp)), to) } it("can move a line up", () => test("one\ntwo|\nthree", "two|\none\nthree")) it("preserves multiple cursors on a single line", () => test("one\nt|w|o|\n", "t|w|o|\none\n")) it("moves selected blocks as one", () => test("one\ntwo\nthr\n", "one\nthr\ntwo\n")) it("moves blocks made of multiple ranges as one", () => test("one\nree\nfo|u\n", "ree\nfo|u\none\n")) it("does not include a trailing line after a range", () => test("one\nfour", "one\nfour")) }) describe("moveLineDown", () => { function test(from: string, to: string) { ist(stateStr(cmd(mkState(from), moveLineDown)), to) } it("can move a line own", () => test("one\ntwo|\nthree", "one\nthree\ntwo|")) it("preserves multiple cursors on a single line", () => test("one\nt|w|o|\nthree", "one\nthree\nt|w|o|")) it("moves selected blocks as one", () => test("one\ntwo\nthr\nsix", "one\ntwo\nsix\nthr")) it("moves blocks made of multiple ranges as one", () => test("one\nree\nfo|u\nsix\n", "one\nsix\nree\nfo|u\n")) it("does not include a trailing line after a range", () => test("one\nfour\n", "one\nfour\n")) it("clips the selection when moving to the end of the doc", () => test("one\nfour", "one\nfour\n")) }) }) commands-6.6.0/test/test-comment.ts000066400000000000000000000170341462762217600173140ustar00rootroot00000000000000import ist from "ist" import {SelectionRange, EditorState, EditorSelection, Extension, StateCommand} from "@codemirror/state" import {toggleLineComment, CommentTokens, toggleBlockComment, toggleBlockCommentByLine} from "@codemirror/commands" import {htmlLanguage} from "@codemirror/lang-html" describe("comment", () => { const defaultConfig: CommentTokens = {line: "//", block: {open: "/*", close: "*/"}} /// Creates a new `EditorState` using `doc` as the document text. /// The selection ranges in the returned state can be specified /// within the `doc` argument: /// The character `|` is used a marker to indicate both the /// start and the end of a `SelectionRange`, *e.g.*, /// /// ```typescript /// s("line 1\nlin|e 2\nline 3") /// ``` function s(doc: string, config: CommentTokens = defaultConfig, extensions: readonly Extension[] = []): EditorState { let markers = [], pos while ((pos = doc.indexOf("|", 0)) >= 0) { markers.push(pos) doc = doc.slice(0, pos) + doc.slice(pos + 1) } const ranges: SelectionRange[] = [] if (markers.length == 1) { ranges.push(EditorSelection.cursor(markers[0])) } else if (markers.length % 2 != 0) { throw "Markers for multiple selections need to be even."; } else { for (let i = 0; i < markers.length; i += 2) ranges.push(EditorSelection.range(markers[i], markers[i + 1])) if (ranges.length == 0) ranges.push(EditorSelection.cursor(0)) } return EditorState.create({ doc, selection: EditorSelection.create(ranges), extensions: [EditorState.allowMultipleSelections.of(true), EditorState.languageData.of(() => [{commentTokens: config}])].concat(extensions) }) } function same(actualState: EditorState, expectedState: EditorState) { ist(actualState.doc.toString(), expectedState.doc.toString()) ist(JSON.stringify(actualState.selection), JSON.stringify(expectedState.selection)) } function checkToggleChain(toggle: StateCommand, config: CommentTokens, docs: string[]) { let state = s(docs[0], config) for (let i = 1; i <= docs.length; i++) { toggle({state, dispatch(tr) { state = tr.state }}) same(state, s(docs[i == docs.length ? docs.length - 2 : i], config)) } } // Runs all tests for the given line-comment token, `k`. function runLineCommentTests(k: string) { function check(...docs: string[]) { checkToggleChain(toggleLineComment, {line: k}, docs) } describe(`Line comments ('${k}')`, () => { it("toggles in an empty single selection", () => { check(`\nline 1\n ${k} ${k} ${k} ${k}line| 2\nline 3\n`, `\nline 1\n ${k} ${k} ${k}line| 2\nline 3\n`, `\nline 1\n ${k} ${k}line| 2\nline 3\n`, `\nline 1\n ${k}line| 2\nline 3\n`, `\nline 1\n line| 2\nline 3\n`, `\nline 1\n ${k} line| 2\nline 3\n`) check(`\nline 1\n ${k}line 2|\nline 3\n`, `\nline 1\n line 2|\nline 3\n`, `\nline 1\n ${k} line 2|\nline 3\n`) check(`\nline 1\n| ${k}line 2\nline 3\n`, `\nline 1\n| line 2\nline 3\n`, `\nline 1\n| ${k} line 2\nline 3\n`) check(`\nline 1\n|${k}\nline 3\n`, `\nline 1\n|\nline 3\n`, `\nline 1\n${k} |\nline 3\n`) check(`\nline 1\n line 2\nline 3\n|${k}`, `\nline 1\n line 2\nline 3\n|`, `\nline 1\n line 2\nline 3\n${k} |`) }) it("toggles comments in a single line when the cursor is at the beginning", () => { check(`line 1\n |line 2\nline 3\n`, `line 1\n ${k} |line 2\nline 3\n`) }) it("toggles comments in a single line selection", () => { check(`line 1\n ${k}li|ne |2\nline 3\n`, `line 1\n li|ne |2\nline 3\n`, `line 1\n ${k} li|ne |2\nline 3\n`) }) it("toggles comments in a multi-line selection", () => { check(`\n ${k}lin|e 1\n ${k} line 2\n ${k} line |3\n`, `\n lin|e 1\n line 2\n line |3\n`, `\n ${k} lin|e 1\n ${k} line 2\n ${k} line |3\n`) check(`\n ${k}lin|e 1\n ${k} line 2\n line 3\n ${k} li|ne 4\n`, `\n ${k} ${k}lin|e 1\n ${k} ${k} line 2\n ${k} line 3\n ${k} ${k} li|ne 4\n`) check(`\n ${k} lin|e 1\n\n ${k} line |3\n`, `\n lin|e 1\n\n line |3\n`) check(`\n ${k} lin|e 1\n \n ${k} line |3\n`, `\n lin|e 1\n \n line |3\n`) check(`\n|\n ${k} line 2\n | \n`, `\n|\n line 2\n | \n`) check(`\n|\n\n | \n`, `\n|\n\n | \n`) }) it("toggles comments in a multi-line multi-range selection", () => { check(`\n lin|e 1\n line |2\n line 3\n l|ine 4\n line| 5\n`, `\n ${k} lin|e 1\n ${k} line |2\n line 3\n ${k} l|ine 4\n ${k} line| 5\n`) }) it("can handle multiple selections on one line", () => { check(`|line| |with| |ranges|`, `${k} |line| |with| |ranges|`) }) it("doesn't include lines in which a selection range ends", () => { check(`line| 1\nline 2\n|line 3`, `${k} line| 1\n${k} line 2\n|line 3`) }) it("leaves empty lines alone", () => { check(`line| 1\n\nline 3|`, `${k} line| 1\n\n${k} line 3|`) }) it("does comment empty lines with a cursor", () => { check(`|\nline 2`, `${k} |\nline 2`) }) }) } /// Runs all tests for the given block-comment tokens. function runBlockCommentTests(o: string, c: string) { describe(`Block comments ('${o} ${c}')`, () => { function check(...docs: string[]) { checkToggleChain(toggleBlockComment, {block: {open: o, close: c}}, docs) } function checkLine(...docs: string[]) { checkToggleChain(toggleBlockCommentByLine, {block: {open: o, close: c}}, docs) } it("toggles block comment in multi-line selection", () => { check(`\n lin|e 1\n line 2\n line 3\n line |4\n line 5\n`, `\n lin${o} |e 1\n line 2\n line 3\n line | ${c}4\n line 5\n`) }) it("toggles block comment in multi-line multi-range selection", () => { check(`\n lin|e 1\n line |2\n l|ine 3\n line 4\n line |5\n`, `\n lin${o} |e 1\n line | ${c}2\n l${o} |ine 3\n line 4\n line | ${c}5\n`) }) it("can toggle comments inside the selection", () => { check(`|${o} one\ntwo ${c}| three`, `|one\ntwo| three`, `${o} |one\ntwo| ${c} three`) }) it("comments the entire line", () => { checkLine(`one|\ntwo`, `${o} one| ${c}\ntwo`) }) it("comments multiple lines", () => { checkLine(`on|e\nt|wo`, `${o} on|e\nt|wo ${c}`) }) it("joins selected blocks of lines", () => { checkLine(`on|e\nt|w|o\nth|ree`, `${o} on|e\nt|w|o\nth|ree ${c}`) }) }) } runLineCommentTests("//") runLineCommentTests("#") runBlockCommentTests("/*", "*/") runBlockCommentTests("") it("toggles line comment in multi-language doc", () => { let state = s(` `, undefined, [htmlLanguage]) toggleLineComment({state, dispatch(tr) { state = tr.state }}) same(state, s(` `)) }) }) commands-6.6.0/test/test-history.ts000066400000000000000000000610441462762217600173530ustar00rootroot00000000000000import ist from "ist" import {EditorState, EditorSelection, Transaction, StateEffect, StateEffectType, StateField, ChangeDesc} from "@codemirror/state" import {isolateHistory, history, redo, redoDepth, redoSelection, undo, undoDepth, undoSelection, invertedEffects, historyField} from "@codemirror/commands" function mkState(config?: any, doc?: string) { return EditorState.create({ extensions: [history(config), EditorState.allowMultipleSelections.of(true)], doc }) } function type(state: EditorState, text: string, at = state.doc.length) { return state.update({changes: {from: at, insert: text}}).state } function timedType(state: EditorState, text: string, atTime: number) { return state.update({changes: {from: state.doc.length, insert: text}, annotations: Transaction.time.of(atTime)}).state } function receive(state: EditorState, text: string, from: number, to = from) { return state.update({changes: {from, to, insert: text}, annotations: Transaction.addToHistory.of(false)}).state } function command(state: EditorState, cmd: any, success: boolean = true) { ist(cmd({state, dispatch(tr: Transaction) { state = tr.state }}), success) return state } describe("history", () => { it("allows to undo a change", () => { let state = mkState() state = type(state, "newtext") state = command(state, undo) ist(state.doc.toString(), "") }) it("allows to undo nearby changes in one change", () => { let state = mkState() state = type(state, "new") state = type(state, "text") state = command(state, undo) ist(state.doc.toString(), "") }) it("allows to redo a change", () => { let state = mkState() state = type(state, "newtext") state = command(state, undo) state = command(state, redo) ist(state.doc.toString(), "newtext") }) it("allows to redo nearby changes in one change", () => { let state = mkState() state = type(state, "new") state = type(state, "text") state = command(state, undo) state = command(state, redo) ist(state.doc.toString(), "newtext") }) it("puts the cursor after the change on redo", () => { let state = mkState({}, "one\n\ntwo") state = state.update({changes: {from: 3, insert: "!"}, selection: {anchor: 4}}).state state = state.update({selection: {anchor: state.doc.length}}).state state = command(state, undo) state = command(state, redo) ist(state.selection.main.head, 4) }) it("tracks multiple levels of history", () => { let state = mkState({}, "one") state = type(state, "new") state = type(state, "text") state = type(state, "some", 0) ist(state.doc.toString(), "someonenewtext") state = command(state, undo) ist(state.doc.toString(), "onenewtext") state = command(state, undo) ist(state.doc.toString(), "one") state = command(state, redo) ist(state.doc.toString(), "onenewtext") state = command(state, redo) ist(state.doc.toString(), "someonenewtext") state = command(state, undo) ist(state.doc.toString(), "onenewtext") }) it("starts a new event when newGroupDelay elapses", () => { let state = mkState({newGroupDelay: 1000}) state = timedType(state, "a", 1000) state = timedType(state, "b", 1600) ist(undoDepth(state), 1) state = timedType(state, "c", 2700) ist(undoDepth(state), 2) state = command(state, undo) state = timedType(state, "d", 2800) ist(undoDepth(state), 2) }) it("supports a custom join predicate", () => { let state = mkState({joinToEvent: (tr: Transaction, adj: boolean) => { if (!adj) return false let space = false if (adj) tr.changes.iterChanges((fA, tA, fB, tB, text) => { if (text.length && text.sliceString(0, 1) == " ") space = true }) return !space }}) for (let ch of "ab cd") state = type(state, ch) ist(state.sliceDoc(), "ab cd") state = command(state, undo) ist(state.sliceDoc(), "ab") state = command(state, undo) ist(state.sliceDoc(), "") }) it("allows changes that aren't part of the history", () => { let state = mkState() state = type(state, "hello") state = receive(state, "oops", 0) state = receive(state, "!", 9) state = command(state, undo) ist(state.doc.toString(), "oops!") }) it("doesn't get confused by an undo not adding any redo item", () => { let state = mkState({}, "ab") state = type(state, "cd", 1) state = receive(state, "123", 0, 4) state = command(state, undo, false) command(state, redo, false) }) it("accurately maps changes through each other", () => { let state = mkState({}, "123") state = state.update({ changes: [{from: 0, to: 1, insert: "ab"}, {from: 1, to: 2, insert: "cd"}, {from: 2, to: 3, insert: "ef"}] }).state state = receive(state, "!!!!!!!!", 2, 2) state = command(state, undo) state = command(state, redo) ist(state.doc.toString(), "ab!!!!!!!!cdef") }) it("can handle complex editing sequences", () => { let state = mkState() state = type(state, "hello") state = state.update({annotations: isolateHistory.of("before")}).state state = type(state, "!") state = receive(state, "....", 0) state = type(state, "\n\n", 2) ist(state.doc.toString(), "..\n\n..hello!") state = receive(state, "\n\n", 1) state = command(state, undo) state = command(state, undo) ist(state.doc.toString(), ".\n\n...hello") state = command(state, undo) ist(state.doc.toString(), ".\n\n...") }) it("supports overlapping edits", () => { let state = mkState() state = type(state, "hello") state = state.update({annotations: isolateHistory.of("before")}).state state = state.update({changes: {from: 0, to: 5}}).state ist(state.doc.toString(), "") state = command(state, undo) ist(state.doc.toString(), "hello") state = command(state, undo) ist(state.doc.toString(), "") }) it("supports overlapping edits that aren't collapsed", () => { let state = mkState() state = receive(state, "h", 0) state = type(state, "ello") state = state.update({annotations: isolateHistory.of("before")}).state state = state.update({changes: {from: 0, to: 5}}).state ist(state.doc.toString(), "") state = command(state, undo) ist(state.doc.toString(), "hello") state = command(state, undo) ist(state.doc.toString(), "h") }) it("supports overlapping unsynced deletes", () => { let state = mkState() state = type(state, "hi") state = state.update({annotations: isolateHistory.of("before")}).state state = type(state, "hello") state = state.update({changes: {from: 0, to: 7}, annotations: Transaction.addToHistory.of(false)}).state ist(state.doc.toString(), "") state = command(state, undo, false) ist(state.doc.toString(), "") }) it("can go back and forth through history multiple times", () => { let state = mkState() state = type(state, "one") state = type(state, " two") state = state.update({annotations: isolateHistory.of("before")}).state state = type(state, " three") state = type(state, "zero ", 0) state = state.update({annotations: isolateHistory.of("before")}).state state = type(state, "\n\n", 0) state = type(state, "top", 0) for (let i = 0; i < 6; i++) { let re = i % 2 for (let j = 0; j < 4; j++) state = command(state, re ? redo : undo) ist(state.doc.toString(), re ? "top\n\nzero one two three" : "") } }) it("supports non-tracked changes next to tracked changes", () => { let state = mkState() state = type(state, "o") state = type(state, "\n\n", 0) state = receive(state, "zzz", 3) state = command(state, undo) ist(state.doc.toString(), "zzz") }) it("can go back and forth through history when preserving items", () => { let state = mkState() state = type(state, "one") state = type(state, " two") state = state.update({annotations: isolateHistory.of("before")}).state state = receive(state, "xxx", state.doc.length) state = type(state, " three") state = type(state, "zero ", 0) state = state.update({annotations: isolateHistory.of("before")}).state state = type(state, "\n\n", 0) state = type(state, "top", 0) state = receive(state, "yyy", 0) for (let i = 0; i < 3; i++) { for (let j = 0; j < 4; j++) state = command(state, undo) ist(state.doc.toString(), "yyyxxx") for (let j = 0; j < 4; j++) state = command(state, redo) ist(state.doc.toString(), "yyytop\n\nzero one twoxxx three") } }) it("restores selection on undo", () => { let state = mkState() state = type(state, "hi") state = state.update({annotations: isolateHistory.of("before")}).state state = state.update({selection: {anchor: 0, head: 2}}).state const selection = state.selection state = state.update(state.replaceSelection("hello")).state const selection2 = state.selection state = command(state, undo) ist(state.selection.eq(selection)) state = command(state, redo) ist(state.selection.eq(selection2)) }) it("restores the selection before the first change in an item (#46)", () => { let state = mkState() state = state.update({changes: {from: 0, insert: "a"}, selection: {anchor: 1}}).state state = state.update({changes: {from: 1, insert: "b"}, selection: {anchor: 2}}).state state = command(state, undo) ist(state.doc.toString(), "") ist(state.selection.main.anchor, 0) }) it("doesn't merge document changes if there's a selection change in between", () => { let state = mkState() state = type(state, "hi") state = state.update({selection: {anchor: 0, head: 2}}).state state = state.update(state.replaceSelection("hello")).state ist(undoDepth(state), 2) }) it("rebases selection on undo", () => { let state = mkState() state = type(state, "hi") state = state.update({annotations: isolateHistory.of("before")}).state state = state.update({selection: {anchor: 0, head: 2}}).state state = type(state, "hello", 0) state = receive(state, "---", 0) state = command(state, undo) ist(state.selection.ranges[0].head, 5) }) it("supports querying for the undo and redo depth", () => { let state = mkState() state = type(state, "a") ist(undoDepth(state), 1) ist(redoDepth(state), 0) state = receive(state, "b", 0) ist(undoDepth(state), 1) ist(redoDepth(state), 0) state = command(state, undo) ist(undoDepth(state), 0) ist(redoDepth(state), 1) state = command(state, redo) ist(undoDepth(state), 1) ist(redoDepth(state), 0) }) it("all functions gracefully handle EditorStates without history", () => { let state = EditorState.create() ist(undoDepth(state), 0) ist(redoDepth(state), 0) command(state, undo, false) command(state, redo, false) }) it("truncates history", () => { let state = mkState({minDepth: 10}) for (let i = 0; i < 40; ++i) { state = type(state, "a") state = state.update({annotations: isolateHistory.of("before")}).state } ist(undoDepth(state) < 40) }) it("doesn't undo selection-only transactions", () => { let state = mkState(undefined, "abc") ist(state.selection.main.head, 0) state = state.update({selection: {anchor: 2}}).state state = command(state, undo, false) ist(state.selection.main.head, 2) }) it("isolates transactions when asked to", () => { let state = mkState() state = state.update({changes: {from: 0, insert: "a"}, annotations: isolateHistory.of("after")}).state state = state.update({changes: {from: 1, insert: "a"}}).state state = state.update({changes: {from: 2, insert: "c"}, annotations: isolateHistory.of("after")}).state state = state.update({changes: {from: 3, insert: "d"}}).state state = state.update({changes: {from: 4, insert: "e"}, annotations: isolateHistory.of("full")}).state state = state.update({changes: {from: 5, insert: "f"}}).state ist(undoDepth(state), 5) }) it("can group events around a non-history transaction", () => { let state = mkState() state = state.update({changes: {from: 0, insert: "a"}}).state state = state.update({changes: {from: 1, insert: "b"}, annotations: Transaction.addToHistory.of(false)}).state state = state.update({changes: {from: 1, insert: "c"}}).state state = command(state, undo) ist(state.doc.toString(), "b") }) it("properly maps selections through non-history changes", () => { let state = mkState({}, "abc") state = state.update({selection: EditorSelection.create([EditorSelection.cursor(0), EditorSelection.cursor(1), EditorSelection.cursor(2)])}).state state = state.update({changes: {from: 0, to: 3, insert: "d"}}).state state = state.update({changes: [{from: 0, insert: "x"}, {from: 1, insert: "y"}], annotations: Transaction.addToHistory.of(false)}).state state = command(state, undo) ist(state.doc.toString(), "xabcy") ist(state.selection.ranges.map(r => r.from).join(","), "0,2,3") }) it("restores selection on redo", () => { let state = mkState({}, "a\nb\nc\n") state = state.update({selection: EditorSelection.create([1, 3, 5].map(n => EditorSelection.cursor(n)))}).state state = state.update(state.replaceSelection("-")).state state = state.update({selection: {anchor: 0}}).state state = command(state, undo) state = state.update({selection: {anchor: 0}}).state state = command(state, redo) ist(state.selection.ranges.map(r => r.head).join(","), "2,5,8") }) describe("undoSelection", () => { it("allows to undo a change", () => { let state = mkState() state = type(state, "newtext") state = command(state, undoSelection) ist(state.doc.toString(), "") }) it("allows to undo selection-only transactions", () => { let state = mkState(undefined, "abc") ist(state.selection.main.head, 0) state = state.update({selection: {anchor: 2}}).state state = command(state, undoSelection) ist(state.selection.main.head, 0) }) it("merges selection-only transactions from keyboard", () => { let state = mkState(undefined, "abc") ist(state.selection.main.head, 0) state = state.update({selection: {anchor: 2}, userEvent: "select"}).state state = state.update({selection: {anchor: 3}, userEvent: "select"}).state state = state.update({selection: {anchor: 1}, userEvent: "select"}).state state = command(state, undoSelection) ist(state.selection.main.head, 0) }) it("doesn't merge selection-only transactions from other sources", () => { let state = mkState(undefined, "abc") ist(state.selection.main.head, 0) state = state.update({selection: {anchor: 2}}).state state = state.update({selection: {anchor: 3}}).state state = state.update({selection: {anchor: 1}}).state state = command(state, undoSelection) ist(state.selection.main.head, 3) state = command(state, undoSelection) ist(state.selection.main.head, 2) state = command(state, undoSelection) ist(state.selection.main.head, 0) }) it("doesn't merge selection-only transactions if they change the number of selections", () => { let state = mkState(undefined, "abc") ist(state.selection.main.head, 0) state = state.update({selection: {anchor: 2}, userEvent: "select"}).state state = state.update({selection: EditorSelection.create([EditorSelection.cursor(1), EditorSelection.cursor(3)]), userEvent: "select"}).state state = state.update({selection: {anchor: 1}, userEvent: "select"}).state state = command(state, undoSelection) ist(state.selection.ranges.length, 2) state = command(state, undoSelection) ist(state.selection.main.head, 0) }) it("doesn't merge selection-only transactions if a selection changes empty state", () => { let state = mkState(undefined, "abc") ist(state.selection.main.head, 0) state = state.update({selection: {anchor: 2}, userEvent: "select"}).state state = state.update({selection: {anchor: 2, head: 3}, userEvent: "select"}).state state = state.update({selection: {anchor: 1}, userEvent: "select"}).state state = command(state, undoSelection) ist(state.selection.main.anchor, 2) ist(state.selection.main.head, 3) state = command(state, undoSelection) ist(state.selection.main.head, 0) }) it("allows to redo a change", () => { let state = mkState() state = type(state, "newtext") state = command(state, undoSelection) state = command(state, redoSelection) ist(state.doc.toString(), "newtext") }) it("allows to redo selection-only transactions", () => { let state = mkState(undefined, "abc") ist(state.selection.main.head, 0) state = state.update({selection: {anchor: 2}}).state state = command(state, undoSelection) state = command(state, redoSelection) ist(state.selection.main.head, 2) }) it("only changes selection", () => { let state = mkState() state = type(state, "hi") state = state.update({annotations: isolateHistory.of("before")}).state const selection = state.selection state = state.update({selection: {anchor: 0, head: 2}}).state const selection2 = state.selection state = command(state, undoSelection) ist(state.selection.eq(selection)) ist(state.doc.toString(), "hi") state = command(state, redoSelection) ist(state.selection.eq(selection2)) state = state.update(state.replaceSelection("hello")).state const selection3 = state.selection state = command(state, undoSelection) ist(state.selection.eq(selection2)) state = command(state, redo) ist(state.selection.eq(selection3)) }) it("can undo a selection through remote changes", () => { let state = mkState() state = type(state, "hello") const selection = state.selection state = state.update({selection: {anchor: 0, head: 2}}).state state = receive(state, "oops", 0) state = receive(state, "!", 9) ist(state.selection.eq(EditorSelection.single(4, 6))) state = command(state, undoSelection) ist(state.doc.toString(), "oopshello!") ist(state.selection.eq(selection)) }) it("preserves text inserted inside a change", () => { let state = mkState() state = type(state, "1234") state = state.update({changes: {from: 2, insert: "x"}, annotations: Transaction.addToHistory.of(false)}).state state = command(state, undo) ist(state.doc.toString(), "x") }) }) describe("effects", () => { it("includes inverted effects in the history", () => { let set = StateEffect.define() let field = StateField.define({ create: () => 0, update(val, tr) { for (let effect of tr.effects) if (effect.is(set)) val = effect.value return val } }) let invert = invertedEffects.of(tr => { for (let e of tr.effects) if (e.is(set)) return [set.of(tr.startState.field(field))] return [] }) let state = EditorState.create({extensions: [history(), field, invert]}) state = state.update({effects: set.of(10), annotations: isolateHistory.of("before")}).state state = state.update({effects: set.of(20), annotations: isolateHistory.of("before")}).state ist(state.field(field), 20) state = command(state, undo) ist(state.field(field), 10) state = command(state, undo) ist(state.field(field), 0) state = command(state, redo) ist(state.field(field), 10) state = command(state, redo) ist(state.field(field), 20) state = command(state, undo) ist(state.field(field), 10) state = command(state, redo) ist(state.field(field), 20) }) class Comment { constructor(readonly from: number, readonly to: number, readonly text: string) {} eq(other: Comment) { return this.from == other.from && this.to == other.to && this.text == other.text } } function mapComment(comment: Comment, mapping: ChangeDesc) { let from = mapping.mapPos(comment.from, 1), to = mapping.mapPos(comment.to, -1) return from >= to ? undefined : new Comment(from, to, comment.text) } let addComment: StateEffectType = StateEffect.define({map: mapComment}) let rmComment: StateEffectType = StateEffect.define({map: mapComment}) let comments = StateField.define({ create: () => [], update(value, tr) { value = value.map(c => mapComment(c, tr.changes)).filter(x => x) as any for (let effect of tr.effects) { if (effect.is(addComment)) value = value.concat(effect.value) else if (effect.is(rmComment)) value = value.filter(c => !c.eq(effect.value)) } return value.sort((a, b) => a.from - b.from) } }) let invertComments = invertedEffects.of(tr => { let effects = [] for (let effect of tr.effects) { if (effect.is(addComment) || effect.is(rmComment)) { let src = mapComment(effect.value, tr.changes.invertedDesc) if (src) effects.push((effect.is(addComment) ? rmComment : addComment).of(src)) } } for (let comment of tr.startState.field(comments)) { if (!mapComment(comment, tr.changes)) effects.push(addComment.of(comment)) } return effects }) function commentStr(state: EditorState) { return state.field(comments).map(c => c.text + "@" + c.from).join(",") } it("can map effects", () => { let state = EditorState.create({extensions: [history(), comments, invertComments], doc: "one two foo"}) state = state.update({effects: addComment.of(new Comment(0, 3, "c1")), annotations: isolateHistory.of("full")}).state ist(commentStr(state), "c1@0") state = state.update({changes: {from: 3, to: 4, insert: "---"}, annotations: isolateHistory.of("full"), effects: addComment.of(new Comment(6, 9, "c2"))}).state ist(commentStr(state), "c1@0,c2@6") state = state.update({changes: {from: 0, insert: "---"}, annotations: Transaction.addToHistory.of(false)}).state ist(commentStr(state), "c1@3,c2@9") state = command(state, undo) ist(state.doc.toString(), "---one two foo") ist(commentStr(state), "c1@3") state = command(state, undo) ist(commentStr(state), "") state = command(state, redo) ist(commentStr(state), "c1@3") state = command(state, redo) ist(commentStr(state), "c1@3,c2@9") ist(state.doc.toString(), "---one---two foo") state = command(state, undo).update({changes: {from: 10, to: 11, insert: "---"}, annotations: Transaction.addToHistory.of(false)}).state state = state.update({effects: addComment.of(new Comment(13, 16, "c3")), annotations: isolateHistory.of("full")}).state ist(commentStr(state), "c1@3,c3@13") state = command(state, undo) ist(state.doc.toString(), "---one two---foo") ist(commentStr(state), "c1@3") state = command(state, redo) ist(commentStr(state), "c1@3,c3@13") }) it("can restore comments lost through deletion", () => { let state = EditorState.create({extensions: [history(), comments, invertComments], doc: "123456"}) state = state.update({effects: addComment.of(new Comment(3, 5, "c1")), annotations: isolateHistory.of("full")}).state state = state.update({changes: {from: 2, to: 6}}).state ist(commentStr(state), "") state = command(state, undo) ist(commentStr(state), "c1@3") }) }) describe("JSON", () => { it("survives serialization", () => { let state = EditorState.create({doc: "abcd", extensions: history()}) state = state.update({changes: {from: 3, to: 4}}).state state = state.update({changes: {from: 0, insert: "d"}}).state state = command(state, undo) let jsonConf = {history: historyField} let json = JSON.stringify(state.toJSON(jsonConf)) state = EditorState.fromJSON(JSON.parse(json), {extensions: history()}, jsonConf) ist(state.doc.toString(), "abc") state = command(state, redo) ist(state.doc.toString(), "dabc") state = command(command(state, undo), undo) ist(state.doc.toString(), "abcd") }) }) }) commands-6.6.0/test/webtest-commands.ts000066400000000000000000000074531462762217600201550ustar00rootroot00000000000000import {EditorView, Command} from "@codemirror/view" import {Extension, EditorState} from "@codemirror/state" import {cursorSubwordForward, cursorSubwordBackward} from "@codemirror/commands" import ist from "ist" import {mkState, stateStr} from "./state.js" const dashWordChar = EditorState.languageData.of(() => [{wordChars: "-"}]) function testCmd(before: string, after: string, command: Command, extensions: Extension = []) { let state = mkState(before, extensions) let view = new EditorView({ state, parent: document.querySelector("#workspace")! as HTMLElement }) try { command(view) ist(stateStr(view.state), after) } finally { view.destroy() } } describe("commands", () => { describe("cursorSubwordForward", () => { it("stops at first camelcase boundary", () => testCmd("|CamelCaseWord", "Camel|CaseWord", cursorSubwordForward)) it("stops at inner camelcase boundary", () => testCmd("Camel|CaseWord", "CamelCase|Word", cursorSubwordForward)) it("stops at last camelcase boundary", () => testCmd("CamelCase|Word", "CamelCaseWord|", cursorSubwordForward)) it("treats ranges of capitals as a single word", () => testCmd("Eat|CSSToken", "EatCSS|Token", cursorSubwordForward)) it("stops at the end of word", () => testCmd("o|kay.", "okay|.", cursorSubwordForward)) it("stops before underscores", () => testCmd("|snake_case", "snake|_case", cursorSubwordForward)) it("stops after underscores", () => testCmd("snake|_case", "snake_|case", cursorSubwordForward)) it("stops before dashes", () => testCmd("|kebab-case", "kebab|-case", cursorSubwordForward, dashWordChar)) it("stops after dashes", () => testCmd("kebab|-case", "kebab-|case", cursorSubwordForward, dashWordChar)) it("stops on dashes at end of word", () => testCmd("one|--..", "one--|..", cursorSubwordForward, dashWordChar)) if (typeof Intl != "undefined" && (Intl as any).Segmenter) { it("stops on CJK word boundaries", () => { testCmd("|马在路上小跑着。", "马|在路上小跑着。", cursorSubwordForward) testCmd("马|在路上小跑着。", "马在|路上小跑着。", cursorSubwordForward) testCmd("马在|路上小跑着。", "马在路上|小跑着。", cursorSubwordForward) }) } }) describe("cursorSubwordBackward", () => { it("stops at camelcase boundary", () => testCmd("CamelCaseWord|", "CamelCase|Word", cursorSubwordBackward)) it("stops at last camelcase boundary", () => testCmd("Camel|CaseWord", "|CamelCaseWord", cursorSubwordBackward)) it("treats ranges of capitals as a single word", () => testCmd("EatCSS|Token", "Eat|CSSToken", cursorSubwordBackward)) it("stops at the end of word", () => testCmd(".o|kay", ".|okay", cursorSubwordBackward)) it("stops before underscores", () => testCmd("snake_case|", "snake_|case", cursorSubwordBackward)) it("stops after underscores", () => testCmd("snake_|case", "snake|_case", cursorSubwordBackward)) it("stops before dashes", () => testCmd("kebab-case|", "kebab-|case", cursorSubwordBackward, dashWordChar)) it("stops after dashes", () => testCmd("kebab--|case", "kebab|--case", cursorSubwordBackward, dashWordChar)) it("stops on dashes at end of word", () => testCmd("..--one|", "..--|one", cursorSubwordBackward, dashWordChar)) if (typeof Intl != "undefined" && (Intl as any).Segmenter) { it("stops on CJK word boundaries", () => { testCmd("马在路上小跑着|。", "马在路上小跑|着。", cursorSubwordBackward) testCmd("马在路上小跑|着。", "马在路上|小跑着。", cursorSubwordBackward) testCmd("马在路上|小跑着。", "马在|路上小跑着。", cursorSubwordBackward) }) } }) })