]> git.gir.st - VimFx.git/blob - extension/lib/commands-frame.coffee
Remove last isXULDocument() references
[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')
262 type = 'clickable'
263 when utils.isFocusable(element) and
264 # Google Drive Documents. The hint for this element would cover the
265 # real hint that allows you to focus the document to start typing.
266 element.id != 'docs-editor'
267 type = 'clickable'
268 when element != vim.state.scrollableElements.largest and
269 vim.state.scrollableElements.has(element)
270 type = 'scrollable'
271 when element.hasAttribute?('onclick') or
272 element.hasAttribute?('onmousedown') or
273 element.hasAttribute?('onmouseup') or
274 element.hasAttribute?('oncommand') or
275 # Twitter.
276 element.classList?.contains('js-new-tweets-bar') or
277 element.hasAttribute?('data-permalink-path') or
278 # Feedly.
279 element.hasAttribute?('data-app-action') or
280 element.hasAttribute?('data-uri') or
281 element.hasAttribute?('data-page-action') or
282 # Google Drive Document.
283 element.classList?.contains('kix-appview-editor')
284 type = 'clickable'
285 # Facebook comment fields.
286 when element.parentElement?.classList?.contains('UFIInputContainer')
287 type = 'clickable-special'
288 # Putting markers on `<label>` elements is generally redundant, because
289 # its `<input>` gets one. However, some sites hide the actual `<input>`
290 # but keeps the `<label>` to click, either for styling purposes or to keep
291 # the `<input>` hidden until it is used. In those cases we should add a
292 # marker for the `<label>`. Trying to access `.control` on the XUL <label>
293 # element returns a string instead of an HTMLElement, so we don't do that.
294 when not utils.isXULElement(element) and
295 element.localName == 'label' and
296 element.control and
297 not getElementShape(element.control).nonCoveredPoint
298 type = 'clickable'
299 # Last resort checks for elements that might be clickable because of
300 # JavaScript.
301 # It is common to listen for clicks on `<html>` or `<body>`. Don’t
302 # waste time on them.
303 when element not in [document.documentElement, document.body] and
304 (utils.includes(element.className, 'button') or
305 utils.includes(element.getAttribute?('aria-label'), 'close') or
306 # Do this last as it’s a potentially expensive check.
307 (utils.hasEventListeners(element, 'click') and
308 # Twitter. The hint for this element would cover the hint for
309 # showing more tweets.
310 not element.classList?.contains('js-new-items-bar-container')))
311 # Make a quick check for likely clickable descendants, to reduce the
312 # number of false positives. the element might be a “button-wrapper” or
313 # a large element with a click-tracking event listener.
314 unless element.querySelector?('a, button, input, [class*=button]')
315 type = 'clickable'
316 # When viewing an image it should get a marker to toggle zoom. This is the
317 # most unlikely rule to match, so keep it last.
318 when document.body?.childElementCount == 1 and
319 element.localName == 'img' and
320 (element.classList?.contains('overflowing') or
321 element.classList?.contains('shrinkToFit'))
322 type = 'clickable'
323 return type
324 )
325
326 commands.follow_in_tab = helper_follow.bind(
327 null, {id: 'tab', selectors: ['a', 'label[is="text-link"]']},
328 ({element}) ->
329 type = if isProperLink(element) then 'link' else null
330 return type
331 )
332
333 commands.follow_copy = helper_follow.bind(
334 null, {id: 'copy'},
335 ({element}) ->
336 type = switch
337 when isProperLink(element)
338 'link'
339 when isContentEditable(element)
340 'contenteditable'
341 when isTypingElement(element)
342 'text'
343 else
344 null
345 return type
346 )
347
348 commands.follow_focus = helper_follow.bind(
349 null, {id: 'focus', combine: false},
350 ({vim, element}) ->
351 type = switch
352 when utils.isFocusable(element)
353 'focusable'
354 when element != vim.state.scrollableElements.largest and
355 vim.state.scrollableElements.has(element)
356 'scrollable'
357 else
358 null
359 return type
360 )
361
362 commands.follow_context = helper_follow.bind(
363 null, {id: 'context', selectors: FOLLOW_CONTEXT_SELECTORS},
364 ({element}) ->
365 type =
366 if element.localName in FOLLOW_CONTEXT_TAGS or
367 utils.hasMarkableTextNode(element)
368 'context'
369 else
370 null
371 return type
372 )
373
374 commands.follow_selectable = helper_follow.bind(
375 null, {id: 'selectable', selectors: FOLLOW_SELECTABLE_SELECTORS},
376 ({element}) ->
377 type =
378 if utils.hasMarkableTextNode(element)
379 'selectable'
380 else
381 null
382 return type
383 )
384
385 commands.focus_marker_element = ({vim, elementIndex, browserOffset, options}) ->
386 {element} = vim.state.markerElements[elementIndex]
387 # To be able to focus scrollable elements, `FLAG_BYKEY` _has_ to be used.
388 options.flag = 'FLAG_BYKEY' if vim.state.scrollableElements.has(element)
389 utils.focusElement(element, options)
390 vim.clearHover()
391 vim.setHover(element, browserOffset)
392
393 commands.click_marker_element = (
394 {vim, elementIndex, type, browserOffset, preventTargetBlank}
395 ) ->
396 {element} = vim.state.markerElements[elementIndex]
397 if element.target == '_blank' and preventTargetBlank
398 targetReset = element.target
399 element.target = ''
400 if type == 'clickable-special' or
401 type in ['clickable', 'link'] and utils.isInShadowRoot(element)
402 element.click()
403 else
404 sequence = switch
405 when utils.isXULElement(element)
406 if element.localName == 'tab' then ['mousedown'] else 'click-xul'
407 when type == 'context'
408 'context'
409 else
410 'click'
411 utils.simulateMouseEvents(element, sequence, browserOffset)
412 utils.openDropdown(element)
413 element.target = targetReset if targetReset
414
415 commands.copy_marker_element = ({vim, elementIndex, property}) ->
416 {element} = vim.state.markerElements[elementIndex]
417 utils.writeToClipboard(element[property])
418
419 commands.element_text_select = ({vim, elementIndex, full, scroll = false}) ->
420 {element} = vim.state.markerElements[elementIndex]
421 window = element.ownerGlobal
422 selection = window.getSelection()
423 range = window.document.createRange()
424
425 # Try to scroll the element into view, but keep the caret visible.
426 if scroll
427 viewport = viewportUtils.getWindowViewport(window)
428 rect = element.getBoundingClientRect()
429 block = switch
430 when rect.bottom > viewport.bottom
431 'end'
432 when rect.top < viewport.top and rect.height < viewport.height
433 'start'
434 else
435 null
436 if block
437 smooth = (
438 prefs.root.get('general.smoothScroll') and
439 prefs.root.get('general.smoothScroll.other')
440 )
441 element.scrollIntoView({
442 block
443 behavior: if smooth then 'smooth' else 'instant'
444 })
445
446 if full
447 range.selectNodeContents(element)
448 else
449 result = viewportUtils.getFirstNonWhitespace(element)
450 if result
451 [node, offset] = result
452 range.setStart(node, offset)
453 range.setEnd(node, offset)
454 else
455 range.setStartBefore(element)
456 range.setEndBefore(element)
457
458 utils.clearSelectionDeep(vim.content)
459
460 # Focus the window so that the selection does not appear greyed out. However,
461 # if a text input was previously focused in that window (frame), that will
462 # cause the text input to be re-focused, so make sure to blur the active
463 # element, so that the caret does not end up there.
464 window.focus()
465 utils.getActiveElement(window)?.blur?()
466
467 selection.addRange(range)
468
469 if full
470 # Force the selected text to appear in the “selection clipboard”. Note:
471 # `selection.modify` would not make any difference here. That makes sense,
472 # since web pages that programmatically change the selection shouldn’t
473 # affect the user’s clipboard. `SelectionManager` uses an internal Firefox
474 # API with clipboard privileges.
475 selectionManager = new SelectionManager(window)
476 error = selectionManager.moveCaret('characterMove', BACKWARD)
477 selectionManager.moveCaret('characterMove', FORWARD) unless error
478
479 commands.follow_pattern = ({vim, type, browserOffset, options}) ->
480 {document} = vim.content
481
482 # If there’s a `<link rel=prev/next>` element we use that.
483 for link in document.head?.getElementsByTagName('link')
484 # Also support `rel=previous`, just like Google.
485 if type == link.rel.toLowerCase().replace(/^previous$/, 'prev')
486 vim.content.location.href = link.href
487 return
488
489 # Otherwise we look for a link or button on the page that seems to go to the
490 # previous or next page.
491 candidates = document.querySelectorAll(options.pattern_selector)
492
493 # Note: Earlier patterns should be favored.
494 {patterns} = options
495
496 # Search for the prev/next patterns in the following attributes of the
497 # element. `rel` should be kept as the first attribute, since the standard way
498 # of marking up prev/next links (`rel="prev"` and `rel="next"`) should be
499 # favored. Even though some of these attributes only allow a fixed set of
500 # keywords, we pattern-match them anyways since lots of sites don’t follow the
501 # spec and use the attributes arbitrarily.
502 attrs = options.pattern_attrs
503
504 matchingLink = do ->
505 # Special-case for Google searches.
506 googleLink = document.getElementById("pn#{type}")
507 return googleLink if googleLink
508
509 # First search in attributes (favoring earlier attributes) as it's likely
510 # that they are more specific than text contexts.
511 for attr in attrs
512 for regex in patterns
513 for element in candidates
514 return element if regex.test(element.getAttribute?(attr))
515
516 # Then search in element contents.
517 for regex in patterns
518 for element in candidates
519 return element if regex.test(element.textContent)
520
521 return null
522
523 if matchingLink
524 utils.simulateMouseEvents(matchingLink, 'click', browserOffset)
525 # When you go to the next page of GitHub’s code search results, the page is
526 # loaded with AJAX. GitHub then annoyingly focuses its search input. This
527 # autofocus cannot be prevented in a reliable way, because the case is
528 # indistinguishable from a button whose job is to focus some text input.
529 # However, in this command we know for sure that we can prevent the next
530 # focus. This must be done _after_ the click has been triggered, since
531 # clicks count as page interactions.
532 vim.markPageInteraction(false)
533 else
534 vim.notify(translate("notification.follow_#{type}.none"))
535
536 commands.focus_text_input = ({vim, count = null}) ->
537 {lastFocusedTextInput} = vim.state
538
539 if lastFocusedTextInput and utils.isDetached(lastFocusedTextInput)
540 lastFocusedTextInput = null
541
542 candidates = utils.querySelectorAllDeep(
543 vim.content, 'input, textarea, textbox, [contenteditable]'
544 )
545 inputs = Array.prototype.filter.call(candidates, (element) ->
546 return isTextInputElement(element) and utils.area(element) > 0
547 )
548 if lastFocusedTextInput and lastFocusedTextInput not in inputs
549 inputs.push(lastFocusedTextInput)
550 inputs.sort((a, b) -> a.tabIndex - b.tabIndex)
551
552 if inputs.length == 0
553 vim.notify(translate('notification.focus_text_input.none'))
554 return
555
556 num = switch
557 when count?
558 count
559 when lastFocusedTextInput
560 inputs.indexOf(lastFocusedTextInput) + 1
561 else
562 1
563 index = Math.min(num, inputs.length) - 1
564 select = (count? or not vim.state.hasFocusedTextInput)
565 utils.focusElement(inputs[index], {select})
566 vim.state.inputs = inputs
567
568 commands.clear_inputs = ({vim}) ->
569 vim.state.inputs = null
570
571 commands.move_focus = ({vim, direction}) ->
572 return false unless vim.state.inputs
573 index = vim.state.inputs.indexOf(utils.getActiveElement(vim.content))
574 # If there’s only one input, `<tab>` would cycle to itself, making it feel
575 # like `<tab>` was not working. Then it’s better to let `<tab>` work as it
576 # usually does.
577 if index == -1 or vim.state.inputs.length <= 1
578 vim.state.inputs = null
579 return false
580 else
581 {inputs} = vim.state
582 nextInput = inputs[(index + direction) %% inputs.length]
583 utils.focusElement(nextInput, {select: true})
584 return true
585
586 # This is an attempt to enhance Firefox’s native “Find in page” functionality.
587 # Firefox starts searching after the end of the first selection range, or from
588 # the top of the page if there are no selection ranges. If there are frames, the
589 # top-most document in DOM order with selections seems to be used.
590 #
591 # Replace the current selection with one single range. (Searching clears the
592 # previous selection anyway.) That single range is either the first visible
593 # range, or a newly created (and collapsed) one at the top of the viewport. This
594 # way we can control where Firefox searches from.
595 commands.find_from_top_of_viewport = ({vim, direction}) ->
596 viewport = viewportUtils.getWindowViewport(vim.content)
597
598 range = viewportUtils.getFirstVisibleRange(vim.content, viewport)
599 if range
600 window = range.startContainer.ownerGlobal
601 selection = window.getSelection()
602 utils.clearSelectionDeep(vim.content)
603 window.focus()
604 # When the next match is in another frame than the current selection (A),
605 # Firefox won’t clear that selection before making a match selection (B) in
606 # the other frame. When searching again, selection B is cleared because
607 # selection A appears further up the viewport. This causes us to search
608 # _again_ from selection A, rather than selection B. In effect, we get stuck
609 # re-selecting selection B over and over. Therefore, collapse the range
610 # first, in case Firefox doesn’t.
611 range.collapse()
612 selection.addRange(range)
613 # Collapsing the range causes backwards search to keep re-selecting the same
614 # match. Therefore, move it one character back.
615 selection.modify('move', 'backward', 'character') if direction == BACKWARD
616 return
617
618 result = viewportUtils.getFirstVisibleText(vim.content, viewport)
619 return unless result
620 [textNode, offset] = result
621
622 utils.clearSelectionDeep(vim.content)
623 window = textNode.ownerGlobal
624 window.focus()
625 range = window.document.createRange()
626 range.setStart(textNode, offset)
627 range.setEnd(textNode, offset)
628 selection = window.getSelection()
629 selection.addRange(range)
630
631 commands.esc = (args) ->
632 {vim} = args
633 commands.blur_active_element(args)
634 vim.clearHover()
635 utils.clearSelectionDeep(vim.content)
636
637 {document} = vim.content
638
639 return unless document.fullscreenElement
640
641 if document.exitFullscreen
642 document.exitFullscreen()
643 else
644 document.mozCancelFullScreen()
645
646 commands.blur_active_element = ({vim}) ->
647 vim.state.explicitBodyFocus = false
648 utils.blurActiveElement(vim.content)
649
650 helper_create_selection_manager = (vim) ->
651 window = utils.getActiveElement(vim.content)?.ownerGlobal ? vim.content
652 return new SelectionManager(window)
653
654 commands.enable_caret = ({vim}) ->
655 return unless selectionManager = helper_create_selection_manager(vim)
656 selectionManager.enableCaret()
657
658 commands.move_caret = ({vim, method, direction, select, count}) ->
659 return unless selectionManager = helper_create_selection_manager(vim)
660 for [0...count] by 1
661 if selectionManager[method]
662 error = selectionManager[method](direction, select)
663 else
664 error = selectionManager.moveCaret(method, direction, select)
665 break if error
666 return
667
668 commands.toggle_selection = ({vim, select}) ->
669 return unless selectionManager = helper_create_selection_manager(vim)
670 if select
671 vim.notify(translate('notification.toggle_selection.enter'))
672 else
673 selectionManager.collapse()
674
675 commands.toggle_selection_direction = ({vim}) ->
676 return unless selectionManager = helper_create_selection_manager(vim)
677 selectionManager.reverseDirection()
678
679 commands.get_selection = ({vim}) ->
680 return unless selectionManager = helper_create_selection_manager(vim)
681 return selectionManager.selection.toString()
682
683 commands.collapse_selection = ({vim}) ->
684 return unless selectionManager = helper_create_selection_manager(vim)
685 selectionManager.collapse()
686
687 commands.clear_selection = ({vim, blur}) ->
688 utils.clearSelectionDeep(vim.content, {blur})
689
690 commands.modal = ({vim, type, args}) ->
691 return vim.content[type](args...)
692
693 module.exports = commands
Imprint / Impressum