openrat-cms

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

Doc.js (17215B)


      1 import CodeMirror from "../edit/CodeMirror.js"
      2 import { docMethodOp } from "../display/operations.js"
      3 import { Line } from "../line/line_data.js"
      4 import { clipPos, clipPosArray, Pos } from "../line/pos.js"
      5 import { visualLine } from "../line/spans.js"
      6 import { getBetween, getLine, getLines, isLine, lineNo } from "../line/utils_line.js"
      7 import { classTest } from "../util/dom.js"
      8 import { splitLinesAuto } from "../util/feature_detection.js"
      9 import { createObj, map, isEmpty, sel_dontScroll } from "../util/misc.js"
     10 import { ensureCursorVisible, scrollToCoords } from "../display/scrolling.js"
     11 
     12 import { changeLine, makeChange, makeChangeFromHistory, replaceRange } from "./changes.js"
     13 import { computeReplacedSel } from "./change_measurement.js"
     14 import { BranchChunk, LeafChunk } from "./chunk.js"
     15 import { directionChanged, linkedDocs, updateDoc } from "./document_data.js"
     16 import { copyHistoryArray, History } from "./history.js"
     17 import { addLineWidget } from "./line_widget.js"
     18 import { copySharedMarkers, detachSharedMarkers, findSharedMarkers, markText } from "./mark_text.js"
     19 import { normalizeSelection, Range, simpleSelection } from "./selection.js"
     20 import { extendSelection, extendSelections, setSelection, setSelectionReplaceHistory, setSimpleSelection } from "./selection_updates.js"
     21 
     22 let nextDocId = 0
     23 let Doc = function(text, mode, firstLine, lineSep, direction) {
     24   if (!(this instanceof Doc)) return new Doc(text, mode, firstLine, lineSep, direction)
     25   if (firstLine == null) firstLine = 0
     26 
     27   BranchChunk.call(this, [new LeafChunk([new Line("", null)])])
     28   this.first = firstLine
     29   this.scrollTop = this.scrollLeft = 0
     30   this.cantEdit = false
     31   this.cleanGeneration = 1
     32   this.modeFrontier = this.highlightFrontier = firstLine
     33   let start = Pos(firstLine, 0)
     34   this.sel = simpleSelection(start)
     35   this.history = new History(null)
     36   this.id = ++nextDocId
     37   this.modeOption = mode
     38   this.lineSep = lineSep
     39   this.direction = (direction == "rtl") ? "rtl" : "ltr"
     40   this.extend = false
     41 
     42   if (typeof text == "string") text = this.splitLines(text)
     43   updateDoc(this, {from: start, to: start, text: text})
     44   setSelection(this, simpleSelection(start), sel_dontScroll)
     45 }
     46 
     47 Doc.prototype = createObj(BranchChunk.prototype, {
     48   constructor: Doc,
     49   // Iterate over the document. Supports two forms -- with only one
     50   // argument, it calls that for each line in the document. With
     51   // three, it iterates over the range given by the first two (with
     52   // the second being non-inclusive).
     53   iter: function(from, to, op) {
     54     if (op) this.iterN(from - this.first, to - from, op)
     55     else this.iterN(this.first, this.first + this.size, from)
     56   },
     57 
     58   // Non-public interface for adding and removing lines.
     59   insert: function(at, lines) {
     60     let height = 0
     61     for (let i = 0; i < lines.length; ++i) height += lines[i].height
     62     this.insertInner(at - this.first, lines, height)
     63   },
     64   remove: function(at, n) { this.removeInner(at - this.first, n) },
     65 
     66   // From here, the methods are part of the public interface. Most
     67   // are also available from CodeMirror (editor) instances.
     68 
     69   getValue: function(lineSep) {
     70     let lines = getLines(this, this.first, this.first + this.size)
     71     if (lineSep === false) return lines
     72     return lines.join(lineSep || this.lineSeparator())
     73   },
     74   setValue: docMethodOp(function(code) {
     75     let top = Pos(this.first, 0), last = this.first + this.size - 1
     76     makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length),
     77                       text: this.splitLines(code), origin: "setValue", full: true}, true)
     78     if (this.cm) scrollToCoords(this.cm, 0, 0)
     79     setSelection(this, simpleSelection(top), sel_dontScroll)
     80   }),
     81   replaceRange: function(code, from, to, origin) {
     82     from = clipPos(this, from)
     83     to = to ? clipPos(this, to) : from
     84     replaceRange(this, code, from, to, origin)
     85   },
     86   getRange: function(from, to, lineSep) {
     87     let lines = getBetween(this, clipPos(this, from), clipPos(this, to))
     88     if (lineSep === false) return lines
     89     return lines.join(lineSep || this.lineSeparator())
     90   },
     91 
     92   getLine: function(line) {let l = this.getLineHandle(line); return l && l.text},
     93 
     94   getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line)},
     95   getLineNumber: function(line) {return lineNo(line)},
     96 
     97   getLineHandleVisualStart: function(line) {
     98     if (typeof line == "number") line = getLine(this, line)
     99     return visualLine(line)
    100   },
    101 
    102   lineCount: function() {return this.size},
    103   firstLine: function() {return this.first},
    104   lastLine: function() {return this.first + this.size - 1},
    105 
    106   clipPos: function(pos) {return clipPos(this, pos)},
    107 
    108   getCursor: function(start) {
    109     let range = this.sel.primary(), pos
    110     if (start == null || start == "head") pos = range.head
    111     else if (start == "anchor") pos = range.anchor
    112     else if (start == "end" || start == "to" || start === false) pos = range.to()
    113     else pos = range.from()
    114     return pos
    115   },
    116   listSelections: function() { return this.sel.ranges },
    117   somethingSelected: function() {return this.sel.somethingSelected()},
    118 
    119   setCursor: docMethodOp(function(line, ch, options) {
    120     setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options)
    121   }),
    122   setSelection: docMethodOp(function(anchor, head, options) {
    123     setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options)
    124   }),
    125   extendSelection: docMethodOp(function(head, other, options) {
    126     extendSelection(this, clipPos(this, head), other && clipPos(this, other), options)
    127   }),
    128   extendSelections: docMethodOp(function(heads, options) {
    129     extendSelections(this, clipPosArray(this, heads), options)
    130   }),
    131   extendSelectionsBy: docMethodOp(function(f, options) {
    132     let heads = map(this.sel.ranges, f)
    133     extendSelections(this, clipPosArray(this, heads), options)
    134   }),
    135   setSelections: docMethodOp(function(ranges, primary, options) {
    136     if (!ranges.length) return
    137     let out = []
    138     for (let i = 0; i < ranges.length; i++)
    139       out[i] = new Range(clipPos(this, ranges[i].anchor),
    140                          clipPos(this, ranges[i].head))
    141     if (primary == null) primary = Math.min(ranges.length - 1, this.sel.primIndex)
    142     setSelection(this, normalizeSelection(out, primary), options)
    143   }),
    144   addSelection: docMethodOp(function(anchor, head, options) {
    145     let ranges = this.sel.ranges.slice(0)
    146     ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor)))
    147     setSelection(this, normalizeSelection(ranges, ranges.length - 1), options)
    148   }),
    149 
    150   getSelection: function(lineSep) {
    151     let ranges = this.sel.ranges, lines
    152     for (let i = 0; i < ranges.length; i++) {
    153       let sel = getBetween(this, ranges[i].from(), ranges[i].to())
    154       lines = lines ? lines.concat(sel) : sel
    155     }
    156     if (lineSep === false) return lines
    157     else return lines.join(lineSep || this.lineSeparator())
    158   },
    159   getSelections: function(lineSep) {
    160     let parts = [], ranges = this.sel.ranges
    161     for (let i = 0; i < ranges.length; i++) {
    162       let sel = getBetween(this, ranges[i].from(), ranges[i].to())
    163       if (lineSep !== false) sel = sel.join(lineSep || this.lineSeparator())
    164       parts[i] = sel
    165     }
    166     return parts
    167   },
    168   replaceSelection: function(code, collapse, origin) {
    169     let dup = []
    170     for (let i = 0; i < this.sel.ranges.length; i++)
    171       dup[i] = code
    172     this.replaceSelections(dup, collapse, origin || "+input")
    173   },
    174   replaceSelections: docMethodOp(function(code, collapse, origin) {
    175     let changes = [], sel = this.sel
    176     for (let i = 0; i < sel.ranges.length; i++) {
    177       let range = sel.ranges[i]
    178       changes[i] = {from: range.from(), to: range.to(), text: this.splitLines(code[i]), origin: origin}
    179     }
    180     let newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse)
    181     for (let i = changes.length - 1; i >= 0; i--)
    182       makeChange(this, changes[i])
    183     if (newSel) setSelectionReplaceHistory(this, newSel)
    184     else if (this.cm) ensureCursorVisible(this.cm)
    185   }),
    186   undo: docMethodOp(function() {makeChangeFromHistory(this, "undo")}),
    187   redo: docMethodOp(function() {makeChangeFromHistory(this, "redo")}),
    188   undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true)}),
    189   redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true)}),
    190 
    191   setExtending: function(val) {this.extend = val},
    192   getExtending: function() {return this.extend},
    193 
    194   historySize: function() {
    195     let hist = this.history, done = 0, undone = 0
    196     for (let i = 0; i < hist.done.length; i++) if (!hist.done[i].ranges) ++done
    197     for (let i = 0; i < hist.undone.length; i++) if (!hist.undone[i].ranges) ++undone
    198     return {undo: done, redo: undone}
    199   },
    200   clearHistory: function() {this.history = new History(this.history.maxGeneration)},
    201 
    202   markClean: function() {
    203     this.cleanGeneration = this.changeGeneration(true)
    204   },
    205   changeGeneration: function(forceSplit) {
    206     if (forceSplit)
    207       this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null
    208     return this.history.generation
    209   },
    210   isClean: function (gen) {
    211     return this.history.generation == (gen || this.cleanGeneration)
    212   },
    213 
    214   getHistory: function() {
    215     return {done: copyHistoryArray(this.history.done),
    216             undone: copyHistoryArray(this.history.undone)}
    217   },
    218   setHistory: function(histData) {
    219     let hist = this.history = new History(this.history.maxGeneration)
    220     hist.done = copyHistoryArray(histData.done.slice(0), null, true)
    221     hist.undone = copyHistoryArray(histData.undone.slice(0), null, true)
    222   },
    223 
    224   setGutterMarker: docMethodOp(function(line, gutterID, value) {
    225     return changeLine(this, line, "gutter", line => {
    226       let markers = line.gutterMarkers || (line.gutterMarkers = {})
    227       markers[gutterID] = value
    228       if (!value && isEmpty(markers)) line.gutterMarkers = null
    229       return true
    230     })
    231   }),
    232 
    233   clearGutter: docMethodOp(function(gutterID) {
    234     this.iter(line => {
    235       if (line.gutterMarkers && line.gutterMarkers[gutterID]) {
    236         changeLine(this, line, "gutter", () => {
    237           line.gutterMarkers[gutterID] = null
    238           if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null
    239           return true
    240         })
    241       }
    242     })
    243   }),
    244 
    245   lineInfo: function(line) {
    246     let n
    247     if (typeof line == "number") {
    248       if (!isLine(this, line)) return null
    249       n = line
    250       line = getLine(this, line)
    251       if (!line) return null
    252     } else {
    253       n = lineNo(line)
    254       if (n == null) return null
    255     }
    256     return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers,
    257             textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass,
    258             widgets: line.widgets}
    259   },
    260 
    261   addLineClass: docMethodOp(function(handle, where, cls) {
    262     return changeLine(this, handle, where == "gutter" ? "gutter" : "class", line => {
    263       let prop = where == "text" ? "textClass"
    264                : where == "background" ? "bgClass"
    265                : where == "gutter" ? "gutterClass" : "wrapClass"
    266       if (!line[prop]) line[prop] = cls
    267       else if (classTest(cls).test(line[prop])) return false
    268       else line[prop] += " " + cls
    269       return true
    270     })
    271   }),
    272   removeLineClass: docMethodOp(function(handle, where, cls) {
    273     return changeLine(this, handle, where == "gutter" ? "gutter" : "class", line => {
    274       let prop = where == "text" ? "textClass"
    275                : where == "background" ? "bgClass"
    276                : where == "gutter" ? "gutterClass" : "wrapClass"
    277       let cur = line[prop]
    278       if (!cur) return false
    279       else if (cls == null) line[prop] = null
    280       else {
    281         let found = cur.match(classTest(cls))
    282         if (!found) return false
    283         let end = found.index + found[0].length
    284         line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null
    285       }
    286       return true
    287     })
    288   }),
    289 
    290   addLineWidget: docMethodOp(function(handle, node, options) {
    291     return addLineWidget(this, handle, node, options)
    292   }),
    293   removeLineWidget: function(widget) { widget.clear() },
    294 
    295   markText: function(from, to, options) {
    296     return markText(this, clipPos(this, from), clipPos(this, to), options, options && options.type || "range")
    297   },
    298   setBookmark: function(pos, options) {
    299     let realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options),
    300                     insertLeft: options && options.insertLeft,
    301                     clearWhenEmpty: false, shared: options && options.shared,
    302                     handleMouseEvents: options && options.handleMouseEvents}
    303     pos = clipPos(this, pos)
    304     return markText(this, pos, pos, realOpts, "bookmark")
    305   },
    306   findMarksAt: function(pos) {
    307     pos = clipPos(this, pos)
    308     let markers = [], spans = getLine(this, pos.line).markedSpans
    309     if (spans) for (let i = 0; i < spans.length; ++i) {
    310       let span = spans[i]
    311       if ((span.from == null || span.from <= pos.ch) &&
    312           (span.to == null || span.to >= pos.ch))
    313         markers.push(span.marker.parent || span.marker)
    314     }
    315     return markers
    316   },
    317   findMarks: function(from, to, filter) {
    318     from = clipPos(this, from); to = clipPos(this, to)
    319     let found = [], lineNo = from.line
    320     this.iter(from.line, to.line + 1, line => {
    321       let spans = line.markedSpans
    322       if (spans) for (let i = 0; i < spans.length; i++) {
    323         let span = spans[i]
    324         if (!(span.to != null && lineNo == from.line && from.ch >= span.to ||
    325               span.from == null && lineNo != from.line ||
    326               span.from != null && lineNo == to.line && span.from >= to.ch) &&
    327             (!filter || filter(span.marker)))
    328           found.push(span.marker.parent || span.marker)
    329       }
    330       ++lineNo
    331     })
    332     return found
    333   },
    334   getAllMarks: function() {
    335     let markers = []
    336     this.iter(line => {
    337       let sps = line.markedSpans
    338       if (sps) for (let i = 0; i < sps.length; ++i)
    339         if (sps[i].from != null) markers.push(sps[i].marker)
    340     })
    341     return markers
    342   },
    343 
    344   posFromIndex: function(off) {
    345     let ch, lineNo = this.first, sepSize = this.lineSeparator().length
    346     this.iter(line => {
    347       let sz = line.text.length + sepSize
    348       if (sz > off) { ch = off; return true }
    349       off -= sz
    350       ++lineNo
    351     })
    352     return clipPos(this, Pos(lineNo, ch))
    353   },
    354   indexFromPos: function (coords) {
    355     coords = clipPos(this, coords)
    356     let index = coords.ch
    357     if (coords.line < this.first || coords.ch < 0) return 0
    358     let sepSize = this.lineSeparator().length
    359     this.iter(this.first, coords.line, line => { // iter aborts when callback returns a truthy value
    360       index += line.text.length + sepSize
    361     })
    362     return index
    363   },
    364 
    365   copy: function(copyHistory) {
    366     let doc = new Doc(getLines(this, this.first, this.first + this.size),
    367                       this.modeOption, this.first, this.lineSep, this.direction)
    368     doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft
    369     doc.sel = this.sel
    370     doc.extend = false
    371     if (copyHistory) {
    372       doc.history.undoDepth = this.history.undoDepth
    373       doc.setHistory(this.getHistory())
    374     }
    375     return doc
    376   },
    377 
    378   linkedDoc: function(options) {
    379     if (!options) options = {}
    380     let from = this.first, to = this.first + this.size
    381     if (options.from != null && options.from > from) from = options.from
    382     if (options.to != null && options.to < to) to = options.to
    383     let copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from, this.lineSep, this.direction)
    384     if (options.sharedHist) copy.history = this.history
    385     ;(this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist})
    386     copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]
    387     copySharedMarkers(copy, findSharedMarkers(this))
    388     return copy
    389   },
    390   unlinkDoc: function(other) {
    391     if (other instanceof CodeMirror) other = other.doc
    392     if (this.linked) for (let i = 0; i < this.linked.length; ++i) {
    393       let link = this.linked[i]
    394       if (link.doc != other) continue
    395       this.linked.splice(i, 1)
    396       other.unlinkDoc(this)
    397       detachSharedMarkers(findSharedMarkers(this))
    398       break
    399     }
    400     // If the histories were shared, split them again
    401     if (other.history == this.history) {
    402       let splitIds = [other.id]
    403       linkedDocs(other, doc => splitIds.push(doc.id), true)
    404       other.history = new History(null)
    405       other.history.done = copyHistoryArray(this.history.done, splitIds)
    406       other.history.undone = copyHistoryArray(this.history.undone, splitIds)
    407     }
    408   },
    409   iterLinkedDocs: function(f) {linkedDocs(this, f)},
    410 
    411   getMode: function() {return this.mode},
    412   getEditor: function() {return this.cm},
    413 
    414   splitLines: function(str) {
    415     if (this.lineSep) return str.split(this.lineSep)
    416     return splitLinesAuto(str)
    417   },
    418   lineSeparator: function() { return this.lineSep || "\n" },
    419 
    420   setDirection: docMethodOp(function (dir) {
    421     if (dir != "rtl") dir = "ltr"
    422     if (dir == this.direction) return
    423     this.direction = dir
    424     this.iter(line => line.order = null)
    425     if (this.cm) directionChanged(this.cm)
    426   })
    427 })
    428 
    429 // Public alias.
    430 Doc.prototype.eachLine = Doc.prototype.iter
    431 
    432 export default Doc