2 # Copyright Anton Khodakivskiy 2012, 2013, 2014.
3 # Copyright Simon Lydell 2013, 2014.
4 # Copyright Wang Zhuochun 2013, 2014.
6 # This file is part of VimFx.
8 # VimFx is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # VimFx is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with VimFx. If not, see <http://www.gnu.org/licenses/>.
22 notation = require('vim-like-key-notation')
23 Command = require('./command')
24 { Marker } = require('./marker')
25 utils = require('./utils')
26 help = require('./help')
29 , withFirefoxPrefAs } = require('./prefs')
31 { isProperLink, isTextInputElement, isContentEditable } = utils
33 { classes: Cc, interfaces: Ci, utils: Cu } = Components
35 XULDocument = Ci.nsIDOMXULDocument
37 # “Selecting an element” means “focusing and selecting the text, if any, of an
40 # Select the Address Bar.
41 command_focus = (vim) ->
42 # This function works even if the Address Bar has been removed.
43 vim.rootWindow.focusAndSelectUrlBar()
45 # Select the Search Bar.
46 command_focus_search = (vim) ->
47 # The `.webSearch()` method opens a search engine in a tab if the Search Bar
48 # has been removed. Therefore we first check if it exists.
49 if vim.rootWindow.BrowserSearch.searchBar
50 vim.rootWindow.BrowserSearch.webSearch()
52 helper_paste = (vim) ->
53 url = vim.rootWindow.readFromClipboard()
55 if not utils.isURL(url) and submission = utils.browserSearchSubmission(url)
56 url = submission.uri.spec
57 { postData } = submission
58 return {url, postData}
60 # Go to or search for the contents of the system clipboard.
61 command_paste = (vim) ->
62 { url, postData } = helper_paste(vim)
63 vim.rootWindow.gBrowser.loadURIWithFlags(url, null, null, null, postData)
65 # Go to or search for the contents of the system clipboard in a new tab.
66 command_paste_tab = (vim) ->
67 { url, postData } = helper_paste(vim)
68 vim.rootWindow.gBrowser.selectedTab =
69 vim.rootWindow.gBrowser.addTab(url, null, null, postData, null, false)
71 # Copy the current URL to the system clipboard.
72 command_yank = (vim) ->
73 utils.writeToClipboard(vim.window.location.href)
75 # Reload the current tab, possibly from cache.
76 command_reload = (vim) ->
77 vim.rootWindow.BrowserReload()
79 # Reload the current tab, skipping cache.
80 command_reload_force = (vim) ->
81 vim.rootWindow.BrowserReloadSkipCache()
83 # Reload all tabs, possibly from cache.
84 command_reload_all = (vim) ->
85 vim.rootWindow.gBrowser.reloadAllTabs()
87 # Reload all tabs, skipping cache.
88 command_reload_all_force = (vim) ->
89 for tab in vim.rootWindow.gBrowser.visibleTabs
90 window = tab.linkedBrowser.contentWindow
91 window.location.reload(true)
93 # Stop loading the current tab.
94 command_stop = (vim) ->
97 # Stop loading all tabs.
98 command_stop_all = (vim) ->
99 for tab in vim.rootWindow.gBrowser.visibleTabs
100 window = tab.linkedBrowser.contentWindow
104 x: ['left', 'scrollLeftMax', 'clientWidth', 'horizontalScrollDistance', 5]
105 y: ['top', 'scrollTopMax', 'clientHeight', 'verticalScrollDistance', 20]
106 helper_scroll = (method, type, axis, amount, vim, event, count = 1) ->
107 frameDocument = event.target.ownerDocument
109 if vim.state.scrollableElements.has(event.target)
112 frameDocument.documentElement
114 [ direction, max, dimension, distance, lineAmount ] = axisMap[axis]
116 if method == 'scrollTo'
117 amount = Math.min(amount, element[max])
121 getFirefoxPref("toolkit.scrollbox.#{ distance }") * lineAmount
124 amount *= unit * count
127 options[direction] = amount
128 if getFirefoxPref('general.smoothScroll') and
129 getFirefoxPref("general.smoothScroll.#{ type }")
130 options.behavior = 'smooth'
133 'layout.css.scroll-behavior.spring-constant',
134 getPref("smoothScroll.#{ type }.spring-constant"),
136 element[method](options)
137 # When scrolling the whole page, the body sometimes needs to be scrolled
139 if element == frameDocument.documentElement
140 frameDocument.body?[method](options)
143 command_scroll_to_top =
144 helper_scroll.bind(undefined, 'scrollTo', 'other', 'y', 0)
145 command_scroll_to_bottom =
146 helper_scroll.bind(undefined, 'scrollTo', 'other', 'y', Infinity)
147 command_scroll_to_left =
148 helper_scroll.bind(undefined, 'scrollTo', 'other', 'x', 0)
149 command_scroll_to_right =
150 helper_scroll.bind(undefined, 'scrollTo', 'other', 'x', Infinity)
151 command_scroll_down =
152 helper_scroll.bind(undefined, 'scrollBy', 'lines', 'y', +1)
154 helper_scroll.bind(undefined, 'scrollBy', 'lines', 'y', -1)
155 command_scroll_right =
156 helper_scroll.bind(undefined, 'scrollBy', 'lines', 'x', +1)
157 command_scroll_left =
158 helper_scroll.bind(undefined, 'scrollBy', 'lines', 'x', -1)
159 command_scroll_half_page_down =
160 helper_scroll.bind(undefined, 'scrollBy', 'pages', 'y', +0.5)
161 command_scroll_half_page_up =
162 helper_scroll.bind(undefined, 'scrollBy', 'pages', 'y', -0.5)
163 command_scroll_page_down =
164 helper_scroll.bind(undefined, 'scrollBy', 'pages', 'y', +1)
165 command_scroll_page_up =
166 helper_scroll.bind(undefined, 'scrollBy', 'pages', 'y', -1)
168 # Open a new tab and select the Address Bar.
169 command_open_tab = (vim) ->
170 vim.rootWindow.BrowserOpenTab()
172 absoluteTabIndex = (relativeIndex, gBrowser) ->
173 tabs = gBrowser.visibleTabs
174 { selectedTab } = gBrowser
176 currentIndex = tabs.indexOf(selectedTab)
177 absoluteIndex = currentIndex + relativeIndex
178 numTabs = tabs.length
180 wrap = (Math.abs(relativeIndex) == 1)
182 absoluteIndex %%= numTabs
184 absoluteIndex = Math.max(0, absoluteIndex)
185 absoluteIndex = Math.min(absoluteIndex, numTabs - 1)
189 helper_switch_tab = (direction, vim, event, count = 1) ->
190 { gBrowser } = vim.rootWindow
191 gBrowser.selectTabAtIndex(absoluteTabIndex(direction * count, gBrowser))
193 # Switch to the previous tab.
194 command_tab_prev = helper_switch_tab.bind(undefined, -1)
196 # Switch to the next tab.
197 command_tab_next = helper_switch_tab.bind(undefined, +1)
199 helper_move_tab = (direction, vim, event, count = 1) ->
200 { gBrowser } = vim.rootWindow
201 { selectedTab } = gBrowser
202 { pinned } = selectedTab
204 index = absoluteTabIndex(direction * count, gBrowser)
206 if index < gBrowser._numPinnedTabs
207 gBrowser.pinTab(selectedTab) unless pinned
209 gBrowser.unpinTab(selectedTab) if pinned
211 gBrowser.moveTabTo(selectedTab, index)
213 # Move the current tab backward.
214 command_tab_move_left = helper_move_tab.bind(undefined, -1)
216 # Move the current tab forward.
217 command_tab_move_right = helper_move_tab.bind(undefined, +1)
219 # Load the home page.
220 command_home = (vim) ->
221 vim.rootWindow.BrowserHome()
223 # Switch to the first tab.
224 command_tab_first = (vim) ->
225 vim.rootWindow.gBrowser.selectTabAtIndex(0)
227 # Switch to the first non-pinned tab.
228 command_tab_first_non_pinned = (vim) ->
229 firstNonPinned = vim.rootWindow.gBrowser._numPinnedTabs
230 vim.rootWindow.gBrowser.selectTabAtIndex(firstNonPinned)
232 # Switch to the last tab.
233 command_tab_last = (vim) ->
234 vim.rootWindow.gBrowser.selectTabAtIndex(-1)
237 command_toggle_pin_tab = (vim) ->
238 currentTab = vim.rootWindow.gBrowser.selectedTab
241 vim.rootWindow.gBrowser.unpinTab(currentTab)
243 vim.rootWindow.gBrowser.pinTab(currentTab)
245 # Duplicate current tab.
246 command_duplicate_tab = (vim) ->
247 { gBrowser } = vim.rootWindow
248 gBrowser.duplicateTab(gBrowser.selectedTab)
250 # Close all tabs from current to the end.
251 command_close_tabs_to_end = (vim) ->
252 { gBrowser } = vim.rootWindow
253 gBrowser.removeTabsToTheEndFrom(gBrowser.selectedTab)
255 # Close all tabs except the current.
256 command_close_other_tabs = (vim) ->
257 { gBrowser } = vim.rootWindow
258 gBrowser.removeAllTabsBut(gBrowser.selectedTab)
261 command_close_tab = (vim, event, count = 1) ->
262 { gBrowser } = vim.rootWindow
263 return if gBrowser.selectedTab.pinned
264 currentIndex = gBrowser.visibleTabs.indexOf(gBrowser.selectedTab)
265 for tab in gBrowser.visibleTabs[currentIndex...(currentIndex + count)]
266 gBrowser.removeTab(tab)
268 # Restore last closed tab.
269 command_restore_tab = (vim, event, count = 1) ->
270 vim.rootWindow.undoCloseTab() for [1..count]
272 # Combine links with the same href.
273 combine = (hrefs, marker) ->
274 if marker.type == 'link'
275 { href } = marker.element
278 marker.parent = parent
279 parent.weight += marker.weight
285 # Follow links, focus text inputs and click buttons with hint markers.
286 command_follow = (vim, event, count = 1) ->
288 filter = (element, getElementShape) ->
289 document = element.ownerDocument
290 isXUL = (document instanceof XULDocument)
293 when isProperLink(element)
295 when isTextInputElement(element) or isContentEditable(element)
297 when element.tabIndex > -1 and
298 not (isXUL and element.nodeName.endsWith('box'))
300 unless isXUL or element.nodeName in ['A', 'INPUT', 'BUTTON']
302 when element != document.documentElement and
303 vim.state.scrollableElements.has(element)
305 when element.hasAttribute('onclick') or
306 element.hasAttribute('onmousedown') or
307 element.hasAttribute('onmouseup') or
308 element.hasAttribute('oncommand') or
309 element.getAttribute('role') in ['link', 'button'] or
310 # Twitter special-case.
311 element.classList.contains('js-new-tweets-bar') or
312 # Feedly special-case.
313 element.hasAttribute('data-app-action') or
314 element.hasAttribute('data-uri') or
315 element.hasAttribute('data-page-action')
318 # Putting markers on `<label>` elements is generally redundant, because
319 # its `<input>` gets one. However, some sites hide the actual `<input>`
320 # but keeps the `<label>` to click, either for styling purposes or to keep
321 # the `<input>` hidden until it is used. In those cases we should add a
322 # marker for the `<label>`.
323 when element.nodeName == 'LABEL'
325 input = document.getElementById(element.htmlFor)
326 if input and not getElementShape(input)
328 # Elements that have “button” somewhere in the class might be clickable,
329 # unless they contain a real link or button or yet an element with
330 # “button” somewhere in the class, in which case they likely are
331 # “button-wrapper”s. (`<SVG element>.className` is not a string!)
332 when not isXUL and typeof element.className == 'string' and
333 element.className.toLowerCase().contains('button')
334 unless element.querySelector('a, button, [class*=button]')
337 # When viewing an image it should get a marker to toggle zoom.
338 when document.body?.childElementCount == 1 and
339 element.nodeName == 'IMG' and
340 (element.classList.contains('overflowing') or
341 element.classList.contains('shrinkToFit'))
344 return unless shape = getElementShape(element)
345 return combine(hrefs, new Marker(element, shape, {semantic, type}))
347 callback = (marker) ->
351 if not last and marker.type == 'link'
352 utils.openTab(vim.rootWindow, element.href, {
354 relatedToCurrent: true
357 if element.target == '_blank'
358 targetReset = element.target
360 utils.simulateClick(element)
361 element.target = targetReset if targetReset
363 return (not last and marker.type != 'text')
365 vim.enterMode('hints', filter, callback)
367 # Like command_follow but multiple times.
368 command_follow_multiple = (vim, event) ->
369 command_follow(vim, event, Infinity)
371 # Follow links in a new background tab with hint markers.
372 command_follow_in_tab = (vim, event, count = 1, inBackground = true) ->
374 filter = (element, getElementShape) ->
375 return unless isProperLink(element)
376 return unless shape = getElementShape(element)
377 return combine(hrefs, new Marker(element, shape, {semantic: true}))
379 callback = (marker) ->
381 utils.openTab(vim.rootWindow, marker.element.href, {
382 inBackground: if last then inBackground else true
383 relatedToCurrent: true
388 vim.enterMode('hints', filter, callback)
390 # Follow links in a new foreground tab with hint markers.
391 command_follow_in_focused_tab = (vim, event, count = 1) ->
392 command_follow_in_tab(vim, event, count, false)
394 # Copy the URL or text of a markable element to the system clipboard.
395 command_marker_yank = (vim) ->
397 filter = (element, getElementShape) ->
399 when isProperLink(element) then 'link'
400 when isTextInputElement(element) then 'textInput'
401 when isContentEditable(element) then 'contenteditable'
403 return unless shape = getElementShape(element)
404 return combine(hrefs, new Marker(element, shape, {semantic: true, type}))
406 callback = (marker) ->
408 text = switch marker.type
409 when 'link' then element.href
410 when 'textInput' then element.value
411 when 'contenteditable' then element.textContent
412 utils.writeToClipboard(text)
414 vim.enterMode('hints', filter, callback)
416 # Focus element with hint markers.
417 command_marker_focus = (vim) ->
418 filter = (element, getElementShape) ->
420 when element.tabIndex > -1
422 when element != element.ownerDocument.documentElement and
423 vim.state.scrollableElements.has(element)
426 return unless shape = getElementShape(element)
427 return new Marker(element, shape, {semantic: true, type})
429 callback = (marker) ->
434 vim.enterMode('hints', filter, callback)
436 # Search for the prev/next patterns in the following attributes of the element.
437 # `rel` should be kept as the first attribute, since the standard way of marking
438 # up prev/next links (`rel="prev"` and `rel="next"`) should be favored. Even
439 # though some of these attributes only allow a fixed set of keywords, we
440 # pattern-match them anyways since lots of sites don’t follow the spec and use
441 # the attributes arbitrarily.
442 attrs = ['rel', 'role', 'data-tooltip', 'aria-label']
443 helper_follow_pattern = (type, vim) ->
444 { document } = vim.window
446 # If there’s a `<link rel=prev/next>` element we use that.
447 for link in document.head.getElementsByTagName('link')
448 # Also support `rel=previous`, just like Google.
449 if type == link.rel.toLowerCase().replace(/^previous$/, 'prev')
450 vim.rootWindow.gBrowser.loadURI(link.href)
453 # Otherwise we look for a link or button on the page that seems to go to the
454 # previous or next page.
455 candidates = document.querySelectorAll('a, button')
456 patterns = utils.splitListString(getPref("#{ type }_patterns"))
457 if matchingLink = utils.getBestPatternMatch(patterns, attrs, candidates)
458 utils.simulateClick(matchingLink)
460 # Follow previous page.
461 command_follow_prev = helper_follow_pattern.bind(undefined, 'prev')
464 command_follow_next = helper_follow_pattern.bind(undefined, 'next')
466 # Focus last focused or first text input and enter text input mode.
467 command_text_input = (vim, event, count) ->
469 { lastFocusedTextInput } = vim.state
470 inputs = Array.filter(
471 vim.window.document.querySelectorAll('input, textarea'), (element) ->
472 return utils.isTextInputElement(element) and utils.area(element) > 0
474 if lastFocusedTextInput and lastFocusedTextInput not in inputs
475 inputs.push(lastFocusedTextInput)
476 return unless inputs.length > 0
477 inputs.sort((a, b) -> a.tabIndex - b.tabIndex)
478 if count == null and lastFocusedTextInput
479 count = inputs.indexOf(lastFocusedTextInput) + 1
480 inputs[count - 1].select()
481 vim.enterMode('text-input', inputs)
483 # Go up one level in the URL hierarchy.
484 command_go_up_path = (vim, event, count = 1) ->
485 { pathname } = vim.window.location
486 vim.window.location.pathname = pathname.replace(
487 /// (?: /[^/]+ ){1,#{ count }} /?$ ///, ''
490 # Go up to root of the URL hierarchy.
491 command_go_to_root = (vim) ->
492 vim.window.location.href = vim.window.location.origin
494 helper_go_history = (num, vim, event, count = 1) ->
495 { index } = vim.rootWindow.getWebNavigation().sessionHistory
496 { history } = vim.window
498 num = Math.max(num, -index)
499 num = Math.min(num, history.length - 1 - index)
503 # Go back in history.
504 command_back = helper_go_history.bind(undefined, -1)
506 # Go forward in history.
507 command_forward = helper_go_history.bind(undefined, +1)
509 findStorage = {lastSearchString: ''}
511 helper_find = (highlight, vim) ->
512 findBar = vim.rootWindow.gBrowser.getFindBar()
514 findBar.onFindCommand()
515 findBar._findField.focus()
516 findBar._findField.select()
518 return unless highlightButton = findBar.getElement('highlight')
519 if highlightButton.checked != highlight
520 highlightButton.click()
522 # Open the find bar, making sure that hightlighting is off.
523 command_find = helper_find.bind(undefined, false)
525 # Open the find bar, making sure that hightlighting is on.
526 command_find_hl = helper_find.bind(undefined, true)
528 helper_find_again = (direction, vim) ->
529 findBar = vim.rootWindow.gBrowser.getFindBar()
530 if findStorage.lastSearchString.length > 0
531 findBar._findField.value = findStorage.lastSearchString
532 findBar.onFindAgainCommand(direction)
534 # Search for the last pattern.
535 command_find_next = helper_find_again.bind(undefined, false)
537 # Search for the last pattern backwards.
538 command_find_prev = helper_find_again.bind(undefined, true)
541 command_insert_mode = (vim) ->
542 vim.enterMode('insert')
544 # Quote next keypress (pass it through to the page).
545 command_quote = (vim, event, count = 1) ->
546 vim.enterMode('insert', count)
548 # Display the Help Dialog.
549 command_help = (vim) ->
550 help.injectHelp(vim.window.document, require('./modes'))
552 # Open and select the Developer Toolbar.
553 command_dev = (vim) ->
554 vim.rootWindow.DeveloperToolbar.show(true) # focus
556 command_Esc = (vim, event) ->
557 utils.blurActiveElement(vim.window)
559 # Blur active XUL control.
560 callback = -> event.originalTarget?.ownerDocument?.activeElement?.blur()
561 vim.window.setTimeout(callback, 0)
563 help.removeHelp(vim.window.document)
565 vim.rootWindow.DeveloperToolbar.hide()
567 vim.rootWindow.gBrowser.getFindBar().close()
569 vim.rootWindow.TabView.hide()
571 { document } = vim.window
572 if document.exitFullscreen
573 document.exitFullscreen()
575 document.mozCancelFullScreen()
578 # coffeelint: disable=max_line_length
580 new Command('urls', 'focus', command_focus, [['o']])
581 new Command('urls', 'focus_search', command_focus_search, [['O']])
582 new Command('urls', 'paste', command_paste, [['p']])
583 new Command('urls', 'paste_tab', command_paste_tab, [['P']])
584 new Command('urls', 'marker_yank', command_marker_yank, [['y', 'f']])
585 new Command('urls', 'marker_focus', command_marker_focus, [['v', 'f']])
586 new Command('urls', 'yank', command_yank, [['y', 'y']])
587 new Command('urls', 'reload', command_reload, [['r']])
588 new Command('urls', 'reload_force', command_reload_force, [['R']])
589 new Command('urls', 'reload_all', command_reload_all, [['a', 'r']])
590 new Command('urls', 'reload_all_force', command_reload_all_force, [['a', 'R']])
591 new Command('urls', 'stop', command_stop, [['s']])
592 new Command('urls', 'stop_all', command_stop_all, [['a', 's']])
594 new Command('nav', 'scroll_to_top', command_scroll_to_top , [['g', 'g']])
595 new Command('nav', 'scroll_to_bottom', command_scroll_to_bottom, [['G']])
596 new Command('nav', 'scroll_to_left', command_scroll_to_left , [['0'], ['^']])
597 new Command('nav', 'scroll_to_right', command_scroll_to_right, [['$']])
598 new Command('nav', 'scroll_down', command_scroll_down, [['j']])
599 new Command('nav', 'scroll_up', command_scroll_up, [['k']])
600 new Command('nav', 'scroll_left', command_scroll_left, [['h']])
601 new Command('nav', 'scroll_right', command_scroll_right , [['l']])
602 new Command('nav', 'scroll_half_page_down', command_scroll_half_page_down, [['d']])
603 new Command('nav', 'scroll_half_page_up', command_scroll_half_page_up, [['u']])
604 new Command('nav', 'scroll_page_down', command_scroll_page_down, [['<space>']])
605 new Command('nav', 'scroll_page_up', command_scroll_page_up, [['<s-space>']])
607 new Command('tabs', 'open_tab', command_open_tab, [['t']])
608 new Command('tabs', 'tab_prev', command_tab_prev, [['J'], ['g', 'T']])
609 new Command('tabs', 'tab_next', command_tab_next, [['K'], ['g', 't']])
610 new Command('tabs', 'tab_move_left', command_tab_move_left, [['g', 'J']])
611 new Command('tabs', 'tab_move_right', command_tab_move_right, [['g', 'K']])
612 new Command('tabs', 'home', command_home, [['g', 'h']])
613 new Command('tabs', 'tab_first', command_tab_first, [['g', 'H'], ['g', '0']])
614 new Command('tabs', 'tab_first_non_pinned', command_tab_first_non_pinned, [['g', '^']])
615 new Command('tabs', 'tab_last', command_tab_last, [['g', 'L'], ['g', '$']])
616 new Command('tabs', 'toggle_pin_tab', command_toggle_pin_tab, [['g', 'p']])
617 new Command('tabs', 'duplicate_tab', command_duplicate_tab, [['y', 't']])
618 new Command('tabs', 'close_tabs_to_end', command_close_tabs_to_end, [['g', 'x', '$']])
619 new Command('tabs', 'close_other_tabs', command_close_other_tabs, [['g', 'x', 'a']])
620 new Command('tabs', 'close_tab', command_close_tab, [['x']])
621 new Command('tabs', 'restore_tab', command_restore_tab, [['X']])
623 new Command('browse', 'follow', command_follow, [['f']])
624 new Command('browse', 'follow_in_tab', command_follow_in_tab, [['F']])
625 new Command('browse', 'follow_in_focused_tab', command_follow_in_focused_tab, [['g', 'f']])
626 new Command('browse', 'follow_multiple', command_follow_multiple, [['a', 'f']])
627 new Command('browse', 'follow_previous', command_follow_prev, [['[']])
628 new Command('browse', 'follow_next', command_follow_next, [[']']])
629 new Command('browse', 'text_input', command_text_input, [['g', 'i']])
630 new Command('browse', 'go_up_path', command_go_up_path, [['g', 'u']])
631 new Command('browse', 'go_to_root', command_go_to_root, [['g', 'U']])
632 new Command('browse', 'back', command_back, [['H']])
633 new Command('browse', 'forward', command_forward, [['L']])
635 new Command('misc', 'find', command_find, [['/']])
636 new Command('misc', 'find_hl', command_find_hl, [['a', '/']])
637 new Command('misc', 'find_next', command_find_next, [['n']])
638 new Command('misc', 'find_prev', command_find_prev, [['N']])
639 new Command('misc', 'insert_mode', command_insert_mode, [['i']])
640 new Command('misc', 'quote', command_quote, [['I']])
641 new Command('misc', 'help', command_help, [['?']])
642 new Command('misc', 'dev', command_dev, [[':']])
645 new Command('misc', 'Esc', command_Esc, [['<escape>']])
647 # coffeelint: enable=max_line_length
649 exports.commands = commands
650 exports.escapeCommand = escapeCommand
651 exports.findStorage = findStorage