diff --git a/package.json b/package.json index 66f69b6..dfe69ba 100644 --- a/package.json +++ b/package.json @@ -32,11 +32,12 @@ "@codemirror/view": "^6.0.3" }, "devDependencies": { - "codemirror": "6.0.0", "@codemirror/buildhelper": "^0.1.16", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/lang-xml": "^6.0.0", "@codemirror/language": "^6.1.0", + "codemirror": "6.0.0", + "typescript": "^5.0.2", "vite": "^2.9.6" }, "repository": { diff --git a/src/block-cursor.ts b/src/block-cursor.ts index 4c4926c..56842fa 100644 --- a/src/block-cursor.ts +++ b/src/block-cursor.ts @@ -91,7 +91,7 @@ export class BlockCursorPlugin { readPos(): Measure { let {state} = this.view - let cursors = [] + let cursors: Piece[] = [] for (let r of state.selection.ranges) { let prim = r == state.selection.main let piece = measureCursor(this.cm, this.view, r, prim) diff --git a/src/cm_adapter.ts b/src/cm_adapter.ts index d74836b..7e2fda2 100644 --- a/src/cm_adapter.ts +++ b/src/cm_adapter.ts @@ -3,13 +3,11 @@ import { StringStream, matchBrackets, indentUnit, ensureSyntaxTree, foldCode } f import { EditorView, runScopeHandlers, ViewUpdate } from "@codemirror/view" import { RegExpCursor, setSearchQuery, SearchQuery } from "@codemirror/search" import { - insertNewlineAndIndent, indentMore, indentLess, indentSelection, - deleteCharBackward, deleteCharForward, cursorCharLeft, + insertNewlineAndIndent, indentMore, indentLess, indentSelection, cursorCharLeft, undo, redo, cursorLineBoundaryBackward, cursorLineBoundaryForward, cursorCharBackward, } from "@codemirror/commands" +import {vimState, CM5RangeInterface} from "./types" -interface Pos { line: number, ch: number } -interface CM5Range { anchor: Pos, head: Pos } function indexFromPos(doc: Text, pos: Pos): number { var ch = pos.ch; var lineNumber = pos.line + 1; @@ -29,6 +27,9 @@ function posFromIndex(doc: Text, offset: number): Pos { return { line: line.number - 1, ch: offset - line.from } } class Pos { + line: number + ch: number + sticky?: string constructor(line: number, ch: number) { this.line = line; this.ch = ch; } @@ -121,14 +122,14 @@ function runHistoryCommand(cm: CodeMirror, revert: boolean) { var keys: Record void> = {}; "Left|Right|Up|Down|Backspace|Delete".split("|").forEach(key => { - keys[key] = (cm:CodeMirror) => runScopeHandlers(cm.cm6, {key: key}, "editor"); + keys[key] = (cm:CodeMirror) => runScopeHandlers(cm.cm6, {key: key} as KeyboardEvent, "editor"); }); export class CodeMirror { static isMac = typeof navigator != "undefined" && /Mac/.test(navigator.platform); // -------------------------- static Pos = Pos; - static StringStream = StringStream; + static StringStream = StringStream as unknown as StringStream & { new(_: string): StringStream } static commands = { cursorCharLeft: function (cm: CodeMirror) { cursorCharLeft(cm.cm6); }, redo: function (cm: CodeMirror) { runHistoryCommand(cm, false); }, @@ -143,17 +144,16 @@ export class CodeMirror { }, indentAuto: function (cm: CodeMirror) { indentSelection(cm.cm6) - } + }, + newlineAndIndentContinueComment: undefined as any, + save: undefined as any, }; - static defineOption = function (name: string, val: any, setter: Function) { }; static isWordChar = function (ch: string) { return wordChar.test(ch); }; static keys: any = keys; - static keyMap = { - }; - static addClass = function () { }; - static rmClass = function () { }; + static addClass = function (el, str) { }; + static rmClass = function (el, str) { }; static e_preventDefault = function (e: Event) { e.preventDefault() }; @@ -188,10 +188,11 @@ export class CodeMirror { statusbar?: Element | null, dialog?: Element | null, vimPlugin?: any, - vim?: any, + vim?: vimState | null, currentNotificationClose?: Function | null, keyMap?: string, overwrite?: boolean, + textwidth?: number, } = {}; marks: Record = Object.create(null); $mid = 0; // marker id counter @@ -231,12 +232,14 @@ export class CodeMirror { firstLine() { return 0; }; lastLine() { return this.cm6.state.doc.lines - 1; }; lineCount() { return this.cm6.state.doc.lines }; - setCursor(line: Pos | number, ch: number) { + setCursor(line: number, ch: number): void; + setCursor(line: Pos): void; + setCursor(line: Pos | number, ch?: number) { if (typeof line === 'object') { ch = line.ch; line = line.line; } - var offset = indexFromPos(this.cm6.state.doc, { line, ch }) + var offset = indexFromPos(this.cm6.state.doc, { line, ch: ch || 0 }) this.cm6.dispatch({ selection: { anchor: offset } }, { scrollIntoView: !this.curOp }) if (this.curOp && !this.curOp.isVimOp) this.onBeforeEndOperation(); @@ -266,7 +269,7 @@ export class CodeMirror { }; }); }; - setSelections(p: CM5Range[], primIndex?: number) { + setSelections(p: CM5RangeInterface[], primIndex?: number) { var doc = this.cm6.state.doc var ranges = p.map(x => { return EditorSelection.range(indexFromPos(doc, x.anchor), indexFromPos(doc, x.head)) @@ -316,7 +319,7 @@ export class CodeMirror { indexFromPos(doc, e) ) }; - replaceRange(text: string, s: Pos, e: Pos) { + replaceRange(text: string, s: Pos, e?: Pos, source?: string) { if (!e) e = s; var doc = this.cm6.state.doc; var from = indexFromPos(doc, s); @@ -386,7 +389,7 @@ export class CodeMirror { return this.cm6.defaultLineHeight }; - findMatchingBracket(pos: Pos) { + findMatchingBracket(pos: Pos, _options?: any) { var state = this.cm6.state var offset = indexFromPos(state.doc, pos); var m = matchBrackets(state, offset + 1, -1) @@ -403,7 +406,7 @@ export class CodeMirror { return scanForBracket(this, pos, dir, style, config); }; - indentLine(line: number, more: boolean) { + indentLine(line: number, more?: boolean) { // todo how to indent only one line instead of selection if (more) this.indentMore() else this.indentLess() @@ -547,7 +550,7 @@ export class CodeMirror { } } - let pos = posFromIndex(doc, range.head) as Pos&{hitSide: boolean}; + let pos = posFromIndex(doc, range.head) as Pos&{hitSide?: boolean}; // set hitside to true if there was no place to move and cursor was clipped to the edge // of document. Needed for gj/gk if ( @@ -588,7 +591,7 @@ export class CodeMirror { clientHeight: scroller.clientHeight, clientWidth: scroller.clientWidth }; }; - scrollTo(x?: number, y?: number) { + scrollTo(x?: number|null, y?: number|null) { if (x != null) this.cm6.scrollDOM.scrollLeft = x if (y != null) @@ -664,7 +667,7 @@ export class CodeMirror { curOp.cursorActivityHandlers = this._handlers["cursorActivity"] && this._handlers["cursorActivity"].slice(); this.curOp.cursorActivity = true; }; - operation(fn: Function) { + operation(fn: Function, force?: boolean) { if (!this.curOp) this.curOp = { $d: 0 }; this.curOp.$d++; @@ -717,6 +720,8 @@ export class CodeMirror { } }; + getOption(name:"firstLineNumber"|"tabSize"): number; + getOption(name:string): number|boolean|string|undefined; getOption(name: string) { switch (name) { case "firstLineNumber": return 1; @@ -767,7 +772,7 @@ export class CodeMirror { virtualSelectionMode() { return !!this.virtualSelection } - virtualSelection: EditorSelection | null = null; + virtualSelection: Mutable | null = null; forEachSelection(command: Function) { var selection = this.cm6.state.selection; this.virtualSelection = EditorSelection.create(selection.ranges, selection.mainIndex) @@ -785,8 +790,15 @@ export class CodeMirror { hardWrap(options) { return hardWrap(this, options); } + + showMatchesOnScrollbar?: Function // not implemented + save?: Function + static keyName?: Function = undefined }; +type Mutable = { + -readonly [Key in keyof Type]: Type[Key]; +}; /************* dialog *************/ @@ -924,7 +936,7 @@ function scanForBracket(cm: CodeMirror, where: Pos, dir: -1 | 1, style: any, con var maxScanLen = (config && config.maxScanLineLength) || 10000; var maxScanLines = (config && config.maxScanLines) || 1000; - var stack = []; + var stack: string[] = []; var re = bracketRegex(config) var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1) : Math.max(cm.firstLine() - 1, where.line - maxScanLines); @@ -948,7 +960,7 @@ function scanForBracket(cm: CodeMirror, where: Pos, dir: -1 | 1, style: any, con return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null; } -function findMatchingTag(cm: CodeMirror, pos: Pos) { +function findMatchingTag(cm: CodeMirror, pos: Pos): undefined { } function findEnclosingTag(cm: CodeMirror, pos: Pos) { @@ -1021,7 +1033,7 @@ function hardWrap(cm, options) { if (line.length > max) { var space = findSpace(line, max, 5); if (space) { - var indentation = /^\s*/.exec(line)[0]; + var indentation = /^\s*/.exec(line)?.[0]; cm.replaceRange("\n" + indentation, new Pos(row, space.start), new Pos(row, space.end)); } endRow++; diff --git a/src/index.ts b/src/index.ts index 366f4a2..4d02160 100644 --- a/src/index.ts +++ b/src/index.ts @@ -71,6 +71,7 @@ const vimPlugin = ViewPlugin.fromClass( this.updateStatus(); }); this.cm.on("vim-mode-change", (e: any) => { + if (!cm.state.vim) return; cm.state.vim.mode = e.mode; if (e.subMode) { cm.state.vim.mode += " block"; @@ -226,7 +227,7 @@ const vimPlugin = ViewPlugin.fromClass( vim.status = (vim.status || "") + key; let result = Vim.multiSelectHandleKey(cm, key, "user"); - vim = cm.state.vim; // the object can change if there is an exception in handleKey + vim = Vim.maybeInitVimState_(cm); // the object can change if there is an exception in handleKey // insert mode if (!result && vim.insertMode && cm.state.overwrite) { diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..289957b --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,336 @@ +import { CodeMirror } from "./cm_adapter" +import {initVim} from "./vim" +export type Vim = ReturnType +export type vimState = { + onPasteFn?: any, + sel: {head: Pos, anchor: Pos}, + insertModeReturn: boolean, + visualBlock: boolean, + marks: {[mark: string]: Marker}, + visualMode: boolean, + insertMode: boolean, + pasteFn: any, + lastSelection: any, + searchState_: any, + lastEditActionCommand: actionCommand|void, + lastPastedText: any, + lastMotion: any, + options: {[optionName: string]: vimOption}, + lastEditInputState: InputStateInterface|void, + inputState: InputStateInterface, + visualLine: boolean, + insertModeRepeat: any, + lastHSPos: number, + lastHPos: number, + wasInVisualBlock?: boolean, + insert?: any, + insertEnd?: Marker, + status: string, + exMode?: boolean, + mode?: any, + expectLiteralNext?: boolean, + constructor(): void; +} +export type Marker = ReturnType +export type LineHandle = ReturnType +export type Pos = { line: number, ch: number, sticky?: string } + +export interface CM5Range { + anchor: Pos, + head: Pos, + + from(): Pos, + empty(): boolean +} +export interface CM5RangeInterface { + anchor: Pos, + head: Pos, +} + +export type RegisterController = ReturnType +export type Register = ReturnType + +export type SearchArgs = { + forward?: boolean, + toJumplist?: boolean, + wholeWordOnly?: boolean, + querySrc?: string, +} + +export type OperatorArgs = { + repeat?: number, + forward?: boolean, + linewise?: boolean, + fullLine?: boolean, + registerName?: string|null, + indentRight?: boolean, + toLower?: boolean, + shouldMoveCursor?: boolean, + selectedCharacter?: string, + lastSel?: any; + keepCursor?: boolean; +} +// version of CodeMirror with vim state checked +export type CodeMirrorV = CodeMirror & {state: {vim: vimState}} +export type OperatorFn = (cm: CodeMirrorV, args: OperatorArgs, ranges: CM5RangeInterface[], oldAnchor: Pos, newHead?: Pos) => Pos|void +export type vimOperators = { + change(cm: CodeMirrorV, args: OperatorArgs, ranges: CM5RangeInterface[]): void, + delete(cm: CodeMirrorV, args: OperatorArgs, ranges: CM5RangeInterface[]): void, + indent(cm: CodeMirrorV, args: OperatorArgs, ranges: CM5RangeInterface[]): void, + indentAuto(cm: CodeMirrorV, args: OperatorArgs, ranges: CM5RangeInterface[]): void, + hardWrap(cm: CodeMirrorV, args: OperatorArgs, ranges: CM5RangeInterface[], oldAnchor: Pos): Pos|void, + changeCase(cm: CodeMirrorV, args: OperatorArgs, ranges: CM5RangeInterface[], oldAnchor: Pos, newHead?: Pos): Pos|void, + yank(cm: CodeMirrorV, args: OperatorArgs, ranges: CM5RangeInterface[], oldAnchor: Pos): Pos|void, +} & { + [key: string]: OperatorFn +} + +export type ActionArgsPartial = { + repeat?: number, + forward?: boolean, + head?: Pos, + position?: string + backtrack?: boolean, + increase?: boolean, + repeatIsExplicit?: boolean, + indentRight?: boolean, + selectedCharacter?: string, + after?: boolean, + matchIndent?: boolean, + registerName?: string, + isEdit?: boolean + linewise?: boolean, + insertAt?: string, + blockwise?: boolean, + keepSpaces?: boolean, + replace?: boolean, + keepCursor?: boolean +} +export type ActionArgs = ActionArgsPartial & {repeat: number}; + +export type ActionFn = (cm: CodeMirrorV, actionArgs: ActionArgs, vim: vimState) => void + +export type vimActions = { + jumpListWalk(cm: CodeMirrorV, actionArgs: ActionArgs, vim: vimState): void, + continuePaste(cm: CodeMirrorV, actionArgs: ActionArgs, vim: vimState, text: string, register: Register): void + enterInsertMode(cm: CodeMirrorV, actionArgs: ActionArgsPartial, vum: vimState): void, +} & { + [key: string]: ActionFn +} + +export type MotionArgsPartial = { + repeat?: number, + forward?: boolean, + selectedCharacter?: string, + linewise?: boolean, + textObjectInner?: boolean, + sameLine?: boolean, + repeatOffset?: number, + toJumplist?: boolean, + inclusive?: boolean, + wordEnd?: boolean, + toFirstChar?:boolean, + explicitRepeat?: boolean, + bigWord?: boolean, + repeatIsExplicit?: boolean, + noRepeat?: boolean +}; + +export type MotionArgs = MotionArgsPartial & {repeat: number}; + +export type MotionFn = (cm: CodeMirrorV, head: Pos, motionArgs: MotionArgs, vim: vimState, inputState: InputStateInterface) => Pos|[Pos,Pos]|null|undefined +export type vimMotions = { + moveToTopLine(cm: CodeMirrorV, head: Pos, motionArgs: MotionArgs): Pos + moveToMiddleLine(cm: CodeMirrorV): Pos + moveToBottomLine(cm: CodeMirrorV, head: Pos, motionArgs: MotionArgs): Pos + expandToLine(_cm: CodeMirrorV, head: Pos, motionArgs: MotionArgs): Pos + findNext(_cm: CodeMirrorV, _head: Pos, motionArgs: MotionArgs): Pos | undefined + findAndSelectNextInclusive(cm: CodeMirrorV, head: Pos, motionArgs: MotionArgs, vim: vimState, inputState: InputStateInterface): Pos|[Pos,Pos] | undefined + goToMark(cm: CodeMirrorV, _head: Pos, motionArgs: MotionArgs, vim: vimState, inputState: InputStateInterface): Pos | undefined | null + moveToOtherHighlightedEnd(cm: CodeMirrorV, _head: Pos, motionArgs: MotionArgs, vim: vimState): [Pos,Pos] + jumpToMark(cm: CodeMirrorV, head: Pos, motionArgs: MotionArgs, vim: vimState):Pos + moveByCharacters(_cm: CodeMirrorV, head: Pos, motionArgs: MotionArgs): Pos + moveByLines(cm: CodeMirrorV, head: Pos, motionArgs: MotionArgs, vim: vimState): Pos + moveByDisplayLines(cm: CodeMirrorV, head: Pos, motionArgs: MotionArgs, vim: vimState): Pos + moveByPage(cm: CodeMirrorV, head: Pos, motionArgs: MotionArgs): Pos + moveByParagraph(cm: CodeMirrorV, head: Pos, motionArgs: MotionArgs): Pos + moveBySentence(cm: CodeMirrorV, head: Pos, motionArgs: MotionArgs): Pos + moveByScroll(cm: CodeMirrorV, head: Pos, motionArgs: MotionArgs, vim: vimState): Pos | null + moveByWords(cm: CodeMirrorV, head: Pos, motionArgs: MotionArgs): Pos | undefined + moveTillCharacter(cm: CodeMirrorV, _head: Pos, motionArgs: MotionArgs): Pos | null + moveToCharacter(cm: CodeMirrorV, head: Pos, motionArgs: MotionArgs): Pos + moveToSymbol(cm: CodeMirrorV, head: Pos, motionArgs: MotionArgs): Pos + moveToColumn(cm: CodeMirrorV, head: Pos, motionArgs: MotionArgs, vim: vimState): Pos + moveToEol(cm: CodeMirrorV, head: Pos, motionArgs: MotionArgs, vim: vimState): Pos + moveToFirstNonWhiteSpaceCharacter(cm: CodeMirrorV, head: Pos): Pos + moveToMatchedSymbol(cm: CodeMirrorV, head: Pos): Pos | undefined + moveToStartOfLine(_cm: CodeMirrorV, head: Pos, motionArgs?: MotionArgs, vim?: vimState): Pos + moveToLineOrEdgeOfDocument(cm: CodeMirrorV, _head: Pos, motionArgs: MotionArgs): Pos + moveToStartOfDisplayLine(cm: CodeMirrorV): Pos + moveToEndOfDisplayLine(cm: CodeMirrorV): Pos + textObjectManipulation(cm: CodeMirrorV, head: Pos, motionArgs: MotionArgs, vim: vimState): Pos | [Pos, Pos] | null + repeatLastCharacterSearch(cm: CodeMirrorV, head: Pos, motionArgs: MotionArgs): Pos + [key: string]: MotionFn +} + + + +export type optionCallback = (value?: string|undefined, cm?: CodeMirror) => any +export type vimOption = { + type?: string, + defaultValue?: unknown, + callback?: optionCallback, + value?: unknown +} + + +export type ExFn = ()=> void; + +type allCommands = { + keys: string, + context?: string, + interlaceInsertRepeat?: boolean, + exitVisualBlock?: boolean, + isEdit?: boolean, + repeatOverride?: number +} +export type motionCommand = allCommands & { + type: 'motion', + motion: string, + motionArgs?: MotionArgsPartial, + repeatOverride?: number +} +export type operatorCommand = allCommands & { + type: 'operator', + operator: string, + operatorArgs?: OperatorArgs +} +export type actionCommand = allCommands & { + type: 'action', + action: string, + actionArgs?: ActionArgsPartial, + motion?: string, + operator?: string, + interlaceInsertRepeat?: boolean +} +export type searchCommand = allCommands & { + type: 'search', + searchArgs: SearchArgs +} +export type operatorMotionCommand = allCommands & { + type: 'operatorMotion', + motion: string, + operator: string, + motionArgs?: MotionArgsPartial, + operatorArgs?: OperatorArgs, + operatorMotionArgs?: { [arg: string]: boolean | string } +} +export type idleCommand = allCommands & { type: 'idle' } +export type exCommand = allCommands & { type: 'ex' } +export type keyToExCommand = allCommands & { type: 'keyToEx', exArgs: { [arg: string]: any } } +export type keyToKeyCommand = allCommands & { toKeys: string, type: 'keyToKey' } + +export type vimKey = + motionCommand + | operatorCommand + | actionCommand + | searchCommand + | operatorMotionCommand + | idleCommand + | exCommand + | keyToExCommand + | keyToKeyCommand; + +export type vimKeyMap = vimKey[]; + +export interface InputStateInterface { + prefixRepeat: string[]; + motionRepeat: any[]; + operator: any| undefined | null; + operatorArgs: OperatorArgs | undefined | null; + motion: string | undefined | null; + motionArgs: MotionArgs | null; + keyBuffer: any[]; + registerName?: string; + changeQueue: any; + operatorShortcut?: string; + selectedCharacter?: string; + repeatOverride?: number; + changeQueueList?: any[]; + pushRepeatDigit(n: string): void; + getRepeat(): number; +} + +export type vimExCommands = { + colorscheme(cm: CodeMirrorV, params: vimExCommandsParams): void, + map(cm: CodeMirrorV, params: vimExCommandsParams, ctx: string): void, + imap(cm: CodeMirrorV, params: vimExCommandsParams): void, + nmap(cm: CodeMirrorV, params: vimExCommandsParams): void, + vmap(cm: CodeMirrorV, params: vimExCommandsParams): void, + unmap(cm: CodeMirrorV, params: vimExCommandsParams, ctx: string): void, + move(cm: CodeMirrorV, params: vimExCommandsParams): void, + set(cm: CodeMirrorV, params: vimExCommandsParams): void, + setlocal(cm: CodeMirrorV, params: vimExCommandsParams): void, + setglobal(cm: CodeMirrorV, params: vimExCommandsParams): void, + registers(cm: CodeMirrorV, params: vimExCommandsParams): void, + sort(cm: CodeMirrorV, params: vimExCommandsParams): void, + vglobal(cm: CodeMirrorV, params: vimExCommandsParams): void, + global(cm: CodeMirrorV, params: vimExCommandsParams): void, + substitute(cm: CodeMirrorV, params: vimExCommandsParams): void, + redo(cm: CodeMirrorV): void, + undo(cm: CodeMirrorV): void, + write(cm: CodeMirrorV & {save?: Function}): void, + nohlsearch(cm: CodeMirrorV): void, + yank(cm: CodeMirrorV): void, + delete(cm: CodeMirrorV, params: vimExCommandsParams): void, + join(cm: CodeMirrorV, params: vimExCommandsParams): void, + delmarks(cm: CodeMirrorV, params: vimExCommandsParams): void, + [key: string]:(cm: CodeMirrorV, params: vimExCommandsParams, ctx: string)=> void, +} + +type vimExCommandsParams = { + args?: any[], + input?: string, + line?: number, + setCfg?: any, + argString?: string, + lineEnd?: number, + commandName?: any[], + callback?: () => any, + selectionLine?: number, + selectionLineEnd?: number +} + + +export type InsertModeChanges = { + changes: any; + expectCursorActivityForChange: any; + visualBlock?: number, + maybeReset?: boolean, + ignoreCount?: number, + repeatOverride?: number, +} + +export type ExParams = { + commandName: string, + argString: string, + input: string, + args?: string[], + + line: number, + lineEnd?: number, + selectionLine: number, + selectionLineEnd?: number, + + setCfg?: Object, + callback?: any, + +} + + +declare global { + function isNaN(v: any): v is Exclude; + interface String { + trimStart(): string + } +} \ No newline at end of file diff --git a/src/vim.js b/src/vim.js index 17a9c21..464fb13 100644 --- a/src/vim.js +++ b/src/vim.js @@ -1,3 +1,5 @@ +//@ts-check + // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: https://codemirror.net/5/LICENSE @@ -34,10 +36,26 @@ * 9. Ex command implementations. */ +/** + * @typedef { import("./cm_adapter").CodeMirror } CodeMirror + * @typedef { import("./types").CodeMirrorV} CodeMirrorV + * @typedef { import("./types").Pos } Pos + * @typedef { import("./types").CM5Range } CM5Range + * @typedef { import("./types").vimState } vimState + * @typedef { import("./types").ExFn } ExFn + * @typedef { import("./types").MotionArgs } MotionArgs + * @typedef { import("./types").ActionArgs } ActionArgs + * @typedef { import("./types").OperatorArgs } OperatorArgs + * @typedef { import("./types").vimKey } vimKey + * @typedef { import("./types").InputStateInterface } InputStateInterface + */ + +/** @arg {typeof import("./cm_adapter").CodeMirror} CodeMirror */ export function initVim(CodeMirror) { var Pos = CodeMirror.Pos; + /** @arg {CodeMirror} cm @arg {Pos} curStart @arg {Pos} curEnd */ function updateSelectionForSurrogateCharacters(cm, curStart, curEnd) { // start and character position when no selection // is the same in visual mode, and differs in 1 character in normal mode @@ -51,7 +69,7 @@ export function initVim(CodeMirror) { return {start: curStart, end: curEnd}; } - + /** @type {import("./types").vimKeyMap} */ var defaultKeymap = [ // Key to key mapping. This goes first to make it possible to override // existing mappings. @@ -291,278 +309,320 @@ export function initVim(CodeMirror) { */ var langmap = parseLangmap(''); - function enterVimMode(cm) { - cm.setOption('disableInput', true); - cm.setOption('showCursorWhenSelecting', false); - CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); - cm.on('cursorActivity', onCursorActivity); - maybeInitVimState(cm); - CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm)); - } + /** @arg {CodeMirror} cm */ + function enterVimMode(cm) { + cm.setOption('disableInput', true); + cm.setOption('showCursorWhenSelecting', false); + CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); + cm.on('cursorActivity', onCursorActivity); + maybeInitVimState(cm); + // @ts-ignore + CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm)); + } - function leaveVimMode(cm) { - cm.setOption('disableInput', false); - cm.off('cursorActivity', onCursorActivity); - CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm)); - cm.state.vim = null; - if (highlightTimeout) clearTimeout(highlightTimeout); + /** @arg {CodeMirror} cm */ + function leaveVimMode(cm) { + cm.setOption('disableInput', false); + cm.off('cursorActivity', onCursorActivity); + // @ts-ignore + CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm)); + cm.state.vim = null; + if (highlightTimeout) clearTimeout(highlightTimeout); + } + + /** @arg {CodeMirrorV} cm */ + function getOnPasteFn(cm) { + var vim = cm.state.vim; + if (!vim.onPasteFn) { + vim.onPasteFn = function() { + if (!vim.insertMode) { + cm.setCursor(offsetCursor(cm.getCursor(), 0, 1)); + actions.enterInsertMode(cm, {}, vim); + } + }; } + return vim.onPasteFn; + } - function getOnPasteFn(cm) { - var vim = cm.state.vim; - if (!vim.onPasteFn) { - vim.onPasteFn = function() { - if (!vim.insertMode) { - cm.setCursor(offsetCursor(cm.getCursor(), 0, 1)); - actions.enterInsertMode(cm, {}, vim); - } - }; + var numberRegex = /[\d]/; + var wordCharTest = [CodeMirror.isWordChar, function(ch) { + return ch && !CodeMirror.isWordChar(ch) && !/\s/.test(ch); + }], bigWordCharTest = [function(ch) { + return /\S/.test(ch); + }]; + var validMarks = ['<', '>']; + var validRegisters = ['-', '"', '.', ':', '_', '/', '+']; + var latinCharRegex = /^\w$/ + var upperCaseChars; + try { upperCaseChars = new RegExp("^[\\p{Lu}]$", "u"); } + catch (_) { upperCaseChars = /^[A-Z]$/; } + + /** @arg {CodeMirror} cm @arg {number} line */ + function isLine(cm, line) { + return line >= cm.firstLine() && line <= cm.lastLine(); + } + /** @arg {string} k */ + function isLowerCase(k) { + return (/^[a-z]$/).test(k); + } + /** @arg {string} k */ + function isMatchableSymbol(k) { + return '()[]{}'.indexOf(k) != -1; + } + /** @arg {string} k */ + function isNumber(k) { + return numberRegex.test(k); + } + /** @arg {string} k */ + function isUpperCase(k) { + return upperCaseChars.test(k); + } + /** @arg {string} k */ + function isWhiteSpaceString(k) { + return (/^\s*$/).test(k); + } + /** @arg {string} k */ + function isEndOfSentenceSymbol(k) { + return '.?!'.indexOf(k) != -1; + } + /** @arg {any} val @arg {string | any[]} arr */ + function inArray(val, arr) { + for (var i = 0; i < arr.length; i++) { + if (arr[i] == val) { + return true; } - return vim.onPasteFn; } + return false; + } - var numberRegex = /[\d]/; - var wordCharTest = [CodeMirror.isWordChar, function(ch) { - return ch && !CodeMirror.isWordChar(ch) && !/\s/.test(ch); - }], bigWordCharTest = [function(ch) { - return /\S/.test(ch); - }]; - var validMarks = ['<', '>']; - var validRegisters = ['-', '"', '.', ':', '_', '/', '+']; - var latinCharRegex = /^\w$/ - var upperCaseChars; - try { upperCaseChars = new RegExp("^[\\p{Lu}]$", "u"); } - catch (_) { upperCaseChars = /^[A-Z]$/; } - function isLine(cm, line) { - return line >= cm.firstLine() && line <= cm.lastLine(); - } - function isLowerCase(k) { - return (/^[a-z]$/).test(k); + /** @typedef {import("./types").optionCallback} optionCallback */ + /** @typedef {import("./types").vimOption} vimOption */ + /** @type {Object} */ + var options = {}; + /** + * @arg {string} name + * @arg {any} defaultValue + * @arg {string} type + * @arg {string[] } [aliases] + * @arg {optionCallback} [callback] + * */ + function defineOption(name, defaultValue, type, aliases, callback) { + if (defaultValue === undefined && !callback) { + throw Error('defaultValue is required unless callback is provided'); } - function isMatchableSymbol(k) { - return '()[]{}'.indexOf(k) != -1; - } - function isNumber(k) { - return numberRegex.test(k); - } - function isUpperCase(k) { - return upperCaseChars.test(k); + if (!type) { type = 'string'; } + options[name] = { + type: type, + defaultValue: defaultValue, + callback: callback + }; + if (aliases) { + for (var i = 0; i < aliases.length; i++) { + options[aliases[i]] = options[name]; + } } - function isWhiteSpaceString(k) { - return (/^\s*$/).test(k); + if (defaultValue) { + setOption(name, defaultValue); } - function isEndOfSentenceSymbol(k) { - return '.?!'.indexOf(k) != -1; + } + + /** + * @arg {string} name + * @arg {any} value + * @arg {CodeMirrorV} [cm] + * @arg {{ scope?: any; } | undefined} [cfg] */ + function setOption(name, value, cm, cfg) { + var option = options[name]; + cfg = cfg || {}; + var scope = cfg.scope; + if (!option) { + return new Error('Unknown option: ' + name); } - function inArray(val, arr) { - for (var i = 0; i < arr.length; i++) { - if (arr[i] == val) { - return true; - } + if (option.type == 'boolean') { + if (value && value !== true) { + return new Error('Invalid argument: ' + name + '=' + value); + } else if (value !== false) { + // Boolean options are set to true if value is not defined. + value = true; } - return false; } - - var options = {}; - function defineOption(name, defaultValue, type, aliases, callback) { - if (defaultValue === undefined && !callback) { - throw Error('defaultValue is required unless callback is provided'); - } - if (!type) { type = 'string'; } - options[name] = { - type: type, - defaultValue: defaultValue, - callback: callback - }; - if (aliases) { - for (var i = 0; i < aliases.length; i++) { - options[aliases[i]] = options[name]; - } - } - if (defaultValue) { - setOption(name, defaultValue); + if (option.callback) { + if (scope !== 'local') { + option.callback(value, undefined); } - } - - function setOption(name, value, cm, cfg) { - var option = options[name]; - cfg = cfg || {}; - var scope = cfg.scope; - if (!option) { - return new Error('Unknown option: ' + name); + if (scope !== 'global' && cm) { + option.callback(value, cm); } - if (option.type == 'boolean') { - if (value && value !== true) { - return new Error('Invalid argument: ' + name + '=' + value); - } else if (value !== false) { - // Boolean options are set to true if value is not defined. - value = true; - } + } else { + if (scope !== 'local') { + option.value = option.type == 'boolean' ? !!value : value; } - if (option.callback) { - if (scope !== 'local') { - option.callback(value, undefined); - } - if (scope !== 'global' && cm) { - option.callback(value, cm); - } - } else { - if (scope !== 'local') { - option.value = option.type == 'boolean' ? !!value : value; - } - if (scope !== 'global' && cm) { - cm.state.vim.options[name] = {value: value}; - } + if (scope !== 'global' && cm) { + cm.state.vim.options[name] = {value: value}; } } + } - function getOption(name, cm, cfg) { - var option = options[name]; - cfg = cfg || {}; - var scope = cfg.scope; - if (!option) { - return new Error('Unknown option: ' + name); - } - if (option.callback) { - var local = cm && option.callback(undefined, cm); - if (scope !== 'global' && local !== undefined) { - return local; - } - if (scope !== 'local') { - return option.callback(); - } - return; - } else { - var local = (scope !== 'global') && (cm && cm.state.vim.options[name]); - return (local || (scope !== 'local') && option || {}).value; - } + /** + * @arg {string} name + * @arg {CodeMirrorV} [cm] + * @arg {{ scope?: any; } | undefined} [cfg] */ + function getOption(name, cm, cfg) { + var option = options[name]; + cfg = cfg || {}; + var scope = cfg.scope; + if (!option) { + return new Error('Unknown option: ' + name); } - - defineOption('filetype', undefined, 'string', ['ft'], function(name, cm) { - // Option is local. Do nothing for global. - if (cm === undefined) { - return; + if (option.callback) { + let local = cm && option.callback(undefined, cm); + if (scope !== 'global' && local !== undefined) { + return local; + } + if (scope !== 'local') { + return option.callback(); + } + return; + } else { + let local = (scope !== 'global') && (cm && cm.state.vim.options[name]); + return (local || (scope !== 'local') && option || {}).value; + } + } + /** @arg {string|undefined} name @arg {CodeMirrorV} [cm] */ + defineOption('filetype', undefined, 'string', ['ft'], function(name, cm) { + // Option is local. Do nothing for global. + if (cm === undefined) { + return; + } + // The 'filetype' option proxies to the CodeMirror 'mode' option. + if (name === undefined) { + let mode = cm.getOption('mode'); + return mode == 'null' ? '' : mode; + } else { + let mode = name == '' ? 'null' : name; + cm.setOption('mode', mode); + } + }); + defineOption('textwidth', 80, 'number', ['tw'], function(width, cm) { + // Option is local. Do nothing for global. + if (cm === undefined) { + return; + } + // The 'filetype' option proxies to the CodeMirror 'mode' option. + if (width === undefined) { + var value = cm.getOption('textwidth'); + return value; + } else { + var column = Math.round(/**@type {any}*/(width)); + if (column > 1) { + cm.setOption('textwidth', column); } - // The 'filetype' option proxies to the CodeMirror 'mode' option. - if (name === undefined) { - var mode = cm.getOption('mode'); - return mode == 'null' ? '' : mode; + } + }); + + var createCircularJumpList = function() { + var size = 100; + var pointer = -1; + var head = 0; + var tail = 0; + var buffer = new Array(size); + /** @arg {CodeMirror} cm @arg {any} oldCur @arg {any} newCur */ + function add(cm, oldCur, newCur) { + var current = pointer % size; + var curMark = buffer[current]; + /** @arg {Pos} cursor */ + function useNextSlot(cursor) { + var next = ++pointer % size; + var trashMark = buffer[next]; + if (trashMark) { + trashMark.clear(); + } + buffer[next] = cm.setBookmark(cursor); + } + if (curMark) { + var markPos = curMark.find(); + // avoid recording redundant cursor position + if (markPos && !cursorEqual(markPos, oldCur)) { + useNextSlot(oldCur); + } } else { - var mode = name == '' ? 'null' : name; - cm.setOption('mode', mode); + useNextSlot(oldCur); } - }); - defineOption('textwidth', 80, 'number', ['tw'], function(width, cm) { - // Option is local. Do nothing for global. - if (cm === undefined) { - return; - } - // The 'filetype' option proxies to the CodeMirror 'mode' option. - if (width === undefined) { - var value = cm.getOption('textwidth'); - return value; - } else { - var column = Math.round(width); - if (column > 1) { - cm.setOption('textwidth', column); - } + useNextSlot(newCur); + head = pointer; + tail = pointer - size + 1; + if (tail < 0) { + tail = 0; } - }); - - var createCircularJumpList = function() { - var size = 100; - var pointer = -1; - var head = 0; - var tail = 0; - var buffer = new Array(size); - function add(cm, oldCur, newCur) { - var current = pointer % size; - var curMark = buffer[current]; - function useNextSlot(cursor) { - var next = ++pointer % size; - var trashMark = buffer[next]; - if (trashMark) { - trashMark.clear(); - } - buffer[next] = cm.setBookmark(cursor); - } - if (curMark) { - var markPos = curMark.find(); - // avoid recording redundant cursor position - if (markPos && !cursorEqual(markPos, oldCur)) { - useNextSlot(oldCur); + } + /** @arg {CodeMirror} cm @arg {number} offset */ + function move(cm, offset) { + pointer += offset; + if (pointer > head) { + pointer = head; + } else if (pointer < tail) { + pointer = tail; + } + var mark = buffer[(size + pointer) % size]; + // skip marks that are temporarily removed from text buffer + if (mark && !mark.find()) { + var inc = offset > 0 ? 1 : -1; + var newCur; + var oldCur = cm.getCursor(); + do { + pointer += inc; + mark = buffer[(size + pointer) % size]; + // skip marks that are the same as current position + if (mark && + (newCur = mark.find()) && + !cursorEqual(oldCur, newCur)) { + break; } - } else { - useNextSlot(oldCur); - } - useNextSlot(newCur); - head = pointer; - tail = pointer - size + 1; - if (tail < 0) { - tail = 0; - } - } - function move(cm, offset) { - pointer += offset; - if (pointer > head) { - pointer = head; - } else if (pointer < tail) { - pointer = tail; - } - var mark = buffer[(size + pointer) % size]; - // skip marks that are temporarily removed from text buffer - if (mark && !mark.find()) { - var inc = offset > 0 ? 1 : -1; - var newCur; - var oldCur = cm.getCursor(); - do { - pointer += inc; - mark = buffer[(size + pointer) % size]; - // skip marks that are the same as current position - if (mark && - (newCur = mark.find()) && - !cursorEqual(oldCur, newCur)) { - break; - } - } while (pointer < head && pointer > tail); - } - return mark; + } while (pointer < head && pointer > tail); } - function find(cm, offset) { - var oldPointer = pointer; - var mark = move(cm, offset); - pointer = oldPointer; - return mark && mark.find(); - } - return { - cachedCursor: undefined, //used for # and * jumps - add: add, - find: find, - move: move - }; + return mark; + } + /** @arg {CodeMirror} cm @arg {number} offset */ + function find(cm, offset) { + var oldPointer = pointer; + var mark = move(cm, offset); + pointer = oldPointer; + return mark && mark.find(); + } + return { + cachedCursor: undefined, //used for # and * jumps + add: add, + find: find, + move: move }; - - // Returns an object to track the changes associated insert mode. It - // clones the object that is passed in, or creates an empty object one if - // none is provided. - var createInsertModeChanges = function(c) { - if (c) { - // Copy construction - return { - changes: c.changes, - expectCursorActivityForChange: c.expectCursorActivityForChange - }; - } + }; + + /** + * Returns an object to track the changes associated insert mode. It + * clones the object that is passed in, or creates an empty object one if + * none is provided. + * @arg {import("./types").InsertModeChanges | undefined} [c] + * @returns {import("./types").InsertModeChanges} + */ + var createInsertModeChanges = function(c) { + if (c) { + // Copy construction return { - // Change list - changes: [], - // Set to true on change, false on cursorActivity. - expectCursorActivityForChange: false + changes: c.changes, + expectCursorActivityForChange: c.expectCursorActivityForChange }; + } + return { + // Change list + changes: [], + // Set to true on change, false on cursorActivity. + expectCursorActivityForChange: false }; + }; - function MacroModeState() { + class MacroModeState { + constructor() { this.latestRegister = undefined; this.isPlaying = false; this.isRecording = false; @@ -570,572 +630,600 @@ export function initVim(CodeMirror) { this.onRecordingDone = undefined; this.lastInsertModeChanges = createInsertModeChanges(); } - MacroModeState.prototype = { - exitMacroRecordMode: function() { - var macroModeState = vimGlobalState.macroModeState; - if (macroModeState.onRecordingDone) { - macroModeState.onRecordingDone(); // close dialog - } - macroModeState.onRecordingDone = undefined; - macroModeState.isRecording = false; - }, - enterMacroRecordMode: function(cm, registerName) { - var register = - vimGlobalState.registerController.getRegister(registerName); - if (register) { - register.clear(); - this.latestRegister = registerName; - if (cm.openDialog) { - var template = dom('span', {class: 'cm-vim-message'}, 'recording @' + registerName); - this.onRecordingDone = cm.openDialog(template, null, {bottom:true}); - } - this.isRecording = true; - } + exitMacroRecordMode() { + var macroModeState = vimGlobalState.macroModeState; + if (macroModeState.onRecordingDone) { + macroModeState.onRecordingDone(); // close dialog } - }; - - function maybeInitVimState(cm) { - if (!cm.state.vim) { - // Store instance state in the CodeMirror object. - cm.state.vim = { - inputState: new InputState(), - // Vim's input state that triggered the last edit, used to repeat - // motions and operators with '.'. - lastEditInputState: undefined, - // Vim's action command before the last edit, used to repeat actions - // with '.' and insert mode repeat. - lastEditActionCommand: undefined, - // When using jk for navigation, if you move from a longer line to a - // shorter line, the cursor may clip to the end of the shorter line. - // If j is pressed again and cursor goes to the next line, the - // cursor should go back to its horizontal position on the longer - // line if it can. This is to keep track of the horizontal position. - lastHPos: -1, - // Doing the same with screen-position for gj/gk - lastHSPos: -1, - // The last motion command run. Cleared if a non-motion command gets - // executed in between. - lastMotion: null, - marks: {}, - insertMode: false, - insertModeReturn: false, - // Repeat count for changes made in insert mode, triggered by key - // sequences like 3,i. Only exists when insertMode is true. - insertModeRepeat: undefined, - visualMode: false, - // If we are in visual line mode. No effect if visualMode is false. - visualLine: false, - visualBlock: false, - lastSelection: null, - lastPastedText: null, - sel: {}, - // Buffer-local/window-local values of vim options. - options: {}, - // Whether the next character should be interpreted literally - // Necassary for correct implementation of f, r etc. - // in terms of langmaps. - expectLiteralNext: false - }; + macroModeState.onRecordingDone = undefined; + macroModeState.isRecording = false; + } + enterMacroRecordMode(cm, registerName) { + var register = vimGlobalState.registerController.getRegister(registerName); + if (register) { + register.clear(); + this.latestRegister = registerName; + if (cm.openDialog) { + var template = dom('span', {class: 'cm-vim-message'}, 'recording @' + registerName); + this.onRecordingDone = cm.openDialog(template, null, {bottom:true}); + } + this.isRecording = true; } - return cm.state.vim; - } - var vimGlobalState; - function resetVimGlobalState() { - vimGlobalState = { - // The current search query. - searchQuery: null, - // Whether we are searching backwards. - searchIsReversed: false, - // Replace part of the last substituted pattern - lastSubstituteReplacePart: undefined, - jumpList: createCircularJumpList(), - macroModeState: new MacroModeState, - // Recording latest f, t, F or T motion command. - lastCharacterSearch: {increment:0, forward:true, selectedCharacter:''}, - registerController: new RegisterController({}), - // search history buffer - searchHistoryController: new HistoryController(), - // ex Command history buffer - exCommandHistoryController : new HistoryController() + } + } + /** + * @arg Codemirror + * @return {vimState} + */ + function maybeInitVimState(cm) { + if (!cm.state.vim) { + // Store instance state in the CodeMirror object. + cm.state.vim = { + inputState: new InputState(), + // Vim's input state that triggered the last edit, used to repeat + // motions and operators with '.'. + lastEditInputState: undefined, + // Vim's action command before the last edit, used to repeat actions + // with '.' and insert mode repeat. + lastEditActionCommand: undefined, + // When using jk for navigation, if you move from a longer line to a + // shorter line, the cursor may clip to the end of the shorter line. + // If j is pressed again and cursor goes to the next line, the + // cursor should go back to its horizontal position on the longer + // line if it can. This is to keep track of the horizontal position. + lastHPos: -1, + // Doing the same with screen-position for gj/gk + lastHSPos: -1, + // The last motion command run. Cleared if a non-motion command gets + // executed in between. + lastMotion: null, + marks: {}, + insertMode: false, + insertModeReturn: false, + // Repeat count for changes made in insert mode, triggered by key + // sequences like 3,i. Only exists when insertMode is true. + insertModeRepeat: undefined, + visualMode: false, + // If we are in visual line mode. No effect if visualMode is false. + visualLine: false, + visualBlock: false, + lastSelection: null, + lastPastedText: null, + sel: {}, + // Buffer-local/window-local values of vim options. + options: {}, + // Whether the next character should be interpreted literally + // Necassary for correct implementation of f, r etc. + // in terms of langmaps. + expectLiteralNext: false }; - for (var optionName in options) { - var option = options[optionName]; - option.value = option.defaultValue; + } + return cm.state.vim; + } + /** + * @type { + { + macroModeState: MacroModeState; + registerController: RegisterController; + searchHistoryController: HistoryController; + jumpList: any; + exCommandHistoryController: HistoryController; + lastCharacterSearch: any; + query?: any; + isReversed?: any; + lastSubstituteReplacePart: any; + searchQuery?: null; + searchIsReversed?: boolean; } } + */ + var vimGlobalState; + function resetVimGlobalState() { + vimGlobalState = { + // The current search query. + searchQuery: null, + // Whether we are searching backwards. + searchIsReversed: false, + // Replace part of the last substituted pattern + lastSubstituteReplacePart: undefined, + jumpList: createCircularJumpList(), + macroModeState: new MacroModeState(), + // Recording latest f, t, F or T motion command. + lastCharacterSearch: {increment:0, forward:true, selectedCharacter:''}, + registerController: new RegisterController({}), + // search history buffer + searchHistoryController: new HistoryController(), + // ex Command history buffer + exCommandHistoryController : new HistoryController() + }; + for (var optionName in options) { + var option = options[optionName]; + option.value = option.defaultValue; + } + } - var lastInsertModeKeyTimer; - var vimApi = { - enterVimMode: enterVimMode, - leaveVimMode: leaveVimMode, - buildKeyMap: function() { - // TODO: Convert keymap into dictionary format for fast lookup. - }, - // Testing hook, though it might be useful to expose the register - // controller anyway. - getRegisterController: function() { - return vimGlobalState.registerController; - }, - // Testing hook. - resetVimGlobalState_: resetVimGlobalState, - - // Testing hook. - getVimGlobalState_: function() { - return vimGlobalState; - }, - - // Testing hook. - maybeInitVimState_: maybeInitVimState, - - suppressErrorLogging: false, - - InsertModeKey: InsertModeKey, - map: function(lhs, rhs, ctx) { - // Add user defined key bindings. - exCommandDispatcher.map(lhs, rhs, ctx); - }, - unmap: function(lhs, ctx) { - return exCommandDispatcher.unmap(lhs, ctx); - }, - // Non-recursive map function. - // NOTE: This will not create mappings to key maps that aren't present - // in the default key map. See TODO at bottom of function. - noremap: function(lhs, rhs, ctx) { - exCommandDispatcher.map(lhs, rhs, ctx, true); - }, - // Remove all user-defined mappings for the provided context. - mapclear: function(ctx) { - // Partition the existing keymap into user-defined and true defaults. - var actualLength = defaultKeymap.length, - origLength = defaultKeymapLength; - var userKeymap = defaultKeymap.slice(0, actualLength - origLength); - defaultKeymap = defaultKeymap.slice(actualLength - origLength); - if (ctx) { - // If a specific context is being cleared, we need to keep mappings - // from all other contexts. - for (var i = userKeymap.length - 1; i >= 0; i--) { - var mapping = userKeymap[i]; - if (ctx !== mapping.context) { - if (mapping.context) { - this._mapCommand(mapping); - } else { - // `mapping` applies to all contexts so create keymap copies - // for each context except the one being cleared. - var contexts = ['normal', 'insert', 'visual']; - for (var j in contexts) { - if (contexts[j] !== ctx) { - var newMapping = {}; - for (var key in mapping) { - newMapping[key] = mapping[key]; - } - newMapping.context = contexts[j]; - this._mapCommand(newMapping); - } + /** @type {number | undefined|false} */ + var lastInsertModeKeyTimer; + var vimApi = { + enterVimMode: enterVimMode, + leaveVimMode: leaveVimMode, + buildKeyMap: function() { + // TODO: Convert keymap into dictionary format for fast lookup. + }, + // Testing hook, though it might be useful to expose the register + // controller anyway. + getRegisterController: function() { + return vimGlobalState.registerController; + }, + // Testing hook. + resetVimGlobalState_: resetVimGlobalState, + + // Testing hook. + getVimGlobalState_: function() { + return vimGlobalState; + }, + + // Testing hook. + maybeInitVimState_: maybeInitVimState, + + suppressErrorLogging: false, + + InsertModeKey: InsertModeKey, + /**@type {(lhs: string, rhs: string, ctx: string) => void} */ + map: function(lhs, rhs, ctx) { + // Add user defined key bindings. + exCommandDispatcher.map(lhs, rhs, ctx); + }, + /**@type {(lhs: string, ctx: string) => any} */ + unmap: function(lhs, ctx) { + return exCommandDispatcher.unmap(lhs, ctx); + }, + // Non-recursive map function. + // NOTE: This will not create mappings to key maps that aren't present + // in the default key map. See TODO at bottom of function. + /**@type {(lhs: string, rhs: string, ctx: string) => void} */ + noremap: function(lhs, rhs, ctx) { + exCommandDispatcher.map(lhs, rhs, ctx, true); + }, + // Remove all user-defined mappings for the provided context. + /**@arg {string} [ctx]} */ + mapclear: function(ctx) { + // Partition the existing keymap into user-defined and true defaults. + var actualLength = defaultKeymap.length, + origLength = defaultKeymapLength; + var userKeymap = defaultKeymap.slice(0, actualLength - origLength); + defaultKeymap = defaultKeymap.slice(actualLength - origLength); + if (ctx) { + // If a specific context is being cleared, we need to keep mappings + // from all other contexts. + for (var i = userKeymap.length - 1; i >= 0; i--) { + var mapping = userKeymap[i]; + if (ctx !== mapping.context) { + if (mapping.context) { + this._mapCommand(mapping); + } else { + // `mapping` applies to all contexts so create keymap copies + // for each context except the one being cleared. + var contexts = ['normal', 'insert', 'visual']; + for (var j in contexts) { + if (contexts[j] !== ctx) { + var newMapping = Object.assign({}, mapping); + newMapping.context = contexts[j]; + this._mapCommand(newMapping); } } } } } - }, - langmap: updateLangmap, - vimKeyFromEvent: vimKeyFromEvent, - // TODO: Expose setOption and getOption as instance methods. Need to decide how to namespace - // them, or somehow make them work with the existing CodeMirror setOption/getOption API. - setOption: setOption, - getOption: getOption, - defineOption: defineOption, - defineEx: function(name, prefix, func){ - if (!prefix) { - prefix = name; - } else if (name.indexOf(prefix) !== 0) { - throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered'); - } - exCommands[name]=func; - exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'}; - }, - handleKey: function (cm, key, origin) { - var command = this.findKey(cm, key, origin); - if (typeof command === 'function') { - return command(); - } - }, - multiSelectHandleKey: multiSelectHandleKey, + } + }, + langmap: updateLangmap, + vimKeyFromEvent: vimKeyFromEvent, + // TODO: Expose setOption and getOption as instance methods. Need to decide how to namespace + // them, or somehow make them work with the existing CodeMirror setOption/getOption API. + setOption: setOption, + getOption: getOption, + defineOption: defineOption, + /**@type {(name: string, prefix: string|undefined, func: ExFn) => void} */ + defineEx: function(name, prefix, func){ + if (!prefix) { + prefix = name; + } else if (name.indexOf(prefix) !== 0) { + throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered'); + } + exCommands[name]=func; + exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'}; + }, + /**@type {(cm: CodeMirror, key: string, origin: string) => undefined | boolean} */ + handleKey: function (cm, key, origin) { + var command = this.findKey(cm, key, origin); + if (typeof command === 'function') { + return command(); + } + }, + multiSelectHandleKey: multiSelectHandleKey, - /** - * This is the outermost function called by CodeMirror, after keys have - * been mapped to their Vim equivalents. - * - * Finds a command based on the key (and cached keys if there is a - * multi-key sequence). Returns `undefined` if no key is matched, a noop - * function if a partial match is found (multi-key), and a function to - * execute the bound command if a a key is matched. The function always - * returns true. - */ - findKey: function(cm, key, origin) { - var vim = maybeInitVimState(cm); + /** + * This is the outermost function called by CodeMirror, after keys have + * been mapped to their Vim equivalents. + * + * Finds a command based on the key (and cached keys if there is a + * multi-key sequence). Returns `undefined` if no key is matched, a noop + * function if a partial match is found (multi-key), and a function to + * execute the bound command if a a key is matched. The function always + * returns true. + */ + /**@type {(cm_: CodeMirror, key: string, origin?: string| undefined) => (() => boolean) | undefined} */ + findKey: function(cm_, key, origin) { + var vim = maybeInitVimState(cm_); + var cm = /**@type {CodeMirrorV}*/(cm_); - function handleMacroRecording() { - var macroModeState = vimGlobalState.macroModeState; - if (macroModeState.isRecording) { - if (key == 'q') { - macroModeState.exitMacroRecordMode(); - clearInputState(cm); - return true; - } - if (origin != 'mapping') { - logKey(macroModeState, key); - } - } - } - function handleEsc() { - if (key == '') { - if (vim.visualMode) { - // Get back to normal mode. - exitVisualMode(cm); - } else if (vim.insertMode) { - // Get back to normal mode. - exitInsertMode(cm); - } else { - // We're already in normal mode. Let '' be handled normally. - return; - } + function handleMacroRecording() { + var macroModeState = vimGlobalState.macroModeState; + if (macroModeState.isRecording) { + if (key == 'q') { + macroModeState.exitMacroRecordMode(); clearInputState(cm); return true; } + if (origin != 'mapping') { + logKey(macroModeState, key); + } } - - function handleKeyInsertMode() { - if (handleEsc()) { return true; } - vim.inputState.keyBuffer.push(key); - var keys = vim.inputState.keyBuffer.join(""); - var keysAreChars = key.length == 1; - var match = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert'); - var changeQueue = vim.inputState.changeQueue; - - if (match.type == 'none') { clearInputState(cm); return false; } - else if (match.type == 'partial') { - if (match.expectLiteralNext) vim.expectLiteralNext = true; - if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); } - lastInsertModeKeyTimer = keysAreChars && window.setTimeout( - function() { if (vim.insertMode && vim.inputState.keyBuffer.length) { clearInputState(cm); } }, - getOption('insertModeEscKeysTimeout')); - if (keysAreChars) { - var selections = cm.listSelections(); - if (!changeQueue || changeQueue.removed.length != selections.length) - changeQueue = vim.inputState.changeQueue = new ChangeQueue; - changeQueue.inserted += key; - for (var i = 0; i < selections.length; i++) { - var from = cursorMin(selections[i].anchor, selections[i].head); - var to = cursorMax(selections[i].anchor, selections[i].head); - var text = cm.getRange(from, cm.state.overwrite ? offsetCursor(to, 0, 1) : to); - changeQueue.removed[i] = (changeQueue.removed[i] || "") + text; - } - } - return !keysAreChars; + } + function handleEsc() { + if (key == '') { + if (vim.visualMode) { + // Get back to normal mode. + exitVisualMode(cm); + } else if (vim.insertMode) { + // Get back to normal mode. + exitInsertMode(cm); + } else { + // We're already in normal mode. Let '' be handled normally. + return; } - vim.expectLiteralNext = false; + clearInputState(cm); + return true; + } + } + function handleKeyInsertMode() { + if (handleEsc()) { return true; } + vim.inputState.keyBuffer.push(key); + var keys = vim.inputState.keyBuffer.join(""); + var keysAreChars = key.length == 1; + var match = commandDispatcher.matchCommand(keys, defaultKeymap, vim.inputState, 'insert'); + var changeQueue = vim.inputState.changeQueue; + + if (match.type == 'none') { clearInputState(cm); return false; } + else if (match.type == 'partial') { + if (match.expectLiteralNext) vim.expectLiteralNext = true; if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); } - if (match.command && changeQueue) { + lastInsertModeKeyTimer = keysAreChars && window.setTimeout( + function() { if (vim.insertMode && vim.inputState.keyBuffer.length) { clearInputState(cm); } }, + getOption('insertModeEscKeysTimeout')); + if (keysAreChars) { var selections = cm.listSelections(); + if (!changeQueue || changeQueue.removed.length != selections.length) + changeQueue = vim.inputState.changeQueue = new ChangeQueue; + changeQueue.inserted += key; for (var i = 0; i < selections.length; i++) { - var here = selections[i].head; - cm.replaceRange(changeQueue.removed[i] || "", - offsetCursor(here, 0, -changeQueue.inserted.length), here, '+input'); + var from = cursorMin(selections[i].anchor, selections[i].head); + var to = cursorMax(selections[i].anchor, selections[i].head); + var text = cm.getRange(from, cm.state.overwrite ? offsetCursor(to, 0, 1) : to); + changeQueue.removed[i] = (changeQueue.removed[i] || "") + text; } - vimGlobalState.macroModeState.lastInsertModeChanges.changes.pop(); } - if (!match.command) clearInputState(cm); - return match.command; + return !keysAreChars; } + vim.expectLiteralNext = false; - function handleKeyNonInsertMode() { - if (handleMacroRecording() || handleEsc()) { return true; } - - vim.inputState.keyBuffer.push(key); - var keys = vim.inputState.keyBuffer.join(""); - if (/^[1-9]\d*$/.test(keys)) { return true; } - - var keysMatcher = /^(\d*)(.*)$/.exec(keys); - if (!keysMatcher) { clearInputState(cm); return false; } - var context = vim.visualMode ? 'visual' : - 'normal'; - var mainKey = keysMatcher[2] || keysMatcher[1]; - if (vim.inputState.operatorShortcut && vim.inputState.operatorShortcut.slice(-1) == mainKey) { - // multikey operators act linewise by repeating only the last character - mainKey = vim.inputState.operatorShortcut; - } - var match = commandDispatcher.matchCommand(mainKey, defaultKeymap, vim.inputState, context); - if (match.type == 'none') { clearInputState(cm); return false; } - else if (match.type == 'partial') { - if (match.expectLiteralNext) vim.expectLiteralNext = true; - return true; - } - else if (match.type == 'clear') { clearInputState(cm); return true; } - vim.expectLiteralNext = false; - - vim.inputState.keyBuffer.length = 0; - keysMatcher = /^(\d*)(.*)$/.exec(keys); - if (keysMatcher[1] && keysMatcher[1] != '0') { - vim.inputState.pushRepeatDigit(keysMatcher[1]); + if (lastInsertModeKeyTimer) { window.clearTimeout(lastInsertModeKeyTimer); } + if (match.command && changeQueue) { + var selections = cm.listSelections(); + for (var i = 0; i < selections.length; i++) { + var here = selections[i].head; + cm.replaceRange(changeQueue.removed[i] || "", + offsetCursor(here, 0, -changeQueue.inserted.length), here, '+input'); } - return match.command; - } - - var command; - if (vim.insertMode) { command = handleKeyInsertMode(); } - else { command = handleKeyNonInsertMode(); } - if (command === false) { - return !vim.insertMode && key.length === 1 ? function() { return true; } : undefined; - } else if (command === true) { - // TODO: Look into using CodeMirror's multi-key handling. - // Return no-op since we are caching the key. Counts as handled, but - // don't want act on it just yet. - return function() { return true; }; - } else { - return function() { - return cm.operation(function() { - cm.curOp.isVimOp = true; - try { - if (command.type == 'keyToKey') { - doKeyToKey(cm, command.toKeys, command); - } else { - commandDispatcher.processCommand(cm, vim, command); - } - } catch (e) { - // clear VIM state in case it's in a bad state. - cm.state.vim = undefined; - maybeInitVimState(cm); - if (!vimApi.suppressErrorLogging) { - console['log'](e); - } - throw e; - } - return true; - }); - }; + vimGlobalState.macroModeState.lastInsertModeChanges.changes.pop(); } - }, - handleEx: function(cm, input) { - exCommandDispatcher.processCommand(cm, input); - }, - - defineMotion: defineMotion, - defineAction: defineAction, - defineOperator: defineOperator, - mapCommand: mapCommand, - _mapCommand: _mapCommand, + if (!match.command) clearInputState(cm); + return match.command; + } - defineRegister: defineRegister, + function handleKeyNonInsertMode() { + if (handleMacroRecording() || handleEsc()) { return true; } - exitVisualMode: exitVisualMode, - exitInsertMode: exitInsertMode - }; + vim.inputState.keyBuffer.push(key); + var keys = vim.inputState.keyBuffer.join(""); + if (/^[1-9]\d*$/.test(keys)) { return true; } - var keyToKeyStack = []; - var noremap = false; - var virtualPrompt; - function sendKeyToPrompt(key) { - if (key[0] == "<") { - var lowerKey = key.toLowerCase().slice(1, -1); - var parts = lowerKey.split('-'); - lowerKey = parts.pop() || ''; - if (lowerKey == 'lt') key = '<'; - else if (lowerKey == 'space') key = ' '; - else if (lowerKey == 'cr') key = '\n'; - else if (vimToCmKeyMap[lowerKey]) { - var value = virtualPrompt.value; - var event = { - key: vimToCmKeyMap[lowerKey], - target: { - value: value, - selectionEnd: value.length, - selectionStart: value.length - } - } - if (virtualPrompt.onKeyDown) { - virtualPrompt.onKeyDown(event, virtualPrompt.value, close); - } - if (virtualPrompt && virtualPrompt.onKeyUp) { - virtualPrompt.onKeyUp(event, virtualPrompt.value, close); - } - return; + var keysMatcher = /^(\d*)(.*)$/.exec(keys); + if (!keysMatcher) { clearInputState(cm); return false; } + var context = vim.visualMode ? 'visual' : + 'normal'; + var mainKey = keysMatcher[2] || keysMatcher[1]; + if (vim.inputState.operatorShortcut && vim.inputState.operatorShortcut.slice(-1) == mainKey) { + // multikey operators act linewise by repeating only the last character + mainKey = vim.inputState.operatorShortcut; } - } - if (key == '\n') { - var prompt = virtualPrompt; - virtualPrompt = null; - prompt.onClose && prompt.onClose(prompt.value); - } else { - virtualPrompt.value = (virtualPrompt.value || '') + key; - } + var match = commandDispatcher.matchCommand(mainKey, defaultKeymap, vim.inputState, context); + if (match.type == 'none') { clearInputState(cm); return false; } + else if (match.type == 'partial') { + if (match.expectLiteralNext) vim.expectLiteralNext = true; + return true; + } + else if (match.type == 'clear') { clearInputState(cm); return true; } + vim.expectLiteralNext = false; - function close(value) { - if (typeof value == 'string') { virtualPrompt.value = value; } - else { virtualPrompt = null; } - } - } - function doKeyToKey(cm, keys, fromKey) { - var noremapBefore = noremap; - // prevent infinite recursion. - if (fromKey) { - if (keyToKeyStack.indexOf(fromKey) != -1) return; - keyToKeyStack.push(fromKey); - noremap = fromKey.noremap != false; + vim.inputState.keyBuffer.length = 0; + keysMatcher = /^(\d*)(.*)$/.exec(keys); + if (keysMatcher && keysMatcher[1] && keysMatcher[1] != '0') { + vim.inputState.pushRepeatDigit(keysMatcher[1]); + } + return match.command; } - try { - var vim = maybeInitVimState(cm); - var keyRe = /<(?:[CSMA]-)*\w+>|./gi; - - var match; - // Pull off one command key, which is either a single character - // or a special sequence wrapped in '<' and '>', e.g. ''. - while ((match = keyRe.exec(keys))) { - var key = match[0]; - var wasInsert = vim.insertMode; - if (virtualPrompt) { - sendKeyToPrompt(key); - continue; - } - - var result = vimApi.handleKey(cm, key, 'mapping'); - - if (!result && wasInsert && vim.insertMode) { - if (key[0] == "<") { - var lowerKey = key.toLowerCase().slice(1, -1); - var parts = lowerKey.split('-'); - lowerKey = parts.pop() || ''; - if (lowerKey == 'lt') key = '<'; - else if (lowerKey == 'space') key = ' '; - else if (lowerKey == 'cr') key = '\n'; - else if (vimToCmKeyMap.hasOwnProperty(lowerKey)) { - // todo support codemirror keys in insertmode vimToCmKeyMap - key = vimToCmKeyMap[lowerKey]; - sendCmKey(cm, key); - continue; + var command; + if (vim.insertMode) { command = handleKeyInsertMode(); } + else { command = handleKeyNonInsertMode(); } + if (command === false) { + return !vim.insertMode && key.length === 1 ? function() { return true; } : undefined; + } else if (command === true) { + // TODO: Look into using CodeMirror's multi-key handling. + // Return no-op since we are caching the key. Counts as handled, but + // don't want act on it just yet. + return function() { return true; }; + } else { + return function() { + return cm.operation(function() { + // @ts-ignore + cm.curOp.isVimOp = true; + try { + if (command.type == 'keyToKey') { + doKeyToKey(cm, command.toKeys, command); } else { - key = key[0]; - keyRe.lastIndex = match.index + 1; + commandDispatcher.processCommand(cm, vim, command); + } + } catch (e) { + // clear VIM state in case it's in a bad state. + // @ts-ignore + cm.state.vim = undefined; + maybeInitVimState(cm); + if (!vimApi.suppressErrorLogging) { + console['log'](e); } + throw e; } - cm.replaceSelection(key); - } - } - } finally { - keyToKeyStack.pop(); - noremap = keyToKeyStack.length ? noremapBefore : false; - if (!keyToKeyStack.length && virtualPrompt) { - var promptOptions = virtualPrompt; - virtualPrompt = null; - showPrompt(cm, promptOptions); + return true; + }); + }; + } + }, + handleEx: function(cm, input) { + exCommandDispatcher.processCommand(cm, input); + }, + + defineMotion: defineMotion, + defineAction: defineAction, + defineOperator: defineOperator, + mapCommand: mapCommand, + _mapCommand: _mapCommand, + + defineRegister: defineRegister, + + exitVisualMode: exitVisualMode, + exitInsertMode: exitInsertMode + }; + + var keyToKeyStack = []; + var noremap = false; + var virtualPrompt; + function sendKeyToPrompt(key) { + if (key[0] == "<") { + var lowerKey = key.toLowerCase().slice(1, -1); + var parts = lowerKey.split('-'); + lowerKey = parts.pop() || ''; + if (lowerKey == 'lt') key = '<'; + else if (lowerKey == 'space') key = ' '; + else if (lowerKey == 'cr') key = '\n'; + else if (vimToCmKeyMap[lowerKey]) { + var value = virtualPrompt.value; + var event = { + key: vimToCmKeyMap[lowerKey], + target: { + value: value, + selectionEnd: value.length, + selectionStart: value.length + } + } + if (virtualPrompt.onKeyDown) { + virtualPrompt.onKeyDown(event, virtualPrompt.value, close); + } + if (virtualPrompt && virtualPrompt.onKeyUp) { + virtualPrompt.onKeyUp(event, virtualPrompt.value, close); } + return; } } + if (key == '\n') { + var prompt = virtualPrompt; + virtualPrompt = null; + prompt.onClose && prompt.onClose(prompt.value); + } else { + virtualPrompt.value = (virtualPrompt.value || '') + key; + } - var specialKey = { - Return: 'CR', Backspace: 'BS', 'Delete': 'Del', Escape: 'Esc', Insert: 'Ins', - ArrowLeft: 'Left', ArrowRight: 'Right', ArrowUp: 'Up', ArrowDown: 'Down', - Enter: 'CR', ' ': 'Space' - }; - var ignoredKeys = { Shift: 1, Alt: 1, Command: 1, Control: 1, - CapsLock: 1, AltGraph: 1, Dead: 1, Unidentified: 1 }; - - var vimToCmKeyMap = {}; - 'Left|Right|Up|Down|End|Home'.split('|').concat(Object.keys(specialKey)).forEach(function(x) { - vimToCmKeyMap[(specialKey[x] || '').toLowerCase()] - = vimToCmKeyMap[x.toLowerCase()] = x; - }); + function close(value) { + if (typeof value == 'string') { virtualPrompt.value = value; } + else { virtualPrompt = null; } + } + } + function doKeyToKey(cm, keys, fromKey) { + var noremapBefore = noremap; + // prevent infinite recursion. + if (fromKey) { + if (keyToKeyStack.indexOf(fromKey) != -1) return; + keyToKeyStack.push(fromKey); + noremap = fromKey.noremap != false; + } - function vimKeyFromEvent(e, vim) { - var key = e.key; - if (ignoredKeys[key]) return; - if (key.length > 1 && key[0] == "n") { - key = key.replace("Numpad", ""); - } - key = specialKey[key] || key; - - var name = ''; - if (e.ctrlKey) { name += 'C-'; } - if (e.altKey) { name += 'A-'; } - if (e.metaKey) { name += 'M-'; } - // on mac many characters are entered as option- combos - // (e.g. on swiss keyboard { is option-8) - // so we ignore lonely A- modifier for keypress event on mac - if (CodeMirror.isMac && e.altKey && !e.metaKey && !e.ctrlKey) { - name = name.slice(2); - } - if ((name || key.length > 1) && e.shiftKey) { name += 'S-'; } - - if (vim && !vim.expectLiteralNext && key.length == 1) { - if (langmap.keymap && key in langmap.keymap) { - if (langmap.remapCtrl != false || !name) - key = langmap.keymap[key]; - } else if (key.charCodeAt(0) > 255) { - var code = e.code?.slice(-1) || ""; - if (!e.shiftKey) code = code.toLowerCase(); - if (code) key = code; + try { + var vim = maybeInitVimState(cm); + var keyRe = /<(?:[CSMA]-)*\w+>|./gi; + + var match; + // Pull off one command key, which is either a single character + // or a special sequence wrapped in '<' and '>', e.g. ''. + while ((match = keyRe.exec(keys))) { + var key = match[0]; + var wasInsert = vim.insertMode; + if (virtualPrompt) { + sendKeyToPrompt(key); + continue; + } + + var result = vimApi.handleKey(cm, key, 'mapping'); + + if (!result && wasInsert && vim.insertMode) { + if (key[0] == "<") { + var lowerKey = key.toLowerCase().slice(1, -1); + var parts = lowerKey.split('-'); + lowerKey = parts.pop() || ''; + if (lowerKey == 'lt') key = '<'; + else if (lowerKey == 'space') key = ' '; + else if (lowerKey == 'cr') key = '\n'; + else if (vimToCmKeyMap.hasOwnProperty(lowerKey)) { + // todo support codemirror keys in insertmode vimToCmKeyMap + key = vimToCmKeyMap[lowerKey]; + sendCmKey(cm, key); + continue; + } else { + key = key[0]; + keyRe.lastIndex = match.index + 1; + } + } + cm.replaceSelection(key); } } + } finally { + keyToKeyStack.pop(); + noremap = keyToKeyStack.length ? noremapBefore : false; + if (!keyToKeyStack.length && virtualPrompt) { + var promptOptions = virtualPrompt; + virtualPrompt = null; + showPrompt(cm, promptOptions); + } + } + } - name += key; - if (name.length > 1) { name = '<' + name + '>'; } - return name; - }; + var specialKey = { + Return: 'CR', Backspace: 'BS', 'Delete': 'Del', Escape: 'Esc', Insert: 'Ins', + ArrowLeft: 'Left', ArrowRight: 'Right', ArrowUp: 'Up', ArrowDown: 'Down', + Enter: 'CR', ' ': 'Space' + }; + var ignoredKeys = { Shift: 1, Alt: 1, Command: 1, Control: 1, + CapsLock: 1, AltGraph: 1, Dead: 1, Unidentified: 1 }; + + var vimToCmKeyMap = {}; + 'Left|Right|Up|Down|End|Home'.split('|').concat(Object.keys(specialKey)).forEach(function(x) { + vimToCmKeyMap[(specialKey[x] || '').toLowerCase()] + = vimToCmKeyMap[x.toLowerCase()] = x; + }); + + function vimKeyFromEvent(e, vim) { + var key = e.key; + if (ignoredKeys[key]) return; + if (key.length > 1 && key[0] == "n") { + key = key.replace("Numpad", ""); + } + key = specialKey[key] || key; + + var name = ''; + if (e.ctrlKey) { name += 'C-'; } + if (e.altKey) { name += 'A-'; } + if (e.metaKey) { name += 'M-'; } + // on mac many characters are entered as option- combos + // (e.g. on swiss keyboard { is option-8) + // so we ignore lonely A- modifier for keypress event on mac + if (CodeMirror.isMac && e.altKey && !e.metaKey && !e.ctrlKey) { + name = name.slice(2); + } + if ((name || key.length > 1) && e.shiftKey) { name += 'S-'; } - // langmap support - function updateLangmap(langmapString, remapCtrl) { - if (langmap.string !== langmapString) { - langmap = parseLangmap(langmapString); + if (vim && !vim.expectLiteralNext && key.length == 1) { + if (langmap.keymap && key in langmap.keymap) { + if (langmap.remapCtrl != false || !name) + key = langmap.keymap[key]; + } else if (key.charCodeAt(0) > 255) { + var code = e.code?.slice(-1) || ""; + if (!e.shiftKey) code = code.toLowerCase(); + if (code) key = code; } - langmap.remapCtrl = remapCtrl; } - function parseLangmap(langmapString) { - // From :help langmap - /* - The 'langmap' option is a list of parts, separated with commas. Each - part can be in one of two forms: - 1. A list of pairs. Each pair is a "from" character immediately - followed by the "to" character. Examples: "aA", "aAbBcC". - 2. A list of "from" characters, a semi-colon and a list of "to" - characters. Example: "abc;ABC" - */ - let keymap = {}; - if (!langmapString) return { keymap: keymap, string: '' }; - - function getEscaped(list) { - return list.split(/\\?(.)/).filter(Boolean); - } - langmapString.split(/((?:[^\\,]|\\.)+),/).map(part => { - if (!part) return; - const semicolon = part.split(/((?:[^\\;]|\\.)+);/); - if (semicolon.length == 3) { - const from = getEscaped(semicolon[1]); - const to = getEscaped(semicolon[2]); - if (from.length !== to.length) return; // skip over malformed part - for (let i = 0; i < from.length; ++i) keymap[from[i]] = to[i]; - } else if (semicolon.length == 1) { - const pairs = getEscaped(part); - if (pairs.length % 2 !== 0) return; // skip over malformed part - for (let i = 0; i < pairs.length; i += 2) keymap[pairs[i]] = pairs[i + 1]; - } - }); + name += key; + if (name.length > 1) { name = '<' + name + '>'; } + return name; + }; - return { keymap: keymap, string: langmapString }; + // langmap support + function updateLangmap(langmapString, remapCtrl) { + if (langmap.string !== langmapString) { + langmap = parseLangmap(langmapString); } + langmap.remapCtrl = remapCtrl; + } + /** + * From :help langmap + * The 'langmap' option is a list of parts, separated with commas. Each + * part can be in one of two forms: + * 1. A list of pairs. Each pair is a "from" character immediately + * followed by the "to" character. Examples: "aA", "aAbBcC". + * 2. A list of "from" characters, a semi-colon and a list of "to" + * characters. Example: "abc;ABC" + * @arg {string} langmapString + * @returns {{string: string, keymap: Record, remapCtrl?: boolean}} + */ + function parseLangmap(langmapString) { + let keymap = ({})/**@type {Record}*/; + if (!langmapString) return { keymap: keymap, string: '' }; - defineOption('langmap', undefined, 'string', ['lmap'], function(name, cm) { - // The 'filetype' option proxies to the CodeMirror 'mode' option. - if (name === undefined) { - return langmap.string; - } else { - updateLangmap(name); + function getEscaped(list) { + return list.split(/\\?(.)/).filter(Boolean); + } + langmapString.split(/((?:[^\\,]|\\.)+),/).map(part => { + if (!part) return; + const semicolon = part.split(/((?:[^\\;]|\\.)+);/); + if (semicolon.length == 3) { + const from = getEscaped(semicolon[1]); + const to = getEscaped(semicolon[2]); + if (from.length !== to.length) return; // skip over malformed part + for (let i = 0; i < from.length; ++i) keymap[from[i]] = to[i]; + } else if (semicolon.length == 1) { + const pairs = getEscaped(part); + if (pairs.length % 2 !== 0) return; // skip over malformed part + for (let i = 0; i < pairs.length; i += 2) keymap[pairs[i]] = pairs[i + 1]; } }); - // Represents the current input state. - function InputState() { + return { keymap: keymap, string: langmapString }; + } + + defineOption('langmap', undefined, 'string', ['lmap'], function(name, cm) { + // The 'filetype' option proxies to the CodeMirror 'mode' option. + if (name === undefined) { + return langmap.string; + } else { + updateLangmap(name); + } + }); + + // Represents the current input state. + class InputState { + constructor() { this.prefixRepeat = []; this.motionRepeat = []; @@ -1147,14 +1235,14 @@ export function initVim(CodeMirror) { this.registerName = null; // Defaults to the unnamed register. this.changeQueue = null; // For restoring text used by insert mode keybindings } - InputState.prototype.pushRepeatDigit = function(n) { + pushRepeatDigit(n) { if (!this.operator) { this.prefixRepeat = this.prefixRepeat.concat(n); } else { this.motionRepeat = this.motionRepeat.concat(n); } - }; - InputState.prototype.getRepeat = function() { + } + getRepeat() { var repeat = 0; if (this.prefixRepeat.length > 0 || this.motionRepeat.length > 0) { repeat = 1; @@ -1166,94 +1254,100 @@ export function initVim(CodeMirror) { } } return repeat; - }; - - function clearInputState(cm, reason) { - cm.state.vim.inputState = new InputState(); - cm.state.vim.expectLiteralNext = false; - CodeMirror.signal(cm, 'vim-command-done', reason); } + } - function ChangeQueue() { - this.removed = []; - this.inserted = ""; - } + /** @arg {CodeMirrorV} cm @arg {string} [reason] */ + function clearInputState(cm, reason) { + cm.state.vim.inputState = new InputState(); + cm.state.vim.expectLiteralNext = false; + CodeMirror.signal(cm, 'vim-command-done', reason); + } - /* - * Register stores information about copy and paste registers. Besides - * text, a register must store whether it is linewise (i.e., when it is - * pasted, should it insert itself into a new line, or should the text be - * inserted at the cursor position.) - */ - function Register(text, linewise, blockwise) { - this.clear(); + function ChangeQueue() { + this.removed = []; + this.inserted = ""; + } + + /** + * Register stores information about copy and paste registers. Besides + * text, a register must store whether it is linewise (i.e., when it is + * pasted, should it insert itself into a new line, or should the text be + * inserted at the cursor position.) + */ + class Register { + constructor(text, linewise, blockwise) { + this.clear(); this.keyBuffer = [text || '']; this.insertModeChanges = []; this.searchQueries = []; this.linewise = !!linewise; this.blockwise = !!blockwise; } - Register.prototype = { - setText: function(text, linewise, blockwise) { - this.keyBuffer = [text || '']; - this.linewise = !!linewise; - this.blockwise = !!blockwise; - }, - pushText: function(text, linewise) { - // if this register has ever been set to linewise, use linewise. - if (linewise) { - if (!this.linewise) { - this.keyBuffer.push('\n'); - } - this.linewise = true; + setText(text, linewise, blockwise) { + this.keyBuffer = [text || '']; + this.linewise = !!linewise; + this.blockwise = !!blockwise; + } + pushText(text, linewise) { + // if this register has ever been set to linewise, use linewise. + if (linewise) { + if (!this.linewise) { + this.keyBuffer.push('\n'); } - this.keyBuffer.push(text); - }, - pushInsertModeChanges: function(changes) { - this.insertModeChanges.push(createInsertModeChanges(changes)); - }, - pushSearchQuery: function(query) { - this.searchQueries.push(query); - }, - clear: function() { - this.keyBuffer = []; - this.insertModeChanges = []; - this.searchQueries = []; - this.linewise = false; - }, - toString: function() { - return this.keyBuffer.join(''); + this.linewise = true; } - }; + this.keyBuffer.push(text); + } + pushInsertModeChanges(changes) { + this.insertModeChanges.push(createInsertModeChanges(changes)); + } + pushSearchQuery(query) { + this.searchQueries.push(query); + } + clear() { + this.keyBuffer = []; + this.insertModeChanges = []; + this.searchQueries = []; + this.linewise = false; + } + toString() { + return this.keyBuffer.join(''); + } + } - /** - * Defines an external register. - * - * The name should be a single character that will be used to reference the register. - * The register should support setText, pushText, clear, and toString(). See Register - * for a reference implementation. - */ - function defineRegister(name, register) { - var registers = vimGlobalState.registerController.registers; - if (!name || name.length != 1) { - throw Error('Register name must be 1 character'); - } - if (registers[name]) { - throw Error('Register already defined ' + name); - } - registers[name] = register; - validRegisters.push(name); + /** + * Defines an external register. + * + * The name should be a single character that will be used to reference the register. + * The register should support setText, pushText, clear, and toString(). See Register + * for a reference implementation. + * @arg {string} name + * @arg {Register} register + */ + function defineRegister(name, register) { + var registers = vimGlobalState.registerController.registers; + if (!name || name.length != 1) { + throw Error('Register name must be 1 character'); + } + if (registers[name]) { + throw Error('Register already defined ' + name); } + registers[name] = register; + validRegisters.push(name); + } - /* - * vim registers allow you to keep many independent copy and paste buffers. - * See http://usevim.com/2012/04/13/registers/ for an introduction. - * - * RegisterController keeps the state of all the registers. An initial - * state may be passed in. The unnamed register '"' will always be - * overridden. - */ - function RegisterController(registers) { + /** + * vim registers allow you to keep many independent copy and paste buffers. + * See http://usevim.com/2012/04/13/registers/ for an introduction. + * + * RegisterController keeps the state of all the registers. An initial + * state may be passed in. The unnamed register '"' will always be + * overridden. + */ + class RegisterController { + /** @arg {Object} registers */ + constructor(registers) { this.registers = registers; this.unnamedRegister = registers['"'] = new Register(); registers['.'] = new Register(); @@ -1261,5062 +1355,5388 @@ export function initVim(CodeMirror) { registers['/'] = new Register(); registers['+'] = new Register(); } - RegisterController.prototype = { - pushText: function(registerName, operator, text, linewise, blockwise) { - // The black hole register, "_, means delete/yank to nowhere. - if (registerName === '_') return; - if (linewise && text.charAt(text.length - 1) !== '\n'){ - text += '\n'; - } - // Lowercase and uppercase registers refer to the same register. - // Uppercase just means append. - var register = this.isValidRegister(registerName) ? - this.getRegister(registerName) : null; - // if no register/an invalid register was specified, things go to the - // default registers - if (!register) { - switch (operator) { - case 'yank': - // The 0 register contains the text from the most recent yank. - this.registers['0'] = new Register(text, linewise, blockwise); - break; - case 'delete': - case 'change': - if (text.indexOf('\n') == -1) { - // Delete less than 1 line. Update the small delete register. - this.registers['-'] = new Register(text, linewise); - } else { - // Shift down the contents of the numbered registers and put the - // deleted text into register 1. - this.shiftNumericRegisters_(); - this.registers['1'] = new Register(text, linewise); - } - break; - } - // Make sure the unnamed register is set to what just happened - this.unnamedRegister.setText(text, linewise, blockwise); - return; + pushText(registerName, operator, text, linewise, blockwise) { + // The black hole register, "_, means delete/yank to nowhere. + if (registerName === '_') return; + if (linewise && text.charAt(text.length - 1) !== '\n') { + text += '\n'; + } + // Lowercase and uppercase registers refer to the same register. + // Uppercase just means append. + var register = this.isValidRegister(registerName) ? + this.getRegister(registerName) : null; + // if no register/an invalid register was specified, things go to the + // default registers + if (!register) { + switch (operator) { + case 'yank': + // The 0 register contains the text from the most recent yank. + this.registers['0'] = new Register(text, linewise, blockwise); + break; + case 'delete': + case 'change': + if (text.indexOf('\n') == -1) { + // Delete less than 1 line. Update the small delete register. + this.registers['-'] = new Register(text, linewise); + } else { + // Shift down the contents of the numbered registers and put the + // deleted text into register 1. + this.shiftNumericRegisters_(); + this.registers['1'] = new Register(text, linewise); + } + break; } + // Make sure the unnamed register is set to what just happened + this.unnamedRegister.setText(text, linewise, blockwise); + return; + } - // If we've gotten to this point, we've actually specified a register - var append = isUpperCase(registerName); - if (append) { - register.pushText(text, linewise); - } else { - register.setText(text, linewise, blockwise); - } - if (registerName === '+') { - navigator.clipboard.writeText(text); - } - // The unnamed register always has the same value as the last used - // register. - this.unnamedRegister.setText(register.toString(), linewise); - }, - // Gets the register named @name. If one of @name doesn't already exist, - // create it. If @name is invalid, return the unnamedRegister. - getRegister: function(name) { - if (!this.isValidRegister(name)) { - return this.unnamedRegister; - } - name = name.toLowerCase(); - if (!this.registers[name]) { - this.registers[name] = new Register(); - } - return this.registers[name]; - }, - isValidRegister: function(name) { - return name && (inArray(name, validRegisters) || latinCharRegex.test(name)); - }, - shiftNumericRegisters_: function() { - for (var i = 9; i >= 2; i--) { - this.registers[i] = this.getRegister('' + (i - 1)); - } + // If we've gotten to this point, we've actually specified a register + var append = isUpperCase(registerName); + if (append) { + register.pushText(text, linewise); + } else { + register.setText(text, linewise, blockwise); } - }; - function HistoryController() { - this.historyBuffer = []; - this.iterator = 0; - this.initialPrefix = null; - } - HistoryController.prototype = { - // the input argument here acts a user entered prefix for a small time - // until we start autocompletion in which case it is the autocompleted. - nextMatch: function (input, up) { - var historyBuffer = this.historyBuffer; - var dir = up ? -1 : 1; - if (this.initialPrefix === null) this.initialPrefix = input; - for (var i = this.iterator + dir; up ? i >= 0 : i < historyBuffer.length; i+= dir) { - var element = historyBuffer[i]; - for (var j = 0; j <= element.length; j++) { - if (this.initialPrefix == element.substring(0, j)) { - this.iterator = i; - return element; - } - } - } - // should return the user input in case we reach the end of buffer. - if (i >= historyBuffer.length) { - this.iterator = historyBuffer.length; - return this.initialPrefix; - } - // return the last autocompleted query or exCommand as it is. - if (i < 0 ) return input; - }, - pushInput: function(input) { - var index = this.historyBuffer.indexOf(input); - if (index > -1) this.historyBuffer.splice(index, 1); - if (input.length) this.historyBuffer.push(input); - }, - reset: function() { - this.initialPrefix = null; - this.iterator = this.historyBuffer.length; + if (registerName === '+') { + navigator.clipboard.writeText(text); + } + // The unnamed register always has the same value as the last used + // register. + this.unnamedRegister.setText(register.toString(), linewise); + } + /** + * Gets the register named @name. If one of @name doesn't already exist, + * create it. If @name is invalid, return the unnamedRegister. + * @arg {string} [name] + */ + getRegister(name) { + if (!this.isValidRegister(name)) { + return this.unnamedRegister; + } + name = name.toLowerCase(); + if (!this.registers[name]) { + this.registers[name] = new Register(); + } + return this.registers[name]; + } + /**@type {{(name: any): name is string}} */ + isValidRegister(name) { + return name && (inArray(name, validRegisters) || latinCharRegex.test(name)); + } + shiftNumericRegisters_() { + for (var i = 9; i >= 2; i--) { + this.registers[i] = this.getRegister('' + (i - 1)); + } + } + } + class HistoryController { + constructor() { + this.historyBuffer = []; + this.iterator = 0; + this.initialPrefix = null; + } + // the input argument here acts a user entered prefix for a small time + // until we start autocompletion in which case it is the autocompleted. + nextMatch(input, up) { + var historyBuffer = this.historyBuffer; + var dir = up ? -1 : 1; + if (this.initialPrefix === null) this.initialPrefix = input; + for (var i = this.iterator + dir; up ? i >= 0 : i < historyBuffer.length; i += dir) { + var element = historyBuffer[i]; + for (var j = 0; j <= element.length; j++) { + if (this.initialPrefix == element.substring(0, j)) { + this.iterator = i; + return element; + } + } + } + // should return the user input in case we reach the end of buffer. + if (i >= historyBuffer.length) { + this.iterator = historyBuffer.length; + return this.initialPrefix; + } + // return the last autocompleted query or exCommand as it is. + if (i < 0) return input; + } + pushInput(input) { + var index = this.historyBuffer.indexOf(input); + if (index > -1) this.historyBuffer.splice(index, 1); + if (input.length) this.historyBuffer.push(input); + } + reset() { + this.initialPrefix = null; + this.iterator = this.historyBuffer.length; + } + } + var commandDispatcher = { + matchCommand: function(keys, keyMap, inputState, context) { + var matches = commandMatches(keys, keyMap, context, inputState); + if (!matches.full && !matches.partial) { + return {type: 'none'}; + } else if (!matches.full && matches.partial) { + return { + type: 'partial', + expectLiteralNext: matches.partial.length == 1 && matches.partial[0].keys.slice(-11) == '' // langmap literal logic + }; } - }; - var commandDispatcher = { - matchCommand: function(keys, keyMap, inputState, context) { - var matches = commandMatches(keys, keyMap, context, inputState); - if (!matches.full && !matches.partial) { - return {type: 'none'}; - } else if (!matches.full && matches.partial) { - return { - type: 'partial', - expectLiteralNext: matches.partial.length == 1 && matches.partial[0].keys.slice(-11) == '' // langmap literal logic - }; - } - var bestMatch; - for (var i = 0; i < matches.full.length; i++) { - var match = matches.full[i]; - if (!bestMatch) { - bestMatch = match; - } - } - if (bestMatch.keys.slice(-11) == '' || bestMatch.keys.slice(-10) == '') { - var character = lastChar(keys); - if (!character || character.length > 1) return {type: 'clear'}; - inputState.selectedCharacter = character; - } - return {type: 'full', command: bestMatch}; - }, - processCommand: function(cm, vim, command) { - vim.inputState.repeatOverride = command.repeatOverride; - switch (command.type) { - case 'motion': - this.processMotion(cm, vim, command); - break; - case 'operator': - this.processOperator(cm, vim, command); - break; - case 'operatorMotion': - this.processOperatorMotion(cm, vim, command); - break; - case 'action': - this.processAction(cm, vim, command); - break; - case 'search': - this.processSearch(cm, vim, command); - break; - case 'ex': - case 'keyToEx': - this.processEx(cm, vim, command); - break; - default: - break; - } - }, - processMotion: function(cm, vim, command) { - vim.inputState.motion = command.motion; - vim.inputState.motionArgs = copyArgs(command.motionArgs); - this.evalInput(cm, vim); - }, - processOperator: function(cm, vim, command) { - var inputState = vim.inputState; - if (inputState.operator) { - if (inputState.operator == command.operator) { - // Typing an operator twice like 'dd' makes the operator operate - // linewise - inputState.motion = 'expandToLine'; - inputState.motionArgs = { linewise: true }; - this.evalInput(cm, vim); - return; - } else { - // 2 different operators in a row doesn't make sense. - clearInputState(cm); - } - } - inputState.operator = command.operator; - inputState.operatorArgs = copyArgs(command.operatorArgs); - if (command.keys.length > 1) { - inputState.operatorShortcut = command.keys; + var bestMatch; + // @ts-ignore + for (var i = 0; i < matches.full.length; i++) { + var match = matches.full[i]; + if (!bestMatch) { + bestMatch = match; } - if (command.exitVisualBlock) { - vim.visualBlock = false; - updateCmSelection(cm); - } - if (vim.visualMode) { - // Operating on a selection in visual mode. We don't need a motion. + } + if (bestMatch.keys.slice(-11) == '' || bestMatch.keys.slice(-10) == '') { + var character = lastChar(keys); + if (!character || character.length > 1) return {type: 'clear'}; + inputState.selectedCharacter = character; + } + return {type: 'full', command: bestMatch}; + }, + /** + * @arg {CodeMirrorV} cm + * @arg {vimState} vim + * @arg {vimKey} command + */ + processCommand: function(cm, vim, command) { + vim.inputState.repeatOverride = command.repeatOverride; + switch (command.type) { + case 'motion': + this.processMotion(cm, vim, command); + break; + case 'operator': + this.processOperator(cm, vim, command); + break; + case 'operatorMotion': + this.processOperatorMotion(cm, vim, command); + break; + case 'action': + this.processAction(cm, vim, command); + break; + case 'search': + this.processSearch(cm, vim, command); + break; + case 'ex': + case 'keyToEx': + this.processEx(cm, vim, command); + break; + default: + break; + } + }, + /** + * @arg {CodeMirrorV} cm + * @arg {vimState} vim + * @arg {import("./types").motionCommand|import("./types").operatorMotionCommand} command + */ + processMotion: function(cm, vim, command) { + vim.inputState.motion = command.motion; + vim.inputState.motionArgs = /**@type {MotionArgs}*/(copyArgs(command.motionArgs)); + this.evalInput(cm, vim); + }, + /** + * @arg {CodeMirrorV} cm + * @arg {vimState} vim + * @arg {import("./types").operatorCommand|import("./types").operatorMotionCommand} command + */ + processOperator: function(cm, vim, command) { + var inputState = vim.inputState; + if (inputState.operator) { + if (inputState.operator == command.operator) { + // Typing an operator twice like 'dd' makes the operator operate + // linewise + inputState.motion = 'expandToLine'; + inputState.motionArgs = { linewise: true, repeat: 1 }; this.evalInput(cm, vim); + return; + } else { + // 2 different operators in a row doesn't make sense. + clearInputState(cm); } - }, - processOperatorMotion: function(cm, vim, command) { - var visualMode = vim.visualMode; - var operatorMotionArgs = copyArgs(command.operatorMotionArgs); - if (operatorMotionArgs) { - // Operator motions may have special behavior in visual mode. - if (visualMode && operatorMotionArgs.visualLine) { - vim.visualLine = true; - } + } + inputState.operator = command.operator; + inputState.operatorArgs = copyArgs(command.operatorArgs); + if (command.keys.length > 1) { + inputState.operatorShortcut = command.keys; + } + if (command.exitVisualBlock) { + vim.visualBlock = false; + updateCmSelection(cm); + } + if (vim.visualMode) { + // Operating on a selection in visual mode. We don't need a motion. + this.evalInput(cm, vim); + } + }, + /** + * @arg {CodeMirrorV} cm + * @arg {vimState} vim + * @arg {import("./types").operatorMotionCommand} command + */ + processOperatorMotion: function(cm, vim, command) { + var visualMode = vim.visualMode; + var operatorMotionArgs = copyArgs(command.operatorMotionArgs); + if (operatorMotionArgs) { + // Operator motions may have special behavior in visual mode. + if (visualMode && operatorMotionArgs.visualLine) { + vim.visualLine = true; } + } + this.processOperator(cm, vim, command); + if (!visualMode) { + this.processMotion(cm, vim, command); + } + }, + /** + * @arg {CodeMirrorV} cm + * @arg {vimState} vim + * @arg {import("./types").actionCommand} command + */ + processAction: function(cm, vim, command) { + var inputState = vim.inputState; + var repeat = inputState.getRepeat(); + var repeatIsExplicit = !!repeat; + var actionArgs = /**@type {ActionArgs}*/(copyArgs(command.actionArgs) || {repeat: 1}); + if (inputState.selectedCharacter) { + actionArgs.selectedCharacter = inputState.selectedCharacter; + } + // Actions may or may not have motions and operators. Do these first. + if (command.operator) { + // @ts-ignore this.processOperator(cm, vim, command); - if (!visualMode) { - this.processMotion(cm, vim, command); - } - }, - processAction: function(cm, vim, command) { - var inputState = vim.inputState; - var repeat = inputState.getRepeat(); - var repeatIsExplicit = !!repeat; - var actionArgs = copyArgs(command.actionArgs) || {}; - if (inputState.selectedCharacter) { - actionArgs.selectedCharacter = inputState.selectedCharacter; - } - // Actions may or may not have motions and operators. Do these first. - if (command.operator) { - this.processOperator(cm, vim, command); + } + if (command.motion) { + // @ts-ignore + this.processMotion(cm, vim, command); + } + if (command.motion || command.operator) { + this.evalInput(cm, vim); + } + actionArgs.repeat = repeat || 1; + actionArgs.repeatIsExplicit = repeatIsExplicit; + actionArgs.registerName = inputState.registerName; + clearInputState(cm); + vim.lastMotion = null; + if (command.isEdit) { + this.recordLastEdit(vim, inputState, command); + } + actions[command.action](cm, actionArgs, vim); + }, + /** @arg {CodeMirrorV} cm @arg {vimState} vim @arg {import("./types").searchCommand} command*/ + processSearch: function(cm, vim, command) { + if (!cm.getSearchCursor) { + // Search depends on SearchCursor. + return; + } + var forward = command.searchArgs.forward; + var wholeWordOnly = command.searchArgs.wholeWordOnly; + getSearchState(cm).setReversed(!forward); + var promptPrefix = (forward) ? '/' : '?'; + var originalQuery = getSearchState(cm).getQuery(); + var originalScrollPos = cm.getScrollInfo(); + /** @arg {string} query @arg {boolean} ignoreCase @arg {boolean} smartCase */ + function handleQuery(query, ignoreCase, smartCase) { + vimGlobalState.searchHistoryController.pushInput(query); + vimGlobalState.searchHistoryController.reset(); + try { + updateSearchQuery(cm, query, ignoreCase, smartCase); + } catch (e) { + showConfirm(cm, 'Invalid regex: ' + query); + clearInputState(cm); + return; } - if (command.motion) { - this.processMotion(cm, vim, command); + commandDispatcher.processMotion(cm, vim, { + keys: '', + type: 'motion', + motion: 'findNext', + motionArgs: { forward: true, toJumplist: command.searchArgs.toJumplist } + }); + } + /** @arg {string} query */ + function onPromptClose(query) { + cm.scrollTo(originalScrollPos.left, originalScrollPos.top); + handleQuery(query, true /** ignoreCase */, true /** smartCase */); + var macroModeState = vimGlobalState.macroModeState; + if (macroModeState.isRecording) { + logSearchQuery(macroModeState, query); } - if (command.motion || command.operator) { - this.evalInput(cm, vim); + } + /** + * @arg {KeyboardEvent&{target:HTMLInputElement}} e + * @arg {any} query + * @arg {(arg0: any) => void} close + */ + function onPromptKeyUp(e, query, close) { + var keyName = vimKeyFromEvent(e), up, offset; + if (keyName == '' || keyName == '') { + up = keyName == '' ? true : false; + offset = e.target ? e.target.selectionEnd : 0; + query = vimGlobalState.searchHistoryController.nextMatch(query, up) || ''; + close(query); + if (offset && e.target) e.target.selectionEnd = e.target.selectionStart = Math.min(offset, e.target.value.length); + } else if (keyName && keyName != '' && keyName != '') { + vimGlobalState.searchHistoryController.reset(); } - actionArgs.repeat = repeat || 1; - actionArgs.repeatIsExplicit = repeatIsExplicit; - actionArgs.registerName = inputState.registerName; - clearInputState(cm); - vim.lastMotion = null; - if (command.isEdit) { - this.recordLastEdit(vim, inputState, command); + var parsedQuery; + try { + parsedQuery = updateSearchQuery(cm, query, + true /** ignoreCase */, true /** smartCase */); + } catch (e) { + // Swallow bad regexes for incremental search. } - actions[command.action](cm, actionArgs, vim); - }, - processSearch: function(cm, vim, command) { - if (!cm.getSearchCursor) { - // Search depends on SearchCursor. - return; + if (parsedQuery) { + cm.scrollIntoView(findNext(cm, !forward, parsedQuery), 30); + } else { + clearSearchHighlight(cm); + cm.scrollTo(originalScrollPos.left, originalScrollPos.top); } - var forward = command.searchArgs.forward; - var wholeWordOnly = command.searchArgs.wholeWordOnly; - getSearchState(cm).setReversed(!forward); - var promptPrefix = (forward) ? '/' : '?'; - var originalQuery = getSearchState(cm).getQuery(); - var originalScrollPos = cm.getScrollInfo(); - function handleQuery(query, ignoreCase, smartCase) { + } + /** @arg {KeyboardEvent} e @arg {string} query @arg {(arg0?: string) => void} close */ + function onPromptKeyDown(e, query, close) { + var keyName = vimKeyFromEvent(e); + if (keyName == '' || keyName == '' || keyName == '' || + (keyName == '' && query == '')) { vimGlobalState.searchHistoryController.pushInput(query); vimGlobalState.searchHistoryController.reset(); - try { - updateSearchQuery(cm, query, ignoreCase, smartCase); - } catch (e) { - showConfirm(cm, 'Invalid regex: ' + query); - clearInputState(cm); - return; - } - commandDispatcher.processMotion(cm, vim, { - type: 'motion', - motion: 'findNext', - motionArgs: { forward: true, toJumplist: command.searchArgs.toJumplist } - }); - } - function onPromptClose(query) { + updateSearchQuery(cm, originalQuery); + clearSearchHighlight(cm); cm.scrollTo(originalScrollPos.left, originalScrollPos.top); - handleQuery(query, true /** ignoreCase */, true /** smartCase */); + CodeMirror.e_stop(e); + clearInputState(cm); + close(); + cm.focus(); + } else if (keyName == '' || keyName == '') { + CodeMirror.e_stop(e); + } else if (keyName == '') { + // Ctrl-U clears input. + CodeMirror.e_stop(e); + close(''); + } + } + switch (command.searchArgs.querySrc) { + case 'prompt': var macroModeState = vimGlobalState.macroModeState; - if (macroModeState.isRecording) { - logSearchQuery(macroModeState, query); - } - } - function onPromptKeyUp(e, query, close) { - var keyName = vimKeyFromEvent(e), up, offset; - if (keyName == '' || keyName == '') { - up = keyName == '' ? true : false; - offset = e.target ? e.target.selectionEnd : 0; - query = vimGlobalState.searchHistoryController.nextMatch(query, up) || ''; - close(query); - if (offset && e.target) e.target.selectionEnd = e.target.selectionStart = Math.min(offset, e.target.value.length); - } else if (keyName && keyName != '' && keyName != '') { - vimGlobalState.searchHistoryController.reset(); - } - var parsedQuery; - try { - parsedQuery = updateSearchQuery(cm, query, - true /** ignoreCase */, true /** smartCase */); - } catch (e) { - // Swallow bad regexes for incremental search. - } - if (parsedQuery) { - cm.scrollIntoView(findNext(cm, !forward, parsedQuery), 30); + if (macroModeState.isPlaying) { + let query = macroModeState.replaySearchQueries.shift(); + handleQuery(query, true /** ignoreCase */, false /** smartCase */); } else { - clearSearchHighlight(cm); - cm.scrollTo(originalScrollPos.left, originalScrollPos.top); + showPrompt(cm, { + onClose: onPromptClose, + prefix: promptPrefix, + desc: '(JavaScript regexp)', + onKeyUp: onPromptKeyUp, + onKeyDown: onPromptKeyDown + }); } - } - function onPromptKeyDown(e, query, close) { - var keyName = vimKeyFromEvent(e); - if (keyName == '' || keyName == '' || keyName == '' || - (keyName == '' && query == '')) { - vimGlobalState.searchHistoryController.pushInput(query); - vimGlobalState.searchHistoryController.reset(); - updateSearchQuery(cm, originalQuery); - clearSearchHighlight(cm); - cm.scrollTo(originalScrollPos.left, originalScrollPos.top); - CodeMirror.e_stop(e); + break; + case 'wordUnderCursor': + var word = expandWordUnderCursor(cm, {noSymbol: true}); + var isKeyword = true; + if (!word) { + word = expandWordUnderCursor(cm, {noSymbol: false}); + isKeyword = false; + } + if (!word) { + showConfirm(cm, 'No word under cursor'); clearInputState(cm); - close(); - cm.focus(); - } else if (keyName == '' || keyName == '') { - CodeMirror.e_stop(e); - } else if (keyName == '') { - // Ctrl-U clears input. - CodeMirror.e_stop(e); - close(''); + return; + } + let query = cm.getLine(word.start.line).substring(word.start.ch, + word.end.ch); + if (isKeyword && wholeWordOnly) { + query = '\\b' + query + '\\b'; + } else { + query = escapeRegex(query); } - } - switch (command.searchArgs.querySrc) { - case 'prompt': - var macroModeState = vimGlobalState.macroModeState; - if (macroModeState.isPlaying) { - var query = macroModeState.replaySearchQueries.shift(); - handleQuery(query, true /** ignoreCase */, false /** smartCase */); - } else { - showPrompt(cm, { - onClose: onPromptClose, - prefix: promptPrefix, - desc: '(JavaScript regexp)', - onKeyUp: onPromptKeyUp, - onKeyDown: onPromptKeyDown - }); - } - break; - case 'wordUnderCursor': - var word = expandWordUnderCursor(cm, {noSymbol: true}); - var isKeyword = true; - if (!word) { - word = expandWordUnderCursor(cm, {noSymbol: false}); - isKeyword = false; - } - if (!word) { - showConfirm(cm, 'No word under cursor'); - clearInputState(cm); - return; - } - var query = cm.getLine(word.start.line).substring(word.start.ch, - word.end.ch); - if (isKeyword && wholeWordOnly) { - query = '\\b' + query + '\\b'; - } else { - query = escapeRegex(query); - } - // cachedCursor is used to save the old position of the cursor - // when * or # causes vim to seek for the nearest word and shift - // the cursor before entering the motion. - vimGlobalState.jumpList.cachedCursor = cm.getCursor(); - cm.setCursor(word.start); + // cachedCursor is used to save the old position of the cursor + // when * or # causes vim to seek for the nearest word and shift + // the cursor before entering the motion. + vimGlobalState.jumpList.cachedCursor = cm.getCursor(); + cm.setCursor(word.start); - handleQuery(query, true /** ignoreCase */, false /** smartCase */); - break; - } - }, - processEx: function(cm, vim, command) { - function onPromptClose(input) { - // Give the prompt some time to close so that if processCommand shows - // an error, the elements don't overlap. + handleQuery(query, true /** ignoreCase */, false /** smartCase */); + break; + } + }, + /** + * @arg {CodeMirrorV} cm + * @arg {vimState} vim + * @arg {import("./types").exCommand | import("./types").keyToExCommand} command + */ + processEx: function(cm, vim, command) { + /**@arg {string} input*/ + function onPromptClose(input) { + // Give the prompt some time to close so that if processCommand shows + // an error, the elements don't overlap. + vimGlobalState.exCommandHistoryController.pushInput(input); + vimGlobalState.exCommandHistoryController.reset(); + exCommandDispatcher.processCommand(cm, input); + if (cm.state.vim) clearInputState(cm); + } + /** + * @arg {KeyboardEvent&{target:HTMLInputElement}} e + * @arg {string} input + * @arg {(arg0?: string) => void} close + */ + function onPromptKeyDown(e, input, close) { + var keyName = vimKeyFromEvent(e), up, offset; + if (keyName == '' || keyName == '' || keyName == '' || + (keyName == '' && input == '')) { vimGlobalState.exCommandHistoryController.pushInput(input); vimGlobalState.exCommandHistoryController.reset(); - exCommandDispatcher.processCommand(cm, input); - if (cm.state.vim) clearInputState(cm); - } - function onPromptKeyDown(e, input, close) { - var keyName = vimKeyFromEvent(e), up, offset; - if (keyName == '' || keyName == '' || keyName == '' || - (keyName == '' && input == '')) { - vimGlobalState.exCommandHistoryController.pushInput(input); + CodeMirror.e_stop(e); + clearInputState(cm); + close(); + cm.focus(); + } + if (keyName == '' || keyName == '') { + CodeMirror.e_stop(e); + up = keyName == '' ? true : false; + offset = e.target ? e.target.selectionEnd : 0; + input = vimGlobalState.exCommandHistoryController.nextMatch(input, up) || ''; + close(input); + if (offset && e.target) e.target.selectionEnd = e.target.selectionStart = Math.min(offset, e.target.value.length); + } else if (keyName == '') { + // Ctrl-U clears input. + CodeMirror.e_stop(e); + close(''); + } else if (keyName && keyName != '' && keyName != '') { vimGlobalState.exCommandHistoryController.reset(); - CodeMirror.e_stop(e); - clearInputState(cm); - close(); - cm.focus(); - } - if (keyName == '' || keyName == '') { - CodeMirror.e_stop(e); - up = keyName == '' ? true : false; - offset = e.target ? e.target.selectionEnd : 0; - input = vimGlobalState.exCommandHistoryController.nextMatch(input, up) || ''; - close(input); - if (offset && e.target) e.target.selectionEnd = e.target.selectionStart = Math.min(offset, e.target.value.length); - } else if (keyName == '') { - // Ctrl-U clears input. - CodeMirror.e_stop(e); - close(''); - } else if (keyName && keyName != '' && keyName != '') { - vimGlobalState.exCommandHistoryController.reset(); - } } - if (command.type == 'keyToEx') { - // Handle user defined Ex to Ex mappings - exCommandDispatcher.processCommand(cm, command.exArgs.input); + } + if (command.type == 'keyToEx') { + // Handle user defined Ex to Ex mappings + exCommandDispatcher.processCommand(cm, command.exArgs.input); + } else { + if (vim.visualMode) { + showPrompt(cm, { onClose: onPromptClose, prefix: ':', value: '\'<,\'>', + onKeyDown: onPromptKeyDown, selectValueOnOpen: false}); } else { - if (vim.visualMode) { - showPrompt(cm, { onClose: onPromptClose, prefix: ':', value: '\'<,\'>', - onKeyDown: onPromptKeyDown, selectValueOnOpen: false}); + showPrompt(cm, { onClose: onPromptClose, prefix: ':', + onKeyDown: onPromptKeyDown}); + } + } + }, + /**@arg {CodeMirrorV} cm @arg {vimState} vim */ + evalInput: function(cm, vim) { + // If the motion command is set, execute both the operator and motion. + // Otherwise return. + var inputState = vim.inputState; + var motion = inputState.motion; + /** @type {MotionArgs}*/ + var motionArgs = inputState.motionArgs || { repeat: 1}; + var operator = inputState.operator; + /** @type {OperatorArgs}*/ + var operatorArgs = inputState.operatorArgs || {}; + var registerName = inputState.registerName; + var sel = vim.sel; + // TODO: Make sure cm and vim selections are identical outside visual mode. + var origHead = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.head): cm.getCursor('head')); + var origAnchor = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.anchor) : cm.getCursor('anchor')); + var oldHead = copyCursor(origHead); + var oldAnchor = copyCursor(origAnchor); + var newHead, newAnchor; + var repeat; + if (operator) { + this.recordLastEdit(vim, inputState); + } + if (inputState.repeatOverride !== undefined) { + // If repeatOverride is specified, that takes precedence over the + // input state's repeat. Used by Ex mode and can be user defined. + repeat = inputState.repeatOverride; + } else { + repeat = inputState.getRepeat(); + } + if (repeat > 0 && motionArgs.explicitRepeat) { + motionArgs.repeatIsExplicit = true; + } else if (motionArgs.noRepeat || + (!motionArgs.explicitRepeat && repeat === 0)) { + repeat = 1; + motionArgs.repeatIsExplicit = false; + } + if (inputState.selectedCharacter) { + // If there is a character input, stick it in all of the arg arrays. + motionArgs.selectedCharacter = operatorArgs.selectedCharacter = + inputState.selectedCharacter; + } + motionArgs.repeat = repeat; + clearInputState(cm); + if (motion) { + var motionResult = motions[motion](cm, origHead, motionArgs, vim, inputState); + vim.lastMotion = motions[motion]; + if (!motionResult) { + return; + } + if (motionArgs.toJumplist) { + var jumpList = vimGlobalState.jumpList; + // if the current motion is # or *, use cachedCursor + var cachedCursor = jumpList.cachedCursor; + if (cachedCursor) { + // @ts-ignore + recordJumpPosition(cm, cachedCursor, motionResult); + delete jumpList.cachedCursor; } else { - showPrompt(cm, { onClose: onPromptClose, prefix: ':', - onKeyDown: onPromptKeyDown}); + // @ts-ignore + recordJumpPosition(cm, origHead, motionResult); } } - }, - evalInput: function(cm, vim) { - // If the motion command is set, execute both the operator and motion. - // Otherwise return. - var inputState = vim.inputState; - var motion = inputState.motion; - var motionArgs = inputState.motionArgs || {}; - var operator = inputState.operator; - var operatorArgs = inputState.operatorArgs || {}; - var registerName = inputState.registerName; - var sel = vim.sel; - // TODO: Make sure cm and vim selections are identical outside visual mode. - var origHead = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.head): cm.getCursor('head')); - var origAnchor = copyCursor(vim.visualMode ? clipCursorToContent(cm, sel.anchor) : cm.getCursor('anchor')); - var oldHead = copyCursor(origHead); - var oldAnchor = copyCursor(origAnchor); - var newHead, newAnchor; - var repeat; - if (operator) { - this.recordLastEdit(vim, inputState); - } - if (inputState.repeatOverride !== undefined) { - // If repeatOverride is specified, that takes precedence over the - // input state's repeat. Used by Ex mode and can be user defined. - repeat = inputState.repeatOverride; + if (motionResult instanceof Array) { + newAnchor = motionResult[0]; + newHead = motionResult[1]; } else { - repeat = inputState.getRepeat(); - } - if (repeat > 0 && motionArgs.explicitRepeat) { - motionArgs.repeatIsExplicit = true; - } else if (motionArgs.noRepeat || - (!motionArgs.explicitRepeat && repeat === 0)) { - repeat = 1; - motionArgs.repeatIsExplicit = false; - } - if (inputState.selectedCharacter) { - // If there is a character input, stick it in all of the arg arrays. - motionArgs.selectedCharacter = operatorArgs.selectedCharacter = - inputState.selectedCharacter; - } - motionArgs.repeat = repeat; - clearInputState(cm); - if (motion) { - var motionResult = motions[motion](cm, origHead, motionArgs, vim, inputState); - vim.lastMotion = motions[motion]; - if (!motionResult) { - return; + newHead = motionResult; + } + // TODO: Handle null returns from motion commands better. + if (!newHead) { + newHead = copyCursor(origHead); + } + if (vim.visualMode) { + if (!(vim.visualBlock && newHead.ch === Infinity)) { + newHead = clipCursorToContent(cm, newHead, oldHead); } - if (motionArgs.toJumplist) { - var jumpList = vimGlobalState.jumpList; - // if the current motion is # or *, use cachedCursor - var cachedCursor = jumpList.cachedCursor; - if (cachedCursor) { - recordJumpPosition(cm, cachedCursor, motionResult); - delete jumpList.cachedCursor; - } else { - recordJumpPosition(cm, origHead, motionResult); - } + if (newAnchor) { + newAnchor = clipCursorToContent(cm, newAnchor); } - if (motionResult instanceof Array) { - newAnchor = motionResult[0]; - newHead = motionResult[1]; + newAnchor = newAnchor || oldAnchor; + sel.anchor = newAnchor; + sel.head = newHead; + updateCmSelection(cm); + updateMark(cm, vim, '<', + cursorIsBefore(newAnchor, newHead) ? newAnchor + : newHead); + updateMark(cm, vim, '>', + cursorIsBefore(newAnchor, newHead) ? newHead + : newAnchor); + } else if (!operator) { + newHead = clipCursorToContent(cm, newHead, oldHead); + cm.setCursor(newHead.line, newHead.ch); + } + } + if (operator) { + if (operatorArgs.lastSel) { + // Replaying a visual mode operation + newAnchor = oldAnchor; + var lastSel = operatorArgs.lastSel; + var lineOffset = Math.abs(lastSel.head.line - lastSel.anchor.line); + var chOffset = Math.abs(lastSel.head.ch - lastSel.anchor.ch); + if (lastSel.visualLine) { + // Linewise Visual mode: The same number of lines. + newHead = new Pos(oldAnchor.line + lineOffset, oldAnchor.ch); + } else if (lastSel.visualBlock) { + // Blockwise Visual mode: The same number of lines and columns. + newHead = new Pos(oldAnchor.line + lineOffset, oldAnchor.ch + chOffset); + } else if (lastSel.head.line == lastSel.anchor.line) { + // Normal Visual mode within one line: The same number of characters. + newHead = new Pos(oldAnchor.line, oldAnchor.ch + chOffset); } else { - newHead = motionResult; - } - // TODO: Handle null returns from motion commands better. - if (!newHead) { - newHead = copyCursor(origHead); - } - if (vim.visualMode) { - if (!(vim.visualBlock && newHead.ch === Infinity)) { - newHead = clipCursorToContent(cm, newHead, oldHead); - } - if (newAnchor) { - newAnchor = clipCursorToContent(cm, newAnchor); - } - newAnchor = newAnchor || oldAnchor; - sel.anchor = newAnchor; - sel.head = newHead; - updateCmSelection(cm); - updateMark(cm, vim, '<', - cursorIsBefore(newAnchor, newHead) ? newAnchor - : newHead); - updateMark(cm, vim, '>', - cursorIsBefore(newAnchor, newHead) ? newHead - : newAnchor); - } else if (!operator) { - newHead = clipCursorToContent(cm, newHead, oldHead); - cm.setCursor(newHead.line, newHead.ch); + // Normal Visual mode with several lines: The same number of lines, in the + // last line the same number of characters as in the last line the last time. + newHead = new Pos(oldAnchor.line + lineOffset, oldAnchor.ch); } + vim.visualMode = true; + vim.visualLine = lastSel.visualLine; + vim.visualBlock = lastSel.visualBlock; + sel = vim.sel = { + anchor: newAnchor, + head: newHead + }; + updateCmSelection(cm); + } else if (vim.visualMode) { + operatorArgs.lastSel = { + anchor: copyCursor(sel.anchor), + head: copyCursor(sel.head), + visualBlock: vim.visualBlock, + visualLine: vim.visualLine + }; } - if (operator) { - if (operatorArgs.lastSel) { - // Replaying a visual mode operation - newAnchor = oldAnchor; - var lastSel = operatorArgs.lastSel; - var lineOffset = Math.abs(lastSel.head.line - lastSel.anchor.line); - var chOffset = Math.abs(lastSel.head.ch - lastSel.anchor.ch); - if (lastSel.visualLine) { - // Linewise Visual mode: The same number of lines. - newHead = new Pos(oldAnchor.line + lineOffset, oldAnchor.ch); - } else if (lastSel.visualBlock) { - // Blockwise Visual mode: The same number of lines and columns. - newHead = new Pos(oldAnchor.line + lineOffset, oldAnchor.ch + chOffset); - } else if (lastSel.head.line == lastSel.anchor.line) { - // Normal Visual mode within one line: The same number of characters. - newHead = new Pos(oldAnchor.line, oldAnchor.ch + chOffset); - } else { - // Normal Visual mode with several lines: The same number of lines, in the - // last line the same number of characters as in the last line the last time. - newHead = new Pos(oldAnchor.line + lineOffset, oldAnchor.ch); - } - vim.visualMode = true; - vim.visualLine = lastSel.visualLine; - vim.visualBlock = lastSel.visualBlock; - sel = vim.sel = { - anchor: newAnchor, - head: newHead - }; - updateCmSelection(cm); - } else if (vim.visualMode) { - operatorArgs.lastSel = { - anchor: copyCursor(sel.anchor), - head: copyCursor(sel.head), - visualBlock: vim.visualBlock, - visualLine: vim.visualLine - }; - } - var curStart, curEnd, linewise, mode; - var cmSel; - if (vim.visualMode) { - // Init visual op - curStart = cursorMin(sel.head, sel.anchor); - curEnd = cursorMax(sel.head, sel.anchor); - linewise = vim.visualLine || operatorArgs.linewise; - mode = vim.visualBlock ? 'block' : - linewise ? 'line' : - 'char'; - var newPositions = updateSelectionForSurrogateCharacters(cm, curStart, curEnd); - cmSel = makeCmSelection(cm, { - anchor: newPositions.start, - head: newPositions.end - }, mode); - if (linewise) { - var ranges = cmSel.ranges; - if (mode == 'block') { - // Linewise operators in visual block mode extend to end of line - for (var i = 0; i < ranges.length; i++) { - ranges[i].head.ch = lineLength(cm, ranges[i].head.line); - } - } else if (mode == 'line') { - ranges[0].head = new Pos(ranges[0].head.line + 1, 0); + var curStart, curEnd, linewise; + /** @type {'block'|'line'|'char'}*/ var mode; + var cmSel; + if (vim.visualMode) { + // Init visual op + curStart = cursorMin(sel.head, sel.anchor); + curEnd = cursorMax(sel.head, sel.anchor); + linewise = vim.visualLine || operatorArgs.linewise; + mode = vim.visualBlock ? 'block' : + linewise ? 'line' : + 'char'; + var newPositions = updateSelectionForSurrogateCharacters(cm, curStart, curEnd); + cmSel = makeCmSelection(cm, { + anchor: newPositions.start, + head: newPositions.end + }, mode); + if (linewise) { + var ranges = cmSel.ranges; + if (mode == 'block') { + // Linewise operators in visual block mode extend to end of line + for (var i = 0; i < ranges.length; i++) { + ranges[i].head.ch = lineLength(cm, ranges[i].head.line); } + } else if (mode == 'line') { + ranges[0].head = new Pos(ranges[0].head.line + 1, 0); } - } else { - // Init motion op - curStart = copyCursor(newAnchor || oldAnchor); - curEnd = copyCursor(newHead || oldHead); - if (cursorIsBefore(curEnd, curStart)) { - var tmp = curStart; - curStart = curEnd; - curEnd = tmp; - } - linewise = motionArgs.linewise || operatorArgs.linewise; - if (linewise) { - // Expand selection to entire line. - expandSelectionToLine(cm, curStart, curEnd); - } else if (motionArgs.forward) { - // Clip to trailing newlines only if the motion goes forward. - clipToLine(cm, curStart, curEnd); - } - mode = 'char'; - var exclusive = !motionArgs.inclusive || linewise; - var newPositions = updateSelectionForSurrogateCharacters(cm, curStart, curEnd); - cmSel = makeCmSelection(cm, { - anchor: newPositions.start, - head: newPositions.end - }, mode, exclusive); } - cm.setSelections(cmSel.ranges, cmSel.primary); - vim.lastMotion = null; - operatorArgs.repeat = repeat; // For indent in visual mode. - operatorArgs.registerName = registerName; - // Keep track of linewise as it affects how paste and change behave. - operatorArgs.linewise = linewise; - var operatorMoveTo = operators[operator]( - cm, operatorArgs, cmSel.ranges, oldAnchor, newHead); - if (vim.visualMode) { - exitVisualMode(cm, operatorMoveTo != null); - } - if (operatorMoveTo) { - cm.setCursor(operatorMoveTo); + } else { + // Init motion op + curStart = copyCursor(newAnchor || oldAnchor); + curEnd = copyCursor(newHead || oldHead); + if (cursorIsBefore(curEnd, curStart)) { + var tmp = curStart; + curStart = curEnd; + curEnd = tmp; } + linewise = motionArgs.linewise || operatorArgs.linewise; + if (linewise) { + // Expand selection to entire line. + expandSelectionToLine(cm, curStart, curEnd); + } else if (motionArgs.forward) { + // Clip to trailing newlines only if the motion goes forward. + clipToLine(cm, curStart, curEnd); + } + mode = 'char'; + var exclusive = !motionArgs.inclusive || linewise; + var newPositions = updateSelectionForSurrogateCharacters(cm, curStart, curEnd); + cmSel = makeCmSelection(cm, { + anchor: newPositions.start, + head: newPositions.end + }, mode, exclusive); + } + cm.setSelections(cmSel.ranges, cmSel.primary); + vim.lastMotion = null; + operatorArgs.repeat = repeat; // For indent in visual mode. + operatorArgs.registerName = registerName; + // Keep track of linewise as it affects how paste and change behave. + operatorArgs.linewise = linewise; + var operatorMoveTo = operators[operator]( + cm, operatorArgs, cmSel.ranges, oldAnchor, newHead); + if (vim.visualMode) { + exitVisualMode(cm, operatorMoveTo != null); + } + if (operatorMoveTo) { + cm.setCursor(operatorMoveTo); } - }, - recordLastEdit: function(vim, inputState, actionCommand) { - var macroModeState = vimGlobalState.macroModeState; - if (macroModeState.isPlaying) { return; } - vim.lastEditInputState = inputState; - vim.lastEditActionCommand = actionCommand; - macroModeState.lastInsertModeChanges.changes = []; - macroModeState.lastInsertModeChanges.expectCursorActivityForChange = false; - macroModeState.lastInsertModeChanges.visualBlock = vim.visualBlock ? vim.sel.head.line - vim.sel.anchor.line : 0; } - }; + }, + /**@arg {vimState} vim @arg {InputStateInterface} inputState, @arg {import("./types").actionCommand} [actionCommand] */ + recordLastEdit: function(vim, inputState, actionCommand) { + var macroModeState = vimGlobalState.macroModeState; + if (macroModeState.isPlaying) { return; } + vim.lastEditInputState = inputState; + vim.lastEditActionCommand = actionCommand; + macroModeState.lastInsertModeChanges.changes = []; + macroModeState.lastInsertModeChanges.expectCursorActivityForChange = false; + macroModeState.lastInsertModeChanges.visualBlock = vim.visualBlock ? vim.sel.head.line - vim.sel.anchor.line : 0; + } + }; + /** + * All of the functions below return Cursor objects. + * @type {import("./types").vimMotions}} + */ + var motions = { + moveToTopLine: function(cm, _head, motionArgs) { + var line = getUserVisibleLines(cm).top + motionArgs.repeat -1; + return new Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); + }, + moveToMiddleLine: function(cm) { + var range = getUserVisibleLines(cm); + var line = Math.floor((range.top + range.bottom) * 0.5); + return new Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); + }, + moveToBottomLine: function(cm, _head, motionArgs) { + var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1; + return new Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); + }, + expandToLine: function(_cm, head, motionArgs) { + // Expands forward to end of line, and then to next line if repeat is + // >1. Does not handle backward motion! + var cur = head; + return new Pos(cur.line + motionArgs.repeat - 1, Infinity); + }, + findNext: function(cm, _head, motionArgs) { + var state = getSearchState(cm); + var query = state.getQuery(); + if (!query) { + return; + } + var prev = !motionArgs.forward; + // If search is initiated with ? instead of /, negate direction. + prev = (state.isReversed()) ? !prev : prev; + highlightSearchMatches(cm, query); + return findNext(cm, prev/** prev */, query, motionArgs.repeat); + }, /** - * typedef {Object{line:number,ch:number}} Cursor An object containing the - * position of the cursor. + * Find and select the next occurrence of the search query. If the cursor is currently + * within a match, then find and select the current match. Otherwise, find the next occurrence in the + * appropriate direction. + * + * This differs from `findNext` in the following ways: + * + * 1. Instead of only returning the "from", this returns a "from", "to" range. + * 2. If the cursor is currently inside a search match, this selects the current match + * instead of the next match. + * 3. If there is no associated operator, this will turn on visual mode. */ - // All of the functions below return Cursor objects. - var motions = { - moveToTopLine: function(cm, _head, motionArgs) { - var line = getUserVisibleLines(cm).top + motionArgs.repeat -1; - return new Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); - }, - moveToMiddleLine: function(cm) { - var range = getUserVisibleLines(cm); - var line = Math.floor((range.top + range.bottom) * 0.5); - return new Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); - }, - moveToBottomLine: function(cm, _head, motionArgs) { - var line = getUserVisibleLines(cm).bottom - motionArgs.repeat +1; - return new Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); - }, - expandToLine: function(_cm, head, motionArgs) { - // Expands forward to end of line, and then to next line if repeat is - // >1. Does not handle backward motion! - var cur = head; - return new Pos(cur.line + motionArgs.repeat - 1, Infinity); - }, - findNext: function(cm, _head, motionArgs) { - var state = getSearchState(cm); - var query = state.getQuery(); - if (!query) { - return; - } - var prev = !motionArgs.forward; - // If search is initiated with ? instead of /, negate direction. - prev = (state.isReversed()) ? !prev : prev; - highlightSearchMatches(cm, query); - return findNext(cm, prev/** prev */, query, motionArgs.repeat); - }, - /** - * Find and select the next occurrence of the search query. If the cursor is currently - * within a match, then find and select the current match. Otherwise, find the next occurrence in the - * appropriate direction. - * - * This differs from `findNext` in the following ways: - * - * 1. Instead of only returning the "from", this returns a "from", "to" range. - * 2. If the cursor is currently inside a search match, this selects the current match - * instead of the next match. - * 3. If there is no associated operator, this will turn on visual mode. - */ - findAndSelectNextInclusive: function(cm, _head, motionArgs, vim, prevInputState) { - var state = getSearchState(cm); - var query = state.getQuery(); - - if (!query) { - return; - } - - var prev = !motionArgs.forward; - prev = (state.isReversed()) ? !prev : prev; - - // next: [from, to] | null - var next = findNextFromAndToInclusive(cm, prev, query, motionArgs.repeat, vim); + findAndSelectNextInclusive: function(cm, _head, motionArgs, vim, prevInputState) { + var state = getSearchState(cm); + var query = state.getQuery(); - // No matches. - if (!next) { - return; - } + if (!query) { + return; + } - // If there's an operator that will be executed, return the selection. - if (prevInputState.operator) { - return next; - } + var prev = !motionArgs.forward; + prev = (state.isReversed()) ? !prev : prev; - // At this point, we know that there is no accompanying operator -- let's - // deal with visual mode in order to select an appropriate match. + // next: [from, to] | null + var next = findNextFromAndToInclusive(cm, prev, query, motionArgs.repeat, vim); - var from = next[0]; - // For whatever reason, when we use the "to" as returned by searchcursor.js directly, - // the resulting selection is extended by 1 char. Let's shrink it so that only the - // match is selected. - var to = new Pos(next[1].line, next[1].ch - 1); + // No matches. + if (!next) { + return; + } - if (vim.visualMode) { - // If we were in visualLine or visualBlock mode, get out of it. - if (vim.visualLine || vim.visualBlock) { - vim.visualLine = false; - vim.visualBlock = false; - CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: ""}); - } + // If there's an operator that will be executed, return the selection. + if (prevInputState.operator) { + return next; + } - // If we're currently in visual mode, we should extend the selection to include - // the search result. - var anchor = vim.sel.anchor; - if (anchor) { - if (state.isReversed()) { - if (motionArgs.forward) { - return [anchor, from]; - } + // At this point, we know that there is no accompanying operator -- let's + // deal with visual mode in order to select an appropriate match. - return [anchor, to]; - } else { - if (motionArgs.forward) { - return [anchor, to]; - } + var from = next[0]; + // For whatever reason, when we use the "to" as returned by searchcursor.js directly, + // the resulting selection is extended by 1 char. Let's shrink it so that only the + // match is selected. + var to = new Pos(next[1].line, next[1].ch - 1); - return [anchor, from]; - } - } - } else { - // Let's turn visual mode on. - vim.visualMode = true; + if (vim.visualMode) { + // If we were in visualLine or visualBlock mode, get out of it. + if (vim.visualLine || vim.visualBlock) { vim.visualLine = false; vim.visualBlock = false; CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: ""}); } - return prev ? [to, from] : [from, to]; - }, - goToMark: function(cm, _head, motionArgs, vim) { - var pos = getMarkPos(cm, vim, motionArgs.selectedCharacter); - if (pos) { - return motionArgs.linewise ? { line: pos.line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(pos.line)) } : pos; - } - return null; - }, - moveToOtherHighlightedEnd: function(cm, _head, motionArgs, vim) { - if (vim.visualBlock && motionArgs.sameLine) { - var sel = vim.sel; - return [ - clipCursorToContent(cm, new Pos(sel.anchor.line, sel.head.ch)), - clipCursorToContent(cm, new Pos(sel.head.line, sel.anchor.ch)) - ]; - } else { - return ([vim.sel.head, vim.sel.anchor]); - } - }, - jumpToMark: function(cm, head, motionArgs, vim) { - var best = head; - for (var i = 0; i < motionArgs.repeat; i++) { - var cursor = best; - for (var key in vim.marks) { - if (!isLowerCase(key)) { - continue; + // If we're currently in visual mode, we should extend the selection to include + // the search result. + var anchor = vim.sel.anchor; + if (anchor) { + if (state.isReversed()) { + if (motionArgs.forward) { + return [anchor, from]; } - var mark = vim.marks[key].find(); - var isWrongDirection = (motionArgs.forward) ? - cursorIsBefore(mark, cursor) : cursorIsBefore(cursor, mark); - if (isWrongDirection) { - continue; - } - if (motionArgs.linewise && (mark.line == cursor.line)) { - continue; + return [anchor, to]; + } else { + if (motionArgs.forward) { + return [anchor, to]; } - var equal = cursorEqual(cursor, best); - var between = (motionArgs.forward) ? - cursorIsBetween(cursor, mark, best) : - cursorIsBetween(best, mark, cursor); - - if (equal || between) { - best = mark; - } + return [anchor, from]; } } + } else { + // Let's turn visual mode on. + vim.visualMode = true; + vim.visualLine = false; + vim.visualBlock = false; + CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: ""}); + } - if (motionArgs.linewise) { - // Vim places the cursor on the first non-whitespace character of - // the line if there is one, else it places the cursor at the end - // of the line, regardless of whether a mark was found. - best = new Pos(best.line, findFirstNonWhiteSpaceCharacter(cm.getLine(best.line))); - } - return best; - }, - moveByCharacters: function(_cm, head, motionArgs) { - var cur = head; - var repeat = motionArgs.repeat; - var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat; - return new Pos(cur.line, ch); - }, - moveByLines: function(cm, head, motionArgs, vim) { - var cur = head; - var endCh = cur.ch; - // Depending what our last motion was, we may want to do different - // things. If our last motion was moving vertically, we want to - // preserve the HPos from our last horizontal move. If our last motion - // was going to the end of a line, moving vertically we should go to - // the end of the line, etc. - switch (vim.lastMotion) { - case this.moveByLines: - case this.moveByDisplayLines: - case this.moveByScroll: - case this.moveToColumn: - case this.moveToEol: - endCh = vim.lastHPos; - break; - default: - vim.lastHPos = endCh; - } - var repeat = motionArgs.repeat+(motionArgs.repeatOffset||0); - var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat; - var first = cm.firstLine(); - var last = cm.lastLine(); - var posV = cm.findPosV(cur, (motionArgs.forward ? repeat : -repeat), 'line', vim.lastHSPos); - var hasMarkedText = motionArgs.forward ? posV.line > line : posV.line < line; - if (hasMarkedText) { - line = posV.line; - endCh = posV.ch; - } - // Vim go to line begin or line end when cursor at first/last line and - // move to previous/next line is triggered. - if (line < first && cur.line == first){ - return this.moveToStartOfLine(cm, head, motionArgs, vim); - } else if (line > last && cur.line == last){ - return moveToEol(cm, head, motionArgs, vim, true); - } - if (motionArgs.toFirstChar){ - endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line)); - vim.lastHPos = endCh; - } - vim.lastHSPos = cm.charCoords(new Pos(line, endCh),'div').left; - return new Pos(line, endCh); - }, - moveByDisplayLines: function(cm, head, motionArgs, vim) { - var cur = head; - switch (vim.lastMotion) { - case this.moveByDisplayLines: - case this.moveByScroll: - case this.moveByLines: - case this.moveToColumn: - case this.moveToEol: - break; - default: - vim.lastHSPos = cm.charCoords(cur,'div').left; - } - var repeat = motionArgs.repeat; - var res=cm.findPosV(cur,(motionArgs.forward ? repeat : -repeat),'line',vim.lastHSPos); - if (res.hitSide) { - if (motionArgs.forward) { - var lastCharCoords = cm.charCoords(res, 'div'); - var goalCoords = { top: lastCharCoords.top + 8, left: vim.lastHSPos }; - var res = cm.coordsChar(goalCoords, 'div'); - } else { - var resCoords = cm.charCoords(new Pos(cm.firstLine(), 0), 'div'); - resCoords.left = vim.lastHSPos; - res = cm.coordsChar(resCoords, 'div'); - } - } - vim.lastHPos = res.ch; - return res; - }, - moveByPage: function(cm, head, motionArgs) { - // CodeMirror only exposes functions that move the cursor page down, so - // doing this bad hack to move the cursor and move it back. evalInput - // will move the cursor to where it should be in the end. - var curStart = head; - var repeat = motionArgs.repeat; - return cm.findPosV(curStart, (motionArgs.forward ? repeat : -repeat), 'page'); - }, - moveByParagraph: function(cm, head, motionArgs) { - var dir = motionArgs.forward ? 1 : -1; - return findParagraph(cm, head, motionArgs.repeat, dir); - }, - moveBySentence: function(cm, head, motionArgs) { - var dir = motionArgs.forward ? 1 : -1; - return findSentence(cm, head, motionArgs.repeat, dir); - }, - moveByScroll: function(cm, head, motionArgs, vim) { - var scrollbox = cm.getScrollInfo(); - var curEnd = null; - var repeat = motionArgs.repeat; - if (!repeat) { - repeat = scrollbox.clientHeight / (2 * cm.defaultTextHeight()); - } - var orig = cm.charCoords(head, 'local'); - motionArgs.repeat = repeat; - curEnd = motions.moveByDisplayLines(cm, head, motionArgs, vim); - if (!curEnd) { - return null; - } - var dest = cm.charCoords(curEnd, 'local'); - cm.scrollTo(null, scrollbox.top + dest.top - orig.top); - return curEnd; - }, - moveByWords: function(cm, head, motionArgs) { - return moveToWord(cm, head, motionArgs.repeat, !!motionArgs.forward, - !!motionArgs.wordEnd, !!motionArgs.bigWord); - }, - moveTillCharacter: function(cm, head, motionArgs) { - var repeat = motionArgs.repeat; - var curEnd = moveToCharacter(cm, repeat, motionArgs.forward, - motionArgs.selectedCharacter, head); - var increment = motionArgs.forward ? -1 : 1; - recordLastCharacterSearch(increment, motionArgs); - if (!curEnd) return null; - curEnd.ch += increment; - return curEnd; - }, - moveToCharacter: function(cm, head, motionArgs) { - var repeat = motionArgs.repeat; - recordLastCharacterSearch(0, motionArgs); - return moveToCharacter(cm, repeat, motionArgs.forward, - motionArgs.selectedCharacter, head) || head; - }, - moveToSymbol: function(cm, head, motionArgs) { - var repeat = motionArgs.repeat; - return findSymbol(cm, repeat, motionArgs.forward, - motionArgs.selectedCharacter) || head; - }, - moveToColumn: function(cm, head, motionArgs, vim) { - var repeat = motionArgs.repeat; - // repeat is equivalent to which column we want to move to! - vim.lastHPos = repeat - 1; - vim.lastHSPos = cm.charCoords(head,'div').left; - return moveToColumn(cm, repeat); - }, - moveToEol: function(cm, head, motionArgs, vim) { - return moveToEol(cm, head, motionArgs, vim, false); - }, - moveToFirstNonWhiteSpaceCharacter: function(cm, head) { - // Go to the start of the line where the text begins, or the end for - // whitespace-only lines - var cursor = head; - return new Pos(cursor.line, - findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line))); - }, - moveToMatchedSymbol: function(cm, head) { - var cursor = head; - var line = cursor.line; - var ch = cursor.ch; - var lineText = cm.getLine(line); - var symbol; - for (; ch < lineText.length; ch++) { - symbol = lineText.charAt(ch); - if (symbol && isMatchableSymbol(symbol)) { - var style = cm.getTokenTypeAt(new Pos(line, ch + 1)); - if (style !== "string" && style !== "comment") { - break; - } - } - } - if (ch < lineText.length) { - // Only include angle brackets in analysis if they are being matched. - var re = (ch === '<' || ch === '>') ? /[(){}[\]<>]/ : /[(){}[\]]/; - var matched = cm.findMatchingBracket(new Pos(line, ch), {bracketRegex: re}); - return matched.to; - } else { - return cursor; - } - }, - moveToStartOfLine: function(_cm, head) { - return new Pos(head.line, 0); - }, - moveToLineOrEdgeOfDocument: function(cm, _head, motionArgs) { - var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine(); - if (motionArgs.repeatIsExplicit) { - lineNum = motionArgs.repeat - cm.getOption('firstLineNumber'); - } - return new Pos(lineNum, - findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum))); - }, - moveToStartOfDisplayLine: function(cm) { - cm.execCommand("goLineLeft"); - return cm.getCursor(); - }, - moveToEndOfDisplayLine: function(cm) { - cm.execCommand("goLineRight"); - var head = cm.getCursor(); - if (head.sticky == "before") head.ch--; - return head; - }, - textObjectManipulation: function(cm, head, motionArgs, vim) { - // TODO: lots of possible exceptions that can be thrown here. Try da( - // outside of a () block. - var mirroredPairs = {'(': ')', ')': '(', - '{': '}', '}': '{', - '[': ']', ']': '[', - '<': '>', '>': '<'}; - var selfPaired = {'\'': true, '"': true, '`': true}; - - var character = motionArgs.selectedCharacter; - // 'b' refers to '()' block. - // 'B' refers to '{}' block. - if (character == 'b') { - character = '('; - } else if (character == 'B') { - character = '{'; - } - - // Inclusive is the difference between a and i - // TODO: Instead of using the additional text object map to perform text - // object operations, merge the map into the defaultKeyMap and use - // motionArgs to define behavior. Define separate entries for 'aw', - // 'iw', 'a[', 'i[', etc. - var inclusive = !motionArgs.textObjectInner; - - var tmp, move; - if (mirroredPairs[character]) { - move = true; - tmp = selectCompanionObject(cm, head, character, inclusive); - if (!tmp) { - var sc = cm.getSearchCursor(new RegExp("\\" + character, "g"), head) - if (sc.find()) { - tmp = selectCompanionObject(cm, sc.from(), character, inclusive); - } - } - } else if (selfPaired[character]) { - move = true; - tmp = findBeginningAndEnd(cm, head, character, inclusive); - } else if (character === 'W' || character === 'w') { - var repeat = motionArgs.repeat || 1; - while (repeat-- > 0) { - var repeated = expandWordUnderCursor(cm, { - inclusive, - innerWord: !inclusive, - bigWord: character === 'W', - noSymbol: character === 'W', - multiline: true - }, tmp && tmp.end); - if (repeated) { - if (!tmp) tmp = repeated; - tmp.end = repeated.end; - } - } - } else if (character === 'p') { - tmp = findParagraph(cm, head, motionArgs.repeat, 0, inclusive); - motionArgs.linewise = true; - if (vim.visualMode) { - if (!vim.visualLine) { vim.visualLine = true; } - } else { - var operatorArgs = vim.inputState.operatorArgs; - if (operatorArgs) { operatorArgs.linewise = true; } - tmp.end.line--; + return prev ? [to, from] : [from, to]; + }, + goToMark: function(cm, _head, motionArgs, vim) { + var pos = getMarkPos(cm, vim, motionArgs.selectedCharacter || ""); + if (pos) { + return motionArgs.linewise ? { line: pos.line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(pos.line)) } : pos; + } + return null; + }, + moveToOtherHighlightedEnd: function(cm, _head, motionArgs, vim) { + if (vim.visualBlock && motionArgs.sameLine) { + var sel = vim.sel; + return [ + clipCursorToContent(cm, new Pos(sel.anchor.line, sel.head.ch)), + clipCursorToContent(cm, new Pos(sel.head.line, sel.anchor.ch)) + ]; + } else { + return ([vim.sel.head, vim.sel.anchor]); + } + }, + jumpToMark: function(cm, head, motionArgs, vim) { + var best = head; + for (var i = 0; i < motionArgs.repeat; i++) { + var cursor = best; + for (var key in vim.marks) { + if (!isLowerCase(key)) { + continue; } - } else if (character === 't') { - tmp = expandTagUnderCursor(cm, head, inclusive); - } else if (character === 's') { - // account for cursor on end of sentence symbol - var content = cm.getLine(head.line); - if (head.ch > 0 && isEndOfSentenceSymbol(content[head.ch])) { - head.ch -= 1; + var mark = vim.marks[key].find(); + var isWrongDirection = (motionArgs.forward) ? + // @ts-ignore + cursorIsBefore(mark, cursor) : cursorIsBefore(cursor, mark); + + if (isWrongDirection) { + continue; } - var end = getSentence(cm, head, motionArgs.repeat, 1, inclusive) - var start = getSentence(cm, head, motionArgs.repeat, -1, inclusive) - // closer vim behaviour, 'a' only takes the space after the sentence if there is one before and after - if (isWhiteSpaceString(cm.getLine(start.line)[start.ch]) - && isWhiteSpaceString(cm.getLine(end.line)[end.ch -1])) { - start = {line: start.line, ch: start.ch + 1} + // @ts-ignore + if (motionArgs.linewise && (mark.line == cursor.line)) { + continue; } - tmp = {start: start, end: end}; - } - if (!tmp) { - // No valid text object, don't move. - return null; + var equal = cursorEqual(cursor, best); + var between = (motionArgs.forward) ? + // @ts-ignore + cursorIsBetween(cursor, mark, best) : + // @ts-ignore + cursorIsBetween(best, mark, cursor); + + if (equal || between) { + // @ts-ignore + best = mark; + } } + } - if (!cm.state.vim.visualMode) { - return [tmp.start, tmp.end]; + if (motionArgs.linewise) { + // Vim places the cursor on the first non-whitespace character of + // the line if there is one, else it places the cursor at the end + // of the line, regardless of whether a mark was found. + best = new Pos(best.line, findFirstNonWhiteSpaceCharacter(cm.getLine(best.line))); + } + return best; + }, + moveByCharacters: function(_cm, head, motionArgs) { + var cur = head; + var repeat = motionArgs.repeat; + var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat; + return new Pos(cur.line, ch); + }, + moveByLines: function(cm, head, motionArgs, vim) { + var cur = head; + var endCh = cur.ch; + // Depending what our last motion was, we may want to do different + // things. If our last motion was moving vertically, we want to + // preserve the HPos from our last horizontal move. If our last motion + // was going to the end of a line, moving vertically we should go to + // the end of the line, etc. + switch (vim.lastMotion) { + case this.moveByLines: + case this.moveByDisplayLines: + case this.moveByScroll: + case this.moveToColumn: + case this.moveToEol: + endCh = vim.lastHPos; + break; + default: + vim.lastHPos = endCh; + } + var repeat = motionArgs.repeat+(motionArgs.repeatOffset||0); + var line = motionArgs.forward ? cur.line + repeat : cur.line - repeat; + var first = cm.firstLine(); + var last = cm.lastLine(); + var posV = cm.findPosV(cur, (motionArgs.forward ? repeat : -repeat), 'line', vim.lastHSPos); + var hasMarkedText = motionArgs.forward ? posV.line > line : posV.line < line; + if (hasMarkedText) { + line = posV.line; + endCh = posV.ch; + } + // Vim go to line begin or line end when cursor at first/last line and + // move to previous/next line is triggered. + if (line < first && cur.line == first){ + return this.moveToStartOfLine(cm, head, motionArgs, vim); + } else if (line > last && cur.line == last){ + return moveToEol(cm, head, motionArgs, vim, true); + } + if (motionArgs.toFirstChar){ + endCh=findFirstNonWhiteSpaceCharacter(cm.getLine(line)); + vim.lastHPos = endCh; + } + vim.lastHSPos = cm.charCoords(new Pos(line, endCh),'div').left; + return new Pos(line, endCh); + }, + moveByDisplayLines: function(cm, head, motionArgs, vim) { + var cur = head; + switch (vim.lastMotion) { + case this.moveByDisplayLines: + case this.moveByScroll: + case this.moveByLines: + case this.moveToColumn: + case this.moveToEol: + break; + default: + vim.lastHSPos = cm.charCoords(cur,'div').left; + } + var repeat = motionArgs.repeat; + var res=cm.findPosV(cur,(motionArgs.forward ? repeat : -repeat),'line',vim.lastHSPos); + if (res.hitSide) { + if (motionArgs.forward) { + var lastCharCoords = cm.charCoords(res, 'div'); + var goalCoords = { top: lastCharCoords.top + 8, left: vim.lastHSPos }; + res = cm.coordsChar(goalCoords, 'div'); } else { - return expandSelection(cm, tmp.start, tmp.end, move); + var resCoords = cm.charCoords(new Pos(cm.firstLine(), 0), 'div'); + resCoords.left = vim.lastHSPos; + res = cm.coordsChar(resCoords, 'div'); + } + } + vim.lastHPos = res.ch; + return res; + }, + moveByPage: function(cm, head, motionArgs) { + // CodeMirror only exposes functions that move the cursor page down, so + // doing this bad hack to move the cursor and move it back. evalInput + // will move the cursor to where it should be in the end. + var curStart = head; + var repeat = motionArgs.repeat; + return cm.findPosV(curStart, (motionArgs.forward ? repeat : -repeat), 'page'); + }, + moveByParagraph: function(cm, head, motionArgs) { + var dir = motionArgs.forward ? 1 : -1; + return findParagraph(cm, head, motionArgs.repeat, dir).start; + }, + moveBySentence: function(cm, head, motionArgs) { + var dir = motionArgs.forward ? 1 : -1; + return findSentence(cm, head, motionArgs.repeat, dir); + }, + moveByScroll: function(cm, head, motionArgs, vim) { + var scrollbox = cm.getScrollInfo(); + var curEnd = null; + var repeat = motionArgs.repeat; + if (!repeat) { + repeat = scrollbox.clientHeight / (2 * cm.defaultTextHeight()); + } + var orig = cm.charCoords(head, 'local'); + motionArgs.repeat = repeat; + curEnd = motions.moveByDisplayLines(cm, head, motionArgs, vim); + if (!curEnd) { + return null; + } + var dest = cm.charCoords(curEnd, 'local'); + cm.scrollTo(null, scrollbox.top + dest.top - orig.top); + return curEnd; + }, + moveByWords: function(cm, head, motionArgs) { + return moveToWord(cm, head, motionArgs.repeat, !!motionArgs.forward, + !!motionArgs.wordEnd, !!motionArgs.bigWord); + }, + moveTillCharacter: function(cm, head, motionArgs) { + var repeat = motionArgs.repeat; + var curEnd = moveToCharacter(cm, repeat, motionArgs.forward, + motionArgs.selectedCharacter, head); + var increment = motionArgs.forward ? -1 : 1; + recordLastCharacterSearch(increment, motionArgs); + if (!curEnd) return null; + curEnd.ch += increment; + return curEnd; + }, + moveToCharacter: function(cm, head, motionArgs) { + var repeat = motionArgs.repeat; + recordLastCharacterSearch(0, motionArgs); + return moveToCharacter(cm, repeat, motionArgs.forward, + motionArgs.selectedCharacter, head) || head; + }, + moveToSymbol: function(cm, head, motionArgs) { + var repeat = motionArgs.repeat; + return motionArgs.selectedCharacter + && findSymbol(cm, repeat, motionArgs.forward, + motionArgs.selectedCharacter) || head; + }, + moveToColumn: function(cm, head, motionArgs, vim) { + var repeat = motionArgs.repeat; + // repeat is equivalent to which column we want to move to! + vim.lastHPos = repeat - 1; + vim.lastHSPos = cm.charCoords(head,'div').left; + return moveToColumn(cm, repeat); + }, + moveToEol: function(cm, head, motionArgs, vim) { + return moveToEol(cm, head, motionArgs, vim, false); + }, + moveToFirstNonWhiteSpaceCharacter: function(cm, head) { + // Go to the start of the line where the text begins, or the end for + // whitespace-only lines + var cursor = head; + return new Pos(cursor.line, + findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line))); + }, + moveToMatchedSymbol: function(cm, head) { + var cursor = head; + var line = cursor.line; + var ch = cursor.ch; + var lineText = cm.getLine(line); + var symbol; + for (; ch < lineText.length; ch++) { + symbol = lineText.charAt(ch); + if (symbol && isMatchableSymbol(symbol)) { + var style = cm.getTokenTypeAt(new Pos(line, ch + 1)); + if (style !== "string" && style !== "comment") { + break; + } } - }, - - repeatLastCharacterSearch: function(cm, head, motionArgs) { - var lastSearch = vimGlobalState.lastCharacterSearch; - var repeat = motionArgs.repeat; - var forward = motionArgs.forward === lastSearch.forward; - var increment = (lastSearch.increment ? 1 : 0) * (forward ? -1 : 1); - cm.moveH(-increment, 'char'); - motionArgs.inclusive = forward ? true : false; - var curEnd = moveToCharacter(cm, repeat, forward, lastSearch.selectedCharacter); - if (!curEnd) { - cm.moveH(increment, 'char'); - return head; - } - curEnd.ch += increment; - return curEnd; } - }; + if (ch < lineText.length) { + // Only include angle brackets in analysis if they are being matched. + var re = (symbol === '<' || symbol === '>') ? /[(){}[\]<>]/ : /[(){}[\]]/; + var matched = cm.findMatchingBracket(new Pos(line, ch), {bracketRegex: re}); + return matched.to; + } else { + return cursor; + } + }, + moveToStartOfLine: function(_cm, head) { + return new Pos(head.line, 0); + }, + moveToLineOrEdgeOfDocument: function(cm, _head, motionArgs) { + var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine(); + if (motionArgs.repeatIsExplicit) { + lineNum = motionArgs.repeat - cm.getOption('firstLineNumber'); + } + return new Pos(lineNum, + findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum))); + }, + moveToStartOfDisplayLine: function(cm) { + cm.execCommand("goLineLeft"); + return cm.getCursor(); + }, + moveToEndOfDisplayLine: function(cm) { + cm.execCommand("goLineRight"); + var head = cm.getCursor(); + if (head.sticky == "before") head.ch--; + return head; + }, + textObjectManipulation: function(cm, head, motionArgs, vim) { + // TODO: lots of possible exceptions that can be thrown here. Try da( + // outside of a () block. + /** @type{Object} */ + var mirroredPairs = {'(': ')', ')': '(', + '{': '}', '}': '{', + '[': ']', ']': '[', + '<': '>', '>': '<'}; + /** @type{Object} */ + var selfPaired = {'\'': true, '"': true, '`': true}; + + var character = motionArgs.selectedCharacter || ""; + // 'b' refers to '()' block. + // 'B' refers to '{}' block. + if (character == 'b') { + character = '('; + } else if (character == 'B') { + character = '{'; + } + + // Inclusive is the difference between a and i + // TODO: Instead of using the additional text object map to perform text + // object operations, merge the map into the defaultKeyMap and use + // motionArgs to define behavior. Define separate entries for 'aw', + // 'iw', 'a[', 'i[', etc. + var inclusive = !motionArgs.textObjectInner; + + var tmp, move; + if (mirroredPairs[character]) { + move = true; + tmp = selectCompanionObject(cm, head, character, inclusive); + if (!tmp) { + var sc = cm.getSearchCursor(new RegExp("\\" + character, "g"), head) + if (sc.find()) { + // @ts-ignore + tmp = selectCompanionObject(cm, sc.from(), character, inclusive); + } + } + } else if (selfPaired[character]) { + move = true; + tmp = findBeginningAndEnd(cm, head, character, inclusive); + } else if (character === 'W' || character === 'w') { + var repeat = motionArgs.repeat || 1; + while (repeat-- > 0) { + var repeated = expandWordUnderCursor(cm, { + inclusive, + innerWord: !inclusive, + bigWord: character === 'W', + noSymbol: character === 'W', + multiline: true + }, tmp && tmp.end); + if (repeated) { + if (!tmp) tmp = repeated; + tmp.end = repeated.end; + } + } + } else if (character === 'p') { + tmp = findParagraph(cm, head, motionArgs.repeat, 0, inclusive); + motionArgs.linewise = true; + if (vim.visualMode) { + if (!vim.visualLine) { vim.visualLine = true; } + } else { + var operatorArgs = vim.inputState.operatorArgs; + if (operatorArgs) { operatorArgs.linewise = true; } + tmp.end.line--; + } + } else if (character === 't') { + tmp = expandTagUnderCursor(cm, head, inclusive); + } else if (character === 's') { + // account for cursor on end of sentence symbol + var content = cm.getLine(head.line); + if (head.ch > 0 && isEndOfSentenceSymbol(content[head.ch])) { + head.ch -= 1; + } + var end = getSentence(cm, head, motionArgs.repeat, 1, inclusive) + var start = getSentence(cm, head, motionArgs.repeat, -1, inclusive) + // closer vim behaviour, 'a' only takes the space after the sentence if there is one before and after + if (isWhiteSpaceString(cm.getLine(start.line)[start.ch]) + && isWhiteSpaceString(cm.getLine(end.line)[end.ch -1])) { + start = {line: start.line, ch: start.ch + 1} + } + tmp = {start: start, end: end}; + } + + if (!tmp) { + // No valid text object, don't move. + return null; + } - function defineMotion(name, fn) { - motions[name] = fn; + if (!cm.state.vim.visualMode) { + return [tmp.start, tmp.end]; + } else { + return expandSelection(cm, tmp.start, tmp.end, move); + } + }, + + repeatLastCharacterSearch: function(cm, head, motionArgs) { + var lastSearch = vimGlobalState.lastCharacterSearch; + var repeat = motionArgs.repeat; + var forward = motionArgs.forward === lastSearch.forward; + var increment = (lastSearch.increment ? 1 : 0) * (forward ? -1 : 1); + cm.moveH(-increment, 'char'); + motionArgs.inclusive = forward ? true : false; + var curEnd = moveToCharacter(cm, repeat, forward, lastSearch.selectedCharacter); + if (!curEnd) { + cm.moveH(increment, 'char'); + return head; + } + curEnd.ch += increment; + return curEnd; } + }; - function fillArray(val, times) { - var arr = []; - for (var i = 0; i < times; i++) { - arr.push(val); - } - return arr; + /** @arg {string} name @arg {import("./types").MotionFn} fn */ + function defineMotion(name, fn) { + motions[name] = fn; + } + + /** @arg {string} val @arg {number} times */ + function fillArray(val, times) { + var arr = []; + for (var i = 0; i < times; i++) { + arr.push(val); } - /** - * An operator acts on a text selection. It receives the list of selections - * as input. The corresponding CodeMirror selection is guaranteed to - * match the input selection. - */ - var operators = { - change: function(cm, args, ranges) { - var finalHead, text; - var vim = cm.state.vim; - var anchor = ranges[0].anchor, - head = ranges[0].head; - if (!vim.visualMode) { - text = cm.getRange(anchor, head); - var lastState = vim.lastEditInputState || {}; - if (lastState.motion == "moveByWords" && !isWhiteSpaceString(text)) { - // Exclude trailing whitespace if the range is not all whitespace. - var match = (/\s+$/).exec(text); - if (match && lastState.motionArgs && lastState.motionArgs.forward) { - head = offsetCursor(head, 0, - match[0].length); - text = text.slice(0, - match[0].length); - } - } - if (args.linewise) { - anchor = new Pos(anchor.line, findFirstNonWhiteSpaceCharacter(cm.getLine(anchor.line))); - if (head.line > anchor.line) { - head = new Pos(head.line - 1, Number.MAX_VALUE) - } - } - cm.replaceRange('', anchor, head); - finalHead = anchor; - } else if (args.fullLine) { - head.ch = Number.MAX_VALUE; - head.line--; - cm.setSelection(anchor, head) - text = cm.getSelection(); - cm.replaceSelection(""); - finalHead = anchor; - } else { + return arr; + } + /** + * An operator acts on a text selection. It receives the list of selections + * as input. The corresponding CodeMirror selection is guaranteed to + * match the input selection. + */ + /** @type {import("./types").vimOperators} */ + var operators = { + change: function(cm, args, ranges) { + var finalHead, text; + var vim = cm.state.vim; + var anchor = ranges[0].anchor, + head = ranges[0].head; + if (!vim.visualMode) { + text = cm.getRange(anchor, head); + var lastState = vim.lastEditInputState; + if (lastState?.motion == "moveByWords" && !isWhiteSpaceString(text)) { + // Exclude trailing whitespace if the range is not all whitespace. + var match = (/\s+$/).exec(text); + if (match && lastState.motionArgs && lastState.motionArgs.forward) { + head = offsetCursor(head, 0, - match[0].length); + text = text.slice(0, - match[0].length); + } + } + if (args.linewise) { + anchor = new Pos(anchor.line, findFirstNonWhiteSpaceCharacter(cm.getLine(anchor.line))); + if (head.line > anchor.line) { + head = new Pos(head.line - 1, Number.MAX_VALUE) + } + } + cm.replaceRange('', anchor, head); + finalHead = anchor; + } else if (args.fullLine) { + head.ch = Number.MAX_VALUE; + head.line--; + cm.setSelection(anchor, head) text = cm.getSelection(); - var replacement = fillArray('', ranges.length); - cm.replaceSelections(replacement); - finalHead = cursorMin(ranges[0].head, ranges[0].anchor); - } - vimGlobalState.registerController.pushText( - args.registerName, 'change', text, - args.linewise, ranges.length > 1); - actions.enterInsertMode(cm, {head: finalHead}, cm.state.vim); - }, - // delete is a javascript keyword. - 'delete': function(cm, args, ranges) { - var finalHead, text; - var vim = cm.state.vim; - if (!vim.visualBlock) { - var anchor = ranges[0].anchor, - head = ranges[0].head; - if (args.linewise && - head.line != cm.firstLine() && - anchor.line == cm.lastLine() && - anchor.line == head.line - 1) { - // Special case for dd on last line (and first line). - if (anchor.line == cm.firstLine()) { - anchor.ch = 0; - } else { - anchor = new Pos(anchor.line - 1, lineLength(cm, anchor.line - 1)); - } - } - text = cm.getRange(anchor, head); - cm.replaceRange('', anchor, head); + cm.replaceSelection(""); finalHead = anchor; - if (args.linewise) { - finalHead = motions.moveToFirstNonWhiteSpaceCharacter(cm, anchor); + } else { + text = cm.getSelection(); + var replacement = fillArray('', ranges.length); + cm.replaceSelections(replacement); + finalHead = cursorMin(ranges[0].head, ranges[0].anchor); + } + vimGlobalState.registerController.pushText( + args.registerName, 'change', text, + args.linewise, ranges.length > 1); + actions.enterInsertMode(cm, {head: finalHead}, cm.state.vim); + }, + delete: function(cm, args, ranges) { + var finalHead, text; + var vim = cm.state.vim; + if (!vim.visualBlock) { + var anchor = ranges[0].anchor, + head = ranges[0].head; + if (args.linewise && + head.line != cm.firstLine() && + anchor.line == cm.lastLine() && + anchor.line == head.line - 1) { + // Special case for dd on last line (and first line). + if (anchor.line == cm.firstLine()) { + anchor.ch = 0; + } else { + anchor = new Pos(anchor.line - 1, lineLength(cm, anchor.line - 1)); } - } else { - text = cm.getSelection(); - var replacement = fillArray('', ranges.length); - cm.replaceSelections(replacement); - finalHead = cursorMin(ranges[0].head, ranges[0].anchor); - } - vimGlobalState.registerController.pushText( - args.registerName, 'delete', text, - args.linewise, vim.visualBlock); - return clipCursorToContent(cm, finalHead); - }, - indent: function(cm, args, ranges) { - var vim = cm.state.vim; - if (cm.indentMore) { - var repeat = (vim.visualMode) ? args.repeat : 1; + } + text = cm.getRange(anchor, head); + cm.replaceRange('', anchor, head); + finalHead = anchor; + if (args.linewise) { + finalHead = motions.moveToFirstNonWhiteSpaceCharacter(cm, anchor); + } + } else { + text = cm.getSelection(); + var replacement = fillArray('', ranges.length); + cm.replaceSelections(replacement); + finalHead = cursorMin(ranges[0].head, ranges[0].anchor); + } + vimGlobalState.registerController.pushText( + args.registerName, 'delete', text, + args.linewise, vim.visualBlock); + return clipCursorToContent(cm, finalHead); + }, + indent: function(cm, args, ranges) { + var vim = cm.state.vim; + var repeat = (vim.visualMode) ? args.repeat : 1; + if (cm.indentMore) { + for (var j = 0; j < repeat; j++) { + if (args.indentRight) cm.indentMore(); + else cm.indentLess(); + } + } else { + var startLine = ranges[0].anchor.line; + var endLine = vim.visualBlock ? + ranges[ranges.length - 1].anchor.line : + ranges[0].head.line; + // In visual mode, n> shifts the selection right n times, instead of + // shifting n lines right once. + if (args.linewise) { + // The only way to delete a newline is to delete until the start of + // the next line, so in linewise mode evalInput will include the next + // line. We don't want this in indent, so we go back a line. + endLine--; + } + for (var i = startLine; i <= endLine; i++) { for (var j = 0; j < repeat; j++) { - if (args.indentRight) cm.indentMore(); - else cm.indentLess(); - } + cm.indentLine(i, args.indentRight); + } + } + } + return motions.moveToFirstNonWhiteSpaceCharacter(cm, ranges[0].anchor); + }, + indentAuto: function(cm, _args, ranges) { + cm.execCommand("indentAuto"); + return motions.moveToFirstNonWhiteSpaceCharacter(cm, ranges[0].anchor); + }, + hardWrap: function(cm, operatorArgs, ranges, oldAnchor) { + if (!cm.hardWrap) return; + var from = ranges[0].anchor.line; + var to = ranges[0].head.line; + if (operatorArgs.linewise) to--; + var endRow = cm.hardWrap({from: from, to: to}); + if (endRow > from && operatorArgs.linewise) endRow--; + return operatorArgs.keepCursor ? oldAnchor : new Pos(endRow, 0); + }, + changeCase: function(cm, args, ranges, oldAnchor, newHead) { + var selections = cm.getSelections(); + var swapped = []; + var toLower = args.toLower; + for (var j = 0; j < selections.length; j++) { + var toSwap = selections[j]; + var text = ''; + if (toLower === true) { + text = toSwap.toLowerCase(); + } else if (toLower === false) { + text = toSwap.toUpperCase(); } else { - var startLine = ranges[0].anchor.line; - var endLine = vim.visualBlock ? - ranges[ranges.length - 1].anchor.line : - ranges[0].head.line; - // In visual mode, n> shifts the selection right n times, instead of - // shifting n lines right once. - var repeat = (vim.visualMode) ? args.repeat : 1; - if (args.linewise) { - // The only way to delete a newline is to delete until the start of - // the next line, so in linewise mode evalInput will include the next - // line. We don't want this in indent, so we go back a line. - endLine--; - } - for (var i = startLine; i <= endLine; i++) { - for (var j = 0; j < repeat; j++) { - cm.indentLine(i, args.indentRight); - } + for (var i = 0; i < toSwap.length; i++) { + var character = toSwap.charAt(i); + text += isUpperCase(character) ? character.toLowerCase() : + character.toUpperCase(); } } - return motions.moveToFirstNonWhiteSpaceCharacter(cm, ranges[0].anchor); - }, - indentAuto: function(cm, _args, ranges) { - cm.execCommand("indentAuto"); - return motions.moveToFirstNonWhiteSpaceCharacter(cm, ranges[0].anchor); - }, - hardWrap: function(cm, operatorArgs, ranges, oldAnchor, newHead) { - if (!cm.hardWrap) return; - var from = ranges[0].anchor.line; - var to = ranges[0].head.line; - if (operatorArgs.linewise) to--; - var endRow = cm.hardWrap({from: from, to: to}); - if (endRow > from && operatorArgs.linewise) endRow--; - return operatorArgs.keepCursor ? oldAnchor : new Pos(endRow, 0); - }, - changeCase: function(cm, args, ranges, oldAnchor, newHead) { - var selections = cm.getSelections(); - var swapped = []; - var toLower = args.toLower; - for (var j = 0; j < selections.length; j++) { - var toSwap = selections[j]; - var text = ''; - if (toLower === true) { - text = toSwap.toLowerCase(); - } else if (toLower === false) { - text = toSwap.toUpperCase(); - } else { - for (var i = 0; i < toSwap.length; i++) { - var character = toSwap.charAt(i); - text += isUpperCase(character) ? character.toLowerCase() : - character.toUpperCase(); - } - } - swapped.push(text); - } - cm.replaceSelections(swapped); - if (args.shouldMoveCursor){ - return newHead; - } else if (!cm.state.vim.visualMode && args.linewise && ranges[0].anchor.line + 1 == ranges[0].head.line) { - return motions.moveToFirstNonWhiteSpaceCharacter(cm, oldAnchor); - } else if (args.linewise){ - return oldAnchor; - } else { - return cursorMin(ranges[0].anchor, ranges[0].head); - } - }, - yank: function(cm, args, ranges, oldAnchor) { - var vim = cm.state.vim; - var text = cm.getSelection(); - var endPos = vim.visualMode - ? cursorMin(vim.sel.anchor, vim.sel.head, ranges[0].head, ranges[0].anchor) - : oldAnchor; - vimGlobalState.registerController.pushText( - args.registerName, 'yank', - text, args.linewise, vim.visualBlock); - return endPos; + swapped.push(text); } - }; - - function defineOperator(name, fn) { - operators[name] = fn; + cm.replaceSelections(swapped); + if (args.shouldMoveCursor){ + return newHead; + } else if (!cm.state.vim.visualMode && args.linewise && ranges[0].anchor.line + 1 == ranges[0].head.line) { + return motions.moveToFirstNonWhiteSpaceCharacter(cm, oldAnchor); + } else if (args.linewise){ + return oldAnchor; + } else { + return cursorMin(ranges[0].anchor, ranges[0].head); + } + }, + yank: function(cm, args, ranges, oldAnchor) { + var vim = cm.state.vim; + var text = cm.getSelection(); + var endPos = vim.visualMode + ? cursorMin(vim.sel.anchor, vim.sel.head, ranges[0].head, ranges[0].anchor) + : oldAnchor; + vimGlobalState.registerController.pushText( + args.registerName, 'yank', + text, args.linewise, vim.visualBlock); + return endPos; } + }; - var actions = { - jumpListWalk: function(cm, actionArgs, vim) { - if (vim.visualMode) { - return; - } - var repeat = actionArgs.repeat; - var forward = actionArgs.forward; - var jumpList = vimGlobalState.jumpList; + /** @arg {string} name @arg {import("./types").OperatorFn} fn */ + function defineOperator(name, fn) { + operators[name] = fn; + } - var mark = jumpList.move(cm, forward ? repeat : -repeat); - var markPos = mark ? mark.find() : undefined; - markPos = markPos ? markPos : cm.getCursor(); - cm.setCursor(markPos); - }, - scroll: function(cm, actionArgs, vim) { - if (vim.visualMode) { - return; + /** @type {import("./types").vimActions} */ + var actions = { + jumpListWalk: function(cm, actionArgs, vim) { + if (vim.visualMode) { + return; + } + var repeat = actionArgs.repeat || 1; + var forward = actionArgs.forward; + var jumpList = vimGlobalState.jumpList; + + var mark = jumpList.move(cm, forward ? repeat : -repeat); + var markPos = mark ? mark.find() : undefined; + markPos = markPos ? markPos : cm.getCursor(); + cm.setCursor(markPos); + }, + scroll: function(cm, actionArgs, vim) { + if (vim.visualMode) { + return; + } + var repeat = actionArgs.repeat || 1; + var lineHeight = cm.defaultTextHeight(); + var top = cm.getScrollInfo().top; + var delta = lineHeight * repeat; + var newPos = actionArgs.forward ? top + delta : top - delta; + var cursor = copyCursor(cm.getCursor()); + var cursorCoords = cm.charCoords(cursor, 'local'); + if (actionArgs.forward) { + if (newPos > cursorCoords.top) { + cursor.line += (newPos - cursorCoords.top) / lineHeight; + cursor.line = Math.ceil(cursor.line); + cm.setCursor(cursor); + cursorCoords = cm.charCoords(cursor, 'local'); + cm.scrollTo(null, cursorCoords.top); + } else { + // Cursor stays within bounds. Just reposition the scroll window. + cm.scrollTo(null, newPos); } - var repeat = actionArgs.repeat || 1; - var lineHeight = cm.defaultTextHeight(); - var top = cm.getScrollInfo().top; - var delta = lineHeight * repeat; - var newPos = actionArgs.forward ? top + delta : top - delta; - var cursor = copyCursor(cm.getCursor()); - var cursorCoords = cm.charCoords(cursor, 'local'); - if (actionArgs.forward) { - if (newPos > cursorCoords.top) { - cursor.line += (newPos - cursorCoords.top) / lineHeight; - cursor.line = Math.ceil(cursor.line); - cm.setCursor(cursor); - cursorCoords = cm.charCoords(cursor, 'local'); - cm.scrollTo(null, cursorCoords.top); - } else { - // Cursor stays within bounds. Just reposition the scroll window. - cm.scrollTo(null, newPos); - } + } else { + var newBottom = newPos + cm.getScrollInfo().clientHeight; + if (newBottom < cursorCoords.bottom) { + cursor.line -= (cursorCoords.bottom - newBottom) / lineHeight; + cursor.line = Math.floor(cursor.line); + cm.setCursor(cursor); + cursorCoords = cm.charCoords(cursor, 'local'); + cm.scrollTo( + null, cursorCoords.bottom - cm.getScrollInfo().clientHeight); } else { - var newBottom = newPos + cm.getScrollInfo().clientHeight; - if (newBottom < cursorCoords.bottom) { - cursor.line -= (cursorCoords.bottom - newBottom) / lineHeight; - cursor.line = Math.floor(cursor.line); - cm.setCursor(cursor); - cursorCoords = cm.charCoords(cursor, 'local'); - cm.scrollTo( - null, cursorCoords.bottom - cm.getScrollInfo().clientHeight); + // Cursor stays within bounds. Just reposition the scroll window. + cm.scrollTo(null, newPos); + } + } + }, + scrollToCursor: function(cm, actionArgs) { + var lineNum = cm.getCursor().line; + var charCoords = cm.charCoords(new Pos(lineNum, 0), 'local'); + var height = cm.getScrollInfo().clientHeight; + var y = charCoords.top; + switch (actionArgs.position) { + case 'center': y = charCoords.bottom - height / 2; + break; + case 'bottom': + var lineLastCharPos = new Pos(lineNum, cm.getLine(lineNum).length - 1); + var lineLastCharCoords = cm.charCoords(lineLastCharPos, 'local'); + var lineHeight = lineLastCharCoords.bottom - y; + y = y - height + lineHeight + break; + } + cm.scrollTo(null, y); + }, + replayMacro: function(cm, actionArgs, vim) { + var registerName = actionArgs.selectedCharacter || ""; + var repeat = actionArgs.repeat || 1; + var macroModeState = vimGlobalState.macroModeState; + if (registerName == '@') { + registerName = macroModeState.latestRegister; + } else { + macroModeState.latestRegister = registerName; + } + while(repeat--){ + executeMacroRegister(cm, vim, macroModeState, registerName); + } + }, + enterMacroRecordMode: function(cm, actionArgs) { + var macroModeState = vimGlobalState.macroModeState; + var registerName = actionArgs.selectedCharacter; + if (vimGlobalState.registerController.isValidRegister(registerName)) { + macroModeState.enterMacroRecordMode(cm, registerName); + } + }, + toggleOverwrite: function(cm) { + if (!cm.state.overwrite) { + cm.toggleOverwrite(true); + cm.setOption('keyMap', 'vim-replace'); + CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"}); + } else { + cm.toggleOverwrite(false); + cm.setOption('keyMap', 'vim-insert'); + CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"}); + } + }, + enterInsertMode: function(cm, actionArgs, vim) { + if (cm.getOption('readOnly')) { return; } + vim.insertMode = true; + vim.insertModeRepeat = actionArgs && actionArgs.repeat || 1; + var insertAt = (actionArgs) ? actionArgs.insertAt : null; + var sel = vim.sel; + var head = actionArgs.head || cm.getCursor('head'); + var height = cm.listSelections().length; + if (insertAt == 'eol') { + head = new Pos(head.line, lineLength(cm, head.line)); + } else if (insertAt == 'bol') { + head = new Pos(head.line, 0); + } else if (insertAt == 'charAfter') { + var newPosition = updateSelectionForSurrogateCharacters(cm, head, offsetCursor(head, 0, 1)); + head = newPosition.end; + } else if (insertAt == 'firstNonBlank') { + var newPosition = updateSelectionForSurrogateCharacters(cm, head, motions.moveToFirstNonWhiteSpaceCharacter(cm, head)); + head = newPosition.end; + } else if (insertAt == 'startOfSelectedArea') { + if (!vim.visualMode) + return; + if (!vim.visualBlock) { + if (sel.head.line < sel.anchor.line) { + head = sel.head; } else { - // Cursor stays within bounds. Just reposition the scroll window. - cm.scrollTo(null, newPos); + head = new Pos(sel.anchor.line, 0); } - } - }, - scrollToCursor: function(cm, actionArgs) { - var lineNum = cm.getCursor().line; - var charCoords = cm.charCoords(new Pos(lineNum, 0), 'local'); - var height = cm.getScrollInfo().clientHeight; - var y = charCoords.top; - switch (actionArgs.position) { - case 'center': y = charCoords.bottom - height / 2; - break; - case 'bottom': - var lineLastCharPos = new Pos(lineNum, cm.getLine(lineNum).length - 1); - var lineLastCharCoords = cm.charCoords(lineLastCharPos, 'local'); - var lineHeight = lineLastCharCoords.bottom - y; - y = y - height + lineHeight - break; - } - cm.scrollTo(null, y); - }, - replayMacro: function(cm, actionArgs, vim) { - var registerName = actionArgs.selectedCharacter; - var repeat = actionArgs.repeat; - var macroModeState = vimGlobalState.macroModeState; - if (registerName == '@') { - registerName = macroModeState.latestRegister; - } else { - macroModeState.latestRegister = registerName; - } - while(repeat--){ - executeMacroRegister(cm, vim, macroModeState, registerName); - } - }, - enterMacroRecordMode: function(cm, actionArgs) { - var macroModeState = vimGlobalState.macroModeState; - var registerName = actionArgs.selectedCharacter; - if (vimGlobalState.registerController.isValidRegister(registerName)) { - macroModeState.enterMacroRecordMode(cm, registerName); - } - }, - toggleOverwrite: function(cm) { - if (!cm.state.overwrite) { - cm.toggleOverwrite(true); - cm.setOption('keyMap', 'vim-replace'); - CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"}); } else { - cm.toggleOverwrite(false); - cm.setOption('keyMap', 'vim-insert'); - CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"}); + head = new Pos( + Math.min(sel.head.line, sel.anchor.line), + Math.min(sel.head.ch, sel.anchor.ch)); + height = Math.abs(sel.head.line - sel.anchor.line) + 1; } - }, - enterInsertMode: function(cm, actionArgs, vim) { - if (cm.getOption('readOnly')) { return; } - vim.insertMode = true; - vim.insertModeRepeat = actionArgs && actionArgs.repeat || 1; - var insertAt = (actionArgs) ? actionArgs.insertAt : null; - var sel = vim.sel; - var head = actionArgs.head || cm.getCursor('head'); - var height = cm.listSelections().length; - if (insertAt == 'eol') { - head = new Pos(head.line, lineLength(cm, head.line)); - } else if (insertAt == 'bol') { - head = new Pos(head.line, 0); - } else if (insertAt == 'charAfter') { - var newPosition = updateSelectionForSurrogateCharacters(cm, head, offsetCursor(head, 0, 1)); - head = newPosition.end; - } else if (insertAt == 'firstNonBlank') { - var newPosition = updateSelectionForSurrogateCharacters(cm, head, motions.moveToFirstNonWhiteSpaceCharacter(cm, head)); - head = newPosition.end; - } else if (insertAt == 'startOfSelectedArea') { + } else if (insertAt == 'endOfSelectedArea') { if (!vim.visualMode) - return; - if (!vim.visualBlock) { - if (sel.head.line < sel.anchor.line) { - head = sel.head; - } else { - head = new Pos(sel.anchor.line, 0); - } - } else { - head = new Pos( - Math.min(sel.head.line, sel.anchor.line), - Math.min(sel.head.ch, sel.anchor.ch)); - height = Math.abs(sel.head.line - sel.anchor.line) + 1; - } - } else if (insertAt == 'endOfSelectedArea') { - if (!vim.visualMode) - return; - if (!vim.visualBlock) { - if (sel.head.line >= sel.anchor.line) { - head = offsetCursor(sel.head, 0, 1); - } else { - head = new Pos(sel.anchor.line, 0); - } - } else { - head = new Pos( - Math.min(sel.head.line, sel.anchor.line), - Math.max(sel.head.ch, sel.anchor.ch) + 1); - height = Math.abs(sel.head.line - sel.anchor.line) + 1; - } - } else if (insertAt == 'inplace') { - if (vim.visualMode){ return; + if (!vim.visualBlock) { + if (sel.head.line >= sel.anchor.line) { + head = offsetCursor(sel.head, 0, 1); + } else { + head = new Pos(sel.anchor.line, 0); } - } else if (insertAt == 'lastEdit') { - head = getLastEditPos(cm) || head; - } - cm.setOption('disableInput', false); - if (actionArgs && actionArgs.replace) { - // Handle Replace-mode as a special case of insert mode. - cm.toggleOverwrite(true); - cm.setOption('keyMap', 'vim-replace'); - CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"}); } else { - cm.toggleOverwrite(false); - cm.setOption('keyMap', 'vim-insert'); - CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"}); + head = new Pos( + Math.min(sel.head.line, sel.anchor.line), + Math.max(sel.head.ch, sel.anchor.ch) + 1); + height = Math.abs(sel.head.line - sel.anchor.line) + 1; } - if (!vimGlobalState.macroModeState.isPlaying) { - // Only record if not replaying. - cm.on('change', onChange); - if (vim.insertEnd) vim.insertEnd.clear(); - vim.insertEnd = cm.setBookmark(head, {insertLeft: true}); - CodeMirror.on(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); - } - if (vim.visualMode) { - exitVisualMode(cm); - } - selectForInsert(cm, head, height); - }, - toggleVisualMode: function(cm, actionArgs, vim) { - var repeat = actionArgs.repeat; - var anchor = cm.getCursor(); - var head; - // TODO: The repeat should actually select number of characters/lines - // equal to the repeat times the size of the previous visual - // operation. - if (!vim.visualMode) { - // Entering visual mode - vim.visualMode = true; - vim.visualLine = !!actionArgs.linewise; - vim.visualBlock = !!actionArgs.blockwise; - head = clipCursorToContent( - cm, new Pos(anchor.line, anchor.ch + repeat - 1)); - var newPosition = updateSelectionForSurrogateCharacters(cm, anchor, head) - vim.sel = { - anchor: newPosition.start, - head: newPosition.end - }; - CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""}); - updateCmSelection(cm); - updateMark(cm, vim, '<', cursorMin(anchor, head)); - updateMark(cm, vim, '>', cursorMax(anchor, head)); - } else if (vim.visualLine ^ actionArgs.linewise || - vim.visualBlock ^ actionArgs.blockwise) { - // Toggling between modes - vim.visualLine = !!actionArgs.linewise; - vim.visualBlock = !!actionArgs.blockwise; - CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""}); - updateCmSelection(cm); - } else { - exitVisualMode(cm); + } else if (insertAt == 'inplace') { + if (vim.visualMode){ + return; } - }, - reselectLastSelection: function(cm, _actionArgs, vim) { - var lastSelection = vim.lastSelection; - if (vim.visualMode) { - updateLastSelection(cm, vim); + } else if (insertAt == 'lastEdit') { + head = getLastEditPos(cm) || head; + } + cm.setOption('disableInput', false); + if (actionArgs && actionArgs.replace) { + // Handle Replace-mode as a special case of insert mode. + cm.toggleOverwrite(true); + cm.setOption('keyMap', 'vim-replace'); + CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"}); + } else { + cm.toggleOverwrite(false); + cm.setOption('keyMap', 'vim-insert'); + CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"}); + } + if (!vimGlobalState.macroModeState.isPlaying) { + // Only record if not replaying. + cm.on('change', onChange); + if (vim.insertEnd) vim.insertEnd.clear(); + vim.insertEnd = cm.setBookmark(head, {insertLeft: true}); + CodeMirror.on(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); + } + if (vim.visualMode) { + exitVisualMode(cm); + } + selectForInsert(cm, head, height); + }, + toggleVisualMode: function(cm, actionArgs, vim) { + var repeat = actionArgs.repeat; + var anchor = cm.getCursor(); + var head; + // TODO: The repeat should actually select number of characters/lines + // equal to the repeat times the size of the previous visual + // operation. + if (!vim.visualMode) { + // Entering visual mode + vim.visualMode = true; + vim.visualLine = !!actionArgs.linewise; + vim.visualBlock = !!actionArgs.blockwise; + head = clipCursorToContent( + cm, new Pos(anchor.line, anchor.ch + repeat - 1)); + var newPosition = updateSelectionForSurrogateCharacters(cm, anchor, head) + vim.sel = { + anchor: newPosition.start, + head: newPosition.end + }; + CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""}); + updateCmSelection(cm); + updateMark(cm, vim, '<', cursorMin(anchor, head)); + updateMark(cm, vim, '>', cursorMax(anchor, head)); + } else if (vim.visualLine != !!actionArgs.linewise || + vim.visualBlock != !!actionArgs.blockwise) { + // Toggling between modes + vim.visualLine = !!actionArgs.linewise; + vim.visualBlock = !!actionArgs.blockwise; + CodeMirror.signal(cm, "vim-mode-change", {mode: "visual", subMode: vim.visualLine ? "linewise" : vim.visualBlock ? "blockwise" : ""}); + updateCmSelection(cm); + } else { + exitVisualMode(cm); + } + }, + reselectLastSelection: function(cm, _actionArgs, vim) { + var lastSelection = vim.lastSelection; + if (vim.visualMode) { + updateLastSelection(cm, vim); + } + if (lastSelection) { + var anchor = lastSelection.anchorMark.find(); + var head = lastSelection.headMark.find(); + if (!anchor || !head) { + // If the marks have been destroyed due to edits, do nothing. + return; } - if (lastSelection) { - var anchor = lastSelection.anchorMark.find(); - var head = lastSelection.headMark.find(); - if (!anchor || !head) { - // If the marks have been destroyed due to edits, do nothing. - return; + vim.sel = { + anchor: anchor, + head: head + }; + vim.visualMode = true; + vim.visualLine = lastSelection.visualLine; + vim.visualBlock = lastSelection.visualBlock; + updateCmSelection(cm); + updateMark(cm, vim, '<', cursorMin(anchor, head)); + updateMark(cm, vim, '>', cursorMax(anchor, head)); + CodeMirror.signal(cm, 'vim-mode-change', { + mode: 'visual', + subMode: vim.visualLine ? 'linewise' : + vim.visualBlock ? 'blockwise' : ''}); + } + }, + joinLines: function(cm, actionArgs, vim) { + var curStart, curEnd; + if (vim.visualMode) { + curStart = cm.getCursor('anchor'); + curEnd = cm.getCursor('head'); + if (cursorIsBefore(curEnd, curStart)) { + var tmp = curEnd; + curEnd = curStart; + curStart = tmp; + } + curEnd.ch = lineLength(cm, curEnd.line) - 1; + } else { + // Repeat is the number of lines to join. Minimum 2 lines. + var repeat = Math.max(actionArgs.repeat, 2); + curStart = cm.getCursor(); + curEnd = clipCursorToContent(cm, new Pos(curStart.line + repeat - 1, + Infinity)); + } + var finalCh = 0; + for (var i = curStart.line; i < curEnd.line; i++) { + finalCh = lineLength(cm, curStart.line); + var text = ''; + var nextStartCh = 0; + if (!actionArgs.keepSpaces) { + var nextLine = cm.getLine(curStart.line + 1); + nextStartCh = nextLine.search(/\S/); + if (nextStartCh == -1) { + nextStartCh = nextLine.length; + } else { + text = " "; } - vim.sel = { - anchor: anchor, - head: head - }; - vim.visualMode = true; - vim.visualLine = lastSelection.visualLine; - vim.visualBlock = lastSelection.visualBlock; - updateCmSelection(cm); - updateMark(cm, vim, '<', cursorMin(anchor, head)); - updateMark(cm, vim, '>', cursorMax(anchor, head)); - CodeMirror.signal(cm, 'vim-mode-change', { - mode: 'visual', - subMode: vim.visualLine ? 'linewise' : - vim.visualBlock ? 'blockwise' : ''}); } - }, - joinLines: function(cm, actionArgs, vim) { - var curStart, curEnd; - if (vim.visualMode) { - curStart = cm.getCursor('anchor'); - curEnd = cm.getCursor('head'); - if (cursorIsBefore(curEnd, curStart)) { - var tmp = curEnd; - curEnd = curStart; - curStart = tmp; + cm.replaceRange(text, + new Pos(curStart.line, finalCh), + new Pos(curStart.line + 1, nextStartCh)); + } + var curFinalPos = clipCursorToContent(cm, new Pos(curStart.line, finalCh)); + if (vim.visualMode) { + exitVisualMode(cm, false); + } + cm.setCursor(curFinalPos); + }, + newLineAndEnterInsertMode: function(cm, actionArgs, vim) { + vim.insertMode = true; + var insertAt = copyCursor(cm.getCursor()); + if (insertAt.line === cm.firstLine() && !actionArgs.after) { + // Special case for inserting newline before start of document. + cm.replaceRange('\n', new Pos(cm.firstLine(), 0)); + cm.setCursor(cm.firstLine(), 0); + } else { + insertAt.line = (actionArgs.after) ? insertAt.line : + insertAt.line - 1; + insertAt.ch = lineLength(cm, insertAt.line); + cm.setCursor(insertAt); + var newlineFn = CodeMirror.commands.newlineAndIndentContinueComment || + CodeMirror.commands.newlineAndIndent; + newlineFn(cm); + } + this.enterInsertMode(cm, { repeat: actionArgs.repeat }, vim); + }, + paste: function(cm, actionArgs, vim) { + var register = vimGlobalState.registerController.getRegister( + actionArgs.registerName); + if (actionArgs.registerName === '+') { + navigator.clipboard.readText().then((value) => { + this.continuePaste(cm, actionArgs, vim, value, register); + }) + } else { + var text = register.toString(); + this.continuePaste(cm, actionArgs, vim, text, register); + } + }, + continuePaste: function(cm, actionArgs, vim, text, register) { + var cur = copyCursor(cm.getCursor()); + if (!text) { + return; + } + if (actionArgs.matchIndent) { + var tabSize = cm.getOption("tabSize"); + // length that considers tabs and tabSize + var whitespaceLength = function(/** @type {string} */ str) { + var tabs = (str.split("\t").length - 1); + var spaces = (str.split(" ").length - 1); + return tabs * tabSize + spaces * 1; + }; + var currentLine = cm.getLine(cm.getCursor().line); + // @ts-ignore + var indent = whitespaceLength(currentLine.match(/^\s*/)[0]); + // chomp last newline b/c don't want it to match /^\s*/gm + var chompedText = text.replace(/\n$/, ''); + var wasChomped = text !== chompedText; + // @ts-ignore + var firstIndent = whitespaceLength(text.match(/^\s*/)[0]); + var text = chompedText.replace(/^\s*/gm, function(wspace) { + var newIndent = indent + (whitespaceLength(wspace) - firstIndent); + if (newIndent < 0) { + return ""; + } + else if (cm.getOption("indentWithTabs")) { + var quotient = Math.floor(newIndent / tabSize); + return Array(quotient + 1).join('\t'); + } + else { + return Array(newIndent + 1).join(' '); } - curEnd.ch = lineLength(cm, curEnd.line) - 1; + }); + text += wasChomped ? "\n" : ""; + } + if (actionArgs.repeat > 1) { + var text = Array(actionArgs.repeat + 1).join(text); + } + var linewise = register.linewise; + var blockwise = register.blockwise; + if (blockwise) { + // @ts-ignore + text = text.split('\n'); + if (linewise) { + // @ts-ignore + text.pop(); + } + for (var i = 0; i < text.length; i++) { + // @ts-ignore + text[i] = (text[i] == '') ? ' ' : text[i]; + } + cur.ch += actionArgs.after ? 1 : 0; + cur.ch = Math.min(lineLength(cm, cur.line), cur.ch); + } else if (linewise) { + if(vim.visualMode) { + text = vim.visualLine ? text.slice(0, -1) : '\n' + text.slice(0, text.length - 1) + '\n'; + } else if (actionArgs.after) { + // Move the newline at the end to the start instead, and paste just + // before the newline character of the line we are on right now. + text = '\n' + text.slice(0, text.length - 1); + cur.ch = lineLength(cm, cur.line); } else { - // Repeat is the number of lines to join. Minimum 2 lines. - var repeat = Math.max(actionArgs.repeat, 2); - curStart = cm.getCursor(); - curEnd = clipCursorToContent(cm, new Pos(curStart.line + repeat - 1, - Infinity)); - } - var finalCh = 0; - for (var i = curStart.line; i < curEnd.line; i++) { - finalCh = lineLength(cm, curStart.line); - var text = ''; - var nextStartCh = 0; - if (!actionArgs.keepSpaces) { - var nextLine = cm.getLine(curStart.line + 1); - nextStartCh = nextLine.search(/\S/); - if (nextStartCh == -1) { - nextStartCh = nextLine.length; - } else { - text = " "; - } - } - cm.replaceRange(text, - new Pos(curStart.line, finalCh), - new Pos(curStart.line + 1, nextStartCh)); + cur.ch = 0; } - var curFinalPos = clipCursorToContent(cm, new Pos(curStart.line, finalCh)); - if (vim.visualMode) { - exitVisualMode(cm, false); + } else { + cur.ch += actionArgs.after ? 1 : 0; + } + var curPosFinal; + if (vim.visualMode) { + // save the pasted text for reselection if the need arises + vim.lastPastedText = text; + var lastSelectionCurEnd; + var selectedArea = getSelectedAreaRange(cm, vim); + var selectionStart = selectedArea[0]; + var selectionEnd = selectedArea[1]; + var selectedText = cm.getSelection(); + var selections = cm.listSelections(); + var emptyStrings = new Array(selections.length).join('1').split('1'); + // save the curEnd marker before it get cleared due to cm.replaceRange. + if (vim.lastSelection) { + lastSelectionCurEnd = vim.lastSelection.headMark.find(); } - cm.setCursor(curFinalPos); - }, - newLineAndEnterInsertMode: function(cm, actionArgs, vim) { - vim.insertMode = true; - var insertAt = copyCursor(cm.getCursor()); - if (insertAt.line === cm.firstLine() && !actionArgs.after) { - // Special case for inserting newline before start of document. - cm.replaceRange('\n', new Pos(cm.firstLine(), 0)); - cm.setCursor(cm.firstLine(), 0); - } else { - insertAt.line = (actionArgs.after) ? insertAt.line : - insertAt.line - 1; - insertAt.ch = lineLength(cm, insertAt.line); - cm.setCursor(insertAt); - var newlineFn = CodeMirror.commands.newlineAndIndentContinueComment || - CodeMirror.commands.newlineAndIndent; - newlineFn(cm); - } - this.enterInsertMode(cm, { repeat: actionArgs.repeat }, vim); - }, - paste: function(cm, actionArgs, vim) { - var register = vimGlobalState.registerController.getRegister( - actionArgs.registerName); - if (actionArgs.registerName === '+') { - navigator.clipboard.readText().then((value) => { - this.continuePaste(cm, actionArgs, vim, value, register); - }) + // push the previously selected text to unnamed register + vimGlobalState.registerController.unnamedRegister.setText(selectedText); + if (blockwise) { + // first delete the selected text + cm.replaceSelections(emptyStrings); + // Set new selections as per the block length of the yanked text + selectionEnd = new Pos(selectionStart.line + text.length-1, selectionStart.ch); + cm.setCursor(selectionStart); + selectBlock(cm, selectionEnd); + // @ts-ignore + cm.replaceSelections(text); + curPosFinal = selectionStart; + } else if (vim.visualBlock) { + cm.replaceSelections(emptyStrings); + cm.setCursor(selectionStart); + cm.replaceRange(text, selectionStart, selectionStart); + curPosFinal = selectionStart; } else { - var text = register.toString(); - this.continuePaste(cm, actionArgs, vim, text, register); + cm.replaceRange(text, selectionStart, selectionEnd); + curPosFinal = cm.posFromIndex(cm.indexFromPos(selectionStart) + text.length - 1); } - }, - continuePaste: function(cm, actionArgs, vim, text, register) { - var cur = copyCursor(cm.getCursor()); - if (!text) { - return; + // restore the curEnd marker + if(lastSelectionCurEnd) { + vim.lastSelection.headMark = cm.setBookmark(lastSelectionCurEnd); } - if (actionArgs.matchIndent) { - var tabSize = cm.getOption("tabSize"); - // length that considers tabs and tabSize - var whitespaceLength = function(str) { - var tabs = (str.split("\t").length - 1); - var spaces = (str.split(" ").length - 1); - return tabs * tabSize + spaces * 1; - }; - var currentLine = cm.getLine(cm.getCursor().line); - var indent = whitespaceLength(currentLine.match(/^\s*/)[0]); - // chomp last newline b/c don't want it to match /^\s*/gm - var chompedText = text.replace(/\n$/, ''); - var wasChomped = text !== chompedText; - var firstIndent = whitespaceLength(text.match(/^\s*/)[0]); - var text = chompedText.replace(/^\s*/gm, function(wspace) { - var newIndent = indent + (whitespaceLength(wspace) - firstIndent); - if (newIndent < 0) { - return ""; - } - else if (cm.getOption("indentWithTabs")) { - var quotient = Math.floor(newIndent / tabSize); - return Array(quotient + 1).join('\t'); - } - else { - return Array(newIndent + 1).join(' '); - } - }); - text += wasChomped ? "\n" : ""; - } - if (actionArgs.repeat > 1) { - var text = Array(actionArgs.repeat + 1).join(text); + if (linewise) { + curPosFinal.ch=0; } - var linewise = register.linewise; - var blockwise = register.blockwise; + } else { if (blockwise) { - text = text.split('\n'); - if (linewise) { - text.pop(); - } + cm.setCursor(cur); for (var i = 0; i < text.length; i++) { - text[i] = (text[i] == '') ? ' ' : text[i]; - } - cur.ch += actionArgs.after ? 1 : 0; - cur.ch = Math.min(lineLength(cm, cur.line), cur.ch); - } else if (linewise) { - if(vim.visualMode) { - text = vim.visualLine ? text.slice(0, -1) : '\n' + text.slice(0, text.length - 1) + '\n'; - } else if (actionArgs.after) { - // Move the newline at the end to the start instead, and paste just - // before the newline character of the line we are on right now. - text = '\n' + text.slice(0, text.length - 1); - cur.ch = lineLength(cm, cur.line); - } else { - cur.ch = 0; + var line = cur.line+i; + if (line > cm.lastLine()) { + cm.replaceRange('\n', new Pos(line, 0)); + } + var lastCh = lineLength(cm, line); + if (lastCh < cur.ch) { + extendLineToColumn(cm, line, cur.ch); + } } + cm.setCursor(cur); + selectBlock(cm, new Pos(cur.line + text.length-1, cur.ch)); + // @ts-ignore + cm.replaceSelections(text); + curPosFinal = cur; } else { - cur.ch += actionArgs.after ? 1 : 0; - } - var curPosFinal; - if (vim.visualMode) { - // save the pasted text for reselection if the need arises - vim.lastPastedText = text; - var lastSelectionCurEnd; - var selectedArea = getSelectedAreaRange(cm, vim); - var selectionStart = selectedArea[0]; - var selectionEnd = selectedArea[1]; - var selectedText = cm.getSelection(); - var selections = cm.listSelections(); - var emptyStrings = new Array(selections.length).join('1').split('1'); - // save the curEnd marker before it get cleared due to cm.replaceRange. - if (vim.lastSelection) { - lastSelectionCurEnd = vim.lastSelection.headMark.find(); - } - // push the previously selected text to unnamed register - vimGlobalState.registerController.unnamedRegister.setText(selectedText); - if (blockwise) { - // first delete the selected text - cm.replaceSelections(emptyStrings); - // Set new selections as per the block length of the yanked text - selectionEnd = new Pos(selectionStart.line + text.length-1, selectionStart.ch); - cm.setCursor(selectionStart); - selectBlock(cm, selectionEnd); - cm.replaceSelections(text); - curPosFinal = selectionStart; - } else if (vim.visualBlock) { - cm.replaceSelections(emptyStrings); - cm.setCursor(selectionStart); - cm.replaceRange(text, selectionStart, selectionStart); - curPosFinal = selectionStart; - } else { - cm.replaceRange(text, selectionStart, selectionEnd); - curPosFinal = cm.posFromIndex(cm.indexFromPos(selectionStart) + text.length - 1); - } - // restore the the curEnd marker - if(lastSelectionCurEnd) { - vim.lastSelection.headMark = cm.setBookmark(lastSelectionCurEnd); - } + cm.replaceRange(text, cur); + // Now fine tune the cursor to where we want it. if (linewise) { - curPosFinal.ch=0; - } - } else { - if (blockwise) { - cm.setCursor(cur); - for (var i = 0; i < text.length; i++) { - var line = cur.line+i; - if (line > cm.lastLine()) { - cm.replaceRange('\n', new Pos(line, 0)); - } - var lastCh = lineLength(cm, line); - if (lastCh < cur.ch) { - extendLineToColumn(cm, line, cur.ch); - } - } - cm.setCursor(cur); - selectBlock(cm, new Pos(cur.line + text.length-1, cur.ch)); - cm.replaceSelections(text); - curPosFinal = cur; + var line = actionArgs.after ? cur.line + 1 : cur.line; + curPosFinal = new Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); } else { - cm.replaceRange(text, cur); - // Now fine tune the cursor to where we want it. - if (linewise) { - var line = actionArgs.after ? cur.line + 1 : cur.line; - curPosFinal = new Pos(line, findFirstNonWhiteSpaceCharacter(cm.getLine(line))); - } else { - curPosFinal = copyCursor(cur); - if (!/\n/.test(text)) { - curPosFinal.ch += text.length - (actionArgs.after ? 1 : 0); - } + curPosFinal = copyCursor(cur); + if (!/\n/.test(text)) { + curPosFinal.ch += text.length - (actionArgs.after ? 1 : 0); } } } - if (vim.visualMode) { - exitVisualMode(cm, false); - } - cm.setCursor(curPosFinal); - }, - undo: function(cm, actionArgs) { - cm.operation(function() { - repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)(); - cm.setCursor(clipCursorToContent(cm, cm.getCursor('start'))); - }); - }, - redo: function(cm, actionArgs) { - repeatFn(cm, CodeMirror.commands.redo, actionArgs.repeat)(); - }, - setRegister: function(_cm, actionArgs, vim) { - vim.inputState.registerName = actionArgs.selectedCharacter; - }, - insertRegister: function(cm, actionArgs, vim) { - var registerName = actionArgs.selectedCharacter; - var register = vimGlobalState.registerController.getRegister(registerName); - var text = register && register.toString(); - if (text) { - cm.replaceSelection(text); - } - }, - oneNormalCommand: function(cm, actionArgs, vim) { - exitInsertMode(cm, true); - vim.insertModeReturn = true; - CodeMirror.on(cm, 'vim-command-done', function handler() { - if (vim.visualMode) return; - if (vim.insertModeReturn) { - vim.insertModeReturn = false; - if (!vim.insertMode) { - actions.enterInsertMode(cm, {}, vim); - } - } - CodeMirror.off(cm, 'vim-command-done', handler); - }); - }, - setMark: function(cm, actionArgs, vim) { - var markName = actionArgs.selectedCharacter; - updateMark(cm, vim, markName, cm.getCursor()); - }, - replace: function(cm, actionArgs, vim) { - var replaceWith = actionArgs.selectedCharacter; - var curStart = cm.getCursor(); - var replaceTo; - var curEnd; - var selections = cm.listSelections(); - if (vim.visualMode) { - curStart = cm.getCursor('start'); - curEnd = cm.getCursor('end'); - } else { - var line = cm.getLine(curStart.line); - replaceTo = curStart.ch + actionArgs.repeat; - if (replaceTo > line.length) { - replaceTo=line.length; + } + if (vim.visualMode) { + exitVisualMode(cm, false); + } + cm.setCursor(curPosFinal); + }, + undo: function(cm, actionArgs) { + cm.operation(function() { + repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)(); + cm.setCursor(clipCursorToContent(cm, cm.getCursor('start'))); + }); + }, + redo: function(cm, actionArgs) { + repeatFn(cm, CodeMirror.commands.redo, actionArgs.repeat)(); + }, + setRegister: function(_cm, actionArgs, vim) { + vim.inputState.registerName = actionArgs.selectedCharacter; + }, + insertRegister: function(cm, actionArgs, vim) { + var registerName = actionArgs.selectedCharacter; + var register = vimGlobalState.registerController.getRegister(registerName); + var text = register && register.toString(); + if (text) { + cm.replaceSelection(text); + } + }, + oneNormalCommand: function(cm, actionArgs, vim) { + exitInsertMode(cm, true); + vim.insertModeReturn = true; + CodeMirror.on(cm, 'vim-command-done', function handler() { + if (vim.visualMode) return; + if (vim.insertModeReturn) { + vim.insertModeReturn = false; + if (!vim.insertMode) { + actions.enterInsertMode(cm, {}, vim); } - curEnd = new Pos(curStart.line, replaceTo); } + CodeMirror.off(cm, 'vim-command-done', handler); + }); + }, + setMark: function(cm, actionArgs, vim) { + var markName = actionArgs.selectedCharacter; + if (markName) updateMark(cm, vim, markName, cm.getCursor()); + }, + replace: function(cm, actionArgs, vim) { + var replaceWith = actionArgs.selectedCharacter || ""; + var curStart = cm.getCursor(); + var replaceTo; + var curEnd; + var selections = cm.listSelections(); + if (vim.visualMode) { + curStart = cm.getCursor('start'); + curEnd = cm.getCursor('end'); + } else { + var line = cm.getLine(curStart.line); + replaceTo = curStart.ch + actionArgs.repeat; + if (replaceTo > line.length) { + replaceTo=line.length; + } + curEnd = new Pos(curStart.line, replaceTo); + } - var newPositions = updateSelectionForSurrogateCharacters(cm, curStart, curEnd); - curStart = newPositions.start; - curEnd = newPositions.end; - if (replaceWith=='\n') { - if (!vim.visualMode) cm.replaceRange('', curStart, curEnd); - // special case, where vim help says to replace by just one line-break - (CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm); - } else { - var replaceWithStr = cm.getRange(curStart, curEnd); - // replace all surrogate characters with selected character + var newPositions = updateSelectionForSurrogateCharacters(cm, curStart, curEnd); + curStart = newPositions.start; + curEnd = newPositions.end; + if (replaceWith=='\n') { + if (!vim.visualMode) cm.replaceRange('', curStart, curEnd); + // special case, where vim help says to replace by just one line-break + (CodeMirror.commands.newlineAndIndentContinueComment || CodeMirror.commands.newlineAndIndent)(cm); + } else { + var replaceWithStr = cm.getRange(curStart, curEnd); + // replace all surrogate characters with selected character + replaceWithStr = replaceWithStr.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, replaceWith); + //replace all characters in range by selected, but keep linebreaks + replaceWithStr = replaceWithStr.replace(/[^\n]/g, replaceWith); + if (vim.visualBlock) { + // Tabs are split in visua block before replacing + var spaces = new Array(cm.getOption("tabSize")+1).join(' '); + replaceWithStr = cm.getSelection(); replaceWithStr = replaceWithStr.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, replaceWith); - //replace all characters in range by selected, but keep linebreaks - replaceWithStr = replaceWithStr.replace(/[^\n]/g, replaceWith); - if (vim.visualBlock) { - // Tabs are split in visua block before replacing - var spaces = new Array(cm.getOption("tabSize")+1).join(' '); - replaceWithStr = cm.getSelection(); - replaceWithStr = replaceWithStr.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, replaceWith); - replaceWithStr = replaceWithStr.replace(/\t/g, spaces).replace(/[^\n]/g, replaceWith).split('\n'); - cm.replaceSelections(replaceWithStr); - } else { - cm.replaceRange(replaceWithStr, curStart, curEnd); - } - if (vim.visualMode) { - curStart = cursorIsBefore(selections[0].anchor, selections[0].head) ? - selections[0].anchor : selections[0].head; - cm.setCursor(curStart); - exitVisualMode(cm, false); - } else { - cm.setCursor(offsetCursor(curEnd, 0, -1)); - } - } - }, - incrementNumberToken: function(cm, actionArgs) { - var cur = cm.getCursor(); - var lineStr = cm.getLine(cur.line); - var re = /(-?)(?:(0x)([\da-f]+)|(0b|0|)(\d+))/gi; - var match; - var start; - var end; - var numberStr; - while ((match = re.exec(lineStr)) !== null) { - start = match.index; - end = start + match[0].length; - if (cur.ch < end)break; - } - if (!actionArgs.backtrack && (end <= cur.ch))return; - if (match) { - var baseStr = match[2] || match[4] - var digits = match[3] || match[5] - var increment = actionArgs.increase ? 1 : -1; - var base = {'0b': 2, '0': 8, '': 10, '0x': 16}[baseStr.toLowerCase()]; - var number = parseInt(match[1] + digits, base) + (increment * actionArgs.repeat); - numberStr = number.toString(base); - var zeroPadding = baseStr ? new Array(digits.length - numberStr.length + 1 + match[1].length).join('0') : '' - if (numberStr.charAt(0) === '-') { - numberStr = '-' + baseStr + zeroPadding + numberStr.substr(1); - } else { - numberStr = baseStr + zeroPadding + numberStr; - } - var from = new Pos(cur.line, start); - var to = new Pos(cur.line, end); - cm.replaceRange(numberStr, from, to); + var replaceWithStrings = replaceWithStr.replace(/\t/g, spaces).replace(/[^\n]/g, replaceWith).split('\n'); + cm.replaceSelections(replaceWithStrings); } else { - return; + cm.replaceRange(replaceWithStr, curStart, curEnd); } - cm.setCursor(new Pos(cur.line, start + numberStr.length - 1)); - }, - repeatLastEdit: function(cm, actionArgs, vim) { - var lastEditInputState = vim.lastEditInputState; - if (!lastEditInputState) { return; } - var repeat = actionArgs.repeat; - if (repeat && actionArgs.repeatIsExplicit) { - vim.lastEditInputState.repeatOverride = repeat; + if (vim.visualMode) { + curStart = cursorIsBefore(selections[0].anchor, selections[0].head) ? + selections[0].anchor : selections[0].head; + cm.setCursor(curStart); + exitVisualMode(cm, false); } else { - repeat = vim.lastEditInputState.repeatOverride || repeat; + cm.setCursor(offsetCursor(curEnd, 0, -1)); + } + } + }, + incrementNumberToken: function(cm, actionArgs) { + var cur = cm.getCursor(); + var lineStr = cm.getLine(cur.line); + var re = /(-?)(?:(0x)([\da-f]+)|(0b|0|)(\d+))/gi; + var match; + var start; + var end; + var numberStr; + while ((match = re.exec(lineStr)) !== null) { + start = match.index; + end = start + match[0].length; + if (cur.ch < end)break; + } + // @ts-ignore + if (!actionArgs.backtrack && (end <= cur.ch))return; + if (match) { + var baseStr = match[2] || match[4] + var digits = match[3] || match[5] + var increment = actionArgs.increase ? 1 : -1; + var base = {'0b': 2, '0': 8, '': 10, '0x': 16}[baseStr.toLowerCase()]; + var number = parseInt(match[1] + digits, base) + (increment * actionArgs.repeat); + numberStr = number.toString(base); + var zeroPadding = baseStr ? new Array(digits.length - numberStr.length + 1 + match[1].length).join('0') : '' + if (numberStr.charAt(0) === '-') { + numberStr = '-' + baseStr + zeroPadding + numberStr.substr(1); + } else { + numberStr = baseStr + zeroPadding + numberStr; } - repeatLastEdit(cm, vim, repeat, false /** repeatForInsert */); - }, - indent: function(cm, actionArgs) { - cm.indentLine(cm.getCursor().line, actionArgs.indentRight); - }, - exitInsertMode: exitInsertMode - }; - - function defineAction(name, fn) { - actions[name] = fn; + // @ts-ignore + var from = new Pos(cur.line, start); + // @ts-ignore + var to = new Pos(cur.line, end); + cm.replaceRange(numberStr, from, to); + } else { + return; + } + // @ts-ignore + cm.setCursor(new Pos(cur.line, start + numberStr.length - 1)); + }, + repeatLastEdit: function(cm, actionArgs, vim) { + var lastEditInputState = vim.lastEditInputState; + if (!lastEditInputState) { return; } + var repeat = actionArgs.repeat; + if (repeat && actionArgs.repeatIsExplicit) { + lastEditInputState.repeatOverride = repeat; + } else { + repeat = lastEditInputState.repeatOverride || repeat; + } + repeatLastEdit(cm, vim, repeat, false /** repeatForInsert */); + }, + indent: function(cm, actionArgs) { + cm.indentLine(cm.getCursor().line, actionArgs.indentRight); + }, + exitInsertMode: function(cm, actionArgs) { + exitInsertMode(cm); } + }; - /* - * Below are miscellaneous utility functions used by vim.js - */ + /** @arg {string } name @arg {import("./types").ActionFn} fn */ + function defineAction(name, fn) { + actions[name] = fn; + } - /** - * Clips cursor to ensure that line is within the buffer's range - * and is not inside surrogate pair - * If includeLineBreak is true, then allow cur.ch == lineLength. - */ - function clipCursorToContent(cm, cur, oldCur) { - var vim = cm.state.vim; - var includeLineBreak = vim.insertMode || vim.visualMode; - var line = Math.min(Math.max(cm.firstLine(), cur.line), cm.lastLine() ); - var text = cm.getLine(line); - var maxCh = text.length - 1 + Number(!!includeLineBreak); - var ch = Math.min(Math.max(0, cur.ch), maxCh); - // prevent cursor from entering surrogate pair - var charCode = text.charCodeAt(ch); - if (0xDC00 <= charCode && charCode <= 0xDFFF) { - var direction = 1; - if (oldCur && oldCur.line == line && oldCur.ch > ch) { - direction = -1; - } - ch +=direction; - if (ch > maxCh) ch -=2; - } - return new Pos(line, ch); - } - function copyArgs(args) { - var ret = {}; - for (var prop in args) { - if (args.hasOwnProperty(prop)) { - ret[prop] = args[prop]; - } - } - return ret; - } - function offsetCursor(cur, offsetLine, offsetCh) { - if (typeof offsetLine === 'object') { - offsetCh = offsetLine.ch; - offsetLine = offsetLine.line; - } - return new Pos(cur.line + offsetLine, cur.ch + offsetCh); - } - function commandMatches(keys, keyMap, context, inputState) { - // Partial matches are not applied. They inform the key handler - // that the current key sequence is a subsequence of a valid key - // sequence, so that the key buffer is not cleared. - if (inputState.operator) context = "operatorPending"; - var match, partial = [], full = []; - // if currently expanded key comes from a noremap, searcg only in default keys - var startIndex = noremap ? keyMap.length - defaultKeymapLength : 0; - for (var i = startIndex; i < keyMap.length; i++) { - var command = keyMap[i]; - if (context == 'insert' && command.context != 'insert' || - (command.context && command.context != context) || - inputState.operator && command.type == 'action' || - !(match = commandMatch(keys, command.keys))) { continue; } - if (match == 'partial') { partial.push(command); } - if (match == 'full') { full.push(command); } + /* + * Below are miscellaneous utility functions used by vim.js + */ + + /** + * Clips cursor to ensure that line is within the buffer's range + * and is not inside surrogate pair + * If includeLineBreak is true, then allow cur.ch == lineLength. + * @arg {CodeMirrorV} cm + * @arg {Pos} cur + * @arg {Pos} [oldCur] + * @return {Pos} + */ + function clipCursorToContent(cm, cur, oldCur) { + var vim = cm.state.vim; + var includeLineBreak = vim.insertMode || vim.visualMode; + var line = Math.min(Math.max(cm.firstLine(), cur.line), cm.lastLine() ); + var text = cm.getLine(line); + var maxCh = text.length - 1 + Number(!!includeLineBreak); + var ch = Math.min(Math.max(0, cur.ch), maxCh); + // prevent cursor from entering surrogate pair + var charCode = text.charCodeAt(ch); + if (0xDC00 <= charCode && charCode <= 0xDFFF) { + var direction = 1; + if (oldCur && oldCur.line == line && oldCur.ch > ch) { + direction = -1; + } + ch +=direction; + if (ch > maxCh) ch -=2; + } + return new Pos(line, ch); + } + function copyArgs(args) { + var ret = {}; + for (var prop in args) { + if (args.hasOwnProperty(prop)) { + ret[prop] = args[prop]; + } + } + return ret; + } + function offsetCursor(cur, offsetLine, offsetCh) { + if (typeof offsetLine === 'object') { + offsetCh = offsetLine.ch; + offsetLine = offsetLine.line; + } + return new Pos(cur.line + offsetLine, cur.ch + offsetCh); + } + function commandMatches(keys, keyMap, context, inputState) { + // Partial matches are not applied. They inform the key handler + // that the current key sequence is a subsequence of a valid key + // sequence, so that the key buffer is not cleared. + if (inputState.operator) context = "operatorPending"; + var match, partial = [], full = []; + // if currently expanded key comes from a noremap, searcg only in default keys + var startIndex = noremap ? keyMap.length - defaultKeymapLength : 0; + for (var i = startIndex; i < keyMap.length; i++) { + var command = keyMap[i]; + if (context == 'insert' && command.context != 'insert' || + (command.context && command.context != context) || + inputState.operator && command.type == 'action' || + !(match = commandMatch(keys, command.keys))) { continue; } + if (match == 'partial') { partial.push(command); } + if (match == 'full') { full.push(command); } + } + return { + partial: partial.length && partial, + full: full.length && full + }; + } + /** @arg {string} pressed @arg {string} mapped @return {'full'|'partial'|false}*/ + function commandMatch(pressed, mapped) { + const isLastCharacter = mapped.slice(-11) == ''; + const isLastRegister = mapped.slice(-10) == ''; + if (isLastCharacter || isLastRegister) { + // Last character matches anything. + var prefixLen = mapped.length - (isLastCharacter ? 11 : 10); + var pressedPrefix = pressed.slice(0, prefixLen); + var mappedPrefix = mapped.slice(0, prefixLen); + return pressedPrefix == mappedPrefix && pressed.length > prefixLen ? 'full' : + mappedPrefix.indexOf(pressedPrefix) == 0 ? 'partial' : false; + } else { + return pressed == mapped ? 'full' : + mapped.indexOf(pressed) == 0 ? 'partial' : false; + } + } + /** @arg {string} keys */ + function lastChar(keys) { + var match = /^.*(<[^>]+>)$/.exec(keys); + var selectedCharacter = match ? match[1] : keys.slice(-1); + if (selectedCharacter.length > 1){ + switch(selectedCharacter){ + case '': + selectedCharacter='\n'; + break; + case '': + selectedCharacter=' '; + break; + default: + selectedCharacter=''; + break; } - return { - partial: partial.length && partial, - full: full.length && full - }; } - function commandMatch(pressed, mapped) { - const isLastCharacter = mapped.slice(-11) == ''; - const isLastRegister = mapped.slice(-10) == ''; - if (isLastCharacter || isLastRegister) { - // Last character matches anything. - var prefixLen = mapped.length - (isLastCharacter ? 11 : 10); - var pressedPrefix = pressed.slice(0, prefixLen); - var mappedPrefix = mapped.slice(0, prefixLen); - return pressedPrefix == mappedPrefix && pressed.length > prefixLen ? 'full' : - mappedPrefix.indexOf(pressedPrefix) == 0 ? 'partial' : false; - } else { - return pressed == mapped ? 'full' : - mapped.indexOf(pressed) == 0 ? 'partial' : false; + return selectedCharacter; + } + /** @arg {CodeMirror} cm @arg {{ (cm: CodeMirror): void }} fn @arg {number} repeat */ + function repeatFn(cm, fn, repeat) { + return function() { + for (var i = 0; i < repeat; i++) { + fn(cm); } + }; + } + /** @arg {Pos} cur @return {Pos}*/ + function copyCursor(cur) { + return new Pos(cur.line, cur.ch); + } + /** @arg {Pos} cur1 @arg {Pos} cur2 @return {boolean} */ + function cursorEqual(cur1, cur2) { + return cur1.ch == cur2.ch && cur1.line == cur2.line; + } + /** @arg {Pos} cur1 @arg {Pos} cur2 @return {boolean}*/ + function cursorIsBefore(cur1, cur2) { + if (cur1.line < cur2.line) { + return true; } - function lastChar(keys) { - var match = /^.*(<[^>]+>)$/.exec(keys); - var selectedCharacter = match ? match[1] : keys.slice(-1); - if (selectedCharacter.length > 1){ - switch(selectedCharacter){ - case '': - selectedCharacter='\n'; - break; - case '': - selectedCharacter=' '; - break; - default: - selectedCharacter=''; - break; - } - } - return selectedCharacter; + if (cur1.line == cur2.line && cur1.ch < cur2.ch) { + return true; } - function repeatFn(cm, fn, repeat) { - return function() { - for (var i = 0; i < repeat; i++) { - fn(cm); - } - }; + return false; + } + /** @arg {Pos} cur1 @arg {Pos} cur2 @return {Pos}*/ + function cursorMin(cur1, cur2) { + if (arguments.length > 2) { + // @ts-ignore + cur2 = cursorMin.apply(undefined, Array.prototype.slice.call(arguments, 1)); } - function copyCursor(cur) { - return new Pos(cur.line, cur.ch); + return cursorIsBefore(cur1, cur2) ? cur1 : cur2; + } + /** @arg {Pos} cur1 @arg {Pos} cur2 @return {Pos} */ + function cursorMax(cur1, cur2) { + if (arguments.length > 2) { + // @ts-ignore + cur2 = cursorMax.apply(undefined, Array.prototype.slice.call(arguments, 1)); } - function cursorEqual(cur1, cur2) { - return cur1.ch == cur2.ch && cur1.line == cur2.line; + return cursorIsBefore(cur1, cur2) ? cur2 : cur1; + } + /** @arg {Pos} cur1 @arg {Pos} cur2 @arg {Pos} cur3 @return {boolean}*/ + function cursorIsBetween(cur1, cur2, cur3) { + // returns true if cur2 is between cur1 and cur3. + var cur1before2 = cursorIsBefore(cur1, cur2); + var cur2before3 = cursorIsBefore(cur2, cur3); + return cur1before2 && cur2before3; + } + /** @arg {CodeMirror} cm @arg {number} lineNum */ + function lineLength(cm, lineNum) { + return cm.getLine(lineNum).length; + } + /** @arg {string} s */ + function trim(s) { + if (s.trim) { + return s.trim(); } - function cursorIsBefore(cur1, cur2) { - if (cur1.line < cur2.line) { - return true; - } - if (cur1.line == cur2.line && cur1.ch < cur2.ch) { - return true; - } - return false; - } - function cursorMin(cur1, cur2) { - if (arguments.length > 2) { - cur2 = cursorMin.apply(undefined, Array.prototype.slice.call(arguments, 1)); - } - return cursorIsBefore(cur1, cur2) ? cur1 : cur2; - } - function cursorMax(cur1, cur2) { - if (arguments.length > 2) { - cur2 = cursorMax.apply(undefined, Array.prototype.slice.call(arguments, 1)); - } - return cursorIsBefore(cur1, cur2) ? cur2 : cur1; - } - function cursorIsBetween(cur1, cur2, cur3) { - // returns true if cur2 is between cur1 and cur3. - var cur1before2 = cursorIsBefore(cur1, cur2); - var cur2before3 = cursorIsBefore(cur2, cur3); - return cur1before2 && cur2before3; - } - function lineLength(cm, lineNum) { - return cm.getLine(lineNum).length; - } - function trim(s) { - if (s.trim) { - return s.trim(); - } - return s.replace(/^\s+|\s+$/g, ''); - } - function escapeRegex(s) { - return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, '\\$1'); - } - function extendLineToColumn(cm, lineNum, column) { - var endCh = lineLength(cm, lineNum); - var spaces = new Array(column-endCh+1).join(' '); - cm.setCursor(new Pos(lineNum, endCh)); - cm.replaceRange(spaces, cm.getCursor()); - } - // This functions selects a rectangular block - // of text with selectionEnd as any of its corner - // Height of block: - // Difference in selectionEnd.line and first/last selection.line - // Width of the block: - // Distance between selectionEnd.ch and any(first considered here) selection.ch - function selectBlock(cm, selectionEnd) { - var selections = [], ranges = cm.listSelections(); - var head = copyCursor(cm.clipPos(selectionEnd)); - var isClipped = !cursorEqual(selectionEnd, head); - var curHead = cm.getCursor('head'); - var primIndex = getIndex(ranges, curHead); - var wasClipped = cursorEqual(ranges[primIndex].head, ranges[primIndex].anchor); - var max = ranges.length - 1; - var index = max - primIndex > primIndex ? max : 0; - var base = ranges[index].anchor; - - var firstLine = Math.min(base.line, head.line); - var lastLine = Math.max(base.line, head.line); - var baseCh = base.ch, headCh = head.ch; - - var dir = ranges[index].head.ch - baseCh; - var newDir = headCh - baseCh; - if (dir > 0 && newDir <= 0) { - baseCh++; - if (!isClipped) { headCh--; } - } else if (dir < 0 && newDir >= 0) { - baseCh--; - if (!wasClipped) { headCh++; } - } else if (dir < 0 && newDir == -1) { - baseCh--; - headCh++; - } - for (var line = firstLine; line <= lastLine; line++) { - var range = {anchor: new Pos(line, baseCh), head: new Pos(line, headCh)}; - selections.push(range); - } - cm.setSelections(selections); - selectionEnd.ch = headCh; - base.ch = baseCh; - return base; - } - function selectForInsert(cm, head, height) { - var sel = []; - for (var i = 0; i < height; i++) { - var lineHead = offsetCursor(head, i, 0); - sel.push({anchor: lineHead, head: lineHead}); - } - cm.setSelections(sel, 0); + return s.replace(/^\s+|\s+$/g, ''); + } + /** @arg {string} s */ + function escapeRegex(s) { + return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, '\\$1'); + } + /** @arg {CodeMirror} cm @arg {number} lineNum @arg {number} column */ + function extendLineToColumn(cm, lineNum, column) { + var endCh = lineLength(cm, lineNum); + var spaces = new Array(column-endCh+1).join(' '); + cm.setCursor(new Pos(lineNum, endCh)); + cm.replaceRange(spaces, cm.getCursor()); + } + // This functions selects a rectangular block + // of text with selectionEnd as any of its corner + // Height of block: + // Difference in selectionEnd.line and first/last selection.line + // Width of the block: + // Distance between selectionEnd.ch and any(first considered here) selection.ch + /** @arg {CodeMirror} cm @arg {Pos} selectionEnd */ + function selectBlock(cm, selectionEnd) { + var selections = [], ranges = cm.listSelections(); + var head = copyCursor(cm.clipPos(selectionEnd)); + var isClipped = !cursorEqual(selectionEnd, head); + var curHead = cm.getCursor('head'); + var primIndex = getIndex(ranges, curHead); + var wasClipped = cursorEqual(ranges[primIndex].head, ranges[primIndex].anchor); + var max = ranges.length - 1; + var index = max - primIndex > primIndex ? max : 0; + var base = ranges[index].anchor; + + var firstLine = Math.min(base.line, head.line); + var lastLine = Math.max(base.line, head.line); + var baseCh = base.ch, headCh = head.ch; + + var dir = ranges[index].head.ch - baseCh; + var newDir = headCh - baseCh; + if (dir > 0 && newDir <= 0) { + baseCh++; + if (!isClipped) { headCh--; } + } else if (dir < 0 && newDir >= 0) { + baseCh--; + if (!wasClipped) { headCh++; } + } else if (dir < 0 && newDir == -1) { + baseCh--; + headCh++; } - // getIndex returns the index of the cursor in the selections. - function getIndex(ranges, cursor, end) { - for (var i = 0; i < ranges.length; i++) { - var atAnchor = end != 'head' && cursorEqual(ranges[i].anchor, cursor); - var atHead = end != 'anchor' && cursorEqual(ranges[i].head, cursor); - if (atAnchor || atHead) { - return i; - } - } - return -1; + for (var line = firstLine; line <= lastLine; line++) { + var range = {anchor: new Pos(line, baseCh), head: new Pos(line, headCh)}; + selections.push(range); } - function getSelectedAreaRange(cm, vim) { - var lastSelection = vim.lastSelection; - var getCurrentSelectedAreaRange = function() { - var selections = cm.listSelections(); - var start = selections[0]; - var end = selections[selections.length-1]; - var selectionStart = cursorIsBefore(start.anchor, start.head) ? start.anchor : start.head; - var selectionEnd = cursorIsBefore(end.anchor, end.head) ? end.head : end.anchor; - return [selectionStart, selectionEnd]; - }; - var getLastSelectedAreaRange = function() { - var selectionStart = cm.getCursor(); - var selectionEnd = cm.getCursor(); - var block = lastSelection.visualBlock; - if (block) { - var width = block.width; - var height = block.height; - selectionEnd = new Pos(selectionStart.line + height, selectionStart.ch + width); - var selections = []; - // selectBlock creates a 'proper' rectangular block. - // We do not want that in all cases, so we manually set selections. - for (var i = selectionStart.line; i < selectionEnd.line; i++) { - var anchor = new Pos(i, selectionStart.ch); - var head = new Pos(i, selectionEnd.ch); - var range = {anchor: anchor, head: head}; - selections.push(range); - } - cm.setSelections(selections); - } else { - var start = lastSelection.anchorMark.find(); - var end = lastSelection.headMark.find(); - var line = end.line - start.line; - var ch = end.ch - start.ch; - selectionEnd = {line: selectionEnd.line + line, ch: line ? selectionEnd.ch : ch + selectionEnd.ch}; - if (lastSelection.visualLine) { - selectionStart = new Pos(selectionStart.line, 0); - selectionEnd = new Pos(selectionEnd.line, lineLength(cm, selectionEnd.line)); - } - cm.setSelection(selectionStart, selectionEnd); - } - return [selectionStart, selectionEnd]; - }; - if (!vim.visualMode) { - // In case of replaying the action. - return getLastSelectedAreaRange(); - } else { - return getCurrentSelectedAreaRange(); + cm.setSelections(selections); + selectionEnd.ch = headCh; + base.ch = baseCh; + return base; + } + /** @arg {CodeMirror} cm @arg {any} head @arg {number} height */ + function selectForInsert(cm, head, height) { + var sel = []; + for (var i = 0; i < height; i++) { + var lineHead = offsetCursor(head, i, 0); + sel.push({anchor: lineHead, head: lineHead}); + } + cm.setSelections(sel, 0); + } + // getIndex returns the index of the cursor in the selections. + /** @arg {string | any[]} ranges @arg {any} cursor @arg {string | undefined} [end] */ + function getIndex(ranges, cursor, end) { + for (var i = 0; i < ranges.length; i++) { + var atAnchor = end != 'head' && cursorEqual(ranges[i].anchor, cursor); + var atHead = end != 'anchor' && cursorEqual(ranges[i].head, cursor); + if (atAnchor || atHead) { + return i; } } - // Updates the previous selection with the current selection's values. This - // should only be called in visual mode. - function updateLastSelection(cm, vim) { - var anchor = vim.sel.anchor; - var head = vim.sel.head; - // To accommodate the effect of lastPastedText in the last selection - if (vim.lastPastedText) { - head = cm.posFromIndex(cm.indexFromPos(anchor) + vim.lastPastedText.length); - vim.lastPastedText = null; - } - vim.lastSelection = {'anchorMark': cm.setBookmark(anchor), - 'headMark': cm.setBookmark(head), - 'anchor': copyCursor(anchor), - 'head': copyCursor(head), - 'visualMode': vim.visualMode, - 'visualLine': vim.visualLine, - 'visualBlock': vim.visualBlock}; - } - function expandSelection(cm, start, end, move) { - var sel = cm.state.vim.sel; - var head = move ? start: sel.head; - var anchor = move ? start: sel.anchor; - var tmp; - if (cursorIsBefore(end, start)) { - tmp = end; - end = start; - start = tmp; - } - if (cursorIsBefore(head, anchor)) { - head = cursorMin(start, head); - anchor = cursorMax(anchor, end); + return -1; + } + /** @arg {CodeMirror} cm @arg {vimState} vim */ + function getSelectedAreaRange(cm, vim) { + var lastSelection = vim.lastSelection; + /** @return {[Pos,Pos]} */ + var getCurrentSelectedAreaRange = function() { + var selections = cm.listSelections(); + var start = selections[0]; + var end = selections[selections.length-1]; + var selectionStart = cursorIsBefore(start.anchor, start.head) ? start.anchor : start.head; + var selectionEnd = cursorIsBefore(end.anchor, end.head) ? end.head : end.anchor; + return [selectionStart, selectionEnd]; + }; + var getLastSelectedAreaRange = function() { + var selectionStart = cm.getCursor(); + var selectionEnd = cm.getCursor(); + var block = lastSelection.visualBlock; + if (block) { + var width = block.width; + var height = block.height; + selectionEnd = new Pos(selectionStart.line + height, selectionStart.ch + width); + var selections = []; + // selectBlock creates a 'proper' rectangular block. + // We do not want that in all cases, so we manually set selections. + for (var i = selectionStart.line; i < selectionEnd.line; i++) { + var anchor = new Pos(i, selectionStart.ch); + var head = new Pos(i, selectionEnd.ch); + var range = {anchor: anchor, head: head}; + selections.push(range); + } + cm.setSelections(selections); } else { - anchor = cursorMin(start, anchor); - head = cursorMax(head, end); - head = offsetCursor(head, 0, -1); - if (head.ch == -1 && head.line != cm.firstLine()) { - head = new Pos(head.line - 1, lineLength(cm, head.line - 1)); - } - } - return [anchor, head]; + var start = lastSelection.anchorMark.find(); + var end = lastSelection.headMark.find(); + var line = end.line - start.line; + var ch = end.ch - start.ch; + selectionEnd = {line: selectionEnd.line + line, ch: line ? selectionEnd.ch : ch + selectionEnd.ch}; + if (lastSelection.visualLine) { + selectionStart = new Pos(selectionStart.line, 0); + selectionEnd = new Pos(selectionEnd.line, lineLength(cm, selectionEnd.line)); + } + cm.setSelection(selectionStart, selectionEnd); + } + return [selectionStart, selectionEnd]; + }; + if (!vim.visualMode) { + // In case of replaying the action. + return getLastSelectedAreaRange(); + } else { + return getCurrentSelectedAreaRange(); } - /** - * Updates the CodeMirror selection to match the provided vim selection. - * If no arguments are given, it uses the current vim selection state. - */ - function updateCmSelection(cm, sel, mode) { - var vim = cm.state.vim; - sel = sel || vim.sel; - var mode = mode || - vim.visualLine ? 'line' : vim.visualBlock ? 'block' : 'char'; - var cmSel = makeCmSelection(cm, sel, mode); - cm.setSelections(cmSel.ranges, cmSel.primary); - } - function makeCmSelection(cm, sel, mode, exclusive) { - var head = copyCursor(sel.head); - var anchor = copyCursor(sel.anchor); - if (mode == 'char') { - var headOffset = !exclusive && !cursorIsBefore(sel.head, sel.anchor) ? 1 : 0; - var anchorOffset = cursorIsBefore(sel.head, sel.anchor) ? 1 : 0; - head = offsetCursor(sel.head, 0, headOffset); - anchor = offsetCursor(sel.anchor, 0, anchorOffset); - return { - ranges: [{anchor: anchor, head: head}], - primary: 0 - }; - } else if (mode == 'line') { - if (!cursorIsBefore(sel.head, sel.anchor)) { - anchor.ch = 0; - - var lastLine = cm.lastLine(); - if (head.line > lastLine) { - head.line = lastLine; - } - head.ch = lineLength(cm, head.line); - } else { - head.ch = 0; - anchor.ch = lineLength(cm, anchor.line); - } - return { - ranges: [{anchor: anchor, head: head}], - primary: 0 - }; - } else if (mode == 'block') { - var top = Math.min(anchor.line, head.line), - fromCh = anchor.ch, - bottom = Math.max(anchor.line, head.line), - toCh = head.ch; - if (fromCh < toCh) { toCh += 1 } - else { fromCh += 1 }; - var height = bottom - top + 1; - var primary = head.line == top ? 0 : height - 1; - var ranges = []; - for (var i = 0; i < height; i++) { - ranges.push({ - anchor: new Pos(top + i, fromCh), - head: new Pos(top + i, toCh) - }); - } - return { - ranges: ranges, - primary: primary - }; - } + } + // Updates the previous selection with the current selection's values. This + // should only be called in visual mode. + /** @arg {CodeMirror} cm @arg {vimState} vim */ + function updateLastSelection(cm, vim) { + var anchor = vim.sel.anchor; + var head = vim.sel.head; + // To accommodate the effect of lastPastedText in the last selection + if (vim.lastPastedText) { + head = cm.posFromIndex(cm.indexFromPos(anchor) + vim.lastPastedText.length); + vim.lastPastedText = null; + } + vim.lastSelection = {'anchorMark': cm.setBookmark(anchor), + 'headMark': cm.setBookmark(head), + 'anchor': copyCursor(anchor), + 'head': copyCursor(head), + 'visualMode': vim.visualMode, + 'visualLine': vim.visualLine, + 'visualBlock': vim.visualBlock}; + } + /** @arg {CodeMirrorV} cm @arg {Pos} start @arg {Pos} end @returns {[Pos, Pos]} */ + function expandSelection(cm, start, end, move) { + var sel = cm.state.vim.sel; + var head = move ? start: sel.head; + var anchor = move ? start: sel.anchor; + var tmp; + if (cursorIsBefore(end, start)) { + tmp = end; + end = start; + start = tmp; } - function getHead(cm) { - var cur = cm.getCursor('head'); - if (cm.getSelection().length == 1) { - // Small corner case when only 1 character is selected. The "real" - // head is the left of head and anchor. - cur = cursorMin(cur, cm.getCursor('anchor')); + if (cursorIsBefore(head, anchor)) { + head = cursorMin(start, head); + anchor = cursorMax(anchor, end); + } else { + anchor = cursorMin(start, anchor); + head = cursorMax(head, end); + head = offsetCursor(head, 0, -1); + if (head.ch == -1 && head.line != cm.firstLine()) { + head = new Pos(head.line - 1, lineLength(cm, head.line - 1)); } - return cur; } + return [anchor, head]; + } + /** + * Updates the CodeMirror selection to match the provided vim selection. + * If no arguments are given, it uses the current vim selection state. + * @arg {CodeMirrorV} cm + * @arg {vimState["sel"]} [sel] + * @arg {"char"|"line"|"block" | undefined} [mode] + */ + function updateCmSelection(cm, sel, mode) { + var vim = cm.state.vim; + sel = sel || vim.sel; + if (!mode) { + mode = vim.visualLine ? 'line' : vim.visualBlock ? 'block' : 'char'; + } + var cmSel = makeCmSelection(cm, sel, mode); + cm.setSelections(cmSel.ranges, cmSel.primary); + } + /** + * @arg {CodeMirror} cm + * @arg {import("./types").CM5RangeInterface} sel + * @arg {"char"|"line"|"block"} mode + * @arg {boolean|undefined} [exclusive] + * @return {{ranges: any, primary: number}} + */ + function makeCmSelection(cm, sel, mode, exclusive) { + var head = copyCursor(sel.head); + var anchor = copyCursor(sel.anchor); + if (mode == 'char') { + var headOffset = !exclusive && !cursorIsBefore(sel.head, sel.anchor) ? 1 : 0; + var anchorOffset = cursorIsBefore(sel.head, sel.anchor) ? 1 : 0; + head = offsetCursor(sel.head, 0, headOffset); + anchor = offsetCursor(sel.anchor, 0, anchorOffset); + return { + ranges: [{anchor: anchor, head: head}], + primary: 0 + }; + } else if (mode == 'line') { + if (!cursorIsBefore(sel.head, sel.anchor)) { + anchor.ch = 0; - /** - * If moveHead is set to false, the CodeMirror selection will not be - * touched. The caller assumes the responsibility of putting the cursor - * in the right place. - */ - function exitVisualMode(cm, moveHead) { - var vim = cm.state.vim; - if (moveHead !== false) { - cm.setCursor(clipCursorToContent(cm, vim.sel.head)); - } - updateLastSelection(cm, vim); - vim.visualMode = false; - vim.visualLine = false; - vim.visualBlock = false; - if (!vim.insertMode) CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); - } - - // Remove any trailing newlines from the selection. For - // example, with the caret at the start of the last word on the line, - // 'dw' should word, but not the newline, while 'w' should advance the - // caret to the first character of the next line. - function clipToLine(cm, curStart, curEnd) { - var selection = cm.getRange(curStart, curEnd); - // Only clip if the selection ends with trailing newline + whitespace - if (/\n\s*$/.test(selection)) { - var lines = selection.split('\n'); - // We know this is all whitespace. - lines.pop(); - - // Cases: - // 1. Last word is an empty line - do not clip the trailing '\n' - // 2. Last word is not an empty line - clip the trailing '\n' - var line; - // Find the line containing the last word, and clip all whitespace up - // to it. - for (var line = lines.pop(); lines.length > 0 && line && isWhiteSpaceString(line); line = lines.pop()) { - curEnd.line--; - curEnd.ch = 0; - } - // If the last word is not an empty line, clip an additional newline - if (line) { - curEnd.line--; - curEnd.ch = lineLength(cm, curEnd.line); - } else { - curEnd.ch = 0; + var lastLine = cm.lastLine(); + if (head.line > lastLine) { + head.line = lastLine; } + head.ch = lineLength(cm, head.line); + } else { + head.ch = 0; + anchor.ch = lineLength(cm, anchor.line); + } + return { + ranges: [{anchor: anchor, head: head}], + primary: 0 + }; + } else if (mode == 'block') { + var top = Math.min(anchor.line, head.line), + fromCh = anchor.ch, + bottom = Math.max(anchor.line, head.line), + toCh = head.ch; + if (fromCh < toCh) { toCh += 1 } + else { fromCh += 1 }; + var height = bottom - top + 1; + var primary = head.line == top ? 0 : height - 1; + var ranges = []; + for (var i = 0; i < height; i++) { + ranges.push({ + anchor: new Pos(top + i, fromCh), + head: new Pos(top + i, toCh) + }); } + return { + ranges: ranges, + primary: primary + }; + } + throw "never happens"; + } + /** @arg {CodeMirror} cm */ + function getHead(cm) { + var cur = cm.getCursor('head'); + if (cm.getSelection().length == 1) { + // Small corner case when only 1 character is selected. The "real" + // head is the left of head and anchor. + cur = cursorMin(cur, cm.getCursor('anchor')); } + return cur; + } - // Expand the selection to line ends. - function expandSelectionToLine(_cm, curStart, curEnd) { - curStart.ch = 0; - curEnd.ch = 0; - curEnd.line++; + /** + * If moveHead is set to false, the CodeMirror selection will not be + * touched. The caller assumes the responsibility of putting the cursor + * in the right place. + * @arg {CodeMirrorV} cm + * @arg {boolean} [moveHead] + */ + function exitVisualMode(cm, moveHead) { + var vim = cm.state.vim; + if (moveHead !== false) { + cm.setCursor(clipCursorToContent(cm, vim.sel.head)); } + updateLastSelection(cm, vim); + vim.visualMode = false; + vim.visualLine = false; + vim.visualBlock = false; + if (!vim.insertMode) CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); + } - function findFirstNonWhiteSpaceCharacter(text) { - if (!text) { - return 0; + /** + * Remove any trailing newlines from the selection. For + * example, with the caret at the start of the last word on the line, + * 'dw' should word, but not the newline, while 'w' should advance the + * caret to the first character of the next line. + * @arg {CodeMirror} cm + * @arg {Pos} curStart + * @arg {Pos} curEnd + */ + function clipToLine(cm, curStart, curEnd) { + var selection = cm.getRange(curStart, curEnd); + // Only clip if the selection ends with trailing newline + whitespace + if (/\n\s*$/.test(selection)) { + var lines = selection.split('\n'); + // We know this is all whitespace. + lines.pop(); + + // Cases: + // 1. Last word is an empty line - do not clip the trailing '\n' + // 2. Last word is not an empty line - clip the trailing '\n' + // Find the line containing the last word, and clip all whitespace up + // to it. + for (var line = lines.pop(); lines.length > 0 && line && isWhiteSpaceString(line); line = lines.pop()) { + curEnd.line--; + curEnd.ch = 0; + } + // If the last word is not an empty line, clip an additional newline + if (line) { + curEnd.line--; + curEnd.ch = lineLength(cm, curEnd.line); + } else { + curEnd.ch = 0; } - var firstNonWS = text.search(/\S/); - return firstNonWS == -1 ? text.length : firstNonWS; } + } - function expandWordUnderCursor(cm, {inclusive, innerWord, bigWord, noSymbol, multiline}, cursor) { - var cur = cursor || getHead(cm); - var line = cm.getLine(cur.line); - var endLine = line; - var startLineNumber = cur.line - var endLineNumber = startLineNumber; - var idx = cur.ch; - - var wordOnNextLine; - // Seek to first word or non-whitespace character, depending on if - // noSymbol is true. - var test = noSymbol ? wordCharTest[0] : bigWordCharTest [0]; - if (innerWord && /\s/.test(line.charAt(idx))) { - test = function(ch) { return /\s/.test(ch); }; - } else { - while (!test(line.charAt(idx))) { - idx++; - if (idx >= line.length) { - if (!multiline) return null; - idx--; - wordOnNextLine = findWord(cm, cur, true, bigWord, true); - break - } - } + // Expand the selection to line ends. + /** @arg {CodeMirror} _cm @arg {Pos} curStart @arg {Pos} curEnd */ + function expandSelectionToLine(_cm, curStart, curEnd) { + curStart.ch = 0; + curEnd.ch = 0; + curEnd.line++; + } - if (bigWord) { - test = bigWordCharTest[0]; - } else { - test = wordCharTest[0]; - if (!test(line.charAt(idx))) { - test = wordCharTest[1]; - } - } - } + /** @arg {string} [text] */ + function findFirstNonWhiteSpaceCharacter(text) { + if (!text) { + return 0; + } + var firstNonWS = text.search(/\S/); + return firstNonWS == -1 ? text.length : firstNonWS; + } - var end = idx, start = idx; - while (test(line.charAt(start)) && start >= 0) { start--; } - start++; - if (wordOnNextLine) { - end = wordOnNextLine.to; - endLineNumber = wordOnNextLine.line; - endLine = cm.getLine(endLineNumber); - if (!endLine && end == 0) end++; + /** + * @arg {CodeMirror} cm + * @arg {{inclusive?: boolean, innerWord?: boolean, bigWord?: boolean, noSymbol?: boolean, multiline?: boolean}} options + * @arg {Pos} [cursor] + **/ + function expandWordUnderCursor(cm, {inclusive, innerWord, bigWord, noSymbol, multiline}, cursor) { + var cur = cursor || getHead(cm); + var line = cm.getLine(cur.line); + var endLine = line; + var startLineNumber = cur.line + var endLineNumber = startLineNumber; + var idx = cur.ch; + + var wordOnNextLine; + // Seek to first word or non-whitespace character, depending on if + // noSymbol is true. + var test = noSymbol ? wordCharTest[0] : bigWordCharTest [0]; + if (innerWord && /\s/.test(line.charAt(idx))) { + test = function(/** @type {string} */ ch) { return /\s/.test(ch); }; + } else { + while (!test(line.charAt(idx))) { + idx++; + if (idx >= line.length) { + if (!multiline) return null; + idx--; + wordOnNextLine = findWord(cm, cur, true, bigWord, true); + break + } + } + + if (bigWord) { + test = bigWordCharTest[0]; } else { - while (test(line.charAt(end)) && end < line.length) { end++; } - } - - if (inclusive) { - // If present, include all whitespace after word. - // Otherwise, include all whitespace before word, except indentation. - var wordEnd = end; - var startsWithSpace = cur.ch <= start && /\s/.test(line.charAt(cur.ch)); - if (!startsWithSpace) { - while (/\s/.test(endLine.charAt(end)) && end < endLine.length) { end++; } - } - if (wordEnd == end || startsWithSpace) { - var wordStart = start; - while (/\s/.test(line.charAt(start - 1)) && start > 0) { start--; } - if (!start && !startsWithSpace) { start = wordStart; } + test = wordCharTest[0]; + if (!test(line.charAt(idx))) { + test = wordCharTest[1]; } } + } - return { start: new Pos(startLineNumber, start), end: new Pos(endLineNumber, end) }; + var end = idx, start = idx; + while (test(line.charAt(start)) && start >= 0) { start--; } + start++; + if (wordOnNextLine) { + end = wordOnNextLine.to; + endLineNumber = wordOnNextLine.line; + endLine = cm.getLine(endLineNumber); + if (!endLine && end == 0) end++; + } else { + while (test(line.charAt(end)) && end < line.length) { end++; } } - /** - * Depends on the following: - * - * - editor mode should be htmlmixedmode / xml - * - mode/xml/xml.js should be loaded - * - addon/fold/xml-fold.js should be loaded - * - * If any of the above requirements are not true, this function noops. - * - * This is _NOT_ a 100% accurate implementation of vim tag text objects. - * The following caveats apply (based off cursory testing, I'm sure there - * are other discrepancies): - * - * - Does not work inside comments: - * ``` - * - * ``` - * - Does not work when tags have different cases: - * ``` - *
broken
- * ``` - * - Does not work when cursor is inside a broken tag: - * ``` - *
- * ``` - */ - function expandTagUnderCursor(cm, head, inclusive) { - var cur = head; - if (!CodeMirror.findMatchingTag || !CodeMirror.findEnclosingTag) { - return { start: cur, end: cur }; + if (inclusive) { + // If present, include all whitespace after word. + // Otherwise, include all whitespace before word, except indentation. + var wordEnd = end; + var startsWithSpace = cur.ch <= start && /\s/.test(line.charAt(cur.ch)); + if (!startsWithSpace) { + while (/\s/.test(endLine.charAt(end)) && end < endLine.length) { end++; } } - - var tags = CodeMirror.findMatchingTag(cm, head) || CodeMirror.findEnclosingTag(cm, head); - if (!tags || !tags.open || !tags.close) { - return { start: cur, end: cur }; + if (wordEnd == end || startsWithSpace) { + var wordStart = start; + while (/\s/.test(line.charAt(start - 1)) && start > 0) { start--; } + if (!start && !startsWithSpace) { start = wordStart; } } + } - if (inclusive) { - return { start: tags.open.from, end: tags.close.to }; - } - return { start: tags.open.to, end: tags.close.from }; + return { start: new Pos(startLineNumber, start), end: new Pos(endLineNumber, end) }; + } + + /** + * Depends on the following: + * + * - editor mode should be htmlmixedmode / xml + * - mode/xml/xml.js should be loaded + * - addon/fold/xml-fold.js should be loaded + * + * If any of the above requirements are not true, this function noops. + * + * This is _NOT_ a 100% accurate implementation of vim tag text objects. + * The following caveats apply (based off cursory testing, I'm sure there + * are other discrepancies): + * + * - Does not work inside comments: + * ``` + * + * ``` + * - Does not work when tags have different cases: + * ``` + *
broken
+ * ``` + * - Does not work when cursor is inside a broken tag: + * ``` + *
+ * ``` + * @arg {CodeMirror} cm + * @arg {Pos} head + * @arg {boolean} [inclusive] + */ + function expandTagUnderCursor(cm, head, inclusive) { + var cur = head; + if (!CodeMirror.findMatchingTag || !CodeMirror.findEnclosingTag) { + return { start: cur, end: cur }; } - function recordJumpPosition(cm, oldCur, newCur) { - if (!cursorEqual(oldCur, newCur)) { - vimGlobalState.jumpList.add(cm, oldCur, newCur); - } + var tags = CodeMirror.findMatchingTag(cm, head) || CodeMirror.findEnclosingTag(cm, head); + if (!tags || !tags.open || !tags.close) { + return { start: cur, end: cur }; } - function recordLastCharacterSearch(increment, args) { - vimGlobalState.lastCharacterSearch.increment = increment; - vimGlobalState.lastCharacterSearch.forward = args.forward; - vimGlobalState.lastCharacterSearch.selectedCharacter = args.selectedCharacter; + if (inclusive) { + return { start: tags.open.from, end: tags.close.to }; } + return { start: tags.open.to, end: tags.close.from }; + } - var symbolToMode = { - '(': 'bracket', ')': 'bracket', '{': 'bracket', '}': 'bracket', - '[': 'section', ']': 'section', - '*': 'comment', '/': 'comment', - 'm': 'method', 'M': 'method', - '#': 'preprocess' - }; - var findSymbolModes = { - bracket: { - isComplete: function(state) { - if (state.nextCh === state.symb) { + /** @arg {CodeMirror} cm @arg {Pos} oldCur @arg {Pos} newCur */ + function recordJumpPosition(cm, oldCur, newCur) { + if (!cursorEqual(oldCur, newCur)) { + vimGlobalState.jumpList.add(cm, oldCur, newCur); + } + } + + /** @arg {number} increment @arg {{ forward?: any; selectedCharacter?: any; }} args */ + function recordLastCharacterSearch(increment, args) { + vimGlobalState.lastCharacterSearch.increment = increment; + vimGlobalState.lastCharacterSearch.forward = args.forward; + vimGlobalState.lastCharacterSearch.selectedCharacter = args.selectedCharacter; + } + + var symbolToMode = { + '(': 'bracket', ')': 'bracket', '{': 'bracket', '}': 'bracket', + '[': 'section', ']': 'section', + '*': 'comment', '/': 'comment', + 'm': 'method', 'M': 'method', + '#': 'preprocess' + }; + var findSymbolModes = { + bracket: { + isComplete: function(state) { + if (state.nextCh === state.symb) { + state.depth++; + if (state.depth >= 1)return true; + } else if (state.nextCh === state.reverseSymb) { + state.depth--; + } + return false; + } + }, + section: { + init: function(state) { + state.curMoveThrough = true; + state.symb = (state.forward ? ']' : '[') === state.symb ? '{' : '}'; + }, + isComplete: function(state) { + return state.index === 0 && state.nextCh === state.symb; + } + }, + comment: { + isComplete: function(state) { + var found = state.lastCh === '*' && state.nextCh === '/'; + state.lastCh = state.nextCh; + return found; + } + }, + // TODO: The original Vim implementation only operates on level 1 and 2. + // The current implementation doesn't check for code block level and + // therefore it operates on any levels. + method: { + init: function(state) { + state.symb = (state.symb === 'm' ? '{' : '}'); + state.reverseSymb = state.symb === '{' ? '}' : '{'; + }, + isComplete: function(state) { + if (state.nextCh === state.symb)return true; + return false; + } + }, + preprocess: { + init: function(state) { + state.index = 0; + }, + isComplete: function(state) { + if (state.nextCh === '#') { + var token = state.lineText.match(/^#(\w+)/)[1]; + if (token === 'endif') { + if (state.forward && state.depth === 0) { + return true; + } state.depth++; - if (state.depth >= 1)return true; - } else if (state.nextCh === state.reverseSymb) { - state.depth--; - } - return false; - } - }, - section: { - init: function(state) { - state.curMoveThrough = true; - state.symb = (state.forward ? ']' : '[') === state.symb ? '{' : '}'; - }, - isComplete: function(state) { - return state.index === 0 && state.nextCh === state.symb; - } - }, - comment: { - isComplete: function(state) { - var found = state.lastCh === '*' && state.nextCh === '/'; - state.lastCh = state.nextCh; - return found; - } - }, - // TODO: The original Vim implementation only operates on level 1 and 2. - // The current implementation doesn't check for code block level and - // therefore it operates on any levels. - method: { - init: function(state) { - state.symb = (state.symb === 'm' ? '{' : '}'); - state.reverseSymb = state.symb === '{' ? '}' : '{'; - }, - isComplete: function(state) { - if (state.nextCh === state.symb)return true; - return false; - } - }, - preprocess: { - init: function(state) { - state.index = 0; - }, - isComplete: function(state) { - if (state.nextCh === '#') { - var token = state.lineText.match(/^#(\w+)/)[1]; - if (token === 'endif') { - if (state.forward && state.depth === 0) { - return true; - } - state.depth++; - } else if (token === 'if') { - if (!state.forward && state.depth === 0) { - return true; - } - state.depth--; + } else if (token === 'if') { + if (!state.forward && state.depth === 0) { + return true; } - if (token === 'else' && state.depth === 0)return true; + state.depth--; } - return false; + if (token === 'else' && state.depth === 0)return true; } + return false; } + } + }; + /** @arg {CodeMirrorV} cm @arg {number} repeat @arg {boolean|undefined} forward @arg {string} symb */ + function findSymbol(cm, repeat, forward, symb) { + var cur = copyCursor(cm.getCursor()); + var increment = forward ? 1 : -1; + var endLine = forward ? cm.lineCount() : -1; + var curCh = cur.ch; + var line = cur.line; + var lineText = cm.getLine(line); + var state = { + lineText: lineText, + nextCh: lineText.charAt(curCh), + lastCh: null, + index: curCh, + symb: symb, + reverseSymb: (forward ? { ')': '(', '}': '{' } : { '(': ')', '{': '}' })[symb], + forward: forward, + depth: 0, + curMoveThrough: false }; - function findSymbol(cm, repeat, forward, symb) { - var cur = copyCursor(cm.getCursor()); - var increment = forward ? 1 : -1; - var endLine = forward ? cm.lineCount() : -1; - var curCh = cur.ch; - var line = cur.line; - var lineText = cm.getLine(line); - var state = { - lineText: lineText, - nextCh: lineText.charAt(curCh), - lastCh: null, - index: curCh, - symb: symb, - reverseSymb: (forward ? { ')': '(', '}': '{' } : { '(': ')', '{': '}' })[symb], - forward: forward, - depth: 0, - curMoveThrough: false - }; - var mode = symbolToMode[symb]; - if (!mode)return cur; - var init = findSymbolModes[mode].init; - var isComplete = findSymbolModes[mode].isComplete; - if (init) { init(state); } - while (line !== endLine && repeat) { - state.index += increment; - state.nextCh = state.lineText.charAt(state.index); - if (!state.nextCh) { - line += increment; - state.lineText = cm.getLine(line) || ''; - if (increment > 0) { - state.index = 0; - } else { - var lineLen = state.lineText.length; - state.index = (lineLen > 0) ? (lineLen-1) : 0; - } - state.nextCh = state.lineText.charAt(state.index); - } - if (isComplete(state)) { - cur.line = line; - cur.ch = state.index; - repeat--; + var mode = symbolToMode[symb]; + if (!mode)return cur; + var init = findSymbolModes[mode].init; + var isComplete = findSymbolModes[mode].isComplete; + if (init) { init(state); } + while (line !== endLine && repeat) { + state.index += increment; + state.nextCh = state.lineText.charAt(state.index); + if (!state.nextCh) { + line += increment; + state.lineText = cm.getLine(line) || ''; + if (increment > 0) { + state.index = 0; + } else { + var lineLen = state.lineText.length; + state.index = (lineLen > 0) ? (lineLen-1) : 0; } + state.nextCh = state.lineText.charAt(state.index); } - if (state.nextCh || state.curMoveThrough) { - return new Pos(line, state.index); + if (isComplete(state)) { + cur.line = line; + cur.ch = state.index; + repeat--; } - return cur; } + if (state.nextCh || state.curMoveThrough) { + return new Pos(line, state.index); + } + return cur; + } - /* - * Returns the boundaries of the next word. If the cursor in the middle of - * the word, then returns the boundaries of the current word, starting at - * the cursor. If the cursor is at the start/end of a word, and we are going - * forward/backward, respectively, find the boundaries of the next word. - * - * @param {CodeMirror} cm CodeMirror object. - * @param {Cursor} cur The cursor position. - * @param {boolean} forward True to search forward. False to search - * backward. - * @param {boolean} bigWord True if punctuation count as part of the word. - * False if only [a-zA-Z0-9] characters count as part of the word. - * @param {boolean} emptyLineIsWord True if empty lines should be treated - * as words. - * @return {Object{from:number, to:number, line: number}} The boundaries of - * the word, or null if there are no more words. - */ - function findWord(cm, cur, forward, bigWord, emptyLineIsWord) { - var lineNum = cur.line; - var pos = cur.ch; - var line = cm.getLine(lineNum); - var dir = forward ? 1 : -1; - var charTests = bigWord ? bigWordCharTest: wordCharTest; + /* + * Returns the boundaries of the next word. If the cursor in the middle of + * the word, then returns the boundaries of the current word, starting at + * the cursor. If the cursor is at the start/end of a word, and we are going + * forward/backward, respectively, find the boundaries of the next word. + * + * @arg {CodeMirror} cm CodeMirror object. + * @arg {Cursor} cur The cursor position. + * @arg {boolean} forward True to search forward. False to search + * backward. + * @arg {boolean} bigWord True if punctuation count as part of the word. + * False if only [a-zA-Z0-9] characters count as part of the word. + * @arg {boolean} emptyLineIsWord True if empty lines should be treated + * as words. + * @return {Object{from:number, to:number, line: number}} The boundaries of + * the word, or null if there are no more words. + */ + function findWord(cm, cur, forward, bigWord, emptyLineIsWord) { + var lineNum = cur.line; + var pos = cur.ch; + var line = cm.getLine(lineNum); + var dir = forward ? 1 : -1; + var charTests = bigWord ? bigWordCharTest: wordCharTest; + + if (emptyLineIsWord && line == '') { + lineNum += dir; + line = cm.getLine(lineNum); + if (!isLine(cm, lineNum)) { + return null; + } + pos = (forward) ? 0 : line.length; + } + while (true) { if (emptyLineIsWord && line == '') { - lineNum += dir; - line = cm.getLine(lineNum); - if (!isLine(cm, lineNum)) { - return null; - } - pos = (forward) ? 0 : line.length; - } - - while (true) { - if (emptyLineIsWord && line == '') { - return { from: 0, to: 0, line: lineNum }; - } - var stop = (dir > 0) ? line.length : -1; - var wordStart = stop, wordEnd = stop; - // Find bounds of next word. - while (pos != stop) { - var foundWord = false; - for (var i = 0; i < charTests.length && !foundWord; ++i) { - if (charTests[i](line.charAt(pos))) { - wordStart = pos; - // Advance to end of word. - while (pos != stop && charTests[i](line.charAt(pos))) { - pos += dir; - } - wordEnd = pos; - foundWord = wordStart != wordEnd; - if (wordStart == cur.ch && lineNum == cur.line && - wordEnd == wordStart + dir) { - // We started at the end of a word. Find the next one. - continue; - } else { - return { - from: Math.min(wordStart, wordEnd + 1), - to: Math.max(wordStart, wordEnd), - line: lineNum }; - } + return { from: 0, to: 0, line: lineNum }; + } + var stop = (dir > 0) ? line.length : -1; + var wordStart = stop, wordEnd = stop; + // Find bounds of next word. + while (pos != stop) { + var foundWord = false; + for (var i = 0; i < charTests.length && !foundWord; ++i) { + if (charTests[i](line.charAt(pos))) { + wordStart = pos; + // Advance to end of word. + while (pos != stop && charTests[i](line.charAt(pos))) { + pos += dir; + } + wordEnd = pos; + foundWord = wordStart != wordEnd; + if (wordStart == cur.ch && lineNum == cur.line && + wordEnd == wordStart + dir) { + // We started at the end of a word. Find the next one. + continue; + } else { + return { + from: Math.min(wordStart, wordEnd + 1), + to: Math.max(wordStart, wordEnd), + line: lineNum }; } - } - if (!foundWord) { - pos += dir; } } - // Advance to next/prev line. - lineNum += dir; - if (!isLine(cm, lineNum)) { - return null; + if (!foundWord) { + pos += dir; } - line = cm.getLine(lineNum); - pos = (dir > 0) ? 0 : line.length; } + // Advance to next/prev line. + lineNum += dir; + if (!isLine(cm, lineNum)) { + return null; + } + line = cm.getLine(lineNum); + pos = (dir > 0) ? 0 : line.length; } + } - /** - * @param {CodeMirror} cm CodeMirror object. - * @param {Pos} cur The position to start from. - * @param {int} repeat Number of words to move past. - * @param {boolean} forward True to search forward. False to search - * backward. - * @param {boolean} wordEnd True to move to end of word. False to move to - * beginning of word. - * @param {boolean} bigWord True if punctuation count as part of the word. - * False if only alphabet characters count as part of the word. - * @return {Cursor} The position the cursor should move to. - */ - function moveToWord(cm, cur, repeat, forward, wordEnd, bigWord) { - var curStart = copyCursor(cur); - var words = []; - if (forward && !wordEnd || !forward && wordEnd) { - repeat++; - } - // For 'e', empty lines are not considered words, go figure. - var emptyLineIsWord = !(forward && wordEnd); - for (var i = 0; i < repeat; i++) { - var word = findWord(cm, cur, forward, bigWord, emptyLineIsWord); - if (!word) { - var eodCh = lineLength(cm, cm.lastLine()); - words.push(forward - ? {line: cm.lastLine(), from: eodCh, to: eodCh} - : {line: 0, from: 0, to: 0}); - break; - } - words.push(word); - cur = new Pos(word.line, forward ? (word.to - 1) : word.from); - } - var shortCircuit = words.length != repeat; - var firstWord = words[0]; - var lastWord = words.pop(); - if (forward && !wordEnd) { - // w - if (!shortCircuit && (firstWord.from != curStart.ch || firstWord.line != curStart.line)) { - // We did not start in the middle of a word. Discard the extra word at the end. - lastWord = words.pop(); - } - return new Pos(lastWord.line, lastWord.from); - } else if (forward && wordEnd) { - return new Pos(lastWord.line, lastWord.to - 1); - } else if (!forward && wordEnd) { - // ge - if (!shortCircuit && (firstWord.to != curStart.ch || firstWord.line != curStart.line)) { - // We did not start in the middle of a word. Discard the extra word at the end. - lastWord = words.pop(); - } - return new Pos(lastWord.line, lastWord.to); - } else { - // b - return new Pos(lastWord.line, lastWord.from); - } + /** + * @arg {CodeMirror} cm CodeMirror object. + * @arg {Pos} cur The position to start from. + * @arg {number} repeat Number of words to move past. + * @arg {boolean} forward True to search forward. False to search + * backward. + * @arg {boolean} wordEnd True to move to end of word. False to move to + * beginning of word. + * @arg {boolean} bigWord True if punctuation count as part of the word. + * False if only alphabet characters count as part of the word. + * @return {Pos|undefined} The position the cursor should move to. + */ + function moveToWord(cm, cur, repeat, forward, wordEnd, bigWord) { + var curStart = copyCursor(cur); + var words = []; + if (forward && !wordEnd || !forward && wordEnd) { + repeat++; } + // For 'e', empty lines are not considered words, go figure. + var emptyLineIsWord = !(forward && wordEnd); + for (var i = 0; i < repeat; i++) { + var word = findWord(cm, cur, forward, bigWord, emptyLineIsWord); + if (!word) { + var eodCh = lineLength(cm, cm.lastLine()); + words.push(forward + ? {line: cm.lastLine(), from: eodCh, to: eodCh} + : {line: 0, from: 0, to: 0}); + break; + } + words.push(word); + cur = new Pos(word.line, forward ? (word.to - 1) : word.from); + } + var shortCircuit = words.length != repeat; + var firstWord = words[0]; + var lastWord = words.pop(); + if (forward && !wordEnd) { + // w + if (!shortCircuit && (firstWord.from != curStart.ch || firstWord.line != curStart.line)) { + // We did not start in the middle of a word. Discard the extra word at the end. + lastWord = words.pop(); + } + return lastWord && new Pos(lastWord.line, lastWord.from); + } else if (forward && wordEnd) { + return lastWord && new Pos(lastWord.line, lastWord.to - 1); + } else if (!forward && wordEnd) { + // ge + if (!shortCircuit && (firstWord.to != curStart.ch || firstWord.line != curStart.line)) { + // We did not start in the middle of a word. Discard the extra word at the end. + lastWord = words.pop(); + } + return lastWord && new Pos(lastWord.line, lastWord.to); + } else { + // b + return lastWord && new Pos(lastWord.line, lastWord.from); + } + } - function moveToEol(cm, head, motionArgs, vim, keepHPos) { - var cur = head; - var retval= new Pos(cur.line + motionArgs.repeat - 1, Infinity); - var end=cm.clipPos(retval); - end.ch--; - if (!keepHPos) { - vim.lastHPos = Infinity; - vim.lastHSPos = cm.charCoords(end,'div').left; - } - return retval; + /** + * @arg {CodeMirror} cm + * @arg {Pos} head + * @arg {MotionArgs} motionArgs + * @arg {vimState} vim + * @arg {boolean} keepHPos */ + function moveToEol(cm, head, motionArgs, vim, keepHPos) { + var cur = head; + var retval= new Pos(cur.line + motionArgs.repeat - 1, Infinity); + var end=cm.clipPos(retval); + end.ch--; + if (!keepHPos) { + vim.lastHPos = Infinity; + vim.lastHSPos = cm.charCoords(end,'div').left; } + return retval; + } - function moveToCharacter(cm, repeat, forward, character, head) { - var cur = head || cm.getCursor(); - var start = cur.ch; - var idx; - for (var i = 0; i < repeat; i ++) { - var line = cm.getLine(cur.line); - idx = charIdxInLine(start, line, character, forward, true); - if (idx == -1) { - return null; - } - start = idx; + /** + * @arg {CodeMirror} cm + * @arg {number} repeat + * @arg {boolean} [forward] + * @arg {string} [character] + * @arg {Pos} [head] + */ + function moveToCharacter(cm, repeat, forward, character, head) { + if (!character) return; + var cur = head || cm.getCursor(); + var start = cur.ch; + var idx; + for (var i = 0; i < repeat; i ++) { + var line = cm.getLine(cur.line); + idx = charIdxInLine(start, line, character, forward, true); + if (idx == -1) { + return undefined; } - return new Pos(cm.getCursor().line, idx); + start = idx; } + if (idx != undefined) + return new Pos(cm.getCursor().line, idx); + } - function moveToColumn(cm, repeat) { - // repeat is always >= 1, so repeat - 1 always corresponds - // to the column we want to go to. - var line = cm.getCursor().line; - return clipCursorToContent(cm, new Pos(line, repeat - 1)); - } + /** @arg {CodeMirrorV} cm @arg {number} repeat */ + function moveToColumn(cm, repeat) { + // repeat is always >= 1, so repeat - 1 always corresponds + // to the column we want to go to. + var line = cm.getCursor().line; + return clipCursorToContent(cm, new Pos(line, repeat - 1)); + } - function updateMark(cm, vim, markName, pos) { - if (!inArray(markName, validMarks) && !latinCharRegex.test(markName)) { - return; - } - if (vim.marks[markName]) { - vim.marks[markName].clear(); - } - vim.marks[markName] = cm.setBookmark(pos); + /** + * @arg {CodeMirror} cm + * @arg {vimState} vim + * @arg {string} markName + * @arg {Pos} pos */ + function updateMark(cm, vim, markName, pos) { + if (!inArray(markName, validMarks) && !latinCharRegex.test(markName)) { + return; + } + if (vim.marks[markName]) { + vim.marks[markName].clear(); } + vim.marks[markName] = cm.setBookmark(pos); + } - function charIdxInLine(start, line, character, forward, includeChar) { - // Search for char in line. - // motion_options: {forward, includeChar} - // If includeChar = true, include it too. - // If forward = true, search forward, else search backwards. - // If char is not found on this line, do nothing - var idx; - if (forward) { - idx = line.indexOf(character, start + 1); - if (idx != -1 && !includeChar) { - idx -= 1; - } - } else { - idx = line.lastIndexOf(character, start - 1); - if (idx != -1 && !includeChar) { - idx += 1; - } + /** + * @arg {number} start + * @arg {string | any[]} line + * @arg {any} character + * @arg {boolean} [forward] + * @arg {boolean} [includeChar] */ + function charIdxInLine(start, line, character, forward, includeChar) { + // Search for char in line. + // motion_options: {forward, includeChar} + // If includeChar = true, include it too. + // If forward = true, search forward, else search backwards. + // If char is not found on this line, do nothing + var idx; + if (forward) { + idx = line.indexOf(character, start + 1); + if (idx != -1 && !includeChar) { + idx -= 1; + } + } else { + idx = line.lastIndexOf(character, start - 1); + if (idx != -1 && !includeChar) { + idx += 1; } - return idx; } + return idx; + } - function findParagraph(cm, head, repeat, dir, inclusive) { - var line = head.line; - var min = cm.firstLine(); - var max = cm.lastLine(); - var start, end, i = line; - function isEmpty(i) { return !cm.getLine(i); } - function isBoundary(i, dir, any) { - if (any) { return isEmpty(i) != isEmpty(i + dir); } - return !isEmpty(i) && isEmpty(i + dir); - } - if (dir) { - while (min <= i && i <= max && repeat > 0) { - if (isBoundary(i, dir)) { repeat--; } - i += dir; - } - return new Pos(i, 0); + /** @arg {CodeMirrorV} cm + * @arg {Pos} head + * @arg {number} repeat + * @arg {number} dir + * @arg {boolean} [inclusive] */ + function findParagraph(cm, head, repeat, dir, inclusive) { + var line = head.line; + var min = cm.firstLine(); + var max = cm.lastLine(); + var start, end, i = line; + /** @arg {number} i */ + function isEmpty(i) { return !cm.getLine(i); } + /** @arg {number} i @arg {number} dir @arg {boolean} [any] */ + function isBoundary(i, dir, any) { + if (any) { return isEmpty(i) != isEmpty(i + dir); } + return !isEmpty(i) && isEmpty(i + dir); + } + if (dir) { + while (min <= i && i <= max && repeat > 0) { + if (isBoundary(i, dir)) { repeat--; } + i += dir; } + return {start: new Pos(i, 0), end: head}; + } - var vim = cm.state.vim; - if (vim.visualLine && isBoundary(line, 1, true)) { - var anchor = vim.sel.anchor; - if (isBoundary(anchor.line, -1, true)) { - if (!inclusive || anchor.line != line) { - line += 1; - } + var vim = cm.state.vim; + if (vim.visualLine && isBoundary(line, 1, true)) { + var anchor = vim.sel.anchor; + if (isBoundary(anchor.line, -1, true)) { + if (!inclusive || anchor.line != line) { + line += 1; } } - var startState = isEmpty(line); - for (i = line; i <= max && repeat; i++) { - if (isBoundary(i, 1, true)) { - if (!inclusive || isEmpty(i) != startState) { - repeat--; - } + } + var startState = isEmpty(line); + for (i = line; i <= max && repeat; i++) { + if (isBoundary(i, 1, true)) { + if (!inclusive || isEmpty(i) != startState) { + repeat--; } } - end = new Pos(i, 0); - // select boundary before paragraph for the last one - if (i > max && !startState) { startState = true; } - else { inclusive = false; } - for (i = line; i > min; i--) { - if (!inclusive || isEmpty(i) == startState || i == line) { - if (isBoundary(i, -1, true)) { break; } - } + } + end = new Pos(i, 0); + // select boundary before paragraph for the last one + if (i > max && !startState) { startState = true; } + else { inclusive = false; } + for (i = line; i > min; i--) { + if (!inclusive || isEmpty(i) == startState || i == line) { + if (isBoundary(i, -1, true)) { break; } } - start = new Pos(i, 0); - return { start: start, end: end }; } - - /** - * Based on {@link findSentence}. The internal functions have the same names, - * but their behaviour is different. findSentence() crosses line breaks and - * is used for jumping to sentence beginnings before or after the current cursor position, - * whereas getSentence() is for getting the beginning or end of the sentence - * at the current cursor position, either including (a) or excluding (i) whitespace. - */ - function getSentence(cm, cur, repeat, dir, inclusive /*includes whitespace*/) { - - /* - Takes an index object - { - line: the line string, - ln: line number, - pos: index in line, - dir: direction of traversal (-1 or 1) - } - and modifies the pos member to represent the - next valid position or sets the line to null if there are - no more valid positions. - */ - function nextChar(curr) { - if (curr.pos + curr.dir < 0 || curr.pos + curr.dir >= curr.line.length) { - curr.line = null; - } - else { - curr.pos += curr.dir; - } + start = new Pos(i, 0); + return { start: start, end: end }; + } + + /** + * Based on {@link findSentence}. The internal functions have the same names, + * but their behaviour is different. findSentence() crosses line breaks and + * is used for jumping to sentence beginnings before or after the current cursor position, + * whereas getSentence() is for getting the beginning or end of the sentence + * at the current cursor position, either including (a) or excluding (i) whitespace. + * @arg {CodeMirror} cm + * @arg {Pos} cur + * @arg {number} repeat + * @arg {number} dir + * @arg {boolean} inclusive + */ + function getSentence(cm, cur, repeat, dir, inclusive /*includes whitespace*/) { + + /* + Takes an index object + { + line: the line string, + ln: line number, + pos: index in line, + dir: direction of traversal (-1 or 1) + } + and modifies the pos member to represent the + next valid position or sets the line to null if there are + no more valid positions. + */ + function nextChar(curr) { + if (curr.pos + curr.dir < 0 || curr.pos + curr.dir >= curr.line.length) { + curr.line = null; } - /* - Performs one iteration of traversal in forward direction - Returns an index object of the sentence end - */ - function forward(cm, ln, pos, dir) { - var line = cm.getLine(ln); - - var curr = { - line: line, - ln: ln, - pos: pos, - dir: dir, - }; + else { + curr.pos += curr.dir; + } + } + /* + Performs one iteration of traversal in forward direction + Returns an index object of the sentence end + */ + function forward(cm, ln, pos, dir) { + var line = cm.getLine(ln); + + var curr = { + line: line, + ln: ln, + pos: pos, + dir: dir, + }; - if (curr.line === "") { - return { ln: curr.ln, pos: curr.pos }; - } + if (curr.line === "") { + return { ln: curr.ln, pos: curr.pos }; + } - var lastSentencePos = curr.pos; + var lastSentencePos = curr.pos; - // Move one step to skip character we start on - nextChar(curr); + // Move one step to skip character we start on + nextChar(curr); - while (curr.line !== null) { - lastSentencePos = curr.pos; - if (isEndOfSentenceSymbol(curr.line[curr.pos])) { - if (!inclusive) { - return { ln: curr.ln, pos: curr.pos + 1 }; - } - else { - nextChar(curr); - while (curr.line !== null ) { - if (isWhiteSpaceString(curr.line[curr.pos])) { - lastSentencePos = curr.pos; - nextChar(curr) - } - else { - break; - } + while (curr.line !== null) { + lastSentencePos = curr.pos; + if (isEndOfSentenceSymbol(curr.line[curr.pos])) { + if (!inclusive) { + return { ln: curr.ln, pos: curr.pos + 1 }; + } + else { + nextChar(curr); + while (curr.line !== null ) { + if (isWhiteSpaceString(curr.line[curr.pos])) { + lastSentencePos = curr.pos; + nextChar(curr) + } + else { + break; } - return { ln: curr.ln, pos: lastSentencePos + 1 }; } + return { ln: curr.ln, pos: lastSentencePos + 1 }; } - nextChar(curr); } - return { ln: curr.ln, pos: lastSentencePos + 1 }; + nextChar(curr); } + return { ln: curr.ln, pos: lastSentencePos + 1 }; + } - /* - Performs one iteration of traversal in reverse direction - Returns an index object of the sentence start - */ - function reverse(cm, ln, pos, dir) { - var line = cm.getLine(ln); + /** + * Performs one iteration of traversal in reverse direction + * Returns an index object of the sentence start + * @arg {CodeMirror} cm + * @arg {number} ln + * @arg {number} pos + * @arg {number} dir + */ + function reverse(cm, ln, pos, dir) { + var line = cm.getLine(ln); - var curr = { - line: line, - ln: ln, - pos: pos, - dir: dir, - } + var curr = { + line: line, + ln: ln, + pos: pos, + dir: dir, + } - if (curr.line === "") { - return { ln: curr.ln, pos: curr.pos }; - } + if (curr.line === "") { + return { ln: curr.ln, pos: curr.pos }; + } - var lastSentencePos = curr.pos; + var lastSentencePos = curr.pos; - // Move one step to skip character we start on - nextChar(curr); + // Move one step to skip character we start on + nextChar(curr); - while (curr.line !== null) { - if (!isWhiteSpaceString(curr.line[curr.pos]) && !isEndOfSentenceSymbol(curr.line[curr.pos])) { - lastSentencePos = curr.pos; - } + while (curr.line !== null) { + if (!isWhiteSpaceString(curr.line[curr.pos]) && !isEndOfSentenceSymbol(curr.line[curr.pos])) { + lastSentencePos = curr.pos; + } - else if (isEndOfSentenceSymbol(curr.line[curr.pos]) ) { - if (!inclusive) { - return { ln: curr.ln, pos: lastSentencePos }; + else if (isEndOfSentenceSymbol(curr.line[curr.pos]) ) { + if (!inclusive) { + return { ln: curr.ln, pos: lastSentencePos }; + } + else { + if (isWhiteSpaceString(curr.line[curr.pos + 1])) { + return { ln: curr.ln, pos: curr.pos + 1 }; } else { - if (isWhiteSpaceString(curr.line[curr.pos + 1])) { - return { ln: curr.ln, pos: curr.pos + 1 }; - } - else { - return { ln: curr.ln, pos: lastSentencePos }; - } + return { ln: curr.ln, pos: lastSentencePos }; } } - - nextChar(curr); - } - curr.line = line - if (inclusive && isWhiteSpaceString(curr.line[curr.pos])) { - return { ln: curr.ln, pos: curr.pos }; - } - else { - return { ln: curr.ln, pos: lastSentencePos }; } + nextChar(curr); + } + curr.line = line + if (inclusive && isWhiteSpaceString(curr.line[curr.pos])) { + return { ln: curr.ln, pos: curr.pos }; + } + else { + return { ln: curr.ln, pos: lastSentencePos }; } - var curr_index = { - ln: cur.line, - pos: cur.ch, - }; + } - while (repeat > 0) { - if (dir < 0) { - curr_index = reverse(cm, curr_index.ln, curr_index.pos, dir); - } - else { - curr_index = forward(cm, curr_index.ln, curr_index.pos, dir); - } - repeat--; - } + var curr_index = { + ln: cur.line, + pos: cur.ch, + }; - return new Pos(curr_index.ln, curr_index.pos); + while (repeat > 0) { + if (dir < 0) { + curr_index = reverse(cm, curr_index.ln, curr_index.pos, dir); + } + else { + curr_index = forward(cm, curr_index.ln, curr_index.pos, dir); + } + repeat--; } - function findSentence(cm, cur, repeat, dir) { + return new Pos(curr_index.ln, curr_index.pos); + } + + function findSentence(cm, cur, repeat, dir) { - /* - Takes an index object - { - line: the line string, - ln: line number, - pos: index in line, - dir: direction of traversal (-1 or 1) - } - and modifies the line, ln, and pos members to represent the - next valid position or sets them to null if there are - no more valid positions. - */ - function nextChar(cm, idx) { - if (idx.pos + idx.dir < 0 || idx.pos + idx.dir >= idx.line.length) { - idx.ln += idx.dir; - if (!isLine(cm, idx.ln)) { - idx.line = null; - idx.ln = null; - idx.pos = null; - return; - } - idx.line = cm.getLine(idx.ln); - idx.pos = (idx.dir > 0) ? 0 : idx.line.length - 1; + /* + Takes an index object + { + line: the line string, + ln: line number, + pos: index in line, + dir: direction of traversal (-1 or 1) } - else { - idx.pos += idx.dir; + and modifies the line, ln, and pos members to represent the + next valid position or sets them to null if there are + no more valid positions. + */ + function nextChar(cm, idx) { + if (idx.pos + idx.dir < 0 || idx.pos + idx.dir >= idx.line.length) { + idx.ln += idx.dir; + if (!isLine(cm, idx.ln)) { + idx.line = null; + idx.ln = null; + idx.pos = null; + return; } + idx.line = cm.getLine(idx.ln); + idx.pos = (idx.dir > 0) ? 0 : idx.line.length - 1; } + else { + idx.pos += idx.dir; + } + } - /* - Performs one iteration of traversal in forward direction - Returns an index object of the new location - */ - function forward(cm, ln, pos, dir) { - var line = cm.getLine(ln); - var stop = (line === ""); - - var curr = { - line: line, - ln: ln, - pos: pos, - dir: dir, - } + /* + Performs one iteration of traversal in forward direction + Returns an index object of the new location + */ + /** @arg {CodeMirror} cm @arg {number} ln @arg {number} pos @arg {number} dir */ + function forward(cm, ln, pos, dir) { + var line = cm.getLine(ln); + var stop = (line === ""); - var last_valid = { - ln: curr.ln, - pos: curr.pos, - } + var curr = { + line: line, + ln: ln, + pos: pos, + dir: dir, + } - var skip_empty_lines = (curr.line === ""); + var last_valid = { + ln: curr.ln, + pos: curr.pos, + } - // Move one step to skip character we start on - nextChar(cm, curr); + var skip_empty_lines = (curr.line === ""); - while (curr.line !== null) { - last_valid.ln = curr.ln; - last_valid.pos = curr.pos; + // Move one step to skip character we start on + nextChar(cm, curr); - if (curr.line === "" && !skip_empty_lines) { - return { ln: curr.ln, pos: curr.pos, }; - } - else if (stop && curr.line !== "" && !isWhiteSpaceString(curr.line[curr.pos])) { - return { ln: curr.ln, pos: curr.pos, }; - } - else if (isEndOfSentenceSymbol(curr.line[curr.pos]) - && !stop - && (curr.pos === curr.line.length - 1 - || isWhiteSpaceString(curr.line[curr.pos + 1]))) { - stop = true; - } + while (curr.line !== null) { + last_valid.ln = curr.ln; + last_valid.pos = curr.pos; - nextChar(cm, curr); + if (curr.line === "" && !skip_empty_lines) { + return { ln: curr.ln, pos: curr.pos, }; } - - /* - Set the position to the last non whitespace character on the last - valid line in the case that we reach the end of the document. - */ - var line = cm.getLine(last_valid.ln); - last_valid.pos = 0; - for(var i = line.length - 1; i >= 0; --i) { - if (!isWhiteSpaceString(line[i])) { - last_valid.pos = i; - break; - } + else if (stop && curr.line !== "" && !isWhiteSpaceString(curr.line[curr.pos])) { + return { ln: curr.ln, pos: curr.pos, }; + } + else if (isEndOfSentenceSymbol(curr.line[curr.pos]) + && !stop + && (curr.pos === curr.line.length - 1 + || isWhiteSpaceString(curr.line[curr.pos + 1]))) { + stop = true; } - return last_valid; - + nextChar(cm, curr); } /* - Performs one iteration of traversal in reverse direction - Returns an index object of the new location - */ - function reverse(cm, ln, pos, dir) { - var line = cm.getLine(ln); - - var curr = { - line: line, - ln: ln, - pos: pos, - dir: dir, + Set the position to the last non whitespace character on the last + valid line in the case that we reach the end of the document. + */ + var line = cm.getLine(last_valid.ln); + last_valid.pos = 0; + for(var i = line.length - 1; i >= 0; --i) { + if (!isWhiteSpaceString(line[i])) { + last_valid.pos = i; + break; } + } - var last_valid = { - ln: curr.ln, - pos: null, - }; + return last_valid; - var skip_empty_lines = (curr.line === ""); + } - // Move one step to skip character we start on - nextChar(cm, curr); + /* + Performs one iteration of traversal in reverse direction + Returns an index object of the new location + */ + /** @arg {CodeMirror} cm @arg {number} ln @arg {number} pos @arg {number} dir */ + function reverse(cm, ln, pos, dir) { + var line = cm.getLine(ln); - while (curr.line !== null) { + var curr = { + line: line, + ln: ln, + pos: pos, + dir: dir, + } - if (curr.line === "" && !skip_empty_lines) { - if (last_valid.pos !== null) { - return last_valid; - } - else { - return { ln: curr.ln, pos: curr.pos }; - } - } - else if (isEndOfSentenceSymbol(curr.line[curr.pos]) - && last_valid.pos !== null - && !(curr.ln === last_valid.ln && curr.pos + 1 === last_valid.pos)) { + /** @type {{ln: number, pos: number|null}} */ + var last_valid = { + ln: curr.ln, + pos: null, + }; + + var skip_empty_lines = (curr.line === ""); + + // Move one step to skip character we start on + nextChar(cm, curr); + + while (curr.line !== null) { + + if (curr.line === "" && !skip_empty_lines) { + if (last_valid.pos !== null) { return last_valid; } - else if (curr.line !== "" && !isWhiteSpaceString(curr.line[curr.pos])) { - skip_empty_lines = false; - last_valid = { ln: curr.ln, pos: curr.pos } + else { + return { ln: curr.ln, pos: curr.pos }; } - - nextChar(cm, curr); } - - /* - Set the position to the first non whitespace character on the last - valid line in the case that we reach the beginning of the document. - */ - var line = cm.getLine(last_valid.ln); - last_valid.pos = 0; - for(var i = 0; i < line.length; ++i) { - if (!isWhiteSpaceString(line[i])) { - last_valid.pos = i; - break; - } + else if (isEndOfSentenceSymbol(curr.line[curr.pos]) + && last_valid.pos !== null + && !(curr.ln === last_valid.ln && curr.pos + 1 === last_valid.pos)) { + return last_valid; + } + else if (curr.line !== "" && !isWhiteSpaceString(curr.line[curr.pos])) { + skip_empty_lines = false; + last_valid = { ln: curr.ln, pos: curr.pos } } - return last_valid; - } - var curr_index = { - ln: cur.line, - pos: cur.ch, - }; + nextChar(cm, curr); + } - while (repeat > 0) { - if (dir < 0) { - curr_index = reverse(cm, curr_index.ln, curr_index.pos, dir); - } - else { - curr_index = forward(cm, curr_index.ln, curr_index.pos, dir); + /* + Set the position to the first non whitespace character on the last + valid line in the case that we reach the beginning of the document. + */ + var line = cm.getLine(last_valid.ln); + last_valid.pos = 0; + for(var i = 0; i < line.length; ++i) { + if (!isWhiteSpaceString(line[i])) { + last_valid.pos = i; + break; } - repeat--; } - - return new Pos(curr_index.ln, curr_index.pos); + return last_valid; } - // TODO: perhaps this finagling of start and end positions belongs - // in codemirror/replaceRange? - function selectCompanionObject(cm, head, symb, inclusive) { - var cur = head, start, end; + var curr_index = { + ln: cur.line, + pos: cur.ch, + }; - var bracketRegexp = ({ - '(': /[()]/, ')': /[()]/, - '[': /[[\]]/, ']': /[[\]]/, - '{': /[{}]/, '}': /[{}]/, - '<': /[<>]/, '>': /[<>]/})[symb]; - var openSym = ({ - '(': '(', ')': '(', - '[': '[', ']': '[', - '{': '{', '}': '{', - '<': '<', '>': '<'})[symb]; - var curChar = cm.getLine(cur.line).charAt(cur.ch); - // Due to the behavior of scanForBracket, we need to add an offset if the - // cursor is on a matching open bracket. - var offset = curChar === openSym ? 1 : 0; + while (repeat > 0) { + if (dir < 0) { + curr_index = reverse(cm, curr_index.ln, curr_index.pos, dir); + } + else { + curr_index = forward(cm, curr_index.ln, curr_index.pos, dir); + } + repeat--; + } - start = cm.scanForBracket(new Pos(cur.line, cur.ch + offset), -1, undefined, {'bracketRegex': bracketRegexp}); - end = cm.scanForBracket(new Pos(cur.line, cur.ch + offset), 1, undefined, {'bracketRegex': bracketRegexp}); + return new Pos(curr_index.ln, curr_index.pos); + } - if (!start || !end) return null; + // TODO: perhaps this finagling of start and end positions belongs + // in codemirror/replaceRange? + /** @arg {CodeMirror} cm @arg {Pos} head @arg {string | number} symb @arg {boolean} inclusive */ + function selectCompanionObject(cm, head, symb, inclusive) { + var cur = head; + + var bracketRegexp = ({ + '(': /[()]/, ')': /[()]/, + '[': /[[\]]/, ']': /[[\]]/, + '{': /[{}]/, '}': /[{}]/, + '<': /[<>]/, '>': /[<>]/})[symb]; + var openSym = ({ + '(': '(', ')': '(', + '[': '[', ']': '[', + '{': '{', '}': '{', + '<': '<', '>': '<'})[symb]; + var curChar = cm.getLine(cur.line).charAt(cur.ch); + // Due to the behavior of scanForBracket, we need to add an offset if the + // cursor is on a matching open bracket. + var offset = curChar === openSym ? 1 : 0; + + var startBracket = cm.scanForBracket(new Pos(cur.line, cur.ch + offset), -1, undefined, {'bracketRegex': bracketRegexp}); + var endBracket = cm.scanForBracket(new Pos(cur.line, cur.ch + offset), 1, undefined, {'bracketRegex': bracketRegexp}); + + if (!startBracket || !endBracket) return null; + + var start = startBracket.pos; + var end = endBracket.pos; + + if ((start.line == end.line && start.ch > end.ch) + || (start.line > end.line)) { + var tmp = start; + start = end; + end = tmp; + } - start = start.pos; - end = end.pos; + if (inclusive) { + end.ch += 1; + } else { + start.ch += 1; + } - if ((start.line == end.line && start.ch > end.ch) - || (start.line > end.line)) { - var tmp = start; - start = end; - end = tmp; - } + return { start: start, end: end }; + } - if (inclusive) { - end.ch += 1; - } else { - start.ch += 1; + // Takes in a symbol and a cursor and tries to simulate text objects that + // have identical opening and closing symbols + // TODO support across multiple lines + /** @arg {CodeMirror} cm @arg {Pos} head @arg {string} symb @arg {boolean} inclusive */ + function findBeginningAndEnd(cm, head, symb, inclusive) { + var cur = copyCursor(head); + var line = cm.getLine(cur.line); + var chars = line.split(''); + var start, end, i, len; + var firstIndex = chars.indexOf(symb); + + // the decision tree is to always look backwards for the beginning first, + // but if the cursor is in front of the first instance of the symb, + // then move the cursor forward + if (cur.ch < firstIndex) { + cur.ch = firstIndex; + } + // otherwise if the cursor is currently on the closing symbol + else if (firstIndex < cur.ch && chars[cur.ch] == symb) { + var stringAfter = /string/.test(cm.getTokenTypeAt(offsetCursor(head, 0, 1))); + var stringBefore = /string/.test(cm.getTokenTypeAt(head)); + var isStringStart = stringAfter && !stringBefore + if (!isStringStart) { + end = cur.ch; // assign end to the current cursor + --cur.ch; // make sure to look backwards } - - return { start: start, end: end }; } - // Takes in a symbol and a cursor and tries to simulate text objects that - // have identical opening and closing symbols - // TODO support across multiple lines - function findBeginningAndEnd(cm, head, symb, inclusive) { - var cur = copyCursor(head); - var line = cm.getLine(cur.line); - var chars = line.split(''); - var start, end, i, len; - var firstIndex = chars.indexOf(symb); - - // the decision tree is to always look backwards for the beginning first, - // but if the cursor is in front of the first instance of the symb, - // then move the cursor forward - if (cur.ch < firstIndex) { - cur.ch = firstIndex; - } - // otherwise if the cursor is currently on the closing symbol - else if (firstIndex < cur.ch && chars[cur.ch] == symb) { - var stringAfter = /string/.test(cm.getTokenTypeAt(offsetCursor(head, 0, 1))); - var stringBefore = /string/.test(cm.getTokenTypeAt(head)); - var isStringStart = stringAfter && !stringBefore - if (!isStringStart) { - end = cur.ch; // assign end to the current cursor - --cur.ch; // make sure to look backwards - } - } - - // if we're currently on the symbol, we've got a start - if (chars[cur.ch] == symb && !end) { - start = cur.ch + 1; // assign start to ahead of the cursor - } else { - // go backwards to find the start - for (i = cur.ch; i > -1 && !start; i--) { - if (chars[i] == symb) { - start = i + 1; - } + // if we're currently on the symbol, we've got a start + if (chars[cur.ch] == symb && !end) { + start = cur.ch + 1; // assign start to ahead of the cursor + } else { + // go backwards to find the start + for (i = cur.ch; i > -1 && !start; i--) { + if (chars[i] == symb) { + start = i + 1; } } + } - // look forwards for the end symbol - if (start && !end) { - for (i = start, len = chars.length; i < len && !end; i++) { - if (chars[i] == symb) { - end = i; - } + // look forwards for the end symbol + if (start && !end) { + for (i = start, len = chars.length; i < len && !end; i++) { + if (chars[i] == symb) { + end = i; } } + } - // nothing found - if (!start || !end) { - return { start: cur, end: cur }; - } - - // include the symbols - if (inclusive) { - --start; ++end; - } + // nothing found + if (!start || !end) { + return { start: cur, end: cur }; + } - return { - start: new Pos(cur.line, start), - end: new Pos(cur.line, end) - }; + // include the symbols + if (inclusive) { + --start; ++end; } - // Search functions - defineOption('pcre', true, 'boolean'); - function SearchState() {} - SearchState.prototype = { - getQuery: function() { - return vimGlobalState.query; - }, - setQuery: function(query) { - vimGlobalState.query = query; - }, - getOverlay: function() { - return this.searchOverlay; - }, - setOverlay: function(overlay) { - this.searchOverlay = overlay; - }, - isReversed: function() { - return vimGlobalState.isReversed; - }, - setReversed: function(reversed) { - vimGlobalState.isReversed = reversed; - }, - getScrollbarAnnotate: function() { - return this.annotate; - }, - setScrollbarAnnotate: function(annotate) { - this.annotate = annotate; - } + return { + start: new Pos(cur.line, start), + end: new Pos(cur.line, end) }; - function getSearchState(cm) { - var vim = cm.state.vim; - return vim.searchState_ || (vim.searchState_ = new SearchState()); - } - function splitBySlash(argString) { - return splitBySeparator(argString, '/'); - } + } - function findUnescapedSlashes(argString) { - return findUnescapedSeparators(argString, '/'); - } + // Search functions + defineOption('pcre', true, 'boolean'); + + class SearchState { + getQuery() { + return vimGlobalState.query; + }; + setQuery(query) { + vimGlobalState.query = query; + }; + getOverlay() { + return this.searchOverlay; + }; + setOverlay(overlay) { + this.searchOverlay = overlay; + }; + isReversed() { + return vimGlobalState.isReversed; + }; + setReversed(reversed) { + vimGlobalState.isReversed = reversed; + }; + getScrollbarAnnotate() { + return this.annotate; + }; + setScrollbarAnnotate(annotate) { + this.annotate = annotate; + }; + }; + /** @arg {CodeMirrorV} cm */ + function getSearchState(cm) { + var vim = cm.state.vim; + return vim.searchState_ || (vim.searchState_ = new SearchState()); + } + /** @arg {string} argString */ + function splitBySlash(argString) { + return splitBySeparator(argString, '/'); + } - function splitBySeparator(argString, separator) { - var slashes = findUnescapedSeparators(argString, separator) || []; - if (!slashes.length) return []; - var tokens = []; - // in case of strings like foo/bar - if (slashes[0] !== 0) return; - for (var i = 0; i < slashes.length; i++) { - if (typeof slashes[i] == 'number') - tokens.push(argString.substring(slashes[i] + 1, slashes[i+1])); - } - return tokens; + /** @arg {string} argString */ + function findUnescapedSlashes(argString) { + return findUnescapedSeparators(argString, '/'); + } + + /** @arg {string} argString @arg {string} separator */ + function splitBySeparator(argString, separator) { + var slashes = findUnescapedSeparators(argString, separator) || []; + if (!slashes.length) return []; + var tokens = []; + // in case of strings like foo/bar + if (slashes[0] !== 0) return; + for (var i = 0; i < slashes.length; i++) { + if (typeof slashes[i] == 'number') + tokens.push(argString.substring(slashes[i] + 1, slashes[i+1])); } + return tokens; + } - function findUnescapedSeparators(str, separator) { - if (!separator) - separator = '/'; + /** @arg {string} str @arg {string} separator */ + function findUnescapedSeparators(str, separator) { + if (!separator) + separator = '/'; - var escapeNextChar = false; - var slashes = []; - for (var i = 0; i < str.length; i++) { - var c = str.charAt(i); - if (!escapeNextChar && c == separator) { - slashes.push(i); - } - escapeNextChar = !escapeNextChar && (c == '\\'); + var escapeNextChar = false; + var slashes = []; + for (var i = 0; i < str.length; i++) { + var c = str.charAt(i); + if (!escapeNextChar && c == separator) { + slashes.push(i); } - return slashes; + escapeNextChar = !escapeNextChar && (c == '\\'); } + return slashes; + } - // Translates a search string from ex (vim) syntax into javascript form. - function translateRegex(str) { - // When these match, add a '\' if unescaped or remove one if escaped. - var specials = '|(){'; - // Remove, but never add, a '\' for these. - var unescape = '}'; - var escapeNextChar = false; - var out = []; - for (var i = -1; i < str.length; i++) { - var c = str.charAt(i) || ''; - var n = str.charAt(i+1) || ''; - var specialComesNext = (n && specials.indexOf(n) != -1); - if (escapeNextChar) { - if (c !== '\\' || !specialComesNext) { + // Translates a search string from ex (vim) syntax into javascript form. + /** @arg {string} str */ + function translateRegex(str) { + // When these match, add a '\' if unescaped or remove one if escaped. + var specials = '|(){'; + // Remove, but never add, a '\' for these. + var unescape = '}'; + var escapeNextChar = false; + var out = []; + for (var i = -1; i < str.length; i++) { + var c = str.charAt(i) || ''; + var n = str.charAt(i+1) || ''; + var specialComesNext = (n && specials.indexOf(n) != -1); + if (escapeNextChar) { + if (c !== '\\' || !specialComesNext) { + out.push(c); + } + escapeNextChar = false; + } else { + if (c === '\\') { + escapeNextChar = true; + // Treat the unescape list as special for removing, but not adding '\'. + if (n && unescape.indexOf(n) != -1) { + specialComesNext = true; + } + // Not passing this test means removing a '\'. + if (!specialComesNext || n === '\\') { out.push(c); } - escapeNextChar = false; } else { - if (c === '\\') { - escapeNextChar = true; - // Treat the unescape list as special for removing, but not adding '\'. - if (n && unescape.indexOf(n) != -1) { - specialComesNext = true; - } - // Not passing this test means removing a '\'. - if (!specialComesNext || n === '\\') { - out.push(c); - } - } else { - out.push(c); - if (specialComesNext && n !== '\\') { - out.push('\\'); - } - } - } - } - return out.join(''); - } - - // Translates the replace part of a search and replace from ex (vim) syntax into - // javascript form. Similar to translateRegex, but additionally fixes back references - // (translates '\[0..9]' to '$[0..9]') and follows different rules for escaping '$'. - var charUnescapes = {'\\n': '\n', '\\r': '\r', '\\t': '\t'}; - function translateRegexReplace(str) { - var escapeNextChar = false; - var out = []; - for (var i = -1; i < str.length; i++) { - var c = str.charAt(i) || ''; - var n = str.charAt(i+1) || ''; - if (charUnescapes[c + n]) { - out.push(charUnescapes[c+n]); - i++; - } else if (escapeNextChar) { - // At any point in the loop, escapeNextChar is true if the previous - // character was a '\' and was not escaped. out.push(c); - escapeNextChar = false; - } else { - if (c === '\\') { - escapeNextChar = true; - if ((isNumber(n) || n === '$')) { - out.push('$'); - } else if (n !== '/' && n !== '\\') { - out.push('\\'); - } - } else { - if (c === '$') { - out.push('$'); - } - out.push(c); - if (n === '/') { - out.push('\\'); - } + if (specialComesNext && n !== '\\') { + out.push('\\'); } } } - return out.join(''); } + return out.join(''); + } - // Unescape \ and / in the replace part, for PCRE mode. - var unescapes = {'\\/': '/', '\\\\': '\\', '\\n': '\n', '\\r': '\r', '\\t': '\t', '\\&':'&'}; - function unescapeRegexReplace(str) { - var stream = new CodeMirror.StringStream(str); - var output = []; - while (!stream.eol()) { - // Search for \. - while (stream.peek() && stream.peek() != '\\') { - output.push(stream.next()); - } - var matched = false; - for (var matcher in unescapes) { - if (stream.match(matcher, true)) { - matched = true; - output.push(unescapes[matcher]); - break; + // Translates the replace part of a search and replace from ex (vim) syntax into + // javascript form. Similar to translateRegex, but additionally fixes back references + // (translates '\[0..9]' to '$[0..9]') and follows different rules for escaping '$'. + /** @type{Object} */ + var charUnescapes = {'\\n': '\n', '\\r': '\r', '\\t': '\t'}; + /** @arg {string} str */ + function translateRegexReplace(str) { + var escapeNextChar = false; + var out = []; + for (var i = -1; i < str.length; i++) { + var c = str.charAt(i) || ''; + var n = str.charAt(i+1) || ''; + if (charUnescapes[c + n]) { + out.push(charUnescapes[c+n]); + i++; + } else if (escapeNextChar) { + // At any point in the loop, escapeNextChar is true if the previous + // character was a '\' and was not escaped. + out.push(c); + escapeNextChar = false; + } else { + if (c === '\\') { + escapeNextChar = true; + if ((isNumber(n) || n === '$')) { + out.push('$'); + } else if (n !== '/' && n !== '\\') { + out.push('\\'); + } + } else { + if (c === '$') { + out.push('$'); + } + out.push(c); + if (n === '/') { + out.push('\\'); } - } - if (!matched) { - // Don't change anything - output.push(stream.next()); } } - return output.join(''); } + return out.join(''); + } - /** - * Extract the regular expression from the query and return a Regexp object. - * Returns null if the query is blank. - * If ignoreCase is passed in, the Regexp object will have the 'i' flag set. - * If smartCase is passed in, and the query contains upper case letters, - * then ignoreCase is overridden, and the 'i' flag will not be set. - * If the query contains the /i in the flag part of the regular expression, - * then both ignoreCase and smartCase are ignored, and 'i' will be passed - * through to the Regex object. - */ - function parseQuery(query, ignoreCase, smartCase) { - // First update the last search register - var lastSearchRegister = vimGlobalState.registerController.getRegister('/'); - lastSearchRegister.setText(query); - // Check if the query is already a regex. - if (query instanceof RegExp) { return query; } - // First try to extract regex + flags from the input. If no flags found, - // extract just the regex. IE does not accept flags directly defined in - // the regex string in the form /regex/flags - var slashes = findUnescapedSlashes(query); - var regexPart; - var forceIgnoreCase; - if (!slashes.length) { - // Query looks like 'regexp' - regexPart = query; - } else { - // Query looks like 'regexp/...' - regexPart = query.substring(0, slashes[0]); - var flagsPart = query.substring(slashes[0]); - forceIgnoreCase = (flagsPart.indexOf('i') != -1); - } - if (!regexPart) { - return null; - } - if (!getOption('pcre')) { - regexPart = translateRegex(regexPart); + // Unescape \ and / in the replace part, for PCRE mode. + /** @type{Record} */ + var unescapes = {'\\/': '/', '\\\\': '\\', '\\n': '\n', '\\r': '\r', '\\t': '\t', '\\&':'&'}; + /** @arg {string} str */ + function unescapeRegexReplace(str) { + var stream = new CodeMirror.StringStream(str); + var output = []; + while (!stream.eol()) { + // Search for \. + while (stream.peek() && stream.peek() != '\\') { + output.push(stream.next()); + } + var matched = false; + for (var matcher in unescapes) { + if (stream.match(matcher, true)) { + matched = true; + output.push(unescapes[matcher]); + break; + } } - if (smartCase) { - ignoreCase = (/^[^A-Z]*$/).test(regexPart); + if (!matched) { + // Don't change anything + output.push(stream.next()); } - var regexp = new RegExp(regexPart, - (ignoreCase || forceIgnoreCase) ? 'im' : 'm'); - return regexp; } + return output.join(''); + } - /** - * dom - Document Object Manipulator - * Usage: - * dom(''|[, ...{|<$styles>}||'']) - * Examples: - * dom('div', {id:'xyz'}, dom('p', 'CM rocks!', {$color:'red'})) - * dom(document.head, dom('script', 'alert("hello!")')) - * Not supported: - * dom('p', ['arrays are objects'], Error('objects specify attributes')) - */ - function dom(n) { - if (typeof n === 'string') n = document.createElement(n); - for (var a, i = 1; i < arguments.length; i++) { - if (!(a = arguments[i])) continue; - if (typeof a !== 'object') a = document.createTextNode(a); - if (a.nodeType) n.appendChild(a); - else for (var key in a) { - if (!Object.prototype.hasOwnProperty.call(a, key)) continue; - if (key[0] === '$') n.style[key.slice(1)] = a[key]; - else n.setAttribute(key, a[key]); - } - } - return n; + /** + * Extract the regular expression from the query and return a Regexp object. + * Returns null if the query is blank. + * If ignoreCase is passed in, the Regexp object will have the 'i' flag set. + * If smartCase is passed in, and the query contains upper case letters, + * then ignoreCase is overridden, and the 'i' flag will not be set. + * If the query contains the /i in the flag part of the regular expression, + * then both ignoreCase and smartCase are ignored, and 'i' will be passed + * through to the Regex object. + * @arg {string|RegExp} query + * @arg {boolean} ignoreCase + * @arg {boolean} smartCase + */ + function parseQuery(query, ignoreCase, smartCase) { + // First update the last search register + var lastSearchRegister = vimGlobalState.registerController.getRegister('/'); + lastSearchRegister.setText(query); + // Check if the query is already a regex. + if (query instanceof RegExp) { return query; } + // First try to extract regex + flags from the input. If no flags found, + // extract just the regex. IE does not accept flags directly defined in + // the regex string in the form /regex/flags + var slashes = findUnescapedSlashes(query); + var regexPart; + var forceIgnoreCase; + if (!slashes.length) { + // Query looks like 'regexp' + regexPart = query; + } else { + // Query looks like 'regexp/...' + regexPart = query.substring(0, slashes[0]); + var flagsPart = query.substring(slashes[0]); + forceIgnoreCase = (flagsPart.indexOf('i') != -1); } + if (!regexPart) { + return null; + } + if (!getOption('pcre')) { + regexPart = translateRegex(regexPart); + } + if (smartCase) { + ignoreCase = (/^[^A-Z]*$/).test(regexPart); + } + var regexp = new RegExp(regexPart, + (ignoreCase || forceIgnoreCase) ? 'im' : 'm'); + return regexp; + } - function showConfirm(cm, template) { - var pre = dom('div', {$color: 'red', $whiteSpace: 'pre', class: 'cm-vim-message'}, template); - if (cm.openNotification) { - cm.openNotification(pre, {bottom: true, duration: 5000}); - } else { - alert(pre.innerText); + /** + * dom - Document Object Manipulator + * Usage: + * dom(''|[, ...{|<$styles>}||'']) + * Examples: + * dom('div', {id:'xyz'}, dom('p', 'CM rocks!', {$color:'red'})) + * dom(document.head, dom('script', 'alert("hello!")')) + * Not supported: + * dom('p', ['arrays are objects'], Error('objects specify attributes')) + * @arg {string | HTMLElement } n + */ + function dom(n) { + if (typeof n === 'string') n = document.createElement(n); + for (var a, i = 1; i < arguments.length; i++) { + if (!(a = arguments[i])) continue; + if (typeof a !== 'object') a = document.createTextNode(a); + if (a.nodeType) n.appendChild(a); + else for (var key in a) { + if (!Object.prototype.hasOwnProperty.call(a, key)) continue; + if (key[0] === '$') n.style[key.slice(1)] = a[key]; + else n.setAttribute(key, a[key]); } } + return n; + } - function makePrompt(prefix, desc) { - return dom('div', {$display: 'flex'}, - dom('span', {$fontFamily: 'monospace', $whiteSpace: 'pre', $flex: 1}, - prefix, - dom('input', {type: 'text', autocorrect: 'off', - autocapitalize: 'off', spellcheck: 'false', $width: '100%'})), - desc && dom('span', {$color: '#888'}, desc)); + /** @arg {CodeMirror} cm @arg {any} template */ + function showConfirm(cm, template) { + var pre = dom('div', {$color: 'red', $whiteSpace: 'pre', class: 'cm-vim-message'}, template); + if (cm.openNotification) { + cm.openNotification(pre, {bottom: true, duration: 5000}); + } else { + alert(pre.innerText); + } + } + /** @arg {string} prefix @arg {string} desc */ + function makePrompt(prefix, desc) { + return dom('div', {$display: 'flex'}, + dom('span', {$fontFamily: 'monospace', $whiteSpace: 'pre', $flex: 1}, + prefix, + dom('input', {type: 'text', autocorrect: 'off', + autocapitalize: 'off', spellcheck: 'false', $width: '100%'})), + desc && dom('span', {$color: '#888'}, desc)); + } + /** + * @arg {CodeMirror} cm + * @arg {{ onClose?: any; prefix: any; desc?: any; onKeyUp?: any; onKeyDown: any; value?: any; selectValueOnOpen?: boolean; }} options + */ + function showPrompt(cm, options) { + if (keyToKeyStack.length) { + if (!options.value) options.value = ''; + virtualPrompt = options; + return; + } + var template = makePrompt(options.prefix, options.desc); + if (cm.openDialog) { + cm.openDialog(template, options.onClose, { + onKeyDown: options.onKeyDown, onKeyUp: options.onKeyUp, + bottom: true, selectValueOnOpen: false, value: options.value + }); + } + else { + var shortText = ''; + if (typeof options.prefix != "string" && options.prefix) shortText += options.prefix.textContent; + if (options.desc) shortText += " " + options.desc; + options.onClose(prompt(shortText, '')); } + } - function showPrompt(cm, options) { - if (keyToKeyStack.length) { - if (!options.value) options.value = ''; - virtualPrompt = options; - return; - } - var template = makePrompt(options.prefix, options.desc); - if (cm.openDialog) { - cm.openDialog(template, options.onClose, { - onKeyDown: options.onKeyDown, onKeyUp: options.onKeyUp, - bottom: true, selectValueOnOpen: false, value: options.value - }); - } - else { - var shortText = ''; - if (typeof options.prefix != "string" && options.prefix) shortText += options.prefix.textContent; - if (options.desc) shortText += " " + options.desc; - options.onClose(prompt(shortText, '')); - } + /** @arg {RegExp|unknown} r1 @arg {RegExp|unknown} r2 */ + function regexEqual(r1, r2) { + if (r1 instanceof RegExp && r2 instanceof RegExp) { + var props = ['global', 'multiline', 'ignoreCase', 'source']; + for (var i = 0; i < props.length; i++) { + var prop = props[i]; + if (r1[prop] !== r2[prop]) { + return false; + } + } + return true; + } + return false; + } + // Returns true if the query is valid. + /** + * @arg {CodeMirrorV} cm + * @arg {string | RegExp} rawQuery + * @arg {boolean | undefined} [ignoreCase] + * @arg {boolean | undefined} [smartCase] + */ + function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) { + if (!rawQuery) { + return; } - - function regexEqual(r1, r2) { - if (r1 instanceof RegExp && r2 instanceof RegExp) { - var props = ['global', 'multiline', 'ignoreCase', 'source']; - for (var i = 0; i < props.length; i++) { - var prop = props[i]; - if (r1[prop] !== r2[prop]) { - return false; - } - } - return true; - } - return false; + var state = getSearchState(cm); + var query = parseQuery(rawQuery, !!ignoreCase, !!smartCase); + if (!query) { + return; } - // Returns true if the query is valid. - function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) { - if (!rawQuery) { - return; - } - var state = getSearchState(cm); - var query = parseQuery(rawQuery, !!ignoreCase, !!smartCase); - if (!query) { - return; - } - highlightSearchMatches(cm, query); - if (regexEqual(query, state.getQuery())) { - return query; - } - state.setQuery(query); + highlightSearchMatches(cm, query); + if (regexEqual(query, state.getQuery())) { return query; } - function searchOverlay(query) { - if (query.source.charAt(0) == '^') { - var matchSol = true; - } - return { - token: function(stream) { - if (matchSol && !stream.sol()) { - stream.skipToEnd(); - return; + state.setQuery(query); + return query; + } + /** @arg {RegExp} query */ + function searchOverlay(query) { + if (query.source.charAt(0) == '^') { + var matchSol = true; + } + return { + token: function(stream) { + if (matchSol && !stream.sol()) { + stream.skipToEnd(); + return; + } + var match = stream.match(query, false); + if (match) { + if (match[0].length == 0) { + // Matched empty string, skip to next. + stream.next(); + return 'searching'; } - var match = stream.match(query, false); - if (match) { - if (match[0].length == 0) { - // Matched empty string, skip to next. + if (!stream.sol()) { + // Backtrack 1 to match \b + stream.backUp(1); + if (!query.exec(stream.next() + match[0])) { stream.next(); - return 'searching'; + return null; } - if (!stream.sol()) { - // Backtrack 1 to match \b - stream.backUp(1); - if (!query.exec(stream.next() + match[0])) { - stream.next(); - return null; - } - } - stream.match(query); - return 'searching'; - } - while (!stream.eol()) { - stream.next(); - if (stream.match(query, false)) break; } - }, - query: query - }; - } - var highlightTimeout = 0; - function highlightSearchMatches(cm, query) { - clearTimeout(highlightTimeout); + stream.match(query); + return 'searching'; + } + while (!stream.eol()) { + stream.next(); + if (stream.match(query, false)) break; + } + }, + query: query + }; + } + var highlightTimeout = 0; + /** @arg {CodeMirrorV} cm @arg {RegExp} query */ + function highlightSearchMatches(cm, query) { + clearTimeout(highlightTimeout); + var searchState = getSearchState(cm); + searchState.highlightTimeout = highlightTimeout; + highlightTimeout = setTimeout(function() { + if (!cm.state.vim) return; var searchState = getSearchState(cm); - searchState.highlightTimeout = highlightTimeout; - highlightTimeout = setTimeout(function() { - if (!cm.state.vim) return; - var searchState = getSearchState(cm); - searchState.highlightTimeout = null; - var overlay = searchState.getOverlay(); - if (!overlay || query != overlay.query) { - if (overlay) { - cm.removeOverlay(overlay); - } - overlay = searchOverlay(query); - cm.addOverlay(overlay); - if (cm.showMatchesOnScrollbar) { - if (searchState.getScrollbarAnnotate()) { - searchState.getScrollbarAnnotate().clear(); - } - searchState.setScrollbarAnnotate(cm.showMatchesOnScrollbar(query)); + searchState.highlightTimeout = null; + var overlay = searchState.getOverlay(); + if (!overlay || query != overlay.query) { + if (overlay) { + cm.removeOverlay(overlay); + } + overlay = searchOverlay(query); + cm.addOverlay(overlay); + if (cm.showMatchesOnScrollbar) { + if (searchState.getScrollbarAnnotate()) { + searchState.getScrollbarAnnotate().clear(); } - searchState.setOverlay(overlay); + searchState.setScrollbarAnnotate(cm.showMatchesOnScrollbar(query)); } - }, 50); - } - function findNext(cm, prev, query, repeat) { + searchState.setOverlay(overlay); + } + }, 50); + } + /** @arg {CodeMirror} cm @arg {boolean} prev @arg {RegExp} query @arg {number | undefined} [repeat] */ + function findNext(cm, prev, query, repeat) { + return cm.operation(function() { if (repeat === undefined) { repeat = 1; } - return cm.operation(function() { - var pos = cm.getCursor(); - var cursor = cm.getSearchCursor(query, pos); - for (var i = 0; i < repeat; i++) { - var found = cursor.find(prev); - if (i == 0 && found && cursorEqual(cursor.from(), pos)) { - var lastEndPos = prev ? cursor.from() : cursor.to(); - found = cursor.find(prev); - if (found && !found[0] && cursorEqual(cursor.from(), lastEndPos)) { - if (cm.getLine(lastEndPos.line).length == lastEndPos.ch) - found = cursor.find(prev); - } - } - if (!found) { - // SearchCursor may have returned null because it hit EOF, wrap - // around and try again. - cursor = cm.getSearchCursor(query, - (prev) ? new Pos(cm.lastLine()) : new Pos(cm.firstLine(), 0) ); - if (!cursor.find(prev)) { - return; - } + var pos = cm.getCursor(); + var cursor = cm.getSearchCursor(query, pos); + for (var i = 0; i < repeat; i++) { + var found = cursor.find(prev); + // @ts-ignore + if (i == 0 && found && cursorEqual(cursor.from(), pos)) { + var lastEndPos = prev ? cursor.from() : cursor.to(); + found = cursor.find(prev); + // @ts-ignore + if (found && !found[0] && cursorEqual(cursor.from(), lastEndPos)) { + // @ts-ignore + if (cm.getLine(lastEndPos.line).length == lastEndPos.ch) + found = cursor.find(prev); + } + } + if (!found) { + // SearchCursor may have returned null because it hit EOF, wrap + // around and try again. + cursor = cm.getSearchCursor(query, + // @ts-ignore + (prev) ? new Pos(cm.lastLine()) : new Pos(cm.firstLine(), 0) ); + if (!cursor.find(prev)) { + return; } } - return cursor.from(); - }); - } - /** - * Pretty much the same as `findNext`, except for the following differences: - * - * 1. Before starting the search, move to the previous search. This way if our cursor is - * already inside a match, we should return the current match. - * 2. Rather than only returning the cursor's from, we return the cursor's from and to as a tuple. - */ - function findNextFromAndToInclusive(cm, prev, query, repeat, vim) { + } + return cursor.from(); + }); + } + /** + * Pretty much the same as `findNext`, except for the following differences: + * + * 1. Before starting the search, move to the previous search. This way if our cursor is + * already inside a match, we should return the current match. + * 2. Rather than only returning the cursor's from, we return the cursor's from and to as a tuple. + * @arg {CodeMirror} cm + * @arg {boolean} prev + * @arg {any} query + * @arg {number | undefined} repeat + * @arg {vimState} vim + */ + function findNextFromAndToInclusive(cm, prev, query, repeat, vim) { + return cm.operation(function() { if (repeat === undefined) { repeat = 1; } - return cm.operation(function() { - var pos = cm.getCursor(); - var cursor = cm.getSearchCursor(query, pos); + var pos = cm.getCursor(); + var cursor = cm.getSearchCursor(query, pos); - // Go back one result to ensure that if the cursor is currently a match, we keep it. - var found = cursor.find(!prev); + // Go back one result to ensure that if the cursor is currently a match, we keep it. + var found = cursor.find(!prev); - // If we haven't moved, go back one more (similar to if i==0 logic in findNext). - if (!vim.visualMode && found && cursorEqual(cursor.from(), pos)) { - cursor.find(!prev); - } + // If we haven't moved, go back one more (similar to if i==0 logic in findNext). + // @ts-ignore + if (!vim.visualMode && found && cursorEqual(cursor.from(), pos)) { + cursor.find(!prev); + } - for (var i = 0; i < repeat; i++) { - found = cursor.find(prev); - if (!found) { - // SearchCursor may have returned null because it hit EOF, wrap - // around and try again. - cursor = cm.getSearchCursor(query, - (prev) ? new Pos(cm.lastLine()) : new Pos(cm.firstLine(), 0) ); - if (!cursor.find(prev)) { - return; - } + for (var i = 0; i < repeat; i++) { + found = cursor.find(prev); + if (!found) { + // SearchCursor may have returned null because it hit EOF, wrap + // around and try again. + cursor = cm.getSearchCursor(query, + // @ts-ignore + (prev) ? new Pos(cm.lastLine()) : new Pos(cm.firstLine(), 0) ); + if (!cursor.find(prev)) { + return; } } - return [cursor.from(), cursor.to()]; - }); - } - function clearSearchHighlight(cm) { - var state = getSearchState(cm); - if (state.highlightTimeout) { - clearTimeout(state.highlightTimeout); - state.highlightTimeout = null; - } - cm.removeOverlay(getSearchState(cm).getOverlay()); - state.setOverlay(null); - if (state.getScrollbarAnnotate()) { - state.getScrollbarAnnotate().clear(); - state.setScrollbarAnnotate(null); } + return [cursor.from(), cursor.to()]; + }); + } + /** @arg {CodeMirrorV} cm */ + function clearSearchHighlight(cm) { + var state = getSearchState(cm); + if (state.highlightTimeout) { + clearTimeout(state.highlightTimeout); + state.highlightTimeout = null; } - /** - * Check if pos is in the specified range, INCLUSIVE. - * Range can be specified with 1 or 2 arguments. - * If the first range argument is an array, treat it as an array of line - * numbers. Match pos against any of the lines. - * If the first range argument is a number, - * if there is only 1 range argument, check if pos has the same line - * number - * if there are 2 range arguments, then check if pos is in between the two - * range arguments. - */ - function isInRange(pos, start, end) { - if (typeof pos != 'number') { - // Assume it is a cursor position. Get the line number. - pos = pos.line; - } - if (start instanceof Array) { - return inArray(pos, start); + cm.removeOverlay(getSearchState(cm).getOverlay()); + state.setOverlay(null); + if (state.getScrollbarAnnotate()) { + state.getScrollbarAnnotate().clear(); + state.setScrollbarAnnotate(null); + } + } + /** + * Check if pos is in the specified range, INCLUSIVE. + * Range can be specified with 1 or 2 arguments. + * If the first range argument is an array, treat it as an array of line + * numbers. Match pos against any of the lines. + * If the first range argument is a number, + * if there is only 1 range argument, check if pos has the same line + * number + * if there are 2 range arguments, then check if pos is in between the two + * range arguments. + * @arg {number|Pos} pos + * @arg {number|number[]} start + * @arg {number} end + */ + function isInRange(pos, start, end) { + if (typeof pos != 'number') { + // Assume it is a cursor position. Get the line number. + pos = pos.line; + } + if (start instanceof Array) { + return inArray(pos, start); + } else { + if (typeof end == 'number') { + return (pos >= start && pos <= end); } else { - if (typeof end == 'number') { - return (pos >= start && pos <= end); - } else { - return pos == start; - } + return pos == start; } } - function getUserVisibleLines(cm) { - var scrollInfo = cm.getScrollInfo(); - var occludeToleranceTop = 6; - var occludeToleranceBottom = 10; - var from = cm.coordsChar({left:0, top: occludeToleranceTop + scrollInfo.top}, 'local'); - var bottomY = scrollInfo.clientHeight - occludeToleranceBottom + scrollInfo.top; - var to = cm.coordsChar({left:0, top: bottomY}, 'local'); - return {top: from.line, bottom: to.line}; + } + /** @arg {CodeMirror} cm */ + function getUserVisibleLines(cm) { + var scrollInfo = cm.getScrollInfo(); + var occludeToleranceTop = 6; + var occludeToleranceBottom = 10; + var from = cm.coordsChar({left:0, top: occludeToleranceTop + scrollInfo.top}, 'local'); + var bottomY = scrollInfo.clientHeight - occludeToleranceBottom + scrollInfo.top; + var to = cm.coordsChar({left:0, top: bottomY}, 'local'); + return {top: from.line, bottom: to.line}; + } + + /** @arg {CodeMirror} cm @arg {vimState} vim @arg {string} markName */ + function getMarkPos(cm, vim, markName) { + if (markName == '\'' || markName == '`') { + return vimGlobalState.jumpList.find(cm, -1) || new Pos(0, 0); + } else if (markName == '.') { + return getLastEditPos(cm); } - function getMarkPos(cm, vim, markName) { - if (markName == '\'' || markName == '`') { - return vimGlobalState.jumpList.find(cm, -1) || new Pos(0, 0); - } else if (markName == '.') { - return getLastEditPos(cm); - } + var mark = vim.marks[markName]; + return mark && mark.find(); + } - var mark = vim.marks[markName]; - return mark && mark.find(); + /** @arg {CodeMirror} cm */ + function getLastEditPos(cm) { + if (cm.getLastEditEnd) { + return cm.getLastEditEnd(); } - - function getLastEditPos(cm) { - if (cm.getLastEditEnd) { - return cm.getLastEditEnd(); - } - // for old cm - var done = cm.doc.history.done; - for (var i = done.length; i--;) { - if (done[i].changes) { - return copyCursor(done[i].changes[0].to); - } + // for old cm + var done = /**@type{any}*/(cm).doc.history.done; + for (var i = done.length; i--;) { + if (done[i].changes) { + return copyCursor(done[i].changes[0].to); } } + } - var ExCommandDispatcher = function() { + class ExCommandDispatcher { + constructor() { + /**@type {Record} */ + this.commandMap_ this.buildCommandMap_(); - }; - ExCommandDispatcher.prototype = { - processCommand: function(cm, input, opt_params) { - var that = this; - cm.operation(function () { - cm.curOp.isVimOp = true; - that._processCommand(cm, input, opt_params); - }); - }, - _processCommand: function(cm, input, opt_params) { - var vim = cm.state.vim; - var commandHistoryRegister = vimGlobalState.registerController.getRegister(':'); - var previousCommand = commandHistoryRegister.toString(); - var inputStream = new CodeMirror.StringStream(input); - // update ": with the latest command whether valid or invalid - commandHistoryRegister.setText(input); - var params = opt_params || {}; - params.input = input; - try { - this.parseInput_(cm, inputStream, params); - } catch(e) { - showConfirm(cm, e.toString()); - throw e; - } + } + processCommand(cm, input, opt_params) { + var that = this; + cm.operation(function () { + cm.curOp.isVimOp = true; + that._processCommand(cm, input, opt_params); + }); + } + _processCommand(cm, input, opt_params) { + var vim = cm.state.vim; + var commandHistoryRegister = vimGlobalState.registerController.getRegister(':'); + var previousCommand = commandHistoryRegister.toString(); + var inputStream = new CodeMirror.StringStream(input); + // update ": with the latest command whether valid or invalid + commandHistoryRegister.setText(input); + var params = opt_params || {}; + params.input = input; + try { + this.parseInput_(cm, inputStream, params); + } catch(e) { + showConfirm(cm, e + ""); + throw e; + } - if (vim.visualMode) { - exitVisualMode(cm); - } + if (vim.visualMode) { + exitVisualMode(cm); + } - var command; - var commandName; - if (!params.commandName) { - // If only a line range is defined, move to the line. - if (params.line !== undefined) { - commandName = 'move'; - } - } else { - command = this.matchCommand_(params.commandName); - if (command) { - commandName = command.name; - if (command.excludeFromCommandHistory) { - commandHistoryRegister.setText(previousCommand); - } - this.parseCommandArgs_(inputStream, params, command); - if (command.type == 'exToKey') { - // Handle Ex to Key mapping. - doKeyToKey(cm, command.toKeys, command); - return; - } else if (command.type == 'exToEx') { - // Handle Ex to Ex mapping. - this.processCommand(cm, command.toInput); - return; - } - } - } - if (!commandName) { - showConfirm(cm, 'Not an editor command ":' + input + '"'); - return; + var command; + var commandName; + if (!params.commandName) { + // If only a line range is defined, move to the line. + if (params.line !== undefined) { + commandName = 'move'; } - try { - exCommands[commandName](cm, params); - // Possibly asynchronous commands (e.g. substitute, which might have a - // user confirmation), are responsible for calling the callback when - // done. All others have it taken care of for them here. - if ((!command || !command.possiblyAsync) && params.callback) { - params.callback(); + } else { + command = this.matchCommand_(params.commandName); + if (command) { + commandName = command.name; + if (command.excludeFromCommandHistory) { + commandHistoryRegister.setText(previousCommand); + } + this.parseCommandArgs_(inputStream, params, command); + if (command.type == 'exToKey') { + // Handle Ex to Key mapping. + doKeyToKey(cm, command.toKeys, command); + return; + } else if (command.type == 'exToEx') { + // Handle Ex to Ex mapping. + this.processCommand(cm, command.toInput); + return; } - } catch(e) { - showConfirm(cm, e.toString()); - throw e; } - }, - parseInput_: function(cm, inputStream, result) { - inputStream.eatWhile(':'); - // Parse range. - if (inputStream.eat('%')) { - result.line = cm.firstLine(); - result.lineEnd = cm.lastLine(); - } else { - result.line = this.parseLineSpec_(cm, inputStream); - if (result.line !== undefined && inputStream.eat(',')) { - result.lineEnd = this.parseLineSpec_(cm, inputStream); - } + } + if (!commandName) { + showConfirm(cm, 'Not an editor command ":' + input + '"'); + return; + } + try { + exCommands[commandName](cm, params); + // Possibly asynchronous commands (e.g. substitute, which might have a + // user confirmation), are responsible for calling the callback when + // done. All others have it taken care of for them here. + if ((!command || !command.possiblyAsync) && params.callback) { + params.callback(); } - - if (result.line == undefined) { - if (cm.state.vim.visualMode) { - result.selectionLine = getMarkPos(cm, cm.state.vim, '<')?.line; - result.selectionLineEnd = getMarkPos(cm, cm.state.vim, '>')?.line; - } else { - result.selectionLine = cm.getCursor().line; - } - } else { - result.selectionLine = result.line; - result.selectionLineEnd = result.lineEnd; + } catch(e) { + showConfirm(cm, e + ""); + throw e; + } + } + parseInput_(cm, inputStream, result) { + inputStream.eatWhile(':'); + // Parse range. + if (inputStream.eat('%')) { + result.line = cm.firstLine(); + result.lineEnd = cm.lastLine(); + } else { + result.line = this.parseLineSpec_(cm, inputStream); + if (result.line !== undefined && inputStream.eat(',')) { + result.lineEnd = this.parseLineSpec_(cm, inputStream); } + } - // Parse command name. - var commandMatch = inputStream.match(/^(\w+|!!|@@|[!#&*<=>@~])/); - if (commandMatch) { - result.commandName = commandMatch[1]; + if (result.line == undefined) { + if (cm.state.vim.visualMode) { + result.selectionLine = getMarkPos(cm, cm.state.vim, '<')?.line; + result.selectionLineEnd = getMarkPos(cm, cm.state.vim, '>')?.line; } else { - result.commandName = inputStream.match(/.*/)[0]; + result.selectionLine = cm.getCursor().line; } + } else { + result.selectionLine = result.line; + result.selectionLineEnd = result.lineEnd; + } - return result; - }, - parseLineSpec_: function(cm, inputStream) { - var numberMatch = inputStream.match(/^(\d+)/); - if (numberMatch) { - // Absolute line number plus offset (N+M or N-M) is probably a typo, - // not something the user actually wanted. (NB: vim does allow this.) - return parseInt(numberMatch[1], 10) - 1; - } - switch (inputStream.next()) { - case '.': - return this.parseLineSpecOffset_(inputStream, cm.getCursor().line); - case '$': - return this.parseLineSpecOffset_(inputStream, cm.lastLine()); - case '\'': - var markName = inputStream.next(); - var markPos = getMarkPos(cm, cm.state.vim, markName); - if (!markPos) throw new Error('Mark not set'); - return this.parseLineSpecOffset_(inputStream, markPos.line); - case '-': - case '+': - inputStream.backUp(1); - // Offset is relative to current line if not otherwise specified. - return this.parseLineSpecOffset_(inputStream, cm.getCursor().line); - default: - inputStream.backUp(1); - return undefined; - } - }, - parseLineSpecOffset_: function(inputStream, line) { - var offsetMatch = inputStream.match(/^([+-])?(\d+)/); - if (offsetMatch) { - var offset = parseInt(offsetMatch[2], 10); - if (offsetMatch[1] == "-") { - line -= offset; - } else { - line += offset; - } - } - return line; - }, - parseCommandArgs_: function(inputStream, params, command) { - if (inputStream.eol()) { - return; - } - params.argString = inputStream.match(/.*/)[0]; - // Parse command-line arguments - var delim = command.argDelimiter || /\s+/; - var args = trim(params.argString).split(delim); - if (args.length && args[0]) { - params.args = args; + // Parse command name. + var commandMatch = inputStream.match(/^(\w+|!!|@@|[!#&*<=>@~])/); + if (commandMatch) { + result.commandName = commandMatch[1]; + } else { + result.commandName = inputStream.match(/.*/)[0]; + } + + return result; + } + parseLineSpec_(cm, inputStream) { + var numberMatch = inputStream.match(/^(\d+)/); + if (numberMatch) { + // Absolute line number plus offset (N+M or N-M) is probably a typo, + // not something the user actually wanted. (NB: vim does allow this.) + return parseInt(numberMatch[1], 10) - 1; + } + switch (inputStream.next()) { + case '.': + return this.parseLineSpecOffset_(inputStream, cm.getCursor().line); + case '$': + return this.parseLineSpecOffset_(inputStream, cm.lastLine()); + case '\'': + var markName = inputStream.next(); + var markPos = getMarkPos(cm, cm.state.vim, markName); + if (!markPos) throw new Error('Mark not set'); + return this.parseLineSpecOffset_(inputStream, markPos.line); + case '-': + case '+': + inputStream.backUp(1); + // Offset is relative to current line if not otherwise specified. + return this.parseLineSpecOffset_(inputStream, cm.getCursor().line); + default: + inputStream.backUp(1); + return undefined; + } + } + parseLineSpecOffset_(inputStream, line) { + var offsetMatch = inputStream.match(/^([+-])?(\d+)/); + if (offsetMatch) { + var offset = parseInt(offsetMatch[2], 10); + if (offsetMatch[1] == "-") { + line -= offset; + } else { + line += offset; } - }, - matchCommand_: function(commandName) { - // Return the command in the command map that matches the shortest - // prefix of the passed in command name. The match is guaranteed to be - // unambiguous if the defaultExCommandMap's shortNames are set up - // correctly. (see @code{defaultExCommandMap}). - for (var i = commandName.length; i > 0; i--) { - var prefix = commandName.substring(0, i); - if (this.commandMap_[prefix]) { - var command = this.commandMap_[prefix]; - if (command.name.indexOf(commandName) === 0) { - return command; - } + } + return line; + } + parseCommandArgs_(inputStream, params, command) { + if (inputStream.eol()) { + return; + } + params.argString = inputStream.match(/.*/)[0]; + // Parse command-line arguments + var delim = command.argDelimiter || /\s+/; + var args = trim(params.argString).split(delim); + if (args.length && args[0]) { + params.args = args; + } + } + matchCommand_(commandName) { + // Return the command in the command map that matches the shortest + // prefix of the passed in command name. The match is guaranteed to be + // unambiguous if the defaultExCommandMap's shortNames are set up + // correctly. (see @code{defaultExCommandMap}). + for (var i = commandName.length; i > 0; i--) { + var prefix = commandName.substring(0, i); + if (this.commandMap_[prefix]) { + var command = this.commandMap_[prefix]; + if (command.name.indexOf(commandName) === 0) { + return command; } } - return null; - }, - buildCommandMap_: function() { - this.commandMap_ = {}; - for (var i = 0; i < defaultExCommandMap.length; i++) { - var command = defaultExCommandMap[i]; - var key = command.shortName || command.name; - this.commandMap_[key] = command; - } - }, - map: function(lhs, rhs, ctx, noremap) { - if (lhs != ':' && lhs.charAt(0) == ':') { - if (ctx) { throw Error('Mode not supported for ex mappings'); } - var commandName = lhs.substring(1); - if (rhs != ':' && rhs.charAt(0) == ':') { - // Ex to Ex mapping - this.commandMap_[commandName] = { - name: commandName, - type: 'exToEx', - toInput: rhs.substring(1), - user: true - }; - } else { - // Ex to key mapping - this.commandMap_[commandName] = { - name: commandName, - type: 'exToKey', - toKeys: rhs, - user: true - }; - } + } + return null; + } + buildCommandMap_() { + this.commandMap_ = {}; + for (var i = 0; i < defaultExCommandMap.length; i++) { + var command = defaultExCommandMap[i]; + var key = command.shortName || command.name; + this.commandMap_[key] = command; + } + } + /**@type {(lhs: string, rhs: string, ctx: string, noremap?: boolean) => void} */ + map(lhs, rhs, ctx, noremap) { + if (lhs != ':' && lhs.charAt(0) == ':') { + if (ctx) { throw Error('Mode not supported for ex mappings'); } + var commandName = lhs.substring(1); + if (rhs != ':' && rhs.charAt(0) == ':') { + // Ex to Ex mapping + this.commandMap_[commandName] = { + name: commandName, + type: 'exToEx', + toInput: rhs.substring(1), + user: true + }; } else { - // Key to key or ex mapping - var mapping = { - keys: lhs, - type: 'keyToKey', + // Ex to key mapping + this.commandMap_[commandName] = { + name: commandName, + type: 'exToKey', toKeys: rhs, - noremap: !!noremap + user: true }; - if (ctx) { mapping.context = ctx; } - defaultKeymap.unshift(mapping); } - }, - unmap: function(lhs, ctx) { - if (lhs != ':' && lhs.charAt(0) == ':') { - // Ex to Ex or Ex to key mapping - if (ctx) { throw Error('Mode not supported for ex mappings'); } - var commandName = lhs.substring(1); - if (this.commandMap_[commandName] && this.commandMap_[commandName].user) { - delete this.commandMap_[commandName]; + } else { + // Key to key or ex mapping + var mapping = { + keys: lhs, + type: 'keyToKey', + toKeys: rhs, + noremap: !!noremap + }; + if (ctx) { mapping.context = ctx; } + // @ts-ignore + defaultKeymap.unshift(mapping); + } + } + /**@type {(lhs: string, ctx: string) => boolean|void} */ + unmap(lhs, ctx) { + if (lhs != ':' && lhs.charAt(0) == ':') { + // Ex to Ex or Ex to key mapping + if (ctx) { throw Error('Mode not supported for ex mappings'); } + var commandName = lhs.substring(1); + if (this.commandMap_[commandName] && this.commandMap_[commandName].user) { + delete this.commandMap_[commandName]; + return true; + } + } else { + // Key to Ex or key to key mapping + var keys = lhs; + for (var i = 0; i < defaultKeymap.length; i++) { + if (keys == defaultKeymap[i].keys + && defaultKeymap[i].context === ctx) { + defaultKeymap.splice(i, 1); return true; } - } else { - // Key to Ex or key to key mapping - var keys = lhs; - for (var i = 0; i < defaultKeymap.length; i++) { - if (keys == defaultKeymap[i].keys - && defaultKeymap[i].context === ctx) { - defaultKeymap.splice(i, 1); - return true; - } - } } } - }; + } + } - var exCommands = { - colorscheme: function(cm, params) { - if (!params.args || params.args.length < 1) { - showConfirm(cm, cm.getOption('theme')); - return; - } - cm.setOption('theme', params.args[0]); - }, - map: function(cm, params, ctx, defaultOnly) { - var mapArgs = params.args; - if (!mapArgs || mapArgs.length < 2) { - if (cm) { - showConfirm(cm, 'Invalid mapping: ' + params.input); - } - return; - } - exCommandDispatcher.map(mapArgs[0], mapArgs[1], ctx, defaultOnly); - }, - imap: function(cm, params) { this.map(cm, params, 'insert'); }, - nmap: function(cm, params) { this.map(cm, params, 'normal'); }, - vmap: function(cm, params) { this.map(cm, params, 'visual'); }, - omap: function(cm, params) { this.map(cm, params, 'operatorPending'); }, - noremap: function(cm, params) { this.map(cm, params, undefined, true); }, - inoremap: function(cm, params) { this.map(cm, params, 'insert', true); }, - nnoremap: function(cm, params) { this.map(cm, params, 'normal', true); }, - vnoremap: function(cm, params) { this.map(cm, params, 'visual', true); }, - onoremap: function(cm, params) { this.map(cm, params, 'operatorPending', true); }, - unmap: function(cm, params, ctx) { - var mapArgs = params.args; - if (!mapArgs || mapArgs.length < 1 || !exCommandDispatcher.unmap(mapArgs[0], ctx)) { - if (cm) { - showConfirm(cm, 'No such mapping: ' + params.input); - } - } - }, - mapclear: function(cm, params) { vimApi.mapclear(); }, - imapclear: function(cm, params) { vimApi.mapclear('insert'); }, - nmapclear: function(cm, params) { vimApi.mapclear('normal'); }, - vmapclear: function(cm, params) { vimApi.mapclear('visual'); }, - omapclear: function(cm, params) { vimApi.mapclear('operatorPending'); }, - move: function(cm, params) { - commandDispatcher.processCommand(cm, cm.state.vim, { - type: 'motion', - motion: 'moveToLineOrEdgeOfDocument', - motionArgs: { forward: false, explicitRepeat: true, - linewise: true }, - repeatOverride: params.line+1}); - }, - set: function(cm, params) { - var setArgs = params.args; - // Options passed through to the setOption/getOption calls. May be passed in by the - // local/global versions of the set command - var setCfg = params.setCfg || {}; - if (!setArgs || setArgs.length < 1) { - if (cm) { - showConfirm(cm, 'Invalid mapping: ' + params.input); - } - return; + /** @typedef { import("./types").ExParams} ExParams */ + var exCommands = { + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + colorscheme: function(cm, params) { + if (!params.args || params.args.length < 1) { + showConfirm(cm, cm.getOption('theme')); + return; + } + cm.setOption('theme', params.args[0]); + }, + map: function(cm, params, ctx, defaultOnly) { + var mapArgs = params.args; + if (!mapArgs || mapArgs.length < 2) { + if (cm) { + showConfirm(cm, 'Invalid mapping: ' + params.input); } - var expr = setArgs[0].split('='); - var optionName = expr.shift(); - var value = expr.length > 0 ? expr.join('=') : undefined; - var forceGet = false; - var forceToggle = false; - - if (optionName.charAt(optionName.length - 1) == '?') { - // If post-fixed with ?, then the set is actually a get. - if (value) { throw Error('Trailing characters: ' + params.argString); } - optionName = optionName.substring(0, optionName.length - 1); - forceGet = true; - } else if (optionName.charAt(optionName.length - 1) == '!') { - optionName = optionName.substring(0, optionName.length - 1); - forceToggle = true; - } - if (value === undefined && optionName.substring(0, 2) == 'no') { - // To set boolean options to false, the option name is prefixed with - // 'no'. - optionName = optionName.substring(2); - value = false; - } - - var optionIsBoolean = options[optionName] && options[optionName].type == 'boolean'; - if (optionIsBoolean) { - if (forceToggle) { - value = !getOption(optionName, cm, setCfg); - } else if (value == undefined) { - // Calling set with a boolean option sets it to true. - value = true; - } + return; + } + exCommandDispatcher.map(mapArgs[0], mapArgs[1], ctx, defaultOnly); + }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + imap: function(cm, params) { this.map(cm, params, 'insert'); }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + nmap: function(cm, params) { this.map(cm, params, 'normal'); }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + vmap: function(cm, params) { this.map(cm, params, 'visual'); }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + omap: function(cm, params) { this.map(cm, params, 'operatorPending'); }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + noremap: function(cm, params) { this.map(cm, params, undefined, true); }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + inoremap: function(cm, params) { this.map(cm, params, 'insert', true); }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + nnoremap: function(cm, params) { this.map(cm, params, 'normal', true); }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + vnoremap: function(cm, params) { this.map(cm, params, 'visual', true); }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + onoremap: function(cm, params) { this.map(cm, params, 'operatorPending', true); }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params @arg {string} ctx*/ + unmap: function(cm, params, ctx) { + var mapArgs = params.args; + if (!mapArgs || mapArgs.length < 1 || !exCommandDispatcher.unmap(mapArgs[0], ctx)) { + if (cm) { + showConfirm(cm, 'No such mapping: ' + params.input); + } + } + }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + mapclear: function(cm, params) { vimApi.mapclear(); }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + imapclear: function(cm, params) { vimApi.mapclear('insert'); }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + nmapclear: function(cm, params) { vimApi.mapclear('normal'); }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + vmapclear: function(cm, params) { vimApi.mapclear('visual'); }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + omapclear: function(cm, params) { vimApi.mapclear('operatorPending'); }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + move: function(cm, params) { + commandDispatcher.processCommand(cm, cm.state.vim, { + keys: "", + type: 'motion', + motion: 'moveToLineOrEdgeOfDocument', + motionArgs: { forward: false, explicitRepeat: true, linewise: true }, + repeatOverride: params.line+1 + }); + }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + set: function(cm, params) { + var setArgs = params.args; + // Options passed through to the setOption/getOption calls. May be passed in by the + // local/global versions of the set command + var setCfg = params.setCfg || {}; + if (!setArgs || setArgs.length < 1) { + if (cm) { + showConfirm(cm, 'Invalid mapping: ' + params.input); } - // If no value is provided, then we assume this is a get. - if (!optionIsBoolean && value === undefined || forceGet) { - var oldValue = getOption(optionName, cm, setCfg); - if (oldValue instanceof Error) { - showConfirm(cm, oldValue.message); - } else if (oldValue === true || oldValue === false) { - showConfirm(cm, ' ' + (oldValue ? '' : 'no') + optionName); - } else { - showConfirm(cm, ' ' + optionName + '=' + oldValue); - } - } else { - var setOptionReturn = setOption(optionName, value, cm, setCfg); - if (setOptionReturn instanceof Error) { - showConfirm(cm, setOptionReturn.message); - } + return; + } + var expr = setArgs[0].split('='); + var optionName = expr.shift() || ""; + /**@type {string|boolean|undefined} */ + var value = expr.length > 0 ? expr.join('=') : undefined; + var forceGet = false; + var forceToggle = false; + + if (optionName.charAt(optionName.length - 1) == '?') { + // If post-fixed with ?, then the set is actually a get. + if (value) { throw Error('Trailing characters: ' + params.argString); } + optionName = optionName.substring(0, optionName.length - 1); + forceGet = true; + } else if (optionName.charAt(optionName.length - 1) == '!') { + optionName = optionName.substring(0, optionName.length - 1); + forceToggle = true; + } + if (value === undefined && optionName.substring(0, 2) == 'no') { + // To set boolean options to false, the option name is prefixed with + // 'no'. + optionName = optionName.substring(2); + value = false; + } + + var optionIsBoolean = options[optionName] && options[optionName].type == 'boolean'; + if (optionIsBoolean) { + if (forceToggle) { + value = !getOption(optionName, cm, setCfg); + } else if (value == undefined) { + // Calling set with a boolean option sets it to true. + value = true; } - }, - setlocal: function (cm, params) { - // setCfg is passed through to setOption - params.setCfg = {scope: 'local'}; - this.set(cm, params); - }, - setglobal: function (cm, params) { - // setCfg is passed through to setOption - params.setCfg = {scope: 'global'}; - this.set(cm, params); - }, - registers: function(cm, params) { - var regArgs = params.args; - var registers = vimGlobalState.registerController.registers; - var regInfo = '----------Registers----------\n\n'; - if (!regArgs) { - for (var registerName in registers) { - var text = registers[registerName].toString(); - if (text.length) { - regInfo += '"' + registerName + ' ' + text + '\n' - } - } + } + // If no value is provided, then we assume this is a get. + if (!optionIsBoolean && value === undefined || forceGet) { + var oldValue = getOption(optionName, cm, setCfg); + if (oldValue instanceof Error) { + showConfirm(cm, oldValue.message); + } else if (oldValue === true || oldValue === false) { + showConfirm(cm, ' ' + (oldValue ? '' : 'no') + optionName); } else { - var registerName; - regArgs = regArgs.join(''); - for (var i = 0; i < regArgs.length; i++) { - registerName = regArgs.charAt(i); - if (!vimGlobalState.registerController.isValidRegister(registerName)) { - continue; - } - var register = registers[registerName] || new Register(); - regInfo += '"' + registerName + ' ' + register.toString() + '\n' - } + showConfirm(cm, ' ' + optionName + '=' + oldValue); } - showConfirm(cm, regInfo); - }, - sort: function(cm, params) { - var reverse, ignoreCase, unique, number, pattern; - function parseArgs() { - if (params.argString) { - var args = new CodeMirror.StringStream(params.argString); - if (args.eat('!')) { reverse = true; } - if (args.eol()) { return; } - if (!args.eatSpace()) { return 'Invalid arguments'; } - var opts = args.match(/([dinuox]+)?\s*(\/.+\/)?\s*/); - if (!opts && !args.eol()) { return 'Invalid arguments'; } - if (opts[1]) { - ignoreCase = opts[1].indexOf('i') != -1; - unique = opts[1].indexOf('u') != -1; - var decimal = opts[1].indexOf('d') != -1 || opts[1].indexOf('n') != -1 && 1; - var hex = opts[1].indexOf('x') != -1 && 1; - var octal = opts[1].indexOf('o') != -1 && 1; - if (decimal + hex + octal > 1) { return 'Invalid arguments'; } - number = decimal && 'decimal' || hex && 'hex' || octal && 'octal'; - } - if (opts[2]) { - pattern = new RegExp(opts[2].substr(1, opts[2].length - 2), ignoreCase ? 'i' : ''); - } + } else { + var setOptionReturn = setOption(optionName, value, cm, setCfg); + if (setOptionReturn instanceof Error) { + showConfirm(cm, setOptionReturn.message); + } + } + }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + setlocal: function (cm, params) { + // setCfg is passed through to setOption + params.setCfg = {scope: 'local'}; + this.set(cm, params); + }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + setglobal: function (cm, params) { + // setCfg is passed through to setOption + params.setCfg = {scope: 'global'}; + this.set(cm, params); + }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + registers: function(cm, params) { + var regArgs = params.args; + var registers = vimGlobalState.registerController.registers; + var regInfo = '----------Registers----------\n\n'; + if (!regArgs) { + for (var registerName in registers) { + var text = registers[registerName].toString(); + if (text.length) { + regInfo += '"' + registerName + ' ' + text + '\n' } } - var err = parseArgs(); - if (err) { - showConfirm(cm, err + ': ' + params.argString); - return; - } - var lineStart = params.line || cm.firstLine(); - var lineEnd = params.lineEnd || params.line || cm.lastLine(); - if (lineStart == lineEnd) { return; } - var curStart = new Pos(lineStart, 0); - var curEnd = new Pos(lineEnd, lineLength(cm, lineEnd)); - var text = cm.getRange(curStart, curEnd).split('\n'); - var numberRegex = pattern ? pattern : - (number == 'decimal') ? /(-?)([\d]+)/ : - (number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i : - (number == 'octal') ? /([0-7]+)/ : null; - var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null; - var numPart = [], textPart = []; - if (number || pattern) { - for (var i = 0; i < text.length; i++) { - var matchPart = pattern ? text[i].match(pattern) : null; - if (matchPart && matchPart[0] != '') { - numPart.push(matchPart); - } else if (!pattern && numberRegex.exec(text[i])) { - numPart.push(text[i]); - } else { - textPart.push(text[i]); - } - } - } else { - textPart = text; - } - function compareFn(a, b) { - if (reverse) { var tmp; tmp = a; a = b; b = tmp; } - if (ignoreCase) { a = a.toLowerCase(); b = b.toLowerCase(); } - var anum = number && numberRegex.exec(a); - var bnum = number && numberRegex.exec(b); - if (!anum) { return a < b ? -1 : 1; } - anum = parseInt((anum[1] + anum[2]).toLowerCase(), radix); - bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix); - return anum - bnum; - } - function comparePatternFn(a, b) { - if (reverse) { var tmp; tmp = a; a = b; b = tmp; } - if (ignoreCase) { a[0] = a[0].toLowerCase(); b[0] = b[0].toLowerCase(); } - return (a[0] < b[0]) ? -1 : 1; - } - numPart.sort(pattern ? comparePatternFn : compareFn); - if (pattern) { - for (var i = 0; i < numPart.length; i++) { - numPart[i] = numPart[i].input; + } else { + var registerNames = regArgs.join(''); + for (var i = 0; i < registerNames.length; i++) { + var registerName = registerNames.charAt(i); + if (!vimGlobalState.registerController.isValidRegister(registerName)) { + continue; } - } else if (!number) { textPart.sort(compareFn); } - text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart); - if (unique) { // Remove duplicate lines - var textOld = text; - var lastLine; - text = []; - for (var i = 0; i < textOld.length; i++) { - if (textOld[i] != lastLine) { - text.push(textOld[i]); - } - lastLine = textOld[i]; + var register = registers[registerName] || new Register(); + regInfo += '"' + registerName + ' ' + register.toString() + '\n' + } + } + showConfirm(cm, regInfo); + }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + sort: function(cm, params) { + var reverse, ignoreCase, unique, number, pattern; + function parseArgs() { + if (params.argString) { + var args = new CodeMirror.StringStream(params.argString); + if (args.eat('!')) { reverse = true; } + if (args.eol()) { return; } + if (!args.eatSpace()) { return 'Invalid arguments'; } + var opts = args.match(/([dinuox]+)?\s*(\/.+\/)?\s*/); + if (!opts || !args.eol()) { return 'Invalid arguments'; } + if (opts[1]) { + ignoreCase = opts[1].indexOf('i') != -1; + unique = opts[1].indexOf('u') != -1; + var decimal = opts[1].indexOf('d') != -1 || opts[1].indexOf('n') != -1; + var hex = opts[1].indexOf('x') != -1; + var octal = opts[1].indexOf('o') != -1; + if (Number(decimal) + Number(hex) + Number(octal) > 1) { return 'Invalid arguments'; } + number = decimal && 'decimal' || hex && 'hex' || octal && 'octal'; + } + if (opts[2]) { + pattern = new RegExp(opts[2].substr(1, opts[2].length - 2), ignoreCase ? 'i' : ''); + } + } + } + var err = parseArgs(); + if (err) { + showConfirm(cm, err + ': ' + params.argString); + return; + } + var lineStart = params.line || cm.firstLine(); + var lineEnd = params.lineEnd || params.line || cm.lastLine(); + if (lineStart == lineEnd) { return; } + var curStart = new Pos(lineStart, 0); + var curEnd = new Pos(lineEnd, lineLength(cm, lineEnd)); + var text = cm.getRange(curStart, curEnd).split('\n'); + var numberRegex = + (number == 'decimal') ? /(-?)([\d]+)/ : + (number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i : + (number == 'octal') ? /([0-7]+)/ : null; + var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : undefined; + var numPart = [], textPart = []; + if (number || pattern) { + for (var i = 0; i < text.length; i++) { + var matchPart = pattern ? text[i].match(pattern) : null; + if (matchPart && matchPart[0] != '') { + numPart.push(matchPart); + } else if (numberRegex && numberRegex.exec(text[i])) { + numPart.push(text[i]); + } else { + textPart.push(text[i]); } } - cm.replaceRange(text.join('\n'), curStart, curEnd); - }, - vglobal: function(cm, params) { - // global inspects params.commandName - this.global(cm, params); - }, - normal: function(cm, params) { - var argString = params.argString; - if (argString && argString[0] == '!') { - argString = argString.slice(1); - noremap = true; - } - argString = argString.trimStart(); - if (!argString) { - showConfirm(cm, 'Argument is required.'); - return; - } - var line = params.line; - if (typeof line == 'number') { - var lineEnd = isNaN(params.lineEnd) ? line : params.lineEnd; - for (var i = line; i <= lineEnd; i++) { - cm.setCursor(i, 0); - doKeyToKey(cm, params.argString.trimStart()); - if (cm.state.vim.insertMode) { - exitInsertMode(cm, true); - } - } - } else { + } else { + textPart = text; + } + /** @arg {string} a @arg {string} b */ + function compareFn(a, b) { + if (reverse) { var tmp; tmp = a; a = b; b = tmp; } + if (ignoreCase) { a = a.toLowerCase(); b = b.toLowerCase(); } + var amatch = numberRegex && numberRegex.exec(a); + var bmatch = numberRegex && numberRegex.exec(b); + if (!amatch || !bmatch) { return a < b ? -1 : 1; } + var anum = parseInt((amatch[1] + amatch[2]).toLowerCase(), radix); + var bnum = parseInt((bmatch[1] + bmatch[2]).toLowerCase(), radix); + return anum - bnum; + } + /** @arg {string[]} a @arg {string[]} b */ + function comparePatternFn(a, b) { + if (reverse) { var tmp; tmp = a; a = b; b = tmp; } + if (ignoreCase) { a[0] = a[0].toLowerCase(); b[0] = b[0].toLowerCase(); } + return (a[0] < b[0]) ? -1 : 1; + } + // @ts-ignore + numPart.sort(pattern ? comparePatternFn : compareFn); + if (pattern) { + for (var i = 0; i < numPart.length; i++) { + // @ts-ignore + numPart[i] = numPart[i].input; + } + } else if (!number) { textPart.sort(compareFn); } + text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart); + if (unique) { // Remove duplicate lines + var textOld = text; + var lastLine; + text = []; + for (var i = 0; i < textOld.length; i++) { + if (textOld[i] != lastLine) { + text.push(textOld[i]); + } + lastLine = textOld[i]; + } + } + cm.replaceRange(text.join('\n'), curStart, curEnd); + }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + vglobal: function(cm, params) { + // global inspects params.commandName + this.global(cm, params); + }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + normal: function(cm, params) { + var argString = params.argString; + if (argString && argString[0] == '!') { + argString = argString.slice(1); + noremap = true; + } + argString = argString.trimStart(); + if (!argString) { + showConfirm(cm, 'Argument is required.'); + return; + } + var line = params.line; + if (typeof line == 'number') { + var lineEnd = isNaN(params.lineEnd) ? line : params.lineEnd; + for (var i = line; i <= lineEnd; i++) { + cm.setCursor(i, 0); doKeyToKey(cm, params.argString.trimStart()); if (cm.state.vim.insertMode) { exitInsertMode(cm, true); } } - }, - global: function(cm, params) { - // a global command is of the form - // :[range]g/pattern/[cmd] - // argString holds the string /pattern/[cmd] - var argString = params.argString; - if (!argString) { - showConfirm(cm, 'Regular Expression missing from global'); + } else { + doKeyToKey(cm, params.argString.trimStart()); + if (cm.state.vim.insertMode) { + exitInsertMode(cm, true); + } + } + }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + global: function(cm, params) { + // a global command is of the form + // :[range]g/pattern/[cmd] + // argString holds the string /pattern/[cmd] + var argString = params.argString; + if (!argString) { + showConfirm(cm, 'Regular Expression missing from global'); + return; + } + var inverted = params.commandName[0] === 'v'; + if (argString[0] === '!' && params.commandName[0] === 'g') { + inverted = true; + argString = argString.slice(1); + } + // range is specified here + var lineStart = (params.line !== undefined) ? params.line : cm.firstLine(); + var lineEnd = params.lineEnd || params.line || cm.lastLine(); + // get the tokens from argString + var tokens = splitBySlash(argString); + var regexPart = argString, cmd = ""; + if (tokens && tokens.length) { + regexPart = tokens[0]; + cmd = tokens.slice(1, tokens.length).join('/'); + } + if (regexPart) { + // If regex part is empty, then use the previous query. Otherwise + // use the regex part as the new query. + try { + updateSearchQuery(cm, regexPart, true /** ignoreCase */, + true /** smartCase */); + } catch (e) { + showConfirm(cm, 'Invalid regex: ' + regexPart); return; } - var inverted = params.commandName[0] === 'v'; - if (argString[0] === '!' && params.commandName[0] === 'g') { - inverted = true; - argString = argString.slice(1); + } + // now that we have the regexPart, search for regex matches in the + // specified range of lines + var query = getSearchState(cm).getQuery(); + /**@type {(string|import("./types").LineHandle)[]}*/ + var matchedLines = []; + for (var i = lineStart; i <= lineEnd; i++) { + var line = cm.getLine(i); + var matched = query.test(line); + if (matched !== inverted) { + matchedLines.push(cmd ? cm.getLineHandle(i) : line); } - // range is specified here - var lineStart = (params.line !== undefined) ? params.line : cm.firstLine(); - var lineEnd = params.lineEnd || params.line || cm.lastLine(); - // get the tokens from argString - var tokens = splitBySlash(argString); - var regexPart = argString, cmd; - if (tokens.length) { - regexPart = tokens[0]; - cmd = tokens.slice(1, tokens.length).join('/'); - } - if (regexPart) { - // If regex part is empty, then use the previous query. Otherwise - // use the regex part as the new query. - try { - updateSearchQuery(cm, regexPart, true /** ignoreCase */, - true /** smartCase */); - } catch (e) { - showConfirm(cm, 'Invalid regex: ' + regexPart); - return; + } + // if there is no [cmd], just display the list of matched lines + if (!cmd) { + showConfirm(cm, matchedLines.join('\n')); + return; + } + var index = 0; + var nextCommand = function() { + if (index < matchedLines.length) { + var lineHandle = matchedLines[index++]; + var lineNum = cm.getLineNumber(lineHandle); + if (lineNum == null) { + nextCommand(); + return; } + var command = (lineNum + 1) + cmd; + exCommandDispatcher.processCommand(cm, command, { + callback: nextCommand + }); + } else if (cm.releaseLineHandles) { + cm.releaseLineHandles(); } - // now that we have the regexPart, search for regex matches in the - // specified range of lines - var query = getSearchState(cm).getQuery(); - var matchedLines = []; - for (var i = lineStart; i <= lineEnd; i++) { - var line = cm.getLine(i); - var matched = query.test(line); - if (matched !== inverted) { - matchedLines.push(cmd ? cm.getLineHandle(i) : line); + }; + nextCommand(); + }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + substitute: function(cm, params) { + if (!cm.getSearchCursor) { + throw new Error('Search feature not available. Requires searchcursor.js or ' + + 'any other getSearchCursor implementation.'); + } + var argString = params.argString; + var tokens = argString ? splitBySeparator(argString, argString[0]) : []; + var regexPart = '', replacePart = '', trailing, flagsPart, count; + var confirm = false; // Whether to confirm each replace. + var global = false; // True to replace all instances on a line, false to replace only 1. + if (tokens && tokens.length) { + regexPart = tokens[0]; + if (getOption('pcre') && regexPart !== '') { + regexPart = new RegExp(regexPart).source; //normalize not escaped characters + } + replacePart = tokens[1]; + if (replacePart !== undefined) { + if (getOption('pcre')) { + replacePart = unescapeRegexReplace(replacePart.replace(/([^\\])&/g,"$1$$&")); + } else { + replacePart = translateRegexReplace(replacePart); } + vimGlobalState.lastSubstituteReplacePart = replacePart; } - // if there is no [cmd], just display the list of matched lines - if (!cmd) { - showConfirm(cm, matchedLines.join('\n')); + trailing = tokens[2] ? tokens[2].split(' ') : []; + } else { + // either the argString is empty or its of the form ' hello/world' + // actually splitBySlash returns a list of tokens + // only if the string starts with a '/' + if (argString && argString.length) { + showConfirm(cm, 'Substitutions should be of the form ' + + ':s/pattern/replace/'); return; } - var index = 0; - var nextCommand = function() { - if (index < matchedLines.length) { - var lineHandle = matchedLines[index++]; - var lineNum = cm.getLineNumber(lineHandle); - if (lineNum == null) { - nextCommand(); - return; - } - var command = (lineNum + 1) + cmd; - exCommandDispatcher.processCommand(cm, command, { - callback: nextCommand - }); - } else if (cm.releaseLineHandles) { - cm.releaseLineHandles(); - } - }; - nextCommand(); - }, - substitute: function(cm, params) { - if (!cm.getSearchCursor) { - throw new Error('Search feature not available. Requires searchcursor.js or ' + - 'any other getSearchCursor implementation.'); - } - var argString = params.argString; - var tokens = argString ? splitBySeparator(argString, argString[0]) : []; - var regexPart, replacePart = '', trailing, flagsPart, count; - var confirm = false; // Whether to confirm each replace. - var global = false; // True to replace all instances on a line, false to replace only 1. - if (tokens.length) { - regexPart = tokens[0]; - if (getOption('pcre') && regexPart !== '') { - regexPart = new RegExp(regexPart).source; //normalize not escaped characters - } - replacePart = tokens[1]; - if (replacePart !== undefined) { - if (getOption('pcre')) { - replacePart = unescapeRegexReplace(replacePart.replace(/([^\\])&/g,"$1$$&")); - } else { - replacePart = translateRegexReplace(replacePart); - } - vimGlobalState.lastSubstituteReplacePart = replacePart; - } - trailing = tokens[2] ? tokens[2].split(' ') : []; - } else { - // either the argString is empty or its of the form ' hello/world' - // actually splitBySlash returns a list of tokens - // only if the string starts with a '/' - if (argString && argString.length) { - showConfirm(cm, 'Substitutions should be of the form ' + - ':s/pattern/replace/'); - return; + } + // After the 3rd slash, we can have flags followed by a space followed + // by count. + if (trailing) { + flagsPart = trailing[0]; + count = parseInt(trailing[1]); + if (flagsPart) { + if (flagsPart.indexOf('c') != -1) { + confirm = true; } - } - // After the 3rd slash, we can have flags followed by a space followed - // by count. - if (trailing) { - flagsPart = trailing[0]; - count = parseInt(trailing[1]); - if (flagsPart) { - if (flagsPart.indexOf('c') != -1) { - confirm = true; - } - if (flagsPart.indexOf('g') != -1) { - global = true; - } - if (getOption('pcre')) { - regexPart = regexPart + '/' + flagsPart; - } else { - regexPart = regexPart.replace(/\//g, "\\/") + '/' + flagsPart; - } + if (flagsPart.indexOf('g') != -1) { + global = true; } - } - if (regexPart) { - // If regex part is empty, then use the previous query. Otherwise use - // the regex part as the new query. - try { - updateSearchQuery(cm, regexPart, true /** ignoreCase */, - true /** smartCase */); - } catch (e) { - showConfirm(cm, 'Invalid regex: ' + regexPart); - return; + if (getOption('pcre')) { + regexPart = regexPart + '/' + flagsPart; + } else { + regexPart = regexPart.replace(/\//g, "\\/") + '/' + flagsPart; } } - replacePart = replacePart || vimGlobalState.lastSubstituteReplacePart; - if (replacePart === undefined) { - showConfirm(cm, 'No previous substitute regular expression'); + } + if (regexPart) { + // If regex part is empty, then use the previous query. Otherwise use + // the regex part as the new query. + try { + updateSearchQuery(cm, regexPart, true /** ignoreCase */, + true /** smartCase */); + } catch (e) { + showConfirm(cm, 'Invalid regex: ' + regexPart); return; } - var state = getSearchState(cm); - var query = state.getQuery(); - var lineStart = (params.line !== undefined) ? params.line : cm.getCursor().line; - var lineEnd = params.lineEnd || lineStart; - if (lineStart == cm.firstLine() && lineEnd == cm.lastLine()) { - lineEnd = Infinity; - } - if (count) { - lineStart = lineEnd; - lineEnd = lineStart + count - 1; - } - var startPos = clipCursorToContent(cm, new Pos(lineStart, 0)); - var cursor = cm.getSearchCursor(query, startPos); - doReplace(cm, confirm, global, lineStart, lineEnd, cursor, query, replacePart, params.callback); - }, - startinsert: function(cm, params) { - doKeyToKey(cm, params.argString == '!' ? 'A' : 'i', {}); - }, - redo: CodeMirror.commands.redo, - undo: CodeMirror.commands.undo, - write: function(cm) { - if (CodeMirror.commands.save) { - // If a save command is defined, call it. - CodeMirror.commands.save(cm); - } else if (cm.save) { - // Saves to text area if no save command is defined and cm.save() is available. - cm.save(); - } - }, - nohlsearch: function(cm) { - clearSearchHighlight(cm); - }, - yank: function (cm) { - var cur = copyCursor(cm.getCursor()); - var line = cur.line; - var lineText = cm.getLine(line); - vimGlobalState.registerController.pushText( - '0', 'yank', lineText, true, true); - }, - delete: function(cm, params) { - var line = params.selectionLine; - var lineEnd = isNaN(params.selectionLineEnd) ? line : params.selectionLineEnd; - operators.delete(cm, {linewise: true}, [ - { anchor: new Pos(line, 0), - head: new Pos(lineEnd + 1, 0) } - ]); - }, - join: function(cm, params) { - var line = params.selectionLine; - var lineEnd = isNaN(params.selectionLineEnd) ? line : params.selectionLineEnd; - cm.setCursor(new Pos(line, 0)); - actions.joinLines(cm, {repeat: lineEnd - line}, cm.state.vim); - }, - delmarks: function(cm, params) { - if (!params.argString || !trim(params.argString)) { - showConfirm(cm, 'Argument required'); + } + replacePart = replacePart || vimGlobalState.lastSubstituteReplacePart; + if (replacePart === undefined) { + showConfirm(cm, 'No previous substitute regular expression'); + return; + } + var state = getSearchState(cm); + var query = state.getQuery(); + var lineStart = (params.line !== undefined) ? params.line : cm.getCursor().line; + var lineEnd = params.lineEnd || lineStart; + if (lineStart == cm.firstLine() && lineEnd == cm.lastLine()) { + lineEnd = Infinity; + } + if (count) { + lineStart = lineEnd; + lineEnd = lineStart + count - 1; + } + var startPos = clipCursorToContent(cm, new Pos(lineStart, 0)); + var cursor = cm.getSearchCursor(query, startPos); + doReplace(cm, confirm, global, lineStart, lineEnd, cursor, query, replacePart, params.callback); + }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + startinsert: function(cm, params) { + doKeyToKey(cm, params.argString == '!' ? 'A' : 'i', {}); + }, + redo: CodeMirror.commands.redo, + undo: CodeMirror.commands.undo, + /** @arg {CodeMirrorV} cm */ + write: function(cm) { + if (CodeMirror.commands.save) { + CodeMirror.commands.save(cm); + } else if (cm.save) { + // Saves to text area if no save command is defined and cm.save() is available. + cm.save(); + } + }, + /** @arg {CodeMirrorV} cm */ + nohlsearch: function(cm) { + clearSearchHighlight(cm); + }, + /** @arg {CodeMirrorV} cm */ + yank: function (cm) { + var cur = copyCursor(cm.getCursor()); + var line = cur.line; + var lineText = cm.getLine(line); + vimGlobalState.registerController.pushText( + '0', 'yank', lineText, true, true); + }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + delete: function(cm, params) { + var line = params.selectionLine; + var lineEnd = isNaN(params.selectionLineEnd) ? line : params.selectionLineEnd; + operators.delete(cm, {linewise: true}, [ + { anchor: new Pos(line, 0), + head: new Pos(lineEnd + 1, 0) } + ]); + }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + join: function(cm, params) { + var line = params.selectionLine; + var lineEnd = isNaN(params.selectionLineEnd) ? line : params.selectionLineEnd; + cm.setCursor(new Pos(line, 0)); + actions.joinLines(cm, {repeat: lineEnd - line}, cm.state.vim); + }, + /** @arg {CodeMirrorV} cm @arg {ExParams} params*/ + delmarks: function(cm, params) { + if (!params.argString || !trim(params.argString)) { + showConfirm(cm, 'Argument required'); + return; + } + + var state = cm.state.vim; + var stream = new CodeMirror.StringStream(trim(params.argString)); + while (!stream.eol()) { + stream.eatSpace(); + + // Record the streams position at the beginning of the loop for use + // in error messages. + var count = stream.pos; + + if (!stream.match(/[a-zA-Z]/, false)) { + showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); return; } - var state = cm.state.vim; - var stream = new CodeMirror.StringStream(trim(params.argString)); - while (!stream.eol()) { - stream.eatSpace(); - - // Record the streams position at the beginning of the loop for use - // in error messages. - var count = stream.pos; + var sym = stream.next(); + // Check if this symbol is part of a range + if (stream.match('-', true)) { + // This symbol is part of a range. + // The range must terminate at an alphabetic character. if (!stream.match(/[a-zA-Z]/, false)) { showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); return; } - var sym = stream.next(); - // Check if this symbol is part of a range - if (stream.match('-', true)) { - // This symbol is part of a range. - - // The range must terminate at an alphabetic character. - if (!stream.match(/[a-zA-Z]/, false)) { + var startMark = sym; + var finishMark = stream.next(); + // The range must terminate at an alphabetic character which + // shares the same case as the start of the range. + if (startMark && finishMark && isLowerCase(startMark) == isLowerCase(finishMark)) { + var start = startMark.charCodeAt(0); + var finish = finishMark.charCodeAt(0); + if (start >= finish) { showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); return; } - var startMark = sym; - var finishMark = stream.next(); - // The range must terminate at an alphabetic character which - // shares the same case as the start of the range. - if (isLowerCase(startMark) && isLowerCase(finishMark) || - isUpperCase(startMark) && isUpperCase(finishMark)) { - var start = startMark.charCodeAt(0); - var finish = finishMark.charCodeAt(0); - if (start >= finish) { - showConfirm(cm, 'Invalid argument: ' + params.argString.substring(count)); - return; - } - - // Because marks are always ASCII values, and we have - // determined that they are the same case, we can use - // their char codes to iterate through the defined range. - for (var j = 0; j <= finish - start; j++) { - var mark = String.fromCharCode(start + j); - delete state.marks[mark]; - } - } else { - showConfirm(cm, 'Invalid argument: ' + startMark + '-'); - return; + // Because marks are always ASCII values, and we have + // determined that they are the same case, we can use + // their char codes to iterate through the defined range. + for (var j = 0; j <= finish - start; j++) { + var mark = String.fromCharCode(start + j); + delete state.marks[mark]; } } else { - // This symbol is a valid mark, and is not part of a range. - delete state.marks[sym]; + showConfirm(cm, 'Invalid argument: ' + startMark + '-'); + return; } + } else if (sym) { + // This symbol is a valid mark, and is not part of a range. + delete state.marks[sym]; } } - }; - - var exCommandDispatcher = new ExCommandDispatcher(); + } + }; - /** - * @param {CodeMirror} cm CodeMirror instance we are in. - * @param {boolean} confirm Whether to confirm each replace. - * @param {Cursor} lineStart Line to start replacing from. - * @param {Cursor} lineEnd Line to stop replacing at. - * @param {RegExp} query Query for performing matches with. - * @param {string} replaceWith Text to replace matches with. May contain $1, - * $2, etc for replacing captured groups using JavaScript replace. - * @param {function()} callback A callback for when the replace is done. - */ - function doReplace(cm, confirm, global, lineStart, lineEnd, searchCursor, query, - replaceWith, callback) { - // Set up all the functions. - cm.state.vim.exMode = true; - var done = false; - var lastPos, modifiedLineNumber, joined; - function replaceAll() { - cm.operation(function() { - while (!done) { - replace(); - next(); - } - stop(); - }); - } - function replace() { - var text = cm.getRange(searchCursor.from(), searchCursor.to()); - var newText = text.replace(query, replaceWith); - var unmodifiedLineNumber = searchCursor.to().line; - searchCursor.replace(newText); - modifiedLineNumber = searchCursor.to().line; - lineEnd += modifiedLineNumber - unmodifiedLineNumber; - joined = modifiedLineNumber < unmodifiedLineNumber; - } - function findNextValidMatch() { - var lastMatchTo = lastPos && copyCursor(searchCursor.to()); - var match = searchCursor.findNext(); - if (match && !match[0] && lastMatchTo && cursorEqual(searchCursor.from(), lastMatchTo)) { - match = searchCursor.findNext(); - } - return match; - } - function next() { - // The below only loops to skip over multiple occurrences on the same - // line when 'global' is not true. - while(findNextValidMatch() && - isInRange(searchCursor.from(), lineStart, lineEnd)) { - if (!global && searchCursor.from().line == modifiedLineNumber && !joined) { - continue; - } - cm.scrollIntoView(searchCursor.from(), 30); - cm.setSelection(searchCursor.from(), searchCursor.to()); - lastPos = searchCursor.from(); - done = false; - return; - } - done = true; - } - function stop(close) { - if (close) { close(); } - cm.focus(); - if (lastPos) { - cm.setCursor(lastPos); - var vim = cm.state.vim; - vim.exMode = false; - vim.lastHPos = vim.lastHSPos = lastPos.ch; - } - if (callback) { callback(); } - } - function onPromptKeyDown(e, _value, close) { - // Swallow all keys. - CodeMirror.e_stop(e); - var keyName = vimKeyFromEvent(e); - switch (keyName) { - case 'y': - replace(); next(); break; - case 'n': - next(); break; - case 'a': - // replaceAll contains a call to close of its own. We don't want it - // to fire too early or multiple times. - var savedCallback = callback; - callback = undefined; - cm.operation(replaceAll); - callback = savedCallback; - break; - case 'l': - replace(); - // fall through and exit. - case 'q': - case '': - case '': - case '': - stop(close); - break; - } - if (done) { stop(close); } - return true; - } + var exCommandDispatcher = new ExCommandDispatcher(); - // Actually do replace. - next(); - if (done) { - showConfirm(cm, 'No matches for ' + query.source); - return; - } - if (!confirm) { - replaceAll(); - if (callback) { callback(); } - return; - } - showPrompt(cm, { - prefix: dom('span', 'replace with ', dom('strong', replaceWith), ' (y/n/a/q/l)'), - onKeyDown: onPromptKeyDown +/** + * @arg {CodeMirrorV} cm CodeMirror instance we are in. + * @arg {boolean} confirm Whether to confirm each replace. + * @arg {boolean} global + * @arg {number} lineStart Line to start replacing from. + * @arg {number} lineEnd Line to stop replacing at. + * @arg {RegExp} query Query for performing matches with. + * @arg {string} replaceWith Text to replace matches with. May contain $1, + * $2, etc for replacing captured groups using JavaScript replace. + * @arg {function} [callback] A callback for when the replace is done. + */ + function doReplace(cm, confirm, global, lineStart, lineEnd, searchCursor, query, + replaceWith, callback) { + // Set up all the functions. + cm.state.vim.exMode = true; + var done = false; + + /** @type {Pos}*/ var lastPos; + /** @type {number}*/ var modifiedLineNumber + /** @type {boolean}*/var joined; + function replaceAll() { + cm.operation(function() { + while (!done) { + replace(); + next(); + } + stop(); }); } - - function exitInsertMode(cm, keepCursor) { - var vim = cm.state.vim; - var macroModeState = vimGlobalState.macroModeState; - var insertModeChangeRegister = vimGlobalState.registerController.getRegister('.'); - var isPlaying = macroModeState.isPlaying; - var lastChange = macroModeState.lastInsertModeChanges; - if (!isPlaying) { - cm.off('change', onChange); - if (vim.insertEnd) vim.insertEnd.clear(); - vim.insertEnd = null; - CodeMirror.off(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); + function replace() { + var text = cm.getRange(searchCursor.from(), searchCursor.to()); + var newText = text.replace(query, replaceWith); + var unmodifiedLineNumber = searchCursor.to().line; + searchCursor.replace(newText); + modifiedLineNumber = searchCursor.to().line; + lineEnd += modifiedLineNumber - unmodifiedLineNumber; + joined = modifiedLineNumber < unmodifiedLineNumber; + } + function findNextValidMatch() { + var lastMatchTo = lastPos && copyCursor(searchCursor.to()); + var match = searchCursor.findNext(); + if (match && !match[0] && lastMatchTo && cursorEqual(searchCursor.from(), lastMatchTo)) { + match = searchCursor.findNext(); } - if (!isPlaying && vim.insertModeRepeat > 1) { - // Perform insert mode repeat for commands like 3,a and 3,o. - repeatLastEdit(cm, vim, vim.insertModeRepeat - 1, - true /** repeatForInsert */); - vim.lastEditInputState.repeatOverride = vim.insertModeRepeat; + return match; + } + function next() { + // The below only loops to skip over multiple occurrences on the same + // line when 'global' is not true. + while(findNextValidMatch() && + isInRange(searchCursor.from(), lineStart, lineEnd)) { + if (!global && searchCursor.from().line == modifiedLineNumber && !joined) { + continue; + } + cm.scrollIntoView(searchCursor.from(), 30); + cm.setSelection(searchCursor.from(), searchCursor.to()); + lastPos = searchCursor.from(); + done = false; + return; } - delete vim.insertModeRepeat; - vim.insertMode = false; - if (!keepCursor) { - cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1); + done = true; + } + /** @arg {(() => void) | undefined} [close] */ + function stop(close) { + if (close) { close(); } + cm.focus(); + if (lastPos) { + cm.setCursor(lastPos); + var vim = cm.state.vim; + vim.exMode = false; + vim.lastHPos = vim.lastHSPos = lastPos.ch; } - cm.setOption('keyMap', 'vim'); - cm.setOption('disableInput', true); - cm.toggleOverwrite(false); // exit replace mode if we were in it. - // update the ". register before exiting insert mode - insertModeChangeRegister.setText(lastChange.changes.join('')); - CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); - if (macroModeState.isRecording) { - logInsertModeChange(macroModeState); + if (callback) { callback(); } + } + /** @arg {KeyboardEvent} e @arg {any} _value @arg {any} close */ + function onPromptKeyDown(e, _value, close) { + // Swallow all keys. + CodeMirror.e_stop(e); + var keyName = vimKeyFromEvent(e); + switch (keyName) { + case 'y': + replace(); next(); break; + case 'n': + next(); break; + case 'a': + // replaceAll contains a call to close of its own. We don't want it + // to fire too early or multiple times. + var savedCallback = callback; + callback = undefined; + cm.operation(replaceAll); + callback = savedCallback; + break; + case 'l': + replace(); + // fall through and exit. + case 'q': + case '': + case '': + case '': + stop(close); + break; } + if (done) { stop(close); } + return true; } - function _mapCommand(command) { - defaultKeymap.unshift(command); + // Actually do replace. + next(); + if (done) { + showConfirm(cm, 'No matches for ' + query.source); + return; } + if (!confirm) { + replaceAll(); + if (callback) { callback(); } + return; + } + showPrompt(cm, { + prefix: dom('span', 'replace with ', dom('strong', replaceWith), ' (y/n/a/q/l)'), + onKeyDown: onPromptKeyDown + }); + } - function mapCommand(keys, type, name, args, extra) { - var command = {keys: keys, type: type}; - command[type] = name; - command[type + "Args"] = args; - for (var key in extra) - command[key] = extra[key]; - _mapCommand(command); + /** @arg {CodeMirrorV} cm @arg {boolean} [keepCursor] */ + function exitInsertMode(cm, keepCursor) { + var vim = cm.state.vim; + var macroModeState = vimGlobalState.macroModeState; + var insertModeChangeRegister = vimGlobalState.registerController.getRegister('.'); + var isPlaying = macroModeState.isPlaying; + var lastChange = macroModeState.lastInsertModeChanges; + if (!isPlaying) { + cm.off('change', onChange); + if (vim.insertEnd) vim.insertEnd.clear(); + vim.insertEnd = undefined; + CodeMirror.off(cm.getInputField(), 'keydown', onKeyEventTargetKeyDown); + } + if (!isPlaying && vim.insertModeRepeat > 1) { + // Perform insert mode repeat for commands like 3,a and 3,o. + repeatLastEdit(cm, vim, vim.insertModeRepeat - 1, + true /** repeatForInsert */); + // @ts-ignore + vim.lastEditInputState.repeatOverride = vim.insertModeRepeat; + } + delete vim.insertModeRepeat; + vim.insertMode = false; + if (!keepCursor) { + cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1); + } + cm.setOption('keyMap', 'vim'); + cm.setOption('disableInput', true); + cm.toggleOverwrite(false); // exit replace mode if we were in it. + // update the ". register before exiting insert mode + insertModeChangeRegister.setText(lastChange.changes.join('')); + CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"}); + if (macroModeState.isRecording) { + logInsertModeChange(macroModeState); } + } - // The timeout in milliseconds for the two-character ESC keymap should be - // adjusted according to your typing speed to prevent false positives. - defineOption('insertModeEscKeysTimeout', 200, 'number'); + /** @arg {vimKey} command*/ + function _mapCommand(command) { + defaultKeymap.unshift(command); + } + /** + * @arg {string} keys + * @arg {string} type + * @arg {string} name + * @arg {any} args + * @arg {{ [x: string]: any; }} extra + **/ + function mapCommand(keys, type, name, args, extra) { + /**@type{any} */ + var command = {keys: keys, type: type}; + command[type] = name; + command[type + "Args"] = args; + for (var key in extra) + command[key] = extra[key]; + _mapCommand(command); + } - function executeMacroRegister(cm, vim, macroModeState, registerName) { - var register = vimGlobalState.registerController.getRegister(registerName); - if (registerName == ':') { - // Read-only register containing last Ex command. - if (register.keyBuffer[0]) { - exCommandDispatcher.processCommand(cm, register.keyBuffer[0]); - } - macroModeState.isPlaying = false; - return; + // The timeout in milliseconds for the two-character ESC keymap should be + // adjusted according to your typing speed to prevent false positives. + defineOption('insertModeEscKeysTimeout', 200, 'number'); + + + /** + * @arg {CodeMirrorV} cm + * @arg {vimState} vim + * @arg {MacroModeState} macroModeState + * @arg {string} registerName + */ + function executeMacroRegister(cm, vim, macroModeState, registerName) { + var register = vimGlobalState.registerController.getRegister(registerName); + if (registerName == ':') { + // Read-only register containing last Ex command. + if (register.keyBuffer[0]) { + exCommandDispatcher.processCommand(cm, register.keyBuffer[0]); } - var keyBuffer = register.keyBuffer; - var imc = 0; - macroModeState.isPlaying = true; - macroModeState.replaySearchQueries = register.searchQueries.slice(0); - for (var i = 0; i < keyBuffer.length; i++) { - var text = keyBuffer[i]; - var match, key; - while (text) { - // Pull off one command key, which is either a single character - // or a special sequence wrapped in '<' and '>', e.g. ''. - match = (/<\w+-.+?>|<\w+>|./).exec(text); - key = match[0]; - text = text.substring(match.index + key.length); - vimApi.handleKey(cm, key, 'macro'); - if (vim.insertMode) { - var changes = register.insertModeChanges[imc++].changes; - vimGlobalState.macroModeState.lastInsertModeChanges.changes = - changes; - repeatInsertModeChanges(cm, changes, 1); - exitInsertMode(cm); - } + macroModeState.isPlaying = false; + return; + } + var keyBuffer = register.keyBuffer; + var imc = 0; + macroModeState.isPlaying = true; + macroModeState.replaySearchQueries = register.searchQueries.slice(0); + for (var i = 0; i < keyBuffer.length; i++) { + var text = keyBuffer[i]; + var match, key; + var keyRe = /<(?:[CSMA]-)*\w+>|./gi; + while ((match = keyRe.exec(text))) { + // Pull off one command key, which is either a single character + // or a special sequence wrapped in '<' and '>', e.g. ''. + key = match[0]; + vimApi.handleKey(cm, key, 'macro'); + if (vim.insertMode) { + var changes = register.insertModeChanges[imc++].changes; + vimGlobalState.macroModeState.lastInsertModeChanges.changes = changes; + repeatInsertModeChanges(cm, changes, 1); + exitInsertMode(cm); } } - macroModeState.isPlaying = false; } + macroModeState.isPlaying = false; + } - function logKey(macroModeState, key) { - if (macroModeState.isPlaying) { return; } - var registerName = macroModeState.latestRegister; - var register = vimGlobalState.registerController.getRegister(registerName); - if (register) { - register.pushText(key); - } + /** @arg {MacroModeState} macroModeState @arg {any} key */ + function logKey(macroModeState, key) { + if (macroModeState.isPlaying) { return; } + var registerName = macroModeState.latestRegister; + var register = vimGlobalState.registerController.getRegister(registerName); + if (register) { + register.pushText(key); } + } - function logInsertModeChange(macroModeState) { - if (macroModeState.isPlaying) { return; } - var registerName = macroModeState.latestRegister; - var register = vimGlobalState.registerController.getRegister(registerName); - if (register && register.pushInsertModeChanges) { - register.pushInsertModeChanges(macroModeState.lastInsertModeChanges); - } + /** @arg {MacroModeState} macroModeState */ + function logInsertModeChange(macroModeState) { + if (macroModeState.isPlaying) { return; } + var registerName = macroModeState.latestRegister; + var register = vimGlobalState.registerController.getRegister(registerName); + if (register && register.pushInsertModeChanges) { + register.pushInsertModeChanges(macroModeState.lastInsertModeChanges); } + } - function logSearchQuery(macroModeState, query) { - if (macroModeState.isPlaying) { return; } - var registerName = macroModeState.latestRegister; - var register = vimGlobalState.registerController.getRegister(registerName); - if (register && register.pushSearchQuery) { - register.pushSearchQuery(query); - } + /** @arg {MacroModeState} macroModeState @arg {string} query */ + function logSearchQuery(macroModeState, query) { + if (macroModeState.isPlaying) { return; } + var registerName = macroModeState.latestRegister; + var register = vimGlobalState.registerController.getRegister(registerName); + if (register && register.pushSearchQuery) { + register.pushSearchQuery(query); } + } - /** - * Listens for changes made in insert mode. - * Should only be active in insert mode. - */ - function onChange(cm, changeObj) { - var macroModeState = vimGlobalState.macroModeState; - var lastChange = macroModeState.lastInsertModeChanges; - if (!macroModeState.isPlaying) { - var vim = cm.state.vim; - while(changeObj) { - lastChange.expectCursorActivityForChange = true; - if (lastChange.ignoreCount > 1) { - lastChange.ignoreCount--; - } else if (changeObj.origin == '+input' || changeObj.origin == 'paste' - || changeObj.origin === undefined /* only in testing */) { - var selectionCount = cm.listSelections().length; - if (selectionCount > 1) - lastChange.ignoreCount = selectionCount; - var text = changeObj.text.join('\n'); - if (lastChange.maybeReset) { - lastChange.changes = []; - lastChange.maybeReset = false; - } - if (text) { - if (cm.state.overwrite && !/\n/.test(text)) { - lastChange.changes.push([text]); - } else { - if (text.length > 1) { - var insertEnd = vim && vim.insertEnd && vim.insertEnd.find() - var cursor = cm.getCursor(); - if (insertEnd && insertEnd.line == cursor.line) { - var offset = insertEnd.ch - cursor.ch; - if (offset > 0 && offset < text.length) { - lastChange.changes.push([text, offset]); - text = ''; - } + /** + * Listens for changes made in insert mode. + * Should only be active in insert mode. + * @arg {CodeMirror} cm + * @arg {{ origin: string | undefined; text: any[]; next: any; }} changeObj + */ + function onChange(cm, changeObj) { + var macroModeState = vimGlobalState.macroModeState; + var lastChange = macroModeState.lastInsertModeChanges; + if (!macroModeState.isPlaying) { + var vim = cm.state.vim; + while(changeObj) { + lastChange.expectCursorActivityForChange = true; + // @ts-ignore + if (lastChange.ignoreCount > 1) { + // @ts-ignore + lastChange.ignoreCount--; + } else if (changeObj.origin == '+input' || changeObj.origin == 'paste' + || changeObj.origin === undefined /* only in testing */) { + var selectionCount = cm.listSelections().length; + if (selectionCount > 1) + lastChange.ignoreCount = selectionCount; + var text = changeObj.text.join('\n'); + if (lastChange.maybeReset) { + lastChange.changes = []; + lastChange.maybeReset = false; + } + if (text) { + if (cm.state.overwrite && !/\n/.test(text)) { + lastChange.changes.push([text]); + } else { + if (text.length > 1) { + var insertEnd = vim && vim.insertEnd && vim.insertEnd.find() + var cursor = cm.getCursor(); + if (insertEnd && insertEnd.line == cursor.line) { + var offset = insertEnd.ch - cursor.ch; + if (offset > 0 && offset < text.length) { + lastChange.changes.push([text, offset]); + text = ''; } } - if (text) lastChange.changes.push(text); } + if (text) lastChange.changes.push(text); } } - // Change objects may be chained with next. - changeObj = changeObj.next; } + // Change objects may be chained with next. + changeObj = changeObj.next; } } + } - /** - * Listens for any kind of cursor activity on CodeMirror. - */ - function onCursorActivity(cm) { - var vim = cm.state.vim; - if (vim.insertMode) { - // Tracking cursor activity in insert mode (for macro support). - var macroModeState = vimGlobalState.macroModeState; - if (macroModeState.isPlaying) { return; } - var lastChange = macroModeState.lastInsertModeChanges; - if (lastChange.expectCursorActivityForChange) { - lastChange.expectCursorActivityForChange = false; - } else { - // Cursor moved outside the context of an edit. Reset the change. - lastChange.maybeReset = true; - if (vim.insertEnd) vim.insertEnd.clear(); - vim.insertEnd = cm.setBookmark(cm.getCursor(), {insertLeft: true}); - } - } else if (!cm.curOp.isVimOp) { - handleExternalSelection(cm, vim); + /** + * Listens for any kind of cursor activity on CodeMirror. + * @arg {CodeMirrorV} cm + */ + function onCursorActivity(cm) { + var vim = cm.state.vim; + if (vim.insertMode) { + // Tracking cursor activity in insert mode (for macro support). + var macroModeState = vimGlobalState.macroModeState; + if (macroModeState.isPlaying) { return; } + var lastChange = macroModeState.lastInsertModeChanges; + if (lastChange.expectCursorActivityForChange) { + lastChange.expectCursorActivityForChange = false; + } else { + // Cursor moved outside the context of an edit. Reset the change. + lastChange.maybeReset = true; + if (vim.insertEnd) vim.insertEnd.clear(); + vim.insertEnd = cm.setBookmark(cm.getCursor(), {insertLeft: true}); } + } else if (!cm.curOp?.isVimOp) { + handleExternalSelection(cm, vim); } - function handleExternalSelection(cm, vim) { - var anchor = cm.getCursor('anchor'); - var head = cm.getCursor('head'); - // Enter or exit visual mode to match mouse selection. - if (vim.visualMode && !cm.somethingSelected()) { - exitVisualMode(cm, false); - } else if (!vim.visualMode && !vim.insertMode && cm.somethingSelected()) { - vim.visualMode = true; - vim.visualLine = false; - CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"}); - } - if (vim.visualMode) { - // Bind CodeMirror selection model to vim selection model. - // Mouse selections are considered visual characterwise. - var headOffset = !cursorIsBefore(head, anchor) ? -1 : 0; - var anchorOffset = cursorIsBefore(head, anchor) ? -1 : 0; - head = offsetCursor(head, 0, headOffset); - anchor = offsetCursor(anchor, 0, anchorOffset); - vim.sel = { - anchor: anchor, - head: head - }; - updateMark(cm, vim, '<', cursorMin(head, anchor)); - updateMark(cm, vim, '>', cursorMax(head, anchor)); - } else if (!vim.insertMode) { - // Reset lastHPos if selection was modified by something outside of vim mode e.g. by mouse. - vim.lastHPos = cm.getCursor().ch; - } + } + /** @arg {CodeMirrorV} cm @arg {vimState} vim */ + function handleExternalSelection(cm, vim) { + var anchor = cm.getCursor('anchor'); + var head = cm.getCursor('head'); + // Enter or exit visual mode to match mouse selection. + if (vim.visualMode && !cm.somethingSelected()) { + exitVisualMode(cm, false); + } else if (!vim.visualMode && !vim.insertMode && cm.somethingSelected()) { + vim.visualMode = true; + vim.visualLine = false; + CodeMirror.signal(cm, "vim-mode-change", {mode: "visual"}); } - - /** Wrapper for special keys pressed in insert mode */ - function InsertModeKey(keyName, e) { - this.keyName = keyName; - this.key = e.key; - this.ctrlKey = e.ctrlKey; - this.altKey = e.altKey; - this.metaKey = e.metaKey; - this.shiftKey = e.shiftKey; + if (vim.visualMode) { + // Bind CodeMirror selection model to vim selection model. + // Mouse selections are considered visual characterwise. + var headOffset = !cursorIsBefore(head, anchor) ? -1 : 0; + var anchorOffset = cursorIsBefore(head, anchor) ? -1 : 0; + head = offsetCursor(head, 0, headOffset); + anchor = offsetCursor(anchor, 0, anchorOffset); + vim.sel = { + anchor: anchor, + head: head + }; + updateMark(cm, vim, '<', cursorMin(head, anchor)); + updateMark(cm, vim, '>', cursorMax(head, anchor)); + } else if (!vim.insertMode) { + // Reset lastHPos if selection was modified by something outside of vim mode e.g. by mouse. + vim.lastHPos = cm.getCursor().ch; } + } - /** - * Handles raw key down events from the text area. - * - Should only be active in insert mode. - * - For recording deletes in insert mode. - */ - function onKeyEventTargetKeyDown(e) { - var macroModeState = vimGlobalState.macroModeState; - var lastChange = macroModeState.lastInsertModeChanges; - var keyName = CodeMirror.keyName ? CodeMirror.keyName(e) : e.key; - if (!keyName) { return; } - - if (keyName.indexOf('Delete') != -1 || keyName.indexOf('Backspace') != -1) { - if (lastChange.maybeReset) { - lastChange.changes = []; - lastChange.maybeReset = false; - } - lastChange.changes.push(new InsertModeKey(keyName, e)); - } + /** + * Wrapper for special keys pressed in insert mode + * @arg {string} keyName + */ + function InsertModeKey(keyName, e) { + this.keyName = keyName; + this.key = e.key; + this.ctrlKey = e.ctrlKey; + this.altKey = e.altKey; + this.metaKey = e.metaKey; + this.shiftKey = e.shiftKey; + } + + /** + * Handles raw key down events from the text area. + * - Should only be active in insert mode. + * - For recording deletes in insert mode. + * @arg {KeyboardEvent} e + */ + function onKeyEventTargetKeyDown(e) { + var macroModeState = vimGlobalState.macroModeState; + var lastChange = macroModeState.lastInsertModeChanges; + var keyName = CodeMirror.keyName ? CodeMirror.keyName(e) : e.key; + if (!keyName) { return; } + + if (keyName.indexOf('Delete') != -1 || keyName.indexOf('Backspace') != -1) { + if (lastChange.maybeReset) { + lastChange.changes = []; + lastChange.maybeReset = false; + } + lastChange.changes.push(new InsertModeKey(keyName, e)); } + } - /** - * Repeats the last edit, which includes exactly 1 command and at most 1 - * insert. Operator and motion commands are read from lastEditInputState, - * while action commands are read from lastEditActionCommand. - * - * If repeatForInsert is true, then the function was called by - * exitInsertMode to repeat the insert mode changes the user just made. The - * corresponding enterInsertMode call was made with a count. - */ - function repeatLastEdit(cm, vim, repeat, repeatForInsert) { - var macroModeState = vimGlobalState.macroModeState; - macroModeState.isPlaying = true; - var isAction = !!vim.lastEditActionCommand; - var cachedInputState = vim.inputState; - function repeatCommand() { - if (isAction) { - commandDispatcher.processAction(cm, vim, vim.lastEditActionCommand); - } else { - commandDispatcher.evalInput(cm, vim); - } - } - function repeatInsert(repeat) { - if (macroModeState.lastInsertModeChanges.changes.length > 0) { - // For some reason, repeat cw in desktop VIM does not repeat - // insert mode changes. Will conform to that behavior. - repeat = !vim.lastEditActionCommand ? 1 : repeat; - var changeObject = macroModeState.lastInsertModeChanges; - repeatInsertModeChanges(cm, changeObject.changes, repeat); - } - } - vim.inputState = vim.lastEditInputState; - if (isAction && vim.lastEditActionCommand.interlaceInsertRepeat) { - // o and O repeat have to be interlaced with insert repeats so that the - // insertions appear on separate lines instead of the last line. - for (var i = 0; i < repeat; i++) { - repeatCommand(); - repeatInsert(1); - } + /** + * Repeats the last edit, which includes exactly 1 command and at most 1 + * insert. Operator and motion commands are read from lastEditInputState, + * while action commands are read from lastEditActionCommand. + * + * If repeatForInsert is true, then the function was called by + * exitInsertMode to repeat the insert mode changes the user just made. The + * corresponding enterInsertMode call was made with a count. + * @arg {CodeMirrorV} cm + * @arg {vimState} vim + * @arg {number} repeat + * @arg {boolean} repeatForInsert + */ + function repeatLastEdit(cm, vim, repeat, repeatForInsert) { + var macroModeState = vimGlobalState.macroModeState; + macroModeState.isPlaying = true; + var lastAction = vim.lastEditActionCommand; + var cachedInputState = vim.inputState; + function repeatCommand() { + if (lastAction) { + commandDispatcher.processAction(cm, vim, lastAction); } else { - if (!repeatForInsert) { - // Hack to get the cursor to end up at the right place. If I is - // repeated in insert mode repeat, cursor will be 1 insert - // change set left of where it should be. - repeatCommand(); - } - repeatInsert(repeat); - } - vim.inputState = cachedInputState; - if (vim.insertMode && !repeatForInsert) { - // Don't exit insert mode twice. If repeatForInsert is set, then we - // were called by an exitInsertMode call lower on the stack. - exitInsertMode(cm); + commandDispatcher.evalInput(cm, vim); } - macroModeState.isPlaying = false; - } - - function sendCmKey(cm, key) { - CodeMirror.lookupKey(key, 'vim-insert', function keyHandler(binding) { - if (typeof binding == 'string') { - CodeMirror.commands[binding](cm); - } else { - binding(cm); - } - return true; - }); } - function repeatInsertModeChanges(cm, changes, repeat) { - var head = cm.getCursor('head'); - var visualBlock = vimGlobalState.macroModeState.lastInsertModeChanges.visualBlock; - if (visualBlock) { - // Set up block selection again for repeating the changes. - selectForInsert(cm, head, visualBlock + 1); - repeat = cm.listSelections().length; - cm.setCursor(head); + /** @arg {number} repeat */ + function repeatInsert(repeat) { + if (macroModeState.lastInsertModeChanges.changes.length > 0) { + // For some reason, repeat cw in desktop VIM does not repeat + // insert mode changes. Will conform to that behavior. + repeat = !vim.lastEditActionCommand ? 1 : repeat; + var changeObject = macroModeState.lastInsertModeChanges; + repeatInsertModeChanges(cm, changeObject.changes, repeat); } + } + // @ts-ignore + vim.inputState = vim.lastEditInputState; + if (lastAction && lastAction.interlaceInsertRepeat) { + // o and O repeat have to be interlaced with insert repeats so that the + // insertions appear on separate lines instead of the last line. for (var i = 0; i < repeat; i++) { - if (visualBlock) { - cm.setCursor(offsetCursor(head, i, 0)); - } - for (var j = 0; j < changes.length; j++) { - var change = changes[j]; - if (change instanceof InsertModeKey) { - sendCmKey(cm, change.keyName, change); - } else if (typeof change == "string") { - cm.replaceSelection(change); - } else { - var start = cm.getCursor(); - var end = offsetCursor(start, 0, change[0].length - (change[1] || 0)); - cm.replaceRange(change[0], start, change[1] ? start: end); - cm.setCursor(end); - } - } + repeatCommand(); + repeatInsert(1); } - if (visualBlock) { - cm.setCursor(offsetCursor(head, 0, 1)); + } else { + if (!repeatForInsert) { + // Hack to get the cursor to end up at the right place. If I is + // repeated in insert mode repeat, cursor will be 1 insert + // change set left of where it should be. + repeatCommand(); } + repeatInsert(repeat); } - - // multiselect support - function cloneVimState(state) { - var n = new state.constructor(); - Object.keys(state).forEach(function(key) { - if (key == "insertEnd") return; - var o = state[key]; - if (Array.isArray(o)) - o = o.slice(); - else if (o && typeof o == "object" && o.constructor != Object) - o = cloneVimState(o); - n[key] = o; - }); - if (state.sel) { - n.sel = { - head: state.sel.head && copyCursor(state.sel.head), - anchor: state.sel.anchor && copyCursor(state.sel.anchor) - }; + vim.inputState = cachedInputState; + if (vim.insertMode && !repeatForInsert) { + // Don't exit insert mode twice. If repeatForInsert is set, then we + // were called by an exitInsertMode call lower on the stack. + exitInsertMode(cm); + } + macroModeState.isPlaying = false; + } + /**@arg {CodeMirrorV} cm, @arg {string} key */ + function sendCmKey(cm, key) { + CodeMirror.lookupKey(key, 'vim-insert', function keyHandler(binding) { + if (typeof binding == 'string') { + CodeMirror.commands[binding](cm); + } else { + binding(cm); } - return n; + return true; + }); + } + function repeatInsertModeChanges(cm, changes, repeat) { + var head = cm.getCursor('head'); + var visualBlock = vimGlobalState.macroModeState.lastInsertModeChanges.visualBlock; + if (visualBlock) { + // Set up block selection again for repeating the changes. + selectForInsert(cm, head, visualBlock + 1); + repeat = cm.listSelections().length; + cm.setCursor(head); } - function multiSelectHandleKey(cm, key, origin) { - var isHandled = false; - var vim = vimApi.maybeInitVimState_(cm); - var visualBlock = vim.visualBlock || vim.wasInVisualBlock; - - var wasMultiselect = cm.isInMultiSelectMode(); - if (vim.wasInVisualBlock && !wasMultiselect) { - vim.wasInVisualBlock = false; - } else if (wasMultiselect && vim.visualBlock) { - vim.wasInVisualBlock = true; + for (var i = 0; i < repeat; i++) { + if (visualBlock) { + cm.setCursor(offsetCursor(head, i, 0)); + } + for (var j = 0; j < changes.length; j++) { + var change = changes[j]; + if (change instanceof InsertModeKey) { + sendCmKey(cm, change.keyName); + } else if (typeof change == "string") { + cm.replaceSelection(change); + } else { + var start = cm.getCursor(); + var end = offsetCursor(start, 0, change[0].length - (change[1] || 0)); + cm.replaceRange(change[0], start, change[1] ? start: end); + cm.setCursor(end); + } } + } + if (visualBlock) { + cm.setCursor(offsetCursor(head, 0, 1)); + } + } - if (key == '' && !vim.insertMode && !vim.visualMode && wasMultiselect && vim.status == "") { - // allow editor to exit multiselect - clearInputState(cm); - } else if (visualBlock || !wasMultiselect || cm.inVirtualSelectionMode) { - isHandled = vimApi.handleKey(cm, key, origin); - } else { - var old = cloneVimState(vim); - var changeQueueList = vim.inputState.changeQueueList || []; + // multiselect support + /** @arg {vimState} state */ + function cloneVimState(state) { + var n = new state.constructor(); + Object.keys(state).forEach(function(key) { + if (key == "insertEnd") return; + var o = state[key]; + if (Array.isArray(o)) + o = o.slice(); + else if (o && typeof o == "object" && o.constructor != Object) + o = cloneVimState(o); + n[key] = o; + }); + if (state.sel) { + n.sel = { + head: state.sel.head && copyCursor(state.sel.head), + anchor: state.sel.anchor && copyCursor(state.sel.anchor) + }; + } + return n; + } + /** @arg {CodeMirror} cm_ @arg {string} key @arg {string} origin */ + function multiSelectHandleKey(cm_, key, origin) { + var vim = maybeInitVimState(cm_); + var cm = /**@type {CodeMirrorV}*/(cm_); + /** @type {boolean | undefined} */ + var isHandled = false; + var vim = vimApi.maybeInitVimState_(cm); + var visualBlock = vim.visualBlock || vim.wasInVisualBlock; + + var wasMultiselect = cm.isInMultiSelectMode(); + if (vim.wasInVisualBlock && !wasMultiselect) { + vim.wasInVisualBlock = false; + } else if (wasMultiselect && vim.visualBlock) { + vim.wasInVisualBlock = true; + } - cm.operation(function() { + if (key == '' && !vim.insertMode && !vim.visualMode && wasMultiselect && vim.status == "") { + // allow editor to exit multiselect + clearInputState(cm); + // @ts-ignore + } else if (visualBlock || !wasMultiselect || cm.inVirtualSelectionMode) { + isHandled = vimApi.handleKey(cm, key, origin); + } else { + var old = cloneVimState(vim); + var changeQueueList = vim.inputState.changeQueueList || []; + + cm.operation(function() { + if (cm.curOp) cm.curOp.isVimOp = true; - var index = 0; - cm.forEachSelection(function() { - cm.state.vim.inputState.changeQueue = changeQueueList[index]; - var head = cm.getCursor("head"); - var anchor = cm.getCursor("anchor"); - var headOffset = !cursorIsBefore(head, anchor) ? -1 : 0; - var anchorOffset = cursorIsBefore(head, anchor) ? -1 : 0; - head = offsetCursor(head, 0, headOffset); - anchor = offsetCursor(anchor, 0, anchorOffset); - cm.state.vim.sel.head = head; - cm.state.vim.sel.anchor = anchor; - - isHandled = vimApi.handleKey(cm, key, origin); - if (cm.virtualSelection) { - changeQueueList[index] = cm.state.vim.inputState.changeQueue; - cm.state.vim = cloneVimState(old); - } - index++; - }); - if (cm.curOp.cursorActivity && !isHandled) - cm.curOp.cursorActivity = false; - cm.state.vim = vim - vim.inputState.changeQueueList = changeQueueList - vim.inputState.changeQueue = null; - }, true); - } - // some commands may bring visualMode and selection out of sync - if (isHandled && !vim.visualMode && !vim.insert && vim.visualMode != cm.somethingSelected()) { - handleExternalSelection(cm, vim, true); - } - return isHandled; + var index = 0; + cm.forEachSelection(function() { + cm.state.vim.inputState.changeQueue = changeQueueList[index]; + var head = cm.getCursor("head"); + var anchor = cm.getCursor("anchor"); + var headOffset = !cursorIsBefore(head, anchor) ? -1 : 0; + var anchorOffset = cursorIsBefore(head, anchor) ? -1 : 0; + head = offsetCursor(head, 0, headOffset); + anchor = offsetCursor(anchor, 0, anchorOffset); + cm.state.vim.sel.head = head; + cm.state.vim.sel.anchor = anchor; + + isHandled = vimApi.handleKey(cm, key, origin); + if (cm.virtualSelection) { + changeQueueList[index] = cm.state.vim.inputState.changeQueue; + cm.state.vim = cloneVimState(old); + } + index++; + }); + if (cm.curOp?.cursorActivity && !isHandled) + cm.curOp.cursorActivity = false; + cm.state.vim = vim + vim.inputState.changeQueueList = changeQueueList + vim.inputState.changeQueue = null; + }, true); } - resetVimGlobalState(); + // some commands may bring visualMode and selection out of sync + if (isHandled && !vim.visualMode && !vim.insert && vim.visualMode != cm.somethingSelected()) { + handleExternalSelection(cm, vim); + } + return isHandled; + } + resetVimGlobalState(); return vimApi; }; diff --git a/tsconfig.json b/tsconfig.json index dff4158..154400a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,13 @@ { "compilerOptions": { // "module": "commonjs", - // "noImplicitAny": true, + "noImplicitAny": false, // "noEmit": true, // "declaration": false, // "lib": ["es2019", "DOM"] - - "allowJs": false, - "checkJs": false, + "outDir": "./dist", + "allowJs": true, + "checkJs": true, "lib": ["es6", "dom", "scripthost"], "types": ["mocha"], "stripInternal": true, @@ -20,6 +20,7 @@ "moduleResolution": "node", }, "include": [ - "src/*.ts" + "src/*.ts", + "src/*.js" ] -} \ No newline at end of file +}