]> git.gir.st - VimFx.git/blob - extension/lib/commands-frame.coffee
Fix TypeError when hitting ESC
[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 if utils.isXULDocument(vim.content.document)
143 selectors = ['*']
144
145 if pass == 'auto'
146 pass = if selectors.length == 2 then 'first' else 'single'
147
148 vim.state.markerElements = [] if pass in ['single', 'first']
149 hrefs = {}
150
151 filter = (element, getElementShape) ->
152 type = matcher({vim, element, getElementShape})
153 if vim.hintMatcher
154 type = vim.hintMatcher(id, element, type)
155 if pass == 'complementary'
156 type = if type then null else 'complementary'
157 return unless type
158
159 shape = getElementShape(element)
160
161 unless shape.nonCoveredPoint then switch
162 # CodeMirror editor uses a tiny hidden textarea positioned at the caret.
163 # Targeting those are the only reliable way of focusing CodeMirror
164 # editors, and doing so without moving the caret.
165 when id == 'normal' and element.localName == 'textarea' and
166 element.ownerGlobal == vim.content
167 rect = element.getBoundingClientRect()
168 # Use `.clientWidth` instead of `rect.width` because the latter includes
169 # the width of the borders of the textarea, which are unreliable.
170 if element.clientWidth <= 1 and rect.height > 0
171 shape = {
172 nonCoveredPoint: {
173 x: rect.left
174 y: rect.top + rect.height / 2
175 offset: {left: 0, top: 0}
176 }
177 area: rect.width * rect.height
178 }
179
180 # Putting a large `<input type="file">` inside a smaller wrapper element
181 # with `overflow: hidden;` seems to be a common pattern, used both on
182 # addons.mozilla.org and <https://blueimp.github.io/jQuery-File-Upload/>.
183 when id in ['normal', 'focus'] and element.localName == 'input' and
184 element.type == 'file' and element.parentElement?
185 parentShape = getElementShape(element.parentElement)
186 shape = parentShape if parentShape.area <= shape.area
187
188 if not shape.nonCoveredPoint and pass == 'complementary'
189 shape = getElementShape(element, -1)
190
191 return unless shape.nonCoveredPoint
192
193 text = utils.getText(element)
194
195 originalRect = element.getBoundingClientRect()
196 length = vim.state.markerElements.push({element, originalRect})
197 wrapper = {
198 type, text, shape,
199 combinedArea: shape.area
200 elementIndex: length - 1
201 parentIndex: null
202 }
203
204 if wrapper.type == 'link'
205 {href} = element
206 wrapper.href = href
207
208 # Combine links with the same href.
209 if combine and wrapper.type == 'link' and
210 # If the element has an 'onclick' attribute we cannot be sure that all
211 # links with this href actually do the same thing. On some pages, such
212 # as startpage.com, actual proper links have the 'onclick' attribute,
213 # so we can’t exclude such links in `utils.isProperLink`.
214 not element.hasAttribute?('onclick') and
215 # GitHub’s diff expansion buttons are links with both `href` and
216 # `data-url`. They are JavaScript-powered using the latter attribute.
217 not element.hasAttribute?('data-url')
218 if href of hrefs
219 parent = hrefs[href]
220 wrapper.parentIndex = parent.elementIndex
221 parent.combinedArea += wrapper.shape.area
222 parent.numChildren += 1
223 else
224 hrefs[href] = wrapper
225
226 return wrapper
227
228 selector =
229 if pass == 'complementary'
230 '*'
231 else
232 selectors[if pass == 'second' then 1 else 0]
233 return {
234 wrappers: markableElements.find(vim.content, filter, selector)
235 viewport: viewportUtils.getWindowViewport(vim.content)
236 pass
237 }
238
239 commands.follow = helper_follow.bind(
240 null, {id: 'normal'},
241 ({vim, element, getElementShape}) ->
242 document = element.ownerDocument
243 isXUL = utils.isXULDocument(document)
244 type = null
245 switch
246 # Bootstrap. Match these before regular links, because especially slider
247 # “buttons” often get the same hint otherwise.
248 when element.hasAttribute?('data-toggle') or
249 element.hasAttribute?('data-dismiss') or
250 element.hasAttribute?('data-slide') or
251 element.hasAttribute?('data-slide-to')
252 type = 'clickable'
253 when isProperLink(element)
254 type = 'link'
255 when isTypingElement(element)
256 type = 'text'
257 when element.localName in ['a', 'button'] or
258 element.getAttribute?('role') in CLICKABLE_ARIA_ROLES or
259 # <http://www.w3.org/TR/wai-aria/states_and_properties>
260 element.hasAttribute?('aria-controls') or
261 element.hasAttribute?('aria-pressed') or
262 element.hasAttribute?('aria-checked') or
263 (element.hasAttribute?('aria-haspopup') and
264 element.getAttribute?('role') != 'menu')
265 type = 'clickable'
266 when utils.isFocusable(element) and
267 # Google Drive Documents. The hint for this element would cover the
268 # real hint that allows you to focus the document to start typing.
269 element.id != 'docs-editor'
270 type = 'clickable'
271 when element != vim.state.scrollableElements.largest and
272 vim.state.scrollableElements.has(element)
273 type = 'scrollable'
274 when element.hasAttribute?('onclick') or
275 element.hasAttribute?('onmousedown') or
276 element.hasAttribute?('onmouseup') or
277 element.hasAttribute?('oncommand') or
278 # Twitter.
279 element.classList?.contains('js-new-tweets-bar') or
280 element.hasAttribute?('data-permalink-path') or
281 # Feedly.
282 element.hasAttribute?('data-app-action') or
283 element.hasAttribute?('data-uri') or
284 element.hasAttribute?('data-page-action') or
285 # Google Drive Document.
286 element.classList?.contains('kix-appview-editor')
287 type = 'clickable'
288 # Facebook comment fields.
289 when element.parentElement?.classList?.contains('UFIInputContainer')
290 type = 'clickable-special'
291 # Putting markers on `<label>` elements is generally redundant, because
292 # its `<input>` gets one. However, some sites hide the actual `<input>`
293 # but keeps the `<label>` to click, either for styling purposes or to keep
294 # the `<input>` hidden until it is used. In those cases we should add a
295 # marker for the `<label>`. Trying to access `.control` on an element in
296 # `about:config` throws an error, so exclude XUL pages.
297 when not isXUL and element.localName == 'label' and element.control and
298 not getElementShape(element.control).nonCoveredPoint
299 type = 'clickable'
300 # Last resort checks for elements that might be clickable because of
301 # JavaScript.
302 when (not isXUL and
303 # It is common to listen for clicks on `<html>` or `<body>`. Don’t
304 # waste time on them.
305 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 type = null if isXUL and element.classList?.contains('textbox-input')
326 return type
327 )
328
329 commands.follow_in_tab = helper_follow.bind(
330 null, {id: 'tab', selectors: ['a']},
331 ({element}) ->
332 type = if isProperLink(element) then 'link' else null
333 return type
334 )
335
336 commands.follow_copy = helper_follow.bind(
337 null, {id: 'copy'},
338 ({element}) ->
339 type = switch
340 when isProperLink(element)
341 'link'
342 when isContentEditable(element)
343 'contenteditable'
344 when isTypingElement(element)
345 'text'
346 else
347 null
348 return type
349 )
350
351 commands.follow_focus = helper_follow.bind(
352 null, {id: 'focus', combine: false},
353 ({vim, element}) ->
354 type = switch
355 when utils.isFocusable(element)
356 'focusable'
357 when element != vim.state.scrollableElements.largest and
358 vim.state.scrollableElements.has(element)
359 'scrollable'
360 else
361 null
362 return type
363 )
364
365 commands.follow_context = helper_follow.bind(
366 null, {id: 'context', selectors: FOLLOW_CONTEXT_SELECTORS},
367 ({element}) ->
368 type =
369 if element.localName in FOLLOW_CONTEXT_TAGS or
370 utils.hasMarkableTextNode(element)
371 'context'
372 else
373 null
374 return type
375 )
376
377 commands.follow_selectable = helper_follow.bind(
378 null, {id: 'selectable', selectors: FOLLOW_SELECTABLE_SELECTORS},
379 ({element}) ->
380 type =
381 if utils.hasMarkableTextNode(element)
382 'selectable'
383 else
384 null
385 return type
386 )
387
388 commands.focus_marker_element = ({vim, elementIndex, browserOffset, options}) ->
389 {element} = vim.state.markerElements[elementIndex]
390 # To be able to focus scrollable elements, `FLAG_BYKEY` _has_ to be used.
391 options.flag = 'FLAG_BYKEY' if vim.state.scrollableElements.has(element)
392 utils.focusElement(element, options)
393 vim.clearHover()
394 vim.setHover(element, browserOffset)
395
396 commands.click_marker_element = (
397 {vim, elementIndex, type, browserOffset, preventTargetBlank}
398 ) ->
399 {element} = vim.state.markerElements[elementIndex]
400 if element.target == '_blank' and preventTargetBlank
401 targetReset = element.target
402 element.target = ''
403 if type == 'clickable-special'
404 element.click()
405 else
406 isXUL = utils.isXULDocument(element.ownerDocument)
407 sequence = switch
408 when isXUL
409 if element.localName == 'tab' then ['mousedown'] else 'click-xul'
410 when type == 'context'
411 'context'
412 else
413 'click'
414 utils.simulateMouseEvents(element, sequence, browserOffset)
415 utils.openDropdown(element)
416 element.target = targetReset if targetReset
417
418 commands.copy_marker_element = ({vim, elementIndex, property}) ->
419 {element} = vim.state.markerElements[elementIndex]
420 utils.writeToClipboard(element[property])
421
422 commands.element_text_select = ({vim, elementIndex, full, scroll = false}) ->
423 {element} = vim.state.markerElements[elementIndex]
424 window = element.ownerGlobal
425 selection = window.getSelection()
426 range = window.document.createRange()
427
428 # Try to scroll the element into view, but keep the caret visible.
429 if scroll
430 viewport = viewportUtils.getWindowViewport(window)
431 rect = element.getBoundingClientRect()
432 block = switch
433 when rect.bottom > viewport.bottom
434 'end'
435 when rect.top < viewport.top and rect.height < viewport.height
436 'start'
437 else
438 null
439 if block
440 smooth = (
441 prefs.root.get('general.smoothScroll') and
442 prefs.root.get('general.smoothScroll.other')
443 )
444 element.scrollIntoView({
445 block
446 behavior: if smooth then 'smooth' else 'instant'
447 })
448
449 if full
450 range.selectNodeContents(element)
451 else
452 result = viewportUtils.getFirstNonWhitespace(element)
453 if result
454 [node, offset] = result
455 range.setStart(node, offset)
456 range.setEnd(node, offset)
457 else
458 range.setStartBefore(element)
459 range.setEndBefore(element)
460
461 utils.clearSelectionDeep(vim.content)
462
463 # Focus the window so that the selection does not appear greyed out. However,
464 # if a text input was previously focused in that window (frame), that will
465 # cause the text input to be re-focused, so make sure to blur the active
466 # element, so that the caret does not end up there.
467 window.focus()
468 window.document.activeElement?.blur?()
469
470 selection.addRange(range)
471
472 if full
473 # Force the selected text to appear in the “selection clipboard”. Note:
474 # `selection.modify` would not make any difference here. That makes sense,
475 # since web pages that programmatically change the selection shouldn’t
476 # affect the user’s clipboard. `SelectionManager` uses an internal Firefox
477 # API with clipboard privileges.
478 selectionManager = new SelectionManager(window)
479 error = selectionManager.moveCaret('characterMove', BACKWARD)
480 selectionManager.moveCaret('characterMove', FORWARD) unless error
481
482 commands.follow_pattern = ({vim, type, browserOffset, options}) ->
483 {document} = vim.content
484
485 # If there’s a `<link rel=prev/next>` element we use that.
486 for link in document.head?.getElementsByTagName('link')
487 # Also support `rel=previous`, just like Google.
488 if type == link.rel.toLowerCase().replace(/^previous$/, 'prev')
489 vim.content.location.href = link.href
490 return
491
492 # Otherwise we look for a link or button on the page that seems to go to the
493 # previous or next page.
494 candidates = document.querySelectorAll(options.pattern_selector)
495
496 # Note: Earlier patterns should be favored.
497 {patterns} = options
498
499 # Search for the prev/next patterns in the following attributes of the
500 # element. `rel` should be kept as the first attribute, since the standard way
501 # of marking up prev/next links (`rel="prev"` and `rel="next"`) should be
502 # favored. Even though some of these attributes only allow a fixed set of
503 # keywords, we pattern-match them anyways since lots of sites don’t follow the
504 # spec and use the attributes arbitrarily.
505 attrs = options.pattern_attrs
506
507 matchingLink = do ->
508 # Special-case for Google searches.
509 googleLink = document.getElementById("pn#{type}")
510 return googleLink if googleLink
511
512 # First search in attributes (favoring earlier attributes) as it's likely
513 # that they are more specific than text contexts.
514 for attr in attrs
515 for regex in patterns
516 for element in candidates
517 return element if regex.test(element.getAttribute?(attr))
518
519 # Then search in element contents.
520 for regex in patterns
521 for element in candidates
522 return element if regex.test(element.textContent)
523
524 return null
525
526 if matchingLink
527 utils.simulateMouseEvents(matchingLink, 'click', browserOffset)
528 # When you go to the next page of GitHub’s code search results, the page is
529 # loaded with AJAX. GitHub then annoyingly focuses its search input. This
530 # autofocus cannot be prevented in a reliable way, because the case is
531 # indistinguishable from a button whose job is to focus some text input.
532 # However, in this command we know for sure that we can prevent the next
533 # focus. This must be done _after_ the click has been triggered, since
534 # clicks count as page interactions.
535 vim.markPageInteraction(false)
536 else
537 vim.notify(translate("notification.follow_#{type}.none"))
538
539 commands.focus_text_input = ({vim, count = null}) ->
540 {lastFocusedTextInput} = vim.state
541
542 if lastFocusedTextInput and utils.isDetached(lastFocusedTextInput)
543 lastFocusedTextInput = null
544
545 candidates = utils.querySelectorAllDeep(
546 vim.content, 'input, textarea, textbox, [contenteditable]'
547 )
548 inputs = Array.prototype.filter.call(candidates, (element) ->
549 return isTextInputElement(element) and utils.area(element) > 0
550 )
551 if lastFocusedTextInput and lastFocusedTextInput not in inputs
552 inputs.push(lastFocusedTextInput)
553 inputs.sort((a, b) -> a.tabIndex - b.tabIndex)
554
555 if inputs.length == 0
556 vim.notify(translate('notification.focus_text_input.none'))
557 return
558
559 num = switch
560 when count?
561 count
562 when lastFocusedTextInput
563 inputs.indexOf(lastFocusedTextInput) + 1
564 else
565 1
566 index = Math.min(num, inputs.length) - 1
567 select = (count? or not vim.state.hasFocusedTextInput)
568 utils.focusElement(inputs[index], {select})
569 vim.state.inputs = inputs
570
571 commands.clear_inputs = ({vim}) ->
572 vim.state.inputs = null
573
574 commands.move_focus = ({vim, direction}) ->
575 return false unless vim.state.inputs
576 index = vim.state.inputs.indexOf(utils.getActiveElement(vim.content))
577 # If there’s only one input, `<tab>` would cycle to itself, making it feel
578 # like `<tab>` was not working. Then it’s better to let `<tab>` work as it
579 # usually does.
580 if index == -1 or vim.state.inputs.length <= 1
581 vim.state.inputs = null
582 return false
583 else
584 {inputs} = vim.state
585 nextInput = inputs[(index + direction) %% inputs.length]
586 utils.focusElement(nextInput, {select: true})
587 return true
588
589 # This is an attempt to enhance Firefox’s native “Find in page” functionality.
590 # Firefox starts searching after the end of the first selection range, or from
591 # the top of the page if there are no selection ranges. If there are frames, the
592 # top-most document in DOM order with selections seems to be used.
593 #
594 # Replace the current selection with one single range. (Searching clears the
595 # previous selection anyway.) That single range is either the first visible
596 # range, or a newly created (and collapsed) one at the top of the viewport. This
597 # way we can control where Firefox searches from.
598 commands.find_from_top_of_viewport = ({vim, direction}) ->
599 viewport = viewportUtils.getWindowViewport(vim.content)
600
601 range = viewportUtils.getFirstVisibleRange(vim.content, viewport)
602 if range
603 window = range.startContainer.ownerGlobal
604 selection = window.getSelection()
605 utils.clearSelectionDeep(vim.content)
606 window.focus()
607 # When the next match is in another frame than the current selection (A),
608 # Firefox won’t clear that selection before making a match selection (B) in
609 # the other frame. When searching again, selection B is cleared because
610 # selection A appears further up the viewport. This causes us to search
611 # _again_ from selection A, rather than selection B. In effect, we get stuck
612 # re-selecting selection B over and over. Therefore, collapse the range
613 # first, in case Firefox doesn’t.
614 range.collapse()
615 selection.addRange(range)
616 # Collapsing the range causes backwards search to keep re-selecting the same
617 # match. Therefore, move it one character back.
618 selection.modify('move', 'backward', 'character') if direction == BACKWARD
619 return
620
621 result = viewportUtils.getFirstVisibleText(vim.content, viewport)
622 return unless result
623 [textNode, offset] = result
624
625 utils.clearSelectionDeep(vim.content)
626 window = textNode.ownerGlobal
627 window.focus()
628 range = window.document.createRange()
629 range.setStart(textNode, offset)
630 range.setEnd(textNode, offset)
631 selection = window.getSelection()
632 selection.addRange(range)
633
634 commands.esc = (args) ->
635 {vim} = args
636 commands.blur_active_element(args)
637 vim.clearHover()
638 utils.clearSelectionDeep(vim.content)
639
640 {document} = vim.content
641
642 return unless document.fullscreenElement
643
644 if document.exitFullscreen
645 document.exitFullscreen()
646 else
647 document.mozCancelFullScreen()
648
649 commands.blur_active_element = ({vim}) ->
650 vim.state.explicitBodyFocus = false
651 utils.blurActiveElement(vim.content)
652
653 helper_create_selection_manager = (vim) ->
654 window = utils.getActiveElement(vim.content)?.ownerGlobal ? vim.content
655 return new SelectionManager(window)
656
657 commands.enable_caret = ({vim}) ->
658 return unless selectionManager = helper_create_selection_manager(vim)
659 selectionManager.enableCaret()
660
661 commands.move_caret = ({vim, method, direction, select, count}) ->
662 return unless selectionManager = helper_create_selection_manager(vim)
663 for [0...count] by 1
664 if selectionManager[method]
665 error = selectionManager[method](direction, select)
666 else
667 error = selectionManager.moveCaret(method, direction, select)
668 break if error
669 return
670
671 commands.toggle_selection = ({vim, select}) ->
672 return unless selectionManager = helper_create_selection_manager(vim)
673 if select
674 vim.notify(translate('notification.toggle_selection.enter'))
675 else
676 selectionManager.collapse()
677
678 commands.toggle_selection_direction = ({vim}) ->
679 return unless selectionManager = helper_create_selection_manager(vim)
680 selectionManager.reverseDirection()
681
682 commands.get_selection = ({vim}) ->
683 return unless selectionManager = helper_create_selection_manager(vim)
684 return selectionManager.selection.toString()
685
686 commands.collapse_selection = ({vim}) ->
687 return unless selectionManager = helper_create_selection_manager(vim)
688 selectionManager.collapse()
689
690 commands.clear_selection = ({vim, blur}) ->
691 utils.clearSelectionDeep(vim.content, {blur})
692
693 commands.modal = ({vim, type, args}) ->
694 return vim.content[type](args...)
695
696 module.exports = commands
Imprint / Impressum