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