]> git.gir.st - VimFx.git/blob - extension/lib/commands-frame.coffee
Add `g[`, `g]` and a jump list
[VimFx.git] / extension / lib / commands-frame.coffee
1 ###
2 # Copyright Simon Lydell 2015, 2016.
3 #
4 # This file is part of VimFx.
5 #
6 # VimFx is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # VimFx is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with VimFx. If not, see <http://www.gnu.org/licenses/>.
18 ###
19
20 # This file is the equivalent to commands.coffee, but for frame scripts,
21 # allowing interaction with web page content. Most “commands” here have the
22 # same name as the command in commands.coffee that calls it. There are also a
23 # few more generalized “commands” used in more than one place.
24
25 markableElements = require('./markable-elements')
26 prefs = require('./prefs')
27 SelectionManager = require('./selection')
28 translate = require('./translate')
29 utils = require('./utils')
30 viewportUtils = require('./viewport')
31
32 {FORWARD, BACKWARD} = SelectionManager
33 {isProperLink, isTextInputElement, isTypingElement, isContentEditable} = utils
34
35 XULDocument = Ci.nsIDOMXULDocument
36
37 # <http://www.w3.org/html/wg/drafts/html/master/dom.html#wai-aria>
38 CLICKABLE_ARIA_ROLES = [
39 'link', 'button', 'tab'
40 'checkbox', 'radio', 'combobox', 'option', 'slider', 'textbox'
41 'menuitem', 'menuitemcheckbox', 'menuitemradio'
42 ]
43
44 createComplementarySelectors = (selectors) ->
45 return [
46 selectors.join(',')
47 selectors.map((selector) -> ":not(#{selector})").join('')
48 ]
49
50 FOLLOW_DEFAULT_SELECTORS = createComplementarySelectors([
51 'a', 'button', 'input', 'textarea', 'select'
52 '[role]', '[contenteditable]'
53 ])
54
55 FOLLOW_CONTEXT_TAGS = [
56 'a', 'button', 'input', 'textarea', 'select'
57 'img', 'audio', 'video', 'canvas', 'embed', 'object'
58 ]
59
60 FOLLOW_CONTEXT_SELECTORS = createComplementarySelectors(FOLLOW_CONTEXT_TAGS)
61
62 FOLLOW_SELECTABLE_SELECTORS =
63 createComplementarySelectors(['div', 'span']).reverse()
64
65 commands = {}
66
67 commands.go_up_path = ({vim, count = 1}) ->
68 {pathname} = vim.content.location
69 newPathname = pathname.replace(/// (?: [^/]+(/|$) ){1,#{count}} $ ///, '')
70 if newPathname == pathname
71 vim.notify(translate('notification.go_up_path.limit'))
72 else
73 vim.content.location.pathname = newPathname
74
75 commands.go_to_root = ({vim}) ->
76 # `.origin` is `'null'` (as a string) on `about:` pages.
77 if "#{vim.content.location.origin}/" in [vim.content.location.href, 'null/']
78 vim.notify(translate('notification.go_up_path.limit'))
79 else
80 vim.content.location.href = vim.content.location.origin
81
82 commands.scroll = (args) ->
83 {vim} = args
84 return unless activeElement = utils.getActiveElement(vim.content)
85 element =
86 # If no element is focused on the page, the active element is the topmost
87 # `<body>`, and blurring it is a no-op. If it is scrollable, it means that
88 # you can’t blur it in order to scroll `<html>`. Therefore it may only be
89 # scrolled if it has been explicitly focused.
90 if vim.state.scrollableElements.has(activeElement) and
91 (activeElement != vim.content.document.body or
92 vim.state.explicitBodyFocus)
93 activeElement
94 else
95 vim.state.scrollableElements.filterSuitableDefault()
96 viewportUtils.scroll(element, args)
97
98 commands.mark_scroll_position = (args) ->
99 {vim, keyStr, notify = true, addToJumpList = false} = args
100
101 vim.state.marks[keyStr] = vim.state.scrollableElements.getPageScrollPosition()
102
103 if addToJumpList
104 vim.addToJumpList()
105
106 if notify
107 vim.notify(translate('notification.mark_scroll_position.success', keyStr))
108
109 commands.scroll_to_mark = (args) ->
110 {vim, extra: {keyStr, lastPositionMark}} = args
111
112 unless keyStr of vim.state.marks
113 vim.notify(translate('notification.scroll_to_mark.none', keyStr))
114 return
115
116 args.amounts = vim.state.marks[keyStr]
117 element = vim.state.scrollableElements.filterSuitableDefault()
118
119 commands.mark_scroll_position({
120 vim
121 keyStr: lastPositionMark
122 notify: false
123 addToJumpList: true
124 })
125 viewportUtils.scroll(element, args)
126
127 commands.scroll_to_position = (args) ->
128 {vim, extra: {count, direction, lastPositionMark}} = args
129
130 if direction == 'previous' and vim.state.jumpListIndex >= 0 and
131 vim.state.jumpListIndex == vim.state.jumpList.length - 1
132 vim.addToJumpList()
133
134 {jumpList, jumpListIndex} = vim.state
135 maxIndex = jumpList.length - 1
136
137 if (direction == 'previous' and jumpListIndex <= 0) or
138 (direction == 'next' and jumpListIndex >= maxIndex)
139 vim.notify(translate("notification.scroll_to_#{direction}_position.limit"))
140 return
141
142 index = jumpListIndex + count * (if direction == 'previous' then -1 else +1)
143 index = Math.max(index, 0)
144 index = Math.min(index, maxIndex)
145
146 args.amounts = jumpList[index]
147 element = vim.state.scrollableElements.filterSuitableDefault()
148
149 commands.mark_scroll_position({vim, keyStr: lastPositionMark, notify: false})
150 viewportUtils.scroll(element, args)
151 vim.state.jumpListIndex = index
152
153 helper_follow = (options, matcher, {vim, pass}) ->
154 {id, combine = true, selectors = FOLLOW_DEFAULT_SELECTORS} = options
155 if vim.content.document instanceof XULDocument
156 selectors = ['*']
157
158 if pass == 'auto'
159 pass = if selectors.length == 2 then 'first' else 'single'
160
161 vim.state.markerElements = [] if pass in ['single', 'first']
162 hrefs = {}
163
164 filter = (element, getElementShape) ->
165 type = matcher({vim, element, getElementShape})
166 if vim.hintMatcher
167 type = vim.hintMatcher(id, element, type)
168 if pass == 'complementary'
169 type = if type then null else 'complementary'
170 return unless type
171
172 shape = getElementShape(element)
173
174 unless shape.nonCoveredPoint then switch
175 # CodeMirror editor uses a tiny hidden textarea positioned at the caret.
176 # Targeting those are the only reliable way of focusing CodeMirror
177 # editors, and doing so without moving the caret.
178 when id == 'normal' and element.localName == 'textarea' and
179 element.ownerGlobal == vim.content
180 rect = element.getBoundingClientRect()
181 # Use `.clientWidth` instead of `rect.width` because the latter includes
182 # the width of the borders of the textarea, which are unreliable.
183 if element.clientWidth <= 1 and rect.height > 0
184 shape = {
185 nonCoveredPoint: {
186 x: rect.left
187 y: rect.top + rect.height / 2
188 offset: {left: 0, top: 0}
189 }
190 area: rect.width * rect.height
191 }
192
193 # Putting a large `<input type="file">` inside a smaller wrapper element
194 # with `overflow: hidden;` seems to be a common pattern, used both on
195 # addons.mozilla.org and <https://blueimp.github.io/jQuery-File-Upload/>.
196 when id in ['normal', 'focus'] and element.localName == 'input' and
197 element.type == 'file' and element.parentElement?
198 parentShape = getElementShape(element.parentElement)
199 shape = parentShape if parentShape.area <= shape.area
200
201 if not shape.nonCoveredPoint and pass == 'complementary'
202 shape = getElementShape(element, -1)
203
204 return unless shape.nonCoveredPoint
205
206 text = utils.getText(element)
207
208 originalRect = element.getBoundingClientRect()
209 length = vim.state.markerElements.push({element, originalRect})
210 wrapper = {
211 type, text, shape,
212 combinedArea: shape.area
213 elementIndex: length - 1
214 parentIndex: null
215 }
216
217 if wrapper.type == 'link'
218 {href} = element
219 wrapper.href = href
220
221 # Combine links with the same href.
222 if combine and wrapper.type == 'link' and
223 # If the element has an 'onclick' attribute we cannot be sure that all
224 # links with this href actually do the same thing. On some pages, such
225 # as startpage.com, actual proper links have the 'onclick' attribute,
226 # so we can’t exclude such links in `utils.isProperLink`.
227 not element.hasAttribute?('onclick') and
228 # GitHub’s diff expansion buttons are links with both `href` and
229 # `data-url`. They are JavaScript-powered using the latter attribute.
230 not element.hasAttribute?('data-url')
231 if href of hrefs
232 parent = hrefs[href]
233 wrapper.parentIndex = parent.elementIndex
234 parent.combinedArea += wrapper.shape.area
235 parent.numChildren += 1
236 else
237 hrefs[href] = wrapper
238
239 return wrapper
240
241 selector =
242 if pass == 'complementary'
243 '*'
244 else
245 selectors[if pass == 'second' then 1 else 0]
246 return {
247 wrappers: markableElements.find(vim.content, filter, selector)
248 viewport: viewportUtils.getWindowViewport(vim.content)
249 pass
250 }
251
252 commands.follow = helper_follow.bind(
253 null, {id: 'normal'},
254 ({vim, element, getElementShape}) ->
255 document = element.ownerDocument
256 isXUL = (document instanceof XULDocument)
257 type = null
258 switch
259 # Bootstrap. Match these before regular links, because especially slider
260 # “buttons” often get the same hint otherwise.
261 when element.hasAttribute?('data-toggle') or
262 element.hasAttribute?('data-dismiss') or
263 element.hasAttribute?('data-slide') or
264 element.hasAttribute?('data-slide-to')
265 type = 'clickable'
266 when isProperLink(element)
267 type = 'link'
268 when isTypingElement(element)
269 type = 'text'
270 when element.localName in ['a', 'button'] or
271 element.getAttribute?('role') in CLICKABLE_ARIA_ROLES or
272 # <http://www.w3.org/TR/wai-aria/states_and_properties>
273 element.hasAttribute?('aria-controls') or
274 element.hasAttribute?('aria-pressed') or
275 element.hasAttribute?('aria-checked') or
276 (element.hasAttribute?('aria-haspopup') and
277 element.getAttribute?('role') != 'menu')
278 type = 'clickable'
279 when utils.isFocusable(element) and
280 # Google Drive Documents. The hint for this element would cover the
281 # real hint that allows you to focus the document to start typing.
282 element.id != 'docs-editor'
283 type = 'clickable'
284 when element != vim.state.scrollableElements.largest and
285 vim.state.scrollableElements.has(element)
286 type = 'scrollable'
287 when element.hasAttribute?('onclick') or
288 element.hasAttribute?('onmousedown') or
289 element.hasAttribute?('onmouseup') or
290 element.hasAttribute?('oncommand') or
291 # Twitter.
292 element.classList?.contains('js-new-tweets-bar') or
293 element.hasAttribute?('data-permalink-path') or
294 # Feedly.
295 element.hasAttribute?('data-app-action') or
296 element.hasAttribute?('data-uri') or
297 element.hasAttribute?('data-page-action') or
298 # Google Drive Document.
299 element.classList?.contains('kix-appview-editor')
300 type = 'clickable'
301 # Facebook comment fields.
302 when element.parentElement?.classList?.contains('UFIInputContainer')
303 type = 'clickable-special'
304 # Putting markers on `<label>` elements is generally redundant, because
305 # its `<input>` gets one. However, some sites hide the actual `<input>`
306 # but keeps the `<label>` to click, either for styling purposes or to keep
307 # the `<input>` hidden until it is used. In those cases we should add a
308 # marker for the `<label>`. Trying to access `.control` on an element in
309 # `about:config` throws an error, so exclude XUL pages.
310 when not isXUL and element.localName == 'label' and element.control and
311 not getElementShape(element.control).nonCoveredPoint
312 type = 'clickable'
313 # Last resort checks for elements that might be clickable because of
314 # JavaScript.
315 when (not isXUL and
316 # It is common to listen for clicks on `<html>` or `<body>`. Don’t
317 # waste time on them.
318 element not in [document.documentElement, document.body]) and
319 (utils.includes(element.className, 'button') or
320 utils.includes(element.getAttribute?('aria-label'), 'close') or
321 # Do this last as it’s a potentially expensive check.
322 (utils.hasEventListeners(element, 'click') and
323 # Twitter. The hint for this element would cover the hint for
324 # showing more tweets.
325 not element.classList?.contains('js-new-items-bar-container')))
326 # Make a quick check for likely clickable descendants, to reduce the
327 # number of false positives. the element might be a “button-wrapper” or
328 # a large element with a click-tracking event listener.
329 unless element.querySelector?('a, button, input, [class*=button]')
330 type = 'clickable'
331 # When viewing an image it should get a marker to toggle zoom. This is the
332 # most unlikely rule to match, so keep it last.
333 when document.body?.childElementCount == 1 and
334 element.localName == 'img' and
335 (element.classList?.contains('overflowing') or
336 element.classList?.contains('shrinkToFit'))
337 type = 'clickable'
338 type = null if isXUL and element.classList?.contains('textbox-input')
339 return type
340 )
341
342 commands.follow_in_tab = helper_follow.bind(
343 null, {id: 'tab', selectors: ['a']},
344 ({element}) ->
345 type = if isProperLink(element) then 'link' else null
346 return type
347 )
348
349 commands.follow_copy = helper_follow.bind(
350 null, {id: 'copy'},
351 ({element}) ->
352 type = switch
353 when isProperLink(element)
354 'link'
355 when isContentEditable(element)
356 'contenteditable'
357 when isTypingElement(element)
358 'text'
359 else
360 null
361 return type
362 )
363
364 commands.follow_focus = helper_follow.bind(
365 null, {id: 'focus', combine: false},
366 ({vim, element}) ->
367 type = switch
368 when element.tabIndex > -1
369 'focusable'
370 when element != vim.state.scrollableElements.largest and
371 vim.state.scrollableElements.has(element)
372 'scrollable'
373 else
374 null
375 return type
376 )
377
378 commands.follow_context = helper_follow.bind(
379 null, {id: 'context', selectors: FOLLOW_CONTEXT_SELECTORS},
380 ({element}) ->
381 type =
382 if element.localName in FOLLOW_CONTEXT_TAGS or
383 utils.hasMarkableTextNode(element)
384 'context'
385 else
386 null
387 return type
388 )
389
390 commands.follow_selectable = helper_follow.bind(
391 null, {id: 'selectable', selectors: FOLLOW_SELECTABLE_SELECTORS},
392 ({element}) ->
393 type =
394 if utils.hasMarkableTextNode(element)
395 'selectable'
396 else
397 null
398 return type
399 )
400
401 commands.focus_marker_element = ({vim, elementIndex, browserOffset, options}) ->
402 {element} = vim.state.markerElements[elementIndex]
403 # To be able to focus scrollable elements, `FLAG_BYKEY` _has_ to be used.
404 options.flag = 'FLAG_BYKEY' if vim.state.scrollableElements.has(element)
405 utils.focusElement(element, options)
406 vim.clearHover()
407 vim.setHover(element, browserOffset)
408
409 commands.click_marker_element = (
410 {vim, elementIndex, type, browserOffset, preventTargetBlank}
411 ) ->
412 {element} = vim.state.markerElements[elementIndex]
413 if element.target == '_blank' and preventTargetBlank
414 targetReset = element.target
415 element.target = ''
416 if type == 'clickable-special'
417 element.click()
418 else
419 isXUL = (element.ownerDocument instanceof XULDocument)
420 sequence = switch
421 when isXUL
422 if element.localName == 'tab' then ['mousedown'] else 'click-xul'
423 when type == 'context'
424 'context'
425 else
426 'click'
427 utils.simulateMouseEvents(element, sequence, browserOffset)
428 utils.openDropdown(element)
429 element.target = targetReset if targetReset
430
431 commands.copy_marker_element = ({vim, elementIndex, property}) ->
432 {element} = vim.state.markerElements[elementIndex]
433 utils.writeToClipboard(element[property])
434
435 commands.element_text_select = ({vim, elementIndex, full, scroll = false}) ->
436 {element} = vim.state.markerElements[elementIndex]
437 window = element.ownerGlobal
438 selection = window.getSelection()
439 range = window.document.createRange()
440
441 # Try to scroll the element into view, but keep the caret visible.
442 if scroll
443 viewport = viewportUtils.getWindowViewport(window)
444 rect = element.getBoundingClientRect()
445 block = switch
446 when rect.bottom > viewport.bottom
447 'end'
448 when rect.top < viewport.top and rect.height < viewport.height
449 'start'
450 else
451 null
452 if block
453 smooth = (
454 prefs.root.get('general.smoothScroll') and
455 prefs.root.get('general.smoothScroll.other')
456 )
457 element.scrollIntoView({
458 block
459 behavior: if smooth then 'smooth' else 'instant'
460 })
461
462 if full
463 range.selectNodeContents(element)
464 else
465 result = viewportUtils.getFirstNonWhitespace(element)
466 if result
467 [node, offset] = result
468 range.setStart(node, offset)
469 range.setEnd(node, offset)
470 else
471 range.setStartBefore(element)
472 range.setEndBefore(element)
473
474 utils.clearSelectionDeep(vim.content)
475 window.focus()
476 selection.addRange(range)
477
478 if full
479 # Force the selected text to appear in the “selection clipboard”. Note:
480 # `selection.modify` would not make any difference here. That makes sense,
481 # since web pages that programmatically change the selection shouldn’t
482 # affect the user’s clipboard. `SelectionManager` uses an internal Firefox
483 # API with clipboard privileges.
484 selectionManager = new SelectionManager(window)
485 error = selectionManager.moveCaret('characterMove', BACKWARD)
486 selectionManager.moveCaret('characterMove', FORWARD) unless error
487
488 commands.follow_pattern = ({vim, type, browserOffset, options}) ->
489 {document} = vim.content
490
491 # If there’s a `<link rel=prev/next>` element we use that.
492 for link in document.head?.getElementsByTagName('link')
493 # Also support `rel=previous`, just like Google.
494 if type == link.rel.toLowerCase().replace(/^previous$/, 'prev')
495 vim.content.location.href = link.href
496 return
497
498 # Otherwise we look for a link or button on the page that seems to go to the
499 # previous or next page.
500 candidates = document.querySelectorAll(options.pattern_selector)
501
502 # Note: Earlier patterns should be favored.
503 {patterns} = options
504
505 # Search for the prev/next patterns in the following attributes of the
506 # element. `rel` should be kept as the first attribute, since the standard way
507 # of marking up prev/next links (`rel="prev"` and `rel="next"`) should be
508 # favored. Even though some of these attributes only allow a fixed set of
509 # keywords, we pattern-match them anyways since lots of sites don’t follow the
510 # spec and use the attributes arbitrarily.
511 attrs = options.pattern_attrs
512
513 matchingLink = do ->
514 # First search in attributes (favoring earlier attributes) as it's likely
515 # that they are more specific than text contexts.
516 for attr in attrs
517 for regex in patterns
518 for element in candidates
519 return element if regex.test(element.getAttribute?(attr))
520
521 # Then search in element contents.
522 for regex in patterns
523 for element in candidates
524 return element if regex.test(element.textContent)
525
526 return null
527
528 if matchingLink
529 utils.simulateMouseEvents(matchingLink, 'click', browserOffset)
530 # When you go to the next page of GitHub’s code search results, the page is
531 # loaded with AJAX. GitHub then annoyingly focuses its search input. This
532 # autofocus cannot be prevented in a reliable way, because the case is
533 # indistinguishable from a button whose job is to focus some text input.
534 # However, in this command we know for sure that we can prevent the next
535 # focus. This must be done _after_ the click has been triggered, since
536 # clicks count as page interactions.
537 vim.markPageInteraction(false)
538 else
539 vim.notify(translate("notification.follow_#{type}.none"))
540
541 commands.focus_text_input = ({vim, count = null}) ->
542 {lastFocusedTextInput} = vim.state
543
544 if lastFocusedTextInput and utils.isDetached(lastFocusedTextInput)
545 lastFocusedTextInput = null
546
547 candidates = utils.querySelectorAllDeep(
548 vim.content, 'input, textarea, textbox, [contenteditable]'
549 )
550 inputs = Array.filter(candidates, (element) ->
551 return isTextInputElement(element) and utils.area(element) > 0
552 )
553 if lastFocusedTextInput and lastFocusedTextInput not in inputs
554 inputs.push(lastFocusedTextInput)
555 inputs.sort((a, b) -> a.tabIndex - b.tabIndex)
556
557 if inputs.length == 0
558 vim.notify(translate('notification.focus_text_input.none'))
559 return
560
561 num = switch
562 when count?
563 count
564 when lastFocusedTextInput
565 inputs.indexOf(lastFocusedTextInput) + 1
566 else
567 1
568 index = Math.min(num, inputs.length) - 1
569 select = (count? or not vim.state.hasFocusedTextInput)
570 utils.focusElement(inputs[index], {select})
571 vim.state.inputs = inputs
572
573 commands.clear_inputs = ({vim}) ->
574 vim.state.inputs = null
575
576 commands.move_focus = ({vim, direction}) ->
577 return false unless vim.state.inputs
578 index = vim.state.inputs.indexOf(utils.getActiveElement(vim.content))
579 # If there’s only one input, `<tab>` would cycle to itself, making it feel
580 # like `<tab>` was not working. Then it’s better to let `<tab>` work as it
581 # usually does.
582 if index == -1 or vim.state.inputs.length <= 1
583 vim.state.inputs = null
584 return false
585 else
586 {inputs} = vim.state
587 nextInput = inputs[(index + direction) %% inputs.length]
588 utils.focusElement(nextInput, {select: true})
589 return true
590
591 # This is an attempt to enhance Firefox’s native “Find in page” functionality.
592 # Firefox starts searching after the end of the first selection range, or from
593 # the top of the page if there are no selection ranges. If there are frames, the
594 # top-most document in DOM order with selections seems to be used.
595 #
596 # Replace the current selection with one single range. (Searching clears the
597 # previous selection anyway.) That single range is either the first visible
598 # range, or a newly created (and collapsed) one at the top of the viewport. This
599 # way we can control where Firefox searches from.
600 commands.find_from_top_of_viewport = ({vim, direction}) ->
601 viewport = viewportUtils.getWindowViewport(vim.content)
602
603 range = viewportUtils.getFirstVisibleRange(vim.content, viewport)
604 if range
605 window = range.startContainer.ownerGlobal
606 selection = window.getSelection()
607 utils.clearSelectionDeep(vim.content)
608 window.focus()
609 # When the next match is in another frame than the current selection (A),
610 # Firefox won’t clear that selection before making a match selection (B) in
611 # the other frame. When searching again, selection B is cleared because
612 # selection A appears further up the viewport. This causes us to search
613 # _again_ from selection A, rather than selection B. In effect, we get stuck
614 # re-selecting selection B over and over. Therefore, collapse the range
615 # first, in case Firefox doesn’t.
616 range.collapse()
617 selection.addRange(range)
618 # Collapsing the range causes backwards search to keep re-selecting the same
619 # match. Therefore, move it one character back.
620 selection.modify('move', 'backward', 'character') if direction == BACKWARD
621 return
622
623 result = viewportUtils.getFirstVisibleText(vim.content, viewport)
624 return unless result
625 [textNode, offset] = result
626
627 utils.clearSelectionDeep(vim.content)
628 window = textNode.ownerGlobal
629 window.focus()
630 range = window.document.createRange()
631 range.setStart(textNode, offset)
632 range.setEnd(textNode, offset)
633 selection = window.getSelection()
634 selection.addRange(range)
635
636 commands.esc = (args) ->
637 {vim} = args
638 commands.blur_active_element(args)
639 vim.clearHover()
640 utils.clearSelectionDeep(vim.content)
641
642 {document} = vim.content
643 if document.exitFullscreen
644 document.exitFullscreen()
645 else
646 document.mozCancelFullScreen()
647
648 commands.blur_active_element = ({vim}) ->
649 vim.state.explicitBodyFocus = false
650 utils.blurActiveElement(vim.content)
651
652 helper_create_selection_manager = (vim) ->
653 window = utils.getActiveElement(vim.content)?.ownerGlobal ? vim.content
654 return new SelectionManager(window)
655
656 commands.enable_caret = ({vim}) ->
657 return unless selectionManager = helper_create_selection_manager(vim)
658 selectionManager.enableCaret()
659
660 commands.move_caret = ({vim, method, direction, select, count}) ->
661 return unless selectionManager = helper_create_selection_manager(vim)
662 for [0...count] by 1
663 if selectionManager[method]
664 error = selectionManager[method](direction, select)
665 else
666 error = selectionManager.moveCaret(method, direction, select)
667 break if error
668 return
669
670 commands.toggle_selection = ({vim, select}) ->
671 return unless selectionManager = helper_create_selection_manager(vim)
672 if select
673 vim.notify(translate('notification.toggle_selection.enter'))
674 else
675 selectionManager.collapse()
676
677 commands.toggle_selection_direction = ({vim}) ->
678 return unless selectionManager = helper_create_selection_manager(vim)
679 selectionManager.reverseDirection()
680
681 commands.get_selection = ({vim}) ->
682 return unless selectionManager = helper_create_selection_manager(vim)
683 return selectionManager.selection.toString()
684
685 commands.collapse_selection = ({vim}) ->
686 return unless selectionManager = helper_create_selection_manager(vim)
687 selectionManager.collapse()
688
689 commands.clear_selection = ({vim}) ->
690 utils.clearSelectionDeep(vim.content)
691
692 commands.modal = ({vim, type, args}) ->
693 return vim.content[type](args...)
694
695 module.exports = commands
Imprint / Impressum