]> git.gir.st - VimFx.git/blob - extension/lib/commands-frame.coffee
Change license to MIT
[VimFx.git] / extension / lib / commands-frame.coffee
1 # This file is the equivalent to commands.coffee, but for frame scripts,
2 # allowing interaction with web page content. Most “commands” here have the
3 # same name as the command in commands.coffee that calls it. There are also a
4 # few more generalized “commands” used in more than one place.
5
6 markableElements = require('./markable-elements')
7 prefs = require('./prefs')
8 SelectionManager = require('./selection')
9 translate = require('./translate')
10 utils = require('./utils')
11 viewportUtils = require('./viewport')
12
13 {FORWARD, BACKWARD} = SelectionManager
14 {isProperLink, isTextInputElement, isTypingElement, isContentEditable} = utils
15
16 XULDocument = Ci.nsIDOMXULDocument
17
18 # <http://www.w3.org/html/wg/drafts/html/master/dom.html#wai-aria>
19 CLICKABLE_ARIA_ROLES = [
20 'link', 'button', 'tab'
21 'checkbox', 'radio', 'combobox', 'option', 'slider', 'textbox'
22 'menuitem', 'menuitemcheckbox', 'menuitemradio'
23 ]
24
25 createComplementarySelectors = (selectors) ->
26 return [
27 selectors.join(',')
28 selectors.map((selector) -> ":not(#{selector})").join('')
29 ]
30
31 FOLLOW_DEFAULT_SELECTORS = createComplementarySelectors([
32 'a', 'button', 'input', 'textarea', 'select', 'label'
33 '[role]', '[contenteditable]'
34 ])
35
36 FOLLOW_CONTEXT_TAGS = [
37 'a', 'button', 'input', 'textarea', 'select'
38 'img', 'audio', 'video', 'canvas', 'embed', 'object'
39 ]
40
41 FOLLOW_CONTEXT_SELECTORS = createComplementarySelectors(FOLLOW_CONTEXT_TAGS)
42
43 FOLLOW_SELECTABLE_SELECTORS =
44 createComplementarySelectors(['div', 'span']).reverse()
45
46 commands = {}
47
48 commands.go_up_path = ({vim, count = 1}) ->
49 {pathname} = vim.content.location
50 newPathname = pathname.replace(/// (?: [^/]+(/|$) ){1,#{count}} $ ///, '')
51 if newPathname == pathname
52 vim.notify(translate('notification.go_up_path.limit'))
53 else
54 vim.content.location.pathname = newPathname
55
56 commands.go_to_root = ({vim}) ->
57 # `.origin` is `'null'` (as a string) on `about:` pages.
58 if "#{vim.content.location.origin}/" in [vim.content.location.href, 'null/']
59 vim.notify(translate('notification.go_up_path.limit'))
60 else
61 vim.content.location.href = vim.content.location.origin
62
63 commands.scroll = (args) ->
64 {vim} = args
65 return unless activeElement = utils.getActiveElement(vim.content)
66
67 # If the active element, or one of its parents, is scrollable, use that
68 # element.
69 scrollElement = activeElement
70 until vim.state.scrollableElements.has(scrollElement) or
71 not scrollElement.parentNode
72 scrollElement = scrollElement.parentNode
73
74 element =
75 # If no element is focused on the page, the active element is the topmost
76 # `<body>`, and blurring it is a no-op. If it is scrollable, it means that
77 # you can’t blur it in order to scroll `<html>`. Therefore it may only be
78 # scrolled if it has been explicitly focused.
79 if vim.state.scrollableElements.has(scrollElement) and
80 (scrollElement != vim.content.document.body or
81 vim.state.explicitBodyFocus)
82 scrollElement
83 else
84 vim.state.scrollableElements.filterSuitableDefault()
85 viewportUtils.scroll(element, args)
86
87 commands.mark_scroll_position = (args) ->
88 {vim, keyStr, notify = true, addToJumpList = false} = args
89
90 vim.state.marks[keyStr] = vim.state.scrollableElements.getPageScrollPosition()
91
92 if addToJumpList
93 vim.addToJumpList()
94
95 if notify
96 vim.notify(translate('notification.mark_scroll_position.success', keyStr))
97
98 commands.scroll_to_mark = (args) ->
99 {vim, extra: {keyStr, lastPositionMark}} = args
100
101 unless keyStr of vim.state.marks
102 vim.notify(translate('notification.scroll_to_mark.none', keyStr))
103 return
104
105 args.amounts = vim.state.marks[keyStr]
106 element = vim.state.scrollableElements.filterSuitableDefault()
107
108 commands.mark_scroll_position({
109 vim
110 keyStr: lastPositionMark
111 notify: false
112 addToJumpList: true
113 })
114 viewportUtils.scroll(element, args)
115
116 commands.scroll_to_position = (args) ->
117 {vim, extra: {count, direction, lastPositionMark}} = args
118
119 if direction == 'previous' and vim.state.jumpListIndex >= 0 and
120 vim.state.jumpListIndex == vim.state.jumpList.length - 1
121 vim.addToJumpList()
122
123 {jumpList, jumpListIndex} = vim.state
124 maxIndex = jumpList.length - 1
125
126 if (direction == 'previous' and jumpListIndex <= 0) or
127 (direction == 'next' and jumpListIndex >= maxIndex)
128 vim.notify(translate("notification.scroll_to_#{direction}_position.limit"))
129 return
130
131 index = jumpListIndex + count * (if direction == 'previous' then -1 else +1)
132 index = Math.max(index, 0)
133 index = Math.min(index, maxIndex)
134
135 args.amounts = jumpList[index]
136 element = vim.state.scrollableElements.filterSuitableDefault()
137
138 commands.mark_scroll_position({vim, keyStr: lastPositionMark, notify: false})
139 viewportUtils.scroll(element, args)
140 vim.state.jumpListIndex = index
141
142 helper_follow = (options, matcher, {vim, pass}) ->
143 {id, combine = true, selectors = FOLLOW_DEFAULT_SELECTORS} = options
144 if vim.content.document instanceof XULDocument
145 selectors = ['*']
146
147 if pass == 'auto'
148 pass = if selectors.length == 2 then 'first' else 'single'
149
150 vim.state.markerElements = [] if pass in ['single', 'first']
151 hrefs = {}
152
153 filter = (element, getElementShape) ->
154 type = matcher({vim, element, getElementShape})
155 if vim.hintMatcher
156 type = vim.hintMatcher(id, element, type)
157 if pass == 'complementary'
158 type = if type then null else 'complementary'
159 return unless type
160
161 shape = getElementShape(element)
162
163 unless shape.nonCoveredPoint then switch
164 # CodeMirror editor uses a tiny hidden textarea positioned at the caret.
165 # Targeting those are the only reliable way of focusing CodeMirror
166 # editors, and doing so without moving the caret.
167 when id == 'normal' and element.localName == 'textarea' and
168 element.ownerGlobal == vim.content
169 rect = element.getBoundingClientRect()
170 # Use `.clientWidth` instead of `rect.width` because the latter includes
171 # the width of the borders of the textarea, which are unreliable.
172 if element.clientWidth <= 1 and rect.height > 0
173 shape = {
174 nonCoveredPoint: {
175 x: rect.left
176 y: rect.top + rect.height / 2
177 offset: {left: 0, top: 0}
178 }
179 area: rect.width * rect.height
180 }
181
182 # Putting a large `<input type="file">` inside a smaller wrapper element
183 # with `overflow: hidden;` seems to be a common pattern, used both on
184 # addons.mozilla.org and <https://blueimp.github.io/jQuery-File-Upload/>.
185 when id in ['normal', 'focus'] and element.localName == 'input' and
186 element.type == 'file' and element.parentElement?
187 parentShape = getElementShape(element.parentElement)
188 shape = parentShape if parentShape.area <= shape.area
189
190 if not shape.nonCoveredPoint and pass == 'complementary'
191 shape = getElementShape(element, -1)
192
193 return unless shape.nonCoveredPoint
194
195 text = utils.getText(element)
196
197 originalRect = element.getBoundingClientRect()
198 length = vim.state.markerElements.push({element, originalRect})
199 wrapper = {
200 type, text, shape,
201 combinedArea: shape.area
202 elementIndex: length - 1
203 parentIndex: null
204 }
205
206 if wrapper.type == 'link'
207 {href} = element
208 wrapper.href = href
209
210 # Combine links with the same href.
211 if combine and wrapper.type == 'link' and
212 # If the element has an 'onclick' attribute we cannot be sure that all
213 # links with this href actually do the same thing. On some pages, such
214 # as startpage.com, actual proper links have the 'onclick' attribute,
215 # so we can’t exclude such links in `utils.isProperLink`.
216 not element.hasAttribute?('onclick') and
217 # GitHub’s diff expansion buttons are links with both `href` and
218 # `data-url`. They are JavaScript-powered using the latter attribute.
219 not element.hasAttribute?('data-url')
220 if href of hrefs
221 parent = hrefs[href]
222 wrapper.parentIndex = parent.elementIndex
223 parent.combinedArea += wrapper.shape.area
224 parent.numChildren += 1
225 else
226 hrefs[href] = wrapper
227
228 return wrapper
229
230 selector =
231 if pass == 'complementary'
232 '*'
233 else
234 selectors[if pass == 'second' then 1 else 0]
235 return {
236 wrappers: markableElements.find(vim.content, filter, selector)
237 viewport: viewportUtils.getWindowViewport(vim.content)
238 pass
239 }
240
241 commands.follow = helper_follow.bind(
242 null, {id: 'normal'},
243 ({vim, element, getElementShape}) ->
244 document = element.ownerDocument
245 isXUL = (document instanceof XULDocument)
246 type = null
247 switch
248 # Bootstrap. Match these before regular links, because especially slider
249 # “buttons” often get the same hint otherwise.
250 when element.hasAttribute?('data-toggle') or
251 element.hasAttribute?('data-dismiss') or
252 element.hasAttribute?('data-slide') or
253 element.hasAttribute?('data-slide-to')
254 type = 'clickable'
255 when isProperLink(element)
256 type = 'link'
257 when isTypingElement(element)
258 type = 'text'
259 when element.localName in ['a', 'button'] or
260 element.getAttribute?('role') in CLICKABLE_ARIA_ROLES or
261 # <http://www.w3.org/TR/wai-aria/states_and_properties>
262 element.hasAttribute?('aria-controls') or
263 element.hasAttribute?('aria-pressed') or
264 element.hasAttribute?('aria-checked') or
265 (element.hasAttribute?('aria-haspopup') and
266 element.getAttribute?('role') != 'menu')
267 type = 'clickable'
268 when utils.isFocusable(element) and
269 # Google Drive Documents. The hint for this element would cover the
270 # real hint that allows you to focus the document to start typing.
271 element.id != 'docs-editor'
272 type = 'clickable'
273 when element != vim.state.scrollableElements.largest and
274 vim.state.scrollableElements.has(element)
275 type = 'scrollable'
276 when element.hasAttribute?('onclick') or
277 element.hasAttribute?('onmousedown') or
278 element.hasAttribute?('onmouseup') or
279 element.hasAttribute?('oncommand') or
280 # Twitter.
281 element.classList?.contains('js-new-tweets-bar') or
282 element.hasAttribute?('data-permalink-path') or
283 # Feedly.
284 element.hasAttribute?('data-app-action') or
285 element.hasAttribute?('data-uri') or
286 element.hasAttribute?('data-page-action') or
287 # Google Drive Document.
288 element.classList?.contains('kix-appview-editor')
289 type = 'clickable'
290 # Facebook comment fields.
291 when element.parentElement?.classList?.contains('UFIInputContainer')
292 type = 'clickable-special'
293 # Putting markers on `<label>` elements is generally redundant, because
294 # its `<input>` gets one. However, some sites hide the actual `<input>`
295 # but keeps the `<label>` to click, either for styling purposes or to keep
296 # the `<input>` hidden until it is used. In those cases we should add a
297 # marker for the `<label>`. Trying to access `.control` on an element in
298 # `about:config` throws an error, so exclude XUL pages.
299 when not isXUL and element.localName == 'label' and element.control and
300 not getElementShape(element.control).nonCoveredPoint
301 type = 'clickable'
302 # Last resort checks for elements that might be clickable because of
303 # JavaScript.
304 when (not isXUL and
305 # It is common to listen for clicks on `<html>` or `<body>`. Don’t
306 # waste time on them.
307 element not in [document.documentElement, document.body]) and
308 (utils.includes(element.className, 'button') or
309 utils.includes(element.getAttribute?('aria-label'), 'close') or
310 # Do this last as it’s a potentially expensive check.
311 (utils.hasEventListeners(element, 'click') and
312 # Twitter. The hint for this element would cover the hint for
313 # showing more tweets.
314 not element.classList?.contains('js-new-items-bar-container')))
315 # Make a quick check for likely clickable descendants, to reduce the
316 # number of false positives. the element might be a “button-wrapper” or
317 # a large element with a click-tracking event listener.
318 unless element.querySelector?('a, button, input, [class*=button]')
319 type = 'clickable'
320 # When viewing an image it should get a marker to toggle zoom. This is the
321 # most unlikely rule to match, so keep it last.
322 when document.body?.childElementCount == 1 and
323 element.localName == 'img' and
324 (element.classList?.contains('overflowing') or
325 element.classList?.contains('shrinkToFit'))
326 type = 'clickable'
327 type = null if isXUL and element.classList?.contains('textbox-input')
328 return type
329 )
330
331 commands.follow_in_tab = helper_follow.bind(
332 null, {id: 'tab', selectors: ['a']},
333 ({element}) ->
334 type = if isProperLink(element) then 'link' else null
335 return type
336 )
337
338 commands.follow_copy = helper_follow.bind(
339 null, {id: 'copy'},
340 ({element}) ->
341 type = switch
342 when isProperLink(element)
343 'link'
344 when isContentEditable(element)
345 'contenteditable'
346 when isTypingElement(element)
347 'text'
348 else
349 null
350 return type
351 )
352
353 commands.follow_focus = helper_follow.bind(
354 null, {id: 'focus', combine: false},
355 ({vim, element}) ->
356 type = switch
357 when utils.isFocusable(element)
358 'focusable'
359 when element != vim.state.scrollableElements.largest and
360 vim.state.scrollableElements.has(element)
361 'scrollable'
362 else
363 null
364 return type
365 )
366
367 commands.follow_context = helper_follow.bind(
368 null, {id: 'context', selectors: FOLLOW_CONTEXT_SELECTORS},
369 ({element}) ->
370 type =
371 if element.localName in FOLLOW_CONTEXT_TAGS or
372 utils.hasMarkableTextNode(element)
373 'context'
374 else
375 null
376 return type
377 )
378
379 commands.follow_selectable = helper_follow.bind(
380 null, {id: 'selectable', selectors: FOLLOW_SELECTABLE_SELECTORS},
381 ({element}) ->
382 type =
383 if utils.hasMarkableTextNode(element)
384 'selectable'
385 else
386 null
387 return type
388 )
389
390 commands.focus_marker_element = ({vim, elementIndex, browserOffset, options}) ->
391 {element} = vim.state.markerElements[elementIndex]
392 # To be able to focus scrollable elements, `FLAG_BYKEY` _has_ to be used.
393 options.flag = 'FLAG_BYKEY' if vim.state.scrollableElements.has(element)
394 utils.focusElement(element, options)
395 vim.clearHover()
396 vim.setHover(element, browserOffset)
397
398 commands.click_marker_element = (
399 {vim, elementIndex, type, browserOffset, preventTargetBlank}
400 ) ->
401 {element} = vim.state.markerElements[elementIndex]
402 if element.target == '_blank' and preventTargetBlank
403 targetReset = element.target
404 element.target = ''
405 if type == 'clickable-special'
406 element.click()
407 else
408 isXUL = (element.ownerDocument instanceof XULDocument)
409 sequence = switch
410 when isXUL
411 if element.localName == 'tab' then ['mousedown'] else 'click-xul'
412 when type == 'context'
413 'context'
414 else
415 'click'
416 utils.simulateMouseEvents(element, sequence, browserOffset)
417 utils.openDropdown(element)
418 element.target = targetReset if targetReset
419
420 commands.copy_marker_element = ({vim, elementIndex, property}) ->
421 {element} = vim.state.markerElements[elementIndex]
422 utils.writeToClipboard(element[property])
423
424 commands.element_text_select = ({vim, elementIndex, full, scroll = false}) ->
425 {element} = vim.state.markerElements[elementIndex]
426 window = element.ownerGlobal
427 selection = window.getSelection()
428 range = window.document.createRange()
429
430 # Try to scroll the element into view, but keep the caret visible.
431 if scroll
432 viewport = viewportUtils.getWindowViewport(window)
433 rect = element.getBoundingClientRect()
434 block = switch
435 when rect.bottom > viewport.bottom
436 'end'
437 when rect.top < viewport.top and rect.height < viewport.height
438 'start'
439 else
440 null
441 if block
442 smooth = (
443 prefs.root.get('general.smoothScroll') and
444 prefs.root.get('general.smoothScroll.other')
445 )
446 element.scrollIntoView({
447 block
448 behavior: if smooth then 'smooth' else 'instant'
449 })
450
451 if full
452 range.selectNodeContents(element)
453 else
454 result = viewportUtils.getFirstNonWhitespace(element)
455 if result
456 [node, offset] = result
457 range.setStart(node, offset)
458 range.setEnd(node, offset)
459 else
460 range.setStartBefore(element)
461 range.setEndBefore(element)
462
463 utils.clearSelectionDeep(vim.content)
464
465 # Focus the window so that the selection does not appear greyed out. However,
466 # if a text input was previously focused in that window (frame), that will
467 # cause the text input to be re-focused, so make sure to blur the active
468 # element, so that the caret does not end up there.
469 window.focus()
470 window.document.activeElement?.blur?()
471
472 selection.addRange(range)
473
474 if full
475 # Force the selected text to appear in the “selection clipboard”. Note:
476 # `selection.modify` would not make any difference here. That makes sense,
477 # since web pages that programmatically change the selection shouldn’t
478 # affect the user’s clipboard. `SelectionManager` uses an internal Firefox
479 # API with clipboard privileges.
480 selectionManager = new SelectionManager(window)
481 error = selectionManager.moveCaret('characterMove', BACKWARD)
482 selectionManager.moveCaret('characterMove', FORWARD) unless error
483
484 commands.follow_pattern = ({vim, type, browserOffset, options}) ->
485 {document} = vim.content
486
487 # If there’s a `<link rel=prev/next>` element we use that.
488 for link in document.head?.getElementsByTagName('link')
489 # Also support `rel=previous`, just like Google.
490 if type == link.rel.toLowerCase().replace(/^previous$/, 'prev')
491 vim.content.location.href = link.href
492 return
493
494 # Otherwise we look for a link or button on the page that seems to go to the
495 # previous or next page.
496 candidates = document.querySelectorAll(options.pattern_selector)
497
498 # Note: Earlier patterns should be favored.
499 {patterns} = options
500
501 # Search for the prev/next patterns in the following attributes of the
502 # element. `rel` should be kept as the first attribute, since the standard way
503 # of marking up prev/next links (`rel="prev"` and `rel="next"`) should be
504 # favored. Even though some of these attributes only allow a fixed set of
505 # keywords, we pattern-match them anyways since lots of sites don’t follow the
506 # spec and use the attributes arbitrarily.
507 attrs = options.pattern_attrs
508
509 matchingLink = do ->
510 # Special-case for Google searches.
511 googleLink = document.getElementById("pn#{type}")
512 return googleLink if googleLink
513
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, blur}) ->
690 utils.clearSelectionDeep(vim.content, {blur})
691
692 commands.modal = ({vim, type, args}) ->
693 return vim.content[type](args...)
694
695 module.exports = commands
Imprint / Impressum