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