Mercurial
comparison .cms/lib/codemirror/src/edit/methods.js @ 0:78edf6b517a0 draft
24.10
author | Coffee CMS <info@coffee-cms.ru> |
---|---|
date | Fri, 11 Oct 2024 22:40:23 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:78edf6b517a0 |
---|---|
1 import { deleteNearSelection } from "./deleteNearSelection.js" | |
2 import { commands } from "./commands.js" | |
3 import { attachDoc } from "../model/document_data.js" | |
4 import { activeElt, addClass, rmClass, root, win } from "../util/dom.js" | |
5 import { eventMixin, signal } from "../util/event.js" | |
6 import { getLineStyles, getContextBefore, takeToken } from "../line/highlight.js" | |
7 import { indentLine } from "../input/indent.js" | |
8 import { triggerElectric } from "../input/input.js" | |
9 import { onKeyDown, onKeyPress, onKeyUp } from "./key_events.js" | |
10 import { onMouseDown } from "./mouse_events.js" | |
11 import { getKeyMap } from "../input/keymap.js" | |
12 import { endOfLine, moveLogically, moveVisually } from "../input/movement.js" | |
13 import { endOperation, methodOp, operation, runInOp, startOperation } from "../display/operations.js" | |
14 import { clipLine, clipPos, equalCursorPos, Pos } from "../line/pos.js" | |
15 import { charCoords, charWidth, clearCaches, clearLineMeasurementCache, coordsChar, cursorCoords, displayHeight, displayWidth, estimateLineHeights, fromCoordSystem, intoCoordSystem, scrollGap, textHeight } from "../measurement/position_measurement.js" | |
16 import { Range } from "../model/selection.js" | |
17 import { replaceOneSelection, skipAtomic } from "../model/selection_updates.js" | |
18 import { addToScrollTop, ensureCursorVisible, scrollIntoView, scrollToCoords, scrollToCoordsRange, scrollToRange } from "../display/scrolling.js" | |
19 import { heightAtLine } from "../line/spans.js" | |
20 import { updateGutterSpace } from "../display/update_display.js" | |
21 import { indexOf, insertSorted, isWordChar, sel_dontScroll, sel_move } from "../util/misc.js" | |
22 import { signalLater } from "../util/operation_group.js" | |
23 import { getLine, isLine, lineAtHeight } from "../line/utils_line.js" | |
24 import { regChange, regLineChange } from "../display/view_tracking.js" | |
25 | |
26 // The publicly visible API. Note that methodOp(f) means | |
27 // 'wrap f in an operation, performed on its `this` parameter'. | |
28 | |
29 // This is not the complete set of editor methods. Most of the | |
30 // methods defined on the Doc type are also injected into | |
31 // CodeMirror.prototype, for backwards compatibility and | |
32 // convenience. | |
33 | |
34 export default function(CodeMirror) { | |
35 let optionHandlers = CodeMirror.optionHandlers | |
36 | |
37 let helpers = CodeMirror.helpers = {} | |
38 | |
39 CodeMirror.prototype = { | |
40 constructor: CodeMirror, | |
41 focus: function(){win(this).focus(); this.display.input.focus()}, | |
42 | |
43 setOption: function(option, value) { | |
44 let options = this.options, old = options[option] | |
45 if (options[option] == value && option != "mode") return | |
46 options[option] = value | |
47 if (optionHandlers.hasOwnProperty(option)) | |
48 operation(this, optionHandlers[option])(this, value, old) | |
49 signal(this, "optionChange", this, option) | |
50 }, | |
51 | |
52 getOption: function(option) {return this.options[option]}, | |
53 getDoc: function() {return this.doc}, | |
54 | |
55 addKeyMap: function(map, bottom) { | |
56 this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map)) | |
57 }, | |
58 removeKeyMap: function(map) { | |
59 let maps = this.state.keyMaps | |
60 for (let i = 0; i < maps.length; ++i) | |
61 if (maps[i] == map || maps[i].name == map) { | |
62 maps.splice(i, 1) | |
63 return true | |
64 } | |
65 }, | |
66 | |
67 addOverlay: methodOp(function(spec, options) { | |
68 let mode = spec.token ? spec : CodeMirror.getMode(this.options, spec) | |
69 if (mode.startState) throw new Error("Overlays may not be stateful.") | |
70 insertSorted(this.state.overlays, | |
71 {mode: mode, modeSpec: spec, opaque: options && options.opaque, | |
72 priority: (options && options.priority) || 0}, | |
73 overlay => overlay.priority) | |
74 this.state.modeGen++ | |
75 regChange(this) | |
76 }), | |
77 removeOverlay: methodOp(function(spec) { | |
78 let overlays = this.state.overlays | |
79 for (let i = 0; i < overlays.length; ++i) { | |
80 let cur = overlays[i].modeSpec | |
81 if (cur == spec || typeof spec == "string" && cur.name == spec) { | |
82 overlays.splice(i, 1) | |
83 this.state.modeGen++ | |
84 regChange(this) | |
85 return | |
86 } | |
87 } | |
88 }), | |
89 | |
90 indentLine: methodOp(function(n, dir, aggressive) { | |
91 if (typeof dir != "string" && typeof dir != "number") { | |
92 if (dir == null) dir = this.options.smartIndent ? "smart" : "prev" | |
93 else dir = dir ? "add" : "subtract" | |
94 } | |
95 if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive) | |
96 }), | |
97 indentSelection: methodOp(function(how) { | |
98 let ranges = this.doc.sel.ranges, end = -1 | |
99 for (let i = 0; i < ranges.length; i++) { | |
100 let range = ranges[i] | |
101 if (!range.empty()) { | |
102 let from = range.from(), to = range.to() | |
103 let start = Math.max(end, from.line) | |
104 end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1 | |
105 for (let j = start; j < end; ++j) | |
106 indentLine(this, j, how) | |
107 let newRanges = this.doc.sel.ranges | |
108 if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0) | |
109 replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll) | |
110 } else if (range.head.line > end) { | |
111 indentLine(this, range.head.line, how, true) | |
112 end = range.head.line | |
113 if (i == this.doc.sel.primIndex) ensureCursorVisible(this) | |
114 } | |
115 } | |
116 }), | |
117 | |
118 // Fetch the parser token for a given character. Useful for hacks | |
119 // that want to inspect the mode state (say, for completion). | |
120 getTokenAt: function(pos, precise) { | |
121 return takeToken(this, pos, precise) | |
122 }, | |
123 | |
124 getLineTokens: function(line, precise) { | |
125 return takeToken(this, Pos(line), precise, true) | |
126 }, | |
127 | |
128 getTokenTypeAt: function(pos) { | |
129 pos = clipPos(this.doc, pos) | |
130 let styles = getLineStyles(this, getLine(this.doc, pos.line)) | |
131 let before = 0, after = (styles.length - 1) / 2, ch = pos.ch | |
132 let type | |
133 if (ch == 0) type = styles[2] | |
134 else for (;;) { | |
135 let mid = (before + after) >> 1 | |
136 if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid | |
137 else if (styles[mid * 2 + 1] < ch) before = mid + 1 | |
138 else { type = styles[mid * 2 + 2]; break } | |
139 } | |
140 let cut = type ? type.indexOf("overlay ") : -1 | |
141 return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1) | |
142 }, | |
143 | |
144 getModeAt: function(pos) { | |
145 let mode = this.doc.mode | |
146 if (!mode.innerMode) return mode | |
147 return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode | |
148 }, | |
149 | |
150 getHelper: function(pos, type) { | |
151 return this.getHelpers(pos, type)[0] | |
152 }, | |
153 | |
154 getHelpers: function(pos, type) { | |
155 let found = [] | |
156 if (!helpers.hasOwnProperty(type)) return found | |
157 let help = helpers[type], mode = this.getModeAt(pos) | |
158 if (typeof mode[type] == "string") { | |
159 if (help[mode[type]]) found.push(help[mode[type]]) | |
160 } else if (mode[type]) { | |
161 for (let i = 0; i < mode[type].length; i++) { | |
162 let val = help[mode[type][i]] | |
163 if (val) found.push(val) | |
164 } | |
165 } else if (mode.helperType && help[mode.helperType]) { | |
166 found.push(help[mode.helperType]) | |
167 } else if (help[mode.name]) { | |
168 found.push(help[mode.name]) | |
169 } | |
170 for (let i = 0; i < help._global.length; i++) { | |
171 let cur = help._global[i] | |
172 if (cur.pred(mode, this) && indexOf(found, cur.val) == -1) | |
173 found.push(cur.val) | |
174 } | |
175 return found | |
176 }, | |
177 | |
178 getStateAfter: function(line, precise) { | |
179 let doc = this.doc | |
180 line = clipLine(doc, line == null ? doc.first + doc.size - 1: line) | |
181 return getContextBefore(this, line + 1, precise).state | |
182 }, | |
183 | |
184 cursorCoords: function(start, mode) { | |
185 let pos, range = this.doc.sel.primary() | |
186 if (start == null) pos = range.head | |
187 else if (typeof start == "object") pos = clipPos(this.doc, start) | |
188 else pos = start ? range.from() : range.to() | |
189 return cursorCoords(this, pos, mode || "page") | |
190 }, | |
191 | |
192 charCoords: function(pos, mode) { | |
193 return charCoords(this, clipPos(this.doc, pos), mode || "page") | |
194 }, | |
195 | |
196 coordsChar: function(coords, mode) { | |
197 coords = fromCoordSystem(this, coords, mode || "page") | |
198 return coordsChar(this, coords.left, coords.top) | |
199 }, | |
200 | |
201 lineAtHeight: function(height, mode) { | |
202 height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top | |
203 return lineAtHeight(this.doc, height + this.display.viewOffset) | |
204 }, | |
205 heightAtLine: function(line, mode, includeWidgets) { | |
206 let end = false, lineObj | |
207 if (typeof line == "number") { | |
208 let last = this.doc.first + this.doc.size - 1 | |
209 if (line < this.doc.first) line = this.doc.first | |
210 else if (line > last) { line = last; end = true } | |
211 lineObj = getLine(this.doc, line) | |
212 } else { | |
213 lineObj = line | |
214 } | |
215 return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page", includeWidgets || end).top + | |
216 (end ? this.doc.height - heightAtLine(lineObj) : 0) | |
217 }, | |
218 | |
219 defaultTextHeight: function() { return textHeight(this.display) }, | |
220 defaultCharWidth: function() { return charWidth(this.display) }, | |
221 | |
222 getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo}}, | |
223 | |
224 addWidget: function(pos, node, scroll, vert, horiz) { | |
225 let display = this.display | |
226 pos = cursorCoords(this, clipPos(this.doc, pos)) | |
227 let top = pos.bottom, left = pos.left | |
228 node.style.position = "absolute" | |
229 node.setAttribute("cm-ignore-events", "true") | |
230 this.display.input.setUneditable(node) | |
231 display.sizer.appendChild(node) | |
232 if (vert == "over") { | |
233 top = pos.top | |
234 } else if (vert == "above" || vert == "near") { | |
235 let vspace = Math.max(display.wrapper.clientHeight, this.doc.height), | |
236 hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth) | |
237 // Default to positioning above (if specified and possible); otherwise default to positioning below | |
238 if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) | |
239 top = pos.top - node.offsetHeight | |
240 else if (pos.bottom + node.offsetHeight <= vspace) | |
241 top = pos.bottom | |
242 if (left + node.offsetWidth > hspace) | |
243 left = hspace - node.offsetWidth | |
244 } | |
245 node.style.top = top + "px" | |
246 node.style.left = node.style.right = "" | |
247 if (horiz == "right") { | |
248 left = display.sizer.clientWidth - node.offsetWidth | |
249 node.style.right = "0px" | |
250 } else { | |
251 if (horiz == "left") left = 0 | |
252 else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2 | |
253 node.style.left = left + "px" | |
254 } | |
255 if (scroll) | |
256 scrollIntoView(this, {left, top, right: left + node.offsetWidth, bottom: top + node.offsetHeight}) | |
257 }, | |
258 | |
259 triggerOnKeyDown: methodOp(onKeyDown), | |
260 triggerOnKeyPress: methodOp(onKeyPress), | |
261 triggerOnKeyUp: onKeyUp, | |
262 triggerOnMouseDown: methodOp(onMouseDown), | |
263 | |
264 execCommand: function(cmd) { | |
265 if (commands.hasOwnProperty(cmd)) | |
266 return commands[cmd].call(null, this) | |
267 }, | |
268 | |
269 triggerElectric: methodOp(function(text) { triggerElectric(this, text) }), | |
270 | |
271 findPosH: function(from, amount, unit, visually) { | |
272 let dir = 1 | |
273 if (amount < 0) { dir = -1; amount = -amount } | |
274 let cur = clipPos(this.doc, from) | |
275 for (let i = 0; i < amount; ++i) { | |
276 cur = findPosH(this.doc, cur, dir, unit, visually) | |
277 if (cur.hitSide) break | |
278 } | |
279 return cur | |
280 }, | |
281 | |
282 moveH: methodOp(function(dir, unit) { | |
283 this.extendSelectionsBy(range => { | |
284 if (this.display.shift || this.doc.extend || range.empty()) | |
285 return findPosH(this.doc, range.head, dir, unit, this.options.rtlMoveVisually) | |
286 else | |
287 return dir < 0 ? range.from() : range.to() | |
288 }, sel_move) | |
289 }), | |
290 | |
291 deleteH: methodOp(function(dir, unit) { | |
292 let sel = this.doc.sel, doc = this.doc | |
293 if (sel.somethingSelected()) | |
294 doc.replaceSelection("", null, "+delete") | |
295 else | |
296 deleteNearSelection(this, range => { | |
297 let other = findPosH(doc, range.head, dir, unit, false) | |
298 return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other} | |
299 }) | |
300 }), | |
301 | |
302 findPosV: function(from, amount, unit, goalColumn) { | |
303 let dir = 1, x = goalColumn | |
304 if (amount < 0) { dir = -1; amount = -amount } | |
305 let cur = clipPos(this.doc, from) | |
306 for (let i = 0; i < amount; ++i) { | |
307 let coords = cursorCoords(this, cur, "div") | |
308 if (x == null) x = coords.left | |
309 else coords.left = x | |
310 cur = findPosV(this, coords, dir, unit) | |
311 if (cur.hitSide) break | |
312 } | |
313 return cur | |
314 }, | |
315 | |
316 moveV: methodOp(function(dir, unit) { | |
317 let doc = this.doc, goals = [] | |
318 let collapse = !this.display.shift && !doc.extend && doc.sel.somethingSelected() | |
319 doc.extendSelectionsBy(range => { | |
320 if (collapse) | |
321 return dir < 0 ? range.from() : range.to() | |
322 let headPos = cursorCoords(this, range.head, "div") | |
323 if (range.goalColumn != null) headPos.left = range.goalColumn | |
324 goals.push(headPos.left) | |
325 let pos = findPosV(this, headPos, dir, unit) | |
326 if (unit == "page" && range == doc.sel.primary()) | |
327 addToScrollTop(this, charCoords(this, pos, "div").top - headPos.top) | |
328 return pos | |
329 }, sel_move) | |
330 if (goals.length) for (let i = 0; i < doc.sel.ranges.length; i++) | |
331 doc.sel.ranges[i].goalColumn = goals[i] | |
332 }), | |
333 | |
334 // Find the word at the given position (as returned by coordsChar). | |
335 findWordAt: function(pos) { | |
336 let doc = this.doc, line = getLine(doc, pos.line).text | |
337 let start = pos.ch, end = pos.ch | |
338 if (line) { | |
339 let helper = this.getHelper(pos, "wordChars") | |
340 if ((pos.sticky == "before" || end == line.length) && start) --start; else ++end | |
341 let startChar = line.charAt(start) | |
342 let check = isWordChar(startChar, helper) | |
343 ? ch => isWordChar(ch, helper) | |
344 : /\s/.test(startChar) ? ch => /\s/.test(ch) | |
345 : ch => (!/\s/.test(ch) && !isWordChar(ch)) | |
346 while (start > 0 && check(line.charAt(start - 1))) --start | |
347 while (end < line.length && check(line.charAt(end))) ++end | |
348 } | |
349 return new Range(Pos(pos.line, start), Pos(pos.line, end)) | |
350 }, | |
351 | |
352 toggleOverwrite: function(value) { | |
353 if (value != null && value == this.state.overwrite) return | |
354 if (this.state.overwrite = !this.state.overwrite) | |
355 addClass(this.display.cursorDiv, "CodeMirror-overwrite") | |
356 else | |
357 rmClass(this.display.cursorDiv, "CodeMirror-overwrite") | |
358 | |
359 signal(this, "overwriteToggle", this, this.state.overwrite) | |
360 }, | |
361 hasFocus: function() { return this.display.input.getField() == activeElt(root(this)) }, | |
362 isReadOnly: function() { return !!(this.options.readOnly || this.doc.cantEdit) }, | |
363 | |
364 scrollTo: methodOp(function (x, y) { scrollToCoords(this, x, y) }), | |
365 getScrollInfo: function() { | |
366 let scroller = this.display.scroller | |
367 return {left: scroller.scrollLeft, top: scroller.scrollTop, | |
368 height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight, | |
369 width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth, | |
370 clientHeight: displayHeight(this), clientWidth: displayWidth(this)} | |
371 }, | |
372 | |
373 scrollIntoView: methodOp(function(range, margin) { | |
374 if (range == null) { | |
375 range = {from: this.doc.sel.primary().head, to: null} | |
376 if (margin == null) margin = this.options.cursorScrollMargin | |
377 } else if (typeof range == "number") { | |
378 range = {from: Pos(range, 0), to: null} | |
379 } else if (range.from == null) { | |
380 range = {from: range, to: null} | |
381 } | |
382 if (!range.to) range.to = range.from | |
383 range.margin = margin || 0 | |
384 | |
385 if (range.from.line != null) { | |
386 scrollToRange(this, range) | |
387 } else { | |
388 scrollToCoordsRange(this, range.from, range.to, range.margin) | |
389 } | |
390 }), | |
391 | |
392 setSize: methodOp(function(width, height) { | |
393 let interpret = val => typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val | |
394 if (width != null) this.display.wrapper.style.width = interpret(width) | |
395 if (height != null) this.display.wrapper.style.height = interpret(height) | |
396 if (this.options.lineWrapping) clearLineMeasurementCache(this) | |
397 let lineNo = this.display.viewFrom | |
398 this.doc.iter(lineNo, this.display.viewTo, line => { | |
399 if (line.widgets) for (let i = 0; i < line.widgets.length; i++) | |
400 if (line.widgets[i].noHScroll) { regLineChange(this, lineNo, "widget"); break } | |
401 ++lineNo | |
402 }) | |
403 this.curOp.forceUpdate = true | |
404 signal(this, "refresh", this) | |
405 }), | |
406 | |
407 operation: function(f){return runInOp(this, f)}, | |
408 startOperation: function(){return startOperation(this)}, | |
409 endOperation: function(){return endOperation(this)}, | |
410 | |
411 refresh: methodOp(function() { | |
412 let oldHeight = this.display.cachedTextHeight | |
413 regChange(this) | |
414 this.curOp.forceUpdate = true | |
415 clearCaches(this) | |
416 scrollToCoords(this, this.doc.scrollLeft, this.doc.scrollTop) | |
417 updateGutterSpace(this.display) | |
418 if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5 || this.options.lineWrapping) | |
419 estimateLineHeights(this) | |
420 signal(this, "refresh", this) | |
421 }), | |
422 | |
423 swapDoc: methodOp(function(doc) { | |
424 let old = this.doc | |
425 old.cm = null | |
426 // Cancel the current text selection if any (#5821) | |
427 if (this.state.selectingText) this.state.selectingText() | |
428 attachDoc(this, doc) | |
429 clearCaches(this) | |
430 this.display.input.reset() | |
431 scrollToCoords(this, doc.scrollLeft, doc.scrollTop) | |
432 this.curOp.forceScroll = true | |
433 signalLater(this, "swapDoc", this, old) | |
434 return old | |
435 }), | |
436 | |
437 phrase: function(phraseText) { | |
438 let phrases = this.options.phrases | |
439 return phrases && Object.prototype.hasOwnProperty.call(phrases, phraseText) ? phrases[phraseText] : phraseText | |
440 }, | |
441 | |
442 getInputField: function(){return this.display.input.getField()}, | |
443 getWrapperElement: function(){return this.display.wrapper}, | |
444 getScrollerElement: function(){return this.display.scroller}, | |
445 getGutterElement: function(){return this.display.gutters} | |
446 } | |
447 eventMixin(CodeMirror) | |
448 | |
449 CodeMirror.registerHelper = function(type, name, value) { | |
450 if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {_global: []} | |
451 helpers[type][name] = value | |
452 } | |
453 CodeMirror.registerGlobalHelper = function(type, name, predicate, value) { | |
454 CodeMirror.registerHelper(type, name, value) | |
455 helpers[type]._global.push({pred: predicate, val: value}) | |
456 } | |
457 } | |
458 | |
459 // Used for horizontal relative motion. Dir is -1 or 1 (left or | |
460 // right), unit can be "codepoint", "char", "column" (like char, but | |
461 // doesn't cross line boundaries), "word" (across next word), or | |
462 // "group" (to the start of next group of word or | |
463 // non-word-non-whitespace chars). The visually param controls | |
464 // whether, in right-to-left text, direction 1 means to move towards | |
465 // the next index in the string, or towards the character to the right | |
466 // of the current position. The resulting position will have a | |
467 // hitSide=true property if it reached the end of the document. | |
468 function findPosH(doc, pos, dir, unit, visually) { | |
469 let oldPos = pos | |
470 let origDir = dir | |
471 let lineObj = getLine(doc, pos.line) | |
472 let lineDir = visually && doc.direction == "rtl" ? -dir : dir | |
473 function findNextLine() { | |
474 let l = pos.line + lineDir | |
475 if (l < doc.first || l >= doc.first + doc.size) return false | |
476 pos = new Pos(l, pos.ch, pos.sticky) | |
477 return lineObj = getLine(doc, l) | |
478 } | |
479 function moveOnce(boundToLine) { | |
480 let next | |
481 if (unit == "codepoint") { | |
482 let ch = lineObj.text.charCodeAt(pos.ch + (dir > 0 ? 0 : -1)) | |
483 if (isNaN(ch)) { | |
484 next = null | |
485 } else { | |
486 let astral = dir > 0 ? ch >= 0xD800 && ch < 0xDC00 : ch >= 0xDC00 && ch < 0xDFFF | |
487 next = new Pos(pos.line, Math.max(0, Math.min(lineObj.text.length, pos.ch + dir * (astral ? 2 : 1))), -dir) | |
488 } | |
489 } else if (visually) { | |
490 next = moveVisually(doc.cm, lineObj, pos, dir) | |
491 } else { | |
492 next = moveLogically(lineObj, pos, dir) | |
493 } | |
494 if (next == null) { | |
495 if (!boundToLine && findNextLine()) | |
496 pos = endOfLine(visually, doc.cm, lineObj, pos.line, lineDir) | |
497 else | |
498 return false | |
499 } else { | |
500 pos = next | |
501 } | |
502 return true | |
503 } | |
504 | |
505 if (unit == "char" || unit == "codepoint") { | |
506 moveOnce() | |
507 } else if (unit == "column") { | |
508 moveOnce(true) | |
509 } else if (unit == "word" || unit == "group") { | |
510 let sawType = null, group = unit == "group" | |
511 let helper = doc.cm && doc.cm.getHelper(pos, "wordChars") | |
512 for (let first = true;; first = false) { | |
513 if (dir < 0 && !moveOnce(!first)) break | |
514 let cur = lineObj.text.charAt(pos.ch) || "\n" | |
515 let type = isWordChar(cur, helper) ? "w" | |
516 : group && cur == "\n" ? "n" | |
517 : !group || /\s/.test(cur) ? null | |
518 : "p" | |
519 if (group && !first && !type) type = "s" | |
520 if (sawType && sawType != type) { | |
521 if (dir < 0) {dir = 1; moveOnce(); pos.sticky = "after"} | |
522 break | |
523 } | |
524 | |
525 if (type) sawType = type | |
526 if (dir > 0 && !moveOnce(!first)) break | |
527 } | |
528 } | |
529 let result = skipAtomic(doc, pos, oldPos, origDir, true) | |
530 if (equalCursorPos(oldPos, result)) result.hitSide = true | |
531 return result | |
532 } | |
533 | |
534 // For relative vertical movement. Dir may be -1 or 1. Unit can be | |
535 // "page" or "line". The resulting position will have a hitSide=true | |
536 // property if it reached the end of the document. | |
537 function findPosV(cm, pos, dir, unit) { | |
538 let doc = cm.doc, x = pos.left, y | |
539 if (unit == "page") { | |
540 let pageSize = Math.min(cm.display.wrapper.clientHeight, win(cm).innerHeight || doc(cm).documentElement.clientHeight) | |
541 let moveAmount = Math.max(pageSize - .5 * textHeight(cm.display), 3) | |
542 y = (dir > 0 ? pos.bottom : pos.top) + dir * moveAmount | |
543 | |
544 } else if (unit == "line") { | |
545 y = dir > 0 ? pos.bottom + 3 : pos.top - 3 | |
546 } | |
547 let target | |
548 for (;;) { | |
549 target = coordsChar(cm, x, y) | |
550 if (!target.outside) break | |
551 if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break } | |
552 y += dir * 5 | |
553 } | |
554 return target | |
555 } |