import { deleteNearSelection } from "./deleteNearSelection.js" import { commands } from "./commands.js" import { attachDoc } from "../model/document_data.js" import { activeElt, addClass, rmClass } from "../util/dom.js" import { eventMixin, signal } from "../util/event.js" import { getLineStyles, getContextBefore, takeToken } from "../line/highlight.js" import { indentLine } from "../input/indent.js" import { triggerElectric } from "../input/input.js" import { onKeyDown, onKeyPress, onKeyUp } from "./key_events.js" import { onMouseDown } from "./mouse_events.js" import { getKeyMap } from "../input/keymap.js" import { endOfLine, moveLogically, moveVisually } from "../input/movement.js" import { endOperation, methodOp, operation, runInOp, startOperation } from "../display/operations.js" import { clipLine, clipPos, equalCursorPos, Pos } from "../line/pos.js" import { charCoords, charWidth, clearCaches, clearLineMeasurementCache, coordsChar, cursorCoords, displayHeight, displayWidth, estimateLineHeights, fromCoordSystem, intoCoordSystem, scrollGap, textHeight } from "../measurement/position_measurement.js" import { Range } from "../model/selection.js" import { replaceOneSelection, skipAtomic } from "../model/selection_updates.js" import { addToScrollTop, ensureCursorVisible, scrollIntoView, scrollToCoords, scrollToCoordsRange, scrollToRange } from "../display/scrolling.js" import { heightAtLine } from "../line/spans.js" import { updateGutterSpace } from "../display/update_display.js" import { indexOf, insertSorted, isWordChar, sel_dontScroll, sel_move } from "../util/misc.js" import { signalLater } from "../util/operation_group.js" import { getLine, isLine, lineAtHeight } from "../line/utils_line.js" import { regChange, regLineChange } from "../display/view_tracking.js" // The publicly visible API. Note that methodOp(f) means // 'wrap f in an operation, performed on its `this` parameter'. // This is not the complete set of editor methods. Most of the // methods defined on the Doc type are also injected into // CodeMirror.prototype, for backwards compatibility and // convenience. export default function(CodeMirror) { let optionHandlers = CodeMirror.optionHandlers let helpers = CodeMirror.helpers = {} CodeMirror.prototype = { constructor: CodeMirror, focus: function(){window.focus(); this.display.input.focus()}, setOption: function(option, value) { let options = this.options, old = options[option] if (options[option] == value && option != "mode") return options[option] = value if (optionHandlers.hasOwnProperty(option)) operation(this, optionHandlers[option])(this, value, old) signal(this, "optionChange", this, option) }, getOption: function(option) {return this.options[option]}, getDoc: function() {return this.doc}, addKeyMap: function(map, bottom) { this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map)) }, removeKeyMap: function(map) { let maps = this.state.keyMaps for (let i = 0; i < maps.length; ++i) if (maps[i] == map || maps[i].name == map) { maps.splice(i, 1) return true } }, addOverlay: methodOp(function(spec, options) { let mode = spec.token ? spec : CodeMirror.getMode(this.options, spec) if (mode.startState) throw new Error("Overlays may not be stateful.") insertSorted(this.state.overlays, {mode: mode, modeSpec: spec, opaque: options && options.opaque, priority: (options && options.priority) || 0}, overlay => overlay.priority) this.state.modeGen++ regChange(this) }), removeOverlay: methodOp(function(spec) { let overlays = this.state.overlays for (let i = 0; i < overlays.length; ++i) { let cur = overlays[i].modeSpec if (cur == spec || typeof spec == "string" && cur.name == spec) { overlays.splice(i, 1) this.state.modeGen++ regChange(this) return } } }), indentLine: methodOp(function(n, dir, aggressive) { if (typeof dir != "string" && typeof dir != "number") { if (dir == null) dir = this.options.smartIndent ? "smart" : "prev" else dir = dir ? "add" : "subtract" } if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive) }), indentSelection: methodOp(function(how) { let ranges = this.doc.sel.ranges, end = -1 for (let i = 0; i < ranges.length; i++) { let range = ranges[i] if (!range.empty()) { let from = range.from(), to = range.to() let start = Math.max(end, from.line) end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1 for (let j = start; j < end; ++j) indentLine(this, j, how) let newRanges = this.doc.sel.ranges if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0) replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll) } else if (range.head.line > end) { indentLine(this, range.head.line, how, true) end = range.head.line if (i == this.doc.sel.primIndex) ensureCursorVisible(this) } } }), // Fetch the parser token for a given character. Useful for hacks // that want to inspect the mode state (say, for completion). getTokenAt: function(pos, precise) { return takeToken(this, pos, precise) }, getLineTokens: function(line, precise) { return takeToken(this, Pos(line), precise, true) }, getTokenTypeAt: function(pos) { pos = clipPos(this.doc, pos) let styles = getLineStyles(this, getLine(this.doc, pos.line)) let before = 0, after = (styles.length - 1) / 2, ch = pos.ch let type if (ch == 0) type = styles[2] else for (;;) { let mid = (before + after) >> 1 if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid else if (styles[mid * 2 + 1] < ch) before = mid + 1 else { type = styles[mid * 2 + 2]; break } } let cut = type ? type.indexOf("overlay ") : -1 return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1) }, getModeAt: function(pos) { let mode = this.doc.mode if (!mode.innerMode) return mode return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode }, getHelper: function(pos, type) { return this.getHelpers(pos, type)[0] }, getHelpers: function(pos, type) { let found = [] if (!helpers.hasOwnProperty(type)) return found let help = helpers[type], mode = this.getModeAt(pos) if (typeof mode[type] == "string") { if (help[mode[type]]) found.push(help[mode[type]]) } else if (mode[type]) { for (let i = 0; i < mode[type].length; i++) { let val = help[mode[type][i]] if (val) found.push(val) } } else if (mode.helperType && help[mode.helperType]) { found.push(help[mode.helperType]) } else if (help[mode.name]) { found.push(help[mode.name]) } for (let i = 0; i < help._global.length; i++) { let cur = help._global[i] if (cur.pred(mode, this) && indexOf(found, cur.val) == -1) found.push(cur.val) } return found }, getStateAfter: function(line, precise) { let doc = this.doc line = clipLine(doc, line == null ? doc.first + doc.size - 1: line) return getContextBefore(this, line + 1, precise).state }, cursorCoords: function(start, mode) { let pos, range = this.doc.sel.primary() if (start == null) pos = range.head else if (typeof start == "object") pos = clipPos(this.doc, start) else pos = start ? range.from() : range.to() return cursorCoords(this, pos, mode || "page") }, charCoords: function(pos, mode) { return charCoords(this, clipPos(this.doc, pos), mode || "page") }, coordsChar: function(coords, mode) { coords = fromCoordSystem(this, coords, mode || "page") return coordsChar(this, coords.left, coords.top) }, lineAtHeight: function(height, mode) { height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top return lineAtHeight(this.doc, height + this.display.viewOffset) }, heightAtLine: function(line, mode, includeWidgets) { let end = false, lineObj if (typeof line == "number") { let last = this.doc.first + this.doc.size - 1 if (line < this.doc.first) line = this.doc.first else if (line > last) { line = last; end = true } lineObj = getLine(this.doc, line) } else { lineObj = line } return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page", includeWidgets || end).top + (end ? this.doc.height - heightAtLine(lineObj) : 0) }, defaultTextHeight: function() { return textHeight(this.display) }, defaultCharWidth: function() { return charWidth(this.display) }, getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo}}, addWidget: function(pos, node, scroll, vert, horiz) { let display = this.display pos = cursorCoords(this, clipPos(this.doc, pos)) let top = pos.bottom, left = pos.left node.style.position = "absolute" node.setAttribute("cm-ignore-events", "true") this.display.input.setUneditable(node) display.sizer.appendChild(node) if (vert == "over") { top = pos.top } else if (vert == "above" || vert == "near") { let vspace = Math.max(display.wrapper.clientHeight, this.doc.height), hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth) // Default to positioning above (if specified and possible); otherwise default to positioning below if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) top = pos.top - node.offsetHeight else if (pos.bottom + node.offsetHeight <= vspace) top = pos.bottom if (left + node.offsetWidth > hspace) left = hspace - node.offsetWidth } node.style.top = top + "px" node.style.left = node.style.right = "" if (horiz == "right") { left = display.sizer.clientWidth - node.offsetWidth node.style.right = "0px" } else { if (horiz == "left") left = 0 else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2 node.style.left = left + "px" } if (scroll) scrollIntoView(this, {left, top, right: left + node.offsetWidth, bottom: top + node.offsetHeight}) }, triggerOnKeyDown: methodOp(onKeyDown), triggerOnKeyPress: methodOp(onKeyPress), triggerOnKeyUp: onKeyUp, triggerOnMouseDown: methodOp(onMouseDown), execCommand: function(cmd) { if (commands.hasOwnProperty(cmd)) return commands[cmd].call(null, this) }, triggerElectric: methodOp(function(text) { triggerElectric(this, text) }), findPosH: function(from, amount, unit, visually) { let dir = 1 if (amount < 0) { dir = -1; amount = -amount } let cur = clipPos(this.doc, from) for (let i = 0; i < amount; ++i) { cur = findPosH(this.doc, cur, dir, unit, visually) if (cur.hitSide) break } return cur }, moveH: methodOp(function(dir, unit) { this.extendSelectionsBy(range => { if (this.display.shift || this.doc.extend || range.empty()) return findPosH(this.doc, range.head, dir, unit, this.options.rtlMoveVisually) else return dir < 0 ? range.from() : range.to() }, sel_move) }), deleteH: methodOp(function(dir, unit) { let sel = this.doc.sel, doc = this.doc if (sel.somethingSelected()) doc.replaceSelection("", null, "+delete") else deleteNearSelection(this, range => { let other = findPosH(doc, range.head, dir, unit, false) return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other} }) }), findPosV: function(from, amount, unit, goalColumn) { let dir = 1, x = goalColumn if (amount < 0) { dir = -1; amount = -amount } let cur = clipPos(this.doc, from) for (let i = 0; i < amount; ++i) { let coords = cursorCoords(this, cur, "div") if (x == null) x = coords.left else coords.left = x cur = findPosV(this, coords, dir, unit) if (cur.hitSide) break } return cur }, moveV: methodOp(function(dir, unit) { let doc = this.doc, goals = [] let collapse = !this.display.shift && !doc.extend && doc.sel.somethingSelected() doc.extendSelectionsBy(range => { if (collapse) return dir < 0 ? range.from() : range.to() let headPos = cursorCoords(this, range.head, "div") if (range.goalColumn != null) headPos.left = range.goalColumn goals.push(headPos.left) let pos = findPosV(this, headPos, dir, unit) if (unit == "page" && range == doc.sel.primary()) addToScrollTop(this, charCoords(this, pos, "div").top - headPos.top) return pos }, sel_move) if (goals.length) for (let i = 0; i < doc.sel.ranges.length; i++) doc.sel.ranges[i].goalColumn = goals[i] }), // Find the word at the given position (as returned by coordsChar). findWordAt: function(pos) { let doc = this.doc, line = getLine(doc, pos.line).text let start = pos.ch, end = pos.ch if (line) { let helper = this.getHelper(pos, "wordChars") if ((pos.sticky == "before" || end == line.length) && start) --start; else ++end let startChar = line.charAt(start) let check = isWordChar(startChar, helper) ? ch => isWordChar(ch, helper) : /\s/.test(startChar) ? ch => /\s/.test(ch) : ch => (!/\s/.test(ch) && !isWordChar(ch)) while (start > 0 && check(line.charAt(start - 1))) --start while (end < line.length && check(line.charAt(end))) ++end } return new Range(Pos(pos.line, start), Pos(pos.line, end)) }, toggleOverwrite: function(value) { if (value != null && value == this.state.overwrite) return if (this.state.overwrite = !this.state.overwrite) addClass(this.display.cursorDiv, "CodeMirror-overwrite") else rmClass(this.display.cursorDiv, "CodeMirror-overwrite") signal(this, "overwriteToggle", this, this.state.overwrite) }, hasFocus: function() { return this.display.input.getField() == activeElt() }, isReadOnly: function() { return !!(this.options.readOnly || this.doc.cantEdit) }, scrollTo: methodOp(function (x, y) { scrollToCoords(this, x, y) }), getScrollInfo: function() { let scroller = this.display.scroller return {left: scroller.scrollLeft, top: scroller.scrollTop, height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight, width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth, clientHeight: displayHeight(this), clientWidth: displayWidth(this)} }, scrollIntoView: methodOp(function(range, margin) { if (range == null) { range = {from: this.doc.sel.primary().head, to: null} if (margin == null) margin = this.options.cursorScrollMargin } else if (typeof range == "number") { range = {from: Pos(range, 0), to: null} } else if (range.from == null) { range = {from: range, to: null} } if (!range.to) range.to = range.from range.margin = margin || 0 if (range.from.line != null) { scrollToRange(this, range) } else { scrollToCoordsRange(this, range.from, range.to, range.margin) } }), setSize: methodOp(function(width, height) { let interpret = val => typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val if (width != null) this.display.wrapper.style.width = interpret(width) if (height != null) this.display.wrapper.style.height = interpret(height) if (this.options.lineWrapping) clearLineMeasurementCache(this) let lineNo = this.display.viewFrom this.doc.iter(lineNo, this.display.viewTo, line => { if (line.widgets) for (let i = 0; i < line.widgets.length; i++) if (line.widgets[i].noHScroll) { regLineChange(this, lineNo, "widget"); break } ++lineNo }) this.curOp.forceUpdate = true signal(this, "refresh", this) }), operation: function(f){return runInOp(this, f)}, startOperation: function(){return startOperation(this)}, endOperation: function(){return endOperation(this)}, refresh: methodOp(function() { let oldHeight = this.display.cachedTextHeight regChange(this) this.curOp.forceUpdate = true clearCaches(this) scrollToCoords(this, this.doc.scrollLeft, this.doc.scrollTop) updateGutterSpace(this.display) if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5) estimateLineHeights(this) signal(this, "refresh", this) }), swapDoc: methodOp(function(doc) { let old = this.doc old.cm = null // Cancel the current text selection if any (#5821) if (this.state.selectingText) this.state.selectingText() attachDoc(this, doc) clearCaches(this) this.display.input.reset() scrollToCoords(this, doc.scrollLeft, doc.scrollTop) this.curOp.forceScroll = true signalLater(this, "swapDoc", this, old) return old }), phrase: function(phraseText) { let phrases = this.options.phrases return phrases && Object.prototype.hasOwnProperty.call(phrases, phraseText) ? phrases[phraseText] : phraseText }, getInputField: function(){return this.display.input.getField()}, getWrapperElement: function(){return this.display.wrapper}, getScrollerElement: function(){return this.display.scroller}, getGutterElement: function(){return this.display.gutters} } eventMixin(CodeMirror) CodeMirror.registerHelper = function(type, name, value) { if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {_global: []} helpers[type][name] = value } CodeMirror.registerGlobalHelper = function(type, name, predicate, value) { CodeMirror.registerHelper(type, name, value) helpers[type]._global.push({pred: predicate, val: value}) } } // Used for horizontal relative motion. Dir is -1 or 1 (left or // right), unit can be "char", "column" (like char, but doesn't // cross line boundaries), "word" (across next word), or "group" (to // the start of next group of word or non-word-non-whitespace // chars). The visually param controls whether, in right-to-left // text, direction 1 means to move towards the next index in the // string, or towards the character to the right of the current // position. The resulting position will have a hitSide=true // property if it reached the end of the document. function findPosH(doc, pos, dir, unit, visually) { let oldPos = pos let origDir = dir let lineObj = getLine(doc, pos.line) function findNextLine() { let l = pos.line + dir if (l < doc.first || l >= doc.first + doc.size) return false pos = new Pos(l, pos.ch, pos.sticky) return lineObj = getLine(doc, l) } function moveOnce(boundToLine) { let next if (visually) { next = moveVisually(doc.cm, lineObj, pos, dir) } else { next = moveLogically(lineObj, pos, dir) } if (next == null) { if (!boundToLine && findNextLine()) pos = endOfLine(visually, doc.cm, lineObj, pos.line, dir) else return false } else { pos = next } return true } if (unit == "char") { moveOnce() } else if (unit == "column") { moveOnce(true) } else if (unit == "word" || unit == "group") { let sawType = null, group = unit == "group" let helper = doc.cm && doc.cm.getHelper(pos, "wordChars") for (let first = true;; first = false) { if (dir < 0 && !moveOnce(!first)) break let cur = lineObj.text.charAt(pos.ch) || "\n" let type = isWordChar(cur, helper) ? "w" : group && cur == "\n" ? "n" : !group || /\s/.test(cur) ? null : "p" if (group && !first && !type) type = "s" if (sawType && sawType != type) { if (dir < 0) {dir = 1; moveOnce(); pos.sticky = "after"} break } if (type) sawType = type if (dir > 0 && !moveOnce(!first)) break } } let result = skipAtomic(doc, pos, oldPos, origDir, true) if (equalCursorPos(oldPos, result)) result.hitSide = true return result } // For relative vertical movement. Dir may be -1 or 1. Unit can be // "page" or "line". The resulting position will have a hitSide=true // property if it reached the end of the document. function findPosV(cm, pos, dir, unit) { let doc = cm.doc, x = pos.left, y if (unit == "page") { let pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight) let moveAmount = Math.max(pageSize - .5 * textHeight(cm.display), 3) y = (dir > 0 ? pos.bottom : pos.top) + dir * moveAmount } else if (unit == "line") { y = dir > 0 ? pos.bottom + 3 : pos.top - 3 } let target for (;;) { target = coordsChar(cm, x, y) if (!target.outside) break if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break } y += dir * 5 } return target }